Line data Source code
1 : // Copyright 2026 The Authors
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : import 'dart:ffi';
16 : import 'dart:typed_data';
17 :
18 : import 'package:ffi/ffi.dart';
19 :
20 : import 'third_party/zstd.dart';
21 : import 'zstd_exception.dart';
22 : import 'zstd_limits.dart';
23 :
24 : /// Default compression level for Zstd.
25 : const int defaultLevel = ZSTD_CLEVEL_DEFAULT;
26 :
27 : /// Version of the Zstd library being used.
28 : const String zStdVersion = ZSTD_VERSION_STRING;
29 :
30 : /// Returns the minimum compression level supported by the Zstd library.
31 : @Native<Int32 Function()>(symbol: 'ZSTD_minCLevel')
32 : external int minCLevel();
33 :
34 : /// Returns the maximum compression level supported by the Zstd library.
35 : @Native<Int32 Function()>(symbol: 'ZSTD_maxCLevel')
36 : external int maxCLevel();
37 :
38 : @Native<Size Function(Size)>(symbol: 'ZSTD_compressBound')
39 : external int _compressBound(int srcSize);
40 :
41 : @Native<Size Function(Pointer<Void>, Size, Pointer<Void>, Size, Int32)>(
42 : symbol: 'ZSTD_compress',
43 : )
44 : external int _compress(
45 : Pointer<Void> dst,
46 : int dstCapacity,
47 : Pointer<Void> src,
48 : int srcSize,
49 : int compressionLevel,
50 : );
51 :
52 : @Native<Size Function(Pointer<Void>, Size, Pointer<Void>, Size)>(
53 : symbol: 'ZSTD_decompress',
54 : )
55 : external int _decompress(
56 : Pointer<Void> dst,
57 : int dstCapacity,
58 : Pointer<Void> src,
59 : int compressedSize,
60 : );
61 :
62 : @Native<Uint64 Function(Pointer<Void>, Size)>(
63 : symbol: 'ZSTD_getFrameContentSize',
64 : )
65 : external int _getFrameContentSize(Pointer<Void> src, int srcSize);
66 :
67 : @Native<Uint32 Function(Size)>(symbol: 'ZSTD_isError')
68 : external int _isError(int result);
69 :
70 : @Native<Pointer<Utf8> Function(Size)>(symbol: 'ZSTD_getErrorName')
71 : external Pointer<Utf8> _getErrorName(int result);
72 :
73 : /// A simple interface for Zstd compression and decompression.
74 : ///
75 : /// Use this class for synchronous compression and decompression of byte arrays.
76 : class ZstdSimple {
77 : /// The compression level to use (default: [defaultLevel]).
78 : final int level;
79 :
80 : /// No-op on native platforms; exists so callers can always await
81 : /// [ZstdSimple.init] without platform guards.
82 2 : static Future<void> init({String? wasmUrl}) async {}
83 :
84 : /// Creates a new [ZstdSimple] instance with the given [level].
85 : ///
86 : /// Throws [ArgumentError] if the [level] is invalid.
87 2 : ZstdSimple({this.level = defaultLevel}) {
88 4 : if (!_isValidCLevel(level)) {
89 2 : throw ArgumentError.value(level, 'level', 'Invalid compression level');
90 : }
91 : }
92 :
93 : /// Returns the Zstd version string.
94 2 : String get version => zStdVersion.toString();
95 :
96 2 : bool _isValidCLevel(int level) =>
97 8 : level >= minCLevel() && level <= maxCLevel();
98 :
99 : /// Compresses the given [data].
100 : ///
101 : /// Returns the compressed data as a [Uint8List].
102 : /// Throws an [Exception] if an error occurs during compression.
103 2 : Uint8List compress(List<int> data) {
104 2 : final srcSize = data.length;
105 2 : final dstCapacity = _compressBound(srcSize);
106 :
107 4 : if (_isError(dstCapacity) != 0) {
108 0 : final errorName = _getErrorName(dstCapacity).toDartString();
109 0 : throw ZstdException('compressBound error: $errorName');
110 : }
111 :
112 : // Each allocation is guarded by its own try/finally, nested, so that a
113 : // failure allocating dstPtr cannot leak srcPtr (F-1). This mirrors the
114 : // structure already used by the web compress() path.
115 : final srcPtr = malloc<Uint8>(srcSize);
116 : try {
117 4 : srcPtr.asTypedList(srcSize).setAll(0, data);
118 :
119 : final dstPtr = malloc<Uint8>(dstCapacity);
120 : try {
121 2 : final compressedSize = _compress(
122 2 : dstPtr.cast(),
123 : dstCapacity,
124 2 : srcPtr.cast(),
125 : srcSize,
126 2 : level,
127 : );
128 :
129 4 : if (_isError(compressedSize) != 0) {
130 0 : final errorName = _getErrorName(compressedSize).toDartString();
131 0 : throw ZstdException('compression error: $errorName');
132 : }
133 :
134 4 : final result = Uint8List.fromList(dstPtr.asTypedList(compressedSize));
135 : return result;
136 : } finally {
137 2 : malloc.free(dstPtr);
138 : }
139 : } finally {
140 2 : malloc.free(srcPtr);
141 : }
142 : }
143 :
144 : /// Decompresses the given [data].
145 : ///
146 : /// Reads the declared decompressed size from the frame header and rejects
147 : /// it — before allocating anything for it — if it is negative or exceeds
148 : /// [maxOutputBytes]. [maxOutputBytes] defaults to [defaultMaxOutputBytes]
149 : /// (64 MiB); there is no unbounded mode, so a caller who needs to
150 : /// decompress something larger must pass a larger explicit value.
151 : ///
152 : /// The negative case is not merely defensive: `_getFrameContentSize` is
153 : /// bound as a `Uint64 Function(...)` returning a Dart `int`, so a frame
154 : /// declaring a content size at or above 2^63 arrives as a negative Dart
155 : /// `int` that is neither the `-1` (`ZSTD_CONTENTSIZE_UNKNOWN`) nor `-2`
156 : /// (`ZSTD_CONTENTSIZE_ERROR`) sentinel, and would otherwise reach
157 : /// `malloc<Uint8>` with a negative size.
158 : ///
159 : /// Peak transient memory while decompressing within the cap is
160 : /// approximately 2x [maxOutputBytes]: the native destination buffer plus
161 : /// the copy into the returned Dart-managed [Uint8List].
162 : ///
163 : /// Returns the decompressed data as a [Uint8List].
164 : /// Throws [ZstdLimitExceededException] if the declared content size is
165 : /// negative or exceeds [maxOutputBytes].
166 : /// Throws an [Exception] if an error occurs during decompression.
167 2 : Uint8List decompress(
168 : List<int> data, {
169 : int maxOutputBytes = defaultMaxOutputBytes,
170 : }) {
171 2 : final compressedSize = data.length;
172 : final srcPtr = malloc<Uint8>(compressedSize);
173 : try {
174 4 : srcPtr.asTypedList(compressedSize).setAll(0, data);
175 2 : final decompressedSize = _getFrameContentSize(
176 2 : srcPtr.cast(),
177 : compressedSize,
178 : );
179 :
180 4 : if (decompressedSize == -1) {
181 1 : throw ZstdException(
182 : 'decompression error: unknown content size. Use streaming API.',
183 : );
184 : }
185 4 : if (decompressedSize == -2) {
186 1 : throw ZstdException('decompression error: invalid frame header.');
187 : }
188 : // Reject before allocating anything for the declared size. The `< 0`
189 : // arm is required, not defensive — see the doc comment above.
190 4 : if (decompressedSize < 0 || decompressedSize > maxOutputBytes) {
191 1 : throw ZstdLimitExceededException(decompressedSize, maxOutputBytes);
192 : }
193 :
194 : final dstPtr = malloc<Uint8>(decompressedSize);
195 : try {
196 2 : final resultSize = _decompress(
197 2 : dstPtr.cast(),
198 : decompressedSize,
199 2 : srcPtr.cast(),
200 : compressedSize,
201 : );
202 :
203 4 : if (_isError(resultSize) != 0) {
204 2 : final errorName = _getErrorName(resultSize).toDartString();
205 2 : throw ZstdException('decompression error: $errorName');
206 : }
207 :
208 4 : final result = Uint8List.fromList(dstPtr.asTypedList(resultSize));
209 : return result;
210 : } finally {
211 2 : malloc.free(dstPtr);
212 : }
213 : } finally {
214 2 : malloc.free(srcPtr);
215 : }
216 : }
217 : }
|