- add: linux support
This commit is contained in:
@@ -1,40 +1,66 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Nebula.Shared.Utils;
|
||||||
|
|
||||||
namespace Nebula.Shared.Services;
|
namespace Nebula.Shared.Services;
|
||||||
|
|
||||||
[ServiceRegister]
|
[ServiceRegister]
|
||||||
public class DotnetResolverService(DebugService debugService, ConfigurationService configurationService)
|
public class DotnetResolverService(DebugService debugService, ConfigurationService configurationService)
|
||||||
{
|
{
|
||||||
private HttpClient _httpClient = new HttpClient();
|
private static readonly string FullPath =
|
||||||
|
Path.Join(FileService.RootPath, "dotnet", DotnetUrlHelper.GetRuntimeIdentifier());
|
||||||
|
|
||||||
private static readonly string FullPath = Path.Join(FileService.RootPath, "dotnet", DotnetUrlHelper.GetRuntimeIdentifier());
|
|
||||||
private static readonly string ExecutePath = Path.Join(FullPath, "dotnet" + DotnetUrlHelper.GetExtension());
|
private static readonly string ExecutePath = Path.Join(FullPath, "dotnet" + DotnetUrlHelper.GetExtension());
|
||||||
|
private readonly HttpClient _httpClient = new();
|
||||||
|
|
||||||
public async Task<string> EnsureDotnet(){
|
public async Task<string> EnsureDotnet()
|
||||||
if(!Directory.Exists(FullPath))
|
{
|
||||||
|
if (!Directory.Exists(FullPath))
|
||||||
await Download();
|
await Download();
|
||||||
|
|
||||||
return ExecutePath;
|
return ExecutePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Download(){
|
private async Task Download()
|
||||||
|
{
|
||||||
|
var debugLogger = debugService.GetLogger(this);
|
||||||
|
debugLogger.Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}...");
|
||||||
|
|
||||||
|
var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl(
|
||||||
|
configurationService.GetConfigValue(CurrentConVar.DotnetUrl)!
|
||||||
|
);
|
||||||
|
|
||||||
|
using var response = await _httpClient.GetAsync(url);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||||
|
|
||||||
debugService.GetLogger("DotnetResolver").Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}...");
|
|
||||||
var ridExt =
|
|
||||||
DotnetUrlHelper.GetCurrentPlatformDotnetUrl(configurationService.GetConfigValue(CurrentConVar.DotnetUrl)!);
|
|
||||||
using var response = await _httpClient.GetAsync(ridExt);
|
|
||||||
using var zipArchive = new ZipArchive(await response.Content.ReadAsStreamAsync());
|
|
||||||
Directory.CreateDirectory(FullPath);
|
Directory.CreateDirectory(FullPath);
|
||||||
zipArchive.ExtractToDirectory(FullPath);
|
|
||||||
debugService.GetLogger("DotnetResolver").Log($"Downloading dotnet complete.");
|
if (url.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
using var zipArchive = new ZipArchive(stream);
|
||||||
|
zipArchive.ExtractToDirectory(FullPath, true);
|
||||||
|
}
|
||||||
|
else if (url.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| url.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
TarUtils.ExtractTarGz(stream, FullPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Unsupported archive format.");
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLogger.Log("Downloading dotnet complete.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class DotnetUrlHelper
|
public static class DotnetUrlHelper
|
||||||
{
|
{
|
||||||
|
[Obsolete("FOR TEST USING ONLY!")]
|
||||||
|
public static string? RidOverrideTest = null; // FOR TEST PURPOSES ONLY!!!
|
||||||
|
|
||||||
public static string GetExtension()
|
public static string GetExtension()
|
||||||
{
|
{
|
||||||
if (OperatingSystem.IsWindows()) return ".exe";
|
if (OperatingSystem.IsWindows()) return ".exe";
|
||||||
@@ -43,27 +69,21 @@ public static class DotnetUrlHelper
|
|||||||
|
|
||||||
public static string GetCurrentPlatformDotnetUrl(Dictionary<string, string> dotnetUrl)
|
public static string GetCurrentPlatformDotnetUrl(Dictionary<string, string> dotnetUrl)
|
||||||
{
|
{
|
||||||
string? rid = GetRuntimeIdentifier();
|
var rid = GetRuntimeIdentifier();
|
||||||
|
|
||||||
if (dotnetUrl.TryGetValue(rid, out var url))
|
if (dotnetUrl.TryGetValue(rid, out var url)) return url;
|
||||||
{
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new PlatformNotSupportedException($"No download URL available for the current platform: {rid}");
|
throw new PlatformNotSupportedException($"No download URL available for the current platform: {rid}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetRuntimeIdentifier()
|
public static string GetRuntimeIdentifier()
|
||||||
{
|
{
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if(RidOverrideTest != null) return RidOverrideTest;
|
||||||
{
|
|
||||||
return Environment.Is64BitProcess ? "win-x64" : "win-x86";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
{
|
return Environment.Is64BitProcess ? "win-x64" : "win-x86";
|
||||||
return "linux-x64";
|
|
||||||
}
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return "linux-x64";
|
||||||
|
|
||||||
throw new PlatformNotSupportedException("Unsupported operating system");
|
throw new PlatformNotSupportedException("Unsupported operating system");
|
||||||
}
|
}
|
||||||
|
|||||||
393
Nebula.Shared/Utils/TarUtils.cs
Normal file
393
Nebula.Shared/Utils/TarUtils.cs
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Nebula.Shared.Utils;
|
||||||
|
|
||||||
|
public static class TarUtils
|
||||||
|
{
|
||||||
|
public static void ExtractTarGz(Stream stream, string destinationDirectory)
|
||||||
|
{
|
||||||
|
if (destinationDirectory == null) throw new ArgumentNullException(nameof(destinationDirectory));
|
||||||
|
|
||||||
|
using (var gzs = new GZipStream(stream, CompressionMode.Decompress, leaveOpen: false))
|
||||||
|
{
|
||||||
|
// GZipStream does not expose length, so just pass as streaming source
|
||||||
|
TarExtractor.ExtractTar(gzs, destinationDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TarExtractor
|
||||||
|
{
|
||||||
|
private const int BlockSize = 512;
|
||||||
|
|
||||||
|
public static void ExtractTar(Stream tarStream, string destinationDirectory)
|
||||||
|
{
|
||||||
|
if (tarStream == null) throw new ArgumentNullException(nameof(tarStream));
|
||||||
|
if (destinationDirectory == null) throw new ArgumentNullException(nameof(destinationDirectory));
|
||||||
|
|
||||||
|
Directory.CreateDirectory(destinationDirectory);
|
||||||
|
|
||||||
|
string pendingLongName = null;
|
||||||
|
string pendingLongLink = null;
|
||||||
|
|
||||||
|
var block = new byte[BlockSize];
|
||||||
|
var zeroBlockCount = 0;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var read = ReadExactly(tarStream, block, 0, BlockSize);
|
||||||
|
if (read == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (IsAllZero(block))
|
||||||
|
{
|
||||||
|
zeroBlockCount++;
|
||||||
|
if (zeroBlockCount >= 2) break; // two consecutive zero blocks -> end of archive
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
zeroBlockCount = 0;
|
||||||
|
|
||||||
|
var header = TarHeader.FromBlock(block);
|
||||||
|
|
||||||
|
// validate header checksum (best-effort)
|
||||||
|
if (!header.IsValidChecksum(block))
|
||||||
|
{
|
||||||
|
// Not fatal, but warn (we're not writing warnings to console by default).
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some tar implementations supply the long filename in a preceding entry whose typeflag is 'L'.
|
||||||
|
// If present, use that name for the following file.
|
||||||
|
if (header.TypeFlag == 'L') // GNU long name
|
||||||
|
{
|
||||||
|
// read content blocks with size header.Size
|
||||||
|
var size = header.Size;
|
||||||
|
var nameBytes = new byte[size];
|
||||||
|
ReadExactly(tarStream, nameBytes, 0, (int)size);
|
||||||
|
// skip padding to full 512 block
|
||||||
|
SkipPadding(tarStream, size);
|
||||||
|
pendingLongName = ReadNullTerminatedString(nameBytes);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.TypeFlag == 'K') // GNU long linkname
|
||||||
|
{
|
||||||
|
var size = header.Size;
|
||||||
|
var linkBytes = new byte[size];
|
||||||
|
ReadExactly(tarStream, linkBytes, 0, (int)size);
|
||||||
|
SkipPadding(tarStream, size);
|
||||||
|
pendingLongLink = ReadNullTerminatedString(linkBytes);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine final name
|
||||||
|
var entryName = !string.IsNullOrEmpty(pendingLongName) ? pendingLongName : header.GetName();
|
||||||
|
var entryLinkName = !string.IsNullOrEmpty(pendingLongLink) ? pendingLongLink : header.LinkName;
|
||||||
|
|
||||||
|
// reset pending longs after use
|
||||||
|
pendingLongName = null;
|
||||||
|
pendingLongLink = null;
|
||||||
|
|
||||||
|
// sanitize path separators and avoid absolute paths
|
||||||
|
entryName = entryName.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar)
|
||||||
|
.TrimStart(Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
|
var targetPath = Path.Combine(destinationDirectory, entryName);
|
||||||
|
|
||||||
|
switch (header.TypeFlag)
|
||||||
|
{
|
||||||
|
case '0':
|
||||||
|
case '\0': // normal file
|
||||||
|
case '7': // regular file (SUSv4)
|
||||||
|
EnsureParentDirectoryExists(targetPath);
|
||||||
|
using (var outFile = File.Open(targetPath, FileMode.Create, FileAccess.Write))
|
||||||
|
{
|
||||||
|
CopyExact(tarStream, outFile, header.Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
SkipPadding(tarStream, header.Size);
|
||||||
|
TrySetTimes(targetPath, header.ModTime);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '5': // directory
|
||||||
|
Directory.CreateDirectory(targetPath);
|
||||||
|
TrySetTimes(targetPath, header.ModTime);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '2': // symlink
|
||||||
|
// Creating symlinks require privileges on Windows and may fail.
|
||||||
|
// To keep things robust across platforms, write a small .symlink-info file for Windows fallback,
|
||||||
|
// and attempt real symlink creation on Unix-like platforms.
|
||||||
|
EnsureParentDirectoryExists(targetPath);
|
||||||
|
TryCreateSymlink(entryLinkName, targetPath);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '1': // hard link - we will try to create by copying if target exists; otherwise skip
|
||||||
|
var linkTargetPath = Path.Combine(destinationDirectory,
|
||||||
|
entryLinkName.Replace('/', Path.DirectorySeparatorChar));
|
||||||
|
if (File.Exists(linkTargetPath))
|
||||||
|
{
|
||||||
|
EnsureParentDirectoryExists(targetPath);
|
||||||
|
File.Copy(linkTargetPath, targetPath, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '3': // character device - skip
|
||||||
|
case '4': // block device - skip
|
||||||
|
case '6': // contiguous file - treat as regular
|
||||||
|
// To be safe, treat as file if size > 0
|
||||||
|
if (header.Size > 0)
|
||||||
|
{
|
||||||
|
EnsureParentDirectoryExists(targetPath);
|
||||||
|
using (var outFile = File.Open(targetPath, FileMode.Create, FileAccess.Write))
|
||||||
|
{
|
||||||
|
CopyExact(tarStream, outFile, header.Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
SkipPadding(tarStream, header.Size);
|
||||||
|
TrySetTimes(targetPath, header.ModTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Unknown type - skip the file data
|
||||||
|
if (header.Size > 0)
|
||||||
|
{
|
||||||
|
Skip(tarStream, header.Size);
|
||||||
|
SkipPadding(tarStream, header.Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryCreateSymlink(string linkTarget, string symlinkPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// On Unix-like systems we can try to create a symlink
|
||||||
|
if (IsWindows())
|
||||||
|
{
|
||||||
|
// don't try symlinks on Windows by default - write a .symlink-info file instead
|
||||||
|
File.WriteAllText(symlinkPath + ".symlink-info", $"symlink -> {linkTarget}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unix: use symlink
|
||||||
|
var dir = Path.GetDirectoryName(symlinkPath);
|
||||||
|
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
|
||||||
|
// Use native syscall via Mono.Posix? Not allowed. Fall back to invoking 'ln -s' is not allowed.
|
||||||
|
// Instead use System.IO.File.CreateSymbolicLink if available (net core 2.1+)
|
||||||
|
#if NETCOREAPP2_1_OR_GREATER || NET5_0_OR_GREATER
|
||||||
|
var sym = new FileInfo(symlinkPath);
|
||||||
|
sym.CreateAsSymbolicLink(linkTarget);
|
||||||
|
#else
|
||||||
|
// If unavailable, write a .symlink-info file.
|
||||||
|
File.WriteAllText(symlinkPath + ".symlink-info", $"symlink -> {linkTarget}");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore failures to create symlink; write fallback info
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(symlinkPath + ".symlink-info", $"symlink -> {linkTarget}");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsWindows()
|
||||||
|
{
|
||||||
|
return Path.DirectorySeparatorChar == '\\';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TrySetTimes(string path, DateTimeOffset modTime)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dt = modTime.UtcDateTime;
|
||||||
|
// convert to local to set file time sensibly
|
||||||
|
File.SetLastWriteTimeUtc(path, dt);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
/* best-effort */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureParentDirectoryExists(string path)
|
||||||
|
{
|
||||||
|
var dir = Path.GetDirectoryName(path);
|
||||||
|
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read exactly count bytes or throw if cannot
|
||||||
|
private static int ReadExactly(Stream s, byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
var total = 0;
|
||||||
|
while (total < count)
|
||||||
|
{
|
||||||
|
var r = s.Read(buffer, offset + total, count - total);
|
||||||
|
if (r == 0) break;
|
||||||
|
total += r;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip count bytes by reading and discarding
|
||||||
|
private static void Skip(Stream s, long count)
|
||||||
|
{
|
||||||
|
var tmp = new byte[8192];
|
||||||
|
var remaining = count;
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
var toRead = (int)Math.Min(tmp.Length, remaining);
|
||||||
|
var r = s.Read(tmp, 0, toRead);
|
||||||
|
if (r == 0) break;
|
||||||
|
remaining -= r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CopyExact(Stream source, Stream dest, long count)
|
||||||
|
{
|
||||||
|
var buf = new byte[8192];
|
||||||
|
var remaining = count;
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
var toRead = (int)Math.Min(buf.Length, remaining);
|
||||||
|
var r = source.Read(buf, 0, toRead);
|
||||||
|
if (r == 0) break;
|
||||||
|
dest.Write(buf, 0, r);
|
||||||
|
remaining -= r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SkipPadding(Stream s, long size)
|
||||||
|
{
|
||||||
|
var pad = (BlockSize - size % BlockSize) % BlockSize;
|
||||||
|
if (pad > 0) Skip(s, pad);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAllZero(byte[] block)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < block.Length; i++)
|
||||||
|
if (block[i] != 0)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadNullTerminatedString(byte[] bytes)
|
||||||
|
{
|
||||||
|
var len = 0;
|
||||||
|
while (len < bytes.Length && bytes[len] != 0) len++;
|
||||||
|
return Encoding.UTF8.GetString(bytes, 0, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TarHeader
|
||||||
|
{
|
||||||
|
public string Name { get; private set; }
|
||||||
|
public int Mode { get; private set; }
|
||||||
|
public int Uid { get; private set; }
|
||||||
|
public int Gid { get; private set; }
|
||||||
|
public long Size { get; private set; }
|
||||||
|
public DateTimeOffset ModTime { get; private set; }
|
||||||
|
public int Checksum { get; private set; }
|
||||||
|
public char TypeFlag { get; private set; }
|
||||||
|
public string LinkName { get; private set; }
|
||||||
|
public string Magic { get; private set; }
|
||||||
|
public string Version { get; private set; }
|
||||||
|
public string UName { get; private set; }
|
||||||
|
public string GName { get; private set; }
|
||||||
|
public string DevMajor { get; private set; }
|
||||||
|
public string DevMinor { get; private set; }
|
||||||
|
public string Prefix { get; private set; }
|
||||||
|
|
||||||
|
public static TarHeader FromBlock(byte[] block)
|
||||||
|
{
|
||||||
|
var h = new TarHeader();
|
||||||
|
h.Name = ReadString(block, 0, 100);
|
||||||
|
h.Mode = (int)ReadOctal(block, 100, 8);
|
||||||
|
h.Uid = (int)ReadOctal(block, 108, 8);
|
||||||
|
h.Gid = (int)ReadOctal(block, 116, 8);
|
||||||
|
h.Size = ReadOctal(block, 124, 12);
|
||||||
|
var mtime = ReadOctal(block, 136, 12);
|
||||||
|
h.ModTime = DateTimeOffset.FromUnixTimeSeconds(mtime);
|
||||||
|
h.Checksum = (int)ReadOctal(block, 148, 8);
|
||||||
|
h.TypeFlag = (char)block[156];
|
||||||
|
h.LinkName = ReadString(block, 157, 100);
|
||||||
|
h.Magic = ReadString(block, 257, 6);
|
||||||
|
h.Version = ReadString(block, 263, 2);
|
||||||
|
h.UName = ReadString(block, 265, 32);
|
||||||
|
h.GName = ReadString(block, 297, 32);
|
||||||
|
h.DevMajor = ReadString(block, 329, 8);
|
||||||
|
h.DevMinor = ReadString(block, 337, 8);
|
||||||
|
h.Prefix = ReadString(block, 345, 155);
|
||||||
|
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetName()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(Prefix))
|
||||||
|
return $"{Prefix}/{Name}".Trim('/');
|
||||||
|
return Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsValidChecksum(byte[] block)
|
||||||
|
{
|
||||||
|
// compute checksum where checksum field (148..155) is spaces (0x20)
|
||||||
|
long sum = 0;
|
||||||
|
for (var i = 0; i < block.Length; i++)
|
||||||
|
if (i >= 148 && i < 156) sum += 32; // space
|
||||||
|
else sum += block[i];
|
||||||
|
|
||||||
|
// stored checksum could be octal until null
|
||||||
|
var stored = Checksum;
|
||||||
|
return Math.Abs(sum - stored) <= 1; // allow +/-1 tolerance
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadString(byte[] buf, int offset, int length)
|
||||||
|
{
|
||||||
|
var end = offset;
|
||||||
|
var max = offset + length;
|
||||||
|
while (end < max && buf[end] != 0) end++;
|
||||||
|
if (end == offset) return string.Empty;
|
||||||
|
return Encoding.ASCII.GetString(buf, offset, end - offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long ReadOctal(byte[] buf, int offset, int length)
|
||||||
|
{
|
||||||
|
// Many tars store as ASCII octal, possibly padded with nulls or spaces.
|
||||||
|
var end = offset + length;
|
||||||
|
var i = offset;
|
||||||
|
// skip leading spaces and nulls
|
||||||
|
while (i < end && (buf[i] == 0 || buf[i] == (byte)' ')) i++;
|
||||||
|
long val = 0;
|
||||||
|
var found = false;
|
||||||
|
for (; i < end; i++)
|
||||||
|
{
|
||||||
|
var b = buf[i];
|
||||||
|
if (b == 0 || b == (byte)' ') break;
|
||||||
|
if (b >= (byte)'0' && b <= (byte)'7')
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
val = (val << 3) + (b - (byte)'0');
|
||||||
|
}
|
||||||
|
// some implementations use base-10 ascii or binary; ignore invalid chars
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) return 0;
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ public class CryptographicTest
|
|||||||
public async Task EncryptDecrypt()
|
public async Task EncryptDecrypt()
|
||||||
{
|
{
|
||||||
var key = CryptographicStore.GetComputerKey();
|
var key = CryptographicStore.GetComputerKey();
|
||||||
Console.WriteLine($"Key: {key}");
|
Console.WriteLine($"Key: {Convert.ToBase64String(key)}");
|
||||||
var entry = new TestEncryptEntry("Hello", "World");
|
var entry = new TestEncryptEntry("Hello", "World");
|
||||||
Console.WriteLine($"Raw data: {entry}");
|
Console.WriteLine($"Raw data: {entry}");
|
||||||
var encrypt = CryptographicStore.Encrypt(entry, key);
|
var encrypt = CryptographicStore.Encrypt(entry, key);
|
||||||
|
|||||||
64
Nebula.UnitTest/NebulaSharedTests/TarTest.cs
Normal file
64
Nebula.UnitTest/NebulaSharedTests/TarTest.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using System.IO.Compression;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Nebula.Shared;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.Services.Logging;
|
||||||
|
using Nebula.Shared.Utils;
|
||||||
|
|
||||||
|
namespace Nebula.UnitTest.NebulaSharedTests;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
[TestOf(typeof(DotnetResolverService))]
|
||||||
|
public class TarTest : BaseSharedTest
|
||||||
|
{
|
||||||
|
private FileService _fileService = default!;
|
||||||
|
private ConfigurationService _configurationService = default!;
|
||||||
|
private readonly HttpClient _httpClient = new();
|
||||||
|
|
||||||
|
public override void BeforeServiceBuild(IServiceCollection services)
|
||||||
|
{
|
||||||
|
TestServiceHelper.InitFileServiceTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public override void Setup()
|
||||||
|
{
|
||||||
|
base.Setup();
|
||||||
|
_fileService = _sharedUnit.GetService<FileService>();
|
||||||
|
_configurationService = _sharedUnit.GetService<ConfigurationService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task DownloadTarAndUnzipTest()
|
||||||
|
{
|
||||||
|
DotnetUrlHelper.RidOverrideTest = "linux-x64";
|
||||||
|
Console.WriteLine($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}...");
|
||||||
|
|
||||||
|
var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl(
|
||||||
|
_configurationService.GetConfigValue(CurrentConVar.DotnetUrl)!
|
||||||
|
);
|
||||||
|
|
||||||
|
using var response = await _httpClient.GetAsync(url);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||||
|
|
||||||
|
Directory.CreateDirectory(FileService.RootPath);
|
||||||
|
|
||||||
|
if (url.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
using var zipArchive = new ZipArchive(stream);
|
||||||
|
zipArchive.ExtractToDirectory(FileService.RootPath, true);
|
||||||
|
}
|
||||||
|
else if (url.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| url.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
TarUtils.ExtractTarGz(stream, FileService.RootPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Unsupported archive format.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Downloading dotnet complete.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,9 +13,11 @@ namespace Nebula.UpdateResolver;
|
|||||||
|
|
||||||
public static class DotnetStandalone
|
public static class DotnetStandalone
|
||||||
{
|
{
|
||||||
private static readonly HttpClient HttpClient = new HttpClient();
|
private static readonly HttpClient HttpClient = new();
|
||||||
|
|
||||||
|
private static readonly string FullPath =
|
||||||
|
Path.Join(MainWindow.RootPath, "dotnet", DotnetUrlHelper.GetRuntimeIdentifier());
|
||||||
|
|
||||||
private static readonly string FullPath = Path.Join(MainWindow.RootPath, "dotnet", DotnetUrlHelper.GetRuntimeIdentifier());
|
|
||||||
private static readonly string ExecutePath = Path.Join(FullPath, "dotnet" + DotnetUrlHelper.GetExtension());
|
private static readonly string ExecutePath = Path.Join(FullPath, "dotnet" + DotnetUrlHelper.GetExtension());
|
||||||
|
|
||||||
public static async Task<Process?> Run(string dllPath)
|
public static async Task<Process?> Run(string dllPath)
|
||||||
@@ -34,21 +36,42 @@ public static class DotnetStandalone
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task EnsureDotnet(){
|
private static async Task EnsureDotnet()
|
||||||
if(!Directory.Exists(FullPath))
|
{
|
||||||
|
if (!Directory.Exists(FullPath))
|
||||||
await Download();
|
await Download();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task Download(){
|
private static async Task Download()
|
||||||
|
{
|
||||||
LogStandalone.Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}...");
|
LogStandalone.Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}...");
|
||||||
var ridExt =
|
|
||||||
DotnetUrlHelper.GetCurrentPlatformDotnetUrl(ConfigurationStandalone.GetConfigValue(UpdateConVars.DotnetUrl)!);
|
var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl(
|
||||||
using var response = await HttpClient.GetAsync(ridExt);
|
ConfigurationStandalone.GetConfigValue(UpdateConVars.DotnetUrl)!
|
||||||
using var zipArchive = new ZipArchive(await response.Content.ReadAsStreamAsync());
|
);
|
||||||
|
|
||||||
|
using var response = await HttpClient.GetAsync(url);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||||
|
|
||||||
Directory.CreateDirectory(FullPath);
|
Directory.CreateDirectory(FullPath);
|
||||||
zipArchive.ExtractToDirectory(FullPath);
|
|
||||||
LogStandalone.Log($"Downloading dotnet complete.");
|
if (url.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
using var zipArchive = new ZipArchive(stream);
|
||||||
|
zipArchive.ExtractToDirectory(FullPath, true);
|
||||||
|
}
|
||||||
|
else if (url.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| url.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
TarUtils.ExtractTarGz(stream, FullPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Unsupported archive format.");
|
||||||
|
}
|
||||||
|
|
||||||
|
LogStandalone.Log("Downloading dotnet complete.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,12 +85,9 @@ public static class DotnetUrlHelper
|
|||||||
|
|
||||||
public static string GetCurrentPlatformDotnetUrl(Dictionary<string, string> dotnetUrl)
|
public static string GetCurrentPlatformDotnetUrl(Dictionary<string, string> dotnetUrl)
|
||||||
{
|
{
|
||||||
string? rid = GetRuntimeIdentifier();
|
var rid = GetRuntimeIdentifier();
|
||||||
|
|
||||||
if (dotnetUrl.TryGetValue(rid, out var url))
|
if (dotnetUrl.TryGetValue(rid, out var url)) return url;
|
||||||
{
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new PlatformNotSupportedException($"No download URL available for the current platform: {rid}");
|
throw new PlatformNotSupportedException($"No download URL available for the current platform: {rid}");
|
||||||
}
|
}
|
||||||
@@ -75,14 +95,9 @@ public static class DotnetUrlHelper
|
|||||||
public static string GetRuntimeIdentifier()
|
public static string GetRuntimeIdentifier()
|
||||||
{
|
{
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
{
|
|
||||||
return Environment.Is64BitProcess ? "win-x64" : "win-x86";
|
return Environment.Is64BitProcess ? "win-x64" : "win-x86";
|
||||||
}
|
|
||||||
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return "linux-x64";
|
||||||
{
|
|
||||||
return "linux-x64";
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new PlatformNotSupportedException("Unsupported operating system");
|
throw new PlatformNotSupportedException("Unsupported operating system");
|
||||||
}
|
}
|
||||||
|
|||||||
395
Nebula.UpdateResolver/TarUtils.cs
Normal file
395
Nebula.UpdateResolver/TarUtils.cs
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Nebula.UpdateResolver;
|
||||||
|
|
||||||
|
public static class TarUtils
|
||||||
|
{
|
||||||
|
public static void ExtractTarGz(Stream stream, string destinationDirectory)
|
||||||
|
{
|
||||||
|
if (destinationDirectory == null) throw new ArgumentNullException(nameof(destinationDirectory));
|
||||||
|
|
||||||
|
using (var gzs = new GZipStream(stream, CompressionMode.Decompress, leaveOpen: false))
|
||||||
|
{
|
||||||
|
// GZipStream does not expose length, so just pass as streaming source
|
||||||
|
TarExtractor.ExtractTar(gzs, destinationDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TarExtractor
|
||||||
|
{
|
||||||
|
private const int BlockSize = 512;
|
||||||
|
|
||||||
|
public static void ExtractTar(Stream tarStream, string destinationDirectory)
|
||||||
|
{
|
||||||
|
if (tarStream == null) throw new ArgumentNullException(nameof(tarStream));
|
||||||
|
if (destinationDirectory == null) throw new ArgumentNullException(nameof(destinationDirectory));
|
||||||
|
|
||||||
|
Directory.CreateDirectory(destinationDirectory);
|
||||||
|
|
||||||
|
string pendingLongName = null;
|
||||||
|
string pendingLongLink = null;
|
||||||
|
|
||||||
|
var block = new byte[BlockSize];
|
||||||
|
var zeroBlockCount = 0;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var read = ReadExactly(tarStream, block, 0, BlockSize);
|
||||||
|
if (read == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (IsAllZero(block))
|
||||||
|
{
|
||||||
|
zeroBlockCount++;
|
||||||
|
if (zeroBlockCount >= 2) break; // two consecutive zero blocks -> end of archive
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
zeroBlockCount = 0;
|
||||||
|
|
||||||
|
var header = TarHeader.FromBlock(block);
|
||||||
|
|
||||||
|
// validate header checksum (best-effort)
|
||||||
|
if (!header.IsValidChecksum(block))
|
||||||
|
{
|
||||||
|
// Not fatal, but warn (we're not writing warnings to console by default).
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some tar implementations supply the long filename in a preceding entry whose typeflag is 'L'.
|
||||||
|
// If present, use that name for the following file.
|
||||||
|
if (header.TypeFlag == 'L') // GNU long name
|
||||||
|
{
|
||||||
|
// read content blocks with size header.Size
|
||||||
|
var size = header.Size;
|
||||||
|
var nameBytes = new byte[size];
|
||||||
|
ReadExactly(tarStream, nameBytes, 0, (int)size);
|
||||||
|
// skip padding to full 512 block
|
||||||
|
SkipPadding(tarStream, size);
|
||||||
|
pendingLongName = ReadNullTerminatedString(nameBytes);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.TypeFlag == 'K') // GNU long linkname
|
||||||
|
{
|
||||||
|
var size = header.Size;
|
||||||
|
var linkBytes = new byte[size];
|
||||||
|
ReadExactly(tarStream, linkBytes, 0, (int)size);
|
||||||
|
SkipPadding(tarStream, size);
|
||||||
|
pendingLongLink = ReadNullTerminatedString(linkBytes);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine final name
|
||||||
|
var entryName = !string.IsNullOrEmpty(pendingLongName) ? pendingLongName : header.GetName();
|
||||||
|
var entryLinkName = !string.IsNullOrEmpty(pendingLongLink) ? pendingLongLink : header.LinkName;
|
||||||
|
|
||||||
|
// reset pending longs after use
|
||||||
|
pendingLongName = null;
|
||||||
|
pendingLongLink = null;
|
||||||
|
|
||||||
|
// sanitize path separators and avoid absolute paths
|
||||||
|
entryName = entryName.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar)
|
||||||
|
.TrimStart(Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
|
var targetPath = Path.Combine(destinationDirectory, entryName);
|
||||||
|
|
||||||
|
switch (header.TypeFlag)
|
||||||
|
{
|
||||||
|
case '0':
|
||||||
|
case '\0': // normal file
|
||||||
|
case '7': // regular file (SUSv4)
|
||||||
|
EnsureParentDirectoryExists(targetPath);
|
||||||
|
using (var outFile = File.Open(targetPath, FileMode.Create, FileAccess.Write))
|
||||||
|
{
|
||||||
|
CopyExact(tarStream, outFile, header.Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
SkipPadding(tarStream, header.Size);
|
||||||
|
TrySetTimes(targetPath, header.ModTime);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '5': // directory
|
||||||
|
Directory.CreateDirectory(targetPath);
|
||||||
|
TrySetTimes(targetPath, header.ModTime);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '2': // symlink
|
||||||
|
// Creating symlinks require privileges on Windows and may fail.
|
||||||
|
// To keep things robust across platforms, write a small .symlink-info file for Windows fallback,
|
||||||
|
// and attempt real symlink creation on Unix-like platforms.
|
||||||
|
EnsureParentDirectoryExists(targetPath);
|
||||||
|
TryCreateSymlink(entryLinkName, targetPath);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '1': // hard link - we will try to create by copying if target exists; otherwise skip
|
||||||
|
var linkTargetPath = Path.Combine(destinationDirectory,
|
||||||
|
entryLinkName.Replace('/', Path.DirectorySeparatorChar));
|
||||||
|
if (File.Exists(linkTargetPath))
|
||||||
|
{
|
||||||
|
EnsureParentDirectoryExists(targetPath);
|
||||||
|
File.Copy(linkTargetPath, targetPath, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '3': // character device - skip
|
||||||
|
case '4': // block device - skip
|
||||||
|
case '6': // contiguous file - treat as regular
|
||||||
|
// To be safe, treat as file if size > 0
|
||||||
|
if (header.Size > 0)
|
||||||
|
{
|
||||||
|
EnsureParentDirectoryExists(targetPath);
|
||||||
|
using (var outFile = File.Open(targetPath, FileMode.Create, FileAccess.Write))
|
||||||
|
{
|
||||||
|
CopyExact(tarStream, outFile, header.Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
SkipPadding(tarStream, header.Size);
|
||||||
|
TrySetTimes(targetPath, header.ModTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Unknown type - skip the file data
|
||||||
|
if (header.Size > 0)
|
||||||
|
{
|
||||||
|
Skip(tarStream, header.Size);
|
||||||
|
SkipPadding(tarStream, header.Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryCreateSymlink(string linkTarget, string symlinkPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// On Unix-like systems we can try to create a symlink
|
||||||
|
if (IsWindows())
|
||||||
|
{
|
||||||
|
// don't try symlinks on Windows by default - write a .symlink-info file instead
|
||||||
|
File.WriteAllText(symlinkPath + ".symlink-info", $"symlink -> {linkTarget}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unix: use symlink
|
||||||
|
var dir = Path.GetDirectoryName(symlinkPath);
|
||||||
|
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
|
||||||
|
// Use native syscall via Mono.Posix? Not allowed. Fall back to invoking 'ln -s' is not allowed.
|
||||||
|
// Instead use System.IO.File.CreateSymbolicLink if available (net core 2.1+)
|
||||||
|
#if NETCOREAPP2_1_OR_GREATER || NET5_0_OR_GREATER
|
||||||
|
var sym = new FileInfo(symlinkPath);
|
||||||
|
sym.CreateAsSymbolicLink(linkTarget);
|
||||||
|
#else
|
||||||
|
// If unavailable, write a .symlink-info file.
|
||||||
|
File.WriteAllText(symlinkPath + ".symlink-info", $"symlink -> {linkTarget}");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore failures to create symlink; write fallback info
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(symlinkPath + ".symlink-info", $"symlink -> {linkTarget}");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsWindows()
|
||||||
|
{
|
||||||
|
return Path.DirectorySeparatorChar == '\\';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TrySetTimes(string path, DateTimeOffset modTime)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dt = modTime.UtcDateTime;
|
||||||
|
// convert to local to set file time sensibly
|
||||||
|
File.SetLastWriteTimeUtc(path, dt);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
/* best-effort */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureParentDirectoryExists(string path)
|
||||||
|
{
|
||||||
|
var dir = Path.GetDirectoryName(path);
|
||||||
|
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read exactly count bytes or throw if cannot
|
||||||
|
private static int ReadExactly(Stream s, byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
var total = 0;
|
||||||
|
while (total < count)
|
||||||
|
{
|
||||||
|
var r = s.Read(buffer, offset + total, count - total);
|
||||||
|
if (r == 0) break;
|
||||||
|
total += r;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip count bytes by reading and discarding
|
||||||
|
private static void Skip(Stream s, long count)
|
||||||
|
{
|
||||||
|
var tmp = new byte[8192];
|
||||||
|
var remaining = count;
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
var toRead = (int)Math.Min(tmp.Length, remaining);
|
||||||
|
var r = s.Read(tmp, 0, toRead);
|
||||||
|
if (r == 0) break;
|
||||||
|
remaining -= r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CopyExact(Stream source, Stream dest, long count)
|
||||||
|
{
|
||||||
|
var buf = new byte[8192];
|
||||||
|
var remaining = count;
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
var toRead = (int)Math.Min(buf.Length, remaining);
|
||||||
|
var r = source.Read(buf, 0, toRead);
|
||||||
|
if (r == 0) break;
|
||||||
|
dest.Write(buf, 0, r);
|
||||||
|
remaining -= r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SkipPadding(Stream s, long size)
|
||||||
|
{
|
||||||
|
var pad = (BlockSize - size % BlockSize) % BlockSize;
|
||||||
|
if (pad > 0) Skip(s, pad);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAllZero(byte[] block)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < block.Length; i++)
|
||||||
|
if (block[i] != 0)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadNullTerminatedString(byte[] bytes)
|
||||||
|
{
|
||||||
|
var len = 0;
|
||||||
|
while (len < bytes.Length && bytes[len] != 0) len++;
|
||||||
|
return Encoding.UTF8.GetString(bytes, 0, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TarHeader
|
||||||
|
{
|
||||||
|
public string Name { get; private set; }
|
||||||
|
public int Mode { get; private set; }
|
||||||
|
public int Uid { get; private set; }
|
||||||
|
public int Gid { get; private set; }
|
||||||
|
public long Size { get; private set; }
|
||||||
|
public DateTimeOffset ModTime { get; private set; }
|
||||||
|
public int Checksum { get; private set; }
|
||||||
|
public char TypeFlag { get; private set; }
|
||||||
|
public string LinkName { get; private set; }
|
||||||
|
public string Magic { get; private set; }
|
||||||
|
public string Version { get; private set; }
|
||||||
|
public string UName { get; private set; }
|
||||||
|
public string GName { get; private set; }
|
||||||
|
public string DevMajor { get; private set; }
|
||||||
|
public string DevMinor { get; private set; }
|
||||||
|
public string Prefix { get; private set; }
|
||||||
|
|
||||||
|
public static TarHeader FromBlock(byte[] block)
|
||||||
|
{
|
||||||
|
var h = new TarHeader();
|
||||||
|
h.Name = ReadString(block, 0, 100);
|
||||||
|
h.Mode = (int)ReadOctal(block, 100, 8);
|
||||||
|
h.Uid = (int)ReadOctal(block, 108, 8);
|
||||||
|
h.Gid = (int)ReadOctal(block, 116, 8);
|
||||||
|
h.Size = ReadOctal(block, 124, 12);
|
||||||
|
var mtime = ReadOctal(block, 136, 12);
|
||||||
|
h.ModTime = DateTimeOffset.FromUnixTimeSeconds(mtime);
|
||||||
|
h.Checksum = (int)ReadOctal(block, 148, 8);
|
||||||
|
h.TypeFlag = (char)block[156];
|
||||||
|
h.LinkName = ReadString(block, 157, 100);
|
||||||
|
h.Magic = ReadString(block, 257, 6);
|
||||||
|
h.Version = ReadString(block, 263, 2);
|
||||||
|
h.UName = ReadString(block, 265, 32);
|
||||||
|
h.GName = ReadString(block, 297, 32);
|
||||||
|
h.DevMajor = ReadString(block, 329, 8);
|
||||||
|
h.DevMinor = ReadString(block, 337, 8);
|
||||||
|
h.Prefix = ReadString(block, 345, 155);
|
||||||
|
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetName()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(Prefix))
|
||||||
|
return $"{Prefix}/{Name}".Trim('/');
|
||||||
|
return Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsValidChecksum(byte[] block)
|
||||||
|
{
|
||||||
|
// compute checksum where checksum field (148..155) is spaces (0x20)
|
||||||
|
long sum = 0;
|
||||||
|
for (var i = 0; i < block.Length; i++)
|
||||||
|
if (i >= 148 && i < 156) sum += 32; // space
|
||||||
|
else sum += block[i];
|
||||||
|
|
||||||
|
// stored checksum could be octal until null
|
||||||
|
var stored = Checksum;
|
||||||
|
return Math.Abs(sum - stored) <= 1; // allow +/-1 tolerance
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadString(byte[] buf, int offset, int length)
|
||||||
|
{
|
||||||
|
var end = offset;
|
||||||
|
var max = offset + length;
|
||||||
|
while (end < max && buf[end] != 0) end++;
|
||||||
|
if (end == offset) return string.Empty;
|
||||||
|
return Encoding.ASCII.GetString(buf, offset, end - offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long ReadOctal(byte[] buf, int offset, int length)
|
||||||
|
{
|
||||||
|
// Many tars store as ASCII octal, possibly padded with nulls or spaces.
|
||||||
|
var end = offset + length;
|
||||||
|
var i = offset;
|
||||||
|
// skip leading spaces and nulls
|
||||||
|
while (i < end && (buf[i] == 0 || buf[i] == (byte)' ')) i++;
|
||||||
|
long val = 0;
|
||||||
|
var found = false;
|
||||||
|
for (; i < end; i++)
|
||||||
|
{
|
||||||
|
var b = buf[i];
|
||||||
|
if (b == 0 || b == (byte)' ') break;
|
||||||
|
if (b >= (byte)'0' && b <= (byte)'7')
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
val = (val << 3) + (b - (byte)'0');
|
||||||
|
}
|
||||||
|
// some implementations use base-10 ascii or binary; ignore invalid chars
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) return 0;
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANullable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5acc345db3c207bc9d886a36ff14867ef8d65557432172c2a42f19aeac04d1b_003FNullable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANullable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5acc345db3c207bc9d886a36ff14867ef8d65557432172c2a42f19aeac04d1b_003FNullable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableCollection_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e2c48e6b3ec8b39cf721287f93972c7f3df25d306753bcc539eaad73126c68_003FObservableCollection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableCollection_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e2c48e6b3ec8b39cf721287f93972c7f3df25d306753bcc539eaad73126c68_003FObservableCollection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableObject_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e432edeee9469b7cfdb81d6e6bd278cf57afb9e54ab75649b8bb2f52cdde69_003FObservableObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableObject_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e432edeee9469b7cfdb81d6e6bd278cf57afb9e54ab75649b8bb2f52cdde69_003FObservableObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObsoleteAttribute_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd6ed53c3c6ac5794ce2e51aa4bcfdb5734b7f78ccfeccd5ba93ac6a0da3b2_003FObsoleteAttribute_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APanel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9b699722324e3615b57977447b25bf953fccb2d6e912ae584f16b7e691ad9d3_003FPanel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APanel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9b699722324e3615b57977447b25bf953fccb2d6e912ae584f16b7e691ad9d3_003FPanel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F36fd1a9641998bb3afbf2091e26eafa6aaafabcb494bc746c0ba7471db513143_003FParallel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F36fd1a9641998bb3afbf2091e26eafa6aaafabcb494bc746c0ba7471db513143_003FParallel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002EForEachAsync_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc1d1ed6be2d5d4de542b4af5b36e82f6d1d1a389a35a4e4f9748d137d1c651_003FParallel_002EForEachAsync_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002EForEachAsync_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc1d1ed6be2d5d4de542b4af5b36e82f6d1d1a389a35a4e4f9748d137d1c651_003FParallel_002EForEachAsync_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
@@ -102,5 +103,6 @@
|
|||||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.ConfigurationServiceTests</TestId>
|
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.ConfigurationServiceTests</TestId>
|
||||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.CryptographicTest.EncryptDecrypt</TestId>
|
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.CryptographicTest.EncryptDecrypt</TestId>
|
||||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.CryptographicTest</TestId>
|
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.CryptographicTest</TestId>
|
||||||
|
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.TarTest.Download</TestId>
|
||||||
</TestAncestor>
|
</TestAncestor>
|
||||||
</SessionState></s:String></wpf:ResourceDictionary>
|
</SessionState></s:String></wpf:ResourceDictionary>
|
||||||
Reference in New Issue
Block a user