xc3_write/
lib.rs

1//! A binary writing and layout implementation using separate write and layout passes.
2use std::io::Seek;
3use std::io::SeekFrom;
4use std::io::Write;
5use std::marker::PhantomData;
6use std::ops::Deref;
7
8// io::Error supports custom error variants if needed.
9// Writing will typically only fail from io errors on the writer anyway.
10pub type Xc3Result<T> = Result<T, std::io::Error>;
11
12pub mod strings;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Endian {
16    Little,
17    Big,
18}
19
20/// The write pass that writes fields and placeholder offsets.
21pub trait Xc3Write {
22    /// The type storing offset data to be used in [Xc3WriteOffsets].
23    type Offsets<'a>
24    where
25        Self: 'a;
26
27    /// Write all fields and placeholder offsets.
28    /// This should almost always be derived for non primitive types.
29    ///
30    /// An object's size is defined as the difference between the writer position
31    /// before and after the first pass and does not need to be user defined.
32    /// Custom implementations of [Xc3Write] should ensure the write head points after the data
33    /// when the function returns to ensure correct size calculations.
34    fn xc3_write<W: Write + Seek>(
35        &self,
36        writer: &mut W,
37        endian: Endian,
38    ) -> Xc3Result<Self::Offsets<'_>>;
39
40    /// Return `Some(_)` if the offset should be updated and
41    /// `Some(true)` if the data should also be written.
42    /// Defaults to `Some(true)`.
43    fn should_write(&self) -> Option<bool> {
44        Some(true)
45    }
46
47    /// The alignment of absolute offsets for this type in bytes.
48    const ALIGNMENT: u64 = 4;
49}
50
51/// The layout pass that updates and writes data for all fields in [Xc3Write::Offsets] recursively.
52pub trait Xc3WriteOffsets {
53    type Args;
54
55    /// Update and write pointed to data for all fields in [Xc3Write::Offsets].
56    ///
57    /// The goal is to call [Offset::write] or [Xc3WriteOffsets::write_offsets]
58    /// in increasing order by absolute offset stored in `data_ptr`.
59    /// For writing in order by field recursively, simply derive [Xc3WriteOffsets].
60    /// Manually implementing this trait allows flexibility for cases like placing strings
61    /// for all types at the end of the file.
62    fn write_offsets<W: Write + Seek>(
63        &self,
64        writer: &mut W,
65        base_offset: u64,
66        data_ptr: &mut u64,
67        endian: Endian,
68        args: Self::Args,
69    ) -> Xc3Result<()>;
70}
71
72/// A complete writing combining [Xc3Write] and [Xc3WriteOffsets].
73///
74/// Most types should rely on the blanket impl.
75/// For types without offsets, simply set [Xc3WriteOffsets::Args] to the unit type `()`.
76pub trait WriteFull {
77    /// The type for [Xc3WriteOffsets::Args].
78    type Args;
79
80    /// A complete write uses a two pass approach to handle offsets.
81    ///
82    /// We can fully write any type that can fully write its offset values.
83    /// This includes types with an offset type of () like primitive types.
84    fn write_full<W: Write + Seek>(
85        &self,
86        writer: &mut W,
87        base_offset: u64,
88        data_ptr: &mut u64,
89        endian: Endian,
90        offset_args: Self::Args,
91    ) -> Xc3Result<()>;
92}
93
94impl<T, A> WriteFull for T
95where
96    T: Xc3Write,
97    for<'a> T::Offsets<'a>: Xc3WriteOffsets<Args = A>,
98{
99    type Args = A;
100
101    fn write_full<W: Write + Seek>(
102        &self,
103        writer: &mut W,
104        base_offset: u64,
105        data_ptr: &mut u64,
106        endian: Endian,
107        offset_args: Self::Args,
108    ) -> Xc3Result<()> {
109        // Ensure all items are written before their pointed to data.
110        let offsets = self.xc3_write(writer, endian)?;
111        *data_ptr = (*data_ptr).max(writer.stream_position()?);
112
113        offsets.write_offsets(writer, base_offset, data_ptr, endian, offset_args)?;
114        // Account for padding or alignment added after writing.
115        *data_ptr = (*data_ptr).max(writer.stream_position()?);
116        Ok(())
117    }
118}
119
120// Support importing both the trait and derive macro at once.
121pub use xc3_write_derive::Xc3Write;
122pub use xc3_write_derive::Xc3WriteOffsets;
123
124pub struct FieldPosition<'a, T> {
125    /// The position in the file for the field.
126    pub position: u64,
127    /// The field value.
128    pub data: &'a T,
129}
130
131impl<'a, T> FieldPosition<'a, T> {
132    pub fn new(position: u64, data: &'a T) -> Self {
133        Self { position, data }
134    }
135}
136
137pub struct Offset<'a, P, T> {
138    /// The position in the file for the offset field.
139    pub position: u64,
140    /// The data pointed to by the offset.
141    pub data: &'a T,
142    /// Alignment override applied at the field level.
143    pub field_alignment: Option<u64>,
144    /// The byte used for padding or alignment.
145    /// This is usually `0u8`.
146    pub padding_byte: u8,
147    phantom: PhantomData<P>,
148}
149
150impl<P, T: Xc3Write> std::fmt::Debug for Offset<'_, P, T> {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        // Don't print the actual data to avoid excessive output.
153        f.debug_struct("Offset")
154            .field("position", &self.position)
155            .field("data", &std::any::type_name::<T>())
156            .finish()
157    }
158}
159
160impl<'a, P, T> Offset<'a, P, T> {
161    pub fn new(position: u64, data: &'a T, field_alignment: Option<u64>, padding_byte: u8) -> Self {
162        Self {
163            position,
164            data,
165            field_alignment,
166            padding_byte,
167            phantom: PhantomData,
168        }
169    }
170
171    pub fn set_offset<W>(&self, writer: &mut W, offset: u64, endian: Endian) -> Xc3Result<()>
172    where
173        W: Write + Seek,
174        // TODO: Create a trait for this?
175        P: TryFrom<u64> + Xc3Write,
176        <P as TryFrom<u64>>::Error: std::fmt::Debug,
177    {
178        writer.seek(SeekFrom::Start(self.position))?;
179        let offset = P::try_from(offset).unwrap();
180        offset.xc3_write(writer, endian)?;
181        Ok(())
182    }
183
184    fn set_offset_seek<W>(
185        &self,
186        writer: &mut W,
187        base_offset: u64,
188        data_ptr: &mut u64,
189        type_alignment: u64,
190        should_write: bool,
191        endian: Endian,
192    ) -> Xc3Result<()>
193    where
194        W: Write + Seek,
195        // TODO: Create a trait for this?
196        P: TryFrom<u64> + Xc3Write,
197        <P as TryFrom<u64>>::Error: std::fmt::Debug,
198    {
199        // Assume the data pointer hasn't been modified since the first pass.
200        *data_ptr = (*data_ptr).max(writer.stream_position()?);
201
202        // Account for the type or field alignment.
203        let alignment = self.field_alignment.unwrap_or(type_alignment);
204        let aligned_data_pr = data_ptr.next_multiple_of(alignment);
205
206        // Update the offset value.
207        self.set_offset(writer, aligned_data_pr - base_offset, endian)?;
208
209        if should_write {
210            // Seek to the data position.
211            // Handle any padding up the desired alignment.
212            writer.seek(SeekFrom::Start(*data_ptr))?;
213            vec![self.padding_byte; (aligned_data_pr - *data_ptr) as usize]
214                .xc3_write(writer, endian)?;
215            // Point the data pointer past this data.
216            *data_ptr = (*data_ptr).max(writer.stream_position()?);
217        }
218
219        Ok(())
220    }
221}
222
223impl<P, T> Offset<'_, P, T>
224where
225    T: Xc3Write,
226    P: TryFrom<u64> + Xc3Write,
227    <P as TryFrom<u64>>::Error: std::fmt::Debug,
228{
229    pub fn write<W: Write + Seek>(
230        &self,
231        writer: &mut W,
232        base_offset: u64,
233        data_ptr: &mut u64,
234        endian: Endian,
235    ) -> Xc3Result<T::Offsets<'_>> {
236        if let Some(should_write) = self.data.should_write() {
237            self.set_offset_seek(
238                writer,
239                base_offset,
240                data_ptr,
241                T::ALIGNMENT,
242                should_write,
243                endian,
244            )?;
245        }
246        let offsets = self.data.xc3_write(writer, endian)?;
247        *data_ptr = (*data_ptr).max(writer.stream_position()?);
248        Ok(offsets)
249    }
250}
251
252impl<P, T> Offset<'_, P, T>
253where
254    T: Xc3Write + WriteFull,
255    P: TryFrom<u64> + Xc3Write,
256    <P as TryFrom<u64>>::Error: std::fmt::Debug,
257{
258    pub fn write_full<W: Write + Seek>(
259        &self,
260        writer: &mut W,
261        base_offset: u64,
262        data_ptr: &mut u64,
263        endian: Endian,
264        args: T::Args,
265    ) -> Xc3Result<()> {
266        // Always skip null offsets but not always empty vecs.
267        if let Some(should_write) = self.data.should_write() {
268            self.set_offset_seek(
269                writer,
270                base_offset,
271                data_ptr,
272                T::ALIGNMENT,
273                should_write,
274                endian,
275            )?;
276            self.data
277                .write_full(writer, base_offset, data_ptr, endian, args)?;
278        }
279        Ok(())
280    }
281}
282
283macro_rules! xc3_write_impl {
284    ($($ty:ty),*) => {
285        $(
286            impl Xc3Write for $ty {
287                // This also enables write_full since () implements Xc3WriteOffsets.
288                type Offsets<'a> = ();
289
290                fn xc3_write<W: std::io::Write + std::io::Seek>(
291                    &self,
292                    writer: &mut W,
293                    endian: Endian,
294                ) -> Xc3Result<Self::Offsets<'_>> {
295                    match endian {
296                        Endian::Little => writer.write_all(&self.to_le_bytes())?,
297                        Endian::Big => writer.write_all(&self.to_be_bytes())?,
298                    }
299                    Ok(())
300                }
301
302                // TODO: Should this be specified manually?
303                const ALIGNMENT: u64 = std::mem::align_of::<$ty>() as u64;
304            }
305        )*
306
307    };
308}
309
310xc3_write_impl!(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64);
311
312// TODO: macro for handling larger tuples?
313impl<A: Xc3Write, B: Xc3Write> Xc3Write for (A, B) {
314    type Offsets<'a>
315        = (A::Offsets<'a>, B::Offsets<'a>)
316    where
317        A: 'a,
318        B: 'a;
319
320    fn xc3_write<W: Write + Seek>(
321        &self,
322        writer: &mut W,
323        endian: Endian,
324    ) -> Xc3Result<Self::Offsets<'_>> {
325        Ok((
326            self.0.xc3_write(writer, endian)?,
327            self.1.xc3_write(writer, endian)?,
328        ))
329    }
330}
331
332impl<A: Xc3WriteOffsets, B: Xc3WriteOffsets> Xc3WriteOffsets for (A, B) {
333    type Args = ();
334
335    fn write_offsets<W: Write + Seek>(
336        &self,
337        _: &mut W,
338        _: u64,
339        _: &mut u64,
340        _: Endian,
341        _: (),
342    ) -> Xc3Result<()> {
343        Ok(())
344    }
345}
346
347// TODO: Support types with offsets?
348impl<const N: usize, T> Xc3Write for [T; N]
349where
350    T: Xc3Write + 'static,
351{
352    type Offsets<'a> = ();
353
354    fn xc3_write<W: Write + Seek>(
355        &self,
356        writer: &mut W,
357        endian: Endian,
358    ) -> Xc3Result<Self::Offsets<'_>> {
359        for value in self {
360            value.xc3_write(writer, endian)?;
361        }
362        Ok(())
363    }
364}
365
366impl<T: Xc3Write> Xc3Write for Box<T> {
367    type Offsets<'a>
368        = T::Offsets<'a>
369    where
370        Self: 'a;
371
372    fn xc3_write<W: Write + Seek>(
373        &self,
374        writer: &mut W,
375        endian: Endian,
376    ) -> Xc3Result<Self::Offsets<'_>> {
377        self.deref().xc3_write(writer, endian)
378    }
379}
380
381impl Xc3Write for String {
382    type Offsets<'a> = ();
383
384    fn xc3_write<W: Write + Seek>(
385        &self,
386        writer: &mut W,
387        _: Endian,
388    ) -> Xc3Result<Self::Offsets<'_>> {
389        writer.write_all(self.as_bytes())?;
390        writer.write_all(&[0u8])?;
391        Ok(())
392    }
393
394    const ALIGNMENT: u64 = 1;
395}
396
397// Create a new type to differentiate vec and a vec of offsets.
398// This allows using a blanket implementation for write full.
399pub struct VecOffsets<T>(pub Vec<T>);
400
401impl<T> Xc3Write for Vec<T>
402where
403    T: Xc3Write + 'static,
404{
405    type Offsets<'a> = VecOffsets<T::Offsets<'a>>;
406
407    fn xc3_write<W: Write + Seek>(
408        &self,
409        writer: &mut W,
410        endian: Endian,
411    ) -> Xc3Result<Self::Offsets<'_>> {
412        // TODO: Find a less hacky way to specialize Vec<u8>.
413        let offsets = if let Some(bytes) = <dyn core::any::Any>::downcast_ref::<Vec<u8>>(self) {
414            // Avoiding writing buffers byte by byte to drastically improve performance.
415            writer.write_all(bytes)?;
416            Vec::new()
417        } else {
418            self.iter()
419                .map(|v| v.xc3_write(writer, endian))
420                .collect::<Result<Vec<_>, _>>()?
421        };
422        Ok(VecOffsets(offsets))
423    }
424
425    fn should_write(&self) -> Option<bool> {
426        Some(!self.is_empty())
427    }
428}
429
430impl<T, A> Xc3WriteOffsets for VecOffsets<T>
431where
432    T: Xc3WriteOffsets<Args = A>,
433    A: Clone,
434{
435    type Args = A;
436
437    fn write_offsets<W: Write + Seek>(
438        &self,
439        writer: &mut W,
440        base_offset: u64,
441        data_ptr: &mut u64,
442        endian: Endian,
443        args: Self::Args,
444    ) -> Xc3Result<()> {
445        // TODO: How to support non clone args?
446        for item in &self.0 {
447            item.write_offsets(writer, base_offset, data_ptr, endian, args.clone())?;
448        }
449        Ok(())
450    }
451}
452
453impl Xc3Write for () {
454    type Offsets<'a> = ();
455
456    fn xc3_write<W: Write + Seek>(&self, _: &mut W, _: Endian) -> Xc3Result<Self::Offsets<'_>> {
457        Ok(())
458    }
459
460    const ALIGNMENT: u64 = 1;
461}
462
463impl Xc3WriteOffsets for () {
464    type Args = ();
465    fn write_offsets<W: Write + Seek>(
466        &self,
467        _: &mut W,
468        _: u64,
469        _: &mut u64,
470        _: Endian,
471        _: Self::Args,
472    ) -> Xc3Result<()> {
473        Ok(())
474    }
475}
476
477impl<T> Xc3Write for Option<T>
478where
479    T: Xc3Write,
480{
481    type Offsets<'a>
482        = Option<T::Offsets<'a>>
483    where
484        Self: 'a;
485
486    fn xc3_write<W: Write + Seek>(
487        &self,
488        writer: &mut W,
489        endian: Endian,
490    ) -> Xc3Result<Self::Offsets<'_>> {
491        self.as_ref()
492            .map(|v| v.xc3_write(writer, endian))
493            .transpose()
494    }
495
496    fn should_write(&self) -> Option<bool> {
497        self.as_ref().map(|_| true)
498    }
499
500    const ALIGNMENT: u64 = T::ALIGNMENT;
501}
502
503impl<T, A> Xc3WriteOffsets for Option<T>
504where
505    T: Xc3WriteOffsets<Args = A>,
506{
507    type Args = A;
508
509    fn write_offsets<W: Write + Seek>(
510        &self,
511        writer: &mut W,
512        base_offset: u64,
513        data_ptr: &mut u64,
514        endian: Endian,
515        args: Self::Args,
516    ) -> Xc3Result<()> {
517        if let Some(value) = self {
518            value.write_offsets(writer, base_offset, data_ptr, endian, args)?;
519        }
520        Ok(())
521    }
522}
523
524#[doc(hidden)]
525#[macro_export]
526macro_rules! assert_hex_eq {
527    ($a:expr, $b:expr) => {
528        pretty_assertions::assert_str_eq!(hex::encode($a), hex::encode($b))
529    };
530}