diff --git a/.gitignore b/.gitignore index 2c96eb1..3c96bf2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ Cargo.lock +perf.* diff --git a/Cargo.toml b/Cargo.toml index ce89e51..e67c8c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,11 @@ name = "rpki" version = "0.1.0" edition = "2024" +[features] +default = ["full"] +# Full build used by the main RP implementation (includes RocksDB-backed storage). +full = ["dep:rocksdb"] + [dependencies] der-parser = { version = "10.0.0", features = ["serialize"] } hex = "0.4.3" @@ -16,7 +21,7 @@ url = "2.5.8" serde = { version = "1.0.218", features = ["derive"] } serde_json = "1.0.140" toml = "0.8.20" -rocksdb = { version = "0.22.0", default-features = false, features = ["lz4"] } +rocksdb = { version = "0.22.0", optional = true, default-features = false, features = ["lz4"] } serde_cbor = "0.11.2" roxmltree = "0.20.0" uuid = { version = "1.7.0", features = ["v4"] } diff --git a/benchmark/ours_manifest_bench/Cargo.toml b/benchmark/ours_manifest_bench/Cargo.toml new file mode 100644 index 0000000..a2f2def --- /dev/null +++ b/benchmark/ours_manifest_bench/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "ours-manifest-bench" +version = "0.1.0" +edition = "2024" + +[dependencies] +rpki = { path = "../..", default-features = false } + diff --git a/benchmark/ours_manifest_bench/src/main.rs b/benchmark/ours_manifest_bench/src/main.rs new file mode 100644 index 0000000..f0cded9 --- /dev/null +++ b/benchmark/ours_manifest_bench/src/main.rs @@ -0,0 +1,145 @@ +use rpki::data_model::manifest::ManifestObject; +use std::hint::black_box; +use std::path::PathBuf; +use std::time::Instant; + +#[derive(Debug, Clone)] +struct Config { + sample: Option, + manifest_path: Option, + iterations: u64, + warmup_iterations: u64, + repeats: u32, +} + +fn usage_and_exit() -> ! { + eprintln!( + "Usage:\n ours-manifest-bench (--sample | --manifest ) [--iterations N] [--warmup-iterations N] [--repeats N]\n\nExamples:\n cargo run --release -- --sample small-01 --iterations 20000 --warmup-iterations 2000 --repeats 3\n cargo run --release -- --manifest ../../tests/benchmark/selected_der/small-01.mft" + ); + std::process::exit(2); +} + +fn parse_args() -> Config { + let mut sample: Option = None; + let mut manifest_path: Option = None; + let mut iterations: u64 = 20_000; + let mut warmup_iterations: u64 = 2_000; + let mut repeats: u32 = 3; + + let mut args = std::env::args().skip(1); + while let Some(arg) = args.next() { + match arg.as_str() { + "--sample" => sample = Some(args.next().unwrap_or_else(|| usage_and_exit())), + "--manifest" => { + manifest_path = Some(PathBuf::from(args.next().unwrap_or_else(|| usage_and_exit()))) + } + "--iterations" => { + iterations = args + .next() + .unwrap_or_else(|| usage_and_exit()) + .parse() + .unwrap_or_else(|_| usage_and_exit()) + } + "--warmup-iterations" => { + warmup_iterations = args + .next() + .unwrap_or_else(|| usage_and_exit()) + .parse() + .unwrap_or_else(|_| usage_and_exit()) + } + "--repeats" => { + repeats = args + .next() + .unwrap_or_else(|| usage_and_exit()) + .parse() + .unwrap_or_else(|_| usage_and_exit()) + } + "-h" | "--help" => usage_and_exit(), + _ => usage_and_exit(), + } + } + + if sample.is_none() && manifest_path.is_none() { + usage_and_exit(); + } + if sample.is_some() && manifest_path.is_some() { + usage_and_exit(); + } + + Config { + sample, + manifest_path, + iterations, + warmup_iterations, + repeats, + } +} + +fn derive_manifest_path(sample: &str) -> PathBuf { + // Assumes current working directory is `rpki/benchmark/ours_manifest_bench`. + PathBuf::from(format!("../../tests/benchmark/selected_der/{sample}.mft")) +} + +fn main() { + let cfg = parse_args(); + let manifest_path = cfg + .manifest_path + .clone() + .unwrap_or_else(|| derive_manifest_path(cfg.sample.as_deref().unwrap())); + + let bytes = std::fs::read(&manifest_path).unwrap_or_else(|e| { + eprintln!("read manifest fixture failed: {e}; path={}", manifest_path.display()); + std::process::exit(1); + }); + + let decoded_once = ManifestObject::decode_der(&bytes).unwrap_or_else(|e| { + eprintln!("decode failed: {e}; path={}", manifest_path.display()); + std::process::exit(1); + }); + let file_count = decoded_once.manifest.file_count(); + + let mut round_ns_per_op: Vec = Vec::with_capacity(cfg.repeats as usize); + let mut round_ops_per_s: Vec = Vec::with_capacity(cfg.repeats as usize); + + for _round in 0..cfg.repeats { + for _ in 0..cfg.warmup_iterations { + let obj = ManifestObject::decode_der(black_box(&bytes)).expect("warmup decode"); + black_box(obj); + } + + let start = Instant::now(); + for _ in 0..cfg.iterations { + let obj = ManifestObject::decode_der(black_box(&bytes)).expect("timed decode"); + black_box(obj); + } + let elapsed = start.elapsed(); + + let ns_per_op = (elapsed.as_secs_f64() * 1e9) / (cfg.iterations as f64); + let ops_per_s = (cfg.iterations as f64) / elapsed.as_secs_f64(); + round_ns_per_op.push(ns_per_op); + round_ops_per_s.push(ops_per_s); + } + + let avg_ns_per_op = round_ns_per_op.iter().sum::() / (round_ns_per_op.len() as f64); + let avg_ops_per_s = round_ops_per_s.iter().sum::() / (round_ops_per_s.len() as f64); + + let sample_name = cfg.sample.clone().unwrap_or_else(|| { + manifest_path + .file_name() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_else(|| manifest_path.display().to_string()) + }); + let sample_name = sample_name + .strip_suffix(".mft") + .unwrap_or(&sample_name) + .to_string(); + + println!("fixture: {}", manifest_path.display()); + println!(); + println!("| sample | avg ns/op | ops/s | file count |"); + println!("|---|---:|---:|---:|"); + println!( + "| {} | {:.2} | {:.2} | {} |", + sample_name, avg_ns_per_op, avg_ops_per_s, file_count + ); +} diff --git a/scripts/manifest_perf_compare_m2.sh b/scripts/manifest_perf_compare_m2.sh new file mode 100755 index 0000000..75983c4 --- /dev/null +++ b/scripts/manifest_perf_compare_m2.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash +set -euo pipefail + +# M2: Run per-sample decode+profile benchmark (Ours vs Routinator) on selected_der fixtures. +# +# Outputs: +# - specs/develop/20260224/data/m2_manifest_decode_profile_compare.csv +# - specs/develop/20260224/data/m2_raw.log +# +# Note: This script assumes Routinator benchmark repo exists at: +# /home/yuyr/dev/rust_playground/routinator/benchmark +# +# It also assumes fixtures exist under: +# rpki/tests/benchmark/selected_der/*.mft +# routinator/benchmark/fixtures/selected_der/*.mft + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +RPKI_DIR="$ROOT_DIR" +OURS_BENCH_DIR="$RPKI_DIR/benchmark/ours_manifest_bench" + +ROUT_BENCH_DIR="${ROUT_BENCH_DIR:-/home/yuyr/dev/rust_playground/routinator/benchmark}" +ROUT_BIN="$ROUT_BENCH_DIR/target/release/routinator-manifest-benchmark" + +DATE_TAG="${DATE_TAG:-20260224}" +OUT_DIR="$RPKI_DIR/../specs/develop/${DATE_TAG}/data" +OUT_CSV="${OUT_CSV:-$OUT_DIR/m2_manifest_decode_profile_compare.csv}" +OUT_RAW="${OUT_RAW:-$OUT_DIR/m2_raw.log}" + +REPEATS="${REPEATS:-3}" + +# Iterations / warmups (kept moderate for interactive iteration). +ITER_SMALL="${ITER_SMALL:-20000}" +ITER_MEDIUM="${ITER_MEDIUM:-20000}" +ITER_LARGE="${ITER_LARGE:-20000}" +ITER_XLARGE="${ITER_XLARGE:-2000}" + +WARM_SMALL="${WARM_SMALL:-2000}" +WARM_MEDIUM="${WARM_MEDIUM:-2000}" +WARM_LARGE="${WARM_LARGE:-2000}" +WARM_XLARGE="${WARM_XLARGE:-200}" + +SAMPLES=( + small-01 + small-02 + medium-01 + medium-02 + large-01 + large-02 + xlarge-01 + xlarge-02 +) + +mkdir -p "$OUT_DIR" +: > "$OUT_RAW" + +echo "sample,bucket,manifest_file_count,ours_avg_ns_per_op,ours_ops_per_s,rout_avg_ns_per_op,rout_ops_per_s,ratio_ours_over_rout,iterations,repeats,warmup" > "$OUT_CSV" + +echo "[1/3] Build ours benchmark (release)..." | tee -a "$OUT_RAW" +(cd "$OURS_BENCH_DIR" && cargo build --release -q) +OURS_BIN="$OURS_BENCH_DIR/target/release/ours-manifest-bench" + +echo "[2/3] Build routinator benchmark (release)..." | tee -a "$OUT_RAW" +(cd "$ROUT_BENCH_DIR" && cargo build --release -q) + +taskset_prefix="" +if command -v taskset >/dev/null 2>&1; then + if [[ -n "${TASKSET_CPU:-}" ]]; then + taskset_prefix="taskset -c ${TASKSET_CPU}" + fi +fi + +bucket_for() { + local s="$1" + case "$s" in + small-*) echo "small" ;; + medium-*) echo "medium" ;; + large-*) echo "large" ;; + xlarge-*) echo "xlarge" ;; + *) echo "unknown" ;; + esac +} + +iters_for() { + local b="$1" + case "$b" in + small) echo "$ITER_SMALL" ;; + medium) echo "$ITER_MEDIUM" ;; + large) echo "$ITER_LARGE" ;; + xlarge) echo "$ITER_XLARGE" ;; + *) echo "$ITER_MEDIUM" ;; + esac +} + +warm_for() { + local b="$1" + case "$b" in + small) echo "$WARM_SMALL" ;; + medium) echo "$WARM_MEDIUM" ;; + large) echo "$WARM_LARGE" ;; + xlarge) echo "$WARM_XLARGE" ;; + *) echo "$WARM_MEDIUM" ;; + esac +} + +run_ours() { + local sample="$1" + local iters="$2" + local warm="$3" + local ours_fixture="$RPKI_DIR/tests/benchmark/selected_der/${sample}.mft" + if [[ ! -f "$ours_fixture" ]]; then + echo "ours fixture not found: $ours_fixture" >&2 + exit 1 + fi + + echo "### ours $sample" >> "$OUT_RAW" + local out + out=$($taskset_prefix "$OURS_BIN" --manifest "$ours_fixture" --iterations "$iters" --warmup-iterations "$warm" --repeats "$REPEATS") + echo "$out" >> "$OUT_RAW" + + local line + line=$(echo "$out" | rg "^\\| ${sample} \\|" | tail -n 1) + if [[ -z "${line:-}" ]]; then + echo "failed to parse ours output for $sample" >&2 + exit 1 + fi + # Expected final row: | sample | avg ns/op | ops/s | file count | + local avg ops cnt + avg=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}') + ops=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}') + cnt=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$5); print $5}') + echo "$avg,$ops,$cnt" +} + +run_rout() { + local sample="$1" + local iters="$2" + local warm="$3" + local rout_fixture="$ROUT_BENCH_DIR/fixtures/selected_der/${sample}.mft" + if [[ ! -f "$rout_fixture" ]]; then + echo "routinator fixture not found: $rout_fixture" >&2 + exit 1 + fi + + echo "### routinator $sample" >> "$OUT_RAW" + local out + out=$( + $taskset_prefix "$ROUT_BIN" \ + --target decode_only \ + --manifest "$rout_fixture" \ + --issuer "$ROUT_BENCH_DIR/fixtures/ta.cer" \ + --iterations "$iters" \ + --repeats "$REPEATS" \ + --warmup-iterations "$warm" \ + --strict false + ) + echo "$out" >> "$OUT_RAW" + + local avg_line cnt_line + avg_line=$(echo "$out" | rg "^ avg:") + cnt_line=$(echo "$out" | rg "^ manifest_file_count:") + + local avg_ns ops_s cnt + avg_ns=$(echo "$avg_line" | awk '{print $2}') + ops_s=$(echo "$avg_line" | awk '{gsub(/[()]/,"",$4); print $4}') + cnt=$(echo "$cnt_line" | awk '{print $2}') + echo "$avg_ns,$ops_s,$cnt" +} + +echo "[3/3] Run per-sample benchmarks..." | tee -a "$OUT_RAW" +for s in "${SAMPLES[@]}"; do + b=$(bucket_for "$s") + it=$(iters_for "$b") + warm=$(warm_for "$b") + + IFS=, read -r ours_avg ours_ops ours_cnt < <(run_ours "$s" "$it" "$warm") + IFS=, read -r rout_avg rout_ops rout_cnt < <(run_rout "$s" "$it" "$warm") + + if [[ "$ours_cnt" != "$rout_cnt" ]]; then + echo "WARNING: file count differs for $s (ours=$ours_cnt rout=$rout_cnt)" | tee -a "$OUT_RAW" + fi + + ratio=$(python3 - <> "$OUT_CSV" + echo >> "$OUT_RAW" +done + +echo "Done." +echo "- CSV: $OUT_CSV" +echo "- Raw: $OUT_RAW" diff --git a/scripts/manifest_perf_profile_m3.sh b/scripts/manifest_perf_profile_m3.sh new file mode 100755 index 0000000..ae28650 --- /dev/null +++ b/scripts/manifest_perf_profile_m3.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +set -euo pipefail + +# M3: Generate flamegraphs + top hotspots for Manifest decode+profile (Ours vs Routinator). +# +# Outputs under: +# specs/develop/20260224/flamegraph/ +# specs/develop/20260224/hotspots/ +# specs/develop/20260224/perf/ +# +# Notes: +# - On WSL2, /usr/bin/perf is often a wrapper that fails. This script uses a real perf binary +# from /usr/lib/linux-tools/*/perf (if present). +# - Ours profiling uses perf + flamegraph --perfdata to avoid rebuilding the whole crate graph +# with RocksDB. + +ROOT_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +RPKI_DIR="$ROOT_REPO/rpki" + +DATE_TAG="${DATE_TAG:-20260224}" +OUT_BASE="$ROOT_REPO/specs/develop/${DATE_TAG}" +OUT_FLAME="$OUT_BASE/flamegraph" +OUT_HOT="$OUT_BASE/hotspots" +OUT_PERF="$OUT_BASE/perf" + +RUN_TAG="${RUN_TAG:-p2}" + +OURS_BENCH_DIR="$RPKI_DIR/benchmark/ours_manifest_bench" +OURS_BIN="$OURS_BENCH_DIR/target/release/ours-manifest-bench" + +ROUT_BENCH_DIR="${ROUT_BENCH_DIR:-/home/yuyr/dev/rust_playground/routinator/benchmark}" +ROUT_BIN="$ROUT_BENCH_DIR/target/release/routinator-manifest-benchmark" +ROUT_ISSUER="$ROUT_BENCH_DIR/fixtures/ta.cer" + +PROFILE_HZ="${PROFILE_HZ:-99}" + +mkdir -p "$OUT_FLAME" "$OUT_HOT" "$OUT_PERF" + +PERF_WRAPPER_OUT="$(perf --version 2>&1 || true)" +PERF_REAL="" +if echo "${PERF_WRAPPER_OUT}" | grep -q "WARNING: perf not found for kernel"; then + PERF_REAL="$(ls -1 /usr/lib/linux-tools/*/perf 2>/dev/null | head -n 1 || true)" +else + PERF_REAL="$(command -v perf || true)" +fi + +if [[ -z "${PERF_REAL}" ]]; then + echo "ERROR: usable perf binary not found (wrapper detected and no /usr/lib/linux-tools/*/perf)." >&2 + exit 2 +fi + +SHIM_DIR="$RPKI_DIR/target/bench/tools" +mkdir -p "$SHIM_DIR" +cat > "$SHIM_DIR/perf" </dev/null 2>&1; then + taskset_prefix="taskset -c 0" +fi + +profile_ours() { + local sample="$1" + local iters="$2" + local warm="$3" + local fixture="$RPKI_DIR/tests/benchmark/selected_der/${sample}.mft" + if [[ ! -f "$fixture" ]]; then + echo "ERROR: ours fixture not found: $fixture" >&2 + exit 1 + fi + + local perfdata="$OUT_PERF/ours_${sample}_${RUN_TAG}.perf.data" + local svg="$OUT_FLAME/ours_${sample}_${RUN_TAG}.svg" + local tsv="$OUT_HOT/ours_${sample}_${RUN_TAG}.tsv" + + echo "== ours $sample (iters=$iters warmup=$warm hz=$PROFILE_HZ)" + $taskset_prefix perf record -o "$perfdata" -F "$PROFILE_HZ" -g -- \ + "$OURS_BIN" --manifest "$fixture" --iterations "$iters" --warmup-iterations "$warm" --repeats 1 >/dev/null + + flamegraph --perfdata "$perfdata" --output "$svg" --title "ours ${sample} ManifestObject::decode_der" --deterministic >/dev/null + + perf report -i "$perfdata" --stdio --no-children --sort symbol --percent-limit 0.5 \ + | awk '/^[[:space:]]*[0-9.]+%/ {pct=$1; sub(/%/,"",pct); $1=""; sub(/^[[:space:]]+/,""); print pct "\t" $0}' \ + > "$tsv" +} + +profile_routinator() { + local sample="$1" + local iters="$2" + local warm="$3" + local fixture="$ROUT_BENCH_DIR/fixtures/selected_der/${sample}.mft" + if [[ ! -f "$fixture" ]]; then + echo "ERROR: routinator fixture not found: $fixture" >&2 + exit 1 + fi + + local svg="$OUT_FLAME/routinator_${sample}_${RUN_TAG}.svg" + local tsv="$OUT_HOT/routinator_${sample}_${RUN_TAG}.tsv" + + echo "== routinator $sample (iters=$iters warmup=$warm hz=$PROFILE_HZ)" + $taskset_prefix "$ROUT_BIN" \ + --target decode_only \ + --manifest "$fixture" \ + --issuer "$ROUT_ISSUER" \ + --iterations "$iters" \ + --repeats 1 \ + --warmup-iterations "$warm" \ + --strict false \ + --profile-hz "$PROFILE_HZ" \ + --flamegraph "$svg" \ + --hotspots "$tsv" \ + >/dev/null +} + +echo "[3/3] Profile samples..." + +# Choose iterations so each capture runs ~10-20s serially. +profile_ours small-01 200000 0 +profile_routinator small-01 200000 0 + +profile_ours large-02 50000 0 +profile_routinator large-02 50000 0 + +profile_ours xlarge-02 5000 0 +profile_routinator xlarge-02 5000 0 + +echo "Done." +echo "- Flamegraphs: $OUT_FLAME/" +echo "- Hotspots: $OUT_HOT/" +echo "- Perf data: $OUT_PERF/" diff --git a/specs/arch.excalidraw b/specs/arch.excalidraw index f282369..6dbed3f 100644 --- a/specs/arch.excalidraw +++ b/specs/arch.excalidraw @@ -131,7 +131,7 @@ "version": 61, "versionNonce": 1723253093, "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1770712989659, "link": null, "locked": false @@ -192,7 +192,7 @@ "version": 50, "versionNonce": 935935979, "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1770713011072, "link": null, "locked": false, @@ -229,7 +229,7 @@ "version": 64, "versionNonce": 664751051, "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1770713034384, "link": null, "locked": false, @@ -266,7 +266,7 @@ "version": 5, "versionNonce": 1052482507, "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1770713114585, "link": null, "locked": false, @@ -342,7 +342,7 @@ "version": 31, "versionNonce": 291819, "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1770713136745, "link": null, "locked": false, @@ -392,7 +392,7 @@ "version": 42, "versionNonce": 812442347, "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1770713145632, "link": null, "locked": false, @@ -437,10 +437,6 @@ "versionNonce": 1619895909, "isDeleted": false, "boundElements": [ - { - "id": "cktmBgzDHQVp__ThQSYj7", - "type": "arrow" - }, { "id": "t6L4IloEw2niv4WL5K7nm", "type": "arrow" @@ -1045,7 +1041,7 @@ "version": 8, "versionNonce": 742717515, "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1770714823157, "link": null, "locked": false, @@ -1334,7 +1330,7 @@ "version": 323, "versionNonce": 1467795045, "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1770715873720, "link": null, "locked": false, @@ -1445,7 +1441,7 @@ "version": 42, "versionNonce": 589361675, "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1770715099668, "link": null, "locked": false, @@ -1754,9 +1750,9 @@ { "id": "kX3rMNwDurF6i699O_SE8", "type": "text", - "x": 110.67323112589077, - "y": 157.70055147613942, - "width": 336.02783203125, + "x": 192.37925529581264, + "y": 159.70055147613942, + "width": 476.61578369140625, "height": 35, "angle": 0, "strokeColor": "#1e1e1e", @@ -1771,20 +1767,20 @@ "index": "ay", "roundness": null, "seed": 1264632357, - "version": 105, - "versionNonce": 933795019, + "version": 137, + "versionNonce": 1508339779, "isDeleted": false, - "boundElements": null, - "updated": 1770715475754, + "boundElements": [], + "updated": 1771988976926, "link": null, "locked": false, - "text": "V1. Sequential processing", + "text": "V1. Sequential processing, bulk sync", "fontSize": 28, "fontFamily": 5, "textAlign": "center", "verticalAlign": "top", "containerId": null, - "originalText": "V1. Sequential processing", + "originalText": "V1. Sequential processing, bulk sync", "autoResize": true, "lineHeight": 1.25 }, @@ -2167,7 +2163,7 @@ "version": 19, "versionNonce": 1674239947, "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1770715646534, "link": null, "locked": false, @@ -2221,7 +2217,7 @@ "version": 26, "versionNonce": 1969706949, "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1770715650828, "link": null, "locked": false, @@ -2275,7 +2271,7 @@ "version": 27, "versionNonce": 1392885893, "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1770715655497, "link": null, "locked": false, @@ -2303,6 +2299,2308 @@ "startArrowhead": null, "endArrowhead": "arrow", "elbowed": false + }, + { + "id": "pK2KuweZmIqxZUBP39Lnu", + "type": "rectangle", + "x": 243.34645811055054, + "y": 1394.3305105830423, + "width": 142.66674804687503, + "height": 104, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0D", + "roundness": { + "type": 3 + }, + "seed": 379870445, + "version": 524, + "versionNonce": 618646115, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "n-FMilxfc2ZJiqYA-gFre" + } + ], + "updated": 1771989070180, + "link": null, + "locked": false + }, + { + "id": "n-FMilxfc2ZJiqYA-gFre", + "type": "text", + "x": 266.2098690602576, + "y": 1408.8305105830423, + "width": 96.93992614746094, + "height": 75, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0E", + "roundness": null, + "seed": 222574413, + "version": 575, + "versionNonce": 1908134637, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997746, + "link": null, + "locked": false, + "text": "sync pp\n(rrdp :\nsnapshot)", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "pK2KuweZmIqxZUBP39Lnu", + "originalText": "sync pp\n(rrdp : snapshot)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "J6aT42Ttbxs8J8ICHMZij", + "type": "rectangle", + "x": 464.5129467580115, + "y": 1616.6639761592141, + "width": 273.0000305175781, + "height": 73.3333740234375, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0F", + "roundness": null, + "seed": 733595053, + "version": 184, + "versionNonce": 1107094861, + "isDeleted": false, + "boundElements": [ + { + "id": "WbYpzvCWcQTPm1dKw9dBO", + "type": "arrow" + } + ], + "updated": 1771988997747, + "link": null, + "locked": false + }, + { + "id": "ly_KFmgXjNMaBuHpnswW-", + "type": "rectangle", + "x": 476.17963376973023, + "y": 1623.9973807002298, + "width": 34, + "height": 57.33331298828125, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0G", + "roundness": null, + "seed": 1429112845, + "version": 111, + "versionNonce": 1873919501, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false + }, + { + "id": "eQw5MvT9qLYq7v6i1rrRg", + "type": "rectangle", + "x": 691.1796032521521, + "y": 1623.9973501826516, + "width": 34, + "height": 57.33331298828125, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0H", + "roundness": null, + "seed": 1268514413, + "version": 181, + "versionNonce": 1548252269, + "isDeleted": false, + "boundElements": [ + { + "id": "dIV5isrknYEDYXx3LBF99", + "type": "arrow" + } + ], + "updated": 1771988997747, + "link": null, + "locked": false + }, + { + "id": "EhQ-Tz_NnRqlf9WPub5GX", + "type": "text", + "x": 578.8462902638709, + "y": 1638.6640066767923, + "width": 27.399978637695312, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0I", + "roundness": null, + "seed": 1439410381, + "version": 100, + "versionNonce": 31325485, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": ".....", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": ".....", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "kNxB8eKthsDCnAnPEDR1z", + "type": "text", + "x": 521.5129772755896, + "y": 1713.330693688511, + "width": 186.4598388671875, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0J", + "roundness": null, + "seed": 626460461, + "version": 114, + "versionNonce": 1491062669, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "CA Instance Queue", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "CA Instance Queue", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "fHfTE7gQEL2I3VTo_yUVO", + "type": "text", + "x": 202.17963376973023, + "y": 1646.6640066767923, + "width": 38.319976806640625, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0K", + "roundness": null, + "seed": 2091358605, + "version": 55, + "versionNonce": 1756911085, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "TAL", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "TAL", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "fRP0vWEmoT9UFXnoJhPMF", + "type": "text", + "x": 329.0196148488318, + "y": 1644.1640066767923, + "width": 27.459991455078125, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0L", + "roundness": null, + "seed": 351723501, + "version": 124, + "versionNonce": 1282478157, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "TA", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "TA", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "WbYpzvCWcQTPm1dKw9dBO", + "type": "arrow", + "x": 384.84629026387086, + "y": 1653.330693688511, + "width": 64, + "height": 0.66668701171875, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0M", + "roundness": { + "type": 2 + }, + "seed": 1145985613, + "version": 129, + "versionNonce": 1156599075, + "isDeleted": false, + "boundElements": [], + "updated": 1771988998065, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 64, + 0.66668701171875 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": { + "elementId": "J6aT42Ttbxs8J8ICHMZij", + "focus": -0.05912097242544523, + "gap": 15.666656494140625 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "vrcsQGoaeLT58iWb5XWjr", + "type": "arrow", + "x": 258.8462292287146, + "y": 1653.9973196650735, + "width": 49.333343505859375, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0N", + "roundness": { + "type": 2 + }, + "seed": 1669826733, + "version": 92, + "versionNonce": 263790861, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 49.333343505859375, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "lvox0Kgsgp8drDMi8v5Gb", + "type": "rectangle", + "x": 802.5128552052771, + "y": 1565.9972891474954, + "width": 34, + "height": 57.33331298828125, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0O", + "roundness": null, + "seed": 1951132429, + "version": 245, + "versionNonce": 1802205037, + "isDeleted": false, + "boundElements": [ + { + "id": "1KvGdx5_vzQ6QOksy3HUk", + "type": "arrow" + } + ], + "updated": 1771988997747, + "link": null, + "locked": false + }, + { + "id": "SVCRMaoPvBRF4vedZJJQS", + "type": "text", + "x": 800.9496227834021, + "y": 1638.8306326533548, + "width": 119.69989013671875, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0P", + "roundness": null, + "seed": 118817133, + "version": 206, + "versionNonce": 811352109, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "CA Instance", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "CA Instance", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "dIV5isrknYEDYXx3LBF99", + "type": "arrow", + "x": 739.5128552052771, + "y": 1655.9973196650735, + "width": 46, + "height": 52.66668701171875, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0Q", + "roundness": { + "type": 2 + }, + "seed": 1630292941, + "version": 214, + "versionNonce": 944812227, + "isDeleted": false, + "boundElements": [], + "updated": 1771988998066, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 46, + -52.66668701171875 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "eQw5MvT9qLYq7v6i1rrRg", + "focus": 0.8146120463303684, + "gap": 14.333251953125 + }, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "rpCoFEagKz5ronj_Y5BUq", + "type": "rectangle", + "x": 214.17954221699586, + "y": 1362.663915124058, + "width": 859.3333129882814, + "height": 151.33331298828125, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0R", + "roundness": null, + "seed": 1655392813, + "version": 339, + "versionNonce": 146921709, + "isDeleted": false, + "boundElements": [ + { + "id": "1KvGdx5_vzQ6QOksy3HUk", + "type": "arrow" + }, + { + "id": "NJwDfFdgKniyZWhIgv_vr", + "type": "arrow" + }, + { + "id": "c_GDr8W4M_Jdfd1LgGFO0", + "type": "arrow" + }, + { + "id": "1lEeEIZ16EByBIHWAUQX7", + "type": "arrow" + }, + { + "id": "cPzjO60brqLtWuPhVwRZv", + "type": "arrow" + }, + { + "id": "A7JxruZaup8zBIfc-BVzD", + "type": "arrow" + } + ], + "updated": 1771988997747, + "link": null, + "locked": false + }, + { + "id": "fGqhFkTaP9grj3IAzJNda", + "type": "text", + "x": 592.847771146741, + "y": 1520.3191041182754, + "width": 182.1998291015625, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0S", + "roundness": null, + "seed": 746290317, + "version": 427, + "versionNonce": 2047382925, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "Processing Pipeline", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Processing Pipeline", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "eZK49hSPN0iPNuxgjAnok", + "type": "rectangle", + "x": 408.84616819355836, + "y": 1390.663915124058, + "width": 142.66674804687503, + "height": 104, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0T", + "roundness": { + "type": 3 + }, + "seed": 106324717, + "version": 568, + "versionNonce": 1471614893, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "5_2yBI8uINsiAAyIkC5EN" + }, + { + "id": "p5gl0vDTNCCUrD00wcCXJ", + "type": "arrow" + } + ], + "updated": 1771989072429, + "link": null, + "locked": false + }, + { + "id": "5_2yBI8uINsiAAyIkC5EN", + "type": "text", + "x": 438.7895809743201, + "y": 1417.663915124058, + "width": 82.77992248535156, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0U", + "roundness": null, + "seed": 80051533, + "version": 644, + "versionNonce": 1654675021, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "process\nmanifest", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "eZK49hSPN0iPNuxgjAnok", + "originalText": "process\nmanifest", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "TYgM3ByZBpMnY7_KTXGu9", + "type": "rectangle", + "x": 580.1795422169959, + "y": 1387.3306021357766, + "width": 142.66674804687503, + "height": 104, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0V", + "roundness": { + "type": 3 + }, + "seed": 1864518573, + "version": 612, + "versionNonce": 1800333069, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "WjxbyyeHwaZOKpLD1SqD0" + } + ], + "updated": 1771988997747, + "link": null, + "locked": false + }, + { + "id": "WjxbyyeHwaZOKpLD1SqD0", + "type": "text", + "x": 614.6529461476599, + "y": 1414.3306021357766, + "width": 73.71994018554688, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0W", + "roundness": null, + "seed": 1704349197, + "version": 710, + "versionNonce": 475561325, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "extract\nobjects", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "TYgM3ByZBpMnY7_KTXGu9", + "originalText": "extract\nobjects", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "vsRffvKqgeTUVLJGF6SgH", + "type": "rectangle", + "x": 746.1794811818396, + "y": 1387.3306021357766, + "width": 142.66674804687503, + "height": 104, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0X", + "roundness": { + "type": 3 + }, + "seed": 1192952941, + "version": 672, + "versionNonce": 2023770061, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "BiCmk3W1fJS-KiMNptOfb" + } + ], + "updated": 1771988997747, + "link": null, + "locked": false + }, + { + "id": "BiCmk3W1fJS-KiMNptOfb", + "type": "text", + "x": 764.7629009816443, + "y": 1414.3306021357766, + "width": 105.49990844726562, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0Y", + "roundness": null, + "seed": 1467608781, + "version": 826, + "versionNonce": 1648346669, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "discover\nchildren CA", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "vsRffvKqgeTUVLJGF6SgH", + "originalText": "discover\nchildren CA", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "plwHn3tJhVGDQiWaZjQJs", + "type": "rectangle", + "x": 915.5128552052771, + "y": 1388.6640066767923, + "width": 142.66674804687503, + "height": 104, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0Z", + "roundness": { + "type": 3 + }, + "seed": 625773869, + "version": 712, + "versionNonce": 1008760973, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "BCjZv_yoxP0ZOi0AajlqC" + } + ], + "updated": 1771988997747, + "link": null, + "locked": false + }, + { + "id": "BCjZv_yoxP0ZOi0AajlqC", + "type": "text", + "x": 961.5862499806677, + "y": 1415.6640066767923, + "width": 50.51995849609375, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0a", + "roundness": null, + "seed": 805068685, + "version": 885, + "versionNonce": 1341906669, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "build\naudit", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "plwHn3tJhVGDQiWaZjQJs", + "originalText": "build\naudit", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "1KvGdx5_vzQ6QOksy3HUk", + "type": "arrow", + "x": 786.6085487780363, + "y": 1571.6992603484082, + "width": 649.4359483818903, + "height": 129.18418959695612, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0b", + "roundness": { + "type": 2 + }, + "seed": 1304618477, + "version": 378, + "versionNonce": 624401411, + "isDeleted": false, + "boundElements": [], + "updated": 1771988998067, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -606.3745786685331, + -3.515241767764792 + ], + [ + -649.4359483818903, + -108.09289990413765 + ], + [ + -601.9805465730485, + -129.18418959695612 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "lvox0Kgsgp8drDMi8v5Gb", + "focus": 0.7917180455874213, + "gap": 15.90430642724084 + }, + "endBinding": { + "elementId": "rpCoFEagKz5ronj_Y5BUq", + "focus": 0.74977596595825, + "gap": 29.551540012008104 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "3wxXo7q8NwFLF4zI6EdAm", + "type": "ellipse", + "x": 1161.8578116101708, + "y": 1240.390157557351, + "width": 124.79015750147141, + "height": 92.27443263452369, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0c", + "roundness": null, + "seed": 1370071117, + "version": 111, + "versionNonce": 1922615213, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "F42PrgyRRp1VpQIHqQpb-" + }, + { + "id": "NJwDfFdgKniyZWhIgv_vr", + "type": "arrow" + }, + { + "id": "A_-T50A5cJx9o4PFW1hld", + "type": "arrow" + } + ], + "updated": 1771988997747, + "link": null, + "locked": false + }, + { + "id": "F42PrgyRRp1VpQIHqQpb-", + "type": "text", + "x": 1197.6529265948425, + "y": 1273.9034353516063, + "width": 52.9599609375, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0d", + "roundness": null, + "seed": 1904873133, + "version": 59, + "versionNonce": 752699917, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "VRPS", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "3wxXo7q8NwFLF4zI6EdAm", + "originalText": "VRPS", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "dtqsOPIcBTuUhktIAX3zU", + "type": "ellipse", + "x": 1169.7670854734201, + "y": 1363.8621295672424, + "width": 124.79015750147141, + "height": 92.27443263452369, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0e", + "roundness": null, + "seed": 1812836621, + "version": 159, + "versionNonce": 732236077, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "_1PAF6snWphoSkwPbj7Hr" + }, + { + "id": "c_GDr8W4M_Jdfd1LgGFO0", + "type": "arrow" + }, + { + "id": "VROBhm8es3DMZ4ERxerTd", + "type": "arrow" + } + ], + "updated": 1771988997747, + "link": null, + "locked": false + }, + { + "id": "_1PAF6snWphoSkwPbj7Hr", + "type": "text", + "x": 1200.3022059512562, + "y": 1397.3754073614978, + "width": 63.479949951171875, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0f", + "roundness": null, + "seed": 1856898925, + "version": 113, + "versionNonce": 1306876813, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "ASPAS", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "dtqsOPIcBTuUhktIAX3zU", + "originalText": "ASPAS", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "NJwDfFdgKniyZWhIgv_vr", + "type": "arrow", + "x": 1099.4627730878774, + "y": 1379.2411614445539, + "width": 54.48580488748621, + "height": 69.42552205782272, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0g", + "roundness": { + "type": 2 + }, + "seed": 1001689549, + "version": 178, + "versionNonce": 189577027, + "isDeleted": false, + "boundElements": [], + "updated": 1771988998068, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 54.48580488748621, + -69.42552205782272 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "rpCoFEagKz5ronj_Y5BUq", + "focus": 0.8368105037552904, + "gap": 25.949917882600175 + }, + "endBinding": { + "elementId": "3wxXo7q8NwFLF4zI6EdAm", + "focus": 0.527640471343281, + "gap": 13.81162576334855 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "c_GDr8W4M_Jdfd1LgGFO0", + "type": "arrow", + "x": 1102.9780148556424, + "y": 1408.2416847721793, + "width": 50.97056311972142, + "height": 0.8788305561624838, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0h", + "roundness": { + "type": 2 + }, + "seed": 272289837, + "version": 185, + "versionNonce": 952369795, + "isDeleted": false, + "boundElements": [], + "updated": 1771988998068, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 50.97056311972142, + -0.8788305561624838 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "rpCoFEagKz5ronj_Y5BUq", + "focus": -0.2668984433045746, + "gap": 29.465159650365194 + }, + "endBinding": { + "elementId": "dtqsOPIcBTuUhktIAX3zU", + "focus": 0.08440560920749281, + "gap": 15.888046773141392 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "qIDZwImSjvK9YRnTvRAxb", + "type": "diamond", + "x": 419.70807565833934, + "y": 1205.6773965284394, + "width": 178.39717206123768, + "height": 108.09285967569502, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0i", + "roundness": null, + "seed": 1244283533, + "version": 249, + "versionNonce": 1333363565, + "isDeleted": false, + "boundElements": [ + { + "id": "p5gl0vDTNCCUrD00wcCXJ", + "type": "arrow" + } + ], + "updated": 1771988997747, + "link": null, + "locked": false + }, + { + "id": "PbpWoahheVCEYC8KyPd32", + "type": "diamond", + "x": 194.73441280130749, + "y": 1207.4350576407644, + "width": 211.7915665705777, + "height": 108.09285967569502, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0j", + "roundness": null, + "seed": 1312581869, + "version": 258, + "versionNonce": 425151533, + "isDeleted": false, + "boundElements": [ + { + "id": "1lEeEIZ16EByBIHWAUQX7", + "type": "arrow" + } + ], + "updated": 1771988997747, + "link": null, + "locked": false + }, + { + "id": "bMKvhaEsFb8u8fqkxY6El", + "type": "text", + "x": 250.89138939228974, + "y": 1121.836476107175, + "width": 112.7799072265625, + "height": 75, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 90, + "groupIds": [], + "frameId": null, + "index": "b0k", + "roundness": null, + "seed": 771408717, + "version": 373, + "versionNonce": 1600775405, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "Rocksdb\nRAW \nfile storage", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Rocksdb\nRAW \nfile storage", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "epi8aAp0JHXOw7yxmHi6s", + "type": "text", + "x": 472.7066642037241, + "y": 1118.27371551905, + "width": 81.51992797851562, + "height": 75, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0l", + "roundness": null, + "seed": 230609325, + "version": 471, + "versionNonce": 874037069, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "Rocksdb\nCached \npp pack", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Rocksdb\nCached \npp pack", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "eQwsik-9JA9Z_2btqgmgB", + "type": "text", + "x": 230.59728791380635, + "y": 1249.8840734868252, + "width": 149.5598602294922, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0m", + "roundness": null, + "seed": 1253184525, + "version": 94, + "versionNonce": 1275252067, + "isDeleted": false, + "boundElements": [ + { + "id": "1lEeEIZ16EByBIHWAUQX7", + "type": "arrow" + } + ], + "updated": 1771988998069, + "link": null, + "locked": false, + "text": "roa/mft/crl/cer\n...", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "roa/mft/crl/cer\n...", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "nq219_ONeC4LRyb3lRk-P", + "type": "text", + "x": 439.017849803279, + "y": 1246.541810536718, + "width": 138.01988220214844, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0n", + "roundness": null, + "seed": 1624115821, + "version": 92, + "versionNonce": 645992461, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "pp1/pp2/pp3...", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "pp1/pp2/pp3...", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "1lEeEIZ16EByBIHWAUQX7", + "type": "arrow", + "x": 302.38775662781495, + "y": 1384.5139838677585, + "width": 0, + "height": 61.51624819457322, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0o", + "roundness": null, + "seed": 282866893, + "version": 183, + "versionNonce": 2127658435, + "isDeleted": false, + "boundElements": [], + "updated": 1771988998069, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -61.51624819457322 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "rpCoFEagKz5ronj_Y5BUq", + "focus": -0.7947054697458892, + "gap": 21.850068743700604 + }, + "endBinding": { + "elementId": "eQwsik-9JA9Z_2btqgmgB", + "focus": 0.03997678783799776, + "gap": 14 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "p5gl0vDTNCCUrD00wcCXJ", + "type": "arrow", + "x": 512.4218229998379, + "y": 1376.6047502329516, + "width": 0.8788305561624838, + "height": 55.364635443648694, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0p", + "roundness": null, + "seed": 1182143277, + "version": 177, + "versionNonce": 893587619, + "isDeleted": false, + "boundElements": [], + "updated": 1771988998070, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.8788305561624838, + -55.364635443648694 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "eZK49hSPN0iPNuxgjAnok", + "focus": 0.43229228206783404, + "gap": 14.05916489110632 + }, + "endBinding": { + "elementId": "qIDZwImSjvK9YRnTvRAxb", + "focus": -0.09936772335567425, + "gap": 8.288872253399507 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "hcIF4ls9aRuUJZE5l3Mxa", + "type": "ellipse", + "x": 1173.2823272411847, + "y": 1489.5309567111062, + "width": 124.79015750147141, + "height": 92.27443263452369, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0q", + "roundness": null, + "seed": 1040524685, + "version": 208, + "versionNonce": 1314295597, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "3vtKK2zY2XD1EnAxI7-r3" + }, + { + "id": "cPzjO60brqLtWuPhVwRZv", + "type": "arrow" + }, + { + "id": "ZuQqhKvXROaruvymuHTan", + "type": "arrow" + } + ], + "updated": 1771988997747, + "link": null, + "locked": false + }, + { + "id": "3vtKK2zY2XD1EnAxI7-r3", + "type": "text", + "x": 1199.5574532121848, + "y": 1510.5442345053616, + "width": 71.99993896484375, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0r", + "roundness": null, + "seed": 1791159277, + "version": 180, + "versionNonce": 1819681165, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "children\nCA", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "hcIF4ls9aRuUJZE5l3Mxa", + "originalText": "children\nCA", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "cPzjO60brqLtWuPhVwRZv", + "type": "arrow", + "x": 1100.341523187155, + "y": 1458.3334977926233, + "width": 57.12229655597366, + "height": 49.212982464281595, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0s", + "roundness": null, + "seed": 481879629, + "version": 203, + "versionNonce": 591102947, + "isDeleted": false, + "boundElements": [], + "updated": 1771988998070, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 57.12229655597366, + 49.212982464281595 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "rpCoFEagKz5ronj_Y5BUq", + "focus": -0.8372608711722136, + "gap": 26.828667981877743 + }, + "endBinding": { + "elementId": "hcIF4ls9aRuUJZE5l3Mxa", + "focus": -0.39304316221230523, + "gap": 23.06591148313442 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "ZuQqhKvXROaruvymuHTan", + "type": "arrow", + "x": 1227.7680919002287, + "y": 1602.4572838745885, + "width": 803.2266392680979, + "height": 166.97273688710857, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0t", + "roundness": { + "type": 2 + }, + "seed": 1723897005, + "version": 222, + "versionNonce": 2106003331, + "isDeleted": false, + "boundElements": [], + "updated": 1771988998070, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -140.6086247710855, + 166.09390633094608 + ], + [ + -803.2266392680979, + 166.97273688710857 + ], + [ + -783.8929604020511, + 85.24398932743668 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "hcIF4ls9aRuUJZE5l3Mxa", + "focus": -0.479352215629856, + "gap": 20.94954853984769 + }, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "voaHj166FgqKXTrD_cnHd", + "type": "text", + "x": 170.1101449975834, + "y": 1032.8892001973657, + "width": 922.4036254882812, + "height": 35, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0u", + "roundness": null, + "seed": 1149466381, + "version": 292, + "versionNonce": 516034093, + "isDeleted": false, + "boundElements": [], + "updated": 1771989062577, + "link": null, + "locked": false, + "text": "V1.1 Sequential processing, bulk sync + incremental sync/cache usage", + "fontSize": 28, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "V1.1 Sequential processing, bulk sync + incremental sync/cache usage", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "WqzMgtVmOY9PC4VufdArQ", + "type": "ellipse", + "x": 1157.60360434743, + "y": 1131.6944584800729, + "width": 124.79015750147141, + "height": 92.27443263452369, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0v", + "roundness": null, + "seed": 8064365, + "version": 140, + "versionNonce": 376755149, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "efLu_RpIdM-39Pqv_4O70" + }, + { + "id": "A7JxruZaup8zBIfc-BVzD", + "type": "arrow" + }, + { + "id": "m-o2RdYKsDGYJi_JfuZlU", + "type": "arrow" + } + ], + "updated": 1771988997747, + "link": null, + "locked": false + }, + { + "id": "efLu_RpIdM-39Pqv_4O70", + "type": "text", + "x": 1189.1887278770237, + "y": 1165.2077362743282, + "width": 61.37994384765625, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0w", + "roundness": null, + "seed": 1508956109, + "version": 94, + "versionNonce": 1839387181, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "audits", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "WqzMgtVmOY9PC4VufdArQ", + "originalText": "audits", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "A7JxruZaup8zBIfc-BVzD", + "type": "arrow", + "x": 1084.850440216766, + "y": 1341.9026544272915, + "width": 69.41464522805859, + "height": 132.51886816265744, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0x", + "roundness": { + "type": 2 + }, + "seed": 1795053101, + "version": 185, + "versionNonce": 1307921091, + "isDeleted": false, + "boundElements": [], + "updated": 1771988998071, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 69.41464522805859, + -132.51886816265744 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "rpCoFEagKz5ronj_Y5BUq", + "focus": 0.8320755854741431, + "gap": 23.65524845804479 + }, + "endBinding": { + "elementId": "WqzMgtVmOY9PC4VufdArQ", + "focus": 0.5684502667918756, + "gap": 14.443376509558366 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "UYqc90jYvsoncKtuEQ4vW", + "type": "ellipse", + "x": 1347.7181145697148, + "y": 1239.9974134420977, + "width": 124.79015750147141, + "height": 92.27443263452369, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0y", + "roundness": null, + "seed": 1317865613, + "version": 187, + "versionNonce": 504746925, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "sj5i7H1ZoOhoyR73YbA5w" + }, + { + "id": "A_-T50A5cJx9o4PFW1hld", + "type": "arrow" + } + ], + "updated": 1771988997747, + "link": null, + "locked": false + }, + { + "id": "sj5i7H1ZoOhoyR73YbA5w", + "type": "text", + "x": 1383.5132295543867, + "y": 1261.010691236353, + "width": 52.9599609375, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0z", + "roundness": null, + "seed": 106327789, + "version": 141, + "versionNonce": 2094585357, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "all\nVRPS", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "UYqc90jYvsoncKtuEQ4vW", + "originalText": "all\nVRPS", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "oZSidb5F1VSJh5S-DTAnF", + "type": "ellipse", + "x": 1355.6273884329646, + "y": 1363.4693854519894, + "width": 124.79015750147141, + "height": 92.27443263452369, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b10", + "roundness": null, + "seed": 551815501, + "version": 235, + "versionNonce": 1438283469, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "C9WUXgM_fsPV-JIZHcJx6" + }, + { + "id": "VROBhm8es3DMZ4ERxerTd", + "type": "arrow" + } + ], + "updated": 1771988997747, + "link": null, + "locked": false + }, + { + "id": "C9WUXgM_fsPV-JIZHcJx6", + "type": "text", + "x": 1386.1625089108006, + "y": 1384.4826632462448, + "width": 63.479949951171875, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b11", + "roundness": null, + "seed": 529487789, + "version": 194, + "versionNonce": 440145197, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "all\nASPAS", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "oZSidb5F1VSJh5S-DTAnF", + "originalText": "all\nASPAS", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "lEUdQD50ILVxuvfW0Uo6F", + "type": "ellipse", + "x": 1343.4639073069745, + "y": 1131.3017143648199, + "width": 124.79015750147141, + "height": 92.27443263452369, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b12", + "roundness": null, + "seed": 478272013, + "version": 216, + "versionNonce": 1651535341, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "_CqsX809p_C_S-Km-gjX2" + }, + { + "id": "m-o2RdYKsDGYJi_JfuZlU", + "type": "arrow" + } + ], + "updated": 1771988997747, + "link": null, + "locked": false + }, + { + "id": "_CqsX809p_C_S-Km-gjX2", + "type": "text", + "x": 1375.0490308365684, + "y": 1152.3149921590752, + "width": 61.37994384765625, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b13", + "roundness": null, + "seed": 817172589, + "version": 186, + "versionNonce": 124135501, + "isDeleted": false, + "boundElements": [], + "updated": 1771988997747, + "link": null, + "locked": false, + "text": "all\naudits", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "lEUdQD50ILVxuvfW0Uo6F", + "originalText": "all\naudits", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "m-o2RdYKsDGYJi_JfuZlU", + "type": "arrow", + "x": 1289.413328326225, + "y": 1177.831674797335, + "width": 42.595350480853995, + "height": 1.0517691453775342, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b14", + "roundness": { + "type": 2 + }, + "seed": 435321549, + "version": 165, + "versionNonce": 972928515, + "isDeleted": false, + "boundElements": [], + "updated": 1771988998071, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 42.595350480853995, + 1.0517691453775342 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "WqzMgtVmOY9PC4VufdArQ", + "focus": -0.03594946064605047, + "gap": 7.019566477323565 + }, + "endBinding": { + "elementId": "lEUdQD50ILVxuvfW0Uo6F", + "focus": -0.06854401054158754, + "gap": 11.478115061936741 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "A_-T50A5cJx9o4PFW1hld", + "type": "arrow", + "x": 1294.671981474307, + "y": 1286.1606229315303, + "width": 43.64711962623164, + "height": 1.0517691453775342, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b15", + "roundness": { + "type": 2 + }, + "seed": 550738221, + "version": 172, + "versionNonce": 1691636035, + "isDeleted": false, + "boundElements": [], + "updated": 1771988998071, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 43.64711962623164, + -1.0517691453775342 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "3wxXo7q8NwFLF4zI6EdAm", + "focus": 0.027920319624342795, + "gap": 8.025608282862457 + }, + "endBinding": { + "elementId": "UYqc90jYvsoncKtuEQ4vW", + "focus": 0.05784554647967765, + "gap": 9.411101585368538 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "VROBhm8es3DMZ4ERxerTd", + "type": "arrow", + "x": 1303.0858457691193, + "y": 1401.8516662151605, + "width": 49.957541919691494, + "height": 1.0516728559748572, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b16", + "roundness": { + "type": 2 + }, + "seed": 1052896141, + "version": 173, + "versionNonce": 883760259, + "isDeleted": false, + "boundElements": [], + "updated": 1771988998071, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 49.957541919691494, + -1.0516728559748572 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "dtqsOPIcBTuUhktIAX3zU", + "focus": -0.14024322915764614, + "gap": 9.29755273930883 + }, + "endBinding": { + "elementId": "oZSidb5F1VSJh5S-DTAnF", + "focus": 0.21442257431549464, + "gap": 3.6200174208305973 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false } ], "appState": { diff --git a/src/data_model/manifest.rs b/src/data_model/manifest.rs index 2daf619..12459d5 100644 --- a/src/data_model/manifest.rs +++ b/src/data_model/manifest.rs @@ -4,8 +4,6 @@ use crate::data_model::rc::ResourceCertificate; use crate::data_model::signed_object::{ RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError, }; -use der_parser::ber::BerObjectContent; -use der_parser::der::{DerObject, Tag, parse_der}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct ManifestObject { @@ -28,7 +26,10 @@ pub struct ManifestEContent { pub this_update: UtcTime, pub next_update: UtcTime, pub file_hash_alg: String, - pub files: Vec, + /// DER-encoded content bytes of `Manifest.fileList` (SEQUENCE OF FileAndHash). + pub file_list_der: Vec, + /// Count of FileAndHash entries in `fileList`. + pub file_count: usize, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -39,7 +40,7 @@ pub struct ManifestEContentParsed { #[derive(Clone, Debug, PartialEq, Eq)] pub struct FileAndHash { pub file_name: String, - pub hash_bytes: Vec, + pub hash_bytes: [u8; 32], } #[derive(Debug, thiserror::Error)] @@ -246,7 +247,8 @@ impl ManifestObject { impl ManifestEContent { /// Parse step of scheme A (`parse → validate → verify`). pub fn parse_der(der: &[u8]) -> Result { - let (rem, _obj) = parse_der(der).map_err(|e| ManifestParseError::Parse(e.to_string()))?; + let (_tag, _value, rem) = + der_take_tlv(der).map_err(|e| ManifestParseError::Parse(e))?; if !rem.is_empty() { return Err(ManifestParseError::TrailingBytes(rem.len())); } @@ -265,6 +267,18 @@ impl ManifestEContent { pub fn decode_der(der: &[u8]) -> Result { Ok(Self::parse_der(der)?.validate_profile()?) } + + /// Parse and return the manifest fileList. + /// + /// Note: `ManifestEContent` is profile-validated when produced via `decode_der()`, so this + /// should only fail due to internal inconsistencies (or if constructed manually). + pub fn parse_files(&self) -> Result, ManifestProfileError> { + parse_file_list_sha256_fast(&self.file_list_der) + } + + pub fn file_count(&self) -> usize { + self.file_count + } } impl ManifestObjectParsed { @@ -292,111 +306,328 @@ impl ManifestObjectParsed { impl ManifestEContentParsed { pub fn validate_profile(self) -> Result { - let (_rem, obj) = - parse_der(&self.der).map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; - - let seq = obj - .as_sequence() - .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; - if seq.len() != 5 && seq.len() != 6 { - return Err(ManifestProfileError::InvalidManifestSequenceLen(seq.len())); - } - - let mut idx = 0; - let mut version: u32 = 0; - if seq.len() == 6 { - let v_obj = &seq[0]; - if v_obj.class() != der_parser::ber::Class::ContextSpecific || v_obj.tag() != Tag(0) { - return Err(ManifestProfileError::ProfileDecode( - "Manifest.version must be [0] EXPLICIT INTEGER".into(), - )); - } - let inner_der = v_obj - .as_slice() - .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; - let (rem, inner) = parse_der(inner_der) - .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; - if !rem.is_empty() { - return Err(ManifestProfileError::ProfileDecode( - "trailing bytes inside Manifest.version".into(), - )); - } - let v = inner - .as_u64() - .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; - if v != 0 { - return Err(ManifestProfileError::InvalidManifestVersion(v)); - } - version = 0; - idx = 1; - } - - let manifest_number = parse_manifest_number(&seq[idx])?; - idx += 1; - - let this_update = - parse_generalized_time(&seq[idx], ManifestProfileError::InvalidThisUpdate)?; - idx += 1; - let next_update = - parse_generalized_time(&seq[idx], ManifestProfileError::InvalidNextUpdate)?; - idx += 1; - if next_update <= this_update { - return Err(ManifestProfileError::NextUpdateNotLater); - } - - let file_hash_alg = oid_to_string(&seq[idx])?; - idx += 1; - if file_hash_alg != OID_SHA256 { - return Err(ManifestProfileError::InvalidFileHashAlg(file_hash_alg)); - } - - let files = parse_file_list_sha256(&seq[idx])?; - - Ok(ManifestEContent { - version, - manifest_number, - this_update, - next_update, - file_hash_alg: OID_SHA256.to_string(), - files, - }) + decode_manifest_econtent_fast(&self.der) } } -fn parse_manifest_number(obj: &DerObject<'_>) -> Result { - let n = obj - .as_biguint() - .map_err(|_e| ManifestProfileError::InvalidManifestNumber)?; - let out = BigUnsigned::from_biguint(&n); - if out.bytes_be.len() > 20 { - return Err(ManifestProfileError::ManifestNumberTooLong); - } - Ok(out) -} +fn validate_file_name_bytes(bytes: &[u8]) -> Result<(), ManifestProfileError> { + // RFC 9286 §4.2.2: + // 1+ chars from a-zA-Z0-9-_ , then '.', then 3-letter extension. + if bytes.len() < 5 { + return Err(ManifestProfileError::InvalidFileName( + String::from_utf8_lossy(bytes).into_owned(), + )); + }; -fn parse_generalized_time( - obj: &DerObject<'_>, - err: ManifestProfileError, -) -> Result { - match &obj.content { - BerObjectContent::GeneralizedTime(dt) => dt - .to_datetime() - .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string())), - _ => Err(err), + // "followed by a single . (DOT), followed by a three letter extension" + // -> the dot must be exactly 4 bytes from the end. + let dot_pos = bytes.len() - 4; + if bytes[dot_pos] != b'.' { + return Err(ManifestProfileError::InvalidFileName( + String::from_utf8_lossy(bytes).into_owned(), + )); } -} -fn parse_file_list_sha256(obj: &DerObject<'_>) -> Result, ManifestProfileError> { - let seq = obj - .as_sequence() - .map_err(|_e| ManifestProfileError::InvalidFileList)?; - let mut out = Vec::with_capacity(seq.len()); - for entry in seq { - let (file_name, hash_bytes) = parse_file_and_hash(entry)?; - validate_file_name(&file_name)?; - if hash_bytes.len() != 32 { - return Err(ManifestProfileError::InvalidHashLength(hash_bytes.len())); + #[inline(always)] + fn valid_base_char(b: u8) -> bool { + // RFC 9286 allowed set: a-zA-Z0-9-_ + (b'0'..=b'9').contains(&b) + || (b'a'..=b'z').contains(&b) + || (b'A'..=b'Z').contains(&b) + || b == b'-' + || b == b'_' + } + + for &b in &bytes[..dot_pos] { + if (b & 0x80) != 0 || !valid_base_char(b) { + return Err(ManifestProfileError::InvalidFileName( + String::from_utf8_lossy(bytes).into_owned(), + )); } + } + + let e0 = bytes[dot_pos + 1]; + let e1 = bytes[dot_pos + 2]; + let e2 = bytes[dot_pos + 3]; + if (e0 & 0x80) != 0 || (e1 & 0x80) != 0 || (e2 & 0x80) != 0 { + return Err(ManifestProfileError::InvalidFileName( + String::from_utf8_lossy(bytes).into_owned(), + )); + } + + #[inline(always)] + fn lower_if_alpha(b: u8) -> Option { + match b { + b'a'..=b'z' => Some(b), + b'A'..=b'Z' => Some(b + 32), + _ => None, + } + } + + let Some(l0) = lower_if_alpha(e0) else { + return Err(ManifestProfileError::InvalidFileName( + String::from_utf8_lossy(bytes).into_owned(), + )); + }; + let Some(l1) = lower_if_alpha(e1) else { + return Err(ManifestProfileError::InvalidFileName( + String::from_utf8_lossy(bytes).into_owned(), + )); + }; + let Some(l2) = lower_if_alpha(e2) else { + return Err(ManifestProfileError::InvalidFileName( + String::from_utf8_lossy(bytes).into_owned(), + )); + }; + + match [l0, l1, l2] { + // Full IANA list (see `common.rs`). + [b'a', b's', b'a'] + | [b'c', b'e', b'r'] + | [b'c', b'r', b'l'] + | [b'g', b'b', b'r'] + | [b'm', b'f', b't'] + | [b'r', b'o', b'a'] + | [b's', b'i', b'g'] + | [b't', b'a', b'k'] => Ok(()), + _ => Err(ManifestProfileError::InvalidFileName( + String::from_utf8_lossy(bytes).into_owned(), + )), + } +} + +fn decode_manifest_econtent_fast(der: &[u8]) -> Result { + let (tag, mut seq_content, rem) = der_take_tlv(der) + .map_err(|e| ManifestProfileError::ProfileDecode(format!("DER decode error: {e}")))?; + if !rem.is_empty() { + return Err(ManifestProfileError::ProfileDecode(format!( + "trailing bytes after DER object: {} bytes", + rem.len() + ))); + } + if tag != 0x30 { + return Err(ManifestProfileError::ProfileDecode( + "Manifest eContent must be SEQUENCE".into(), + )); + } + + let seq_len = der_count_elements(seq_content) + .map_err(|e| ManifestProfileError::ProfileDecode(e))?; + if seq_len != 5 && seq_len != 6 { + return Err(ManifestProfileError::InvalidManifestSequenceLen(seq_len)); + } + + let mut version: u32 = 0; + if seq_len == 6 { + let Some(&first_tag) = seq_content.first() else { + return Err(ManifestProfileError::InvalidManifestSequenceLen(0)); + }; + if first_tag != 0xA0 { + return Err(ManifestProfileError::ProfileDecode( + "Manifest.version must be [0] EXPLICIT INTEGER".into(), + )); + } + + let (_cs_tag, cs_value, after) = der_take_tlv(seq_content).map_err(|e| { + ManifestProfileError::ProfileDecode(format!("Manifest.version decode error: {e}")) + })?; + seq_content = after; + + let (inner_tag, inner_value, inner_rem) = der_take_tlv(cs_value).map_err(|e| { + ManifestProfileError::ProfileDecode(format!("Manifest.version inner decode error: {e}")) + })?; + if !inner_rem.is_empty() { + return Err(ManifestProfileError::ProfileDecode( + "trailing bytes inside Manifest.version".into(), + )); + } + if inner_tag != 0x02 { + return Err(ManifestProfileError::ProfileDecode( + "Manifest.version must be [0] EXPLICIT INTEGER".into(), + )); + } + let v = der_integer_to_u64(inner_value).map_err(|e| { + ManifestProfileError::ProfileDecode(format!("Manifest.version decode error: {e}")) + })?; + if v != 0 { + return Err(ManifestProfileError::InvalidManifestVersion(v)); + } + version = 0; + } + + let (mn_tag, mn_value, after) = der_take_tlv(seq_content).map_err(|e| { + ManifestProfileError::ProfileDecode(format!("Manifest.manifestNumber decode error: {e}")) + })?; + seq_content = after; + if mn_tag != 0x02 { + return Err(ManifestProfileError::InvalidManifestNumber); + } + let manifest_number = der_integer_to_bigunsigned(mn_value)?; + + let (tu_tag, tu_value, after) = der_take_tlv(seq_content).map_err(|e| { + ManifestProfileError::ProfileDecode(format!("Manifest.thisUpdate decode error: {e}")) + })?; + seq_content = after; + if tu_tag != 0x18 { + return Err(ManifestProfileError::InvalidThisUpdate); + } + let this_update = parse_generalized_time_bytes(tu_value) + .map_err(|e| ManifestProfileError::ProfileDecode(e))?; + + let (nu_tag, nu_value, after) = der_take_tlv(seq_content).map_err(|e| { + ManifestProfileError::ProfileDecode(format!("Manifest.nextUpdate decode error: {e}")) + })?; + seq_content = after; + if nu_tag != 0x18 { + return Err(ManifestProfileError::InvalidNextUpdate); + } + let next_update = parse_generalized_time_bytes(nu_value) + .map_err(|e| ManifestProfileError::ProfileDecode(e))?; + if next_update <= this_update { + return Err(ManifestProfileError::NextUpdateNotLater); + } + + let (oid_tag, oid_value, after) = der_take_tlv(seq_content).map_err(|e| { + ManifestProfileError::ProfileDecode(format!("Manifest.fileHashAlg decode error: {e}")) + })?; + seq_content = after; + if oid_tag != 0x06 { + return Err(ManifestProfileError::ProfileDecode( + "Manifest.fileHashAlg must be OBJECT IDENTIFIER".into(), + )); + } + if !oid_content_is_sha256(oid_value) { + return Err(ManifestProfileError::InvalidFileHashAlg( + oid_content_to_string(oid_value), + )); + } + + let (fl_tag, fl_value, after) = der_take_tlv(seq_content).map_err(|e| { + ManifestProfileError::ProfileDecode(format!("Manifest.fileList decode error: {e}")) + })?; + seq_content = after; + if fl_tag != 0x30 { + return Err(ManifestProfileError::InvalidFileList); + } + let file_count = validate_file_list_sha256_fast(fl_value)?; + let file_list_der = fl_value.to_vec(); + + if !seq_content.is_empty() { + return Err(ManifestProfileError::InvalidManifestSequenceLen(seq_len)); + } + + Ok(ManifestEContent { + version, + manifest_number, + this_update, + next_update, + file_hash_alg: OID_SHA256.to_string(), + file_list_der, + file_count, + }) +} + +fn validate_file_list_sha256_fast(content: &[u8]) -> Result { + let mut cur = content; + let mut count: usize = 0; + while !cur.is_empty() { + let (tag, value, rem) = der_take_tlv(cur).map_err(|e| { + ManifestProfileError::ProfileDecode(format!("fileList entry decode error: {e}")) + })?; + cur = rem; + if tag != 0x30 { + return Err(ManifestProfileError::InvalidFileAndHash); + } + + let mut entry = value; + let (fn_tag, fn_value, entry_rem) = der_take_tlv(entry).map_err(|e| { + ManifestProfileError::ProfileDecode(format!("fileList fileName decode error: {e}")) + })?; + entry = entry_rem; + if fn_tag != 0x16 { + return Err(ManifestProfileError::InvalidFileAndHash); + } + if entry.is_empty() { + return Err(ManifestProfileError::InvalidFileAndHash); + } + validate_file_name_bytes(fn_value)?; + + let (hash_tag, hash_value, entry_rem) = der_take_tlv(entry).map_err(|_e| { + // Missing second element should map to "SEQUENCE of 2" shape error. + ManifestProfileError::InvalidFileAndHash + })?; + entry = entry_rem; + if !entry.is_empty() { + return Err(ManifestProfileError::InvalidFileAndHash); + } + if hash_tag != 0x03 { + return Err(ManifestProfileError::InvalidHashType); + } + if hash_value.is_empty() { + return Err(ManifestProfileError::InvalidHashLength(0)); + } + let unused_bits = hash_value[0]; + if unused_bits != 0 { + return Err(ManifestProfileError::HashNotOctetAligned); + } + let bits = &hash_value[1..]; + if bits.len() != 32 { + return Err(ManifestProfileError::InvalidHashLength(bits.len())); + } + count += 1; + } + Ok(count) +} + +fn parse_file_list_sha256_fast(content: &[u8]) -> Result, ManifestProfileError> { + // Heuristic initial capacity (avoid a full pre-scan, which is expensive for xlarge manifests). + // Each FileAndHash entry is typically tens of bytes; 80 is a conservative average. + let est = (content.len() / 80).clamp(16, 4096); + let mut cur = content; + let mut out: Vec = Vec::with_capacity(est); + while !cur.is_empty() { + let (tag, value, rem) = der_take_tlv(cur) + .map_err(|e| ManifestProfileError::ProfileDecode(format!("fileList entry decode error: {e}")))?; + cur = rem; + if tag != 0x30 { + return Err(ManifestProfileError::InvalidFileAndHash); + } + let mut entry = value; + let (fn_tag, fn_value, entry_rem) = der_take_tlv(entry) + .map_err(|e| ManifestProfileError::ProfileDecode(format!("fileList fileName decode error: {e}")))?; + entry = entry_rem; + if fn_tag != 0x16 { + return Err(ManifestProfileError::InvalidFileAndHash); + } + if entry.is_empty() { + return Err(ManifestProfileError::InvalidFileAndHash); + } + let file_name = validate_and_copy_file_name(fn_value)?; + + let (hash_tag, hash_value, entry_rem) = der_take_tlv(entry).map_err(|_e| { + // Missing second element should map to "SEQUENCE of 2" shape error. + ManifestProfileError::InvalidFileAndHash + })?; + entry = entry_rem; + if !entry.is_empty() { + return Err(ManifestProfileError::InvalidFileAndHash); + } + if hash_tag != 0x03 { + return Err(ManifestProfileError::InvalidHashType); + } + if hash_value.is_empty() { + return Err(ManifestProfileError::InvalidHashLength(0)); + } + let unused_bits = hash_value[0]; + if unused_bits != 0 { + return Err(ManifestProfileError::HashNotOctetAligned); + } + let bits = &hash_value[1..]; + if bits.len() != 32 { + return Err(ManifestProfileError::InvalidHashLength(bits.len())); + } + let mut hash_bytes = [0u8; 32]; + hash_bytes.copy_from_slice(bits); out.push(FileAndHash { file_name, hash_bytes, @@ -405,58 +636,346 @@ fn parse_file_list_sha256(obj: &DerObject<'_>) -> Result, Manif Ok(out) } -fn parse_file_and_hash(obj: &DerObject<'_>) -> Result<(String, Vec), ManifestProfileError> { - let seq = obj - .as_sequence() - .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; - if seq.len() != 2 { - return Err(ManifestProfileError::InvalidFileAndHash); - } - let file_name = seq[0] - .as_str() - .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))? - .to_string(); - let (unused_bits, bits) = match &seq[1].content { - BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()), - _ => return Err(ManifestProfileError::InvalidHashType), - }; - if unused_bits != 0 { - return Err(ManifestProfileError::HashNotOctetAligned); - } - Ok((file_name, bits)) +fn validate_and_copy_file_name(bytes: &[u8]) -> Result { + validate_file_name_bytes(bytes)?; + Ok(unsafe { String::from_utf8_unchecked(bytes.to_vec()) }) } -fn validate_file_name(name: &str) -> Result<(), ManifestProfileError> { - // RFC 9286 §4.2.2: - // 1+ chars from a-zA-Z0-9-_ , then '.', then 3-letter extension. - let Some((base, ext)) = name.rsplit_once('.') else { - return Err(ManifestProfileError::InvalidFileName(name.to_string())); - }; - if base.is_empty() || ext.len() != 3 { - return Err(ManifestProfileError::InvalidFileName(name.to_string())); +fn der_count_elements(mut input: &[u8]) -> Result { + let mut count: usize = 0; + while !input.is_empty() { + let (_tag, _value, rem) = der_take_tlv(input)?; + input = rem; + count += 1; } - if !base - .bytes() - .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') - { - return Err(ManifestProfileError::InvalidFileName(name.to_string())); - } - if !ext.bytes().all(|b| b.is_ascii_alphabetic()) { - return Err(ManifestProfileError::InvalidFileName(name.to_string())); - } - let ext_lower = ext.to_ascii_lowercase(); - if !crate::data_model::common::IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS - .iter() - .any(|&e| e == ext_lower) - { - return Err(ManifestProfileError::InvalidFileName(name.to_string())); - } - Ok(()) + Ok(count) } -fn oid_to_string(obj: &DerObject<'_>) -> Result { - let oid = obj - .as_oid() - .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; - Ok(oid.to_id_string()) +fn der_integer_to_u64(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Err("INTEGER empty".into()); + } + // Reject negative (two's complement). + if bytes[0] & 0x80 != 0 { + return Err("INTEGER is negative".into()); + } + if bytes.len() > 8 { + return Err("INTEGER too large".into()); + } + let mut v: u64 = 0; + for &b in bytes { + v = (v << 8) | (b as u64); + } + Ok(v) +} + +fn der_integer_to_bigunsigned(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Err(ManifestProfileError::InvalidManifestNumber); + } + // Two's complement: for non-negative values, a leading 0x00 may be present. + if bytes[0] & 0x80 != 0 { + return Err(ManifestProfileError::InvalidManifestNumber); + } + let mut start = 0usize; + while start + 1 < bytes.len() && bytes[start] == 0 { + start += 1; + } + let mut minimal = bytes[start..].to_vec(); + if minimal.is_empty() { + minimal.push(0); + } + if minimal.len() > 20 { + return Err(ManifestProfileError::ManifestNumberTooLong); + } + Ok(BigUnsigned { bytes_be: minimal }) +} + +fn parse_generalized_time_bytes(bytes: &[u8]) -> Result { + // Accept "YYYYMMDDHHMMSSZ" and also allow optional fractional seconds (".fff...Z"). + if !bytes.is_ascii() { + return Err("GeneralizedTime not ASCII".into()); + } + let s = std::str::from_utf8(bytes).map_err(|e| e.to_string())?; + if !s.ends_with('Z') { + return Err("GeneralizedTime must end with 'Z'".into()); + } + let core = &s[..s.len() - 1]; + let (main, frac) = core.split_once('.').map_or((core, None), |(a, b)| (a, Some(b))); + if main.len() != 14 || !main.bytes().all(|b| b.is_ascii_digit()) { + return Err("GeneralizedTime must be YYYYMMDDHHMMSS[.fff]Z".into()); + } + let year: i32 = main[0..4].parse().map_err(|_| "bad year")?; + let month: u8 = main[4..6].parse().map_err(|_| "bad month")?; + let day: u8 = main[6..8].parse().map_err(|_| "bad day")?; + let hour: u8 = main[8..10].parse().map_err(|_| "bad hour")?; + let minute: u8 = main[10..12].parse().map_err(|_| "bad minute")?; + let second: u8 = main[12..14].parse().map_err(|_| "bad second")?; + + let nanosecond: u32 = if let Some(frac) = frac { + if frac.is_empty() || !frac.bytes().all(|b| b.is_ascii_digit()) { + return Err("bad fractional seconds".into()); + } + let mut ns: u32 = 0; + let mut scale: u32 = 1_000_000_000; + for (i, ch) in frac.bytes().enumerate() { + if i >= 9 { + break; + } + scale /= 10; + ns += ((ch - b'0') as u32) * scale; + } + ns + } else { + 0 + }; + + let date = time::Date::from_calendar_date(year, time::Month::try_from(month).map_err(|_| "bad month")?, day) + .map_err(|e| e.to_string())?; + let t = time::Time::from_hms_nano(hour, minute, second, nanosecond).map_err(|e| e.to_string())?; + Ok(date.with_time(t).assume_utc()) +} + +fn oid_content_is_sha256(bytes: &[u8]) -> bool { + // 2.16.840.1.101.3.4.2.1 + let mut arcs = oid_content_iter(bytes); + const EXPECTED: &[u64] = &[2, 16, 840, 1, 101, 3, 4, 2, 1]; + for &e in EXPECTED { + match arcs.next() { + Some(v) if v == e => {} + _ => return false, + } + } + arcs.next().is_none() +} + +fn oid_content_to_string(bytes: &[u8]) -> String { + let arcs: Vec = oid_content_iter(bytes).collect(); + if arcs.is_empty() { + return "".to_string(); + } + let mut s = String::new(); + for (i, a) in arcs.iter().enumerate() { + if i > 0 { + s.push('.'); + } + s.push_str(&a.to_string()); + } + s +} + +fn oid_content_iter(bytes: &[u8]) -> impl Iterator + '_ { + struct It<'a> { + bytes: &'a [u8], + pos: usize, + first_done: bool, + first_a0: u64, + first_a1: u64, + emit_first_idx: u8, + } + impl<'a> Iterator for It<'a> { + type Item = u64; + fn next(&mut self) -> Option { + if !self.first_done { + if self.bytes.is_empty() { + self.first_done = true; + return None; + } + let first = self.bytes[0] as u64; + self.first_a0 = first / 40; + self.first_a1 = first % 40; + self.pos = 1; + self.first_done = true; + self.emit_first_idx = 0; + } + if self.emit_first_idx == 0 { + self.emit_first_idx = 1; + return Some(self.first_a0); + } + if self.emit_first_idx == 1 { + self.emit_first_idx = 2; + return Some(self.first_a1); + } + if self.pos >= self.bytes.len() { + return None; + } + let mut v: u64 = 0; + while self.pos < self.bytes.len() { + let b = self.bytes[self.pos]; + self.pos += 1; + v = (v << 7) | ((b & 0x7F) as u64); + if b & 0x80 == 0 { + return Some(v); + } + } + None + } + } + It { + bytes, + pos: 0, + first_done: false, + first_a0: 0, + first_a1: 0, + emit_first_idx: 0, + } +} + +fn der_take_tlv(input: &[u8]) -> Result<(u8, &[u8], &[u8]), String> { + if input.len() < 2 { + return Err("truncated DER (need tag+len)".into()); + } + let tag = input[0]; + if (tag & 0x1F) == 0x1F { + return Err("high-tag-number form not supported".into()); + } + let len0 = input[1]; + if len0 == 0x80 { + return Err("indefinite length not allowed in DER".into()); + } + let (len, hdr_len) = if len0 & 0x80 == 0 { + (len0 as usize, 2usize) + } else { + let n = (len0 & 0x7F) as usize; + if n == 0 || n > 8 { + return Err("invalid DER length".into()); + } + if input.len() < 2 + n { + return Err("truncated DER (length bytes)".into()); + } + let mut l: usize = 0; + for &b in &input[2..2 + n] { + l = (l << 8) | (b as usize); + } + (l, 2 + n) + }; + if input.len() < hdr_len + len { + return Err("truncated DER (value bytes)".into()); + } + let value = &input[hdr_len..hdr_len + len]; + let rem = &input[hdr_len + len..]; + Ok((tag, value, rem)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn tlv(tag: u8, value: &[u8]) -> Vec { + assert!(value.len() < 128); + let mut out = Vec::with_capacity(2 + value.len()); + out.push(tag); + out.push(value.len() as u8); + out.extend_from_slice(value); + out + } + + fn tlv_long_len(tag: u8, len_bytes: &[u8], value: &[u8]) -> Vec { + let mut out = Vec::with_capacity(2 + len_bytes.len() + value.len()); + out.push(tag); + out.push(0x80 | (len_bytes.len() as u8)); + out.extend_from_slice(len_bytes); + out.extend_from_slice(value); + out + } + + #[test] + fn der_take_tlv_supports_short_and_long_form_lengths_and_errors() { + let v = b"abc"; + let der = tlv(0x04, v); + let (tag, val, rem) = der_take_tlv(&der).expect("short len"); + assert_eq!(tag, 0x04); + assert_eq!(val, v); + assert!(rem.is_empty()); + + // Long-form length with 1 length byte (130). + let v = vec![b'x'; 130]; + let der = tlv_long_len(0x04, &[0x82], &v); + let (tag, val, rem) = der_take_tlv(&der).expect("long len 1"); + assert_eq!(tag, 0x04); + assert_eq!(val.len(), 130); + assert!(rem.is_empty()); + + // Long-form length with 2 length bytes (256). + let v = vec![b'y'; 256]; + let der = tlv_long_len(0x04, &[0x01, 0x00], &v); + let (tag, val, rem) = der_take_tlv(&der).expect("long len 2"); + assert_eq!(tag, 0x04); + assert_eq!(val.len(), 256); + assert!(rem.is_empty()); + + assert!(der_take_tlv(&[]).is_err()); + assert!(der_take_tlv(&[0x04]).is_err()); + + // High-tag-number form not supported. + assert!(der_take_tlv(&[0x1F, 0x01, 0x00]).is_err()); + + // Indefinite length is not allowed in DER. + assert!(der_take_tlv(&[0x04, 0x80]).is_err()); + + // Invalid long-form length encoding. + assert!(der_take_tlv(&[0x04, 0x81]).is_err()); + assert!(der_take_tlv(&[0x04, 0x89]).is_err()); + } + + #[test] + fn parse_generalized_time_bytes_accepts_fraction_and_rejects_invalid() { + let t = parse_generalized_time_bytes(b"20260101000000Z").expect("basic time"); + assert_eq!(t.year(), 2026); + + let t = parse_generalized_time_bytes(b"20260101000000.1Z").expect("fractional"); + assert_eq!(t.nanosecond(), 100_000_000); + + assert!(parse_generalized_time_bytes(b"20260101000000").is_err()); + assert!(parse_generalized_time_bytes(b"20260101000000+00").is_err()); + assert!(parse_generalized_time_bytes(b"2026010100000Z").is_err()); + assert!(parse_generalized_time_bytes(b"20261301000000Z").is_err()); + assert!(parse_generalized_time_bytes(b"20260132000000Z").is_err()); + assert!(parse_generalized_time_bytes(&[0xFF]).is_err()); + } + + #[test] + fn oid_helpers_accept_sha256_and_format_invalid() { + // 2.16.840.1.101.3.4.2.1 + let sha256_oid_content = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01]; + assert!(oid_content_is_sha256(&sha256_oid_content)); + assert!(!oid_content_is_sha256(&[0x55, 0x04, 0x03])); // 2.5.4.3 + + assert_eq!(oid_content_to_string(&[]), "".to_string()); + } + + #[test] + fn validate_file_list_sha256_fast_counts_and_rejects_bad_hash() { + fn file_and_hash(file: &str, digest: u8) -> Vec { + let mut hash = vec![0u8; 33]; + hash[0] = 0; // unused bits + for b in &mut hash[1..] { + *b = digest; + } + let ia5 = tlv(0x16, file.as_bytes()); + let bit = tlv(0x03, &hash); + let mut entry = Vec::new(); + entry.extend_from_slice(&ia5); + entry.extend_from_slice(&bit); + tlv(0x30, &entry) + } + + let mut list = Vec::new(); + list.extend_from_slice(&file_and_hash("A.cer", 0xAA)); + list.extend_from_slice(&file_and_hash("B.roa", 0xBB)); + assert_eq!(validate_file_list_sha256_fast(&list).expect("count"), 2); + + // Wrong hash length. + let mut bad = Vec::new(); + let ia5 = tlv(0x16, b"A.cer"); + let bit = tlv(0x03, &[0u8; 2]); // too short + let mut entry = Vec::new(); + entry.extend_from_slice(&ia5); + entry.extend_from_slice(&bit); + bad.extend_from_slice(&tlv(0x30, &entry)); + assert!(matches!( + validate_file_list_sha256_fast(&bad), + Err(ManifestProfileError::InvalidHashLength(_)) + )); + } } diff --git a/src/data_model/signed_object.rs b/src/data_model/signed_object.rs index 8ea93de..cba2428 100644 --- a/src/data_model/signed_object.rs +++ b/src/data_model/signed_object.rs @@ -7,7 +7,7 @@ use crate::data_model::oid::{ use crate::data_model::rc::{ResourceCertificate, SubjectInfoAccess}; use der_parser::ber::Class; use der_parser::der::{DerObject, Tag, parse_der}; -use sha2::{Digest, Sha256}; +use ring::digest; use x509_parser::prelude::FromDer; use x509_parser::public_key::PublicKey; use x509_parser::x509::SubjectPublicKeyInfo; @@ -812,8 +812,8 @@ fn validate_signed_data_profile( }); } - let computed = Sha256::digest(&encap_content_info.econtent).to_vec(); - if computed != signed_attrs.message_digest { + let computed = digest::digest(&digest::SHA256, &encap_content_info.econtent); + if computed.as_ref() != signed_attrs.message_digest.as_slice() { return Err(SignedObjectValidateError::MessageDigestMismatch); } diff --git a/src/fetch/http.rs b/src/fetch/http.rs index db3da97..55ce3c7 100644 --- a/src/fetch/http.rs +++ b/src/fetch/http.rs @@ -62,3 +62,52 @@ impl Fetcher for BlockingHttpFetcher { self.fetch_bytes(uri) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::{Read, Write}; + use std::net::TcpListener; + use std::thread; + + fn spawn_one_shot_http_server(status_line: &'static str, body: &'static [u8]) -> String { + let listener = TcpListener::bind(("127.0.0.1", 0)).expect("bind"); + let addr = listener.local_addr().expect("addr"); + thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept"); + let mut buf = [0u8; 1024]; + let _ = stream.read(&mut buf); + let hdr = format!( + "{status_line}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + stream.write_all(hdr.as_bytes()).expect("write hdr"); + stream.write_all(body).expect("write body"); + }); + format!("http://{}/", addr) + } + + #[test] + fn fetch_bytes_returns_body_on_success() { + let url = spawn_one_shot_http_server("HTTP/1.1 200 OK", b"hello"); + let http = BlockingHttpFetcher::new(HttpFetcherConfig { + timeout: Duration::from_secs(2), + ..HttpFetcherConfig::default() + }) + .expect("http"); + let got = http.fetch_bytes(&url).expect("fetch"); + assert_eq!(got, b"hello"); + } + + #[test] + fn fetch_bytes_rejects_non_success_status() { + let url = spawn_one_shot_http_server("HTTP/1.1 404 Not Found", b""); + let http = BlockingHttpFetcher::new(HttpFetcherConfig { + timeout: Duration::from_secs(2), + ..HttpFetcherConfig::default() + }) + .expect("http"); + let err = http.fetch_bytes(&url).unwrap_err(); + assert!(err.contains("http status"), "{err}"); + } +} diff --git a/src/fetch/rsync.rs b/src/fetch/rsync.rs index 01bd177..1d85f3d 100644 --- a/src/fetch/rsync.rs +++ b/src/fetch/rsync.rs @@ -80,3 +80,39 @@ fn normalize_rsync_base_uri(s: &str) -> String { format!("{s}/") } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn local_dir_rsync_fetcher_collects_files_and_normalizes_base_uri() { + let tmp = tempfile::tempdir().expect("tempdir"); + std::fs::create_dir_all(tmp.path().join("nested")).expect("mkdir"); + std::fs::write(tmp.path().join("a.mft"), b"a").expect("write"); + std::fs::write(tmp.path().join("nested").join("b.roa"), b"b").expect("write"); + + let f = LocalDirRsyncFetcher::new(tmp.path()); + let mut objects = f + .fetch_objects("rsync://example.net/repo") + .expect("fetch_objects"); + objects.sort_by(|(a, _), (b, _)| a.cmp(b)); + + assert_eq!(objects.len(), 2); + assert_eq!(objects[0].0, "rsync://example.net/repo/a.mft"); + assert_eq!(objects[0].1, b"a"); + assert_eq!(objects[1].0, "rsync://example.net/repo/nested/b.roa"); + assert_eq!(objects[1].1, b"b"); + } + + #[test] + fn local_dir_rsync_fetcher_reports_read_dir_errors() { + let tmp = tempfile::tempdir().expect("tempdir"); + let missing = tmp.path().join("missing"); + let f = LocalDirRsyncFetcher::new(missing); + let err = f.fetch_objects("rsync://example.net/repo").unwrap_err(); + match err { + RsyncFetchError::Fetch(msg) => assert!(!msg.is_empty()), + } + } +} diff --git a/src/fetch/rsync_system.rs b/src/fetch/rsync_system.rs index 5245136..419e38d 100644 --- a/src/fetch/rsync_system.rs +++ b/src/fetch/rsync_system.rs @@ -142,3 +142,67 @@ fn walk_dir_collect( } Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normalize_rsync_base_uri_appends_slash_when_missing() { + assert_eq!( + normalize_rsync_base_uri("rsync://example.net/repo"), + "rsync://example.net/repo/".to_string() + ); + assert_eq!( + normalize_rsync_base_uri("rsync://example.net/repo/"), + "rsync://example.net/repo/".to_string() + ); + } + + #[test] + fn walk_dir_collect_collects_files_and_normalizes_backslashes_in_uri() { + let temp = tempfile::tempdir().expect("tempdir"); + let root = temp.path(); + std::fs::create_dir_all(root.join("sub")).expect("mkdir"); + std::fs::write(root.join("sub").join("a.cer"), b"x").expect("write"); + std::fs::write(root.join("b\\c.mft"), b"y").expect("write backslash file"); + + let mut out: Vec<(String, Vec)> = Vec::new(); + walk_dir_collect(root, root, "rsync://example.net/repo/", &mut out).expect("walk"); + out.sort_by(|a, b| a.0.cmp(&b.0)); + + assert_eq!(out.len(), 2); + assert_eq!(out[0].0, "rsync://example.net/repo/b/c.mft"); + assert_eq!(out[0].1, b"y"); + assert_eq!(out[1].0, "rsync://example.net/repo/sub/a.cer"); + assert_eq!(out[1].1, b"x"); + } + + #[test] + fn system_rsync_fetcher_reports_spawn_and_exit_errors() { + let dst = tempfile::tempdir().expect("tempdir"); + + // 1) Spawn error. + let f = SystemRsyncFetcher::new(SystemRsyncConfig { + rsync_bin: PathBuf::from("/this/does/not/exist/rsync"), + timeout: Duration::from_secs(1), + extra_args: Vec::new(), + }); + let e = f + .run_rsync("rsync://example.net/repo/", dst.path()) + .expect_err("spawn must fail"); + assert!(e.contains("rsync spawn failed:"), "{e}"); + + // 2) Non-zero exit status. + let f = SystemRsyncFetcher::new(SystemRsyncConfig { + rsync_bin: PathBuf::from("false"), + timeout: Duration::from_secs(1), + extra_args: Vec::new(), + }); + let e = f + .run_rsync("rsync://example.net/repo/", dst.path()) + .expect_err("false must fail"); + assert!(e.contains("rsync failed:"), "{e}"); + assert!(e.contains("status="), "{e}"); + } +} diff --git a/src/lib.rs b/src/lib.rs index 3d9f247..147024b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,18 @@ -pub mod audit; -pub mod cli; pub mod data_model; + +#[cfg(feature = "full")] +pub mod audit; +#[cfg(feature = "full")] +pub mod cli; +#[cfg(feature = "full")] pub mod fetch; +#[cfg(feature = "full")] pub mod policy; +#[cfg(feature = "full")] pub mod report; +#[cfg(feature = "full")] pub mod storage; +#[cfg(feature = "full")] pub mod sync; +#[cfg(feature = "full")] pub mod validation; diff --git a/src/sync/rrdp.rs b/src/sync/rrdp.rs index f3e8f9a..dc1f8c4 100644 --- a/src/sync/rrdp.rs +++ b/src/sync/rrdp.rs @@ -288,3 +288,197 @@ fn collect_element_text(node: &roxmltree::Node<'_, '_>) -> Option { fn strip_all_ascii_whitespace(s: &str) -> String { s.chars().filter(|c| !c.is_ascii_whitespace()).collect() } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + struct MapFetcher { + map: HashMap>, + } + + impl Fetcher for MapFetcher { + fn fetch(&self, uri: &str) -> Result, String> { + self.map + .get(uri) + .cloned() + .ok_or_else(|| format!("not found: {uri}")) + } + } + + fn notification_xml(session_id: &str, serial: u64, snapshot_uri: &str, snapshot_hash: &str) -> Vec { + format!( + r#""# + ) + .into_bytes() + } + + fn snapshot_xml(session_id: &str, serial: u64, published: &[(&str, &[u8])]) -> Vec { + let mut out = format!( + r#""# + ); + for (uri, bytes) in published { + let b64 = base64::engine::general_purpose::STANDARD.encode(bytes); + out.push_str(&format!(r#"{b64}"#)); + } + out.push_str(""); + out.into_bytes() + } + + #[test] + fn parse_notification_snapshot_rejects_non_ascii() { + let mut xml = b"".to_vec(); + xml.push(0x80); + let err = parse_notification_snapshot(&xml).unwrap_err(); + assert!(matches!(err, RrdpError::NotAscii)); + } + + #[test] + fn parse_notification_snapshot_parses_valid_minimal_notification() { + let sid = "550e8400-e29b-41d4-a716-446655440000"; + let snapshot_uri = "https://example.net/snapshot.xml"; + let hash = "00".repeat(32); + let xml = notification_xml(sid, 7, snapshot_uri, &hash); + let n = parse_notification_snapshot(&xml).expect("parse"); + assert_eq!(n.session_id, Uuid::parse_str(sid).unwrap()); + assert_eq!(n.serial, 7); + assert_eq!(n.snapshot_uri, snapshot_uri); + assert_eq!(hex::encode(n.snapshot_hash_sha256), hash); + } + + #[test] + fn sync_from_notification_snapshot_applies_snapshot_and_stores_state() { + let tmp = tempfile::tempdir().expect("tempdir"); + let store = RocksStore::open(tmp.path()).expect("open rocksdb"); + + let sid = "550e8400-e29b-41d4-a716-446655440000"; + let serial = 9u64; + let notif_uri = "https://example.net/notification.xml"; + let snapshot_uri = "https://example.net/snapshot.xml"; + + let snapshot = snapshot_xml( + sid, + serial, + &[ + ("rsync://example.net/repo/a.mft", b"mft-bytes"), + ("rsync://example.net/repo/b.roa", b"roa-bytes"), + ], + ); + let snapshot_hash = hex::encode(sha2::Sha256::digest(&snapshot)); + let notif = notification_xml(sid, serial, snapshot_uri, &snapshot_hash); + + let fetcher = MapFetcher { + map: HashMap::from([(snapshot_uri.to_string(), snapshot.clone())]), + }; + + let published = sync_from_notification_snapshot(&store, notif_uri, ¬if, &fetcher) + .expect("sync"); + assert_eq!(published, 2); + + let a = store + .get_raw("rsync://example.net/repo/a.mft") + .expect("get_raw") + .expect("a present"); + assert_eq!(a, b"mft-bytes"); + + let b = store + .get_raw("rsync://example.net/repo/b.roa") + .expect("get_raw") + .expect("b present"); + assert_eq!(b, b"roa-bytes"); + + let state_bytes = store + .get_rrdp_state(notif_uri) + .expect("get_rrdp_state") + .expect("state present"); + let state = RrdpState::decode(&state_bytes).expect("decode state"); + assert_eq!(state.session_id, sid); + assert_eq!(state.serial, serial); + } + + #[test] + fn sync_from_notification_snapshot_rejects_snapshot_hash_mismatch() { + let tmp = tempfile::tempdir().expect("tempdir"); + let store = RocksStore::open(tmp.path()).expect("open rocksdb"); + + let sid = "550e8400-e29b-41d4-a716-446655440000"; + let serial = 1u64; + let notif_uri = "https://example.net/notification.xml"; + let snapshot_uri = "https://example.net/snapshot.xml"; + + let snapshot = snapshot_xml(sid, serial, &[("rsync://example.net/repo/a.mft", b"x")]); + let notif = notification_xml(sid, serial, snapshot_uri, &"00".repeat(32)); + + let fetcher = MapFetcher { + map: HashMap::from([(snapshot_uri.to_string(), snapshot)]), + }; + let err = sync_from_notification_snapshot(&store, notif_uri, ¬if, &fetcher).unwrap_err(); + assert!(matches!(err, RrdpSyncError::Rrdp(RrdpError::SnapshotHashMismatch))); + } + + #[test] + fn apply_snapshot_rejects_session_id_and_serial_mismatch() { + let tmp = tempfile::tempdir().expect("tempdir"); + let store = RocksStore::open(tmp.path()).expect("open rocksdb"); + + let expected_sid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(); + let got_sid = "550e8400-e29b-41d4-a716-446655440001"; + + let snapshot = snapshot_xml(got_sid, 2, &[("rsync://example.net/repo/a.mft", b"x")]); + let err = apply_snapshot(&store, &snapshot, expected_sid, 2).unwrap_err(); + assert!(matches!( + err, + RrdpSyncError::Rrdp(RrdpError::SnapshotSessionIdMismatch { .. }) + )); + + let snapshot = snapshot_xml(expected_sid.to_string().as_str(), 3, &[("rsync://example.net/repo/a.mft", b"x")]); + let err = apply_snapshot(&store, &snapshot, expected_sid, 2).unwrap_err(); + assert!(matches!( + err, + RrdpSyncError::Rrdp(RrdpError::SnapshotSerialMismatch { .. }) + )); + } + + #[test] + fn strip_all_ascii_whitespace_removes_newlines_and_spaces() { + assert_eq!(strip_all_ascii_whitespace(" a \n b\tc "), "abc"); + } + + #[test] + fn apply_snapshot_reports_publish_errors() { + let tmp = tempfile::tempdir().expect("tempdir"); + let store = RocksStore::open(tmp.path()).expect("open rocksdb"); + let sid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(); + + // Missing publish/@uri + let xml = format!( + r#"AA=="# + ) + .into_bytes(); + let err = apply_snapshot(&store, &xml, sid, 1).unwrap_err(); + assert!(matches!(err, RrdpSyncError::Rrdp(RrdpError::PublishUriMissing))); + + // Missing base64 content (no text nodes). + let xml = format!( + r#""# + ) + .into_bytes(); + let err = apply_snapshot(&store, &xml, sid, 1).unwrap_err(); + assert!(matches!( + err, + RrdpSyncError::Rrdp(RrdpError::PublishContentMissing) + )); + + // Invalid base64 content. + let xml = format!( + r#"!!!"# + ) + .into_bytes(); + let err = apply_snapshot(&store, &xml, sid, 1).unwrap_err(); + assert!(matches!( + err, + RrdpSyncError::Rrdp(RrdpError::PublishBase64(_)) + )); + } +} diff --git a/src/validation/manifest.rs b/src/validation/manifest.rs index ff832e3..81b4030 100644 --- a/src/validation/manifest.rs +++ b/src/validation/manifest.rs @@ -260,13 +260,17 @@ fn revalidate_cached_pack_with_current_time( .map(|f| (f.rsync_uri.as_str(), f)) .collect(); - for entry in &manifest.manifest.files { + let entries = manifest + .manifest + .parse_files() + .map_err(|e| ManifestFreshError::Decode(ManifestDecodeError::Validate(e)))?; + for entry in &entries { let rsync_uri = - join_rsync_dir_and_file(&pack.publication_point_rsync_uri, &entry.file_name); + join_rsync_dir_and_file(&pack.publication_point_rsync_uri, entry.file_name.as_str()); let Some(file) = by_uri.get(rsync_uri.as_str()) else { return Err(ManifestCachedError::CachedMissingFile { rsync_uri }); }; - if file.sha256.as_slice() != entry.hash_bytes.as_slice() { + if file.sha256.as_slice() != entry.hash_bytes.as_ref() { return Err(ManifestCachedError::CachedHashMismatch { rsync_uri }); } } @@ -388,9 +392,11 @@ fn try_build_fresh_pack( } } - let mut files = Vec::with_capacity(manifest.manifest.files.len()); - for entry in &manifest.manifest.files { - let rsync_uri = join_rsync_dir_and_file(publication_point_rsync_uri, &entry.file_name); + let entries = manifest.manifest.parse_files().map_err(ManifestDecodeError::Validate)?; + let mut files = Vec::with_capacity(manifest.manifest.file_count()); + for entry in &entries { + let rsync_uri = + join_rsync_dir_and_file(publication_point_rsync_uri, entry.file_name.as_str()); let bytes = store .get_raw(&rsync_uri) .map_err(|_e| ManifestFreshError::MissingFile { @@ -401,7 +407,7 @@ fn try_build_fresh_pack( })?; let computed = sha2::Sha256::digest(&bytes); - if computed.as_slice() != entry.hash_bytes.as_slice() { + if computed.as_slice() != entry.hash_bytes.as_ref() { return Err(ManifestFreshError::HashMismatch { rsync_uri }); } diff --git a/tests/bench_manifest_decode_profile.rs b/tests/bench_manifest_decode_profile.rs new file mode 100644 index 0000000..ff972df --- /dev/null +++ b/tests/bench_manifest_decode_profile.rs @@ -0,0 +1,320 @@ +use rpki::data_model::manifest::ManifestObject; +use std::path::{Path, PathBuf}; +use std::time::Instant; + +fn default_samples_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/benchmark/selected_der") +} + +fn read_samples(dir: &Path) -> Vec { + let mut out = Vec::new(); + let rd = std::fs::read_dir(dir).unwrap_or_else(|e| panic!("read_dir {}: {e}", dir.display())); + for ent in rd.flatten() { + let path = ent.path(); + if path.extension().and_then(|s| s.to_str()) != Some("mft") { + continue; + } + let name = path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("unknown") + .to_string(); + out.push(Sample { name, path }); + } + out.sort_by(|a, b| a.name.cmp(&b.name)); + out +} + +#[derive(Clone, Debug)] +struct Sample { + name: String, + path: PathBuf, +} + +fn env_u64(name: &str, default: u64) -> u64 { + std::env::var(name) + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(default) +} + +fn env_u64_opt(name: &str) -> Option { + std::env::var(name) + .ok() + .and_then(|s| s.parse::().ok()) +} + +fn env_bool(name: &str) -> bool { + matches!( + std::env::var(name).as_deref(), + Ok("1") | Ok("true") | Ok("TRUE") | Ok("yes") | Ok("YES") + ) +} + +fn env_string(name: &str) -> Option { + std::env::var(name).ok().filter(|s| !s.trim().is_empty()) +} + +fn escape_md(s: &str) -> String { + s.replace('|', "\\|").replace('\n', " ") +} + +fn create_parent_dirs(path: &Path) { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).unwrap_or_else(|e| { + panic!("create_dir_all {}: {e}", parent.display()); + }); + } +} + +#[test] +#[ignore = "manual performance benchmark; prints Markdown table"] +fn manifest_decode_profile_benchmark_selected_der() { + let dir = env_string("BENCH_DIR") + .map(PathBuf::from) + .unwrap_or_else(default_samples_dir); + + let sample_filter = env_string("BENCH_SAMPLE"); + let fixed_iters = env_u64_opt("BENCH_ITERS"); + let warmup_iters = env_u64("BENCH_WARMUP_ITERS", 100); + let rounds = env_u64("BENCH_ROUNDS", 5); + let min_round_ms = env_u64("BENCH_MIN_ROUND_MS", 200); + let max_adaptive_iters = env_u64("BENCH_MAX_ITERS", 1_000_000); + let verbose = env_bool("BENCH_VERBOSE"); + let out_md = env_string("BENCH_OUT_MD").map(|p| PathBuf::from(p)); + let out_json = env_string("BENCH_OUT_JSON").map(|p| PathBuf::from(p)); + + if let Some(n) = fixed_iters { + assert!(n >= 1, "BENCH_ITERS must be >= 1"); + } + assert!(rounds >= 1, "BENCH_ROUNDS must be >= 1"); + assert!(min_round_ms >= 1, "BENCH_MIN_ROUND_MS must be >= 1"); + assert!(max_adaptive_iters >= 1, "BENCH_MAX_ITERS must be >= 1"); + + let mut samples = read_samples(&dir); + assert!( + !samples.is_empty(), + "no .mft files found under: {}", + dir.display() + ); + + if let Some(filter) = sample_filter.as_deref() { + samples.retain(|s| s.name == filter); + assert!(!samples.is_empty(), "no sample matched BENCH_SAMPLE={filter}"); + } + + println!("# Manifest decode + profile validate benchmark (debug build)"); + println!(); + println!("- dir: {}", dir.display()); + if let Some(n) = fixed_iters { + println!("- iters: {} (fixed)", n); + } else { + println!( + "- warmup: {} iters, rounds: {}, min_round: {}ms (adaptive iters, max {})", + warmup_iters, rounds, min_round_ms, max_adaptive_iters + ); + } + if let Some(filter) = sample_filter.as_deref() { + println!("- sample: {}", filter); + } + if verbose { + println!("- verbose: true"); + } + if let Some(p) = out_md.as_ref() { + println!("- out_md: {}", p.display()); + } + if let Some(p) = out_json.as_ref() { + println!("- out_json: {}", p.display()); + } + println!(); + + println!("Samples:"); + for s in &samples { + println!("- {}", s.name); + } + println!(); + + println!("| sample | file_count | avg ns/op | ops/s |"); + println!("|---|---:|---:|---:|"); + + let mut rows: Vec = Vec::with_capacity(samples.len()); + + for s in samples { + let bytes = + std::fs::read(&s.path).unwrap_or_else(|e| panic!("read {}: {e}", s.path.display())); + + let file_count = ManifestObject::decode_der(bytes.as_slice()) + .unwrap_or_else(|e| panic!("decode {}: {e}", s.name)) + .manifest + .file_count(); + + // Warm-up: exercise the exact decode path but don't time it. + for _ in 0..warmup_iters { + let input = std::hint::black_box(bytes.as_slice()); + let decoded = ManifestObject::decode_der(input).expect("decode"); + std::hint::black_box(decoded); + } + + let mut per_round_ns_per_op = Vec::with_capacity(rounds as usize); + for round in 0..rounds { + let iters = if let Some(n) = fixed_iters { + n + } else { + choose_iters_adaptive( + bytes.as_slice(), + min_round_ms, + max_adaptive_iters, + ) + }; + + let start = Instant::now(); + for _ in 0..iters { + let input = std::hint::black_box(bytes.as_slice()); + let decoded = ManifestObject::decode_der(input).expect("decode"); + std::hint::black_box(decoded); + } + let elapsed = start.elapsed(); + let total_ns = elapsed.as_secs_f64() * 1e9; + let ns_per_op = total_ns / (iters as f64); + per_round_ns_per_op.push(ns_per_op); + + if verbose { + println!( + "# {} round {}: iters={} total_ms={:.2} ns/op={:.2}", + s.name, + round + 1, + iters, + elapsed.as_secs_f64() * 1e3, + ns_per_op + ); + } + } + + let avg_ns = per_round_ns_per_op.iter().sum::() / (per_round_ns_per_op.len() as f64); + let ops_per_sec = 1e9_f64 / avg_ns; + + println!( + "| {} | {} | {:.2} | {:.2} |", + s.name, file_count, avg_ns, ops_per_sec + ); + + rows.push(ResultRow { + sample: s.name, + file_count, + avg_ns_per_op: avg_ns, + ops_per_sec, + }); + } + + if out_md.is_some() || out_json.is_some() { + let timestamp_utc = + time::OffsetDateTime::now_utc().format(&time::format_description::well_known::Rfc3339) + .unwrap_or_else(|_| "unknown".to_string()); + let cfg = RunConfig { + dir: dir.display().to_string(), + sample: sample_filter, + fixed_iters, + warmup_iters, + rounds, + min_round_ms, + max_adaptive_iters, + timestamp_utc, + }; + + if let Some(path) = out_md { + let md = render_markdown(&cfg, &rows); + write_text_file(&path, &md); + eprintln!("Wrote {}", path.display()); + } + if let Some(path) = out_json { + let json = serde_json::to_string_pretty(&BenchmarkOutput { config: cfg, rows }) + .expect("serialize json"); + write_text_file(&path, &json); + eprintln!("Wrote {}", path.display()); + } + } +} + +fn choose_iters_adaptive(bytes: &[u8], min_round_ms: u64, max_iters: u64) -> u64 { + let min_secs = (min_round_ms as f64) / 1e3; + let mut iters: u64 = 1; + loop { + let start = Instant::now(); + for _ in 0..iters { + let input = std::hint::black_box(bytes); + let decoded = ManifestObject::decode_der(input).expect("decode"); + std::hint::black_box(decoded); + } + let elapsed = start.elapsed().as_secs_f64(); + if elapsed >= min_secs { + return iters; + } + if iters >= max_iters { + return iters; + } + iters = (iters.saturating_mul(2)).min(max_iters); + } +} + +fn render_markdown(cfg: &RunConfig, rows: &[ResultRow]) -> String { + let mut out = String::new(); + out.push_str("# Manifest decode + profile validate benchmark (debug build)\n\n"); + out.push_str(&format!("- timestamp_utc: {}\n", cfg.timestamp_utc)); + out.push_str(&format!("- dir: `{}`\n", cfg.dir)); + if let Some(s) = cfg.sample.as_deref() { + out.push_str(&format!("- sample: `{}`\n", s)); + } + if let Some(n) = cfg.fixed_iters { + out.push_str(&format!("- iters: {} (fixed)\n", n)); + } else { + out.push_str(&format!( + "- warmup: {} iters, rounds: {}, min_round: {}ms (adaptive iters, max {})\n", + cfg.warmup_iters, cfg.rounds, cfg.min_round_ms, cfg.max_adaptive_iters + )); + } + out.push('\n'); + out.push_str("| sample | file_count | avg ns/op | ops/s |\n"); + out.push_str("|---|---:|---:|---:|\n"); + for r in rows { + out.push_str(&format!( + "| {} | {} | {:.2} | {:.2} |\n", + escape_md(&r.sample), + r.file_count, + r.avg_ns_per_op, + r.ops_per_sec + )); + } + out +} + +fn write_text_file(path: &Path, content: &str) { + create_parent_dirs(path); + std::fs::write(path, content).unwrap_or_else(|e| panic!("write {}: {e}", path.display())); +} + +#[derive(Clone, Debug, serde::Serialize)] +struct RunConfig { + dir: String, + sample: Option, + fixed_iters: Option, + warmup_iters: u64, + rounds: u64, + min_round_ms: u64, + max_adaptive_iters: u64, + timestamp_utc: String, +} + +#[derive(Clone, Debug, serde::Serialize)] +struct ResultRow { + sample: String, + file_count: usize, + avg_ns_per_op: f64, + ops_per_sec: f64, +} + +#[derive(Clone, Debug, serde::Serialize)] +struct BenchmarkOutput { + config: RunConfig, + rows: Vec, +} diff --git a/tests/benchmark/flamegraph_manifest_decode_profile.sh b/tests/benchmark/flamegraph_manifest_decode_profile.sh new file mode 100755 index 0000000..f03b77c --- /dev/null +++ b/tests/benchmark/flamegraph_manifest_decode_profile.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generates a flamegraph SVG for Manifest decode+profile validate. +# +# Prereqs: +# - `perf` installed and usable by current user +# - `cargo flamegraph` installed (`cargo install flamegraph`) +# +# Usage examples: +# BENCH_SAMPLE=large-02 ./tests/benchmark/flamegraph_manifest_decode_profile.sh +# BENCH_SAMPLE=large-02 BENCH_ITERS=5000 FLAMEGRAPH_PROFILE=dev ./tests/benchmark/flamegraph_manifest_decode_profile.sh +# BENCH_SAMPLE=large-02 BENCH_ITERS=2000 FLAMEGRAPH_PROFILE=release ./tests/benchmark/flamegraph_manifest_decode_profile.sh + +PROFILE="${FLAMEGRAPH_PROFILE:-dev}" # dev|release +SAMPLE="${BENCH_SAMPLE:-large-02}" + +ITERS="${BENCH_ITERS:-2000}" +FREQ="${FLAMEGRAPH_FREQ:-99}" +WARMUP="${BENCH_WARMUP_ITERS:-1}" +ROUNDS="${BENCH_ROUNDS:-1}" +MIN_ROUND_MS="${BENCH_MIN_ROUND_MS:-1}" +MAX_ITERS="${BENCH_MAX_ITERS:-100000}" + +if ! command -v perf >/dev/null 2>&1; then + echo "ERROR: perf not found. Install linux-tools/perf first (may require sudo)." >&2 + exit 2 +fi + +if ! command -v cargo-flamegraph >/dev/null 2>&1 && ! command -v flamegraph >/dev/null 2>&1; then + # cargo installs as `cargo flamegraph`, but the binary is `cargo-flamegraph`. + if ! command -v cargo-flamegraph >/dev/null 2>&1; then + echo "ERROR: cargo-flamegraph not found. Install with: cargo install flamegraph" >&2 + exit 2 + fi +fi + +OUT="target/bench/flamegraph_mft_decode_profile_${SAMPLE}_${PROFILE}.svg" + +echo "profile=${PROFILE} sample=${SAMPLE} iters=${ITERS} freq=${FREQ} out=${OUT}" + +FLAGS=() +if [[ "${PROFILE}" == "release" ]]; then + FLAGS+=(--release) +else + FLAGS+=(--dev) +fi + +# On WSL2, /usr/bin/perf is often a wrapper that exits 2 because there is no +# kernel-matched perf binary. In that case, prefer a real perf binary under +# /usr/lib/linux-tools/*/perf by putting a shim earlier in PATH. +PERF_WRAPPER_OK=1 +PERF_VERSION_OUT="$(perf --version 2>&1 || true)" +if echo "${PERF_VERSION_OUT}" | grep -q "WARNING: perf not found for kernel"; then + PERF_WRAPPER_OK=0 +fi + +if [[ "${PERF_WRAPPER_OK}" == "0" ]]; then + PERF_REAL="$(ls -1 /usr/lib/linux-tools/*/perf 2>/dev/null | head -n 1 || true)" + if [[ -z "${PERF_REAL}" ]]; then + echo "ERROR: perf wrapper found, but no real perf binary under /usr/lib/linux-tools/*/perf" >&2 + exit 2 + fi + + SHIM_DIR="target/bench/tools" + mkdir -p "${SHIM_DIR}" + cat > "${SHIM_DIR}/perf" < ${PERF_REAL}" +fi + +BENCH_SAMPLE="${SAMPLE}" \ +BENCH_ITERS="${ITERS}" \ +BENCH_WARMUP_ITERS="${WARMUP}" \ +BENCH_ROUNDS="${ROUNDS}" \ +BENCH_MIN_ROUND_MS="${MIN_ROUND_MS}" \ +BENCH_MAX_ITERS="${MAX_ITERS}" \ +cargo flamegraph "${FLAGS[@]}" \ + -F "${FREQ}" \ + --output "${OUT}" \ + --test bench_manifest_decode_profile -- --ignored --nocapture + +echo "Wrote ${OUT}" diff --git a/tests/benchmark/selected_der/large-01.mft b/tests/benchmark/selected_der/large-01.mft new file mode 100644 index 0000000..a433200 Binary files /dev/null and b/tests/benchmark/selected_der/large-01.mft differ diff --git a/tests/benchmark/selected_der/large-02.mft b/tests/benchmark/selected_der/large-02.mft new file mode 100644 index 0000000..4126399 Binary files /dev/null and b/tests/benchmark/selected_der/large-02.mft differ diff --git a/tests/benchmark/selected_der/medium-01.mft b/tests/benchmark/selected_der/medium-01.mft new file mode 100644 index 0000000..ab6cdcf Binary files /dev/null and b/tests/benchmark/selected_der/medium-01.mft differ diff --git a/tests/benchmark/selected_der/medium-02.mft b/tests/benchmark/selected_der/medium-02.mft new file mode 100644 index 0000000..1b13e46 Binary files /dev/null and b/tests/benchmark/selected_der/medium-02.mft differ diff --git a/tests/benchmark/selected_der/small-01.mft b/tests/benchmark/selected_der/small-01.mft new file mode 100644 index 0000000..f0741e1 Binary files /dev/null and b/tests/benchmark/selected_der/small-01.mft differ diff --git a/tests/benchmark/selected_der/small-02.mft b/tests/benchmark/selected_der/small-02.mft new file mode 100644 index 0000000..7db285d Binary files /dev/null and b/tests/benchmark/selected_der/small-02.mft differ diff --git a/tests/benchmark/selected_der/sources.tsv b/tests/benchmark/selected_der/sources.tsv new file mode 100644 index 0000000..c0bdb7f --- /dev/null +++ b/tests/benchmark/selected_der/sources.tsv @@ -0,0 +1,9 @@ +name input_path der_path input_size der_size source_format +large-01 benchmark/fixtures/selected/large-01.mft benchmark/fixtures/selected_der/large-01.mft 262869 2250 stored_point_container +large-02 benchmark/fixtures/selected/large-02.mft benchmark/fixtures/selected_der/large-02.mft 264331 10476 stored_point_container +medium-01 benchmark/fixtures/selected/medium-01.mft benchmark/fixtures/selected_der/medium-01.mft 16384 2681 stored_point_container +medium-02 benchmark/fixtures/selected/medium-02.mft benchmark/fixtures/selected_der/medium-02.mft 16384 2681 stored_point_container +small-01 benchmark/fixtures/selected/small-01.mft benchmark/fixtures/selected_der/small-01.mft 3160 1875 stored_point_container +small-02 benchmark/fixtures/selected/small-02.mft benchmark/fixtures/selected_der/small-02.mft 3160 1875 stored_point_container +xlarge-01 benchmark/fixtures/selected/xlarge-01.mft benchmark/fixtures/selected_der/xlarge-01.mft 4522145 144968 stored_point_container +xlarge-02 benchmark/fixtures/selected/xlarge-02.mft benchmark/fixtures/selected_der/xlarge-02.mft 4615094 150261 stored_point_container diff --git a/tests/benchmark/selected_der/xlarge-01.mft b/tests/benchmark/selected_der/xlarge-01.mft new file mode 100644 index 0000000..402cdc3 Binary files /dev/null and b/tests/benchmark/selected_der/xlarge-01.mft differ diff --git a/tests/benchmark/selected_der/xlarge-02.mft b/tests/benchmark/selected_der/xlarge-02.mft new file mode 100644 index 0000000..499b50f Binary files /dev/null and b/tests/benchmark/selected_der/xlarge-02.mft differ diff --git a/tests/test_fetch_cache_pp_revalidation_m3.rs b/tests/test_fetch_cache_pp_revalidation_m3.rs index 3d1a90a..8a5fb7c 100644 --- a/tests/test_fetch_cache_pp_revalidation_m3.rs +++ b/tests/test_fetch_cache_pp_revalidation_m3.rs @@ -59,8 +59,15 @@ fn store_raw_publication_point_files( store .put_raw(manifest_rsync_uri, manifest_bytes) .expect("store manifest raw"); - for entry in &manifest.manifest.files { - let file_path = manifest_path.parent().unwrap().join(&entry.file_name); + let entries = manifest + .manifest + .parse_files() + .expect("parse validated manifest fileList"); + for entry in &entries { + let file_path = manifest_path + .parent() + .unwrap() + .join(entry.file_name.as_str()); let bytes = std::fs::read(&file_path) .unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}")); let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name); diff --git a/tests/test_manifest_cache_errors_more.rs b/tests/test_manifest_cache_errors_more.rs index a22960a..43486a5 100644 --- a/tests/test_manifest_cache_errors_more.rs +++ b/tests/test_manifest_cache_errors_more.rs @@ -80,8 +80,15 @@ fn cache_pack_publication_point_mismatch_is_rejected() { store .put_raw(&manifest_rsync_uri, &manifest_bytes) .expect("store manifest"); - for entry in &manifest.manifest.files { - let file_path = manifest_path.parent().unwrap().join(&entry.file_name); + let entries = manifest + .manifest + .parse_files() + .expect("parse validated manifest fileList"); + for entry in &entries { + let file_path = manifest_path + .parent() + .unwrap() + .join(entry.file_name.as_str()); let bytes = std::fs::read(&file_path) .unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}")); let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name); diff --git a/tests/test_manifest_decode.rs b/tests/test_manifest_decode.rs index 5a8b531..463fb49 100644 --- a/tests/test_manifest_decode.rs +++ b/tests/test_manifest_decode.rs @@ -18,13 +18,16 @@ fn decode_manifest_fixture_smoke() { rpki::data_model::oid::OID_SHA256 ); assert!(mft.manifest.next_update > mft.manifest.this_update); - assert!(!mft.manifest.files.is_empty()); + assert!(mft.manifest.file_count() > 0); // The manifest file MUST NOT be listed in its own fileList. + let entries = mft + .manifest + .parse_files() + .expect("parse validated manifest fileList"); assert!( - mft.manifest - .files + entries .iter() - .all(|f| !f.file_name.to_ascii_lowercase().ends_with(".mft")) + .all(|f| !f.file_name.as_str().to_ascii_lowercase().ends_with(".mft")) ); } diff --git a/tests/test_manifest_processor_m4.rs b/tests/test_manifest_processor_m4.rs index fb06d15..bd5e500 100644 --- a/tests/test_manifest_processor_m4.rs +++ b/tests/test_manifest_processor_m4.rs @@ -57,8 +57,15 @@ fn manifest_success_writes_fetch_cache_pp_pack() { store .put_raw(&manifest_rsync_uri, &manifest_bytes) .expect("store manifest"); - for entry in &manifest.manifest.files { - let file_path = manifest_path.parent().unwrap().join(&entry.file_name); + let entries = manifest + .manifest + .parse_files() + .expect("parse validated manifest fileList"); + for entry in &entries { + let file_path = manifest_path + .parent() + .unwrap() + .join(entry.file_name.as_str()); let bytes = std::fs::read(&file_path) .unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}")); let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name); @@ -111,8 +118,15 @@ fn manifest_hash_mismatch_falls_back_to_fetch_cache_pp_when_enabled() { store .put_raw(&manifest_rsync_uri, &manifest_bytes) .expect("store manifest"); - for entry in &manifest.manifest.files { - let file_path = manifest_path.parent().unwrap().join(&entry.file_name); + let entries = manifest + .manifest + .parse_files() + .expect("parse validated manifest fileList"); + for entry in &entries { + let file_path = manifest_path + .parent() + .unwrap() + .join(entry.file_name.as_str()); let bytes = std::fs::read(&file_path) .unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}")); let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name); @@ -140,11 +154,11 @@ fn manifest_hash_mismatch_falls_back_to_fetch_cache_pp_when_enabled() { .expect("fetch_cache_pp pack exists"); let cached_pack = FetchCachePpPack::decode(&cached_bytes).expect("decode cached"); - let victim = manifest + let entries = manifest .manifest - .files - .first() - .expect("non-empty file list"); + .parse_files() + .expect("parse validated manifest fileList"); + let victim = entries.first().expect("non-empty file list"); let victim_uri = format!("{publication_point_rsync_uri}{}", victim.file_name); let mut tampered = store .get_raw(&victim_uri) @@ -186,8 +200,15 @@ fn manifest_failed_fetch_stop_all_output() { store .put_raw(&manifest_rsync_uri, &manifest_bytes) .expect("store manifest"); - for entry in &manifest.manifest.files { - let file_path = manifest_path.parent().unwrap().join(&entry.file_name); + let entries = manifest + .manifest + .parse_files() + .expect("parse validated manifest fileList"); + for entry in &entries { + let file_path = manifest_path + .parent() + .unwrap() + .join(entry.file_name.as_str()); let bytes = std::fs::read(&file_path) .unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}")); let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name); @@ -208,11 +229,11 @@ fn manifest_failed_fetch_stop_all_output() { ) .expect("first run stores fetch_cache_pp pack"); - let victim = manifest + let entries = manifest .manifest - .files - .first() - .expect("non-empty file list"); + .parse_files() + .expect("parse validated manifest fileList"); + let victim = entries.first().expect("non-empty file list"); let victim_uri = format!("{publication_point_rsync_uri}{}", victim.file_name); let mut tampered = store .get_raw(&victim_uri) @@ -255,8 +276,15 @@ fn manifest_fallback_pack_is_revalidated_and_rejected_if_stale() { store .put_raw(&manifest_rsync_uri, &manifest_bytes) .expect("store manifest"); - for entry in &manifest.manifest.files { - let file_path = manifest_path.parent().unwrap().join(&entry.file_name); + let entries = manifest + .manifest + .parse_files() + .expect("parse validated manifest fileList"); + for entry in &entries { + let file_path = manifest_path + .parent() + .unwrap() + .join(entry.file_name.as_str()); let bytes = std::fs::read(&file_path) .unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}")); let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name); @@ -314,8 +342,15 @@ fn manifest_replay_is_treated_as_failed_fetch_and_uses_fetch_cache_pp() { store .put_raw(&manifest_rsync_uri, &manifest_bytes) .expect("store manifest"); - for entry in &manifest.manifest.files { - let file_path = manifest_path.parent().unwrap().join(&entry.file_name); + let entries = manifest + .manifest + .parse_files() + .expect("parse validated manifest fileList"); + for entry in &entries { + let file_path = manifest_path + .parent() + .unwrap() + .join(entry.file_name.as_str()); let bytes = std::fs::read(&file_path) .unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}")); let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name); diff --git a/tests/test_manifest_rfc9286_section6_1.rs b/tests/test_manifest_rfc9286_section6_1.rs index 9bbaefc..819d95a 100644 --- a/tests/test_manifest_rfc9286_section6_1.rs +++ b/tests/test_manifest_rfc9286_section6_1.rs @@ -41,8 +41,12 @@ fn manifest_outside_publication_point_is_failed_fetch_rfc9286_section6_1() { // Store all referenced files under the (different) publication point so that §6.4/§6.5 // would otherwise succeed if §6.1 was not enforced. - for entry in &manifest.manifest.files { - let file_path = fixture_dir.join(&entry.file_name); + let entries = manifest + .manifest + .parse_files() + .expect("parse validated manifest fileList"); + for entry in &entries { + let file_path = fixture_dir.join(entry.file_name.as_str()); let bytes = std::fs::read(&file_path) .unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}")); let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name); diff --git a/tests/test_model_print_real_fixtures.rs b/tests/test_model_print_real_fixtures.rs index 8d22384..ca19e3e 100644 --- a/tests/test_model_print_real_fixtures.rs +++ b/tests/test_model_print_real_fixtures.rs @@ -319,13 +319,16 @@ struct ManifestEContentPretty { impl From<&ManifestEContent> for ManifestEContentPretty { fn from(v: &ManifestEContent) -> Self { + let entries = v + .parse_files() + .expect("parse validated manifest fileList"); Self { version: v.version, manifest_number: v.manifest_number.to_hex_upper(), this_update: v.this_update, next_update: v.next_update, file_hash_alg: v.file_hash_alg.clone(), - files: v.files.iter().map(FileAndHashPretty::from).collect(), + files: entries.iter().map(FileAndHashPretty::from).collect(), } } } @@ -339,8 +342,8 @@ struct FileAndHashPretty { impl From<&FileAndHash> for FileAndHashPretty { fn from(v: &FileAndHash) -> Self { Self { - file_name: v.file_name.clone(), - hash_hex: hex::encode(&v.hash_bytes), + file_name: v.file_name.as_str().to_string(), + hash_hex: hex::encode(v.hash_bytes.as_ref()), } } } diff --git a/tests/test_objects_errors_more.rs b/tests/test_objects_errors_more.rs index 73bfe87..c5aec50 100644 --- a/tests/test_objects_errors_more.rs +++ b/tests/test_objects_errors_more.rs @@ -51,8 +51,15 @@ fn build_cernet_pack_and_validation_time() -> ( store .put_raw(&manifest_rsync_uri, &manifest_bytes) .expect("store manifest"); - for entry in &manifest.manifest.files { - let file_path = manifest_path.parent().unwrap().join(&entry.file_name); + let entries = manifest + .manifest + .parse_files() + .expect("parse validated manifest fileList"); + for entry in &entries { + let file_path = manifest_path + .parent() + .unwrap() + .join(entry.file_name.as_str()); let bytes = std::fs::read(&file_path) .unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}")); let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name); diff --git a/tests/test_objects_policy_m8.rs b/tests/test_objects_policy_m8.rs index 3128611..5b34548 100644 --- a/tests/test_objects_policy_m8.rs +++ b/tests/test_objects_policy_m8.rs @@ -51,8 +51,15 @@ fn build_cernet_pack_and_validation_time() -> ( store .put_raw(&manifest_rsync_uri, &manifest_bytes) .expect("store manifest"); - for entry in &manifest.manifest.files { - let file_path = manifest_path.parent().unwrap().join(&entry.file_name); + let entries = manifest + .manifest + .parse_files() + .expect("parse validated manifest fileList"); + for entry in &entries { + let file_path = manifest_path + .parent() + .unwrap() + .join(entry.file_name.as_str()); let bytes = std::fs::read(&file_path) .unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}")); let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name); diff --git a/tests/test_rpki_bin_coverage.rs b/tests/test_rpki_bin_coverage.rs new file mode 100644 index 0000000..829e35f --- /dev/null +++ b/tests/test_rpki_bin_coverage.rs @@ -0,0 +1,33 @@ +use std::process::Command; + +#[test] +fn rpki_bin_help_exits_success_and_prints_usage() { + // This also increases coverage for `src/bin/rpki.rs` because it executes the binary. + let exe = env!("CARGO_BIN_EXE_rpki"); + let out = Command::new(exe) + .arg("--help") + .output() + .expect("run rpki --help"); + + assert!(out.status.success(), "status={}", out.status); + let stdout = String::from_utf8_lossy(&out.stdout); + // `cli::usage()` contains "Usage:". + assert!( + stdout.contains("Usage:") || stdout.contains("USAGE:"), + "stdout={stdout}" + ); +} + +#[test] +fn rpki_bin_without_args_exits_2_and_prints_error() { + let exe = env!("CARGO_BIN_EXE_rpki"); + let out = Command::new(exe).output().expect("run rpki"); + + assert_eq!(out.status.code(), Some(2), "status={}", out.status); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + !stderr.trim().is_empty(), + "expected non-empty stderr, got empty" + ); +} +