diff --git a/src/OneScript.StandardLibrary/Binary/BinaryDataContext.cs b/src/OneScript.StandardLibrary/Binary/BinaryDataContext.cs index 4ef0ee605..53f473f15 100644 --- a/src/OneScript.StandardLibrary/Binary/BinaryDataContext.cs +++ b/src/OneScript.StandardLibrary/Binary/BinaryDataContext.cs @@ -125,19 +125,21 @@ public Stream GetStream() return InMemory ? new MemoryStream(_buffer, 0, _buffer.Length, false, true) : _backingFile.OpenReadStream(); } + /// + /// Данные целиком. Данные во временном файле при каждом обращении читаются в новый массив, + /// а сами остаются в файле: большие файлы для этого в нем и хранятся + /// public byte[] Buffer { get { - if (!InMemory) - { - using var readStream = _backingFile.OpenReadStream(); - LoadToBuffer(readStream); - _backingFile.Dispose(); - _backingFile = null; - } + if (InMemory) + return _buffer; - return _buffer; + using var readStream = _backingFile.OpenReadStream(); + var buffer = new byte[readStream.Length]; + readStream.ReadExactly(buffer); + return buffer; } } diff --git a/src/OneScript.StandardLibrary/Binary/GlobalBinaryData.cs b/src/OneScript.StandardLibrary/Binary/GlobalBinaryData.cs index 7bc79fc20..f47833190 100644 --- a/src/OneScript.StandardLibrary/Binary/GlobalBinaryData.cs +++ b/src/OneScript.StandardLibrary/Binary/GlobalBinaryData.cs @@ -383,7 +383,7 @@ public BinaryDataContext GetBinaryDataFromBase64BinaryData(BinaryDataContext dat try { var enc = new UTF8Encoding(false,true); - var str = enc.GetString(data.Buffer, 0, data.Buffer.Length); + var str = enc.GetString(data.Buffer); return new BinaryDataContext(Convert.FromBase64String(str)); } catch @@ -554,7 +554,8 @@ public BinaryDataBuffer GetBinaryDataBufferFromBinaryData(BinaryDataContext data { CheckAndThrowIfNull(data); - return new BinaryDataBuffer(data.Buffer); + // Копия: запись в буфер не должна менять двоичные данные (из файла Buffer и так читает новый массив) + return new BinaryDataBuffer(data.InMemory ? (byte[])data.Buffer.Clone() : data.Buffer); } /// @@ -567,7 +568,8 @@ public BinaryDataContext GetBinaryDataFromBinaryDataBuffer(BinaryDataBuffer buff { CheckAndThrowIfNull(buffer); - return new BinaryDataContext(buffer.Bytes); + // Копия: двоичные данные не должны меняться вместе с буфером + return new BinaryDataContext((byte[])buffer.Bytes.Clone()); } } diff --git a/src/OneScript.StandardLibrary/NativeApi/NativeApiVariant.cs b/src/OneScript.StandardLibrary/NativeApi/NativeApiVariant.cs index 8d5f7ddb2..172aadd6f 100644 --- a/src/OneScript.StandardLibrary/NativeApi/NativeApiVariant.cs +++ b/src/OneScript.StandardLibrary/NativeApi/NativeApiVariant.cs @@ -45,7 +45,8 @@ public void Assign(IValue value) NativeApiProxy.SetVariantReal(Ptr, Convert.ToDouble(value.AsNumber())); break; case BinaryDataContext binaryData: - NativeApiProxy.SetVariantBlob(Ptr, binaryData.Buffer, binaryData.Buffer.Length); + var blob = binaryData.Buffer; + NativeApiProxy.SetVariantBlob(Ptr, blob, blob.Length); break; case DateTime dt: NativeApiProxy.SetVariantTm( diff --git a/src/Tests/OneScript.StandardLibrary.Tests/BinaryDataThreadSafetyTests.cs b/src/Tests/OneScript.StandardLibrary.Tests/BinaryDataThreadSafetyTests.cs new file mode 100644 index 000000000..02acc63a8 --- /dev/null +++ b/src/Tests/OneScript.StandardLibrary.Tests/BinaryDataThreadSafetyTests.cs @@ -0,0 +1,84 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Threading; +using FluentAssertions; +using OneScript.StandardLibrary.Binary; +using Xunit; + +namespace OneScript.StandardLibrary.Tests +{ + public class BinaryDataThreadSafetyTests + { + [Fact] + public void FileBackedDataIsReadFromManyThreads() + { + const int threadsCount = 8; + var expected = new byte[64 * 1024]; + new Random(42).NextBytes(expected); + var errors = new ConcurrentQueue(); + + for (var iteration = 0; iteration < 50; iteration++) + { + // Больше лимита памяти: данные во временном файле + using var data = new BinaryDataContext(new MemoryStream(expected), 1); + using var barrier = new Barrier(threadsCount); + + var threads = Enumerable.Range(0, threadsCount).Select(number => new Thread(() => + { + barrier.SignalAndWait(); + try + { + if (!ReadAll(data, viaBuffer: number % 2 == 0).AsSpan().SequenceEqual(expected)) + errors.Enqueue("данные отличаются"); + } + catch (Exception e) + { + errors.Enqueue($"{e.GetType().Name}: {e.Message}"); + } + })).ToArray(); + + foreach (var thread in threads) + { + thread.Start(); + } + foreach (var thread in threads) + { + thread.Join(); + } + } + + errors.Should().BeEmpty(); + } + + [Fact] + public void BufferKeepsFileBackedDataInFile() + { + var expected = new byte[] { 1, 2, 3 }; + using var data = new BinaryDataContext(new MemoryStream(expected), 1); + + data.Buffer.Should().Equal(expected); + data.InMemory.Should().BeFalse(); + ReadAll(data, viaBuffer: false).Should().Equal(expected); + } + + private static byte[] ReadAll(BinaryDataContext data, bool viaBuffer) + { + if (viaBuffer) + return data.Buffer; + + using var stream = data.GetStream(); + using var copy = new MemoryStream(); + stream.CopyTo(copy); + return copy.ToArray(); + } + } +} diff --git a/tests/BinaryData-global.os b/tests/BinaryData-global.os index 0de250614..96aaffa4a 100644 --- a/tests/BinaryData-global.os +++ b/tests/BinaryData-global.os @@ -29,6 +29,8 @@ ВсеТесты.Добавить("ТестДолжен_Проверить_ОтсутствиеBOMДляКодировокБезBOM"); ВсеТесты.Добавить("ТестДолжен_Проверить_КодировкиДляПолучитьСтрокуИзДвоичныхДанных"); ВсеТесты.Добавить("ТестДолжен_Проверить_КодировкиДляПолучитьСтрокуИзБуфераДвоичныхДанных"); + ВсеТесты.Добавить("ТестДолжен_Проверить_ЗаписьВБуферИзДвоичныхДанныхНеМеняетИх"); + ВсеТесты.Добавить("ТестДолжен_Проверить_ДвоичныеДанныеИзБуфераНеМеняютсяВместеСБуфером"); Возврат ВсеТесты; КонецФункции @@ -471,3 +473,27 @@ юТест.ПроверитьРавенство(ПолучитьСтрокуИзБуфераДвоичныхДанных(ДвоичныеДанные, Кодировка), "⌥", "Неверная Строка из ДвоичныхДанных без BOM для "+Кодировка); КонецПроцедуры + +Процедура ТестДолжен_Проверить_ЗаписьВБуферИзДвоичныхДанныхНеМеняетИх() Экспорт + + ДвоичныеДанные = ПолучитьДвоичныеДанныеИзСтроки("AAAA"); + Буфер1 = ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных(ДвоичныеДанные); + Буфер2 = ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных(ДвоичныеДанные); + + Буфер1.Установить(0, КодСимвола("Z")); + + юТест.ПроверитьРавенство("AAAA", ПолучитьСтрокуИзДвоичныхДанных(ДвоичныеДанные)); + юТест.ПроверитьРавенство(КодСимвола("A"), Буфер2[0]); + +КонецПроцедуры + +Процедура ТестДолжен_Проверить_ДвоичныеДанныеИзБуфераНеМеняютсяВместеСБуфером() Экспорт + + Буфер = ПолучитьБуферДвоичныхДанныхИзСтроки("AAAA"); + ДвоичныеДанные = ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(Буфер); + + Буфер.Установить(0, КодСимвола("Z")); + + юТест.ПроверитьРавенство("AAAA", ПолучитьСтрокуИзДвоичныхДанных(ДвоичныеДанные)); + +КонецПроцедуры