Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build-nkxtool.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ name: Build NkxTool Executable # Nom du workflow affiché sur GitHub
on:
push:
branches:
- main # Déclenche le workflow sur les pushes vers la branche 'main'
- dev-nki # Déclenche le workflow sur les pushes vers la branche 'main'
pull_request:
branches:
- main # Déclenche le workflow sur les pull requests vers la branche 'main'
- dev-nki # Déclenche le workflow sur les pull requests vers la branche 'main'

jobs:
build:
Expand Down
179 changes: 179 additions & 0 deletions nki/NkiChunkScanner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;

namespace NkiTool
{
public class NkiRegion
{
public string Tag = "";
public long Offset;
public int Size;
public byte[] Payload = Array.Empty<byte>();
public bool IsInflated;
public List<NkiRegion> Children = new();
public List<long> ZlibCandidateOffsets = new();
}

public static class NkiChunkScanner
{
// Fenêtre de recherche d'en-tête ZLIB (0x78 ..) à l'intérieur d'un buffer.
// 234 Ko de fichier -> on peut se permettre de scanner large sans souci de perf.
private const int ZlibSearchWindow = 1_000_000;

public static List<NkiRegion> Scan(byte[] data)
{
var regions = new List<NkiRegion>();
TryScanAsChunks(data, 0, data.Length, regions);
return regions;
}

private static void TryScanAsChunks(byte[] data, int start, int end, List<NkiRegion> outRegions)
{
int pos = start;
while (pos + 8 <= end)
{
string tag = SafeAscii(data, pos, 4);
uint size = BitConverter.ToUInt32(data, pos + 4);

if (size == 0 || pos + 8 + size > end || size > 200_000_000)
{
var opaque = new NkiRegion
{
Tag = "RAW",
Offset = pos,
Size = end - pos,
Payload = Slice(data, pos, end - pos)
};

// Correctif : on tente quand même de trouver et décompresser
// un flux ZLIB n'importe où dans ce bloc opaque, plutôt que
// d'abandonner silencieusement.
TryInflateAndRecurse(opaque);
ScanForKnownMarkers(opaque);

outRegions.Add(opaque);
return;
}

var region = new NkiRegion
{
Tag = tag,
Offset = pos,
Size = (int)size,
Payload = Slice(data, pos + 8, (int)size)
};

TryInflateAndRecurse(region);
outRegions.Add(region);

pos += 8 + (int)size;
}

if (pos < end)
{
var tail = new NkiRegion
{
Tag = "TAIL",
Offset = pos,
Size = end - pos,
Payload = Slice(data, pos, end - pos)
};
TryInflateAndRecurse(tail);
outRegions.Add(tail);
}
}

private static void TryInflateAndRecurse(NkiRegion region)
{
var (inflated, foundAtOffset) = TryZlibInflateAnywhere(region.Payload);
if (inflated != null)
{
region.IsInflated = true;
region.ZlibCandidateOffsets.Add(region.Offset + foundAtOffset);
TryScanAsChunks(inflated, 0, inflated.Length, region.Children);
region.Payload = inflated;
}
}

/// <summary>
/// Recherche un en-tête ZLIB (0x78 suivi d'un second octet plausible)
/// n'importe où dans le buffer (pas seulement au début), sur une fenêtre
/// raisonnable, et tente une décompression à chaque candidat trouvé.
/// </summary>
public static (byte[]? data, int offset) TryZlibInflateAnywhere(byte[] buffer)
{
int limit = Math.Min(buffer.Length - 2, ZlibSearchWindow);
for (int offset = 0; offset < limit; offset++)
{
if (buffer[offset] != 0x78) continue;

byte second = buffer[offset + 1];
// Bytes valides usuels après 0x78 pour un flux zlib : 0x01, 0x5E, 0x9C, 0xDA
if (second != 0x01 && second != 0x5E && second != 0x9C && second != 0xDA) continue;

try
{
using var input = new MemoryStream(buffer, offset, buffer.Length - offset);
using var zlib = new ZLibStream(input, CompressionMode.Decompress);
using var output = new MemoryStream();
zlib.CopyTo(output);
var result = output.ToArray();
if (result.Length > 0) return (result, offset);
}
catch
{
// pas un flux valide à cet offset, on continue
}
}
return (null, -1);
}

private static readonly string[] KnownMarkers = { "hsin", "DSIN", "2SAM", "PRES", "PROG", "PLST", "FNTB", "PARS" };

/// <summary>
/// Recherche des tags/marqueurs connus (issus de la documentation
/// communautaire du format NI DSIN) n'importe où dans un bloc, pour
/// aider à localiser la structure même sans specs officielles.
/// </summary>
private static void ScanForKnownMarkers(NkiRegion region)
{
foreach (var marker in KnownMarkers)
{
var markerBytes = Encoding.ASCII.GetBytes(marker);
for (int i = 0; i + markerBytes.Length <= region.Payload.Length; i++)
{
bool match = true;
for (int j = 0; j < markerBytes.Length; j++)
{
if (region.Payload[i + j] != markerBytes[j]) { match = false; break; }
}
if (match)
{
region.ZlibCandidateOffsets.Add(-(region.Offset + i)); // négatif = marqueur texte, pas zlib
}
}
}
}

private static string SafeAscii(byte[] data, int offset, int len)
{
var sb = new StringBuilder();
for (int i = 0; i < len; i++)
{
byte b = data[offset + i];
sb.Append(b >= 32 && b < 127 ? (char)b : '.');
}
return sb.ToString();
}

private static byte[] Slice(byte[] data, int offset, int len)
{
var result = new byte[len];
Array.Copy(data, offset, result, 0, len);
return result;
}
}
}
101 changes: 101 additions & 0 deletions nki/NkiDumpCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System;
using System.IO;
using System.Text;

namespace NkiTool
{
public static class NkiDumpCommand
{
public static int Run(string path, string? outPath)
{
byte[] data = File.ReadAllBytes(path);
var sb = new StringBuilder();

sb.AppendLine($"Fichier: {path}");
sb.AppendLine($"Taille: {data.Length} octets");
sb.AppendLine($"En-tête (16 premiers octets, hex): {BitConverter.ToString(data, 0, Math.Min(16, data.Length))}");
sb.AppendLine();

sb.AppendLine("=== Arborescence de chunks détectée ===");
var regions = NkiChunkScanner.Scan(data);
DumpRegions(regions, 0, sb);

sb.AppendLine();
sb.AppendLine("=== Références d'échantillons trouvées (.wav / .ncw) ===");
var found = StringExtractor.FindSampleReferences(data);
foreach (var f in found)
{
sb.AppendLine($" offset={f.Offset,-10} encodage={f.Encoding,-8} valeur=\"{f.Value}\"");
}

sb.AppendLine();
sb.AppendLine("=== Scan récursif complémentaire dans chaque région (y compris blocs décompressés) ===");
ScanRegionsForStrings(regions, sb);

int totalFound = found.Count + CountNestedStrings(regions);
if (totalFound == 0)
{
sb.AppendLine();
sb.AppendLine("AUCUNE référence .wav/.ncw trouvée nulle part (racine + sous-blocs décompressés).");
sb.AppendLine("Voir la liste des marqueurs/offsets ZLIB candidats ci-dessus pour diagnostiquer.");
}

string report = sb.ToString();
Console.WriteLine(report);

if (outPath != null)
{
File.WriteAllText(outPath, report);
Console.WriteLine($"\nRapport écrit dans: {outPath}");
}

return 0;
}

private static void DumpRegions(System.Collections.Generic.List<NkiRegion> regions, int depth, StringBuilder sb)
{
string indent = new string(' ', depth * 2);
foreach (var r in regions)
{
string flag = r.IsInflated ? " [ZLIB -> décompressé]" : "";
sb.AppendLine($"{indent}- tag=\"{r.Tag}\" offset={r.Offset} taille={r.Size}{flag}");

foreach (var candidate in r.ZlibCandidateOffsets)
{
if (candidate >= 0)
sb.AppendLine($"{indent} [candidat ZLIB trouvé à l'offset absolu {candidate}]");
else
sb.AppendLine($"{indent} [marqueur texte connu trouvé à l'offset absolu {-candidate}]");
}

if (r.Children.Count > 0)
DumpRegions(r.Children, depth + 1, sb);
}
}

private static void ScanRegionsForStrings(System.Collections.Generic.List<NkiRegion> regions, StringBuilder sb)
{
foreach (var r in regions)
{
var found = StringExtractor.FindSampleReferences(r.Payload, r.Offset);
foreach (var f in found)
{
sb.AppendLine($" [chunk \"{r.Tag}\"{(r.IsInflated ? " décompressé" : "")}] offset={f.Offset,-10} encodage={f.Encoding,-8} valeur=\"{f.Value}\"");
}
if (r.Children.Count > 0)
ScanRegionsForStrings(r.Children, sb);
}
}

private static int CountNestedStrings(System.Collections.Generic.List<NkiRegion> regions)
{
int count = 0;
foreach (var r in regions)
{
count += StringExtractor.FindSampleReferences(r.Payload, r.Offset).Count;
count += CountNestedStrings(r.Children);
}
return count;
}
}
}
Loading
Loading