Skip to main content

git_zero_server/receive/
gitmodules.rs

1//! Allocation-free, streamed config policy for a designated gitmodules blob.
2use git_zero_core::{Error, Result};
3mod names;
4mod url;
5use names::{Field, Key, Section};
6use url::Url;
7
8#[derive(Clone, Copy)]
9enum Phase {
10    Main,
11    Comment,
12    Base,
13    SectionSpace,
14    Name,
15    NameEscape,
16    SectionEnd,
17    Key,
18    KeySpace,
19    Value,
20    Halted,
21}
22enum ValueStep {
23    More,
24    Complete,
25    Malformed,
26}
27
28/// Validate the settings that strict receive fsck checks in gitmodules.
29/// Config syntax errors are warning-only and stop parsing subsequent entries;
30/// an unsafe setting parsed before that error remains fatal. Names, values and
31/// URL paths are reduced to bounded properties rather than retained strings.
32pub struct FsckModules {
33    remaining: u64,
34    normalized: u64,
35    pending_cr: bool,
36    eof_seen: bool,
37    limited: bool,
38    phase: Phase,
39    base_seen: bool,
40    section: Section,
41    key: Key,
42    value: Value,
43    done: bool,
44    failure: Option<Error>,
45}
46impl FsckModules {
47    /// Match strict receive's buffer policy for a newly packed plain blob.
48    /// Native Git streams plain blobs above its default 512 MiB threshold
49    /// without retaining a config buffer and reports gitmodulesLarge. Delta
50    /// results and blobs read from earlier packs still provide their contents.
51    pub fn new_received(size: u64, plain: bool) -> Result<Self> {
52        if plain && size > 512 * 1024 * 1024 {
53            return Err(Error::InvalidObject);
54        }
55        Ok(Self::new(size))
56    }
57
58    pub fn new(size: u64) -> Self {
59        Self {
60            remaining: size,
61            normalized: 0,
62            pending_cr: false,
63            eof_seen: false,
64            limited: false,
65            phase: Phase::Main,
66            base_seen: false,
67            section: Section::new(),
68            key: Key::default(),
69            value: Value::new(None),
70            done: false,
71            failure: None,
72        }
73    }
74    pub fn update(&mut self, bytes: &[u8]) -> Result<()> {
75        if let Some(error) = self.failure {
76            return Err(error);
77        }
78        let result = self.feed(bytes);
79        if let Err(error) = result {
80            self.failure = Some(error);
81        }
82        result
83    }
84    fn feed(&mut self, bytes: &[u8]) -> Result<()> {
85        self.remaining = self
86            .remaining
87            .checked_sub(bytes.len() as u64)
88            .ok_or(Error::InvalidObject)?;
89        for &byte in bytes {
90            if self.limited || matches!(self.phase, Phase::Halted) {
91                break;
92            }
93            if self.pending_cr {
94                self.pending_cr = false;
95                if byte == b'\n' {
96                    self.character(b'\n')?;
97                    continue;
98                }
99                self.character(b'\r')?;
100                if byte == 0xff {
101                    continue;
102                }
103            }
104            if byte == b'\r' {
105                self.pending_cr = true;
106            } else {
107                self.character(byte)?;
108            }
109        }
110        Ok(())
111    }
112    fn character(&mut self, byte: u8) -> Result<()> {
113        if self.limited {
114            return Ok(());
115        }
116        // Match Git's memory-backed config source: on the reference targets
117        // 0xff is returned as EOF, and non-ASCII leading bytes (including a
118        // UTF-8 BOM) are syntax warnings rather than file-style BOM handling.
119        if byte == 0xff {
120            self.eof_seen = true;
121            return self.consume(b'\n');
122        }
123        self.normalized += 1;
124        // Native config supplies NUL after INT_MAX until EOF. An unquoted
125        // value still invokes its callback with the visible prefix; discarding
126        // it here could suppress an unsafe setting parsed before the limit.
127        if self.normalized > i32::MAX as u64 {
128            self.limited = true;
129            self.eof_seen = true;
130            if matches!(self.phase, Phase::Value) {
131                self.consume(0)?;
132            } else {
133                self.phase = Phase::Halted;
134            }
135            return Ok(());
136        }
137        self.consume(byte)
138    }
139    fn boolean(&mut self) -> Result<()> {
140        if self.section.entry(self.key).is_some_and(|(bad, _)| bad) {
141            return Err(Error::InvalidObject);
142        }
143        self.phase = Phase::Main;
144        Ok(())
145    }
146    fn start_value(&mut self) {
147        self.value = Value::new(self.section.entry(self.key));
148        self.phase = Phase::Value;
149    }
150    fn consume(&mut self, byte: u8) -> Result<()> {
151        match self.phase {
152            Phase::Main => self.main(byte),
153            Phase::Comment => {
154                if byte == b'\n' {
155                    self.phase = Phase::Main;
156                }
157            }
158            Phase::Base => self.base(byte),
159            Phase::SectionSpace => self.section_space(byte),
160            Phase::Name => match byte {
161                b'\n' => self.phase = Phase::Halted,
162                b'\\' => self.phase = Phase::NameEscape,
163                b'"' => self.phase = Phase::SectionEnd,
164                _ => self.section.name_byte(byte),
165            },
166            Phase::NameEscape => {
167                if byte == b'\n' {
168                    self.phase = Phase::Halted;
169                } else {
170                    self.section.name_byte(byte);
171                    self.phase = Phase::Name;
172                }
173            }
174            Phase::SectionEnd => {
175                self.phase = if byte == b']' {
176                    Phase::Main
177                } else {
178                    Phase::Halted
179                }
180            }
181            Phase::Key | Phase::KeySpace => self.key(byte)?,
182            Phase::Value => self.value(byte)?,
183            Phase::Halted => {}
184        }
185        Ok(())
186    }
187    fn main(&mut self, byte: u8) {
188        match byte {
189            b'\n' if self.eof_seen => self.phase = Phase::Halted,
190            b'#' | b';' => self.phase = Phase::Comment,
191            b'[' => {
192                self.section = Section::new();
193                self.base_seen = false;
194                self.phase = Phase::Base;
195            }
196            _ if space(byte) => {}
197            _ if byte.is_ascii_alphabetic() => {
198                self.key = Key::default();
199                self.key.push(byte.to_ascii_lowercase());
200                self.phase = Phase::Key;
201            }
202            _ => self.phase = Phase::Halted,
203        }
204    }
205    fn base(&mut self, byte: u8) {
206        if self.eof_seen {
207            self.phase = Phase::Halted;
208            return;
209        }
210        match byte {
211            b']' if self.base_seen => self.phase = Phase::Main,
212            b' ' | b'\t' | b'\r' => self.phase = Phase::SectionSpace,
213            _ if key_char(byte) || byte == b'.' => {
214                self.base_seen = true;
215                self.section.base(byte);
216            }
217            _ => self.phase = Phase::Halted,
218        }
219    }
220    fn section_space(&mut self, byte: u8) {
221        match byte {
222            b' ' | b'\t' | b'\r' => {}
223            b'"' if self.base_seen => {
224                self.section.extended();
225                self.phase = Phase::Name;
226            }
227            _ => self.phase = Phase::Halted,
228        }
229    }
230    fn key(&mut self, byte: u8) -> Result<()> {
231        if matches!(self.phase, Phase::Key) && key_char(byte) && !self.eof_seen {
232            self.key.push(byte.to_ascii_lowercase());
233            return Ok(());
234        }
235        match byte {
236            b' ' | b'\t' => self.phase = Phase::KeySpace,
237            b'=' => self.start_value(),
238            b'\n' => self.boolean()?,
239            _ => self.phase = Phase::Halted,
240        }
241        Ok(())
242    }
243    fn value(&mut self, byte: u8) -> Result<()> {
244        match self.value.push(byte) {
245            ValueStep::More => {}
246            ValueStep::Malformed => self.phase = Phase::Halted,
247            ValueStep::Complete => {
248                if self.value.invalid() {
249                    return Err(Error::InvalidObject);
250                }
251                self.phase = Phase::Main;
252            }
253        }
254        Ok(())
255    }
256    pub fn finish(&mut self) -> Result<()> {
257        if let Some(error) = self.failure {
258            return Err(error);
259        }
260        if self.done {
261            return Ok(());
262        }
263        let result = self.end();
264        if let Err(error) = result {
265            self.failure = Some(error);
266        }
267        result
268    }
269    fn end(&mut self) -> Result<()> {
270        if self.remaining != 0 {
271            return Err(Error::InvalidObject);
272        }
273        if self.pending_cr {
274            self.pending_cr = false;
275            self.character(b'\r')?;
276        }
277        // Native config EOF supplies a newline; a final backslash can consume
278        // it as a continuation and require the next EOF newline to finish.
279        self.consume(b'\n')?;
280        self.consume(b'\n')?;
281        self.done = true;
282        Ok(())
283    }
284}
285fn key_char(byte: u8) -> bool {
286    byte.is_ascii_alphanumeric() || byte == b'-'
287}
288fn space(byte: u8) -> bool {
289    matches!(byte, b' ' | b'\t' | b'\n' | b'\r')
290}
291
292struct Value {
293    field: Field,
294    bad_name: bool,
295    bad: bool,
296    actual: bool,
297    pending_space: bool,
298    quote: bool,
299    escape: bool,
300    comment: bool,
301    url: Url,
302}
303impl Value {
304    fn new(entry: Option<(bool, Field)>) -> Self {
305        let (bad_name, field) = entry.unwrap_or((false, Field::Other));
306        Self {
307            field,
308            bad_name,
309            bad: false,
310            actual: false,
311            pending_space: false,
312            quote: false,
313            escape: false,
314            comment: false,
315            url: Url::new(),
316        }
317    }
318    fn emit(&mut self, byte: u8) {
319        self.actual = true;
320        if self.field == Field::Other {
321            return;
322        }
323        if byte == 0 {
324            self.bad |= self.field == Field::Url && self.url.invalid();
325            self.field = Field::Other;
326            return;
327        }
328        match self.field {
329            Field::Path => {
330                self.bad |= byte == b'-';
331                self.field = Field::Other;
332            }
333            Field::Update => {
334                self.bad |= byte == b'!';
335                self.field = Field::Other;
336            }
337            Field::Url => {
338                self.url.push(byte);
339                if let Url::Other(bad) = self.url {
340                    self.bad |= bad;
341                    self.field = Field::Other;
342                }
343            }
344            Field::Other => {}
345        }
346    }
347    fn flush_space(&mut self) {
348        // For these policies all non-newline whitespace has the same effect:
349        // it breaks URL tokens/hostnames, or makes a non-dot path segment.
350        if self.pending_space && self.actual {
351            self.emit(b' ');
352        }
353        self.pending_space = false;
354    }
355    fn push(&mut self, byte: u8) -> ValueStep {
356        if self.comment {
357            return if byte == b'\n' {
358                ValueStep::Complete
359            } else {
360                ValueStep::More
361            };
362        }
363        if self.escape {
364            self.escape = false;
365            let decoded = match byte {
366                b'\n' => return ValueStep::More,
367                b't' => b'\t',
368                b'b' => 8,
369                b'n' => b'\n',
370                b'\\' | b'"' => byte,
371                _ => return ValueStep::Malformed,
372            };
373            self.emit(decoded);
374            return ValueStep::More;
375        }
376        if byte == b'\n' {
377            return if self.quote {
378                ValueStep::Malformed
379            } else {
380                ValueStep::Complete
381            };
382        }
383        if !self.quote && space(byte) {
384            self.pending_space = true;
385            return ValueStep::More;
386        }
387        if !self.quote && matches!(byte, b'#' | b';') {
388            self.comment = true;
389            return ValueStep::More;
390        }
391        self.flush_space();
392        match byte {
393            b'\\' => self.escape = true,
394            b'"' => self.quote = !self.quote,
395            _ => self.emit(byte),
396        }
397        ValueStep::More
398    }
399    fn invalid(&self) -> bool {
400        self.bad_name || self.bad || (self.field == Field::Url && self.url.invalid())
401    }
402}