quitters-0.1.0/.cargo_vcs_info.json0000644000000001460000000000100127130ustar { "git": { "sha1": "5c458487b2df2c9afefccd91b2813959e1da88d2" }, "path_in_vcs": "quitters" }quitters-0.1.0/Cargo.lock0000644000000026620000000000100106730ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" dependencies = [ "memchr", ] [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "once_cell" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "quitters" version = "0.1.0" dependencies = [ "once_cell", "regex", "semver", ] [[package]] name = "regex" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "semver" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" quitters-0.1.0/Cargo.toml0000644000000017000000000000100107060ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "quitters" version = "0.1.0" authors = ["Sergey \"Shnatsel\" Davidoff"] description = "List dependencies of a Rust binary by parsing panic messages" categories = ["parsing"] license = "MIT OR Apache-2.0" repository = "https://github.com/rustsec/rustsec" resolver = "1" [dependencies.once_cell] version = "1.15.0" [dependencies.regex] version = "1.6.0" features = [ "std", "perf", ] default-features = false [dependencies.semver] version = "1.0.14" quitters-0.1.0/Cargo.toml.orig000064400000000000000000000010111046102023000143620ustar 00000000000000[package] name = "quitters" version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" authors = ["Sergey \"Shnatsel\" Davidoff"] description = "List dependencies of a Rust binary by parsing panic messages" repository = "https://github.com/rustsec/rustsec" categories = ["parsing"] [dependencies] once_cell = "1.15.0" # we don't need unicode support in regex, so drop it to reduce binary size and attack surface regex = { version = "1.6.0", default-features = false, features = ["std", "perf"] } semver = "1.0.14" quitters-0.1.0/examples/from_file.rs000064400000000000000000000004511046102023000156300ustar 00000000000000fn main() -> Result<(), Box> { let path = std::env::args_os().nth(1).unwrap(); let file = std::fs::read(path)?; let versions = quitters::versions(&file); for (krate, version) in versions.iter() { println!("{} v{}", krate, version) } Ok(()) } quitters-0.1.0/src/lib.rs000064400000000000000000000150511046102023000134070ustar 00000000000000#![forbid(unsafe_code)] //! Obtains the dependency list from a compiled Rust binary by parsing its panic messages. //! Recovers both crate names and versions. //! //! ## Caveats //! * If the crate never panics, it will not show up. //! The Rust compiler is very good at removing unreachable panics, //! so we can only discover at around a half of all dependencies. //! * C code such as `openssl-src` never shows up, because it can't panic. //! * Only crates installed from a registry are discovered. Crates from local workspace or git don't show up. //! //! # Alternatives //! [`cargo auditable`](https://crates.io/crates/cargo-auditable) embeds the **complete** dependency information //! into binaries, which can then be recovered using [`auditable-info`](https://crates.io/crates/auditable-info). //! It should be used instead of `quitters` whenever possible, unless you're specifically after panics. use std::collections::BTreeSet; use once_cell::sync::OnceCell; use regex::bytes::Regex; use semver::Version; // This regex works suprisingly well. We can even split the crate name and version reliably // because crate names publishable on crates.io cannot contain the `.` character, // which *must* appear in the version string. // Versions like "1" are not valid in Cargo, or under the semver spec. const REGEX_STRING: &str = "(?-u)cargo/registry/src/[^/]+/(?P[0-9A-Za-z_-]+)-(?P[0-9]+\\.[0-9]+\\.[0-9]+[0-9A-Za-z+.-]*)/"; // Compiled regular expressions use interior mutability and may cause contention // in heavily multi-threaded workloads. This should not be an issue here // because we only use `.captures_iter()`, which acquires the mutable state // only once per invocation and for a short amount of time: // https://github.com/rust-lang/regex/blob/0d0023e412f7ead27b0809f5d2f95690d0f0eaef/PERFORMANCE.md#using-a-regex-from-multiple-threads // This could be refactored into cloning in case it *does* end up being a bottleneck in practice, // which would sacrifice ergonomics. static REGEX_UNIX: OnceCell = OnceCell::new(); static REGEX_WINDOWS: OnceCell = OnceCell::new(); /// Obtains the dependency list from a compiled Rust binary by parsing its panic messages. /// /// ## Caveats /// * If the crate never panics, it will not show up. /// The Rust compiler is very good at removing unreachable panics, /// so we can only discover at around a half of all dependencies. /// * C code such as `openssl-src` never shows up, because it can't panic. /// * Only crates installed from a registry are discovered. Crates from local workspace or git don't show up. /// /// ## Usage /// ```rust,ignore /// let file = std::fs::read("target/release/my-program")?; /// let versions = quitters::versions(&file); /// for (krate, version) in versions.iter() { /// println!("{krate} v{version}") /// } /// ``` pub fn versions(data: &[u8]) -> BTreeSet<(&str, Version)> { // You might think that just making two functions, versions_unix and versions_windows // and then calling the appropriate function for your platform would be faster, // since \ paths cannot be used on Unix. I briefly thought so! // However, cross-compilation from Windows to Unix would put \ paths into a Unix binary. // So that optimization would miss cross-compiled binaries. // It only gets you a 20% reduction in runtime because the I/O dominates anyway. // // A significant optimization to tackle the I/O problem would be only ever reading things // into the CPU cache as opposed to loading the entire file to memory. // Basically streaming the data. This requires special handling of the start and end, // so either needs a state-machine-based parser like nom or capping the possible match length. // The latter is doable but only makes sense if it turns out that the current approach is too slow. let re = REGEX_UNIX.get_or_init(|| Regex::new(REGEX_STRING).unwrap()); let versions = versions_for_regex(data, re); if !versions.is_empty() { versions } else { // Sadly the single-pass RegexSet only lets you check for presence of matches, // and doesn't let you find out where they are. // And using a composite regex like `unix_regex|windows_regex` is as slow as two passes, // so we'll just use two passes. That's what Regex crate documentation recommends, too. let re = REGEX_WINDOWS.get_or_init(|| { let windows_regex = REGEX_STRING.replace('/', "\\\\"); Regex::new(&windows_regex).unwrap() }); versions_for_regex(data, re) } } fn versions_for_regex<'a>(data: &'a [u8], re: &Regex) -> BTreeSet<(&'a str, Version)> { let mut versions = BTreeSet::new(); for c in re.captures_iter(data) { if let Some(parsed) = parse_capture(c) { versions.insert(parsed); } } versions } /// Extracts crate and version from a single regex match fn parse_capture(c: regex::bytes::Captures) -> Option<(&str, Version)> { Some(( std::str::from_utf8(c.name("crate").unwrap().as_bytes()).ok()?, Version::parse(std::str::from_utf8(c.name("version").unwrap().as_bytes()).ok()?).ok()?, )) } #[cfg(test)] mod tests { use super::*; #[test] fn two_crates_one_line() { let data = b"\x7FELF/cargo/registry/src/github.com-1ecc6299db9ec823/xz2-0.1.6/src/stream.rsunknown return code: lzma data errorNoCheckProgramMemFormatOptionszstd returned null pointer when creating new context/cargo/registry/src/github.com-1ecc6299db9ec823/zstd-safe-5.0.2+zstd.1.5.2/src/lib.rsbad error message from zstdGiven position outside of the buffer bounds."; assert_eq!(versions(data).len(), 2); } #[test] fn complex_versions() { for version_suffix in [ "", "+foobar", "+Fo0bar", "+zstd.1.5.2", "-rc", "-alpha.1", "-alpha.1+zstd.1.5.2", ] { let string = format!("new context/cargo/registry/src/github.com-1ecc6299db9ec823/zstd-safe-5.0.2{}/src/lib.rsbad error message from zstdGiven position outside of the buffer bounds.", version_suffix); let expected_version = format!("5.0.2{}", version_suffix); assert!(versions(string.as_bytes()) .contains(&("zstd-safe", Version::parse(&expected_version).unwrap()))); } } #[test] fn windows_matching() { let data = br"C:\Users\runneradmin\.cargo\registry\src\github.com-1ecc6299db9ec823\rustc-demangle-0.1.21\src\legacy.rs"; assert!(versions(data).contains(&("rustc-demangle", Version::parse("0.1.21").unwrap()))) } }