Skip to main content

grit_lib/
git_binary_base85.rs

1//! Base-85 codec for `GIT binary patch` sections (matches `git/base85.c`).
2
3use thiserror::Error;
4
5const EN85: &[u8; 85] =
6    b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~";
7
8/// Errors returned while decoding a Git binary-patch base85 line.
9#[derive(Clone, Debug, Error, PartialEq, Eq)]
10pub enum DecodeError {
11    /// The encoded line ended before all required base85 digits were read.
12    #[error("truncated base85 line")]
13    TruncatedLine,
14    /// The encoded line contained a byte outside Git's base85 alphabet.
15    #[error("invalid base85 alphabet byte {0}")]
16    InvalidAlphabetByte(u8),
17    /// The decoded base85 accumulator overflowed `u32`.
18    #[error("base85 overflow")]
19    Overflow,
20    /// The encoded line contained extra data after the expected output length.
21    #[error("trailing base85 data")]
22    TrailingData,
23}
24
25fn prep_decode_table() -> [u8; 256] {
26    let mut de85 = [0u8; 256];
27    for (i, &c) in EN85.iter().enumerate() {
28        de85[c as usize] = (i + 1) as u8;
29    }
30    de85
31}
32
33/// Encode bytes using Git's base-85 alphabet (matches `encode_85` in `git/base85.c`).
34#[must_use]
35pub fn encode(mut data: &[u8]) -> String {
36    let mut result = String::new();
37    while !data.is_empty() {
38        let mut acc: u32 = 0;
39        let mut cnt = 24i32;
40        while cnt >= 0 {
41            let ch = u32::from(data[0]);
42            data = &data[1..];
43            acc |= ch << cnt;
44            if data.is_empty() {
45                break;
46            }
47            cnt -= 8;
48        }
49        let mut buf = [0u8; 5];
50        for i in (0..5).rev() {
51            let val = (acc % 85) as usize;
52            acc /= 85;
53            buf[i] = EN85[val];
54        }
55        result.extend(buf.iter().map(|b| char::from(*b)));
56    }
57    result
58}
59
60/// Decode the base-85 body of one binary patch line.
61///
62/// `out_len` is the number of raw (compressed) bytes this line contributes.
63pub fn decode_body(buffer: &[u8], mut out_len: usize) -> Result<Vec<u8>, DecodeError> {
64    static DE85: std::sync::OnceLock<[u8; 256]> = std::sync::OnceLock::new();
65    let de85 = DE85.get_or_init(prep_decode_table);
66
67    let mut dst = Vec::with_capacity(out_len);
68    let mut pos = 0usize;
69
70    while out_len > 0 {
71        let mut acc: u32 = 0;
72        for _ in 0..4 {
73            let ch = *buffer.get(pos).ok_or(DecodeError::TruncatedLine)?;
74            pos += 1;
75            let de = de85[ch as usize];
76            if de == 0 {
77                return Err(DecodeError::InvalidAlphabetByte(ch));
78            }
79            acc = acc
80                .checked_mul(85)
81                .and_then(|a| a.checked_add(u32::from(de - 1)))
82                .ok_or(DecodeError::Overflow)?;
83        }
84        let ch = *buffer.get(pos).ok_or(DecodeError::TruncatedLine)?;
85        pos += 1;
86        let de = de85[ch as usize];
87        if de == 0 {
88            return Err(DecodeError::InvalidAlphabetByte(ch));
89        }
90        acc = acc
91            .checked_mul(85)
92            .and_then(|a| a.checked_add(u32::from(de - 1)))
93            .ok_or(DecodeError::Overflow)?;
94
95        let chunk = out_len.min(4);
96        out_len -= chunk;
97        let mut a = acc;
98        for _ in 0..chunk {
99            a = a.rotate_left(8);
100            dst.push(a as u8);
101        }
102    }
103
104    if pos != buffer.len() {
105        return Err(DecodeError::TrailingData);
106    }
107    Ok(dst)
108}