mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-07-24 00:08:10 -07:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66e5ccbf79 | |||
| 56d6a83412 | |||
| 5ad7f029ce | |||
| eb71bf9a00 | |||
| 7df322e5e0 | |||
| 144df3fde1 | |||
| 6b622eb208 | |||
| 6d870a7f7b | |||
| f3d694373f | |||
| 7e1cd0ce09 | |||
| 075ae08ace | |||
| 396cb58868 | |||
| 1635060295 | |||
| 00eed6d24b | |||
| bbaeebeacc | |||
| ff290a9d19 | |||
| 967e823805 | |||
| 4ecad7f66a | |||
| 17c301d8e2 | |||
| 2d4b36432a | |||
| 3412e86862 | |||
| 724512860d | |||
| 7c8d5ebafc | |||
| 30f663be2f | |||
| 64c59d5e08 | |||
| 71fde4dcdb | |||
| c30428b188 | |||
| ba17c9f9d9 | |||
| 6922646a5d | |||
| 5def0b388e | |||
| d834e1fc51 | |||
| d57b1c937c | |||
| a12b6116ad | |||
| 8fe516c2a6 | |||
| a3cf7ac1ee | |||
| e8d4a489de | |||
| 97e7e7c3d0 | |||
| 39c183d67f | |||
| cd28354bec | |||
| 1e4b812273 | |||
| 04cf2cbd67 | |||
| 9627cec737 | |||
| 10f560b5e4 | |||
| 88d2725427 | |||
| 76ae8fccd9 | |||
| 94b989c3c0 | |||
| f5a0cddc88 | |||
| 2702ee0828 | |||
| 58338850dd | |||
| d122ce6e6d | |||
| 9280067e31 | |||
| 3b3532d3fd | |||
| 30c4cb0e0c | |||
| 17a9dfe0ff | |||
| b3f63864ad | |||
| cfefa3c901 | |||
| ce821b825f | |||
| 772dac681e | |||
| 2df331a0bc | |||
| 0a1dce3215 | |||
| 2df31300e3 | |||
| fbd8110be9 |
@@ -44,11 +44,12 @@ jobs:
|
||||
|
||||
# We rebuild everything if any of these conditions hold:
|
||||
# * We are on main
|
||||
# * The run was triggered by a tag
|
||||
# * Changes are made to github workflows
|
||||
# * A cargo-workspace file changed (lockfile or .cargo), as that could affect any crate anywhere
|
||||
# * Something from the script or dist folder changed (could be gated to installer, but some scripts like build_wpa_supplicant are part of the build process)
|
||||
# * #build-all was used by the user to explicitly ask for this
|
||||
if [ ${GITHUB_REF} = 'refs/heads/main' ] || git diff --name-only $lcommit..HEAD | grep -qe ^.github/workflows/ -e ^.cargo -e '^Cargo\.lock$' -e '^Cargo\.toml$' -e ^dist/ -e ^scripts/ || git log -1 --format='%s %b' | grep -qF '#build-all'
|
||||
if [ ${GITHUB_REF} = 'refs/heads/main' ] || [ ${GITHUB_REF_TYPE} = 'tag' ] || git diff --name-only $lcommit..HEAD | grep -qe ^.github/workflows/ -e ^.cargo -e '^Cargo\.lock$' -e '^Cargo\.toml$' -e ^dist/ -e ^scripts/ || git log -1 --format='%s %b' | grep -qF '#build-all'
|
||||
then
|
||||
echo "building everything"
|
||||
echo code_count=forced >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# To use: navigate on Github to Actions, select "Release rayhunter" on the left, click "Run workflow" > "Run workflow" on the right.
|
||||
# https://github.com/EFForg/rayhunter/actions/workflows/release.yml
|
||||
# To learn how to use this workflow, please read CONTRIBUTING.md.
|
||||
name: Release rayhunter
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
+4
-6
@@ -75,11 +75,9 @@ You can read our [full policy](https://www.eff.org/about/opportunities/volunteer
|
||||
This one is for maintainers of Rayhunter.
|
||||
|
||||
1. Make a PR changing the versions in `Cargo.toml` and other files.
|
||||
This could be automated better but right now it's manual. You can do this easily with sed:
|
||||
`sed -i "" -E 's/x.x.x/y.y.y/g' */Cargo.toml installer-gui/src-tauri/Cargo.toml`
|
||||
This can be done by running `scripts/set-versions.sh VERSION_NUM`.
|
||||
|
||||
2. Merge PR and make a tag.
|
||||
2. Merge the PR, make a tag, and push the tag to GitHub. Pushing the tag should
|
||||
trigger the [release workflow](https://github.com/EFForg/rayhunter/actions/workflows/release.yml).
|
||||
|
||||
3. [Run release workflow.](https://github.com/EFForg/rayhunter/actions/workflows/release.yml)
|
||||
|
||||
4. Write changelog, edit it into the release, announce on mattermost.
|
||||
3. Write changelog, edit it into the release, announce on mattermost.
|
||||
|
||||
Generated
+103
-43
@@ -276,13 +276,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.33"
|
||||
version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93c1f86859c1af3d514fa19e8323147ff10ea98684e6c7b307912509f50e67b2"
|
||||
checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac"
|
||||
dependencies = [
|
||||
"compression-codecs",
|
||||
"compression-core",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
@@ -718,6 +717,15 @@ dependencies = [
|
||||
"alloc-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bs58"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "built"
|
||||
version = "0.7.7"
|
||||
@@ -1007,9 +1015,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "compression-codecs"
|
||||
version = "0.4.32"
|
||||
version = "0.4.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "680dc087785c5230f8e8843e2e57ac7c1c90488b6a91b88caa265410568f441b"
|
||||
checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf"
|
||||
dependencies = [
|
||||
"compression-core",
|
||||
"flate2",
|
||||
@@ -1323,6 +1331,16 @@ dependencies = [
|
||||
"darling_macro 0.21.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
||||
dependencies = [
|
||||
"darling_core 0.23.0",
|
||||
"darling_macro 0.23.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.11"
|
||||
@@ -1351,6 +1369,19 @@ dependencies = [
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
|
||||
dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.11"
|
||||
@@ -1373,6 +1404,17 @@ dependencies = [
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
||||
dependencies = [
|
||||
"darling_core 0.23.0",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbus"
|
||||
version = "0.9.11"
|
||||
@@ -1906,9 +1948,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.1"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
@@ -1997,9 +2039,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -2012,9 +2054,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -2022,15 +2064,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
@@ -2039,9 +2081,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
@@ -2058,9 +2100,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2069,21 +2111,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -2093,7 +2135,6 @@ dependencies = [
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
@@ -2670,7 +2711,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.61.2",
|
||||
"windows-core 0.57.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2947,7 +2988,9 @@ name = "installer-gui"
|
||||
version = "0.11.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"installer",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
@@ -4356,12 +4399,6 @@ version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.4"
|
||||
@@ -4856,6 +4893,7 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
name = "rayhunter"
|
||||
version = "0.11.2"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crc",
|
||||
@@ -5482,14 +5520,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.140"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5545,19 +5584,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.14.1"
|
||||
version = "3.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e"
|
||||
checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bs58",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.12.1",
|
||||
"schemars 0.9.0",
|
||||
"schemars 1.0.4",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
"time",
|
||||
@@ -5565,11 +5604,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.14.1"
|
||||
version = "3.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e"
|
||||
checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660"
|
||||
dependencies = [
|
||||
"darling 0.21.3",
|
||||
"darling 0.23.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
@@ -6394,6 +6433,21 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.45.0"
|
||||
@@ -7935,6 +7989,12 @@ dependencies = [
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.4.12"
|
||||
|
||||
+24
-37
@@ -1,15 +1,13 @@
|
||||
use clap::Parser;
|
||||
use futures::TryStreamExt;
|
||||
use log::{debug, error, info, warn};
|
||||
use pcap_file_tokio::pcapng::{Block, PcapNgReader};
|
||||
use rayhunter::{
|
||||
analysis::analyzer::{AnalysisRow, AnalyzerConfig, EventType, Harness},
|
||||
diag::DataType,
|
||||
gsmtap_parser,
|
||||
gsmtap::parser as gsmtap_parser,
|
||||
pcap::GsmtapPcapWriter,
|
||||
qmdl::QmdlReader,
|
||||
qmdl::QmdlMessageReader,
|
||||
};
|
||||
use std::{collections::HashMap, future, path::PathBuf, pin::pin};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use tokio::fs::File;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
@@ -113,26 +111,16 @@ async fn analyze_pcap(pcap_path: &str, show_skipped: bool) {
|
||||
async fn analyze_qmdl(qmdl_path: &str, show_skipped: bool) {
|
||||
let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
|
||||
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open file");
|
||||
let file_size = qmdl_file
|
||||
.metadata()
|
||||
let mut qmdl_reader = QmdlMessageReader::new(qmdl_file)
|
||||
.await
|
||||
.expect("failed to get QMDL file metadata")
|
||||
.len();
|
||||
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
|
||||
let mut qmdl_stream = pin!(
|
||||
qmdl_reader
|
||||
.as_stream()
|
||||
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace))
|
||||
);
|
||||
.expect("failed to open QmdlReader");
|
||||
let mut report = Report::new(qmdl_path);
|
||||
while let Some(container) = qmdl_stream
|
||||
.try_next()
|
||||
while let Some(maybe_message) = qmdl_reader
|
||||
.get_next_message()
|
||||
.await
|
||||
.expect("failed getting QMDL container")
|
||||
.expect("failed to get message")
|
||||
{
|
||||
for row in harness.analyze_qmdl_messages(container) {
|
||||
report.process_row(row);
|
||||
}
|
||||
report.process_row(harness.analyze_qmdl_message(maybe_message));
|
||||
}
|
||||
report.print_summary(show_skipped);
|
||||
}
|
||||
@@ -141,8 +129,9 @@ async fn pcapify(qmdl_path: &PathBuf) {
|
||||
let qmdl_file = &mut File::open(&qmdl_path)
|
||||
.await
|
||||
.expect("failed to open qmdl file");
|
||||
let qmdl_file_size = qmdl_file.metadata().await.unwrap().len();
|
||||
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(qmdl_file_size as usize));
|
||||
let mut qmdl_reader = QmdlMessageReader::new(qmdl_file)
|
||||
.await
|
||||
.expect("failed to open QmdlReader");
|
||||
let mut pcap_path = qmdl_path.clone();
|
||||
pcap_path.set_extension("pcapng");
|
||||
let pcap_file = &mut File::create(&pcap_path)
|
||||
@@ -150,21 +139,21 @@ async fn pcapify(qmdl_path: &PathBuf) {
|
||||
.expect("failed to open pcap file");
|
||||
let mut pcap_writer = GsmtapPcapWriter::new(pcap_file).await.unwrap();
|
||||
pcap_writer.write_iface_header().await.unwrap();
|
||||
while let Some(container) = qmdl_reader
|
||||
.get_next_messages_container()
|
||||
while let Some(maybe_message) = qmdl_reader
|
||||
.get_next_message()
|
||||
.await
|
||||
.expect("failed to get container")
|
||||
.expect("failed to get message")
|
||||
{
|
||||
for msg in container.messages().into_iter().flatten() {
|
||||
if let Ok(Some((timestamp, parsed))) = gsmtap_parser::parse(msg) {
|
||||
pcap_writer
|
||||
.write_gsmtap_message(parsed, timestamp, None)
|
||||
.await
|
||||
.expect("failed to write");
|
||||
}
|
||||
if let Ok(msg) = maybe_message
|
||||
&& let Ok(Some((timestamp, parsed))) = gsmtap_parser::parse(msg)
|
||||
{
|
||||
pcap_writer
|
||||
.write_gsmtap_message(parsed, timestamp, None)
|
||||
.await
|
||||
.expect("failed to write");
|
||||
}
|
||||
}
|
||||
info!("wrote pcap to {:?}", &pcap_path);
|
||||
info!("wrote pcap to {:?}", pcap_path);
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -197,9 +186,7 @@ async fn main() {
|
||||
let name_str = name.to_str().unwrap();
|
||||
let path = entry.path();
|
||||
let path_str = path.to_str().unwrap();
|
||||
// instead of relying on the QMDL extension, can we check if a file is
|
||||
// QMDL by inspecting the contents?
|
||||
if name_str.ends_with(".qmdl") {
|
||||
if name_str.ends_with(".qmdl") || name_str.ends_with(".qmdl.gz") {
|
||||
info!("**** Beginning analysis of {name_str}");
|
||||
analyze_qmdl(path_str, args.show_skipped).await;
|
||||
if args.pcapify {
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ futures-macro = "0.3.30"
|
||||
include_dir = "0.7.3"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
tokio-stream = { version = "0.1.14", default-features = false, features = ["io-util"] }
|
||||
futures = { version = "0.3.30", default-features = false }
|
||||
futures = { version = "0.3.32", default-features = false, features = ["std"] }
|
||||
serde_json = "1.0.114"
|
||||
image = { version = "0.25.1", default-features = false, features = ["png", "gif"] }
|
||||
tempfile = "3.10.2"
|
||||
|
||||
+24
-22
@@ -1,16 +1,15 @@
|
||||
use std::cmp;
|
||||
use std::sync::Arc;
|
||||
use std::{cmp, future, pin};
|
||||
|
||||
use axum::Json;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use futures::TryStreamExt;
|
||||
use log::{error, info};
|
||||
use rayhunter::analysis::analyzer::{AnalyzerConfig, EventType, Harness};
|
||||
use rayhunter::diag::{DataType, MessagesContainer};
|
||||
use rayhunter::qmdl::QmdlReader;
|
||||
use rayhunter::diag::{DiagParsingError, Message, MessagesContainer};
|
||||
use rayhunter::qmdl::QmdlMessageReader;
|
||||
use serde::Serialize;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncWriteExt, BufWriter};
|
||||
@@ -47,7 +46,7 @@ impl AnalysisWriter {
|
||||
|
||||
// Runs the analysis harness on the given container, serializing the results
|
||||
// to the analysis file, returning the whether any warnings were detected
|
||||
pub async fn analyze(
|
||||
pub async fn analyze_container(
|
||||
&mut self,
|
||||
container: MessagesContainer,
|
||||
) -> Result<EventType, std::io::Error> {
|
||||
@@ -62,6 +61,17 @@ impl AnalysisWriter {
|
||||
Ok(max_type)
|
||||
}
|
||||
|
||||
pub async fn analyze_message(
|
||||
&mut self,
|
||||
maybe_qmdl_msg: Result<Message, DiagParsingError>,
|
||||
) -> Result<EventType, std::io::Error> {
|
||||
let row = self.harness.analyze_qmdl_message(maybe_qmdl_msg);
|
||||
if !row.is_empty() {
|
||||
self.write(&row).await?;
|
||||
}
|
||||
Ok(row.get_max_event_type())
|
||||
}
|
||||
|
||||
async fn write<T: Serialize>(&mut self, value: &T) -> Result<(), std::io::Error> {
|
||||
let mut value_str = serde_json::to_string(value).unwrap();
|
||||
value_str.push('\n');
|
||||
@@ -135,7 +145,7 @@ async fn perform_analysis(
|
||||
analyzer_config: &AnalyzerConfig,
|
||||
) -> Result<(), String> {
|
||||
info!("Opening QMDL and analysis file for {name}...");
|
||||
let (analysis_file, qmdl_file) = {
|
||||
let (analysis_file, mut qmdl_reader) = {
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
let (entry_index, _) = qmdl_store
|
||||
.entry_for_name(name)
|
||||
@@ -149,33 +159,25 @@ async fn perform_analysis(
|
||||
.await
|
||||
.map_err(|e| format!("{e:?}"))?
|
||||
.ok_or("QMDL file not found")?;
|
||||
let qmdl_reader = QmdlMessageReader::new(qmdl_file)
|
||||
.await
|
||||
.map_err(|e| format!("{e:?}"))?;
|
||||
|
||||
(analysis_file, qmdl_file)
|
||||
(analysis_file, qmdl_reader)
|
||||
};
|
||||
|
||||
let mut analysis_writer = AnalysisWriter::new(analysis_file, analyzer_config)
|
||||
.await
|
||||
.map_err(|e| format!("{e:?}"))?;
|
||||
let file_size = qmdl_file
|
||||
.metadata()
|
||||
.await
|
||||
.expect("failed to get QMDL file metadata")
|
||||
.len();
|
||||
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
|
||||
let mut qmdl_stream = pin::pin!(
|
||||
qmdl_reader
|
||||
.as_stream()
|
||||
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace))
|
||||
);
|
||||
|
||||
info!("Starting analysis for {name}...");
|
||||
while let Some(container) = qmdl_stream
|
||||
.try_next()
|
||||
while let Some(maybe_message) = qmdl_reader
|
||||
.get_next_message()
|
||||
.await
|
||||
.expect("failed getting QMDL container")
|
||||
.expect("failed to get message")
|
||||
{
|
||||
let _ = analysis_writer
|
||||
.analyze(container)
|
||||
.analyze_message(maybe_message)
|
||||
.await
|
||||
.map_err(|e| format!("{e:?}"))?;
|
||||
}
|
||||
|
||||
+40
-31
@@ -74,7 +74,7 @@ pub struct DiagTask {
|
||||
|
||||
enum DiagState {
|
||||
Recording {
|
||||
qmdl_writer: QmdlWriter<File>,
|
||||
qmdl_writer: Box<QmdlWriter<File>>,
|
||||
analysis_writer: Box<AnalysisWriter>,
|
||||
},
|
||||
Stopped,
|
||||
@@ -158,7 +158,7 @@ impl DiagTask {
|
||||
DiskSpaceCheck::Failed => {}
|
||||
}
|
||||
|
||||
let (qmdl_file, analysis_file) = qmdl_store.new_entry(self.gps_mode).await?;
|
||||
let (qmdl_gz_file, analysis_file) = qmdl_store.new_entry(self.gps_mode).await?;
|
||||
|
||||
// For fixed-mode sessions, write the configured coordinates to the storage
|
||||
// immediately so the per-session GPS is stored durably and isn't affected
|
||||
@@ -185,13 +185,11 @@ impl DiagTask {
|
||||
.await
|
||||
.map_err(RecordingStoreError::WriteFileError)?;
|
||||
}
|
||||
|
||||
self.stop_current_recording().await;
|
||||
let qmdl_writer = QmdlWriter::new(qmdl_file);
|
||||
self.stop_current_recording(qmdl_store).await;
|
||||
let qmdl_writer = Box::new(QmdlWriter::new(qmdl_gz_file));
|
||||
let analysis_writer = AnalysisWriter::new(analysis_file, &self.analyzer_config)
|
||||
.await
|
||||
.map_err(RecordingStoreError::WriteFileError)?;
|
||||
|
||||
self.state = DiagState::Recording {
|
||||
qmdl_writer,
|
||||
analysis_writer: Box::new(analysis_writer),
|
||||
@@ -209,7 +207,7 @@ impl DiagTask {
|
||||
|
||||
/// Stop recording, optionally annotating the entry with a reason.
|
||||
async fn stop(&mut self, qmdl_store: &mut RecordingStore, reason: Option<String>) {
|
||||
self.stop_current_recording().await;
|
||||
self.stop_current_recording(qmdl_store).await;
|
||||
if let Some(reason) = reason
|
||||
&& let Err(e) = qmdl_store.set_current_stop_reason(reason).await
|
||||
{
|
||||
@@ -296,17 +294,31 @@ impl DiagTask {
|
||||
}
|
||||
}
|
||||
|
||||
async fn stop_current_recording(&mut self) {
|
||||
async fn stop_current_recording(&mut self, qmdl_store: &mut RecordingStore) {
|
||||
let mut state = DiagState::Stopped;
|
||||
std::mem::swap(&mut self.state, &mut state);
|
||||
if let DiagState::Recording {
|
||||
analysis_writer, ..
|
||||
qmdl_writer,
|
||||
analysis_writer,
|
||||
..
|
||||
} = state
|
||||
{
|
||||
analysis_writer
|
||||
.close()
|
||||
.await
|
||||
.expect("failed to close analysis writer");
|
||||
match (qmdl_writer.close().await, analysis_writer.close().await) {
|
||||
(Ok(size), Ok(())) => {
|
||||
if let Err(err) = qmdl_store.update_current_entry_qmdl_size(size).await {
|
||||
error!("failed to update QMDL entry size while closing it: {err:?}");
|
||||
}
|
||||
}
|
||||
(qmdl_result, analysis_result) => {
|
||||
if let Err(err) = qmdl_result {
|
||||
error!("failed to close QmdlWriter: {err:?}");
|
||||
}
|
||||
if let Err(err) = analysis_result {
|
||||
error!("failed to close AnalysisWriter: {err:?}");
|
||||
}
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,23 +386,19 @@ impl DiagTask {
|
||||
self.stop(qmdl_store, Some(reason)).await;
|
||||
return;
|
||||
}
|
||||
debug!(
|
||||
"total QMDL bytes written: {}, updating manifest...",
|
||||
qmdl_writer.total_written
|
||||
);
|
||||
let index = qmdl_store
|
||||
.current_entry
|
||||
.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???");
|
||||
if let Err(e) = qmdl_store
|
||||
.update_entry_qmdl_size(index, qmdl_writer.total_written)
|
||||
.await
|
||||
{
|
||||
let reason = format!("failed to update manifest (disk full?): {e}");
|
||||
error!("{reason}");
|
||||
self.stop(qmdl_store, Some(reason)).await;
|
||||
return;
|
||||
if let Ok(file_size) = qmdl_writer.size().await {
|
||||
debug!(
|
||||
"total QMDL bytes written: {}, updating manifest...",
|
||||
file_size
|
||||
);
|
||||
if let Err(e) = qmdl_store.update_current_entry_qmdl_size(file_size).await {
|
||||
let reason = format!("failed to update manifest (disk full?): {e}");
|
||||
error!("{reason}");
|
||||
self.stop(qmdl_store, Some(reason)).await;
|
||||
return;
|
||||
}
|
||||
debug!("done!");
|
||||
}
|
||||
debug!("done!");
|
||||
|
||||
// Extract the latest packet timestamp from this container
|
||||
if let Some(ts) = container
|
||||
@@ -407,7 +415,7 @@ impl DiagTask {
|
||||
|
||||
let container_bytes: usize = container.messages.iter().map(|m| m.data.len()).sum();
|
||||
self.bytes_since_space_check += container_bytes;
|
||||
let max_type = match analysis_writer.analyze(container).await {
|
||||
let max_type = match analysis_writer.analyze_container(container).await {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
warn!("failed to analyze container: {e}");
|
||||
@@ -501,7 +509,8 @@ pub fn run_diag_read_thread(
|
||||
// time to go
|
||||
Some(DiagDeviceCtrlMessage::Exit) | None => {
|
||||
info!("Diag reader thread exiting...");
|
||||
diag_task.stop_current_recording().await;
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
diag_task.stop_current_recording(qmdl_store.deref_mut()).await;
|
||||
return Ok(())
|
||||
},
|
||||
Some(DiagDeviceCtrlMessage::DeleteEntry { name, response_tx }) => {
|
||||
|
||||
@@ -7,10 +7,18 @@ use crate::config;
|
||||
use crate::display::DisplayState;
|
||||
|
||||
pub fn update_ui(
|
||||
_task_tracker: &TaskTracker,
|
||||
task_tracker: &TaskTracker,
|
||||
_config: &config::Config,
|
||||
_shutdown_token: CancellationToken,
|
||||
_ui_update_rx: Receiver<DisplayState>,
|
||||
shutdown_token: CancellationToken,
|
||||
mut ui_update_rx: Receiver<DisplayState>,
|
||||
) {
|
||||
info!("Headless mode, not spawning UI.");
|
||||
task_tracker.spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = shutdown_token.cancelled() => break,
|
||||
_ = ui_update_rx.recv() => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+20
-27
@@ -10,12 +10,11 @@ use axum::http::StatusCode;
|
||||
use axum::http::header::CONTENT_TYPE;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use log::error;
|
||||
use rayhunter::diag::DataType;
|
||||
use rayhunter::gsmtap_parser;
|
||||
use rayhunter::gsmtap::parser as gsmtap_parser;
|
||||
use rayhunter::pcap::{GpsPoint, GsmtapPcapWriter};
|
||||
use rayhunter::qmdl::QmdlReader;
|
||||
use rayhunter::qmdl::QmdlMessageReader;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, duplex};
|
||||
use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, duplex};
|
||||
use tokio_util::io::ReaderStream;
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
@@ -51,18 +50,20 @@ pub async fn get_pcap(
|
||||
"QMDL file is empty, try again in a bit!".to_string(),
|
||||
));
|
||||
}
|
||||
let qmdl_size_bytes = entry.qmdl_size_bytes;
|
||||
let qmdl_file = qmdl_store
|
||||
.open_file(entry_index, FileKind::Qmdl)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "QMDL file not found".to_string()))?;
|
||||
let qmdl_reader = QmdlMessageReader::new(qmdl_file)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))?;
|
||||
let (reader, writer) = duplex(1024);
|
||||
let gps_records = load_gps_records_for_entry(&state, entry_index).await;
|
||||
drop(qmdl_store);
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = generate_pcap_data(writer, qmdl_file, qmdl_size_bytes, gps_records).await {
|
||||
if let Err(e) = generate_pcap_data(writer, qmdl_reader, gps_records).await {
|
||||
error!("failed to generate PCAP: {e:?}");
|
||||
}
|
||||
});
|
||||
@@ -131,37 +132,29 @@ fn find_nearest_gps(records: &[GpsRecord], packet_timestamp: i64) -> Option<GpsP
|
||||
|
||||
pub async fn generate_pcap_data<R, W>(
|
||||
writer: W,
|
||||
qmdl_file: R,
|
||||
qmdl_size_bytes: usize,
|
||||
mut reader: QmdlMessageReader<R>,
|
||||
gps_records: Vec<GpsRecord>,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
W: AsyncWrite + Unpin + Send,
|
||||
R: AsyncRead + Unpin,
|
||||
R: AsyncRead + AsyncSeek + Unpin,
|
||||
{
|
||||
let mut pcap_writer = GsmtapPcapWriter::new(writer).await?;
|
||||
pcap_writer.write_iface_header().await?;
|
||||
|
||||
let mut reader = QmdlReader::new(qmdl_file, Some(qmdl_size_bytes));
|
||||
while let Some(container) = reader.get_next_messages_container().await? {
|
||||
if container.data_type != DataType::UserSpace {
|
||||
continue;
|
||||
}
|
||||
|
||||
for maybe_msg in container.messages() {
|
||||
match maybe_msg {
|
||||
Ok(msg) => {
|
||||
let maybe_gsmtap_msg = gsmtap_parser::parse(msg)?;
|
||||
if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg {
|
||||
let packet_unix_ts = timestamp.to_datetime().timestamp();
|
||||
let gps = find_nearest_gps(&gps_records, packet_unix_ts);
|
||||
pcap_writer
|
||||
.write_gsmtap_message(gsmtap_msg, timestamp, gps.as_ref())
|
||||
.await?;
|
||||
}
|
||||
while let Some(maybe_msg) = reader.get_next_message().await? {
|
||||
match maybe_msg {
|
||||
Ok(msg) => {
|
||||
let maybe_gsmtap_msg = gsmtap_parser::parse(msg)?;
|
||||
if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg {
|
||||
let packet_unix_ts = timestamp.to_datetime().timestamp();
|
||||
let gps = find_nearest_gps(&gps_records, packet_unix_ts);
|
||||
pcap_writer
|
||||
.write_gsmtap_message(gsmtap_msg, timestamp, gps.as_ref())
|
||||
.await?;
|
||||
}
|
||||
Err(e) => error!("error parsing message: {e:?}"),
|
||||
}
|
||||
Err(e) => error!("error parsing message: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+39
-25
@@ -55,16 +55,24 @@ impl FileKind {
|
||||
// List of all possible physical files on disk.
|
||||
pub const ALL: &'static [FileKind] = &[FileKind::Qmdl, FileKind::Analysis, FileKind::Gps];
|
||||
|
||||
pub fn get_filename(&self, entry_name: &str) -> String {
|
||||
pub fn get_filename(&self, entry_name: &str, qmdl_compressed: bool) -> String {
|
||||
match self {
|
||||
FileKind::Qmdl if qmdl_compressed => format!("{}.qmdl.gz", entry_name),
|
||||
FileKind::Qmdl => format!("{}.qmdl", entry_name),
|
||||
FileKind::Analysis => format!("{}.ndjson", entry_name),
|
||||
FileKind::Gps => format!("{}-gps.ndjson", entry_name),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_filepath<P: AsRef<Path>>(&self, entry_name: &str, base_path: P) -> PathBuf {
|
||||
base_path.as_ref().join(self.get_filename(entry_name))
|
||||
pub fn get_filepath<P: AsRef<Path>>(
|
||||
&self,
|
||||
entry_name: &str,
|
||||
base_path: P,
|
||||
qmdl_compressed: bool,
|
||||
) -> PathBuf {
|
||||
base_path
|
||||
.as_ref()
|
||||
.join(self.get_filename(entry_name, qmdl_compressed))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +109,6 @@ pub struct ManifestEntry {
|
||||
/// The system time when the last message was recorded to the file
|
||||
#[cfg_attr(feature = "apidocs", schema(value_type = String))]
|
||||
pub last_message_time: Option<DateTime<Local>>,
|
||||
/// The size of the QMDL file in bytes
|
||||
pub qmdl_size_bytes: usize,
|
||||
/// The rayhunter daemon version which generated the file
|
||||
pub rayhunter_version: Option<String>,
|
||||
@@ -116,6 +123,8 @@ pub struct ManifestEntry {
|
||||
pub upload_time: Option<DateTime<Local>>,
|
||||
#[serde(default)]
|
||||
pub gps_mode: Option<GpsMode>,
|
||||
#[serde(default)]
|
||||
pub compressed: bool,
|
||||
}
|
||||
|
||||
impl ManifestEntry {
|
||||
@@ -133,11 +142,12 @@ impl ManifestEntry {
|
||||
stop_reason: None,
|
||||
upload_time: None,
|
||||
gps_mode: Some(gps_mode),
|
||||
compressed: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_filepath<P: AsRef<Path>>(&self, file_kind: FileKind, path: P) -> PathBuf {
|
||||
file_kind.get_filepath(&self.name, path)
|
||||
file_kind.get_filepath(&self.name, path, self.compressed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,8 +206,9 @@ impl RecordingStore {
|
||||
}
|
||||
|
||||
// Does a best-effort attempt to recover the manifest from a directory of
|
||||
// QMDL files. We expect these files to be named like "<timestamp>.qmdl",
|
||||
// and skip any files which don't match that pattern.
|
||||
// QMDL files. We expect these files to be named like "<timestamp>.qmdl"
|
||||
// or "<timestamp>.qmdl.gz", and skip any files which don't match that
|
||||
// pattern.
|
||||
pub async fn recover<P>(path: P) -> Result<Self, RecordingStoreError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
@@ -217,11 +228,14 @@ impl RecordingStore {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !filename.ends_with(".qmdl") {
|
||||
let (stem, compressed) = if filename.ends_with(".qmdl") {
|
||||
(filename.trim_end_matches(".qmdl"), false)
|
||||
} else if filename.ends_with(".qmdl.gz") {
|
||||
(filename.trim_end_matches(".qmdl.gz"), true)
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let stem = filename.trim_end_matches(".qmdl");
|
||||
let Ok(start_timestamp) = stem.parse::<i64>() else {
|
||||
warn!("QMDL file has invalid name {os_filename:?}, skipping");
|
||||
continue;
|
||||
@@ -248,6 +262,7 @@ impl RecordingStore {
|
||||
info!("successfully recovered QMDL entry {os_filename:?}!");
|
||||
manifest_entries.push(ManifestEntry {
|
||||
name: stem.to_string(),
|
||||
compressed,
|
||||
start_time: start_time.into(),
|
||||
last_message_time: Some(last_message_time.into()),
|
||||
qmdl_size_bytes: metadata.size() as usize,
|
||||
@@ -322,7 +337,7 @@ impl RecordingStore {
|
||||
file_kind: FileKind,
|
||||
) -> Result<Option<File>, RecordingStoreError> {
|
||||
let entry = &self.manifest.entries[entry_index];
|
||||
let filepath = file_kind.get_filepath(&entry.name, &self.path);
|
||||
let filepath = file_kind.get_filepath(&entry.name, &self.path, entry.compressed);
|
||||
|
||||
match File::open(&filepath).await {
|
||||
Ok(file) => Ok(Some(file)),
|
||||
@@ -373,12 +388,14 @@ impl RecordingStore {
|
||||
}
|
||||
}
|
||||
|
||||
// Sets the given entry's size and updates the last_message_time to now, updating the manifest
|
||||
pub async fn update_entry_qmdl_size(
|
||||
// Sets the current entry's size and updates the last_message_time to now, updating the manifest
|
||||
pub async fn update_current_entry_qmdl_size(
|
||||
&mut self,
|
||||
entry_index: usize,
|
||||
size_bytes: usize,
|
||||
) -> Result<(), RecordingStoreError> {
|
||||
let Some(entry_index) = self.current_entry else {
|
||||
return Err(RecordingStoreError::NoCurrentEntry);
|
||||
};
|
||||
self.manifest.entries[entry_index].qmdl_size_bytes = size_bytes;
|
||||
self.manifest.entries[entry_index].last_message_time =
|
||||
Some(rayhunter::clock::get_adjusted_now());
|
||||
@@ -496,7 +513,11 @@ impl RecordingStore {
|
||||
self.write_manifest().await?;
|
||||
|
||||
for &file_kind in FileKind::ALL {
|
||||
let filepath = file_kind.get_filepath(&entry_to_delete.name, &self.path);
|
||||
let filepath = file_kind.get_filepath(
|
||||
&entry_to_delete.name,
|
||||
&self.path,
|
||||
entry_to_delete.compressed,
|
||||
);
|
||||
remove_file_if_exists(&filepath)
|
||||
.await
|
||||
.map_err(RecordingStoreError::DeleteFileError)?;
|
||||
@@ -513,7 +534,7 @@ impl RecordingStore {
|
||||
|
||||
'entries: for entry in &self.manifest.entries {
|
||||
for &file_kind in FileKind::ALL {
|
||||
let filepath = file_kind.get_filepath(&entry.name, &self.path);
|
||||
let filepath = file_kind.get_filepath(&entry.name, &self.path, entry.compressed);
|
||||
if let Err(e) = remove_file_if_exists(&filepath).await {
|
||||
log::warn!("failed to remove {filepath:?}: {e:?}");
|
||||
// Some error happened with deleting this entry, abort and go to the next one.
|
||||
@@ -575,10 +596,7 @@ mod tests {
|
||||
.is_none()
|
||||
);
|
||||
|
||||
store
|
||||
.update_entry_qmdl_size(entry_index, 1000)
|
||||
.await
|
||||
.unwrap();
|
||||
store.update_current_entry_qmdl_size(1000).await.unwrap();
|
||||
let (entry_index, entry) = store
|
||||
.entry_for_name(&store.manifest.entries[entry_index].name)
|
||||
.unwrap();
|
||||
@@ -601,11 +619,7 @@ mod tests {
|
||||
let dir = make_temp_dir();
|
||||
let mut store = RecordingStore::create(dir.path()).await.unwrap();
|
||||
let _ = store.new_entry(GpsMode::Disabled).await.unwrap();
|
||||
let entry_index = store.current_entry.unwrap();
|
||||
store
|
||||
.update_entry_qmdl_size(entry_index, 1000)
|
||||
.await
|
||||
.unwrap();
|
||||
store.update_current_entry_qmdl_size(1000).await.unwrap();
|
||||
let store = RecordingStore::create(dir.path()).await.unwrap();
|
||||
assert_eq!(store.manifest.entries.len(), 0);
|
||||
}
|
||||
|
||||
+96
-46
@@ -6,17 +6,22 @@ use axum::Json;
|
||||
use axum::body::Body;
|
||||
use axum::extract::Path;
|
||||
use axum::extract::State;
|
||||
use axum::http::header::{self, CONTENT_LENGTH, CONTENT_TYPE};
|
||||
use axum::http::header::{self, CONTENT_TYPE};
|
||||
use axum::http::{HeaderValue, StatusCode};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use chrono::{DateTime, Local};
|
||||
use futures::TryStreamExt;
|
||||
use log::{error, warn};
|
||||
use rayhunter::qmdl::QmdlMessageReader;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::pin::pin;
|
||||
use std::sync::Arc;
|
||||
use tokio::fs::write;
|
||||
use tokio::io::{AsyncReadExt, copy, duplex};
|
||||
use tokio::io::copy;
|
||||
use tokio::io::duplex;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio_util::compat::FuturesAsyncReadCompatExt;
|
||||
use tokio_util::compat::FuturesAsyncWriteCompatExt;
|
||||
use tokio_util::io::ReaderStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -67,7 +72,7 @@ pub async fn get_qmdl(
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
let qmdl_idx = qmdl_name.trim_end_matches(".qmdl");
|
||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||
let (entry_index, entry) = qmdl_store.entry_for_name(qmdl_idx).ok_or((
|
||||
let (entry_index, _) = qmdl_store.entry_for_name(qmdl_idx).ok_or((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("couldn't find qmdl file with name {qmdl_idx}"),
|
||||
))?;
|
||||
@@ -81,14 +86,15 @@ pub async fn get_qmdl(
|
||||
)
|
||||
})?
|
||||
.ok_or((StatusCode::NOT_FOUND, "QMDL file not found".to_string()))?;
|
||||
let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64);
|
||||
let qmdl_stream = ReaderStream::new(limited_qmdl_file);
|
||||
let qmdl_reader = QmdlMessageReader::new(qmdl_file).await.map_err(|err| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("error reading QMDL file: {err}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let headers = [
|
||||
(CONTENT_TYPE, "application/octet-stream"),
|
||||
(CONTENT_LENGTH, &entry.qmdl_size_bytes.to_string()),
|
||||
];
|
||||
let body = Body::from_stream(qmdl_stream);
|
||||
let headers = [(CONTENT_TYPE, "application/octet-stream")];
|
||||
let body = Body::from_stream(qmdl_reader.into_qmdl_stream());
|
||||
Ok((headers, body).into_response())
|
||||
}
|
||||
|
||||
@@ -334,7 +340,7 @@ pub async fn get_zip(
|
||||
Path(entry_name): Path<String>,
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
let qmdl_idx = entry_name.trim_end_matches(".zip").to_owned();
|
||||
let (entry_index, qmdl_size_bytes) = {
|
||||
let entry_index = {
|
||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_idx).ok_or((
|
||||
StatusCode::NOT_FOUND,
|
||||
@@ -348,7 +354,7 @@ pub async fn get_zip(
|
||||
));
|
||||
}
|
||||
|
||||
(entry_index, entry.qmdl_size_bytes)
|
||||
entry_index
|
||||
};
|
||||
|
||||
let qmdl_store_lock = state.qmdl_store_lock.clone();
|
||||
@@ -377,23 +383,34 @@ pub async fn get_zip(
|
||||
continue;
|
||||
};
|
||||
|
||||
let entry = ZipEntryBuilder::new(
|
||||
file_kind.get_filename(&qmdl_idx).into(),
|
||||
/*
|
||||
* `qmdl_compressed` is always false here because even if the
|
||||
* QMDL was already compressed, we decompress it before zipping.
|
||||
* This is for two reasons
|
||||
* 1. If this is the current entry, it's still being written and
|
||||
* lacks a GZIP footer. If we zipped up this partial .gz
|
||||
* file, some software might consider it damaged and refuse to
|
||||
* extract it.
|
||||
* 2. Zipping an already-GZIP'd file is redundant and
|
||||
* inconvenient for the user.
|
||||
*/
|
||||
let zip_entry = ZipEntryBuilder::new(
|
||||
file_kind.get_filename(&qmdl_idx, false).into(),
|
||||
Compression::Stored,
|
||||
);
|
||||
// FuturesAsyncWriteCompatExt::compat_write because async-zip's entrystream does
|
||||
// not impl tokio's AsyncWrite, but only future's AsyncWrite. This can be removed
|
||||
// once https://github.com/Majored/rs-async-zip/pull/160 is released.
|
||||
let mut entry_writer = zip.write_entry_stream(entry).await?.compat_write();
|
||||
let mut entry_writer = zip.write_entry_stream(zip_entry).await?.compat_write();
|
||||
|
||||
// Truncating to qmdl_size_bytes is an attempt to ignore partial writes by the diag
|
||||
// thread.
|
||||
if file_kind == FileKind::Qmdl {
|
||||
copy(&mut file.take(qmdl_size_bytes as u64), &mut entry_writer).await?;
|
||||
let reader = QmdlMessageReader::new(&mut file).await?;
|
||||
let stream = reader.into_qmdl_stream();
|
||||
let mut reader = pin!(stream.into_async_read().compat());
|
||||
copy(&mut reader, &mut entry_writer).await?;
|
||||
} else {
|
||||
copy(&mut file, &mut entry_writer).await?;
|
||||
}
|
||||
|
||||
entry_writer.into_inner().close().await?;
|
||||
}
|
||||
|
||||
@@ -409,16 +426,11 @@ pub async fn get_zip(
|
||||
.open_file(entry_index, FileKind::Qmdl)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("QMDL file not found"))?
|
||||
.take(qmdl_size_bytes as u64)
|
||||
};
|
||||
let qmdl_reader = QmdlMessageReader::new(qmdl_file_for_pcap).await?;
|
||||
|
||||
if let Err(e) = generate_pcap_data(
|
||||
&mut entry_writer,
|
||||
qmdl_file_for_pcap,
|
||||
qmdl_size_bytes,
|
||||
gps_records,
|
||||
)
|
||||
.await
|
||||
if let Err(e) =
|
||||
generate_pcap_data(&mut entry_writer, qmdl_reader, gps_records).await
|
||||
{
|
||||
// if we fail to generate the PCAP file, we should still continue and give the
|
||||
// user the QMDL.
|
||||
@@ -532,10 +544,17 @@ pub async fn debug_set_display_state(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::*;
|
||||
use crate::config::GpsMode;
|
||||
use async_zip::base::read::mem::ZipFileReader;
|
||||
use axum::extract::{Path, State};
|
||||
use futures::AsyncReadExt;
|
||||
use rayhunter::{
|
||||
diag::{DataType, HdlcEncapsulatedMessage, Message, MessagesContainer},
|
||||
qmdl::{QmdlMessageReader, QmdlWriter},
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
|
||||
async fn create_test_qmdl_store() -> (TempDir, Arc<RwLock<crate::qmdl_store::RecordingStore>>) {
|
||||
@@ -549,24 +568,25 @@ mod tests {
|
||||
|
||||
async fn create_test_entry_with_data(
|
||||
store_lock: &Arc<RwLock<crate::qmdl_store::RecordingStore>>,
|
||||
test_data: &[u8],
|
||||
test_data: &MessagesContainer,
|
||||
) -> String {
|
||||
let entry_name = {
|
||||
let mut store = store_lock.write().await;
|
||||
let (mut qmdl_file, _analysis_file) = store.new_entry(GpsMode::Disabled).await.unwrap();
|
||||
let (mut qmdl_gz_file, _analysis_file) =
|
||||
store.new_entry(GpsMode::Disabled).await.unwrap();
|
||||
|
||||
if !test_data.is_empty() {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
qmdl_file.write_all(test_data).await.unwrap();
|
||||
qmdl_file.flush().await.unwrap();
|
||||
}
|
||||
let mut writer = QmdlWriter::new(&mut qmdl_gz_file);
|
||||
writer.write_container(test_data).await.unwrap();
|
||||
writer.close().await.unwrap();
|
||||
|
||||
let qmdl_file_size = qmdl_gz_file.metadata().await.unwrap().len() as usize;
|
||||
|
||||
let current_entry = store.current_entry.unwrap();
|
||||
let entry = &store.manifest.entries[current_entry];
|
||||
let entry_name = entry.name.clone();
|
||||
|
||||
store
|
||||
.update_entry_qmdl_size(current_entry, test_data.len())
|
||||
.update_current_entry_qmdl_size(qmdl_file_size)
|
||||
.await
|
||||
.unwrap();
|
||||
entry_name
|
||||
@@ -604,17 +624,32 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
// valid HDLC encapsulated diag message generated from
|
||||
// rayhunter::diag::test::get_test_message
|
||||
fn create_test_container() -> MessagesContainer {
|
||||
MessagesContainer {
|
||||
data_type: DataType::UserSpace,
|
||||
num_messages: 1,
|
||||
messages: vec![HdlcEncapsulatedMessage {
|
||||
len: 39,
|
||||
data: vec![
|
||||
16, 0, 32, 0, 32, 0, 192, 176, 26, 165, 245, 135, 118, 35, 2, 1, 20, 14, 48, 0,
|
||||
160, 0, 2, 8, 0, 0, 217, 15, 5, 0, 0, 0, 0, 1, 0, 10, 13, 196, 126,
|
||||
],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_zip_success() {
|
||||
let (_temp_dir, store_lock) = create_test_qmdl_store().await;
|
||||
let test_qmdl_data = vec![0x7E, 0x00, 0x00, 0x00, 0x10, 0x00, 0x7E];
|
||||
let test_qmdl_data = create_test_container();
|
||||
let entry_name = create_test_entry_with_data(&store_lock, &test_qmdl_data).await;
|
||||
let state = create_test_server_state(store_lock);
|
||||
|
||||
let result = get_zip(State(state), Path(entry_name.clone())).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
let response = get_zip(State(state), Path(entry_name.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let headers = response.headers();
|
||||
assert_eq!(headers.get("content-type").unwrap(), "application/zip");
|
||||
@@ -623,14 +658,12 @@ mod tests {
|
||||
let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||
|
||||
let zip_reader = ZipFileReader::new(body_bytes.to_vec()).await.unwrap();
|
||||
|
||||
let filenames = zip_reader
|
||||
.file()
|
||||
let zip_reader_file = zip_reader.file();
|
||||
let filenames: Vec<String> = zip_reader_file
|
||||
.entries()
|
||||
.iter()
|
||||
.map(|entry| entry.filename().as_str().unwrap().to_owned())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
.map(|entry| entry.filename().as_str().unwrap().to_string())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
filenames,
|
||||
vec![
|
||||
@@ -639,5 +672,22 @@ mod tests {
|
||||
format!("{entry_name}.pcapng"),
|
||||
]
|
||||
);
|
||||
|
||||
let mut qmdl_body = Vec::with_capacity(128);
|
||||
zip_reader
|
||||
.reader_without_entry(0)
|
||||
.await
|
||||
.unwrap()
|
||||
.read_to_end(&mut qmdl_body)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut qmdl_reader = QmdlMessageReader::new(Cursor::new(qmdl_body))
|
||||
.await
|
||||
.unwrap();
|
||||
let expected_message = Message::from_hdlc(&test_qmdl_data.messages[0].data).unwrap();
|
||||
assert_eq!(
|
||||
qmdl_reader.get_next_message().await.unwrap(),
|
||||
Some(Ok(expected_message)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -115,15 +115,15 @@ pub struct MemoryStats {
|
||||
|
||||
// runs the given command and returns its stdout as a string
|
||||
async fn get_cmd_output(mut cmd: Command) -> Result<String, String> {
|
||||
let cmd_str = format!("{:?}", &cmd);
|
||||
let cmd_str = format!("{:?}", cmd);
|
||||
let output = cmd
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("error running command {}: {}", &cmd_str, e))?;
|
||||
.map_err(|e| format!("error running command {}: {}", cmd_str, e))?;
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"command {} failed with exit code {}",
|
||||
&cmd_str,
|
||||
cmd_str,
|
||||
output.status.code().unwrap()
|
||||
));
|
||||
}
|
||||
|
||||
@@ -102,7 +102,8 @@ async fn try_upload_entry(
|
||||
shutdown_token: CancellationToken,
|
||||
) -> Option<()> {
|
||||
let read_lock = store.read().await;
|
||||
let entry_idx = read_lock.entry_for_name(&entry_name)?.0;
|
||||
let (entry_idx, entry) = read_lock.entry_for_name(&entry_name)?;
|
||||
let compressed = entry.compressed;
|
||||
let file = read_lock.open_file(entry_idx, file_kind).await;
|
||||
drop(read_lock);
|
||||
|
||||
@@ -118,7 +119,7 @@ async fn try_upload_entry(
|
||||
}
|
||||
};
|
||||
|
||||
let file_name = file_kind.get_filename(&entry_name);
|
||||
let file_name = file_kind.get_filename(&entry_name, compressed);
|
||||
|
||||
let res = select! {
|
||||
_ = shutdown_token.cancelled() => {
|
||||
@@ -299,7 +300,7 @@ mod tests {
|
||||
analysis_file.flush().await.unwrap();
|
||||
let entry_index = store.current_entry.unwrap();
|
||||
let name = store.manifest.entries[entry_index].name.clone();
|
||||
store.update_entry_qmdl_size(entry_index, 17).await.unwrap();
|
||||
store.update_current_entry_qmdl_size(17).await.unwrap();
|
||||
store.close_current_entry().await.unwrap();
|
||||
(Arc::new(RwLock::new(store)), name)
|
||||
}
|
||||
@@ -331,7 +332,7 @@ mod tests {
|
||||
let recorded = captured.lock().await;
|
||||
assert_eq!(recorded.len(), 3);
|
||||
let paths: Vec<&str> = recorded.iter().map(|r| r.path.as_str()).collect();
|
||||
let qmdl_path = format!("dav/{}.qmdl", entry_name);
|
||||
let qmdl_path = format!("dav/{}.qmdl.gz", entry_name);
|
||||
let ndjson_path = format!("dav/{}.ndjson", entry_name);
|
||||
let gps_path = format!("dav/{}-gps.ndjson", entry_name);
|
||||
assert!(paths.contains(&qmdl_path.as_str()));
|
||||
|
||||
Generated
+110
-92
@@ -27,7 +27,7 @@
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.59.0",
|
||||
"vite": "^8.0.10",
|
||||
"vite": "^8.0.16",
|
||||
"vitest": "^4.1.5"
|
||||
}
|
||||
},
|
||||
@@ -329,9 +329,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.127.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
|
||||
"integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
|
||||
"version": "0.133.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
||||
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -346,9 +346,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -363,9 +363,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -380,9 +380,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
|
||||
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -397,9 +397,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
|
||||
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -414,9 +414,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
|
||||
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -431,13 +431,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -448,13 +451,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
|
||||
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -465,13 +471,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -482,13 +491,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -499,13 +511,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -516,13 +531,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
|
||||
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -533,9 +551,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -550,9 +568,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
|
||||
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
@@ -569,9 +587,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
||||
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -586,9 +604,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
|
||||
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -603,9 +621,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1489,9 +1507,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2546,9 +2564,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2680,9 +2698,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
||||
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2700,7 +2718,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -2878,14 +2896,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
||||
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.127.0",
|
||||
"@rolldown/pluginutils": "1.0.0-rc.17"
|
||||
"@oxc-project/types": "=0.133.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
@@ -2894,21 +2912,21 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.0-rc.17",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.17",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
|
||||
"@rolldown/binding-android-arm64": "1.0.3",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.3",
|
||||
"@rolldown/binding-darwin-x64": "1.0.3",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.3",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.3",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.3",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.3",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.3",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/sade": {
|
||||
@@ -3183,9 +3201,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3316,17 +3334,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.10",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
|
||||
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
|
||||
"version": "8.0.16",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
||||
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.10",
|
||||
"rolldown": "1.0.0-rc.17",
|
||||
"tinyglobby": "^0.2.16"
|
||||
"postcss": "^8.5.15",
|
||||
"rolldown": "1.0.3",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@@ -3342,7 +3360,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.1.0",
|
||||
"@vitejs/devtools": "^0.1.18",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.59.0",
|
||||
"vite": "^8.0.10",
|
||||
"vite": "^8.0.16",
|
||||
"vitest": "^4.1.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,12 +61,6 @@ On the first detection of a crash, a diagnostic snapshot is saved to `/data/rayh
|
||||
|
||||
If recovery fails after 5 attempts, the status will change to **failed**. A reboot of the device will reset WiFi.
|
||||
|
||||
You can also configure WiFi during installation:
|
||||
|
||||
```sh
|
||||
./installer orbic --admin-password 'mypassword' --wifi-ssid 'MyNetwork' --wifi-password 'networkpass'
|
||||
```
|
||||
|
||||
## WebDAV Upload
|
||||
|
||||
Rayhunter can automatically upload finished recordings to a WebDAV server. When a `[webdav]` section is present in `config.toml`, a background worker periodically scans the recording store and uploads any closed entry that is older than `min_age_secs`. Each eligible entry uploads two files: the raw `.qmdl` capture and its `.ndjson` analysis output. After a successful upload the entry is either marked as uploaded in the manifest (and skipped on subsequent polls), or deleted locally if `delete_on_upload = true`. With no `[webdav]` section, no upload worker runs.
|
||||
|
||||
+6
-4
@@ -4,20 +4,24 @@
|
||||
|
||||
**It Depends**. Operation of Rayhunter does require the insertion of a SIM card into the device, but that sim card does not have to be actively registered with a service plan. If you want to use the device as a hotspot in addition to a research device, or get [notifications](./configuration.md), an active plan would of course be necessary.
|
||||
|
||||
### If I am located outside of the United States, should I use the SIM card that came with my device?
|
||||
|
||||
We recommend using a SIM card from a carrier from whichever country you are located in.
|
||||
|
||||
### How can I test that my device is working?
|
||||
|
||||
You can enable the `Test Heuristic` under `Analyzer Heuristic Settings` in the config section on your web dashboard. This will cause an alert to trigger every time your device sees a cell tower, you might need to reboot your device or move around a bit to get this one to trigger, but it will be very noisy once it does. People have also tested it by building IMSI catchers at home, but we don't recommend that, since it violates FCC regulations and will probably upset your neighbors.
|
||||
|
||||
<a name="red"></a>
|
||||
|
||||
### Help, Rayhunter's line is red/orange/yellow/dotted/dashed! What should I do?
|
||||
|
||||
Unfortunately, the circumstances that might lead to a positive cell site simulator (CSS) signal are quite varied, so we don't have a universal recommendation for how to deal with the a positive signal. Depending on your circumstances and threat model, you may want to turn off your phone until you are out of the area and tell your friends to do the same!
|
||||
Unfortunately, the circumstances that might lead to a positive cell site simulator (CSS) signal are quite varied, so we don't have a universal recommendation for how to deal with a positive signal. Depending on your circumstances and threat model, you may want to turn off your phone until you are out of the area and tell your friends to do the same!
|
||||
|
||||
If you've received a Rayhunter warning and would like to help us with our research, please send your Rayhunter data captures (Zip file downloaded from the web interface) to us at our [Signal](https://signal.org/) username [**ElectronicFrontierFoundation.90**](https://signal.me/#eu/HZbPPED5LyMkbTxJsG2PtWc2TXxPUR1OxBMcJGLOPeeCDGPuaTpOi5cfGRY6RrGf) with the following information: capture date, capture location, device, device model, and Rayhunter version. If you're unfamiliar with Signal, feel free to check out our [Security Self Defense guide on it](https://ssd.eff.org/module/how-to-use-signal).
|
||||
|
||||
Please note that this file may contain sensitive information such as your IMSI and the unique IDs of cell towers you were near which could be used to ascertain your location at the time.
|
||||
|
||||
|
||||
### Should I get a locked or unlocked orbic device? What is the difference?
|
||||
|
||||
If you want to use a non-Verizon SIM card you will probably need an unlocked device. But it's not clear which devices are locked nor how to unlock them, we welcome any experimentation and information regarding the use of unlocked devices. So far most verizon branded orbic devices we have encountered are actually unlocked.
|
||||
@@ -36,7 +40,6 @@ relevant as it does not use or touch USB.
|
||||
|
||||
[First obtain a shell](./orbic.md#shell), then:
|
||||
|
||||
|
||||
```sh
|
||||
# inside of Orbic's shell:
|
||||
echo 9 > /usrdata/mode.cfg
|
||||
@@ -55,7 +58,6 @@ reboot
|
||||
|
||||
See `/data/usb/boot_hsusb_composition` for a list of USB modes and Android USB gadget settings.
|
||||
|
||||
|
||||
### How do I connect my device to an existing WiFi network?
|
||||
|
||||
The Orbic, Moxee, UZ801, and TMOHS1 can connect to a nearby WiFi network while still running their own hotspot. This gives the device internet access for ntfy notifications and lets you reach the web UI from your home network. See [WiFi Client Mode](./configuration.md#wifi-client-mode) in the configuration guide for setup instructions.
|
||||
|
||||
@@ -42,8 +42,11 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
|
||||
# For Orbic:
|
||||
./installer orbic --admin-password 'mypassword'
|
||||
|
||||
# Note: the arguments --admin-username 'myusername' and --admin-ip 'mydeviceip'
|
||||
# may be required if different from the default.
|
||||
# Important notes:
|
||||
# - the arguments --admin-username 'myusername' and --admin-ip 'mydeviceip'
|
||||
# may be required if different from the default.
|
||||
# - on Windows this command should be run in PowerShell and not the Command
|
||||
# Prompt (CMD) where characters like quotes are treated differently.
|
||||
|
||||
# For TP-Link:
|
||||
./installer tplink
|
||||
|
||||
Generated
+125
-107
@@ -29,13 +29,13 @@
|
||||
"svelte-check": "^4.4.6",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.58.2",
|
||||
"vite": "^8.0.9"
|
||||
"vite": "^8.0.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
||||
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -44,9 +44,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
|
||||
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -289,13 +289,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
|
||||
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
"@tybys/wasm-util": "^0.10.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -307,9 +307,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz",
|
||||
"integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==",
|
||||
"version": "0.133.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
||||
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
@@ -323,9 +323,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -339,9 +339,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -355,9 +355,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
|
||||
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -371,9 +371,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
|
||||
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -387,9 +387,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
|
||||
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -403,12 +403,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -419,12 +422,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
|
||||
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -435,12 +441,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -451,12 +460,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -467,12 +479,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -483,12 +498,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
|
||||
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -499,9 +517,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -515,17 +533,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
|
||||
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "1.9.2",
|
||||
"@emnapi/runtime": "1.9.2",
|
||||
"@emnapi/core": "1.10.0",
|
||||
"@emnapi/runtime": "1.10.0",
|
||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -533,9 +551,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
||||
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -549,9 +567,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
|
||||
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -565,9 +583,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
@@ -1211,9 +1229,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1559,9 +1577,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2611,9 +2629,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -2735,9 +2753,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
||||
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -2754,7 +2772,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -2932,13 +2950,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
||||
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.126.0",
|
||||
"@rolldown/pluginutils": "1.0.0-rc.16"
|
||||
"@oxc-project/types": "=0.133.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
@@ -2947,21 +2965,21 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.0-rc.16",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.16",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.16",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.16",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.16",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.16",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16"
|
||||
"@rolldown/binding-android-arm64": "1.0.3",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.3",
|
||||
"@rolldown/binding-darwin-x64": "1.0.3",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.3",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.3",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.3",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.3",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.3",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/sade": {
|
||||
@@ -3146,9 +3164,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
@@ -3260,16 +3278,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.9",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz",
|
||||
"integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==",
|
||||
"version": "8.0.16",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
||||
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.10",
|
||||
"rolldown": "1.0.0-rc.16",
|
||||
"tinyglobby": "^0.2.16"
|
||||
"postcss": "^8.5.15",
|
||||
"rolldown": "1.0.3",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@@ -3285,7 +3303,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.1.0",
|
||||
"@vitejs/devtools": "^0.1.18",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
|
||||
@@ -37,6 +37,6 @@
|
||||
"svelte-check": "^4.4.6",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.58.2",
|
||||
"vite": "^8.0.9"
|
||||
"vite": "^8.0.16"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.37" }
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -23,3 +24,4 @@ serde_json = "1"
|
||||
anyhow = "1.0.100"
|
||||
shlex = "1"
|
||||
installer = { path = "../../installer" }
|
||||
log = "0.4.20"
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
//! Combines the values we care about from the clap CLI and the modifiers module into something
|
||||
//! serializable by serde.
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Context;
|
||||
use log::error;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::modifiers;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Command<'a> {
|
||||
subcommands: Vec<Subcommand<'a>>,
|
||||
}
|
||||
|
||||
impl Command<'_> {
|
||||
pub fn new(command: &clap::Command) -> Command<'_> {
|
||||
let subcommand_map: HashMap<&str, &clap::Command> = command
|
||||
.get_subcommands()
|
||||
.map(|s| (s.get_name(), s))
|
||||
.collect();
|
||||
|
||||
Command {
|
||||
// this resulting vector contains the subcommands that are found in both
|
||||
// command.get_subcommands() and modifiers::subcommand_modifiers() in the order defined
|
||||
// by subcommand_modifiers()
|
||||
subcommands: modifiers::subcommand_modifiers()
|
||||
.iter()
|
||||
.filter_map(|modifier| match subcommand_map.get(modifier.command) {
|
||||
Some(clap_command) => Some(Subcommand::new(clap_command, modifier)),
|
||||
None => {
|
||||
error!(
|
||||
"failed to find clap command for subcommand {}",
|
||||
modifier.command
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct Argument<'a> {
|
||||
advanced: bool,
|
||||
flag: String,
|
||||
label: &'a str,
|
||||
takes_values: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct Subcommand<'a> {
|
||||
arguments: Vec<Argument<'a>>,
|
||||
command: &'a str,
|
||||
label: &'a str,
|
||||
}
|
||||
|
||||
impl Argument<'_> {
|
||||
fn try_new<'a>(
|
||||
argument: &'a clap::Arg,
|
||||
modifier: &modifiers::ArgumentModifier<'static>,
|
||||
) -> anyhow::Result<Argument<'a>> {
|
||||
let partial_flag = argument.get_long().with_context(|| {
|
||||
format!(
|
||||
"Missing long form command line flag for {}",
|
||||
argument.get_id().as_str(),
|
||||
)
|
||||
})?;
|
||||
Ok(Argument {
|
||||
advanced: modifier.advanced,
|
||||
flag: format!("--{}", partial_flag),
|
||||
label: modifier.gui_label,
|
||||
takes_values: argument.get_action().takes_values(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Subcommand<'_> {
|
||||
fn new<'a>(
|
||||
command: &'a clap::Command,
|
||||
modifier: &modifiers::SubcommandModifier<'static>,
|
||||
) -> Subcommand<'a> {
|
||||
let argument_map: HashMap<&str, &clap::Arg> = command
|
||||
.get_arguments()
|
||||
.map(|a| (a.get_id().as_str(), a))
|
||||
.collect();
|
||||
|
||||
Subcommand {
|
||||
// this resulting vector contains the arguments that are found in both
|
||||
// command.get_arguments() and modifier.arg_modifiers in the order defined by by
|
||||
// arg_modifiers
|
||||
arguments: modifier
|
||||
.arg_modifiers
|
||||
.iter()
|
||||
.filter_map(|arg_modifier| {
|
||||
let Some(arg) = argument_map.get(arg_modifier.clap_id) else {
|
||||
error!(
|
||||
"failed to find clap argument with id {}",
|
||||
arg_modifier.clap_id
|
||||
);
|
||||
return None;
|
||||
};
|
||||
match Argument::try_new(arg, arg_modifier) {
|
||||
Ok(modified_arg) => Some(modified_arg),
|
||||
Err(err) => {
|
||||
error!("failed to create modified argument: {:?}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
command: modifier.command,
|
||||
label: modifier.gui_label,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::CommandFactory;
|
||||
use tauri::Emitter;
|
||||
|
||||
mod introspect;
|
||||
mod modifiers;
|
||||
|
||||
static INSTALLER_COMMAND: LazyLock<clap::Command> = LazyLock::new(installer::Args::command);
|
||||
|
||||
async fn run_installer(app_handle: tauri::AppHandle, args: String) -> anyhow::Result<()> {
|
||||
let args_vec = shlex::split(&args).context("Failed to parse arguments: unclosed quote")?;
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
@@ -25,11 +33,17 @@ async fn install_rayhunter(app_handle: tauri::AppHandle, args: String) -> Result
|
||||
.map_err(|error| format!("{error:?}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn rayhunter_options() -> introspect::Command<'static> {
|
||||
introspect::Command::new(&INSTALLER_COMMAND)
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![install_rayhunter])
|
||||
.invoke_handler(tauri::generate_handler![rayhunter_options])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
//! Adds or "modifies" installer CLI attributes for use in the GUI.
|
||||
//!
|
||||
//! This module contains little logic (outside of tests) and instead just provides additional
|
||||
//! metadata about CLI commands and options for the GUI installer.
|
||||
//!
|
||||
//! If we like this approach, I think we should consider renaming this file something like
|
||||
//! gui_modifiers.rs and moving it into the crate for the CLI installer. I think this would simplify
|
||||
//! development as any breaking changes to the CLI installer interface would cause tests to fail in
|
||||
//! its own crate instead of installer-gui and it'd help to keep the two interfaces to the installer
|
||||
//! in sync.
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ArgumentModifier<'a> {
|
||||
/// The name or "ID" of the argument as defined in clap. This will usually be the name of the
|
||||
/// field in the struct the argument is derived from.
|
||||
pub clap_id: &'a str,
|
||||
/// The text for displaying this argument in the GUI.
|
||||
pub gui_label: &'a str,
|
||||
/// Whether this argument should be hidden behind a menu for "advanced" options.
|
||||
pub advanced: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SubcommandModifier<'a> {
|
||||
/// The name of the subcommand on the CLI.
|
||||
pub command: &'a str,
|
||||
/// The text for displaying this subcommand in the GUI.
|
||||
pub gui_label: &'a str,
|
||||
/// Modifications to the arguments of this subcommand. The order arguments are defined in this
|
||||
/// vector will match the order the arguments are displayed in the GUI.
|
||||
pub arg_modifiers: Vec<ArgumentModifier<'a>>,
|
||||
}
|
||||
|
||||
/// Provides "modifiers" or additional metadata about each subcommand.
|
||||
///
|
||||
/// The order of the subcommands in the returned vector is the same order that subcommands will be
|
||||
/// shown in the GUI. Similarly, the order of the elements in each arg_modifiers field controls the
|
||||
/// order that a subcommand's options will be shown in the GUI.
|
||||
pub fn subcommand_modifiers() -> Vec<SubcommandModifier<'static>> {
|
||||
let admin_ip = ArgumentModifier {
|
||||
clap_id: "admin_ip",
|
||||
gui_label: "Admin IP",
|
||||
advanced: true,
|
||||
};
|
||||
let admin_username = ArgumentModifier {
|
||||
clap_id: "admin_username",
|
||||
gui_label: "Admin Username",
|
||||
advanced: true,
|
||||
};
|
||||
let admin_password = ArgumentModifier {
|
||||
clap_id: "admin_password",
|
||||
gui_label: "Admin Password",
|
||||
advanced: false,
|
||||
};
|
||||
let data_dir = ArgumentModifier {
|
||||
clap_id: "data_dir",
|
||||
gui_label: "Data Directory",
|
||||
advanced: true,
|
||||
};
|
||||
let reset_config = ArgumentModifier {
|
||||
clap_id: "reset_config",
|
||||
gui_label: "Reset config.toml",
|
||||
advanced: true,
|
||||
};
|
||||
let orbic_and_moxee_args = vec![
|
||||
admin_password,
|
||||
admin_ip,
|
||||
admin_username,
|
||||
reset_config,
|
||||
data_dir,
|
||||
];
|
||||
|
||||
vec![
|
||||
SubcommandModifier {
|
||||
command: "orbic",
|
||||
gui_label: "Orbic/Kajeet (via network)",
|
||||
arg_modifiers: orbic_and_moxee_args.clone(),
|
||||
},
|
||||
SubcommandModifier {
|
||||
command: "orbic-usb",
|
||||
gui_label: "Orbic/Kajeet (via legacy USB+ADB installer)",
|
||||
arg_modifiers: vec![reset_config],
|
||||
},
|
||||
SubcommandModifier {
|
||||
command: "tplink",
|
||||
gui_label: "TP-Link",
|
||||
arg_modifiers: vec![
|
||||
admin_ip,
|
||||
reset_config,
|
||||
data_dir,
|
||||
ArgumentModifier {
|
||||
clap_id: "skip_sdcard",
|
||||
gui_label: "Skip SD Card",
|
||||
advanced: true,
|
||||
},
|
||||
ArgumentModifier {
|
||||
clap_id: "sdcard_path",
|
||||
gui_label: "SD Card Path",
|
||||
advanced: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
SubcommandModifier {
|
||||
command: "moxee",
|
||||
gui_label: "Moxee",
|
||||
arg_modifiers: orbic_and_moxee_args,
|
||||
},
|
||||
SubcommandModifier {
|
||||
command: "pinephone",
|
||||
gui_label: "PinePhone",
|
||||
arg_modifiers: vec![],
|
||||
},
|
||||
SubcommandModifier {
|
||||
command: "tmobile",
|
||||
gui_label: "TMobile",
|
||||
arg_modifiers: vec![admin_password, admin_ip],
|
||||
},
|
||||
SubcommandModifier {
|
||||
command: "uz801",
|
||||
gui_label: "UZ801",
|
||||
arg_modifiers: vec![admin_ip],
|
||||
},
|
||||
SubcommandModifier {
|
||||
command: "wingtech",
|
||||
gui_label: "Wingtech",
|
||||
arg_modifiers: vec![admin_password, admin_ip],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
//! Subcommands and arguments not returned from subcommand_modifiers() will be excluded from the
|
||||
//! GUI. This is by design as it allows us to exclude things like some or all of the installer
|
||||
//! utils from the GUI. The tests below help ensure that exclusions were done deliberately
|
||||
//! rather than on accident.
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Lists the subcommands that are purposefully excluded from subcommand_modifiers().
|
||||
fn excluded_subcommands() -> Vec<&'static str> {
|
||||
vec!["util"]
|
||||
}
|
||||
|
||||
/// Lists the arguments that are purposefully excluded from subcommand_modifiers(). Items in the
|
||||
/// list take the form of (subcommand, argument_id) tuples.
|
||||
fn excluded_arguments() -> Vec<(&'static str, &'static str)> {
|
||||
// if for example we wanted to exclude the "--admin-password" argument for "orbic", we'd
|
||||
// return vec![("orbic", "admin_password")] here
|
||||
vec![]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_subcommands_excluded_or_modified() {
|
||||
let mut all_subcommands: Vec<&str> = crate::INSTALLER_COMMAND
|
||||
.get_subcommands()
|
||||
.map(|c| c.get_name())
|
||||
.collect();
|
||||
let mut excluded_or_modified_subcommands: Vec<&str> = subcommand_modifiers()
|
||||
.into_iter()
|
||||
.map(|m| m.command)
|
||||
.chain(excluded_subcommands())
|
||||
.collect();
|
||||
|
||||
all_subcommands.sort_unstable();
|
||||
excluded_or_modified_subcommands.sort_unstable();
|
||||
|
||||
assert_eq!(
|
||||
all_subcommands, excluded_or_modified_subcommands,
|
||||
"Every subcommand must be included exactly once in subcommand_modifiers() or excluded_subcommands()."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arguments_excluded_or_modified() {
|
||||
// create maps of subcommand name to lists of argument names
|
||||
let all_args_for_nonexcluded_subcommands: HashMap<&str, Vec<&str>> =
|
||||
nonexcluded_subcommand_objects()
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
(
|
||||
c.get_name(),
|
||||
c.get_arguments().map(|a| a.get_id().as_str()).collect(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let modified_args: HashMap<&str, Vec<&str>> = subcommand_modifiers()
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
(
|
||||
m.command,
|
||||
m.arg_modifiers
|
||||
.into_iter()
|
||||
.map(|arg_m| arg_m.clap_id)
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// add excluded_arguments to modified_args
|
||||
let mut excluded_or_modified_args = modified_args;
|
||||
for (subcommand_name, arg_name) in excluded_arguments() {
|
||||
excluded_or_modified_args
|
||||
.entry(subcommand_name)
|
||||
.or_default()
|
||||
.push(arg_name);
|
||||
}
|
||||
|
||||
// assert that all arguments are excluded or modified
|
||||
for (subcommand_name, mut expected_args) in all_args_for_nonexcluded_subcommands {
|
||||
let mut found_args = excluded_or_modified_args
|
||||
.remove(subcommand_name)
|
||||
.unwrap_or_default();
|
||||
|
||||
expected_args.sort_unstable();
|
||||
found_args.sort_unstable();
|
||||
|
||||
assert_eq!(
|
||||
expected_args, found_args,
|
||||
"Excluded and modified arguments differ from expected arguments for {subcommand_name}."
|
||||
)
|
||||
}
|
||||
assert!(
|
||||
excluded_or_modified_args.is_empty(),
|
||||
"Excluded or modified arguments found for unexpected subcommands. Map of unexpected arguments is {:?}",
|
||||
excluded_or_modified_args
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arguments_have_long_flag() {
|
||||
// any arguments without a long form command line flag will be excluded from the GUI so
|
||||
// let's test for it here to avoid surprises
|
||||
|
||||
let excluded_args = excluded_arguments();
|
||||
let nonexcluded_args: Vec<(&str, &clap::Arg)> = nonexcluded_subcommand_objects()
|
||||
.into_iter()
|
||||
.flat_map(|c| {
|
||||
c.get_arguments().filter_map(|a| {
|
||||
let subcommand_name = c.get_name();
|
||||
|
||||
if excluded_args.contains(&(subcommand_name, a.get_id().as_str())) {
|
||||
None
|
||||
} else {
|
||||
Some((subcommand_name, a))
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (subcommand_name, arg) in nonexcluded_args {
|
||||
assert!(
|
||||
arg.get_long().is_some(),
|
||||
"The {} argument for {subcommand_name} is missing a long form command line flag.",
|
||||
arg.get_id().as_str()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn nonexcluded_subcommand_objects() -> Vec<&'static clap::Command> {
|
||||
let excluded_subcommands = excluded_subcommands();
|
||||
|
||||
crate::INSTALLER_COMMAND
|
||||
.get_subcommands()
|
||||
.filter(|s| !excluded_subcommands.contains(&s.get_name()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,11 @@ use crate::output::eprintln;
|
||||
static CONFIG_TOML: &str = include_str!("../../dist/config.toml.in");
|
||||
static RAYHUNTER_DAEMON_INIT: &str = include_str!("../../dist/scripts/rayhunter_daemon");
|
||||
|
||||
// We mark this as public so it can be used by installer-gui to programmatically introspect the
|
||||
// installer's options.
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about)]
|
||||
struct Args {
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
@@ -346,9 +346,13 @@ async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> {
|
||||
admin_ip: admin_ip.to_owned(),
|
||||
});
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:4000")
|
||||
let bind_addr = "127.0.0.1:4000";
|
||||
let listener = tokio::net::TcpListener::bind(bind_addr)
|
||||
.await
|
||||
.unwrap();
|
||||
.with_context(|| format!(
|
||||
"Failed to bind to {bind_addr}. Is another process using this port?\n\
|
||||
Try closing any application that might be listening on port 4000 and rerun the installer."
|
||||
))?;
|
||||
|
||||
println!("Listening on http://{}", listener.local_addr().unwrap());
|
||||
println!("Please open above URL in your browser and log into the router to continue.");
|
||||
|
||||
@@ -30,6 +30,7 @@ serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
num_enum = "0.7.4"
|
||||
utoipa = { version = "5.4.0", optional = true }
|
||||
async-compression = { version = "0.4.41", features = ["tokio", "gzip"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
@@ -5,9 +5,9 @@ use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::analysis::diagnostic::DiagnosticAnalyzer;
|
||||
use crate::gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType};
|
||||
use crate::diag::{DiagParsingError, Message, MessagesContainer};
|
||||
use crate::gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType, parser as gsmtap_parser};
|
||||
use crate::util::RuntimeMetadata;
|
||||
use crate::{diag::MessagesContainer, gsmtap_parser};
|
||||
|
||||
use super::{
|
||||
connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer,
|
||||
@@ -223,7 +223,7 @@ impl AnalysisLineNormalizer {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[derive(Serialize, Debug, Default)]
|
||||
pub struct AnalysisRow {
|
||||
pub packet_timestamp: Option<DateTime<FixedOffset>>,
|
||||
pub skipped_message_reason: Option<String>,
|
||||
@@ -231,6 +231,10 @@ pub struct AnalysisRow {
|
||||
}
|
||||
|
||||
impl AnalysisRow {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.skipped_message_reason.is_none() && !self.contains_warnings()
|
||||
}
|
||||
@@ -412,50 +416,48 @@ impl Harness {
|
||||
row
|
||||
}
|
||||
|
||||
pub fn analyze_qmdl_message(
|
||||
&mut self,
|
||||
maybe_qmdl_message: Result<Message, DiagParsingError>,
|
||||
) -> AnalysisRow {
|
||||
let mut row = AnalysisRow::new();
|
||||
self.packet_num += 1;
|
||||
|
||||
let qmdl_message = match maybe_qmdl_message {
|
||||
Ok(msg) => msg,
|
||||
Err(err) => {
|
||||
row.skipped_message_reason = Some(format!("{err:?}"));
|
||||
return row;
|
||||
}
|
||||
};
|
||||
let (timestamp, gsmtap_msg) = match gsmtap_parser::parse(qmdl_message) {
|
||||
Ok(Some(msg)) => msg,
|
||||
Ok(None) => return row,
|
||||
Err(err) => {
|
||||
row.skipped_message_reason = Some(format!("{err:?}"));
|
||||
return row;
|
||||
}
|
||||
};
|
||||
row.packet_timestamp = Some(timestamp.to_datetime());
|
||||
|
||||
let element = match InformationElement::try_from(&gsmtap_msg) {
|
||||
Ok(element) => element,
|
||||
Err(err) => {
|
||||
row.skipped_message_reason = Some(format!("{err:?}"));
|
||||
return row;
|
||||
}
|
||||
};
|
||||
|
||||
row.events = self.analyze_information_element(&element);
|
||||
row
|
||||
}
|
||||
|
||||
pub fn analyze_qmdl_messages(&mut self, container: MessagesContainer) -> Vec<AnalysisRow> {
|
||||
let mut rows = Vec::new();
|
||||
for maybe_qmdl_message in container.messages() {
|
||||
self.packet_num += 1;
|
||||
|
||||
rows.push(AnalysisRow {
|
||||
packet_timestamp: None,
|
||||
skipped_message_reason: None,
|
||||
events: Vec::new(),
|
||||
});
|
||||
// unwrap is safe here since we just pushed a value
|
||||
let row = rows.last_mut().unwrap();
|
||||
let qmdl_message = match maybe_qmdl_message {
|
||||
Ok(msg) => msg,
|
||||
Err(err) => {
|
||||
row.skipped_message_reason = Some(format!("{err:?}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let gsmtap_message = match gsmtap_parser::parse(qmdl_message) {
|
||||
Ok(msg) => msg,
|
||||
Err(err) => {
|
||||
row.skipped_message_reason = Some(format!("{err:?}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let Some((timestamp, gsmtap_msg)) = gsmtap_message else {
|
||||
continue;
|
||||
};
|
||||
row.packet_timestamp = Some(timestamp.to_datetime());
|
||||
|
||||
let element = match InformationElement::try_from(&gsmtap_msg) {
|
||||
Ok(element) => element,
|
||||
Err(err) => {
|
||||
row.skipped_message_reason = Some(format!("{err:?}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
row.events = self.analyze_information_element(&element);
|
||||
}
|
||||
rows
|
||||
container
|
||||
.messages()
|
||||
.drain(..)
|
||||
.map(|maybe_message| self.analyze_qmdl_message(maybe_message))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec<Option<Event>> {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
use deku::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(bit_order = "lsb")]
|
||||
pub struct ServingCellTiming {
|
||||
#[deku(assert_eq = "1")]
|
||||
pub version: u8,
|
||||
#[deku(bits = 5, assert = "(1..=20).contains(num_records)")]
|
||||
pub num_records: u8,
|
||||
#[deku(bits = 4, assert = "*starting_sub_fn <= 9")]
|
||||
pub starting_sub_fn: u8,
|
||||
#[deku(
|
||||
bits = 10,
|
||||
pad_bits_after = "5",
|
||||
assert = "*starting_system_fn <= 1023"
|
||||
)]
|
||||
pub starting_system_fn: u16,
|
||||
#[deku(
|
||||
bits = 19,
|
||||
pad_bits_after = "13",
|
||||
assert = "*starting_dl_frame_timing_offs <= 307200"
|
||||
)]
|
||||
pub starting_dl_frame_timing_offs: u32, // in Ts units
|
||||
#[deku(bits = 19, assert = "*starting_ul_frame_timing_offs <= 307200")]
|
||||
pub starting_ul_frame_timing_offs: u32, // in Ts units
|
||||
#[deku(bits = 11, pad_bits_after = "2")]
|
||||
pub starting_ul_timing_advance: u16, // in 16 Ts units
|
||||
#[deku(count = "num_records")]
|
||||
pub timing_adjustment: Vec<TimingAdjustment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(bit_order = "lsb", ctx = "_: deku::ctx::Order")]
|
||||
pub struct TimingAdjustment {
|
||||
#[deku(
|
||||
bits = 11,
|
||||
assert = "(-512..=511).contains(dl_frame_timing_adjustment)"
|
||||
)]
|
||||
pub dl_frame_timing_adjustment: i16, // in Ts units
|
||||
#[deku(bits = 5, assert = "(-16..=15).contains(ul_frame_timing_adjustment)")]
|
||||
pub ul_frame_timing_adjustment: i8, // in Ts units
|
||||
#[deku(assert = "(-128..=127).contains(timing_advance)")]
|
||||
pub timing_advance: i8, // in 16 Ts units
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
//! Diag MAC RACH serialization/deserialization. As with most of our diag
|
||||
//! parsers, these structs were derived from SCAT:
|
||||
//! https://github.com/fgsect/scat/blob/9763cb5b1dcd5ee980f5b0ead9a8d520c8c51a51/src/scat/parsers/qualcomm/diagltelogparser.py#L853
|
||||
|
||||
use deku::prelude::*;
|
||||
|
||||
#[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)]
|
||||
pub struct Packet {
|
||||
#[deku(assert_eq = "1")]
|
||||
pub version: u8,
|
||||
pub num_subpackets: u8,
|
||||
#[deku(pad_bytes_before = "2", count = "num_subpackets")]
|
||||
pub subpackets: Vec<Subpacket>,
|
||||
}
|
||||
|
||||
#[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)]
|
||||
pub struct Subpacket {
|
||||
pub id: u8,
|
||||
pub version: u8,
|
||||
pub size: u16,
|
||||
// size includes the header length, so subtract that
|
||||
#[deku(ctx = "*id, *version, *size - 4")]
|
||||
pub body: SubpacketBody,
|
||||
}
|
||||
|
||||
#[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)]
|
||||
#[deku(ctx = "id: u8, version: u8, size: u16", id = "id")]
|
||||
pub enum SubpacketBody {
|
||||
#[deku(id = 0x06)]
|
||||
RachAttempt(#[deku(ctx = "version")] rach::Attempt),
|
||||
#[deku(id_pat = "_")]
|
||||
Other {
|
||||
#[deku(count = "size")]
|
||||
data: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
pub mod rach {
|
||||
//! Derived from https://github.com/fgsect/scat/blob/9763cb5b1dcd5ee980f5b0ead9a8d520c8c51a51/src/scat/parsers/qualcomm/diagltelogparser.py#L496
|
||||
use super::*;
|
||||
|
||||
#[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)]
|
||||
#[deku(ctx = "version: u8")]
|
||||
pub struct Attempt {
|
||||
#[deku(ctx = "version")]
|
||||
pub header: AttemptHeader,
|
||||
// since we may not have meaningful msg fields, they're marked private and the get_msg
|
||||
// functions should be used instead
|
||||
#[deku(ctx = "version")]
|
||||
msg1: Msg1,
|
||||
msg2: Msg2,
|
||||
#[deku(ctx = "version")]
|
||||
msg3: Msg3,
|
||||
#[deku(cond = "version == 0x31 || version == 0x32")]
|
||||
pub additional_info: Option<AdditionalInfo>,
|
||||
}
|
||||
|
||||
impl Attempt {
|
||||
pub fn get_msg1(&self) -> Option<&Msg1> {
|
||||
if self.header.has_msg1() {
|
||||
Some(&self.msg1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_msg2(&self) -> Option<&Msg2> {
|
||||
if self.header.has_msg2() {
|
||||
Some(&self.msg2)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_msg3(&self) -> Option<&Msg3> {
|
||||
if self.header.has_msg3() {
|
||||
Some(&self.msg3)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)]
|
||||
pub struct AdditionalInfo {
|
||||
pub ul_earfcn: u32,
|
||||
pub p_max: u8,
|
||||
pub scell_id: u8,
|
||||
pub unk1: u32,
|
||||
pub unk2: u32,
|
||||
}
|
||||
|
||||
#[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)]
|
||||
#[deku(ctx = "version: u8", id = "version")]
|
||||
pub enum Msg1 {
|
||||
#[deku(id = "0x02")]
|
||||
V2 {
|
||||
preamble_index: u8,
|
||||
preamble_index_mask: u8,
|
||||
preamble_power_offset: i16,
|
||||
},
|
||||
#[deku(id_pat = "0x03 | 0x31")]
|
||||
V3Or31 {
|
||||
preamble_index: u8,
|
||||
preamble_index_mask: u8,
|
||||
preamble_power_offset: i16,
|
||||
},
|
||||
#[deku(id = "0x32")]
|
||||
V32 {
|
||||
preamble_index: u8,
|
||||
preamble_index_mask: u8,
|
||||
preamble_power_offset: i16,
|
||||
unk1: u16,
|
||||
group: i8,
|
||||
},
|
||||
}
|
||||
|
||||
impl Msg1 {
|
||||
pub fn get_preamble_index(&self) -> u8 {
|
||||
match self {
|
||||
Msg1::V2 { preamble_index, .. } => *preamble_index,
|
||||
Msg1::V3Or31 { preamble_index, .. } => *preamble_index,
|
||||
Msg1::V32 { preamble_index, .. } => *preamble_index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)]
|
||||
pub struct Msg2 {
|
||||
pub backoff: u16,
|
||||
pub result: u8,
|
||||
pub tc_rnti: u16,
|
||||
pub ta: u16,
|
||||
}
|
||||
|
||||
#[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)]
|
||||
#[deku(ctx = "version: u8")]
|
||||
pub struct Msg3 {
|
||||
#[deku(ctx = "version")]
|
||||
pub grant: Msg3Grant,
|
||||
pub unk_grant: u16,
|
||||
pub harq_id: u8,
|
||||
pub mac_pdu: [u8; 10],
|
||||
}
|
||||
|
||||
impl Msg3 {
|
||||
pub fn get_grant(&self) -> u32 {
|
||||
match self.grant {
|
||||
Msg3Grant::V1 { grant } => grant,
|
||||
Msg3Grant::V32 { grant } => grant,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)]
|
||||
#[deku(ctx = "version: u8", id = "version")]
|
||||
pub enum Msg3Grant {
|
||||
#[deku(id_pat = "0..0x32")]
|
||||
V1 {
|
||||
#[deku(endian = "little", map = "Msg3Grant::map_grant")]
|
||||
grant: u32,
|
||||
},
|
||||
#[deku(id_pat = "0x32..")]
|
||||
V32 {
|
||||
#[deku(endian = "big", map = "Msg3Grant::map_grant")]
|
||||
grant: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl Msg3Grant {
|
||||
fn map_grant(grant: u32) -> Result<u32, DekuError> {
|
||||
Ok(grant & 0xfffff)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)]
|
||||
#[deku(ctx = "version: u8", id = "version")]
|
||||
pub enum AttemptHeader {
|
||||
#[deku(id = 0x02)]
|
||||
V2 {
|
||||
num_attempt: u8,
|
||||
rach_result: u8,
|
||||
contention: u8,
|
||||
msg_bitmask: u8,
|
||||
},
|
||||
#[deku(id_pat = "0x03 | 0x31 | 0x32")]
|
||||
V3 {
|
||||
sub_id: u8,
|
||||
cell_id: u8,
|
||||
num_attempt: u8,
|
||||
rach_result: u8,
|
||||
contention: u8,
|
||||
msg_bitmask: u8,
|
||||
},
|
||||
}
|
||||
|
||||
impl AttemptHeader {
|
||||
fn get_bitmask(&self) -> u8 {
|
||||
match self {
|
||||
AttemptHeader::V2 { msg_bitmask, .. } => *msg_bitmask,
|
||||
AttemptHeader::V3 { msg_bitmask, .. } => *msg_bitmask,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_msg1(&self) -> bool {
|
||||
self.get_bitmask() & 0x01 > 0
|
||||
}
|
||||
|
||||
pub fn has_msg2(&self) -> bool {
|
||||
self.get_bitmask() & 0x02 > 0
|
||||
}
|
||||
|
||||
pub fn has_msg3(&self) -> bool {
|
||||
self.get_bitmask() & 0x04 > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test {
|
||||
//! These tests were adapted from SCAT's MAC RACH parser's unit tests,
|
||||
//! and the values were produced by modifying the tests to output the
|
||||
//! entire parsed struct rather than the hexlified gsmtap packets. See
|
||||
//! the changes in this commit for more info:
|
||||
//! https://github.com/wgreenberg/scat/commit/adb21575832b4f3b30c8f2aaca9ee843ef74f38b
|
||||
|
||||
use super::*;
|
||||
use crate::diag::diaglog::mac::rach::{AdditionalInfo, AttemptHeader, Msg1, Msg2, Msg3};
|
||||
use crate::{diag::diaglog::mac::rach::Msg3Grant, test_util::unhexlify};
|
||||
|
||||
use std::io::Seek;
|
||||
|
||||
pub fn mac_rach_test_packets_from_scat() -> Vec<Packet> {
|
||||
// test data from SCAT unit tests: https://github.com/fgsect/scat/blob/9763cb5b1dcd5ee980f5b0ead9a8d520c8c51a51/tests/test_diagltelogparser.py#L129
|
||||
vec![
|
||||
parse_rach_packet(
|
||||
"0101a06906022400010001071BFF98FF000001231A0400181C010007000600465C80BD0648000000",
|
||||
),
|
||||
parse_rach_packet(
|
||||
"0101a0690603280001000100010718ffa4ff000001c6610b00b4a2000012000120061f423f8d95075800",
|
||||
),
|
||||
parse_rach_packet(
|
||||
"0101739e063134000100010000033f0098ff0000013c6b070058ac010007000000468f47e2d446000000644b0000180001000000d5040000",
|
||||
),
|
||||
parse_rach_packet(
|
||||
"01010000063134000100010001070aff98ff0000011c48070018e2000007000000523b7dfd69b6000000f5540000ff0001000000d6040000",
|
||||
),
|
||||
parse_rach_packet(
|
||||
"01010000063238000100010000032900a4ffeb000000000195b603000000a0b412000420061f425dc9be41b800885e000017000100000065050000",
|
||||
),
|
||||
parse_rach_packet(
|
||||
"010100000632380001000100010713ffa0ffeb0000000001ad5a0500000146b412000420061f425dc9be41b400665300001800010000001a050000",
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn parse_rach_packet(bytes_str: &str) -> Packet {
|
||||
let (total_size, mut reader) = unhexlify(bytes_str);
|
||||
let packet = Packet::from_reader_with_ctx(&mut reader, ()).unwrap();
|
||||
let leftover_bits = reader.rest().len();
|
||||
let leftover_bytes = total_size - reader.stream_position().unwrap() as usize;
|
||||
assert_eq!(leftover_bytes, 0);
|
||||
assert_eq!(leftover_bits, 0);
|
||||
packet
|
||||
}
|
||||
|
||||
fn assert_rach_subpacket(
|
||||
packet: &Packet,
|
||||
header: AttemptHeader,
|
||||
msg1: Option<Msg1>,
|
||||
msg2: Option<Msg2>,
|
||||
msg3: Option<Msg3>,
|
||||
additional_info: Option<AdditionalInfo>,
|
||||
) {
|
||||
assert_eq!(packet.version, 0x01);
|
||||
assert_eq!(packet.num_subpackets, 1);
|
||||
assert_eq!(packet.subpackets.len(), 1);
|
||||
if let SubpacketBody::RachAttempt(attempt) = &packet.subpackets[0].body {
|
||||
assert_eq!(attempt.header, header);
|
||||
assert_eq!(attempt.get_msg1(), msg1.as_ref());
|
||||
assert_eq!(attempt.get_msg2(), msg2.as_ref());
|
||||
assert_eq!(attempt.get_msg3(), msg3.as_ref());
|
||||
assert_eq!(attempt.additional_info, additional_info);
|
||||
} else {
|
||||
panic!("not rach attempt {:?}", packet.subpackets[0].body);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rach_attempt_parsing() {
|
||||
let test_packets = mac_rach_test_packets_from_scat();
|
||||
assert_rach_subpacket(
|
||||
&test_packets[0],
|
||||
rach::AttemptHeader::V2 {
|
||||
num_attempt: 1,
|
||||
rach_result: 0,
|
||||
contention: 1,
|
||||
msg_bitmask: 7,
|
||||
},
|
||||
Some(Msg1::V2 {
|
||||
preamble_index: 27,
|
||||
preamble_index_mask: 255,
|
||||
preamble_power_offset: -104,
|
||||
}),
|
||||
Some(Msg2 {
|
||||
backoff: 0,
|
||||
result: 1,
|
||||
tc_rnti: 6691,
|
||||
ta: 4,
|
||||
}),
|
||||
Some(Msg3 {
|
||||
grant: Msg3Grant::V1 { grant: 72728 },
|
||||
unk_grant: 7,
|
||||
harq_id: 6,
|
||||
mac_pdu: [0x00, 0x46, 0x5c, 0x80, 0xbd, 0x06, 0x48, 0x00, 0x00, 0x00],
|
||||
}),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_rach_subpacket(
|
||||
&test_packets[1],
|
||||
rach::AttemptHeader::V3 {
|
||||
sub_id: 1,
|
||||
cell_id: 0,
|
||||
num_attempt: 1,
|
||||
rach_result: 0,
|
||||
contention: 1,
|
||||
msg_bitmask: 7,
|
||||
},
|
||||
Some(Msg1::V3Or31 {
|
||||
preamble_index: 24,
|
||||
preamble_index_mask: 255,
|
||||
preamble_power_offset: -92,
|
||||
}),
|
||||
Some(Msg2 {
|
||||
backoff: 0,
|
||||
result: 1,
|
||||
tc_rnti: 25030,
|
||||
ta: 11,
|
||||
}),
|
||||
Some(Msg3 {
|
||||
grant: Msg3Grant::V1 { grant: 41652 },
|
||||
unk_grant: 18,
|
||||
harq_id: 1,
|
||||
mac_pdu: [0x20, 0x06, 0x1f, 0x42, 0x3f, 0x8d, 0x95, 0x07, 0x58, 0x00],
|
||||
}),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_rach_subpacket(
|
||||
&test_packets[2],
|
||||
rach::AttemptHeader::V3 {
|
||||
sub_id: 1,
|
||||
cell_id: 0,
|
||||
num_attempt: 1,
|
||||
rach_result: 0,
|
||||
contention: 0,
|
||||
msg_bitmask: 3,
|
||||
},
|
||||
Some(Msg1::V3Or31 {
|
||||
preamble_index: 63,
|
||||
preamble_index_mask: 0,
|
||||
preamble_power_offset: -104,
|
||||
}),
|
||||
Some(Msg2 {
|
||||
backoff: 0,
|
||||
result: 1,
|
||||
tc_rnti: 27452,
|
||||
ta: 7,
|
||||
}),
|
||||
None,
|
||||
Some(AdditionalInfo {
|
||||
ul_earfcn: 19300,
|
||||
p_max: 24,
|
||||
scell_id: 0,
|
||||
unk1: 1,
|
||||
unk2: 1237,
|
||||
}),
|
||||
);
|
||||
|
||||
assert_rach_subpacket(
|
||||
&test_packets[3],
|
||||
AttemptHeader::V3 {
|
||||
sub_id: 1,
|
||||
cell_id: 0,
|
||||
num_attempt: 1,
|
||||
rach_result: 0,
|
||||
contention: 1,
|
||||
msg_bitmask: 7,
|
||||
},
|
||||
Some(Msg1::V3Or31 {
|
||||
preamble_index: 10,
|
||||
preamble_index_mask: 255,
|
||||
preamble_power_offset: -104,
|
||||
}),
|
||||
Some(Msg2 {
|
||||
backoff: 0,
|
||||
result: 1,
|
||||
tc_rnti: 18460,
|
||||
ta: 7,
|
||||
}),
|
||||
Some(Msg3 {
|
||||
grant: Msg3Grant::V1 { grant: 57880 },
|
||||
unk_grant: 7,
|
||||
harq_id: 0,
|
||||
mac_pdu: [0x00, 0x52, 0x3b, 0x7d, 0xfd, 0x69, 0xb6, 0x00, 0x00, 0x00],
|
||||
}),
|
||||
Some(AdditionalInfo {
|
||||
ul_earfcn: 21749,
|
||||
p_max: 255,
|
||||
scell_id: 0,
|
||||
unk1: 1,
|
||||
unk2: 1238,
|
||||
}),
|
||||
);
|
||||
|
||||
assert_rach_subpacket(
|
||||
&test_packets[4],
|
||||
AttemptHeader::V3 {
|
||||
sub_id: 1,
|
||||
cell_id: 0,
|
||||
num_attempt: 1,
|
||||
rach_result: 0,
|
||||
contention: 0,
|
||||
msg_bitmask: 3,
|
||||
},
|
||||
Some(Msg1::V32 {
|
||||
preamble_index: 41,
|
||||
preamble_index_mask: 0,
|
||||
preamble_power_offset: -92,
|
||||
unk1: 235,
|
||||
group: 0,
|
||||
}),
|
||||
Some(Msg2 {
|
||||
backoff: 0,
|
||||
result: 1,
|
||||
tc_rnti: 46741,
|
||||
ta: 3,
|
||||
}),
|
||||
None,
|
||||
Some(AdditionalInfo {
|
||||
ul_earfcn: 24200,
|
||||
p_max: 23,
|
||||
scell_id: 0,
|
||||
unk1: 1,
|
||||
unk2: 1381,
|
||||
}),
|
||||
);
|
||||
|
||||
assert_rach_subpacket(
|
||||
&test_packets[5],
|
||||
AttemptHeader::V3 {
|
||||
sub_id: 1,
|
||||
cell_id: 0,
|
||||
num_attempt: 1,
|
||||
rach_result: 0,
|
||||
contention: 1,
|
||||
msg_bitmask: 7,
|
||||
},
|
||||
Some(Msg1::V32 {
|
||||
preamble_index: 19,
|
||||
preamble_index_mask: 255,
|
||||
preamble_power_offset: -96,
|
||||
unk1: 235,
|
||||
group: 0,
|
||||
}),
|
||||
Some(Msg2 {
|
||||
backoff: 0,
|
||||
result: 1,
|
||||
tc_rnti: 23213,
|
||||
ta: 5,
|
||||
}),
|
||||
Some(Msg3 {
|
||||
grant: Msg3Grant::V32 { grant: 83636 },
|
||||
unk_grant: 18,
|
||||
harq_id: 4,
|
||||
mac_pdu: [0x20, 0x06, 0x1f, 0x42, 0x5d, 0xc9, 0xbe, 0x41, 0xb4, 0x00],
|
||||
}),
|
||||
Some(AdditionalInfo {
|
||||
ul_earfcn: 21350,
|
||||
p_max: 24,
|
||||
scell_id: 0,
|
||||
unk1: 1,
|
||||
unk2: 1306,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
//! Diag ML1 measurement log serialization/deserialization. As with most of our
|
||||
//! diag parsers, these structs were derived from SCAT:
|
||||
//! Neighbor cell measurements: https://github.com/fgsect/scat/blob/9763cb5b1dcd5ee980f5b0ead9a8d520c8c51a51/src/scat/parsers/qualcomm/diagltelogparser.py#L192
|
||||
//! Serving cell measurements: https://github.com/fgsect/scat/blob/9763cb5b1dcd5ee980f5b0ead9a8d520c8c51a51/src/scat/parsers/qualcomm/diagltelogparser.py#L114
|
||||
|
||||
use deku::ctx::Order;
|
||||
use deku::prelude::*;
|
||||
|
||||
fn decode_rsrp(rsrp: u16) -> f32 {
|
||||
rsrp as f32 / 16.0 - 180.0
|
||||
}
|
||||
|
||||
fn decode_rssi(rssi: u16) -> f32 {
|
||||
rssi as f32 / 16.0 - 110.0
|
||||
}
|
||||
|
||||
fn decode_rsrq(rsrq: u16) -> f32 {
|
||||
rsrq as f32 / 16.0 - 30.0
|
||||
}
|
||||
|
||||
pub mod serving_cell {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(bit_order = "lsb")]
|
||||
pub struct MeasurementAndEvaluation {
|
||||
pub header: MeasurementAndEvaluationHeader,
|
||||
#[deku(bits = 12, pad_bits_after = "20")]
|
||||
meas_rsrp: u16,
|
||||
#[deku(bits = 12, pad_bits_after = "20")]
|
||||
avg_rsrp: u16,
|
||||
#[deku(bits = 10, pad_bits_after = "22")]
|
||||
meas_rsrq: u16,
|
||||
#[deku(pad_bits_before = "10", bits = 11, pad_bits_after = "11")]
|
||||
meas_rssi: u16,
|
||||
rxlev: u32,
|
||||
s_search: u32,
|
||||
#[deku(cond = "header.get_rrc_rel() == 0x01")]
|
||||
r9_data: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "_: Order", id_type = "u8", bit_order = "lsb")]
|
||||
pub enum MeasurementAndEvaluationHeader {
|
||||
#[deku(id = "4")]
|
||||
V4 {
|
||||
rrc_rel: u8,
|
||||
_reserved: u16,
|
||||
earfcn: u16,
|
||||
#[deku(bits = 9)]
|
||||
pci: u16,
|
||||
#[deku(bits = 7)]
|
||||
serv_layer_priority: u8,
|
||||
},
|
||||
#[deku(id = "5")]
|
||||
V5 {
|
||||
rrc_rel: u8,
|
||||
_reserved: u16,
|
||||
earfcn: u32,
|
||||
#[deku(bits = 9)]
|
||||
pci: u16,
|
||||
#[deku(bits = 7, pad_bytes_after = "2")]
|
||||
serv_layer_priority: u8,
|
||||
},
|
||||
}
|
||||
|
||||
impl MeasurementAndEvaluationHeader {
|
||||
fn get_rrc_rel(&self) -> u8 {
|
||||
match self {
|
||||
MeasurementAndEvaluationHeader::V4 { rrc_rel, .. } => *rrc_rel,
|
||||
MeasurementAndEvaluationHeader::V5 { rrc_rel, .. } => *rrc_rel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MeasurementAndEvaluation {
|
||||
pub fn get_pci(&self) -> u16 {
|
||||
match &self.header {
|
||||
MeasurementAndEvaluationHeader::V4 { pci, .. } => *pci,
|
||||
MeasurementAndEvaluationHeader::V5 { pci, .. } => *pci,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_earfcn(&self) -> u32 {
|
||||
match &self.header {
|
||||
MeasurementAndEvaluationHeader::V4 { earfcn, .. } => *earfcn as u32,
|
||||
MeasurementAndEvaluationHeader::V5 { earfcn, .. } => *earfcn,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_meas_rsrp(&self) -> f32 {
|
||||
decode_rsrp(self.meas_rsrp)
|
||||
}
|
||||
|
||||
pub fn get_meas_rssi(&self) -> f32 {
|
||||
decode_rssi(self.meas_rssi)
|
||||
}
|
||||
|
||||
pub fn get_meas_rsrq(&self) -> f32 {
|
||||
decode_rsrq(self.meas_rsrq)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod neighbor_cells {
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, DekuRead, DekuWrite, PartialEq)]
|
||||
#[deku(id_type = "u8", bit_order = "lsb")]
|
||||
pub enum MeasurementsHeader {
|
||||
#[deku(id = "4")]
|
||||
V4 {
|
||||
rrc_rel: u8,
|
||||
_reserved1: u16,
|
||||
earfcn: u16,
|
||||
#[deku(bits = 6)]
|
||||
q_rxlevmin: u8,
|
||||
#[deku(bits = 10)]
|
||||
n_cells: u16,
|
||||
},
|
||||
#[deku(id = "5")]
|
||||
V5 {
|
||||
rrc_rel: u8,
|
||||
_reserved1: u16,
|
||||
earfcn: u32,
|
||||
#[deku(bits = 6)]
|
||||
q_rxlevmin: u8,
|
||||
#[deku(bits = 26)]
|
||||
n_cells: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl MeasurementsHeader {
|
||||
fn get_n_cells(&self) -> usize {
|
||||
match self {
|
||||
MeasurementsHeader::V4 { n_cells, .. } => *n_cells as usize,
|
||||
MeasurementsHeader::V5 { n_cells, .. } => *n_cells as usize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, DekuRead, DekuWrite, PartialEq)]
|
||||
pub struct Measurements {
|
||||
pub header: MeasurementsHeader,
|
||||
#[deku(count = "header.get_n_cells()")]
|
||||
pub cells: Vec<MeasurementsCell>,
|
||||
}
|
||||
|
||||
impl Measurements {
|
||||
pub fn get_earfcn(&self) -> u32 {
|
||||
match &self.header {
|
||||
MeasurementsHeader::V4 { earfcn, .. } => *earfcn as u32,
|
||||
MeasurementsHeader::V5 { earfcn, .. } => *earfcn,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, DekuRead, DekuWrite, PartialEq)]
|
||||
#[deku(bit_order = "lsb")]
|
||||
pub struct MeasurementsCell {
|
||||
#[deku(bits = 9)]
|
||||
pub pci: u16,
|
||||
#[deku(bits = 11)]
|
||||
meas_rssi: u16,
|
||||
#[deku(bits = 12)]
|
||||
meas_rsrp: u16,
|
||||
#[deku(pad_bits_before = "12", bits = 12, pad_bits_after = "8")]
|
||||
avg_rsrp: u16,
|
||||
#[deku(pad_bits_before = "12", bits = 10, pad_bits_after = "10")]
|
||||
meas_rsrq: u16,
|
||||
#[deku(bits = 10, pad_bits_after = "10")]
|
||||
avg_rsrq: u16,
|
||||
#[deku(bits = 6, pad_bits_after = "6")]
|
||||
s_rxlev: u8,
|
||||
n_freq_offset: u16,
|
||||
val5: u16,
|
||||
ant0_offset: u32,
|
||||
ant1_offset: u32,
|
||||
unk1: u32,
|
||||
}
|
||||
|
||||
impl MeasurementsCell {
|
||||
pub fn get_meas_rsrp(&self) -> f32 {
|
||||
decode_rsrp(self.meas_rsrp)
|
||||
}
|
||||
|
||||
pub fn get_meas_rssi(&self) -> f32 {
|
||||
decode_rssi(self.meas_rssi)
|
||||
}
|
||||
|
||||
pub fn get_meas_rsrq(&self) -> f32 {
|
||||
decode_rsrq(self.meas_rsrq)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
//! The tests for serving cell/neighbor cell measurements were adapted from
|
||||
//! SCAT's tests. The expected values were collected by modifying SCAT to
|
||||
//! print out the full-precision expected values. See:
|
||||
//! https://github.com/wgreenberg/scat/commit/e53d657861e8a66b52d635ff9518ac896c23ab06
|
||||
|
||||
use super::*;
|
||||
use crate::diag::diaglog::LogBody;
|
||||
use crate::log_codes::{LOG_LTE_ML1_NEIGHBOR_MEAS, LOG_LTE_ML1_SERVING_CELL_MEAS_AND_EVAL_C};
|
||||
use crate::test_util::unhexlify;
|
||||
use std::io::Seek;
|
||||
|
||||
fn parse_ncell_measurements(hexlified_bytes: &str) -> (u8, neighbor_cells::Measurements) {
|
||||
let (total_size, mut reader) = unhexlify(hexlified_bytes);
|
||||
match LogBody::from_reader_with_ctx(&mut reader, (LOG_LTE_ML1_NEIGHBOR_MEAS as u16, 0)) {
|
||||
Ok(LogBody::LteMl1NeighborCellsMeasurements { data }) => {
|
||||
if !reader.end() {
|
||||
let leftover_bits = reader.rest();
|
||||
let leftover_bytes = total_size - reader.stream_position().unwrap() as usize;
|
||||
panic!(
|
||||
"failed to read entire buffer ({} bytes, {} bits left)",
|
||||
leftover_bytes,
|
||||
leftover_bits.len()
|
||||
);
|
||||
}
|
||||
let pkt_version = match data.header {
|
||||
neighbor_cells::MeasurementsHeader::V4 { .. } => 4,
|
||||
neighbor_cells::MeasurementsHeader::V5 { .. } => 5,
|
||||
};
|
||||
(pkt_version, data)
|
||||
}
|
||||
Ok(x) => panic!("expected MeasurementAndEvaluation, but parsed {:?}", x),
|
||||
Err(x) => panic!("failed to parse MeasurementAndEvaluation {:?}", x),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_meas_eval(hexlified_bytes: &str) -> (u8, serving_cell::MeasurementAndEvaluation) {
|
||||
let (total_size, mut reader) = unhexlify(hexlified_bytes);
|
||||
match LogBody::from_reader_with_ctx(
|
||||
&mut reader,
|
||||
(LOG_LTE_ML1_SERVING_CELL_MEAS_AND_EVAL_C as u16, 0),
|
||||
) {
|
||||
Ok(LogBody::LteMl1ServingCellMeasurementAndEvaluation { data }) => {
|
||||
if !reader.end() {
|
||||
let leftover_bits = reader.rest();
|
||||
let leftover_bytes = total_size - reader.stream_position().unwrap() as usize;
|
||||
panic!(
|
||||
"failed to read entire buffer ({} bytes, {} bits left)",
|
||||
leftover_bytes,
|
||||
leftover_bits.len()
|
||||
);
|
||||
}
|
||||
let pkt_version = match data.header {
|
||||
serving_cell::MeasurementAndEvaluationHeader::V4 { .. } => 4,
|
||||
serving_cell::MeasurementAndEvaluationHeader::V5 { .. } => 5,
|
||||
};
|
||||
(pkt_version, data)
|
||||
}
|
||||
Ok(x) => panic!("expected MeasurementAndEvaluation, but parsed {:?}", x),
|
||||
Err(x) => panic!("failed to parse MeasurementAndEvaluation {:?}", x),
|
||||
}
|
||||
}
|
||||
|
||||
fn scell_meas_and_eval_case(
|
||||
hexlified_bytes: &str,
|
||||
pkt_version: u8,
|
||||
pci: u16,
|
||||
earfcn: u32,
|
||||
rsrp: f32,
|
||||
rsrq: f32,
|
||||
rssi: f32,
|
||||
) {
|
||||
let (parsed_pkt_version, data) = parse_meas_eval(hexlified_bytes);
|
||||
assert_eq!(parsed_pkt_version, pkt_version);
|
||||
assert_eq!(data.get_pci(), pci, "incorrect pci");
|
||||
assert_eq!(data.get_earfcn(), earfcn, "incorrect earfcn");
|
||||
assert_eq!(data.get_meas_rsrp(), rsrp, "incorrect rsrp");
|
||||
assert_eq!(data.get_meas_rsrq(), rsrq, "incorrect rsrq");
|
||||
assert_eq!(data.get_meas_rssi(), rssi, "incorrect rssi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scell_meas() {
|
||||
scell_meas_and_eval_case(
|
||||
"040100009C18D60AECC44E00E2244E00FFFCE30FFED80A0047AD56021D310100A2624100",
|
||||
4,
|
||||
214,
|
||||
6300,
|
||||
-101.25,
|
||||
-14.0625,
|
||||
-66.625,
|
||||
);
|
||||
scell_meas_and_eval_case(
|
||||
"05010000160d0000d40e00004bb444005444450039e514133149070048adfe019f310100a23f0000",
|
||||
5,
|
||||
212,
|
||||
3350,
|
||||
-111.3125,
|
||||
-10.4375,
|
||||
-80.875,
|
||||
);
|
||||
scell_meas_and_eval_case(
|
||||
"05010000f424000a4d43434d4e434d41524b45527c307c3236327c317c34323330333233347c7c4d",
|
||||
5,
|
||||
333,
|
||||
167781620,
|
||||
-127.125,
|
||||
-22.25,
|
||||
2.75,
|
||||
);
|
||||
scell_meas_and_eval_case(
|
||||
"0501000000190000a90d0000d9944d00d9944d006081d5d55d2568bc48ad3e027f314fe0891900e0",
|
||||
5,
|
||||
425,
|
||||
6400,
|
||||
-102.4375,
|
||||
-8.0,
|
||||
-77.4375,
|
||||
);
|
||||
}
|
||||
|
||||
fn ncell_meas_case(
|
||||
hexlified_bytes: &str,
|
||||
pkt_version: u8,
|
||||
earfcn: u32,
|
||||
cells: Vec<(u16, f32, f32, f32)>,
|
||||
) {
|
||||
let (parsed_pkt_version, data) = parse_ncell_measurements(hexlified_bytes);
|
||||
assert_eq!(parsed_pkt_version, pkt_version, "incorrect pkt_version");
|
||||
assert_eq!(data.cells.len(), cells.len(), "incorrect number of cells");
|
||||
assert_eq!(data.get_earfcn(), earfcn, "incorrect earfcn");
|
||||
for (parsed, (pci, rsrp, rssi, rsrq)) in data.cells.iter().zip(cells) {
|
||||
assert_eq!(parsed.pci, pci, "incorrect pci");
|
||||
assert_eq!(parsed.get_meas_rsrp(), rsrp, "incorrect rsrp");
|
||||
assert_eq!(parsed.get_meas_rssi(), rssi, "incorrect rssi");
|
||||
assert_eq!(parsed.get_meas_rsrq(), rsrq, "incorrect rsrq");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ncell_meas() {
|
||||
ncell_meas_case(
|
||||
"040100009C1847008348E44DDEA44C00CAB4CC32B6D8420300000000FF773301FF77330122020100",
|
||||
4,
|
||||
6300,
|
||||
vec![(131, -102.125, -75.75, -17.3125)],
|
||||
);
|
||||
ncell_meas_case(
|
||||
"05010000160d0000480000006cea413bb4433b00b4f3cc33cf3c130200000000ffefc00fffefc00f45081600",
|
||||
5,
|
||||
3350,
|
||||
vec![(108, -120.75, -94.6875, -17.0625)],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,156 +1,12 @@
|
||||
//! Diag protocol serialization/deserialization
|
||||
//! Diag LogBody serialization/deserialization
|
||||
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use crc::{Algorithm, Crc};
|
||||
use deku::prelude::*;
|
||||
|
||||
use crate::hdlc::{self, hdlc_decapsulate};
|
||||
use log::warn;
|
||||
use thiserror::Error;
|
||||
|
||||
pub const MESSAGE_TERMINATOR: u8 = 0x7e;
|
||||
pub const MESSAGE_ESCAPE_CHAR: u8 = 0x7d;
|
||||
|
||||
pub const ESCAPED_MESSAGE_TERMINATOR: u8 = 0x5e;
|
||||
pub const ESCAPED_MESSAGE_ESCAPE_CHAR: u8 = 0x5d;
|
||||
|
||||
#[derive(Debug, Clone, DekuWrite)]
|
||||
pub struct RequestContainer {
|
||||
pub data_type: DataType,
|
||||
#[deku(skip)]
|
||||
pub use_mdm: bool,
|
||||
#[deku(skip, cond = "!*use_mdm")]
|
||||
pub mdm_field: i32,
|
||||
pub hdlc_encapsulated_request: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuWrite)]
|
||||
#[deku(id_type = "u32")]
|
||||
pub enum Request {
|
||||
#[deku(id = "115")]
|
||||
LogConfig(LogConfigRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuWrite)]
|
||||
#[deku(id_type = "u32", endian = "little")]
|
||||
pub enum LogConfigRequest {
|
||||
#[deku(id = "1")]
|
||||
RetrieveIdRanges,
|
||||
|
||||
#[deku(id = "3")]
|
||||
SetMask {
|
||||
log_type: u32,
|
||||
log_mask_bitsize: u32,
|
||||
log_mask: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(id_type = "u32", endian = "little")]
|
||||
pub enum DataType {
|
||||
#[deku(id = "32")]
|
||||
UserSpace,
|
||||
#[deku(id_pat = "_")]
|
||||
Other(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Error)]
|
||||
pub enum DiagParsingError {
|
||||
#[error("Failed to parse Message: {0}, data: {1:?}")]
|
||||
MessageParsingError(deku::DekuError, Vec<u8>),
|
||||
#[error("HDLC decapsulation of message failed: {0}, data: {1:?}")]
|
||||
HdlcDecapsulationError(hdlc::HdlcError, Vec<u8>),
|
||||
}
|
||||
|
||||
// this is sorta based on the params qcsuper uses, plus what seems to be used in
|
||||
// https://github.com/fgsect/scat/blob/f1538b397721df3ab8ba12acd26716abcf21f78b/util.py#L47
|
||||
pub const CRC_CCITT_ALG: Algorithm<u16> = Algorithm {
|
||||
poly: 0x1021,
|
||||
init: 0xffff,
|
||||
refin: true,
|
||||
refout: true,
|
||||
width: 16,
|
||||
xorout: 0xffff,
|
||||
check: 0x2189,
|
||||
residue: 0x0000,
|
||||
};
|
||||
|
||||
pub const CRC_CCITT: Crc<u16> = Crc::<u16>::new(&CRC_CCITT_ALG);
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
pub struct MessagesContainer {
|
||||
pub data_type: DataType,
|
||||
pub num_messages: u32,
|
||||
#[deku(count = "num_messages")]
|
||||
pub messages: Vec<HdlcEncapsulatedMessage>,
|
||||
}
|
||||
|
||||
impl MessagesContainer {
|
||||
pub fn messages(&self) -> Vec<Result<Message, DiagParsingError>> {
|
||||
let mut result = Vec::new();
|
||||
for msg in &self.messages {
|
||||
for sub_msg in msg.data.split_inclusive(|&b| b == MESSAGE_TERMINATOR) {
|
||||
match hdlc_decapsulate(sub_msg, &CRC_CCITT) {
|
||||
Ok(data) => match Message::from_bytes((&data, 0)) {
|
||||
Ok(((leftover_bytes, _), res)) => {
|
||||
if !leftover_bytes.is_empty() {
|
||||
warn!(
|
||||
"warning: {} leftover bytes when parsing Message",
|
||||
leftover_bytes.len()
|
||||
);
|
||||
}
|
||||
result.push(Ok(res));
|
||||
}
|
||||
Err(e) => result.push(Err(DiagParsingError::MessageParsingError(e, data))),
|
||||
},
|
||||
Err(err) => result.push(Err(DiagParsingError::HdlcDecapsulationError(
|
||||
err,
|
||||
sub_msg.to_vec(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
pub struct HdlcEncapsulatedMessage {
|
||||
pub len: u32,
|
||||
#[deku(count = "len")]
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(id_type = "u8")]
|
||||
pub enum Message {
|
||||
#[deku(id = "16")]
|
||||
Log {
|
||||
pending_msgs: u8,
|
||||
outer_length: u16,
|
||||
inner_length: u16,
|
||||
log_type: u16,
|
||||
timestamp: Timestamp,
|
||||
// pass the log type and log length (inner_length - (sizeof(log_type) + sizeof(timestamp)))
|
||||
#[deku(ctx = "*log_type, inner_length.saturating_sub(12)")]
|
||||
body: LogBody,
|
||||
},
|
||||
|
||||
// kinda unpleasant deku hackery here. deku expects an enum's variant to be
|
||||
// right before its data, but in this case, a status value comes between the
|
||||
// variants and the data. so we need to use deku's context (ctx) feature to
|
||||
// pass those opcodes down to their respective parsers.
|
||||
#[deku(id_pat = "_")]
|
||||
Response {
|
||||
opcode1: u8, // the "id" (from deku's POV) gets parsed into this field
|
||||
opcode2: u8,
|
||||
opcode3: u8,
|
||||
opcode4: u8,
|
||||
subopcode: u32,
|
||||
status: u32,
|
||||
#[deku(ctx = "u32::from_le_bytes([*opcode1, *opcode2, *opcode3, *opcode4]), *subopcode")]
|
||||
payload: ResponsePayload,
|
||||
},
|
||||
}
|
||||
pub mod ll1;
|
||||
pub mod mac;
|
||||
pub mod ml1;
|
||||
pub mod rrc;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "log_type: u16, hdr_len: u16", id = "log_type")]
|
||||
@@ -183,7 +39,7 @@ pub enum LogBody {
|
||||
LteRrcOtaMessage {
|
||||
ext_header_version: u8,
|
||||
#[deku(ctx = "*ext_header_version")]
|
||||
packet: LteRrcOtaPacket,
|
||||
packet: rrc::LteRrcOtaPacket,
|
||||
},
|
||||
// the four NAS command opcodes refer to:
|
||||
// * 0xb0e2: plain ESM NAS message (incoming)
|
||||
@@ -222,6 +78,22 @@ pub enum LogBody {
|
||||
#[deku(count = "hdr_len")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0xb17f")]
|
||||
LteMl1ServingCellMeasurementAndEvaluation {
|
||||
data: ml1::serving_cell::MeasurementAndEvaluation,
|
||||
},
|
||||
#[deku(id = "0xb180")]
|
||||
LteMl1NeighborCellsMeasurements {
|
||||
data: ml1::neighbor_cells::Measurements,
|
||||
},
|
||||
#[deku(id = "0xb062")]
|
||||
LteMacRachResponse { packet: mac::Packet },
|
||||
#[deku(id = "0xb063")]
|
||||
LteMacDl { packet: mac::Packet },
|
||||
#[deku(id = "0xb064")]
|
||||
LteMacUl { packet: mac::Packet },
|
||||
#[deku(id = "0xb114")]
|
||||
LteLl1ServingCellTiming { data: ll1::ServingCellTiming },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
@@ -237,113 +109,6 @@ pub enum Nas4GMessageDirection {
|
||||
Uplink,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "ext_header_version: u8", id = "ext_header_version")]
|
||||
pub enum LteRrcOtaPacket {
|
||||
#[deku(id_pat = "0..=4")]
|
||||
V0 {
|
||||
rrc_rel_maj: u8,
|
||||
rrc_rel_min: u8,
|
||||
bearer_id: u8,
|
||||
phy_cell_id: u16,
|
||||
earfcn: u16,
|
||||
sfn_subfn: u16,
|
||||
pdu_num: u8,
|
||||
len: u16,
|
||||
#[deku(count = "len")]
|
||||
packet: Vec<u8>,
|
||||
},
|
||||
#[deku(id_pat = "5..=7")]
|
||||
V5 {
|
||||
rrc_rel_maj: u8,
|
||||
rrc_rel_min: u8,
|
||||
bearer_id: u8,
|
||||
phy_cell_id: u16,
|
||||
earfcn: u16,
|
||||
sfn_subfn: u16,
|
||||
pdu_num: u8,
|
||||
sib_mask: u32,
|
||||
len: u16,
|
||||
#[deku(count = "len")]
|
||||
packet: Vec<u8>,
|
||||
},
|
||||
#[deku(id_pat = "8..=24")]
|
||||
V8 {
|
||||
rrc_rel_maj: u8,
|
||||
rrc_rel_min: u8,
|
||||
bearer_id: u8,
|
||||
phy_cell_id: u16,
|
||||
earfcn: u32,
|
||||
sfn_subfn: u16,
|
||||
pdu_num: u8,
|
||||
sib_mask: u32,
|
||||
len: u16,
|
||||
#[deku(count = "len")]
|
||||
packet: Vec<u8>,
|
||||
},
|
||||
#[deku(id_pat = "25..")]
|
||||
V25 {
|
||||
rrc_rel_maj: u8,
|
||||
rrc_rel_min: u8,
|
||||
nr_rrc_rel_maj: u8,
|
||||
nr_rrc_rel_min: u8,
|
||||
bearer_id: u8,
|
||||
phy_cell_id: u16,
|
||||
earfcn: u32,
|
||||
sfn_subfn: u16,
|
||||
pdu_num: u8,
|
||||
sib_mask: u32,
|
||||
len: u16,
|
||||
#[deku(count = "len")]
|
||||
packet: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
impl LteRrcOtaPacket {
|
||||
fn get_sfn_subfn(&self) -> u16 {
|
||||
match self {
|
||||
LteRrcOtaPacket::V0 { sfn_subfn, .. } => *sfn_subfn,
|
||||
LteRrcOtaPacket::V5 { sfn_subfn, .. } => *sfn_subfn,
|
||||
LteRrcOtaPacket::V8 { sfn_subfn, .. } => *sfn_subfn,
|
||||
LteRrcOtaPacket::V25 { sfn_subfn, .. } => *sfn_subfn,
|
||||
}
|
||||
}
|
||||
pub fn get_sfn(&self) -> u32 {
|
||||
self.get_sfn_subfn() as u32 >> 4
|
||||
}
|
||||
|
||||
pub fn get_subfn(&self) -> u8 {
|
||||
(self.get_sfn_subfn() & 0xf) as u8
|
||||
}
|
||||
|
||||
pub fn get_pdu_num(&self) -> u8 {
|
||||
match self {
|
||||
LteRrcOtaPacket::V0 { pdu_num, .. } => *pdu_num,
|
||||
LteRrcOtaPacket::V5 { pdu_num, .. } => *pdu_num,
|
||||
LteRrcOtaPacket::V8 { pdu_num, .. } => *pdu_num,
|
||||
LteRrcOtaPacket::V25 { pdu_num, .. } => *pdu_num,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_earfcn(&self) -> u32 {
|
||||
match self {
|
||||
LteRrcOtaPacket::V0 { earfcn, .. } => *earfcn as u32,
|
||||
LteRrcOtaPacket::V5 { earfcn, .. } => *earfcn as u32,
|
||||
LteRrcOtaPacket::V8 { earfcn, .. } => *earfcn,
|
||||
LteRrcOtaPacket::V25 { earfcn, .. } => *earfcn,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_payload(self) -> Vec<u8> {
|
||||
match self {
|
||||
LteRrcOtaPacket::V0 { packet, .. } => packet,
|
||||
LteRrcOtaPacket::V5 { packet, .. } => packet,
|
||||
LteRrcOtaPacket::V8 { packet, .. } => packet,
|
||||
LteRrcOtaPacket::V25 { packet, .. } => packet,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(endian = "little")]
|
||||
pub struct Timestamp {
|
||||
@@ -364,58 +129,92 @@ impl Timestamp {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "opcode: u32, subopcode: u32", id = "opcode")]
|
||||
pub enum ResponsePayload {
|
||||
#[deku(id = "115")]
|
||||
LogConfig(#[deku(ctx = "subopcode")] LogConfigResponse),
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test {
|
||||
use super::*;
|
||||
use crate::{diag::*, hdlc, log_codes};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "subopcode: u32", id = "subopcode")]
|
||||
pub enum LogConfigResponse {
|
||||
#[deku(id = "1")]
|
||||
RetrieveIdRanges { log_mask_sizes: [u32; 16] },
|
||||
|
||||
#[deku(id = "3")]
|
||||
SetMask,
|
||||
}
|
||||
|
||||
pub fn build_log_mask_request(
|
||||
log_type: u32,
|
||||
log_mask_bitsize: u32,
|
||||
accepted_log_codes: &[u32],
|
||||
) -> Request {
|
||||
let mut current_byte: u8 = 0;
|
||||
let mut num_bits_written: u8 = 0;
|
||||
let mut log_mask: Vec<u8> = vec![];
|
||||
for i in 0..log_mask_bitsize {
|
||||
let log_code: u32 = (log_type << 12) | i;
|
||||
if accepted_log_codes.contains(&log_code) {
|
||||
current_byte |= 1 << num_bits_written;
|
||||
}
|
||||
num_bits_written += 1;
|
||||
|
||||
if num_bits_written == 8 || i == log_mask_bitsize - 1 {
|
||||
log_mask.push(current_byte);
|
||||
current_byte = 0;
|
||||
num_bits_written = 0;
|
||||
}
|
||||
#[test]
|
||||
fn test_logs() {
|
||||
let data = vec![
|
||||
16, 0, 38, 0, 38, 0, 192, 176, 26, 165, 245, 135, 118, 35, 2, 1, 20, 14, 48, 0, 160, 0,
|
||||
2, 8, 0, 0, 217, 15, 5, 0, 0, 0, 0, 7, 0, 64, 1, 238, 173, 213, 77, 208,
|
||||
];
|
||||
let msg = Message::from_bytes((&data, 0)).unwrap().1;
|
||||
assert_eq!(
|
||||
msg,
|
||||
Message::Log {
|
||||
pending_msgs: 0,
|
||||
outer_length: 38,
|
||||
inner_length: 38,
|
||||
log_type: 0xb0c0,
|
||||
timestamp: Timestamp {
|
||||
ts: 72659535985485082
|
||||
},
|
||||
body: LogBody::LteRrcOtaMessage {
|
||||
ext_header_version: 20,
|
||||
packet: rrc::LteRrcOtaPacket::V8 {
|
||||
rrc_rel_maj: 14,
|
||||
rrc_rel_min: 48,
|
||||
bearer_id: 0,
|
||||
phy_cell_id: 160,
|
||||
earfcn: 2050,
|
||||
sfn_subfn: 4057,
|
||||
pdu_num: 5,
|
||||
sib_mask: 0,
|
||||
len: 7,
|
||||
packet: vec![0x40, 0x1, 0xee, 0xad, 0xd5, 0x4d, 0xd0],
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Request::LogConfig(LogConfigRequest::SetMask {
|
||||
log_type,
|
||||
log_mask_bitsize,
|
||||
log_mask,
|
||||
})
|
||||
}
|
||||
#[test]
|
||||
fn test_fuzz_crash_inner_length_underflow() {
|
||||
// Regression test: inner_length < 12 previously caused panic.
|
||||
// Fixed by using saturating_sub in Message::Log body length calculation.
|
||||
let fuzz_data = b"\x10\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||
let _ = Message::from_bytes((fuzz_data, 0));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_fuzz_crash_nas_hdr_len_underflow() {
|
||||
// Regression test for two things:
|
||||
// - hdr_len < 4 previously caused panic in Nas4GMessage.
|
||||
// - Upgrading to deku 0.20 caused incorrect parsing behavior (double-read of discriminant)
|
||||
let nas_msg =
|
||||
b"\x10\x00\x14\x00\x02\x00\xe2\xb0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00";
|
||||
|
||||
let ((rest, _), msg) = Message::from_bytes((nas_msg, 0)).unwrap();
|
||||
|
||||
assert_eq!(rest.len(), 0);
|
||||
assert!(
|
||||
matches!(
|
||||
msg,
|
||||
Message::Log {
|
||||
log_type: 0xb0e2,
|
||||
body: LogBody::Nas4GMessage {
|
||||
direction: Nas4GMessageDirection::Downlink,
|
||||
..
|
||||
},
|
||||
..
|
||||
}
|
||||
),
|
||||
"Unexpected message: {:?}",
|
||||
msg
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_crash_ip_traffic_hdr_len_underflow() {
|
||||
// Regression test: hdr_len < 8 previously caused panic in IpTraffic.
|
||||
// Fixed by using saturating_sub for msg length calculation.
|
||||
let ip_msg = b"\x10\x00\x14\x00\x02\x00\xeb\x11\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00";
|
||||
let _ = Message::from_bytes((ip_msg, 0));
|
||||
}
|
||||
|
||||
// Just about all of these test cases from manually parsing diag packets w/ QCSuper
|
||||
|
||||
#[test]
|
||||
fn test_request_serialization() {
|
||||
let req = Request::LogConfig(LogConfigRequest::RetrieveIdRanges);
|
||||
@@ -439,7 +238,17 @@ mod test {
|
||||
let req = build_log_mask_request(
|
||||
log_type,
|
||||
bitsize,
|
||||
&crate::diag_device::LOG_CODES_FOR_RAW_PACKET_LOGGING,
|
||||
&[
|
||||
log_codes::LOG_GSM_RR_SIGNALING_MESSAGE_C,
|
||||
log_codes::WCDMA_SIGNALLING_MESSAGE,
|
||||
log_codes::LOG_LTE_RRC_OTA_MSG_LOG_C,
|
||||
log_codes::LOG_NR_RRC_OTA_MSG_LOG_C,
|
||||
log_codes::LOG_UMTS_NAS_OTA_MESSAGE_LOG_PACKET_C,
|
||||
log_codes::LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C,
|
||||
log_codes::LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C,
|
||||
log_codes::LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C,
|
||||
log_codes::LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C,
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
req,
|
||||
@@ -447,11 +256,9 @@ mod test {
|
||||
log_type,
|
||||
log_mask_bitsize: bitsize,
|
||||
log_mask: vec![
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xc, 0x30, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
|
||||
0, 0, 12, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
],
|
||||
})
|
||||
);
|
||||
@@ -478,42 +285,6 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logs() {
|
||||
let data = vec![
|
||||
16, 0, 38, 0, 38, 0, 192, 176, 26, 165, 245, 135, 118, 35, 2, 1, 20, 14, 48, 0, 160, 0,
|
||||
2, 8, 0, 0, 217, 15, 5, 0, 0, 0, 0, 7, 0, 64, 1, 238, 173, 213, 77, 208,
|
||||
];
|
||||
let msg = Message::from_bytes((&data, 0)).unwrap().1;
|
||||
assert_eq!(
|
||||
msg,
|
||||
Message::Log {
|
||||
pending_msgs: 0,
|
||||
outer_length: 38,
|
||||
inner_length: 38,
|
||||
log_type: 0xb0c0,
|
||||
timestamp: Timestamp {
|
||||
ts: 72659535985485082
|
||||
},
|
||||
body: LogBody::LteRrcOtaMessage {
|
||||
ext_header_version: 20,
|
||||
packet: LteRrcOtaPacket::V8 {
|
||||
rrc_rel_maj: 14,
|
||||
rrc_rel_min: 48,
|
||||
bearer_id: 0,
|
||||
phy_cell_id: 160,
|
||||
earfcn: 2050,
|
||||
sfn_subfn: 4057,
|
||||
pdu_num: 5,
|
||||
sib_mask: 0,
|
||||
len: 7,
|
||||
packet: vec![0x40, 0x1, 0xee, 0xad, 0xd5, 0x4d, 0xd0],
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn make_container(data_type: DataType, message: HdlcEncapsulatedMessage) -> MessagesContainer {
|
||||
MessagesContainer {
|
||||
data_type,
|
||||
@@ -525,7 +296,7 @@ mod test {
|
||||
// this log is based on one captured on a real device -- if it fails to
|
||||
// serialize or deserialize, that's probably a problem with this mock, not
|
||||
// the DiagReader implementation
|
||||
fn get_test_message(payload: &[u8]) -> (HdlcEncapsulatedMessage, Message) {
|
||||
pub fn get_test_message(payload: &[u8]) -> (HdlcEncapsulatedMessage, Message) {
|
||||
let length_with_payload = 31 + payload.len() as u16;
|
||||
let message = Message::Log {
|
||||
pending_msgs: 0,
|
||||
@@ -537,7 +308,7 @@ mod test {
|
||||
},
|
||||
body: LogBody::LteRrcOtaMessage {
|
||||
ext_header_version: 20,
|
||||
packet: LteRrcOtaPacket::V8 {
|
||||
packet: diaglog::rrc::LteRrcOtaPacket::V8 {
|
||||
rrc_rel_maj: 14,
|
||||
rrc_rel_min: 48,
|
||||
bearer_id: 0,
|
||||
@@ -559,6 +330,8 @@ mod test {
|
||||
len: encapsulated_data.len() as u32,
|
||||
data: encapsulated_data,
|
||||
};
|
||||
// sanity check
|
||||
assert_eq!(&Message::from_hdlc(&encapsulated.data).unwrap(), &message);
|
||||
(encapsulated, message)
|
||||
}
|
||||
|
||||
@@ -619,50 +392,6 @@ mod test {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_crash_inner_length_underflow() {
|
||||
// Regression test: inner_length < 12 previously caused panic.
|
||||
// Fixed by using saturating_sub in Message::Log body length calculation.
|
||||
let fuzz_data = b"\x10\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||
let _ = Message::from_bytes((fuzz_data, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_crash_nas_hdr_len_underflow() {
|
||||
// Regression test for two things:
|
||||
// - hdr_len < 4 previously caused panic in Nas4GMessage.
|
||||
// - Upgrading to deku 0.20 caused incorrect parsing behavior (double-read of discriminant)
|
||||
let nas_msg =
|
||||
b"\x10\x00\x14\x00\x02\x00\xe2\xb0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00";
|
||||
|
||||
let ((rest, _), msg) = Message::from_bytes((nas_msg, 0)).unwrap();
|
||||
|
||||
assert_eq!(rest.len(), 0);
|
||||
assert!(
|
||||
matches!(
|
||||
msg,
|
||||
Message::Log {
|
||||
log_type: 0xb0e2,
|
||||
body: LogBody::Nas4GMessage {
|
||||
direction: Nas4GMessageDirection::Downlink,
|
||||
..
|
||||
},
|
||||
..
|
||||
}
|
||||
),
|
||||
"Unexpected message: {:?}",
|
||||
msg
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_crash_ip_traffic_hdr_len_underflow() {
|
||||
// Regression test: hdr_len < 8 previously caused panic in IpTraffic.
|
||||
// Fixed by using saturating_sub for msg length calculation.
|
||||
let ip_msg = b"\x10\x00\x14\x00\x02\x00\xeb\x11\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00";
|
||||
let _ = Message::from_bytes((ip_msg, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_crash_response_opcode_parsing() {
|
||||
// Regression test: Upgrading to deku 0.20 caused incorrect parsing of Response messages.
|
||||
@@ -0,0 +1,110 @@
|
||||
//! Diag LTE RRC serialization/deserialization
|
||||
|
||||
use deku::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "ext_header_version: u8", id = "ext_header_version")]
|
||||
pub enum LteRrcOtaPacket {
|
||||
#[deku(id_pat = "0..=4")]
|
||||
V0 {
|
||||
rrc_rel_maj: u8,
|
||||
rrc_rel_min: u8,
|
||||
bearer_id: u8,
|
||||
phy_cell_id: u16,
|
||||
earfcn: u16,
|
||||
sfn_subfn: u16,
|
||||
pdu_num: u8,
|
||||
len: u16,
|
||||
#[deku(count = "len")]
|
||||
packet: Vec<u8>,
|
||||
},
|
||||
#[deku(id_pat = "5..=7")]
|
||||
V5 {
|
||||
rrc_rel_maj: u8,
|
||||
rrc_rel_min: u8,
|
||||
bearer_id: u8,
|
||||
phy_cell_id: u16,
|
||||
earfcn: u16,
|
||||
sfn_subfn: u16,
|
||||
pdu_num: u8,
|
||||
sib_mask: u32,
|
||||
len: u16,
|
||||
#[deku(count = "len")]
|
||||
packet: Vec<u8>,
|
||||
},
|
||||
#[deku(id_pat = "8..=24")]
|
||||
V8 {
|
||||
rrc_rel_maj: u8,
|
||||
rrc_rel_min: u8,
|
||||
bearer_id: u8,
|
||||
phy_cell_id: u16,
|
||||
earfcn: u32,
|
||||
sfn_subfn: u16,
|
||||
pdu_num: u8,
|
||||
sib_mask: u32,
|
||||
len: u16,
|
||||
#[deku(count = "len")]
|
||||
packet: Vec<u8>,
|
||||
},
|
||||
#[deku(id_pat = "25..")]
|
||||
V25 {
|
||||
rrc_rel_maj: u8,
|
||||
rrc_rel_min: u8,
|
||||
nr_rrc_rel_maj: u8,
|
||||
nr_rrc_rel_min: u8,
|
||||
bearer_id: u8,
|
||||
phy_cell_id: u16,
|
||||
earfcn: u32,
|
||||
sfn_subfn: u16,
|
||||
pdu_num: u8,
|
||||
sib_mask: u32,
|
||||
len: u16,
|
||||
#[deku(count = "len")]
|
||||
packet: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
impl LteRrcOtaPacket {
|
||||
fn get_sfn_subfn(&self) -> u16 {
|
||||
match self {
|
||||
LteRrcOtaPacket::V0 { sfn_subfn, .. } => *sfn_subfn,
|
||||
LteRrcOtaPacket::V5 { sfn_subfn, .. } => *sfn_subfn,
|
||||
LteRrcOtaPacket::V8 { sfn_subfn, .. } => *sfn_subfn,
|
||||
LteRrcOtaPacket::V25 { sfn_subfn, .. } => *sfn_subfn,
|
||||
}
|
||||
}
|
||||
pub fn get_sfn(&self) -> u32 {
|
||||
self.get_sfn_subfn() as u32 >> 4
|
||||
}
|
||||
|
||||
pub fn get_subfn(&self) -> u8 {
|
||||
(self.get_sfn_subfn() & 0xf) as u8
|
||||
}
|
||||
|
||||
pub fn get_pdu_num(&self) -> u8 {
|
||||
match self {
|
||||
LteRrcOtaPacket::V0 { pdu_num, .. } => *pdu_num,
|
||||
LteRrcOtaPacket::V5 { pdu_num, .. } => *pdu_num,
|
||||
LteRrcOtaPacket::V8 { pdu_num, .. } => *pdu_num,
|
||||
LteRrcOtaPacket::V25 { pdu_num, .. } => *pdu_num,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_earfcn(&self) -> u32 {
|
||||
match self {
|
||||
LteRrcOtaPacket::V0 { earfcn, .. } => *earfcn as u32,
|
||||
LteRrcOtaPacket::V5 { earfcn, .. } => *earfcn as u32,
|
||||
LteRrcOtaPacket::V8 { earfcn, .. } => *earfcn,
|
||||
LteRrcOtaPacket::V25 { earfcn, .. } => *earfcn,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_payload(self) -> Vec<u8> {
|
||||
match self {
|
||||
LteRrcOtaPacket::V0 { packet, .. } => packet,
|
||||
LteRrcOtaPacket::V5 { packet, .. } => packet,
|
||||
LteRrcOtaPacket::V8 { packet, .. } => packet,
|
||||
LteRrcOtaPacket::V25 { packet, .. } => packet,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
//! Diag protocol serialization/deserialization
|
||||
|
||||
use crc::{Algorithm, Crc};
|
||||
use deku::prelude::*;
|
||||
|
||||
use crate::hdlc::{self, hdlc_decapsulate};
|
||||
use log::warn;
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod diaglog;
|
||||
|
||||
use diaglog::{LogBody, Timestamp};
|
||||
|
||||
pub const MESSAGE_TERMINATOR: u8 = 0x7e;
|
||||
pub const MESSAGE_ESCAPE_CHAR: u8 = 0x7d;
|
||||
|
||||
pub const ESCAPED_MESSAGE_TERMINATOR: u8 = 0x5e;
|
||||
pub const ESCAPED_MESSAGE_ESCAPE_CHAR: u8 = 0x5d;
|
||||
|
||||
#[derive(Debug, Clone, DekuWrite)]
|
||||
pub struct RequestContainer {
|
||||
pub data_type: DataType,
|
||||
#[deku(skip)]
|
||||
pub use_mdm: bool,
|
||||
#[deku(skip, cond = "!*use_mdm")]
|
||||
pub mdm_field: i32,
|
||||
pub hdlc_encapsulated_request: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuWrite)]
|
||||
#[deku(id_type = "u32")]
|
||||
pub enum Request {
|
||||
#[deku(id = "115")]
|
||||
LogConfig(LogConfigRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuWrite)]
|
||||
#[deku(id_type = "u32", endian = "little")]
|
||||
pub enum LogConfigRequest {
|
||||
#[deku(id = "1")]
|
||||
RetrieveIdRanges,
|
||||
|
||||
#[deku(id = "3")]
|
||||
SetMask {
|
||||
log_type: u32,
|
||||
log_mask_bitsize: u32,
|
||||
log_mask: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(id_type = "u32", endian = "little")]
|
||||
pub enum DataType {
|
||||
#[deku(id = "32")]
|
||||
UserSpace,
|
||||
#[deku(id_pat = "_")]
|
||||
Other(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Error)]
|
||||
pub enum DiagParsingError {
|
||||
#[error("Failed to parse Message: {0}, data: {1:?}")]
|
||||
MessageParsingError(deku::DekuError, Vec<u8>),
|
||||
#[error("HDLC decapsulation of message failed: {0}, data: {1:?}")]
|
||||
HdlcDecapsulationError(hdlc::HdlcError, Vec<u8>),
|
||||
}
|
||||
|
||||
// this is sorta based on the params qcsuper uses, plus what seems to be used in
|
||||
// https://github.com/fgsect/scat/blob/f1538b397721df3ab8ba12acd26716abcf21f78b/util.py#L47
|
||||
pub const CRC_CCITT_ALG: Algorithm<u16> = Algorithm {
|
||||
poly: 0x1021,
|
||||
init: 0xffff,
|
||||
refin: true,
|
||||
refout: true,
|
||||
width: 16,
|
||||
xorout: 0xffff,
|
||||
check: 0x2189,
|
||||
residue: 0x0000,
|
||||
};
|
||||
|
||||
pub const CRC_CCITT: Crc<u16> = Crc::<u16>::new(&CRC_CCITT_ALG);
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
pub struct MessagesContainer {
|
||||
pub data_type: DataType,
|
||||
pub num_messages: u32,
|
||||
#[deku(count = "num_messages")]
|
||||
pub messages: Vec<HdlcEncapsulatedMessage>,
|
||||
}
|
||||
|
||||
impl MessagesContainer {
|
||||
pub fn messages(&self) -> Vec<Result<Message, DiagParsingError>> {
|
||||
let mut result = Vec::new();
|
||||
for msg in &self.messages {
|
||||
for sub_msg in msg.data.split_inclusive(|&b| b == MESSAGE_TERMINATOR) {
|
||||
result.push(Message::from_hdlc(sub_msg));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
pub struct HdlcEncapsulatedMessage {
|
||||
pub len: u32,
|
||||
#[deku(count = "len")]
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(id_type = "u8")]
|
||||
pub enum Message {
|
||||
#[deku(id = "16")]
|
||||
Log {
|
||||
pending_msgs: u8,
|
||||
outer_length: u16,
|
||||
inner_length: u16,
|
||||
log_type: u16,
|
||||
timestamp: Timestamp,
|
||||
// pass the log type and log length (inner_length - (sizeof(log_type) + sizeof(timestamp)))
|
||||
#[deku(ctx = "*log_type, inner_length.saturating_sub(12)")]
|
||||
body: LogBody,
|
||||
},
|
||||
|
||||
// kinda unpleasant deku hackery here. deku expects an enum's variant to be
|
||||
// right before its data, but in this case, a status value comes between the
|
||||
// variants and the data. so we need to use deku's context (ctx) feature to
|
||||
// pass those opcodes down to their respective parsers.
|
||||
#[deku(id_pat = "_")]
|
||||
Response {
|
||||
opcode1: u8, // the "id" (from deku's POV) gets parsed into this field
|
||||
opcode2: u8,
|
||||
opcode3: u8,
|
||||
opcode4: u8,
|
||||
subopcode: u32,
|
||||
status: u32,
|
||||
#[deku(ctx = "u32::from_le_bytes([*opcode1, *opcode2, *opcode3, *opcode4]), *subopcode")]
|
||||
payload: ResponsePayload,
|
||||
},
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn from_hdlc(data: &[u8]) -> Result<Message, DiagParsingError> {
|
||||
match hdlc_decapsulate(data, &CRC_CCITT) {
|
||||
Ok(data) => match Message::from_bytes((&data, 0)) {
|
||||
Ok(((leftover_bytes, _), res)) => {
|
||||
if !leftover_bytes.is_empty() {
|
||||
warn!(
|
||||
"warning: {} leftover bytes when parsing Message",
|
||||
leftover_bytes.len()
|
||||
);
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
Err(e) => Err(DiagParsingError::MessageParsingError(e, data)),
|
||||
},
|
||||
Err(err) => Err(DiagParsingError::HdlcDecapsulationError(err, data.to_vec())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "opcode: u32, subopcode: u32", id = "opcode")]
|
||||
pub enum ResponsePayload {
|
||||
#[deku(id = "115")]
|
||||
LogConfig(#[deku(ctx = "subopcode")] LogConfigResponse),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "subopcode: u32", id = "subopcode")]
|
||||
pub enum LogConfigResponse {
|
||||
#[deku(id = "1")]
|
||||
RetrieveIdRanges { log_mask_sizes: [u32; 16] },
|
||||
|
||||
#[deku(id = "3")]
|
||||
SetMask,
|
||||
}
|
||||
|
||||
pub fn build_log_mask_request(
|
||||
log_type: u32,
|
||||
log_mask_bitsize: u32,
|
||||
accepted_log_codes: &[u32],
|
||||
) -> Request {
|
||||
let mut current_byte: u8 = 0;
|
||||
let mut num_bits_written: u8 = 0;
|
||||
let mut log_mask: Vec<u8> = vec![];
|
||||
for i in 0..log_mask_bitsize {
|
||||
let log_code: u32 = (log_type << 12) | i;
|
||||
if accepted_log_codes.contains(&log_code) {
|
||||
current_byte |= 1 << num_bits_written;
|
||||
}
|
||||
num_bits_written += 1;
|
||||
|
||||
if num_bits_written == 8 || i == log_mask_bitsize - 1 {
|
||||
log_mask.push(current_byte);
|
||||
current_byte = 0;
|
||||
num_bits_written = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Request::LogConfig(LogConfigRequest::SetMask {
|
||||
log_type,
|
||||
log_mask_bitsize,
|
||||
log_mask,
|
||||
})
|
||||
}
|
||||
+20
-12
@@ -40,22 +40,30 @@ pub enum DiagDeviceError {
|
||||
ParseMessagesContainerError(deku::DekuError),
|
||||
}
|
||||
|
||||
pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 11] = [
|
||||
pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 17] = [
|
||||
// Layer 2:
|
||||
log_codes::LOG_GPRS_MAC_SIGNALLING_MESSAGE_C, // 0x5226
|
||||
log_codes::LOG_GPRS_MAC_SIGNALLING_MESSAGE_C,
|
||||
// Layer 3:
|
||||
log_codes::LOG_GSM_RR_SIGNALING_MESSAGE_C, // 0x512f
|
||||
log_codes::WCDMA_SIGNALLING_MESSAGE, // 0x412f
|
||||
log_codes::LOG_LTE_RRC_OTA_MSG_LOG_C, // 0xb0c0
|
||||
log_codes::LOG_NR_RRC_OTA_MSG_LOG_C, // 0xb821
|
||||
log_codes::LOG_GSM_RR_SIGNALING_MESSAGE_C,
|
||||
log_codes::WCDMA_SIGNALLING_MESSAGE,
|
||||
log_codes::LOG_LTE_RRC_OTA_MSG_LOG_C,
|
||||
log_codes::LOG_NR_RRC_OTA_MSG_LOG_C,
|
||||
// NAS:
|
||||
log_codes::LOG_UMTS_NAS_OTA_MESSAGE_LOG_PACKET_C, // 0x713a
|
||||
log_codes::LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C, // 0xb0e2
|
||||
log_codes::LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C, // 0xb0e3
|
||||
log_codes::LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C, // 0xb0ec
|
||||
log_codes::LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C, // 0xb0ed
|
||||
log_codes::LOG_UMTS_NAS_OTA_MESSAGE_LOG_PACKET_C,
|
||||
log_codes::LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C,
|
||||
log_codes::LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C,
|
||||
log_codes::LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C,
|
||||
log_codes::LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C,
|
||||
// User IP traffic:
|
||||
log_codes::LOG_DATA_PROTOCOL_LOGGING_C, // 0x11eb
|
||||
log_codes::LOG_DATA_PROTOCOL_LOGGING_C,
|
||||
// LTE physical layer serving cell measurements: RSRP, RSRQ, RSSI
|
||||
log_codes::LOG_LTE_ML1_SERVING_CELL_MEAS_AND_EVAL_C,
|
||||
log_codes::LOG_LTE_ML1_NEIGHBOR_MEAS,
|
||||
// LTE MAC Random Access Channel response: contains Timing Advance
|
||||
log_codes::LOG_LTE_MAC_RACH_RESPONSE_C,
|
||||
log_codes::LOG_LTE_MAC_DL,
|
||||
log_codes::LOG_LTE_MAC_UL,
|
||||
0xb114, // maybe timing advance related?
|
||||
];
|
||||
|
||||
const BUFFER_LEN: usize = 1024 * 1024 * 10;
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
//! The structs/enum values defined here are derived from a number of sources:
|
||||
//! * SCAT's construction of MAC GSMTAP packets: https://github.com/fgsect/scat/blob/9763cb5b1dcd5ee980f5b0ead9a8d520c8c51a51/src/scat/parsers/qualcomm/diagltelogparser.py#L562-L640
|
||||
//! * https://www.sharetechnote.com/html/MAC_LTE.html#MAC_PDU_Structure_RAR
|
||||
//! * 3GPP's TS 36.321, mostly sections 6.1.4, 6.1.5, and 6.1.6
|
||||
|
||||
use deku::prelude::*;
|
||||
|
||||
use crate::{
|
||||
diag::diaglog::mac::SubpacketBody,
|
||||
gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType},
|
||||
};
|
||||
use deku::{DekuContainerWrite, DekuError};
|
||||
|
||||
#[derive(DekuRead, DekuWrite)]
|
||||
pub struct Header {
|
||||
pub radio_type: RadioType,
|
||||
pub direction: Direction,
|
||||
pub rnti_type: RntiType,
|
||||
}
|
||||
|
||||
#[derive(DekuRead, DekuWrite)]
|
||||
#[deku(id_type = "u8")]
|
||||
pub enum RadioType {
|
||||
#[deku(id = "1")]
|
||||
Fdd,
|
||||
#[deku(id = "2")]
|
||||
Tdd,
|
||||
}
|
||||
|
||||
#[derive(DekuRead, DekuWrite)]
|
||||
#[deku(id_type = "u8")]
|
||||
pub enum Direction {
|
||||
#[deku(id = "0")]
|
||||
Uplink,
|
||||
#[deku(id = "1")]
|
||||
Downlink,
|
||||
}
|
||||
|
||||
#[derive(DekuRead, DekuWrite)]
|
||||
#[deku(id_type = "u8")]
|
||||
pub enum RntiType {
|
||||
#[deku(id = "0")]
|
||||
No,
|
||||
#[deku(id = "1")]
|
||||
P,
|
||||
#[deku(id = "2")]
|
||||
Ra,
|
||||
#[deku(id = "3")]
|
||||
C,
|
||||
#[deku(id = "4")]
|
||||
Si,
|
||||
#[deku(id = "5")]
|
||||
Sps,
|
||||
#[deku(id = "6")]
|
||||
M,
|
||||
#[deku(id = "7")]
|
||||
Sl,
|
||||
#[deku(id = "9")]
|
||||
Sc,
|
||||
#[deku(id = "10")]
|
||||
G,
|
||||
}
|
||||
|
||||
#[derive(DekuRead, DekuWrite)]
|
||||
#[deku(endian = "big")]
|
||||
pub struct ETRAPIDSubheader {
|
||||
#[deku(bits = 1)]
|
||||
pub extended: bool,
|
||||
#[deku(bits = 1)]
|
||||
pub type_field: bool,
|
||||
#[deku(bits = 6)]
|
||||
pub rapid: u8,
|
||||
}
|
||||
|
||||
#[derive(DekuRead, DekuWrite)]
|
||||
#[deku(endian = "big")]
|
||||
pub struct RACHResponse {
|
||||
#[deku(pad_bits_before = "1", bits = 11)]
|
||||
pub tac: u16,
|
||||
#[deku(bits = 20)]
|
||||
pub ul_grant: u32,
|
||||
pub tc_rnti: u16,
|
||||
}
|
||||
|
||||
pub fn mac_subpacket_to_gsmtap(
|
||||
subpacket: &SubpacketBody,
|
||||
) -> Result<Option<GsmtapMessage>, DekuError> {
|
||||
match subpacket {
|
||||
SubpacketBody::RachAttempt(attempt) => {
|
||||
let (Some(msg1), Some(msg2), Some(msg3)) =
|
||||
(attempt.get_msg1(), attempt.get_msg2(), attempt.get_msg3())
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut payload = Vec::new();
|
||||
payload.extend(
|
||||
Header {
|
||||
radio_type: RadioType::Fdd,
|
||||
direction: Direction::Downlink,
|
||||
rnti_type: RntiType::Ra,
|
||||
}
|
||||
.to_bytes()?,
|
||||
);
|
||||
payload.push(0x01); // MAC Payload Tag
|
||||
payload.extend(
|
||||
ETRAPIDSubheader {
|
||||
extended: false,
|
||||
type_field: true,
|
||||
rapid: msg1.get_preamble_index() & 0b111111,
|
||||
}
|
||||
.to_bytes()?,
|
||||
);
|
||||
payload.extend(
|
||||
RACHResponse {
|
||||
tac: msg2.ta & 0b11111111111,
|
||||
ul_grant: msg3.get_grant(),
|
||||
tc_rnti: msg2.tc_rnti,
|
||||
}
|
||||
.to_bytes()?,
|
||||
);
|
||||
Ok(Some(GsmtapMessage {
|
||||
header: GsmtapHeader::new(GsmtapType::LteMacFramed),
|
||||
payload,
|
||||
}))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::diag::diaglog::mac::Packet;
|
||||
use crate::diag::diaglog::mac::test::mac_rach_test_packets_from_scat;
|
||||
use crate::test_util::unhexlify;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn assert_mac_gsmtap(packet: &Packet, expected_hexstr: Option<&str>) {
|
||||
assert_eq!(packet.subpackets.len(), 1);
|
||||
let subpacket = &packet.subpackets[0];
|
||||
let result = mac_subpacket_to_gsmtap(&subpacket.body).unwrap();
|
||||
match (result, expected_hexstr) {
|
||||
(Some(msg), Some(hexstr)) => {
|
||||
let (_, data) = unhexlify(hexstr);
|
||||
// SCAT's test cases use GSMTAP v3, but we're on V2, so skip
|
||||
// their GSMTAP header and just compare the payloads
|
||||
let expected_payload = &data.into_inner().into_inner()[34..];
|
||||
assert_eq!(&msg.payload, expected_payload);
|
||||
}
|
||||
(Some(msg), None) => panic!("expected no GSMTAP message, got {msg:?}"),
|
||||
(None, Some(_)) => panic!("expected GSMTAP message, got None"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mac_rach() {
|
||||
// test data from SCAT unit tests: https://github.com/fgsect/scat/blob/9763cb5b1dcd5ee980f5b0ead9a8d520c8c51a51/tests/test_diagltelogparser.py#L129
|
||||
let test_packets = mac_rach_test_packets_from_scat();
|
||||
assert_mac_gsmtap(
|
||||
&test_packets[0],
|
||||
Some(
|
||||
"03000009040000000000000c0000000012d53d80000000000002000400000000fffe010102015b00411c181a23",
|
||||
),
|
||||
);
|
||||
assert_mac_gsmtap(
|
||||
&test_packets[1],
|
||||
Some(
|
||||
"03000009040000000000000c0000000012d53d80000000000002000400000000fffe010102015800b0a2b461c6",
|
||||
),
|
||||
);
|
||||
assert_mac_gsmtap(&test_packets[2], None);
|
||||
assert_mac_gsmtap(
|
||||
&test_packets[3],
|
||||
Some(
|
||||
"03000009040000000000000c0000000012d53d80000000000002000400000ea5fffe010102014a0070e218481c",
|
||||
),
|
||||
);
|
||||
assert_mac_gsmtap(&test_packets[4], None);
|
||||
assert_mac_gsmtap(
|
||||
&test_packets[5],
|
||||
Some(
|
||||
"03000009040000000000000c0000000012d53d80000000000002000400000d16fffe0101020153005146b45aad",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@
|
||||
use deku::prelude::*;
|
||||
use num_enum::TryFromPrimitive;
|
||||
|
||||
mod mac;
|
||||
pub mod parser;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum GsmtapType {
|
||||
Um(UmSubtype),
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::diag::*;
|
||||
use crate::gsmtap::*;
|
||||
use crate::diag::Message;
|
||||
use crate::diag::diaglog::{LogBody, Nas4GMessageDirection, Timestamp};
|
||||
use crate::gsmtap::mac::mac_subpacket_to_gsmtap;
|
||||
use crate::gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType, LteNasSubtype, LteRrcSubtype};
|
||||
|
||||
use log::error;
|
||||
use log::{debug, warn};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -10,6 +12,8 @@ pub enum GsmtapParserError {
|
||||
InvalidLteRrcOtaExtHeaderVersion(u8),
|
||||
#[error("Invalid LteRrcOtaMessage header/PDU number combination: {0}/{1}")]
|
||||
InvalidLteRrcOtaHeaderPduNum(u8, u8),
|
||||
#[error("Invalid LteMacRachResponse packet: {0}")]
|
||||
InvalidLteMacRachResponse(String),
|
||||
}
|
||||
|
||||
pub fn parse(msg: Message) -> Result<Option<(Timestamp, GsmtapMessage)>, GsmtapParserError> {
|
||||
@@ -135,7 +139,7 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
||||
}
|
||||
};
|
||||
let mut header = GsmtapHeader::new(gsmtap_type);
|
||||
header.arfcn = packet.get_earfcn().try_into().unwrap_or(0);
|
||||
header.arfcn = (packet.get_earfcn() as u16) & 0x3FFF;
|
||||
header.frame_number = packet.get_sfn();
|
||||
header.subslot = packet.get_subfn();
|
||||
Ok(Some(GsmtapMessage {
|
||||
@@ -152,9 +156,48 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
||||
payload: msg,
|
||||
}))
|
||||
}
|
||||
LogBody::LteMacRachResponse { packet } => {
|
||||
if packet.subpackets.len() > 1 {
|
||||
warn!(
|
||||
"expected 1 MAC subpacket for LogBody::LteMacRachResponse, but got {}! ignoring all but the first",
|
||||
packet.subpackets.len()
|
||||
);
|
||||
}
|
||||
let Some(subpacket) = packet.subpackets.first() else {
|
||||
return Err(GsmtapParserError::InvalidLteMacRachResponse(
|
||||
"no subpackets".to_string(),
|
||||
));
|
||||
};
|
||||
mac_subpacket_to_gsmtap(&subpacket.body).map_err(|err| {
|
||||
GsmtapParserError::InvalidLteMacRachResponse(format!(
|
||||
"unable to serialize GSMTAP payload: {err:?}"
|
||||
))
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
error!("gsmtap_sink: ignoring unhandled log type: {value:?}");
|
||||
debug!("gsmtap_sink: ignoring unhandled log type: {value:?}");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::gsmtap::GsmtapType;
|
||||
use deku::DekuContainerWrite;
|
||||
|
||||
#[test]
|
||||
fn test_arfcn_exceeding_14_bits_does_not_panic() {
|
||||
let mut header = GsmtapHeader::new(GsmtapType::LteRrc(LteRrcSubtype::DlDcch));
|
||||
// EARFCN 54540 (band 46) exceeds 14-bit max of 16383
|
||||
let large_earfcn: u32 = 54540;
|
||||
header.arfcn = (large_earfcn as u16) & 0x3FFF;
|
||||
let msg = GsmtapMessage {
|
||||
header,
|
||||
payload: vec![0x00],
|
||||
};
|
||||
// This would panic before the fix with "bit size of input is larger than bit requested size"
|
||||
assert!(msg.to_bytes().is_ok());
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -15,11 +15,12 @@ pub mod analysis;
|
||||
pub mod clock;
|
||||
pub mod diag;
|
||||
pub mod gsmtap;
|
||||
pub mod gsmtap_parser;
|
||||
pub mod hdlc;
|
||||
pub mod log_codes;
|
||||
pub mod pcap;
|
||||
pub mod qmdl;
|
||||
#[cfg(test)]
|
||||
mod test_util;
|
||||
pub mod util;
|
||||
|
||||
// bin/check.rs may target windows and does not use this mod
|
||||
|
||||
@@ -31,6 +31,16 @@ pub const LOG_NR_RRC_OTA_MSG_LOG_C: u32 = 0xb821;
|
||||
// These are 4G-related log types.
|
||||
|
||||
pub const LOG_LTE_RRC_OTA_MSG_LOG_C: u32 = 0xb0c0;
|
||||
// Qualcomm ML1 (physical layer) serving cell measurement report: RSRP, RSRQ, RSSI
|
||||
pub const LOG_LTE_ML1_SERVING_CELL_MEAS_AND_EVAL_C: u32 = 0xb17f;
|
||||
pub const LOG_LTE_ML1_SERVING_CELL_MEAS_RESPONSE: u32 = 0xb193;
|
||||
pub const LOG_LTE_ML1_NEIGHBOR_MEAS: u32 = 0xb180;
|
||||
pub const LOG_LTE_ML1_MAC_RAR_MSG1_REPORT: u32 = 0xb167;
|
||||
pub const LOG_LTE_ML1_MAC_RAR_MSG2_REPORT: u32 = 0xb168;
|
||||
// Qualcomm MAC layer RACH response log: contains Timing Advance from Random Access Response
|
||||
pub const LOG_LTE_MAC_RACH_RESPONSE_C: u32 = 0xb062;
|
||||
pub const LOG_LTE_MAC_DL: u32 = 0xb063;
|
||||
pub const LOG_LTE_MAC_UL: u32 = 0xb064;
|
||||
pub const LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C: u32 = 0xb0e2;
|
||||
pub const LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C: u32 = 0xb0e3;
|
||||
pub const LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C: u32 = 0xb0ec;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
//! Parse QMDL files and create a pcap file.
|
||||
//! Creates a plausible IP header and [GSMtap](https://osmocom.org/projects/baseband/wiki/GSMTAP) header and then puts the rest of the data under that for wireshark to parse.
|
||||
use crate::diag::Timestamp;
|
||||
use crate::diag::diaglog::Timestamp;
|
||||
use crate::gsmtap::GsmtapMessage;
|
||||
|
||||
use chrono::prelude::*;
|
||||
|
||||
+302
-153
@@ -3,109 +3,214 @@
|
||||
//! QmdlReader and QmdlWriter can read and write MessagesContainers to and from
|
||||
//! QMDL files.
|
||||
|
||||
use crate::diag::{DataType, HdlcEncapsulatedMessage, MESSAGE_TERMINATOR, MessagesContainer};
|
||||
use std::io::ErrorKind;
|
||||
use std::pin::Pin;
|
||||
use std::task::Poll;
|
||||
|
||||
use crate::diag::{DiagParsingError, MESSAGE_TERMINATOR, Message, MessagesContainer};
|
||||
|
||||
use async_compression::tokio::bufread::GzipDecoder;
|
||||
use async_compression::tokio::write::GzipEncoder;
|
||||
use futures::TryStream;
|
||||
use log::error;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
|
||||
use tokio::io::{
|
||||
AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt,
|
||||
BufReader,
|
||||
};
|
||||
|
||||
const GZIP_MAGIC_NUMBER: u16 = 0x1f8b;
|
||||
|
||||
pub struct QmdlWriter<T>
|
||||
where
|
||||
T: AsyncWrite + Unpin,
|
||||
{
|
||||
writer: T,
|
||||
pub total_written: usize,
|
||||
writer: GzipEncoder<T>,
|
||||
}
|
||||
|
||||
impl<T> QmdlWriter<T>
|
||||
where
|
||||
T: AsyncWrite + Unpin,
|
||||
T: AsyncWrite + AsyncSeek + Unpin,
|
||||
{
|
||||
pub fn new(writer: T) -> Self {
|
||||
QmdlWriter::new_with_existing_size(writer, 0)
|
||||
let gzip_writer = GzipEncoder::new(writer);
|
||||
QmdlWriter {
|
||||
writer: gzip_writer,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_existing_size(writer: T, existing_size: usize) -> Self {
|
||||
QmdlWriter {
|
||||
writer,
|
||||
total_written: existing_size,
|
||||
}
|
||||
pub async fn size(&mut self) -> std::io::Result<usize> {
|
||||
let size = self.writer.get_mut().stream_position().await?;
|
||||
Ok(size as usize)
|
||||
}
|
||||
|
||||
pub async fn write_container(&mut self, container: &MessagesContainer) -> std::io::Result<()> {
|
||||
for msg in &container.messages {
|
||||
self.writer.write_all(&msg.data).await?;
|
||||
self.total_written += msg.data.len();
|
||||
}
|
||||
self.writer.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn close(mut self) -> std::io::Result<usize> {
|
||||
self.writer.shutdown().await?;
|
||||
self.size().await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QmdlReader<T>
|
||||
#[derive(Debug)]
|
||||
enum QmdlReaderSource<T> {
|
||||
Compressed {
|
||||
reader: GzipDecoder<BufReader<T>>,
|
||||
eof: bool,
|
||||
},
|
||||
Uncompressed {
|
||||
reader: T,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct QmdlAsyncReader<T> {
|
||||
source: QmdlReaderSource<T>,
|
||||
}
|
||||
|
||||
impl<T> QmdlAsyncReader<T>
|
||||
where
|
||||
T: AsyncRead,
|
||||
{
|
||||
reader: BufReader<T>,
|
||||
bytes_read: usize,
|
||||
max_bytes: Option<usize>,
|
||||
pub fn new(reader: T, compressed: bool) -> Self {
|
||||
let source = if compressed {
|
||||
QmdlReaderSource::Compressed {
|
||||
reader: GzipDecoder::new(BufReader::new(reader)),
|
||||
eof: false,
|
||||
}
|
||||
} else {
|
||||
QmdlReaderSource::Uncompressed { reader }
|
||||
};
|
||||
Self { source }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> QmdlReader<T>
|
||||
impl<T> AsyncRead for QmdlAsyncReader<T>
|
||||
where
|
||||
T: AsyncRead + Unpin,
|
||||
{
|
||||
pub fn new(reader: T, max_bytes: Option<usize>) -> Self {
|
||||
QmdlReader {
|
||||
reader: BufReader::new(reader),
|
||||
bytes_read: 0,
|
||||
max_bytes,
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
match &mut self.get_mut().source {
|
||||
QmdlReaderSource::Compressed { reader, eof } => {
|
||||
// if we already determined we've reached the Gzip EOF, don't read more
|
||||
if *eof {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
match Pin::new(reader).poll_read(cx, buf) {
|
||||
// if we hit an unexpected EOF in a Gzip file, it shouldn't
|
||||
// be considered fatal, just a truncated file. mark that
|
||||
// we're done and return the result as usual
|
||||
Poll::Ready(Err(err)) if err.kind() == ErrorKind::UnexpectedEof => {
|
||||
*eof = true;
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
res => res,
|
||||
}
|
||||
}
|
||||
QmdlReaderSource::Uncompressed { reader } => Pin::new(reader).poll_read(cx, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_stream(
|
||||
&mut self,
|
||||
) -> impl TryStream<Ok = MessagesContainer, Error = std::io::Error> + '_ {
|
||||
futures::stream::try_unfold(self, |reader| async {
|
||||
let maybe_container = reader.get_next_messages_container().await?;
|
||||
match maybe_container {
|
||||
Some(container) => Ok(Some((container, reader))),
|
||||
#[derive(Debug)]
|
||||
pub struct QmdlMessageReader<T>
|
||||
where
|
||||
T: AsyncRead,
|
||||
{
|
||||
buf_reader: BufReader<QmdlAsyncReader<T>>,
|
||||
}
|
||||
|
||||
async fn is_gzip_stream<T>(mut reader: T) -> std::io::Result<bool>
|
||||
where
|
||||
T: AsyncRead + AsyncSeek + Unpin,
|
||||
{
|
||||
let magic_number = reader.read_u16().await?;
|
||||
reader.rewind().await?;
|
||||
// this is safe because 0x1f8b.... doesn't overlap with any known
|
||||
// diag::DataType values
|
||||
Ok(magic_number == GZIP_MAGIC_NUMBER)
|
||||
}
|
||||
|
||||
impl<T> QmdlMessageReader<T>
|
||||
where
|
||||
T: AsyncRead + AsyncSeek + Unpin,
|
||||
{
|
||||
pub async fn new(mut reader: T) -> std::io::Result<Self> {
|
||||
let compressed = is_gzip_stream(&mut reader).await.unwrap_or(false);
|
||||
Ok(QmdlMessageReader {
|
||||
buf_reader: BufReader::new(QmdlAsyncReader::new(reader, compressed)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_compressed(&self) -> bool {
|
||||
matches!(
|
||||
self.buf_reader.get_ref().source,
|
||||
QmdlReaderSource::Compressed { .. }
|
||||
)
|
||||
}
|
||||
|
||||
pub fn into_qmdl_stream(self) -> impl TryStream<Ok = Vec<u8>, Error = std::io::Error> {
|
||||
futures::stream::try_unfold(self, |mut reader| async {
|
||||
let mut buf = vec![];
|
||||
match reader
|
||||
.buf_reader
|
||||
.read_until(MESSAGE_TERMINATOR, &mut buf)
|
||||
.await
|
||||
{
|
||||
Err(err) => Err(err),
|
||||
Ok(0) => Ok(None),
|
||||
Ok(_) => Ok(Some((buf, reader))),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn into_message_stream(
|
||||
self,
|
||||
) -> impl TryStream<Ok = Result<Message, DiagParsingError>, Error = std::io::Error> {
|
||||
futures::stream::try_unfold(self, |mut reader| async {
|
||||
match reader.get_next_message().await? {
|
||||
Some(res) => Ok(Some((res, reader))),
|
||||
None => Ok(None),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_next_messages_container(
|
||||
pub async fn get_next_message(
|
||||
&mut self,
|
||||
) -> Result<Option<MessagesContainer>, std::io::Error> {
|
||||
if let Some(max_bytes) = self.max_bytes
|
||||
&& self.bytes_read >= max_bytes
|
||||
) -> Result<Option<Result<Message, DiagParsingError>>, std::io::Error> {
|
||||
let mut buf = vec![];
|
||||
if self
|
||||
.buf_reader
|
||||
.read_until(MESSAGE_TERMINATOR, &mut buf)
|
||||
.await?
|
||||
== 0
|
||||
{
|
||||
if self.bytes_read > max_bytes {
|
||||
error!(
|
||||
"warning: {} bytes read, but max_bytes was {}",
|
||||
self.bytes_read, max_bytes
|
||||
);
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let bytes_read = self.reader.read_until(MESSAGE_TERMINATOR, &mut buf).await?;
|
||||
self.bytes_read += bytes_read;
|
||||
Ok(Some(Message::from_hdlc(&buf)))
|
||||
}
|
||||
}
|
||||
|
||||
// Since QMDL is just a flat list of messages, we can't actually
|
||||
// reproduce the container structure they came from in the original
|
||||
// read. So we'll just pretend that all containers had exactly one
|
||||
// message. As far as I know, the number of messages per container
|
||||
// doesn't actually affect anything, so this should be fine.
|
||||
Ok(Some(MessagesContainer {
|
||||
data_type: DataType::UserSpace,
|
||||
num_messages: 1,
|
||||
messages: vec![HdlcEncapsulatedMessage {
|
||||
len: bytes_read as u32,
|
||||
data: buf,
|
||||
}],
|
||||
}))
|
||||
impl<T> AsyncRead for QmdlMessageReader<T>
|
||||
where
|
||||
T: AsyncRead + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut self.get_mut().buf_reader).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,132 +218,176 @@ where
|
||||
mod test {
|
||||
use std::io::Cursor;
|
||||
|
||||
use crate::diag::CRC_CCITT;
|
||||
use crate::hdlc::hdlc_encapsulate;
|
||||
use crate::diag::{DataType, HdlcEncapsulatedMessage, diaglog::test::get_test_message};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn get_test_messages() -> Vec<HdlcEncapsulatedMessage> {
|
||||
let messages: Vec<HdlcEncapsulatedMessage> = (10..20)
|
||||
.map(|i| {
|
||||
let data = hdlc_encapsulate(&vec![i as u8; i], &CRC_CCITT);
|
||||
HdlcEncapsulatedMessage {
|
||||
len: data.len() as u32,
|
||||
data,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
messages
|
||||
fn get_test_messages() -> (Vec<HdlcEncapsulatedMessage>, Vec<Message>) {
|
||||
let mut hdlcs = Vec::new();
|
||||
let mut messages = Vec::new();
|
||||
for i in 10..20 {
|
||||
let (hdlc, msg) = get_test_message(&[i]);
|
||||
hdlcs.push(hdlc);
|
||||
messages.push(msg);
|
||||
}
|
||||
(hdlcs, messages)
|
||||
}
|
||||
|
||||
// returns a byte array consisting of concatenated HDLC encapsulated
|
||||
// test messages
|
||||
fn get_test_message_bytes() -> Vec<u8> {
|
||||
get_test_messages()
|
||||
.iter()
|
||||
.flat_map(|msg| msg.data.clone())
|
||||
.collect()
|
||||
let (hdlcs, _) = get_test_messages();
|
||||
hdlcs.iter().flat_map(|msg| msg.data.clone()).collect()
|
||||
}
|
||||
|
||||
fn get_test_containers() -> Vec<MessagesContainer> {
|
||||
let messages = get_test_messages();
|
||||
let (messages1, messages2) = messages.split_at(5);
|
||||
let (hdlcs, _) = get_test_messages();
|
||||
let (hdlcs1, hdlcs2) = hdlcs.split_at(5);
|
||||
vec![
|
||||
MessagesContainer {
|
||||
data_type: DataType::UserSpace,
|
||||
num_messages: messages1.len() as u32,
|
||||
messages: messages1.to_vec(),
|
||||
num_messages: hdlcs1.len() as u32,
|
||||
messages: hdlcs1.to_vec(),
|
||||
},
|
||||
MessagesContainer {
|
||||
data_type: DataType::UserSpace,
|
||||
num_messages: messages2.len() as u32,
|
||||
messages: messages2.to_vec(),
|
||||
num_messages: hdlcs2.len() as u32,
|
||||
messages: hdlcs2.to_vec(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unbounded_qmdl_reader() {
|
||||
async fn test_qmdl_reader() {
|
||||
let mut buf = Cursor::new(get_test_message_bytes());
|
||||
let mut reader = QmdlReader::new(&mut buf, None);
|
||||
let expected_messages = get_test_messages();
|
||||
let mut reader = QmdlMessageReader::new(&mut buf).await.unwrap();
|
||||
assert!(!reader.is_compressed());
|
||||
let (_, expected_messages) = get_test_messages();
|
||||
for msg in expected_messages {
|
||||
assert_eq!(Ok(msg), reader.get_next_message().await.unwrap().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_truncation() {
|
||||
run_truncation_tests(false).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_compressed_truncation() {
|
||||
run_truncation_tests(true).await;
|
||||
}
|
||||
|
||||
async fn run_truncation_tests(compressed: bool) {
|
||||
let (hdlcs, expected_messages) = get_test_messages();
|
||||
let (bytes, message_lengths): (Vec<u8>, Vec<usize>) = if compressed {
|
||||
let mut buf = Vec::new();
|
||||
let mut compressed_lengths = Vec::new();
|
||||
let mut writer = GzipEncoder::new(&mut buf);
|
||||
for hdlc in &hdlcs {
|
||||
let before = writer.get_ref().len();
|
||||
writer.write_all(&hdlc.data).await.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
let after = writer.get_ref().len();
|
||||
compressed_lengths.push(after - before);
|
||||
}
|
||||
(buf, compressed_lengths)
|
||||
} else {
|
||||
(
|
||||
get_test_message_bytes(),
|
||||
hdlcs.iter().map(|hdlc| hdlc.data.len()).collect(),
|
||||
)
|
||||
};
|
||||
for truncated_hdlc_i in 1..hdlcs.len() {
|
||||
let whole_bytes: usize = message_lengths.iter().take(truncated_hdlc_i).sum();
|
||||
for truncated_byte in 1..message_lengths[truncated_hdlc_i] {
|
||||
let mut truncated_bytes = Cursor::new(&bytes[0..whole_bytes + truncated_byte]);
|
||||
let mut reader = QmdlMessageReader::new(&mut truncated_bytes).await.unwrap();
|
||||
for msg in expected_messages.iter().take(truncated_hdlc_i) {
|
||||
assert_eq!(
|
||||
Ok(msg),
|
||||
reader.get_next_message().await.unwrap().unwrap().as_ref()
|
||||
);
|
||||
}
|
||||
if compressed {
|
||||
// for a compressed reader, we have a couple possible
|
||||
// outcomes, depending on how far along the Gzip DEFLATE
|
||||
// block was before it was truncated:
|
||||
match reader.get_next_message().await.unwrap() {
|
||||
// if the block was truncated early enough, the
|
||||
// GzipDecoder will detect an unexpected EOF, and our
|
||||
// QmdlReader will indicate the stream of messages is
|
||||
// done
|
||||
None => {}
|
||||
// if it's further along, the expanded result will be an
|
||||
// invalid HDLC block. if that's the case, make sure the
|
||||
// QmdlReader indicates the stream of messages is over
|
||||
// with afterwards
|
||||
Some(Err(DiagParsingError::HdlcDecapsulationError(_, _))) => {
|
||||
assert!(matches!(reader.get_next_message().await, Ok(None)));
|
||||
}
|
||||
// if it's further along still, we may get a complete
|
||||
// Message, so make sure it matches the next expected
|
||||
// one. then, make sure we've hit the end of the message
|
||||
// stream
|
||||
Some(Ok(msg)) => {
|
||||
assert_eq!(&msg, &expected_messages[truncated_hdlc_i]);
|
||||
assert!(matches!(reader.get_next_message().await, Ok(None)));
|
||||
}
|
||||
// we should never be able to decapsulate the HDLC into
|
||||
// an invalid Diag message
|
||||
Some(Err(DiagParsingError::MessageParsingError(_, _))) => {
|
||||
panic!("unexpected MessageParsingError");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// a truncated uncompressed reader should always end on an
|
||||
// HdlcDecapsulationError, and then return Ok(None) to
|
||||
// indicate the message stream is over
|
||||
assert!(matches!(
|
||||
reader.get_next_message().await,
|
||||
Ok(Some(Err(DiagParsingError::HdlcDecapsulationError(_, _))))
|
||||
));
|
||||
assert!(matches!(reader.get_next_message().await, Ok(None)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes the test containers to a QmdlWriter, optionally finishing the
|
||||
/// gzip stream with a footer. Then, attempts to decompress the buffer with
|
||||
/// a QmdlWriter, asserting that the containers match what's expected.
|
||||
async fn run_compressed_reading_and_writing_tests(do_close: bool) {
|
||||
let containers = get_test_containers();
|
||||
let mut buf = Cursor::new(Vec::new());
|
||||
let writer_size = {
|
||||
let mut writer = QmdlWriter::new(&mut buf);
|
||||
for container in &containers {
|
||||
writer.write_container(&container).await.unwrap();
|
||||
}
|
||||
if do_close {
|
||||
writer.close().await.unwrap()
|
||||
} else {
|
||||
writer.size().await.unwrap()
|
||||
}
|
||||
};
|
||||
assert_eq!(buf.position() as usize, writer_size);
|
||||
buf.set_position(0);
|
||||
let mut reader = QmdlMessageReader::new(buf).await.unwrap();
|
||||
assert!(reader.is_compressed());
|
||||
let (_, expected_messages) = get_test_messages();
|
||||
for message in expected_messages {
|
||||
let expected_container = MessagesContainer {
|
||||
data_type: DataType::UserSpace,
|
||||
num_messages: 1,
|
||||
messages: vec![message],
|
||||
};
|
||||
assert_eq!(
|
||||
expected_container,
|
||||
reader.get_next_messages_container().await.unwrap().unwrap()
|
||||
Ok(message),
|
||||
reader.get_next_message().await.unwrap().unwrap()
|
||||
);
|
||||
}
|
||||
assert!(matches!(reader.get_next_message().await, Ok(None)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_bounded_qmdl_reader() {
|
||||
let mut buf = Cursor::new(get_test_message_bytes());
|
||||
|
||||
// bound the reader to the first two messages
|
||||
let mut expected_messages = get_test_messages();
|
||||
let limit = expected_messages[0].len + expected_messages[1].len;
|
||||
|
||||
let mut reader = QmdlReader::new(&mut buf, Some(limit as usize));
|
||||
for message in expected_messages.drain(0..2) {
|
||||
let expected_container = MessagesContainer {
|
||||
data_type: DataType::UserSpace,
|
||||
num_messages: 1,
|
||||
messages: vec![message],
|
||||
};
|
||||
assert_eq!(
|
||||
expected_container,
|
||||
reader.get_next_messages_container().await.unwrap().unwrap()
|
||||
);
|
||||
}
|
||||
assert!(matches!(
|
||||
reader.get_next_messages_container().await,
|
||||
Ok(None)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_qmdl_writer() {
|
||||
let mut buf = Vec::new();
|
||||
let mut writer = QmdlWriter::new(&mut buf);
|
||||
let expected_containers = get_test_containers();
|
||||
for container in &expected_containers {
|
||||
writer.write_container(container).await.unwrap();
|
||||
}
|
||||
assert_eq!(writer.total_written, buf.len());
|
||||
assert_eq!(buf, get_test_message_bytes());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_writing_and_reading() {
|
||||
let mut buf = Vec::new();
|
||||
let mut writer = QmdlWriter::new(&mut buf);
|
||||
let expected_containers = get_test_containers();
|
||||
for container in &expected_containers {
|
||||
writer.write_container(container).await.unwrap();
|
||||
}
|
||||
|
||||
let limit = Some(buf.len());
|
||||
let mut reader = QmdlReader::new(Cursor::new(&mut buf), limit);
|
||||
let expected_messages = get_test_messages();
|
||||
for message in expected_messages {
|
||||
let expected_container = MessagesContainer {
|
||||
data_type: DataType::UserSpace,
|
||||
num_messages: 1,
|
||||
messages: vec![message],
|
||||
};
|
||||
assert_eq!(
|
||||
expected_container,
|
||||
reader.get_next_messages_container().await.unwrap().unwrap()
|
||||
);
|
||||
}
|
||||
assert!(matches!(
|
||||
reader.get_next_messages_container().await,
|
||||
Ok(None)
|
||||
));
|
||||
async fn test_compressed_reading_and_writing() {
|
||||
run_compressed_reading_and_writing_tests(true).await;
|
||||
run_compressed_reading_and_writing_tests(false).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
use deku::reader::Reader;
|
||||
use std::io::Cursor;
|
||||
|
||||
pub fn unhexlify(hexlified_bytes: &str) -> (usize, Reader<Cursor<Vec<u8>>>) {
|
||||
let byte_len = hexlified_bytes.len() / 2;
|
||||
let bytes = (0..hexlified_bytes.len())
|
||||
.step_by(2)
|
||||
.map(|i| u8::from_str_radix(&hexlified_bytes[i..i + 2], 16).unwrap())
|
||||
.collect();
|
||||
(byte_len, Reader::new(Cursor::new(bytes)))
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
use deku::prelude::*;
|
||||
use rayhunter::{
|
||||
diag::{LogBody, LteRrcOtaPacket, Message, Timestamp},
|
||||
gsmtap_parser,
|
||||
diag::Message,
|
||||
diag::diaglog::{LogBody, Timestamp, rrc::LteRrcOtaPacket},
|
||||
gsmtap::parser as gsmtap_parser,
|
||||
};
|
||||
|
||||
// Tests here are based on https://github.com/fgsect/scat/blob/97442580e628de414c9f7c2a185f4e28d0ee7523/tests/test_diagltelogparser.py
|
||||
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
# Sets Rayhunter package versions in preparation for a release.
|
||||
#
|
||||
# Usage: ./scripts/set-versions.sh VERSION_NUM
|
||||
# Example: ./scripts/set-versions.sh 0.12.3
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Error: Missing required version number argument."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SED_COMMAND="s/^version = \".*\"/version = \"$1\"/"
|
||||
TOML_FILES=(*/Cargo.toml installer-gui/src-tauri/Cargo.toml)
|
||||
|
||||
echo "Updating Cargo.toml files"
|
||||
if sed --version > /dev/null 2>&1; then
|
||||
# we have GNU sed
|
||||
sed -i -E "$SED_COMMAND" "${TOML_FILES[@]}"
|
||||
else
|
||||
# we have macOS/BSD sed
|
||||
sed -i "" -E "$SED_COMMAND" "${TOML_FILES[@]}"
|
||||
fi
|
||||
|
||||
echo "Updating Cargo.lock"
|
||||
cargo update --workspace
|
||||
Reference in New Issue
Block a user