Initial vid-repair scaffold
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/.local/
|
||||
/target/
|
||||
**/*.log
|
||||
967
Cargo.lock
generated
Normal file
967
Cargo.lock
generated
Normal file
@@ -0,0 +1,967 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "directories"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs-err"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.178"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "6.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"crossbeam-channel",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio",
|
||||
"walkdir",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.148"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.112"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_write",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "vid-repair"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"rayon",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"toml",
|
||||
"vid-repair-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vid-repair-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"directories",
|
||||
"fs-err",
|
||||
"globset",
|
||||
"notify",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.1+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.53.1",
|
||||
"windows_x86_64_gnu 0.53.1",
|
||||
"windows_x86_64_gnullvm 0.53.1",
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aac060176f7020d62c3bcc1cdbcec619d54f48b07ad1963a3f80ce7a0c17755f"
|
||||
26
Cargo.toml
Normal file
26
Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"vid-repair",
|
||||
"vid-repair-core",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
directories = "5"
|
||||
fs-err = "2"
|
||||
globset = "0.4"
|
||||
notify = "6"
|
||||
rayon = "1"
|
||||
regex = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tempfile = "3"
|
||||
thiserror = "1"
|
||||
toml = "0.8"
|
||||
walkdir = "2"
|
||||
170
rulesets/core.toml
Normal file
170
rulesets/core.toml
Normal file
@@ -0,0 +1,170 @@
|
||||
[[rule]]
|
||||
id = "MOOV_ATOM_NOT_FOUND"
|
||||
domain = "container.mp4"
|
||||
severity = "severe"
|
||||
confidence = 0.9
|
||||
fix_tier = "reencode"
|
||||
stop_scan = true
|
||||
patterns = ["(?i)moov atom not found"]
|
||||
notes = "MP4/MOV metadata is missing; file may be incomplete."
|
||||
|
||||
[[rule]]
|
||||
id = "COULD_NOT_FIND_CODEC_PARAMS"
|
||||
domain = "probe"
|
||||
severity = "medium"
|
||||
confidence = 0.6
|
||||
fix_tier = "none"
|
||||
stop_scan = false
|
||||
patterns = ["(?i)could not find codec parameters"]
|
||||
notes = "Insufficient probe data; consider higher analyzeduration/probesize."
|
||||
|
||||
[[rule]]
|
||||
id = "INVALID_DATA_FOUND"
|
||||
domain = "decode"
|
||||
severity = "high"
|
||||
confidence = 0.7
|
||||
fix_tier = "reencode"
|
||||
stop_scan = false
|
||||
patterns = ["(?i)Invalid data found when processing input"]
|
||||
notes = "Decoder encountered invalid data; may indicate corruption."
|
||||
|
||||
[[rule]]
|
||||
id = "FILE_ENDED_PREMATURELY"
|
||||
domain = "decode"
|
||||
severity = "high"
|
||||
confidence = 0.7
|
||||
fix_tier = "reencode"
|
||||
stop_scan = false
|
||||
patterns = ["(?i)File ended prematurely"]
|
||||
notes = "File appears truncated."
|
||||
|
||||
[[rule]]
|
||||
id = "INVALID_NAL_UNIT_SIZE"
|
||||
domain = "codec.h264"
|
||||
severity = "severe"
|
||||
confidence = 0.85
|
||||
fix_tier = "reencode"
|
||||
stop_scan = true
|
||||
patterns = ["(?i)Invalid NAL unit size", "(?i)Error splitting the input into NAL units"]
|
||||
notes = "H.264/HEVC bitstream corruption detected."
|
||||
|
||||
[[rule]]
|
||||
id = "MISSING_PICTURE_ACCESS_UNIT"
|
||||
domain = "codec.h264"
|
||||
severity = "high"
|
||||
confidence = 0.7
|
||||
fix_tier = "reencode"
|
||||
stop_scan = false
|
||||
patterns = ["(?i)missing picture in access unit"]
|
||||
notes = "Missing picture in access unit; possible corruption."
|
||||
|
||||
[[rule]]
|
||||
id = "PPS_ID_OUT_OF_RANGE"
|
||||
domain = "codec.hevc"
|
||||
severity = "high"
|
||||
confidence = 0.75
|
||||
fix_tier = "reencode"
|
||||
stop_scan = false
|
||||
patterns = ["(?i)PPS id out of range", "(?i)Error parsing NAL unit"]
|
||||
notes = "HEVC parameter set corruption detected."
|
||||
|
||||
[[rule]]
|
||||
id = "SEI_TRUNCATED"
|
||||
domain = "codec.h264"
|
||||
severity = "low"
|
||||
confidence = 0.5
|
||||
fix_tier = "none"
|
||||
stop_scan = false
|
||||
patterns = ["(?i)SEI type .* truncated"]
|
||||
notes = "SEI message truncated; often benign unless paired with decode errors."
|
||||
|
||||
[[rule]]
|
||||
id = "NON_MONOTONOUS_DTS"
|
||||
domain = "timestamp"
|
||||
severity = "medium"
|
||||
confidence = 0.6
|
||||
fix_tier = "remux"
|
||||
stop_scan = false
|
||||
patterns = ["(?i)Non-monotonous DTS", "(?i)non monotonically increasing dts"]
|
||||
notes = "Timestamp discontinuity detected."
|
||||
|
||||
[[rule]]
|
||||
id = "DTS_DISCONTINUITY"
|
||||
domain = "timestamp"
|
||||
severity = "medium"
|
||||
confidence = 0.6
|
||||
fix_tier = "remux"
|
||||
stop_scan = false
|
||||
patterns = ["(?i)DTS discontinuity"]
|
||||
notes = "Timestamp discontinuity detected."
|
||||
|
||||
[[rule]]
|
||||
id = "PES_PACKET_SIZE_MISMATCH"
|
||||
domain = "transport.ts"
|
||||
severity = "medium"
|
||||
confidence = 0.6
|
||||
fix_tier = "remux"
|
||||
stop_scan = false
|
||||
patterns = ["(?i)PES packet size mismatch", "(?i)Packet corrupt"]
|
||||
notes = "Transport stream corruption detected."
|
||||
|
||||
[[rule]]
|
||||
id = "CONTINUITY_COUNTER_ERROR"
|
||||
domain = "transport.ts"
|
||||
severity = "low"
|
||||
confidence = 0.4
|
||||
fix_tier = "none"
|
||||
stop_scan = false
|
||||
patterns = ["(?i)continuity counter error"]
|
||||
notes = "Continuity counter errors can be benign in segmented streams."
|
||||
|
||||
[[rule]]
|
||||
id = "AAC_ADTS_HEADER_ERROR"
|
||||
domain = "codec.aac"
|
||||
severity = "medium"
|
||||
confidence = 0.7
|
||||
fix_tier = "reencode"
|
||||
stop_scan = false
|
||||
patterns = ["(?i)Error parsing ADTS frame header", "(?i)Error decoding AAC frame header"]
|
||||
notes = "AAC bitstream errors detected."
|
||||
|
||||
[[rule]]
|
||||
id = "MP3_HEADER_MISSING"
|
||||
domain = "codec.mp3"
|
||||
severity = "medium"
|
||||
confidence = 0.6
|
||||
fix_tier = "reencode"
|
||||
stop_scan = false
|
||||
patterns = ["(?i)Header missing"]
|
||||
notes = "MP3 framing errors detected."
|
||||
|
||||
[[rule]]
|
||||
id = "AC3_FRAME_SYNC_ERROR"
|
||||
domain = "codec.ac3"
|
||||
severity = "medium"
|
||||
confidence = 0.6
|
||||
fix_tier = "reencode"
|
||||
stop_scan = false
|
||||
patterns = ["(?i)frame sync error"]
|
||||
notes = "AC-3 frame sync error detected."
|
||||
|
||||
[[rule]]
|
||||
id = "EBML_HEADER_PARSING_FAILED"
|
||||
domain = "container.mkv"
|
||||
severity = "high"
|
||||
confidence = 0.7
|
||||
fix_tier = "reencode"
|
||||
stop_scan = false
|
||||
patterns = ["(?i)EBML header parsing failed"]
|
||||
notes = "Matroska/WebM header parsing failed; file may be truncated."
|
||||
|
||||
[[rule]]
|
||||
id = "FASTSTART_RECOMMENDED"
|
||||
domain = "container.mp4"
|
||||
severity = "low"
|
||||
confidence = 0.4
|
||||
fix_tier = "remux"
|
||||
stop_scan = false
|
||||
action = "faststart"
|
||||
patterns = ["(?i)faststart"]
|
||||
notes = "MP4 likely has moov atom at end; faststart remux recommended."
|
||||
1
vid-repair-core/.gitignore
vendored
Normal file
1
vid-repair-core/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
19
vid-repair-core/Cargo.toml
Normal file
19
vid-repair-core/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "vid-repair-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
directories = { workspace = true }
|
||||
fs-err = { workspace = true }
|
||||
globset = { workspace = true }
|
||||
notify = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
47
vid-repair-core/src/config/defaults.rs
Normal file
47
vid-repair-core/src/config/defaults.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
pub const DEFAULT_CONFIG: &str = r#"# Vid-Repair configuration
|
||||
# Lines beginning with '#' are comments.
|
||||
# Values here are defaults; CLI flags override config.
|
||||
|
||||
ffmpeg_path = "ffmpeg"
|
||||
ffprobe_path = "ffprobe"
|
||||
|
||||
[scan]
|
||||
# quick|standard|deep - deep runs full decode (slowest, most thorough)
|
||||
depth = "deep"
|
||||
# If quick/standard find suspicious signals, auto-escalate to deep.
|
||||
auto_escalate = true
|
||||
# Follow symlinks while scanning directories.
|
||||
follow_symlinks = false
|
||||
# Recurse into subdirectories when scanning directories.
|
||||
recursive = true
|
||||
# Extensions to include (lowercase, no dot). Add/remove as needed.
|
||||
include_ext = ["mp4","m4v","mov","mkv","avi","wmv","flv","webm","mpg","mpeg","m2ts","mts","ts","3gp","3g2","ogv","vob","f4v"]
|
||||
# Glob patterns to exclude.
|
||||
exclude = ["**/.git/**", "**/node_modules/**"]
|
||||
|
||||
[repair]
|
||||
# safe|aggressive - aggressive allows re-encode when corruption is found.
|
||||
policy = "safe"
|
||||
# Empty means in-place replacement with temp + atomic rename.
|
||||
output_dir = ""
|
||||
# If true (and in-place), rename original to *.original.* after successful fix.
|
||||
keep_original = false
|
||||
|
||||
[report]
|
||||
# Emit JSON instead of human-readable text.
|
||||
json = false
|
||||
# Pretty-print JSON output.
|
||||
pretty = true
|
||||
|
||||
[performance]
|
||||
# 0 = auto (num CPU threads). Higher values spawn more concurrent scans.
|
||||
jobs = 0
|
||||
|
||||
[watch]
|
||||
# Enable watch mode (monitor and process files as they settle).
|
||||
enabled = false
|
||||
# Wait for no changes for this many seconds before processing.
|
||||
settle_seconds = 10
|
||||
# Ignore these extensions during watch (lowercase, no dot).
|
||||
ignore_ext = ["part","crdownload","partial","tmp","download"]
|
||||
"#;
|
||||
299
vid-repair-core/src/config/mod.rs
Normal file
299
vid-repair-core/src/config/mod.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use directories::ProjectDirs;
|
||||
use fs_err as fs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod defaults;
|
||||
|
||||
pub use defaults::DEFAULT_CONFIG;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub ffmpeg_path: String,
|
||||
#[serde(default)]
|
||||
pub ffprobe_path: String,
|
||||
#[serde(default)]
|
||||
pub scan: ScanConfig,
|
||||
#[serde(default)]
|
||||
pub repair: RepairConfig,
|
||||
#[serde(default)]
|
||||
pub report: ReportConfig,
|
||||
#[serde(default)]
|
||||
pub performance: PerformanceConfig,
|
||||
#[serde(default)]
|
||||
pub watch: WatchConfig,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ffmpeg_path: "ffmpeg".to_string(),
|
||||
ffprobe_path: "ffprobe".to_string(),
|
||||
scan: ScanConfig::default(),
|
||||
repair: RepairConfig::default(),
|
||||
report: ReportConfig::default(),
|
||||
performance: PerformanceConfig::default(),
|
||||
watch: WatchConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ScanConfig {
|
||||
#[serde(default)]
|
||||
pub depth: ScanDepth,
|
||||
#[serde(default)]
|
||||
pub auto_escalate: bool,
|
||||
#[serde(default)]
|
||||
pub follow_symlinks: bool,
|
||||
#[serde(default)]
|
||||
pub recursive: bool,
|
||||
#[serde(default)]
|
||||
pub include_ext: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub exclude: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for ScanConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
depth: ScanDepth::Deep,
|
||||
auto_escalate: true,
|
||||
follow_symlinks: false,
|
||||
recursive: true,
|
||||
include_ext: vec![
|
||||
"mp4", "m4v", "mov", "mkv", "avi", "wmv", "flv", "webm", "mpg",
|
||||
"mpeg", "m2ts", "mts", "ts", "3gp", "3g2", "ogv", "vob", "f4v",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect(),
|
||||
exclude: vec!["**/.git/**".to_string(), "**/node_modules/**".to_string()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ScanDepth {
|
||||
Quick,
|
||||
Standard,
|
||||
Deep,
|
||||
}
|
||||
|
||||
impl Default for ScanDepth {
|
||||
fn default() -> Self {
|
||||
ScanDepth::Deep
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RepairConfig {
|
||||
#[serde(default)]
|
||||
pub policy: FixPolicy,
|
||||
#[serde(default)]
|
||||
pub output_dir: String,
|
||||
#[serde(default)]
|
||||
pub keep_original: bool,
|
||||
}
|
||||
|
||||
impl Default for RepairConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
policy: FixPolicy::Safe,
|
||||
output_dir: String::new(),
|
||||
keep_original: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FixPolicy {
|
||||
Safe,
|
||||
Aggressive,
|
||||
}
|
||||
|
||||
impl Default for FixPolicy {
|
||||
fn default() -> Self {
|
||||
FixPolicy::Safe
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReportConfig {
|
||||
#[serde(default)]
|
||||
pub json: bool,
|
||||
#[serde(default)]
|
||||
pub pretty: bool,
|
||||
}
|
||||
|
||||
impl Default for ReportConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
json: false,
|
||||
pretty: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PerformanceConfig {
|
||||
#[serde(default)]
|
||||
pub jobs: usize,
|
||||
}
|
||||
|
||||
impl Default for PerformanceConfig {
|
||||
fn default() -> Self {
|
||||
Self { jobs: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WatchConfig {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub settle_seconds: u64,
|
||||
#[serde(default)]
|
||||
pub ignore_ext: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for WatchConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
settle_seconds: 10,
|
||||
ignore_ext: vec!["part", "crdownload", "partial", "tmp", "download"]
|
||||
.into_iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ConfigOverrides {
|
||||
pub ffmpeg_path: Option<String>,
|
||||
pub ffprobe_path: Option<String>,
|
||||
pub scan_depth: Option<ScanDepth>,
|
||||
pub scan_recursive: Option<bool>,
|
||||
pub policy: Option<FixPolicy>,
|
||||
pub output_dir: Option<String>,
|
||||
pub keep_original: Option<bool>,
|
||||
pub json: Option<bool>,
|
||||
pub jobs: Option<usize>,
|
||||
pub watch: Option<bool>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn apply_overrides(&mut self, overrides: &ConfigOverrides) {
|
||||
if let Some(value) = &overrides.ffmpeg_path {
|
||||
self.ffmpeg_path = value.clone();
|
||||
}
|
||||
if let Some(value) = &overrides.ffprobe_path {
|
||||
self.ffprobe_path = value.clone();
|
||||
}
|
||||
if let Some(value) = overrides.scan_depth {
|
||||
self.scan.depth = value;
|
||||
}
|
||||
if let Some(value) = overrides.scan_recursive {
|
||||
self.scan.recursive = value;
|
||||
}
|
||||
if let Some(value) = overrides.policy {
|
||||
self.repair.policy = value;
|
||||
}
|
||||
if let Some(value) = &overrides.output_dir {
|
||||
self.repair.output_dir = value.clone();
|
||||
}
|
||||
if let Some(value) = overrides.keep_original {
|
||||
self.repair.keep_original = value;
|
||||
}
|
||||
if let Some(value) = overrides.json {
|
||||
self.report.json = value;
|
||||
}
|
||||
if let Some(value) = overrides.jobs {
|
||||
self.performance.jobs = value;
|
||||
}
|
||||
if let Some(value) = overrides.watch {
|
||||
self.watch.enabled = value;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_or_init(path: Option<PathBuf>) -> Result<(Self, PathBuf)> {
|
||||
let path = match path {
|
||||
Some(path) => path,
|
||||
None => default_config_path()?,
|
||||
};
|
||||
|
||||
if !path.exists() {
|
||||
init_config_at(&path, false)?;
|
||||
}
|
||||
|
||||
let raw = fs::read_to_string(&path)
|
||||
.with_context(|| format!("Failed to read config file at {}", path.display()))?;
|
||||
let mut config: Config = toml::from_str(&raw)
|
||||
.with_context(|| format!("Failed to parse config file at {}", path.display()))?;
|
||||
|
||||
config.normalize();
|
||||
|
||||
Ok((config, path))
|
||||
}
|
||||
|
||||
pub fn normalize(&mut self) {
|
||||
self.scan.include_ext = self
|
||||
.scan
|
||||
.include_ext
|
||||
.iter()
|
||||
.map(|ext| ext.trim_start_matches('.').to_lowercase())
|
||||
.collect();
|
||||
|
||||
self.watch.ignore_ext = self
|
||||
.watch
|
||||
.ignore_ext
|
||||
.iter()
|
||||
.map(|ext| ext.trim_start_matches('.').to_lowercase())
|
||||
.collect();
|
||||
|
||||
if self.watch.settle_seconds == 0 {
|
||||
self.watch.settle_seconds = 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_config_path() -> Result<PathBuf> {
|
||||
let proj_dirs = ProjectDirs::from("cc", "44r0n", "vid-repair")
|
||||
.context("Unable to resolve default config directory")?;
|
||||
Ok(proj_dirs.config_dir().join("config.toml"))
|
||||
}
|
||||
|
||||
pub fn init_config_at(path: &Path, force: bool) -> Result<()> {
|
||||
if path.exists() && !force {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create config directory {}", parent.display()))?;
|
||||
}
|
||||
|
||||
fs::write(path, DEFAULT_CONFIG)
|
||||
.with_context(|| format!("Failed to write default config to {}", path.display()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_config_parses() {
|
||||
let config: Config = toml::from_str(DEFAULT_CONFIG).expect("default config parse");
|
||||
assert_eq!(config.scan.depth, ScanDepth::Deep);
|
||||
assert!(!config.scan.include_ext.is_empty());
|
||||
}
|
||||
}
|
||||
202
vid-repair-core/src/fix/executor.rs
Normal file
202
vid-repair-core/src/fix/executor.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use fs_err as fs;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::fix::{FixKind, FixOutcome, FixPlan};
|
||||
use crate::scan::scan_file;
|
||||
use crate::rules::RuleSet;
|
||||
|
||||
pub fn apply_fix(path: &Path, plan: &FixPlan, config: &Config, ruleset: &RuleSet) -> Result<FixOutcome> {
|
||||
if plan.actions.is_empty() {
|
||||
return Ok(FixOutcome {
|
||||
plan: plan.clone(),
|
||||
applied: false,
|
||||
success: false,
|
||||
message: plan
|
||||
.blocked_reason
|
||||
.clone()
|
||||
.unwrap_or_else(|| "No fix actions available".to_string()),
|
||||
output_path: None,
|
||||
re_scan_required: false,
|
||||
});
|
||||
}
|
||||
|
||||
let action = &plan.actions[0];
|
||||
let output = prepare_output_path(path, config)?;
|
||||
|
||||
run_ffmpeg_fix(path, &output.temp_path, action.kind, config)?;
|
||||
|
||||
let verification = scan_file(&output.temp_path, config, ruleset)
|
||||
.with_context(|| format!("Failed to verify output {}", output.temp_path.display()))?;
|
||||
|
||||
if has_severe_issues(&verification) {
|
||||
return Ok(FixOutcome {
|
||||
plan: plan.clone(),
|
||||
applied: true,
|
||||
success: false,
|
||||
message: "Verification failed; repaired file still has severe issues".to_string(),
|
||||
output_path: Some(output.temp_path.display().to_string()),
|
||||
re_scan_required: true,
|
||||
});
|
||||
}
|
||||
|
||||
finalize_output(path, &output, config)?;
|
||||
|
||||
Ok(FixOutcome {
|
||||
plan: plan.clone(),
|
||||
applied: true,
|
||||
success: true,
|
||||
message: "Fix applied successfully".to_string(),
|
||||
output_path: Some(output.final_path.display().to_string()),
|
||||
re_scan_required: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn run_ffmpeg_fix(path: &Path, output: &Path, kind: FixKind, config: &Config) -> Result<()> {
|
||||
let mut cmd = Command::new(&config.ffmpeg_path);
|
||||
cmd.arg("-y").arg("-v").arg("error").arg("-i").arg(path);
|
||||
|
||||
match kind {
|
||||
FixKind::Remux => {
|
||||
cmd.arg("-c").arg("copy");
|
||||
}
|
||||
FixKind::Faststart => {
|
||||
cmd.arg("-c").arg("copy");
|
||||
cmd.arg("-movflags").arg("+faststart");
|
||||
}
|
||||
FixKind::Reencode => {
|
||||
cmd.arg("-c:v").arg("libx264");
|
||||
cmd.arg("-c:a").arg("aac");
|
||||
cmd.arg("-movflags").arg("+faststart");
|
||||
}
|
||||
}
|
||||
|
||||
cmd.arg(output);
|
||||
|
||||
let output = cmd
|
||||
.output()
|
||||
.with_context(|| format!("Failed to run ffmpeg fix for {}", path.display()))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!("ffmpeg fix failed: {}", stderr.trim());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn has_severe_issues(report: &crate::scan::ScanOutcome) -> bool {
|
||||
report
|
||||
.issues
|
||||
.iter()
|
||||
.any(|issue| matches!(issue.severity, crate::rules::Severity::High | crate::rules::Severity::Severe))
|
||||
}
|
||||
|
||||
struct OutputPaths {
|
||||
temp_path: PathBuf,
|
||||
final_path: PathBuf,
|
||||
}
|
||||
|
||||
fn prepare_output_path(path: &Path, config: &Config) -> Result<OutputPaths> {
|
||||
if config.repair.output_dir.is_empty() {
|
||||
let parent = path
|
||||
.parent()
|
||||
.context("Input file has no parent directory")?;
|
||||
let temp = NamedTempFile::new_in(parent)
|
||||
.with_context(|| format!("Failed to create temp file in {}", parent.display()))?;
|
||||
let temp_path = temp.path().to_path_buf();
|
||||
temp.keep()?;
|
||||
Ok(OutputPaths {
|
||||
temp_path,
|
||||
final_path: path.to_path_buf(),
|
||||
})
|
||||
} else {
|
||||
let output_dir = PathBuf::from(&config.repair.output_dir);
|
||||
fs::create_dir_all(&output_dir).with_context(|| {
|
||||
format!("Failed to create output directory {}", output_dir.display())
|
||||
})?;
|
||||
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.context("Input file has no filename")?
|
||||
.to_os_string();
|
||||
|
||||
let final_path = output_dir.join(file_name);
|
||||
let temp = NamedTempFile::new_in(&output_dir).with_context(|| {
|
||||
format!("Failed to create temp file in {}", output_dir.display())
|
||||
})?;
|
||||
let temp_path = temp.path().to_path_buf();
|
||||
temp.keep()?;
|
||||
|
||||
Ok(OutputPaths {
|
||||
temp_path,
|
||||
final_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_output(input: &Path, output: &OutputPaths, config: &Config) -> Result<()> {
|
||||
if output.final_path == output.temp_path {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if output.final_path == input {
|
||||
if config.repair.keep_original {
|
||||
let backup = next_original_path(input)?;
|
||||
fs::rename(input, &backup).with_context(|| {
|
||||
format!("Failed to rename original {}", input.display())
|
||||
})?;
|
||||
}
|
||||
|
||||
fs::rename(&output.temp_path, input).with_context(|| {
|
||||
format!("Failed to move repaired file into place for {}", input.display())
|
||||
})?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fs::rename(&output.temp_path, &output.final_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to move repaired file to {}",
|
||||
output.final_path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next_original_path(path: &Path) -> Result<PathBuf> {
|
||||
let parent = path
|
||||
.parent()
|
||||
.context("Input file has no parent directory")?;
|
||||
let stem = path
|
||||
.file_stem()
|
||||
.context("Input file has no stem")?
|
||||
.to_string_lossy();
|
||||
let ext = path.extension().map(|ext| ext.to_string_lossy());
|
||||
|
||||
for idx in 0..1000 {
|
||||
let candidate = if idx == 0 {
|
||||
if let Some(ext) = &ext {
|
||||
format!("{}.original.{}", stem, ext)
|
||||
} else {
|
||||
format!("{}.original", stem)
|
||||
}
|
||||
} else if let Some(ext) = &ext {
|
||||
format!("{}.original.{}.{}", stem, idx, ext)
|
||||
} else {
|
||||
format!("{}.original.{}", stem, idx)
|
||||
};
|
||||
|
||||
let path = parent.join(candidate);
|
||||
if !path.exists() {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!("Unable to find available .original name for {}", path.display());
|
||||
}
|
||||
48
vid-repair-core/src/fix/mod.rs
Normal file
48
vid-repair-core/src/fix/mod.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::FixPolicy;
|
||||
use crate::rules::FixTier;
|
||||
|
||||
pub mod executor;
|
||||
pub mod planner;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FixAction {
|
||||
pub kind: FixKind,
|
||||
pub command: Vec<String>,
|
||||
pub destructive: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FixKind {
|
||||
Remux,
|
||||
Faststart,
|
||||
Reencode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FixPlan {
|
||||
pub policy: FixPolicy,
|
||||
pub recommended: Option<FixKind>,
|
||||
pub actions: Vec<FixAction>,
|
||||
pub blocked_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FixOutcome {
|
||||
pub plan: FixPlan,
|
||||
pub applied: bool,
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub output_path: Option<String>,
|
||||
pub re_scan_required: bool,
|
||||
}
|
||||
|
||||
pub fn highest_fix_tier(tiers: &[FixTier]) -> FixTier {
|
||||
tiers
|
||||
.iter()
|
||||
.copied()
|
||||
.max_by_key(|tier| tier.rank())
|
||||
.unwrap_or(FixTier::None)
|
||||
}
|
||||
67
vid-repair-core/src/fix/planner.rs
Normal file
67
vid-repair-core/src/fix/planner.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use crate::config::FixPolicy;
|
||||
use crate::fix::{FixAction, FixKind, FixOutcome, FixPlan};
|
||||
use crate::rules::FixTier;
|
||||
use crate::scan::Issue;
|
||||
|
||||
pub fn plan_fix(issues: &[Issue], policy: FixPolicy) -> FixPlan {
|
||||
let mut recommended = None;
|
||||
|
||||
let mut has_faststart = false;
|
||||
let mut has_remux = false;
|
||||
let mut has_reencode = false;
|
||||
|
||||
for issue in issues {
|
||||
if let Some(action) = &issue.action {
|
||||
if action.eq_ignore_ascii_case("faststart") {
|
||||
has_faststart = true;
|
||||
}
|
||||
}
|
||||
|
||||
match issue.fix_tier {
|
||||
FixTier::Remux => has_remux = true,
|
||||
FixTier::Reencode => has_reencode = true,
|
||||
FixTier::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
if has_faststart {
|
||||
recommended = Some(FixKind::Faststart);
|
||||
} else if has_remux {
|
||||
recommended = Some(FixKind::Remux);
|
||||
} else if has_reencode {
|
||||
recommended = Some(FixKind::Reencode);
|
||||
}
|
||||
|
||||
let mut actions = Vec::new();
|
||||
let mut blocked_reason = None;
|
||||
|
||||
if let Some(kind) = recommended {
|
||||
if kind == FixKind::Reencode && policy == FixPolicy::Safe {
|
||||
blocked_reason = Some("Re-encode required but policy is safe".to_string());
|
||||
} else {
|
||||
actions.push(FixAction {
|
||||
kind,
|
||||
command: Vec::new(),
|
||||
destructive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
FixPlan {
|
||||
policy,
|
||||
recommended,
|
||||
actions,
|
||||
blocked_reason,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn plan_outcome(plan: FixPlan) -> FixOutcome {
|
||||
FixOutcome {
|
||||
plan,
|
||||
applied: false,
|
||||
success: false,
|
||||
message: "Fix plan generated".to_string(),
|
||||
output_path: None,
|
||||
re_scan_required: false,
|
||||
}
|
||||
}
|
||||
69
vid-repair-core/src/fs/collect.rs
Normal file
69
vid-repair-core/src/fs/collect.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use globset::{Glob, GlobSetBuilder};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
pub fn collect_files(paths: &[PathBuf], config: &Config) -> Result<Vec<PathBuf>> {
|
||||
let mut builder = GlobSetBuilder::new();
|
||||
for pattern in &config.scan.exclude {
|
||||
if let Ok(glob) = Glob::new(pattern) {
|
||||
builder.add(glob);
|
||||
}
|
||||
}
|
||||
let exclude_set = builder.build()?;
|
||||
|
||||
let mut files = Vec::new();
|
||||
|
||||
for input in paths {
|
||||
if input.is_file() {
|
||||
if should_include(input, config, &exclude_set) {
|
||||
files.push(input.clone());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if input.is_dir() {
|
||||
if config.scan.recursive {
|
||||
for entry in WalkDir::new(input)
|
||||
.follow_links(config.scan.follow_symlinks)
|
||||
.into_iter()
|
||||
.filter_map(|entry| entry.ok())
|
||||
{
|
||||
if entry.file_type().is_file() {
|
||||
let path = entry.path();
|
||||
if should_include(path, config, &exclude_set) {
|
||||
files.push(path.to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for entry in std::fs::read_dir(input)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && should_include(&path, config, &exclude_set) {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
fn should_include(path: &Path, config: &Config, exclude_set: &globset::GlobSet) -> bool {
|
||||
if exclude_set.is_match(path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let ext = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
|
||||
let ext = ext.to_lowercase();
|
||||
if ext.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
config.scan.include_ext.iter().any(|e| e == &ext)
|
||||
}
|
||||
3
vid-repair-core/src/fs/mod.rs
Normal file
3
vid-repair-core/src/fs/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod collect;
|
||||
|
||||
pub use collect::collect_files;
|
||||
14
vid-repair-core/src/lib.rs
Normal file
14
vid-repair-core/src/lib.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
pub mod config;
|
||||
pub mod fix;
|
||||
pub mod fs;
|
||||
pub mod report;
|
||||
pub mod rules;
|
||||
pub mod scan;
|
||||
pub mod watch;
|
||||
|
||||
pub use config::{Config, ConfigOverrides};
|
||||
pub use config::FixPolicy;
|
||||
pub use fix::{FixOutcome, FixPlan};
|
||||
pub use report::{Report, ScanReport};
|
||||
pub use rules::{RuleSet, Severity};
|
||||
pub use scan::{ScanOutcome, ScanRequest};
|
||||
10
vid-repair-core/src/report/json.rs
Normal file
10
vid-repair-core/src/report/json.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
|
||||
pub fn render_json<T: Serialize>(value: &T, pretty: bool) -> Result<String> {
|
||||
if pretty {
|
||||
Ok(serde_json::to_string_pretty(value)?)
|
||||
} else {
|
||||
Ok(serde_json::to_string(value)?)
|
||||
}
|
||||
}
|
||||
7
vid-repair-core/src/report/mod.rs
Normal file
7
vid-repair-core/src/report/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod json;
|
||||
mod text;
|
||||
mod types;
|
||||
|
||||
pub use json::render_json;
|
||||
pub use text::{render_fix_line, render_scan_line, render_summary};
|
||||
pub use types::{Report, ScanReport};
|
||||
54
vid-repair-core/src/report/text.rs
Normal file
54
vid-repair-core/src/report/text.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use crate::fix::FixOutcome;
|
||||
use crate::rules::Severity;
|
||||
use crate::scan::ScanOutcome;
|
||||
|
||||
pub fn render_scan_line(scan: &ScanOutcome) -> String {
|
||||
if scan.issues.is_empty() {
|
||||
format!("[OK] {}", scan.path.display())
|
||||
} else {
|
||||
let max = max_severity(scan);
|
||||
format!(
|
||||
"[ISSUES] {} ({} issues, max {:?})",
|
||||
scan.path.display(),
|
||||
scan.issues.len(),
|
||||
max
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_fix_line(scan: &ScanOutcome, fix: &FixOutcome) -> String {
|
||||
if fix.success {
|
||||
format!("[FIXED] {}", scan.path.display())
|
||||
} else if fix.applied {
|
||||
format!("[FAILED] {} - {}", scan.path.display(), fix.message)
|
||||
} else {
|
||||
format!("[SKIPPED] {} - {}", scan.path.display(), fix.message)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_summary(scans: &[ScanOutcome], fixes: Option<&[FixOutcome]>) -> String {
|
||||
let total = scans.len();
|
||||
let issues = scans.iter().filter(|scan| !scan.issues.is_empty()).count();
|
||||
|
||||
let mut line = format!("Summary: {} files, {} with issues", total, issues);
|
||||
|
||||
if let Some(fixes) = fixes {
|
||||
let fixed = fixes.iter().filter(|fix| fix.success).count();
|
||||
let failed = fixes.iter().filter(|fix| fix.applied && !fix.success).count();
|
||||
let skipped = fixes.iter().filter(|fix| !fix.applied).count();
|
||||
line.push_str(&format!(
|
||||
", {} fixed, {} failed, {} skipped",
|
||||
fixed, failed, skipped
|
||||
));
|
||||
}
|
||||
|
||||
line
|
||||
}
|
||||
|
||||
fn max_severity(scan: &ScanOutcome) -> Severity {
|
||||
scan.issues
|
||||
.iter()
|
||||
.map(|issue| issue.severity)
|
||||
.max_by_key(|sev| sev.rank())
|
||||
.unwrap_or(Severity::Info)
|
||||
}
|
||||
15
vid-repair-core/src/report/types.rs
Normal file
15
vid-repair-core/src/report/types.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::fix::FixOutcome;
|
||||
use crate::scan::ScanOutcome;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ScanReport {
|
||||
pub scan: ScanOutcome,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Report {
|
||||
pub scan: ScanOutcome,
|
||||
pub fix: Option<FixOutcome>,
|
||||
}
|
||||
78
vid-repair-core/src/rules/loader.rs
Normal file
78
vid-repair-core/src/rules/loader.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use regex::Regex;
|
||||
|
||||
use super::matcher::CompiledRule;
|
||||
use super::model::{Rule, RuleFile};
|
||||
|
||||
pub fn load_rules_from_dir(dir: &Path) -> Result<Vec<Rule>> {
|
||||
if !dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut entries: Vec<PathBuf> = fs::read_dir(dir)
|
||||
.with_context(|| format!("Failed to read ruleset dir {}", dir.display()))?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| path.extension().map(|ext| ext == "toml").unwrap_or(false))
|
||||
.collect();
|
||||
|
||||
entries.sort();
|
||||
|
||||
let mut rules = Vec::new();
|
||||
|
||||
for path in entries {
|
||||
let raw = fs::read_to_string(&path)
|
||||
.with_context(|| format!("Failed to read ruleset {}", path.display()))?;
|
||||
let file: RuleFile = toml::from_str(&raw)
|
||||
.with_context(|| format!("Failed to parse ruleset {}", path.display()))?;
|
||||
rules.extend(file.rules);
|
||||
}
|
||||
|
||||
Ok(rules)
|
||||
}
|
||||
|
||||
pub fn compile_rules(rules: Vec<Rule>) -> Result<Vec<CompiledRule>> {
|
||||
let mut compiled = Vec::new();
|
||||
|
||||
for rule in rules {
|
||||
let patterns = rule
|
||||
.patterns
|
||||
.iter()
|
||||
.map(|pattern| Regex::new(pattern))
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.with_context(|| format!("Invalid regex in rule {}", rule.id))?;
|
||||
|
||||
compiled.push(CompiledRule { rule, patterns });
|
||||
}
|
||||
|
||||
Ok(compiled)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_rule_file() {
|
||||
let toml = r#"
|
||||
[[rule]]
|
||||
id = "TEST_RULE"
|
||||
domain = "test"
|
||||
severity = "low"
|
||||
confidence = 0.8
|
||||
fix_tier = "none"
|
||||
stop_scan = false
|
||||
patterns = ["(?i)foo"]
|
||||
notes = "test"
|
||||
"#;
|
||||
let file: RuleFile = toml::from_str(toml).expect("rule file parse");
|
||||
assert_eq!(file.rules.len(), 1);
|
||||
|
||||
let compiled = compile_rules(file.rules).expect("compile rules");
|
||||
assert_eq!(compiled.len(), 1);
|
||||
assert_eq!(compiled[0].rule.id, "TEST_RULE");
|
||||
}
|
||||
}
|
||||
78
vid-repair-core/src/rules/matcher.rs
Normal file
78
vid-repair-core/src/rules/matcher.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use super::model::{FixTier, Rule, Severity};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompiledRule {
|
||||
pub rule: Rule,
|
||||
pub patterns: Vec<Regex>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RuleMatch {
|
||||
pub rule_id: String,
|
||||
pub domain: String,
|
||||
pub severity: Severity,
|
||||
pub confidence: f32,
|
||||
pub fix_tier: FixTier,
|
||||
pub stop_scan: bool,
|
||||
pub notes: Option<String>,
|
||||
pub action: Option<String>,
|
||||
pub evidence: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct RuleContext {
|
||||
pub tags: HashSet<String>,
|
||||
}
|
||||
|
||||
impl RuleContext {
|
||||
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
|
||||
self.tags.insert(tag.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl CompiledRule {
|
||||
pub fn matches(&self, lines: &[String], context: &RuleContext) -> Option<RuleMatch> {
|
||||
if !self.rule.requires.is_empty()
|
||||
&& !self.rule.requires.iter().all(|req| context.tags.contains(req))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
if !self.rule.excludes.is_empty()
|
||||
&& self.rule.excludes.iter().any(|ex| context.tags.contains(ex))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut evidence = Vec::new();
|
||||
|
||||
for line in lines {
|
||||
if self.patterns.iter().any(|re| re.is_match(line)) {
|
||||
if evidence.len() < 3 {
|
||||
evidence.push(line.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if evidence.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(RuleMatch {
|
||||
rule_id: self.rule.id.clone(),
|
||||
domain: self.rule.domain.clone(),
|
||||
severity: self.rule.severity,
|
||||
confidence: self.rule.confidence,
|
||||
fix_tier: self.rule.fix_tier,
|
||||
stop_scan: self.rule.stop_scan,
|
||||
notes: self.rule.notes.clone(),
|
||||
action: self.rule.action.clone(),
|
||||
evidence,
|
||||
})
|
||||
}
|
||||
}
|
||||
109
vid-repair-core/src/rules/mod.rs
Normal file
109
vid-repair-core/src/rules/mod.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::scan::ProbeData;
|
||||
|
||||
mod loader;
|
||||
mod matcher;
|
||||
mod model;
|
||||
|
||||
pub use matcher::{RuleContext, RuleMatch};
|
||||
pub use model::{FixTier, Severity};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RuleSet {
|
||||
pub rules: Vec<matcher::CompiledRule>,
|
||||
}
|
||||
|
||||
impl RuleSet {
|
||||
pub fn load() -> Result<Self> {
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
if let Ok(current) = std::env::current_dir() {
|
||||
candidates.push(current.join("rulesets"));
|
||||
}
|
||||
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(parent) = exe.parent() {
|
||||
candidates.push(parent.join("rulesets"));
|
||||
}
|
||||
}
|
||||
|
||||
for dir in candidates {
|
||||
let rules = loader::load_rules_from_dir(&dir)?;
|
||||
if !rules.is_empty() {
|
||||
let compiled = loader::compile_rules(rules)?;
|
||||
return Ok(Self { rules: compiled });
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self { rules: Vec::new() })
|
||||
}
|
||||
|
||||
pub fn match_lines(&self, lines: &[String], context: &RuleContext) -> Vec<RuleMatch> {
|
||||
let mut matches = Vec::new();
|
||||
for rule in &self.rules {
|
||||
if let Some(hit) = rule.matches(lines, context) {
|
||||
matches.push(hit);
|
||||
}
|
||||
}
|
||||
matches
|
||||
}
|
||||
|
||||
pub fn best_match<'a>(&self, matches: &'a [RuleMatch]) -> Option<&'a RuleMatch> {
|
||||
matches.iter().max_by(|a, b| {
|
||||
a.severity
|
||||
.rank()
|
||||
.cmp(&b.severity.rank())
|
||||
.then_with(|| a.confidence.partial_cmp(&b.confidence).unwrap_or(std::cmp::Ordering::Equal))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_context(probe: &ProbeData) -> RuleContext {
|
||||
let mut context = RuleContext::default();
|
||||
|
||||
if let Some(format) = &probe.format_name {
|
||||
context = context.with_tag(format!("container:{}", format.to_lowercase()));
|
||||
}
|
||||
|
||||
for stream in &probe.streams {
|
||||
if let Some(codec_type) = &stream.codec_type {
|
||||
context = context.with_tag(format!("stream:{}", codec_type.to_lowercase()));
|
||||
}
|
||||
if let Some(codec) = &stream.codec_name {
|
||||
context = context.with_tag(format!("codec:{}", codec.to_lowercase()));
|
||||
}
|
||||
}
|
||||
|
||||
context
|
||||
}
|
||||
|
||||
pub fn ruleset_dir_for_display() -> Result<PathBuf> {
|
||||
if let Ok(current) = std::env::current_dir() {
|
||||
let dir = current.join("rulesets");
|
||||
if dir.exists() {
|
||||
return Ok(dir);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(parent) = exe.parent() {
|
||||
return Ok(parent.join("rulesets"));
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!("No ruleset directory found"))
|
||||
}
|
||||
|
||||
pub fn ensure_ruleset_loaded(ruleset: &RuleSet) -> Result<()> {
|
||||
if ruleset.rules.is_empty() {
|
||||
let dir = ruleset_dir_for_display().unwrap_or_else(|_| PathBuf::from("rulesets"));
|
||||
return Err(anyhow::anyhow!(
|
||||
"No rulesets found. Expected TOML files in {}",
|
||||
dir.display()
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
72
vid-repair-core/src/rules/model.rs
Normal file
72
vid-repair-core/src/rules/model.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Severity {
|
||||
Info,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Severe,
|
||||
}
|
||||
|
||||
impl Severity {
|
||||
pub fn rank(self) -> u8 {
|
||||
match self {
|
||||
Severity::Info => 0,
|
||||
Severity::Low => 1,
|
||||
Severity::Medium => 2,
|
||||
Severity::High => 3,
|
||||
Severity::Severe => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FixTier {
|
||||
None,
|
||||
Remux,
|
||||
Reencode,
|
||||
}
|
||||
|
||||
impl FixTier {
|
||||
pub fn rank(self) -> u8 {
|
||||
match self {
|
||||
FixTier::None => 0,
|
||||
FixTier::Remux => 1,
|
||||
FixTier::Reencode => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Rule {
|
||||
pub id: String,
|
||||
pub domain: String,
|
||||
pub severity: Severity,
|
||||
#[serde(default = "default_confidence")]
|
||||
pub confidence: f32,
|
||||
pub fix_tier: FixTier,
|
||||
#[serde(default)]
|
||||
pub stop_scan: bool,
|
||||
pub patterns: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub notes: Option<String>,
|
||||
#[serde(default)]
|
||||
pub action: Option<String>,
|
||||
#[serde(default)]
|
||||
pub requires: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub excludes: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_confidence() -> f32 {
|
||||
0.5
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct RuleFile {
|
||||
#[serde(rename = "rule")]
|
||||
pub rules: Vec<Rule>,
|
||||
}
|
||||
72
vid-repair-core/src/scan/decode.rs
Normal file
72
vid-repair-core/src/scan/decode.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::rules::RuleSet;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DecodeOutput {
|
||||
pub lines: Vec<String>,
|
||||
pub early_stop: bool,
|
||||
}
|
||||
|
||||
pub fn run_decode(path: &Path, ffmpeg_path: &str, ruleset: &RuleSet) -> Result<DecodeOutput> {
|
||||
let mut child = Command::new(ffmpeg_path)
|
||||
.arg("-v")
|
||||
.arg("error")
|
||||
.arg("-i")
|
||||
.arg(path)
|
||||
.arg("-f")
|
||||
.arg("null")
|
||||
.arg("-")
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::null())
|
||||
.spawn()
|
||||
.with_context(|| format!("Failed to run ffmpeg decode for {}", path.display()))?;
|
||||
|
||||
let stderr = child.stderr.take().context("Failed to capture ffmpeg stderr")?;
|
||||
let reader = BufReader::new(stderr);
|
||||
|
||||
let early_stop = Arc::new(AtomicBool::new(false));
|
||||
let early_stop_flag = early_stop.clone();
|
||||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
for line in reader.lines() {
|
||||
let line = line.unwrap_or_default();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
lines.push(line.clone());
|
||||
|
||||
if should_stop(&line, ruleset) {
|
||||
early_stop_flag.store(true, Ordering::SeqCst);
|
||||
let _ = child.kill();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = child.wait();
|
||||
|
||||
Ok(DecodeOutput {
|
||||
lines,
|
||||
early_stop: early_stop.load(Ordering::SeqCst),
|
||||
})
|
||||
}
|
||||
|
||||
fn should_stop(line: &str, ruleset: &RuleSet) -> bool {
|
||||
for rule in &ruleset.rules {
|
||||
if !rule.rule.stop_scan {
|
||||
continue;
|
||||
}
|
||||
|
||||
if rule.patterns.iter().any(|re| re.is_match(line)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
83
vid-repair-core/src/scan/ffprobe.rs
Normal file
83
vid-repair-core/src/scan/ffprobe.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::types::{ProbeData, StreamInfo};
|
||||
|
||||
pub fn run_ffprobe(path: &Path, ffprobe_path: &str) -> Result<ProbeData> {
|
||||
let output = Command::new(ffprobe_path)
|
||||
.arg("-v")
|
||||
.arg("error")
|
||||
.arg("-print_format")
|
||||
.arg("json")
|
||||
.arg("-show_format")
|
||||
.arg("-show_streams")
|
||||
.arg(path)
|
||||
.output()
|
||||
.with_context(|| format!("Failed to run ffprobe on {}", path.display()))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!(
|
||||
"ffprobe failed for {}: {}",
|
||||
path.display(),
|
||||
stderr.trim()
|
||||
);
|
||||
}
|
||||
|
||||
let raw: Value = serde_json::from_slice(&output.stdout)
|
||||
.with_context(|| format!("Failed to parse ffprobe output for {}", path.display()))?;
|
||||
|
||||
let format_name = raw
|
||||
.get("format")
|
||||
.and_then(|fmt| fmt.get("format_name"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let duration = raw
|
||||
.get("format")
|
||||
.and_then(|fmt| fmt.get("duration"))
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse::<f64>().ok());
|
||||
|
||||
let streams = raw
|
||||
.get("streams")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.map(|stream| StreamInfo {
|
||||
codec_type: stream
|
||||
.get("codec_type")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
codec_name: stream
|
||||
.get("codec_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
width: stream.get("width").and_then(|v| v.as_u64()).map(|v| v as u32),
|
||||
height: stream
|
||||
.get("height")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|v| v as u32),
|
||||
sample_rate: stream
|
||||
.get("sample_rate")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
channels: stream
|
||||
.get("channels")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|v| v as u32),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(ProbeData {
|
||||
format_name,
|
||||
duration,
|
||||
streams,
|
||||
raw,
|
||||
})
|
||||
}
|
||||
48
vid-repair-core/src/scan/mod.rs
Normal file
48
vid-repair-core/src/scan/mod.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::rules::{build_context, RuleMatch, RuleSet};
|
||||
|
||||
mod decode;
|
||||
mod ffprobe;
|
||||
mod types;
|
||||
|
||||
pub use types::{Issue, ProbeData, ScanOutcome, ScanRequest};
|
||||
|
||||
pub fn scan_file(path: &Path, config: &Config, ruleset: &RuleSet) -> Result<ScanOutcome> {
|
||||
let probe = ffprobe::run_ffprobe(path, &config.ffprobe_path)?;
|
||||
|
||||
let decode = decode::run_decode(path, &config.ffmpeg_path, ruleset)?;
|
||||
|
||||
let context = build_context(&probe);
|
||||
let matches = ruleset.match_lines(&decode.lines, &context);
|
||||
|
||||
let issues = matches
|
||||
.iter()
|
||||
.map(|hit| issue_from_match(hit))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(ScanOutcome {
|
||||
path: path.to_path_buf(),
|
||||
probe,
|
||||
issues,
|
||||
decode_errors: decode.lines,
|
||||
early_stop: decode.early_stop,
|
||||
})
|
||||
}
|
||||
|
||||
fn issue_from_match(hit: &RuleMatch) -> Issue {
|
||||
Issue {
|
||||
code: hit.rule_id.clone(),
|
||||
severity: hit.severity,
|
||||
fix_tier: hit.fix_tier,
|
||||
message: hit
|
||||
.notes
|
||||
.clone()
|
||||
.unwrap_or_else(|| hit.rule_id.clone()),
|
||||
evidence: hit.evidence.clone(),
|
||||
action: hit.action.clone(),
|
||||
}
|
||||
}
|
||||
47
vid-repair-core/src/scan/types.rs
Normal file
47
vid-repair-core/src/scan/types.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::rules::{FixTier, Severity};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StreamInfo {
|
||||
pub codec_type: Option<String>,
|
||||
pub codec_name: Option<String>,
|
||||
pub width: Option<u32>,
|
||||
pub height: Option<u32>,
|
||||
pub sample_rate: Option<String>,
|
||||
pub channels: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProbeData {
|
||||
pub format_name: Option<String>,
|
||||
pub duration: Option<f64>,
|
||||
pub streams: Vec<StreamInfo>,
|
||||
pub raw: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Issue {
|
||||
pub code: String,
|
||||
pub severity: Severity,
|
||||
pub fix_tier: FixTier,
|
||||
pub message: String,
|
||||
pub evidence: Vec<String>,
|
||||
pub action: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ScanOutcome {
|
||||
pub path: PathBuf,
|
||||
pub probe: ProbeData,
|
||||
pub issues: Vec<Issue>,
|
||||
pub decode_errors: Vec<String>,
|
||||
pub early_stop: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScanRequest {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
109
vid-repair-core/src/watch/mod.rs
Normal file
109
vid-repair-core/src/watch/mod.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::{self, RecvTimeoutError};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::Result;
|
||||
use notify::{RecommendedWatcher, RecursiveMode, Watcher, EventKind};
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct WatchEntry {
|
||||
last_event: Instant,
|
||||
size: u64,
|
||||
mtime: std::time::SystemTime,
|
||||
}
|
||||
|
||||
pub fn watch_paths<F>(paths: &[PathBuf], config: &Config, mut handler: F) -> Result<()>
|
||||
where
|
||||
F: FnMut(PathBuf),
|
||||
{
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let mut watcher: RecommendedWatcher = Watcher::new(tx, notify::Config::default())?;
|
||||
|
||||
for path in paths {
|
||||
let mode = if path.is_dir() {
|
||||
RecursiveMode::Recursive
|
||||
} else {
|
||||
RecursiveMode::NonRecursive
|
||||
};
|
||||
watcher.watch(path, mode)?;
|
||||
}
|
||||
|
||||
let mut entries: HashMap<PathBuf, WatchEntry> = HashMap::new();
|
||||
let settle = Duration::from_secs(config.watch.settle_seconds);
|
||||
|
||||
loop {
|
||||
match rx.recv_timeout(Duration::from_secs(1)) {
|
||||
Ok(event) => {
|
||||
if let Ok(event) = event {
|
||||
if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_)) {
|
||||
for path in event.paths {
|
||||
if should_ignore(&path, config) {
|
||||
continue;
|
||||
}
|
||||
if let Ok(metadata) = std::fs::metadata(&path) {
|
||||
if !metadata.is_file() {
|
||||
continue;
|
||||
}
|
||||
let entry = WatchEntry {
|
||||
last_event: Instant::now(),
|
||||
size: metadata.len(),
|
||||
mtime: metadata.modified().unwrap_or_else(|_| std::time::SystemTime::UNIX_EPOCH),
|
||||
};
|
||||
entries.insert(path, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) => {
|
||||
// tick
|
||||
}
|
||||
Err(RecvTimeoutError::Disconnected) => break,
|
||||
}
|
||||
|
||||
let ready: Vec<PathBuf> = entries
|
||||
.iter()
|
||||
.filter_map(|(path, entry)| {
|
||||
if entry.last_event.elapsed() < settle {
|
||||
return None;
|
||||
}
|
||||
|
||||
let metadata = std::fs::metadata(path).ok()?;
|
||||
if !metadata.is_file() {
|
||||
return None;
|
||||
}
|
||||
let size = metadata.len();
|
||||
let mtime = metadata.modified().ok()?;
|
||||
if size == entry.size && mtime == entry.mtime {
|
||||
Some(path.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
for path in ready {
|
||||
entries.remove(&path);
|
||||
handler(path);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_ignore(path: &Path, config: &Config) -> bool {
|
||||
let ext = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
|
||||
let ext = ext.to_lowercase();
|
||||
if ext.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if config.watch.ignore_ext.iter().any(|e| e == &ext) {
|
||||
return true;
|
||||
}
|
||||
|
||||
!config.scan.include_ext.iter().any(|e| e == &ext)
|
||||
}
|
||||
1
vid-repair/.gitignore
vendored
Normal file
1
vid-repair/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
13
vid-repair/Cargo.toml
Normal file
13
vid-repair/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "vid-repair"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
vid-repair-core = { path = "../vid-repair-core" }
|
||||
489
vid-repair/src/main.rs
Normal file
489
vid-repair/src/main.rs
Normal file
@@ -0,0 +1,489 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use rayon::prelude::*;
|
||||
use rayon::ThreadPoolBuilder;
|
||||
|
||||
use vid_repair_core::config::{Config, ConfigOverrides, FixPolicy, ScanDepth};
|
||||
use vid_repair_core::fix::{self, FixOutcome};
|
||||
use vid_repair_core::report::{render_fix_line, render_json, render_scan_line, render_summary};
|
||||
use vid_repair_core::rules::{ensure_ruleset_loaded, RuleSet};
|
||||
use vid_repair_core::scan::{scan_file, ScanOutcome};
|
||||
use vid_repair_core::{fs, watch};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "vid-repair")]
|
||||
#[command(about = "Scan and repair video files using ffmpeg/ffprobe", long_about = None)]
|
||||
struct Cli {
|
||||
/// Path to config file (defaults to XDG config location)
|
||||
#[arg(long)]
|
||||
config: Option<PathBuf>,
|
||||
|
||||
/// Emit JSON output
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
|
||||
/// Number of parallel jobs (0 = auto)
|
||||
#[arg(long)]
|
||||
jobs: Option<usize>,
|
||||
|
||||
/// Override ffmpeg binary path
|
||||
#[arg(long)]
|
||||
ffmpeg_path: Option<String>,
|
||||
|
||||
/// Override ffprobe binary path
|
||||
#[arg(long)]
|
||||
ffprobe_path: Option<String>,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CommonArgs {
|
||||
config: Option<PathBuf>,
|
||||
json: bool,
|
||||
jobs: Option<usize>,
|
||||
ffmpeg_path: Option<String>,
|
||||
ffprobe_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Commands {
|
||||
Scan(ScanArgs),
|
||||
Fix(FixArgs),
|
||||
Report(ScanArgs),
|
||||
Config(ConfigArgs),
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Debug, Clone, Copy)]
|
||||
enum ScanDepthArg {
|
||||
Quick,
|
||||
Standard,
|
||||
Deep,
|
||||
}
|
||||
|
||||
impl From<ScanDepthArg> for ScanDepth {
|
||||
fn from(value: ScanDepthArg) -> Self {
|
||||
match value {
|
||||
ScanDepthArg::Quick => ScanDepth::Quick,
|
||||
ScanDepthArg::Standard => ScanDepth::Standard,
|
||||
ScanDepthArg::Deep => ScanDepth::Deep,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Debug, Clone, Copy)]
|
||||
enum FixPolicyArg {
|
||||
Safe,
|
||||
Aggressive,
|
||||
}
|
||||
|
||||
impl From<FixPolicyArg> for FixPolicy {
|
||||
fn from(value: FixPolicyArg) -> Self {
|
||||
match value {
|
||||
FixPolicyArg::Safe => FixPolicy::Safe,
|
||||
FixPolicyArg::Aggressive => FixPolicy::Aggressive,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct ScanArgs {
|
||||
/// Paths to scan (files or directories)
|
||||
#[arg(value_name = "PATH")]
|
||||
paths: Vec<PathBuf>,
|
||||
|
||||
/// Watch for changes and scan when files settle
|
||||
#[arg(long)]
|
||||
watch: bool,
|
||||
|
||||
/// Override scan depth (quick|standard|deep)
|
||||
#[arg(long)]
|
||||
scan_depth: Option<ScanDepthArg>,
|
||||
|
||||
/// Override recursive scanning
|
||||
#[arg(long)]
|
||||
recursive: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct FixArgs {
|
||||
/// Paths to fix (files or directories)
|
||||
#[arg(value_name = "PATH")]
|
||||
paths: Vec<PathBuf>,
|
||||
|
||||
/// Dry-run: show plan without applying changes
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
|
||||
/// Watch for changes and fix when files settle
|
||||
#[arg(long)]
|
||||
watch: bool,
|
||||
|
||||
/// Override scan depth (quick|standard|deep)
|
||||
#[arg(long)]
|
||||
scan_depth: Option<ScanDepthArg>,
|
||||
|
||||
/// Override recursive scanning
|
||||
#[arg(long)]
|
||||
recursive: bool,
|
||||
|
||||
/// Override repair policy (safe|aggressive)
|
||||
#[arg(long)]
|
||||
policy: Option<FixPolicyArg>,
|
||||
|
||||
/// Output directory for repaired files (empty = in-place)
|
||||
#[arg(long)]
|
||||
output_dir: Option<String>,
|
||||
|
||||
/// Keep original file (rename to *.original.* after successful fix)
|
||||
#[arg(long)]
|
||||
keep_original: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct ConfigArgs {
|
||||
#[command(subcommand)]
|
||||
command: ConfigCommand,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum ConfigCommand {
|
||||
Init {
|
||||
/// Overwrite existing config
|
||||
#[arg(long)]
|
||||
force: bool,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let Cli {
|
||||
config,
|
||||
json,
|
||||
jobs,
|
||||
ffmpeg_path,
|
||||
ffprobe_path,
|
||||
command,
|
||||
} = cli;
|
||||
|
||||
let common = CommonArgs {
|
||||
config,
|
||||
json,
|
||||
jobs,
|
||||
ffmpeg_path,
|
||||
ffprobe_path,
|
||||
};
|
||||
|
||||
match command {
|
||||
Commands::Config(args) => handle_config(args, common.config.clone()),
|
||||
Commands::Scan(args) => handle_scan(args, &common),
|
||||
Commands::Report(args) => handle_report(args, &common),
|
||||
Commands::Fix(args) => handle_fix(args, &common),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_config(args: ConfigArgs, path: Option<PathBuf>) -> Result<()> {
|
||||
match args.command {
|
||||
ConfigCommand::Init { force } => {
|
||||
let path = path.unwrap_or_else(|| vid_repair_core::config::default_config_path().unwrap());
|
||||
vid_repair_core::config::init_config_at(&path, force)?;
|
||||
println!("Config written to {}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_scan(args: ScanArgs, common: &CommonArgs) -> Result<()> {
|
||||
let (mut config, _config_path) = Config::load_or_init(common.config.clone())?;
|
||||
let mut overrides = ConfigOverrides::default();
|
||||
|
||||
overrides.ffmpeg_path = common.ffmpeg_path.clone();
|
||||
overrides.ffprobe_path = common.ffprobe_path.clone();
|
||||
if common.json {
|
||||
overrides.json = Some(true);
|
||||
}
|
||||
if let Some(jobs) = common.jobs {
|
||||
overrides.jobs = Some(jobs);
|
||||
}
|
||||
overrides.scan_depth = args.scan_depth.map(ScanDepth::from);
|
||||
if args.recursive {
|
||||
overrides.scan_recursive = Some(true);
|
||||
}
|
||||
overrides.watch = Some(args.watch);
|
||||
|
||||
config.apply_overrides(&overrides);
|
||||
config.normalize();
|
||||
|
||||
let ruleset = RuleSet::load()?;
|
||||
ensure_ruleset_loaded(&ruleset)?;
|
||||
|
||||
if args.paths.is_empty() {
|
||||
anyhow::bail!("No input paths provided");
|
||||
}
|
||||
|
||||
if config.watch.enabled {
|
||||
return watch_scan(args.paths, &config, &ruleset);
|
||||
}
|
||||
|
||||
let files = fs::collect_files(&args.paths, &config)?;
|
||||
if files.is_empty() {
|
||||
println!("No matching files found.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let scans = run_scans(files, &config, &ruleset)?;
|
||||
|
||||
if config.report.json {
|
||||
let json = render_json(&scans, config.report.pretty)?;
|
||||
println!("{}", json);
|
||||
} else {
|
||||
for scan in &scans {
|
||||
println!("{}", render_scan_line(scan));
|
||||
}
|
||||
println!("{}", render_summary(&scans, None));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_report(args: ScanArgs, common: &CommonArgs) -> Result<()> {
|
||||
let mut common = common.clone();
|
||||
common.json = true;
|
||||
handle_scan(args, &common)
|
||||
}
|
||||
|
||||
fn handle_fix(args: FixArgs, common: &CommonArgs) -> Result<()> {
|
||||
let (mut config, _config_path) = Config::load_or_init(common.config.clone())?;
|
||||
let mut overrides = ConfigOverrides::default();
|
||||
|
||||
overrides.ffmpeg_path = common.ffmpeg_path.clone();
|
||||
overrides.ffprobe_path = common.ffprobe_path.clone();
|
||||
if common.json {
|
||||
overrides.json = Some(true);
|
||||
}
|
||||
overrides.jobs = common.jobs;
|
||||
overrides.scan_depth = args.scan_depth.map(ScanDepth::from);
|
||||
if args.recursive {
|
||||
overrides.scan_recursive = Some(true);
|
||||
}
|
||||
overrides.policy = args.policy.map(FixPolicy::from);
|
||||
overrides.output_dir = args.output_dir;
|
||||
overrides.keep_original = Some(args.keep_original);
|
||||
overrides.watch = Some(args.watch);
|
||||
|
||||
config.apply_overrides(&overrides);
|
||||
config.normalize();
|
||||
|
||||
let ruleset = RuleSet::load()?;
|
||||
ensure_ruleset_loaded(&ruleset)?;
|
||||
|
||||
if args.paths.is_empty() {
|
||||
anyhow::bail!("No input paths provided");
|
||||
}
|
||||
|
||||
if config.watch.enabled {
|
||||
return watch_fix(args.paths, &config, &ruleset, args.dry_run);
|
||||
}
|
||||
|
||||
let files = fs::collect_files(&args.paths, &config)?;
|
||||
if files.is_empty() {
|
||||
println!("No matching files found.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (scans, fixes) = run_fixes(files, &config, &ruleset, args.dry_run)?;
|
||||
|
||||
if config.report.json {
|
||||
let payload = serde_json::json!({ "scans": scans, "fixes": fixes });
|
||||
let json = render_json(&payload, config.report.pretty)?;
|
||||
println!("{}", json);
|
||||
} else {
|
||||
for (scan, fix) in scans.iter().zip(fixes.iter()) {
|
||||
println!("{}", render_fix_line(scan, fix));
|
||||
}
|
||||
println!("{}", render_summary(&scans, Some(&fixes)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_scans(files: Vec<PathBuf>, config: &Config, ruleset: &RuleSet) -> Result<Vec<ScanOutcome>> {
|
||||
let jobs = if config.performance.jobs == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(config.performance.jobs)
|
||||
};
|
||||
|
||||
let scans = if let Some(jobs) = jobs {
|
||||
let pool = ThreadPoolBuilder::new().num_threads(jobs).build()?;
|
||||
pool.install(|| {
|
||||
files
|
||||
.par_iter()
|
||||
.filter_map(|path| match scan_file(path, config, ruleset) {
|
||||
Ok(scan) => Some(scan),
|
||||
Err(err) => {
|
||||
eprintln!("[ERROR] {}: {}", path.display(), err);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
} else {
|
||||
files
|
||||
.iter()
|
||||
.filter_map(|path| match scan_file(path, config, ruleset) {
|
||||
Ok(scan) => Some(scan),
|
||||
Err(err) => {
|
||||
eprintln!("[ERROR] {}: {}", path.display(), err);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
Ok(scans)
|
||||
}
|
||||
|
||||
fn run_fixes(
|
||||
files: Vec<PathBuf>,
|
||||
config: &Config,
|
||||
ruleset: &RuleSet,
|
||||
dry_run: bool,
|
||||
) -> Result<(Vec<ScanOutcome>, Vec<FixOutcome>)> {
|
||||
let jobs = if config.performance.jobs == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(config.performance.jobs)
|
||||
};
|
||||
|
||||
if let Some(jobs) = jobs {
|
||||
let pool = ThreadPoolBuilder::new().num_threads(jobs).build()?;
|
||||
pool.install(|| process_fix_batch_parallel(files, config, ruleset, dry_run))
|
||||
} else {
|
||||
process_fix_batch(files, config, ruleset, dry_run)
|
||||
}
|
||||
}
|
||||
|
||||
fn process_fix_batch(
|
||||
files: Vec<PathBuf>,
|
||||
config: &Config,
|
||||
ruleset: &RuleSet,
|
||||
dry_run: bool,
|
||||
) -> Result<(Vec<ScanOutcome>, Vec<FixOutcome>)> {
|
||||
let mut scans = Vec::new();
|
||||
let mut fixes = Vec::new();
|
||||
|
||||
for path in files {
|
||||
let scan = match scan_file(&path, config, ruleset) {
|
||||
Ok(scan) => scan,
|
||||
Err(err) => {
|
||||
eprintln!("[ERROR] {}: {}", path.display(), err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let plan = fix::planner::plan_fix(&scan.issues, config.repair.policy);
|
||||
let outcome = if dry_run {
|
||||
fix::planner::plan_outcome(plan)
|
||||
} else {
|
||||
match fix::executor::apply_fix(&path, &plan, config, ruleset) {
|
||||
Ok(outcome) => outcome,
|
||||
Err(err) => FixOutcome {
|
||||
plan: plan.clone(),
|
||||
applied: true,
|
||||
success: false,
|
||||
message: format!("Fix failed: {}", err),
|
||||
output_path: None,
|
||||
re_scan_required: true,
|
||||
},
|
||||
}
|
||||
};
|
||||
scans.push(scan);
|
||||
fixes.push(outcome);
|
||||
}
|
||||
|
||||
Ok((scans, fixes))
|
||||
}
|
||||
|
||||
fn process_fix_batch_parallel(
|
||||
files: Vec<PathBuf>,
|
||||
config: &Config,
|
||||
ruleset: &RuleSet,
|
||||
dry_run: bool,
|
||||
) -> Result<(Vec<ScanOutcome>, Vec<FixOutcome>)> {
|
||||
let results = files
|
||||
.par_iter()
|
||||
.filter_map(|path| {
|
||||
let scan = match scan_file(path, config, ruleset) {
|
||||
Ok(scan) => scan,
|
||||
Err(err) => {
|
||||
eprintln!("[ERROR] {}: {}", path.display(), err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let plan = fix::planner::plan_fix(&scan.issues, config.repair.policy);
|
||||
let outcome = if dry_run {
|
||||
fix::planner::plan_outcome(plan)
|
||||
} else {
|
||||
match fix::executor::apply_fix(path, &plan, config, ruleset) {
|
||||
Ok(outcome) => outcome,
|
||||
Err(err) => FixOutcome {
|
||||
plan: plan.clone(),
|
||||
applied: true,
|
||||
success: false,
|
||||
message: format!("Fix failed: {}", err),
|
||||
output_path: None,
|
||||
re_scan_required: true,
|
||||
},
|
||||
}
|
||||
};
|
||||
Some((scan, outcome))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (scans, fixes): (Vec<_>, Vec<_>) = results.into_iter().unzip();
|
||||
Ok((scans, fixes))
|
||||
}
|
||||
|
||||
fn watch_scan(paths: Vec<PathBuf>, config: &Config, ruleset: &RuleSet) -> Result<()> {
|
||||
println!("Watch mode enabled. Waiting for files to settle...");
|
||||
watch::watch_paths(&paths, config, |path| {
|
||||
match scan_file(&path, config, ruleset) {
|
||||
Ok(scan) => {
|
||||
println!("{}", render_scan_line(&scan));
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("[ERROR] {}: {}", path.display(), err);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn watch_fix(paths: Vec<PathBuf>, config: &Config, ruleset: &RuleSet, dry_run: bool) -> Result<()> {
|
||||
println!("Watch mode enabled. Waiting for files to settle...");
|
||||
watch::watch_paths(&paths, config, |path| {
|
||||
match scan_file(&path, config, ruleset) {
|
||||
Ok(scan) => {
|
||||
let plan = fix::planner::plan_fix(&scan.issues, config.repair.policy);
|
||||
let outcome = if dry_run {
|
||||
fix::planner::plan_outcome(plan)
|
||||
} else {
|
||||
match fix::executor::apply_fix(&path, &plan, config, ruleset) {
|
||||
Ok(outcome) => outcome,
|
||||
Err(err) => {
|
||||
eprintln!("[ERROR] Fix failed {}: {}", path.display(), err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
println!("{}", render_fix_line(&scan, &outcome));
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("[ERROR] {}: {}", path.display(), err);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user