#include "ResourceProvider.hpp"
#include <cstdint>
#include <fstream>
#include <unordered_map>
#include <sstream>
#include <sys/stat.h>
#if defined(_WIN32) || defined(_WIN64)
#include <windows.h>
#include "WinUtil.hpp"
#else
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#endif
#include "Exception.hpp"
namespace opencc {
namespace {
bool IsSeparator(char ch) { return ch == '/' || ch == '\\'; }
uint16_t ReadLe16(const unsigned char* data) {
return static_cast<uint16_t>(data[0]) |
(static_cast<uint16_t>(data[1]) << 8);
}
uint32_t ReadLe32(const unsigned char* data) {
return static_cast<uint32_t>(data[0]) |
(static_cast<uint32_t>(data[1]) << 8) |
(static_cast<uint32_t>(data[2]) << 16) |
(static_cast<uint32_t>(data[3]) << 24);
}
bool IsAbsolutePath(const std::string& path) {
if (path.empty()) {
return false;
}
if (IsSeparator(path[0])) {
return true;
}
#if defined(_WIN32) || defined(_WIN64)
return path.size() >= 3 && path[1] == ':' && IsSeparator(path[2]);
#else
return false;
#endif
}
bool IsRegularFileUtf8(const std::string& path) {
#if defined(_WIN32) || defined(_WIN64)
const DWORD attributes =
GetFileAttributesW(internal::WideFromUtf8(path).c_str());
return attributes != INVALID_FILE_ATTRIBUTES &&
(attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
#else
struct stat info;
if (stat(path.c_str(), &info) != 0) {
return false;
}
return (info.st_mode & S_IFMT) == S_IFREG;
#endif
}
std::string JoinPath(const std::string& root, const std::string& resource) {
if (root.empty()) {
return resource;
}
if (IsSeparator(root.back())) {
return root + resource;
}
return root + "/" + resource;
}
std::string NormalizeResourceName(std::string_view resourceName) {
std::string normalized(resourceName);
for (char& ch : normalized) {
if (ch == '\\') {
ch = '/';
}
}
while (!normalized.empty() && normalized.front() == '/') {
normalized.erase(normalized.begin());
}
return normalized;
}
bool IsSafeZipResourceName(const std::string& name) {
if (name.empty() || IsAbsolutePath(name)) {
return false;
}
size_t start = 0;
for (;;) {
const size_t pos = name.find('/', start);
const std::string part = name.substr(start, pos - start);
if (part.empty() || part == "." || part == "..") {
return false;
}
if (pos == std::string::npos) {
return true;
}
start = pos + 1;
}
}
std::string BaseName(const std::string& path) {
const size_t pos = path.find_last_of('/');
if (pos == std::string::npos) {
return path;
}
return path.substr(pos + 1);
}
bool GetFileFreshnessCacheKey(const std::string& path, std::string* cacheKey) {
#if defined(_WIN32) || defined(_WIN64)
WIN32_FILE_ATTRIBUTE_DATA fileInfo;
const std::wstring widePath = internal::WideFromUtf8(path);
if (widePath.empty() ||
!GetFileAttributesExW(widePath.c_str(), GetFileExInfoStandard,
&fileInfo)) {
return false;
}
#else
struct stat statBuf;
if (stat(path.c_str(), &statBuf) != 0) {
return false;
}
#endif
*cacheKey = path;
cacheKey->push_back('\n');
#if defined(_WIN32) || defined(_WIN64)
cacheKey->append(
std::to_string(static_cast<unsigned long long>(
fileInfo.ftLastWriteTime.dwHighDateTime)));
cacheKey->push_back('.');
cacheKey->append(
std::to_string(static_cast<unsigned long long>(
fileInfo.ftLastWriteTime.dwLowDateTime)));
cacheKey->push_back('\n');
cacheKey->append(
std::to_string(static_cast<unsigned long long>(fileInfo.nFileSizeHigh)));
cacheKey->push_back('.');
cacheKey->append(
std::to_string(static_cast<unsigned long long>(fileInfo.nFileSizeLow)));
#else
cacheKey->append(std::to_string(static_cast<long long>(statBuf.st_mtime)));
cacheKey->push_back('.');
#if defined(__APPLE__) && defined(__MACH__)
cacheKey->append(
std::to_string(static_cast<long long>(statBuf.st_mtimespec.tv_nsec)));
#elif defined(st_mtime_nsec)
cacheKey->append(
std::to_string(static_cast<long long>(statBuf.st_mtime_nsec)));
#else
cacheKey->append(
std::to_string(static_cast<long long>(statBuf.st_mtim.tv_nsec)));
#endif
cacheKey->push_back('\n');
cacheKey->append(std::to_string(static_cast<long long>(statBuf.st_size)));
#endif
return true;
}
#if defined(_WIN32) || defined(_WIN64)
std::vector<unsigned char> ReadBinaryFile(const std::string& path) {
FILE* file = _wfopen(internal::WideFromUtf8(path).c_str(), L"rb");
if (file == nullptr) {
throw FileNotFound(path);
}
if (fseek(file, 0, SEEK_END) != 0) {
fclose(file);
throw FileNotFound(path);
}
const long fileSize = ftell(file);
if (fileSize < 0) {
fclose(file);
throw FileNotFound(path);
}
if (fseek(file, 0, SEEK_SET) != 0) {
fclose(file);
throw FileNotFound(path);
}
std::vector<unsigned char> data(static_cast<size_t>(fileSize));
if (!data.empty() &&
fread(data.data(), 1, data.size(), file) != data.size()) {
fclose(file);
throw FileNotFound(path);
}
fclose(file);
return data;
}
#endif
struct ZipEntry {
uint16_t method;
uint32_t compressedSize;
uint32_t uncompressedSize;
uint32_t dataOffset;
};
class MappedZipArchive {
public:
explicit MappedZipArchive(const std::string& path) : fileName(path) {
if (!GetFileFreshnessCacheKey(path, &cacheKey)) {
throw FileNotFound(path);
}
#if defined(_WIN32) || defined(_WIN64)
buffer = ReadBinaryFile(path);
data = buffer.empty() ? nullptr : buffer.data();
size = buffer.size();
#else
fd = open(path.c_str(), O_RDONLY);
if (fd < 0) {
throw FileNotFound(path);
}
struct stat info;
if (fstat(fd, &info) != 0 || info.st_size < 0) {
close(fd);
fd = -1;
throw FileNotFound(path);
}
size = static_cast<size_t>(info.st_size);
if (size == 0) {
close(fd);
fd = -1;
data = nullptr;
return;
}
void* mapped = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (mapped == MAP_FAILED) {
close(fd);
fd = -1;
throw FileNotFound(path);
}
data = static_cast<const unsigned char*>(mapped);
#endif
}
MappedZipArchive(const MappedZipArchive&) = delete;
MappedZipArchive& operator=(const MappedZipArchive&) = delete;
~MappedZipArchive() {
#if !defined(_WIN32) && !defined(_WIN64)
if (data != nullptr) {
munmap(const_cast<unsigned char*>(data), size);
}
if (fd >= 0) {
close(fd);
}
#endif
}
const std::string fileName;
std::string cacheKey;
const unsigned char* data = nullptr;
size_t size = 0;
private:
#if defined(_WIN32) || defined(_WIN64)
std::vector<unsigned char> buffer;
#else
int fd = -1;
#endif
};
}
struct ZipResourceProvider::Internal {
explicit Internal(const std::string& zipFileName_)
: archive(std::make_shared<MappedZipArchive>(zipFileName_)) {
Index();
}
void Index();
std::shared_ptr<MappedZipArchive> archive;
std::unordered_map<std::string, ZipEntry> entries;
};
void ZipResourceProvider::Internal::Index() {
const unsigned char* data = archive->data;
const size_t size = archive->size;
if (size < 22) {
throw InvalidFormat("Invalid zip archive: " + archive->fileName);
}
const size_t maxCommentSize = 65535;
const size_t searchStart =
size > maxCommentSize + 22 ? size - maxCommentSize - 22 : 0;
size_t eocdOffset = std::string::npos;
for (size_t pos = size - 22;; pos--) {
if (data[pos] == 0x50 && data[pos + 1] == 0x4b && data[pos + 2] == 0x05 &&
data[pos + 3] == 0x06) {
eocdOffset = pos;
break;
}
if (pos == searchStart) {
break;
}
}
if (eocdOffset == std::string::npos) {
throw InvalidFormat("Invalid zip archive: " + archive->fileName);
}
const uint16_t entryCount = ReadLe16(&data[eocdOffset + 10]);
const uint32_t centralDirectorySize = ReadLe32(&data[eocdOffset + 12]);
const uint32_t centralDirectoryOffset = ReadLe32(&data[eocdOffset + 16]);
if (centralDirectoryOffset > size ||
centralDirectorySize > size - centralDirectoryOffset) {
throw InvalidFormat("Invalid zip central directory: " + archive->fileName);
}
size_t pos = centralDirectoryOffset;
for (uint16_t i = 0; i < entryCount; i++) {
if (pos + 46 > size || ReadLe32(&data[pos]) != 0x02014b50) {
throw InvalidFormat("Invalid zip central directory: " + archive->fileName);
}
const uint16_t method = ReadLe16(&data[pos + 10]);
const uint32_t compressedSize = ReadLe32(&data[pos + 20]);
const uint32_t uncompressedSize = ReadLe32(&data[pos + 24]);
const uint16_t fileNameLength = ReadLe16(&data[pos + 28]);
const uint16_t extraLength = ReadLe16(&data[pos + 30]);
const uint16_t commentLength = ReadLe16(&data[pos + 32]);
const uint32_t localHeaderOffset = ReadLe32(&data[pos + 42]);
const size_t next = pos + 46 + fileNameLength + extraLength + commentLength;
if (next > size) {
throw InvalidFormat("Invalid zip central directory: " + archive->fileName);
}
const std::string name(
reinterpret_cast<const char*>(&data[pos + 46]), fileNameLength);
const std::string normalized = NormalizeResourceName(name);
if (!IsAbsolutePath(name) && !normalized.empty() &&
normalized.back() != '/' && IsSafeZipResourceName(normalized)) {
if (localHeaderOffset + 30 > size ||
ReadLe32(&data[localHeaderOffset]) != 0x04034b50) {
throw InvalidFormat("Invalid zip local header: " + archive->fileName);
}
const uint16_t localNameLength = ReadLe16(&data[localHeaderOffset + 26]);
const uint16_t localExtraLength = ReadLe16(&data[localHeaderOffset + 28]);
const size_t dataOffset =
localHeaderOffset + 30 + localNameLength + localExtraLength;
if (dataOffset > size || compressedSize > size - dataOffset) {
throw InvalidFormat("Invalid zip entry data: " + archive->fileName);
}
entries[normalized] = ZipEntry{
method, compressedSize, uncompressedSize,
static_cast<uint32_t>(dataOffset)};
}
pos = next;
}
}
ResourceProvider::Resource::Resource(std::string name_, const char* data_,
size_t size_,
std::shared_ptr<const void> owner_,
std::string cacheKey_)
: name(std::move(name_)), data(data_), size(size_),
owner(std::move(owner_)), cacheKey(std::move(cacheKey_)) {}
std::shared_ptr<const ResourceProvider::Resource>
ResourceProvider::GetResource(std::string_view resourceName) const {
const std::string path = Resolve(resourceName);
std::shared_ptr<std::string> content(new std::string);
#if defined(_WIN32) || defined(_WIN64)
const std::vector<unsigned char> data = ReadBinaryFile(path);
content->assign(reinterpret_cast<const char*>(data.data()), data.size());
#else
std::ifstream input(path, std::ios::binary);
if (!input) {
throw FileNotFound(path);
}
content->assign(std::istreambuf_iterator<char>(input),
std::istreambuf_iterator<char>());
if (input.bad()) {
throw FileNotFound(path);
}
#endif
std::string cacheKey;
if (!GetFileFreshnessCacheKey(path, &cacheKey)) {
throw FileNotFound(path);
}
return std::make_shared<Resource>(path, content->data(), content->size(),
content, cacheKey);
}
FilesystemResourceProvider::FilesystemResourceProvider(
std::vector<std::string> searchPaths_)
: searchPaths(std::move(searchPaths_)) {}
std::string
FilesystemResourceProvider::Resolve(std::string_view resourceName) const {
const std::string resourcePath(resourceName);
if (resourcePath.empty()) {
throw FileNotFound(resourcePath);
}
if (IsAbsolutePath(resourcePath)) {
if (IsRegularFileUtf8(resourcePath)) {
return resourcePath;
}
throw FileNotFound(resourcePath);
}
std::ostringstream searched;
for (size_t i = 0; i < searchPaths.size(); i++) {
const std::string& root = searchPaths[i];
if (i != 0) {
searched << ", ";
}
searched << root;
const std::string candidate = JoinPath(root, resourcePath);
if (IsRegularFileUtf8(candidate)) {
return candidate;
}
}
throw FileNotFound(resourcePath + " (searched: " + searched.str() + ")");
}
ZipResourceProvider::ZipResourceProvider(std::string zipFileName)
: internal(new Internal(zipFileName)) {}
ZipResourceProvider::~ZipResourceProvider() = default;
std::string ZipResourceProvider::Resolve(std::string_view resourceName) const {
throw FileNotFound(std::string(resourceName));
}
std::shared_ptr<const ResourceProvider::Resource>
ZipResourceProvider::GetResource(std::string_view resourceName) const {
if (IsAbsolutePath(std::string(resourceName))) {
throw FileNotFound(std::string(resourceName));
}
const std::string normalized = NormalizeResourceName(resourceName);
if (!IsSafeZipResourceName(normalized)) {
throw FileNotFound(std::string(resourceName));
}
auto entry = internal->entries.find(normalized);
if (entry == internal->entries.end()) {
const std::string baseName = BaseName(normalized);
if (baseName != normalized) {
entry = internal->entries.find(baseName);
}
}
if (entry == internal->entries.end()) {
throw FileNotFound(normalized);
}
if (entry->second.method != 0) {
throw InvalidFormat("Unsupported zip compression method for " + entry->first);
}
if (entry->second.compressedSize != entry->second.uncompressedSize) {
throw InvalidFormat("Invalid stored zip entry size for " + entry->first);
}
const char* data = reinterpret_cast<const char*>(
internal->archive->data + entry->second.dataOffset);
std::string cacheKey = internal->archive->cacheKey;
cacheKey.push_back('\n');
cacheKey.append(entry->first);
cacheKey.push_back('\n');
cacheKey.append(std::to_string(entry->second.uncompressedSize));
return std::make_shared<ResourceProvider::Resource>(
entry->first, data, entry->second.uncompressedSize, internal->archive,
cacheKey);
}
}