From ac9becba4d95924d7500295eb7b3c42847848b4c Mon Sep 17 00:00:00 2001 From: Guust Ysebie Date: Wed, 11 Jun 2025 12:18:58 +0200 Subject: [PATCH] Move Limitedinput stream to IO package DEVSIX-9163 --- .../resource/DefaultResourceRetrieverTest.cs | 57 +++++ .../resource/LimitedInputStreamTest.cs | 235 ++++++++++++++++++ .../resolver/resource/LimitedInputStream.cs | 150 +++++++++++ 3 files changed, 442 insertions(+) create mode 100644 itext.tests/itext.io.tests/itext/io/resolver/resource/DefaultResourceRetrieverTest.cs create mode 100644 itext.tests/itext.io.tests/itext/io/resolver/resource/LimitedInputStreamTest.cs create mode 100644 itext/itext.io/itext/io/resolver/resource/LimitedInputStream.cs diff --git a/itext.tests/itext.io.tests/itext/io/resolver/resource/DefaultResourceRetrieverTest.cs b/itext.tests/itext.io.tests/itext/io/resolver/resource/DefaultResourceRetrieverTest.cs new file mode 100644 index 000000000..1eb9d34ea --- /dev/null +++ b/itext.tests/itext.io.tests/itext/io/resolver/resource/DefaultResourceRetrieverTest.cs @@ -0,0 +1,57 @@ +/* +This file is part of the iText (R) project. +Copyright (c) 1998-2025 Apryse Group NV +Authors: Apryse Software. + +This program is offered under a commercial and under the AGPL license. +For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below. + +AGPL licensing: +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +using System; +using System.Net; +using iText.Commons.Utils; +using iText.Test; + +namespace iText.IO.Resolver.Resource { +//\cond DO_NOT_DOCUMENT + [NUnit.Framework.Category("IntegrationTest")] + internal class DefaultResourceRetrieverTest : ExtendedITextTest { + [NUnit.Framework.Test] + public virtual void RetrieveResourceConnectTimeoutTest() { + bool exceptionThrown = false; + Uri url = new Uri("http://10.255.255.1/"); + DefaultResourceRetriever resourceRetriever = new DefaultResourceRetriever(); + resourceRetriever.SetConnectTimeout(500); + + try { + // We check 2 possible exceptions + resourceRetriever.GetInputStreamByUrl(url); + } + catch (WebException e) { + exceptionThrown = true; + // Do not check exception message because it is localized + } + catch (OperationCanceledException e) { + exceptionThrown = true; + NUnit.Framework.Assert.AreEqual("the operation was canceled.", StringNormalizer.ToLowerCase(e.Message)); + } + + NUnit.Framework.Assert.True(exceptionThrown); + } + } +//\endcond +} \ No newline at end of file diff --git a/itext.tests/itext.io.tests/itext/io/resolver/resource/LimitedInputStreamTest.cs b/itext.tests/itext.io.tests/itext/io/resolver/resource/LimitedInputStreamTest.cs new file mode 100644 index 000000000..726326263 --- /dev/null +++ b/itext.tests/itext.io.tests/itext/io/resolver/resource/LimitedInputStreamTest.cs @@ -0,0 +1,235 @@ +/* +This file is part of the iText (R) project. +Copyright (c) 1998-2025 Apryse Group NV +Authors: Apryse Software. + +This program is offered under a commercial and under the AGPL license. +For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below. + +AGPL licensing: +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ +using System; +using System.IO; +using iText.IO.Exceptions; +using iText.Test; + +namespace iText.IO.Resolver.Resource { + [NUnit.Framework.Category("UnitTest")] + public class LimitedInputStreamTest : ExtendedITextTest { + [NUnit.Framework.Test] + public virtual void ReadingByteAfterFileReadingTest() { + using (Stream stream = new LimitedInputStream(new LimitedInputStreamTest.TestStreamGenerator().OpenStream( + ), 100)) { + // The user can call the reading methods as many times as he want, and if the + // stream has been read, then should not throw an ReadingByteLimitException exception + for (int i = 0; i < 101; i++) { + NUnit.Framework.Assert.DoesNotThrow(() => stream.Read()); + } + } + } + + [NUnit.Framework.Test] + public virtual void ReadingByteArrayAfterFileReadingTest() { + using (Stream stream = new LimitedInputStream(new LimitedInputStreamTest.TestStreamGenerator().OpenStream( + ), 100)) { + // The user can call the reading methods as many times as he want, and if the + // stream has been read, then should not throw an ReadingByteLimitException exception + NUnit.Framework.Assert.DoesNotThrow(() => stream.Read(new byte[100])); + NUnit.Framework.Assert.DoesNotThrow(() => stream.Read(new byte[1])); + } + } + + [NUnit.Framework.Test] + public virtual void ReadingByteArrayWithOffsetAfterFileReadingTest() { + using (Stream stream = new LimitedInputStream(new LimitedInputStreamTest.TestStreamGenerator().OpenStream( + ), 100)) { + // The user can call the reading methods as many times as he want, and if the + // stream has been read, then should not throw an ReadingByteLimitException exception + NUnit.Framework.Assert.DoesNotThrow(() => { + stream.JRead(new byte[100], 0, 100); + } + ); + NUnit.Framework.Assert.DoesNotThrow(() => { + stream.JRead(new byte[1], 0, 1); + } + ); + } + } + + [NUnit.Framework.Test] + public virtual void ReadingByteWithLimitOfOneLessThenFileSizeTest() { + using (Stream stream = new LimitedInputStream(new LimitedInputStreamTest.TestStreamGenerator().OpenStream( + ), 88)) { + for (int i = 0; i < 88; i++) { + NUnit.Framework.Assert.AreNotEqual(-1, stream.Read()); + } + NUnit.Framework.Assert.Catch(typeof(ReadingByteLimitException), () => stream.Read()); + } + } + + [NUnit.Framework.Test] + public virtual void ReadingByteArrayWithLimitOfOneLessThenFileSizeTest() { + using (Stream stream = new LimitedInputStream(new LimitedInputStreamTest.TestStreamGenerator().OpenStream( + ), 88)) { + byte[] bytes = new byte[100]; + int numOfReadBytes = stream.Read(bytes); + NUnit.Framework.Assert.AreEqual(88, numOfReadBytes); + NUnit.Framework.Assert.AreEqual(10, bytes[87]); + NUnit.Framework.Assert.AreEqual(0, bytes[88]); + NUnit.Framework.Assert.Catch(typeof(ReadingByteLimitException), () => stream.Read(new byte[1])); + } + } + + [NUnit.Framework.Test] + public virtual void ReadingByteArrayWithOffsetAndLimitOfOneLessThenFileSizeTest() { + using (Stream stream = new LimitedInputStream(new LimitedInputStreamTest.TestStreamGenerator().OpenStream( + ), 88)) { + byte[] bytes = new byte[100]; + int numOfReadBytes = stream.JRead(bytes, 0, 88); + NUnit.Framework.Assert.AreEqual(88, numOfReadBytes); + NUnit.Framework.Assert.AreEqual(10, bytes[87]); + NUnit.Framework.Assert.AreEqual(0, bytes[88]); + NUnit.Framework.Assert.Catch(typeof(ReadingByteLimitException), () => stream.JRead(bytes, 88, 1)); + } + } + + [NUnit.Framework.Test] + public virtual void ReadingByteArrayWithSmallBufferTest() { + using (Stream stream = new LimitedInputStream(new LimitedInputStreamTest.TestStreamGenerator().OpenStream( + ), 89)) { + byte[] bytes = new byte[20]; + MemoryStream output = new MemoryStream(); + while (true) { + int read = stream.Read(bytes); + if (read < 1) { + break; + } + output.Write(bytes, 0, read); + } + NUnit.Framework.Assert.AreEqual(89, output.Length); + output.Dispose(); + } + } + + [NUnit.Framework.Test] + public virtual void ReadingByteArrayWithBigBufferTest() { + // retrieveStyleSheetTest.css.dat size is 89 bytes + using (Stream stream = new LimitedInputStream(new LimitedInputStreamTest.TestStreamGenerator().OpenStream( + ), 89)) { + byte[] bytes = new byte[100]; + NUnit.Framework.Assert.AreEqual(89, stream.Read(bytes)); + byte[] tempBytes = (byte[])bytes.Clone(); + NUnit.Framework.Assert.AreEqual(-1, stream.Read(bytes)); + // Check that the array has not changed when we have read the entire LimitedInputStream + NUnit.Framework.Assert.AreEqual(tempBytes, bytes); + } + } + + [NUnit.Framework.Test] + public virtual void ReadingByteArrayWithOffsetAndBigBufferTest() { + using (Stream stream = new LimitedInputStream(new LimitedInputStreamTest.TestStreamGenerator().OpenStream( + ), 89)) { + byte[] bytes = new byte[100]; + NUnit.Framework.Assert.AreEqual(89, stream.JRead(bytes, 0, 100)); + byte[] tempBytes = (byte[])bytes.Clone(); + NUnit.Framework.Assert.AreEqual(-1, stream.JRead(bytes, 0, 100)); + // Check that the array has not changed when we have read the entire LimitedInputStream + NUnit.Framework.Assert.AreEqual(tempBytes, bytes); + } + } + + [NUnit.Framework.Test] + public virtual void ByteArrayOverwritingTest() { + using (Stream stream = new LimitedInputStream(new LimitedInputStreamTest.TestStreamGenerator().OpenStream( + ), 90)) { + byte[] bytes = new byte[100]; + bytes[89] = 13; + NUnit.Framework.Assert.AreEqual(89, stream.Read(bytes)); + // Check that when calling the read(byte[]) method, as many bytes were copied into + // the original array as were read, and not all bytes from the auxiliary array. + NUnit.Framework.Assert.AreEqual(13, bytes[89]); + } + } + + [NUnit.Framework.Test] + public virtual void ReadingByteWithZeroLimitTest() { + using (LimitedInputStream stream = new LimitedInputStream(new MemoryStream(new byte[1]), 0)) { + NUnit.Framework.Assert.Catch(typeof(ReadingByteLimitException), () => stream.Read()); + } + } + + [NUnit.Framework.Test] + public virtual void ReadingByteArrayWithZeroLimitTest() { + using (LimitedInputStream stream = new LimitedInputStream(new MemoryStream(new byte[1]), 0)) { + byte[] bytes = new byte[100]; + NUnit.Framework.Assert.Catch(typeof(ReadingByteLimitException), () => stream.Read(bytes)); + } + } + + [NUnit.Framework.Test] + public virtual void ReadingByteArrayWithOffsetAndZeroLimitTest() { + using (LimitedInputStream stream = new LimitedInputStream(new MemoryStream(new byte[1]), 0)) { + byte[] bytes = new byte[100]; + NUnit.Framework.Assert.Catch(typeof(ReadingByteLimitException), () => stream.JRead(bytes, 0, 100)); + } + } + + [NUnit.Framework.Test] + public virtual void ReadingEmptyByteWithZeroLimitTest() { + using (LimitedInputStream stream = new LimitedInputStream(new MemoryStream(new byte[0]), 0)) { + NUnit.Framework.Assert.AreEqual(-1, stream.Read()); + } + } + + [NUnit.Framework.Test] + public virtual void ReadingEmptyByteArrayWithZeroLimitTest() { + using (LimitedInputStream stream = new LimitedInputStream(new MemoryStream(new byte[0]), 0)) { + byte[] bytes = new byte[100]; + NUnit.Framework.Assert.AreEqual(-1, stream.Read(bytes)); + } + } + + [NUnit.Framework.Test] + public virtual void ReadingEmptyByteArrayWithOffsetAndZeroLimitTest() { + using (LimitedInputStream stream = new LimitedInputStream(new MemoryStream(new byte[0]), 0)) { + byte[] bytes = new byte[100]; + NUnit.Framework.Assert.AreEqual(-1, stream.JRead(bytes, 0, 100)); + } + } + + [NUnit.Framework.Test] + public virtual void IllegalReadingByteLimitValueTest() { + Exception e = NUnit.Framework.Assert.Catch(typeof(ArgumentException), () => new LimitedInputStream(new MemoryStream + (new byte[0]), -1)); + NUnit.Framework.Assert.AreEqual(IoExceptionMessageConstant.READING_BYTE_LIMIT_MUST_NOT_BE_LESS_ZERO, e.Message + ); + } + +//\cond DO_NOT_DOCUMENT + internal class TestStreamGenerator { +//\cond DO_NOT_DOCUMENT + internal String data = "body {\n" + " background-color: lightblue;\n" + "}\n" + "\n" + "h1 {\n" + " color: navy;\n" + + " margin-left: 20px;\n" + "}"; +//\endcond + +//\cond DO_NOT_DOCUMENT + internal virtual Stream OpenStream() { + return new MemoryStream(data.GetBytes(System.Text.Encoding.UTF8)); + } +//\endcond + } +//\endcond + } +} diff --git a/itext/itext.io/itext/io/resolver/resource/LimitedInputStream.cs b/itext/itext.io/itext/io/resolver/resource/LimitedInputStream.cs new file mode 100644 index 000000000..05b702f89 --- /dev/null +++ b/itext/itext.io/itext/io/resolver/resource/LimitedInputStream.cs @@ -0,0 +1,150 @@ +/* + This file is part of the iText (R) project. +Copyright (c) 1998-2025 Apryse Group NV + Authors: Apryse Software. + + This program is offered under a commercial and under the AGPL license. + For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below. + + AGPL licensing: + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + */ +using System; +using System.IO; +using iText.IO.Exceptions; + +namespace iText.IO.Resolver.Resource { + //\cond DO_NOT_DOCUMENT + /// + /// Implementation of the + /// + /// abstract class, which is used to restrict + /// reading bytes from input stream i.e. if more bytes are read than the readingByteLimit, + /// an + /// + /// exception will be thrown. + /// + /// Note that the readingByteLimit is not taken into account in the , + /// methods. + /// + internal class LimitedInputStream : Stream { + private bool isLimitViolated; + + private long readingByteLimit; + + private Stream inputStream; + + /// + /// Creates a new + /// + /// instance. + /// + /// the input stream, the reading of bytes from which will be limited + /// the reading byte limit, must not be less than zero + public LimitedInputStream(Stream inputStream, long readingByteLimit) { + if (readingByteLimit < 0) { + throw new ArgumentException(IoExceptionMessageConstant.READING_BYTE_LIMIT_MUST_NOT_BE_LESS_ZERO); + } + this.isLimitViolated = false; + this.inputStream = inputStream; + this.readingByteLimit = readingByteLimit; + } + + public override int Read(byte[] buffer, int offset, int count) { + if (isLimitViolated) { + throw new ReadingByteLimitException(); + } + + if (count > readingByteLimit) { + if (readingByteLimit == 0) { + // Still need to test if end of stream is reached, so setting 1 byte to read + count = 1; + } else { + // Safe to cast to int, because count is int and greater + count = (int) readingByteLimit; + } + } + + int bytesRead = inputStream.Read(buffer, offset, count); + readingByteLimit -= bytesRead; + + // If end of stream is met at the moment when readingByteLimit == 0 + // we will not throw an exception, because readingByteLimit would not change + if (readingByteLimit < 0) { + isLimitViolated = true; + throw new ReadingByteLimitException(); + } + + return bytesRead; + } + + protected override void Dispose(bool disposing) { + if (disposing) { + inputStream.Dispose(); + } + + base.Dispose(disposing); + } + + public override void Flush() { + inputStream.Flush(); + } + + public override long Seek(long offset, SeekOrigin origin) { + return inputStream.Seek(offset, origin); + } + + public override void SetLength(long value) { + inputStream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) { + inputStream.Write(buffer, offset, count); + } + + public override bool CanRead { + get { + return inputStream.CanRead; + } + } + + public override bool CanSeek { + get { + return inputStream.CanSeek; + } + } + + public override bool CanWrite { + get { + return inputStream.CanWrite; + } + } + + public override long Length { + get { + return inputStream.Length; + } + } + + public override long Position { + get { + return inputStream.Position; + } + set { + inputStream.Position = value; + } + } + } + //\endcond +}