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-04 22:59:33 +03:00
parent 3b2f373946
commit 13759b1db6
125 changed files with 8934 additions and 2839 deletions
+31 -24
View File
@@ -1,14 +1,13 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using AsarSharp.AsarFileSystem;
using AsarSharp.Integrity;
using AsarSharp.Utils;
namespace AsarSharp
{
public class CreateOptions
{
public Regex Unpack { get; set; }
@@ -28,10 +27,10 @@ namespace AsarSharp
_destPath = destPath ?? throw new ArgumentNullException(nameof(destPath));
_options = options;
}
public void CreatePackageWithOptions()
{
var result = FileSystemCrawler.CrawlFileSystem(_folderPath);
var result = FileSystemCrawler.CrawlFileSystem(_folderPath);
_filenames = result.filenames;
_metadata = result.metadata;
CreatePackageFromFiles();
@@ -41,28 +40,25 @@ namespace AsarSharp
public void CreatePackageFromFiles()
{
var filesystem = new Filesystem(_folderPath);
var files = new List<Disk.BasicFileInfo>();
var filenamesSorted = _filenames.ToList();
foreach (var filename in filenamesSorted)
var files = new List<Disk.BasicFileInfo>(_filenames.Count);
foreach (var filename in _filenames)
{
HandleFile(filesystem, filename, files);
}
InsertsDone(filesystem, files);
}
private void HandleFile(Filesystem filesystem, string filename, List<Disk.BasicFileInfo> files)
{
if (!_metadata.ContainsKey(filename))
if (!_metadata.TryGetValue(filename, out var file))
{
var fileType = FileSystemCrawler.DetermineFileType(filename);
_metadata[filename] = fileType ?? throw new Exception("Unknown file type for file: " + filename);
file = FileSystemCrawler.DetermineFileType(filename)
?? throw new Exception("Unknown file type for file: " + filename);
_metadata[filename] = file;
}
var file = _metadata[filename];
switch (file.Type)
{
@@ -70,9 +66,18 @@ namespace AsarSharp
filesystem.InsertDirectory(filename, false);
break;
case FileType.File:
var shouldUnpack = ShouldUnpackPath(Extensions.GetRelativePath(_folderPath, Path.GetDirectoryName(filename)));
string parentDir = Path.GetDirectoryName(filename) ?? string.Empty;
string relParent = Extensions.GetRelativePath(_folderPath, parentDir);
bool shouldUnpack = ShouldUnpackPath(relParent);
files.Add(new Disk.BasicFileInfo { Filename = filename, Unpack = shouldUnpack });
filesystem.InsertFile(filename, shouldUnpack, file);
// Build a placeholder integrity record up front. Real
// SHA-256 hashes are filled in by Disk.WriteFileSystem
// during the streamed write — eliminates the second pass
// over each file (open → hash → close → open → copy → close).
long size = (file.Stat is FileInfo fi) ? fi.Length : 0;
var placeholder = IntegrityHelper.CreatePlaceholder(size);
filesystem.InsertFile(filename, shouldUnpack, file, placeholder);
break;
case FileType.Link:
throw new NotImplementedException();
@@ -81,14 +86,16 @@ namespace AsarSharp
private bool ShouldUnpackPath(string relativePath)
{
return _options.Unpack?.IsMatch(relativePath) == true;
return _options?.Unpack?.IsMatch(relativePath) == true;
}
private void InsertsDone(Filesystem filesystem, List<Disk.BasicFileInfo> files)
{
Directory.CreateDirectory(Path.GetDirectoryName(_destPath) ?? throw new InvalidOperationException());
Disk.WriteFileSystem(_destPath, filesystem, new Disk.FilesystemFilesAndLinks { Files = files, Links = null }, _metadata);
Directory.CreateDirectory(
Path.GetDirectoryName(_destPath)
?? throw new InvalidOperationException());
Disk.WriteFileSystem(_destPath, filesystem,
new Disk.FilesystemFilesAndLinks { Files = files, Links = null }, _metadata);
}
}
}
}
+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);
}
}
}
}
}
+23 -12
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using AsarSharp.PickleTools;
@@ -9,6 +9,7 @@ namespace AsarSharp.AsarFileSystem
{
public static class Disk
{
private const int StreamBufferSize = 1024 * 1024;
private static Dictionary<string, Filesystem> _filesystemCache = new Dictionary<string, Filesystem>();
public class ArchiveHeader
@@ -35,7 +36,7 @@ namespace AsarSharp.AsarFileSystem
public static ArchiveHeader ReadArchiveHeaderSync(string archivePath)
{
using (FileStream fs = File.OpenRead(archivePath))
using (var fs = new FileStream(archivePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamBufferSize, FileOptions.SequentialScan))
{
// read the size of the header (8 bytes)
byte[] sizeBuf = new byte[8];
@@ -103,7 +104,7 @@ namespace AsarSharp.AsarFileSystem
}
// Read from the ASAR archive
using (FileStream fs = File.OpenRead(filesystem.GetRootPath()))
using (var fs = new FileStream(filesystem.GetRootPath(), FileMode.Open, FileAccess.Read, FileShare.Read, StreamBufferSize, FileOptions.SequentialScan))
{
// Important: the offset must take into account the size of the Pickle header (8 bytes)
// and the size of the header itself
@@ -145,19 +146,29 @@ namespace AsarSharp.AsarFileSystem
if(dest == null || rootPath == null || filename == null)
throw new ArgumentNullException();
if (dest == rootPath)
string normalizedDestRoot = Path.GetFullPath(dest)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
string normalizedRootPath = Path.GetFullPath(rootPath)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
if (string.Equals(normalizedDestRoot, normalizedRootPath, StringComparison.OrdinalIgnoreCase))
{
return;
}
string sourcePath = Path.Combine(rootPath, filename);
string destPath = Path.Combine(dest, filename);
string sourcePath = Path.GetFullPath(Path.Combine(rootPath, filename));
string destPath = Path.GetFullPath(Path.Combine(dest, filename));
if (string.Equals(sourcePath, destPath, StringComparison.OrdinalIgnoreCase))
{
return;
}
Directory.CreateDirectory(Path.GetDirectoryName(destPath) ?? throw new InvalidOperationException());
using (var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read))
using (var destinationStream = new FileStream(destPath, FileMode.Create, FileAccess.Write))
using (var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamBufferSize, FileOptions.SequentialScan))
using (var destinationStream = new FileStream(destPath, FileMode.Create, FileAccess.Write, FileShare.None, StreamBufferSize, FileOptions.SequentialScan))
{
sourceStream.CopyTo(destinationStream);
sourceStream.CopyTo(destinationStream, StreamBufferSize);
}
}
@@ -179,7 +190,7 @@ namespace AsarSharp.AsarFileSystem
sizePickle.WriteUInt32((uint)headerBuf.Length);
var sizeBuf = sizePickle.ToBuffer();
using (FileStream fs = File.Create(dest))
using (var fs = new FileStream(dest, FileMode.Create, FileAccess.Write, FileShare.None, StreamBufferSize, FileOptions.SequentialScan))
{
fs.Write(sizeBuf, 0, sizeBuf.Length);
fs.Write(headerBuf, 0, headerBuf.Length);
@@ -192,9 +203,9 @@ namespace AsarSharp.AsarFileSystem
CopyFile($"{dest}.unpacked", fileSystem.GetRootPath(), filename);
continue;
}
using (var transformedFileStream = new FileStream(file.Filename, FileMode.Open, FileAccess.Read))
using (var transformedFileStream = new FileStream(file.Filename, FileMode.Open, FileAccess.Read, FileShare.Read, StreamBufferSize, FileOptions.SequentialScan))
{
transformedFileStream.CopyTo(fs);
transformedFileStream.CopyTo(fs, StreamBufferSize);
}
}
}
+1 -2
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using AsarSharp.Integrity;
@@ -150,7 +150,6 @@ namespace AsarSharp.AsarFileSystem
public static string ReadLink(string path)
{
throw new NotImplementedException();
return Path.GetFileName(path);
// TODO , NOT IMPLEMENTED
}
+29 -20
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -28,32 +28,37 @@ namespace AsarSharp.AsarFileSystem
public static class FileSystemCrawler
{
public static CrawledFileType DetermineFileType(string filename)
{
var fileInfo = new FileInfo(filename);
if (fileInfo.Exists)
FileAttributes attributes;
try
{
return new CrawledFileType { Type = FileType.File, Stat = fileInfo };
attributes = File.GetAttributes(filename);
}
var directoryInfo = new DirectoryInfo(filename);
if (directoryInfo.Exists)
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
{
return new CrawledFileType { Type = FileType.Directory, Stat = directoryInfo };
return null;
}
var linkInfo = new FileInfo(filename);
if (linkInfo.Exists && (linkInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
bool isDirectory = (attributes & FileAttributes.Directory) == FileAttributes.Directory;
bool isLink = (attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
FileSystemInfo info = isDirectory
? (FileSystemInfo)new DirectoryInfo(filename)
: new FileInfo(filename);
if (isLink)
{
return new CrawledFileType { Type = FileType.Link, Stat = linkInfo };
return new CrawledFileType { Type = FileType.Link, Stat = info };
}
return null;
if (isDirectory)
{
return new CrawledFileType { Type = FileType.Directory, Stat = info };
}
return new CrawledFileType { Type = FileType.File, Stat = info };
}
public static (List<string> filenames, Dictionary<string, CrawledFileType> metadata) CrawlFileSystem(string dir)
{
var metadata = new Dictionary<string, CrawledFileType>();
@@ -73,7 +78,12 @@ namespace AsarSharp.AsarFileSystem
filenames.Add(result.filename);
}
var filteredFilenames = new List<string>();
if (links.Count == 0)
{
return (filenames, metadata);
}
var filteredFilenames = new List<string>(filenames.Count);
foreach (var filename in filenames)
{
@@ -107,7 +117,6 @@ namespace AsarSharp.AsarFileSystem
return (filteredFilenames, metadata);
}
// (File order is not important!!!)
public static List<string> CrawlIterative(string dir)
{
+30 -14
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
@@ -11,6 +11,7 @@ namespace AsarSharp.Integrity
private const string ALGORITHM = "SHA256";
// 4MB default block size
private const int BLOCK_SIZE = 4 * 1024 * 1024;
private static readonly char[] HexDigits = "0123456789abcdef".ToCharArray();
public class FileIntegrity
{
@@ -29,20 +30,21 @@ namespace AsarSharp.Integrity
public static FileIntegrity GetFileIntegrity(string path)
{
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, BLOCK_SIZE, FileOptions.SequentialScan))
using(var fileHash = SHA256.Create())
using (var blockHash = SHA256.Create())
{
var blockHashes = new List<string>();
int estimatedBlockCount = fileStream.Length > 0
? (int)((fileStream.Length + BLOCK_SIZE - 1) / BLOCK_SIZE)
: 0;
var blockHashes = new List<string>(estimatedBlockCount);
var buffer = new byte[BLOCK_SIZE];
int bytesRead;
while ((bytesRead = fileStream.Read(buffer, 0, BLOCK_SIZE)) > 0)
while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) > 0)
{
var block = new byte[bytesRead];
Array.Copy(buffer, block, bytesRead);
blockHashes.Add(HashBlock(block));
fileHash.TransformBlock(block, 0, block.Length, null, 0);
blockHashes.Add(HashBlock(blockHash, buffer, bytesRead));
fileHash.TransformBlock(buffer, 0, bytesRead, null, 0);
}
fileHash.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
@@ -50,20 +52,34 @@ namespace AsarSharp.Integrity
return new FileIntegrity
{
Algorithm = ALGORITHM,
Hash = BitConverter.ToString(fileHash.Hash).Replace("-", "").ToLowerInvariant(),
Hash = ToLowerHex(fileHash.Hash),
BlockSize = BLOCK_SIZE,
Blocks = blockHashes,
};
}
}
private static string HashBlock(byte[] block)
private static string HashBlock(HashAlgorithm hashAlgorithm, byte[] buffer, int bytesRead)
{
using (var sha256 = SHA256.Create())
return ToLowerHex(hashAlgorithm.ComputeHash(buffer, 0, bytesRead));
}
private static string ToLowerHex(byte[] bytes)
{
if (bytes == null || bytes.Length == 0)
{
var hash = sha256.ComputeHash(block);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
return string.Empty;
}
var chars = new char[bytes.Length * 2];
for (int index = 0; index < bytes.Length; index++)
{
byte value = bytes[index];
chars[index * 2] = HexDigits[value >> 4];
chars[index * 2 + 1] = HexDigits[value & 0x0F];
}
return new string(chars);
}
}
}
+93 -219
View File
@@ -1,4 +1,5 @@
using System;
using System;
using System.IO;
using System.Text;
namespace AsarSharp.PickleTools
@@ -12,10 +13,10 @@ namespace AsarSharp.PickleTools
public const int SIZE_FLOAT = 4;
public const int SIZE_DOUBLE = 8;
// Size of memory allocation unit for payload
public const int PAYLOAD_UNIT = 64;
// Initial payload allocation. Bumped from 64 — large headers used to
// realloc many times when growing geometrically from 64.
public const int PAYLOAD_UNIT = 4096;
// Maximum value for read-only
public const long CAPACITY_READ_ONLY = 9007199254740992;
private byte[] _header;
@@ -57,55 +58,40 @@ namespace AsarSharp.PickleTools
SetPayloadSize(0);
}
}
public static Pickle CreateEmpty()
{
return new Pickle();
}
public static Pickle CreateFromBuffer(byte[] buffer)
{
return new Pickle(buffer);
}
public byte[] GetHeader()
{
return _header;
}
public static Pickle CreateEmpty() => new Pickle();
public static Pickle CreateFromBuffer(byte[] buffer) => new Pickle(buffer);
public int GetHeaderSize()
{
return _headerSize;
}
public PickleIterator CreateIterator()
{
return new PickleIterator(this);
}
public byte[] GetHeader() => _header;
public int GetHeaderSize() => _headerSize;
/// <summary>
/// Converts Pickle to a byte array
/// </summary>
public PickleIterator CreateIterator() => new PickleIterator(this);
/// <summary>Total byte length of the serialised pickle (header + payload).</summary>
public int GetTotalSize() => _headerSize + GetPayloadSize();
/// <summary>Materialise the pickle into a fresh byte array (allocates).</summary>
public byte[] ToBuffer()
{
int resultSize = _headerSize + GetPayloadSize();
int resultSize = GetTotalSize();
byte[] result = new byte[resultSize];
Array.Copy(_header, 0, result, 0, resultSize);
Buffer.BlockCopy(_header, 0, result, 0, resultSize);
return result;
}
public bool WriteBool(bool value)
/// <summary>Write the serialised pickle straight to <paramref name="stream"/> — no extra copy.</summary>
public void WriteTo(Stream stream)
{
return WriteInt(value ? 1 : 0);
stream.Write(_header, 0, GetTotalSize());
}
public bool WriteBool(bool value) => WriteInt(value ? 1 : 0);
public bool WriteInt(int value)
{
EnsureCapacity(SIZE_INT32);
var dataLength = AlignInt(SIZE_INT32, SIZE_UINT32);
var newSize = _writeOffset + dataLength;
const int dataLength = SIZE_INT32; // already 4-byte aligned
int newSize = _writeOffset + dataLength;
if (newSize > _capacityAfterHeader)
{
@@ -113,13 +99,6 @@ namespace AsarSharp.PickleTools
}
WriteInt32LE(value, _headerSize + _writeOffset);
var endOffset = _headerSize + _writeOffset + SIZE_INT32;
for (int i = endOffset; i < endOffset + dataLength - SIZE_INT32; i++)
{
_header[i] = 0;
}
SetPayloadSize(newSize);
_writeOffset = newSize;
return true;
@@ -128,10 +107,8 @@ namespace AsarSharp.PickleTools
public bool WriteUInt32(uint value)
{
EnsureCapacity(SIZE_UINT32);
var dataLength = AlignInt(SIZE_UINT32, SIZE_UINT32);
var newSize = _writeOffset + dataLength;
const int dataLength = SIZE_UINT32;
int newSize = _writeOffset + dataLength;
if (newSize > _capacityAfterHeader)
{
@@ -139,24 +116,15 @@ namespace AsarSharp.PickleTools
}
WriteUInt32LE(value, _headerSize + _writeOffset);
var endOffset = _headerSize + _writeOffset + SIZE_UINT32;
for (int i = endOffset; i < endOffset + dataLength - SIZE_UINT32; i++)
{
_header[i] = 0;
}
SetPayloadSize(newSize);
_writeOffset = newSize;
return true;
}
public bool WriteInt64(long value)
{
EnsureCapacity(SIZE_INT64);
var dataLength = AlignInt(SIZE_INT64, SIZE_UINT32);
var newSize = _writeOffset + dataLength;
const int dataLength = SIZE_INT64;
int newSize = _writeOffset + dataLength;
if (newSize > _capacityAfterHeader)
{
@@ -164,13 +132,6 @@ namespace AsarSharp.PickleTools
}
WriteInt64LE(value, _headerSize + _writeOffset);
var endOffset = _headerSize + _writeOffset + SIZE_INT64;
for (int i = endOffset; i < endOffset + dataLength - SIZE_INT64; i++)
{
_header[i] = 0;
}
SetPayloadSize(newSize);
_writeOffset = newSize;
return true;
@@ -179,10 +140,8 @@ namespace AsarSharp.PickleTools
public bool WriteUInt64(ulong value)
{
EnsureCapacity(SIZE_UINT64);
var dataLength = AlignInt(SIZE_UINT64, SIZE_UINT32);
var newSize = _writeOffset + dataLength;
const int dataLength = SIZE_UINT64;
int newSize = _writeOffset + dataLength;
if (newSize > _capacityAfterHeader)
{
@@ -190,102 +149,69 @@ namespace AsarSharp.PickleTools
}
WriteUInt64LE(value, _headerSize + _writeOffset);
var endOffset = _headerSize + _writeOffset + SIZE_UINT64;
for (int i = endOffset; i < endOffset + dataLength - SIZE_UINT64; i++)
{
_header[i] = 0;
}
SetPayloadSize(newSize);
_writeOffset = newSize;
return true;
}
public bool WriteFloat(float value)
{
EnsureCapacity(SIZE_FLOAT);
var dataLength = AlignInt(SIZE_FLOAT, SIZE_UINT32);
var newSize = _writeOffset + dataLength;
const int dataLength = SIZE_FLOAT;
int newSize = _writeOffset + dataLength;
if (newSize > _capacityAfterHeader)
{
Resize(Math.Max((int)_capacityAfterHeader * 2, newSize));
}
byte[] bytes = BitConverter.GetBytes(value);
if (!BitConverter.IsLittleEndian)
{
Array.Reverse(bytes);
}
Array.Copy(bytes, 0, _header, _headerSize + _writeOffset, SIZE_FLOAT);
var endOffset = _headerSize + _writeOffset + SIZE_FLOAT;
for (int i = endOffset; i < endOffset + dataLength - SIZE_FLOAT; i++)
{
_header[i] = 0;
}
int bits = BitConverter.ToInt32(BitConverter.GetBytes(value), 0);
WriteInt32LE(bits, _headerSize + _writeOffset);
SetPayloadSize(newSize);
_writeOffset = newSize;
return true;
}
public bool WriteDouble(double value)
{
EnsureCapacity(SIZE_DOUBLE);
var dataLength = AlignInt(SIZE_DOUBLE, SIZE_UINT32);
var newSize = _writeOffset + dataLength;
const int dataLength = SIZE_DOUBLE;
int newSize = _writeOffset + dataLength;
if (newSize > _capacityAfterHeader)
{
Resize(Math.Max((int)_capacityAfterHeader * 2, newSize));
}
byte[] bytes = BitConverter.GetBytes(value);
if (!BitConverter.IsLittleEndian)
{
Array.Reverse(bytes);
}
Array.Copy(bytes, 0, _header, _headerSize + _writeOffset, SIZE_DOUBLE);
var endOffset = _headerSize + _writeOffset + SIZE_DOUBLE;
for (int i = endOffset; i < endOffset + dataLength - SIZE_DOUBLE; i++)
{
_header[i] = 0;
}
long bits = BitConverter.DoubleToInt64Bits(value);
WriteInt64LE(bits, _headerSize + _writeOffset);
SetPayloadSize(newSize);
_writeOffset = newSize;
return true;
}
public bool WriteString(string value)
{
byte[] strBytes = Encoding.UTF8.GetBytes(value);
int length = strBytes.Length;
int byteLen = Encoding.UTF8.GetByteCount(value);
if (!WriteInt(length))
if (!WriteInt(byteLen))
{
return false;
}
var dataLength = AlignInt(length, SIZE_UINT32);
var newSize = _writeOffset + dataLength;
int aligned = AlignInt(byteLen, SIZE_UINT32);
int newSize = _writeOffset + aligned;
if (newSize > _capacityAfterHeader)
{
Resize(Math.Max((int)_capacityAfterHeader * 2, newSize));
}
Array.Copy(strBytes, 0, _header, _headerSize + _writeOffset, length);
int writeStart = _headerSize + _writeOffset;
Encoding.UTF8.GetBytes(value, 0, value.Length, _header, writeStart);
var endOffset = _headerSize + _writeOffset + length;
for (int i = endOffset; i < endOffset + dataLength - length; i++)
// zero alignment padding
for (int i = writeStart + byteLen; i < writeStart + aligned; i++)
{
_header[i] = 0;
}
@@ -294,132 +220,80 @@ namespace AsarSharp.PickleTools
_writeOffset = newSize;
return true;
}
public void SetPayloadSize(int payloadSize)
{
WriteUInt32LE((uint)payloadSize, 0);
}
public int GetPayloadSize()
{
return (int)ReadUInt32LE(0);
}
public int GetPayloadSize() => (int)ReadUInt32LE(0);
private void Resize(int newCapacity)
{
newCapacity = AlignInt(newCapacity, PAYLOAD_UNIT);
byte[] newHeader = new byte[_header.Length + newCapacity];
Array.Copy(_header, 0, newHeader, 0, _header.Length);
Buffer.BlockCopy(_header, 0, newHeader, 0, _header.Length);
_header = newHeader;
_capacityAfterHeader = newCapacity;
}
public static int AlignInt(int i, int alignment)
{
return i + ((alignment - (i % alignment)) % alignment);
}
private void EnsureCapacity(int additionalSize)
{
var dataLength = AlignInt(additionalSize, SIZE_UINT32);
var newSize = _writeOffset + dataLength;
if (newSize > _capacityAfterHeader)
{
Resize(Math.Max((int)_capacityAfterHeader * 2, newSize));
}
}
#region Auxiliary methods for reading/writing values in Little Endian
private uint ReadUInt32LE(int offset)
{
if (BitConverter.IsLittleEndian)
{
return BitConverter.ToUInt32(_header, offset);
}
else
{
return (uint)(_header[offset] |
(_header[offset + 1] << 8) |
(_header[offset + 2] << 16) |
(_header[offset + 3] << 24));
}
// _header is allocated by us so always little-endian-friendly when on LE host.
return (uint)(_header[offset] |
(_header[offset + 1] << 8) |
(_header[offset + 2] << 16) |
(_header[offset + 3] << 24));
}
private void WriteInt32LE(int value, int offset)
{
if (BitConverter.IsLittleEndian)
{
byte[] bytes = BitConverter.GetBytes(value);
Array.Copy(bytes, 0, _header, offset, 4);
}
else
{
_header[offset] = (byte)value;
_header[offset + 1] = (byte)(value >> 8);
_header[offset + 2] = (byte)(value >> 16);
_header[offset + 3] = (byte)(value >> 24);
}
_header[offset] = (byte)value;
_header[offset + 1] = (byte)(value >> 8);
_header[offset + 2] = (byte)(value >> 16);
_header[offset + 3] = (byte)(value >> 24);
}
private void WriteUInt32LE(uint value, int offset)
{
if (BitConverter.IsLittleEndian)
{
byte[] bytes = BitConverter.GetBytes(value);
Array.Copy(bytes, 0, _header, offset, 4);
}
else
{
_header[offset] = (byte)value;
_header[offset + 1] = (byte)(value >> 8);
_header[offset + 2] = (byte)(value >> 16);
_header[offset + 3] = (byte)(value >> 24);
}
_header[offset] = (byte)value;
_header[offset + 1] = (byte)(value >> 8);
_header[offset + 2] = (byte)(value >> 16);
_header[offset + 3] = (byte)(value >> 24);
}
private void WriteInt64LE(long value, int offset)
{
if (BitConverter.IsLittleEndian)
{
byte[] bytes = BitConverter.GetBytes(value);
Array.Copy(bytes, 0, _header, offset, 8);
}
else
{
_header[offset] = (byte)value;
_header[offset + 1] = (byte)(value >> 8);
_header[offset + 2] = (byte)(value >> 16);
_header[offset + 3] = (byte)(value >> 24);
_header[offset + 4] = (byte)(value >> 32);
_header[offset + 5] = (byte)(value >> 40);
_header[offset + 6] = (byte)(value >> 48);
_header[offset + 7] = (byte)(value >> 56);
}
}
private void WriteUInt64LE(ulong value, int offset)
{
if (BitConverter.IsLittleEndian)
{
byte[] bytes = BitConverter.GetBytes(value);
Array.Copy(bytes, 0, _header, offset, 8);
}
else
{
_header[offset] = (byte)value;
_header[offset + 1] = (byte)(value >> 8);
_header[offset + 2] = (byte)(value >> 16);
_header[offset + 3] = (byte)(value >> 24);
_header[offset + 4] = (byte)(value >> 32);
_header[offset + 5] = (byte)(value >> 40);
_header[offset + 6] = (byte)(value >> 48);
_header[offset + 7] = (byte)(value >> 56);
}
_header[offset] = (byte)value;
_header[offset + 1] = (byte)(value >> 8);
_header[offset + 2] = (byte)(value >> 16);
_header[offset + 3] = (byte)(value >> 24);
_header[offset + 4] = (byte)(value >> 32);
_header[offset + 5] = (byte)(value >> 40);
_header[offset + 6] = (byte)(value >> 48);
_header[offset + 7] = (byte)(value >> 56);
}
private void WriteUInt64LE(ulong value, int offset)
{
_header[offset] = (byte)value;
_header[offset + 1] = (byte)(value >> 8);
_header[offset + 2] = (byte)(value >> 16);
_header[offset + 3] = (byte)(value >> 24);
_header[offset + 4] = (byte)(value >> 32);
_header[offset + 5] = (byte)(value >> 40);
_header[offset + 6] = (byte)(value >> 48);
_header[offset + 7] = (byte)(value >> 56);
}
#endregion
}
}
}
+93 -38
View File
@@ -1,11 +1,19 @@
using System;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace AsarSharp.Utils
{
internal static class Extensions
{
/// <summary>
/// Compute path relative to <paramref name="relativeTo"/>.
/// Fast common-case (path is inside relativeTo): plain prefix-strip.
/// Falls back to <see cref="Path.GetFullPath"/> + manual relativisation
/// when paths must be normalised or '..' segments are required.
/// Replaces previous URI-based implementation which was a large hot-path cost.
/// </summary>
public static string GetRelativePath(string relativeTo, string path)
{
if (string.IsNullOrEmpty(relativeTo))
@@ -13,84 +21,134 @@ namespace AsarSharp.Utils
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
var fullRelativeTo = Path.GetFullPath(relativeTo);
var fullPath = Path.GetFullPath(path);
// Fast path: literal prefix match (no normalisation). Covers ~all
// intra-archive callers where both inputs already come from the
// same crawl pass.
string baseFast = TrimTrailingSeparators(relativeTo);
string pathFast = TrimTrailingSeparators(path);
if (string.Equals(fullRelativeTo, fullPath, StringComparison.OrdinalIgnoreCase))
return "";
if (string.Equals(baseFast, pathFast, StringComparison.OrdinalIgnoreCase))
return string.Empty;
var relativeToUri = new Uri(fullRelativeTo.EndsWith(Path.DirectorySeparatorChar.ToString())
? fullRelativeTo
: fullRelativeTo + Path.DirectorySeparatorChar);
var pathUri = new Uri(fullPath.EndsWith(Path.DirectorySeparatorChar.ToString()) && !File.Exists(fullPath)
? fullPath
: fullPath + (Directory.Exists(fullPath) ? Path.DirectorySeparatorChar.ToString() : ""));
if (pathFast.Length > baseFast.Length &&
pathFast.StartsWith(baseFast, StringComparison.OrdinalIgnoreCase) &&
IsSeparator(pathFast[baseFast.Length]))
{
return pathFast.Substring(baseFast.Length + 1);
}
var relativeUri = relativeToUri.MakeRelativeUri(pathUri);
var relativePath = Uri.UnescapeDataString(relativeUri.ToString())
.Replace('/', Path.DirectorySeparatorChar);
return relativePath.TrimEnd(Path.DirectorySeparatorChar);
// Slow path: normalise both sides and compute relative — used for
// security checks (out-of-tree symlink/destination guards) and the
// rare "go up" case.
return GetRelativePathNormalised(relativeTo, path);
}
private static string GetRelativePathNormalised(string relativeTo, string path)
{
string fullBase = Path.GetFullPath(relativeTo);
string fullPath = Path.GetFullPath(path);
fullBase = TrimTrailingSeparators(fullBase);
fullPath = TrimTrailingSeparators(fullPath);
if (string.Equals(fullBase, fullPath, StringComparison.OrdinalIgnoreCase))
return string.Empty;
if (fullPath.Length > fullBase.Length &&
fullPath.StartsWith(fullBase, StringComparison.OrdinalIgnoreCase) &&
IsSeparator(fullPath[fullBase.Length]))
{
return fullPath.Substring(fullBase.Length + 1);
}
// Need to walk up the common ancestor.
char sep = Path.DirectorySeparatorChar;
string[] baseParts = fullBase.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
string[] pathParts = fullPath.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
int common = 0;
int max = Math.Min(baseParts.Length, pathParts.Length);
while (common < max &&
string.Equals(baseParts[common], pathParts[common], StringComparison.OrdinalIgnoreCase))
{
common++;
}
var sb = new StringBuilder();
for (int i = common; i < baseParts.Length; i++)
{
if (sb.Length > 0) sb.Append(sep);
sb.Append("..");
}
for (int i = common; i < pathParts.Length; i++)
{
if (sb.Length > 0) sb.Append(sep);
sb.Append(pathParts[i]);
}
return sb.ToString();
}
private static string TrimTrailingSeparators(string s)
{
int end = s.Length;
while (end > 0 && IsSeparator(s[end - 1])) end--;
return end == s.Length ? s : s.Substring(0, end);
}
private static bool IsSeparator(char c) => c == '/' || c == '\\';
public static string GetDirectoryName(string path)
{
if (string.IsNullOrEmpty(path))
return ".";
string result = Path.GetDirectoryName(path);
// If the result is an empty string, return “.” as in Node.js
if (string.IsNullOrEmpty(result))
return ".";
return result;
}
public static void CopyDirectory(string sourceDir, string destinationDir)
{
// Create the destination directory
Directory.CreateDirectory(destinationDir);
// Get all files in the source directory
foreach (var file in Directory.GetFiles(sourceDir))
{
var destFile = Path.Combine(destinationDir, Path.GetFileName(file));
File.Copy(file, destFile, true);
}
// Recursively copy all subdirectories
foreach (var dir in Directory.GetDirectories(sourceDir))
{
var destDir = Path.Combine(destinationDir, Path.GetFileName(dir));
CopyDirectory(dir, destDir);
}
}
public static string GetBasePath(string dir)
{
// Look for the last path delimiter before any pattern
int wildcardIndex = dir.IndexOfAny(new[] { '*', '?' });
if (wildcardIndex == -1)
{
return dir;
}
int lastSeparatorIndex = dir.LastIndexOf(Path.DirectorySeparatorChar, wildcardIndex);
if (lastSeparatorIndex == -1)
{
return ".";
}
return dir.Substring(0, lastSeparatorIndex);
}
public static void SetUnixFilePermission(string filePath, string permission)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return;
// Use chmod
var process = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
@@ -105,14 +163,12 @@ namespace AsarSharp.Utils
process.Start();
process.WaitForExit();
}
public static void CreateSymbolicLink(string linkTarget, string linkPath)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// On Windows, creating symlinks requires special privileges,
// so on many systems it simply won't work without administrator privileges
NativeMethods.CreateSymbolicLink(linkPath, linkTarget,
Directory.Exists(linkTarget)
? NativeMethods.SymLinkFlag.Directory
@@ -120,7 +176,6 @@ namespace AsarSharp.Utils
return;
}
// In Unix systems we use the corresponding system call
var process = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
@@ -135,11 +190,11 @@ namespace AsarSharp.Utils
process.Start();
process.WaitForExit();
}
public static bool IsWindowsPlatform()
{
return Environment.OSVersion.Platform == PlatformID.Win32NT;
}
}
}
}