1use crate::object::{ReservedMetadataName, reserved_tree_entry_name, validate_tree_entry_name};
5
6#[derive(Clone, Debug, Eq, PartialEq)]
7pub enum GitTreeNameClassification {
8 Representable(String),
9 NeedsLossy(GitTreeNameLossy),
10}
11
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct GitTreeNameLossy {
14 pub name: String,
15 pub action: GitTreeNameLossyAction,
16 pub reason: &'static str,
17}
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum GitTreeNameLossyAction {
21 Dropped,
22 Converted,
23}
24
25pub fn classify_git_tree_name(
33 raw_name: &[u8],
34 at_root: bool,
35 symlink: bool,
36) -> Result<GitTreeNameClassification, ReservedMetadataName> {
37 match reserved_tree_entry_name(raw_name, at_root, symlink) {
40 Some(reason) => Err(reason),
41 None => Ok(classify_git_tree_name_representable(raw_name)),
42 }
43}
44
45pub fn classify_git_tree_name_representable(raw_name: &[u8]) -> GitTreeNameClassification {
49 let (name, utf8_lossy) = match std::str::from_utf8(raw_name) {
50 Ok(name) => (name.to_string(), false),
51 Err(_) => (String::from_utf8_lossy(raw_name).into_owned(), true),
52 };
53
54 match (validate_tree_entry_name(&name), utf8_lossy) {
62 (Ok(()), false) => GitTreeNameClassification::Representable(name),
63 (Ok(()), true) => GitTreeNameClassification::NeedsLossy(GitTreeNameLossy {
64 name,
65 action: GitTreeNameLossyAction::Converted,
66 reason: "tree entry name is not valid UTF-8 and was converted with replacement characters",
67 }),
68 (Err(_), _) => GitTreeNameClassification::NeedsLossy(GitTreeNameLossy {
69 name,
70 action: GitTreeNameLossyAction::Dropped,
71 reason: "tree entry name is not representable in Heddle",
72 }),
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
84 fn representable_iff_validator_accepts() {
85 let cases = [
86 "ok.txt",
87 "with space",
88 "ünïcödé",
89 "",
90 ".",
91 "..",
92 "a/b",
93 "a\\b",
94 "ctrl\u{0001}",
95 "del\u{7f}",
96 ".git",
97 ".heddle",
98 ];
99 for c in cases {
100 let classified_representable = matches!(
101 classify_git_tree_name_representable(c.as_bytes()),
102 GitTreeNameClassification::Representable(_)
103 );
104 let validator_accepts = validate_tree_entry_name(c).is_ok();
105 assert_eq!(
106 classified_representable, validator_accepts,
107 "classifier/validator disagree on {c:?}"
108 );
109 }
110 }
111
112 #[test]
113 fn backslash_name_is_not_representable() {
114 assert!(matches!(
115 classify_git_tree_name_representable(b"foo\\bar"),
116 GitTreeNameClassification::NeedsLossy(_)
117 ));
118 }
119
120 #[test]
121 fn invalid_utf8_is_converted_not_dropped() {
122 match classify_git_tree_name_representable(&[b'a', 0xff, b'b']) {
123 GitTreeNameClassification::NeedsLossy(lossy) => {
124 assert_eq!(lossy.action, GitTreeNameLossyAction::Converted);
125 }
126 other => panic!("expected NeedsLossy/Converted, got {other:?}"),
127 }
128 }
129
130 #[test]
131 fn invalid_utf8_that_stays_unrepresentable_after_conversion_is_dropped() {
132 match classify_git_tree_name_representable(b"bad\\\xff") {
137 GitTreeNameClassification::NeedsLossy(lossy) => {
138 assert_eq!(lossy.action, GitTreeNameLossyAction::Dropped);
139 }
140 other => panic!("expected NeedsLossy/Dropped, got {other:?}"),
141 }
142 }
143
144 #[test]
145 fn reserved_names_fail_rather_than_go_lossy() {
146 let cases: [(&[u8], bool, bool); 10] = [
147 (b".git", false, false),
148 (b".GIT", false, false),
149 (b".git::$INDEX_ALLOCATION", false, false),
150 (b"git~1", false, false),
151 (b".git\\hooks", false, false),
152 (b".git\xff", false, false),
153 (b".heddle", true, false),
154 (b".HEDDLE", true, false),
155 (b".gitmodules", false, true),
156 (b"GITMOD~1", false, true),
157 ];
158 for (raw, at_root, symlink) in cases {
159 assert!(
160 classify_git_tree_name(raw, at_root, symlink).is_err(),
161 "{raw:?}"
162 );
163 }
164 assert_eq!(
166 classify_git_tree_name(b".heddle", false, false),
167 Ok(GitTreeNameClassification::Representable(
168 ".heddle".to_string()
169 ))
170 );
171 assert_eq!(
173 classify_git_tree_name(b".gitmodules", true, false),
174 Ok(GitTreeNameClassification::Representable(
175 ".gitmodules".to_string()
176 ))
177 );
178 }
179}