git_zero_server/receive/
tree.rs1use git_zero_core::{Error, Result};
3
4#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6pub struct BlobChecks(u8);
7impl BlobChecks {
8 pub const fn bits(self) -> u8 {
10 self.0
11 }
12}
13
14pub fn check_tree_entry(name: &[u8], mode: u32, zero_padded: bool) -> Result<BlobChecks> {
18 if name.len() > 4096 {
19 return Err(Error::InvalidFileName);
20 }
21 if zero_padded {
22 return Err(Error::InvalidObject);
23 }
24 let symlink = mode & 0o170000 == 0o120000;
25 let blob = mode & 0o170000 == 0o100000;
26 let mut modules = hfs(name, b"gitmodules");
27 if hfs(name, b"git") {
28 return Err(Error::InvalidFileName);
29 }
30 let mut part = name;
32 loop {
33 if ntfs_git(part) {
34 return Err(Error::InvalidFileName);
35 }
36 modules |= ntfs_special(part, b"gitmodules", b"gi7eba");
37 match part.iter().position(|&b| b == b'\\') {
38 Some(end) => part = &part[end + 1..],
39 None => break,
40 }
41 }
42 let attributes = !symlink
43 && (hfs(name, b"gitattributes") || ntfs_special(name, b"gitattributes", b"gi7d29"));
44 if (modules || attributes) && !blob {
45 return Err(Error::InvalidObject);
46 }
47 Ok(BlobChecks(u8::from(modules) | (u8::from(attributes) << 1)))
48}
49
50fn ntfs_git(name: &[u8]) -> bool {
51 let rest = if name
52 .get(..4)
53 .is_some_and(|s| s.eq_ignore_ascii_case(b".git"))
54 {
55 &name[4..]
56 } else if name
57 .get(..5)
58 .is_some_and(|s| s.eq_ignore_ascii_case(b"git~1"))
59 {
60 &name[5..]
61 } else {
62 return false;
63 };
64 for &byte in rest {
65 if matches!(byte, b'/' | b'\\' | b':') {
66 return true;
67 }
68 if byte != b'.' && byte != b' ' {
69 return false;
70 }
71 }
72 true
73}
74
75fn ntfs_special(name: &[u8], base: &[u8], fallback: &[u8; 6]) -> bool {
76 let tail = if name.first() == Some(&b'.')
77 && name
78 .get(1..base.len() + 1)
79 .is_some_and(|s| s.eq_ignore_ascii_case(base))
80 {
81 base.len() + 1
82 } else if name
83 .get(..6)
84 .is_some_and(|s| s.eq_ignore_ascii_case(&base[..6]))
85 && name.get(6) == Some(&b'~')
86 && name.get(7).is_some_and(|b| matches!(b, b'1'..=b'4'))
87 {
88 8
89 } else {
90 if name.len() < 8 {
91 return false;
92 }
93 let mut digits = false;
94 let mut index = 0;
95 while index < 8 {
96 let byte = name[index];
97 if digits {
98 if !byte.is_ascii_digit() {
99 return false;
100 }
101 } else if byte == b'~' {
102 index += 1;
103 if index >= 8 || !matches!(name[index], b'1'..=b'9') {
104 return false;
105 }
106 digits = true;
107 } else if index >= 6 || byte.to_ascii_lowercase() != fallback[index] {
108 return false;
109 }
110 index += 1;
111 }
112 8
113 };
114 for &byte in &name[tail..] {
115 if byte == b':' {
116 return true;
117 }
118 if byte != b' ' && byte != b'.' {
119 return false;
120 }
121 }
122 true
123}
124
125fn hfs(mut name: &[u8], base: &[u8]) -> bool {
126 if next_hfs(&mut name) != u32::from(b'.') {
127 return false;
128 }
129 for &byte in base {
130 let c = next_hfs(&mut name);
131 if c > 127 || (c as u8).to_ascii_lowercase() != byte {
132 return false;
133 }
134 }
135 matches!(next_hfs(&mut name), 0 | 47)
136}
137
138fn next_hfs(name: &mut &[u8]) -> u32 {
139 loop {
140 let Some(&first) = name.first() else {
141 return 0;
142 };
143 let width = match first {
144 0..=127 => 1,
145 0xc2..=0xdf => 2,
146 0xe0..=0xef => 3,
147 0xf0..=0xf4 => 4,
148 _ => return 0,
149 };
150 let Some(bytes) = name.get(..width) else {
151 return 0;
152 };
153 let Ok(text) = core::str::from_utf8(bytes) else {
154 return 0;
155 };
156 let Some(c) = text.chars().next() else {
157 return 0;
158 };
159 *name = &name[width..];
160 let value = u32::from(c);
161 if !matches!(value, 0x200c..=0x200f | 0x202a..=0x202e | 0x206a..=0x206f | 0xfeff) {
162 return value;
163 }
164 }
165}