Skip to content
Open
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);

//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());
break;
Comment on lines +101 to +120
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Bounds checks needed for codec 6 (.ogg) parsing to avoid OOB access

Indexing st at computed offsets (extradataOffset + 0x02, +0x10, +0x14, and decrypt loop) assumes well-formed data. Malformed SCDs can throw IndexOutOfRangeException or expose incorrect data. Add bounds checks before reading/looping and validate oggSize fits within st.

Example guard pattern:

-                        var extradataOffset = 0u;
-                        if (streamInfo.AuxChunkCount > 0) extradataOffset += BitConverter.ToUInt32(st.Skip(0x04).Take(4).ToArray(), 0);
+                        var extradataOffset = 0u;
+                        if (streamInfo.AuxChunkCount > 0)
+                        {
+                            if (st.Length < 8) throw new InvalidDataException("SCD OGG extradata header truncated.");
+                            extradataOffset += BitConverter.ToUInt32(st.AsSpan(0x04, 4));
+                        }
                         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;
+                        if (startOffset > (uint)st.Length) throw new InvalidDataException("SCD OGG start offset beyond payload.");
 
                         var decryptedFile = st.ToArray();
 
                         var endPosition = startOffset + vorbHeaderSize;
+                        if (endPosition > (uint)decryptedFile.Length) throw new InvalidDataException("SCD OGG vorb header truncated.");
 
                         for (var i = startOffset; i < endPosition; i++) decryptedFile[i] = (byte)(decryptedFile[i]^encryptionKey);
 
                         var oggSize = vorbHeaderSize + streamInfo.StreamSize;
+                        if (startOffset + oggSize > (uint)decryptedFile.Length) throw new InvalidDataException("SCD OGG size exceeds payload.");
📝 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());
break;
{
var extradataOffset = 0u;
if (streamInfo.AuxChunkCount > 0)
{
if (st.Length < 8)
throw new InvalidDataException("SCD OGG extradata header truncated.");
extradataOffset += BitConverter.ToUInt32(st.AsSpan(0x04, 4));
}
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;
if (startOffset > (uint)st.Length)
throw new InvalidDataException("SCD OGG start offset beyond payload.");
var decryptedFile = st.ToArray();
var endPosition = startOffset + vorbHeaderSize;
if (endPosition > (uint)decryptedFile.Length)
throw new InvalidDataException("SCD OGG vorb header truncated.");
for (var i = startOffset; i < endPosition; i++)
decryptedFile[i] = (byte)(decryptedFile[i] ^ encryptionKey);
var oggSize = vorbHeaderSize + streamInfo.StreamSize;
if (startOffset + oggSize > (uint)decryptedFile.Length)
throw new InvalidDataException("SCD OGG size exceeds payload.");
scd.MediaFiles.Add(decryptedFile.Skip((int)startOffset).Take((int)oggSize).ToArray());
break;
}
🤖 Prompt for AI Agents
In OpenKh.Bbs/Scd.cs around lines 100-119, add defensive bounds checks before
indexing into st: verify extradataOffset is within st.Length and, if
streamInfo.AuxChunkCount > 0, that the additional 4-byte read at offset 0x04 is
available; ensure (extradataOffset + 0x02) < st.Length before reading
encryptionKey, and that reading 4-byte values at (extradataOffset + 0x10) and
(extradataOffset + 0x14) is only done if (extradataOffset + 0x10 + 4) <=
st.Length and (extradataOffset + 0x14 + 4) <= st.Length; compute startOffset and
endPosition only after these checks and ensure endPosition <= st.Length and
startOffset <= endPosition before running the decryption loop; validate oggSize
is non-negative and that startOffset + oggSize <= st.Length before slicing; on
any failure either throw a clear exception or skip the malformed entry instead
of indexing out of bounds.

}
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);
Comment on lines +128 to +142
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: RIFF chunk size written as 8 bytes (long) instead of 4 bytes

length is inferred as long due to uint + int, so BitConverter.GetBytes(length) emits 8 bytes, breaking the RIFF header. Use a 32-bit value.

-                        var length = streamSize + (0x4e - 0x8);
+                        var length = checked((int)(streamSize + 0x46)); // overall RIFF size minus 8
                         var msadpcm = Array.Empty<byte>()
                             .Concat(BitConverter.GetBytes(0x46464952)) //"RIFF"
-                            .Concat(BitConverter.GetBytes(length)) //overall file size - 8
+                            .Concat(BitConverter.GetBytes(length)) // overall file size - 8 (4 bytes)
                             .Concat(BitConverter.GetBytes(0x45564157)) //"WAVE"
📝 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 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);
var length = checked((int)(streamSize + 0x46)); // overall RIFF size minus 8
var msadpcm = Array.Empty<byte>()
.Concat(BitConverter.GetBytes(0x46464952)) //"RIFF"
.Concat(BitConverter.GetBytes(length)) // overall file size - 8 (4 bytes)
.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);
🤖 Prompt for AI Agents
In OpenKh.Bbs/Scd.cs around lines 127 to 141, the RIFF "chunk size" is being
written using BitConverter.GetBytes(length) where length is a 64-bit long
(resulting in 8 bytes) because of mixed uint + int arithmetic; replace length
with a 32-bit value (cast to int or uint) so BitConverter.GetBytes produces a
4-byte little-endian value, and ensure the casted value accurately represents
the overall file size - 8 before writing to the header.

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