A Data/NosSmooth.Data.NOSFiles/Decryptors/DatDecryptor.cs => Data/NosSmooth.Data.NOSFiles/Decryptors/DatDecryptor.cs +77 -0
@@ 0,0 1,77 @@
+//
+// DatDecryptor.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Text;
+using Remora.Results;
+
+namespace NosSmooth.Data.NOSFiles.Decryptors;
+
+/// <inheritdoc />
+public class DatDecryptor : IDecryptor
+{
+ private readonly byte[] _cryptoArray;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DatDecryptor"/> class.
+ /// </summary>
+ public DatDecryptor()
+ {
+ _cryptoArray = new byte[] { 0x00, 0x20, 0x2D, 0x2E, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x0A, 0x00 };
+ }
+
+ /// <inheritdoc />
+ public Result<byte[]> Decrypt(ReadOnlySpan<byte> data)
+ {
+ using var output = new MemoryStream();
+ int i = 0;
+ while (i < data.Length)
+ {
+ byte currentByte = data[i];
+ i++;
+
+ if (currentByte == 0xFF)
+ {
+ output.WriteByte(0xD);
+ continue;
+ }
+
+ int validate = currentByte & 0x7F;
+ if ((currentByte & 0x80) != 0)
+ {
+ for (; validate > 0 && i < data.Length; validate -= 2)
+ {
+ currentByte = data[i];
+ i++;
+ byte firstByte = _cryptoArray[(currentByte & 0xF0) >> 4];
+ output.WriteByte(firstByte);
+
+ if (validate <= 1)
+ {
+ break;
+ }
+
+ byte secondByte = _cryptoArray[currentByte & 0x0F];
+ if (secondByte == 0)
+ {
+ break;
+ }
+ output.WriteByte(secondByte);
+ }
+ }
+ else
+ {
+ for (; validate > 0 && i < data.Length; validate--)
+ {
+ currentByte = data[i];
+ output.WriteByte((byte)(currentByte ^ 0x33));
+ i++;
+ }
+ }
+ }
+
+ return output.ToArray();
+ }
+}<
\ No newline at end of file
A Data/NosSmooth.Data.NOSFiles/Decryptors/IDecryptor.cs => Data/NosSmooth.Data.NOSFiles/Decryptors/IDecryptor.cs +22 -0
@@ 0,0 1,22 @@
+//
+// IDecryptor.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Remora.Results;
+
+namespace NosSmooth.Data.NOSFiles.Decryptors;
+
+/// <summary>
+/// A decryptor interface.
+/// </summary>
+public interface IDecryptor
+{
+ /// <summary>
+ /// Decrypts the given data.
+ /// </summary>
+ /// <param name="data">The data.</param>
+ /// <returns>An array with data or an error.</returns>
+ public Result<byte[]> Decrypt(ReadOnlySpan<byte> data);
+}<
\ No newline at end of file
A Data/NosSmooth.Data.NOSFiles/Decryptors/LstDecryptor.cs => Data/NosSmooth.Data.NOSFiles/Decryptors/LstDecryptor.cs +36 -0
@@ 0,0 1,36 @@
+//
+// LstDecryptor.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Buffers.Binary;
+using Remora.Results;
+
+namespace NosSmooth.Data.NOSFiles.Decryptors;
+
+/// <inheritdoc />
+public class LstDecryptor : IDecryptor
+{
+ /// <inheritdoc />
+ public Result<byte[]> Decrypt(ReadOnlySpan<byte> data)
+ {
+ var output = new MemoryStream();
+ int linesCount = BinaryPrimitives.ReadInt32LittleEndian(data);
+ data = data.Slice(4);
+ for (var i = 0; i < linesCount; i++)
+ {
+ int lineLength = BinaryPrimitives.ReadInt32LittleEndian(data);
+ data = data.Slice(4);
+ ReadOnlySpan<byte> line = data.Slice(0, lineLength);
+ data = data.Slice(lineLength);
+ foreach (var c in line)
+ {
+ output.WriteByte((byte)(c ^ 0x1));
+ }
+ output.WriteByte((byte)'\n');
+ }
+
+ return output.ToArray();
+ }
+}<
\ No newline at end of file
A Data/NosSmooth.Data.NOSFiles/Errors/UnknownFileTypeError.cs => Data/NosSmooth.Data.NOSFiles/Errors/UnknownFileTypeError.cs +12 -0
@@ 0,0 1,12 @@
+//
+// UnknownFileTypeError.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Data.NOSFiles.Files;
+using Remora.Results;
+
+namespace NosSmooth.Data.NOSFiles.Errors;
+
+public record UnknownFileTypeError(RawFile file) : NotFoundError($"Could not find reader for the given file {file.Path}.");<
\ No newline at end of file
A Data/NosSmooth.Data.NOSFiles/Extensions/ServiceCollectionExtensions.cs => Data/NosSmooth.Data.NOSFiles/Extensions/ServiceCollectionExtensions.cs +42 -0
@@ 0,0 1,42 @@
+//
+// ServiceCollectionExtensions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.DependencyInjection;
+using NosSmooth.Data.NOSFiles.Readers;
+using NosSmooth.Data.NOSFiles.Readers.Types;
+
+namespace NosSmooth.Data.NOSFiles.Extensions;
+
+/// <summary>
+/// Extension methods for <see cref="IServiceProvider"/>.
+/// </summary>
+public static class ServiceCollectionExtensions
+{
+ /// <summary>
+ /// Add the file reader and NosTale type readers.
+ /// </summary>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <returns>The collection.</returns>
+ public static IServiceCollection AddFileReader(this IServiceCollection serviceCollection)
+ {
+ return serviceCollection
+ .AddSingleton<FileReader>()
+ .AddFileTypeReader<NosZlibFileTypeReader>()
+ .AddFileTypeReader<NosTextFileTypeReader>();
+ }
+
+ /// <summary>
+ /// Add the given file type reader.
+ /// </summary>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <typeparam name="TTypeReader">The type of the reader.</typeparam>
+ /// <returns>The collection.</returns>
+ public static IServiceCollection AddFileTypeReader<TTypeReader>(this IServiceCollection serviceCollection)
+ where TTypeReader : class, IFileTypeReader
+ {
+ return serviceCollection.AddSingleton<IFileTypeReader, TTypeReader>();
+ }
+}<
\ No newline at end of file
A Data/NosSmooth.Data.NOSFiles/Files/FileArchive.cs => Data/NosSmooth.Data.NOSFiles/Files/FileArchive.cs +9 -0
@@ 0,0 1,9 @@
+//
+// FileArchive.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace NosSmooth.Data.NOSFiles.Files;
+
+public record FileArchive(IReadOnlyList<RawFile> Files);<
\ No newline at end of file
A Data/NosSmooth.Data.NOSFiles/Files/FileType.cs => Data/NosSmooth.Data.NOSFiles/Files/FileType.cs +23 -0
@@ 0,0 1,23 @@
+//
+// FileType.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace NosSmooth.Data.NOSFiles.Files;
+
+/// <summary>
+/// A type of a file.
+/// </summary>
+public enum FileType
+{
+ /// <summary>
+ /// The file is a text file.
+ /// </summary>
+ Text,
+
+ /// <summary>
+ /// The file is a binary file with special meaning.
+ /// </summary>
+ Binary
+}<
\ No newline at end of file
A Data/NosSmooth.Data.NOSFiles/Files/RawFile.cs => Data/NosSmooth.Data.NOSFiles/Files/RawFile.cs +20 -0
@@ 0,0 1,20 @@
+//
+// RawFile.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Mime;
+using System.Security.Cryptography;
+
+namespace NosSmooth.Data.NOSFiles.Files;
+
+[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Upper case is standard.")]
+public record struct RawFile
+(
+ FileType? FileType,
+ string Path,
+ long Length,
+ byte[] Content
+);<
\ No newline at end of file
A Data/NosSmooth.Data.NOSFiles/Files/ReadFile.cs => Data/NosSmooth.Data.NOSFiles/Files/ReadFile.cs +16 -0
@@ 0,0 1,16 @@
+//
+// ReadFile.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace NosSmooth.Data.NOSFiles.Files;
+
+[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Upper case is standard.")]
+public record struct ReadFile<TContent>
+(
+ string Path,
+ TContent Content
+);<
\ No newline at end of file
M Data/NosSmooth.Data.NOSFiles/NosSmooth.Data.NOSFiles.csproj => Data/NosSmooth.Data.NOSFiles/NosSmooth.Data.NOSFiles.csproj +3 -2
@@ 7,8 7,9 @@
</PropertyGroup>
<ItemGroup>
- <Folder Include="Dat" />
- <Folder Include="Nos" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
+ <PackageReference Include="Remora.Results" Version="7.1.0" />
+ <PackageReference Include="SharpZipLib" Version="1.3.3" />
</ItemGroup>
</Project>
A Data/NosSmooth.Data.NOSFiles/Readers/FileReader.cs => Data/NosSmooth.Data.NOSFiles/Readers/FileReader.cs +98 -0
@@ 0,0 1,98 @@
+//
+// FileReader.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Runtime.InteropServices;
+using NosSmooth.Data.NOSFiles.Errors;
+using NosSmooth.Data.NOSFiles.Files;
+using Remora.Results;
+
+namespace NosSmooth.Data.NOSFiles.Readers;
+
+/// <summary>
+/// Reader of files.
+/// </summary>
+public class FileReader
+{
+ private readonly IReadOnlyList<IFileTypeReader> _typeReaders;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FileReader"/> class.
+ /// </summary>
+ /// <param name="typeReaders">The readers of specific types.</param>
+ public FileReader(IEnumerable<IFileTypeReader> typeReaders)
+ {
+ _typeReaders = typeReaders.ToArray();
+ }
+
+ /// <summary>
+ /// Get a file type reader for the given file.
+ /// </summary>
+ /// <param name="file">The file.</param>
+ /// <returns>A type reader or an error.</returns>
+ public Result<IFileTypeReader> GetFileTypeReader(RawFile file)
+ {
+ foreach (var typeReader in _typeReaders)
+ {
+ if (typeReader.SupportsFile(file))
+ {
+ return Result<IFileTypeReader>.FromSuccess(typeReader);
+ }
+ }
+
+ return new UnknownFileTypeError(file);
+ }
+
+ /// <summary>
+ /// Read the given file.
+ /// </summary>
+ /// <param name="file">The raw file.</param>
+ /// <typeparam name="TContent">The type of the content to assume.</typeparam>
+ /// <returns>The content or an error.</returns>
+ public Result<ReadFile<TContent>> Read<TContent>(RawFile file)
+ {
+ var fileReaderResult = GetFileTypeReader(file);
+ if (!fileReaderResult.IsSuccess)
+ {
+ return Result<ReadFile<TContent>>.FromError(fileReaderResult);
+ }
+
+ var fileReader = fileReaderResult.Entity;
+ var readResult = fileReader.Read(file);
+ if (!readResult.IsSuccess)
+ {
+ return Result<ReadFile<TContent>>.FromError(readResult);
+ }
+
+ try
+ {
+ var content = (ReadFile<TContent>)readResult.Entity;
+ return content;
+ }
+ catch (Exception e)
+ {
+ return e;
+ }
+ }
+
+ /// <summary>
+ /// Read a file from a filesystem path.
+ /// </summary>
+ /// <param name="path">The path to the file.</param>
+ /// <typeparam name="TContent">The type of the content to assume.</typeparam>
+ /// <returns>The content or an error.</returns>
+ public Result<ReadFile<TContent>> ReadFileSystemFile<TContent>(string path)
+ {
+ try
+ {
+ var readData = File.ReadAllBytes(path);
+ return Read<TContent>(new RawFile(null, path, readData.Length, readData));
+ }
+ catch (Exception e)
+ {
+ return e;
+ }
+ }
+}<
\ No newline at end of file
A Data/NosSmooth.Data.NOSFiles/Readers/IFileTypeReader.cs => Data/NosSmooth.Data.NOSFiles/Readers/IFileTypeReader.cs +45 -0
@@ 0,0 1,45 @@
+//
+// IFileTypeReader.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Xml;
+using NosSmooth.Data.NOSFiles.Files;
+using Remora.Results;
+
+namespace NosSmooth.Data.NOSFiles.Readers;
+
+/// <summary>
+/// Reader of a particular file type.
+/// </summary>
+public interface IFileTypeReader
+{
+ /// <summary>
+ /// Checks whether the given raw file is supported by this reader.
+ /// </summary>
+ /// <param name="file">The file to check.</param>
+ /// <returns>Whether the file can be read by this reader.</returns>
+ public bool SupportsFile(RawFile file);
+
+ /// <summary>
+ /// Read the given file contents.
+ /// </summary>
+ /// <param name="file">The file to read.</param>
+ /// <returns>Contents of the file or an error.</returns>
+ public Result<object> Read(RawFile file);
+}
+
+/// <summary>
+/// Reader of a particular file type.
+/// </summary>
+/// <typeparam name="TContent">The content of the file.</typeparam>
+public interface IFileTypeReader<TContent> : IFileTypeReader
+{
+ /// <summary>
+ /// Read the given file contents.
+ /// </summary>
+ /// <param name="file">The file to read.</param>
+ /// <returns>Contents of the file or an error.</returns>
+ public Result<ReadFile<TContent>> ReadExact(RawFile file);
+}<
\ No newline at end of file
A Data/NosSmooth.Data.NOSFiles/Readers/Types/BaseFileTypeReader.cs => Data/NosSmooth.Data.NOSFiles/Readers/Types/BaseFileTypeReader.cs +32 -0
@@ 0,0 1,32 @@
+//
+// BaseFileTypeReader.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Data.NOSFiles.Files;
+using Remora.Results;
+
+namespace NosSmooth.Data.NOSFiles.Readers.Types;
+
+/// <inheritdoc />
+public abstract class BaseFileTypeReader<TContent> : IFileTypeReader<TContent>
+{
+ /// <inheritdoc />
+ public abstract Result<ReadFile<TContent>> ReadExact(RawFile file);
+
+ /// <inheritdoc />
+ public abstract bool SupportsFile(RawFile file);
+
+ /// <inheritdoc />
+ public Result<object> Read(RawFile file)
+ {
+ var readResult = ReadExact(file);
+ if (!readResult.IsSuccess)
+ {
+ return Result<object>.FromError(readResult);
+ }
+
+ return Result<object>.FromSuccess(readResult.Entity);
+ }
+}<
\ No newline at end of file
A Data/NosSmooth.Data.NOSFiles/Readers/Types/NosTextFileTypeReader.cs => Data/NosSmooth.Data.NOSFiles/Readers/Types/NosTextFileTypeReader.cs +99 -0
@@ 0,0 1,99 @@
+//
+// NosTextFileTypeReader.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Buffers;
+using System.Buffers.Binary;
+using System.Runtime.InteropServices;
+using System.Runtime.Serialization;
+using System.Text;
+using NosSmooth.Data.NOSFiles.Decryptors;
+using NosSmooth.Data.NOSFiles.Files;
+using Remora.Results;
+
+namespace NosSmooth.Data.NOSFiles.Readers.Types;
+
+/// <summary>
+/// Reader of .NOS files that contain text files.
+/// </summary>
+public class NosTextFileTypeReader : BaseFileTypeReader<FileArchive>
+{
+ private readonly IDecryptor _datDecryptor;
+ private readonly IDecryptor _lstDecryptor;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NosTextFileTypeReader"/> class.
+ /// </summary>
+ public NosTextFileTypeReader()
+ {
+ _datDecryptor = new DatDecryptor();
+ _lstDecryptor = new LstDecryptor();
+ }
+
+ /// <inheritdoc />
+ public override bool SupportsFile(RawFile file)
+ {
+ // Just checks that the number of the first file is zero.
+ // ?Should? be enough for excluding all other types.
+ // This is questionable. The thing is
+ // that there is not really an header for these files.
+ // TODO: maybe try to read till the first file name, size etc.
+ // and verify the name is a string, the number or the file is zero etc?
+ if (file.Length < 10)
+ {
+ return false;
+ }
+
+ return file.Path.EndsWith
+ ("NOS") && file.Content[4] == 1 && file.Content[5] == 0 && file.Content[6] == 0 && file.Content[7] == 0;
+ }
+
+ /// <inheritdoc />
+ public override Result<ReadFile<FileArchive>> ReadExact(RawFile file)
+ {
+ List<RawFile> files = new List<RawFile>();
+ ReadOnlySpan<byte> content = file.Content;
+ var filesCount = BinaryPrimitives.ReadInt32LittleEndian(content);
+ content = content.Slice(4); // skip file count
+
+ for (var i = 0; i < filesCount; i++)
+ {
+ var fileResult = ReadFile(ref content);
+ if (!fileResult.IsSuccess)
+ {
+ return Result<ReadFile<FileArchive>>.FromError(fileResult);
+ }
+
+ files.Add(fileResult.Entity);
+ }
+
+ // TODO: read time information?
+ return new ReadFile<FileArchive>(file.Path, new FileArchive(files));
+ }
+
+ private Result<RawFile> ReadFile(ref ReadOnlySpan<byte> content)
+ {
+ // skip file number, it is of no interest, I think.
+ content = content.Slice(4);
+ var stringNameLength = BinaryPrimitives.ReadInt32LittleEndian(content);
+ content = content.Slice(4);
+ var fileName = Encoding.ASCII.GetString(content.Slice(0, stringNameLength).ToArray());
+ content = content.Slice(stringNameLength);
+ var isDat = BinaryPrimitives.ReadInt32LittleEndian(content);
+ content = content.Slice(4);
+ var fileSize = BinaryPrimitives.ReadInt32LittleEndian(content);
+ content = content.Slice(4);
+ var fileContent = content.Slice(0, fileSize);
+ content = content.Slice(fileSize);
+
+ var datDecryptResult = isDat != 0 ? _datDecryptor.Decrypt(fileContent) : _lstDecryptor.Decrypt(fileContent);
+ if (!datDecryptResult.IsSuccess)
+ {
+ return Result<RawFile>.FromError(datDecryptResult);
+ }
+
+ return new RawFile(FileType.Text, fileName, fileSize, datDecryptResult.Entity);
+ }
+}<
\ No newline at end of file
A Data/NosSmooth.Data.NOSFiles/Readers/Types/NosZlibFileTypeReader.cs => Data/NosSmooth.Data.NOSFiles/Readers/Types/NosZlibFileTypeReader.cs +97 -0
@@ 0,0 1,97 @@
+//
+// NosZlibFileTypeReader.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Buffers;
+using System.Buffers.Binary;
+using System.Data.Common;
+using System.IO.Compression;
+using System.Text;
+using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
+using NosSmooth.Data.NOSFiles.Files;
+using Remora.Results;
+
+namespace NosSmooth.Data.NOSFiles.Readers.Types;
+
+/// <inheritdoc />
+public class NosZlibFileTypeReader : BaseFileTypeReader<FileArchive>
+{
+ /// <inheritdoc />
+ public override Result<ReadFile<FileArchive>> ReadExact(RawFile file)
+ {
+ using var fileStream = new MemoryStream(file.Content, false);
+ ReadOnlySpan<byte> contentFromStart = file.Content;
+ ReadOnlySpan<byte> content = contentFromStart;
+ content = content.Slice(16); // skip header
+ List<RawFile> files = new List<RawFile>();
+ var filesCount = BinaryPrimitives.ReadInt32LittleEndian(content);
+ content = content.Slice(4);
+
+ content = content.Slice(1); // separator
+ for (var i = 0; i < filesCount; i++)
+ {
+ var id = BinaryPrimitives.ReadInt32LittleEndian(content);
+ var offset = BinaryPrimitives.ReadInt32LittleEndian(content.Slice(4));
+ content = content.Slice(8);
+
+ fileStream.Seek(offset, SeekOrigin.Begin);
+ var readFileResult = ReadFile(id, fileStream, contentFromStart.Slice(offset));
+ if (!readFileResult.IsSuccess)
+ {
+ return Result<ReadFile<FileArchive>>.FromError(readFileResult);
+ }
+
+ files.Add(readFileResult.Entity);
+ }
+
+ return new ReadFile<FileArchive>(file.Path, new FileArchive(files));
+ }
+
+ /// <inheritdoc />
+ public override bool SupportsFile(RawFile file)
+ {
+ if (file.Length < 16)
+ {
+ return false;
+ }
+
+ var header = GetHeader(file.Content);
+ return header.StartsWith("NT Data 05");
+ }
+
+ private ReadOnlySpan<char> GetHeader(ReadOnlySpan<byte> content)
+ {
+ return Encoding.ASCII.GetString(content.Slice(0, 16));
+ }
+
+ private Result<RawFile> ReadFile(int id, MemoryStream stream, ReadOnlySpan<byte> content)
+ {
+ // int creationDate = BinaryPrimitives.ReadInt32LittleEndian(content);
+ content = content.Slice(4);
+ int dataSize = BinaryPrimitives.ReadInt32LittleEndian(content);
+ content = content.Slice(4);
+ int compressedDataSize = BinaryPrimitives.ReadInt32LittleEndian(content);
+ content = content.Slice(4);
+ bool isCompressed = content[0] != 0;
+ content = content.Slice(1);
+ ReadOnlySpan<byte> data = content.Slice(0, compressedDataSize);
+ byte[]? dataArray = null;
+
+ if (isCompressed)
+ {
+ stream.Seek(4 + 4 + 4 + 1, SeekOrigin.Current);
+ using var decompressionStream = new InflaterInputStream(stream)
+ {
+ IsStreamOwner = false
+ };
+ dataArray = new byte[dataSize];
+ using var outputStream = new MemoryStream(dataSize);
+ decompressionStream.CopyTo(outputStream);
+ }
+
+ // TODO: check that data size matches data.Length?
+ return new RawFile(FileType.Binary, id.ToString(), data.Length, dataArray ?? data.ToArray());
+ }
+}<
\ No newline at end of file