Skip to content

Commit 66231c6

Browse files
committed
TC-3071 Add support for the xz compression format.
1 parent aa4ab29 commit 66231c6

File tree

9 files changed

+91
-1676
lines changed

9 files changed

+91
-1676
lines changed

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ tokio = { version = "1.43.1", features = ["sync"] }
2424
urlencoding = "2"
2525
packageurl = "0.4.2"
2626
rand = "0.9.2"
27+
xz2 = "0.1"
2728

2829
[features]
2930
default = ["postgres"]

scenarios/full-20250604.json5

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,5 +114,5 @@
114114
],
115115
"download_advisory": "24ae57c3-4b57-4f4e-82c1-83ae26059a89",
116116
"get_advisory": "24ae57c3-4b57-4f4e-82c1-83ae26059a89",
117-
"upload_advisory_files": "./test-data/advisories/rhba-2024_11505.json"
117+
"upload_advisory_files": "./test-data/advisories/rhba-2024_11505.json.xz"
118118
}

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ async fn main() -> Result<(), anyhow::Error> {
242242
.set_weight(1)?;
243243
if let Some(file) = &scenario.upload_advisory_files {
244244
// Use sequential transaction execution to ensure immediate deletion after upload
245-
s = s.register_transaction(tx!(upload_and_immediately_delete(file.clone()),name: format!("upload_and_immediately_delete")));
245+
s = s.register_transaction(tx!(upload_and_immediately_delete(file.clone()),name: "upload_and_immediately_delete"));
246246
}
247247
s
248248
})

src/restapi.rs

Lines changed: 66 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@ use crate::utils::DisplayVec;
22
use goose::goose::{GooseUser, TransactionError, TransactionResult};
33
use rand::Rng;
44
use serde_json::json;
5-
use std::sync::{
6-
Arc,
7-
atomic::{AtomicUsize, Ordering},
5+
use std::{
6+
collections::HashMap,
7+
io::Read,
8+
sync::{
9+
Arc,
10+
atomic::{AtomicUsize, Ordering},
11+
},
812
};
9-
use tokio::sync::OnceCell;
13+
use tokio::sync::{OnceCell, RwLock};
1014
use urlencoding::encode;
15+
use xz2::read::XzDecoder;
1116

12-
/// Cache for base advisory content with file path
13-
static BASE_ADVISORY_CONTENT: OnceCell<(String, Vec<u8>)> = OnceCell::const_new();
17+
/// Thread-safe cache for multiple advisory files using async-aware RwLock
18+
static FILE_CACHE: OnceCell<RwLock<HashMap<String, Vec<u8>>>> = OnceCell::const_new();
1419

1520
/// Generate a new advisory with modified ID
1621
fn generate_advisory_content(base_content: &[u8]) -> Result<Vec<u8>, Box<TransactionError>> {
@@ -42,32 +47,65 @@ fn generate_advisory_content(base_content: &[u8]) -> Result<Vec<u8>, Box<Transac
4247
})
4348
}
4449

45-
/// Get base advisory content from file path - cached and initialized on first access
46-
async fn get_base_advisory_content(file_path: &str) -> Result<&'static Vec<u8>, String> {
47-
let result = BASE_ADVISORY_CONTENT
48-
.get_or_init(|| async {
49-
match tokio::fs::read(file_path).await {
50-
Ok(content) => (file_path.to_string(), content),
51-
Err(e) => {
52-
panic!("Failed to load base advisory file {}: {}", file_path, e);
53-
}
54-
}
55-
})
50+
/// Get base upload content from file path - cached and initialized on first access
51+
async fn get_base_upload_content(file_path: &str) -> Result<Vec<u8>, String> {
52+
// Initialize cache if not already done
53+
let cache = FILE_CACHE
54+
.get_or_init(|| async { RwLock::new(HashMap::new()) })
5655
.await;
5756

58-
let (cached_path, content) = result;
57+
// Check if the file is already cached
58+
{
59+
let cache_read = cache.read().await;
60+
if let Some(content) = cache_read.get(file_path) {
61+
return Ok(content.clone());
62+
}
63+
}
64+
65+
// Load the file content
66+
let content = read_upload_file(file_path)
67+
.await
68+
.map_err(|e| format!("Failed to load base advisory file {}: {}", file_path, e))?;
5969

60-
// Verify that the cached path matches the requested path
61-
if cached_path != file_path {
62-
return Err(format!(
63-
"Base advisory file path mismatch: expected {}, got {}",
64-
file_path, cached_path
65-
));
70+
// Insert into cache
71+
{
72+
let mut cache_write = cache.write().await;
73+
cache_write.insert(file_path.to_string(), content.clone());
6674
}
6775

6876
Ok(content)
6977
}
7078

79+
/// Read upload file, handling both regular JSON and XZ compressed files
80+
async fn read_upload_file(file_path: &str) -> Result<Vec<u8>, String> {
81+
let file_bytes = tokio::fs::read(file_path)
82+
.await
83+
.map_err(|e| format!("Failed to read file {}: {}", file_path, e))?;
84+
85+
// Check if file is XZ compressed by looking at the magic bytes
86+
if file_path.ends_with(".xz") || is_xz_compressed(&file_bytes) {
87+
decompress_xz(&file_bytes)
88+
} else {
89+
Ok(file_bytes)
90+
}
91+
}
92+
93+
/// Check if data is XZ compressed by examining magic bytes
94+
fn is_xz_compressed(data: &[u8]) -> bool {
95+
data.len() >= 6 && data[0..6] == [0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]
96+
}
97+
98+
/// Decompress XZ compressed data
99+
fn decompress_xz(compressed_data: &[u8]) -> Result<Vec<u8>, String> {
100+
let mut decoder = XzDecoder::new(compressed_data);
101+
let mut decompressed = Vec::new();
102+
103+
decoder
104+
.read_to_end(&mut decompressed)
105+
.map_err(|e| format!("Failed to decompress XZ data: {}", e))?;
106+
Ok(decompressed)
107+
}
108+
71109
/// Upload advisory data and extract advisory ID from response
72110
async fn upload_advisory_and_extract_id(
73111
file_bytes: Vec<u8>,
@@ -120,16 +158,16 @@ pub async fn list_advisory_labels(user: &mut GooseUser) -> TransactionResult {
120158

121159
/// Upload file and return advisory ID
122160
pub async fn upload_advisory_and_get_id(
123-
advisory_file: String,
161+
advisory_file: &str,
124162
user: &mut GooseUser,
125163
) -> Result<String, Box<TransactionError>> {
126164
// Check cache first - only cache the base template
127-
let base_content = get_base_advisory_content(&advisory_file)
165+
let base_content = get_base_upload_content(advisory_file)
128166
.await
129167
.map_err(|e| Box::new(TransactionError::Custom(e)))?;
130168

131169
// Generate new content with modified ID
132-
let generated_content = generate_advisory_content(base_content)?;
170+
let generated_content = generate_advisory_content(&base_content)?;
133171

134172
// Upload the generated content and extract advisory ID
135173
upload_advisory_and_extract_id(generated_content, user).await
@@ -148,7 +186,7 @@ pub async fn upload_and_immediately_delete(
148186
user: &mut GooseUser,
149187
) -> TransactionResult {
150188
// 1. Upload file and get ID
151-
let advisory_id = upload_advisory_and_get_id(advisory_file.clone(), user).await?;
189+
let advisory_id = upload_advisory_and_get_id(&advisory_file, user).await?;
152190

153191
// 2. Immediately delete (no waiting required)
154192
delete_advisory_by_id(advisory_id, user).await?;

src/scenario/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ FROM public.advisory order by modified desc limit 1;"#,
409409
let files: Vec<PathBuf> = entries
410410
.filter_map(|entry| {
411411
let path = entry.ok()?.path();
412-
if path.extension()? == "json" {
412+
if path.extension()? == "xz" {
413413
Some(path)
414414
} else {
415415
None

test-data/advisories/.DS_Store

-6 KB
Binary file not shown.

0 commit comments

Comments
 (0)