├── .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 | 
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 |
--------------------------------------------------------------------------------