The way this is resolved is similar to what git does by means of comparing the expected and actual hashes of the file at hand. Note that we do not actually protect against recursion but assume that nobody can manufacture a file hash that in fact is recursive. We know that this can actually happen but chose to not consider it worth protecting against just yet, as seemingly neither does git.
Unified · Split
| @@ -22,6 +22,11 @@ mod error { · 21 unmodified lines | ||
| 22 | 22 | Extension(#[from] extension::decode::Error), |
| 23 | 23 | #[error("Index trailer should have been {expected} bytes long, but was {actual}")] |
| 24 | 24 | UnexpectedTrailerLength { expected: usize, actual: usize }, |
| 25 | + #[error("Shared index checksum was {actual_checksum} but should have been {expected_checksum}")] | |
| 26 | + ChecksumMismatch { | |
| 27 | + actual_checksum: git_hash::ObjectId, | |
| 28 | + expected_checksum: git_hash::ObjectId, | |
| 29 | + }, | |
| 25 | 30 | } |
| 26 | 31 | } |
| 27 | 32 | pub use error::Error; |
| @@ -41,6 +46,10 @@ pub struct Options { · 13 unmodified lines | ||
| 41 | 46 | /// The minimum size in bytes to load extensions in their own thread, assuming there is enough `num_threads` available. |
| 42 | 47 | /// If set to 0, for example, extensions will always be read in their own thread if enough threads are available. |
| 43 | 48 | pub min_extension_block_in_bytes_for_threading: usize, |
| 49 | + /// Set the expected hash of this index if we are read as part of a `link` extension. | |
| 50 | + /// | |
| 51 | + /// We will abort reading this file if it doesn't match. | |
| 52 | + pub expected_checksum: Option<git_hash::ObjectId>, | |
| 44 | 53 | } |
| 45 | 54 | |
| 46 | 55 | impl State { |
| @@ -53,6 +62,7 @@ impl State { · 6 unmodified lines | ||
| 53 | 62 | Options { |
| 54 | 63 | thread_limit, |
| 55 | 64 | min_extension_block_in_bytes_for_threading, |
| 65 | + expected_checksum, | |
| 56 | 66 | }: Options, |
| 57 | 67 | ) -> Result<(Self, git_hash::ObjectId), Error> { |
| 58 | 68 | let (version, num_entries, post_header_data) = header::decode(data, object_hash)?; |
| @@ -205,6 +215,14 @@ impl State { · 146 unmodified lines | ||
| 205 | 215 | } |
| 206 | 216 | |
| 207 | 217 | let checksum = git_hash::ObjectId::from(data); |
| 218 | + if let Some(expected_checksum) = expected_checksum { | |
| 219 | + if checksum != expected_checksum { | |
| 220 | + return Err(Error::ChecksumMismatch { | |
| 221 | + actual_checksum: checksum, | |
| 222 | + expected_checksum, | |
| 223 | + }); | |
| 224 | + } | |
| 225 | + } | |
| 208 | 226 | let EntriesOutcome { |
| 209 | 227 | entries, |
| 210 | 228 | path_backing, |
| @@ -2,7 +2,7 @@ use std::convert::TryInto; · 1 unmodified line | ||
| 2 | 2 | |
| 3 | 3 | use crate::{extension, extension::Signature, util::from_be_u32}; |
| 4 | 4 | |
| 5 | −pub fn header(data: &[u8]) -> (Signature, u32, &[u8]) { | |
| 5 | +pub(crate) fn header(data: &[u8]) -> (Signature, u32, &[u8]) { | |
| 6 | 6 | let (signature, data) = data.split_at(4); |
| 7 | 7 | let (size, data) = data.split_at(4); |
| 8 | 8 | (signature.try_into().unwrap(), from_be_u32(size), data) |
| @@ -11,8 +11,9 @@ pub fn header(data: &[u8]) -> (Signature, u32, &[u8]) { · 2 unmodified lines | ||
| 11 | 11 | mod error { |
| 12 | 12 | use crate::extension; |
| 13 | 13 | |
| 14 | − /// The error returned by [extension::decode::all()][extension::decode::all()]. | |
| 14 | + /// The error returned when decoding extensions. | |
| 15 | 15 | #[derive(Debug, thiserror::Error)] |
| 16 | + #[allow(missing_docs)] | |
| 16 | 17 | pub enum Error { |
| 17 | 18 | #[error( |
| 18 | 19 | "Encountered mandatory extension '{}' which isn't implemented yet", |
| @@ -25,7 +26,10 @@ mod error { · 6 unmodified lines | ||
| 25 | 26 | } |
| 26 | 27 | pub use error::Error; |
| 27 | 28 | |
| 28 | −pub fn all(maybe_beginning_of_extensions: &[u8], object_hash: git_hash::Kind) -> Result<(Outcome, &[u8]), Error> { | |
| 29 | +pub(crate) fn all( | |
| 30 | + maybe_beginning_of_extensions: &[u8], | |
| 31 | + object_hash: git_hash::Kind, | |
| 32 | +) -> Result<(Outcome, &[u8]), Error> { | |
| 29 | 33 | let mut ext_iter = match extension::Iter::new_without_checksum(maybe_beginning_of_extensions, object_hash) { |
| 30 | 34 | Some(iter) => iter, |
| 31 | 35 | None => return Ok((Outcome::default(), maybe_beginning_of_extensions)), |
| @@ -66,7 +70,7 @@ pub fn all(maybe_beginning_of_extensions: &[u8], object_hash: git_hash::Kind) -> · 34 unmodified lines | ||
| 66 | 70 | } |
| 67 | 71 | |
| 68 | 72 | #[derive(Default)] |
| 69 | −pub struct Outcome { | |
| 73 | +pub(crate) struct Outcome { | |
| 70 | 74 | pub tree: Option<extension::Tree>, |
| 71 | 75 | pub link: Option<extension::Link>, |
| 72 | 76 | pub resolve_undo: Option<extension::resolve_undo::Paths>, |
| @@ -60,3 +60,75 @@ pub(crate) fn decode(data: &[u8], object_hash: git_hash::Kind) -> Result<Link, d · 59 unmodified lines | ||
| 60 | 60 | bitmaps: Some(Bitmaps { delete, replace }), |
| 61 | 61 | }) |
| 62 | 62 | } |
| 63 | + | |
| 64 | +impl Link { | |
| 65 | + pub(crate) fn dissolve_into( | |
| 66 | + self, | |
| 67 | + split_index: &mut crate::File, | |
| 68 | + object_hash: git_hash::Kind, | |
| 69 | + options: crate::decode::Options, | |
| 70 | + ) -> Result<(), crate::file::init::Error> { | |
| 71 | + let shared_index_path = split_index | |
| 72 | + .path | |
| 73 | + .parent() | |
| 74 | + .expect("split index file in .git folder") | |
| 75 | + .join(format!("sharedindex.{}", self.shared_index_checksum)); | |
| 76 | + let mut shared_index = crate::File::at( | |
| 77 | + &shared_index_path, | |
| 78 | + object_hash, | |
| 79 | + crate::decode::Options { | |
| 80 | + expected_checksum: self.shared_index_checksum.into(), | |
| 81 | + ..options | |
| 82 | + }, | |
| 83 | + )?; | |
| 84 | + | |
| 85 | + if let Some(bitmaps) = &self.bitmaps { | |
| 86 | + let mut counter = 0; | |
| 87 | + let shared_entries = shared_index.entries_mut(); | |
| 88 | + let split_entries = split_index.entries(); | |
| 89 | + | |
| 90 | + bitmaps.replace.for_each_set_bit(|index| { | |
| 91 | + match (shared_entries.get_mut(index), split_entries.get(counter)) { | |
| 92 | + (Some(shared_entry), Some(split_entry)) => { | |
| 93 | + shared_entry.stat = split_entry.stat; | |
| 94 | + shared_entry.id = split_entry.id; | |
| 95 | + shared_entry.flags = split_entry.flags; | |
| 96 | + shared_entry.mode = split_entry.mode; | |
| 97 | + } | |
| 98 | + _ => unreachable!(), | |
| 99 | + } | |
| 100 | + | |
| 101 | + counter += 1; | |
| 102 | + Some(()) | |
| 103 | + }); | |
| 104 | + | |
| 105 | + if split_entries.len() > counter { | |
| 106 | + split_entries[counter..].iter().for_each(|split_entry| { | |
| 107 | + let mut e = split_entry.clone(); | |
| 108 | + let start = shared_index.path_backing.len(); | |
| 109 | + e.path = start..start + split_entry.path.len(); | |
| 110 | + shared_index.entries.push(e); | |
| 111 | + | |
| 112 | + shared_index | |
| 113 | + .path_backing | |
| 114 | + .extend_from_slice(&split_index.path_backing[split_entry.path.clone()]); | |
| 115 | + }); | |
| 116 | + } | |
| 117 | + | |
| 118 | + let mut removed_count = 0; | |
| 119 | + bitmaps.delete.for_each_set_bit(|index| { | |
| 120 | + shared_index.entries.remove(index - removed_count); | |
| 121 | + removed_count += 1; | |
| 122 | + Some(()) | |
| 123 | + }); | |
| 124 | + | |
| 125 | + let mut shared_entries = std::mem::take(&mut shared_index.entries); | |
| 126 | + shared_entries.sort_by(|a, b| a.cmp(b, &shared_index.state)); | |
| 127 | + | |
| 128 | + std::mem::swap(&mut split_index.entries, &mut shared_entries); | |
| 129 | + std::mem::swap(&mut split_index.path_backing, &mut shared_index.path_backing); | |
| 130 | + } | |
| 131 | + | |
| 132 | + Ok(()) | |
| 133 | + } | |
| 134 | +} | |
| @@ -73,7 +73,8 @@ mod iter; · 72 unmodified lines | ||
| 73 | 73 | |
| 74 | 74 | pub(crate) mod fs_monitor; |
| 75 | 75 | |
| 76 | −pub(crate) mod decode; | |
| 76 | +/// | |
| 77 | +pub mod decode; | |
| 77 | 78 | |
| 78 | 79 | /// |
| 79 | 80 | pub mod tree; |
| @@ -40,8 +40,8 @@ impl File { · 39 unmodified lines | ||
| 40 | 40 | path, |
| 41 | 41 | checksum: Some(checksum), |
| 42 | 42 | }; |
| 43 | − if let Some(link) = file.link.take() { | |
| 44 | − dissolve_link_extension(&mut file, link, object_hash, options)?; | |
| 43 | + if let Some(mut link) = file.link.take() { | |
| 44 | + link.dissolve_into(&mut file, object_hash, options)?; | |
| 45 | 45 | } |
| 46 | 46 | |
| 47 | 47 | Ok(file) |
| @@ -58,66 +58,3 @@ impl File { · 10 unmodified lines | ||
| 58 | 58 | } |
| 59 | 59 | } |
| 60 | 60 | } |
| 61 | − | |
| 62 | −fn dissolve_link_extension( | |
| 63 | − split_index: &mut File, | |
| 64 | − link: extension::Link, | |
| 65 | − object_hash: git_hash::Kind, | |
| 66 | − options: decode::Options, | |
| 67 | −) -> Result<(), Error> { | |
| 68 | − let shared_index_path = split_index | |
| 69 | − .path | |
| 70 | − .parent() | |
| 71 | − .expect("split index file in .git folder") | |
| 72 | − .join(format!("sharedindex.{}", link.shared_index_checksum)); | |
| 73 | − let mut shared_index = File::at(&shared_index_path, object_hash, options)?; | |
| 74 | − | |
| 75 | − if let Some(bitmaps) = &link.bitmaps { | |
| 76 | − let mut counter = 0; | |
| 77 | − let mut shared_entries = shared_index.entries_mut(); | |
| 78 | − let split_entries = split_index.entries(); | |
| 79 | − | |
| 80 | − bitmaps.replace.for_each_set_bit(|index| { | |
| 81 | − match (shared_entries.get_mut(index), split_entries.get(counter)) { | |
| 82 | − (Some(shared_entry), Some(split_entry)) => { | |
| 83 | − shared_entry.stat = split_entry.stat; | |
| 84 | − shared_entry.id = split_entry.id; | |
| 85 | − shared_entry.flags = split_entry.flags; | |
| 86 | − shared_entry.mode = split_entry.mode; | |
| 87 | − } | |
| 88 | − _ => unreachable!(), | |
| 89 | − } | |
| 90 | − | |
| 91 | − counter += 1; | |
| 92 | − Some(()) | |
| 93 | − }); | |
| 94 | − | |
| 95 | − if split_entries.len() > counter { | |
| 96 | − split_entries[counter..].iter().for_each(|split_entry| { | |
| 97 | − let mut e = split_entry.clone(); | |
| 98 | − let start = shared_index.path_backing.len(); | |
| 99 | − e.path = start..start + split_entry.path.len(); | |
| 100 | − shared_index.entries.push(e); | |
| 101 | − | |
| 102 | − shared_index | |
| 103 | − .path_backing | |
| 104 | − .extend_from_slice(&split_index.path_backing[split_entry.path.clone()]); | |
| 105 | − }); | |
| 106 | − } | |
| 107 | − | |
| 108 | − let mut removed_count = 0; | |
| 109 | − bitmaps.delete.for_each_set_bit(|index| { | |
| 110 | − shared_index.entries.remove(index - removed_count); | |
| 111 | − removed_count += 1; | |
| 112 | − Some(()) | |
| 113 | − }); | |
| 114 | − | |
| 115 | − let mut shared_entries = std::mem::take(&mut shared_index.entries); | |
| 116 | − shared_entries.sort_by(|a, b| a.cmp(b, &shared_index.state)); | |
| 117 | − | |
| 118 | − std::mem::swap(&mut split_index.entries, &mut shared_entries); | |
| 119 | − std::mem::swap(&mut split_index.path_backing, &mut shared_index.path_backing); | |
| 120 | − } | |
| 121 | − | |
| 122 | − Ok(()) | |
| 123 | −} | |
| @@ -0,0 +1,19 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +set -eu -o pipefail | |
| 3 | + | |
| 4 | +export GIT_INDEX_VERSION=2 | |
| 5 | +git init -q | |
| 6 | +git config index.threads 1 | |
| 7 | + | |
| 8 | +touch a | |
| 9 | +git add a | |
| 10 | +git commit -m "empty" | |
| 11 | + | |
| 12 | +git update-index --split-index | |
| 13 | + | |
| 14 | +shared_index=".git/sharedindex.*" | |
| 15 | +test -f $shared_index || \ | |
| 16 | + { echo "shared index must be present in 'split' repository" && exit 42; } | |
| 17 | + | |
| 18 | +cp .git/index $shared_index | |
| 19 | + | |
| @@ -23,14 +23,16 @@ pub(crate) fn loose_file(name: &str) -> git_index::File { · 22 unmodified lines | ||
| 23 | 23 | let file = git_index::File::at(path, git_hash::Kind::Sha1, Default::default()).unwrap(); |
| 24 | 24 | verify(file) |
| 25 | 25 | } |
| 26 | −pub(crate) fn file(name: &str) -> git_index::File { | |
| 26 | +pub(crate) fn try_file(name: &str) -> Result<git_index::File, git_index::file::init::Error> { | |
| 27 | 27 | let file = git_index::File::at( |
| 28 | 28 | crate::fixture_index_path(name), |
| 29 | 29 | git_hash::Kind::Sha1, |
| 30 | 30 | Default::default(), |
| 31 | − ) | |
| 32 | − .unwrap(); | |
| 33 | − verify(file) | |
| 31 | + )?; | |
| 32 | + Ok(verify(file)) | |
| 33 | +} | |
| 34 | +pub(crate) fn file(name: &str) -> git_index::File { | |
| 35 | + try_file(name).unwrap() | |
| 34 | 36 | } |
| 35 | 37 | fn file_opt(name: &str, opts: git_index::decode::Options) -> git_index::File { |
| 36 | 38 | let file = git_index::File::at(crate::fixture_index_path(name), git_hash::Kind::Sha1, opts).unwrap(); |
| @@ -290,6 +292,15 @@ fn v2_split_index() { · 253 unmodified lines | ||
| 290 | 292 | assert_eq!(file.version(), Version::V2); |
| 291 | 293 | } |
| 292 | 294 | |
| 295 | +#[test] | |
| 296 | +fn v2_split_index_recursion_is_handled_gracefully() { | |
| 297 | + let err = try_file("v2_split_index_recursive").expect_err("recursion fails gracefully"); | |
| 298 | + assert!(matches!( | |
| 299 | + err, | |
| 300 | + git_index::file::init::Error::Decode(git_index::decode::Error::ChecksumMismatch { .. }) | |
| 301 | + )); | |
| 302 | +} | |
| 303 | + | |
| 293 | 304 | #[test] |
| 294 | 305 | fn split_index_and_regular_index_of_same_content_are_indeed_the_same() { |
| 295 | 306 | let base = |