diff --git a/.changeset/wasm-memlimit-option.md b/.changeset/wasm-memlimit-option.md new file mode 100644 index 0000000..02d0d42 --- /dev/null +++ b/.changeset/wasm-memlimit-option.md @@ -0,0 +1,18 @@ +--- +'node-liblzma': minor +--- + +Add `memlimit` option to `LZMAOptions` and wire it through `unxzAsync`/`unxz` (WASM). + +Callers can now set a memory usage limit for WASM decompression: + +```ts +await unxzAsync(buf, { memlimit: 64 * 1024 * 1024 }); // 64 MiB limit +``` + +When the compressed stream would require more memory than the limit, the promise rejects with `LZMAMemoryLimitError` (`errno === LZMA_MEMLIMIT_ERROR === 6`). + +**Accepted types:** `number | bigint` (both coerced to `bigint` for the WASM C ABI). +**Default:** `BigInt(256 * 1024 * 1024)` (256 MiB — unchanged from existing behaviour). + +**Native parity:** The native Node.js binding (`InitializeDecoder`) still hardcodes `UINT64_MAX` and ignores `memlimit`. This is WASM-only for now; native tracking in TODO.md. diff --git a/TODO.md b/TODO.md index 35f02f1..b1ba981 100644 --- a/TODO.md +++ b/TODO.md @@ -10,14 +10,20 @@ _None_ ## Pending - MEDIUM -_None_ +- [ ] [tar-xz] True streaming for Node `extract()`/`list()` — replace `Buffer.concat` accumulation (extract.ts:59,91 + list.ts:26) with incremental header→content parsing so memory stays O(largest entry) instead of O(archive). Public README v6.0.0 advertises this as a "planned optimization" — Priority: M +- [ ] [Native] Wire `memlimit` in `src/bindings/node-liblzma.cpp` `InitializeDecoder` — currently hardcodes `UINT64_MAX`; should read `opts.memlimit` and call `lzma_stream_decoder(stream, memlimit, flags)`. WASM already supports it. — Priority: M +- [→] [WASM] Wire `memlimit` through `LZMAOptions` and `unxzAsync` — moved to In Progress (2026-04-28) → ✅ completed (2026-04-28) ## Pending - LOW (Nice to Have) -_None_ +- [ ] [WASM] `validateMemlimit` symmetry — bigint branch has no UINT64_MAX upper-bound guard (only the `number` branch checks `MAX_SAFE_INTEGER`). Currently benign because native side hardcodes UINT64_MAX and `lzma_stream_decoder` reads `uint64_t` (so `2n ** 65n` would silently wrap to a benign-but-unintended ceiling, no security/leak/crash). Becomes load-bearing once `[Native] Wire memlimit` lands — fix together. Priority: L (defer until native parity work). ## Completed +- [x] ✅ [WASM] PR #111 Round 3 Copilot fixes — C-3-001/2/3 duplicate JSDoc blocks removed from decoderInit/autoDecoderInit/validateMemlimit, C-3-004 stale xzAsync/unxzAsync comment fixed in lzma.ts:370; tsc+memlimit+full suite pass (2026-04-28) +- [x] ✅ [WASM] PR #111 Round 2 Copilot fixes — C-2-001 TSDoc xzAsync removed from honored-by list, C-2-002 stale lzma.ts comment, C-2-003 LZMA_OPTIONS_ERROR constant replaces magic 8, C-2-004 MAX_SAFE_INTEGER guard + TSDoc, C-2-005 validateMemlimit lifted to decoderInit+autoDecoderInit; 12 new tests, 474+99+27=600 tests pass (2026-04-28) +- [x] ✅ [WASM] PR #111 Round 1 review fixes — F-001 memlimit validation (NaN/Inf/frac/neg → LZMAOptionsError), F-002 ResolvedLZMAOptions internal type, C-001/C-002 async callback fixture pattern, C-003 byte-equality assertion, F-003 TSDoc reorder, F-004 stale comment, F-005 fixture comment magnitude, F-006 default-path caveat; 4 new tests (12 total in decompress-memlimit.test.ts), 458+99+27=584 tests pass (2026-04-28) +- [x] ✅ [WASM] Wire `memlimit` through `LZMAOptions` → `unxzAsync`/`unxz` — `LZMAMemoryLimitError` thrown when limit exceeded; 8 new tests in `test/wasm/decompress-memlimit.test.ts`; TSDoc with parity note (2026-04-28) - [x] ✅ [tar-xz v6] Universal stream-first redesign: `create()`/`extract()`/`list()` with `AsyncIterable`, identical Node/Browser signatures, `tar-xz/file` subpath for fs helpers — published as `tar-xz@6.0.0` + `nxz-cli@6.0.0` (2026-04-27) - [x] ✅ [tar-xz v6] Security hardening: 18 path/symlink TOCTOU vectors audited and closed (leaf check, ENOENT walk, hardlink linkSource, NUL/empty rejection, setuid mask, fd-based fs ops with O_NOFOLLOW, pipeline error propagation) — 8 Copilot review rounds + 1 consolidated audit (2026-04-27) - [x] ✅ [Infra] Independent versioning per workspace package: `release.yml`/`publish.yml` accept `target_package` input, no cross-package version sync; proven in prod — `tar-xz@6.0.0` published without bumping `node-liblzma` (still at 5.0.0) (2026-04-27) diff --git a/src/errors.ts b/src/errors.ts index fd64199..0f79ad5 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -66,8 +66,8 @@ export class LZMAFormatError extends LZMAError { * Options error - thrown when invalid or unsupported options are provided */ export class LZMAOptionsError extends LZMAError { - constructor(errno: number) { - super('Invalid or unsupported options', errno); + constructor(errno: number, message?: string) { + super(message ?? 'Invalid or unsupported options', errno); this.name = 'LZMAOptionsError'; } } diff --git a/src/lzma.ts b/src/lzma.ts index ad734cb..af51bca 100644 --- a/src/lzma.ts +++ b/src/lzma.ts @@ -301,8 +301,29 @@ export type { * * Emits `progress` event after each chunk with `{bytesRead, bytesWritten}` info. */ + +/** + * Internal resolved options for XzStream instances. + * + * All fields are required (defaults applied in constructor) EXCEPT memlimit, + * which is genuinely optional: the native binding ignores it (UINT64_MAX + * hardcoded; see TODO "[Native] Wire memlimit in src/bindings/node-liblzma.cpp"). + * Only the WASM Buffer API decompression paths (unxz/unxzAsync/streamBufferDecode) honour memlimit; xzAsync is compression-only and ignores this field. + */ +interface ResolvedLZMAOptions { + check: number; + preset: number; + filters: number[]; + mode: number; + threads: number; + chunkSize: number; + flushFlag: number; + /** Honoured only by the WASM Buffer API; native streams ignore this field. */ + memlimit?: number | bigint; +} + export abstract class XzStream extends Transform { - protected _opts: Required; + protected _opts: ResolvedLZMAOptions; protected _chunkSize: number; protected _flushFlag: number; protected lzma: NativeLZMA; @@ -344,6 +365,10 @@ export abstract class XzStream extends Transform { threads: opts.threads ?? 1, chunkSize: opts.chunkSize ?? liblzma.BUFSIZ, flushFlag: opts.flushFlag ?? liblzma.LZMA_RUN, + // memlimit is genuinely optional in ResolvedLZMAOptions: the native binding ignores it + // (UINT64_MAX hardcoded; see TODO "[Native] Wire memlimit in src/bindings/node-liblzma.cpp"). + // Only the WASM decompression APIs (unxz/unxzAsync/streamBufferDecode) honour this field. xzAsync is compression-only and ignores it. + memlimit: opts.memlimit, }; this._chunkSize = this._opts.chunkSize; diff --git a/src/types.ts b/src/types.ts index 43f2558..4467bc4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -53,6 +53,32 @@ export interface LZMAOptions { chunkSize?: number; /** Flush flag to use */ flushFlag?: number; + /** + * Memory usage limit for decompression, in bytes. + * + * **Honored only by `unxz`/`unxzAsync` (WASM Buffer API decompression).** + * `xzAsync` is compression-only and does not read `memlimit`. + * Currently **silently ignored** by native streams (`Xz`/`Unxz`/`createXz`/`createUnxz`) — + * see TODO `[Native] Wire memlimit in src/bindings/node-liblzma.cpp`. + * + * Accepted types: `number` or `bigint` (both are validated then coerced to + * `bigint` before being passed to the WASM C ABI, which maps `uint64_t` to + * `BigInt`). The `number` form must be a finite, non-negative integer; passing + * `NaN`, `Infinity`, a fractional value, or a negative number throws + * `LZMAOptionsError` before any decompression is attempted. + * For values ≥ `Number.MAX_SAFE_INTEGER` (2^53 - 1), use `bigint` to avoid + * precision loss on coercion; passing a `number` above this threshold also + * throws `LZMAOptionsError`. + * + * WASM default: `BigInt(256 * 1024 * 1024)` (256 MiB). + * Native: ignored (UINT64_MAX hardcoded — see follow-up TODO above). + * + * When the compressed stream requires more memory than this limit, + * decompression throws `LZMAMemoryLimitError` with + * `code === LZMA_MEMLIMIT_ERROR` (numeric constant `6`, re-exported from + * `src/errors.ts`). + */ + memlimit?: number | bigint; } /** diff --git a/src/wasm/bindings.ts b/src/wasm/bindings.ts index 4fe4c60..8e2e4ab 100644 --- a/src/wasm/bindings.ts +++ b/src/wasm/bindings.ts @@ -5,7 +5,7 @@ * with proper memory management and error handling. */ -import { createLZMAError, LZMAError } from '../errors.js'; +import { createLZMAError, LZMAError, LZMA_OPTIONS_ERROR, LZMAOptionsError } from '../errors.js'; import { copyFromWasm, copyToWasm, type WasmLzmaStream, wasmAlloc, wasmFree } from './memory.js'; import { LZMA_BUF_ERROR, @@ -116,12 +116,14 @@ export function encoderInit( * * @param stream - Allocated WasmLzmaStream * @param memlimit - Memory limit in bytes (default: 256MB) + * @throws LZMAOptionsError if memlimit is invalid * @throws LZMAError on initialization failure */ export function decoderInit( stream: WasmLzmaStream, memlimit: number | bigint = DEFAULT_MEMLIMIT ): void { + validateMemlimit(memlimit); const module = getModule(); const limit = typeof memlimit === 'number' ? BigInt(memlimit) : memlimit; const ret = module._lzma_stream_decoder(stream.ptr, limit, 0); @@ -137,12 +139,14 @@ export function decoderInit( * * @param stream - Allocated WasmLzmaStream * @param memlimit - Memory limit in bytes (default: 256MB) + * @throws LZMAOptionsError if memlimit is invalid * @throws LZMAError on initialization failure */ export function autoDecoderInit( stream: WasmLzmaStream, memlimit: number | bigint = DEFAULT_MEMLIMIT ): void { + validateMemlimit(memlimit); const module = getModule(); const limit = typeof memlimit === 'number' ? BigInt(memlimit) : memlimit; const ret = module._lzma_auto_decoder(stream.ptr, limit, 0); @@ -244,6 +248,49 @@ export function easyBufferEncode( * @returns Decompressed data * @throws LZMAError on decompression failure */ + +/** + * Validate a memlimit value before coercion to BigInt. + * + * BigInt() throws a native RangeError for NaN, Infinity, and non-integer + * numbers (e.g. 1.5). Negative integers produce a huge unsigned value when + * interpreted by the C ABI (uint64_t wrap-around). Numbers above + * Number.MAX_SAFE_INTEGER (2^53 - 1) lose precision on coercion to BigInt. + * All these cases are rejected here with LZMAOptionsError so callers always + * get an LZMAError subclass. + * + * @throws LZMAOptionsError if the value is invalid + */ +function validateMemlimit(memlimit: number | bigint): void { + if (typeof memlimit === 'bigint') { + if (memlimit < 0n) { + throw new LZMAOptionsError(LZMA_OPTIONS_ERROR, 'memlimit must be a non-negative value'); + } + return; + } + if (!Number.isFinite(memlimit)) { + throw new LZMAOptionsError( + LZMA_OPTIONS_ERROR, + 'memlimit must be a finite number (NaN and Infinity are not allowed)' + ); + } + if (!Number.isInteger(memlimit)) { + throw new LZMAOptionsError( + LZMA_OPTIONS_ERROR, + 'memlimit must be an integer (fractional values are not allowed)' + ); + } + if (memlimit < 0) { + throw new LZMAOptionsError(LZMA_OPTIONS_ERROR, 'memlimit must be a non-negative value'); + } + if (memlimit > Number.MAX_SAFE_INTEGER) { + throw new LZMAOptionsError( + LZMA_OPTIONS_ERROR, + 'memlimit number exceeds MAX_SAFE_INTEGER (use bigint for values >= 2^53)' + ); + } +} + export function streamBufferDecode( input: Uint8Array, memlimit: number | bigint = DEFAULT_MEMLIMIT @@ -261,6 +308,7 @@ export function streamBufferDecode( let outPtr = wasmAlloc(module, outSize); try { + validateMemlimit(memlimit); const limit = typeof memlimit === 'number' ? BigInt(memlimit) : memlimit; module.setValue(memlimitPtr, limit, 'i64'); diff --git a/src/wasm/decompress.ts b/src/wasm/decompress.ts index 47ad7eb..97bdf32 100644 --- a/src/wasm/decompress.ts +++ b/src/wasm/decompress.ts @@ -19,12 +19,11 @@ import { toUint8Array } from './utils.js'; */ export async function unxzAsync( buffer: Uint8Array | ArrayBuffer | string, - _opts?: LZMAOptions + opts?: LZMAOptions ): Promise { await initModule(); const input = toUint8Array(buffer); - // TODO: pass opts.memlimit when LZMAOptions supports it - return streamBufferDecode(input); + return streamBufferDecode(input, opts?.memlimit); } /** diff --git a/test/wasm/decompress-memlimit.test.ts b/test/wasm/decompress-memlimit.test.ts new file mode 100644 index 0000000..b11f513 --- /dev/null +++ b/test/wasm/decompress-memlimit.test.ts @@ -0,0 +1,240 @@ +/** + * Tests for memlimit option wired through unxzAsync / unxz (WASM). + * + * Observable Success criteria: + * - Very small memlimit (1024 bytes) causes LZMAMemoryLimitError (code === 'LZMA_MEMLIMIT_ERROR') + * - Sufficient memlimit (256 MiB) allows decompression to succeed + * - Both number and bigint forms of memlimit are accepted + * - Callback variant (unxz) inherits the same behaviour + * - Invalid memlimit values (NaN, Infinity, fractional, negative) throw LZMAOptionsError + * - Number above MAX_SAFE_INTEGER throws LZMAOptionsError (precision loss) + * - decoderInit / autoDecoderInit validate memlimit at every entry point + */ + +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { LZMA_MEMLIMIT_ERROR, LZMAMemoryLimitError, LZMAOptionsError } from '../../src/errors.js'; +import { xzAsync } from '../../src/wasm/compress.js'; +import { unxz, unxzAsync } from '../../src/wasm/decompress.js'; +import { autoDecoderInit, decoderInit } from '../../src/wasm/bindings.js'; +import type { WasmLzmaStream } from '../../src/wasm/memory.js'; +import { loadWasmModule, unloadWasmModule } from './wasm-helpers.utils.js'; + +/** + * Build a fixture compressed at preset 6 (default dictionary = 8 MiB decoder requirement). + * Preset 9 cannot be used in WASM because the encoder itself exceeds the WASM memory budget. + * The decoder for preset-6 streams needs ~8 MiB; memlimit: 1024 (1 KiB) is well below ANY + * realistic dictionary size, so it reliably triggers LZMA_MEMLIMIT_ERROR from + * lzma_stream_buffer_decode regardless of the exact stream content. + */ +async function makeFixture(): Promise<{ original: Uint8Array; compressed: Uint8Array }> { + const original = new TextEncoder().encode('memlimit fixture: ' + 'x'.repeat(512)); + const compressed = await xzAsync(original, { preset: 6 }); + return { original, compressed }; +} + +describe('WASM unxzAsync — memlimit option', () => { + beforeAll(async () => { + await loadWasmModule(); + }); + + afterAll(() => { + unloadWasmModule(); + }); + + it('rejects with LZMAMemoryLimitError when memlimit is too small (number)', async () => { + const { compressed } = await makeFixture(); + await expect(unxzAsync(compressed, { memlimit: 1024 })).rejects.toThrow(LZMAMemoryLimitError); + }); + + it('rejects with code LZMA_MEMLIMIT_ERROR when memlimit is too small', async () => { + const { compressed } = await makeFixture(); + const err = await unxzAsync(compressed, { memlimit: 1024 }).catch((e: unknown) => e); + expect(err).toBeInstanceOf(LZMAMemoryLimitError); + expect((err as LZMAMemoryLimitError).code).toBe(LZMA_MEMLIMIT_ERROR); + }); + + it('rejects with LZMAMemoryLimitError when memlimit is too small (bigint)', async () => { + const { compressed } = await makeFixture(); + await expect(unxzAsync(compressed, { memlimit: 1024n })).rejects.toThrow(LZMAMemoryLimitError); + }); + + it('succeeds with sufficient memlimit (number: 256 MiB)', async () => { + const { original, compressed } = await makeFixture(); + const decompressed = await unxzAsync(compressed, { memlimit: 256 * 1024 * 1024 }); + expect(decompressed).toEqual(original); + }); + + it('succeeds with sufficient memlimit (bigint: 256 MiB)', async () => { + const { original, compressed } = await makeFixture(); + const decompressed = await unxzAsync(compressed, { + memlimit: BigInt(256 * 1024 * 1024), + }); + expect(decompressed).toEqual(original); + }); + + it('succeeds with no memlimit (uses default 256 MiB)', async () => { + const { original, compressed } = await makeFixture(); + // Note: this test proves no-throw with the default, NOT that the default is specifically + // 256 MiB. A truly exhaustive proof would require a fixture larger than 256 MiB + // (impractical in a unit test). The 256 MiB default is enforced by DEFAULT_MEMLIMIT + // in src/wasm/bindings.ts. + const decompressed = await unxzAsync(compressed); + expect(decompressed).toEqual(original); + }); + + // F-001: Validate memlimit before BigInt coercion — BigInt(NaN/Infinity/1.5) would throw + // a native RangeError; negative integers silently wrap to huge uint64_t values. + // All these must throw LZMAOptionsError (an LZMAError subclass), not a raw RangeError. + describe('invalid memlimit values — throw LZMAOptionsError', () => { + it('rejects NaN memlimit with LZMAOptionsError', async () => { + const { compressed } = await makeFixture(); + await expect(unxzAsync(compressed, { memlimit: Number.NaN })).rejects.toThrow( + LZMAOptionsError + ); + }); + + it('rejects Infinity memlimit with LZMAOptionsError', async () => { + const { compressed } = await makeFixture(); + await expect(unxzAsync(compressed, { memlimit: Number.POSITIVE_INFINITY })).rejects.toThrow( + LZMAOptionsError + ); + }); + + it('rejects fractional memlimit with LZMAOptionsError', async () => { + const { compressed } = await makeFixture(); + await expect(unxzAsync(compressed, { memlimit: 1.5 })).rejects.toThrow(LZMAOptionsError); + }); + + it('rejects negative memlimit with LZMAOptionsError', async () => { + const { compressed } = await makeFixture(); + await expect(unxzAsync(compressed, { memlimit: -1024 })).rejects.toThrow(LZMAOptionsError); + }); + }); + + describe('callback variant (unxz)', () => { + it('passes LZMAMemoryLimitError to callback when memlimit too small', async () => { + // C-001: await the fixture directly so rejection bubbles to vitest instead of hanging. + const { compressed } = await makeFixture(); + await new Promise((resolve, reject) => { + unxz(compressed, { memlimit: 1024 }, (err) => { + try { + expect(err).toBeInstanceOf(LZMAMemoryLimitError); + expect((err as LZMAMemoryLimitError).code).toBe(LZMA_MEMLIMIT_ERROR); + resolve(); + } catch (e) { + reject(e); + } + }); + }); + }); + + it('succeeds via callback with sufficient memlimit', async () => { + // C-002: await fixture before constructing the callback promise. + // C-003: assert byte-level equality, not just defined-ness. + const { original, compressed } = await makeFixture(); + await new Promise((resolve, reject) => { + unxz(compressed, { memlimit: 256 * 1024 * 1024 }, (err, result) => { + try { + expect(err).toBeNull(); + expect(result).toBeDefined(); + // Verify decompressed bytes equal the original, not merely that something was returned. + expect(Array.from(result!)).toEqual(Array.from(original)); + resolve(); + } catch (e) { + reject(e); + } + }); + }); + }); + }); + + // C-2-004: Number above MAX_SAFE_INTEGER loses precision on BigInt coercion. + // Verify this is rejected with LZMAOptionsError, not silently passed through. + describe('C-2-004 — number > MAX_SAFE_INTEGER rejected (precision loss)', () => { + it('rejects number equal to 2**53 with LZMAOptionsError', async () => { + const { compressed } = await makeFixture(); + // 2**53 === Number.MAX_SAFE_INTEGER + 1 — the first integer that cannot + // be represented exactly; BigInt(2**53) may not equal 2n**53n. + const tooLarge = 2 ** 53; + await expect(unxzAsync(compressed, { memlimit: tooLarge })).rejects.toThrow(LZMAOptionsError); + }); + + it('rejects number well above MAX_SAFE_INTEGER (2**60) with LZMAOptionsError', async () => { + const { compressed } = await makeFixture(); + await expect(unxzAsync(compressed, { memlimit: 2 ** 60 })).rejects.toThrow(LZMAOptionsError); + }); + + it('accepts bigint 2n**53n (same magnitude, no precision loss)', async () => { + const { original, compressed } = await makeFixture(); + // bigint path bypasses the MAX_SAFE_INTEGER guard — no precision loss. + const decompressed = await unxzAsync(compressed, { memlimit: 2n ** 53n }); + expect(decompressed).toEqual(original); + }); + + it('accepts Number.MAX_SAFE_INTEGER itself (exactly representable)', async () => { + const { original, compressed } = await makeFixture(); + const decompressed = await unxzAsync(compressed, { memlimit: Number.MAX_SAFE_INTEGER }); + expect(decompressed).toEqual(original); + }); + }); + + // C-2-005: validateMemlimit must be called at every public memlimit entry point. + // decoderInit and autoDecoderInit previously did raw BigInt(memlimit) without validation. + describe('C-2-005 — decoderInit / autoDecoderInit validate memlimit', () => { + // validateMemlimit runs before getModule(), so we can test with a null stream + // cast — the throw happens before the stream is accessed. + const nullStream = null as unknown as WasmLzmaStream; + + describe('decoderInit — invalid memlimit rejected', () => { + it('rejects NaN memlimit', () => { + expect(() => decoderInit(nullStream, Number.NaN)).toThrow(LZMAOptionsError); + }); + + it('rejects Infinity memlimit', () => { + expect(() => decoderInit(nullStream, Infinity)).toThrow(LZMAOptionsError); + }); + + it('rejects fractional memlimit', () => { + expect(() => decoderInit(nullStream, 1.5)).toThrow(LZMAOptionsError); + }); + + it('rejects negative memlimit', () => { + expect(() => decoderInit(nullStream, -1)).toThrow(LZMAOptionsError); + }); + + it('rejects negative bigint memlimit', () => { + expect(() => decoderInit(nullStream, -1n)).toThrow(LZMAOptionsError); + }); + + it('rejects number above MAX_SAFE_INTEGER', () => { + expect(() => decoderInit(nullStream, 2 ** 53)).toThrow(LZMAOptionsError); + }); + }); + + describe('autoDecoderInit — invalid memlimit rejected', () => { + it('rejects NaN memlimit', () => { + expect(() => autoDecoderInit(nullStream, Number.NaN)).toThrow(LZMAOptionsError); + }); + + it('rejects Infinity memlimit', () => { + expect(() => autoDecoderInit(nullStream, Infinity)).toThrow(LZMAOptionsError); + }); + + it('rejects fractional memlimit', () => { + expect(() => autoDecoderInit(nullStream, 1.5)).toThrow(LZMAOptionsError); + }); + + it('rejects negative memlimit', () => { + expect(() => autoDecoderInit(nullStream, -1)).toThrow(LZMAOptionsError); + }); + + it('rejects negative bigint memlimit', () => { + expect(() => autoDecoderInit(nullStream, -1n)).toThrow(LZMAOptionsError); + }); + + it('rejects number above MAX_SAFE_INTEGER', () => { + expect(() => autoDecoderInit(nullStream, 2 ** 53)).toThrow(LZMAOptionsError); + }); + }); + }); +});