<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anish Prakash</title>
    <description>The latest articles on DEV Community by Anish Prakash (@anish_prakash1).</description>
    <link>https://dev.to/anish_prakash1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4072430%2F3aec6781-e149-4b11-aeeb-1ab57700e54e.jpg</url>
      <title>DEV Community: Anish Prakash</title>
      <link>https://dev.to/anish_prakash1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anish_prakash1"/>
    <language>en</language>
    <item>
      <title>I Ported QOI to Rust. Here's What Almost Broke It.</title>
      <dc:creator>Anish Prakash</dc:creator>
      <pubDate>Tue, 11 Aug 2026 06:50:49 +0000</pubDate>
      <link>https://dev.to/anish_prakash1/i-ported-qoi-to-rust-heres-what-almost-broke-it-2g0</link>
      <guid>https://dev.to/anish_prakash1/i-ported-qoi-to-rust-heres-what-almost-broke-it-2g0</guid>
      <description>&lt;p&gt;The &lt;a href="https://github.com/phoboslab/qoi" rel="noopener noreferrer"&gt;PortMortem hackathon&lt;/a&gt; asked participants to choose a track and then within the track pick a library and port it — cleanly, correctly, and provably. I picked &lt;strong&gt;QOI&lt;/strong&gt; (the "Quite OK Image Format" by phoboslab), a fast lossless image codec in a single 649-line header file.&lt;/p&gt;

&lt;p&gt;QOI looked deceptively simple. It wasn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why QOI
&lt;/h2&gt;

&lt;p&gt;Most libraries have layers. QOI is a single header: &lt;code&gt;qoi.h&lt;/code&gt;. One encoder, one decoder, ~650 lines, no dependencies. That sounds easy to port.&lt;/p&gt;

&lt;p&gt;What it actually means is there's nowhere to hide. Every line has to be correct. Every edge case in the C has to be made explicit in Rust. That's the whole game.&lt;/p&gt;

&lt;p&gt;Final numbers: &lt;strong&gt;0 unsafe blocks, 0 core library dependencies, byte-for-byte identical output to the C reference, 22 integration tests, 27,966,810 fuzz iterations — zero divergences.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Decision That Mattered Most: Ditching the Union
&lt;/h2&gt;

&lt;p&gt;The central type in QOI's C source is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;qoi_rgba_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;v&lt;/code&gt; field is load-bearing. It lets the encoder check &lt;code&gt;px.v == px_prev.v&lt;/code&gt; — one 32-bit integer compare to detect if all four channels are unchanged. Clean, clever, and completely &lt;code&gt;unsafe&lt;/code&gt; in Rust if you try to replicate it literally.&lt;/p&gt;

&lt;p&gt;My translation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Clone,&lt;/span&gt; &lt;span class="nd"&gt;Copy,&lt;/span&gt; &lt;span class="nd"&gt;PartialEq,&lt;/span&gt; &lt;span class="nd"&gt;Eq,&lt;/span&gt; &lt;span class="nd"&gt;Default,&lt;/span&gt; &lt;span class="nd"&gt;Debug)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Pixel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;#[derive(PartialEq)]&lt;/code&gt; generates a four-field comparison. At &lt;code&gt;-O2&lt;/code&gt;, LLVM folds that into a single 32-bit integer compare — &lt;em&gt;the exact same machine code&lt;/em&gt; as &lt;code&gt;px.v == px_prev.v&lt;/code&gt;. The compiler does the work a human would otherwise do unsafely. Zero &lt;code&gt;unsafe&lt;/code&gt; needed.&lt;/p&gt;

&lt;p&gt;That was the cleanest decision in the whole port.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Edge Case That Will Eat You Alive
&lt;/h2&gt;

&lt;p&gt;The decoder maintains a 64-slot running index array of recently-seen pixels. When a new chunk is decoded, the index is updated. When a RUN chunk is processed (repeating the previous pixel N times), the index is &lt;em&gt;not&lt;/em&gt; updated.&lt;/p&gt;

&lt;p&gt;In C:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;chunks_len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// decode chunk...&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;QOI_COLOR_HASH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// ← INSIDE this block&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This seems obvious when you read it slowly. At 2am with a test that's almost passing, it's invisible. Placing that index update &lt;em&gt;outside&lt;/em&gt; the &lt;code&gt;else if&lt;/code&gt; — so it also runs during run-length repetitions — will produce output that looks correct on simple images and breaks on anything that mixes RUN and INDEX chunks.&lt;/p&gt;

&lt;p&gt;I got this right on the first pass only because I was reading the C spec annotation carefully. The subtlety is documented as Decision #7 in my DECISIONS.md, but naming it doesn't convey how easy it is to get wrong. This is the one I'd warn every QOI porter about.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Broke: The Fuzz Harness
&lt;/h2&gt;

&lt;p&gt;My plan was to use &lt;code&gt;cargo-fuzz&lt;/code&gt; with &lt;code&gt;libfuzzer-sys&lt;/code&gt; — the obvious Rust equivalent of the original &lt;code&gt;qoifuzz.c&lt;/code&gt;. The harness compiled fine. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STATUS_DLL_NOT_FOUND (exit code: 0xc0000135)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;libFuzzer on Windows (MINGW64) requires runtime DLLs — &lt;code&gt;vcruntime140.dll&lt;/code&gt; etc. — that simply aren't present in the Git Bash environment. &lt;code&gt;cargo-fuzz&lt;/code&gt; is effectively Linux-only outside of WSL or a full MSVC setup.&lt;/p&gt;

&lt;p&gt;I replaced it with a standalone binary fuzzer using an &lt;strong&gt;xorshift64 PRNG&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nf"&gt;Xorshift64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// seeded from SystemTime&lt;/span&gt;
&lt;span class="c1"&gt;// ~470,000 iterations/second&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fuzzer mirrors &lt;code&gt;qoifuzz.c&lt;/code&gt;'s invariants exactly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First 4 bytes select &lt;code&gt;channels&lt;/code&gt; (0, 3, or 4)&lt;/li&gt;
&lt;li&gt;Remaining bytes are the payload passed to &lt;code&gt;decode()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A successful decode must roundtrip: &lt;code&gt;decode → encode → decode → same pixels&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: &lt;strong&gt;27,966,810 decode iterations and 2,296,853 roundtrip iterations in 60 seconds — zero panics, zero divergences.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The tradeoff is real: PRNG fuzzing has no coverage-guided corpus evolution. What it has is throughput — roughly 12× more iterations per second than a typical libFuzzer run. For a 60-second run, that's not nothing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Arithmetic: The One You Can't Skip
&lt;/h2&gt;

&lt;p&gt;QOI's diff encoding does this in C:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;signed&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;vr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;px_prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;b1&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x03&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;C's unsigned char arithmetic wraps silently. Rust doesn't — in debug mode, it panics. In release mode it wraps, but invisibly.&lt;/p&gt;

&lt;p&gt;The fix isn't hard, but it has to be intentional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;vr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="py"&gt;.r&lt;/span&gt;&lt;span class="nf"&gt;.wrapping_sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;px_prev&lt;/span&gt;&lt;span class="py"&gt;.r&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="py"&gt;.r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="py"&gt;.r&lt;/span&gt;&lt;span class="nf"&gt;.wrapping_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dr&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Casting &lt;code&gt;i8(-2)&lt;/code&gt; to &lt;code&gt;u8&lt;/code&gt; gives &lt;code&gt;254&lt;/code&gt;. &lt;code&gt;wrapping_add(254u8)&lt;/code&gt; is subtracting 2 modulo 256. Same two's-complement behavior as C, made visible in the source. A future reader — or a security auditor — can verify the arithmetic without knowing C's implicit conversion rules.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Decision I'd Take Back
&lt;/h2&gt;

&lt;p&gt;The module split into &lt;code&gt;types.rs&lt;/code&gt;, &lt;code&gt;encode.rs&lt;/code&gt;, &lt;code&gt;decode.rs&lt;/code&gt;, &lt;code&gt;io.rs&lt;/code&gt; was the right call for readability. But I put all 22 integration tests in a single &lt;code&gt;tests/integration_test.rs&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;That file is long. It works, it's comprehensive, but it should have been split by module the same way the source is. &lt;code&gt;tests/encode_test.rs&lt;/code&gt;, &lt;code&gt;tests/decode_test.rs&lt;/code&gt;, &lt;code&gt;tests/roundtrip_test.rs&lt;/code&gt;. The test file ended up harder to navigate than any of the source files it was testing — which is exactly backwards.&lt;/p&gt;

&lt;p&gt;If I were starting over, test structure mirrors source structure, from day one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Behavioral Equivalence: How I Actually Proved It
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;22 integration tests&lt;/strong&gt; — all encoding paths (RUN, INDEX, DIFF, LUMA, RGB, RGBA chunks), RUN boundary conditions (63 vs 62 pixels), wrapping arithmetic on deliberate overflow inputs, channel override semantics, error paths.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Byte-for-byte diff against C reference&lt;/strong&gt; — compile &lt;code&gt;qoiconv.c&lt;/code&gt; with gcc, encode a corpus of PNGs with both, &lt;code&gt;diff&lt;/code&gt; the outputs. Every image passes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;27M+ fuzz iterations&lt;/strong&gt; — roundtrip invariant on pseudo-random byte streams. If the encoder and decoder disagree on anything, the harness finds it.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these alone is sufficient. The fuzz harness won't find a bug that only appears on your specific test image. The integration tests won't find a bug that only appears on random inputs. You need all three layers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Port:&lt;/strong&gt; &lt;a href="https://github.com/AnishPrakash/qoi-rust" rel="noopener noreferrer"&gt;github.com/AnishPrakash/qoi-rust&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original:&lt;/strong&gt; &lt;a href="https://github.com/phoboslab/qoi" rel="noopener noreferrer"&gt;github.com/phoboslab/qoi&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DECISIONS.md:&lt;/strong&gt; 15 architectural divergences, each with the C source, Rust translation, and why it matters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hackathon:&lt;/strong&gt; &lt;a href="https://coderesurrection.com/2026/" rel="noopener noreferrer"&gt;PortMortem&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag:&lt;/strong&gt; &lt;a href="https://dev.to/raptorsdev"&gt;@raptorsdev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hackathonraptors</category>
      <category>portmortem</category>
      <category>c</category>
      <category>rust</category>
    </item>
  </channel>
</rss>
