mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-07-02 14:58:56 -07:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 rebuild everything if any of these conditions hold:
|
||||||
# * We are on main
|
# * We are on main
|
||||||
|
# * The run was triggered by a tag
|
||||||
# * Changes are made to github workflows
|
# * Changes are made to github workflows
|
||||||
# * A cargo-workspace file changed (lockfile or .cargo), as that could affect any crate anywhere
|
# * 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)
|
# * 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
|
# * #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
|
then
|
||||||
echo "building everything"
|
echo "building everything"
|
||||||
echo code_count=forced >> "$GITHUB_OUTPUT"
|
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.
|
# To learn how to use this workflow, please read CONTRIBUTING.md.
|
||||||
# https://github.com/EFForg/rayhunter/actions/workflows/release.yml
|
|
||||||
name: Release rayhunter
|
name: Release rayhunter
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
push:
|
||||||
|
tags:
|
||||||
|
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
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.
|
This one is for maintainers of Rayhunter.
|
||||||
|
|
||||||
1. Make a PR changing the versions in `Cargo.toml` and other files.
|
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:
|
This can be done by running `scripts/set-versions.sh VERSION_NUM`.
|
||||||
`sed -i "" -E 's/x.x.x/y.y.y/g' */Cargo.toml installer-gui/src-tauri/Cargo.toml`
|
|
||||||
|
|
||||||
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)
|
3. Write changelog, edit it into the release, announce on mattermost.
|
||||||
|
|
||||||
4. Write changelog, edit it into the release, announce on mattermost.
|
|
||||||
|
|||||||
Generated
+27
-32
@@ -276,13 +276,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-compression"
|
name = "async-compression"
|
||||||
version = "0.4.33"
|
version = "0.4.42"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93c1f86859c1af3d514fa19e8323147ff10ea98684e6c7b307912509f50e67b2"
|
checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"compression-codecs",
|
"compression-codecs",
|
||||||
"compression-core",
|
"compression-core",
|
||||||
"futures-core",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
@@ -1007,9 +1006,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "compression-codecs"
|
name = "compression-codecs"
|
||||||
version = "0.4.32"
|
version = "0.4.38"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "680dc087785c5230f8e8843e2e57ac7c1c90488b6a91b88caa265410568f441b"
|
checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"compression-core",
|
"compression-core",
|
||||||
"flate2",
|
"flate2",
|
||||||
@@ -1906,9 +1905,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.1"
|
version = "1.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
|
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
@@ -1997,9 +1996,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.31"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -2012,9 +2011,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.31"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
@@ -2022,15 +2021,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.31"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
version = "0.3.31"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
@@ -2039,9 +2038,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.31"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-lite"
|
name = "futures-lite"
|
||||||
@@ -2058,9 +2057,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.31"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2069,21 +2068,21 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.31"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.31"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.31"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -2093,7 +2092,6 @@ dependencies = [
|
|||||||
"futures-task",
|
"futures-task",
|
||||||
"memchr",
|
"memchr",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pin-utils",
|
|
||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2947,7 +2945,9 @@ name = "installer-gui"
|
|||||||
version = "0.11.2"
|
version = "0.11.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"clap",
|
||||||
"installer",
|
"installer",
|
||||||
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"shlex",
|
"shlex",
|
||||||
@@ -4356,12 +4356,6 @@ version = "0.2.16"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pin-utils"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "piper"
|
name = "piper"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -4856,6 +4850,7 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
|||||||
name = "rayhunter"
|
name = "rayhunter"
|
||||||
version = "0.11.2"
|
version = "0.11.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-compression",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
|
|||||||
+19
-32
@@ -1,15 +1,13 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use futures::TryStreamExt;
|
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use pcap_file_tokio::pcapng::{Block, PcapNgReader};
|
use pcap_file_tokio::pcapng::{Block, PcapNgReader};
|
||||||
use rayhunter::{
|
use rayhunter::{
|
||||||
analysis::analyzer::{AnalysisRow, AnalyzerConfig, EventType, Harness},
|
analysis::analyzer::{AnalysisRow, AnalyzerConfig, EventType, Harness},
|
||||||
diag::DataType,
|
gsmtap::parser as gsmtap_parser,
|
||||||
gsmtap_parser,
|
|
||||||
pcap::GsmtapPcapWriter,
|
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 tokio::fs::File;
|
||||||
use walkdir::WalkDir;
|
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) {
|
async fn analyze_qmdl(qmdl_path: &str, show_skipped: bool) {
|
||||||
let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
|
let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
|
||||||
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open file");
|
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open file");
|
||||||
let file_size = qmdl_file
|
let mut qmdl_reader = QmdlMessageReader::new(qmdl_file)
|
||||||
.metadata()
|
|
||||||
.await
|
.await
|
||||||
.expect("failed to get QMDL file metadata")
|
.expect("failed to open QmdlReader");
|
||||||
.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))
|
|
||||||
);
|
|
||||||
let mut report = Report::new(qmdl_path);
|
let mut report = Report::new(qmdl_path);
|
||||||
while let Some(container) = qmdl_stream
|
while let Some(maybe_message) = qmdl_reader
|
||||||
.try_next()
|
.get_next_message()
|
||||||
.await
|
.await
|
||||||
.expect("failed getting QMDL container")
|
.expect("failed to get message")
|
||||||
{
|
{
|
||||||
for row in harness.analyze_qmdl_messages(container) {
|
report.process_row(harness.analyze_qmdl_message(maybe_message));
|
||||||
report.process_row(row);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
report.print_summary(show_skipped);
|
report.print_summary(show_skipped);
|
||||||
}
|
}
|
||||||
@@ -141,8 +129,9 @@ async fn pcapify(qmdl_path: &PathBuf) {
|
|||||||
let qmdl_file = &mut File::open(&qmdl_path)
|
let qmdl_file = &mut File::open(&qmdl_path)
|
||||||
.await
|
.await
|
||||||
.expect("failed to open qmdl file");
|
.expect("failed to open qmdl file");
|
||||||
let qmdl_file_size = qmdl_file.metadata().await.unwrap().len();
|
let mut qmdl_reader = QmdlMessageReader::new(qmdl_file)
|
||||||
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(qmdl_file_size as usize));
|
.await
|
||||||
|
.expect("failed to open QmdlReader");
|
||||||
let mut pcap_path = qmdl_path.clone();
|
let mut pcap_path = qmdl_path.clone();
|
||||||
pcap_path.set_extension("pcapng");
|
pcap_path.set_extension("pcapng");
|
||||||
let pcap_file = &mut File::create(&pcap_path)
|
let pcap_file = &mut File::create(&pcap_path)
|
||||||
@@ -150,20 +139,20 @@ async fn pcapify(qmdl_path: &PathBuf) {
|
|||||||
.expect("failed to open pcap file");
|
.expect("failed to open pcap file");
|
||||||
let mut pcap_writer = GsmtapPcapWriter::new(pcap_file).await.unwrap();
|
let mut pcap_writer = GsmtapPcapWriter::new(pcap_file).await.unwrap();
|
||||||
pcap_writer.write_iface_header().await.unwrap();
|
pcap_writer.write_iface_header().await.unwrap();
|
||||||
while let Some(container) = qmdl_reader
|
while let Some(maybe_message) = qmdl_reader
|
||||||
.get_next_messages_container()
|
.get_next_message()
|
||||||
.await
|
.await
|
||||||
.expect("failed to get container")
|
.expect("failed to get message")
|
||||||
|
{
|
||||||
|
if let Ok(msg) = maybe_message
|
||||||
|
&& let Ok(Some((timestamp, parsed))) = gsmtap_parser::parse(msg)
|
||||||
{
|
{
|
||||||
for msg in container.messages().into_iter().flatten() {
|
|
||||||
if let Ok(Some((timestamp, parsed))) = gsmtap_parser::parse(msg) {
|
|
||||||
pcap_writer
|
pcap_writer
|
||||||
.write_gsmtap_message(parsed, timestamp, None)
|
.write_gsmtap_message(parsed, timestamp, None)
|
||||||
.await
|
.await
|
||||||
.expect("failed to write");
|
.expect("failed to write");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
info!("wrote pcap to {:?}", &pcap_path);
|
info!("wrote pcap to {:?}", &pcap_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,9 +186,7 @@ async fn main() {
|
|||||||
let name_str = name.to_str().unwrap();
|
let name_str = name.to_str().unwrap();
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
let path_str = path.to_str().unwrap();
|
let path_str = path.to_str().unwrap();
|
||||||
// instead of relying on the QMDL extension, can we check if a file is
|
if name_str.ends_with(".qmdl") || name_str.ends_with(".qmdl.gz") {
|
||||||
// QMDL by inspecting the contents?
|
|
||||||
if name_str.ends_with(".qmdl") {
|
|
||||||
info!("**** Beginning analysis of {name_str}");
|
info!("**** Beginning analysis of {name_str}");
|
||||||
analyze_qmdl(path_str, args.show_skipped).await;
|
analyze_qmdl(path_str, args.show_skipped).await;
|
||||||
if args.pcapify {
|
if args.pcapify {
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ futures-macro = "0.3.30"
|
|||||||
include_dir = "0.7.3"
|
include_dir = "0.7.3"
|
||||||
chrono = { version = "0.4.31", features = ["serde"] }
|
chrono = { version = "0.4.31", features = ["serde"] }
|
||||||
tokio-stream = { version = "0.1.14", default-features = false, features = ["io-util"] }
|
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"
|
serde_json = "1.0.114"
|
||||||
image = { version = "0.25.1", default-features = false, features = ["png", "gif"] }
|
image = { version = "0.25.1", default-features = false, features = ["png", "gif"] }
|
||||||
tempfile = "3.10.2"
|
tempfile = "3.10.2"
|
||||||
|
|||||||
+24
-22
@@ -1,16 +1,15 @@
|
|||||||
|
use std::cmp;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{cmp, future, pin};
|
|
||||||
|
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
};
|
};
|
||||||
use futures::TryStreamExt;
|
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use rayhunter::analysis::analyzer::{AnalyzerConfig, EventType, Harness};
|
use rayhunter::analysis::analyzer::{AnalyzerConfig, EventType, Harness};
|
||||||
use rayhunter::diag::{DataType, MessagesContainer};
|
use rayhunter::diag::{DiagParsingError, Message, MessagesContainer};
|
||||||
use rayhunter::qmdl::QmdlReader;
|
use rayhunter::qmdl::QmdlMessageReader;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
use tokio::io::{AsyncWriteExt, BufWriter};
|
use tokio::io::{AsyncWriteExt, BufWriter};
|
||||||
@@ -47,7 +46,7 @@ impl AnalysisWriter {
|
|||||||
|
|
||||||
// Runs the analysis harness on the given container, serializing the results
|
// Runs the analysis harness on the given container, serializing the results
|
||||||
// to the analysis file, returning the whether any warnings were detected
|
// to the analysis file, returning the whether any warnings were detected
|
||||||
pub async fn analyze(
|
pub async fn analyze_container(
|
||||||
&mut self,
|
&mut self,
|
||||||
container: MessagesContainer,
|
container: MessagesContainer,
|
||||||
) -> Result<EventType, std::io::Error> {
|
) -> Result<EventType, std::io::Error> {
|
||||||
@@ -62,6 +61,17 @@ impl AnalysisWriter {
|
|||||||
Ok(max_type)
|
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> {
|
async fn write<T: Serialize>(&mut self, value: &T) -> Result<(), std::io::Error> {
|
||||||
let mut value_str = serde_json::to_string(value).unwrap();
|
let mut value_str = serde_json::to_string(value).unwrap();
|
||||||
value_str.push('\n');
|
value_str.push('\n');
|
||||||
@@ -135,7 +145,7 @@ async fn perform_analysis(
|
|||||||
analyzer_config: &AnalyzerConfig,
|
analyzer_config: &AnalyzerConfig,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
info!("Opening QMDL and analysis file for {name}...");
|
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 mut qmdl_store = qmdl_store_lock.write().await;
|
||||||
let (entry_index, _) = qmdl_store
|
let (entry_index, _) = qmdl_store
|
||||||
.entry_for_name(name)
|
.entry_for_name(name)
|
||||||
@@ -149,33 +159,25 @@ async fn perform_analysis(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| format!("{e:?}"))?
|
.map_err(|e| format!("{e:?}"))?
|
||||||
.ok_or("QMDL file not found")?;
|
.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)
|
let mut analysis_writer = AnalysisWriter::new(analysis_file, analyzer_config)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("{e:?}"))?;
|
.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}...");
|
info!("Starting analysis for {name}...");
|
||||||
while let Some(container) = qmdl_stream
|
while let Some(maybe_message) = qmdl_reader
|
||||||
.try_next()
|
.get_next_message()
|
||||||
.await
|
.await
|
||||||
.expect("failed getting QMDL container")
|
.expect("failed to get message")
|
||||||
{
|
{
|
||||||
let _ = analysis_writer
|
let _ = analysis_writer
|
||||||
.analyze(container)
|
.analyze_message(maybe_message)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("{e:?}"))?;
|
.map_err(|e| format!("{e:?}"))?;
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-23
@@ -74,7 +74,7 @@ pub struct DiagTask {
|
|||||||
|
|
||||||
enum DiagState {
|
enum DiagState {
|
||||||
Recording {
|
Recording {
|
||||||
qmdl_writer: QmdlWriter<File>,
|
qmdl_writer: Box<QmdlWriter<File>>,
|
||||||
analysis_writer: Box<AnalysisWriter>,
|
analysis_writer: Box<AnalysisWriter>,
|
||||||
},
|
},
|
||||||
Stopped,
|
Stopped,
|
||||||
@@ -158,7 +158,7 @@ impl DiagTask {
|
|||||||
DiskSpaceCheck::Failed => {}
|
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
|
// For fixed-mode sessions, write the configured coordinates to the storage
|
||||||
// immediately so the per-session GPS is stored durably and isn't affected
|
// immediately so the per-session GPS is stored durably and isn't affected
|
||||||
@@ -185,13 +185,11 @@ impl DiagTask {
|
|||||||
.await
|
.await
|
||||||
.map_err(RecordingStoreError::WriteFileError)?;
|
.map_err(RecordingStoreError::WriteFileError)?;
|
||||||
}
|
}
|
||||||
|
self.stop_current_recording(qmdl_store).await;
|
||||||
self.stop_current_recording().await;
|
let qmdl_writer = Box::new(QmdlWriter::new(qmdl_gz_file));
|
||||||
let qmdl_writer = QmdlWriter::new(qmdl_file);
|
|
||||||
let analysis_writer = AnalysisWriter::new(analysis_file, &self.analyzer_config)
|
let analysis_writer = AnalysisWriter::new(analysis_file, &self.analyzer_config)
|
||||||
.await
|
.await
|
||||||
.map_err(RecordingStoreError::WriteFileError)?;
|
.map_err(RecordingStoreError::WriteFileError)?;
|
||||||
|
|
||||||
self.state = DiagState::Recording {
|
self.state = DiagState::Recording {
|
||||||
qmdl_writer,
|
qmdl_writer,
|
||||||
analysis_writer: Box::new(analysis_writer),
|
analysis_writer: Box::new(analysis_writer),
|
||||||
@@ -209,7 +207,7 @@ impl DiagTask {
|
|||||||
|
|
||||||
/// Stop recording, optionally annotating the entry with a reason.
|
/// Stop recording, optionally annotating the entry with a reason.
|
||||||
async fn stop(&mut self, qmdl_store: &mut RecordingStore, reason: Option<String>) {
|
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
|
if let Some(reason) = reason
|
||||||
&& let Err(e) = qmdl_store.set_current_stop_reason(reason).await
|
&& 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;
|
let mut state = DiagState::Stopped;
|
||||||
std::mem::swap(&mut self.state, &mut state);
|
std::mem::swap(&mut self.state, &mut state);
|
||||||
if let DiagState::Recording {
|
if let DiagState::Recording {
|
||||||
analysis_writer, ..
|
qmdl_writer,
|
||||||
|
analysis_writer,
|
||||||
|
..
|
||||||
} = state
|
} = state
|
||||||
{
|
{
|
||||||
analysis_writer
|
match (qmdl_writer.close().await, analysis_writer.close().await) {
|
||||||
.close()
|
(Ok(size), Ok(())) => {
|
||||||
.await
|
if let Err(err) = qmdl_store.update_current_entry_qmdl_size(size).await {
|
||||||
.expect("failed to close analysis writer");
|
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;
|
self.stop(qmdl_store, Some(reason)).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if let Ok(file_size) = qmdl_writer.size().await {
|
||||||
debug!(
|
debug!(
|
||||||
"total QMDL bytes written: {}, updating manifest...",
|
"total QMDL bytes written: {}, updating manifest...",
|
||||||
qmdl_writer.total_written
|
file_size
|
||||||
);
|
);
|
||||||
let index = qmdl_store
|
if let Err(e) = qmdl_store.update_current_entry_qmdl_size(file_size).await {
|
||||||
.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}");
|
let reason = format!("failed to update manifest (disk full?): {e}");
|
||||||
error!("{reason}");
|
error!("{reason}");
|
||||||
self.stop(qmdl_store, Some(reason)).await;
|
self.stop(qmdl_store, Some(reason)).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
debug!("done!");
|
debug!("done!");
|
||||||
|
}
|
||||||
|
|
||||||
// Extract the latest packet timestamp from this container
|
// Extract the latest packet timestamp from this container
|
||||||
if let Some(ts) = 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();
|
let container_bytes: usize = container.messages.iter().map(|m| m.data.len()).sum();
|
||||||
self.bytes_since_space_check += container_bytes;
|
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,
|
Ok(t) => t,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("failed to analyze container: {e}");
|
warn!("failed to analyze container: {e}");
|
||||||
@@ -501,7 +509,8 @@ pub fn run_diag_read_thread(
|
|||||||
// time to go
|
// time to go
|
||||||
Some(DiagDeviceCtrlMessage::Exit) | None => {
|
Some(DiagDeviceCtrlMessage::Exit) | None => {
|
||||||
info!("Diag reader thread exiting...");
|
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(())
|
return Ok(())
|
||||||
},
|
},
|
||||||
Some(DiagDeviceCtrlMessage::DeleteEntry { name, response_tx }) => {
|
Some(DiagDeviceCtrlMessage::DeleteEntry { name, response_tx }) => {
|
||||||
|
|||||||
@@ -7,10 +7,18 @@ use crate::config;
|
|||||||
use crate::display::DisplayState;
|
use crate::display::DisplayState;
|
||||||
|
|
||||||
pub fn update_ui(
|
pub fn update_ui(
|
||||||
_task_tracker: &TaskTracker,
|
task_tracker: &TaskTracker,
|
||||||
_config: &config::Config,
|
_config: &config::Config,
|
||||||
_shutdown_token: CancellationToken,
|
shutdown_token: CancellationToken,
|
||||||
_ui_update_rx: Receiver<DisplayState>,
|
mut ui_update_rx: Receiver<DisplayState>,
|
||||||
) {
|
) {
|
||||||
info!("Headless mode, not spawning UI.");
|
info!("Headless mode, not spawning UI.");
|
||||||
|
task_tracker.spawn(async move {
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = shutdown_token.cancelled() => break,
|
||||||
|
_ = ui_update_rx.recv() => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-17
@@ -10,12 +10,11 @@ use axum::http::StatusCode;
|
|||||||
use axum::http::header::CONTENT_TYPE;
|
use axum::http::header::CONTENT_TYPE;
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use log::error;
|
use log::error;
|
||||||
use rayhunter::diag::DataType;
|
use rayhunter::gsmtap::parser as gsmtap_parser;
|
||||||
use rayhunter::gsmtap_parser;
|
|
||||||
use rayhunter::pcap::{GpsPoint, GsmtapPcapWriter};
|
use rayhunter::pcap::{GpsPoint, GsmtapPcapWriter};
|
||||||
use rayhunter::qmdl::QmdlReader;
|
use rayhunter::qmdl::QmdlMessageReader;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::io::{AsyncRead, AsyncWrite, duplex};
|
use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, duplex};
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
|
|
||||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
#[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(),
|
"QMDL file is empty, try again in a bit!".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let qmdl_size_bytes = entry.qmdl_size_bytes;
|
|
||||||
let qmdl_file = qmdl_store
|
let qmdl_file = qmdl_store
|
||||||
.open_file(entry_index, FileKind::Qmdl)
|
.open_file(entry_index, FileKind::Qmdl)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))?
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))?
|
||||||
.ok_or((StatusCode::NOT_FOUND, "QMDL file not found".to_string()))?;
|
.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 (reader, writer) = duplex(1024);
|
||||||
let gps_records = load_gps_records_for_entry(&state, entry_index).await;
|
let gps_records = load_gps_records_for_entry(&state, entry_index).await;
|
||||||
drop(qmdl_store);
|
drop(qmdl_store);
|
||||||
|
|
||||||
tokio::spawn(async move {
|
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:?}");
|
error!("failed to generate PCAP: {e:?}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -131,24 +132,17 @@ fn find_nearest_gps(records: &[GpsRecord], packet_timestamp: i64) -> Option<GpsP
|
|||||||
|
|
||||||
pub async fn generate_pcap_data<R, W>(
|
pub async fn generate_pcap_data<R, W>(
|
||||||
writer: W,
|
writer: W,
|
||||||
qmdl_file: R,
|
mut reader: QmdlMessageReader<R>,
|
||||||
qmdl_size_bytes: usize,
|
|
||||||
gps_records: Vec<GpsRecord>,
|
gps_records: Vec<GpsRecord>,
|
||||||
) -> Result<(), Error>
|
) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
W: AsyncWrite + Unpin + Send,
|
W: AsyncWrite + Unpin + Send,
|
||||||
R: AsyncRead + Unpin,
|
R: AsyncRead + AsyncSeek + Unpin,
|
||||||
{
|
{
|
||||||
let mut pcap_writer = GsmtapPcapWriter::new(writer).await?;
|
let mut pcap_writer = GsmtapPcapWriter::new(writer).await?;
|
||||||
pcap_writer.write_iface_header().await?;
|
pcap_writer.write_iface_header().await?;
|
||||||
|
|
||||||
let mut reader = QmdlReader::new(qmdl_file, Some(qmdl_size_bytes));
|
while let Some(maybe_msg) = reader.get_next_message().await? {
|
||||||
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 {
|
match maybe_msg {
|
||||||
Ok(msg) => {
|
Ok(msg) => {
|
||||||
let maybe_gsmtap_msg = gsmtap_parser::parse(msg)?;
|
let maybe_gsmtap_msg = gsmtap_parser::parse(msg)?;
|
||||||
@@ -163,7 +157,6 @@ where
|
|||||||
Err(e) => error!("error parsing message: {e:?}"),
|
Err(e) => error!("error parsing message: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-25
@@ -55,16 +55,24 @@ impl FileKind {
|
|||||||
// List of all possible physical files on disk.
|
// List of all possible physical files on disk.
|
||||||
pub const ALL: &'static [FileKind] = &[FileKind::Qmdl, FileKind::Analysis, FileKind::Gps];
|
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 {
|
match self {
|
||||||
|
FileKind::Qmdl if qmdl_compressed => format!("{}.qmdl.gz", entry_name),
|
||||||
FileKind::Qmdl => format!("{}.qmdl", entry_name),
|
FileKind::Qmdl => format!("{}.qmdl", entry_name),
|
||||||
FileKind::Analysis => format!("{}.ndjson", entry_name),
|
FileKind::Analysis => format!("{}.ndjson", entry_name),
|
||||||
FileKind::Gps => format!("{}-gps.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 {
|
pub fn get_filepath<P: AsRef<Path>>(
|
||||||
base_path.as_ref().join(self.get_filename(entry_name))
|
&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
|
/// The system time when the last message was recorded to the file
|
||||||
#[cfg_attr(feature = "apidocs", schema(value_type = String))]
|
#[cfg_attr(feature = "apidocs", schema(value_type = String))]
|
||||||
pub last_message_time: Option<DateTime<Local>>,
|
pub last_message_time: Option<DateTime<Local>>,
|
||||||
/// The size of the QMDL file in bytes
|
|
||||||
pub qmdl_size_bytes: usize,
|
pub qmdl_size_bytes: usize,
|
||||||
/// The rayhunter daemon version which generated the file
|
/// The rayhunter daemon version which generated the file
|
||||||
pub rayhunter_version: Option<String>,
|
pub rayhunter_version: Option<String>,
|
||||||
@@ -116,6 +123,8 @@ pub struct ManifestEntry {
|
|||||||
pub upload_time: Option<DateTime<Local>>,
|
pub upload_time: Option<DateTime<Local>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub gps_mode: Option<GpsMode>,
|
pub gps_mode: Option<GpsMode>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub compressed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManifestEntry {
|
impl ManifestEntry {
|
||||||
@@ -133,11 +142,12 @@ impl ManifestEntry {
|
|||||||
stop_reason: None,
|
stop_reason: None,
|
||||||
upload_time: None,
|
upload_time: None,
|
||||||
gps_mode: Some(gps_mode),
|
gps_mode: Some(gps_mode),
|
||||||
|
compressed: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_filepath<P: AsRef<Path>>(&self, file_kind: FileKind, path: P) -> PathBuf {
|
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
|
// 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",
|
// QMDL files. We expect these files to be named like "<timestamp>.qmdl"
|
||||||
// and skip any files which don't match that pattern.
|
// or "<timestamp>.qmdl.gz", and skip any files which don't match that
|
||||||
|
// pattern.
|
||||||
pub async fn recover<P>(path: P) -> Result<Self, RecordingStoreError>
|
pub async fn recover<P>(path: P) -> Result<Self, RecordingStoreError>
|
||||||
where
|
where
|
||||||
P: AsRef<Path>,
|
P: AsRef<Path>,
|
||||||
@@ -217,11 +228,14 @@ impl RecordingStore {
|
|||||||
continue;
|
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;
|
continue;
|
||||||
}
|
};
|
||||||
|
|
||||||
let stem = filename.trim_end_matches(".qmdl");
|
|
||||||
let Ok(start_timestamp) = stem.parse::<i64>() else {
|
let Ok(start_timestamp) = stem.parse::<i64>() else {
|
||||||
warn!("QMDL file has invalid name {os_filename:?}, skipping");
|
warn!("QMDL file has invalid name {os_filename:?}, skipping");
|
||||||
continue;
|
continue;
|
||||||
@@ -248,6 +262,7 @@ impl RecordingStore {
|
|||||||
info!("successfully recovered QMDL entry {os_filename:?}!");
|
info!("successfully recovered QMDL entry {os_filename:?}!");
|
||||||
manifest_entries.push(ManifestEntry {
|
manifest_entries.push(ManifestEntry {
|
||||||
name: stem.to_string(),
|
name: stem.to_string(),
|
||||||
|
compressed,
|
||||||
start_time: start_time.into(),
|
start_time: start_time.into(),
|
||||||
last_message_time: Some(last_message_time.into()),
|
last_message_time: Some(last_message_time.into()),
|
||||||
qmdl_size_bytes: metadata.size() as usize,
|
qmdl_size_bytes: metadata.size() as usize,
|
||||||
@@ -322,7 +337,7 @@ impl RecordingStore {
|
|||||||
file_kind: FileKind,
|
file_kind: FileKind,
|
||||||
) -> Result<Option<File>, RecordingStoreError> {
|
) -> Result<Option<File>, RecordingStoreError> {
|
||||||
let entry = &self.manifest.entries[entry_index];
|
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 {
|
match File::open(&filepath).await {
|
||||||
Ok(file) => Ok(Some(file)),
|
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
|
// Sets the current entry's size and updates the last_message_time to now, updating the manifest
|
||||||
pub async fn update_entry_qmdl_size(
|
pub async fn update_current_entry_qmdl_size(
|
||||||
&mut self,
|
&mut self,
|
||||||
entry_index: usize,
|
|
||||||
size_bytes: usize,
|
size_bytes: usize,
|
||||||
) -> Result<(), RecordingStoreError> {
|
) -> 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].qmdl_size_bytes = size_bytes;
|
||||||
self.manifest.entries[entry_index].last_message_time =
|
self.manifest.entries[entry_index].last_message_time =
|
||||||
Some(rayhunter::clock::get_adjusted_now());
|
Some(rayhunter::clock::get_adjusted_now());
|
||||||
@@ -496,7 +513,11 @@ impl RecordingStore {
|
|||||||
self.write_manifest().await?;
|
self.write_manifest().await?;
|
||||||
|
|
||||||
for &file_kind in FileKind::ALL {
|
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)
|
remove_file_if_exists(&filepath)
|
||||||
.await
|
.await
|
||||||
.map_err(RecordingStoreError::DeleteFileError)?;
|
.map_err(RecordingStoreError::DeleteFileError)?;
|
||||||
@@ -513,7 +534,7 @@ impl RecordingStore {
|
|||||||
|
|
||||||
'entries: for entry in &self.manifest.entries {
|
'entries: for entry in &self.manifest.entries {
|
||||||
for &file_kind in FileKind::ALL {
|
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 {
|
if let Err(e) = remove_file_if_exists(&filepath).await {
|
||||||
log::warn!("failed to remove {filepath:?}: {e:?}");
|
log::warn!("failed to remove {filepath:?}: {e:?}");
|
||||||
// Some error happened with deleting this entry, abort and go to the next one.
|
// Some error happened with deleting this entry, abort and go to the next one.
|
||||||
@@ -575,10 +596,7 @@ mod tests {
|
|||||||
.is_none()
|
.is_none()
|
||||||
);
|
);
|
||||||
|
|
||||||
store
|
store.update_current_entry_qmdl_size(1000).await.unwrap();
|
||||||
.update_entry_qmdl_size(entry_index, 1000)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let (entry_index, entry) = store
|
let (entry_index, entry) = store
|
||||||
.entry_for_name(&store.manifest.entries[entry_index].name)
|
.entry_for_name(&store.manifest.entries[entry_index].name)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -601,11 +619,7 @@ mod tests {
|
|||||||
let dir = make_temp_dir();
|
let dir = make_temp_dir();
|
||||||
let mut store = RecordingStore::create(dir.path()).await.unwrap();
|
let mut store = RecordingStore::create(dir.path()).await.unwrap();
|
||||||
let _ = store.new_entry(GpsMode::Disabled).await.unwrap();
|
let _ = store.new_entry(GpsMode::Disabled).await.unwrap();
|
||||||
let entry_index = store.current_entry.unwrap();
|
store.update_current_entry_qmdl_size(1000).await.unwrap();
|
||||||
store
|
|
||||||
.update_entry_qmdl_size(entry_index, 1000)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let store = RecordingStore::create(dir.path()).await.unwrap();
|
let store = RecordingStore::create(dir.path()).await.unwrap();
|
||||||
assert_eq!(store.manifest.entries.len(), 0);
|
assert_eq!(store.manifest.entries.len(), 0);
|
||||||
}
|
}
|
||||||
|
|||||||
+96
-46
@@ -6,17 +6,22 @@ use axum::Json;
|
|||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::extract::Path;
|
use axum::extract::Path;
|
||||||
use axum::extract::State;
|
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::http::{HeaderValue, StatusCode};
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
|
use futures::TryStreamExt;
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
|
use rayhunter::qmdl::QmdlMessageReader;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::pin::pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::fs::write;
|
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::RwLock;
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
|
use tokio_util::compat::FuturesAsyncReadCompatExt;
|
||||||
use tokio_util::compat::FuturesAsyncWriteCompatExt;
|
use tokio_util::compat::FuturesAsyncWriteCompatExt;
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
@@ -67,7 +72,7 @@ pub async fn get_qmdl(
|
|||||||
) -> Result<Response, (StatusCode, String)> {
|
) -> Result<Response, (StatusCode, String)> {
|
||||||
let qmdl_idx = qmdl_name.trim_end_matches(".qmdl");
|
let qmdl_idx = qmdl_name.trim_end_matches(".qmdl");
|
||||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
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,
|
StatusCode::NOT_FOUND,
|
||||||
format!("couldn't find qmdl file with name {qmdl_idx}"),
|
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()))?;
|
.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_reader = QmdlMessageReader::new(qmdl_file).await.map_err(|err| {
|
||||||
let qmdl_stream = ReaderStream::new(limited_qmdl_file);
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("error reading QMDL file: {err}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let headers = [
|
let headers = [(CONTENT_TYPE, "application/octet-stream")];
|
||||||
(CONTENT_TYPE, "application/octet-stream"),
|
let body = Body::from_stream(qmdl_reader.into_qmdl_stream());
|
||||||
(CONTENT_LENGTH, &entry.qmdl_size_bytes.to_string()),
|
|
||||||
];
|
|
||||||
let body = Body::from_stream(qmdl_stream);
|
|
||||||
Ok((headers, body).into_response())
|
Ok((headers, body).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +340,7 @@ pub async fn get_zip(
|
|||||||
Path(entry_name): Path<String>,
|
Path(entry_name): Path<String>,
|
||||||
) -> Result<Response, (StatusCode, String)> {
|
) -> Result<Response, (StatusCode, String)> {
|
||||||
let qmdl_idx = entry_name.trim_end_matches(".zip").to_owned();
|
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 qmdl_store = state.qmdl_store_lock.read().await;
|
||||||
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_idx).ok_or((
|
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_idx).ok_or((
|
||||||
StatusCode::NOT_FOUND,
|
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();
|
let qmdl_store_lock = state.qmdl_store_lock.clone();
|
||||||
@@ -377,23 +383,34 @@ pub async fn get_zip(
|
|||||||
continue;
|
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,
|
Compression::Stored,
|
||||||
);
|
);
|
||||||
// FuturesAsyncWriteCompatExt::compat_write because async-zip's entrystream does
|
// FuturesAsyncWriteCompatExt::compat_write because async-zip's entrystream does
|
||||||
// not impl tokio's AsyncWrite, but only future's AsyncWrite. This can be removed
|
// 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.
|
// 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 {
|
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 {
|
} else {
|
||||||
copy(&mut file, &mut entry_writer).await?;
|
copy(&mut file, &mut entry_writer).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
entry_writer.into_inner().close().await?;
|
entry_writer.into_inner().close().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,16 +426,11 @@ pub async fn get_zip(
|
|||||||
.open_file(entry_index, FileKind::Qmdl)
|
.open_file(entry_index, FileKind::Qmdl)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| anyhow::anyhow!("QMDL file not found"))?
|
.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(
|
if let Err(e) =
|
||||||
&mut entry_writer,
|
generate_pcap_data(&mut entry_writer, qmdl_reader, gps_records).await
|
||||||
qmdl_file_for_pcap,
|
|
||||||
qmdl_size_bytes,
|
|
||||||
gps_records,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
{
|
||||||
// if we fail to generate the PCAP file, we should still continue and give the
|
// if we fail to generate the PCAP file, we should still continue and give the
|
||||||
// user the QMDL.
|
// user the QMDL.
|
||||||
@@ -532,10 +544,17 @@ pub async fn debug_set_display_state(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::GpsMode;
|
use crate::config::GpsMode;
|
||||||
use async_zip::base::read::mem::ZipFileReader;
|
use async_zip::base::read::mem::ZipFileReader;
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
|
use futures::AsyncReadExt;
|
||||||
|
use rayhunter::{
|
||||||
|
diag::{DataType, HdlcEncapsulatedMessage, Message, MessagesContainer},
|
||||||
|
qmdl::{QmdlMessageReader, QmdlWriter},
|
||||||
|
};
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
async fn create_test_qmdl_store() -> (TempDir, Arc<RwLock<crate::qmdl_store::RecordingStore>>) {
|
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(
|
async fn create_test_entry_with_data(
|
||||||
store_lock: &Arc<RwLock<crate::qmdl_store::RecordingStore>>,
|
store_lock: &Arc<RwLock<crate::qmdl_store::RecordingStore>>,
|
||||||
test_data: &[u8],
|
test_data: &MessagesContainer,
|
||||||
) -> String {
|
) -> String {
|
||||||
let entry_name = {
|
let entry_name = {
|
||||||
let mut store = store_lock.write().await;
|
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() {
|
let mut writer = QmdlWriter::new(&mut qmdl_gz_file);
|
||||||
use tokio::io::AsyncWriteExt;
|
writer.write_container(test_data).await.unwrap();
|
||||||
qmdl_file.write_all(test_data).await.unwrap();
|
writer.close().await.unwrap();
|
||||||
qmdl_file.flush().await.unwrap();
|
|
||||||
}
|
let qmdl_file_size = qmdl_gz_file.metadata().await.unwrap().len() as usize;
|
||||||
|
|
||||||
let current_entry = store.current_entry.unwrap();
|
let current_entry = store.current_entry.unwrap();
|
||||||
let entry = &store.manifest.entries[current_entry];
|
let entry = &store.manifest.entries[current_entry];
|
||||||
let entry_name = entry.name.clone();
|
let entry_name = entry.name.clone();
|
||||||
|
|
||||||
store
|
store
|
||||||
.update_entry_qmdl_size(current_entry, test_data.len())
|
.update_current_entry_qmdl_size(qmdl_file_size)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
entry_name
|
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]
|
#[tokio::test]
|
||||||
async fn test_get_zip_success() {
|
async fn test_get_zip_success() {
|
||||||
let (_temp_dir, store_lock) = create_test_qmdl_store().await;
|
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 entry_name = create_test_entry_with_data(&store_lock, &test_qmdl_data).await;
|
||||||
let state = create_test_server_state(store_lock);
|
let state = create_test_server_state(store_lock);
|
||||||
|
|
||||||
let result = get_zip(State(state), Path(entry_name.clone())).await;
|
let response = get_zip(State(state), Path(entry_name.clone()))
|
||||||
|
.await
|
||||||
assert!(result.is_ok());
|
.unwrap();
|
||||||
let response = result.unwrap();
|
|
||||||
|
|
||||||
let headers = response.headers();
|
let headers = response.headers();
|
||||||
assert_eq!(headers.get("content-type").unwrap(), "application/zip");
|
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 body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||||
|
|
||||||
let zip_reader = ZipFileReader::new(body_bytes.to_vec()).await.unwrap();
|
let zip_reader = ZipFileReader::new(body_bytes.to_vec()).await.unwrap();
|
||||||
|
let zip_reader_file = zip_reader.file();
|
||||||
let filenames = zip_reader
|
let filenames: Vec<String> = zip_reader_file
|
||||||
.file()
|
|
||||||
.entries()
|
.entries()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| entry.filename().as_str().unwrap().to_owned())
|
.map(|entry| entry.filename().as_str().unwrap().to_string())
|
||||||
.collect::<Vec<String>>();
|
.collect();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
filenames,
|
filenames,
|
||||||
vec![
|
vec![
|
||||||
@@ -639,5 +672,22 @@ mod tests {
|
|||||||
format!("{entry_name}.pcapng"),
|
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)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,8 @@ async fn try_upload_entry(
|
|||||||
shutdown_token: CancellationToken,
|
shutdown_token: CancellationToken,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let read_lock = store.read().await;
|
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;
|
let file = read_lock.open_file(entry_idx, file_kind).await;
|
||||||
drop(read_lock);
|
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! {
|
let res = select! {
|
||||||
_ = shutdown_token.cancelled() => {
|
_ = shutdown_token.cancelled() => {
|
||||||
@@ -299,7 +300,7 @@ mod tests {
|
|||||||
analysis_file.flush().await.unwrap();
|
analysis_file.flush().await.unwrap();
|
||||||
let entry_index = store.current_entry.unwrap();
|
let entry_index = store.current_entry.unwrap();
|
||||||
let name = store.manifest.entries[entry_index].name.clone();
|
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();
|
store.close_current_entry().await.unwrap();
|
||||||
(Arc::new(RwLock::new(store)), name)
|
(Arc::new(RwLock::new(store)), name)
|
||||||
}
|
}
|
||||||
@@ -331,7 +332,7 @@ mod tests {
|
|||||||
let recorded = captured.lock().await;
|
let recorded = captured.lock().await;
|
||||||
assert_eq!(recorded.len(), 3);
|
assert_eq!(recorded.len(), 3);
|
||||||
let paths: Vec<&str> = recorded.iter().map(|r| r.path.as_str()).collect();
|
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 ndjson_path = format!("dav/{}.ndjson", entry_name);
|
||||||
let gps_path = format!("dav/{}-gps.ndjson", entry_name);
|
let gps_path = format!("dav/{}-gps.ndjson", entry_name);
|
||||||
assert!(paths.contains(&qmdl_path.as_str()));
|
assert!(paths.contains(&qmdl_path.as_str()));
|
||||||
|
|||||||
Generated
+107
-89
@@ -27,7 +27,7 @@
|
|||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"typescript-eslint": "^8.59.0",
|
"typescript-eslint": "^8.59.0",
|
||||||
"vite": "^8.0.10",
|
"vite": "^8.0.16",
|
||||||
"vitest": "^4.1.5"
|
"vitest": "^4.1.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -329,9 +329,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.127.0",
|
"version": "0.133.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
||||||
"integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
|
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -346,9 +346,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
||||||
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
|
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -363,9 +363,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
|
||||||
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
|
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -380,9 +380,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
|
||||||
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
|
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -397,9 +397,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
|
||||||
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
|
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -414,9 +414,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
|
||||||
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
|
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -431,13 +431,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
|
||||||
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
|
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -448,13 +451,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
|
||||||
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
|
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -465,13 +471,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
|
||||||
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
|
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -482,13 +491,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
|
||||||
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
|
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -499,13 +511,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
|
||||||
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
|
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -516,13 +531,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
|
||||||
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
|
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -533,9 +551,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
|
||||||
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
|
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -550,9 +568,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
|
||||||
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
|
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
@@ -569,9 +587,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
||||||
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
|
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -586,9 +604,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
|
||||||
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
|
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -603,9 +621,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||||
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
|
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -2546,9 +2564,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2680,9 +2698,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.10",
|
"version": "8.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||||
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2700,7 +2718,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.12",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -2878,14 +2896,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
||||||
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
|
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.127.0",
|
"@oxc-project/types": "=0.133.0",
|
||||||
"@rolldown/pluginutils": "1.0.0-rc.17"
|
"@rolldown/pluginutils": "^1.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"rolldown": "bin/cli.mjs"
|
"rolldown": "bin/cli.mjs"
|
||||||
@@ -2894,21 +2912,21 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.0.0-rc.17",
|
"@rolldown/binding-android-arm64": "1.0.3",
|
||||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
|
"@rolldown/binding-darwin-arm64": "1.0.3",
|
||||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.17",
|
"@rolldown/binding-darwin-x64": "1.0.3",
|
||||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
|
"@rolldown/binding-freebsd-x64": "1.0.3",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
|
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
|
"@rolldown/binding-linux-arm64-musl": "1.0.3",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
|
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
|
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
|
"@rolldown/binding-linux-x64-gnu": "1.0.3",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
|
"@rolldown/binding-linux-x64-musl": "1.0.3",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
|
"@rolldown/binding-openharmony-arm64": "1.0.3",
|
||||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
|
"@rolldown/binding-wasm32-wasi": "1.0.3",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
|
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
|
"@rolldown/binding-win32-x64-msvc": "1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sade": {
|
"node_modules/sade": {
|
||||||
@@ -3183,9 +3201,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.16",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3316,17 +3334,17 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.10",
|
"version": "8.0.16",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
||||||
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
|
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
"postcss": "^8.5.10",
|
"postcss": "^8.5.15",
|
||||||
"rolldown": "1.0.0-rc.17",
|
"rolldown": "1.0.3",
|
||||||
"tinyglobby": "^0.2.16"
|
"tinyglobby": "^0.2.17"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
@@ -3342,7 +3360,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/node": "^20.19.0 || >=22.12.0",
|
"@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",
|
"esbuild": "^0.27.0 || ^0.28.0",
|
||||||
"jiti": ">=1.21.0",
|
"jiti": ">=1.21.0",
|
||||||
"less": "^4.0.0",
|
"less": "^4.0.0",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"typescript-eslint": "^8.59.0",
|
"typescript-eslint": "^8.59.0",
|
||||||
"vite": "^8.0.10",
|
"vite": "^8.0.16",
|
||||||
"vitest": "^4.1.5"
|
"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.
|
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
|
## 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.
|
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.
|
||||||
|
|||||||
Generated
+122
-104
@@ -29,13 +29,13 @@
|
|||||||
"svelte-check": "^4.4.6",
|
"svelte-check": "^4.4.6",
|
||||||
"typescript": "~6.0.3",
|
"typescript": "~6.0.3",
|
||||||
"typescript-eslint": "^8.58.2",
|
"typescript-eslint": "^8.58.2",
|
||||||
"vite": "^8.0.9"
|
"vite": "^8.0.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.9.2",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||||
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
|
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -44,9 +44,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.9.2",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||||
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
|
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -289,13 +289,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
|
||||||
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tybys/wasm-util": "^0.10.1"
|
"@tybys/wasm-util": "^0.10.2"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -307,9 +307,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.126.0",
|
"version": "0.133.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
||||||
"integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==",
|
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/Boshen"
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
@@ -323,9 +323,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
||||||
"integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==",
|
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -339,9 +339,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
|
||||||
"integrity": "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==",
|
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -355,9 +355,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
|
||||||
"integrity": "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==",
|
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -371,9 +371,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
|
||||||
"integrity": "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==",
|
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -387,9 +387,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
|
||||||
"integrity": "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==",
|
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -403,12 +403,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
|
||||||
"integrity": "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==",
|
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -419,12 +422,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
|
||||||
"integrity": "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==",
|
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -435,12 +441,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
|
||||||
"integrity": "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==",
|
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -451,12 +460,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
|
||||||
"integrity": "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==",
|
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -467,12 +479,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
|
||||||
"integrity": "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==",
|
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -483,12 +498,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
|
||||||
"integrity": "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==",
|
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -499,9 +517,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
|
||||||
"integrity": "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==",
|
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -515,17 +533,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
|
||||||
"integrity": "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==",
|
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/core": "1.9.2",
|
"@emnapi/core": "1.10.0",
|
||||||
"@emnapi/runtime": "1.9.2",
|
"@emnapi/runtime": "1.10.0",
|
||||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -533,9 +551,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
||||||
"integrity": "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==",
|
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -549,9 +567,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
|
||||||
"integrity": "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==",
|
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -565,9 +583,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||||
"integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==",
|
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@standard-schema/spec": {
|
"node_modules/@standard-schema/spec": {
|
||||||
@@ -1211,9 +1229,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.1",
|
"version": "0.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2611,9 +2629,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -2735,9 +2753,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.10",
|
"version": "8.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||||
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -2754,7 +2772,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.12",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -2932,13 +2950,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
||||||
"integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==",
|
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.126.0",
|
"@oxc-project/types": "=0.133.0",
|
||||||
"@rolldown/pluginutils": "1.0.0-rc.16"
|
"@rolldown/pluginutils": "^1.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"rolldown": "bin/cli.mjs"
|
"rolldown": "bin/cli.mjs"
|
||||||
@@ -2947,21 +2965,21 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.0.0-rc.16",
|
"@rolldown/binding-android-arm64": "1.0.3",
|
||||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.16",
|
"@rolldown/binding-darwin-arm64": "1.0.3",
|
||||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.16",
|
"@rolldown/binding-darwin-x64": "1.0.3",
|
||||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.16",
|
"@rolldown/binding-freebsd-x64": "1.0.3",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16",
|
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16",
|
"@rolldown/binding-linux-arm64-musl": "1.0.3",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16",
|
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16",
|
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16",
|
"@rolldown/binding-linux-x64-gnu": "1.0.3",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.16",
|
"@rolldown/binding-linux-x64-musl": "1.0.3",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.16",
|
"@rolldown/binding-openharmony-arm64": "1.0.3",
|
||||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.16",
|
"@rolldown/binding-wasm32-wasi": "1.0.3",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16",
|
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16"
|
"@rolldown/binding-win32-x64-msvc": "1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sade": {
|
"node_modules/sade": {
|
||||||
@@ -3146,9 +3164,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.16",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -3260,16 +3278,16 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.9",
|
"version": "8.0.16",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
||||||
"integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==",
|
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
"postcss": "^8.5.10",
|
"postcss": "^8.5.15",
|
||||||
"rolldown": "1.0.0-rc.16",
|
"rolldown": "1.0.3",
|
||||||
"tinyglobby": "^0.2.16"
|
"tinyglobby": "^0.2.17"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
@@ -3285,7 +3303,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/node": "^20.19.0 || >=22.12.0",
|
"@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",
|
"esbuild": "^0.27.0 || ^0.28.0",
|
||||||
"jiti": ">=1.21.0",
|
"jiti": ">=1.21.0",
|
||||||
"less": "^4.0.0",
|
"less": "^4.0.0",
|
||||||
|
|||||||
@@ -37,6 +37,6 @@
|
|||||||
"svelte-check": "^4.4.6",
|
"svelte-check": "^4.4.6",
|
||||||
"typescript": "~6.0.3",
|
"typescript": "~6.0.3",
|
||||||
"typescript-eslint": "^8.58.2",
|
"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 = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
clap = { version = "4.5.37" }
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
@@ -23,3 +24,4 @@ serde_json = "1"
|
|||||||
anyhow = "1.0.100"
|
anyhow = "1.0.100"
|
||||||
shlex = "1"
|
shlex = "1"
|
||||||
installer = { path = "../../installer" }
|
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 anyhow::Context;
|
||||||
|
use clap::CommandFactory;
|
||||||
use tauri::Emitter;
|
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<()> {
|
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")?;
|
let args_vec = shlex::split(&args).context("Failed to parse arguments: unclosed quote")?;
|
||||||
tauri::async_runtime::spawn_blocking(move || {
|
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:?}"))
|
.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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.invoke_handler(tauri::generate_handler![install_rayhunter])
|
.invoke_handler(tauri::generate_handler![install_rayhunter])
|
||||||
|
.invoke_handler(tauri::generate_handler![rayhunter_options])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.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 CONFIG_TOML: &str = include_str!("../../dist/config.toml.in");
|
||||||
static RAYHUNTER_DAEMON_INIT: &str = include_str!("../../dist/scripts/rayhunter_daemon");
|
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)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about)]
|
#[command(version, about)]
|
||||||
struct Args {
|
pub struct Args {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Command,
|
command: Command,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -346,9 +346,13 @@ async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> {
|
|||||||
admin_ip: admin_ip.to_owned(),
|
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
|
.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!("Listening on http://{}", listener.local_addr().unwrap());
|
||||||
println!("Please open above URL in your browser and log into the router to continue.");
|
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"
|
serde_json = "1.0"
|
||||||
num_enum = "0.7.4"
|
num_enum = "0.7.4"
|
||||||
utoipa = { version = "5.4.0", optional = true }
|
utoipa = { version = "5.4.0", optional = true }
|
||||||
|
async-compression = { version = "0.4.41", features = ["tokio", "gzip"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use crate::analysis::diagnostic::DiagnosticAnalyzer;
|
use crate::analysis::diagnostic::DiagnosticAnalyzer;
|
||||||
|
use crate::diag::{DiagParsingError, Message};
|
||||||
use crate::gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType};
|
use crate::gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType};
|
||||||
use crate::util::RuntimeMetadata;
|
use crate::util::RuntimeMetadata;
|
||||||
use crate::{diag::MessagesContainer, gsmtap_parser};
|
use crate::{diag::MessagesContainer, gsmtap::parser as gsmtap_parser};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer,
|
connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer,
|
||||||
@@ -223,7 +224,7 @@ impl AnalysisLineNormalizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug, Default)]
|
||||||
pub struct AnalysisRow {
|
pub struct AnalysisRow {
|
||||||
pub packet_timestamp: Option<DateTime<FixedOffset>>,
|
pub packet_timestamp: Option<DateTime<FixedOffset>>,
|
||||||
pub skipped_message_reason: Option<String>,
|
pub skipped_message_reason: Option<String>,
|
||||||
@@ -231,6 +232,10 @@ pub struct AnalysisRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AnalysisRow {
|
impl AnalysisRow {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.skipped_message_reason.is_none() && !self.contains_warnings()
|
self.skipped_message_reason.is_none() && !self.contains_warnings()
|
||||||
}
|
}
|
||||||
@@ -412,36 +417,30 @@ impl Harness {
|
|||||||
row
|
row
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn analyze_qmdl_messages(&mut self, container: MessagesContainer) -> Vec<AnalysisRow> {
|
pub fn analyze_qmdl_message(
|
||||||
let mut rows = Vec::new();
|
&mut self,
|
||||||
for maybe_qmdl_message in container.messages() {
|
maybe_qmdl_message: Result<Message, DiagParsingError>,
|
||||||
|
) -> AnalysisRow {
|
||||||
|
let mut row = AnalysisRow::new();
|
||||||
self.packet_num += 1;
|
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 {
|
let qmdl_message = match maybe_qmdl_message {
|
||||||
Ok(msg) => msg,
|
Ok(msg) => msg,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
row.skipped_message_reason = Some(format!("{err:?}"));
|
row.skipped_message_reason = Some(format!("{err:?}"));
|
||||||
continue;
|
return row;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let gsmtap_message = match gsmtap_parser::parse(qmdl_message) {
|
let gsmtap_message = match gsmtap_parser::parse(qmdl_message) {
|
||||||
Ok(msg) => msg,
|
Ok(msg) => msg,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
row.skipped_message_reason = Some(format!("{err:?}"));
|
row.skipped_message_reason = Some(format!("{err:?}"));
|
||||||
continue;
|
return row;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some((timestamp, gsmtap_msg)) = gsmtap_message else {
|
let Some((timestamp, gsmtap_msg)) = gsmtap_message else {
|
||||||
continue;
|
return row;
|
||||||
};
|
};
|
||||||
row.packet_timestamp = Some(timestamp.to_datetime());
|
row.packet_timestamp = Some(timestamp.to_datetime());
|
||||||
|
|
||||||
@@ -449,13 +448,20 @@ impl Harness {
|
|||||||
Ok(element) => element,
|
Ok(element) => element,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
row.skipped_message_reason = Some(format!("{err:?}"));
|
row.skipped_message_reason = Some(format!("{err:?}"));
|
||||||
continue;
|
return row;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
row.events = self.analyze_information_element(&element);
|
row.events = self.analyze_information_element(&element);
|
||||||
|
row
|
||||||
}
|
}
|
||||||
rows
|
|
||||||
|
pub fn analyze_qmdl_messages(&mut self, container: MessagesContainer) -> Vec<AnalysisRow> {
|
||||||
|
container
|
||||||
|
.messages()
|
||||||
|
.drain(..)
|
||||||
|
.map(|maybe_message| self.analyze_qmdl_message(maybe_message))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec<Option<Event>> {
|
fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec<Option<Event>> {
|
||||||
|
|||||||
@@ -1,156 +1,9 @@
|
|||||||
//! Diag protocol serialization/deserialization
|
//! Diag LogBody serialization/deserialization
|
||||||
|
|
||||||
use chrono::{DateTime, FixedOffset};
|
use chrono::{DateTime, FixedOffset};
|
||||||
use crc::{Algorithm, Crc};
|
|
||||||
use deku::prelude::*;
|
use deku::prelude::*;
|
||||||
|
|
||||||
use crate::hdlc::{self, hdlc_decapsulate};
|
pub mod rrc;
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||||
#[deku(ctx = "log_type: u16, hdr_len: u16", id = "log_type")]
|
#[deku(ctx = "log_type: u16, hdr_len: u16", id = "log_type")]
|
||||||
@@ -183,7 +36,7 @@ pub enum LogBody {
|
|||||||
LteRrcOtaMessage {
|
LteRrcOtaMessage {
|
||||||
ext_header_version: u8,
|
ext_header_version: u8,
|
||||||
#[deku(ctx = "*ext_header_version")]
|
#[deku(ctx = "*ext_header_version")]
|
||||||
packet: LteRrcOtaPacket,
|
packet: rrc::LteRrcOtaPacket,
|
||||||
},
|
},
|
||||||
// the four NAS command opcodes refer to:
|
// the four NAS command opcodes refer to:
|
||||||
// * 0xb0e2: plain ESM NAS message (incoming)
|
// * 0xb0e2: plain ESM NAS message (incoming)
|
||||||
@@ -237,113 +90,6 @@ pub enum Nas4GMessageDirection {
|
|||||||
Uplink,
|
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)]
|
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||||
#[deku(endian = "little")]
|
#[deku(endian = "little")]
|
||||||
pub struct Timestamp {
|
pub struct Timestamp {
|
||||||
@@ -364,55 +110,90 @@ 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),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
pub(crate) mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::{diag::*, hdlc};
|
||||||
|
|
||||||
|
#[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],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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));
|
||||||
|
}
|
||||||
|
|
||||||
// Just about all of these test cases from manually parsing diag packets w/ QCSuper
|
// Just about all of these test cases from manually parsing diag packets w/ QCSuper
|
||||||
|
|
||||||
@@ -478,42 +259,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 {
|
fn make_container(data_type: DataType, message: HdlcEncapsulatedMessage) -> MessagesContainer {
|
||||||
MessagesContainer {
|
MessagesContainer {
|
||||||
data_type,
|
data_type,
|
||||||
@@ -525,7 +270,7 @@ mod test {
|
|||||||
// this log is based on one captured on a real device -- if it fails to
|
// 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
|
// serialize or deserialize, that's probably a problem with this mock, not
|
||||||
// the DiagReader implementation
|
// 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 length_with_payload = 31 + payload.len() as u16;
|
||||||
let message = Message::Log {
|
let message = Message::Log {
|
||||||
pending_msgs: 0,
|
pending_msgs: 0,
|
||||||
@@ -537,7 +282,7 @@ mod test {
|
|||||||
},
|
},
|
||||||
body: LogBody::LteRrcOtaMessage {
|
body: LogBody::LteRrcOtaMessage {
|
||||||
ext_header_version: 20,
|
ext_header_version: 20,
|
||||||
packet: LteRrcOtaPacket::V8 {
|
packet: diaglog::rrc::LteRrcOtaPacket::V8 {
|
||||||
rrc_rel_maj: 14,
|
rrc_rel_maj: 14,
|
||||||
rrc_rel_min: 48,
|
rrc_rel_min: 48,
|
||||||
bearer_id: 0,
|
bearer_id: 0,
|
||||||
@@ -559,6 +304,8 @@ mod test {
|
|||||||
len: encapsulated_data.len() as u32,
|
len: encapsulated_data.len() as u32,
|
||||||
data: encapsulated_data,
|
data: encapsulated_data,
|
||||||
};
|
};
|
||||||
|
// sanity check
|
||||||
|
assert_eq!(&Message::from_hdlc(&encapsulated.data).unwrap(), &message);
|
||||||
(encapsulated, message)
|
(encapsulated, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -619,50 +366,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]
|
#[test]
|
||||||
fn test_fuzz_crash_response_opcode_parsing() {
|
fn test_fuzz_crash_response_opcode_parsing() {
|
||||||
// Regression test: Upgrading to deku 0.20 caused incorrect parsing of Response messages.
|
// 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
use deku::prelude::*;
|
use deku::prelude::*;
|
||||||
use num_enum::TryFromPrimitive;
|
use num_enum::TryFromPrimitive;
|
||||||
|
|
||||||
|
pub mod parser;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
pub enum GsmtapType {
|
pub enum GsmtapType {
|
||||||
Um(UmSubtype),
|
Um(UmSubtype),
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::diag::*;
|
use crate::diag::Message;
|
||||||
use crate::gsmtap::*;
|
use crate::diag::diaglog::{LogBody, Nas4GMessageDirection, Timestamp};
|
||||||
|
use crate::gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType, LteNasSubtype, LteRrcSubtype};
|
||||||
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@@ -135,7 +136,7 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut header = GsmtapHeader::new(gsmtap_type);
|
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.frame_number = packet.get_sfn();
|
||||||
header.subslot = packet.get_subfn();
|
header.subslot = packet.get_subfn();
|
||||||
Ok(Some(GsmtapMessage {
|
Ok(Some(GsmtapMessage {
|
||||||
@@ -158,3 +159,23 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@ pub mod analysis;
|
|||||||
pub mod clock;
|
pub mod clock;
|
||||||
pub mod diag;
|
pub mod diag;
|
||||||
pub mod gsmtap;
|
pub mod gsmtap;
|
||||||
pub mod gsmtap_parser;
|
|
||||||
pub mod hdlc;
|
pub mod hdlc;
|
||||||
pub mod log_codes;
|
pub mod log_codes;
|
||||||
pub mod pcap;
|
pub mod pcap;
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
//! Parse QMDL files and create a pcap file.
|
//! 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.
|
//! 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 crate::gsmtap::GsmtapMessage;
|
||||||
|
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
|
|||||||
+297
-148
@@ -3,109 +3,214 @@
|
|||||||
//! QmdlReader and QmdlWriter can read and write MessagesContainers to and from
|
//! QmdlReader and QmdlWriter can read and write MessagesContainers to and from
|
||||||
//! QMDL files.
|
//! 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 futures::TryStream;
|
||||||
use log::error;
|
use tokio::io::{
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
|
AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt,
|
||||||
|
BufReader,
|
||||||
|
};
|
||||||
|
|
||||||
|
const GZIP_MAGIC_NUMBER: u16 = 0x1f8b;
|
||||||
|
|
||||||
pub struct QmdlWriter<T>
|
pub struct QmdlWriter<T>
|
||||||
where
|
where
|
||||||
T: AsyncWrite + Unpin,
|
T: AsyncWrite + Unpin,
|
||||||
{
|
{
|
||||||
writer: T,
|
writer: GzipEncoder<T>,
|
||||||
pub total_written: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> QmdlWriter<T>
|
impl<T> QmdlWriter<T>
|
||||||
where
|
where
|
||||||
T: AsyncWrite + Unpin,
|
T: AsyncWrite + AsyncSeek + Unpin,
|
||||||
{
|
{
|
||||||
pub fn new(writer: T) -> Self {
|
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 {
|
pub async fn size(&mut self) -> std::io::Result<usize> {
|
||||||
QmdlWriter {
|
let size = self.writer.get_mut().stream_position().await?;
|
||||||
writer,
|
Ok(size as usize)
|
||||||
total_written: existing_size,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn write_container(&mut self, container: &MessagesContainer) -> std::io::Result<()> {
|
pub async fn write_container(&mut self, container: &MessagesContainer) -> std::io::Result<()> {
|
||||||
for msg in &container.messages {
|
for msg in &container.messages {
|
||||||
self.writer.write_all(&msg.data).await?;
|
self.writer.write_all(&msg.data).await?;
|
||||||
self.total_written += msg.data.len();
|
|
||||||
}
|
}
|
||||||
|
self.writer.flush().await?;
|
||||||
Ok(())
|
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
|
where
|
||||||
T: AsyncRead,
|
T: AsyncRead,
|
||||||
{
|
{
|
||||||
reader: BufReader<T>,
|
pub fn new(reader: T, compressed: bool) -> Self {
|
||||||
bytes_read: usize,
|
let source = if compressed {
|
||||||
max_bytes: Option<usize>,
|
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
|
where
|
||||||
T: AsyncRead + Unpin,
|
T: AsyncRead + Unpin,
|
||||||
{
|
{
|
||||||
pub fn new(reader: T, max_bytes: Option<usize>) -> Self {
|
fn poll_read(
|
||||||
QmdlReader {
|
self: Pin<&mut Self>,
|
||||||
reader: BufReader::new(reader),
|
cx: &mut std::task::Context<'_>,
|
||||||
bytes_read: 0,
|
buf: &mut tokio::io::ReadBuf<'_>,
|
||||||
max_bytes,
|
) -> 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(()));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_stream(
|
match Pin::new(reader).poll_read(cx, buf) {
|
||||||
&mut self,
|
// if we hit an unexpected EOF in a Gzip file, it shouldn't
|
||||||
) -> impl TryStream<Ok = MessagesContainer, Error = std::io::Error> + '_ {
|
// be considered fatal, just a truncated file. mark that
|
||||||
futures::stream::try_unfold(self, |reader| async {
|
// we're done and return the result as usual
|
||||||
let maybe_container = reader.get_next_messages_container().await?;
|
Poll::Ready(Err(err)) if err.kind() == ErrorKind::UnexpectedEof => {
|
||||||
match maybe_container {
|
*eof = true;
|
||||||
Some(container) => Ok(Some((container, reader))),
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
res => res,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QmdlReaderSource::Uncompressed { reader } => Pin::new(reader).poll_read(cx, buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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),
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_next_messages_container(
|
pub async fn get_next_message(
|
||||||
&mut self,
|
&mut self,
|
||||||
) -> Result<Option<MessagesContainer>, std::io::Error> {
|
) -> Result<Option<Result<Message, DiagParsingError>>, std::io::Error> {
|
||||||
if let Some(max_bytes) = self.max_bytes
|
let mut buf = vec![];
|
||||||
&& self.bytes_read >= max_bytes
|
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);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
Ok(Some(Message::from_hdlc(&buf)))
|
||||||
let bytes_read = self.reader.read_until(MESSAGE_TERMINATOR, &mut buf).await?;
|
}
|
||||||
self.bytes_read += bytes_read;
|
}
|
||||||
|
|
||||||
// Since QMDL is just a flat list of messages, we can't actually
|
impl<T> AsyncRead for QmdlMessageReader<T>
|
||||||
// reproduce the container structure they came from in the original
|
where
|
||||||
// read. So we'll just pretend that all containers had exactly one
|
T: AsyncRead + Unpin,
|
||||||
// message. As far as I know, the number of messages per container
|
{
|
||||||
// doesn't actually affect anything, so this should be fine.
|
fn poll_read(
|
||||||
Ok(Some(MessagesContainer {
|
self: Pin<&mut Self>,
|
||||||
data_type: DataType::UserSpace,
|
cx: &mut std::task::Context<'_>,
|
||||||
num_messages: 1,
|
buf: &mut tokio::io::ReadBuf<'_>,
|
||||||
messages: vec![HdlcEncapsulatedMessage {
|
) -> Poll<std::io::Result<()>> {
|
||||||
len: bytes_read as u32,
|
Pin::new(&mut self.get_mut().buf_reader).poll_read(cx, buf)
|
||||||
data: buf,
|
|
||||||
}],
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,132 +218,176 @@ where
|
|||||||
mod test {
|
mod test {
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
use crate::diag::CRC_CCITT;
|
use crate::diag::{DataType, HdlcEncapsulatedMessage, diaglog::test::get_test_message};
|
||||||
use crate::hdlc::hdlc_encapsulate;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn get_test_messages() -> Vec<HdlcEncapsulatedMessage> {
|
fn get_test_messages() -> (Vec<HdlcEncapsulatedMessage>, Vec<Message>) {
|
||||||
let messages: Vec<HdlcEncapsulatedMessage> = (10..20)
|
let mut hdlcs = Vec::new();
|
||||||
.map(|i| {
|
let mut messages = Vec::new();
|
||||||
let data = hdlc_encapsulate(&vec![i as u8; i], &CRC_CCITT);
|
for i in 10..20 {
|
||||||
HdlcEncapsulatedMessage {
|
let (hdlc, msg) = get_test_message(&[i]);
|
||||||
len: data.len() as u32,
|
hdlcs.push(hdlc);
|
||||||
data,
|
messages.push(msg);
|
||||||
}
|
}
|
||||||
})
|
(hdlcs, messages)
|
||||||
.collect();
|
|
||||||
messages
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns a byte array consisting of concatenated HDLC encapsulated
|
// returns a byte array consisting of concatenated HDLC encapsulated
|
||||||
// test messages
|
// test messages
|
||||||
fn get_test_message_bytes() -> Vec<u8> {
|
fn get_test_message_bytes() -> Vec<u8> {
|
||||||
get_test_messages()
|
let (hdlcs, _) = get_test_messages();
|
||||||
.iter()
|
hdlcs.iter().flat_map(|msg| msg.data.clone()).collect()
|
||||||
.flat_map(|msg| msg.data.clone())
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_test_containers() -> Vec<MessagesContainer> {
|
fn get_test_containers() -> Vec<MessagesContainer> {
|
||||||
let messages = get_test_messages();
|
let (hdlcs, _) = get_test_messages();
|
||||||
let (messages1, messages2) = messages.split_at(5);
|
let (hdlcs1, hdlcs2) = hdlcs.split_at(5);
|
||||||
vec![
|
vec![
|
||||||
MessagesContainer {
|
MessagesContainer {
|
||||||
data_type: DataType::UserSpace,
|
data_type: DataType::UserSpace,
|
||||||
num_messages: messages1.len() as u32,
|
num_messages: hdlcs1.len() as u32,
|
||||||
messages: messages1.to_vec(),
|
messages: hdlcs1.to_vec(),
|
||||||
},
|
},
|
||||||
MessagesContainer {
|
MessagesContainer {
|
||||||
data_type: DataType::UserSpace,
|
data_type: DataType::UserSpace,
|
||||||
num_messages: messages2.len() as u32,
|
num_messages: hdlcs2.len() as u32,
|
||||||
messages: messages2.to_vec(),
|
messages: hdlcs2.to_vec(),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_unbounded_qmdl_reader() {
|
async fn test_qmdl_reader() {
|
||||||
let mut buf = Cursor::new(get_test_message_bytes());
|
let mut buf = Cursor::new(get_test_message_bytes());
|
||||||
let mut reader = QmdlReader::new(&mut buf, None);
|
let mut reader = QmdlMessageReader::new(&mut buf).await.unwrap();
|
||||||
let expected_messages = get_test_messages();
|
assert!(!reader.is_compressed());
|
||||||
for message in expected_messages {
|
let (_, expected_messages) = get_test_messages();
|
||||||
let expected_container = MessagesContainer {
|
for msg in expected_messages {
|
||||||
data_type: DataType::UserSpace,
|
assert_eq!(Ok(msg), reader.get_next_message().await.unwrap().unwrap());
|
||||||
num_messages: 1,
|
|
||||||
messages: vec![message],
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
expected_container,
|
|
||||||
reader.get_next_messages_container().await.unwrap().unwrap()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_bounded_qmdl_reader() {
|
async fn test_truncation() {
|
||||||
let mut buf = Cursor::new(get_test_message_bytes());
|
run_truncation_tests(false).await;
|
||||||
|
|
||||||
// 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]
|
#[tokio::test]
|
||||||
async fn test_qmdl_writer() {
|
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 buf = Vec::new();
|
||||||
let mut writer = QmdlWriter::new(&mut buf);
|
let mut compressed_lengths = Vec::new();
|
||||||
let expected_containers = get_test_containers();
|
let mut writer = GzipEncoder::new(&mut buf);
|
||||||
for container in &expected_containers {
|
for hdlc in &hdlcs {
|
||||||
writer.write_container(container).await.unwrap();
|
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);
|
||||||
}
|
}
|
||||||
assert_eq!(writer.total_written, buf.len());
|
(buf, compressed_lengths)
|
||||||
assert_eq!(buf, get_test_message_bytes());
|
} 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 {
|
||||||
|
assert_eq!(
|
||||||
|
Ok(message),
|
||||||
|
reader.get_next_message().await.unwrap().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(matches!(reader.get_next_message().await, Ok(None)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_writing_and_reading() {
|
async fn test_compressed_reading_and_writing() {
|
||||||
let mut buf = Vec::new();
|
run_compressed_reading_and_writing_tests(true).await;
|
||||||
let mut writer = QmdlWriter::new(&mut buf);
|
run_compressed_reading_and_writing_tests(false).await;
|
||||||
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)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use deku::prelude::*;
|
use deku::prelude::*;
|
||||||
use rayhunter::{
|
use rayhunter::{
|
||||||
diag::{LogBody, LteRrcOtaPacket, Message, Timestamp},
|
diag::Message,
|
||||||
gsmtap_parser,
|
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
|
// 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