on_disk.rs
843 lines
| 27.1 KiB
| application/rls-services+xml
|
RustLexer
|
r48058 | //! The "version 2" disk representation of the dirstate | ||
//! | ||||
|
r48978 | //! See `mercurial/helptext/internals/dirstate-v2.txt` | ||
|
r48058 | |||
|
r49991 | use crate::dirstate::{DirstateV2Data, TruncatedTimestamp}; | ||
|
r48124 | use crate::dirstate_tree::dirstate_map::{self, DirstateMap, NodeRef}; | ||
|
r48058 | use crate::dirstate_tree::path_with_basename::WithBasename; | ||
use crate::errors::HgError; | ||||
use crate::utils::hg_path::HgPath; | ||||
use crate::DirstateEntry; | ||||
use crate::DirstateError; | ||||
use crate::DirstateParents; | ||||
|
r48951 | use bitflags::bitflags; | ||
|
r49008 | use bytes_cast::unaligned::{U16Be, U32Be}; | ||
|
r48058 | use bytes_cast::BytesCast; | ||
|
r48474 | use format_bytes::format_bytes; | ||
|
r49249 | use rand::Rng; | ||
|
r48058 | use std::borrow::Cow; | ||
|
r48475 | use std::convert::{TryFrom, TryInto}; | ||
|
r49249 | use std::fmt::Write; | ||
|
r48058 | |||
|
r48055 | /// Added at the start of `.hg/dirstate` when the "v2" format is used. | ||
|
r48058 | /// This a redundant sanity check more than an actual "magic number" since | ||
/// `.hg/requires` already governs which format should be used. | ||||
|
r48055 | pub const V2_FORMAT_MARKER: &[u8; 12] = b"dirstate-v2\n"; | ||
|
r48058 | |||
|
r48474 | /// Keep space for 256-bit hashes | ||
const STORED_NODE_ID_BYTES: usize = 32; | ||||
/// … even though only 160 bits are used for now, with SHA-1 | ||||
const USED_NODE_ID_BYTES: usize = 20; | ||||
|
r48202 | pub(super) const IGNORE_PATTERNS_HASH_LEN: usize = 20; | ||
pub(super) type IgnorePatternsHash = [u8; IGNORE_PATTERNS_HASH_LEN]; | ||||
|
r49035 | /// Must match constants of the same names in `mercurial/dirstateutils/v2.py` | ||
|
r48484 | const TREE_METADATA_SIZE: usize = 44; | ||
|
r49045 | const NODE_SIZE: usize = 44; | ||
|
r49010 | |||
|
r48482 | /// Make sure that size-affecting changes are made knowingly | ||
#[allow(unused)] | ||||
fn static_assert_size_of() { | ||||
let _ = std::mem::transmute::<TreeMetadata, [u8; TREE_METADATA_SIZE]>; | ||||
|
r48484 | let _ = std::mem::transmute::<DocketHeader, [u8; TREE_METADATA_SIZE + 81]>; | ||
|
r49010 | let _ = std::mem::transmute::<Node, [u8; NODE_SIZE]>; | ||
|
r48482 | } | ||
|
r48474 | // Must match `HEADER` in `mercurial/dirstateutils/docket.py` | ||
#[derive(BytesCast)] | ||||
#[repr(C)] | ||||
struct DocketHeader { | ||||
marker: [u8; V2_FORMAT_MARKER.len()], | ||||
parent_1: [u8; STORED_NODE_ID_BYTES], | ||||
parent_2: [u8; STORED_NODE_ID_BYTES], | ||||
|
r48477 | |||
|
r48977 | metadata: TreeMetadata, | ||
|
r48477 | /// Counted in bytes | ||
|
r48474 | data_size: Size, | ||
|
r48477 | |||
|
r48474 | uuid_size: u8, | ||
} | ||||
pub struct Docket<'on_disk> { | ||||
header: &'on_disk DocketHeader, | ||||
|
r49248 | pub uuid: &'on_disk [u8], | ||
|
r48474 | } | ||
|
r49002 | /// Fields are documented in the *Tree metadata in the docket file* | ||
/// section of `mercurial/helptext/internals/dirstate-v2.txt` | ||||
|
r48058 | #[derive(BytesCast)] | ||
#[repr(C)] | ||||
|
r49249 | pub struct TreeMetadata { | ||
|
r48476 | root_nodes: ChildNodes, | ||
|
r48058 | nodes_with_entry_count: Size, | ||
nodes_with_copy_source_count: Size, | ||||
|
r48481 | unreachable_bytes: Size, | ||
|
r48484 | unused: [u8; 4], | ||
|
r49002 | /// See *Optional hash of ignore patterns* section of | ||
/// `mercurial/helptext/internals/dirstate-v2.txt` | ||||
|
r48202 | ignore_patterns_hash: IgnorePatternsHash, | ||
|
r48058 | } | ||
|
r49002 | /// Fields are documented in the *The data file format* | ||
/// section of `mercurial/helptext/internals/dirstate-v2.txt` | ||||
|
r50016 | #[derive(BytesCast, Debug)] | ||
|
r48058 | #[repr(C)] | ||
|
r48128 | pub(super) struct Node { | ||
|
r48058 | full_path: PathSlice, | ||
/// In bytes from `self.full_path.start` | ||||
|
r48477 | base_name_start: PathSize, | ||
|
r48058 | |||
copy_source: OptPathSlice, | ||||
children: ChildNodes, | ||||
|
r48272 | pub(super) descendants_with_entry_count: Size, | ||
|
r48128 | pub(super) tracked_descendants_count: Size, | ||
|
r49045 | flags: U16Be, | ||
|
r49033 | size: U32Be, | ||
mtime: PackedTruncatedTimestamp, | ||||
|
r48058 | } | ||
|
r48951 | bitflags! { | ||
#[repr(C)] | ||||
|
r49045 | struct Flags: u16 { | ||
|
r48951 | const WDIR_TRACKED = 1 << 0; | ||
const P1_TRACKED = 1 << 1; | ||||
const P2_INFO = 1 << 2; | ||||
r49085 | const MODE_EXEC_PERM = 1 << 3; | |||
const MODE_IS_SYMLINK = 1 << 4; | ||||
const HAS_FALLBACK_EXEC = 1 << 5; | ||||
const FALLBACK_EXEC = 1 << 6; | ||||
const HAS_FALLBACK_SYMLINK = 1 << 7; | ||||
const FALLBACK_SYMLINK = 1 << 8; | ||||
const EXPECTED_STATE_IS_MODIFIED = 1 << 9; | ||||
const HAS_MODE_AND_SIZE = 1 <<10; | ||||
const HAS_MTIME = 1 <<11; | ||||
const MTIME_SECOND_AMBIGUOUS = 1 << 12; | ||||
const DIRECTORY = 1 <<13; | ||||
const ALL_UNKNOWN_RECORDED = 1 <<14; | ||||
const ALL_IGNORED_RECORDED = 1 <<15; | ||||
|
r48951 | } | ||
} | ||||
|
r48137 | /// Duration since the Unix epoch | ||
|
r50016 | #[derive(BytesCast, Copy, Clone, Debug)] | ||
|
r48137 | #[repr(C)] | ||
|
r49033 | struct PackedTruncatedTimestamp { | ||
|
r49007 | truncated_seconds: U32Be, | ||
|
r48137 | nanoseconds: U32Be, | ||
} | ||||
|
r48058 | /// Counted in bytes from the start of the file | ||
/// | ||||
|
r48270 | /// NOTE: not supporting `.hg/dirstate` files larger than 4 GiB. | ||
type Offset = U32Be; | ||||
|
r48058 | |||
/// Counted in number of items | ||||
/// | ||||
|
r48477 | /// NOTE: we choose not to support counting more than 4 billion nodes anywhere. | ||
|
r48058 | type Size = U32Be; | ||
|
r48477 | /// Counted in bytes | ||
|
r48058 | /// | ||
|
r48477 | /// NOTE: we choose not to support file names/paths longer than 64 KiB. | ||
type PathSize = U16Be; | ||||
|
r48058 | |||
/// A contiguous sequence of `len` times `Node`, representing the child nodes | ||||
/// of either some other node or of the repository root. | ||||
/// | ||||
/// Always sorted by ascending `full_path`, to allow binary search. | ||||
/// Since nodes with the same parent nodes also have the same parent path, | ||||
/// only the `base_name`s need to be compared during binary search. | ||||
|
r50016 | #[derive(BytesCast, Copy, Clone, Debug)] | ||
|
r48477 | #[repr(C)] | ||
struct ChildNodes { | ||||
start: Offset, | ||||
len: Size, | ||||
} | ||||
|
r48058 | |||
/// A `HgPath` of `len` bytes | ||||
|
r50016 | #[derive(BytesCast, Copy, Clone, Debug)] | ||
|
r48477 | #[repr(C)] | ||
struct PathSlice { | ||||
start: Offset, | ||||
len: PathSize, | ||||
} | ||||
|
r48058 | |||
/// Either nothing if `start == 0`, or a `HgPath` of `len` bytes | ||||
|
r48477 | type OptPathSlice = PathSlice; | ||
|
r48058 | |||
|
r48125 | /// Unexpected file format found in `.hg/dirstate` with the "v2" format. | ||
|
r48126 | /// | ||
/// This should only happen if Mercurial is buggy or a repository is corrupted. | ||||
#[derive(Debug)] | ||||
pub struct DirstateV2ParseError; | ||||
|
r48125 | |||
impl From<DirstateV2ParseError> for HgError { | ||||
fn from(_: DirstateV2ParseError) -> Self { | ||||
HgError::corrupted("dirstate-v2 parse error") | ||||
} | ||||
} | ||||
impl From<DirstateV2ParseError> for crate::DirstateError { | ||||
fn from(error: DirstateV2ParseError) -> Self { | ||||
HgError::from(error).into() | ||||
} | ||||
} | ||||
|
r49249 | impl TreeMetadata { | ||
pub fn as_bytes(&self) -> &[u8] { | ||||
BytesCast::as_bytes(self) | ||||
} | ||||
} | ||||
|
r48474 | impl<'on_disk> Docket<'on_disk> { | ||
|
r49249 | /// Generate the identifier for a new data file | ||
/// | ||||
/// TODO: support the `HGTEST_UUIDFILE` environment variable. | ||||
/// See `mercurial/revlogutils/docket.py` | ||||
pub fn new_uid() -> String { | ||||
const ID_LENGTH: usize = 8; | ||||
let mut id = String::with_capacity(ID_LENGTH); | ||||
let mut rng = rand::thread_rng(); | ||||
for _ in 0..ID_LENGTH { | ||||
// One random hexadecimal digit. | ||||
// `unwrap` never panics because `impl Write for String` | ||||
// never returns an error. | ||||
|
r49403 | write!(&mut id, "{:x}", rng.gen_range(0..16)).unwrap(); | ||
|
r49249 | } | ||
id | ||||
} | ||||
pub fn serialize( | ||||
parents: DirstateParents, | ||||
tree_metadata: TreeMetadata, | ||||
data_size: u64, | ||||
uuid: &[u8], | ||||
) -> Result<Vec<u8>, std::num::TryFromIntError> { | ||||
let header = DocketHeader { | ||||
marker: *V2_FORMAT_MARKER, | ||||
parent_1: parents.p1.pad_to_256_bits(), | ||||
parent_2: parents.p2.pad_to_256_bits(), | ||||
metadata: tree_metadata, | ||||
data_size: u32::try_from(data_size)?.into(), | ||||
uuid_size: uuid.len().try_into()?, | ||||
}; | ||||
let header = header.as_bytes(); | ||||
let mut docket = Vec::with_capacity(header.len() + uuid.len()); | ||||
docket.extend_from_slice(header); | ||||
docket.extend_from_slice(uuid); | ||||
Ok(docket) | ||||
} | ||||
|
r48474 | pub fn parents(&self) -> DirstateParents { | ||
use crate::Node; | ||||
let p1 = Node::try_from(&self.header.parent_1[..USED_NODE_ID_BYTES]) | ||||
.unwrap() | ||||
.clone(); | ||||
let p2 = Node::try_from(&self.header.parent_2[..USED_NODE_ID_BYTES]) | ||||
.unwrap() | ||||
.clone(); | ||||
DirstateParents { p1, p2 } | ||||
} | ||||
|
r48482 | pub fn tree_metadata(&self) -> &[u8] { | ||
self.header.metadata.as_bytes() | ||||
} | ||||
|
r48475 | pub fn data_size(&self) -> usize { | ||
// This `unwrap` could only panic on a 16-bit CPU | ||||
self.header.data_size.get().try_into().unwrap() | ||||
} | ||||
|
r48474 | pub fn data_filename(&self) -> String { | ||
|
r48780 | String::from_utf8(format_bytes!(b"dirstate.{}", self.uuid)).unwrap() | ||
|
r48474 | } | ||
} | ||||
pub fn read_docket( | ||||
on_disk: &[u8], | ||||
) -> Result<Docket<'_>, DirstateV2ParseError> { | ||||
let (header, uuid) = | ||||
DocketHeader::from_bytes(on_disk).map_err(|_| DirstateV2ParseError)?; | ||||
let uuid_size = header.uuid_size as usize; | ||||
if header.marker == *V2_FORMAT_MARKER && uuid.len() == uuid_size { | ||||
Ok(Docket { header, uuid }) | ||||
|
r48165 | } else { | ||
Err(DirstateV2ParseError) | ||||
} | ||||
} | ||||
|
r48058 | pub(super) fn read<'on_disk>( | ||
on_disk: &'on_disk [u8], | ||||
|
r48482 | metadata: &[u8], | ||
|
r48474 | ) -> Result<DirstateMap<'on_disk>, DirstateV2ParseError> { | ||
|
r48058 | if on_disk.is_empty() { | ||
|
r48474 | return Ok(DirstateMap::empty(on_disk)); | ||
|
r48058 | } | ||
|
r48482 | let (meta, _) = TreeMetadata::from_bytes(metadata) | ||
.map_err(|_| DirstateV2ParseError)?; | ||||
|
r48058 | let dirstate_map = DirstateMap { | ||
on_disk, | ||||
|
r48477 | root: dirstate_map::ChildNodes::OnDisk(read_nodes( | ||
|
r48165 | on_disk, | ||
|
r48482 | meta.root_nodes, | ||
|
r48128 | )?), | ||
|
r48482 | nodes_with_entry_count: meta.nodes_with_entry_count.get(), | ||
nodes_with_copy_source_count: meta.nodes_with_copy_source_count.get(), | ||||
ignore_patterns_hash: meta.ignore_patterns_hash, | ||||
unreachable_bytes: meta.unreachable_bytes.get(), | ||||
|
r48058 | }; | ||
|
r48474 | Ok(dirstate_map) | ||
|
r48058 | } | ||
impl Node { | ||||
|
r48128 | pub(super) fn full_path<'on_disk>( | ||
|
r48058 | &self, | ||
on_disk: &'on_disk [u8], | ||||
|
r48128 | ) -> Result<&'on_disk HgPath, DirstateV2ParseError> { | ||
read_hg_path(on_disk, self.full_path) | ||||
} | ||||
pub(super) fn base_name_start<'on_disk>( | ||||
&self, | ||||
) -> Result<usize, DirstateV2ParseError> { | ||||
let start = self.base_name_start.get(); | ||||
if start < self.full_path.len.get() { | ||||
let start = usize::try_from(start) | ||||
// u32 -> usize, could only panic on a 16-bit CPU | ||||
.expect("dirstate-v2 base_name_start out of bounds"); | ||||
Ok(start) | ||||
|
r48058 | } else { | ||
|
r48125 | Err(DirstateV2ParseError) | ||
|
r48058 | } | ||
} | ||||
|
r48128 | pub(super) fn base_name<'on_disk>( | ||
&self, | ||||
on_disk: &'on_disk [u8], | ||||
) -> Result<&'on_disk HgPath, DirstateV2ParseError> { | ||||
let full_path = self.full_path(on_disk)?; | ||||
let base_name_start = self.base_name_start()?; | ||||
Ok(HgPath::new(&full_path.as_bytes()[base_name_start..])) | ||||
} | ||||
pub(super) fn path<'on_disk>( | ||||
&self, | ||||
on_disk: &'on_disk [u8], | ||||
) -> Result<dirstate_map::NodeKey<'on_disk>, DirstateV2ParseError> { | ||||
Ok(WithBasename::from_raw_parts( | ||||
Cow::Borrowed(self.full_path(on_disk)?), | ||||
self.base_name_start()?, | ||||
)) | ||||
} | ||||
pub(super) fn has_copy_source<'on_disk>(&self) -> bool { | ||||
self.copy_source.start.get() != 0 | ||||
} | ||||
|
r48058 | pub(super) fn copy_source<'on_disk>( | ||
&self, | ||||
on_disk: &'on_disk [u8], | ||||
|
r48128 | ) -> Result<Option<&'on_disk HgPath>, DirstateV2ParseError> { | ||
Ok(if self.has_copy_source() { | ||||
|
r48058 | Some(read_hg_path(on_disk, self.copy_source)?) | ||
} else { | ||||
None | ||||
}) | ||||
} | ||||
|
r49045 | fn flags(&self) -> Flags { | ||
Flags::from_bits_truncate(self.flags.get()) | ||||
} | ||||
|
r48951 | fn has_entry(&self) -> bool { | ||
|
r49045 | self.flags().intersects( | ||
|
r48951 | Flags::WDIR_TRACKED | Flags::P1_TRACKED | Flags::P2_INFO, | ||
) | ||||
} | ||||
|
r48137 | pub(super) fn node_data( | ||
&self, | ||||
) -> Result<dirstate_map::NodeData, DirstateV2ParseError> { | ||||
|
r48951 | if self.has_entry() { | ||
|
r49079 | Ok(dirstate_map::NodeData::Entry(self.assume_entry()?)) | ||
|
r49007 | } else if let Some(mtime) = self.cached_directory_mtime()? { | ||
|
r48951 | Ok(dirstate_map::NodeData::CachedDirectory { mtime }) | ||
} else { | ||||
Ok(dirstate_map::NodeData::None) | ||||
|
r48137 | } | ||
|
r48128 | } | ||
|
r49007 | pub(super) fn cached_directory_mtime( | ||
&self, | ||||
) -> Result<Option<TruncatedTimestamp>, DirstateV2ParseError> { | ||||
r49083 | // For now we do not have code to handle the absence of | |||
// ALL_UNKNOWN_RECORDED, so we ignore the mtime if the flag is | ||||
// unset. | ||||
if self.flags().contains(Flags::DIRECTORY) | ||||
&& self.flags().contains(Flags::HAS_MTIME) | ||||
r49067 | && self.flags().contains(Flags::ALL_UNKNOWN_RECORDED) | |||
{ | ||||
|
r49332 | Ok(Some(self.mtime()?)) | ||
|
r49046 | } else { | ||
Ok(None) | ||||
} | ||||
|
r48138 | } | ||
|
r49009 | fn synthesize_unix_mode(&self) -> u32 { | ||
|
r49045 | let file_type = if self.flags().contains(Flags::MODE_IS_SYMLINK) { | ||
|
r49009 | libc::S_IFLNK | ||
} else { | ||||
libc::S_IFREG | ||||
}; | ||||
|
r49045 | let permisions = if self.flags().contains(Flags::MODE_EXEC_PERM) { | ||
|
r49009 | 0o755 | ||
} else { | ||||
0o644 | ||||
}; | ||||
|
r49417 | (file_type | permisions).into() | ||
|
r49009 | } | ||
|
r49332 | fn mtime(&self) -> Result<TruncatedTimestamp, DirstateV2ParseError> { | ||
let mut m: TruncatedTimestamp = self.mtime.try_into()?; | ||||
if self.flags().contains(Flags::MTIME_SECOND_AMBIGUOUS) { | ||||
m.second_ambiguous = true; | ||||
} | ||||
Ok(m) | ||||
} | ||||
|
r49079 | fn assume_entry(&self) -> Result<DirstateEntry, DirstateV2ParseError> { | ||
|
r48951 | // TODO: convert through raw bits instead? | ||
|
r49991 | let wc_tracked = self.flags().contains(Flags::WDIR_TRACKED); | ||
|
r49045 | let p1_tracked = self.flags().contains(Flags::P1_TRACKED); | ||
let p2_info = self.flags().contains(Flags::P2_INFO); | ||||
|
r49066 | let mode_size = if self.flags().contains(Flags::HAS_MODE_AND_SIZE) | ||
&& !self.flags().contains(Flags::EXPECTED_STATE_IS_MODIFIED) | ||||
{ | ||||
|
r49033 | Some((self.synthesize_unix_mode(), self.size.into())) | ||
|
r48951 | } else { | ||
None | ||||
}; | ||||
r49083 | let mtime = if self.flags().contains(Flags::HAS_MTIME) | |||
&& !self.flags().contains(Flags::DIRECTORY) | ||||
|
r49066 | && !self.flags().contains(Flags::EXPECTED_STATE_IS_MODIFIED) | ||
{ | ||||
|
r49332 | Some(self.mtime()?) | ||
|
r48951 | } else { | ||
None | ||||
}; | ||||
r49084 | let fallback_exec = if self.flags().contains(Flags::HAS_FALLBACK_EXEC) | |||
{ | ||||
Some(self.flags().contains(Flags::FALLBACK_EXEC)) | ||||
} else { | ||||
None | ||||
}; | ||||
let fallback_symlink = | ||||
if self.flags().contains(Flags::HAS_FALLBACK_SYMLINK) { | ||||
Some(self.flags().contains(Flags::FALLBACK_SYMLINK)) | ||||
} else { | ||||
None | ||||
}; | ||||
|
r49991 | Ok(DirstateEntry::from_v2_data(DirstateV2Data { | ||
wc_tracked, | ||||
|
r48951 | p1_tracked, | ||
p2_info, | ||||
mode_size, | ||||
mtime, | ||||
r49084 | fallback_exec, | |||
fallback_symlink, | ||||
|
r49991 | })) | ||
|
r48128 | } | ||
|
r48125 | pub(super) fn entry( | ||
&self, | ||||
) -> Result<Option<DirstateEntry>, DirstateV2ParseError> { | ||||
|
r48951 | if self.has_entry() { | ||
|
r49079 | Ok(Some(self.assume_entry()?)) | ||
|
r48951 | } else { | ||
Ok(None) | ||||
} | ||||
|
r48128 | } | ||
pub(super) fn children<'on_disk>( | ||||
&self, | ||||
on_disk: &'on_disk [u8], | ||||
) -> Result<&'on_disk [Node], DirstateV2ParseError> { | ||||
|
r48477 | read_nodes(on_disk, self.children) | ||
|
r48058 | } | ||
pub(super) fn to_in_memory_node<'on_disk>( | ||||
&self, | ||||
on_disk: &'on_disk [u8], | ||||
|
r48125 | ) -> Result<dirstate_map::Node<'on_disk>, DirstateV2ParseError> { | ||
|
r48058 | Ok(dirstate_map::Node { | ||
|
r48128 | children: dirstate_map::ChildNodes::OnDisk( | ||
self.children(on_disk)?, | ||||
), | ||||
copy_source: self.copy_source(on_disk)?.map(Cow::Borrowed), | ||||
|
r48137 | data: self.node_data()?, | ||
|
r48272 | descendants_with_entry_count: self | ||
.descendants_with_entry_count | ||||
.get(), | ||||
|
r48058 | tracked_descendants_count: self.tracked_descendants_count.get(), | ||
}) | ||||
} | ||||
|
r49033 | fn from_dirstate_entry( | ||
entry: &DirstateEntry, | ||||
) -> (Flags, U32Be, PackedTruncatedTimestamp) { | ||||
|
r49991 | let DirstateV2Data { | ||
wc_tracked, | ||||
r49070 | p1_tracked, | |||
p2_info, | ||||
|
r49991 | mode_size: mode_size_opt, | ||
mtime: mtime_opt, | ||||
r49070 | fallback_exec, | |||
fallback_symlink, | ||||
|
r49991 | } = entry.v2_data(); | ||
// TODO: convert through raw flag bits instead? | ||||
|
r48951 | let mut flags = Flags::empty(); | ||
|
r49991 | flags.set(Flags::WDIR_TRACKED, wc_tracked); | ||
|
r48951 | flags.set(Flags::P1_TRACKED, p1_tracked); | ||
flags.set(Flags::P2_INFO, p2_info); | ||||
|
r49033 | let size = if let Some((m, s)) = mode_size_opt { | ||
|
r49417 | let exec_perm = m & (libc::S_IXUSR as u32) != 0; | ||
let is_symlink = m & (libc::S_IFMT as u32) == libc::S_IFLNK as u32; | ||||
|
r49009 | flags.set(Flags::MODE_EXEC_PERM, exec_perm); | ||
flags.set(Flags::MODE_IS_SYMLINK, is_symlink); | ||||
|
r49033 | flags.insert(Flags::HAS_MODE_AND_SIZE); | ||
s.into() | ||||
|
r48951 | } else { | ||
|
r49033 | 0.into() | ||
|
r48951 | }; | ||
|
r49033 | let mtime = if let Some(m) = mtime_opt { | ||
r49083 | flags.insert(Flags::HAS_MTIME); | |||
r49230 | if m.second_ambiguous { | |||
flags.insert(Flags::MTIME_SECOND_AMBIGUOUS); | ||||
}; | ||||
|
r49079 | m.into() | ||
|
r49033 | } else { | ||
PackedTruncatedTimestamp::null() | ||||
|
r49006 | }; | ||
r49070 | if let Some(f_exec) = fallback_exec { | |||
flags.insert(Flags::HAS_FALLBACK_EXEC); | ||||
if f_exec { | ||||
flags.insert(Flags::FALLBACK_EXEC); | ||||
} | ||||
} | ||||
if let Some(f_symlink) = fallback_symlink { | ||||
flags.insert(Flags::HAS_FALLBACK_SYMLINK); | ||||
if f_symlink { | ||||
flags.insert(Flags::FALLBACK_SYMLINK); | ||||
} | ||||
} | ||||
|
r49033 | (flags, size, mtime) | ||
|
r48137 | } | ||
} | ||||
|
r48125 | fn read_hg_path( | ||
on_disk: &[u8], | ||||
|
r48477 | slice: PathSlice, | ||
|
r48128 | ) -> Result<&HgPath, DirstateV2ParseError> { | ||
|
r48477 | read_slice(on_disk, slice.start, slice.len.get()).map(HgPath::new) | ||
|
r48058 | } | ||
|
r48477 | fn read_nodes( | ||
|
r48125 | on_disk: &[u8], | ||
|
r48477 | slice: ChildNodes, | ||
) -> Result<&[Node], DirstateV2ParseError> { | ||||
read_slice(on_disk, slice.start, slice.len.get()) | ||||
} | ||||
fn read_slice<T, Len>( | ||||
on_disk: &[u8], | ||||
start: Offset, | ||||
len: Len, | ||||
|
r48125 | ) -> Result<&[T], DirstateV2ParseError> | ||
|
r48058 | where | ||
T: BytesCast, | ||||
|
r48477 | Len: TryInto<usize>, | ||
|
r48058 | { | ||
// Either `usize::MAX` would result in "out of bounds" error since a single | ||||
// `&[u8]` cannot occupy the entire addess space. | ||||
|
r48477 | let start = start.get().try_into().unwrap_or(std::usize::MAX); | ||
let len = len.try_into().unwrap_or(std::usize::MAX); | ||||
|
r48058 | on_disk | ||
.get(start..) | ||||
.and_then(|bytes| T::slice_from_bytes(bytes, len).ok()) | ||||
.map(|(slice, _rest)| slice) | ||||
|
r48125 | .ok_or_else(|| DirstateV2ParseError) | ||
|
r48058 | } | ||
|
r48165 | pub(crate) fn for_each_tracked_path<'on_disk>( | ||
on_disk: &'on_disk [u8], | ||||
|
r48482 | metadata: &[u8], | ||
|
r48165 | mut f: impl FnMut(&'on_disk HgPath), | ||
) -> Result<(), DirstateV2ParseError> { | ||||
|
r48482 | let (meta, _) = TreeMetadata::from_bytes(metadata) | ||
.map_err(|_| DirstateV2ParseError)?; | ||||
|
r48165 | fn recur<'on_disk>( | ||
on_disk: &'on_disk [u8], | ||||
|
r48477 | nodes: ChildNodes, | ||
|
r48165 | f: &mut impl FnMut(&'on_disk HgPath), | ||
) -> Result<(), DirstateV2ParseError> { | ||||
|
r48477 | for node in read_nodes(on_disk, nodes)? { | ||
|
r48951 | if let Some(entry) = node.entry()? { | ||
|
r50027 | if entry.tracked() { | ||
|
r48165 | f(node.full_path(on_disk)?) | ||
} | ||||
} | ||||
recur(on_disk, node.children, f)? | ||||
} | ||||
Ok(()) | ||||
} | ||||
|
r48482 | recur(on_disk, meta.root_nodes, &mut f) | ||
|
r48165 | } | ||
|
r48482 | /// Returns new data and metadata, together with whether that data should be | ||
/// appended to the existing data file whose content is at | ||||
/// `dirstate_map.on_disk` (true), instead of written to a new data file | ||||
/// (false). | ||||
|
r48058 | pub(super) fn write( | ||
|
r49244 | dirstate_map: &DirstateMap, | ||
|
r48478 | can_append: bool, | ||
|
r49249 | ) -> Result<(Vec<u8>, TreeMetadata, bool), DirstateError> { | ||
|
r48478 | let append = can_append && dirstate_map.write_should_append(); | ||
|
r48058 | |||
// This ignores the space for paths, and for nodes without an entry. | ||||
// TODO: better estimate? Skip the `Vec` and write to a file directly? | ||||
|
r48482 | let size_guess = std::mem::size_of::<Node>() | ||
* dirstate_map.nodes_with_entry_count as usize; | ||||
|
r48058 | |||
|
r48478 | let mut writer = Writer { | ||
dirstate_map, | ||||
append, | ||||
out: Vec::with_capacity(size_guess), | ||||
}; | ||||
let root_nodes = writer.write_nodes(dirstate_map.root.as_ref())?; | ||||
|
r48058 | |||
|
r48482 | let meta = TreeMetadata { | ||
|
r48476 | root_nodes, | ||
|
r48058 | nodes_with_entry_count: dirstate_map.nodes_with_entry_count.into(), | ||
nodes_with_copy_source_count: dirstate_map | ||||
.nodes_with_copy_source_count | ||||
.into(), | ||||
|
r48481 | unreachable_bytes: dirstate_map.unreachable_bytes.into(), | ||
|
r48484 | unused: [0; 4], | ||
|
r48202 | ignore_patterns_hash: dirstate_map.ignore_patterns_hash, | ||
|
r48058 | }; | ||
|
r49249 | Ok((writer.out, meta, append)) | ||
|
r48478 | } | ||
struct Writer<'dmap, 'on_disk> { | ||||
dirstate_map: &'dmap DirstateMap<'on_disk>, | ||||
append: bool, | ||||
out: Vec<u8>, | ||||
|
r48058 | } | ||
|
r48478 | impl Writer<'_, '_> { | ||
fn write_nodes( | ||||
&mut self, | ||||
nodes: dirstate_map::ChildNodesRef, | ||||
) -> Result<ChildNodes, DirstateError> { | ||||
|
r48479 | // Reuse already-written nodes if possible | ||
if self.append { | ||||
if let dirstate_map::ChildNodesRef::OnDisk(nodes_slice) = nodes { | ||||
|
r48480 | let start = self.on_disk_offset_of(nodes_slice).expect( | ||
"dirstate-v2 OnDisk nodes not found within on_disk", | ||||
); | ||||
|
r48479 | let len = child_nodes_len_from_usize(nodes_slice.len()); | ||
return Ok(ChildNodes { start, len }); | ||||
} | ||||
} | ||||
// `dirstate_map::ChildNodes::InMemory` contains a `HashMap` which has | ||||
// undefined iteration order. Sort to enable binary search in the | ||||
// written file. | ||||
|
r48478 | let nodes = nodes.sorted(); | ||
let nodes_len = nodes.len(); | ||||
|
r48058 | |||
|
r48478 | // First accumulate serialized nodes in a `Vec` | ||
let mut on_disk_nodes = Vec::with_capacity(nodes_len); | ||||
for node in nodes { | ||||
let children = | ||||
self.write_nodes(node.children(self.dirstate_map.on_disk)?)?; | ||||
let full_path = node.full_path(self.dirstate_map.on_disk)?; | ||||
let full_path = self.write_path(full_path.as_bytes()); | ||||
let copy_source = if let Some(source) = | ||||
node.copy_source(self.dirstate_map.on_disk)? | ||||
{ | ||||
self.write_path(source.as_bytes()) | ||||
|
r48127 | } else { | ||
|
r48477 | PathSlice { | ||
|
r48127 | start: 0.into(), | ||
len: 0.into(), | ||||
} | ||||
}; | ||||
|
r48478 | on_disk_nodes.push(match node { | ||
NodeRef::InMemory(path, node) => { | ||||
|
r49033 | let (flags, size, mtime) = match &node.data { | ||
|
r48951 | dirstate_map::NodeData::Entry(entry) => { | ||
|
r49033 | Node::from_dirstate_entry(entry) | ||
|
r48951 | } | ||
|
r49332 | dirstate_map::NodeData::CachedDirectory { mtime } => { | ||
r49067 | // we currently never set a mtime if unknown file | |||
// are present. | ||||
// So if we have a mtime for a directory, we know | ||||
// they are no unknown | ||||
// files and we | ||||
// blindly set ALL_UNKNOWN_RECORDED. | ||||
// | ||||
// We never set ALL_IGNORED_RECORDED since we | ||||
// don't track that case | ||||
// currently. | ||||
|
r49332 | let mut flags = Flags::DIRECTORY | ||
r49083 | | Flags::HAS_MTIME | |||
|
r49332 | | Flags::ALL_UNKNOWN_RECORDED; | ||
if mtime.second_ambiguous { | ||||
flags.insert(Flags::MTIME_SECOND_AMBIGUOUS) | ||||
} | ||||
(flags, 0.into(), (*mtime).into()) | ||||
} | ||||
|
r48478 | dirstate_map::NodeData::None => ( | ||
r49083 | Flags::DIRECTORY, | |||
|
r49033 | 0.into(), | ||
PackedTruncatedTimestamp::null(), | ||||
|
r48478 | ), | ||
}; | ||||
Node { | ||||
children, | ||||
copy_source, | ||||
full_path, | ||||
base_name_start: u16::try_from(path.base_name_start()) | ||||
// Could only panic for paths over 64 KiB | ||||
.expect("dirstate-v2 path length overflow") | ||||
.into(), | ||||
descendants_with_entry_count: node | ||||
.descendants_with_entry_count | ||||
.into(), | ||||
tracked_descendants_count: node | ||||
.tracked_descendants_count | ||||
.into(), | ||||
|
r49045 | flags: flags.bits().into(), | ||
|
r49033 | size, | ||
mtime, | ||||
|
r48124 | } | ||
|
r48478 | } | ||
NodeRef::OnDisk(node) => Node { | ||||
|
r48137 | children, | ||
copy_source, | ||||
full_path, | ||||
|
r48478 | ..*node | ||
}, | ||||
}) | ||||
} | ||||
// … so we can write them contiguously, after writing everything else | ||||
// they refer to. | ||||
let start = self.current_offset(); | ||||
|
r48479 | let len = child_nodes_len_from_usize(nodes_len); | ||
|
r48478 | self.out.extend(on_disk_nodes.as_bytes()); | ||
Ok(ChildNodes { start, len }) | ||||
|
r48058 | } | ||
|
r48480 | /// If the given slice of items is within `on_disk`, returns its offset | ||
/// from the start of `on_disk`. | ||||
fn on_disk_offset_of<T>(&self, slice: &[T]) -> Option<Offset> | ||||
|
r48479 | where | ||
T: BytesCast, | ||||
{ | ||||
fn address_range(slice: &[u8]) -> std::ops::RangeInclusive<usize> { | ||||
let start = slice.as_ptr() as usize; | ||||
let end = start + slice.len(); | ||||
start..=end | ||||
} | ||||
let slice_addresses = address_range(slice.as_bytes()); | ||||
let on_disk_addresses = address_range(self.dirstate_map.on_disk); | ||||
|
r48480 | if on_disk_addresses.contains(slice_addresses.start()) | ||
&& on_disk_addresses.contains(slice_addresses.end()) | ||||
{ | ||||
let offset = slice_addresses.start() - on_disk_addresses.start(); | ||||
Some(offset_from_usize(offset)) | ||||
} else { | ||||
None | ||||
} | ||||
|
r48479 | } | ||
|
r48478 | fn current_offset(&mut self) -> Offset { | ||
let mut offset = self.out.len(); | ||||
if self.append { | ||||
offset += self.dirstate_map.on_disk.len() | ||||
} | ||||
|
r48479 | offset_from_usize(offset) | ||
|
r48478 | } | ||
|
r48477 | |||
|
r48478 | fn write_path(&mut self, slice: &[u8]) -> PathSlice { | ||
|
r48480 | let len = path_len_from_usize(slice.len()); | ||
// Reuse an already-written path if possible | ||||
if self.append { | ||||
if let Some(start) = self.on_disk_offset_of(slice) { | ||||
return PathSlice { start, len }; | ||||
} | ||||
} | ||||
|
r48478 | let start = self.current_offset(); | ||
self.out.extend(slice.as_bytes()); | ||||
PathSlice { start, len } | ||||
} | ||||
|
r48058 | } | ||
|
r48479 | |||
fn offset_from_usize(x: usize) -> Offset { | ||||
u32::try_from(x) | ||||
// Could only panic for a dirstate file larger than 4 GiB | ||||
.expect("dirstate-v2 offset overflow") | ||||
.into() | ||||
} | ||||
fn child_nodes_len_from_usize(x: usize) -> Size { | ||||
u32::try_from(x) | ||||
// Could only panic with over 4 billion nodes | ||||
.expect("dirstate-v2 slice length overflow") | ||||
.into() | ||||
} | ||||
fn path_len_from_usize(x: usize) -> PathSize { | ||||
u16::try_from(x) | ||||
// Could only panic for paths over 64 KiB | ||||
.expect("dirstate-v2 path length overflow") | ||||
.into() | ||||
} | ||||
|
r49033 | |||
impl From<TruncatedTimestamp> for PackedTruncatedTimestamp { | ||||
fn from(timestamp: TruncatedTimestamp) -> Self { | ||||
Self { | ||||
truncated_seconds: timestamp.truncated_seconds().into(), | ||||
nanoseconds: timestamp.nanoseconds().into(), | ||||
} | ||||
} | ||||
} | ||||
impl TryFrom<PackedTruncatedTimestamp> for TruncatedTimestamp { | ||||
type Error = DirstateV2ParseError; | ||||
fn try_from( | ||||
timestamp: PackedTruncatedTimestamp, | ||||
) -> Result<Self, Self::Error> { | ||||
Self::from_already_truncated( | ||||
timestamp.truncated_seconds.get(), | ||||
timestamp.nanoseconds.get(), | ||||
r49227 | false, | |||
|
r49033 | ) | ||
} | ||||
} | ||||
impl PackedTruncatedTimestamp { | ||||
fn null() -> Self { | ||||
Self { | ||||
truncated_seconds: 0.into(), | ||||
nanoseconds: 0.into(), | ||||
} | ||||
} | ||||
} | ||||