1use std::fs;
21use std::io::{Read, Write};
22use std::path::{Path, PathBuf};
23use std::sync::{Arc, Mutex, OnceLock};
24
25use flate2::read::ZlibDecoder;
26use flate2::write::ZlibEncoder;
27use flate2::Compression;
28use sha1::{Digest, Sha1};
29use sha2::Sha256;
30
31use crate::config::ConfigSet;
32use crate::error::{Error, Result};
33use crate::midx::{midx_oid_listed_in_tip, try_read_object_via_midx};
34use crate::objects::{HashAlgo, Object, ObjectId, ObjectKind};
35use crate::pack;
36
37fn read_zlib_loose_payload(mut file: fs::File) -> Result<Vec<u8>> {
43 let mut hdr = [0u8; 2];
44 file.read_exact(&mut hdr).map_err(Error::Io)?;
45 let cmf_flg = u16::from(hdr[0]) << 8 | u16::from(hdr[1]);
46 let looks_like_zlib_header = cmf_flg != 0 && cmf_flg % 31 == 0;
47 let preset_dictionary = looks_like_zlib_header && (hdr[1] & 0x20) != 0;
48 let mut decoder = ZlibDecoder::new(hdr.as_slice().chain(file));
49 let mut raw = Vec::new();
50 match decoder.read_to_end(&mut raw) {
51 Ok(_) => Ok(raw),
52 Err(e) => {
53 if preset_dictionary {
54 Err(Error::Zlib("needs dictionary".to_owned()))
55 } else {
56 Err(Error::Zlib(e.to_string()))
57 }
58 }
59 }
60}
61
62fn exists_materialized_in_objects_dir(objects_dir: &Path, oid: &ObjectId) -> bool {
64 let loose = objects_dir
65 .join(oid.loose_prefix())
66 .join(oid.loose_suffix());
67 if loose.exists() {
68 return true;
69 }
70 let Ok(indexes) = pack::read_local_pack_indexes_cached(objects_dir) else {
71 return false;
72 };
73 for idx in &indexes {
74 if idx.pack_path.with_extension("promisor").is_file() {
75 continue;
76 }
77 if idx.contains(oid) {
78 return true;
79 }
80 }
81 false
82}
83
84#[derive(Clone)]
86pub struct Odb {
87 objects_dir: PathBuf,
88 work_tree: Option<PathBuf>,
90 submodule_alternate_dirs: Arc<Mutex<Vec<PathBuf>>>,
92 config_git_dir: Option<PathBuf>,
94 core_multi_pack_index_cache: Arc<OnceLock<bool>>,
100 mem_overlay: Arc<Mutex<Option<std::collections::HashMap<ObjectId, (ObjectKind, Vec<u8>)>>>>,
105 hash_algo_cache: Arc<OnceLock<HashAlgo>>,
109}
110
111impl std::fmt::Debug for Odb {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 f.debug_struct("Odb")
114 .field("objects_dir", &self.objects_dir)
115 .field("work_tree", &self.work_tree)
116 .field("submodule_alternate_dirs", &"<mutex>")
117 .field("config_git_dir", &self.config_git_dir)
118 .finish()
119 }
120}
121
122impl Odb {
123 #[must_use]
128 pub fn new(objects_dir: &Path) -> Self {
129 Self {
130 objects_dir: objects_dir.to_path_buf(),
131 work_tree: None,
132 submodule_alternate_dirs: Arc::new(Mutex::new(Vec::new())),
133 config_git_dir: None,
134 core_multi_pack_index_cache: Arc::new(OnceLock::new()),
135 mem_overlay: Arc::new(Mutex::new(None)),
136 hash_algo_cache: Arc::new(OnceLock::new()),
137 }
138 }
139
140 #[must_use]
142 pub fn with_work_tree(objects_dir: &Path, work_tree: &Path) -> Self {
143 Self {
144 objects_dir: objects_dir.to_path_buf(),
145 work_tree: Some(work_tree.to_path_buf()),
146 submodule_alternate_dirs: Arc::new(Mutex::new(Vec::new())),
147 config_git_dir: None,
148 core_multi_pack_index_cache: Arc::new(OnceLock::new()),
149 mem_overlay: Arc::new(Mutex::new(None)),
150 hash_algo_cache: Arc::new(OnceLock::new()),
151 }
152 }
153
154 pub fn enable_mem_overlay(&self) {
159 if let Ok(mut guard) = self.mem_overlay.lock() {
160 *guard = Some(std::collections::HashMap::new());
161 }
162 }
163
164 pub fn disable_mem_overlay(&self) {
166 if let Ok(mut guard) = self.mem_overlay.lock() {
167 *guard = None;
168 }
169 }
170
171 fn overlay_store(&self, oid: ObjectId, kind: ObjectKind, data: &[u8]) -> bool {
174 if let Ok(mut guard) = self.mem_overlay.lock() {
175 if let Some(map) = guard.as_mut() {
176 map.entry(oid).or_insert_with(|| (kind, data.to_vec()));
177 return true;
178 }
179 }
180 false
181 }
182
183 fn overlay_read(&self, oid: &ObjectId) -> Option<Object> {
185 if let Ok(guard) = self.mem_overlay.lock() {
186 if let Some(map) = guard.as_ref() {
187 if let Some((kind, data)) = map.get(oid) {
188 return Some(Object {
189 kind: *kind,
190 data: data.clone(),
191 });
192 }
193 }
194 }
195 None
196 }
197
198 pub fn register_submodule_object_directories_from_index(
202 &self,
203 work_tree: &Path,
204 index: &crate::index::Index,
205 ) {
206 use crate::diff::submodule_embedded_git_dir;
207
208 let Ok(mut dirs) = self.submodule_alternate_dirs.lock() else {
209 return;
210 };
211 dirs.clear();
212 for e in &index.entries {
213 if e.stage() != 0 || e.mode != crate::index::MODE_GITLINK {
214 continue;
215 }
216 let path_str = String::from_utf8_lossy(&e.path);
217 let abs = work_tree.join(path_str.as_ref());
218 let Some(sub_git) = submodule_embedded_git_dir(&abs) else {
219 continue;
220 };
221 let objects = sub_git.join("objects");
222 if !objects.is_dir() {
223 continue;
224 }
225 let canon = objects.canonicalize().unwrap_or(objects);
226 if !dirs.iter().any(|p| p == &canon) {
227 dirs.push(canon);
228 }
229 }
230 }
231
232 #[must_use]
234 pub fn with_config_git_dir(mut self, git_dir: PathBuf) -> Self {
235 self.config_git_dir = Some(git_dir);
236 self
237 }
238
239 #[must_use]
246 pub fn hash_algo(&self) -> HashAlgo {
247 *self.hash_algo_cache.get_or_init(|| {
248 let git_dir = self
249 .config_git_dir
250 .clone()
251 .or_else(|| self.objects_dir.parent().map(Path::to_path_buf));
252 let Some(git_dir) = git_dir else {
253 return HashAlgo::Sha1;
254 };
255 let cfg = ConfigSet::load(Some(&git_dir), true).unwrap_or_default();
256 cfg.get("extensions.objectformat")
257 .and_then(|v| HashAlgo::from_name(&v))
258 .unwrap_or(HashAlgo::Sha1)
259 })
260 }
261
262 fn core_multi_pack_index_enabled(&self) -> bool {
263 *self.core_multi_pack_index_cache.get_or_init(|| {
267 let Some(git_dir) = &self.config_git_dir else {
268 return false;
269 };
270 let cfg = ConfigSet::load(Some(git_dir), true).unwrap_or_default();
271 match cfg.get_bool("core.multiPackIndex") {
272 Some(Ok(b)) => b,
273 Some(Err(_)) => true,
274 None => true,
275 }
276 })
277 }
278
279 #[must_use]
281 pub fn objects_dir(&self) -> &Path {
282 &self.objects_dir
283 }
284
285 #[must_use]
291 pub fn config_git_dir(&self) -> Option<&Path> {
292 self.config_git_dir.as_deref()
293 }
294
295 #[must_use]
297 pub fn object_path(&self, oid: &ObjectId) -> PathBuf {
298 self.objects_dir
299 .join(oid.loose_prefix())
300 .join(oid.loose_suffix())
301 }
302
303 #[must_use]
316 pub fn exists_local(&self, oid: &ObjectId) -> bool {
317 const EMPTY_TREE: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
318 if oid.to_hex() == EMPTY_TREE {
319 return true;
320 }
321 exists_materialized_in_objects_dir(&self.objects_dir, oid)
322 }
323
324 #[must_use]
326 pub fn exists(&self, oid: &ObjectId) -> bool {
327 const EMPTY_TREE_CANON: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
331 const EMPTY_TREE_LEGACY: &str = "4b825dc642cb6eb9a060e54bf899d69f7c6948d4";
332 let hex = oid.to_hex();
333 if hex == EMPTY_TREE_CANON || hex == EMPTY_TREE_LEGACY {
334 return true;
335 }
336 if self.exists_in_dir(&self.objects_dir, oid) {
337 return true;
338 }
339 if let Ok(alts) = pack::read_alternates_recursive(&self.objects_dir) {
341 for alt_dir in &alts {
342 if self.exists_in_dir(alt_dir, oid) {
343 return true;
344 }
345 }
346 }
347 for alt_dir in env_alternate_dirs(self.work_tree.as_deref()) {
349 if self.exists_in_dir(&alt_dir, oid) {
350 return true;
351 }
352 }
353 if let Ok(guard) = self.submodule_alternate_dirs.lock() {
354 for alt_dir in guard.iter() {
355 if self.exists_in_dir(alt_dir, oid) {
356 return true;
357 }
358 }
359 }
360 false
361 }
362
363 fn exists_in_dir(&self, objects_dir: &Path, oid: &ObjectId) -> bool {
365 let loose = objects_dir
366 .join(oid.loose_prefix())
367 .join(oid.loose_suffix());
368 if loose.exists() {
369 return true;
370 }
371 if let Ok(indexes) = pack::read_local_pack_indexes_cached(objects_dir) {
372 for idx in &indexes {
373 if idx.contains(oid) {
374 return true;
375 }
376 }
377 }
378 if objects_dir == self.objects_dir.as_path()
379 && self.config_git_dir.is_some()
380 && self.core_multi_pack_index_enabled()
381 {
382 match midx_oid_listed_in_tip(objects_dir, oid) {
383 Ok(Some(true)) => return true,
384 Ok(Some(false)) | Ok(None) => {}
385 Err(_) => return false,
386 }
387 }
388 false
389 }
390
391 #[must_use]
396 pub fn freshen_object(&self, oid: &ObjectId) -> bool {
397 const EMPTY_TREE_CANON: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
398 const EMPTY_TREE_LEGACY: &str = "4b825dc642cb6eb9a060e54bf899d69f7c6948d4";
399 let hex = oid.to_hex();
400 if hex == EMPTY_TREE_CANON || hex == EMPTY_TREE_LEGACY {
401 return false;
402 }
403
404 let loose = self.object_path(oid);
405 if loose.is_file() {
406 return touch_path_mtime(&loose);
407 }
408
409 if freshen_object_in_objects_dir(&self.objects_dir, oid) {
410 return true;
411 }
412
413 if let Ok(alts) = pack::read_alternates_recursive(&self.objects_dir) {
414 for alt_dir in &alts {
415 if freshen_object_in_objects_dir(alt_dir, oid) {
416 return true;
417 }
418 }
419 }
420
421 for alt_dir in env_alternate_dirs(self.work_tree.as_deref()) {
422 if freshen_object_in_objects_dir(&alt_dir, oid) {
423 return true;
424 }
425 }
426
427 if let Ok(guard) = self.submodule_alternate_dirs.lock() {
428 for alt_dir in guard.iter() {
429 if freshen_object_in_objects_dir(alt_dir, oid) {
430 return true;
431 }
432 }
433 }
434
435 false
436 }
437
438 pub fn read_loose_verify_oid(path: &Path, expected_oid: &ObjectId) -> Result<Object> {
449 let file = fs::File::open(path).map_err(Error::Io)?;
450 let raw = read_zlib_loose_payload(file)?;
451 let obj = parse_object_bytes_with_oid(&raw, expected_oid)?;
452 let computed = hash_object_data_with(expected_oid.algo(), obj.kind, &obj.data);
455 if computed != *expected_oid {
456 return Err(Error::LooseHashMismatch {
457 path: path.display().to_string(),
458 real_oid: computed.to_hex(),
459 });
460 }
461 Ok(obj)
462 }
463
464 pub fn read(&self, oid: &ObjectId) -> Result<Object> {
472 const EMPTY_TREE_CANON: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
474 const EMPTY_TREE_LEGACY: &str = "4b825dc642cb6eb9a060e54bf899d69f7c6948d4";
475 let hex = oid.to_hex();
476 if hex == EMPTY_TREE_CANON || hex == EMPTY_TREE_LEGACY {
477 return Ok(crate::objects::Object {
478 kind: crate::objects::ObjectKind::Tree,
479 data: Vec::new(),
480 });
481 }
482
483 if let Some(obj) = self.overlay_read(oid) {
486 return Ok(obj);
487 }
488
489 if self.config_git_dir.is_some() && self.core_multi_pack_index_enabled() {
494 crate::midx::validate_midx_referenced_packs(&self.objects_dir);
495 }
496
497 let path = self.object_path(oid);
498 match fs::File::open(&path) {
499 Ok(file) => {
500 let raw = read_zlib_loose_payload(file)?;
501 return parse_object_bytes(&raw);
504 }
505 Err(_) => {
506 }
508 }
509
510 if self.config_git_dir.is_some() && self.core_multi_pack_index_enabled() {
511 if let Some(obj) = try_read_object_via_midx(&self.objects_dir, oid)? {
512 return Ok(obj);
513 }
514 }
515
516 match pack::read_object_from_packs(&self.objects_dir, oid) {
518 Ok(obj) => return Ok(obj),
519 Err(Error::ObjectNotFound(_)) => {}
520 Err(err) => return Err(err),
521 }
522
523 let midx_alt = self.config_git_dir.is_some() && self.core_multi_pack_index_enabled();
524
525 if let Ok(alts) = pack::read_alternates_recursive(&self.objects_dir) {
527 for alt_dir in &alts {
528 if let Ok(obj) = Self::read_from_dir(alt_dir, oid, midx_alt) {
529 return Ok(obj);
530 }
531 }
532 }
533
534 for alt_dir in env_alternate_dirs(self.work_tree.as_deref()) {
536 if let Ok(obj) = Self::read_from_dir(&alt_dir, oid, midx_alt) {
537 return Ok(obj);
538 }
539 }
540
541 if let Ok(guard) = self.submodule_alternate_dirs.lock() {
542 for alt_dir in guard.iter() {
543 if let Ok(obj) = Self::read_from_dir(alt_dir, oid, false) {
544 return Ok(obj);
545 }
546 }
547 }
548
549 Err(Error::ObjectNotFound(oid.to_hex()))
550 }
551
552 fn read_from_dir(objects_dir: &Path, oid: &ObjectId, use_midx: bool) -> Result<Object> {
554 let loose = objects_dir
555 .join(oid.loose_prefix())
556 .join(oid.loose_suffix());
557 if let Ok(file) = fs::File::open(&loose) {
558 let raw = read_zlib_loose_payload(file)?;
559 return parse_object_bytes(&raw);
560 }
561 if use_midx {
562 if let Some(obj) = try_read_object_via_midx(objects_dir, oid)? {
563 return Ok(obj);
564 }
565 }
566 match pack::read_object_from_packs(objects_dir, oid) {
567 Ok(obj) => Ok(obj),
568 Err(Error::ObjectNotFound(_)) => Err(Error::ObjectNotFound(oid.to_hex())),
569 Err(err) => Err(err),
570 }
571 }
572
573 #[must_use]
579 pub fn hash_object_data(kind: ObjectKind, data: &[u8]) -> ObjectId {
580 hash_object_data_with(HashAlgo::Sha1, kind, data)
581 }
582
583 #[must_use]
587 pub fn hash(&self, kind: ObjectKind, data: &[u8]) -> ObjectId {
588 hash_object_data_with(self.hash_algo(), kind, data)
589 }
590
591 pub fn write(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
600 let store_bytes = build_store_bytes(kind, data);
601 let oid = hash_bytes_with(self.hash_algo(), &store_bytes);
602
603 if !self.exists(&oid) && self.overlay_store(oid, kind, data) {
606 return Ok(oid);
607 }
608
609 let path = self.object_path(&oid);
610 if path.exists() {
611 let _ = self.freshen_object(&oid);
612 return Ok(oid);
613 }
614 if self.exists(&oid) {
615 let _ = self.freshen_object(&oid);
616 return Ok(oid);
617 }
618
619 let prefix_dir = path
620 .parent()
621 .ok_or_else(|| Error::PathError("object path has no parent".to_owned()))?;
622 fs::create_dir_all(prefix_dir)?;
623
624 let tmp_path = prefix_dir.join(format!("tmp_{}", oid.loose_suffix()));
626 {
627 let tmp_file = fs::File::create(&tmp_path)?;
628 let mut encoder = ZlibEncoder::new(tmp_file, Compression::default());
629 encoder
630 .write_all(&store_bytes)
631 .map_err(|e| Error::Zlib(e.to_string()))?;
632 encoder.finish().map_err(|e| Error::Zlib(e.to_string()))?;
633 }
634 fs::rename(&tmp_path, &path)?;
635 #[cfg(unix)]
636 {
637 use std::os::unix::fs::PermissionsExt;
638 let _ = fs::set_permissions(&path, fs::Permissions::from_mode(0o444));
639 }
640
641 Ok(oid)
642 }
643
644 pub fn write_local(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
660 let store_bytes = build_store_bytes(kind, data);
661 let oid = hash_bytes_with(self.hash_algo(), &store_bytes);
662
663 let path = self.object_path(&oid);
664 if path.exists() {
665 let _ = self.freshen_object(&oid);
666 return Ok(oid);
667 }
668 if exists_materialized_in_objects_dir(&self.objects_dir, &oid) {
669 let _ = self.freshen_object(&oid);
670 return Ok(oid);
671 }
672
673 let prefix_dir = path
674 .parent()
675 .ok_or_else(|| Error::PathError("object path has no parent".to_owned()))?;
676 fs::create_dir_all(prefix_dir)?;
677
678 let tmp_path = prefix_dir.join(format!("tmp_{}", oid.loose_suffix()));
679 {
680 let tmp_file = fs::File::create(&tmp_path)?;
681 let mut encoder = ZlibEncoder::new(tmp_file, Compression::default());
682 encoder
683 .write_all(&store_bytes)
684 .map_err(|e| Error::Zlib(e.to_string()))?;
685 encoder.finish().map_err(|e| Error::Zlib(e.to_string()))?;
686 }
687 fs::rename(&tmp_path, &path)?;
688 #[cfg(unix)]
689 {
690 use std::os::unix::fs::PermissionsExt;
691 let _ = fs::set_permissions(&path, fs::Permissions::from_mode(0o444));
692 }
693
694 Ok(oid)
695 }
696
697 pub fn write_loose_materialize(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
704 let store_bytes = build_store_bytes(kind, data);
705 let oid = hash_bytes_with(self.hash_algo(), &store_bytes);
706 let path = self.object_path(&oid);
707 if path.exists() {
708 let _ = self.freshen_object(&oid);
709 return Ok(oid);
710 }
711
712 let prefix_dir = path
713 .parent()
714 .ok_or_else(|| Error::PathError("object path has no parent".to_owned()))?;
715 fs::create_dir_all(prefix_dir)?;
716
717 let tmp_path = prefix_dir.join(format!("tmp_{}", oid.loose_suffix()));
718 {
719 let tmp_file = fs::File::create(&tmp_path)?;
720 let mut encoder = ZlibEncoder::new(tmp_file, Compression::default());
721 encoder
722 .write_all(&store_bytes)
723 .map_err(|e| Error::Zlib(e.to_string()))?;
724 encoder.finish().map_err(|e| Error::Zlib(e.to_string()))?;
725 }
726 fs::rename(&tmp_path, &path)?;
727 #[cfg(unix)]
728 {
729 use std::os::unix::fs::PermissionsExt;
730 let _ = fs::set_permissions(&path, fs::Permissions::from_mode(0o444));
731 }
732
733 Ok(oid)
734 }
735
736 pub fn write_raw(&self, store_bytes: &[u8]) -> Result<ObjectId> {
746 parse_object_bytes(store_bytes)?;
748
749 let oid = hash_bytes_with(self.hash_algo(), store_bytes);
750 let path = self.object_path(&oid);
751 if path.exists() {
752 let _ = self.freshen_object(&oid);
753 return Ok(oid);
754 }
755 if self.exists(&oid) {
756 let _ = self.freshen_object(&oid);
757 return Ok(oid);
758 }
759
760 let prefix_dir = path
761 .parent()
762 .ok_or_else(|| Error::PathError("object path has no parent".to_owned()))?;
763 fs::create_dir_all(prefix_dir)?;
764
765 let tmp_path = prefix_dir.join(format!("tmp_{}", oid.loose_suffix()));
766 {
767 let tmp_file = fs::File::create(&tmp_path)?;
768 let mut encoder = ZlibEncoder::new(tmp_file, Compression::default());
769 encoder
770 .write_all(store_bytes)
771 .map_err(|e| Error::Zlib(e.to_string()))?;
772 encoder.finish().map_err(|e| Error::Zlib(e.to_string()))?;
773 }
774 fs::rename(&tmp_path, &path)?;
775 #[cfg(unix)]
776 {
777 use std::os::unix::fs::PermissionsExt;
778 let _ = fs::set_permissions(&path, fs::Permissions::from_mode(0o444));
779 }
780
781 Ok(oid)
782 }
783
784 pub fn write_raw_local(&self, store_bytes: &[u8]) -> Result<ObjectId> {
792 parse_object_bytes(store_bytes)?;
793
794 let oid = hash_bytes_with(self.hash_algo(), store_bytes);
795 let path = self.object_path(&oid);
796 if path.exists() {
797 let _ = self.freshen_object(&oid);
798 return Ok(oid);
799 }
800 if exists_materialized_in_objects_dir(&self.objects_dir, &oid) {
801 let _ = self.freshen_object(&oid);
802 return Ok(oid);
803 }
804
805 let prefix_dir = path
806 .parent()
807 .ok_or_else(|| Error::PathError("object path has no parent".to_owned()))?;
808 fs::create_dir_all(prefix_dir)?;
809
810 let tmp_path = prefix_dir.join(format!("tmp_{}", oid.loose_suffix()));
811 {
812 let tmp_file = fs::File::create(&tmp_path)?;
813 let mut encoder = ZlibEncoder::new(tmp_file, Compression::default());
814 encoder
815 .write_all(store_bytes)
816 .map_err(|e| Error::Zlib(e.to_string()))?;
817 encoder.finish().map_err(|e| Error::Zlib(e.to_string()))?;
818 }
819 fs::rename(&tmp_path, &path)?;
820 #[cfg(unix)]
821 {
822 use std::os::unix::fs::PermissionsExt;
823 let _ = fs::set_permissions(&path, fs::Permissions::from_mode(0o444));
824 }
825
826 Ok(oid)
827 }
828
829 #[must_use]
835 pub fn loose_object_plumbing_ok(&self, oid: &ObjectId) -> bool {
836 let path = self.object_path(oid);
837 let Ok(file) = fs::File::open(&path) else {
838 return false;
839 };
840 let Ok(raw) = read_zlib_loose_payload(file) else {
841 return false;
842 };
843 loose_store_bytes_header_valid(&raw)
844 }
845}
846
847fn loose_store_bytes_header_valid(raw: &[u8]) -> bool {
848 let nul = match raw.iter().position(|&b| b == 0) {
849 Some(i) => i,
850 None => return false,
851 };
852 let header = &raw[..nul];
853 let data = &raw[nul + 1..];
854 let sp = match header.iter().position(|&b| b == b' ') {
855 Some(i) => i,
856 None => return false,
857 };
858 if sp == 0 || sp > 32 {
859 return false;
860 }
861 let size_str = match std::str::from_utf8(&header[sp + 1..]) {
862 Ok(s) => s,
863 Err(_) => return false,
864 };
865 let size: usize = match size_str.parse() {
866 Ok(s) => s,
867 Err(_) => return false,
868 };
869 data.len() == size
870}
871
872fn touch_path_mtime(path: &Path) -> bool {
874 let now = filetime::FileTime::now();
876 filetime::set_file_times(path, now, now).is_ok()
877}
878
879fn freshen_object_in_objects_dir(objects_dir: &Path, oid: &ObjectId) -> bool {
880 let Ok(indexes) = pack::read_local_pack_indexes_cached(objects_dir) else {
881 return false;
882 };
883 for idx in &indexes {
884 if idx.contains(oid) {
885 let touched = touch_path_mtime(&idx.pack_path);
886 if touched {
887 pack::refresh_pack_bytes_signature(&idx.pack_path);
889 }
890 return touched;
891 }
892 }
893 false
894}
895
896fn hash_object_data_with(algo: HashAlgo, kind: ObjectKind, data: &[u8]) -> ObjectId {
899 let header = format!("{} {}\0", kind, data.len());
900 match algo {
901 HashAlgo::Sha1 => {
902 let mut hasher = Sha1::new();
903 hasher.update(header.as_bytes());
904 hasher.update(data);
905 ObjectId::from_bytes(hasher.finalize().as_slice())
906 .unwrap_or_else(|_| unreachable!("SHA-1 is 20 bytes"))
907 }
908 HashAlgo::Sha256 => {
909 let mut hasher = Sha256::new();
910 hasher.update(header.as_bytes());
911 hasher.update(data);
912 ObjectId::from_bytes(hasher.finalize().as_slice())
913 .unwrap_or_else(|_| unreachable!("SHA-256 is 32 bytes"))
914 }
915 }
916}
917
918fn hash_bytes_with(algo: HashAlgo, data: &[u8]) -> ObjectId {
920 match algo {
921 HashAlgo::Sha1 => {
922 let mut hasher = Sha1::new();
923 hasher.update(data);
924 ObjectId::from_bytes(hasher.finalize().as_slice())
925 .unwrap_or_else(|_| unreachable!("SHA-1 is 20 bytes"))
926 }
927 HashAlgo::Sha256 => {
928 let mut hasher = Sha256::new();
929 hasher.update(data);
930 ObjectId::from_bytes(hasher.finalize().as_slice())
931 .unwrap_or_else(|_| unreachable!("SHA-256 is 32 bytes"))
932 }
933 }
934}
935
936fn build_store_bytes(kind: ObjectKind, data: &[u8]) -> Vec<u8> {
938 let header = format!("{} {}\0", kind, data.len());
939 let mut out = Vec::with_capacity(header.len() + data.len());
940 out.extend_from_slice(header.as_bytes());
941 out.extend_from_slice(data);
942 out
943}
944
945pub(crate) fn parse_object_bytes(raw: &[u8]) -> Result<Object> {
947 parse_object_bytes_inner(raw, None)
948}
949
950pub(crate) fn parse_object_bytes_with_oid(raw: &[u8], oid: &ObjectId) -> Result<Object> {
951 parse_object_bytes_inner(raw, Some(oid))
952}
953
954fn parse_object_bytes_inner(raw: &[u8], oid_hint: Option<&ObjectId>) -> Result<Object> {
955 let nul = raw
956 .iter()
957 .position(|&b| b == 0)
958 .ok_or_else(|| Error::CorruptObject("missing NUL in object header".to_owned()))?;
959
960 let header = &raw[..nul];
961 let data = raw[nul + 1..].to_vec();
962
963 let sp = header
964 .iter()
965 .position(|&b| b == b' ')
966 .ok_or_else(|| Error::CorruptObject("missing space in object header".to_owned()))?;
967
968 if sp > 32 {
969 let oid_str = oid_hint
970 .map(|o| o.to_hex())
971 .unwrap_or_else(|| hash_bytes_with(HashAlgo::Sha1, raw).to_hex());
972 return Err(Error::ObjectHeaderTooLong { oid: oid_str });
973 }
974
975 let kind = ObjectKind::from_bytes(&header[..sp])?;
976
977 let size_str = std::str::from_utf8(&header[sp + 1..])
978 .map_err(|_| Error::CorruptObject("non-UTF-8 object size".to_owned()))?;
979 let size: usize = size_str
980 .parse()
981 .map_err(|_| Error::CorruptObject(format!("invalid object size: {size_str}")))?;
982
983 if data.len() != size {
984 return Err(Error::CorruptObject(format!(
985 "object size mismatch: header says {size} but got {}",
986 data.len()
987 )));
988 }
989
990 Ok(Object::new(kind, data))
991}
992
993fn env_alternate_dirs(resolve_base: Option<&Path>) -> Vec<PathBuf> {
1001 match std::env::var("GIT_ALTERNATE_OBJECT_DIRECTORIES") {
1002 Ok(val) if !val.is_empty() => {
1003 let mut dirs = parse_alternate_env(&val);
1004 if let Some(base) = resolve_base {
1005 for dir in &mut dirs {
1006 if dir.is_relative() {
1007 *dir = base.join(&dir);
1008 }
1009 }
1010 }
1011 dirs
1012 }
1013 _ => Vec::new(),
1014 }
1015}
1016
1017fn parse_alternate_env(val: &str) -> Vec<PathBuf> {
1020 let mut result = Vec::new();
1021 let mut chars = val.chars().peekable();
1022 while chars.peek().is_some() {
1023 if chars.peek() == Some(&':') {
1024 chars.next();
1025 continue;
1026 }
1027 if chars.peek() == Some(&'"') {
1028 chars.next(); let saved: Vec<char> = chars.clone().collect();
1032 let mut path = String::new();
1033 let mut properly_closed = false;
1034 loop {
1035 match chars.next() {
1036 None => break,
1037 Some('"') => {
1038 properly_closed = true;
1039 break;
1040 }
1041 Some('\\') => match chars.peek() {
1042 Some(c) if c.is_ascii_digit() => {
1043 let mut oct = String::new();
1044 for _ in 0..3 {
1045 if let Some(&c) = chars.peek() {
1046 if c.is_ascii_digit() {
1047 oct.push(c);
1048 chars.next();
1049 } else {
1050 break;
1051 }
1052 } else {
1053 break;
1054 }
1055 }
1056 if let Ok(byte) = u8::from_str_radix(&oct, 8) {
1057 path.push(byte as char);
1058 }
1059 }
1060 Some(_) => {
1061 if let Some(c) = chars.next() {
1062 match c {
1063 'n' => path.push('\n'),
1064 't' => path.push('\t'),
1065 'r' => path.push('\r'),
1066 _ => path.push(c),
1067 }
1068 }
1069 }
1070 None => {}
1071 },
1072 Some(c) => path.push(c),
1073 }
1074 }
1075 if !properly_closed {
1076 let raw: String = std::iter::once('"').chain(saved).collect();
1079 let raw_path = raw.split(':').next().unwrap_or(&raw);
1081 if !raw_path.is_empty() {
1082 result.push(PathBuf::from(raw_path));
1083 }
1084 let remainder = &raw[raw_path.len()..];
1088 if let Some(rest) = remainder.strip_prefix(':') {
1089 result.extend(parse_alternate_env(rest));
1091 }
1092 return result;
1093 } else if !path.is_empty() {
1094 result.push(PathBuf::from(path));
1095 }
1096 } else {
1097 let mut path = String::new();
1098 while let Some(&c) = chars.peek() {
1099 if c == ':' {
1100 break;
1101 }
1102 path.push(c);
1103 chars.next();
1104 }
1105 if !path.is_empty() {
1106 result.push(PathBuf::from(path));
1107 }
1108 }
1109 }
1110 result
1111}
1112
1113#[cfg(test)]
1114mod tests {
1115 #![allow(clippy::expect_used, clippy::unwrap_used)]
1116
1117 use super::*;
1118 use tempfile::TempDir;
1119
1120 #[test]
1121 fn round_trip_blob() {
1122 let dir = TempDir::new().unwrap();
1123 let odb = Odb::new(dir.path());
1124 let data = b"hello world";
1125 let oid = odb.write(ObjectKind::Blob, data).unwrap();
1126 let obj = odb.read(&oid).unwrap();
1127 assert_eq!(obj.kind, ObjectKind::Blob);
1128 assert_eq!(obj.data, data);
1129 }
1130
1131 #[test]
1132 fn known_blob_hash() {
1133 let oid = Odb::hash_object_data(ObjectKind::Blob, b"hello");
1136 assert_eq!(oid.to_hex(), "b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0");
1137 }
1138}