1use std::collections::{HashMap, HashSet};
4use std::fs::{self, File};
5use std::io::{self, Read, Write};
6use std::ops::ControlFlow;
7use std::path::{Path, PathBuf};
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::{Arc, Mutex};
10use std::time::SystemTime;
11
12use flate2::write::ZlibEncoder;
13use flate2::Compression;
14use flate2::{Decompress, FlushDecompress, Status};
15
16use super::{BoundedRead, ObjectStore, ObjectStream, WritableObjectStore};
17use crate::error::{Error, Result};
18use crate::git_dir::io_error::{map_objects_dir_io, GitDirPathOperation};
19use crate::hash::{self, ObjectPayloadHasher};
20use crate::objects::{HashAlgo, Object, ObjectId, ObjectIdPrefix, ObjectInfo, ObjectKind};
21use crate::odb::WriteOptions;
22use crate::zlib_inflate::{
23 with_loose_scratch, LooseFileReadBudget, LOOSE_INFLATE_INPUT_CHUNK, LOOSE_INFLATE_OUTPUT_CHUNK,
24};
25const STREAM_CHUNK: usize = 128 * 1024;
27
28fn objects_dir_mtime(objects_dir: &Path) -> SystemTime {
29 fs::metadata(objects_dir)
30 .and_then(|m| m.modified())
31 .unwrap_or(SystemTime::UNIX_EPOCH)
32}
33
34fn fanout_dir_mtime(objects_dir: &Path, prefix: &str) -> SystemTime {
35 fs::metadata(objects_dir.join(prefix))
36 .and_then(|m| m.modified())
37 .unwrap_or(SystemTime::UNIX_EPOCH)
38}
39
40fn loose_fanout_prefix(objects_dir: &Path, path: &Path) -> Option<String> {
41 path.strip_prefix(objects_dir)
42 .ok()
43 .and_then(|rel| rel.components().next())
44 .and_then(|c| c.as_os_str().to_str())
45 .filter(|s| s.len() == 2 && s.chars().all(|ch| ch.is_ascii_hexdigit()))
46 .map(str::to_owned)
47}
48
49#[derive(Debug)]
51pub(crate) struct LooseNegativeCache {
52 root_mtime: SystemTime,
53 fanout_mtime: HashMap<String, SystemTime>,
54 absent_paths: HashSet<PathBuf>,
55 absent_fanout: HashSet<String>,
56}
57
58impl Default for LooseNegativeCache {
59 fn default() -> Self {
60 Self {
61 root_mtime: SystemTime::UNIX_EPOCH,
62 fanout_mtime: HashMap::new(),
63 absent_paths: HashSet::new(),
64 absent_fanout: HashSet::new(),
65 }
66 }
67}
68
69impl LooseNegativeCache {
70 fn sync_root(&mut self, objects_dir: &Path) {
71 let mt = objects_dir_mtime(objects_dir);
72 if self.root_mtime != mt {
73 *self = Self {
74 root_mtime: mt,
75 ..Self::default()
76 };
77 }
78 }
79
80 fn sync_fanout(&mut self, objects_dir: &Path, prefix: &str) {
81 let mt = fanout_dir_mtime(objects_dir, prefix);
82 if self.fanout_mtime.get(prefix) != Some(&mt) {
83 self.fanout_mtime.insert(prefix.to_owned(), mt);
84 let fanout_dir = objects_dir.join(prefix);
85 self.absent_paths.retain(|p| !p.starts_with(&fanout_dir));
86 self.absent_fanout.remove(prefix);
87 }
88 }
89
90 fn may_skip_open(&mut self, objects_dir: &Path, path: &Path) -> bool {
91 self.sync_root(objects_dir);
92 if let Some(prefix) = loose_fanout_prefix(objects_dir, path) {
93 self.sync_fanout(objects_dir, &prefix);
94 if self.absent_fanout.contains(&prefix) {
95 return true;
96 }
97 }
98 self.absent_paths.contains(path)
99 }
100
101 fn record_absent(&mut self, objects_dir: &Path, path: &Path) {
102 self.sync_root(objects_dir);
103 self.absent_paths.insert(path.to_path_buf());
104 if let Some(prefix) = loose_fanout_prefix(objects_dir, path) {
105 self.sync_fanout(objects_dir, &prefix);
106 let fanout_dir = objects_dir.join(&prefix);
107 if !fanout_dir.is_dir() {
108 self.absent_fanout.insert(prefix);
109 }
110 }
111 }
112}
113
114pub(crate) type SharedLooseNegativeCache = Arc<Mutex<LooseNegativeCache>>;
115
116#[derive(Debug, Clone)]
118pub struct LooseStore {
119 objects_dir: PathBuf,
120 hash_algo: HashAlgo,
121 compression: Compression,
122 negative_cache: SharedLooseNegativeCache,
123}
124
125impl LooseStore {
126 #[must_use]
128 pub fn new(objects_dir: PathBuf, hash_algo: HashAlgo, compression: Compression) -> Self {
129 Self::new_with_negative_cache(
130 objects_dir,
131 hash_algo,
132 compression,
133 Arc::new(Mutex::new(LooseNegativeCache::default())),
134 )
135 }
136
137 pub(crate) fn new_with_negative_cache(
138 objects_dir: PathBuf,
139 hash_algo: HashAlgo,
140 compression: Compression,
141 negative_cache: SharedLooseNegativeCache,
142 ) -> Self {
143 Self {
144 objects_dir,
145 hash_algo,
146 compression,
147 negative_cache,
148 }
149 }
150
151 fn invalidate_negative_cache(&self) {
152 if let Ok(mut guard) = self.negative_cache.lock() {
153 *guard = LooseNegativeCache::default();
154 }
155 }
156
157 fn open_may_be_absent(&self, path: &Path) -> bool {
158 self.negative_cache
159 .lock()
160 .ok()
161 .is_some_and(|mut guard| guard.may_skip_open(&self.objects_dir, path))
162 }
163
164 fn record_absent_path(&self, path: &Path) {
165 if let Ok(mut guard) = self.negative_cache.lock() {
166 guard.record_absent(&self.objects_dir, path);
167 }
168 }
169
170 #[must_use]
172 pub fn objects_dir(&self) -> &Path {
173 &self.objects_dir
174 }
175
176 #[must_use]
178 pub fn object_path(&self, oid: &ObjectId) -> PathBuf {
179 oid.loose_path_in(&self.objects_dir)
180 }
181
182 pub fn ensure_all_loose_prefix_dirs(&self) -> Result<()> {
188 for i in 0u8..=255 {
189 let prefix = self.objects_dir.join(format!("{i:02x}"));
190 fs::create_dir_all(&prefix).map_err(|source| {
191 map_objects_dir_io(
192 &self.objects_dir,
193 &prefix,
194 GitDirPathOperation::CreateDir,
195 source,
196 )
197 })?;
198 }
199 Ok(())
200 }
201
202 pub fn zlib_compress_store_bytes(&self, store_bytes: &[u8]) -> Result<Vec<u8>> {
208 zlib_compress_store_bytes(store_bytes, self.compression)
209 }
210
211 pub(crate) fn try_finish_if_loose_present(
213 &self,
214 oid: &ObjectId,
215 options: WriteOptions,
216 ) -> Result<Option<ObjectId>> {
217 let path = self.object_path(oid);
218 if path.is_file() {
219 if !options.silent {
220 let _ = self.freshen(oid)?;
221 }
222 return Ok(Some(*oid));
223 }
224 Ok(None)
225 }
226
227 pub fn write_store_prehashed(
233 &self,
234 oid: &ObjectId,
235 store_bytes: &[u8],
236 options: WriteOptions,
237 ) -> Result<ObjectId> {
238 parse_object_bytes(store_bytes)?;
239 if oid.algo() != self.hash_algo {
240 return Err(Error::InvalidObjectId(oid.to_hex()));
241 }
242 if let Some(existing) = self.try_finish_if_loose_present(oid, options)? {
243 return Ok(existing);
244 }
245 let path = self.object_path(oid);
246 let prefix_dir = path
247 .parent()
248 .ok_or_else(|| Error::PathError("object path has no parent".to_owned()))?;
249 let zlib = zlib_compress_store_bytes(store_bytes, self.compression)?;
250 publish_loose_object(&self.objects_dir, &path, prefix_dir, false, |mut file| {
251 file.write_all(&zlib).map_err(|source| {
252 map_objects_dir_io(
253 &self.objects_dir,
254 &path,
255 GitDirPathOperation::WriteObject,
256 source,
257 )
258 })
259 })?;
260 self.invalidate_negative_cache();
261 Ok(*oid)
262 }
263
264 pub fn write_zlib_prehashed(
270 &self,
271 oid: &ObjectId,
272 zlib_store: &[u8],
273 options: WriteOptions,
274 ) -> Result<ObjectId> {
275 if oid.algo() != self.hash_algo {
276 return Err(Error::InvalidObjectId(oid.to_hex()));
277 }
278 if let Some(existing) = self.try_finish_if_loose_present(oid, options)? {
279 return Ok(existing);
280 }
281 let path = self.object_path(oid);
282 let prefix_dir = path
283 .parent()
284 .ok_or_else(|| Error::PathError("object path has no parent".to_owned()))?;
285 publish_loose_object(
286 &self.objects_dir,
287 &path,
288 prefix_dir,
289 options.loose_prefix_dirs_ready,
290 |mut file| {
291 file.write_all(zlib_store).map_err(|source| {
292 map_objects_dir_io(
293 &self.objects_dir,
294 &path,
295 GitDirPathOperation::WriteObject,
296 source,
297 )
298 })
299 },
300 )?;
301 self.invalidate_negative_cache();
302 Ok(*oid)
303 }
304
305 pub(crate) fn write_prehashed(
313 &self,
314 oid: &ObjectId,
315 kind: ObjectKind,
316 data: &[u8],
317 options: WriteOptions,
318 ) -> Result<ObjectId> {
319 if oid.algo() != self.hash_algo {
320 return Err(Error::InvalidObjectId(oid.to_hex()));
321 }
322 if let Some(existing) = self.try_finish_if_loose_present(oid, options)? {
323 return Ok(existing);
324 }
325 let path = self.object_path(oid);
326 let prefix_dir = path
327 .parent()
328 .ok_or_else(|| Error::PathError("object path has no parent".to_owned()))?;
329 publish_loose_object(
330 &self.objects_dir,
331 &path,
332 prefix_dir,
333 options.loose_prefix_dirs_ready,
334 |file| deflate_object_into(file, kind, data, self.compression),
335 )?;
336 self.invalidate_negative_cache();
337 Ok(*oid)
338 }
339
340 pub fn read_verify_oid(path: &Path, expected_oid: &ObjectId) -> Result<Object> {
348 let file = fs::File::open(path).map_err(Error::Io)?;
349 let raw = match read_zlib_loose_payload(file) {
350 Ok(raw) => raw,
351 Err(err) => {
352 return Err(Error::wrap_loose_read_failure(
353 *expected_oid,
354 path.to_path_buf(),
355 err,
356 ));
357 }
358 };
359 let obj = match parse_object_bytes_with_oid(&raw, expected_oid) {
360 Ok(obj) => obj,
361 Err(err) => {
362 return Err(Error::wrap_loose_read_failure(
363 *expected_oid,
364 path.to_path_buf(),
365 err,
366 ));
367 }
368 };
369 let computed = hash::hash_object(expected_oid.algo(), obj.kind, &obj.data)?;
370 if computed != *expected_oid {
371 return Err(Error::LooseHashMismatch {
372 path: path.display().to_string(),
373 real_oid: computed.to_hex(),
374 });
375 }
376 Ok(obj)
377 }
378
379 pub(crate) fn read_unless_larger(
389 &self,
390 oid: &ObjectId,
391 limit: u64,
392 ) -> Result<Option<BoundedRead>> {
393 let mut file = match fs::File::open(self.object_path(oid)) {
394 Ok(f) => f,
395 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
396 Err(err) => return Err(Error::Io(err)),
397 };
398 let mut buf = Vec::new();
399 let complete =
400 with_loose_scratch(|s| s.inflate_loose_file_unless_larger(&mut file, &mut buf, limit))?;
401 if !complete {
402 return Ok(Some(BoundedRead::Larger));
403 }
404 let kind = strip_decompressed_object_header(&mut buf, Some(oid))?;
405 Ok(Some(BoundedRead::Object(Object::new(kind, buf))))
406 }
407
408 fn touch_mtime(path: &Path) -> bool {
409 crate::fs_readonly::set_file_times_now(path).is_some()
410 }
411}
412
413impl ObjectStore for LooseStore {
414 fn hash_algo(&self) -> HashAlgo {
415 self.hash_algo
416 }
417
418 fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
419 let mut data = Vec::new();
420 Ok(self
421 .read_into(oid, &mut data)?
422 .map(|kind| Object::new(kind, data)))
423 }
424
425 fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
426 let path = self.object_path(oid);
427 if self.open_may_be_absent(&path) {
428 return Ok(None);
429 }
430 let file = match fs::File::open(&path) {
431 Ok(f) => f,
432 Err(err) if err.kind() == io::ErrorKind::NotFound => {
433 self.record_absent_path(&path);
434 return Ok(None);
435 }
436 Err(err) => return Err(Error::Io(err)),
437 };
438 if let Err(err) = read_zlib_loose_payload_into(file, buf) {
439 return Err(Error::wrap_loose_read_failure(*oid, path, err));
440 }
441 let kind = match strip_decompressed_object_header(buf, Some(oid)) {
442 Ok(kind) => kind,
443 Err(err) => return Err(Error::wrap_loose_read_failure(*oid, path, err)),
444 };
445 Ok(Some(kind))
446 }
447
448 fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
449 let path = self.object_path(oid);
450 if self.open_may_be_absent(&path) {
451 return Ok(None);
452 }
453 let mut file = match fs::File::open(&path) {
454 Ok(f) => f,
455 Err(err) if err.kind() == io::ErrorKind::NotFound => {
456 self.record_absent_path(&path);
457 return Ok(None);
458 }
459 Err(err) => return Err(Error::Io(err)),
460 };
461 match loose_object_info_from_file(&mut file) {
462 Err(Error::Io(err)) if err.kind() == io::ErrorKind::IsADirectory => Ok(None),
463 Err(err) => Err(Error::wrap_loose_read_failure(*oid, path, err)),
464 Ok(info) => Ok(Some(info)),
465 }
466 }
467
468 fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'_>>> {
469 let path = self.object_path(oid);
470 if self.open_may_be_absent(&path) {
471 return Ok(None);
472 }
473 let file = match fs::File::open(&path) {
474 Ok(f) => f,
475 Err(err) if err.kind() == io::ErrorKind::NotFound => {
476 self.record_absent_path(&path);
477 return Ok(None);
478 }
479 Err(err) => return Err(Error::Io(err)),
480 };
481 let reader = match LoosePayloadReader::open(file) {
482 Ok(reader) => reader,
483 Err(err) => return Err(Error::wrap_loose_read_failure(*oid, path, err)),
484 };
485 Ok(Some(ObjectStream {
486 kind: reader.kind,
487 size: reader.payload_size,
488 reader: Box::new(reader),
489 }))
490 }
491
492 fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
493 for_each_loose_object_id(&self.objects_dir, self.hash_algo, f)
494 }
495
496 fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
497 let prefix = ObjectIdPrefix::from_hex(prefix, self.hash_algo)?;
498 let start_len = out.len();
499 let mut collect = |oid: &ObjectId| {
500 if prefix.matches(oid) {
501 out.push(*oid);
502 if limit != 0 && out.len() - start_len >= limit {
503 return ControlFlow::Break(());
504 }
505 }
506 ControlFlow::Continue(())
507 };
508 if prefix.nibbles() < 2 {
509 return for_each_loose_object_id(&self.objects_dir, self.hash_algo, &mut collect);
510 }
511 let fanout = format!("{:02x}", prefix.lowest().as_bytes()[0]);
512 let fanout_dir = self.objects_dir.join(&fanout);
513 for_each_object_id_in_fanout_dir(&fanout_dir, &fanout, self.hash_algo, &mut collect)
514 .map(drop)
515 }
516}
517
518impl WritableObjectStore for LooseStore {
519 fn write(&self, kind: ObjectKind, data: &[u8], options: WriteOptions) -> Result<ObjectId> {
520 let oid = hash::hash_object(self.hash_algo, kind, data)?;
521 if let Some(existing) = self.try_finish_if_loose_present(&oid, options)? {
522 return Ok(existing);
523 }
524 let path = self.object_path(&oid);
525 let prefix_dir = path
526 .parent()
527 .ok_or_else(|| Error::PathError("object path has no parent".to_owned()))?;
528 publish_loose_object(&self.objects_dir, &path, prefix_dir, false, |file| {
529 deflate_object_into(file, kind, data, self.compression)
530 })?;
531 self.invalidate_negative_cache();
532 Ok(oid)
533 }
534
535 fn write_stream(
536 &self,
537 kind: ObjectKind,
538 size: u64,
539 reader: &mut dyn Read,
540 options: WriteOptions,
541 ) -> Result<ObjectId> {
542 self.write_payload_stream(kind, size, reader, options)
543 }
544
545 fn freshen(&self, oid: &ObjectId) -> Result<bool> {
546 let path = self.object_path(oid);
547 if path.is_file() {
548 return Ok(LooseStore::touch_mtime(&path));
549 }
550 Ok(false)
551 }
552}
553
554pub fn enumerate_loose_objects(
556 objects_dir: &Path,
557 hash_algo: HashAlgo,
558) -> Result<Vec<(ObjectId, PathBuf)>> {
559 let mut out = Vec::new();
560 for_each_loose_object_id(objects_dir, hash_algo, &mut |oid| {
561 let path = oid.loose_path_in(objects_dir);
562 out.push((*oid, path));
563 ControlFlow::Continue(())
564 })?;
565 Ok(out)
566}
567
568pub fn for_each_loose_object_id(
570 objects_dir: &Path,
571 hash_algo: HashAlgo,
572 f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
573) -> Result<()> {
574 let top = match fs::read_dir(objects_dir) {
575 Ok(rd) => rd,
576 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
577 Err(e) => return Err(Error::Io(e)),
578 };
579
580 for top_entry in top {
581 let top_entry = top_entry.map_err(Error::Io)?;
582 let name = top_entry.file_name();
583 let Some(prefix) = name.to_str() else {
584 continue;
585 };
586 if prefix.len() != 2 || !prefix.bytes().all(|b| b.is_ascii_hexdigit()) {
587 continue;
588 }
589 if !top_entry.file_type().map_err(Error::Io)?.is_dir() {
590 continue;
591 }
592 if for_each_object_id_in_fanout_dir(&top_entry.path(), prefix, hash_algo, f)?.is_break() {
593 return Ok(());
594 }
595 }
596 Ok(())
597}
598
599fn for_each_object_id_in_fanout_dir(
602 fanout_dir: &Path,
603 fanout: &str,
604 hash_algo: HashAlgo,
605 f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
606) -> Result<ControlFlow<()>> {
607 let sub = match fs::read_dir(fanout_dir) {
608 Ok(rd) => rd,
609 Err(e)
610 if matches!(
611 e.kind(),
612 io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
613 ) =>
614 {
615 return Ok(ControlFlow::Continue(()));
616 }
617 Err(e) => return Err(Error::Io(e)),
618 };
619 for sub_entry in sub {
620 let sub_entry = sub_entry.map_err(Error::Io)?;
621 if !sub_entry.file_type().map_err(Error::Io)?.is_file() {
622 continue;
623 }
624 let suffix_name = sub_entry.file_name();
625 let Some(suffix) = suffix_name.to_str() else {
626 continue;
627 };
628 if !is_loose_suffix_for_algo(suffix, hash_algo) {
629 continue;
630 }
631 let hex = format!("{fanout}{suffix}");
632 let Ok(oid) = ObjectId::from_hex(&hex) else {
633 continue;
634 };
635 if f(&oid).is_break() {
636 return Ok(ControlFlow::Break(()));
637 }
638 }
639 Ok(ControlFlow::Continue(()))
640}
641
642pub fn read_zlib_loose_payload(file: File) -> Result<Vec<u8>> {
644 let mut out = Vec::new();
645 read_zlib_loose_payload_into(file, &mut out)?;
646 Ok(out)
647}
648
649pub(crate) fn read_zlib_loose_payload_into(mut file: File, buf: &mut Vec<u8>) -> Result<()> {
651 with_loose_scratch(|scratch| scratch.inflate_loose_file(&mut file, buf))
652}
653
654pub(crate) fn strip_decompressed_object_header(
656 buf: &mut Vec<u8>,
657 oid_hint: Option<&ObjectId>,
658) -> Result<ObjectKind> {
659 let nul = buf
660 .iter()
661 .position(|&b| b == 0)
662 .ok_or_else(|| Error::CorruptObject("missing NUL in object header".to_owned()))?;
663 let (kind, size) = parse_object_header_prefix(buf)?;
664 let payload_start = nul + 1;
665 let size_usize = usize::try_from(size)
666 .map_err(|_| Error::CorruptObject(format!("invalid object size: {size}")))?;
667 if buf.len().saturating_sub(payload_start) != size_usize {
668 let oid_str = oid_hint.map(ObjectId::to_hex).unwrap_or_else(|| {
669 hash_algo_digest_hex(HashAlgo::Sha1, buf).unwrap_or_else(|_| "?".to_owned())
670 });
671 return Err(Error::CorruptObject(format!(
672 "object {oid_str} size mismatch: header says {size}, got {}",
673 buf.len().saturating_sub(payload_start)
674 )));
675 }
676 if payload_start > 0 {
677 buf.copy_within(payload_start.., 0);
678 }
679 buf.truncate(size_usize);
680 Ok(kind)
681}
682
683pub fn read_loose_object_info(path: &Path) -> Result<ObjectInfo> {
685 let mut file = fs::File::open(path).map_err(Error::Io)?;
686 match loose_object_info_from_file(&mut file) {
687 Ok(info) => Ok(info),
688 Err(err) => {
689 if let Some(oid) = oid_from_loose_object_path(path) {
690 Err(Error::wrap_loose_read_failure(oid, path.to_path_buf(), err))
691 } else {
692 Err(err)
693 }
694 }
695 }
696}
697
698fn oid_from_loose_object_path(path: &Path) -> Option<ObjectId> {
699 let file_name = path.file_name()?.to_str()?;
700 if !ObjectId::is_loose_suffix_len(file_name.len()) {
701 return None;
702 }
703 let parent = path.parent()?.file_name()?.to_str()?;
704 if parent.len() != 2 {
705 return None;
706 }
707 let hex = format!("{parent}{file_name}");
708 ObjectId::from_hex(&hex).ok()
709}
710
711fn loose_object_info_from_file(file: &mut File) -> Result<ObjectInfo> {
712 let prefix = with_loose_scratch(|scratch| scratch.inflate_loose_header(file))?;
713 let (kind, size) = parse_object_header_prefix(&prefix)?;
714 Ok(ObjectInfo { kind, size })
715}
716
717pub(crate) fn parse_object_bytes(raw: &[u8]) -> Result<Object> {
719 parse_object_bytes_inner(raw, None)
720}
721
722pub(crate) fn parse_object_bytes_with_oid(raw: &[u8], oid: &ObjectId) -> Result<Object> {
723 parse_object_bytes_inner(raw, Some(oid))
724}
725
726pub(crate) fn zlib_compress_store_bytes(
727 store_bytes: &[u8],
728 compression: Compression,
729) -> Result<Vec<u8>> {
730 let mut out = Vec::new();
731 let mut encoder = ZlibEncoder::new(&mut out, compression);
732 encoder
733 .write_all(store_bytes)
734 .map_err(|e| Error::Zlib(e.to_string()))?;
735 encoder.finish().map_err(|e| Error::Zlib(e.to_string()))?;
736 Ok(out)
737}
738
739pub(crate) fn decompress_zlib_loose_bytes(zlib: &[u8]) -> Result<Vec<u8>> {
740 let mut decoder = flate2::read::ZlibDecoder::new(zlib);
741 let mut raw = Vec::new();
742 decoder
743 .read_to_end(&mut raw)
744 .map_err(|e| Error::Zlib(e.to_string()))?;
745 Ok(raw)
746}
747
748pub(crate) fn parse_object_header_prefix(raw: &[u8]) -> Result<(ObjectKind, u64)> {
750 let nul = raw
751 .iter()
752 .position(|&b| b == 0)
753 .ok_or_else(|| Error::CorruptObject("missing NUL in object header".to_owned()))?;
754
755 let header = &raw[..nul];
756 let sp = header
757 .iter()
758 .position(|&b| b == b' ')
759 .ok_or_else(|| Error::CorruptObject("missing space in object header".to_owned()))?;
760
761 if sp > 32 {
762 return Err(Error::ObjectHeaderTooLong {
763 oid: hash_algo_digest_hex(HashAlgo::Sha1, raw).unwrap_or_else(|_| "?".to_owned()),
764 });
765 }
766
767 let kind = ObjectKind::from_bytes(&header[..sp])?;
768 let size_str = std::str::from_utf8(&header[sp + 1..])
769 .map_err(|_| Error::CorruptObject("non-UTF-8 object size".to_owned()))?;
770 let size: u64 = size_str
771 .parse()
772 .map_err(|_| Error::CorruptObject(format!("invalid object size: {size_str}")))?;
773 Ok((kind, size))
774}
775
776fn parse_object_bytes_inner(raw: &[u8], oid_hint: Option<&ObjectId>) -> Result<Object> {
777 let nul = raw
778 .iter()
779 .position(|&b| b == 0)
780 .ok_or_else(|| Error::CorruptObject("missing NUL in object header".to_owned()))?;
781
782 let header = &raw[..nul];
783 let data = raw[nul + 1..].to_vec();
784
785 let sp = header
786 .iter()
787 .position(|&b| b == b' ')
788 .ok_or_else(|| Error::CorruptObject("missing space in object header".to_owned()))?;
789
790 if sp > 32 {
791 let oid_str = oid_hint.map(|o| o.to_hex()).unwrap_or_else(|| {
792 hash_algo_digest_hex(HashAlgo::Sha1, raw).unwrap_or_else(|_| "?".to_owned())
793 });
794 return Err(Error::ObjectHeaderTooLong { oid: oid_str });
795 }
796
797 let kind = ObjectKind::from_bytes(&header[..sp])?;
798
799 let size_str = std::str::from_utf8(&header[sp + 1..])
800 .map_err(|_| Error::CorruptObject("non-UTF-8 object size".to_owned()))?;
801 let size: usize = size_str
802 .parse()
803 .map_err(|_| Error::CorruptObject(format!("invalid object size: {size_str}")))?;
804
805 if data.len() != size {
806 return Err(Error::CorruptObject(format!(
807 "object size mismatch: header says {size} but got {}",
808 data.len()
809 )));
810 }
811
812 Ok(Object::new(kind, data))
813}
814
815fn hash_algo_digest_hex(algo: HashAlgo, data: &[u8]) -> Result<String> {
816 Ok(algo.digest(data)?.to_hex())
817}
818
819static LOOSE_TMP_SEQ: AtomicU64 = AtomicU64::new(0);
822
823fn unique_loose_tmp_path(prefix_dir: &Path) -> PathBuf {
826 let seq = LOOSE_TMP_SEQ.fetch_add(1, Ordering::Relaxed);
827 prefix_dir.join(format!("tmp_obj_{}_{seq}", std::process::id()))
828}
829
830fn deflate_object_into(
832 file: File,
833 kind: ObjectKind,
834 data: &[u8],
835 compression: Compression,
836) -> Result<()> {
837 let zlib_err = |e: io::Error| Error::Zlib(e.to_string());
838 let mut encoder = ZlibEncoder::new(file, compression);
839 write!(encoder, "{} {}\0", kind, data.len()).map_err(zlib_err)?;
840 encoder.write_all(data).map_err(zlib_err)?;
841 encoder.finish().map_err(zlib_err)?;
842 Ok(())
843}
844
845fn read_payload_chunks(
846 reader: &mut dyn Read,
847 size: u64,
848 mut sink: impl FnMut(&[u8]) -> Result<()>,
849) -> Result<()> {
850 let mut remaining = size;
851 let mut buf = [0u8; STREAM_CHUNK];
852 while remaining > 0 {
853 let want = usize::try_from(remaining.min(STREAM_CHUNK as u64)).map_err(|_| {
854 Error::CorruptObject(format!("object size {size} exceeds address space"))
855 })?;
856 let n = match reader.read(&mut buf[..want]) {
857 Ok(0) => {
858 return Err(Error::ObjectStreamTruncated {
859 expected: size,
860 actual: size - remaining,
861 })
862 }
863 Ok(n) => n,
864 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
865 Err(e) => return Err(Error::Io(e)),
866 };
867 sink(&buf[..n])?;
868 remaining -= n as u64;
869 }
870 Ok(())
871}
872
873impl LooseStore {
874 pub(crate) fn write_payload_stream(
878 &self,
879 kind: ObjectKind,
880 size: u64,
881 reader: &mut dyn Read,
882 options: WriteOptions,
883 ) -> Result<ObjectId> {
884 let (first, tail) = crate::odb::stream::read_first_payload_chunk(reader, size)?;
885 let compression =
886 crate::odb::stream::compression_after_first_chunk(&first, self.compression);
887 self.write_payload_stream_with(kind, size, options, compression, |chunk_sink| {
888 chunk_sink(&first)?;
889 if tail > 0 {
890 read_payload_chunks(reader, tail, chunk_sink)
891 } else {
892 Ok(())
893 }
894 })
895 }
896
897 pub(crate) fn write_payload_stream_with(
899 &self,
900 kind: ObjectKind,
901 size: u64,
902 options: WriteOptions,
903 compression: Compression,
904 mut supply: impl FnMut(&mut dyn FnMut(&[u8]) -> Result<()>) -> Result<()>,
905 ) -> Result<ObjectId> {
906 let prefix_dir = &self.objects_dir;
907 let mut hasher = ObjectPayloadHasher::new(self.hash_algo, kind, size);
908 let tmp_path = unique_loose_tmp_path(prefix_dir);
909 fs::create_dir_all(prefix_dir).map_err(Error::Io)?;
910 let file = fs::OpenOptions::new()
911 .write(true)
912 .create_new(true)
913 .open(&tmp_path)
914 .map_err(Error::Io)?;
915 let zlib_err = |e: io::Error| Error::Zlib(e.to_string());
916 let mut encoder = ZlibEncoder::new(file, compression);
917 if let Err(e) = (|| {
918 write!(encoder, "{} {}\0", kind, size).map_err(zlib_err)?;
919 supply(&mut |chunk| {
920 hasher.update(chunk);
921 encoder.write_all(chunk).map_err(zlib_err)
922 })?;
923 encoder.finish().map_err(zlib_err)?;
924 Ok::<(), Error>(())
925 })() {
926 let _ = fs::remove_file(&tmp_path);
927 return Err(e);
928 }
929 let oid = match hasher.finish() {
930 Ok(oid) => oid,
931 Err(e) => {
932 let _ = fs::remove_file(&tmp_path);
933 return Err(e);
934 }
935 };
936 if let Some(existing) = self.try_finish_if_loose_present(&oid, options)? {
937 let _ = fs::remove_file(&tmp_path);
938 return Ok(existing);
939 }
940 let path = self.object_path(&oid);
941 let dest_prefix = path
942 .parent()
943 .ok_or_else(|| Error::PathError("object path has no parent".to_owned()))?;
944 if let Err(e) = fs::create_dir_all(dest_prefix) {
945 let _ = fs::remove_file(&tmp_path);
946 return Err(Error::Io(e));
947 }
948 match fs::rename(&tmp_path, &path) {
949 Ok(()) => {
950 set_loose_object_mode(&path);
951 Ok(oid)
952 }
953 Err(e) => {
954 let _ = fs::remove_file(&tmp_path);
955 if path.is_file() {
956 return Ok(oid);
957 }
958 Err(Error::Io(e))
959 }
960 }
961 }
962}
963
964fn publish_loose_object(
977 objects_dir: &Path,
978 path: &Path,
979 prefix_dir: &Path,
980 prefix_dir_ready: bool,
981 mut fill: impl FnMut(File) -> Result<()>,
982) -> Result<()> {
983 const MAX_ATTEMPTS: u32 = 4;
984 let mut attempt = 0;
985 loop {
986 attempt += 1;
987 let can_retry = attempt < MAX_ATTEMPTS;
988 if !prefix_dir_ready || attempt > 1 {
989 fs::create_dir_all(prefix_dir).map_err(|source| {
990 map_objects_dir_io(
991 objects_dir,
992 prefix_dir,
993 GitDirPathOperation::CreateDir,
994 source,
995 )
996 })?;
997 }
998 let tmp_path = unique_loose_tmp_path(prefix_dir);
999 let file = match fs::OpenOptions::new()
1000 .write(true)
1001 .create_new(true)
1002 .open(&tmp_path)
1003 {
1004 Ok(file) => file,
1005 Err(e)
1006 if can_retry
1007 && matches!(
1008 e.kind(),
1009 io::ErrorKind::NotFound | io::ErrorKind::AlreadyExists
1010 ) =>
1011 {
1012 continue;
1013 }
1014 Err(e) => {
1015 return Err(map_objects_dir_io(
1016 objects_dir,
1017 &tmp_path,
1018 GitDirPathOperation::WriteObject,
1019 e,
1020 ));
1021 }
1022 };
1023 if let Err(e) = fill(file) {
1024 let _ = fs::remove_file(&tmp_path);
1025 return Err(e);
1026 }
1027 match fs::rename(&tmp_path, path) {
1028 Ok(()) => {
1029 set_loose_object_mode(path);
1030 return Ok(());
1031 }
1032 Err(e) => {
1033 let _ = fs::remove_file(&tmp_path);
1034 if path.is_file() {
1035 return Ok(());
1036 }
1037 if can_retry && e.kind() == io::ErrorKind::NotFound {
1038 continue;
1039 }
1040 return Err(map_objects_dir_io(
1041 objects_dir,
1042 path,
1043 GitDirPathOperation::WriteObject,
1044 e,
1045 ));
1046 }
1047 }
1048 }
1049}
1050
1051fn set_loose_object_mode(path: &Path) {
1052 #[cfg(unix)]
1053 {
1054 use std::os::unix::fs::PermissionsExt;
1055 let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o444));
1056 }
1057 let _ = path;
1058}
1059
1060fn is_loose_suffix_for_algo(suffix: &str, algo: HashAlgo) -> bool {
1061 suffix.len() == algo.hex_len() - 2 && suffix.bytes().all(|b| b.is_ascii_hexdigit())
1062}
1063
1064struct LoosePayloadReader {
1066 file: File,
1067 read_budget: LooseFileReadBudget,
1068 dec: Decompress,
1069 pending: Vec<u8>,
1071 pending_pos: usize,
1072 input_exhausted: bool,
1074 zlib_complete: bool,
1076 preset_dictionary: bool,
1077 kind: ObjectKind,
1078 payload_size: u64,
1079 payload_remaining: u64,
1080 phase: LooseReadPhase,
1081 header_buf: Vec<u8>,
1082 extra_payload: Vec<u8>,
1084}
1085
1086enum LooseReadPhase {
1087 Header,
1088 Payload,
1089}
1090
1091impl LoosePayloadReader {
1092 fn open(mut file: File) -> Result<Self> {
1093 let read_budget = LooseFileReadBudget::new(&file)?;
1094 let mut hdr = [0u8; 2];
1095 file.read_exact(&mut hdr).map_err(Error::Io)?;
1096 let cmf_flg = u16::from(hdr[0]) << 8 | u16::from(hdr[1]);
1097 let looks_like_zlib_header = cmf_flg != 0 && cmf_flg % 31 == 0;
1098 let preset_dictionary = looks_like_zlib_header && (hdr[1] & 0x20) != 0;
1099 let mut read_budget = read_budget;
1100 read_budget.note_consumed(2);
1101 let mut reader = Self {
1102 file,
1103 read_budget,
1104 dec: Decompress::new(true),
1105 pending: hdr.to_vec(),
1106 pending_pos: 0,
1107 input_exhausted: false,
1108 zlib_complete: false,
1109 preset_dictionary,
1110 kind: ObjectKind::Blob,
1111 payload_size: 0,
1112 payload_remaining: 0,
1113 phase: LooseReadPhase::Header,
1114 header_buf: Vec::with_capacity(64),
1115 extra_payload: Vec::new(),
1116 };
1117 reader.parse_header()?;
1118 Ok(reader)
1119 }
1120
1121 fn parse_header(&mut self) -> Result<()> {
1122 let mut header_buf = std::mem::take(&mut self.header_buf);
1123 loop {
1124 self.pump_zlib(&mut |bytes| {
1125 header_buf.extend_from_slice(bytes);
1126 })?;
1127 if header_buf.contains(&0) || header_buf.len() >= 128 {
1128 break;
1129 }
1130 if self.input_exhausted && self.pending_pos == self.pending.len() {
1131 break;
1132 }
1133 }
1134 let nul = header_buf
1135 .iter()
1136 .position(|&b| b == 0)
1137 .ok_or_else(|| Error::CorruptObject("missing NUL in object header".to_owned()))?;
1138 let (kind, size) = parse_object_header_prefix(&header_buf[..=nul])?;
1139 self.extra_payload = header_buf[nul + 1..].to_vec();
1140 self.header_buf = header_buf;
1141 self.kind = kind;
1142 self.payload_size = size;
1143 self.payload_remaining = size;
1144 self.phase = LooseReadPhase::Payload;
1145 Ok(())
1146 }
1147
1148 fn pump_zlib(&mut self, on_out: &mut dyn FnMut(&[u8])) -> Result<()> {
1149 let mut scratch = [0u8; LOOSE_INFLATE_OUTPUT_CHUNK];
1150 let produced = self.inflate_into(&mut scratch)?;
1151 if produced > 0 {
1152 on_out(&scratch[..produced]);
1153 }
1154 Ok(())
1155 }
1156
1157 fn inflate_into(&mut self, out: &mut [u8]) -> Result<usize> {
1161 if self.zlib_complete {
1162 return Ok(0);
1163 }
1164 let cap = out.len().min(LOOSE_INFLATE_OUTPUT_CHUNK);
1165 let out = &mut out[..cap];
1166 if self.pending_pos == self.pending.len() && !self.input_exhausted {
1167 let mut buf = [0u8; LOOSE_INFLATE_INPUT_CHUNK];
1168 let n = self.read_budget.read(&mut self.file, &mut buf)?;
1169 self.pending.clear();
1170 self.pending.extend_from_slice(&buf[..n]);
1171 self.pending_pos = 0;
1172 self.input_exhausted = n == 0;
1173 }
1174 let flush = if self.input_exhausted && self.pending_pos == self.pending.len() {
1175 FlushDecompress::Finish
1176 } else {
1177 FlushDecompress::None
1178 };
1179 let before_in = self.dec.total_in();
1180 let before_out = self.dec.total_out();
1181 let status = match self
1182 .dec
1183 .decompress(&self.pending[self.pending_pos..], out, flush)
1184 {
1185 Ok(s) => s,
1186 Err(e) => {
1187 if self.preset_dictionary {
1188 return Err(Error::Zlib("needs dictionary".to_owned()));
1189 }
1190 return Err(Error::Zlib(e.to_string()));
1191 }
1192 };
1193 self.pending_pos += (self.dec.total_in() - before_in) as usize;
1195 let produced = (self.dec.total_out() - before_out) as usize;
1196 if matches!(status, Status::StreamEnd) {
1197 self.zlib_complete = true;
1198 }
1199 Ok(produced)
1200 }
1201
1202 fn ensure_zlib_complete(&mut self) -> io::Result<()> {
1204 if self.zlib_complete {
1205 return Ok(());
1206 }
1207 if !self.extra_payload.is_empty() {
1208 return Err(io::Error::other(
1209 "corrupt object: inflated bytes remain after declared payload size",
1210 ));
1211 }
1212 loop {
1213 let mut excess = 0usize;
1214 self.pump_zlib(&mut |bytes| {
1215 excess += bytes.len();
1216 })
1217 .map_err(|e| io::Error::other(e.to_string()))?;
1218 if excess > 0 {
1219 return Err(io::Error::other(
1220 "corrupt object: excess decompressed data after declared payload size",
1221 ));
1222 }
1223 if self.zlib_complete {
1224 return Ok(());
1225 }
1226 if self.input_exhausted && self.pending_pos == self.pending.len() {
1227 return Err(io::Error::other(
1228 "corrupt object: zlib stream ended before trailer validation",
1229 ));
1230 }
1231 }
1232 }
1233}
1234
1235impl Read for LoosePayloadReader {
1236 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1237 if !matches!(self.phase, LooseReadPhase::Payload) {
1238 return Ok(0);
1239 }
1240 if self.payload_remaining == 0 {
1241 self.ensure_zlib_complete()?;
1242 return Ok(0);
1243 }
1244 if buf.is_empty() {
1245 return Ok(0);
1246 }
1247 let mut delivered = 0usize;
1248 while delivered < buf.len() && self.payload_remaining > 0 {
1249 if !self.extra_payload.is_empty() {
1250 let take = self
1251 .extra_payload
1252 .len()
1253 .min(buf.len() - delivered)
1254 .min(self.payload_remaining as usize);
1255 buf[delivered..delivered + take].copy_from_slice(&self.extra_payload[..take]);
1256 self.extra_payload.drain(..take);
1257 delivered += take;
1258 self.payload_remaining -= take as u64;
1259 continue;
1260 }
1261 let want = usize::try_from(self.payload_remaining)
1262 .map_or(buf.len() - delivered, |r| r.min(buf.len() - delivered));
1263 let produced = self
1264 .inflate_into(&mut buf[delivered..delivered + want])
1265 .map_err(|e| io::Error::other(e.to_string()))?;
1266 if produced == 0 {
1267 if self.zlib_complete
1268 || (self.input_exhausted && self.pending_pos == self.pending.len())
1269 {
1270 return Err(io::Error::other(format!(
1271 "corrupt object: zlib stream ended with {} payload bytes remaining",
1272 self.payload_remaining
1273 )));
1274 }
1275 continue;
1276 }
1277 delivered += produced;
1278 self.payload_remaining -= produced as u64;
1279 }
1280 if self.payload_remaining == 0 {
1281 self.ensure_zlib_complete()?;
1282 }
1283 Ok(delivered)
1284 }
1285}
1286
1287#[cfg(test)]
1288mod tests {
1289 use super::*;
1290 use std::io::Read;
1291
1292 use super::super::{ObjectStore, WritableObjectStore};
1293
1294 const READ_CHUNK: usize = LOOSE_INFLATE_INPUT_CHUNK;
1295
1296 #[test]
1297 fn lookup_prefix_scans_single_fanout_dir() {
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 oid =
1306 WritableObjectStore::write(&store, ObjectKind::Blob, b"x", WriteOptions::default())
1307 .unwrap();
1308 let mut out = Vec::new();
1309 ObjectStore::lookup_prefix(&store, &oid.to_hex()[..4], 0, &mut out).unwrap();
1310 assert_eq!(out, vec![oid]);
1311 }
1312
1313 fn temp_store() -> (tempfile::TempDir, LooseStore) {
1314 let dir = tempfile::tempdir().unwrap();
1315 let store = LooseStore::new(
1316 dir.path().join("objects"),
1317 HashAlgo::Sha1,
1318 Compression::default(),
1319 );
1320 fs::create_dir_all(store.objects_dir()).unwrap();
1321 (dir, store)
1322 }
1323
1324 fn tmp_entries(store: &LooseStore) -> Vec<PathBuf> {
1325 fs::read_dir(store.objects_dir())
1326 .unwrap()
1327 .flatten()
1328 .filter(|shard| shard.file_type().is_ok_and(|t| t.is_dir()))
1329 .flat_map(|shard| fs::read_dir(shard.path()).unwrap().flatten())
1330 .map(|entry| entry.path())
1331 .filter(|path| {
1332 path.file_name()
1333 .is_some_and(|n| n.to_string_lossy().starts_with("tmp_"))
1334 })
1335 .collect()
1336 }
1337
1338 #[test]
1339 fn identical_writes_through_separate_handles_all_succeed() {
1340 let (_dir, store) = temp_store();
1341 let threads = 8;
1342 let barrier = std::sync::Arc::new(std::sync::Barrier::new(threads));
1343 let payloads: Vec<Vec<u8>> = (0..200)
1344 .map(|j| format!("same {j}").into_bytes())
1345 .chain(std::iter::once(vec![0x5a; 1024 * 1024]))
1346 .collect();
1347 let payloads = std::sync::Arc::new(payloads);
1348 let handles: Vec<_> = (0..threads)
1349 .map(|_| {
1350 let store = store.clone();
1351 let barrier = std::sync::Arc::clone(&barrier);
1352 let payloads = std::sync::Arc::clone(&payloads);
1353 std::thread::spawn(move || {
1354 barrier.wait();
1355 payloads
1356 .iter()
1357 .map(|data| {
1358 WritableObjectStore::write(
1359 &store,
1360 ObjectKind::Blob,
1361 data,
1362 WriteOptions::default(),
1363 )
1364 })
1365 .collect::<Vec<_>>()
1366 })
1367 })
1368 .collect();
1369 for handle in handles {
1370 for (data, result) in payloads.iter().zip(handle.join().unwrap()) {
1371 let oid = result.expect("concurrent identical write succeeds");
1372 let obj = ObjectStore::read(&store, &oid).unwrap().expect("object");
1373 assert!(obj.data == *data);
1374 }
1375 }
1376 assert!(tmp_entries(&store).is_empty());
1377 }
1378
1379 #[test]
1380 fn prehashed_write_recreates_pruned_fanout_dir_marked_ready() {
1381 let (_dir, store) = temp_store();
1382 let data = b"fanout pruned under a ready writer";
1383 let oid = hash::hash_object(HashAlgo::Sha1, ObjectKind::Blob, data).unwrap();
1384 let zlib = crate::odb::zlib_compress_blob_loose(data, Compression::default()).unwrap();
1385 let options = WriteOptions {
1386 loose_prefix_dirs_ready: true,
1387 ..WriteOptions::default()
1388 };
1389 assert!(!store.object_path(&oid).parent().unwrap().exists());
1390 let written = store.write_zlib_prehashed(&oid, &zlib, options).unwrap();
1391 assert_eq!(written, oid);
1392 assert_eq!(ObjectStore::read(&store, &oid).unwrap().unwrap().data, data);
1393 assert!(tmp_entries(&store).is_empty());
1394 }
1395
1396 #[test]
1397 fn unique_tmp_paths_differ_and_are_not_loose_names() {
1398 let prefix = Path::new("objects/ab");
1399 let a = unique_loose_tmp_path(prefix);
1400 let b = unique_loose_tmp_path(prefix);
1401 assert_ne!(a, b);
1402 for path in [a, b] {
1403 let name = path.file_name().unwrap().to_str().unwrap();
1404 assert!(name.starts_with("tmp_obj_"), "{name}");
1405 assert!(!is_loose_suffix_for_algo(name, HashAlgo::Sha1));
1406 assert!(!is_loose_suffix_for_algo(name, HashAlgo::Sha256));
1407 }
1408 }
1409
1410 #[test]
1411 fn read_info_and_read_agree_for_object_spanning_read_chunks() {
1412 let (_dir, store) = temp_store();
1413 let data: Vec<u8> = (0..3 * READ_CHUNK as u32)
1414 .map(|i| (i.wrapping_mul(2_654_435_761) >> 13) as u8)
1415 .collect();
1416 let oid =
1417 WritableObjectStore::write(&store, ObjectKind::Blob, &data, WriteOptions::default())
1418 .unwrap();
1419 let info = ObjectStore::read_info(&store, &oid).unwrap().expect("info");
1420 assert_eq!(info.kind, ObjectKind::Blob);
1421 assert_eq!(info.size, data.len() as u64);
1422 let obj = ObjectStore::read(&store, &oid).unwrap().expect("object");
1423 assert_eq!(obj.kind, ObjectKind::Blob);
1424 assert_eq!(obj.data, data);
1425 let mut buf = Vec::new();
1426 let kind = ObjectStore::read_into(&store, &oid, &mut buf).unwrap();
1427 assert_eq!(kind, Some(ObjectKind::Blob));
1428 assert_eq!(buf, data);
1429 }
1430
1431 #[test]
1432 fn read_unless_larger_reads_small_and_skips_large_objects() {
1433 let (_dir, store) = temp_store();
1434 let data = vec![b'x'; 3 * READ_CHUNK];
1435 let oid =
1436 WritableObjectStore::write(&store, ObjectKind::Blob, &data, WriteOptions::default())
1437 .unwrap();
1438 match store.read_unless_larger(&oid, data.len() as u64).unwrap() {
1439 Some(BoundedRead::Object(obj)) => {
1440 assert_eq!(obj.kind, ObjectKind::Blob);
1441 assert_eq!(obj.data, data);
1442 }
1443 other => panic!("expected the object, got {other:?}"),
1444 }
1445 assert!(matches!(
1446 store
1447 .read_unless_larger(&oid, data.len() as u64 - 1)
1448 .unwrap(),
1449 Some(BoundedRead::Larger)
1450 ));
1451 let missing = ObjectId::from_hex("1234567890123456789012345678901234567890").unwrap();
1452 assert!(store.read_unless_larger(&missing, 0).unwrap().is_none());
1453 }
1454
1455 #[test]
1456 fn stream_reader_delivers_payload_through_tiny_and_huge_buffers() {
1457 let (_dir, store) = temp_store();
1458 let data: Vec<u8> = (0..5 * READ_CHUNK as u32)
1459 .map(|i| (i.wrapping_mul(2_654_435_761) >> 11) as u8)
1460 .collect();
1461 let oid =
1462 WritableObjectStore::write(&store, ObjectKind::Blob, &data, WriteOptions::default())
1463 .unwrap();
1464 for chunk_len in [1usize, 7, 4096, 10 * READ_CHUNK] {
1465 let mut stream = ObjectStore::open_stream(&store, &oid)
1466 .unwrap()
1467 .expect("stream");
1468 let mut out = Vec::new();
1469 let mut chunk = vec![0u8; chunk_len];
1470 loop {
1471 let n = stream.reader.read(&mut chunk).unwrap();
1472 if n == 0 {
1473 break;
1474 }
1475 out.extend_from_slice(&chunk[..n]);
1476 }
1477 assert!(out == data, "chunk length {chunk_len}");
1478 }
1479 }
1480
1481 #[test]
1482 fn read_info_is_none_for_missing_object_or_directory_at_its_path() {
1483 let (_dir, store) = temp_store();
1484 let missing = ObjectId::from_hex("1234567890123456789012345678901234567890").unwrap();
1485 assert!(ObjectStore::read_info(&store, &missing).unwrap().is_none());
1486 fs::create_dir_all(store.object_path(&missing)).unwrap();
1487 assert!(ObjectStore::read_info(&store, &missing).unwrap().is_none());
1488 }
1489
1490 #[test]
1491 fn lookup_prefix_one_hex_char_does_not_overflow() {
1492 let dir = tempfile::tempdir().unwrap();
1493 let store = LooseStore::new(
1494 dir.path().join("objects"),
1495 HashAlgo::Sha1,
1496 Compression::default(),
1497 );
1498 fs::create_dir_all(store.objects_dir()).unwrap();
1499 let oid =
1500 WritableObjectStore::write(&store, ObjectKind::Blob, b"x", WriteOptions::default())
1501 .unwrap();
1502 let mut out = Vec::new();
1503 ObjectStore::lookup_prefix(&store, &oid.to_hex()[..1], 0, &mut out).unwrap();
1504 assert!(out.contains(&oid));
1505 }
1506
1507 #[test]
1508 fn truncated_zlib_stream_errors_on_read() {
1509 let dir = tempfile::tempdir().unwrap();
1510 let store = LooseStore::new(
1511 dir.path().join("objects"),
1512 HashAlgo::Sha1,
1513 Compression::default(),
1514 );
1515 fs::create_dir_all(store.objects_dir()).unwrap();
1516 let payload = vec![0u8; 100_000];
1517 let oid =
1518 WritableObjectStore::write(&store, ObjectKind::Blob, &payload, WriteOptions::default())
1519 .unwrap();
1520 let path = store.object_path(&oid);
1521 let mut bytes = fs::read(&path).unwrap();
1522 bytes.truncate(bytes.len() / 2);
1523 #[cfg(unix)]
1524 {
1525 use std::os::unix::fs::PermissionsExt;
1526 fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
1527 }
1528 fs::write(&path, &bytes).unwrap();
1529 let mut stream = ObjectStore::open_stream(&store, &oid)
1530 .unwrap()
1531 .expect("stream");
1532 let mut out = Vec::new();
1533 let err = stream.reader.read_to_end(&mut out).unwrap_err();
1534 assert!(
1535 err.to_string().contains("payload bytes remaining")
1536 || err.to_string().contains("corrupt object"),
1537 "unexpected error: {err}"
1538 );
1539 assert!(out.len() < payload.len());
1540 }
1541
1542 #[test]
1543 fn truncated_zlib_checksum_only_errors_on_read() {
1544 let dir = tempfile::tempdir().unwrap();
1545 let store = LooseStore::new(
1546 dir.path().join("objects"),
1547 HashAlgo::Sha1,
1548 Compression::default(),
1549 );
1550 fs::create_dir_all(store.objects_dir()).unwrap();
1551 let payload = vec![0u8; 100_000];
1552 let oid =
1553 WritableObjectStore::write(&store, ObjectKind::Blob, &payload, WriteOptions::default())
1554 .unwrap();
1555 let path = store.object_path(&oid);
1556 let mut bytes = fs::read(&path).unwrap();
1557 assert!(bytes.len() > 1);
1558 bytes.pop();
1559 #[cfg(unix)]
1560 {
1561 use std::os::unix::fs::PermissionsExt;
1562 fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
1563 }
1564 fs::write(&path, &bytes).unwrap();
1565 match ObjectStore::read(&store, &oid) {
1566 Ok(None) => {}
1567 Ok(Some(_)) => panic!("full read must not succeed on checksum-truncated loose object"),
1568 Err(_) => {}
1569 }
1570 let mut stream = ObjectStore::open_stream(&store, &oid)
1571 .expect("open_stream")
1572 .expect("stream hit");
1573 let mut out = Vec::new();
1574 let err = stream.reader.read_to_end(&mut out).unwrap_err();
1575 assert!(
1576 err.to_string().contains("trailer validation")
1577 || err.to_string().contains("corrupt object"),
1578 "unexpected error: {err}"
1579 );
1580 }
1581
1582 #[test]
1583 fn stream_sixteen_mib_peak_buffer_under_one_mib() {
1584 let dir = tempfile::tempdir().unwrap();
1585 let store = LooseStore::new(
1586 dir.path().join("objects"),
1587 HashAlgo::Sha1,
1588 Compression::default(),
1589 );
1590 fs::create_dir_all(store.objects_dir()).unwrap();
1591 let payload = vec![0xCDu8; 16 * 1024 * 1024];
1592 let oid =
1593 WritableObjectStore::write(&store, ObjectKind::Blob, &payload, WriteOptions::default())
1594 .unwrap();
1595 let mut stream = ObjectStore::open_stream(&store, &oid)
1596 .unwrap()
1597 .expect("stream");
1598 assert_eq!(stream.size, payload.len() as u64);
1599 let mut out = Vec::new();
1600 let mut chunk = [0u8; 64 * 1024];
1601 let mut peak = 0usize;
1602 loop {
1603 let n = stream.reader.read(&mut chunk).unwrap();
1604 if n == 0 {
1605 break;
1606 }
1607 peak = peak.max(n);
1608 out.extend_from_slice(&chunk[..n]);
1609 }
1610 assert_eq!(out, payload);
1611 assert!(
1612 peak < 1024 * 1024,
1613 "peak in-flight buffer {peak} must stay under 1 MiB"
1614 );
1615 }
1616
1617 #[test]
1618 fn external_loose_write_in_fanout_visible_after_negative_cache() {
1619 use std::process::{Command, Stdio};
1620
1621 use crate::odb::Odb;
1622
1623 let dir = tempfile::tempdir().unwrap();
1624 let objects = dir.path().join("objects");
1625 fs::create_dir_all(&objects).unwrap();
1626 let odb = Odb::new(&objects);
1627 let payload = b"written externally into an existing fanout dir\n";
1628 let oid = odb.hash(ObjectKind::Blob, payload).unwrap();
1629 assert!(matches!(odb.read(&oid), Err(Error::ObjectNotFound(_))));
1630
1631 let mut child = Command::new("git")
1632 .args(["hash-object", "-w", "--stdin"])
1633 .stdin(Stdio::piped())
1634 .stdout(Stdio::piped())
1635 .env("GIT_OBJECT_DIRECTORY", &objects)
1636 .env("GIT_CONFIG_GLOBAL", "/dev/null")
1637 .env("GIT_CONFIG_SYSTEM", "/dev/null")
1638 .spawn()
1639 .expect("git hash-object");
1640 if let Some(mut stdin) = child.stdin.take() {
1641 use std::io::Write;
1642 stdin.write_all(payload).unwrap();
1643 }
1644 let out = child.wait_with_output().expect("git wait");
1645 assert!(
1646 out.status.success(),
1647 "git hash-object: {}",
1648 String::from_utf8_lossy(&out.stderr)
1649 );
1650 assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), oid.to_hex());
1651
1652 let obj = odb
1653 .read(&oid)
1654 .expect("external loose write must be visible");
1655 assert_eq!(obj.data, payload);
1656 }
1657}