exarch-rs 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +1 -0
- package/README.md +1 -1
- package/biome.json +47 -0
- package/exarch-rs.darwin-arm64.node +0 -0
- package/index.d.ts +551 -181
- package/index.js +588 -0
- package/package.json +25 -5
- package/src/config.rs +303 -47
- package/src/error.rs +42 -0
- package/src/lib.rs +553 -14
- package/src/report.rs +538 -0
- package/tests/create.test.js +124 -0
- package/tests/creation-config.test.js +97 -0
- package/tests/extract.test.js +118 -0
- package/tests/list-verify.test.js +148 -0
- package/tests/security-config.test.js +187 -0
package/src/report.rs
CHANGED
|
@@ -42,6 +42,201 @@ impl From<CoreReport> for ExtractionReport {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/// Report of an archive creation operation.
|
|
46
|
+
///
|
|
47
|
+
/// Contains statistics and metadata about the creation process.
|
|
48
|
+
#[napi(object)]
|
|
49
|
+
#[derive(Debug, Clone)]
|
|
50
|
+
pub struct CreationReport {
|
|
51
|
+
/// Number of files added to the archive.
|
|
52
|
+
pub files_added: u32,
|
|
53
|
+
/// Number of directories added to the archive.
|
|
54
|
+
pub directories_added: u32,
|
|
55
|
+
/// Number of symlinks added to the archive.
|
|
56
|
+
pub symlinks_added: u32,
|
|
57
|
+
/// Total bytes written to the archive (uncompressed).
|
|
58
|
+
pub bytes_written: i64,
|
|
59
|
+
/// Total bytes in the final archive (compressed).
|
|
60
|
+
pub bytes_compressed: i64,
|
|
61
|
+
/// Duration of the creation operation in milliseconds.
|
|
62
|
+
pub duration_ms: i64,
|
|
63
|
+
/// Number of files skipped (due to filters or errors).
|
|
64
|
+
pub files_skipped: u32,
|
|
65
|
+
/// Warnings generated during creation.
|
|
66
|
+
pub warnings: Vec<String>,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
impl From<exarch_core::creation::CreationReport> for CreationReport {
|
|
70
|
+
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
|
71
|
+
fn from(report: exarch_core::creation::CreationReport) -> Self {
|
|
72
|
+
Self {
|
|
73
|
+
files_added: report.files_added.min(u32::MAX as usize) as u32,
|
|
74
|
+
directories_added: report.directories_added.min(u32::MAX as usize) as u32,
|
|
75
|
+
symlinks_added: report.symlinks_added.min(u32::MAX as usize) as u32,
|
|
76
|
+
bytes_written: report.bytes_written.min(i64::MAX as u64) as i64,
|
|
77
|
+
bytes_compressed: report.bytes_compressed.min(i64::MAX as u64) as i64,
|
|
78
|
+
duration_ms: report.duration.as_millis().min(i64::MAX as u128) as i64,
|
|
79
|
+
files_skipped: report.files_skipped.min(u32::MAX as usize) as u32,
|
|
80
|
+
warnings: report.warnings,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Complete manifest of archive contents.
|
|
86
|
+
///
|
|
87
|
+
/// Generated by `listArchive()`, contains metadata about all entries
|
|
88
|
+
/// without extracting them to disk.
|
|
89
|
+
#[napi(object)]
|
|
90
|
+
#[derive(Debug, Clone)]
|
|
91
|
+
pub struct ArchiveManifest {
|
|
92
|
+
/// All entries in the archive (files, dirs, symlinks, hardlinks).
|
|
93
|
+
pub entries: Vec<ArchiveEntry>,
|
|
94
|
+
/// Total number of entries.
|
|
95
|
+
pub total_entries: u32,
|
|
96
|
+
/// Total uncompressed size in bytes.
|
|
97
|
+
pub total_size: i64,
|
|
98
|
+
/// Archive format (e.g., `TarGz`, `Zip`).
|
|
99
|
+
pub format: String,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
impl From<exarch_core::inspection::ArchiveManifest> for ArchiveManifest {
|
|
103
|
+
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
|
104
|
+
fn from(manifest: exarch_core::inspection::ArchiveManifest) -> Self {
|
|
105
|
+
Self {
|
|
106
|
+
entries: manifest
|
|
107
|
+
.entries
|
|
108
|
+
.into_iter()
|
|
109
|
+
.map(ArchiveEntry::from)
|
|
110
|
+
.collect(),
|
|
111
|
+
total_entries: manifest.total_entries.min(u32::MAX as usize) as u32,
|
|
112
|
+
total_size: manifest.total_size.min(i64::MAX as u64) as i64,
|
|
113
|
+
format: format!("{:?}", manifest.format),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Single entry in archive manifest.
|
|
119
|
+
///
|
|
120
|
+
/// Contains metadata about a file, directory, symlink, or hardlink
|
|
121
|
+
/// without extracting it to disk.
|
|
122
|
+
#[napi(object)]
|
|
123
|
+
#[derive(Debug, Clone)]
|
|
124
|
+
pub struct ArchiveEntry {
|
|
125
|
+
/// Entry path (relative, as stored in archive).
|
|
126
|
+
pub path: String,
|
|
127
|
+
/// Entry type ("File", "Directory", "Symlink", "Hardlink").
|
|
128
|
+
pub entry_type: String,
|
|
129
|
+
/// Uncompressed size in bytes (0 for directories).
|
|
130
|
+
pub size: i64,
|
|
131
|
+
/// Compressed size in bytes (if available, ZIP only).
|
|
132
|
+
pub compressed_size: Option<i64>,
|
|
133
|
+
/// File permissions (Unix mode).
|
|
134
|
+
pub mode: Option<u32>,
|
|
135
|
+
/// Modification time (milliseconds since Unix epoch).
|
|
136
|
+
pub modified: Option<i64>,
|
|
137
|
+
/// Symlink target (if `entry_type` is "Symlink").
|
|
138
|
+
pub symlink_target: Option<String>,
|
|
139
|
+
/// Hardlink target (if `entry_type` is "Hardlink").
|
|
140
|
+
pub hardlink_target: Option<String>,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
impl From<exarch_core::inspection::ArchiveEntry> for ArchiveEntry {
|
|
144
|
+
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
|
145
|
+
fn from(entry: exarch_core::inspection::ArchiveEntry) -> Self {
|
|
146
|
+
Self {
|
|
147
|
+
path: entry.path.to_string_lossy().into_owned(),
|
|
148
|
+
entry_type: entry.entry_type.to_string(),
|
|
149
|
+
size: entry.size.min(i64::MAX as u64) as i64,
|
|
150
|
+
compressed_size: entry.compressed_size.map(|s| s.min(i64::MAX as u64) as i64),
|
|
151
|
+
mode: entry.mode,
|
|
152
|
+
modified: entry.modified.and_then(|t| {
|
|
153
|
+
t.duration_since(std::time::UNIX_EPOCH)
|
|
154
|
+
.ok()
|
|
155
|
+
.map(|d| d.as_millis().min(i64::MAX as u128) as i64)
|
|
156
|
+
}),
|
|
157
|
+
symlink_target: entry
|
|
158
|
+
.symlink_target
|
|
159
|
+
.map(|p| p.to_string_lossy().into_owned()),
|
|
160
|
+
hardlink_target: entry
|
|
161
|
+
.hardlink_target
|
|
162
|
+
.map(|p| p.to_string_lossy().into_owned()),
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// Result of archive verification.
|
|
168
|
+
///
|
|
169
|
+
/// Generated by `verifyArchive()`, contains security and integrity checks
|
|
170
|
+
/// performed without extracting files to disk.
|
|
171
|
+
#[napi(object)]
|
|
172
|
+
#[derive(Debug, Clone)]
|
|
173
|
+
pub struct VerificationReport {
|
|
174
|
+
/// Overall verification status ("Pass", "Fail", "Warning").
|
|
175
|
+
pub status: String,
|
|
176
|
+
/// Integrity check result ("Pass", "Fail", "Warning", "Skipped").
|
|
177
|
+
pub integrity_status: String,
|
|
178
|
+
/// Security check result ("Pass", "Fail", "Warning", "Skipped").
|
|
179
|
+
pub security_status: String,
|
|
180
|
+
/// List of all issues found (sorted by severity).
|
|
181
|
+
pub issues: Vec<VerificationIssue>,
|
|
182
|
+
/// Total entries scanned.
|
|
183
|
+
pub total_entries: u32,
|
|
184
|
+
/// Entries flagged as suspicious.
|
|
185
|
+
pub suspicious_entries: u32,
|
|
186
|
+
/// Total uncompressed size.
|
|
187
|
+
pub total_size: i64,
|
|
188
|
+
/// Archive format (e.g., `TarGz`, `Zip`).
|
|
189
|
+
pub format: String,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
impl From<exarch_core::inspection::VerificationReport> for VerificationReport {
|
|
193
|
+
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
|
194
|
+
fn from(report: exarch_core::inspection::VerificationReport) -> Self {
|
|
195
|
+
Self {
|
|
196
|
+
status: report.status.to_string(),
|
|
197
|
+
integrity_status: report.integrity_status.to_string(),
|
|
198
|
+
security_status: report.security_status.to_string(),
|
|
199
|
+
issues: report
|
|
200
|
+
.issues
|
|
201
|
+
.into_iter()
|
|
202
|
+
.map(VerificationIssue::from)
|
|
203
|
+
.collect(),
|
|
204
|
+
total_entries: report.total_entries.min(u32::MAX as usize) as u32,
|
|
205
|
+
suspicious_entries: report.suspicious_entries.min(u32::MAX as usize) as u32,
|
|
206
|
+
total_size: report.total_size.min(i64::MAX as u64) as i64,
|
|
207
|
+
format: format!("{:?}", report.format),
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/// Single verification issue.
|
|
213
|
+
#[napi(object)]
|
|
214
|
+
#[derive(Debug, Clone)]
|
|
215
|
+
pub struct VerificationIssue {
|
|
216
|
+
/// Issue severity level ("Critical", "High", "Medium", "Low", "Info").
|
|
217
|
+
pub severity: String,
|
|
218
|
+
/// Issue category (`PathTraversal`, `SymlinkEscape`, etc.).
|
|
219
|
+
pub category: String,
|
|
220
|
+
/// Entry path that triggered issue (if applicable).
|
|
221
|
+
pub entry_path: Option<String>,
|
|
222
|
+
/// Human-readable description.
|
|
223
|
+
pub message: String,
|
|
224
|
+
/// Optional context (compression ratio, target path, etc.).
|
|
225
|
+
pub context: Option<String>,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
impl From<exarch_core::inspection::VerificationIssue> for VerificationIssue {
|
|
229
|
+
fn from(issue: exarch_core::inspection::VerificationIssue) -> Self {
|
|
230
|
+
Self {
|
|
231
|
+
severity: issue.severity.to_string(),
|
|
232
|
+
category: issue.category.to_string(),
|
|
233
|
+
entry_path: issue.entry_path.map(|p| p.to_string_lossy().into_owned()),
|
|
234
|
+
message: issue.message,
|
|
235
|
+
context: issue.context,
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
45
240
|
#[cfg(test)]
|
|
46
241
|
#[allow(clippy::unwrap_used, clippy::expect_used)]
|
|
47
242
|
mod tests {
|
|
@@ -173,4 +368,347 @@ mod tests {
|
|
|
173
368
|
"third warning should be at index 2"
|
|
174
369
|
);
|
|
175
370
|
}
|
|
371
|
+
|
|
372
|
+
// CreationReport tests
|
|
373
|
+
#[test]
|
|
374
|
+
fn test_creation_report_conversion() {
|
|
375
|
+
let mut core_report = exarch_core::creation::CreationReport::new();
|
|
376
|
+
core_report.files_added = 10;
|
|
377
|
+
core_report.directories_added = 5;
|
|
378
|
+
core_report.symlinks_added = 2;
|
|
379
|
+
core_report.bytes_written = 1024;
|
|
380
|
+
core_report.bytes_compressed = 512;
|
|
381
|
+
core_report.duration = std::time::Duration::from_millis(500);
|
|
382
|
+
core_report.files_skipped = 1;
|
|
383
|
+
core_report.add_warning("Test warning".to_string());
|
|
384
|
+
|
|
385
|
+
let report = CreationReport::from(core_report);
|
|
386
|
+
|
|
387
|
+
assert_eq!(report.files_added, 10);
|
|
388
|
+
assert_eq!(report.directories_added, 5);
|
|
389
|
+
assert_eq!(report.symlinks_added, 2);
|
|
390
|
+
assert_eq!(report.bytes_written, 1024);
|
|
391
|
+
assert_eq!(report.bytes_compressed, 512);
|
|
392
|
+
assert_eq!(report.duration_ms, 500);
|
|
393
|
+
assert_eq!(report.files_skipped, 1);
|
|
394
|
+
assert_eq!(report.warnings.len(), 1);
|
|
395
|
+
assert_eq!(report.warnings[0], "Test warning");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ArchiveEntry tests
|
|
399
|
+
#[test]
|
|
400
|
+
fn test_archive_entry_conversion() {
|
|
401
|
+
use std::path::PathBuf;
|
|
402
|
+
use std::time::UNIX_EPOCH;
|
|
403
|
+
|
|
404
|
+
let core_entry = exarch_core::inspection::ArchiveEntry {
|
|
405
|
+
path: PathBuf::from("test/file.txt"),
|
|
406
|
+
entry_type: exarch_core::inspection::ManifestEntryType::File,
|
|
407
|
+
size: 1024,
|
|
408
|
+
compressed_size: Some(512),
|
|
409
|
+
mode: Some(0o644),
|
|
410
|
+
modified: Some(UNIX_EPOCH + std::time::Duration::from_secs(1000)),
|
|
411
|
+
symlink_target: None,
|
|
412
|
+
hardlink_target: None,
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
let entry = ArchiveEntry::from(core_entry);
|
|
416
|
+
|
|
417
|
+
assert_eq!(entry.path, "test/file.txt");
|
|
418
|
+
assert_eq!(entry.entry_type, "File");
|
|
419
|
+
assert_eq!(entry.size, 1024);
|
|
420
|
+
assert_eq!(entry.compressed_size, Some(512));
|
|
421
|
+
assert_eq!(entry.mode, Some(0o644));
|
|
422
|
+
assert_eq!(entry.modified, Some(1_000_000));
|
|
423
|
+
assert_eq!(entry.symlink_target, None);
|
|
424
|
+
assert_eq!(entry.hardlink_target, None);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ArchiveManifest tests
|
|
428
|
+
#[test]
|
|
429
|
+
fn test_archive_manifest_conversion() {
|
|
430
|
+
use exarch_core::formats::detect::ArchiveType;
|
|
431
|
+
|
|
432
|
+
let mut core_manifest = exarch_core::inspection::ArchiveManifest::new(ArchiveType::TarGz);
|
|
433
|
+
core_manifest.total_entries = 10;
|
|
434
|
+
core_manifest.total_size = 5000;
|
|
435
|
+
|
|
436
|
+
let manifest = ArchiveManifest::from(core_manifest);
|
|
437
|
+
|
|
438
|
+
assert_eq!(manifest.total_entries, 10);
|
|
439
|
+
assert_eq!(manifest.total_size, 5000);
|
|
440
|
+
assert!(manifest.format.contains("TarGz"));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// VerificationReport tests
|
|
444
|
+
#[test]
|
|
445
|
+
fn test_verification_report_conversion() {
|
|
446
|
+
use exarch_core::formats::detect::ArchiveType;
|
|
447
|
+
use exarch_core::inspection::CheckStatus;
|
|
448
|
+
use exarch_core::inspection::VerificationStatus;
|
|
449
|
+
|
|
450
|
+
let core_report = exarch_core::inspection::VerificationReport {
|
|
451
|
+
status: VerificationStatus::Pass,
|
|
452
|
+
integrity_status: CheckStatus::Pass,
|
|
453
|
+
security_status: CheckStatus::Pass,
|
|
454
|
+
issues: vec![],
|
|
455
|
+
total_entries: 10,
|
|
456
|
+
suspicious_entries: 0,
|
|
457
|
+
total_size: 5000,
|
|
458
|
+
format: ArchiveType::TarGz,
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
let report = VerificationReport::from(core_report);
|
|
462
|
+
|
|
463
|
+
assert_eq!(report.status, "PASS");
|
|
464
|
+
assert_eq!(report.integrity_status, "OK");
|
|
465
|
+
assert_eq!(report.security_status, "OK");
|
|
466
|
+
assert_eq!(report.total_entries, 10);
|
|
467
|
+
assert_eq!(report.suspicious_entries, 0);
|
|
468
|
+
assert_eq!(report.total_size, 5000);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// VerificationIssue tests
|
|
472
|
+
#[test]
|
|
473
|
+
fn test_verification_issue_conversion() {
|
|
474
|
+
use exarch_core::inspection::IssueCategory;
|
|
475
|
+
use exarch_core::inspection::IssueSeverity;
|
|
476
|
+
use std::path::PathBuf;
|
|
477
|
+
|
|
478
|
+
let core_issue = exarch_core::inspection::VerificationIssue {
|
|
479
|
+
severity: IssueSeverity::Critical,
|
|
480
|
+
category: IssueCategory::PathTraversal,
|
|
481
|
+
entry_path: Some(PathBuf::from("../etc/passwd")),
|
|
482
|
+
message: "Path traversal detected".to_string(),
|
|
483
|
+
context: Some("Attempt to escape directory".to_string()),
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
let issue = VerificationIssue::from(core_issue);
|
|
487
|
+
|
|
488
|
+
assert_eq!(issue.severity, "CRITICAL");
|
|
489
|
+
assert_eq!(issue.category, "Path Traversal");
|
|
490
|
+
assert_eq!(issue.entry_path, Some("../etc/passwd".to_string()));
|
|
491
|
+
assert_eq!(issue.message, "Path traversal detected");
|
|
492
|
+
assert_eq!(
|
|
493
|
+
issue.context,
|
|
494
|
+
Some("Attempt to escape directory".to_string())
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// CR-004: Overflow/saturating conversion tests
|
|
499
|
+
#[test]
|
|
500
|
+
fn test_extraction_report_saturates_files_extracted_at_u32_max() {
|
|
501
|
+
let mut core_report = CoreReport::new();
|
|
502
|
+
// Set to value > u32::MAX
|
|
503
|
+
core_report.files_extracted = (u32::MAX as usize) + 1000;
|
|
504
|
+
|
|
505
|
+
let report = ExtractionReport::from(core_report);
|
|
506
|
+
|
|
507
|
+
assert_eq!(
|
|
508
|
+
report.files_extracted,
|
|
509
|
+
u32::MAX,
|
|
510
|
+
"files_extracted should saturate to u32::MAX"
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
#[test]
|
|
515
|
+
fn test_extraction_report_saturates_bytes_written_at_i64_max() {
|
|
516
|
+
let mut core_report = CoreReport::new();
|
|
517
|
+
// Set to value > i64::MAX
|
|
518
|
+
core_report.bytes_written = u64::MAX;
|
|
519
|
+
|
|
520
|
+
let report = ExtractionReport::from(core_report);
|
|
521
|
+
|
|
522
|
+
assert_eq!(
|
|
523
|
+
report.bytes_written,
|
|
524
|
+
i64::MAX,
|
|
525
|
+
"bytes_written should saturate to i64::MAX"
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
#[test]
|
|
530
|
+
fn test_extraction_report_saturates_duration_ms_at_i64_max() {
|
|
531
|
+
let mut core_report = CoreReport::new();
|
|
532
|
+
// Create a very large duration that exceeds i64::MAX milliseconds
|
|
533
|
+
// i64::MAX ms is about 292 million years, so use u64::MAX seconds
|
|
534
|
+
core_report.duration = Duration::from_secs(u64::MAX);
|
|
535
|
+
|
|
536
|
+
let report = ExtractionReport::from(core_report);
|
|
537
|
+
|
|
538
|
+
assert_eq!(
|
|
539
|
+
report.duration_ms,
|
|
540
|
+
i64::MAX,
|
|
541
|
+
"duration_ms should saturate to i64::MAX"
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
#[test]
|
|
546
|
+
fn test_creation_report_saturates_files_added_at_u32_max() {
|
|
547
|
+
let mut core_report = exarch_core::creation::CreationReport::new();
|
|
548
|
+
core_report.files_added = (u32::MAX as usize) + 1000;
|
|
549
|
+
|
|
550
|
+
let report = CreationReport::from(core_report);
|
|
551
|
+
|
|
552
|
+
assert_eq!(
|
|
553
|
+
report.files_added,
|
|
554
|
+
u32::MAX,
|
|
555
|
+
"files_added should saturate to u32::MAX"
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
#[test]
|
|
560
|
+
fn test_creation_report_saturates_bytes_written_at_i64_max() {
|
|
561
|
+
let mut core_report = exarch_core::creation::CreationReport::new();
|
|
562
|
+
core_report.bytes_written = u64::MAX;
|
|
563
|
+
|
|
564
|
+
let report = CreationReport::from(core_report);
|
|
565
|
+
|
|
566
|
+
assert_eq!(
|
|
567
|
+
report.bytes_written,
|
|
568
|
+
i64::MAX,
|
|
569
|
+
"bytes_written should saturate to i64::MAX"
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
#[test]
|
|
574
|
+
fn test_creation_report_saturates_bytes_compressed_at_i64_max() {
|
|
575
|
+
let mut core_report = exarch_core::creation::CreationReport::new();
|
|
576
|
+
core_report.bytes_compressed = u64::MAX;
|
|
577
|
+
|
|
578
|
+
let report = CreationReport::from(core_report);
|
|
579
|
+
|
|
580
|
+
assert_eq!(
|
|
581
|
+
report.bytes_compressed,
|
|
582
|
+
i64::MAX,
|
|
583
|
+
"bytes_compressed should saturate to i64::MAX"
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
#[test]
|
|
588
|
+
fn test_archive_manifest_saturates_total_entries_at_u32_max() {
|
|
589
|
+
use exarch_core::formats::detect::ArchiveType;
|
|
590
|
+
|
|
591
|
+
let mut core_manifest = exarch_core::inspection::ArchiveManifest::new(ArchiveType::TarGz);
|
|
592
|
+
core_manifest.total_entries = (u32::MAX as usize) + 1000;
|
|
593
|
+
|
|
594
|
+
let manifest = ArchiveManifest::from(core_manifest);
|
|
595
|
+
|
|
596
|
+
assert_eq!(
|
|
597
|
+
manifest.total_entries,
|
|
598
|
+
u32::MAX,
|
|
599
|
+
"total_entries should saturate to u32::MAX"
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
#[test]
|
|
604
|
+
fn test_archive_manifest_saturates_total_size_at_i64_max() {
|
|
605
|
+
use exarch_core::formats::detect::ArchiveType;
|
|
606
|
+
|
|
607
|
+
let mut core_manifest = exarch_core::inspection::ArchiveManifest::new(ArchiveType::TarGz);
|
|
608
|
+
core_manifest.total_size = u64::MAX;
|
|
609
|
+
|
|
610
|
+
let manifest = ArchiveManifest::from(core_manifest);
|
|
611
|
+
|
|
612
|
+
assert_eq!(
|
|
613
|
+
manifest.total_size,
|
|
614
|
+
i64::MAX,
|
|
615
|
+
"total_size should saturate to i64::MAX"
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
#[test]
|
|
620
|
+
fn test_archive_entry_saturates_size_at_i64_max() {
|
|
621
|
+
use std::path::PathBuf;
|
|
622
|
+
|
|
623
|
+
let core_entry = exarch_core::inspection::ArchiveEntry {
|
|
624
|
+
path: PathBuf::from("test.txt"),
|
|
625
|
+
entry_type: exarch_core::inspection::ManifestEntryType::File,
|
|
626
|
+
size: u64::MAX,
|
|
627
|
+
compressed_size: None,
|
|
628
|
+
mode: None,
|
|
629
|
+
modified: None,
|
|
630
|
+
symlink_target: None,
|
|
631
|
+
hardlink_target: None,
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
let entry = ArchiveEntry::from(core_entry);
|
|
635
|
+
|
|
636
|
+
assert_eq!(entry.size, i64::MAX, "size should saturate to i64::MAX");
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
#[test]
|
|
640
|
+
fn test_archive_entry_saturates_compressed_size_at_i64_max() {
|
|
641
|
+
use std::path::PathBuf;
|
|
642
|
+
|
|
643
|
+
let core_entry = exarch_core::inspection::ArchiveEntry {
|
|
644
|
+
path: PathBuf::from("test.txt"),
|
|
645
|
+
entry_type: exarch_core::inspection::ManifestEntryType::File,
|
|
646
|
+
size: 1024,
|
|
647
|
+
compressed_size: Some(u64::MAX),
|
|
648
|
+
mode: None,
|
|
649
|
+
modified: None,
|
|
650
|
+
symlink_target: None,
|
|
651
|
+
hardlink_target: None,
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
let entry = ArchiveEntry::from(core_entry);
|
|
655
|
+
|
|
656
|
+
assert_eq!(
|
|
657
|
+
entry.compressed_size,
|
|
658
|
+
Some(i64::MAX),
|
|
659
|
+
"compressed_size should saturate to i64::MAX"
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
#[test]
|
|
664
|
+
fn test_verification_report_saturates_total_entries_at_u32_max() {
|
|
665
|
+
use exarch_core::formats::detect::ArchiveType;
|
|
666
|
+
use exarch_core::inspection::CheckStatus;
|
|
667
|
+
use exarch_core::inspection::VerificationStatus;
|
|
668
|
+
|
|
669
|
+
let core_report = exarch_core::inspection::VerificationReport {
|
|
670
|
+
status: VerificationStatus::Pass,
|
|
671
|
+
integrity_status: CheckStatus::Pass,
|
|
672
|
+
security_status: CheckStatus::Pass,
|
|
673
|
+
issues: vec![],
|
|
674
|
+
total_entries: (u32::MAX as usize) + 1000,
|
|
675
|
+
suspicious_entries: 0,
|
|
676
|
+
total_size: 5000,
|
|
677
|
+
format: ArchiveType::TarGz,
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
let report = VerificationReport::from(core_report);
|
|
681
|
+
|
|
682
|
+
assert_eq!(
|
|
683
|
+
report.total_entries,
|
|
684
|
+
u32::MAX,
|
|
685
|
+
"total_entries should saturate to u32::MAX"
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
#[test]
|
|
690
|
+
fn test_verification_report_saturates_suspicious_entries_at_u32_max() {
|
|
691
|
+
use exarch_core::formats::detect::ArchiveType;
|
|
692
|
+
use exarch_core::inspection::CheckStatus;
|
|
693
|
+
use exarch_core::inspection::VerificationStatus;
|
|
694
|
+
|
|
695
|
+
let core_report = exarch_core::inspection::VerificationReport {
|
|
696
|
+
status: VerificationStatus::Fail,
|
|
697
|
+
integrity_status: CheckStatus::Pass,
|
|
698
|
+
security_status: CheckStatus::Fail,
|
|
699
|
+
issues: vec![],
|
|
700
|
+
total_entries: 100,
|
|
701
|
+
suspicious_entries: (u32::MAX as usize) + 1000,
|
|
702
|
+
total_size: 5000,
|
|
703
|
+
format: ArchiveType::TarGz,
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
let report = VerificationReport::from(core_report);
|
|
707
|
+
|
|
708
|
+
assert_eq!(
|
|
709
|
+
report.suspicious_entries,
|
|
710
|
+
u32::MAX,
|
|
711
|
+
"suspicious_entries should saturate to u32::MAX"
|
|
712
|
+
);
|
|
713
|
+
}
|
|
176
714
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for archive creation functions
|
|
3
|
+
*/
|
|
4
|
+
const { describe, it, beforeEach } = require('node:test');
|
|
5
|
+
const assert = require('node:assert');
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const os = require('node:os');
|
|
9
|
+
const {
|
|
10
|
+
createArchive,
|
|
11
|
+
createArchiveSync,
|
|
12
|
+
listArchiveSync,
|
|
13
|
+
CreationConfig,
|
|
14
|
+
} = require('../index.js');
|
|
15
|
+
|
|
16
|
+
function createTempDir() {
|
|
17
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'exarch-test-'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createTestFiles(dir) {
|
|
21
|
+
fs.writeFileSync(path.join(dir, 'file1.txt'), 'Content of file 1');
|
|
22
|
+
fs.writeFileSync(path.join(dir, 'file2.txt'), 'Content of file 2');
|
|
23
|
+
|
|
24
|
+
const subdir = path.join(dir, 'subdir');
|
|
25
|
+
fs.mkdirSync(subdir);
|
|
26
|
+
fs.writeFileSync(path.join(subdir, 'nested.txt'), 'Nested file content');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('createArchive (async)', () => {
|
|
30
|
+
let tempDir;
|
|
31
|
+
let sourceDir;
|
|
32
|
+
let outputPath;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
tempDir = createTempDir();
|
|
36
|
+
sourceDir = path.join(tempDir, 'source');
|
|
37
|
+
fs.mkdirSync(sourceDir);
|
|
38
|
+
createTestFiles(sourceDir);
|
|
39
|
+
outputPath = path.join(tempDir, 'output.tar.gz');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should create a tar.gz archive', async () => {
|
|
43
|
+
const report = await createArchive(outputPath, [sourceDir]);
|
|
44
|
+
|
|
45
|
+
assert.ok(report.filesAdded >= 3);
|
|
46
|
+
assert.ok(report.bytesWritten > 0);
|
|
47
|
+
assert.ok(report.durationMs >= 0);
|
|
48
|
+
assert.ok(fs.existsSync(outputPath));
|
|
49
|
+
assert.ok(fs.statSync(outputPath).size > 0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should create archive from multiple sources', async () => {
|
|
53
|
+
const file1 = path.join(sourceDir, 'file1.txt');
|
|
54
|
+
const file2 = path.join(sourceDir, 'file2.txt');
|
|
55
|
+
|
|
56
|
+
const report = await createArchive(outputPath, [file1, file2]);
|
|
57
|
+
|
|
58
|
+
assert.strictEqual(report.filesAdded, 2);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should accept custom CreationConfig', async () => {
|
|
62
|
+
const config = new CreationConfig();
|
|
63
|
+
config.setCompressionLevel(9);
|
|
64
|
+
config.setIncludeHidden(false);
|
|
65
|
+
|
|
66
|
+
const report = await createArchive(outputPath, [sourceDir], config);
|
|
67
|
+
|
|
68
|
+
assert.ok(report.filesAdded > 0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should throw on invalid source path', async () => {
|
|
72
|
+
await assert.rejects(
|
|
73
|
+
createArchive(outputPath, ['/nonexistent/path']),
|
|
74
|
+
/IO_ERROR|No such file|not found/i
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should report warnings array', async () => {
|
|
79
|
+
const report = await createArchive(outputPath, [sourceDir]);
|
|
80
|
+
|
|
81
|
+
assert.ok(Array.isArray(report.warnings));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('createArchiveSync', () => {
|
|
86
|
+
let tempDir;
|
|
87
|
+
let sourceDir;
|
|
88
|
+
let outputPath;
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
tempDir = createTempDir();
|
|
92
|
+
sourceDir = path.join(tempDir, 'source');
|
|
93
|
+
fs.mkdirSync(sourceDir);
|
|
94
|
+
createTestFiles(sourceDir);
|
|
95
|
+
outputPath = path.join(tempDir, 'output.tar.gz');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should create archive synchronously', () => {
|
|
99
|
+
const report = createArchiveSync(outputPath, [sourceDir]);
|
|
100
|
+
|
|
101
|
+
assert.ok(report.filesAdded >= 3);
|
|
102
|
+
assert.ok(fs.existsSync(outputPath));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should accept custom CreationConfig', () => {
|
|
106
|
+
const config = new CreationConfig();
|
|
107
|
+
config.setCompressionLevel(1);
|
|
108
|
+
const report = createArchiveSync(outputPath, [sourceDir], config);
|
|
109
|
+
|
|
110
|
+
assert.ok(report.filesAdded > 0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should support exclude patterns', () => {
|
|
114
|
+
fs.writeFileSync(path.join(sourceDir, 'debug.log'), 'log content');
|
|
115
|
+
|
|
116
|
+
const config = new CreationConfig();
|
|
117
|
+
config.addExcludePattern('*.log');
|
|
118
|
+
createArchiveSync(outputPath, [sourceDir], config);
|
|
119
|
+
|
|
120
|
+
const manifest = listArchiveSync(outputPath);
|
|
121
|
+
const logEntries = manifest.entries.filter((e) => e.path.endsWith('.log'));
|
|
122
|
+
assert.strictEqual(logEntries.length, 0);
|
|
123
|
+
});
|
|
124
|
+
});
|