Skip to content
Closed
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
106 changes: 79 additions & 27 deletions OpenKh.Bbs/Scd.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using Xe.BinaryMapper;
using System.IO;
using System.Linq;
using OpenKh.Common;

namespace OpenKh.Bbs
Expand Down Expand Up @@ -41,7 +42,7 @@ public class StreamHeader
[Data] public uint StreamSize { get; set; }
[Data] public uint ChannelCount { get; set; }
[Data] public uint SampleRate { get; set; }
[Data] public uint Codec { get; set; }
[Data] public uint Codec { get; set; } //6 = .ogg, 12 = MSADPCM .wav
[Data] public uint LoopStart { get; set; }
[Data] public uint LoopEnd { get; set; }
[Data] public uint ExtraDataSize { get; set; }
Expand All @@ -61,44 +62,95 @@ public class VAGHeader
[Data(Count = 16)] public string Name { get; set; }
}

public static Header header = new Header();
public static TableOffsetHeader tableOffsetHeader = new TableOffsetHeader();
public static List<byte[]> StreamFiles = new List<byte[]>();
public Header FileHeader = new();
public TableOffsetHeader tableOffsetHeader = new();
public List<StreamHeader> StreamHeaders = [];
public List<byte[]> StreamFiles = [];
public List<byte[]> MediaFiles = []; //6 = .ogg file, everything else is a .wav with msadpcm codec, throw it at ffmpeg /shrug

public static Scd Read(Stream stream)
{
Scd scd = new Scd();

header = BinaryMapping.ReadObject<Header>(stream);
tableOffsetHeader = BinaryMapping.ReadObject<TableOffsetHeader>(stream);
stream.Seek(0, SeekOrigin.Begin);

var scd = new Scd
{
FileHeader = BinaryMapping.ReadObject<Header>(stream),
tableOffsetHeader = BinaryMapping.ReadObject<TableOffsetHeader>(stream),
};

stream.Seek(tableOffsetHeader.Table1Offset, SeekOrigin.Begin);
stream.Seek(scd.tableOffsetHeader.Table1Offset, SeekOrigin.Begin);

List<uint> SoundOffsets = new List<uint>();
for (int i = 0; i < tableOffsetHeader.Table1ElementCount; i++)
{
SoundOffsets.Add(stream.ReadUInt32());
}
var soundOffsets = new List<uint>();
for (var i = 0; i < scd.tableOffsetHeader.Table1ElementCount; i++) soundOffsets.Add(stream.ReadUInt32());

foreach(uint off in SoundOffsets)
for (var index = 0; index < soundOffsets.Count; index++)
{
var off = soundOffsets[index];
var next = (int)(index == soundOffsets.Count - 1 ? stream.Length : soundOffsets[index + 1] - off);
stream.Seek(off, SeekOrigin.Begin);
var streamInfo = BinaryMapping.ReadObject<StreamHeader>(stream);

byte[] st = stream.ReadBytes((int)streamInfo.StreamSize);
StreamFiles.Add(st);

// Convert to VAG Streams.
/*VAGHeader vagHeader = new VAGHeader();
vagHeader.ChannelCount = (byte)streamInfo.ChannelCount;
vagHeader.SamplingFrequency = (byte)streamInfo.SampleRate;
vagHeader.Version = 3;
vagHeader.Magic = 0x70474156;
vagHeader.Name = p.ToString();*/
scd.StreamHeaders.Add(streamInfo);

var st = stream.ReadBytes(next - 0x20);
scd.StreamFiles.Add(st);
Comment on lines +86 to +95
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Incorrect last-stream size computation (can overread).

For the last entry, next is set to stream.Length instead of (stream.Length - off). This can yield a huge next - 0x20, causing overreads and truncated buffers.

Apply:

-                var next = (int)(index == soundOffsets.Count - 1 ? stream.Length : soundOffsets[index + 1] - off);
+                var next = (int)(index == soundOffsets.Count - 1
+                    ? (stream.Length - off)
+                    : (soundOffsets[index + 1] - off));
+                if (next < 0x20) throw new InvalidDataException($"Stream block too small at offset 0x{off:X}.");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (var index = 0; index < soundOffsets.Count; index++)
{
var off = soundOffsets[index];
var next = (int)(index == soundOffsets.Count - 1 ? stream.Length : soundOffsets[index + 1] - off);
stream.Seek(off, SeekOrigin.Begin);
var streamInfo = BinaryMapping.ReadObject<StreamHeader>(stream);
byte[] st = stream.ReadBytes((int)streamInfo.StreamSize);
StreamFiles.Add(st);
// Convert to VAG Streams.
/*VAGHeader vagHeader = new VAGHeader();
vagHeader.ChannelCount = (byte)streamInfo.ChannelCount;
vagHeader.SamplingFrequency = (byte)streamInfo.SampleRate;
vagHeader.Version = 3;
vagHeader.Magic = 0x70474156;
vagHeader.Name = p.ToString();*/
scd.StreamHeaders.Add(streamInfo);
var st = stream.ReadBytes(next - 0x20);
scd.StreamFiles.Add(st);
for (var index = 0; index < soundOffsets.Count; index++)
{
var off = soundOffsets[index];
var next = (int)(index == soundOffsets.Count - 1
? (stream.Length - off)
: (soundOffsets[index + 1] - off));
if (next < 0x20) throw new InvalidDataException($"Stream block too small at offset 0x{off:X}.");
stream.Seek(off, SeekOrigin.Begin);
var streamInfo = BinaryMapping.ReadObject<StreamHeader>(stream);
scd.StreamHeaders.Add(streamInfo);
var st = stream.ReadBytes(next - 0x20);
scd.StreamFiles.Add(st);
🤖 Prompt for AI Agents
In OpenKh.Bbs/Scd.cs around lines 86 to 95, the computation of `next` for the
last stream uses `stream.Length` (an absolute position) instead of the remaining
size, which can make `next - 0x20` huge and cause overreads; change the
calculation so `next` is the size of the stream chunk: for the last entry set
`next = (int)(stream.Length - off)` otherwise `next = (int)(soundOffsets[index +
1] - off)`, and then ensure the read length (e.g., `next - 0x20`) is clamped to
>= 0 and does not exceed `stream.Length - stream.Position` before calling
`ReadBytes`.


Comment on lines +94 to +96
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against negative/oversized payload read.

stream.ReadBytes(next - 0x20) assumes next >= 0x20. Add a check to avoid negative or oversized reads.

-                var st = stream.ReadBytes(next - 0x20);
+                if (next < 0x20) throw new InvalidDataException("Invalid stream header size.");
+                var payloadSize = next - 0x20;
+                var st = stream.ReadBytes(payloadSize);
+                if (st.Length != payloadSize) throw new EndOfStreamException("Unexpected end of stream.");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var st = stream.ReadBytes(next - 0x20);
scd.StreamFiles.Add(st);
if (next < 0x20)
throw new InvalidDataException("Invalid stream header size.");
var payloadSize = next - 0x20;
var st = stream.ReadBytes(payloadSize);
if (st.Length != payloadSize)
throw new EndOfStreamException("Unexpected end of stream.");
scd.StreamFiles.Add(st);
🤖 Prompt for AI Agents
In OpenKh.Bbs/Scd.cs around lines 94 to 96, the call stream.ReadBytes(next -
0x20) assumes next >= 0x20 and that the resulting length fits within the
remaining stream; validate before reading: compute length = next - 0x20, if
length < 0 or length > remainingBytes (e.g., stream.Length - stream.Position)
then handle as invalid data (throw a descriptive exception or clamp/skip as
appropriate for your error policy) and only call stream.ReadBytes(length) when
the length is valid; this prevents negative or oversized reads and avoids
corrupt/overflow reads.

//https://github.com/Leinxad/KHPCSoundTools/blob/main/SCDInfo/Program.cs#L109
switch (streamInfo.Codec)
{
case 6:
{
var extradataOffset = 0u;
if (streamInfo.AuxChunkCount > 0) extradataOffset += BitConverter.ToUInt32(st.Skip(0x04).Take(4).ToArray(), 0);

var encryptionKey = st[extradataOffset + 0x02];
var seekTableSize = BitConverter.ToUInt32(st.Skip((int)extradataOffset + 0x10).Take(4).ToArray(), 0);
var vorbHeaderSize = BitConverter.ToUInt32(st.Skip((int)extradataOffset + 0x14).Take(4).ToArray(), 0);

var startOffset = extradataOffset + 0x20 + seekTableSize;

var decryptedFile = st.ToArray();

var endPosition = startOffset + vorbHeaderSize;

for (var i = startOffset; i < endPosition; i++) decryptedFile[i] = (byte)(decryptedFile[i]^encryptionKey);

var oggSize = vorbHeaderSize + streamInfo.StreamSize;

scd.MediaFiles.Add(decryptedFile.Skip((int)startOffset).Take((int)oggSize).ToArray());
Comment on lines +102 to +119
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Array index type mismatches (won’t compile) and missing bounds checks.

  • st[extradataOffset + 0x02] uses uint/long for indexing; arrays require int.
  • Loop variables i in for (var i = startOffset; ... ) are uint; indexing decryptedFile[i] won’t compile.
  • No guards for insufficient st length before slicing.

Use int indices and validate sizes:

-                        var extradataOffset = 0u;
-                        if (streamInfo.AuxChunkCount > 0) extradataOffset += BitConverter.ToUInt32(st.Skip(0x04).Take(4).ToArray(), 0);
-
-                        var encryptionKey = st[extradataOffset + 0x02];
-                        var seekTableSize = BitConverter.ToUInt32(st.Skip((int)extradataOffset + 0x10).Take(4).ToArray(), 0);
-                        var vorbHeaderSize = BitConverter.ToUInt32(st.Skip((int)extradataOffset + 0x14).Take(4).ToArray(), 0);
+                        var extradataOffset = 0;
+                        if (streamInfo.AuxChunkCount > 0)
+                            extradataOffset = (int)BitConverter.ToUInt32(st.AsSpan(0x04, 4));
+
+                        if (extradataOffset + 0x20 > st.Length)
+                            throw new InvalidDataException("Extradata offset beyond stream data.");
+
+                        var encryptionKey = st[extradataOffset + 0x02];
+                        var seekTableSize = (int)BitConverter.ToUInt32(st.AsSpan(extradataOffset + 0x10, 4));
+                        var vorbHeaderSize = (int)BitConverter.ToUInt32(st.AsSpan(extradataOffset + 0x14, 4));
@@
-                        var startOffset = extradataOffset + 0x20 + seekTableSize;
+                        var startOffset = extradataOffset + 0x20 + seekTableSize;
@@
-                        var endPosition = startOffset + vorbHeaderSize;
-                
-                        for (var i = startOffset; i < endPosition; i++) decryptedFile[i] = (byte)(decryptedFile[i]^encryptionKey);
+                        var endPosition = startOffset + vorbHeaderSize;
+                        if (endPosition > decryptedFile.Length)
+                            throw new InvalidDataException("Vorb header extends past buffer.");
+                        for (int i = startOffset; i < endPosition; i++)
+                            decryptedFile[i] = (byte)(decryptedFile[i] ^ encryptionKey);
@@
-                        var oggSize = vorbHeaderSize + streamInfo.StreamSize;
-                    
-                        scd.MediaFiles.Add(decryptedFile.Skip((int)startOffset).Take((int)oggSize).ToArray());
+                        var oggSize = vorbHeaderSize + (int)streamInfo.StreamSize;
+                        if (startOffset + oggSize > decryptedFile.Length)
+                            oggSize = decryptedFile.Length - startOffset;
+                        scd.MediaFiles.Add(decryptedFile.AsSpan(startOffset, oggSize).ToArray());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var extradataOffset = 0u;
if (streamInfo.AuxChunkCount > 0) extradataOffset += BitConverter.ToUInt32(st.Skip(0x04).Take(4).ToArray(), 0);
var encryptionKey = st[extradataOffset + 0x02];
var seekTableSize = BitConverter.ToUInt32(st.Skip((int)extradataOffset + 0x10).Take(4).ToArray(), 0);
var vorbHeaderSize = BitConverter.ToUInt32(st.Skip((int)extradataOffset + 0x14).Take(4).ToArray(), 0);
var startOffset = extradataOffset + 0x20 + seekTableSize;
var decryptedFile = st.ToArray();
var endPosition = startOffset + vorbHeaderSize;
for (var i = startOffset; i < endPosition; i++) decryptedFile[i] = (byte)(decryptedFile[i]^encryptionKey);
var oggSize = vorbHeaderSize + streamInfo.StreamSize;
scd.MediaFiles.Add(decryptedFile.Skip((int)startOffset).Take((int)oggSize).ToArray());
var extradataOffset = 0;
if (streamInfo.AuxChunkCount > 0)
extradataOffset = (int)BitConverter.ToUInt32(st.AsSpan(0x04, 4));
// Ensure we have at least the fixed extradata header
if (extradataOffset + 0x20 > st.Length)
throw new InvalidDataException("Extradata offset beyond stream data.");
var encryptionKey = st[extradataOffset + 0x02];
var seekTableSize = (int)BitConverter.ToUInt32(st.AsSpan(extradataOffset + 0x10, 4));
var vorbHeaderSize = (int)BitConverter.ToUInt32(st.AsSpan(extradataOffset + 0x14, 4));
var startOffset = extradataOffset + 0x20 + seekTableSize;
var decryptedFile = st.ToArray();
var endPosition = startOffset + vorbHeaderSize;
// Make sure the Vorbis header fits in the buffer
if (endPosition > decryptedFile.Length)
throw new InvalidDataException("Vorb header extends past buffer.");
for (int i = startOffset; i < endPosition; i++)
decryptedFile[i] = (byte)(decryptedFile[i] ^ encryptionKey);
// Clamp Ogg data to the actual buffer length
var oggSize = vorbHeaderSize + (int)streamInfo.StreamSize;
if (startOffset + oggSize > decryptedFile.Length)
oggSize = decryptedFile.Length - startOffset;
scd.MediaFiles.Add(decryptedFile.AsSpan(startOffset, oggSize).ToArray());
🤖 Prompt for AI Agents
In OpenKh.Bbs/Scd.cs around lines 102 to 119, the code uses uint/long values to
index arrays and slices without validating buffer lengths which causes compile
errors and possible out-of-bounds reads; change all index variables
(extradataOffset, startOffset, endPosition, loop index i, oggSize,
seekTableSize, vorbHeaderSize) to int (or explicitly cast uint->int after
validation), perform explicit bounds checks before reading from st (ensure
st.Length >= extradataOffset + required header sizes and that extradataOffset +
0x02, +0x10, +0x14 are inside the buffer), validate startOffset + vorbHeaderSize
and startOffset + oggSize are within decryptedFile.Length before the XOR loop
and the final Slice/Take, and if checks fail throw a clear exception or return
early; then use an int loop for i from startOffset to endPosition and index
decryptedFile[i] safely.

break;
}
case 12:
{
var streamSize = streamInfo.StreamSize;
var channelCount = streamInfo.ChannelCount;
var sampleRate = streamInfo.SampleRate;

var length = streamSize + (0x4e - 0x8);

var msadpcm = Array.Empty<byte>()
.Concat(BitConverter.GetBytes(0x46464952)) //"RIFF"
.Concat(BitConverter.GetBytes(length)) //overall file size - 8
.Concat(BitConverter.GetBytes(0x45564157)) //"WAVE"
.Concat(BitConverter.GetBytes(0x20746D66)) //"fmt "
.Concat(BitConverter.GetBytes(0x32))
.Concat(st.Take(0x32))
.Concat(BitConverter.GetBytes(0x61746164)) //"data"
.Concat(BitConverter.GetBytes((int)streamSize))
.Concat(st.Skip(0x32))
.ToArray();

scd.MediaFiles.Add(msadpcm);
break;
}
default:
{
scd.MediaFiles.Add(null);
break;
}
}
}



return scd;
}

Expand Down
20 changes: 17 additions & 3 deletions OpenKh.Egs/EgsTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ public class EgsTools

#region Extract

public static void Extract(string inputHed, string output, bool doNotExtractAgain = false)
public enum ExtractFileOutputMode
{
SeparateRoot,
Adjacent,
}

public static void Extract(string inputHed, string output, bool doNotExtractAgain = false, ExtractFileOutputMode outputMode = ExtractFileOutputMode.SeparateRoot)
{
var outputDir = output ?? Path.GetFileNameWithoutExtension(inputHed);
using var hedStream = File.OpenRead(inputHed);
Expand All @@ -80,7 +86,11 @@ public static void Extract(string inputHed, string output, bool doNotExtractAgai
if (!Names.TryGetValue(hash, out var fileName))
fileName = $"{hash}.dat";

var outputFileName = Path.Combine(outputDir, ORIGINAL_FILES_FOLDER_NAME, fileName);
var outputFileName = outputMode switch
{
ExtractFileOutputMode.Adjacent => Path.Combine(outputDir, fileName),
_ => Path.Combine(outputDir, ORIGINAL_FILES_FOLDER_NAME, fileName),
};

if (doNotExtractAgain && File.Exists(outputFileName))
continue;
Expand All @@ -92,7 +102,11 @@ public static void Extract(string inputHed, string output, bool doNotExtractAgai

File.Create(outputFileName).Using(stream => stream.Write(hdAsset.OriginalData));

outputFileName = Path.Combine(outputDir, REMASTERED_FILES_FOLDER_NAME, fileName);
outputFileName = outputMode switch
{
ExtractFileOutputMode.Adjacent => Path.Combine(outputDir, $"{fileName}.remastered.d"),
_ => Path.Combine(outputDir, REMASTERED_FILES_FOLDER_NAME, fileName),
};

foreach (var asset in hdAsset.Assets)
{
Expand Down
Loading
Loading