summary refs log tree commit diff
path: root/Rhea.Nar
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-08-14 05:21:15 +0200
committerRory& <root@rory.gay>2026-08-14 05:21:15 +0200
commitf3d4f84fde24bb92e7cf6a22d94a703c753f3cbe (patch)
treed50b915a252fdb2e5d96b8ff9c6607a9cf9b78a2 /Rhea.Nar
downloadRhea-f3d4f84fde24bb92e7cf6a22d94a703c753f3cbe.tar.xz
Working files and symlinks?
Diffstat (limited to 'Rhea.Nar')
-rw-r--r--Rhea.Nar/NarWriter.cs149
-rw-r--r--Rhea.Nar/Rhea.Nar.csproj14
2 files changed, 163 insertions, 0 deletions
diff --git a/Rhea.Nar/NarWriter.cs b/Rhea.Nar/NarWriter.cs
new file mode 100644

index 0000000..a01cd3d --- /dev/null +++ b/Rhea.Nar/NarWriter.cs
@@ -0,0 +1,149 @@ +using System.IO.Enumeration; +using System.Runtime.InteropServices; +using ArcaneLibs; +using ArcaneLibs.Extensions; + +namespace Rhea.Nar; + +public ref struct NarConsts +{ + public static readonly byte[] Header = NarWriter.GetLengthPrefixedPadded("nix-archive-1"u8); + + public static readonly byte[] LeftParen = NarWriter.GetLengthPrefixedPadded("("u8); + public static readonly byte[] RightParen = NarWriter.GetLengthPrefixedPadded(")"u8); + + public static readonly byte[] Regular = NarWriter.GetLengthPrefixedPadded("regular"u8); + public static readonly byte[] Directory = NarWriter.GetLengthPrefixedPadded("directory"u8); + public static readonly byte[] Symlink = NarWriter.GetLengthPrefixedPadded("symlink"u8); + + public static readonly byte[] Entry = NarWriter.GetLengthPrefixedPadded("entry"u8); + public static readonly byte[] Name = NarWriter.GetLengthPrefixedPadded("name"u8); + public static readonly byte[] Target = NarWriter.GetLengthPrefixedPadded("target"u8); + public static readonly byte[] Node = NarWriter.GetLengthPrefixedPadded("node"u8); + public static readonly byte[] Type = NarWriter.GetLengthPrefixedPadded("type"u8); + public static readonly byte[] Executable = NarWriter.GetLengthPrefixedPadded("executable"u8); + + public static readonly byte[] Contents = NarWriter.GetLengthPrefixedPadded("contents"u8); +} + +public class NarWriter +{ + public Span<byte> GetHeader() + { + return NarConsts.Header; + } + + public IEnumerable<byte[]> GetNarStreamContents(string storePath) + { + yield return NarConsts.Header; + FileAttributes fattr = File.GetAttributes(storePath); + if (fattr.HasFlag(FileAttributes.Directory)) + yield return NarConsts.Directory; + else + { + var linkTarget = new FileInfo(storePath).LinkTarget; + if (string.IsNullOrWhiteSpace(linkTarget)) + foreach (var chunk in GetNarFileContents(new FileInfo(storePath))) + yield return chunk; + + else + foreach (var chunk in GetNarSymlinkContents(new FileInfo(storePath))) + yield return chunk; + } + // yield return NarConsts.LeftParen; + // yield return NarConsts.Type; + // FileAttributes fattr = File.GetAttributes(storePath); + // if (fattr.HasFlag(FileAttributes.Directory)) + // yield return NarConsts.Directory; + // else + // { + // var linkTarget = new FileInfo(storePath).LinkTarget; + // if (string.IsNullOrWhiteSpace(linkTarget)) + // yield return NarConsts.Regular; + // else yield return NarConsts.Symlink; + // } + // + // foreach (var fe in FileUtils.EnumerateDirectory(storePath).OrderBy(x=>x.Name)) + // { + // yield return NarConsts.Entry; + // yield return NarConsts.LeftParen; + // + // yield return NarConsts.Name; + // yield return GetLengthPrefixedPadded(fe.Name.AsBytes().ToArray()); + // + // yield return NarConsts.Type; + // if (fe.LinkTarget is not null) + // { + // yield return NarConsts.Symlink; + // yield return NarConsts.Target; + // yield return GetLengthPrefixedPadded(fe.LinkTarget.AsBytes().ToArray()); + // } + // + // yield return NarConsts.RightParen; + // } + } + + private IEnumerable<byte[]> GetNarSymlinkContents(FileInfo fe) + { + yield return NarConsts.LeftParen; + + yield return NarConsts.Type; + yield return NarConsts.Symlink; + yield return NarConsts.Target; + yield return GetLengthPrefixedPadded(fe.LinkTarget!.AsBytes().ToArray()); + + yield return NarConsts.RightParen; + } + + private IEnumerable<byte[]> GetNarFileContents(FileInfo fe) + { + yield return NarConsts.LeftParen; + + yield return NarConsts.Type; + yield return NarConsts.Regular; + if ( + fe.UnixFileMode.HasFlag(UnixFileMode.UserExecute) + || fe.UnixFileMode.HasFlag(UnixFileMode.GroupExecute) + || fe.UnixFileMode.HasFlag(UnixFileMode.OtherExecute) + ) + yield return NarConsts.Executable; + + var targetLength = Math.Ceiling(fe.Length / 8d) * 8UL; + yield return NarConsts.Contents; + yield return BitConverter.GetBytes((ulong)fe.Length); + + using (var fs = File.OpenRead(fe.FullName)) + { + int chunkSize = 32; + long remaining = fs.Length; + byte[] buf = new byte[chunkSize]; + while (remaining > 0) + { + int read; + remaining -= read = fs.Read(buf, 0, chunkSize); + yield return buf[..read]; + } + } + + var padSize = (int)(targetLength - fe.Length); + yield return new byte[padSize]; + + yield return NarConsts.RightParen; + } + + #region Utility methods + + internal static byte[] GetLengthPrefixedPadded(ReadOnlySpan<byte> data) + { + byte[] unpadded = [..BitConverter.GetBytes((ulong)data.Length), .. data]; + var targetLength = Math.Ceiling(unpadded.Length / 8d) * 8; + // Console.Write($"{unpadded.Length:X2} - "); + // unpadded.HexDump(); + Array.Resize(ref unpadded, (int)targetLength); + // Console.Write($"{unpadded.Length:X2} - "); + // unpadded.HexDump(); + return unpadded; + } + + #endregion +} \ No newline at end of file diff --git a/Rhea.Nar/Rhea.Nar.csproj b/Rhea.Nar/Rhea.Nar.csproj new file mode 100644
index 0000000..bcfc328 --- /dev/null +++ b/Rhea.Nar/Rhea.Nar.csproj
@@ -0,0 +1,14 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net10.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="ArcaneLibs" Version="1.0.1-preview.20260812-173005" /> + </ItemGroup> + +</Project>