omnizip 0.3.8 → 0.3.10

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.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +51 -50
  3. data/Gemfile +2 -0
  4. data/Rakefile +3 -0
  5. data/TODO.refactor/00-overview.md +67 -0
  6. data/TODO.refactor/01-registry-base-class.md +81 -0
  7. data/TODO.refactor/02-error-hierarchy.md +57 -0
  8. data/TODO.refactor/03-autoload-entry-point.md +69 -0
  9. data/TODO.refactor/04-filter-base-consolidation.md +49 -0
  10. data/TODO.refactor/05-send-private-methods.md +64 -0
  11. data/TODO.refactor/06-instance-variable-access.md +69 -0
  12. data/TODO.refactor/07-respond-to-replacement.md +91 -0
  13. data/TODO.refactor/08-spec-doubles.md +27 -0
  14. data/TODO.refactor/09-cli-shared-module.md +35 -0
  15. data/TODO.refactor/10-format-detector-ocp.md +34 -0
  16. data/TODO.refactor/11-convenience-decoupling.md +38 -0
  17. data/TODO.refactor/12-thread-safety.md +8 -0
  18. data/TODO.refactor/13-lutaml-model-migration.md +82 -0
  19. data/TODO.refactor/14-add-missing-specs.md +25 -0
  20. data/lib/omnizip/algorithm.rb +0 -2
  21. data/lib/omnizip/algorithm_registry.rb +29 -57
  22. data/lib/omnizip/algorithms/lzma/bit_model.rb +1 -1
  23. data/lib/omnizip/algorithms/lzma/dictionary.rb +4 -3
  24. data/lib/omnizip/algorithms/lzma/distance_coder.rb +5 -102
  25. data/lib/omnizip/algorithms/lzma/length_coder.rb +4 -92
  26. data/lib/omnizip/algorithms/lzma/literal_decoder.rb +4 -119
  27. data/lib/omnizip/algorithms/lzma/lzip_decoder.rb +2 -0
  28. data/lib/omnizip/algorithms/lzma/match_finder.rb +59 -29
  29. data/lib/omnizip/algorithms/lzma/optimal_encoder.rb +20 -12
  30. data/lib/omnizip/algorithms/lzma/range_decoder.rb +77 -269
  31. data/lib/omnizip/algorithms/lzma/range_encoder.rb +0 -9
  32. data/lib/omnizip/algorithms/lzma/xz_range_encoder_exact.rb +0 -15
  33. data/lib/omnizip/algorithms/lzma/xz_state.rb +0 -8
  34. data/lib/omnizip/algorithms/lzma/xz_utils_decoder.rb +189 -968
  35. data/lib/omnizip/algorithms/lzma2/encoder.rb +0 -2
  36. data/lib/omnizip/algorithms/lzma2/lzma2_chunk.rb +0 -2
  37. data/lib/omnizip/algorithms/lzma2/properties.rb +0 -2
  38. data/lib/omnizip/algorithms/ppmd7/model.rb +1 -1
  39. data/lib/omnizip/algorithms/ppmd8/context.rb +0 -2
  40. data/lib/omnizip/algorithms/ppmd8/model.rb +1 -1
  41. data/lib/omnizip/algorithms/ppmd8.rb +0 -2
  42. data/lib/omnizip/algorithms/ppmd_base.rb +0 -2
  43. data/lib/omnizip/algorithms/sevenzip_lzma2.rb +0 -5
  44. data/lib/omnizip/algorithms/xz_lzma2.rb +0 -5
  45. data/lib/omnizip/algorithms/zstandard/frame/header.rb +3 -5
  46. data/lib/omnizip/algorithms.rb +1 -0
  47. data/lib/omnizip/archive_handler.rb +75 -0
  48. data/lib/omnizip/archive_handlers/tar_handler.rb +51 -0
  49. data/lib/omnizip/archive_handlers/zip_handler.rb +73 -0
  50. data/lib/omnizip/archive_handlers.rb +11 -0
  51. data/lib/omnizip/checksum_registry.rb +23 -96
  52. data/lib/omnizip/cli/shared.rb +31 -0
  53. data/lib/omnizip/cli.rb +6 -43
  54. data/lib/omnizip/commands/archive_create_command.rb +0 -6
  55. data/lib/omnizip/commands/archive_extract_command.rb +0 -4
  56. data/lib/omnizip/commands/archive_list_command.rb +0 -4
  57. data/lib/omnizip/commands/archive_repair_command.rb +0 -2
  58. data/lib/omnizip/commands/archive_verify_command.rb +0 -2
  59. data/lib/omnizip/commands/compress_command.rb +0 -2
  60. data/lib/omnizip/commands/decompress_command.rb +0 -2
  61. data/lib/omnizip/commands/list_command.rb +0 -2
  62. data/lib/omnizip/commands/metadata_command.rb +0 -5
  63. data/lib/omnizip/commands/parity_create_command.rb +0 -2
  64. data/lib/omnizip/commands/parity_repair_command.rb +0 -2
  65. data/lib/omnizip/commands/parity_verify_command.rb +0 -2
  66. data/lib/omnizip/commands/profile_list_command.rb +0 -2
  67. data/lib/omnizip/commands/profile_show_command.rb +0 -2
  68. data/lib/omnizip/convenience.rb +111 -234
  69. data/lib/omnizip/converter/seven_zip_to_zip_strategy.rb +1 -1
  70. data/lib/omnizip/crypto/aes256/cipher.rb +6 -1
  71. data/lib/omnizip/error.rb +22 -18
  72. data/lib/omnizip/filter.rb +5 -3
  73. data/lib/omnizip/filter_registry.rb +74 -98
  74. data/lib/omnizip/filters/bcj2/decoder.rb +196 -130
  75. data/lib/omnizip/filters/bcj2.rb +0 -2
  76. data/lib/omnizip/filters/bcj_x86.rb +0 -2
  77. data/lib/omnizip/filters/filter_base.rb +4 -70
  78. data/lib/omnizip/filters/registry.rb +0 -2
  79. data/lib/omnizip/format_detector.rb +21 -12
  80. data/lib/omnizip/format_registry.rb +79 -38
  81. data/lib/omnizip/formats/bzip2_file.rb +0 -2
  82. data/lib/omnizip/formats/cpio/entry.rb +0 -2
  83. data/lib/omnizip/formats/cpio/reader.rb +0 -1
  84. data/lib/omnizip/formats/cpio/writer.rb +0 -1
  85. data/lib/omnizip/formats/cpio.rb +0 -1
  86. data/lib/omnizip/formats/format_spec_loader.rb +1 -1
  87. data/lib/omnizip/formats/gzip.rb +0 -2
  88. data/lib/omnizip/formats/iso/directory_record.rb +6 -3
  89. data/lib/omnizip/formats/iso/reader.rb +2 -2
  90. data/lib/omnizip/formats/iso.rb +0 -1
  91. data/lib/omnizip/formats/lzip.rb +5 -25
  92. data/lib/omnizip/formats/lzma_alone.rb +7 -25
  93. data/lib/omnizip/formats/msi/cab_extractor.rb +181 -0
  94. data/lib/omnizip/formats/msi/constants.rb +157 -0
  95. data/lib/omnizip/formats/msi/directory_resolver.rb +188 -0
  96. data/lib/omnizip/formats/msi/entry.rb +128 -0
  97. data/lib/omnizip/formats/msi/reader.rb +301 -0
  98. data/lib/omnizip/formats/msi/string_pool.rb +246 -0
  99. data/lib/omnizip/formats/msi/table_parser.rb +244 -0
  100. data/lib/omnizip/formats/msi.rb +84 -0
  101. data/lib/omnizip/formats/ole/allocation_table.rb +10 -1
  102. data/lib/omnizip/formats/ole/dirent.rb +20 -3
  103. data/lib/omnizip/formats/ole/header.rb +0 -1
  104. data/lib/omnizip/formats/ole/ranges_io.rb +21 -3
  105. data/lib/omnizip/formats/ole/storage.rb +19 -4
  106. data/lib/omnizip/formats/ole.rb +0 -2
  107. data/lib/omnizip/formats/rar/block_parser.rb +0 -1
  108. data/lib/omnizip/formats/rar/compression/ppmd/encoder.rb +1 -1
  109. data/lib/omnizip/formats/rar/decompressor.rb +38 -1
  110. data/lib/omnizip/formats/rar/header.rb +0 -1
  111. data/lib/omnizip/formats/rar/rar5/writer.rb +0 -2
  112. data/lib/omnizip/formats/rar/reader.rb +2 -3
  113. data/lib/omnizip/formats/rar/writer.rb +0 -1
  114. data/lib/omnizip/formats/rar.rb +2 -2
  115. data/lib/omnizip/formats/rpm/entry.rb +0 -1
  116. data/lib/omnizip/formats/rpm/header.rb +3 -8
  117. data/lib/omnizip/formats/rpm/lead.rb +37 -26
  118. data/lib/omnizip/formats/rpm/tag.rb +0 -1
  119. data/lib/omnizip/formats/rpm/writer.rb +0 -1
  120. data/lib/omnizip/formats/seven_zip/bcj2_stream_decompressor.rb +98 -61
  121. data/lib/omnizip/formats/seven_zip/coder_chain.rb +1 -2
  122. data/lib/omnizip/formats/seven_zip/encoded_header.rb +0 -1
  123. data/lib/omnizip/formats/seven_zip/encrypted_header.rb +0 -1
  124. data/lib/omnizip/formats/seven_zip/header.rb +0 -1
  125. data/lib/omnizip/formats/seven_zip/header_encryptor.rb +0 -1
  126. data/lib/omnizip/formats/seven_zip/header_writer.rb +22 -61
  127. data/lib/omnizip/formats/seven_zip/models/file_entry.rb +1 -1
  128. data/lib/omnizip/formats/seven_zip/parser.rb +14 -13
  129. data/lib/omnizip/formats/seven_zip/reader.rb +82 -19
  130. data/lib/omnizip/formats/seven_zip/split_archive_reader.rb +0 -15
  131. data/lib/omnizip/formats/seven_zip/split_archive_writer.rb +0 -1
  132. data/lib/omnizip/formats/seven_zip/writer.rb +22 -68
  133. data/lib/omnizip/formats/seven_zip.rb +63 -5
  134. data/lib/omnizip/formats/tar/entry.rb +0 -2
  135. data/lib/omnizip/formats/tar/header.rb +0 -2
  136. data/lib/omnizip/formats/tar/reader.rb +0 -2
  137. data/lib/omnizip/formats/tar/writer.rb +0 -2
  138. data/lib/omnizip/formats/tar.rb +0 -1
  139. data/lib/omnizip/formats/xar/entry.rb +0 -1
  140. data/lib/omnizip/formats/xar/header.rb +0 -1
  141. data/lib/omnizip/formats/xar/reader.rb +0 -1
  142. data/lib/omnizip/formats/xar/toc.rb +3 -3
  143. data/lib/omnizip/formats/xar/writer.rb +11 -3
  144. data/lib/omnizip/formats/xar.rb +0 -1
  145. data/lib/omnizip/formats/xz/reader.rb +7 -3
  146. data/lib/omnizip/formats/xz.rb +3 -40
  147. data/lib/omnizip/formats/xz_impl/block_decoder.rb +4 -10
  148. data/lib/omnizip/formats/xz_impl/block_encoder.rb +0 -2
  149. data/lib/omnizip/formats/xz_impl/block_header_parser.rb +0 -2
  150. data/lib/omnizip/formats/xz_impl/index_decoder.rb +0 -1
  151. data/lib/omnizip/formats/xz_impl/index_encoder.rb +0 -1
  152. data/lib/omnizip/formats/xz_impl/stream_encoder.rb +1 -1
  153. data/lib/omnizip/formats/xz_impl/stream_footer.rb +0 -1
  154. data/lib/omnizip/formats/xz_impl/stream_footer_parser.rb +0 -1
  155. data/lib/omnizip/formats/xz_impl/stream_header.rb +0 -1
  156. data/lib/omnizip/formats/xz_impl/stream_header_parser.rb +0 -2
  157. data/lib/omnizip/formats/zip/central_directory_header.rb +2 -3
  158. data/lib/omnizip/formats/zip/end_of_central_directory.rb +0 -2
  159. data/lib/omnizip/formats/zip/local_file_header.rb +0 -2
  160. data/lib/omnizip/formats/zip/reader.rb +12 -2
  161. data/lib/omnizip/formats/zip/writer.rb +73 -1
  162. data/lib/omnizip/formats/zip/zip64_end_of_central_directory.rb +0 -2
  163. data/lib/omnizip/formats/zip/zip64_end_of_central_directory_locator.rb +0 -2
  164. data/lib/omnizip/formats/zip/zip64_extra_field.rb +0 -2
  165. data/lib/omnizip/formats/zip.rb +0 -1
  166. data/lib/omnizip/formats.rb +9 -22
  167. data/lib/omnizip/implementations/seven_zip/lzma/range_decoder.rb +2 -2
  168. data/lib/omnizip/implementations/seven_zip/lzma/range_encoder.rb +1 -1
  169. data/lib/omnizip/implementations/seven_zip/lzma2/encoder.rb +0 -6
  170. data/lib/omnizip/implementations/xz_utils/lzma2/decoder.rb +18 -185
  171. data/lib/omnizip/implementations/xz_utils/lzma2/encoder.rb +3 -3
  172. data/lib/omnizip/io/source.rb +136 -0
  173. data/lib/omnizip/io.rb +2 -0
  174. data/lib/omnizip/link_handler.rb +7 -3
  175. data/lib/omnizip/metadata/metadata_editor.rb +1 -1
  176. data/lib/omnizip/models/algorithm_metadata.rb +33 -60
  177. data/lib/omnizip/models/compression_options.rb +40 -53
  178. data/lib/omnizip/models/conversion_options.rb +9 -18
  179. data/lib/omnizip/models/parallel_options.rb +25 -0
  180. data/lib/omnizip/optimization_registry.rb +8 -44
  181. data/lib/omnizip/parallel/job_scheduler.rb +26 -8
  182. data/lib/omnizip/parallel/parallel_compressor.rb +10 -20
  183. data/lib/omnizip/parallel/parallel_extractor.rb +20 -11
  184. data/lib/omnizip/parity/par2_repairer.rb +5 -5
  185. data/lib/omnizip/parity/par2_verifier.rb +27 -22
  186. data/lib/omnizip/parity.rb +4 -4
  187. data/lib/omnizip/password/encryption_registry.rb +11 -50
  188. data/lib/omnizip/password/zip_crypto_strategy.rb +1 -4
  189. data/lib/omnizip/pipe/stream_decompressor.rb +2 -3
  190. data/lib/omnizip/profile/archive_profile.rb +0 -2
  191. data/lib/omnizip/profile/binary_profile.rb +0 -2
  192. data/lib/omnizip/profile/compression_profile.rb +0 -1
  193. data/lib/omnizip/profile/profile_detector.rb +0 -2
  194. data/lib/omnizip/profile/text_profile.rb +0 -2
  195. data/lib/omnizip/profile.rb +0 -2
  196. data/lib/omnizip/registry.rb +112 -0
  197. data/lib/omnizip/version.rb +1 -1
  198. data/lib/omnizip/zip/file.rb +20 -9
  199. data/lib/omnizip.rb +26 -55
  200. data/lib/tasks/msi_test_files.rake +66 -0
  201. metadata +57 -6
@@ -17,8 +17,6 @@ module Omnizip
17
17
  #
18
18
  # @param archive_path [String] Path to archive
19
19
  def run(archive_path)
20
- require "omnizip/formats/rar"
21
-
22
20
  unless File.exist?(archive_path)
23
21
  raise "Archive not found: #{archive_path}"
24
22
  end
@@ -16,8 +16,6 @@
16
16
  # See the COPYING file for the complete text of the license.
17
17
  #
18
18
 
19
- require "omnizip/cli/output_formatter"
20
-
21
19
  module Omnizip
22
20
  module Commands
23
21
  # Command to compress files using specified algorithm.
@@ -16,8 +16,6 @@
16
16
  # See the COPYING file for the complete text of the license.
17
17
  #
18
18
 
19
- require "omnizip/cli/output_formatter"
20
-
21
19
  module Omnizip
22
20
  module Commands
23
21
  # Command to decompress files using specified algorithm.
@@ -16,8 +16,6 @@
16
16
  # See the COPYING file for the complete text of the license.
17
17
  #
18
18
 
19
- require "omnizip/cli/output_formatter"
20
-
21
19
  module Omnizip
22
20
  module Commands
23
21
  # Command to list available compression algorithms.
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "omnizip/cli/output_formatter"
4
-
5
3
  module Omnizip
6
4
  module Commands
7
5
  # Metadata command implementation
@@ -16,9 +14,6 @@ module Omnizip
16
14
  # @param archive_path [String] Path to archive
17
15
  # @param pattern [String, nil] Optional pattern to match entries
18
16
  def run(archive_path, pattern = nil)
19
- require "omnizip/zip/file"
20
- require "omnizip/metadata"
21
-
22
17
  unless File.exist?(archive_path)
23
18
  raise Errno::ENOENT, "Archive not found: #{archive_path}"
24
19
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "omnizip/parity"
4
-
5
3
  module Omnizip
6
4
  module Commands
7
5
  # Command to create PAR2 parity files
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "omnizip/parity"
4
-
5
3
  module Omnizip
6
4
  module Commands
7
5
  # Command to repair files using PAR2
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "omnizip/parity"
4
-
5
3
  module Omnizip
6
4
  module Commands
7
5
  # Command to verify files using PAR2
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "omnizip/cli/output_formatter"
4
-
5
3
  module Omnizip
6
4
  module Commands
7
5
  # Command to list available compression profiles
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "omnizip/cli/output_formatter"
4
-
5
3
  module Omnizip
6
4
  module Commands
7
5
  # Command to show details of a specific compression profile
@@ -1,309 +1,191 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omnizip
4
- # Convenience methods for common archive operations
4
+ # Convenience methods for common archive operations.
5
+ #
6
+ # All public methods accept an optional +format:+ keyword (default
7
+ # +:zip+) that selects the underlying archive handler via
8
+ # +Omnizip::ArchiveHandler+. New formats gain access to the convenience
9
+ # API by registering a handler; no edits to this file are needed.
5
10
  module Convenience
6
- # Compress a single file to a ZIP archive
7
- # @param input_path [String] Path to input file
8
- # @param output_path [String] Path to output ZIP file
9
- # @param options [Hash] Compression options
10
- # @option options [Symbol, Omnizip::Profile::CompressionProfile] :profile
11
- # Compression profile (:fast, :balanced, :maximum, :text, :binary, :archive, :auto)
12
- # @option options [Symbol] :compression Compression method (:deflate, :bzip2, :lzma, :zstandard)
13
- # @option options [Integer] :level Compression level (1-9)
14
- # @option options [Boolean] :chunked Use chunked processing for large files
15
- # @option options [Integer] :chunk_size Chunk size in bytes (default: 64MB)
16
- # @option options [Integer] :max_memory Maximum memory usage (default: 256MB)
17
- # @option options [Proc] :progress Progress callback
18
- # @return [String] Path to created archive
11
+ DEFAULT_FORMAT = :zip
12
+
13
+ # Compress a single file into an archive.
19
14
  #
20
- # @example
21
- # Omnizip.compress_file('document.txt', 'document.zip')
22
- # Omnizip.compress_file('image.png', 'image.zip', compression: :lzma, level: 9)
23
- # Omnizip.compress_file('huge.dat', 'huge.zip', chunked: true, max_memory: 128.megabytes)
24
- # Omnizip.compress_file('data.txt', 'data.zip', profile: :fast)
25
- # Omnizip.compress_file('app.exe', 'app.zip', profile: :auto)
26
- def compress_file(input_path, output_path, **options)
15
+ # @param input_path [String] Path to input file
16
+ # @param output_path [String] Path to output archive
17
+ # @param format [Symbol] Archive format (default +:zip+)
18
+ # @param options [Hash] Compression / format-specific options
19
+ # @return [String] +output_path+
20
+ def compress_file(input_path, output_path, format: DEFAULT_FORMAT, **options)
27
21
  unless ::File.exist?(input_path)
28
- raise Errno::ENOENT,
29
- "Input file not found: #{input_path}"
22
+ raise Errno::ENOENT, "Input file not found: #{input_path}"
30
23
  end
31
24
  if ::File.directory?(input_path)
32
- raise ArgumentError,
33
- "Input is a directory: #{input_path}"
25
+ raise ArgumentError, "Input is a directory: #{input_path}"
34
26
  end
35
27
 
36
- # Apply profile settings if specified
37
28
  options = apply_profile(input_path, options) if options[:profile]
38
29
 
39
- # Use chunked processing for large files if requested
40
30
  if options[:chunked]
41
- return Omnizip::Chunked.compress_file(input_path, output_path,
42
- **options)
31
+ return Omnizip::Chunked.compress_file(input_path, output_path, **options)
43
32
  end
44
33
 
45
- Omnizip::Zip::File.create(output_path) do |zip|
46
- basename = ::File.basename(input_path)
47
- zip.add(basename, input_path)
34
+ Omnizip::ArchiveHandler.for(format).create(output_path) do |archive|
35
+ archive.add(::File.basename(input_path), input_path)
48
36
  end
49
37
 
50
38
  output_path
51
39
  end
52
40
 
53
- # Compress a directory to a ZIP archive
54
- # @param input_dir [String] Path to input directory
55
- # @param output_path [String] Path to output ZIP file
56
- # @param options [Hash] Compression options
57
- # @option options [Symbol, Omnizip::Profile::CompressionProfile] :profile
58
- # Compression profile (:fast, :balanced, :maximum, etc.)
59
- # @option options [Symbol] :compression Compression method
60
- # @option options [Integer] :level Compression level (1-9)
61
- # @option options [Boolean] :recursive Include subdirectories (default: true)
62
- # @option options [Integer] :max_memory Maximum memory usage for large files
63
- # @option options [Proc] :progress Progress callback
64
- # @return [String] Path to created archive
41
+ # Compress a directory into an archive.
65
42
  #
66
- # @example
67
- # Omnizip.compress_directory('project/', 'backup.zip')
68
- # Omnizip.compress_directory('src/', 'src.zip', compression: :lzma2, level: 9)
69
- # Omnizip.compress_directory('large/', 'backup.zip', max_memory: 256.megabytes)
70
- # Omnizip.compress_directory('src/', 'backup.7z', profile: :maximum)
71
- def compress_directory(input_dir, output_path, recursive: true, **options)
43
+ # @param input_dir [String] Path to input directory
44
+ # @param output_path [String] Path to output archive
45
+ # @param format [Symbol] Archive format (default +:zip+)
46
+ # @param recursive [Boolean] Include subdirectories (default +true+)
47
+ # @param options [Hash] Compression / format-specific options
48
+ # @return [String] +output_path+
49
+ def compress_directory(input_dir, output_path, format: DEFAULT_FORMAT,
50
+ recursive: true, **options)
72
51
  unless ::File.exist?(input_dir)
73
- raise Errno::ENOENT,
74
- "Input directory not found: #{input_dir}"
52
+ raise Errno::ENOENT, "Input directory not found: #{input_dir}"
75
53
  end
76
54
  unless ::File.directory?(input_dir)
77
- raise ArgumentError,
78
- "Input is not a directory: #{input_dir}"
55
+ raise ArgumentError, "Input is not a directory: #{input_dir}"
79
56
  end
80
57
 
81
- # Apply profile settings if specified (use first file for auto-detection)
82
58
  if options[:profile]
83
59
  first_file = find_first_file(input_dir)
84
60
  apply_profile(first_file, options)
85
61
  end
86
62
 
87
- Omnizip::Zip::File.create(output_path) do |zip|
88
- add_directory_contents(zip, input_dir, "", recursive: recursive)
63
+ Omnizip::ArchiveHandler.for(format).create(output_path) do |archive|
64
+ add_directory_contents(archive, input_dir, "", recursive: recursive)
89
65
  end
90
66
 
91
67
  output_path
92
68
  end
93
69
 
94
- # Extract a ZIP archive to a directory
95
- # @param archive_path [String] Path to ZIP archive
96
- # @param output_dir [String] Path to output directory
97
- # @param options [Hash] Extraction options
98
- # @option options [Boolean] :overwrite Overwrite existing files (default: false)
99
- # @return [Array<String>] List of extracted file paths
70
+ # Extract an archive to a directory.
100
71
  #
101
- # @example
102
- # Omnizip.extract_archive('backup.zip', 'restore/')
103
- # Omnizip.extract_archive('archive.zip', 'output/', overwrite: true)
104
- def extract_archive(archive_path, output_dir, overwrite: false, **_options)
105
- unless ::File.exist?(archive_path)
106
- raise Errno::ENOENT,
107
- "Archive not found: #{archive_path}"
108
- end
109
-
110
- extracted_files = []
111
-
112
- Omnizip::Zip::File.open(archive_path) do |zip|
113
- zip.each do |entry|
114
- dest_path = ::File.join(output_dir, entry.name)
115
-
116
- # Handle overwrite option
117
- on_exists = if overwrite
118
- proc { true }
119
- else
120
- proc { |_e, path| raise "File exists: #{path}" }
121
- end
122
-
123
- zip.extract(entry, dest_path, &on_exists)
124
- extracted_files << dest_path
125
- end
126
- end
127
-
128
- extracted_files
72
+ # @param archive_path [String] Path to archive
73
+ # @param output_dir [String] Output directory
74
+ # @param format [Symbol] Archive format (default +:zip+)
75
+ # @param overwrite [Boolean] Overwrite existing files (default +false+)
76
+ # @param options [Hash] Format-specific extraction options
77
+ # @return [Array<String>] Extracted file paths
78
+ def extract_archive(archive_path, output_dir, format: DEFAULT_FORMAT,
79
+ overwrite: false, **options)
80
+ require_archive!(archive_path)
81
+ Omnizip::ArchiveHandler.for(format)
82
+ .extract_to(archive_path, output_dir,
83
+ overwrite: overwrite, **options)
129
84
  end
130
85
 
131
- # List contents of a ZIP archive
132
- # @param archive_path [String] Path to ZIP archive
133
- # @param options [Hash] Listing options
134
- # @option options [Boolean] :details Include detailed information (default: false)
135
- # @return [Array<String>, Array<Hash>] List of entry names or detailed info
136
- #
137
- # @example
138
- # Omnizip.list_archive('backup.zip')
139
- # # => ["file1.txt", "file2.txt", "dir/"]
86
+ # List contents of an archive.
140
87
  #
141
- # Omnizip.list_archive('backup.zip', details: true)
142
- # # => [{name: "file1.txt", size: 1024, compressed_size: 512, ...}, ...]
143
- def list_archive(archive_path, details: false, **_options)
144
- unless ::File.exist?(archive_path)
145
- raise Errno::ENOENT,
146
- "Archive not found: #{archive_path}"
147
- end
148
-
149
- Omnizip::Zip::File.open(archive_path) do |zip|
150
- if details
151
- zip.entries.map do |entry|
152
- {
153
- name: entry.name,
154
- size: entry.size,
155
- compressed_size: entry.compressed_size,
156
- compression_method: entry.compression_method,
157
- crc: entry.crc,
158
- time: entry.time,
159
- directory: entry.directory?,
160
- }
161
- end
162
- else
163
- zip.names
164
- end
165
- end
88
+ # @param archive_path [String] Path to archive
89
+ # @param format [Symbol] Archive format (default +:zip+)
90
+ # @param details [Boolean] Include detailed info (default +false+)
91
+ # @param options [Hash] Format-specific listing options
92
+ # @return [Array<String>, Array<Hash>]
93
+ def list_archive(archive_path, format: DEFAULT_FORMAT, details: false,
94
+ **options)
95
+ require_archive!(archive_path)
96
+ Omnizip::ArchiveHandler.for(format)
97
+ .list(archive_path, details: details, **options)
166
98
  end
167
99
 
168
- # Read a single file from a ZIP archive
169
- # @param archive_path [String] Path to ZIP archive
170
- # @param entry_name [String] Name of entry to read
171
- # @return [String] Contents of the file
100
+ # Read a single entry from an archive.
172
101
  #
173
- # @example
174
- # content = Omnizip.read_from_archive('backup.zip', 'config.yml')
175
- def read_from_archive(archive_path, entry_name)
176
- unless ::File.exist?(archive_path)
177
- raise Errno::ENOENT,
178
- "Archive not found: #{archive_path}"
179
- end
180
-
181
- Omnizip::Zip::File.open(archive_path) do |zip|
182
- entry = zip.get_entry(entry_name)
183
- raise Errno::ENOENT, "Entry not found: #{entry_name}" unless entry
184
-
185
- zip.read(entry)
186
- end
102
+ # @param archive_path [String] Path to archive
103
+ # @param entry_name [String] Entry to read
104
+ # @param format [Symbol] Archive format (default +:zip+)
105
+ # @return [String] Entry contents
106
+ def read_from_archive(archive_path, entry_name, format: DEFAULT_FORMAT)
107
+ require_archive!(archive_path)
108
+ Omnizip::ArchiveHandler.for(format).read_entry(archive_path, entry_name)
187
109
  end
188
110
 
189
- # Add a file to an existing ZIP archive
190
- # @param archive_path [String] Path to ZIP archive
191
- # @param entry_name [String] Name for entry in archive
192
- # @param source_path [String] Path to source file
193
- # @return [String] Path to archive
111
+ # Add a file to an existing archive.
194
112
  #
195
- # @example
196
- # Omnizip.add_to_archive('backup.zip', 'new_file.txt', 'path/to/new_file.txt')
197
- def add_to_archive(archive_path, entry_name, source_path)
198
- unless ::File.exist?(archive_path)
199
- raise Errno::ENOENT,
200
- "Archive not found: #{archive_path}"
201
- end
113
+ # @param archive_path [String] Path to archive
114
+ # @param entry_name [String] Entry name in archive
115
+ # @param source_path [String] Path to source file
116
+ # @param format [Symbol] Archive format (default +:zip+)
117
+ # @return [String] +archive_path+
118
+ def add_to_archive(archive_path, entry_name, source_path,
119
+ format: DEFAULT_FORMAT)
120
+ require_archive!(archive_path)
202
121
  unless ::File.exist?(source_path)
203
- raise Errno::ENOENT,
204
- "Source file not found: #{source_path}"
122
+ raise Errno::ENOENT, "Source file not found: #{source_path}"
205
123
  end
206
124
 
207
- Omnizip::Zip::File.open(archive_path) do |zip|
208
- zip.add(entry_name, source_path)
125
+ handler = Omnizip::ArchiveHandler.for(format)
126
+ if handler.respond_to?(:add_entry)
127
+ handler.add_entry(archive_path, entry_name, source_path)
128
+ else
129
+ raise Omnizip::UnsupportedFormatError,
130
+ "Format #{format.inspect} does not support adding entries"
209
131
  end
210
132
 
211
133
  archive_path
212
134
  end
213
135
 
214
- # Remove a file from a ZIP archive
215
- # @param archive_path [String] Path to ZIP archive
216
- # @param entry_name [String] Name of entry to remove
217
- # @return [String] Path to archive
136
+ # Remove an entry from an existing archive.
218
137
  #
219
- # @example
220
- # Omnizip.remove_from_archive('backup.zip', 'old_file.txt')
221
- def remove_from_archive(archive_path, entry_name)
222
- unless ::File.exist?(archive_path)
223
- raise Errno::ENOENT,
224
- "Archive not found: #{archive_path}"
225
- end
226
-
227
- Omnizip::Zip::File.open(archive_path) do |zip|
228
- zip.remove(entry_name)
229
- end
230
-
138
+ # @param archive_path [String] Path to archive
139
+ # @param entry_name [String] Entry to remove
140
+ # @param format [Symbol] Archive format (default +:zip+)
141
+ # @return [String] +archive_path+
142
+ def remove_from_archive(archive_path, entry_name, format: DEFAULT_FORMAT)
143
+ require_archive!(archive_path)
144
+ handler = Omnizip::ArchiveHandler.for(format)
145
+ unless handler.respond_to?(:remove_entry)
146
+ raise Omnizip::UnsupportedFormatError,
147
+ "Format #{format.inspect} does not support removing entries"
148
+ end
149
+
150
+ handler.remove_entry(archive_path, entry_name)
231
151
  archive_path
232
152
  end
233
153
 
234
- # Create a RAR archive
235
- # @param archive_path [String] Path to output RAR file
236
- # @param options [Hash] Creation options
237
- # @option options [Integer] :version RAR version (4 or 5, default: 5 for pure Ruby)
238
- # @option options [Symbol] :compression For RAR5: :store, :lzma, :auto (default: :store)
239
- # @option options [Integer] :level For RAR5: LZMA compression level 1-5 (default: 3)
240
- # @option options [Boolean] :include_mtime Include modification time (RAR5 only, default: false)
241
- # @option options [Boolean] :include_crc32 Include CRC32 checksum (RAR5 only, default: false)
242
- # @yield [writer] RAR writer instance
243
- # @return [String] Path to created archive
244
- #
245
- # @example Create RAR5 archive with STORE compression
246
- # Omnizip.create_rar('backup.rar') do |rar|
247
- # rar.add_file('document.pdf')
248
- # end
249
- #
250
- # @example Create RAR5 with LZMA compression
251
- # Omnizip.create_rar('backup.rar', compression: :lzma, level: 5) do |rar|
252
- # rar.add_file('data.txt')
253
- # end
254
- #
255
- # @example Create RAR5 with optional fields
256
- # Omnizip.create_rar('backup.rar',
257
- # compression: :lzma,
258
- # level: 3,
259
- # include_mtime: true,
260
- # include_crc32: true
261
- # ) do |rar|
262
- # rar.add_file('important.doc')
263
- # end
154
+ # Create a RAR archive (requires RAR license — see NotLicensedError).
264
155
  # rubocop:disable Naming/BlockForwarding, Style/ArgumentsForwarding -- Ruby 3.0 compatibility
265
156
  def create_rar(archive_path, **options, &block)
266
- # Default to RAR5 (pure Ruby) unless explicitly specified
267
157
  options[:version] ||= 5
268
-
269
158
  Omnizip::Formats::Rar.create(archive_path, options, &block)
270
159
  end
271
160
  # rubocop:enable Naming/BlockForwarding, Style/ArgumentsForwarding
272
161
 
273
162
  private
274
163
 
275
- # Apply compression profile to options
276
- #
277
- # @param file_path [String, nil] File path for auto-detection
278
- # @param options [Hash] Compression options
279
- # @return [Hash] Updated options with profile settings
164
+ def require_archive!(archive_path)
165
+ return if ::File.exist?(archive_path)
166
+
167
+ raise Errno::ENOENT, "Archive not found: #{archive_path}"
168
+ end
169
+
170
+ # Apply compression profile to options.
280
171
  def apply_profile(file_path, options)
281
172
  profile_spec = options.delete(:profile)
282
173
  return options unless profile_spec
283
174
 
284
- # Get the profile
285
175
  profile = case profile_spec
286
176
  when :auto
287
- # Auto-detect based on file
288
177
  file_path ? Omnizip::Profile.detect(file_path) : Omnizip::Profile.get(:balanced)
289
178
  when Symbol
290
- # Get by name
291
179
  Omnizip::Profile.get(profile_spec) || Omnizip::Profile.get(:balanced)
292
180
  when Omnizip::Profile::CompressionProfile
293
- # Use the profile directly
294
181
  profile_spec
295
182
  else
296
183
  Omnizip::Profile.get(:balanced)
297
184
  end
298
185
 
299
- # Apply profile to options
300
186
  profile.apply_to(options)
301
187
  end
302
188
 
303
- # Find first file in directory for profile detection
304
- #
305
- # @param dir_path [String] Directory path
306
- # @return [String, nil] Path to first file or nil
307
189
  def find_first_file(dir_path)
308
190
  Dir.foreach(dir_path) do |entry|
309
191
  next if [".", ".."].include?(entry)
@@ -311,7 +193,6 @@ module Omnizip
311
193
  full_path = ::File.join(dir_path, entry)
312
194
  return full_path if ::File.file?(full_path)
313
195
 
314
- # Check subdirectories
315
196
  if ::File.directory?(full_path)
316
197
  result = find_first_file(full_path)
317
198
  return result if result
@@ -320,16 +201,17 @@ module Omnizip
320
201
  nil
321
202
  end
322
203
 
323
- # Recursively add directory contents to archive
324
- def add_directory_contents(zip, base_dir, relative_path, recursive: true)
204
+ # Recursively add directory contents to an archive. The +archive+
205
+ # argument is whatever the handler's +#create+ block yields (e.g.
206
+ # +Omnizip::Zip::File+ or +Omnizip::Formats::Tar::Writer+) and must
207
+ # respond to +#add(entry_name, src_path = nil)+.
208
+ def add_directory_contents(archive, base_dir, relative_path, recursive: true)
325
209
  dir_path = ::File.join(base_dir, relative_path)
326
210
 
327
211
  Dir.foreach(dir_path) do |entry|
328
212
  next if [".", ".."].include?(entry)
329
213
 
330
214
  full_path = ::File.join(dir_path, entry)
331
-
332
- # Create archive path (strip leading separator if at root)
333
215
  archive_path = if relative_path.empty?
334
216
  entry
335
217
  else
@@ -337,23 +219,18 @@ module Omnizip
337
219
  end
338
220
 
339
221
  if ::File.directory?(full_path)
340
- # Add directory entry with trailing slash
341
222
  dir_entry_name = archive_path.end_with?("/") ? archive_path : "#{archive_path}/"
342
- zip.add(dir_entry_name)
343
-
344
- # Recursively add contents if requested
223
+ archive.add(dir_entry_name)
345
224
  if recursive
346
- add_directory_contents(zip, base_dir, archive_path,
225
+ add_directory_contents(archive, base_dir, archive_path,
347
226
  recursive: recursive)
348
227
  end
349
228
  else
350
- # Add file with its data
351
- zip.add(archive_path, full_path)
229
+ archive.add(archive_path, full_path)
352
230
  end
353
231
  end
354
232
  end
355
233
  end
356
234
 
357
- # Extend Omnizip module with convenience methods
358
235
  extend Convenience
359
236
  end
@@ -59,7 +59,7 @@ module Omnizip
59
59
  # Extract entry data
60
60
  unless entry.name.end_with?("/")
61
61
  File.open(source_path, "rb") do |io|
62
- data[:content] = reader.send(:extract_entry_data, io, entry)
62
+ data[:content] = reader.extract_entry_data(io, entry)
63
63
  end
64
64
  end
65
65
 
@@ -87,7 +87,12 @@ module Omnizip
87
87
  # @return [OpenSSL::Cipher] Configured cipher
88
88
  def create_cipher(mode)
89
89
  cipher = OpenSSL::Cipher.new("AES-256-CBC")
90
- cipher.send(mode) # Call encrypt or decrypt first
90
+ case mode
91
+ when :encrypt then cipher.encrypt
92
+ when :decrypt then cipher.decrypt
93
+ else
94
+ raise ArgumentError, "Unknown cipher mode: #{mode.inspect}"
95
+ end
91
96
  cipher.key = @key # Then set key
92
97
  cipher.iv = @iv # Then set IV
93
98
  cipher.padding = 1 # PKCS7 padding