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 +4 -4
- data/README.md +15 -16
- data/lib.rb +159 -53
- data/zipography-extract +17 -3
- data/zipography-info +21 -6
- data/zipography-inject +24 -4
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1d865aef5a55e1a953eb86bbbdb97d2e1f78a9c9eb77e81b7954cf37f9d154d6
|
4
|
+
data.tar.gz: 5616046d45ccc02435baa0c99e7b1fc799faca97e2daa2762c5f8b98fe17ccd3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
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
|
-
|
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 :
|
11
|
-
|
12
|
-
|
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
|
-
|
21
|
+
class IOReverseChunks
|
22
|
+
include Enumerable
|
16
23
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
25
|
-
|
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
|
-
|
33
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
76
|
-
|
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
|
data/zipography-extract
CHANGED
@@ -3,6 +3,20 @@
|
|
3
3
|
require_relative './lib'
|
4
4
|
include Zipography
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
data/zipography-info
CHANGED
@@ -3,10 +3,25 @@
|
|
3
3
|
require_relative './lib'
|
4
4
|
include Zipography
|
5
5
|
|
6
|
-
|
7
|
-
blob = z.blob
|
6
|
+
eocd = eocd_parse ARGV[0], eocd_position(ARGV[0])
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
data/zipography-inject
CHANGED
@@ -3,8 +3,28 @@
|
|
3
3
|
require_relative './lib'
|
4
4
|
include Zipography
|
5
5
|
|
6
|
-
|
6
|
+
output = options 'orig.zip blob [-o new.zip]', 2
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
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.
|
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-
|
11
|
+
date: 2020-08-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bindata
|