Skip to main content

gemini_cli/
fs.rs

1use std::fs::{self, File, OpenOptions};
2use std::io::{self, Read, Write};
3use std::path::{Path, PathBuf};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6#[cfg(unix)]
7use std::os::unix::fs::PermissionsExt;
8
9pub const SECRET_FILE_MODE: u32 = 0o600;
10
11pub fn sha256_file(path: &Path) -> io::Result<String> {
12    let mut file = File::open(path)?;
13    let mut hasher = Sha256::new();
14    let mut buf = [0u8; 8192];
15
16    loop {
17        let read = file.read(&mut buf)?;
18        if read == 0 {
19            break;
20        }
21        hasher.update(&buf[..read]);
22    }
23
24    Ok(hex_encode(&hasher.finalize()))
25}
26
27pub fn write_atomic(path: &Path, contents: &[u8], mode: u32) -> io::Result<()> {
28    if let Some(parent) = path.parent() {
29        fs::create_dir_all(parent)?;
30    }
31
32    let mut attempt = 0u32;
33    loop {
34        let tmp_path = temp_path(path, attempt);
35        match OpenOptions::new()
36            .write(true)
37            .create_new(true)
38            .open(&tmp_path)
39        {
40            Ok(mut file) => {
41                file.write_all(contents)?;
42                let _ = file.flush();
43                set_permissions(&tmp_path, mode)?;
44                drop(file);
45
46                fs::rename(&tmp_path, path)?;
47                set_permissions(path, mode)?;
48                return Ok(());
49            }
50            Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {
51                attempt += 1;
52                if attempt > 10 {
53                    return Err(io::Error::new(
54                        io::ErrorKind::AlreadyExists,
55                        format!("failed to create unique temp file for {}", path.display()),
56                    ));
57                }
58            }
59            Err(err) => return Err(err),
60        }
61    }
62}
63
64pub fn write_timestamp(path: &Path, iso: Option<&str>) -> io::Result<()> {
65    if let Some(parent) = path.parent() {
66        fs::create_dir_all(parent)?;
67    }
68
69    if let Some(raw) = iso {
70        let trimmed = raw.split(&['\n', '\r'][..]).next().unwrap_or("");
71        if !trimmed.is_empty() {
72            fs::write(path, trimmed)?;
73            return Ok(());
74        }
75    }
76
77    match fs::remove_file(path) {
78        Ok(()) => Ok(()),
79        Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
80        Err(err) => Err(err),
81    }
82}
83
84#[cfg(unix)]
85fn set_permissions(path: &Path, mode: u32) -> io::Result<()> {
86    let perm = fs::Permissions::from_mode(mode);
87    fs::set_permissions(path, perm)
88}
89
90#[cfg(not(unix))]
91fn set_permissions(_path: &Path, _mode: u32) -> io::Result<()> {
92    Ok(())
93}
94
95fn temp_path(path: &Path, attempt: u32) -> PathBuf {
96    let filename = path
97        .file_name()
98        .and_then(|name| name.to_str())
99        .unwrap_or("tmp");
100    let pid = std::process::id();
101    let nanos = SystemTime::now()
102        .duration_since(UNIX_EPOCH)
103        .map(|duration| duration.as_nanos())
104        .unwrap_or(0);
105    let tmp_name = format!(".{filename}.tmp-{pid}-{nanos}-{attempt}");
106    path.with_file_name(tmp_name)
107}
108
109fn hex_encode(bytes: &[u8]) -> String {
110    const HEX: &[u8; 16] = b"0123456789abcdef";
111
112    let mut out = String::with_capacity(bytes.len() * 2);
113    for byte in bytes {
114        out.push(HEX[(byte >> 4) as usize] as char);
115        out.push(HEX[(byte & 0x0f) as usize] as char);
116    }
117    out
118}
119
120struct Sha256 {
121    state: [u32; 8],
122    buffer: [u8; 64],
123    buffer_len: usize,
124    total_len: u64,
125}
126
127impl Sha256 {
128    fn new() -> Self {
129        Self {
130            state: [
131                0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
132                0x5be0cd19,
133            ],
134            buffer: [0u8; 64],
135            buffer_len: 0,
136            total_len: 0,
137        }
138    }
139
140    fn update(&mut self, mut data: &[u8]) {
141        self.total_len = self.total_len.wrapping_add(data.len() as u64);
142
143        if self.buffer_len > 0 {
144            let need = 64 - self.buffer_len;
145            let take = need.min(data.len());
146            self.buffer[self.buffer_len..self.buffer_len + take].copy_from_slice(&data[..take]);
147            self.buffer_len += take;
148            data = &data[take..];
149
150            if self.buffer_len == 64 {
151                let block = self.buffer;
152                self.compress(&block);
153                self.buffer_len = 0;
154            }
155        }
156
157        while data.len() >= 64 {
158            let block: [u8; 64] = data[..64].try_into().expect("64-byte block");
159            self.compress(&block);
160            data = &data[64..];
161        }
162
163        if !data.is_empty() {
164            self.buffer[..data.len()].copy_from_slice(data);
165            self.buffer_len = data.len();
166        }
167    }
168
169    fn finalize(mut self) -> [u8; 32] {
170        let bit_len = self.total_len.wrapping_mul(8);
171
172        self.buffer[self.buffer_len] = 0x80;
173        self.buffer_len += 1;
174
175        if self.buffer_len > 56 {
176            self.buffer[self.buffer_len..].fill(0);
177            let block = self.buffer;
178            self.compress(&block);
179            self.buffer = [0u8; 64];
180            self.buffer_len = 0;
181        }
182
183        self.buffer[self.buffer_len..56].fill(0);
184        self.buffer[56..64].copy_from_slice(&bit_len.to_be_bytes());
185        let block = self.buffer;
186        self.compress(&block);
187
188        let mut out = [0u8; 32];
189        for (index, chunk) in out.chunks_exact_mut(4).enumerate() {
190            chunk.copy_from_slice(&self.state[index].to_be_bytes());
191        }
192        out
193    }
194
195    fn compress(&mut self, block: &[u8; 64]) {
196        let mut schedule = [0u32; 64];
197        for (index, word) in schedule.iter_mut().take(16).enumerate() {
198            let offset = index * 4;
199            *word = u32::from_be_bytes([
200                block[offset],
201                block[offset + 1],
202                block[offset + 2],
203                block[offset + 3],
204            ]);
205        }
206
207        for index in 16..64 {
208            let s0 = schedule[index - 15].rotate_right(7)
209                ^ schedule[index - 15].rotate_right(18)
210                ^ (schedule[index - 15] >> 3);
211            let s1 = schedule[index - 2].rotate_right(17)
212                ^ schedule[index - 2].rotate_right(19)
213                ^ (schedule[index - 2] >> 10);
214            schedule[index] = schedule[index - 16]
215                .wrapping_add(s0)
216                .wrapping_add(schedule[index - 7])
217                .wrapping_add(s1);
218        }
219
220        let mut a = self.state[0];
221        let mut b = self.state[1];
222        let mut c = self.state[2];
223        let mut d = self.state[3];
224        let mut e = self.state[4];
225        let mut f = self.state[5];
226        let mut g = self.state[6];
227        let mut h = self.state[7];
228
229        for index in 0..64 {
230            let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
231            let choice = (e & f) ^ ((!e) & g);
232            let t1 = h
233                .wrapping_add(s1)
234                .wrapping_add(choice)
235                .wrapping_add(ROUND_CONSTANTS[index])
236                .wrapping_add(schedule[index]);
237            let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
238            let majority = (a & b) ^ (a & c) ^ (b & c);
239            let t2 = s0.wrapping_add(majority);
240
241            h = g;
242            g = f;
243            f = e;
244            e = d.wrapping_add(t1);
245            d = c;
246            c = b;
247            b = a;
248            a = t1.wrapping_add(t2);
249        }
250
251        self.state[0] = self.state[0].wrapping_add(a);
252        self.state[1] = self.state[1].wrapping_add(b);
253        self.state[2] = self.state[2].wrapping_add(c);
254        self.state[3] = self.state[3].wrapping_add(d);
255        self.state[4] = self.state[4].wrapping_add(e);
256        self.state[5] = self.state[5].wrapping_add(f);
257        self.state[6] = self.state[6].wrapping_add(g);
258        self.state[7] = self.state[7].wrapping_add(h);
259    }
260}
261
262const ROUND_CONSTANTS: [u32; 64] = [
263    0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
264    0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
265    0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
266    0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
267    0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
268    0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
269    0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
270    0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
271];