Gzip decompression in 250 lines of Rust

(iev.ee)

55 points | by vismit2000 3 days ago

6 comments

  • stgn 1 hour ago
    > so i wrote a gzip decompressor from scratch

    After skimming through the author's Rust code, it appears to be a fairly straightforward port of puff.c (included in the zlib source): https://github.com/madler/zlib/blob/develop/contrib/puff/puf...

    • dymk 18 minutes ago
      This feels like it should have been mentioned in the article.

      It makes me wonder if there was some LLM help, based on how similar the fn structure and identifier names are.

  • Lerc 25 minutes ago
    The function

      fn bits(&mut self, need: i32) -> i32 { ....
    
    Put me in mind of one of my early experiments in Rust. It would be interesting to compare a iterator based form that just called .take(need)

    I haven't written a lot of Rust, but one thing I did was to write an iterator that took an iterator of bytes as input and provided bits as output. Then used an iterator that gave bytes from a block of memory.

    It was mostly as a test to see how much high level abstraction left an imprint on the compiled code.

    The dissasembly showed it pulling in 32 bits at a time and shifting out the bits pretty much the same way I would have written in ASM.

    I was quite impressed. Although I tested it was working by counting the bits and someone critizised it for not using popcount, so I guess you can't have everything.

  • nayuki 2 hours ago
    Just like that author, many years ago, I went through the process of understanding the DEFLATE compression standard and producing a short and concise decompressor for gzip+DEFLATE. Here are the resources I published as a result of that exploration:

    * https://www.nayuki.io/page/deflate-specification-v1-3-html

    * https://www.nayuki.io/page/simple-deflate-decompressor

    * https://github.com/nayuki/Simple-DEFLATE-decompressor

  • MisterTea 2 hours ago
    > twenty five thousand lines of pure C not counting CMake files. ...

    Keep in mind this is also 31 years of cruft and lord knows what.

    Plan 9 gzip is 738 lines total:

      gzip.c 217 lines
      gzip.h 40 lines
      zip.c  398 lines
      zip.h  83 lines
    
    Even the zipfs file server that mounts zip files as file systems is 391 lines.

    edit - post a link to said code: https://github.com/9front/9front/tree/front/sys/src/cmd/gzip

    > ... (and whenever working with C always keep in mind that C stands for CVE).

    Sigh.

    • bboozzoo 1 hour ago
      You forgot to include https://github.com/9front/9front/tree/front/sys/src/libflate which gzip is built around, which brings it closer to 10k lines.
    • tyingq 2 hours ago
      His also omits CRC, which is part of the 25k lines, no --fast/--best/etc, missing some output formats, and so on. I'm sure the 25k includes a lot of bloat, but the comparison is odd. Comparing to your list would make much more sense.
      • kibwen 1 hour ago
        I would expect a CRC to add a negligible number of lines of code. The reason that production-grade decompressors are tens of thousands of LOC is likely attributable to extreme manual optimization. For example, I wouldn't be surprised if a measurable fraction of those lines are actually inline assembly.
        • nayuki 1 hour ago
          True. A most basic CRC implementation is about 7 lines of code: (presented in Java to avoid some C/C++ footguns)

              int crc32(byte[] data) {
                  int crc = ~0;
                  for (byte b : data) {
                      crc ^= b & 0xFF;
                      for (int i = 0; i < 8; i++)
                          crc = (crc >>> 1) ^ ((crc & 1) * 0xEDB88320);
                  }
                  return ~crc;
              }
          
          Or smooshed down slightly (with caveats):

              int crc32(byte[] data) {
                  int crc = ~0;
                  for (int i = 0; i < data.length * 8; i++) {
                      crc ^= (data[i / 8] >> (i % 8)) & 1;
                      crc = (crc >>> 1) ^ ((crc & 1) * 0xEDB88320);
                  }
                  return ~crc;
              }
          
          But one reason that many CRC implementations are large is because they include a pre-computed table of 256× 32-bit constants so that one byte can processed at a time. For example: https://github.com/madler/zlib/blob/7cdaaa09095e9266dee21314...
          • xxs 1 hour ago
            That's java code, though... bit weird, esp. i % 8 (which is just i & 7). The compiler should be able to optimize it since 'i' is guaranteed to be non-negative, still awkward.

            Java CRC32 nowadays uses intrinsics and avx128 for crc32.

        • ack_complete 1 hour ago
          Doesn't need to be inline assembly, just pre-encoded lookup tables and intrinsics-based vectorized CRC alone will add quite a lot of code. Most multi-platform CRC algorithms tend to have at least a few paths for byte/word/dword at a time, hardware CRC, and hardware GF(2) multiply. It's not really extreme optimization, just better algorithms to match better hardware capabilities.

          The Huffman decoding implementation is also bigger in production implementations for both speed and error checking. Two Huffman trees need to be exactly complete except in the special case of a single code, and in most cases they are flattened to two-level tables for speed (though the latest desktop CPUs have enough L1 cache to use single-level).

          Finally, the LZ copy typically has special cases added for using wider than byte copies for non-overlapping, non-wrapping runs. This is a significant decoding speed optimization.

        • tyingq 1 hour ago
          Yes, there's subdirs with language bindings for many non-C langs, an examples folder with example C code, win32 specific C code, test code, etc.

          More reasons it's an odd comparison.

      • fullstop 1 hour ago
        gzip also contains a significant amount of compatibility code for different platforms.
      • xxs 1 hour ago
        Crc32 can be written in handful lines of code. Although it'd be better to use the vector instruction set - e.g. AVX when available.
  • jeffrallen 2 hours ago
    But probably without any error checking.

    Feels like Rust culture inherited "throw and forget" as an error handling "strategy" from Java

    Sigh.

    • dmit 32 minutes ago
      Hehe, why "probably"? It says "250 lines" right there in the subject. Surely one can skim the single file of code (https://github.com/ieviev/mini-gzip/blob/main/src/main.rs) and offer criticism that isn't based on hypotheticals?

      Anyway, I skimmed the file for you this time, and basically you're either correct or wrong, depending on your definition of "error checking." The code handles error conditions by aborting the process. Seeing as it's a standalone CLI program and not a library meant for reuse, safely shutting down with a meaningful message sounds like fair game to me.

    • dymk 1 hour ago
      This is an educational project. Not something for production. The article even says so!

      You can leave the snide comments about “Rust culture” (whatever that is) out next time.

    • throwaway27448 1 hour ago
      Why people ascribe error handling practices to languages is baffling. What language doesn't allow punting error handling until later? Even Haskell has "panic" functionality that fudges the type constraints to allow this.
  • up2isomorphism 1 hour ago
    Another dev who doesn’t show respect to what has been done and expect a particular language will do wonders for him. Also I don’t see this is much better in term of readability.