1use super::*;
2
3impl ImageEditor {
4 pub fn open(image: Vec<u8>, cfg: Config) -> Result<Self> {
5 let fs = Filesystem::mount(&image, cfg)?;
6 let used_blocks = fs.used_blocks()?;
7 let root = MetadataPair::read(&image, cfg, [0, 1])?;
8 Ok(Self {
9 cfg,
10 image,
11 root,
12 used_blocks,
13 })
14 }
15
16 pub fn used_blocks(&self) -> &[bool] {
17 &self.used_blocks
18 }
19
20 pub fn create_file(&mut self, path: &str, data: &[u8]) -> Result<&mut Self> {
28 let parts = components(path)?;
29 let (name, parents) = split_parent(&parts)?;
30 if !parents.is_empty() {
31 return self.create_file_in_directory(parents, name, data);
32 }
33 if name.len() > DEFAULT_NAME_MAX as usize {
34 return Err(Error::Unsupported);
35 }
36
37 let files = self.root.files()?;
38 if files.iter().any(|file| file.name == name) {
39 return Err(Error::Unsupported);
40 }
41 let id = root_create_id(&files, name)?;
42 let name_entry = CommitEntry::new(
43 Tag::new(LFS_TYPE_REG, id, checked_u10(name.len())?),
44 name.as_bytes(),
45 );
46
47 if data.len() <= DEFAULT_ATTR_MAX as usize {
48 let entries = [
49 CommitEntry::new(Tag::new(LFS_TYPE_CREATE, id, 0), &[]),
50 name_entry,
51 CommitEntry::new(
52 Tag::new(LFS_TYPE_INLINESTRUCT, id, checked_u10(data.len())?),
53 data,
54 ),
55 ];
56 self.append_root_entries(&entries)?;
57 return Ok(self);
58 }
59
60 let mut allocator = FreshAllocator::from_used(self.cfg, self.used_blocks.clone())?;
61 let blocks = allocator.alloc_ctz_blocks(data.len())?;
62 let storage = FileStorage::Ctz(CtzFile::new(data, blocks));
63 storage.erase_blocks(&mut self.image, self.cfg)?;
64 storage.write_blocks(&mut self.image, self.cfg)?;
65 let entries = [
66 CommitEntry::new(Tag::new(LFS_TYPE_CREATE, id, 0), &[]),
67 name_entry,
68 storage_struct_entry(id, &storage)?,
69 ];
70 self.append_root_entries(&entries)?;
71 self.used_blocks = allocator.into_used();
72 Ok(self)
73 }
74
75 fn create_file_in_directory(
76 &mut self,
77 parents: &[&str],
78 name: &str,
79 data: &[u8],
80 ) -> Result<&mut Self> {
81 if name.len() > DEFAULT_NAME_MAX as usize {
82 return Err(Error::Unsupported);
83 }
84
85 let parent = self.resolve_directory(parents)?;
86 let files = self.files_in_pair_chain(&parent)?;
87 if files.iter().any(|file| file.name == name) {
88 return Err(Error::Unsupported);
89 }
90 let id = dir_create_id(&files, name)?;
91 let name_entry = CommitEntry::new(
92 Tag::new(LFS_TYPE_REG, id, checked_u10(name.len())?),
93 name.as_bytes(),
94 );
95
96 if data.len() <= DEFAULT_ATTR_MAX as usize {
97 let entries = [
98 CommitEntry::new(Tag::new(LFS_TYPE_CREATE, id, 0), &[]),
99 name_entry,
100 CommitEntry::new(
101 Tag::new(LFS_TYPE_INLINESTRUCT, id, checked_u10(data.len())?),
102 data,
103 ),
104 ];
105 self.append_pair_entries(&parent, &entries)?;
106 return Ok(self);
107 }
108
109 let mut allocator = FreshAllocator::from_used(self.cfg, self.used_blocks.clone())?;
110 let blocks = allocator.alloc_ctz_blocks(data.len())?;
111 let storage = FileStorage::Ctz(CtzFile::new(data, blocks));
112 storage.erase_blocks(&mut self.image, self.cfg)?;
113 storage.write_blocks(&mut self.image, self.cfg)?;
114 let entries = [
115 CommitEntry::new(Tag::new(LFS_TYPE_CREATE, id, 0), &[]),
116 name_entry,
117 storage_struct_entry(id, &storage)?,
118 ];
119 self.append_pair_entries(&parent, &entries)?;
120 self.used_blocks = allocator.into_used();
121 Ok(self)
122 }
123
124 pub fn create_dir(&mut self, path: &str) -> Result<&mut Self> {
131 let parts = components(path)?;
132 let (name, parents) = split_parent(&parts)?;
133 if !parents.is_empty() {
134 return self.create_dir_in_directory(parents, name);
135 }
136 if name.len() > DEFAULT_NAME_MAX as usize {
137 return Err(Error::Unsupported);
138 }
139
140 let files = self.root.files()?;
141 if files.iter().any(|file| file.name == name) {
142 return Err(Error::Unsupported);
143 }
144 let id = root_create_id(&files, name)?;
145 let mut allocator = FreshAllocator::from_used(self.cfg, self.used_blocks.clone())?;
146 let pair = allocator.alloc_pair()?;
147 Directory::new(pair, self.cfg, FilesystemOptions::default())
148 .write_empty_pair(&mut self.image, self.cfg)?;
149
150 let mut pair_payload = Vec::with_capacity(8);
151 pair_payload.extend_from_slice(&pair[0].to_le_bytes());
152 pair_payload.extend_from_slice(&pair[1].to_le_bytes());
153 let entries = [
154 CommitEntry::new(Tag::new(LFS_TYPE_CREATE, id, 0), &[]),
155 CommitEntry::new(
156 Tag::new(LFS_TYPE_DIR, id, checked_u10(name.len())?),
157 name.as_bytes(),
158 ),
159 CommitEntry::new(Tag::new(LFS_TYPE_DIRSTRUCT, id, 8), &pair_payload),
160 ];
161 self.append_root_entries(&entries)?;
162 self.used_blocks = allocator.into_used();
163 Ok(self)
164 }
165
166 fn create_dir_in_directory(&mut self, parents: &[&str], name: &str) -> Result<&mut Self> {
167 if name.len() > DEFAULT_NAME_MAX as usize {
168 return Err(Error::Unsupported);
169 }
170
171 let parent = self.resolve_directory(parents)?;
172 let files = self.files_in_pair_chain(&parent)?;
173 if files.iter().any(|file| file.name == name) {
174 return Err(Error::Unsupported);
175 }
176 let id = dir_create_id(&files, name)?;
177 let mut allocator = FreshAllocator::from_used(self.cfg, self.used_blocks.clone())?;
178 let pair = allocator.alloc_pair()?;
179 Directory::new(pair, self.cfg, FilesystemOptions::default())
180 .write_empty_pair(&mut self.image, self.cfg)?;
181
182 let mut pair_payload = Vec::with_capacity(8);
183 pair_payload.extend_from_slice(&pair[0].to_le_bytes());
184 pair_payload.extend_from_slice(&pair[1].to_le_bytes());
185 let entries = [
186 CommitEntry::new(Tag::new(LFS_TYPE_CREATE, id, 0), &[]),
187 CommitEntry::new(
188 Tag::new(LFS_TYPE_DIR, id, checked_u10(name.len())?),
189 name.as_bytes(),
190 ),
191 CommitEntry::new(Tag::new(LFS_TYPE_DIRSTRUCT, id, 8), &pair_payload),
192 ];
193 self.append_pair_entries(&parent, &entries)?;
194 self.used_blocks = allocator.into_used();
195 Ok(self)
196 }
197
198 pub fn update_inline_file(&mut self, path: &str, data: &[u8]) -> Result<&mut Self> {
206 if data.len() > DEFAULT_ATTR_MAX as usize {
207 return Err(Error::Unsupported);
208 }
209
210 let parts = components(path)?;
211 let (name, parents) = split_parent(&parts)?;
212 if !parents.is_empty() {
213 let parent = self.resolve_directory(parents)?;
214 let (pair, id) = self.find_file_in_pair_chain(&parent, name)?;
215 let entry = CommitEntry::new(
216 Tag::new(LFS_TYPE_INLINESTRUCT, id, checked_u10(data.len())?),
217 data,
218 );
219 self.append_pair_entries(&pair, &[entry])?;
220 return Ok(self);
221 }
222
223 let id = self.root_file_id_from_name(name)?;
224 self.apply_root_edit(RootEdit::Storage {
225 id,
226 storage: FileStorage::Inline(data.to_vec()),
227 })?;
228 Ok(self)
229 }
230
231 pub fn update_file(&mut self, path: &str, data: &[u8]) -> Result<&mut Self> {
232 if data.len() <= DEFAULT_ATTR_MAX as usize {
233 return self.update_inline_file(path, data);
234 }
235
236 let mut allocator = FreshAllocator::from_used(self.cfg, self.used_blocks.clone())?;
237 let blocks = allocator.alloc_ctz_blocks(data.len())?;
238 let storage = FileStorage::Ctz(CtzFile::new(data, blocks));
239 storage.erase_blocks(&mut self.image, self.cfg)?;
240 storage.write_blocks(&mut self.image, self.cfg)?;
241
242 let parts = components(path)?;
243 let (name, parents) = split_parent(&parts)?;
244 if !parents.is_empty() {
245 let parent = self.resolve_directory(parents)?;
246 let (pair, id) = self.find_file_in_pair_chain(&parent, name)?;
247 let entry = storage_struct_entry(id, &storage)?;
248 self.append_pair_entries(&pair, &[entry])?;
249 self.used_blocks = allocator.into_used();
250 return Ok(self);
251 }
252
253 let id = self.root_file_id_from_name(name)?;
254 self.apply_root_edit(RootEdit::Storage { id, storage })?;
255 self.used_blocks = allocator.into_used();
256 Ok(self)
257 }
258
259 pub fn update_attr(&mut self, path: &str, attr_type: u8, data: &[u8]) -> Result<&mut Self> {
260 if data.len() > DEFAULT_ATTR_MAX as usize {
261 return Err(Error::Unsupported);
262 }
263
264 let parts = components(path)?;
265 let (name, parents) = split_parent(&parts)?;
266 if !parents.is_empty() {
267 let parent = self.resolve_directory(parents)?;
268 let (pair, id) = self.find_file_in_pair_chain(&parent, name)?;
269 let entry = CommitEntry::new(
270 Tag::new(
271 LFS_TYPE_USERATTR + u16::from(attr_type),
272 id,
273 checked_u10(data.len())?,
274 ),
275 data,
276 );
277 self.append_pair_entries(&pair, &[entry])?;
278 return Ok(self);
279 }
280
281 let id = self.root_file_id(path)?;
282 self.apply_root_edit(RootEdit::Attr {
283 id,
284 attr_type,
285 value: Some(data.to_vec()),
286 })?;
287 Ok(self)
288 }
289
290 pub fn delete_attr(&mut self, path: &str, attr_type: u8) -> Result<&mut Self> {
291 let parts = components(path)?;
292 let (name, parents) = split_parent(&parts)?;
293 if !parents.is_empty() {
294 let parent = self.resolve_directory(parents)?;
295 let (pair, id) = self.find_file_in_pair_chain(&parent, name)?;
296 let entry = CommitEntry::new(
297 Tag::new(LFS_TYPE_USERATTR + u16::from(attr_type), id, 0x3ff),
298 &[],
299 );
300 self.append_pair_entries(&pair, &[entry])?;
301 return Ok(self);
302 }
303
304 let id = self.root_file_id(path)?;
305 self.apply_root_edit(RootEdit::Attr {
306 id,
307 attr_type,
308 value: None,
309 })?;
310 Ok(self)
311 }
312
313 pub fn delete_file(&mut self, path: &str) -> Result<&mut Self> {
314 let parts = components(path)?;
315 let (name, parents) = split_parent(&parts)?;
316 if !parents.is_empty() {
317 let parent = self.resolve_directory(parents)?;
318 let (pair, id) = self.find_file_in_pair_chain(&parent, name)?;
319 let entry = CommitEntry::new(Tag::new(LFS_TYPE_DELETE, id, 0), &[]);
320 self.append_pair_entries(&pair, &[entry])?;
321 return Ok(self);
322 }
323
324 let id = self.root_file_id(path)?;
325 self.apply_root_edit(RootEdit::Delete { id })?;
326 Ok(self)
327 }
328
329 pub fn delete_dir(&mut self, path: &str) -> Result<&mut Self> {
330 let parts = components(path)?;
331 let (name, parents) = split_parent(&parts)?;
332 if !parents.is_empty() {
333 let parent = self.resolve_directory(parents)?;
334 let (pair, file) = self.find_file_record_in_pair_chain(&parent, name)?;
335 let FileData::Directory(child) = file.data else {
336 return Err(Error::Unsupported);
337 };
338 if file.ty != crate::types::FileType::Dir {
339 return Err(Error::Unsupported);
340 }
341 let child = MetadataPair::read(&self.image, self.cfg, child)?;
342 if !self.files_in_pair_chain(&child)?.is_empty() {
343 return Err(Error::Unsupported);
344 }
345 let entry = CommitEntry::new(Tag::new(LFS_TYPE_DELETE, file.id, 0), &[]);
346 self.append_pair_entries(&pair, &[entry])?;
347 return Ok(self);
348 }
349
350 let file = self
351 .root
352 .files()?
353 .into_iter()
354 .find(|file| file.name == name)
355 .ok_or(Error::NotFound)?;
356 let FileData::Directory(child) = file.data else {
357 return Err(Error::Unsupported);
358 };
359 if file.ty != crate::types::FileType::Dir {
360 return Err(Error::Unsupported);
361 }
362 let child = MetadataPair::read(&self.image, self.cfg, child)?;
363 if !self.files_in_pair_chain(&child)?.is_empty() {
364 return Err(Error::Unsupported);
365 }
366 self.apply_root_edit(RootEdit::Delete { id: file.id })?;
367 Ok(self)
368 }
369
370 fn apply_root_edit(&mut self, edit: RootEdit) -> Result<()> {
371 let entries = edit.commit_entries()?;
372 match self.append_root_entries(&entries) {
373 Ok(()) => Ok(()),
374 Err(Error::NoSpace) => self.compact_root_with_edit(&edit),
375 Err(err) => Err(err),
376 }
377 }
378
379 fn append_root_entries(&mut self, entries: &[CommitEntry]) -> Result<()> {
380 self.append_pair_entries(&self.root.clone(), entries)?;
381 self.root = MetadataPair::read(&self.image, self.cfg, [0, 1])?;
382 Ok(())
383 }
384
385 fn append_pair_entries(&mut self, pair: &MetadataPair, entries: &[CommitEntry]) -> Result<()> {
386 let block = image_block_mut(&mut self.image, self.cfg, pair.active_block)?;
387 let mut commit = MetadataCommitWriter::append(block, METADATA_PROG_SIZE, pair.state)?;
388 commit.write_entries(entries)?;
389 commit.finish()?;
390 Ok(())
391 }
392
393 fn compact_root_with_edit(&mut self, edit: &RootEdit) -> Result<()> {
394 let mut entries = self.root_entries_for_compaction()?;
395 edit.apply_to_root_entries(&mut entries)?;
396
397 let alternate = if self.root.active_block == self.root.pair[0] {
398 self.root.pair[1]
399 } else {
400 self.root.pair[0]
401 };
402 {
403 let block = image_block_mut(&mut self.image, self.cfg, alternate)?;
404 block.fill(0xff);
405 let root = RootCommit::from_entries(self.cfg, FilesystemOptions::default(), &entries)?;
406 root.write_into_rev(
407 block,
408 self.cfg,
409 METADATA_PROG_SIZE,
410 self.root.rev.wrapping_add(1),
411 )?;
412 }
413
414 self.root = MetadataPair::read(&self.image, self.cfg, [0, 1])?;
415 Ok(())
416 }
417
418 fn root_entries_for_compaction(&self) -> Result<BTreeMap<String, RootEntry>> {
419 let mut entries = BTreeMap::new();
420 for file in self.root.files()? {
421 match file.data {
422 FileData::Inline(data) if file.ty == crate::types::FileType::File => {
423 entries.insert(
424 file.name,
425 RootEntry::File(InlineFile {
426 storage: FileStorage::Inline(data),
427 attrs: file.attrs,
428 }),
429 );
430 }
431 FileData::Ctz { head, size } if file.ty == crate::types::FileType::File => {
432 entries.insert(
433 file.name,
434 RootEntry::File(InlineFile {
435 storage: FileStorage::ExistingCtz { head, size },
436 attrs: file.attrs,
437 }),
438 );
439 }
440 FileData::Directory(pair) if file.ty == crate::types::FileType::Dir => {
441 entries.insert(
442 file.name,
443 RootEntry::Dir(Directory::new(
444 pair,
445 self.cfg,
446 FilesystemOptions::default(),
447 )),
448 );
449 }
450 _ => return Err(Error::Corrupt),
451 }
452 }
453 Ok(entries)
454 }
455
456 fn root_file_id(&self, path: &str) -> Result<u16> {
457 let parts = components(path)?;
458 let (name, parents) = split_parent(&parts)?;
459 if !parents.is_empty() {
460 return Err(Error::Unsupported);
461 }
462 self.root_file_id_from_name(name)
463 }
464
465 fn root_file_id_from_name(&self, name: &str) -> Result<u16> {
466 let file = self
467 .root
468 .files()?
469 .into_iter()
470 .find(|file| file.name == name)
471 .ok_or(Error::NotFound)?;
472 if file.ty != crate::types::FileType::File {
473 return Err(Error::Unsupported);
474 }
475 Ok(file.id)
476 }
477
478 fn resolve_directory(&self, path: &[&str]) -> Result<MetadataPair> {
479 let mut pair = self.root.clone();
480 for component in path {
481 let (dir_pair, file) = self.find_file_record_in_pair_chain(&pair, component)?;
482 match file.data {
483 FileData::Directory(child) if file.ty == crate::types::FileType::Dir => {
484 pair = MetadataPair::read(&self.image, self.cfg, child)?;
485 }
486 _ => {
487 let _ = dir_pair;
488 return Err(Error::Unsupported);
489 }
490 }
491 }
492 Ok(pair)
493 }
494
495 fn find_file_in_pair_chain(
496 &self,
497 pair: &MetadataPair,
498 name: &str,
499 ) -> Result<(MetadataPair, u16)> {
500 let (pair, file) = self.find_file_record_in_pair_chain(pair, name)?;
501 if file.ty != crate::types::FileType::File {
502 return Err(Error::Unsupported);
503 }
504 Ok((pair, file.id))
505 }
506
507 fn find_file_record_in_pair_chain(
508 &self,
509 pair: &MetadataPair,
510 name: &str,
511 ) -> Result<(MetadataPair, crate::metadata::FileRecord)> {
512 let mut current = pair.clone();
513 let mut seen = Vec::<[u32; 2]>::new();
514 loop {
515 if seen.contains(¤t.pair) {
516 return Err(Error::Corrupt);
517 }
518 seen.push(current.pair);
519 if let Some(file) = current.files()?.into_iter().find(|file| file.name == name) {
520 return Ok((current, file));
521 }
522 match current.hardtail()? {
523 Some(next) if next != [crate::format::LFS_NULL, crate::format::LFS_NULL] => {
524 current = MetadataPair::read(&self.image, self.cfg, next)?;
525 }
526 _ => return Err(Error::NotFound),
527 }
528 }
529 }
530
531 fn files_in_pair_chain(&self, pair: &MetadataPair) -> Result<Vec<crate::metadata::FileRecord>> {
532 let mut files = Vec::new();
533 let mut current = pair.clone();
534 let mut seen = Vec::<[u32; 2]>::new();
535 loop {
536 if seen.contains(¤t.pair) {
537 return Err(Error::Corrupt);
538 }
539 seen.push(current.pair);
540 files.extend(current.files()?);
541 match current.hardtail()? {
542 Some(next) if next != [crate::format::LFS_NULL, crate::format::LFS_NULL] => {
543 current = MetadataPair::read(&self.image, self.cfg, next)?;
544 }
545 _ => return Ok(files),
546 }
547 }
548 }
549
550 pub fn into_bytes(self) -> Vec<u8> {
551 self.image
552 }
553}