Browse Source

+WIP improving read/write contexts

Vicente Penades 3 years ago
parent
commit
5ce59d2659

+ 18 - 12
src/SharpGLTF.Core/Schema2/Serialization.Binary.cs

@@ -32,17 +32,6 @@ namespace SharpGLTF.Schema2
             }
         }
 
-        public static bool IsBinaryHeader(Byte a, Byte b, Byte c, Byte d)
-        {
-            uint magic = 0;
-            magic |= (uint)a;
-            magic |= (uint)b << 8;
-            magic |= (uint)c << 16;
-            magic |= (uint)d << 24;
-
-            return magic == GLTFHEADER;
-        }
-
         internal static bool _Identify(Stream stream)
         {
             Guard.NotNull(stream, nameof(stream));
@@ -60,6 +49,23 @@ namespace SharpGLTF.Schema2
             return IsBinaryHeader((Byte)a, (Byte)b, (Byte)c, (Byte)d);
         }
 
+        internal static bool IsBinaryHeader(ReadOnlySpan<Byte> span)
+        {
+            if (span.Length < 4) return false;
+            return IsBinaryHeader(span[0], span[1], span[2], span[3]);
+        }
+
+        public static bool IsBinaryHeader(Byte a, Byte b, Byte c, Byte d)
+        {
+            uint magic = 0;
+            magic |= (uint)a;
+            magic |= (uint)b << 8;
+            magic |= (uint)c << 16;
+            magic |= (uint)d << 24;
+
+            return magic == GLTFHEADER;
+        }
+
         public static IReadOnlyDictionary<UInt32, Byte[]> ReadBinaryFile(Stream stream)
         {
             Guard.NotNull(stream, nameof(stream));
@@ -157,7 +163,7 @@ namespace SharpGLTF.Schema2
         {
             var ex = IsBinaryCompatible(model); if (ex != null) throw ex;
 
-            var jsonText = model.GetJSON(false);
+            var jsonText = model._GetJSON(false);
             var jsonChunk = Encoding.UTF8.GetBytes(jsonText);
             var jsonPadding = jsonChunk.Length & 3; if (jsonPadding != 0) jsonPadding = 4 - jsonPadding;
 

+ 84 - 45
src/SharpGLTF.Core/Schema2/Serialization.ReadContext.cs

@@ -95,9 +95,16 @@ namespace SharpGLTF.Schema2
 
         #region data
 
-        private FileReaderCallback _FileReader;
+        /// <summary>
+        /// Unescapes glTF asset URIs so they can be consumed by <see cref="_FileReader"/>
+        /// </summary>
         private UriResolver _UriResolver;
 
+        /// <summary>
+        /// Retrieves file blobs from the current context.
+        /// </summary>
+        private FileReaderCallback _FileReader;
+
         /// <summary>
         /// When loading a GLB, this represents the internal binary data chunk.
         /// </summary>
@@ -119,43 +126,63 @@ namespace SharpGLTF.Schema2
             return true;
         }
 
-        public BYTES ReadAllBytesToEnd(string fileName)
-        {
-            if (_BinaryChunk != null)
-            {
-                if (string.IsNullOrEmpty(fileName)) return new BYTES(_BinaryChunk);
-            }
-
-            return _FileReader(fileName);
-        }
-
         /// <summary>
         /// Opens a file relative to this <see cref="ReadContext"/>.
         /// </summary>
-        /// <param name="fileName">A relative file Name path.</param>
+        /// <param name="resourceName">A relative file Name path.</param>
         /// <returns>A <see cref="Stream"/>.</returns>
-        public Stream OpenFile(string fileName)
+        public Stream OpenFile(string resourceName)
         {
-            var content = _FileReader(fileName);
+            var content = ReadAllBytesToEnd(resourceName);
 
             return new MemoryStream(content.Array, content.Offset, content.Count);
         }
 
+        public BYTES ReadAllBytesToEnd(string resourceName)
+        {
+            if (_BinaryChunk != null)
+            {
+                if (string.IsNullOrEmpty(resourceName)) return new BYTES(_BinaryChunk);
+            }
+
+            return _FileReader(resourceName);
+        }
+
         #endregion
 
         #region API
 
-        public Validation.ValidationResult Validate(string filePath)
+        public Validation.ValidationResult Validate(string resourceName)
         {
-            using (var stream = File.OpenRead(filePath))
+            Guard.FilePathMustBeValid(resourceName, nameof(resourceName));
+
+            var root = this.ReadAllBytesToEnd(resourceName);
+
+            if (!_BinarySerialization.IsBinaryHeader(root))
             {
-                bool isBinary = _BinarySerialization._Identify(stream);
+                return _Read(root).Validation;
+            }
 
-                if (isBinary) return _ReadGLB(stream).Validation;
+            using (var stream = new MemoryStream(root.Array, root.Offset, root.Count, false))
+            {
+                return _ReadGLB(stream).Validation;
+            }
+        }
 
-                var json = stream.ReadBytesToEnd();
+        /// <summary>
+        /// Reads a <see cref="MODEL"/> instance from the current context containing a GLB or a GLTF file.
+        /// </summary>
+        /// <param name="resourceName">The name of the resource within the context.</param>
+        /// <returns>A <see cref="MODEL"/> instance.</returns>
+        public MODEL ReadSchema2(string resourceName)
+        {
+            Guard.FilePathMustBeValid(resourceName, nameof(resourceName));
+
+            var root = this.ReadAllBytesToEnd(resourceName);
 
-                return _Read(json).Validation;
+            using (var stream = new MemoryStream(root.Array, root.Offset, root.Count, false))
+            {
+                return ReadSchema2(stream);
             }
         }
 
@@ -171,18 +198,9 @@ namespace SharpGLTF.Schema2
 
             bool binaryFile = _BinarySerialization._Identify(stream);
 
-            return binaryFile ? ReadBinarySchema2(stream) : ReadTextSchema2(stream);
-        }
-
-        internal MODEL _ReadFromDictionary(string fileName)
-        {
-            var json = this.ReadAllBytesToEnd(fileName);
-
-            var mv = this._Read(json);
-
-            if (mv.Validation.HasErrors) throw mv.Validation.Errors.FirstOrDefault();
-
-            return mv.Model;
+            return binaryFile
+                ? ReadBinarySchema2(stream)
+                : ReadTextSchema2(stream);
         }
 
         /// <summary>
@@ -197,11 +215,7 @@ namespace SharpGLTF.Schema2
 
             var json = stream.ReadBytesToEnd();
 
-            var mv = this._Read(json);
-
-            if (mv.Validation.HasErrors) throw mv.Validation.Errors.FirstOrDefault();
-
-            return mv.Model;
+            return _FilterErrors(this._Read(json));
         }
 
         /// <summary>
@@ -214,9 +228,17 @@ namespace SharpGLTF.Schema2
             Guard.NotNull(stream, nameof(stream));
             Guard.IsTrue(stream.CanRead, nameof(stream));
 
-            var mv = this._ReadGLB(stream);
+            return _FilterErrors(this._ReadGLB(stream));
+        }
 
-            if (mv.Validation.HasErrors) throw mv.Validation.Errors.FirstOrDefault();
+        private static MODEL _FilterErrors((MODEL Model, Validation.ValidationResult Validation) mv)
+        {
+            if (mv.Validation.HasErrors)
+            {
+                var ex = mv.Validation.Errors.FirstOrDefault();
+                SharpGLTF.Validation.ModelException._Decorate(ex);
+                throw ex;
+            }
 
             return mv.Model;
         }
@@ -238,7 +260,7 @@ namespace SharpGLTF.Schema2
             catch (System.IO.EndOfStreamException ex)
             {
                 var vr = new Validation.ValidationResult(null, this.Validation);
-                vr.SetError(new Validation.SchemaException(null, ex.Message));
+                vr.SetSchemaError(ex);
                 return (null, vr);
             }
             catch (Validation.SchemaException ex)
@@ -272,13 +294,15 @@ namespace SharpGLTF.Schema2
             try {
             #endif
 
-            if (jsonUtf8Bytes.IsEmpty) throw new System.Text.Json.JsonException("JSon is empty.");
+            if (jsonUtf8Bytes.IsEmpty) throw new JsonException("JSon is empty.");
+
+            jsonUtf8Bytes = _Preprocess(jsonUtf8Bytes);
 
             var reader = new Utf8JsonReader(jsonUtf8Bytes.Span);
 
             if (!reader.Read())
             {
-                vcontext.SetError(new Validation.SchemaException(root, "Json is empty"));
+                vcontext.SetSchemaError(root, "Json is empty");
                 return (null, vcontext);
             }
 
@@ -324,17 +348,17 @@ namespace SharpGLTF.Schema2
             }
             catch (JsonException rex)
             {
-                vcontext.SetError(new Validation.SchemaException(root, rex));
+                vcontext.SetSchemaError(root, rex);
                 return (null, vcontext);
             }
             catch (System.FormatException fex)
             {
-                vcontext.SetError(new Validation.ModelException(null, fex));
+                vcontext.SetModelError(fex);
                 return (null, vcontext);
             }
             catch (ArgumentException aex)
             {
-                vcontext.SetError(new Validation.ModelException(root, aex));
+                vcontext.SetModelError(root, aex);
                 return (null, vcontext);
             }
             catch (Validation.ModelException mex)
@@ -347,6 +371,21 @@ namespace SharpGLTF.Schema2
             return (root, vcontext);
         }
 
+        private ReadOnlyMemory<Byte> _Preprocess(ReadOnlyMemory<Byte> jsonUtf8Bytes)
+        {
+            if (this.JsonPreprocessor == null) return jsonUtf8Bytes;
+
+            #if NETSTANDARD2_0
+            var text = Encoding.UTF8.GetString(jsonUtf8Bytes.ToArray());
+            #else
+            var text = Encoding.UTF8.GetString(jsonUtf8Bytes.Span);
+            #endif
+
+            text = this.JsonPreprocessor.Invoke(text);
+
+            return new ReadOnlyMemory<Byte>(Encoding.UTF8.GetBytes(text));
+        }
+
         #endregion
 
         #region extras

+ 28 - 8
src/SharpGLTF.Core/Schema2/Serialization.ReadSettings.cs

@@ -20,6 +20,13 @@ namespace SharpGLTF.Schema2
     /// </returns>
     public delegate Boolean ImageDecodeCallback(Image image);
 
+    /// <summary>
+    /// Callback used to preprocess and postprocess json before reading and after writing.
+    /// </summary>
+    /// <param name="json">The source json text.</param>
+    /// <returns>The processed json text.</returns>
+    public delegate string JsonFilterCallback(string json);
+
     /// <summary>
     /// Read settings and base class of <see cref="ReadContext"/>
     /// </summary>
@@ -57,6 +64,11 @@ namespace SharpGLTF.Schema2
         /// </summary>
         public ImageDecodeCallback ImageDecoder { get; set; }
 
+        /// <summary>
+        /// Gets or sets the callback used to preprocess the json text before parsing it.
+        /// </summary>
+        public JsonFilterCallback JsonPreprocessor { get; set; }
+
         #endregion
 
         #region API
@@ -66,6 +78,7 @@ namespace SharpGLTF.Schema2
             Guard.NotNull(other, nameof(other));
             other.Validation = this.Validation;
             other.ImageDecoder = this.ImageDecoder;
+            other.JsonPreprocessor = this.JsonPreprocessor;
         }
 
         #endregion
@@ -94,18 +107,21 @@ namespace SharpGLTF.Schema2
         /// <param name="filePath">A valid file path.</param>
         /// <param name="settings">Optional settings.</param>
         /// <returns>A <see cref="MODEL"/> instance.</returns>
+        /// <remarks>
+        /// <paramref name="settings"/> can be either a plain <see cref="ReadSettings"/> instance,
+        /// or a <see cref="ReadContext"/>, in which case, the context will be used to read the
+        /// files from it.
+        /// </remarks>
         public static MODEL Load(string filePath, ReadSettings settings = null)
         {
-            Guard.FilePathMustExist(filePath, nameof(filePath));
-
-            var context = ReadContext
-                .CreateFromFile(filePath)
-                .WithSettingsFrom(settings);
-
-            using (var s = File.OpenRead(filePath))
+            if (!(settings is ReadContext context))
             {
-                return context.ReadSchema2(s);
+                context = ReadContext
+                    .CreateFromFile(filePath)
+                    .WithSettingsFrom(settings);
             }
+
+            return context.ReadSchema2(filePath);
         }
 
         /// <summary>
@@ -116,6 +132,8 @@ namespace SharpGLTF.Schema2
         /// <returns>A <see cref="MODEL"/> instance.</returns>
         public static MODEL ParseGLB(BYTES glb, ReadSettings settings = null)
         {
+            System.Diagnostics.Debug.Assert(!(settings is ReadContext), "Use Load method.");
+
             Guard.NotNull(glb, nameof(glb));
 
             using (var m = new MemoryStream(glb.Array, glb.Offset, glb.Count, false))
@@ -132,6 +150,8 @@ namespace SharpGLTF.Schema2
         /// <returns>A <see cref="MODEL"/> instance.</returns>
         public static MODEL ReadGLB(Stream stream, ReadSettings settings = null)
         {
+            System.Diagnostics.Debug.Assert(!(settings is ReadContext), "Use Load method.");
+
             Guard.NotNull(stream, nameof(stream));
             Guard.IsTrue(stream.CanRead, nameof(stream));
 

+ 9 - 3
src/SharpGLTF.Core/Schema2/Serialization.WriteContext.cs

@@ -175,10 +175,16 @@ namespace SharpGLTF.Schema2
         }
 
         /// <summary>
-        /// Writes <paramref name="model"/> to this context.
+        /// Writes <paramref name="model"/> to this context using the glTF json container.
         /// </summary>
         /// <param name="baseName">The base name to use for asset files, without extension.</param>
         /// <param name="model">The <see cref="MODEL"/> to write.</param>
+        /// <remarks>
+        /// If the model has associated resources like binary assets and textures,<br/>
+        /// these additional resources will be also written as associated files using the pattern:<br/>
+        /// <br/>
+        /// "<paramref name="baseName"/>.{Number}.bin|png|jpg|dds"
+        /// </remarks>
         public void WriteTextSchema2(string baseName, MODEL model)
         {
             Guard.NotNullOrEmpty(baseName, nameof(baseName));
@@ -195,7 +201,7 @@ namespace SharpGLTF.Schema2
 
             using (var m = new MemoryStream())
             {
-                model._WriteJSON(m, this.JsonOptions);
+                model._WriteJSON(m, this.JsonOptions, this.JsonPostprocessor);
 
                 WriteAllBytesToEnd($"{baseName}.gltf", m.ToArraySegment());
             }
@@ -204,7 +210,7 @@ namespace SharpGLTF.Schema2
         }
 
         /// <summary>
-        /// Writes <paramref name="model"/> to this context.
+        /// Writes <paramref name="model"/> to this context using the GLB binary container.
         /// </summary>
         /// <param name="baseName">The base name to use for asset files, without extension.</param>
         /// <param name="model">The <see cref="MODEL"/> to write.</param>

+ 55 - 10
src/SharpGLTF.Core/Schema2/Serialization.WriteSettings.cs

@@ -116,6 +116,11 @@ namespace SharpGLTF.Schema2
         /// </summary>
         public VALIDATIONMODE Validation { get; set; } = VALIDATIONMODE.Strict;
 
+        /// <summary>
+        /// Gets or sets the callback used to postprocess the json text before parsing it.
+        /// </summary>
+        public JsonFilterCallback JsonPostprocessor { get; set; }
+
         #endregion
 
         #region API
@@ -130,6 +135,7 @@ namespace SharpGLTF.Schema2
             other.BuffersMaxSize = this.BuffersMaxSize;
             other._JsonOptions = this._JsonOptions;
             other.Validation = this.Validation;
+            other.JsonPostprocessor = this.JsonPostprocessor;
         }
 
         #endregion
@@ -196,26 +202,37 @@ namespace SharpGLTF.Schema2
             context.WriteTextSchema2(name, this);
         }
 
+        [Obsolete("Use GetJsonPreview", true)]
+        public string GetJSON(bool indented) { return GetJsonPreview(); }
+
+        /// <summary>
+        /// Gets the JSON document of this <see cref="MODEL"/>.
+        /// </summary>
+        /// <returns>A JSON content.</returns>
+        /// <remarks>
+        /// ⚠ Beware: this method serializes the current model into a json, without taking care of the binary buffers,
+        /// so the produced json might not be usable!
+        /// </remarks>
+        public string GetJsonPreview()
+        {
+            return _GetJSON(true);
+        }
+
         /// <summary>
         /// Gets the JSON document of this <see cref="MODEL"/>.
         /// </summary>
         /// <param name="indented">The formatting of the JSON document.</param>
         /// <returns>A JSON content.</returns>
-        public string GetJSON(bool indented)
+        internal string _GetJSON(bool indented)
         {
             var options = new System.Text.Json.JsonWriterOptions
             {
                 Indented = indented
             };
 
-            return GetJSON(options);
-        }
-
-        public string GetJSON(System.Text.Json.JsonWriterOptions options)
-        {
             using (var mm = new System.IO.MemoryStream())
             {
-                _WriteJSON(mm, options);
+                _WriteJSON(mm, options, null);
 
                 mm.Position = 0;
 
@@ -269,11 +286,39 @@ namespace SharpGLTF.Schema2
 
         #region core
 
-        internal void _WriteJSON(System.IO.Stream sw, System.Text.Json.JsonWriterOptions options)
+        internal void _WriteJSON(System.IO.Stream sw, System.Text.Json.JsonWriterOptions options, JsonFilterCallback filter)
         {
-            using (var writer = new System.Text.Json.Utf8JsonWriter(sw, options))
+            if (filter == null)
+            {
+                using (var writer = new System.Text.Json.Utf8JsonWriter(sw, options))
+                {
+                    this.Serialize(writer);
+                }
+
+                return;
+            }
+
+            string text = null;
+
+            using (var mm = new System.IO.MemoryStream())
+            {
+                _WriteJSON(mm, options, null);
+
+                mm.Position = 0;
+
+                using (var ss = new System.IO.StreamReader(mm))
+                {
+                    text = ss.ReadToEnd();
+                }
+            }
+
+            text = filter.Invoke(text);
+
+            var bytes = System.Text.Encoding.UTF8.GetBytes(text);
+
+            using (var mm = new System.IO.MemoryStream(bytes, false))
             {
-                this.Serialize(writer);
+                mm.CopyTo(sw);
             }
         }
 

+ 1 - 1
src/SharpGLTF.Core/Schema2/gltf.Root.cs

@@ -69,7 +69,7 @@ namespace SharpGLTF.Schema2
 
             var rcontext = ReadContext.CreateFromDictionary(dict, wcontext._UpdateSupportedExtensions);
             rcontext.Validation = Validation.ValidationMode.Skip;
-            var cloned = rcontext._ReadFromDictionary("deepclone.gltf");
+            var cloned = rcontext.ReadSchema2("deepclone.gltf");
 
             // Restore MemoryImage source URIs (they're not cloned as part of the serialization)
             foreach (var srcImg in this.LogicalImages)

+ 46 - 0
src/SharpGLTF.Core/Validation/ModelException.cs

@@ -48,6 +48,52 @@ namespace SharpGLTF.Validation
         private readonly TARGET _Target;
 
         #endregion
+
+        #region properties
+
+        internal string MessageSuffix { get; set; }
+
+        public override string Message
+        {
+            get
+            {
+                if (string.IsNullOrWhiteSpace(MessageSuffix)) return base.Message;
+                else return base.Message + MessageSuffix;
+            }
+        }
+
+        private string _Generator
+        {
+            get
+            {
+                Schema2.ModelRoot root = null;
+                if (_Target is Schema2.ModelRoot troot) root = troot;
+                if (_Target is Schema2.LogicalChildOfRoot tchild) root = tchild.LogicalParent;
+
+                return root?.Asset?.Generator ?? string.Empty;
+            }
+        }
+
+        #endregion
+
+        #region API
+
+        internal static void _Decorate(Exception ex)
+        {
+            if (!(ex is ModelException mex)) return;
+
+            var gen = mex._Generator;
+
+            if (gen.ToLower().Contains("sharpgltf"))
+            {
+                mex.MessageSuffix = $"Model generated by <{gen}> seems to be malformed.";
+                return;
+            }
+
+            mex.MessageSuffix = $"Model generated by <{gen}> seems to be malformed; Please, check the file at https://github.khronos.org/glTF-Validator/";
+        }
+
+        #endregion
     }
 
     /// <summary>

+ 25 - 0
src/SharpGLTF.Core/Validation/ValidationResult.cs

@@ -46,6 +46,31 @@ namespace SharpGLTF.Validation
 
         public ValidationContext GetContext() { return new ValidationContext(this); }
 
+        public void SetSchemaError(System.IO.EndOfStreamException ex)
+        {
+            SetError(new SchemaException(null, ex.Message));
+        }
+
+        public void SetSchemaError(Schema2.ModelRoot model, string error)
+        {
+            SetError(new SchemaException(model, error));
+        }
+
+        public void SetSchemaError(Schema2.ModelRoot model, System.Text.Json.JsonException ex)
+        {
+            SetError(new SchemaException(model, ex));
+        }
+
+        public void SetModelError(System.FormatException ex)
+        {
+            SetError(new ModelException(null, ex));
+        }
+
+        public void SetModelError(Schema2.ModelRoot model, System.ArgumentException ex)
+        {
+            SetError(new ModelException(model, ex));
+        }
+
         public void SetError(ModelException ex)
         {
             if (_InstantThrow) throw ex;

+ 6 - 16
src/SharpGLTF.Toolkit/IO/Zip.cs

@@ -14,11 +14,11 @@ namespace SharpGLTF.IO
     {
         #region static API
 
-        public static ModelRoot LoadGltf2(string zipPath, ReadSettings settings = null)
+        public static ModelRoot LoadModelFromZip(string zipPath, ReadSettings settings = null)
         {
             using (var zip = new ZipReader(zipPath))
             {
-                return zip.LoadGltf2(settings);
+                return zip.LoadModel(settings);
             }
         }
 
@@ -69,29 +69,19 @@ namespace SharpGLTF.IO
                 .OrderBy(item => item.FullName);
         }
 
-        public ModelRoot LoadGltf2(ReadSettings settings = null)
+        public ModelRoot LoadModel(ReadSettings settings = null)
         {
             var gltfFile = ModelFiles.First();
-            return this._LoadGltf2(gltfFile, settings);
+            return this.LoadModel(gltfFile, settings);
         }
 
-        private ModelRoot _LoadGltf2(string gltfFile, ReadSettings settings = null)
+        public ModelRoot LoadModel(string gltfFile, ReadSettings settings = null)
         {
             var context = ReadContext
                 .Create(_ReadAsset)
                 .WithSettingsFrom(settings);
 
-            using (var m = new System.IO.MemoryStream())
-            {
-                using (var s = _Archive.GetEntry(gltfFile).Open())
-                {
-                    s.CopyTo(m);
-                }
-
-                m.Position = 0;
-
-                return context.ReadSchema2(m);
-            }
+            return context.ReadSchema2(gltfFile);
         }
 
         private ArraySegment<Byte> _ReadAsset(string rawUri)

+ 2 - 3
tests/SharpGLTF.Core.Tests/Schema2/Authoring/BasicSceneCreationTests.cs

@@ -51,14 +51,13 @@ namespace SharpGLTF.Schema2.Authoring
             var extras = JsonContent.CreateFrom(dict);
 
             root.Extras = extras;
-
-            var json = root.GetJSON(true);
+            
             var bytes = root.WriteGLB();
             var rootBis = ModelRoot.ParseGLB(bytes);
 
             var a = root.Extras;
             var b = rootBis.Extras;
-            json = rootBis.Extras.ToJson();
+            var json = rootBis.Extras.ToJson();
             var c = IO.JsonContent.Parse(json);
 
             Assert.IsTrue(JsonContentTests.AreEqual(a,b));

+ 1 - 0
tests/SharpGLTF.Core.Tests/Schema2/LoadAndSave/LoadSampleTests.cs

@@ -171,6 +171,7 @@ namespace SharpGLTF.Schema2.LoadAndSave
         [TestCase(@"glTF-Quantized\AnimatedMorphCube.gltf")]
         [TestCase(@"glTF-Quantized\Duck.gltf")]
         [TestCase(@"glTF-Quantized\Lantern.gltf")]
+        [TestCase(@"MosquitoInAmber.glb")]
         public void LoadModelsWithExtensions(string filePath)
         {
             TestContext.CurrentContext.AttachShowDirLink();

+ 68 - 0
tests/SharpGLTF.Toolkit.Tests/IO/ZipTests.cs

@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using NUnit.Framework;
+
+using SharpGLTF.Scenes;
+using SharpGLTF.Geometry;
+using SharpGLTF.Geometry.Parametric;
+
+
+namespace SharpGLTF.IO
+{
+    internal class ZipTests
+    {
+        [Test]
+        public void ZipRoundtripTest()
+        {
+            // create a model
+            
+            var mesh = new MeshBuilder<Geometry.VertexTypes.VertexPositionNormal, Geometry.VertexTypes.VertexEmpty, Geometry.VertexTypes.VertexEmpty>("SphereMesh");
+            mesh.AddSphere(Materials.MaterialBuilder.CreateDefault(), 50, System.Numerics.Matrix4x4.Identity);
+
+            var scene = new SceneBuilder();
+            scene.AddRigidMesh(mesh, System.Numerics.Matrix4x4.Identity).WithName("Sphere");
+
+            Schema2.ModelRoot model = scene.ToGltf2();
+
+            Assert.AreEqual("SphereMesh", model.LogicalMeshes[0].Name);
+            Assert.AreEqual("Sphere", model.LogicalNodes[0].Name);            
+
+            model = _ZipRoundtrip(model);
+
+            Assert.AreEqual("SphereMesh", model.LogicalMeshes[0].Name);
+            Assert.AreEqual("Sphere", model.LogicalNodes[0].Name);
+        }
+
+
+        private static Schema2.ModelRoot _ZipRoundtrip(Schema2.ModelRoot model)
+        {
+            byte[] raw;
+
+            // write to zip into memory:
+
+            using (var memory = new System.IO.MemoryStream())
+            {
+                using (var zipWriter = new ZipWriter(memory))
+                {
+                    zipWriter.AddModel("model.gltf", model);
+                }
+
+                raw = memory.ToArray();
+            }
+
+            // read the model back:
+
+            using (var memory = new System.IO.MemoryStream(raw, false))
+            {
+                using (var zipReader = new ZipReader(memory))
+                {
+                    return zipReader.LoadModel("model.gltf");
+                }
+            }
+        }
+    }
+}