disk/
header.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
//---------------------------------------------------------------------------------------------------- Header check/append.
macro_rules! ensure_header {
	($bytes:ident) => {
		let len = $bytes.len();

		// Ensure our `[u8; 25]` HEADER + VERSION bytes are there.
		if len < 25 {
			bail!("invalid header bytes, total byte length less than 25: {len}");
		}

		// Ensure our HEADER is correct.
		if $bytes[..24] != Self::HEADER {
			bail!("incorrect header bytes\nexpected: {:?}\nfound: {:?}", Self::HEADER, &$bytes[..24],);
		}

		// Ensure our VERSION is correct.
		if $bytes[24] != Self::VERSION {
			bail!("incorrect version byte\nexpected: {:?}\nfound: {:?}", Self::VERSION, &$bytes[24],);
		}
	}
}
pub(crate) use ensure_header;

macro_rules! header_return {
	($buf:ident) => {{
		let mut bytes = Self::full_header().to_vec();
		bytes.append(&mut $buf);

		Ok(bytes)
	}}
}
pub(crate) use header_return;

//---------------------------------------------------------------------------------------------------- Header impl.
macro_rules! impl_header {
	() => {
		/// A custom 24-byte length identifying header for your binary file.
		///
		/// This is combined with [`Self::VERSION`] to prefix your file with 25 bytes.
		///
		/// **Note: [`Self::save_gzip()`] applies compression AFTER, meaning the entire file must be decompressed to get these headers.**
		const HEADER: [u8; 24];
		/// What the version byte will be (0-255).
		const VERSION: u8;

		#[inline(always)]
		/// Read the associated file and attempt to convert the first 24 bytes to a [`String`].
		///
		/// This is useful if your [`Self::HEADER`] should be bytes representing a UTF-8 [`String`].
		fn file_header_to_string() -> Result<String, anyhow::Error> {
			let bytes = Self::file_bytes(0,24)?;
			Ok(String::from_utf8(bytes)?)
		}

		#[inline]
		/// Return the 25 bytes header bytes.
		///
		/// First 24 bytes are the [`Self::HEADER`] bytes.
		///
		/// Last byte is [`Self::VERSION`].
		fn full_header() -> [u8; 25] {
			[
				Self::HEADER[0],
				Self::HEADER[1],
				Self::HEADER[2],
				Self::HEADER[3],
				Self::HEADER[4],
				Self::HEADER[5],
				Self::HEADER[6],
				Self::HEADER[7],
				Self::HEADER[8],
				Self::HEADER[9],
				Self::HEADER[10],
				Self::HEADER[11],
				Self::HEADER[12],
				Self::HEADER[13],
				Self::HEADER[14],
				Self::HEADER[15],
				Self::HEADER[16],
				Self::HEADER[17],
				Self::HEADER[18],
				Self::HEADER[19],
				Self::HEADER[20],
				Self::HEADER[21],
				Self::HEADER[22],
				Self::HEADER[23],
				Self::VERSION
			]
		}

		#[inline]
		/// Reads the first 24 bytes of the associated file and matches it against [`Self::HEADER`].
		///
		/// If the bytes match, the next byte _may be_ be our [`Self::VERSION`] and is returned.
		///
		/// ## Note
		/// This only works on a non-compressed file.
		fn file_version() -> Result<u8, anyhow::Error> {
			use std::io::Read;

			let mut bytes = [0; 25];

			let mut file = std::fs::File::open(Self::absolute_path()?)?;

			file.read_exact(&mut bytes)?;

			if bytes[0..24] == Self::HEADER {
				Ok(bytes[24])
			} else {
				bail!("header bytes failed to match.\nexpected: {:?}\nfound: {:?}", Self::HEADER, &bytes[0..24]);
			}
		}

		#[inline]
		/// This is the function that ties the versioning system together.
		///
		/// It takes a variable static array of `(VERSION, Struct::constructor)`
		/// tuples, attempting to deserialize them starting from index `0`.
		///
		/// AKA, you give a list of versions and your choice of `disk`
		/// constructors for various versions of the same-ish struct.
		///
		/// An example:
		/// ```rust,ignore
		/// disk::bincode!(Data0, Dir::Data, "Data", "", "data", [255_u8; 24], 0); // <- note: version 0.
		/// struct Data0 {
		///     data: Vec<u8>,
		/// }
		///
		/// // This converts a `Data0` into a `Data5`
		/// impl Data0 {
		///     fn to_data5() -> Result<Data5, anyhow::Error> {
		///         match Self::from_file() {
		///             Ok(s)  => Ok(Data1 { data: s.data, ..Default::default() }),
		///             Err(e) => Err(e),
		///         }
		///     }
		/// }
		///
		/// /* ... data1, data2, data3, data4 ... */
		///
		/// disk::bincode!(Data1, Dir::Data, "Data", "", "data", [255_u8; 24], 5); // <- note: version 5.
		/// struct Data5 {
		///     data: Vec<u8>,
		///     more_data: Vec<u8>,
		/// }
		/// ```
		///
		/// The `Data0::to_data5()` can be used as the constructor for this function.
		///
		/// Now, if we'd like to deserialize `Data5`, but fallback if
		/// the file detected is an older version, we can write this:
		/// ```rust,ignore
		/// let data = Data5::from_versions(&[
		///     (5, Data5::from_file), // Your choice of function here.
		///     (4, Data4::to_data5),  // These as well.
		///     (3, Data3::to_data5),
		///     (2, Data2::to_data5),
		///     (1, Data1::to_data5),
		///     (0, Data0::to_data5),
		/// ]).unwrap();
		///```
		/// This will go top-to-bottom starting at `5`, ending at `0`,
		/// checking if the version matches, then attempting deserialization.
		///
		/// The returned `Ok(u8, Self)` contains the version that successfully
		/// matched and the resulting (converted) deserialized data.
		///
		/// The output data is always `Self`, so the `fn()` constructors you
		/// input are responsible for converting between the various types.
		fn from_versions(
			versions_and_constructors: &'static [(u8, fn() -> Result<Self, anyhow::Error>)],
		) -> Result<(u8, Self), anyhow::Error> {
			// Get on-disk version.
			let file = Self::file_version()?;

//			// If target version, attempt deserialization and return.
//			if file == Self::VERSION {
//				return match constructor() {
//					Ok(data) => Ok((file, data)),
//					Err(e)   => Err(e),
//				};
//			}

			// Else, attempt the other version constructors.
			for (version, constructor) in versions_and_constructors {
				// If not the matching version, continue.
				if file != *version {
					continue;
				}

				// If version matches, attempt to construct.
				return match constructor() {
					Ok(data) => Ok((*version, data)),
					Err(e)   => Err(e),
				};
			}

			// Return error if nothing worked.
			Err(anyhow!("all versions failed to match: {versions_and_constructors:#?}"))
		}
	}
}
pub(crate) use impl_header;