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 CHANGED
@@ -8,6 +8,7 @@ rust-version.workspace = true
8
8
  license.workspace = true
9
9
  repository.workspace = true
10
10
  homepage.workspace = true
11
+ publish = false
11
12
 
12
13
  [lib]
13
14
  crate-type = ["cdylib"]
package/README.md CHANGED
@@ -34,7 +34,7 @@ bun add exarch-rs
34
34
 
35
35
  ## Requirements
36
36
 
37
- - Node.js >= 14
37
+ - Node.js >= 18
38
38
 
39
39
  ## Quick Start
40
40
 
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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "exarch-rs",
3
- "version": "0.1.0",
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
- "@napi-rs/cli": "^3.0.0"
51
+ "@biomejs/biome": "^2.0",
52
+ "@napi-rs/cli": "^3.5"
33
53
  },
34
54
  "engines": {
35
- "node": ">= 14"
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 max_file_size(&mut self, size: i64) -> Result<&Self> {
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 max_total_size(&mut self, size: i64) -> Result<&Self> {
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 max_compression_ratio(&mut self, ratio: f64) -> Result<&Self> {
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 max_file_count(&mut self, count: u32) -> &Self {
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 max_path_depth(&mut self, depth: u32) -> &Self {
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 allow_symlinks(&mut self, allow: Option<bool>) -> &Self {
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 allow_hardlinks(&mut self, allow: Option<bool>) -> &Self {
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 allow_absolute_paths(&mut self, allow: Option<bool>) -> &Self {
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 allow_world_writable(&mut self, allow: Option<bool>) -> &Self {
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 preserve_permissions(&mut self, preserve: Option<bool>) -> &Self {
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.max_file_size(100_000_000).unwrap();
404
- config.max_total_size(1_000_000_000).unwrap();
405
- config.max_file_count(50_000);
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.max_compression_ratio(200.0);
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.max_compression_ratio(f64::NAN);
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.max_compression_ratio(f64::INFINITY);
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.max_compression_ratio(-10.0);
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.max_compression_ratio(0.0);
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.max_file_size(-1);
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.max_file_size(i64::MAX);
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.max_total_size(-1);
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.max_total_size(i64::MAX);
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.max_file_count(u32::MAX);
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.max_path_depth(u32::MAX);
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.max_file_size(0);
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.max_file_size(100_000_000).unwrap();
605
- config.max_total_size(1_000_000_000).unwrap();
606
- config.max_compression_ratio(250.0).unwrap();
607
- config.max_file_count(50_000);
608
- config.max_path_depth(64);
609
- config.preserve_permissions(Some(true));
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.max_file_size(100_000_000).unwrap();
792
- config.max_total_size(1_000_000_000).unwrap();
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.max_compression_ratio(f64::NEG_INFINITY);
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.max_compression_ratio(0.000001);
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.max_compression_ratio(1_000_000.0);
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.max_file_size(100_000_000).unwrap();
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
  }