mirror of
https://github.com/mehah/otclient
synced 2026-08-15 16:29:06 -04:00
fix: make client asset installation archive-first (#1739)
Restore archive first modern client asset installation This commit reworks the modern client asset auto installation flow so the normal path is based on downloading and extracting the GitHub release or source ZIP instead of relying on the manifest flow as the primary installer. The installer now keeps the final runtime files in the same locations already expected by OTClient. Asset files are installed under data things version, sound files are installed under data sounds version, and configured runtime extras are installed into their expected runtime locations such as bin. The manifest based path is still kept as a compatibility fallback, but it is no longer the preferred installation path for modern client assets. This restores the archive first behavior and makes the install result match the standard runtime loader layout more directly. For client versions 1281 and newer, OTClient now checks the existing files under data things version before attempting installation. When auto install is enabled and the required files are missing, the client prompts the user, downloads the configured release or source ZIP, extracts the assets folder into data things version, extracts the sounds folder into data sounds version, and installs configured runtime extras into their target runtime paths. If Services clientAssets enabled is false, OTClient now skips the asset ensure download flow during login. This avoids recursive login attempts caused by the installer prompt and keeps manual or custom asset folders usable. In that mode, OTClient proceeds with the provided local files and lets the normal runtime loader report missing or invalid assets when appropriate. The archive extraction support was also expanded. ZIP extraction now works through the vendored minizip fallback when libarchive is not available. Visual Studio project configuration was updated to enable libarchive so Windows builds can handle ZIP and RAR archive extraction. Android builds remain safe because unsupported libarchive linkage is not required there. Main changes included in this commit • Restores archive first installation for modern client assets • Keeps the manifest path only as a compatibility fallback • Downloads the GitHub release or source ZIP as the normal install path • Extracts assets into data things version • Extracts sounds into data sounds version • Installs runtime extras into their expected runtime locations such as bin • Keeps final installed files in the same OTClient runtime paths already used by the loader • Adds ZIP extraction support through the vendored minizip fallback • Enables libarchive in the Visual Studio project for Windows archive extraction • Allows Windows builds to handle ZIP and RAR extraction through libarchive • Preserves Android build safety by not requiring unsupported libarchive linkage • Skips asset ensure and download prompts when Services clientAssets enabled is false • Avoids recursive login attempts when asset auto install is disabled • Keeps manual and custom asset folders usable • Lets the normal runtime loader report actual missing or invalid files when auto install is disabled • Documents the archive first flow fallback behavior install paths and troubleshooting notes Runtime behavior For modern client versions 1281 and newer, OTClient first checks the existing files under data things version. If the files are already present, login continues without triggering the installer. If files are missing and auto install is enabled, OTClient prompts the user and installs assets from the configured GitHub release or source archive. The archive folders are mapped into the runtime locations expected by the client. The assets folder is installed into data things version. The sounds folder is installed into data sounds version. Runtime extras such as bin files are installed into their configured runtime destinations. If auto install is disabled, the installer is not called during login. The client continues with the manually provided files and the regular loader remains responsible for reporting any actual asset problems. Verification performed • luac p modules client assets client assets lua init lua • luac p modules client entergame entergame lua • git diff check • Local Windows build using vc18 otclient sln OpenGL x64 Build • GitHub Actions fast checks passed on the PR branch • GitHub Actions Lua syntax checks passed on the PR branch Overall this commit restores the expected archive based installation flow for modern client assets while preserving compatibility fallback behavior. It keeps installed files aligned with the existing OTClient runtime layout improves archive extraction support across build targets and avoids unwanted installer prompts when automatic asset installation is disabled.
This commit is contained in:
parent
6cf47c2012
commit
8d98d5a6ba
9 changed files with 284 additions and 29 deletions
2
.gitattributes
vendored
2
.gitattributes
vendored
|
|
@ -1,5 +1,7 @@
|
|||
# Normalize source code before commits
|
||||
.gitattributes text eol=lf
|
||||
*.lua eol=lf
|
||||
*.md text eol=lf
|
||||
*.txt eol=lf
|
||||
*.ot* eol=lf
|
||||
*.cpp eol=lf
|
||||
|
|
|
|||
|
|
@ -31,8 +31,9 @@ Do not introduce an alternative permanent assets root for runtime loading.
|
|||
|
||||
The flow supports:
|
||||
|
||||
- archive-first installation from release/tag package
|
||||
- manifest-driven installation
|
||||
- archive installation from the release/tag source ZIP as the default path
|
||||
- manifest-driven installation as a fallback path when the archive cannot be installed
|
||||
- manifest hash identifier installation into `data/things/<version>/assets.json.sha256`
|
||||
- packaged files list (including large binaries distributed as `.zip`/`.rar`)
|
||||
- extraction of `.zip` and `.rar`
|
||||
- optional `.lzma` decompression
|
||||
|
|
@ -43,13 +44,18 @@ Defaults are hardened:
|
|||
|
||||
- `strictManifestSha256 = true`
|
||||
- `allowRawFallbackHashMismatch = false`
|
||||
- `allowMissingPackedRawFallback = true`
|
||||
|
||||
`allowMissingPackedRawFallback` is a narrow compatibility fallback for repository releases that reference official `.lzma`/archive package files not stored in the assets repository. It is only used after the packed file is missing and the client falls back to the raw file from the same manifest/release source. It does not enable arbitrary hash mismatches for normal raw downloads.
|
||||
|
||||
Release cache is scoped per source (`releasesUrl` / repository key), avoiding stale cross-source reuse.
|
||||
|
||||
## Runtime/Platform Notes
|
||||
|
||||
- Desktop targets use `libarchive` for archive extraction.
|
||||
- Android build excludes `libarchive` dependency (CI compatibility). In this target, archive extraction through `ResourceManager` is unavailable and returns failure explicitly.
|
||||
- Desktop targets use `libarchive` for archive extraction when it is available.
|
||||
- Builds without `libarchive` still extract `.zip` archives through the vendored minizip fallback. This keeps the GitHub source ZIP flow functional on clean desktop builds.
|
||||
- `.rar` extraction requires `libarchive`. If a packaged `.rar` is optional and the build cannot extract it, installation should fail clearly or skip it according to the package configuration.
|
||||
- The default flow is archive-first because the release source ZIP is the canonical package for this repository. The manifest path remains a compatibility fallback, not the primary installation path.
|
||||
- Emscripten login fallback was aligned with native `httpLogin` semantics.
|
||||
|
||||
## UX Behavior
|
||||
|
|
@ -69,11 +75,15 @@ Check:
|
|||
- `data/things/<version>/assets.json.sha256`
|
||||
- `data/sounds/<version>/catalog-sound.json` (when sounds are enabled)
|
||||
|
||||
### 2) SHA-256 mismatch
|
||||
### 2) Missing `.lzma` package file
|
||||
|
||||
If the console shows a 404 for `*.lzma`, the client is using the manifest fallback instead of the release source ZIP. First check why archive installation failed. The manifest fallback can install raw files through `allowMissingPackedRawFallback`, but this path is slower and should not be the normal flow for clean installs.
|
||||
|
||||
### 3) SHA-256 mismatch
|
||||
|
||||
By default, mismatches fail installation. Verify upstream files and hashes first before changing integrity flags.
|
||||
|
||||
### 3) Slow progress / “stuck”
|
||||
### 4) Slow progress / “stuck”
|
||||
|
||||
If Content-Length is missing, UI may run in indeterminate mode during download and extraction. Use console logs to confirm active phase.
|
||||
|
||||
|
|
@ -90,4 +100,3 @@ When changing this system, validate:
|
|||
3. Runtime loads modern assets from those paths.
|
||||
4. Hash verification behavior matches configuration.
|
||||
5. Windows/Linux CI remains green; Android does not attempt to resolve unsupported libarchive linkage.
|
||||
|
||||
|
|
|
|||
2
init.lua
2
init.lua
|
|
@ -14,7 +14,9 @@ Services = {
|
|||
installSounds = true,
|
||||
strictManifestSha256 = true,
|
||||
allowRawFallbackHashMismatch = false,
|
||||
allowMissingPackedRawFallback = true,
|
||||
preferArchive = true,
|
||||
fallbackToArchiveOnManifestFailure = false,
|
||||
installArchiveExtras = true,
|
||||
archiveExtraPrefixes = { "bin" },
|
||||
installPackagedFiles = true
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ local DEFAULT_CONFIG = {
|
|||
preferPackedManifestUrls = true,
|
||||
strictManifestSha256 = true,
|
||||
allowRawFallbackHashMismatch = false,
|
||||
allowMissingPackedRawFallback = true,
|
||||
preferArchive = true,
|
||||
fallbackToArchiveOnManifestFailure = false,
|
||||
installArchiveExtras = true,
|
||||
archiveExtraPrefixes = { 'bin' },
|
||||
archiveExtrasDestination = '',
|
||||
|
|
@ -67,6 +69,29 @@ local function isLzmaPath(path)
|
|||
return endsWith(tostring(path or ''):lower(), '.lzma')
|
||||
end
|
||||
|
||||
local function isNotFoundError(message)
|
||||
message = tostring(message or ''):lower()
|
||||
return message:find('404', 1, true) or message:find('not found', 1, true)
|
||||
end
|
||||
|
||||
local function isMissingPackedHashError(message)
|
||||
return tostring(message or ''):find(HTTP_NOT_FOUND_SHA256, 1, true) ~= nil
|
||||
end
|
||||
|
||||
local function isPackedToRawFallback(downloadInfo, fallbackDownload)
|
||||
if not downloadInfo or not fallbackDownload then
|
||||
return false
|
||||
end
|
||||
|
||||
local sourcePath = tostring(downloadInfo.sourcePath or '')
|
||||
local fallbackPath = tostring(fallbackDownload.sourcePath or '')
|
||||
if fallbackPath == '' or isLzmaPath(fallbackPath) or isArchivePath(fallbackPath) then
|
||||
return false
|
||||
end
|
||||
|
||||
return isLzmaPath(sourcePath) or isArchivePath(sourcePath)
|
||||
end
|
||||
|
||||
local function parentPath(path)
|
||||
path = tostring(path or ''):gsub('\\', '/')
|
||||
return path:match('^(.*)/[^/]+$') or ''
|
||||
|
|
@ -318,6 +343,18 @@ local function shouldInstallInWorkDir(config)
|
|||
return config and config.installInWorkDir ~= false
|
||||
end
|
||||
|
||||
local function writeInstallFile(config, path, contents)
|
||||
if shouldInstallInWorkDir(config) and g_resources.writeFileContentsToWorkDir then
|
||||
return g_resources.writeFileContentsToWorkDir(path, contents)
|
||||
end
|
||||
|
||||
local directory = parentPath(path)
|
||||
if directory ~= '' then
|
||||
g_resources.makeDir('/' .. directory)
|
||||
end
|
||||
return g_resources.writeFileContents('/' .. path, contents)
|
||||
end
|
||||
|
||||
local function hasCatalogEntryFile(basePath, entry)
|
||||
if type(entry) ~= 'table' or type(entry.file) ~= 'string' then
|
||||
return false
|
||||
|
|
@ -751,20 +788,34 @@ local function extractDownloadedArchive(config, downloadPath, destinationPath, e
|
|||
return g_resources.extractDownloadedArchive(downloadPath, destinationPath, entryPrefix or '', stripPrefix == true)
|
||||
end
|
||||
|
||||
local function installDownloadedFile(config, downloadPath, destinationPath, decompressLzma, expectedFileSha256, allowHashMismatch)
|
||||
local function installDownloadedFile(config, downloadPath, destinationPath, decompressLzma, expectedFileSha256, allowHashMismatch, allowHashMismatchReason)
|
||||
if not writeDownloadedFile(config, downloadPath, destinationPath, decompressLzma) then
|
||||
return false, 'Unable to write downloaded asset: ' .. destinationPath
|
||||
end
|
||||
|
||||
local ok, hashError = verifyInstalledSha256(destinationPath, expectedFileSha256, not allowHashMismatch)
|
||||
if not ok and allowHashMismatch and not hashError:find(HTTP_NOT_FOUND_SHA256, 1, true) then
|
||||
logWarning(hashError .. ' Continuing with raw fallback because allowRawFallbackHashMismatch is enabled.')
|
||||
logWarning(hashError .. ' Continuing because ' .. (allowHashMismatchReason or 'allowRawFallbackHashMismatch is enabled') .. '.')
|
||||
return true
|
||||
end
|
||||
|
||||
return ok, hashError
|
||||
end
|
||||
|
||||
local function writeManifestHashIdentifier(config, version, sha256)
|
||||
if not sha256 or sha256 == '' then
|
||||
return true
|
||||
end
|
||||
|
||||
local destinationPath = string.format('data/things/%d/assets.json.sha256', version)
|
||||
if not writeInstallFile(config, destinationPath, sha256 .. '\n') then
|
||||
return false, 'Unable to write asset hash identifier: ' .. destinationPath
|
||||
end
|
||||
|
||||
logInfo(string.format('Asset hash identifier install path: %s.', physicalInstallPath(destinationPath)))
|
||||
return true
|
||||
end
|
||||
|
||||
local function installDownloadedArchive(config, downloadPath, destinationPath, entryPrefix, stripPrefix, expectedPath, expectedSha256)
|
||||
if not extractDownloadedArchive(config, downloadPath, destinationPath, entryPrefix, stripPrefix) then
|
||||
return false, 'Unable to extract downloaded archive: ' .. destinationPath
|
||||
|
|
@ -892,11 +943,15 @@ local function installManifestEntries(config, descriptor, files, index, installe
|
|||
return installManifestEntries(config, descriptor, files, index + 1, installed, total, callback)
|
||||
end
|
||||
|
||||
local preferPacked = descriptor.preferPackedManifestUrls or config.preferPackedManifestUrls
|
||||
local preferPacked = descriptor.preferPackedManifestUrls
|
||||
if preferPacked == nil then
|
||||
preferPacked = config.preferPackedManifestUrls
|
||||
end
|
||||
local directDownload = buildManifestDownload(entry, descriptor, selectedEntry, preferPacked)
|
||||
local fallbackDownload
|
||||
if config.allowRawFallbackHashMismatch ~= false and not directDownload.extractArchive and not isLzmaPath(directDownload.sourcePath) then
|
||||
directDownload.allowHashMismatch = true
|
||||
directDownload.allowHashMismatchReason = 'allowRawFallbackHashMismatch is enabled'
|
||||
end
|
||||
if entry.url then
|
||||
fallbackDownload = buildManifestDownload(entry, descriptor, selectedEntry, not preferPacked)
|
||||
|
|
@ -904,6 +959,7 @@ local function installManifestEntries(config, descriptor, files, index, installe
|
|||
fallbackDownload = nil
|
||||
elseif config.allowRawFallbackHashMismatch ~= false and not isArchivePath(fallbackDownload.sourcePath) and not isLzmaPath(fallbackDownload.sourcePath) then
|
||||
fallbackDownload.allowHashMismatch = true
|
||||
fallbackDownload.allowHashMismatchReason = 'allowRawFallbackHashMismatch is enabled'
|
||||
end
|
||||
end
|
||||
if directDownload.expectedFileSha256 and g_resources.fileSha256('/' .. directDownload.destinationPath) == directDownload.expectedFileSha256 then
|
||||
|
|
@ -930,6 +986,10 @@ local function installManifestEntries(config, descriptor, files, index, installe
|
|||
return downloadEntry(downloadInfo, nextFallback, retriesLeft - 1)
|
||||
end
|
||||
if nextFallback then
|
||||
if config.allowMissingPackedRawFallback ~= false and isNotFoundError(err) and isPackedToRawFallback(downloadInfo, nextFallback) then
|
||||
nextFallback.allowHashMismatch = true
|
||||
nextFallback.allowHashMismatchReason = 'the packed asset is missing and allowMissingPackedRawFallback is enabled'
|
||||
end
|
||||
logWarning(string.format('Download failed for %s: %s. Trying %s.', downloadInfo.sourcePath, err, nextFallback.sourcePath))
|
||||
return downloadEntry(nextFallback, nil, config.retries or 0)
|
||||
end
|
||||
|
|
@ -939,11 +999,15 @@ local function installManifestEntries(config, descriptor, files, index, installe
|
|||
local ok, hashError = verifyDownloadedSha256(path, downloadInfo.expectedDownloadSha256)
|
||||
if not ok then
|
||||
if nextFallback then
|
||||
if config.allowMissingPackedRawFallback ~= false and isMissingPackedHashError(hashError) and isPackedToRawFallback(downloadInfo, nextFallback) then
|
||||
nextFallback.allowHashMismatch = true
|
||||
nextFallback.allowHashMismatchReason = 'the packed asset is missing and allowMissingPackedRawFallback is enabled'
|
||||
end
|
||||
logWarning(hashError .. ' Trying ' .. nextFallback.sourcePath .. '.')
|
||||
return downloadEntry(nextFallback, nil, config.retries or 0)
|
||||
end
|
||||
if downloadInfo.allowHashMismatch and not hashError:find(HTTP_NOT_FOUND_SHA256, 1, true) then
|
||||
logWarning(hashError .. ' Continuing with raw fallback because allowRawFallbackHashMismatch is enabled.')
|
||||
logWarning(hashError .. ' Continuing because ' .. (downloadInfo.allowHashMismatchReason or 'allowRawFallbackHashMismatch is enabled') .. '.')
|
||||
ok = true
|
||||
else
|
||||
return callback(false, hashError)
|
||||
|
|
@ -965,7 +1029,7 @@ local function installManifestEntries(config, descriptor, files, index, installe
|
|||
downloadInfo.expectedFileSha256
|
||||
)
|
||||
else
|
||||
ok, hashError = installDownloadedFile(config, path, downloadInfo.destinationPath, downloadInfo.decompressLzma, downloadInfo.expectedFileSha256, downloadInfo.allowHashMismatch)
|
||||
ok, hashError = installDownloadedFile(config, path, downloadInfo.destinationPath, downloadInfo.decompressLzma, downloadInfo.expectedFileSha256, downloadInfo.allowHashMismatch, downloadInfo.allowHashMismatchReason)
|
||||
end
|
||||
if not ok then
|
||||
if nextFallback then
|
||||
|
|
@ -1018,8 +1082,8 @@ local function installFromManifest(config, descriptor, callback)
|
|||
end
|
||||
|
||||
fetchManifestSha256(config, descriptor, function(expectedSha256)
|
||||
local actualSha256 = g_crypt.sha256(data)
|
||||
if expectedSha256 then
|
||||
local actualSha256 = g_crypt.sha256(data)
|
||||
if actualSha256 ~= expectedSha256 then
|
||||
local hashError = string.format('Invalid assets manifest SHA-256 for %s. Expected %s, got %s.', descriptor.manifestUrl, expectedSha256, actualSha256)
|
||||
if descriptor.strictManifestSha256 or config.strictManifestSha256 then
|
||||
|
|
@ -1036,6 +1100,11 @@ local function installFromManifest(config, descriptor, callback)
|
|||
return callback(false, 'Invalid assets manifest.')
|
||||
end
|
||||
|
||||
local wroteHashIdentifier, writeHashError = writeManifestHashIdentifier(config, descriptor.version, actualSha256)
|
||||
if not wroteHashIdentifier then
|
||||
return callback(false, writeHashError)
|
||||
end
|
||||
|
||||
installManifestEntries(config, descriptor, manifest.files, 1, 0, #manifest.files, callback)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -1202,7 +1271,18 @@ local function installPackagedFileList(config, descriptor, files, index, callbac
|
|||
scheduleDownloadStep(function()
|
||||
local ok, extractError = installDownloadedArchive(config, downloadPath, destinationPath, '', false)
|
||||
if not ok then
|
||||
return callback(false, extractError)
|
||||
if descriptor.packagedFilesRequired then
|
||||
return callback(false, extractError)
|
||||
end
|
||||
logWarning(string.format(
|
||||
'Skipping optional packaged file %d/%d for client %s: %s (%s).',
|
||||
index,
|
||||
#files,
|
||||
versionLabel(descriptor.version),
|
||||
path,
|
||||
extractError or 'unable to extract archive'
|
||||
))
|
||||
return installPackagedFileList(config, descriptor, files, index + 1, callback)
|
||||
end
|
||||
|
||||
logInfo(string.format('Finished packaged file %d/%d for client %s: %s.', index, #files, versionLabel(descriptor.version), path))
|
||||
|
|
@ -1244,6 +1324,8 @@ local function installPackagedFiles(config, descriptor, callback)
|
|||
end
|
||||
|
||||
local function installDescriptor(config, descriptor, callback)
|
||||
local archiveAttempted = false
|
||||
|
||||
local function finishWithPackagedFiles(ok, message)
|
||||
if not ok then
|
||||
return callback(false, message)
|
||||
|
|
@ -1254,25 +1336,49 @@ local function installDescriptor(config, descriptor, callback)
|
|||
end)
|
||||
end
|
||||
|
||||
local function archiveFallback(nextCallback)
|
||||
archiveAttempted = true
|
||||
installFromArchive(config, descriptor, nextCallback)
|
||||
end
|
||||
|
||||
local function manifestFallback()
|
||||
installFromManifest(config, descriptor, function(ok, message)
|
||||
if ok or not descriptor.archiveUrl then
|
||||
local allowArchiveFallback = descriptor.fallbackToArchiveOnManifestFailure
|
||||
if allowArchiveFallback == nil then
|
||||
allowArchiveFallback = config.fallbackToArchiveOnManifestFailure
|
||||
end
|
||||
if ok or not descriptor.archiveUrl or archiveAttempted or not allowArchiveFallback then
|
||||
return finishWithPackagedFiles(ok, message)
|
||||
end
|
||||
installFromArchive(config, descriptor, finishWithPackagedFiles)
|
||||
logWarning((message or 'Asset manifest install failed.') .. ' Trying archive fallback.')
|
||||
archiveFallback(finishWithPackagedFiles)
|
||||
end)
|
||||
end
|
||||
|
||||
if descriptor.preferArchive or config.preferArchive then
|
||||
return installFromArchive(config, descriptor, function(ok, message)
|
||||
local preferArchive = descriptor.preferArchive
|
||||
if preferArchive == nil then
|
||||
preferArchive = config.preferArchive
|
||||
end
|
||||
|
||||
if preferArchive and descriptor.archiveUrl then
|
||||
return archiveFallback(function(ok, message)
|
||||
if ok or not descriptor.manifestUrl then
|
||||
return finishWithPackagedFiles(ok, message)
|
||||
end
|
||||
logWarning((message or 'Archive asset install failed.') .. ' Falling back to asset manifest.')
|
||||
manifestFallback()
|
||||
end)
|
||||
end
|
||||
|
||||
manifestFallback()
|
||||
if descriptor.manifestUrl then
|
||||
return manifestFallback()
|
||||
end
|
||||
|
||||
if descriptor.archiveUrl then
|
||||
return archiveFallback(finishWithPackagedFiles)
|
||||
end
|
||||
|
||||
finishWithPackagedFiles(false, 'No assets source found.')
|
||||
end
|
||||
|
||||
local function resolveFromCustomManifest(config, version, callback)
|
||||
|
|
|
|||
|
|
@ -801,6 +801,7 @@ function EnterGame.doLogin()
|
|||
g_settings.set('client-version', clientVersion)
|
||||
|
||||
if clientVersion >= 1281 and modules.client_assets and modules.client_assets.ensureClientVersion and
|
||||
(not modules.client_assets.isEnabled or modules.client_assets.isEnabled()) and
|
||||
not modules.client_assets.isClientVersionInstalled(clientVersion) then
|
||||
modules.client_assets.ensureClientVersion(clientVersion, function(success, message)
|
||||
if success then
|
||||
|
|
|
|||
|
|
@ -521,8 +521,8 @@ if (WASM)
|
|||
)
|
||||
endif()
|
||||
|
||||
if(ANDROID)
|
||||
# Vendored minizip sources for APK asset extraction (must be after SOURCE_FILES definition)
|
||||
if(NOT WASM)
|
||||
# Vendored minizip sources for ZIP extraction fallback (must be after SOURCE_FILES definition)
|
||||
set(SOURCE_FILES ${SOURCE_FILES}
|
||||
framework/core/minizip/ioapi.c
|
||||
framework/core/minizip/ioapi_mem.c
|
||||
|
|
@ -567,8 +567,8 @@ else()
|
|||
message(STATUS "Use precompiled header: OFF")
|
||||
endif(TOGGLE_PRE_COMPILED_HEADER)
|
||||
|
||||
# Exclude vendored minizip C files from PCH and unity build (Android only, C files cannot use C++ PCH)
|
||||
if(ANDROID)
|
||||
# Exclude vendored minizip C files from PCH and unity build (C files cannot use C++ PCH)
|
||||
if(NOT WASM)
|
||||
set_source_files_properties(
|
||||
framework/core/minizip/ioapi.c
|
||||
framework/core/minizip/ioapi_mem.c
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@
|
|||
#ifdef FRAMEWORK_HAVE_LIBARCHIVE
|
||||
#include <archive.h>
|
||||
#include <archive_entry.h>
|
||||
#else
|
||||
#include "minizip/ioapi.h"
|
||||
#include "minizip/ioapi_mem.h"
|
||||
#include "minizip/unzip.h"
|
||||
#endif
|
||||
|
||||
ResourceManager g_resources;
|
||||
|
|
@ -192,6 +196,109 @@ HttpResult_ptr getDownloadedFile(std::string path)
|
|||
return g_http.getFile(path);
|
||||
}
|
||||
|
||||
#ifndef FRAMEWORK_HAVE_LIBARCHIVE
|
||||
bool extractDownloadedZipArchive(ResourceManager& resourceManager, const std::string& path, const std::string& archive, std::string destinationPath, const std::string& entryPrefix, const bool stripPrefix)
|
||||
{
|
||||
if (archive.size() > UINT32_MAX) {
|
||||
g_logger.error("Downloaded archive '{}' is too large for zip extraction", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
zlib_filefunc_def fileFunctions = {};
|
||||
ourmemory_t archiveMemory = {};
|
||||
archiveMemory.base = const_cast<char*>(archive.data());
|
||||
archiveMemory.size = static_cast<uint32_t>(archive.size());
|
||||
archiveMemory.grow = 0;
|
||||
fill_memory_filefunc(&fileFunctions, &archiveMemory);
|
||||
|
||||
unzFile zipFile = unzOpen2(nullptr, &fileFunctions);
|
||||
if (!zipFile) {
|
||||
g_logger.error("Unable to open downloaded zip archive '{}'. Non-zip archives require libarchive support.", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
unz_global_info globalInfo = {};
|
||||
if (unzGetGlobalInfo(zipFile, &globalInfo) != UNZ_OK) {
|
||||
g_logger.error("Unable to read zip archive info for '{}'", path);
|
||||
unzClose(zipFile);
|
||||
return false;
|
||||
}
|
||||
|
||||
destinationPath = normalizeVirtualPath(std::move(destinationPath));
|
||||
if (!destinationPath.empty() && !destinationPath.ends_with('/'))
|
||||
destinationPath.push_back('/');
|
||||
|
||||
bool wroteFile = false;
|
||||
constexpr int maxFilenameSize = 1024;
|
||||
constexpr int readSize = 8192;
|
||||
std::array<char, maxFilenameSize> fileName = {};
|
||||
std::array<char, readSize> readBuffer = {};
|
||||
|
||||
for (uint32_t i = 0; i < globalInfo.number_entry; ++i) {
|
||||
unz_file_info fileInfo = {};
|
||||
fileName.fill('\0');
|
||||
if (unzGetCurrentFileInfo(zipFile, &fileInfo, fileName.data(), static_cast<uint16_t>(fileName.size()), nullptr, 0, nullptr, 0) != UNZ_OK) {
|
||||
g_logger.error("Unable to read zip entry info from '{}'", path);
|
||||
unzClose(zipFile);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string entryName = fileName.data();
|
||||
const bool isDirectory = entryName.ends_with('/') || entryName.ends_with('\\');
|
||||
const auto relativePath = selectArchiveEntryPath(entryName, entryPrefix, stripPrefix);
|
||||
|
||||
if (!isDirectory && !relativePath.empty()) {
|
||||
if (unzOpenCurrentFile(zipFile) != UNZ_OK) {
|
||||
g_logger.error("Unable to open zip entry '{}' from '{}'", entryName, path);
|
||||
unzClose(zipFile);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string contents;
|
||||
int readBytes = 0;
|
||||
do {
|
||||
readBytes = unzReadCurrentFile(zipFile, readBuffer.data(), readBuffer.size());
|
||||
if (readBytes < 0) {
|
||||
g_logger.error("Unable to read zip entry '{}' from '{}': {}", entryName, path, readBytes);
|
||||
unzCloseCurrentFile(zipFile);
|
||||
unzClose(zipFile);
|
||||
return false;
|
||||
}
|
||||
if (readBytes > 0)
|
||||
contents.append(readBuffer.data(), static_cast<size_t>(readBytes));
|
||||
} while (readBytes > 0);
|
||||
|
||||
if (unzCloseCurrentFile(zipFile) != UNZ_OK) {
|
||||
g_logger.error("Unable to close zip entry '{}' from '{}'", entryName, path);
|
||||
unzClose(zipFile);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto destinationFile = destinationPath + relativePath;
|
||||
if (!resourceManager.writeFileBuffer(
|
||||
destinationFile,
|
||||
reinterpret_cast<const uint8_t*>(contents.data()),
|
||||
static_cast<uint32_t>(contents.size()),
|
||||
true
|
||||
)) {
|
||||
unzClose(zipFile);
|
||||
return false;
|
||||
}
|
||||
wroteFile = true;
|
||||
}
|
||||
|
||||
if (i + 1 < globalInfo.number_entry && unzGoToNextFile(zipFile) != UNZ_OK) {
|
||||
g_logger.error("Unable to advance zip archive '{}' to next entry", path);
|
||||
unzClose(zipFile);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
unzClose(zipFile);
|
||||
return wroteFile;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void ResourceManager::init(const char* argv0)
|
||||
|
|
@ -833,12 +940,19 @@ bool ResourceManager::writeDownloadedFileToWorkDir(const std::string& path, std:
|
|||
bool ResourceManager::extractDownloadedArchive(const std::string& path, std::string destinationPath, const std::string& entryPrefix, const bool stripPrefix)
|
||||
{
|
||||
#ifndef FRAMEWORK_HAVE_LIBARCHIVE
|
||||
(void)path;
|
||||
(void)destinationPath;
|
||||
(void)entryPrefix;
|
||||
(void)stripPrefix;
|
||||
g_logger.error("Archive extraction is unavailable on this platform.");
|
||||
return false;
|
||||
const auto downloadedFile = getDownloadedFile(path);
|
||||
if (!downloadedFile) {
|
||||
g_logger.error("Cannot find downloaded archive '{}'", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& archive = downloadedFile->response;
|
||||
if (archive.empty()) {
|
||||
g_logger.error("Downloaded archive '{}' is empty", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
return extractDownloadedZipArchive(*this, path, archive, std::move(destinationPath), entryPrefix, stripPrefix);
|
||||
#else
|
||||
const auto downloadedFile = getDownloadedFile(path);
|
||||
if (!downloadedFile) {
|
||||
|
|
|
|||
|
|
@ -411,6 +411,26 @@
|
|||
<ClCompile Include="..\src\framework\core\logger.cpp" />
|
||||
<ClCompile Include="..\src\framework\core\module.cpp" />
|
||||
<ClCompile Include="..\src\framework\core\modulemanager.cpp" />
|
||||
<ClCompile Include="..\src\framework\core\minizip\crypt.c">
|
||||
<IncludeInUnityFile>false</IncludeInUnityFile>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<ForcedIncludeFiles></ForcedIncludeFiles>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\framework\core\minizip\ioapi.c">
|
||||
<IncludeInUnityFile>false</IncludeInUnityFile>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<ForcedIncludeFiles></ForcedIncludeFiles>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\framework\core\minizip\ioapi_mem.c">
|
||||
<IncludeInUnityFile>false</IncludeInUnityFile>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<ForcedIncludeFiles></ForcedIncludeFiles>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\framework\core\minizip\unzip.c">
|
||||
<IncludeInUnityFile>false</IncludeInUnityFile>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<ForcedIncludeFiles></ForcedIncludeFiles>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\framework\core\resourcemanager.cpp" />
|
||||
<ClCompile Include="..\src\framework\core\scheduledevent.cpp" />
|
||||
<ClCompile Include="..\src\framework\core\timer.cpp" />
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
FRAMEWORK_NET;
|
||||
FRAMEWORK_SOUND;
|
||||
FRAMEWORK_PROTOBUF;
|
||||
FRAMEWORK_HAVE_LIBARCHIVE;
|
||||
USE_PRECOMPILED_HEADERS;
|
||||
OPENSSL_API_COMPAT=0x10100000L;
|
||||
OPENSSL_SUPPRESS_DEPRECATED;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue