Compare commits

...

24 Commits

Author SHA1 Message Date
Will Greenberg cd28354bec run cargo fmt 2026-06-25 15:45:59 -07:00
Will Greenberg 1e4b812273 lib: refactor gsmtap/gsmtap_parser into a single module
This'll allow us to break out more specific GSMTAP parsing into
submodules more easily.
2026-06-25 15:45:59 -07:00
Will Greenberg 04cf2cbd67 lib/diag.rs refactor
This splits diag.rs, which was growing way too big for my taste, into a
number of submodules. This should help us compartmentalize tests better,
as well as use mod namespaces to shorten our struct/enum names.
2026-06-25 15:45:59 -07:00
Will Greenberg 9627cec737 Refactors in response to review comments
A few minor refactors, and a more major one that renames
RecordingStore's update_entry_qmdl_size to
update_current_entry_qmdl_size, since the only time we're ever updating
an entry's QMDL size is when it's the current one.
2026-06-22 19:40:54 -07:00
Will Greenberg 10f560b5e4 more clippy appeasement 2026-06-22 19:40:54 -07:00
Will Greenberg 88d2725427 fix clippy 2026-06-22 19:40:54 -07:00
Will Greenberg 76ae8fccd9 run cargo fmt 2026-06-22 19:40:54 -07:00
Will Greenberg 94b989c3c0 Add support for compressed QMDL
Major changes:
* QmdlWriter now outputs gzipped QMDL files by default
* QmdlReader renamed to QmdlMessageReader, and reads both compressed and
  uncompressed QMDL. It no longer requires bounding to avoid reading
  partially written files.
2026-06-22 19:40:54 -07:00
Brad Warren f5a0cddc88 improve basic logging 2026-06-18 15:25:12 -07:00
Brad Warren 2702ee0828 add basic logging
Co-authored-by: Will Greenberg <ifnspifn@gmail.com>
2026-06-18 15:25:12 -07:00
Brad Warren 58338850dd move comment to better location 2026-06-18 15:25:12 -07:00
Brad Warren d122ce6e6d convert introspect::Argument::new to ::try_new 2026-06-18 15:25:12 -07:00
Brad Warren 9280067e31 better document the effect of arg_modifiers order 2026-06-18 15:25:12 -07:00
Brad Warren 3b3532d3fd add modifiers.rs 2026-06-18 15:25:12 -07:00
Brad Warren 30c4cb0e0c add basic rayhunter_options endpoint 2026-06-18 15:25:12 -07:00
Deven Ducommun 17a9dfe0ff fix(parsing): mask EARFCN to 14 bits to prevent GSMTAP serialization panic
LTE EARFCNs can exceed the 14-bit GSMTAP ARFCN field maximum (16383),
causing Deku to panic with "bit size of input is larger than bit
requested size: 16 exceeds 14". This broke pcap generation for
international captures (e.g. UK, Switzerland).

Mask the value to 14 bits per the GSMTAP spec instead of panicking.

Fixes #1012, fixes #945

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-17 11:28:23 -07:00
dependabot[bot] b3f63864ad Bump vite in /daemon/web in the security group across 1 directory
Bumps the security group with 1 update in the /daemon/web directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 8.0.10 to 8.0.16
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:development
  dependency-group: security
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 10:29:37 -07:00
dependabot[bot] cfefa3c901 Bump vite in /installer-gui in the security group across 1 directory
Bumps the security group with 1 update in the /installer-gui directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 8.0.9 to 8.0.16
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:development
  dependency-group: security
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 10:29:11 -07:00
Brad Warren ce821b825f add set_versions.sh 2026-06-12 15:52:01 -07:00
Brad Warren 772dac681e run everything on tag pushes 2026-06-12 15:50:17 -07:00
Brad Warren 2df331a0bc run the release when the tag is pushed 2026-06-12 15:50:17 -07:00
Deven Ducommun 0a1dce3215 fix(daemon): drain UI channel in headless mode to prevent panic
On PinePhone (headless display), the UI update receiver was dropped
immediately, causing sends from diag.rs to fail with SendError and
panic. Spawn a task that drains the channel until shutdown.

Fixes #657

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-10 11:06:34 -07:00
KAI Maintainer 2df31300e3 fix(installer): improve error message when port 4000 is already in use
Replace .unwrap() on TcpListener::bind with .with_context() so users
see a clear message about the port conflict instead of a panic.

Fixes #906
2026-06-10 00:11:43 +02:00
Brad Warren fbd8110be9 remove another instance of outdated docs 2026-05-29 13:17:32 -07:00
36 changed files with 1755 additions and 1030 deletions
+2 -1
View File
@@ -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"
+4 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 }) => {
+11 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)),
);
} }
} }
+5 -4
View File
@@ -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()));
+107 -89
View File
@@ -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",
+1 -1
View File
@@ -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"
} }
} }
-6
View File
@@ -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.
+122 -104
View File
@@ -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",
+1 -1
View File
@@ -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"
} }
} }
+2
View File
@@ -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"
+117
View File
@@ -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,
}
}
}
+14
View File
@@ -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");
} }
+268
View File
@@ -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()
}
}
+3 -1
View File
@@ -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,
} }
+6 -2
View File
@@ -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.");
+1
View File
@@ -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"
+24 -18
View File
@@ -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>> {
+89 -386
View File
@@ -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.
+110
View File
@@ -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,
}
}
}
+205
View File
@@ -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());
}
}
-1
View File
@@ -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
View File
@@ -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
View File
@@ -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)
));
} }
} }
+3 -2
View File
@@ -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
+32
View File
@@ -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