jam1garner / binrw

A Rust crate for helping parse and rebuild binary data using ✨macro magic✨.
https://binrw.rs
MIT License
620 stars 36 forks source link

Get stream position relative to object start #283

Closed ckrenslehner closed 3 months ago

ckrenslehner commented 3 months ago

Is there a way to get a stream position which is not absolute to the stream? I want to parse multiple elements within a stream where I need a relative stream position, because otherwise the calculations are off. See the test code below.

Outer parses the total length of Inner and Inner parses two fixed fields before parsing the rest into a vec. Now I solved this via making a custom stream which provides a relative position. Is there any other, more elegant way?

Greetings and thanks, Christian

#[test]
fn relative_stream() {
    struct RelativeStream<T> {
        inner: T,
        starting_position: Option<u64>,
    }

    impl<T: binrw::io::Seek> RelativeStream<T> {
        /// Get the relative stream position to the starting position which is saved when `new` is called
        pub fn relative_stream_position(&mut self) -> Result<u64, binrw::io::Error> {
            let current_position = self.inner.stream_position()?;

            let relative_position = current_position
                .checked_sub(
                    self.starting_position
                        .clone()
                        .ok_or(binrw::io::Error::other("No starting position"))?,
                )
                .ok_or(binrw::io::Error::other(
                    "Current position must be greater than staring position",
                ))?;

            Ok(relative_position)
        }
    }

    impl<T: binrw::io::Seek> RelativeStream<T> {
        fn new(mut inner: T) -> Self {
            // No result can be returned here so the error handling is deferred
            let starting_position = inner.stream_position().ok();

            Self {
                inner,
                starting_position,
            }
        }
    }

    impl<T: binrw::io::Read> binrw::io::Read for RelativeStream<T> {
        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
            self.inner.read(buf)
        }
    }

    impl<T: binrw::io::Seek> binrw::io::Seek for RelativeStream<T> {
        fn seek(&mut self, pos: binrw::io::SeekFrom) -> binrw::io::Result<u64> {
            self.inner.seek(pos)
        }
    }

    #[binrw::binrw]
    #[derive(Debug)]
    struct Outer {
        len: u8,
        #[br(args(len))]
        inner: Inner,
    }

    #[binrw::binrw]
    #[derive(Debug)]
    #[br(import(len: u8))]
    #[br(stream = r, map_stream = RelativeStream::new)]
    struct Inner {
        pre_1: u8,
        pre_2: u16,

        // Uses relative stream position to ensure that not only the first element within the stream is parsed correctly
        // Relative stream position avoids hardcoding the offset caused by `pre_1` and `pre_2`
        #[br(count = (len - r.relative_stream_position()? as u8) as usize)]
        data: Vec<u8>,
    }

    #[binrw::binrw]
    #[derive(Debug)]
    struct Multi {
        #[br(parse_with = binrw::helpers::until_eof)]
        all: Vec<Outer>,
    }

    let mut cursor = binrw::io::Cursor::new(hex!("080102FFFFFFFFFFFF060304DDDDDDDD"));
    let multi = Multi::read_le(&mut cursor).unwrap();
    dbg!(multi);
}
ismell commented 3 months ago

I think this might solve my use case as well. I have a struct that has a header_size in the middle of the struct. I was to add an assert on my struct that verifies that the total amount of bytes read equals the defined header_size.

e.g.,

#[binrw]
#[br(assert(header_size == relative_stream_position()))]
struct Header {
    ...
    header_size: u32,
    ...
}

Unless there is already a way to do this and I missed it.