diff --git a/pom.xml b/pom.xml index be742e2..f9c57c6 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ moe.yo3explorer dotnet4j - 1.2.4 + 1.2.5 jar @@ -15,7 +15,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.12.1 17 @@ -23,7 +23,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.2.2 + 3.2.5 -Djava.util.logging.config.file=${project.build.testOutputDirectory}/logging.properties @@ -34,7 +34,7 @@ org.apache.maven.plugins maven-source-plugin - 3.2.1 + 3.3.0 attach-sources @@ -47,7 +47,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.5.0 + 3.7.0 attach-javadocs @@ -63,7 +63,7 @@ -Xdoclint:-missing en_US - true + false false false false @@ -80,7 +80,7 @@ org.junit junit-bom - 5.10.2 + 5.10.3 pom import @@ -104,7 +104,7 @@ com.github.umjammer vavi-commons - 1.1.11 + 1.1.14 diff --git a/src/main/java/dotnet4j/io/BinaryReader.java b/src/main/java/dotnet4j/io/BinaryReader.java index ba4d634..d421bf7 100644 --- a/src/main/java/dotnet4j/io/BinaryReader.java +++ b/src/main/java/dotnet4j/io/BinaryReader.java @@ -425,14 +425,14 @@ private int internalReadOneChar() { charsRead = this.singleChar.length; assert charsRead < 2 : "InternalReadOneChar - assuming we only got 0 or 1 char, not 2!"; -// System.err.println("That became: " + charsRead + " characters."); +//logger.log(Level.TRACE, "That became: " + charsRead + " characters."); } if (charsRead == 0) return -1; return this.singleChar[0]; } - //[SecuritySafeCritical] + // [SecuritySafeCritical] public char[] readChars(int count) throws java.io.IOException { if (count < 0) { throw new IndexOutOfBoundsException("count: " + count); diff --git a/src/main/java/dotnet4j/io/FileStream.java b/src/main/java/dotnet4j/io/FileStream.java index 971dc4f..a859e82 100644 --- a/src/main/java/dotnet4j/io/FileStream.java +++ b/src/main/java/dotnet4j/io/FileStream.java @@ -3,16 +3,21 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; -import vavi.util.Debug; +import static java.lang.System.getLogger; /** * Created by FT on 27.11.14. */ public class FileStream extends Stream { + + private static final Logger logger = getLogger(FileStream.class.getName()); + private FileChannel channel; private final FileMode myMode; private final FileAccess myAccess; @@ -156,12 +161,12 @@ public int read(byte[] buffer, int offset, int length) { int m; try { m = channel.read(tmp); -//Debug.println(m + ", " + offset + ", " + length + " / " + channel.size() + ", " + channel.size() + ", " + channel.position()); +//logger.log(Level.TRACE, m + ", " + offset + ", " + length + " / " + channel.size() + ", " + channel.size() + ", " + channel.position()); if (m == -1) { return 0; } } catch (IOException e) { - Debug.printStackTrace(e); + logger.log(Level.TRACE, e.getMessage(), e); throw new dotnet4j.io.IOException(e); } return m; diff --git a/src/main/java/dotnet4j/io/MemoryStream.java b/src/main/java/dotnet4j/io/MemoryStream.java index 410d968..00c7329 100644 --- a/src/main/java/dotnet4j/io/MemoryStream.java +++ b/src/main/java/dotnet4j/io/MemoryStream.java @@ -273,8 +273,6 @@ public int read(byte[] buffer, int offset, int count) { return n; } -//public static boolean debug; - @Override public void write(byte[] buffer, int offset, int count) { if (buffer == null) @@ -289,7 +287,7 @@ public void write(byte[] buffer, int offset, int count) { throw new dotnet4j.io.IOException("object disposed"); if (!canWrite()) throw new dotnet4j.io.IOException("not writable"); -//if (debug) { Debug.println(offset + ", " + count + "\n" + StringUtil.getDump(buffer, offset, Math.min(count, 64))); new Exception().printStackTrace(); } +//logger.log(Level.TRACE, offset + ", " + count + "\n" + StringUtil.getDump(buffer, offset, Math.min(count, 64))); new Exception().printStackTrace(); } int i = position + count; // Check for overflow diff --git a/src/main/java/dotnet4j/io/compat/JavaIOStream.java b/src/main/java/dotnet4j/io/compat/JavaIOStream.java index c2ae189..a3016b0 100644 --- a/src/main/java/dotnet4j/io/compat/JavaIOStream.java +++ b/src/main/java/dotnet4j/io/compat/JavaIOStream.java @@ -15,7 +15,7 @@ /** - * JavaIOStream. + * InputStream + OutputStream = Stream. * * @author Naohide Sano (umjammer) * @version 0.00 2019/10/09 umjammer initial version
@@ -118,6 +118,9 @@ public void setLength(long value) { throw new UnsupportedOperationException(); } + /** + * @return 0 when EOF (it's C# spec) ⚠️⚠️⚠️ CAUTION not same as the java specs. ⚠️⚠️⚠️ + */ @Override public int read(byte[] buffer, int offset, int length) { if (is == null) { @@ -125,17 +128,17 @@ public int read(byte[] buffer, int offset, int length) { } try { -//Debug.println(buffer.length + ", " + offset + ", " + length + ", " + is.available()); +//logger.log(Level.TRACE, buffer.length + ", " + offset + ", " + length + ", " + is.available()); int r = is.read(buffer, offset, length); -//Debug.println(StringUtil.getDump(buffer, 16)); +//logger.log(Level.TRACE, StringUtil.getDump(buffer, 16)); if (r > 0) { position += r; } if (r == -1) { -//Debug.println("EOF"); +//logger.log(Level.TRACE, "EOF"); return 0; // C# Spec. } -//Debug.println("position: " + position); +//logger.log(Level.TRACE, "position: " + position); return r; } catch (IOException e) { throw new dotnet4j.io.IOException(e); @@ -166,7 +169,7 @@ public void write(byte[] buffer, int offset, int count) { } try { -//Debug.println("w: " + count + ", " + os); +//logger.log(Level.TRACE, "w: " + count + ", " + os); os.write(buffer, offset, count); position += count; } catch (IOException e) { diff --git a/src/main/java/dotnet4j/io/compat/StreamInputStream.java b/src/main/java/dotnet4j/io/compat/StreamInputStream.java index 6859403..ce78586 100644 --- a/src/main/java/dotnet4j/io/compat/StreamInputStream.java +++ b/src/main/java/dotnet4j/io/compat/StreamInputStream.java @@ -14,7 +14,7 @@ /** - * StreamInputStream. + * Treats Stream as InputStream. * * @author Naohide Sano (umjammer) * @version 0.00 2019/09/30 umjammer initial version
diff --git a/src/main/java/dotnet4j/io/compat/StreamOutputStream.java b/src/main/java/dotnet4j/io/compat/StreamOutputStream.java index a3c0039..0630d32 100644 --- a/src/main/java/dotnet4j/io/compat/StreamOutputStream.java +++ b/src/main/java/dotnet4j/io/compat/StreamOutputStream.java @@ -14,7 +14,7 @@ /** - * StreamOutputStream. + * Treats Stream as OutputStream. * * @author Naohide Sano (umjammer) * @version 0.00 2019/09/30 umjammer initial version
@@ -34,7 +34,7 @@ public void write(int b) { @Override public void write(byte[] buffer, int offset, int count) { -//Debug.println("w: " + count + ", " + stream); +//logger.log(Level.TRACE, "w: " + count + ", " + stream); stream.write(buffer, offset, count); } diff --git a/src/main/java/dotnet4j/io/compression/GZipStream.java b/src/main/java/dotnet4j/io/compression/GZipStream.java index 44fe4bc..15badd4 100644 --- a/src/main/java/dotnet4j/io/compression/GZipStream.java +++ b/src/main/java/dotnet4j/io/compression/GZipStream.java @@ -20,6 +20,7 @@ import vavi.io.InputEngineOutputStream; import vavi.io.OutputEngine; import vavi.io.OutputEngineInputStream; +import vavi.util.Debug; /** @@ -30,10 +31,14 @@ */ public class GZipStream extends JavaIOStream { + /** + * @param stream assume as input stream + * @param compressionMode {@link CompressionMode} + */ static InputStream toInputStream(Stream stream, CompressionMode compressionMode) { try { InputStream is = new StreamInputStream(stream); - return compressionMode == CompressionMode.Decompress ? new GZIPInputStream(is) + return compressionMode == CompressionMode.Decompress ? new GZIPInputStream(is) // TODO should be null stream? this maybe no mean : new OutputEngineInputStream(new OutputEngine() { OutputStream out; @@ -45,7 +50,8 @@ static InputStream toInputStream(Stream stream, CompressionMode compressionMode) @Override public void execute() throws IOException { int r = is.read(buf); - out.write(buf, 0, r); + if (r < 0) out.close(); + else out.write(buf, 0, r); } @Override public void finish() { @@ -56,24 +62,30 @@ static InputStream toInputStream(Stream stream, CompressionMode compressionMode) } } + /** + * @param stream assume as output stream + * @param compressionMode {@link CompressionMode} + */ static OutputStream toOutputStream(Stream stream, CompressionMode compressionMode) { try { OutputStream os = new StreamOutputStream(stream); return compressionMode == CompressionMode.Compress ? new GZIPOutputStream(os) - : new InputEngineOutputStream(new InputEngine() { + : new InputEngineOutputStream(new InputEngine() { // TODO should be null stream? this maybe no mean InputStream in; - @Override public void initialize(InputStream in) { + @Override public void initialize(InputStream in) throws IOException { + if (this.in == null && in.available() > 0 /* means stream is for input */) { +Debug.println(in + ", " + in.available()); + this.in = new GZIPInputStream(in); + } } final byte[] buf = new byte[8192]; @Override public void execute() throws IOException { - if (in == null) { - this.in = new GZIPInputStream(in); - } int r = in.read(buf); - os.write(buf, 0, r); + if (r < 0) in.close(); + else os.write(buf, 0, r); } @Override public void finish() { @@ -84,9 +96,7 @@ static OutputStream toOutputStream(Stream stream, CompressionMode compressionMod } } - /** - * - */ + /** @throws dotnet4j.io.IOException when an error occurs */ public GZipStream(Stream stream, CompressionMode compressionMode) { super(toInputStream(stream, compressionMode), toOutputStream(stream, compressionMode)); } diff --git a/src/main/java/dotnet4j/security/accessControl/AccessControlSections.java b/src/main/java/dotnet4j/security/accessControl/AccessControlSections.java index 0b7f631..7b07598 100644 --- a/src/main/java/dotnet4j/security/accessControl/AccessControlSections.java +++ b/src/main/java/dotnet4j/security/accessControl/AccessControlSections.java @@ -4,15 +4,15 @@ public enum AccessControlSections { - /** 随意アクセス制御リスト (DACL: Discretionary Access Control List)。 */ + /** DACL: Discretionary Access Control List */ Access(0x2), - /** システム アクセス制御リスト (SACL: System Access Control List)。 */ + /** SACL: System Access Control List */ Audit(0x1), - /** プライマリ グループ。 */ + /** primary group */ Group(0x8), - /** セクションを指定しません。 */ + /** no section specified */ None(0x0), - /** 所有者。 */ + /** owner */ Owner(0x4); final int value; @@ -21,6 +21,6 @@ public enum AccessControlSections { this.value = value; } - /** セキュリティ記述子全体。 */ + /** entire security descriptor */ public static final EnumSet All = EnumSet.of(Audit, Access, Owner, Group); } diff --git a/src/main/java/dotnet4j/security/accessControl/RegistrySecurity.java b/src/main/java/dotnet4j/security/accessControl/RegistrySecurity.java index 2355aaf..e10a30c 100644 --- a/src/main/java/dotnet4j/security/accessControl/RegistrySecurity.java +++ b/src/main/java/dotnet4j/security/accessControl/RegistrySecurity.java @@ -72,7 +72,7 @@ public String getSecurityDescriptorSddlForm(EnumSet secti /** TODO impl */ public void setSecurityDescriptorSddlForm(String form, EnumSet sections) { -//System.err.println(form); +//logger.log(Level.TRACE, form); binaryForm = form.getBytes(StandardCharsets.US_ASCII); } } diff --git a/src/main/java/dotnet4j/win32/RegistryValueOptions.java b/src/main/java/dotnet4j/win32/RegistryValueOptions.java index 462848f..c1cb31e 100644 --- a/src/main/java/dotnet4j/win32/RegistryValueOptions.java +++ b/src/main/java/dotnet4j/win32/RegistryValueOptions.java @@ -1,10 +1,10 @@ package dotnet4j.win32; public enum RegistryValueOptions { - /** オプションの動作は指定されていません。 */ + /** optional behavior is unspecified */ None, /** - * 型の値が、埋め込まれた環境変数を展開せずに取得されます。 + * the value of the type is retrieved without expanding embedded environment variables. * * @see "F:Microsoft.Win32.RegistryValueKind.ExpandString" */ diff --git a/src/test/java/dotnet4j/io/compat/JavaIOStreamTest.java b/src/test/java/dotnet4j/io/compat/JavaIOStreamTest.java new file mode 100644 index 0000000..eb1b8c2 --- /dev/null +++ b/src/test/java/dotnet4j/io/compat/JavaIOStreamTest.java @@ -0,0 +1,270 @@ +/* + * https://claude.ai/chat/5cb0054b-dc67-4dbd-91d7-960966aec653 + */ + +package dotnet4j.io.compat; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import dotnet4j.io.SeekOrigin; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/** + * JavaIOStreamTest. + * + * @author Naohide Sano (nsano) + * @version 0.00 2024-11-25 nsano initial version
+ */ +class JavaIOStreamTest { + + private ByteArrayInputStream inputStream; + private ByteArrayOutputStream outputStream; + private JavaIOStream stream; + private final byte[] testData = "Hello, World!".getBytes(); + + @BeforeEach + void setUp() { + inputStream = new ByteArrayInputStream(testData); + outputStream = new ByteArrayOutputStream(); + } + + @AfterEach + void tearDown() throws IOException { + if (stream != null) { + stream.close(); + } + inputStream.close(); + outputStream.close(); + } + + @Nested + @DisplayName("Constructor Tests") + class ConstructorTests { + @Test + @DisplayName("Input-only constructor should initialize correctly") + void testInputOnlyConstructor() { + stream = new JavaIOStream(inputStream); + assertTrue(stream.canRead()); + assertFalse(stream.canWrite()); + } + + @Test + @DisplayName("Input-output constructor should initialize correctly") + void testInputOutputConstructor() { + stream = new JavaIOStream(inputStream, outputStream); + assertTrue(stream.canRead()); + assertTrue(stream.canWrite()); + } + + @Test + @DisplayName("Constructor with leaveOpen should initialize correctly") + void testLeaveOpenConstructor() { + stream = new JavaIOStream(inputStream, outputStream, true); + assertTrue(stream.canRead()); + assertTrue(stream.canWrite()); + } + } + + @Nested + @DisplayName("Stream Capability Tests") + class StreamCapabilityTests { + @Test + @DisplayName("Stream capabilities should be correctly reported") + void testStreamCapabilities() { + stream = new JavaIOStream(inputStream, outputStream); + assertAll( + () -> assertTrue(stream.canRead()), + () -> assertTrue(stream.canWrite()), + () -> assertFalse(stream.canSeek()) + ); + } + + @Test + @DisplayName("Stream length should match input data") + void testGetLength() { + stream = new JavaIOStream(inputStream); + assertEquals(testData.length, stream.getLength()); + } + + @Test + @DisplayName("Position should be tracked correctly") + void testPosition() { + stream = new JavaIOStream(inputStream); + assertEquals(0, stream.position()); + + byte[] buffer = new byte[5]; + stream.read(buffer, 0, 5); + assertEquals(5, stream.position()); + } + } + + @Nested + @DisplayName("Unsupported Operation Tests") + class UnsupportedOperationTests { + @BeforeEach + void init() { + stream = new JavaIOStream(inputStream); + } + + @Test + @DisplayName("Setting position should throw UnsupportedOperationException") + void testSetPositionThrowsException() { + assertThrows(UnsupportedOperationException.class, + () -> stream.position(5)); + } + + @Test + @DisplayName("Seek should throw UnsupportedOperationException") + void testSeekThrowsException() { + assertThrows(UnsupportedOperationException.class, + () -> stream.seek(5, SeekOrigin.Begin)); + } + + @Test + @DisplayName("SetLength should throw UnsupportedOperationException") + void testSetLengthThrowsException() { + assertThrows(UnsupportedOperationException.class, + () -> stream.setLength(100)); + } + } + + @Nested + @DisplayName("Read Operation Tests") + class ReadOperationTests { + @Test + @DisplayName("Reading buffer should work correctly") + void testRead() { + stream = new JavaIOStream(inputStream); + byte[] buffer = new byte[testData.length]; + int bytesRead = stream.read(buffer, 0, buffer.length); + + assertAll( + () -> assertEquals(testData.length, bytesRead), + () -> assertArrayEquals(testData, buffer) + ); + } + + @Test + @DisplayName("Reading single byte should work correctly") + void testReadByte() { + stream = new JavaIOStream(inputStream); + assertAll( + () -> assertEquals(testData[0], stream.readByte()), + () -> assertEquals(1, stream.position()) + ); + } + + @Test + @DisplayName("Reading from closed stream should throw exception") + void testReadOnClosedStream() throws IOException { + stream = new JavaIOStream(inputStream); + stream.close(); + + Exception exception = assertThrows(dotnet4j.io.IOException.class, + () -> stream.read(new byte[1], 0, 1)); + assertEquals("closed", exception.getMessage()); + } + } + + @Nested + @DisplayName("Write Operation Tests") + class WriteOperationTests { + @Test + @DisplayName("Writing buffer should work correctly") + void testWrite() { + stream = new JavaIOStream(new ByteArrayInputStream(new byte[0]), outputStream); + stream.write(testData, 0, testData.length); + + assertAll( + () -> assertArrayEquals(testData, outputStream.toByteArray()), + () -> assertEquals(testData.length, stream.position()) + ); + } + + @Test + @DisplayName("Writing single byte should work correctly") + void testWriteByte() { + stream = new JavaIOStream(new ByteArrayInputStream(new byte[0]), outputStream); + stream.writeByte((byte) 65); // ASCII 'A' + + assertAll( + () -> assertArrayEquals(new byte[] { 65 }, outputStream.toByteArray()), + () -> assertEquals(2, stream.position(), "position must be count up 1") + ); + } + + @Test + @DisplayName("Writing to closed stream should throw exception") + void testWriteOnClosedStream() throws IOException { + stream = new JavaIOStream(inputStream, outputStream); + stream.close(); + + Exception exception = assertThrows(dotnet4j.io.IOException.class, + () -> stream.write(new byte[1], 0, 1)); + assertEquals("closed", exception.getMessage()); + } + } + + @Nested + @DisplayName("Stream Management Tests") + class StreamManagementTests { + @Test + @DisplayName("Flush should work correctly") + void testFlush() { + stream = new JavaIOStream(inputStream, outputStream); + stream.write(testData, 0, testData.length); + stream.flush(); + assertArrayEquals(testData, outputStream.toByteArray()); + } + + @Test + @DisplayName("Flush on closed stream should throw exception") + void testFlushOnClosedStream() throws IOException { + stream = new JavaIOStream(inputStream, outputStream); + stream.close(); + + Exception exception = assertThrows(dotnet4j.io.IOException.class, + () -> stream.flush()); + assertEquals("closed", exception.getMessage()); + } + + @Test + @DisplayName("Close with leaveOpen=false should close streams") + void testClose() throws IOException { + stream = new JavaIOStream(inputStream, outputStream, false); + stream.close(); + + Exception exception = assertThrows(dotnet4j.io.IOException.class, + () -> stream.read(new byte[1], 0, 1)); + assertEquals("closed", exception.getMessage()); + } + + @Test + @DisplayName("Close with leaveOpen=true should keep streams open") + void testLeaveOpenTrue() throws IOException { + stream = new JavaIOStream(inputStream, outputStream, true); + stream.close(); + + // Verify that the underlying streams are still usable + assertAll( + () -> assertDoesNotThrow(() -> inputStream.read()), + () -> assertDoesNotThrow(() -> outputStream.write(65)) + ); + } + } +} diff --git a/src/test/java/dotnet4j/io/compression/GZipStreamTest.java b/src/test/java/dotnet4j/io/compression/GZipStreamTest.java new file mode 100644 index 0000000..bf16818 --- /dev/null +++ b/src/test/java/dotnet4j/io/compression/GZipStreamTest.java @@ -0,0 +1,301 @@ +/* + * https://claude.ai/chat/5cb0054b-dc67-4dbd-91d7-960966aec653 + */ + +package dotnet4j.io.compression; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.zip.GZIPOutputStream; + +import dotnet4j.io.MemoryStream; +import dotnet4j.io.SeekOrigin; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; +import vavi.util.Debug; +import vavi.util.StringUtil; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/** + * GZipStreamTest. + * + * @author Naohide Sano (nsano) + * @version 0.00 2024-11-25 nsano initial version
+ */ +class GZipStreamTest { + + private MemoryStream memoryStream; + private final String testString = """ + This test suite covers: + + Compression Tests + + + Basic compression functionality + Compression followed by decompression + Handling empty data + Handling large data sets + + + Decompression Tests + + + Error handling for invalid data + Multiple read operations + Partial reads + + + Stream Capability Tests + + + Verification of supported operations + Testing unsupported operations throw correct exceptions + + + Resource Management Tests + + + Proper stream closing behavior + Flush operations + + Key features of the test suite: + + Organized using nested test classes for better readability and organization + Comprehensive test coverage for both compression and decompression + Testing of edge cases and error conditions + Resource cleanup using try-with-resources and @AfterEach + Use of assertAll() for multiple related assertions + Clear test names using @DisplayName + + The tests use a MemoryStream as the underlying stream for predictable behavior and easy verification. Error conditions and edge cases are also tested to ensure robust behavior. + Would you like me to add any additional test cases or modify the existing ones? + """; + private final byte[] testData = testString.getBytes(); + + @BeforeEach + void setUp() { + memoryStream = new MemoryStream(); + } + + @AfterEach + void tearDown() throws IOException { + memoryStream.close(); + } + + @Nested + @DisplayName("Compression Tests") + class CompressionTests { + + @Test + @Disabled("just confirmation") + void testX() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + GZIPOutputStream gzos = new GZIPOutputStream(baos); + gzos.write(testData, 0, testData.length); + gzos.flush(); + gzos.close(); +Debug.print("compressed size: " + baos.size() + "\n" + StringUtil.getDump(baos.toByteArray())); + } + + @Test + @DisplayName("Should compress data correctly") + void testCompression() throws IOException { + // Write test data to memory stream using GZipStream + try (GZipStream compressionStream = new GZipStream(memoryStream, CompressionMode.Compress)) { + compressionStream.write(testData, 0, testData.length); + compressionStream.flush(); + } + + byte[] compressedData = memoryStream.toArray(); +//Debug.print("compressed size: " + compressedData.length + "\n" + StringUtil.getDump(compressedData)); + + // Verify compression actually occurred + assertAll( + () -> assertTrue(compressedData.length > 0), + () -> assertTrue(compressedData.length < testData.length, "expect " + compressedData.length + " < " + testData.length), + () -> assertThrows(AssertionFailedError.class, () -> assertArrayEquals(testData, compressedData)) // TODO there isn't assertArrayNotEquals? + ); + } + + @Test + @DisplayName("Should compress and decompress data correctly") + void testCompressionDecompression() throws IOException { + // First compress the data + // TODO leave open doesn't work, GZIPOutputStream outputs something when close? + try (GZipStream compressionStream = new GZipStream(memoryStream, CompressionMode.Compress)) { + compressionStream.write(testData, 0, testData.length); + compressionStream.flush(); + } + +// memoryStream.position(0); // TODO ditto + MemoryStream memoryStream2 = new MemoryStream(memoryStream.toArray()); + + // Now decompress the data + byte[] decompressedData = new byte[testData.length]; + try (GZipStream decompressionStream = new GZipStream(memoryStream2, CompressionMode.Decompress)) { + int bytesRead = 0; + while (true) { + int r = decompressionStream.read(decompressedData, bytesRead, decompressedData.length - bytesRead); +Debug.println("decompressionStream: " + r); + if (r <= 0) break; // TODO GZipStream doesn't return -1 (not java spec), EOF is 0 (c# spec) + bytesRead += r; + } + assertEquals(testData.length, bytesRead); + } + + assertArrayEquals(testData, decompressedData); + } + + @Test + @DisplayName("Should compress empty data correctly") + void testCompressEmptyData() throws IOException { + byte[] emptyData = new byte[0]; + + try (GZipStream compressionStream = new GZipStream(memoryStream, CompressionMode.Compress)) { + compressionStream.write(emptyData, 0, 0); + } + + byte[] compressedData = memoryStream.toArray(); + assertTrue(compressedData.length > 0); // GZip header should still be present + } + + @Test + @DisplayName("Should compress large data correctly") + void testCompressLargeData() throws IOException { + // Create large data set + byte[] largeData = new byte[1000000]; // 1MB of data + Arrays.fill(largeData, (byte) 'A'); + + try (GZipStream compressionStream = new GZipStream(memoryStream, CompressionMode.Compress)) { + compressionStream.write(largeData, 0, largeData.length); + } + + byte[] compressedData = memoryStream.toArray(); + + // Highly compressible data should compress well + assertTrue(compressedData.length < largeData.length / 10); + } + } + + @Nested + @DisplayName("Decompression Tests") + class DecompressionTests { + + @Test + @DisplayName("Should throw exception for invalid compressed data") + void testDecompressInvalidData() { + byte[] invalidData = {1, 2, 3, 4, 5}; // Invalid GZip data + memoryStream.write(invalidData, 0, invalidData.length); + memoryStream.position(0); + + assertThrows(dotnet4j.io.IOException.class, () -> { + try (GZipStream decompressionStream = new GZipStream(memoryStream, CompressionMode.Decompress)) { + decompressionStream.read(new byte[10], 0, 10); + } + }); + } + + @Test + @DisplayName("Should handle multiple read operations correctly") + void testMultipleReads() throws IOException { + // First compress some data + // TODO leave open doesn't work, GZIPOutputStream outputs something when close? + try (GZipStream compressionStream = new GZipStream(memoryStream, CompressionMode.Compress)) { + compressionStream.write(testData, 0, testData.length); + compressionStream.flush(); + } + +// memoryStream.position(0); // TODO ditto + MemoryStream memoryStream2 = new MemoryStream(memoryStream.toArray()); + + // Read in small chunks + try (GZipStream decompressionStream = new GZipStream(memoryStream2, CompressionMode.Decompress)) { + byte[] decompressedData = new byte[testData.length]; + int totalBytesRead = 0; + int bytesRead; + int chunkSize = 4; + + while ((bytesRead = decompressionStream.read(decompressedData, totalBytesRead, + Math.min(chunkSize, testData.length - totalBytesRead))) > 0) { + totalBytesRead += bytesRead; + } + + assertEquals(testData.length, totalBytesRead); + assertArrayEquals(testData, decompressedData); + } + } + } + + @Nested + @DisplayName("Stream Capability Tests") + class StreamCapabilityTests { + + @Test + @DisplayName("Should report correct stream capabilities") + void testStreamCapabilities() throws IOException { + try (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Compress)) { + assertAll( + () -> assertTrue(gzipStream.canWrite()), + () -> assertFalse(gzipStream.canSeek()) + ); + } + } + + @Test + @DisplayName("Should throw exception for unsupported operations") + void testUnsupportedOperations() { + GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Compress); + + assertAll( + () -> assertThrows(UnsupportedOperationException.class, + () -> gzipStream.seek(0, SeekOrigin.Begin)), + () -> assertThrows(UnsupportedOperationException.class, + () -> gzipStream.setLength(100)), + () -> assertThrows(UnsupportedOperationException.class, + () -> gzipStream.position(50)) + ); + } + } + + @Nested + @DisplayName("Resource Management Tests") + class ResourceManagementTests { + + @Test + @DisplayName("Should close underlying streams correctly") + void testStreamClosing() throws IOException { + GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Compress); + gzipStream.write(testData, 0, testData.length); + gzipStream.close(); + + // Verify that operations on closed stream throw exceptions + assertThrows(dotnet4j.io.IOException.class, + () -> gzipStream.write(testData, 0, testData.length)); + } + + @Test + @DisplayName("Should flush compressed data correctly") + void testFlush() throws IOException { + try (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Compress)) { + gzipStream.write(testData, 0, testData.length); + gzipStream.flush(); + + // Verify that data was written to underlying stream + assertTrue(memoryStream.getLength() > 0); + } + } + } +}