gix_pack/multi_index/
init.rs

1use std::path::Path;
2
3use crate::multi_index::{chunk, File, Version};
4
5mod error {
6    use crate::multi_index::chunk;
7
8    /// The error returned by [File::at()][super::File::at()].
9    #[derive(Debug, thiserror::Error)]
10    #[allow(missing_docs)]
11    pub enum Error {
12        #[error("Could not open multi-index file at '{path}'")]
13        Io {
14            source: std::io::Error,
15            path: std::path::PathBuf,
16        },
17        #[error("{message}")]
18        Corrupt { message: &'static str },
19        #[error("Unsupported multi-index version: {version})")]
20        UnsupportedVersion { version: u8 },
21        #[error("Unsupported hash kind: {kind})")]
22        UnsupportedObjectHash { kind: u8 },
23        #[error(transparent)]
24        ChunkFileQuery(#[from] gix_error::Message),
25        #[error(transparent)]
26        ChunkFileDecode(#[from] gix_error::ParseError),
27        #[error("The multi-pack fan doesn't have the correct size of 256 * 4 bytes")]
28        MultiPackFanSize,
29        #[error(transparent)]
30        PackNames(#[from] chunk::index_names::decode::Error),
31        #[error("multi-index chunk {:?} has invalid size: {message}", String::from_utf8_lossy(.id))]
32        InvalidChunkSize { id: gix_chunk::Id, message: &'static str },
33    }
34}
35
36pub use error::Error;
37
38/// Initialization
39impl File {
40    /// Open the multi-index file at the given `path`.
41    pub fn at(path: impl AsRef<Path>) -> Result<Self, Error> {
42        Self::try_from(path.as_ref())
43    }
44}
45
46impl TryFrom<&Path> for File {
47    type Error = Error;
48
49    fn try_from(path: &Path) -> Result<Self, Self::Error> {
50        let data = crate::mmap::read_only(path).map_err(|source| Error::Io {
51            source,
52            path: path.to_owned(),
53        })?;
54
55        const TRAILER_LEN: usize = gix_hash::Kind::shortest().len_in_bytes(); /* trailing hash */
56        if data.len()
57            < Self::HEADER_LEN
58                + gix_chunk::file::Index::size_for_entries(4 /*index names, fan, offsets, oids*/)
59                + chunk::fanout::SIZE
60                + TRAILER_LEN
61        {
62            return Err(Error::Corrupt {
63                message: "multi-index file is truncated and too short",
64            });
65        }
66
67        let (version, object_hash, num_chunks, num_indices) = {
68            let (signature, data) = data.split_at(4);
69            if signature != Self::SIGNATURE {
70                return Err(Error::Corrupt {
71                    message: "Invalid signature",
72                });
73            }
74            let (version, data) = data.split_at(1);
75            let version = match version[0] {
76                1 => Version::V1,
77                version => return Err(Error::UnsupportedVersion { version }),
78            };
79
80            let (object_hash, data) = data.split_at(1);
81            let object_hash = gix_hash::Kind::try_from(object_hash[0])
82                .map_err(|unknown| Error::UnsupportedObjectHash { kind: unknown })?;
83            let (num_chunks, data) = data.split_at(1);
84            let num_chunks = num_chunks[0];
85
86            let (_num_base_files, data) = data.split_at(1); // TODO: handle base files once it's clear what this does
87
88            let (num_indices, _) = data.split_at(4);
89            let num_indices = crate::read_u32(num_indices);
90
91            (version, object_hash, num_chunks, num_indices)
92        };
93
94        let chunks = gix_chunk::file::Index::from_bytes(&data, Self::HEADER_LEN, u32::from(num_chunks))?;
95
96        let index_names = chunks.data_by_id(&data, chunk::index_names::ID)?;
97        let index_names = chunk::index_names::from_bytes(index_names, num_indices)?;
98
99        let fan = chunks.data_by_id(&data, chunk::fanout::ID)?;
100        let fan = chunk::fanout::from_bytes(fan).ok_or(Error::MultiPackFanSize)?;
101        let num_objects = fan[255];
102
103        let lookup = chunks.validated_usize_offset_by_id(chunk::lookup::ID, |offset| {
104            chunk::lookup::is_valid(&offset, object_hash, num_objects)
105                .then_some(offset)
106                .ok_or(Error::InvalidChunkSize {
107                    id: chunk::lookup::ID,
108                    message: "The chunk with alphabetically ordered object ids doesn't have the correct size",
109                })
110        })??;
111        let offsets = chunks.validated_usize_offset_by_id(chunk::offsets::ID, |offset| {
112            chunk::offsets::is_valid(&offset, num_objects)
113                .then_some(offset)
114                .ok_or(Error::InvalidChunkSize {
115                    id: chunk::offsets::ID,
116                    message: "The chunk with offsets into the pack doesn't have the correct size",
117                })
118        })??;
119        let large_offsets = chunks
120            .validated_usize_offset_by_id(chunk::large_offsets::ID, |offset| {
121                chunk::large_offsets::is_valid(&offset)
122                    .then_some(offset)
123                    .ok_or(Error::InvalidChunkSize {
124                        id: chunk::large_offsets::ID,
125                        message: "The chunk with large offsets into the pack doesn't have the correct size",
126                    })
127            })
128            .ok()
129            .transpose()?;
130
131        let checksum_offset = chunks.highest_offset() as usize;
132        let trailer = &data[checksum_offset..];
133        if trailer.len() != object_hash.len_in_bytes() {
134            return Err(Error::Corrupt {
135                message:
136                    "Trailing checksum didn't have the expected size or there were unknown bytes after the checksum.",
137            });
138        }
139
140        Ok(File {
141            data,
142            path: path.to_owned(),
143            version,
144            hash_len: object_hash.len_in_bytes(),
145            object_hash,
146            fan,
147            index_names,
148            lookup_ofs: lookup.start,
149            offsets_ofs: offsets.start,
150            large_offsets_ofs: large_offsets.map(|r| r.start),
151            num_objects,
152            num_indices,
153        })
154    }
155}