Skip to content

Commit dc536ad

Browse files
committed
[commitgraph] Clean up {file,graph}::verify::Error types.
Get rid of `file::verify::EitherError` in favor of having `graph::verify::Error` manually copy file errors into the graph error object. So: 1. `graph::verify::Error`'s `File` variant uses a dummy type parameter for its nested `file::verify::Error` value. This dummy type parameter is essentially a never type, as `graph::verify::Error::File(file::verify::Error::Processor(...))` is not valid. 2. `Graph::verify_integrity` calls `File::traverse` with its normal error type (`graph::verify::Error`) also serving as the processor's error type. 3. `Graph::traverse` propagates processor errors to its caller via `Error::Processor(err)`. 4. `Graph::verify_integrity` manually transcribes `file::verify::Error<T>` variants into `file::verify::Error<NeverType>` variants before embedding the file error into `graph::verify::Error::File` variants.
1 parent 8dad3ff commit dc536ad

File tree

3 files changed

+64
-44
lines changed

3 files changed

+64
-44
lines changed

git-commitgraph/src/file/verify.rs

+17-37
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::convert::TryFrom;
1010
use std::path::Path;
1111

1212
#[derive(thiserror::Error, Debug)]
13-
pub enum Error {
13+
pub enum Error<E: std::error::Error + 'static> {
1414
#[error(transparent)]
1515
Commit(#[from] file::commit::Error),
1616
#[error("commit at file position {pos} has invalid ID {id}")]
@@ -26,24 +26,13 @@ pub enum Error {
2626
#[error("commit {id} has invalid generation {generation}")]
2727
Generation { generation: u32, id: owned::Id },
2828
#[error("checksum mismatch: expected {expected}, got {actual}")]
29-
Mismatch { expected: owned::Id, actual: owned::Id },
29+
Mismatch { actual: owned::Id, expected: owned::Id },
30+
#[error("{0}")]
31+
Processor(#[source] E),
3032
#[error("commit {id} has invalid root tree ID {root_tree_id}")]
3133
RootTreeId { id: owned::Id, root_tree_id: owned::Id },
3234
}
3335

34-
// This is a separate type to let `traverse`'s caller use the same error type for its result and its
35-
// processor error type while also letting that error type contain file::verify::Error values.
36-
// Is there a better way? Should the caller's error type just use boxes to avoid recursive type
37-
// errors?
38-
#[derive(thiserror::Error, Debug)]
39-
pub enum EitherError<E1: std::error::Error + 'static, E2: std::error::Error + 'static> {
40-
#[error(transparent)]
41-
Internal(#[from] E1),
42-
// Why can't I use #[from] here? Boo!
43-
#[error("{0}")]
44-
Processor(#[source] E2),
45-
}
46-
4736
#[derive(Clone, Debug, Eq, PartialEq)]
4837
#[cfg_attr(feature = "serde1", derive(serde::Deserialize, serde::Serialize))]
4938
pub struct Outcome {
@@ -59,13 +48,14 @@ impl File {
5948
borrowed::Id::try_from(&self.data[self.data.len() - SHA1_SIZE..]).expect("file to be large enough for a hash")
6049
}
6150

62-
pub fn traverse<'a, E, Processor>(&'a self, mut processor: Processor) -> Result<Outcome, EitherError<Error, E>>
51+
pub fn traverse<'a, E, Processor>(&'a self, mut processor: Processor) -> Result<Outcome, Error<E>>
6352
where
6453
E: std::error::Error + 'static,
6554
Processor: FnMut(&file::Commit<'a>) -> Result<(), E>,
6655
{
67-
self.verify_checksum()?;
68-
verify_split_chain_filename_hash(&self.path, self.checksum())?;
56+
self.verify_checksum()
57+
.map_err(|(actual, expected)| Error::Mismatch { actual, expected })?;
58+
verify_split_chain_filename_hash(&self.path, self.checksum()).map_err(Error::Filename)?;
6959

7060
// This probably belongs in borrowed::Id itself?
7161
let null_id = borrowed::Id::from(&[0u8; SHA1_SIZE]);
@@ -86,32 +76,28 @@ impl File {
8676
return Err(Error::CommitId {
8777
pos: commit.position(),
8878
id: commit.id().into(),
89-
}
90-
.into());
79+
});
9180
}
9281
return Err(Error::CommitsOutOfOrder {
9382
pos: commit.position(),
9483
id: commit.id().into(),
9584
predecessor_id: prev_id.into(),
96-
}
97-
.into());
85+
});
9886
}
9987
if commit.root_tree_id() == null_id {
10088
return Err(Error::RootTreeId {
10189
id: commit.id().into(),
10290
root_tree_id: commit.root_tree_id().into(),
103-
}
104-
.into());
91+
});
10592
}
10693
if commit.generation() > GENERATION_NUMBER_MAX {
10794
return Err(Error::Generation {
10895
generation: commit.generation(),
10996
id: commit.id().into(),
110-
}
111-
.into());
97+
});
11298
}
11399

114-
processor(&commit).map_err(EitherError::Processor)?;
100+
processor(&commit).map_err(Error::Processor)?;
115101

116102
stats.max_generation = max(stats.max_generation, commit.generation());
117103
stats.min_generation = min(stats.min_generation, commit.generation());
@@ -130,7 +116,7 @@ impl File {
130116
Ok(stats)
131117
}
132118

133-
pub fn verify_checksum(&self) -> Result<owned::Id, Error> {
119+
pub fn verify_checksum(&self) -> Result<owned::Id, (owned::Id, owned::Id)> {
134120
// TODO: Use/copy git_odb::hash::bytes_of_file.
135121
let data_len_without_trailer = self.data.len() - SHA1_SIZE;
136122
let mut hasher = git_features::hash::Sha1::default();
@@ -141,27 +127,21 @@ impl File {
141127
if actual.to_borrowed() == expected {
142128
Ok(actual)
143129
} else {
144-
Err(Error::Mismatch {
145-
actual,
146-
expected: expected.into(),
147-
})
130+
Err((actual, expected.into()))
148131
}
149132
}
150133
}
151134

152135
/// If the given path's filename matches "graph-{hash}.graph", check that `hash` matches the
153136
/// expected hash.
154-
fn verify_split_chain_filename_hash(path: impl AsRef<Path>, expected: borrowed::Id<'_>) -> Result<(), Error> {
137+
fn verify_split_chain_filename_hash(path: impl AsRef<Path>, expected: borrowed::Id<'_>) -> Result<(), String> {
155138
let path = path.as_ref();
156139
path.file_name()
157140
.and_then(|filename| filename.to_str())
158141
.and_then(|filename| filename.strip_suffix(".graph"))
159142
.and_then(|stem| stem.strip_prefix("graph-"))
160143
.map_or(Ok(()), |hex| match owned::Id::from_40_bytes_in_hex(hex.as_bytes()) {
161144
Ok(actual) if actual.to_borrowed() == expected => Ok(()),
162-
_ => Err(Error::Filename(format!(
163-
"graph-{}.graph",
164-
expected.to_sha1_hex().as_bstr()
165-
))),
145+
_ => Err(format!("graph-{}.graph", expected.to_sha1_hex().as_bstr())),
166146
})
167147
}

git-commitgraph/src/graph/verify.rs

+35-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
22
file::{self, commit},
3-
graph, Graph, GENERATION_NUMBER_MAX,
3+
graph, Graph, ImpossibleVariantError, GENERATION_NUMBER_MAX,
44
};
55
use git_object::owned;
66
use std::cmp::{max, min};
@@ -22,7 +22,15 @@ pub enum Error<E: std::error::Error + 'static> {
2222
#[error(transparent)]
2323
Commit(#[from] commit::Error),
2424
#[error("{}: {err}", .path.display())]
25-
File { err: file::verify::Error, path: PathBuf },
25+
File {
26+
// Use zero-size error type. We will never return
27+
// `graph::verify::Error::File(file::verify::Error::Processor(...))`, because we are the
28+
// file's processor, and we convert`file::verify::Error::Processor<graph::verify::Error>`
29+
// variants into direct `graph::verify::Error` values.
30+
// TODO: Use never type when it becomes available.
31+
err: file::verify::Error<ImpossibleVariantError>,
32+
path: PathBuf,
33+
},
2634
#[error("Commit {id}'s generation should be {expected} but is {actual}")]
2735
Generation { actual: u32, expected: u32, id: owned::Id },
2836
#[error(
@@ -132,12 +140,32 @@ impl Graph {
132140

133141
Ok(())
134142
})
135-
.map_err(|e| match e {
136-
file::verify::EitherError::Internal(err) => Error::File {
137-
err,
138-
path: file.path().to_owned(),
143+
.map_err(|err| Error::File {
144+
err: match err {
145+
file::verify::Error::Processor(e) => return e,
146+
file::verify::Error::RootTreeId { id, root_tree_id } => {
147+
file::verify::Error::RootTreeId { id, root_tree_id }
148+
}
149+
file::verify::Error::Mismatch { actual, expected } => {
150+
file::verify::Error::Mismatch { actual, expected }
151+
}
152+
file::verify::Error::Generation { generation, id } => {
153+
file::verify::Error::Generation { generation, id }
154+
}
155+
file::verify::Error::Filename(expected) => file::verify::Error::Filename(expected),
156+
file::verify::Error::Commit(err) => file::verify::Error::Commit(err),
157+
file::verify::Error::CommitId { id, pos } => file::verify::Error::CommitId { id, pos },
158+
file::verify::Error::CommitsOutOfOrder {
159+
id,
160+
pos,
161+
predecessor_id,
162+
} => file::verify::Error::CommitsOutOfOrder {
163+
id,
164+
pos,
165+
predecessor_id,
166+
},
139167
},
140-
file::verify::EitherError::Processor(e) => e,
168+
path: file.path().to_owned(),
141169
})?;
142170

143171
max_generation = max(max_generation, file_stats.max_generation);

git-commitgraph/src/lib.rs

+12
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,15 @@ pub const GENERATION_NUMBER_MAX: u32 = 0x3fff_ffff;
1111

1212
/// The maximum number of commits that can be stored in a commit graph.
1313
pub const MAX_COMMITS: u32 = (1 << 30) + (1 << 29) + (1 << 28) - 1;
14+
15+
// TODO: pub type ImpossibleVariantError = !;
16+
#[derive(Debug)]
17+
pub struct ImpossibleVariantError;
18+
19+
impl std::error::Error for ImpossibleVariantError {}
20+
21+
impl std::fmt::Display for ImpossibleVariantError {
22+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23+
f.write_str("this enum was constructed with an invalid variant")
24+
}
25+
}

0 commit comments

Comments
 (0)