GCC Code Coverage Report


Directory: ./
File: fseq/FSEQFile.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 1337 1556 85.9%
Functions: 112 131 85.5%
Branches: 853 1458 58.5%

Line Branch Exec Source
1 // This #define must be before any #include's
2 #define _FILE_OFFSET_BITS 64
3 #define __STDC_FORMAT_MACROS
4
5 #include <algorithm>
6 #include <cerrno>
7 #include <cstring>
8 #include <memory>
9 #include <vector>
10
11 #include <atomic>
12 #include <condition_variable>
13 #include <deque>
14 #include <list>
15 #include <map>
16 #include <mutex>
17 #include <thread>
18
19 #include <chrono>
20 using namespace std::literals;
21 using namespace std::chrono_literals;
22 using namespace std::literals::chrono_literals;
23
24 #include <sys/file.h>
25 #include <sys/stat.h>
26 #include <sys/types.h>
27 #include <fcntl.h>
28 #include <inttypes.h>
29 #include <stdio.h>
30
31 #ifdef _MSC_VER
32 #include <wx/wx.h>
33 int gettimeofday(struct timeval* tp, struct timezone* tzp) {
34 // Note: some broken versions only have 8 trailing zero's, the correct epoch has 9 trailing zero's
35 // This magic number is the number of 100 nanosecond intervals since January 1, 1601 (UTC)
36 // until 00:00:00 January 1, 1970
37 static const uint64_t EPOCH = ((uint64_t)116444736000000000ULL);
38
39 SYSTEMTIME system_time;
40 FILETIME file_time;
41 uint64_t time;
42
43 GetSystemTime(&system_time);
44 SystemTimeToFileTime(&system_time, &file_time);
45 time = ((uint64_t)file_time.dwLowDateTime);
46 time += ((uint64_t)file_time.dwHighDateTime) << 32;
47
48 tp->tv_sec = (long)((time - EPOCH) / 10000000L);
49 tp->tv_usec = (long)(system_time.wMilliseconds * 1000);
50 return 0;
51 }
52 #define ftello _ftelli64
53 #define fseeko _fseeki64
54
55 #else
56 #include <sys/time.h>
57 #include <unistd.h>
58 #endif
59
60 #include "FSEQFile.h"
61
62 68 static void SetThreadName(const std::string& name) {
63 #ifdef PLATFORM_OSX
64 pthread_setname_np(name.c_str());
65 #else
66 68 pthread_setname_np(pthread_self(), name.c_str());
67 #endif
68 68 }
69
70 #if defined(PLATFORM_OSX)
71 #define PLATFORM_UNKNOWN
72 #endif
73
74 #if defined(PLATFORM_PI) || defined(PLATFORM_BBB) || defined(PLATFORM_BB64) || defined(PLATFORM_UNKNOWN) || defined(PLATFORM_DEBIAN) || defined(PLATFORM_FEDORA) || defined(PLATFORM_UBUNTU) || defined(PLATFORM_MINT)
75 // for FPP, use FPP logging
76 #include "Warnings.h"
77 #include "log.h"
78 ✗ inline void AddSlowStorageWarning() {
79 ✗ WarningHolder::AddWarningTimeout(90, 18, "FSEQ Data Block not available - Likely slow storage");
80 ✗ }
81 #else
82 // compiling within xLights, use log4cpp
83 #define PLATFORM_UNKNOWN
84 #include <log4cpp/Category.hh>
85 template<typename... Args>
86 static void LogErr(int i, const char* fmt, Args... args) {
87 static log4cpp::Category& fseq_logger_base = log4cpp::Category::getInstance(std::string("log_base"));
88 char buf[256];
89 const char* nfmt = fmt;
90 if (fmt[strlen(fmt) - 1] == '\n') {
91 strcpy(buf, fmt);
92 buf[strlen(fmt) - 1] = 0;
93 nfmt = buf;
94 }
95 fseq_logger_base.error(nfmt, args...);
96 }
97 template<typename... Args>
98 static void LogInfo(int i, const char* fmt, Args... args) {
99 static log4cpp::Category& fseq_logger_base = log4cpp::Category::getInstance(std::string("log_base"));
100 char buf[256];
101 const char* nfmt = fmt;
102 if (fmt[strlen(fmt) - 1] == '\n') {
103 strcpy(buf, fmt);
104 buf[strlen(fmt) - 1] = 0;
105 nfmt = buf;
106 }
107 fseq_logger_base.info(nfmt, args...);
108 }
109 template<typename... Args>
110 static void LogDebug(int i, const char* fmt, Args... args) {
111 static log4cpp::Category& fseq_logger_base = log4cpp::Category::getInstance(std::string("log_base"));
112 char buf[256];
113 const char* nfmt = fmt;
114 if (fmt[strlen(fmt) - 1] == '\n') {
115 strcpy(buf, fmt);
116 buf[strlen(fmt) - 1] = 0;
117 nfmt = buf;
118 }
119 fseq_logger_base.debug(nfmt, args...);
120 }
121 inline void AddSlowStorageWarning() {
122 }
123
124 #define VB_SEQUENCE 1
125 #define VB_ALL 0
126 #endif
127
128 #ifndef NO_ZSTD
129 #include <zstd.h>
130 #endif
131 #ifndef NO_ZLIB
132 #include <zlib.h>
133 #endif
134
135 using FrameData = FSEQFile::FrameData;
136
137 9 inline void DumpHeader(const char* title, unsigned char data[], int len) {
138 9 int x = 0;
139 char tmpStr[128];
140
141 9 snprintf(tmpStr, 128, "%s: (%d bytes)\n", title, len);
142
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 LogInfo(VB_ALL, tmpStr);
143
144
2/2
✓ Branch 0 taken 64 times.
✓ Branch 1 taken 9 times.
73 for (int y = 0; y < len; y++) {
145
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 57 times.
64 if (x == 0) {
146 7 snprintf(tmpStr, 128, "%06x: ", y);
147 }
148 64 snprintf(tmpStr + strlen(tmpStr), 128 - strlen(tmpStr), "%02x ", (int)(data[y] & 0xFF));
149 64 x++;
150
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 64 times.
64 if (x == 16) {
151 ✗ x = 0;
152 ✗ snprintf(tmpStr + strlen(tmpStr), 128 - strlen(tmpStr), "\n");
153 ✗ LogInfo(VB_ALL, tmpStr);
154 }
155 }
156
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 2 times.
9 if (x != 0) {
157 7 snprintf(tmpStr + strlen(tmpStr), 128 - strlen(tmpStr), "\n");
158
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 LogInfo(VB_ALL, tmpStr);
159 }
160 9 }
161
162 ✗ inline uint64_t GetTime(void) {
163 struct timeval now_tv;
164 ✗ gettimeofday(&now_tv, NULL);
165 ✗ return now_tv.tv_sec * 1000000LL + now_tv.tv_usec;
166 }
167
168 968 inline long roundTo4Internal(long i) {
169 968 long remainder = i % 4;
170
2/2
✓ Branch 0 taken 866 times.
✓ Branch 1 taken 102 times.
968 if (remainder == 0) {
171 866 return i;
172 }
173 102 return i + 4 - remainder;
174 }
175
176 6417 inline uint32_t read4ByteUInt(const uint8_t* data) {
177 6417 uint32_t r = (data[0]) + (data[1] << 8) + (data[2] << 16) + (data[3] << 24);
178 6417 return r;
179 }
180 10826 inline uint32_t read3ByteUInt(const uint8_t* data) {
181 10826 uint32_t r = (data[0]) + (data[1] << 8) + (data[2] << 16);
182 10826 return r;
183 }
184 5807 inline uint32_t read2ByteUInt(const uint8_t* data) {
185 5807 uint32_t r = (data[0]) + (data[1] << 8);
186 5807 return r;
187 }
188 1029 inline void write2ByteUInt(uint8_t* data, uint32_t v) {
189 1029 data[0] = (uint8_t)(v & 0xFF);
190 1029 data[1] = (uint8_t)((v >> 8) & 0xFF);
191 1029 }
192 7012 inline void write3ByteUInt(uint8_t* data, uint32_t v) {
193 7012 data[0] = (uint8_t)(v & 0xFF);
194 7012 data[1] = (uint8_t)((v >> 8) & 0xFF);
195 7012 data[2] = (uint8_t)((v >> 16) & 0xFF);
196 7012 }
197 3276 inline void write4ByteUInt(uint8_t* data, uint32_t v) {
198 3276 data[0] = (uint8_t)(v & 0xFF);
199 3276 data[1] = (uint8_t)((v >> 8) & 0xFF);
200 3276 data[2] = (uint8_t)((v >> 16) & 0xFF);
201 3276 data[3] = (uint8_t)((v >> 24) & 0xFF);
202 3276 }
203
204 static const int V1FSEQ_MINOR_VERSION = 0;
205 static const int V1FSEQ_MAJOR_VERSION = 1;
206
207 static const int V2FSEQ_MINOR_VERSION = 0;
208 static const int V2FSEQ_MAJOR_VERSION = 2;
209
210 static const int V1ESEQ_MINOR_VERSION = 0;
211 static const int V1ESEQ_MAJOR_VERSION = 2;
212 static const int V1ESEQ_HEADER_IDENTIFIER = 'E';
213 static const int V1ESEQ_CHANNEL_DATA_OFFSET = 20;
214 static const int V1ESEQ_STEP_TIME = 50;
215
216 // Largest channel count any real writer input buffer can hold. Sequence
217 // m_seqData is FPPD_MAX_CHANNELS (8192*1024) and the fsequtils buffers are
218 // sized to match, so clamping file-fed counts/ranges here to the same ceiling
219 // keeps the write path inside every real caller buffer. Valid files sit far
220 // below it, so this is a no-op for them. Kept file-local to avoid a
221 // Sequence -> FSEQFile include inversion (cf. FPPD_MAX_CHANNELS in Sequence.h).
222 static constexpr uint64_t MAX_CHANNEL_SPACE = (uint64_t)8192 * 1024;
223
224 1465 FSEQFile* FSEQFile::openFSEQFile(const std::string& fn) {
225
1/2
✓ Branch 0 taken 1465 times.
✗ Branch 1 not taken.
1465 FILE* seqFile = fopen((const char*)fn.c_str(), "rb");
226
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1464 times.
1465 if (seqFile == NULL) {
227
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_SEQUENCE, "Error pre-reading FSEQ file (%s), fopen returned NULL\n", fn.c_str());
228 1 return nullptr;
229 }
230 1464 flock(fileno(seqFile), LOCK_SH);
231
1/2
✓ Branch 0 taken 1464 times.
✗ Branch 1 not taken.
1464 fseeko(seqFile, 0L, SEEK_SET);
232
233 // An initial read request of 8 bytes covers the file identifier, version fields and channel data offset
234 // This is the minimum needed to validate the file and prepare the proper sized buffer for a larger read
235 static const int initialReadLen = 8;
236
237 unsigned char headerPeek[initialReadLen];
238
1/2
✓ Branch 0 taken 1464 times.
✗ Branch 1 not taken.
1464 int bytesRead = fread(headerPeek, 1, initialReadLen, seqFile);
239 #ifndef PLATFORM_UNKNOWN
240 37 posix_fadvise(fileno(seqFile), 0, 0, POSIX_FADV_SEQUENTIAL);
241 37 posix_fadvise(fileno(seqFile), 0, 1024 * 1024, POSIX_FADV_WILLNEED);
242 #endif
243
244 // Validate bytesRead covers at least the initial read length
245
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1462 times.
1464 if (bytesRead < initialReadLen) {
246
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_SEQUENCE, "Error pre-reading FSEQ file (%s) header, required %d bytes but read %d\n", fn.c_str(), initialReadLen, bytesRead);
247
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 DumpHeader("File hader peek:", headerPeek, bytesRead);
248 2 flock(fileno(seqFile), LOCK_UN);
249
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 fclose(seqFile);
250 2 return nullptr;
251 }
252
253 // Validate the 4 byte file format identifier is supported
254
8/12
✓ Branch 0 taken 45 times.
✓ Branch 1 taken 1417 times.
✓ Branch 2 taken 45 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 43 times.
✓ Branch 5 taken 2 times.
✓ Branch 6 taken 1460 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1460 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 1460 times.
1462 if ((headerPeek[0] != 'P' && headerPeek[0] != 'F' && headerPeek[0] != V1ESEQ_HEADER_IDENTIFIER) || headerPeek[1] != 'S' || headerPeek[2] != 'E' || headerPeek[3] != 'Q') {
255
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_SEQUENCE, "Error pre-reading FSEQ file (%s) header, invalid identifier\n", fn.c_str());
256
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 DumpHeader("File header peek:", headerPeek, bytesRead);
257 2 flock(fileno(seqFile), LOCK_UN);
258
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 fclose(seqFile);
259 2 return nullptr;
260 }
261
262 1460 uint64_t seqChanDataOffset = read2ByteUInt(&headerPeek[4]);
263 1460 int seqVersionMinor = headerPeek[6];
264 1460 int seqVersionMajor = headerPeek[7];
265
266 // Test for a ESEQ file (identifier[0] == 'E')
267 // ESEQ files are uncompressed V2 FSEQ files with a custom header
268
2/2
✓ Branch 0 taken 43 times.
✓ Branch 1 taken 1417 times.
1460 if (headerPeek[0] == V1ESEQ_HEADER_IDENTIFIER) {
269 43 seqChanDataOffset = V1ESEQ_CHANNEL_DATA_OFFSET;
270 43 seqVersionMajor = V1ESEQ_MAJOR_VERSION;
271 43 seqVersionMinor = V1ESEQ_MINOR_VERSION;
272 }
273
274 // The ctors index fixed header fields directly, so require at least the
275 // bytes they touch before building the header buffer. Anything shorter is
276 // truncated/corrupt: base reads [18] (normal) or [8..11] (ESEQ), V2 reads
277 // [20],[21],[22],[24..31], V2-ESEQ reads [12..19]. All callers already
278 // handle a nullptr return, and valid files are far larger than these floors.
279 1460 size_t minHeaderSize = 19;
280
2/2
✓ Branch 0 taken 43 times.
✓ Branch 1 taken 1417 times.
1460 if (headerPeek[0] == V1ESEQ_HEADER_IDENTIFIER) {
281 43 minHeaderSize = 20;
282
2/2
✓ Branch 0 taken 1414 times.
✓ Branch 1 taken 3 times.
1417 } else if (seqVersionMajor == V2FSEQ_MAJOR_VERSION) {
283 1414 minHeaderSize = 32;
284 }
285
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1457 times.
1460 if (seqChanDataOffset < minHeaderSize) {
286
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 LogErr(VB_SEQUENCE, "Error reading FSEQ file (%s) header, channel data offset %" PRIu64 " is below minimum %d bytes, file is truncated\n",
287 fn.c_str(), seqChanDataOffset, (int)minHeaderSize);
288
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 DumpHeader("File header peek:", headerPeek, bytesRead);
289 3 flock(fileno(seqFile), LOCK_UN);
290
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 fclose(seqFile);
291 3 return nullptr;
292 }
293
294 // Read the full header size (beginning at 0 and ending at seqChanDataOffset)
295
1/2
✓ Branch 0 taken 1457 times.
✗ Branch 1 not taken.
1457 std::vector<uint8_t> header(seqChanDataOffset);
296
1/2
✓ Branch 0 taken 1457 times.
✗ Branch 1 not taken.
1457 fseeko(seqFile, 0L, SEEK_SET);
297
1/2
✓ Branch 0 taken 1457 times.
✗ Branch 1 not taken.
1457 bytesRead = fread(&header[0], 1, seqChanDataOffset, seqFile);
298
299
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1455 times.
1457 if (bytesRead != seqChanDataOffset) {
300
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_SEQUENCE, "Error reading FSEQ file (%s) header, length is %d bytes but read %d\n", fn.c_str(), seqChanDataOffset, bytesRead);
301
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 DumpHeader("File header:", &header[0], bytesRead);
302 2 flock(fileno(seqFile), LOCK_UN);
303
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 fclose(seqFile);
304 2 return nullptr;
305 }
306
307 // Validate the major version is supported
308 // Return a file wrapper to handle version specific metadata
309 1455 FSEQFile* file = nullptr;
310
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1452 times.
1455 if (seqVersionMajor == V1FSEQ_MAJOR_VERSION) {
311
3/8
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
3 file = new V1FSEQFile(fn, seqFile, header);
312
1/2
✓ Branch 0 taken 1452 times.
✗ Branch 1 not taken.
1452 } else if (seqVersionMajor == V2FSEQ_MAJOR_VERSION) {
313
3/8
✓ Branch 0 taken 1452 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1452 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1452 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
1452 file = new V2FSEQFile(fn, seqFile, header);
314 } else {
315 ✗ LogErr(VB_SEQUENCE, "Error opening FSEQ file (%s), unknown version %d.%d\n", fn.c_str(), seqVersionMajor, seqVersionMinor);
316 ✗ DumpHeader("File header:", &header[0], bytesRead);
317 ✗ flock(fileno(seqFile), LOCK_UN);
318 ✗ fclose(seqFile);
319 ✗ return nullptr;
320 }
321
322
1/2
✓ Branch 0 taken 1455 times.
✗ Branch 1 not taken.
1455 file->dumpInfo();
323 1455 return file;
324 1457 }
325 485 FSEQFile* FSEQFile::createFSEQFile(const std::string& fn,
326 int version,
327 CompressionType ct,
328 int level) {
329
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 479 times.
485 if (version == V1FSEQ_MAJOR_VERSION) {
330
2/6
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
6 V1FSEQFile* f = new V1FSEQFile(fn);
331
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (!f->m_seqFile) {
332 ✗ delete f;
333 ✗ f = nullptr;
334 }
335 6 return f;
336
1/2
✓ Branch 0 taken 479 times.
✗ Branch 1 not taken.
479 } else if (version == V2FSEQ_MAJOR_VERSION) {
337
2/6
✓ Branch 0 taken 479 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 479 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
479 V2FSEQFile* f = new V2FSEQFile(fn, ct, level);
338
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 478 times.
479 if (!f->m_seqFile) {
339
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 delete f;
340 1 f = nullptr;
341 }
342 479 return f;
343 }
344 ✗ LogErr(VB_SEQUENCE, "Error creating FSEQ file (%s), unknown version %d\n", fn.c_str(), version);
345 ✗ return nullptr;
346 }
347 ✗ std::string FSEQFile::getMediaFilename(const std::string& fn) {
348 ✗ std::unique_ptr<FSEQFile> file(FSEQFile::openFSEQFile(fn));
349 ✗ if (file) {
350 ✗ return file->getMediaFilename();
351 }
352 ✗ return "";
353 ✗ }
354 ✗ std::string FSEQFile::getMediaFilename() const {
355 ✗ for (auto& a : m_variableHeaders) {
356 ✗ if (a.code[0] == 'm' && a.code[1] == 'f') {
357 ✗ const auto& data = a.getData();
358 ✗ if (data.empty()) {
359 ✗ continue;
360 }
361 ✗ const char* d = (const char*)data.data();
362 ✗ size_t len = strnlen(d, data.size());
363 ✗ if (len == 0) {
364 ✗ continue;
365 }
366 ✗ return std::string(d, len);
367 }
368 }
369 ✗ return "";
370 }
371
372 static const int FSEQ_DEFAULT_STEP_TIME = 50;
373 static const int FSEQ_VARIABLE_HEADER_SIZE = 4;
374
375 485 FSEQFile::FSEQFile(const std::string& fn) :
376 485 m_filename(fn),
377 485 m_seqNumFrames(0),
378 485 m_seqChannelCount(0),
379 485 m_seqStepTime(FSEQ_DEFAULT_STEP_TIME),
380 485 m_variableHeaders(),
381 485 m_uniqueId(0),
382 485 m_seqFileSize(0),
383 485 m_memoryBuffer(),
384 485 m_seqChanDataOffset(0),
385 485 m_memoryBufferPos(0) {
386
2/4
✓ Branch 0 taken 485 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 485 times.
485 if (fn == "-memory-") {
387 ✗ m_seqFile = nullptr;
388 ✗ m_memoryBuffer.reserve(1024 * 1024);
389 } else {
390
1/2
✓ Branch 0 taken 485 times.
✗ Branch 1 not taken.
485 m_seqFile = fopen((const char*)fn.c_str(), "wb");
391
2/2
✓ Branch 0 taken 484 times.
✓ Branch 1 taken 1 times.
485 if (m_seqFile) {
392 484 flock(fileno(m_seqFile), LOCK_EX);
393 } else {
394 // createFSEQFile() checks m_seqFile and returns nullptr; the
395 // caller is expected to handle that, so only log here.
396
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_SEQUENCE, "Error creating FSEQ file (%s), fopen returned NULL: %s\n", fn.c_str(), strerror(errno));
397 }
398 }
399 485 }
400
401 1939 void FSEQFile::dumpInfo(bool indent) {
402 1939 char ind[5] = " ";
403
2/2
✓ Branch 0 taken 1455 times.
✓ Branch 1 taken 484 times.
1939 if (!indent) {
404 1455 ind[0] = 0;
405 }
406
1/2
✓ Branch 0 taken 1939 times.
✗ Branch 1 not taken.
1939 LogDebug(VB_SEQUENCE, "%sSequence File Information\n", ind);
407
1/2
✓ Branch 0 taken 1939 times.
✗ Branch 1 not taken.
1939 LogDebug(VB_SEQUENCE, "%sseqFilename : %s\n", ind, m_filename.c_str());
408
1/2
✓ Branch 0 taken 1939 times.
✗ Branch 1 not taken.
1939 LogDebug(VB_SEQUENCE, "%sseqVersion : %d.%d\n", ind, m_seqVersionMajor, m_seqVersionMinor);
409
1/2
✓ Branch 0 taken 1939 times.
✗ Branch 1 not taken.
1939 LogDebug(VB_SEQUENCE, "%sseqChanDataOffset : %d\n", ind, m_seqChanDataOffset);
410
1/2
✓ Branch 0 taken 1939 times.
✗ Branch 1 not taken.
1939 LogDebug(VB_SEQUENCE, "%sseqChannelCount : %d\n", ind, m_seqChannelCount);
411
1/2
✓ Branch 0 taken 1939 times.
✗ Branch 1 not taken.
1939 LogDebug(VB_SEQUENCE, "%sseqNumPeriods : %d\n", ind, m_seqNumFrames);
412
1/2
✓ Branch 0 taken 1939 times.
✗ Branch 1 not taken.
1939 LogDebug(VB_SEQUENCE, "%sseqStepTime : %dms\n", ind, m_seqStepTime);
413 1939 }
414
415 29 void FSEQFile::initializeFromFSEQ(const FSEQFile& fseq) {
416 29 m_seqNumFrames = fseq.m_seqNumFrames;
417 29 m_seqChannelCount = fseq.m_seqChannelCount;
418 29 m_seqStepTime = fseq.m_seqStepTime;
419 29 m_variableHeaders = fseq.m_variableHeaders;
420 29 m_uniqueId = fseq.m_uniqueId;
421
422
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 if (fseq.getVersionMajor() >= 2) {
423
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 const V2FSEQFile* v2 = dynamic_cast<const V2FSEQFile*>(&fseq);
424
5/6
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 28 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 28 times.
29 if (v2 && !v2->m_sparseRanges.empty()) {
425
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 times.
4 for (auto& a : v2->m_sparseRanges) {
426 // File-fed: use 64-bit so a.first + a.second cannot wrap, and
427 // cap the expansion at channel space. Without the cap a crafted
428 // sparse end (3-byte fields allow ~33M) inflates m_seqChannelCount
429 // far beyond any real input buffer, turning the dense write path
430 // (write(data, getChannelCount())) into an OOB read.
431 3 uint64_t end = (uint64_t)a.first + (uint64_t)a.second;
432
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (end > MAX_CHANNEL_SPACE) {
433 ✗ end = MAX_CHANNEL_SPACE;
434 }
435 3 m_seqChannelCount = std::max(m_seqChannelCount, (uint32_t)end);
436 }
437 }
438 }
439 29 }
440
441 1455 FSEQFile::FSEQFile(const std::string& fn, FILE* file, const std::vector<uint8_t>& header) :
442 1455 m_filename(fn),
443 1455 m_seqFile(file),
444 1455 m_uniqueId(0),
445 1455 m_memoryBuffer(),
446 1455 m_memoryBufferPos(0) {
447
1/2
✓ Branch 0 taken 1455 times.
✗ Branch 1 not taken.
1455 fseeko(m_seqFile, 0L, SEEK_END);
448
1/2
✓ Branch 0 taken 1455 times.
✗ Branch 1 not taken.
1455 m_seqFileSize = ftello(m_seqFile);
449
1/2
✓ Branch 0 taken 1455 times.
✗ Branch 1 not taken.
1455 fseeko(m_seqFile, 0L, SEEK_SET);
450
451
2/2
✓ Branch 0 taken 41 times.
✓ Branch 1 taken 1414 times.
1455 if (header[0] == V1ESEQ_HEADER_IDENTIFIER) {
452 41 m_seqChanDataOffset = V1ESEQ_CHANNEL_DATA_OFFSET;
453 41 m_seqVersionMinor = V1ESEQ_MINOR_VERSION;
454 41 m_seqVersionMajor = V1ESEQ_MAJOR_VERSION;
455 41 m_seqChannelCount = read4ByteUInt(&header[8]);
456 41 m_seqStepTime = V1ESEQ_STEP_TIME;
457 // Both operands of the frame-count division come straight out of the
458 // file, and openFSEQFile() only validates the 'ESEQ' magic - so a
459 // truncated or corrupt effect file reaches here unfiltered. A zero
460 // channel count divides by zero, which on 32-bit ARM traps in
461 // __aeabi_ldiv0 and takes fppd down from StartEffect(); a file shorter
462 // than the header underflows the unsigned subtraction into a huge frame
463 // count. Report either as an empty sequence and let the caller's
464 // existing zero-frame handling deal with it.
465
3/4
✓ Branch 0 taken 39 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 39 times.
41 if (m_seqChannelCount == 0 || m_seqFileSize < (uint64_t)V1ESEQ_CHANNEL_DATA_OFFSET) {
466
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_SEQUENCE, "Invalid ESEQ file %s: %u channels, %" PRIu64 " bytes\n",
467 m_filename.c_str(), m_seqChannelCount, m_seqFileSize);
468 2 m_seqNumFrames = 0;
469 } else {
470 39 m_seqNumFrames = (m_seqFileSize - V1ESEQ_CHANNEL_DATA_OFFSET) / m_seqChannelCount;
471 }
472 } else {
473 1414 m_seqChanDataOffset = read2ByteUInt(&header[4]);
474 1414 m_seqVersionMinor = header[6];
475 1414 m_seqVersionMajor = header[7];
476 1414 m_seqChannelCount = read4ByteUInt(&header[10]);
477 1414 m_seqNumFrames = read4ByteUInt(&header[14]);
478 1414 m_seqStepTime = header[18];
479 // The step time is one unvalidated byte out of the file, and every
480 // consumer divides by it: a truncated or zero-filled header yields 0
481 // and the first "which frame are we on?" division traps (SIGFPE on
482 // x86_64, __aeabi_idiv0 on 32-bit ARM) and takes fppd down from the
483 // playlist's main loop. Clamp it here, at the single point it enters
484 // the program, rather than guarding each divide.
485
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1412 times.
1414 if (m_seqStepTime <= 0) {
486
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_SEQUENCE, "Invalid FSEQ file %s: step time %d, using %dms\n",
487 m_filename.c_str(), m_seqStepTime, FSEQ_DEFAULT_STEP_TIME);
488 2 m_seqStepTime = FSEQ_DEFAULT_STEP_TIME;
489 }
490 }
491 1455 }
492 3660 FSEQFile::~FSEQFile() {
493
2/2
✓ Branch 0 taken 1829 times.
✓ Branch 1 taken 1 times.
3660 if (m_seqFile) {
494 3658 flock(fileno(m_seqFile), LOCK_UN);
495 3658 fclose(m_seqFile);
496 }
497 3660 }
498
499 163173 int FSEQFile::seek(uint64_t location, int origin) {
500
1/2
✓ Branch 0 taken 163173 times.
✗ Branch 1 not taken.
163173 if (m_seqFile) {
501 163173 return fseeko(m_seqFile, location, origin);
502 ✗ } else if (origin == SEEK_SET) {
503 ✗ m_memoryBufferPos = location;
504 ✗ } else if (origin == SEEK_CUR) {
505 ✗ m_memoryBufferPos += location;
506 ✗ } else if (origin == SEEK_END) {
507 ✗ m_memoryBufferPos = m_memoryBuffer.size();
508 }
509 ✗ return 0;
510 }
511
512 1196 uint64_t FSEQFile::tell() {
513
1/2
✓ Branch 0 taken 1196 times.
✗ Branch 1 not taken.
1196 if (m_seqFile) {
514 1196 return ftello(m_seqFile);
515 }
516 ✗ return m_memoryBufferPos;
517 }
518
519 327454 uint64_t FSEQFile::write(const void* ptr, uint64_t size) {
520
1/2
✓ Branch 0 taken 327454 times.
✗ Branch 1 not taken.
327454 if (m_seqFile) {
521 327454 return fwrite(ptr, 1, size, m_seqFile);
522 }
523 ✗ if ((m_memoryBufferPos + size) > m_memoryBuffer.size()) {
524 ✗ m_memoryBuffer.resize(m_memoryBufferPos + size);
525 }
526 ✗ memcpy(&m_memoryBuffer[m_memoryBufferPos], ptr, size);
527 ✗ m_memoryBufferPos += size;
528 ✗ return size;
529 }
530
531 82771 uint64_t FSEQFile::read(void* ptr, uint64_t size) {
532 82771 return fread(ptr, 1, size, m_seqFile);
533 }
534
535 196 void FSEQFile::preload(uint64_t pos, uint64_t size) {
536 #ifndef PLATFORM_UNKNOWN
537 ✗ if (posix_fadvise(fileno(m_seqFile), pos, size, POSIX_FADV_WILLNEED) != 0) {
538 ✗ LogErr(VB_SEQUENCE, "Could not advise kernel %d size: %d\n", (int)pos, (int)size);
539 }
540 #endif
541 196 }
542
543 101 inline bool isRecognizedStringVariableHeader(uint8_t a, uint8_t b) {
544 // mf - media filename
545 // sp - sequence producer
546 // see https://github.com/FalconChristmas/fpp/blob/master/docs/FSEQ_Sequence_File_Format.txt#L48 for more information
547
6/8
✓ Branch 0 taken 40 times.
✓ Branch 1 taken 61 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 40 times.
✓ Branch 4 taken 45 times.
✓ Branch 5 taken 16 times.
✓ Branch 6 taken 45 times.
✗ Branch 7 not taken.
101 return (a == 'm' && b == 'f') || (a == 's' && b == 'p');
548 }
549 16 inline bool isRecognizedBinaryVariableHeader(uint8_t a, uint8_t b) {
550 // FC - FPP Commands
551 // FE - FPP Effects
552 // ED - Extended data
553 // SR - Sparse ranges past the 255 the fixed header can count
554 // XS - xLight xsq (zstd compressed binary)
555 // XN - xLight xlights_network.xml (zstd compressed binary)
556 // XR - xLight xlights_rgbeffects.xml (zstd compressed binary)
557
5/28
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 14 times.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 2 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
16 return (a == 'F' && b == 'C') || (a == 'F' && b == 'E') || (a == 'E' && b == 'D') || (a == 'X' && b == 'S') || (a == 'X' && b == 'N') || (a == 'X' && b == 'R') || (a == 'S' && b == 'R');
558 }
559 213 void FSEQFile::VariableHeader::loadData() const {
560
6/8
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 205 times.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 8 times.
✓ Branch 6 taken 205 times.
✓ Branch 7 taken 8 times.
213 if (!data.empty() || length == 0 || !fseqFile) {
561 205 return;
562 }
563
564 // offset/length come straight from the file (ED header): never allocate or
565 // read past EOF. An unchecked length is a multi-GB resize (OOM) from a tiny
566 // file, and an offset past EOF makes the read return nothing.
567 8 uint64_t fileSize = fseqFile->m_seqFileSize;
568
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (offset >= fileSize) {
569 ✗ return; // leave empty; consumers treat empty as absent
570 }
571 8 uint64_t toRead = length;
572
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (toRead > fileSize - offset) {
573 ✗ toRead = fileSize - offset;
574 }
575
576 // seek to the offset and read the data
577 8 uint64_t currentPos = fseqFile->tell();
578 8 fseqFile->seek(offset, SEEK_SET);
579 8 data.resize((size_t)toRead);
580 8 uint64_t got = fseqFile->read(data.data(), toRead);
581
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (got < toRead) {
582 ✗ data.resize((size_t)got);
583 }
584 8 fseqFile->seek(currentPos, SEEK_SET);
585 }
586
587 1414 void FSEQFile::parseVariableHeaders(const std::vector<uint8_t>& header, int readIndex) {
588 1414 const int VariableCodeSize = 2;
589 1414 const int VariableLengthSize = 2;
590
591 // when encoding, the header size is rounded to the nearest multiple of 4
592 // this comparison ensures that there is enough bytes left to at least constitute a 2 byte length + 2 byte code
593
2/2
✓ Branch 0 taken 111 times.
✓ Branch 1 taken 1412 times.
1523 while (readIndex + FSEQ_VARIABLE_HEADER_SIZE < header.size()) {
594 111 int dataLength = read2ByteUInt(&header[readIndex]);
595 111 readIndex += VariableLengthSize;
596
597 111 uint8_t code0 = header[readIndex];
598 111 uint8_t code1 = header[readIndex + 1];
599 111 readIndex += VariableCodeSize;
600
601 111 VariableHeader vheader(this);
602 111 vheader.code[0] = code0;
603 111 vheader.code[1] = code1;
604
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 111 times.
111 if (dataLength <= FSEQ_VARIABLE_HEADER_SIZE) {
605 // empty data, advance only the length of the 2 byte code
606 ✗ LogInfo(VB_SEQUENCE, "VariableHeader has 0 length data: %c%c", code0, code1);
607
3/4
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 103 times.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
111 } else if (code0 == 'E' && code1 == 'D') {
608 // The actual data is elsewhere in the file
609 // The 2-byte codes + 8-byte offset + 4-byte length below need 14
610 // bytes; a truncated ED block would otherwise memcpy past the end.
611
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (readIndex + 14 > header.size()) {
612 ✗ LogErr(VB_SEQUENCE, "VariableHeader 'ED' is truncated, %d bytes remain but 14 needed\n",
613 (int)(header.size() - readIndex));
614 ✗ return;
615 }
616 8 code0 = header[readIndex];
617 8 code1 = header[readIndex + 1];
618 8 readIndex += VariableCodeSize;
619 8 vheader.code[0] = code0;
620 8 vheader.code[1] = code1;
621 8 vheader.extendedData = true;
622
623 uint64_t offset;
624 8 memcpy(&offset, &header[readIndex], 8);
625 uint32_t len;
626 8 memcpy(&len, &header[readIndex + 8], 4);
627
628 8 vheader.setDataLocation(offset, len);
629 8 readIndex += 12;
630
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 101 times.
111 } else if (readIndex + (dataLength - FSEQ_VARIABLE_HEADER_SIZE) > header.size()) {
631 // ensure the data length is contained within the header
632 // this is primarily protection against hand modified, or corrupted, sequence files
633
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_SEQUENCE, "VariableHeader '%c%c' has out of bounds data length: %d bytes, max length: %d bytes\n",
634 code0, code1, readIndex + dataLength, header.size());
635
636 // there is no reasonable way to recover from this error - the reported dataLength is longer than possible
637 // return from parsing variable headers and let the program attempt to read the rest of the file
638 2 return;
639 } else {
640 // log when reading unrecongized variable headers
641 101 dataLength -= FSEQ_VARIABLE_HEADER_SIZE;
642
2/2
✓ Branch 0 taken 85 times.
✓ Branch 1 taken 16 times.
101 if (isRecognizedStringVariableHeader(code0, code1)) {
643 // print a warning if the data is not null terminated
644 // this is to assist debugging potential string related issues
645 // the data is not forcibly null terminated to avoid mutating unknown data
646
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 85 times.
85 if (header.size() < readIndex + dataLength) {
647 ✗ LogErr(VB_SEQUENCE, "VariableHeader %c%c data exceeds header buffer size! %d > %d\n",
648 code0, code1, (readIndex + dataLength), header.size());
649
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 83 times.
85 } else if (header[readIndex + dataLength - 1] != '\0') {
650
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_SEQUENCE, "VariableHeader %c%c data is not NULL terminated!\n", code0, code1);
651 }
652
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 } else if (!isRecognizedBinaryVariableHeader(code0, code1)) {
653 ✗ LogDebug(VB_SEQUENCE, "Unrecognized VariableHeader code: %c%c, length: %d bytes\n", code0, code1, dataLength);
654 }
655
656
1/2
✓ Branch 0 taken 101 times.
✗ Branch 1 not taken.
101 vheader.resizeData(dataLength);
657
1/2
✓ Branch 0 taken 101 times.
✗ Branch 1 not taken.
101 memcpy(vheader.getData().data(), &header[readIndex], dataLength);
658
659
2/4
✗ Branch 0 not taken.
✓ Branch 1 taken 101 times.
✓ Branch 2 taken 101 times.
✗ Branch 3 not taken.
101 LogDebug(VB_SEQUENCE, "Variable Header: Code %c%c, Length: %d, Extended: %s, ExtDataOffset: %" PRIu64 "\n", vheader.code[0], vheader.code[1], vheader.getDataLength(), vheader.extendedData ? "Yes" : "No", vheader.getExtDataOffset());
660
661 // advance the length of the data
662 // readIndex now points at the next VariableHeader's length (if any)
663 101 readIndex += dataLength;
664 }
665
1/2
✓ Branch 0 taken 109 times.
✗ Branch 1 not taken.
109 m_variableHeaders.push_back(vheader);
666
2/2
✓ Branch 0 taken 109 times.
✓ Branch 1 taken 2 times.
111 }
667 }
668 484 void FSEQFile::finalize() {
669 484 fflush(m_seqFile);
670 484 }
671
672 static const int V1FSEQ_HEADER_SIZE = 28;
673
674 6 V1FSEQFile::V1FSEQFile(const std::string& fn) :
675 FSEQFile(fn),
676 6 m_dataBlockSize(0) {
677 6 m_seqVersionMinor = V1FSEQ_MINOR_VERSION;
678 6 m_seqVersionMajor = V1FSEQ_MAJOR_VERSION;
679 6 }
680
681 6 void V1FSEQFile::writeHeader() {
682 // Cap a file-inflated count (see initializeFromFSEQ) so the header and
683 // the per-frame writes agree. No-op for valid files (<= channel space).
684
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if ((uint64_t)m_seqChannelCount > MAX_CHANNEL_SPACE) {
685 ✗ m_seqChannelCount = (uint32_t)MAX_CHANNEL_SPACE;
686 }
687 // Additional file format documentation available at:
688 // https://github.com/FalconChristmas/fpp/blob/master/docs/FSEQ_Sequence_File_Format.txt#L1
689
690 // Compute headerSize to include the header and variable headers
691 6 int headerSize = V1FSEQ_HEADER_SIZE;
692 6 headerSize += m_variableHeaders.size() * FSEQ_VARIABLE_HEADER_SIZE;
693
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 6 times.
9 for (auto& a : m_variableHeaders) {
694
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 headerSize += a.getData().size();
695 }
696
697 // Round to a product of 4 for better memory alignment
698 6 m_seqChanDataOffset = roundTo4Internal(headerSize);
699
700 // Use m_seqChanDataOffset for buffer size to avoid additional writes or buffer allocations
701 // It also comes pre-memory aligned to avoid additional padding
702 6 uint8_t* header = (uint8_t*)malloc(m_seqChanDataOffset);
703 6 memset(header, 0, m_seqChanDataOffset);
704
705 // File identifier (PSEQ) - 4 bytes
706 6 header[0] = 'P';
707 6 header[1] = 'S';
708 6 header[2] = 'E';
709 6 header[3] = 'Q';
710
711 // Channel data start offset - 2 bytes
712 6 write2ByteUInt(&header[4], m_seqChanDataOffset);
713
714 // File format version - 2 bytes
715 6 header[6] = m_seqVersionMinor;
716 6 header[7] = m_seqVersionMajor;
717
718 // Fixed header length - 2 bytes
719 // Unlike m_seqChanDataOffset this is a static value and does not include m_variableHeaders
720 6 write2ByteUInt(&header[8], V1FSEQ_HEADER_SIZE);
721 // Channel count - 4 bytes
722 6 write4ByteUInt(&header[10], m_seqChannelCount);
723 // Number of frames - 4 bytes
724 6 write4ByteUInt(&header[14], m_seqNumFrames);
725
726 // Step time in milliseconds - 1 byte
727 6 header[18] = m_seqStepTime;
728 // Flags (unused & reserved, should be 0) - 1 byte
729 6 header[19] = 0;
730
731 // Universe count (unused by fpp, should be 0) - 2 bytes
732 6 write2ByteUInt(&header[20], 0);
733 // Universe size (unused by fpp, should be 0) - 2 bytes
734 6 write2ByteUInt(&header[22], 0);
735
736 // Gamma (unused by fpp, should be 1) - 1 byte
737 6 header[24] = 1;
738 // Color order (unused by fpp, should be 2) - 1 byte
739 6 header[25] = 2;
740 // Unused and reserved field(s), should be 0 - 2 bytes
741 6 header[26] = 0;
742 6 header[27] = 0;
743
744 6 int writePos = V1FSEQ_HEADER_SIZE;
745
746 // Variable headers
747 // 4 byte size minimum (2 byte length + 2 byte code)
748
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 6 times.
9 for (auto& a : m_variableHeaders) {
749
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 auto& data = a.getData();
750 3 uint32_t len = FSEQ_VARIABLE_HEADER_SIZE + data.size();
751 3 write2ByteUInt(&header[writePos], len);
752 3 header[writePos + 2] = a.code[0];
753 3 header[writePos + 3] = a.code[1];
754
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (!data.empty()) {
755 3 memcpy(&header[writePos + 4], data.data(), data.size());
756 }
757 3 writePos += len;
758 }
759
760 // Validate final write position matches expected channel data offset
761
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (roundTo4Internal(writePos) != m_seqChanDataOffset) {
762 ✗ LogErr(VB_SEQUENCE, "Final write position (%d, roundTo4 = %d) does not match channel data offset (%d)! This means the header size failed to compute an accurate buffer size.\n", writePos, roundTo4Internal(writePos), m_seqChanDataOffset);
763 }
764
765 // Write full header at once
766 // header buffer is sized to the value of m_seqChanDataOffset, which comes padded for memory alignment
767 // If writePos extends past m_seqChanDataOffset (in error), writing m_seqChanDataOffset prevents data overflow
768 6 write(header, m_seqChanDataOffset);
769 6 free(header);
770 6 LogDebug(VB_SEQUENCE, "Setup for writing v1 FSEQ\n");
771 6 dumpInfo(true);
772 6 }
773
774 // Uncompressed channel data is frameSize bytes a frame from dataOffset, so a
775 // file cut short (an interrupted upload, a full disk) holds fewer frames than
776 // its header says. Without this every frame past the end failed its read and
777 // logged it -- a line a frame for the rest of the sequence. Returns the frame
778 // count the file can actually supply, having logged a short file once.
779 1412 static uint32_t FramesPresent(const std::string& fn, uint32_t headerFrames, uint64_t fileSize,
780 uint64_t dataOffset, uint64_t frameSize) {
781
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1410 times.
1412 if (frameSize == 0) {
782 2 return headerFrames;
783 }
784
2/2
✓ Branch 0 taken 1408 times.
✓ Branch 1 taken 2 times.
1410 uint64_t present = fileSize > dataOffset ? (fileSize - dataOffset) / frameSize : 0;
785
2/2
✓ Branch 0 taken 1409 times.
✓ Branch 1 taken 1 times.
1410 if (present >= headerFrames) {
786 1409 return headerFrames;
787 }
788 1 LogErr(VB_SEQUENCE, "FSEQ file %s is truncated: its header says %u frames but it holds %" PRIu64 ". Playing those.\n",
789 fn.c_str(), headerFrames, present);
790 1 return (uint32_t)present;
791 }
792
793 3 V1FSEQFile::V1FSEQFile(const std::string& fn, FILE* file, const std::vector<uint8_t>& header) :
794 3 FSEQFile(fn, file, header) {
795
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 parseVariableHeaders(header, V1FSEQ_HEADER_SIZE);
796
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 m_seqNumFrames = FramesPresent(fn, m_seqNumFrames, m_seqFileSize, m_seqChanDataOffset, m_seqChannelCount);
797
798 // Use the last modified time for the uniqueId
799 struct stat stats;
800 3 fstat(fileno(file), &stats);
801 3 m_uniqueId = stats.st_mtime;
802 3 }
803
804 36 V1FSEQFile::~V1FSEQFile() {
805 36 }
806
807 class UncompressedFrameData : public FSEQFile::FrameData {
808 public:
809 125137 UncompressedFrameData(uint32_t frame,
810 uint32_t sz,
811 125137 const std::vector<std::pair<uint32_t, uint32_t>>& ranges) :
812 FrameData(frame),
813
1/2
✓ Branch 0 taken 125137 times.
✗ Branch 1 not taken.
125137 m_ranges(ranges) {
814 125137 m_size = sz;
815 // Zeroed: every reader returns the frame even when a seek or read
816 // failed part-way (a truncated file), and an unzeroed buffer put
817 // whatever the heap held on the lights.
818 125137 m_data = (uint8_t*)calloc(1, sz);
819 125137 }
820 500548 virtual ~UncompressedFrameData() {
821
1/2
✓ Branch 0 taken 125137 times.
✗ Branch 1 not taken.
250274 if (m_data != nullptr) {
822 250274 free(m_data);
823 }
824 500548 }
825
826 111072 virtual bool readFrame(uint8_t* data, uint32_t maxChannels) override {
827
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 111072 times.
111072 if (m_data == nullptr)
828 ✗ return false;
829 111072 uint32_t offset = 0;
830
2/2
✓ Branch 0 taken 257974 times.
✓ Branch 1 taken 111072 times.
369046 for (auto& rng : m_ranges) {
831 257974 uint32_t toRead = rng.second;
832
1/2
✓ Branch 0 taken 257974 times.
✗ Branch 1 not taken.
257974 if ((uint64_t)offset + toRead <= m_size) {
833 // Sparse ranges come from the file (or the caller), so a start
834 // past the destination must copy nothing -- the subtraction
835 // below would otherwise underflow unsigned and memcpy off the
836 // end of the channel buffer. The source offset still advances
837 // so later ranges stay aligned.
838
2/2
✓ Branch 0 taken 257946 times.
✓ Branch 1 taken 28 times.
257974 if (rng.first < maxChannels) {
839 257946 uint32_t toCopy = std::min(toRead, maxChannels - rng.first);
840 257946 memcpy(&data[rng.first], &m_data[offset], toCopy);
841 }
842 257974 offset += toRead;
843 } else {
844 ✗ return false;
845 }
846 }
847 111072 return true;
848 }
849
850 uint32_t m_size;
851 uint8_t* m_data;
852 std::vector<std::pair<uint32_t, uint32_t>> m_ranges;
853 };
854
855 // A frame whose channel buffer could not be allocated is handed back untouched:
856 // readFrame() already reports false for a null buffer, whereas the read/memcpy
857 // paths below would write through it. malloc() failing here is not hypothetical --
858 // a channel range that starts past the end of the sequence turns m_dataBlockSize
859 // into a multi-gigabyte request, which no controller can satisfy.
860 125137 static bool frameBufferAllocated(const UncompressedFrameData* data, uint32_t frame, uint32_t sz) {
861
1/2
✓ Branch 0 taken 125137 times.
✗ Branch 1 not taken.
125137 if (data->m_data != nullptr) {
862 125137 return true;
863 }
864 ✗ LogErr(VB_SEQUENCE, "Failed to allocate %u bytes of channel data for frame %d\n", sz, (int)frame);
865 ✗ return false;
866 }
867
868 2 void V1FSEQFile::prepareRead(const std::vector<std::pair<uint32_t, uint32_t>>& ranges, uint32_t startFrame) {
869 2 m_rangesToRead.clear();
870 2 m_dataBlockSize = 0;
871
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 for (auto rng : ranges) {
872 // make sure we don't read beyond the end of the sequence data.
873 // Skip any range that starts past the channels present in the file; without
874 // this guard "m_seqChannelCount - rng.first" underflows (unsigned), producing a
875 // huge length that later overruns a buffer. Mirrors V2FSEQFile::prepareRead.
876 2 int toRead = rng.second;
877
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (rng.first < m_seqChannelCount) {
878
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if ((rng.first + toRead) > m_seqChannelCount) {
879 ✗ toRead = m_seqChannelCount - rng.first;
880 ✗ rng.second = toRead;
881 }
882 2 m_dataBlockSize += toRead;
883
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 m_rangesToRead.push_back(rng);
884 }
885 }
886
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (m_dataBlockSize == 0) {
887 ✗ m_rangesToRead.push_back(std::pair<uint32_t, uint32_t>(0, getMaxChannel()));
888 ✗ m_dataBlockSize = getMaxChannel();
889 }
890 2 FrameData* f = getFrame(startFrame);
891
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (f) {
892
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 delete f;
893 }
894 2 }
895
896 202 FrameData* V1FSEQFile::getFrame(uint32_t frame) {
897
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 202 times.
202 if (m_rangesToRead.empty()) {
898 ✗ std::vector<std::pair<uint32_t, uint32_t>> range;
899 ✗ range.push_back(std::pair<uint32_t, uint32_t>(0, m_seqChannelCount));
900 ✗ prepareRead(range, frame);
901 ✗ }
902 202 uint64_t offset = m_seqChannelCount;
903 202 offset *= frame;
904 202 offset += m_seqChanDataOffset;
905
906
2/6
✓ Branch 0 taken 202 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 202 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
202 UncompressedFrameData* data = new UncompressedFrameData(frame, m_dataBlockSize, m_rangesToRead);
907
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 202 times.
202 if (!frameBufferAllocated(data, frame, m_dataBlockSize)) {
908 ✗ return data;
909 }
910
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 202 times.
202 if (seek(offset, SEEK_SET)) {
911 ✗ LogErr(VB_SEQUENCE, "Failed to seek to proper offset for channel data for frame %d! %" PRIu64 "\n", frame, offset);
912 ✗ return data;
913 }
914 202 uint32_t sz = 0;
915 // read the ranges into the buffer
916
2/2
✓ Branch 0 taken 202 times.
✓ Branch 1 taken 202 times.
404 for (auto& rng : data->m_ranges) {
917
1/2
✓ Branch 0 taken 202 times.
✗ Branch 1 not taken.
202 if (rng.first < m_seqChannelCount) {
918 202 int toRead = rng.second;
919 202 uint64_t doffset = offset;
920 202 doffset += rng.first;
921
1/2
✓ Branch 0 taken 202 times.
✗ Branch 1 not taken.
202 seek(doffset, SEEK_SET);
922
1/2
✓ Branch 0 taken 202 times.
✗ Branch 1 not taken.
202 size_t bread = read(&data->m_data[sz], toRead);
923
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 202 times.
202 if (bread != toRead) {
924 ✗ LogErr(VB_SEQUENCE, "Failed to read channel data for frame %d! Needed to read %d but read %d\n",
925 frame, toRead, (int)bread);
926 }
927 202 sz += toRead;
928 }
929 }
930 202 return data;
931 }
932
933 3211 void V1FSEQFile::addFrame(uint32_t frame,
934 const uint8_t* data) {
935 3211 uint64_t toWrite = m_seqChannelCount;
936
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3211 times.
3211 if (toWrite > MAX_CHANNEL_SPACE) {
937 ✗ toWrite = MAX_CHANNEL_SPACE;
938 }
939 3211 write(data, toWrite);
940 3211 }
941
942 6 void V1FSEQFile::finalize() {
943 6 FSEQFile::finalize();
944 6 }
945
946 1 uint32_t V1FSEQFile::getMaxChannel() const {
947 1 return m_seqChannelCount;
948 }
949
950 static constexpr int V2FSEQ_HEADER_SIZE = 32;
951 static constexpr int V2FSEQ_SPARSE_RANGE_SIZE = 6;
952 static constexpr int V2FSEQ_COMPRESSION_BLOCK_SIZE = 8;
953 #if !defined(NO_ZLIB) || !defined(NO_ZSTD)
954 // Size of the writers' compressed output buffer, chosen from the memory
955 // available. Every writer reads it through here. It used to be a static that
956 // only the ZSTD handler's constructor ever filled in, so a process that wrote a
957 // zlib file without having built a ZSTD handler first -- fsequtils merging
958 // uncompressed or zlib inputs into zlib output -- got a 0 byte buffer, and
959 // deflate(Z_FINISH), unable to make progress, spun forever.
960 3523 static int V2OutBufferSize() {
961 55 static const int size = []() {
962 55 uint64_t pages = sysconf(_SC_PHYS_PAGES);
963 55 uint64_t page_size = sysconf(_SC_PAGESIZE);
964 55 uint64_t total_mem = pages * page_size;
965 // more than 1GB of RAM, use a bit larger buffer
966
2/2
✓ Branch 0 taken 54 times.
✓ Branch 1 taken 1 times.
55 return total_mem > (1024 * 1024 * 1024) ? 32 * 1024 * 1024 : 8 * 1024 * 1024;
967
3/4
✓ Branch 0 taken 55 times.
✓ Branch 1 taken 3468 times.
✓ Branch 2 taken 55 times.
✗ Branch 3 not taken.
3523 }();
968 3523 return size;
969 }
970 static constexpr int V2FSEQ_OUT_BUFFER_FLUSH_SIZE = 4 * 1024 * 1024; // 50% full, flush it
971 static constexpr int V2FSEQ_OUT_COMPRESSION_BLOCK_SIZE = 64 * 1024; // 64KB blocks
972 #endif
973
974 class V2Handler {
975 public:
976 1931 V2Handler(V2FSEQFile* f) :
977 1931 m_file(f) {
978 1931 m_seqChanDataOffset = f->m_seqChanDataOffset;
979 1931 }
980 3642 virtual ~V2Handler() {}
981
982 virtual uint8_t getCompressionType() = 0;
983 virtual FrameData* getFrame(uint32_t frame) = 0;
984
985 2309 virtual uint32_t computeMaxBlocks(int max = 255) { return 0; }
986 virtual void addFrame(uint32_t frame, const uint8_t* data) = 0;
987 virtual std::string GetType() const = 0;
988
989 162753 int seek(uint64_t location, int origin) {
990 162753 return m_file->seek(location, origin);
991 }
992 1188 uint64_t tell() {
993 1188 return m_file->tell();
994 }
995 323759 uint64_t write(const void* ptr, uint64_t size) {
996 323759 return m_file->write(ptr, size);
997 }
998 82561 uint64_t read(void* ptr, uint64_t size) {
999 82561 return m_file->read(ptr, size);
1000 }
1001 153 void preload(uint64_t pos, uint64_t size) {
1002 153 m_file->preload(pos, size);
1003 153 }
1004
1005 ✗ virtual void prepareRead(uint32_t frame) {}
1006
1007 478 virtual void finalize() {
1008
2/2
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 432 times.
478 if (!m_file->getVariableHeaders().empty()) {
1009
2/2
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 46 times.
92 for (int x = 0; x < m_variableHeaderOffsets.size(); x++) {
1010
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 42 times.
46 if (m_variableHeaderOffsets[x] != 0) {
1011
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 uint64_t curEnd = tell();
1012 4 auto& h = m_file->getVariableHeaders()[x];
1013
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 auto& data = h.getData();
1014
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (!data.empty()) {
1015
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 write(data.data(), data.size());
1016 }
1017
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 size_t cur = tell();
1018 4 uint64_t off = m_variableHeaderOffsets[x];
1019
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 seek(off, SEEK_SET);
1020
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 write(&curEnd, 8);
1021
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 seek(cur, SEEK_SET);
1022 }
1023 }
1024 }
1025 478 }
1026
1027 V2FSEQFile* m_file = nullptr;
1028 uint64_t m_seqChanDataOffset = 0;
1029
1030 std::vector<uint64_t> m_variableHeaderOffsets;
1031 };
1032
1033 class V2NoneCompressionHandler : public V2Handler {
1034 public:
1035 1859 V2NoneCompressionHandler(V2FSEQFile* f) :
1036 1859 V2Handler(f) {}
1037 6996 virtual ~V2NoneCompressionHandler() {}
1038
1039 450 virtual uint8_t getCompressionType() override { return 0; }
1040 ✗ virtual std::string GetType() const override { return "No Compression"; }
1041 1332 virtual void prepareRead(uint32_t frame) override {
1042 1332 FrameData* f = getFrame(frame);
1043
1/2
✓ Branch 0 taken 1332 times.
✗ Branch 1 not taken.
1332 if (f) {
1044
1/2
✓ Branch 0 taken 1332 times.
✗ Branch 1 not taken.
1332 delete f;
1045 }
1046 1332 }
1047 81151 virtual FrameData* getFrame(uint32_t frame) override {
1048
2/6
✓ Branch 0 taken 81151 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 81151 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
81151 UncompressedFrameData* data = new UncompressedFrameData(frame, m_file->m_dataBlockSize, m_file->m_rangesToRead);
1049
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 81151 times.
81151 if (!frameBufferAllocated(data, frame, m_file->m_dataBlockSize)) {
1050 ✗ return data;
1051 }
1052 81151 uint64_t offset = m_file->getChannelCount();
1053 81151 offset *= frame;
1054 81151 offset += m_seqChanDataOffset;
1055
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 81151 times.
81151 if (seek(offset, SEEK_SET)) {
1056 ✗ LogErr(VB_SEQUENCE, "Failed to seek to proper offset for channel data! %" PRIu64 "\n", offset);
1057 ✗ return data;
1058 }
1059
2/2
✓ Branch 0 taken 80128 times.
✓ Branch 1 taken 1023 times.
81151 if (m_file->m_sparseRanges.empty()) {
1060 80128 uint32_t sz = 0;
1061 // read the ranges into the buffer
1062
2/2
✓ Branch 0 taken 80149 times.
✓ Branch 1 taken 80128 times.
160277 for (auto& rng : data->m_ranges) {
1063
1/2
✓ Branch 0 taken 80149 times.
✗ Branch 1 not taken.
80149 if (rng.first < m_file->getChannelCount()) {
1064 80149 int toRead = rng.second;
1065 80149 uint64_t doffset = offset;
1066 80149 doffset += rng.first;
1067
1/2
✓ Branch 0 taken 80149 times.
✗ Branch 1 not taken.
80149 seek(doffset, SEEK_SET);
1068
1/2
✓ Branch 0 taken 80149 times.
✗ Branch 1 not taken.
80149 size_t bread = read(&data->m_data[sz], toRead);
1069
2/2
✓ Branch 0 taken 119 times.
✓ Branch 1 taken 80030 times.
80149 if (bread != toRead) {
1070
1/2
✓ Branch 0 taken 119 times.
✗ Branch 1 not taken.
119 LogErr(VB_SEQUENCE, "Failed to read channel data! Needed to read %d but read %d\n", toRead, (int)bread);
1071 }
1072 80149 sz += toRead;
1073 }
1074 }
1075 } else {
1076 1023 size_t bread = read(data->m_data, m_file->m_dataBlockSize);
1077
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1021 times.
1023 if (bread != m_file->m_dataBlockSize) {
1078 2 LogErr(VB_SEQUENCE, "Failed to read channel data! Needed to read %d but read %d\n", m_file->m_dataBlockSize, (int)bread);
1079 }
1080 }
1081 81151 return data;
1082 }
1083 206323 virtual void addFrame(uint32_t frame, const uint8_t* data) override {
1084 // Sparse positions index the caller's full input buffer, NOT the packed
1085 // channel count (getMaxChannel extends by design), so bound them to
1086 // channel space. The ceiling matches every real input buffer
1087 // (Sequence m_seqData == FPPD_MAX_CHANNELS; fsequtils buffers are sized
1088 // to match) and writeHeader() normalizes to the same ceiling, so header
1089 // and body agree. No-op for valid files, whose ranges sit far below it.
1090
2/2
✓ Branch 0 taken 202862 times.
✓ Branch 1 taken 3461 times.
206323 if (m_file->m_sparseRanges.empty()) {
1091 202862 uint64_t toWrite = m_file->getChannelCount();
1092
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 202862 times.
202862 if (toWrite > MAX_CHANNEL_SPACE) {
1093 ✗ toWrite = MAX_CHANNEL_SPACE;
1094 }
1095 202862 write(data, toWrite);
1096 } else {
1097
2/2
✓ Branch 0 taken 118582 times.
✓ Branch 1 taken 3461 times.
122043 for (auto& a : m_file->m_sparseRanges) {
1098
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 118582 times.
118582 if ((uint64_t)a.first >= MAX_CHANNEL_SPACE) {
1099 ✗ continue;
1100 }
1101 118582 uint64_t len = a.second;
1102
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 118582 times.
118582 if ((uint64_t)a.first + len > MAX_CHANNEL_SPACE) {
1103 ✗ len = MAX_CHANNEL_SPACE - a.first;
1104 }
1105
1/2
✓ Branch 0 taken 118582 times.
✗ Branch 1 not taken.
118582 write(&data[a.first], (uint32_t)len);
1106 }
1107 }
1108 206323 }
1109 };
1110 class V2CompressedHandler : public V2Handler {
1111 public:
1112 72 V2CompressedHandler(V2FSEQFile* f) :
1113 V2Handler(f),
1114 72 m_maxBlocks(0),
1115 72 m_curBlock(99999),
1116 72 m_framesPerBlock(0),
1117 72 m_curFrameInBlock(0),
1118 72 m_readThread(nullptr) {
1119
2/2
✓ Branch 0 taken 43 times.
✓ Branch 1 taken 29 times.
72 if (!m_file->m_frameOffsets.empty()) {
1120 43 m_maxBlocks = m_file->m_frameOffsets.size() - 1;
1121 }
1122 72 }
1123 144 virtual ~V2CompressedHandler() {
1124
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 60 times.
144 if (m_readThread) {
1125 24 m_readThreadRunning = false;
1126 24 m_readSignal.notify_all();
1127 24 m_readThread->join();
1128
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
24 delete m_readThread;
1129 }
1130
2/2
✓ Branch 0 taken 112 times.
✓ Branch 1 taken 72 times.
368 for (auto& a : m_blockMap) {
1131
2/2
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 66 times.
224 if (a.second) {
1132 92 free(a.second);
1133 }
1134 }
1135 144 m_blockMap.clear();
1136 144 }
1137
1138 99 virtual uint32_t computeMaxBlocks(int maxNumBlocks) override {
1139
2/2
✓ Branch 0 taken 71 times.
✓ Branch 1 taken 28 times.
99 if (m_maxBlocks > 0) {
1140 71 return m_maxBlocks;
1141 }
1142 // determine a good number of compression blocks
1143 28 uint64_t datasize = m_file->getChannelCount();
1144 28 uint64_t numFrames = m_file->getNumFrames();
1145 28 datasize *= numFrames;
1146 28 uint64_t numBlocks = datasize / V2FSEQ_OUT_COMPRESSION_BLOCK_SIZE;
1147
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 27 times.
28 if (numBlocks > maxNumBlocks) {
1148 // need a lot of blocks, use as many as we can
1149 1 numBlocks = maxNumBlocks;
1150
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 13 times.
27 } else if (numBlocks < 1) {
1151 14 numBlocks = 1;
1152 }
1153 28 m_framesPerBlock = numFrames / numBlocks;
1154
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 26 times.
28 if (m_framesPerBlock < 2)
1155 2 m_framesPerBlock = 2;
1156 28 m_curFrameInBlock = 0;
1157 28 m_curBlock = 0;
1158
1159 28 numBlocks = m_file->getNumFrames() / m_framesPerBlock + 1;
1160
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 28 times.
28 while (numBlocks > maxNumBlocks) {
1161 ✗ m_framesPerBlock++;
1162 ✗ numBlocks = m_file->getNumFrames() / m_framesPerBlock + 1;
1163 }
1164 // first block is going to be smaller, so add some blocks
1165
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 if (numBlocks < (maxNumBlocks - 1)) {
1166 28 numBlocks += 2;
1167 ✗ } else if (numBlocks < maxNumBlocks) {
1168 ✗ numBlocks++;
1169 }
1170 28 m_maxBlocks = numBlocks;
1171 28 m_curBlock = 0;
1172 28 return m_maxBlocks;
1173 }
1174
1175 28 virtual void finalize() override {
1176
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 uint64_t lastFrame = tell();
1177 28 uint64_t off = V2FSEQ_HEADER_SIZE;
1178
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 seek(off, SEEK_SET);
1179 28 int count = m_file->m_frameOffsets.size();
1180
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 28 times.
28 if (count == 0) {
1181 // not good, count should never be 0 unless no frames were written,
1182 // this really should never happen, but I've seen fseq files without the
1183 // blocks filled in so I know it DOES happen, just haven't figured out
1184 // how it's possible yet.
1185 ✗ LogErr(VB_SEQUENCE, "Error writing fseq file. No compressed blocks created.\n");
1186
1187 // we'll use the offset to the data for the 0 frame
1188 ✗ seek(0, SEEK_SET);
1189 uint8_t header[10];
1190 ✗ read(header, 10);
1191 ✗ int seqChanDataOffset = read2ByteUInt(&header[4]);
1192 ✗ seek(off, SEEK_SET);
1193 ✗ m_file->m_frameOffsets.push_back(std::pair<uint32_t, uint64_t>(0, seqChanDataOffset));
1194 ✗ count++;
1195 }
1196
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 m_file->m_frameOffsets.push_back(std::pair<uint32_t, uint64_t>(99999999, lastFrame));
1197
2/2
✓ Branch 0 taken 1152 times.
✓ Branch 1 taken 28 times.
1180 for (int x = 0; x < count; x++) {
1198 uint8_t buf[8];
1199 1152 uint32_t frame = m_file->m_frameOffsets[x].first;
1200 1152 write4ByteUInt(buf, frame);
1201
1202 1152 uint64_t len64 = m_file->m_frameOffsets[x + 1].second;
1203 1152 len64 -= m_file->m_frameOffsets[x].second;
1204 1152 uint32_t len = len64;
1205 1152 write4ByteUInt(&buf[4], len);
1206
1/2
✓ Branch 0 taken 1152 times.
✗ Branch 1 not taken.
1152 write(buf, 8);
1207 // printf("%d %d: %d\n", x, frame, len);
1208 }
1209 28 m_file->m_frameOffsets.pop_back();
1210
1211
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 seek(lastFrame, SEEK_SET);
1212
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 V2Handler::finalize();
1213 28 }
1214
1215 // True once a subclass has taken over reading entirely (see the bulk read
1216 // in V2ZSTDCompressionHandler), in which case the read-ahead thread below
1217 // would only prefetch blocks nobody ever asks getBlock() for.
1218 5 virtual bool bulkReadActive() { return false; }
1219
1220 // Index of the block holding `frame`. m_frameOffsets ends with a sentinel
1221 // entry past the last frame, but reaching that sentinel is not a bound we
1222 // can rely on: the frame number is not always ours to trust - a MultiSync
1223 // sync packet or a playlist start position can name a frame from a longer
1224 // copy of the sequence than this box holds - and the start frames it is
1225 // compared against come out of the file's own block table. The size of the
1226 // table is the bound.
1227 92 uint32_t findBlockForFrame(uint32_t frame) const {
1228 92 uint32_t block = 0;
1229 // the last entry terminates the table, so the last real block is size() - 2
1230
6/6
✓ Branch 0 taken 266 times.
✓ Branch 1 taken 17 times.
✓ Branch 2 taken 191 times.
✓ Branch 3 taken 75 times.
✓ Branch 4 taken 191 times.
✓ Branch 5 taken 92 times.
283 while ((block + 2) < m_file->m_frameOffsets.size() && frame >= m_file->m_frameOffsets[block + 1].first) {
1231 191 block++;
1232 }
1233 92 return block;
1234 }
1235
1236 32 virtual void prepareRead(uint32_t frame) override {
1237
3/4
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 18 times.
32 if (bulkReadActive()) {
1238 14 return;
1239 }
1240 // start reading the first couple blocks immediately
1241 18 int block = findBlockForFrame(frame);
1242
1243
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 LogDebug(VB_SEQUENCE, "Preparing to read starting frame: %d block: %d\n", frame, block);
1244
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 m_blocksToRead.push_back(block);
1245
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 m_blocksToRead.push_back(block + 1);
1246
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 m_blocksToRead.push_back(block + 2);
1247
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 m_blocksToRead.push_back(block + 3);
1248 18 m_firstBlock = block;
1249
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
18 if (m_readThread) {
1250 // Already reading ahead for this file. Overwriting the pointer
1251 // here would leak the previous thread, which stays joinable and
1252 // spinning for the life of the daemon; the blocks queued above are
1253 // all the existing thread needs.
1254 ✗ m_readSignal.notify_all();
1255 ✗ return;
1256 }
1257 18 m_readThreadRunning = true;
1258 try {
1259
2/4
✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
30 m_readThread = new std::thread([this]() {
1260
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 SetThreadName("FSEQReadThread");
1261
2/2
✓ Branch 0 taken 1747 times.
✓ Branch 1 taken 12 times.
1759 while (m_readThreadRunning) {
1262
1/2
✓ Branch 0 taken 1747 times.
✗ Branch 1 not taken.
1747 std::unique_lock<std::mutex> readerlock(m_readMutex);
1263
2/2
✓ Branch 0 taken 120 times.
✓ Branch 1 taken 1627 times.
1747 if (!m_blocksToRead.empty()) {
1264
1/2
✓ Branch 0 taken 120 times.
✗ Branch 1 not taken.
120 readQueuedBlock(readerlock);
1265 } else {
1266
2/4
✓ Branch 0 taken 1627 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1627 times.
✗ Branch 3 not taken.
1627 m_readSignal.wait_for(readerlock, 25ms);
1267 }
1268 1747 }
1269
3/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✓ Branch 3 taken 6 times.
36 });
1270
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 } catch (const std::system_error& e) {
1271 // Every open compressed FSEQ gets its own read-ahead thread, so a
1272 // box running a lot of FSEQ overlay effects at once can run the
1273 // process out of threads or (on 32 bit) address space, and the
1274 // thread constructor throws. The read-ahead is an optimization,
1275 // not a requirement - getBlock() reads inline when this is null -
1276 // so degrade rather than let the exception abort fppd.
1277 6 m_readThread = nullptr;
1278 6 m_readThreadRunning = false;
1279
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 LogWarn(VB_SEQUENCE, "Could not start FSEQ read ahead thread (%s), reading blocks inline\n", e.what());
1280 6 }
1281 }
1282
1283 // Compressed size of `block`: the buffer readQueuedBlock() allocates and
1284 // the input length every decoder is given. They must come from this one
1285 // place. They used to clamp separately -- readQueuedBlock() against
1286 // frames x channels in 32 bit arithmetic, which wraps for a big show, the
1287 // decoders against the same product in 64 bit or not at all -- and the
1288 // decoder read off the end of the buffer whenever they disagreed. The
1289 // product was never a bound on a compressed block anyway: a short,
1290 // noisy sequence compresses to more bytes than it has channel data, and
1291 // clamping it cut off a valid block. The header parse bounds the block
1292 // table by the file, which is the real limit.
1293 2776 uint64_t blockLength(uint32_t block) const {
1294
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2776 times.
2776 if (block + 1 >= m_file->m_frameOffsets.size()) {
1295 ✗ return 0;
1296 }
1297 2776 return m_file->m_frameOffsets[block + 1].second - m_file->m_frameOffsets[block].second;
1298 }
1299
1300 // Reads the block at the front of m_blocksToRead into m_blockMap. Called
1301 // by the read-ahead thread, and by getBlock() when that thread could not
1302 // be created. readerlock is held on entry and on return but is dropped
1303 // around the file I/O.
1304 168 void readQueuedBlock(std::unique_lock<std::mutex>& readerlock) {
1305 168 int block = m_blocksToRead.front();
1306 168 m_blocksToRead.pop_front();
1307
1/2
✓ Branch 0 taken 168 times.
✗ Branch 1 not taken.
168 uint8_t* data = m_blockMap[block];
1308
6/6
✓ Branch 0 taken 139 times.
✓ Branch 1 taken 29 times.
✓ Branch 2 taken 76 times.
✓ Branch 3 taken 63 times.
✓ Branch 4 taken 76 times.
✓ Branch 5 taken 92 times.
168 if (!data && block < (m_file->m_frameOffsets.size() - 1)) {
1309
1/2
✓ Branch 0 taken 76 times.
✗ Branch 1 not taken.
76 readerlock.unlock();
1310 76 uint64_t offset = m_file->m_frameOffsets[block].second;
1311 76 uint64_t size = blockLength(block);
1312 // At least one byte, so an empty block still reads as present
1313 // rather than as a block getBlock() must keep waiting for.
1314
2/2
✓ Branch 0 taken 74 times.
✓ Branch 1 taken 2 times.
76 data = (uint8_t*)malloc(size ? size : 1);
1315
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 76 times.
76 if (!data) {
1316 // Leave the block unread; getBlock() asks for it again.
1317 ✗ LogWarn(VB_SEQUENCE, "Could not allocate %" PRIu64 " bytes for sequence block %d\n", size, block);
1318 } else {
1319
1/2
✓ Branch 0 taken 76 times.
✗ Branch 1 not taken.
76 seek(offset, SEEK_SET);
1320
1/2
✓ Branch 0 taken 76 times.
✗ Branch 1 not taken.
76 uint64_t got = read(data, size);
1321
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 76 times.
76 if (got < size) {
1322 // Hand the decoder zeros, not whatever malloc returned.
1323 ✗ LogErr(VB_SEQUENCE, "Short read of sequence block %d: wanted %" PRIu64 " bytes at %" PRIu64 ", got %" PRIu64 "\n",
1324 block, size, offset, got);
1325 ✗ memset(data + got, 0, size - got);
1326 }
1327 }
1328
1329
1/2
✓ Branch 0 taken 76 times.
✗ Branch 1 not taken.
76 readerlock.lock();
1330
1/2
✓ Branch 0 taken 76 times.
✗ Branch 1 not taken.
76 m_blockMap[block] = data;
1331 76 m_readSignal.notify_all();
1332 }
1333 168 }
1334
1335 58 void preloadBlock(int block) {
1336
2/2
✓ Branch 0 taken 232 times.
✓ Branch 1 taken 58 times.
290 for (int b = block; b < block + 4; b++) {
1337 // let the kernel know that we'll likely need the next few blocks in the near future
1338
2/2
✓ Branch 0 taken 153 times.
✓ Branch 1 taken 79 times.
232 if (b < m_file->m_frameOffsets.size() - 1) {
1339 153 uint64_t len2 = m_file->m_frameOffsets[b + 1].second;
1340
2/2
✓ Branch 0 taken 113 times.
✓ Branch 1 taken 40 times.
153 if (b < m_file->m_frameOffsets.size() - 2) {
1341 113 len2 = m_file->m_frameOffsets[b + 2].second;
1342 }
1343 153 len2 -= m_file->m_frameOffsets[b].second;
1344 153 uint64_t pos = m_file->m_frameOffsets[b].second;
1345
1/2
✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
153 preload(pos, len2);
1346 }
1347
1/2
✓ Branch 0 taken 232 times.
✗ Branch 1 not taken.
232 std::unique_lock<std::mutex> readerlock(m_readMutex);
1348
1/2
✓ Branch 0 taken 232 times.
✗ Branch 1 not taken.
232 m_blocksToRead.push_back(b);
1349 232 m_readSignal.notify_all();
1350 232 }
1351 58 }
1352 74 uint8_t* getBlock(int block) {
1353
1/2
✓ Branch 0 taken 74 times.
✗ Branch 1 not taken.
74 std::unique_lock<std::mutex> readerlock(m_readMutex);
1354
1/2
✓ Branch 0 taken 74 times.
✗ Branch 1 not taken.
74 uint8_t* data = m_blockMap[block];
1355
2/2
✓ Branch 0 taken 56 times.
✓ Branch 1 taken 74 times.
130 while (data == nullptr) {
1356
3/6
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 48 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
56 if (m_readThread && (block > (m_firstBlock + 3)) && m_firstBlock) {
1357 // if not one of the first few blocks and it's not already
1358 // available, then something is really slow
1359 ✗ AddSlowStorageWarning();
1360 ✗ LogWarn(VB_SEQUENCE, "Data block not available when needed %d/%d. First block requested: %d. Likely slow storage.\n", block, m_maxBlocks, m_firstBlock);
1361 ✗ LogWarn(VB_SEQUENCE, "Blocks: %d First: %d\n", m_blocksToRead.size(), m_blocksToRead.empty() ? -1 : m_blocksToRead.front());
1362 }
1363
1/2
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
56 m_blocksToRead.push_front(block);
1364
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 48 times.
56 if (m_readThread) {
1365
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 m_readSignal.wait_for(readerlock, 10s);
1366 } else {
1367 // No read ahead thread, so nothing else will service the queue
1368
1/2
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
48 readQueuedBlock(readerlock);
1369
2/4
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 48 times.
48 if (m_blockMap[block] == nullptr) {
1370 // the block cannot be produced at all (past the end of the
1371 // file); wait like the read ahead path does rather than
1372 // spinning on it
1373 ✗ m_readSignal.wait_for(readerlock, 10s);
1374 }
1375 }
1376
1/2
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
56 data = m_blockMap[block];
1377 }
1378
2/2
✓ Branch 0 taken 30 times.
✓ Branch 1 taken 44 times.
74 if (block > 2) {
1379 // clean up old blocks we don't need anymore
1380
1/2
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
30 uint8_t* old = m_blockMap[block - 2];
1381
1/2
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
30 m_blockMap[block - 2] = nullptr;
1382 30 free(old);
1383 }
1384 74 return data;
1385 74 }
1386
1387 // for compressed files, this is the compression data
1388 uint32_t m_framesPerBlock;
1389 uint32_t m_curFrameInBlock;
1390 uint32_t m_curBlock;
1391 uint32_t m_maxBlocks;
1392
1393 std::atomic_bool m_readThreadRunning;
1394 std::thread* m_readThread;
1395 std::mutex m_readMutex;
1396 std::map<int, uint8_t*> m_blockMap;
1397 std::list<int> m_blocksToRead;
1398 std::condition_variable m_readSignal;
1399 int m_firstBlock = 0;
1400 };
1401
1402 #ifndef NO_ZSTD
1403 class V2ZSTDCompressionHandler : public V2CompressedHandler {
1404 public:
1405 59 V2ZSTDCompressionHandler(V2FSEQFile* f) :
1406 V2CompressedHandler(f),
1407 59 m_cctx(nullptr),
1408
1/2
✓ Branch 0 taken 59 times.
✗ Branch 1 not taken.
59 m_dctx(nullptr) {
1409 59 m_outBuffer.pos = 0;
1410
1411 59 m_outBuffer.size = V2OutBufferSize();
1412 59 m_outBuffer.dst = malloc(m_outBuffer.size);
1413
1/2
✓ Branch 0 taken 59 times.
✗ Branch 1 not taken.
59 m_outBufferCapacity = m_outBuffer.dst ? m_outBuffer.size : 0;
1414 59 m_inBuffer.src = nullptr;
1415 59 m_inBuffer.size = 0;
1416 59 m_inBuffer.pos = 0;
1417
1/2
✓ Branch 0 taken 59 times.
✗ Branch 1 not taken.
59 LogDebug(VB_SEQUENCE, " Prepared to read/write a ZSTD compress fseq file.\n");
1418 59 }
1419 236 virtual ~V2ZSTDCompressionHandler() {
1420 118 bulkShutdown();
1421 118 free(m_outBuffer.dst);
1422
2/2
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 36 times.
118 if (m_cctx) {
1423 46 ZSTD_freeCStream(m_cctx);
1424 }
1425
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 47 times.
118 if (m_dctx) {
1426 24 ZSTD_freeDStream(m_dctx);
1427 }
1428 236 }
1429 23 virtual uint8_t getCompressionType() override { return 1; }
1430 ✗ virtual std::string GetType() const override { return "Compressed ZSTD"; }
1431
1432 // ---- ReadPattern::Bulk: decompress whole blocks ahead, in parallel ----
1433 //
1434 // Each seekable block is an independent zstd frame, so blocks can be
1435 // decompressed concurrently. A caller that reads every frame front to back
1436 // (fseq conversion, or xLights loading a whole sequence) can therefore
1437 // spread the decompression across cores instead of doing it one frame at a
1438 // time on the calling thread. Playback does not: it wants the next frame
1439 // now and nothing else, which is what the streaming path below already
1440 // does, so ReadPattern::Streaming - the default - never comes near this.
1441 //
1442 // Blocks are held in a ring of slots indexed block % slots. The ring is
1443 // only ever filled over the half open range [m_bulkWindowStart,
1444 // m_bulkNextDispatch), which is never wider than the ring, so that index
1445 // cannot collide. Slot buffers and worker threads are both reused for the
1446 // life of the read: blocks are small and numerous (commonly a few frames
1447 // each, thousands per file), so allocating either per block costs far more
1448 // than the decompression itself on a slow ARM host.
1449 //
1450 // This reads its own blocks rather than going through the read-ahead thread
1451 // and getBlock(): it needs several blocks in flight at once, while getBlock
1452 // hands out one and frees the one two behind it. prepareRead() therefore
1453 // skips starting that thread when this path is active (bulkReadActive()).
1454 static constexpr uint64_t BULK_INFLIGHT_BYTE_BUDGET = 256ull * 1024 * 1024;
1455 static constexpr uint32_t NO_BLOCK = 0xFFFFFFFF;
1456 enum BulkSlotState { SLOT_EMPTY = 0,
1457 SLOT_QUEUED = 1,
1458 SLOT_DONE = 2 };
1459 struct BulkSlot {
1460 uint32_t block = NO_BLOCK;
1461 uint32_t startFrame = 0;
1462 int state = SLOT_EMPTY;
1463 std::vector<uint8_t> comp;
1464 std::vector<uint8_t> out;
1465 size_t outSize = 0;
1466 };
1467
1468 27 virtual bool bulkReadActive() override {
1469
1/2
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
27 if (!m_bulkConfigured) {
1470 27 bulkConfigure();
1471 }
1472 27 return m_bulkWorkers > 0;
1473 }
1474 27 void bulkConfigure() {
1475 27 m_bulkConfigured = true;
1476
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 14 times.
27 if (m_file->getReadPattern() != FSEQFile::ReadPattern::Bulk) {
1477 13 return;
1478 }
1479
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
14 if (m_file->m_frameOffsets.size() < 3) {
1480 ✗ return; // one real block at most - nothing to overlap
1481 }
1482 14 unsigned int hw = std::thread::hardware_concurrency();
1483
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
14 if (hw <= 1) {
1484 // Nothing to win: decompressing a block on "another" core that does
1485 // not exist just buys a thread and some context switches. Measured
1486 // as a wash on a single core BeagleBone, so stay on the simpler path.
1487 ✗ return;
1488 }
1489 // Size a slot from the largest block the file actually contains. The
1490 // uncompressed side comes from the frame counts in the header and can
1491 // never be inferred from the compressed size: pixel data routinely
1492 // expands 10:1 and an all-black block far more. Each slot also keeps
1493 // the compressed bytes it read, and neither buffer ever shrinks, so
1494 // both count against the budget.
1495 14 uint32_t maxFramesPerBlock = 0;
1496 14 uint64_t maxCompBytes = 0;
1497
2/2
✓ Branch 0 taken 1313 times.
✓ Branch 1 taken 14 times.
1327 for (size_t i = 0; i + 1 < m_file->m_frameOffsets.size(); i++) {
1498 1313 uint32_t end = m_file->m_frameOffsets[i + 1].first;
1499
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 1299 times.
1313 if (end > m_file->getNumFrames()) {
1500 14 end = m_file->getNumFrames();
1501 }
1502 1313 uint32_t start = m_file->m_frameOffsets[i].first;
1503
3/4
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 27 times.
✓ Branch 3 taken 1286 times.
1313 if (end > start && (end - start) > maxFramesPerBlock) {
1504 27 maxFramesPerBlock = end - start;
1505 }
1506 1313 uint64_t comp = blockLength(i);
1507
2/2
✓ Branch 0 taken 30 times.
✓ Branch 1 taken 1283 times.
1313 if (comp > maxCompBytes) {
1508 30 maxCompBytes = comp;
1509 }
1510 }
1511 14 uint64_t slotBytes = (uint64_t)maxFramesPerBlock * m_file->getChannelCount() + maxCompBytes;
1512
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 14 times.
14 if (maxFramesPerBlock == 0 || slotBytes == 0) {
1513 ✗ return;
1514 }
1515 // The window is (workers + 1) slots. If even two blow the budget - a
1516 // legacy file with few, huge blocks - stay on the streaming path, which
1517 // only ever buffers one.
1518
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
14 if (slotBytes * 2 > BULK_INFLIGHT_BYTE_BUDGET) {
1519 ✗ LogDebug(VB_SEQUENCE, " Bulk read declined: %" PRIu64 " bytes per block exceeds the budget.\n", slotBytes);
1520 ✗ return;
1521 }
1522 14 uint64_t byBudget = (BULK_INFLIGHT_BYTE_BUDGET / slotBytes) - 1;
1523 14 uint64_t workers = hw < byBudget ? hw : byBudget;
1524
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
14 if (workers < 1) {
1525 ✗ workers = 1;
1526 }
1527 14 m_bulkWorkers = (int)workers;
1528 14 m_bulkSlots.resize(m_bulkWorkers + 1);
1529
2/2
✓ Branch 0 taken 56 times.
✓ Branch 1 taken 14 times.
70 for (int i = 0; i < m_bulkWorkers; i++) {
1530
1/2
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
112 m_bulkPool.emplace_back([this]() { bulkWorker(); });
1531 }
1532 14 LogDebug(VB_SEQUENCE, " Bulk read: %d workers, %" PRIu64 " bytes per block.\n", m_bulkWorkers, slotBytes);
1533 }
1534 56 void bulkWorker() {
1535
1/2
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
56 SetThreadName("FSEQBulkDecomp");
1536 // One decompression stream per worker, reused for every block it takes.
1537
1/2
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
56 ZSTD_DStream* dctx = ZSTD_createDStream();
1538
1/2
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
56 std::unique_lock<std::mutex> lk(m_bulkMutex);
1539 while (true) {
1540
5/6
✓ Branch 0 taken 1369 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2682 times.
✓ Branch 3 taken 56 times.
✓ Branch 4 taken 1313 times.
✓ Branch 5 taken 1369 times.
4107 m_bulkWork.wait(lk, [this]() { return m_bulkStop || !m_bulkQueue.empty(); });
1541
2/2
✓ Branch 0 taken 56 times.
✓ Branch 1 taken 1313 times.
1369 if (m_bulkStop) {
1542 56 break;
1543 }
1544 1313 uint32_t si = m_bulkQueue.front();
1545 1313 m_bulkQueue.pop_front();
1546 1313 BulkSlot& s = m_bulkSlots[si];
1547 1313 size_t outSize = s.outSize;
1548
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
1313 lk.unlock();
1549
1550 // A block's byte range can hold more than one zstd frame, so this
1551 // decompresses as a stream and stops once the frames the block
1552 // contributes are out. One-shot ZSTD_decompress would instead
1553 // insist on room for every concatenated frame in the range.
1554
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
1313 ZSTD_initDStream(dctx);
1555 1313 ZSTD_inBuffer_s in = { s.comp.data(), s.comp.size(), 0 };
1556 1313 ZSTD_outBuffer_s out = { s.out.data(), outSize, 0 };
1557
3/4
✓ Branch 0 taken 1507 times.
✓ Branch 1 taken 1312 times.
✓ Branch 2 taken 1507 times.
✗ Branch 3 not taken.
2819 while (out.pos < outSize && in.pos < in.size) {
1558
1/2
✓ Branch 0 taken 1507 times.
✗ Branch 1 not taken.
1507 size_t r = ZSTD_decompressStream(dctx, &out, &in);
1559
3/4
✓ Branch 0 taken 1507 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1506 times.
1507 if (ZSTD_isError(r)) {
1560
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 LogErr(VB_SEQUENCE, "Failed to decompress block %d: %s\n", (int)s.block, ZSTD_getErrorName(r));
1561 1 break;
1562 }
1563
4/6
✓ Branch 0 taken 1506 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 194 times.
✓ Branch 3 taken 1312 times.
✓ Branch 4 taken 194 times.
✗ Branch 5 not taken.
1506 if (r == 0 && out.pos < outSize && in.pos < in.size) {
1564
1/2
✓ Branch 0 taken 194 times.
✗ Branch 1 not taken.
194 ZSTD_initDStream(dctx); // on to the next concatenated frame
1565 }
1566 }
1567
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1312 times.
1313 if (out.pos < outSize) {
1568 // Leave no stale bytes from whatever block used this slot last.
1569 1 memset(&s.out[out.pos], 0, outSize - out.pos);
1570 }
1571
1572
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
1313 lk.lock();
1573 1313 s.state = SLOT_DONE;
1574 1313 m_bulkDone.notify_all();
1575 1313 }
1576
1/2
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
56 ZSTD_freeDStream(dctx);
1577 56 }
1578 59 void bulkShutdown() {
1579
2/2
✓ Branch 0 taken 45 times.
✓ Branch 1 taken 14 times.
59 if (m_bulkPool.empty()) {
1580 45 return;
1581 }
1582 {
1583
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 std::unique_lock<std::mutex> lk(m_bulkMutex);
1584 14 m_bulkStop = true;
1585 14 m_bulkWork.notify_all();
1586 14 }
1587
2/2
✓ Branch 0 taken 56 times.
✓ Branch 1 taken 14 times.
70 for (auto& t : m_bulkPool) {
1588
1/2
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
56 if (t.joinable()) {
1589
1/2
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
56 t.join();
1590 }
1591 }
1592 14 m_bulkPool.clear();
1593 }
1594 // Reads the block's compressed bytes here on the calling thread, then hands
1595 // the decompression to the pool.
1596 1313 void bulkDispatch(uint32_t block) {
1597 1313 uint32_t si = block % m_bulkSlots.size();
1598 {
1599
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
1313 std::unique_lock<std::mutex> lk(m_bulkMutex);
1600
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
2626 m_bulkDone.wait(lk, [this, si]() { return m_bulkSlots[si].state == SLOT_EMPTY; });
1601 1313 }
1602 1313 BulkSlot& s = m_bulkSlots[si];
1603 1313 s.block = block;
1604 1313 s.startFrame = m_file->m_frameOffsets[block].first;
1605 1313 uint32_t endFrame = m_file->m_frameOffsets[block + 1].first;
1606
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 1299 times.
1313 if (endFrame > m_file->getNumFrames()) {
1607 14 endFrame = m_file->getNumFrames();
1608 }
1609
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
1313 uint32_t frames = endFrame > s.startFrame ? endFrame - s.startFrame : 0;
1610 1313 s.outSize = (size_t)frames * m_file->getChannelCount();
1611
2/2
✓ Branch 0 taken 83 times.
✓ Branch 1 taken 1230 times.
1313 if (s.out.size() < s.outSize) {
1612
1/2
✓ Branch 0 taken 83 times.
✗ Branch 1 not taken.
83 s.out.resize(s.outSize);
1613 }
1614
1615 1313 uint64_t offset = m_file->m_frameOffsets[block].second;
1616 1313 uint64_t len = blockLength(block);
1617
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
1313 s.comp.resize(len);
1618
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
1313 seek(offset, SEEK_SET);
1619
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
1313 uint64_t bread = read(s.comp.data(), len);
1620
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1313 times.
1313 if (bread != len) {
1621 ✗ LogErr(VB_SEQUENCE, "Failed to read block %d! Needed %" PRIu64 " but read %" PRIu64 "\n", (int)block, len, bread);
1622 ✗ s.comp.resize(bread);
1623 }
1624
1625
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
1313 std::unique_lock<std::mutex> lk(m_bulkMutex);
1626 1313 s.state = SLOT_QUEUED;
1627
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
1313 m_bulkQueue.push_back(si);
1628 1313 m_bulkWork.notify_one();
1629 1313 }
1630 // A block that was dispatched but skipped over may still be mid-decompress,
1631 // so wait for the worker to drop it before handing its buffers back out.
1632 1299 void bulkRelease(uint32_t block) {
1633 1299 uint32_t si = block % m_bulkSlots.size();
1634
1/2
✓ Branch 0 taken 1299 times.
✗ Branch 1 not taken.
1299 std::unique_lock<std::mutex> lk(m_bulkMutex);
1635
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1299 times.
1299 if (m_bulkSlots[si].block != block) {
1636 ✗ return;
1637 }
1638
1/2
✓ Branch 0 taken 1299 times.
✗ Branch 1 not taken.
2598 m_bulkDone.wait(lk, [this, si]() { return m_bulkSlots[si].state != SLOT_QUEUED; });
1639 1299 m_bulkSlots[si].state = SLOT_EMPTY;
1640 1299 m_bulkSlots[si].block = NO_BLOCK;
1641 1299 m_bulkDone.notify_all();
1642
1/2
✓ Branch 0 taken 1299 times.
✗ Branch 1 not taken.
1299 }
1643 ✗ void bulkDrain() {
1644 ✗ std::unique_lock<std::mutex> lk(m_bulkMutex);
1645 ✗ for (auto& s : m_bulkSlots) {
1646 ✗ m_bulkDone.wait(lk, [&s]() { return s.state != SLOT_QUEUED; });
1647 ✗ s.state = SLOT_EMPTY;
1648 ✗ s.block = NO_BLOCK;
1649 }
1650 ✗ m_bulkCurBlock = NO_BLOCK;
1651 ✗ }
1652 42014 BulkSlot* bulkGetBlock(uint32_t block) {
1653 42014 uint32_t maxBlock = m_file->m_frameOffsets.size() - 1;
1654
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 42014 times.
42014 if (block >= maxBlock) {
1655 ✗ return nullptr;
1656 }
1657
2/2
✓ Branch 0 taken 40701 times.
✓ Branch 1 taken 1313 times.
42014 if (block == m_bulkCurBlock) {
1658 40701 return &m_bulkSlots[block % m_bulkSlots.size()]; // still held - no locking
1659 }
1660
2/4
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1313 times.
1313 if (block < m_bulkWindowStart || block > m_bulkNextDispatch) {
1661 ✗ bulkDrain(); // seek outside the window: restart the pipeline here
1662 ✗ m_bulkWindowStart = m_bulkNextDispatch = block;
1663 }
1664
2/2
✓ Branch 0 taken 1299 times.
✓ Branch 1 taken 1313 times.
2612 while (m_bulkWindowStart < block) {
1665
1/2
✓ Branch 0 taken 1299 times.
✗ Branch 1 not taken.
1299 bulkRelease(m_bulkWindowStart);
1666 1299 m_bulkWindowStart++;
1667 }
1668 1313 uint64_t limit = (uint64_t)block + m_bulkSlots.size();
1669
2/2
✓ Branch 0 taken 56 times.
✓ Branch 1 taken 1257 times.
1313 if (limit > maxBlock) {
1670 56 limit = maxBlock;
1671 }
1672
2/2
✓ Branch 0 taken 1313 times.
✓ Branch 1 taken 1313 times.
2626 while (m_bulkNextDispatch < limit) {
1673
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
1313 bulkDispatch(m_bulkNextDispatch++);
1674 }
1675 1313 uint32_t si = block % m_bulkSlots.size();
1676
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
1313 std::unique_lock<std::mutex> lk(m_bulkMutex);
1677
3/6
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1313 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1313 times.
✗ Branch 5 not taken.
2626 m_bulkDone.wait(lk, [this, si, block]() { return m_bulkSlots[si].state == SLOT_DONE && m_bulkSlots[si].block == block; });
1678 1313 m_bulkCurBlock = block;
1679 1313 return &m_bulkSlots[si];
1680 1313 }
1681 42014 FrameData* getFrameBulk(uint32_t frame) {
1682 // Resume from the block already in hand - a bulk caller walks the frames
1683 // in order, so this is almost always either that block or the next one.
1684 42014 uint32_t block = m_bulkCurBlock;
1685
5/6
✓ Branch 0 taken 42000 times.
✓ Branch 1 taken 14 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 42000 times.
✓ Branch 4 taken 14 times.
✓ Branch 5 taken 42000 times.
42014 if (block == NO_BLOCK || frame < m_file->m_frameOffsets[block].first) {
1686 14 block = 0;
1687 }
1688
5/6
✓ Branch 0 taken 43313 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1299 times.
✓ Branch 3 taken 42014 times.
✓ Branch 4 taken 1299 times.
✓ Branch 5 taken 42014 times.
43313 while (block + 1 < m_file->m_frameOffsets.size() && frame >= m_file->m_frameOffsets[block + 1].first) {
1689 1299 block++;
1690 }
1691 42014 BulkSlot* b = bulkGetBlock(block);
1692
2/6
✓ Branch 0 taken 42014 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 42014 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
42014 UncompressedFrameData* data = new UncompressedFrameData(frame, m_file->m_dataBlockSize, m_file->m_rangesToRead);
1693
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 42014 times.
42014 if (!frameBufferAllocated(data, frame, m_file->m_dataBlockSize)) {
1694 ✗ return data;
1695 }
1696
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 42014 times.
42014 if (b == nullptr) {
1697 ✗ LogErr(VB_SEQUENCE, "Failed to get block %d for frame %d\n", (int)block, (int)frame);
1698 ✗ return data;
1699 }
1700 42014 uint64_t fidx = (uint64_t)(frame - b->startFrame) * m_file->getChannelCount();
1701
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 42014 times.
42014 if ((fidx + m_file->getChannelCount()) > b->outSize) {
1702 ✗ LogErr(VB_SEQUENCE, "Frame %d falls outside block %d\n", (int)frame, (int)block);
1703 ✗ return data;
1704 }
1705 42014 const uint8_t* fdata = b->out.data();
1706
2/2
✓ Branch 0 taken 3001 times.
✓ Branch 1 taken 39013 times.
42014 if (!m_file->m_sparseRanges.empty()) {
1707 3001 memcpy(data->m_data, &fdata[fidx], m_file->getChannelCount());
1708 } else {
1709 39013 uint32_t sz = 0;
1710 // read the ranges into the buffer
1711
2/2
✓ Branch 0 taken 42014 times.
✓ Branch 1 taken 39013 times.
81027 for (auto& rng : data->m_ranges) {
1712
1/2
✓ Branch 0 taken 42014 times.
✗ Branch 1 not taken.
42014 if (rng.first < m_file->getChannelCount()) {
1713 42014 uint64_t start = fidx + rng.first;
1714 42014 memcpy(&data->m_data[sz], &fdata[start], rng.second);
1715 42014 sz += rng.second;
1716 }
1717 }
1718 }
1719 42014 return data;
1720 }
1721
1722 43104 virtual FrameData* getFrame(uint32_t frame) override {
1723
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 43104 times.
43104 if (!m_bulkConfigured) {
1724 ✗ bulkConfigure();
1725 }
1726
2/2
✓ Branch 0 taken 42014 times.
✓ Branch 1 taken 1090 times.
43104 if (m_bulkWorkers > 0) {
1727 42014 return getFrameBulk(frame);
1728 }
1729
7/8
✓ Branch 0 taken 1078 times.
✓ Branch 1 taken 12 times.
✓ Branch 2 taken 1078 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 33 times.
✓ Branch 5 taken 1045 times.
✓ Branch 6 taken 45 times.
✓ Branch 7 taken 1045 times.
1090 if ((m_curBlock + 1) >= m_file->m_frameOffsets.size() || (frame < m_file->m_frameOffsets[m_curBlock].first) || (frame >= m_file->m_frameOffsets[m_curBlock + 1].first)) {
1730 // frame is not in the current block
1731 45 m_curBlock = findBlockForFrame(frame);
1732
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 33 times.
45 if (m_dctx == nullptr) {
1733 12 m_dctx = ZSTD_createDStream();
1734 }
1735 45 ZSTD_initDStream(m_dctx);
1736
1737 45 m_inBuffer.pos = 0;
1738 45 m_inBuffer.size = blockLength(m_curBlock);
1739
1740 45 m_inBuffer.src = getBlock(m_curBlock);
1741
1742
2/2
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 11 times.
45 if (m_curBlock < m_file->m_frameOffsets.size() - 2) {
1743 // let the kernel know that we'll likely need the next block in the near future
1744 34 preloadBlock(m_curBlock + 1);
1745 }
1746
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 33 times.
45 uint32_t blockEnd = m_file->m_frameOffsets[m_curBlock + 1].first > m_file->getNumFrames() ? m_file->getNumFrames() : m_file->m_frameOffsets[m_curBlock + 1].first;
1747 45 uint32_t blockStart = m_file->m_frameOffsets[m_curBlock].first;
1748
1/2
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
45 m_framesPerBlock = blockEnd > blockStart ? blockEnd - blockStart : 0;
1749 45 uint64_t needed = (uint64_t)m_framesPerBlock * m_file->getChannelCount();
1750
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 45 times.
45 if (needed > m_outBufferCapacity) {
1751 ✗ free(m_outBuffer.dst);
1752 ✗ m_outBuffer.dst = malloc(needed);
1753 ✗ m_outBufferCapacity = m_outBuffer.dst ? needed : 0;
1754 }
1755 45 m_outBuffer.size = 0;
1756 45 m_outBuffer.pos = 0;
1757 45 m_curFrameInBlock = 0;
1758 }
1759
2/6
✓ Branch 0 taken 1090 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1090 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
1090 UncompressedFrameData* data = new UncompressedFrameData(frame, m_file->m_dataBlockSize, m_file->m_rangesToRead);
1760
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1090 times.
1090 if (!frameBufferAllocated(data, frame, m_file->m_dataBlockSize)) {
1761 ✗ return data;
1762 }
1763
1764 1090 uint32_t blockStart = m_file->m_frameOffsets[m_curBlock].first;
1765
1/2
✓ Branch 0 taken 1090 times.
✗ Branch 1 not taken.
1090 uint64_t frameEnd = frame < blockStart ? 0 : ((uint64_t)(frame - blockStart) + 1) * m_file->getChannelCount();
1766
3/6
✓ Branch 0 taken 1090 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1090 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1090 times.
1090 if (m_outBuffer.dst == nullptr || frame < blockStart || frameEnd > m_outBufferCapacity) {
1767 // The block table did not put this frame where it said it would, or
1768 // the buffer for the block could not be allocated. Either way the
1769 // frame lies outside the decompressed block, which is what the old
1770 // "fidx < 0" test here was reaching for - but fidx is unsigned, so
1771 // it could never fire, and the read ran off the buffer instead.
1772 ✗ LogErr(VB_SEQUENCE, "Frame %d is not within block %d (frames %d to %d). Aborting frame load.\n",
1773 (int)frame, (int)m_curBlock, (int)blockStart, (int)(blockStart + m_framesPerBlock));
1774 ✗ return data;
1775 }
1776 1090 uint32_t frameInBlock = frame - blockStart;
1777
1/2
✓ Branch 0 taken 1090 times.
✗ Branch 1 not taken.
1090 if (frameInBlock >= m_curFrameInBlock) {
1778 1090 m_outBuffer.size = frameEnd;
1779 1090 size_t zr = ZSTD_decompressStream(m_dctx, &m_outBuffer, &m_inBuffer);
1780
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1090 times.
1090 if (ZSTD_isError(zr)) {
1781 // The stream is unusable after an error (zstd.h), so don't feed
1782 // it again: blank the rest of the block and treat every frame
1783 // in it as decoded. The next block re-initializes the stream.
1784 ✗ LogErr(VB_SEQUENCE, "Corrupt compressed data in sequence block %d at frame %d: %s\n",
1785 (int)m_curBlock, (int)frame, ZSTD_getErrorName(zr));
1786 ✗ uint64_t blockBytes = (uint64_t)m_framesPerBlock * m_file->getChannelCount();
1787 ✗ if (m_outBuffer.pos < blockBytes) {
1788 ✗ memset((uint8_t*)m_outBuffer.dst + m_outBuffer.pos, 0, blockBytes - m_outBuffer.pos);
1789 }
1790 ✗ m_curFrameInBlock = m_framesPerBlock;
1791 } else {
1792 1090 m_curFrameInBlock = frameInBlock + 1;
1793 }
1794 }
1795
1796 1090 uint64_t fidx = (uint64_t)frameInBlock * m_file->getChannelCount();
1797 1090 uint8_t* fdata = (uint8_t*)m_outBuffer.dst;
1798
1799
2/2
✓ Branch 0 taken 320 times.
✓ Branch 1 taken 770 times.
1090 if (!m_file->m_sparseRanges.empty()) {
1800 320 memcpy(data->m_data, &fdata[fidx], m_file->getChannelCount());
1801 } else {
1802 770 uint32_t sz = 0;
1803 // read the ranges into the buffer
1804
2/2
✓ Branch 0 taken 770 times.
✓ Branch 1 taken 770 times.
1540 for (auto& rng : data->m_ranges) {
1805
1/2
✓ Branch 0 taken 770 times.
✗ Branch 1 not taken.
770 if (rng.first < m_file->getChannelCount()) {
1806 770 uint64_t start = fidx + rng.first;
1807 770 memcpy(&data->m_data[sz], &fdata[start], rng.second);
1808 770 sz += rng.second;
1809 }
1810 }
1811 }
1812 1090 return data;
1813 }
1814 // Feeds all of `input` to the stream. A zstd error consumes nothing and
1815 // leaves the stream unusable, so stop on one rather than spin forever.
1816 75474 void compressData(ZSTD_CStream* m_cctx, ZSTD_inBuffer_s& input, ZSTD_outBuffer_s& output) {
1817
2/2
✓ Branch 0 taken 75474 times.
✓ Branch 1 taken 75454 times.
150928 while (input.pos < input.size) {
1818
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 75474 times.
75474 if (output.pos > V2FSEQ_OUT_BUFFER_FLUSH_SIZE) {
1819 ✗ write(output.dst, output.pos);
1820 ✗ output.pos = 0;
1821 }
1822 75474 size_t zr = ZSTD_compressStream2(m_cctx, &output, &input, ZSTD_e_continue);
1823
2/2
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 75454 times.
75474 if (ZSTD_isError(zr)) {
1824 20 LogErr(VB_SEQUENCE, "zstd could not compress sequence block %d: %s\n", (int)m_curBlock, ZSTD_getErrorName(zr));
1825 20 return;
1826 }
1827 }
1828 }
1829 // Ends the current zstd frame. ZSTD_compressStream2 returns the bytes
1830 // still to flush, but its error codes are also non-zero, so an unchecked
1831 // "> 0" loop never terminates on an error.
1832 1049 void endBlock() {
1833 1049 ZSTD_inBuffer_s input = {
1834 0, 0, 0
1835 };
1836 size_t zr;
1837
3/4
✓ Branch 0 taken 1049 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1047 times.
1049 while ((zr = ZSTD_compressStream2(m_cctx, &m_outBuffer, &input, ZSTD_e_end)) > 0) {
1838
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (ZSTD_isError(zr)) {
1839
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 LogErr(VB_SEQUENCE, "zstd could not finish sequence block %d: %s\n", (int)m_curBlock, ZSTD_getErrorName(zr));
1840 2 break;
1841 }
1842 ✗ write(m_outBuffer.dst, m_outBuffer.pos);
1843 ✗ m_outBuffer.pos = 0;
1844 }
1845
1/2
✓ Branch 0 taken 1049 times.
✗ Branch 1 not taken.
1049 write(m_outBuffer.dst, m_outBuffer.pos);
1846 1049 m_outBuffer.pos = 0;
1847 1049 }
1848 33492 virtual void addFrame(uint32_t frame, const uint8_t* data) override {
1849
2/2
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 33469 times.
33492 if (m_cctx == nullptr) {
1850 23 m_cctx = ZSTD_createCStream();
1851 }
1852
2/2
✓ Branch 0 taken 1049 times.
✓ Branch 1 taken 32443 times.
33492 if (m_curFrameInBlock == 0) {
1853
1/2
✓ Branch 0 taken 1049 times.
✗ Branch 1 not taken.
1049 uint64_t offset = tell();
1854 // LogDebug(VB_SEQUENCE, " Preparing to create a compressed block of data starting at frame %d, offset %" PRIu64 ".\n", frame, offset);
1855
1/2
✓ Branch 0 taken 1049 times.
✗ Branch 1 not taken.
1049 m_file->m_frameOffsets.push_back(std::pair<uint32_t, uint64_t>(frame, offset));
1856
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1045 times.
1049 int clevel = m_file->m_compressionLevel == -99 ? 2 : m_file->m_compressionLevel;
1857
2/4
✓ Branch 0 taken 1049 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1049 times.
1049 if (clevel < -25 || clevel > 25) {
1858 ✗ clevel = 2;
1859 }
1860
6/8
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 1026 times.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 23 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 23 times.
✓ Branch 7 taken 1026 times.
1049 if (frame == 0 && (ZSTD_versionNumber() > 10305)) {
1861 // first frame needs to be grabbed as fast as possible
1862 // or remotes may be off by a few frames at start. Thus,
1863 // if using recent zstd, we'll use the negative levels
1864 // for the first block so the decompression can
1865 // be as fast as possible
1866 23 clevel = -10;
1867 }
1868
3/8
✓ Branch 0 taken 1049 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1049 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 1049 times.
1049 if (ZSTD_versionNumber() <= 10305 && clevel < 0) {
1869 ✗ clevel = 0;
1870 }
1871
1/2
✓ Branch 0 taken 1049 times.
✗ Branch 1 not taken.
1049 ZSTD_initCStream(m_cctx, clevel);
1872 }
1873
1874 33492 uint8_t* curData = (uint8_t*)data;
1875
2/2
✓ Branch 0 taken 30271 times.
✓ Branch 1 taken 3221 times.
33492 if (m_file->m_sparseRanges.empty()) {
1876 30271 uint64_t toCompress = m_file->getChannelCount();
1877
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 30271 times.
30271 if (toCompress > MAX_CHANNEL_SPACE) {
1878 ✗ toCompress = MAX_CHANNEL_SPACE;
1879 }
1880 30271 ZSTD_inBuffer_s input = {
1881 curData,
1882 (size_t)toCompress,
1883 0
1884 30271 };
1885
1/2
✓ Branch 0 taken 30271 times.
✗ Branch 1 not taken.
30271 compressData(m_cctx, input, m_outBuffer);
1886 } else {
1887
2/2
✓ Branch 0 taken 45203 times.
✓ Branch 1 taken 3221 times.
48424 for (auto& a : m_file->m_sparseRanges) {
1888
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 45203 times.
45203 if ((uint64_t)a.first >= MAX_CHANNEL_SPACE) {
1889 ✗ continue;
1890 }
1891 45203 uint64_t len = a.second;
1892
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 45203 times.
45203 if ((uint64_t)a.first + len > MAX_CHANNEL_SPACE) {
1893 ✗ len = MAX_CHANNEL_SPACE - a.first;
1894 }
1895 45203 ZSTD_inBuffer_s input = {
1896 45203 &curData[a.first],
1897 (size_t)len,
1898 0
1899 45203 };
1900
1/2
✓ Branch 0 taken 45203 times.
✗ Branch 1 not taken.
45203 compressData(m_cctx, input, m_outBuffer);
1901 }
1902 }
1903
1904
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 33492 times.
33492 if (m_outBuffer.pos > V2FSEQ_OUT_BUFFER_FLUSH_SIZE) {
1905 // buffer is getting full, better flush it
1906 ✗ write(m_outBuffer.dst, m_outBuffer.pos);
1907 ✗ m_outBuffer.pos = 0;
1908 }
1909
1910 33492 m_curFrameInBlock++;
1911 // if we hit the max per block OR we're in the first block and hit frame #10
1912 // we'll start a new block. We want the first block to be small so startup is
1913 // quicker and we can get the first few frames as fast as possible.
1914
9/10
✓ Branch 0 taken 221 times.
✓ Branch 1 taken 33271 times.
✓ Branch 2 taken 199 times.
✓ Branch 3 taken 22 times.
✓ Branch 4 taken 1004 times.
✓ Branch 5 taken 32466 times.
✓ Branch 6 taken 1004 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1026 times.
✓ Branch 9 taken 32466 times.
33492 if ((m_curBlock == 0 && m_curFrameInBlock == 10) || (m_curFrameInBlock >= m_framesPerBlock && m_file->m_frameOffsets.size() < m_maxBlocks)) {
1915 1026 endBlock();
1916 // LogDebug(VB_SEQUENCE, " Finalized block of data ending at frame %d. Frames in block: %d.\n", frame, m_curFrameInBlock);
1917 1026 m_curFrameInBlock = 0;
1918 1026 m_curBlock++;
1919 }
1920 33492 }
1921 23 virtual void finalize() override {
1922
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 if (m_curFrameInBlock) {
1923 23 endBlock();
1924 23 LogDebug(VB_SEQUENCE, " Finalized last block of data. Frames in block: %d.\n", m_curFrameInBlock);
1925 23 m_curFrameInBlock = 0;
1926 23 m_curBlock++;
1927 }
1928 23 V2CompressedHandler::finalize();
1929 23 }
1930
1931 ZSTD_CCtx* m_cctx = nullptr;
1932 ZSTD_DStream* m_dctx = nullptr;
1933
1934 // Block-parallel read state (see bulkConfigure). m_bulkWorkers == 0 selects
1935 // the serial streaming path, which is what ReadPattern::Streaming - the
1936 // default, and all playback - always gets.
1937 bool m_bulkConfigured = false;
1938 int m_bulkWorkers = 0;
1939 std::vector<BulkSlot> m_bulkSlots;
1940 std::vector<std::thread> m_bulkPool;
1941 std::deque<uint32_t> m_bulkQueue; // slot indices waiting to be decompressed
1942 std::mutex m_bulkMutex;
1943 std::condition_variable m_bulkWork; // wakes the workers
1944 std::condition_variable m_bulkDone; // wakes the caller
1945 bool m_bulkStop = false;
1946 uint32_t m_bulkNextDispatch = 0;
1947 uint32_t m_bulkWindowStart = 0;
1948 uint32_t m_bulkCurBlock = NO_BLOCK;
1949 ZSTD_outBuffer_s m_outBuffer;
1950 ZSTD_inBuffer_s m_inBuffer;
1951 // What m_outBuffer.dst can actually hold. m_outBuffer.size is the limit
1952 // handed to zstd for one frame and moves with every frame read, so it
1953 // cannot answer "does this fit".
1954 size_t m_outBufferCapacity = 0;
1955 };
1956 #endif
1957
1958 #ifndef NO_ZLIB
1959 class V2ZLIBCompressionHandler : public V2CompressedHandler {
1960 public:
1961 13 V2ZLIBCompressionHandler(V2FSEQFile* f) :
1962 V2CompressedHandler(f),
1963 13 m_stream(nullptr),
1964 13 m_outBuffer(nullptr),
1965 13 m_inBuffer(nullptr) {
1966 13 }
1967 52 virtual ~V2ZLIBCompressionHandler() {
1968
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 3 times.
26 if (m_outBuffer) {
1969 20 free(m_outBuffer);
1970 }
1971 // Only a writer still holds a stream here: the read side ends and
1972 // frees its inflate stream after every block. The writer's last
1973 // block was finished by finalize() but its deflate state never ended.
1974
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 8 times.
26 if (m_stream) {
1975 10 deflateEnd(m_stream);
1976 10 free(m_stream);
1977 }
1978 52 }
1979 5 virtual uint8_t getCompressionType() override { return 2; }
1980 ✗ virtual std::string GetType() const override { return "Compressed ZLIB"; }
1981
1982 680 virtual FrameData* getFrame(uint32_t frame) override {
1983
7/8
✓ Branch 0 taken 675 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 675 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 24 times.
✓ Branch 5 taken 651 times.
✓ Branch 6 taken 29 times.
✓ Branch 7 taken 651 times.
680 if ((m_curBlock + 1) >= m_file->m_frameOffsets.size() || (frame < m_file->m_frameOffsets[m_curBlock].first) || (frame >= m_file->m_frameOffsets[m_curBlock + 1].first)) {
1984 // frame is not in the current block
1985 29 m_curBlock = findBlockForFrame(frame);
1986
1987 29 uint64_t len = blockLength(m_curBlock);
1988 29 m_inBuffer = getBlock(m_curBlock);
1989
1990
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 5 times.
29 if (m_curBlock < m_file->m_frameOffsets.size() - 2) {
1991 // let the kernel know that we'll likely need the next block in the near future
1992 24 preloadBlock(m_curBlock + 1);
1993 }
1994
1995
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 if (m_stream == nullptr) {
1996 29 m_stream = (z_stream*)calloc(1, sizeof(z_stream));
1997 29 m_stream->next_in = m_inBuffer;
1998 29 m_stream->avail_in = len;
1999 29 inflateInit(m_stream);
2000 }
2001
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 5 times.
29 if (m_outBuffer != nullptr) {
2002 24 free(m_outBuffer);
2003 }
2004
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 24 times.
29 uint32_t blockEnd = m_file->m_frameOffsets[m_curBlock + 1].first > m_file->getNumFrames() ? m_file->getNumFrames() : m_file->m_frameOffsets[m_curBlock + 1].first;
2005 29 uint32_t blockStart = m_file->m_frameOffsets[m_curBlock].first;
2006
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 uint32_t numFrames = blockEnd > blockStart ? blockEnd - blockStart : 0;
2007 29 uint64_t outsize = (uint64_t)numFrames * m_file->getChannelCount();
2008
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 m_outBuffer = outsize ? (uint8_t*)malloc(outsize) : nullptr;
2009
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 m_outBufferSize = m_outBuffer ? outsize : 0;
2010 29 m_stream->next_out = m_outBuffer;
2011 29 m_stream->avail_out = m_outBufferSize;
2012
2013
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 if (m_outBuffer != nullptr) {
2014 29 inflate(m_stream, Z_SYNC_FLUSH);
2015 }
2016 29 inflateEnd(m_stream);
2017 29 free(m_stream);
2018 29 m_stream = nullptr;
2019 }
2020
2/6
✓ Branch 0 taken 680 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 680 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
680 UncompressedFrameData* data = new UncompressedFrameData(frame, m_file->m_dataBlockSize, m_file->m_rangesToRead);
2021
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 680 times.
680 if (!frameBufferAllocated(data, frame, m_file->m_dataBlockSize)) {
2022 ✗ return data;
2023 }
2024
2025 680 uint32_t blockStart = m_file->m_frameOffsets[m_curBlock].first;
2026
1/2
✓ Branch 0 taken 680 times.
✗ Branch 1 not taken.
680 uint64_t frameEnd = frame < blockStart ? 0 : ((uint64_t)(frame - blockStart) + 1) * m_file->getChannelCount();
2027
3/6
✓ Branch 0 taken 680 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 680 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 680 times.
680 if (m_outBuffer == nullptr || frame < blockStart || frameEnd > m_outBufferSize) {
2028 // see the matching check in the ZSTD handler - the block table can
2029 // put this frame outside the block it selected
2030 ✗ LogErr(VB_SEQUENCE, "Frame %d is not within block %d (starts at frame %d). Aborting frame load.\n",
2031 (int)frame, (int)m_curBlock, (int)blockStart);
2032 ✗ return data;
2033 }
2034 680 uint64_t fidx = (uint64_t)(frame - blockStart) * m_file->getChannelCount();
2035 680 uint8_t* fdata = (uint8_t*)m_outBuffer;
2036
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 680 times.
680 if (!m_file->m_sparseRanges.empty()) {
2037 ✗ memcpy(data->m_data, &fdata[fidx], m_file->getChannelCount());
2038 } else {
2039 680 uint32_t sz = 0;
2040 // read the ranges into the buffer
2041
2/2
✓ Branch 0 taken 680 times.
✓ Branch 1 taken 680 times.
1360 for (auto& rng : data->m_ranges) {
2042
1/2
✓ Branch 0 taken 680 times.
✗ Branch 1 not taken.
680 if (rng.first < m_file->getChannelCount()) {
2043 680 memcpy(&data->m_data[sz], &fdata[fidx + rng.first], rng.second);
2044 680 sz += rng.second;
2045 }
2046 }
2047 }
2048 680 return data;
2049 }
2050 // One deflate() call stops once the output buffer is full, leaving the
2051 // rest of the input unconsumed; the next caller then points next_in
2052 // somewhere else and that remainder is silently dropped. Keep going,
2053 // flushing as needed, until all of it has been taken.
2054 3144 void deflateInput(uint8_t* in, uint32_t len) {
2055 3144 m_stream->next_in = in;
2056 3144 m_stream->avail_in = len;
2057
2/2
✓ Branch 0 taken 3144 times.
✓ Branch 1 taken 3144 times.
6288 while (m_stream->avail_in > 0) {
2058
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3144 times.
3144 if (m_stream->avail_out == 0) {
2059 ✗ write(m_outBuffer, V2OutBufferSize());
2060 ✗ m_stream->next_out = m_outBuffer;
2061 ✗ m_stream->avail_out = V2OutBufferSize();
2062 }
2063 3144 int zr = deflate(m_stream, Z_NO_FLUSH);
2064
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3144 times.
3144 if (zr != Z_OK) {
2065 ✗ LogErr(VB_SEQUENCE, "zlib could not compress sequence block %d: %d\n", (int)m_curBlock, zr);
2066 ✗ break;
2067 }
2068 }
2069 3144 }
2070 3144 virtual void addFrame(uint32_t frame, const uint8_t* data) override {
2071
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 3139 times.
3144 if (m_outBuffer == nullptr) {
2072 5 m_outBuffer = (uint8_t*)malloc(V2OutBufferSize());
2073 }
2074
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 3139 times.
3144 if (m_stream == nullptr) {
2075 5 m_stream = (z_stream*)calloc(1, sizeof(z_stream));
2076 }
2077
2/2
✓ Branch 0 taken 103 times.
✓ Branch 1 taken 3041 times.
3144 if (m_curFrameInBlock == 0) {
2078
1/2
✓ Branch 0 taken 103 times.
✗ Branch 1 not taken.
103 uint64_t offset = tell();
2079
1/2
✓ Branch 0 taken 103 times.
✗ Branch 1 not taken.
103 m_file->m_frameOffsets.push_back(std::pair<uint32_t, uint64_t>(frame, offset));
2080
1/2
✓ Branch 0 taken 103 times.
✗ Branch 1 not taken.
103 deflateEnd(m_stream);
2081 103 memset(m_stream, 0, sizeof(z_stream));
2082 }
2083
2/2
✓ Branch 0 taken 103 times.
✓ Branch 1 taken 3041 times.
3144 if (m_curFrameInBlock == 0) {
2084
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 103 times.
103 int clevel = m_file->m_compressionLevel == -99 ? 3 : m_file->m_compressionLevel;
2085
2/4
✓ Branch 0 taken 103 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 103 times.
103 if (clevel < 0 || clevel > 9) {
2086 ✗ clevel = 3;
2087 }
2088 103 deflateInit(m_stream, clevel);
2089 103 m_stream->next_out = m_outBuffer;
2090 103 m_stream->avail_out = V2OutBufferSize();
2091 }
2092
2093 3144 uint8_t* curData = (uint8_t*)data;
2094
1/2
✓ Branch 0 taken 3144 times.
✗ Branch 1 not taken.
3144 if (m_file->m_sparseRanges.empty()) {
2095 3144 uint64_t toCompress = m_file->getChannelCount();
2096
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3144 times.
3144 if (toCompress > MAX_CHANNEL_SPACE) {
2097 ✗ toCompress = MAX_CHANNEL_SPACE;
2098 }
2099 3144 deflateInput(curData, (uint32_t)toCompress);
2100 } else {
2101 ✗ for (auto& a : m_file->m_sparseRanges) {
2102 ✗ if ((uint64_t)a.first >= MAX_CHANNEL_SPACE) {
2103 ✗ continue;
2104 }
2105 ✗ uint64_t len = a.second;
2106 ✗ if ((uint64_t)a.first + len > MAX_CHANNEL_SPACE) {
2107 ✗ len = MAX_CHANNEL_SPACE - a.first;
2108 }
2109 ✗ deflateInput(&curData[a.first], (uint32_t)len);
2110 }
2111 }
2112
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3141 times.
3144 if (m_stream->avail_out < (V2OutBufferSize() - V2FSEQ_OUT_BUFFER_FLUSH_SIZE)) {
2113 // buffer is getting full, better flush it
2114 3 uint64_t sz = V2OutBufferSize();
2115 3 sz -= m_stream->avail_out;
2116 3 write(m_outBuffer, sz);
2117 3 m_stream->next_out = m_outBuffer;
2118 3 m_stream->avail_out = V2OutBufferSize();
2119 }
2120 3144 m_curFrameInBlock++;
2121 // if we hit the max per block OR we're in the first block and hit frame #10
2122 // we'll start a new block. We want the first block to be small so startup is
2123 // quicker and we can get the first few frames as fast as possible.
2124
9/10
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 3102 times.
✓ Branch 2 taken 38 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 94 times.
✓ Branch 5 taken 3046 times.
✓ Branch 6 taken 94 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 98 times.
✓ Branch 9 taken 3046 times.
3144 if ((m_curBlock == 0 && m_curFrameInBlock == 10) || (m_curFrameInBlock == m_framesPerBlock && m_file->m_frameOffsets.size() < m_maxBlocks)) {
2125 int zr;
2126
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 98 times.
98 while ((zr = deflate(m_stream, Z_FINISH)) != Z_STREAM_END) {
2127 ✗ if (zr != Z_OK) {
2128 ✗ LogErr(VB_SEQUENCE, "zlib could not finish sequence block %d: %d\n", (int)m_curBlock, zr);
2129 ✗ break;
2130 }
2131 ✗ uint64_t sz = V2OutBufferSize();
2132 ✗ sz -= m_stream->avail_out;
2133 ✗ write(m_outBuffer, sz);
2134 ✗ m_stream->next_out = m_outBuffer;
2135 ✗ m_stream->avail_out = V2OutBufferSize();
2136 }
2137 98 uint64_t sz = V2OutBufferSize();
2138 98 sz -= m_stream->avail_out;
2139 98 write(m_outBuffer, sz);
2140 98 m_stream->next_out = m_outBuffer;
2141 98 m_stream->avail_out = V2OutBufferSize();
2142
2143 98 m_curFrameInBlock = 0;
2144 98 m_curBlock++;
2145 }
2146 3144 }
2147 5 virtual void finalize() override {
2148
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 if (m_curFrameInBlock) {
2149 int zr;
2150
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 while ((zr = deflate(m_stream, Z_FINISH)) != Z_STREAM_END) {
2151 ✗ if (zr != Z_OK) {
2152 ✗ LogErr(VB_SEQUENCE, "zlib could not finish sequence block %d: %d\n", (int)m_curBlock, zr);
2153 ✗ break;
2154 }
2155 ✗ uint64_t sz = V2OutBufferSize();
2156 ✗ sz -= m_stream->avail_out;
2157 ✗ write(m_outBuffer, sz);
2158 ✗ m_stream->next_out = m_outBuffer;
2159 ✗ m_stream->avail_out = V2OutBufferSize();
2160 }
2161 5 uint64_t sz = V2OutBufferSize();
2162 5 sz -= m_stream->avail_out;
2163 5 write(m_outBuffer, sz);
2164 5 m_stream->next_out = m_outBuffer;
2165 5 m_stream->avail_out = V2OutBufferSize();
2166
2167 5 m_curFrameInBlock = 0;
2168 5 m_curBlock++;
2169 }
2170 5 V2CompressedHandler::finalize();
2171 5 }
2172
2173 z_stream* m_stream;
2174 uint8_t* m_outBuffer;
2175 // bytes m_outBuffer can hold on the read side (0 when it holds the fixed
2176 // size write buffer, which the read path never touches)
2177 uint64_t m_outBufferSize = 0;
2178 uint8_t* m_inBuffer;
2179 };
2180 #endif
2181
2182 1931 void V2FSEQFile::createHandler() {
2183
3/4
✓ Branch 0 taken 1859 times.
✓ Branch 1 taken 59 times.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
1931 switch (m_compressionType) {
2184 1859 case CompressionType::none:
2185
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1859 times.
1859 m_handler = new V2NoneCompressionHandler(this);
2186 1859 break;
2187 59 case CompressionType::zstd:
2188 #ifdef NO_ZSTD
2189 LogErr(VB_ALL, "No support for zstd compression");
2190 #else
2191
2/6
✓ Branch 0 taken 59 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 59 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
59 m_handler = new V2ZSTDCompressionHandler(this);
2192 #endif
2193 59 break;
2194 13 case CompressionType::zlib:
2195 #ifdef NO_ZLIB
2196 LogErr(VB_ALL, "No support for zlib compression");
2197 #else
2198
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 m_handler = new V2ZLIBCompressionHandler(this);
2199 #endif
2200 13 break;
2201 }
2202
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1931 times.
1931 if (m_handler == nullptr) {
2203 ✗ LogDebug(VB_SEQUENCE, "Creating a default none compression handler. %d", (int)m_compressionType);
2204 ✗ m_handler = new V2NoneCompressionHandler(this);
2205 }
2206 1931 }
2207
2208 479 V2FSEQFile::V2FSEQFile(const std::string& fn, CompressionType ct, int cl) :
2209 FSEQFile(fn),
2210 479 m_compressionType(ct),
2211 479 m_compressionLevel(cl),
2212 479 m_handler(nullptr),
2213 479 m_allowExtendedBlocks(false) {
2214 479 m_seqVersionMajor = V2FSEQ_MAJOR_VERSION;
2215 479 m_seqVersionMinor = V2FSEQ_MINOR_VERSION;
2216
2217
1/2
✓ Branch 0 taken 479 times.
✗ Branch 1 not taken.
479 createHandler();
2218 479 }
2219 9 void V2FSEQFile::fitSparseRangesToHeader(std::vector<std::pair<uint32_t, uint32_t>>& ranges) {
2220
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 4 times.
9 if (ranges.size() <= 255) {
2221 5 return;
2222 }
2223 4 std::sort(ranges.begin(), ranges.end());
2224
2/2
✓ Branch 0 taken 180 times.
✓ Branch 1 taken 4 times.
184 while (ranges.size() > 255) {
2225 180 size_t best = 1;
2226 180 uint32_t bestGap = UINT32_MAX;
2227
2/2
✓ Branch 0 taken 49860 times.
✓ Branch 1 taken 180 times.
50040 for (size_t i = 1; i < ranges.size(); ++i) {
2228 49860 uint32_t prevEnd = ranges[i - 1].first + ranges[i - 1].second;
2229
1/2
✓ Branch 0 taken 49860 times.
✗ Branch 1 not taken.
49860 uint32_t gap = ranges[i].first > prevEnd ? ranges[i].first - prevEnd : 0;
2230
2/2
✓ Branch 0 taken 180 times.
✓ Branch 1 taken 49680 times.
49860 if (gap < bestGap) {
2231 180 bestGap = gap;
2232 180 best = i;
2233 }
2234 }
2235 180 auto& prev = ranges[best - 1];
2236 180 uint32_t end = std::max(prev.first + prev.second, ranges[best].first + ranges[best].second);
2237 180 prev.second = end - prev.first;
2238
1/2
✓ Branch 0 taken 180 times.
✗ Branch 1 not taken.
180 ranges.erase(ranges.begin() + best);
2239 }
2240 }
2241
2242 478 void V2FSEQFile::writeHeader() {
2243
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 464 times.
478 if (!m_sparseRanges.empty()) {
2244 // make sure the sparse ranges fit, and then
2245 // recalculate the channel count for in the fseq.
2246 // Clamp to channel space in 64-bit as well: addFrame() enforces the
2247 // same ceiling, so normalizing here keeps the header's packed count
2248 // (sum of lens) equal to the bytes each frame actually writes.
2249 // Without this, a clamped range would leave frames short and shift
2250 // every subsequent frame in the file.
2251 14 std::vector<std::pair<uint32_t, uint32_t>> newRanges;
2252
2/2
✓ Branch 0 taken 2666 times.
✓ Branch 1 taken 14 times.
2680 for (auto& a : m_sparseRanges) {
2253 2666 uint64_t start = a.first;
2254
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2666 times.
2666 if (start >= MAX_CHANNEL_SPACE) {
2255 ✗ continue;
2256 }
2257 2666 uint64_t len = a.second;
2258
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2666 times.
2666 if (start + len > MAX_CHANNEL_SPACE) {
2259 ✗ len = MAX_CHANNEL_SPACE - start;
2260 }
2261
1/2
✓ Branch 0 taken 2666 times.
✗ Branch 1 not taken.
2666 if (start < m_seqChannelCount) {
2262
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2666 times.
2666 if (start + len > m_seqChannelCount) {
2263 ✗ len = (uint64_t)m_seqChannelCount - start;
2264 }
2265
1/2
✓ Branch 0 taken 2666 times.
✗ Branch 1 not taken.
2666 newRanges.push_back(std::pair<uint32_t, uint32_t>((uint32_t)start, (uint32_t)len));
2266 }
2267 }
2268
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 m_sparseRanges = newRanges;
2269
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 5 times.
14 if (m_seqVersionMinor < 3) {
2270
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 fitSparseRangesToHeader(m_sparseRanges);
2271 }
2272
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 if (!m_sparseRanges.empty()) {
2273 14 m_seqChannelCount = 0;
2274
2/2
✓ Branch 0 taken 2486 times.
✓ Branch 1 taken 14 times.
2500 for (auto& a : m_sparseRanges) {
2275 2486 m_seqChannelCount += a.second;
2276 }
2277 }
2278
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 463 times.
478 } else if ((uint64_t)m_seqChannelCount > MAX_CHANNEL_SPACE) {
2279 // Dense output with a file-inflated count (see initializeFromFSEQ):
2280 // cap it so the header and the per-frame writes agree.
2281 1 m_seqChannelCount = (uint32_t)MAX_CHANNEL_SPACE;
2282 }
2283
2284 // Additional file format documentation available at:
2285 // https://github.com/FalconChristmas/fpp/blob/master/docs/FSEQ_Sequence_File_Format.txt#L17
2286
2287
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 469 times.
478 uint16_t maxBlocks = m_handler->computeMaxBlocks(m_allowExtendedBlocks ? 4095 : 255) & 0xFFF;
2288
2289 // A table longer than the header can count goes whole into an 'SR' block. The
2290 // header keeps the first 255 so readers that predate 'SR' still play those channels.
2291
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 42 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
520 std::erase_if(m_variableHeaders, [](const VariableHeader& vh) { return vh.code[0] == 'S' && vh.code[1] == 'R'; });
2292 478 size_t headerRanges = m_sparseRanges.size();
2293
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 474 times.
478 if (headerRanges > 255) {
2294 4 headerRanges = 255;
2295 4 VariableHeader sr;
2296 4 sr.code[0] = 'S';
2297 4 sr.code[1] = 'R';
2298 4 sr.extendedData = true;
2299
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 sr.resizeData(m_sparseRanges.size() * V2FSEQ_SPARSE_RANGE_SIZE);
2300
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 auto& data = sr.getData();
2301
2/2
✓ Branch 0 taken 1200 times.
✓ Branch 1 taken 4 times.
1204 for (size_t i = 0; i < m_sparseRanges.size(); i++) {
2302 1200 write3ByteUInt(&data[i * V2FSEQ_SPARSE_RANGE_SIZE], m_sparseRanges[i].first);
2303 1200 write3ByteUInt(&data[i * V2FSEQ_SPARSE_RANGE_SIZE + 3], m_sparseRanges[i].second);
2304 }
2305
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 m_variableHeaders.push_back(sr);
2306 4 }
2307
2308 // Compute headerSize to include the header, compression blocks and sparse ranges
2309 478 int headerSize = V2FSEQ_HEADER_SIZE;
2310 478 headerSize += maxBlocks * V2FSEQ_COMPRESSION_BLOCK_SIZE;
2311 478 headerSize += headerRanges * V2FSEQ_SPARSE_RANGE_SIZE;
2312
2313 // Channel data offset is the headerSize plus size of variable headers
2314 // Round to a product of 4 for better memory alignment
2315 478 m_seqChanDataOffset = headerSize;
2316 478 uint64_t seqChanDataOffset2 = headerSize;
2317
2/2
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 478 times.
524 for (auto& a : m_variableHeaders) {
2318 46 uint32_t sze = a.getDataLength() + FSEQ_VARIABLE_HEADER_SIZE;
2319
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 42 times.
46 if (a.extendedData) {
2320 4 seqChanDataOffset2 += FSEQ_VARIABLE_HEADER_SIZE + 14;
2321 4 m_seqChanDataOffset += FSEQ_VARIABLE_HEADER_SIZE + 14;
2322
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 } else if (sze <= (14 + FSEQ_VARIABLE_HEADER_SIZE)) {
2323 // smaller than the extended data type so would never be output
2324 // as extended data
2325 42 m_seqChanDataOffset += sze;
2326 42 seqChanDataOffset2 += sze; // smaller than the shifted size
2327 } else {
2328 // could potentially be placed in the extended area if the header is too large
2329 // record both sizes to compare later
2330 ✗ m_seqChanDataOffset += sze;
2331 ✗ seqChanDataOffset2 += FSEQ_VARIABLE_HEADER_SIZE + 14; // 64bit offset into file, 32bit length, new 2byte code
2332 }
2333 }
2334 478 bool forceExtended = false;
2335
3/4
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 471 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
478 if (m_seqVersionMinor >= 2 && (m_seqChanDataOffset >= 0xFFFF)) {
2336 // beyond the byte header size. The variable header data will need to be stored elsewhere
2337 ✗ m_seqChanDataOffset = seqChanDataOffset2;
2338 ✗ forceExtended = true;
2339 }
2340 478 m_seqChanDataOffset = roundTo4Internal(m_seqChanDataOffset);
2341
2342 // Use m_seqChanDataOffset for buffer size to avoid additional writes or buffer allocations
2343 // It also comes pre-memory aligned to avoid adding padding
2344 // uint8_t header[m_seqChanDataOffset]; // MSW does not support non compile time constant array sizes
2345 478 uint8_t* header = (uint8_t*)malloc(m_seqChanDataOffset);
2346 478 memset(header, 0, m_seqChanDataOffset);
2347
2348 // File identifier (PSEQ) - 4 bytes
2349 478 header[0] = 'P';
2350 478 header[1] = 'S';
2351 478 header[2] = 'E';
2352 478 header[3] = 'Q';
2353
2354 // Channel data start offset - 2 bytes
2355 478 write2ByteUInt(&header[4], m_seqChanDataOffset);
2356
2357 // File format version - 2 bytes
2358 // 2.3 only changes how the sparse ranges are stored, so a file that doesn't
2359 // need it is marked 2.2 and older readers take it without complaint.
2360
4/4
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 473 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 1 times.
478 header[6] = (m_seqVersionMinor >= 3 && headerRanges == m_sparseRanges.size()) ? 2 : m_seqVersionMinor;
2361 478 header[7] = m_seqVersionMajor;
2362
2363 // Computed header length - 2 bytes
2364 478 write2ByteUInt(&header[8], headerSize);
2365 // Channel count - 4 bytes
2366 478 write4ByteUInt(&header[10], m_seqChannelCount);
2367 // Number of frames - 4 bytes
2368 478 write4ByteUInt(&header[14], m_seqNumFrames);
2369
2370 // Step time in milliseconds - 1 byte
2371 478 header[18] = m_seqStepTime;
2372 // Flags (unused & reserved, should be 0) - 1 byte
2373 478 header[19] = 0;
2374 // Compression type and upper 4 bits of max blocks- 1 byte
2375 478 header[20] = ((maxBlocks >> 4) & 0xF0) | m_handler->getCompressionType();
2376 // Number of blocks in compressed channel data (should be 0 if not compressed) - 1 byte, lower 8 bits
2377 478 header[21] = maxBlocks & 0xFF;
2378 // Number of ranges in sparse range index - 1 byte
2379 478 header[22] = headerRanges;
2380 // Flags (unused & reserved, should be 0) - 1 byte
2381 478 header[23] = 0;
2382
2383 // Timestamp based UUID - 8 bytes
2384
2/2
✓ Branch 0 taken 451 times.
✓ Branch 1 taken 27 times.
478 if (m_uniqueId == 0) {
2385 451 m_uniqueId = GetTime();
2386 }
2387 478 memcpy(&header[24], &m_uniqueId, sizeof(m_uniqueId));
2388
2389 478 int writePos = V2FSEQ_HEADER_SIZE;
2390
2391 // Empty compression blocks are automatically added when calculating headerSize (see maxBlocks)
2392 // Their data is initialized to 0 by memset and computed later
2393 478 writePos += maxBlocks * V2FSEQ_COMPRESSION_BLOCK_SIZE;
2394
2395 // Sparse ranges
2396 // 6 byte size (3 byte value + 3 byte value)
2397
2/2
✓ Branch 0 taken 2306 times.
✓ Branch 1 taken 478 times.
2784 for (size_t i = 0; i < headerRanges; i++) {
2398 2306 write3ByteUInt(&header[writePos], m_sparseRanges[i].first);
2399 2306 write3ByteUInt(&header[writePos + 3], m_sparseRanges[i].second);
2400 2306 writePos += V2FSEQ_SPARSE_RANGE_SIZE;
2401 }
2402
2403 // Variable headers
2404 // 4 byte size minimum (2 byte length + 2 byte code)
2405 478 int idx = 0;
2406 478 m_handler->m_variableHeaderOffsets.resize(m_variableHeaders.size());
2407
2/2
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 478 times.
524 for (auto& a : m_variableHeaders) {
2408 46 uint32_t len = FSEQ_VARIABLE_HEADER_SIZE + a.getDataLength();
2409 46 bool doExtended = a.extendedData;
2410
3/6
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 42 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
46 if (!doExtended && forceExtended && len > 18) {
2411 // longer than the extended header and we need to save space
2412 ✗ doExtended = true;
2413 }
2414
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 42 times.
46 if (doExtended) {
2415 4 len = 14 + FSEQ_VARIABLE_HEADER_SIZE;
2416 }
2417 46 write2ByteUInt(&header[writePos], len);
2418 46 writePos += 2;
2419
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 42 times.
46 if (doExtended) {
2420 4 header[writePos] = 'E';
2421 4 header[writePos + 1] = 'D';
2422 4 writePos += 2;
2423 }
2424 46 header[writePos] = a.code[0];
2425 46 header[writePos + 1] = a.code[1];
2426 46 writePos += 2;
2427
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 42 times.
46 if (doExtended) {
2428 4 memset(&header[writePos], 0, 8);
2429 4 m_handler->m_variableHeaderOffsets[idx] = writePos;
2430 4 writePos += 8; // file position
2431 4 write4ByteUInt(&header[writePos], a.getDataLength());
2432 4 writePos += 4;
2433 } else {
2434
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 auto& data = a.getData();
2435 42 m_handler->m_variableHeaderOffsets[idx] = 0;
2436
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 if (!data.empty()) {
2437 42 memcpy(&header[writePos], data.data(), data.size());
2438 }
2439 42 writePos += data.size();
2440 }
2441 46 ++idx;
2442 }
2443
2444 // Validate final write position matches expected channel data offset
2445
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 478 times.
478 if (roundTo4Internal(writePos) != m_seqChanDataOffset) {
2446 ✗ LogErr(VB_SEQUENCE, "Final write position (%d, roundTo4 = %d) does not match channel data offset (%d)! This means the header size failed to compute an accurate buffer size.\n", writePos, roundTo4Internal(writePos), m_seqChanDataOffset);
2447 }
2448
2449 // Write full header at once
2450 // header buffer is sized to the value of m_seqChanDataOffset, which comes padded for memory alignment
2451 // If writePos extends past m_seqChanDataOffset (in error), writing m_seqChanDataOffset prevents data overflow
2452 478 write(header, m_seqChanDataOffset);
2453
2454 478 free(header);
2455
2456 478 LogDebug(VB_SEQUENCE, "Setup for writing v2 FSEQ\n");
2457 478 dumpInfo(true);
2458 478 }
2459
2460 1452 V2FSEQFile::V2FSEQFile(const std::string& fn, FILE* file, const std::vector<uint8_t>& header) :
2461 FSEQFile(fn, file, header),
2462 1452 m_compressionType(none),
2463 1452 m_handler(nullptr) {
2464
2/4
✓ Branch 0 taken 1452 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1452 times.
1452 if (m_seqVersionMajor == 2 && m_seqVersionMinor > 3) {
2465 ✗ LogErr(VB_SEQUENCE, "Unknown minor version: %d. FSEQ may not load properly.\n", m_seqVersionMinor);
2466 }
2467
2468
2/2
✓ Branch 0 taken 41 times.
✓ Branch 1 taken 1411 times.
1452 if (header[0] == V1ESEQ_HEADER_IDENTIFIER) {
2469 41 m_compressionType = CompressionType::none;
2470
2471 41 uint32_t modelStart = read4ByteUInt(&header[12]);
2472 41 uint32_t modelLen = read4ByteUInt(&header[16]);
2473
2474 // ESEQ files use 1 based start channels, offset to start at 0
2475
2/4
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✗ Branch 3 not taken.
41 m_sparseRanges.push_back(std::pair<uint32_t, uint32_t>(modelStart ? modelStart - 1 : modelStart, modelLen));
2476 } else {
2477
3/4
✓ Branch 0 taken 1368 times.
✓ Branch 1 taken 35 times.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
1411 switch (header[20] & 0xF) {
2478 1368 case 0:
2479 1368 m_compressionType = CompressionType::none;
2480 1368 break;
2481 35 case 1:
2482 35 m_compressionType = CompressionType::zstd;
2483 35 break;
2484 8 case 2:
2485 8 m_compressionType = CompressionType::zlib;
2486 8 break;
2487 ✗ default:
2488 ✗ LogErr(VB_SEQUENCE, "Unknown compression type: %d\n", (int)header[20]);
2489 }
2490
2491 // readPos tracks the reader index for variable length data past the fixed header size
2492 // This is used to check for reader index overflows
2493 1411 int readPos = V2FSEQ_HEADER_SIZE;
2494
2495 // Read compression blocks
2496 // 8 byte size each (4 byte firstFrame + 4 byte length)
2497 // header[21] is the "max blocks count" field, lower 8 bits
2498 // header[20] & 0xF0 is the upper 4 bits, total 12 bits
2499 // for maximum of 4095 blocks
2500 1411 uint64_t lastBlockOffset = m_seqChanDataOffset;
2501 1411 uint32_t numBlocks = header[20];
2502 1411 numBlocks &= 0xF0;
2503 1411 numBlocks <<= 4;
2504 1411 numBlocks |= header[21];
2505
2506 // The block and sparse range counts are file data too; don't let them
2507 // walk readPos past the header buffer.
2508
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1409 times.
1411 if ((uint64_t)readPos + (uint64_t)numBlocks * V2FSEQ_COMPRESSION_BLOCK_SIZE > header.size()) {
2509 2 uint32_t fits = (header.size() - readPos) / V2FSEQ_COMPRESSION_BLOCK_SIZE;
2510
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_SEQUENCE, "FSEQ header claims %d compression blocks but only has room for %d. File is truncated or corrupt.\n",
2511 (int)numBlocks, (int)fits);
2512 2 numBlocks = fits;
2513 }
2514
2515 1411 uint32_t lastFirstFrame = 0;
2516
2/2
✓ Branch 0 taken 1733 times.
✓ Branch 1 taken 1411 times.
3144 for (uint32_t i = 0; i < numBlocks; i++) {
2517 1733 uint32_t firstFrame = read4ByteUInt(&header[readPos]);
2518 1733 uint64_t length = read4ByteUInt(&header[readPos + 4]);
2519
2520
2/2
✓ Branch 0 taken 1694 times.
✓ Branch 1 taken 39 times.
1733 if (length > 0) {
2521 // These start frames are file data. Every reader indexes a
2522 // block as (frame - block start) and searches the table by
2523 // walking forward, so a table that is not in order - the
2524 // writer's own finalize() has seen files whose block table was
2525 // never filled in - drives that index off the front of the
2526 // decompressed block. Hold the ordering here, where the table
2527 // is built, rather than at each of the places that trust it.
2528
5/8
✓ Branch 0 taken 1694 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 43 times.
✓ Branch 3 taken 1651 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 43 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 1694 times.
1694 if (firstFrame < lastFirstFrame || (m_frameOffsets.empty() && firstFrame != 0)) {
2529 ✗ LogErr(VB_SEQUENCE, "FSEQ block %d claims to start at frame %d, out of order with the block before it at %d. Block table is corrupt.\n",
2530 (int)i, (int)firstFrame, (int)lastFirstFrame);
2531 ✗ firstFrame = lastFirstFrame;
2532 }
2533
1/2
✓ Branch 0 taken 1694 times.
✗ Branch 1 not taken.
1694 m_frameOffsets.push_back(std::pair<uint32_t, uint64_t>(firstFrame, lastBlockOffset));
2534 1694 lastFirstFrame = firstFrame;
2535 1694 lastBlockOffset += length;
2536 }
2537
2538 1733 readPos += V2FSEQ_COMPRESSION_BLOCK_SIZE;
2539
2540 // Duplicated legacy behavior
2541 // Preloads up to [length] bytes starting at m_seqChanDataOffset
2542 // This pre-buffers the first compression block
2543
2/2
✓ Branch 0 taken 43 times.
✓ Branch 1 taken 1690 times.
1733 if (i == 0) {
2544
1/2
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
43 preload(m_seqChanDataOffset, length);
2545 }
2546 }
2547
2548 // The block lengths read above sum to exactly the channel data, so once
2549 // every block has been seen lastBlockOffset is the end of the channel
2550 // data. Anything the writer put after it (extended variable headers,
2551 // embedded files) is not part of any block.
2552 1411 const bool haveBlocks = !m_frameOffsets.empty();
2553 1411 uint64_t chanDataEnd = lastBlockOffset;
2554
2555
2/2
✓ Branch 0 taken 1368 times.
✓ Branch 1 taken 43 times.
1411 if (m_compressionType == CompressionType::none) {
2556 // Push frame offsets that cover the entire file length given the channel data is effectively a single block
2557 // For uncompressed blocks, maxBlocks should always be 0 and m_frameOffsets initially empty
2558
1/2
✓ Branch 0 taken 1368 times.
✗ Branch 1 not taken.
1368 m_frameOffsets.push_back(std::pair<uint32_t, uint64_t>(0, m_seqChanDataOffset));
2559
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 43 times.
43 } else if (m_frameOffsets.size() == 0) {
2560 ✗ LogErr(VB_SEQUENCE, "FSEQ file corrupt: did not load any block references from header.\n");
2561
2562 // File is flagged as compressed but no compression blocks were read
2563 // The file is likely corrupted, read the full channel data as a single block as a recovery attempt
2564 ✗ m_frameOffsets.push_back(std::pair<uint32_t, uint64_t>(0, m_seqChanDataOffset));
2565 }
2566
2567 // Push a final frame offset that terminates the last block. Without a
2568 // usable block table (uncompressed, or the corrupt-file recovery above)
2569 // fall back to the file size so the last block still covers the data.
2570
5/6
✓ Branch 0 taken 43 times.
✓ Branch 1 taken 1368 times.
✓ Branch 2 taken 39 times.
✓ Branch 3 taken 4 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 39 times.
1411 if (!haveBlocks || chanDataEnd > m_seqFileSize || chanDataEnd <= m_seqChanDataOffset) {
2571 1372 chanDataEnd = m_seqFileSize;
2572 }
2573 // Every reader takes a block's length as the next block's offset minus
2574 // its own, so no block may start past the end of the data. A table
2575 // claiming more bytes than the file holds -- truncated file, corrupt
2576 // table -- otherwise leaves the later blocks past chanDataEnd and their
2577 // lengths underflow to nearly 2^64. Clamp here, where the table is
2578 // built, so the offsets are non-decreasing and bounded by the file for
2579 // every reader.
2580 1411 bool clamped = false;
2581
2/2
✓ Branch 0 taken 3062 times.
✓ Branch 1 taken 1411 times.
4473 for (auto& fo : m_frameOffsets) {
2582
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3060 times.
3062 if (fo.second > chanDataEnd) {
2583 2 fo.second = chanDataEnd;
2584 2 clamped = true;
2585 }
2586 }
2587
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1409 times.
1411 if (clamped) {
2588
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_SEQUENCE, "FSEQ block table runs past the end of the channel data (%" PRIu64 " bytes). File is truncated or corrupt.\n",
2589 chanDataEnd);
2590 }
2591
1/2
✓ Branch 0 taken 1411 times.
✗ Branch 1 not taken.
1411 m_frameOffsets.push_back(std::pair<uint32_t, uint64_t>(getNumFrames() + 2, chanDataEnd));
2592
2593 // Read sparse ranges
2594 // 6 byte size each (3 byte firstChannel + 3 byte length)
2595 // header[22] is the "sparse range count" field
2596 1411 int numRanges = header[22];
2597 // Writers before 2.3 stored tables of more than 255 ranges with only the low
2598 // byte of the count. Take the count implied by the header size (bytes 8-9) when
2599 // its low byte matches, the table fits in the header, and the ranges add up to
2600 // the channel count.
2601 1411 int tableEnd = read2ByteUInt(&header[8]);
2602
5/6
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1410 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 1410 times.
1412 if (tableEnd > readPos + numRanges * V2FSEQ_SPARSE_RANGE_SIZE && tableEnd <= (int)header.size() &&
2603
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 (tableEnd - readPos) % V2FSEQ_SPARSE_RANGE_SIZE == 0) {
2604 1 int fullRanges = (tableEnd - readPos) / V2FSEQ_SPARSE_RANGE_SIZE;
2605 1 uint64_t total = 0;
2606
2/2
✓ Branch 0 taken 300 times.
✓ Branch 1 taken 1 times.
301 for (int i = 0; i < fullRanges; i++) {
2607 300 total += read3ByteUInt(&header[readPos + i * V2FSEQ_SPARSE_RANGE_SIZE + 3]);
2608 }
2609
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if ((fullRanges & 0xFF) == numRanges && total == m_seqChannelCount) {
2610
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogInfo(VB_SEQUENCE, "FSEQ header range count %d is the low byte of %d, using %d.\n", numRanges, fullRanges, fullRanges);
2611 1 numRanges = fullRanges;
2612 }
2613 }
2614
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1411 times.
1411 if (readPos + numRanges * V2FSEQ_SPARSE_RANGE_SIZE > (int)header.size()) {
2615 ✗ LogErr(VB_SEQUENCE, "FSEQ header claims %d sparse ranges but only has room for %d. File is truncated or corrupt.\n",
2616 numRanges, ((int)header.size() - readPos) / V2FSEQ_SPARSE_RANGE_SIZE);
2617 ✗ numRanges = ((int)header.size() - readPos) / V2FSEQ_SPARSE_RANGE_SIZE;
2618 }
2619
2/2
✓ Branch 0 taken 2863 times.
✓ Branch 1 taken 1411 times.
4274 for (int i = 0; i < numRanges; i++) {
2620 2863 uint32_t startChan = read3ByteUInt(&header[readPos]);
2621 2863 uint32_t length = read3ByteUInt(&header[readPos + 3]);
2622
2623
1/2
✓ Branch 0 taken 2863 times.
✗ Branch 1 not taken.
2863 m_sparseRanges.push_back(std::pair<uint32_t, uint32_t>(startChan, length));
2624
2625 2863 readPos += V2FSEQ_SPARSE_RANGE_SIZE;
2626 }
2627
2628 // Validate read position matches expected header size
2629 // This does not include the variable headers length
2630 1411 uint16_t headerSize = read2ByteUInt(&header[8]);
2631
2632
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1409 times.
1411 if (readPos != headerSize) {
2633
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_SEQUENCE, "Read position (%d) does not match expected header size %d!\n", readPos, headerSize);
2634 }
2635
2636 // Read timestamp based UUID - 8 bytes
2637 // This does not advance readPos since it is a fixed index
2638 1411 m_uniqueId = *((uint64_t*)&header[24]);
2639
2640 // The remainder of the buffer (m_seqChanDataOffset - headerSize) contains an unknown count of variable headers
2641 // This will loop and continue reading until it hits padding or m_seqChanDataOffset
2642 // As long as readPos == headerSize prior to this call, the read is a success
2643
1/2
✓ Branch 0 taken 1411 times.
✗ Branch 1 not taken.
1411 parseVariableHeaders(header, readPos);
2644
2645 // The last m_frameOffsets entry may need to be adjusted if there is extended data as the extended data is
2646 // placed after the channel data, but the frame offsets and sizes need to be within the channel data area
2647
2/2
✓ Branch 0 taken 107 times.
✓ Branch 1 taken 1411 times.
1518 for (auto& a : m_variableHeaders) {
2648
6/6
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 99 times.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 5 times.
✓ Branch 5 taken 102 times.
107 if (a.extendedData && a.getExtDataOffset() < m_frameOffsets.back().second) {
2649 5 m_frameOffsets.back().second = a.getExtDataOffset();
2650 }
2651 }
2652
2653 // 2.3+: the whole table when it is longer than the 255 the header can count.
2654 // Only take it when it extends the header's table and covers the channel count,
2655 // so a stale copy carried along by an older writer is ignored.
2656
1/2
✓ Branch 0 taken 1411 times.
✗ Branch 1 not taken.
1411 auto sr = std::find_if(m_variableHeaders.begin(), m_variableHeaders.end(),
2657
3/4
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 99 times.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
107 [](const VariableHeader& vh) { return vh.code[0] == 'S' && vh.code[1] == 'R'; });
2658
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1403 times.
1411 if (sr != m_variableHeaders.end()) {
2659
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 const auto& data = sr->getData();
2660 8 std::vector<std::pair<uint32_t, uint32_t>> ranges;
2661 8 uint64_t total = 0;
2662
2/2
✓ Branch 0 taken 2400 times.
✓ Branch 1 taken 8 times.
2408 for (size_t pos = 0; pos + V2FSEQ_SPARSE_RANGE_SIZE <= data.size(); pos += V2FSEQ_SPARSE_RANGE_SIZE) {
2663
1/2
✓ Branch 0 taken 2400 times.
✗ Branch 1 not taken.
2400 ranges.emplace_back(read3ByteUInt(&data[pos]), read3ByteUInt(&data[pos + 3]));
2664 2400 total += ranges.back().second;
2665 }
2666
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
16 if (data.size() % V2FSEQ_SPARSE_RANGE_SIZE == 0 && ranges.size() > 255 && m_sparseRanges.size() == 255 &&
2667
7/10
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 6 times.
✓ Branch 7 taken 2 times.
✓ Branch 8 taken 6 times.
✓ Branch 9 taken 2 times.
16 std::equal(m_sparseRanges.begin(), m_sparseRanges.end(), ranges.begin()) && total == m_seqChannelCount) {
2668 6 m_sparseRanges = std::move(ranges);
2669 } else {
2670
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_SEQUENCE, "FSEQ extended sparse range block does not match the header, using the %d ranges in the header.\n", (int)m_sparseRanges.size());
2671 }
2672
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 m_variableHeaders.erase(sr);
2673 8 }
2674 }
2675
2676
2/2
✓ Branch 0 taken 1409 times.
✓ Branch 1 taken 43 times.
1452 if (m_compressionType == CompressionType::none) {
2677
1/2
✓ Branch 0 taken 1409 times.
✗ Branch 1 not taken.
1409 m_seqNumFrames = FramesPresent(fn, m_seqNumFrames, m_seqFileSize, m_seqChanDataOffset, m_seqChannelCount);
2678 }
2679
1/2
✓ Branch 0 taken 1452 times.
✗ Branch 1 not taken.
1452 createHandler();
2680 1452 }
2681 7284 V2FSEQFile::~V2FSEQFile() {
2682
1/2
✓ Branch 0 taken 1821 times.
✗ Branch 1 not taken.
3642 if (m_handler) {
2683
1/2
✓ Branch 0 taken 1821 times.
✗ Branch 1 not taken.
3642 delete m_handler;
2684 }
2685 7284 }
2686 1930 void V2FSEQFile::dumpInfo(bool indent) {
2687
1/2
✓ Branch 0 taken 1930 times.
✗ Branch 1 not taken.
1930 FSEQFile::dumpInfo(indent);
2688 1930 char ind[5] = " ";
2689
2/2
✓ Branch 0 taken 1452 times.
✓ Branch 1 taken 478 times.
1930 if (!indent) {
2690 1452 ind[0] = 0;
2691 }
2692
2693
1/2
✓ Branch 0 taken 1930 times.
✗ Branch 1 not taken.
1930 LogDebug(VB_SEQUENCE, "%sSequence File Information\n", ind);
2694
1/2
✓ Branch 0 taken 1930 times.
✗ Branch 1 not taken.
1930 LogDebug(VB_SEQUENCE, "%scompressionType : %d\n", ind, m_compressionType);
2695
2/4
✓ Branch 0 taken 1930 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1930 times.
✗ Branch 3 not taken.
1930 LogDebug(VB_SEQUENCE, "%snumBlocks : %d\n", ind, m_handler->computeMaxBlocks());
2696 // Commented out to declutter the logs ... we can add it back in if we start seeing issues
2697 // for (auto &a : m_frameOffsets) {
2698 // LogDebug(VB_SEQUENCE, "%s %d : %" PRIu64 "\n", ind, a.first, a.second);
2699 //}
2700
1/2
✓ Branch 0 taken 1930 times.
✗ Branch 1 not taken.
1930 LogDebug(VB_SEQUENCE, "%snumRanges : %d\n", ind, m_sparseRanges.size());
2701 // Commented out to declutter the logs ... we can add it back in if we start seeing issues
2702 // for (auto &a : m_sparseRanges) {
2703 // LogDebug(VB_SEQUENCE, "%s Start: %d Len: %d\n", ind, a.first, a.second);
2704 //}
2705 1930 }
2706
2707 1366 bool isWithinRange(const std::vector<std::pair<uint32_t, uint32_t>>& ranges, uint32_t start_channel, uint32_t channel_count) {
2708
2/2
✓ Branch 0 taken 3629 times.
✓ Branch 1 taken 66 times.
3695 for (auto& a : ranges) {
2709
6/6
✓ Branch 0 taken 1341 times.
✓ Branch 1 taken 2288 times.
✓ Branch 2 taken 1338 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 1300 times.
✓ Branch 5 taken 38 times.
3629 if ((start_channel >= a.first && start_channel < (a.first + a.second)) && ((start_channel + channel_count - 1) < (a.first + a.second))) {
2710 1300 return true;
2711 }
2712 }
2713 66 return false;
2714 }
2715
2716 1364 void V2FSEQFile::prepareRead(const std::vector<std::pair<uint32_t, uint32_t>>& ranges, uint32_t startFrame) {
2717
2/2
✓ Branch 0 taken 1317 times.
✓ Branch 1 taken 47 times.
1364 if (m_sparseRanges.empty()) {
2718 1317 m_rangesToRead.clear();
2719 1317 m_dataBlockSize = 0;
2720
2/2
✓ Branch 0 taken 1319 times.
✓ Branch 1 taken 1317 times.
2636 for (auto rng : ranges) {
2721 // make sure we don't read beyond the end of the sequence data
2722 1319 int toRead = rng.second;
2723
2724
2/2
✓ Branch 0 taken 1318 times.
✓ Branch 1 taken 1 times.
1319 if (rng.first < m_seqChannelCount) {
2725
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 1291 times.
1318 if ((rng.first + toRead) > m_seqChannelCount) {
2726 27 toRead = m_seqChannelCount - rng.first;
2727 27 rng.second = toRead;
2728 }
2729 1318 m_dataBlockSize += toRead;
2730
1/2
✓ Branch 0 taken 1318 times.
✗ Branch 1 not taken.
1318 m_rangesToRead.push_back(rng);
2731 }
2732 }
2733
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1316 times.
1317 if (m_dataBlockSize == 0) {
2734
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 m_rangesToRead.push_back(std::pair<uint32_t, uint32_t>(0, getMaxChannel()));
2735 1 m_dataBlockSize = getMaxChannel();
2736 }
2737
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 42 times.
47 } else if (m_compressionType != CompressionType::none) {
2738 // with compression, there is no way to NOT read the entire frame of data, we'll just
2739 // use the sparse data range since we'll have everything anyway so the ranges
2740 // needed is relatively irrelevant
2741 5 m_dataBlockSize = m_seqChannelCount;
2742 5 m_rangesToRead = m_sparseRanges;
2743 } else {
2744 // no compression with sparse ranges
2745 // FIXME - an intersection between the two would be useful, but hard
2746 // for now, just assume that if it's sparse, it has all the data that is needed
2747 // and read everything
2748 42 m_dataBlockSize = m_seqChannelCount;
2749 42 m_rangesToRead = m_sparseRanges;
2750 }
2751
2/2
✓ Branch 0 taken 1366 times.
✓ Branch 1 taken 1364 times.
2730 for (auto const& [st, cnt] : ranges) {
2752
3/4
✓ Branch 0 taken 1366 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 66 times.
✓ Branch 3 taken 1300 times.
1366 if (!isWithinRange(m_rangesToRead, st, cnt)) {
2753
1/2
✓ Branch 0 taken 66 times.
✗ Branch 1 not taken.
66 LogErr(VB_SEQUENCE, "Requested output range outside read ranges. Requested %d channels starting at %d\n", cnt, st);
2754 }
2755 }
2756 1364 m_handler->prepareRead(startFrame);
2757 1364 }
2758 123640 FrameData* V2FSEQFile::getFrame(uint32_t frame) {
2759
2/2
✓ Branch 0 taken 148 times.
✓ Branch 1 taken 123492 times.
123640 if (m_rangesToRead.empty()) {
2760 148 std::vector<std::pair<uint32_t, uint32_t>> range;
2761
2/4
✓ Branch 0 taken 148 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 148 times.
✗ Branch 3 not taken.
148 range.push_back(std::pair<uint32_t, uint32_t>(0, getMaxChannel()));
2762
1/2
✓ Branch 0 taken 148 times.
✗ Branch 1 not taken.
148 prepareRead(range, frame);
2763 148 }
2764
2/2
✓ Branch 0 taken 37 times.
✓ Branch 1 taken 123603 times.
123640 if (frame >= m_seqNumFrames) {
2765 37 return nullptr;
2766 }
2767
1/2
✓ Branch 0 taken 123603 times.
✗ Branch 1 not taken.
123603 if (m_handler != nullptr) {
2768 123603 FrameData* fd = nullptr;
2769 try {
2770
1/2
✓ Branch 0 taken 123603 times.
✗ Branch 1 not taken.
123603 fd = m_handler->getFrame(frame);
2771 ✗ } catch (...) {
2772 ✗ LogErr(VB_SEQUENCE, "Error getting frame from handler %s.\n", m_handler->GetType().c_str());
2773 ✗ }
2774 123603 return fd;
2775 }
2776 ✗ return nullptr;
2777 }
2778 242959 void V2FSEQFile::addFrame(uint32_t frame,
2779 const uint8_t* data) {
2780
1/2
✓ Branch 0 taken 242959 times.
✗ Branch 1 not taken.
242959 if (m_handler != nullptr) {
2781 242959 m_handler->addFrame(frame, data);
2782 }
2783 242959 }
2784
2785 478 void V2FSEQFile::finalize() {
2786
1/2
✓ Branch 0 taken 478 times.
✗ Branch 1 not taken.
478 if (m_handler != nullptr) {
2787 478 m_handler->finalize();
2788 }
2789 478 FSEQFile::finalize();
2790 478 }
2791
2792 199 uint32_t V2FSEQFile::getMaxChannel() const {
2793 199 uint64_t ret = m_seqChannelCount;
2794
2/2
✓ Branch 0 taken 897 times.
✓ Branch 1 taken 199 times.
1096 for (auto& a : m_sparseRanges) {
2795 897 uint64_t m = (uint64_t)a.first + (uint64_t)a.second;
2796
2/2
✓ Branch 0 taken 457 times.
✓ Branch 1 taken 440 times.
897 if (m > ret) {
2797 457 ret = m;
2798 }
2799 }
2800
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 199 times.
199 if (ret > 0xFFFFFFFFu) {
2801 ✗ ret = 0xFFFFFFFFu;
2802 }
2803 199 return (uint32_t)ret;
2804 }
2805