1use crate::names::{cmp_tree_names, validate_name};
2use core::cmp::Ordering;
3
4use crate::{
5 Error, Object, ObjectFormat, ObjectId, ObjectKind, Repository, Result,
6 io::{SliceWriter, Write},
7 repository::validate_refs,
8};
9
10pub const MAX_TREE_DEPTH: usize = 64;
12
13#[derive(Clone, Copy, Debug)]
14pub struct File<'a> {
15 pub name: &'a str,
17 pub contents: &'a [u8],
18}
19
20#[derive(Clone, Copy, Debug)]
22pub enum EntryKind<'a> {
23 File(&'a [u8]),
24 Executable(&'a [u8]),
25 Symlink(&'a [u8]),
26 Tree(&'a [Entry<'a>]),
27}
28
29impl EntryKind<'_> {
30 fn mode(self) -> &'static [u8] {
31 match self {
32 Self::File(_) => b"100644",
33 Self::Executable(_) => b"100755",
34 Self::Symlink(_) => b"120000",
35 Self::Tree(_) => b"40000",
36 }
37 }
38}
39
40#[derive(Clone, Copy, Debug)]
41pub struct Entry<'a> {
42 pub name: &'a str,
44 pub kind: EntryKind<'a>,
45}
46
47impl Entry<'_> {
48 pub const EMPTY: Self = Self {
50 name: "",
51 kind: EntryKind::File(&[]),
52 };
53
54 pub fn cmp_name(&self, other: &Self) -> Ordering {
56 cmp_tree_names(
57 self.name.as_bytes(),
58 matches!(self.kind, EntryKind::Tree(_)),
59 other.name.as_bytes(),
60 matches!(other.kind, EntryKind::Tree(_)),
61 )
62 }
63}
64
65#[derive(Clone, Copy, Debug)]
67pub struct CommitIdentity<'a> {
68 pub name: &'a str,
69 pub email: &'a str,
70 pub timestamp: u64,
72 pub timezone: &'a str,
74}
75
76impl Default for CommitIdentity<'_> {
77 fn default() -> Self {
78 Self {
79 name: "git-zero",
80 email: "git-zero@example.invalid",
81 timestamp: 0,
82 timezone: "+0000",
83 }
84 }
85}
86
87#[derive(Clone, Copy, Debug)]
91pub struct CommitOptions<'a> {
92 pub author: CommitIdentity<'a>,
93 pub committer: CommitIdentity<'a>,
94 pub message: &'a str,
95}
96
97impl Default for CommitOptions<'_> {
98 fn default() -> Self {
99 Self {
100 author: CommitIdentity::default(),
101 committer: CommitIdentity::default(),
102 message: "In-memory snapshot\n",
103 }
104 }
105}
106
107impl CommitOptions<'_> {
108 fn validate(self) -> Result<()> {
109 for identity in [self.author, self.committer] {
110 let timezone = identity.timezone.as_bytes();
111 if [identity.name, identity.email].iter().any(|value| {
112 value.trim().is_empty()
113 || value
114 .bytes()
115 .any(|byte| byte.is_ascii_control() || matches!(byte, b'<' | b'>'))
116 }) || identity.timestamp > i64::MAX as u64
117 || timezone.len() != 5
118 || !matches!(timezone[0], b'+' | b'-')
119 || !timezone[1..].iter().all(u8::is_ascii_digit)
120 || (timezone[1] - b'0') * 10 + timezone[2] - b'0' > 23
121 || (timezone[3] - b'0') * 10 + timezone[4] - b'0' > 59
122 {
123 return Err(Error::InvalidObject);
124 }
125 }
126 if self.message.as_bytes().contains(&0) {
127 return Err(Error::InvalidObject);
128 }
129 Ok(())
130 }
131}
132
133pub struct SnapshotBuilder<'storage, 'data> {
137 entries: &'storage mut [Entry<'data>],
138 length: usize,
139 tree_storage: &'storage mut [u8],
140 commit_storage: &'storage mut [u8],
141}
142
143impl<'storage, 'data: 'storage> SnapshotBuilder<'storage, 'data> {
144 pub fn new(
145 entries: &'storage mut [Entry<'data>],
146 tree_storage: &'storage mut [u8],
147 commit_storage: &'storage mut [u8],
148 ) -> Self {
149 Self {
150 entries,
151 length: 0,
152 tree_storage,
153 commit_storage,
154 }
155 }
156
157 pub fn len(&self) -> usize {
158 self.length
159 }
160
161 pub fn is_empty(&self) -> bool {
162 self.length == 0
163 }
164
165 pub fn push(&mut self, entry: Entry<'data>) -> Result<()> {
168 validate_name(entry.name.as_bytes())?;
169 let occupied = &self.entries[..self.length];
170 if occupied.iter().any(|previous| previous.name == entry.name) {
171 return Err(Error::UnsortedFiles);
172 }
173 if self.length == self.entries.len() {
174 return Err(Error::BufferTooSmall);
175 }
176 let position = occupied
177 .iter()
178 .position(|previous| entry.cmp_name(previous).is_lt())
179 .unwrap_or(self.length);
180 self.entries
181 .copy_within(position..self.length, position + 1);
182 self.entries[position] = entry;
183 self.length += 1;
184 Ok(())
185 }
186
187 pub fn finish(self) -> Result<Snapshot<'storage>> {
188 self.finish_with_commit(CommitOptions::default())
189 }
190
191 pub fn finish_with_commit(self, options: CommitOptions<'_>) -> Result<Snapshot<'storage>> {
194 self.finish_with_commit_and_format(options, ObjectFormat::Sha1)
195 }
196
197 pub fn finish_with_format(self, format: ObjectFormat) -> Result<Snapshot<'storage>> {
198 self.finish_with_commit_and_format(CommitOptions::default(), format)
199 }
200
201 pub fn finish_with_commit_and_format(
202 self,
203 options: CommitOptions<'_>,
204 format: ObjectFormat,
205 ) -> Result<Snapshot<'storage>> {
206 Snapshot::from_entries_with_commit_and_format(
207 &self.entries[..self.length],
208 self.tree_storage,
209 self.commit_storage,
210 options,
211 format,
212 )
213 }
214}
215
216#[derive(Clone, Copy)]
217enum Input<'a> {
218 Files(&'a [File<'a>]),
219 Entries(&'a [Entry<'a>]),
220}
221
222impl<'a> Input<'a> {
223 fn len(self) -> usize {
224 match self {
225 Self::Files(files) => files.len(),
226 Self::Entries(entries) => entries.len(),
227 }
228 }
229
230 fn entries(self) -> impl Iterator<Item = Entry<'a>> {
231 (0..self.len()).map(move |index| match self {
232 Self::Files(files) => Entry {
233 name: files[index].name,
234 kind: EntryKind::File(files[index].contents),
235 },
236 Self::Entries(entries) => entries[index],
237 })
238 }
239
240 fn blob_at(self, index: &mut usize) -> Option<&'a [u8]> {
241 if let Self::Files(files) = self {
242 return files.get(*index).map(|file| file.contents);
243 }
244 for entry in self.entries() {
245 match entry.kind {
246 EntryKind::Tree(children) => {
247 if let Some(blob) = Self::Entries(children).blob_at(index) {
248 return Some(blob);
249 }
250 }
251 EntryKind::File(data) | EntryKind::Executable(data) | EntryKind::Symlink(data) => {
252 if *index == 0 {
253 return Some(data);
254 }
255 *index -= 1;
256 }
257 }
258 }
259 None
260 }
261}
262
263struct Blobs<'a> {
264 input: Input<'a>,
265 index: usize,
266}
267
268impl<'a> Iterator for Blobs<'a> {
269 type Item = &'a [u8];
270
271 fn next(&mut self) -> Option<Self::Item> {
272 let mut index = self.index;
273 let result = self.input.blob_at(&mut index)?;
274 self.index += 1;
275 Some(result)
276 }
277}
278
279struct Trees<'a> {
282 bytes: &'a [u8],
283 framed: bool,
284 done: bool,
285}
286
287impl<'a> Iterator for Trees<'a> {
288 type Item = &'a [u8];
289
290 fn next(&mut self) -> Option<Self::Item> {
291 if self.done {
292 return None;
293 }
294 if !self.framed {
295 self.done = true;
296 return Some(self.bytes);
297 }
298 if self.bytes.is_empty() {
299 return None;
300 }
301 let length = u32::from_le_bytes(self.bytes[..4].try_into().ok()?) as usize;
302 let (tree, rest) = self.bytes[4..].split_at(length);
303 self.bytes = rest;
304 Some(tree)
305 }
306}
307
308pub struct Snapshot<'a> {
311 input: Input<'a>,
312 trees: &'a [u8],
313 framed: bool,
314 commit: &'a [u8],
315 head: ObjectId,
316 head_ref: &'a str,
317 refs: &'a [&'a str],
318}
319
320impl<'a> Snapshot<'a> {
321 fn blobs(&self) -> Blobs<'a> {
322 Blobs {
323 input: self.input,
324 index: 0,
325 }
326 }
327
328 fn trees(&self) -> Trees<'a> {
329 Trees {
330 bytes: self.trees,
331 framed: self.framed,
332 done: false,
333 }
334 }
335
336 fn unique_blobs(&self) -> impl Iterator<Item = &'a [u8]> + '_ {
337 self.blobs().enumerate().filter_map(|(index, data)| {
338 (!self.blobs().take(index).any(|previous| previous == data)).then_some(data)
339 })
340 }
341
342 fn unique_trees(&self) -> impl Iterator<Item = &'a [u8]> + '_ {
343 self.trees().enumerate().filter_map(|(index, data)| {
344 (!self.trees().take(index).any(|previous| previous == data)).then_some(data)
345 })
346 }
347
348 pub fn new(
351 files: &'a [File<'a>],
352 tree_storage: &'a mut [u8],
353 commit_storage: &'a mut [u8],
354 ) -> Result<Self> {
355 Self::new_with_commit(
356 files,
357 tree_storage,
358 commit_storage,
359 CommitOptions::default(),
360 )
361 }
362
363 pub fn new_with_commit(
365 files: &'a [File<'a>],
366 tree_storage: &'a mut [u8],
367 commit_storage: &'a mut [u8],
368 options: CommitOptions<'_>,
369 ) -> Result<Self> {
370 Self::new_with_commit_and_format(
371 files,
372 tree_storage,
373 commit_storage,
374 options,
375 ObjectFormat::Sha1,
376 )
377 }
378
379 pub fn new_with_format(
380 files: &'a [File<'a>],
381 tree_storage: &'a mut [u8],
382 commit_storage: &'a mut [u8],
383 format: ObjectFormat,
384 ) -> Result<Self> {
385 Self::new_with_commit_and_format(
386 files,
387 tree_storage,
388 commit_storage,
389 CommitOptions::default(),
390 format,
391 )
392 }
393
394 pub fn new_with_commit_and_format(
395 files: &'a [File<'a>],
396 tree_storage: &'a mut [u8],
397 commit_storage: &'a mut [u8],
398 options: CommitOptions<'_>,
399 format: ObjectFormat,
400 ) -> Result<Self> {
401 Self::build(
402 Input::Files(files),
403 tree_storage,
404 commit_storage,
405 false,
406 options,
407 format,
408 )
409 }
410
411 pub fn from_entries(
418 entries: &'a [Entry<'a>],
419 tree_storage: &'a mut [u8],
420 commit_storage: &'a mut [u8],
421 ) -> Result<Self> {
422 Self::from_entries_with_commit(
423 entries,
424 tree_storage,
425 commit_storage,
426 CommitOptions::default(),
427 )
428 }
429
430 pub fn from_entries_with_commit(
433 entries: &'a [Entry<'a>],
434 tree_storage: &'a mut [u8],
435 commit_storage: &'a mut [u8],
436 options: CommitOptions<'_>,
437 ) -> Result<Self> {
438 Self::from_entries_with_commit_and_format(
439 entries,
440 tree_storage,
441 commit_storage,
442 options,
443 ObjectFormat::Sha1,
444 )
445 }
446
447 pub fn from_entries_with_format(
448 entries: &'a [Entry<'a>],
449 tree_storage: &'a mut [u8],
450 commit_storage: &'a mut [u8],
451 format: ObjectFormat,
452 ) -> Result<Self> {
453 Self::from_entries_with_commit_and_format(
454 entries,
455 tree_storage,
456 commit_storage,
457 CommitOptions::default(),
458 format,
459 )
460 }
461
462 pub fn from_entries_with_commit_and_format(
463 entries: &'a [Entry<'a>],
464 tree_storage: &'a mut [u8],
465 commit_storage: &'a mut [u8],
466 options: CommitOptions<'_>,
467 format: ObjectFormat,
468 ) -> Result<Self> {
469 Self::build(
470 Input::Entries(entries),
471 tree_storage,
472 commit_storage,
473 true,
474 options,
475 format,
476 )
477 }
478
479 fn build(
480 input: Input<'a>,
481 tree_storage: &'a mut [u8],
482 commit_storage: &'a mut [u8],
483 framed: bool,
484 options: CommitOptions<'_>,
485 format: ObjectFormat,
486 ) -> Result<Self> {
487 options.validate()?;
488 let length = write_tree(input, tree_storage, 0, framed, format)?;
489 let trees = &tree_storage[..length];
490 let root = Trees {
491 bytes: trees,
492 framed,
493 done: false,
494 }
495 .next()
496 .ok_or(Error::ObjectNotFound)?;
497 let tree_id = Object {
498 kind: ObjectKind::Tree,
499 data: root,
500 }
501 .id_with_format(format);
502 let mut commit = SliceWriter::new(commit_storage);
503 commit.write_all(b"tree ")?;
504 commit.write_all(&tree_id.to_hex())?;
505 commit.write_all(b"\n")?;
506 write_identity(&mut commit, b"author", options.author)?;
507 write_identity(&mut commit, b"committer", options.committer)?;
508 commit.write_all(b"\n")?;
509 commit.write_all(options.message.as_bytes())?;
510 let commit = commit.finish();
511 let head = Object {
512 kind: ObjectKind::Commit,
513 data: commit,
514 }
515 .id_with_format(format);
516 Ok(Self {
517 input,
518 trees,
519 framed,
520 commit,
521 head,
522 head_ref: "refs/heads/main",
523 refs: &["refs/heads/main"],
524 })
525 }
526
527 pub fn with_refs(mut self, head_ref: &'a str, refs: &'a [&'a str]) -> Result<Self> {
532 self.head_ref = head_ref;
533 self.refs = refs;
534 validate_refs(&self)?;
535 Ok(self)
536 }
537}
538
539fn write_identity(
540 output: &mut SliceWriter<'_>,
541 header: &[u8],
542 identity: CommitIdentity<'_>,
543) -> Result<()> {
544 use core::fmt::Write as _;
545 output.write_all(header)?;
546 output.write_all(b" ")?;
547 output.write_all(identity.name.as_bytes())?;
548 output.write_all(b" <")?;
549 output.write_all(identity.email.as_bytes())?;
550 writeln!(output, "> {} {}", identity.timestamp, identity.timezone)
551 .map_err(|_| Error::BufferTooSmall)
552}
553
554fn write_tree(
555 input: Input<'_>,
556 storage: &mut [u8],
557 depth: usize,
558 framed: bool,
559 format: ObjectFormat,
560) -> Result<usize> {
561 if depth >= MAX_TREE_DEPTH {
562 return Err(Error::Unsupported);
563 }
564 let mut length = 0_usize;
565 let mut previous: Option<Entry<'_>> = None;
566 for (index, entry) in input.entries().enumerate() {
567 validate_name(entry.name.as_bytes())?;
568 if previous.is_some_and(|previous| previous.cmp_name(&entry) != Ordering::Less)
569 || input
570 .entries()
571 .take(index)
572 .any(|previous| previous.name == entry.name)
573 {
574 return Err(Error::UnsortedFiles);
575 }
576 previous = Some(entry);
577 length = length
578 .checked_add(entry.kind.mode().len())
579 .and_then(|length| length.checked_add(entry.name.len()))
580 .and_then(|length| length.checked_add(2 + format.id_length()))
581 .ok_or(Error::BufferTooSmall)?;
582 }
583 let prefix = if framed { 4 } else { 0 };
584 let record_length = length.checked_add(prefix).ok_or(Error::BufferTooSmall)?;
585 if record_length > storage.len() {
586 return Err(Error::BufferTooSmall);
587 }
588 let (record, children_storage) = storage.split_at_mut(record_length);
589 if framed {
590 record[..4].copy_from_slice(
591 &u32::try_from(length)
592 .map_err(|_| Error::BufferTooSmall)?
593 .to_le_bytes(),
594 );
595 }
596 let mut tree = SliceWriter::new(&mut record[prefix..]);
597 let mut children_length = 0;
598 for entry in input.entries() {
599 let object = match entry.kind {
600 EntryKind::Tree(children) => {
601 let storage = &mut children_storage[children_length..];
602 let length =
603 write_tree(Input::Entries(children), storage, depth + 1, true, format)?;
604 children_length += length;
605 Object {
606 kind: ObjectKind::Tree,
607 data: Trees {
608 bytes: &storage[..length],
609 framed: true,
610 done: false,
611 }
612 .next()
613 .ok_or(Error::ObjectNotFound)?,
614 }
615 }
616 EntryKind::File(data) | EntryKind::Executable(data) | EntryKind::Symlink(data) => {
617 Object {
618 kind: ObjectKind::Blob,
619 data,
620 }
621 }
622 };
623 tree.write_all(entry.kind.mode())?;
624 tree.write_all(b" ")?;
625 tree.write_all(entry.name.as_bytes())?;
626 tree.write_all(&[0])?;
627 tree.write_all(object.id_with_format(format).as_bytes())?;
628 }
629 Ok(record_length + children_length)
630}
631
632impl Repository for Snapshot<'_> {
633 fn object_format(&self) -> ObjectFormat {
634 self.head.format()
635 }
636 fn head(&self) -> ObjectId {
637 self.head
638 }
639
640 fn head_ref(&self) -> &str {
641 self.head_ref
642 }
643
644 fn reference_count(&self) -> usize {
645 self.refs.len()
646 }
647
648 fn reference_name(&self, index: usize) -> Option<&str> {
649 self.refs.get(index).copied()
650 }
651
652 fn object_count(&self) -> usize {
653 1 + self.unique_trees().count() + self.unique_blobs().count()
654 }
655
656 fn object(&self, index: usize) -> Option<Object<'_>> {
657 if index == 0 {
658 return Some(Object {
659 kind: ObjectKind::Commit,
660 data: self.commit,
661 });
662 }
663 if let Some(data) = self.unique_trees().nth(index - 1) {
664 return Some(Object {
665 kind: ObjectKind::Tree,
666 data,
667 });
668 }
669 self.unique_blobs()
670 .nth(index - 1 - self.unique_trees().count())
671 .map(|data| Object {
672 kind: ObjectKind::Blob,
673 data,
674 })
675 }
676}