1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 2017-2021, The LegacyClonk Team and contributors
5 *
6 * Distributed under the terms of the ISC license; see accompanying file
7 * "COPYING" for details.
8 *
9 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
10 * See accompanying file "TRADEMARK" for details.
11 *
12 * To redistribute this file separately, substitute the full license texts
13 * for the above references.
14 */
15
16#include <StdGzCompressedFile.h>
17
18#include <algorithm>
19#include <cerrno>
20#include <cstring>
21#include <format>
22#include <memory>
23
24namespace StdGzCompressedFile
25{
26Read::Read(const std::string &filename)
27{
28 file = fopen(filename: filename.c_str(), modes: "rb");
29 if (!file)
30 {
31 throw Exception{std::format(fmt: "Opening \"{}\": {}", args: filename, args: std::strerror(errno))};
32 }
33
34 gzStream.next_out = nullptr;
35 gzStream.avail_out = 0;
36
37 try
38 {
39 PrepareInflate();
40 }
41 catch (...)
42 {
43 fclose(stream: file);
44 throw;
45 }
46}
47
48Read::~Read()
49{
50 if (file)
51 {
52 fclose(stream: file);
53 file = nullptr;
54 }
55
56 if (gzStreamValid)
57 {
58 inflateEnd(strm: &gzStream);
59 }
60}
61
62void Read::CheckMagicBytes()
63{
64 uint8_t magicBytesBuffer[2];
65 uint8_t *readTarget = magicBytesBuffer;
66 unsigned int readLeft = 2;
67
68 for (; readLeft > 0;)
69 {
70 if (bufferedSize == 0)
71 {
72 RefillBuffer();
73
74 if (feof(stream: file) && bufferedSize == 0)
75 {
76 throw Exception("Unexpected end of file while reading the magic bytes");
77 }
78 }
79
80 const auto progress = readLeft > bufferedSize ? bufferedSize : readLeft;
81 std::memcpy(dest: readTarget, src: bufferPtr, n: progress);
82
83 readLeft -= progress;
84 readTarget += progress;
85 bufferPtr += progress;
86 bufferedSize -= progress;
87 }
88
89 if (!std::equal(first1: magicBytesBuffer, last1: std::end(arr&: magicBytesBuffer), first2: C4GroupMagic) &&
90 !std::equal(first1: magicBytesBuffer, last1: std::end(arr&: magicBytesBuffer), first2: GZMagic))
91 {
92 throw Exception("Magic bytes don't match");
93 }
94}
95
96void Read::PrepareInflate()
97{
98 uint8_t fakeBuf;
99
100 CheckMagicBytes();
101
102 const auto storeNextOut = gzStream.next_out;
103 const auto storeAvailOut = gzStream.avail_out;
104
105 gzStream.zalloc = nullptr;
106 gzStream.zfree = nullptr;
107 gzStream.opaque = nullptr;
108 gzStream.next_in = GZMagic;
109 gzStream.avail_in = sizeof(GZMagic);
110
111 gzStream.next_out = &fakeBuf;
112 gzStream.avail_out = 1;
113
114 if (const auto ret = inflateInit2(&gzStream, 15 + 16); ret != Z_OK) // window size 15 + automatic gzip
115 {
116 throw Exception(std::string{"inflateInit2 failed: "} + zError(ret));
117 }
118
119 if (const auto ret = inflate(strm: &gzStream, Z_NO_FLUSH); ret != Z_OK)
120 {
121 throw Exception(std::string{"inflate on the fake magic failed: "} + zError(ret));
122 }
123
124 if (gzStream.avail_in > 0)
125 {
126 throw Exception("After inflating the fake magic, avail_in must be 0");
127 }
128
129 gzStream.next_out = storeNextOut;
130 gzStream.avail_out = storeAvailOut;
131
132 gzStream.next_in = bufferPtr;
133 gzStream.avail_in = bufferedSize;
134
135 gzStreamValid = true;
136}
137
138size_t Read::UncompressedSize()
139{
140 std::unique_ptr<uint8_t[]> buffer{new uint8_t[ChunkSize]};
141 size_t size = 0;
142 for (;;)
143 {
144 const auto progress = ReadData(toBuffer: buffer.get(), size: ChunkSize);
145 if (progress == 0) break;
146
147 size += progress;
148 }
149 return size;
150}
151
152size_t Read::ReadData(uint8_t *const toBuffer, const size_t size)
153{
154 size_t readSize = 0;
155 gzStream.next_out = toBuffer;
156 gzStream.avail_out = checked_cast<unsigned int>(from: size);
157 for (; size > readSize;)
158 {
159 if (!gzStreamValid && !(feof(stream: file) && bufferedSize == 0))
160 {
161 PrepareInflate();
162 }
163
164 if (gzStream.avail_in == 0)
165 {
166 if (bufferedSize == 0)
167 {
168 RefillBuffer();
169
170 if (feof(stream: file) && bufferedSize == 0)
171 {
172 break;
173 }
174 }
175
176 gzStream.next_in = bufferPtr;
177 gzStream.avail_in = bufferedSize;
178 }
179
180 const auto oldAvailIn = gzStream.avail_in;
181 const auto oldAvailOut = gzStream.avail_out;
182
183 if (const auto ret = inflate(strm: &gzStream, Z_SYNC_FLUSH); ret != Z_OK)
184 {
185 if (ret == Z_STREAM_END)
186 {
187 inflateEnd(strm: &gzStream);
188 gzStreamValid = false;
189 }
190 else if (ret != Z_BUF_ERROR && gzStream.avail_out != 0)
191 {
192 throw Exception(std::string{"inflate failed: "} + zError(ret));
193 }
194 }
195 const auto outProgress = oldAvailOut - gzStream.avail_out;
196 position += outProgress;
197 readSize += outProgress;
198
199 const auto inProgress = oldAvailIn - gzStream.avail_in;
200 bufferPtr += inProgress;
201 bufferedSize -= inProgress;
202 }
203
204 return readSize;
205}
206
207void Read::RefillBuffer()
208{
209 bufferedSize = static_cast<unsigned int>(fread(ptr: buffer.get(), size: 1, n: ChunkSize, stream: file));
210 if (ferror(stream: file)) throw Exception("fread failed");
211 bufferPtr = buffer.get();
212}
213
214void Read::Rewind()
215{
216 position = 0;
217 fseek(stream: file, off: 0, SEEK_SET);
218
219 inflateEnd(strm: &gzStream);
220
221 gzStream.next_out = nullptr;
222 gzStream.avail_out = 0;
223 bufferedSize = 0;
224 PrepareInflate();
225}
226
227Write::Write(const std::string &filename)
228{
229 file = fopen(filename: filename.c_str(), modes: "wb");
230 if (!file)
231 {
232 throw Exception{std::format(fmt: "Opening \"{}\": {}", args: filename, args: std::strerror(errno))};
233 }
234
235 gzStream.zalloc = nullptr;
236 gzStream.zfree = nullptr;
237 gzStream.opaque = nullptr;
238 gzStream.next_in = nullptr;
239 gzStream.avail_in = 0;
240 gzStream.next_out = buffer.get();
241 gzStream.avail_out = ChunkSize;
242
243 if (const auto ret = deflateInit2(&gzStream, 9, Z_DEFLATED, 15 + 16, CompressionLevel, Z_DEFAULT_STRATEGY); ret != Z_OK)
244 {
245 fclose(stream: file);
246 throw Exception(std::string{"deflateInit2 failed: "} + zError(ret));
247 }
248}
249
250Write::~Write() noexcept(false)
251{
252 if (file)
253 {
254 DeflateToBuffer(fromBuffer: nullptr, size: 0, Z_FINISH, Z_STREAM_END);
255
256 FlushBuffer();
257 fclose(stream: file);
258 }
259
260 deflateEnd(strm: &gzStream);
261}
262
263void Write::FlushBuffer()
264{
265 if (static_cast<unsigned int>(fwrite(ptr: buffer.get(), size: 1, n: bufferedSize, s: file)) != bufferedSize)
266 {
267 throw Exception("fwrite failed");
268 }
269
270 bufferedSize = 0;
271}
272
273void Write::DeflateToBuffer(const uint8_t *const fromBuffer, const size_t size, int flushMode, int expectedRet)
274{
275 gzStream.next_in = fromBuffer;
276 gzStream.avail_in = checked_cast<unsigned int>(from: size);
277
278 if (!magicBytesDone)
279 {
280 static_assert(ChunkSize >= sizeof(C4GroupMagic));
281 std::copy(first: C4GroupMagic, last: std::end(arr: C4GroupMagic), result: buffer.get());
282
283 uint8_t magicDummy[2];
284 gzStream.next_out = magicDummy;
285 gzStream.avail_out = 2;
286
287 if (const auto ret = deflate(strm: &gzStream, Z_NO_FLUSH); ret != Z_OK)
288 {
289 throw Exception(std::string{"Deflating into the magic dummy buffer: "} + zError(ret));
290 }
291
292 magicBytesDone = true;
293 gzStream.next_out = buffer.get() + 2;
294 gzStream.avail_out = ChunkSize - 2;
295 bufferedSize += 2;
296 }
297
298 int ret = Z_BUF_ERROR;
299 while (ret == Z_BUF_ERROR || gzStream.avail_in > 0 || (ret == Z_OK && flushMode == Z_FINISH))
300 {
301 if (gzStream.avail_out == 0)
302 {
303 FlushBuffer();
304
305 gzStream.next_out = buffer.get();
306 gzStream.avail_out = ChunkSize;
307 }
308
309 const auto oldAvailOut = gzStream.avail_out;
310 ret = deflate(strm: &gzStream, flush: flushMode);
311 bufferedSize += oldAvailOut - gzStream.avail_out;
312
313 if (ret == Z_STREAM_ERROR)
314 {
315 break;
316 }
317 }
318
319 if (ret == Z_STREAM_ERROR || ret != expectedRet)
320 {
321 throw Exception(std::string{"Deflating the data to write: "} + zError(ret));
322 }
323}
324
325void Write::WriteData(const uint8_t *const fromBuffer, const size_t size)
326{
327 DeflateToBuffer(fromBuffer, size, Z_NO_FLUSH, Z_OK);
328}
329}
330