feat: ship 1.0.8.0 release automation and runtime overhaul

- reduce ASAR IO overhead with streamed archive reads, buffered copies, faster relative-path handling and placeholder integrity records
- fix in-place app.asar.unpacked packing/extraction self-copy cases that caused locked-file failures
- tighten JS patch discovery with candidate bundle filters and search hints
- require prebuilt remote-panel dist artifacts and clean up embedded bridge/script packaging
- add unified build entrypoints for PowerShell, cmd and bash and move native CMake output under .tmp
- add release metadata validation, changelog section extraction, pre-commit hook and GitHub Actions validation/release pipelines
- make CHANGELOG the source of truth for release notes and document the tag-driven release flow
- add updater release notes UI with latest/full changelog loading and localize the new update strings
- modularize bridge renderer scripts, add installed apps and game status sync, and support remote launch/stop commands
- centralize bridge protocol, IPC and WebSocket constants and improve LAN IP selection for QR pairing
- refactor remote panel controls/state enums, persist accent color, polish library/session UI and refresh assets
This commit is contained in:
kitbyte
2026-05-06 13:07:09 +03:00
parent 3b2f373946
commit 13759b1db6
125 changed files with 8933 additions and 2838 deletions
+136 -102
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -10,126 +10,62 @@ namespace AsarSharp
{
public class AsarExtractor
{
private const int IO_BUFFER_SIZE = 1024 * 1024;
private const int FS_INTERNAL_BUFFER = 4096;
public static void ExtractAll(string archivePath, string dest)
{
var filesystem = Disk.ReadFilesystemSync(archivePath);
var filenames = filesystem.ListFiles();
// under windows just extract links as regular files
// On Windows, links are extracted as plain files.
bool followLinks = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
// create destination directory
Directory.CreateDirectory(dest);
byte[] ioBuffer = new byte[IO_BUFFER_SIZE];
var dirCache = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { Path.GetFullPath(dest) };
var extractionErrors = new List<Exception>();
foreach (var fullPath in filenames)
string rootPath = filesystem.GetRootPath();
long dataOffset = 8 + filesystem.GetHeaderSize();
// One archive handle for all reads — old code opened it per file.
using (var archive = new FileStream(rootPath, FileMode.Open, FileAccess.Read, FileShare.Read,
FS_INTERNAL_BUFFER, FileOptions.RandomAccess))
{
try
foreach (var fullPath in filenames)
{
// Remove leading slash
var filename = fullPath.Substring(1);
var destFilename = Path.Combine(dest, filename);
var file = filesystem.GetFile(filename, followLinks);
// Check that the file is not written outside the specified destination folder
string relativePath = Extensions.GetRelativePath(dest, destFilename);
if (relativePath.StartsWith(".."))
try
{
throw new InvalidOperationException($"{fullPath}: file \"{destFilename}\" writes out of the package");
}
var filename = fullPath.Substring(1);
var destFilename = Path.Combine(dest, filename);
var file = filesystem.GetFile(filename, followLinks);
if (file.IsDirectory)
{
// it's a directory, create it and continue with the next entry
Directory.CreateDirectory(destFilename);
}
// TODO (LINK NOT SUPPORTED)
else if (file.IsLink)
{
// it's a symlink, create a symlink
var linkSrcPath = Extensions.GetDirectoryName(Path.Combine(dest, file.Link));
var linkDestPath = Extensions.GetDirectoryName(destFilename);
var relativeLinkPath = Extensions.GetRelativePath(linkDestPath, linkSrcPath);
// try to delete output file, because we can't overwrite a link
try
{
File.Delete(destFilename);
}
catch {
// Ignore errors during file link deletion
}
var linkTo = Path.Combine(relativeLinkPath, Path.GetFileName(file.Link));
if (Extensions.GetRelativePath(dest, linkSrcPath).StartsWith(".."))
// Path-traversal guard.
string relativePath = Extensions.GetRelativePath(dest, destFilename);
if (relativePath.StartsWith(".."))
{
throw new InvalidOperationException(
$"{fullPath}: file \"{file.Link}\" links out of the package to \"{linkSrcPath}\"");
$"{fullPath}: file \"{destFilename}\" writes out of the package");
}
// On Windows, creating symlinks requires additional permissions or enabling Developer Mode,
// so just copy the contents of the file
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (file.IsDirectory)
{
var targetPath = Path.Combine(linkSrcPath, Path.GetFileName(file.Link));
if (Directory.Exists(targetPath))
{
Directory.CreateDirectory(destFilename);
Extensions.CopyDirectory(targetPath, destFilename);
}
else if (File.Exists(targetPath))
{
Directory.CreateDirectory(Extensions.GetDirectoryName(destFilename));
File.Copy(targetPath, destFilename, true);
}
EnsureDirectory(destFilename, dirCache);
continue;
}
else
if (file.IsLink)
{
// On Unix systems we use symlinks
Directory.CreateDirectory(Extensions.GetDirectoryName(destFilename));
Extensions.CreateSymbolicLink(linkTo, destFilename);
ExtractLink(dest, fullPath, destFilename, file, dirCache);
continue;
}
}
else if (file.IsFile)
{
// it's a file, try to extract it
if (!file.IsFile) continue;
try
{
// Unpacked entries already live on disk next to the archive in
// "<archive>.unpacked". When the caller extracts INTO that same
// directory (e.g. re-extracting in place to repack later) reading +
// writing the file is a self-copy that needlessly fails when the
// file is locked by another process (TrainerLib_x64.dll) or has been
// removed from disk by an installer (auxiliary/GameLauncher.exe).
if (file.Unpacked == true)
{
string unpackedSourcePath = Path.GetFullPath(
Path.Combine($"{filesystem.GetRootPath()}.unpacked", filename));
string unpackedDestPath = Path.GetFullPath(destFilename);
if (string.Equals(unpackedSourcePath, unpackedDestPath, StringComparison.OrdinalIgnoreCase))
{
// Nothing to do the file is already at the destination.
continue;
}
if (!File.Exists(unpackedSourcePath))
{
// The header references an unpacked file that no longer
// exists on disk; skip it instead of aborting the whole
// extraction so the rest of the asar can still be repacked.
continue;
}
Directory.CreateDirectory(Extensions.GetDirectoryName(destFilename));
File.Copy(unpackedSourcePath, destFilename, true);
}
else
{
byte[] content = Disk.ReadFileSync(filesystem, filename, file);
File.WriteAllBytes(destFilename, content);
}
ExtractFile(archive, dataOffset, rootPath, filename, destFilename, file, ioBuffer, dirCache);
if (file.Executable == true && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
@@ -141,10 +77,10 @@ namespace AsarSharp
extractionErrors.Add(e);
}
}
}
catch (Exception ex)
{
extractionErrors.Add(ex);
catch (Exception ex)
{
extractionErrors.Add(ex);
}
}
}
@@ -156,5 +92,103 @@ namespace AsarSharp
extractionErrors);
}
}
private static void EnsureDirectory(string path, HashSet<string> cache)
{
string full = Path.GetFullPath(path);
if (cache.Contains(full)) return;
Directory.CreateDirectory(full);
// Mark every ancestor too so siblings skip the syscall.
string p = full;
while (!string.IsNullOrEmpty(p) && cache.Add(p))
{
p = Path.GetDirectoryName(p);
}
}
private static void EnsureParentDir(string filePath, HashSet<string> cache)
{
string parent = Path.GetDirectoryName(filePath);
if (string.IsNullOrEmpty(parent)) return;
EnsureDirectory(parent, cache);
}
private static void ExtractFile(FileStream archive, long dataOffset, string rootPath,
string filename, string destFilename, FilesystemEntry file, byte[] buffer,
HashSet<string> dirCache)
{
EnsureParentDir(destFilename, dirCache);
if (file.Unpacked == true)
{
string unpackedSourcePath = Path.GetFullPath(Path.Combine($"{rootPath}.unpacked", filename));
string unpackedDestPath = Path.GetFullPath(destFilename);
if (string.Equals(unpackedSourcePath, unpackedDestPath, StringComparison.OrdinalIgnoreCase))
return; // self-copy
if (!File.Exists(unpackedSourcePath))
return; // header references a missing unpacked file — skip rather than abort
File.Copy(unpackedSourcePath, destFilename, true);
return;
}
long size = file.Size ?? 0;
using (var dst = new FileStream(destFilename, FileMode.Create, FileAccess.Write, FileShare.None,
FS_INTERNAL_BUFFER, FileOptions.SequentialScan))
{
if (size <= 0) return;
archive.Position = dataOffset + long.Parse(file.Offset);
long remaining = size;
while (remaining > 0)
{
int toRead = remaining > buffer.Length ? buffer.Length : (int)remaining;
int got = archive.Read(buffer, 0, toRead);
if (got <= 0) throw new EndOfStreamException("Archive truncated");
dst.Write(buffer, 0, got);
remaining -= got;
}
}
}
private static void ExtractLink(string dest, string fullPath, string destFilename,
FilesystemEntry file, HashSet<string> dirCache)
{
var linkSrcPath = Extensions.GetDirectoryName(Path.Combine(dest, file.Link));
var linkDestPath = Extensions.GetDirectoryName(destFilename);
var relativeLinkPath = Extensions.GetRelativePath(linkDestPath, linkSrcPath);
try { File.Delete(destFilename); }
catch { /* ignore — failing to remove an existing link is non-fatal */ }
var linkTo = Path.Combine(relativeLinkPath, Path.GetFileName(file.Link));
if (Extensions.GetRelativePath(dest, linkSrcPath).StartsWith(".."))
{
throw new InvalidOperationException(
$"{fullPath}: file \"{file.Link}\" links out of the package to \"{linkSrcPath}\"");
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var targetPath = Path.Combine(linkSrcPath, Path.GetFileName(file.Link));
if (Directory.Exists(targetPath))
{
EnsureDirectory(destFilename, dirCache);
Extensions.CopyDirectory(targetPath, destFilename);
}
else if (File.Exists(targetPath))
{
EnsureParentDir(destFilename, dirCache);
File.Copy(targetPath, destFilename, true);
}
}
else
{
EnsureParentDir(destFilename, dirCache);
Extensions.CreateSymbolicLink(linkTo, destFilename);
}
}
}
}
}