zipography 0.0.1 → 0.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1827f4d1fd43192ac37e029981ab04de876a87d8a9e91b044873d671de1738cc
4
- data.tar.gz: c2de37c58542aca50915c71712cda9f420cc2dded28cf95a9af1c369465774f7
3
+ metadata.gz: 1d865aef5a55e1a953eb86bbbdb97d2e1f78a9c9eb77e81b7954cf37f9d154d6
4
+ data.tar.gz: 5616046d45ccc02435baa0c99e7b1fc799faca97e2daa2762c5f8b98fe17ccd3
5
5
  SHA512:
6
- metadata.gz: 960794c6d6a3cdb205c54e4b9ef1c38d840ca061e6fd547340c1122c88310312a19f2ff40e481a01d365bc8e76076fe01ee6d8a1fe25e243ce7e086c271fd24a
7
- data.tar.gz: 381072f98336e90d7dc953821a7c5812cf736b09a0f19216915acdd5c82a498e7fb1f4e4c911c2519f8222484a26305fbd1becac4ee9fa15907adc61c2a00b66
6
+ metadata.gz: 89a240e96e8f953a1a9072e53b90ba2a4a27896170d3382ad0fbfa8e5e532360feee56184ac047c3e09ad77f36b7742c167b9100e3224eaa7c5de8a14e6942eb
7
+ data.tar.gz: cbf032cfcfa94752a5cac53f4f0ad5bf2de13e3a842b6a001df36fc8e177ce4706893707e8ee83979e58bcaa9d13d1b3cf376b9eab8a9bc4930aeb9578e2b582
data/README.md CHANGED
@@ -9,11 +9,17 @@ file managers (Windows Explorer), the blob is invisible.
9
9
  Limitations:
10
10
 
11
11
  * single blob only (but you can just add another .zip as a blob);
12
- * doesn't work w/ zip64 files (this means 4GB max for an archive+blob
13
- combo);
14
- * corrupts zip spec v6.2+ files that use *central directory*
15
- encryption;
16
- * memory hungry, for it's just a toy, not a serious spy tool.
12
+ * doesn't work w/ zip64 files (this means ~4GB max for an archive+blob
13
+ combo).
14
+
15
+ ## How does it work?
16
+
17
+ A blob is injected after a file section right before the 1st *central
18
+ directory header*. After that, a pointer in an *end of central
19
+ directory* record is updated to compensate the shift of the *central
20
+ directory header*.
21
+
22
+ <img src='doc/zip.svg'>
17
23
 
18
24
  ## Usage
19
25
 
@@ -32,7 +38,9 @@ Inject a picture into the archive:
32
38
 
33
39
  $ zipography-inject orig.zip blob1.png > 1.zip
34
40
 
35
- It it visible? No:
41
+ (On Windows, use `-o 1.zip`, instead of a redirection.)
42
+
43
+ Is it visible? It isn't:
36
44
 
37
45
  ~~~
38
46
  $ bsdtar tf 1.zip
@@ -51,7 +59,7 @@ Check if we have the picture in the archive:
51
59
  $ zipography-info 1.zip
52
60
  Payload size: 18313
53
61
  Adler32: 0x6812d9f
54
- Version: 1
62
+ Blob version: 1
55
63
  Valid: true
56
64
  ~~~
57
65
 
@@ -64,15 +72,6 @@ $ xdg-open !$
64
72
 
65
73
  <img src='test/blob1.png'>
66
74
 
67
- ## How does it work?
68
-
69
- A blob is injected after a file section right before the 1st *central
70
- directory header*. After that, a pointer in an *end of central
71
- directory* record is updated to compensate the shift of the *central
72
- directory header*.
73
-
74
- <img src='doc/zip.svg'>
75
-
76
75
  ## License
77
76
 
78
77
  MIT.
data/lib.rb CHANGED
@@ -1,80 +1,186 @@
1
+ require 'optparse'
1
2
  require 'zlib'
2
3
  require 'bindata'
3
4
 
4
5
  module Zipography
5
-
6
- HEADER_SIZE = 9 # bytes
7
- class HiddenBlob < BinData::Record
6
+ class Eocd < BinData::Record
8
7
  endian :little
9
8
 
10
- uint32 :checksum
11
- uint32 :len
12
- uint8 :version, initial_value: 1
9
+ uint32 :signature, asserted_value: 0x06054b50
10
+ uint16 :disk
11
+ uint16 :disk_cd_start
12
+ uint16 :cd_entries_disk
13
+ uint16 :cd_entries_total
14
+ uint32 :cd_size
15
+ uint32 :cd_offset_start
16
+ uint16 :comment_len
17
+ string :comment, :read_length => :comment_len,
18
+ onlyif: -> { comment_len.nonzero? }
13
19
  end
14
20
 
15
- def checksum s; Zlib.adler32(s); end
21
+ class IOReverseChunks
22
+ include Enumerable
16
23
 
17
- def blob_make file
18
- payload = File.read file
19
- header = HiddenBlob.new len: File.stat(file).size,
20
- checksum: checksum(payload)
21
- [payload.force_encoding('ASCII-8BIT'), header.to_binary_s].join
22
- end
24
+ def initialize io, bufsiz: 4096, debug: false
25
+ @io = io
26
+ @fsize = io.stat.size
27
+ @bufsiz = bufsiz > @fsize ? (@fsize/2.0).ceil : bufsiz
28
+
29
+ @seek = -@bufsiz
30
+ @io.seek @seek, IO::SEEK_END
23
31
 
24
- class MyZip
25
- def initialize file
26
- @file = file
27
- @buf = File.read file
28
- @eocd = end_of_central_dir
29
- @first_cdh = first_central_dir_header
32
+ @debug = debug
33
+ $stderr.puts "#{@io.path}: seek=#{@seek}, bufsiz=#{@bufsiz}" if @debug
30
34
  end
35
+ attr_reader :io, :fsize
36
+
37
+ def each
38
+ idx = 0
39
+ while (chunk = @io.read(@bufsiz))
40
+ return if chunk.size == 0
31
41
 
32
- def first_central_dir_header
33
- @buf.index [0x02014b50].pack('V')
42
+ yield chunk
43
+ idx += 1
44
+
45
+ return if @seek == 0
46
+
47
+ @seek = -@bufsiz*(idx+1)
48
+ begin
49
+ @io.seek @seek, IO::SEEK_END
50
+ rescue Errno::EINVAL
51
+ @bufsiz = -(@bufsiz*(idx+1) - @fsize - @bufsiz)
52
+ @seek = 0
53
+ @io.seek @seek
54
+ end
55
+
56
+ $stderr.puts "#{@io.path}: seek=#{@seek}, bufsiz=#{@bufsiz}" if @debug
57
+ end
34
58
  end
59
+ end
35
60
 
36
- def end_of_central_dir
37
- pos = @buf.rindex [0x06054b50].pack('V')
38
- fail 'not a zip file' unless pos
39
- pos
61
+ def file_rindex file, substring
62
+ my_rindex = ->(s1, s2) { s1.rindex s2.dup.force_encoding('ASCII-8BIT') }
63
+
64
+ r = nil
65
+ File.open(file, 'rb') do |io|
66
+ iorc = IOReverseChunks.new io
67
+ prev_chunk = ''
68
+ bytes_read = 0
69
+
70
+ r = iorc.each do |chunk|
71
+ bytes_read += chunk.size
72
+
73
+ if (idx = my_rindex.call(chunk, substring))
74
+ break iorc.fsize - bytes_read + idx
75
+ end
76
+
77
+ if my_rindex.call(chunk, substring[0])
78
+ two_chunks = chunk + prev_chunk
79
+ if (idx = my_rindex.call(two_chunks, substring))
80
+ break iorc.fsize - bytes_read + idx
81
+ end
82
+ end
83
+
84
+ prev_chunk = chunk
85
+ end
40
86
  end
41
87
 
42
- # start of central dir offset
43
- def offset
44
- @buf.slice(@eocd+16, 4).unpack('V').first
88
+ r
89
+ end
90
+
91
+ def eocd_position file
92
+ file_rindex(file, [0x06054b50].pack('V')) || fail("#{file}: not a zip")
93
+ end
94
+
95
+ def eocd_parse file, pos
96
+ File.open(file, 'rb') do |f|
97
+ f.seek pos
98
+ r = Eocd.read f
99
+ fail "no support for zip64 format" if r[:cd_offset_start] == 0xFFFFFFFF
100
+ r
45
101
  end
102
+ end
46
103
 
47
- # very crude: instead of an intelligent parsing of eocd, modifying
48
- # an offset, replacing eocd, we just change the offset. this won't
49
- # fly for zip64 files
50
- def repack data
51
- [
52
- @buf.slice(0, @first_cdh),
53
- data,
54
- # just before the offset of start of central dir
55
- @buf.slice(@first_cdh, (@eocd+16) - @first_cdh),
56
- # inject new offset
57
- [offset + data.bytesize].pack('V'),
58
- # the rest
59
- @buf.slice(@eocd+16+4, @buf.size)
60
- ]
104
+ HIDDEN_BLOB_VERSION = 1
105
+ HIDDEN_BLOB_HEADER_SIZE = 9 # bytes
106
+
107
+ class HiddenBlobHeader < BinData::Record
108
+ endian :little
109
+
110
+ uint32 :checksum
111
+ uint32 :len
112
+ uint8 :version, initial_value: HIDDEN_BLOB_VERSION
113
+ end
114
+
115
+ def adler32_file file, bufsiz: 1 << 16
116
+ r = nil
117
+ File.open(file, 'rb') do |f|
118
+ while (chunk = f.read bufsiz)
119
+ r = Zlib.adler32 chunk, r
120
+ end
61
121
  end
122
+ r
123
+ end
124
+
125
+ def blob_write file, dest
126
+ blob_size = File.stat(file).size
127
+
128
+ header = HiddenBlobHeader.new len: blob_size, checksum: adler32_file(file)
129
+ IO.copy_stream file, dest
130
+ dest.write header.to_binary_s
62
131
 
63
- def blob
64
- payload = ''
65
- header = {}
66
- File.open(@file) do |f|
67
- f.seek(offset-HEADER_SIZE)
68
- header = HiddenBlob.read f
69
- f.seek(offset-HEADER_SIZE-header.len)
70
- payload = f.read header.len
132
+ blob_size + header.num_bytes
133
+ end
134
+
135
+ def adler32_file_slice file, start, length, bufsiz: 1 << 16
136
+ r = nil
137
+ File.open(file, 'rb') do |f|
138
+ f.seek start
139
+ bytes_read = 0
140
+ idx = 0
141
+ finito = false
142
+ while (chunk = f.read bufsiz)
143
+ bytes_read += chunk.bytesize
144
+
145
+ if bytes_read > length
146
+ if idx == 0
147
+ chunk = chunk.byteslice(0, length)
148
+ else
149
+ chunk = chunk.byteslice(0, chunk.bytesize - (bytes_read - length))
150
+ end
151
+ finito = true
152
+ end
153
+ r = Zlib.adler32 chunk, r
154
+ break if finito
155
+ idx += 1
71
156
  end
72
- { header: header, payload: payload }
73
157
  end
158
+ r
159
+ end
160
+
161
+ def adler2hex i; "0x" + i.to_i.to_s(16); end
74
162
 
75
- def payload_valid? blob
76
- blob[:header][:checksum] == checksum(blob[:payload])
163
+ def options usage, argv_size
164
+ opt = {}; op = OptionParser.new do |o|
165
+ o.banner = "Usage: #{File.basename $0} #{usage}"
166
+ o.on("-o FILE", "output") do |arg|
167
+ opt[:output] = File.open arg, 'wb'
168
+ end
77
169
  end
170
+ op.parse!
171
+
172
+ abort op.help if ARGV.size < argv_size
173
+ opt[:output] || $stdout
78
174
  end
79
175
 
176
+ def hbh_validate header
177
+ header[:len] <= 2**32 - 70 &&
178
+ header[:version] > 0 && header[:version] <= HIDDEN_BLOB_VERSION
179
+ end
180
+
181
+ def cd_offset_start_overflow zip_file, offset
182
+ max = (2**32)-1
183
+ actual = offset + File.stat(zip_file).size + HIDDEN_BLOB_HEADER_SIZE
184
+ actual - max
185
+ end
80
186
  end
@@ -3,6 +3,20 @@
3
3
  require_relative './lib'
4
4
  include Zipography
5
5
 
6
- z = MyZip.new ARGV[0]
7
- blob = z.blob
8
- z.payload_valid?(blob) ? $stdout.write(blob[:payload]) : exit(1)
6
+ output = options 'file.zip [-o blob]', 1
7
+
8
+ eocd = eocd_parse ARGV[0], eocd_position(ARGV[0])
9
+
10
+ File.open(ARGV[0], 'rb') do |f|
11
+ f.seek eocd[:cd_offset_start] - HIDDEN_BLOB_HEADER_SIZE
12
+ header = HiddenBlobHeader.read f
13
+
14
+ # validate
15
+ exit 1 unless hbh_validate header
16
+ start_location = eocd[:cd_offset_start] - HIDDEN_BLOB_HEADER_SIZE - header.len
17
+ checksum = adler32_file_slice ARGV[0], start_location, header.len
18
+ exit 1 unless checksum == header[:checksum]
19
+
20
+ f.seek start_location
21
+ IO.copy_stream f, output, header.len
22
+ end
@@ -3,10 +3,25 @@
3
3
  require_relative './lib'
4
4
  include Zipography
5
5
 
6
- z = MyZip.new ARGV[0]
7
- blob = z.blob
6
+ eocd = eocd_parse ARGV[0], eocd_position(ARGV[0])
8
7
 
9
- puts "Payload size: #{blob[:header][:len]}"
10
- puts "Adler32: 0x" + blob[:header][:checksum].to_i.to_s(16)
11
- puts "Version: #{blob[:header][:version]}"
12
- puts "Valid: #{z.payload_valid?(blob)}"
8
+ header = File.open(ARGV[0], 'rb') do |f|
9
+ f.seek eocd[:cd_offset_start] - HIDDEN_BLOB_HEADER_SIZE
10
+ HiddenBlobHeader.read f
11
+ end
12
+
13
+ puts "Payload size: #{header[:len]}"
14
+ puts "Adler32: " + adler2hex(header[:checksum])
15
+ puts "Blob version: #{header[:version]}"
16
+
17
+ unless hbh_validate header
18
+ puts "Error: incompatible version or invalid payload size"
19
+ exit 1
20
+ end
21
+
22
+ checksum = adler32_file_slice ARGV[0], eocd[:cd_offset_start]-HIDDEN_BLOB_HEADER_SIZE-header.len, header.len
23
+
24
+ if checksum != header[:checksum]
25
+ puts "Error: invalid checksum #{adler2hex(checksum)}"
26
+ exit 1
27
+ end
@@ -3,8 +3,28 @@
3
3
  require_relative './lib'
4
4
  include Zipography
5
5
 
6
- abort "Usage: #{File.basename __FILE__} old.zip blob > new.zip" if ARGV.size < 2
6
+ output = options 'orig.zip blob [-o new.zip]', 2
7
7
 
8
- z = MyZip.new ARGV[0]
9
- data = blob_make ARGV[1]
10
- z.repack(data).each {|buf| $stdout.write buf }
8
+ # 0. get EOCD
9
+ eocd_pos = eocd_position ARGV[0]
10
+ eocd = eocd_parse ARGV[0], eocd_pos
11
+
12
+ # 1. check a would-be zip size
13
+ oo = cd_offset_start_overflow ARGV[1], eocd[:cd_offset_start]
14
+ fail "`#{ARGV[1]}` is too big: #{oo} extra byte(s)" if oo > 0
15
+
16
+ File.open(ARGV[0], 'rb') do |f|
17
+ # 2. Copy everything before CDH
18
+ IO.copy_stream f, output, eocd[:cd_offset_start]
19
+
20
+ # 3. Inject our blob
21
+ blob_size = blob_write ARGV[1], output
22
+
23
+ # 4. Copy CDH
24
+ f.seek eocd[:cd_offset_start]
25
+ IO.copy_stream f, output, eocd_pos - eocd[:cd_offset_start]
26
+
27
+ # 5. Add an updated EOCD
28
+ eocd[:cd_offset_start] += blob_size
29
+ output.write eocd.to_binary_s
30
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zipography
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Gromnitsky
8
8
  autorequire:
9
9
  bindir: "."
10
10
  cert_chain: []
11
- date: 2020-08-22 00:00:00.000000000 Z
11
+ date: 2020-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bindata