view rust/hg-core/src/revlog/nodemap_docket.rs @ 47385:f6bb181c75f8

rust: Parse "subinclude"d files along the way, not later When parsing a `.hgignore` file and encountering an `include:` line, the included file is parsed recursively right then in a depth-first fashion. With `subinclude:` however included files were parsed (recursively) much later. This changes it to be expanded during parsing, like `.hgignore`. The motivation for this is an upcoming changeset that needs to detect changes in which files are ignored or not. The plan is to hash all ignore files while they are being read, and store that hash in the dirstate (in v2 format). In order to allow a potential alternative implementations to read that format, the algorithm to compute that hash must be documented. Having a well-defined depth-first ordering for the tree of (sub-)included files makes that easier. Differential Revision: https://phab.mercurial-scm.org/D10834
author Simon Sapin <simon.sapin@octobus.net>
date Wed, 02 Jun 2021 18:03:43 +0200
parents 842f2372ced6
children e834b79def74
line wrap: on
line source

use crate::errors::{HgError, HgResultExt};
use crate::requirements;
use bytes_cast::{unaligned, BytesCast};
use memmap::Mmap;
use std::path::{Path, PathBuf};

use super::revlog::RevlogError;
use crate::repo::Repo;
use crate::utils::strip_suffix;

const ONDISK_VERSION: u8 = 1;

pub(super) struct NodeMapDocket {
    pub data_length: usize,
    // TODO: keep here more of the data from `parse()` when we need it
}

#[derive(BytesCast)]
#[repr(C)]
struct DocketHeader {
    uid_size: u8,
    _tip_rev: unaligned::U64Be,
    data_length: unaligned::U64Be,
    _data_unused: unaligned::U64Be,
    tip_node_size: unaligned::U64Be,
}

impl NodeMapDocket {
    /// Return `Ok(None)` when the caller should proceed without a persistent
    /// nodemap:
    ///
    /// * This revlog does not have a `.n` docket file (it is not generated for
    ///   small revlogs), or
    /// * The docket has an unsupported version number (repositories created by
    ///   later hg, maybe that should be a requirement instead?), or
    /// * The docket file points to a missing (likely deleted) data file (this
    ///   can happen in a rare race condition).
    pub fn read_from_file(
        repo: &Repo,
        index_path: &Path,
    ) -> Result<Option<(Self, Mmap)>, RevlogError> {
        if !repo
            .requirements()
            .contains(requirements::NODEMAP_REQUIREMENT)
        {
            // If .hg/requires does not opt it, don’t try to open a nodemap
            return Ok(None);
        }

        let docket_path = index_path.with_extension("n");
        let docket_bytes = if let Some(bytes) =
            repo.store_vfs().read(&docket_path).io_not_found_as_none()?
        {
            bytes
        } else {
            return Ok(None);
        };

        let input = if let Some((&ONDISK_VERSION, rest)) =
            docket_bytes.split_first()
        {
            rest
        } else {
            return Ok(None);
        };

        /// Treat any error as a parse error
        fn parse<T, E>(result: Result<T, E>) -> Result<T, RevlogError> {
            result.map_err(|_| {
                HgError::corrupted("nodemap docket parse error").into()
            })
        }

        let (header, rest) = parse(DocketHeader::from_bytes(input))?;
        let uid_size = header.uid_size as usize;
        // TODO: do we care about overflow for 4 GB+ nodemap files on 32-bit
        // systems?
        let tip_node_size = header.tip_node_size.get() as usize;
        let data_length = header.data_length.get() as usize;
        let (uid, rest) = parse(u8::slice_from_bytes(rest, uid_size))?;
        let (_tip_node, _rest) =
            parse(u8::slice_from_bytes(rest, tip_node_size))?;
        let uid = parse(std::str::from_utf8(uid))?;
        let docket = NodeMapDocket { data_length };

        let data_path = rawdata_path(&docket_path, uid);
        // TODO: use `vfs.read()` here when the `persistent-nodemap.mmap`
        // config is false?
        if let Some(mmap) = repo
            .store_vfs()
            .mmap_open(&data_path)
            .io_not_found_as_none()?
        {
            if mmap.len() >= data_length {
                Ok(Some((docket, mmap)))
            } else {
                Err(HgError::corrupted("persistent nodemap too short").into())
            }
        } else {
            // Even if .hg/requires opted in, some revlogs are deemed small
            // enough to not need a persistent nodemap.
            Ok(None)
        }
    }
}

fn rawdata_path(docket_path: &Path, uid: &str) -> PathBuf {
    let docket_name = docket_path
        .file_name()
        .expect("expected a base name")
        .to_str()
        .expect("expected an ASCII file name in the store");
    let prefix = strip_suffix(docket_name, ".n.a")
        .or_else(|| strip_suffix(docket_name, ".n"))
        .expect("expected docket path in .n or .n.a");
    let name = format!("{}-{}.nd", prefix, uid);
    docket_path
        .parent()
        .expect("expected a non-root path")
        .join(name)
}