cabriolet 0.1.0
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 +7 -0
- data/ARCHITECTURE.md +799 -0
- data/CHANGELOG.md +44 -0
- data/LICENSE +29 -0
- data/README.adoc +1207 -0
- data/exe/cabriolet +6 -0
- data/lib/cabriolet/auto.rb +173 -0
- data/lib/cabriolet/binary/bitstream.rb +148 -0
- data/lib/cabriolet/binary/bitstream_writer.rb +180 -0
- data/lib/cabriolet/binary/chm_structures.rb +213 -0
- data/lib/cabriolet/binary/hlp_structures.rb +66 -0
- data/lib/cabriolet/binary/kwaj_structures.rb +74 -0
- data/lib/cabriolet/binary/lit_structures.rb +107 -0
- data/lib/cabriolet/binary/oab_structures.rb +112 -0
- data/lib/cabriolet/binary/structures.rb +56 -0
- data/lib/cabriolet/binary/szdd_structures.rb +60 -0
- data/lib/cabriolet/cab/compressor.rb +382 -0
- data/lib/cabriolet/cab/decompressor.rb +510 -0
- data/lib/cabriolet/cab/extractor.rb +357 -0
- data/lib/cabriolet/cab/parser.rb +264 -0
- data/lib/cabriolet/chm/compressor.rb +513 -0
- data/lib/cabriolet/chm/decompressor.rb +436 -0
- data/lib/cabriolet/chm/parser.rb +254 -0
- data/lib/cabriolet/cli.rb +776 -0
- data/lib/cabriolet/compressors/base.rb +34 -0
- data/lib/cabriolet/compressors/lzss.rb +250 -0
- data/lib/cabriolet/compressors/lzx.rb +581 -0
- data/lib/cabriolet/compressors/mszip.rb +315 -0
- data/lib/cabriolet/compressors/quantum.rb +446 -0
- data/lib/cabriolet/constants.rb +75 -0
- data/lib/cabriolet/decompressors/base.rb +39 -0
- data/lib/cabriolet/decompressors/lzss.rb +138 -0
- data/lib/cabriolet/decompressors/lzx.rb +726 -0
- data/lib/cabriolet/decompressors/mszip.rb +390 -0
- data/lib/cabriolet/decompressors/none.rb +27 -0
- data/lib/cabriolet/decompressors/quantum.rb +456 -0
- data/lib/cabriolet/errors.rb +39 -0
- data/lib/cabriolet/format_detector.rb +156 -0
- data/lib/cabriolet/hlp/compressor.rb +272 -0
- data/lib/cabriolet/hlp/decompressor.rb +198 -0
- data/lib/cabriolet/hlp/parser.rb +131 -0
- data/lib/cabriolet/huffman/decoder.rb +79 -0
- data/lib/cabriolet/huffman/encoder.rb +108 -0
- data/lib/cabriolet/huffman/tree.rb +138 -0
- data/lib/cabriolet/kwaj/compressor.rb +479 -0
- data/lib/cabriolet/kwaj/decompressor.rb +237 -0
- data/lib/cabriolet/kwaj/parser.rb +183 -0
- data/lib/cabriolet/lit/compressor.rb +255 -0
- data/lib/cabriolet/lit/decompressor.rb +250 -0
- data/lib/cabriolet/models/cabinet.rb +81 -0
- data/lib/cabriolet/models/chm_file.rb +28 -0
- data/lib/cabriolet/models/chm_header.rb +67 -0
- data/lib/cabriolet/models/chm_section.rb +38 -0
- data/lib/cabriolet/models/file.rb +119 -0
- data/lib/cabriolet/models/folder.rb +102 -0
- data/lib/cabriolet/models/folder_data.rb +21 -0
- data/lib/cabriolet/models/hlp_file.rb +45 -0
- data/lib/cabriolet/models/hlp_header.rb +37 -0
- data/lib/cabriolet/models/kwaj_header.rb +98 -0
- data/lib/cabriolet/models/lit_header.rb +55 -0
- data/lib/cabriolet/models/oab_header.rb +95 -0
- data/lib/cabriolet/models/szdd_header.rb +72 -0
- data/lib/cabriolet/modifier.rb +326 -0
- data/lib/cabriolet/oab/compressor.rb +353 -0
- data/lib/cabriolet/oab/decompressor.rb +315 -0
- data/lib/cabriolet/parallel.rb +333 -0
- data/lib/cabriolet/repairer.rb +288 -0
- data/lib/cabriolet/streaming.rb +221 -0
- data/lib/cabriolet/system/file_handle.rb +107 -0
- data/lib/cabriolet/system/io_system.rb +87 -0
- data/lib/cabriolet/system/memory_handle.rb +105 -0
- data/lib/cabriolet/szdd/compressor.rb +217 -0
- data/lib/cabriolet/szdd/decompressor.rb +184 -0
- data/lib/cabriolet/szdd/parser.rb +127 -0
- data/lib/cabriolet/validator.rb +332 -0
- data/lib/cabriolet/version.rb +5 -0
- data/lib/cabriolet.rb +104 -0
- metadata +157 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
module Models
|
|
5
|
+
# Cabinet represents a CAB file or cabinet set
|
|
6
|
+
class Cabinet
|
|
7
|
+
attr_accessor :filename, :base_offset, :length, :set_id, :set_index, :flags, :header_resv, :prevname, :nextname,
|
|
8
|
+
:previnfo, :nextinfo, :folders, :files, :next_cabinet, :prev_cabinet, :next
|
|
9
|
+
attr_reader :blocks_offset, :block_resv
|
|
10
|
+
|
|
11
|
+
# Initialize a new cabinet
|
|
12
|
+
#
|
|
13
|
+
# @param filename [String] Path to the cabinet file
|
|
14
|
+
def initialize(filename = nil)
|
|
15
|
+
@filename = filename
|
|
16
|
+
@base_offset = 0
|
|
17
|
+
@length = 0
|
|
18
|
+
@set_id = 0
|
|
19
|
+
@set_index = 0
|
|
20
|
+
@flags = 0
|
|
21
|
+
@header_resv = 0
|
|
22
|
+
@prevname = nil
|
|
23
|
+
@nextname = nil
|
|
24
|
+
@previnfo = nil
|
|
25
|
+
@nextinfo = nil
|
|
26
|
+
@folders = []
|
|
27
|
+
@files = []
|
|
28
|
+
@next_cabinet = nil
|
|
29
|
+
@prev_cabinet = nil
|
|
30
|
+
@next = nil
|
|
31
|
+
@blocks_offset = 0
|
|
32
|
+
@block_resv = 0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Check if this cabinet has a predecessor
|
|
36
|
+
#
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
def has_prev?
|
|
39
|
+
@flags.anybits?(Constants::FLAG_PREV_CABINET)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Check if this cabinet has a successor
|
|
43
|
+
#
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
def has_next?
|
|
46
|
+
@flags.anybits?(Constants::FLAG_NEXT_CABINET)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if this cabinet has reserved space
|
|
50
|
+
#
|
|
51
|
+
# @return [Boolean]
|
|
52
|
+
def has_reserve?
|
|
53
|
+
@flags.anybits?(Constants::FLAG_RESERVE_PRESENT)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Set the blocks offset and reserved space
|
|
57
|
+
#
|
|
58
|
+
# @param offset [Integer] Offset to data blocks
|
|
59
|
+
# @param resv [Integer] Reserved bytes per block
|
|
60
|
+
# @return [void]
|
|
61
|
+
def set_blocks_info(offset, resv)
|
|
62
|
+
@blocks_offset = offset
|
|
63
|
+
@block_resv = resv
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get total number of files
|
|
67
|
+
#
|
|
68
|
+
# @return [Integer]
|
|
69
|
+
def file_count
|
|
70
|
+
@files.size
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Get total number of folders
|
|
74
|
+
#
|
|
75
|
+
# @return [Integer]
|
|
76
|
+
def folder_count
|
|
77
|
+
@folders.size
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
module Models
|
|
5
|
+
# Represents a file within a CHM archive
|
|
6
|
+
class CHMFile
|
|
7
|
+
attr_accessor :next_file, :section, :offset, :length, :filename
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@next_file = nil
|
|
11
|
+
@section = nil
|
|
12
|
+
@offset = 0
|
|
13
|
+
@length = 0
|
|
14
|
+
@filename = ""
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Check if this is a system file (starts with ::)
|
|
18
|
+
def system_file?
|
|
19
|
+
filename.start_with?("::")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Check if this is an empty file
|
|
23
|
+
def empty?
|
|
24
|
+
length.zero?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "chm_section"
|
|
4
|
+
|
|
5
|
+
module Cabriolet
|
|
6
|
+
module Models
|
|
7
|
+
# Represents a CHM file header and metadata
|
|
8
|
+
class CHMHeader
|
|
9
|
+
attr_accessor :filename, :version, :timestamp, :language, :length, :files, :sysfiles, :sec0, :sec1, :dir_offset,
|
|
10
|
+
:num_chunks, :chunk_size, :density, :depth, :index_root, :first_pmgl, :last_pmgl, :chunk_cache
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@filename = nil
|
|
14
|
+
@version = 0
|
|
15
|
+
@timestamp = 0
|
|
16
|
+
@language = 0
|
|
17
|
+
@length = 0
|
|
18
|
+
@files = nil
|
|
19
|
+
@sysfiles = nil
|
|
20
|
+
@sec0 = CHMSecUncompressed.new(self)
|
|
21
|
+
@sec1 = CHMSecMSCompressed.new(self)
|
|
22
|
+
@dir_offset = 0
|
|
23
|
+
@num_chunks = 0
|
|
24
|
+
@chunk_size = 0
|
|
25
|
+
@density = 0
|
|
26
|
+
@depth = 0
|
|
27
|
+
@index_root = 0
|
|
28
|
+
@first_pmgl = 0
|
|
29
|
+
@last_pmgl = 0
|
|
30
|
+
@chunk_cache = nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Get all files as an array
|
|
34
|
+
def all_files
|
|
35
|
+
result = []
|
|
36
|
+
file = files
|
|
37
|
+
while file
|
|
38
|
+
result << file
|
|
39
|
+
file = file.next_file
|
|
40
|
+
end
|
|
41
|
+
result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get all system files as an array
|
|
45
|
+
def all_sysfiles
|
|
46
|
+
result = []
|
|
47
|
+
file = sysfiles
|
|
48
|
+
while file
|
|
49
|
+
result << file
|
|
50
|
+
file = file.next_file
|
|
51
|
+
end
|
|
52
|
+
result
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Find a file by name
|
|
56
|
+
def find_file(filename)
|
|
57
|
+
file = files
|
|
58
|
+
while file
|
|
59
|
+
return file if file.filename == filename
|
|
60
|
+
|
|
61
|
+
file = file.next_file
|
|
62
|
+
end
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
module Models
|
|
5
|
+
# Base class for CHM sections
|
|
6
|
+
class CHMSection
|
|
7
|
+
attr_accessor :chm, :id
|
|
8
|
+
|
|
9
|
+
def initialize(chm, id)
|
|
10
|
+
@chm = chm
|
|
11
|
+
@id = id
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Section 0: Uncompressed data
|
|
16
|
+
class CHMSecUncompressed < CHMSection
|
|
17
|
+
attr_accessor :offset
|
|
18
|
+
|
|
19
|
+
def initialize(chm)
|
|
20
|
+
super(chm, 0)
|
|
21
|
+
@offset = 0
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Section 1: MSCompressed (LZX) data
|
|
26
|
+
class CHMSecMSCompressed < CHMSection
|
|
27
|
+
attr_accessor :content, :control, :spaninfo, :rtable
|
|
28
|
+
|
|
29
|
+
def initialize(chm)
|
|
30
|
+
super(chm, 1)
|
|
31
|
+
@content = nil
|
|
32
|
+
@control = nil
|
|
33
|
+
@spaninfo = nil
|
|
34
|
+
@rtable = nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Cabriolet
|
|
6
|
+
module Models
|
|
7
|
+
# File represents a file within a cabinet
|
|
8
|
+
class File
|
|
9
|
+
attr_accessor :filename, :length, :offset, :folder, :folder_index, :attribs, :time_h, :time_m, :time_s, :date_d,
|
|
10
|
+
:date_m, :date_y, :next_file
|
|
11
|
+
|
|
12
|
+
# Initialize a new file
|
|
13
|
+
def initialize
|
|
14
|
+
@filename = nil
|
|
15
|
+
@length = 0
|
|
16
|
+
@offset = 0
|
|
17
|
+
@folder = nil
|
|
18
|
+
@folder_index = 0
|
|
19
|
+
@attribs = 0
|
|
20
|
+
@time_h = 0
|
|
21
|
+
@time_m = 0
|
|
22
|
+
@time_s = 0
|
|
23
|
+
@date_d = 1
|
|
24
|
+
@date_m = 1
|
|
25
|
+
@date_y = 1980
|
|
26
|
+
@next_file = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Parse date and time from CAB format
|
|
30
|
+
#
|
|
31
|
+
# @param date_bits [Integer] 16-bit date value
|
|
32
|
+
# @param time_bits [Integer] 16-bit time value
|
|
33
|
+
# @return [void]
|
|
34
|
+
def parse_datetime(date_bits, time_bits)
|
|
35
|
+
@time_h = (time_bits >> 11) & 0x1F
|
|
36
|
+
@time_m = (time_bits >> 5) & 0x3F
|
|
37
|
+
@time_s = (time_bits & 0x1F) << 1
|
|
38
|
+
|
|
39
|
+
@date_d = date_bits & 0x1F
|
|
40
|
+
@date_m = (date_bits >> 5) & 0x0F
|
|
41
|
+
@date_y = ((date_bits >> 9) & 0x7F) + 1980
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get the file's modification time as a Time object
|
|
45
|
+
#
|
|
46
|
+
# @return [Time, nil] Modification time or nil if invalid
|
|
47
|
+
def modification_time
|
|
48
|
+
Time.new(@date_y, @date_m, @date_d, @time_h, @time_m, @time_s)
|
|
49
|
+
rescue ::ArgumentError
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check if filename is UTF-8 encoded
|
|
54
|
+
#
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def utf8_filename?
|
|
57
|
+
@attribs.anybits?(Constants::ATTRIB_UTF_NAME)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Check if file is read-only
|
|
61
|
+
#
|
|
62
|
+
# @return [Boolean]
|
|
63
|
+
def readonly?
|
|
64
|
+
@attribs.anybits?(Constants::ATTRIB_READONLY)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Check if file is hidden
|
|
68
|
+
#
|
|
69
|
+
# @return [Boolean]
|
|
70
|
+
def hidden?
|
|
71
|
+
@attribs.anybits?(Constants::ATTRIB_HIDDEN)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check if file is a system file
|
|
75
|
+
#
|
|
76
|
+
# @return [Boolean]
|
|
77
|
+
def system?
|
|
78
|
+
@attribs.anybits?(Constants::ATTRIB_SYSTEM)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Check if file is archived
|
|
82
|
+
#
|
|
83
|
+
# @return [Boolean]
|
|
84
|
+
def archived?
|
|
85
|
+
@attribs.anybits?(Constants::ATTRIB_ARCH)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Check if file is executable
|
|
89
|
+
#
|
|
90
|
+
# @return [Boolean]
|
|
91
|
+
def executable?
|
|
92
|
+
@attribs.anybits?(Constants::ATTRIB_EXEC)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Check if this file is continued from a previous cabinet
|
|
96
|
+
#
|
|
97
|
+
# @return [Boolean]
|
|
98
|
+
def continued_from_prev?
|
|
99
|
+
@folder_index == Constants::FOLDER_CONTINUED_FROM_PREV ||
|
|
100
|
+
@folder_index == Constants::FOLDER_CONTINUED_PREV_AND_NEXT
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Check if this file is continued to a next cabinet
|
|
104
|
+
#
|
|
105
|
+
# @return [Boolean]
|
|
106
|
+
def continued_to_next?
|
|
107
|
+
@folder_index == Constants::FOLDER_CONTINUED_TO_NEXT ||
|
|
108
|
+
@folder_index == Constants::FOLDER_CONTINUED_PREV_AND_NEXT
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Get a human-readable representation of the file
|
|
112
|
+
#
|
|
113
|
+
# @return [String]
|
|
114
|
+
def to_s
|
|
115
|
+
"#{@filename} (#{@length} bytes)"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "folder_data"
|
|
4
|
+
|
|
5
|
+
module Cabriolet
|
|
6
|
+
module Models
|
|
7
|
+
# Folder represents a compressed data stream within a cabinet
|
|
8
|
+
class Folder
|
|
9
|
+
attr_accessor :comp_type, :num_blocks, :data, :next_folder, :merge_prev,
|
|
10
|
+
:merge_next
|
|
11
|
+
|
|
12
|
+
# Initialize a new folder
|
|
13
|
+
#
|
|
14
|
+
# @param cabinet [Cabinet, nil] Cabinet containing this folder
|
|
15
|
+
# @param offset [Integer] Data offset within cabinet
|
|
16
|
+
def initialize(cabinet = nil, offset = 0)
|
|
17
|
+
@comp_type = Constants::COMP_TYPE_NONE
|
|
18
|
+
@num_blocks = 0
|
|
19
|
+
@data = FolderData.new(cabinet, offset)
|
|
20
|
+
@next_folder = nil
|
|
21
|
+
@merge_prev = nil
|
|
22
|
+
@merge_next = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Get the primary data cabinet (for backwards compatibility)
|
|
26
|
+
#
|
|
27
|
+
# @return [Cabinet, nil]
|
|
28
|
+
def data_cab
|
|
29
|
+
@data.cabinet
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Set the primary data cabinet (for backwards compatibility)
|
|
33
|
+
#
|
|
34
|
+
# @param cabinet [Cabinet]
|
|
35
|
+
def data_cab=(cabinet)
|
|
36
|
+
@data.cabinet = cabinet
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get the primary data offset (for backwards compatibility)
|
|
40
|
+
#
|
|
41
|
+
# @return [Integer]
|
|
42
|
+
def data_offset
|
|
43
|
+
@data.offset
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Set the primary data offset (for backwards compatibility)
|
|
47
|
+
#
|
|
48
|
+
# @param offset [Integer]
|
|
49
|
+
def data_offset=(offset)
|
|
50
|
+
@data.offset = offset
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get the compression method
|
|
54
|
+
#
|
|
55
|
+
# @return [Integer] One of COMP_TYPE_* constants
|
|
56
|
+
def compression_method
|
|
57
|
+
@comp_type & Constants::COMP_TYPE_MASK
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get the compression level (for LZX and Quantum)
|
|
61
|
+
#
|
|
62
|
+
# @return [Integer] Compression level
|
|
63
|
+
def compression_level
|
|
64
|
+
(@comp_type >> 8) & 0x1F
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get human-readable compression name
|
|
68
|
+
#
|
|
69
|
+
# @return [String] Name of compression method
|
|
70
|
+
def compression_name
|
|
71
|
+
case compression_method
|
|
72
|
+
when Constants::COMP_TYPE_NONE then "None"
|
|
73
|
+
when Constants::COMP_TYPE_MSZIP then "MSZIP"
|
|
74
|
+
when Constants::COMP_TYPE_QUANTUM then "Quantum"
|
|
75
|
+
when Constants::COMP_TYPE_LZX then "LZX"
|
|
76
|
+
else "Unknown"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Check if this folder is uncompressed
|
|
81
|
+
#
|
|
82
|
+
# @return [Boolean]
|
|
83
|
+
def uncompressed?
|
|
84
|
+
compression_method == Constants::COMP_TYPE_NONE
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Check if this folder needs to be merged with a previous folder
|
|
88
|
+
#
|
|
89
|
+
# @return [Boolean]
|
|
90
|
+
def needs_prev_merge?
|
|
91
|
+
!@merge_prev.nil?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Check if this folder needs to be merged with a next folder
|
|
95
|
+
#
|
|
96
|
+
# @return [Boolean]
|
|
97
|
+
def needs_next_merge?
|
|
98
|
+
!@merge_next.nil?
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
module Models
|
|
5
|
+
# FolderData represents a data location span for a folder
|
|
6
|
+
# Folders may span multiple cabinets, so they have a chain of FolderData
|
|
7
|
+
class FolderData
|
|
8
|
+
attr_accessor :next_data, :cabinet, :offset
|
|
9
|
+
|
|
10
|
+
# Initialize a new FolderData
|
|
11
|
+
#
|
|
12
|
+
# @param cabinet [Cabinet] Cabinet containing this data
|
|
13
|
+
# @param offset [Integer] Offset within cabinet file to data blocks
|
|
14
|
+
def initialize(cabinet, offset)
|
|
15
|
+
@cabinet = cabinet
|
|
16
|
+
@offset = offset
|
|
17
|
+
@next_data = nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
module Models
|
|
5
|
+
# HLP internal file model
|
|
6
|
+
#
|
|
7
|
+
# Represents a file within an HLP archive. HLP files contain an internal
|
|
8
|
+
# file system where each file can be compressed using LZSS MODE_MSHELP.
|
|
9
|
+
class HLPFile
|
|
10
|
+
attr_accessor :filename, :offset, :length, :compressed_length,
|
|
11
|
+
:compressed, :data
|
|
12
|
+
|
|
13
|
+
# Initialize HLP file
|
|
14
|
+
#
|
|
15
|
+
# @param filename [String] File name within the HLP archive
|
|
16
|
+
# @param offset [Integer] Offset in the HLP archive
|
|
17
|
+
# @param length [Integer] Uncompressed file length
|
|
18
|
+
# @param compressed_length [Integer] Compressed file length
|
|
19
|
+
# @param compressed [Boolean] Whether the file is compressed
|
|
20
|
+
def initialize(filename: nil, offset: 0, length: 0,
|
|
21
|
+
compressed_length: 0, compressed: true)
|
|
22
|
+
@filename = filename
|
|
23
|
+
@offset = offset
|
|
24
|
+
@length = length
|
|
25
|
+
@compressed_length = compressed_length
|
|
26
|
+
@compressed = compressed
|
|
27
|
+
@data = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Check if file is compressed
|
|
31
|
+
#
|
|
32
|
+
# @return [Boolean] true if file is compressed
|
|
33
|
+
def compressed?
|
|
34
|
+
@compressed
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get the size to read from archive
|
|
38
|
+
#
|
|
39
|
+
# @return [Integer] Size to read (compressed or uncompressed)
|
|
40
|
+
def read_size
|
|
41
|
+
compressed? ? @compressed_length : @length
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
module Models
|
|
5
|
+
# HLP file header model
|
|
6
|
+
#
|
|
7
|
+
# NOTE: This implementation is based on the knowledge that HLP files use
|
|
8
|
+
# LZSS compression with MODE_MSHELP, but cannot be fully validated due to
|
|
9
|
+
# lack of test fixtures. Testing relies on round-trip
|
|
10
|
+
# compression/decompression and comparison with libmspack tools if
|
|
11
|
+
# available.
|
|
12
|
+
class HLPHeader
|
|
13
|
+
attr_accessor :magic, :version, :filename, :length, :files
|
|
14
|
+
|
|
15
|
+
# Initialize HLP header
|
|
16
|
+
#
|
|
17
|
+
# @param magic [String] Magic number (should be specific to HLP)
|
|
18
|
+
# @param version [Integer] Format version
|
|
19
|
+
# @param filename [String] Original filename
|
|
20
|
+
# @param length [Integer] Uncompressed file length
|
|
21
|
+
def initialize(magic: nil, version: nil, filename: nil, length: 0)
|
|
22
|
+
@magic = magic
|
|
23
|
+
@version = version
|
|
24
|
+
@filename = filename
|
|
25
|
+
@length = length
|
|
26
|
+
@files = []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Check if header is valid
|
|
30
|
+
#
|
|
31
|
+
# @return [Boolean] true if header appears valid
|
|
32
|
+
def valid?
|
|
33
|
+
!@magic.nil? && !@version.nil?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
module Models
|
|
5
|
+
# Represents a KWAJ file header
|
|
6
|
+
#
|
|
7
|
+
# KWAJ files support multiple compression methods and optional headers
|
|
8
|
+
# determined by flag bits. The header structure is more flexible than SZDD.
|
|
9
|
+
class KWAJHeader
|
|
10
|
+
# Compression type
|
|
11
|
+
# @return [Integer] One of KWAJ_COMP_* constants
|
|
12
|
+
attr_accessor :comp_type
|
|
13
|
+
|
|
14
|
+
# Offset to compressed data
|
|
15
|
+
# @return [Integer] Byte offset where compressed data starts
|
|
16
|
+
attr_accessor :data_offset
|
|
17
|
+
|
|
18
|
+
# Header flags
|
|
19
|
+
# @return [Integer] Bitfield indicating which optional headers are present
|
|
20
|
+
attr_accessor :headers
|
|
21
|
+
|
|
22
|
+
# Uncompressed length
|
|
23
|
+
# @return [Integer, nil] Length of uncompressed data if present
|
|
24
|
+
attr_accessor :length
|
|
25
|
+
|
|
26
|
+
# Original filename
|
|
27
|
+
# @return [String, nil] Original filename if present
|
|
28
|
+
attr_accessor :filename
|
|
29
|
+
|
|
30
|
+
# Extra text data
|
|
31
|
+
# @return [String, nil] Extra text data if present
|
|
32
|
+
attr_accessor :extra
|
|
33
|
+
|
|
34
|
+
# Length of extra data
|
|
35
|
+
# @return [Integer] Number of bytes in extra data
|
|
36
|
+
attr_accessor :extra_length
|
|
37
|
+
|
|
38
|
+
# Initialize a new KWAJ header
|
|
39
|
+
def initialize
|
|
40
|
+
@comp_type = Constants::KWAJ_COMP_NONE
|
|
41
|
+
@data_offset = 0
|
|
42
|
+
@headers = 0
|
|
43
|
+
@length = nil
|
|
44
|
+
@filename = nil
|
|
45
|
+
@extra = nil
|
|
46
|
+
@extra_length = 0
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Get human-readable compression type name
|
|
50
|
+
#
|
|
51
|
+
# @return [String] Compression type name
|
|
52
|
+
def compression_name
|
|
53
|
+
case @comp_type
|
|
54
|
+
when Constants::KWAJ_COMP_NONE
|
|
55
|
+
"None"
|
|
56
|
+
when Constants::KWAJ_COMP_XOR
|
|
57
|
+
"XOR"
|
|
58
|
+
when Constants::KWAJ_COMP_SZDD
|
|
59
|
+
"SZDD"
|
|
60
|
+
when Constants::KWAJ_COMP_LZH
|
|
61
|
+
"LZH"
|
|
62
|
+
when Constants::KWAJ_COMP_MSZIP
|
|
63
|
+
"MSZIP"
|
|
64
|
+
else
|
|
65
|
+
"Unknown (#{@comp_type})"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Check if header has length field
|
|
70
|
+
#
|
|
71
|
+
# @return [Boolean] true if length is present
|
|
72
|
+
def has_length?
|
|
73
|
+
@headers.anybits?(Constants::KWAJ_HDR_HASLENGTH)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Check if header has filename
|
|
77
|
+
#
|
|
78
|
+
# @return [Boolean] true if filename is present
|
|
79
|
+
def has_filename?
|
|
80
|
+
@headers.anybits?(Constants::KWAJ_HDR_HASFILENAME)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Check if header has file extension
|
|
84
|
+
#
|
|
85
|
+
# @return [Boolean] true if file extension is present
|
|
86
|
+
def has_file_extension?
|
|
87
|
+
@headers.anybits?(Constants::KWAJ_HDR_HASFILEEXT)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check if header has extra text
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean] true if extra text is present
|
|
93
|
+
def has_extra_text?
|
|
94
|
+
@headers.anybits?(Constants::KWAJ_HDR_HASEXTRATEXT)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
module Models
|
|
5
|
+
# Represents the header of a Microsoft Reader LIT file
|
|
6
|
+
#
|
|
7
|
+
# LIT files are Microsoft Reader eBook files that use LZX compression
|
|
8
|
+
# and may use DES encryption for DRM-protected content.
|
|
9
|
+
class LITHeader
|
|
10
|
+
attr_accessor :version, :filename, :length, :encrypted, :files
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@version = 0
|
|
14
|
+
@filename = ""
|
|
15
|
+
@length = 0
|
|
16
|
+
@encrypted = false
|
|
17
|
+
@files = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Check if the LIT file is encrypted
|
|
21
|
+
#
|
|
22
|
+
# @return [Boolean] true if the file uses DES encryption
|
|
23
|
+
def encrypted?
|
|
24
|
+
@encrypted
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Represents a file entry within a LIT archive
|
|
29
|
+
class LITFile
|
|
30
|
+
attr_accessor :filename, :offset, :length, :compressed, :encrypted
|
|
31
|
+
|
|
32
|
+
def initialize
|
|
33
|
+
@filename = ""
|
|
34
|
+
@offset = 0
|
|
35
|
+
@length = 0
|
|
36
|
+
@compressed = true
|
|
37
|
+
@encrypted = false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Check if the file is compressed
|
|
41
|
+
#
|
|
42
|
+
# @return [Boolean] true if the file uses LZX compression
|
|
43
|
+
def compressed?
|
|
44
|
+
@compressed
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Check if the file is encrypted
|
|
48
|
+
#
|
|
49
|
+
# @return [Boolean] true if the file uses DES encryption
|
|
50
|
+
def encrypted?
|
|
51
|
+
@encrypted
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|