node-liblzma 1.1.9 → 2.0.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.
- package/.claude/settings.local.json +92 -0
- package/.gitattributes +3 -0
- package/.release-it.json +6 -0
- package/CHANGELOG.md +209 -0
- package/History.md +20 -0
- package/README.md +750 -30
- package/RELEASING.md +131 -0
- package/binding.gyp +159 -438
- package/biome.json +81 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/errors.ts.html +586 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +146 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/errors.ts.html +586 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +146 -0
- package/coverage/lcov-report/lzma.ts.html +2596 -0
- package/coverage/lcov-report/pool.ts.html +769 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +636 -0
- package/coverage/lzma.ts.html +2596 -0
- package/coverage/pool.ts.html +769 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage-reports/assets/monocart-coverage-app.js +2 -0
- package/coverage-reports/coverage-data.js +1 -0
- package/coverage-reports/index.html +48 -0
- package/err.log +26 -0
- package/index.d.ts +254 -0
- package/lib/errors.d.ts +72 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +153 -0
- package/lib/errors.js.map +1 -0
- package/lib/lzma.d.ts +245 -0
- package/lib/lzma.d.ts.map +1 -0
- package/lib/lzma.js +626 -345
- package/lib/lzma.js.map +1 -0
- package/lib/pool.d.ts +123 -0
- package/lib/pool.d.ts.map +1 -0
- package/lib/pool.js +188 -0
- package/lib/pool.js.map +1 -0
- package/lib/types.d.ts +27 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +5 -0
- package/lib/types.js.map +1 -0
- package/package.json +60 -21
- package/pnpm-workspace.yaml +3 -0
- package/scripts/analyze-coverage.js +132 -0
- package/scripts/build_xz_with_cmake.py +390 -0
- package/scripts/compare-coverage-tools.js +93 -0
- package/scripts/copy_dll.py +51 -0
- package/scripts/download_xz_from_github.py +375 -0
- package/src/bindings/node-liblzma.cpp +411 -229
- package/src/bindings/node-liblzma.hpp +101 -48
- package/src/errors.ts +167 -0
- package/src/lzma.ts +839 -0
- package/src/pool.ts +228 -0
- package/src/types.ts +30 -0
- package/tsconfig.json +50 -0
- package/vitest.config.istanbul.ts +29 -0
- package/vitest.config.monocart.ts +44 -0
- package/vitest.config.ts +44 -0
- package/xz-version.json +8 -0
- package/prebuilds/darwin-x64/node.napi.node +0 -0
- package/prebuilds/linux-x64/node.napi.node +0 -0
- package/prebuilds/win32-x64/node.napi.node +0 -0
- package/scripts/download_extract_deps.py +0 -29
- package/src/lzma.coffee +0 -344
|
@@ -0,0 +1,375 @@
|
|
|
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
|
+
# Validate version exists
|
|
316
|
+
validated_version = validate_version(version)
|
|
317
|
+
if not validated_version:
|
|
318
|
+
print(f"[ERROR] Version {version} not found, falling back to v5.4.0")
|
|
319
|
+
validated_version = 'v5.4.0'
|
|
320
|
+
|
|
321
|
+
# Prepare and validate paths
|
|
322
|
+
tarball = os.path.abspath(args.tarball)
|
|
323
|
+
dirname = os.path.abspath(args.dirname)
|
|
324
|
+
|
|
325
|
+
# Additional security validation for output paths
|
|
326
|
+
if not tarball or not dirname:
|
|
327
|
+
print("[ERROR] Invalid paths provided")
|
|
328
|
+
return 1
|
|
329
|
+
|
|
330
|
+
# Ensure paths don't contain suspicious patterns
|
|
331
|
+
suspicious_patterns = ['..', '~', '$']
|
|
332
|
+
for pattern in suspicious_patterns:
|
|
333
|
+
if pattern in args.tarball or pattern in args.dirname:
|
|
334
|
+
print(f"[ERROR] Suspicious pattern '{pattern}' detected in paths")
|
|
335
|
+
return 1
|
|
336
|
+
|
|
337
|
+
# Ensure we're using .tar.gz extension (GitHub uses gzip, not xz)
|
|
338
|
+
if tarball.endswith('.tar.xz'):
|
|
339
|
+
tarball = tarball.replace('.tar.xz', '.tar.gz')
|
|
340
|
+
print(f"[NOTE] Adjusted tarball name to: {tarball}")
|
|
341
|
+
|
|
342
|
+
# Create directories if needed
|
|
343
|
+
os.makedirs(os.path.dirname(tarball), exist_ok=True)
|
|
344
|
+
os.makedirs(dirname, exist_ok=True)
|
|
345
|
+
|
|
346
|
+
# Check if already extracted with correct version (smart cache)
|
|
347
|
+
if is_xz_already_extracted(dirname, validated_version):
|
|
348
|
+
print(f"[SKIP] XZ {validated_version} already available, skipping download")
|
|
349
|
+
return 0
|
|
350
|
+
|
|
351
|
+
# Download if not cached
|
|
352
|
+
if os.path.exists(tarball):
|
|
353
|
+
print(f"[CACHED] Using cached tarball: {tarball}")
|
|
354
|
+
else:
|
|
355
|
+
url = get_tarball_url(validated_version)
|
|
356
|
+
download_tarball(url, tarball)
|
|
357
|
+
|
|
358
|
+
# Extract
|
|
359
|
+
extract_tarball(tarball, dirname)
|
|
360
|
+
|
|
361
|
+
# Write version marker for future cache validation
|
|
362
|
+
write_version_marker(dirname, validated_version)
|
|
363
|
+
|
|
364
|
+
print(f"[DONE] Successfully prepared XZ {validated_version}")
|
|
365
|
+
return 0
|
|
366
|
+
|
|
367
|
+
if __name__ == '__main__':
|
|
368
|
+
try:
|
|
369
|
+
sys.exit(main())
|
|
370
|
+
except KeyboardInterrupt:
|
|
371
|
+
print("\\n[ERROR] Interrupted by user")
|
|
372
|
+
sys.exit(1)
|
|
373
|
+
except Exception as e:
|
|
374
|
+
print(f"[ERROR] Error: {e}")
|
|
375
|
+
sys.exit(1)
|