Skip to main content

git_zero_server/receive/
tree.rs

1//! Strict receive tree policy, including platform aliases of special files.
2use git_zero_core::{Error, Result};
3
4/// Additional policies to apply to the designated blob after resolving its ID.
5#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6pub struct BlobChecks(u8);
7impl BlobChecks {
8    /// Bit 0 selects gitmodules config checks, bit 1 attributes size/line checks.
9    pub const fn bits(self) -> u8 {
10        self.0
11    }
12}
13
14/// Add strict receive tree policy after structural link validation. Historical regular-file
15/// permissions and attributes symlinks remain warnings. Returned checks apply
16/// even when the target blob was published by an earlier request.
17pub 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    // Windows path components after a backslash are checked by native fsck too.
31    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}