using System; using System.Collections.Generic; using System.IO; using System.Text; using MODEL = SharpGLTF.Schema2.ModelRoot; namespace SharpGLTF.Schema2 { static class _BinarySerialization { #region constants public const uint GLTFHEADER = 0x46546C67; public const uint GLTFVERSION2 = 2; public const uint CHUNKJSON = 0x4E4F534A; public const uint CHUNKBIN = 0x004E4942; #endregion #region read public static Memory ReadBytesToEnd(this Stream s) { using (var m = new MemoryStream()) { s.CopyTo(m); if (m.TryGetBuffer(out ArraySegment segment)) return segment; return m.ToArray(); } } 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) { Guard.NotNull(stream, nameof(stream)); Guard.IsTrue(stream.CanSeek, nameof(stream), "A seekable stream is required for glTF/GLB format identification"); var currPos = stream.Position; var a = stream.ReadByte(); var b = stream.ReadByte(); var c = stream.ReadByte(); var d = stream.ReadByte(); stream.Position = currPos; // restart read position return IsBinaryHeader((Byte)a, (Byte)b, (Byte)c, (Byte)d); } internal static bool IsBinaryHeader(ReadOnlySpan 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 ReadBinaryFile(Stream stream) { Guard.NotNull(stream, nameof(stream)); // WARNING: BinaryReader requires Encoding.ASCII because // the binaryReader.PeekChar() must read single bytes // in some cases, trying to read the end of the file will throw // an exception if encoding is UTF8 and there's just 1 byte left to read. using (var binaryReader = new BinaryReader(stream, Encoding.ASCII)) { // body length can actually be smaller than the stream length, // in which case, the data afterwards is considered "extra data". var remaining = _ReadBinaryHeader(binaryReader); void _checkCanRead(long count) { if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); if (count > int.MaxValue) throw new Validation.SchemaException(null, $"{count} bytes to read exceeds maximum capacity."); if (remaining < count) throw new Validation.SchemaException(null, "unexpected End of GLB block."); remaining -= count; } var chunks = new Dictionary(); while (remaining >= 4) { remaining -= 4; if (!binaryReader._TryReadUInt32(out var chunkLength)) break; if (chunkLength == 0) { throw new Validation.SchemaException(null, $"The chunk must non zero size."); } if ((chunkLength & 3) != 0) { throw new Validation.SchemaException(null, $"The chunk must be padded to 4 bytes: {chunkLength}"); } _checkCanRead(4); uint chunkId = binaryReader.ReadUInt32(); if (chunks.ContainsKey(chunkId)) { throw new Validation.SchemaException(null, $"Duplicated chunk found {chunkId}"); } _checkCanRead(chunkLength); var data = binaryReader.ReadBytes((int)chunkLength); chunks[chunkId] = data; } // finish reading remainder // if (remaining > 0) { binaryReader.ReadBytes((int)remaining); } if (!chunks.ContainsKey(CHUNKJSON)) throw new Validation.SchemaException(null, "JSON Chunk chunk not found"); // warnings // if (!chunks.ContainsKey(CHUNKBIN)) throw new Validation.SchemaException(null, "BIN Chunk chunk not found"); // if (remaining > 0) throw new Validation.SchemaException(null, "Extra bytes found"); return chunks; } } private static long _ReadBinaryHeader(BinaryReader binaryReader) { Guard.NotNull(binaryReader, nameof(binaryReader)); uint magic = binaryReader.ReadUInt32(); if (magic != GLTFHEADER) throw new Validation.SchemaException(null, $"Unexpected magic number: {magic}"); uint version = binaryReader.ReadUInt32(); if (version != GLTFVERSION2) throw new Validation.SchemaException(null, $"Unknown version number: {version}"); uint bodyLength = binaryReader.ReadUInt32(); // length of the actual glb body try // check stream is large enough { if (binaryReader.BaseStream.CanSeek) { var fileLength = (uint)binaryReader.BaseStream.Length; if (bodyLength > fileLength) { throw new Validation.SchemaException(null, $"The specified length of the file ({bodyLength}) is not equal to the actual length of the file ({fileLength})."); } } } catch (System.NotSupportedException) { // Some streams like Android assets don't support getting the length // https://github.com/vpenades/SharpGLTF/issues/178 } return bodyLength - 12; } #endregion #region write /// /// Tells if a given model can be stored as Binary format. /// /// the model to test /// null if it can be stored as binary, or an exception object if it can't /// /// Due to the limitations of Binary Format, not all models can be saved as Binary. /// public static Exception IsBinaryCompatible(MODEL model) { try { Guard.NotNull(model, nameof(model)); Guard.IsTrue(model.LogicalBuffers.Count <= 1, nameof(model), $"GLB format only supports one binary buffer, {model.LogicalBuffers.Count} found. It can be solved by calling {nameof(ModelRoot.MergeImages)} and {nameof(ModelRoot.MergeBuffers)}"); } catch (ArgumentException ex) { return ex; } // todo: buffer[0].Uri must be null return null; } /// /// Writes a instance into a . /// /// The destination stream. /// The source instance. public static void WriteBinaryModel(this BinaryWriter binaryWriter, MODEL model) { var ex = IsBinaryCompatible(model); if (ex != null) throw ex; var jsonText = model._GetJSON(false); var jsonChunk = Encoding.UTF8.GetBytes(jsonText); var jsonPadding = jsonChunk.Length & 3; if (jsonPadding != 0) jsonPadding = 4 - jsonPadding; var buffer = model.LogicalBuffers.Count > 0 ? model.LogicalBuffers[0].Content : null; if (buffer != null && buffer.Length == 0) buffer = null; var binPadding = buffer == null ? 0 : buffer.Length & 3; if (binPadding != 0) binPadding = 4 - binPadding; int fullLength = 4 + 4 + 4; fullLength += 8 + jsonChunk.Length + jsonPadding; if (buffer != null) fullLength += 8 + buffer.Length + binPadding; binaryWriter.Write(GLTFHEADER); binaryWriter.Write(GLTFVERSION2); binaryWriter.Write(fullLength); binaryWriter.Write(jsonChunk.Length + jsonPadding); binaryWriter.Write(CHUNKJSON); binaryWriter.Write(jsonChunk); for (int i = 0; i < jsonPadding; ++i) binaryWriter.Write((Byte)0x20); if (buffer != null) { binaryWriter.Write(buffer.Length + binPadding); binaryWriter.Write(CHUNKBIN); binaryWriter.Write(buffer); for (int i = 0; i < binPadding; ++i) binaryWriter.Write((Byte)0); } } #endregion } }