use super::*;
pub(super) const REDACTED_PATH: &str = "[redacted]";
fn secret_like_path_case_insensitive(path: &Path) -> bool {
path.file_name()
.and_then(OsStr::to_str)
.is_some_and(|name| {
name.eq_ignore_ascii_case("auth.json")
|| name.eq_ignore_ascii_case("id_rsa")
|| name.eq_ignore_ascii_case("id_ed25519")
|| name
.get(..4)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(".env"))
|| name
.get(name.len().saturating_sub(4)..)
.is_some_and(|suffix| suffix.eq_ignore_ascii_case(".pem"))
|| name
.get(name.len().saturating_sub(4)..)
.is_some_and(|suffix| suffix.eq_ignore_ascii_case(".key"))
})
}
fn secret_path_for_rewind(path: &Path) -> bool {
secret_like_path(path) || secret_like_path_case_insensitive(path)
}
fn ascii_case_insensitive_component_eq(left: &OsStr, right: &OsStr) -> bool {
match (left.to_str(), right.to_str()) {
(Some(left), Some(right)) => left.eq_ignore_ascii_case(right),
_ => left == right,
}
}
fn path_starts_with_ascii_case_insensitive(path: &Path, prefix: &Path) -> bool {
let mut path_components = path.components();
for prefix_component in prefix.components() {
let Some(path_component) = path_components.next() else {
return false;
};
if !ascii_case_insensitive_component_eq(
path_component.as_os_str(),
prefix_component.as_os_str(),
) {
return false;
}
}
true
}
pub(super) fn redacted_relative_path(
reason: &SnapshotCaptureStatus,
relative_path: PathBuf,
) -> PathBuf {
if matches!(
reason,
SnapshotCaptureStatus::Excluded {
reason: super::SnapshotExclusionReason::SecretPath
}
) || secret_path_for_rewind(&relative_path)
{
PathBuf::from(REDACTED_PATH)
} else {
relative_path
}
}
pub(crate) fn redacted_path_for_display(path: &Path) -> String {
safe_provider_path(&path.to_string_lossy())
}
pub(super) fn safe_rewind_target_path(
store: &CheckpointStore,
cwd: &Path,
relative_path: &Path,
) -> Result<(), &'static str> {
if relative_path.is_absolute() || relative_path.as_os_str().is_empty() {
return Err("denied path");
}
let mut has_normal_component = false;
for component in relative_path.components() {
match component {
Component::Normal(name) => {
has_normal_component = true;
if name
.to_str()
.is_some_and(|name| name.eq_ignore_ascii_case(".git"))
{
return Err("denied path");
}
}
Component::CurDir => {}
Component::ParentDir | Component::Prefix(_) | Component::RootDir => {
return Err("denied path");
}
}
}
if !has_normal_component || secret_path_for_rewind(relative_path) {
return Err("denied path");
}
let root = lexical_normalize(cwd);
let path = lexical_normalize(&root.join(relative_path));
if !path_starts_with_ascii_case_insensitive(&path, &root) || path == root {
return Err("denied path");
}
let checkpoint_root = store
.root
.canonicalize()
.unwrap_or_else(|_| lexical_normalize(&store.root));
if path_starts_with_ascii_case_insensitive(&path, &checkpoint_root) {
return Err("denied path");
}
if let Some(mc_root) = checkpoint_root.parent() {
let session_root = lexical_normalize(&mc_root.join("sessions"));
if path_starts_with_ascii_case_insensitive(&path, &session_root) {
return Err("denied path");
}
}
Ok(())
}
pub(super) fn safe_provider_path(path: &str) -> String {
let sanitized = redact_sensitive_text(path);
if sanitized.contains("..")
|| sanitized.starts_with('/')
|| secret_path_for_rewind(Path::new(&sanitized))
{
"<redacted>".to_string()
} else {
sanitized
}
}