exarch-rs 0.1.0 → 0.1.2
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/native/exarch-rs.darwin-arm64.node +0 -0
- package/native/exarch-rs.darwin-x64.node +0 -0
- package/native/exarch-rs.linux-arm64-gnu.node +0 -0
- package/native/exarch-rs.linux-x64-gnu.node +0 -0
- package/native/exarch-rs.win32-x64-msvc.node +0 -0
- package/package.json +25 -5
- package/src/config.rs +303 -47
- package/src/error.rs +42 -0
- package/src/lib.rs +560 -17
- package/src/report.rs +538 -0
- package/tests/create.test.js +136 -0
- package/tests/creation-config.test.js +97 -0
- package/tests/extract.test.js +117 -0
- package/tests/list-verify.test.js +172 -0
- package/tests/security-config.test.js +187 -0
- package/index.d.ts +0 -287
package/Cargo.toml
CHANGED
package/README.md
CHANGED
package/biome.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": false,
|
|
7
|
+
"defaultBranch": "main"
|
|
8
|
+
},
|
|
9
|
+
"linter": {
|
|
10
|
+
"enabled": true,
|
|
11
|
+
"rules": {
|
|
12
|
+
"recommended": true
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"formatter": {
|
|
16
|
+
"enabled": true,
|
|
17
|
+
"indentStyle": "space",
|
|
18
|
+
"indentWidth": 2,
|
|
19
|
+
"lineWidth": 100,
|
|
20
|
+
"lineEnding": "lf"
|
|
21
|
+
},
|
|
22
|
+
"javascript": {
|
|
23
|
+
"formatter": {
|
|
24
|
+
"quoteStyle": "single",
|
|
25
|
+
"trailingCommas": "es5",
|
|
26
|
+
"semicolons": "always",
|
|
27
|
+
"arrowParentheses": "always"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"json": {
|
|
31
|
+
"formatter": {
|
|
32
|
+
"trailingCommas": "none"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": {
|
|
36
|
+
"includes": [
|
|
37
|
+
"**/*.js",
|
|
38
|
+
"**/*.json",
|
|
39
|
+
"!**/node_modules",
|
|
40
|
+
"!**/target",
|
|
41
|
+
"!**/*.node",
|
|
42
|
+
"!**/package-lock.json",
|
|
43
|
+
"!**/index.js",
|
|
44
|
+
"!**/index.d.ts"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "exarch-rs",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Memory-safe archive extraction library",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Memory-safe archive extraction library with built-in security validation",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"keywords": [
|
|
@@ -10,14 +10,25 @@
|
|
|
10
10
|
"security",
|
|
11
11
|
"tar",
|
|
12
12
|
"zip",
|
|
13
|
+
"gzip",
|
|
14
|
+
"bzip2",
|
|
15
|
+
"xz",
|
|
16
|
+
"zstd",
|
|
17
|
+
"path-traversal",
|
|
18
|
+
"zip-bomb",
|
|
13
19
|
"napi-rs",
|
|
14
20
|
"rust"
|
|
15
21
|
],
|
|
22
|
+
"author": "Exarch Contributors",
|
|
16
23
|
"license": "MIT OR Apache-2.0",
|
|
24
|
+
"homepage": "https://github.com/bug-ops/exarch",
|
|
17
25
|
"repository": {
|
|
18
26
|
"type": "git",
|
|
19
27
|
"url": "https://github.com/bug-ops/exarch"
|
|
20
28
|
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/bug-ops/exarch/issues"
|
|
31
|
+
},
|
|
21
32
|
"napi": {
|
|
22
33
|
"name": "exarch-rs",
|
|
23
34
|
"triples": {
|
|
@@ -26,12 +37,21 @@
|
|
|
26
37
|
},
|
|
27
38
|
"scripts": {
|
|
28
39
|
"build": "napi build --platform --release",
|
|
29
|
-
"build:debug": "napi build --platform"
|
|
40
|
+
"build:debug": "napi build --platform",
|
|
41
|
+
"test": "node --test tests/*.test.js",
|
|
42
|
+
"test:coverage": "node --test --experimental-test-coverage tests/*.test.js",
|
|
43
|
+
"format": "biome format --write .",
|
|
44
|
+
"format:check": "biome format .",
|
|
45
|
+
"lint": "biome lint .",
|
|
46
|
+
"lint:fix": "biome lint --write .",
|
|
47
|
+
"check": "biome check --write .",
|
|
48
|
+
"check:ci": "biome check ."
|
|
30
49
|
},
|
|
31
50
|
"devDependencies": {
|
|
32
|
-
"@
|
|
51
|
+
"@biomejs/biome": "^2.0",
|
|
52
|
+
"@napi-rs/cli": "^3.5"
|
|
33
53
|
},
|
|
34
54
|
"engines": {
|
|
35
|
-
"node": ">=
|
|
55
|
+
"node": ">= 18"
|
|
36
56
|
}
|
|
37
57
|
}
|
package/src/config.rs
CHANGED
|
@@ -73,8 +73,8 @@ impl SecurityConfig {
|
|
|
73
73
|
/// # Errors
|
|
74
74
|
///
|
|
75
75
|
/// Returns error if size is negative.
|
|
76
|
-
#[napi]
|
|
77
|
-
pub fn
|
|
76
|
+
#[napi(js_name = "setMaxFileSize")]
|
|
77
|
+
pub fn set_max_file_size(&mut self, size: i64) -> Result<&Self> {
|
|
78
78
|
if size < 0 {
|
|
79
79
|
return Err(Error::from_reason("max file size cannot be negative"));
|
|
80
80
|
}
|
|
@@ -90,8 +90,8 @@ impl SecurityConfig {
|
|
|
90
90
|
/// # Errors
|
|
91
91
|
///
|
|
92
92
|
/// Returns error if size is negative.
|
|
93
|
-
#[napi]
|
|
94
|
-
pub fn
|
|
93
|
+
#[napi(js_name = "setMaxTotalSize")]
|
|
94
|
+
pub fn set_max_total_size(&mut self, size: i64) -> Result<&Self> {
|
|
95
95
|
if size < 0 {
|
|
96
96
|
return Err(Error::from_reason("max total size cannot be negative"));
|
|
97
97
|
}
|
|
@@ -107,8 +107,8 @@ impl SecurityConfig {
|
|
|
107
107
|
/// # Errors
|
|
108
108
|
///
|
|
109
109
|
/// Returns error if ratio is not a positive finite number.
|
|
110
|
-
#[napi]
|
|
111
|
-
pub fn
|
|
110
|
+
#[napi(js_name = "setMaxCompressionRatio")]
|
|
111
|
+
pub fn set_max_compression_ratio(&mut self, ratio: f64) -> Result<&Self> {
|
|
112
112
|
if !ratio.is_finite() || ratio <= 0.0 {
|
|
113
113
|
return Err(Error::from_reason(
|
|
114
114
|
"compression ratio must be a positive finite number",
|
|
@@ -119,50 +119,50 @@ impl SecurityConfig {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/// Sets the maximum file count.
|
|
122
|
-
#[napi]
|
|
123
|
-
pub fn
|
|
122
|
+
#[napi(js_name = "setMaxFileCount")]
|
|
123
|
+
pub fn set_max_file_count(&mut self, count: u32) -> &Self {
|
|
124
124
|
self.inner.max_file_count = count as usize;
|
|
125
125
|
self
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/// Sets the maximum path depth.
|
|
129
|
-
#[napi]
|
|
130
|
-
pub fn
|
|
129
|
+
#[napi(js_name = "setMaxPathDepth")]
|
|
130
|
+
pub fn set_max_path_depth(&mut self, depth: u32) -> &Self {
|
|
131
131
|
self.inner.max_path_depth = depth as usize;
|
|
132
132
|
self
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
/// Allows or denies symlinks.
|
|
136
|
-
#[napi]
|
|
137
|
-
pub fn
|
|
136
|
+
#[napi(js_name = "setAllowSymlinks")]
|
|
137
|
+
pub fn set_allow_symlinks(&mut self, allow: Option<bool>) -> &Self {
|
|
138
138
|
self.inner.allowed.symlinks = allow.unwrap_or(true);
|
|
139
139
|
self
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/// Allows or denies hardlinks.
|
|
143
|
-
#[napi]
|
|
144
|
-
pub fn
|
|
143
|
+
#[napi(js_name = "setAllowHardlinks")]
|
|
144
|
+
pub fn set_allow_hardlinks(&mut self, allow: Option<bool>) -> &Self {
|
|
145
145
|
self.inner.allowed.hardlinks = allow.unwrap_or(true);
|
|
146
146
|
self
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/// Allows or denies absolute paths.
|
|
150
|
-
#[napi]
|
|
151
|
-
pub fn
|
|
150
|
+
#[napi(js_name = "setAllowAbsolutePaths")]
|
|
151
|
+
pub fn set_allow_absolute_paths(&mut self, allow: Option<bool>) -> &Self {
|
|
152
152
|
self.inner.allowed.absolute_paths = allow.unwrap_or(true);
|
|
153
153
|
self
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
/// Allows or denies world-writable files.
|
|
157
|
-
#[napi]
|
|
158
|
-
pub fn
|
|
157
|
+
#[napi(js_name = "setAllowWorldWritable")]
|
|
158
|
+
pub fn set_allow_world_writable(&mut self, allow: Option<bool>) -> &Self {
|
|
159
159
|
self.inner.allowed.world_writable = allow.unwrap_or(true);
|
|
160
160
|
self
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
/// Sets whether to preserve permissions from archive.
|
|
164
|
-
#[napi]
|
|
165
|
-
pub fn
|
|
164
|
+
#[napi(js_name = "setPreservePermissions")]
|
|
165
|
+
pub fn set_preserve_permissions(&mut self, preserve: Option<bool>) -> &Self {
|
|
166
166
|
self.inner.preserve_permissions = preserve.unwrap_or(true);
|
|
167
167
|
self
|
|
168
168
|
}
|
|
@@ -361,6 +361,185 @@ impl SecurityConfig {
|
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
/// Configuration for archive creation operations.
|
|
365
|
+
///
|
|
366
|
+
/// Controls how archives are created from filesystem sources, including
|
|
367
|
+
/// security options, compression settings, and file filtering.
|
|
368
|
+
///
|
|
369
|
+
/// # Defaults
|
|
370
|
+
///
|
|
371
|
+
/// | Setting | Default Value |
|
|
372
|
+
/// |---------|--------------|
|
|
373
|
+
/// | `compression_level` | 6 (balanced) |
|
|
374
|
+
/// | `preserve_permissions` | true |
|
|
375
|
+
/// | `follow_symlinks` | false (secure) |
|
|
376
|
+
/// | `include_hidden` | false |
|
|
377
|
+
/// | `max_file_size` | None (no limit) |
|
|
378
|
+
/// | `exclude_patterns` | `[".git", ".DS_Store", "*.tmp"]` |
|
|
379
|
+
#[napi]
|
|
380
|
+
#[derive(Debug, Clone)]
|
|
381
|
+
pub struct CreationConfig {
|
|
382
|
+
inner: exarch_core::creation::CreationConfig,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#[napi]
|
|
386
|
+
impl CreationConfig {
|
|
387
|
+
/// Creates a new `CreationConfig` with secure defaults.
|
|
388
|
+
#[napi(constructor)]
|
|
389
|
+
pub fn new() -> Self {
|
|
390
|
+
Self {
|
|
391
|
+
inner: exarch_core::creation::CreationConfig::default(),
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/// Creates a `CreationConfig` with secure defaults.
|
|
396
|
+
///
|
|
397
|
+
/// This is equivalent to calling `new CreationConfig()`.
|
|
398
|
+
#[napi(factory)]
|
|
399
|
+
pub fn default() -> Self {
|
|
400
|
+
Self::new()
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/// Sets the compression level (1-9).
|
|
404
|
+
///
|
|
405
|
+
/// Higher values provide better compression but slower speed.
|
|
406
|
+
/// Default: 6 (balanced).
|
|
407
|
+
///
|
|
408
|
+
/// # Errors
|
|
409
|
+
///
|
|
410
|
+
/// Returns error if level is not in range 1-9.
|
|
411
|
+
#[napi(js_name = "setCompressionLevel")]
|
|
412
|
+
#[allow(clippy::cast_possible_truncation)] // level is validated to be 1-9
|
|
413
|
+
pub fn set_compression_level(&mut self, level: u32) -> Result<&Self> {
|
|
414
|
+
if !(1..=9).contains(&level) {
|
|
415
|
+
return Err(Error::from_reason(
|
|
416
|
+
"compression level must be between 1 and 9",
|
|
417
|
+
));
|
|
418
|
+
}
|
|
419
|
+
self.inner.compression_level = Some(level as u8);
|
|
420
|
+
Ok(self)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/// Sets whether to preserve file permissions from source.
|
|
424
|
+
///
|
|
425
|
+
/// Default: true.
|
|
426
|
+
#[napi(js_name = "setPreservePermissions")]
|
|
427
|
+
pub fn set_preserve_permissions(&mut self, preserve: Option<bool>) -> &Self {
|
|
428
|
+
self.inner.preserve_permissions = preserve.unwrap_or(true);
|
|
429
|
+
self
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/// Sets whether to follow symlinks when adding files.
|
|
433
|
+
///
|
|
434
|
+
/// Default: false (store symlinks as symlinks).
|
|
435
|
+
///
|
|
436
|
+
/// Security note: Following symlinks may include unintended files
|
|
437
|
+
/// from outside the source directory.
|
|
438
|
+
#[napi(js_name = "setFollowSymlinks")]
|
|
439
|
+
pub fn set_follow_symlinks(&mut self, follow: Option<bool>) -> &Self {
|
|
440
|
+
self.inner.follow_symlinks = follow.unwrap_or(true);
|
|
441
|
+
self
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/// Sets whether to include hidden files (files starting with '.').
|
|
445
|
+
///
|
|
446
|
+
/// Default: false.
|
|
447
|
+
#[napi(js_name = "setIncludeHidden")]
|
|
448
|
+
pub fn set_include_hidden(&mut self, include: Option<bool>) -> &Self {
|
|
449
|
+
self.inner.include_hidden = include.unwrap_or(true);
|
|
450
|
+
self
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/// Sets maximum size for a single file in bytes.
|
|
454
|
+
///
|
|
455
|
+
/// Files larger than this limit will be skipped.
|
|
456
|
+
///
|
|
457
|
+
/// # Errors
|
|
458
|
+
///
|
|
459
|
+
/// Returns error if size is negative.
|
|
460
|
+
#[napi(js_name = "setMaxFileSize")]
|
|
461
|
+
pub fn set_max_file_size(&mut self, size: i64) -> Result<&Self> {
|
|
462
|
+
if size < 0 {
|
|
463
|
+
return Err(Error::from_reason("max file size cannot be negative"));
|
|
464
|
+
}
|
|
465
|
+
#[allow(clippy::cast_sign_loss)]
|
|
466
|
+
{
|
|
467
|
+
self.inner.max_file_size = if size == 0 { None } else { Some(size as u64) };
|
|
468
|
+
}
|
|
469
|
+
Ok(self)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/// Adds an exclude pattern.
|
|
473
|
+
///
|
|
474
|
+
/// Files matching this pattern will be skipped.
|
|
475
|
+
///
|
|
476
|
+
/// # Errors
|
|
477
|
+
///
|
|
478
|
+
/// Returns error if pattern contains null bytes.
|
|
479
|
+
#[napi]
|
|
480
|
+
pub fn add_exclude_pattern(&mut self, pattern: String) -> Result<&Self> {
|
|
481
|
+
if pattern.contains('\0') {
|
|
482
|
+
return Err(Error::from_reason(
|
|
483
|
+
"pattern contains null bytes - potential security issue",
|
|
484
|
+
));
|
|
485
|
+
}
|
|
486
|
+
self.inner.exclude_patterns.push(pattern);
|
|
487
|
+
Ok(self)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/// Finalizes the configuration (for API consistency).
|
|
491
|
+
#[napi]
|
|
492
|
+
pub fn build(&self) -> &Self {
|
|
493
|
+
self
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Property getters
|
|
497
|
+
|
|
498
|
+
/// Compression level (1-9).
|
|
499
|
+
#[napi(getter)]
|
|
500
|
+
pub fn get_compression_level(&self) -> Option<u32> {
|
|
501
|
+
self.inner.compression_level.map(u32::from)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/// Whether to preserve permissions.
|
|
505
|
+
#[napi(getter)]
|
|
506
|
+
pub fn get_preserve_permissions(&self) -> bool {
|
|
507
|
+
self.inner.preserve_permissions
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/// Whether to follow symlinks.
|
|
511
|
+
#[napi(getter)]
|
|
512
|
+
pub fn get_follow_symlinks(&self) -> bool {
|
|
513
|
+
self.inner.follow_symlinks
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/// Whether to include hidden files.
|
|
517
|
+
#[napi(getter)]
|
|
518
|
+
pub fn get_include_hidden(&self) -> bool {
|
|
519
|
+
self.inner.include_hidden
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/// Maximum file size in bytes.
|
|
523
|
+
#[napi(getter)]
|
|
524
|
+
#[allow(clippy::cast_possible_wrap)]
|
|
525
|
+
pub fn get_max_file_size(&self) -> Option<i64> {
|
|
526
|
+
self.inner.max_file_size.map(|s| s as i64)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/// List of exclude patterns.
|
|
530
|
+
#[napi(getter)]
|
|
531
|
+
pub fn get_exclude_patterns(&self) -> Vec<String> {
|
|
532
|
+
self.inner.exclude_patterns.clone()
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
impl CreationConfig {
|
|
537
|
+
/// Returns a reference to the inner `CoreCreationConfig`.
|
|
538
|
+
pub fn as_core(&self) -> &exarch_core::creation::CreationConfig {
|
|
539
|
+
&self.inner
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
364
543
|
#[cfg(test)]
|
|
365
544
|
#[allow(
|
|
366
545
|
clippy::unwrap_used,
|
|
@@ -400,9 +579,9 @@ mod tests {
|
|
|
400
579
|
#[test]
|
|
401
580
|
fn test_builder_pattern_method_chaining() {
|
|
402
581
|
let mut config = SecurityConfig::new();
|
|
403
|
-
config.
|
|
404
|
-
config.
|
|
405
|
-
config.
|
|
582
|
+
config.set_max_file_size(100_000_000).unwrap();
|
|
583
|
+
config.set_max_total_size(1_000_000_000).unwrap();
|
|
584
|
+
config.set_max_file_count(50_000);
|
|
406
585
|
|
|
407
586
|
assert_eq!(config.get_max_file_size(), 100_000_000);
|
|
408
587
|
assert_eq!(config.get_max_total_size(), 1_000_000_000);
|
|
@@ -412,7 +591,7 @@ mod tests {
|
|
|
412
591
|
#[test]
|
|
413
592
|
fn test_builder_compression_ratio_valid() {
|
|
414
593
|
let mut config = SecurityConfig::new();
|
|
415
|
-
let result = config.
|
|
594
|
+
let result = config.set_max_compression_ratio(200.0);
|
|
416
595
|
assert!(result.is_ok());
|
|
417
596
|
assert_eq!(config.get_max_compression_ratio(), 200.0);
|
|
418
597
|
}
|
|
@@ -420,28 +599,28 @@ mod tests {
|
|
|
420
599
|
#[test]
|
|
421
600
|
fn test_builder_compression_ratio_rejects_nan() {
|
|
422
601
|
let mut config = SecurityConfig::new();
|
|
423
|
-
let result = config.
|
|
602
|
+
let result = config.set_max_compression_ratio(f64::NAN);
|
|
424
603
|
assert!(result.is_err());
|
|
425
604
|
}
|
|
426
605
|
|
|
427
606
|
#[test]
|
|
428
607
|
fn test_builder_compression_ratio_rejects_infinity() {
|
|
429
608
|
let mut config = SecurityConfig::new();
|
|
430
|
-
let result = config.
|
|
609
|
+
let result = config.set_max_compression_ratio(f64::INFINITY);
|
|
431
610
|
assert!(result.is_err());
|
|
432
611
|
}
|
|
433
612
|
|
|
434
613
|
#[test]
|
|
435
614
|
fn test_builder_compression_ratio_rejects_negative() {
|
|
436
615
|
let mut config = SecurityConfig::new();
|
|
437
|
-
let result = config.
|
|
616
|
+
let result = config.set_max_compression_ratio(-10.0);
|
|
438
617
|
assert!(result.is_err());
|
|
439
618
|
}
|
|
440
619
|
|
|
441
620
|
#[test]
|
|
442
621
|
fn test_builder_compression_ratio_rejects_zero() {
|
|
443
622
|
let mut config = SecurityConfig::new();
|
|
444
|
-
let result = config.
|
|
623
|
+
let result = config.set_max_compression_ratio(0.0);
|
|
445
624
|
assert!(result.is_err());
|
|
446
625
|
}
|
|
447
626
|
|
|
@@ -524,7 +703,7 @@ mod tests {
|
|
|
524
703
|
#[test]
|
|
525
704
|
fn test_max_file_size_negative_value() {
|
|
526
705
|
let mut config = SecurityConfig::new();
|
|
527
|
-
let result = config.
|
|
706
|
+
let result = config.set_max_file_size(-1);
|
|
528
707
|
assert!(result.is_err(), "negative file size should be rejected");
|
|
529
708
|
assert!(
|
|
530
709
|
result.unwrap_err().to_string().contains("negative"),
|
|
@@ -535,7 +714,7 @@ mod tests {
|
|
|
535
714
|
#[test]
|
|
536
715
|
fn test_max_file_size_i64_max() {
|
|
537
716
|
let mut config = SecurityConfig::new();
|
|
538
|
-
let result = config.
|
|
717
|
+
let result = config.set_max_file_size(i64::MAX);
|
|
539
718
|
assert!(result.is_ok(), "i64::MAX should be accepted");
|
|
540
719
|
assert_eq!(
|
|
541
720
|
config.get_max_file_size(),
|
|
@@ -547,7 +726,7 @@ mod tests {
|
|
|
547
726
|
#[test]
|
|
548
727
|
fn test_max_total_size_negative_value() {
|
|
549
728
|
let mut config = SecurityConfig::new();
|
|
550
|
-
let result = config.
|
|
729
|
+
let result = config.set_max_total_size(-1);
|
|
551
730
|
assert!(result.is_err(), "negative total size should be rejected");
|
|
552
731
|
assert!(
|
|
553
732
|
result.unwrap_err().to_string().contains("negative"),
|
|
@@ -558,7 +737,7 @@ mod tests {
|
|
|
558
737
|
#[test]
|
|
559
738
|
fn test_max_total_size_i64_max() {
|
|
560
739
|
let mut config = SecurityConfig::new();
|
|
561
|
-
let result = config.
|
|
740
|
+
let result = config.set_max_total_size(i64::MAX);
|
|
562
741
|
assert!(result.is_ok(), "i64::MAX should be accepted");
|
|
563
742
|
assert_eq!(
|
|
564
743
|
config.get_max_total_size(),
|
|
@@ -570,7 +749,7 @@ mod tests {
|
|
|
570
749
|
#[test]
|
|
571
750
|
fn test_max_file_count_u32_max() {
|
|
572
751
|
let mut config = SecurityConfig::new();
|
|
573
|
-
config.
|
|
752
|
+
config.set_max_file_count(u32::MAX);
|
|
574
753
|
assert_eq!(
|
|
575
754
|
config.get_max_file_count(),
|
|
576
755
|
u32::MAX,
|
|
@@ -581,7 +760,7 @@ mod tests {
|
|
|
581
760
|
#[test]
|
|
582
761
|
fn test_max_path_depth_u32_max() {
|
|
583
762
|
let mut config = SecurityConfig::new();
|
|
584
|
-
config.
|
|
763
|
+
config.set_max_path_depth(u32::MAX);
|
|
585
764
|
assert_eq!(
|
|
586
765
|
config.get_max_path_depth(),
|
|
587
766
|
u32::MAX,
|
|
@@ -592,7 +771,7 @@ mod tests {
|
|
|
592
771
|
#[test]
|
|
593
772
|
fn test_max_file_size_zero() {
|
|
594
773
|
let mut config = SecurityConfig::new();
|
|
595
|
-
let result = config.
|
|
774
|
+
let result = config.set_max_file_size(0);
|
|
596
775
|
assert!(result.is_ok(), "zero file size should be accepted");
|
|
597
776
|
assert_eq!(config.get_max_file_size(), 0);
|
|
598
777
|
}
|
|
@@ -601,12 +780,12 @@ mod tests {
|
|
|
601
780
|
#[test]
|
|
602
781
|
fn test_property_getters_return_correct_values() {
|
|
603
782
|
let mut config = SecurityConfig::new();
|
|
604
|
-
config.
|
|
605
|
-
config.
|
|
606
|
-
config.
|
|
607
|
-
config.
|
|
608
|
-
config.
|
|
609
|
-
config.
|
|
783
|
+
config.set_max_file_size(100_000_000).unwrap();
|
|
784
|
+
config.set_max_total_size(1_000_000_000).unwrap();
|
|
785
|
+
config.set_max_compression_ratio(250.0).unwrap();
|
|
786
|
+
config.set_max_file_count(50_000);
|
|
787
|
+
config.set_max_path_depth(64);
|
|
788
|
+
config.set_preserve_permissions(Some(true));
|
|
610
789
|
|
|
611
790
|
assert_eq!(
|
|
612
791
|
config.get_max_file_size(),
|
|
@@ -788,8 +967,8 @@ mod tests {
|
|
|
788
967
|
#[test]
|
|
789
968
|
fn test_builder_pattern_with_build() {
|
|
790
969
|
let mut config = SecurityConfig::new();
|
|
791
|
-
config.
|
|
792
|
-
config.
|
|
970
|
+
config.set_max_file_size(100_000_000).unwrap();
|
|
971
|
+
config.set_max_total_size(1_000_000_000).unwrap();
|
|
793
972
|
let result = config.build();
|
|
794
973
|
|
|
795
974
|
assert_eq!(
|
|
@@ -804,14 +983,14 @@ mod tests {
|
|
|
804
983
|
#[test]
|
|
805
984
|
fn test_builder_compression_ratio_rejects_negative_infinity() {
|
|
806
985
|
let mut config = SecurityConfig::new();
|
|
807
|
-
let result = config.
|
|
986
|
+
let result = config.set_max_compression_ratio(f64::NEG_INFINITY);
|
|
808
987
|
assert!(result.is_err(), "negative infinity should be rejected");
|
|
809
988
|
}
|
|
810
989
|
|
|
811
990
|
#[test]
|
|
812
991
|
fn test_builder_compression_ratio_accepts_very_small_positive() {
|
|
813
992
|
let mut config = SecurityConfig::new();
|
|
814
|
-
let result = config.
|
|
993
|
+
let result = config.set_max_compression_ratio(0.000001);
|
|
815
994
|
assert!(
|
|
816
995
|
result.is_ok(),
|
|
817
996
|
"very small positive values should be accepted"
|
|
@@ -822,7 +1001,7 @@ mod tests {
|
|
|
822
1001
|
#[test]
|
|
823
1002
|
fn test_builder_compression_ratio_accepts_very_large() {
|
|
824
1003
|
let mut config = SecurityConfig::new();
|
|
825
|
-
let result = config.
|
|
1004
|
+
let result = config.set_max_compression_ratio(1_000_000.0);
|
|
826
1005
|
assert!(result.is_ok(), "very large values should be accepted");
|
|
827
1006
|
assert_eq!(config.get_max_compression_ratio(), 1_000_000.0);
|
|
828
1007
|
}
|
|
@@ -831,7 +1010,7 @@ mod tests {
|
|
|
831
1010
|
#[test]
|
|
832
1011
|
fn test_security_config_clone() {
|
|
833
1012
|
let mut config = SecurityConfig::new();
|
|
834
|
-
let _ = config.
|
|
1013
|
+
let _ = config.set_max_file_size(100_000_000).unwrap();
|
|
835
1014
|
|
|
836
1015
|
let cloned = config.clone();
|
|
837
1016
|
assert_eq!(
|
|
@@ -924,4 +1103,81 @@ mod tests {
|
|
|
924
1103
|
".git should be in default banned components"
|
|
925
1104
|
);
|
|
926
1105
|
}
|
|
1106
|
+
|
|
1107
|
+
// CreationConfig tests
|
|
1108
|
+
#[test]
|
|
1109
|
+
fn test_creation_config_default() {
|
|
1110
|
+
let config = CreationConfig::new();
|
|
1111
|
+
assert_eq!(config.get_compression_level(), Some(6));
|
|
1112
|
+
assert!(config.get_preserve_permissions());
|
|
1113
|
+
assert!(!config.get_follow_symlinks());
|
|
1114
|
+
assert!(!config.get_include_hidden());
|
|
1115
|
+
assert_eq!(config.get_max_file_size(), None);
|
|
1116
|
+
assert_eq!(config.get_exclude_patterns().len(), 3);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
#[test]
|
|
1120
|
+
fn test_creation_config_builder() {
|
|
1121
|
+
let mut config = CreationConfig::new();
|
|
1122
|
+
config.set_compression_level(9).unwrap();
|
|
1123
|
+
config.set_preserve_permissions(Some(false));
|
|
1124
|
+
config.set_follow_symlinks(Some(true));
|
|
1125
|
+
config.set_include_hidden(Some(true));
|
|
1126
|
+
config.set_max_file_size(1_000_000).unwrap();
|
|
1127
|
+
|
|
1128
|
+
assert_eq!(config.get_compression_level(), Some(9));
|
|
1129
|
+
assert!(!config.get_preserve_permissions());
|
|
1130
|
+
assert!(config.get_follow_symlinks());
|
|
1131
|
+
assert!(config.get_include_hidden());
|
|
1132
|
+
assert_eq!(config.get_max_file_size(), Some(1_000_000));
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
#[test]
|
|
1136
|
+
fn test_creation_config_compression_level_rejects_invalid() {
|
|
1137
|
+
let mut config = CreationConfig::new();
|
|
1138
|
+
assert!(config.set_compression_level(0).is_err());
|
|
1139
|
+
assert!(config.set_compression_level(10).is_err());
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
#[test]
|
|
1143
|
+
fn test_creation_config_compression_level_accepts_valid() {
|
|
1144
|
+
let mut config = CreationConfig::new();
|
|
1145
|
+
assert!(config.set_compression_level(1).is_ok());
|
|
1146
|
+
assert!(config.set_compression_level(9).is_ok());
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
#[test]
|
|
1150
|
+
fn test_creation_config_max_file_size_negative() {
|
|
1151
|
+
let mut config = CreationConfig::new();
|
|
1152
|
+
let result = config.set_max_file_size(-1);
|
|
1153
|
+
assert!(result.is_err());
|
|
1154
|
+
assert!(
|
|
1155
|
+
result
|
|
1156
|
+
.unwrap_err()
|
|
1157
|
+
.to_string()
|
|
1158
|
+
.contains("cannot be negative")
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
#[test]
|
|
1163
|
+
fn test_creation_config_add_exclude_pattern() {
|
|
1164
|
+
let mut config = CreationConfig::new();
|
|
1165
|
+
config.add_exclude_pattern("*.log".to_string()).unwrap();
|
|
1166
|
+
let patterns = config.get_exclude_patterns();
|
|
1167
|
+
assert!(patterns.contains(&"*.log".to_string()));
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
#[test]
|
|
1171
|
+
fn test_creation_config_add_exclude_pattern_rejects_null() {
|
|
1172
|
+
let mut config = CreationConfig::new();
|
|
1173
|
+
let result = config.add_exclude_pattern("bad\0pattern".to_string());
|
|
1174
|
+
assert!(result.is_err());
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
#[test]
|
|
1178
|
+
fn test_creation_config_as_core() {
|
|
1179
|
+
let config = CreationConfig::new();
|
|
1180
|
+
let core = config.as_core();
|
|
1181
|
+
assert_eq!(core.compression_level, Some(6));
|
|
1182
|
+
}
|
|
927
1183
|
}
|