Skip to content

Commit 069300c

Browse files
authored
Merge pull request #2 from caspervg/fix/no-sscanf
Fix/no sscanf
2 parents 791d1bb + 549d9e8 commit 069300c

6 files changed

Lines changed: 305 additions & 51 deletions

File tree

.github/workflows/ci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main, master ]
6+
pull_request:
7+
branches: [ main, master ]
8+
9+
jobs:
10+
build-and-test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@v4
16+
with:
17+
submodules: recursive
18+
19+
- name: Install dependencies
20+
run: sudo apt-get update && sudo apt-get install -y ninja-build g++-14 gcc-14
21+
22+
- name: Configure
23+
run: cmake -S . -B build -G Ninja -DCMAKE_CXX_STANDARD=23 -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14
24+
25+
- name: Build
26+
run: cmake --build build
27+
28+
- name: Test
29+
run: ctest --test-dir build --output-on-failure

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,16 @@ endif()
7676
# Test executable
7777
add_executable(DBPFKitTests
7878
tests/tests.cpp
79+
tests/parse_helpers_tests.cpp
7980
)
8081
target_link_libraries(DBPFKitTests PRIVATE
8182
DBPFKitLib
8283
libsquish::Squish
8384
Catch2::Catch2WithMain
8485
)
86+
target_compile_definitions(
87+
DBPFKitLib PUBLIC INI_MAX_LINE=1000
88+
)
8589

8690
# Enable testing
8791
enable_testing()

src/LTextReader.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,9 @@ namespace LText {
123123

124124
const size_t payloadBytes = buffer.size() - 4;
125125
const size_t expectedBytes = static_cast<size_t>(charCount) * 2;
126-
const bool hasControl = control == kControlChar;
127126
const bool lengthMatches = payloadBytes == expectedBytes && (payloadBytes % 2 == 0);
128127

129-
if (!hasControl || !lengthMatches) {
128+
if (!lengthMatches) {
130129
auto fallback = ParseFallback(buffer);
131130
if (fallback.has_value()) {
132131
return fallback;

src/RUL0.cpp

Lines changed: 120 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,74 @@
11
#include "RUL0.h"
22

3+
#include <cctype>
4+
#include <charconv>
35
#include <cstdint>
4-
#include <cstdio>
56
#include <cstring>
67
#include <format>
78
#include <ranges>
89

910
#include "ParseTypes.h"
1011
#include "ini.h"
1112

13+
namespace RUL0::ParseHelpers {
14+
std::string_view Trim(std::string_view s) {
15+
while (!s.empty() && std::isspace(static_cast<unsigned char>(s.front()))) {
16+
s.remove_prefix(1);
17+
}
18+
while (!s.empty() && std::isspace(static_cast<unsigned char>(s.back()))) {
19+
s.remove_suffix(1);
20+
}
21+
return s;
22+
}
23+
24+
bool ParseFloat(std::string_view s, float& out) {
25+
s = Trim(s);
26+
const auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), out, std::chars_format::general);
27+
return ec == std::errc() && ptr == s.data() + s.size();
28+
}
29+
30+
bool ParseHex(std::string_view s, uint32_t& out) {
31+
s = Trim(s);
32+
if (s.size() >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
33+
s = s.substr(2);
34+
}
35+
const auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), out, 16);
36+
return ec == std::errc() && ptr == s.data() + s.size();
37+
}
38+
39+
bool EqualsIgnoreCase(const std::string_view a, const std::string_view b) {
40+
if (a.size() != b.size()) {
41+
return false;
42+
}
43+
for (size_t i = 0; i < a.size(); ++i) {
44+
const auto ca = static_cast<unsigned char>(a[i]);
45+
const auto cb = static_cast<unsigned char>(b[i]);
46+
if (std::tolower(ca) != std::tolower(cb)) {
47+
return false;
48+
}
49+
}
50+
return true;
51+
}
52+
53+
bool StartsWithIgnoreCase(const std::string_view text, const std::string_view prefix) {
54+
if (prefix.size() > text.size()) {
55+
return false;
56+
}
57+
for (size_t i = 0; i < prefix.size(); ++i) {
58+
const auto ct = static_cast<unsigned char>(text[i]);
59+
const auto cp = static_cast<unsigned char>(prefix[i]);
60+
if (std::tolower(ct) != std::tolower(cp)) {
61+
return false;
62+
}
63+
}
64+
return true;
65+
}
66+
}
67+
68+
namespace {
69+
using namespace RUL0::ParseHelpers;
70+
}
71+
1272
namespace RUL0 {
1373
// Convert PuzzlePiece to human-readable string representation
1474
std::string PuzzlePiece::ToString() const {
@@ -153,10 +213,29 @@ namespace RUL0 {
153213
int rotation, flip; // The game reads these as %i (which can also be octal or hexadecimal format), so we do too
154214
uint32_t instanceId;
155215
std::string name;
156-
const auto res = sscanf(value.data(), "%f, %f, %i, %i, 0x%x", &x, &y, &rotation, &flip, &instanceId);
157-
if (res != 5) {
216+
217+
std::string_view parts[5];
218+
size_t count = 0;
219+
size_t start = 0;
220+
size_t semi = value.find(kCommentPrefix);
221+
if (semi != std::string_view::npos) {
222+
value = value.substr(0, semi);
223+
}
224+
225+
while (start < value.size() && count < 5) {
226+
size_t comma = value.find(kListDelimiter, start);
227+
if (comma == std::string_view::npos) comma = value.size();
228+
parts[count++] = Trim(value.substr(start, comma - start));
229+
start = comma + 1;
230+
}
231+
if (count != 5) return false;
232+
233+
if (!ParseFloat(parts[0], x) || !ParseFloat(parts[1], y) ||
234+
!ParseIntAuto(parts[2], rotation) || !ParseIntAuto(parts[3], flip) ||
235+
!ParseHex(parts[4], instanceId)) {
158236
return false;
159237
}
238+
160239
previewEffect.initialized = true;
161240
previewEffect.x = x;
162241
previewEffect.y = y;
@@ -228,7 +307,7 @@ namespace RUL0 {
228307

229308
if (expectChar(',')) {
230309
auto mask = nextToken();
231-
nc.hexMask = std::stoul(std::string(mask.substr(0,std::min(mask.length(), 10zu))), nullptr, 16);
310+
nc.hexMask = std::stoul(std::string(mask.substr(0, std::min(mask.length(), 10zu))), nullptr, 16);
232311
}
233312

234313
ct.networks.push_back(nc);
@@ -245,13 +324,13 @@ namespace RUL0 {
245324
const auto valStr = std::string_view(value);
246325

247326
// We are either in the Ordering section or in a sectionless preamble
248-
if (secStr == kOrderingSection || secStr.empty()) {
249-
if (keyStr == kRotationRingKey) {
327+
if (EqualsIgnoreCase(secStr, kOrderingSection) || secStr.empty()) {
328+
if (EqualsIgnoreCase(keyStr, kRotationRingKey)) {
250329
// Start a new ordering when we discovered a new RotationRing entry
251330
data->orderings.emplace_back();
252331
data->orderings.back().rotationRing = ParseIdList(valStr);
253332
}
254-
else if (keyStr == kAddTypesKey) {
333+
else if (EqualsIgnoreCase(keyStr, kAddTypesKey)) {
255334
if (data->orderings.empty()) {
256335
// Malformed RUL0: AddTypes before RotationRing
257336
return 0;
@@ -267,7 +346,7 @@ namespace RUL0 {
267346
}
268347

269348
// We have found a HighwayIntersectionInfo section
270-
if (secStr.starts_with(kIntersectionInfoPrefix)) {
349+
if (StartsWithIgnoreCase(secStr, kIntersectionInfoPrefix)) {
271350
const uint32_t id = ParsePieceId(secStr);
272351

273352
// We are starting a new puzzle piece
@@ -279,32 +358,31 @@ namespace RUL0 {
279358

280359
auto* piece = data->currentPiece;
281360

282-
if (keyStr == kPieceKey) {
361+
if (EqualsIgnoreCase(keyStr, kPieceKey)) {
283362
ParsePieceValue(valStr, piece->effect);
284363
}
285-
else if (keyStr == kPreviewEffectKey) {
364+
else if (EqualsIgnoreCase(keyStr, kPreviewEffectKey)) {
286365
piece->effect.name = std::string(valStr);
287366
}
288-
else if (keyStr == kCellLayoutKey) {
289-
piece->cellLayout.push_back(std::string(valStr));
367+
else if (EqualsIgnoreCase(keyStr, kCellLayoutKey)) {
368+
piece->cellLayout.emplace_back(valStr);
290369
}
291-
else if (keyStr == kCheckTypeKey) {
370+
else if (EqualsIgnoreCase(keyStr, kCheckTypeKey)) {
292371
piece->checkTypes.push_back(ParseCheckType(valStr));
293372
}
294-
else if (keyStr == kConsLayoutKey) {
295-
piece->consLayout.push_back(std::string(valStr));
373+
else if (EqualsIgnoreCase(keyStr, kConsLayoutKey)) {
374+
piece->consLayout.emplace_back(valStr);
296375
}
297-
else if (keyStr == kAutoPathBaseKey) {
376+
else if (EqualsIgnoreCase(keyStr, kAutoPathBaseKey)) {
298377
piece->autoPathBase = std::strtoul(value, nullptr, 16);
299378
}
300-
else if (keyStr == kAutoTileBaseKey) {
379+
else if (EqualsIgnoreCase(keyStr, kAutoTileBaseKey)) {
301380
piece->autoTileBase = std::strtoul(value, nullptr, 16);
302381
}
303-
else if (keyStr == kReplacementIntersectionKey) {
382+
else if (StartsWithIgnoreCase(keyStr, kReplacementIntersectionKey)) {
304383
int replRotation;
305384
uint32_t replFlip;
306-
auto const ret = sscanf(value, "%d, %d", &replRotation, &replFlip);
307-
if (ret != 2) {
385+
if (!ParseIntPair(value, replRotation, replFlip)) {
308386
// Invalid ReplacementIntersection format
309387
return 0;
310388
}
@@ -318,69 +396,65 @@ namespace RUL0 {
318396
replFlip
319397
};
320398
}
321-
else if (keyStr == kPlaceQueryIdKey) {
399+
else if (EqualsIgnoreCase(keyStr, kPlaceQueryIdKey)) {
322400
piece->placeQueryId = std::strtoul(value, nullptr, 16);
323401
}
324-
else if (keyStr == kCostsKey) {
325-
if (valStr.size() > 0) {
402+
else if (EqualsIgnoreCase(keyStr, kCostsKey)) {
403+
if (!valStr.empty()) {
326404
piece->costs = std::stoi(value);
327405
}
328406
else {
329407
piece->costs = 0;
330408
}
331409
}
332-
else if (keyStr == kConvertQueryIdKey) {
410+
else if (EqualsIgnoreCase(keyStr, kConvertQueryIdKey)) {
333411
piece->convertQueryId = std::strtoul(value, nullptr, 16);
334412
}
335-
else if (keyStr == kAutoPlaceKey) {
413+
else if (EqualsIgnoreCase(keyStr, kAutoPlaceKey)) {
336414
piece->autoPlace = (std::stoi(value) != 0);
337415
}
338-
else if (keyStr == kHandleOffsetKey) {
339-
const auto ret = sscanf(value,
340-
"%d, %d",
341-
&piece->handleOffset.deltaStraight,
342-
&piece->handleOffset.deltaSide
343-
);
344-
if (ret == 2) {
345-
piece->stepOffsets.initialized = true;
416+
else if (EqualsIgnoreCase(keyStr, kHandleOffsetKey)) {
417+
if (ParseIntPair(value,
418+
piece->handleOffset.deltaStraight,
419+
piece->handleOffset.deltaSide)) {
420+
piece->handleOffset.initialized = true;
346421
}
347422
}
348-
else if (keyStr == kStepOffsetsKey) {
349-
const auto ret = sscanf(value,
350-
"%d, %d",
351-
&piece->stepOffsets.dragStartThreshold,
352-
&piece->stepOffsets.dragCompletionOffset
353-
);
354-
if (ret == 2) {
423+
else if (EqualsIgnoreCase(keyStr, kStepOffsetsKey)) {
424+
if (ParseIntPair(value,
425+
piece->stepOffsets.dragStartThreshold,
426+
piece->stepOffsets.dragCompletionOffset)) {
355427
piece->stepOffsets.initialized = true;
356428
}
357429
}
358-
else if (keyStr == kOneWayDirKey) {
430+
else if (EqualsIgnoreCase(keyStr, kOneWayDirKey)) {
359431
const auto val = std::stoi(value);
360432
if (val < +OneWayDir::WEST || val > +OneWayDir::SOUTH_WEST) {
361433
// Invalid OneWayDir value
362434
return 0;
363435
}
364436
piece->oneWayDir = static_cast<OneWayDir>(val);
365437
}
366-
else if (keyStr == kCopyFromKey) {
438+
else if (EqualsIgnoreCase(keyStr, kCopyFromKey)) {
367439
piece->copyFrom = std::strtoul(value, nullptr, 16);
368440
// TODO: Actually do something with this!
369441
}
370-
else if (keyStr == kRotateKey) {
442+
else if (EqualsIgnoreCase(keyStr, kRotateKey)) {
371443
const auto val = std::stoi(value);
372444
if (val < +Rotation::ROT_0 || val > +Rotation::ROT_270) {
373445
// Invalid rotation value
374446
return 0;
375447
}
376448
piece->rotate = static_cast<Rotation>(val);
377449
}
378-
else if (keyStr == kTransposeKey) {
450+
else if (EqualsIgnoreCase(keyStr, kTransposeKey)) {
379451
piece->transpose = (std::stoi(value) != 0);
380452
}
381-
else if (keyStr == kTranslateKey) {
453+
else if (EqualsIgnoreCase(keyStr, kTranslateKey)) {
382454
// This key is not documented, but present in SC4 game decompilation, so included.
383-
sscanf(value, "%d, %d", &piece->translate.x, &piece->translate.z);
455+
if (ParseIntPair(value, piece->translate.x, piece->translate.z)) {
456+
piece->translate.initialized = true;
457+
}
384458
}
385459
else {
386460
// Malformed RUL0: Unknown key in HighwayIntersectionInfo section
@@ -718,5 +792,4 @@ namespace RUL0 {
718792
BuildNavigationIndices(data);
719793
return data;
720794
}
721-
722795
}

0 commit comments

Comments
 (0)