From 07d09ca06226b7e3f6273f7270035a8139c679d2 Mon Sep 17 00:00:00 2001 From: manzing Date: Fri, 7 Aug 2026 10:29:01 +0200 Subject: [PATCH 01/13] Change workflow triggers to 'dev-nki' branch Updated the workflow to trigger on the 'dev-nki' branch instead of 'main'. --- .github/workflows/build-nkxtool.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-nkxtool.yml b/.github/workflows/build-nkxtool.yml index b6874a1..f49ae9c 100644 --- a/.github/workflows/build-nkxtool.yml +++ b/.github/workflows/build-nkxtool.yml @@ -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: From 74ef3e91cf7d3c32b06ddac153c936259af5acf3 Mon Sep 17 00:00:00 2001 From: manzing Date: Fri, 7 Aug 2026 10:30:56 +0200 Subject: [PATCH 02/13] Add nki support files --- nki/NkiChunkScanner.cs | 149 +++++++++++++++++++++++++++++++++++++++++ nki/StringExtractor.cs | 88 ++++++++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 nki/NkiChunkScanner.cs create mode 100644 nki/StringExtractor.cs diff --git a/nki/NkiChunkScanner.cs b/nki/NkiChunkScanner.cs new file mode 100644 index 0000000..986ba6d --- /dev/null +++ b/nki/NkiChunkScanner.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Text; + +namespace NkiTool +{ + /// + /// Représente une région de données identifiée dans le fichier NKI : + /// soit un chunk brut (tag + taille + payload), soit un bloc obtenu + /// après décompression ZLIB d'un chunk parent. + /// + public class NkiRegion + { + public string Tag = ""; + public long Offset; + public int Size; + public byte[] Payload = Array.Empty(); + public bool IsInflated; + public List Children = new(); + } + + /// + /// Scanner générique et défensif : il ne présuppose PAS la structure exacte + /// du format NKI (tags, tailles de champs). Il tente une lecture de type + /// "conteneur de chunks" (tag ASCII 4 octets + taille UInt32 LE + payload), + /// et détecte automatiquement les blocs ZLIB imbriqués pour les décompresser + /// et les ré-analyser récursivement. Le but est de VALIDER la structure + /// réelle sur vos fichiers avant d'écrire la logique de patch. + /// + public static class NkiChunkScanner + { + public static List Scan(byte[] data) + { + var regions = new List(); + TryScanAsChunks(data, 0, data.Length, regions); + return regions; + } + + private static void TryScanAsChunks(byte[] data, int start, int end, List outRegions) + { + int pos = start; + while (pos + 8 <= end) + { + string tag = SafeAscii(data, pos, 4); + uint size = BitConverter.ToUInt32(data, pos + 4); + + // Garde-fou : si la taille annoncée est aberrante, on abandonne + // l'hypothèse "chunk" à partir d'ici et on traite le reste comme + // un bloc opaque (permet de ne pas planter sur un format différent). + 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) + }; + 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) + { + outRegions.Add(new NkiRegion + { + Tag = "TAIL", + Offset = pos, + Size = end - pos, + Payload = Slice(data, pos, end - pos) + }); + } + } + + private static void TryInflateAndRecurse(NkiRegion region) + { + var inflated = TryZlibInflate(region.Payload); + if (inflated != null) + { + region.IsInflated = true; + TryScanAsChunks(inflated, 0, inflated.Length, region.Children); + // Remplace le payload affiché par la version décompressée pour + // que le scan de chaînes (StringScanner) l'exploite aussi. + region.Payload = inflated; + } + } + + /// + /// Tente une décompression ZLIB à partir de n'importe quel offset où + /// l'en-tête ZLIB (0x78 ..) est détecté, pas uniquement au début du buffer. + /// + public static byte[]? TryZlibInflate(byte[] buffer) + { + for (int offset = 0; offset < Math.Min(buffer.Length, 16); offset++) + { + if (buffer[offset] != 0x78) 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; + } + catch + { + // Pas un flux ZLIB valide à cet offset, on continue. + } + } + return null; + } + + 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; + } + } +} diff --git a/nki/StringExtractor.cs b/nki/StringExtractor.cs new file mode 100644 index 0000000..1a8cbae --- /dev/null +++ b/nki/StringExtractor.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace NkiTool +{ + public record FoundString(long Offset, string Encoding, string Value); + + /// + /// Extrait toutes les chaînes plausibles (ASCII et UTF-16LE) contenant + /// ".wav" ou ".ncw" dans un buffer, avec leur offset absolu. + /// Objectif : localiser empiriquement, sur vos vrais fichiers, où et + /// comment sont stockés les noms d'échantillons (encodage, longueur de + /// champ fixe ou variable, présence d'un chemin complet ou du nom seul). + /// + public static class StringExtractor + { + private static readonly string[] Needles = { ".wav", ".WAV", ".ncw", ".NCW" }; + + public static List FindSampleReferences(byte[] data, long baseOffset = 0) + { + var results = new List(); + ScanAscii(data, baseOffset, results); + ScanUtf16Le(data, baseOffset, results); + return results; + } + + private static void ScanAscii(byte[] data, long baseOffset, List results) + { + var sb = new StringBuilder(); + long stringStart = 0; + for (int i = 0; i < data.Length; i++) + { + byte b = data[i]; + if (b >= 32 && b < 127) + { + if (sb.Length == 0) stringStart = i; + sb.Append((char)b); + } + else + { + FlushIfMatch(sb, stringStart, baseOffset, "ASCII", results); + sb.Clear(); + } + } + FlushIfMatch(sb, stringStart, baseOffset, "ASCII", results); + } + + private static void ScanUtf16Le(byte[] data, long baseOffset, List results) + { + var sb = new StringBuilder(); + long stringStart = 0; + int i = 0; + while (i + 1 < data.Length) + { + char c = (char)(data[i] | (data[i + 1] << 8)); + bool printable = c >= 32 && c < 127; + if (printable) + { + if (sb.Length == 0) stringStart = i; + sb.Append(c); + i += 2; + } + else + { + FlushIfMatch(sb, stringStart, baseOffset, "UTF16LE", results); + sb.Clear(); + i += 1; // décalage impair pour ne pas rater un flux mal aligné + } + } + FlushIfMatch(sb, stringStart, baseOffset, "UTF16LE", results); + } + + private static void FlushIfMatch(StringBuilder sb, long stringStart, long baseOffset, string encoding, List results) + { + if (sb.Length < 5) return; + string s = sb.ToString(); + foreach (var needle in Needles) + { + if (s.Contains(needle)) + { + results.Add(new FoundString(baseOffset + stringStart, encoding, s)); + break; + } + } + } + } +} From 34640b427cd177f8024e2dc225c2deddbe12ba30 Mon Sep 17 00:00:00 2001 From: manzing Date: Fri, 7 Aug 2026 10:46:25 +0200 Subject: [PATCH 03/13] Update program.cs --- program.cs | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/program.cs b/program.cs index 0f26864..d3971f5 100644 --- a/program.cs +++ b/program.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Xml; using System.Text.RegularExpressions; - +using NkiTool; public class Program { private const string PluginDllName = "inNKX.wcx64"; @@ -182,6 +182,32 @@ private static int RunTool(string[] args) return UpdateUserDb(customXmlPath); } + // Commande dump : diagnostic lecture seule d'un fichier NKI + if (operation == "dump") + { + if (argsList.Count < 2) + { + Console.WriteLine("Usage: NkxTool dump [--out rapport.txt]"); + return 1; + } + + string nkiPath = Path.GetFullPath(argsList[1]); + if (!File.Exists(nkiPath)) + { + Console.WriteLine($"Error: The source file '{nkiPath}' does not exist."); + return 1; + } + + string? outReport = null; + int outIndex = argsList.IndexOf("--out"); + if (outIndex >= 0 && outIndex + 1 < argsList.Count) + { + outReport = Path.GetFullPath(argsList[outIndex + 1]); + } + + return NkiDumpCommand.Run(nkiPath, outReport); + } + if (argsList.Count < 2) { ShowUsage(); @@ -196,6 +222,65 @@ private static int RunTool(string[] args) return 1; } + try + { + PackDefaultParamStruct dps = new PackDefaultParamStruct(); + dps.size = Marshal.SizeOf(typeof(PackDefaultParamStruct)); + dps.PluginInterfaceVersionLow = 1; + dps.PluginInterfaceVersionHi = 2; + dps.DefaultIniName = Path.Combine(exeDirectory, "inNKX.ini"); + PackSetDefaultParams(ref dps); + } + catch { } + + try + { + if (operation == "list") + { + string? outList = argsList.Count >= 3 ? Path.GetFullPath(argsList[2]) : null; + return ListArchive(path1, outList); + } + else if (operation == "unpack" && argsList.Count >= 3) + { + string destinationFolder = Path.GetFullPath(argsList[2]); + HashSet? selectedFiles = null; + + if (argsList.Count >= 4 && argsList[3].StartsWith("@")) + { + string listFile = argsList[3].Substring(1); + if (File.Exists(listFile)) + { + var lines = File.ReadAllLines(listFile); + selectedFiles = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var line in lines) + { + if (!string.IsNullOrWhiteSpace(line)) + selectedFiles.Add(line.Trim()); + } + } + } + + return DecompressArchive(path1, destinationFolder, selectedFiles, overwrite); + } + else if (operation == "pack" && argsList.Count >= 3) + { + string rootPath = argsList.Count >= 4 ? Path.GetFullPath(argsList[3]) : ""; + return CompressFolder(argsList[2], path1, rootPath); + } + else + { + Console.WriteLine("Invalid operation or missing arguments."); + ShowUsage(); + return 1; + } + } + catch (Exception ex) + { + Console.WriteLine($"Critical Error: {ex.Message}"); + return 1; + } +} + try { PackDefaultParamStruct dps = new PackDefaultParamStruct(); @@ -262,6 +347,7 @@ private static void ShowUsage() Console.WriteLine(" NkxTool pack [rootPath]"); Console.WriteLine(" NkxTool list [outputList.txt]"); Console.WriteLine(" NkxTool update [-f ]"); + Console.WriteLine(" NkxTool dump [--out rapport.txt]"); Console.WriteLine(); Console.WriteLine("Examples:"); Console.WriteLine(" NkxTool unpack archive.nkx output_folder"); From ca303bbed225787f96d2a592a1dfbc64d9ec2fc5 Mon Sep 17 00:00:00 2001 From: manzing Date: Fri, 7 Aug 2026 10:49:55 +0200 Subject: [PATCH 04/13] Update program.cs --- program.cs | 59 ------------------------------------------------------ 1 file changed, 59 deletions(-) diff --git a/program.cs b/program.cs index d3971f5..1a4f68b 100644 --- a/program.cs +++ b/program.cs @@ -222,65 +222,6 @@ private static int RunTool(string[] args) return 1; } - try - { - PackDefaultParamStruct dps = new PackDefaultParamStruct(); - dps.size = Marshal.SizeOf(typeof(PackDefaultParamStruct)); - dps.PluginInterfaceVersionLow = 1; - dps.PluginInterfaceVersionHi = 2; - dps.DefaultIniName = Path.Combine(exeDirectory, "inNKX.ini"); - PackSetDefaultParams(ref dps); - } - catch { } - - try - { - if (operation == "list") - { - string? outList = argsList.Count >= 3 ? Path.GetFullPath(argsList[2]) : null; - return ListArchive(path1, outList); - } - else if (operation == "unpack" && argsList.Count >= 3) - { - string destinationFolder = Path.GetFullPath(argsList[2]); - HashSet? selectedFiles = null; - - if (argsList.Count >= 4 && argsList[3].StartsWith("@")) - { - string listFile = argsList[3].Substring(1); - if (File.Exists(listFile)) - { - var lines = File.ReadAllLines(listFile); - selectedFiles = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var line in lines) - { - if (!string.IsNullOrWhiteSpace(line)) - selectedFiles.Add(line.Trim()); - } - } - } - - return DecompressArchive(path1, destinationFolder, selectedFiles, overwrite); - } - else if (operation == "pack" && argsList.Count >= 3) - { - string rootPath = argsList.Count >= 4 ? Path.GetFullPath(argsList[3]) : ""; - return CompressFolder(argsList[2], path1, rootPath); - } - else - { - Console.WriteLine("Invalid operation or missing arguments."); - ShowUsage(); - return 1; - } - } - catch (Exception ex) - { - Console.WriteLine($"Critical Error: {ex.Message}"); - return 1; - } -} - try { PackDefaultParamStruct dps = new PackDefaultParamStruct(); From 164c650fbcc327c13c80a18d841613571699e63c Mon Sep 17 00:00:00 2001 From: manzing Date: Fri, 7 Aug 2026 10:51:47 +0200 Subject: [PATCH 05/13] Create NkiDumpCommand.cs --- nki/NkiDumpCommand.cs | 82 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 nki/NkiDumpCommand.cs diff --git a/nki/NkiDumpCommand.cs b/nki/NkiDumpCommand.cs new file mode 100644 index 0000000..f289df8 --- /dev/null +++ b/nki/NkiDumpCommand.cs @@ -0,0 +1,82 @@ +using System; +using System.IO; +using System.Text; + +namespace NkiTool +{ + /// + /// Implémentation de la commande "dump" appelée depuis le dispatch + /// principal de program.cs. Lecture seule, ne modifie jamais le fichier. + /// + 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); + if (found.Count == 0) + { + sb.AppendLine("Aucune référence trouvée en clair au niveau racine."); + sb.AppendLine("(Normal si les données sont compressées en ZLIB : voir les sous-blocs 'inflated' ci-dessus.)"); + } + 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 ==="); + ScanRegionsForStrings(regions, sb); + + 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 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}"); + if (r.Children.Count > 0) + DumpRegions(r.Children, depth + 1, sb); + } + } + + private static void ScanRegionsForStrings(System.Collections.Generic.List 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}\"] offset={f.Offset,-10} encodage={f.Encoding,-8} valeur=\"{f.Value}\""); + } + if (r.Children.Count > 0) + ScanRegionsForStrings(r.Children, sb); + } + } + } +} From 2d48feb2c4314ac8667ac0bbec317aee1cb7bf86 Mon Sep 17 00:00:00 2001 From: manzing Date: Fri, 7 Aug 2026 11:09:01 +0200 Subject: [PATCH 06/13] Update NkiChunkScanner.cs --- nki/NkiChunkScanner.cs | 86 ++++++++++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 28 deletions(-) diff --git a/nki/NkiChunkScanner.cs b/nki/NkiChunkScanner.cs index 986ba6d..9f19565 100644 --- a/nki/NkiChunkScanner.cs +++ b/nki/NkiChunkScanner.cs @@ -6,11 +6,6 @@ namespace NkiTool { - /// - /// Représente une région de données identifiée dans le fichier NKI : - /// soit un chunk brut (tag + taille + payload), soit un bloc obtenu - /// après décompression ZLIB d'un chunk parent. - /// public class NkiRegion { public string Tag = ""; @@ -19,18 +14,15 @@ public class NkiRegion public byte[] Payload = Array.Empty(); public bool IsInflated; public List Children = new(); + public List ZlibCandidateOffsets = new(); } - /// - /// Scanner générique et défensif : il ne présuppose PAS la structure exacte - /// du format NKI (tags, tailles de champs). Il tente une lecture de type - /// "conteneur de chunks" (tag ASCII 4 octets + taille UInt32 LE + payload), - /// et détecte automatiquement les blocs ZLIB imbriqués pour les décompresser - /// et les ré-analyser récursivement. Le but est de VALIDER la structure - /// réelle sur vos fichiers avant d'écrire la logique de patch. - /// 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 Scan(byte[] data) { var regions = new List(); @@ -46,9 +38,6 @@ private static void TryScanAsChunks(byte[] data, int start, int end, List end || size > 200_000_000) { var opaque = new NkiRegion @@ -58,6 +47,13 @@ private static void TryScanAsChunks(byte[] data, int start, int end, List - /// Tente une décompression ZLIB à partir de n'importe quel offset où - /// l'en-tête ZLIB (0x78 ..) est détecté, pas uniquement au début du buffer. + /// 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é. /// - public static byte[]? TryZlibInflate(byte[] buffer) + public static (byte[]? data, int offset) TryZlibInflateAnywhere(byte[] buffer) { - for (int offset = 0; offset < Math.Min(buffer.Length, 16); offset++) + 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); @@ -118,14 +121,41 @@ private static void TryInflateAndRecurse(NkiRegion region) using var output = new MemoryStream(); zlib.CopyTo(output); var result = output.ToArray(); - if (result.Length > 0) return result; + if (result.Length > 0) return (result, offset); } catch { - // Pas un flux ZLIB valide à cet offset, on continue. + // pas un flux valide à cet offset, on continue + } + } + return (null, -1); + } + + private static readonly string[] KnownMarkers = { "hsin", "DSIN", "2SAM", "PRES", "PROG", "PLST", "FNTB", "PARS" }; + + /// + /// 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. + /// + 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 + } } } - return null; } private static string SafeAscii(byte[] data, int offset, int len) From 02ec48e18ac8bed5eaecac3b0bc65c2534863e89 Mon Sep 17 00:00:00 2001 From: manzing Date: Fri, 7 Aug 2026 11:09:51 +0200 Subject: [PATCH 07/13] Update NkiDumpCommand.cs --- nki/NkiDumpCommand.cs | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/nki/NkiDumpCommand.cs b/nki/NkiDumpCommand.cs index f289df8..e22867f 100644 --- a/nki/NkiDumpCommand.cs +++ b/nki/NkiDumpCommand.cs @@ -4,10 +4,6 @@ namespace NkiTool { - /// - /// Implémentation de la commande "dump" appelée depuis le dispatch - /// principal de program.cs. Lecture seule, ne modifie jamais le fichier. - /// public static class NkiDumpCommand { public static int Run(string path, string? outPath) @@ -27,20 +23,23 @@ public static int Run(string path, string? outPath) sb.AppendLine(); sb.AppendLine("=== Références d'échantillons trouvées (.wav / .ncw) ==="); var found = StringExtractor.FindSampleReferences(data); - if (found.Count == 0) - { - sb.AppendLine("Aucune référence trouvée en clair au niveau racine."); - sb.AppendLine("(Normal si les données sont compressées en ZLIB : voir les sous-blocs 'inflated' ci-dessus.)"); - } 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 ==="); + 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); @@ -60,6 +59,15 @@ private static void DumpRegions(System.Collections.Generic.List regio { 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); } @@ -72,11 +80,22 @@ private static void ScanRegionsForStrings(System.Collections.Generic.List 0) ScanRegionsForStrings(r.Children, sb); } } + + private static int CountNestedStrings(System.Collections.Generic.List regions) + { + int count = 0; + foreach (var r in regions) + { + count += StringExtractor.FindSampleReferences(r.Payload, r.Offset).Count; + count += CountNestedStrings(r.Children); + } + return count; + } } -} +} \ No newline at end of file From 0d3686772bac107f0ad79dd1cb4ff506268fb132 Mon Sep 17 00:00:00 2001 From: manzing Date: Sat, 8 Aug 2026 10:20:23 +0200 Subject: [PATCH 08/13] Update program.cs --- program.cs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/program.cs b/program.cs index 1a4f68b..e6c6f49 100644 --- a/program.cs +++ b/program.cs @@ -207,6 +207,37 @@ private static int RunTool(string[] args) return NkiDumpCommand.Run(nkiPath, outReport); } + if (operation == "nki-versionscan") + { + if (argsList.Count < 2) + { + Console.WriteLine("Usage: NkxTool nki-versionscan [--length N] [--out rapport.txt]"); + return 1; + } + + string nkiPath = Path.GetFullPath(argsList[1]); + if (!File.Exists(nkiPath)) + { + Console.WriteLine($"Error: The source file '{nkiPath}' does not exist."); + return 1; + } + + int scanLength = 4096; + int lenIndex = argsList.IndexOf("--length"); + if (lenIndex >= 0 && lenIndex + 1 < argsList.Count) + { + int.TryParse(argsList[lenIndex + 1], out scanLength); + } + + string? outReport = null; + int outIndex = argsList.IndexOf("--out"); + if (outIndex >= 0 && outIndex + 1 < argsList.Count) + { + outReport = Path.GetFullPath(argsList[outIndex + 1]); + } + + return NkiVersionScan.Run(nkiPath, scanLength, outReport); + } if (argsList.Count < 2) { From a3c555b85b84221ad3bd499c65601851622457a3 Mon Sep 17 00:00:00 2001 From: manzing Date: Sat, 8 Aug 2026 10:25:37 +0200 Subject: [PATCH 09/13] Create NkiVersionScan.cs --- nki/NkiVersionScan.cs | 185 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 nki/NkiVersionScan.cs diff --git a/nki/NkiVersionScan.cs b/nki/NkiVersionScan.cs new file mode 100644 index 0000000..e33552e --- /dev/null +++ b/nki/NkiVersionScan.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace NkiTool +{ + public static class NkiVersionScan + { + private static readonly Regex VersionPattern = + new Regex(@"\b\d{1,3}\.\d{1,3}(\.\d{1,5}){0,2}\b", RegexOptions.Compiled); + + public static int Run(string path, int scanLength, string? outPath) + { + byte[] data = File.ReadAllBytes(path); + int limit = Math.Min(scanLength, data.Length); + var sb = new StringBuilder(); + + sb.AppendLine($"Fichier: {path}"); + sb.AppendLine($"Taille totale: {data.Length} octets"); + sb.AppendLine($"Zone scannée: 0 à {limit} (sur {scanLength} demandés)"); + sb.AppendLine(); + + sb.AppendLine("=== Chaînes candidates de version (ASCII) ==="); + var asciiHits = ScanAscii(data, limit); + foreach (var hit in asciiHits) + sb.AppendLine($" offset={hit.Offset,-6} valeur=\"{hit.Value}\" (contexte: \"{hit.Context}\")"); + if (asciiHits.Count == 0) sb.AppendLine(" (aucune)"); + + sb.AppendLine(); + sb.AppendLine("=== Chaînes candidates de version (UTF-16LE) ==="); + var utf16Hits = ScanUtf16Le(data, limit); + foreach (var hit in utf16Hits) + sb.AppendLine($" offset={hit.Offset,-6} valeur=\"{hit.Value}\" (contexte: \"{hit.Context}\")"); + if (utf16Hits.Count == 0) sb.AppendLine(" (aucune)"); + + sb.AppendLine(); + sb.AppendLine("=== Toutes les chaînes lisibles trouvées (>=4 caractères), pour contexte manuel ==="); + foreach (var s in ExtractAllPrintableStrings(data, limit, "ASCII")) + sb.AppendLine($" [ASCII] offset={s.Offset,-6} \"{s.Value}\""); + foreach (var s in ExtractAllPrintableStrings(data, limit, "UTF16LE")) + sb.AppendLine($" [UTF16LE] offset={s.Offset,-6} \"{s.Value}\""); + + string report = sb.ToString(); + Console.WriteLine(report); + + if (outPath != null) + { + File.WriteAllText(outPath, report); + Console.WriteLine($"\nRapport écrit dans: {outPath}"); + } + + return 0; + } + + private record Hit(long Offset, string Value, string Context); + + private static List ScanAscii(byte[] data, int limit) + { + var hits = new List(); + var sb = new StringBuilder(); + long start = 0; + + for (int i = 0; i < limit; i++) + { + byte b = data[i]; + bool printable = b >= 32 && b < 127; + if (printable) + { + if (sb.Length == 0) start = i; + sb.Append((char)b); + } + else + { + FlushAsciiCandidate(sb, start, hits); + sb.Clear(); + } + } + FlushAsciiCandidate(sb, start, hits); + return hits; + } + + private static void FlushAsciiCandidate(StringBuilder sb, long start, List hits) + { + if (sb.Length < 3) return; + string s = sb.ToString(); + foreach (Match m in VersionPattern.Matches(s)) + { + hits.Add(new Hit(start + m.Index, m.Value, s)); + } + } + + private static List ScanUtf16Le(byte[] data, int limit) + { + var hits = new List(); + var sb = new StringBuilder(); + long start = 0; + int i = 0; + + while (i + 1 < limit) + { + char c = (char)(data[i] | (data[i + 1] << 8)); + bool printable = c >= 32 && c < 127; + if (printable) + { + if (sb.Length == 0) start = i; + sb.Append(c); + i += 2; + } + else + { + FlushUtf16Candidate(sb, start, hits); + sb.Clear(); + i += 1; + } + } + FlushUtf16Candidate(sb, start, hits); + return hits; + } + + private static void FlushUtf16Candidate(StringBuilder sb, long start, List hits) + { + if (sb.Length < 3) return; + string s = sb.ToString(); + foreach (Match m in VersionPattern.Matches(s)) + { + hits.Add(new Hit(start + m.Index * 2, m.Value, s)); + } + } + + private record StringHit(long Offset, string Value); + + private static List ExtractAllPrintableStrings(byte[] data, int limit, string encoding) + { + var results = new List(); + var sb = new StringBuilder(); + long start = 0; + + if (encoding == "ASCII") + { + for (int i = 0; i < limit; i++) + { + byte b = data[i]; + bool printable = b >= 32 && b < 127; + if (printable) + { + if (sb.Length == 0) start = i; + sb.Append((char)b); + } + else + { + if (sb.Length >= 4) results.Add(new StringHit(start, sb.ToString())); + sb.Clear(); + } + } + if (sb.Length >= 4) results.Add(new StringHit(start, sb.ToString())); + } + else + { + int i = 0; + while (i + 1 < limit) + { + char c = (char)(data[i] | (data[i + 1] << 8)); + bool printable = c >= 32 && c < 127; + if (printable) + { + if (sb.Length == 0) start = i; + sb.Append(c); + i += 2; + } + else + { + if (sb.Length >= 4) results.Add(new StringHit(start, sb.ToString())); + sb.Clear(); + i += 1; + } + } + if (sb.Length >= 4) results.Add(new StringHit(start, sb.ToString())); + } + + return results; + } + } +} \ No newline at end of file From 7d2210ccdfc9309fd06c3db21a642d6a7cdbdea7 Mon Sep 17 00:00:00 2001 From: manzing Date: Sat, 8 Aug 2026 10:55:13 +0200 Subject: [PATCH 10/13] Add nki instrument version command --- nki/NkiVersionCommand.cs | 120 +++++++++++++++++++++++++++++++++++++++ program.cs | 19 +++++++ 2 files changed, 139 insertions(+) create mode 100644 nki/NkiVersionCommand.cs diff --git a/nki/NkiVersionCommand.cs b/nki/NkiVersionCommand.cs new file mode 100644 index 0000000..0161b21 --- /dev/null +++ b/nki/NkiVersionCommand.cs @@ -0,0 +1,120 @@ +using System; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace NkiTool +{ + /// + /// Commande dédiée : extrait et affiche UNIQUEMENT le numéro de version + /// minimale requise, tel que trouvé en UTF-16LE dans la zone claire + /// de l'en-tête du NKI (empiriquement observé à l'offset 389 sur les + /// deux échantillons testés, mais recherché dynamiquement pour rester + /// robuste si la position varie légèrement selon les métadonnées). + /// + /// Sortie sur stdout : uniquement la valeur (ex: "8.0.0.0"), rien d'autre, + /// pour un usage direct dans un script PowerShell : + /// $version = & NkxTool.exe nki-version "Instrument.nki" + /// + /// Code de retour : 0 si trouvé, 1 si non trouvé ou erreur. + /// + public static class NkiVersionCommand + { + private static readonly Regex VersionPattern = + new Regex(@"\b\d{1,3}\.\d{1,3}(\.\d{1,5}){0,2}\b", RegexOptions.Compiled); + + private const int ScanLength = 4096; + private const int PreferredOffsetMin = 300; + private const int PreferredOffsetMax = 500; + + public static int Run(string path, bool verbose) + { + byte[] data; + try + { + data = File.ReadAllBytes(path); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error: impossible de lire le fichier ({ex.Message})"); + return 1; + } + + int limit = Math.Min(ScanLength, data.Length); + var candidates = ScanUtf16Le(data, limit); + + if (candidates.Count == 0) + { + Console.Error.WriteLine("Error: aucune chaîne de version trouvée dans les 4096 premiers octets."); + return 1; + } + + // Priorité : un candidat situé dans la fenêtre 300-500, cohérente + // avec les deux échantillons observés (offset 389 dans les deux cas). + (long Offset, string Value)? best = null; + foreach (var c in candidates) + { + if (c.Offset >= PreferredOffsetMin && c.Offset <= PreferredOffsetMax) + { + best = c; + break; + } + } + + // Repli : premier candidat trouvé, si rien dans la fenêtre préférée. + best ??= candidates[0]; + + if (verbose) + { + Console.Error.WriteLine($"[diagnostic] {candidates.Count} candidat(s) trouvé(s) au total."); + foreach (var c in candidates) + { + string marker = (c.Offset == best.Value.Offset) ? " <-- retenu" : ""; + Console.Error.WriteLine($"[diagnostic] offset={c.Offset,-6} valeur=\"{c.Value}\"{marker}"); + } + } + + // Seule ligne envoyée sur stdout : la valeur, pour capture facile en script. + Console.WriteLine(best.Value.Value); + return 0; + } + + private static System.Collections.Generic.List<(long Offset, string Value)> ScanUtf16Le(byte[] data, int limit) + { + var hits = new System.Collections.Generic.List<(long, string)>(); + var sb = new StringBuilder(); + long start = 0; + int i = 0; + + while (i + 1 < limit) + { + char c = (char)(data[i] | (data[i + 1] << 8)); + bool printable = c >= 32 && c < 127; + if (printable) + { + if (sb.Length == 0) start = i; + sb.Append(c); + i += 2; + } + else + { + Flush(sb, start, hits); + sb.Clear(); + i += 1; + } + } + Flush(sb, start, hits); + return hits; + } + + private static void Flush(StringBuilder sb, long start, System.Collections.Generic.List<(long, string)> hits) + { + if (sb.Length < 3) return; + string s = sb.ToString(); + foreach (Match m in VersionPattern.Matches(s)) + { + hits.Add((start + m.Index * 2, m.Value)); + } + } + } +} diff --git a/program.cs b/program.cs index e6c6f49..707e887 100644 --- a/program.cs +++ b/program.cs @@ -238,6 +238,25 @@ private static int RunTool(string[] args) return NkiVersionScan.Run(nkiPath, scanLength, outReport); } + if (operation == "nki-version") + { + if (argsList.Count < 2) + { + Console.WriteLine("Usage: NkxTool nki-version [-v]"); + return 1; + } + + string nkiPath = Path.GetFullPath(argsList[1]); + if (!File.Exists(nkiPath)) + { + Console.Error.WriteLine($"Error: The source file '{nkiPath}' does not exist."); + return 1; + } + + bool verbose = argsList.Contains("-v") || argsList.Contains("--verbose"); + + return NkiVersionCommand.Run(nkiPath, verbose); + } if (argsList.Count < 2) { From ab48afb480cac5c5ec972ce9f811a9cd7e0f1d03 Mon Sep 17 00:00:00 2001 From: manzing Date: Sat, 8 Aug 2026 10:59:50 +0200 Subject: [PATCH 11/13] Cleaning info dump --- nki/{NkiVersionScan.cs => NkiInfoScan.cs} | 0 program.cs | 10 +++++----- 2 files changed, 5 insertions(+), 5 deletions(-) rename nki/{NkiVersionScan.cs => NkiInfoScan.cs} (100%) diff --git a/nki/NkiVersionScan.cs b/nki/NkiInfoScan.cs similarity index 100% rename from nki/NkiVersionScan.cs rename to nki/NkiInfoScan.cs diff --git a/program.cs b/program.cs index 707e887..dbb7a76 100644 --- a/program.cs +++ b/program.cs @@ -187,7 +187,7 @@ private static int RunTool(string[] args) { if (argsList.Count < 2) { - Console.WriteLine("Usage: NkxTool dump [--out rapport.txt]"); + Console.WriteLine("Usage: NkxTool dump [--out report.txt]"); return 1; } @@ -207,11 +207,11 @@ private static int RunTool(string[] args) return NkiDumpCommand.Run(nkiPath, outReport); } - if (operation == "nki-versionscan") + if (operation == "nki-infoscan") { if (argsList.Count < 2) { - Console.WriteLine("Usage: NkxTool nki-versionscan [--length N] [--out rapport.txt]"); + Console.WriteLine("Usage: NkxTool nki-infoscan [--length N] [--out report.txt]"); return 1; } @@ -236,13 +236,13 @@ private static int RunTool(string[] args) outReport = Path.GetFullPath(argsList[outIndex + 1]); } - return NkiVersionScan.Run(nkiPath, scanLength, outReport); + return NkiInfoScan.Run(nkiPath, scanLength, outReport); } if (operation == "nki-version") { if (argsList.Count < 2) { - Console.WriteLine("Usage: NkxTool nki-version [-v]"); + Console.WriteLine("Usage: NkxTool nki-version [-v]"); return 1; } From 5d3988fe61e4ee9c82a4d1c2eea549d82b3ffbf5 Mon Sep 17 00:00:00 2001 From: manzing Date: Sat, 8 Aug 2026 11:07:11 +0200 Subject: [PATCH 12/13] Update NkiInfoScan.cs --- nki/NkiInfoScan.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nki/NkiInfoScan.cs b/nki/NkiInfoScan.cs index e33552e..4cbd7a3 100644 --- a/nki/NkiInfoScan.cs +++ b/nki/NkiInfoScan.cs @@ -6,7 +6,7 @@ namespace NkiTool { - public static class NkiVersionScan + public static class NkiInfoScan { private static readonly Regex VersionPattern = new Regex(@"\b\d{1,3}\.\d{1,3}(\.\d{1,5}){0,2}\b", RegexOptions.Compiled); From dd4d0727d69696c16fb041057bda1ad508385917 Mon Sep 17 00:00:00 2001 From: manzing Date: Sun, 16 Aug 2026 17:52:25 +0200 Subject: [PATCH 13/13] Added description for nki commands --- program.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/program.cs b/program.cs index dbb7a76..5446688 100644 --- a/program.cs +++ b/program.cs @@ -339,15 +339,22 @@ private static void ShowUsage() Console.WriteLine(" NkxTool list [outputList.txt]"); Console.WriteLine(" NkxTool update [-f ]"); Console.WriteLine(" NkxTool dump [--out rapport.txt]"); + Console.WriteLine(" NkxTool nki-infoscan [--length N] [--out report.txt]"); + Console.WriteLine(" NkxTool nki-version [-v]"); Console.WriteLine(); Console.WriteLine("Examples:"); Console.WriteLine(" NkxTool unpack archive.nkx output_folder"); Console.WriteLine(" NkxTool update"); Console.WriteLine(" NkxTool update -f \"C:\\Program Files\\Common Files\\Native Instruments\\Service Center\\NativeAccess.xml\""); + Console.WriteLine(" NkxTool nki-infoscan \"Piano.nki\" --length 4096 --out version_report.txt"); + Console.WriteLine(" NkxTool nki-version \"Piano.nki\""); Console.WriteLine(); Console.WriteLine("Options:"); Console.WriteLine(" -y : Overwrite existing files without skipping (unpack only)"); Console.WriteLine(" -f : Specify a custom path to NativeAccess.xml (update only)"); + Console.WriteLine(" --length N : Number of bytes to scan from the start of the file (nki-infoscan only, default 4096)"); + Console.WriteLine(" --out : Write the report to a file instead of (or in addition to) the console"); + Console.WriteLine(" -v : Verbose mode, lists all version candidates found (nki-version only)"); Console.WriteLine(); Console.WriteLine("Supported extensions: .nkx, .nkr, .nicnt, .nks"); }