omnizip 0.3.4 → 0.3.5
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/lib/omnizip/formats/ole/allocation_table.rb +5 -0
- data/lib/omnizip/formats/ole/dirent.rb +37 -5
- data/lib/omnizip/formats/ole/ranges_io.rb +67 -2
- data/lib/omnizip/formats/ole/storage.rb +154 -3
- data/lib/omnizip/formats/ole/types/variant.rb +6 -3
- data/lib/omnizip/formats/rpm/writer.rb +546 -0
- data/lib/omnizip/formats/rpm.rb +24 -1
- data/lib/omnizip/version.rb +1 -1
- data/lib/omnizip.rb +10 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a395e650c4db2554fed636efcec40fec1b1f77e77d2c778fbfbe336fe5fbcd8
|
|
4
|
+
data.tar.gz: 7846eed6f91af6b9e21cb0d17418bbbbe868c2c76109e1456a8db2a8eb561094
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 32c4633a90fe9d134cea551c0dd2293d0e488d32650a136d1cc41d5c8b438fd762a181cdadaa212366fe10a0c6066b2fc27a162a9129dacbd5485906b130a6ce
|
|
7
|
+
data.tar.gz: dcc450866ff51cec1373009aee2dc1d4a7d37a061dd2dbcb3ceb9e50f34cdf8465b33a63261bf10f334d5dc6512940b7cb1d97392c0b33067d53f85a99b5679e
|
|
@@ -212,9 +212,15 @@ module Omnizip
|
|
|
212
212
|
@reserved = values[13]
|
|
213
213
|
|
|
214
214
|
# Decode name from UTF-16LE
|
|
215
|
-
|
|
215
|
+
# name_len includes the null terminator
|
|
216
|
+
name_data = @name_utf16[0...@name_len] if @name_len.positive?
|
|
216
217
|
@name = begin
|
|
217
|
-
|
|
218
|
+
# Decode and strip null terminator
|
|
219
|
+
decoded = name_data.dup.force_encoding(Encoding::UTF_16LE)
|
|
220
|
+
# Remove trailing null character (UTF-16 null = 2 bytes)
|
|
221
|
+
null_char = "\x00".encode(Encoding::UTF_16LE)
|
|
222
|
+
decoded = decoded.chomp(null_char)
|
|
223
|
+
decoded.encode(Encoding::UTF_8)
|
|
218
224
|
rescue StandardError
|
|
219
225
|
""
|
|
220
226
|
end
|
|
@@ -248,11 +254,14 @@ module Omnizip
|
|
|
248
254
|
#
|
|
249
255
|
# @return [String] 128-byte binary data
|
|
250
256
|
def pack
|
|
251
|
-
# Encode name to UTF-16LE
|
|
257
|
+
# Encode name to UTF-16LE with null terminator
|
|
252
258
|
name_data = Types::Variant.dump(Types::Variant::VT_LPWSTR, @name)
|
|
253
|
-
|
|
254
|
-
name_data
|
|
259
|
+
# Truncate to 62 bytes if needed (leaving room for null terminator)
|
|
260
|
+
name_data = name_data[0, 62] + "\x00\x00".b if name_data.length > 62
|
|
261
|
+
# Ensure null terminator exists
|
|
262
|
+
name_data += "\x00\x00".b unless name_data.end_with?("\x00\x00".b)
|
|
255
263
|
@name_len = name_data.length
|
|
264
|
+
# Pad to 64 bytes total
|
|
256
265
|
@name_utf16 = name_data + ("\x00".b * (64 - name_data.length))
|
|
257
266
|
|
|
258
267
|
# Set type_id from type
|
|
@@ -291,6 +300,29 @@ module Omnizip
|
|
|
291
300
|
bat.read(@first_block, @size)
|
|
292
301
|
end
|
|
293
302
|
|
|
303
|
+
# Open stream for reading or writing
|
|
304
|
+
#
|
|
305
|
+
# @param mode [String] Open mode ('r' for read, 'w' for write)
|
|
306
|
+
# @yield [RangesIOMigrateable] IO object
|
|
307
|
+
# @return [RangesIOMigrateable]
|
|
308
|
+
# @raise [Errno::EISDIR] If entry is a directory
|
|
309
|
+
def open(mode = "r")
|
|
310
|
+
raise Errno::EISDIR unless file?
|
|
311
|
+
|
|
312
|
+
io = RangesIOMigrateable.new(self, mode)
|
|
313
|
+
@modify_time = Time.now if io.respond_to?(:writeable?) && io.writeable?
|
|
314
|
+
|
|
315
|
+
if block_given?
|
|
316
|
+
begin
|
|
317
|
+
yield io
|
|
318
|
+
ensure
|
|
319
|
+
io.close
|
|
320
|
+
end
|
|
321
|
+
else
|
|
322
|
+
io
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
294
326
|
# Lookup child by name
|
|
295
327
|
#
|
|
296
328
|
# @param name [String] Child name
|
|
@@ -51,8 +51,12 @@ module Omnizip
|
|
|
51
51
|
#
|
|
52
52
|
# @param ranges [Array<Range, Array>] Byte ranges
|
|
53
53
|
def ranges=(ranges)
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
ranges ||= []
|
|
55
|
+
|
|
56
|
+
# Convert Range objects to arrays, filtering out nils
|
|
57
|
+
@ranges = ranges.filter_map do |r|
|
|
58
|
+
next nil if r.nil?
|
|
59
|
+
|
|
56
60
|
r.is_a?(Range) ? [r.begin, r.end - r.begin] : r
|
|
57
61
|
end
|
|
58
62
|
|
|
@@ -259,6 +263,67 @@ module Omnizip
|
|
|
259
263
|
@io.truncate(max_pos) if max_pos > @io.size
|
|
260
264
|
end
|
|
261
265
|
end
|
|
266
|
+
|
|
267
|
+
# RangesIO that can migrate between BAT and SBAT based on size
|
|
268
|
+
class RangesIOMigrateable < RangesIOResizeable
|
|
269
|
+
# @return [Dirent] Associated dirent
|
|
270
|
+
attr_reader :dirent
|
|
271
|
+
|
|
272
|
+
# Initialize migrateable RangesIO
|
|
273
|
+
#
|
|
274
|
+
# @param dirent [Dirent] Associated dirent
|
|
275
|
+
# @param mode [String] Open mode
|
|
276
|
+
def initialize(dirent, mode = "r")
|
|
277
|
+
@dirent = dirent
|
|
278
|
+
bat = dirent.ole.bat_for_size(dirent.size)
|
|
279
|
+
super(bat, first_block: dirent.first_block, size: dirent.size)
|
|
280
|
+
@mode = mode
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Check if writable
|
|
284
|
+
def writeable?
|
|
285
|
+
@mode.include?("w") || @mode.include?("a") || @mode.include?("+")
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Truncate with BAT migration support
|
|
289
|
+
#
|
|
290
|
+
# @param new_size [Integer] New size in bytes
|
|
291
|
+
def truncate(new_size)
|
|
292
|
+
new_bat = @dirent.ole.bat_for_size(new_size)
|
|
293
|
+
|
|
294
|
+
if new_bat.instance_of?(@bat.class)
|
|
295
|
+
super
|
|
296
|
+
else
|
|
297
|
+
# BAT migration needed
|
|
298
|
+
pos = [@pos, new_size].min
|
|
299
|
+
self.pos = 0
|
|
300
|
+
keep = read([@size, new_size].min)
|
|
301
|
+
super(0)
|
|
302
|
+
|
|
303
|
+
@bat = new_bat
|
|
304
|
+
@io = new_bat.io
|
|
305
|
+
super
|
|
306
|
+
|
|
307
|
+
self.pos = 0
|
|
308
|
+
write(keep)
|
|
309
|
+
self.pos = pos
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Update dirent's first_block and size after any resize
|
|
313
|
+
@dirent.first_block = @first_block
|
|
314
|
+
@dirent.size = new_size
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Forward first_block to dirent (for reading)
|
|
318
|
+
def first_block
|
|
319
|
+
@first_block
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def first_block=(val)
|
|
323
|
+
@first_block = val
|
|
324
|
+
@dirent.first_block = val
|
|
325
|
+
end
|
|
326
|
+
end
|
|
262
327
|
end
|
|
263
328
|
end
|
|
264
329
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "set"
|
|
3
4
|
require_relative "constants"
|
|
4
5
|
require_relative "header"
|
|
5
6
|
require_relative "allocation_table"
|
|
@@ -142,28 +143,178 @@ module Omnizip
|
|
|
142
143
|
#
|
|
143
144
|
# @param dirents [Array<Dirent>]
|
|
144
145
|
# @param idx [Integer]
|
|
146
|
+
# @param visited [Set] Set of visited indices to detect cycles
|
|
145
147
|
# @return [Array<Dirent>]
|
|
146
|
-
def build_tree(dirents, idx = 0)
|
|
148
|
+
def build_tree(dirents, idx = 0, visited = nil)
|
|
147
149
|
return [] if idx == EOT
|
|
148
150
|
|
|
151
|
+
# Initialize visited set on first call
|
|
152
|
+
visited ||= Set.new
|
|
153
|
+
|
|
154
|
+
# Check for circular references
|
|
155
|
+
return [] if visited.include?(idx)
|
|
156
|
+
|
|
157
|
+
visited << idx
|
|
158
|
+
|
|
149
159
|
dirent = dirents[idx]
|
|
160
|
+
return [] unless dirent
|
|
150
161
|
|
|
151
162
|
# Build children recursively
|
|
152
|
-
build_tree(dirents, dirent.child).each { |child| dirent << child }
|
|
163
|
+
build_tree(dirents, dirent.child, visited).each { |child| dirent << child }
|
|
153
164
|
|
|
154
165
|
# Set index
|
|
155
166
|
dirent.idx = idx
|
|
156
167
|
|
|
157
168
|
# Return list for tree building
|
|
158
|
-
build_tree(dirents, dirent.prev) + [dirent] + build_tree(dirents, dirent.next)
|
|
169
|
+
build_tree(dirents, dirent.prev, visited) + [dirent] + build_tree(dirents, dirent.next, visited)
|
|
159
170
|
end
|
|
160
171
|
|
|
161
172
|
# Close storage
|
|
162
173
|
def close
|
|
163
174
|
@sb_file&.close
|
|
175
|
+
flush if @writeable
|
|
164
176
|
@io.close if @close_parent
|
|
165
177
|
end
|
|
166
178
|
|
|
179
|
+
# Flush all changes to disk
|
|
180
|
+
#
|
|
181
|
+
# Writes all metadata (dirents, allocation tables, header) to the file.
|
|
182
|
+
# This is the main "save" method for OLE documents.
|
|
183
|
+
def flush
|
|
184
|
+
return unless @writeable
|
|
185
|
+
|
|
186
|
+
# Update root dirent
|
|
187
|
+
@root.name = "Root Entry"
|
|
188
|
+
@root.first_block = @sb_file.first_block
|
|
189
|
+
@root.size = @sb_file.size
|
|
190
|
+
|
|
191
|
+
# Flatten dirent tree
|
|
192
|
+
@dirents = @root.flatten
|
|
193
|
+
|
|
194
|
+
# Serialize dirents using bbat
|
|
195
|
+
dirent_io = RangesIOResizeable.new(@bbat, first_block: @header.dirent_start)
|
|
196
|
+
dirent_io.write(@dirents.map(&:pack).join)
|
|
197
|
+
# Pad to block boundary
|
|
198
|
+
padding = ((dirent_io.size / @bbat.block_size.to_f).ceil * @bbat.block_size) - dirent_io.size
|
|
199
|
+
dirent_io.write("\x00".b * padding) if padding.positive?
|
|
200
|
+
@header.dirent_start = dirent_io.first_block
|
|
201
|
+
dirent_io.close
|
|
202
|
+
|
|
203
|
+
# Serialize sbat
|
|
204
|
+
sbat_io = RangesIOResizeable.new(@bbat, first_block: @header.sbat_start)
|
|
205
|
+
sbat_io.write(@sbat.pack)
|
|
206
|
+
@header.sbat_start = sbat_io.first_block
|
|
207
|
+
@header.num_sbat = @bbat.chain(@header.sbat_start).length
|
|
208
|
+
sbat_io.close
|
|
209
|
+
|
|
210
|
+
# Clear BAT/META_BAT markers
|
|
211
|
+
@bbat.entries.each_with_index do |val, idx|
|
|
212
|
+
if [BAT, META_BAT].include?(val)
|
|
213
|
+
@bbat.entries[idx] = AVAIL
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Calculate and allocate BAT blocks
|
|
218
|
+
write_bat_blocks
|
|
219
|
+
|
|
220
|
+
# Write header
|
|
221
|
+
@io.seek(0)
|
|
222
|
+
@io.write(@header.pack)
|
|
223
|
+
@io.write(@bbat_chain.pack("V*"))
|
|
224
|
+
@io.flush
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
# Write BAT (Block Allocation Table) blocks
|
|
230
|
+
def write_bat_blocks
|
|
231
|
+
# Truncate bbat to remove trailing AVAILs
|
|
232
|
+
@bbat.entries.replace(@bbat.entries.reject { |e| e == AVAIL }.push(AVAIL))
|
|
233
|
+
|
|
234
|
+
# Calculate space needed for BAT
|
|
235
|
+
num_mbat_blocks = 0
|
|
236
|
+
io = RangesIOResizeable.new(@bbat, first_block: EOC)
|
|
237
|
+
|
|
238
|
+
@bbat.truncate_entries
|
|
239
|
+
@io.truncate(@bbat.block_size * (@bbat.length + 1))
|
|
240
|
+
|
|
241
|
+
# Iteratively calculate BAT/MBAT space
|
|
242
|
+
loop do
|
|
243
|
+
bbat_data_len = ((@bbat.length + num_mbat_blocks) * 4 / @bbat.block_size.to_f).ceil * @bbat.block_size
|
|
244
|
+
new_num_mbat_blocks = calculate_mbat_blocks(bbat_data_len)
|
|
245
|
+
|
|
246
|
+
if new_num_mbat_blocks != num_mbat_blocks
|
|
247
|
+
num_mbat_blocks = new_num_mbat_blocks
|
|
248
|
+
elsif io.size != bbat_data_len
|
|
249
|
+
io.truncate(bbat_data_len)
|
|
250
|
+
else
|
|
251
|
+
break
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Get BAT chain and mark blocks
|
|
256
|
+
@bbat_chain = @bbat.chain(io.first_block)
|
|
257
|
+
io.close
|
|
258
|
+
|
|
259
|
+
@bbat_chain.each { |b| @bbat.entries[b] = BAT }
|
|
260
|
+
@header.num_bat = @bbat_chain.length
|
|
261
|
+
|
|
262
|
+
# Allocate MBAT blocks if needed
|
|
263
|
+
mbat_blocks = allocate_mbat_blocks(num_mbat_blocks)
|
|
264
|
+
@header.mbat_start = mbat_blocks.first || EOC
|
|
265
|
+
@header.num_mbat = num_mbat_blocks
|
|
266
|
+
|
|
267
|
+
# Write BAT data
|
|
268
|
+
RangesIO.open(@io, @bbat.ranges(@bbat_chain)) do |f|
|
|
269
|
+
f.write(@bbat.pack)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Write MBAT if present
|
|
273
|
+
write_mbat_blocks(mbat_blocks, num_mbat_blocks) if num_mbat_blocks.positive?
|
|
274
|
+
|
|
275
|
+
# Pad BAT chain to 109 entries in header
|
|
276
|
+
@bbat_chain += [AVAIL] * [109 - @bbat_chain.length, 0].max
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Calculate number of MBAT blocks needed
|
|
280
|
+
def calculate_mbat_blocks(bbat_data_len)
|
|
281
|
+
excess_bat_blocks = (bbat_data_len / @bbat.block_size) - 109
|
|
282
|
+
return 0 if excess_bat_blocks <= 0
|
|
283
|
+
|
|
284
|
+
(excess_bat_blocks * 4 / (@bbat.block_size - 4).to_f).ceil
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Allocate MBAT blocks
|
|
288
|
+
def allocate_mbat_blocks(count)
|
|
289
|
+
(0...count).map do
|
|
290
|
+
block = @bbat.free_block
|
|
291
|
+
@bbat.entries[block] = META_BAT
|
|
292
|
+
block
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Write MBAT blocks
|
|
297
|
+
def write_mbat_blocks(mbat_blocks, _num_mbat)
|
|
298
|
+
# Get BAT entries beyond the first 109
|
|
299
|
+
mbat_data = @bbat_chain[109..] || []
|
|
300
|
+
|
|
301
|
+
# Add linked list pointers
|
|
302
|
+
entries_per_block = (@bbat.block_size / 4) - 1
|
|
303
|
+
mbat_data = mbat_data.each_slice(entries_per_block).to_a
|
|
304
|
+
|
|
305
|
+
mbat_data.zip(mbat_blocks[1..] + [nil]).each_with_index do |(entries, next_block), idx|
|
|
306
|
+
block_data = entries + (next_block ? [next_block] : [])
|
|
307
|
+
# Pad to block size
|
|
308
|
+
block_data += [AVAIL] * ((@bbat.block_size / 4) - block_data.length)
|
|
309
|
+
|
|
310
|
+
RangesIO.open(@io, @bbat.ranges([mbat_blocks[idx]])) do |f|
|
|
311
|
+
f.write(block_data.pack("V*"))
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
public
|
|
317
|
+
|
|
167
318
|
# Get appropriate BAT for size
|
|
168
319
|
#
|
|
169
320
|
# @param size [Integer] File size
|
|
@@ -42,9 +42,12 @@ module Omnizip
|
|
|
42
42
|
def self.load(str)
|
|
43
43
|
return new("") if str.nil? || str.empty?
|
|
44
44
|
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
# Force binary data to UTF-16LE encoding, then transcode to UTF-8
|
|
46
|
+
# Strip null terminator (UTF-16 null = 2 bytes)
|
|
47
|
+
decoded = str.dup.force_encoding(Encoding::UTF_16LE)
|
|
48
|
+
decoded = decoded.chomp("\x00".encode(Encoding::UTF_16LE))
|
|
49
|
+
decoded = decoded.encode(Encoding::UTF_8)
|
|
50
|
+
new(decoded)
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
# Dump to UTF-16LE binary data
|
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
require "zlib"
|
|
5
|
+
require "stringio"
|
|
6
|
+
require "digest"
|
|
7
|
+
require_relative "constants"
|
|
8
|
+
require_relative "lead"
|
|
9
|
+
require_relative "header"
|
|
10
|
+
require_relative "tag"
|
|
11
|
+
require_relative "../cpio/writer"
|
|
12
|
+
|
|
13
|
+
module Omnizip
|
|
14
|
+
module Formats
|
|
15
|
+
module Rpm
|
|
16
|
+
# RPM package writer
|
|
17
|
+
#
|
|
18
|
+
# Creates RPM packages with binary payload (CPIO archive).
|
|
19
|
+
# Supports gzip compression for the payload.
|
|
20
|
+
#
|
|
21
|
+
# @example Create an RPM package
|
|
22
|
+
# writer = Rpm::Writer.new(
|
|
23
|
+
# name: "myapp",
|
|
24
|
+
# version: "1.0.0",
|
|
25
|
+
# release: "1",
|
|
26
|
+
# arch: "x86_64"
|
|
27
|
+
# )
|
|
28
|
+
# writer.add_file("/usr/bin/myapp", content, mode: 0755)
|
|
29
|
+
# writer.add_directory("/etc/myapp")
|
|
30
|
+
# writer.add_file("/etc/myapp/config.yml", config_content)
|
|
31
|
+
# writer.write("myapp-1.0.0-1.x86_64.rpm")
|
|
32
|
+
#
|
|
33
|
+
class Writer
|
|
34
|
+
include Constants
|
|
35
|
+
|
|
36
|
+
# Architecture mapping
|
|
37
|
+
ARCHITECTURES = {
|
|
38
|
+
"noarch" => 0,
|
|
39
|
+
"i386" => 1,
|
|
40
|
+
"i486" => 2,
|
|
41
|
+
"i586" => 3,
|
|
42
|
+
"i686" => 4,
|
|
43
|
+
"x86_64" => 9,
|
|
44
|
+
"amd64" => 9,
|
|
45
|
+
"ia64" => 11,
|
|
46
|
+
"ppc" => 5,
|
|
47
|
+
"ppc64" => 16,
|
|
48
|
+
"sparc" => 6,
|
|
49
|
+
"sparc64" => 7,
|
|
50
|
+
"alpha" => 8,
|
|
51
|
+
"s390" => 14,
|
|
52
|
+
"s390x" => 15,
|
|
53
|
+
"arm" => 12,
|
|
54
|
+
"aarch64" => 19,
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
# Tag ID mapping (reverse of Tag::TAG_IDS)
|
|
58
|
+
TAG_NAMES = {
|
|
59
|
+
# Header tags
|
|
60
|
+
name: 1000,
|
|
61
|
+
version: 1001,
|
|
62
|
+
release: 1002,
|
|
63
|
+
epoch: 1003,
|
|
64
|
+
summary: 1004,
|
|
65
|
+
description: 1005,
|
|
66
|
+
buildtime: 1006,
|
|
67
|
+
buildhost: 1007,
|
|
68
|
+
size: 1009,
|
|
69
|
+
distribution: 1010,
|
|
70
|
+
vendor: 1011,
|
|
71
|
+
license: 1014,
|
|
72
|
+
packager: 1015,
|
|
73
|
+
group: 1016,
|
|
74
|
+
url: 1020,
|
|
75
|
+
os: 1021,
|
|
76
|
+
arch: 1022,
|
|
77
|
+
prein: 1023,
|
|
78
|
+
postin: 1024,
|
|
79
|
+
preun: 1025,
|
|
80
|
+
postun: 1026,
|
|
81
|
+
filesizes: 1028,
|
|
82
|
+
filemodes: 1030,
|
|
83
|
+
fileuids: 1031,
|
|
84
|
+
filegids: 1032,
|
|
85
|
+
filemtimes: 1034,
|
|
86
|
+
filedigests: 1035,
|
|
87
|
+
filelinktos: 1036,
|
|
88
|
+
fileflags: 1037,
|
|
89
|
+
fileusername: 1039,
|
|
90
|
+
filegroupname: 1040,
|
|
91
|
+
archivesize: 1046,
|
|
92
|
+
rpmversion: 1064,
|
|
93
|
+
dirindexes: 1116,
|
|
94
|
+
basenames: 1117,
|
|
95
|
+
dirnames: 1118,
|
|
96
|
+
payloadformat: 1124,
|
|
97
|
+
payloadcompressor: 1125,
|
|
98
|
+
payloadflags: 1126,
|
|
99
|
+
|
|
100
|
+
# Signature tags
|
|
101
|
+
sigsize: 257,
|
|
102
|
+
sha1header: 269,
|
|
103
|
+
}.freeze
|
|
104
|
+
|
|
105
|
+
# @return [String] Package name
|
|
106
|
+
attr_reader :name
|
|
107
|
+
|
|
108
|
+
# @return [String] Package version
|
|
109
|
+
attr_reader :version
|
|
110
|
+
|
|
111
|
+
# @return [String] Package release
|
|
112
|
+
attr_reader :release
|
|
113
|
+
|
|
114
|
+
# @return [String, nil] Package epoch
|
|
115
|
+
attr_reader :epoch
|
|
116
|
+
|
|
117
|
+
# @return [String] Architecture
|
|
118
|
+
attr_reader :arch
|
|
119
|
+
|
|
120
|
+
# @return [Hash<String, String>] File contents
|
|
121
|
+
attr_reader :files
|
|
122
|
+
|
|
123
|
+
# @return [Hash] Additional metadata
|
|
124
|
+
attr_reader :metadata
|
|
125
|
+
|
|
126
|
+
# Initialize RPM writer
|
|
127
|
+
#
|
|
128
|
+
# @param name [String] Package name
|
|
129
|
+
# @param version [String] Package version
|
|
130
|
+
# @param release [String] Package release
|
|
131
|
+
# @param arch [String] Architecture (default: "noarch")
|
|
132
|
+
# @param epoch [String, nil] Optional epoch
|
|
133
|
+
# @param metadata [Hash] Additional metadata
|
|
134
|
+
def initialize(name:, version:, release:, arch: "noarch", epoch: nil, **metadata)
|
|
135
|
+
@name = name
|
|
136
|
+
@version = version
|
|
137
|
+
@release = release
|
|
138
|
+
@arch = arch
|
|
139
|
+
@epoch = epoch
|
|
140
|
+
@metadata = metadata
|
|
141
|
+
@files = []
|
|
142
|
+
@directories = []
|
|
143
|
+
@compression = :gzip
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Add file to package
|
|
147
|
+
#
|
|
148
|
+
# @param path [String] File path in package (absolute path)
|
|
149
|
+
# @param content [String] File content
|
|
150
|
+
# @param mode [Integer] File permissions (default: 0644)
|
|
151
|
+
# @param owner [String] File owner (default: "root")
|
|
152
|
+
# @param group [String] File group (default: "root")
|
|
153
|
+
# @param mtime [Integer] Modification time (default: now)
|
|
154
|
+
def add_file(path, content, mode: 0o644, owner: "root", group: "root", mtime: nil)
|
|
155
|
+
@files << {
|
|
156
|
+
path: path,
|
|
157
|
+
content: content,
|
|
158
|
+
mode: mode,
|
|
159
|
+
owner: owner,
|
|
160
|
+
group: group,
|
|
161
|
+
mtime: mtime || Time.now.to_i,
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Add directory to package
|
|
166
|
+
#
|
|
167
|
+
# @param path [String] Directory path in package (absolute path)
|
|
168
|
+
# @param mode [Integer] Directory permissions (default: 0755)
|
|
169
|
+
# @param owner [String] Directory owner (default: "root")
|
|
170
|
+
# @param group [String] Directory group (default: "root")
|
|
171
|
+
def add_directory(path, mode: 0o755, owner: "root", group: "root")
|
|
172
|
+
@directories << {
|
|
173
|
+
path: path,
|
|
174
|
+
mode: mode,
|
|
175
|
+
owner: owner,
|
|
176
|
+
group: group,
|
|
177
|
+
}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Write RPM package to file
|
|
181
|
+
#
|
|
182
|
+
# @param output_path [String] Output file path
|
|
183
|
+
# @return [String] Output path
|
|
184
|
+
def write(output_path)
|
|
185
|
+
File.open(output_path, "wb") do |io|
|
|
186
|
+
write_to_io(io)
|
|
187
|
+
end
|
|
188
|
+
output_path
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Write RPM package to IO
|
|
192
|
+
#
|
|
193
|
+
# @param io [IO] Output IO
|
|
194
|
+
def write_to_io(io)
|
|
195
|
+
# Build payload (compressed CPIO)
|
|
196
|
+
payload_io = StringIO.new("".b)
|
|
197
|
+
cpio_data = build_cpio_payload
|
|
198
|
+
compress_payload(cpio_data, payload_io)
|
|
199
|
+
payload_io.rewind
|
|
200
|
+
payload_data = payload_io.read
|
|
201
|
+
|
|
202
|
+
# Build headers
|
|
203
|
+
main_header = build_main_header(payload_data.bytesize)
|
|
204
|
+
sig_header = build_signature_header(main_header.bytesize, payload_data.bytesize)
|
|
205
|
+
|
|
206
|
+
# Build lead
|
|
207
|
+
lead = build_lead
|
|
208
|
+
|
|
209
|
+
# Write RPM in order
|
|
210
|
+
io.write(lead)
|
|
211
|
+
io.write(sig_header)
|
|
212
|
+
io.write(main_header)
|
|
213
|
+
io.write(payload_data)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
# Build lead (96 bytes)
|
|
219
|
+
#
|
|
220
|
+
# @return [String] Packed lead
|
|
221
|
+
def build_lead
|
|
222
|
+
name_field = "#{@name}-#{@version}-#{@release}"
|
|
223
|
+
name_field = name_field[0, 65] # Truncate to 65 chars + null
|
|
224
|
+
name_padded = name_field.ljust(66, "\0")
|
|
225
|
+
|
|
226
|
+
arch_num = ARCHITECTURES.fetch(@arch.downcase, 0)
|
|
227
|
+
|
|
228
|
+
# Pack format:
|
|
229
|
+
# A4 = magic (4 bytes)
|
|
230
|
+
# CC = major/minor version (2 bytes)
|
|
231
|
+
# n = package type (2 bytes, 0 = binary)
|
|
232
|
+
# n = architecture (2 bytes)
|
|
233
|
+
# A66 = name (66 bytes)
|
|
234
|
+
# n = os (2 bytes, 1 = Linux)
|
|
235
|
+
# n = signature type (2 bytes, 5 = signed)
|
|
236
|
+
# A16 = reserved (16 bytes)
|
|
237
|
+
[LEAD_MAGIC, 3, 0, PACKAGE_BINARY, arch_num, name_padded, 1,
|
|
238
|
+
HEADER_SIGNED_TYPE, "\0" * 16].pack("A4 CC n n A66 n n A16")
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Build signature header
|
|
242
|
+
#
|
|
243
|
+
# @param header_size [Integer] Main header size
|
|
244
|
+
# @param payload_size [Integer] Payload size
|
|
245
|
+
# @return [String] Packed signature header
|
|
246
|
+
def build_signature_header(header_size, payload_size)
|
|
247
|
+
tags = [
|
|
248
|
+
{ id: TAG_NAMES[:sigsize], type: TYPE_INT32, value: [header_size + payload_size] },
|
|
249
|
+
{ id: TAG_NAMES[:sha1header], type: TYPE_STRING, value: "" },
|
|
250
|
+
]
|
|
251
|
+
build_header_data(tags)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Build main header
|
|
255
|
+
#
|
|
256
|
+
# @param payload_size [Integer] Payload size (uncompressed)
|
|
257
|
+
# @return [String] Packed main header
|
|
258
|
+
def build_main_header(payload_size)
|
|
259
|
+
# Build file lists
|
|
260
|
+
dirnames = []
|
|
261
|
+
basenames = []
|
|
262
|
+
dirindexes = []
|
|
263
|
+
filemodes = []
|
|
264
|
+
filesizes = []
|
|
265
|
+
fileowners = []
|
|
266
|
+
filegroups = []
|
|
267
|
+
filemtimes = []
|
|
268
|
+
filedigests = []
|
|
269
|
+
filelinktos = []
|
|
270
|
+
fileflags = []
|
|
271
|
+
|
|
272
|
+
# Process directories first
|
|
273
|
+
dir_set = Set.new
|
|
274
|
+
@directories.each do |dir|
|
|
275
|
+
dir_path = dir[:path]
|
|
276
|
+
parent = File.dirname(dir_path.sub(%r{/+$}, ""))
|
|
277
|
+
dir_name = File.basename(dir_path.sub(%r{/+$}, ""))
|
|
278
|
+
|
|
279
|
+
dir_set.size
|
|
280
|
+
dir_set << dir_path
|
|
281
|
+
|
|
282
|
+
# Parent directory for relative path
|
|
283
|
+
parent_dir = parent == "." ? "/" : parent
|
|
284
|
+
dirnames.size
|
|
285
|
+
dirnames << parent_dir unless dirnames.include?(parent_dir)
|
|
286
|
+
parent_index = dirnames.index(parent_dir)
|
|
287
|
+
|
|
288
|
+
basenames << dir_name
|
|
289
|
+
dirindexes << parent_index
|
|
290
|
+
filemodes << (dir[:mode] | 0o040000) # Directory flag
|
|
291
|
+
filesizes << 4096 # Typical directory size
|
|
292
|
+
fileowners << dir[:owner]
|
|
293
|
+
filegroups << dir[:group]
|
|
294
|
+
filemtimes << Time.now.to_i
|
|
295
|
+
filedigests << ""
|
|
296
|
+
filelinktos << ""
|
|
297
|
+
fileflags << 0
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Process files
|
|
301
|
+
@files.each do |file|
|
|
302
|
+
file_path = file[:path]
|
|
303
|
+
parent_dir = File.dirname(file_path)
|
|
304
|
+
base_name = File.basename(file_path)
|
|
305
|
+
|
|
306
|
+
# Ensure directory is in list
|
|
307
|
+
dir_index = dirnames.index(parent_dir)
|
|
308
|
+
dir_index ||= dirnames.size.tap { dirnames << parent_dir }
|
|
309
|
+
|
|
310
|
+
basenames << base_name
|
|
311
|
+
dirindexes << dir_index
|
|
312
|
+
filemodes << file[:mode]
|
|
313
|
+
filesizes << file[:content].bytesize
|
|
314
|
+
fileowners << file[:owner]
|
|
315
|
+
filegroups << file[:group]
|
|
316
|
+
filemtimes << file[:mtime]
|
|
317
|
+
filedigests << Digest::MD5.hexdigest(file[:content])
|
|
318
|
+
filelinktos << ""
|
|
319
|
+
fileflags << 0
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
tags = [
|
|
323
|
+
{ id: TAG_NAMES[:name], type: TYPE_STRING, value: @name },
|
|
324
|
+
{ id: TAG_NAMES[:version], type: TYPE_STRING, value: @version },
|
|
325
|
+
{ id: TAG_NAMES[:release], type: TYPE_STRING, value: @release },
|
|
326
|
+
{ id: TAG_NAMES[:arch], type: TYPE_STRING, value: @arch },
|
|
327
|
+
{ id: TAG_NAMES[:os], type: TYPE_STRING, value: "linux" },
|
|
328
|
+
{ id: TAG_NAMES[:rpmversion], type: TYPE_STRING, value: "4.16.0" },
|
|
329
|
+
{ id: TAG_NAMES[:payloadformat], type: TYPE_STRING, value: "cpio" },
|
|
330
|
+
{ id: TAG_NAMES[:payloadcompressor], type: TYPE_STRING, value: "gzip" },
|
|
331
|
+
{ id: TAG_NAMES[:payloadflags], type: TYPE_STRING, value: "9" },
|
|
332
|
+
{ id: TAG_NAMES[:archivesize], type: TYPE_INT32, value: [payload_size] },
|
|
333
|
+
{ id: TAG_NAMES[:dirnames], type: TYPE_STRING_ARRAY, value: dirnames },
|
|
334
|
+
{ id: TAG_NAMES[:basenames], type: TYPE_STRING_ARRAY, value: basenames },
|
|
335
|
+
{ id: TAG_NAMES[:dirindexes], type: TYPE_INT32, value: dirindexes },
|
|
336
|
+
{ id: TAG_NAMES[:filemodes], type: TYPE_INT16, value: filemodes },
|
|
337
|
+
{ id: TAG_NAMES[:filesizes], type: TYPE_INT32, value: filesizes },
|
|
338
|
+
{ id: TAG_NAMES[:fileusername], type: TYPE_STRING_ARRAY, value: fileowners },
|
|
339
|
+
{ id: TAG_NAMES[:filegroupname], type: TYPE_STRING_ARRAY, value: filegroups },
|
|
340
|
+
{ id: TAG_NAMES[:filemtimes], type: TYPE_INT32, value: filemtimes },
|
|
341
|
+
{ id: TAG_NAMES[:filedigests], type: TYPE_STRING_ARRAY, value: filedigests },
|
|
342
|
+
{ id: TAG_NAMES[:filelinktos], type: TYPE_STRING_ARRAY, value: filelinktos },
|
|
343
|
+
{ id: TAG_NAMES[:fileflags], type: TYPE_INT32, value: fileflags },
|
|
344
|
+
]
|
|
345
|
+
|
|
346
|
+
# Add optional metadata
|
|
347
|
+
{
|
|
348
|
+
summary: TYPE_STRING,
|
|
349
|
+
description: TYPE_STRING,
|
|
350
|
+
license: TYPE_STRING,
|
|
351
|
+
group: TYPE_STRING,
|
|
352
|
+
url: TYPE_STRING,
|
|
353
|
+
vendor: TYPE_STRING,
|
|
354
|
+
packager: TYPE_STRING,
|
|
355
|
+
}.each do |key, type|
|
|
356
|
+
value = @metadata[key]
|
|
357
|
+
tags << { id: TAG_NAMES[key], type: type, value: value } if value
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
tags << { id: TAG_NAMES[:epoch], type: TYPE_INT32, value: [@epoch.to_i] } if @epoch
|
|
361
|
+
|
|
362
|
+
build_header_data(tags)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Build header data structure
|
|
366
|
+
#
|
|
367
|
+
# @param tags [Array<Hash>] Array of tag definitions
|
|
368
|
+
# @return [String] Packed header
|
|
369
|
+
def build_header_data(tags)
|
|
370
|
+
# Build data blob and tag entries
|
|
371
|
+
data_blob = "".b
|
|
372
|
+
tag_entries = []
|
|
373
|
+
|
|
374
|
+
tags.each do |tag_def|
|
|
375
|
+
offset = data_blob.bytesize
|
|
376
|
+
value = tag_def[:value]
|
|
377
|
+
type = tag_def[:type]
|
|
378
|
+
|
|
379
|
+
packed_data = pack_tag_value(type, value)
|
|
380
|
+
data_blob << packed_data
|
|
381
|
+
|
|
382
|
+
count = case type
|
|
383
|
+
when TYPE_STRING_ARRAY
|
|
384
|
+
value.is_a?(Array) ? value.size : 1
|
|
385
|
+
when TYPE_STRING
|
|
386
|
+
1
|
|
387
|
+
else
|
|
388
|
+
value.is_a?(Array) ? value.size : 1
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
tag_entries << [tag_def[:id], type, offset, count].pack("NNNN")
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Build complete header
|
|
395
|
+
entry_count = tags.size
|
|
396
|
+
data_length = data_blob.bytesize
|
|
397
|
+
|
|
398
|
+
# Pad data blob to 8-byte boundary
|
|
399
|
+
padding = (8 - (data_length % 8)) % 8
|
|
400
|
+
data_blob << ("\0" * padding)
|
|
401
|
+
|
|
402
|
+
# Header header: magic (8) + entry_count (4) + data_length (4)
|
|
403
|
+
header_header = [HEADER_MAGIC, entry_count, data_blob.bytesize].pack("A8 NN")
|
|
404
|
+
|
|
405
|
+
header_header + tag_entries.join + data_blob
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Pack tag value based on type
|
|
409
|
+
#
|
|
410
|
+
# @param type [Integer] Tag type
|
|
411
|
+
# @param value [Object] Value to pack
|
|
412
|
+
# @return [String] Packed data
|
|
413
|
+
def pack_tag_value(type, value)
|
|
414
|
+
case type
|
|
415
|
+
when TYPE_STRING
|
|
416
|
+
"#{value}\0".b
|
|
417
|
+
when TYPE_STRING_ARRAY
|
|
418
|
+
value.map { |v| "#{v}\0" }.join.b
|
|
419
|
+
when TYPE_INT8
|
|
420
|
+
[value].flatten.pack("C*")
|
|
421
|
+
when TYPE_INT16
|
|
422
|
+
[value].flatten.pack("n*")
|
|
423
|
+
when TYPE_INT32
|
|
424
|
+
[value].flatten.pack("N*")
|
|
425
|
+
when TYPE_INT64
|
|
426
|
+
[value].flatten.pack("Q>")
|
|
427
|
+
when TYPE_BINARY
|
|
428
|
+
value.b
|
|
429
|
+
else
|
|
430
|
+
value.to_s.b
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Build CPIO payload
|
|
435
|
+
#
|
|
436
|
+
# @return [String] CPIO archive data
|
|
437
|
+
def build_cpio_payload
|
|
438
|
+
cpio_io = StringIO.new("".b)
|
|
439
|
+
|
|
440
|
+
# Write directories
|
|
441
|
+
@directories.each do |dir|
|
|
442
|
+
entry_data = build_cpio_directory_entry(dir)
|
|
443
|
+
cpio_io.write(entry_data)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Write files
|
|
447
|
+
@files.each do |file|
|
|
448
|
+
entry_data = build_cpio_file_entry(file)
|
|
449
|
+
cpio_io.write(entry_data)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# Write trailer
|
|
453
|
+
cpio_io.write(build_cpio_trailer)
|
|
454
|
+
|
|
455
|
+
cpio_io.string
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Build CPIO directory entry
|
|
459
|
+
#
|
|
460
|
+
# @param dir [Hash] Directory info
|
|
461
|
+
# @return [String] Packed CPIO entry
|
|
462
|
+
def build_cpio_directory_entry(dir)
|
|
463
|
+
mode = dir[:mode] | 0o040000 # Directory flag
|
|
464
|
+
path = dir[:path].sub(%r{/+$}, "") # Remove trailing slashes
|
|
465
|
+
|
|
466
|
+
build_cpio_entry(path, "", mode)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Build CPIO file entry
|
|
470
|
+
#
|
|
471
|
+
# @param file [Hash] File info
|
|
472
|
+
# @return [String] Packed CPIO entry
|
|
473
|
+
def build_cpio_file_entry(file)
|
|
474
|
+
build_cpio_entry(file[:path], file[:content], file[:mode])
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Build CPIO entry (newc format)
|
|
478
|
+
#
|
|
479
|
+
# @param path [String] Entry path
|
|
480
|
+
# @param data [String] Entry data
|
|
481
|
+
# @param mode [Integer] File mode
|
|
482
|
+
# @return [String] Packed CPIO entry
|
|
483
|
+
def build_cpio_entry(path, data, mode)
|
|
484
|
+
name = path.start_with?("/") ? path[1..] : path
|
|
485
|
+
namesize = name.bytesize + 1
|
|
486
|
+
filesize = data.bytesize
|
|
487
|
+
|
|
488
|
+
inode = @inode_counter ||= 1
|
|
489
|
+
@inode_counter += 1
|
|
490
|
+
|
|
491
|
+
# Build header (110 bytes for newc)
|
|
492
|
+
header = format(
|
|
493
|
+
"070701%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%s\x00",
|
|
494
|
+
inode, # inode
|
|
495
|
+
mode, # mode
|
|
496
|
+
0, # uid
|
|
497
|
+
0, # gid
|
|
498
|
+
1, # nlink
|
|
499
|
+
Time.now.to_i, # mtime
|
|
500
|
+
filesize, # filesize
|
|
501
|
+
0, # devmajor
|
|
502
|
+
0, # devminor
|
|
503
|
+
0, # rdevmajor
|
|
504
|
+
0, # rdevminor
|
|
505
|
+
namesize, # namesize
|
|
506
|
+
0, # checksum (0 for newc)
|
|
507
|
+
name, # name
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# Pad header to 4-byte boundary
|
|
511
|
+
header_padding = (4 - (header.bytesize % 4)) % 4
|
|
512
|
+
header << ("\0" * header_padding)
|
|
513
|
+
|
|
514
|
+
# Pad data to 4-byte boundary
|
|
515
|
+
data_padding = (4 - (filesize % 4)) % 4
|
|
516
|
+
|
|
517
|
+
header + data + ("\0" * data_padding)
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Build CPIO trailer
|
|
521
|
+
#
|
|
522
|
+
# @return [String] Trailer entry
|
|
523
|
+
def build_cpio_trailer
|
|
524
|
+
build_cpio_entry("TRAILER!!!", "", 0)
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Compress payload
|
|
528
|
+
#
|
|
529
|
+
# @param data [String] Uncompressed data
|
|
530
|
+
# @param output [IO] Output IO for compressed data
|
|
531
|
+
def compress_payload(data, output)
|
|
532
|
+
case @compression
|
|
533
|
+
when :gzip
|
|
534
|
+
gz = Zlib::GzipWriter.new(output, 9)
|
|
535
|
+
gz.write(data)
|
|
536
|
+
gz.finish
|
|
537
|
+
when :none
|
|
538
|
+
output.write(data)
|
|
539
|
+
else
|
|
540
|
+
raise ArgumentError, "Unsupported compression: #{@compression}"
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
end
|
data/lib/omnizip/formats/rpm.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Omnizip
|
|
|
12
12
|
module Formats
|
|
13
13
|
# RPM package format support
|
|
14
14
|
#
|
|
15
|
-
# Provides read access to RPM packages, extracting metadata
|
|
15
|
+
# Provides read and write access to RPM packages, extracting metadata
|
|
16
16
|
# and file contents from the payload.
|
|
17
17
|
#
|
|
18
18
|
# @example Open RPM and list files
|
|
@@ -22,7 +22,17 @@ module Omnizip
|
|
|
22
22
|
#
|
|
23
23
|
# @example Extract RPM contents
|
|
24
24
|
# Omnizip::Formats::Rpm.extract('package.rpm', 'output/')
|
|
25
|
+
#
|
|
26
|
+
# @example Create an RPM package
|
|
27
|
+
# writer = Omnizip::Formats::Rpm::Writer.new(
|
|
28
|
+
# name: "mypkg",
|
|
29
|
+
# version: "1.0.0",
|
|
30
|
+
# release: "1"
|
|
31
|
+
# )
|
|
32
|
+
# writer.add_file("/usr/bin/app", "#!/bin/sh\necho hello")
|
|
33
|
+
# writer.write("mypkg.rpm")
|
|
25
34
|
module Rpm
|
|
35
|
+
autoload :Writer, "omnizip/formats/rpm/writer"
|
|
26
36
|
class << self
|
|
27
37
|
# Open RPM package
|
|
28
38
|
#
|
|
@@ -302,6 +312,19 @@ module Omnizip
|
|
|
302
312
|
extract_cpio(decompressor, output_dir)
|
|
303
313
|
end
|
|
304
314
|
|
|
315
|
+
# Get raw payload data
|
|
316
|
+
#
|
|
317
|
+
# Returns the compressed payload as-is (without decompression).
|
|
318
|
+
# Useful for saving the payload as a file (e.g., fonts.src.cpio.gz).
|
|
319
|
+
#
|
|
320
|
+
# @return [String] Raw compressed payload data
|
|
321
|
+
def raw_payload
|
|
322
|
+
raise "RPM not opened" unless @file
|
|
323
|
+
|
|
324
|
+
payload_io = payload
|
|
325
|
+
payload_io.read
|
|
326
|
+
end
|
|
327
|
+
|
|
305
328
|
private
|
|
306
329
|
|
|
307
330
|
def parse!
|
data/lib/omnizip/version.rb
CHANGED
data/lib/omnizip.rb
CHANGED
|
@@ -96,6 +96,16 @@ require_relative "omnizip/formats/xar"
|
|
|
96
96
|
# ISO 9660 CD-ROM format (Weeks 11-14)
|
|
97
97
|
require_relative "omnizip/formats/iso"
|
|
98
98
|
|
|
99
|
+
# CPIO, RPM, and OLE formats (autoload for lazy loading)
|
|
100
|
+
# These formats benefit most from autoload since they are less commonly used
|
|
101
|
+
module Omnizip
|
|
102
|
+
module Formats
|
|
103
|
+
autoload :Cpio, "omnizip/formats/cpio"
|
|
104
|
+
autoload :Rpm, "omnizip/formats/rpm"
|
|
105
|
+
autoload :Ole, "omnizip/formats/ole"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
99
109
|
# Platform-specific features (Weeks 11-14)
|
|
100
110
|
require_relative "omnizip/platform"
|
|
101
111
|
require_relative "omnizip/platform/ntfs_streams"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: omnizip
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -449,6 +449,7 @@ files:
|
|
|
449
449
|
- lib/omnizip/formats/rpm/header.rb
|
|
450
450
|
- lib/omnizip/formats/rpm/lead.rb
|
|
451
451
|
- lib/omnizip/formats/rpm/tag.rb
|
|
452
|
+
- lib/omnizip/formats/rpm/writer.rb
|
|
452
453
|
- lib/omnizip/formats/seven_zip.rb
|
|
453
454
|
- lib/omnizip/formats/seven_zip/bcj2_stream_decompressor.rb
|
|
454
455
|
- lib/omnizip/formats/seven_zip/coder_chain.rb
|