-
Notifications
You must be signed in to change notification settings - Fork 14
Check integrity early for smaller files #164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| use std::{ | ||
| collections::HashSet, | ||
| fs::File, | ||
| io::{BufRead, BufReader, BufWriter, Error, ErrorKind, Read, Write}, | ||
| io::{BufRead, BufReader, BufWriter, Cursor, Error, ErrorKind, Read, Write}, | ||
| path::{Path, PathBuf}, | ||
| sync::{Arc, Mutex}, | ||
| }; | ||
|
|
@@ -300,8 +300,35 @@ impl<T: Read> std::ops::Drop for ChecksumReader<T> { | |
| } | ||
|
|
||
| /// Open the file pointed to by `path` as a `Box<dyn Read>`. If the file does not exist, look for | ||
| /// compressed versions. | ||
| fn open_file(path: PathBuf) -> Option<Box<dyn Read>> { | ||
| /// compressed versions. If `early_check` is true, we check the checksum before returning the file. | ||
| fn open_file(path: PathBuf, early_check: bool) -> Option<Box<dyn Read>> { | ||
| fn do_early_check<T: Read>(path: PathBuf, mut reader: T) -> Option<Box<dyn Read>> { | ||
| let mut file_contents = Vec::new(); | ||
| let num_bytes = std::io::copy(&mut reader, &mut file_contents) | ||
| .unwrap_or_else(|e| panic!("Error when reading from {path:?}: {e}")); | ||
| if num_bytes < 4 { | ||
| tracing::warn!("File {path:?} is too short to contain a checksum. Deleting file."); | ||
| std::fs::remove_file(&path) | ||
| .unwrap_or_else(|e| panic!("Error when deleting {path:?}: {e}")); | ||
| return None; | ||
| } | ||
|
|
||
| let checksum_pos = num_bytes as usize - 4; | ||
| let (content_bytes, mut check_bytes) = file_contents.split_at(checksum_pos); | ||
| let mut adler = adler::Adler32::new(); | ||
| adler.write_slice(content_bytes); // Everything except the 32-bit checksum | ||
| let checksum = check_bytes.read_u32::<LittleEndian>().unwrap(); | ||
|
|
||
| if adler.checksum() == checksum { | ||
| Some(Box::new(Cursor::new(file_contents))) | ||
| } else { | ||
| tracing::warn!("Checksum mismatch for {path:?}. Deleting file."); | ||
| std::fs::remove_file(&path) | ||
| .unwrap_or_else(|e| panic!("Error when deleting {path:?}: {e}")); | ||
| None | ||
| } | ||
| } | ||
|
|
||
| // We should try in decreasing order of access speed. | ||
| match File::open(&path) { | ||
| Ok(f) => { | ||
|
|
@@ -316,7 +343,11 @@ fn open_file(path: PathBuf) -> Option<Box<dyn Read>> { | |
| .unwrap_or_else(|e| panic!("Error when deleting empty file {path:?}: {e}")); | ||
| return None; | ||
| } | ||
| return Some(Box::new(ChecksumReader::new(reader))); | ||
| return if early_check { | ||
| do_early_check(path, reader) | ||
| } else { | ||
| Some(Box::new(ChecksumReader::new(reader))) | ||
| }; | ||
| } | ||
| Err(e) => { | ||
| if e.kind() != ErrorKind::NotFound { | ||
|
|
@@ -331,9 +362,12 @@ fn open_file(path: PathBuf) -> Option<Box<dyn Read>> { | |
| path.set_extension("zst"); | ||
| match File::open(&path) { | ||
| Ok(f) => { | ||
| return Some(Box::new(ChecksumReader::new( | ||
| zstd::stream::Decoder::new(f).unwrap(), | ||
| ))) | ||
| let reader = zstd::stream::Decoder::new(f).unwrap(); | ||
| return if early_check { | ||
| do_early_check(path, reader) | ||
| } else { | ||
| Some(Box::new(ChecksumReader::new(reader))) | ||
| }; | ||
| } | ||
| Err(e) => { | ||
| if e.kind() != ErrorKind::NotFound { | ||
|
|
@@ -399,6 +433,17 @@ impl<A: Algebra> SaveFile<A> { | |
| Ok(()) | ||
| } | ||
|
|
||
| /// Whether we should load the file in memory and check the checksum before returning it. This | ||
| /// only returns false for quasi-inverses because they are our largest files by far. This is a | ||
| /// function of `SaveFile` and not just `SaveKind` because we may want to change the behavior | ||
| /// depending on the stem or some other heuristic. | ||
| fn should_check_early(&self) -> bool { | ||
| !matches!( | ||
| self.kind, | ||
| SaveKind::AugmentationQi | SaveKind::NassauQi | SaveKind::ResQi | ||
| ) | ||
| } | ||
|
Comment on lines
+440
to
+445
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes sense as an improvement over the status quo but there may be a better design of the file format that would allow checking only the section of the file that is used? For instance you could place a checksum every 4kb or imitate the zip file format. Another possibility is there might be an existing container format that is appropriate and that could make the checksums transparent to our code.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I looked around and didn't see a generic container that would do that for us, but one interesting option would be to always output zstd-compressed data. Zstd has options to do frame-level and block-level checksumming, and we already support reading compressed files. Also, I just checked with our stem 200 data and, apart from the quasi-inverses, the biggest files only take a few MB, with the vast majority taking less than 1 kB. In fact, the first thing that the code does after opening those files is reading the entire contents, so we're really just prefetching |
||
|
|
||
| /// This panics if there is no save dir | ||
| fn get_save_path(&self, mut dir: PathBuf) -> PathBuf { | ||
| if let Some(idx) = self.idx { | ||
|
|
@@ -422,7 +467,7 @@ impl<A: Algebra> SaveFile<A> { | |
| pub fn open_file(&self, dir: PathBuf) -> Option<Box<dyn Read>> { | ||
| let file_path = self.get_save_path(dir); | ||
| let path_string = file_path.to_string_lossy().into_owned(); | ||
| if let Some(mut f) = open_file(file_path) { | ||
| if let Some(mut f) = open_file(file_path, self.should_check_early()) { | ||
| self.validate_header(&mut f).unwrap(); | ||
| tracing::info!("success open_read: {}", path_string); | ||
| Some(f) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps you should instead move these to a corrupted files directory? It might be useful to be able to look at them to find out what went wrong, this just destroys the evidence.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, that sounds like a good idea. Or maybe rename them with a ".old" suffix, and silently delete the previous .old version if there is one