1use std::fs::{self, File};
4use std::io::{self, Read, Write};
5use std::ops::ControlFlow;
6use std::path::{Path, PathBuf};
7use std::sync::atomic::{AtomicU64, Ordering};
8
9use flate2::write::ZlibEncoder;
10use flate2::Compression;
11use flate2::{Decompress, FlushDecompress, Status};
12
13use super::{BoundedRead, ObjectStore, ObjectStream, WritableObjectStore};
14use crate::error::{Error, Result};
15use crate::git_dir::io_error::{map_objects_dir_io, GitDirPathOperation};
16use crate::hash::{self, ObjectPayloadHasher};
17use crate::objects::{HashAlgo, Object, ObjectId, ObjectIdPrefix, ObjectInfo, ObjectKind};
18use crate::odb::WriteOptions;
19use crate::zlib_inflate::with_loose_scratch;
20
21const READ_CHUNK: usize = 64 * 1024;
22const STREAM_CHUNK: usize = 128 * 1024;
24
25#[derive(Debug, Clone)]
27pub struct LooseStore {
28 objects_dir: PathBuf,
29 hash_algo: HashAlgo,
30 compression: Compression,
31}
32
33impl LooseStore {
34 #[must_use]
36 pub fn new(objects_dir: PathBuf, hash_algo: HashAlgo, compression: Compression) -> Self {
37 Self {
38 objects_dir,
39 hash_algo,
40 compression,
41 }
42 }
43
44 #[must_use]
46 pub fn objects_dir(&self) -> &Path {
47 &self.objects_dir
48 }
49
50 #[must_use]
52 pub fn object_path(&self, oid: &ObjectId) -> PathBuf {
53 oid.loose_path_in(&self.objects_dir)
54 }
55
56 pub fn ensure_all_loose_prefix_dirs(&self) -> Result<()> {
62 for i in 0u8..=255 {
63 let prefix = self.objects_dir.join(format!("{i:02x}"));
64 fs::create_dir_all(&prefix).map_err(|source| {
65 map_objects_dir_io(
66 &self.objects_dir,
67 &prefix,
68 GitDirPathOperation::CreateDir,
69 source,
70 )
71 })?;
72 }
73 Ok(())
74 }
75
76 pub fn zlib_compress_store_bytes(&self, store_bytes: &[u8]) -> Result<Vec<u8>> {
82 zlib_compress_store_bytes(store_bytes, self.compression)
83 }
84
85 pub(crate) fn try_finish_if_loose_present(
87 &self,
88 oid: &ObjectId,
89 options: WriteOptions,
90 ) -> Result<Option<ObjectId>> {
91 let path = self.object_path(oid);
92 if path.is_file() {
93 if !options.silent {
94 let _ = self.freshen(oid)?;
95 }
96 return Ok(Some(*oid));
97 }
98 Ok(None)
99 }
100
101 pub fn write_store_prehashed(
107 &self,
108 oid: &ObjectId,
109 store_bytes: &[u8],
110 options: WriteOptions,
111 ) -> Result<ObjectId> {
112 parse_object_bytes(store_bytes)?;
113 if oid.algo() != self.hash_algo {
114 return Err(Error::InvalidObjectId(oid.to_hex()));
115 }
116 if let Some(existing) = self.try_finish_if_loose_present(oid, options)? {
117 return Ok(existing);
118 }
119 let path = self.object_path(oid);
120 let prefix_dir = path
121 .parent()
122 .ok_or_else(|| Error::PathError("object path has no parent".to_owned()))?;
123 let zlib = zlib_compress_store_bytes(store_bytes, self.compression)?;
124 publish_loose_object(&self.objects_dir, &path, prefix_dir, false, |mut file| {
125 file.write_all(&zlib).map_err(|source| {
126 map_objects_dir_io(
127 &self.objects_dir,
128 &path,
129 GitDirPathOperation::WriteObject,
130 source,
131 )
132 })
133 })?;
134 Ok(*oid)
135 }
136
137 pub fn write_zlib_prehashed(
143 &self,
144 oid: &ObjectId,
145 zlib_store: &[u8],
146 options: WriteOptions,
147 ) -> Result<ObjectId> {
148 if oid.algo() != self.hash_algo {
149 return Err(Error::InvalidObjectId(oid.to_hex()));
150 }
151 if let Some(existing) = self.try_finish_if_loose_present(oid, options)? {
152 return Ok(existing);
153 }
154 let path = self.object_path(oid);
155 let prefix_dir = path
156 .parent()
157 .ok_or_else(|| Error::PathError("object path has no parent".to_owned()))?;
158 publish_loose_object(
159 &self.objects_dir,
160 &path,
161 prefix_dir,
162 options.loose_prefix_dirs_ready,
163 |mut file| {
164 file.write_all(zlib_store).map_err(|source| {
165 map_objects_dir_io(
166 &self.objects_dir,
167 &path,
168 GitDirPathOperation::WriteObject,
169 source,
170 )
171 })
172 },
173 )?;
174 Ok(*oid)
175 }
176
177 pub(crate) fn write_prehashed(
185 &self,
186 oid: &ObjectId,
187 kind: ObjectKind,
188 data: &[u8],
189 options: WriteOptions,
190 ) -> Result<ObjectId> {
191 if oid.algo() != self.hash_algo {
192 return Err(Error::InvalidObjectId(oid.to_hex()));
193 }
194 if let Some(existing) = self.try_finish_if_loose_present(oid, options)? {
195 return Ok(existing);
196 }
197 let path = self.object_path(oid);
198 let prefix_dir = path
199 .parent()
200 .ok_or_else(|| Error::PathError("object path has no parent".to_owned()))?;
201 publish_loose_object(
202 &self.objects_dir,
203 &path,
204 prefix_dir,
205 options.loose_prefix_dirs_ready,
206 |file| deflate_object_into(file, kind, data, self.compression),
207 )?;
208 Ok(*oid)
209 }
210
211 pub fn read_verify_oid(path: &Path, expected_oid: &ObjectId) -> Result<Object> {
219 let file = fs::File::open(path).map_err(Error::Io)?;
220 let raw = read_zlib_loose_payload(file)?;
221 let obj = parse_object_bytes_with_oid(&raw, expected_oid)?;
222 let computed = hash::hash_object(expected_oid.algo(), obj.kind, &obj.data)?;
223 if computed != *expected_oid {
224 return Err(Error::LooseHashMismatch {
225 path: path.display().to_string(),
226 real_oid: computed.to_hex(),
227 });
228 }
229 Ok(obj)
230 }
231
232 pub(crate) fn read_unless_larger(
242 &self,
243 oid: &ObjectId,
244 limit: u64,
245 ) -> Result<Option<BoundedRead>> {
246 let mut file = match fs::File::open(self.object_path(oid)) {
247 Ok(f) => f,
248 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
249 Err(err) => return Err(Error::Io(err)),
250 };
251 let mut buf = Vec::new();
252 let complete =
253 with_loose_scratch(|s| s.inflate_loose_file_unless_larger(&mut file, &mut buf, limit))?;
254 if !complete {
255 return Ok(Some(BoundedRead::Larger));
256 }
257 let kind = strip_decompressed_object_header(&mut buf, Some(oid))?;
258 Ok(Some(BoundedRead::Object(Object::new(kind, buf))))
259 }
260
261 fn touch_mtime(path: &Path) -> bool {
262 let now = filetime::FileTime::now();
263 filetime::set_file_times(path, now, now).is_ok()
264 }
265}
266
267impl ObjectStore for LooseStore {
268 fn hash_algo(&self) -> HashAlgo {
269 self.hash_algo
270 }
271
272 fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
273 let mut data = Vec::new();
274 Ok(self
275 .read_into(oid, &mut data)?
276 .map(|kind| Object::new(kind, data)))
277 }
278
279 fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
280 let path = self.object_path(oid);
281 let file = match fs::File::open(&path) {
282 Ok(f) => f,
283 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
284 Err(err) => return Err(Error::Io(err)),
285 };
286 read_zlib_loose_payload_into(file, buf)?;
287 let kind = strip_decompressed_object_header(buf, Some(oid))?;
288 Ok(Some(kind))
289 }
290
291 fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
292 let path = self.object_path(oid);
293 let mut file = match fs::File::open(&path) {
294 Ok(f) => f,
295 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
296 Err(err) => return Err(Error::Io(err)),
297 };
298 match loose_object_info_from_file(&mut file) {
299 Err(Error::Io(err)) if err.kind() == io::ErrorKind::IsADirectory => Ok(None),
300 other => other.map(Some),
301 }
302 }
303
304 fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'_>>> {
305 let path = self.object_path(oid);
306 let file = match fs::File::open(&path) {
307 Ok(f) => f,
308 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
309 Err(err) => return Err(Error::Io(err)),
310 };
311 let reader = LoosePayloadReader::open(file)?;
312 Ok(Some(ObjectStream {
313 kind: reader.kind,
314 size: reader.payload_size,
315 reader: Box::new(reader),
316 }))
317 }
318
319 fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
320 for_each_loose_object_id(&self.objects_dir, self.hash_algo, f)
321 }
322
323 fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
324 let prefix = ObjectIdPrefix::from_hex(prefix, self.hash_algo)?;
325 let start_len = out.len();
326 let mut collect = |oid: &ObjectId| {
327 if prefix.matches(oid) {
328 out.push(*oid);
329 if limit != 0 && out.len() - start_len >= limit {
330 return ControlFlow::Break(());
331 }
332 }
333 ControlFlow::Continue(())
334 };
335 if prefix.nibbles() < 2 {
336 return for_each_loose_object_id(&self.objects_dir, self.hash_algo, &mut collect);
337 }
338 let fanout = format!("{:02x}", prefix.lowest().as_bytes()[0]);
339 let fanout_dir = self.objects_dir.join(&fanout);
340 for_each_object_id_in_fanout_dir(&fanout_dir, &fanout, self.hash_algo, &mut collect)
341 .map(drop)
342 }
343}
344
345impl WritableObjectStore for LooseStore {
346 fn write(&self, kind: ObjectKind, data: &[u8], options: WriteOptions) -> Result<ObjectId> {
347 let oid = hash::hash_object(self.hash_algo, kind, data)?;
348 if let Some(existing) = self.try_finish_if_loose_present(&oid, options)? {
349 return Ok(existing);
350 }
351 let path = self.object_path(&oid);
352 let prefix_dir = path
353 .parent()
354 .ok_or_else(|| Error::PathError("object path has no parent".to_owned()))?;
355 publish_loose_object(&self.objects_dir, &path, prefix_dir, false, |file| {
356 deflate_object_into(file, kind, data, self.compression)
357 })?;
358 Ok(oid)
359 }
360
361 fn write_stream(
362 &self,
363 kind: ObjectKind,
364 size: u64,
365 reader: &mut dyn Read,
366 options: WriteOptions,
367 ) -> Result<ObjectId> {
368 self.write_payload_stream(kind, size, reader, options)
369 }
370
371 fn freshen(&self, oid: &ObjectId) -> Result<bool> {
372 let path = self.object_path(oid);
373 if path.is_file() {
374 return Ok(LooseStore::touch_mtime(&path));
375 }
376 Ok(false)
377 }
378}
379
380pub fn enumerate_loose_objects(
382 objects_dir: &Path,
383 hash_algo: HashAlgo,
384) -> Result<Vec<(ObjectId, PathBuf)>> {
385 let mut out = Vec::new();
386 for_each_loose_object_id(objects_dir, hash_algo, &mut |oid| {
387 let path = oid.loose_path_in(objects_dir);
388 out.push((*oid, path));
389 ControlFlow::Continue(())
390 })?;
391 Ok(out)
392}
393
394pub fn for_each_loose_object_id(
396 objects_dir: &Path,
397 hash_algo: HashAlgo,
398 f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
399) -> Result<()> {
400 let top = match fs::read_dir(objects_dir) {
401 Ok(rd) => rd,
402 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
403 Err(e) => return Err(Error::Io(e)),
404 };
405
406 for top_entry in top {
407 let top_entry = top_entry.map_err(Error::Io)?;
408 let name = top_entry.file_name();
409 let Some(prefix) = name.to_str() else {
410 continue;
411 };
412 if prefix.len() != 2 || !prefix.bytes().all(|b| b.is_ascii_hexdigit()) {
413 continue;
414 }
415 if !top_entry.file_type().map_err(Error::Io)?.is_dir() {
416 continue;
417 }
418 if for_each_object_id_in_fanout_dir(&top_entry.path(), prefix, hash_algo, f)?.is_break() {
419 return Ok(());
420 }
421 }
422 Ok(())
423}
424
425fn for_each_object_id_in_fanout_dir(
428 fanout_dir: &Path,
429 fanout: &str,
430 hash_algo: HashAlgo,
431 f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
432) -> Result<ControlFlow<()>> {
433 let sub = match fs::read_dir(fanout_dir) {
434 Ok(rd) => rd,
435 Err(e)
436 if matches!(
437 e.kind(),
438 io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
439 ) =>
440 {
441 return Ok(ControlFlow::Continue(()));
442 }
443 Err(e) => return Err(Error::Io(e)),
444 };
445 for sub_entry in sub {
446 let sub_entry = sub_entry.map_err(Error::Io)?;
447 if !sub_entry.file_type().map_err(Error::Io)?.is_file() {
448 continue;
449 }
450 let suffix_name = sub_entry.file_name();
451 let Some(suffix) = suffix_name.to_str() else {
452 continue;
453 };
454 if !is_loose_suffix_for_algo(suffix, hash_algo) {
455 continue;
456 }
457 let hex = format!("{fanout}{suffix}");
458 let Ok(oid) = ObjectId::from_hex(&hex) else {
459 continue;
460 };
461 if f(&oid).is_break() {
462 return Ok(ControlFlow::Break(()));
463 }
464 }
465 Ok(ControlFlow::Continue(()))
466}
467
468pub fn read_zlib_loose_payload(file: File) -> Result<Vec<u8>> {
470 let mut out = Vec::new();
471 read_zlib_loose_payload_into(file, &mut out)?;
472 Ok(out)
473}
474
475pub(crate) fn read_zlib_loose_payload_into(mut file: File, buf: &mut Vec<u8>) -> Result<()> {
477 with_loose_scratch(|scratch| scratch.inflate_loose_file(&mut file, buf))
478}
479
480pub(crate) fn strip_decompressed_object_header(
482 buf: &mut Vec<u8>,
483 oid_hint: Option<&ObjectId>,
484) -> Result<ObjectKind> {
485 let nul = buf
486 .iter()
487 .position(|&b| b == 0)
488 .ok_or_else(|| Error::CorruptObject("missing NUL in object header".to_owned()))?;
489 let (kind, size) = parse_object_header_prefix(buf)?;
490 let payload_start = nul + 1;
491 let size_usize = usize::try_from(size)
492 .map_err(|_| Error::CorruptObject(format!("invalid object size: {size}")))?;
493 if buf.len().saturating_sub(payload_start) != size_usize {
494 let oid_str = oid_hint.map(ObjectId::to_hex).unwrap_or_else(|| {
495 hash_algo_digest_hex(HashAlgo::Sha1, buf).unwrap_or_else(|_| "?".to_owned())
496 });
497 return Err(Error::CorruptObject(format!(
498 "object {oid_str} size mismatch: header says {size}, got {}",
499 buf.len().saturating_sub(payload_start)
500 )));
501 }
502 if payload_start > 0 {
503 buf.copy_within(payload_start.., 0);
504 }
505 buf.truncate(size_usize);
506 Ok(kind)
507}
508
509pub fn read_loose_object_info(path: &Path) -> Result<ObjectInfo> {
511 let mut file = fs::File::open(path).map_err(Error::Io)?;
512 loose_object_info_from_file(&mut file)
513}
514
515fn loose_object_info_from_file(file: &mut File) -> Result<ObjectInfo> {
516 let prefix = with_loose_scratch(|scratch| scratch.inflate_loose_header(file))?;
517 let (kind, size) = parse_object_header_prefix(&prefix)?;
518 Ok(ObjectInfo { kind, size })
519}
520
521pub(crate) fn parse_object_bytes(raw: &[u8]) -> Result<Object> {
523 parse_object_bytes_inner(raw, None)
524}
525
526pub(crate) fn parse_object_bytes_with_oid(raw: &[u8], oid: &ObjectId) -> Result<Object> {
527 parse_object_bytes_inner(raw, Some(oid))
528}
529
530pub(crate) fn zlib_compress_store_bytes(
531 store_bytes: &[u8],
532 compression: Compression,
533) -> Result<Vec<u8>> {
534 let mut out = Vec::new();
535 let mut encoder = ZlibEncoder::new(&mut out, compression);
536 encoder
537 .write_all(store_bytes)
538 .map_err(|e| Error::Zlib(e.to_string()))?;
539 encoder.finish().map_err(|e| Error::Zlib(e.to_string()))?;
540 Ok(out)
541}
542
543pub(crate) fn decompress_zlib_loose_bytes(zlib: &[u8]) -> Result<Vec<u8>> {
544 let mut decoder = flate2::read::ZlibDecoder::new(zlib);
545 let mut raw = Vec::new();
546 decoder
547 .read_to_end(&mut raw)
548 .map_err(|e| Error::Zlib(e.to_string()))?;
549 Ok(raw)
550}
551
552pub(crate) fn parse_object_header_prefix(raw: &[u8]) -> Result<(ObjectKind, u64)> {
554 let nul = raw
555 .iter()
556 .position(|&b| b == 0)
557 .ok_or_else(|| Error::CorruptObject("missing NUL in object header".to_owned()))?;
558
559 let header = &raw[..nul];
560 let sp = header
561 .iter()
562 .position(|&b| b == b' ')
563 .ok_or_else(|| Error::CorruptObject("missing space in object header".to_owned()))?;
564
565 if sp > 32 {
566 return Err(Error::ObjectHeaderTooLong {
567 oid: hash_algo_digest_hex(HashAlgo::Sha1, raw).unwrap_or_else(|_| "?".to_owned()),
568 });
569 }
570
571 let kind = ObjectKind::from_bytes(&header[..sp])?;
572 let size_str = std::str::from_utf8(&header[sp + 1..])
573 .map_err(|_| Error::CorruptObject("non-UTF-8 object size".to_owned()))?;
574 let size: u64 = size_str
575 .parse()
576 .map_err(|_| Error::CorruptObject(format!("invalid object size: {size_str}")))?;
577 Ok((kind, size))
578}
579
580fn parse_object_bytes_inner(raw: &[u8], oid_hint: Option<&ObjectId>) -> Result<Object> {
581 let nul = raw
582 .iter()
583 .position(|&b| b == 0)
584 .ok_or_else(|| Error::CorruptObject("missing NUL in object header".to_owned()))?;
585
586 let header = &raw[..nul];
587 let data = raw[nul + 1..].to_vec();
588
589 let sp = header
590 .iter()
591 .position(|&b| b == b' ')
592 .ok_or_else(|| Error::CorruptObject("missing space in object header".to_owned()))?;
593
594 if sp > 32 {
595 let oid_str = oid_hint.map(|o| o.to_hex()).unwrap_or_else(|| {
596 hash_algo_digest_hex(HashAlgo::Sha1, raw).unwrap_or_else(|_| "?".to_owned())
597 });
598 return Err(Error::ObjectHeaderTooLong { oid: oid_str });
599 }
600
601 let kind = ObjectKind::from_bytes(&header[..sp])?;
602
603 let size_str = std::str::from_utf8(&header[sp + 1..])
604 .map_err(|_| Error::CorruptObject("non-UTF-8 object size".to_owned()))?;
605 let size: usize = size_str
606 .parse()
607 .map_err(|_| Error::CorruptObject(format!("invalid object size: {size_str}")))?;
608
609 if data.len() != size {
610 return Err(Error::CorruptObject(format!(
611 "object size mismatch: header says {size} but got {}",
612 data.len()
613 )));
614 }
615
616 Ok(Object::new(kind, data))
617}
618
619fn hash_algo_digest_hex(algo: HashAlgo, data: &[u8]) -> Result<String> {
620 Ok(algo.digest(data)?.to_hex())
621}
622
623static LOOSE_TMP_SEQ: AtomicU64 = AtomicU64::new(0);
626
627fn unique_loose_tmp_path(prefix_dir: &Path) -> PathBuf {
630 let seq = LOOSE_TMP_SEQ.fetch_add(1, Ordering::Relaxed);
631 prefix_dir.join(format!("tmp_obj_{}_{seq}", std::process::id()))
632}
633
634fn deflate_object_into(
636 file: File,
637 kind: ObjectKind,
638 data: &[u8],
639 compression: Compression,
640) -> Result<()> {
641 let zlib_err = |e: io::Error| Error::Zlib(e.to_string());
642 let mut encoder = ZlibEncoder::new(file, compression);
643 write!(encoder, "{} {}\0", kind, data.len()).map_err(zlib_err)?;
644 encoder.write_all(data).map_err(zlib_err)?;
645 encoder.finish().map_err(zlib_err)?;
646 Ok(())
647}
648
649fn read_payload_chunks(
650 reader: &mut dyn Read,
651 size: u64,
652 mut sink: impl FnMut(&[u8]) -> Result<()>,
653) -> Result<()> {
654 let mut remaining = size;
655 let mut buf = [0u8; STREAM_CHUNK];
656 while remaining > 0 {
657 let want = usize::try_from(remaining.min(STREAM_CHUNK as u64)).map_err(|_| {
658 Error::CorruptObject(format!("object size {size} exceeds address space"))
659 })?;
660 let n = match reader.read(&mut buf[..want]) {
661 Ok(0) => {
662 return Err(Error::ObjectStreamTruncated {
663 expected: size,
664 actual: size - remaining,
665 })
666 }
667 Ok(n) => n,
668 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
669 Err(e) => return Err(Error::Io(e)),
670 };
671 sink(&buf[..n])?;
672 remaining -= n as u64;
673 }
674 Ok(())
675}
676
677impl LooseStore {
678 pub(crate) fn write_payload_stream(
682 &self,
683 kind: ObjectKind,
684 size: u64,
685 reader: &mut dyn Read,
686 options: WriteOptions,
687 ) -> Result<ObjectId> {
688 self.write_payload_stream_with(kind, size, options, |chunk_sink| {
689 read_payload_chunks(reader, size, chunk_sink)
690 })
691 }
692
693 pub(crate) fn write_payload_stream_with(
695 &self,
696 kind: ObjectKind,
697 size: u64,
698 options: WriteOptions,
699 mut supply: impl FnMut(&mut dyn FnMut(&[u8]) -> Result<()>) -> Result<()>,
700 ) -> Result<ObjectId> {
701 let prefix_dir = &self.objects_dir;
702 let mut hasher = ObjectPayloadHasher::new(self.hash_algo, kind, size);
703 let tmp_path = unique_loose_tmp_path(prefix_dir);
704 fs::create_dir_all(prefix_dir).map_err(Error::Io)?;
705 let file = fs::OpenOptions::new()
706 .write(true)
707 .create_new(true)
708 .open(&tmp_path)
709 .map_err(Error::Io)?;
710 let zlib_err = |e: io::Error| Error::Zlib(e.to_string());
711 let mut encoder = ZlibEncoder::new(file, self.compression);
712 if let Err(e) = (|| {
713 write!(encoder, "{} {}\0", kind, size).map_err(zlib_err)?;
714 supply(&mut |chunk| {
715 hasher.update(chunk);
716 encoder.write_all(chunk).map_err(zlib_err)
717 })?;
718 encoder.finish().map_err(zlib_err)?;
719 Ok::<(), Error>(())
720 })() {
721 let _ = fs::remove_file(&tmp_path);
722 return Err(e);
723 }
724 let oid = match hasher.finish() {
725 Ok(oid) => oid,
726 Err(e) => {
727 let _ = fs::remove_file(&tmp_path);
728 return Err(e);
729 }
730 };
731 if let Some(existing) = self.try_finish_if_loose_present(&oid, options)? {
732 let _ = fs::remove_file(&tmp_path);
733 return Ok(existing);
734 }
735 let path = self.object_path(&oid);
736 let dest_prefix = path
737 .parent()
738 .ok_or_else(|| Error::PathError("object path has no parent".to_owned()))?;
739 if let Err(e) = fs::create_dir_all(dest_prefix) {
740 let _ = fs::remove_file(&tmp_path);
741 return Err(Error::Io(e));
742 }
743 match fs::rename(&tmp_path, &path) {
744 Ok(()) => {
745 set_loose_object_mode(&path);
746 Ok(oid)
747 }
748 Err(e) => {
749 let _ = fs::remove_file(&tmp_path);
750 if path.is_file() {
751 return Ok(oid);
752 }
753 Err(Error::Io(e))
754 }
755 }
756 }
757}
758
759fn publish_loose_object(
772 objects_dir: &Path,
773 path: &Path,
774 prefix_dir: &Path,
775 prefix_dir_ready: bool,
776 mut fill: impl FnMut(File) -> Result<()>,
777) -> Result<()> {
778 const MAX_ATTEMPTS: u32 = 4;
779 let mut attempt = 0;
780 loop {
781 attempt += 1;
782 let can_retry = attempt < MAX_ATTEMPTS;
783 if !prefix_dir_ready || attempt > 1 {
784 fs::create_dir_all(prefix_dir).map_err(|source| {
785 map_objects_dir_io(
786 objects_dir,
787 prefix_dir,
788 GitDirPathOperation::CreateDir,
789 source,
790 )
791 })?;
792 }
793 let tmp_path = unique_loose_tmp_path(prefix_dir);
794 let file = match fs::OpenOptions::new()
795 .write(true)
796 .create_new(true)
797 .open(&tmp_path)
798 {
799 Ok(file) => file,
800 Err(e)
801 if can_retry
802 && matches!(
803 e.kind(),
804 io::ErrorKind::NotFound | io::ErrorKind::AlreadyExists
805 ) =>
806 {
807 continue;
808 }
809 Err(e) => {
810 return Err(map_objects_dir_io(
811 objects_dir,
812 &tmp_path,
813 GitDirPathOperation::WriteObject,
814 e,
815 ));
816 }
817 };
818 if let Err(e) = fill(file) {
819 let _ = fs::remove_file(&tmp_path);
820 return Err(e);
821 }
822 match fs::rename(&tmp_path, path) {
823 Ok(()) => {
824 set_loose_object_mode(path);
825 return Ok(());
826 }
827 Err(e) => {
828 let _ = fs::remove_file(&tmp_path);
829 if path.is_file() {
830 return Ok(());
831 }
832 if can_retry && e.kind() == io::ErrorKind::NotFound {
833 continue;
834 }
835 return Err(map_objects_dir_io(
836 objects_dir,
837 path,
838 GitDirPathOperation::WriteObject,
839 e,
840 ));
841 }
842 }
843 }
844}
845
846fn set_loose_object_mode(path: &Path) {
847 #[cfg(unix)]
848 {
849 use std::os::unix::fs::PermissionsExt;
850 let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o444));
851 }
852 let _ = path;
853}
854
855fn is_loose_suffix_for_algo(suffix: &str, algo: HashAlgo) -> bool {
856 suffix.len() == algo.hex_len() - 2 && suffix.bytes().all(|b| b.is_ascii_hexdigit())
857}
858
859struct LoosePayloadReader {
861 file: File,
862 dec: Decompress,
863 pending: Vec<u8>,
865 pending_pos: usize,
866 input_exhausted: bool,
868 zlib_complete: bool,
870 preset_dictionary: bool,
871 kind: ObjectKind,
872 payload_size: u64,
873 payload_remaining: u64,
874 phase: LooseReadPhase,
875 header_buf: Vec<u8>,
876 extra_payload: Vec<u8>,
878}
879
880enum LooseReadPhase {
881 Header,
882 Payload,
883}
884
885impl LoosePayloadReader {
886 fn open(mut file: File) -> Result<Self> {
887 let mut hdr = [0u8; 2];
888 file.read_exact(&mut hdr).map_err(Error::Io)?;
889 let cmf_flg = u16::from(hdr[0]) << 8 | u16::from(hdr[1]);
890 let looks_like_zlib_header = cmf_flg != 0 && cmf_flg % 31 == 0;
891 let preset_dictionary = looks_like_zlib_header && (hdr[1] & 0x20) != 0;
892 let mut reader = Self {
893 file,
894 dec: Decompress::new(true),
895 pending: hdr.to_vec(),
896 pending_pos: 0,
897 input_exhausted: false,
898 zlib_complete: false,
899 preset_dictionary,
900 kind: ObjectKind::Blob,
901 payload_size: 0,
902 payload_remaining: 0,
903 phase: LooseReadPhase::Header,
904 header_buf: Vec::with_capacity(64),
905 extra_payload: Vec::new(),
906 };
907 reader.parse_header()?;
908 Ok(reader)
909 }
910
911 fn parse_header(&mut self) -> Result<()> {
912 let mut header_buf = std::mem::take(&mut self.header_buf);
913 loop {
914 self.pump_zlib(&mut |bytes| {
915 header_buf.extend_from_slice(bytes);
916 })?;
917 if header_buf.contains(&0) || header_buf.len() >= 128 {
918 break;
919 }
920 if self.input_exhausted && self.pending_pos == self.pending.len() {
921 break;
922 }
923 }
924 let nul = header_buf
925 .iter()
926 .position(|&b| b == 0)
927 .ok_or_else(|| Error::CorruptObject("missing NUL in object header".to_owned()))?;
928 let (kind, size) = parse_object_header_prefix(&header_buf[..=nul])?;
929 self.extra_payload = header_buf[nul + 1..].to_vec();
930 self.header_buf = header_buf;
931 self.kind = kind;
932 self.payload_size = size;
933 self.payload_remaining = size;
934 self.phase = LooseReadPhase::Payload;
935 Ok(())
936 }
937
938 fn pump_zlib(&mut self, on_out: &mut dyn FnMut(&[u8])) -> Result<()> {
939 let mut out_chunk = [0u8; READ_CHUNK];
940 let produced = self.inflate_into(&mut out_chunk)?;
941 on_out(&out_chunk[..produced]);
942 Ok(())
943 }
944
945 fn inflate_into(&mut self, out: &mut [u8]) -> Result<usize> {
949 if self.zlib_complete {
950 return Ok(0);
951 }
952 if self.pending_pos == self.pending.len() && !self.input_exhausted {
953 self.pending.resize(READ_CHUNK, 0);
954 let n = loop {
955 match self.file.read(&mut self.pending) {
956 Ok(n) => break n,
957 Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
958 Err(e) => return Err(Error::Io(e)),
959 }
960 };
961 self.pending.truncate(n);
962 self.pending_pos = 0;
963 self.input_exhausted = n == 0;
964 }
965 let flush = if self.input_exhausted && self.pending_pos == self.pending.len() {
966 FlushDecompress::Finish
967 } else {
968 FlushDecompress::None
969 };
970 let before_in = self.dec.total_in();
971 let before_out = self.dec.total_out();
972 let status = match self
973 .dec
974 .decompress(&self.pending[self.pending_pos..], out, flush)
975 {
976 Ok(s) => s,
977 Err(e) => {
978 if self.preset_dictionary {
979 return Err(Error::Zlib("needs dictionary".to_owned()));
980 }
981 return Err(Error::Zlib(e.to_string()));
982 }
983 };
984 self.pending_pos += (self.dec.total_in() - before_in) as usize;
986 let produced = (self.dec.total_out() - before_out) as usize;
987 if matches!(status, Status::StreamEnd) {
988 self.zlib_complete = true;
989 }
990 Ok(produced)
991 }
992
993 fn ensure_zlib_complete(&mut self) -> io::Result<()> {
995 if self.zlib_complete {
996 return Ok(());
997 }
998 if !self.extra_payload.is_empty() {
999 return Err(io::Error::other(
1000 "corrupt object: inflated bytes remain after declared payload size",
1001 ));
1002 }
1003 loop {
1004 let mut excess = 0usize;
1005 self.pump_zlib(&mut |bytes| {
1006 excess += bytes.len();
1007 })
1008 .map_err(|e| io::Error::other(e.to_string()))?;
1009 if excess > 0 {
1010 return Err(io::Error::other(
1011 "corrupt object: excess decompressed data after declared payload size",
1012 ));
1013 }
1014 if self.zlib_complete {
1015 return Ok(());
1016 }
1017 if self.input_exhausted && self.pending_pos == self.pending.len() {
1018 return Err(io::Error::other(
1019 "corrupt object: zlib stream ended before trailer validation",
1020 ));
1021 }
1022 }
1023 }
1024}
1025
1026impl Read for LoosePayloadReader {
1027 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1028 if !matches!(self.phase, LooseReadPhase::Payload) {
1029 return Ok(0);
1030 }
1031 if self.payload_remaining == 0 {
1032 self.ensure_zlib_complete()?;
1033 return Ok(0);
1034 }
1035 if buf.is_empty() {
1036 return Ok(0);
1037 }
1038 let mut delivered = 0usize;
1039 while delivered < buf.len() && self.payload_remaining > 0 {
1040 if !self.extra_payload.is_empty() {
1041 let take = self
1042 .extra_payload
1043 .len()
1044 .min(buf.len() - delivered)
1045 .min(self.payload_remaining as usize);
1046 buf[delivered..delivered + take].copy_from_slice(&self.extra_payload[..take]);
1047 self.extra_payload.drain(..take);
1048 delivered += take;
1049 self.payload_remaining -= take as u64;
1050 continue;
1051 }
1052 let want = usize::try_from(self.payload_remaining)
1053 .map_or(buf.len() - delivered, |r| r.min(buf.len() - delivered));
1054 let produced = self
1055 .inflate_into(&mut buf[delivered..delivered + want])
1056 .map_err(|e| io::Error::other(e.to_string()))?;
1057 if produced == 0 {
1058 if self.zlib_complete
1059 || (self.input_exhausted && self.pending_pos == self.pending.len())
1060 {
1061 return Err(io::Error::other(format!(
1062 "corrupt object: zlib stream ended with {} payload bytes remaining",
1063 self.payload_remaining
1064 )));
1065 }
1066 continue;
1067 }
1068 delivered += produced;
1069 self.payload_remaining -= produced as u64;
1070 }
1071 if self.payload_remaining == 0 {
1072 self.ensure_zlib_complete()?;
1073 }
1074 Ok(delivered)
1075 }
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080 use super::*;
1081 use std::io::Read;
1082
1083 use super::super::{ObjectStore, WritableObjectStore};
1084
1085 #[test]
1086 fn lookup_prefix_scans_single_fanout_dir() {
1087 let dir = tempfile::tempdir().unwrap();
1088 let store = LooseStore::new(
1089 dir.path().join("objects"),
1090 HashAlgo::Sha1,
1091 Compression::default(),
1092 );
1093 fs::create_dir_all(store.objects_dir()).unwrap();
1094 let oid =
1095 WritableObjectStore::write(&store, ObjectKind::Blob, b"x", WriteOptions::default())
1096 .unwrap();
1097 let mut out = Vec::new();
1098 ObjectStore::lookup_prefix(&store, &oid.to_hex()[..4], 0, &mut out).unwrap();
1099 assert_eq!(out, vec![oid]);
1100 }
1101
1102 fn temp_store() -> (tempfile::TempDir, LooseStore) {
1103 let dir = tempfile::tempdir().unwrap();
1104 let store = LooseStore::new(
1105 dir.path().join("objects"),
1106 HashAlgo::Sha1,
1107 Compression::default(),
1108 );
1109 fs::create_dir_all(store.objects_dir()).unwrap();
1110 (dir, store)
1111 }
1112
1113 fn tmp_entries(store: &LooseStore) -> Vec<PathBuf> {
1114 fs::read_dir(store.objects_dir())
1115 .unwrap()
1116 .flatten()
1117 .filter(|shard| shard.file_type().is_ok_and(|t| t.is_dir()))
1118 .flat_map(|shard| fs::read_dir(shard.path()).unwrap().flatten())
1119 .map(|entry| entry.path())
1120 .filter(|path| {
1121 path.file_name()
1122 .is_some_and(|n| n.to_string_lossy().starts_with("tmp_"))
1123 })
1124 .collect()
1125 }
1126
1127 #[test]
1128 fn identical_writes_through_separate_handles_all_succeed() {
1129 let (_dir, store) = temp_store();
1130 let threads = 8;
1131 let barrier = std::sync::Arc::new(std::sync::Barrier::new(threads));
1132 let payloads: Vec<Vec<u8>> = (0..200)
1133 .map(|j| format!("same {j}").into_bytes())
1134 .chain(std::iter::once(vec![0x5a; 1024 * 1024]))
1135 .collect();
1136 let payloads = std::sync::Arc::new(payloads);
1137 let handles: Vec<_> = (0..threads)
1138 .map(|_| {
1139 let store = store.clone();
1140 let barrier = std::sync::Arc::clone(&barrier);
1141 let payloads = std::sync::Arc::clone(&payloads);
1142 std::thread::spawn(move || {
1143 barrier.wait();
1144 payloads
1145 .iter()
1146 .map(|data| {
1147 WritableObjectStore::write(
1148 &store,
1149 ObjectKind::Blob,
1150 data,
1151 WriteOptions::default(),
1152 )
1153 })
1154 .collect::<Vec<_>>()
1155 })
1156 })
1157 .collect();
1158 for handle in handles {
1159 for (data, result) in payloads.iter().zip(handle.join().unwrap()) {
1160 let oid = result.expect("concurrent identical write succeeds");
1161 let obj = ObjectStore::read(&store, &oid).unwrap().expect("object");
1162 assert!(obj.data == *data);
1163 }
1164 }
1165 assert!(tmp_entries(&store).is_empty());
1166 }
1167
1168 #[test]
1169 fn prehashed_write_recreates_pruned_fanout_dir_marked_ready() {
1170 let (_dir, store) = temp_store();
1171 let data = b"fanout pruned under a ready writer";
1172 let oid = hash::hash_object(HashAlgo::Sha1, ObjectKind::Blob, data).unwrap();
1173 let zlib = crate::odb::zlib_compress_blob_loose(data, Compression::default()).unwrap();
1174 let options = WriteOptions {
1175 loose_prefix_dirs_ready: true,
1176 ..WriteOptions::default()
1177 };
1178 assert!(!store.object_path(&oid).parent().unwrap().exists());
1179 let written = store.write_zlib_prehashed(&oid, &zlib, options).unwrap();
1180 assert_eq!(written, oid);
1181 assert_eq!(ObjectStore::read(&store, &oid).unwrap().unwrap().data, data);
1182 assert!(tmp_entries(&store).is_empty());
1183 }
1184
1185 #[test]
1186 fn unique_tmp_paths_differ_and_are_not_loose_names() {
1187 let prefix = Path::new("objects/ab");
1188 let a = unique_loose_tmp_path(prefix);
1189 let b = unique_loose_tmp_path(prefix);
1190 assert_ne!(a, b);
1191 for path in [a, b] {
1192 let name = path.file_name().unwrap().to_str().unwrap();
1193 assert!(name.starts_with("tmp_obj_"), "{name}");
1194 assert!(!is_loose_suffix_for_algo(name, HashAlgo::Sha1));
1195 assert!(!is_loose_suffix_for_algo(name, HashAlgo::Sha256));
1196 }
1197 }
1198
1199 #[test]
1200 fn read_info_and_read_agree_for_object_spanning_read_chunks() {
1201 let (_dir, store) = temp_store();
1202 let data: Vec<u8> = (0..3 * READ_CHUNK as u32)
1203 .map(|i| (i.wrapping_mul(2_654_435_761) >> 13) as u8)
1204 .collect();
1205 let oid =
1206 WritableObjectStore::write(&store, ObjectKind::Blob, &data, WriteOptions::default())
1207 .unwrap();
1208 let info = ObjectStore::read_info(&store, &oid).unwrap().expect("info");
1209 assert_eq!(info.kind, ObjectKind::Blob);
1210 assert_eq!(info.size, data.len() as u64);
1211 let obj = ObjectStore::read(&store, &oid).unwrap().expect("object");
1212 assert_eq!(obj.kind, ObjectKind::Blob);
1213 assert_eq!(obj.data, data);
1214 let mut buf = Vec::new();
1215 let kind = ObjectStore::read_into(&store, &oid, &mut buf).unwrap();
1216 assert_eq!(kind, Some(ObjectKind::Blob));
1217 assert_eq!(buf, data);
1218 }
1219
1220 #[test]
1221 fn read_unless_larger_reads_small_and_skips_large_objects() {
1222 let (_dir, store) = temp_store();
1223 let data = vec![b'x'; 3 * READ_CHUNK];
1224 let oid =
1225 WritableObjectStore::write(&store, ObjectKind::Blob, &data, WriteOptions::default())
1226 .unwrap();
1227 match store.read_unless_larger(&oid, data.len() as u64).unwrap() {
1228 Some(BoundedRead::Object(obj)) => {
1229 assert_eq!(obj.kind, ObjectKind::Blob);
1230 assert_eq!(obj.data, data);
1231 }
1232 other => panic!("expected the object, got {other:?}"),
1233 }
1234 assert!(matches!(
1235 store
1236 .read_unless_larger(&oid, data.len() as u64 - 1)
1237 .unwrap(),
1238 Some(BoundedRead::Larger)
1239 ));
1240 let missing = ObjectId::from_hex("1234567890123456789012345678901234567890").unwrap();
1241 assert!(store.read_unless_larger(&missing, 0).unwrap().is_none());
1242 }
1243
1244 #[test]
1245 fn stream_reader_delivers_payload_through_tiny_and_huge_buffers() {
1246 let (_dir, store) = temp_store();
1247 let data: Vec<u8> = (0..5 * READ_CHUNK as u32)
1248 .map(|i| (i.wrapping_mul(2_654_435_761) >> 11) as u8)
1249 .collect();
1250 let oid =
1251 WritableObjectStore::write(&store, ObjectKind::Blob, &data, WriteOptions::default())
1252 .unwrap();
1253 for chunk_len in [1usize, 7, 4096, 10 * READ_CHUNK] {
1254 let mut stream = ObjectStore::open_stream(&store, &oid)
1255 .unwrap()
1256 .expect("stream");
1257 let mut out = Vec::new();
1258 let mut chunk = vec![0u8; chunk_len];
1259 loop {
1260 let n = stream.reader.read(&mut chunk).unwrap();
1261 if n == 0 {
1262 break;
1263 }
1264 out.extend_from_slice(&chunk[..n]);
1265 }
1266 assert!(out == data, "chunk length {chunk_len}");
1267 }
1268 }
1269
1270 #[test]
1271 fn read_info_is_none_for_missing_object_or_directory_at_its_path() {
1272 let (_dir, store) = temp_store();
1273 let missing = ObjectId::from_hex("1234567890123456789012345678901234567890").unwrap();
1274 assert!(ObjectStore::read_info(&store, &missing).unwrap().is_none());
1275 fs::create_dir_all(store.object_path(&missing)).unwrap();
1276 assert!(ObjectStore::read_info(&store, &missing).unwrap().is_none());
1277 }
1278
1279 #[test]
1280 fn lookup_prefix_one_hex_char_does_not_overflow() {
1281 let dir = tempfile::tempdir().unwrap();
1282 let store = LooseStore::new(
1283 dir.path().join("objects"),
1284 HashAlgo::Sha1,
1285 Compression::default(),
1286 );
1287 fs::create_dir_all(store.objects_dir()).unwrap();
1288 let oid =
1289 WritableObjectStore::write(&store, ObjectKind::Blob, b"x", WriteOptions::default())
1290 .unwrap();
1291 let mut out = Vec::new();
1292 ObjectStore::lookup_prefix(&store, &oid.to_hex()[..1], 0, &mut out).unwrap();
1293 assert!(out.contains(&oid));
1294 }
1295
1296 #[test]
1297 fn truncated_zlib_stream_errors_on_read() {
1298 let dir = tempfile::tempdir().unwrap();
1299 let store = LooseStore::new(
1300 dir.path().join("objects"),
1301 HashAlgo::Sha1,
1302 Compression::default(),
1303 );
1304 fs::create_dir_all(store.objects_dir()).unwrap();
1305 let payload = vec![0u8; 100_000];
1306 let oid =
1307 WritableObjectStore::write(&store, ObjectKind::Blob, &payload, WriteOptions::default())
1308 .unwrap();
1309 let path = store.object_path(&oid);
1310 let mut bytes = fs::read(&path).unwrap();
1311 bytes.truncate(bytes.len() / 2);
1312 #[cfg(unix)]
1313 {
1314 use std::os::unix::fs::PermissionsExt;
1315 fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
1316 }
1317 fs::write(&path, &bytes).unwrap();
1318 let mut stream = ObjectStore::open_stream(&store, &oid)
1319 .unwrap()
1320 .expect("stream");
1321 let mut out = Vec::new();
1322 let err = stream.reader.read_to_end(&mut out).unwrap_err();
1323 assert!(
1324 err.to_string().contains("payload bytes remaining")
1325 || err.to_string().contains("corrupt object"),
1326 "unexpected error: {err}"
1327 );
1328 assert!(out.len() < payload.len());
1329 }
1330
1331 #[test]
1332 fn truncated_zlib_checksum_only_errors_on_read() {
1333 let dir = tempfile::tempdir().unwrap();
1334 let store = LooseStore::new(
1335 dir.path().join("objects"),
1336 HashAlgo::Sha1,
1337 Compression::default(),
1338 );
1339 fs::create_dir_all(store.objects_dir()).unwrap();
1340 let payload = vec![0u8; 100_000];
1341 let oid =
1342 WritableObjectStore::write(&store, ObjectKind::Blob, &payload, WriteOptions::default())
1343 .unwrap();
1344 let path = store.object_path(&oid);
1345 let mut bytes = fs::read(&path).unwrap();
1346 assert!(bytes.len() > 1);
1347 bytes.pop();
1348 #[cfg(unix)]
1349 {
1350 use std::os::unix::fs::PermissionsExt;
1351 fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
1352 }
1353 fs::write(&path, &bytes).unwrap();
1354 match ObjectStore::read(&store, &oid) {
1355 Ok(None) => {}
1356 Ok(Some(_)) => panic!("full read must not succeed on checksum-truncated loose object"),
1357 Err(_) => {}
1358 }
1359 let mut stream = ObjectStore::open_stream(&store, &oid)
1360 .expect("open_stream")
1361 .expect("stream hit");
1362 let mut out = Vec::new();
1363 let err = stream.reader.read_to_end(&mut out).unwrap_err();
1364 assert!(
1365 err.to_string().contains("trailer validation")
1366 || err.to_string().contains("corrupt object"),
1367 "unexpected error: {err}"
1368 );
1369 }
1370
1371 #[test]
1372 fn stream_sixteen_mib_peak_buffer_under_one_mib() {
1373 let dir = tempfile::tempdir().unwrap();
1374 let store = LooseStore::new(
1375 dir.path().join("objects"),
1376 HashAlgo::Sha1,
1377 Compression::default(),
1378 );
1379 fs::create_dir_all(store.objects_dir()).unwrap();
1380 let payload = vec![0xCDu8; 16 * 1024 * 1024];
1381 let oid =
1382 WritableObjectStore::write(&store, ObjectKind::Blob, &payload, WriteOptions::default())
1383 .unwrap();
1384 let mut stream = ObjectStore::open_stream(&store, &oid)
1385 .unwrap()
1386 .expect("stream");
1387 assert_eq!(stream.size, payload.len() as u64);
1388 let mut out = Vec::new();
1389 let mut chunk = [0u8; 64 * 1024];
1390 let mut peak = 0usize;
1391 loop {
1392 let n = stream.reader.read(&mut chunk).unwrap();
1393 if n == 0 {
1394 break;
1395 }
1396 peak = peak.max(n);
1397 out.extend_from_slice(&chunk[..n]);
1398 }
1399 assert_eq!(out, payload);
1400 assert!(
1401 peak < 1024 * 1024,
1402 "peak in-flight buffer {peak} must stay under 1 MiB"
1403 );
1404 }
1405}