Skip to content

Commit b370c9b

Browse files
committed
feat: replace blind injector with stream-safe replacer
1 parent 3e794df commit b370c9b

5 files changed

Lines changed: 142 additions & 106 deletions

File tree

‎.github/workflows/build.yml‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ jobs:
1818
$vsPath = vswhere -latest -property installationPath
1919
Import-Module "$vsPath\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"
2020
Enter-VsDevShell -VsInstallPath $vsPath -Arch amd64 -SkipAutomaticLocation
21-
@('xa-interleaver.cxx','xa-deinterleaver.cxx','xa-injector.cxx') | ForEach-Object { (Get-Content $_ -Raw) -replace 'VERSION', "${{ env.ref_name }}" | Set-Content $_ -NoNewline }
22-
cl /std:c++17 /O2 /EHsc /Fexa-injector xa-injector.cxx
21+
@('xa-interleaver.cxx','xa-deinterleaver.cxx','xa-replacer.cxx') | ForEach-Object { (Get-Content $_ -Raw) -replace 'VERSION', "${{ env.ref_name }}" | Set-Content $_ -NoNewline }
22+
cl /std:c++17 /O2 /EHsc /Fexa-replacer xa-replacer.cxx
2323
cl /std:c++17 /O2 /EHsc /Fexa-interleaver xa-interleaver.cxx
2424
cl /std:c++17 /O2 /EHsc /Fexa-deinterleaver xa-deinterleaver.cxx
25-
Compress-Archive -Path example.csv,xa-interleaver.exe,xa-deinterleaver.exe,xa-injector.exe -DestinationPath xa-interleaver-${{ env.ref_name }}-windows.zip
25+
Compress-Archive -Path example.csv,xa-interleaver.exe,xa-deinterleaver.exe,xa-replacer.exe -DestinationPath xa-interleaver-${{ env.ref_name }}-windows.zip
2626
2727
- name: Upload build artifacts
2828
uses: actions/upload-artifact@main
@@ -41,11 +41,11 @@ jobs:
4141

4242
- name: Build and package xa-interleaver
4343
run: |
44-
for file in xa-interleaver.cxx xa-deinterleaver.cxx xa-injector.cxx; do perl -pi -e "s/VERSION/${{ env.ref_name }}/g" "$file"; done
45-
g++ -std=c++17 -O2 -o xa-injector xa-injector.cxx
44+
for file in xa-interleaver.cxx xa-deinterleaver.cxx xa-replacer.cxx; do perl -pi -e "s/VERSION/${{ env.ref_name }}/g" "$file"; done
45+
g++ -std=c++17 -O2 -o xa-replacer xa-replacer.cxx
4646
g++ -std=c++17 -O2 -o xa-interleaver xa-interleaver.cxx
4747
g++ -std=c++17 -O2 -o xa-deinterleaver xa-deinterleaver.cxx
48-
zip xa-interleaver-${{ env.ref_name }}-linux.zip example.csv xa-interleaver xa-deinterleaver xa-injector
48+
zip xa-interleaver-${{ env.ref_name }}-linux.zip example.csv xa-interleaver xa-deinterleaver xa-replacer
4949
5050
- name: Upload build artifacts
5151
uses: actions/upload-artifact@main
@@ -64,11 +64,11 @@ jobs:
6464

6565
- name: Build and package xa-interleaver
6666
run: |
67-
for file in xa-interleaver.cxx xa-deinterleaver.cxx xa-injector.cxx; do perl -pi -e "s/VERSION/${{ env.ref_name }}/g" "$file"; done
68-
clang++ -arch x86_64 -arch arm64 -mmacos-version-min=10.15 -std=c++17 -O2 -o xa-injector xa-injector.cxx
67+
for file in xa-interleaver.cxx xa-deinterleaver.cxx xa-replacer.cxx; do perl -pi -e "s/VERSION/${{ env.ref_name }}/g" "$file"; done
68+
clang++ -arch x86_64 -arch arm64 -mmacos-version-min=10.15 -std=c++17 -O2 -o xa-replacer xa-replacer.cxx
6969
clang++ -arch x86_64 -arch arm64 -mmacos-version-min=10.15 -std=c++17 -O2 -o xa-interleaver xa-interleaver.cxx
7070
clang++ -arch x86_64 -arch arm64 -mmacos-version-min=10.15 -std=c++17 -O2 -o xa-deinterleaver xa-deinterleaver.cxx
71-
zip xa-interleaver-${{ env.ref_name }}-macos.zip example.csv xa-interleaver xa-deinterleaver xa-injector
71+
zip xa-interleaver-${{ env.ref_name }}-macos.zip example.csv xa-interleaver xa-deinterleaver xa-replacer
7272
7373
- name: Upload build artifacts
7474
uses: actions/upload-artifact@main

‎.gitignore‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@
1010
!LICENSE
1111
!README.md
1212
!xa-deinterleaver.cxx
13-
!xa-injector.cxx
1413
!xa-interleaver.cxx
14+
!xa-replacer.cxx

‎README.md‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# xa-interleaver
2-
`xa-interleaver ` builds a mixed audio xa file, supported by psx, from a manifest file with single audio xa files.
2+
`⠀xa-interleaver ` builds a mixed audio xa file, supported by psx, from a manifest file with single audio xa files.
33

44
`xa-deinterleaver` dumps a mixed audio xa file into single audio xa files and generates a manifest file of them.
55

6-
`xa-injector ` injects a single audio xa file into a specific sector of a given target file.
6+
`⠀⠀xa-replacer ` replaces a mixed audio xa file stream with a given single audio xa file.
77
>[!IMPORTANT]
88
>What's the difference between other tools? Well, this **CAN** interleave in succession with a custom stride pattern.
99
>
@@ -49,14 +49,14 @@ xa-interleaver path/to/input.csv 8 2336 path/to/output.xa
4949
xa-deinterleaver path/to/input.xa 2352 path/to/output/
5050
```
5151

52-
### xa-injector
52+
### xa-replacer
5353
```
54-
xa-injector <target> <source> <sector> <stride>
54+
xa-replacer <target> <source> <sector>
5555
Example:
56-
xa-injector path/to/target.str path/to/source.xa 7 8
56+
xa-replacer path/to/target.str path/to/source.xa 7
5757
```
5858
>[!CAUTION]
59-
>If the source file is larger than the original stream, the injector will overwrite subsequent sectors, potentially corrupting data.
59+
>Files containing non-media data might cause the tool to misidentify null data sectors as null audio sectors.
6060
6161
## Manifest
6262
The manifest text file must be in the following format:
@@ -74,6 +74,7 @@ chunk,type,file,null_termination,xa_file_number,xa_channel_number,xa_null_subhea
7474
|`xa_null_subheader`|`0xFFFFFFFF` |***OPTIONAL*** subheader value for null-termination sectors. Defaults to 0x`xa_file_number`000000.|
7575

7676
>[!NOTE]
77+
>The deinterleaver's `sector_beg-end` and `stride` fields are purely informative and do not affect interleaving.
7778
><details>
7879
><summary>Example.csv manifest for Megaman X6 BGM.XA:</summary>
7980
>
@@ -122,26 +123,26 @@ chunk,type,file,null_termination,xa_file_number,xa_channel_number,xa_null_subhea
122123
>```
123124
></details>
124125
>
125-
>Or to reemplace the audio of an image or .str file, simply use `xa-injector`
126+
>Or to replace the audio of an image or .str file, simply use `xa-replacer`
126127
127128
## Compile
128129
A C++ compiler (MSVC, GCC, Clang) is required.
129130
130131
### MSVC:
131132
```
132-
cl /std:c++17 /O2 /EHsc /Fexa-injector xa-injector.cxx
133+
cl /std:c++17 /O2 /EHsc /Fexa-replacer xa-replacer.cxx
133134
cl /std:c++17 /O2 /EHsc /Fexa-interleaver xa-interleaver.cxx
134135
cl /std:c++17 /O2 /EHsc /Fexa-deinterleaver xa-deinterleaver.cxx
135136
```
136137
### GCC:
137138
```
138-
g++ -std=c++17 -O2 -o xa-injector xa-injector.cxx
139+
g++ -std=c++17 -O2 -o xa-replacer xa-replacer.cxx
139140
g++ -std=c++17 -O2 -o xa-interleaver xa-interleaver.cxx
140141
g++ -std=c++17 -O2 -o xa-deinterleaver xa-deinterleaver.cxx
141142
```
142143
### Clang:
143144
```
144-
clang++ -std=c++17 -O2 -o xa-injector xa-injector.cxx
145+
clang++ -std=c++17 -O2 -o xa-replacer xa-replacer.cxx
145146
clang++ -std=c++17 -O2 -o xa-interleaver xa-interleaver.cxx
146147
clang++ -std=c++17 -O2 -o xa-deinterleaver xa-deinterleaver.cxx
147148
```

‎xa-injector.cxx‎

Lines changed: 0 additions & 86 deletions
This file was deleted.

‎xa-replacer.cxx‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#include "libxa_deinterleaver.hxx"
2+
3+
#define VER "VERSION"
4+
#define XA_DATA_SIZE 2336
5+
#define CD_SECTOR_SIZE 2352
6+
#define SUBHEAD_OFFSET 16
7+
8+
int check_file(FILE *fp, const std::filesystem::path &path, uintmax_t &fileSize, uint8_t *buffer)
9+
{
10+
if (!fp)
11+
{
12+
fprintf(stderr, "Error: Cannot open file \"%s\".\n", path.filename().string().c_str());
13+
exit(1);
14+
}
15+
16+
fileSize = std::filesystem::file_size(path);
17+
if (fread(buffer + SUBHEAD_OFFSET, 1, XA_DATA_SIZE, fp) == XA_DATA_SIZE)
18+
{
19+
if (fileSize % CD_SECTOR_SIZE == 0 && memcmp(buffer + SUBHEAD_OFFSET, buffer, 12) == 0)
20+
return CD_SECTOR_SIZE;
21+
if (fileSize % XA_DATA_SIZE == 0)
22+
return XA_DATA_SIZE;
23+
}
24+
25+
fprintf(stderr, "Error: File \"%s\" is not aligned to 2336/2352 bytes.\n", path.filename().string().c_str());
26+
exit(1);
27+
}
28+
29+
int main(int argc, char *argv[])
30+
{
31+
if (argc < 4)
32+
{
33+
printf("xa-replacer " VER " by N4gtan\n\n"
34+
" Usage: xa-replacer <target> <source> <sector>\n\n"
35+
"Target: File to be modified (in-place)\n"
36+
"Source: Deinterleaved XA file to inject\n"
37+
"Sector: Starting LBA of the stream to replace\n");
38+
return 0;
39+
}
40+
41+
const std::filesystem::path tgtPath = argv[1];
42+
const std::filesystem::path srcPath = argv[2];
43+
const int sector = atoi(argv[3]);
44+
45+
const std::vector<deinterleaver::FileInfo> entries = std::move(deinterleaver(tgtPath).entries);
46+
const auto it = std::find_if(entries.begin(), entries.end(), [sector](const auto &entry) { return entry.begSec == sector; });
47+
if (it == entries.end())
48+
{
49+
fprintf(stderr, "Error: There is no stream that starts at sector %d.\n", sector);
50+
return 1;
51+
}
52+
const auto &entry = *it;
53+
54+
uint8_t buffer[CD_SECTOR_SIZE] {0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00};
55+
std::unique_ptr<FILE, decltype(&fclose)> tgtFile(fopen(tgtPath.string().c_str(), "r+b"), &fclose);
56+
std::unique_ptr<FILE, decltype(&fclose)> srcFile(fopen(srcPath.string().c_str(), "rb"), &fclose);
57+
58+
// Retrieve sector sizes and offsets
59+
uintmax_t tgtSize, srcSize;
60+
const int tgtSectorSize = check_file(tgtFile.get(), tgtPath, tgtSize, buffer);
61+
const int srcSectorSize = check_file(srcFile.get(), srcPath, srcSize, buffer);
62+
const int dataOffset = tgtSectorSize - XA_DATA_SIZE;
63+
const int buffOffset = CD_SECTOR_SIZE - srcSectorSize;
64+
65+
// Validate target available space
66+
const int srcSectorCount = srcSize / srcSectorSize;
67+
if (srcSectorCount > entry.sectorCount)
68+
{
69+
fprintf(stderr, "Error: Source exceeds target stream size by %d sector(s).\n", srcSectorCount - entry.sectorCount);
70+
return 1;
71+
}
72+
73+
// Check eof bit and reset file pointers
74+
fseeko(tgtFile.get(), static_cast<int64_t>(entry.endSec) * tgtSectorSize + dataOffset + 2, SEEK_SET);
75+
uint8_t eofBit = fgetc(tgtFile.get()) & 0x80;
76+
fseeko(srcFile.get(), 0, SEEK_SET);
77+
fseeko(tgtFile.get(), static_cast<int64_t>(sector) * tgtSectorSize + dataOffset, SEEK_SET);
78+
79+
// Write data
80+
auto processSector = [&](auto tag) -> void
81+
{
82+
[[maybe_unused]] size_t __ = fread(buffer + buffOffset, 1, srcSectorSize, srcFile.get());
83+
buffer[SUBHEAD_OFFSET + 4] = buffer[SUBHEAD_OFFSET] = entry.filenum;
84+
buffer[SUBHEAD_OFFSET + 5] = buffer[SUBHEAD_OFFSET + 1] = entry.channel;
85+
if constexpr (tag)
86+
buffer[SUBHEAD_OFFSET + 6] = buffer[SUBHEAD_OFFSET + 2] = (buffer[SUBHEAD_OFFSET + 2] & 0x7F) | eofBit;
87+
fwrite(buffer + SUBHEAD_OFFSET, 1, XA_DATA_SIZE, tgtFile.get());
88+
};
89+
90+
const int skipSize = entry.sectorStride * tgtSectorSize + dataOffset;
91+
for (int i = 1; i < srcSectorCount; ++i)
92+
{
93+
processSector(std::false_type{});
94+
fseeko(tgtFile.get(), skipSize, SEEK_CUR);
95+
}
96+
processSector(std::true_type{});
97+
98+
// Null filler
99+
int sectorsToFill = (entry.sectorCount - entry.nullTermination) - srcSectorCount;
100+
if (sectorsToFill > 0)
101+
{
102+
printf("Warning: Source is smaller than target stream size by %d sector(s).\n"
103+
" Write null sector(s) as filler? <Y/n> ", sectorsToFill);
104+
105+
const uint8_t key = getchar();
106+
if (std::tolower(key) != 'n')
107+
{
108+
memcpy(&buffer[SUBHEAD_OFFSET], &entry.nullSubheader, sizeof(entry.nullSubheader));
109+
memcpy(&buffer[SUBHEAD_OFFSET + 4], &entry.nullSubheader, sizeof(entry.nullSubheader));
110+
memset(&buffer[SUBHEAD_OFFSET + 8], entry.nullSubheader[2] == 0xFF ? 0xFF : 0, sizeof(buffer) - (SUBHEAD_OFFSET + 8));
111+
while (sectorsToFill-- > 0)
112+
{
113+
fseeko(tgtFile.get(), skipSize, SEEK_CUR);
114+
fwrite(buffer + SUBHEAD_OFFSET, 1, XA_DATA_SIZE, tgtFile.get());
115+
}
116+
}
117+
}
118+
119+
printf("Process complete.\n");
120+
return 0;
121+
}

0 commit comments

Comments
 (0)