Browse Source

Fixed binary reader from stream to handle support for streams that can't seek. Fixes #178

Vicente Penades 2 years ago
parent
commit
a2048f58a8

+ 33 - 9
src/SharpGLTF.Core/Schema2/Serialization.Binary.cs

@@ -32,6 +32,21 @@ namespace SharpGLTF.Schema2
             }
             }
         }
         }
 
 
+        internal static bool _TryReadUInt32(this System.IO.BinaryReader r, out UInt32 result)
+        {
+            try
+            {
+                result = r.ReadUInt32();
+                return true;
+            }
+            catch(System.IO.EndOfStreamException)
+            {
+                result = 0;
+                return false;
+            }
+        }
+        
+
         internal static bool _Identify(Stream stream)
         internal static bool _Identify(Stream stream)
         {
         {
             Guard.NotNull(stream, nameof(stream));
             Guard.NotNull(stream, nameof(stream));
@@ -80,14 +95,9 @@ namespace SharpGLTF.Schema2
                 _ReadBinaryHeader(binaryReader);
                 _ReadBinaryHeader(binaryReader);
 
 
                 var chunks = new Dictionary<uint, Byte[]>();
                 var chunks = new Dictionary<uint, Byte[]>();
-
-                // keep reading until EndOfFile
-                while (true)
+                
+                while (binaryReader._TryReadUInt32(out var chunkLength)) // keep reading until EndOfFile
                 {
                 {
-                    if (binaryReader.PeekChar() < 0) break;
-
-                    uint chunkLength = binaryReader.ReadUInt32();
-
                     if ((chunkLength & 3) != 0)
                     if ((chunkLength & 3) != 0)
                     {
                     {
                         throw new Validation.SchemaException(null, $"The chunk must be padded to 4 bytes: {chunkLength}");
                         throw new Validation.SchemaException(null, $"The chunk must be padded to 4 bytes: {chunkLength}");
@@ -120,9 +130,23 @@ namespace SharpGLTF.Schema2
             if (version != GLTFVERSION2) throw new Validation.SchemaException(null, $"Unknown version number: {version}");
             if (version != GLTFVERSION2) throw new Validation.SchemaException(null, $"Unknown version number: {version}");
 
 
             uint length = binaryReader.ReadUInt32();
             uint length = binaryReader.ReadUInt32();
-            long fileLength = binaryReader.BaseStream.Length;
 
 
-            if (length != fileLength) throw new Validation.SchemaException(null, $"The specified length of the file ({length}) is not equal to the actual length of the file ({fileLength}).");
+            uint? fileLength = null;
+
+            try
+            {
+                fileLength = (uint)binaryReader.BaseStream.Length;                
+            }
+            catch (System.NotSupportedException)
+            {
+                // Some streams like Android assets don't support getting the length
+                // https://github.com/vpenades/SharpGLTF/issues/178
+            }
+
+            if (fileLength.HasValue && length != fileLength.Value)
+            {
+                throw new Validation.SchemaException(null, $"The specified length of the file ({length}) is not equal to the actual length of the file ({fileLength}).");
+            }
         }
         }
 
 
         #endregion
         #endregion

+ 16 - 2
tests/SharpGLTF.Core.Tests/Schema2/LoadAndSave/RegressionTests.cs

@@ -11,6 +11,7 @@ using SharpGLTF.Validation;
 namespace SharpGLTF.Schema2.LoadAndSave
 namespace SharpGLTF.Schema2.LoadAndSave
 {
 {
     [AttachmentPathFormat("*/TestResults/LoadAndSave/?", true)]
     [AttachmentPathFormat("*/TestResults/LoadAndSave/?", true)]
+    [ResourcePathFormat("*/Assets/SpecialCases")]
     internal class RegressionTests
     internal class RegressionTests
     {
     {
         [Test]
         [Test]
@@ -20,7 +21,7 @@ namespace SharpGLTF.Schema2.LoadAndSave
 
 
             var cdir = Environment.CurrentDirectory;
             var cdir = Environment.CurrentDirectory;
 
 
-            var modelPath = System.IO.Path.Combine(TestContext.CurrentContext.TestDirectory, "Assets\\SpecialCases\\RelativePaths.gltf");
+            var modelPath = ResourceInfo.From("RelativePaths.gltf");
 
 
             // absolute path            
             // absolute path            
 
 
@@ -88,6 +89,19 @@ namespace SharpGLTF.Schema2.LoadAndSave
 
 
             Assert.AreEqual(suzanne1Mem, suzanne3Mem);
             Assert.AreEqual(suzanne1Mem, suzanne3Mem);
             Assert.AreEqual(suzanne1.LogicalMeshes.Count, suzanne3.LogicalMeshes.Count);
             Assert.AreEqual(suzanne1.LogicalMeshes.Count, suzanne3.LogicalMeshes.Count);
-        }        
+        }
+
+        [Test]
+        public void LoadBinaryWithLimitedStream()
+        {
+            var path1 = TestFiles.GetSampleModelsPaths().First(item => item.EndsWith("BrainStem.glb"));
+
+            var bytes = System.IO.File.ReadAllBytes(path1);
+            using(var ls = new ReadOnlyTestStream(bytes))
+            {
+                var model = ModelRoot.ReadGLB(ls);
+                Assert.NotNull(model);
+            }
+        }
     }
     }
 }
 }

+ 74 - 0
tests/SharpGLTF.Core.Tests/Schema2/LoadAndSave/StreamUtils.cs

@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SharpGLTF.Schema2.LoadAndSave
+{
+    /// <summary>
+    /// Utility stream used to simulate streams that don't support <see cref="CanSeek"/> nor <see cref="Length"/>
+    /// </summary>
+    internal class ReadOnlyTestStream : System.IO.Stream
+    {
+        public ReadOnlyTestStream(Byte[] data)
+        {
+            _Data = data;
+        }
+
+        private readonly Byte[] _Data;
+        private int _Position;
+
+        public override bool CanRead => true;
+
+        public override bool CanSeek => false;
+
+        public override bool CanWrite => false;
+
+        public override long Length => throw new NotSupportedException();
+
+        public override long Position
+        {
+            get => _Position;
+            set => throw new NotSupportedException();
+        }
+
+        public override void Flush() { }
+
+        public override int Read(byte[] buffer, int offset, int count)
+        {
+            if (_Position >= _Data.Length) return 0;
+
+            if (count > 1) count /= 2; // simulate partial reads
+            
+            var bytesLeft = _Data.Length - _Position;
+
+            count = Math.Min(count, bytesLeft);
+
+            var dst = buffer.AsSpan().Slice(offset, count);
+            var src = _Data.AsSpan().Slice(_Position, count);
+
+            src.CopyTo(dst);
+
+            _Position += count;
+
+            return count;
+        }
+
+        public override long Seek(long offset, SeekOrigin origin)
+        {
+            throw new NotSupportedException();
+        }
+
+        public override void SetLength(long value)
+        {
+            throw new NotSupportedException();
+        }
+
+        public override void Write(byte[] buffer, int offset, int count)
+        {
+            throw new NotSupportedException();
+        }
+    }
+}