manticore-smash 3.1.0 → 3.3.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.md +700 -492
- data/lib/manticore.rb +5 -0
- data/lib/xlsxkit/sax_parser.rb +294 -0
- data/lib/xlsxkit/workbook.rb +427 -0
- data/lib/xlsxkit/writer.rb +279 -0
- data/lib/xlsxkit/zip_reader.rb +177 -0
- data/lib/xlsxkit/zip_writer.rb +118 -0
- metadata +7 -2
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# Copyright (C) 2024 Manticore Authors
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU Affero General Public License as published
|
|
7
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU Affero General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
require 'stringio'
|
|
19
|
+
require 'zlib'
|
|
20
|
+
|
|
21
|
+
module XlsxKit
|
|
22
|
+
##
|
|
23
|
+
# 最小化 ZIP32 读取器,专为 XLSX 解包设计。
|
|
24
|
+
#
|
|
25
|
+
# 不依赖 rubyzip 等第三方 gem,仅支持 XLSX 中出现的两种压缩方式:
|
|
26
|
+
# - DEFLATE (method 8): 通用压缩
|
|
27
|
+
# - STORED (method 0): 无压缩
|
|
28
|
+
#
|
|
29
|
+
# 实现思路:
|
|
30
|
+
# 1. 从文件末尾反向搜索 EOCD (End of Central Directory) 签名
|
|
31
|
+
# 2. 解析中央目录,获取所有条目的元数据与 Local Header 偏移
|
|
32
|
+
# 3. 按需读取单个条目:定位 Local Header → 跳过头部 → Zlib::Inflate 解压
|
|
33
|
+
#
|
|
34
|
+
# 与全量解包不同,本 reader 仅按 name 读取单个条目,避免将整个 ZIP 展开到内存或磁盘。
|
|
35
|
+
|
|
36
|
+
class ZipReader
|
|
37
|
+
# ZIP 格式签名(freeze 防止外部 << 修改)
|
|
38
|
+
EOCD_SIG = "PK\x05\x06".b.freeze # End of Central Directory
|
|
39
|
+
CDENTRY_SIG = "PK\x01\x02".b.freeze # Central Directory File Header
|
|
40
|
+
LOCAL_SIG = "PK\x03\x04".b.freeze # Local File Header
|
|
41
|
+
|
|
42
|
+
# 压缩方法
|
|
43
|
+
STORED = 0
|
|
44
|
+
DEFLATE = 8
|
|
45
|
+
|
|
46
|
+
attr_reader :entries
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# 初始化 ZIP 读取器。
|
|
50
|
+
#
|
|
51
|
+
# @param io [IO, String] 文件路径或已打开的 IO(必须可 seek)
|
|
52
|
+
def initialize(io)
|
|
53
|
+
@io = io.is_a?(String) ? File.open(io, 'rb') : io
|
|
54
|
+
@entries = nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# 便捷类方法:打开文件并读取后关闭。
|
|
59
|
+
def self.open(path)
|
|
60
|
+
reader = new(path)
|
|
61
|
+
yield reader
|
|
62
|
+
ensure
|
|
63
|
+
reader&.close
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
# 关闭底层 IO(若由本类打开)。
|
|
68
|
+
def close
|
|
69
|
+
@io.close unless @io.closed?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
##
|
|
73
|
+
# 解析中央目录,返回 {name => entry} 的 Hash。
|
|
74
|
+
# entry 为包含 :name, :method, :compressed_size, :uncompressed_size, :local_offset 的 Hash。
|
|
75
|
+
def entries
|
|
76
|
+
return @entries if @entries
|
|
77
|
+
|
|
78
|
+
offset = find_eocd
|
|
79
|
+
@io.seek(offset)
|
|
80
|
+
sig = @io.read(4)
|
|
81
|
+
raise "EOCD signature not found" unless sig == EOCD_SIG
|
|
82
|
+
|
|
83
|
+
_disk_num, _ent_disk, _ent_here, total_ent,
|
|
84
|
+
_cd_size, cd_offset = @io.read(16).unpack('vvvvVV')
|
|
85
|
+
|
|
86
|
+
@io.seek(cd_offset)
|
|
87
|
+
@entries = {}
|
|
88
|
+
|
|
89
|
+
total_ent.times do
|
|
90
|
+
sig = @io.read(4)
|
|
91
|
+
break unless sig == CDENTRY_SIG
|
|
92
|
+
|
|
93
|
+
_ver_made, _ver_need, _flags, method,
|
|
94
|
+
_mtime, _mdate, crc32, comp_size, uncomp_size,
|
|
95
|
+
name_len, extra_len, comm_len,
|
|
96
|
+
_disk, _iattr, _eattr, local_offset = @io.read(42).unpack('vvvvvvVVVvvvvvVV')
|
|
97
|
+
|
|
98
|
+
name = @io.read(name_len).force_encoding('UTF-8')
|
|
99
|
+
@io.read(extra_len + comm_len) # 跳过 extra field 与 comment
|
|
100
|
+
|
|
101
|
+
@entries[name] = {
|
|
102
|
+
name: name,
|
|
103
|
+
method: method,
|
|
104
|
+
compressed_size: comp_size,
|
|
105
|
+
uncompressed_size: uncomp_size,
|
|
106
|
+
crc32: crc32,
|
|
107
|
+
local_offset: local_offset
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
@entries
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
##
|
|
115
|
+
# 读取指定条目的原始字节(已解压)。
|
|
116
|
+
#
|
|
117
|
+
# @param name [String] 条目名(如 "xl/sharedStrings.xml")
|
|
118
|
+
# @return [String, nil] 二进制内容;条目不存在时返回 nil
|
|
119
|
+
def read_entry(name)
|
|
120
|
+
entry = entries[name]
|
|
121
|
+
return nil unless entry
|
|
122
|
+
|
|
123
|
+
data_offset = resolve_data_offset(entry[:local_offset])
|
|
124
|
+
@io.seek(data_offset)
|
|
125
|
+
raw = @io.read(entry[:compressed_size])
|
|
126
|
+
|
|
127
|
+
case entry[:method]
|
|
128
|
+
when STORED
|
|
129
|
+
raw
|
|
130
|
+
when DEFLATE
|
|
131
|
+
Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(raw)
|
|
132
|
+
else
|
|
133
|
+
raise "Unsupported compression method: #{entry[:method]}"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
##
|
|
138
|
+
# 读取指定条目并强制 UTF-8 编码(用于 XML 文本)。
|
|
139
|
+
def read_entry_utf8(name)
|
|
140
|
+
data = read_entry(name)
|
|
141
|
+
return nil unless data
|
|
142
|
+
data.force_encoding('UTF-8')
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
##
|
|
148
|
+
# 从文件末尾反向搜索 EOCD 签名。
|
|
149
|
+
# EOCD 最小 22 字节,最大 22 + 65535(comment 区段),从尾部向前扫。
|
|
150
|
+
def find_eocd
|
|
151
|
+
size = @io.size
|
|
152
|
+
scan_size = [size, 65557].min # 22 + 65535
|
|
153
|
+
@io.seek(size - scan_size)
|
|
154
|
+
tail = @io.read(scan_size)
|
|
155
|
+
tail = tail.force_encoding('BINARY')
|
|
156
|
+
|
|
157
|
+
pat = EOCD_SIG.b # dup 避免修改 frozen 常量
|
|
158
|
+
pos = tail.rindex(pat)
|
|
159
|
+
raise "EOCD record not found — not a valid ZIP file" unless pos
|
|
160
|
+
|
|
161
|
+
size - scan_size + pos
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
##
|
|
165
|
+
# 解析 Local File Header,返回实际数据起始偏移。
|
|
166
|
+
def resolve_data_offset(local_offset)
|
|
167
|
+
@io.seek(local_offset)
|
|
168
|
+
sig = @io.read(4)
|
|
169
|
+
raise "Local file header signature not found at #{local_offset}" unless sig == LOCAL_SIG
|
|
170
|
+
|
|
171
|
+
_ver, _flags, _method, _mtime, _mdate,
|
|
172
|
+
_crc, _comp, _uncomp, name_len, extra_len = @io.read(26).unpack('vvvvvVVVvv')
|
|
173
|
+
|
|
174
|
+
local_offset + 30 + name_len + extra_len
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# Copyright (C) 2024 Manticore Authors
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU Affero General Public License as published
|
|
7
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU Affero General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
require 'zlib'
|
|
19
|
+
|
|
20
|
+
module XlsxKit
|
|
21
|
+
##
|
|
22
|
+
# 最小化 ZIP32 写入器,专为 XLSX 打包设计。
|
|
23
|
+
#
|
|
24
|
+
# 与 ZipReader 对应,不依赖 rubyzip,仅支持写入 XLSX 所需的两种压缩方式:
|
|
25
|
+
# - DEFLATE (method 8): 通用压缩
|
|
26
|
+
# - STORED (method 0): 无压缩
|
|
27
|
+
#
|
|
28
|
+
# 实现思路:
|
|
29
|
+
# 1. add_entry 时即时写出 Local File Header + 数据,并记录偏移
|
|
30
|
+
# 2. 同时在内存累积 Central Directory 记录
|
|
31
|
+
# 3. close 时写出 Central Directory + EOCD 收尾
|
|
32
|
+
#
|
|
33
|
+
# 边写边压,避免将整个 ZIP 缓冲在内存或磁盘。
|
|
34
|
+
|
|
35
|
+
class ZipWriter
|
|
36
|
+
# ZIP 格式签名
|
|
37
|
+
LOCAL_SIG = "PK\x03\x04".b.freeze # Local File Header
|
|
38
|
+
CDENTRY_SIG = "PK\x01\x02".b.freeze # Central Directory File Header
|
|
39
|
+
EOCD_SIG = "PK\x05\x06".b.freeze # End of Central Directory
|
|
40
|
+
|
|
41
|
+
# 压缩方法
|
|
42
|
+
STORED = 0
|
|
43
|
+
DEFLATE = 8
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# 初始化写入器。
|
|
47
|
+
#
|
|
48
|
+
# @param io [IO] 可写 IO(必须已以二进制模式打开)
|
|
49
|
+
def initialize(io)
|
|
50
|
+
@io = io
|
|
51
|
+
@offset = 0 # 当前数据起始偏移(用于 Central Directory 定位)
|
|
52
|
+
@count = 0 # 条目数
|
|
53
|
+
@central = ''.b # 中央目录累积缓冲
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# 便捷类方法:写入文件路径,块结束后自动收尾并关闭。
|
|
58
|
+
def self.open(path)
|
|
59
|
+
File.open(path, 'wb') do |f|
|
|
60
|
+
writer = new(f)
|
|
61
|
+
yield writer
|
|
62
|
+
writer.close
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
# 添加一个条目(文件),立即写出数据。
|
|
68
|
+
#
|
|
69
|
+
# @param name [String] 条目名(如 "xl/worksheets/sheet1.xml")
|
|
70
|
+
# @param data [String] 内容(UTF-8 字符串或二进制)
|
|
71
|
+
# @param method [Integer] 压缩方式(DEFLATE / STORED)
|
|
72
|
+
def add_entry(name, data, method: DEFLATE)
|
|
73
|
+
name_bytes = name.b
|
|
74
|
+
data_bytes = data.b
|
|
75
|
+
crc = Zlib.crc32(data_bytes)
|
|
76
|
+
|
|
77
|
+
compressed =
|
|
78
|
+
case method
|
|
79
|
+
when STORED then data_bytes
|
|
80
|
+
when DEFLATE then Zlib::Deflate.new(nil, -Zlib::MAX_WBITS).deflate(data_bytes, Zlib::FINISH)
|
|
81
|
+
else raise ArgumentError, "unsupported compression method: #{method}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Local File Header (30 bytes + name + data)
|
|
85
|
+
local = LOCAL_SIG.dup
|
|
86
|
+
local << [20, 0, method, 0, 0, crc,
|
|
87
|
+
compressed.bytesize, data_bytes.bytesize,
|
|
88
|
+
name_bytes.bytesize, 0].pack('vvvvvVVVvv')
|
|
89
|
+
local << name_bytes
|
|
90
|
+
local << compressed
|
|
91
|
+
@io.write(local)
|
|
92
|
+
|
|
93
|
+
# Central Directory Record (46 bytes + name)
|
|
94
|
+
cd = CDENTRY_SIG.dup
|
|
95
|
+
cd << [20, 20, 0, method, 0, 0, crc,
|
|
96
|
+
compressed.bytesize, data_bytes.bytesize,
|
|
97
|
+
name_bytes.bytesize, 0, 0, 0, 0, 0, @offset].pack('vvvvvvVVVvvvvvVV')
|
|
98
|
+
cd << name_bytes
|
|
99
|
+
@central << cd
|
|
100
|
+
|
|
101
|
+
@offset += local.bytesize
|
|
102
|
+
@count += 1
|
|
103
|
+
self
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
##
|
|
107
|
+
# 写出 Central Directory 与 EOCD,收尾 ZIP 文件。
|
|
108
|
+
def close
|
|
109
|
+
cd_offset = @offset
|
|
110
|
+
@io.write(@central)
|
|
111
|
+
|
|
112
|
+
eocd = EOCD_SIG.dup
|
|
113
|
+
eocd << [0, 0, @count, @count, @central.bytesize, cd_offset, 0].pack('vvvvVVv')
|
|
114
|
+
@io.write(eocd)
|
|
115
|
+
@io
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: manticore-smash
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Frampt
|
|
@@ -18,6 +18,11 @@ files:
|
|
|
18
18
|
- README.md
|
|
19
19
|
- lib/manticore.rb
|
|
20
20
|
- lib/mdutils/rediscount.rb
|
|
21
|
+
- lib/xlsxkit/sax_parser.rb
|
|
22
|
+
- lib/xlsxkit/workbook.rb
|
|
23
|
+
- lib/xlsxkit/writer.rb
|
|
24
|
+
- lib/xlsxkit/zip_reader.rb
|
|
25
|
+
- lib/xlsxkit/zip_writer.rb
|
|
21
26
|
- lib/xmlutils/formatters.rb
|
|
22
27
|
- lib/xmlutils/node.rb
|
|
23
28
|
- lib/xmlutils/tokenizer.rb
|
|
@@ -42,7 +47,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
42
47
|
- !ruby/object:Gem::Version
|
|
43
48
|
version: '0'
|
|
44
49
|
requirements: []
|
|
45
|
-
rubygems_version: 4.0.
|
|
50
|
+
rubygems_version: 4.0.19
|
|
46
51
|
specification_version: 4
|
|
47
52
|
summary: A multi-format toolkit
|
|
48
53
|
test_files: []
|