diff --git a/.github/dix.png b/.github/dix.png deleted file mode 100644 index 17b337f..0000000 Binary files a/.github/dix.png and /dev/null differ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..a4f8b89 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Cargo Build & Test + +on: + push: + branches: [ $default-branch ] + pull_request: + branches: [ $default-branch ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build_and_test: + name: dix - latest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: cargo build --verbose + - run: cargo test --verbose + diff --git a/.gitignore b/.gitignore index 6a2460c..1afea7e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,3 @@ /.direnv /target - - -# Added by cargo -# -# already existing elements were commented out - -#/target +/result diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..df184f2 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,30 @@ +# Taken from https://github.com/cull-os/carcass. +# Modified to have 2 space indents and 80 line width. + +# float_literal_trailing_zero = "Always" # TODO: Warning for some reason? +condense_wildcard_suffixes = true +doc_comment_code_block_width = 80 +edition = "2024" # Keep in sync with Cargo.toml. +enum_discrim_align_threshold = 60 +force_explicit_abi = false +force_multiline_blocks = true +format_code_in_doc_comments = true +format_macro_matchers = true +format_strings = true +group_imports = "StdExternalCrate" +hex_literal_case = "Upper" +imports_granularity = "Crate" +imports_layout = "Vertical" +inline_attribute_width = 60 +match_block_trailing_comma = true +max_width = 80 +newline_style = "Unix" +normalize_comments = true +normalize_doc_attributes = true +overflow_delimited_expr = true +struct_field_align_threshold = 60 +tab_spaces = 2 +unstable_features = true +use_field_init_shorthand = true +use_try_shorthand = true +wrap_comments = true diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 0000000..9abeaee --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,15 @@ +# Taken from https://github.com/cull-os/carcass. + +[formatting] +align_entries = true +column_width = 100 +compact_arrays = false +reorder_inline_tables = true +reorder_keys = true + +[[rule]] +include = [ "**/Cargo.toml" ] +keys = [ "package" ] + +[rule.formatting] +reorder_keys = false diff --git a/Cargo.lock b/Cargo.lock index 52df5be..b91736c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,27 +62,10 @@ dependencies = [ ] [[package]] -name = "atty" -version = "0.2.14" +name = "anyhow" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "bitflags" @@ -90,18 +73,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" -[[package]] -name = "bumpalo" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - [[package]] name = "cc" version = "1.2.21" @@ -111,23 +82,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "bitflags 1.3.2", - "textwrap", - "unicode-width", -] - [[package]] name = "clap" version = "4.5.37" @@ -138,6 +92,16 @@ dependencies = [ "clap_derive", ] +[[package]] +name = "clap-verbosity-flag" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84" +dependencies = [ + "clap", + "log", +] + [[package]] name = "clap_builder" version = "4.5.37" @@ -175,85 +139,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] -name = "criterion" -version = "0.3.6" +name = "convert_case" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b01d6de93b2b6c65e17c634a26653a29d107b3c98c607c765bf38d041531cd8f" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" dependencies = [ - "atty", - "cast", - "clap 2.34.0", - "criterion-plot", - "csv", - "itertools", - "lazy_static", - "num-traits", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_cbor", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", + "unicode-segmentation", ] [[package]] -name = "criterion-plot" -version = "0.4.5" +name = "derive_more" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" dependencies = [ - "cast", - "itertools", + "derive_more-impl", ] [[package]] -name = "crossbeam-deque" -version = "0.8.6" +name = "derive_more-impl" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "csv" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "csv-core" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" -dependencies = [ - "memchr", + "convert_case", + "proc-macro2", + "quote", + "syn", + "unicode-xid", ] [[package]] @@ -266,15 +179,18 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" name = "dix" version = "0.1.0" dependencies = [ - "clap 4.5.37", - "criterion", + "anyhow", + "clap", + "clap-verbosity-flag", + "derive_more", "diff", "env_logger", - "libc", + "itertools", "log", "regex", "rusqlite", - "thiserror", + "size", + "unicode-width", "yansi", ] @@ -325,12 +241,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "half" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" - [[package]] name = "hashbrown" version = "0.15.3" @@ -357,11 +267,19 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ + "hermit-abi", "libc", + "windows-sys", ] [[package]] @@ -372,19 +290,13 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" -version = "0.10.5" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - [[package]] name = "jiff" version = "0.2.12" @@ -409,22 +321,6 @@ dependencies = [ "syn", ] -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" version = "0.2.172" @@ -454,61 +350,18 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - [[package]] name = "portable-atomic" version = "1.11.0" @@ -542,26 +395,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "regex" version = "1.11.1" @@ -597,7 +430,7 @@ version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b" dependencies = [ - "bitflags 2.9.0", + "bitflags", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -605,27 +438,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "rustversion" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "serde" version = "1.0.219" @@ -635,16 +447,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde_cbor" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" -dependencies = [ - "half", - "serde", -] - [[package]] name = "serde_derive" version = "1.0.219" @@ -656,24 +458,18 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_json" -version = "1.0.140" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "size" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b" + [[package]] name = "smallvec" version = "1.15.0" @@ -697,45 +493,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "thiserror" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "unicode-ident" version = "1.0.18" @@ -743,10 +500,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] -name = "unicode-width" -version = "0.1.14" +name = "unicode-segmentation" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "utf8parse" @@ -760,115 +529,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-sys" version = "0.59.0" @@ -947,3 +607,6 @@ name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +dependencies = [ + "is-terminal", +] diff --git a/Cargo.toml b/Cargo.toml index 95a9578..fa5129d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,39 +1,105 @@ [package] -name = "dix" -version = "0.1.0" -edition = "2024" - -[[bin]] -name = "dix" -path = "src/main.rs" - -[lib] -name = "dixlib" -path = "src/lib.rs" - +name = "dix" +description = "Diff Nix" +version = "0.1.0" +edition = "2024" [dependencies] -clap = { version = "4.5.37", features = ["derive"] } -regex = "1.11.1" -yansi = "1.0.1" -thiserror = "2.0.12" -log = "0.4.20" -env_logger = "0.11.3" -rusqlite = { version = "0.35.0", features = ["bundled"] } -diff = "0.1.13" +anyhow = "1.0.98" +clap = { version = "4.5.37", features = [ "derive" ] } +clap-verbosity-flag = "3.0.2" +derive_more = { version = "2.0.1", features = [ "full" ] } +diff = "0.1.13" +env_logger = "0.11.3" +itertools = "0.14.0" +log = "0.4.20" +regex = "1.11.1" +rusqlite = { version = "0.35.0", features = [ "bundled" ] } +size = "0.5.0" +unicode-width = "0.2.0" +yansi = { version = "1.0.1", features = [ "detect-env", "detect-tty" ] } -[dev-dependencies] -criterion = "0.3" -libc = "0.2" +[lints.clippy] +pedantic = { level = "warn", priority = -1 } -[[bench]] -name = "store" -harness=false +blanket_clippy_restriction_lints = "allow" +restriction = { level = "warn", priority = -1 } -[[bench]] -name = "print" -harness=false - -[[bench]] -name = "util" -harness=false +alloc_instead_of_core = "allow" +allow_attributes_without_reason = "allow" +arbitrary_source_item_ordering = "allow" +arithmetic_side_effects = "allow" +as_conversions = "allow" +as_pointer_underscore = "allow" +as_underscore = "allow" +big_endian_bytes = "allow" +clone_on_ref_ptr = "allow" +dbg_macro = "allow" +disallowed_script_idents = "allow" +else_if_without_else = "allow" +error_impl_error = "allow" +exhaustive_enums = "allow" +exhaustive_structs = "allow" +expect_used = "allow" +field_scoped_visibility_modifiers = "allow" +float_arithmetic = "allow" +host_endian_bytes = "allow" +impl_trait_in_params = "allow" +implicit_return = "allow" +indexing_slicing = "allow" +inline_asm_x86_intel_syntax = "allow" +integer_division = "allow" +integer_division_remainder_used = "allow" +large_include_file = "allow" +let_underscore_must_use = "allow" +let_underscore_untyped = "allow" +little_endian_bytes = "allow" +map_err_ignore = "allow" +match_same_arms = "allow" +missing_assert_message = "allow" +missing_docs_in_private_items = "allow" +missing_errors_doc = "allow" +missing_inline_in_public_items = "allow" +missing_panics_doc = "allow" +missing_trait_methods = "allow" +mod_module_files = "allow" +multiple_inherent_impl = "allow" +mutex_atomic = "allow" +mutex_integer = "allow" +new_without_default = "allow" +non_ascii_literal = "allow" +panic = "allow" +panic_in_result_fn = "allow" +partial_pub_fields = "allow" +print_stderr = "allow" +print_stdout = "allow" +pub_use = "allow" +pub_with_shorthand = "allow" +pub_without_shorthand = "allow" +question_mark_used = "allow" +ref_patterns = "allow" +renamed_function_params = "allow" +same_name_method = "allow" +semicolon_outside_block = "allow" +separated_literal_suffix = "allow" +shadow_reuse = "allow" +shadow_same = "allow" +shadow_unrelated = "allow" +single_call_fn = "allow" +single_char_lifetime_names = "allow" +single_match_else = "allow" +std_instead_of_alloc = "allow" +std_instead_of_core = "allow" +string_add = "allow" +string_slice = "allow" +todo = "allow" +too_many_lines = "allow" +try_err = "allow" +unimplemented = "allow" +unnecessary_safety_comment = "allow" +unnecessary_safety_doc = "allow" +unreachable = "allow" +unwrap_in_result = "allow" +unwrap_used = "allow" +use_debug = "allow" +wildcard_enum_match_arm = "allow" diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702..0000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..5e144cb --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,611 @@ +# GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +## Preamble + +The GNU General Public License is a free, copyleft license for software and +other kinds of works. + +The licenses for most software and other practical works are designed to take +away your freedom to share and change the works. By contrast, the GNU General +Public License is intended to guarantee your freedom to share and change all +versions of a program--to make sure it remains free software for all its users. +We, the Free Software Foundation, use the GNU General Public License for most of +our software; it applies also to any other work released this way by its +authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom to +distribute copies of free software (and charge for them if you wish), that you +receive source code or can get it if you want it, that you can change the +software or use pieces of it in new free programs, and that you know you can do +these things. + +To protect your rights, we need to prevent others from denying you these rights +or asking you to surrender the rights. Therefore, you have certain +responsibilities if you distribute copies of the software, or if you modify it: +responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a +fee, you must pass on to the recipients the same freedoms that you received. You +must make sure that they, too, receive or can get the source code. And you must +show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert +copyright on the software, and (2) offer you this License giving you legal +permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there +is no warranty for this free software. For both users' and authors' sake, the +GPL requires that modified versions be marked as changed, so that their problems +will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified +versions of the software inside them, although the manufacturer can do so. This +is fundamentally incompatible with the aim of protecting users' freedom to +change the software. The systematic pattern of such abuse occurs in the area of +products for individuals to use, which is precisely where it is most +unacceptable. Therefore, we have designed this version of the GPL to prohibit +the practice for those products. If such problems arise substantially in other +domains, we stand ready to extend this provision to those domains in future +versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States +should not allow patents to restrict development and use of software on +general-purpose computers, but in those that do, we wish to avoid the special +danger that patents applied to a free program could make it effectively +proprietary. To prevent this, the GPL assures that patents cannot be used to +render the program non-free. + +The precise terms and conditions for copying, distribution and modification +follow. + +## TERMS AND CONDITIONS + +### 0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, +such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. Each +licensee is addressed as "you". "Licensees" and "recipients" may be individuals +or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in a +fashion requiring copyright permission, other than the making of an exact copy. +The resulting work is called a "modified version" of the earlier work or a work +"based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the +Program. + +To "propagate" a work means to do anything with it that, without permission, +would make you directly or secondarily liable for infringement under applicable +copyright law, except executing it on a computer or modifying a private copy. +Propagation includes copying, distribution (with or without modification), +making available to the public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other parties to +make or receive copies. Mere interaction with a user through a computer network, +with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the extent +that it includes a convenient and prominently visible feature that (1) displays +an appropriate copyright notice, and (2) tells the user that there is no +warranty for the work (except to the extent that warranties are provided), that +licensees may convey the work under this License, and how to view a copy of this +License. If the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code. + +The "source code" for a work means the preferred form of the work for making +modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard +defined by a recognized standards body, or, in the case of interfaces specified +for a particular programming language, one that is widely used among developers +working in that language. + +The "System Libraries" of an executable work include anything, other than the +work as a whole, that (a) is included in the normal form of packaging a Major +Component, but which is not part of that Major Component, and (b) serves only to +enable use of the work with that Major Component, or to implement a Standard +Interface for which an implementation is available to the public in source code +form. A "Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system (if any) on +which the executable work runs, or a compiler used to produce the work, or an +object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source +code needed to generate, install, and (for an executable work) run the object +code and to modify the work, including scripts to control those activities. +However, it does not include the work's System Libraries, or general-purpose +tools or generally available free programs which are used unmodified in +performing those activities but which are not part of the work. For example, +Corresponding Source includes interface definition files associated with source +files for the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, such as by +intimate data communication or control flow between those subprograms and other +parts of the work. + +The Corresponding Source need not include anything that users can regenerate +automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +### 2. Basic Permissions. + +All rights granted under this License are granted for the term of copyright on +the Program, and are irrevocable provided the stated conditions are met. This +License explicitly affirms your unlimited permission to run the unmodified +Program. The output from running a covered work is covered by this License only +if the output, given its content, constitutes a covered work. This License +acknowledges your rights of fair use or other equivalent, as provided by +copyright law. + +You may make, run and propagate covered works that you do not convey, without +conditions so long as your license otherwise remains in force. You may convey +covered works to others for the sole purpose of having them make modifications +exclusively for you, or provide you with facilities for running those works, +provided that you comply with the terms of this License in conveying all +material for which you do not control copyright. Those thus making or running +the covered works for you must do so exclusively on your behalf, under your +direction and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions +stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological measure under +any applicable law fulfilling obligations under article 11 of the WIPO copyright +treaty adopted on 20 December 1996, or similar laws prohibiting or restricting +circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention is +effected by exercising rights under this License with respect to the covered +work, and you disclaim any intention to limit operation or modification of the +work as a means of enforcing, against the work's users, your or third parties' +legal rights to forbid circumvention of technological measures. + +### 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you receive it, +in any medium, provided that you conspicuously and appropriately publish on each +copy an appropriate copyright notice; keep intact all notices stating that this +License and any non-permissive terms added in accord with section 7 apply to the +code; keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may +offer support or warranty protection for a fee. + +### 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to produce it +from the Program, in the form of source code under the terms of section 4, +provided that you also meet all of these conditions: + +- a) The work must carry prominent notices stating that you modified it, and + giving a relevant date. +- b) The work must carry prominent notices stating that it is released under + this License and any conditions added under section 7. This requirement + modifies the requirement in section 4 to "keep intact all notices". +- c) You must license the entire work, as a whole, under this License to anyone + who comes into possession of a copy. This License will therefore apply, along + with any applicable section 7 additional terms, to the whole of the work, and + all its parts, regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not invalidate + such permission if you have separately received it. +- d) If the work has interactive user interfaces, each must display Appropriate + Legal Notices; however, if the Program has interactive interfaces that do not + display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which +are not by their nature extensions of the covered work, and which are not +combined with it such as to form a larger program, in or on a volume of a +storage or distribution medium, is called an "aggregate" if the compilation and +its resulting copyright are not used to limit the access or legal rights of the +compilation's users beyond what the individual works permit. Inclusion of a +covered work in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +### 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of sections 4 +and 5, provided that you also convey the machine-readable Corresponding Source +under the terms of this License, in one of these ways: + +- a) Convey the object code in, or embodied in, a physical product (including a + physical distribution medium), accompanied by the Corresponding Source fixed + on a durable physical medium customarily used for software interchange. +- b) Convey the object code in, or embodied in, a physical product (including a + physical distribution medium), accompanied by a written offer, valid for at + least three years and valid for as long as you offer spare parts or customer + support for that product model, to give anyone who possesses the object code + either (1) a copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical medium + customarily used for software interchange, for a price no more than your + reasonable cost of physically performing this conveying of source, or (2) + access to copy the Corresponding Source from a network server at no charge. +- c) Convey individual copies of the object code with a copy of the written + offer to provide the Corresponding Source. This alternative is allowed only + occasionally and noncommercially, and only if you received the object code + with such an offer, in accord with subsection 6b. +- d) Convey the object code by offering access from a designated place (gratis + or for a charge), and offer equivalent access to the Corresponding Source in + the same way through the same place at no further charge. You need not require + recipients to copy the Corresponding Source along with the object code. If the + place to copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) that supports + equivalent copying facilities, provided you maintain clear directions next to + the object code saying where to find the Corresponding Source. Regardless of + what server hosts the Corresponding Source, you remain obligated to ensure + that it is available for as long as needed to satisfy these requirements. +- e) Convey the object code using peer-to-peer transmission, provided you inform + other peers where the object code and Corresponding Source of the work are + being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the +Corresponding Source as a System Library, need not be included in conveying the +object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible +personal property which is normally used for personal, family, or household +purposes, or (2) anything designed or sold for incorporation into a dwelling. In +determining whether a product is a consumer product, doubtful cases shall be +resolved in favor of coverage. For a particular product received by a particular +user, "normally used" refers to a typical or common use of that class of +product, regardless of the status of the particular user or of the way in which +the particular user actually uses, or expects or is expected to use, the +product. A product is a consumer product regardless of whether the product has +substantial commercial, industrial or non-consumer uses, unless such uses +represent the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, +authorization keys, or other information required to install and execute +modified versions of a covered work in that User Product from a modified version +of its Corresponding Source. The information must suffice to ensure that the +continued functioning of the modified object code is in no case prevented or +interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as part of a +transaction in which the right of possession and use of the User Product is +transferred to the recipient in perpetuity or for a fixed term (regardless of +how the transaction is characterized), the Corresponding Source conveyed under +this section must be accompanied by the Installation Information. But this +requirement does not apply if neither you nor any third party retains the +ability to install modified object code on the User Product (for example, the +work has been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates for a +work that has been modified or installed by the recipient, or for the User +Product in which it has been modified or installed. Access to a network may be +denied when the modification itself materially and adversely affects the +operation of the network or violates the rules and protocols for communication +across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord +with this section must be in a format that is publicly documented (and with an +implementation available to the public in source code form), and must require no +special password or key for unpacking, reading or copying. + +### 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this License by +making exceptions from one or more of its conditions. Additional permissions +that are applicable to the entire Program shall be treated as though they were +included in this License, to the extent that they are valid under applicable +law. If additional permissions apply only to part of the Program, that part may +be used separately under those permissions, but the entire Program remains +governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any +additional permissions from that copy, or from any part of it. (Additional +permissions may be written to require their own removal in certain cases when +you modify the work.) You may place additional permissions on material, added by +you to a covered work, for which you have or can give appropriate copyright +permission. + +Notwithstanding any other provision of this License, for material you add to a +covered work, you may (if authorized by the copyright holders of that material) +supplement the terms of this License with terms: + +- a) Disclaiming warranty or limiting liability differently from the terms of + sections 15 and 16 of this License; or +- b) Requiring preservation of specified reasonable legal notices or author + attributions in that material or in the Appropriate Legal Notices displayed by + works containing it; or +- c) Prohibiting misrepresentation of the origin of that material, or requiring + that modified versions of such material be marked in reasonable ways as + different from the original version; or +- d) Limiting the use for publicity purposes of names of licensors or authors of + the material; or +- e) Declining to grant rights under trademark law for use of some trade names, + trademarks, or service marks; or +- f) Requiring indemnification of licensors and authors of that material by + anyone who conveys the material (or modified versions of it) with contractual + assumptions of liability to the recipient, for any liability that these + contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered "further restrictions" +within the meaning of section 10. If the Program as you received it, or any part +of it, contains a notice stating that it is governed by this License along with +a term that is a further restriction, you may remove that term. If a license +document contains a further restriction but permits relicensing or conveying +under this License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does not survive +such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, +in the relevant source files, a statement of the additional terms that apply to +those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a +separately written license, or stated as exceptions; the above requirements +apply either way. + +### 8. Termination. + +You may not propagate or modify a covered work except as expressly provided +under this License. Any attempt otherwise to propagate or modify it is void, and +will automatically terminate your rights under this License (including any +patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a +particular copyright holder is reinstated (a) provisionally, unless and until +the copyright holder explicitly and finally terminates your license, and (b) +permanently, if the copyright holder fails to notify you of the violation by +some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated +permanently if the copyright holder notifies you of the violation by some +reasonable means, this is the first time you have received notice of violation +of this License (for any work) from that copyright holder, and you cure the +violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of +parties who have received copies or rights from you under this License. If your +rights have been terminated and not permanently reinstated, you do not qualify +to receive new licenses for the same material under section 10. + +### 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy of +the Program. Ancillary propagation of a covered work occurring solely as a +consequence of using peer-to-peer transmission to receive a copy likewise does +not require acceptance. However, nothing other than this License grants you +permission to propagate or modify any covered work. These actions infringe +copyright if you do not accept this License. Therefore, by modifying or +propagating a covered work, you indicate your acceptance of this License to do +so. + +### 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives a +license from the original licensors, to run, modify and propagate that work, +subject to this License. You are not responsible for enforcing compliance by +third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered work results +from an entity transaction, each party to that transaction who receives a copy +of the work also receives whatever licenses to the work the party's predecessor +in interest had or could give under the previous paragraph, plus a right to +possession of the Corresponding Source of the work from the predecessor in +interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights +granted or affirmed under this License. For example, you may not impose a +license fee, royalty, or other charge for exercise of rights granted under this +License, and you may not initiate litigation (including a cross-claim or +counterclaim in a lawsuit) alleging that any patent claim is infringed by +making, using, selling, offering for sale, or importing the Program or any +portion of it. + +### 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this License of +the Program or a work on which the Program is based. The work thus licensed is +called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or +controlled by the contributor, whether already acquired or hereafter acquired, +that would be infringed by some manner, permitted by this License, of making, +using, or selling its contributor version, but do not include claims that would +be infringed only as a consequence of further modification of the contributor +version. For purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent +license under the contributor's essential patent claims, to make, use, sell, +offer for sale, import and otherwise run, modify and propagate the contents of +its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement +or commitment, however denominated, not to enforce a patent (such as an express +permission to practice a patent or covenant not to sue for patent infringement). +To "grant" such a patent license to a party means to make such an agreement or +commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the +Corresponding Source of the work is not available for anyone to copy, free of +charge and under the terms of this License, through a publicly available network +server or other readily accessible means, then you must either (1) cause the +Corresponding Source to be so available, or (2) arrange to deprive yourself of +the benefit of the patent license for this particular work, or (3) arrange, in a +manner consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have actual +knowledge that, but for the patent license, your conveying the covered work in a +country, or your recipient's use of the covered work in a country, would +infringe one or more identifiable patents in that country that you have reason +to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you +convey, or propagate by procuring conveyance of, a covered work, and grant a +patent license to some of the parties receiving the covered work authorizing +them to use, propagate, modify or convey a specific copy of the covered work, +then the patent license you grant is automatically extended to all recipients of +the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope of +its coverage, prohibits the exercise of, or is conditioned on the non-exercise +of one or more of the rights that are specifically granted under this License. +You may not convey a covered work if you are a party to an arrangement with a +third party that is in the business of distributing software, under which you +make payment to the third party based on the extent of your activity of +conveying the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by you (or +copies made from those copies), or (b) primarily for and in connection with +specific products or compilations that contain the covered work, unless you +entered into that arrangement, or that patent license was granted, prior to 28 +March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied +license or other defenses to infringement that may otherwise be available to you +under applicable patent law. + +### 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not excuse +you from the conditions of this License. If you cannot convey a covered work so +as to satisfy simultaneously your obligations under this License and any other +pertinent obligations, then as a consequence you may not convey it at all. For +example, if you agree to terms that obligate you to collect a royalty for +further conveying from those to whom you convey the Program, the only way you +could satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +### 13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have permission to link +or combine any covered work with a work licensed under version 3 of the GNU +Affero General Public License into a single combined work, and to convey the +resulting work. The terms of this License will continue to apply to the part +which is the covered work, but the special requirements of the GNU Affero +General Public License, section 13, concerning interaction through a network +will apply to the combination as such. + +### 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the GNU +General Public License from time to time. Such new versions will be similar in +spirit to the present version, but may differ in detail to address new problems +or concerns. + +Each version is given a distinguishing version number. If the Program specifies +that a certain numbered version of the GNU General Public License "or any later +version" applies to it, you have the option of following the terms and +conditions either of that numbered version or of any later version published by +the Free Software Foundation. If the Program does not specify a version number +of the GNU General Public License, you may choose any version ever published by +the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the +GNU General Public License can be used, that proxy's public statement of +acceptance of a version permanently authorizes you to choose that version for +the Program. + +Later license versions may give you additional or different permissions. +However, no additional obligations are imposed on any author or copyright holder +as a result of your choosing to follow a later version. + +### 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER +PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER +EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE +QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +### 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY +COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS +PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, +INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE +THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED +INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE +PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY +HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +### 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot +be given local legal effect according to their terms, reviewing courts shall +apply local law that most closely approximates an absolute waiver of all civil +liability in connection with the Program, unless a warranty or assumption of +liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +## How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use +to the public, the best way to achieve this is to make it free software which +everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively state the exclusion of +warranty; and each file should have at least the "copyright" line and a pointer +to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like +this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands \`show w' and \`show c' should show the appropriate +parts of the General Public License. Of course, your program's commands might be +different; for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, if +any, to sign a "copyright disclaimer" for the program, if necessary. For more +information on this, and how to apply and follow the GNU GPL, see +. + +The GNU General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may consider +it more useful to permit linking proprietary applications with the library. If +this is what you want to do, use the GNU Lesser General Public License instead +of this License. But first, please read +. diff --git a/README.md b/README.md index f866b0e..645e2ce 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,34 @@ -# dix +# Diff Nix -## diff Nix stuff +A tool to diff any Nix related thing. -Currently only system closures +Currently only supports closures (a derivation graph, such as a system build or +package). ## Usage `dix /nix/var/profiles/system--link /run/current-system` -# Why dix? +## Output -![dix nuts](.github/dix.png) +![output of `dix /nix/var/nix/profiles/system-165-link/ /run/current-system`](images/dix.png) + +## License + +``` +Dix: Diff Nix +Copyright (C) 2025-present bloxx12 + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +``` diff --git a/benches/common.rs b/benches/common.rs deleted file mode 100644 index debf5d3..0000000 --- a/benches/common.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::{ - env, - fs::{self, DirEntry}, - path::PathBuf, - sync::OnceLock, -}; - -use dixlib::{store, util::PackageDiff}; - -/// tries to get the path of the oldest nixos system derivation -/// this function is pretty hacky and only used so that -/// you don't have to specify a specific derivation to -/// run the benchmarks -fn get_oldest_nixos_system() -> Option { - let profile_dir = fs::read_dir("/nix/var/nix/profiles").ok()?; - - let files = profile_dir.filter_map(Result::ok).filter_map(|entry| { - entry - .file_type() - .ok() - .and_then(|f| f.is_symlink().then_some(entry.path())) - }); - - files.min_by_key(|path| { - // extract all digits from the file name and use that as key - let p = path.as_os_str().to_str().unwrap_or_default(); - let digits: String = p.chars().filter(|c| c.is_ascii_digit()).collect(); - // if we are not able to produce a key (e.g. because the path does not contain digits) - // we put it last - digits.parse::().unwrap_or(u32::MAX) - }) -} - -pub fn get_deriv_query() -> &'static PathBuf { - static _QUERY_DERIV: OnceLock = OnceLock::new(); - _QUERY_DERIV.get_or_init(|| { - let path = PathBuf::from( - env::var("DIX_BENCH_NEW_SYSTEM") - .unwrap_or_else(|_| "/run/current-system/system".into()), - ); - path - }) -} -pub fn get_deriv_query_old() -> &'static PathBuf { - static _QUERY_DERIV: OnceLock = OnceLock::new(); - _QUERY_DERIV.get_or_init(|| { - let path = env::var("DIX_BENCH_OLD_SYSTEM") - .ok() - .map(PathBuf::from) - .or(get_oldest_nixos_system()) - .unwrap_or_else(|| PathBuf::from("/run/current-system/system")); - path - }) -} - -pub fn get_packages() -> &'static (Vec, Vec) { - static _PKGS: OnceLock<(Vec, Vec)> = OnceLock::new(); - _PKGS.get_or_init(|| { - let pkgs_before = store::get_packages(std::path::Path::new(get_deriv_query_old())) - .unwrap() - .into_iter() - .map(|(_, name)| name) - .collect::>(); - let pkgs_after = store::get_packages(std::path::Path::new(get_deriv_query())) - .unwrap() - .into_iter() - .map(|(_, name)| name) - .collect::>(); - (pkgs_before, pkgs_after) - }) -} - -pub fn get_pkg_diff() -> &'static PackageDiff<'static> { - static _PKG_DIFF: OnceLock = OnceLock::new(); - _PKG_DIFF.get_or_init(|| { - let (pkgs_before, pkgs_after) = get_packages(); - PackageDiff::new(pkgs_before, pkgs_after) - }) -} - -/// prints the old and new NixOs system used for benchmarking -/// -/// is used to give information about the old and new system -pub fn print_used_nixos_systems() { - let old = get_deriv_query_old(); - let new = get_deriv_query(); - println!("old system used {:?}", old); - println!("new system used {:?}", new); -} diff --git a/benches/print.rs b/benches/print.rs deleted file mode 100644 index af2832c..0000000 --- a/benches/print.rs +++ /dev/null @@ -1,86 +0,0 @@ -mod common; - -use std::{fs::File, os::fd::AsRawFd}; - -use common::{get_pkg_diff, print_used_nixos_systems}; -use criterion::{Criterion, black_box, criterion_group, criterion_main}; -use dixlib::print; - -/// reroutes stdout and stderr to the null device before -/// executing `f` -fn suppress_output(f: F) { - let stdout = std::io::stdout(); - let stderr = std::io::stderr(); - - // Save original FDs - let orig_stdout_fd = stdout.as_raw_fd(); - let orig_stderr_fd = stderr.as_raw_fd(); - - // Open /dev/null and get its FD - let devnull = File::create("/dev/null").unwrap(); - let null_fd = devnull.as_raw_fd(); - - // Redirect stdout and stderr to /dev/null - let _ = unsafe { libc::dup2(null_fd, orig_stdout_fd) }; - let _ = unsafe { libc::dup2(null_fd, orig_stderr_fd) }; - - f(); - - let _ = unsafe { libc::dup2(orig_stdout_fd, 1) }; - let _ = unsafe { libc::dup2(orig_stderr_fd, 2) }; -} - -pub fn bench_print_added(c: &mut Criterion) { - print_used_nixos_systems(); - let diff = get_pkg_diff(); - c.bench_function("print_added", |b| { - b.iter(|| { - suppress_output(|| { - print::print_added( - black_box(&diff.added), - black_box(&diff.pkg_to_versions_post), - 30, - ); - }); - }); - }); -} -pub fn bench_print_removed(c: &mut Criterion) { - print_used_nixos_systems(); - let diff = get_pkg_diff(); - c.bench_function("print_removed", |b| { - b.iter(|| { - suppress_output(|| { - print::print_removed( - black_box(&diff.removed), - black_box(&diff.pkg_to_versions_pre), - 30, - ); - }); - }); - }); -} -pub fn bench_print_changed(c: &mut Criterion) { - print_used_nixos_systems(); - let diff = get_pkg_diff(); - c.bench_function("print_changed", |b| { - b.iter(|| { - suppress_output(|| { - print::print_changes( - black_box(&diff.changed), - black_box(&diff.pkg_to_versions_pre), - black_box(&diff.pkg_to_versions_post), - 30, - ); - }); - }); - }); -} - -criterion_group!( - benches, - bench_print_added, - bench_print_removed, - bench_print_changed -); -criterion_main!(benches); diff --git a/benches/store.rs b/benches/store.rs deleted file mode 100644 index cac7b52..0000000 --- a/benches/store.rs +++ /dev/null @@ -1,36 +0,0 @@ -mod common; -use criterion::{Criterion, black_box, criterion_group, criterion_main}; -use dixlib::store; - -// basic benchmarks using the current system -// -// problem: this is not reproducible at all -// since this is very depending on the current -// system and the nature of the system in general -// -// we might want to think about using a copy of the sqlite -// db to benchmark instead to make the results comparable - -pub fn bench_get_packages(c: &mut Criterion) { - c.bench_function("get_packages", |b| { - b.iter(|| store::get_packages(black_box(common::get_deriv_query()))); - }); -} -pub fn bench_get_closure_size(c: &mut Criterion) { - c.bench_function("get_closure_size", |b| { - b.iter(|| store::get_closure_size(black_box(common::get_deriv_query()))); - }); -} -pub fn bench_get_dependency_graph(c: &mut Criterion) { - c.bench_function("get_dependency_graph", |b| { - b.iter(|| store::get_dependency_graph(black_box(common::get_deriv_query()))); - }); -} - -criterion_group!( - benches, - bench_get_packages, - bench_get_closure_size, - bench_get_dependency_graph -); -criterion_main!(benches); diff --git a/benches/util.rs b/benches/util.rs deleted file mode 100644 index ec8eb19..0000000 --- a/benches/util.rs +++ /dev/null @@ -1,15 +0,0 @@ -mod common; - -use common::get_packages; -use criterion::{Criterion, black_box, criterion_group, criterion_main}; -use dixlib::util::PackageDiff; - -pub fn bench_package_diff(c: &mut Criterion) { - let (pkgs_before, pkgs_after) = get_packages(); - c.bench_function("PackageDiff::new", |b| { - b.iter(|| PackageDiff::new(black_box(pkgs_before), black_box(pkgs_after))); - }); -} - -criterion_group!(benches, bench_package_diff); -criterion_main!(benches); diff --git a/flake.lock b/flake.lock index 957dc1d..f1b0f1a 100644 --- a/flake.lock +++ b/flake.lock @@ -19,21 +19,42 @@ "root": { "inputs": { "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay", "systems": "systems" } }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1746758179, + "narHash": "sha256-JECUw1YBEsTsVauvupRzE5ykZaJoyhHCpoY87ZZJGas=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "4fd00513eac6b6140c5dced3e1b8133e2369a0f8", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, "systems": { "locked": { - "lastModified": 1689347949, - "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "owner": "nix-systems", - "repo": "default-linux", - "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { "owner": "nix-systems", - "repo": "default-linux", + "repo": "default", "type": "github" } } diff --git a/flake.nix b/flake.nix index bf63f2e..1193ad9 100644 --- a/flake.nix +++ b/flake.nix @@ -1,8 +1,13 @@ { - description = "Nix version differ"; + description = "Dix - Diff Nix"; + inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - systems.url = "github:nix-systems/default-linux"; + systems.url = "github:nix-systems/default"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = inputs: let @@ -10,8 +15,18 @@ pkgsFor = inputs.nixpkgs.legacyPackages; in { packages = eachSystem (system: { - default = inputs.self.packages.${system}.ralc; - ralc = pkgsFor.${system}.callPackage ./nix/package.nix {}; + default = inputs.self.packages.${system}.dix; + dix = pkgsFor.${system}.callPackage ./nix/package.nix {}; + }); + + apps = eachSystem (system: let + inherit (inputs.self.packages.${system}) dix; + in { + default = inputs.self.apps.${system}.dix; + dix = { + type = "app"; + program = "${dix}/bin/dix"; + }; }); devShells = eachSystem (system: { @@ -21,13 +36,18 @@ (pkgsFor.${system}) cargo rustc - rustfmt bacon ; inherit (pkgsFor.${system}.rustPackages) clippy ; + + inherit + ((pkgsFor.${system}.extend + inputs.rust-overlay.overlays.default).rust-bin.nightly.latest) + rustfmt + ; }; }; }); diff --git a/images/dix.png b/images/dix.png new file mode 100644 index 0000000..277662b Binary files /dev/null and b/images/dix.png differ diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 0000000..4852111 --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,18 @@ +{ + rustPlatform, + lib, + ... +}: let + toml = (lib.importTOML ../Cargo.toml).package; + pname = toml.name; + inherit (toml) version; +in + rustPlatform.buildRustPackage { + inherit pname version; + src = builtins.path { + name = "${pname}-${version}"; + path = lib.sources.cleanSource ../.; + }; + cargoLock.lockFile = ../Cargo.lock; + doCheck = true; + } diff --git a/src/diff.rs b/src/diff.rs new file mode 100644 index 0000000..38a7beb --- /dev/null +++ b/src/diff.rs @@ -0,0 +1,437 @@ +use std::{ + collections::HashMap, + fmt::{ + self, + Write as _, + }, + path::{ + Path, + PathBuf, + }, + thread, +}; + +use anyhow::{ + Context as _, + Error, + Result, +}; +use itertools::{ + EitherOrBoth, + Itertools, +}; +use size::Size; +use unicode_width::UnicodeWidthStr as _; +use yansi::Paint as _; + +use crate::{ + StorePath, + Version, + store, +}; + +#[derive(Debug, Default)] +struct Diff { + old: T, + new: T, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum DiffStatus { + Changed, + Added, + Removed, +} + +impl DiffStatus { + fn char(self) -> impl fmt::Display { + match self { + Self::Added => "A".green(), + Self::Removed => "R".red(), + Self::Changed => "C".yellow(), + } + } +} + +/// Writes the diff header (<<< out, >>>in) and package diff. +/// +/// # Returns +/// +/// Will return the amount of package diffs written. Even when zero, +/// the header will be written. +pub fn write_paths_diffln( + writer: &mut impl fmt::Write, + path_old: &Path, + path_new: &Path, +) -> Result { + let mut connection = store::connect()?; + + let paths_old = connection.query_dependents(path_old).with_context(|| { + format!( + "failed to query dependencies of path '{path}'", + path = path_old.display() + ) + })?; + + log::info!( + "found {count} packages in old closure", + count = paths_old.len(), + ); + + let paths_new = connection.query_dependents(path_new).with_context(|| { + format!( + "failed to query dependencies of path '{path}'", + path = path_new.display() + ) + })?; + log::info!( + "found {count} packages in new closure", + count = paths_new.len(), + ); + + drop(connection); + + writeln!( + writer, + "{arrows} {old}", + arrows = "<<<".bold(), + old = path_old.display(), + )?; + writeln!( + writer, + "{arrows} {new}", + arrows = ">>>".bold(), + new = path_new.display(), + )?; + + writeln!(writer)?; + + #[expect(clippy::pattern_type_mismatch)] + Ok(write_packages_diffln( + writer, + paths_old.iter().map(|(_, path)| path), + paths_new.iter().map(|(_, path)| path), + )?) +} + +/// Takes a list of versions which may contain duplicates and deduplicates it by +/// replacing multiple occurrences of an element with the same element plus the +/// amount it occurs. +/// +/// # Example +/// +/// ```rs +/// let mut versions = vec!["2.3", "1.0", "2.3", "4.8", "2.3", "1.0"]; +/// +/// deduplicate_versions(&mut versions); +/// assert_eq!(*versions, &["1.0 ×2", "2.3 ×3", "4.8"]); +/// ``` +fn deduplicate_versions(versions: &mut Vec) { + versions.sort_unstable(); + + let mut deduplicated = Vec::new(); + + // Push a version onto the final vec. If it occurs more than once, + // we add a ×{count} to signify the amount of times it occurs. + let mut deduplicated_push = |mut version: Version, count: usize| { + if count > 1 { + write!(version, " ×{count}").unwrap(); + } + deduplicated.push(version); + }; + + let mut last_version = None::<(Version, usize)>; + for version in versions.iter() { + #[expect(clippy::mixed_read_write_in_expression)] + let Some((last_version_value, count)) = last_version.take() else { + last_version = Some((version.clone(), 1)); + continue; + }; + + // If the last version matches the current version, we increase the count by + // one. Otherwise, we push the last version to the result. + if last_version_value == *version { + last_version = Some((last_version_value, count + 1)); + } else { + deduplicated_push(last_version_value, count); + } + } + + // Push the final element, if it exists. + if let Some((version, count)) = last_version.take() { + deduplicated_push(version, count); + } + + *versions = deduplicated; +} + +#[expect(clippy::cognitive_complexity, clippy::too_many_lines)] +fn write_packages_diffln<'a>( + writer: &mut impl fmt::Write, + paths_old: impl Iterator, + paths_new: impl Iterator, +) -> Result { + let mut paths = HashMap::<&str, Diff>>::new(); + + for path in paths_old { + match path.parse_name_and_version() { + Ok((name, version)) => { + log::debug!("parsed name: {name}"); + log::debug!("parsed version: {version:?}"); + + paths + .entry(name) + .or_default() + .old + .push(version.unwrap_or_else(|| Version::from("".to_owned()))); + }, + + Err(error) => { + log::warn!("error parsing old path name and version: {error}"); + }, + } + } + + for path in paths_new { + match path.parse_name_and_version() { + Ok((name, version)) => { + log::debug!("parsed name: {name}"); + log::debug!("parsed version: {version:?}"); + + paths + .entry(name) + .or_default() + .new + .push(version.unwrap_or_else(|| Version::from("".to_owned()))); + }, + + Err(error) => { + log::warn!("error parsing new path name and version: {error}"); + }, + } + } + + let mut diffs = paths + .into_iter() + .filter_map(|(name, mut versions)| { + deduplicate_versions(&mut versions.old); + deduplicate_versions(&mut versions.new); + + let status = match (versions.old.len(), versions.new.len()) { + (0, 0) => unreachable!(), + (0, _) => DiffStatus::Added, + (_, 0) => DiffStatus::Removed, + (..) if versions.old != versions.new => DiffStatus::Changed, + (..) => return None, + }; + + Some((name, versions, status)) + }) + .collect::>(); + + diffs.sort_by(|&(a_name, _, a_status), &(b_name, _, b_status)| { + a_status.cmp(&b_status).then_with(|| a_name.cmp(b_name)) + }); + + let name_width = diffs + .iter() + .map(|&(name, ..)| name.width()) + .max() + .unwrap_or(0); + + let mut last_status = None::; + + for &(name, ref versions, status) in &diffs { + if last_status != Some(status) { + writeln!( + writer, + "{nl}{status}", + nl = if last_status.is_some() { "\n" } else { "" }, + status = match status { + DiffStatus::Added => "ADDED", + DiffStatus::Removed => "REMOVED", + DiffStatus::Changed => "CHANGED", + } + .bold(), + )?; + + last_status = Some(status); + } + + write!( + writer, + "[{status}] {name: { + if oldwrote { + write!(oldacc, ", ")?; + } else { + write!(oldacc, " ")?; + oldwrote = true; + } + + for old_comp in old_version { + match old_comp { + Ok(old_comp) => write!(oldacc, "{old}", old = old_comp.red())?, + Err(ignored) => write!(oldacc, "{ignored}")?, + } + } + }, + + EitherOrBoth::Right(new_version) => { + if newwrote { + write!(newacc, ", ")?; + } else { + write!(newacc, " ")?; + newwrote = true; + } + + for new_comp in new_version { + match new_comp { + Ok(new_comp) => write!(newacc, "{new}", new = new_comp.green())?, + Err(ignored) => write!(newacc, "{ignored}")?, + } + } + }, + + EitherOrBoth::Both(old_version, new_version) => { + if old_version == new_version { + continue; + } + + if oldwrote { + write!(oldacc, ", ")?; + } else { + write!(oldacc, " ")?; + oldwrote = true; + } + if newwrote { + write!(newacc, ", ")?; + } else { + write!(newacc, " ")?; + newwrote = true; + } + + for diff in Itertools::zip_longest( + old_version.into_iter(), + new_version.into_iter(), + ) { + match diff { + EitherOrBoth::Left(old_comp) => { + match old_comp { + Ok(old_comp) => { + write!(oldacc, "{old}", old = old_comp.red())?; + }, + Err(ignored) => { + write!(oldacc, "{ignored}")?; + }, + } + }, + + EitherOrBoth::Right(new_comp) => { + match new_comp { + Ok(new_comp) => { + write!(newacc, "{new}", new = new_comp.green())?; + }, + Err(ignored) => { + write!(newacc, "{ignored}")?; + }, + } + }, + + EitherOrBoth::Both(old_comp, new_comp) => { + if let Err(ignored) = old_comp { + write!(oldacc, "{ignored}")?; + } + + if let Err(ignored) = new_comp { + write!(newacc, "{ignored}")?; + } + + if let (Ok(old_comp), Ok(new_comp)) = (old_comp, new_comp) { + if old_comp == new_comp { + write!(oldacc, "{old}", old = old_comp.yellow())?; + write!(newacc, "{new}", new = new_comp.yellow())?; + } else { + write!(oldacc, "{old}", old = old_comp.red())?; + write!(newacc, "{new}", new = new_comp.green())?; + } + } + }, + } + } + }, + } + } + + write!( + writer, + "{oldacc}{arrow}{newacc}", + arrow = if !oldacc.is_empty() && !newacc.is_empty() { + " ->" + } else { + "" + } + )?; + + writeln!(writer)?; + } + + Ok(diffs.len()) +} + +/// Spawns a task to compute the data required by [`write_size_diffln`]. +#[must_use] +pub fn spawn_size_diff( + path_old: PathBuf, + path_new: PathBuf, +) -> thread::JoinHandle> { + log::debug!("calculating closure sizes in background"); + + thread::spawn(move || { + let mut connection = store::connect()?; + + Ok::<_, Error>(( + connection.query_closure_size(&path_old)?, + connection.query_closure_size(&path_new)?, + )) + }) +} + +/// Writes the size difference. +pub fn write_size_diffln( + writer: &mut impl fmt::Write, + size_old: Size, + size_new: Size, +) -> fmt::Result { + let size_diff = size_new - size_old; + + writeln!( + writer, + "{header}: {size_old} -> {size_new}", + header = "SIZE".bold(), + size_old = size_old.red(), + size_new = size_new.green(), + )?; + + writeln!( + writer, + "{header}: {size_diff}", + header = "DIFF".bold(), + size_diff = if size_diff.bytes() > 0 { + size_diff.green() + } else { + size_diff.red() + }, + ) +} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 4305c6d..0000000 --- a/src/error.rs +++ /dev/null @@ -1,123 +0,0 @@ -use thiserror::Error; - -/// Application errors with thiserror -#[derive(Debug, Error)] -pub enum AppError { - #[error("Command failed: {command} {args:?} - {message}")] - CommandFailed { - command: String, - args: Vec, - message: String, - }, - - #[error("Failed to decode command output from {context}: {source}")] - CommandOutputError { - source: std::str::Utf8Error, - context: String, - }, - - #[error("Failed to parse data in {context}: {message}")] - ParseError { - message: String, - context: String, - #[source] - source: Option>, - }, - - #[error("Regex error in {context}: {source}")] - RegexError { - source: regex::Error, - context: String, - }, - - #[error("IO error in {context}: {source}")] - IoError { - source: std::io::Error, - context: String, - }, - - #[error("Database error: {source}")] - DatabaseError { source: rusqlite::Error }, -} - -// Implement From traits to support the ? operator -impl From for AppError { - fn from(source: std::io::Error) -> Self { - Self::IoError { - source, - context: "unknown context".into(), - } - } -} - -impl From for AppError { - fn from(source: std::str::Utf8Error) -> Self { - Self::CommandOutputError { - source, - context: "command output".into(), - } - } -} - -impl From for AppError { - fn from(source: rusqlite::Error) -> Self { - Self::DatabaseError { source } - } -} - -impl From for AppError { - fn from(source: regex::Error) -> Self { - Self::RegexError { - source, - context: "regex operation".into(), - } - } -} - -impl AppError { - /// Create a command failure error with context - pub fn command_failed>(command: S, args: &[&str], message: S) -> Self { - Self::CommandFailed { - command: command.into(), - args: args.iter().map(|&s| s.to_string()).collect(), - message: message.into(), - } - } - - /// Create a parse error with context - pub fn parse_error, C: Into>( - message: S, - context: C, - source: Option>, - ) -> Self { - Self::ParseError { - message: message.into(), - context: context.into(), - source, - } - } - - /// Create an IO error with context - pub fn io_error>(source: std::io::Error, context: C) -> Self { - Self::IoError { - source, - context: context.into(), - } - } - - /// Create a regex error with context - pub fn regex_error>(source: regex::Error, context: C) -> Self { - Self::RegexError { - source, - context: context.into(), - } - } - - /// Create a command output error with context - pub fn command_output_error>(source: std::str::Utf8Error, context: C) -> Self { - Self::CommandOutputError { - source, - context: context.into(), - } - } -} diff --git a/src/lib.rs b/src/lib.rs index f820eb9..f1d73d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,98 @@ -pub mod error; -pub mod print; -pub mod store; -pub mod util; +use std::{ + path::PathBuf, + sync, +}; + +use anyhow::{ + Context as _, + Error, + Result, + anyhow, + bail, +}; +use derive_more::Deref; + +mod diff; +pub use diff::{ + spawn_size_diff, + write_paths_diffln, + write_size_diffln, +}; + +mod store; + +mod version; +use version::Version; + +#[derive(Deref, Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct DerivationId(i64); + +/// A validated store path. Always starts with /nix/store. +/// +/// Can be created using `StorePath::try_from(path_buf)`. +#[derive(Deref, Debug, Clone, PartialEq, Eq, Hash)] +pub struct StorePath(PathBuf); + +impl TryFrom for StorePath { + type Error = Error; + + fn try_from(path: PathBuf) -> Result { + if !path.starts_with("/nix/store") { + bail!( + "path {path} must start with /nix/store", + path = path.display(), + ); + } + + Ok(StorePath(path)) + } +} + +impl StorePath { + /// Parses a Nix store path to extract the packages name and possibly its + /// version. + /// + /// This function first drops the inputs first 44 chars, since that is exactly + /// the length of the `/nix/store/0004yybkm5hnwjyxv129js3mjp7kbrax-` prefix. + /// Then it matches that against our store path regex. + fn parse_name_and_version(&self) -> Result<(&str, Option)> { + static STORE_PATH_REGEX: sync::LazyLock = + sync::LazyLock::new(|| { + regex::Regex::new("(.+?)(-([0-9].*?))?$") + .expect("failed to compile regex for Nix store paths") + }); + + let path = self.to_str().with_context(|| { + format!( + "failed to convert path '{path}' to valid unicode", + path = self.display(), + ) + })?; + + // We can strip the path since it _always_ follows the format: + // + // /nix/store/0004yybkm5hnwjyxv129js3mjp7kbrax-... + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + // This part is exactly 44 chars long, so we just remove it. + assert_eq!(&path[..11], "/nix/store/"); + assert_eq!(&path[43..44], "-"); + let path = &path[44..]; + + log::debug!("stripped path: {path}"); + + let captures = STORE_PATH_REGEX.captures(path).ok_or_else(|| { + anyhow!("path '{path}' does not match expected Nix store format") + })?; + + let name = captures.get(1).map_or("", |capture| capture.as_str()); + if name.is_empty() { + bail!("failed to extract name from path '{path}'"); + } + + let version: Option = captures.get(2).map(|capture| { + Version::from(capture.as_str().trim_start_matches('-').to_owned()) + }); + + Ok((name, version)) + } +} diff --git a/src/main.rs b/src/main.rs index aa63a1d..5131478 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,214 +1,133 @@ -use clap::Parser; -use core::str; -use dixlib::print; -use dixlib::store; -use dixlib::util::PackageDiff; -use log::{debug, error}; use std::{ - collections::{HashMap, HashSet}, - thread, + fmt::{ + self, + Write as _, + }, + io::{ + self, + Write as _, + }, + path::PathBuf, + process, }; -use yansi::Paint; -#[derive(Parser, Debug)] -#[command(name = "dix")] -#[command(version = "1.0")] -#[command(about = "Diff Nix stuff", long_about = None)] -#[command(version, about, long_about = None)] -struct Args { - path: std::path::PathBuf, - path2: std::path::PathBuf, +use anyhow::{ + Result, + anyhow, +}; +use clap::Parser as _; +use yansi::Paint as _; - /// Print the whole store paths - #[arg(short, long)] - paths: bool, +struct WriteFmt(W); - /// Print the closure size - #[arg(long, short)] - closure_size: bool, - - /// Verbosity level: -v for debug, -vv for trace - #[arg(short, long, action = clap::ArgAction::Count)] - verbose: u8, - - /// Silence all output except errors - #[arg(short, long)] - quiet: bool, +impl fmt::Write for WriteFmt { + fn write_str(&mut self, string: &str) -> fmt::Result { + self.0.write_all(string.as_bytes()).map_err(|_| fmt::Error) + } } -#[derive(Debug, Clone)] -struct Package<'a> { - name: &'a str, - versions: HashSet<&'a str>, - /// Save if a package is a dependency of another package - is_dep: bool, +#[derive(clap::Parser, Debug)] +#[command(version, about)] +struct Cli { + old_path: PathBuf, + new_path: PathBuf, + + #[command(flatten)] + verbose: clap_verbosity_flag::Verbosity, } -impl<'a> Package<'a> { - fn new(name: &'a str, version: &'a str, is_dep: bool) -> Self { - let mut versions = HashSet::new(); - versions.insert(version); - Self { - name, - versions, - is_dep, - } - } +fn real_main() -> Result<()> { + let Cli { + old_path, + new_path, + verbose, + } = Cli::parse(); - fn add_version(&mut self, version: &'a str) { - self.versions.insert(version); - } + yansi::whenever(yansi::Condition::TTY_AND_COLOR); + + env_logger::Builder::new() + .filter_level(verbose.log_level_filter()) + .format(|out, arguments| { + let header = match arguments.level() { + log::Level::Error => "error:".red(), + log::Level::Warn => "warn:".yellow(), + log::Level::Info => "info:".green(), + log::Level::Debug => "debug:".blue(), + log::Level::Trace => "trace:".cyan(), + }; + + writeln!(out, "{header} {message}", message = arguments.args()) + }) + .init(); + + let mut out = WriteFmt(io::stdout()); + + // Handle to the thread collecting closure size information. + // We do this as early as possible because Nix is slow. + let closure_size_handle = + dix::spawn_size_diff(old_path.clone(), new_path.clone()); + + let wrote = dix::write_paths_diffln(&mut out, &old_path, &new_path)?; + + let (size_old, size_new) = closure_size_handle + .join() + .map_err(|_| anyhow!("failed to get closure size due to thread error"))??; + + if wrote > 0 { + writeln!(out)?; + } + + dix::write_size_diffln(&mut out, size_old, size_new)?; + + Ok(()) } -#[allow(clippy::cognitive_complexity, clippy::too_many_lines)] +#[allow(clippy::allow_attributes, clippy::exit)] fn main() { - let args = Args::parse(); + let Err(error) = real_main() else { + return; + }; - // Configure logger based on verbosity flags and environment variables - // Respects RUST_LOG environment variable if present. - // XXX:We can also dedicate a specific env variable for this tool, if we want to. - let env = env_logger::Env::default().filter_or( - "RUST_LOG", - if args.quiet { - "error" - } else { - match args.verbose { - 0 => "info", - 1 => "debug", - _ => "trace", - } - }, + let mut err = io::stderr(); + + let mut message = String::new(); + let mut chain = error.chain().rev().peekable(); + + while let Some(error) = chain.next() { + let _ = write!( + err, + "{header} ", + header = if chain.peek().is_none() { + "error:" + } else { + "cause:" + } + .red() + .bold(), ); - // Build and initialize the logger - env_logger::Builder::from_env(env) - .format_timestamp(Some(env_logger::fmt::TimestampPrecision::Seconds)) - .init(); + String::clear(&mut message); + let _ = write!(message, "{error}"); - // handles to the threads collecting closure size information - // We do this as early as possible because nix is slow. - let closure_size_handles = if args.closure_size { - debug!("Calculating closure sizes in background"); - let path = args.path.clone(); - let path2 = args.path2.clone(); - Some(( - thread::spawn(move || store::get_closure_size(&path)), - thread::spawn(move || store::get_closure_size(&path2)), - )) - } else { - None + let mut chars = message.char_indices(); + + let _ = match (chars.next(), chars.next()) { + (Some((_, first)), Some((second_start, second))) + if second.is_lowercase() => + { + writeln!( + err, + "{first_lowercase}{rest}", + first_lowercase = first.to_lowercase(), + rest = &message[second_start..], + ) + }, + + _ => { + writeln!(err, "{message}") + }, }; + } - // Get package lists and handle potential errors - let package_list_pre = match store::get_packages(&args.path) { - Ok(packages) => { - debug!("Found {} packages in first closure", packages.len()); - packages.into_iter().map(|(_, path)| path).collect() - } - Err(e) => { - error!( - "Error getting packages from path {}: {}", - args.path.display(), - e - ); - eprintln!( - "Error getting packages from path {}: {}", - args.path.display(), - e - ); - Vec::new() - } - }; - - let package_list_post = match store::get_packages(&args.path2) { - Ok(packages) => { - debug!("Found {} packages in second closure", packages.len()); - packages.into_iter().map(|(_, path)| path).collect() - } - Err(e) => { - error!( - "Error getting packages from path {}: {}", - args.path2.display(), - e - ); - eprintln!( - "Error getting packages from path {}: {}", - args.path2.display(), - e - ); - Vec::new() - } - }; - - let PackageDiff { - pkg_to_versions_pre: pre, - pkg_to_versions_post: post, - pre_keys: _, - post_keys: _, - added, - removed, - changed, - } = PackageDiff::new(&package_list_pre, &package_list_post); - - debug!("Added packages: {}", added.len()); - debug!("Removed packages: {}", removed.len()); - debug!( - "Changed packages: {}", - changed - .iter() - .filter(|p| !p.is_empty() - && match (pre.get(*p), post.get(*p)) { - (Some(ver_pre), Some(ver_post)) => ver_pre != ver_post, - _ => false, - }) - .count() - ); - - println!("Difference between the two generations:"); - println!(); - - let width_changes = changed - .iter() - .filter(|&&p| match (pre.get(p), post.get(p)) { - (Some(version_pre), Some(version_post)) => version_pre != version_post, - _ => false, - }); - - let col_width = added - .iter() - .chain(removed.iter()) - .chain(width_changes) - .map(|p| p.len()) - .max() - .unwrap_or_default(); - - println!("<<< {}", args.path.to_string_lossy()); - println!(">>> {}", args.path2.to_string_lossy()); - print::print_added(&added, &post, col_width); - print::print_removed(&removed, &pre, col_width); - print::print_changes(&changed, &pre, &post, col_width); - - if let Some((pre_handle, post_handle)) = closure_size_handles { - match (pre_handle.join(), post_handle.join()) { - (Ok(Ok(pre_size)), Ok(Ok(post_size))) => { - let pre_size = pre_size / 1024 / 1024; - let post_size = post_size / 1024 / 1024; - debug!("Pre closure size: {pre_size} MiB"); - debug!("Post closure size: {post_size} MiB"); - - println!("{}", "Closure Size:".underline().bold()); - println!("Before: {pre_size} MiB"); - println!("After: {post_size} MiB"); - println!("Difference: {} MiB", post_size - pre_size); - } - (Ok(Err(e)), _) | (_, Ok(Err(e))) => { - error!("Error getting closure size: {e}"); - eprintln!("Error getting closure size: {e}"); - } - _ => { - error!("Failed to get closure size information due to a thread error"); - eprintln!("Error: Failed to get closure size information due to a thread error"); - } - } - } + process::exit(1); } diff --git a/src/print.rs b/src/print.rs deleted file mode 100644 index d0c537c..0000000 --- a/src/print.rs +++ /dev/null @@ -1,190 +0,0 @@ -use core::str; -use regex::Regex; -use std::{ - collections::{HashMap, HashSet}, - string::ToString, - sync::OnceLock, -}; -use yansi::Paint; - -/// diffs two strings character by character, and returns a tuple of strings -/// colored in a way to represent the differences between the two input strings. -/// -/// # Returns: -/// -/// * (String, String) - The differing chars being red in the left, and green in the right one. -fn diff_versions(left: &str, right: &str) -> (String, String) { - let mut prev = "\x1b[33m".to_string(); - let mut post = "\x1b[33m".to_string(); - - // We only have to filter the left once, since we stop if the left one is empty. - // We do this to display things like -man, -dev properly. - let matches = name_regex().captures(left); - let mut suffix = String::new(); - - if let Some(m) = matches { - let tmp = m.get(0).map_or("", |m| m.as_str()); - suffix.push_str(tmp); - } - // string without the suffix - let filtered_left = &left[..left.len() - suffix.len()]; - let filtered_right = &right[..right.len() - suffix.len()]; - - for diff in diff::chars(filtered_left, filtered_right) { - match diff { - diff::Result::Both(l, _) => { - let string_to_push = format!("{l}"); - prev.push_str(&string_to_push); - post.push_str(&string_to_push); - } - diff::Result::Left(l) => { - let string_to_push = format!("\x1b[1;91m{l}"); - prev.push_str(&string_to_push); - } - - diff::Result::Right(r) => { - let string_to_push = format!("\x1b[1;92m{r}"); - post.push_str(&string_to_push); - } - } - } - - // push removed suffix - prev.push_str(&format!("\x1b[33m{}", &suffix)); - post.push_str(&format!("\x1b[33m{}", &suffix)); - - //reset - prev.push_str("\x1b[0m"); - post.push_str("\x1b[0m"); - - (prev, post) -} - -/// print the packages added between two closures. -pub fn print_added(set: &HashSet<&str>, post: &HashMap<&str, HashSet<&str>>, col_width: usize) { - println!("{}", "Packages added:".underline().bold()); - - // Use sorted outpu - let mut sorted: Vec<_> = set - .iter() - .filter_map(|p| post.get(p).map(|ver| (*p, ver))) - .collect(); - - // Sort by package name for consistent output - sorted.sort_by(|(a, _), (b, _)| a.cmp(b)); - - for (p, ver) in sorted { - let mut version_vec = ver.iter().copied().collect::>(); - version_vec.sort_unstable(); - let version_str = version_vec.join(", "); - println!( - "[{}] {:col_width$} \x1b[33m{}\x1b[0m", - "A:".green().bold(), - p, - version_str - ); - } -} - -/// print the packages removed between two closures. -pub fn print_removed(set: &HashSet<&str>, pre: &HashMap<&str, HashSet<&str>>, col_width: usize) { - println!("{}", "Packages removed:".underline().bold()); - - // Use sorted output for more predictable and readable results - let mut sorted: Vec<_> = set - .iter() - .filter_map(|p| pre.get(p).map(|ver| (*p, ver))) - .collect(); - - // Sort by package name for consistent output - sorted.sort_by(|(a, _), (b, _)| a.cmp(b)); - - for (p, ver) in sorted { - let mut version_vec = ver.iter().copied().collect::>(); - version_vec.sort_unstable(); - let version_str = version_vec.join(", "); - println!( - "[{}] {:col_width$} \x1b[33m{}\x1b[0m", - "R:".red().bold(), - p, - version_str - ); - } -} - -pub fn print_changes( - set: &HashSet<&str>, - pre: &HashMap<&str, HashSet<&str>>, - post: &HashMap<&str, HashSet<&str>>, - col_width: usize, -) { - println!("{}", "Version changes:".underline().bold()); - - // Use sorted output for more predictable and readable results - let mut changes = Vec::new(); - - for p in set.iter().filter(|p| !p.is_empty()) { - if let (Some(ver_pre), Some(ver_post)) = (pre.get(p), post.get(p)) { - if ver_pre != ver_post { - changes.push((*p, ver_pre, ver_post)); - } - } - } - - // Sort by package name for consistent output - changes.sort_by(|(a, _, _), (b, _, _)| a.cmp(b)); - - for (p, ver_pre, ver_post) in changes { - let mut version_vec_pre = ver_pre.difference(ver_post).copied().collect::>(); - let mut version_vec_post = ver_post.difference(ver_pre).copied().collect::>(); - - version_vec_pre.sort_unstable(); - version_vec_post.sort_unstable(); - - let mut diffed_pre: String; - let diffed_post: String; - - if version_vec_pre.len() == version_vec_post.len() { - let mut diff_pre: Vec = vec![]; - let mut diff_post: Vec = vec![]; - - for (pre, post) in version_vec_pre.iter().zip(version_vec_post.iter()) { - let (a, b) = diff_versions(pre, post); - diff_pre.push(a); - diff_post.push(b); - } - diffed_pre = diff_pre.join(", "); - diffed_post = diff_post.join(", "); - } else { - let version_str_pre = version_vec_pre.join(", "); - let version_str_post = version_vec_post.join(", "); - (diffed_pre, diffed_post) = diff_versions(&version_str_pre, &version_str_post); - } - - // push a space to the diffed_pre, if it is non-empty, we do this here and not in the println - // in order to properly align the ±. - if !version_vec_pre.is_empty() { - let mut tmp = " ".to_string(); - tmp.push_str(&diffed_pre); - diffed_pre = tmp; - } - - println!( - "[{}] {:col_width$}{} \x1b[0m\u{00B1}\x1b[0m {}", - "C:".bold().bright_yellow(), - p, - diffed_pre, - diffed_post - ); - } -} - -// Returns a reference to the compiled regex pattern. -// The regex is compiled only once. -fn name_regex() -> &'static Regex { - static REGEX: OnceLock = OnceLock::new(); - REGEX.get_or_init(|| { - Regex::new(r"(-man|-lib|-doc|-dev|-out|-terminfo)") - .expect("Failed to compile regex pattern for name") - }) -} diff --git a/src/store.rs b/src/store.rs index d62c17e..51baaf0 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,115 +1,200 @@ -use std::collections::HashMap; +use std::{ + collections::HashMap, + path::Path, + result, +}; -use crate::error::AppError; -use rusqlite::Connection; +use anyhow::{ + Context as _, + Result, + anyhow, +}; +use derive_more::Deref; +use rusqlite::OpenFlags; +use size::Size; -// Use type alias for Result with our custom error type -type Result = std::result::Result; +use crate::{ + DerivationId, + StorePath, +}; -const DATABASE_URL: &str = "/nix/var/nix/db/db.sqlite"; +#[derive(Deref)] +pub struct Connection(rusqlite::Connection); -const QUERY_PKGS: &str = " -WITH RECURSIVE - graph(p) AS ( - SELECT id - FROM ValidPaths - WHERE path = ? - UNION - SELECT reference FROM Refs - JOIN graph ON referrer = p - ) -SELECT id, path from graph -JOIN ValidPaths ON id = p; -"; - -const QUERY_CLOSURE_SIZE: &str = " -WITH RECURSIVE - graph(p) AS ( - SELECT id - FROM ValidPaths - WHERE path = ? - UNION - SELECT reference FROM Refs - JOIN graph ON referrer = p - ) -SELECT SUM(narSize) as sum from graph -JOIN ValidPaths ON p = id; -"; - -const QUERY_DEPENDENCY_GRAPH: &str = " -WITH RECURSIVE - graph(p, c) AS ( - SELECT id as par, reference as chd - FROM ValidPaths - JOIN Refs ON referrer = id - WHERE path = ? - UNION - SELECT referrer as par, reference as chd FROM Refs - JOIN graph ON referrer = c - ) -SELECT p, c from graph; -"; - -/// executes a query on the nix db directly -/// to gather all derivations that the derivation given by the path -/// depends on +/// Connects to the Nix database /// -/// The ids of the derivations in the database are returned as well, since these -/// can be used to later convert nodes (represented by the the ids) of the -/// dependency graph to actual paths -/// -/// in the future, we might wan't to switch to async -pub fn get_packages(path: &std::path::Path) -> Result> { - // resolve symlinks and convert to a string - let p: String = path.canonicalize()?.to_string_lossy().into_owned(); - let conn = Connection::open(DATABASE_URL)?; +/// and sets some basic settings +pub fn connect() -> Result { + const DATABASE_PATH: &str = "/nix/var/nix/db/db.sqlite"; - let mut stmt = conn.prepare_cached(QUERY_PKGS)?; - let queried_pkgs: std::result::Result, _> = stmt - .query_map([p], |row| Ok((row.get(0)?, row.get(1)?)))? - .collect(); - Ok(queried_pkgs?) + let inner = rusqlite::Connection::open_with_flags( + DATABASE_PATH, + OpenFlags::SQLITE_OPEN_READ_ONLY // We only run queries, safeguard against corrupting the DB. + | OpenFlags::SQLITE_OPEN_NO_MUTEX // Part of the default flags, rusqlite takes care of locking anyways. + | OpenFlags::SQLITE_OPEN_URI, + ) + .with_context(|| { + format!("failed to connect to Nix database at {DATABASE_PATH}") + })?; + + // Perform a batched query to set some settings using PRAGMA + // the main performance bottleneck when dix was run before + // was that the database file has to be brought from disk into + // memory. + // + // We read a large part of the DB anyways in each query, + // so it makes sense to set aside a large region of memory-mapped + // I/O prevent incurring page faults which can be done using + // `mmap_size`. + // + // This made a performance difference of about 500ms (but only + // when it was first run for a long time!). + // + // The file pages of the store can be evicted from main memory + // using `dd of=/nix/var/nix/db/db.sqlite oflag=nocache conv=notrunc,fdatasync + // count=0` if you want to test this. Source: . + // + // Documentation about the settings can be found here: + // + // [0]: 256MB, enough to fit the whole DB (at least on my system - Dragyx). + // [1]: Always store temporary tables ain memory. + inner + .execute_batch( + " + PRAGMA mmap_size=268435456; -- See [0]. + PRAGMA temp_store=2; -- See [1]. + PRAGMA query_only; + ", + ) + .with_context(|| { + format!("failed to cache Nix database at {DATABASE_PATH}") + })?; + + Ok(Connection(inner)) } -/// executes a query on the nix db directly -/// to get the total closure size of the derivation -/// by summing up the nar size of all derivations -/// depending on the derivation -/// -/// in the future, we might wan't to switch to async -pub fn get_closure_size(path: &std::path::Path) -> Result { - // resolve symlinks and convert to a string - let p: String = path.canonicalize()?.to_string_lossy().into_owned(); - let conn = Connection::open(DATABASE_URL)?; +fn path_to_canonical_string(path: &Path) -> Result { + let path = path.canonicalize().with_context(|| { + format!( + "failed to canonicalize path '{path}'", + path = path.display(), + ) + })?; - let mut stmt = conn.prepare_cached(QUERY_CLOSURE_SIZE)?; - let queried_sum = stmt.query_row([p], |row| row.get(0))?; - Ok(queried_sum) + let path = path.into_os_string().into_string().map_err(|path| { + anyhow!( + "failed to convert path '{path}' to valid unicode", + path = Path::new(&*path).display(), /* TODO: use .display() directly + * after Rust 1.87.0 in flake. */ + ) + })?; + + Ok(path) } -/// returns the complete dependency graph of -/// of the derivation as an adjacency list. The nodes are -/// represented by the DB ids -/// -/// We might want to collect the paths in the graph directly as -/// well in the future, depending on how much we use them -/// in the operations on the graph -/// -/// The mapping from id to graph can be obtained by using [``get_packages``] -pub fn get_dependency_graph(path: &std::path::Path) -> Result>> { - // resolve symlinks and convert to a string - let p: String = path.canonicalize()?.to_string_lossy().into_owned(); - let conn = Connection::open(DATABASE_URL)?; +impl Connection { + /// Gets the total closure size of the given store path by summing up the nar + /// size of all dependent derivations. + pub fn query_closure_size(&mut self, path: &Path) -> Result { + const QUERY: &str = " + WITH RECURSIVE + graph(p) AS ( + SELECT id + FROM ValidPaths + WHERE path = ? + UNION + SELECT reference FROM Refs + JOIN graph ON referrer = p + ) + SELECT SUM(narSize) as sum from graph + JOIN ValidPaths ON p = id; + "; - let mut stmt = conn.prepare_cached(QUERY_DEPENDENCY_GRAPH)?; - let mut adj = HashMap::>::new(); - let queried_edges = - stmt.query_map([p], |row| Ok::<(i64, i64), _>((row.get(0)?, row.get(1)?)))?; - for row in queried_edges { - let (from, to) = row?; - adj.entry(from).or_default().push(to); - adj.entry(to).or_default(); + let path = path_to_canonical_string(path)?; + + let closure_size = self + .prepare_cached(QUERY)? + .query_row([path], |row| Ok(Size::from_bytes(row.get::<_, i64>(0)?)))?; + + Ok(closure_size) + } + + /// Gathers all derivations that the given profile path depends on. + pub fn query_dependents( + &mut self, + path: &Path, + ) -> Result> { + const QUERY: &str = " + WITH RECURSIVE + graph(p) AS ( + SELECT id + FROM ValidPaths + WHERE path = ? + UNION + SELECT reference FROM Refs + JOIN graph ON referrer = p + ) + SELECT id, path from graph + JOIN ValidPaths ON id = p; + "; + + let path = path_to_canonical_string(path)?; + + let packages: result::Result, _> = self + .prepare_cached(QUERY)? + .query_map([path], |row| { + Ok(( + DerivationId(row.get(0)?), + StorePath(row.get::<_, String>(1)?.into()), + )) + })? + .collect(); + + Ok(packages?) + } + + /// Gathers the complete dependency graph of of the store path as an adjacency + /// list. + /// + /// We might want to collect the paths in the graph directly as + /// well in the future, depending on how much we use them + /// in the operations on the graph. + #[expect(dead_code)] + pub fn query_dependency_graph( + &mut self, + path: &StorePath, + ) -> Result>> { + const QUERY: &str = " + WITH RECURSIVE + graph(p, c) AS ( + SELECT id as par, reference as chd + FROM ValidPaths + JOIN Refs ON referrer = id + WHERE path = ? + UNION + SELECT referrer as par, reference as chd FROM Refs + JOIN graph ON referrer = c + ) + SELECT p, c from graph; + "; + + let path = path_to_canonical_string(path)?; + + let mut adj = HashMap::>::new(); + + let mut statement = self.prepare_cached(QUERY)?; + + let edges = statement.query_map([path], |row| { + Ok((DerivationId(row.get(0)?), DerivationId(row.get(1)?))) + })?; + + for row in edges { + let (from, to) = row?; + + adj.entry(from).or_default().push(to); + adj.entry(to).or_default(); } Ok(adj) + } } diff --git a/src/util.rs b/src/util.rs deleted file mode 100644 index 6a34273..0000000 --- a/src/util.rs +++ /dev/null @@ -1,246 +0,0 @@ -use std::{ - cmp::Ordering, - collections::{HashMap, HashSet}, - sync::OnceLock, -}; - -use crate::error::AppError; -use log::debug; -use regex::Regex; - -// Use type alias for Result with our custom error type -type Result = std::result::Result; - -use std::string::ToString; - -#[derive(Eq, PartialEq, Debug)] -enum VersionComponent { - Number(u64), - Text(String), -} - -impl std::cmp::Ord for VersionComponent { - fn cmp(&self, other: &Self) -> Ordering { - use VersionComponent::{Number, Text}; - match (self, other) { - (Number(x), Number(y)) => x.cmp(y), - (Text(x), Text(y)) => match (x.as_str(), y.as_str()) { - ("pre", _) => Ordering::Less, - (_, "pre") => Ordering::Greater, - _ => x.cmp(y), - }, - (Text(_), Number(_)) => Ordering::Less, - (Number(_), Text(_)) => Ordering::Greater, - } - } -} - -impl PartialOrd for VersionComponent { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -// takes a version string and outputs the different components -// -// a component is delimited by '-' or '.' and consists of just digits or letters -struct VersionComponentIterator<'a> { - v: &'a [u8], - pos: usize, -} - -impl<'a> VersionComponentIterator<'a> { - pub fn new>(v: I) -> Self { - Self { - v: v.into().as_bytes(), - pos: 0, - } - } -} - -impl Iterator for VersionComponentIterator<'_> { - type Item = VersionComponent; - - fn next(&mut self) -> Option { - // skip all '-' and '.' in the beginning - while let Some(b'.' | b'-') = self.v.get(self.pos) { - self.pos += 1; - } - - // get the next character and decide if it is a digit or char - let c = self.v.get(self.pos)?; - let is_digit = c.is_ascii_digit(); - // based on this collect characters after this into the component - let component_len = self.v[self.pos..] - .iter() - .copied() - .take_while(|&c| c.is_ascii_digit() == is_digit && c != b'.' && c != b'-') - .count(); - let component = - String::from_utf8_lossy(&self.v[self.pos..(self.pos + component_len)]).into_owned(); - - // remember what chars we used - self.pos += component_len; - - if component.is_empty() { - None - } else if is_digit { - component.parse::().ok().map(VersionComponent::Number) - } else { - Some(VersionComponent::Text(component)) - } - } -} - -/// Compares two strings of package versions, and figures out the greater one. -/// -/// # Returns -/// -/// * Ordering -pub fn compare_versions(a: &str, b: &str) -> Ordering { - let iter_a = VersionComponentIterator::new(a); - let iter_b = VersionComponentIterator::new(b); - - iter_a.cmp(iter_b) -} - -/// Parses a nix store path to extract the packages name and version -/// -/// This function first drops the inputs first 44 chars, since that is exactly the length of the /nix/store/... prefix. Then it matches that against our store path regex. -/// -/// # Returns -/// -/// * Result<(&'a str, &'a str)> - The Package's name and version, or an error if -/// one or both cannot be retrieved. -pub fn get_version<'a>(pack: impl Into<&'a str>) -> Result<(&'a str, &'a str)> { - let path = pack.into(); - - // We can strip the path since it _always_ follows the format - // /nix/store/<...>--...... - // This part is exactly 44 chars long, so we just remove it. - let stripped_path = &path[44..]; - debug!("Stripped path: {stripped_path}"); - - // Match the regex against the input - if let Some(cap) = store_path_regex().captures(stripped_path) { - // Handle potential missing captures safely - let name = cap.get(1).map_or("", |m| m.as_str()); - let mut version = cap.get(2).map_or("", |m| m.as_str()); - - if version.starts_with('-') { - version = &version[1..]; - } - - if name.is_empty() { - return Err(AppError::ParseError { - message: format!("Failed to extract name from path: {path}"), - context: "get_version".to_string(), - source: None, - }); - } - - return Ok((name, version)); - } - - Err(AppError::ParseError { - message: format!("Path does not match expected nix store format: {path}"), - context: "get_version".to_string(), - source: None, - }) -} - -// Returns a reference to the compiled regex pattern. -// The regex is compiled only once. -pub fn store_path_regex() -> &'static Regex { - static REGEX: OnceLock = OnceLock::new(); - REGEX.get_or_init(|| { - Regex::new(r"(.+?)(-([0-9].*?))?$") - .expect("Failed to compile regex pattern for nix store paths") - }) -} - -// TODO: move this somewhere else, this does not really -// belong into this file -pub struct PackageDiff<'a> { - pub pkg_to_versions_pre: HashMap<&'a str, HashSet<&'a str>>, - pub pkg_to_versions_post: HashMap<&'a str, HashSet<&'a str>>, - pub pre_keys: HashSet<&'a str>, - pub post_keys: HashSet<&'a str>, - pub added: HashSet<&'a str>, - pub removed: HashSet<&'a str>, - pub changed: HashSet<&'a str>, -} - -impl<'a> PackageDiff<'a> { - pub fn new + 'a>(pkgs_pre: &'a [S], pkgs_post: &'a [S]) -> Self { - // Map from packages of the first closure to their version - let mut pre = HashMap::<&str, HashSet<&str>>::new(); - let mut post = HashMap::<&str, HashSet<&str>>::new(); - - for p in pkgs_pre { - match get_version(p.as_ref()) { - Ok((name, version)) => { - pre.entry(name).or_default().insert(version); - } - Err(e) => { - debug!("Error parsing package version: {e}"); - } - } - } - - for p in pkgs_post { - match get_version(p.as_ref()) { - Ok((name, version)) => { - post.entry(name).or_default().insert(version); - } - Err(e) => { - debug!("Error parsing package version: {e}"); - } - } - } - - // Compare the package names of both versions - let pre_keys: HashSet<&str> = pre.keys().copied().collect(); - let post_keys: HashSet<&str> = post.keys().copied().collect(); - - // Difference gives us added and removed packages - let added: HashSet<&str> = &post_keys - &pre_keys; - - let removed: HashSet<&str> = &pre_keys - &post_keys; - // Get the intersection of the package names for version changes - let changed: HashSet<&str> = &pre_keys & &post_keys; - Self { - pkg_to_versions_pre: pre, - pkg_to_versions_post: post, - pre_keys, - post_keys, - added, - removed, - changed, - } - } -} - -mod test { - - #[test] - fn test_version_component_iter() { - use super::VersionComponent::{Number, Text}; - use crate::util::VersionComponentIterator; - let v = "132.1.2test234-1-man----.--.......---------..---"; - - let comp: Vec<_> = VersionComponentIterator::new(v).collect(); - assert_eq!( - comp, - [ - Number(132), - Number(1), - Number(2), - Text("test".into()), - Number(234), - Number(1), - Text("man".into()) - ] - ); - } -} diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 0000000..24386bf --- /dev/null +++ b/src/version.rs @@ -0,0 +1,147 @@ +use std::cmp; + +use derive_more::{ + Deref, + DerefMut, + Display, + From, +}; + +#[derive(Deref, DerefMut, Display, Debug, Clone, PartialEq, Eq, From)] +pub struct Version(String); + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl cmp::Ord for Version { + fn cmp(&self, that: &Self) -> cmp::Ordering { + let this = VersionComponentIter::from(&***self).filter_map(Result::ok); + let that = VersionComponentIter::from(&***that).filter_map(Result::ok); + + this.cmp(that) + } +} + +impl<'a> IntoIterator for &'a Version { + type Item = Result, &'a str>; + + type IntoIter = VersionComponentIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + VersionComponentIter::from(&***self) + } +} + +#[derive(Display, Debug, Clone, Copy, Eq, PartialEq)] +pub enum VersionComponent<'a> { + Number(u64), + Text(&'a str), +} + +impl PartialOrd for VersionComponent<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl cmp::Ord for VersionComponent<'_> { + fn cmp(&self, other: &Self) -> cmp::Ordering { + use VersionComponent::{ + Number, + Text, + }; + + match (*self, *other) { + (Number(this), Number(that)) => this.cmp(&that), + (Text(this), Text(that)) => { + match (this, that) { + ("pre", _) => cmp::Ordering::Less, + (_, "pre") => cmp::Ordering::Greater, + _ => this.cmp(that), + } + }, + (Text(_), Number(_)) => cmp::Ordering::Less, + (Number(_), Text(_)) => cmp::Ordering::Greater, + } + } +} + +/// Yields [`VertionComponent`] from a version string. +#[derive(Deref, DerefMut, From)] +pub struct VersionComponentIter<'a>(&'a str); + +impl<'a> Iterator for VersionComponentIter<'a> { + type Item = Result, &'a str>; + + fn next(&mut self) -> Option { + if self.starts_with(['.', '-', '*', '×', ' ']) { + let len = self.chars().next().unwrap().len_utf8(); + let (this, rest) = self.split_at(len); + + **self = rest; + return Some(Err(this)); + } + + // Get the next character and decide if it is a digit. + let is_digit = self.chars().next()?.is_ascii_digit(); + + // Based on this collect characters after this into the component. + let component_len = self + .chars() + .take_while(|&char| { + char.is_ascii_digit() == is_digit + && !matches!(char, '.' | '-' | '*' | ' ' | '×') + }) + .map(char::len_utf8) + .sum(); + + let component = &self[..component_len]; + **self = &self[component_len..]; + + assert!(!component.is_empty()); + + if is_digit { + component + .parse::() + .ok() + .map(VersionComponent::Number) + .map(Ok) + } else { + Some(Ok(VersionComponent::Text(component))) + } + } +} + +#[cfg(test)] +mod tests { + use crate::version::{ + VersionComponent::{ + Number, + Text, + }, + VersionComponentIter, + }; + + #[test] + fn version_component_iter() { + let version = "132.1.2test234-1-man----.--.......---------..---"; + + assert_eq!( + VersionComponentIter::from(version) + .filter_map(Result::ok) + .collect::>(), + [ + Number(132), + Number(1), + Number(2), + Text("test"), + Number(234), + Number(1), + Text("man") + ] + ); + } +}