deku

repository·master·Indexed 23 days ago

https://github.com/sharksforarms/deku

A declarative Rust crate for bit-level, symmetric binary serialization and deserialization of structs and enums. It uses proc-macros to automatically generate reader and writer functions via DekuRead and DekuWrite derives, allowing users to specify endianness, bit-level widths, and parsing limits through attributes. It supports no_std environments and provides tools for handling variable-length vectors, enum variant mapping, and detailed decoding errors.

Tokens
4.7K
Snippets
18
Records
33
Agent score
81%

What's inside deku

  1. What is Deku?

    master
    Deku is a Rust crate that provides declarative, bit-level, symmetric serialization and deserialization for structs and enums. It allows you to define binary formats using attributes, automatically generating the necessary reader and writer functions to avoid manual, error-prone parsing of network headers or binary structs.
  2. Install Deku for no_std environments

    master

    If you are working in a no_std environment, disable default features and enable the alloc feature instead.

    [dependencies]
    deku = { version = "0.20", default-features = false, features = ["alloc"] }
  3. Use Deku for declarative binary serialization

    master

    Deku provides bit-level, symmetric serialization and deserialization by deriving DekuRead and DekuWrite on structs or enums. This is ideal for binary structures like TLVs or network protocols, allowing you to define the data representation while Deku handles the parsing/writing logic. It supports both byte-aligned and bit-level control (using the bitvec crate when the bits feature is enabled).

    use deku::prelude::*;
    
    #[derive(Debug, PartialEq, DekuRead, DekuWrite)]
    #[deku(endian = "big")]
    struct DekuTest {
        #[deku(bits = 4)]
        field_a: u8,
        #[deku(bits = 4)]
        field_b: u8,
        field_c: u16,
    }
  4. Use the Reader for bit-level binary parsing

    master

    The Reader struct is used with from_reader_with_ctx to parse binary data that may not be byte-aligned. It wraps an underlying type R that implements Read + Seek.

    When parsing fields using the bits attribute, the Reader handles the complexity of reading partial bits and buffering leftover bits or bytes to ensure subsequent reads are correct. If a struct's bit-level fields do not end on a byte boundary, the Reader will store the remaining bits in its leftover field.

    To check for any unconsumed bits after a parsing operation, use the rest() method. This is particularly useful when your data format uses bit-fields that don't align to the next byte.

    use deku::prelude::*
    
    #[cfg(feature = "bits")]
    #[derive(Debug, PartialEq, DekuRead, DekuWrite)]
    #[deku(endian = "big")]
    struct DekuTest {
        #[deku(bits = 4)]
        field_a: u8,
        #[deku(bits = 2)]
        field_b: u8,
    }
    
    // ... setup cursor and reader ...
    let val = DekuTest::from_reader_with_ctx(&mut reader, ()).unwrap();
    
    // If field_a and field_b only used 6 bits, rest() returns the remaining 2 bits of that byte
    let unused_bits = reader.rest(); 
  5. Compose Deku structs using context (`ctx`)

    master

    When composing Deku types, child structs/enums may need access to configuration (like endianness) defined in the parent. Use the #[deku(ctx = "...")] attribute to pass this context down. If you specify endian, bit_order, or bits at the top level, you must pass that context to children to avoid type mismatches during derivation.

    #[derive(Debug, PartialEq, DekuRead, DekuWrite)]
    #[deku(endian = "big")]
    struct DekuTest {
        header: DekuHeader,
        data: DekuData,
    }
    
    #[derive(Debug, PartialEq, DekuRead, DekuWrite)]
    #[deku(ctx = "endian: deku::ctx::Endian")]
    struct DekuHeader(u8);
    
    #[derive(Debug, PartialEq, DekuRead, DekuWrite)]
    #[deku(ctx = "endian: deku::ctx::Endian")]
    struct DekuData(u16);
  6. Deserialize Enums with `id_type` and `id`

    master

    To deserialize enums, specify how the variant identifier is read using #[deku(id_type = "...")]. Then, map specific values to variants using #[deku(id = ...)]. If no id is provided, Deku defaults to the variant's discriminant. If no variant matches, Deku returns a DekuError::Parse unless a #[deku(default = "...")] variant is specified.

    #[derive(Debug, PartialEq, DekuRead, DekuWrite)]
    #[deku(id_type = "u8")]
    enum DekuTest {
        #[deku(id = 0x01)]
        VariantA,
        #[deku(id = 0x02)]
        VariantB(u16),
    }
  7. Handle variable-length vectors with `count` and `update`

    master

    You can use Vec<T> in Deku by specifying the number of elements to read using the #[deku(count = "...")] attribute. If the length of the Vec changes during runtime (e.g., via .push()), the field used for the count will not automatically update. Use the #[deku(update = "...")] attribute and call .update() on the struct to synchronize the count field with the actual length of the vector before writing.

    #[derive(Debug, PartialEq, DekuRead, DekuWrite)]
    struct DekuTest {
        #[deku(update = "self.data.len()")]
        count: u8,
        #[deku(count = "count")]
        data: Vec<u8>,
    }
    
    // After modifying data:
    val.data.push(0xAA);
    val.update().unwrap();
  8. Debug decoders with the `logging` feature

    master

    To troubleshoot parsing errors, enable the logging feature in your Cargo.toml. You must also include the log crate and a compatible logger (like env_logger) in your project. Deku uses the trace logging level; run your application with RUST_LOG=trace to see deserialization steps.

    # Cargo.toml
    deku = { version = "*", features = ["logging"] }
    log = "*"
    env_logger = "*"
  9. Use Deku for bit-level binary serialization

    master

    To use Deku, derive DekuRead and DekuWrite on your structs or enums. You can use the #[deku(...)] attribute to specify endianness and bit-level widths for individual fields.

    • Use #[deku(endian = "...")] on the struct to set the byte order (e.g., big or little).
    • Use #[deku(bits = N)] on a field to specify its size in bits.
    • Use from_bytes to deserialize data. It returns a tuple containing the remaining unparsed bytes and the deserialized value.
    • Use to_bytes to serialize the struct back into a byte vector.
    use deku::prelude::*;
    
    #[derive(Debug, PartialEq, DekuRead, DekuWrite)]
    #[deku(endian = "big")]
    struct DekuTest {
        #[deku(bits = 4)]
        field_a: u8,
        #[deku(bits = 4)]
        field_b: u8,
        field_c: u16,
    }
    
    fn main() {
        let data: Vec<u8> = vec![0b0110_1001, 0xBE, 0xEF];
        // from_bytes returns (remaining_bytes, deserialized_value)
        let (_rest, mut val) = DekuTest::from_bytes((data.as_ref(), 0)).unwrap();
        assert_eq!(DekuTest {
            field_a: 0b0110,
            field_b: 0b1001,
            field_c: 0xBEEF,
        }, val);
    
        val.field_c = 0xC0FE;
    
        let data_out = val.to_bytes().unwrap();
        assert_eq!(vec![0b0110_1001, 0xC0, 0xFE], data_out);
    }
  10. Retrieve unused bits from a Reader

    master

    Returns the bits that were read into the internal buffer but not yet consumed by a parsing operation. This happens when a field uses the bits attribute and the total bits read do not align to a full byte boundary.

    Note: This requires the alloc feature.