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.
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