cabriolet 0.1.2 → 0.2.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 +4 -4
- data/README.adoc +700 -38
- data/lib/cabriolet/algorithm_factory.rb +250 -0
- data/lib/cabriolet/base_compressor.rb +206 -0
- data/lib/cabriolet/binary/bitstream.rb +154 -14
- data/lib/cabriolet/binary/bitstream_writer.rb +129 -17
- 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 +35 -43
- data/lib/cabriolet/cab/decompressor.rb +14 -19
- data/lib/cabriolet/cab/extractor.rb +140 -31
- 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/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 +34 -45
- 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 +3 -2
- data/lib/cabriolet/errors.rb +3 -0
- data/lib/cabriolet/file_entry.rb +156 -0
- data/lib/cabriolet/file_manager.rb +144 -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 +626 -0
- data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -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/parser.rb +274 -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/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 +633 -38
- data/lib/cabriolet/lit/decompressor.rb +518 -152
- data/lib/cabriolet/lit/parser.rb +670 -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/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 +67 -17
- metadata +33 -2
|
@@ -2,44 +2,145 @@
|
|
|
2
2
|
|
|
3
3
|
module Cabriolet
|
|
4
4
|
module Models
|
|
5
|
-
#
|
|
5
|
+
# QuickHelp topic model
|
|
6
6
|
#
|
|
7
|
-
# Represents a
|
|
8
|
-
#
|
|
9
|
-
class
|
|
10
|
-
attr_accessor :
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# @param
|
|
16
|
-
# @param
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
7
|
+
# Represents a single topic in a QuickHelp help database.
|
|
8
|
+
# Each topic contains formatted text lines with styles and hyperlinks.
|
|
9
|
+
class HLPTopic
|
|
10
|
+
attr_accessor :index, :offset, :size, :lines, :source_data, :metadata
|
|
11
|
+
|
|
12
|
+
# Initialize a QuickHelp topic
|
|
13
|
+
#
|
|
14
|
+
# @param index [Integer] Topic index in the database
|
|
15
|
+
# @param offset [Integer] Offset of topic data in file
|
|
16
|
+
# @param size [Integer] Size of compressed topic data
|
|
17
|
+
def initialize(index: 0, offset: 0, size: 0)
|
|
18
|
+
@index = index
|
|
23
19
|
@offset = offset
|
|
24
|
-
@
|
|
25
|
-
@
|
|
26
|
-
@
|
|
27
|
-
@
|
|
20
|
+
@size = size
|
|
21
|
+
@lines = []
|
|
22
|
+
@source_data = nil
|
|
23
|
+
@metadata = {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Check if topic has any content
|
|
27
|
+
#
|
|
28
|
+
# @return [Boolean] true if topic has lines
|
|
29
|
+
def empty?
|
|
30
|
+
@lines.empty?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Get plain text content (without formatting)
|
|
34
|
+
#
|
|
35
|
+
# @return [String] plain text of all lines
|
|
36
|
+
def plain_text
|
|
37
|
+
@lines.map(&:text).join("\n")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Add a line to the topic
|
|
41
|
+
#
|
|
42
|
+
# @param line [HLPLine] line to add
|
|
43
|
+
# @return [void]
|
|
44
|
+
def add_line(line)
|
|
45
|
+
@lines << line
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# QuickHelp topic line model
|
|
50
|
+
#
|
|
51
|
+
# Represents a single line within a topic, including text, styles, and links.
|
|
52
|
+
class HLPLine
|
|
53
|
+
attr_accessor :text, :attributes
|
|
54
|
+
|
|
55
|
+
# Initialize a topic line
|
|
56
|
+
#
|
|
57
|
+
# @param text [String] plain text content
|
|
58
|
+
def initialize(text = "")
|
|
59
|
+
@text = text
|
|
60
|
+
@attributes = Array.new(text.length) { TextAttribute.new }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get line length in characters
|
|
64
|
+
#
|
|
65
|
+
# @return [Integer] character count
|
|
66
|
+
def length
|
|
67
|
+
@text.length
|
|
28
68
|
end
|
|
29
69
|
|
|
30
|
-
#
|
|
70
|
+
# Apply style to a range of characters
|
|
31
71
|
#
|
|
32
|
-
# @
|
|
33
|
-
|
|
34
|
-
|
|
72
|
+
# @param start_index [Integer] start position (0-based)
|
|
73
|
+
# @param end_index [Integer] end position (0-based, inclusive)
|
|
74
|
+
# @param style [Integer] style flags
|
|
75
|
+
# @return [void]
|
|
76
|
+
def apply_style(start_index, end_index, style)
|
|
77
|
+
(start_index..end_index).each do |i|
|
|
78
|
+
@attributes[i].style = style if i < @attributes.length
|
|
79
|
+
end
|
|
35
80
|
end
|
|
36
81
|
|
|
37
|
-
#
|
|
82
|
+
# Apply link to a range of characters
|
|
38
83
|
#
|
|
39
|
-
# @
|
|
40
|
-
|
|
41
|
-
|
|
84
|
+
# @param start_index [Integer] start position (1-based, as per format)
|
|
85
|
+
# @param end_index [Integer] end position (1-based, inclusive)
|
|
86
|
+
# @param link [String] link target (context string or topic index)
|
|
87
|
+
# @return [void]
|
|
88
|
+
def apply_link(start_index, end_index, link)
|
|
89
|
+
# Convert from 1-based to 0-based indexing
|
|
90
|
+
start_idx = start_index - 1
|
|
91
|
+
end_idx = end_index - 1
|
|
92
|
+
|
|
93
|
+
(start_idx..end_idx).each do |i|
|
|
94
|
+
@attributes[i].link = link if i >= 0 && i < @attributes.length
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Text attribute model
|
|
100
|
+
#
|
|
101
|
+
# Represents style and link information for a single character.
|
|
102
|
+
class TextAttribute
|
|
103
|
+
attr_accessor :style, :link
|
|
104
|
+
|
|
105
|
+
# Initialize text attribute
|
|
106
|
+
#
|
|
107
|
+
# @param style [Integer] style flags (bold, italic, underline)
|
|
108
|
+
# @param link [String, nil] link target if any
|
|
109
|
+
def initialize(style = 0, link = nil)
|
|
110
|
+
@style = style
|
|
111
|
+
@link = link
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check if character is bold
|
|
115
|
+
#
|
|
116
|
+
# @return [Boolean] true if bold
|
|
117
|
+
def bold?
|
|
118
|
+
@style.anybits?(Binary::HLPStructures::TextStyle::BOLD)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Check if character is italic
|
|
122
|
+
#
|
|
123
|
+
# @return [Boolean] true if italic
|
|
124
|
+
def italic?
|
|
125
|
+
@style.anybits?(Binary::HLPStructures::TextStyle::ITALIC)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Check if character is underlined
|
|
129
|
+
#
|
|
130
|
+
# @return [Boolean] true if underlined
|
|
131
|
+
def underline?
|
|
132
|
+
@style.anybits?(Binary::HLPStructures::TextStyle::UNDERLINE)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Check if character has a link
|
|
136
|
+
#
|
|
137
|
+
# @return [Boolean] true if linked
|
|
138
|
+
def linked?
|
|
139
|
+
!@link.nil?
|
|
42
140
|
end
|
|
43
141
|
end
|
|
142
|
+
|
|
143
|
+
# Backward compatibility alias
|
|
144
|
+
HLPFile = HLPTopic
|
|
44
145
|
end
|
|
45
146
|
end
|
|
@@ -2,35 +2,123 @@
|
|
|
2
2
|
|
|
3
3
|
module Cabriolet
|
|
4
4
|
module Models
|
|
5
|
-
#
|
|
5
|
+
# QuickHelp database header model
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# compression/decompression and comparison with libmspack tools if
|
|
11
|
-
# available.
|
|
7
|
+
# Represents the metadata of a QuickHelp help database (.HLP file).
|
|
8
|
+
# HLP files contain topics, context strings, and optional compression
|
|
9
|
+
# (keyword dictionary and Huffman coding).
|
|
12
10
|
class HLPHeader
|
|
13
|
-
attr_accessor :magic, :version, :
|
|
11
|
+
attr_accessor :magic, :version, :attributes, :control_character,
|
|
12
|
+
:topic_count, :context_count, :display_width, :predefined_ctx_count, :database_name, :topic_index_offset, :context_strings_offset, :context_map_offset, :keywords_offset, :huffman_tree_offset, :topic_text_offset, :database_size, :filename, :keywords, :huffman_tree
|
|
14
13
|
|
|
15
|
-
#
|
|
14
|
+
# Topics and context data
|
|
15
|
+
attr_accessor :topics, :contexts, :context_map
|
|
16
|
+
|
|
17
|
+
# Initialize QuickHelp database header
|
|
16
18
|
#
|
|
17
|
-
# @param magic [String] Magic number (should be
|
|
18
|
-
# @param version [Integer] Format version
|
|
19
|
-
# @param
|
|
20
|
-
# @param
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
# @param magic [String] Magic number (should be 0x4C 0x4E)
|
|
20
|
+
# @param version [Integer] Format version (should be 2)
|
|
21
|
+
# @param attributes [Integer] Attribute flags
|
|
22
|
+
# @param control_character [Integer] Control character (usually ':' or 0xFF)
|
|
23
|
+
# @param topic_count [Integer] Number of topics
|
|
24
|
+
# @param context_count [Integer] Number of context strings
|
|
25
|
+
# @param display_width [Integer] Display width in characters
|
|
26
|
+
# @param database_name [String] Database name for external links
|
|
27
|
+
def initialize(
|
|
28
|
+
magic: nil,
|
|
29
|
+
version: 2,
|
|
30
|
+
attributes: 0,
|
|
31
|
+
control_character: 0x3A,
|
|
32
|
+
topic_count: 0,
|
|
33
|
+
context_count: 0,
|
|
34
|
+
display_width: 80,
|
|
35
|
+
predefined_ctx_count: 0,
|
|
36
|
+
database_name: "",
|
|
37
|
+
topic_index_offset: 0,
|
|
38
|
+
context_strings_offset: 0,
|
|
39
|
+
context_map_offset: 0,
|
|
40
|
+
keywords_offset: 0,
|
|
41
|
+
huffman_tree_offset: 0,
|
|
42
|
+
topic_text_offset: 0,
|
|
43
|
+
database_size: 0,
|
|
44
|
+
filename: nil
|
|
45
|
+
)
|
|
46
|
+
@magic = magic || Binary::HLPStructures::SIGNATURE
|
|
23
47
|
@version = version
|
|
48
|
+
@attributes = attributes
|
|
49
|
+
@control_character = control_character
|
|
50
|
+
@topic_count = topic_count
|
|
51
|
+
@context_count = context_count
|
|
52
|
+
@display_width = display_width
|
|
53
|
+
@predefined_ctx_count = predefined_ctx_count
|
|
54
|
+
@database_name = database_name
|
|
55
|
+
@topic_index_offset = topic_index_offset
|
|
56
|
+
@context_strings_offset = context_strings_offset
|
|
57
|
+
@context_map_offset = context_map_offset
|
|
58
|
+
@keywords_offset = keywords_offset
|
|
59
|
+
@huffman_tree_offset = huffman_tree_offset
|
|
60
|
+
@topic_text_offset = topic_text_offset
|
|
61
|
+
@database_size = database_size
|
|
24
62
|
@filename = filename
|
|
25
|
-
|
|
26
|
-
|
|
63
|
+
|
|
64
|
+
# Collections
|
|
65
|
+
@topics = []
|
|
66
|
+
@contexts = []
|
|
67
|
+
@context_map = []
|
|
68
|
+
@keywords = []
|
|
69
|
+
@huffman_tree = nil
|
|
27
70
|
end
|
|
28
71
|
|
|
29
72
|
# Check if header is valid
|
|
30
73
|
#
|
|
31
74
|
# @return [Boolean] true if header appears valid
|
|
32
75
|
def valid?
|
|
33
|
-
|
|
76
|
+
@magic == Binary::HLPStructures::SIGNATURE &&
|
|
77
|
+
@version == 2 &&
|
|
78
|
+
@topic_count >= 0 &&
|
|
79
|
+
@context_count >= 0
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check if case-sensitive context strings
|
|
83
|
+
#
|
|
84
|
+
# @return [Boolean] true if case-sensitive
|
|
85
|
+
def case_sensitive?
|
|
86
|
+
@attributes.anybits?(Binary::HLPStructures::Attributes::CASE_SENSITIVE)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Check if database is locked (cannot be decoded by HELPMAKE)
|
|
90
|
+
#
|
|
91
|
+
# @return [Boolean] true if locked
|
|
92
|
+
def locked?
|
|
93
|
+
@attributes.anybits?(Binary::HLPStructures::Attributes::LOCKED)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Check if keyword compression is used
|
|
97
|
+
#
|
|
98
|
+
# @return [Boolean] true if keywords present
|
|
99
|
+
def has_keywords?
|
|
100
|
+
@keywords_offset.positive? && !@keywords.empty?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Check if Huffman compression is used
|
|
104
|
+
#
|
|
105
|
+
# @return [Boolean] true if Huffman tree present
|
|
106
|
+
def has_huffman?
|
|
107
|
+
@huffman_tree_offset.positive? && !@huffman_tree.nil?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Get control character as string
|
|
111
|
+
#
|
|
112
|
+
# @return [String] control character
|
|
113
|
+
def control_char
|
|
114
|
+
@control_character.chr(Encoding::ASCII)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Get database name without null padding
|
|
118
|
+
#
|
|
119
|
+
# @return [String] trimmed database name
|
|
120
|
+
def db_name
|
|
121
|
+
@database_name.split("\x00").first || ""
|
|
34
122
|
end
|
|
35
123
|
end
|
|
36
124
|
end
|
|
@@ -2,53 +2,240 @@
|
|
|
2
2
|
|
|
3
3
|
module Cabriolet
|
|
4
4
|
module Models
|
|
5
|
-
# Represents
|
|
5
|
+
# Represents a Microsoft Reader LIT file structure
|
|
6
6
|
#
|
|
7
|
-
# LIT files
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
# LIT files have a complex structure with:
|
|
8
|
+
# - Primary and secondary headers
|
|
9
|
+
# - Piece table pointing to various data structures
|
|
10
|
+
# - Internal directory with IFCM/AOLL/AOLI chunks
|
|
11
|
+
# - DataSpace sections with transformation layers (compression/encryption)
|
|
12
|
+
# - Manifest mapping internal to original filenames
|
|
13
|
+
class LITFile
|
|
14
|
+
attr_accessor :version, :header_guid, :piece3_guid, :piece4_guid,
|
|
15
|
+
:content_offset, :timestamp, :language_id, :creator_id,
|
|
16
|
+
:entry_chunklen, :count_chunklen, :entry_unknown,
|
|
17
|
+
:count_unknown, :drm_level, :sections, :directory, :manifest
|
|
11
18
|
|
|
12
19
|
def initialize
|
|
13
20
|
@version = 0
|
|
14
|
-
@
|
|
15
|
-
@
|
|
16
|
-
@
|
|
17
|
-
@
|
|
21
|
+
@header_guid = ""
|
|
22
|
+
@piece3_guid = ""
|
|
23
|
+
@piece4_guid = ""
|
|
24
|
+
@content_offset = 0
|
|
25
|
+
@timestamp = 0
|
|
26
|
+
@language_id = 0
|
|
27
|
+
@creator_id = 0
|
|
28
|
+
@entry_chunklen = 0
|
|
29
|
+
@count_chunklen = 0
|
|
30
|
+
@entry_unknown = 0
|
|
31
|
+
@count_unknown = 0
|
|
32
|
+
@drm_level = 0
|
|
33
|
+
@sections = []
|
|
34
|
+
@directory = nil
|
|
35
|
+
@manifest = nil
|
|
18
36
|
end
|
|
19
37
|
|
|
20
|
-
# Check if the LIT file
|
|
38
|
+
# Check if the LIT file has DRM encryption
|
|
21
39
|
#
|
|
22
|
-
# @return [Boolean] true if
|
|
40
|
+
# @return [Boolean] true if DRM is present
|
|
23
41
|
def encrypted?
|
|
24
|
-
|
|
42
|
+
drm_level.positive?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get section by name
|
|
46
|
+
#
|
|
47
|
+
# @param name [String] Section name
|
|
48
|
+
# @return [LITSection, nil] The section or nil if not found
|
|
49
|
+
def section(name)
|
|
50
|
+
sections.find { |s| s.name == name }
|
|
25
51
|
end
|
|
26
52
|
end
|
|
27
53
|
|
|
28
|
-
# Represents a
|
|
29
|
-
|
|
30
|
-
|
|
54
|
+
# Represents a section within the LIT file
|
|
55
|
+
#
|
|
56
|
+
# Sections contain compressed/encrypted data with transform layers
|
|
57
|
+
class LITSection
|
|
58
|
+
attr_accessor :name, :transforms, :compressed, :encrypted,
|
|
59
|
+
:uncompressed_length, :compressed_length,
|
|
60
|
+
:window_size, :reset_interval, :reset_table
|
|
31
61
|
|
|
32
62
|
def initialize
|
|
33
|
-
@
|
|
34
|
-
@
|
|
35
|
-
@
|
|
36
|
-
@compressed = true
|
|
63
|
+
@name = ""
|
|
64
|
+
@transforms = []
|
|
65
|
+
@compressed = false
|
|
37
66
|
@encrypted = false
|
|
67
|
+
@uncompressed_length = 0
|
|
68
|
+
@compressed_length = 0
|
|
69
|
+
@window_size = 0
|
|
70
|
+
@reset_interval = 0
|
|
71
|
+
@reset_table = []
|
|
38
72
|
end
|
|
39
73
|
|
|
40
|
-
# Check if
|
|
74
|
+
# Check if section is compressed
|
|
41
75
|
#
|
|
42
|
-
# @return [Boolean] true if
|
|
76
|
+
# @return [Boolean] true if compressed
|
|
43
77
|
def compressed?
|
|
44
|
-
|
|
78
|
+
compressed
|
|
45
79
|
end
|
|
46
80
|
|
|
47
|
-
# Check if
|
|
81
|
+
# Check if section is encrypted
|
|
48
82
|
#
|
|
49
|
-
# @return [Boolean] true if
|
|
83
|
+
# @return [Boolean] true if encrypted
|
|
50
84
|
def encrypted?
|
|
51
|
-
|
|
85
|
+
encrypted
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Represents the internal directory structure
|
|
90
|
+
#
|
|
91
|
+
# Directory contains file entries with encoded integers for efficiency
|
|
92
|
+
class LITDirectory
|
|
93
|
+
attr_accessor :entries, :num_chunks, :entry_chunklen, :count_chunklen
|
|
94
|
+
|
|
95
|
+
def initialize
|
|
96
|
+
@entries = []
|
|
97
|
+
@num_chunks = 0
|
|
98
|
+
@entry_chunklen = 0
|
|
99
|
+
@count_chunklen = 0
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Find entry by name
|
|
103
|
+
#
|
|
104
|
+
# @param name [String] Entry name
|
|
105
|
+
# @return [LITDirectoryEntry, nil] The entry or nil if not found
|
|
106
|
+
def find(name)
|
|
107
|
+
entries.find { |e| e.name == name }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Get all entries in a section
|
|
111
|
+
#
|
|
112
|
+
# @param section_id [Integer] Section ID
|
|
113
|
+
# @return [Array<LITDirectoryEntry>] Entries in the section
|
|
114
|
+
def entries_in_section(section_id)
|
|
115
|
+
entries.select { |e| e.section == section_id }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Represents a single directory entry
|
|
120
|
+
#
|
|
121
|
+
# Entries use variable-length encoded integers to save space
|
|
122
|
+
class LITDirectoryEntry
|
|
123
|
+
attr_accessor :name, :section, :offset, :size
|
|
124
|
+
|
|
125
|
+
def initialize
|
|
126
|
+
@name = ""
|
|
127
|
+
@section = 0
|
|
128
|
+
@offset = 0
|
|
129
|
+
@size = 0
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Check if this is a root entry
|
|
133
|
+
#
|
|
134
|
+
# @return [Boolean] true if root entry
|
|
135
|
+
def root?
|
|
136
|
+
["/", ""].include?(name)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Get the directory portion of the name
|
|
140
|
+
#
|
|
141
|
+
# @return [String] Directory path
|
|
142
|
+
def directory
|
|
143
|
+
return "/" if root?
|
|
144
|
+
|
|
145
|
+
parts = name.split("/")
|
|
146
|
+
parts[0..-2].join("/")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Get the filename portion
|
|
150
|
+
#
|
|
151
|
+
# @return [String] Filename
|
|
152
|
+
def filename
|
|
153
|
+
return "" if root?
|
|
154
|
+
|
|
155
|
+
name.split("/").last
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Represents the manifest file
|
|
160
|
+
#
|
|
161
|
+
# Maps internal filenames to original filenames and content types
|
|
162
|
+
class LITManifest
|
|
163
|
+
attr_accessor :mappings
|
|
164
|
+
|
|
165
|
+
def initialize
|
|
166
|
+
@mappings = []
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Find mapping by internal name
|
|
170
|
+
#
|
|
171
|
+
# @param internal_name [String] Internal filename
|
|
172
|
+
# @return [LITManifestMapping, nil] The mapping or nil
|
|
173
|
+
def find_by_internal(internal_name)
|
|
174
|
+
mappings.find { |m| m.internal_name == internal_name }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Find mapping by original name
|
|
178
|
+
#
|
|
179
|
+
# @param original_name [String] Original filename
|
|
180
|
+
# @return [LITManifestMapping, nil] The mapping or nil
|
|
181
|
+
def find_by_original(original_name)
|
|
182
|
+
mappings.find { |m| m.original_name == original_name }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Get all HTML files
|
|
186
|
+
#
|
|
187
|
+
# @return [Array<LITManifestMapping>] HTML file mappings
|
|
188
|
+
def html_files
|
|
189
|
+
mappings.select { |m| m.content_type =~ /html/i }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Get all CSS files
|
|
193
|
+
#
|
|
194
|
+
# @return [Array<LITManifestMapping>] CSS file mappings
|
|
195
|
+
def css_files
|
|
196
|
+
mappings.select { |m| m.content_type =~ /css/i }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Get all image files
|
|
200
|
+
#
|
|
201
|
+
# @return [Array<LITManifestMapping>] Image file mappings
|
|
202
|
+
def image_files
|
|
203
|
+
mappings.select { |m| m.content_type =~ /image/i }
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Represents a single manifest mapping
|
|
208
|
+
class LITManifestMapping
|
|
209
|
+
attr_accessor :offset, :internal_name, :original_name, :content_type,
|
|
210
|
+
:group
|
|
211
|
+
|
|
212
|
+
def initialize
|
|
213
|
+
@offset = 0
|
|
214
|
+
@internal_name = ""
|
|
215
|
+
@original_name = ""
|
|
216
|
+
@content_type = ""
|
|
217
|
+
@group = 0
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Check if this is an HTML file
|
|
221
|
+
#
|
|
222
|
+
# @return [Boolean] true if HTML
|
|
223
|
+
def html?
|
|
224
|
+
content_type =~ /html/i
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Check if this is a CSS file
|
|
228
|
+
#
|
|
229
|
+
# @return [Boolean] true if CSS
|
|
230
|
+
def css?
|
|
231
|
+
content_type =~ /css/i
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Check if this is an image
|
|
235
|
+
#
|
|
236
|
+
# @return [Boolean] true if image
|
|
237
|
+
def image?
|
|
238
|
+
content_type =~ /image/i
|
|
52
239
|
end
|
|
53
240
|
end
|
|
54
241
|
end
|
|
@@ -64,8 +64,16 @@ module Cabriolet
|
|
|
64
64
|
return compressed_filename unless normal_format? && @missing_char
|
|
65
65
|
|
|
66
66
|
# Replace trailing underscore with missing character
|
|
67
|
-
#
|
|
68
|
-
compressed_filename.
|
|
67
|
+
# Uppercase unless all extension characters are lowercase
|
|
68
|
+
extension_match = compressed_filename.match(/\.(\w+)_$/)
|
|
69
|
+
if extension_match
|
|
70
|
+
extension = extension_match[1]
|
|
71
|
+
# Uppercase unless extension is entirely lowercase
|
|
72
|
+
missing_char = extension == extension.downcase ? @missing_char.downcase : @missing_char.upcase
|
|
73
|
+
compressed_filename.sub(/\.(\w+)_$/, ".\\1#{missing_char}")
|
|
74
|
+
else
|
|
75
|
+
compressed_filename
|
|
76
|
+
end
|
|
69
77
|
end
|
|
70
78
|
end
|
|
71
79
|
end
|