| // This program is free software; you can redistribute it and/or modify | |
| // it under the terms of the GNU General Public License as published by | |
| // the Free Software Foundation: version 2 of the License, dated June 1991. | |
| // | |
| // This program is distributed in the hope that it will be useful, | |
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| // GNU General Public License for more details. | |
| // | |
| // You should have received a copy of the GNU General Public License along | |
| // with this program; if not, see <https://www.gnu.org/licenses/>. | |
| use crate::hash::{HashAlgorithm, ObjectID, GIT_MAX_RAWSZ}; | |
| use std::collections::BTreeMap; | |
| use std::convert::TryInto; | |
| use std::io::{self, Write}; | |
| /// The type of object stored in the map. | |
| /// | |
| /// If this value is `Reserved`, then it is never written to disk and is used primarily to store | |
| /// certain hard-coded objects, like the empty tree, empty blob, or null object ID. | |
| /// | |
| /// If this value is `LooseObject`, then this represents a loose object. `Shallow` represents a | |
| /// shallow commit, its parent, or its tree. `Submodule` represents a submodule commit. | |
| #[repr(C)] | |
| #[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)] | |
| pub enum MapType { | |
| Reserved = 0, | |
| LooseObject = 1, | |
| Shallow = 2, | |
| Submodule = 3, | |
| } | |
| impl MapType { | |
| pub fn from_u32(n: u32) -> Option<MapType> { | |
| match n { | |
| 0 => Some(Self::Reserved), | |
| 1 => Some(Self::LooseObject), | |
| 2 => Some(Self::Shallow), | |
| 3 => Some(Self::Submodule), | |
| _ => None, | |
| } | |
| } | |
| } | |
| /// The value of an object stored in a `ObjectMemoryMap`. | |
| /// | |
| /// This keeps the object ID to which the key is mapped and its kind together. | |
| struct MappedObject { | |
| oid: ObjectID, | |
| kind: MapType, | |
| } | |
| /// Memory storage for a loose object. | |
| struct ObjectMemoryMap { | |
| to_compat: BTreeMap<ObjectID, MappedObject>, | |
| to_storage: BTreeMap<ObjectID, MappedObject>, | |
| compat: HashAlgorithm, | |
| storage: HashAlgorithm, | |
| } | |
| impl ObjectMemoryMap { | |
| /// Create a new `ObjectMemoryMap`. | |
| /// | |
| /// The storage and compatibility `HashAlgorithm` instances are used to store the object IDs in | |
| /// the correct map. | |
| fn new(storage: HashAlgorithm, compat: HashAlgorithm) -> Self { | |
| Self { | |
| to_compat: BTreeMap::new(), | |
| to_storage: BTreeMap::new(), | |
| compat, | |
| storage, | |
| } | |
| } | |
| fn len(&self) -> usize { | |
| self.to_compat.len() | |
| } | |
| /// Write this map to an interface implementing `std::io::Write`. | |
| fn write<W: Write>(&self, wrtr: W) -> io::Result<()> { | |
| const VERSION_NUMBER: u32 = 1; | |
| const NUM_OBJECT_FORMATS: u32 = 2; | |
| const PADDING: [u8; 4] = [0u8; 4]; | |
| let mut wrtr = wrtr; | |
| let header_size: u32 = (4 * 5) + (4 + 4 + 8) * NUM_OBJECT_FORMATS + 8; | |
| wrtr.write_all(b"LMAP")?; | |
| wrtr.write_all(&VERSION_NUMBER.to_be_bytes())?; | |
| wrtr.write_all(&header_size.to_be_bytes())?; | |
| wrtr.write_all(&(self.to_compat.len() as u32).to_be_bytes())?; | |
| wrtr.write_all(&NUM_OBJECT_FORMATS.to_be_bytes())?; | |
| let storage_short_len = self.find_short_name_len(&self.to_compat, self.storage); | |
| let compat_short_len = self.find_short_name_len(&self.to_storage, self.compat); | |
| let storage_npadding = Self::required_nul_padding(self.to_compat.len(), storage_short_len); | |
| let compat_npadding = Self::required_nul_padding(self.to_compat.len(), compat_short_len); | |
| let mut offset: u64 = header_size as u64; | |
| for (algo, len, npadding) in &[ | |
| (self.storage, storage_short_len, storage_npadding), | |
| (self.compat, compat_short_len, compat_npadding), | |
| ] { | |
| wrtr.write_all(&algo.format_id().to_be_bytes())?; | |
| wrtr.write_all(&(*len as u32).to_be_bytes())?; | |
| offset += *npadding; | |
| wrtr.write_all(&offset.to_be_bytes())?; | |
| offset += self.to_compat.len() as u64 * (*len as u64 + algo.raw_len() as u64 + 4); | |
| } | |
| wrtr.write_all(&offset.to_be_bytes())?; | |
| let order_map: BTreeMap<&ObjectID, usize> = self | |
| .to_compat | |
| .keys() | |
| .enumerate() | |
| .map(|(i, oid)| (oid, i)) | |
| .collect(); | |
| wrtr.write_all(&PADDING[0..storage_npadding as usize])?; | |
| for oid in self.to_compat.keys() { | |
| wrtr.write_all(&oid.as_slice().unwrap()[0..storage_short_len])?; | |
| } | |
| for oid in self.to_compat.keys() { | |
| wrtr.write_all(oid.as_slice().unwrap())?; | |
| } | |
| for meta in self.to_compat.values() { | |
| wrtr.write_all(&(meta.kind as u32).to_be_bytes())?; | |
| } | |
| wrtr.write_all(&PADDING[0..compat_npadding as usize])?; | |
| for oid in self.to_storage.keys() { | |
| wrtr.write_all(&oid.as_slice().unwrap()[0..compat_short_len])?; | |
| } | |
| for meta in self.to_compat.values() { | |
| wrtr.write_all(meta.oid.as_slice().unwrap())?; | |
| } | |
| for meta in self.to_storage.values() { | |
| wrtr.write_all(&(order_map[&meta.oid] as u32).to_be_bytes())?; | |
| } | |
| Ok(()) | |
| } | |
| fn required_nul_padding(nitems: usize, short_len: usize) -> u64 { | |
| let shortened_table_len = nitems as u64 * short_len as u64; | |
| let misalignment = shortened_table_len & 3; | |
| // If the value is 0, return 0; otherwise, return the difference from 4. | |
| (4 - misalignment) & 3 | |
| } | |
| fn last_matching_offset(a: &ObjectID, b: &ObjectID, algop: HashAlgorithm) -> usize { | |
| for i in 0..=algop.raw_len() { | |
| if a.hash[i] != b.hash[i] { | |
| return i; | |
| } | |
| } | |
| algop.raw_len() | |
| } | |
| fn find_short_name_len( | |
| &self, | |
| map: &BTreeMap<ObjectID, MappedObject>, | |
| algop: HashAlgorithm, | |
| ) -> usize { | |
| if map.len() <= 1 { | |
| return 1; | |
| } | |
| let mut len = 1; | |
| let mut iter = map.keys(); | |
| let mut cur = match iter.next() { | |
| Some(cur) => cur, | |
| None => return len, | |
| }; | |
| for item in iter { | |
| let offset = Self::last_matching_offset(cur, item, algop); | |
| if offset >= len { | |
| len = offset + 1; | |
| } | |
| cur = item; | |
| } | |
| if len > algop.raw_len() { | |
| algop.raw_len() | |
| } else { | |
| len | |
| } | |
| } | |
| } | |
| struct ObjectFormatData { | |
| data_off: usize, | |
| shortened_len: usize, | |
| full_off: usize, | |
| mapping_off: Option<usize>, | |
| } | |
| pub struct MmapedObjectMapIter<'a> { | |
| offset: usize, | |
| algos: Vec<HashAlgorithm>, | |
| source: &'a MmapedObjectMap<'a>, | |
| } | |
| impl<'a> Iterator for MmapedObjectMapIter<'a> { | |
| type Item = Vec<ObjectID>; | |
| fn next(&mut self) -> Option<Self::Item> { | |
| if self.offset >= self.source.nitems { | |
| return None; | |
| } | |
| let offset = self.offset; | |
| self.offset += 1; | |
| let v: Vec<ObjectID> = self | |
| .algos | |
| .iter() | |
| .cloned() | |
| .filter_map(|algo| self.source.oid_from_offset(offset, algo)) | |
| .collect(); | |
| if v.len() != self.algos.len() { | |
| return None; | |
| } | |
| Some(v) | |
| } | |
| } | |
| #[allow(dead_code)] | |
| pub struct MmapedObjectMap<'a> { | |
| memory: &'a [u8], | |
| nitems: usize, | |
| meta_off: usize, | |
| obj_formats: BTreeMap<HashAlgorithm, ObjectFormatData>, | |
| main_algo: HashAlgorithm, | |
| } | |
| #[derive(Debug)] | |
| #[allow(dead_code)] | |
| enum MmapedParseError { | |
| HeaderTooSmall, | |
| InvalidSignature, | |
| InvalidVersion, | |
| UnknownAlgorithm, | |
| OffsetTooLarge, | |
| TooFewObjectFormats, | |
| UnalignedData, | |
| InvalidTrailerOffset, | |
| } | |
| #[allow(dead_code)] | |
| impl<'a> MmapedObjectMap<'a> { | |
| fn new( | |
| slice: &'a [u8], | |
| hash_algo: HashAlgorithm, | |
| ) -> Result<MmapedObjectMap<'a>, MmapedParseError> { | |
| let object_format_header_size = 4 + 4 + 8; | |
| let trailer_offset_size = 8; | |
| let header_size: usize = | |
| 4 + 4 + 4 + 4 + 4 + object_format_header_size * 2 + trailer_offset_size; | |
| if slice.len() < header_size { | |
| return Err(MmapedParseError::HeaderTooSmall); | |
| } | |
| if slice[0..4] != *b"LMAP" { | |
| return Err(MmapedParseError::InvalidSignature); | |
| } | |
| if Self::u32_at_offset(slice, 4) != 1 { | |
| return Err(MmapedParseError::InvalidVersion); | |
| } | |
| let _ = Self::u32_at_offset(slice, 8) as usize; | |
| let nitems = Self::u32_at_offset(slice, 12) as usize; | |
| let nobj_formats = Self::u32_at_offset(slice, 16) as usize; | |
| if nobj_formats < 2 { | |
| return Err(MmapedParseError::TooFewObjectFormats); | |
| } | |
| let mut offset = 20; | |
| let mut meta_off = None; | |
| let mut data = BTreeMap::new(); | |
| for i in 0..nobj_formats { | |
| if offset + object_format_header_size + trailer_offset_size > slice.len() { | |
| return Err(MmapedParseError::HeaderTooSmall); | |
| } | |
| let format_id = Self::u32_at_offset(slice, offset); | |
| let shortened_len = Self::u32_at_offset(slice, offset + 4) as usize; | |
| let data_off = Self::u64_at_offset(slice, offset + 8); | |
| let algo = HashAlgorithm::from_format_id(format_id) | |
| .ok_or(MmapedParseError::UnknownAlgorithm)?; | |
| let data_off: usize = data_off | |
| .try_into() | |
| .map_err(|_| MmapedParseError::OffsetTooLarge)?; | |
| // Every object format must have these entries. | |
| let shortened_table_len = shortened_len | |
| .checked_mul(nitems) | |
| .ok_or(MmapedParseError::OffsetTooLarge)?; | |
| let full_off = data_off | |
| .checked_add(shortened_table_len) | |
| .ok_or(MmapedParseError::OffsetTooLarge)?; | |
| Self::verify_aligned(full_off)?; | |
| Self::verify_valid(slice, full_off as u64)?; | |
| let full_length = algo | |
| .raw_len() | |
| .checked_mul(nitems) | |
| .ok_or(MmapedParseError::OffsetTooLarge)?; | |
| let off = full_length | |
| .checked_add(full_off) | |
| .ok_or(MmapedParseError::OffsetTooLarge)?; | |
| Self::verify_aligned(off)?; | |
| Self::verify_valid(slice, off as u64)?; | |
| // This is for the metadata for the first object format and for the order mapping for | |
| // other object formats. | |
| let meta_size = nitems | |
| .checked_mul(4) | |
| .ok_or(MmapedParseError::OffsetTooLarge)?; | |
| let meta_end = off | |
| .checked_add(meta_size) | |
| .ok_or(MmapedParseError::OffsetTooLarge)?; | |
| Self::verify_valid(slice, meta_end as u64)?; | |
| let mut mapping_off = None; | |
| if i == 0 { | |
| meta_off = Some(off); | |
| } else { | |
| mapping_off = Some(off); | |
| } | |
| data.insert( | |
| algo, | |
| ObjectFormatData { | |
| data_off, | |
| shortened_len, | |
| full_off, | |
| mapping_off, | |
| }, | |
| ); | |
| offset += object_format_header_size; | |
| } | |
| let trailer = Self::u64_at_offset(slice, offset); | |
| Self::verify_aligned(trailer as usize)?; | |
| Self::verify_valid(slice, trailer)?; | |
| let end = trailer | |
| .checked_add(hash_algo.raw_len() as u64) | |
| .ok_or(MmapedParseError::OffsetTooLarge)?; | |
| if end != slice.len() as u64 { | |
| return Err(MmapedParseError::InvalidTrailerOffset); | |
| } | |
| match meta_off { | |
| Some(meta_off) => Ok(MmapedObjectMap { | |
| memory: slice, | |
| nitems, | |
| meta_off, | |
| obj_formats: data, | |
| main_algo: hash_algo, | |
| }), | |
| None => Err(MmapedParseError::TooFewObjectFormats), | |
| } | |
| } | |
| fn iter(&self) -> MmapedObjectMapIter<'_> { | |
| let mut algos = Vec::with_capacity(self.obj_formats.len()); | |
| algos.push(self.main_algo); | |
| for algo in self.obj_formats.keys().cloned() { | |
| if algo != self.main_algo { | |
| algos.push(algo); | |
| } | |
| } | |
| MmapedObjectMapIter { | |
| offset: 0, | |
| algos, | |
| source: self, | |
| } | |
| } | |
| /// Treats `sl` as if it were a set of slices of `wanted.len()` bytes, and searches for | |
| /// `wanted` within it. | |
| /// | |
| /// If found, returns the offset of the subslice in `sl`. | |
| /// | |
| /// ``` | |
| /// let sl = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; | |
| /// | |
| /// assert_eq!(MmapedObjectMap::binary_search_slice(sl, &[2, 3]), Some(1)); | |
| /// assert_eq!(MmapedObjectMap::binary_search_slice(sl, &[6, 7]), Some(4)); | |
| /// assert_eq!(MmapedObjectMap::binary_search_slice(sl, &[1, 2]), None); | |
| /// assert_eq!(MmapedObjectMap::binary_search_slice(sl, &[10, 20]), None); | |
| /// ``` | |
| fn binary_search_slice(sl: &[u8], wanted: &[u8]) -> Option<usize> { | |
| let len = wanted.len(); | |
| let res = sl.binary_search_by(|item| { | |
| // We would like element_offset, but that is currently nightly only. Instead, do a | |
| // pointer subtraction to find the index. | |
| let index = unsafe { (item as *const u8).offset_from(sl.as_ptr()) } as usize; | |
| // Now we have the index of this object. Round it down to the nearest full-sized | |
| // chunk to find the actual offset where this starts. | |
| let index = index - (index % len); | |
| // Compute the comparison of that value instead, which will provide the expected | |
| // result. | |
| sl[index..index + wanted.len()].cmp(wanted) | |
| }); | |
| res.ok().map(|offset| offset / len) | |
| } | |
| /// Look up `oid` in the map in order to convert it to `algo`. | |
| /// | |
| /// If this object is in the map, return the offset in the table for the main algorithm. | |
| fn look_up_object(&self, oid: &ObjectID) -> Option<usize> { | |
| let oid_algo = HashAlgorithm::from_u32(oid.algo)?; | |
| let params = self.obj_formats.get(&oid_algo)?; | |
| let short_table = | |
| &self.memory[params.data_off..params.data_off + (params.shortened_len * self.nitems)]; | |
| let index = Self::binary_search_slice( | |
| short_table, | |
| &oid.as_slice().unwrap()[0..params.shortened_len], | |
| )?; | |
| match params.mapping_off { | |
| Some(from_off) => { | |
| // oid is in a compatibility algorithm. Find the mapping index. | |
| let mapped = Self::u32_at_offset(self.memory, from_off + index * 4) as usize; | |
| if mapped >= self.nitems { | |
| return None; | |
| } | |
| let oid_offset = params.full_off + mapped * oid_algo.raw_len(); | |
| if self.memory[oid_offset..oid_offset + oid_algo.raw_len()] | |
| != *oid.as_slice().unwrap() | |
| { | |
| return None; | |
| } | |
| Some(mapped) | |
| } | |
| None => { | |
| // oid is in the main algorithm. Find the object ID in the main map to confirm | |
| // it's correct. | |
| let oid_offset = params.full_off + index * oid_algo.raw_len(); | |
| if self.memory[oid_offset..oid_offset + oid_algo.raw_len()] | |
| != *oid.as_slice().unwrap() | |
| { | |
| return None; | |
| } | |
| Some(index) | |
| } | |
| } | |
| } | |
| #[allow(dead_code)] | |
| fn map_object(&self, oid: &ObjectID, algo: HashAlgorithm) -> Option<MappedObject> { | |
| let main = self.look_up_object(oid)?; | |
| let meta = MapType::from_u32(Self::u32_at_offset(self.memory, self.meta_off + (main * 4)))?; | |
| Some(MappedObject { | |
| oid: self.oid_from_offset(main, algo)?, | |
| kind: meta, | |
| }) | |
| } | |
| fn map_oid(&self, oid: &ObjectID, algo: HashAlgorithm) -> Option<ObjectID> { | |
| if algo as u32 == oid.algo { | |
| return Some(oid.clone()); | |
| } | |
| let main = self.look_up_object(oid)?; | |
| self.oid_from_offset(main, algo) | |
| } | |
| fn oid_from_offset(&self, offset: usize, algo: HashAlgorithm) -> Option<ObjectID> { | |
| let aparams = self.obj_formats.get(&algo)?; | |
| let mut hash = [0u8; GIT_MAX_RAWSZ]; | |
| let len = algo.raw_len(); | |
| let oid_off = aparams.full_off + (offset * len); | |
| hash[0..len].copy_from_slice(&self.memory[oid_off..oid_off + len]); | |
| Some(ObjectID { | |
| hash, | |
| algo: algo as u32, | |
| }) | |
| } | |
| fn u32_at_offset(slice: &[u8], offset: usize) -> u32 { | |
| u32::from_be_bytes(slice[offset..offset + 4].try_into().unwrap()) | |
| } | |
| fn u64_at_offset(slice: &[u8], offset: usize) -> u64 { | |
| u64::from_be_bytes(slice[offset..offset + 8].try_into().unwrap()) | |
| } | |
| fn verify_aligned(offset: usize) -> Result<(), MmapedParseError> { | |
| if (offset & 3) != 0 { | |
| return Err(MmapedParseError::UnalignedData); | |
| } | |
| Ok(()) | |
| } | |
| fn verify_valid(slice: &[u8], offset: u64) -> Result<(), MmapedParseError> { | |
| if offset >= slice.len() as u64 { | |
| return Err(MmapedParseError::OffsetTooLarge); | |
| } | |
| Ok(()) | |
| } | |
| } | |
| /// A map for loose and other non-packed object IDs that maps between a storage and compatibility | |
| /// mapping. | |
| /// | |
| /// In addition to the in-memory option, there is an optional batched storage, which can be used to | |
| /// write objects to disk in an efficient way. | |
| pub struct ObjectMap { | |
| mem: ObjectMemoryMap, | |
| batch: Option<ObjectMemoryMap>, | |
| } | |
| impl ObjectMap { | |
| /// Create a new `ObjectMap` with the given hash algorithms. | |
| /// | |
| /// This initializes the memory map to automatically map the empty tree, empty blob, and null | |
| /// object ID. | |
| pub fn new(storage: HashAlgorithm, compat: HashAlgorithm) -> Self { | |
| let mut map = ObjectMemoryMap::new(storage, compat); | |
| for (main, compat) in &[ | |
| (storage.empty_tree(), compat.empty_tree()), | |
| (storage.empty_blob(), compat.empty_blob()), | |
| (storage.null_oid(), compat.null_oid()), | |
| ] { | |
| map.to_storage.insert( | |
| (*compat).clone(), | |
| MappedObject { | |
| oid: (*main).clone(), | |
| kind: MapType::Reserved, | |
| }, | |
| ); | |
| map.to_compat.insert( | |
| (*main).clone(), | |
| MappedObject { | |
| oid: (*compat).clone(), | |
| kind: MapType::Reserved, | |
| }, | |
| ); | |
| } | |
| Self { | |
| mem: map, | |
| batch: None, | |
| } | |
| } | |
| pub fn hash_algo(&self) -> HashAlgorithm { | |
| self.mem.storage | |
| } | |
| /// Start a batch for efficient writing. | |
| /// | |
| /// If there is already a batch started, this does nothing and the existing batch is retained. | |
| pub fn start_batch(&mut self) { | |
| if self.batch.is_none() { | |
| self.batch = Some(ObjectMemoryMap::new(self.mem.storage, self.mem.compat)); | |
| } | |
| } | |
| pub fn batch_len(&self) -> Option<usize> { | |
| self.batch.as_ref().map(|b| b.len()) | |
| } | |
| /// If a batch exists, write it to the writer. | |
| pub fn finish_batch<W: Write>(&mut self, w: W) -> io::Result<()> { | |
| if let Some(txn) = self.batch.take() { | |
| txn.write(w)?; | |
| } | |
| Ok(()) | |
| } | |
| /// If a batch exists, write it to the writer. | |
| pub fn abort_batch(&mut self) { | |
| self.batch = None; | |
| } | |
| /// Return whether there is a batch already started. | |
| /// | |
| /// If you just want a batch to exist and don't care whether one has already been started, you | |
| /// may simply call `start_batch` unconditionally. | |
| pub fn has_batch(&self) -> bool { | |
| self.batch.is_some() | |
| } | |
| /// Insert an object into the map. | |
| /// | |
| /// If `write` is true and there is a batch started, write the object into the batch as well as | |
| /// into the memory map. | |
| pub fn insert(&mut self, oid1: &ObjectID, oid2: &ObjectID, kind: MapType, write: bool) { | |
| let (compat_oid, storage_oid) = | |
| if HashAlgorithm::from_u32(oid1.algo) == Some(self.mem.compat) { | |
| (oid1, oid2) | |
| } else { | |
| (oid2, oid1) | |
| }; | |
| Self::insert_into(&mut self.mem, storage_oid, compat_oid, kind); | |
| if write { | |
| if let Some(ref mut batch) = self.batch { | |
| Self::insert_into(batch, storage_oid, compat_oid, kind); | |
| } | |
| } | |
| } | |
| fn insert_into( | |
| map: &mut ObjectMemoryMap, | |
| storage: &ObjectID, | |
| compat: &ObjectID, | |
| kind: MapType, | |
| ) { | |
| map.to_compat.insert( | |
| storage.clone(), | |
| MappedObject { | |
| oid: compat.clone(), | |
| kind, | |
| }, | |
| ); | |
| map.to_storage.insert( | |
| compat.clone(), | |
| MappedObject { | |
| oid: storage.clone(), | |
| kind, | |
| }, | |
| ); | |
| } | |
| #[allow(dead_code)] | |
| fn map_object(&self, oid: &ObjectID, algo: HashAlgorithm) -> Option<&MappedObject> { | |
| let map = if algo == self.mem.storage { | |
| &self.mem.to_storage | |
| } else { | |
| &self.mem.to_compat | |
| }; | |
| map.get(oid) | |
| } | |
| #[allow(dead_code)] | |
| fn map_oid<'a, 'b: 'a>( | |
| &'b self, | |
| oid: &'a ObjectID, | |
| algo: HashAlgorithm, | |
| ) -> Option<&'a ObjectID> { | |
| if algo as u32 == oid.algo { | |
| return Some(oid); | |
| } | |
| let entry = self.map_object(oid, algo); | |
| entry.map(|obj| &obj.oid) | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::{MapType, MmapedObjectMap, ObjectMap, ObjectMemoryMap}; | |
| use crate::hash::{CryptoDigest, CryptoHasher, HashAlgorithm, ObjectID}; | |
| use std::convert::TryInto; | |
| use std::io::{self, Cursor, Write}; | |
| struct TrailingWriter { | |
| curs: Cursor<Vec<u8>>, | |
| hasher: CryptoHasher, | |
| } | |
| impl TrailingWriter { | |
| fn new() -> Self { | |
| Self { | |
| curs: Cursor::new(Vec::new()), | |
| hasher: CryptoHasher::new(HashAlgorithm::SHA256), | |
| } | |
| } | |
| fn finalize(mut self) -> Vec<u8> { | |
| let _ = self.hasher.flush(); | |
| let mut v = self.curs.into_inner(); | |
| v.extend(self.hasher.into_vec()); | |
| v | |
| } | |
| } | |
| impl Write for TrailingWriter { | |
| fn write(&mut self, data: &[u8]) -> io::Result<usize> { | |
| self.hasher.write_all(data)?; | |
| self.curs.write_all(data)?; | |
| Ok(data.len()) | |
| } | |
| fn flush(&mut self) -> io::Result<()> { | |
| self.hasher.flush()?; | |
| self.curs.flush()?; | |
| Ok(()) | |
| } | |
| } | |
| fn sha1_oid(b: &[u8]) -> ObjectID { | |
| assert_eq!(b.len(), 20); | |
| let mut data = [0u8; 32]; | |
| data[0..20].copy_from_slice(b); | |
| ObjectID { | |
| hash: data, | |
| algo: HashAlgorithm::SHA1 as u32, | |
| } | |
| } | |
| fn sha256_oid(b: &[u8]) -> ObjectID { | |
| assert_eq!(b.len(), 32); | |
| ObjectID { | |
| hash: b.try_into().unwrap(), | |
| algo: HashAlgorithm::SHA256 as u32, | |
| } | |
| } | |
| #[allow(clippy::type_complexity)] | |
| fn test_entries() -> &'static [(&'static str, &'static [u8], &'static [u8], MapType, bool)] { | |
| // These are all example blobs containing the content in the first argument. | |
| &[ | |
| ("abc", b"\xf2\xba\x8f\x84\xab\x5c\x1b\xce\x84\xa7\xb4\x41\xcb\x19\x59\xcf\xc7\x09\x3b\x7f", b"\xc1\xcf\x6e\x46\x50\x77\x93\x0e\x88\xdc\x51\x36\x64\x1d\x40\x2f\x72\xa2\x29\xdd\xd9\x96\xf6\x27\xd6\x0e\x96\x39\xea\xba\x35\xa6", MapType::LooseObject, false), | |
| ("def", b"\x0c\x00\x38\x32\xe7\xbf\xa9\xca\x8b\x5c\x20\x35\xc9\xbd\x68\x4a\x5f\x26\x23\xbc", b"\x8a\x90\x17\x26\x48\x4d\xb0\xf2\x27\x9f\x30\x8d\x58\x96\xd9\x6b\xf6\x3a\xd6\xde\x95\x7c\xa3\x8a\xdc\x33\x61\x68\x03\x6e\xf6\x63", MapType::Shallow, true), | |
| ("ghi", b"\x45\xa8\x2e\x29\x5c\x52\x47\x31\x14\xc5\x7c\x18\xf4\xf5\x23\x68\xdf\x2a\x3c\xfd", b"\x6e\x47\x4c\x74\xf5\xd7\x78\x14\xc7\xf7\xf0\x7c\x37\x80\x07\x90\x53\x42\xaf\x42\x81\xe6\x86\x8d\x33\x46\x45\x4b\xb8\x63\xab\xc3", MapType::Submodule, false), | |
| ("jkl", b"\x45\x32\x8c\x36\xff\x2e\x9b\x9b\x4e\x59\x2c\x84\x7d\x3f\x9a\x7f\xd9\xb3\xe7\x16", b"\xc3\xee\xf7\x54\xa2\x1e\xc6\x9d\x43\x75\xbe\x6f\x18\x47\x89\xa8\x11\x6f\xd9\x66\xfc\x67\xdc\x31\xd2\x11\x15\x42\xc8\xd5\xa0\xaf", MapType::LooseObject, true), | |
| ] | |
| } | |
| fn test_map(write_all: bool) -> Box<ObjectMap> { | |
| let mut map = Box::new(ObjectMap::new(HashAlgorithm::SHA256, HashAlgorithm::SHA1)); | |
| map.start_batch(); | |
| for (_blob_content, sha1, sha256, kind, swap) in test_entries() { | |
| let s256 = sha256_oid(sha256); | |
| let s1 = sha1_oid(sha1); | |
| let write = write_all || (*kind as u32 & 2) == 0; | |
| if *swap { | |
| // Insert the item into the batch arbitrarily based on the type. This tests that | |
| // we can specify either order and we'll do the right thing. | |
| map.insert(&s256, &s1, *kind, write); | |
| } else { | |
| map.insert(&s1, &s256, *kind, write); | |
| } | |
| } | |
| map | |
| } | |
| #[test] | |
| fn can_read_and_write_format() { | |
| for full in &[true, false] { | |
| let mut map = test_map(*full); | |
| let mut wrtr = TrailingWriter::new(); | |
| map.finish_batch(&mut wrtr).unwrap(); | |
| assert!(!map.has_batch()); | |
| let data = wrtr.finalize(); | |
| MmapedObjectMap::new(&data, HashAlgorithm::SHA256).unwrap(); | |
| } | |
| } | |
| #[test] | |
| fn looks_up_from_mmaped() { | |
| let mut map = test_map(true); | |
| let mut wrtr = TrailingWriter::new(); | |
| map.finish_batch(&mut wrtr).unwrap(); | |
| assert!(!map.has_batch()); | |
| let data = wrtr.finalize(); | |
| let entries = test_entries(); | |
| let map = MmapedObjectMap::new(&data, HashAlgorithm::SHA256).unwrap(); | |
| for (_, sha1, sha256, kind, _) in entries { | |
| let s256 = sha256_oid(sha256); | |
| let s1 = sha1_oid(sha1); | |
| let res = map.map_object(&s256, HashAlgorithm::SHA1).unwrap(); | |
| assert_eq!(res.oid, s1); | |
| assert_eq!(res.kind, *kind); | |
| let res = map.map_oid(&s256, HashAlgorithm::SHA1).unwrap(); | |
| assert_eq!(res, s1); | |
| let res = map.map_object(&s256, HashAlgorithm::SHA256).unwrap(); | |
| assert_eq!(res.oid, s256); | |
| assert_eq!(res.kind, *kind); | |
| let res = map.map_oid(&s256, HashAlgorithm::SHA256).unwrap(); | |
| assert_eq!(res, s256); | |
| let res = map.map_object(&s1, HashAlgorithm::SHA256).unwrap(); | |
| assert_eq!(res.oid, s256); | |
| assert_eq!(res.kind, *kind); | |
| let res = map.map_oid(&s1, HashAlgorithm::SHA256).unwrap(); | |
| assert_eq!(res, s256); | |
| let res = map.map_object(&s1, HashAlgorithm::SHA1).unwrap(); | |
| assert_eq!(res.oid, s1); | |
| assert_eq!(res.kind, *kind); | |
| let res = map.map_oid(&s1, HashAlgorithm::SHA1).unwrap(); | |
| assert_eq!(res, s1); | |
| } | |
| for octet in &[0x00u8, 0x6d, 0x6e, 0x8a, 0xff] { | |
| let missing_oid = ObjectID { | |
| hash: [*octet; 32], | |
| algo: HashAlgorithm::SHA256 as u32, | |
| }; | |
| assert!(map.map_object(&missing_oid, HashAlgorithm::SHA1).is_none()); | |
| assert!(map.map_oid(&missing_oid, HashAlgorithm::SHA1).is_none()); | |
| assert_eq!( | |
| map.map_oid(&missing_oid, HashAlgorithm::SHA256).unwrap(), | |
| missing_oid | |
| ); | |
| } | |
| } | |
| #[test] | |
| fn binary_searches_slices_correctly() { | |
| let sl = &[ | |
| 0, 1, 2, 15, 14, 13, 18, 10, 2, 20, 20, 20, 21, 21, 0, 21, 21, 1, 21, 21, 21, 21, 21, | |
| 22, 22, 23, 24, | |
| ]; | |
| let expected: &[(&[u8], Option<usize>)] = &[ | |
| (&[0, 1, 2], Some(0)), | |
| (&[15, 14, 13], Some(1)), | |
| (&[18, 10, 2], Some(2)), | |
| (&[20, 20, 20], Some(3)), | |
| (&[21, 21, 0], Some(4)), | |
| (&[21, 21, 1], Some(5)), | |
| (&[21, 21, 21], Some(6)), | |
| (&[21, 21, 22], Some(7)), | |
| (&[22, 23, 24], Some(8)), | |
| (&[2, 15, 14], None), | |
| (&[0, 21, 21], None), | |
| (&[21, 21, 23], None), | |
| (&[22, 22, 23], None), | |
| (&[0xff, 0xff, 0xff], None), | |
| (&[0, 0, 0], None), | |
| ]; | |
| for (wanted, value) in expected { | |
| assert_eq!(MmapedObjectMap::binary_search_slice(sl, wanted), *value); | |
| } | |
| } | |
| #[test] | |
| fn looks_up_oid_correctly() { | |
| let map = test_map(false); | |
| let entries = test_entries(); | |
| let s256 = sha256_oid(entries[0].2); | |
| let s1 = sha1_oid(entries[0].1); | |
| let missing_oid = ObjectID { | |
| hash: [0xffu8; 32], | |
| algo: HashAlgorithm::SHA256 as u32, | |
| }; | |
| let res = map.map_object(&s256, HashAlgorithm::SHA1).unwrap(); | |
| assert_eq!(res.oid, s1); | |
| assert_eq!(res.kind, MapType::LooseObject); | |
| let res = map.map_oid(&s256, HashAlgorithm::SHA1).unwrap(); | |
| assert_eq!(*res, s1); | |
| let res = map.map_object(&s1, HashAlgorithm::SHA256).unwrap(); | |
| assert_eq!(res.oid, s256); | |
| assert_eq!(res.kind, MapType::LooseObject); | |
| let res = map.map_oid(&s1, HashAlgorithm::SHA256).unwrap(); | |
| assert_eq!(*res, s256); | |
| assert!(map.map_object(&missing_oid, HashAlgorithm::SHA1).is_none()); | |
| assert!(map.map_oid(&missing_oid, HashAlgorithm::SHA1).is_none()); | |
| assert_eq!( | |
| *map.map_oid(&missing_oid, HashAlgorithm::SHA256).unwrap(), | |
| missing_oid | |
| ); | |
| } | |
| #[test] | |
| fn looks_up_known_oids_correctly() { | |
| let map = test_map(false); | |
| let funcs: &[&dyn Fn(HashAlgorithm) -> &'static ObjectID] = &[ | |
| &|h: HashAlgorithm| h.empty_tree(), | |
| &|h: HashAlgorithm| h.empty_blob(), | |
| &|h: HashAlgorithm| h.null_oid(), | |
| ]; | |
| for f in funcs { | |
| let s256 = f(HashAlgorithm::SHA256); | |
| let s1 = f(HashAlgorithm::SHA1); | |
| let res = map.map_object(s256, HashAlgorithm::SHA1).unwrap(); | |
| assert_eq!(res.oid, *s1); | |
| assert_eq!(res.kind, MapType::Reserved); | |
| let res = map.map_oid(s256, HashAlgorithm::SHA1).unwrap(); | |
| assert_eq!(*res, *s1); | |
| let res = map.map_object(s1, HashAlgorithm::SHA256).unwrap(); | |
| assert_eq!(res.oid, *s256); | |
| assert_eq!(res.kind, MapType::Reserved); | |
| let res = map.map_oid(s1, HashAlgorithm::SHA256).unwrap(); | |
| assert_eq!(*res, *s256); | |
| } | |
| } | |
| #[test] | |
| fn nul_padding() { | |
| assert_eq!(ObjectMemoryMap::required_nul_padding(1, 1), 3); | |
| assert_eq!(ObjectMemoryMap::required_nul_padding(2, 1), 2); | |
| assert_eq!(ObjectMemoryMap::required_nul_padding(3, 1), 1); | |
| assert_eq!(ObjectMemoryMap::required_nul_padding(2, 2), 0); | |
| assert_eq!(ObjectMemoryMap::required_nul_padding(39, 3), 3); | |
| } | |
| } |