Skip to main content

gemini_cli/auth/
mod.rs

1pub mod auto_refresh;
2pub mod current;
3pub mod login;
4pub mod output;
5pub mod refresh;
6pub mod remove;
7pub mod save;
8pub mod sync;
9pub mod use_secret;
10
11use std::io;
12use std::path::{Path, PathBuf};
13use std::time::{SystemTime, UNIX_EPOCH};
14
15pub(crate) const SECRET_FILE_MODE: u32 = crate::fs::SECRET_FILE_MODE;
16
17#[cfg(test)]
18pub(crate) fn test_env_lock() -> std::sync::MutexGuard<'static, ()> {
19    use std::sync::{Mutex, OnceLock};
20
21    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
22    match LOCK.get_or_init(|| Mutex::new(())).lock() {
23        Ok(guard) => guard,
24        Err(poisoned) => poisoned.into_inner(),
25    }
26}
27
28pub fn identity_from_auth_file(path: &Path) -> io::Result<Option<String>> {
29    crate::runtime::auth::identity_from_auth_file(path).map_err(core_error_to_io)
30}
31
32pub fn email_from_auth_file(path: &Path) -> io::Result<Option<String>> {
33    crate::runtime::auth::email_from_auth_file(path).map_err(core_error_to_io)
34}
35
36pub fn account_id_from_auth_file(path: &Path) -> io::Result<Option<String>> {
37    crate::runtime::auth::account_id_from_auth_file(path).map_err(core_error_to_io)
38}
39
40pub fn last_refresh_from_auth_file(path: &Path) -> io::Result<Option<String>> {
41    crate::runtime::auth::last_refresh_from_auth_file(path).map_err(core_error_to_io)
42}
43
44pub fn identity_key_from_auth_file(path: &Path) -> io::Result<Option<String>> {
45    crate::runtime::auth::identity_key_from_auth_file(path).map_err(core_error_to_io)
46}
47
48pub(crate) fn write_atomic(path: &Path, contents: &[u8], mode: u32) -> io::Result<()> {
49    crate::fs::write_atomic(path, contents, mode)
50}
51
52pub(crate) fn write_timestamp(path: &Path, iso: Option<&str>) -> io::Result<()> {
53    crate::fs::write_timestamp(path, iso)
54}
55
56pub(crate) fn normalize_iso(raw: &str) -> String {
57    let mut trimmed = crate::json::strip_newlines(raw);
58    if let Some(dot) = trimmed.find('.')
59        && trimmed.ends_with('Z')
60    {
61        trimmed.truncate(dot);
62        trimmed.push('Z');
63    }
64    trimmed
65}
66
67pub(crate) fn parse_rfc3339_epoch(raw: &str) -> Option<i64> {
68    let normalized = normalize_iso(raw);
69    let (datetime, offset_seconds) = if normalized.ends_with('Z') {
70        (&normalized[..normalized.len().saturating_sub(1)], 0i64)
71    } else {
72        if normalized.len() < 6 {
73            return None;
74        }
75        let tail_index = normalized.len() - 6;
76        let sign = normalized.as_bytes().get(tail_index).copied()? as char;
77        if sign != '+' && sign != '-' {
78            return None;
79        }
80        if normalized.as_bytes().get(tail_index + 3).copied()? as char != ':' {
81            return None;
82        }
83        let hours = parse_u32(&normalized[tail_index + 1..tail_index + 3])? as i64;
84        let minutes = parse_u32(&normalized[tail_index + 4..])? as i64;
85        let mut offset = hours * 3600 + minutes * 60;
86        if sign == '-' {
87            offset = -offset;
88        }
89        (&normalized[..tail_index], offset)
90    };
91
92    if datetime.len() != 19 {
93        return None;
94    }
95    if datetime.as_bytes().get(4).copied()? as char != '-'
96        || datetime.as_bytes().get(7).copied()? as char != '-'
97        || datetime.as_bytes().get(10).copied()? as char != 'T'
98        || datetime.as_bytes().get(13).copied()? as char != ':'
99        || datetime.as_bytes().get(16).copied()? as char != ':'
100    {
101        return None;
102    }
103
104    let year = parse_i64(&datetime[0..4])?;
105    let month = parse_u32(&datetime[5..7])? as i64;
106    let day = parse_u32(&datetime[8..10])? as i64;
107    let hour = parse_u32(&datetime[11..13])? as i64;
108    let minute = parse_u32(&datetime[14..16])? as i64;
109    let second = parse_u32(&datetime[17..19])? as i64;
110
111    if !(1..=12).contains(&month)
112        || !(1..=31).contains(&day)
113        || hour > 23
114        || minute > 59
115        || second > 60
116    {
117        return None;
118    }
119
120    let days = days_from_civil(year, month, day);
121    let local_epoch = days * 86_400 + hour * 3_600 + minute * 60 + second;
122    Some(local_epoch - offset_seconds)
123}
124
125pub(crate) fn now_epoch_seconds() -> i64 {
126    SystemTime::now()
127        .duration_since(UNIX_EPOCH)
128        .map(|duration| duration.as_secs() as i64)
129        .unwrap_or(0)
130}
131
132pub(crate) fn now_utc_iso() -> String {
133    epoch_to_utc_iso(now_epoch_seconds())
134}
135
136pub(crate) fn temp_file_path(prefix: &str) -> PathBuf {
137    let mut path = std::env::temp_dir();
138    let pid = std::process::id();
139    let nanos = SystemTime::now()
140        .duration_since(UNIX_EPOCH)
141        .map(|duration| duration.as_nanos())
142        .unwrap_or(0);
143    path.push(format!("{prefix}-{pid}-{nanos}.json"));
144    path
145}
146
147fn core_error_to_io(err: crate::runtime::CoreError) -> io::Error {
148    io::Error::other(err.to_string())
149}
150
151fn parse_u32(raw: &str) -> Option<u32> {
152    raw.parse::<u32>().ok()
153}
154
155fn parse_i64(raw: &str) -> Option<i64> {
156    raw.parse::<i64>().ok()
157}
158
159fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
160    let adjusted_year = year - i64::from(month <= 2);
161    let era = if adjusted_year >= 0 {
162        adjusted_year / 400
163    } else {
164        (adjusted_year - 399) / 400
165    };
166    let year_of_era = adjusted_year - era * 400;
167    let month_prime = month + if month > 2 { -3 } else { 9 };
168    let day_of_year = (153 * month_prime + 2) / 5 + day - 1;
169    let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
170    era * 146_097 + day_of_era - 719_468
171}
172
173fn epoch_to_utc_iso(epoch: i64) -> String {
174    let days = epoch.div_euclid(86_400);
175    let seconds_of_day = epoch.rem_euclid(86_400);
176
177    let (year, month, day) = civil_from_days(days);
178    let hour = seconds_of_day / 3_600;
179    let minute = (seconds_of_day % 3_600) / 60;
180    let second = seconds_of_day % 60;
181
182    format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z")
183}
184
185fn civil_from_days(days_since_epoch: i64) -> (i64, i64, i64) {
186    let z = days_since_epoch + 719_468;
187    let era = if z >= 0 {
188        z / 146_097
189    } else {
190        (z - 146_096) / 146_097
191    };
192    let day_of_era = z - era * 146_097;
193    let year_of_era =
194        (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
195    let year = year_of_era + era * 400;
196    let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
197    let month_prime = (5 * day_of_year + 2) / 153;
198    let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
199    let month = month_prime + if month_prime < 10 { 3 } else { -9 };
200    let full_year = year + i64::from(month <= 2);
201
202    (full_year, month, day)
203}
204
205#[cfg(test)]
206mod tests {
207    use super::{normalize_iso, parse_rfc3339_epoch};
208
209    #[test]
210    fn normalize_iso_removes_fractional_seconds() {
211        assert_eq!(
212            normalize_iso("2025-01-20T12:34:56.789Z"),
213            "2025-01-20T12:34:56Z"
214        );
215    }
216
217    #[test]
218    fn parse_rfc3339_epoch_supports_zulu_and_offsets() {
219        assert_eq!(parse_rfc3339_epoch("1970-01-01T00:00:00Z"), Some(0));
220        assert_eq!(parse_rfc3339_epoch("1970-01-01T01:00:00+01:00"), Some(0));
221    }
222}