~ruther/NosSmooth

e0b8687280cdb96a7eefdf11c6a3df0de5f03d82 — František Boháček 3 years ago eb8b44e
feat(data): add .NOS file readers
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

Do not follow this link