Skip to content

Linux: implement pathCopy and fileRename, and fix three string functions - #86

Merged
greenfire27 merged 2 commits into
developmentfrom
linux-path-copy-rename
Aug 8, 2026
Merged

Linux: implement pathCopy and fileRename, and fix three string functions#86
greenfire27 merged 2 commits into
developmentfrom
linux-path-copy-rename

Conversation

@greenfire27

Copy link
Copy Markdown
Collaborator

Five fixes to the Unix platform back-end, all of them things the other two back-ends have had working all along. Found by running the tests/ suites and the GoogleTest unit tests on Linux; none of this is specific to the branch that surfaced it.

Platform::pathCopy and Platform::fileRename were stubs

Both were return false; in platformX86UNIX/x86UNIXFileio.cc while Windows (winFileio.cc) and macOS (osxFileIO.mm) implement them. pathCopy returns a bool that almost nobody checks, so the work simply did not happen and everything downstream blamed the missing files.

What that broke on Linux:

  • Creating a project produced an empty folder. The Project Manager stamps a new project out of a template with a recursive pathCopy (ProjectGamePanel.cs:100), and the new-project dialog copies the theme library the same way (NewProjectDialog.cs:146).
  • Themes got no cursor art. Both AppCore and the Gui Editor give a theme its own copy of the stock cursors (themes.cs:130, GuiProfileEditorLibrary.cs:181).
  • fileRename failed silently in ModuleManager (renaming a module definition) and ZipArchive (swapping a rebuilt archive into place). It has no script binding, so nothing script-side could see it either.

pathCopy now takes a file or a whole tree, since both callers exist. Paths are used as given rather than routed through MungePath into the pref directory, matching isFile/isDirectory/fileDelete beside it — every caller builds an absolute path and then asks isFile whether the copy arrived, so a copy landing elsewhere would read as a failure. Parent directories are created on that same raw path, because Platform::createPath would munge a relative one into the pref directory and leave the open to fail on a parent it had just made somewhere else.

Details worth keeping: the mode is carried across so a copied executable stays executable; a copy that fails partway is unlinked rather than left for the next run to mistake for real content; the recursion asks isDirectory rather than trusting dirent::d_type, which is DT_UNKNOWN on filesystems that do not carry the kind; and a tree is refused if the destination is inside it, which would otherwise recurse until the path outgrew MaxPath. Platform::isSubDirectory looks like it would answer that last question and does not — it matches a bare child name against the parent's entries, not one path inside another.

fileRename is rename(2) with a copy-and-delete fallback on EXDEV, since the pref directory and the game directory are not always on one filesystem.

Also removes dPathCopy: dead code that returned CopyFile's error flag as though it were a success flag, so anyone reaching for it would have got the answer backwards.

Three bugs in x86UNIXStrings.cc

Each was failing its own test in PlatformStringTests on every Linux run.

  • dStrcatl walked one byte too far looking for the end of dst. while (dstSize && *p++) increments on the iteration that ends the loop too, so p came to rest past the terminator and the append landed in the wrong place: "Garage" + "Games" produced "Garage\0Games", which reads back as "Garage".
  • dStrrev reversed nothing. The loop read x < 1 where it meant x < l, so it ran once and swapped only the first and last character — "GarageGames" came back as "sarageGameG". The corrected bound also keeps the XOR swap off the middle character of an odd-length string (which it would zero) and off str[-1] for an empty one.
  • dItoa never terminated the string it had just built, so dStrrev's dStrlen read past the digits into whatever the caller's buffer held and reversed that: dItoa(16384) produced "\x7F83614". Terminating before the reverse is what the K&R original this is copied from does. While there, the digits are taken off an unsigned copy, because the old n = -n is undefined for S32_MIN — the one input that could not be printed at all.

These survive because Windows and macOS have their own copies of these functions and nobody runs the unit tests on Linux.

Testing

  • New PlatformFileIOTests.PathCopyAndRename covers file copy, nooverwrite in both directions, a missing source leaving nothing behind, recursive tree copy, refusing a copy into itself, and rename. It is also the only way to reach fileRename, which has no script binding.
  • runAllUnitTests() on Linux: 88 tests, 0 failures. Previously 3 failed.
  • tests/run.sh: all 14 smoke suites green, no regressions.
  • Verified against the Gui Editor branch (Gui Editor: an editor you can build a screen in #85) as well, where these fixes take cursorPane from 52 passed / 17 failed to 69 passed, 0 failed, and the full suite to 37/37.

Read the diff with whitespace shown if it looks odd — both files are CRLF in-tree and stay that way.

🤖 Generated with Claude Code

greenfire27 and others added 2 commits August 7, 2026 15:19
Both were stubs returning false on the Unix back-end while Windows and
macOS have had real ones all along, and a copy that never happens is not
something a caller can see: pathCopy returns a bool that almost nobody
checks, so the work simply did not get done and everything downstream
blamed the missing files.

That reaches further than it sounds. The Project Manager stamps a new
project out of a template with a recursive pathCopy, and the editor's new
project dialog copies the theme library the same way, so creating a
project on Linux produced a folder with nothing in it. AppCore and the Gui
Editor both give a theme its own copy of the stock cursor art, which is
why the Gui Editor's cursor pane had no art to measure. fileRename is not
exposed to script at all, but ModuleManager renames a module definition
with it and ZipArchive swaps a rebuilt archive into place, and both were
quietly failing.

pathCopy takes a file or a whole tree, since both callers exist. Paths are
used as given rather than sent through MungePath into the pref directory,
matching isFile, isDirectory and fileDelete beside it: every caller builds
an absolute path and then asks isFile whether the copy arrived, so a copy
routed elsewhere would read as a failure. The directories above a
destination are made on that same raw path for the same reason --
Platform::createPath would munge a relative one into the pref directory
and leave the open to fail on a parent it had just created somewhere else.

Details worth keeping: the mode is carried across so a copied executable
is still executable; a copy that fails partway is unlinked rather than
left for the next run to mistake for good art; the recursion asks
isDirectory rather than trusting dirent::d_type, which is DT_UNKNOWN on
filesystems that do not carry the kind; and a tree is refused if the
destination is inside it, which would otherwise recurse until the path
outgrew MaxPath. Platform::isSubDirectory looks like it would answer that
last question and does not -- it matches a bare child name against the
parent's entries, not one path inside another.

fileRename is rename(2) with a copy-and-delete fallback on EXDEV, because
the pref directory and the game directory are not always on one
filesystem.

dPathCopy goes: it was dead code, and it returned CopyFile's error flag as
though it were a success flag, so anyone who had reached for it would have
got the answer backwards.

Covered by PlatformFileIOTests.PathCopyAndRename, which is also the only
way to reach fileRename, having no script binding. The three failures left
in PlatformStringTests on Linux -- dStrcatl, dStrrev and dItoa -- are
separate pre-existing bugs in x86UNIXStrings.cc and are not touched here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three bugs in the Unix string back-end, each failing its own test in
PlatformStringTests on every Linux run since the tests were written.

dStrcatl walked one byte too far looking for the end of dst. "while
(dstSize && *p++)" increments on the iteration that ends the loop too, so
p came to rest past the terminator rather than on it and the append landed
in the wrong place: "Garage" and "Games" concatenated to "Garage\0Games",
which reads back as "Garage". The test and the pointer walk now agree.

dStrrev reversed nothing. The loop read "x < 1" where it meant "x < l", so
it ran a single iteration and swapped only the first and last character --
"GarageGames" came back as "sarageGameG". The corrected bound also keeps
the XOR swap off the middle character of an odd-length string, which it
would zero rather than leave alone, and off str[-1] for an empty one.

dItoa never terminated the string it had just built. dStrrev measures with
dStrlen, so it read past the digits into whatever the caller's buffer
happened to hold and reversed that: dItoa(16384) produced "\x7F83614".
Terminating before the reverse is what the K&R original this is copied
from does. While here, the digits are taken off an unsigned copy, because
the old "n = -n" is undefined for S32_MIN -- the one input that could not
be printed at all.

None of this is exotic; it is the sort of thing that survives because the
Windows and macOS back-ends have their own copies of these functions and
nobody runs the unit tests on Linux. PlatformStringTests now passes, and
with the copy work in the preceding commit the whole suite is green on
Linux for the first time: 88 tests, no failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@greenfire27
greenfire27 merged commit 7374690 into development Aug 8, 2026
18 checks passed
@greenfire27
greenfire27 deleted the linux-path-copy-rename branch August 8, 2026 04:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant