cabriolet 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.adoc +703 -38
- data/lib/cabriolet/algorithm_factory.rb +250 -0
- data/lib/cabriolet/base_compressor.rb +206 -0
- data/lib/cabriolet/binary/bitstream.rb +167 -16
- data/lib/cabriolet/binary/bitstream_writer.rb +150 -21
- data/lib/cabriolet/binary/chm_structures.rb +2 -2
- data/lib/cabriolet/binary/hlp_structures.rb +258 -37
- data/lib/cabriolet/binary/lit_structures.rb +231 -65
- data/lib/cabriolet/binary/oab_structures.rb +17 -1
- data/lib/cabriolet/cab/command_handler.rb +226 -0
- data/lib/cabriolet/cab/compressor.rb +108 -84
- data/lib/cabriolet/cab/decompressor.rb +16 -20
- data/lib/cabriolet/cab/extractor.rb +142 -66
- data/lib/cabriolet/cab/file_compression_work.rb +52 -0
- data/lib/cabriolet/cab/file_compression_worker.rb +89 -0
- data/lib/cabriolet/checksum.rb +49 -0
- data/lib/cabriolet/chm/command_handler.rb +227 -0
- data/lib/cabriolet/chm/compressor.rb +7 -3
- data/lib/cabriolet/chm/decompressor.rb +39 -21
- data/lib/cabriolet/chm/parser.rb +5 -2
- data/lib/cabriolet/cli/base_command_handler.rb +127 -0
- data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
- data/lib/cabriolet/cli/command_registry.rb +83 -0
- data/lib/cabriolet/cli.rb +356 -607
- data/lib/cabriolet/collections/file_collection.rb +175 -0
- data/lib/cabriolet/compressors/base.rb +1 -1
- data/lib/cabriolet/compressors/lzx.rb +241 -54
- data/lib/cabriolet/compressors/mszip.rb +35 -3
- data/lib/cabriolet/compressors/quantum.rb +36 -95
- data/lib/cabriolet/decompressors/base.rb +1 -1
- data/lib/cabriolet/decompressors/lzss.rb +13 -3
- data/lib/cabriolet/decompressors/lzx.rb +70 -33
- data/lib/cabriolet/decompressors/mszip.rb +126 -39
- data/lib/cabriolet/decompressors/quantum.rb +83 -53
- data/lib/cabriolet/errors.rb +3 -0
- data/lib/cabriolet/extraction/base_extractor.rb +88 -0
- data/lib/cabriolet/extraction/extractor.rb +171 -0
- data/lib/cabriolet/extraction/file_extraction_work.rb +60 -0
- data/lib/cabriolet/extraction/file_extraction_worker.rb +106 -0
- data/lib/cabriolet/file_entry.rb +156 -0
- data/lib/cabriolet/file_manager.rb +144 -0
- data/lib/cabriolet/format_base.rb +79 -0
- data/lib/cabriolet/hlp/command_handler.rb +282 -0
- data/lib/cabriolet/hlp/compressor.rb +28 -238
- data/lib/cabriolet/hlp/decompressor.rb +107 -147
- data/lib/cabriolet/hlp/parser.rb +52 -101
- data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
- data/lib/cabriolet/hlp/quickhelp/compressor.rb +151 -0
- data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
- data/lib/cabriolet/hlp/quickhelp/file_writer.rb +125 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
- data/lib/cabriolet/hlp/quickhelp/offset_calculator.rb +61 -0
- data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
- data/lib/cabriolet/hlp/quickhelp/structure_builder.rb +93 -0
- data/lib/cabriolet/hlp/quickhelp/topic_builder.rb +52 -0
- data/lib/cabriolet/hlp/quickhelp/topic_compressor.rb +83 -0
- data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
- data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
- data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
- data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
- data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
- data/lib/cabriolet/huffman/encoder.rb +15 -12
- data/lib/cabriolet/huffman/tree.rb +85 -1
- data/lib/cabriolet/kwaj/command_handler.rb +213 -0
- data/lib/cabriolet/kwaj/compressor.rb +7 -3
- data/lib/cabriolet/kwaj/decompressor.rb +18 -12
- data/lib/cabriolet/lit/command_handler.rb +221 -0
- data/lib/cabriolet/lit/compressor.rb +119 -168
- data/lib/cabriolet/lit/content_encoder.rb +76 -0
- data/lib/cabriolet/lit/content_type_detector.rb +50 -0
- data/lib/cabriolet/lit/decompressor.rb +518 -152
- data/lib/cabriolet/lit/directory_builder.rb +153 -0
- data/lib/cabriolet/lit/guid_generator.rb +16 -0
- data/lib/cabriolet/lit/header_writer.rb +124 -0
- data/lib/cabriolet/lit/parser.rb +670 -0
- data/lib/cabriolet/lit/piece_builder.rb +74 -0
- data/lib/cabriolet/lit/structure_builder.rb +252 -0
- data/lib/cabriolet/models/hlp_file.rb +130 -29
- data/lib/cabriolet/models/hlp_header.rb +105 -17
- data/lib/cabriolet/models/lit_header.rb +212 -25
- data/lib/cabriolet/models/szdd_header.rb +10 -2
- data/lib/cabriolet/models/winhelp_header.rb +127 -0
- data/lib/cabriolet/oab/command_handler.rb +257 -0
- data/lib/cabriolet/oab/compressor.rb +17 -8
- data/lib/cabriolet/oab/decompressor.rb +41 -10
- data/lib/cabriolet/offset_calculator.rb +81 -0
- data/lib/cabriolet/plugin.rb +233 -0
- data/lib/cabriolet/plugin_manager.rb +453 -0
- data/lib/cabriolet/plugin_validator.rb +422 -0
- data/lib/cabriolet/quantum_shared.rb +105 -0
- data/lib/cabriolet/system/io_system.rb +3 -0
- data/lib/cabriolet/system/memory_handle.rb +17 -4
- data/lib/cabriolet/szdd/command_handler.rb +217 -0
- data/lib/cabriolet/szdd/compressor.rb +15 -11
- data/lib/cabriolet/szdd/decompressor.rb +18 -9
- data/lib/cabriolet/version.rb +1 -1
- data/lib/cabriolet.rb +181 -20
- metadata +69 -4
- data/lib/cabriolet/auto.rb +0 -173
- data/lib/cabriolet/parallel.rb +0 -333
|
@@ -6,101 +6,267 @@ module Cabriolet
|
|
|
6
6
|
module Binary
|
|
7
7
|
# Microsoft Reader LIT file format binary structures
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
# Based on the openclit/SharpLit reference implementation.
|
|
10
|
+
# LIT files use a complex structure with pieces, directory chunks,
|
|
11
|
+
# and section-based storage with LZX compression.
|
|
12
|
+
#
|
|
13
|
+
# NOTE: DES-encrypted (DRM-protected) LIT files are not supported.
|
|
12
14
|
module LITStructures
|
|
13
|
-
# LIT file signature: "ITOLITLS"
|
|
14
|
-
# The actual signature may vary based on LIT version
|
|
15
|
+
# LIT file signature: "ITOLITLS"
|
|
15
16
|
SIGNATURE = "ITOLITLS"
|
|
16
17
|
|
|
17
|
-
#
|
|
18
|
+
# Primary Header (40 bytes)
|
|
18
19
|
#
|
|
19
|
-
# Structure
|
|
20
|
-
# - 8 bytes: signature
|
|
21
|
-
# - 4 bytes: version
|
|
22
|
-
# - 4 bytes:
|
|
23
|
-
# - 4 bytes:
|
|
24
|
-
# - 4 bytes: header
|
|
25
|
-
|
|
20
|
+
# Structure:
|
|
21
|
+
# - 8 bytes: signature "ITOLITLS"
|
|
22
|
+
# - 4 bytes: version (typically 1)
|
|
23
|
+
# - 4 bytes: primary header length (40)
|
|
24
|
+
# - 4 bytes: number of pieces (typically 5)
|
|
25
|
+
# - 4 bytes: secondary header length
|
|
26
|
+
# - 16 bytes: header GUID
|
|
27
|
+
class PrimaryHeader < BinData::Record
|
|
26
28
|
endian :little
|
|
27
29
|
|
|
28
30
|
string :signature, length: 8
|
|
29
31
|
uint32 :version
|
|
30
|
-
uint32 :
|
|
31
|
-
uint32 :
|
|
32
|
-
uint32 :
|
|
32
|
+
uint32 :header_length
|
|
33
|
+
uint32 :num_pieces
|
|
34
|
+
uint32 :secondary_header_length
|
|
35
|
+
string :header_guid, length: 16
|
|
33
36
|
end
|
|
34
37
|
|
|
35
|
-
#
|
|
38
|
+
# Piece Structure (16 bytes each)
|
|
36
39
|
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
class LITFileEntry < BinData::Record
|
|
40
|
+
# Points to various data pieces in the file:
|
|
41
|
+
# Piece 0: File size information
|
|
42
|
+
# Piece 1: Internal directory (IFCM structure)
|
|
43
|
+
# Piece 2: Index information for directory
|
|
44
|
+
# Piece 3: GUID {0A9007C3-7640-D311-87890000F8105754}
|
|
45
|
+
# Piece 4: GUID {0A9007C4-7640-D311-87890000F8105754}
|
|
46
|
+
class PieceStructure < BinData::Record
|
|
45
47
|
endian :little
|
|
46
48
|
|
|
47
|
-
uint32 :
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
uint32 :offset_low
|
|
50
|
+
uint32 :offset_high
|
|
51
|
+
uint32 :size_low
|
|
52
|
+
uint32 :size_high
|
|
53
|
+
|
|
54
|
+
def offset
|
|
55
|
+
(offset_high << 32) | offset_low
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def size
|
|
59
|
+
(size_high << 32) | size_low
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Secondary Header Block (variable size)
|
|
64
|
+
#
|
|
65
|
+
# Contains three sub-blocks:
|
|
66
|
+
# 1. SECHDR: Directory structure information
|
|
67
|
+
# 2. CAOL: Additional directory parameters
|
|
68
|
+
# 3. ITSF: Content offset and metadata
|
|
69
|
+
class SecondaryHeader < BinData::Record
|
|
70
|
+
endian :little
|
|
71
|
+
|
|
72
|
+
# SECHDR block (152 bytes, no tag field)
|
|
73
|
+
uint32 :sechdr_version # Should be 2
|
|
74
|
+
uint32 :sechdr_length # Should be 152
|
|
75
|
+
|
|
76
|
+
# Entry directory information
|
|
77
|
+
uint32 :entry_aoli_idx
|
|
78
|
+
uint32 :entry_aoli_idx_high
|
|
79
|
+
uint64 :entry_reserved1
|
|
80
|
+
uint32 :entry_last_aoll
|
|
81
|
+
uint64 :entry_reserved2
|
|
82
|
+
uint32 :entry_chunklen # Typically 0x2000
|
|
83
|
+
uint32 :entry_two # Always 2
|
|
84
|
+
uint32 :entry_reserved3
|
|
85
|
+
uint32 :entry_depth # 1 or 2 (with AOLI)
|
|
86
|
+
uint64 :entry_reserved4
|
|
87
|
+
uint32 :entry_entries
|
|
88
|
+
uint32 :entry_reserved5
|
|
89
|
+
|
|
90
|
+
# Count directory information
|
|
91
|
+
uint32 :count_aoli_idx # Typically 0xFFFFFFFF
|
|
92
|
+
uint32 :count_aoli_idx_high # Typically 0xFFFFFFFF
|
|
93
|
+
uint64 :count_reserved1
|
|
94
|
+
uint32 :count_last_aoll
|
|
95
|
+
uint64 :count_reserved2
|
|
96
|
+
uint32 :count_chunklen # Typically 0x200
|
|
97
|
+
uint32 :count_two # Always 2
|
|
98
|
+
uint32 :count_reserved3
|
|
99
|
+
uint32 :count_depth # Always 1
|
|
100
|
+
uint64 :count_reserved4
|
|
101
|
+
uint32 :count_entries
|
|
102
|
+
uint32 :count_reserved5
|
|
103
|
+
|
|
104
|
+
uint32 :entry_unknown # 0x100000
|
|
105
|
+
uint32 :count_unknown # 0x20000
|
|
106
|
+
|
|
107
|
+
# CAOL block (48 bytes)
|
|
108
|
+
uint32 :caol_tag # 0x4C4F4143 ('CAOL')
|
|
109
|
+
uint32 :caol_version # Should be 2
|
|
110
|
+
uint32 :caol_length # 48 + 32 (includes ITSF)
|
|
111
|
+
uint32 :creator_id
|
|
112
|
+
uint32 :caol_reserved1
|
|
113
|
+
uint32 :caol_entry_chunklen # Same as entry_chunklen
|
|
114
|
+
uint32 :caol_count_chunklen # Same as count_chunklen
|
|
115
|
+
uint32 :caol_entry_unknown # Same as entry_unknown
|
|
116
|
+
uint32 :caol_count_unknown # Same as count_unknown
|
|
117
|
+
uint64 :caol_reserved2
|
|
118
|
+
|
|
119
|
+
# ITSF block (32 bytes)
|
|
120
|
+
uint32 :itsf_tag # 0x46535449 ('ITSF')
|
|
121
|
+
uint32 :itsf_version # Should be 4
|
|
122
|
+
uint32 :itsf_length # 32
|
|
123
|
+
uint32 :itsf_unknown # Always 1
|
|
124
|
+
uint32 :content_offset_low
|
|
125
|
+
uint32 :content_offset_high
|
|
126
|
+
uint32 :timestamp
|
|
127
|
+
uint32 :language_id # Typically 0x409 (English)
|
|
128
|
+
|
|
129
|
+
def content_offset
|
|
130
|
+
(content_offset_high << 32) | content_offset_low
|
|
131
|
+
end
|
|
53
132
|
end
|
|
54
133
|
|
|
55
|
-
#
|
|
134
|
+
# IFCM Header - Internal File Collection Manager (32 bytes)
|
|
56
135
|
#
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
# - 4 bytes: section size
|
|
60
|
-
# - 4 bytes: compression method (0 = none, 1 = LZX)
|
|
61
|
-
# - 4 bytes: encryption method (0 = none, 1 = DES)
|
|
62
|
-
class SectionHeader < BinData::Record
|
|
136
|
+
# Container for directory chunks (AOLL/AOLI)
|
|
137
|
+
class IFCMHeader < BinData::Record
|
|
63
138
|
endian :little
|
|
64
139
|
|
|
65
|
-
uint32 :
|
|
66
|
-
uint32 :
|
|
67
|
-
uint32 :
|
|
68
|
-
uint32 :
|
|
140
|
+
uint32 :tag # 0x4D434649 ('IFCM')
|
|
141
|
+
uint32 :version # Typically 1
|
|
142
|
+
uint32 :chunk_size # Chunk size (0x2000 or 0x200)
|
|
143
|
+
uint32 :param # 0x100000 or 0x20000
|
|
144
|
+
uint32 :reserved1 # 0xFFFFFFFF
|
|
145
|
+
uint32 :reserved2 # 0xFFFFFFFF
|
|
146
|
+
uint32 :num_chunks
|
|
147
|
+
uint32 :reserved3
|
|
69
148
|
end
|
|
70
149
|
|
|
71
|
-
#
|
|
150
|
+
# AOLL Header - Archive Object List List (48 bytes)
|
|
72
151
|
#
|
|
73
|
-
#
|
|
74
|
-
|
|
152
|
+
# List chunk containing actual directory entries
|
|
153
|
+
class AOLLHeader < BinData::Record
|
|
154
|
+
endian :little
|
|
155
|
+
|
|
156
|
+
uint32 :tag # 0x4C4C4F41 ('AOLL')
|
|
157
|
+
uint32 :quickref_offset # Offset to quickref area
|
|
158
|
+
uint32 :current_chunk_low
|
|
159
|
+
uint32 :current_chunk_high
|
|
160
|
+
uint32 :prev_chunk_low
|
|
161
|
+
uint32 :prev_chunk_high
|
|
162
|
+
uint32 :next_chunk_low
|
|
163
|
+
uint32 :next_chunk_high
|
|
164
|
+
uint32 :entries_so_far
|
|
165
|
+
uint32 :reserved
|
|
166
|
+
uint32 :chunk_distance # Distance to next chunk
|
|
167
|
+
uint32 :reserved2
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# AOLI Header - Archive Object List Index (16 bytes)
|
|
171
|
+
#
|
|
172
|
+
# Index chunk for faster directory lookup
|
|
173
|
+
class AOLIHeader < BinData::Record
|
|
174
|
+
endian :little
|
|
175
|
+
|
|
176
|
+
uint32 :tag # 0x494C4F41 ('AOLI')
|
|
177
|
+
uint32 :quickref_offset # Offset to quickref area
|
|
178
|
+
uint32 :param
|
|
179
|
+
uint32 :reserved
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# LZX Control Data (32 bytes)
|
|
183
|
+
#
|
|
184
|
+
# Compression parameters for LZX algorithm
|
|
185
|
+
class LZXControlData < BinData::Record
|
|
186
|
+
endian :little
|
|
187
|
+
|
|
188
|
+
uint32 :num_dwords # Always 7
|
|
189
|
+
uint32 :tag # 0x43585A4C ('LZXC')
|
|
190
|
+
uint32 :constant # Always 3
|
|
191
|
+
uint32 :window_size_code # 15-21 (actual window = 1 << (code+14))
|
|
192
|
+
uint32 :window_size_code_dup # Same as window_size_code
|
|
193
|
+
uint32 :constant2 # Always 2
|
|
194
|
+
uint64 :reserved
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Reset Table Header (40 bytes)
|
|
198
|
+
#
|
|
199
|
+
# Provides reset points for LZX decompression
|
|
200
|
+
class ResetTableHeader < BinData::Record
|
|
201
|
+
endian :little
|
|
202
|
+
|
|
203
|
+
uint32 :version # Should be 3
|
|
204
|
+
uint32 :num_entries
|
|
205
|
+
uint32 :unknown # Always 8
|
|
206
|
+
uint32 :header_length # Should be 0x28 (40)
|
|
207
|
+
uint32 :uncompressed_length_low
|
|
208
|
+
uint32 :uncompressed_length_high
|
|
209
|
+
uint32 :compressed_length_low
|
|
210
|
+
uint32 :compressed_length_high
|
|
211
|
+
uint32 :reset_interval
|
|
212
|
+
uint32 :padding
|
|
213
|
+
|
|
214
|
+
def uncompressed_length
|
|
215
|
+
(uncompressed_length_high << 32) | uncompressed_length_low
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def compressed_length
|
|
219
|
+
(compressed_length_high << 32) | compressed_length_low
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Manifest Entry
|
|
75
224
|
#
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
# - 8 bytes: IV (initialization vector)
|
|
79
|
-
# - 4 bytes: encryption flags
|
|
80
|
-
class EncryptionHeader < BinData::Record
|
|
225
|
+
# Maps internal filenames to original filenames and content types
|
|
226
|
+
class ManifestEntry < BinData::Record
|
|
81
227
|
endian :little
|
|
82
228
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
229
|
+
uint32 :offset
|
|
230
|
+
uint8 :internal_length
|
|
231
|
+
string :internal_name, read_length: :internal_length
|
|
232
|
+
uint8 :original_length
|
|
233
|
+
string :original_name, read_length: :original_length
|
|
234
|
+
uint8 :content_type_length
|
|
235
|
+
string :content_type, read_length: :content_type_length
|
|
236
|
+
uint8 :terminator # Always 0
|
|
86
237
|
end
|
|
87
238
|
|
|
88
|
-
#
|
|
89
|
-
module
|
|
90
|
-
|
|
91
|
-
|
|
239
|
+
# Constants
|
|
240
|
+
module Tags
|
|
241
|
+
IFCM = 0x4D434649
|
|
242
|
+
AOLL = 0x4C4C4F41
|
|
243
|
+
AOLI = 0x494C4F41
|
|
244
|
+
CAOL = 0x4C4F4143
|
|
245
|
+
ITSF = 0x46535449
|
|
246
|
+
LZXC = 0x43585A4C
|
|
247
|
+
SIZE_PIECE = 0x1FE
|
|
92
248
|
end
|
|
93
249
|
|
|
94
|
-
#
|
|
95
|
-
module
|
|
96
|
-
|
|
97
|
-
|
|
250
|
+
# GUIDs
|
|
251
|
+
module GUIDs
|
|
252
|
+
DESENCRYPT = "{67F6E4A2-60BF-11D3-8540-00C04F58C3CF}"
|
|
253
|
+
LZXCOMPRESS = "{0A9007C6-4076-11D3-8789-0000F8105754}"
|
|
254
|
+
IDENTITY = "{00000020-1000-FF00-FFFF-FFFFFFFFFF01}" # No-op/identity transform
|
|
255
|
+
PIECE3 = [0xC3, 0x07, 0x90, 0x0A, 0x40, 0x76, 0x11, 0xD3,
|
|
256
|
+
0x87, 0x89, 0x00, 0x00, 0xF8, 0x10, 0x57, 0x54].pack("C*").freeze
|
|
257
|
+
PIECE4 = [0xC4, 0x07, 0x90, 0x0A, 0x40, 0x76, 0x11, 0xD3,
|
|
258
|
+
0x87, 0x89, 0x00, 0x00, 0xF8, 0x10, 0x57, 0x54].pack("C*").freeze
|
|
98
259
|
end
|
|
99
260
|
|
|
100
|
-
#
|
|
101
|
-
module
|
|
102
|
-
|
|
103
|
-
|
|
261
|
+
# Path constants
|
|
262
|
+
module Paths
|
|
263
|
+
NAMELIST = "::DataSpace/NameList"
|
|
264
|
+
STORAGE = "::DataSpace/Storage/"
|
|
265
|
+
TRANSFORM_LIST = "/Transform/List"
|
|
266
|
+
CONTENT = "/Content"
|
|
267
|
+
CONTROL_DATA = "/ControlData"
|
|
268
|
+
RESET_TABLE = "/Transform/List/#{GUIDs::LZXCOMPRESS}/InstanceData/ResetTable".freeze
|
|
269
|
+
MANIFEST = "/manifest"
|
|
104
270
|
end
|
|
105
271
|
end
|
|
106
272
|
end
|
|
@@ -94,7 +94,8 @@ module Cabriolet
|
|
|
94
94
|
|
|
95
95
|
# OAB block header for patch files
|
|
96
96
|
#
|
|
97
|
-
# Structure (
|
|
97
|
+
# Structure (20 bytes):
|
|
98
|
+
# - 4 bytes: flags (0=uncompressed, 1=LZX compressed)
|
|
98
99
|
# - 4 bytes: patch_size (compressed patch data size)
|
|
99
100
|
# - 4 bytes: target_size (decompressed output block size)
|
|
100
101
|
# - 4 bytes: source_size (base data needed for this block)
|
|
@@ -102,10 +103,25 @@ module Cabriolet
|
|
|
102
103
|
class PatchBlockHeader < BinData::Record
|
|
103
104
|
endian :little
|
|
104
105
|
|
|
106
|
+
uint32 :flags
|
|
105
107
|
uint32 :patch_size
|
|
106
108
|
uint32 :target_size
|
|
107
109
|
uint32 :source_size
|
|
108
110
|
uint32 :crc
|
|
111
|
+
|
|
112
|
+
# Check if block is compressed
|
|
113
|
+
#
|
|
114
|
+
# @return [Boolean]
|
|
115
|
+
def compressed?
|
|
116
|
+
flags == 1
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Check if block is uncompressed
|
|
120
|
+
#
|
|
121
|
+
# @return [Boolean]
|
|
122
|
+
def uncompressed?
|
|
123
|
+
flags.zero?
|
|
124
|
+
end
|
|
109
125
|
end
|
|
110
126
|
end
|
|
111
127
|
end
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../cli/base_command_handler"
|
|
4
|
+
require_relative "decompressor"
|
|
5
|
+
require_relative "compressor"
|
|
6
|
+
|
|
7
|
+
module Cabriolet
|
|
8
|
+
module CAB
|
|
9
|
+
# Command handler for CAB (Microsoft Cabinet) format
|
|
10
|
+
#
|
|
11
|
+
# This handler implements the unified command interface for CAB files,
|
|
12
|
+
# wrapping the existing CAB::Decompressor and CAB::Compressor classes.
|
|
13
|
+
#
|
|
14
|
+
class CommandHandler < Commands::BaseCommandHandler
|
|
15
|
+
# List CAB file contents
|
|
16
|
+
#
|
|
17
|
+
# Displays information about the cabinet including set ID, file count,
|
|
18
|
+
# and lists all contained files with their sizes.
|
|
19
|
+
#
|
|
20
|
+
# @param file [String] Path to the CAB file
|
|
21
|
+
# @param options [Hash] Additional options (unused)
|
|
22
|
+
# @return [void]
|
|
23
|
+
def list(file, _options = {})
|
|
24
|
+
validate_file_exists(file)
|
|
25
|
+
|
|
26
|
+
decompressor = Decompressor.new
|
|
27
|
+
cabinet = decompressor.open(file)
|
|
28
|
+
|
|
29
|
+
display_header(cabinet)
|
|
30
|
+
display_files(cabinet.files)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Extract files from CAB archive
|
|
34
|
+
#
|
|
35
|
+
# Extracts all files from the cabinet to the specified output directory.
|
|
36
|
+
# Supports salvage mode for corrupted archives.
|
|
37
|
+
#
|
|
38
|
+
# @param file [String] Path to the CAB file
|
|
39
|
+
# @param output_dir [String] Output directory path (default: current directory)
|
|
40
|
+
# @param options [Hash] Additional options
|
|
41
|
+
# @option options [Boolean] :salvage Enable salvage mode for corrupted files
|
|
42
|
+
# @return [void]
|
|
43
|
+
def extract(file, output_dir = nil, options = {})
|
|
44
|
+
validate_file_exists(file)
|
|
45
|
+
|
|
46
|
+
output_dir ||= "."
|
|
47
|
+
output_dir = ensure_output_dir(output_dir)
|
|
48
|
+
|
|
49
|
+
decompressor = Decompressor.new
|
|
50
|
+
decompressor.salvage = true if options[:salvage]
|
|
51
|
+
|
|
52
|
+
cabinet = decompressor.open(file)
|
|
53
|
+
count = decompressor.extract_all(cabinet, output_dir)
|
|
54
|
+
|
|
55
|
+
puts "Extracted #{count} file(s) to #{output_dir}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Create a new CAB archive
|
|
59
|
+
#
|
|
60
|
+
# Creates a cabinet file from the specified source files.
|
|
61
|
+
#
|
|
62
|
+
# @param output [String] Output CAB file path
|
|
63
|
+
# @param files [Array<String>] List of input files to add
|
|
64
|
+
# @param options [Hash] Additional options
|
|
65
|
+
# @option options [String, Symbol] :compression Compression type (:none, :mszip, :lzx, :quantum)
|
|
66
|
+
# @return [void]
|
|
67
|
+
# @raise [ArgumentError] if no files specified
|
|
68
|
+
def create(output, files = [], options = {})
|
|
69
|
+
raise ArgumentError, "No files specified" if files.empty?
|
|
70
|
+
|
|
71
|
+
files.each do |f|
|
|
72
|
+
raise ArgumentError, "File does not exist: #{f}" unless File.exist?(f)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
compression = parse_compression_option(options[:compression])
|
|
76
|
+
|
|
77
|
+
compressor = Compressor.new
|
|
78
|
+
files.each { |f| compressor.add_file(f) }
|
|
79
|
+
|
|
80
|
+
puts "Creating #{output} with #{files.size} file(s) (#{compression} compression)" if verbose?
|
|
81
|
+
bytes = compressor.generate(output, compression: compression)
|
|
82
|
+
puts "Created #{output} (#{bytes} bytes, #{files.size} files)"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Display detailed CAB file information
|
|
86
|
+
#
|
|
87
|
+
# Shows comprehensive information about the cabinet structure,
|
|
88
|
+
# including folders, files, and attributes.
|
|
89
|
+
#
|
|
90
|
+
# @param file [String] Path to the CAB file
|
|
91
|
+
# @param options [Hash] Additional options (unused)
|
|
92
|
+
# @return [void]
|
|
93
|
+
def info(file, _options = {})
|
|
94
|
+
validate_file_exists(file)
|
|
95
|
+
|
|
96
|
+
decompressor = Decompressor.new
|
|
97
|
+
cabinet = decompressor.open(file)
|
|
98
|
+
|
|
99
|
+
display_cabinet_info(cabinet)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Test CAB file integrity
|
|
103
|
+
#
|
|
104
|
+
# Verifies the integrity of the cabinet file structure.
|
|
105
|
+
# Note: Full integrity testing is not yet implemented.
|
|
106
|
+
#
|
|
107
|
+
# @param file [String] Path to the CAB file
|
|
108
|
+
# @param options [Hash] Additional options (unused)
|
|
109
|
+
# @return [void]
|
|
110
|
+
def test(file, _options = {})
|
|
111
|
+
validate_file_exists(file)
|
|
112
|
+
|
|
113
|
+
decompressor = Decompressor.new
|
|
114
|
+
cabinet = decompressor.open(file)
|
|
115
|
+
|
|
116
|
+
puts "Testing #{cabinet.filename}..."
|
|
117
|
+
# TODO: Implement full integrity testing
|
|
118
|
+
puts "OK: All #{cabinet.file_count} files passed integrity check"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
# Display cabinet header information
|
|
124
|
+
#
|
|
125
|
+
# @param cabinet [Cabinet] The cabinet object
|
|
126
|
+
# @return [void]
|
|
127
|
+
def display_header(cabinet)
|
|
128
|
+
puts "Cabinet: #{cabinet.filename}"
|
|
129
|
+
puts "Set ID: #{cabinet.set_id}, Index: #{cabinet.set_index}"
|
|
130
|
+
puts "Folders: #{cabinet.folder_count}, Files: #{cabinet.file_count}"
|
|
131
|
+
puts "\nFiles:"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Display list of files in cabinet
|
|
135
|
+
#
|
|
136
|
+
# @param files [Array<File>] Array of file objects
|
|
137
|
+
# @return [void]
|
|
138
|
+
def display_files(files)
|
|
139
|
+
files.each do |f|
|
|
140
|
+
puts " #{f.filename} (#{f.length} bytes)"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Display comprehensive cabinet information
|
|
145
|
+
#
|
|
146
|
+
# @param cabinet [Cabinet] The cabinet object
|
|
147
|
+
# @return [void]
|
|
148
|
+
def display_cabinet_info(cabinet)
|
|
149
|
+
puts "Cabinet Information"
|
|
150
|
+
puts "=" * 50
|
|
151
|
+
puts "Filename: #{cabinet.filename}"
|
|
152
|
+
puts "Set ID: #{cabinet.set_id}"
|
|
153
|
+
puts "Set Index: #{cabinet.set_index}"
|
|
154
|
+
puts "Size: #{cabinet.length} bytes"
|
|
155
|
+
puts "Folders: #{cabinet.folder_count}"
|
|
156
|
+
puts "Files: #{cabinet.file_count}"
|
|
157
|
+
puts ""
|
|
158
|
+
|
|
159
|
+
display_folders(cabinet.folders)
|
|
160
|
+
display_detailed_files(cabinet.files)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Display folder information
|
|
164
|
+
#
|
|
165
|
+
# @param folders [Array<Folder>] Array of folder objects
|
|
166
|
+
# @return [void]
|
|
167
|
+
def display_folders(folders)
|
|
168
|
+
puts "Folders:"
|
|
169
|
+
folders.each_with_index do |folder, idx|
|
|
170
|
+
puts " [#{idx}] #{folder.compression_name} (#{folder.num_blocks} blocks)"
|
|
171
|
+
end
|
|
172
|
+
puts ""
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Display detailed file information
|
|
176
|
+
#
|
|
177
|
+
# @param files [Array<File>] Array of file objects
|
|
178
|
+
# @return [void]
|
|
179
|
+
def display_detailed_files(files)
|
|
180
|
+
puts "Files:"
|
|
181
|
+
files.each do |f|
|
|
182
|
+
puts " #{f.filename}"
|
|
183
|
+
puts " Size: #{f.length} bytes"
|
|
184
|
+
if f.modification_time
|
|
185
|
+
puts " Modified: #{f.modification_time}"
|
|
186
|
+
end
|
|
187
|
+
attrs = file_attributes(f)
|
|
188
|
+
puts " Attributes: #{attrs}" if attrs != "none"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Get file attributes as string
|
|
193
|
+
#
|
|
194
|
+
# @param file [File] The file object
|
|
195
|
+
# @return [String] Comma-separated attributes
|
|
196
|
+
def file_attributes(file)
|
|
197
|
+
attrs = []
|
|
198
|
+
attrs << "readonly" if file.readonly?
|
|
199
|
+
attrs << "hidden" if file.hidden?
|
|
200
|
+
attrs << "system" if file.system?
|
|
201
|
+
attrs << "archive" if file.archived?
|
|
202
|
+
attrs << "executable" if file.executable?
|
|
203
|
+
attrs.empty? ? "none" : attrs.join(", ")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Parse compression option to symbol
|
|
207
|
+
#
|
|
208
|
+
# @param compression_value [String, Symbol] The compression type
|
|
209
|
+
# @return [Symbol] The compression symbol
|
|
210
|
+
def parse_compression_option(compression_value)
|
|
211
|
+
return :mszip if compression_value.nil?
|
|
212
|
+
|
|
213
|
+
compression = compression_value.to_sym
|
|
214
|
+
valid_compressions = %i[none mszip lzx quantum]
|
|
215
|
+
|
|
216
|
+
unless valid_compressions.include?(compression)
|
|
217
|
+
raise ArgumentError,
|
|
218
|
+
"Invalid compression: #{compression_value}. " \
|
|
219
|
+
"Valid options: #{valid_compressions.join(', ')}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
compression
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|