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
data/lib/manticore.rb
CHANGED
|
@@ -22,3 +22,8 @@ require_relative 'xmlutils/xpath'
|
|
|
22
22
|
require_relative 'xmlutils/formatters'
|
|
23
23
|
require_relative 'xmlutils/xml_doc'
|
|
24
24
|
require_relative 'mdutils/rediscount'
|
|
25
|
+
require_relative 'xlsxkit/zip_reader'
|
|
26
|
+
require_relative 'xlsxkit/zip_writer'
|
|
27
|
+
require_relative 'xlsxkit/sax_parser'
|
|
28
|
+
require_relative 'xlsxkit/workbook'
|
|
29
|
+
require_relative 'xlsxkit/writer'
|
|
@@ -0,0 +1,294 @@
|
|
|
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
|
+
|
|
20
|
+
module XlsxKit
|
|
21
|
+
##
|
|
22
|
+
# 轻量 SAX 风格 XML 解析器,专为 XLSX 内部 XML 流式处理设计。
|
|
23
|
+
#
|
|
24
|
+
# 与 XmlUtils::TreeParser(DOM 全量建树)不同,本解析器不在内存中构建节点树,
|
|
25
|
+
# 而是逐标签触发回调(start_element / end_element / characters),内存占用恒定。
|
|
26
|
+
#
|
|
27
|
+
# 核心算法:增量缓冲 — 每次从 IO 读取一块数据追加到 @buf,
|
|
28
|
+
# 尝试提取已完整的标签/文本段并回调,不完整的尾部保留到下次。
|
|
29
|
+
# 这样即使数百 MB 的 sheet XML 也只占用几十 KB 内存。
|
|
30
|
+
#
|
|
31
|
+
# Handler 协议(鸭子类型,无需继承):
|
|
32
|
+
# start_element(name, attrs) — 遇到开始标签
|
|
33
|
+
# end_element(name) — 遇到结束标签
|
|
34
|
+
# characters(text) — 遇到文本内容(可能分多次回调)
|
|
35
|
+
|
|
36
|
+
class SAXParser
|
|
37
|
+
BUFFER_SIZE = 65536 # 64KB 读取块
|
|
38
|
+
|
|
39
|
+
def self.parse(source, handler)
|
|
40
|
+
new(source, handler).parse
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(source, handler)
|
|
44
|
+
@source = source
|
|
45
|
+
@handler = handler
|
|
46
|
+
@buf = ''.b
|
|
47
|
+
@eof = false
|
|
48
|
+
@i = 0 # 已消费位置
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse
|
|
52
|
+
until @eof && @i >= @buf.length
|
|
53
|
+
fill_buffer if !@eof && (@buf.length - @i) < BUFFER_SIZE
|
|
54
|
+
step || (@eof = true)
|
|
55
|
+
end
|
|
56
|
+
# 尾部残余文本
|
|
57
|
+
if @i < @buf.length
|
|
58
|
+
text = @buf[@i..]
|
|
59
|
+
emit_text(text) unless text.strip.empty?
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def fill_buffer
|
|
66
|
+
chunk = @source.read(BUFFER_SIZE)
|
|
67
|
+
if chunk && !chunk.empty?
|
|
68
|
+
@buf << chunk
|
|
69
|
+
else
|
|
70
|
+
@eof = true
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
##
|
|
75
|
+
# 尝试从 @buf 的 @i 位置提取一个完整元素(文本段或标签),
|
|
76
|
+
# 成功返回 true(已消费),失败返回 nil(需要更多数据)。
|
|
77
|
+
def step
|
|
78
|
+
return nil if @i >= @buf.length
|
|
79
|
+
|
|
80
|
+
lt = @buf.index('<', @i)
|
|
81
|
+
|
|
82
|
+
# --- 情况一:当前位置是 '<',解析标签 ---
|
|
83
|
+
if @i == lt
|
|
84
|
+
parse_tag
|
|
85
|
+
# --- 情况二:有 '<' 在前方,先处理文本 ---
|
|
86
|
+
elsif lt
|
|
87
|
+
text = @buf[@i...lt]
|
|
88
|
+
@i = lt
|
|
89
|
+
emit_text(text) unless text.strip.empty?
|
|
90
|
+
true # 文本已消费,下次循环处理标签
|
|
91
|
+
# --- 情况三:无 '<',整个缓冲区都是文本(或已到 EOF)---
|
|
92
|
+
else
|
|
93
|
+
if @eof
|
|
94
|
+
text = @buf[@i..]
|
|
95
|
+
@i = @buf.length
|
|
96
|
+
emit_text(text) unless text.strip.empty?
|
|
97
|
+
true
|
|
98
|
+
else
|
|
99
|
+
# 留最后 1 字节(可能是 '<' 的一半... 虽然不会发生),等下次
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
#---------------------------------------------------------------------------
|
|
106
|
+
# 标签解析
|
|
107
|
+
#---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def parse_tag
|
|
110
|
+
# 确保 '>' 存在
|
|
111
|
+
return nil unless ensure_char('>')
|
|
112
|
+
|
|
113
|
+
ch = @buf[@i + 1]
|
|
114
|
+
|
|
115
|
+
case ch
|
|
116
|
+
when '!'
|
|
117
|
+
parse_special
|
|
118
|
+
when '?'
|
|
119
|
+
parse_pi
|
|
120
|
+
when '/'
|
|
121
|
+
parse_end_tag
|
|
122
|
+
else
|
|
123
|
+
parse_start_tag
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def parse_start_tag
|
|
128
|
+
# 注意:属性值中可能包含 '>',因此需逐字符扫描,跳过引号区域
|
|
129
|
+
gt_pos = find_tag_end(@i)
|
|
130
|
+
return nil unless gt_pos
|
|
131
|
+
|
|
132
|
+
raw = @buf[(@i + 1)...gt_pos]
|
|
133
|
+
@i = gt_pos + 1
|
|
134
|
+
|
|
135
|
+
self_closing = raw.end_with?('/')
|
|
136
|
+
raw = raw[0..-2] if self_closing
|
|
137
|
+
|
|
138
|
+
name, attrs = extract_name_and_attrs(raw)
|
|
139
|
+
@handler.start_element(name, attrs) if @handler.respond_to?(:start_element)
|
|
140
|
+
@handler.end_element(name) if self_closing && @handler.respond_to?(:end_element)
|
|
141
|
+
|
|
142
|
+
true
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def parse_end_tag
|
|
146
|
+
gt_pos = @buf.index('>', @i)
|
|
147
|
+
return nil unless gt_pos
|
|
148
|
+
|
|
149
|
+
name = @buf[(@i + 2)...gt_pos].strip
|
|
150
|
+
@i = gt_pos + 1
|
|
151
|
+
@handler.end_element(name) if @handler.respond_to?(:end_element)
|
|
152
|
+
true
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def parse_special
|
|
156
|
+
# <!-- comment --> 或 <![CDATA[ ... ]]>
|
|
157
|
+
if @buf[(@i + 2), 2] == '--'
|
|
158
|
+
end_idx = @buf.index('-->', @i)
|
|
159
|
+
return nil unless end_idx
|
|
160
|
+
@i = end_idx + 3
|
|
161
|
+
elsif @buf[(@i + 2), 7] == '[CDATA['
|
|
162
|
+
end_idx = @buf.index(']]>', @i)
|
|
163
|
+
return nil unless end_idx
|
|
164
|
+
|
|
165
|
+
data = @buf[(@i + 9)...end_idx]
|
|
166
|
+
emit_text(data)
|
|
167
|
+
@i = end_idx + 3
|
|
168
|
+
else
|
|
169
|
+
gt_pos = @buf.index('>', @i)
|
|
170
|
+
return nil unless gt_pos
|
|
171
|
+
@i = gt_pos + 1
|
|
172
|
+
end
|
|
173
|
+
true
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def parse_pi
|
|
177
|
+
close = @buf.index('?>', @i)
|
|
178
|
+
if close
|
|
179
|
+
@i = close + 2
|
|
180
|
+
else
|
|
181
|
+
gt_pos = @buf.index('>', @i)
|
|
182
|
+
return nil unless gt_pos
|
|
183
|
+
@i = gt_pos + 1
|
|
184
|
+
end
|
|
185
|
+
true
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
#---------------------------------------------------------------------------
|
|
189
|
+
# 辅助:标签内扫描(跳过引号区域内的 '>')
|
|
190
|
+
#---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
def find_tag_end(start)
|
|
193
|
+
i = start + 1
|
|
194
|
+
in_quote = nil
|
|
195
|
+
|
|
196
|
+
while i < @buf.length
|
|
197
|
+
ch = @buf[i]
|
|
198
|
+
|
|
199
|
+
if in_quote
|
|
200
|
+
i += 1
|
|
201
|
+
in_quote = nil if ch == in_quote
|
|
202
|
+
elsif ch == '"' || ch == "'"
|
|
203
|
+
in_quote = ch
|
|
204
|
+
i += 1
|
|
205
|
+
elsif ch == '>'
|
|
206
|
+
return i
|
|
207
|
+
else
|
|
208
|
+
i += 1
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# 未找到 '>'
|
|
213
|
+
nil
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def ensure_char(target)
|
|
217
|
+
@buf.index(target, @i) ? true : false
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def extract_name_and_attrs(raw)
|
|
221
|
+
# raw 形如: 'c r="A1" t="s"' 或 'c r="A1"/'
|
|
222
|
+
i = 0
|
|
223
|
+
i += 1 while raw[i] && raw[i] =~ /[A-Za-z0-9_:.\-]/
|
|
224
|
+
name = raw[0...i]
|
|
225
|
+
|
|
226
|
+
attrs = {}
|
|
227
|
+
while i < raw.length
|
|
228
|
+
i += 1 while raw[i] =~ /\s/
|
|
229
|
+
break if i >= raw.length
|
|
230
|
+
|
|
231
|
+
# 属性名
|
|
232
|
+
nstart = i
|
|
233
|
+
i += 1 while raw[i] && raw[i] !~ /[\s=\/]/
|
|
234
|
+
aname = raw[nstart...i]
|
|
235
|
+
|
|
236
|
+
# 跳过空格
|
|
237
|
+
i += 1 while raw[i] =~ /\s/
|
|
238
|
+
|
|
239
|
+
if raw[i] == '='
|
|
240
|
+
i += 1
|
|
241
|
+
i += 1 while raw[i] =~ /\s/
|
|
242
|
+
quote = raw[i]
|
|
243
|
+
if quote == '"' || quote == "'"
|
|
244
|
+
i += 1
|
|
245
|
+
vend = raw.index(quote, i)
|
|
246
|
+
attrs[aname] = unescape(raw[i...vend])
|
|
247
|
+
i = vend + 1
|
|
248
|
+
else
|
|
249
|
+
vstart = i
|
|
250
|
+
i += 1 while raw[i] && raw[i] !~ /[\s\/]/
|
|
251
|
+
attrs[aname] = raw[vstart...i]
|
|
252
|
+
end
|
|
253
|
+
else
|
|
254
|
+
attrs[aname] = nil
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
[name, attrs]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
#---------------------------------------------------------------------------
|
|
262
|
+
# 实体反转义
|
|
263
|
+
#---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
ENTITY_MAP = {
|
|
266
|
+
'amp' => '&',
|
|
267
|
+
'lt' => '<',
|
|
268
|
+
'gt' => '>',
|
|
269
|
+
'quot' => '"',
|
|
270
|
+
'apos' => "'"
|
|
271
|
+
}.freeze
|
|
272
|
+
|
|
273
|
+
def emit_text(text)
|
|
274
|
+
decoded = unescape(text)
|
|
275
|
+
@handler.characters(decoded) if @handler.respond_to?(:characters)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def unescape(str)
|
|
279
|
+
return '' if str.nil? || str.empty?
|
|
280
|
+
str = str.dup.force_encoding('UTF-8')
|
|
281
|
+
str.gsub!(/&(amp|lt|gt|quot|apos|#\d+|#x[0-9A-Fa-f]+);/) do
|
|
282
|
+
m = $1
|
|
283
|
+
if m.start_with?('#x')
|
|
284
|
+
[m[2..].to_i(16)].pack('U')
|
|
285
|
+
elsif m.start_with?('#')
|
|
286
|
+
[m[1..].to_i].pack('U')
|
|
287
|
+
else
|
|
288
|
+
ENTITY_MAP[m] || "&#{m};"
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
str
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|