gen5: fix dex skin footer, fix musical terminator

Closes #4603
Add some filename import length sanitization
This commit is contained in:
Kurt
2026-04-06 01:04:26 -05:00
parent 097ff3a870
commit 0ebf575a03
4 changed files with 73 additions and 4 deletions

View File

@@ -213,6 +213,13 @@ protected ushort WriteExtSection(ReadOnlySpan<byte> data, int offset, int size,
ArgumentOutOfRangeException.ThrowIfNotEqual(data.Length, size);
SetData(data, offset);
return RefreshExtSectionFooter(offset, size, count);
}
private ushort RefreshExtSectionFooter(int offset, int size, ushort count)
{
var data = Data.Slice(offset, size);
// Update Tail Section
ushort chk = Checksums.CRC16_CCITT(data);
var tail = Data[(offset + size)..];
@@ -292,8 +299,9 @@ public void SetMusical(ReadOnlySpan<byte> data, ushort count = 1)
public void SetPokeDexSkin(ReadOnlySpan<byte> data, ushort count = 1)
{
WriteExtSection(data, ExtPokeDexSkinOffset, PokeDexSkin5.SIZE, count);
IsAvailablePokedexSkin = true; // checksum might be changed via this, need to refresh footer to be safe
RefreshExtSectionFooter(ExtPokeDexSkinOffset, PokeDexSkin5.SIZE, count);
PlayerData.UpdateExtData(ExtDataSectionNote5.PokedexSkin, count);
IsAvailablePokedexSkin = true;
}
private Span<byte> DexSkinFooter => Data.Slice(ExtPokeDexSkinOffset + PokeDexSkin5.SIZE - 4, 4);

View File

@@ -6,12 +6,12 @@ public sealed class Musical5(SAV5 SAV, Memory<byte> raw) : SaveBlock<SAV5>(SAV,
{
public const int MusicalNameMaxLength = 20;
private Span<byte> MusicalTrash => Data.Slice(0x208, MusicalNameMaxLength * 2);
private Span<byte> MusicalTrash => Data.Slice(0x208, MusicalNameMaxLength * 2 + 2); // require terminator, so +2 for 0xFFFF
public string MusicalName
{
get => StringConverter5.GetString(MusicalTrash);
set => StringConverter5.SetString(MusicalTrash, value, MusicalNameMaxLength, 0, StringConverterOption.ClearZero);
set => StringConverter5.SetString(MusicalTrash, value, MusicalNameMaxLength, SAV.Language, StringConverterOption.ClearZero);
}
private const int PropOffset = 0x258;

View File

@@ -180,6 +180,38 @@ private static void ExportFile(string extension, string name, ReadOnlySpan<byte>
File.WriteAllBytes(sfd.FileName, data);
}
private static string GetImportedMusicalName(string path)
{
var name = Path.GetFileNameWithoutExtension(path).Trim();
var split = name.LastIndexOf(" - ", StringComparison.Ordinal);
if (split >= 0 && split + 3 < name.Length)
name = name[(split + 3)..].Trim();
var suffix = name.LastIndexOf(" (", StringComparison.Ordinal);
if (suffix > 0 && name[^1] == ')' && IsLikelyLanguageTag(name[(suffix + 2)..^1]))
name = name[..suffix].TrimEnd();
if (name.Length > Musical5.MusicalNameMaxLength)
name = name[..Musical5.MusicalNameMaxLength].TrimEnd();
return name;
}
private static bool IsLikelyLanguageTag(ReadOnlySpan<char> value)
{
if (value.Length is < 2 or > 5)
return false;
foreach (var c in value)
{
if ((uint)(c - 'A') > 'Z' - 'A')
return false;
}
return true;
}
private void B_ImportPNGCGear_Click(object sender, EventArgs e)
{
using var ofd = new OpenFileDialog();
@@ -355,7 +387,7 @@ private void B_MusicalImport_Click(object sender, EventArgs e)
var musical = new MusicalShow5(data);
SAV.SetMusical(data);
if (LastImportedFile is { } name)
SAV.Musical.MusicalName = musical.IsUninitialized ? "" : Path.GetFileNameWithoutExtension(name).Trim();
SAV.Musical.MusicalName = musical.IsUninitialized ? "" : GetImportedMusicalName(name);
}
private void B_MusicalExport_Click(object sender, EventArgs e)

View File

@@ -0,0 +1,29 @@
using FluentAssertions;
using Xunit;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core.Tests.Saves;
public class SAV5DLC
{
[Fact]
public void SetPokeDexSkin_RecomputesChecksumAfterSettingAvailabilityFlag()
{
var sav = new SAV5B2W2();
// Create a random garbage PokeDex skin data with the last 4 bytes reserved for the availability flag and checksum
byte[] data = new byte[PokeDexSkin5.SIZE];
for (int i = 0; i < data.Length - sizeof(uint); i++)
data[i] = (byte)i;
sav.SetPokeDexSkin(data);
sav.IsAvailablePokedexSkin.Should().BeTrue();
ReadUInt32LittleEndian(sav.PokedexSkinData.Span[^sizeof(uint)..]).Should().Be(1u);
const int offset = 0x6D800;
var tail = sav.Data[(offset + PokeDexSkin5.SIZE)..];
ReadUInt16LittleEndian(tail).Should().Be(1);
ReadUInt16LittleEndian(tail[2..]).Should().Be(Checksums.CRC16_CCITT(sav.PokedexSkinData.Span));
}
}