From c9fb07866b6ac9e00af940652cd6dd0d76fde880 Mon Sep 17 00:00:00 2001 From: "C. Titus Brown" Date: Mon, 4 Nov 2024 08:57:01 -0800 Subject: [PATCH] MRG: panic when `FSStorage::load_sig` encounters more than one `Signature` in a JSON record (#3333) This PR was originally about debugging https://github.com/sourmash-bio/sourmash_plugin_branchwater/pull/445, but that's going to require more work to fix properly. For now, I would like to nominate it for merge because sourmash fails silently in this situation, and that's Bad. In brief, the main thing this PR does is panic with an `unimplemented!` when `FSStorage::load_sig` encounters more than one `Signature` in a JSON record. This PR also adds a bit of documentation to `InnerStorage`, per the bottom of [this comment](https://github.com/sourmash-bio/sourmash_plugin_branchwater/pull/445#issuecomment-2374546853). --- The problem at hand: when loading a `SigStore`/`Signature` from a `Storage`, sourmash only loads the first one and ignores any others. https://github.com/sourmash-bio/sourmash/blob/26b50f3e3566006fd6356a4f8b4d47c5e381aeec/src/core/src/storage/mod.rs#L34-L38 This results from the concept of a `Signature` as containing one or more sketches; the history of this is described [here](https://github.com/sourmash-bio/sourmash/issues/616#issuecomment-671076716), and it leads to some interesting silliness [in the Python layer](https://github.com/sourmash-bio/sourmash/blob/d63c464e825529fa54bb7e8b81faa53b858b09de/src/sourmash/save_load.py#L297). The contrapositive is that, in Rust, a single `Signature` can include multiple sketches, e.g. with different ksizes. So this works fine for the wort case where we have a single `.sig` file with k=21, k=31, k51. Note that the Python layer (and hence the entire sourmash CLI) fully supports multiple `Signature`s in JSON: this is well tested and well covered behavior. The branchwater plugin runs into it because it is using the Rust layer and the API is not fully fleshed out there. --- --- src/core/src/collection.rs | 17 +++++++++++++++++ src/core/src/storage/mod.rs | 35 ++++++++++++++++++++++++++--------- tests/test-data/short.sig.gz | Bin 0 -> 10995 bytes 3 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 tests/test-data/short.sig.gz diff --git a/src/core/src/collection.rs b/src/core/src/collection.rs index 9ad2d891bc..28659df05f 100644 --- a/src/core/src/collection.rs +++ b/src/core/src/collection.rs @@ -416,6 +416,23 @@ mod test { } } + #[test] + #[should_panic] // for now... + fn sigstore_sig_from_record_2() { + let mut filename = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + filename.push("../../tests/test-data/short.sig.gz"); + let v = [filename]; + let collection = Collection::from_paths(&v).expect("no sigs!?"); + + // pull off first record + let v: Vec<_> = collection.iter().collect(); + let (_idx, rec) = v.first().expect("no records in collection?!"); + + // this will panic with "unimplemented" because there are two + // sketches and that is not supported. + let _first_sig = collection.sig_from_record(rec).expect("no sig!?"); + } + #[test] fn sigstore_selection_moltype_zip() { // load test sigs diff --git a/src/core/src/storage/mod.rs b/src/core/src/storage/mod.rs index 536ec52926..e098f58eba 100644 --- a/src/core/src/storage/mod.rs +++ b/src/core/src/storage/mod.rs @@ -34,9 +34,11 @@ pub trait Storage { /// Load signature from internal path fn load_sig(&self, path: &str) -> Result { let raw = self.load(path)?; - let sig = Signature::from_reader(&mut &raw[..])? - // TODO: select the right sig? - .swap_remove(0); + let mut vs = Signature::from_reader(&mut &raw[..])?; + if vs.len() > 1 { + unimplemented!("only one Signature currently allowed"); + } + let sig = vs.swap_remove(0); Ok(sig.into()) } @@ -70,6 +72,16 @@ pub enum StorageError { MissingFeature(String, String), } +/// InnerStorage: a catch-all type that allows using any Storage in +/// parallel contexts. +/// +/// Arc allows ref counting to share it between threads; +/// RwLock makes sure there is only one writer possible (and a lot of readers); +/// dyn Storage so we can init with anything that implements the Storage trait. + +// Send + Sync + 'static is kind of a cheat to avoid lifetimes issues: we +// should get rid of that 'static if possible... -- Luiz. + #[derive(Clone)] pub struct InnerStorage(Arc>); @@ -299,9 +311,12 @@ impl Storage for FSStorage { fn load_sig(&self, path: &str) -> Result { let raw = self.load(path)?; - let sig = Signature::from_reader(&mut &raw[..])? - // TODO: select the right sig? - .swap_remove(0); + + let mut vs = Signature::from_reader(&mut &raw[..])?; + if vs.len() > 1 { + unimplemented!("only one Signature currently allowed when using 'load_sig'"); + } + let sig = vs.swap_remove(0); Ok(sig.into()) } @@ -369,9 +384,11 @@ impl Storage for ZipStorage { fn load_sig(&self, path: &str) -> Result { let raw = self.load(path)?; - let sig = Signature::from_reader(&mut &raw[..])? - // TODO: select the right sig? - .swap_remove(0); + let mut vs = Signature::from_reader(&mut &raw[..])?; + if vs.len() > 1 { + unimplemented!("only one Signature currently allowed"); + } + let sig = vs.swap_remove(0); Ok(sig.into()) } diff --git a/tests/test-data/short.sig.gz b/tests/test-data/short.sig.gz new file mode 100644 index 0000000000000000000000000000000000000000..84047034dc965758f24d9889e8f54b813fe40ef5 GIT binary patch literal 10995 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