whistle.mockbubu 1.0.0-dev.4 → 2.0.0-beta.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.
@@ -0,0 +1,332 @@
1
+ /**
2
+ * 压缩包工具函数
3
+ * 使用 Node.js 内置 API,无需额外依赖
4
+ */
5
+ const fs = require('fs').promises
6
+ const path = require('path')
7
+ const zlib = require('zlib')
8
+ const { createReadStream, createWriteStream } = require('fs')
9
+ const { pipeline } = require('stream/promises')
10
+ const os = require('os')
11
+
12
+ /**
13
+ * 递归复制目录
14
+ */
15
+ async function copyDirectory(src, dest) {
16
+ await fs.mkdir(dest, { recursive: true })
17
+ const entries = await fs.readdir(src, { withFileTypes: true })
18
+
19
+ for (const entry of entries) {
20
+ const srcPath = path.join(src, entry.name)
21
+ const destPath = path.join(dest, entry.name)
22
+
23
+ if (entry.isDirectory()) {
24
+ await copyDirectory(srcPath, destPath)
25
+ } else {
26
+ await fs.copyFile(srcPath, destPath)
27
+ }
28
+ }
29
+ }
30
+
31
+ /**
32
+ * 递归读取目录,返回所有文件路径
33
+ */
34
+ async function getAllFiles(dir, baseDir = dir) {
35
+ const files = []
36
+ const entries = await fs.readdir(dir, { withFileTypes: true })
37
+
38
+ for (const entry of entries) {
39
+ const fullPath = path.join(dir, entry.name)
40
+ if (entry.isDirectory()) {
41
+ files.push(...await getAllFiles(fullPath, baseDir))
42
+ } else {
43
+ files.push({
44
+ path: fullPath,
45
+ name: path.relative(baseDir, fullPath),
46
+ })
47
+ }
48
+ }
49
+
50
+ return files
51
+ }
52
+
53
+ /**
54
+ * 创建 tar.gz 压缩包
55
+ * 使用 Node.js 内置 zlib + tar 格式手动实现
56
+ */
57
+ async function createTarGz(sourceDir, outputFile) {
58
+ // 获取所有文件
59
+ const files = await getAllFiles(sourceDir)
60
+
61
+ // 创建 tar 文件
62
+ const tarPath = outputFile.replace(/\.gz$/, '')
63
+ await createTar(sourceDir, tarPath, files)
64
+
65
+ // gzip 压缩
66
+ await new Promise((resolve, reject) => {
67
+ const input = createReadStream(tarPath)
68
+ const output = createWriteStream(outputFile)
69
+ const gzip = zlib.createGzip()
70
+
71
+ pipeline(input, gzip, output)
72
+ .then(() => {
73
+ // 删除临时 tar 文件
74
+ fs.unlink(tarPath).catch(() => {})
75
+ resolve()
76
+ })
77
+ .catch(reject)
78
+ })
79
+ }
80
+
81
+ /**
82
+ * 创建 tar 文件(简化实现,支持基本的 tar 格式)
83
+ */
84
+ async function createTar(sourceDir, outputFile, files) {
85
+ const output = createWriteStream(outputFile)
86
+
87
+ for (const file of files) {
88
+ const stat = await fs.stat(file.path)
89
+ const content = await fs.readFile(file.path)
90
+
91
+ // 写入 tar header (512 bytes)
92
+ const header = createTarHeader(file.name, stat.size)
93
+ output.write(header)
94
+
95
+ // 写入文件内容
96
+ output.write(content)
97
+
98
+ // 对齐到 512 字节边界
99
+ const padding = 512 - (content.length % 512)
100
+ if (padding < 512) {
101
+ output.write(Buffer.alloc(padding, 0))
102
+ }
103
+ }
104
+
105
+ // 写入结束标记(两个 512 字节的零块)
106
+ output.write(Buffer.alloc(1024, 0))
107
+ output.end()
108
+
109
+ return new Promise((resolve, reject) => {
110
+ output.on('finish', resolve)
111
+ output.on('error', reject)
112
+ })
113
+ }
114
+
115
+ /**
116
+ * 创建 tar header (POSIX ustar 格式)
117
+ */
118
+ function createTarHeader(filename, size) {
119
+ const header = Buffer.alloc(512, 0)
120
+
121
+ // 文件名 (100 bytes)
122
+ header.write(filename, 0, 100, 'utf8')
123
+
124
+ // 文件模式 (8 bytes) - 0644
125
+ header.write('0000644 ', 100, 8, 'utf8')
126
+
127
+ // UID (8 bytes)
128
+ header.write('0000000 ', 108, 8, 'utf8')
129
+
130
+ // GID (8 bytes)
131
+ header.write('0000000 ', 116, 8, 'utf8')
132
+
133
+ // 文件大小 (12 bytes, 8进制)
134
+ const sizeOctal = size.toString(8).padStart(11, '0') + ' '
135
+ header.write(sizeOctal, 124, 12, 'utf8')
136
+
137
+ // 修改时间 (12 bytes, 8进制)
138
+ const mtime = Math.floor(Date.now() / 1000).toString(8).padStart(11, '0') + ' '
139
+ header.write(mtime, 136, 12, 'utf8')
140
+
141
+ // Checksum (8 bytes) - 先填充空格
142
+ header.write(' ', 148, 8, 'utf8')
143
+
144
+ // 文件类型 (1 byte) - '0' = 普通文件
145
+ header.write('0', 156, 1, 'utf8')
146
+
147
+ // ustar 标识 (6 bytes)
148
+ header.write('ustar', 257, 6, 'utf8')
149
+
150
+ // ustar 版本 (2 bytes)
151
+ header.write('00', 263, 2, 'utf8')
152
+
153
+ // 计算 checksum
154
+ let checksum = 0
155
+ for (let i = 0; i < 512; i++) {
156
+ checksum += header[i]
157
+ }
158
+ const checksumOctal = checksum.toString(8).padStart(6, '0') + '\0 '
159
+ header.write(checksumOctal, 148, 8, 'utf8')
160
+
161
+ return header
162
+ }
163
+
164
+ /**
165
+ * 解压 tar.gz 文件
166
+ */
167
+ async function extractTarGz(archiveFile, destDir) {
168
+ // 先 gunzip 解压
169
+ const tarPath = path.join(os.tmpdir(), `extract-${Date.now()}.tar`)
170
+
171
+ await new Promise((resolve, reject) => {
172
+ const input = createReadStream(archiveFile)
173
+ const output = createWriteStream(tarPath)
174
+ const gunzip = zlib.createGunzip()
175
+
176
+ pipeline(input, gunzip, output)
177
+ .then(resolve)
178
+ .catch(reject)
179
+ })
180
+
181
+ // 解压 tar
182
+ await extractTar(tarPath, destDir)
183
+
184
+ // 删除临时 tar 文件
185
+ await fs.unlink(tarPath).catch(() => {})
186
+ }
187
+
188
+ /**
189
+ * 解压 tar 文件
190
+ */
191
+ async function extractTar(tarFile, destDir) {
192
+ const buffer = await fs.readFile(tarFile)
193
+ let offset = 0
194
+
195
+ while (offset < buffer.length) {
196
+ // 检查是否到达结束标记
197
+ if (buffer[offset] === 0) {
198
+ break
199
+ }
200
+
201
+ // 读取 header
202
+ const header = buffer.subarray(offset, offset + 512)
203
+
204
+ // 读取文件名
205
+ const filename = header.subarray(0, 100).toString('utf8').replace(/\0.*$/, '')
206
+ if (!filename) break
207
+
208
+ // 读取文件大小
209
+ const sizeStr = header.subarray(124, 136).toString('utf8').trim()
210
+ const size = parseInt(sizeStr, 8)
211
+
212
+ // 跳过 header
213
+ offset += 512
214
+
215
+ // 读取文件内容
216
+ const content = buffer.subarray(offset, offset + size)
217
+
218
+ // 写入文件
219
+ const filepath = path.join(destDir, filename)
220
+ await fs.mkdir(path.dirname(filepath), { recursive: true })
221
+ await fs.writeFile(filepath, content)
222
+
223
+ // 移动到下一个文件(对齐到 512 字节)
224
+ const padding = 512 - (size % 512)
225
+ offset += size + (padding < 512 ? padding : 0)
226
+ }
227
+ }
228
+
229
+ /**
230
+ * 文件名安全处理(移除特殊字符)
231
+ */
232
+ function sanitizeFilename(filename) {
233
+ // eslint-disable-next-line no-control-regex
234
+ return filename.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_')
235
+ }
236
+
237
+ /**
238
+ * 检查路径是否在指定目录内(防止路径遍历攻击)
239
+ */
240
+ function isPathSafe(filepath, baseDir) {
241
+ const normalized = path.normalize(filepath)
242
+ const relative = path.relative(baseDir, normalized)
243
+ return !relative.startsWith('..') && !path.isAbsolute(relative)
244
+ }
245
+
246
+ /**
247
+ * 解析 multipart/form-data 文件上传(原生实现)
248
+ */
249
+ async function parseMultipartFile(ctx) {
250
+ return new Promise((resolve, reject) => {
251
+ const contentType = ctx.request.headers['content-type']
252
+ if (!contentType || !contentType.includes('multipart/form-data')) {
253
+ reject(new Error('不是 multipart/form-data 格式'))
254
+ return
255
+ }
256
+
257
+ // 提取 boundary
258
+ const boundaryMatch = contentType.match(/boundary=(.+)/)
259
+ if (!boundaryMatch) {
260
+ reject(new Error('缺少 boundary'))
261
+ return
262
+ }
263
+ const boundary = '--' + boundaryMatch[1]
264
+
265
+ const chunks = []
266
+ ctx.req.on('data', chunk => chunks.push(chunk))
267
+ ctx.req.on('end', () => {
268
+ try {
269
+ const buffer = Buffer.concat(chunks)
270
+ const file = parseMultipartBuffer(buffer, boundary)
271
+ resolve(file)
272
+ } catch (err) {
273
+ reject(err)
274
+ }
275
+ })
276
+ ctx.req.on('error', reject)
277
+ })
278
+ }
279
+
280
+ /**
281
+ * 解析 multipart buffer
282
+ */
283
+ function parseMultipartBuffer(buffer, boundary) {
284
+ const parts = []
285
+ let start = 0
286
+
287
+ while (start < buffer.length) {
288
+ // 查找 boundary
289
+ const boundaryIndex = buffer.indexOf(boundary, start)
290
+ if (boundaryIndex === -1) break
291
+
292
+ // 查找下一个 boundary
293
+ const nextBoundaryIndex = buffer.indexOf(boundary, boundaryIndex + boundary.length)
294
+ if (nextBoundaryIndex === -1) break
295
+
296
+ // 提取部分内容
297
+ const partBuffer = buffer.subarray(boundaryIndex + boundary.length, nextBoundaryIndex)
298
+
299
+ // 查找 headers 和 body 的分隔符(\r\n\r\n)
300
+ const headerEndIndex = partBuffer.indexOf('\r\n\r\n')
301
+ if (headerEndIndex !== -1) {
302
+ const headers = partBuffer.subarray(0, headerEndIndex).toString('utf8')
303
+ const body = partBuffer.subarray(headerEndIndex + 4, partBuffer.length - 2) // 去掉结尾的 \r\n
304
+
305
+ // 解析 Content-Disposition
306
+ const filenameMatch = headers.match(/filename="(.+?)"/)
307
+ const nameMatch = headers.match(/name="(.+?)"/)
308
+
309
+ if (filenameMatch) {
310
+ parts.push({
311
+ fieldname: nameMatch ? nameMatch[1] : 'file',
312
+ filename: filenameMatch[1],
313
+ buffer: body,
314
+ })
315
+ }
316
+ }
317
+
318
+ start = nextBoundaryIndex
319
+ }
320
+
321
+ return parts[0] // 返回第一个文件
322
+ }
323
+
324
+ module.exports = {
325
+ copyDirectory,
326
+ getAllFiles,
327
+ createTarGz,
328
+ extractTarGz,
329
+ sanitizeFilename,
330
+ isPathSafe,
331
+ parseMultipartFile,
332
+ }
package/lib/const.js CHANGED
@@ -26,3 +26,22 @@ exports.LockedFilterMap = {
26
26
  'locked': { key: 'locked', value: true, method: 'equal' },
27
27
  'unlocked': { key: 'locked', value: false, method: 'equal' },
28
28
  }
29
+
30
+ // HTTP Method 筛选映射
31
+ exports.MethodFilterMap = {
32
+ 'GET': { key: 'method', value: 'GET', method: 'equal' },
33
+ 'POST': { key: 'method', value: 'POST', method: 'equal' },
34
+ 'PUT': { key: 'method', value: 'PUT', method: 'equal' },
35
+ 'DELETE': { key: 'method', value: 'DELETE', method: 'equal' },
36
+ 'PATCH': { key: 'method', value: 'PATCH', method: 'equal' },
37
+ 'HEAD': { key: 'method', value: 'HEAD', method: 'equal' },
38
+ 'OPTIONS': { key: 'method', value: 'OPTIONS', method: 'equal' },
39
+ }
40
+
41
+ // 状态码筛选映射(使用自定义 method: 'statusRange')
42
+ exports.StatusFilterMap = {
43
+ '2xx': { key: 'status', value: [200, 299], method: 'range' },
44
+ '3xx': { key: 'status', value: [300, 399], method: 'range' },
45
+ '4xx': { key: 'status', value: [400, 499], method: 'range' },
46
+ '5xx': { key: 'status', value: [500, 599], method: 'range' },
47
+ }