#include "codecs/compression.h"
#include "crypto.h"
#include "file_data.h"
#include <whiteout/interfaces.h>
#include <whiteout/utils/job_group.h>
#include <algorithm>
#include <atomic>
#include <cstring>
namespace whiteout::storages::mpq {
namespace {
std::string getBaseName(const std::string& path) {
auto pos = path.rfind('\\');
if (pos == std::string::npos)
pos = path.rfind('/');
if (pos == std::string::npos)
return path;
return path.substr(pos + 1);
}
}
u32 deriveFileKey(const std::string& filename, const BlockEntry& block) {
std::string const baseName = getBaseName(filename);
u32 key = hashString(baseName, HashType::FileKey);
if (block.hasFixKey()) {
key = (key + block.fileOffset) ^ block.uncompressedSize;
}
return key;
}
std::vector<u8> extractFileData(std::span<const u8> archiveData, size_t archiveOffset,
const BlockEntry& block, u32 sectorSize, u32 fileKey,
std::string* error, interfaces::WorkerPool* pool) {
auto setErr = [&](std::string msg) {
if (error)
*error = std::move(msg);
};
if (!block.exists()) {
setErr("block does not exist");
return {};
}
if (block.uncompressedSize == 0)
return {};
u64 const dataStart = archiveOffset + block.fileOffset;
if (dataStart + block.compressedSize > archiveData.size()) {
setErr("data out of bounds");
return {};
}
auto fileSpan = archiveData.subspan(dataStart, block.compressedSize);
if (block.isSingleUnit()) {
std::vector<u8> buf(fileSpan.begin(), fileSpan.end());
if (block.isEncrypted() && fileKey != 0) {
size_t const alignedCount = buf.size() / 4;
if (alignedCount > 0) {
decryptBlock(reinterpret_cast<u32*>(buf.data()), alignedCount, fileKey);
}
}
if (block.isCompressed() && block.compressedSize < block.uncompressedSize) {
std::string decompErr;
auto decompressed =
mpqDecompress(std::span<const u8>(buf), block.uncompressedSize, &decompErr);
if (decompressed.empty()) {
setErr(decompErr.empty() ? "single-unit decompression failed"
: std::move(decompErr));
return {};
}
return decompressed;
}
buf.resize(block.uncompressedSize);
return buf;
}
u32 numSectors = (block.uncompressedSize + sectorSize - 1) / sectorSize;
if (!block.isCompressed()) {
std::vector<u8> buf(fileSpan.begin(), fileSpan.end());
if (block.isEncrypted() && fileKey != 0) {
for (u32 i = 0; i < numSectors; ++i) {
u32 const off = i * sectorSize;
u32 const len = std::min(sectorSize, block.uncompressedSize - off);
size_t const alignedCount = len / 4;
if (alignedCount > 0) {
decryptBlock(reinterpret_cast<u32*>(buf.data() + off), alignedCount,
fileKey + i);
}
}
}
buf.resize(block.uncompressedSize);
return buf;
}
bool const hasCrc = block.hasSectorCrc();
u32 const numOffsetEntries = numSectors + 1 + (hasCrc ? 1u : 0u);
u32 const offsetTableSize = numOffsetEntries * sizeof(u32);
if (fileSpan.size() < offsetTableSize) {
setErr("sector offset table truncated: have=" + std::to_string(fileSpan.size()) +
" need=" + std::to_string(offsetTableSize));
return {};
}
std::vector<u32> sectorOffsets(numOffsetEntries);
std::memcpy(sectorOffsets.data(), fileSpan.data(), offsetTableSize);
if (block.isEncrypted() && fileKey != 0) {
decryptBlock(sectorOffsets.data(), sectorOffsets.size(), fileKey - 1);
}
if (sectorOffsets[0] != offsetTableSize) {
setErr("sector offset table validation failed: offsets[0]=" +
std::to_string(sectorOffsets[0]) + " expected=" + std::to_string(offsetTableSize) +
" numSectors=" + std::to_string(numSectors));
return {};
}
for (u32 i = 0; i < numSectors; ++i) {
u32 const sectorStart = sectorOffsets[i];
u32 const sectorEnd = sectorOffsets[i + 1];
if (sectorEnd < sectorStart || sectorEnd > block.compressedSize) {
setErr("sector " + std::to_string(i) + " bounds invalid: start=" +
std::to_string(sectorStart) + " end=" + std::to_string(sectorEnd) +
" compressedSize=" + std::to_string(block.compressedSize));
return {};
}
}
auto decodeSector = [&](u32 sectorIdx, u32 sectorStart, u32 sectorEnd,
u32 expectedUncompressed) -> std::pair<std::vector<u8>, std::string> {
std::vector<u8> sectorBuf(fileSpan.data() + sectorStart, fileSpan.data() + sectorEnd);
u32 const sectorLen = sectorEnd - sectorStart;
if (block.isEncrypted() && fileKey != 0) {
size_t const alignedCount = sectorBuf.size() / 4;
if (alignedCount > 0) {
decryptBlock(reinterpret_cast<u32*>(sectorBuf.data()), alignedCount,
fileKey + sectorIdx);
}
}
if (block.isCompressed() && sectorLen < expectedUncompressed) {
std::string decompErr;
auto decompressed =
mpqDecompress(std::span<const u8>(sectorBuf), expectedUncompressed, &decompErr);
if (decompressed.empty()) {
return {{},
"sector " + std::to_string(sectorIdx) + " decomp failed: " +
(decompErr.empty() ? std::string("unknown") : std::move(decompErr))};
}
return {std::move(decompressed), {}};
}
sectorBuf.resize(expectedUncompressed);
return {std::move(sectorBuf), {}};
};
auto expectedSectorSize = [&](u32 i) -> u32 {
return (i < numSectors - 1) ? sectorSize : (block.uncompressedSize - i * sectorSize);
};
if (pool && pool->threadCount() > 0 && numSectors >= 4) {
std::vector<std::vector<u8>> sectorResults(numSectors);
std::vector<std::string> sectorErrors(numSectors);
std::atomic<bool> failed{false};
utils::JobGroup jobGroup;
jobGroup.add(numSectors);
for (u32 i = 0; i < numSectors; ++i) {
interfaces::WorkerTask task;
task.fn = [i, §orOffsets, &expectedSectorSize, &decodeSector, §orResults,
§orErrors, &failed, &jobGroup]() {
if (!failed.load(std::memory_order_acquire)) {
auto [data, err] = decodeSector(i, sectorOffsets[i], sectorOffsets[i + 1],
expectedSectorSize(i));
if (!err.empty()) {
failed.store(true, std::memory_order_relaxed);
sectorErrors[i] = std::move(err);
} else {
sectorResults[i] = std::move(data);
}
}
jobGroup.done();
};
pool->submit(task);
}
jobGroup.wait();
if (failed.load(std::memory_order_relaxed)) {
for (u32 i = 0; i < numSectors; ++i) {
if (!sectorErrors[i].empty()) {
setErr(std::move(sectorErrors[i]));
break;
}
}
return {};
}
size_t totalSize = 0;
for (u32 i = 0; i < numSectors; ++i)
totalSize += sectorResults[i].size();
std::vector<u8> output(totalSize);
size_t writePos = 0;
for (u32 i = 0; i < numSectors; ++i) {
std::memcpy(output.data() + writePos, sectorResults[i].data(), sectorResults[i].size());
writePos += sectorResults[i].size();
}
return output;
}
std::vector<u8> output;
output.reserve(block.uncompressedSize);
for (u32 i = 0; i < numSectors; ++i) {
auto [data, err] =
decodeSector(i, sectorOffsets[i], sectorOffsets[i + 1], expectedSectorSize(i));
if (!err.empty()) {
setErr(std::move(err));
return {};
}
output.insert(output.end(), data.begin(), data.end());
}
return output;
}
EncodedFile encodeFileData(std::span<const u8> rawData, const EncodeOptions& opts,
interfaces::WorkerPool* pool) {
EncodedFile result;
if (rawData.empty())
return result;
FileFlag flags = FileFlag::kExists;
if (opts.singleUnit) {
flags |= FileFlag::kSingleUnit;
std::vector<u8> encoded;
if (opts.compression != CompressionFlag::None) {
flags |= FileFlag::kCompress;
auto compressed = mpqCompress(rawData, opts.compression);
if (!compressed.empty()) {
encoded = std::move(compressed);
}
}
if (encoded.empty()) {
flags &= ~FileFlag::kCompress;
encoded.assign(rawData.begin(), rawData.end());
}
u32 fileKey = 0;
if (opts.encrypt && !opts.filename.empty()) {
flags |= FileFlag::kEncrypted;
fileKey =
deriveFileKey(opts.filename, BlockEntry{0, static_cast<u32>(encoded.size()),
static_cast<u32>(rawData.size()), flags});
size_t const alignedCount = encoded.size() / 4;
if (alignedCount > 0) {
encryptBlock(reinterpret_cast<u32*>(encoded.data()), alignedCount, fileKey);
}
}
result.data = std::move(encoded);
result.compressedSize = static_cast<u32>(result.data.size());
result.flags = flags;
return result;
}
u32 sectorSize = opts.sectorSize;
u32 const numSectors = (static_cast<u32>(rawData.size()) + sectorSize - 1) / sectorSize;
std::vector<std::vector<u8>> sectorData(numSectors);
bool anyCompressed = false;
auto compressSector = [&](u32 i) -> bool {
size_t const srcStart = static_cast<size_t>(i) * sectorSize;
size_t const srcLen = std::min<size_t>(sectorSize, rawData.size() - srcStart);
auto sectorRaw = rawData.subspan(srcStart, srcLen);
if (opts.compression != CompressionFlag::None) {
auto compressed = mpqCompress(sectorRaw, opts.compression);
if (!compressed.empty()) {
sectorData[i] = std::move(compressed);
return true; }
}
sectorData[i].assign(sectorRaw.begin(), sectorRaw.end());
return false; };
if (pool && pool->threadCount() > 0 && numSectors >= 2) {
std::vector<bool> sectorCompressed(numSectors, false);
std::atomic<bool> encodeFailed{false};
utils::JobGroup jobGroup;
jobGroup.add(numSectors);
for (u32 i = 0; i < numSectors; ++i) {
interfaces::WorkerTask task;
task.fn = [i, &compressSector, §orCompressed, &encodeFailed, &jobGroup]() {
if (!encodeFailed.load(std::memory_order_acquire))
sectorCompressed[i] = compressSector(i);
jobGroup.done();
};
pool->submit(task);
}
jobGroup.wait();
for (u32 i = 0; i < numSectors; ++i) {
if (sectorCompressed[i]) {
anyCompressed = true;
break;
}
}
} else {
for (u32 i = 0; i < numSectors; ++i) {
if (compressSector(i))
anyCompressed = true;
}
}
std::vector<u32> sectorOffsets;
sectorOffsets.reserve(numSectors + 1);
u32 currentOffset = (numSectors + 1) * sizeof(u32);
sectorOffsets.push_back(currentOffset);
for (u32 i = 0; i < numSectors; ++i) {
currentOffset += static_cast<u32>(sectorData[i].size());
sectorOffsets.push_back(currentOffset);
}
if (anyCompressed) {
flags |= FileFlag::kCompress;
}
u32 fileKey = 0;
if (opts.encrypt && !opts.filename.empty()) {
flags |= FileFlag::kEncrypted;
fileKey = deriveFileKey(
opts.filename, BlockEntry{0, currentOffset, static_cast<u32>(rawData.size()), flags});
}
std::vector<u32> encSectorOffsets = sectorOffsets;
if (fileKey != 0) {
encryptBlock(encSectorOffsets.data(), encSectorOffsets.size(), fileKey - 1);
}
if (fileKey != 0) {
for (u32 i = 0; i < numSectors; ++i) {
size_t const alignedCount = sectorData[i].size() / 4;
if (alignedCount > 0) {
encryptBlock(reinterpret_cast<u32*>(sectorData[i].data()), alignedCount,
fileKey + i);
}
}
}
result.data.resize(currentOffset);
std::memcpy(result.data.data(), encSectorOffsets.data(), encSectorOffsets.size() * sizeof(u32));
size_t writePos = encSectorOffsets.size() * sizeof(u32);
for (const auto& sector : sectorData) {
std::memcpy(result.data.data() + writePos, sector.data(), sector.size());
writePos += sector.size();
}
result.compressedSize = currentOffset;
result.flags = flags;
return result;
}
BatchEncodeResult encodeBatch(std::span<const std::pair<std::span<const u8>, EncodeOptions>> items,
interfaces::WorkerPool* pool) {
BatchEncodeResult result;
result.files.resize(items.size());
if (items.empty())
return result;
const bool usePool = pool && pool->threadCount() > 0;
const bool useSemaphores = usePool && [&]() {
auto test = pool->createTimelineSemaphore();
return test != nullptr;
}();
if (!useSemaphores) {
if (usePool && items.size() >= 2) {
std::atomic<bool> failed{false};
utils::JobGroup jobGroup;
jobGroup.add(items.size());
for (size_t i = 0; i < items.size(); ++i) {
interfaces::WorkerTask task;
task.fn = [i, &items, &result, &failed, &jobGroup]() {
if (!failed.load(std::memory_order_acquire))
result.files[i] = encodeFileData(items[i].first, items[i].second, nullptr);
jobGroup.done();
};
pool->submit(task);
}
jobGroup.wait();
} else {
for (size_t i = 0; i < items.size(); ++i) {
result.files[i] =
encodeFileData(items[i].first, items[i].second, usePool ? pool : nullptr);
}
}
return result;
}
struct FileState {
std::unique_ptr<interfaces::TimelineSemaphore> sem;
interfaces::TimelineSemaphore::Value completeDone = 0;
std::vector<SectorResult> sectorResults;
};
std::vector<FileState> states(items.size());
std::atomic<bool> failed{false};
for (size_t i = 0; i < items.size(); ++i) {
const auto& [rawData, opts] = items[i];
auto& state = states[i];
state.sem = pool->createTimelineSemaphore();
u32 numSectors = 0;
if (!rawData.empty() && !opts.singleUnit) {
numSectors = (static_cast<u32>(rawData.size()) + opts.sectorSize - 1) / opts.sectorSize;
}
if (rawData.empty() || opts.singleUnit || numSectors <= 1) {
state.completeDone = state.sem->next();
interfaces::WorkerTask task;
task.fn = [i, &items, &result, &failed]() {
if (!failed.load(std::memory_order_acquire))
result.files[i] = encodeFileData(items[i].first, items[i].second, nullptr);
};
task.signalSemaphore = state.sem.get();
task.signalValue = state.completeDone;
pool->submit(task);
continue;
}
state.sectorResults.resize(numSectors);
auto compressDone = state.sem->next();
auto compressGroup = std::make_shared<utils::JobGroup>();
compressGroup->add(numSectors);
compressGroup->signalOnComplete(state.sem.get(), compressDone);
for (u32 j = 0; j < numSectors; ++j) {
size_t const srcStart = static_cast<size_t>(j) * opts.sectorSize;
size_t const srcLen = std::min<size_t>(opts.sectorSize, rawData.size() - srcStart);
interfaces::WorkerTask task;
task.fn = [j, srcStart, srcLen, i, &items, &states, &failed, compressGroup]() {
if (!failed.load(std::memory_order_acquire)) {
const auto& [rawData, opts] = items[i];
auto sectorRaw = rawData.subspan(srcStart, srcLen);
auto& sr = states[i].sectorResults[j];
if (opts.compression != CompressionFlag::None) {
auto compressed = mpqCompress(sectorRaw, opts.compression);
if (!compressed.empty()) {
sr.data = std::move(compressed);
sr.wasCompressed = true;
} else {
sr.data.assign(sectorRaw.begin(), sectorRaw.end());
}
} else {
sr.data.assign(sectorRaw.begin(), sectorRaw.end());
}
}
compressGroup->done();
};
pool->submit(task);
}
state.completeDone = state.sem->next();
interfaces::WorkerTask assembleTask;
assembleTask.fn = [i, &items, &result, &states, &failed, numSectors]() {
if (failed.load(std::memory_order_acquire))
return;
const auto& [rawData, opts] = items[i];
auto& state = states[i];
FileFlag flags = FileFlag::kExists;
bool anyCompressed = false;
for (u32 j = 0; j < numSectors; ++j) {
if (state.sectorResults[j].wasCompressed) {
anyCompressed = true;
break;
}
}
if (anyCompressed)
flags |= FileFlag::kCompress;
std::vector<u32> sectorOffsets;
sectorOffsets.reserve(numSectors + 1);
u32 currentOffset = (numSectors + 1) * sizeof(u32);
sectorOffsets.push_back(currentOffset);
for (u32 j = 0; j < numSectors; ++j) {
currentOffset += static_cast<u32>(state.sectorResults[j].data.size());
sectorOffsets.push_back(currentOffset);
}
u32 fileKey = 0;
if (opts.encrypt && !opts.filename.empty()) {
flags |= FileFlag::kEncrypted;
fileKey = deriveFileKey(
opts.filename,
BlockEntry{0, currentOffset, static_cast<u32>(rawData.size()), flags});
}
auto encOffsets = sectorOffsets;
if (fileKey != 0)
encryptBlock(encOffsets.data(), encOffsets.size(), fileKey - 1);
if (fileKey != 0) {
for (u32 j = 0; j < numSectors; ++j) {
size_t const aligned = state.sectorResults[j].data.size() / 4;
if (aligned > 0)
encryptBlock(reinterpret_cast<u32*>(state.sectorResults[j].data.data()),
aligned, fileKey + j);
}
}
EncodedFile& ef = result.files[i];
ef.data.resize(currentOffset);
std::memcpy(ef.data.data(), encOffsets.data(), encOffsets.size() * sizeof(u32));
size_t writePos = encOffsets.size() * sizeof(u32);
for (u32 j = 0; j < numSectors; ++j) {
std::memcpy(ef.data.data() + writePos, state.sectorResults[j].data.data(),
state.sectorResults[j].data.size());
writePos += state.sectorResults[j].data.size();
}
ef.compressedSize = currentOffset;
ef.flags = flags;
};
assembleTask.waitSemaphore = state.sem.get();
assembleTask.waitValue = compressDone;
assembleTask.signalSemaphore = state.sem.get();
assembleTask.signalValue = state.completeDone;
pool->submit(assembleTask);
}
for (size_t i = 0; i < items.size(); ++i) {
states[i].sem->wait(states[i].completeDone);
}
return result;
}
std::vector<std::optional<std::vector<u8>>> extractBatch(std::span<const u8> archiveData,
size_t archiveOffset,
std::span<const ExtractFileInfo> files,
u32 sectorSize,
interfaces::WorkerPool* pool) {
std::vector<std::optional<std::vector<u8>>> results(files.size());
if (files.empty())
return results;
const bool usePool = pool && pool->threadCount() > 0;
const bool useSemaphores = usePool && [&]() {
auto test = pool->createTimelineSemaphore();
return test != nullptr;
}();
if (!useSemaphores) {
for (size_t i = 0; i < files.size(); ++i) {
std::string err;
auto data = extractFileData(archiveData, archiveOffset, files[i].block, sectorSize,
files[i].fileKey, &err, usePool ? pool : nullptr);
if (err.empty() && !data.empty())
results[i] = std::move(data);
else if (files[i].block.uncompressedSize == 0)
results[i] = std::vector<u8>{};
}
return results;
}
struct FileState {
std::unique_ptr<interfaces::TimelineSemaphore> sem;
interfaces::TimelineSemaphore::Value completeDone = 0;
std::vector<std::vector<u8>> sectorResults;
std::atomic<bool> fileFailed{false};
std::string errorMsg;
};
std::vector<FileState> states(files.size());
for (size_t i = 0; i < files.size(); ++i) {
const auto& fi = files[i];
const auto& block = fi.block;
auto& state = states[i];
state.sem = pool->createTimelineSemaphore();
if (!block.exists() || block.uncompressedSize == 0) {
if (block.exists() && block.uncompressedSize == 0)
results[i] = std::vector<u8>{};
state.completeDone = state.sem->next();
state.sem->signal(state.completeDone);
continue;
}
u64 const dataStart = archiveOffset + block.fileOffset;
if (dataStart + block.compressedSize > archiveData.size()) {
state.completeDone = state.sem->next();
state.sem->signal(state.completeDone);
continue;
}
auto fileSpan = archiveData.subspan(dataStart, block.compressedSize);
if (block.isSingleUnit()) {
state.completeDone = state.sem->next();
interfaces::WorkerTask task;
task.fn = [i, &files, &results, fileSpan]() {
const auto& block = files[i].block;
u32 const fileKey = files[i].fileKey;
std::vector<u8> buf(fileSpan.begin(), fileSpan.end());
if (block.isEncrypted() && fileKey != 0) {
size_t const alignedCount = buf.size() / 4;
if (alignedCount > 0)
decryptBlock(reinterpret_cast<u32*>(buf.data()), alignedCount, fileKey);
}
if (block.isCompressed() && block.compressedSize < block.uncompressedSize) {
auto decompressed =
mpqDecompress(std::span<const u8>(buf), block.uncompressedSize);
if (!decompressed.empty()) {
results[i] = std::move(decompressed);
}
} else {
buf.resize(block.uncompressedSize);
results[i] = std::move(buf);
}
};
task.signalSemaphore = state.sem.get();
task.signalValue = state.completeDone;
pool->submit(task);
continue;
}
u32 const numSectors = (block.uncompressedSize + sectorSize - 1) / sectorSize;
bool const hasCrc = block.hasSectorCrc();
u32 const numOffsetEntries = numSectors + 1 + (hasCrc ? 1u : 0u);
u32 const offsetTableSize = numOffsetEntries * sizeof(u32);
if (fileSpan.size() < offsetTableSize) {
state.completeDone = state.sem->next();
state.sem->signal(state.completeDone);
continue;
}
auto sectorOffsets = std::make_shared<std::vector<u32>>(numOffsetEntries);
std::memcpy(sectorOffsets->data(), fileSpan.data(), offsetTableSize);
if (block.isEncrypted() && fi.fileKey != 0) {
decryptBlock(sectorOffsets->data(), sectorOffsets->size(), fi.fileKey - 1);
}
if ((*sectorOffsets)[0] != offsetTableSize) {
state.completeDone = state.sem->next();
state.sem->signal(state.completeDone);
continue;
}
bool offsetsValid = true;
for (u32 s = 0; s < numSectors; ++s) {
if ((*sectorOffsets)[s + 1] < (*sectorOffsets)[s] ||
(*sectorOffsets)[s + 1] > block.compressedSize) {
offsetsValid = false;
break;
}
}
if (!offsetsValid) {
state.completeDone = state.sem->next();
state.sem->signal(state.completeDone);
continue;
}
state.sectorResults.resize(numSectors);
auto decodeDone = state.sem->next();
auto decodeGroup = std::make_shared<utils::JobGroup>();
decodeGroup->add(numSectors);
decodeGroup->signalOnComplete(state.sem.get(), decodeDone);
for (u32 j = 0; j < numSectors; ++j) {
interfaces::WorkerTask task;
task.fn = [i, j, &files, &states, fileSpan, sectorOffsets, numSectors, sectorSize,
decodeGroup]() {
if (states[i].fileFailed.load(std::memory_order_acquire)) {
decodeGroup->done();
return;
}
const auto& block = files[i].block;
u32 const fileKey = files[i].fileKey;
u32 const sectorStart = (*sectorOffsets)[j];
u32 const sectorEnd = (*sectorOffsets)[j + 1];
u32 const expectedSize =
(j < numSectors - 1) ? sectorSize : (block.uncompressedSize - j * sectorSize);
std::vector<u8> sectorBuf(fileSpan.data() + sectorStart,
fileSpan.data() + sectorEnd);
u32 const sectorLen = sectorEnd - sectorStart;
if (block.isEncrypted() && fileKey != 0) {
size_t const alignedCount = sectorBuf.size() / 4;
if (alignedCount > 0)
decryptBlock(reinterpret_cast<u32*>(sectorBuf.data()), alignedCount,
fileKey + j);
}
if (block.isCompressed() && sectorLen < expectedSize) {
auto decompressed = mpqDecompress(std::span<const u8>(sectorBuf), expectedSize);
if (decompressed.empty()) {
states[i].fileFailed.store(true, std::memory_order_release);
} else {
states[i].sectorResults[j] = std::move(decompressed);
}
} else {
sectorBuf.resize(expectedSize);
states[i].sectorResults[j] = std::move(sectorBuf);
}
decodeGroup->done();
};
pool->submit(task);
}
state.completeDone = state.sem->next();
interfaces::WorkerTask assembleTask;
assembleTask.fn = [i, &files, &results, &states, numSectors]() {
if (states[i].fileFailed.load(std::memory_order_acquire))
return;
const auto& block = files[i].block;
std::vector<u8> output(block.uncompressedSize);
size_t writePos = 0;
for (u32 j = 0; j < numSectors; ++j) {
std::memcpy(output.data() + writePos, states[i].sectorResults[j].data(),
states[i].sectorResults[j].size());
writePos += states[i].sectorResults[j].size();
}
results[i] = std::move(output);
};
assembleTask.waitSemaphore = state.sem.get();
assembleTask.waitValue = decodeDone;
assembleTask.signalSemaphore = state.sem.get();
assembleTask.signalValue = state.completeDone;
pool->submit(assembleTask);
}
for (size_t i = 0; i < files.size(); ++i) {
states[i].sem->wait(states[i].completeDone);
}
return results;
}
}