diff --git a/Archiver/.gitignore b/Archiver/.gitignore
new file mode 100644
index 0000000..1746e32
--- /dev/null
+++ b/Archiver/.gitignore
@@ -0,0 +1,2 @@
+bin
+obj
diff --git a/Archiver/Archiver.csproj b/Archiver/Archiver.csproj
new file mode 100644
index 0000000..bb3fa24
--- /dev/null
+++ b/Archiver/Archiver.csproj
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/Archiver/Archiver.sln b/Archiver/Archiver.sln
new file mode 100644
index 0000000..7433994
--- /dev/null
+++ b/Archiver/Archiver.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.002.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Archiver", "Archiver.csproj", "{EF1E4119-D2B7-420D-9D4B-3EBC40FCE68C}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {EF1E4119-D2B7-420D-9D4B-3EBC40FCE68C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EF1E4119-D2B7-420D-9D4B-3EBC40FCE68C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EF1E4119-D2B7-420D-9D4B-3EBC40FCE68C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EF1E4119-D2B7-420D-9D4B-3EBC40FCE68C}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {92B64C8C-A729-4720-8CCF-F1A1BE5FB4D1}
+ EndGlobalSection
+EndGlobal
diff --git a/Archiver/Lib/ArchiveDecoder.cs b/Archiver/Lib/ArchiveDecoder.cs
new file mode 100644
index 0000000..9d08379
--- /dev/null
+++ b/Archiver/Lib/ArchiveDecoder.cs
@@ -0,0 +1,85 @@
+using PacketIO;
+
+namespace Archiver.Lib;
+
+public static class ArchiveDecoder
+{
+ public static async ValueTask Decode(string inputPath, string outputPath)
+ {
+ if (!File.Exists(inputPath))
+ {
+ throw new FileNotFoundException($"{inputPath} does not exist", inputPath);
+ }
+
+ FileAttributes attributes = File.GetAttributes(inputPath);
+ if (attributes.HasFlag(FileAttributes.Directory))
+ {
+ throw new IOException("Target does not file.");
+ }
+
+ using (FileStream fs = new FileStream(inputPath, FileMode.Open))
+ {
+ byte[] buffer = new byte[ArchiveEncoder.CreateFileHeader().Length];
+ await fs.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
+ if (!buffer.SequenceEqual(ArchiveEncoder.CreateFileHeader()))
+ {
+ throw new IOException("Not supported file type.");
+ }
+
+ await ReadFile(fs, outputPath).ConfigureAwait(false);
+ }
+ }
+
+ private static async ValueTask ReadFile(Stream stream, string outputPath)
+ {
+ IPacketFile? file = await TreePacketFile.ReadTreeFile(stream).ConfigureAwait(false);
+ if (file is null)
+ {
+ return;
+ }
+
+ await OutputFile(file, outputPath).ConfigureAwait(false);
+ await ReadFile(stream, outputPath).ConfigureAwait(false);
+ }
+
+ private static async ValueTask OutputFile(IPacketFile file, string outputPath)
+ {
+ switch (file.GetFileType())
+ {
+ case PacketFileType.Directory:
+ {
+ string dirPath = Path.Combine(outputPath, file.GetFileName());
+ if (!Directory.Exists(dirPath))
+ {
+ Directory.CreateDirectory(dirPath);
+ }
+
+ foreach (IPacketFile inFile in file.EnumerableFiles())
+ {
+ await OutputFile(inFile, dirPath).ConfigureAwait(false);
+ }
+
+ break;
+ }
+ case PacketFileType.File:
+ {
+ string filePath = Path.Combine(outputPath, file.GetFileName());
+ using (FileStream outputStream = new FileStream(filePath, FileMode.OpenOrCreate))
+ {
+ PacketFile pfile = (PacketFile) file;
+ byte[] buffer = new byte[8192];
+ int c;
+ do
+ {
+ c = await pfile.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
+ await outputStream.WriteAsync(buffer, 0, c).ConfigureAwait(false);
+ } while (c > 0);
+ }
+
+ break;
+ }
+ }
+
+ file.Dispose();
+ }
+}
diff --git a/Archiver/Lib/ArchiveEncoder.cs b/Archiver/Lib/ArchiveEncoder.cs
new file mode 100644
index 0000000..02c238e
--- /dev/null
+++ b/Archiver/Lib/ArchiveEncoder.cs
@@ -0,0 +1,186 @@
+using PacketIO;
+
+namespace Archiver.Lib;
+
+public static class ArchiveEncoder
+{
+ public static async ValueTask Encode(string inputPath, string outputPath)
+ {
+ if (!Directory.Exists(inputPath))
+ {
+ throw new FileNotFoundException($"{inputPath} does not exist", inputPath);
+ }
+
+ using (FileStream fs = new FileStream(outputPath, FileMode.OpenOrCreate))
+ {
+ byte[] buffer = CreateFileHeader();
+ await fs.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
+
+ FileAttributes attributes = File.GetAttributes(inputPath);
+ if (attributes.HasFlag(FileAttributes.Directory))
+ {
+ DirectoryInfo dirInfo = new DirectoryInfo(inputPath);
+ PacketDirectoryModel model = await CreatePacketDirectory(dirInfo).ConfigureAwait(false);
+ try
+ {
+ await TreePacketFile.WriteTreeFiles(fs, model.PacketDirectory).ConfigureAwait(false);
+ }
+ finally
+ {
+ DisposeFiles(model);
+ }
+ }
+ else
+ {
+ FileInfo fileInfo = new FileInfo(inputPath);
+ PacketFileModel model = await CreatePacketFile(fileInfo).ConfigureAwait(false);
+ try
+ {
+ await TreePacketFile.WriteTreeFiles(fs, model.PacketFile).ConfigureAwait(false);
+ }
+ finally
+ {
+ DisposeFiles(model);
+ }
+ }
+ }
+ }
+
+ public static async ValueTask Encode(IEnumerable inputFiles, string outputPath)
+ {
+ using (FileStream fs = new FileStream(outputPath, FileMode.OpenOrCreate))
+ {
+ byte[] buffer = CreateFileHeader();
+ await fs.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
+
+ foreach (string inputPath in inputFiles)
+ {
+ FileAttributes attributes = File.GetAttributes(inputPath);
+ if (attributes.HasFlag(FileAttributes.Directory))
+ {
+ DirectoryInfo dirInfo = new DirectoryInfo(inputPath);
+ PacketDirectoryModel model = await CreatePacketDirectory(dirInfo).ConfigureAwait(false);
+ try
+ {
+ await TreePacketFile.WriteTreeFiles(fs, model.PacketDirectory).ConfigureAwait(false);
+ }
+ finally
+ {
+ DisposeFiles(model);
+ }
+ }
+ else
+ {
+ FileInfo fileInfo = new FileInfo(inputPath);
+ PacketFileModel model = await CreatePacketFile(fileInfo).ConfigureAwait(false);
+ try
+ {
+ await TreePacketFile.WriteTreeFiles(fs, model.PacketFile).ConfigureAwait(false);
+ }
+ finally
+ {
+ DisposeFiles(model);
+ }
+ }
+ }
+ }
+ }
+
+ public static bool IsArchiveFile(string inputPath)
+ {
+ FileAttributes attributes = File.GetAttributes(inputPath);
+ if (attributes.HasFlag(FileAttributes.Directory))
+ {
+ return false;
+ }
+
+ using (FileStream fs = File.OpenRead(inputPath))
+ {
+ byte[] buffer = new byte[CreateFileHeader().Length];
+ int c = fs.Read(buffer, 0, buffer.Length);
+ if (c <= 0)
+ {
+ return false;
+ }
+
+ return buffer.SequenceEqual(CreateFileHeader());
+ }
+ }
+
+ internal static byte[] CreateFileHeader()
+ {
+ byte[] header = new byte[]
+ {
+ 0xCC, 0xDE, 0xFF, 0x00,
+ };
+ return header;
+ }
+
+ private static void DisposeFiles(PacketDirectoryModel dirModel)
+ {
+ dirModel.PacketDirectory.Dispose();
+ foreach (PacketDirectoryModel subDirModel in dirModel.Directories)
+ {
+ DisposeFiles(subDirModel);
+ }
+
+ foreach (PacketFileModel fileModel in dirModel.Files)
+ {
+ DisposeFiles(fileModel);
+ }
+ }
+
+ private static void DisposeFiles(PacketFileModel fileModel)
+ {
+ fileModel.PacketFile.Dispose();
+ fileModel.TempFile.Dispose();
+ }
+
+ private static async ValueTask CreatePacketDirectory(DirectoryInfo dirInfo)
+ {
+ PacketDirectory packetDirectory = new PacketDirectory(dirInfo.Name);
+ PacketDirectoryModel dirModel = new PacketDirectoryModel()
+ {
+ PacketDirectory = packetDirectory,
+ Files = new List(),
+ };
+
+ foreach (FileInfo fileInfo in dirInfo.EnumerateFiles())
+ {
+ PacketFileModel model = await CreatePacketFile(fileInfo).ConfigureAwait(false);
+ dirModel.Files.Add(model);
+ packetDirectory.AddFile(model.PacketFile);
+ }
+
+ foreach (DirectoryInfo subInfo in dirInfo.EnumerateDirectories())
+ {
+ PacketDirectoryModel subDirModel = await CreatePacketDirectory(subInfo).ConfigureAwait(false);
+ dirModel.Directories.Add(subDirModel);
+ packetDirectory.AddFile(subDirModel.PacketDirectory);
+ }
+
+ return dirModel;
+ }
+
+ private static async ValueTask CreatePacketFile(FileInfo fileInfo)
+ {
+ TempFile tempFile = new TempFile();
+ Stream outputStream = tempFile.Open(FileMode.Open);
+
+ using (FileStream sourceStream = fileInfo.OpenRead())
+ {
+ await sourceStream.CopyToAsync(outputStream).ConfigureAwait(false);
+ }
+
+ outputStream.Seek(0, SeekOrigin.Begin);
+ PacketFile file = new PacketFile(fileInfo.Name, outputStream);
+ PacketFileModel model = new PacketFileModel()
+ {
+ TempFile = tempFile,
+ PacketFile = file,
+ };
+
+ return model;
+ }
+}
+
diff --git a/Archiver/Lib/PacketDirectoryModel.cs b/Archiver/Lib/PacketDirectoryModel.cs
new file mode 100644
index 0000000..fb32c64
--- /dev/null
+++ b/Archiver/Lib/PacketDirectoryModel.cs
@@ -0,0 +1,10 @@
+using PacketIO;
+
+namespace Archiver.Lib;
+
+public class PacketDirectoryModel
+{
+ public PacketDirectory PacketDirectory { get; set; }
+ public List Files { get; set; } = new List();
+ public List Directories { get; set; } = new List();
+}
diff --git a/Archiver/Lib/PacketFileModel.cs b/Archiver/Lib/PacketFileModel.cs
new file mode 100644
index 0000000..8351dd7
--- /dev/null
+++ b/Archiver/Lib/PacketFileModel.cs
@@ -0,0 +1,9 @@
+using PacketIO;
+
+namespace Archiver.Lib;
+
+public class PacketFileModel
+{
+ public TempFile TempFile { get; set;}
+ public PacketFile PacketFile { get; set; }
+}
diff --git a/Archiver/Lib/TempFile.cs b/Archiver/Lib/TempFile.cs
new file mode 100644
index 0000000..67da443
--- /dev/null
+++ b/Archiver/Lib/TempFile.cs
@@ -0,0 +1,21 @@
+namespace Archiver.Lib;
+
+public class TempFile : IDisposable
+{
+ public string FileName { get; }
+
+ public TempFile()
+ {
+ FileName = Path.GetTempFileName();
+ }
+
+ public Stream Open(FileMode mode)
+ {
+ return new FileStream(FileName, mode);
+ }
+
+ public void Dispose()
+ {
+ File.Delete(FileName);
+ }
+}
diff --git a/Archiver/Options.cs b/Archiver/Options.cs
new file mode 100644
index 0000000..85def64
--- /dev/null
+++ b/Archiver/Options.cs
@@ -0,0 +1,12 @@
+using CommandLine;
+
+namespace Archiver;
+
+public class Options
+{
+ [Option('i', "input", Required = true)]
+ public IEnumerable InputFiles { get; set; }
+
+ [Option('o', "output", Required = true)]
+ public string OutputPath { get; set; }
+}
diff --git a/Archiver/Program.cs b/Archiver/Program.cs
new file mode 100644
index 0000000..1974abd
--- /dev/null
+++ b/Archiver/Program.cs
@@ -0,0 +1,56 @@
+using Archiver.Lib;
+
+namespace Archiver;
+
+public class Program
+{
+ public static void Main(string[] args)
+ {
+ try
+ {
+ Options options = CommandLine.Parser.Default.ParseArguments(args).Value;
+ if (options is null || options.InputFiles is null || string.IsNullOrEmpty(options.OutputPath))
+ {
+ Console.WriteLine("[Archiver] Usage: archiver -i -o