omnizip 0.3.9 → 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 (182) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.refactor/00-overview.md +67 -0
  3. data/TODO.refactor/01-registry-base-class.md +81 -0
  4. data/TODO.refactor/02-error-hierarchy.md +57 -0
  5. data/TODO.refactor/03-autoload-entry-point.md +69 -0
  6. data/TODO.refactor/04-filter-base-consolidation.md +49 -0
  7. data/TODO.refactor/05-send-private-methods.md +64 -0
  8. data/TODO.refactor/06-instance-variable-access.md +69 -0
  9. data/TODO.refactor/07-respond-to-replacement.md +91 -0
  10. data/TODO.refactor/08-spec-doubles.md +27 -0
  11. data/TODO.refactor/09-cli-shared-module.md +35 -0
  12. data/TODO.refactor/10-format-detector-ocp.md +34 -0
  13. data/TODO.refactor/11-convenience-decoupling.md +38 -0
  14. data/TODO.refactor/12-thread-safety.md +8 -0
  15. data/TODO.refactor/13-lutaml-model-migration.md +82 -0
  16. data/TODO.refactor/14-add-missing-specs.md +25 -0
  17. data/lib/omnizip/algorithm.rb +0 -2
  18. data/lib/omnizip/algorithm_registry.rb +29 -57
  19. data/lib/omnizip/algorithms/lzma/dictionary.rb +4 -3
  20. data/lib/omnizip/algorithms/lzma/lzip_decoder.rb +2 -0
  21. data/lib/omnizip/algorithms/lzma/range_decoder.rb +1 -1
  22. data/lib/omnizip/algorithms/lzma/xz_utils_decoder.rb +6 -5
  23. data/lib/omnizip/algorithms/lzma2/encoder.rb +0 -2
  24. data/lib/omnizip/algorithms/lzma2/lzma2_chunk.rb +0 -2
  25. data/lib/omnizip/algorithms/lzma2/properties.rb +0 -2
  26. data/lib/omnizip/algorithms/ppmd7/model.rb +1 -1
  27. data/lib/omnizip/algorithms/ppmd8/context.rb +0 -2
  28. data/lib/omnizip/algorithms/ppmd8/model.rb +1 -1
  29. data/lib/omnizip/algorithms/ppmd8.rb +0 -2
  30. data/lib/omnizip/algorithms/ppmd_base.rb +0 -2
  31. data/lib/omnizip/algorithms/sevenzip_lzma2.rb +0 -5
  32. data/lib/omnizip/algorithms/xz_lzma2.rb +0 -5
  33. data/lib/omnizip/algorithms/zstandard/frame/header.rb +3 -5
  34. data/lib/omnizip/algorithms.rb +1 -0
  35. data/lib/omnizip/archive_handler.rb +75 -0
  36. data/lib/omnizip/archive_handlers/tar_handler.rb +51 -0
  37. data/lib/omnizip/archive_handlers/zip_handler.rb +73 -0
  38. data/lib/omnizip/archive_handlers.rb +11 -0
  39. data/lib/omnizip/checksum_registry.rb +23 -96
  40. data/lib/omnizip/cli/shared.rb +31 -0
  41. data/lib/omnizip/cli.rb +6 -43
  42. data/lib/omnizip/commands/archive_create_command.rb +0 -6
  43. data/lib/omnizip/commands/archive_extract_command.rb +0 -4
  44. data/lib/omnizip/commands/archive_list_command.rb +0 -4
  45. data/lib/omnizip/commands/archive_repair_command.rb +0 -2
  46. data/lib/omnizip/commands/archive_verify_command.rb +0 -2
  47. data/lib/omnizip/commands/compress_command.rb +0 -2
  48. data/lib/omnizip/commands/decompress_command.rb +0 -2
  49. data/lib/omnizip/commands/list_command.rb +0 -2
  50. data/lib/omnizip/commands/metadata_command.rb +0 -5
  51. data/lib/omnizip/commands/parity_create_command.rb +0 -2
  52. data/lib/omnizip/commands/parity_repair_command.rb +0 -2
  53. data/lib/omnizip/commands/parity_verify_command.rb +0 -2
  54. data/lib/omnizip/commands/profile_list_command.rb +0 -2
  55. data/lib/omnizip/commands/profile_show_command.rb +0 -2
  56. data/lib/omnizip/convenience.rb +111 -234
  57. data/lib/omnizip/converter/seven_zip_to_zip_strategy.rb +1 -1
  58. data/lib/omnizip/crypto/aes256/cipher.rb +6 -1
  59. data/lib/omnizip/error.rb +22 -18
  60. data/lib/omnizip/filter.rb +5 -3
  61. data/lib/omnizip/filter_registry.rb +74 -98
  62. data/lib/omnizip/filters/bcj2.rb +0 -2
  63. data/lib/omnizip/filters/bcj_x86.rb +0 -2
  64. data/lib/omnizip/filters/filter_base.rb +4 -70
  65. data/lib/omnizip/filters/registry.rb +0 -2
  66. data/lib/omnizip/format_detector.rb +21 -12
  67. data/lib/omnizip/format_registry.rb +69 -53
  68. data/lib/omnizip/formats/bzip2_file.rb +0 -2
  69. data/lib/omnizip/formats/cpio/entry.rb +0 -2
  70. data/lib/omnizip/formats/cpio/reader.rb +0 -1
  71. data/lib/omnizip/formats/cpio/writer.rb +0 -1
  72. data/lib/omnizip/formats/cpio.rb +0 -1
  73. data/lib/omnizip/formats/format_spec_loader.rb +1 -1
  74. data/lib/omnizip/formats/gzip.rb +0 -2
  75. data/lib/omnizip/formats/iso/directory_record.rb +6 -3
  76. data/lib/omnizip/formats/iso/reader.rb +2 -2
  77. data/lib/omnizip/formats/iso.rb +0 -1
  78. data/lib/omnizip/formats/lzip.rb +5 -25
  79. data/lib/omnizip/formats/lzma_alone.rb +7 -25
  80. data/lib/omnizip/formats/msi/reader.rb +1 -0
  81. data/lib/omnizip/formats/msi/string_pool.rb +2 -3
  82. data/lib/omnizip/formats/msi.rb +1 -4
  83. data/lib/omnizip/formats/ole/allocation_table.rb +0 -1
  84. data/lib/omnizip/formats/ole/dirent.rb +9 -4
  85. data/lib/omnizip/formats/ole/header.rb +0 -1
  86. data/lib/omnizip/formats/ole/ranges_io.rb +1 -1
  87. data/lib/omnizip/formats/ole/storage.rb +5 -6
  88. data/lib/omnizip/formats/ole.rb +0 -1
  89. data/lib/omnizip/formats/rar/block_parser.rb +0 -1
  90. data/lib/omnizip/formats/rar/compression/ppmd/encoder.rb +1 -1
  91. data/lib/omnizip/formats/rar/decompressor.rb +1 -0
  92. data/lib/omnizip/formats/rar/header.rb +0 -1
  93. data/lib/omnizip/formats/rar/rar5/writer.rb +0 -3
  94. data/lib/omnizip/formats/rar/reader.rb +2 -3
  95. data/lib/omnizip/formats/rar/writer.rb +0 -1
  96. data/lib/omnizip/formats/rar.rb +0 -1
  97. data/lib/omnizip/formats/rpm/entry.rb +0 -1
  98. data/lib/omnizip/formats/rpm/header.rb +1 -6
  99. data/lib/omnizip/formats/rpm/lead.rb +37 -26
  100. data/lib/omnizip/formats/rpm/tag.rb +0 -1
  101. data/lib/omnizip/formats/rpm/writer.rb +0 -1
  102. data/lib/omnizip/formats/seven_zip/bcj2_stream_decompressor.rb +0 -1
  103. data/lib/omnizip/formats/seven_zip/coder_chain.rb +1 -2
  104. data/lib/omnizip/formats/seven_zip/encoded_header.rb +0 -1
  105. data/lib/omnizip/formats/seven_zip/encrypted_header.rb +0 -1
  106. data/lib/omnizip/formats/seven_zip/header.rb +0 -1
  107. data/lib/omnizip/formats/seven_zip/header_encryptor.rb +0 -1
  108. data/lib/omnizip/formats/seven_zip/header_writer.rb +0 -1
  109. data/lib/omnizip/formats/seven_zip/models/file_entry.rb +1 -1
  110. data/lib/omnizip/formats/seven_zip/parser.rb +1 -2
  111. data/lib/omnizip/formats/seven_zip/reader.rb +13 -2
  112. data/lib/omnizip/formats/seven_zip/split_archive_reader.rb +0 -1
  113. data/lib/omnizip/formats/seven_zip/split_archive_writer.rb +0 -1
  114. data/lib/omnizip/formats/seven_zip/writer.rb +4 -6
  115. data/lib/omnizip/formats/seven_zip.rb +2 -4
  116. data/lib/omnizip/formats/tar/entry.rb +0 -2
  117. data/lib/omnizip/formats/tar/header.rb +0 -2
  118. data/lib/omnizip/formats/tar/reader.rb +0 -2
  119. data/lib/omnizip/formats/tar/writer.rb +0 -2
  120. data/lib/omnizip/formats/tar.rb +0 -1
  121. data/lib/omnizip/formats/xar/entry.rb +0 -1
  122. data/lib/omnizip/formats/xar/header.rb +0 -1
  123. data/lib/omnizip/formats/xar/reader.rb +0 -1
  124. data/lib/omnizip/formats/xar/toc.rb +1 -2
  125. data/lib/omnizip/formats/xar/writer.rb +11 -3
  126. data/lib/omnizip/formats/xar.rb +0 -1
  127. data/lib/omnizip/formats/xz/reader.rb +7 -3
  128. data/lib/omnizip/formats/xz.rb +3 -40
  129. data/lib/omnizip/formats/xz_impl/block_decoder.rb +4 -10
  130. data/lib/omnizip/formats/xz_impl/block_encoder.rb +0 -2
  131. data/lib/omnizip/formats/xz_impl/block_header_parser.rb +0 -2
  132. data/lib/omnizip/formats/xz_impl/index_decoder.rb +0 -1
  133. data/lib/omnizip/formats/xz_impl/index_encoder.rb +0 -1
  134. data/lib/omnizip/formats/xz_impl/stream_encoder.rb +1 -1
  135. data/lib/omnizip/formats/xz_impl/stream_footer.rb +0 -1
  136. data/lib/omnizip/formats/xz_impl/stream_footer_parser.rb +0 -1
  137. data/lib/omnizip/formats/xz_impl/stream_header.rb +0 -1
  138. data/lib/omnizip/formats/xz_impl/stream_header_parser.rb +0 -2
  139. data/lib/omnizip/formats/zip/central_directory_header.rb +2 -3
  140. data/lib/omnizip/formats/zip/end_of_central_directory.rb +0 -2
  141. data/lib/omnizip/formats/zip/local_file_header.rb +0 -2
  142. data/lib/omnizip/formats/zip/reader.rb +12 -2
  143. data/lib/omnizip/formats/zip/writer.rb +73 -1
  144. data/lib/omnizip/formats/zip/zip64_end_of_central_directory.rb +0 -2
  145. data/lib/omnizip/formats/zip/zip64_end_of_central_directory_locator.rb +0 -2
  146. data/lib/omnizip/formats/zip/zip64_extra_field.rb +0 -2
  147. data/lib/omnizip/formats/zip.rb +0 -1
  148. data/lib/omnizip/formats.rb +8 -24
  149. data/lib/omnizip/implementations/seven_zip/lzma/range_decoder.rb +2 -2
  150. data/lib/omnizip/implementations/seven_zip/lzma/range_encoder.rb +1 -1
  151. data/lib/omnizip/implementations/seven_zip/lzma2/encoder.rb +0 -6
  152. data/lib/omnizip/implementations/xz_utils/lzma2/decoder.rb +3 -4
  153. data/lib/omnizip/implementations/xz_utils/lzma2/encoder.rb +0 -1
  154. data/lib/omnizip/io/source.rb +136 -0
  155. data/lib/omnizip/io.rb +2 -0
  156. data/lib/omnizip/link_handler.rb +7 -3
  157. data/lib/omnizip/metadata/metadata_editor.rb +1 -1
  158. data/lib/omnizip/models/algorithm_metadata.rb +33 -60
  159. data/lib/omnizip/models/compression_options.rb +40 -53
  160. data/lib/omnizip/models/conversion_options.rb +9 -18
  161. data/lib/omnizip/models/parallel_options.rb +25 -0
  162. data/lib/omnizip/optimization_registry.rb +8 -44
  163. data/lib/omnizip/parallel/job_scheduler.rb +26 -8
  164. data/lib/omnizip/parallel/parallel_compressor.rb +10 -20
  165. data/lib/omnizip/parallel/parallel_extractor.rb +20 -11
  166. data/lib/omnizip/parity/par2_repairer.rb +5 -5
  167. data/lib/omnizip/parity/par2_verifier.rb +27 -22
  168. data/lib/omnizip/parity.rb +4 -4
  169. data/lib/omnizip/password/encryption_registry.rb +11 -50
  170. data/lib/omnizip/password/zip_crypto_strategy.rb +1 -4
  171. data/lib/omnizip/pipe/stream_decompressor.rb +2 -3
  172. data/lib/omnizip/profile/archive_profile.rb +0 -2
  173. data/lib/omnizip/profile/binary_profile.rb +0 -2
  174. data/lib/omnizip/profile/compression_profile.rb +0 -1
  175. data/lib/omnizip/profile/profile_detector.rb +0 -2
  176. data/lib/omnizip/profile/text_profile.rb +0 -2
  177. data/lib/omnizip/profile.rb +0 -2
  178. data/lib/omnizip/registry.rb +112 -0
  179. data/lib/omnizip/version.rb +1 -1
  180. data/lib/omnizip/zip/file.rb +20 -9
  181. data/lib/omnizip.rb +26 -58
  182. metadata +24 -2
@@ -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
data/lib/omnizip/error.rb CHANGED
@@ -3,59 +3,64 @@
3
3
  # Copyright (C) 2025 Ribose Inc.
4
4
 
5
5
  module Omnizip
6
- # Base error class for all Omnizip errors
7
6
  class Error < StandardError
8
7
  end
9
8
 
10
- # Error raised when compression fails
11
9
  class CompressionError < Error
12
10
  end
13
11
 
14
- # Error raised when decompression fails
15
12
  class DecompressionError < Error
16
13
  end
17
14
 
18
- # Error raised when an algorithm is not found
19
15
  class AlgorithmNotFoundError < Error
20
16
  end
21
17
 
22
- # Error raised when an unknown algorithm is requested
23
- class UnknownAlgorithmError < Error
18
+ # Canonical alias of +AlgorithmNotFoundError+. Older code referenced
19
+ # +UnknownAlgorithmError+; both names are kept for backward
20
+ # compatibility.
21
+ UnknownAlgorithmError = AlgorithmNotFoundError
22
+
23
+ class UnknownChecksumError < Error
24
+ end
25
+
26
+ class UnknownFilterError < Error
27
+ end
28
+
29
+ class UnknownEncryptionStrategyError < Error
30
+ end
31
+
32
+ class ConversionNotSupportedError < Error
24
33
  end
25
34
 
26
- # Error raised when a format is not supported
27
35
  class UnsupportedFormatError < Error
28
36
  end
29
37
 
30
- # Error raised when format parsing fails
31
38
  class FormatError < Error
32
39
  end
33
40
 
34
- # Error raised when archive is invalid
35
41
  class InvalidArchiveError < Error
36
42
  end
37
43
 
38
- # Error raised when I/O operations fail
39
- class IOError < Error
44
+ # Renamed from +IOError+ to avoid shadowing Ruby's built-in IOError.
45
+ # The legacy name is kept as an alias.
46
+ class IOOperationError < Error
40
47
  end
48
+ IOError = IOOperationError
41
49
 
42
- # Error raised when checksum validation fails
43
50
  class ChecksumError < Error
44
51
  end
45
52
 
46
- # Error raised when optimization strategy is not found
47
- class OptimizationNotFound < Error
53
+ # Canonical name. +OptimizationNotFound+ kept as alias.
54
+ class OptimizationNotFoundError < Error
48
55
  end
56
+ OptimizationNotFound = OptimizationNotFoundError
49
57
 
50
- # Error raised when progress tracking fails
51
58
  class ProgressError < Error
52
59
  end
53
60
 
54
- # Error raised when ETA calculation fails
55
61
  class ETAError < Error
56
62
  end
57
63
 
58
- # Error raised when RAR write is attempted without license
59
64
  class NotLicensedError < Error
60
65
  def initialize(message = default_message)
61
66
  super
@@ -82,7 +87,6 @@ module Omnizip
82
87
  end
83
88
  end
84
89
 
85
- # Error raised when RAR executable is not found
86
90
  class RarNotAvailableError < Error
87
91
  def initialize(message = default_message)
88
92
  super
@@ -57,7 +57,8 @@ module Omnizip
57
57
  # end
58
58
  # end
59
59
  class Filter
60
- # @return [Symbol] Architecture identifier (:x86, :arm, :arm64, :powerpc, :ia64, :sparc)
60
+ # @return [Symbol, nil] Architecture identifier (:x86, :arm, :arm64,
61
+ # :powerpc, :ia64, :sparc) or nil for architecture-agnostic filters
61
62
  attr_reader :architecture
62
63
 
63
64
  # @return [String] Human-readable filter name
@@ -65,9 +66,10 @@ module Omnizip
65
66
 
66
67
  # Initialize filter
67
68
  #
68
- # @param architecture [Symbol] Target architecture
69
+ # @param architecture [Symbol, nil] Target architecture (optional;
70
+ # filters that don't vary by architecture leave this as nil)
69
71
  # @param name [String] Human-readable name
70
- def initialize(architecture:, name: "Unknown")
72
+ def initialize(architecture: nil, name: "Unknown")
71
73
  @architecture = architecture
72
74
  @name = name
73
75
  end