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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/ARCHITECTURE.md +799 -0
  3. data/CHANGELOG.md +44 -0
  4. data/LICENSE +29 -0
  5. data/README.adoc +1207 -0
  6. data/exe/cabriolet +6 -0
  7. data/lib/cabriolet/auto.rb +173 -0
  8. data/lib/cabriolet/binary/bitstream.rb +148 -0
  9. data/lib/cabriolet/binary/bitstream_writer.rb +180 -0
  10. data/lib/cabriolet/binary/chm_structures.rb +213 -0
  11. data/lib/cabriolet/binary/hlp_structures.rb +66 -0
  12. data/lib/cabriolet/binary/kwaj_structures.rb +74 -0
  13. data/lib/cabriolet/binary/lit_structures.rb +107 -0
  14. data/lib/cabriolet/binary/oab_structures.rb +112 -0
  15. data/lib/cabriolet/binary/structures.rb +56 -0
  16. data/lib/cabriolet/binary/szdd_structures.rb +60 -0
  17. data/lib/cabriolet/cab/compressor.rb +382 -0
  18. data/lib/cabriolet/cab/decompressor.rb +510 -0
  19. data/lib/cabriolet/cab/extractor.rb +357 -0
  20. data/lib/cabriolet/cab/parser.rb +264 -0
  21. data/lib/cabriolet/chm/compressor.rb +513 -0
  22. data/lib/cabriolet/chm/decompressor.rb +436 -0
  23. data/lib/cabriolet/chm/parser.rb +254 -0
  24. data/lib/cabriolet/cli.rb +776 -0
  25. data/lib/cabriolet/compressors/base.rb +34 -0
  26. data/lib/cabriolet/compressors/lzss.rb +250 -0
  27. data/lib/cabriolet/compressors/lzx.rb +581 -0
  28. data/lib/cabriolet/compressors/mszip.rb +315 -0
  29. data/lib/cabriolet/compressors/quantum.rb +446 -0
  30. data/lib/cabriolet/constants.rb +75 -0
  31. data/lib/cabriolet/decompressors/base.rb +39 -0
  32. data/lib/cabriolet/decompressors/lzss.rb +138 -0
  33. data/lib/cabriolet/decompressors/lzx.rb +726 -0
  34. data/lib/cabriolet/decompressors/mszip.rb +390 -0
  35. data/lib/cabriolet/decompressors/none.rb +27 -0
  36. data/lib/cabriolet/decompressors/quantum.rb +456 -0
  37. data/lib/cabriolet/errors.rb +39 -0
  38. data/lib/cabriolet/format_detector.rb +156 -0
  39. data/lib/cabriolet/hlp/compressor.rb +272 -0
  40. data/lib/cabriolet/hlp/decompressor.rb +198 -0
  41. data/lib/cabriolet/hlp/parser.rb +131 -0
  42. data/lib/cabriolet/huffman/decoder.rb +79 -0
  43. data/lib/cabriolet/huffman/encoder.rb +108 -0
  44. data/lib/cabriolet/huffman/tree.rb +138 -0
  45. data/lib/cabriolet/kwaj/compressor.rb +479 -0
  46. data/lib/cabriolet/kwaj/decompressor.rb +237 -0
  47. data/lib/cabriolet/kwaj/parser.rb +183 -0
  48. data/lib/cabriolet/lit/compressor.rb +255 -0
  49. data/lib/cabriolet/lit/decompressor.rb +250 -0
  50. data/lib/cabriolet/models/cabinet.rb +81 -0
  51. data/lib/cabriolet/models/chm_file.rb +28 -0
  52. data/lib/cabriolet/models/chm_header.rb +67 -0
  53. data/lib/cabriolet/models/chm_section.rb +38 -0
  54. data/lib/cabriolet/models/file.rb +119 -0
  55. data/lib/cabriolet/models/folder.rb +102 -0
  56. data/lib/cabriolet/models/folder_data.rb +21 -0
  57. data/lib/cabriolet/models/hlp_file.rb +45 -0
  58. data/lib/cabriolet/models/hlp_header.rb +37 -0
  59. data/lib/cabriolet/models/kwaj_header.rb +98 -0
  60. data/lib/cabriolet/models/lit_header.rb +55 -0
  61. data/lib/cabriolet/models/oab_header.rb +95 -0
  62. data/lib/cabriolet/models/szdd_header.rb +72 -0
  63. data/lib/cabriolet/modifier.rb +326 -0
  64. data/lib/cabriolet/oab/compressor.rb +353 -0
  65. data/lib/cabriolet/oab/decompressor.rb +315 -0
  66. data/lib/cabriolet/parallel.rb +333 -0
  67. data/lib/cabriolet/repairer.rb +288 -0
  68. data/lib/cabriolet/streaming.rb +221 -0
  69. data/lib/cabriolet/system/file_handle.rb +107 -0
  70. data/lib/cabriolet/system/io_system.rb +87 -0
  71. data/lib/cabriolet/system/memory_handle.rb +105 -0
  72. data/lib/cabriolet/szdd/compressor.rb +217 -0
  73. data/lib/cabriolet/szdd/decompressor.rb +184 -0
  74. data/lib/cabriolet/szdd/parser.rb +127 -0
  75. data/lib/cabriolet/validator.rb +332 -0
  76. data/lib/cabriolet/version.rb +5 -0
  77. data/lib/cabriolet.rb +104 -0
  78. 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