-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgzip.ts
More file actions
210 lines (194 loc) · 6.21 KB
/
gzip.ts
File metadata and controls
210 lines (194 loc) · 6.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
/* oxlint-disable socket/sort-source-methods -- functions ordered by call graph (compress/decompress variants share helpers); type / const declarations between them block autofix. */
/**
* @fileoverview Gzip compression / decompression — same calling shapes
* as brotli: in-memory, file-to-file, and raw-stream variants. Default
* level is 6 (zlib default). The decompress-file helper recognises
* `.gz` / `.gzip` / `.tgz`, and special-cases `.tgz` back to `.tar` on
* inPlace decompress so a round-trip stays lossless.
*
* await compressGzip(JSON.stringify(payload))
* await compressGzipFile('input.json', 'input.json.gz')
* readable.pipe(createGzipCompressor()).pipe(writable)
*/
import { Buffer } from 'node:buffer'
import { createReadStream, createWriteStream } from 'node:fs'
import path from 'node:path'
import { pipeline } from 'node:stream/promises'
import { promisify } from 'node:util'
import {
createGunzip,
createGzip,
gunzip,
gzip,
type ZlibOptions,
} from 'node:zlib'
import { safeDelete } from '../fs/safe'
import { ErrorCtor } from '../primordials/error'
import { StringPrototypeToLowerCase } from '../primordials/string'
import { resolveFileArgs, stripExt } from './_internal'
import type { CompressFileOptions, CompressOptions } from './types'
const gzipAsync = promisify(gzip)
const gunzipAsync = promisify(gunzip)
/**
* Translate `CompressOptions` into the `ZlibOptions` zlib expects.
* Returns an empty options object when no `level` is given (zlib uses
* its default, level 6). Exposed for parity with
* `resolveBrotliOptions` and for unit-test coverage.
*/
export function resolveGzipOptions(
options: CompressOptions | undefined,
): ZlibOptions {
const level = options?.level
if (level === undefined) {
return { __proto__: null } as unknown as ZlibOptions
}
return { __proto__: null, level } as unknown as ZlibOptions
}
/**
* Compress a string or Buffer with gzip. Strings are encoded as UTF-8
* before compression. Default level is 6 (zlib default).
*/
export async function compressGzip(
input: string | Buffer,
options?: CompressOptions | undefined,
): Promise<Buffer> {
const buf = typeof input === 'string' ? Buffer.from(input, 'utf8') : input
return await gzipAsync(buf, resolveGzipOptions(options))
}
/**
* Decompress a gzip-compressed Buffer.
*/
export async function decompressGzip(input: Buffer): Promise<Buffer> {
return await gunzipAsync(input)
}
/**
* Stream-compress a file with gzip. Two call shapes:
*
* compressGzipFile(src, dest, options?)
* Writes compressed output to `dest`. Source is left intact.
*
* compressGzipFile(src, { inPlace: true, ...options })
* Writes to `<src>.gz` and deletes `src` after the write
* succeeds. Returns the new path.
*/
export async function compressGzipFile(
srcPath: string,
destPath: string,
options?: CompressOptions | undefined,
): Promise<string>
export async function compressGzipFile(
srcPath: string,
options: CompressFileOptions,
): Promise<string>
export async function compressGzipFile(
srcPath: string,
destOrOptions?: string | CompressFileOptions,
maybeOptions?: CompressOptions | undefined,
): Promise<string> {
const { destPath, options, inPlace } = resolveFileArgs(
'compressGzipFile',
srcPath,
destOrOptions,
maybeOptions,
p => `${p}.gz`,
)
await pipeline(
createReadStream(srcPath),
createGzip(resolveGzipOptions(options)),
createWriteStream(destPath),
)
if (inPlace) {
await safeDelete(srcPath)
}
return destPath
}
/**
* Stream-decompress a gzip file. Two call shapes:
*
* decompressGzipFile(src, dest)
* Writes decompressed output to `dest`. Source is left intact.
*
* decompressGzipFile(src, { inPlace: true })
* Strips the `.gz`/`.gzip`/`.tgz` suffix to derive the
* destination, then deletes the compressed source after the
* write succeeds. Throws if `src` has no recognizable extension.
*/
export async function decompressGzipFile(
srcPath: string,
destPath: string,
): Promise<string>
export async function decompressGzipFile(
srcPath: string,
options: CompressFileOptions,
): Promise<string>
export async function decompressGzipFile(
srcPath: string,
destOrOptions?: string | CompressFileOptions,
): Promise<string> {
const { destPath, inPlace } = resolveFileArgs(
'decompressGzipFile',
srcPath,
destOrOptions,
undefined,
p => {
if (!hasGzipExt(p)) {
throw new ErrorCtor(
`decompressGzipFile: ${p} has no .gz/.gzip/.tgz extension; can't infer destination`,
)
}
// .tgz is conventionally .tar.gz collapsed — recover the .tar so
// a round-trip through compress/decompress is lossless.
const stripped = stripExt(p, GZIP_EXTS)
return StringPrototypeToLowerCase(path.extname(p)) === '.tgz'
? `${stripped}.tar`
: stripped
},
)
await pipeline(
createReadStream(srcPath),
createGunzip(),
createWriteStream(destPath),
)
if (inPlace) {
await safeDelete(srcPath)
}
return destPath
}
/**
* Create a gzip compress transform stream.
*/
export function createGzipCompressor(options?: CompressOptions | undefined) {
return createGzip(resolveGzipOptions(options))
}
/**
* Create a gzip decompress transform stream.
*/
export function createGzipDecompressor() {
return createGunzip()
}
// Gzip has a real magic-byte signature: 0x1f 0x8b.
const GZIP_MAGIC_0 = 0x1f
const GZIP_MAGIC_1 = 0x8b
/**
* Magic-byte check for gzip. Reads the first two bytes and matches
* the gzip spec's 0x1f 0x8b signature. Authoritative.
*/
export function isGzipCompressed(input: Buffer): boolean {
return (
Buffer.isBuffer(input) &&
input.byteLength >= 2 &&
input[0] === GZIP_MAGIC_0 &&
input[1] === GZIP_MAGIC_1
)
}
// Exported so callers can introspect what counts as a "gzip"
// extension without re-implementing the list, and so tests can pin
// the recognized set.
export const GZIP_EXTS: ReadonlySet<string> = new Set(['.gz', '.gzip', '.tgz'])
/**
* Extension check for gzip paths — matches `.gz` / `.gzip` / `.tgz`
* (case-insensitive). Naming follows node:path's `extname`.
*/
export function hasGzipExt(filePath: string): boolean {
return GZIP_EXTS.has(StringPrototypeToLowerCase(path.extname(filePath)))
}