Skip to main content

epserde/ser/
write_with_names.rs

1/*
2 * SPDX-FileCopyrightText: 2023 Inria
3 * SPDX-FileCopyrightText: 2023 Sebastiano Vigna
4 *
5 * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
6 */
7
8//! Traits and implementations to write named field during serialization.
9//!
10//! [`SerInner::_ser_inner`] writes on a [`WriteWithNames`], rather
11//! than on a [`WriteWithPos`], with the purpose of easily recording write
12//! events happening during a serialization.
13
14use super::*;
15
16#[cfg(not(feature = "std"))]
17use alloc::{
18    format,
19    string::{String, ToString},
20    vec::Vec,
21};
22
23/// Trait extending [`WriteWithPos`] with methods providing alignment,
24/// serialization of named data, and writing of byte slices of zero-copy types.
25///
26/// The purpose of this trait is that of interposing between [`SerInner`] and
27/// the underlying [`WriteWithPos`] a layer in which serialization operations
28/// can be easily intercepted and recorded. In particular, serialization methods
29/// must use the methods of this trait if they want to record the schema of the
30/// serialized data; this is true (maybe counterintuitively) even of ancillary
31/// data such as tags and slice lengths: see [`helpers`] or the [implementation
32/// of `Option`](impls::prim) for examples. All methods have a default
33/// implementation that must be replicated in other implementations.
34///
35/// There are two implementations of [`WriteWithNames`]: [`WriterWithPos`],
36/// which uses the default implementation, and [`SchemaWriter`], which
37/// additionally records a [`Schema`] of the serialized data.
38pub trait WriteWithNames: WriteWithPos + Sized {
39    /// Add some zero padding so that `self.pos() % V:align_to() == 0.`
40    ///
41    /// Other implementations must write the same number of zeros.
42    fn align<V: AlignTo>(&mut self) -> Result<()> {
43        let padding = pad_align_to(self.pos(), V::align_to());
44        for _ in 0..padding {
45            self.write_all(&[0])?;
46        }
47        Ok(())
48    }
49
50    /// Writes a value with an associated name.
51    ///
52    /// The default implementation simply delegates to [`SerInner::_ser_inner`].
53    /// Other implementations might use the name information (e.g., [`SchemaWriter`]),
54    /// but they must in the end delegate to [`SerInner::_ser_inner`].
55    fn write<V: SerInner>(&mut self, _field_name: &str, value: &V) -> Result<()> {
56        unsafe { value._ser_inner(self) }
57    }
58
59    /// Write the memory representation of a (slice of a) zero-copy type.
60    ///
61    /// The default implementation simply delegates to [`WriteNoStd::write_all`].
62    /// Other implementations might use the type information in `V` (e.g., [`SchemaWriter`]),
63    /// but they must in the end delegate to [`WriteNoStd::write_all`].
64    fn write_bytes<V: SerInner + ZeroCopy>(&mut self, value: &[u8]) -> Result<()> {
65        self.write_all(value)
66    }
67}
68
69impl<F: WriteNoStd> WriteWithNames for WriterWithPos<'_, F> {}
70
71/// Information about data written during serialization, either fields or
72/// ancillary data such as option tags and slice lengths.
73#[derive(Debug, Clone)]
74#[cfg_attr(feature = "mem_dbg", derive(mem_dbg::MemDbg, mem_dbg::MemSize))]
75pub struct SchemaRow {
76    /// Name of the piece of data.
77    pub field: String,
78    /// Type of the piece of data.
79    pub ty: String,
80    /// Offset from the start of the file.
81    pub offset: usize,
82    /// Length in bytes of the piece of data.
83    pub size: usize,
84    /// The alignment needed by the piece of data, zero if not applicable
85    /// (e.g., primitive fields, ancillary data, or structures).
86    pub align: usize,
87}
88
89#[derive(Default, Debug, Clone)]
90#[cfg_attr(feature = "mem_dbg", derive(mem_dbg::MemDbg, mem_dbg::MemSize))]
91/// A vector containing all the fields written during serialization, including
92/// ancillary data such as slice lengths and [`Option`] tags.
93pub struct Schema(pub Vec<SchemaRow>);
94
95impl Schema {
96    /// Returns a CSV representation of the schema, including data.
97    ///
98    /// WARNING: the size of the CSV will be larger than the size of the
99    /// serialized file, so it is not a good idea to call this method
100    /// on big structures.
101    pub fn debug(&self, data: &[u8]) -> String {
102        let mut result = "field,offset,align,size,ty,bytes\n".to_string();
103        for i in 0..self.0.len().saturating_sub(1) {
104            let row = &self.0[i];
105            // if it's a composed type, don't print the bytes
106            if row.offset == self.0[i + 1].offset {
107                result.push_str(&format!(
108                    "{},{},{},{},{},\n",
109                    row.field, row.offset, row.align, row.size, row.ty,
110                ));
111            } else {
112                result.push_str(&format!(
113                    "{},{},{},{},{},{:02x?}\n",
114                    row.field,
115                    row.offset,
116                    row.align,
117                    row.size,
118                    row.ty,
119                    &data[row.offset..row.offset + row.size],
120                ));
121            }
122        }
123
124        // the last field can't be a composed type by definition
125        if let Some(row) = self.0.last() {
126            result.push_str(&format!(
127                "{},{},{},{},{},{:02x?}\n",
128                row.field,
129                row.offset,
130                row.align,
131                row.size,
132                row.ty,
133                &data[row.offset..row.offset + row.size],
134            ));
135        }
136
137        result
138    }
139
140    /// Return a CSV representation of the schema, excluding data.
141    pub fn to_csv(&self) -> String {
142        let mut result = "field,offset,align,size,ty\n".to_string();
143        for row in &self.0 {
144            result.push_str(&format!(
145                "{},{},{},{},{}\n",
146                row.field, row.offset, row.align, row.size, row.ty
147            ));
148        }
149        result
150    }
151}
152
153/// A [`WriteWithNames`] that keeps track of the data written on an underlying
154/// [`WriteWithPos`] in a [`Schema`].
155#[derive(Debug)]
156#[cfg_attr(feature = "mem_dbg", derive(mem_dbg::MemDbg, mem_dbg::MemSize))]
157pub struct SchemaWriter<'a, W> {
158    /// The schema so far.
159    pub schema: Schema,
160    /// A recursively-built sequence of previous names.
161    path: Vec<String>,
162    /// What we actually write on.
163    writer: &'a mut W,
164}
165
166impl<'a, W: WriteWithPos> SchemaWriter<'a, W> {
167    /// Create a new empty [`SchemaWriter`] on top of a generic writer `W`.
168    pub fn new(backend: &'a mut W) -> Self {
169        Self {
170            schema: Default::default(),
171            path: Vec::new(),
172            writer: backend,
173        }
174    }
175}
176impl<W: WriteNoStd> WriteNoStd for SchemaWriter<'_, W> {
177    fn write_all(&mut self, buf: &[u8]) -> ser::Result<()> {
178        self.writer.write_all(buf)
179    }
180
181    fn flush(&mut self) -> ser::Result<()> {
182        self.writer.flush()
183    }
184}
185
186impl<W: WriteWithPos> WriteWithPos for SchemaWriter<'_, W> {
187    fn pos(&self) -> usize {
188        self.writer.pos()
189    }
190}
191
192/// WARNING: these implementations must be kept in sync with the ones
193/// in the default implementation of [`WriteWithNames`].
194impl<W: WriteWithPos> WriteWithNames for SchemaWriter<'_, W> {
195    fn align<T: AlignTo>(&mut self) -> Result<()> {
196        let padding = pad_align_to(self.pos(), T::align_to());
197        if padding != 0 {
198            self.schema.0.push(SchemaRow {
199                field: "PADDING".into(),
200                ty: format!("[u8; {}]", padding),
201                offset: self.pos(),
202                size: padding,
203                align: 1,
204            });
205            for _ in 0..padding {
206                self.write_all(&[0])?;
207            }
208        }
209
210        Ok(())
211    }
212
213    fn write<V: SerInner>(&mut self, field_name: &str, value: &V) -> Result<()> {
214        // prepare a row with the field name and the type
215        self.path.push(field_name.into());
216        let pos = self.pos();
217
218        let len = self.schema.0.len();
219        unsafe { value._ser_inner(self)? };
220
221        // This is slightly inefficient because we have to shift
222        // the whole vector, but it's not a big deal and it keeps
223        // the schema in the correct order.
224        self.schema.0.insert(
225            len,
226            SchemaRow {
227                field: self.path.join("."),
228                ty: core::any::type_name::<V>().to_string(),
229                offset: pos,
230                align: 0,
231                size: self.pos() - pos,
232            },
233        );
234        self.path.pop();
235        Ok(())
236    }
237
238    fn write_bytes<V: SerInner + ZeroCopy>(&mut self, value: &[u8]) -> Result<()> {
239        self.path.push("zero".to_string());
240        // Note that we are writing the schema row of the field before
241        // having written its content.
242        self.schema.0.push(SchemaRow {
243            field: self.path.join("."),
244            ty: core::any::type_name::<V>().to_string(),
245            offset: self.pos(),
246            size: value.len(),
247            align: V::align_to(),
248        });
249        self.path.pop();
250
251        self.write_all(value)
252    }
253}