diff --git a/skyscraper8.Tests/IntegrationTests.cs b/skyscraper8.Tests/IntegrationTests.cs new file mode 100644 index 0000000..a71c8f2 --- /dev/null +++ b/skyscraper8.Tests/IntegrationTests.cs @@ -0,0 +1,32 @@ +using System; +using System.IO; +using skyscraper5.Skyscraper; +using skyscraper8.Skyscraper.Scraper.Storage.Tar; + +namespace skyscraper8.Tests; + +[TestClass] +public class IntegrationTests +{ + [TestMethod] + public void CreateTarArchive() + { + string filename = String.Format("{0}.tar", DateTime.Now.ToUnixTime()); + FileInfo fi = new FileInfo(filename); + FileStream fileStream = fi.Open(FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + + Random rng = new Random(); + + TarArchive tar = new TarArchive(fileStream); + for (int i = 0; i < 42; i++) + { + filename = String.Format("{0}.bin", i); + + int randomSize = rng.Next(4096); + byte[] buffer = new byte[randomSize]; + rng.NextBytes(buffer); + + tar.WriteEntry(filename, buffer); + } + } +} diff --git a/skyscraper8/Skyscraper/Scraper/Storage/Tar/TarArchive.cs b/skyscraper8/Skyscraper/Scraper/Storage/Tar/TarArchive.cs new file mode 100644 index 0000000..613b052 --- /dev/null +++ b/skyscraper8/Skyscraper/Scraper/Storage/Tar/TarArchive.cs @@ -0,0 +1,77 @@ +namespace skyscraper8.Skyscraper.Scraper.Storage.Tar; + +public class TarArchive : IDisposable, IAsyncDisposable +{ + public TarArchive(Stream stream) + { + if (!stream.CanSeek) + throw new Exception("TarArchive must be able to seek."); + + backend = stream; + byte[] headerBuffer = null; + while (stream.Position < stream.Length) + { + if (headerBuffer == null) + headerBuffer = new byte[512]; + + stream.Read(headerBuffer, 0, 512); + TarHeader header = TarHeader.Deserialize(headerBuffer); + + TarArchiveEntry entry = new TarArchiveEntry(); + entry.FileName = header.Filename; + entry.FileSize = header.Size; + entry.Offset = stream.Position; + + stream.Position += header.GetSizeOnTape(); + + if (entries == null) + entries = new List(); + entries.Add(entry); + } + } + + struct TarArchiveEntry + { + public string FileName; + public long FileSize; + public long Offset; + } + + private List entries; + private Stream backend; + + public void WriteEntry(string filename, byte[] buffer) + { + backend.Position = backend.Length; + + //Write header + TarHeader header = new TarHeader(filename, buffer.Length); + byte[] serialize = header.Serialize(); + backend.Write(serialize); + + //Write actual data + backend.Write(buffer, 0, buffer.Length); + + //Write stuffing + long stuffingLength64 = header.GetSizeOnTape(); + stuffingLength64 -= header.Size; + if (stuffingLength64 > 0) + { + byte[] stuffingBuffer = new byte[stuffingLength64]; + backend.Write(stuffingBuffer, 0, (int)stuffingLength64); + } + backend.Flush(); + } + + public void Dispose() + { + backend.Close(); + backend.Dispose(); + } + + public async ValueTask DisposeAsync() + { + backend.Close(); + await backend.DisposeAsync(); + } +} diff --git a/skyscraper8/Skyscraper/Scraper/Storage/Tar/TarHeader.cs b/skyscraper8/Skyscraper/Scraper/Storage/Tar/TarHeader.cs new file mode 100644 index 0000000..ce61e6b --- /dev/null +++ b/skyscraper8/Skyscraper/Scraper/Storage/Tar/TarHeader.cs @@ -0,0 +1,158 @@ +using System.Text; +using skyscraper5.Skyscraper; + +namespace skyscraper8.Skyscraper.Scraper.Storage.Tar; + +public class TarHeader +{ + public TarHeader(string filename, long size) + { + Filename = filename; + Permissions = new UnixFilePermissions(); + Uid = 1000; + Gid = 1000; + Size = size; + ModificationTime = DateTime.Now; + OwnerUsername = "sophia"; + OwnerGroupName = "sophia"; + } + + private string _filename; + public string Filename + { + get => _filename; + set + { + if (value.Length > 99) + throw new ArgumentOutOfRangeException(nameof(Filename), "Filename must not exceed 99 characters."); + if (value.Contains("\0")) + throw new ArgumentOutOfRangeException(nameof(Filename), "Filename must not contain NULL characters."); + _filename = value; + } + } + + public UnixFilePermissions Permissions {get; set;} + public int Uid { get; set; } + public int Gid { get; set; } + public long Size { get; set; } + public DateTime ModificationTime { get; set; } + + private string _username; + + public string OwnerUsername + { + get => _username; + set + { + if (value.Length > 31) + throw new ArgumentOutOfRangeException("Username", "Username must not exceed 31 characters."); + _username = value; + } + } + + private string _groupname; + + public string OwnerGroupName + { + get => _groupname; + set + { + if (value.Length > 31) + throw new ArgumentOutOfRangeException("GroupName", "Group name must not exceed 31 characters."); + _groupname = value; + } + } + + public long GetSizeOnTape() + { + long fileSize = Size; + if (fileSize % 512 == 0) + return fileSize; + + fileSize /= 512; + fileSize++; + fileSize *= 512; + return fileSize; + } + + public static TarHeader Deserialize(byte[] buffer) + { + if (buffer.Length != 512) + throw new InvalidDataException("Invalid TAR header length"); + + throw new NotImplementedException(); + } + + public byte[] Serialize() + { + byte[] buffer = new byte[512]; + Encoding.UTF8.GetBytes(Filename,0,Filename.Length,buffer,0); + + string numericFilemode = "0000" + Permissions.ToString(); + Encoding.ASCII.GetBytes(numericFilemode, 0, numericFilemode.Length, buffer, 100); + + string numericUid = LongToOctal(Gid, 7); + Encoding.ASCII.GetBytes(numericUid, 0, numericUid.Length, buffer, 108); + + string numericGid = LongToOctal(Gid, 7); + Encoding.ASCII.GetBytes(numericGid, 0, numericGid.Length, buffer, 116); + + string numericSize = LongToOctal(Size, 11); + Encoding.ASCII.GetBytes(numericSize, 0, numericSize.Length, buffer, 124); + + string numericLastModification = LongToOctal(ModificationTime.ToUnixTime(),11); + Encoding.ASCII.GetBytes(numericLastModification, 0, numericLastModification.Length, buffer, 136); + + buffer[257] = (byte)'u'; + buffer[258] = (byte)'s'; + buffer[259] = (byte)'t'; + buffer[260] = (byte)'a'; + buffer[261] = (byte)'r'; + buffer[262] = 0x20; + buffer[263] = 0x20; + buffer[264] = 0; + + string username = OwnerUsername; + Encoding.ASCII.GetBytes(username, 0, username.Length, buffer, 265); + + string groupname = OwnerGroupName; + Encoding.ASCII.GetBytes(groupname, 0, groupname.Length, buffer, 297); + + string checksumBlanco = " "; + Encoding.ASCII.GetBytes(checksumBlanco, 0, checksumBlanco.Length, buffer, 148); + long checkSum = 0; + for (int i = 0; i < 512; ++i) + { + checkSum += buffer[i]; + } + checksumBlanco = LongToOctal(checkSum, 6); + Encoding.ASCII.GetBytes(checksumBlanco, 0, checksumBlanco.Length, buffer, 148); + + return buffer; + } + + private static string LongToOctal(long value, int length) + { + string result = string.Empty; + + while (value > 0) + { + result = (char)('0' + value % 8) + result; + value = value / 8; + } + + return new string('0', length - result.Length) + result; + } + + private static long OctalToLong(string value) + { + long result = 0; + + for (int i = 0; i < value.Length; ++i) + { + result = result * 8 + (value[i] - '0'); + } + + return result; + } +} diff --git a/skyscraper8/Skyscraper/Scraper/Storage/Tar/UnixFilePermissions.cs b/skyscraper8/Skyscraper/Scraper/Storage/Tar/UnixFilePermissions.cs new file mode 100644 index 0000000..6e16b10 --- /dev/null +++ b/skyscraper8/Skyscraper/Scraper/Storage/Tar/UnixFilePermissions.cs @@ -0,0 +1,114 @@ +namespace skyscraper8.Skyscraper.Scraper.Storage.Tar; + +public class UnixFilePermissions +{ + public UnixFilePermissions() + { + UserRead = true; + UserWrite = true; + GroupRead = true; + GroupWrite = true; + } + + public UnixFilePermissions(string permissionString) + { + if (permissionString.Length == 7) + permissionString = permissionString.Substring(4); + + string userChar = permissionString.Substring(0, 1); + string groupChar = permissionString.Substring(1, 1); + string worldChar = permissionString.Substring(2, 1); + + int userInt = Int32.Parse(userChar); + int groupInt = Int32.Parse(groupChar); + int worldInt = Int32.Parse(worldChar); + + if (userInt >= 4) + { + UserRead = true; + userInt -= 4; + } + if (userInt >= 2) + { + UserWrite = true; + userInt -= 2; + } + if (userInt >= 1) + { + UserExecute = true; + userInt -= 1; + } + + if (groupInt >= 4) + { + GroupRead = true; + groupInt -= 4; + } + if (groupInt >= 2) + { + GroupWrite = true; + groupInt -= 2; + } + if (groupInt >= 1) + { + GroupExecute = true; + groupInt -= 1; + } + + if (worldInt >= 4) + { + WorldRead = true; + worldInt -= 4; + } + if (worldInt >= 2) + { + WorldWrite = true; + worldInt -= 2; + } + if (worldInt >= 1) + { + WorldExecute = true; + worldInt -= 1; + } + } + + public string ToString() + { + int user = Fuse(UserRead, UserWrite, UserExecute); + int group = Fuse(GroupRead, GroupWrite, GroupExecute); + int world = Fuse(WorldRead, WorldWrite, WorldExecute); + string result = String.Format("{0}{1}{2}",user,group,world); + return result; + } + + public int ToInt32() + { + int user = Fuse(UserRead, UserWrite, UserExecute); + int group = Fuse(GroupRead, GroupWrite, GroupExecute); + int world = Fuse(WorldRead, WorldWrite, WorldExecute); + return (user * 100) + (group * 10) + world; + } + + private int Fuse(bool read, bool write, bool execute) + { + int result = 0; + if (read) + result += 4; + if (write) + result += 2; + if (execute) + result += 1; + return result; + } + public bool UserRead { get; set; } + public bool UserWrite { get; set; } + public bool UserExecute { get; set; } + + public bool GroupRead { get; set; } + public bool GroupWrite { get; set; } + public bool GroupExecute { get; set; } + + public bool WorldRead { get; set; } + public bool WorldWrite { get; set; } + public bool WorldExecute { get; set; } +}