Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ internal ZipArchiveEntry() { }
public string Name { get { throw null; } }
public void Delete() { }
public System.IO.Stream Open() { throw null; }
public System.IO.Stream Open(FileAccess access) { throw null; }
public System.Threading.Tasks.Task<System.IO.Stream> OpenAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public override string ToString() { throw null; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,15 @@
<data name="InvalidOffsetToZip64EOCD" xml:space="preserve">
<value>Invalid offset to the Zip64 End of Central Directory record.</value>
</data>
<data name="CannotBeWrittenInReadMode" xml:space="preserve">
<value>Cannot open entry for writing when archive is opened in read-only mode.</value>
</data>
<data name="CannotBeReadInCreateMode" xml:space="preserve">
<value>Cannot open entry for reading when archive is opened in create mode.</value>
</data>
<data name="InvalidFileAccess" xml:space="preserve">
<value>The specified FileAccess value is not valid.</value>
</data>
<data name="IO_SeekBeforeBegin" xml:space="preserve">
<value>An attempt was made to move the position before the beginning of the stream.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,50 @@ public Stream Open()
}
}

/// <summary>
/// Opens the entry with the specified access mode. This allows for more granular control over the returned stream's capabilities.
/// </summary>
/// <param name="access">The desired file access mode for the returned stream.</param>
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The <param> description should be a noun phrase that begins with an introductory article, per the documentation guidelines. Consider: "The file access mode for the returned stream."

Copilot uses AI. Check for mistakes.
/// <returns>A Stream that represents the contents of the entry with the specified access capabilities.</returns>
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The <returns> description should be a noun phrase that begins with an introductory article. Consider: "A stream that represents the contents of the entry with the specified access capabilities."

Suggested change
/// <returns>A Stream that represents the contents of the entry with the specified access capabilities.</returns>
/// <returns>A <see cref="Stream"/> that represents the contents of the entry with the specified access capabilities.</returns>

Copilot uses AI. Check for mistakes.
/// <exception cref="ArgumentException">The requested access is not compatible with the archive's open mode.</exception>
/// <exception cref="IOException">The entry is already currently open for writing. -or- The entry has been deleted from the archive. -or- The archive that this entry belongs to was opened in ZipArchiveMode.Create, and this entry has already been written to once.</exception>
/// <exception cref="InvalidDataException">The entry is missing from the archive or is corrupt and cannot be read. -or- The entry has been compressed using a compression method that is not supported.</exception>
/// <exception cref="ObjectDisposedException">The ZipArchive that this entry belongs to has been disposed.</exception>
public Stream Open(FileAccess access)
{
ThrowIfInvalidArchive();

if (access is not FileAccess.Read and not FileAccess.Write and not FileAccess.ReadWrite)
throw new ArgumentException(SR.InvalidFileAccess, nameof(access));

// Validate that the requested access is compatible with the archive's mode
switch (_archive.Mode)
{
case ZipArchiveMode.Read:
if (access != FileAccess.Read)
throw new ArgumentException(SR.CannotBeWrittenInReadMode, nameof(access));
return OpenInReadMode(checkOpenable: true);

case ZipArchiveMode.Create:
if (access == FileAccess.Read)
throw new ArgumentException(SR.CannotBeReadInCreateMode, nameof(access));
return OpenInWriteMode();

case ZipArchiveMode.Update:
default:
Debug.Assert(_archive.Mode == ZipArchiveMode.Update);
return access switch
{
FileAccess.Read => OpenInReadMode(checkOpenable: true),
// In Update mode, OpenInWriteMode() cannot be used because it requires archive stream ownership
// which is only acquired in Create mode. OpenInUpdateMode() provides a writable stream.
FileAccess.Write => OpenInUpdateMode(),
FileAccess.ReadWrite => OpenInUpdateMode(),
_ => throw new UnreachableException()
};
}
}

/// <summary>
/// Returns the FullName of the entry.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2194,5 +2194,281 @@ private static byte[] GenerateInvalidExtraFieldData(byte modifiedVersionToExtrac
// comment length
0x00, 0x00
};

[Theory]
[MemberData(nameof(Get_Booleans_Data))]
public static async Task OpenWithFileAccess_ReadMode_ReadAccess_Succeeds(bool async)
{
using MemoryStream ms = await StreamHelpers.CreateTempCopyStream(zfile("normal.zip"));
ZipArchive archive = await CreateZipArchive(async, ms, ZipArchiveMode.Read);

ZipArchiveEntry entry = archive.GetEntry("first.txt");
Assert.NotNull(entry);

using Stream stream = entry.Open(FileAccess.Read);
Assert.True(stream.CanRead);
Assert.False(stream.CanWrite);

await DisposeZipArchive(async, archive);
}

[Theory]
[MemberData(nameof(Get_Booleans_Data))]
public static async Task OpenWithFileAccess_ReadMode_WriteAccess_Throws(bool async)
{
using MemoryStream ms = await StreamHelpers.CreateTempCopyStream(zfile("normal.zip"));
ZipArchive archive = await CreateZipArchive(async, ms, ZipArchiveMode.Read);

ZipArchiveEntry entry = archive.GetEntry("first.txt");
Assert.NotNull(entry);

Assert.Throws<ArgumentException>("access", () => entry.Open(FileAccess.Write));
Assert.Throws<ArgumentException>("access", () => entry.Open(FileAccess.ReadWrite));

await DisposeZipArchive(async, archive);
}

[Theory]
[MemberData(nameof(Get_Booleans_Data))]
public static async Task OpenWithFileAccess_CreateMode_WriteAccess_Succeeds(bool async)
{
using var ms = new MemoryStream();
ZipArchive archive = await CreateZipArchive(async, ms, ZipArchiveMode.Create, leaveOpen: true);

ZipArchiveEntry entry = archive.CreateEntry("test.txt");

using Stream stream = entry.Open(FileAccess.Write);
Assert.False(stream.CanRead);
Assert.True(stream.CanWrite);

await DisposeZipArchive(async, archive);
}

[Theory]
[MemberData(nameof(Get_Booleans_Data))]
public static async Task OpenWithFileAccess_CreateMode_ReadWriteAccess_Succeeds(bool async)
{
using var ms = new MemoryStream();
ZipArchive archive = await CreateZipArchive(async, ms, ZipArchiveMode.Create, leaveOpen: true);

ZipArchiveEntry entry = archive.CreateEntry("test.txt");

// ReadWrite should be allowed in Create mode (it opens in write mode)
using Stream stream = entry.Open(FileAccess.ReadWrite);
Assert.True(stream.CanWrite);
Comment on lines +2257 to +2258
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Create mode with FileAccess.ReadWrite, the test verifies stream.CanWrite but doesn't verify stream.CanRead like the corresponding Update mode test does (line 2324-2326). For consistency and completeness, consider adding an assertion for stream.CanRead to match the behavior documented in the comment on line 2256.

Copilot uses AI. Check for mistakes.

await DisposeZipArchive(async, archive);
}

[Theory]
[MemberData(nameof(Get_Booleans_Data))]
public static async Task OpenWithFileAccess_CreateMode_ReadAccess_Throws(bool async)
{
using var ms = new MemoryStream();
ZipArchive archive = await CreateZipArchive(async, ms, ZipArchiveMode.Create, leaveOpen: true);

ZipArchiveEntry entry = archive.CreateEntry("test.txt");

Assert.Throws<ArgumentException>("access", () => entry.Open(FileAccess.Read));

await DisposeZipArchive(async, archive);
}

[Theory]
[MemberData(nameof(Get_Booleans_Data))]
public static async Task OpenWithFileAccess_UpdateMode_ReadAccess_Succeeds(bool async)
{
using MemoryStream ms = await StreamHelpers.CreateTempCopyStream(zfile("normal.zip"));
ZipArchive archive = await CreateZipArchive(async, ms, ZipArchiveMode.Update);

ZipArchiveEntry entry = archive.GetEntry("first.txt");
Assert.NotNull(entry);

using Stream stream = entry.Open(FileAccess.Read);
Assert.True(stream.CanRead);
Assert.False(stream.CanWrite);

await DisposeZipArchive(async, archive);
}

[Theory]
[MemberData(nameof(Get_Booleans_Data))]
public static async Task OpenWithFileAccess_UpdateMode_WriteAccess_Succeeds(bool async)
{
using MemoryStream ms = await StreamHelpers.CreateTempCopyStream(zfile("normal.zip"));
ZipArchive archive = await CreateZipArchive(async, ms, ZipArchiveMode.Update);

ZipArchiveEntry entry = archive.GetEntry("first.txt");
Assert.NotNull(entry);

// In Update mode, FileAccess.Write uses OpenInUpdateMode which returns a read-write-seekable stream
using Stream stream = entry.Open(FileAccess.Write);
Assert.True(stream.CanWrite);
Assert.True(stream.CanRead);
Assert.True(stream.CanSeek);

await DisposeZipArchive(async, archive);
}

[Theory]
[MemberData(nameof(Get_Booleans_Data))]
public static async Task OpenWithFileAccess_UpdateMode_ReadWriteAccess_Succeeds(bool async)
{
using MemoryStream ms = await StreamHelpers.CreateTempCopyStream(zfile("normal.zip"));
ZipArchive archive = await CreateZipArchive(async, ms, ZipArchiveMode.Update);

ZipArchiveEntry entry = archive.GetEntry("first.txt");
Assert.NotNull(entry);

using Stream stream = entry.Open(FileAccess.ReadWrite);
Assert.True(stream.CanRead);
Assert.True(stream.CanWrite);
Assert.True(stream.CanSeek);

await DisposeZipArchive(async, archive);
}

[Theory]
[MemberData(nameof(Get_Booleans_Data))]
public static async Task OpenWithFileAccess_ReadMode_InvalidAccess_Throws(bool async)
{
using MemoryStream ms = await StreamHelpers.CreateTempCopyStream(zfile("normal.zip"));
ZipArchive archive = await CreateZipArchive(async, ms, ZipArchiveMode.Read);

ZipArchiveEntry entry = archive.GetEntry("first.txt");
Assert.NotNull(entry);

// Test with invalid FileAccess values
Assert.Throws<ArgumentException>("access", () => entry.Open((FileAccess)0));
Assert.Throws<ArgumentException>("access", () => entry.Open((FileAccess)4));

await DisposeZipArchive(async, archive);
}

[Theory]
[MemberData(nameof(Get_Booleans_Data))]
public static async Task OpenWithFileAccess_CreateMode_InvalidAccess_Throws(bool async)
{
using var ms = new MemoryStream();
ZipArchive archive = await CreateZipArchive(async, ms, ZipArchiveMode.Create, leaveOpen: true);

ZipArchiveEntry entry = archive.CreateEntry("test.txt");

// Test with invalid FileAccess values
Assert.Throws<ArgumentException>("access", () => entry.Open((FileAccess)0));
Assert.Throws<ArgumentException>("access", () => entry.Open((FileAccess)4));

await DisposeZipArchive(async, archive);
}

[Theory]
[MemberData(nameof(Get_Booleans_Data))]
public static async Task OpenWithFileAccess_UpdateMode_InvalidAccess_Throws(bool async)
{
using MemoryStream ms = await StreamHelpers.CreateTempCopyStream(zfile("normal.zip"));
ZipArchive archive = await CreateZipArchive(async, ms, ZipArchiveMode.Update);

ZipArchiveEntry entry = archive.GetEntry("first.txt");
Assert.NotNull(entry);

// Test with invalid FileAccess values
Assert.Throws<ArgumentException>("access", () => entry.Open((FileAccess)0));
Assert.Throws<ArgumentException>("access", () => entry.Open((FileAccess)4));

await DisposeZipArchive(async, archive);
}

[Theory]
[MemberData(nameof(Get_Booleans_Data))]
public static async Task OpenWithFileAccess_MatchesParameterlessOpen_ReadMode(bool async)
{
using MemoryStream ms1 = await StreamHelpers.CreateTempCopyStream(zfile("small.zip"));
using MemoryStream ms2 = await StreamHelpers.CreateTempCopyStream(zfile("small.zip"));

ZipArchive archive1 = await CreateZipArchive(async, ms1, ZipArchiveMode.Read);
ZipArchive archive2 = await CreateZipArchive(async, ms2, ZipArchiveMode.Read);

ZipArchiveEntry entry1 = archive1.Entries[0];
ZipArchiveEntry entry2 = archive2.Entries[0];

byte[] contents1, contents2;

using (Stream stream1 = entry1.Open())
{
using var reader = new MemoryStream();
stream1.CopyTo(reader);
contents1 = reader.ToArray();
}

using (Stream stream2 = entry2.Open(FileAccess.Read))
{
using var reader = new MemoryStream();
stream2.CopyTo(reader);
contents2 = reader.ToArray();
}

Assert.Equal(contents1, contents2);

await DisposeZipArchive(async, archive1);
await DisposeZipArchive(async, archive2);
}

[Theory]
[MemberData(nameof(Get_Booleans_Data))]
public static async Task OpenWithFileAccess_MatchesParameterlessOpen_UpdateMode(bool async)
{
using MemoryStream ms1 = await StreamHelpers.CreateTempCopyStream(zfile("small.zip"));
using MemoryStream ms2 = await StreamHelpers.CreateTempCopyStream(zfile("small.zip"));

ZipArchive archive1 = await CreateZipArchive(async, ms1, ZipArchiveMode.Update);
ZipArchive archive2 = await CreateZipArchive(async, ms2, ZipArchiveMode.Update);

ZipArchiveEntry entry1 = archive1.Entries[0];
ZipArchiveEntry entry2 = archive2.Entries[0];

byte[] contents1, contents2;

using (Stream stream1 = entry1.Open())
{
Assert.True(stream1.CanRead);
Assert.True(stream1.CanWrite);
Assert.True(stream1.CanSeek);

using var reader = new MemoryStream();
stream1.CopyTo(reader);
contents1 = reader.ToArray();
}

using (Stream stream2 = entry2.Open(FileAccess.ReadWrite))
{
Assert.True(stream2.CanRead);
Assert.True(stream2.CanWrite);
Assert.True(stream2.CanSeek);

using var reader = new MemoryStream();
stream2.CopyTo(reader);
contents2 = reader.ToArray();
}

Assert.Equal(contents1, contents2);

await DisposeZipArchive(async, archive1);
await DisposeZipArchive(async, archive2);
}

[Theory]
[MemberData(nameof(Get_Booleans_Data))]
public static async Task OpenWithFileAccess_DisposedArchive_Throws(bool async)
{
using MemoryStream ms = await StreamHelpers.CreateTempCopyStream(zfile("normal.zip"));
ZipArchive archive = await CreateZipArchive(async, ms, ZipArchiveMode.Read);

ZipArchiveEntry entry = archive.GetEntry("first.txt");
Assert.NotNull(entry);

await DisposeZipArchive(async, archive);

Assert.Throws<ObjectDisposedException>(() => entry.Open(FileAccess.Read));
}
}
}
Loading