1pub mod account_history;
2pub mod auth;
3pub mod cli;
4pub mod doctor;
5pub mod error;
6pub mod format;
7pub mod limits;
8pub mod pricing;
9pub mod prompt;
10pub(crate) mod session_scan;
11pub mod stats;
12pub mod storage;
13pub mod time;
14
15use crate::auth::{
16 format_auth_profile_entry, format_auth_profile_list, format_auth_status,
17 list_codex_auth_profiles, read_codex_auth_status, remove_codex_auth_profile,
18 save_current_codex_auth_profile, switch_codex_auth_profile, AuthCommandOptions,
19 AuthProfileEntry, AuthProfileListReport,
20};
21use crate::cli::{
22 AuthCliCommand, AuthCliPaths, AuthProfileCliOptions, AuthRemoveCliOptions,
23 AuthSelectCliOptions, AuthStatusCliOptions, CliCommand, DoctorCliCommand, DoctorCliPaths,
24 LimitCliCommand, ParsedCli, StatCliCommand,
25};
26use crate::doctor::{format_doctor_report, read_doctor_report, DoctorOptions};
27use crate::error::AppError;
28use crate::limits::run_limit_command;
29use crate::prompt::{DialoguerPrompt, Prompt};
30use crate::stats::run_stat_command;
31use chrono::{DateTime, Utc};
32use std::env;
33
34const PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION");
35
36#[derive(Debug, Eq, PartialEq)]
37pub struct CliResult {
38 pub code: i32,
39 pub stdout: String,
40 pub stderr: String,
41}
42
43impl CliResult {
44 fn success(stdout: impl Into<String>) -> Self {
45 Self {
46 code: 0,
47 stdout: ensure_trailing_newline(stdout.into()),
48 stderr: String::new(),
49 }
50 }
51
52 fn app_error(error: AppError) -> Self {
53 Self {
54 code: error.exit_code(),
55 stdout: String::new(),
56 stderr: ensure_trailing_newline(error.message().to_string()),
57 }
58 }
59
60 fn parse_error(code: i32, stderr: impl Into<String>) -> Self {
61 Self {
62 code,
63 stdout: String::new(),
64 stderr: ensure_trailing_newline(stderr.into()),
65 }
66 }
67}
68
69pub fn run_cli<I, S>(args: I) -> CliResult
70where
71 I: IntoIterator<Item = S>,
72 S: Into<String>,
73{
74 let args: Vec<String> = args.into_iter().map(Into::into).collect();
75
76 match cli::parse_cli(&args) {
77 Ok(ParsedCli::Help(help)) => CliResult::success(help),
78 Ok(ParsedCli::Version) => CliResult::success(PACKAGE_VERSION),
79 Ok(ParsedCli::Command(command)) => match *command {
80 CliCommand::Auth(command) => run_auth(command),
81 CliCommand::Doctor(command) => run_doctor(command),
82 CliCommand::Stat(command) => run_stat(command),
83 CliCommand::Limit(command) => run_limit(command),
84 },
85 Err(error) => CliResult::parse_error(error.code, error.message),
86 }
87}
88
89fn run_auth(command: AuthCliCommand) -> CliResult {
90 match command {
91 AuthCliCommand::Status(options) => run_auth_status(options),
92 AuthCliCommand::Save(options) => run_auth_save(options),
93 AuthCliCommand::List(options) => run_auth_list(options),
94 AuthCliCommand::Select(options) => run_auth_select(options),
95 AuthCliCommand::Remove(options) => run_auth_remove(options),
96 }
97}
98
99fn run_auth_status(options: AuthStatusCliOptions) -> CliResult {
100 let result = (|| {
101 let auth_options = auth_command_options(&options.paths);
102 let report = read_codex_auth_status(&auth_options, cli_now()?)?;
103 format_auth_status(&report, options.json, options.include_token_claims)
104 })();
105
106 cli_result_from_string(result)
107}
108
109fn run_auth_save(options: AuthProfileCliOptions) -> CliResult {
110 let result = (|| {
111 let auth_options = auth_command_options(&options.paths);
112 let report = save_current_codex_auth_profile(&auth_options, cli_now()?)?;
113 Ok(format!(
114 "Saved auth profile: {}\nStore: {}\n",
115 format_auth_profile_entry(&report.profile),
116 report.store_dir
117 ))
118 })();
119
120 cli_result_from_string(result)
121}
122
123fn run_auth_list(options: AuthProfileCliOptions) -> CliResult {
124 let result = (|| {
125 let auth_options = auth_command_options(&options.paths);
126 let report = list_codex_auth_profiles(&auth_options, cli_now()?)?;
127 Ok(format_auth_profile_list(&report))
128 })();
129
130 cli_result_from_string(result)
131}
132
133fn run_auth_select(options: AuthSelectCliOptions) -> CliResult {
134 let result = (|| {
135 let now = cli_now()?;
136 let auth_options = auth_command_options(&options.paths);
137 let report = list_codex_auth_profiles(&auth_options, now)?;
138 if let Some(account_id) = options.account_id.as_deref() {
139 return select_auth_profile_by_account_id(account_id, &report, &auth_options, now);
140 }
141
142 if !prompt::stdin_and_stderr_are_terminals() {
143 return Err(AppError::new(
144 "auth select requires an interactive terminal unless --account-id is supplied.",
145 ));
146 }
147
148 let mut prompt = DialoguerPrompt::default();
149 select_auth_profile_interactively(&report, &auth_options, now, &mut prompt)
150 })();
151
152 cli_result_from_string(result)
153}
154
155fn run_auth_remove(options: AuthRemoveCliOptions) -> CliResult {
156 let result = (|| {
157 let now = cli_now()?;
158 let auth_options = auth_command_options(&options.paths);
159 let report = list_codex_auth_profiles(&auth_options, now)?;
160 if report.stored.is_empty() {
161 return Ok("No persisted auth profiles.\n".to_string());
162 }
163
164 if let Some(account_id) = options.account_id.as_deref() {
165 if !options.yes {
166 return Err(AppError::new(
167 "auth remove --account-id requires --yes when not running interactively.",
168 ));
169 }
170 return remove_auth_profile_by_account_id(account_id, &report, &auth_options, now);
171 }
172
173 if !prompt::stdin_and_stderr_are_terminals() {
174 return Err(AppError::new(
175 "auth remove requires an interactive terminal unless --account-id is supplied.",
176 ));
177 }
178
179 let mut prompt = DialoguerPrompt::default();
180 remove_auth_profiles_interactively(&report, &auth_options, now, &mut prompt)
181 })();
182
183 cli_result_from_string(result)
184}
185
186fn select_auth_profile_interactively(
187 report: &AuthProfileListReport,
188 options: &AuthCommandOptions,
189 now: DateTime<Utc>,
190 prompt: &mut impl Prompt,
191) -> Result<String, AppError> {
192 if report.stored.is_empty() {
193 return Err(AppError::new("No persisted auth profiles."));
194 }
195
196 let items = report
197 .stored
198 .iter()
199 .map(format_auth_profile_entry)
200 .collect::<Vec<_>>();
201 let selected_index = prompt
202 .select("Select auth profile", &items)?
203 .ok_or_else(|| AppError::new("auth select cancelled."))?;
204 let selected = report
205 .stored
206 .get(selected_index)
207 .ok_or_else(|| AppError::new("Prompt returned an invalid auth profile selection."))?;
208
209 select_auth_profile_entry(selected, report, options, now)
210}
211
212fn select_auth_profile_by_account_id(
213 account_id: &str,
214 report: &AuthProfileListReport,
215 options: &AuthCommandOptions,
216 now: DateTime<Utc>,
217) -> Result<String, AppError> {
218 let selected = report
219 .stored
220 .iter()
221 .find(|entry| entry.account_id == account_id)
222 .ok_or_else(|| {
223 AppError::new(format!(
224 "No persisted auth profile found for account id: {account_id}"
225 ))
226 })?;
227
228 select_auth_profile_entry(selected, report, options, now)
229}
230
231fn select_auth_profile_entry(
232 selected: &AuthProfileEntry,
233 report: &AuthProfileListReport,
234 options: &AuthCommandOptions,
235 now: DateTime<Utc>,
236) -> Result<String, AppError> {
237 if Some(&selected.account_id) == report.current.as_ref().map(|entry| &entry.account_id) {
238 return Ok(format!(
239 "Auth profile already active: {}\n",
240 format_auth_profile_entry(selected)
241 ));
242 }
243
244 let switched = switch_codex_auth_profile(&selected.account_id, options, now)?;
245 Ok(format!(
246 "Saved current auth profile: {}\nActivated auth profile: {}\n",
247 format_auth_profile_entry(&switched.saved_current),
248 format_auth_profile_entry(&switched.activated)
249 ))
250}
251
252fn remove_auth_profiles_interactively(
253 report: &AuthProfileListReport,
254 options: &AuthCommandOptions,
255 now: DateTime<Utc>,
256 prompt: &mut impl Prompt,
257) -> Result<String, AppError> {
258 let current_account_id = report
259 .current
260 .as_ref()
261 .map(|entry| entry.account_id.as_str());
262 let candidates = report
263 .stored
264 .iter()
265 .filter(|entry| Some(entry.account_id.as_str()) != current_account_id)
266 .collect::<Vec<_>>();
267 if candidates.is_empty() {
268 return Ok("No removable persisted auth profiles.\n".to_string());
269 }
270
271 let items = candidates
272 .iter()
273 .map(|entry| format_auth_profile_entry(entry))
274 .collect::<Vec<_>>();
275 let selected_indices = prompt
276 .multi_select("Remove auth profiles", &items)?
277 .ok_or_else(|| AppError::new("auth remove cancelled."))?;
278 if selected_indices.is_empty() {
279 return Err(AppError::new("auth remove cancelled."));
280 }
281
282 let selected = selected_indices
283 .into_iter()
284 .map(|index| {
285 candidates
286 .get(index)
287 .copied()
288 .ok_or_else(|| AppError::new("Prompt returned an invalid auth profile selection."))
289 })
290 .collect::<Result<Vec<_>, _>>()?;
291 let confirmed = prompt
292 .confirm("Remove selected auth profiles?", false)?
293 .unwrap_or(false);
294 if !confirmed {
295 return Err(AppError::new("auth remove cancelled."));
296 }
297
298 selected
299 .into_iter()
300 .map(|entry| remove_auth_profile_entry(entry, options, now))
301 .collect::<Result<Vec<_>, _>>()
302 .map(|lines| lines.join("\n"))
303}
304
305fn remove_auth_profile_by_account_id(
306 account_id: &str,
307 report: &AuthProfileListReport,
308 options: &AuthCommandOptions,
309 now: DateTime<Utc>,
310) -> Result<String, AppError> {
311 let selected = report
312 .stored
313 .iter()
314 .find(|entry| entry.account_id == account_id)
315 .ok_or_else(|| {
316 AppError::new(format!(
317 "No persisted auth profile found for account id: {account_id}"
318 ))
319 })?;
320
321 remove_auth_profile_entry(selected, options, now)
322}
323
324fn remove_auth_profile_entry(
325 selected: &AuthProfileEntry,
326 options: &AuthCommandOptions,
327 now: DateTime<Utc>,
328) -> Result<String, AppError> {
329 let removed = remove_codex_auth_profile(&selected.account_id, options, now)?;
330 Ok(format!(
331 "Removed auth profile: {}",
332 format_auth_profile_entry(&removed.removed)
333 ))
334}
335
336fn run_doctor(command: DoctorCliCommand) -> CliResult {
337 let DoctorCliCommand::Run(options) = command;
338 let result = (|| {
339 let doctor_options = doctor_command_options(&options.paths);
340 let report = read_doctor_report(&doctor_options, cli_now()?);
341 format_doctor_report(&report, options.json)
342 })();
343
344 cli_result_from_string(result)
345}
346
347fn run_stat(command: StatCliCommand) -> CliResult {
348 let result = (|| {
349 run_stat_command(
350 command.view.as_deref(),
351 command.session.as_deref(),
352 command.options,
353 cli_now()?,
354 )
355 })();
356 cli_result_from_string(result)
357}
358
359fn run_limit(command: LimitCliCommand) -> CliResult {
360 let result = (|| run_limit_command(command.command, command.options, cli_now()?))();
361 cli_result_from_string(result)
362}
363
364fn auth_command_options(paths: &AuthCliPaths) -> AuthCommandOptions {
365 AuthCommandOptions {
366 auth_file: paths.auth_file.clone(),
367 codex_home: paths.codex_home.clone(),
368 store_dir: paths.store_dir.clone(),
369 account_history_file: paths.account_history_file.clone(),
370 }
371}
372
373fn doctor_command_options(paths: &DoctorCliPaths) -> DoctorOptions {
374 DoctorOptions {
375 auth_file: paths.auth_file.clone(),
376 codex_home: paths.codex_home.clone(),
377 sessions_dir: paths.sessions_dir.clone(),
378 }
379}
380
381fn cli_now() -> Result<DateTime<Utc>, AppError> {
382 match env::var("CODEX_OPS_FIXED_NOW") {
383 Ok(value) if !value.trim().is_empty() => DateTime::parse_from_rfc3339(value.trim())
384 .map(|date| date.with_timezone(&Utc))
385 .map_err(|_| {
386 AppError::new("Invalid CODEX_OPS_FIXED_NOW. Expected an ISO date string.")
387 }),
388 _ => Ok(Utc::now()),
389 }
390}
391
392fn cli_result_from_string(result: Result<String, AppError>) -> CliResult {
393 match result {
394 Ok(stdout) => CliResult::success(stdout),
395 Err(error) => CliResult::app_error(error),
396 }
397}
398
399fn ensure_trailing_newline(mut value: String) -> String {
400 if !value.ends_with('\n') {
401 value.push('\n');
402 }
403 value
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 #[test]
411 fn root_help_lists_top_level_commands() {
412 let result = run_cli(["--help"]);
413
414 assert_eq!(result.code, 0);
415 assert!(result.stderr.is_empty());
416 assert!(result.stdout.contains("Usage: codex-ops"));
417 assert!(result.stdout.contains("auth"));
418 assert!(result.stdout.contains("doctor"));
419 assert!(result.stdout.contains("stat"));
420 assert!(result.stdout.contains("limit"));
421 assert!(!result.stdout.contains("cycle"));
422 }
423
424 #[test]
425 fn auth_help_lists_profile_commands() {
426 let result = run_cli(["auth", "--help"]);
427
428 assert_eq!(result.code, 0);
429 assert!(result.stdout.contains("status"));
430 assert!(result.stdout.contains("save"));
431 assert!(result.stdout.contains("list"));
432 assert!(result.stdout.contains("select"));
433 assert!(result.stdout.contains("remove"));
434 }
435
436 #[test]
437 fn child_help_works() {
438 let result = run_cli(["stat", "--help"]);
439
440 assert_eq!(result.code, 0);
441 assert!(result.stdout.contains("--group-by"));
442 assert!(result.stdout.contains("sessions"));
443 }
444
445 #[test]
446 fn unknown_command_returns_error() {
447 let result = run_cli(["missing"]);
448
449 assert_eq!(result.code, 2);
450 assert!(result.stderr.contains("unrecognized subcommand 'missing'"));
451 }
452
453 #[test]
454 fn removed_cycle_command_returns_unknown_command() {
455 let result = run_cli(["cycle", "current"]);
456
457 assert_eq!(result.code, 2);
458 assert!(result.stdout.is_empty());
459 assert!(result.stderr.contains("unrecognized subcommand 'cycle'"));
460 }
461
462 #[test]
463 fn interactive_auth_select_switches_profile_and_writes_history() {
464 let fixture = AuthPromptFixture::new("select-switch");
465 let now = fixed_now();
466 let current_content = auth_content("account-a", "a@example.test", "plus");
467 let selected_content = auth_content("account-b", "b@example.test", "pro");
468
469 fixture.save_profile(&selected_content, now);
470 std::fs::write(&fixture.auth_file, ¤t_content).expect("write current auth");
471 let options = fixture.options();
472 let report = list_codex_auth_profiles(&options, now).expect("list profiles");
473 let mut prompt = FakePrompt {
474 select: Some(Some(0)),
475 ..FakePrompt::default()
476 };
477
478 let output =
479 select_auth_profile_interactively(&report, &options, now, &mut prompt).unwrap();
480
481 assert!(output.contains("Activated auth profile: b@example.test(account-b) - pro"));
482 assert_eq!(
483 std::fs::read_to_string(&fixture.auth_file).expect("read auth"),
484 selected_content
485 );
486 let history: serde_json::Value = serde_json::from_str(
487 &std::fs::read_to_string(&fixture.history_file).expect("read history"),
488 )
489 .expect("parse history");
490 assert_eq!(history["switches"][0]["fromAccountId"], "account-a");
491 assert_eq!(history["switches"][0]["toAccountId"], "account-b");
492 assert_eq!(
493 prompt.select_items[0],
494 vec!["b@example.test(account-b) - pro".to_string()]
495 );
496 }
497
498 #[test]
499 fn interactive_auth_select_cancel_has_no_side_effects() {
500 let fixture = AuthPromptFixture::new("select-cancel");
501 let now = fixed_now();
502 let current_content = auth_content("account-a", "a@example.test", "plus");
503 let selected_content = auth_content("account-b", "b@example.test", "pro");
504
505 fixture.save_profile(&selected_content, now);
506 std::fs::write(&fixture.auth_file, ¤t_content).expect("write current auth");
507 let options = fixture.options();
508 let report = list_codex_auth_profiles(&options, now).expect("list profiles");
509 let mut prompt = FakePrompt {
510 select: Some(None),
511 ..FakePrompt::default()
512 };
513
514 let error =
515 select_auth_profile_interactively(&report, &options, now, &mut prompt).unwrap_err();
516
517 assert_eq!(error.message(), "auth select cancelled.");
518 assert_eq!(
519 std::fs::read_to_string(&fixture.auth_file).expect("read auth"),
520 current_content
521 );
522 assert!(!fixture.history_file.exists());
523 }
524
525 #[test]
526 fn interactive_auth_remove_deletes_selected_profiles_but_not_current() {
527 let fixture = AuthPromptFixture::new("remove-selected");
528 let now = fixed_now();
529 let current_content = auth_content("account-a", "a@example.test", "plus");
530 let other_content = auth_content("account-b", "b@example.test", "pro");
531 let third_content = auth_content("account-c", "c@example.test", "team");
532
533 fixture.save_profile(¤t_content, now);
534 fixture.save_profile(&other_content, now);
535 fixture.save_profile(&third_content, now);
536 std::fs::write(&fixture.auth_file, ¤t_content).expect("write current auth");
537 let options = fixture.options();
538 let report = list_codex_auth_profiles(&options, now).expect("list profiles");
539 let mut prompt = FakePrompt {
540 multi_select: Some(Some(vec![0])),
541 confirm: Some(Some(true)),
542 ..FakePrompt::default()
543 };
544
545 let output =
546 remove_auth_profiles_interactively(&report, &options, now, &mut prompt).unwrap();
547
548 assert!(output.contains("Removed auth profile: b@example.test(account-b) - pro"));
549 assert_eq!(
550 prompt.multi_select_items[0],
551 vec![
552 "b@example.test(account-b) - pro".to_string(),
553 "c@example.test(account-c) - team".to_string()
554 ]
555 );
556 assert!(fixture.profile_file("account-a").exists());
557 assert!(!fixture.profile_file("account-b").exists());
558 assert!(fixture.profile_file("account-c").exists());
559 }
560
561 #[test]
562 fn interactive_auth_remove_confirmation_reject_has_no_side_effects() {
563 let fixture = AuthPromptFixture::new("remove-cancel");
564 let now = fixed_now();
565 let current_content = auth_content("account-a", "a@example.test", "plus");
566 let other_content = auth_content("account-b", "b@example.test", "pro");
567
568 fixture.save_profile(¤t_content, now);
569 fixture.save_profile(&other_content, now);
570 std::fs::write(&fixture.auth_file, ¤t_content).expect("write current auth");
571 let options = fixture.options();
572 let report = list_codex_auth_profiles(&options, now).expect("list profiles");
573 let mut prompt = FakePrompt {
574 multi_select: Some(Some(vec![0])),
575 confirm: Some(Some(false)),
576 ..FakePrompt::default()
577 };
578
579 let error =
580 remove_auth_profiles_interactively(&report, &options, now, &mut prompt).unwrap_err();
581
582 assert_eq!(error.message(), "auth remove cancelled.");
583 assert!(fixture.profile_file("account-a").exists());
584 assert!(fixture.profile_file("account-b").exists());
585 }
586
587 #[derive(Default)]
588 struct FakePrompt {
589 select: Option<Option<usize>>,
590 multi_select: Option<Option<Vec<usize>>>,
591 confirm: Option<Option<bool>>,
592 select_items: Vec<Vec<String>>,
593 multi_select_items: Vec<Vec<String>>,
594 }
595
596 impl Prompt for FakePrompt {
597 fn select(&mut self, _prompt: &str, items: &[String]) -> Result<Option<usize>, AppError> {
598 self.select_items.push(items.to_vec());
599 self.select
600 .take()
601 .ok_or_else(|| AppError::new("missing fake select response"))
602 }
603
604 fn multi_select(
605 &mut self,
606 _prompt: &str,
607 items: &[String],
608 ) -> Result<Option<Vec<usize>>, AppError> {
609 self.multi_select_items.push(items.to_vec());
610 self.multi_select
611 .take()
612 .ok_or_else(|| AppError::new("missing fake multi-select response"))
613 }
614
615 fn confirm(&mut self, _prompt: &str, _default: bool) -> Result<Option<bool>, AppError> {
616 self.confirm
617 .take()
618 .ok_or_else(|| AppError::new("missing fake confirm response"))
619 }
620 }
621
622 struct AuthPromptFixture {
623 root: std::path::PathBuf,
624 auth_file: std::path::PathBuf,
625 store_dir: std::path::PathBuf,
626 history_file: std::path::PathBuf,
627 }
628
629 impl AuthPromptFixture {
630 fn new(label: &str) -> Self {
631 let root = temp_dir(&format!("codex-ops-auth-{label}"));
632 std::fs::create_dir_all(&root).expect("create auth fixture root");
633 Self {
634 auth_file: root.join("auth.json"),
635 store_dir: root.join("auth-profiles"),
636 history_file: root.join("auth-account-history.json"),
637 root,
638 }
639 }
640
641 fn options(&self) -> AuthCommandOptions {
642 AuthCommandOptions {
643 auth_file: Some(self.auth_file.clone()),
644 store_dir: Some(self.store_dir.clone()),
645 account_history_file: Some(self.history_file.clone()),
646 ..AuthCommandOptions::default()
647 }
648 }
649
650 fn save_profile(&self, content: &str, now: DateTime<Utc>) {
651 std::fs::write(&self.auth_file, content).expect("write auth profile content");
652 save_current_codex_auth_profile(
653 &AuthCommandOptions {
654 auth_file: Some(self.auth_file.clone()),
655 store_dir: Some(self.store_dir.clone()),
656 ..AuthCommandOptions::default()
657 },
658 now,
659 )
660 .expect("save auth profile");
661 }
662
663 fn profile_file(&self, account_id: &str) -> std::path::PathBuf {
664 self.store_dir.join(format!("{account_id}.json"))
665 }
666 }
667
668 impl Drop for AuthPromptFixture {
669 fn drop(&mut self) {
670 let _ = std::fs::remove_dir_all(&self.root);
671 }
672 }
673
674 fn fixed_now() -> DateTime<Utc> {
675 DateTime::parse_from_rfc3339("2026-05-13T00:00:00.000Z")
676 .expect("fixed now")
677 .with_timezone(&Utc)
678 }
679
680 fn auth_content(account_id: &str, email: &str, plan: &str) -> String {
681 let payload = serde_json::json!({
682 "sub": format!("auth0|{account_id}"),
683 "email": email,
684 "https://api.openai.com/auth": {
685 "chatgpt_account_id": account_id,
686 "chatgpt_plan_type": plan,
687 "chatgpt_user_id": format!("user-{account_id}"),
688 "user_id": format!("user-{account_id}")
689 }
690 });
691 let token = jwt(r#"{"alg":"RS256","kid":"key-1"}"#, &payload.to_string());
692 serde_json::to_string_pretty(&serde_json::json!({
693 "auth_mode": "chatgpt",
694 "tokens": {
695 "id_token": token,
696 "refresh_token": "synthetic-refresh-token",
697 "account_id": account_id
698 },
699 "last_refresh": "2026-05-12T05:32:41.917677755Z"
700 }))
701 .expect("serialize auth content")
702 }
703
704 fn jwt(header: &str, payload: &str) -> String {
705 format!(
706 "{}.{}.signature",
707 encode_base64url(header),
708 encode_base64url(payload)
709 )
710 }
711
712 fn encode_base64url(value: &str) -> String {
713 const TABLE: &[u8; 64] =
714 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
715 let bytes = value.as_bytes();
716 let mut output = String::new();
717 let mut index = 0;
718
719 while index < bytes.len() {
720 let b0 = bytes[index];
721 let b1 = *bytes.get(index + 1).unwrap_or(&0);
722 let b2 = *bytes.get(index + 2).unwrap_or(&0);
723 output.push(TABLE[(b0 >> 2) as usize] as char);
724 output.push(TABLE[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
725 if index + 1 < bytes.len() {
726 output.push(TABLE[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char);
727 }
728 if index + 2 < bytes.len() {
729 output.push(TABLE[(b2 & 0x3f) as usize] as char);
730 }
731 index += 3;
732 }
733
734 output
735 }
736
737 fn temp_dir(prefix: &str) -> std::path::PathBuf {
738 let millis = std::time::SystemTime::now()
739 .duration_since(std::time::UNIX_EPOCH)
740 .expect("system time")
741 .as_millis();
742 std::env::temp_dir().join(format!("{prefix}-{millis}-{}", std::process::id()))
743 }
744}