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