feat: init
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
595
Cargo.lock
generated
Normal file
595
Cargo.lock
generated
Normal file
@@ -0,0 +1,595 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[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",
|
||||
]
|
||||
|
||||
[[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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[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.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "ectf-tools"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"libc",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[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 = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[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 = "js-sys"
|
||||
version = "0.3.90"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.182"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[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 = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
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 = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[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.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[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.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.113"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.113"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.113"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.113"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[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.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
10
Cargo.toml
Normal file
10
Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "ectf-tools"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
anyhow = "1"
|
||||
libc = "0.2"
|
||||
131
src/log.rs
Normal file
131
src/log.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
|
||||
static VERBOSITY: AtomicU8 = AtomicU8::new(0);
|
||||
|
||||
pub fn set_verbosity(level: u8) {
|
||||
VERBOSITY.store(level, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn verbosity() -> u8 {
|
||||
VERBOSITY.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
// ANSI codes
|
||||
const RESET: &str = "\x1b[0m";
|
||||
const BOLD: &str = "\x1b[1m";
|
||||
const DIM: &str = "\x1b[2m";
|
||||
|
||||
// 256-color: \x1b[38;5;Nm
|
||||
const GRAY: &str = "\x1b[38;5;242m";
|
||||
const BLUE: &str = "\x1b[38;5;75m";
|
||||
const GREEN: &str = "\x1b[38;5;114m";
|
||||
const YELLOW: &str = "\x1b[38;5;221m";
|
||||
const RED: &str = "\x1b[38;5;203m";
|
||||
const MAGENTA: &str = "\x1b[38;5;183m";
|
||||
const WHITE: &str = "\x1b[38;5;252m";
|
||||
|
||||
// Padding so messages align. Longest tag is [error] = 7 chars.
|
||||
// Each tag pads to 8 total (tag + spaces) before the message.
|
||||
const PAD: usize = 8; // "[error] " = 8
|
||||
|
||||
macro_rules! tag {
|
||||
($label:expr) => {
|
||||
// pad_len = PAD - len("[") - len($label) - len("]") - len(" ")
|
||||
// but easier: total visible = 2 + label.len(), pad to PAD
|
||||
concat!("[", $label, "]")
|
||||
};
|
||||
}
|
||||
|
||||
/// Trace (verbosity >= 2) — gray
|
||||
pub fn trace(msg: &str) {
|
||||
if verbosity() >= 2 {
|
||||
eprintln!("{RED}{:<PAD$}{RESET}{msg}", tag!("trace"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Debug (verbosity >= 1) — blue
|
||||
pub fn debug(msg: &str) {
|
||||
if verbosity() >= 1 {
|
||||
eprintln!("{BLUE}{:<PAD$}{RESET}{msg}", tag!("debug"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Info — white
|
||||
pub fn info(msg: &str) {
|
||||
println!("{WHITE}{:<PAD$}{RESET}{msg}", tag!("info"));
|
||||
}
|
||||
|
||||
/// Success — green
|
||||
pub fn success(msg: &str) {
|
||||
println!("{GREEN}{:<PAD$}{RESET}{msg}", tag!("ok"));
|
||||
}
|
||||
|
||||
/// Warning — yellow
|
||||
pub fn warning(msg: &str) {
|
||||
eprintln!("{YELLOW}{:<PAD$}{RESET}{msg}", tag!("warn"));
|
||||
}
|
||||
|
||||
/// Error — bold red
|
||||
pub fn error(msg: &str) {
|
||||
eprintln!("{BOLD}{RED}{:<PAD$}{RESET}{msg}", tag!("error"));
|
||||
}
|
||||
|
||||
/// Error cause — red, indented to match message column
|
||||
pub fn error_cause(msg: &str) {
|
||||
eprintln!("{:PAD$}{RED}{msg}{RESET}", "");
|
||||
}
|
||||
|
||||
/// HSM debug messages from firmware — magenta
|
||||
pub fn hsm_debug(msg: &str) {
|
||||
eprintln!("{MAGENTA}{:<PAD$}{RESET}{msg}", tag!("hsm"));
|
||||
}
|
||||
|
||||
/// HSM debug messages that aren't valid UTF-8 — show as hexdump
|
||||
pub fn hsm_debug_hex(data: &[u8]) {
|
||||
eprintln!("{MAGENTA}{:<PAD$}{RESET}({} bytes)", tag!("hsm"), data.len());
|
||||
for (i, chunk) in data.chunks(16).enumerate() {
|
||||
let offset = i * 16;
|
||||
let mut hex_part = String::with_capacity(40);
|
||||
let mut ascii_part = String::with_capacity(16);
|
||||
for (j, &b) in chunk.iter().enumerate() {
|
||||
if j > 0 && j % 2 == 0 {
|
||||
hex_part.push(' ');
|
||||
}
|
||||
hex_part.push_str(&format!("{b:02x}"));
|
||||
ascii_part.push(if b.is_ascii_graphic() || b == b' ' {
|
||||
b as char
|
||||
} else {
|
||||
'.'
|
||||
});
|
||||
}
|
||||
eprintln!("{:PAD$}{GRAY}{offset:08x}:{RESET} {hex_part:<39} {GREEN}{ascii_part}{RESET}", "");
|
||||
}
|
||||
}
|
||||
|
||||
/// Trace-level hex+ASCII dump of a byte buffer (verbosity >= 2)
|
||||
/// Format matches xxd: paired hex bytes, 16 per line.
|
||||
pub fn trace_hex(label: &str, data: &[u8]) {
|
||||
if verbosity() < 2 {
|
||||
return;
|
||||
}
|
||||
eprintln!("{RED}{:<PAD$}{RESET}{label} ({} bytes)", tag!("trace"), data.len());
|
||||
for (i, chunk) in data.chunks(16).enumerate() {
|
||||
let offset = i * 16;
|
||||
let mut hex_part = String::with_capacity(40);
|
||||
let mut ascii_part = String::with_capacity(16);
|
||||
for (j, &b) in chunk.iter().enumerate() {
|
||||
if j > 0 && j % 2 == 0 {
|
||||
hex_part.push(' ');
|
||||
}
|
||||
hex_part.push_str(&format!("{b:02x}"));
|
||||
ascii_part.push(if b.is_ascii_graphic() || b == b' ' {
|
||||
b as char
|
||||
} else {
|
||||
'.'
|
||||
});
|
||||
}
|
||||
eprintln!("{:PAD$}{GRAY}{offset:08x}:{RESET} {hex_part:<39} {GREEN}{ascii_part}{RESET}", "");
|
||||
}
|
||||
}
|
||||
309
src/main.rs
Normal file
309
src/main.rs
Normal file
@@ -0,0 +1,309 @@
|
||||
mod log;
|
||||
mod protocol;
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use clap::{Parser, Subcommand};
|
||||
use protocol::{HSMIntf, Opcode};
|
||||
|
||||
const PIN_LEN: usize = 6;
|
||||
const MAX_NAME_LEN: usize = 32;
|
||||
const MAX_FILE_LEN: usize = 8192;
|
||||
const UUID_LEN: usize = 16;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "ectf-tools", about = "eCTF host tools")]
|
||||
struct Cli {
|
||||
/// Serial port
|
||||
port: String,
|
||||
|
||||
/// Verbosity level (-v, -vv)
|
||||
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
|
||||
verbose: u8,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
/// Write a file to the HSM
|
||||
Write {
|
||||
/// 6-digit PIN
|
||||
pin: String,
|
||||
/// Slot (0-7)
|
||||
slot: u8,
|
||||
/// Group ID (decimal or 0x hex)
|
||||
gid: String,
|
||||
/// Path to file to write
|
||||
file: PathBuf,
|
||||
/// UUID (hex, 32 chars). Random if omitted.
|
||||
#[arg(short, long)]
|
||||
uuid: Option<String>,
|
||||
},
|
||||
/// Read a file from the HSM
|
||||
Read {
|
||||
/// 6-digit PIN
|
||||
pin: String,
|
||||
/// Slot (0-7)
|
||||
slot: u8,
|
||||
/// Output directory
|
||||
output_dir: PathBuf,
|
||||
/// Overwrite existing file
|
||||
#[arg(short, long)]
|
||||
force: bool,
|
||||
},
|
||||
/// List files on the HSM
|
||||
List {
|
||||
/// 6-digit PIN
|
||||
pin: String,
|
||||
},
|
||||
/// Interrogate files on a connected HSM
|
||||
Interrogate {
|
||||
/// 6-digit PIN
|
||||
pin: String,
|
||||
},
|
||||
/// Alert the HSM to listen for another HSM
|
||||
Listen,
|
||||
/// Receive a file from another HSM
|
||||
Receive {
|
||||
/// 6-digit PIN
|
||||
pin: String,
|
||||
/// Read slot (0-7)
|
||||
read_slot: u8,
|
||||
/// Write slot (0-7)
|
||||
write_slot: u8,
|
||||
},
|
||||
}
|
||||
|
||||
fn parse_gid(s: &str) -> Result<u16> {
|
||||
let val = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
|
||||
u16::from_str_radix(hex, 16)?
|
||||
} else {
|
||||
s.parse()?
|
||||
};
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
fn validate_pin(pin: &str) -> Result<()> {
|
||||
if pin.len() != PIN_LEN {
|
||||
bail!("PIN must be exactly {PIN_LEN} characters, got {}", pin.len());
|
||||
}
|
||||
if !pin.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
bail!("PIN must contain only hex digits (0-9, a-f, A-F)");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_slot(slot: u8) -> Result<()> {
|
||||
if slot > 7 {
|
||||
bail!("Slot must be 0-7, got {slot}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unpack_files(body: &[u8]) -> Result<Vec<(u8, u16, String)>> {
|
||||
if body.len() < 4 {
|
||||
bail!("File list response too short");
|
||||
}
|
||||
let n_files = u32::from_le_bytes(body[0..4].try_into().unwrap()) as usize;
|
||||
log::debug(&format!("Reported {n_files} files"));
|
||||
let entries = &body[4..];
|
||||
let entry_size = 1 + 2 + MAX_NAME_LEN; // 35 bytes
|
||||
if entries.len() != n_files * entry_size {
|
||||
bail!(
|
||||
"Expected {} bytes for {n_files} files, got {}",
|
||||
n_files * entry_size,
|
||||
entries.len()
|
||||
);
|
||||
}
|
||||
let mut files = Vec::with_capacity(n_files);
|
||||
for i in 0..n_files {
|
||||
let off = i * entry_size;
|
||||
let slot = entries[off];
|
||||
let group_id = u16::from_le_bytes(entries[off + 1..off + 3].try_into().unwrap());
|
||||
let name_bytes = &entries[off + 3..off + 3 + MAX_NAME_LEN];
|
||||
let name = String::from_utf8_lossy(
|
||||
&name_bytes[..name_bytes.iter().position(|&b| b == 0).unwrap_or(MAX_NAME_LEN)],
|
||||
)
|
||||
.into_owned();
|
||||
files.push((slot, group_id, name));
|
||||
}
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
log::set_verbosity(cli.verbose);
|
||||
|
||||
if let Err(e) = run(cli) {
|
||||
let chain: Vec<String> = e.chain().map(|c| c.to_string()).collect();
|
||||
log::error(&chain[0]);
|
||||
for cause in &chain[1..] {
|
||||
log::error_cause(cause);
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run(cli: Cli) -> Result<()> {
|
||||
let mut hsm = HSMIntf::open(&cli.port).context("Failed to open serial port")?;
|
||||
|
||||
match cli.command {
|
||||
Command::Write {
|
||||
pin,
|
||||
slot,
|
||||
gid,
|
||||
file,
|
||||
uuid,
|
||||
} => {
|
||||
validate_pin(&pin)?;
|
||||
validate_slot(slot)?;
|
||||
let gid = parse_gid(&gid)?;
|
||||
|
||||
let uuid_bytes: [u8; UUID_LEN] = match uuid {
|
||||
Some(hex) => {
|
||||
let bytes = hex_decode(&hex).context("Invalid UUID hex")?;
|
||||
bytes
|
||||
.try_into()
|
||||
.map_err(|v: Vec<u8>| anyhow::anyhow!("UUID must be 16 bytes, got {}", v.len()))?
|
||||
}
|
||||
None => {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
*id.as_bytes()
|
||||
}
|
||||
};
|
||||
|
||||
let contents = fs::read(&file).context("Failed to read input file")?;
|
||||
if contents.len() > MAX_FILE_LEN {
|
||||
bail!(
|
||||
"File too large: {} bytes (max {MAX_FILE_LEN})",
|
||||
contents.len()
|
||||
);
|
||||
}
|
||||
|
||||
let filename = file
|
||||
.file_name()
|
||||
.context("No filename")?
|
||||
.to_str()
|
||||
.context("Filename not UTF-8")?;
|
||||
let mut name_buf = [0u8; MAX_NAME_LEN];
|
||||
let name_bytes = filename.as_bytes();
|
||||
if name_bytes.len() > MAX_NAME_LEN {
|
||||
bail!("Filename too long (max {MAX_NAME_LEN} bytes)");
|
||||
}
|
||||
name_buf[..name_bytes.len()].copy_from_slice(name_bytes);
|
||||
|
||||
// Pack frame: pin(6) + slot(1) + gid(2) + name(32) + uuid(16) + contents_len(2) + contents
|
||||
let mut frame = Vec::with_capacity(59 + contents.len());
|
||||
frame.extend_from_slice(pin.as_bytes());
|
||||
frame.push(slot);
|
||||
frame.extend_from_slice(&gid.to_le_bytes());
|
||||
frame.extend_from_slice(&name_buf);
|
||||
frame.extend_from_slice(&uuid_bytes);
|
||||
frame.extend_from_slice(&(contents.len() as u16).to_le_bytes());
|
||||
frame.extend_from_slice(&contents);
|
||||
|
||||
hsm.send_respond(Opcode::Write, &frame)?;
|
||||
log::success("Write successful");
|
||||
}
|
||||
|
||||
Command::Read {
|
||||
pin,
|
||||
slot,
|
||||
output_dir,
|
||||
force,
|
||||
} => {
|
||||
validate_pin(&pin)?;
|
||||
validate_slot(slot)?;
|
||||
|
||||
// Pack frame: pin(6) + slot(1)
|
||||
let mut frame = Vec::with_capacity(7);
|
||||
frame.extend_from_slice(pin.as_bytes());
|
||||
frame.push(slot);
|
||||
|
||||
let resp = hsm.send_respond(Opcode::Read, &frame)?;
|
||||
let body = &resp.body;
|
||||
if body.len() < MAX_NAME_LEN {
|
||||
bail!("Read response too short");
|
||||
}
|
||||
let name_bytes = &body[..MAX_NAME_LEN];
|
||||
let name = String::from_utf8_lossy(
|
||||
&name_bytes[..name_bytes
|
||||
.iter()
|
||||
.position(|&b| b == 0)
|
||||
.unwrap_or(MAX_NAME_LEN)],
|
||||
);
|
||||
let contents = &body[MAX_NAME_LEN..];
|
||||
|
||||
let full_path = output_dir.join(name.as_ref());
|
||||
if !force && full_path.exists() {
|
||||
bail!("File {} already exists (use --force to overwrite)", full_path.display());
|
||||
}
|
||||
fs::create_dir_all(&output_dir)?;
|
||||
fs::write(&full_path, contents)?;
|
||||
log::success(&format!(
|
||||
"Read successful. Wrote file to {}",
|
||||
full_path.canonicalize().unwrap_or(full_path).display()
|
||||
));
|
||||
}
|
||||
|
||||
Command::List { pin } => {
|
||||
validate_pin(&pin)?;
|
||||
let resp = hsm.send_respond(Opcode::List, pin.as_bytes())?;
|
||||
let files = unpack_files(&resp.body)?;
|
||||
for (slot, group_id, name) in &files {
|
||||
log::info(&format!("Found file: Slot {slot:x}, Group {group_id:x}, {name}"));
|
||||
}
|
||||
log::success("List successful");
|
||||
}
|
||||
|
||||
Command::Interrogate { pin } => {
|
||||
validate_pin(&pin)?;
|
||||
let resp = hsm.send_respond(Opcode::Interrogate, pin.as_bytes())?;
|
||||
let files = unpack_files(&resp.body)?;
|
||||
for (slot, group_id, name) in &files {
|
||||
log::info(&format!("Found remote file: Slot {slot:x}, Group {group_id:x}, {name}"));
|
||||
}
|
||||
log::success("Interrogate successful");
|
||||
}
|
||||
|
||||
Command::Listen => {
|
||||
hsm.send_respond(Opcode::Listen, &[])?;
|
||||
log::success("Listen successful");
|
||||
}
|
||||
|
||||
Command::Receive {
|
||||
pin,
|
||||
read_slot,
|
||||
write_slot,
|
||||
} => {
|
||||
validate_pin(&pin)?;
|
||||
validate_slot(read_slot)?;
|
||||
validate_slot(write_slot)?;
|
||||
|
||||
// Pack frame: pin(6) + read_slot(1) + write_slot(1)
|
||||
let mut frame = Vec::with_capacity(8);
|
||||
frame.extend_from_slice(pin.as_bytes());
|
||||
frame.push(read_slot);
|
||||
frame.push(write_slot);
|
||||
|
||||
hsm.send_respond(Opcode::Receive, &frame)?;
|
||||
log::success(&format!("Receive successful. Wrote file to local slot {write_slot}"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hex_decode(s: &str) -> Result<Vec<u8>> {
|
||||
if s.len() % 2 != 0 {
|
||||
bail!("Hex string must have even length");
|
||||
}
|
||||
(0..s.len())
|
||||
.step_by(2)
|
||||
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(Into::into))
|
||||
.collect()
|
||||
}
|
||||
272
src/protocol.rs
Normal file
272
src/protocol.rs
Normal file
@@ -0,0 +1,272 @@
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::os::fd::FromRawFd;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
use crate::log;
|
||||
|
||||
const MAGIC: u8 = 0x25; // '%'
|
||||
const BLOCK_LEN: usize = 256;
|
||||
const HDR_LEN: usize = 4;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum Opcode {
|
||||
List = 0x4C,
|
||||
Read = 0x52,
|
||||
Write = 0x57,
|
||||
Receive = 0x43,
|
||||
Interrogate = 0x49,
|
||||
Listen = 0x4E,
|
||||
Ack = 0x41,
|
||||
Debug = 0x44,
|
||||
Error = 0x45,
|
||||
}
|
||||
|
||||
impl Opcode {
|
||||
fn from_u8(b: u8) -> Result<Self> {
|
||||
match b {
|
||||
0x4C => Ok(Self::List),
|
||||
0x52 => Ok(Self::Read),
|
||||
0x57 => Ok(Self::Write),
|
||||
0x43 => Ok(Self::Receive),
|
||||
0x49 => Ok(Self::Interrogate),
|
||||
0x4E => Ok(Self::Listen),
|
||||
0x41 => Ok(Self::Ack),
|
||||
0x44 => Ok(Self::Debug),
|
||||
0x45 => Ok(Self::Error),
|
||||
_ => bail!("Unknown opcode: 0x{b:02X}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_ack(self) -> bool {
|
||||
!matches!(self, Self::Ack | Self::Debug)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Message {
|
||||
pub opcode: Opcode,
|
||||
pub body: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct HSMIntf {
|
||||
file: File,
|
||||
stream: Vec<u8>,
|
||||
}
|
||||
|
||||
impl HSMIntf {
|
||||
pub fn open(port: &str) -> Result<Self> {
|
||||
// Open with O_NONBLOCK to avoid blocking on carrier detect (macOS CDC-ACM).
|
||||
// Then clear O_NONBLOCK so subsequent reads are blocking.
|
||||
let c_port = std::ffi::CString::new(port)?;
|
||||
let fd = unsafe { libc::open(c_port.as_ptr(), libc::O_RDWR | libc::O_NOCTTY | libc::O_NONBLOCK) };
|
||||
if fd < 0 {
|
||||
bail!("Failed to open {port}: {}", io::Error::last_os_error());
|
||||
}
|
||||
|
||||
// Clear O_NONBLOCK now that we're past the open
|
||||
unsafe {
|
||||
let flags = libc::fcntl(fd, libc::F_GETFL);
|
||||
if flags < 0 || libc::fcntl(fd, libc::F_SETFL, flags & !libc::O_NONBLOCK) < 0 {
|
||||
libc::close(fd);
|
||||
bail!("Failed to clear O_NONBLOCK: {}", io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
|
||||
let file = unsafe { File::from_raw_fd(fd) };
|
||||
|
||||
// Configure termios for raw serial at 115200
|
||||
unsafe {
|
||||
let mut termios: libc::termios = std::mem::zeroed();
|
||||
if libc::tcgetattr(fd, &mut termios) != 0 {
|
||||
bail!("tcgetattr failed: {}", io::Error::last_os_error());
|
||||
}
|
||||
|
||||
// Input flags: disable all processing
|
||||
termios.c_iflag &= !(libc::IGNBRK
|
||||
| libc::BRKINT
|
||||
| libc::PARMRK
|
||||
| libc::ISTRIP
|
||||
| libc::INLCR
|
||||
| libc::IGNCR
|
||||
| libc::ICRNL
|
||||
| libc::IXON
|
||||
| libc::IXOFF
|
||||
| libc::IXANY);
|
||||
|
||||
// Output flags: disable all processing
|
||||
termios.c_oflag &= !libc::OPOST;
|
||||
|
||||
// Control flags: 8N1, no flow control
|
||||
termios.c_cflag &= !(libc::CSIZE | libc::PARENB | libc::CSTOPB | libc::CRTSCTS);
|
||||
termios.c_cflag |= libc::CS8 | libc::CLOCAL | libc::CREAD;
|
||||
|
||||
// Local flags: raw mode
|
||||
termios.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN);
|
||||
|
||||
// Blocking read: VMIN=1, VTIME=0
|
||||
termios.c_cc[libc::VMIN] = 1;
|
||||
termios.c_cc[libc::VTIME] = 0;
|
||||
|
||||
// Set baud rate to 115200
|
||||
libc::cfsetispeed(&mut termios, libc::B115200);
|
||||
libc::cfsetospeed(&mut termios, libc::B115200);
|
||||
|
||||
if libc::tcsetattr(fd, libc::TCSAFLUSH, &termios) != 0 {
|
||||
bail!("tcsetattr failed: {}", io::Error::last_os_error());
|
||||
}
|
||||
|
||||
// Flush input buffer (the MITRE tool doesn't do this — we should)
|
||||
libc::tcflush(fd, libc::TCIFLUSH);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
file,
|
||||
stream: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn read_byte(&mut self) -> Result<u8> {
|
||||
let mut buf = [0u8; 1];
|
||||
self.file.read_exact(&mut buf)?;
|
||||
log::trace(&format!("RX byte: {:02x}", buf[0]));
|
||||
Ok(buf[0])
|
||||
}
|
||||
|
||||
fn read_exact(&mut self, n: usize) -> Result<Vec<u8>> {
|
||||
let mut buf = vec![0u8; n];
|
||||
self.file.read_exact(&mut buf)?;
|
||||
log::trace_hex("RX", &buf);
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn write_all(&mut self, data: &[u8]) -> Result<()> {
|
||||
log::trace_hex("TX", data);
|
||||
self.file.write_all(data)?;
|
||||
self.file.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pack_header(opcode: Opcode, size: u16) -> [u8; HDR_LEN] {
|
||||
let size_bytes = size.to_le_bytes();
|
||||
[MAGIC, opcode as u8, size_bytes[0], size_bytes[1]]
|
||||
}
|
||||
|
||||
fn send_ack(&mut self) -> Result<()> {
|
||||
log::trace("TX ACK");
|
||||
let hdr = Self::pack_header(Opcode::Ack, 0);
|
||||
self.write_all(&hdr)
|
||||
}
|
||||
|
||||
fn get_ack(&mut self) -> Result<()> {
|
||||
let msg = self.get_msg()?;
|
||||
if msg.opcode != Opcode::Ack {
|
||||
bail!("Expected ACK, got {:?}", msg.opcode);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Try to parse a header from the internal stream buffer.
|
||||
/// Returns Some((opcode, size)) if found, consuming up through the header.
|
||||
fn try_parse_header(&mut self) -> Option<(Opcode, u16)> {
|
||||
if let Some(pos) = self.stream.iter().position(|&b| b == MAGIC) {
|
||||
// Need at least 3 more bytes after magic
|
||||
if pos + 4 <= self.stream.len() {
|
||||
let opc = self.stream[pos + 1];
|
||||
let size = u16::from_le_bytes([self.stream[pos + 2], self.stream[pos + 3]]);
|
||||
self.stream.drain(..pos + 4);
|
||||
if let Ok(opcode) = Opcode::from_u8(opc) {
|
||||
return Some((opcode, size));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Read a raw message from the device (may be DEBUG, ACK, ERROR, or data).
|
||||
fn get_raw_msg(&mut self) -> Result<Message> {
|
||||
// Read bytes until we can parse a header
|
||||
let (opcode, size) = loop {
|
||||
if let Some(hdr) = self.try_parse_header() {
|
||||
break hdr;
|
||||
}
|
||||
let b = self.read_byte()?;
|
||||
self.stream.push(b);
|
||||
};
|
||||
|
||||
log::debug(&format!("Found header: opcode={opcode:?}, size={size}"));
|
||||
|
||||
if opcode.needs_ack() {
|
||||
self.send_ack()?;
|
||||
}
|
||||
|
||||
// Read body in BLOCK_LEN chunks
|
||||
let mut body = Vec::with_capacity(size as usize);
|
||||
let mut remaining = size as usize;
|
||||
while remaining > 0 {
|
||||
let chunk_size = remaining.min(BLOCK_LEN);
|
||||
let chunk = self.read_exact(chunk_size)?;
|
||||
body.extend_from_slice(&chunk);
|
||||
remaining -= chunk_size;
|
||||
if opcode.needs_ack() {
|
||||
self.send_ack()?;
|
||||
}
|
||||
log::debug(&format!("Read block ({chunk_size} bytes)"));
|
||||
}
|
||||
|
||||
Ok(Message { opcode, body })
|
||||
}
|
||||
|
||||
/// Read a message, filtering out DEBUG and raising on ERROR.
|
||||
fn get_msg(&mut self) -> Result<Message> {
|
||||
loop {
|
||||
let msg = self.get_raw_msg()?;
|
||||
match msg.opcode {
|
||||
Opcode::Error => {
|
||||
let text = String::from_utf8_lossy(&msg.body);
|
||||
bail!("HSM error: {text}");
|
||||
}
|
||||
Opcode::Debug => {
|
||||
match std::str::from_utf8(&msg.body) {
|
||||
Ok(text) => log::hsm_debug(text.trim()),
|
||||
Err(_) => log::hsm_debug_hex(&msg.body),
|
||||
}
|
||||
continue;
|
||||
}
|
||||
_ => return Ok(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a message with ACK flow control (header + chunked body).
|
||||
fn send_msg(&mut self, opcode: Opcode, body: &[u8]) -> Result<()> {
|
||||
let hdr = Self::pack_header(opcode, body.len() as u16);
|
||||
log::debug(&format!("Sending header: opcode={opcode:?}, size={}", body.len()));
|
||||
|
||||
self.write_all(&hdr)?;
|
||||
self.get_ack()?;
|
||||
|
||||
for chunk in body.chunks(BLOCK_LEN) {
|
||||
log::debug(&format!("Sending chunk ({} bytes)", chunk.len()));
|
||||
self.write_all(chunk)?;
|
||||
self.get_ack()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a command and wait for the matching response.
|
||||
pub fn send_respond(&mut self, opcode: Opcode, body: &[u8]) -> Result<Message> {
|
||||
self.send_msg(opcode, body)?;
|
||||
let resp = self.get_msg()?;
|
||||
if resp.opcode != opcode {
|
||||
bail!(
|
||||
"Response opcode mismatch: expected {:?}, got {:?}",
|
||||
opcode,
|
||||
resp.opcode
|
||||
);
|
||||
}
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user