1use ::alloc::{
2 collections::BTreeMap,
3 string::{String, ToString},
4 vec,
5 vec::Vec,
6};
7
8mod alloc;
9mod block;
10mod builder;
11mod commits;
12mod ctz;
13mod editor;
14mod tree;
15
16use self::{alloc::FreshAllocator, block::image_block_mut, ctz::CtzFile};
17use crate::{
18 commit::{CommitEntry, CommitState, MetadataCommitWriter, checked_u10},
19 format::{
20 LFS_TYPE_CREATE, LFS_TYPE_CTZSTRUCT, LFS_TYPE_DELETE, LFS_TYPE_DIR, LFS_TYPE_DIRSTRUCT,
21 LFS_TYPE_INLINESTRUCT, LFS_TYPE_REG, LFS_TYPE_SUPERBLOCK, LFS_TYPE_USERATTR, Tag,
22 },
23 fs::Filesystem,
24 metadata::{FileData, MetadataPair},
25 path::components,
26 types::{Config, Error, FilesystemOptions, Result},
27};
28
29const DISK_VERSION: u32 = 0x0002_0001;
35
36const DEFAULT_NAME_MAX: u32 = 255;
37const DEFAULT_ATTR_MAX: u32 = 1_022;
38const METADATA_PROG_SIZE: usize = 16;
39
40#[derive(Debug, Clone)]
46pub struct ImageBuilder {
47 cfg: Config,
48 options: FilesystemOptions,
49 entries: BTreeMap<String, RootEntry>,
50 visible_entries: BTreeMap<String, RootKind>,
51 update_commits: Vec<RootUpdateCommit>,
52 allocator: FreshAllocator,
53}
54
55#[derive(Debug, Clone)]
60pub struct ImageEditor {
61 cfg: Config,
62 image: Vec<u8>,
63 root: MetadataPair,
64 used_blocks: Vec<bool>,
65}
66
67#[derive(Debug, Clone)]
68enum RootEdit {
69 Storage {
70 id: u16,
71 storage: FileStorage,
72 },
73 Attr {
74 id: u16,
75 attr_type: u8,
76 value: Option<Vec<u8>>,
77 },
78 Delete {
79 id: u16,
80 },
81}
82
83#[derive(Debug, Clone)]
84enum RootEntry {
85 File(InlineFile),
86 Dir(Directory),
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90enum RootKind {
91 File,
92 Dir,
93}
94
95#[derive(Debug, Clone)]
96struct InlineFile {
97 storage: FileStorage,
98 attrs: BTreeMap<u8, Vec<u8>>,
99}
100
101#[derive(Debug, Clone)]
102enum FileStorage {
103 Inline(Vec<u8>),
104 Ctz(CtzFile),
105 ExistingCtz { head: u32, size: u32 },
106}
107
108#[derive(Debug, Clone)]
109struct Directory {
110 pair: [u32; 2],
111 cfg: Config,
112 options: FilesystemOptions,
113 entries: BTreeMap<String, DirectoryEntry>,
114 visible_entries: BTreeMap<String, RootKind>,
115 update_commits: Vec<DirUpdateCommit>,
116}
117
118#[derive(Debug, Clone)]
119enum DirectoryEntry {
120 File(InlineFile),
121 Dir(Directory),
122}
123
124#[derive(Debug, Clone)]
125struct DirUpdateCommit {
126 id: u16,
127 storage: Option<FileStorage>,
128 attrs: BTreeMap<u8, Option<Vec<u8>>>,
129 delete_file: bool,
130}
131
132#[derive(Debug, Clone)]
133struct RootUpdateCommit {
134 id: u16,
135 storage: Option<FileStorage>,
136 attrs: BTreeMap<u8, Option<Vec<u8>>>,
137 delete_file: bool,
138}
139
140#[derive(Debug)]
141struct RootCommit {
142 entries: Vec<CommitEntry>,
143}
144
145fn superblock_payload(cfg: Config, options: FilesystemOptions) -> Vec<u8> {
146 let mut payload = Vec::with_capacity(24);
147 for word in [
148 DISK_VERSION,
149 cfg.block_size as u32,
150 cfg.block_count as u32,
151 options.name_max,
152 options.file_max,
153 options.attr_max,
154 ] {
155 payload.extend_from_slice(&word.to_le_bytes());
156 }
157 payload
158}
159
160fn storage_struct_entry(id: u16, storage: &FileStorage) -> Result<CommitEntry> {
161 match storage {
162 FileStorage::Inline(data) => Ok(CommitEntry::new(
163 Tag::new(LFS_TYPE_INLINESTRUCT, id, checked_u10(data.len())?),
164 data,
165 )),
166 FileStorage::Ctz(ctz) => {
167 let mut payload = Vec::with_capacity(8);
168 payload.extend_from_slice(&ctz.head()?.to_le_bytes());
169 payload.extend_from_slice(&(ctz.len() as u32).to_le_bytes());
170 Ok(CommitEntry::new(
171 Tag::new(LFS_TYPE_CTZSTRUCT, id, 8),
172 &payload,
173 ))
174 }
175 FileStorage::ExistingCtz { head, size } => {
176 let mut payload = Vec::with_capacity(8);
177 payload.extend_from_slice(&head.to_le_bytes());
178 payload.extend_from_slice(&size.to_le_bytes());
179 Ok(CommitEntry::new(
180 Tag::new(LFS_TYPE_CTZSTRUCT, id, 8),
181 &payload,
182 ))
183 }
184 }
185}
186
187fn split_parent<'a>(parts: &'a [&'a str]) -> Result<(&'a str, &'a [&'a str])> {
188 let (name, parents) = parts.split_last().ok_or(Error::Unsupported)?;
189 Ok((*name, parents))
190}
191
192fn root_entry_id<T>(entries: &BTreeMap<String, T>, name: &str) -> Result<u16> {
193 for (index, existing) in entries.keys().enumerate() {
197 if existing == name {
198 let id = u16::try_from(index + 1).map_err(|_| Error::Unsupported)?;
199 return if id < 0x3ff {
200 Ok(id)
201 } else {
202 Err(Error::Unsupported)
203 };
204 }
205 }
206 Err(Error::NotFound)
207}
208
209fn root_key_for_id<T>(entries: &BTreeMap<String, T>, id: u16) -> Result<&str> {
210 if id == 0 {
211 return Err(Error::Unsupported);
212 }
213 entries
214 .keys()
215 .nth(id as usize - 1)
216 .map(|key| key.as_str())
217 .ok_or(Error::Corrupt)
218}
219
220fn root_create_id(files: &[crate::metadata::FileRecord], name: &str) -> Result<u16> {
221 let id = files
222 .iter()
223 .filter(|file| file.name.as_str() < name)
224 .count()
225 .checked_add(1)
226 .ok_or(Error::Unsupported)?;
227 let id = u16::try_from(id).map_err(|_| Error::Unsupported)?;
228 if id < 0x3ff {
229 Ok(id)
230 } else {
231 Err(Error::Unsupported)
232 }
233}
234
235fn dir_create_id(files: &[crate::metadata::FileRecord], name: &str) -> Result<u16> {
236 let id = files
237 .iter()
238 .filter(|file| file.name.as_str() < name)
239 .count();
240 let id = u16::try_from(id).map_err(|_| Error::Unsupported)?;
241 if id < 0x3ff {
242 Ok(id)
243 } else {
244 Err(Error::Unsupported)
245 }
246}
247
248fn dir_key_for_id<T>(entries: &BTreeMap<String, T>, id: u16) -> Result<&str> {
249 entries
250 .keys()
251 .nth(id as usize)
252 .map(|key| key.as_str())
253 .ok_or(Error::Corrupt)
254}
255
256fn directory_entries(
257 entries_by_name: &BTreeMap<String, DirectoryEntry>,
258) -> Result<Vec<CommitEntry>> {
259 let mut entries = Vec::new();
260 for (index, (name, entry)) in entries_by_name.iter().enumerate() {
261 let id = u16::try_from(index).map_err(|_| Error::Unsupported)?;
262 if id >= 0x3ff {
263 return Err(Error::Unsupported);
264 }
265 entries.push(CommitEntry::new(Tag::new(LFS_TYPE_CREATE, id, 0), &[]));
266 match entry {
267 DirectoryEntry::File(file) => {
268 entries.push(CommitEntry::new(
269 Tag::new(LFS_TYPE_REG, id, checked_u10(name.len())?),
270 name.as_bytes(),
271 ));
272 match &file.storage {
273 FileStorage::Inline(data) => {
274 entries.push(CommitEntry::new(
275 Tag::new(LFS_TYPE_INLINESTRUCT, id, checked_u10(data.len())?),
276 data,
277 ));
278 }
279 FileStorage::Ctz(ctz) => {
280 let mut payload = Vec::with_capacity(8);
281 payload.extend_from_slice(&ctz.head()?.to_le_bytes());
282 payload.extend_from_slice(&(ctz.len() as u32).to_le_bytes());
283 entries.push(CommitEntry::new(
284 Tag::new(LFS_TYPE_CTZSTRUCT, id, 8),
285 &payload,
286 ));
287 }
288 FileStorage::ExistingCtz { head, size } => {
289 let mut payload = Vec::with_capacity(8);
290 payload.extend_from_slice(&head.to_le_bytes());
291 payload.extend_from_slice(&size.to_le_bytes());
292 entries.push(CommitEntry::new(
293 Tag::new(LFS_TYPE_CTZSTRUCT, id, 8),
294 &payload,
295 ));
296 }
297 }
298 for (attr_type, attr) in &file.attrs {
299 entries.push(CommitEntry::new(
300 Tag::new(
301 LFS_TYPE_USERATTR + u16::from(*attr_type),
302 id,
303 checked_u10(attr.len())?,
304 ),
305 attr,
306 ));
307 }
308 }
309 DirectoryEntry::Dir(dir) => {
310 entries.push(CommitEntry::new(
311 Tag::new(LFS_TYPE_DIR, id, checked_u10(name.len())?),
312 name.as_bytes(),
313 ));
314 let mut pair = Vec::with_capacity(8);
315 pair.extend_from_slice(&dir.pair[0].to_le_bytes());
316 pair.extend_from_slice(&dir.pair[1].to_le_bytes());
317 entries.push(CommitEntry::new(Tag::new(LFS_TYPE_DIRSTRUCT, id, 8), &pair));
318 }
319 }
320 }
321 Ok(entries)
322}
323
324fn child_file_id(entries: &BTreeMap<String, RootKind>, name: &str) -> Result<u16> {
325 for (index, (existing, kind)) in entries.iter().enumerate() {
326 if existing == name {
327 if *kind != RootKind::File {
328 return Err(Error::Unsupported);
329 }
330 let id = u16::try_from(index).map_err(|_| Error::Unsupported)?;
331 return if id < 0x3ff {
332 Ok(id)
333 } else {
334 Err(Error::Unsupported)
335 };
336 }
337 }
338 Err(Error::NotFound)
339}
340
341fn child_dir_id(entries: &BTreeMap<String, RootKind>, name: &str) -> Result<u16> {
342 for (index, (existing, kind)) in entries.iter().enumerate() {
343 if existing == name {
344 if *kind != RootKind::Dir {
345 return Err(Error::Unsupported);
346 }
347 let id = u16::try_from(index).map_err(|_| Error::Unsupported)?;
348 return if id < 0x3ff {
349 Ok(id)
350 } else {
351 Err(Error::Unsupported)
352 };
353 }
354 }
355 Err(Error::NotFound)
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use crate::Filesystem;
362
363 #[test]
364 fn built_image_mounts_with_rust_reader() {
365 let mut builder = ImageBuilder::new(Config {
366 block_size: 512,
367 block_count: 64,
368 })
369 .expect("builder");
370 builder
371 .add_inline_file("/hello.txt", b"hello from rust\n")
372 .expect("add file")
373 .set_attr("/hello.txt", 0x42, b"greeting")
374 .expect("set attr");
375
376 let image = builder.build().expect("build image");
377 let fs = Filesystem::mount(
378 &image,
379 Config {
380 block_size: 512,
381 block_count: 64,
382 },
383 )
384 .expect("mount generated image");
385
386 assert_eq!(
387 fs.read_file("/hello.txt").expect("read generated file"),
388 b"hello from rust\n"
389 );
390 assert_eq!(
391 fs.read_attr("/hello.txt", 0x42)
392 .expect("read generated attr"),
393 b"greeting"
394 );
395 }
396}