├── .clippy.toml ├── .deny.toml ├── .envrc ├── .github ├── dix.png └── workflows │ └── nix.yml ├── .gitignore ├── .rustfmt.toml ├── .taplo.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── flake.lock ├── flake.nix └── src ├── diff.rs ├── lib.rs ├── main.rs ├── store.rs └── version.rs /.clippy.toml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.deny.toml: -------------------------------------------------------------------------------- 1 | [graph] 2 | all-features = true 3 | 4 | # cargo deny is really only ever intended to run on the "normal" tier-1 targets. 5 | targets = [ 6 | "x86_64-unknown-linux-gnu", 7 | "aarch64-unknown-linux-gnu", 8 | "x86_64-unknown-linux-musl", 9 | "aarch64-apple-darwin", 10 | "x86_64-apple-darwin", 11 | "x86_64-pc-windows-msvc", 12 | ] 13 | 14 | [bans] 15 | multiple-versions = "deny" 16 | wildcards = "deny" 17 | 18 | deny = [ 19 | { crate = "git2", use-instead = "gix" }, 20 | { crate = "openssl", use-instead = "rustls" }, 21 | { crate = "openssl-sys", use-instead = "rustls" }, 22 | "libssh2-sys", 23 | { crate = "cmake", use-instead = "cc" }, 24 | { crate = "windows", reason = "bloated and unnecessary", use-instead = "ideally inline bindings, practically, windows-sys" }, 25 | ] 26 | skip = [ 27 | { crate = "heck@0.4.1", reason = "ouroboros uses this old version" }, 28 | { crate = "hashbrown@0.14.5", reason = "gix uses this old version" }, 29 | { crate = "core-foundation@0.9.4", reason = "reqwest -> system-configuration uses this old version" }, 30 | { crate = "getrandom@0.2.15", reason = "ring uses this old version" }, 31 | ] 32 | skip-tree = [ 33 | { crate = "windows-sys@0.52.0", reason = "a foundational crate for many that bumps far too frequently to ever have a shared version" }, 34 | { crate = "thiserror@1.0.69", reason = "gix depends on both the 1.0 and 2.0 versions" }, 35 | ] 36 | 37 | [sources] 38 | unknown-git = "deny" 39 | unknown-registry = "deny" 40 | 41 | [licenses] 42 | allow = [ 43 | "GPL-3.0", 44 | "Apache-2.0", 45 | "Apache-2.0 WITH LLVM-exception", 46 | "MIT", 47 | "MPL-2.0", 48 | "BSD-3-Clause", 49 | "ISC", 50 | "Unicode-3.0", 51 | "Zlib", 52 | ] 53 | confidence-threshold = 0.93 54 | 55 | [[licenses.clarify]] 56 | crate = "webpki" 57 | expression = "ISC" 58 | license-files = [ { path = "LICENSE", hash = 0x001c7e6c } ] 59 | 60 | # Actually "ISC-style". 61 | [[licenses.clarify]] 62 | crate = "rustls-webpki" 63 | expression = "ISC" 64 | license-files = [ { path = "LICENSE", hash = 0x001c7e6c } ] 65 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/dix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloxx12/dix/19b4fb0151e250e080affbed6716ce631c504066/.github/dix.png -------------------------------------------------------------------------------- /.github/workflows/nix.yml: -------------------------------------------------------------------------------- 1 | name: Nix CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | check: 9 | name: Nix CI (${{ matrix.system }}) 10 | runs-on: ${{ matrix.runner_label }} 11 | 12 | permissions: 13 | id-token: write 14 | contents: read 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | include: 20 | - system: x86_64-linux 21 | runner_label: ubuntu-latest 22 | - system: x86_64-darwin 23 | runner_label: macos-13 24 | - system: aarch64-darwin 25 | runner_label: macos-14 26 | 27 | steps: 28 | - name: Checkout Repository 29 | uses: actions/checkout@v4 30 | 31 | - name: Install Nix 32 | uses: DeterminateSystems/nix-installer-action@main 33 | with: 34 | determinate: true 35 | extra-conf: lazy-trees = true 36 | 37 | - name: Set Up Cachix 38 | uses: cachix/cachix-action@v14 39 | with: 40 | name: dix 41 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 42 | 43 | - name: Run `nix flake check` 44 | run: nix flake check 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.direnv 2 | /target 3 | /result 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/cull-os/carcass. 2 | # Modified to have 2 space indents and 80 line width. 3 | 4 | # float_literal_trailing_zero = "Always" # TODO: Warning for some reason? 5 | condense_wildcard_suffixes = true 6 | doc_comment_code_block_width = 80 7 | edition = "2024" # Keep in sync with Cargo.toml. 8 | enum_discrim_align_threshold = 60 9 | force_explicit_abi = false 10 | force_multiline_blocks = true 11 | format_code_in_doc_comments = true 12 | format_macro_matchers = true 13 | format_strings = true 14 | group_imports = "StdExternalCrate" 15 | hex_literal_case = "Upper" 16 | imports_granularity = "Crate" 17 | imports_layout = "Vertical" 18 | inline_attribute_width = 60 19 | match_block_trailing_comma = true 20 | max_width = 80 21 | newline_style = "Unix" 22 | normalize_comments = true 23 | normalize_doc_attributes = true 24 | overflow_delimited_expr = true 25 | struct_field_align_threshold = 60 26 | tab_spaces = 2 27 | unstable_features = true 28 | use_field_init_shorthand = true 29 | use_try_shorthand = true 30 | wrap_comments = true 31 | -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/cull-os/carcass. 2 | 3 | [formatting] 4 | align_entries = true 5 | column_width = 100 6 | compact_arrays = false 7 | reorder_inline_tables = true 8 | reorder_keys = true 9 | 10 | [[rule]] 11 | include = [ "**/Cargo.toml" ] 12 | keys = [ "package" ] 13 | 14 | [rule.formatting] 15 | reorder_keys = false 16 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "aliasable" 16 | version = "0.1.3" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.18" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.10" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.6" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.2" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 55 | dependencies = [ 56 | "windows-sys", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.7" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 64 | dependencies = [ 65 | "anstyle", 66 | "once_cell", 67 | "windows-sys", 68 | ] 69 | 70 | [[package]] 71 | name = "anyhow" 72 | version = "1.0.98" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 75 | 76 | [[package]] 77 | name = "autocfg" 78 | version = "1.4.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 81 | 82 | [[package]] 83 | name = "bit-set" 84 | version = "0.8.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" 87 | dependencies = [ 88 | "bit-vec", 89 | ] 90 | 91 | [[package]] 92 | name = "bit-vec" 93 | version = "0.8.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" 96 | 97 | [[package]] 98 | name = "bitflags" 99 | version = "2.9.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 102 | 103 | [[package]] 104 | name = "cc" 105 | version = "1.2.21" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0" 108 | dependencies = [ 109 | "shlex", 110 | ] 111 | 112 | [[package]] 113 | name = "cfg-if" 114 | version = "1.0.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 117 | 118 | [[package]] 119 | name = "clap" 120 | version = "4.5.37" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" 123 | dependencies = [ 124 | "clap_builder", 125 | "clap_derive", 126 | ] 127 | 128 | [[package]] 129 | name = "clap-verbosity-flag" 130 | version = "3.0.2" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84" 133 | dependencies = [ 134 | "clap", 135 | "log", 136 | ] 137 | 138 | [[package]] 139 | name = "clap_builder" 140 | version = "4.5.37" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" 143 | dependencies = [ 144 | "anstream", 145 | "anstyle", 146 | "clap_lex", 147 | "strsim", 148 | ] 149 | 150 | [[package]] 151 | name = "clap_derive" 152 | version = "4.5.32" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 155 | dependencies = [ 156 | "heck 0.5.0", 157 | "proc-macro2", 158 | "quote", 159 | "syn", 160 | ] 161 | 162 | [[package]] 163 | name = "clap_lex" 164 | version = "0.7.4" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 167 | 168 | [[package]] 169 | name = "colorchoice" 170 | version = "1.0.3" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 173 | 174 | [[package]] 175 | name = "convert_case" 176 | version = "0.7.1" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" 179 | dependencies = [ 180 | "unicode-segmentation", 181 | ] 182 | 183 | [[package]] 184 | name = "derive_more" 185 | version = "2.0.1" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 188 | dependencies = [ 189 | "derive_more-impl", 190 | ] 191 | 192 | [[package]] 193 | name = "derive_more-impl" 194 | version = "2.0.1" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 197 | dependencies = [ 198 | "convert_case", 199 | "proc-macro2", 200 | "quote", 201 | "syn", 202 | "unicode-xid", 203 | ] 204 | 205 | [[package]] 206 | name = "diff" 207 | version = "0.1.13" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 210 | 211 | [[package]] 212 | name = "dix" 213 | version = "1.0.0" 214 | dependencies = [ 215 | "anyhow", 216 | "clap", 217 | "clap-verbosity-flag", 218 | "derive_more", 219 | "diff", 220 | "env_logger", 221 | "itertools", 222 | "log", 223 | "ouroboros", 224 | "proptest", 225 | "regex", 226 | "rusqlite", 227 | "size", 228 | "unicode-width", 229 | "yansi", 230 | ] 231 | 232 | [[package]] 233 | name = "either" 234 | version = "1.15.0" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 237 | 238 | [[package]] 239 | name = "env_filter" 240 | version = "0.1.3" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 243 | dependencies = [ 244 | "log", 245 | "regex", 246 | ] 247 | 248 | [[package]] 249 | name = "env_logger" 250 | version = "0.11.8" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 253 | dependencies = [ 254 | "anstream", 255 | "anstyle", 256 | "env_filter", 257 | "jiff", 258 | "log", 259 | ] 260 | 261 | [[package]] 262 | name = "errno" 263 | version = "0.3.11" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 266 | dependencies = [ 267 | "libc", 268 | "windows-sys", 269 | ] 270 | 271 | [[package]] 272 | name = "fallible-iterator" 273 | version = "0.3.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" 276 | 277 | [[package]] 278 | name = "fallible-streaming-iterator" 279 | version = "0.1.9" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 282 | 283 | [[package]] 284 | name = "fastrand" 285 | version = "2.3.0" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 288 | 289 | [[package]] 290 | name = "fnv" 291 | version = "1.0.7" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 294 | 295 | [[package]] 296 | name = "foldhash" 297 | version = "0.1.5" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 300 | 301 | [[package]] 302 | name = "getrandom" 303 | version = "0.2.16" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 306 | dependencies = [ 307 | "cfg-if", 308 | "libc", 309 | "wasi 0.11.0+wasi-snapshot-preview1", 310 | ] 311 | 312 | [[package]] 313 | name = "getrandom" 314 | version = "0.3.3" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 317 | dependencies = [ 318 | "cfg-if", 319 | "libc", 320 | "r-efi", 321 | "wasi 0.14.2+wasi-0.2.4", 322 | ] 323 | 324 | [[package]] 325 | name = "hashbrown" 326 | version = "0.15.3" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 329 | dependencies = [ 330 | "foldhash", 331 | ] 332 | 333 | [[package]] 334 | name = "hashlink" 335 | version = "0.10.0" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 338 | dependencies = [ 339 | "hashbrown", 340 | ] 341 | 342 | [[package]] 343 | name = "heck" 344 | version = "0.4.1" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 347 | 348 | [[package]] 349 | name = "heck" 350 | version = "0.5.0" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 353 | 354 | [[package]] 355 | name = "hermit-abi" 356 | version = "0.5.1" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" 359 | 360 | [[package]] 361 | name = "is-terminal" 362 | version = "0.4.16" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" 365 | dependencies = [ 366 | "hermit-abi", 367 | "libc", 368 | "windows-sys", 369 | ] 370 | 371 | [[package]] 372 | name = "is_terminal_polyfill" 373 | version = "1.70.1" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 376 | 377 | [[package]] 378 | name = "itertools" 379 | version = "0.14.0" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 382 | dependencies = [ 383 | "either", 384 | ] 385 | 386 | [[package]] 387 | name = "jiff" 388 | version = "0.2.12" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "d07d8d955d798e7a4d6f9c58cd1f1916e790b42b092758a9ef6e16fef9f1b3fd" 391 | dependencies = [ 392 | "jiff-static", 393 | "log", 394 | "portable-atomic", 395 | "portable-atomic-util", 396 | "serde", 397 | ] 398 | 399 | [[package]] 400 | name = "jiff-static" 401 | version = "0.2.12" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "f244cfe006d98d26f859c7abd1318d85327e1882dc9cef80f62daeeb0adcf300" 404 | dependencies = [ 405 | "proc-macro2", 406 | "quote", 407 | "syn", 408 | ] 409 | 410 | [[package]] 411 | name = "lazy_static" 412 | version = "1.5.0" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 415 | 416 | [[package]] 417 | name = "libc" 418 | version = "0.2.172" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 421 | 422 | [[package]] 423 | name = "libsqlite3-sys" 424 | version = "0.33.0" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa" 427 | dependencies = [ 428 | "cc", 429 | "pkg-config", 430 | "vcpkg", 431 | ] 432 | 433 | [[package]] 434 | name = "linux-raw-sys" 435 | version = "0.9.4" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 438 | 439 | [[package]] 440 | name = "log" 441 | version = "0.4.27" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 444 | 445 | [[package]] 446 | name = "memchr" 447 | version = "2.7.4" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 450 | 451 | [[package]] 452 | name = "num-traits" 453 | version = "0.2.19" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 456 | dependencies = [ 457 | "autocfg", 458 | ] 459 | 460 | [[package]] 461 | name = "once_cell" 462 | version = "1.21.3" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 465 | 466 | [[package]] 467 | name = "ouroboros" 468 | version = "0.18.5" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" 471 | dependencies = [ 472 | "aliasable", 473 | "ouroboros_macro", 474 | "static_assertions", 475 | ] 476 | 477 | [[package]] 478 | name = "ouroboros_macro" 479 | version = "0.18.5" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" 482 | dependencies = [ 483 | "heck 0.4.1", 484 | "proc-macro2", 485 | "proc-macro2-diagnostics", 486 | "quote", 487 | "syn", 488 | ] 489 | 490 | [[package]] 491 | name = "pkg-config" 492 | version = "0.3.32" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 495 | 496 | [[package]] 497 | name = "portable-atomic" 498 | version = "1.11.0" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 501 | 502 | [[package]] 503 | name = "portable-atomic-util" 504 | version = "0.2.4" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 507 | dependencies = [ 508 | "portable-atomic", 509 | ] 510 | 511 | [[package]] 512 | name = "ppv-lite86" 513 | version = "0.2.21" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 516 | dependencies = [ 517 | "zerocopy", 518 | ] 519 | 520 | [[package]] 521 | name = "proc-macro2" 522 | version = "1.0.95" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 525 | dependencies = [ 526 | "unicode-ident", 527 | ] 528 | 529 | [[package]] 530 | name = "proc-macro2-diagnostics" 531 | version = "0.10.1" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" 534 | dependencies = [ 535 | "proc-macro2", 536 | "quote", 537 | "syn", 538 | "version_check", 539 | "yansi", 540 | ] 541 | 542 | [[package]] 543 | name = "proptest" 544 | version = "1.6.0" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" 547 | dependencies = [ 548 | "bit-set", 549 | "bit-vec", 550 | "bitflags", 551 | "lazy_static", 552 | "num-traits", 553 | "rand", 554 | "rand_chacha", 555 | "rand_xorshift", 556 | "regex-syntax", 557 | "rusty-fork", 558 | "tempfile", 559 | "unarray", 560 | ] 561 | 562 | [[package]] 563 | name = "quick-error" 564 | version = "1.2.3" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 567 | 568 | [[package]] 569 | name = "quote" 570 | version = "1.0.40" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 573 | dependencies = [ 574 | "proc-macro2", 575 | ] 576 | 577 | [[package]] 578 | name = "r-efi" 579 | version = "5.2.0" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 582 | 583 | [[package]] 584 | name = "rand" 585 | version = "0.8.5" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 588 | dependencies = [ 589 | "libc", 590 | "rand_chacha", 591 | "rand_core", 592 | ] 593 | 594 | [[package]] 595 | name = "rand_chacha" 596 | version = "0.3.1" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 599 | dependencies = [ 600 | "ppv-lite86", 601 | "rand_core", 602 | ] 603 | 604 | [[package]] 605 | name = "rand_core" 606 | version = "0.6.4" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 609 | dependencies = [ 610 | "getrandom 0.2.16", 611 | ] 612 | 613 | [[package]] 614 | name = "rand_xorshift" 615 | version = "0.3.0" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" 618 | dependencies = [ 619 | "rand_core", 620 | ] 621 | 622 | [[package]] 623 | name = "regex" 624 | version = "1.11.1" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 627 | dependencies = [ 628 | "aho-corasick", 629 | "memchr", 630 | "regex-automata", 631 | "regex-syntax", 632 | ] 633 | 634 | [[package]] 635 | name = "regex-automata" 636 | version = "0.4.9" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 639 | dependencies = [ 640 | "aho-corasick", 641 | "memchr", 642 | "regex-syntax", 643 | ] 644 | 645 | [[package]] 646 | name = "regex-syntax" 647 | version = "0.8.5" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 650 | 651 | [[package]] 652 | name = "rusqlite" 653 | version = "0.35.0" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b" 656 | dependencies = [ 657 | "bitflags", 658 | "fallible-iterator", 659 | "fallible-streaming-iterator", 660 | "hashlink", 661 | "libsqlite3-sys", 662 | "smallvec", 663 | ] 664 | 665 | [[package]] 666 | name = "rustix" 667 | version = "1.0.7" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 670 | dependencies = [ 671 | "bitflags", 672 | "errno", 673 | "libc", 674 | "linux-raw-sys", 675 | "windows-sys", 676 | ] 677 | 678 | [[package]] 679 | name = "rusty-fork" 680 | version = "0.3.0" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" 683 | dependencies = [ 684 | "fnv", 685 | "quick-error", 686 | "tempfile", 687 | "wait-timeout", 688 | ] 689 | 690 | [[package]] 691 | name = "serde" 692 | version = "1.0.219" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 695 | dependencies = [ 696 | "serde_derive", 697 | ] 698 | 699 | [[package]] 700 | name = "serde_derive" 701 | version = "1.0.219" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 704 | dependencies = [ 705 | "proc-macro2", 706 | "quote", 707 | "syn", 708 | ] 709 | 710 | [[package]] 711 | name = "shlex" 712 | version = "1.3.0" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 715 | 716 | [[package]] 717 | name = "size" 718 | version = "0.5.0" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b" 721 | 722 | [[package]] 723 | name = "smallvec" 724 | version = "1.15.0" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 727 | 728 | [[package]] 729 | name = "static_assertions" 730 | version = "1.1.0" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 733 | 734 | [[package]] 735 | name = "strsim" 736 | version = "0.11.1" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 739 | 740 | [[package]] 741 | name = "syn" 742 | version = "2.0.101" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 745 | dependencies = [ 746 | "proc-macro2", 747 | "quote", 748 | "unicode-ident", 749 | ] 750 | 751 | [[package]] 752 | name = "tempfile" 753 | version = "3.19.1" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 756 | dependencies = [ 757 | "fastrand", 758 | "getrandom 0.3.3", 759 | "once_cell", 760 | "rustix", 761 | "windows-sys", 762 | ] 763 | 764 | [[package]] 765 | name = "unarray" 766 | version = "0.1.4" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" 769 | 770 | [[package]] 771 | name = "unicode-ident" 772 | version = "1.0.18" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 775 | 776 | [[package]] 777 | name = "unicode-segmentation" 778 | version = "1.12.0" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 781 | 782 | [[package]] 783 | name = "unicode-width" 784 | version = "0.2.0" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 787 | 788 | [[package]] 789 | name = "unicode-xid" 790 | version = "0.2.6" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 793 | 794 | [[package]] 795 | name = "utf8parse" 796 | version = "0.2.2" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 799 | 800 | [[package]] 801 | name = "vcpkg" 802 | version = "0.2.15" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 805 | 806 | [[package]] 807 | name = "version_check" 808 | version = "0.9.5" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 811 | 812 | [[package]] 813 | name = "wait-timeout" 814 | version = "0.2.1" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" 817 | dependencies = [ 818 | "libc", 819 | ] 820 | 821 | [[package]] 822 | name = "wasi" 823 | version = "0.11.0+wasi-snapshot-preview1" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 826 | 827 | [[package]] 828 | name = "wasi" 829 | version = "0.14.2+wasi-0.2.4" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 832 | dependencies = [ 833 | "wit-bindgen-rt", 834 | ] 835 | 836 | [[package]] 837 | name = "windows-sys" 838 | version = "0.59.0" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 841 | dependencies = [ 842 | "windows-targets", 843 | ] 844 | 845 | [[package]] 846 | name = "windows-targets" 847 | version = "0.52.6" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 850 | dependencies = [ 851 | "windows_aarch64_gnullvm", 852 | "windows_aarch64_msvc", 853 | "windows_i686_gnu", 854 | "windows_i686_gnullvm", 855 | "windows_i686_msvc", 856 | "windows_x86_64_gnu", 857 | "windows_x86_64_gnullvm", 858 | "windows_x86_64_msvc", 859 | ] 860 | 861 | [[package]] 862 | name = "windows_aarch64_gnullvm" 863 | version = "0.52.6" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 866 | 867 | [[package]] 868 | name = "windows_aarch64_msvc" 869 | version = "0.52.6" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 872 | 873 | [[package]] 874 | name = "windows_i686_gnu" 875 | version = "0.52.6" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 878 | 879 | [[package]] 880 | name = "windows_i686_gnullvm" 881 | version = "0.52.6" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 884 | 885 | [[package]] 886 | name = "windows_i686_msvc" 887 | version = "0.52.6" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 890 | 891 | [[package]] 892 | name = "windows_x86_64_gnu" 893 | version = "0.52.6" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 896 | 897 | [[package]] 898 | name = "windows_x86_64_gnullvm" 899 | version = "0.52.6" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 902 | 903 | [[package]] 904 | name = "windows_x86_64_msvc" 905 | version = "0.52.6" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 908 | 909 | [[package]] 910 | name = "wit-bindgen-rt" 911 | version = "0.39.0" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 914 | dependencies = [ 915 | "bitflags", 916 | ] 917 | 918 | [[package]] 919 | name = "yansi" 920 | version = "1.0.1" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 923 | dependencies = [ 924 | "is-terminal", 925 | ] 926 | 927 | [[package]] 928 | name = "zerocopy" 929 | version = "0.8.25" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" 932 | dependencies = [ 933 | "zerocopy-derive", 934 | ] 935 | 936 | [[package]] 937 | name = "zerocopy-derive" 938 | version = "0.8.25" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" 941 | dependencies = [ 942 | "proc-macro2", 943 | "quote", 944 | "syn", 945 | ] 946 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dix" 3 | description = "Diff Nix" 4 | version = "1.0.0" 5 | license = "GPL-3.0-only" 6 | edition = "2024" 7 | homepage = "https://github.com/bloxx12/dix" 8 | repository = "https://github.com/bloxx12/dix" 9 | keywords = [ "nix", "nixos" ] 10 | 11 | [dependencies] 12 | anyhow = "1.0.98" 13 | clap = { version = "4.5.37", features = [ "derive" ] } 14 | clap-verbosity-flag = "3.0.2" 15 | derive_more = { version = "2.0.1", features = [ "full" ] } 16 | diff = "0.1.13" 17 | env_logger = "0.11.3" 18 | itertools = "0.14.0" 19 | log = "0.4.20" 20 | ouroboros = "0.18.5" 21 | regex = "1.11.1" 22 | rusqlite = { version = "0.35.0", features = [ "bundled" ] } 23 | size = "0.5.0" 24 | unicode-width = "0.2.0" 25 | yansi = { version = "1.0.1", features = [ "detect-env", "detect-tty" ] } 26 | 27 | [dev-dependencies] 28 | proptest = "1.6.0" 29 | 30 | [lints.clippy] 31 | pedantic = { level = "warn", priority = -1 } 32 | 33 | blanket_clippy_restriction_lints = "allow" 34 | restriction = { level = "warn", priority = -1 } 35 | 36 | alloc_instead_of_core = "allow" 37 | allow_attributes_without_reason = "allow" 38 | arbitrary_source_item_ordering = "allow" 39 | arithmetic_side_effects = "allow" 40 | as_conversions = "allow" 41 | as_pointer_underscore = "allow" 42 | as_underscore = "allow" 43 | big_endian_bytes = "allow" 44 | clone_on_ref_ptr = "allow" 45 | dbg_macro = "allow" 46 | disallowed_script_idents = "allow" 47 | else_if_without_else = "allow" 48 | error_impl_error = "allow" 49 | exhaustive_enums = "allow" 50 | exhaustive_structs = "allow" 51 | expect_used = "allow" 52 | field_scoped_visibility_modifiers = "allow" 53 | float_arithmetic = "allow" 54 | host_endian_bytes = "allow" 55 | impl_trait_in_params = "allow" 56 | implicit_return = "allow" 57 | indexing_slicing = "allow" 58 | inline_asm_x86_intel_syntax = "allow" 59 | integer_division = "allow" 60 | integer_division_remainder_used = "allow" 61 | large_include_file = "allow" 62 | let_underscore_must_use = "allow" 63 | let_underscore_untyped = "allow" 64 | little_endian_bytes = "allow" 65 | map_err_ignore = "allow" 66 | match_same_arms = "allow" 67 | missing_assert_message = "allow" 68 | missing_docs_in_private_items = "allow" 69 | missing_errors_doc = "allow" 70 | missing_inline_in_public_items = "allow" 71 | missing_panics_doc = "allow" 72 | missing_trait_methods = "allow" 73 | mod_module_files = "allow" 74 | multiple_inherent_impl = "allow" 75 | mutex_atomic = "allow" 76 | mutex_integer = "allow" 77 | new_without_default = "allow" 78 | non_ascii_literal = "allow" 79 | panic = "allow" 80 | panic_in_result_fn = "allow" 81 | partial_pub_fields = "allow" 82 | print_stderr = "allow" 83 | print_stdout = "allow" 84 | pub_use = "allow" 85 | pub_with_shorthand = "allow" 86 | pub_without_shorthand = "allow" 87 | question_mark_used = "allow" 88 | ref_patterns = "allow" 89 | renamed_function_params = "allow" 90 | same_name_method = "allow" 91 | semicolon_outside_block = "allow" 92 | separated_literal_suffix = "allow" 93 | shadow_reuse = "allow" 94 | shadow_same = "allow" 95 | shadow_unrelated = "allow" 96 | single_call_fn = "allow" 97 | single_char_lifetime_names = "allow" 98 | single_match_else = "allow" 99 | std_instead_of_alloc = "allow" 100 | std_instead_of_core = "allow" 101 | string_add = "allow" 102 | string_slice = "allow" 103 | todo = "allow" 104 | too_many_lines = "allow" 105 | try_err = "allow" 106 | unimplemented = "allow" 107 | unnecessary_safety_comment = "allow" 108 | unnecessary_safety_doc = "allow" 109 | unreachable = "allow" 110 | unwrap_in_result = "allow" 111 | unwrap_used = "allow" 112 | use_debug = "allow" 113 | wildcard_enum_match_arm = "allow" 114 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # GNU GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 29 June 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | Everyone is permitted to copy and distribute verbatim copies of this license 8 | document, but changing it is not allowed. 9 | 10 | ## Preamble 11 | 12 | The GNU General Public License is a free, copyleft license for software and 13 | other kinds of works. 14 | 15 | The licenses for most software and other practical works are designed to take 16 | away your freedom to share and change the works. By contrast, the GNU General 17 | Public License is intended to guarantee your freedom to share and change all 18 | versions of a program--to make sure it remains free software for all its users. 19 | We, the Free Software Foundation, use the GNU General Public License for most of 20 | our software; it applies also to any other work released this way by its 21 | authors. You can apply it to your programs, too. 22 | 23 | When we speak of free software, we are referring to freedom, not price. Our 24 | General Public Licenses are designed to make sure that you have the freedom to 25 | distribute copies of free software (and charge for them if you wish), that you 26 | receive source code or can get it if you want it, that you can change the 27 | software or use pieces of it in new free programs, and that you know you can do 28 | these things. 29 | 30 | To protect your rights, we need to prevent others from denying you these rights 31 | or asking you to surrender the rights. Therefore, you have certain 32 | responsibilities if you distribute copies of the software, or if you modify it: 33 | responsibilities to respect the freedom of others. 34 | 35 | For example, if you distribute copies of such a program, whether gratis or for a 36 | fee, you must pass on to the recipients the same freedoms that you received. You 37 | must make sure that they, too, receive or can get the source code. And you must 38 | show them these terms so they know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: (1) assert 41 | copyright on the software, and (2) offer you this License giving you legal 42 | permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains that there 45 | is no warranty for this free software. For both users' and authors' sake, the 46 | GPL requires that modified versions be marked as changed, so that their problems 47 | will not be attributed erroneously to authors of previous versions. 48 | 49 | Some devices are designed to deny users access to install or run modified 50 | versions of the software inside them, although the manufacturer can do so. This 51 | is fundamentally incompatible with the aim of protecting users' freedom to 52 | change the software. The systematic pattern of such abuse occurs in the area of 53 | products for individuals to use, which is precisely where it is most 54 | unacceptable. Therefore, we have designed this version of the GPL to prohibit 55 | the practice for those products. If such problems arise substantially in other 56 | domains, we stand ready to extend this provision to those domains in future 57 | versions of the GPL, as needed to protect the freedom of users. 58 | 59 | Finally, every program is threatened constantly by software patents. States 60 | should not allow patents to restrict development and use of software on 61 | general-purpose computers, but in those that do, we wish to avoid the special 62 | danger that patents applied to a free program could make it effectively 63 | proprietary. To prevent this, the GPL assures that patents cannot be used to 64 | render the program non-free. 65 | 66 | The precise terms and conditions for copying, distribution and modification 67 | follow. 68 | 69 | ## TERMS AND CONDITIONS 70 | 71 | ### 0. Definitions. 72 | 73 | "This License" refers to version 3 of the GNU General Public License. 74 | 75 | "Copyright" also means copyright-like laws that apply to other kinds of works, 76 | such as semiconductor masks. 77 | 78 | "The Program" refers to any copyrightable work licensed under this License. Each 79 | licensee is addressed as "you". "Licensees" and "recipients" may be individuals 80 | or organizations. 81 | 82 | To "modify" a work means to copy from or adapt all or part of the work in a 83 | fashion requiring copyright permission, other than the making of an exact copy. 84 | The resulting work is called a "modified version" of the earlier work or a work 85 | "based on" the earlier work. 86 | 87 | A "covered work" means either the unmodified Program or a work based on the 88 | Program. 89 | 90 | To "propagate" a work means to do anything with it that, without permission, 91 | would make you directly or secondarily liable for infringement under applicable 92 | copyright law, except executing it on a computer or modifying a private copy. 93 | Propagation includes copying, distribution (with or without modification), 94 | making available to the public, and in some countries other activities as well. 95 | 96 | To "convey" a work means any kind of propagation that enables other parties to 97 | make or receive copies. Mere interaction with a user through a computer network, 98 | with no transfer of a copy, is not conveying. 99 | 100 | An interactive user interface displays "Appropriate Legal Notices" to the extent 101 | that it includes a convenient and prominently visible feature that (1) displays 102 | an appropriate copyright notice, and (2) tells the user that there is no 103 | warranty for the work (except to the extent that warranties are provided), that 104 | licensees may convey the work under this License, and how to view a copy of this 105 | License. If the interface presents a list of user commands or options, such as a 106 | menu, a prominent item in the list meets this criterion. 107 | 108 | ### 1. Source Code. 109 | 110 | The "source code" for a work means the preferred form of the work for making 111 | modifications to it. "Object code" means any non-source form of a work. 112 | 113 | A "Standard Interface" means an interface that either is an official standard 114 | defined by a recognized standards body, or, in the case of interfaces specified 115 | for a particular programming language, one that is widely used among developers 116 | working in that language. 117 | 118 | The "System Libraries" of an executable work include anything, other than the 119 | work as a whole, that (a) is included in the normal form of packaging a Major 120 | Component, but which is not part of that Major Component, and (b) serves only to 121 | enable use of the work with that Major Component, or to implement a Standard 122 | Interface for which an implementation is available to the public in source code 123 | form. A "Major Component", in this context, means a major essential component 124 | (kernel, window system, and so on) of the specific operating system (if any) on 125 | which the executable work runs, or a compiler used to produce the work, or an 126 | object code interpreter used to run it. 127 | 128 | The "Corresponding Source" for a work in object code form means all the source 129 | code needed to generate, install, and (for an executable work) run the object 130 | code and to modify the work, including scripts to control those activities. 131 | However, it does not include the work's System Libraries, or general-purpose 132 | tools or generally available free programs which are used unmodified in 133 | performing those activities but which are not part of the work. For example, 134 | Corresponding Source includes interface definition files associated with source 135 | files for the work, and the source code for shared libraries and dynamically 136 | linked subprograms that the work is specifically designed to require, such as by 137 | intimate data communication or control flow between those subprograms and other 138 | parts of the work. 139 | 140 | The Corresponding Source need not include anything that users can regenerate 141 | automatically from other parts of the Corresponding Source. 142 | 143 | The Corresponding Source for a work in source code form is that same work. 144 | 145 | ### 2. Basic Permissions. 146 | 147 | All rights granted under this License are granted for the term of copyright on 148 | the Program, and are irrevocable provided the stated conditions are met. This 149 | License explicitly affirms your unlimited permission to run the unmodified 150 | Program. The output from running a covered work is covered by this License only 151 | if the output, given its content, constitutes a covered work. This License 152 | acknowledges your rights of fair use or other equivalent, as provided by 153 | copyright law. 154 | 155 | You may make, run and propagate covered works that you do not convey, without 156 | conditions so long as your license otherwise remains in force. You may convey 157 | covered works to others for the sole purpose of having them make modifications 158 | exclusively for you, or provide you with facilities for running those works, 159 | provided that you comply with the terms of this License in conveying all 160 | material for which you do not control copyright. Those thus making or running 161 | the covered works for you must do so exclusively on your behalf, under your 162 | direction and control, on terms that prohibit them from making any copies of 163 | your copyrighted material outside their relationship with you. 164 | 165 | Conveying under any other circumstances is permitted solely under the conditions 166 | stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 167 | 168 | ### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 169 | 170 | No covered work shall be deemed part of an effective technological measure under 171 | any applicable law fulfilling obligations under article 11 of the WIPO copyright 172 | treaty adopted on 20 December 1996, or similar laws prohibiting or restricting 173 | circumvention of such measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention is 177 | effected by exercising rights under this License with respect to the covered 178 | work, and you disclaim any intention to limit operation or modification of the 179 | work as a means of enforcing, against the work's users, your or third parties' 180 | legal rights to forbid circumvention of technological measures. 181 | 182 | ### 4. Conveying Verbatim Copies. 183 | 184 | You may convey verbatim copies of the Program's source code as you receive it, 185 | in any medium, provided that you conspicuously and appropriately publish on each 186 | copy an appropriate copyright notice; keep intact all notices stating that this 187 | License and any non-permissive terms added in accord with section 7 apply to the 188 | code; keep intact all notices of the absence of any warranty; and give all 189 | recipients a copy of this License along with the Program. 190 | 191 | You may charge any price or no price for each copy that you convey, and you may 192 | offer support or warranty protection for a fee. 193 | 194 | ### 5. Conveying Modified Source Versions. 195 | 196 | You may convey a work based on the Program, or the modifications to produce it 197 | from the Program, in the form of source code under the terms of section 4, 198 | provided that you also meet all of these conditions: 199 | 200 | - a) The work must carry prominent notices stating that you modified it, and 201 | giving a relevant date. 202 | - b) The work must carry prominent notices stating that it is released under 203 | this License and any conditions added under section 7. This requirement 204 | modifies the requirement in section 4 to "keep intact all notices". 205 | - c) You must license the entire work, as a whole, under this License to anyone 206 | who comes into possession of a copy. This License will therefore apply, along 207 | with any applicable section 7 additional terms, to the whole of the work, and 208 | all its parts, regardless of how they are packaged. This License gives no 209 | permission to license the work in any other way, but it does not invalidate 210 | such permission if you have separately received it. 211 | - d) If the work has interactive user interfaces, each must display Appropriate 212 | Legal Notices; however, if the Program has interactive interfaces that do not 213 | display Appropriate Legal Notices, your work need not make them do so. 214 | 215 | A compilation of a covered work with other separate and independent works, which 216 | are not by their nature extensions of the covered work, and which are not 217 | combined with it such as to form a larger program, in or on a volume of a 218 | storage or distribution medium, is called an "aggregate" if the compilation and 219 | its resulting copyright are not used to limit the access or legal rights of the 220 | compilation's users beyond what the individual works permit. Inclusion of a 221 | covered work in an aggregate does not cause this License to apply to the other 222 | parts of the aggregate. 223 | 224 | ### 6. Conveying Non-Source Forms. 225 | 226 | You may convey a covered work in object code form under the terms of sections 4 227 | and 5, provided that you also convey the machine-readable Corresponding Source 228 | under the terms of this License, in one of these ways: 229 | 230 | - a) Convey the object code in, or embodied in, a physical product (including a 231 | physical distribution medium), accompanied by the Corresponding Source fixed 232 | on a durable physical medium customarily used for software interchange. 233 | - b) Convey the object code in, or embodied in, a physical product (including a 234 | physical distribution medium), accompanied by a written offer, valid for at 235 | least three years and valid for as long as you offer spare parts or customer 236 | support for that product model, to give anyone who possesses the object code 237 | either (1) a copy of the Corresponding Source for all the software in the 238 | product that is covered by this License, on a durable physical medium 239 | customarily used for software interchange, for a price no more than your 240 | reasonable cost of physically performing this conveying of source, or (2) 241 | access to copy the Corresponding Source from a network server at no charge. 242 | - c) Convey individual copies of the object code with a copy of the written 243 | offer to provide the Corresponding Source. This alternative is allowed only 244 | occasionally and noncommercially, and only if you received the object code 245 | with such an offer, in accord with subsection 6b. 246 | - d) Convey the object code by offering access from a designated place (gratis 247 | or for a charge), and offer equivalent access to the Corresponding Source in 248 | the same way through the same place at no further charge. You need not require 249 | recipients to copy the Corresponding Source along with the object code. If the 250 | place to copy the object code is a network server, the Corresponding Source 251 | may be on a different server (operated by you or a third party) that supports 252 | equivalent copying facilities, provided you maintain clear directions next to 253 | the object code saying where to find the Corresponding Source. Regardless of 254 | what server hosts the Corresponding Source, you remain obligated to ensure 255 | that it is available for as long as needed to satisfy these requirements. 256 | - e) Convey the object code using peer-to-peer transmission, provided you inform 257 | other peers where the object code and Corresponding Source of the work are 258 | being offered to the general public at no charge under subsection 6d. 259 | 260 | A separable portion of the object code, whose source code is excluded from the 261 | Corresponding Source as a System Library, need not be included in conveying the 262 | object code work. 263 | 264 | A "User Product" is either (1) a "consumer product", which means any tangible 265 | personal property which is normally used for personal, family, or household 266 | purposes, or (2) anything designed or sold for incorporation into a dwelling. In 267 | determining whether a product is a consumer product, doubtful cases shall be 268 | resolved in favor of coverage. For a particular product received by a particular 269 | user, "normally used" refers to a typical or common use of that class of 270 | product, regardless of the status of the particular user or of the way in which 271 | the particular user actually uses, or expects or is expected to use, the 272 | product. A product is a consumer product regardless of whether the product has 273 | substantial commercial, industrial or non-consumer uses, unless such uses 274 | represent the only significant mode of use of the product. 275 | 276 | "Installation Information" for a User Product means any methods, procedures, 277 | authorization keys, or other information required to install and execute 278 | modified versions of a covered work in that User Product from a modified version 279 | of its Corresponding Source. The information must suffice to ensure that the 280 | continued functioning of the modified object code is in no case prevented or 281 | interfered with solely because modification has been made. 282 | 283 | If you convey an object code work under this section in, or with, or 284 | specifically for use in, a User Product, and the conveying occurs as part of a 285 | transaction in which the right of possession and use of the User Product is 286 | transferred to the recipient in perpetuity or for a fixed term (regardless of 287 | how the transaction is characterized), the Corresponding Source conveyed under 288 | this section must be accompanied by the Installation Information. But this 289 | requirement does not apply if neither you nor any third party retains the 290 | ability to install modified object code on the User Product (for example, the 291 | work has been installed in ROM). 292 | 293 | The requirement to provide Installation Information does not include a 294 | requirement to continue to provide support service, warranty, or updates for a 295 | work that has been modified or installed by the recipient, or for the User 296 | Product in which it has been modified or installed. Access to a network may be 297 | denied when the modification itself materially and adversely affects the 298 | operation of the network or violates the rules and protocols for communication 299 | across the network. 300 | 301 | Corresponding Source conveyed, and Installation Information provided, in accord 302 | with this section must be in a format that is publicly documented (and with an 303 | implementation available to the public in source code form), and must require no 304 | special password or key for unpacking, reading or copying. 305 | 306 | ### 7. Additional Terms. 307 | 308 | "Additional permissions" are terms that supplement the terms of this License by 309 | making exceptions from one or more of its conditions. Additional permissions 310 | that are applicable to the entire Program shall be treated as though they were 311 | included in this License, to the extent that they are valid under applicable 312 | law. If additional permissions apply only to part of the Program, that part may 313 | be used separately under those permissions, but the entire Program remains 314 | governed by this License without regard to the additional permissions. 315 | 316 | When you convey a copy of a covered work, you may at your option remove any 317 | additional permissions from that copy, or from any part of it. (Additional 318 | permissions may be written to require their own removal in certain cases when 319 | you modify the work.) You may place additional permissions on material, added by 320 | you to a covered work, for which you have or can give appropriate copyright 321 | permission. 322 | 323 | Notwithstanding any other provision of this License, for material you add to a 324 | covered work, you may (if authorized by the copyright holders of that material) 325 | supplement the terms of this License with terms: 326 | 327 | - a) Disclaiming warranty or limiting liability differently from the terms of 328 | sections 15 and 16 of this License; or 329 | - b) Requiring preservation of specified reasonable legal notices or author 330 | attributions in that material or in the Appropriate Legal Notices displayed by 331 | works containing it; or 332 | - c) Prohibiting misrepresentation of the origin of that material, or requiring 333 | that modified versions of such material be marked in reasonable ways as 334 | different from the original version; or 335 | - d) Limiting the use for publicity purposes of names of licensors or authors of 336 | the material; or 337 | - e) Declining to grant rights under trademark law for use of some trade names, 338 | trademarks, or service marks; or 339 | - f) Requiring indemnification of licensors and authors of that material by 340 | anyone who conveys the material (or modified versions of it) with contractual 341 | assumptions of liability to the recipient, for any liability that these 342 | contractual assumptions directly impose on those licensors and authors. 343 | 344 | All other non-permissive additional terms are considered "further restrictions" 345 | within the meaning of section 10. If the Program as you received it, or any part 346 | of it, contains a notice stating that it is governed by this License along with 347 | a term that is a further restriction, you may remove that term. If a license 348 | document contains a further restriction but permits relicensing or conveying 349 | under this License, you may add to a covered work material governed by the terms 350 | of that license document, provided that the further restriction does not survive 351 | such relicensing or conveying. 352 | 353 | If you add terms to a covered work in accord with this section, you must place, 354 | in the relevant source files, a statement of the additional terms that apply to 355 | those files, or a notice indicating where to find the applicable terms. 356 | 357 | Additional terms, permissive or non-permissive, may be stated in the form of a 358 | separately written license, or stated as exceptions; the above requirements 359 | apply either way. 360 | 361 | ### 8. Termination. 362 | 363 | You may not propagate or modify a covered work except as expressly provided 364 | under this License. Any attempt otherwise to propagate or modify it is void, and 365 | will automatically terminate your rights under this License (including any 366 | patent licenses granted under the third paragraph of section 11). 367 | 368 | However, if you cease all violation of this License, then your license from a 369 | particular copyright holder is reinstated (a) provisionally, unless and until 370 | the copyright holder explicitly and finally terminates your license, and (b) 371 | permanently, if the copyright holder fails to notify you of the violation by 372 | some reasonable means prior to 60 days after the cessation. 373 | 374 | Moreover, your license from a particular copyright holder is reinstated 375 | permanently if the copyright holder notifies you of the violation by some 376 | reasonable means, this is the first time you have received notice of violation 377 | of this License (for any work) from that copyright holder, and you cure the 378 | violation prior to 30 days after your receipt of the notice. 379 | 380 | Termination of your rights under this section does not terminate the licenses of 381 | parties who have received copies or rights from you under this License. If your 382 | rights have been terminated and not permanently reinstated, you do not qualify 383 | to receive new licenses for the same material under section 10. 384 | 385 | ### 9. Acceptance Not Required for Having Copies. 386 | 387 | You are not required to accept this License in order to receive or run a copy of 388 | the Program. Ancillary propagation of a covered work occurring solely as a 389 | consequence of using peer-to-peer transmission to receive a copy likewise does 390 | not require acceptance. However, nothing other than this License grants you 391 | permission to propagate or modify any covered work. These actions infringe 392 | copyright if you do not accept this License. Therefore, by modifying or 393 | propagating a covered work, you indicate your acceptance of this License to do 394 | so. 395 | 396 | ### 10. Automatic Licensing of Downstream Recipients. 397 | 398 | Each time you convey a covered work, the recipient automatically receives a 399 | license from the original licensors, to run, modify and propagate that work, 400 | subject to this License. You are not responsible for enforcing compliance by 401 | third parties with this License. 402 | 403 | An "entity transaction" is a transaction transferring control of an 404 | organization, or substantially all assets of one, or subdividing an 405 | organization, or merging organizations. If propagation of a covered work results 406 | from an entity transaction, each party to that transaction who receives a copy 407 | of the work also receives whatever licenses to the work the party's predecessor 408 | in interest had or could give under the previous paragraph, plus a right to 409 | possession of the Corresponding Source of the work from the predecessor in 410 | interest, if the predecessor has it or can get it with reasonable efforts. 411 | 412 | You may not impose any further restrictions on the exercise of the rights 413 | granted or affirmed under this License. For example, you may not impose a 414 | license fee, royalty, or other charge for exercise of rights granted under this 415 | License, and you may not initiate litigation (including a cross-claim or 416 | counterclaim in a lawsuit) alleging that any patent claim is infringed by 417 | making, using, selling, offering for sale, or importing the Program or any 418 | portion of it. 419 | 420 | ### 11. Patents. 421 | 422 | A "contributor" is a copyright holder who authorizes use under this License of 423 | the Program or a work on which the Program is based. The work thus licensed is 424 | called the contributor's "contributor version". 425 | 426 | A contributor's "essential patent claims" are all patent claims owned or 427 | controlled by the contributor, whether already acquired or hereafter acquired, 428 | that would be infringed by some manner, permitted by this License, of making, 429 | using, or selling its contributor version, but do not include claims that would 430 | be infringed only as a consequence of further modification of the contributor 431 | version. For purposes of this definition, "control" includes the right to grant 432 | patent sublicenses in a manner consistent with the requirements of this License. 433 | 434 | Each contributor grants you a non-exclusive, worldwide, royalty-free patent 435 | license under the contributor's essential patent claims, to make, use, sell, 436 | offer for sale, import and otherwise run, modify and propagate the contents of 437 | its contributor version. 438 | 439 | In the following three paragraphs, a "patent license" is any express agreement 440 | or commitment, however denominated, not to enforce a patent (such as an express 441 | permission to practice a patent or covenant not to sue for patent infringement). 442 | To "grant" such a patent license to a party means to make such an agreement or 443 | commitment not to enforce a patent against the party. 444 | 445 | If you convey a covered work, knowingly relying on a patent license, and the 446 | Corresponding Source of the work is not available for anyone to copy, free of 447 | charge and under the terms of this License, through a publicly available network 448 | server or other readily accessible means, then you must either (1) cause the 449 | Corresponding Source to be so available, or (2) arrange to deprive yourself of 450 | the benefit of the patent license for this particular work, or (3) arrange, in a 451 | manner consistent with the requirements of this License, to extend the patent 452 | license to downstream recipients. "Knowingly relying" means you have actual 453 | knowledge that, but for the patent license, your conveying the covered work in a 454 | country, or your recipient's use of the covered work in a country, would 455 | infringe one or more identifiable patents in that country that you have reason 456 | to believe are valid. 457 | 458 | If, pursuant to or in connection with a single transaction or arrangement, you 459 | convey, or propagate by procuring conveyance of, a covered work, and grant a 460 | patent license to some of the parties receiving the covered work authorizing 461 | them to use, propagate, modify or convey a specific copy of the covered work, 462 | then the patent license you grant is automatically extended to all recipients of 463 | the covered work and works based on it. 464 | 465 | A patent license is "discriminatory" if it does not include within the scope of 466 | its coverage, prohibits the exercise of, or is conditioned on the non-exercise 467 | of one or more of the rights that are specifically granted under this License. 468 | You may not convey a covered work if you are a party to an arrangement with a 469 | third party that is in the business of distributing software, under which you 470 | make payment to the third party based on the extent of your activity of 471 | conveying the work, and under which the third party grants, to any of the 472 | parties who would receive the covered work from you, a discriminatory patent 473 | license (a) in connection with copies of the covered work conveyed by you (or 474 | copies made from those copies), or (b) primarily for and in connection with 475 | specific products or compilations that contain the covered work, unless you 476 | entered into that arrangement, or that patent license was granted, prior to 28 477 | March 2007. 478 | 479 | Nothing in this License shall be construed as excluding or limiting any implied 480 | license or other defenses to infringement that may otherwise be available to you 481 | under applicable patent law. 482 | 483 | ### 12. No Surrender of Others' Freedom. 484 | 485 | If conditions are imposed on you (whether by court order, agreement or 486 | otherwise) that contradict the conditions of this License, they do not excuse 487 | you from the conditions of this License. If you cannot convey a covered work so 488 | as to satisfy simultaneously your obligations under this License and any other 489 | pertinent obligations, then as a consequence you may not convey it at all. For 490 | example, if you agree to terms that obligate you to collect a royalty for 491 | further conveying from those to whom you convey the Program, the only way you 492 | could satisfy both those terms and this License would be to refrain entirely 493 | from conveying the Program. 494 | 495 | ### 13. Use with the GNU Affero General Public License. 496 | 497 | Notwithstanding any other provision of this License, you have permission to link 498 | or combine any covered work with a work licensed under version 3 of the GNU 499 | Affero General Public License into a single combined work, and to convey the 500 | resulting work. The terms of this License will continue to apply to the part 501 | which is the covered work, but the special requirements of the GNU Affero 502 | General Public License, section 13, concerning interaction through a network 503 | will apply to the combination as such. 504 | 505 | ### 14. Revised Versions of this License. 506 | 507 | The Free Software Foundation may publish revised and/or new versions of the GNU 508 | General Public License from time to time. Such new versions will be similar in 509 | spirit to the present version, but may differ in detail to address new problems 510 | or concerns. 511 | 512 | Each version is given a distinguishing version number. If the Program specifies 513 | that a certain numbered version of the GNU General Public License "or any later 514 | version" applies to it, you have the option of following the terms and 515 | conditions either of that numbered version or of any later version published by 516 | the Free Software Foundation. If the Program does not specify a version number 517 | of the GNU General Public License, you may choose any version ever published by 518 | the Free Software Foundation. 519 | 520 | If the Program specifies that a proxy can decide which future versions of the 521 | GNU General Public License can be used, that proxy's public statement of 522 | acceptance of a version permanently authorizes you to choose that version for 523 | the Program. 524 | 525 | Later license versions may give you additional or different permissions. 526 | However, no additional obligations are imposed on any author or copyright holder 527 | as a result of your choosing to follow a later version. 528 | 529 | ### 15. Disclaimer of Warranty. 530 | 531 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 532 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER 533 | PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER 534 | EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 535 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE 536 | QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 537 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 538 | 539 | ### 16. Limitation of Liability. 540 | 541 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY 542 | COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS 543 | PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, 544 | INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE 545 | THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED 546 | INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE 547 | PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY 548 | HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 549 | 550 | ### 17. Interpretation of Sections 15 and 16. 551 | 552 | If the disclaimer of warranty and limitation of liability provided above cannot 553 | be given local legal effect according to their terms, reviewing courts shall 554 | apply local law that most closely approximates an absolute waiver of all civil 555 | liability in connection with the Program, unless a warranty or assumption of 556 | liability accompanies a copy of the Program in return for a fee. 557 | 558 | END OF TERMS AND CONDITIONS 559 | 560 | ## How to Apply These Terms to Your New Programs 561 | 562 | If you develop a new program, and you want it to be of the greatest possible use 563 | to the public, the best way to achieve this is to make it free software which 564 | everyone can redistribute and change under these terms. 565 | 566 | To do so, attach the following notices to the program. It is safest to attach 567 | them to the start of each source file to most effectively state the exclusion of 568 | warranty; and each file should have at least the "copyright" line and a pointer 569 | to where the full notice is found. 570 | 571 | 572 | Copyright (C) 573 | 574 | This program is free software: you can redistribute it and/or modify 575 | it under the terms of the GNU General Public License as published by 576 | the Free Software Foundation, either version 3 of the License, or 577 | (at your option) any later version. 578 | 579 | This program is distributed in the hope that it will be useful, 580 | but WITHOUT ANY WARRANTY; without even the implied warranty of 581 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 582 | GNU General Public License for more details. 583 | 584 | You should have received a copy of the GNU General Public License 585 | along with this program. If not, see . 586 | 587 | Also add information on how to contact you by electronic and paper mail. 588 | 589 | If the program does terminal interaction, make it output a short notice like 590 | this when it starts in an interactive mode: 591 | 592 | Copyright (C) 593 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 594 | This is free software, and you are welcome to redistribute it 595 | under certain conditions; type `show c' for details. 596 | 597 | The hypothetical commands \`show w' and \`show c' should show the appropriate 598 | parts of the General Public License. Of course, your program's commands might be 599 | different; for a GUI interface, you would use an "about box". 600 | 601 | You should also get your employer (if you work as a programmer) or school, if 602 | any, to sign a "copyright disclaimer" for the program, if necessary. For more 603 | information on this, and how to apply and follow the GNU GPL, see 604 | . 605 | 606 | The GNU General Public License does not permit incorporating your program into 607 | proprietary programs. If your program is a subroutine library, you may consider 608 | it more useful to permit linking proprietary applications with the library. If 609 | this is what you want to do, use the GNU Lesser General Public License instead 610 | of this License. But first, please read 611 | . 612 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Diff Nix 2 | 3 | A blazingly fast tool to diff Nix related things. 4 | 5 | Currently only supports closures (a derivation graph, such as a system build or 6 | package). 7 | 8 | ![output of `dix /nix/var/nix/profiles/system-69-link/ /run/current-system`](.github/dix.png) 9 | 10 | ## Usage 11 | 12 | ```bash 13 | $ dix --help 14 | 15 | Diff Nix 16 | 17 | Usage: dix [OPTIONS] 18 | 19 | Arguments: 20 | 21 | 22 | 23 | Options: 24 | -v, --verbose... Increase logging verbosity 25 | -q, --quiet... Decrease logging verbosity 26 | -h, --help Print help 27 | -V, --version Print version 28 | 29 | $ dix /nix/var/profiles/system-69-link /run/current-system 30 | ``` 31 | 32 | ## Contributing 33 | 34 | If you have any problems, feature requests or want to contribute code or want to 35 | provide input in some other way, feel free to create an issue or a pull request! 36 | 37 | ## Thanks 38 | 39 | Huge thanks to [nvd](https://git.sr.ht/~khumba/nvd) for the original idea! Dix 40 | is heavily inspired by this and basically just a "Rewrite it in Rust" version of 41 | nvd, with a few things like version diffing done better. 42 | 43 | Furthermore, many thanks to the amazing people who made this projects possible 44 | by contributing code and offering advice: 45 | 46 | - [@RGBCube](https://github.com/RGBCube) - Giving the codebase a deep scrub. 47 | - [@Dragyx](https://github.com/Dragyx) - Cool SQL queries. Much of dix's speed 48 | is thanks to him. 49 | - [@NotAShelf](https://github.com/NotAShelf) - Implementing proper error 50 | handling. 51 | 52 | ## License 53 | 54 | Dix is licensed under [GPLv3](LICENSE.md). See the license file for more 55 | details. 56 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "advisory-db": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1746689539, 7 | "narHash": "sha256-rVUs0CjpuO7FKVHecsuMaYiUr8iKscsgeo/b2XlnPmQ=", 8 | "owner": "rustsec", 9 | "repo": "advisory-db", 10 | "rev": "796d034fbcb1c5bc83c0d0912dc31eb4e34458bf", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "rustsec", 15 | "repo": "advisory-db", 16 | "type": "github" 17 | } 18 | }, 19 | "crane": { 20 | "locked": { 21 | "lastModified": 1746291859, 22 | "narHash": "sha256-DdWJLA+D5tcmrRSg5Y7tp/qWaD05ATI4Z7h22gd1h7Q=", 23 | "owner": "ipetkov", 24 | "repo": "crane", 25 | "rev": "dfd9a8dfd09db9aad544c4d3b6c47b12562544a5", 26 | "type": "github" 27 | }, 28 | "original": { 29 | "owner": "ipetkov", 30 | "repo": "crane", 31 | "type": "github" 32 | } 33 | }, 34 | "fenix": { 35 | "inputs": { 36 | "nixpkgs": [ 37 | "nixpkgs" 38 | ], 39 | "rust-analyzer-src": "rust-analyzer-src" 40 | }, 41 | "locked": { 42 | "lastModified": 1746858783, 43 | "narHash": "sha256-oLrH70QIWB/KpaI+nztyP1hG4zAEEpMiNk6sA8QLQ/8=", 44 | "owner": "nix-community", 45 | "repo": "fenix", 46 | "rev": "4e3cd098060cca21f2a213ce8c086948df946940", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "nix-community", 51 | "repo": "fenix", 52 | "type": "github" 53 | } 54 | }, 55 | "nixpkgs": { 56 | "locked": { 57 | "lastModified": 1746576598, 58 | "narHash": "sha256-FshoQvr6Aor5SnORVvh/ZdJ1Sa2U4ZrIMwKBX5k2wu0=", 59 | "owner": "NixOS", 60 | "repo": "nixpkgs", 61 | "rev": "b3582c75c7f21ce0b429898980eddbbf05c68e55", 62 | "type": "github" 63 | }, 64 | "original": { 65 | "owner": "NixOS", 66 | "ref": "nixpkgs-unstable", 67 | "repo": "nixpkgs", 68 | "type": "github" 69 | } 70 | }, 71 | "root": { 72 | "inputs": { 73 | "advisory-db": "advisory-db", 74 | "crane": "crane", 75 | "fenix": "fenix", 76 | "nixpkgs": "nixpkgs", 77 | "systems": "systems" 78 | } 79 | }, 80 | "rust-analyzer-src": { 81 | "flake": false, 82 | "locked": { 83 | "lastModified": 1746722075, 84 | "narHash": "sha256-t4ZntWiW4C3lE621lV3XyK3KltC5/SW1V9G+CSz70rQ=", 85 | "owner": "rust-lang", 86 | "repo": "rust-analyzer", 87 | "rev": "8b624868e4ce2cb5b39559175f0978bee86bdeea", 88 | "type": "github" 89 | }, 90 | "original": { 91 | "owner": "rust-lang", 92 | "ref": "nightly", 93 | "repo": "rust-analyzer", 94 | "type": "github" 95 | } 96 | }, 97 | "systems": { 98 | "locked": { 99 | "lastModified": 1681028828, 100 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 101 | "owner": "nix-systems", 102 | "repo": "default", 103 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 104 | "type": "github" 105 | }, 106 | "original": { 107 | "owner": "nix-systems", 108 | "repo": "default", 109 | "type": "github" 110 | } 111 | } 112 | }, 113 | "root": "root", 114 | "version": 7 115 | } 116 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Dix - Diff Nix"; 3 | 4 | nixConfig = { 5 | extra-substituters = [ 6 | "https://dix.cachix.org/" 7 | ]; 8 | 9 | extra-trusted-public-keys = [ 10 | "dix.cachix.org-1:8zQJZGvlOLYwlSCY/gVY14rqL8taVslOVbtT0jZFDGk=" 11 | ]; 12 | }; 13 | 14 | inputs = { 15 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 16 | systems.url = "github:nix-systems/default"; 17 | 18 | crane.url = "github:ipetkov/crane"; 19 | 20 | fenix = { 21 | url = "github:nix-community/fenix"; 22 | inputs.nixpkgs.follows = "nixpkgs"; 23 | }; 24 | 25 | advisory-db = { 26 | url = "github:rustsec/advisory-db"; 27 | flake = false; 28 | }; 29 | }; 30 | 31 | outputs = inputs @ { self, nixpkgs, systems, ... }: let 32 | inherit (nixpkgs) lib; 33 | 34 | eachSystem = lib.genAttrs (import systems); 35 | 36 | pkgsFor = eachSystem (system: import nixpkgs { 37 | inherit system; 38 | 39 | overlays = [ 40 | inputs.fenix.overlays.default 41 | 42 | (self: _: { 43 | crane = (inputs.crane.mkLib self).overrideToolchain (self.fenix.combine (lib.attrValues { 44 | inherit (self.fenix.stable) 45 | cargo 46 | clippy 47 | rust-analyzer 48 | rustc 49 | ; 50 | 51 | # Nightly rustfmt for the formatting options. 52 | inherit (self.fenix.default) 53 | rustfmt 54 | ; 55 | })); 56 | 57 | dix = { 58 | src = self.crane.cleanCargoSource ./.; 59 | 60 | cargoArguments = { 61 | inherit (self.dix) src; 62 | 63 | strictDeps = true; 64 | }; 65 | 66 | cargoArtifacts = self.crane.buildDepsOnly self.dix.cargoArguments; 67 | }; 68 | }) 69 | ]; 70 | }); 71 | in { 72 | packages = eachSystem (system: let pkgs = pkgsFor.${system}; in { 73 | default = self.packages.${system}.dix; 74 | 75 | dix = pkgs.crane.buildPackage (pkgs.dix.cargoArguments // { 76 | inherit (pkgs.dix) cargoArtifacts; 77 | 78 | pname = "dix"; 79 | cargoExtraArgs = "--package dix"; 80 | 81 | doCheck = false; 82 | }); 83 | }); 84 | 85 | devShells = eachSystem (system: let pkgs = pkgsFor.${system}; in { 86 | default = self.devShells.${system}.dix; 87 | 88 | dix = pkgs.crane.devShell { 89 | packages = lib.attrValues { 90 | inherit (pkgs) 91 | # A nice compiler daemon. 92 | bacon 93 | 94 | # Better tests. 95 | cargo-nextest 96 | 97 | # TOML formatting. 98 | taplo 99 | ; 100 | }; 101 | 102 | # For some reason rust-analyzer doesn't pick it up sometimes. 103 | env.CLIPPY_CONF_DIR = pkgs.writeTextDir "clippy.toml" (lib.readFile ./.clippy.toml); 104 | 105 | shellHook = '' 106 | # So we can do `dix` instead of `./target/debug/dix` 107 | root=$(git rev-parse --show-toplevel 2>/dev/null || pwd) 108 | export PATH="$PATH":"$root/target/debug" 109 | ''; 110 | }; 111 | }); 112 | 113 | checks = eachSystem (system: let pkgs = pkgsFor.${system}; in { 114 | inherit (self.packages.${system}) dix; 115 | 116 | dix-doctest = pkgs.crane.cargoDocTest (pkgs.dix.cargoArguments // { 117 | inherit (pkgs.dix) cargoArtifacts; 118 | }); 119 | 120 | dix-nextest = pkgs.crane.cargoNextest (pkgs.dix.cargoArguments // { 121 | inherit (pkgs.dix) cargoArtifacts; 122 | }); 123 | 124 | dix-clippy = pkgs.crane.cargoClippy (pkgs.dix.cargoArguments // { 125 | inherit (pkgs.dix) cargoArtifacts; 126 | 127 | env.CLIPPY_CONF_DIR = pkgs.writeTextDir "clippy.toml" (lib.readFile ./.clippy.toml); 128 | 129 | cargoClippyExtraArgs = "--all-targets -- --deny warnings"; 130 | }); 131 | 132 | dix-doc = pkgs.crane.cargoDoc (pkgs.dix.cargoArguments // { 133 | inherit (pkgs.dix) cargoArtifacts; 134 | }); 135 | 136 | dix-fmt = pkgs.crane.cargoFmt { 137 | inherit (pkgs.dix) src; 138 | 139 | rustFmtExtraArgs = "--config-path ${./.rustfmt.toml}"; 140 | }; 141 | 142 | dix-toml-fmt = pkgs.crane.taploFmt { 143 | src = lib.sources.sourceFilesBySuffices pkgs.dix.src [ ".toml" ]; 144 | 145 | taploExtraArgs = "--config ${./.taplo.toml}"; 146 | }; 147 | 148 | dix-audit = pkgs.crane.cargoAudit { 149 | inherit (inputs) advisory-db; 150 | inherit (pkgs.dix) src; 151 | }; 152 | 153 | dix-deny = pkgs.crane.cargoDeny { 154 | inherit (pkgs.dix) src; 155 | 156 | cargoDenyChecks = "bans licenses sources --config ${./.deny.toml}"; 157 | }; 158 | }); 159 | }; 160 | } 161 | -------------------------------------------------------------------------------- /src/diff.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cmp, 3 | collections::{ 4 | HashMap, 5 | HashSet, 6 | }, 7 | fmt::{ 8 | self, 9 | Write as _, 10 | }, 11 | path::{ 12 | Path, 13 | PathBuf, 14 | }, 15 | thread, 16 | }; 17 | 18 | use anyhow::{ 19 | Context as _, 20 | Error, 21 | Result, 22 | }; 23 | use itertools::{ 24 | EitherOrBoth, 25 | Itertools, 26 | }; 27 | use size::Size; 28 | use unicode_width::UnicodeWidthStr as _; 29 | use yansi::{ 30 | Paint as _, 31 | Painted, 32 | }; 33 | 34 | use crate::{ 35 | StorePath, 36 | Version, 37 | store, 38 | }; 39 | 40 | #[derive(Debug, Default)] 41 | struct Diff { 42 | old: T, 43 | new: T, 44 | } 45 | 46 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 47 | enum DiffStatus { 48 | Changed, 49 | Upgraded, 50 | Downgraded, 51 | Added, 52 | Removed, 53 | } 54 | 55 | impl DiffStatus { 56 | fn char(self) -> Painted<&'static char> { 57 | match self { 58 | Self::Changed => 'C'.yellow().bold(), 59 | Self::Upgraded => 'U'.bright_cyan().bold(), 60 | Self::Downgraded => 'D'.magenta().bold(), 61 | Self::Added => 'A'.green().bold(), 62 | Self::Removed => 'R'.red().bold(), 63 | } 64 | } 65 | } 66 | 67 | impl PartialOrd for DiffStatus { 68 | fn partial_cmp(&self, other: &Self) -> Option { 69 | Some(self.cmp(other)) 70 | } 71 | } 72 | 73 | impl cmp::Ord for DiffStatus { 74 | fn cmp(&self, other: &Self) -> cmp::Ordering { 75 | use DiffStatus::{ 76 | Added, 77 | Changed, 78 | Downgraded, 79 | Removed, 80 | Upgraded, 81 | }; 82 | 83 | #[expect(clippy::match_same_arms)] 84 | match (*self, *other) { 85 | // `Changed` gets displayed earlier than `Added` and `Removed`. 86 | (Changed | Upgraded | Downgraded, Removed | Added) => cmp::Ordering::Less, 87 | 88 | // `Added` gets displayed before `Removed`. 89 | (Added, Removed) => cmp::Ordering::Less, 90 | (Removed | Added, _) => cmp::Ordering::Greater, 91 | 92 | _ => cmp::Ordering::Equal, 93 | } 94 | } 95 | } 96 | 97 | /// Documents if the derivation is a system package and if 98 | /// it was added / removed as such. 99 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 100 | enum DerivationSelectionStatus { 101 | /// The derivation is a system package, status unchanged. 102 | Selected, 103 | /// The derivation was not a system package before but is now. 104 | NewlySelected, 105 | /// The derivation is and was a dependency. 106 | Unselected, 107 | /// The derivation was a system package before but is not anymore. 108 | NewlyUnselected, 109 | } 110 | 111 | impl DerivationSelectionStatus { 112 | fn from_names( 113 | name: &str, 114 | old: &HashSet, 115 | new: &HashSet, 116 | ) -> Self { 117 | match (old.contains(name), new.contains(name)) { 118 | (true, true) => Self::Selected, 119 | (true, false) => Self::NewlyUnselected, 120 | (false, true) => Self::NewlySelected, 121 | (false, false) => Self::Unselected, 122 | } 123 | } 124 | 125 | fn char(self) -> Painted<&'static char> { 126 | match self { 127 | Self::Selected => '*'.bold(), 128 | Self::NewlySelected => '+'.bold(), 129 | Self::Unselected => Painted::new(&'.'), 130 | Self::NewlyUnselected => Painted::new(&'-'), 131 | } 132 | } 133 | } 134 | 135 | /// Writes the diff header (<<< out, >>>in) and package diff. 136 | /// 137 | /// # Returns 138 | /// 139 | /// Will return the amount of package diffs written. Even when zero, 140 | /// the header will be written. 141 | #[expect(clippy::missing_errors_doc)] 142 | pub fn write_paths_diffln( 143 | writer: &mut impl fmt::Write, 144 | path_old: &Path, 145 | path_new: &Path, 146 | ) -> Result { 147 | let connection = store::connect()?; 148 | 149 | let paths_old = connection 150 | .query_dependents(path_old) 151 | .with_context(|| { 152 | format!( 153 | "failed to query dependencies of path '{path}'", 154 | path = path_old.display() 155 | ) 156 | })? 157 | .map(|(_, path)| path); 158 | 159 | log::info!( 160 | "found {count}+ packages in old closure", 161 | count = paths_old.size_hint().0, 162 | ); 163 | 164 | let paths_new = connection 165 | .query_dependents(path_new) 166 | .with_context(|| { 167 | format!( 168 | "failed to query dependencies of path '{path}'", 169 | path = path_new.display() 170 | ) 171 | })? 172 | .map(|(_, path)| path); 173 | 174 | let system_derivations_old = connection 175 | .query_system_derivations(path_old) 176 | .with_context(|| { 177 | format!( 178 | "failed to query system derivations of path '{path}", 179 | path = path_old.display() 180 | ) 181 | })? 182 | .map(|(_, path)| path); 183 | 184 | let system_derivations_new = connection 185 | .query_system_derivations(path_new) 186 | .with_context(|| { 187 | format!( 188 | "failed to query system derivations of path '{path}", 189 | path = path_old.display() 190 | ) 191 | })? 192 | .map(|(_, path)| path); 193 | 194 | log::info!( 195 | "found {count}+ packages in new closure", 196 | count = paths_new.size_hint().0, 197 | ); 198 | 199 | writeln!( 200 | writer, 201 | "{arrows} {old}", 202 | arrows = "<<<".bold(), 203 | old = path_old.display(), 204 | )?; 205 | writeln!( 206 | writer, 207 | "{arrows} {new}", 208 | arrows = ">>>".bold(), 209 | new = path_new.display(), 210 | )?; 211 | 212 | writeln!(writer)?; 213 | 214 | Ok(write_packages_diffln( 215 | writer, 216 | paths_old, 217 | paths_new, 218 | system_derivations_old, 219 | system_derivations_new, 220 | )?) 221 | } 222 | 223 | /// Takes a list of versions which may contain duplicates and deduplicates it by 224 | /// replacing multiple occurrences of an element with the same element plus the 225 | /// amount it occurs. 226 | /// 227 | /// # Example 228 | /// 229 | /// ```rs 230 | /// let mut versions = vec!["2.3", "1.0", "2.3", "4.8", "2.3", "1.0"]; 231 | /// 232 | /// deduplicate_versions(&mut versions); 233 | /// assert_eq!(*versions, &["1.0 ×2", "2.3 ×3", "4.8"]); 234 | /// ``` 235 | fn deduplicate_versions(versions: &mut Vec) { 236 | versions.sort_unstable(); 237 | 238 | let mut deduplicated = Vec::new(); 239 | 240 | // Push a version onto the final vec. If it occurs more than once, 241 | // we add a ×{count} to signify the amount of times it occurs. 242 | let mut deduplicated_push = |mut version: Version, count: usize| { 243 | if count > 1 { 244 | write!(version, " ×{count}").unwrap(); 245 | } 246 | deduplicated.push(version); 247 | }; 248 | 249 | let mut last_version = None::<(Version, usize)>; 250 | for version in versions.iter() { 251 | #[expect(clippy::mixed_read_write_in_expression)] 252 | let Some((last_version_value, count)) = last_version.take() else { 253 | last_version = Some((version.clone(), 1)); 254 | continue; 255 | }; 256 | 257 | // If the last version matches the current version, we increase the count by 258 | // one. Otherwise, we push the last version to the result. 259 | if last_version_value == *version { 260 | last_version = Some((last_version_value, count + 1)); 261 | } else { 262 | deduplicated_push(last_version_value, count); 263 | last_version = Some((version.clone(), 1)); 264 | } 265 | } 266 | 267 | // Push the final element, if it exists. 268 | if let Some((version, count)) = last_version.take() { 269 | deduplicated_push(version, count); 270 | } 271 | 272 | *versions = deduplicated; 273 | } 274 | 275 | #[expect(clippy::cognitive_complexity, clippy::too_many_lines)] 276 | fn write_packages_diffln( 277 | writer: &mut impl fmt::Write, 278 | paths_old: impl Iterator, 279 | paths_new: impl Iterator, 280 | system_paths_old: impl Iterator, 281 | system_paths_new: impl Iterator, 282 | ) -> Result { 283 | let mut paths = HashMap::>>::new(); 284 | 285 | // Collect the names of old and new paths. 286 | let system_derivations_old: HashSet = system_paths_old 287 | .filter_map(|path| { 288 | match path.parse_name_and_version() { 289 | Ok((name, _)) => Some(name.into()), 290 | Err(error) => { 291 | log::warn!("error parsing old system path name and version: {error}"); 292 | None 293 | }, 294 | } 295 | }) 296 | .collect(); 297 | 298 | let system_derivations_new: HashSet = system_paths_new 299 | .filter_map(|path| { 300 | match path.parse_name_and_version() { 301 | Ok((name, _)) => Some(name.into()), 302 | Err(error) => { 303 | log::warn!("error parsing new system path name and version: {error}"); 304 | None 305 | }, 306 | } 307 | }) 308 | .collect(); 309 | 310 | for path in paths_old { 311 | match path.parse_name_and_version() { 312 | Ok((name, version)) => { 313 | log::debug!("parsed name: {name}"); 314 | log::debug!("parsed version: {version:?}"); 315 | 316 | paths 317 | .entry(name.into()) 318 | .or_default() 319 | .old 320 | .push(version.unwrap_or_else(|| Version::from("".to_owned()))); 321 | }, 322 | 323 | Err(error) => { 324 | log::warn!("error parsing old path name and version: {error}"); 325 | }, 326 | } 327 | } 328 | 329 | for path in paths_new { 330 | match path.parse_name_and_version() { 331 | Ok((name, version)) => { 332 | log::debug!("parsed name: {name}"); 333 | log::debug!("parsed version: {version:?}"); 334 | 335 | paths 336 | .entry(name.into()) 337 | .or_default() 338 | .new 339 | .push(version.unwrap_or_else(|| Version::from("".to_owned()))); 340 | }, 341 | 342 | Err(error) => { 343 | log::warn!("error parsing new path name and version: {error}"); 344 | }, 345 | } 346 | } 347 | 348 | let mut diffs = paths 349 | .into_iter() 350 | .filter_map(|(name, mut versions)| { 351 | deduplicate_versions(&mut versions.old); 352 | deduplicate_versions(&mut versions.new); 353 | 354 | let status = match (versions.old.len(), versions.new.len()) { 355 | (0, 0) => unreachable!(), 356 | (0, _) => DiffStatus::Added, 357 | (_, 0) => DiffStatus::Removed, 358 | _ => { 359 | let mut saw_upgrade = false; 360 | let mut saw_downgrade = false; 361 | 362 | for diff in 363 | Itertools::zip_longest(versions.old.iter(), versions.new.iter()) 364 | { 365 | match diff { 366 | EitherOrBoth::Left(_) => saw_downgrade = true, 367 | EitherOrBoth::Right(_) => saw_upgrade = true, 368 | 369 | EitherOrBoth::Both(old, new) => { 370 | match old.cmp(new) { 371 | cmp::Ordering::Less => saw_upgrade = true, 372 | cmp::Ordering::Greater => saw_downgrade = true, 373 | cmp::Ordering::Equal => {}, 374 | } 375 | 376 | if saw_upgrade && saw_downgrade { 377 | break; 378 | } 379 | }, 380 | } 381 | } 382 | 383 | match (saw_upgrade, saw_downgrade) { 384 | (true, true) => DiffStatus::Changed, 385 | (true, false) => DiffStatus::Upgraded, 386 | (false, true) => DiffStatus::Downgraded, 387 | _ => return None, 388 | } 389 | }, 390 | }; 391 | 392 | let selection = DerivationSelectionStatus::from_names( 393 | &name, 394 | &system_derivations_old, 395 | &system_derivations_new, 396 | ); 397 | 398 | Some((name, versions, status, selection)) 399 | }) 400 | .collect::>(); 401 | 402 | diffs.sort_by( 403 | |&(ref a_name, _, a_status, _), &(ref b_name, _, b_status, _)| { 404 | a_status.cmp(&b_status).then_with(|| a_name.cmp(b_name)) 405 | }, 406 | ); 407 | 408 | #[expect(clippy::pattern_type_mismatch)] 409 | let name_width = diffs 410 | .iter() 411 | .map(|(name, ..)| name.width()) 412 | .max() 413 | .unwrap_or(0); 414 | 415 | let mut last_status = None::; 416 | 417 | for &(ref name, ref versions, status, selection) in &diffs { 418 | use DiffStatus::{ 419 | Added, 420 | Changed, 421 | Downgraded, 422 | Removed, 423 | Upgraded, 424 | }; 425 | 426 | let merged_status = if let Downgraded | Upgraded = status { 427 | Changed 428 | } else { 429 | status 430 | }; 431 | 432 | if last_status != Some(merged_status) { 433 | writeln!( 434 | writer, 435 | "{nl}{status}", 436 | nl = if last_status.is_some() { "\n" } else { "" }, 437 | status = match merged_status { 438 | Changed => "CHANGED", 439 | Upgraded | Downgraded => unreachable!(), 440 | Added => "ADDED", 441 | Removed => "REMOVED", 442 | } 443 | .bold(), 444 | )?; 445 | 446 | last_status = Some(merged_status); 447 | } 448 | 449 | let status = status.char(); 450 | let selection = selection.char(); 451 | let name = name.paint(selection.style); 452 | 453 | write!(writer, "[{status}{selection}] {name: { 464 | if oldwrote { 465 | write!(oldacc, ", ")?; 466 | } else { 467 | write!(oldacc, " ")?; 468 | oldwrote = true; 469 | } 470 | 471 | for old_comp in old_version { 472 | match old_comp { 473 | Ok(old_comp) => write!(oldacc, "{old}", old = old_comp.red())?, 474 | Err(ignored) => write!(oldacc, "{ignored}")?, 475 | } 476 | } 477 | }, 478 | 479 | EitherOrBoth::Right(new_version) => { 480 | if newwrote { 481 | write!(newacc, ", ")?; 482 | } else { 483 | write!(newacc, " ")?; 484 | newwrote = true; 485 | } 486 | 487 | for new_comp in new_version { 488 | match new_comp { 489 | Ok(new_comp) => write!(newacc, "{new}", new = new_comp.green())?, 490 | Err(ignored) => write!(newacc, "{ignored}")?, 491 | } 492 | } 493 | }, 494 | 495 | EitherOrBoth::Both(old_version, new_version) => { 496 | if old_version == new_version { 497 | continue; 498 | } 499 | 500 | if oldwrote { 501 | write!(oldacc, ", ")?; 502 | } else { 503 | write!(oldacc, " ")?; 504 | oldwrote = true; 505 | } 506 | if newwrote { 507 | write!(newacc, ", ")?; 508 | } else { 509 | write!(newacc, " ")?; 510 | newwrote = true; 511 | } 512 | 513 | for diff in Itertools::zip_longest( 514 | old_version.into_iter(), 515 | new_version.into_iter(), 516 | ) { 517 | match diff { 518 | EitherOrBoth::Left(old_comp) => { 519 | match old_comp { 520 | Ok(old_comp) => { 521 | write!(oldacc, "{old}", old = old_comp.red())?; 522 | }, 523 | Err(ignored) => { 524 | write!(oldacc, "{ignored}")?; 525 | }, 526 | } 527 | }, 528 | 529 | EitherOrBoth::Right(new_comp) => { 530 | match new_comp { 531 | Ok(new_comp) => { 532 | write!(newacc, "{new}", new = new_comp.green())?; 533 | }, 534 | Err(ignored) => { 535 | write!(newacc, "{ignored}")?; 536 | }, 537 | } 538 | }, 539 | 540 | EitherOrBoth::Both(old_comp, new_comp) => { 541 | match (old_comp, new_comp) { 542 | (Ok(old_comp), Ok(new_comp)) => { 543 | for char in diff::chars(*old_comp, *new_comp) { 544 | match char { 545 | diff::Result::Left(old_part) => { 546 | write!(oldacc, "{old}", old = old_part.red())?; 547 | }, 548 | diff::Result::Right(new_part) => { 549 | write!(newacc, "{new}", new = new_part.green())?; 550 | }, 551 | 552 | diff::Result::Both(old_part, new_part) => { 553 | write!(oldacc, "{old}", old = old_part.yellow())?; 554 | write!(newacc, "{new}", new = new_part.yellow())?; 555 | }, 556 | } 557 | } 558 | }, 559 | 560 | (old_comp, new_comp) => { 561 | match old_comp { 562 | Ok(old_comp) => { 563 | write!(oldacc, "{old}", old = old_comp.yellow())?; 564 | }, 565 | Err(old_comp) => write!(oldacc, "{old_comp}")?, 566 | } 567 | 568 | match new_comp { 569 | Ok(new_comp) => { 570 | write!(newacc, "{new}", new = new_comp.yellow())?; 571 | }, 572 | Err(new_comp) => write!(newacc, "{new_comp}")?, 573 | } 574 | }, 575 | } 576 | }, 577 | } 578 | } 579 | }, 580 | } 581 | } 582 | 583 | write!( 584 | writer, 585 | "{oldacc}{arrow}{newacc}", 586 | arrow = if !oldacc.is_empty() && !newacc.is_empty() { 587 | " ->" 588 | } else { 589 | "" 590 | } 591 | )?; 592 | 593 | writeln!(writer)?; 594 | } 595 | 596 | Ok(diffs.len()) 597 | } 598 | 599 | /// Spawns a task to compute the data required by [`write_size_diffln`]. 600 | #[must_use] 601 | pub fn spawn_size_diff( 602 | path_old: PathBuf, 603 | path_new: PathBuf, 604 | ) -> thread::JoinHandle> { 605 | log::debug!("calculating closure sizes in background"); 606 | 607 | thread::spawn(move || { 608 | let connection = store::connect()?; 609 | 610 | Ok::<_, Error>(( 611 | connection.query_closure_size(&path_old)?, 612 | connection.query_closure_size(&path_new)?, 613 | )) 614 | }) 615 | } 616 | 617 | /// Writes the size difference between two numbers to `writer`. 618 | /// 619 | /// # Returns 620 | /// 621 | /// Will return nothing when successful. 622 | /// 623 | /// # Errors 624 | /// 625 | /// Returns `Err` when writing to `writer` fails. 626 | pub fn write_size_diffln( 627 | writer: &mut impl fmt::Write, 628 | size_old: Size, 629 | size_new: Size, 630 | ) -> fmt::Result { 631 | let size_diff = size_new - size_old; 632 | 633 | writeln!( 634 | writer, 635 | "{header}: {size_old} -> {size_new}", 636 | header = "SIZE".bold(), 637 | size_old = size_old.red(), 638 | size_new = size_new.green(), 639 | )?; 640 | 641 | writeln!( 642 | writer, 643 | "{header}: {size_diff}", 644 | header = "DIFF".bold(), 645 | size_diff = if size_diff.bytes() > 0 { 646 | size_diff.green() 647 | } else { 648 | size_diff.red() 649 | }, 650 | ) 651 | } 652 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | path::PathBuf, 3 | sync, 4 | }; 5 | 6 | use anyhow::{ 7 | Context as _, 8 | Error, 9 | Result, 10 | anyhow, 11 | bail, 12 | }; 13 | use derive_more::Deref; 14 | 15 | mod diff; 16 | pub use diff::{ 17 | spawn_size_diff, 18 | write_paths_diffln, 19 | write_size_diffln, 20 | }; 21 | 22 | mod store; 23 | 24 | mod version; 25 | use version::Version; 26 | 27 | #[derive(Deref, Debug, Clone, Copy, PartialEq, Eq, Hash)] 28 | struct DerivationId(i64); 29 | 30 | /// A validated store path. Always starts with `/nix/store`. 31 | /// 32 | /// Can be created using `StorePath::try_from(path_buf)`. 33 | #[derive(Deref, Debug, Clone, PartialEq, Eq, Hash)] 34 | pub struct StorePath(PathBuf); 35 | 36 | impl TryFrom for StorePath { 37 | type Error = Error; 38 | 39 | fn try_from(path: PathBuf) -> Result { 40 | if !path.starts_with("/nix/store") { 41 | bail!( 42 | "path {path} must start with /nix/store", 43 | path = path.display(), 44 | ); 45 | } 46 | 47 | Ok(Self(path)) 48 | } 49 | } 50 | 51 | impl StorePath { 52 | /// Parses a Nix store path to extract the packages name and possibly its 53 | /// version. 54 | /// 55 | /// This function first drops the inputs first 44 chars, since that is exactly 56 | /// the length of the `/nix/store/0004yybkm5hnwjyxv129js3mjp7kbrax-` prefix. 57 | /// Then it matches that against our store path regex. 58 | fn parse_name_and_version(&self) -> Result<(&str, Option)> { 59 | static STORE_PATH_REGEX: sync::LazyLock = 60 | sync::LazyLock::new(|| { 61 | regex::Regex::new("(.+?)(-([0-9].*?))?$") 62 | .expect("failed to compile regex for Nix store paths") 63 | }); 64 | 65 | let path = self.to_str().with_context(|| { 66 | format!( 67 | "failed to convert path '{path}' to valid unicode", 68 | path = self.display(), 69 | ) 70 | })?; 71 | 72 | // We can strip the path since it _always_ follows the format: 73 | // 74 | // /nix/store/0004yybkm5hnwjyxv129js3mjp7kbrax-... 75 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 76 | // This part is exactly 44 chars long, so we just remove it. 77 | assert_eq!(&path[..11], "/nix/store/"); 78 | assert_eq!(&path[43..44], "-"); 79 | let path = &path[44..]; 80 | 81 | log::debug!("stripped path: {path}"); 82 | 83 | let captures = STORE_PATH_REGEX.captures(path).ok_or_else(|| { 84 | anyhow!("path '{path}' does not match expected Nix store format") 85 | })?; 86 | 87 | let name = captures.get(1).map_or("", |capture| capture.as_str()); 88 | if name.is_empty() { 89 | bail!("failed to extract name from path '{path}'"); 90 | } 91 | 92 | let version: Option = captures.get(2).map(|capture| { 93 | Version::from(capture.as_str().trim_start_matches('-').to_owned()) 94 | }); 95 | 96 | Ok((name, version)) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{ 3 | self, 4 | Write as _, 5 | }, 6 | io::{ 7 | self, 8 | Write as _, 9 | }, 10 | path::PathBuf, 11 | process, 12 | }; 13 | 14 | use anyhow::{ 15 | Result, 16 | anyhow, 17 | }; 18 | use clap::Parser as _; 19 | use yansi::Paint as _; 20 | 21 | struct WriteFmt(W); 22 | 23 | impl fmt::Write for WriteFmt { 24 | fn write_str(&mut self, string: &str) -> fmt::Result { 25 | self.0.write_all(string.as_bytes()).map_err(|_| fmt::Error) 26 | } 27 | } 28 | 29 | #[derive(clap::Parser, Debug)] 30 | #[command(version, about)] 31 | struct Cli { 32 | old_path: PathBuf, 33 | new_path: PathBuf, 34 | 35 | #[command(flatten)] 36 | verbose: clap_verbosity_flag::Verbosity, 37 | } 38 | 39 | fn real_main() -> Result<()> { 40 | let Cli { 41 | old_path, 42 | new_path, 43 | verbose, 44 | } = Cli::parse(); 45 | 46 | yansi::whenever(yansi::Condition::TTY_AND_COLOR); 47 | 48 | env_logger::Builder::new() 49 | .filter_level(verbose.log_level_filter()) 50 | .format(|out, arguments| { 51 | let header = match arguments.level() { 52 | log::Level::Error => "error:".red(), 53 | log::Level::Warn => "warn:".yellow(), 54 | log::Level::Info => "info:".green(), 55 | log::Level::Debug => "debug:".blue(), 56 | log::Level::Trace => "trace:".cyan(), 57 | }; 58 | 59 | writeln!(out, "{header} {message}", message = arguments.args()) 60 | }) 61 | .init(); 62 | 63 | let mut out = WriteFmt(io::stdout()); 64 | 65 | // Handle to the thread collecting closure size information. 66 | let closure_size_handle = 67 | dix::spawn_size_diff(old_path.clone(), new_path.clone()); 68 | 69 | let wrote = dix::write_paths_diffln(&mut out, &old_path, &new_path)?; 70 | 71 | let (size_old, size_new) = closure_size_handle 72 | .join() 73 | .map_err(|_| anyhow!("failed to get closure size due to thread error"))??; 74 | 75 | if wrote > 0 { 76 | writeln!(out)?; 77 | } 78 | 79 | dix::write_size_diffln(&mut out, size_old, size_new)?; 80 | 81 | Ok(()) 82 | } 83 | 84 | #[allow(clippy::allow_attributes, clippy::exit)] 85 | fn main() { 86 | let Err(error) = real_main() else { 87 | return; 88 | }; 89 | 90 | let mut err = io::stderr(); 91 | 92 | let mut message = String::new(); 93 | let mut chain = error.chain().rev().peekable(); 94 | 95 | while let Some(error) = chain.next() { 96 | let _ = write!( 97 | err, 98 | "{header} ", 99 | header = if chain.peek().is_none() { 100 | "error:" 101 | } else { 102 | "cause:" 103 | } 104 | .red() 105 | .bold(), 106 | ); 107 | 108 | String::clear(&mut message); 109 | let _ = write!(message, "{error}"); 110 | 111 | let mut chars = message.char_indices(); 112 | 113 | let _ = match (chars.next(), chars.next()) { 114 | (Some((_, first)), Some((second_start, second))) 115 | if second.is_lowercase() => 116 | { 117 | writeln!( 118 | err, 119 | "{first_lowercase}{rest}", 120 | first_lowercase = first.to_lowercase(), 121 | rest = &message[second_start..], 122 | ) 123 | }, 124 | 125 | _ => { 126 | writeln!(err, "{message}") 127 | }, 128 | }; 129 | } 130 | 131 | process::exit(1); 132 | } 133 | -------------------------------------------------------------------------------- /src/store.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::mem_forget)] 2 | 3 | use std::{ 4 | iter::{ 5 | FilterMap, 6 | Iterator, 7 | Peekable, 8 | }, 9 | path::Path, 10 | }; 11 | 12 | use anyhow::{ 13 | Context as _, 14 | Result, 15 | anyhow, 16 | }; 17 | use derive_more::Deref; 18 | use ouroboros::self_referencing; 19 | use rusqlite::{ 20 | CachedStatement, 21 | MappedRows, 22 | OpenFlags, 23 | Row, 24 | }; 25 | use size::Size; 26 | 27 | use crate::{ 28 | DerivationId, 29 | StorePath, 30 | }; 31 | 32 | #[derive(Deref)] 33 | /// A Nix database connection. 34 | pub struct Connection(rusqlite::Connection); 35 | 36 | type FilterOkFunc = fn(Result) -> Option; 37 | 38 | #[self_referencing] 39 | /// Contains the SQL statement and the query resulting from it. 40 | /// 41 | /// This is necessary since the statement is only created during 42 | /// the query method on the Connection. The query however contains 43 | /// a reference to it, so we can't simply return the Query 44 | struct QueryIteratorCell<'conn, T, F> 45 | where 46 | T: 'static, 47 | F: Fn(&rusqlite::Row) -> rusqlite::Result, 48 | { 49 | /// statement prepared by the sql connection 50 | stmt: CachedStatement<'conn>, 51 | /// The actual iterator we generate from the query iterator 52 | /// 53 | /// note that the concrete datatype is rather complicated, 54 | /// since we wan't to avoid a box, since we currently only have a single 55 | /// way to deal wihh queries that return multiple rows 56 | #[borrows(mut stmt)] 57 | #[not_covariant] 58 | inner: FilterMap>, FilterOkFunc>, 59 | } 60 | 61 | /// The iterator over the data resulting from a SQL query, 62 | /// where the rows are mapped to `T`. 63 | /// 64 | /// We ignore all rows where the conversion fails, 65 | /// but take a look at the first row to make sure 66 | /// the conversion is not trivially wrong. 67 | /// 68 | /// The idea is to only use very trivial 69 | /// conversions that will never fail 70 | /// if the query actually returns the correct number 71 | /// of rows. 72 | pub struct QueryIterator<'conn, T, F> 73 | where 74 | T: 'static, 75 | F: Fn(&rusqlite::Row) -> rusqlite::Result, 76 | { 77 | cell: QueryIteratorCell<'conn, T, F>, 78 | } 79 | 80 | impl<'conn, T, F> QueryIterator<'conn, T, F> 81 | where 82 | F: Fn(&rusqlite::Row) -> rusqlite::Result, 83 | { 84 | /// May fail if the query itself fails or 85 | /// if the first row of the query result can not 86 | /// be mapped to `T`. 87 | pub fn try_new( 88 | stmt: CachedStatement<'conn>, 89 | params: P, 90 | map: F, 91 | ) -> Result { 92 | let cell_res = QueryIteratorCell::try_new(stmt, |stmt| { 93 | let inner_iter = stmt 94 | .query_map(params, map) 95 | .map(Iterator::peekable) 96 | .with_context(|| "Unable to perform query"); 97 | 98 | match inner_iter { 99 | Ok(mut iter) => { 100 | #[expect(clippy::pattern_type_mismatch)] 101 | if let Some(Err(err)) = iter.peek() { 102 | return Err(anyhow!("First row conversion failed: {err:?}")); 103 | } 104 | let iter_filtered = iter.filter_map( 105 | (|row| { 106 | if let Err(ref err) = row { 107 | log::warn!("Row conversion failed: {err:?}"); 108 | } 109 | row.ok() 110 | }) as FilterOkFunc, 111 | ); 112 | 113 | Ok(iter_filtered) 114 | }, 115 | Err(err) => Err(err), 116 | } 117 | }); 118 | cell_res.map(|cell| Self { cell }) 119 | } 120 | } 121 | 122 | impl Iterator for QueryIterator<'_, T, F> 123 | where 124 | F: Fn(&rusqlite::Row) -> rusqlite::Result, 125 | { 126 | type Item = T; 127 | fn next(&mut self) -> Option { 128 | self.cell.with_inner_mut(|inner| inner.next()) 129 | } 130 | } 131 | 132 | /// Connects to the Nix database 133 | /// 134 | /// and sets some basic settings 135 | pub fn connect() -> Result { 136 | const DATABASE_PATH: &str = "/nix/var/nix/db/db.sqlite"; 137 | 138 | let inner = rusqlite::Connection::open_with_flags( 139 | DATABASE_PATH, 140 | OpenFlags::SQLITE_OPEN_READ_ONLY // We only run queries, safeguard against corrupting the DB. 141 | | OpenFlags::SQLITE_OPEN_NO_MUTEX // Part of the default flags, rusqlite takes care of locking anyways. 142 | | OpenFlags::SQLITE_OPEN_URI, 143 | ) 144 | .with_context(|| { 145 | format!("failed to connect to Nix database at {DATABASE_PATH}") 146 | })?; 147 | 148 | // Perform a batched query to set some settings using PRAGMA 149 | // the main performance bottleneck when dix was run before 150 | // was that the database file has to be brought from disk into 151 | // memory. 152 | // 153 | // We read a large part of the DB anyways in each query, 154 | // so it makes sense to set aside a large region of memory-mapped 155 | // I/O prevent incurring page faults which can be done using 156 | // `mmap_size`. 157 | // 158 | // This made a performance difference of about 500ms (but only 159 | // when it was first run for a long time!). 160 | // 161 | // The file pages of the store can be evicted from main memory 162 | // using: 163 | // 164 | // ```bash 165 | // dd of=/nix/var/nix/db/db.sqlite oflag=nocache conv=notrunc,fdatasync count=0 166 | // ``` 167 | // 168 | // If you want to test this. Source: . 169 | // 170 | // Documentation about the settings can be found here: 171 | // 172 | // [0]: 256MB, enough to fit the whole DB (at least on my system - Dragyx). 173 | // [1]: Always store temporary tables in memory. 174 | inner 175 | .execute_batch( 176 | " 177 | PRAGMA mmap_size=268435456; -- See [0]. 178 | PRAGMA temp_store=2; -- See [1]. 179 | PRAGMA query_only; 180 | ", 181 | ) 182 | .with_context(|| { 183 | format!("failed to cache Nix database at {DATABASE_PATH}") 184 | })?; 185 | 186 | Ok(Connection(inner)) 187 | } 188 | 189 | fn path_to_canonical_string(path: &Path) -> Result { 190 | let path = path.canonicalize().with_context(|| { 191 | format!( 192 | "failed to canonicalize path '{path}'", 193 | path = path.display(), 194 | ) 195 | })?; 196 | 197 | let path = path.into_os_string().into_string().map_err(|path| { 198 | anyhow!( 199 | "failed to convert path '{path}' to valid unicode", 200 | path = Path::new(&*path).display(), /* TODO: use .display() directly 201 | * after Rust 1.87.0 in flake. */ 202 | ) 203 | })?; 204 | 205 | Ok(path) 206 | } 207 | 208 | impl Connection { 209 | /// Executes a query that returns multiple rows and returns 210 | /// an iterator over them where the `map` is used to map 211 | /// the rows to `T`. 212 | pub fn execute_row_query_with_path( 213 | &self, 214 | query: &str, 215 | path: &Path, 216 | map: M, 217 | ) -> Result> 218 | where 219 | T: 'static, 220 | M: Fn(&Row) -> rusqlite::Result, 221 | { 222 | let path = path_to_canonical_string(path)?; 223 | let stmt = self.prepare_cached(query)?; 224 | QueryIterator::try_new(stmt, [path], map) 225 | } 226 | 227 | /// Gets the total closure size of the given store path by summing up the nar 228 | /// size of all dependent derivations. 229 | pub fn query_closure_size(&self, path: &Path) -> Result { 230 | const QUERY: &str = " 231 | WITH RECURSIVE 232 | graph(p) AS ( 233 | SELECT id 234 | FROM ValidPaths 235 | WHERE path = ? 236 | UNION 237 | SELECT reference FROM Refs 238 | JOIN graph ON referrer = p 239 | ) 240 | SELECT SUM(narSize) as sum from graph 241 | JOIN ValidPaths ON p = id; 242 | "; 243 | 244 | let path = path_to_canonical_string(path)?; 245 | 246 | let closure_size = self 247 | .prepare_cached(QUERY)? 248 | .query_row([path], |row| Ok(Size::from_bytes(row.get::<_, i64>(0)?)))?; 249 | 250 | Ok(closure_size) 251 | } 252 | 253 | /// Gets the derivations that are directly included in the system derivation. 254 | /// 255 | /// Will not work on non-system derivations. 256 | pub fn query_system_derivations( 257 | &self, 258 | system: &Path, 259 | ) -> Result> { 260 | const QUERY: &str = " 261 | WITH 262 | systemderiv AS ( 263 | SELECT id FROM ValidPaths 264 | WHERE path = ? 265 | ), 266 | systempath AS ( 267 | SELECT reference as id FROM systemderiv sd 268 | JOIN Refs ON sd.id = referrer 269 | JOIN ValidPaths vp ON reference = vp.id 270 | WHERE (vp.path LIKE '%-system-path') 271 | ), 272 | pkgs AS ( 273 | SELECT reference as id FROM Refs 274 | JOIN systempath ON referrer = id 275 | ) 276 | SELECT pkgs.id, path FROM pkgs 277 | JOIN ValidPaths vp ON vp.id = pkgs.id; 278 | "; 279 | 280 | self.execute_row_query_with_path(QUERY, system, |row| { 281 | Ok(( 282 | DerivationId(row.get(0)?), 283 | StorePath(row.get::<_, String>(1)?.into()), 284 | )) 285 | }) 286 | } 287 | 288 | /// Gathers all derivations that the given profile path depends on. 289 | pub fn query_dependents( 290 | &self, 291 | path: &Path, 292 | ) -> Result> { 293 | const QUERY: &str = " 294 | WITH RECURSIVE 295 | graph(p) AS ( 296 | SELECT id 297 | FROM ValidPaths 298 | WHERE path = ? 299 | UNION 300 | SELECT reference FROM Refs 301 | JOIN graph ON referrer = p 302 | ) 303 | SELECT id, path from graph 304 | JOIN ValidPaths ON id = p; 305 | "; 306 | 307 | self.execute_row_query_with_path(QUERY, path, |row| { 308 | Ok(( 309 | DerivationId(row.get(0)?), 310 | StorePath(row.get::<_, String>(1)?.into()), 311 | )) 312 | }) 313 | } 314 | 315 | /// Returns all edges of the dependency graph. 316 | /// 317 | /// You might want to build an adjacency list from the resulting 318 | /// edges. 319 | #[expect(dead_code)] 320 | pub fn query_dependency_graph( 321 | &self, 322 | path: &StorePath, 323 | ) -> Result> { 324 | const QUERY: &str = " 325 | WITH RECURSIVE 326 | graph(p, c) AS ( 327 | SELECT id as par, reference as chd 328 | FROM ValidPaths 329 | JOIN Refs ON referrer = id 330 | WHERE path = ? 331 | UNION 332 | SELECT referrer as par, reference as chd FROM Refs 333 | JOIN graph ON referrer = c 334 | ) 335 | SELECT p, c from graph; 336 | "; 337 | 338 | self.execute_row_query_with_path(QUERY, path, |row| { 339 | Ok((DerivationId(row.get(0)?), DerivationId(row.get(1)?))) 340 | }) 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/version.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | 3 | use derive_more::{ 4 | Deref, 5 | DerefMut, 6 | Display, 7 | From, 8 | }; 9 | 10 | #[derive(Deref, DerefMut, Display, Debug, Clone, PartialEq, Eq, From)] 11 | pub struct Version(String); 12 | 13 | impl PartialOrd for Version { 14 | fn partial_cmp(&self, other: &Self) -> Option { 15 | Some(self.cmp(other)) 16 | } 17 | } 18 | 19 | impl cmp::Ord for Version { 20 | fn cmp(&self, that: &Self) -> cmp::Ordering { 21 | let this = VersionComponentIter::from(&***self).filter_map(Result::ok); 22 | let that = VersionComponentIter::from(&***that).filter_map(Result::ok); 23 | 24 | this.cmp(that) 25 | } 26 | } 27 | 28 | impl<'a> IntoIterator for &'a Version { 29 | type Item = Result, &'a str>; 30 | 31 | type IntoIter = VersionComponentIter<'a>; 32 | 33 | fn into_iter(self) -> Self::IntoIter { 34 | VersionComponentIter::from(&***self) 35 | } 36 | } 37 | 38 | #[derive(Deref, Display, Debug, Clone, Copy)] 39 | pub struct VersionComponent<'a>(&'a str); 40 | 41 | impl PartialEq for VersionComponent<'_> { 42 | fn eq(&self, other: &Self) -> bool { 43 | self.cmp(other) == cmp::Ordering::Equal 44 | } 45 | } 46 | 47 | impl Eq for VersionComponent<'_> {} 48 | 49 | impl PartialOrd for VersionComponent<'_> { 50 | fn partial_cmp(&self, other: &Self) -> Option { 51 | Some(self.cmp(other)) 52 | } 53 | } 54 | 55 | impl cmp::Ord for VersionComponent<'_> { 56 | fn cmp(&self, other: &Self) -> cmp::Ordering { 57 | let self_digit = self.0.bytes().all(|char| char.is_ascii_digit()); 58 | let other_digit = other.0.bytes().all(|char| char.is_ascii_digit()); 59 | 60 | match (self_digit, other_digit) { 61 | (true, true) => { 62 | let self_nonzero = self.0.trim_start_matches('0'); 63 | let other_nonzero = other.0.trim_start_matches('0'); 64 | 65 | self_nonzero 66 | .len() 67 | .cmp(&other_nonzero.len()) 68 | .then_with(|| self_nonzero.cmp(other_nonzero)) 69 | }, 70 | 71 | (false, false) => { 72 | match (self.0, other.0) { 73 | ("pre", _) => cmp::Ordering::Less, 74 | (_, "pre") => cmp::Ordering::Greater, 75 | _ => self.0.cmp(other.0), 76 | } 77 | }, 78 | 79 | (true, false) => cmp::Ordering::Greater, 80 | (false, true) => cmp::Ordering::Less, 81 | } 82 | } 83 | } 84 | 85 | /// Yields [`VertionComponent`] from a version string. 86 | #[derive(Deref, DerefMut, From)] 87 | pub struct VersionComponentIter<'a>(&'a str); 88 | 89 | impl<'a> Iterator for VersionComponentIter<'a> { 90 | type Item = Result, &'a str>; 91 | 92 | fn next(&mut self) -> Option { 93 | if self.starts_with(['.', '-', '*', '×', ' ']) { 94 | let len = self.chars().next().unwrap().len_utf8(); 95 | let (this, rest) = self.split_at(len); 96 | 97 | **self = rest; 98 | return Some(Err(this)); 99 | } 100 | 101 | // Get the next character and decide if it is a digit. 102 | let is_digit = self.chars().next()?.is_ascii_digit(); 103 | 104 | // Based on this collect characters after this into the component. 105 | let component_len = self 106 | .chars() 107 | .take_while(|&char| { 108 | char.is_ascii_digit() == is_digit 109 | && !matches!(char, '.' | '-' | '*' | ' ' | '×') 110 | }) 111 | .map(char::len_utf8) 112 | .sum(); 113 | 114 | let component = &self[..component_len]; 115 | **self = &self[component_len..]; 116 | 117 | assert!(!component.is_empty()); 118 | 119 | Some(Ok(VersionComponent(component))) 120 | } 121 | } 122 | 123 | #[cfg(test)] 124 | mod tests { 125 | use proptest::proptest; 126 | 127 | use super::{ 128 | VersionComponent, 129 | VersionComponentIter, 130 | }; 131 | 132 | #[test] 133 | fn version_component_iter() { 134 | let version = "132.1.2test234-1-man----.--.......---------..---"; 135 | 136 | assert_eq!( 137 | VersionComponentIter::from(version) 138 | .filter_map(Result::ok) 139 | .collect::>(), 140 | [ 141 | VersionComponent("132"), 142 | VersionComponent("1"), 143 | VersionComponent("2"), 144 | VersionComponent("test"), 145 | VersionComponent("234"), 146 | VersionComponent("1"), 147 | VersionComponent("man") 148 | ] 149 | ); 150 | } 151 | 152 | proptest! { 153 | #[test] 154 | fn version_cmp_number(this: u128, that: u128) { 155 | let real_ord = this.cmp(&that); 156 | 157 | let component_ord = VersionComponent(&this.to_string()) 158 | .cmp(&VersionComponent(&that.to_string())); 159 | 160 | assert_eq!(real_ord, component_ord); 161 | } 162 | } 163 | } 164 | --------------------------------------------------------------------------------