gix_pack/multi_index/
init.rs1use std::path::Path;
2
3use crate::multi_index::{chunk, File, Version};
4
5mod error {
6 use crate::multi_index::chunk;
7
8 #[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
38impl File {
40 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(); if data.len()
57 < Self::HEADER_LEN
58 + gix_chunk::file::Index::size_for_entries(4 )
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); 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}