diff --git a/.github/dix.png b/.github/dix.png new file mode 100644 index 0000000..17b337f Binary files /dev/null and b/.github/dix.png differ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index a4f8b89..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,20 +0,0 @@ -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 1afea7e..6a2460c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ /.direnv /target -/result + + +# Added by cargo +# +# already existing elements were commented out + +#/target diff --git a/.rustfmt.toml b/.rustfmt.toml deleted file mode 100644 index df184f2..0000000 --- a/.rustfmt.toml +++ /dev/null @@ -1,30 +0,0 @@ -# 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 deleted file mode 100644 index 9abeaee..0000000 --- a/.taplo.toml +++ /dev/null @@ -1,15 +0,0 @@ -# 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 b91736c..52df5be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,10 +62,27 @@ dependencies = [ ] [[package]] -name = "anyhow" -version = "1.0.98" +name = "atty" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +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" [[package]] name = "bitflags" @@ -73,6 +90,18 @@ 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" @@ -82,6 +111,23 @@ 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" @@ -92,16 +138,6 @@ 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" @@ -139,34 +175,85 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] -name = "convert_case" -version = "0.7.1" +name = "criterion" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +checksum = "b01d6de93b2b6c65e17c634a26653a29d107b3c98c607c765bf38d041531cd8f" dependencies = [ - "unicode-segmentation", + "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", ] [[package]] -name = "derive_more" -version = "2.0.1" +name = "criterion-plot" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876" dependencies = [ - "derive_more-impl", + "cast", + "itertools", ] [[package]] -name = "derive_more-impl" -version = "2.0.1" +name = "crossbeam-deque" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "syn", - "unicode-xid", + "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", ] [[package]] @@ -179,18 +266,15 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" name = "dix" version = "0.1.0" dependencies = [ - "anyhow", - "clap", - "clap-verbosity-flag", - "derive_more", + "clap 4.5.37", + "criterion", "diff", "env_logger", - "itertools", + "libc", "log", "regex", "rusqlite", - "size", - "unicode-width", + "thiserror", "yansi", ] @@ -241,6 +325,12 @@ 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" @@ -267,19 +357,11 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.1" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" - -[[package]] -name = "is-terminal" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ - "hermit-abi", "libc", - "windows-sys", ] [[package]] @@ -290,13 +372,19 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" -version = "0.14.0" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 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" @@ -321,6 +409,22 @@ 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" @@ -350,18 +454,61 @@ 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" @@ -395,6 +542,26 @@ 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" @@ -430,7 +597,7 @@ version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b" dependencies = [ - "bitflags", + "bitflags 2.9.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -438,6 +605,27 @@ 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" @@ -447,6 +635,16 @@ 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" @@ -458,18 +656,24 @@ 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" @@ -493,29 +697,56 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.1.14" 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" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "utf8parse" @@ -529,6 +760,115 @@ 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" @@ -607,6 +947,3 @@ 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 fa5129d..95a9578 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,105 +1,39 @@ [package] -name = "dix" -description = "Diff Nix" -version = "0.1.0" -edition = "2024" +name = "dix" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "dix" +path = "src/main.rs" + +[lib] +name = "dixlib" +path = "src/lib.rs" + [dependencies] -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" ] } +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" -[lints.clippy] -pedantic = { level = "warn", priority = -1 } +[dev-dependencies] +criterion = "0.3" +libc = "0.2" -blanket_clippy_restriction_lints = "allow" -restriction = { level = "warn", priority = -1 } +[[bench]] +name = "store" +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" +[[bench]] +name = "print" +harness=false + +[[bench]] +name = "util" +harness=false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + 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 deleted file mode 100644 index 5e144cb..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,611 +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/README.md b/README.md index 645e2ce..f866b0e 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,13 @@ -# Diff Nix +# dix -A tool to diff any Nix related thing. +## diff Nix stuff -Currently only supports closures (a derivation graph, such as a system build or -package). +Currently only system closures ## Usage `dix /nix/var/profiles/system--link /run/current-system` -## Output +# Why dix? -![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 . -``` +![dix nuts](.github/dix.png) diff --git a/benches/common.rs b/benches/common.rs new file mode 100644 index 0000000..debf5d3 --- /dev/null +++ b/benches/common.rs @@ -0,0 +1,89 @@ +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 new file mode 100644 index 0000000..af2832c --- /dev/null +++ b/benches/print.rs @@ -0,0 +1,86 @@ +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 new file mode 100644 index 0000000..cac7b52 --- /dev/null +++ b/benches/store.rs @@ -0,0 +1,36 @@ +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 new file mode 100644 index 0000000..ec8eb19 --- /dev/null +++ b/benches/util.rs @@ -0,0 +1,15 @@ +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 f1b0f1a..957dc1d 100644 --- a/flake.lock +++ b/flake.lock @@ -19,42 +19,21 @@ "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": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "lastModified": 1689347949, + "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "repo": "default-linux", + "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", "type": "github" }, "original": { "owner": "nix-systems", - "repo": "default", + "repo": "default-linux", "type": "github" } } diff --git a/flake.nix b/flake.nix index 1193ad9..bf63f2e 100644 --- a/flake.nix +++ b/flake.nix @@ -1,13 +1,8 @@ { - description = "Dix - Diff Nix"; - + description = "Nix version differ"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - systems.url = "github:nix-systems/default"; - rust-overlay = { - url = "github:oxalica/rust-overlay"; - inputs.nixpkgs.follows = "nixpkgs"; - }; + systems.url = "github:nix-systems/default-linux"; }; outputs = inputs: let @@ -15,18 +10,8 @@ pkgsFor = inputs.nixpkgs.legacyPackages; in { packages = eachSystem (system: { - 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"; - }; + default = inputs.self.packages.${system}.ralc; + ralc = pkgsFor.${system}.callPackage ./nix/package.nix {}; }); devShells = eachSystem (system: { @@ -36,18 +21,13 @@ (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 deleted file mode 100644 index 277662b..0000000 Binary files a/images/dix.png and /dev/null differ diff --git a/nix/package.nix b/nix/package.nix deleted file mode 100644 index 4852111..0000000 --- a/nix/package.nix +++ /dev/null @@ -1,18 +0,0 @@ -{ - 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 deleted file mode 100644 index 38a7beb..0000000 --- a/src/diff.rs +++ /dev/null @@ -1,437 +0,0 @@ -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 new file mode 100644 index 0000000..4305c6d --- /dev/null +++ b/src/error.rs @@ -0,0 +1,123 @@ +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 f1d73d9..f820eb9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,98 +1,4 @@ -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)) - } -} +pub mod error; +pub mod print; +pub mod store; +pub mod util; diff --git a/src/main.rs b/src/main.rs index 5131478..aa63a1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,133 +1,214 @@ +use clap::Parser; +use core::str; +use dixlib::print; +use dixlib::store; +use dixlib::util::PackageDiff; +use log::{debug, error}; use std::{ - fmt::{ - self, - Write as _, - }, - io::{ - self, - Write as _, - }, - path::PathBuf, - process, + collections::{HashMap, HashSet}, + thread, }; +use yansi::Paint; -use anyhow::{ - Result, - anyhow, -}; -use clap::Parser as _; -use yansi::Paint as _; +#[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, -struct WriteFmt(W); + /// Print the whole store paths + #[arg(short, long)] + paths: 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) - } + /// 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, } -#[derive(clap::Parser, Debug)] -#[command(version, about)] -struct Cli { - old_path: PathBuf, - new_path: PathBuf, - - #[command(flatten)] - verbose: clap_verbosity_flag::Verbosity, +#[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, } -fn real_main() -> Result<()> { - let Cli { - old_path, - new_path, - verbose, - } = Cli::parse(); +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, + } + } - 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(()) + fn add_version(&mut self, version: &'a str) { + self.versions.insert(version); + } } -#[allow(clippy::allow_attributes, clippy::exit)] +#[allow(clippy::cognitive_complexity, clippy::too_many_lines)] fn main() { - let Err(error) = real_main() else { - return; - }; + let args = Args::parse(); - 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(), + // 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", + } + }, ); - String::clear(&mut message); - let _ = write!(message, "{error}"); + // Build and initialize the logger + env_logger::Builder::from_env(env) + .format_timestamp(Some(env_logger::fmt::TimestampPrecision::Seconds)) + .init(); - 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}") - }, + // 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 }; - } - process::exit(1); + // 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"); + } + } + } } diff --git a/src/print.rs b/src/print.rs new file mode 100644 index 0000000..d0c537c --- /dev/null +++ b/src/print.rs @@ -0,0 +1,190 @@ +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 51baaf0..d62c17e 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,200 +1,115 @@ -use std::{ - collections::HashMap, - path::Path, - result, -}; +use std::collections::HashMap; -use anyhow::{ - Context as _, - Result, - anyhow, -}; -use derive_more::Deref; -use rusqlite::OpenFlags; -use size::Size; +use crate::error::AppError; +use rusqlite::Connection; -use crate::{ - DerivationId, - StorePath, -}; +// Use type alias for Result with our custom error type +type Result = std::result::Result; -#[derive(Deref)] -pub struct Connection(rusqlite::Connection); +const DATABASE_URL: &str = "/nix/var/nix/db/db.sqlite"; -/// Connects to the Nix database +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 /// -/// and sets some basic settings -pub fn connect() -> Result { - const DATABASE_PATH: &str = "/nix/var/nix/db/db.sqlite"; +/// 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)?; - 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)) + 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?) } -fn path_to_canonical_string(path: &Path) -> Result { - let path = path.canonicalize().with_context(|| { - format!( - "failed to canonicalize path '{path}'", - path = path.display(), - ) - })?; +/// 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)?; - 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) + let mut stmt = conn.prepare_cached(QUERY_CLOSURE_SIZE)?; + let queried_sum = stmt.query_row([p], |row| row.get(0))?; + Ok(queried_sum) } -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; - "; +/// 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)?; - 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(); + 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(); } Ok(adj) - } } diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..6a34273 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,246 @@ +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 deleted file mode 100644 index 24386bf..0000000 --- a/src/version.rs +++ /dev/null @@ -1,147 +0,0 @@ -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") - ] - ); - } -}