node-liblzma 1.1.9 → 2.0.3

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.
Files changed (54) hide show
  1. package/.gitattributes +3 -0
  2. package/.release-it.json +7 -0
  3. package/.release-it.manual.json +7 -0
  4. package/.release-it.retry.json +3 -0
  5. package/CHANGELOG.md +271 -0
  6. package/History.md +20 -0
  7. package/README.md +750 -30
  8. package/RELEASING.md +131 -0
  9. package/binding.gyp +162 -436
  10. package/biome.json +81 -0
  11. package/index.d.ts +254 -0
  12. package/lib/errors.d.ts +72 -0
  13. package/lib/errors.d.ts.map +1 -0
  14. package/lib/errors.js +153 -0
  15. package/lib/errors.js.map +1 -0
  16. package/lib/lzma.d.ts +245 -0
  17. package/lib/lzma.d.ts.map +1 -0
  18. package/lib/lzma.js +626 -345
  19. package/lib/lzma.js.map +1 -0
  20. package/lib/pool.d.ts +123 -0
  21. package/lib/pool.d.ts.map +1 -0
  22. package/lib/pool.js +188 -0
  23. package/lib/pool.js.map +1 -0
  24. package/lib/types.d.ts +27 -0
  25. package/lib/types.d.ts.map +1 -0
  26. package/lib/types.js +5 -0
  27. package/lib/types.js.map +1 -0
  28. package/package.json +61 -22
  29. package/pnpm-workspace.yaml +3 -0
  30. package/prebuilds/darwin-x64/node-liblzma.node +0 -0
  31. package/prebuilds/linux-x64/node-liblzma.node +0 -0
  32. package/prebuilds/win32-x64/node-liblzma.node +0 -0
  33. package/scripts/analyze-coverage.js +132 -0
  34. package/scripts/build_xz_with_cmake.py +390 -0
  35. package/scripts/compare-coverage-tools.js +93 -0
  36. package/scripts/copy_dll.py +51 -0
  37. package/scripts/download_xz_from_github.py +376 -0
  38. package/src/bindings/node-liblzma.cpp +411 -229
  39. package/src/bindings/node-liblzma.hpp +101 -48
  40. package/src/errors.ts +167 -0
  41. package/src/lzma.ts +839 -0
  42. package/src/pool.ts +228 -0
  43. package/src/types.ts +30 -0
  44. package/tsconfig.json +50 -0
  45. package/vitest.config.istanbul.ts +29 -0
  46. package/vitest.config.monocart.ts +44 -0
  47. package/vitest.config.ts +52 -0
  48. package/xz-version.json +8 -0
  49. package/prebuilds/darwin-x64/node.napi.node +0 -0
  50. package/prebuilds/linux-x64/node.napi.node +0 -0
  51. package/prebuilds/win32-x64/node.napi.node +0 -0
  52. package/scripts/download_extract_deps.py +0 -29
  53. package/scripts/prebuildify.py +0 -13
  54. package/src/lzma.coffee +0 -344
@@ -0,0 +1,376 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Download and extract XZ Utils from GitHub with intelligent version management.
4
+
5
+ Version priority:
6
+ 1. XZ_VERSION environment variable (highest priority - for CI/CD overrides)
7
+ 2. xz-version.json configuration file (stable default)
8
+ 3. Fallback to v5.4.0 if no config found
9
+
10
+ Features:
11
+ - Smart caching: Skip download if correct version already extracted
12
+ - GitHub API authentication: Use GITHUB_TOKEN to avoid rate limiting (60/h → 5000/h)
13
+ - Security: Path traversal protection and safe tarball extraction
14
+
15
+ Usage:
16
+ python3 download_xz_from_github.py <tarball_path> <extract_dir>
17
+
18
+ Environment variables:
19
+ XZ_VERSION: Specific version (e.g., 'v5.8.1', 'latest')
20
+ GITHUB_TOKEN: GitHub token for authenticated API requests (optional, increases rate limit)
21
+ """
22
+
23
+ import urllib.request
24
+ import json
25
+ import sys
26
+ import tarfile
27
+ import os
28
+ import argparse
29
+ from datetime import datetime
30
+ from pathlib import Path
31
+ import tempfile
32
+
33
+ def get_github_headers():
34
+ """Get headers with optional GitHub token for authentication"""
35
+ headers = {'User-Agent': 'node-liblzma'}
36
+
37
+ # Use GITHUB_TOKEN if available (in CI) to avoid rate limiting
38
+ # Increases limit from 60/hour to 5000/hour
39
+ token = os.environ.get('GITHUB_TOKEN', '').strip()
40
+ if token:
41
+ headers['Authorization'] = f'token {token}'
42
+ print("[AUTH] Using GitHub token for authenticated requests")
43
+
44
+ return headers
45
+
46
+ def load_version_config():
47
+ """Load version configuration from xz-version.json"""
48
+ script_dir = os.path.dirname(os.path.abspath(__file__))
49
+ config_path = os.path.join(script_dir, '..', 'xz-version.json')
50
+
51
+ if os.path.exists(config_path):
52
+ try:
53
+ with open(config_path, 'r') as f:
54
+ config = json.load(f)
55
+ print(f"Loaded XZ config: {config.get('version', 'unknown')} ({config.get('comment', 'no comment')})")
56
+ return config
57
+ except (json.JSONDecodeError, IOError) as e:
58
+ print(f"Warning: Could not read xz-version.json: {e}")
59
+
60
+ # Fallback configuration
61
+ return {
62
+ 'version': 'v5.4.0',
63
+ 'comment': 'Fallback stable version',
64
+ 'allow_override': True
65
+ }
66
+
67
+ def get_latest_version():
68
+ """Get the latest XZ version from GitHub API"""
69
+ api_url = "https://api.github.com/repos/tukaani-project/xz/releases/latest"
70
+ headers = get_github_headers()
71
+ req = urllib.request.Request(api_url, headers=headers)
72
+
73
+ try:
74
+ with urllib.request.urlopen(req) as response:
75
+ data = json.loads(response.read())
76
+ return data['tag_name']
77
+ except Exception as e:
78
+ print(f"Warning: Could not fetch latest version: {e}")
79
+ return 'v5.8.1' # Safe fallback
80
+
81
+ def determine_version():
82
+ """Determine which XZ version to use based on priority hierarchy"""
83
+ # 1. Environment variable has highest priority (CI/CD overrides)
84
+ env_version = os.environ.get('XZ_VERSION', '').strip()
85
+ if env_version:
86
+ if env_version.lower() == 'latest':
87
+ version = get_latest_version()
88
+ print(f"[LAUNCH] Using latest XZ version: {version}")
89
+ return version
90
+ else:
91
+ print(f"[TARGET] Using XZ version from environment: {env_version}")
92
+ return env_version
93
+
94
+ # 2. Repository configuration file
95
+ config = load_version_config()
96
+ configured_version = config.get('version', 'v5.4.0')
97
+ print(f"[CONFIG] Using configured XZ version: {configured_version}")
98
+ return configured_version
99
+
100
+ def validate_version(version):
101
+ """Validate that the version exists on GitHub"""
102
+ if not version.startswith('v'):
103
+ version = 'v' + version
104
+
105
+ # Check if version exists
106
+ api_url = f"https://api.github.com/repos/tukaani-project/xz/releases/tags/{version}"
107
+ headers = get_github_headers()
108
+ req = urllib.request.Request(api_url, headers=headers)
109
+
110
+ try:
111
+ with urllib.request.urlopen(req) as response:
112
+ return version
113
+ except urllib.error.HTTPError as e:
114
+ if e.code == 404:
115
+ print(f"Warning: Version {version} not found on GitHub")
116
+ return None
117
+ raise
118
+
119
+ def get_tarball_url(version):
120
+ """Get the tarball URL for a specific version"""
121
+ return f'https://api.github.com/repos/tukaani-project/xz/tarball/{version}'
122
+
123
+ def download_tarball(url, tarball_path):
124
+ """Download tarball from GitHub with proper user agent"""
125
+ print(f"[DOWNLOAD] Downloading from: {url}")
126
+ headers = get_github_headers()
127
+ req = urllib.request.Request(url, headers=headers)
128
+
129
+ with urllib.request.urlopen(req) as response:
130
+ # GitHub redirects to the actual download URL
131
+ final_url = response.geturl()
132
+ print(f"[PACKAGE] Resolved to: {final_url}")
133
+
134
+ with urllib.request.urlopen(final_url) as final_response:
135
+ with open(tarball_path, 'wb') as f:
136
+ data = final_response.read()
137
+ f.write(data)
138
+ print(f"[SUCCESS] Downloaded {len(data)} bytes to {tarball_path}")
139
+
140
+ def is_safe_path(member_path, extract_dir):
141
+ """Validate that the extraction path is safe and within bounds."""
142
+ # Reject obviously dangerous patterns first
143
+ if not member_path or member_path.startswith('/') or member_path.startswith('\\'):
144
+ return False
145
+
146
+ # Normalize path separators and check for traversal patterns
147
+ normalized_path = member_path.replace('\\', '/')
148
+ if '..' in normalized_path:
149
+ return False
150
+
151
+ # Check each path component
152
+ path_parts = Path(normalized_path).parts
153
+ for part in path_parts:
154
+ # Reject dangerous components
155
+ if part in ('..', '.', '') or part.startswith('..'):
156
+ return False
157
+ # Reject absolute path indicators
158
+ if os.path.isabs(part) or ':' in part:
159
+ return False
160
+ # Reject null bytes and other control characters
161
+ if '\x00' in part or any(ord(c) < 32 for c in part if c not in '\t'):
162
+ return False
163
+
164
+ # Final validation: resolve the full path and ensure it's within bounds
165
+ extract_dir = os.path.abspath(extract_dir)
166
+ try:
167
+ member_abs_path = os.path.abspath(os.path.join(extract_dir, normalized_path))
168
+ # Ensure the resolved path is within the extraction directory
169
+ common_path = os.path.commonpath([member_abs_path, extract_dir])
170
+ if common_path != extract_dir:
171
+ return False
172
+ # Double-check with string prefix (for additional safety)
173
+ if not member_abs_path.startswith(extract_dir + os.sep) and member_abs_path != extract_dir:
174
+ return False
175
+ except (ValueError, OSError):
176
+ return False
177
+
178
+ return True
179
+
180
+ def is_xz_already_extracted(extract_dir, version):
181
+ """Check if XZ is already extracted with the correct version"""
182
+ xz_dir = os.path.join(extract_dir, 'xz')
183
+ cmake_file = os.path.join(xz_dir, 'CMakeLists.txt')
184
+ version_file = os.path.join(xz_dir, '.xz-version')
185
+
186
+ # Check if XZ directory exists with CMakeLists.txt
187
+ if not os.path.exists(cmake_file):
188
+ return False
189
+
190
+ # Check if version file exists and matches
191
+ if os.path.exists(version_file):
192
+ try:
193
+ with open(version_file, 'r') as f:
194
+ cached_version = f.read().strip()
195
+ if cached_version == version:
196
+ print(f"[CACHE HIT] XZ {version} already extracted")
197
+ return True
198
+ else:
199
+ print(f"[CACHE MISS] Version mismatch: cached {cached_version} != requested {version}")
200
+ return False
201
+ except IOError:
202
+ pass
203
+
204
+ # Version file doesn't exist, assume cache miss
205
+ print(f"[CACHE MISS] No version file found")
206
+ return False
207
+
208
+ def extract_tarball(tarball_path, extract_dir):
209
+ """Extract tarball and rename root directory to 'xz' with security validation."""
210
+ print(f"[EXTRACT] Extracting to {extract_dir}/xz")
211
+
212
+ # Ensure extract_dir exists and is absolute
213
+ extract_dir = os.path.abspath(extract_dir)
214
+ os.makedirs(extract_dir, exist_ok=True)
215
+
216
+ with tarfile.open(tarball_path, 'r:gz') as tfile:
217
+ members = tfile.getmembers()
218
+ if not members:
219
+ raise ValueError("Empty tarball")
220
+
221
+ # GitHub creates directories like "tukaani-project-xz-{commit_hash}"
222
+ root_dir = members[0].name.split('/')[0]
223
+ print(f"[DIR] Root directory: {root_dir}")
224
+
225
+ # Security validation: check all members before extraction
226
+ safe_members = []
227
+ for member in members:
228
+ # Create the new path by replacing root directory with 'xz'
229
+ if not member.name.startswith(root_dir):
230
+ print(f"[SKIP] Skipping member not in root directory: {member.name}")
231
+ continue
232
+
233
+ new_name = member.name.replace(root_dir, 'xz', 1)
234
+
235
+ # Validate the new path is safe
236
+ if not is_safe_path(new_name, extract_dir):
237
+ print(f"[SECURITY] Rejecting unsafe path: {member.name} -> {new_name}")
238
+ continue
239
+
240
+ # Additional safety checks for member properties
241
+ if member.islnk() or member.issym():
242
+ # Validate link targets are also safe
243
+ if member.linkname and not is_safe_path(member.linkname, extract_dir):
244
+ print(f"[SECURITY] Rejecting unsafe link target: {member.linkname}")
245
+ continue
246
+
247
+ # Create a new member with the safe name
248
+ safe_member = member
249
+ safe_member.name = new_name
250
+ safe_members.append(safe_member)
251
+
252
+ if not safe_members:
253
+ raise ValueError("No safe members to extract")
254
+
255
+ # Extract all validated members
256
+ for member in safe_members:
257
+ try:
258
+ # Use data filter for additional safety on Python 3.12+
259
+ tfile.extract(member, extract_dir, filter='data')
260
+ except TypeError:
261
+ # Fallback for Python versions that don't support filter parameter
262
+ # Manual validation since we can't use the data filter
263
+ if member.isfile() and member.size > 100 * 1024 * 1024: # 100MB limit
264
+ print(f"[SECURITY] Skipping oversized file: {member.name} ({member.size} bytes)")
265
+ continue
266
+ tfile.extract(member, extract_dir)
267
+ except Exception as e:
268
+ print(f"[ERROR] Failed to extract {member.name}: {e}")
269
+ continue
270
+
271
+ print(f"[SUCCESS] Successfully extracted XZ to {extract_dir}/xz")
272
+
273
+ def write_version_marker(extract_dir, version):
274
+ """Write version marker file for cache validation"""
275
+ xz_dir = os.path.join(extract_dir, 'xz')
276
+ version_file = os.path.join(xz_dir, '.xz-version')
277
+
278
+ try:
279
+ with open(version_file, 'w') as f:
280
+ f.write(version)
281
+ print(f"[VERSION] Wrote version marker: {version}")
282
+ except IOError as e:
283
+ print(f"[WARNING] Could not write version marker: {e}")
284
+
285
+ def main():
286
+ parser = argparse.ArgumentParser(
287
+ description='Download XZ Utils from GitHub with intelligent version management',
288
+ epilog='''
289
+ Version priority:
290
+ 1. XZ_VERSION environment variable (e.g., XZ_VERSION=v5.8.1)
291
+ 2. xz-version.json configuration file
292
+ 3. Fallback to v5.4.0
293
+
294
+ Examples:
295
+ python3 download_xz_from_github.py deps/xz.tar.gz deps/
296
+ XZ_VERSION=latest python3 download_xz_from_github.py deps/xz.tar.gz deps/
297
+ XZ_VERSION=v5.6.4 python3 download_xz_from_github.py deps/xz.tar.gz deps/
298
+ ''',
299
+ formatter_class=argparse.RawDescriptionHelpFormatter
300
+ )
301
+
302
+ parser.add_argument('tarball', help='Output tarball path (.tar.gz will be used)')
303
+ parser.add_argument('dirname', help='Extract directory')
304
+ parser.add_argument('--verbose', '-v', action='store_true',
305
+ help='Verbose output')
306
+
307
+ args = parser.parse_args()
308
+
309
+ if args.verbose:
310
+ print("[VERBOSE] Verbose mode enabled")
311
+
312
+ # Determine version to use
313
+ version = determine_version()
314
+
315
+ # Prepare and validate paths
316
+ tarball = os.path.abspath(args.tarball)
317
+ dirname = os.path.abspath(args.dirname)
318
+
319
+ # Additional security validation for output paths
320
+ if not tarball or not dirname:
321
+ print("[ERROR] Invalid paths provided")
322
+ return 1
323
+
324
+ # Ensure paths don't contain suspicious patterns
325
+ suspicious_patterns = ['..', '~', '$']
326
+ for pattern in suspicious_patterns:
327
+ if pattern in args.tarball or pattern in args.dirname:
328
+ print(f"[ERROR] Suspicious pattern '{pattern}' detected in paths")
329
+ return 1
330
+
331
+ # Ensure we're using .tar.gz extension (GitHub uses gzip, not xz)
332
+ if tarball.endswith('.tar.xz'):
333
+ tarball = tarball.replace('.tar.xz', '.tar.gz')
334
+ print(f"[NOTE] Adjusted tarball name to: {tarball}")
335
+
336
+ # Create directories if needed
337
+ os.makedirs(os.path.dirname(tarball), exist_ok=True)
338
+ os.makedirs(dirname, exist_ok=True)
339
+
340
+ # Check if already extracted with correct version (smart cache)
341
+ # Do this BEFORE any GitHub API calls to avoid rate limiting
342
+ if is_xz_already_extracted(dirname, version):
343
+ print(f"[SKIP] XZ {version} already available, skipping download")
344
+ return 0
345
+
346
+ # Only validate version if we need to download (avoids GitHub API call when cached)
347
+ validated_version = validate_version(version)
348
+ if not validated_version:
349
+ print(f"[ERROR] Version {version} not found, falling back to v5.4.0")
350
+ validated_version = 'v5.4.0'
351
+
352
+ # Download if not cached
353
+ if os.path.exists(tarball):
354
+ print(f"[CACHED] Using cached tarball: {tarball}")
355
+ else:
356
+ url = get_tarball_url(validated_version)
357
+ download_tarball(url, tarball)
358
+
359
+ # Extract
360
+ extract_tarball(tarball, dirname)
361
+
362
+ # Write version marker for future cache validation
363
+ write_version_marker(dirname, validated_version)
364
+
365
+ print(f"[DONE] Successfully prepared XZ {validated_version}")
366
+ return 0
367
+
368
+ if __name__ == '__main__':
369
+ try:
370
+ sys.exit(main())
371
+ except KeyboardInterrupt:
372
+ print("\\n[ERROR] Interrupted by user")
373
+ sys.exit(1)
374
+ except Exception as e:
375
+ print(f"[ERROR] Error: {e}")
376
+ sys.exit(1)