diff --git a/MatFileHandler.Tests/MatFileReaderTests.cs b/MatFileHandler.Tests/MatFileReaderTests.cs index 68bad4c..b424227 100755 --- a/MatFileHandler.Tests/MatFileReaderTests.cs +++ b/MatFileHandler.Tests/MatFileReaderTests.cs @@ -292,7 +292,38 @@ namespace MatFileHandler.Tests [Test] public void TestObject() { - Assert.That(() => GetTests("bad")["object"], Throws.TypeOf()); + var matFile = GetTests("good")["object"]; + var obj = matFile["object_"].Value as IMatObject; + Assert.IsNotNull(obj); + Assert.That(obj.ClassName, Is.EqualTo("Point")); + Assert.That(obj.FieldNames, Is.EquivalentTo(new[] { "x", "y" })); + Assert.That(obj["x", 0].ConvertToDoubleArray(), Is.EqualTo(new[] { 3.0 })); + Assert.That(obj["y", 0].ConvertToDoubleArray(), Is.EqualTo(new[] { 5.0 })); + Assert.That(obj["x", 1].ConvertToDoubleArray(), Is.EqualTo(new[] { -2.0 })); + Assert.That(obj["y", 1].ConvertToDoubleArray(), Is.EqualTo(new[] { 6.0 })); + } + + /// + /// Test reading another object. + /// + [Test] + public void TestObject2() + { + var matFile = GetTests("good")["object2"]; + var obj = matFile["object2"].Value as IMatObject; + Assert.IsNotNull(obj); + Assert.That(obj.ClassName, Is.EqualTo("Point")); + Assert.That(obj.FieldNames, Is.EquivalentTo(new[] { "x", "y" })); + Assert.That(obj["x", 0, 0].ConvertToDoubleArray(), Is.EqualTo(new[] { 3.0 })); + Assert.That(obj["y", 0, 0].ConvertToDoubleArray(), Is.EqualTo(new[] { 5.0 })); + Assert.That(obj["x", 1, 0].ConvertToDoubleArray(), Is.EqualTo(new[] { 1.0 })); + Assert.That(obj["y", 1, 0].ConvertToDoubleArray(), Is.EqualTo(new[] { 0.0 })); + Assert.That(obj["x", 0, 1].ConvertToDoubleArray(), Is.EqualTo(new[] { -2.0 })); + Assert.That(obj["y", 0, 1].ConvertToDoubleArray(), Is.EqualTo(new[] { 6.0 })); + Assert.That(obj["x", 1, 1].ConvertToDoubleArray(), Is.EqualTo(new[] { 0.0 })); + Assert.That(obj["y", 1, 1].ConvertToDoubleArray(), Is.EqualTo(new[] { 1.0 })); + Assert.That(obj[0, 1]["x"].ConvertToDoubleArray(), Is.EqualTo(new[] { -2.0 })); + Assert.That(obj[2]["x"].ConvertToDoubleArray(), Is.EqualTo(new[] { -2.0 })); } private static AbstractTestDataFactory GetTests(string factoryName) => diff --git a/MatFileHandler.Tests/test-data/bad/object.mat b/MatFileHandler.Tests/test-data/good/object.mat old mode 100755 new mode 100644 similarity index 100% rename from MatFileHandler.Tests/test-data/bad/object.mat rename to MatFileHandler.Tests/test-data/good/object.mat diff --git a/MatFileHandler.Tests/test-data/good/object2.mat b/MatFileHandler.Tests/test-data/good/object2.mat new file mode 100644 index 0000000..1b074ec Binary files /dev/null and b/MatFileHandler.Tests/test-data/good/object2.mat differ diff --git a/MatFileHandler/ArrayFlags.cs b/MatFileHandler/ArrayFlags.cs index c5700f7..4167e9d 100755 --- a/MatFileHandler/ArrayFlags.cs +++ b/MatFileHandler/ArrayFlags.cs @@ -85,9 +85,9 @@ namespace MatFileHandler MxUInt64 = 15, /// - /// Undocumented object (?) array type. + /// Undocumented opaque object type. /// - MxNewObject = 17, + MxOpaque = 17, } /// diff --git a/MatFileHandler/DataElementReader.cs b/MatFileHandler/DataElementReader.cs index 3c1021e..709462d 100755 --- a/MatFileHandler/DataElementReader.cs +++ b/MatFileHandler/DataElementReader.cs @@ -12,14 +12,25 @@ namespace MatFileHandler /// /// Functions for reading data elements from a .mat file. /// - internal static class DataElementReader + internal class DataElementReader { + private readonly SubsystemData subsystemData; + + /// + /// Initializes a new instance of the class. + /// + /// Reference to file's SubsystemData. + public DataElementReader(SubsystemData subsystemData) + { + this.subsystemData = subsystemData ?? throw new ArgumentNullException(nameof(subsystemData)); + } + /// /// Read a data element. /// /// Input reader. /// Data element. - public static DataElement Read(BinaryReader reader) + public DataElement Read(BinaryReader reader) { var (dataReader, tag) = ReadTag(reader); DataElement result; @@ -66,6 +77,7 @@ namespace MatFileHandler default: throw new NotSupportedException("Unknown element."); } + if (tag.Type != DataType.MiCompressed) { var position = reader.BaseStream.Position; @@ -74,9 +86,109 @@ namespace MatFileHandler reader.ReadBytes(8 - (int)(position % 8)); } } + return result; } + private static (int[] dimensions, int[] links, int classIndex) ParseOpaqueData(MatNumericalArrayOf data) + { + var nDims = data.Data[1]; + var dimensions = new int[nDims]; + var position = 2; + for (var i = 0; i < nDims; i++) + { + dimensions[i] = (int)data.Data[position]; + position++; + } + + var count = dimensions.NumberOfElements(); + var links = new int[count]; + for (var i = 0; i < count; i++) + { + links[i] = (int)data.Data[position]; + position++; + } + + var classIndex = (int)data.Data[position]; + + return (dimensions, links, classIndex); + } + + private static ArrayFlags ReadArrayFlags(DataElement element) + { + var flagData = (element as MiNum)?.Data ?? + throw new HandlerException("Unexpected type in array flags."); + var class_ = (ArrayType)(flagData[0] & 0xff); + var variableFlags = (flagData[0] >> 8) & 0x0e; + return new ArrayFlags + { + Class = class_, + Variable = (Variable)variableFlags, + }; + } + + private static DataElement ReadData(DataElement element) + { + return element; + } + + private static int[] ReadDimensionsArray(MiNum element) + { + return element.Data; + } + + private static string[] ReadFieldNames(MiNum element, int fieldNameLength) + { + var numberOfFields = element.Data.Length / fieldNameLength; + var result = new string[numberOfFields]; + for (var i = 0; i < numberOfFields; i++) + { + var list = new List(); + var position = i * fieldNameLength; + while (element.Data[position] != 0) + { + list.Add((byte)element.Data[position]); + position++; + } + + result[i] = Encoding.ASCII.GetString(list.ToArray()); + } + + return result; + } + + private static string ReadName(MiNum element) + { + return Encoding.ASCII.GetString(element.Data.Select(x => (byte)x).ToArray()); + } + + private static DataElement ReadNum(Tag tag, BinaryReader reader) + where T : struct + { + var bytes = reader.ReadBytes(tag.Length); + if (tag.Type == DataType.MiUInt8) + { + return new MiNum(bytes); + } + + var result = new T[bytes.Length / tag.ElementSize]; + Buffer.BlockCopy(bytes, 0, result, 0, bytes.Length); + return new MiNum(result); + } + + private static SparseArrayFlags ReadSparseArrayFlags(DataElement element) + { + var arrayFlags = ReadArrayFlags(element); + var flagData = (element as MiNum)?.Data ?? + throw new HandlerException("Unexpected type in sparse array flags."); + var nzMax = flagData[1]; + return new SparseArrayFlags + { + ArrayFlags = arrayFlags, + NzMax = nzMax, + }; + } + private static (BinaryReader, Tag) ReadTag(BinaryReader reader) { var type = reader.ReadInt32(); @@ -95,87 +207,66 @@ namespace MatFileHandler } } - private static ArrayFlags ReadArrayFlags(DataElement element) + private DataElement ContinueReadingCellArray( + BinaryReader reader, + ArrayFlags flags, + int[] dimensions, + string name) { - var flagData = (element as MiNum)?.Data ?? - throw new HandlerException("Unexpected type in array flags."); - var class_ = (ArrayType)(flagData[0] & 0xff); - var variableFlags = (flagData[0] >> 8) & 0x0e; - return new ArrayFlags + var numberOfElements = dimensions.NumberOfElements(); + var elements = new List(); + for (var i = 0; i < numberOfElements; i++) { - Class = class_, - Variable = (Variable)variableFlags, - }; - } - - private static SparseArrayFlags ReadSparseArrayFlags(DataElement element) - { - var arrayFlags = ReadArrayFlags(element); - var flagData = (element as MiNum)?.Data ?? - throw new HandlerException("Unexpected type in sparse array flags."); - var nzMax = flagData[1]; - return new SparseArrayFlags - { - ArrayFlags = arrayFlags, - NzMax = nzMax, - }; - } - - private static int[] ReadDimensionsArray(MiNum element) - { - return element.Data; - } - - private static DataElement ReadData(DataElement element) - { - return element; - } - - private static string ReadName(MiNum element) - { - return Encoding.ASCII.GetString(element.Data.Select(x => (byte)x).ToArray()); - } - - private static DataElement ReadNum(Tag tag, BinaryReader reader) - where T : struct - { - var bytes = reader.ReadBytes(tag.Length); - if (tag.Type == DataType.MiUInt8) - { - return new MiNum(bytes); + var element = Read(reader) as IArray; + elements.Add(element); } - var result = new T[bytes.Length / tag.ElementSize]; - Buffer.BlockCopy(bytes, 0, result, 0, bytes.Length); - return new MiNum(result); + + return new MatCellArray(flags, dimensions, name, elements); } - private static string[] ReadFieldNames(MiNum element, int fieldNameLength) + private DataElement ContinueReadingOpaque(BinaryReader reader) { - var numberOfFields = element.Data.Length / fieldNameLength; - var result = new string[numberOfFields]; - for (var i = 0; i < numberOfFields; i++) + var nameElement = Read(reader) as MiNum ?? + throw new HandlerException("Unexpected type in object name."); + var name = ReadName(nameElement); + var anotherElement = Read(reader) as MiNum ?? + throw new HandlerException("Unexpected type in object type description."); + var typeDescription = ReadName(anotherElement); + var classNameElement = Read(reader) as MiNum ?? + throw new HandlerException("Unexpected type in class name."); + var className = ReadName(classNameElement); + var dataElement = Read(reader); + var data = ReadData(dataElement); + if (data is MatNumericalArrayOf linkElement) { - var list = new List(); - var position = i * fieldNameLength; - while (element.Data[position] != 0) - { - list.Add((byte)element.Data[position]); - position++; - } - result[i] = Encoding.ASCII.GetString(list.ToArray()); + var (dimensions, links, classIndex) = ParseOpaqueData(linkElement); + return new OpaqueLink( + name, + typeDescription, + className, + dimensions, + data, + links, + classIndex, + subsystemData); + } + else + { + return new Opaque(name, typeDescription, className, new int[] { }, data); } - return result; } - private static DataElement ContinueReadingSparseArray( + private DataElement ContinueReadingSparseArray( BinaryReader reader, DataElement firstElement, int[] dimensions, string name) { var sparseArrayFlags = ReadSparseArrayFlags(firstElement); - var rowIndex = Read(reader) as MiNum ?? throw new HandlerException("Unexpected type in row indices of a sparse array."); - var columnIndex = Read(reader) as MiNum ?? throw new HandlerException("Unexpected type in column indices of a sparse array."); + var rowIndex = Read(reader) as MiNum ?? + throw new HandlerException("Unexpected type in row indices of a sparse array."); + var columnIndex = Read(reader) as MiNum ?? + throw new HandlerException("Unexpected type in column indices of a sparse array."); var data = Read(reader); if (sparseArrayFlags.ArrayFlags.Variable.HasFlag(Variable.IsLogical)) { @@ -187,6 +278,7 @@ namespace MatFileHandler columnIndex.Data, data); } + if (sparseArrayFlags.ArrayFlags.Variable.HasFlag(Variable.IsComplex)) { var imaginaryData = Read(reader); @@ -199,6 +291,7 @@ namespace MatFileHandler data, imaginaryData); } + switch (data) { case MiNum _: @@ -214,23 +307,7 @@ namespace MatFileHandler } } - private static DataElement ContinueReadingCellArray( - BinaryReader reader, - ArrayFlags flags, - int[] dimensions, - string name) - { - var numberOfElements = dimensions.NumberOfElements(); - var elements = new List(); - for (var i = 0; i < numberOfElements; i++) - { - var element = Read(reader) as IArray; - elements.Add(element); - } - return new MatCellArray(flags, dimensions, name, elements); - } - - private static DataElement ContinueReadingStructure( + private DataElement ContinueReadingStructure( BinaryReader reader, ArrayFlags flags, int[] dimensions, @@ -244,6 +321,7 @@ namespace MatFileHandler { fields[fieldName] = new List(); } + var numberOfElements = dimensions.NumberOfElements(); for (var i = 0; i < numberOfElements; i++) { @@ -253,27 +331,53 @@ namespace MatFileHandler fields[fieldName].Add(field); } } + return new MatStructureArray(flags, dimensions, name, fields); } - private static DataElement ContinueReadingNewObject() + private DataElement Read(Stream stream) { - throw new HandlerException("Cannot read objects."); + using (var reader = new BinaryReader(stream)) + { + return Read(reader); + } } - private static DataElement ReadMatrix(Tag tag, BinaryReader reader) + private DataElement ReadCompressed(Tag tag, BinaryReader reader) + { + reader.ReadBytes(2); + var compressedData = new byte[tag.Length - 6]; + reader.BaseStream.Read(compressedData, 0, tag.Length - 6); + reader.ReadBytes(4); + var resultStream = new MemoryStream(); + using (var compressedStream = new MemoryStream(compressedData)) + { + using (var stream = new DeflateStream(compressedStream, CompressionMode.Decompress, leaveOpen: true)) + { + stream.CopyTo(resultStream); + } + } + + resultStream.Position = 0; + return Read(resultStream); + } + + private DataElement ReadMatrix(Tag tag, BinaryReader reader) { if (tag.Length == 0) { return MatArray.Empty(); } + var element1 = Read(reader); var flags = ReadArrayFlags(element1); - if (flags.Class == ArrayType.MxNewObject) + if (flags.Class == ArrayType.MxOpaque) { - return ContinueReadingNewObject(); + return ContinueReadingOpaque(reader); } - var element2 = Read(reader) as MiNum ?? throw new HandlerException("Unexpected type in array dimensions data."); + + var element2 = Read(reader) as MiNum ?? + throw new HandlerException("Unexpected type in array dimensions data."); var dimensions = ReadDimensionsArray(element2); var element3 = Read(reader) as MiNum ?? throw new HandlerException("Unexpected type in array name."); var name = ReadName(element3); @@ -281,10 +385,12 @@ namespace MatFileHandler { return ContinueReadingCellArray(reader, flags, dimensions, name); } + if (flags.Class == ArrayType.MxSparse) { return ContinueReadingSparseArray(reader, element1, dimensions, name); } + var element4 = Read(reader); var data = ReadData(element4); DataElement imaginaryData = null; @@ -293,12 +399,15 @@ namespace MatFileHandler var element5 = Read(reader); imaginaryData = ReadData(element5); } + if (flags.Class == ArrayType.MxStruct) { var fieldNameLengthElement = data as MiNum ?? - throw new HandlerException("Unexpected type in structure field name length."); + throw new HandlerException( + "Unexpected type in structure field name length."); return ContinueReadingStructure(reader, flags, dimensions, name, fieldNameLengthElement.Data[0]); } + switch (flags.Class) { case ArrayType.MxChar: @@ -339,6 +448,7 @@ namespace MatFileHandler data, imaginaryData); } + return DataElementConverter.ConvertToMatNumericalArrayOf( flags, dimensions, @@ -405,31 +515,5 @@ namespace MatFileHandler throw new HandlerException("Unknown data type."); } } - - private static DataElement ReadCompressed(Tag tag, BinaryReader reader) - { - reader.ReadBytes(2); - var compressedData = new byte[tag.Length - 6]; - reader.BaseStream.Read(compressedData, 0, tag.Length - 6); - reader.ReadBytes(4); - var resultStream = new MemoryStream(); - using (var compressedStream = new MemoryStream(compressedData)) - { - using (var stream = new DeflateStream(compressedStream, CompressionMode.Decompress, leaveOpen: true)) - { - stream.CopyTo(resultStream); - } - } - resultStream.Position = 0; - return Read(resultStream); - } - - private static DataElement Read(Stream stream) - { - using (var reader = new BinaryReader(stream)) - { - return Read(reader); - } - } } } \ No newline at end of file diff --git a/MatFileHandler/Header.cs b/MatFileHandler/Header.cs index 56034d4..06b68ce 100755 --- a/MatFileHandler/Header.cs +++ b/MatFileHandler/Header.cs @@ -13,7 +13,7 @@ namespace MatFileHandler /// internal class Header { - private Header(string text, byte[] subsystemDataOffset, int version) + private Header(string text, long subsystemDataOffset, int version) { Text = text; SubsystemDataOffset = subsystemDataOffset; @@ -28,7 +28,7 @@ namespace MatFileHandler /// /// Gets subsystem data offset. /// - public byte[] SubsystemDataOffset { get; } + public long SubsystemDataOffset { get; } /// /// Gets file version. @@ -55,7 +55,7 @@ namespace MatFileHandler platform = platform.Remove(length); } var text = $"MATLAB 5.0 MAT-file, Platform: {platform}, Created on: {dateTime}{padding}"; - return new Header(text, subsystemDataOffset, 256); + return new Header(text, 0, 256); } /// @@ -67,7 +67,8 @@ namespace MatFileHandler { var textBytes = reader.ReadBytes(116); var text = System.Text.Encoding.UTF8.GetString(textBytes); - var subsystemDataOffset = reader.ReadBytes(8); + var subsystemDataOffsetBytes = reader.ReadBytes(8); + var subsystemDataOffset = BitConverter.ToInt64(subsystemDataOffsetBytes, 0); var version = reader.ReadInt16(); var endian = reader.ReadInt16(); var isLittleEndian = endian == 19785; diff --git a/MatFileHandler/IMatObject.cs b/MatFileHandler/IMatObject.cs new file mode 100644 index 0000000..faab81e --- /dev/null +++ b/MatFileHandler/IMatObject.cs @@ -0,0 +1,34 @@ +// Copyright 2017-2018 Alexander Luzgarev + +using System.Collections.Generic; + +namespace MatFileHandler +{ + /// + /// An interface to access Matlab objects (more precisely, "object arrays"). + /// This is very similar to the interface: + /// an object holds fields that you can access, and the name of its class. + /// Additionally, you can treat is as an array of dictionaries mapping + /// field names to contents of fields. + /// + public interface IMatObject : IArrayOf> + { + /// + /// Gets the name of object's class. + /// + string ClassName { get; } + + /// + /// Gets the names of object's fields. + /// + IEnumerable FieldNames { get; } + + /// + /// Access a given field of a given object in the array. + /// + /// Field name. + /// Index of the object to access. + /// The value of the field in the selected object. + IArray this[string field, params int[] list] { get; set; } + } +} diff --git a/MatFileHandler/MatFileReader.cs b/MatFileHandler/MatFileReader.cs index 7fcc8a2..5bcb135 100755 --- a/MatFileHandler/MatFileReader.cs +++ b/MatFileHandler/MatFileReader.cs @@ -1,5 +1,6 @@ // Copyright 2017-2018 Alexander Luzgarev +using System; using System.Collections.Generic; using System.IO; @@ -33,35 +34,72 @@ namespace MatFileHandler } } - private static void ReadHeader(BinaryReader reader) + /// + /// Read raw variables from a .mat file. + /// + /// Binary reader. + /// Offset to the subsystem data to use (read from the file header). + /// Raw variables read. + internal static List ReadRawVariables(BinaryReader reader, long subsystemDataOffset) { - Header.Read(reader); - } - - private static IMatFile Read(BinaryReader reader) - { - ReadHeader(reader); - var variables = new List(); + var variables = new List(); + var subsystemData = new SubsystemData(); + var dataElementReader = new DataElementReader(subsystemData); while (true) { try { - var dataElement = DataElementReader.Read(reader) as MatArray; - if (dataElement == null) + var position = reader.BaseStream.Position; + var dataElement = dataElementReader.Read(reader); + if (position == subsystemDataOffset) { - continue; + var subsystemDataElement = dataElement as IArrayOf; + subsystemData.Set(ReadSubsystemData(subsystemDataElement.Data)); + } + else + { + variables.Add(new RawVariable(position, dataElement)); } - variables.Add(new MatVariable( - dataElement, - dataElement.Name, - dataElement.Flags.Variable.HasFlag(Variable.IsGlobal))); } catch (EndOfStreamException) { break; } } + + return variables; + } + + private static IMatFile Read(BinaryReader reader) + { + var header = ReadHeader(reader); + var rawVariables = ReadRawVariables(reader, header.SubsystemDataOffset); + var variables = new List(); + foreach (var variable in rawVariables) + { + var array = variable.DataElement as MatArray; + if (array is null) + { + continue; + } + + variables.Add(new MatVariable( + array, + array.Name, + array.Flags.Variable.HasFlag(Variable.IsGlobal))); + } + return new MatFile(variables); } + + private static Header ReadHeader(BinaryReader reader) + { + return Header.Read(reader); + } + + private static SubsystemData ReadSubsystemData(byte[] bytes) + { + return SubsystemDataReader.Read(bytes); + } } } \ No newline at end of file diff --git a/MatFileHandler/Opaque.cs b/MatFileHandler/Opaque.cs new file mode 100644 index 0000000..92859ad --- /dev/null +++ b/MatFileHandler/Opaque.cs @@ -0,0 +1,58 @@ +// Copyright 2017-2018 Alexander Luzgarev + +using System; +using System.Numerics; + +namespace MatFileHandler +{ + /// + /// Matlab "opaque object" structure. + /// If this object appears in the "main" section of the .mat file, + /// it just contains a small data structure pointing to the object's + /// storage in the "subsystem data" portion of the file. + /// In this case, an instance of class + /// will be created. + /// If this object appears in the "subsystem data" part, it contains + /// the data of all opaque objects in the file, and that is what we + /// put into property. + /// + internal class Opaque : MatArray, IArray + { + /// + /// Initializes a new instance of the class. + /// + /// Name of the object. + /// Type description. + /// Class name. + /// Dimensions of the object. + /// Raw object's data. + public Opaque(string name, string typeDescription, string className, int[] dimensions, DataElement rawData) + : base(new ArrayFlags(ArrayType.MxOpaque, 0), dimensions, name) + { + TypeDescription = typeDescription ?? throw new ArgumentNullException(nameof(typeDescription)); + ClassName = className ?? throw new ArgumentNullException(nameof(className)); + RawData = rawData ?? throw new ArgumentNullException(nameof(rawData)); + } + + /// + /// Gets class name of the opaque object. + /// + public string ClassName { get; } + + /// + /// Gets raw object's data: either links to subsystem data, or actual data. + /// + public DataElement RawData { get; } + + /// + /// Gets "type description" of the opaque object. + /// + public string TypeDescription { get; } + + /// + public override Complex[] ConvertToComplexArray() => null; + + /// + public override double[] ConvertToDoubleArray() => null; + } +} \ No newline at end of file diff --git a/MatFileHandler/OpaqueLink.cs b/MatFileHandler/OpaqueLink.cs new file mode 100644 index 0000000..54779b4 --- /dev/null +++ b/MatFileHandler/OpaqueLink.cs @@ -0,0 +1,169 @@ +// Copyright 2017-2018 Alexander Luzgarev + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace MatFileHandler +{ + /// + /// Implementation of Matlab's "opaque objects" via links to subsystem data. + /// + internal class OpaqueLink : Opaque, IMatObject + { + private readonly SubsystemData subsystemData; + + /// + /// Initializes a new instance of the class. + /// + /// Name of the object. + /// Description of object's class. + /// Name of the object's class. + /// Dimensions of the object. + /// Raw data containing links to object's storage. + /// Links to object's storage. + /// Index of object's class. + /// Reference to global subsystem data. + public OpaqueLink( + string name, + string typeDescription, + string className, + int[] dimensions, + DataElement data, + int[] links, + int classIndex, + SubsystemData subsystemData) + : base(name, typeDescription, className, dimensions, data) + { + Links = links ?? throw new ArgumentNullException(nameof(links)); + ClassIndex = classIndex; + this.subsystemData = subsystemData ?? throw new ArgumentNullException(nameof(subsystemData)); + } + + /// + /// Gets index of this object's class in subsystem data class list. + /// + public int ClassIndex { get; } + + /// + public IReadOnlyDictionary[] Data => null; + + /// + public IEnumerable FieldNames => FieldNamesArray; + + /// + /// Gets links to the fields stored in subsystem data. + /// + public int[] Links { get; } + + private string[] FieldNamesArray => subsystemData.ClassInformation[ClassIndex - 1].FieldNames.ToArray(); + + /// + public IArray this[string field, params int[] list] + { + get + { + if (TryGetValue(field, out var result, list)) + { + return result; + } + + throw new IndexOutOfRangeException(); + } + set => throw new NotImplementedException(); + } + + /// + public IReadOnlyDictionary this[params int[] list] + { + get => ExtractObject(Dimensions.DimFlatten(list)); + set => throw new NotImplementedException(); + } + + private IReadOnlyDictionary ExtractObject(int i) + { + return new OpaqueObjectArrayElement(this, i); + } + + private bool TryGetValue(string field, out IArray output, params int[] list) + { + var index = Dimensions.DimFlatten(list); + var maybeFieldIndex = subsystemData.ClassInformation[ClassIndex - 1].FindField(field); + if (!(maybeFieldIndex is int fieldIndex)) + { + output = default(IArray); + return false; + } + + if (index >= subsystemData.ObjectInformation.Length) + { + output = default(IArray); + return false; + } + + var dataIndex = subsystemData.ObjectInformation[index].FieldLinks[fieldIndex + 1]; + output = subsystemData.Data[dataIndex]; + return true; + } + + /// + /// Provides access to a single object in object array. + /// + internal class OpaqueObjectArrayElement : IReadOnlyDictionary + { + private readonly int index; + private readonly OpaqueLink parent; + + /// + /// Initializes a new instance of the class. + /// + /// Parent object array. + /// Index of the object in the array. + public OpaqueObjectArrayElement(OpaqueLink parent, int index) + { + this.parent = parent ?? throw new ArgumentNullException(nameof(parent)); + this.index = index; + } + + /// + public int Count => parent.FieldNamesArray.Length; + + /// + public IEnumerable Keys => parent.FieldNames; + + /// + public IEnumerable Values => parent.FieldNames.Select(fieldName => parent[fieldName, index]); + + /// + public IArray this[string key] => parent[key, index]; + + /// + public bool ContainsKey(string key) + { + return parent.FieldNames.Contains(key); + } + + /// + public IEnumerator> GetEnumerator() + { + foreach (var field in parent.FieldNamesArray) + { + yield return new KeyValuePair(field, parent[field, index]); + } + } + + /// + public bool TryGetValue(string key, out IArray value) + { + return parent.TryGetValue(key, out value, index); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + } +} \ No newline at end of file diff --git a/MatFileHandler/RawVariable.cs b/MatFileHandler/RawVariable.cs new file mode 100644 index 0000000..c384b21 --- /dev/null +++ b/MatFileHandler/RawVariable.cs @@ -0,0 +1,36 @@ +// Copyright 2017-2018 Alexander Luzgarev + +using System; + +namespace MatFileHandler +{ + /// + /// Raw variable read from the file. + /// This gives a way to deal with "subsystem data" which looks like + /// a variable and can only be detected by comparing its offset with + /// the value stored in the file's header. + /// + internal class RawVariable + { + /// + /// Initializes a new instance of the class. + /// + /// Offset of the variable in the source file. + /// Data element parsed from the file. + internal RawVariable(long offset, DataElement dataElement) + { + Offset = offset; + DataElement = dataElement ?? throw new ArgumentNullException(nameof(dataElement)); + } + + /// + /// Gets data element with the variable's contents. + /// + public DataElement DataElement { get; } + + /// + /// Gets offset of the variable in the .mat file. + /// + public long Offset { get; } + } +} \ No newline at end of file diff --git a/MatFileHandler/SubsystemData.cs b/MatFileHandler/SubsystemData.cs new file mode 100644 index 0000000..acd0338 --- /dev/null +++ b/MatFileHandler/SubsystemData.cs @@ -0,0 +1,144 @@ +// Copyright 2017-2018 Alexander Luzgarev + +using System; +using System.Collections.Generic; + +namespace MatFileHandler +{ + /// + /// "Subsystem data" of the .mat file. + /// Subsystem data stores the actual contents + /// of all the "opaque objects" in the file. + /// + internal class SubsystemData + { + /// + /// Initializes a new instance of the class. + /// Default constructor: initializes everything to null. + /// + public SubsystemData() + { + ClassInformation = null; + ObjectInformation = null; + Data = null; + } + + /// + /// Initializes a new instance of the class. + /// The actual constructor. + /// + /// Information about the classes. + /// Information about the objects. + /// Field values. + public SubsystemData(ClassInfo[] classInformation, ObjectInfo[] objectInformation, Dictionary data) + { + this.ClassInformation = + classInformation ?? throw new ArgumentNullException(nameof(classInformation)); + this.ObjectInformation = + objectInformation ?? throw new ArgumentNullException(nameof(objectInformation)); + this.Data = data ?? throw new ArgumentNullException(nameof(data)); + } + + /// + /// Gets or sets information about all the classes occurring in the file. + /// + public ClassInfo[] ClassInformation { get; set; } + + /// + /// Gets or sets the actual data: mapping of "object field" indices to their values. + /// + public IReadOnlyDictionary Data { get; set; } + + /// + /// Gets or sets information about all the objects occurring in the file. + /// + public ObjectInfo[] ObjectInformation { get; set; } + + /// + /// Initialize this object from another object. + /// This ugly hack allows us to read the opaque objects and store references to + /// the subsystem data in them before parsing the actual subsystem data (which + /// comes later in the file). + /// + /// Another subsystem data. + public void Set(SubsystemData data) + { + this.ClassInformation = data.ClassInformation; + this.ObjectInformation = data.ObjectInformation; + this.Data = data.Data; + } + + /// + /// Stores information about a class. + /// + internal class ClassInfo + { + private readonly string[] fieldNames; + + private readonly Dictionary fieldToIndex; + + /// + /// Initializes a new instance of the class. + /// + /// Class name. + /// Names of the fields. + public ClassInfo(string name, string[] fieldNames) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + this.fieldNames = fieldNames ?? throw new ArgumentNullException(nameof(fieldNames)); + fieldToIndex = new Dictionary(); + for (var i = 0; i < fieldNames.Length; i++) + { + fieldToIndex[fieldNames[i]] = i; + } + } + + /// + /// Gets names of the fields in the class. + /// + public IReadOnlyCollection FieldNames => fieldNames; + + /// + /// Gets name of the class. + /// + public string Name { get; } + + /// + /// Find a field index given its name. + /// + /// Field name. + /// Field index. + public int? FindField(string fieldName) + { + if (fieldToIndex.TryGetValue(fieldName, out var index)) + { + return index; + } + + return null; + } + } + + /// + /// Stores information about an object. + /// + internal class ObjectInfo + { + private readonly Dictionary fieldLinks; + + /// + /// Initializes a new instance of the class. + /// + /// A dictionary mapping the field indices to "field values" indices. + public ObjectInfo(Dictionary fieldLinks) + { + this.fieldLinks = fieldLinks ?? throw new ArgumentNullException(nameof(fieldLinks)); + } + + /// + /// Gets mapping between the field indices and "field values" indices. + /// + public IReadOnlyDictionary FieldLinks => fieldLinks; + } + } +} \ No newline at end of file diff --git a/MatFileHandler/SubsystemDataReader.cs b/MatFileHandler/SubsystemDataReader.cs new file mode 100644 index 0000000..d6b2d35 --- /dev/null +++ b/MatFileHandler/SubsystemDataReader.cs @@ -0,0 +1,168 @@ +// Copyright 2017-2018 Alexander Luzgarev + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace MatFileHandler +{ + /// + /// Reader for "subsystem data" in .mat files. + /// + internal class SubsystemDataReader + { + /// + /// Read subsystem data from a given byte array. + /// + /// Byte array with the data. + /// Subsystem data read. + public static SubsystemData Read(byte[] bytes) + { + List rawVariables = null; + using (var stream = new MemoryStream(bytes)) + { + using (var reader = new BinaryReader(stream)) + { + reader.ReadBytes(8); + rawVariables = MatFileReader.ReadRawVariables(reader, -1); + } + } + + // Parse subsystem data. + var mainVariable = rawVariables[0].DataElement as IStructureArray; + var mcosData = mainVariable["MCOS", 0] as Opaque; + var opaqueData = mcosData.RawData as ICellArray; + var info = (opaqueData[0] as IArrayOf).Data; + var (offsets, position) = ReadOffsets(info, 0); + var fieldNames = ReadFieldNames(info, position, offsets[1]); + var numberOfClasses = ((offsets[3] - offsets[2]) / 16) - 1; + SubsystemData.ClassInfo[] classInformation = null; + using (var stream = new MemoryStream(info, offsets[2], offsets[3] - offsets[2])) + { + using (var reader = new BinaryReader(stream)) + { + classInformation = ReadClassInformation(reader, fieldNames, numberOfClasses); + } + } + var numberOfObjects = ((offsets[5] - offsets[4]) / 24) - 1; + SubsystemData.ObjectInfo[] objectInformation = null; + using (var stream = new MemoryStream(info, offsets[5], offsets[6] - offsets[5])) + { + using (var reader = new BinaryReader(stream)) + { + objectInformation = ReadObjectInformation(reader, numberOfObjects); + } + } + + var allFields = objectInformation.SelectMany(obj => obj.FieldLinks.Values); + var data = new Dictionary(); + foreach (var i in allFields) + { + data[i] = opaqueData[i + 2]; + } + return new SubsystemData(classInformation, objectInformation, data); + } + + private static SubsystemData.ObjectInfo ReadObjectInformation(BinaryReader reader) + { + var length = reader.ReadInt32(); + var fieldLinks = new Dictionary(); + for (var i = 0; i < length; i++) + { + var x = reader.ReadInt32(); + var y = reader.ReadInt32(); + var index = x * y; + var link = reader.ReadInt32(); + fieldLinks[index] = link; + } + return new SubsystemData.ObjectInfo(fieldLinks); + } + + private static SubsystemData.ObjectInfo[] ReadObjectInformation(BinaryReader reader, int numberOfObjects) + { + var result = new SubsystemData.ObjectInfo[numberOfObjects]; + reader.ReadBytes(8); + for (var objectIndex = 0; objectIndex < numberOfObjects; objectIndex++) + { + result[objectIndex] = ReadObjectInformation(reader); + var position = reader.BaseStream.Position; + if (position % 8 != 0) + { + reader.ReadBytes(8 - (int)(position % 8)); + } + } + return result; + } + + private static SubsystemData.ClassInfo[] ReadClassInformation( + BinaryReader reader, + string[] fieldNames, + int numberOfClasses) + { + var result = new SubsystemData.ClassInfo[numberOfClasses]; + var indices = new int[numberOfClasses + 1]; + for (var i = 0; i <= numberOfClasses; i++) + { + reader.ReadInt32(); + indices[i] = reader.ReadInt32(); + reader.ReadInt32(); + reader.ReadInt32(); + } + + for (var i = 0; i < numberOfClasses; i++) + { + var numberOfFields = indices[i + 1] - indices[i] - 1; + var names = new string[numberOfFields]; + Array.Copy(fieldNames, indices[i], names, 0, numberOfFields); + var className = fieldNames[indices[i + 1] - 1]; + result[i] = new SubsystemData.ClassInfo(className, names); + } + + return result; + } + + private static (int[] offsets, int newPosition) ReadOffsets(byte[] bytes, int startPosition) + { + var position = startPosition; + var offsets = new List(); + while (true) + { + var next = BitConverter.ToInt32(bytes, position); + position += 4; + if (next == 0) + { + if (position % 8 != 0) + { + position += 4; + } + + break; + } + offsets.Add(next); + } + + return (offsets.ToArray(), position); + } + + private static string[] ReadFieldNames(byte[] bytes, int startPosition, int numberOfFields) + { + var result = new string[numberOfFields]; + var position = startPosition; + for (var i = 0; i < numberOfFields; i++) + { + var list = new List(); + while (bytes[position] != 0) + { + list.Add(bytes[position]); + position++; + } + result[i] = Encoding.ASCII.GetString(list.ToArray()); + position++; + } + + return result; + } + } +}