Add support for empty directory entries in archives

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-10-28 10:22:58 +00:00
parent fda1c2cc79
commit d148f36e87
19 changed files with 810 additions and 39 deletions

View File

@@ -96,6 +96,9 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
DateTime? modified
) => AddEntry(key, source, closeStream, size, modified);
IArchiveEntry IWritableArchive.AddDirectoryEntry(string key, DateTime? modified) =>
AddDirectoryEntry(key, modified);
public TEntry AddEntry(
string key,
Stream source,
@@ -136,6 +139,22 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
return false;
}
public TEntry AddDirectoryEntry(string key, DateTime? modified = null)
{
if (key.Length > 0 && key[0] is '/' or '\\')
{
key = key.Substring(1);
}
if (DoesKeyMatchExisting(key))
{
throw new ArchiveException("Cannot add entry with duplicate key: " + key);
}
var entry = CreateDirectoryEntry(key, modified);
newEntries.Add(entry);
RebuildModifiedCollection();
return entry;
}
public void SaveTo(Stream stream, WriterOptions options)
{
//reset streams of new entries
@@ -180,6 +199,8 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
bool closeStream
);
protected abstract TEntry CreateDirectoryEntry(string key, DateTime? modified);
protected abstract void SaveTo(
Stream stream,
WriterOptions options,

View File

@@ -185,6 +185,9 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
return new GZipWritableArchiveEntry(this, source, filePath, size, modified, closeStream);
}
protected override GZipArchiveEntry CreateDirectoryEntry(string directoryPath, DateTime? modified) =>
throw new NotSupportedException("GZip archives do not support directory entries.");
protected override void SaveTo(
Stream stream,
WriterOptions options,

View File

@@ -18,6 +18,8 @@ public interface IWritableArchive : IArchive
DateTime? modified = null
);
IArchiveEntry AddDirectoryEntry(string key, DateTime? modified = null);
void SaveTo(Stream stream, WriterOptions options);
Task SaveToAsync(

View File

@@ -224,6 +224,9 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
closeStream
);
protected override TarArchiveEntry CreateDirectoryEntry(string directoryPath, DateTime? modified) =>
new TarWritableArchiveEntry(this, directoryPath, modified);
protected override void SaveTo(
Stream stream,
WriterOptions options,
@@ -232,15 +235,25 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
)
{
using var writer = new TarWriter(stream, new TarWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
foreach (var entry in oldEntries.Concat(newEntries))
{
using var entryStream = entry.OpenEntryStream();
writer.Write(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime,
entry.Size
);
if (entry.IsDirectory)
{
writer.WriteDirectory(
entry.Key.NotNull("Entry Key is null"),
entry.LastModifiedTime
);
}
else
{
using var entryStream = entry.OpenEntryStream();
writer.Write(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime,
entry.Size
);
}
}
}
@@ -253,18 +266,31 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
)
{
using var writer = new TarWriter(stream, new TarWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
foreach (var entry in oldEntries.Concat(newEntries))
{
using var entryStream = entry.OpenEntryStream();
await writer
.WriteAsync(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime,
entry.Size,
cancellationToken
)
.ConfigureAwait(false);
if (entry.IsDirectory)
{
await writer
.WriteDirectoryAsync(
entry.Key.NotNull("Entry Key is null"),
entry.LastModifiedTime,
cancellationToken
)
.ConfigureAwait(false);
}
else
{
using var entryStream = entry.OpenEntryStream();
await writer
.WriteAsync(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime,
entry.Size,
cancellationToken
)
.ConfigureAwait(false);
}
}
}

View File

@@ -9,7 +9,8 @@ namespace SharpCompress.Archives.Tar;
internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiveEntry
{
private readonly bool closeStream;
private readonly Stream stream;
private readonly Stream? stream;
private readonly bool isDirectory;
internal TarWritableArchiveEntry(
TarArchive archive,
@@ -27,6 +28,22 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv
Size = size;
LastModifiedTime = lastModified;
this.closeStream = closeStream;
isDirectory = false;
}
internal TarWritableArchiveEntry(
TarArchive archive,
string directoryPath,
DateTime? lastModified
)
: base(archive, null, CompressionType.None)
{
stream = null;
Key = directoryPath;
Size = 0;
LastModifiedTime = lastModified;
closeStream = false;
isDirectory = true;
}
public override long Crc => 0;
@@ -47,15 +64,19 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv
public override bool IsEncrypted => false;
public override bool IsDirectory => false;
public override bool IsDirectory => isDirectory;
public override bool IsSplitAfter => false;
internal override IEnumerable<FilePart> Parts => throw new NotImplementedException();
Stream IWritableArchiveEntry.Stream => stream;
Stream IWritableArchiveEntry.Stream => stream ?? Stream.Null;
public override Stream OpenEntryStream()
{
if (stream is null)
{
return Stream.Null;
}
//ensure new stream is at the start, this could be reset
stream.Seek(0, SeekOrigin.Begin);
return SharpCompressStream.Create(stream, leaveOpen: true);
@@ -63,7 +84,7 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv
internal override void Close()
{
if (closeStream)
if (closeStream && stream is not null)
{
stream.Dispose();
}

View File

@@ -308,14 +308,24 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
)
{
using var writer = new ZipWriter(stream, new ZipWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
foreach (var entry in oldEntries.Concat(newEntries))
{
using var entryStream = entry.OpenEntryStream();
writer.Write(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime
);
if (entry.IsDirectory)
{
writer.WriteDirectory(
entry.Key.NotNull("Entry Key is null"),
entry.LastModifiedTime
);
}
else
{
using var entryStream = entry.OpenEntryStream();
writer.Write(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime
);
}
}
}
@@ -328,12 +338,29 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
)
{
using var writer = new ZipWriter(stream, new ZipWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
foreach (var entry in oldEntries.Concat(newEntries))
{
using var entryStream = entry.OpenEntryStream();
await writer
.WriteAsync(entry.Key.NotNull("Entry Key is null"), entryStream, cancellationToken)
.ConfigureAwait(false);
if (entry.IsDirectory)
{
await writer
.WriteDirectoryAsync(
entry.Key.NotNull("Entry Key is null"),
entry.LastModifiedTime,
cancellationToken
)
.ConfigureAwait(false);
}
else
{
using var entryStream = entry.OpenEntryStream();
await writer
.WriteAsync(
entry.Key.NotNull("Entry Key is null"),
entryStream,
cancellationToken
)
.ConfigureAwait(false);
}
}
}
@@ -345,6 +372,9 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
bool closeStream
) => new ZipWritableArchiveEntry(this, source, filePath, size, modified, closeStream);
protected override ZipArchiveEntry CreateDirectoryEntry(string directoryPath, DateTime? modified) =>
new ZipWritableArchiveEntry(this, directoryPath, modified);
public static ZipArchive Create() => new();
protected override IReader CreateReaderForSolidExtraction()

View File

@@ -9,7 +9,8 @@ namespace SharpCompress.Archives.Zip;
internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
{
private readonly bool closeStream;
private readonly Stream stream;
private readonly Stream? stream;
private readonly bool isDirectory;
private bool isDisposed;
internal ZipWritableArchiveEntry(
@@ -27,6 +28,22 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
Size = size;
LastModifiedTime = lastModified;
this.closeStream = closeStream;
isDirectory = false;
}
internal ZipWritableArchiveEntry(
ZipArchive archive,
string directoryPath,
DateTime? lastModified
)
: base(archive, null)
{
stream = null;
Key = directoryPath;
Size = 0;
LastModifiedTime = lastModified;
closeStream = false;
isDirectory = true;
}
public override long Crc => 0;
@@ -47,16 +64,20 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
public override bool IsEncrypted => false;
public override bool IsDirectory => false;
public override bool IsDirectory => isDirectory;
public override bool IsSplitAfter => false;
internal override IEnumerable<FilePart> Parts => throw new NotImplementedException();
Stream IWritableArchiveEntry.Stream => stream;
Stream IWritableArchiveEntry.Stream => stream ?? Stream.Null;
public override Stream OpenEntryStream()
{
if (stream is null)
{
return Stream.Null;
}
//ensure new stream is at the start, this could be reset
stream.Seek(0, SeekOrigin.Begin);
return SharpCompressStream.Create(stream, leaveOpen: true);
@@ -64,7 +85,7 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
internal override void Close()
{
if (closeStream && !isDisposed)
if (closeStream && !isDisposed && stream is not null)
{
stream.Dispose();
isDisposed = true;

View File

@@ -37,6 +37,20 @@ public abstract class AbstractWriter(ArchiveType type, WriterOptions writerOptio
await Task.CompletedTask.ConfigureAwait(false);
}
public abstract void WriteDirectory(string directoryName, DateTime? modificationTime);
public virtual async Task WriteDirectoryAsync(
string directoryName,
DateTime? modificationTime,
CancellationToken cancellationToken = default
)
{
// Default implementation calls synchronous version
// Derived classes should override for true async behavior
WriteDirectory(directoryName, modificationTime);
await Task.CompletedTask.ConfigureAwait(false);
}
protected virtual void Dispose(bool isDisposing)
{
if (isDisposing)

View File

@@ -50,4 +50,7 @@ public sealed class GZipWriter : AbstractWriter
source.CopyTo(stream);
_wroteToStream = true;
}
public override void WriteDirectory(string directoryName, DateTime? modificationTime) =>
throw new NotSupportedException("GZip archives do not support directory entries.");
}

View File

@@ -16,4 +16,10 @@ public interface IWriter : IDisposable
DateTime? modificationTime,
CancellationToken cancellationToken = default
);
void WriteDirectory(string directoryName, DateTime? modificationTime);
Task WriteDirectoryAsync(
string directoryName,
DateTime? modificationTime,
CancellationToken cancellationToken = default
);
}

View File

@@ -55,6 +55,9 @@ public static class IWriterExtensions
}
}
public static void WriteDirectory(this IWriter writer, string directoryName) =>
writer.WriteDirectory(directoryName, null);
// Async extensions
public static Task WriteAsync(
this IWriter writer,
@@ -121,4 +124,10 @@ public static class IWriterExtensions
.ConfigureAwait(false);
}
}
public static Task WriteDirectoryAsync(
this IWriter writer,
string directoryName,
CancellationToken cancellationToken = default
) => writer.WriteDirectoryAsync(directoryName, null, cancellationToken);
}

View File

@@ -74,6 +74,44 @@ public class TarWriter : AbstractWriter
return filename.Trim('/');
}
private string NormalizeDirectoryName(string directoryName)
{
directoryName = NormalizeFilename(directoryName);
// Ensure directory name ends with '/' for tar format
if (!string.IsNullOrEmpty(directoryName) && !directoryName.EndsWith('/'))
{
directoryName += '/';
}
return directoryName;
}
public override void WriteDirectory(string directoryName, DateTime? modificationTime)
{
var normalizedName = NormalizeDirectoryName(directoryName);
if (string.IsNullOrEmpty(normalizedName))
{
return; // Skip empty or root directory
}
var header = new TarHeader(WriterOptions.ArchiveEncoding);
header.LastModifiedTime = modificationTime ?? TarHeader.EPOCH;
header.Name = normalizedName;
header.Size = 0;
header.EntryType = EntryType.Directory;
header.Write(OutputStream);
}
public override async Task WriteDirectoryAsync(
string directoryName,
DateTime? modificationTime,
CancellationToken cancellationToken = default
)
{
// Synchronous implementation is sufficient for header-only write
WriteDirectory(directoryName, modificationTime);
await Task.CompletedTask.ConfigureAwait(false);
}
public void Write(string filename, Stream source, DateTime? modificationTime, long? size)
{
if (!source.CanSeek && size is null)

View File

@@ -3,6 +3,8 @@ using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Zip;
using SharpCompress.Common.Zip.Headers;
@@ -135,6 +137,73 @@ public class ZipWriter : AbstractWriter
return filename.Trim('/');
}
private string NormalizeDirectoryName(string directoryName)
{
directoryName = NormalizeFilename(directoryName);
// Ensure directory name ends with '/' for zip format
if (!string.IsNullOrEmpty(directoryName) && !directoryName.EndsWith('/'))
{
directoryName += '/';
}
return directoryName;
}
public override void WriteDirectory(string directoryName, DateTime? modificationTime)
{
var normalizedName = NormalizeDirectoryName(directoryName);
if (string.IsNullOrEmpty(normalizedName))
{
return; // Skip empty or root directory
}
var options = new ZipWriterEntryOptions { ModificationDateTime = modificationTime };
WriteDirectoryEntry(normalizedName, options);
}
public override async Task WriteDirectoryAsync(
string directoryName,
DateTime? modificationTime,
CancellationToken cancellationToken = default
)
{
// Synchronous implementation is sufficient for directory entries
WriteDirectory(directoryName, modificationTime);
await Task.CompletedTask.ConfigureAwait(false);
}
private void WriteDirectoryEntry(string directoryPath, ZipWriterEntryOptions options)
{
var compression = ZipCompressionMethod.None;
options.ModificationDateTime ??= DateTime.Now;
options.EntryComment ??= string.Empty;
var entry = new ZipCentralDirectoryEntry(
compression,
directoryPath,
(ulong)streamPosition,
WriterOptions.ArchiveEncoding
)
{
Comment = options.EntryComment,
ModificationTime = options.ModificationDateTime,
Crc = 0,
Compressed = 0,
Decompressed = 0,
};
// Use the archive default setting for zip64 and allow overrides
var useZip64 = isZip64;
if (options.EnableZip64.HasValue)
{
useZip64 = options.EnableZip64.Value;
}
var headersize = (uint)WriteHeader(directoryPath, options, entry, useZip64);
streamPosition += headersize;
entries.Add(entry);
}
private int WriteHeader(
string filename,
ZipWriterEntryOptions zipWriterEntryOptions,

View File

@@ -0,0 +1,18 @@
using System;
using System.IO;
using SharpCompress.Archives.GZip;
using Xunit;
namespace SharpCompress.Test.GZip;
public class GZipArchiveDirectoryTests : TestBase
{
[Fact]
public void GZipArchive_AddDirectoryEntry_ThrowsNotSupportedException()
{
using var archive = GZipArchive.Create();
Assert.Throws<NotSupportedException>(() =>
archive.AddDirectoryEntry("test-dir", DateTime.Now));
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.IO;
using SharpCompress.Common;
using SharpCompress.Writers.GZip;
using Xunit;
namespace SharpCompress.Test.GZip;
public class GZipWriterDirectoryTests : TestBase
{
[Fact]
public void GZipWriter_WriteDirectory_ThrowsNotSupportedException()
{
using var memoryStream = new MemoryStream();
using var writer = new GZipWriter(memoryStream, new GZipWriterOptions());
Assert.Throws<NotSupportedException>(() =>
writer.WriteDirectory("test-dir", DateTime.Now));
}
}

View File

@@ -0,0 +1,103 @@
using System;
using System.IO;
using System.Linq;
using SharpCompress.Archives.Tar;
using SharpCompress.Common;
using Xunit;
namespace SharpCompress.Test.Tar;
public class TarArchiveDirectoryTests : TestBase
{
[Fact]
public void TarArchive_AddDirectoryEntry_CreatesDirectoryEntry()
{
using var archive = TarArchive.Create();
archive.AddDirectoryEntry("test-dir", DateTime.Now);
var entries = archive.Entries.ToList();
Assert.Single(entries);
Assert.Equal("test-dir", entries[0].Key);
Assert.True(entries[0].IsDirectory);
}
[Fact]
public void TarArchive_AddDirectoryEntry_MultipleDirectories()
{
using var archive = TarArchive.Create();
archive.AddDirectoryEntry("dir1", DateTime.Now);
archive.AddDirectoryEntry("dir2", DateTime.Now);
archive.AddDirectoryEntry("dir1/subdir", DateTime.Now);
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.True(entries.All(e => e.IsDirectory));
}
[Fact]
public void TarArchive_AddDirectoryEntry_MixedWithFiles()
{
using var archive = TarArchive.Create();
archive.AddDirectoryEntry("dir1", DateTime.Now);
using var contentStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("test content"));
archive.AddEntry("dir1/file.txt", contentStream, false, contentStream.Length, DateTime.Now);
archive.AddDirectoryEntry("dir2", DateTime.Now);
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.True(entries[0].IsDirectory);
Assert.False(entries[1].IsDirectory);
Assert.True(entries[2].IsDirectory);
}
[Fact]
public void TarArchive_AddDirectoryEntry_SaveAndReload()
{
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "tar-directory-test.tar");
using (var archive = TarArchive.Create())
{
archive.AddDirectoryEntry("dir1", DateTime.Now);
archive.AddDirectoryEntry("dir2", DateTime.Now);
using var contentStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("test content"));
archive.AddEntry("dir1/file.txt", contentStream, false, contentStream.Length, DateTime.Now);
using (var fileStream = File.Create(scratchPath))
{
archive.SaveTo(fileStream, CompressionType.None);
}
}
using (var archive = TarArchive.Open(scratchPath))
{
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.Equal("dir1/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
Assert.Equal("dir1/file.txt", entries[1].Key);
Assert.False(entries[1].IsDirectory);
Assert.Equal("dir2/", entries[2].Key);
Assert.True(entries[2].IsDirectory);
}
}
[Fact]
public void TarArchive_AddDirectoryEntry_DuplicateKey_ThrowsException()
{
using var archive = TarArchive.Create();
archive.AddDirectoryEntry("test-dir", DateTime.Now);
Assert.Throws<ArchiveException>(() =>
archive.AddDirectoryEntry("test-dir", DateTime.Now));
}
}

View File

@@ -0,0 +1,132 @@
using System;
using System.IO;
using System.Linq;
using SharpCompress.Archives.Tar;
using SharpCompress.Common;
using SharpCompress.Writers.Tar;
using Xunit;
namespace SharpCompress.Test.Tar;
public class TarWriterDirectoryTests : TestBase
{
[Fact]
public void TarWriter_WriteDirectory_CreatesDirectoryEntry()
{
using var memoryStream = new MemoryStream();
using (var writer = new TarWriter(memoryStream, new TarWriterOptions(CompressionType.None, true)))
{
writer.WriteDirectory("test-dir", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = TarArchive.Open(memoryStream);
var entries = archive.Entries.ToList();
Assert.Single(entries);
Assert.Equal("test-dir/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
}
[Fact]
public void TarWriter_WriteDirectory_WithTrailingSlash()
{
using var memoryStream = new MemoryStream();
using (var writer = new TarWriter(memoryStream, new TarWriterOptions(CompressionType.None, true)))
{
writer.WriteDirectory("test-dir/", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = TarArchive.Open(memoryStream);
var entries = archive.Entries.ToList();
Assert.Single(entries);
Assert.Equal("test-dir/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
}
[Fact]
public void TarWriter_WriteDirectory_WithBackslash()
{
using var memoryStream = new MemoryStream();
using (var writer = new TarWriter(memoryStream, new TarWriterOptions(CompressionType.None, true)))
{
writer.WriteDirectory("test-dir\\subdir", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = TarArchive.Open(memoryStream);
var entries = archive.Entries.ToList();
Assert.Single(entries);
Assert.Equal("test-dir/subdir/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
}
[Fact]
public void TarWriter_WriteDirectory_EmptyString_IsSkipped()
{
using var memoryStream = new MemoryStream();
using (var writer = new TarWriter(memoryStream, new TarWriterOptions(CompressionType.None, true)))
{
writer.WriteDirectory("", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = TarArchive.Open(memoryStream);
Assert.Empty(archive.Entries);
}
[Fact]
public void TarWriter_WriteDirectory_MultipleDirectories()
{
using var memoryStream = new MemoryStream();
using (var writer = new TarWriter(memoryStream, new TarWriterOptions(CompressionType.None, true)))
{
writer.WriteDirectory("dir1", DateTime.Now);
writer.WriteDirectory("dir2", DateTime.Now);
writer.WriteDirectory("dir1/subdir", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = TarArchive.Open(memoryStream);
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.Equal("dir1/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
Assert.Equal("dir1/subdir/", entries[1].Key);
Assert.True(entries[1].IsDirectory);
Assert.Equal("dir2/", entries[2].Key);
Assert.True(entries[2].IsDirectory);
}
[Fact]
public void TarWriter_WriteDirectory_MixedWithFiles()
{
using var memoryStream = new MemoryStream();
using (var writer = new TarWriter(memoryStream, new TarWriterOptions(CompressionType.None, true)))
{
writer.WriteDirectory("dir1", DateTime.Now);
using var contentStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("test content"));
writer.Write("dir1/file.txt", contentStream, DateTime.Now);
writer.WriteDirectory("dir2", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = TarArchive.Open(memoryStream);
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.Equal("dir1/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
Assert.Equal("dir1/file.txt", entries[1].Key);
Assert.False(entries[1].IsDirectory);
Assert.Equal("dir2/", entries[2].Key);
Assert.True(entries[2].IsDirectory);
}
}

View File

@@ -0,0 +1,103 @@
using System;
using System.IO;
using System.Linq;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using Xunit;
namespace SharpCompress.Test.Zip;
public class ZipArchiveDirectoryTests : TestBase
{
[Fact]
public void ZipArchive_AddDirectoryEntry_CreatesDirectoryEntry()
{
using var archive = ZipArchive.Create();
archive.AddDirectoryEntry("test-dir", DateTime.Now);
var entries = archive.Entries.ToList();
Assert.Single(entries);
Assert.Equal("test-dir", entries[0].Key);
Assert.True(entries[0].IsDirectory);
}
[Fact]
public void ZipArchive_AddDirectoryEntry_MultipleDirectories()
{
using var archive = ZipArchive.Create();
archive.AddDirectoryEntry("dir1", DateTime.Now);
archive.AddDirectoryEntry("dir2", DateTime.Now);
archive.AddDirectoryEntry("dir1/subdir", DateTime.Now);
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.True(entries.All(e => e.IsDirectory));
}
[Fact]
public void ZipArchive_AddDirectoryEntry_MixedWithFiles()
{
using var archive = ZipArchive.Create();
archive.AddDirectoryEntry("dir1", DateTime.Now);
using var contentStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("test content"));
archive.AddEntry("dir1/file.txt", contentStream, false, contentStream.Length, DateTime.Now);
archive.AddDirectoryEntry("dir2", DateTime.Now);
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.True(entries[0].IsDirectory);
Assert.False(entries[1].IsDirectory);
Assert.True(entries[2].IsDirectory);
}
[Fact]
public void ZipArchive_AddDirectoryEntry_SaveAndReload()
{
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "zip-directory-test.zip");
using (var archive = ZipArchive.Create())
{
archive.AddDirectoryEntry("dir1", DateTime.Now);
archive.AddDirectoryEntry("dir2", DateTime.Now);
using var contentStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("test content"));
archive.AddEntry("dir1/file.txt", contentStream, false, contentStream.Length, DateTime.Now);
using (var fileStream = File.Create(scratchPath))
{
archive.SaveTo(fileStream, CompressionType.Deflate);
}
}
using (var archive = ZipArchive.Open(scratchPath))
{
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.Equal("dir1/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
Assert.Equal("dir1/file.txt", entries[1].Key);
Assert.False(entries[1].IsDirectory);
Assert.Equal("dir2/", entries[2].Key);
Assert.True(entries[2].IsDirectory);
}
}
[Fact]
public void ZipArchive_AddDirectoryEntry_DuplicateKey_ThrowsException()
{
using var archive = ZipArchive.Create();
archive.AddDirectoryEntry("test-dir", DateTime.Now);
Assert.Throws<ArchiveException>(() =>
archive.AddDirectoryEntry("test-dir", DateTime.Now));
}
}

View File

@@ -0,0 +1,132 @@
using System;
using System.IO;
using System.Linq;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Writers.Zip;
using Xunit;
namespace SharpCompress.Test.Zip;
public class ZipWriterDirectoryTests : TestBase
{
[Fact]
public void ZipWriter_WriteDirectory_CreatesDirectoryEntry()
{
using var memoryStream = new MemoryStream();
using (var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate)))
{
writer.WriteDirectory("test-dir", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = ZipArchive.Open(memoryStream);
var entries = archive.Entries.ToList();
Assert.Single(entries);
Assert.Equal("test-dir/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
}
[Fact]
public void ZipWriter_WriteDirectory_WithTrailingSlash()
{
using var memoryStream = new MemoryStream();
using (var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate)))
{
writer.WriteDirectory("test-dir/", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = ZipArchive.Open(memoryStream);
var entries = archive.Entries.ToList();
Assert.Single(entries);
Assert.Equal("test-dir/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
}
[Fact]
public void ZipWriter_WriteDirectory_WithBackslash()
{
using var memoryStream = new MemoryStream();
using (var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate)))
{
writer.WriteDirectory("test-dir\\subdir", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = ZipArchive.Open(memoryStream);
var entries = archive.Entries.ToList();
Assert.Single(entries);
Assert.Equal("test-dir/subdir/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
}
[Fact]
public void ZipWriter_WriteDirectory_EmptyString_IsSkipped()
{
using var memoryStream = new MemoryStream();
using (var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate)))
{
writer.WriteDirectory("", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = ZipArchive.Open(memoryStream);
Assert.Empty(archive.Entries);
}
[Fact]
public void ZipWriter_WriteDirectory_MultipleDirectories()
{
using var memoryStream = new MemoryStream();
using (var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate)))
{
writer.WriteDirectory("dir1", DateTime.Now);
writer.WriteDirectory("dir2", DateTime.Now);
writer.WriteDirectory("dir1/subdir", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = ZipArchive.Open(memoryStream);
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.Equal("dir1/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
Assert.Equal("dir1/subdir/", entries[1].Key);
Assert.True(entries[1].IsDirectory);
Assert.Equal("dir2/", entries[2].Key);
Assert.True(entries[2].IsDirectory);
}
[Fact]
public void ZipWriter_WriteDirectory_MixedWithFiles()
{
using var memoryStream = new MemoryStream();
using (var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate)))
{
writer.WriteDirectory("dir1", DateTime.Now);
using var contentStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("test content"));
writer.Write("dir1/file.txt", contentStream, DateTime.Now);
writer.WriteDirectory("dir2", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = ZipArchive.Open(memoryStream);
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.Equal("dir1/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
Assert.Equal("dir1/file.txt", entries[1].Key);
Assert.False(entries[1].IsDirectory);
Assert.Equal("dir2/", entries[2].Key);
Assert.True(entries[2].IsDirectory);
}
}