use eyre::{Result, WrapErr};
use std::fs;
use std::path::{Path, PathBuf};
const JSONL_EXTENSION: &str = "jsonl";
const COMPRESSED_SUFFIX: &str = ".zst";
const COMPRESSED_JSONL_SUFFIX: &str = ".jsonl.zst";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::app) enum SessionFileFormat {
Plain,
Compressed,
}
impl SessionFileFormat {
pub(in crate::app) const fn is_compressed(self) -> bool {
matches!(self, Self::Compressed)
}
pub(in crate::app) const fn as_str(self) -> &'static str {
match self {
Self::Plain => "plain",
Self::Compressed => "compressed",
}
}
pub(in crate::app) fn from_str(value: &str) -> Option<Self> {
match value {
"plain" => Some(Self::Plain),
"compressed" => Some(Self::Compressed),
_ => None,
}
}
}
pub(in crate::app) fn session_file_format(path: &Path) -> Option<SessionFileFormat> {
let extension = path.extension().and_then(|extension| extension.to_str())?;
if extension.eq_ignore_ascii_case(JSONL_EXTENSION) {
return Some(SessionFileFormat::Plain);
}
if extension.eq_ignore_ascii_case(&COMPRESSED_SUFFIX[1..])
&& path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| ends_with_ignore_ascii_case(name, COMPRESSED_JSONL_SUFFIX))
{
return Some(SessionFileFormat::Compressed);
}
None
}
#[cfg(test)]
pub(in crate::app) fn is_session_file_path(path: &Path) -> bool {
session_file_format(path).is_some()
}
pub(in crate::app) fn should_skip_format(path: &Path, file_format: SessionFileFormat) -> bool {
file_format == SessionFileFormat::Compressed && plain_session_path(path).is_file()
}
pub(in crate::app) fn plain_session_path(path: &Path) -> PathBuf {
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
return path.to_path_buf();
};
if !ends_with_ignore_ascii_case(file_name, COMPRESSED_SUFFIX) {
return path.to_path_buf();
}
let plain_name = &file_name[..file_name.len() - COMPRESSED_SUFFIX.len()];
path.with_file_name(plain_name)
}
pub(in crate::app) fn compressed_session_path(path: &Path) -> PathBuf {
if session_file_format(path) == Some(SessionFileFormat::Compressed) {
return path.to_path_buf();
}
let mut file_name = path.file_name().map_or_else(
|| std::ffi::OsStr::new("session.jsonl").to_os_string(),
std::ffi::OsStr::to_os_string,
);
file_name.push(COMPRESSED_SUFFIX);
path.with_file_name(file_name)
}
pub(in crate::app) fn existing_session_path(logical_path: &Path) -> Result<Option<PathBuf>> {
let plain_path = plain_session_path(logical_path);
match fs::metadata(&plain_path) {
Ok(metadata) if metadata.is_file() => return Ok(Some(plain_path)),
Ok(_) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => {
return Err(error).wrap_err_with(|| {
format!("failed to access session file {}", plain_path.display())
});
}
}
let compressed_path = compressed_session_path(&plain_path);
match fs::metadata(&compressed_path) {
Ok(metadata) if metadata.is_file() => Ok(Some(compressed_path)),
Ok(_) => Ok(None),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error).wrap_err_with(|| {
format!(
"failed to access compressed session file {}",
compressed_path.display()
)
}),
}
}
fn ends_with_ignore_ascii_case(value: &str, suffix: &str) -> bool {
value
.get(value.len().saturating_sub(suffix.len())..)
.is_some_and(|tail| tail.eq_ignore_ascii_case(suffix))
}