From 3ced2c585c7e050fbf14696235db3a751883874d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BA=D0=BF=D0=B0=D0=B5=D0=B2=20=D0=95=D0=B2=D0=B3?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0?= =?UTF-8?q?=D0=BD=D0=B4=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Sun, 3 Jan 2021 00:22:00 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D1=8C=D1=82?= =?UTF-8?q?=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OneSTools.FileDatabase.sln | 31 ++ .../FileDatabaseConnection.cs | 104 ++++++ OneSTools.FileDatabase/HighLevel/Field.cs | 85 +++++ OneSTools.FileDatabase/HighLevel/FieldType.cs | 42 +++ OneSTools.FileDatabase/HighLevel/Index.cs | 40 ++ .../HighLevel/IndexField.cs | 27 ++ OneSTools.FileDatabase/HighLevel/Table.cs | 120 ++++++ OneSTools.FileDatabase/HighLevel/TableRows.cs | 342 ++++++++++++++++++ .../LowLevel/FileDatabaseStream.cs | 55 +++ .../LowLevel/Files/DataFile.cs | 44 +++ .../LowLevel/Files/DatabaseDescriptionFile.cs | 57 +++ .../LowLevel/Files/InnerFile.cs | 155 ++++++++ .../LowLevel/Files/InnerFileType.cs | 8 + .../LowLevel/Files/UnlimitedLengthDataFile.cs | 54 +++ .../LowLevel/Pages/FreePagesPage.cs | 37 ++ .../LowLevel/Pages/HeaderPage.cs | 35 ++ .../LowLevel/Pages/IndexPage.cs | 30 ++ .../LowLevel/Pages/RootPage.cs | 41 +++ .../OneSTools.FileDatabase.csproj | 26 ++ .../OneSTools.FileDatabaseTestApp.csproj | 14 + OneSTools.FileDatabaseTestApp/Program.cs | 46 +++ 21 files changed, 1393 insertions(+) create mode 100644 OneSTools.FileDatabase.sln create mode 100644 OneSTools.FileDatabase/FileDatabaseConnection.cs create mode 100644 OneSTools.FileDatabase/HighLevel/Field.cs create mode 100644 OneSTools.FileDatabase/HighLevel/FieldType.cs create mode 100644 OneSTools.FileDatabase/HighLevel/Index.cs create mode 100644 OneSTools.FileDatabase/HighLevel/IndexField.cs create mode 100644 OneSTools.FileDatabase/HighLevel/Table.cs create mode 100644 OneSTools.FileDatabase/HighLevel/TableRows.cs create mode 100644 OneSTools.FileDatabase/LowLevel/FileDatabaseStream.cs create mode 100644 OneSTools.FileDatabase/LowLevel/Files/DataFile.cs create mode 100644 OneSTools.FileDatabase/LowLevel/Files/DatabaseDescriptionFile.cs create mode 100644 OneSTools.FileDatabase/LowLevel/Files/InnerFile.cs create mode 100644 OneSTools.FileDatabase/LowLevel/Files/InnerFileType.cs create mode 100644 OneSTools.FileDatabase/LowLevel/Files/UnlimitedLengthDataFile.cs create mode 100644 OneSTools.FileDatabase/LowLevel/Pages/FreePagesPage.cs create mode 100644 OneSTools.FileDatabase/LowLevel/Pages/HeaderPage.cs create mode 100644 OneSTools.FileDatabase/LowLevel/Pages/IndexPage.cs create mode 100644 OneSTools.FileDatabase/LowLevel/Pages/RootPage.cs create mode 100644 OneSTools.FileDatabase/OneSTools.FileDatabase.csproj create mode 100644 OneSTools.FileDatabaseTestApp/OneSTools.FileDatabaseTestApp.csproj create mode 100644 OneSTools.FileDatabaseTestApp/Program.cs diff --git a/OneSTools.FileDatabase.sln b/OneSTools.FileDatabase.sln new file mode 100644 index 0000000..91e7956 --- /dev/null +++ b/OneSTools.FileDatabase.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30804.86 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OneSTools.FileDatabase", "OneSTools.FileDatabase\OneSTools.FileDatabase.csproj", "{EDF41082-9B3F-4AA2-9E60-C18901AFC25A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OneSTools.FileDatabaseTestApp", "OneSTools.FileDatabaseTestApp\OneSTools.FileDatabaseTestApp.csproj", "{40FCEDB2-4C4B-4419-90BB-1BD80840AC05}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EDF41082-9B3F-4AA2-9E60-C18901AFC25A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EDF41082-9B3F-4AA2-9E60-C18901AFC25A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDF41082-9B3F-4AA2-9E60-C18901AFC25A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EDF41082-9B3F-4AA2-9E60-C18901AFC25A}.Release|Any CPU.Build.0 = Release|Any CPU + {40FCEDB2-4C4B-4419-90BB-1BD80840AC05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {40FCEDB2-4C4B-4419-90BB-1BD80840AC05}.Debug|Any CPU.Build.0 = Debug|Any CPU + {40FCEDB2-4C4B-4419-90BB-1BD80840AC05}.Release|Any CPU.ActiveCfg = Release|Any CPU + {40FCEDB2-4C4B-4419-90BB-1BD80840AC05}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {37516B47-10E9-4C36-B852-487DB3568E24} + EndGlobalSection +EndGlobal diff --git a/OneSTools.FileDatabase/FileDatabaseConnection.cs b/OneSTools.FileDatabase/FileDatabaseConnection.cs new file mode 100644 index 0000000..6187708 --- /dev/null +++ b/OneSTools.FileDatabase/FileDatabaseConnection.cs @@ -0,0 +1,104 @@ +using OneSTools.FileDatabase.HighLevel; +using System; +using System.Collections.Generic; +using System.IO; +using OneSTools.FileDatabase.LowLevel; +using System.Linq; +using OneSTools.FileDatabase.LowLevel.Pages; +using OneSTools.FileDatabase.LowLevel.Files; +using OneSTools.BracketsFile; +using System.Text; +using System.Data.Common; +using System.Collections.ObjectModel; +using System.Data; + +namespace OneSTools.FileDatabase +{ + /// + /// Provides properties and methods for reading 1C file database data + /// + public class FileDatabaseConnection : IDisposable + { + private readonly string _path; + private FileDatabaseStream _stream; + private HeaderPage _headerPage; + private FreePagesPage _freePagesPage; + private DatabaseDescriptionFile _databaseDescriptionFile; + + /// + /// The flag of database opening + /// + public bool Opened { get; private set; } = false; + /// + /// Version of the database file + /// + public string Version => _headerPage?.Version; + /// + /// Locale of the database + /// + public string Language => _databaseDescriptionFile?.Language; + /// + /// A collection of database tables + /// + public ReadOnlyCollection Tables { get; private set; } = null; + + public FileDatabaseConnection(string path) + => _path = path; + + /// + /// Open database file + /// + public void Open() + { + if (!File.Exists(_path)) + throw new FileNotFoundException("Cannot find a database file", _path); + + var stream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + _stream = new FileDatabaseStream(stream); + + ReadStructure(); + + Opened = true; + } + + private void ReadStructure() + { + _headerPage = new HeaderPage(); + _headerPage.Read(_stream); + + _stream.PageSize = _headerPage.PageSize; + + _stream.CurrentPage = 1; + _freePagesPage = new FreePagesPage(); + _freePagesPage.Read(_stream); + + _databaseDescriptionFile = new DatabaseDescriptionFile(_stream); + + var tables = new List
(); + + for (int i = 0; i < _databaseDescriptionFile.TablesCount; i++) + { + var tableDefinitionData = _databaseDescriptionFile.ReadTableDefinitionData(); + var tableDefinitionStr = Encoding.UTF8.GetString(tableDefinitionData); + var tableDefinitionNode = BracketsParser.ParseBlock(tableDefinitionStr); + + var table = new Table(_stream, tableDefinitionNode); + tables.Add(table); + } + + Tables = tables.AsReadOnly(); + } + + /// + /// Close database file + /// + public void Close() + { + Opened = false; + _stream?.Dispose(); + } + + public void Dispose() + => Close(); + } +} diff --git a/OneSTools.FileDatabase/HighLevel/Field.cs b/OneSTools.FileDatabase/HighLevel/Field.cs new file mode 100644 index 0000000..8459cd9 --- /dev/null +++ b/OneSTools.FileDatabase/HighLevel/Field.cs @@ -0,0 +1,85 @@ +using System; +using OneSTools.BracketsFile; + +namespace OneSTools.FileDatabase.HighLevel +{ + public class Field + { + /// + /// Real size of the value (bytes) + /// + internal int MaxSize { get; private set; } + /// + /// Internal name of the field + /// + public string Name { get; private set; } + /// + /// Type of the field's value + /// + public FieldType Type { get; private set; } + /// + /// If the flag is True a value can be null + /// + public bool Nullable { get; private set; } + /// + /// Length of the value + /// + public int Length { get; private set; } + /// + /// "Numeric" value only - position of a point in a value + /// + public int Precision { get; private set; } + public string CaseSensitive { get; private set; } + + internal void Read(BracketsNode node) + { + Name = node[0]; + + var type = (string)node[1]; + + Type = type switch + { + "B" => FieldType.Binary, + "L" => FieldType.Logical, + "N" => FieldType.Numeric, + "NC" => FieldType.NChar, + "NVC" => FieldType.NVarChar, + "RV" => FieldType.RowVersion, + "NT" => FieldType.NText, + "I" => FieldType.Image, + "DT" => FieldType.DateTime, + _ => throw new Exception($"{type} is unknown field type"), + }; + + Nullable = node[2]; + Length = node[3]; + Precision = node[4]; + CaseSensitive = node[5]; + + CalculateRealSize(); + } + + public override string ToString() + => $"{Name} ({Type})"; + + private void CalculateRealSize() + { + MaxSize = Type switch + { + FieldType.Binary => Length, + FieldType.Logical => 1, + FieldType.Numeric => (Length + 1) / 2 + (Length + 1) % 2, + FieldType.NChar => Length * 2, + FieldType.NVarChar => Length * 2 + 2, + FieldType.RowVersion => 16, + FieldType.NText => 8, + FieldType.Image => 8, + FieldType.DateTime => 7, + _ => throw new NotImplementedException($"There is no alghorithm to calculate size of \"{Type}\" type"), + }; + + if (Nullable) + MaxSize++; + } + } +} diff --git a/OneSTools.FileDatabase/HighLevel/FieldType.cs b/OneSTools.FileDatabase/HighLevel/FieldType.cs new file mode 100644 index 0000000..99cc264 --- /dev/null +++ b/OneSTools.FileDatabase/HighLevel/FieldType.cs @@ -0,0 +1,42 @@ +namespace OneSTools.FileDatabase.HighLevel +{ + public enum FieldType + { + /// + /// Fixed-length binary data + /// + Binary, + /// + /// Boolean value + /// + Logical, + /// + /// Fixed point decimal + /// + Numeric, + /// + /// Fixed-length Unicode string + /// + NChar, // Unicode string + /// + /// Variable-length Unicode string + /// + NVarChar, + /// + /// Version of the row + /// + RowVersion, + /// + /// Unlimited-length Unicode string (UTF-16) + /// + NText, + /// + /// Unlimited-length binary data + /// + Image, + /// + /// Date and time + /// + DateTime + } +} diff --git a/OneSTools.FileDatabase/HighLevel/Index.cs b/OneSTools.FileDatabase/HighLevel/Index.cs new file mode 100644 index 0000000..beefa6e --- /dev/null +++ b/OneSTools.FileDatabase/HighLevel/Index.cs @@ -0,0 +1,40 @@ +using OneSTools.BracketsFile; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace OneSTools.FileDatabase.HighLevel +{ + public class Index + { + /// + /// Internal name of the index + /// + public string Name { get; private set; } + /// + /// Collection of the index fields + /// + public ReadOnlyCollection Fields { get; private set; } = null; + + internal void Read(BracketsNode node) + { + var fields = new List(); + + Name = (string)node[0]; + + for (int i = 2; i < node.Count; i++) + { + var indexField = new IndexField(); + indexField.Read(node[i]); + + fields.Add(indexField); + } + + Fields = fields.AsReadOnly(); + } + + public override string ToString() + { + return Name; + } + } +} diff --git a/OneSTools.FileDatabase/HighLevel/IndexField.cs b/OneSTools.FileDatabase/HighLevel/IndexField.cs new file mode 100644 index 0000000..7d15c52 --- /dev/null +++ b/OneSTools.FileDatabase/HighLevel/IndexField.cs @@ -0,0 +1,27 @@ +using OneSTools.BracketsFile; + +namespace OneSTools.FileDatabase.HighLevel +{ + public class IndexField + { + /// + /// Internal name of the index field + /// + public string Name { get; private set; } + /// + /// The length of the value + /// + public int Length { get; private set; } + + internal void Read(BracketsNode node) + { + Name = node[0]; + Length = node[1]; + } + + public override string ToString() + { + return Name; + } + } +} diff --git a/OneSTools.FileDatabase/HighLevel/Table.cs b/OneSTools.FileDatabase/HighLevel/Table.cs new file mode 100644 index 0000000..0156f11 --- /dev/null +++ b/OneSTools.FileDatabase/HighLevel/Table.cs @@ -0,0 +1,120 @@ +using OneSTools.BracketsFile; +using System.Collections.Generic; +using System.IO; +using System; +using OneSTools.FileDatabase.LowLevel; +using System.Collections.ObjectModel; + +namespace OneSTools.FileDatabase.HighLevel +{ + public class Table + { + internal int MaxRowSize { get; private set; } + internal uint DataFilePage { get; private set; } + internal uint UnlimitedLengthDataFilePage { get; private set; } + internal uint IndexFilePage { get; private set; } + + /// + /// Internal name of the table + /// + public string Name { get; private set; } + /// + /// Collection of table fields + /// + public ReadOnlyCollection Fields { get; private set; } = null; + /// + /// Collection of table indexes + /// + public ReadOnlyCollection Indexes { get; private set; } = null; + public bool RecordLock { get; private set; } + /// + /// Collection of table rows + /// + public IReadOnlyList Rows { get; private set; } = null; + + internal Table(FileDatabaseStream stream, BracketsNode node) + { + Name = node[0]; + + for (int i = 2; i < node.Count; i++) + { + var currentNode = node[i]; + string nodeName = currentNode[0]; + + switch (nodeName) + { + case "Fields": + ReadFields(currentNode); + break; + case "Indexes": + ReadIndexes(currentNode); + break; + case "Recordlock": + RecordLock = currentNode[1]; + break; + case "Files": + DataFilePage = currentNode[1]; + UnlimitedLengthDataFilePage = currentNode[2]; + IndexFilePage = currentNode[3]; + break; + default: + throw new Exception($"{nodeName} is unknown table description node"); + } + } + + Rows = new TableRows(stream, this); + } + + private void ReadFields(BracketsNode node) + { + var fields = new List(); + + for (int i = 1; i < node.Count; i++) + { + var field = new Field(); + field.Read(node[i]); + + if (field.Type == FieldType.RowVersion) + { + fields.Insert(0, field); + } + else + fields.Add(field); + + MaxRowSize += field.MaxSize; + } + + // add "free row" mark length + MaxRowSize++; + + // add "short version" data length + if (fields.Count > 0 && fields[0].Type == FieldType.RowVersion && RecordLock) + MaxRowSize += 8; + + Fields = fields.AsReadOnly(); + } + + private void ReadIndexes(BracketsNode node) + { + var indexes = new List(); + + if (node.Count > 1) + { + for (int i = 1; i < node.Count; i++) + { + var index = new Index(); + index.Read(node[i]); + + indexes.Add(index); + } + } + + Indexes = indexes.AsReadOnly(); + } + + public override string ToString() + { + return Name; + } + } +} diff --git a/OneSTools.FileDatabase/HighLevel/TableRows.cs b/OneSTools.FileDatabase/HighLevel/TableRows.cs new file mode 100644 index 0000000..2872010 --- /dev/null +++ b/OneSTools.FileDatabase/HighLevel/TableRows.cs @@ -0,0 +1,342 @@ +using System.Collections.Generic; +using System.Text; +using System; +using OneSTools.FileDatabase.LowLevel; +using OneSTools.FileDatabase.LowLevel.Files; +using System.Buffers.Binary; +using System.Collections; +using System.Collections.ObjectModel; +using System.Globalization; + +namespace OneSTools.FileDatabase.HighLevel +{ + internal class TableRows : IReadOnlyList + { + private readonly FileDatabaseStream _stream; + private readonly Table _table; + private DataFile _dataFile; + private UnlimitedLengthDataFile _unlimitedLengthDataFile; + + internal TableRows(FileDatabaseStream stream, Table table) + { + _stream = stream; + _table = table; + } + + public object[] this[int index] + { + get + { + if (index >= Count) + throw new IndexOutOfRangeException(); + else + { + return Get(index + 1); + } + } + } + + public int Count + { + get + { + InitializeFiles(); + + return (int)(_dataFile?.RootPage.DataLength / Convert.ToUInt64(_table.MaxRowSize)); + } + } + + public IEnumerator GetEnumerator() + { + for (int i = 0; i < Count; i++) + { + yield return Get(i + 1); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private void InitializeFiles() + { + if (_dataFile == null) + { + if (_table.DataFilePage != 0) + { + _dataFile = new DataFile(_stream, _table.DataFilePage, _table.MaxRowSize, _table.Fields.Count > 0 && _table.Fields[0].Type != FieldType.RowVersion && _table.RecordLock); + + if (_dataFile.HasData()) + { + _dataFile.GoToDataStartingPosition(); + _dataFile.GoToRow(1); + } + } + + if (_table.UnlimitedLengthDataFilePage != 0) + { + _unlimitedLengthDataFile = new UnlimitedLengthDataFile(_stream, _table.UnlimitedLengthDataFilePage); + + if (_unlimitedLengthDataFile.HasData()) + _dataFile.GoToDataStartingPosition(); + } + } + } + + private object[] Get(int rowNumber) + { + InitializeFiles(); + + _dataFile.GoToRow(rowNumber); + + var values = new object[_table.Fields.Count]; + + var rawData = _dataFile.ReadRow(); + + if (rawData == null) + return null; + else + { + var currentOffset = 0; + + for (int i = 0; i < _table.Fields.Count; i++) + { + var field = _table.Fields[i]; + + var fieldData = rawData[currentOffset..(currentOffset + field.MaxSize)]; + + currentOffset += field.MaxSize; + + values[i] = field.Type switch + { + FieldType.Binary => ReadBinary(fieldData, field.Nullable), + FieldType.Logical => ReadLogical(fieldData, field.Nullable), + FieldType.Numeric => ReadNumericValue(fieldData, field.Precision, field.Nullable), + FieldType.NChar => ReadNChar(fieldData, field.Nullable), + FieldType.NVarChar => ReadNVarChar(fieldData, field.Nullable), + FieldType.RowVersion => ReadRowVersion(fieldData, field.Nullable), + FieldType.NText => ReadNText(fieldData, field.Nullable), + FieldType.Image => ReadImage(fieldData, field.Nullable), + FieldType.DateTime => ReadDateTime(fieldData, field.Nullable), + _ => throw new Exception($"Reading value for a field with type {field.Type} is not implemented") + }; + } + + return values; + } + } + + private byte[] ReadBinary(byte[] data, bool nullable) + { + if (!HasValue(data, nullable)) + return null; + else + { + var valueData = GetValueData(data, nullable); + + return valueData; + } + } + + private bool? ReadLogical(byte[] data, bool nullable) + { + if (!HasValue(data, nullable)) + return null; + else + { + var valueData = GetValueData(data, nullable); + + return valueData[0] != 0; + } + } + + private decimal? ReadNumericValue(byte[] data, int precision, bool nullable) + { + if (!HasValue(data, nullable)) + return null; + else + { + var valueData = GetValueData(data, nullable); + var doubleStr = new StringBuilder(); + + var negative = ReadTetrad(valueData[0]) == 0; + if (negative) + doubleStr.Append('-'); + + doubleStr.Append(ReadTetrad(valueData[0], true)); + + for (int i = 1; i < valueData.Length; i++) + { + doubleStr.Append(ReadTetrad(valueData[i])); + doubleStr.Append(ReadTetrad(valueData[i], true)); + } + + if (precision > 0) + doubleStr.Insert(doubleStr.Length - 1 - precision, '.'); + + // remove last zero, I don't know what is it + doubleStr.Remove(doubleStr.Length - 1, 1); + + return decimal.Parse(doubleStr.ToString(), new NumberFormatInfo() { NumberDecimalSeparator = "." }); + } + } + + private void AddNumberToNumeric(StringBuilder doubleStr, int value) + { + // don't add leading zeros + if (value != 0) + doubleStr.Append(value); + else if (doubleStr.Length > 0) + doubleStr.Append(value); + } + + private string ReadNChar(byte[] data, bool nullable) + { + if (!HasValue(data, nullable)) + return null; + else + { + var valueData = GetValueData(data, nullable); + + return Encoding.Unicode.GetString(valueData); + } + } + + private string ReadNVarChar(byte[] data, bool nullable) + { + if (!HasValue(data, nullable)) + return null; + else + { + var valueData = GetValueData(data, nullable); + + var length = BinaryPrimitives.ReadUInt16LittleEndian(valueData); + + if (length == 0) + return ""; + else + return Encoding.Unicode.GetString(valueData[2..(length * 2 + 2)]); + } + } + + private DateTime? ReadDateTime(byte[] data, bool nullable) + { + if (!HasValue(data, nullable)) + return null; + else + { + var valueData = GetValueData(data, nullable); + + var year1 = ReadTetrad(valueData[0]); + var year2 = ReadTetrad(valueData[0], true); + var year3 = ReadTetrad(valueData[1]); + var year4 = ReadTetrad(valueData[1], true); + var year = int.Parse($"{year1}{year2}{year3}{year4}"); + + var month1 = ReadTetrad(valueData[2]); + var month2 = ReadTetrad(valueData[2], true); + var month = int.Parse($"{month1}{month2}"); + + var day1 = ReadTetrad(valueData[3]); + var day2 = ReadTetrad(valueData[3], true); + var day = int.Parse($"{day1}{day2}"); + + var hour1 = ReadTetrad(valueData[4]); + var hour2 = ReadTetrad(valueData[4], true); + var hour = int.Parse($"{hour1}{hour2}"); + + var minute1 = ReadTetrad(valueData[5]); + var minute2 = ReadTetrad(valueData[5], true); + var minute = int.Parse($"{minute1}{minute2}"); + + var second1 = ReadTetrad(valueData[6]); + var second2 = ReadTetrad(valueData[6], true); + var second = int.Parse($"{second1}{second2}"); + + if (year == 0 && month == 0 && day == 0 && hour == 0 && minute == 0 && second == 0) + return DateTime.MinValue; + else + return new DateTime(year, month, day, hour, minute, second); + } + } + + private string ReadRowVersion(byte[] data, bool nullable) + { + if (!HasValue(data, nullable)) + return null; + else + { + var valueData = GetValueData(data, nullable); + + var v1 = BinaryPrimitives.ReadUInt32LittleEndian(valueData[..5]); + var v2 = BinaryPrimitives.ReadUInt32LittleEndian(valueData[5..9]); + var v3 = BinaryPrimitives.ReadUInt32LittleEndian(valueData[9..13]); + var v4 = BinaryPrimitives.ReadUInt32LittleEndian(valueData[12..]); + + return $"{v1}.{v2}.{v3}.{v4}"; + } + } + + private string ReadNText(byte[] data, bool nullable) + { + var valueData = ReadUnlimitedData(data, nullable); + + if (valueData is null) + return null; + else + return Encoding.Unicode.GetString(valueData); + } + + private byte[] ReadImage(byte[] data, bool nullable) + { + var valueData = ReadUnlimitedData(data, nullable); + + if (valueData is null) + return null; + else + return valueData; + } + + private byte[] ReadUnlimitedData(byte[] data, bool nullable) + { + if (!HasValue(data, nullable)) + return null; + + var valueData = GetValueData(data, nullable); + + var blockNumber = BinaryPrimitives.ReadUInt32LittleEndian(valueData); + var dataLength = BinaryPrimitives.ReadUInt32LittleEndian(valueData[4..]); + + _unlimitedLengthDataFile.GoToBlock(blockNumber); + + var valueRawData = _unlimitedLengthDataFile.ReadBlockChain(); + + return valueRawData[..(int)dataLength]; + } + + private bool HasValue(byte[] data, bool nullable) + { + if (nullable) + return (bool)ReadLogical(data, false); + else + return true; + } + + private byte[] GetValueData(byte[] data, bool nullable) + { + if (nullable) + return data[1..]; + else + return data; + } + + private int ReadTetrad(byte b, bool second = false) + { + if (second) + return b & 0b_0000_1111; + else + return b >> 4; + } + } +} diff --git a/OneSTools.FileDatabase/LowLevel/FileDatabaseStream.cs b/OneSTools.FileDatabase/LowLevel/FileDatabaseStream.cs new file mode 100644 index 0000000..df32111 --- /dev/null +++ b/OneSTools.FileDatabase/LowLevel/FileDatabaseStream.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; + +namespace OneSTools.FileDatabase.LowLevel +{ + internal class FileDatabaseStream : IDisposable + { + private readonly BinaryReader reader; + + public long Position + { + get => reader.BaseStream.Position; + set => reader.BaseStream.Position = value; + } + public uint PageSize { get; set; } + public uint CurrentPage + { + get => (uint)(Position / PageSize); + set => reader.BaseStream.Position = PageSize * value; + } + public uint PositionOnPage => (uint)(Position - (CurrentPage * PageSize)); + + public FileDatabaseStream(Stream stream) + { + reader = new BinaryReader(stream); + } + + public byte[] ReadBytes(int count) + => reader.ReadBytes(count); + + public int ReadBytes(byte[] buffer, int offset, int count) + { + var forReading = MaxBytesCanTake(count); + + return reader.Read(buffer, offset, (int)forReading); + } + + public long SkipBytes(long count) + { + var forSkip = MaxBytesCanTake(count); + + reader.BaseStream.Position += forSkip; + + return forSkip; + } + + private long MaxBytesCanTake(long requestedCount) + => Math.Min(((CurrentPage + 1) * PageSize) - Position, requestedCount); + + public void Dispose() + { + reader?.Dispose(); + } + } +} diff --git a/OneSTools.FileDatabase/LowLevel/Files/DataFile.cs b/OneSTools.FileDatabase/LowLevel/Files/DataFile.cs new file mode 100644 index 0000000..73fa3f7 --- /dev/null +++ b/OneSTools.FileDatabase/LowLevel/Files/DataFile.cs @@ -0,0 +1,44 @@ +using System; + +namespace OneSTools.FileDatabase.LowLevel.Files +{ + internal class DataFile : InnerFile + { + private readonly int _rowSize; + private readonly bool _hasShortVersion; + + public DataFile(FileDatabaseStream stream, uint rootPageNumber, int rowSize, bool hasShortVersion) : base(stream, rootPageNumber) + { + _rowSize = rowSize; + _hasShortVersion = hasShortVersion; + } + + public void GoToRow(int number) + { + GoToDataStartingPosition(); + + SkipBytes(_rowSize * number); + } + + public byte[] ReadRow() + { + while (!EndOfDataStream) + { + var data = ReadBytes(_rowSize); + + // it's not a free row + if (data[0] == 0) + { + _dataStreamPosition += Convert.ToUInt64(_rowSize); + + if (_hasShortVersion) + return data[9..]; + else + return data[1..]; + } + } + + return null; + } + } +} diff --git a/OneSTools.FileDatabase/LowLevel/Files/DatabaseDescriptionFile.cs b/OneSTools.FileDatabase/LowLevel/Files/DatabaseDescriptionFile.cs new file mode 100644 index 0000000..8364b0a --- /dev/null +++ b/OneSTools.FileDatabase/LowLevel/Files/DatabaseDescriptionFile.cs @@ -0,0 +1,57 @@ +using System; +using System.Buffers.Binary; +using System.Text; +using System.Linq; + +namespace OneSTools.FileDatabase.LowLevel.Files +{ + internal class DatabaseDescriptionFile: UnlimitedLengthDataFile + { + private int _currentTableStartBlock = -1; + + public string Language { get; private set; } + public uint TablesCount { get; private set; } + public uint[] TablesStartBlocks { get; private set; } + + public DatabaseDescriptionFile(FileDatabaseStream stream) : base(stream, 2) + { + GoToDataStartingPosition(); + + SkipFirstBlock(); + + var ddfHeaderData = ReadBlockChain(); + + Language = Encoding.UTF8.GetString(ddfHeaderData[..(Array.IndexOf(ddfHeaderData, (byte)0))]); + TablesCount = BinaryPrimitives.ReadUInt32LittleEndian(ddfHeaderData[32..36]); + TablesStartBlocks = new uint[TablesCount]; + + for (int i = 0; i < TablesCount; i++) + { + var startIndex = 36 + i * 4; + var number = BinaryPrimitives.ReadUInt32LittleEndian(ddfHeaderData[startIndex..(startIndex + 4)]); + + if (number == 0) + break; + + TablesStartBlocks[i] = number; + } + } + + public byte[] ReadTableDefinitionData() + { + GoToNextTableStartBlock(); + + return ReadBlockChain(); + } + + private void GoToNextTableStartBlock() + { + _currentTableStartBlock++; + + if (TablesStartBlocks.Length <= _currentTableStartBlock) + throw new Exception("There are no more data pages in the root page list"); + else + GoToBlock(TablesStartBlocks[_currentTableStartBlock]); + } + } +} diff --git a/OneSTools.FileDatabase/LowLevel/Files/InnerFile.cs b/OneSTools.FileDatabase/LowLevel/Files/InnerFile.cs new file mode 100644 index 0000000..9e4d0b4 --- /dev/null +++ b/OneSTools.FileDatabase/LowLevel/Files/InnerFile.cs @@ -0,0 +1,155 @@ +using OneSTools.FileDatabase.LowLevel.Pages; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace OneSTools.FileDatabase.LowLevel.Files +{ + + internal abstract class InnerFile + { + protected readonly FileDatabaseStream _stream; + private uint _rootPageNumber; + private readonly List _dataPages = new List(); + protected int _dataPage = -1; + private long _position = 0; + protected ulong _dataStreamPosition = 0; + + public RootPage RootPage { get; private set; } + public List IndexPages { get; private set; } + public bool EndOfDataStream => _dataStreamPosition >= RootPage.DataLength; + + public InnerFile(FileDatabaseStream stream, uint rootPageNumber) + { + _stream = stream; + _rootPageNumber = rootPageNumber; + + ReadRootPage(); + + if (RootPage.Type == InnerFileType.Full) + { + ReadIndexPages(); + + IndexPages.ForEach(c => _dataPages.AddRange(c.PageNumbers)); + } + else + _dataPages.AddRange(RootPage.PageNumbers); + } + + public bool HasData() + => RootPage.DataLength > 0; + + public void GoToDataStartingPosition() + { + if (!HasData()) + throw new Exception("The file doesn't contain data"); + + _dataPage = -1; + + GoToNextDataPage(); + } + + protected byte[] ReadBytes(int count) + { + _stream.Position = _position; + + if (_dataPage == -1) + GoToNextDataPage(); + + var buffer = new byte[count]; + + // Loop till read requested quantity + var read = 0; + + while (read < count) + { + read += _stream.ReadBytes(buffer, read, count - read); + + if (read < count) + GoToNextDataPage(); + } + + if (PageReadingCompleted()) + GoToNextDataPage(); + + _position = _stream.Position; + + return buffer; + } + + protected void SkipBytes(long count) + { + _stream.Position = _position; + + if (_dataPage == -1) + GoToNextDataPage(); + + // Loop till read requested quantity + long skipped = 0; + + while (skipped < count) + { + skipped += _stream.SkipBytes(count - skipped); + + if (skipped < count) + GoToNextDataPage(); + } + + if (PageReadingCompleted()) + GoToNextDataPage(); + + _position = _stream.Position; + } + + private void ReadRootPage() + { + _stream.CurrentPage = _rootPageNumber; + + RootPage = new RootPage(); + RootPage.Read(_stream); + + _position = _stream.Position; + } + + private void ReadIndexPages() + { + IndexPages = new List(); + + foreach (var pageNumber in RootPage.PageNumbers) + { + if (pageNumber == 0) + break; + else + { + _stream.CurrentPage = pageNumber; + + var indexPage = new IndexPage(); + indexPage.Read(_stream); + + IndexPages.Add(indexPage); + } + } + + _position = _stream.Position; + } + + private bool PageReadingCompleted() + => _dataPages[_dataPage] != _stream.CurrentPage; + + private void GoToNextDataPage() + { + _dataPage++; + + if (_dataPages.Count <= _dataPage) + throw new Exception("There are no more data pages in the root page list"); + else + { + var dataPage = _dataPages[_dataPage]; + + _stream.CurrentPage = dataPage; + } + + _position = _stream.Position; + } + } +} diff --git a/OneSTools.FileDatabase/LowLevel/Files/InnerFileType.cs b/OneSTools.FileDatabase/LowLevel/Files/InnerFileType.cs new file mode 100644 index 0000000..4358f96 --- /dev/null +++ b/OneSTools.FileDatabase/LowLevel/Files/InnerFileType.cs @@ -0,0 +1,8 @@ +namespace OneSTools.FileDatabase.LowLevel.Files +{ + public enum InnerFileType + { + Short = 0xFD1C, + Full = 0x01FD1C + } +} diff --git a/OneSTools.FileDatabase/LowLevel/Files/UnlimitedLengthDataFile.cs b/OneSTools.FileDatabase/LowLevel/Files/UnlimitedLengthDataFile.cs new file mode 100644 index 0000000..805e20a --- /dev/null +++ b/OneSTools.FileDatabase/LowLevel/Files/UnlimitedLengthDataFile.cs @@ -0,0 +1,54 @@ +using System.Buffers.Binary; +using System.Collections.Generic; +using System; + +namespace OneSTools.FileDatabase.LowLevel.Files +{ + + internal class UnlimitedLengthDataFile : InnerFile + { + private const int UNLIMITED_DATA_BLOCK_MAX_SIZE = 256; + + public UnlimitedLengthDataFile(FileDatabaseStream stream, uint rootPageNumber) : base(stream, rootPageNumber) + { + + } + + public void SkipFirstBlock() + { + // It skips the first block, cause it's a first block of the chain of free blocks + SkipBytes(UNLIMITED_DATA_BLOCK_MAX_SIZE); + } + + public void GoToBlock(uint blockNumber) + { + GoToDataStartingPosition(); + + SkipBytes(blockNumber * UNLIMITED_DATA_BLOCK_MAX_SIZE); + } + + public byte[] ReadBlockChain() + { + var buffer = new List(); + + while (true) + { + var nextBlock = BinaryPrimitives.ReadUInt32LittleEndian(ReadBytes(4)); + var blockDatalength = BinaryPrimitives.ReadUInt16LittleEndian(ReadBytes(2)); + + if (blockDatalength > 0) + { + var blockData = ReadBytes(blockDatalength); + buffer.AddRange(blockData); + } + + if (nextBlock == 0) + break; + else + GoToBlock(nextBlock); + } + + return buffer.ToArray(); + } + } +} diff --git a/OneSTools.FileDatabase/LowLevel/Pages/FreePagesPage.cs b/OneSTools.FileDatabase/LowLevel/Pages/FreePagesPage.cs new file mode 100644 index 0000000..08e8ea3 --- /dev/null +++ b/OneSTools.FileDatabase/LowLevel/Pages/FreePagesPage.cs @@ -0,0 +1,37 @@ +using System; +using System.Buffers.Binary; + +namespace OneSTools.FileDatabase.LowLevel.Pages +{ + internal class FreePagesPage + { + public uint Type { get; private set; } + public uint PagesCount { get; private set; } + public int Version { get; private set; } + public int[] PageNumbers { get; private set; } + + public void Read(FileDatabaseStream reader) + { + var type = BinaryPrimitives.ReadUInt32LittleEndian(reader.ReadBytes(4)); + + if (!(type is 0x0000FF1C)) + throw new Exception($"{type} is not a type of \"Free pages\" block"); + + Type = type; + PagesCount = BinaryPrimitives.ReadUInt32LittleEndian(reader.ReadBytes(4)); + Version = BinaryPrimitives.ReadInt32LittleEndian(reader.ReadBytes(4)); + + PageNumbers = new int[PagesCount]; + + for (int i = 0; i < PagesCount; i++) + { + var number = BinaryPrimitives.ReadInt32LittleEndian(reader.ReadBytes(4)); + + if (number == 0) + break; + + PageNumbers[i] = number; + } + } + } +} diff --git a/OneSTools.FileDatabase/LowLevel/Pages/HeaderPage.cs b/OneSTools.FileDatabase/LowLevel/Pages/HeaderPage.cs new file mode 100644 index 0000000..39ed9fc --- /dev/null +++ b/OneSTools.FileDatabase/LowLevel/Pages/HeaderPage.cs @@ -0,0 +1,35 @@ +using System; +using System.Buffers.Binary; +using System.Text; + +namespace OneSTools.FileDatabase.LowLevel.Pages +{ + internal class HeaderPage + { + public string Signature { get; private set; } + public string Version { get; private set; } + public uint PagesCount { get; private set; } + public uint PageSize { get; private set; } + + public void Read(FileDatabaseStream reader) + { + var signature = Encoding.UTF8.GetString(reader.ReadBytes(8)); + if (signature != "1CDBMSV8") + throw new Exception("This file is not a 1C database"); + + var versionData = reader.ReadBytes(4); + var version = $"{versionData[0]}.{versionData[1]}.{versionData[2]}.{versionData[3]}"; + if (version != "8.3.8.0") + throw new Exception($"{version} is unknown version"); + + Signature = signature; + Version = version; + PagesCount = BinaryPrimitives.ReadUInt32LittleEndian(reader.ReadBytes(4)); + + // skip 4 bytes of unknown value + reader.ReadBytes(4); + + PageSize = BinaryPrimitives.ReadUInt32LittleEndian(reader.ReadBytes(4)); + } + } +} diff --git a/OneSTools.FileDatabase/LowLevel/Pages/IndexPage.cs b/OneSTools.FileDatabase/LowLevel/Pages/IndexPage.cs new file mode 100644 index 0000000..b6187ee --- /dev/null +++ b/OneSTools.FileDatabase/LowLevel/Pages/IndexPage.cs @@ -0,0 +1,30 @@ +using System.Buffers.Binary; +using System.Collections.Generic; + +namespace OneSTools.FileDatabase.LowLevel.Pages +{ + internal class IndexPage + { + public ulong DataLength { get; private set; } + public uint[] PageNumbers { get; private set; } + + public virtual void Read(FileDatabaseStream reader) + { + DataLength = BinaryPrimitives.ReadUInt64LittleEndian(reader.ReadBytes(8)); + + var pageNumbers = new List(); + + while (true) + { + var pageNumber = BinaryPrimitives.ReadUInt32LittleEndian(reader.ReadBytes(4)); + + if (pageNumber == 0) + break; + + pageNumbers.Add(pageNumber); + } + + PageNumbers = pageNumbers.ToArray(); + } + } +} diff --git a/OneSTools.FileDatabase/LowLevel/Pages/RootPage.cs b/OneSTools.FileDatabase/LowLevel/Pages/RootPage.cs new file mode 100644 index 0000000..639cd14 --- /dev/null +++ b/OneSTools.FileDatabase/LowLevel/Pages/RootPage.cs @@ -0,0 +1,41 @@ +using OneSTools.FileDatabase.LowLevel.Files; +using System.Buffers.Binary; +using System.Collections.Generic; + +namespace OneSTools.FileDatabase.LowLevel.Pages +{ + internal class RootPage + { + public InnerFileType Type { get; private set; } + public string Version { get; private set; } + public ulong DataLength { get; private set; } + public uint[] PageNumbers { get; private set; } + + public void Read(FileDatabaseStream reader) + { + Type = (InnerFileType)BinaryPrimitives.ReadUInt32LittleEndian(reader.ReadBytes(4)); + + var v1 = BinaryPrimitives.ReadUInt32LittleEndian(reader.ReadBytes(4)); + var v2 = BinaryPrimitives.ReadUInt32LittleEndian(reader.ReadBytes(4)); + var v3 = BinaryPrimitives.ReadUInt32LittleEndian(reader.ReadBytes(4)); + + Version = $"{v1}.{v2}.{v3}"; + + DataLength = BinaryPrimitives.ReadUInt64LittleEndian(reader.ReadBytes(8)); + + var pageNumbers = new List(); + + while (true) + { + var pageNumber = BinaryPrimitives.ReadUInt32LittleEndian(reader.ReadBytes(4)); + + if (pageNumber == 0) + break; + + pageNumbers.Add(pageNumber); + } + + PageNumbers = pageNumbers.ToArray(); + } + } +} diff --git a/OneSTools.FileDatabase/OneSTools.FileDatabase.csproj b/OneSTools.FileDatabase/OneSTools.FileDatabase.csproj new file mode 100644 index 0000000..9d93e88 --- /dev/null +++ b/OneSTools.FileDatabase/OneSTools.FileDatabase.csproj @@ -0,0 +1,26 @@ + + + + netstandard2.1 + OneSTools.FileDatabase + OneSTools.FileDatabase + Akpaev Evgeny + Akpaev Evgeny + Библиотека для чтения данных файловых информационных баз 1С + Akpaev Evgeny + onestools_icon_nuget.png + + + + + + + + + + True + + + + + diff --git a/OneSTools.FileDatabaseTestApp/OneSTools.FileDatabaseTestApp.csproj b/OneSTools.FileDatabaseTestApp/OneSTools.FileDatabaseTestApp.csproj new file mode 100644 index 0000000..59ad671 --- /dev/null +++ b/OneSTools.FileDatabaseTestApp/OneSTools.FileDatabaseTestApp.csproj @@ -0,0 +1,14 @@ + + + + Exe + netcoreapp3.1 + OneSTools.FileDatabaseTestApp + OneSTools.FileDatabaseTestApp + + + + + + + diff --git a/OneSTools.FileDatabaseTestApp/Program.cs b/OneSTools.FileDatabaseTestApp/Program.cs new file mode 100644 index 0000000..58c7a3a --- /dev/null +++ b/OneSTools.FileDatabaseTestApp/Program.cs @@ -0,0 +1,46 @@ +using OneSTools.FileDatabase; +using System; +using System.Collections.Generic; +using System.Linq; +using OneSTools.FileDatabase.HighLevel; + +namespace OneSTools.FileDatabaseTestApp +{ + class Program + { + static void Main(string[] args) + { + var filePath = @"C:\Users\akpaev.e.ENTERPRISE\Desktop\Новая папка (2)\1Cv8.1CD"; + var filePath2 = @"C:\Users\akpaev.e.ENTERPRISE\Documents\InfoBase10\1Cv8.1CD"; + + using var database = new FileDatabaseConnection(filePath2); + database.Open(); + + var table = database.Tables.FirstOrDefault(c => c.Name == "_Document38"); + + if (table != null) + { + foreach (var values in table.Rows) + { + for (int i = 0; i < table.Fields.Count; i++) + { + var field = table.Fields[i]; + var value = values[i]; + + // Or another one what you need + if (field.Type == FieldType.Numeric) + { + var typedValue = (decimal?)value; + } + if (field.Type == FieldType.NChar + || field.Type == FieldType.NText + || field.Type == FieldType.NVarChar) + { + var typedValue = (string)value; + } + } + } + } + } + } +}