smb_fscc/
chained_list.rs

1//! A genric utility struct to wrap "chained"-encoded entries.
2//! Many fscc-query structs have a common "next entry offset" field,
3//! which is used to chain multiple entries together.
4//! This struct wraps the value, and the offset, and provides a way to iterate over them.
5//! See [`ChainedItemList<T>`][crate::ChainedItemList] to see how to write this type when in a list.
6
7use std::io::{Read, Seek, SeekFrom, Write};
8
9use binrw::{Endian, prelude::*};
10
11const CHAINED_ITEM_DEFAULT_OFFSET_PAD: u32 = 4;
12
13/// The size of added fields to the size of each entry in [`ChainedItemList<T>`],
14/// when bin-writing the data, before the actual T data.
15///
16/// A possible additional padding of `OFFSET_PAD` bytes may be added after T,
17/// to align the next entry offset field.
18pub const CHAINED_ITEM_PREFIX_SIZE: usize = std::mem::size_of::<NextEntryOffsetType>();
19
20type NextEntryOffsetType = u32;
21
22/// Implements a chained item list.
23///
24/// A chained item list is a sequence of T entries,
25/// where each entry contains a value of type `T` and an offset to the next entry before it.
26/// The last entry in the list has a next entry offset of `0`.
27///
28/// This is a common pattern for Microsoft fscc-query responses, and is used to
29/// represent lists of variable-length entries.
30///
31/// This struct provides conversion to and from [`Vec<T>`] for ease of use.
32///
33/// The struct supports data of length 0, and puts an empty vector in that case.
34#[derive(Debug, PartialEq, Eq)]
35pub struct ChainedItemList<T, const OFFSET_PAD: u32 = CHAINED_ITEM_DEFAULT_OFFSET_PAD> {
36    values: Vec<T>,
37}
38
39impl<T, const OFFSET_PAD: u32> ChainedItemList<T, OFFSET_PAD> {
40    /// Returns an iterator over the values in the chained item list.
41    #[inline]
42    pub fn iter(&self) -> impl Iterator<Item = &T> {
43        self.values.iter()
44    }
45
46    /// Returns true if the chained item list is empty.
47    #[inline]
48    pub fn is_empty(&self) -> bool {
49        self.values.is_empty()
50    }
51
52    /// Returns the number of items in the chained item list.
53    #[inline]
54    pub fn len(&self) -> usize {
55        self.values.len()
56    }
57}
58
59impl<T, const OFFSET_PAD: u32> BinWrite for ChainedItemList<T, OFFSET_PAD>
60where
61    T: BinWrite,
62    for<'b> <T as BinWrite>::Args<'b>: Default,
63{
64    type Args<'a> = ();
65
66    #[allow(clippy::ptr_arg)] // writer accepts exact type.
67    fn write_options<W: Write + Seek>(
68        &self,
69        writer: &mut W,
70        endian: Endian,
71        _args: Self::Args<'_>,
72    ) -> BinResult<()> {
73        for (i, item) in self.values.iter().enumerate() {
74            let position_before = writer.stream_position()?;
75
76            // Placeholder for next_entry_offset.
77            let next_entry_offset_pos = writer.stream_position()?;
78            NextEntryOffsetType::write_options(&0u32, writer, endian, ())?;
79
80            // Write the value.
81            item.write_options(writer, endian, Default::default())?;
82
83            // Last item: don't align, next item offset is 0.
84            if i == self.values.len() - 1 {
85                break;
86            }
87
88            let position_after_item = writer.stream_position()?;
89            let padding_needed =
90                (OFFSET_PAD as u64 - (position_after_item % OFFSET_PAD as u64)) % OFFSET_PAD as u64;
91            writer.seek(SeekFrom::Current(padding_needed as i64))?;
92            debug_assert!(
93                writer.stream_position()? % OFFSET_PAD as u64 == 0,
94                "ChainedItemList item not aligned to OFFSET_PAD {} after padding",
95                OFFSET_PAD
96            );
97
98            // Calculate and write the next_entry_offset.
99            let position_after = writer.stream_position()?;
100            let next_entry_offset = if i == self.values.len() - 1 {
101                0u32
102            } else {
103                (position_after - position_before) as u32
104            };
105
106            // Seek back to write the next_entry_offset.
107            writer.seek(SeekFrom::Start(next_entry_offset_pos))?;
108            NextEntryOffsetType::write_options(&next_entry_offset, writer, endian, ())?;
109
110            writer.seek(SeekFrom::Start(position_after))?;
111        }
112        Ok(())
113    }
114}
115
116impl<T, const OFFSET_PAD: u32> BinRead for ChainedItemList<T, OFFSET_PAD>
117where
118    T: BinRead,
119    for<'b> <T as BinRead>::Args<'b>: Default,
120{
121    type Args<'a> = ();
122
123    fn read_options<R: Read + Seek>(
124        reader: &mut R,
125        endian: Endian,
126        _args: Self::Args<'_>,
127    ) -> BinResult<Self> {
128        let stream_end = {
129            let current = reader.stream_position()?;
130            // Determine the end of the stream.
131            let end = reader.seek(SeekFrom::End(0))?;
132            // Revert to original position.
133            reader.seek(SeekFrom::Start(current))?;
134            end
135        };
136        if reader.stream_position()? == stream_end {
137            // No data to read, return empty vector.
138            return Ok(Self { values: Vec::new() });
139        }
140
141        let mut values = Vec::new();
142        loop {
143            let position_before = reader.stream_position()?;
144
145            if position_before % OFFSET_PAD as u64 != 0 {
146                return Err(binrw::Error::AssertFail {
147                    pos: position_before,
148                    message: format!(
149                        "ChainedItemList item not aligned to OFFSET_PAD {}",
150                        OFFSET_PAD
151                    ),
152                });
153            }
154
155            let next_item_offset = NextEntryOffsetType::read_options(reader, endian, ())?;
156
157            let item: T = T::read_options(reader, endian, Default::default())?;
158
159            values.push(item);
160
161            if next_item_offset == 0 {
162                break;
163            }
164            reader.seek(SeekFrom::Start(position_before + next_item_offset as u64))?;
165        }
166        Ok(Self { values })
167    }
168}
169
170impl<T, const OFFSET_PAD: u32> From<ChainedItemList<T, OFFSET_PAD>> for Vec<T> {
171    fn from(value: ChainedItemList<T, OFFSET_PAD>) -> Self {
172        value.values
173    }
174}
175
176impl<T, const OFFSET_PAD: u32> From<Vec<T>> for ChainedItemList<T, OFFSET_PAD> {
177    fn from(vec: Vec<T>) -> Self {
178        Self { values: vec }
179    }
180}
181
182impl<T, const OFFSET_PAD: u32> FromIterator<T> for ChainedItemList<T, OFFSET_PAD> {
183    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
184        let values = iter.into_iter().collect();
185        Self { values }
186    }
187}
188
189impl<T, const OFFSET_PAD: u32> std::ops::Deref for ChainedItemList<T, OFFSET_PAD> {
190    type Target = [T];
191
192    fn deref(&self) -> &Self::Target {
193        &self.values
194    }
195}
196
197impl<T, const OFFSET_PAD: u32> std::ops::DerefMut for ChainedItemList<T, OFFSET_PAD> {
198    fn deref_mut(&mut self) -> &mut Self::Target {
199        &mut self.values
200    }
201}
202
203impl<T, const OFFSET_PAD: u32> Default for ChainedItemList<T, OFFSET_PAD> {
204    fn default() -> Self {
205        Self { values: Vec::new() }
206    }
207}