Handle pre-epoch file timestamps consistently and record Rust audit

This commit is contained in:
karl committed 2026-10-13 17:02:16 +02:00
1 parent 59b2fc39b2
commit 14adbbc616
7 files changed
+262 -46

No files matched your search

+3 -10
View File
@@ -21,7 +21,6 @@ use std::{
fs,
path::{Path, PathBuf},
sync::atomic::{AtomicBool, Ordering},
time::UNIX_EPOCH,
};
use walkdir::WalkDir;
@@ -657,12 +656,7 @@ impl SearchIndex {
report.discovered += 1;
let metadata = entry.metadata()?;
let size = i64::try_from(metadata.len()).context("file exceeds supported size")?;
let modified = metadata
.modified()?
.duration_since(UNIX_EPOCH)
.context("file timestamp predates Unix epoch")?;
let mtime = i64::try_from(modified.as_nanos())
.context("file timestamp exceeds supported range")?;
let mtime = searchi_media::unix_time_ns(metadata.modified()?)?;
let extension = entry
.path()
.extension()
@@ -1257,7 +1251,7 @@ impl SearchIndex {
pub fn file_signature(&self, path: &str) -> Result<Option<(u64, i64)>> {
self.connection
.query_row(
"SELECT size,mtime FROM files WHERE path=?1 AND size>=0 AND mtime>=0",
"SELECT size,mtime FROM files WHERE path=?1 AND size>=0",
[path],
|row| Ok((row.get(0)?, row.get(1)?)),
)
@@ -1294,9 +1288,8 @@ impl SearchIndex {
Ok(value) if value.is_file() => value,
_ => return Ok(None),
};
let mtime = metadata.modified()?.duration_since(UNIX_EPOCH)?;
let size = i64::try_from(metadata.len())?;
let modified = i64::try_from(mtime.as_nanos())?;
let modified = searchi_media::unix_time_ns(metadata.modified()?)?;
let Some(expected_hash) = self.extracted_source_hash(path)? else {
return Ok(None);
};
@@ -90,3 +90,40 @@ fn oversized_text_and_unsupported_files_remain_filename_only() -> Result<()> {
assert_eq!(index.search("large.txt", 10)?.len(), 1);
Ok(())
}
#[test]
fn pre_epoch_source_remains_searchable_and_verifiable() -> Result<()> {
let directory = tempfile::tempdir()?;
let path = directory.path().join("archived-notes.txt");
let content = "Historical supplier cancellation notice\n";
fs::write(&path, content)?;
fs::File::options().write(true).open(&path)?.set_times(
FileTimes::new().set_modified(std::time::UNIX_EPOCH - std::time::Duration::from_secs(1)),
)?;
let mut index = SearchIndex::open(":memory:")?;
let report = index.index_folder(directory.path())?;
assert_eq!(report.indexed, 1);
assert_eq!(report.failures, 0);
assert_eq!(index.search("archived-notes.txt", 10)?.len(), 1);
assert_eq!(index.search("cancellation", 10)?.len(), 1);
let passage = index.passage_batch(0, 1)?.remove(0);
assert_eq!(
index.file_signature(&passage.path)?,
Some((content.len() as u64, -1_000_000_000))
);
assert_eq!(
index.extracted_source_hash(&passage.path)?,
Some(format!("{:x}", Sha256::digest(content.as_bytes())))
);
assert_eq!(
index
.source_passage(&passage.path, &passage.source, 8192)?
.unwrap()
.text,
content
);
let rescan = index.index_folder(directory.path())?;
assert_eq!(rescan.unchanged, 1);
assert_eq!(rescan.indexed, 0);
Ok(())
}
+39 -3
View File
@@ -14,7 +14,7 @@ use std::{
path::{Path, PathBuf},
process::{Command, Stdio},
thread,
time::{Duration, Instant, UNIX_EPOCH},
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
@@ -28,6 +28,26 @@ pub enum MediaKind {
mod tests {
use super::*;
#[test]
fn signed_unix_time_preserves_epoch_and_storage_boundaries() -> Result<()> {
// Windows SystemTime has 100 ns precision; storage conversion itself
// must still handle every signed nanosecond boundary exactly.
assert_eq!(unix_time_ns(UNIX_EPOCH)?, 0);
assert_eq!(unix_time_ns(UNIX_EPOCH + Duration::from_nanos(100))?, 100);
assert_eq!(unix_time_ns(UNIX_EPOCH - Duration::from_nanos(100))?, -100);
assert_eq!(
signed_duration_ns(Duration::from_nanos(i64::MAX as u64), false)?,
i64::MAX
);
assert_eq!(
signed_duration_ns(Duration::from_nanos(i64::MAX as u64 + 1), true)?,
i64::MIN
);
assert!(signed_duration_ns(Duration::from_nanos(i64::MAX as u64 + 1), false).is_err());
assert!(signed_duration_ns(Duration::from_nanos(i64::MAX as u64 + 2), true).is_err());
Ok(())
}
fn fixture(duration_ms: u32) -> MediaProbe {
MediaProbe {
duration_ms,
@@ -414,9 +434,25 @@ impl MediaIdentity {
Ok(())
}
}
/// Convert a filesystem timestamp to signed nanoseconds since the Unix epoch.
/// Times outside the signed 64-bit storage range are rejected explicitly.
pub fn unix_time_ns(time: SystemTime) -> Result<i64> {
match time.duration_since(UNIX_EPOCH) {
Ok(duration) => signed_duration_ns(duration, false),
Err(error) => signed_duration_ns(error.duration(), true),
}
}
fn signed_duration_ns(duration: Duration, before_epoch: bool) -> Result<i64> {
// Duration's u64 seconds fit in i128 nanoseconds, including i64::MIN's
// magnitude, which cannot first be represented as positive i64.
let nanos = duration.as_nanos() as i128;
i64::try_from(if before_epoch { -nanos } else { nanos })
.context("File timestamp exceeds supported nanosecond range")
}
fn modified_ns(metadata: &fs::Metadata) -> Result<i64> {
i64::try_from(metadata.modified()?.duration_since(UNIX_EPOCH)?.as_nanos())
.context("Unsupported media timestamp")
unix_time_ns(metadata.modified()?)
}
#[derive(Debug, Clone, Serialize)]
+80 -28
View File
@@ -778,34 +778,30 @@ impl Store {
))
})?;
if let (Some(size), Some(mtime)) = (size, mtime) {
let valid = *filesystem_validity
.entry(result.path.clone())
.or_insert_with(|| {
#[cfg(test)]
MEDIA_STAT_CALLS.with(|count| count.set(count.get() + 1));
let validation_span = trace.begin(
"semantic.parent_filesystem_validation",
materialization_span,
);
let valid =
std::fs::metadata(&result.path)
.ok()
.is_some_and(|metadata| {
metadata.is_file()
&& i64::try_from(metadata.len()).ok() == Some(size)
&& metadata
.modified()
.ok()
.and_then(|t| {
t.duration_since(std::time::UNIX_EPOCH).ok()
})
.and_then(|d| i64::try_from(d.as_nanos()).ok())
== Some(mtime)
});
trace.counter(validation_span, "valid", u64::from(valid));
trace.end(validation_span, Default::default());
valid
});
let valid =
*filesystem_validity
.entry(result.path.clone())
.or_insert_with(|| {
#[cfg(test)]
MEDIA_STAT_CALLS.with(|count| count.set(count.get() + 1));
let validation_span = trace.begin(
"semantic.parent_filesystem_validation",
materialization_span,
);
let valid =
std::fs::metadata(&result.path)
.ok()
.is_some_and(|metadata| {
metadata.is_file()
&& i64::try_from(metadata.len()).ok() == Some(size)
&& metadata.modified().ok().and_then(|t| {
searchi_media::unix_time_ns(t).ok()
}) == Some(mtime)
});
trace.counter(validation_span, "valid", u64::from(valid));
trace.end(validation_span, Default::default());
valid
});
if !valid {
continue;
}
@@ -1141,6 +1137,62 @@ mod tests {
Ok(())
}
#[test]
fn pre_epoch_media_parent_remains_eligible_for_semantic_results() -> Result<()> {
let directory = tempfile::tempdir()?;
let root = directory.path().join("media");
std::fs::create_dir(&root)?;
let path = root.join("archived.mp4");
std::fs::write(&path, b"bounded media identity fixture")?;
std::fs::File::options()
.write(true)
.open(&path)?
.set_times(
std::fs::FileTimes::new()
.set_modified(std::time::UNIX_EPOCH - std::time::Duration::from_secs(1)),
)?;
let db = directory.path().join("index.db");
let mut lexical = SearchIndex::open(&db)?;
lexical.index_folder(&root)?;
let mut store = Store::open(&db, DIMENSIONS)?;
let generation = store.begin("pre-epoch-media-proof")?;
let mut query = vec![0.; DIMENSIONS];
query[0] = 1.;
let (id, indexed_path, _, _) = store.media_batch(0)?.remove(0);
let identity = searchi_media::MediaIdentity::read(Path::new(&indexed_path), 1024)?;
assert_eq!(identity.modified_ns, -1_000_000_000);
store.insert_media(generation, id, 0, &identity, "video", 0, &query)?;
store.activate(generation)?;
let mut trace = TraceCollector::new(MetricsMode::Basic);
let results = store.search_origin_traced(
&query,
"pre-epoch-media-proof",
10,
&SearchOptions::default(),
None,
&mut trace,
None,
)?;
assert_eq!(results.len(), 1);
assert_eq!(results[0].path, indexed_path);
// Timestamp changes still invalidate the prepared media source.
std::fs::File::options()
.write(true)
.open(&path)?
.set_times(std::fs::FileTimes::new().set_modified(std::time::UNIX_EPOCH))?;
let results = store.search_origin_traced(
&query,
"pre-epoch-media-proof",
10,
&SearchOptions::default(),
None,
&mut trace,
None,
)?;
assert!(results.is_empty());
Ok(())
}
#[test]
fn many_video_windows_do_not_crowd_out_other_parent_files() -> Result<()> {
// Geometry verifies grouped retrieval, not model inference or codecs.