use anyhow::Result;
use std::fs;
use std::path::{Path, PathBuf};
pub(crate) fn determine_archive_dir(codex_dir: &Path, base_name: &str) -> Result<PathBuf> {
let base_dir = codex_dir.join(base_name);
if !base_dir.exists() {
return Ok(base_dir);
}
let mut max_incremental = 0;
for entry in fs::read_dir(codex_dir)? {
let entry = entry?;
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with(base_name)
&& let Some(suffix) = name_str.strip_prefix(base_name)
&& let Some(num_str) = suffix.strip_prefix('.')
&& let Ok(num) = num_str.parse::<u32>()
{
max_incremental = max_incremental.max(num);
}
}
Ok(codex_dir.join(format!("{}.{}", base_name, max_incremental + 1)))
}
pub(crate) fn parse_archive_name(name: &str) -> (String, u32) {
if let Some(dot_pos) = name.rfind('.')
&& let Ok(num) = name[dot_pos + 1..].parse::<u32>()
{
let base = &name[..dot_pos];
let short_id = extract_short_id(base);
return (short_id, num);
}
(extract_short_id(name), 0)
}
fn extract_short_id(name: &str) -> String {
let candidate = name.split('-').next_back().unwrap_or(name);
if candidate.is_empty() || !candidate.chars().all(|c| c.is_ascii_alphanumeric()) {
return name.to_string();
}
candidate.to_string()
}
pub(crate) fn get_base_archive_name(name: &str) -> String {
if let Some(dot_pos) = name.rfind('.')
&& name[dot_pos + 1..].parse::<u32>().is_ok()
{
return name[..dot_pos].to_string();
}
name.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_archive_name_no_suffix() {
let (short, incr) = parse_archive_name("2026-01-03-141500-abc12345");
assert_eq!(short, "abc12345");
assert_eq!(incr, 0);
}
#[test]
fn parse_archive_name_with_suffix() {
let (short, incr) = parse_archive_name("2026-01-03-141500-abc12345.7");
assert_eq!(short, "abc12345");
assert_eq!(incr, 7);
}
#[test]
fn get_base_archive_name_strips_suffix() {
assert_eq!(
get_base_archive_name("2026-01-03-141500-abc12345.7"),
"2026-01-03-141500-abc12345"
);
assert_eq!(
get_base_archive_name("2026-01-03-141500-abc12345"),
"2026-01-03-141500-abc12345"
);
}
#[test]
fn extract_short_id_validates_trailing_segment() {
assert_eq!(extract_short_id("2026-01-03-141500-abc12345"), "abc12345");
assert_eq!(extract_short_id("2026-01-03-141500-"), "2026-01-03-141500-");
assert_eq!(
extract_short_id("2026-01-03-141500-bad/path"),
"2026-01-03-141500-bad/path"
);
assert_eq!(extract_short_id("abc12345"), "abc12345");
}
#[test]
fn determine_archive_dir_picks_first_free() {
let tmp = tempfile::tempdir().unwrap();
let base = "2026-04-29-120000-deadbeef";
let p = determine_archive_dir(tmp.path(), base).unwrap();
assert_eq!(p, tmp.path().join(base));
fs::create_dir(tmp.path().join(base)).unwrap();
let p = determine_archive_dir(tmp.path(), base).unwrap();
assert_eq!(p, tmp.path().join(format!("{}.1", base)));
fs::create_dir(tmp.path().join(format!("{}.1", base))).unwrap();
let p = determine_archive_dir(tmp.path(), base).unwrap();
assert_eq!(p, tmp.path().join(format!("{}.2", base)));
}
}