Skip to content
fr0staman
All posts

Maintaining the dependency yourself

3 min read

My Telegram bot sends voice messages. Telegram wants them as Ogg Opus, so the bot decodes and re-encodes audio at 48 kHz mono. That is a solved problem with a crate for it — a small one, written by someone else, that had gone quiet.

I now maintain it. Not through any grand decision: I needed a fix, sent it, kept needing fixes.

Small dependencies are a different kind of risk

The usual worry about dependencies is size and supply chain. For a crate of about nine hundred lines, the real risk is simpler: it does exactly what you need, and then it stops moving. Not abandoned dramatically — just quiet. A Rust edition passes. A dependency you both share goes to 2.0. Your build starts warning, then breaking, over something the author has no reason to care about any more.

You have three options. Vendor it and diverge. Replace it with something larger. Or pick it up.

What picking it up actually involves

Mostly unglamorous work. Following thiserror to 2.0. Following rand through 0.9 and then 0.10. Moving to the 2024 edition and setting an honest MSRV. Adding the tests that would have caught the thing that made me open the file in the first place.

The interesting part was allocation behaviour. Decoding an Ogg Opus stream, the old code grew a Vec from empty, reallocating as it went — for a voice message that is a handful of reallocations and a lot of copying, on every message the bot sends. The stream tells you how long it is, via the granule position, so you can size the buffer once up front.

Which introduces a question the original code did not have to answer: what if the file lies? A granule position is just a number in a header. A corrupt file — or a hostile one, and this is a bot that accepts audio from strangers — can ask you to reserve something absurd before a single sample is decoded.

So the size hint is clamped. Above the ceiling it is ignored and the buffer grows the old way. A wrong hint then costs at most a reallocation; a malicious one costs nothing at all. Encoding got the same treatment, with capacity estimated from the payload plus an eighth and a small constant, so one allocation usually covers the whole stream.

What changed for me

The obvious thing is that the bug I needed fixed got fixed. The less obvious thing is that I stopped treating that layer as opaque. When the bot does something odd with an audio file now, the codec is not a black box I file an issue against and wait — it is code I have read, with tests I wrote.

That is worth more than the performance. Most of the time, "I could go look" is the difference between debugging a problem and working around it.

When not to do this

If the crate is large, or its domain is one where being wrong is dangerous and you are not an expert in it, adopting it is how you acquire a liability. Codec internals are a reasonable place to be careful — I am maintaining a thin Ogg container layer around a battle-tested Opus implementation, not writing an audio codec.

The rule I have settled on: adopt what you already understand and already depend on, where the alternative is waiting. Everything else, use and hope.

Share