From f20cec73332b087d01cda6f94c7562ed22ea2d9b Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Tue, 13 Oct 2026 13:49:12 -0500 Subject: [PATCH] Pathname and drive letter enhancements. --- docs/dvx_basic_reference.html | 11 +- docs/dvx_system_reference.html | 65 ++++-- screenshots/86box/cpanel-cp-types.png | 4 +- screenshots/86box/filedlg-fd-back.png | 3 + screenshots/86box/filedlg-fd-list.png | 3 + screenshots/86box/filedlg-fd-nordy.png | 3 + screenshots/86box/filedlg-fd-open.png | 3 + screenshots/86box/paths-paths.png | 3 + screenshots/dosbox/cpanel-cp-types.png | 4 +- screenshots/dosbox/filedlg-fd-back.png | 3 + screenshots/dosbox/filedlg-fd-list.png | 3 + screenshots/dosbox/filedlg-fd-nordy.png | 3 + screenshots/dosbox/filedlg-fd-open.png | 3 + screenshots/dosbox/paths-paths.png | 3 + screenshots/masks/pardemo-pardemo.mask | 3 + src/apps/kpunch/basdemo/basdemo.dbp | 5 +- src/apps/kpunch/basdemo/basdemo.frm | 4 +- src/apps/kpunch/cpanel/cpSounds.c | 17 +- src/apps/kpunch/dvxbasic/basBuild.c | 55 +---- src/apps/kpunch/dvxbasic/basBuild.h | 3 - src/apps/kpunch/dvxbasic/basdev/basProject.c | 76 ++++-- src/apps/kpunch/dvxbasic/formrt/formCtl.c | 8 +- src/apps/kpunch/dvxbasic/formrt/formrt.c | 2 +- src/apps/kpunch/dvxbasic/ide/ideMain.c | 26 +-- src/apps/kpunch/dvxbasic/ide/ideProject.c | 15 +- src/apps/kpunch/dvxbasic/ide/ideProperties.c | 15 +- src/apps/kpunch/dvxbasic/ideguide.dhs | 3 + src/apps/kpunch/dvxbasic/langref.dhs | 10 +- src/apps/kpunch/dvxbasic/runtime/vm.c | 32 +-- src/apps/kpunch/dvxbasic/stub/bascomp.c | 6 +- src/apps/kpunch/dvxbasic/stub/basrun.c | 2 +- src/apps/kpunch/dvxhelp/dvxhelp.c | 2 +- src/apps/kpunch/iconed/iconed.dbp | 7 +- src/apps/kpunch/iconed/iconed.frm | 2 +- src/apps/kpunch/progman/userHelp.dhs | 3 +- src/include/basic/fileops.bas | 5 + src/installer/pkgRead.c | 32 +-- src/installer/pkgRead.h | 2 +- src/installer/setup.c | 2 + src/installer/setupCore.c | 10 +- src/libs/kpunch/audio/audio.h | 2 +- src/libs/kpunch/audio/audioBas.c | 8 +- src/libs/kpunch/dvxshell/dvxshell.dhs | 15 +- src/libs/kpunch/dvxshell/pmModel.c | 38 ++- src/libs/kpunch/dvxshell/shellApp.c | 104 ++------- src/libs/kpunch/dvxshell/shellApp.h | 6 +- src/libs/kpunch/dvxshell/shellAssoc.c | 21 ++ src/libs/kpunch/dvxshell/shellBas.c | 4 +- src/libs/kpunch/dvxshell/shellProg.c | 6 +- src/libs/kpunch/dvxshell/shellRun.c | 2 +- src/libs/kpunch/fileops/fileOps.c | 93 +++----- src/libs/kpunch/fileops/fileOps.h | 13 +- src/libs/kpunch/fileops/fileOpsBas.c | 9 + src/libs/kpunch/fileops/fileOpsSys.c | 16 +- src/libs/kpunch/fileops/fileops.dhs | 10 +- src/libs/kpunch/libdvx/apiref.dhs | 46 +++- src/libs/kpunch/libdvx/arch.dhs | 2 +- src/libs/kpunch/libdvx/dvxApp.c | 3 +- src/libs/kpunch/libdvx/dvxDialog.c | 154 +++++++++--- src/libs/kpunch/libdvx/dvxPrefs.c | 4 +- src/libs/kpunch/libdvx/platform/dvxPlat.h | 63 ++++- .../kpunch/libdvx/platform/dvxPlatformDos.c | 29 ++- .../kpunch/libdvx/platform/dvxPlatformUtil.c | 220 +++++++++++++++--- src/libs/kpunch/print/printBas.c | 2 +- src/libs/kpunch/print/printJob.c | 1 + src/libs/kpunch/xfer/xfer.c | 15 +- src/libs/kpunch/xfer/xfer.h | 2 +- src/libs/kpunch/xfer/xferBas.c | 7 +- src/loader/loaderMain.c | 11 +- src/test/basdev/testBasDev.c | 2 +- src/test/dialogs/dialogsTest.c | 51 +++- src/test/dos/fixtures/pathsmk/pathsmk.dbp | 19 ++ src/test/dos/fixtures/pathsmk/pathsmk.frm | 133 +++++++++++ src/test/dos/scripts/filedlg.scr | 32 +++ src/test/dos/scripts/paths.scr | 19 ++ src/test/fileops/fileOpsTest.c | 16 +- src/test/formrt/testFormRt.c | 26 ++- src/test/ide/testIde.c | 2 +- src/test/lint/analyzerBaseline.txt | 2 + src/test/lint/clangTidyBaseline.txt | 4 +- src/test/samples/golden/basdemo-debug.app | Bin 24376 -> 25653 bytes src/test/samples/golden/basdemo-release.app | Bin 21093 -> 22370 bytes src/test/samples/golden/fileMgr-debug.app | Bin 34458 -> 34477 bytes src/test/samples/golden/fileMgr-release.app | Bin 29253 -> 29272 bytes src/test/samples/golden/iconed-debug.app | Bin 17066 -> 18333 bytes src/test/samples/golden/iconed-release.app | Bin 14480 -> 15747 bytes src/test/samples/golden/inc-fileops-debug.app | Bin 7379 -> 7398 bytes .../samples/golden/inc-fileops-release.app | Bin 7327 -> 7346 bytes src/test/samples/golden/terminal-debug.app | Bin 46341 -> 46360 bytes src/test/samples/golden/terminal-release.app | Bin 41089 -> 41108 bytes src/test/shell/progmanTest.c | 4 +- src/test/unit/testUtil.c | 50 +++- src/tools/dvxhlpc.c | 12 +- 93 files changed, 1181 insertions(+), 558 deletions(-) create mode 100644 screenshots/86box/filedlg-fd-back.png create mode 100644 screenshots/86box/filedlg-fd-list.png create mode 100644 screenshots/86box/filedlg-fd-nordy.png create mode 100644 screenshots/86box/filedlg-fd-open.png create mode 100644 screenshots/86box/paths-paths.png create mode 100644 screenshots/dosbox/filedlg-fd-back.png create mode 100644 screenshots/dosbox/filedlg-fd-list.png create mode 100644 screenshots/dosbox/filedlg-fd-nordy.png create mode 100644 screenshots/dosbox/filedlg-fd-open.png create mode 100644 screenshots/dosbox/paths-paths.png create mode 100644 screenshots/masks/pardemo-pardemo.mask create mode 100644 src/test/dos/fixtures/pathsmk/pathsmk.dbp create mode 100644 src/test/dos/fixtures/pathsmk/pathsmk.frm create mode 100644 src/test/dos/scripts/filedlg.scr create mode 100644 src/test/dos/scripts/paths.scr diff --git a/docs/dvx_basic_reference.html b/docs/dvx_basic_reference.html index cb98113a..13736413 100644 --- a/docs/dvx_basic_reference.html +++ b/docs/dvx_basic_reference.html @@ -505,6 +505,7 @@ img { max-width: 100%; }
  • FileOpsMakePath
  • FileOpsPathIsAbsolute
  • FileOpsPathIsInside
  • +
  • FileOpsPathNative$
  • FileOpsPathSep$
  • FileOpsReadOnlySystem
  • FileOpsReplaceFile
  • @@ -819,6 +820,7 @@ img { max-width: 100%; }
  • PARAMARRAY
  • Parent
  • Paste
  • +
  • Path Separators
  • PCOPY
  • PEN
  • PEN ON
  • @@ -2185,6 +2187,9 @@ End Sub
  • Appending to a String variable with s = s + x or s = s & x no longer copies the whole string each time, so building long strings in a loop is much faster. When x is InputBox$, MsgBox, CreateForm, CreateControl or Shell, s is still read before x runs (left to right).
  • And, Or, Xor, Eqv and Imp of two Booleans give a Boolean, and Not of a Boolean gives a Boolean, as in Visual Basic 4 and later: Print (a > 1) Or (b > 1) prints True instead of -1, and TypeName gives "Boolean". With a number on either side they still work on the bits and give a number.
  • Big disks and long file names: where the disk takes long file names (DVX under Windows, or a long file name driver loaded before DVX), OPEN, KILL, NAME, MKDIR, DIR$ and the other file statements use them, and the File Manager, the file dialogs and Terminal downloads keep them. A file can be bigger than 2 GB on a FAT32 disk; LOF, FILELEN, LOC and SEEK raise error 6 (Overflow) for a size or position past 2 GB rather than give a wrong number, and EOF stays right below it. FileOpsFreeBytes and FileOpsTotalBytes are right for disks past 2 GB.
  • +
  • Paths may be written with / or \ wherever a path is taken (OPEN, KILL, NAME, MKDIR, CHDIR, DIR$, FILELEN and the file and folder functions of the libraries), mixed too; paths DVX gives back (CURDIR$, App.Path, App.Data, the file dialogs, the FileOps results) use \, as DOS shows them. New: FileOpsPathNative$ gives a path of your own that form. DIR$("C:\*.*") lists the root of C: (before, it listed C:'s current folder). SHELL commands still go to DOS as written, where / starts a switch.
  • +
  • The IDE shows a project's file paths with \ and keeps writing them with / in the .dbp file, so a project reads the same everywhere.
  • +
  • The Open and Save As dialogs (DlgFileOpen$, DlgFileSave$ and the IDE's own) have a Drive list beside the Path box (Alt+D); a drive with no disk in it says it is not ready.
  • Changed: FileOpsValidName takes the folder the name is for, FileOpsValidName(folder, itemName), since the rules depend on its disk; XferSafeName$ takes the folder a file is received into, XferSafeName$(name, folder). New: FileOpsLongNames, FileOpsShortPath$, FileOpsValidLongName and FileOpsValidShortName (fileops.bas), and ShellArgQuote$ (shell.bas), which puts a path with spaces in quotes for a command line.
  • Programs compiled with an older release must be rebuilt. A program whose procedure table records more than 64 locals for a procedure is refused when it is loaded.
  • A class may have at most 65536 member variables and a form at most 65536 form-level variables (as many as the compiled program can number); more is a compile error ("Too many member variables in X", "Too many form-level variables"), and a program file that records more is refused when it is loaded.
  • @@ -3287,6 +3292,7 @@ ErrorHandler:

    When used as a function, SHELL returns the exit code of the command.

    Shell "DIR /B"
     exitCode = Shell("COPY A.TXT B.TXT")
    +

    The command goes to DOS as it is written. DOS commands read / as the start of a switch (DIR /B), so write the paths in a command with \ (SHELL "DEL C:\TEMP\X.TXT"); FileOpsPathNative$ (fileops.bas) turns a path built with / into that form. Everywhere else in DVX BASIC a path may use / or \.

    SLEEP

    Pauses execution for the given number of seconds. With no argument, sleeps for 1 second. The desktop and the program's own forms keep running while it sleeps, SLEEP 0 just lets pending events run, and closing the program ends the sleep.

    SLEEP [seconds]
    @@ -3624,6 +3630,7 @@ pos = SEEK(channel) ' Function: get current position

    File System Statements

    DVX BASIC provides statements for working with the file system -- creating, deleting, renaming, and copying files and directories, and reading or setting file attributes.

    +

    A path may be written with / or \ between its parts, mixed too: OPEN "DATA/SCORES.TXT" and OPEN "DATA\SCORES.TXT" open the same file, and so do the FileOps, Shell and other library functions that take a path (SHELL commands are the exception: see SHELL). Paths DVX gives back -- CURDIR$, App.Path, App.Data, the file dialogs, FileOpsPathJoin$ and the other FileOps results -- use \, the separator DOS shows; FileOpsPathNative$ gives a path of your own that form before you show it.

    KILL

    Deletes a file.

    KILL filename$
    @@ -3666,7 +3673,7 @@ End If SetAttr "secret.dat", vbHidden + vbReadOnly

    CURDIR$ / DIR$

    -

    CURDIR$ returns the current directory. DIR$ returns the first file matching a pattern; subsequent calls with no arguments return additional matches (or "" when done). DIR$ returns only files: never ".", ".." or subdirectories.

    +

    CURDIR$ returns the current directory. DIR$ returns the first file matching a pattern; subsequent calls with no arguments return additional matches (or "" when done). DIR$ returns only files: never ".", ".." or subdirectories. DIR$("C:\*.*") lists the root of C: (DIR$("C:*.*") the drive's current folder).

    cwd$ = CURDIR$
     first$ = DIR$("*.txt")
     WHILE first$ <> ""
    @@ -5003,7 +5010,7 @@ Sub cmdOK_Click ()
     End Sub

    Files and Folders

    With fileops.bas in the project, a program can show and change what is on the disks. FileOpsList(folder, filter) reads a folder once -- its folders and the files matching filter -- and returns a listing (a number; FileOpsListFree frees it). FileOpsListRows$ gives the whole listing as name, size, type and date columns, ready for a ListView's SetRows, so even a folder of hundreds of files fills the list at once; FileOpsListSort re-sorts it by name, size, type or date with folders first. FileOpsListName$, FileOpsListIsFolder and FileOpsListSize read single entries, and FileOpsListFind finds one by name.

    -

    Paths: FileOpsPathNormalize$ (the full path of an existing folder, or ""), FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (True for a path that is complete by itself, such as "C:\DOCS", False for one that counts from the current folder), FileOpsPathIsInside(path, folder) (True when path is folder itself or anywhere inside it: check it before copying or moving a folder, so it is never copied into itself), FileOpsPathSep$ (the character between folder names, "\" on DOS), FileOpsBaseName$ and FileOpsExtension$. FileOpsFilterMatch(itemName, filter) tells whether a name matches a filter the way the file dialogs match it: wildcards * and ?, several patterns separated by ; such as "*.bmp;*.png", case not mattering.

    +

    Paths: FileOpsPathNormalize$ (the full path of an existing folder, or ""), FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (True for a path that is complete by itself, such as "C:\DOCS", False for one that counts from the current folder), FileOpsPathIsInside(path, folder) (True when path is folder itself or anywhere inside it: check it before copying or moving a folder, so it is never copied into itself), FileOpsPathSep$ (the character between folder names, "\" on DOS), FileOpsPathNative$ (a path written with that character, for showing it: "C:/DOCS/A.TXT" becomes "C:\DOCS\A.TXT"), FileOpsBaseName$ and FileOpsExtension$. Every one of them takes / and \ alike and gives back \. FileOpsFilterMatch(itemName, filter) tells whether a name matches a filter the way the file dialogs match it: wildcards * and ?, several patterns separated by ; such as "*.bmp;*.png", case not mattering.

    Files: FileOpsMakeDir and FileOpsRename (FileOpsError$ says why one failed), FileOpsTempPath$ and FileOpsReplaceFile (save safely: write the new file to FileOpsTempPath$(path$), then FileOpsReplaceFile swaps it in; the old file comes back if that fails), FileOpsValidName(folder, itemName) (whether a name can be used for a new file or folder in folder, by the rules of its disk; FileOpsError$ says why not), FileOpsValidLongName and FileOpsValidShortName (the long-name or the 8.3 rules by themselves, whatever the disk takes), FileOpsShortPath$ (the short 8.3 form of an existing path, for an old program that cannot take long names or blanks), FileOpsMakePath (a folder and every missing folder above it, which MKDIR does not make), FileOpsIsFolder, FileOpsGetAttr and FileOpsSetAttr (the FILEOPS_ATTR_ bits), FileOpsModified$ and FileOpsSetModified (when a file was last changed, read and set in the form "YYYY-MM-DD HH:MM"; FileOpsSetModified returns False for a date that does not exist), FileOpsTreeSize with FileOpsTreeBytes, FileOpsTreeFiles and FileOpsTreeFolders (the size of a whole folder) and FileOpsFormatSize$.

    Drives: FileOpsDriveList$ (the drive letters), FileOpsDriveType (FILEOPS_DRIVE_FIXED for a hard disk, FILEOPS_DRIVE_REMOVABLE for a floppy disk drive, FILEOPS_DRIVE_REMOTE for a network drive or a CD-ROM; asking never makes a drive spin up), FileOpsBootDrive$ (the drive the computer started from), FileOpsFreeBytes, FileOpsFreeKb and FileOpsTotalBytes (the size of the disk; right for disks past 2 GB too), FileOpsLongNames(path) (True when the disk holding path takes long file names: up to 255 characters, with spaces and several dots; otherwise names are 8.3). FileOpsIsWritable(folder) is False where files cannot be made (a CD-ROM, a write-protected floppy), and FileOpsReadOnlySystem() is True when DVX itself runs from such a disk, so settings cannot be saved in its folder. FileOpsTempDir$() is the folder for scratch files ("" when there is none).

    FileOpsCopyTree, FileOpsMoveTree and FileOpsDeleteTree do a whole folder at once. For a long copy, move or delete the program stays usable when it runs the work as a job, a little at a time from a Timer: FileOpsJobCreate, then FileOpsJobAddSource for each file or folder, then FileOpsJobRunFor from the timer until it returns FILEOPS_STEP_DONE or FILEOPS_STEP_CANCELLED. When it returns FILEOPS_STEP_ASK (a file already exists, a file is read-only) or FILEOPS_STEP_ERROR, show FileOpsJobMessage$ and pass the user's choice to FileOpsJobAnswer. FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone and FileOpsJobBytesTotal show the progress, and FileOpsJobCancel stops the job. Files copied from a CD-ROM are read-only; FileOpsJobClearAttr job, FILEOPS_ATTR_READONLY before the job runs makes every copy changeable (FILEOPS_ATTR_EDITABLE clears all four attribute bits). Whatever a program leaves open is freed when it ends.

    diff --git a/docs/dvx_system_reference.html b/docs/dvx_system_reference.html index 27e4013e..16f789ac 100644 --- a/docs/dvx_system_reference.html +++ b/docs/dvx_system_reference.html @@ -1028,6 +1028,7 @@ img { max-width: 100%; }
  • Page Setup
  • Palette
  • Palette
  • +
  • Path Separators
  • PC speaker
  • Pixel Format
  • PixelFormatT
  • @@ -1071,8 +1072,15 @@ img { max-width: 100%; }
  • platformMicros
  • platformMousePoll
  • platformNormalizeFKeyScan
  • +
  • platformPathDirName
  • platformPathIsAbsolute
  • +
  • platformPathIsSep
  • +
  • platformPathIsUnder
  • +
  • platformPathJoin
  • +
  • platformPathNative
  • +
  • platformPathSame
  • platformPathSibling
  • +
  • platformPathTidy
  • platformReplaceFile
  • platformSetAppExitHook
  • platformSetLogMemory
  • @@ -1303,7 +1311,6 @@ img { max-width: 100%; }
  • ShellOpenDocument
  • shellOpenHelp
  • shellOpenTaskManager
  • -
  • shellPathsMatch
  • ShellPriority
  • ShellProgramCount
  • ShellProgramDescription
  • @@ -2096,6 +2103,7 @@ img { max-width: 100%; }

    Parts of the Dialog Box

    Copying and Pasting

    @@ -2589,7 +2597,7 @@ img { max-width: 100%; } dvxMessageBox Generic message box with button/icon flags (MB_OK, MB_YESNO, MB_ICONERROR, etc.) dvxErrorBox Shortcut for MB_OK + MB_ICONERROR dvxInfoBox Shortcut for MB_OK + MB_ICONINFO - dvxFileDialog File open/save dialog with directory navigation and filter dropdown + dvxFileDialog File open/save dialog with directory navigation, a drive dropdown and a filter dropdown dvxInputBox Single-line text input with OK/Cancel dvxIntInputBox Integer input with spinner, clamp range, and step dvxChoiceDialog List selection with OK/Cancel @@ -5583,7 +5591,7 @@ int32_t pick = dvxButtonBox(ctx, "Copy", "The disk is full." FD_SAVE 0x0001 File save dialog (with overwrite prompt)

    dvxFileDialog

    bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const char *initialDir, const FileFilterT *filters, int32_t filterCount, char *outPath, int32_t outPathSize);
    -

    Display a modal file open/save dialog. Shows a directory listing with parent/drive-letter navigation, a filename input, and an optional filter dropdown. The path box always shows the current folder. Typing "D:" or "D:folder" in the path box changes drive/folder; the file name box also accepts a relative or absolute path ("sub\x.txt", "C:\X\A.TXT") and navigates to its folder. A folder that does not exist shows an "Invalid Path" error. The dialog closes at once if the app that owns it is ended.

    +

    Display a modal file open/save dialog. Shows a directory listing with parent/drive-letter navigation, a drive dropdown, a filename input, and an optional filter dropdown. The path box always shows the current folder, and the drive dropdown (platformDriveList, so listing the drives never touches empty floppy or CD-ROM drives) its drive; choosing a drive goes to its root, and a drive that cannot be read shows "Drive X: is not ready." and leaves the dialog where it was. Typing "D:" or "D:folder" in the path box changes drive/folder; the file name box also accepts a relative or absolute path ("sub\x.txt", "C:\X\A.TXT") and navigates to its folder. A folder that does not exist shows an "Invalid Path" error. The dialog closes at once if the app that owns it is ended.

      Parameter     Description
       ---------     -----------
       ctx           Application context
    @@ -6326,7 +6334,7 @@ bool        platformShortPath(const char *path, char *out, int32_t outSize);platformLongNames is true when the drive holding path (NULL or "": the current drive) takes long file names. On DOS that is a loaded long-name driver covering that drive; elsewhere always.

    platformValidateLongName and platformValidateShortName are the two DOS name rules by themselves, whatever the disk takes; each returns NULL or why the name is not valid. A long name has 1 to PLATFORM_LONG_NAME_MAX characters and may hold spaces, several dots and + , ; = [ ]; it may not hold a control character or \ / : * ? " < > |, and may not end with a dot or a space (DOS drops them). An 8.3 name has 1 to 8 printable ASCII characters, then at most one dot and up to 3 more, none of them a space or " * + , / : ; < = > ? [ \ ] |. Neither may be . or .., or a device name (CON, PRN, AUX, NUL, CLOCK$, COM1 to COM9, LPT1 to LPT9) before its first dot. They are shared code, so the host tests check the DOS rules too.

    platformLongNameChar is true when a long name may hold character c; a file transfer uses it to turn the characters a sender's name cannot keep into _.

    -

    platformShortPath writes the short (8.3) form of an existing path into out, for what reads names before a long-name driver loads (AUTOEXEC.BAT) or cannot take blanks (an old program's command line). On a drive with long names DOS gives every part's short name (INT 21h AX=7160h CL=1); elsewhere, and when DOS cannot, path itself is copied. False when out is too small.

    +

    platformShortPath writes the short (8.3) form of an existing path into out, for what reads names before a long-name driver loads (AUTOEXEC.BAT) or cannot take blanks (an old program's command line). On a drive with long names DOS gives every part's short name (INT 21h AX=7160h CL=1); elsewhere, and when DOS cannot, path itself is copied (platformPathTidy: "C:/DVX" gives "C:\\DVX"). False when out is too small.

    platformStatSize

    uint64_t platformStatSize(const struct stat *st);

    The size stat reported for a file. A FAT32 file reaches 4 GB - 1 byte, but the DOS C library's st_size is a signed 32-bit off_t, so a file past 2 GB reads as negative there; platformStatSize reads it as the unsigned 32-bit size DOS keeps. Use it wherever a file size comes from stat or fstat.

    @@ -6336,18 +6344,36 @@ bool platformShortPath(const char *path, char *out, int32_t outSize);platformChdir
    int32_t platformChdir(const char *path);

    Change the working directory, including drive letter on DOS (switches drive first when the path starts with "X:"). If the chdir fails the previous drive is restored. A drive prefix that is not a letter is rejected. Returns 0 on success, -1 on failure.

    +

    Path separators: platformPathIsSep / platformPathNative / platformPathSame / platformPathIsUnder / platformPathJoin / platformPathDirName / platformPathTidy

    +
    bool        platformPathIsSep(char c);
    +char       *platformPathNative(char *path);
    +bool        platformPathSame(const char *a, const char *b);
    +const char *platformPathIsUnder(const char *path, const char *dir);
    +bool        platformPathJoin(char *out, int32_t outSize, const char *dir, const char *name);
    +bool        platformPathDirName(const char *path, char *out, int32_t outSize);
    +char       *platformPathTidy(char *path);
    +

    DVX paths follow DOS rules on every platform: '/' and '\\' are the same separator, so a path a program writes with '/' works on DOS exactly as with '\\', and names compare without case (DOS and the host build also share INI files, projects and program paths). Every path DVX shows uses the native separator, DVX_PATH_SEP ('\\' on DOS). DVX's own path code goes through these functions; a program that splits, compares or joins paths should too, never testing for one separator by hand.

    +
      +
    • platformPathIsSep: true for '/' and '\\'.
    • +
    • platformPathNative: rewrites path's separators as DVX_PATH_SEP, in place, before it is shown (the DOS C library's getcwd and realpath give "c:/dvx"). Returns path.
    • +
    • platformPathSame: true when a and b name the same place by DOS rules: separators alike, a trailing one ignored, case ignored. Text only: "." or a relative path is not resolved.
    • +
    • platformPathIsUnder: when path is dir or lies below it, the rest of path after dir and its separator ("" for dir itself); NULL otherwise.
    • +
    • platformPathJoin: dir and name with one separator between them (none added when dir ends in one), every separator native; an absolute name, or an empty dir, gives name alone. False when it does not fit.
    • +
    • platformPathDirName: the folder part of path in native separators: a root keeps its separator ("C:\\X.TXT" gives "C:\\", since "C:" alone is the drive's current folder), "C:X.TXT" gives "C:", a bare name gives ".". False when it does not fit.
    • +
    • platformPathTidy: platformPathNative, then each run of separators made one (a leading pair, a network "\\\\server", kept) and a trailing separator dropped unless it is a root's: the form for a path handed to DOS directly, without the C library in between. platformShortPath and the file operations library's attribute calls use it.
    • +

    platformPathDirEnd

    char *platformPathDirEnd(const char *path);
    -

    Return a pointer to the last directory separator in path, or NULL. On DOS accepts both "/" and "\\".

    +

    Return a pointer to the last directory separator in path (platformPathIsSep), or NULL.

    platformPathIsAbsolute

    bool platformPathIsAbsolute(const char *path);
    -

    True for a path rooted at a separator ('/' or '\\') or qualified with a drive letter ("C:..."). Both separators count on every platform. A NULL path is not absolute.

    +

    True for a path rooted at a separator ('/' or '\\') or qualified with a drive letter ("C:..."). A NULL path is not absolute.

    platformPathBaseName

    const char *platformPathBaseName(const char *path);
    -

    Return a pointer to the leaf (basename) portion of path.

    +

    Return a pointer to the leaf (basename) portion of path: after the last separator, or after a drive ("C:A.TXT" gives "A.TXT").

    platformCopyFile

    bool platformCopyFile(const char *srcPath, const char *dstPath);
    -

    Byte-for-byte file copy. Returns false on any open, read, or write failure; a partially written destination is removed so no truncated copy is left behind.

    +

    Byte-for-byte file copy. Returns false on any open, read, or write failure; a partially written destination is removed so no truncated copy is left behind. A copy onto itself (platformPathSame) does nothing and returns true: opening the copy would otherwise empty the original first.

    platformPathSibling

    #define PLATFORM_TMP_EXT         ".$"
     #define PLATFORM_BAK_EXT         ".~"
    @@ -6398,7 +6424,7 @@ bool        platformDirIsWritable(const char *dir);
     const char *platformTempDir(void);
     void        platformSetLogMemory(void);
     const char *platformLogMemory(void);
    -

    The loader tests at startup whether the DVX folder can be written to (platformDirIsWritable creates and removes a scratch file in dir) and records the answer with platformSetReadOnly. When DVX runs from a CD-ROM or a write-protected disk (platformIsReadOnly is true): the log goes to DVX.LOG in the TEMP folder (platformTempDir: the TEMP or else TMP variable, NULL when neither is set) or, without one, to memory; the boot-time help rebuild is skipped (its output and counts could not be saved); prefsSave and prefsSaveAs keep a file they cannot write in memory, and prefsLoad reads it back until DVX exits; the shell says so once at startup; and a program that allows several copies runs only once when there is no TEMP folder (a second copy is a temporary copy of the module). platformSetLogMemory sends dvxLog to a buffer holding the latest PLATFORM_LOG_MEM_MAX bytes (the oldest whole lines are dropped), which platformLogMemory returns; platformSetLogPath goes back to a file. SETUP.EXE logs to memory unless /LOG= is given, so it never writes to its own disk.

    +

    The loader tests at startup whether the DVX folder can be written to (platformDirIsWritable creates and removes a scratch file in dir) and records the answer with platformSetReadOnly. When DVX runs from a CD-ROM or a write-protected disk (platformIsReadOnly is true): the log goes to DVX.LOG in the TEMP folder (platformTempDir: the TEMP or else TMP variable in native separators, NULL when neither is set) or, without one, to memory; the boot-time help rebuild is skipped (its output and counts could not be saved); prefsSave and prefsSaveAs keep a file they cannot write in memory, and prefsLoad reads it back until DVX exits; the shell says so once at startup; and a program that allows several copies runs only once when there is no TEMP folder (a second copy is a temporary copy of the module). platformSetLogMemory sends dvxLog to a buffer holding the latest PLATFORM_LOG_MEM_MAX bytes (the oldest whole lines are dropped), which platformLogMemory returns; platformSetLogPath goes back to a file. SETUP.EXE logs to memory unless /LOG= is given, so it never writes to its own disk.

    platformLineEnding / platformStripLineEndings

    const char *platformLineEnding(void);
     int32_t     platformStripLineEndings(char *buf, int32_t len);
    @@ -7609,10 +7635,7 @@ bool shellCloseApp(AppContextT *ctx, int32_t appId);

    Bring an app to the front, as the Task Manager's Switch To does (it calls this): its topmost visible window that is not being destroyed is restored when minimized (dvxRestoreWindow; a maximized window stays maximized), else raised (dvxRaiseWindow), and gets the focus. False when the app has no visible window. BASIC: ShellRunningActivate.

    shellAppFindByPath

    ShellAppT *shellAppFindByPath(const char *path);
    -

    Return an active app slot loaded from path, or NULL. Paths are compared with shellPathsMatch.

    -

    shellPathsMatch

    -
    bool shellPathsMatch(const char *a, const char *b);
    -

    True when two paths name the same file under DOS rules: case-insensitive, with / and \ the same separator.

    +

    Return an active app slot loaded from path, or NULL. Paths are compared with platformPathSame (/ and \ the same separator, a trailing one ignored, case ignored on DOS).

    Starting Up

    @@ -7713,7 +7736,7 @@ void shellOpenTaskManager(AppContextT *ctx); } ShellProgramT;

    shellProgramScan

    int32_t shellProgramScan(const char *dir, const char *skipPath, bool readInfo, ShellProgressFnT progress, void *progressCtx, ShellProgramT **outList);
    -

    Every program under dir (NULL for SHELL_PROG_DIR) and its subfolders, except skipPath (compared with shellPathsMatch; NULL for none), in the order found. With readInfo each entry is filled by shellProgramInfo; without it only path is set, which is fast (no program file is opened) and enough to notice programs being added or removed. progress, when not NULL, is called with progressCtx after each program (for example to keep the screen responsive). Returns the count; *outList is an array of that many entries, NULL when there are none. Free it with shellProgramListFree.

    +

    Every program under dir (NULL for SHELL_PROG_DIR) and its subfolders, except skipPath (compared with platformPathSame; NULL for none), in the order found. With readInfo each entry is filled by shellProgramInfo; without it only path is set, which is fast (no program file is opened) and enough to notice programs being added or removed. progress, when not NULL, is called with progressCtx after each program (for example to keep the screen responsive). Returns the count; *outList is an array of that many entries, NULL when there are none. Free it with shellProgramListFree.

    shellProgramInfo

    void shellProgramInfo(const char *path, ShellProgramT *out);

    Fill out with the program's path, display name and description. Always succeeds: without resources (or for a file that is not a program) the name is the file name without its extension, first letter capitalized, and the description is empty.

    @@ -7825,7 +7848,7 @@ int32_t ShellRunningClose(int32_t appId); int32_t ShellRunningKill(int32_t appId); int32_t ShellAppIsHung(int32_t appId); int32_t ShellAppMemory(int32_t appId); -

    The Task Manager's operations, one routine each with it. ShellRunningId is shellRunningAppId. ShellRunningName and ShellRunningPath are the ShellAppT name and path of an app in AppStateRunningE ("" otherwise; the same state every call below requires). ShellRunningFind is shellAppFindByPath (shellPathsMatch rules), 0 when that app is not running. ShellRunningActivate is shellActivateApp. ShellRunningClose is shellRequestAppClose: the app is asked at the main loop's top level, after the BASIC code has returned; True when the request was recorded. ShellRunningKill is shellForceKillApp followed by shellDesktopUpdate (End Task: no shutdownFn, callbacks purged, the unload deferred while the app's code is on a stack); the running program itself is refused (BASIC's END ends it). ShellAppIsHung is dvxAppIsHung and ShellAppMemory dvxMemGetAppUsage (clamped to INT32_MAX; 0 for an app that is not running).

    +

    The Task Manager's operations, one routine each with it. ShellRunningId is shellRunningAppId. ShellRunningName and ShellRunningPath are the ShellAppT name and path of an app in AppStateRunningE ("" otherwise; the same state every call below requires). ShellRunningFind is shellAppFindByPath (platformPathSame rules), 0 when that app is not running. ShellRunningActivate is shellActivateApp. ShellRunningClose is shellRequestAppClose: the app is asked at the main loop's top level, after the BASIC code has returned; True when the request was recorded. ShellRunningKill is shellForceKillApp followed by shellDesktopUpdate (End Task: no shutdownFn, callbacks purged, the unload deferred while the app's code is on a stack); the running program itself is refused (BASIC's END ends it). ShellAppIsHung is dvxAppIsHung and ShellAppMemory dvxMemGetAppUsage (clamped to INT32_MAX; 0 for an app that is not running).

    Command lines and priority

    const char *ShellCommandProgram(const char *command);
     const char *ShellCommandArgs(const char *command);
    @@ -8206,11 +8229,11 @@ dvxSqlExec(db, sql);
    fileOpsMakeDir(dir, name, err, errSize) Create a folder; a reason in err on failure. fileOpsRename(dir, oldName, newName, err, errSize) Rename within a folder; refuses a name already in use. fileOpsValidateName(dir, name, err, errSize) Check a new name for folder dir by the rules of its disk (platformValidateFilename): on DOS long names where the drive takes them (platformLongNames), else 8.3; never a device name. - fileOpsPathJoin(out, size, dir, name) dir + separator + name; false when it does not fit. - fileOpsPathParent(path, out, size) Parent folder; false at a root. + fileOpsPathJoin(out, size, dir, name) dir + separator + name in native separators (platformPathJoin: no doubled separator; an absolute name stands alone); false when it does not fit. + fileOpsPathParent(path, out, size) Parent folder, in native separators; false at a root. fileOpsPathIsRoot(path) True for "C:\", "C:" or "/". - fileOpsPathIsInside(path, dir) True when path is dir or below it. - fileOpsPathSame(a, b) Same place (separators equal, DOS ignores case, a trailing separator ignored). + fileOpsPathIsInside(path, dir) True when path is dir or below it (platformPathIsUnder). + fileOpsPathSame(a, b) Same place (platformPathSame: separators equal, case ignored, a trailing separator ignored). fileOpsPathNormalize(path, out, size) Canonical absolute form of an existing folder (DOS: upper case, except names on a disk with long names, which keep their case).

    Jobs

    A job copies, moves or deletes any number of files and folders (recursively):

    @@ -8257,7 +8280,7 @@ fileOpsJobFree(job);

    fileOpsJobClearAttr(job, attr), called before the first step, clears those attribute bits on every file a copy or move writes: Setup passes FILEOPS_ATTR_READONLY, because every file on a CD-ROM is read-only and the installed copies must not be.

    fileOpsJobRun(job, callbacks) runs a job to the end instead: progress(job, ctx) is called after every step (return false to cancel) and ask(job, kind, message, ctx) answers questions. With no callbacks, existing files are kept (No) and failing items are skipped.

    BASIC

    -

    src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename for a name in a folder), FileOpsValidLongName and FileOpsValidShortName (platformValidateLongName / platformValidateShortName, the rules alone), FileOpsLongNames (platformLongNames), FileOpsShortPath$ (platformShortPath, "" when it does not fit), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$) and jobs (FileOpsJobCreate, FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value.

    +

    src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsPathNative$ (platformPathNative), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename for a name in a folder), FileOpsValidLongName and FileOpsValidShortName (platformValidateLongName / platformValidateShortName, the rules alone), FileOpsLongNames (platformLongNames), FileOpsShortPath$ (platformShortPath, "" when it does not fit), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$) and jobs (FileOpsJobCreate, FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value.

    Listings and jobs are handles (small numbers, 0 = none) into a table owned by the library; each records the app that made it, and the shell's app-cleanup hook frees what an app leaves open when it ends. FileOpsJobRunFor(job, ms) steps the job until the time is used or it needs an answer, so a BASIC Timer can run it without blocking the program. A returned string stays valid until the next call returning one (the BASIC runtime copies it at once). Single files are handled by the built-in FILECOPY, KILL, NAME, MKDIR, RMDIR, GETATTR and SETATTR statements.

    diff --git a/screenshots/86box/cpanel-cp-types.png b/screenshots/86box/cpanel-cp-types.png index e8d0d9b3..cc510c12 100644 --- a/screenshots/86box/cpanel-cp-types.png +++ b/screenshots/86box/cpanel-cp-types.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:539e278fe16fea322ab084e47d2acbcc8044e13cc63ee8fc473956788fe3b95d -size 22658 +oid sha256:362e4f37e6e99b0368da74c2bc15b5e08d9d6bb6267aeddbccdd908745767993 +size 23159 diff --git a/screenshots/86box/filedlg-fd-back.png b/screenshots/86box/filedlg-fd-back.png new file mode 100644 index 00000000..482d1115 --- /dev/null +++ b/screenshots/86box/filedlg-fd-back.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48c7b0b31ec7922f1364cf3e0175896190a6394ad27146ccfa62b0c500465d35 +size 15077 diff --git a/screenshots/86box/filedlg-fd-list.png b/screenshots/86box/filedlg-fd-list.png new file mode 100644 index 00000000..7b764871 --- /dev/null +++ b/screenshots/86box/filedlg-fd-list.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9f468b5c0772dfc324784e10376c88a526a92999de6c6e47b5db09918bdc046 +size 15173 diff --git a/screenshots/86box/filedlg-fd-nordy.png b/screenshots/86box/filedlg-fd-nordy.png new file mode 100644 index 00000000..50946708 --- /dev/null +++ b/screenshots/86box/filedlg-fd-nordy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd9002e8613634671d40f7e48b3fac6b6823475edd695ab527784305b087352a +size 15452 diff --git a/screenshots/86box/filedlg-fd-open.png b/screenshots/86box/filedlg-fd-open.png new file mode 100644 index 00000000..ff2fc78d --- /dev/null +++ b/screenshots/86box/filedlg-fd-open.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b738091c36cea453d9a4aced6ce293bc1e2d073402bc302bebdba8910ecac14a +size 15028 diff --git a/screenshots/86box/paths-paths.png b/screenshots/86box/paths-paths.png new file mode 100644 index 00000000..74b0aa43 --- /dev/null +++ b/screenshots/86box/paths-paths.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91f9cc6604359edf4e355bc5b1eb56e44f0802124ee3a00b41fe1cf667d0ac58 +size 16581 diff --git a/screenshots/dosbox/cpanel-cp-types.png b/screenshots/dosbox/cpanel-cp-types.png index e8d0d9b3..cc510c12 100644 --- a/screenshots/dosbox/cpanel-cp-types.png +++ b/screenshots/dosbox/cpanel-cp-types.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:539e278fe16fea322ab084e47d2acbcc8044e13cc63ee8fc473956788fe3b95d -size 22658 +oid sha256:362e4f37e6e99b0368da74c2bc15b5e08d9d6bb6267aeddbccdd908745767993 +size 23159 diff --git a/screenshots/dosbox/filedlg-fd-back.png b/screenshots/dosbox/filedlg-fd-back.png new file mode 100644 index 00000000..482d1115 --- /dev/null +++ b/screenshots/dosbox/filedlg-fd-back.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48c7b0b31ec7922f1364cf3e0175896190a6394ad27146ccfa62b0c500465d35 +size 15077 diff --git a/screenshots/dosbox/filedlg-fd-list.png b/screenshots/dosbox/filedlg-fd-list.png new file mode 100644 index 00000000..372a7b83 --- /dev/null +++ b/screenshots/dosbox/filedlg-fd-list.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:469c9fea60a56141a1515fd8e01085791abc29fae5a98ff379a3ab77da8dc34c +size 15262 diff --git a/screenshots/dosbox/filedlg-fd-nordy.png b/screenshots/dosbox/filedlg-fd-nordy.png new file mode 100644 index 00000000..50946708 --- /dev/null +++ b/screenshots/dosbox/filedlg-fd-nordy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd9002e8613634671d40f7e48b3fac6b6823475edd695ab527784305b087352a +size 15452 diff --git a/screenshots/dosbox/filedlg-fd-open.png b/screenshots/dosbox/filedlg-fd-open.png new file mode 100644 index 00000000..ff2fc78d --- /dev/null +++ b/screenshots/dosbox/filedlg-fd-open.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b738091c36cea453d9a4aced6ce293bc1e2d073402bc302bebdba8910ecac14a +size 15028 diff --git a/screenshots/dosbox/paths-paths.png b/screenshots/dosbox/paths-paths.png new file mode 100644 index 00000000..74b0aa43 --- /dev/null +++ b/screenshots/dosbox/paths-paths.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91f9cc6604359edf4e355bc5b1eb56e44f0802124ee3a00b41fe1cf667d0ac58 +size 16581 diff --git a/screenshots/masks/pardemo-pardemo.mask b/screenshots/masks/pardemo-pardemo.mask new file mode 100644 index 00000000..a745c0ca --- /dev/null +++ b/screenshots/masks/pardemo-pardemo.mask @@ -0,0 +1,3 @@ +# The ANSI terminal's blinking cursor (column 0 of the row after the last +# line, mostly behind the main window): on or off depends on timing +5 428 8 16 diff --git a/src/apps/kpunch/basdemo/basdemo.dbp b/src/apps/kpunch/basdemo/basdemo.dbp index 9baa3477..3f047df4 100644 --- a/src/apps/kpunch/basdemo/basdemo.dbp +++ b/src/apps/kpunch/basdemo/basdemo.dbp @@ -9,8 +9,9 @@ Icon = ICON32.BMP [Modules] File0 = ../../../include/basic/commdlg.bas -File1 = ../../../include/basic/help.bas -Count = 2 +File1 = ../../../include/basic/fileops.bas +File2 = ../../../include/basic/help.bas +Count = 3 [Classes] File0 = shape.cls diff --git a/src/apps/kpunch/basdemo/basdemo.frm b/src/apps/kpunch/basdemo/basdemo.frm index 57771ea8..26f68e48 100644 --- a/src/apps/kpunch/basdemo/basdemo.frm +++ b/src/apps/kpunch/basdemo/basdemo.frm @@ -644,7 +644,7 @@ SUB btnFileIO_Click DIM path AS STRING DIM f AS INTEGER - path = App.Data + "/demo.txt" + path = FileOpsPathJoin$(App.Data, "demo.txt") Say "Writing: " + path f = FREEFILE @@ -699,7 +699,7 @@ SUB btnIni_Click Header "INI Read/Write" DIM path AS STRING - path = App.Data + "/demo.ini" + path = FileOpsPathJoin$(App.Data, "demo.ini") Say "Writing: " + path IniWrite path, "General", "UserName", "Scott" diff --git a/src/apps/kpunch/cpanel/cpSounds.c b/src/apps/kpunch/cpanel/cpSounds.c index 87059dc2..0c60c28b 100644 --- a/src/apps/kpunch/cpanel/cpSounds.c +++ b/src/apps/kpunch/cpanel/cpSounds.c @@ -160,15 +160,22 @@ static int32_t addDriver(const char *name, const char *label) { // The item of a sound file, added when it is not listed yet ("" is -// "(None)"). Files are told apart ignoring case: DOS names have none. +// "(None)"). Files are kept with native separators (DVX.INI may say +// "config/sounds/x.wav") and told apart ignoring case: DOS names have +// none. static int32_t addFile(const char *path) { + char native[DVX_MAX_PATH]; + + snprintf(native, sizeof(native), "%s", path); + platformPathNative(native); + for (int32_t i = 0; i < (int32_t)arrlen(sFileValues); i++) { - if (strcasecmp(sFileValues[i], path) == 0) { + if (strcasecmp(sFileValues[i], native) == 0) { return i; } } - char *value = strdup(path); + char *value = strdup(native); if (!value) { return 0; @@ -179,7 +186,7 @@ static int32_t addFile(const char *path) { if (!value[0]) { label = "(None)"; - } else if (strncasecmp(value, AUDIO_SOUNDS_DIR, dirLen) == 0 && value[dirLen] == '/') { + } else if (strncasecmp(value, AUDIO_SOUNDS_DIR, dirLen) == 0 && value[dirLen] == DVX_PATH_SEP[0]) { label = value + dirLen + 1; } @@ -215,7 +222,7 @@ static void addSoundsDir(void) { if (dvxHasExt(names[i], sSoundExts[e])) { char path[DVX_MAX_PATH]; - snprintf(path, sizeof(path), "%s/%s", AUDIO_SOUNDS_DIR, names[i]); + snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", AUDIO_SOUNDS_DIR, names[i]); addFile(path); } } diff --git a/src/apps/kpunch/dvxbasic/basBuild.c b/src/apps/kpunch/dvxbasic/basBuild.c index f938c494..c08cb54f 100644 --- a/src/apps/kpunch/dvxbasic/basBuild.c +++ b/src/apps/kpunch/dvxbasic/basBuild.c @@ -59,7 +59,6 @@ #include "stb_ds_wrap.h" -#include #include #include #include @@ -74,10 +73,6 @@ // Longest progress line handed to the log callback. #define BAS_BUILD_LOG_BUF 128 -// Separator used when joining project-relative paths. '/' is accepted by -// DJGPP as well as every host OS bascomp runs on; DVX_PATH_SEP is not. -#define BAS_BUILD_SEP "/" - // Growth unit of the project source buffer. #define BAS_BUILD_SOURCE_CHUNK 8192 // Room a wrapper line needs besides its directive, name and control @@ -133,7 +128,6 @@ bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, i BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit, const BasCtrlValidatorT *validator, const BasCompilerT **liveCompiler, BasCompileErrorT *err); bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out); const char *basBuildControl(const char *outPath, const BasBuildSpecT *spec); -void basBuildDirOf(const char *path, char *out, int32_t outSize); const char *basBuildErrorText(const char *message); void basBuildFreeProject(BasProjectSourceT *out); bool basBuildIsControlProject(PrefsHandleT *prefs); @@ -151,7 +145,6 @@ static const char *emitCtlTexts(const char *outPath, const BasBuildSpecT *spec); static const char *emitIcon(const char *outPath, const BasBuildSpecT *spec); static const char *emitResources(const char *outPath, const BasBuildSpecT *spec, const uint8_t *modData, int32_t modLen, const uint8_t *dbgData, int32_t dbgLen, uint8_t **frmData, const int32_t *frmLens, const BasObfFrmT *obfFrms, int32_t frmCount); static void freeFrmWork(uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms); -static bool pathsEqual(const char *a, const char *b); static const char *prepareModule(const BasBuildSpecT *spec, uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms, int32_t frmCount, uint8_t **outMod, int32_t *outModLen, uint8_t **outDbg, int32_t *outDbgLen); static const char *stubCarrier(const BasBuildSpecT *spec); static const char *writeMarker(const char *outPath); @@ -497,7 +490,7 @@ bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProje } char path[DVX_MAX_PATH]; - snprintf(path, sizeof(path), "%s%s%s", projectDir, BAS_BUILD_SEP, val); + platformPathJoin(path, sizeof(path), projectDir, val); int32_t srcLen = 0; char *src = platformReadFile(path, &srcLen); @@ -563,19 +556,6 @@ const char *basBuildControl(const char *outPath, const BasBuildSpecT *spec) { } -void basBuildDirOf(const char *path, char *out, int32_t outSize) { - snprintf(out, outSize, "%s", path); - - char *sep = platformPathDirEnd(out); - - if (sep) { - *sep = '\0'; - } else { - snprintf(out, outSize, "."); - } -} - - const char *basBuildErrorText(const char *message) { if (strncasecmp(message, BAS_BUILD_LINE_PREFIX, sizeof(BAS_BUILD_LINE_PREFIX) - 1) != 0) { return message; @@ -623,7 +603,7 @@ int32_t basBuildMapFileLine(const BasSourceMapT *map, const char *path, int32_t for (int32_t i = 0; i < (int32_t)arrlen(map); i++) { const BasSourceMapT *m = &map[i]; - if (pathsEqual(m->path, path)) { + if (platformPathSame(m->path, path)) { int32_t offset = fileLine - m->firstFileLine; return (offset >= 0 && offset < m->lineCount) ? m->startLine + offset : 0; @@ -666,8 +646,7 @@ bool basBuildOpenProject(const char *dbpPath, PrefsHandleT **prefs, char *projec return false; } - basBuildDirOf(dbpPath, projectDir, projectDirSize); - return true; + return platformPathDirName(dbpPath, projectDir, projectDirSize); } @@ -724,13 +703,13 @@ static const char *copyHelpFile(const char *outPath, const BasBuildSpecT *spec) } char helpSrc[DVX_MAX_PATH]; - snprintf(helpSrc, sizeof(helpSrc), "%s" BAS_BUILD_SEP "%s", spec->projectDir, spec->helpFile); + platformPathJoin(helpSrc, sizeof(helpSrc), spec->projectDir, spec->helpFile); - char outDir[DVX_MAX_PATH]; - basBuildDirOf(outPath, outDir, sizeof(outDir)); + char appFolder[DVX_MAX_PATH]; + platformPathDirName(outPath, appFolder, sizeof(appFolder)); char helpDst[DVX_MAX_PATH]; - snprintf(helpDst, sizeof(helpDst), "%s" BAS_BUILD_SEP "%s", outDir, platformPathBaseName(spec->helpFile)); + platformPathJoin(helpDst, sizeof(helpDst), appFolder, platformPathBaseName(spec->helpFile)); if (!platformCopyFile(helpSrc, helpDst)) { return "Failed copying help file to output directory."; @@ -784,7 +763,7 @@ static const char *emitControls(const char *outPath, const BasBuildSpecT *spec) char iconPath[DVX_MAX_PATH]; int32_t iconLen = 0; - snprintf(iconPath, sizeof(iconPath), "%s" BAS_BUILD_SEP "%s", spec->projectDir, value); + platformPathJoin(iconPath, sizeof(iconPath), spec->projectDir, value); void *iconData = platformReadFile(iconPath, &iconLen); @@ -843,7 +822,7 @@ static const char *emitIcon(const char *outPath, const BasBuildSpecT *spec) { int32_t iconLen = 0; if (spec->iconPath && spec->iconPath[0]) { - snprintf(iconPath, sizeof(iconPath), "%s" BAS_BUILD_SEP "%s", spec->projectDir, spec->iconPath); + platformPathJoin(iconPath, sizeof(iconPath), spec->projectDir, spec->iconPath); iconData = platformReadFile(iconPath, &iconLen); if (!iconData) { @@ -969,22 +948,6 @@ static void freeFrmWork(uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms } -// True when a and b name the same file the way DOS compares names: -// letters in any case, '/' and '\\' alike. -static bool pathsEqual(const char *a, const char *b) { - for (; *a && *b; a++, b++) { - bool sepA = (*a == '/' || *a == '\\'); - bool sepB = (*b == '/' || *b == '\\'); - - if (sepA != sepB || (!sepA && tolower((unsigned char)*a) != tolower((unsigned char)*b))) { - return false; - } - } - - return *a == *b; -} - - // Serialize the module (and, for debug builds, its debug info). Release // builds run strip -> obfuscate -> compact on a private copy so the // caller's module stays intact; obfuscation also fills obfFrms. diff --git a/src/apps/kpunch/dvxbasic/basBuild.h b/src/apps/kpunch/dvxbasic/basBuild.h index 130a7c9c..6947db2f 100644 --- a/src/apps/kpunch/dvxbasic/basBuild.h +++ b/src/apps/kpunch/dvxbasic/basBuild.h @@ -159,9 +159,6 @@ bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, Ba // call basBuildFreeProject either way. bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out); -// Writes the directory part of path into out, or "." when path has none. -void basBuildDirOf(const char *path, char *out, int32_t outSize); - // Releases everything basBuildConcatProject allocated. void basBuildFreeProject(BasProjectSourceT *out); diff --git a/src/apps/kpunch/dvxbasic/basdev/basProject.c b/src/apps/kpunch/dvxbasic/basdev/basProject.c index e433ee06..9d65ef79 100644 --- a/src/apps/kpunch/dvxbasic/basdev/basProject.c +++ b/src/apps/kpunch/dvxbasic/basdev/basProject.c @@ -96,16 +96,20 @@ _Static_assert(DVX_MAX_PATH <= PRJ_MAX_FIELD && PRJ_MAX_STRING <= PRJ_MAX_FIELD, // A [Project] or [Settings] value of PrjStateT: a text buffer (size bytes) // or a flag (size 0). always: written even when empty / false, the rest // only when set, so projects that never use a setting stay unchanged. +// path: a file path, native in memory and written with '/' (as the file +// lists are: prjPortableSeps). typedef struct { const char *section; const char *key; size_t offset; int32_t size; bool always; + bool path; } PrjFieldT; -#define PRJ_TEXT(sec, k, field, alw) { sec, k, offsetof(PrjStateT, field), (int32_t)sizeof(((PrjStateT *)0)->field), alw } -#define PRJ_FLAG(sec, k, field, alw) { sec, k, offsetof(PrjStateT, field), 0, alw } +#define PRJ_TEXT(sec, k, field, alw) { sec, k, offsetof(PrjStateT, field), (int32_t)sizeof(((PrjStateT *)0)->field), alw, false } +#define PRJ_PATH(sec, k, field) { sec, k, offsetof(PrjStateT, field), (int32_t)sizeof(((PrjStateT *)0)->field), false, true } +#define PRJ_FLAG(sec, k, field, alw) { sec, k, offsetof(PrjStateT, field), 0, alw, false } // The one list of .dbp settings: load, save, prjFieldGet / prjFieldSet static const PrjFieldT sFields[] = { @@ -116,8 +120,8 @@ static const PrjFieldT sFields[] = { PRJ_TEXT(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_VERSION, version, false), PRJ_TEXT(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_COPYRIGHT, copyright, false), PRJ_TEXT(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_DESCRIPTION, description, false), - PRJ_TEXT(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_ICON, iconPath, false), - PRJ_TEXT(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, helpFile, false), + PRJ_PATH(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_ICON, iconPath), + PRJ_PATH(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, helpFile), PRJ_TEXT(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_COMMANDARGS, commandArgs, false), PRJ_TEXT(BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, startupForm, true), PRJ_FLAG(BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, optionExplicit, true), @@ -205,6 +209,7 @@ bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int void prjNew(PrjStateT *prj, const char *name, const char *directory); static bool prjParseFlag(const char *text); bool prjPathExists(const char *path); +static void prjPortableSeps(char *path); char *prjReadFileText(const PrjStateT *prj, int32_t fileIdx, bool *outMissing); void prjRemoveFile(PrjStateT *prj, int32_t idx); bool prjRenameFile(PrjStateT *prj, int32_t idx, const char *newName, char *err, int32_t errSize); @@ -218,6 +223,7 @@ int32_t prjAddFile(PrjStateT *prj, const char *relativePath, PrjFileKindE kind) PrjFileT entry; memset(&entry, 0, sizeof(entry)); snprintf(entry.path, sizeof(entry.path), "%s", relativePath); + platformPathNative(entry.path); entry.kind = (uint8_t)kind; arrput(prj->files, entry); prj->dirty = true; @@ -530,18 +536,10 @@ char *prjCtlNew(const char *name) { } -// Set prj->projectDir to the directory containing dbpPath, or "." when -// dbpPath has no directory component. +// Set prj->projectDir to the directory containing dbpPath (native +// separators), or "." when dbpPath has no directory component. static void prjDeriveDir(PrjStateT *prj, const char *dbpPath) { - snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", dbpPath); - char *sep = platformPathDirEnd(prj->projectDir); - - if (sep) { - *sep = '\0'; - } else { - prj->projectDir[0] = '.'; - prj->projectDir[1] = '\0'; - } + platformPathDirName(dbpPath, prj->projectDir, sizeof(prj->projectDir)); } @@ -590,6 +588,10 @@ bool prjFieldSet(PrjStateT *prj, const char *key, const char *value) { prjSingleLine(at, f->size, value); } + if (f->path) { + platformPathNative(at); + } + prj->dirty = true; return true; } @@ -602,7 +604,7 @@ int32_t prjFileCount(const PrjStateT *prj) { int32_t prjFileIndexByPath(const PrjStateT *prj, const char *path) { for (int32_t i = 0; i < prjFileCount(prj); i++) { - if (strcasecmp(prj->files[i].path, path) == 0) { + if (platformPathSame(prj->files[i].path, path)) { return i; } } @@ -617,7 +619,7 @@ void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t o return; } - snprintf(outPath, outSize, "%s" DVX_PATH_SEP "%s", prj->projectDir, prj->files[fileIdx].path); + platformPathJoin(outPath, outSize, prj->projectDir, prj->files[fileIdx].path); } @@ -667,6 +669,7 @@ bool prjLoad(PrjStateT *prj, const char *dbpPath) { // starting from a blank state. prjClose(prj); snprintf(prj->projectPath, sizeof(prj->projectPath), "%s", dbpPath); + platformPathNative(prj->projectPath); // Derive project directory prjDeriveDir(prj, dbpPath); @@ -728,6 +731,10 @@ static void prjLoadFields(PrjStateT *prj, PrefsHandleT *h) { } else { snprintf(at, (size_t)f->size, "%s", prefsGetString(h, f->section, f->key, at)); } + + if (f->path) { + platformPathNative(at); + } } } @@ -783,8 +790,12 @@ bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, i void prjNew(PrjStateT *prj, const char *name, const char *directory) { prjInit(prj); snprintf(prj->name, sizeof(prj->name), "%s", name); + char file[DVX_MAX_PATH]; + snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", directory); - snprintf(prj->projectPath, sizeof(prj->projectPath), "%s" DVX_PATH_SEP "%s" BAS_DBP_EXT, directory, name); + platformPathNative(prj->projectDir); + snprintf(file, sizeof(file), "%s" BAS_DBP_EXT, name); + platformPathJoin(prj->projectPath, sizeof(prj->projectPath), directory, file); prj->dirty = true; } @@ -810,6 +821,18 @@ bool prjPathExists(const char *path) { } +// A path as the .dbp keeps it, in place: '/' between its parts on every +// platform (the file is shared with the host build, whose Makefile reads +// the File entries); in memory project paths are native. +static void prjPortableSeps(char *path) { + for (char *p = path; *p; p++) { + if (platformPathIsSep(*p)) { + *p = '/'; + } + } +} + + char *prjReadFileText(const PrjStateT *prj, int32_t fileIdx, bool *outMissing) { char fullPath[DVX_MAX_PATH]; @@ -869,7 +892,7 @@ void prjRemoveFile(PrjStateT *prj, int32_t idx) { // The source map is keyed by path: drop the removed file's lines so // they no longer map to a file until the next compile rebuilds it. for (int32_t i = (int32_t)arrlen(prj->sourceMap) - 1; i >= 0; i--) { - if (strcmp(prj->sourceMap[i].path, prj->files[idx].path) == 0) { + if (platformPathSame(prj->sourceMap[i].path, prj->files[idx].path)) { arrdel(prj->sourceMap, i); } } @@ -916,7 +939,7 @@ bool prjRenameFile(PrjStateT *prj, int32_t idx, const char *newName, char *err, return false; } - if (strcasecmp(name, file->path) == 0) { + if (platformPathSame(name, file->path)) { return true; } @@ -931,7 +954,7 @@ bool prjRenameFile(PrjStateT *prj, int32_t idx, const char *newName, char *err, snprintf(oldPath, sizeof(oldPath), "%s", file->path); prjFullPath(prj, idx, oldFull, sizeof(oldFull)); - snprintf(newFull, sizeof(newFull), "%s" DVX_PATH_SEP "%s", prj->projectDir, name); + platformPathJoin(newFull, sizeof(newFull), prj->projectDir, name); if (prjPathExists(newFull)) { snprintf(err, errSize, "%s already exists.", name); @@ -1016,6 +1039,11 @@ static void prjSaveFields(PrefsHandleT *h, const PrjStateT *prj, const char *sec } } else if (f->always || at[0]) { prjSingleLine(line, (int32_t)sizeof(line), at); + + if (f->path) { + prjPortableSeps(line); + } + prefsSetString(h, f->section, f->key, line); } } @@ -1030,8 +1058,12 @@ static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, PrjFileKin for (int32_t i = 0; i < prjFileCount(prj); i++) { if (prj->files[i].kind == kind) { char key[PRJ_INI_KEY_LEN]; + char path[DVX_MAX_PATH]; + snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, idx++); - prefsSetString(h, sKinds[kind].section, key, prj->files[i].path); + snprintf(path, sizeof(path), "%s", prj->files[i].path); + prjPortableSeps(path); + prefsSetString(h, sKinds[kind].section, key, path); } } } diff --git a/src/apps/kpunch/dvxbasic/formrt/formCtl.c b/src/apps/kpunch/dvxbasic/formrt/formCtl.c index b6d1585d..e74a8e40 100644 --- a/src/apps/kpunch/dvxbasic/formrt/formCtl.c +++ b/src/apps/kpunch/dvxbasic/formrt/formCtl.c @@ -1204,13 +1204,7 @@ static bool ucFileReady(UcFileT *file, WidgetT *parent) { char dir[DVX_MAX_PATH]; const char *error = NULL; - snprintf(dir, sizeof(dir), "%s", file->path); - - char *sep = platformPathDirEnd(dir); - - if (sep) { - *sep = '\0'; - } + platformPathDirName(file->path, dir, sizeof(dir)); memset(&spec, 0, sizeof(spec)); spec.appDir = dir; diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.c b/src/apps/kpunch/dvxbasic/formrt/formrt.c index d8280b24..856ee231 100644 --- a/src/apps/kpunch/dvxbasic/formrt/formrt.c +++ b/src/apps/kpunch/dvxbasic/formrt/formrt.c @@ -2713,7 +2713,7 @@ void *basFormRtLoadPicture(void *ctx, const char *path, int32_t *outErr) { BasFormRtT *rt = (BasFormRtT *)ctx; for (int32_t i = 0; i < (int32_t)arrlen(rt->pictures); i++) { - if (strcmp(rt->pictures[i]->path, path) == 0) { + if (platformPathSame(rt->pictures[i]->path, path)) { return rt->pictures[i]; } } diff --git a/src/apps/kpunch/dvxbasic/ide/ideMain.c b/src/apps/kpunch/dvxbasic/ide/ideMain.c index 98a563d0..df0c247a 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideMain.c +++ b/src/apps/kpunch/dvxbasic/ide/ideMain.c @@ -945,23 +945,15 @@ static bool addFileToProject(const char *path) { const char *fileName = platformPathBaseName(path); char dir[DVX_MAX_PATH]; - snprintf(dir, sizeof(dir), "%s", path); - char *sep = platformPathDirEnd(dir); + platformPathDirName(path, dir, sizeof(dir)); - if (sep) { - *sep = '\0'; - } else { - dir[0] = '.'; - dir[1] = '\0'; - } - - if (strcasecmp(dir, sProject.projectDir) != 0) { + if (!platformPathSame(dir, sProject.projectDir)) { ideErrorBox("Files must be located in the project directory."); return false; } for (int32_t i = 0; i < prjFileCount(&sProject); i++) { - if (strcasecmp(sProject.files[i].path, fileName) == 0) { + if (platformPathSame(sProject.files[i].path, fileName)) { return activateFile(i, ViewAutoE); } } @@ -1412,7 +1404,7 @@ static bool classNameUsed(const char *name, const char *fileName) { for (int32_t i = 0; i < prjFileCount(&sProject) && !used; i++) { PrjFileT *file = &sProject.files[i]; - used = strcasecmp(file->path, fileName) == 0 || (file->kind == PrjKindFormE && strcasecmp(file->formName, name) == 0); + used = platformPathSame(file->path, fileName) || (file->kind == PrjKindFormE && strcasecmp(file->formName, name) == 0); if (!used && (file->kind == PrjKindClassE || file->kind == PrjKindControlE)) { bool missing = false; @@ -4435,7 +4427,7 @@ static void newClassFile(PrjFileKindE kind) { for (int32_t n = 1; ; n++) { snprintf(name, sizeof(name), control ? IDE_NEW_CTL_FMT : IDE_NEW_CLASS_FMT, (int)n); snprintf(fileName, sizeof(fileName), "%s%s", name, ext); - snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", sProject.projectDir, fileName); + platformPathJoin(path, sizeof(path), sProject.projectDir, fileName); if (!classNameUsed(name, fileName) && !prjPathExists(path)) { break; @@ -6415,7 +6407,7 @@ static void recentAdd(const char *path) { // If already in the list, move it to the top for (int32_t i = 0; i < (int32_t)arrlen(sRecentFiles); i++) { - if (strcasecmp(sRecentFiles[i], path) == 0) { + if (platformPathSame(sRecentFiles[i], path)) { char *existing = sRecentFiles[i]; arrdel(sRecentFiles, i); arrins(sRecentFiles, 0, existing); @@ -6517,7 +6509,7 @@ static void recentRebuildMenu(void) { // recentRemove -- drop path from the Recent list (it no longer opens) static void recentRemove(const char *path) { for (int32_t i = 0; i < (int32_t)arrlen(sRecentFiles); i++) { - if (strcasecmp(sRecentFiles[i], path) == 0) { + if (platformPathSame(sRecentFiles[i], path)) { free(sRecentFiles[i]); arrdel(sRecentFiles, i); recentSave(); @@ -6845,7 +6837,7 @@ static void runModule(BasModuleT *mod) { BasRunSpecT spec; if (sProject.helpFile[0]) { - snprintf(helpPath, sizeof(helpPath), "%s" DVX_PATH_SEP "%s", sProject.projectDir, sProject.helpFile); + platformPathJoin(helpPath, sizeof(helpPath), sProject.projectDir, sProject.helpFile); } memset(&spec, 0, sizeof(spec)); @@ -9044,7 +9036,7 @@ int32_t appMain(DxeAppContextT *ctx) { sAc = ctx->shellCtx; // Set help file and context-sensitive F1 handler - snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s" DVX_PATH_SEP "%s", ctx->appDir, IDE_HELP_FILE); + platformPathJoin(ctx->helpFile, sizeof(ctx->helpFile), ctx->appDir, IDE_HELP_FILE); snprintf(sIdeHelpFile, sizeof(sIdeHelpFile), "%s", ctx->helpFile); ctx->onHelpQuery = helpQueryHandler; ctx->helpQueryCtx = NULL; diff --git a/src/apps/kpunch/dvxbasic/ide/ideProject.c b/src/apps/kpunch/dvxbasic/ide/ideProject.c index 58c7c807..f034c013 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideProject.c +++ b/src/apps/kpunch/dvxbasic/ide/ideProject.c @@ -209,7 +209,7 @@ static void ppdLoadIconPreview(void) { const char *relPath = sPpd.iconPath; char fullPath[DVX_MAX_PATH * 2]; - snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", sPpd.prj->projectDir, relPath); + platformPathJoin(fullPath, sizeof(fullPath), sPpd.prj->projectDir, relPath); if (!validateIcon(fullPath, true)) { sPpd.iconPath[0] = '\0'; @@ -281,7 +281,7 @@ static void ppdOnBrowseIcon(WidgetT *w) { // Check if destination already exists char destPath[DVX_MAX_PATH * 2]; - snprintf(destPath, sizeof(destPath), "%s" DVX_PATH_SEP "%s", sPpd.prj->projectDir, fname); + platformPathJoin(destPath, sizeof(destPath), sPpd.prj->projectDir, fname); if (prjPathExists(destPath)) { char msg[DVX_MAX_PATH + PRJ_MSG_BUF]; @@ -327,7 +327,7 @@ static void ppdOnOk(WidgetT *w) { // Validate icon path if set if (sPpd.iconPath[0] && sPpd.prj) { char fullPath[DVX_MAX_PATH * 2]; - snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", sPpd.prj->projectDir, sPpd.iconPath); + platformPathJoin(fullPath, sizeof(fullPath), sPpd.prj->projectDir, sPpd.iconPath); if (!validateIcon(fullPath, true)) { return; @@ -428,14 +428,9 @@ int32_t prjGetSelectedFileIdx(void) { // Return path relative to prj->projectDir when path lives under it, else NULL // (callers apply their own fallback). static const char *prjMakeRelative(const PrjStateT *prj, const char *path) { - int32_t dirLen = (int32_t)strlen(prj->projectDir); + const char *rel = platformPathIsUnder(path, prj->projectDir); - if (strncasecmp(path, prj->projectDir, dirLen) == 0 && - (path[dirLen] == '/' || path[dirLen] == '\\')) { - return path + dirLen + 1; - } - - return NULL; + return (rel && rel[0]) ? rel : NULL; } diff --git a/src/apps/kpunch/dvxbasic/ide/ideProperties.c b/src/apps/kpunch/dvxbasic/ide/ideProperties.c index 5aac439a..18bb9f26 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideProperties.c +++ b/src/apps/kpunch/dvxbasic/ide/ideProperties.c @@ -1496,18 +1496,9 @@ void prpShowCtl(const char *text, PrpCtlSetFnT onSet) { // resolveDbPath -- resolve a DatabaseName against the project directory static void resolveDbPath(const char *dbName, char *out, int32_t outSize) { - // If it's already an absolute path (starts with drive letter or /), use as-is - if ((dbName[0] && dbName[1] == ':') || dbName[0] == '/' || dbName[0] == '\\') { - snprintf(out, outSize, "%s", dbName); - return; - } - - // Resolve relative to project directory - if (sDs && sDs->projectDir && sDs->projectDir[0]) { - snprintf(out, outSize, "%s" DVX_PATH_SEP "%s", sDs->projectDir, dbName); - } else { - snprintf(out, outSize, "%s", dbName); - } + // An absolute name stands as it is; else it counts from the project + // folder (platformPathJoin) + platformPathJoin(out, outSize, (sDs && sDs->projectDir) ? sDs->projectDir : "", dbName); } diff --git a/src/apps/kpunch/dvxbasic/ideguide.dhs b/src/apps/kpunch/dvxbasic/ideguide.dhs index 597964ce..6ad9becd 100644 --- a/src/apps/kpunch/dvxbasic/ideguide.dhs +++ b/src/apps/kpunch/dvxbasic/ideguide.dhs @@ -1324,6 +1324,9 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec .item Appending to a String variable with s = s + x or s = s & x no longer copies the whole string each time, so building long strings in a loop is much faster. When x is InputBox$, MsgBox, CreateForm, CreateControl or Shell, s is still read before x runs (left to right). .item And, Or, Xor, Eqv and Imp of two Booleans give a Boolean, and Not of a Boolean gives a Boolean, as in Visual Basic 4 and later: Print (a > 1) Or (b > 1) prints True instead of -1, and TypeName gives "Boolean". With a number on either side they still work on the bits and give a number. .item Big disks and long file names: where the disk takes long file names (DVX under Windows, or a long file name driver loaded before DVX), OPEN, KILL, NAME, MKDIR, DIR$ and the other file statements use them, and the File Manager, the file dialogs and Terminal downloads keep them. A file can be bigger than 2 GB on a FAT32 disk; LOF, FILELEN, LOC and SEEK raise error 6 (Overflow) for a size or position past 2 GB rather than give a wrong number, and EOF stays right below it. FileOpsFreeBytes and FileOpsTotalBytes are right for disks past 2 GB. +.item Paths may be written with / or \ wherever a path is taken (OPEN, KILL, NAME, MKDIR, CHDIR, DIR$, FILELEN and the file and folder functions of the libraries), mixed too; paths DVX gives back (CURDIR$, App.Path, App.Data, the file dialogs, the FileOps results) use \, as DOS shows them. New: FileOpsPathNative$ gives a path of your own that form. DIR$("C:\*.*") lists the root of C: (before, it listed C:'s current folder). SHELL commands still go to DOS as written, where / starts a switch. +.item The IDE shows a project's file paths with \ and keeps writing them with / in the .dbp file, so a project reads the same everywhere. +.item The Open and Save As dialogs (DlgFileOpen$, DlgFileSave$ and the IDE's own) have a Drive list beside the Path box (Alt+D); a drive with no disk in it says it is not ready. .item Changed: FileOpsValidName takes the folder the name is for, FileOpsValidName(folder, itemName), since the rules depend on its disk; XferSafeName$ takes the folder a file is received into, XferSafeName$(name, folder). New: FileOpsLongNames, FileOpsShortPath$, FileOpsValidLongName and FileOpsValidShortName (fileops.bas), and ShellArgQuote$ (shell.bas), which puts a path with spaces in quotes for a command line. .item Programs compiled with an older release must be rebuilt. A program whose procedure table records more than 64 locals for a procedure is refused when it is loaded. .item A class may have at most 65536 member variables and a form at most 65536 form-level variables (as many as the compiled program can number); more is a compile error ("Too many member variables in X", "Too many form-level variables"), and a program file that records more is refused when it is loaded. diff --git a/src/apps/kpunch/dvxbasic/langref.dhs b/src/apps/kpunch/dvxbasic/langref.dhs index fbe9a82c..4dd74109 100644 --- a/src/apps/kpunch/dvxbasic/langref.dhs +++ b/src/apps/kpunch/dvxbasic/langref.dhs @@ -1807,6 +1807,8 @@ Shell "DIR /B" exitCode = Shell("COPY A.TXT B.TXT") .endcode +The command goes to DOS as it is written. DOS commands read / as the start of a switch (DIR /B), so write the paths in a command with \ (SHELL "DEL C:\TEMP\X.TXT"); FileOpsPathNative$ (fileops.bas) turns a path built with / into that form. Everywhere else in DVX BASIC a path may use / or \. + .h2 SLEEP Pauses execution for the given number of seconds. With no argument, sleeps for 1 second. The desktop and the program's own forms keep running while it sleeps, SLEEP 0 just lets pending events run, and closing the program ends the sleep. @@ -2469,6 +2471,8 @@ pos = SEEK(channel) ' Function: get current position DVX BASIC provides statements for working with the file system -- creating, deleting, renaming, and copying files and directories, and reading or setting file attributes. +A path may be written with / or \ between its parts, mixed too: OPEN "DATA/SCORES.TXT" and OPEN "DATA\SCORES.TXT" open the same file, and so do the FileOps, Shell and other library functions that take a path (SHELL commands are the exception: see SHELL). Paths DVX gives back -- CURDIR$, App.Path, App.Data, the file dialogs, FileOpsPathJoin$ and the other FileOps results -- use \, the separator DOS shows; FileOpsPathNative$ gives a path of your own that form before you show it. + .h2 KILL Deletes a file. @@ -2563,7 +2567,7 @@ SetAttr "secret.dat", vbHidden + vbReadOnly .h2 CURDIR$ / DIR$ -CURDIR$ returns the current directory. DIR$ returns the first file matching a pattern; subsequent calls with no arguments return additional matches (or "" when done). DIR$ returns only files: never ".", ".." or subdirectories. +CURDIR$ returns the current directory. DIR$ returns the first file matching a pattern; subsequent calls with no arguments return additional matches (or "" when done). DIR$ returns only files: never ".", ".." or subdirectories. DIR$("C:\*.*") lists the root of C: (DIR$("C:*.*") the drive's current folder). .code cwd$ = CURDIR$ @@ -4865,6 +4869,8 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic .index FileOpsMakePath .index FileOpsReplaceFile .index FileOpsValidName +.index FileOpsPathNative$ +.index Path Separators .index FileOpsValidLongName .index FileOpsValidShortName .index FileOpsShortPath$ @@ -5286,7 +5292,7 @@ End Sub With fileops.bas in the project, a program can show and change what is on the disks. FileOpsList(folder, filter) reads a folder once -- its folders and the files matching filter -- and returns a listing (a number; FileOpsListFree frees it). FileOpsListRows$ gives the whole listing as name, size, type and date columns, ready for a ListView's SetRows, so even a folder of hundreds of files fills the list at once; FileOpsListSort re-sorts it by name, size, type or date with folders first. FileOpsListName$, FileOpsListIsFolder and FileOpsListSize read single entries, and FileOpsListFind finds one by name. -Paths: FileOpsPathNormalize$ (the full path of an existing folder, or ""), FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (True for a path that is complete by itself, such as "C:\DOCS", False for one that counts from the current folder), FileOpsPathIsInside(path, folder) (True when path is folder itself or anywhere inside it: check it before copying or moving a folder, so it is never copied into itself), FileOpsPathSep$ (the character between folder names, "\" on DOS), FileOpsBaseName$ and FileOpsExtension$. FileOpsFilterMatch(itemName, filter) tells whether a name matches a filter the way the file dialogs match it: wildcards * and ?, several patterns separated by ; such as "*.bmp;*.png", case not mattering. +Paths: FileOpsPathNormalize$ (the full path of an existing folder, or ""), FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (True for a path that is complete by itself, such as "C:\DOCS", False for one that counts from the current folder), FileOpsPathIsInside(path, folder) (True when path is folder itself or anywhere inside it: check it before copying or moving a folder, so it is never copied into itself), FileOpsPathSep$ (the character between folder names, "\" on DOS), FileOpsPathNative$ (a path written with that character, for showing it: "C:/DOCS/A.TXT" becomes "C:\DOCS\A.TXT"), FileOpsBaseName$ and FileOpsExtension$. Every one of them takes / and \ alike and gives back \. FileOpsFilterMatch(itemName, filter) tells whether a name matches a filter the way the file dialogs match it: wildcards * and ?, several patterns separated by ; such as "*.bmp;*.png", case not mattering. Files: FileOpsMakeDir and FileOpsRename (FileOpsError$ says why one failed), FileOpsTempPath$ and FileOpsReplaceFile (save safely: write the new file to FileOpsTempPath$(path$), then FileOpsReplaceFile swaps it in; the old file comes back if that fails), FileOpsValidName(folder, itemName) (whether a name can be used for a new file or folder in folder, by the rules of its disk; FileOpsError$ says why not), FileOpsValidLongName and FileOpsValidShortName (the long-name or the 8.3 rules by themselves, whatever the disk takes), FileOpsShortPath$ (the short 8.3 form of an existing path, for an old program that cannot take long names or blanks), FileOpsMakePath (a folder and every missing folder above it, which MKDIR does not make), FileOpsIsFolder, FileOpsGetAttr and FileOpsSetAttr (the FILEOPS_ATTR_ bits), FileOpsModified$ and FileOpsSetModified (when a file was last changed, read and set in the form "YYYY-MM-DD HH:MM"; FileOpsSetModified returns False for a date that does not exist), FileOpsTreeSize with FileOpsTreeBytes, FileOpsTreeFiles and FileOpsTreeFolders (the size of a whole folder) and FileOpsFormatSize$. diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.c b/src/apps/kpunch/dvxbasic/runtime/vm.c index 52b9326c..1ef2f4f9 100644 --- a/src/apps/kpunch/dvxbasic/runtime/vm.c +++ b/src/apps/kpunch/dvxbasic/runtime/vm.c @@ -5815,9 +5815,7 @@ static const char *dirNext(BasVmT *vm) { char full[DVX_MAX_PATH]; struct stat st; - snprintf(full, sizeof(full), "%s/%s", vm->dirPath, ent->d_name); - - if (stat(full, &st) == 0 && S_ISDIR(st.st_mode)) { + if (platformPathJoin(full, sizeof(full), vm->dirPath, ent->d_name) && stat(full, &st) == 0 && S_ISDIR(st.st_mode)) { continue; } @@ -7820,7 +7818,7 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) { cwd[0] = '\0'; } - if (!push(vm, basValStringFromC(cwd))) { + if (!push(vm, basValStringFromC(platformPathNative(cwd)))) { return BAS_VM_STACK_OVERFLOW; } @@ -7836,30 +7834,16 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) { dirClose(vm); - // Split pattern into directory + filename pattern + // Split pattern into directory + filename pattern (a root + // keeps its separator: "C:\\*.*" lists the root, not C:'s + // current folder; nothing readable when it is too long) const char *pat = patStr.strVal->data; - const char *lastSep = NULL; - for (const char *c = pat; *c; c++) { - if (*c == '/' || *c == '\\') { - lastSep = c; - } + if (!platformPathDirName(pat, vm->dirPath, (int32_t)sizeof(vm->dirPath))) { + vm->dirPath[0] = '\0'; } - if (lastSep) { - int32_t dirLen = (int32_t)(lastSep - pat); - - if (dirLen >= (int32_t)sizeof(vm->dirPath)) { - dirLen = (int32_t)sizeof(vm->dirPath) - 1; - } - - memcpy(vm->dirPath, pat, dirLen); - vm->dirPath[dirLen] = '\0'; - snprintf(vm->dirPattern, sizeof(vm->dirPattern), "%s", lastSep + 1); - } else { - snprintf(vm->dirPath, sizeof(vm->dirPath), "."); - snprintf(vm->dirPattern, sizeof(vm->dirPattern), "%s", pat); - } + snprintf(vm->dirPattern, sizeof(vm->dirPattern), "%s", platformPathBaseName(pat)); basValRelease(&patStr); diff --git a/src/apps/kpunch/dvxbasic/stub/bascomp.c b/src/apps/kpunch/dvxbasic/stub/bascomp.c index 4f74edfe..78684fba 100644 --- a/src/apps/kpunch/dvxbasic/stub/bascomp.c +++ b/src/apps/kpunch/dvxbasic/stub/bascomp.c @@ -144,15 +144,17 @@ int main(int argc, char **argv) { if (!outputPath) { if (projName[0]) { - snprintf(outBuf, sizeof(outBuf), "%s/%s%s", projectDir, projName, ext); + snprintf(outBuf, sizeof(outBuf), "%s" DVX_PATH_SEP "%s%s", projectDir, projName, ext); } else { const char *base = platformPathBaseName(dbpPath); const char *dot = strrchr(base, '.'); int32_t baseLen = dot ? (int32_t)(dot - base) : (int32_t)strlen(base); - snprintf(outBuf, sizeof(outBuf), "%s/%.*s%s", projectDir, (int)baseLen, base, ext); + snprintf(outBuf, sizeof(outBuf), "%s" DVX_PATH_SEP "%.*s%s", projectDir, (int)baseLen, base, ext); } + platformPathNative(outBuf); + outputPath = outBuf; } diff --git a/src/apps/kpunch/dvxbasic/stub/basrun.c b/src/apps/kpunch/dvxbasic/stub/basrun.c index 5ee74a16..cc104f4e 100644 --- a/src/apps/kpunch/dvxbasic/stub/basrun.c +++ b/src/apps/kpunch/dvxbasic/stub/basrun.c @@ -281,7 +281,7 @@ static int32_t runModule(BasModuleT *mod, const OptionsT *opt) { basVmSetDoEventsCallback(vm, hostDoEvents, NULL); char dir[DVX_MAX_PATH]; - basBuildDirOf(opt->program, dir, sizeof(dir)); + platformPathDirName(opt->program, dir, sizeof(dir)); const char *dataDir = opt->dataDir ? opt->dataDir : dir; basVmSetAppPaths(vm, dir, dataDir, dataDir); diff --git a/src/apps/kpunch/dvxhelp/dvxhelp.c b/src/apps/kpunch/dvxhelp/dvxhelp.c index ef0c9841..bb3be021 100644 --- a/src/apps/kpunch/dvxhelp/dvxhelp.c +++ b/src/apps/kpunch/dvxhelp/dvxhelp.c @@ -164,7 +164,7 @@ static void applyNewArgs(const char *args) { return; } - if (!shellPathsMatch(path, sHlpPath)) { + if (!platformPathSame(path, sHlpPath)) { HlpFileT *newFile = hlpFileOpen(path); if (!newFile) { diff --git a/src/apps/kpunch/iconed/iconed.dbp b/src/apps/kpunch/iconed/iconed.dbp index eda68698..a0f75a5a 100644 --- a/src/apps/kpunch/iconed/iconed.dbp +++ b/src/apps/kpunch/iconed/iconed.dbp @@ -9,9 +9,10 @@ Icon = ICON32.BMP [Modules] File0 = ../../../include/basic/commdlg.bas -File1 = ../../../include/basic/help.bas -File2 = ../../../include/basic/print.bas -Count = 3 +File1 = ../../../include/basic/fileops.bas +File2 = ../../../include/basic/help.bas +File3 = ../../../include/basic/print.bas +Count = 4 [Forms] File0 = iconed.frm diff --git a/src/apps/kpunch/iconed/iconed.frm b/src/apps/kpunch/iconed/iconed.frm index da405948..3fe59a61 100644 --- a/src/apps/kpunch/iconed/iconed.frm +++ b/src/apps/kpunch/iconed/iconed.frm @@ -521,7 +521,7 @@ END SUB SUB mnuPrint_Click DIM tmp AS STRING DIM title AS STRING - tmp = App.Data + "/PRINTICO.BMP" + tmp = FileOpsPathJoin$(App.Data, "PRINTICO.BMP") IF NOT SaveIcon(tmp) THEN MsgBox "Could not prepare the icon for printing." EXIT SUB diff --git a/src/apps/kpunch/progman/userHelp.dhs b/src/apps/kpunch/progman/userHelp.dhs index 2566b208..fdd086da 100644 --- a/src/apps/kpunch/progman/userHelp.dhs +++ b/src/apps/kpunch/progman/userHelp.dhs @@ -461,6 +461,7 @@ When a program opens or saves a file, it shows the same Open or Save As dialog b .list .item Path shows the folder you are looking at. Type a different folder or drive here (for example D: or C:\DOCS) and press Enter to go there. +.item Drive, beside it, lists the computer's drives and shows which one you are on. Choose another to go to the top folder of that drive. If there is no disk in it (an empty floppy drive, say), the dialog tells you the drive is not ready and stays where it was. .item The list shows the folders in square brackets, such as [DOCS], followed by the files. The [..] entry takes you up one folder. .item Filter, when present, chooses which kinds of files the list shows, for example only text files. .item File name is where you type or check the name of the file. @@ -475,7 +476,7 @@ Double-click a folder to look inside it. Click a file to put its name in the Fil Go to the folder you want, type a name in the File name box and click Save. If you leave off the ending (such as .txt), the dialog adds the one from the Filter box. If a file with that name already exists, DVX asks whether to replace it. .note tip -There is no separate drive list. To change drives, type the drive letter and a colon, such as A:, in the Path box and press Enter. +Alt+D opens the Drive list from the keyboard. You can also change drives by typing the drive letter and a colon, such as A:, in the Path box and pressing Enter. .endnote .topic user.clipboard diff --git a/src/include/basic/fileops.bas b/src/include/basic/fileops.bas index cafd0da4..c7afec38 100644 --- a/src/include/basic/fileops.bas +++ b/src/include/basic/fileops.bas @@ -198,6 +198,11 @@ DECLARE LIBRARY "fileops" ' The character between folder names in a path ("\" on DOS). DECLARE FUNCTION FileOpsPathSep$() AS STRING + ' path written with that character, for showing it to the user: on DOS + ' "C:/DOCS/A.TXT" becomes "C:\DOCS\A.TXT" (a program may write paths + ' with / on DOS too; everything that takes a path accepts both). + DECLARE FUNCTION FileOpsPathNative$(BYVAL path AS STRING) AS STRING + ' True when itemName matches filter the way the file dialogs and ' FileOpsList match: wildcards * and ?, several patterns separated by ' ; , or blanks, case does not matter; "" and "*.*" match everything. diff --git a/src/installer/pkgRead.c b/src/installer/pkgRead.c index f145764a..594094d8 100644 --- a/src/installer/pkgRead.c +++ b/src/installer/pkgRead.c @@ -91,6 +91,7 @@ struct PkgJobT { char msg[PKG_MSG_MAX]; // the file being unpacked char name[PKG_NAME_MAX]; + char shownName[PKG_NAME_MAX]; // name in native separators, for people char outPath[DVX_MAX_PATH]; FILE *out; bool skipping; // kept: its data is read, not written @@ -117,7 +118,6 @@ struct PkgJobT { // Prototypes // ============================================================ -static void dirOfPath(const char *path, char *out, int32_t outSize); static time_t dosTimeToTime(uint16_t date, uint16_t time); static bool ensureDirs(PkgJobT *job, const char *relName, bool lastIsDir); static void entryAbort(PkgJobT *job); @@ -145,19 +145,6 @@ static bool readBytes(PkgJobT *job, uint8_t *buf, uint32_t len); static bool writeOut(PkgJobT *job, const uint8_t *data, uint32_t len); -// The folder part of path ("." when it has none). -static void dirOfPath(const char *path, char *out, int32_t outSize) { - const char *end = platformPathDirEnd(path); - - if (!end) { - snprintf(out, outSize, "."); - return; - } - - snprintf(out, outSize, "%.*s", (int)(end - path), path); -} - - static time_t dosTimeToTime(uint16_t date, uint16_t time) { struct tm t; @@ -271,7 +258,7 @@ static PkgStepE entryData(PkgJobT *job) { if (status < TINFL_STATUS_DONE || (status == TINFL_STATUS_NEEDS_MORE_INPUT && job->packedLeft == 0)) { entryAbort(job); - return fail(job, "The file %s in the DVX package is damaged (disk %d).", job->name, (int)job->disk); + return fail(job, "The file %s in the DVX package is damaged (disk %d).", job->shownName, (int)job->disk); } done = status == TINFL_STATUS_DONE; @@ -291,7 +278,7 @@ static bool entryFinish(PkgJobT *job) { if (!job->skipping) { if (job->packedLeft != 0 || job->sizeDone != job->size || job->crcHave != job->crcWant) { entryAbort(job); - fail(job, "The file %s in the DVX package is damaged (disk %d).", job->name, (int)job->disk); + fail(job, "The file %s in the DVX package is damaged (disk %d).", job->shownName, (int)job->disk); return false; } @@ -380,8 +367,11 @@ static PkgStepE entryHeader(PkgJobT *job) { job->name[nameLen - 1] = '\0'; } + memcpy(job->shownName, job->name, sizeof(job->shownName)); + platformPathNative(job->shownName); + if (!pkgNameIsValid(job->name)) { - return fail(job, "The DVX package holds a name Setup does not accept: %s", job->name); + return fail(job, "The DVX package holds a name Setup does not accept: %s", job->shownName); } if (isDir) { @@ -412,7 +402,7 @@ static bool entryOpen(PkgJobT *job) { } if (!pkgLocalPath(job->destDir, job->name, job->outPath, sizeof(job->outPath))) { - fail(job, "The path of %s is too long.", job->name); + fail(job, "The path of %s is too long.", job->shownName); return false; } @@ -598,7 +588,7 @@ void pkgFind(const char *exePath, PkgSourceT *src) { memset(&h, 0, sizeof(h)); memset(src, 0, sizeof(*src)); src->found = PkgFoundNoneE; - dirOfPath(exePath, src->partDir, sizeof(src->partDir)); + platformPathDirName(exePath, src->partDir, sizeof(src->partDir)); FILE *f = fopen(exePath, "rb"); @@ -715,7 +705,7 @@ void pkgJobProgress(const PkgJobT *job, PkgProgressT *out) { out->fileCount = (int32_t)job->src.info.fileCount; out->percent = out->bytesTotal ? (int32_t)(out->bytesDone * PERCENT_FULL / out->bytesTotal) : PERCENT_FULL; out->disk = job->disk; - out->current = job->state == JobDataE ? job->name : ""; + out->current = job->state == JobDataE ? job->shownName : ""; } @@ -786,7 +776,7 @@ static bool readBytes(PkgJobT *job, uint8_t *buf, uint32_t len) { static bool writeOut(PkgJobT *job, const uint8_t *data, uint32_t len) { if (job->sizeDone + len > job->size) { entryAbort(job); - fail(job, "The file %s in the DVX package is damaged (disk %d).", job->name, (int)job->disk); + fail(job, "The file %s in the DVX package is damaged (disk %d).", job->shownName, (int)job->disk); return false; } diff --git a/src/installer/pkgRead.h b/src/installer/pkgRead.h index 35521ca6..a48f5943 100644 --- a/src/installer/pkgRead.h +++ b/src/installer/pkgRead.h @@ -82,7 +82,7 @@ typedef struct { int32_t fileCount; int32_t percent; int32_t disk; // disk being read - const char *current; // file being unpacked (relative name) + const char *current; // file being unpacked (relative name, native separators) } PkgProgressT; // Ask for disk (1-based) because of problem; true = look again, false = diff --git a/src/installer/setup.c b/src/installer/setup.c index 1ebf6ce8..28854ea1 100644 --- a/src/installer/setup.c +++ b/src/installer/setup.c @@ -760,6 +760,7 @@ static void installRun(void) { struct stat st; dvxCopyTrimmed(destDir, sizeof(destDir), sSetup.autoMode ? sSetup.autoDest : wgtGetText(sSetup.dest)); + platformPathNative(destDir); if (!installCheck(destDir, &upgrade, &keepSettings)) { sSetup.exitCode = SETUP_EXIT_FAILED; @@ -1122,6 +1123,7 @@ static void uninstallRun(void) { int main(int argc, char *argv[]) { memset(&sSetup, 0, sizeof(sSetup)); snprintf(sSetup.exePath, sizeof(sSetup.exePath), "%s", argv[0]); + platformPathNative(sSetup.exePath); parseArgs(argc, argv); // Never a log file on the disk Setup runs from. diff --git a/src/installer/setupCore.c b/src/installer/setupCore.c index 4da9dd90..ad6cc10d 100644 --- a/src/installer/setupCore.c +++ b/src/installer/setupCore.c @@ -102,10 +102,12 @@ static int byLengthDesc(const void *a, const void *b) { } -// A line of a batch file (trimmed in place) is line, case ignored. +// A line of a batch file (trimmed in place) is line: case ignored, and +// '/' and '\\' alike (platformPathSame), so "C:/DVX/DVX.EXE" written by +// hand is the same line. static bool lineMatches(char *l, const char *line) { dvxTrimRight(l); - return strcasecmp(dvxSkipWs(l), line) == 0; + return platformPathSame(dvxSkipWs(l), line); } @@ -433,14 +435,14 @@ bool setupUninstall(const char *destDir, bool removeSettings, char *err, int32_t // A folder of the package: removed with the others when empty. size_t len = strlen(path); - if (path[len - 1] == DVX_PATH_SEP[0]) { + if (platformPathIsSep(path[len - 1])) { path[len - 1] = '\0'; arrput(dirs, strdup(path)); continue; } // Every folder the name is in, for the empty-folder pass. - for (char *sep = strrchr(path, DVX_PATH_SEP[0]); sep && sep > path + strlen(destDir); sep = strrchr(path, DVX_PATH_SEP[0])) { + for (char *sep = platformPathDirEnd(path); sep && sep > path + strlen(destDir); sep = platformPathDirEnd(path)) { *sep = '\0'; arrput(dirs, strdup(path)); } diff --git a/src/libs/kpunch/audio/audio.h b/src/libs/kpunch/audio/audio.h index 271ba63c..0f6ce12c 100644 --- a/src/libs/kpunch/audio/audio.h +++ b/src/libs/kpunch/audio/audio.h @@ -458,7 +458,7 @@ int32_t audioMidiRelease(AudioCtxT *ctx); // AUDIO_SOUNDS_DIR. #define AUDIO_SOUNDS_SECTION "sounds" -#define AUDIO_SOUNDS_DIR "config/sounds" +#define AUDIO_SOUNDS_DIR "config" DVX_PATH_SEP "sounds" // The DVX.INI key of an event ("startup", "error", ...), and its name for // people ("Start DVX", "Error", ...); NULL for an event out of range. diff --git a/src/libs/kpunch/audio/audioBas.c b/src/libs/kpunch/audio/audioBas.c index 946ab9b2..82f67079 100644 --- a/src/libs/kpunch/audio/audioBas.c +++ b/src/libs/kpunch/audio/audioBas.c @@ -613,9 +613,13 @@ int32_t SoundEventCount(void) { } -// The file DVX.INI (a prefs.bas handle) names for the event ("" none). +// The file DVX.INI (a prefs.bas handle) names for the event ("" none), in +// native separators (the shipped DVX.INI says "config/sounds/..."). const char *SoundEventFile(int32_t prefs, int32_t eventIdx) { - return audioDeskFile(prefsBasHandle(prefs), (DvxSoundE)eventIdx); + static char file[DVX_MAX_PATH]; + + snprintf(file, sizeof(file), "%s", audioDeskFile(prefsBasHandle(prefs), (DvxSoundE)eventIdx)); + return platformPathNative(file); } diff --git a/src/libs/kpunch/dvxshell/dvxshell.dhs b/src/libs/kpunch/dvxshell/dvxshell.dhs index 4320bac3..614999fb 100644 --- a/src/libs/kpunch/dvxshell/dvxshell.dhs +++ b/src/libs/kpunch/dvxshell/dvxshell.dhs @@ -380,7 +380,6 @@ The shell itself exits DVX when the desktop app ends in any other way -- its las .index shellRunningAppId .index shellActivateApp .index shellAppFindByPath -.index shellPathsMatch .h1 Query API @@ -430,15 +429,7 @@ Bring an app to the front, as the Task Manager's Switch To does (it calls this): ShellAppT *shellAppFindByPath(const char *path); .endcode -Return an active app slot loaded from path, or NULL. Paths are compared with shellPathsMatch. - -.h2 shellPathsMatch - -.code -bool shellPathsMatch(const char *a, const char *b); -.endcode - -True when two paths name the same file under DOS rules: case-insensitive, with / and \ the same separator. +Return an active app slot loaded from path, or NULL. Paths are compared with platformPathSame (/ and \ the same separator, a trailing one ignored, case ignored on DOS). .topic shell.startup .title Starting Up @@ -676,7 +667,7 @@ typedef struct { int32_t shellProgramScan(const char *dir, const char *skipPath, bool readInfo, ShellProgressFnT progress, void *progressCtx, ShellProgramT **outList); .endcode -Every program under dir (NULL for SHELL_PROG_DIR) and its subfolders, except skipPath (compared with shellPathsMatch; NULL for none), in the order found. With readInfo each entry is filled by shellProgramInfo; without it only path is set, which is fast (no program file is opened) and enough to notice programs being added or removed. progress, when not NULL, is called with progressCtx after each program (for example to keep the screen responsive). Returns the count; *outList is an array of that many entries, NULL when there are none. Free it with shellProgramListFree. +Every program under dir (NULL for SHELL_PROG_DIR) and its subfolders, except skipPath (compared with platformPathSame; NULL for none), in the order found. With readInfo each entry is filled by shellProgramInfo; without it only path is set, which is fast (no program file is opened) and enough to notice programs being added or removed. progress, when not NULL, is called with progressCtx after each program (for example to keep the screen responsive). Returns the count; *outList is an array of that many entries, NULL when there are none. Free it with shellProgramListFree. .h2 shellProgramInfo @@ -974,7 +965,7 @@ int32_t ShellAppIsHung(int32_t appId); int32_t ShellAppMemory(int32_t appId); .endcode -The Task Manager's operations, one routine each with it. ShellRunningId is shellRunningAppId. ShellRunningName and ShellRunningPath are the ShellAppT name and path of an app in AppStateRunningE ("" otherwise; the same state every call below requires). ShellRunningFind is shellAppFindByPath (shellPathsMatch rules), 0 when that app is not running. ShellRunningActivate is shellActivateApp. ShellRunningClose is shellRequestAppClose: the app is asked at the main loop's top level, after the BASIC code has returned; True when the request was recorded. ShellRunningKill is shellForceKillApp followed by shellDesktopUpdate (End Task: no shutdownFn, callbacks purged, the unload deferred while the app's code is on a stack); the running program itself is refused (BASIC's END ends it). ShellAppIsHung is dvxAppIsHung and ShellAppMemory dvxMemGetAppUsage (clamped to INT32_MAX; 0 for an app that is not running). +The Task Manager's operations, one routine each with it. ShellRunningId is shellRunningAppId. ShellRunningName and ShellRunningPath are the ShellAppT name and path of an app in AppStateRunningE ("" otherwise; the same state every call below requires). ShellRunningFind is shellAppFindByPath (platformPathSame rules), 0 when that app is not running. ShellRunningActivate is shellActivateApp. ShellRunningClose is shellRequestAppClose: the app is asked at the main loop's top level, after the BASIC code has returned; True when the request was recorded. ShellRunningKill is shellForceKillApp followed by shellDesktopUpdate (End Task: no shutdownFn, callbacks purged, the unload deferred while the app's code is on a stack); the running program itself is refused (BASIC's END ends it). ShellAppIsHung is dvxAppIsHung and ShellAppMemory dvxMemGetAppUsage (clamped to INT32_MAX; 0 for an app that is not running). .h2 Command lines and priority diff --git a/src/libs/kpunch/dvxshell/pmModel.c b/src/libs/kpunch/dvxshell/pmModel.c index 5b4be5ee..201780e5 100644 --- a/src/libs/kpunch/dvxshell/pmModel.c +++ b/src/libs/kpunch/dvxshell/pmModel.c @@ -68,21 +68,24 @@ #define DUP_SUFFIX_MAX 8 // " (64)" plus NUL #define DECIMAL 10 -// An item's text fields by name (PROGMAN.INI's item keys) +// An item's text fields by name (PROGMAN.INI's item keys); path: kept in +// native separators (itemPathsNative) typedef struct { const char *name; size_t offset; int32_t size; + bool path; } PmTextFieldT; -#define PM_TEXT_FIELD(n, field) { n, offsetof(PmItemT, field), (int32_t)sizeof(((PmItemT *)0)->field) } +#define PM_TEXT_FIELD(n, field) { n, offsetof(PmItemT, field), (int32_t)sizeof(((PmItemT *)0)->field), false } +#define PM_PATH_FIELD(n, field) { n, offsetof(PmItemT, field), (int32_t)sizeof(((PmItemT *)0)->field), true } static const PmTextFieldT sTextFields[] = { - PM_TEXT_FIELD(INI_ITEM_NAME, name), - PM_TEXT_FIELD(INI_ITEM_PROGRAM, program), - PM_TEXT_FIELD(INI_ITEM_ARGS, args), - PM_TEXT_FIELD(INI_ITEM_FOLDER, folder), - PM_TEXT_FIELD(INI_ITEM_ICON, iconPath), + PM_TEXT_FIELD(INI_ITEM_NAME, name), + PM_PATH_FIELD(INI_ITEM_PROGRAM, program), + PM_TEXT_FIELD(INI_ITEM_ARGS, args), + PM_PATH_FIELD(INI_ITEM_FOLDER, folder), + PM_PATH_FIELD(INI_ITEM_ICON, iconPath), PM_TEXT_FIELD(PM_FIELD_DESCRIPTION, description) }; @@ -99,6 +102,7 @@ static const PrefsChoiceT sViewChoices[PmViewCountE] = { static void groupFree(PmGroupT *g); static void itemKey(char *out, int32_t outSize, int32_t n, const char *field); +static void itemPathsNative(PmItemT *item); static void loadGroup(PmModelT *m, PrefsHandleT *h, int32_t n); static void loadItem(PmModelT *m, int32_t g, PrefsHandleT *h, const char *section, int32_t n); void pmArrangeGroup(PmGroupT *g, int32_t cols); @@ -151,6 +155,16 @@ static void itemKey(char *out, int32_t outSize, int32_t n, const char *field) { } +// The item's path fields (program, folder, icon) in native separators. +static void itemPathsNative(PmItemT *item) { + for (int32_t i = 0; i < DVX_ARRAY_LEN(sTextFields); i++) { + if (sTextFields[i].path) { + platformPathNative((char *)item + sTextFields[i].offset); + } + } +} + + // [groupN]: a group without a usable name gets "Group N"; a duplicate name // gets " (N)" appended; items past PM_ITEM_MAX are ignored. static void loadGroup(PmModelT *m, PrefsHandleT *h, int32_t n) { @@ -207,6 +221,7 @@ static void loadItem(PmModelT *m, int32_t g, PrefsHandleT *h, const char *sectio dvxCopyTrimmed(item.folder, sizeof(item.folder), prefsGetString(h, section, key, "")); itemKey(key, sizeof(key), n, INI_ITEM_ICON); dvxCopyTrimmed(item.iconPath, sizeof(item.iconPath), prefsGetString(h, section, key, "")); + itemPathsNative(&item); itemKey(key, sizeof(key), n, INI_ITEM_MIN); item.runMinimized = prefsGetBool(h, section, key, false); itemKey(key, sizeof(key), n, INI_ITEM_COL); @@ -345,7 +360,7 @@ void pmHistoryAdd(PmModelT *m, const char *command) { } for (int32_t i = (int32_t)arrlen(m->history) - 1; i >= 0; i--) { - if (strcasecmp(m->history[i], trimmed) == 0) { + if (platformPathSame(m->history[i], trimmed)) { free(m->history[i]); arrdel(m->history, i); } @@ -396,7 +411,7 @@ bool pmInstalledChanged(const PmModelT *m, const char *skipPath) { bool known = false; for (int32_t j = 0; j < arrlen(installed->items) && !known; j++) { - known = shellPathsMatch(found[i].path, installed->items[j].program); + known = platformPathSame(found[i].path, installed->items[j].program); } changed = !known; @@ -459,6 +474,7 @@ int32_t pmItemAdd(PmModelT *m, int32_t g, const PmItemT *item) { PmItemT copy = *item; + itemPathsNative(©); copy.icon = NULL; copy.iconSmall = NULL; copy.iconTried = false; @@ -598,6 +614,10 @@ bool pmItemSetField(PmItemT *item, const char *field, const char *value) { dvxCopyTrimmed((char *)item + text->offset, text->size, v); + if (text->path) { + platformPathNative((char *)item + text->offset); + } + if (strcasecmp(field, INI_ITEM_PROGRAM) == 0 || strcasecmp(field, INI_ITEM_ICON) == 0) { pmItemFreeIcons(item); } diff --git a/src/libs/kpunch/dvxshell/shellApp.c b/src/libs/kpunch/dvxshell/shellApp.c index 331c4694..3625d4ac 100644 --- a/src/libs/kpunch/dvxshell/shellApp.c +++ b/src/libs/kpunch/dvxshell/shellApp.c @@ -30,7 +30,6 @@ #include "dvxPlat.h" #include "stb_ds_wrap.h" -#include #include #include #include @@ -102,8 +101,6 @@ static ShellAppT *findRunningPath(const char *path); static void forceKillNow(AppContextT *ctx, ShellAppT *app); static void killAppTask(ShellAppT *app); static bool makeTempPath(const char *origPath, int32_t id, char *out, int32_t outSize); -static bool pathCharsMatch(char a, char b); -static const char *pathUnderDir(const char *path, const char *dir); static int32_t refuseSecondCopy(AppContextT *ctx, const char *path); static void releaseAppResources(ShellAppT *app); bool shellActivateApp(AppContextT *ctx, int32_t appId); @@ -126,7 +123,6 @@ int32_t shellLoadAppWithArgs(AppContextT *ctx, const char *path, cons static int32_t shellLoadFail(void *handle, const char *tempPath); void shellOpenHelp(AppContextT *ctx, const char *args); void shellOpenTaskManager(AppContextT *ctx); -bool shellPathsMatch(const char *a, const char *b); void shellReapApp(AppContextT *ctx, ShellAppT *app); bool shellReapApps(AppContextT *ctx); void shellRegisterAppCleanup(ShellAppCleanupFnT fn); @@ -390,11 +386,12 @@ static void deriveConfigDir(DxeAppContextT *dxeCtx, const char *path) { if (platformPathIsAbsolute(rel)) { char cwd[DVX_MAX_PATH]; - rel = getcwd(cwd, sizeof(cwd)) ? pathUnderDir(rel, cwd) : NULL; + rel = getcwd(cwd, sizeof(cwd)) ? platformPathIsUnder(rel, cwd) : NULL; } if (rel && rel[0] && strcmp(rel, ".") != 0) { - snprintf(dxeCtx->configDir, sizeof(dxeCtx->configDir), SHELL_CONFIG_ROOT "/%s", rel); + snprintf(dxeCtx->configDir, sizeof(dxeCtx->configDir), SHELL_CONFIG_ROOT DVX_PATH_SEP "%s", rel); + platformPathNative(dxeCtx->configDir); return; } @@ -402,7 +399,7 @@ static void deriveConfigDir(DxeAppContextT *dxeCtx, const char *path) { const char *dot = strrchr(base, '.'); int32_t len = dot ? (int32_t)(dot - base) : (int32_t)strlen(base); - snprintf(dxeCtx->configDir, sizeof(dxeCtx->configDir), SHELL_CONFIG_ROOT "/" SHELL_CONFIG_APPS "/%.*s", (int)len, base); + snprintf(dxeCtx->configDir, sizeof(dxeCtx->configDir), SHELL_CONFIG_ROOT DVX_PATH_SEP SHELL_CONFIG_APPS DVX_PATH_SEP "%.*s", (int)len, base); } @@ -463,12 +460,12 @@ static void destroyAppWindows(AppContextT *ctx, int32_t appId) { // Find an active slot with the given DXE path. Returns the slot pointer, // or NULL if no running app was loaded from this path. -// Paths are compared with DOS semantics (shellPathsMatch), so the same app +// Paths are compared with platformPathSame, so the same app // launched as "APPS\KPUNCH\DVXHELP\DVXHELP.APP" and as a directory-scan // spelling like "apps/kpunch/dvxhelp/dvxhelp.app" is one loaded app. static ShellAppT *findLoadedPath(const char *path) { for (int32_t i = 1; i < arrlen(sApps); i++) { - if (sApps[i]->state != AppStateFreeE && shellPathsMatch(sApps[i]->path, path)) { + if (sApps[i]->state != AppStateFreeE && platformPathSame(sApps[i]->path, path)) { return sApps[i]; } } @@ -477,11 +474,11 @@ static ShellAppT *findLoadedPath(const char *path) { } -// Running app loaded from this path (shellPathsMatch), or NULL. Unlike +// Running app loaded from this path (platformPathSame), or NULL. Unlike // findLoadedPath it skips an instance whose deferred kill is pending. static ShellAppT *findRunningPath(const char *path) { for (int32_t i = 1; i < arrlen(sApps); i++) { - if (sApps[i]->state == AppStateRunningE && shellPathsMatch(sApps[i]->path, path)) { + if (sApps[i]->state == AppStateRunningE && platformPathSame(sApps[i]->path, path)) { return sApps[i]; } } @@ -546,54 +543,15 @@ static bool makeTempPath(const char *origPath, int32_t id, char *out, int32_t ou // The fallback is anchored to the working directory: a bare file name // would make dlopen search the library path instead of opening it. - if (tmpDir) { - snprintf(out, outSize, "%s" DVX_PATH_SEP "_dvx%02ld%s", tmpDir, (long)id, dot); - } else if (platformIsReadOnly()) { + char name[DVX_MAX_PATH]; + + snprintf(name, sizeof(name), "_dvx%02ld%s", (long)id, dot); + + if (!tmpDir && platformIsReadOnly()) { return false; - } else { - snprintf(out, outSize, "." DVX_PATH_SEP "_dvx%02ld%s", (long)id, dot); } - return true; -} - - -// DOS path semantics on every platform: case-insensitive, and '/' and -// '\\' are the same separator (DJGPP's getcwd uses '/', DOS paths typed or -// picked by the user use '\\'). -static bool pathCharsMatch(char a, char b) { - if (a == '\\') { - a = '/'; - } - - if (b == '\\') { - b = '/'; - } - - return tolower((unsigned char)a) == tolower((unsigned char)b); -} - - -// If path lies under dir, return the remainder after dir and its separator; -// otherwise NULL. Compared with pathCharsMatch. -static const char *pathUnderDir(const char *path, const char *dir) { - size_t n = strlen(dir); - - while (n > 0 && (dir[n - 1] == '/' || dir[n - 1] == '\\')) { - n--; - } - - for (size_t i = 0; i < n; i++) { - if (!pathCharsMatch(path[i], dir[i])) { - return NULL; - } - } - - if (path[n] != '/' && path[n] != '\\') { - return NULL; - } - - return path + n + 1; + return platformPathJoin(out, outSize, tmpDir ? tmpDir : ".", name); } @@ -715,7 +673,7 @@ void shellAppTestReset(void) { void shellConfigPath(const DxeAppContextT *ctx, const char *filename, char *outPath, int32_t outSize) { - snprintf(outPath, outSize, "%s" DVX_PATH_SEP "%s", ctx->configDir, filename); + platformPathJoin(outPath, outSize, ctx->configDir, filename); } @@ -900,6 +858,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch // desc comes from an arbitrary DXE: never read past its name field. snprintf(app->name, SHELL_APP_NAME_MAX, "%.*s", SHELL_APP_NAME_MAX - 1, desc->name); snprintf(app->path, sizeof(app->path), "%s", path); + platformPathNative(app->path); snprintf(app->tempPath, sizeof(app->tempPath), "%s", tempPath); app->dxeHandle = handle; app->hasMainLoop = desc->hasMainLoop; @@ -921,22 +880,14 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch app->dxeCtx->shellCtx = ctx; app->dxeCtx->appId = id; - // Store the full app path so the app can load its own resources. - snprintf(app->dxeCtx->appPath, sizeof(app->dxeCtx->appPath), "%s", path); + // Store the full app path so the app can load its own resources + // (native separators: programs show it, as App.Path and the like). + snprintf(app->dxeCtx->appPath, sizeof(app->dxeCtx->appPath), "%s", app->path); // Derive app directory from path (everything up to the last separator). // This lets apps load resources relative to their own location rather // than the shell's working directory. - snprintf(app->dxeCtx->appDir, sizeof(app->dxeCtx->appDir), "%s", path); - - char *sep = platformPathDirEnd(app->dxeCtx->appDir); - - if (sep) { - *sep = '\0'; - } else { - app->dxeCtx->appDir[0] = '.'; - app->dxeCtx->appDir[1] = '\0'; - } + platformPathDirName(app->path, app->dxeCtx->appDir, sizeof(app->dxeCtx->appDir)); deriveConfigDir(app->dxeCtx, path); defaultHelpFile(app->dxeCtx); @@ -1061,21 +1012,6 @@ void shellOpenTaskManager(AppContextT *ctx) { } -// Whole-path match under pathCharsMatch. -bool shellPathsMatch(const char *a, const char *b) { - while (*a && *b) { - if (!pathCharsMatch(*a, *b)) { - return false; - } - - a++; - b++; - } - - return *a == *b; -} - - // Graceful reap -- called from shellReapApps when an app has reached // the Terminating state. Unlike forceKill, this calls the app's // shutdown hook (if provided) giving it a chance to save state, close diff --git a/src/libs/kpunch/dvxshell/shellApp.h b/src/libs/kpunch/dvxshell/shellApp.h index 1ab198a3..726ba483 100644 --- a/src/libs/kpunch/dvxshell/shellApp.h +++ b/src/libs/kpunch/dvxshell/shellApp.h @@ -197,10 +197,6 @@ void shellAppInit(void); // application Help menus both use this. void shellOpenHelp(AppContextT *ctx, const char *args); -// True when two paths name the same file under DOS rules: case-insensitive, -// with '/' and '\\' the same separator. -bool shellPathsMatch(const char *a, const char *b); - // Load and start an app from a DXE file. Returns app ID (>= 1) or -1 on error. // For callback-only apps, appMain runs synchronously and returns before // shellLoadApp returns. For main-loop apps, a task is created and the @@ -296,7 +292,7 @@ ShellAppT *shellGetApp(int32_t appId); // False when it has no visible window. bool shellActivateApp(AppContextT *ctx, int32_t appId); -// Get an active app slot loaded from this path (shellPathsMatch), or NULL. +// Get an active app slot loaded from this path (platformPathSame), or NULL. ShellAppT *shellAppFindByPath(const char *path); // Total number of app slots (for iteration bounds) diff --git a/src/libs/kpunch/dvxshell/shellAssoc.c b/src/libs/kpunch/dvxshell/shellAssoc.c index 6770190d..9bcd629e 100644 --- a/src/libs/kpunch/dvxshell/shellAssoc.c +++ b/src/libs/kpunch/dvxshell/shellAssoc.c @@ -48,6 +48,7 @@ static ShellAssocLaunchFnT sLaunch = shellLoadAppWithArgs; // Prototypes // ============================================================ +static void assocProgramNative(char *value); static bool assocSave(void); static PrefsHandleT *assocStore(void); bool shellAssocCommand(const char *documentPath, char *program, int32_t programSize, char *args, int32_t argsSize); @@ -65,6 +66,24 @@ void shellAssocTestReset(const char *storePath, ShellAssocLaunch // Writes the store; on failure the cached copy (which holds the change // that did not reach the disk) is dropped. +// A stored "program,arguments" value with its program part in native +// separators (the shipped ASSOC.INI says "apps/kpunch/..."); the +// arguments are left alone ("/doc" is a switch, not a path). +static void assocProgramNative(char *value) { + char *argsSep = strchr(value, SHELL_ASSOC_ARGS_SEP); + + if (argsSep) { + *argsSep = '\0'; + } + + platformPathNative(value); + + if (argsSep) { + *argsSep = SHELL_ASSOC_ARGS_SEP; + } +} + + static bool assocSave(void) { if (prefsSave(sStore)) { return true; @@ -164,6 +183,7 @@ bool shellAssocGet(int32_t index, char *extension, int32_t extensionSize, char * snprintf(extension, extensionSize, "%s", key); snprintf(programPath, programPathSize, "%s", prefsGetString(store, SHELL_ASSOC_SECTION, key, "")); + assocProgramNative(programPath); return true; } @@ -177,6 +197,7 @@ bool shellAssocLookup(const char *extension, char *programPath, int32_t programP } snprintf(programPath, programPathSize, "%s", value); + assocProgramNative(programPath); return true; } diff --git a/src/libs/kpunch/dvxshell/shellBas.c b/src/libs/kpunch/dvxshell/shellBas.c index 81a0c42c..2caa1143 100644 --- a/src/libs/kpunch/dvxshell/shellBas.c +++ b/src/libs/kpunch/dvxshell/shellBas.c @@ -589,7 +589,7 @@ int32_t ShellInstanceCount(void) { for (int32_t i = 1; self && i < shellAppSlotCount(); i++) { const ShellAppT *app = shellGetApp(i); - if (app && app->state != AppStateTerminatingE && shellPathsMatch(app->path, self->path)) { + if (app && app->state != AppStateTerminatingE && platformPathSame(app->path, self->path)) { count++; } } @@ -611,7 +611,7 @@ const char *ShellItemIcon(const char *iconPath, const char *path) { static char file[DVX_MAX_PATH]; shellItemIconFile(shellContext(), orEmpty(iconPath), orEmpty(path), file, (int32_t)sizeof(file)); - return file; + return platformPathNative(file); } diff --git a/src/libs/kpunch/dvxshell/shellProg.c b/src/libs/kpunch/dvxshell/shellProg.c index 7d498e20..052cbcc8 100644 --- a/src/libs/kpunch/dvxshell/shellProg.c +++ b/src/libs/kpunch/dvxshell/shellProg.c @@ -94,14 +94,16 @@ static void scanDir(const char *dir, const char *skipPath, bool readInfo, ShellP char path[DVX_MAX_PATH]; struct stat st; - snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", dir, names[i]); + if (!platformPathJoin(path, sizeof(path), dir, names[i])) { + continue; + } if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { scanDir(path, skipPath, readInfo, progress, progressCtx, list); continue; } - if (!shellIsProgram(path) || (skipPath && shellPathsMatch(path, skipPath))) { + if (!shellIsProgram(path) || (skipPath && platformPathSame(path, skipPath))) { continue; } diff --git a/src/libs/kpunch/dvxshell/shellRun.c b/src/libs/kpunch/dvxshell/shellRun.c index 3ab8811f..8215d3ee 100644 --- a/src/libs/kpunch/dvxshell/shellRun.c +++ b/src/libs/kpunch/dvxshell/shellRun.c @@ -88,7 +88,7 @@ static bool fileInFolder(const char *folder, const char *name, char *out, int32_ return false; } - return dvxStrAppendf(out, outSize, 0, "%s" DVX_PATH_SEP "%s", folder, name) >= 0 && stat(out, &st) == 0 && !S_ISDIR(st.st_mode); + return platformPathJoin(out, outSize, folder, name) && stat(out, &st) == 0 && !S_ISDIR(st.st_mode); } diff --git a/src/libs/kpunch/fileops/fileOps.c b/src/libs/kpunch/fileops/fileOps.c index 7f06121a..b09d5e12 100644 --- a/src/libs/kpunch/fileops/fileOps.c +++ b/src/libs/kpunch/fileops/fileOps.c @@ -165,8 +165,6 @@ static int compareEntries(const void *a, const void *b); static const char *errorText(int32_t err); static void keepAncestors(FileOpsJobT *job, int32_t idx); static bool listScanEntry(const FileOpsSysEntryT *e, void *ctx); -static bool pathCharEq(char a, char b); -static int32_t pathTrimmedLen(const char *path); static int32_t percentOf(const FileOpsJobT *job); static bool scanFolder(FileOpsJobT *job, int32_t idx); static bool scanFolderEntry(const FileOpsSysEntryT *e, void *ctx); @@ -651,34 +649,21 @@ bool fileOpsParseDate(const char *text, time_t *out) { bool fileOpsPathIsInside(const char *path, const char *dir) { - int32_t len = pathTrimmedLen(dir); - - for (int32_t i = 0; i < len; i++) { - if (!pathCharEq(path[i], dir[i])) { - return false; - } - } - - return path[len] == '\0' || path[len] == '/' || path[len] == '\\'; + return platformPathIsUnder(path, dir) != NULL; } bool fileOpsPathIsRoot(const char *path) { - if ((path[0] == '/' || path[0] == '\\') && path[1] == '\0') { + if (platformPathIsSep(path[0]) && path[1] == '\0') { return true; } - return path[0] && path[1] == ':' && (path[2] == '\0' || ((path[2] == '/' || path[2] == '\\') && path[3] == '\0')); + return path[0] && path[1] == ':' && (path[2] == '\0' || (platformPathIsSep(path[2]) && path[3] == '\0')); } bool fileOpsPathJoin(char *out, int32_t outSize, const char *dir, const char *name) { - size_t dirLen = strlen(dir); - bool hasSep = dirLen > 0 && (dir[dirLen - 1] == '/' || dir[dirLen - 1] == '\\'); - const char *sep = hasSep ? "" : DVX_PATH_SEP; - int32_t n = snprintf(out, (size_t)outSize, "%s%s%s", dir, sep, name); - - return n >= 0 && n < outSize; + return platformPathJoin(out, outSize, dir, name); } @@ -700,17 +685,13 @@ bool fileOpsPathNormalize(const char *path, char *out, int32_t outSize) { int32_t upperTo = 0; #endif - for (int32_t i = 0; canon[i]; i++) { - char c = canon[i]; + memcpy(out, canon, strlen(canon) + 1); - if (c == '/' || c == '\\') { - c = DVX_PATH_SEP[0]; - } - - out[i] = (i < upperTo) ? (char)toupper((unsigned char)c) : c; - out[i + 1] = '\0'; + for (int32_t i = 0; i < upperTo; i++) { + out[i] = (char)toupper((unsigned char)out[i]); } + platformPathNative(out); return true; } @@ -720,11 +701,15 @@ bool fileOpsPathParent(const char *path, char *out, int32_t outSize) { return false; } - int32_t len = pathTrimmedLen(path); + int32_t len = (int32_t)strlen(path); int32_t sep = -1; + if (len > 0 && platformPathIsSep(path[len - 1])) { + len--; + } + for (int32_t i = 0; i < len; i++) { - if (path[i] == '/' || path[i] == '\\') { + if (platformPathIsSep(path[i])) { sep = i; } } @@ -743,14 +728,13 @@ bool fileOpsPathParent(const char *path, char *out, int32_t outSize) { memcpy(out, path, (size_t)keep); out[keep] = '\0'; + platformPathNative(out); return true; } bool fileOpsPathSame(const char *a, const char *b) { - int32_t lenA = pathTrimmedLen(a); - - return pathTrimmedLen(b) == lenA && fileOpsPathIsInside(a, b); + return platformPathSame(a, b); } @@ -768,11 +752,17 @@ bool fileOpsRename(const char *dir, const char *oldName, const char *newName, ch return false; } - // A different item already using the name (a change of case alone on - // a case-insensitive disk names the same item). - bool clash = !fileOpsPathSame(oldPath, newPath) && stat(newPath, &st) == 0; - bool ok = !clash && rename(oldPath, newPath) == 0; - int32_t e = clash ? EEXIST : errno; + // A different item already using the name. On DOS a change of case + // alone names the same item; elsewhere the disk tells names apart by + // case, so only the very same name is the same item. +#ifdef __DJGPP__ + bool sameName = fileOpsPathSame(oldPath, newPath); +#else + bool sameName = strcmp(oldPath, newPath) == 0; +#endif + bool clash = !sameName && stat(newPath, &st) == 0; + bool ok = !clash && rename(oldPath, newPath) == 0; + int32_t e = clash ? EEXIST : errno; if (!ok) { snprintf(err, errSize, "Cannot rename %s to %s.\n%s", oldName, newName, errorText(e)); @@ -855,35 +845,6 @@ static bool listScanEntry(const FileOpsSysEntryT *e, void *ctx) { } -// Separators are interchangeable; DOS names compare without case. -static bool pathCharEq(char a, char b) { - bool sepA = a == '/' || a == '\\'; - bool sepB = b == '/' || b == '\\'; - - if (sepA || sepB) { - return sepA && sepB; - } - -#ifdef __DJGPP__ - return toupper((unsigned char)a) == toupper((unsigned char)b); -#else - return a == b; -#endif -} - - -// Length of path without a trailing separator (a lone "/" becomes 0). -static int32_t pathTrimmedLen(const char *path) { - int32_t len = (int32_t)strlen(path); - - if (len > 0 && (path[len - 1] == '/' || path[len - 1] == '\\')) { - len--; - } - - return len; -} - - // Percent complete by bytes, or by files when there are no bytes. static int32_t percentOf(const FileOpsJobT *job) { if (job->phase == FileOpsPhaseDoneE) { diff --git a/src/libs/kpunch/fileops/fileOps.h b/src/libs/kpunch/fileops/fileOps.h index 2ada02b4..c95aaba5 100644 --- a/src/libs/kpunch/fileops/fileOps.h +++ b/src/libs/kpunch/fileops/fileOps.h @@ -197,13 +197,15 @@ void fileOpsListSort(FileOpsEntryT *entries, int32_t count, FileOpsSortE // Create folder dir/name. False with a reason in err. bool fileOpsMakeDir(const char *dir, const char *name, char *err, int32_t errSize); -// True when path is dir itself or anywhere below it. +// True when path is dir itself or anywhere below it (platformPathIsUnder). bool fileOpsPathIsInside(const char *path, const char *dir); // True for a drive or file-system root ("C:\", "/"). bool fileOpsPathIsRoot(const char *path); -// out = dir + separator + name. False when it does not fit. +// out = dir + separator + name in native separators (platformPathJoin: no +// doubled separator; an absolute name stands alone). False when it does +// not fit. bool fileOpsPathJoin(char *out, int32_t outSize, const char *dir, const char *name); // Canonical absolute form of an existing folder (native separators; on @@ -211,10 +213,10 @@ bool fileOpsPathJoin(char *out, int32_t outSize, const char *dir, const // their case). False when path is not an existing folder. bool fileOpsPathNormalize(const char *path, char *out, int32_t outSize); -// Parent folder of path. False at a root. +// Parent folder of path, in native separators. False at a root. bool fileOpsPathParent(const char *path, char *out, int32_t outSize); -// True when the two paths name the same place. +// True when the two paths name the same place (platformPathSame). bool fileOpsPathSame(const char *a, const char *b); // Rename dir/oldName to dir/newName. False with a reason in err. @@ -373,6 +375,9 @@ int32_t FileOpsPathIsAbsolute(const char *path); int32_t FileOpsPathIsInside(const char *path, const char *dir); int32_t FileOpsPathIsRoot(const char *path); const char *FileOpsPathJoin(const char *dir, const char *name); + +// platformPathNative on a copy of path. +const char *FileOpsPathNative(const char *path); const char *FileOpsPathNormalize(const char *path); const char *FileOpsPathParent(const char *path); int32_t FileOpsPathSame(const char *a, const char *b); diff --git a/src/libs/kpunch/fileops/fileOpsBas.c b/src/libs/kpunch/fileops/fileOpsBas.c index a35bef53..216e4dbb 100644 --- a/src/libs/kpunch/fileops/fileOpsBas.c +++ b/src/libs/kpunch/fileops/fileOpsBas.c @@ -604,6 +604,15 @@ const char *FileOpsPathJoin(const char *dir, const char *name) { } +// path with the platform's separators ('\\' on DOS) for showing it, "" +// when it is too long for a path. +const char *FileOpsPathNative(const char *path) { + char native[DVX_MAX_PATH]; + + return (path && snprintf(native, sizeof(native), "%s", path) < (int)sizeof(native)) ? textOf(platformPathNative(native)) : ""; +} + + const char *FileOpsPathNormalize(const char *path) { char norm[DVX_MAX_PATH]; diff --git a/src/libs/kpunch/fileops/fileOpsSys.c b/src/libs/kpunch/fileops/fileOpsSys.c index d4ad71d7..25360f62 100644 --- a/src/libs/kpunch/fileops/fileOpsSys.c +++ b/src/libs/kpunch/fileops/fileOpsSys.c @@ -40,6 +40,7 @@ #include "fileOpsSys.h" #include "dvxPlat.h" +#include #include #include @@ -369,16 +370,23 @@ bool fileOpsSysScan(const char *dir, bool followLinks, FileOpsScanFnT fn, void * #ifdef __DJGPP__ // Copy a NUL-terminated path into the transfer buffer and point DS:DX -// at it. False when it does not fit. +// at it, in the form DOS takes (platformPathTidy: the C library is not in +// between to accept '/', doubled or trailing separators). False when it +// is longer than DVX_MAX_PATH (which the transfer buffer, 2 KB at least, +// always holds). static bool putPath(const char *path, __dpmi_regs *r) { - uint32_t len = (uint32_t)strlen(path) + 1; + char tidy[DVX_MAX_PATH]; - if (len > __tb_size) { + if (snprintf(tidy, sizeof(tidy), "%s", path) >= (int)sizeof(tidy)) { return false; } + platformPathTidy(tidy); + + uint32_t len = (uint32_t)strlen(tidy) + 1; + for (uint32_t i = 0; i < len; i++) { - _farpokeb(_dos_ds, __tb + i, (uint8_t)path[i]); + _farpokeb(_dos_ds, __tb + i, (uint8_t)tidy[i]); } r->x.ds = (uint16_t)(__tb >> PARAGRAPH_SHIFT); diff --git a/src/libs/kpunch/fileops/fileops.dhs b/src/libs/kpunch/fileops/fileops.dhs index 31e95255..a183b246 100644 --- a/src/libs/kpunch/fileops/fileops.dhs +++ b/src/libs/kpunch/fileops/fileops.dhs @@ -76,11 +76,11 @@ Disks past 2 GB report their real size and free space: platformDiskSpace asks DO fileOpsMakeDir(dir, name, err, errSize) Create a folder; a reason in err on failure. fileOpsRename(dir, oldName, newName, err, errSize) Rename within a folder; refuses a name already in use. fileOpsValidateName(dir, name, err, errSize) Check a new name for folder dir by the rules of its disk (platformValidateFilename): on DOS long names where the drive takes them (platformLongNames), else 8.3; never a device name. - fileOpsPathJoin(out, size, dir, name) dir + separator + name; false when it does not fit. - fileOpsPathParent(path, out, size) Parent folder; false at a root. + fileOpsPathJoin(out, size, dir, name) dir + separator + name in native separators (platformPathJoin: no doubled separator; an absolute name stands alone); false when it does not fit. + fileOpsPathParent(path, out, size) Parent folder, in native separators; false at a root. fileOpsPathIsRoot(path) True for "C:\", "C:" or "/". - fileOpsPathIsInside(path, dir) True when path is dir or below it. - fileOpsPathSame(a, b) Same place (separators equal, DOS ignores case, a trailing separator ignored). + fileOpsPathIsInside(path, dir) True when path is dir or below it (platformPathIsUnder). + fileOpsPathSame(a, b) Same place (platformPathSame: separators equal, case ignored, a trailing separator ignored). fileOpsPathNormalize(path, out, size) Canonical absolute form of an existing folder (DOS: upper case, except names on a disk with long names, which keep their case). .endtable @@ -143,6 +143,6 @@ fileOpsJobRun(job, callbacks) runs a job to the end instead: progress(job, ctx) .h2 BASIC -src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename for a name in a folder), FileOpsValidLongName and FileOpsValidShortName (platformValidateLongName / platformValidateShortName, the rules alone), FileOpsLongNames (platformLongNames), FileOpsShortPath$ (platformShortPath, "" when it does not fit), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$) and jobs (FileOpsJobCreate, FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value. +src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsPathNative$ (platformPathNative), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename for a name in a folder), FileOpsValidLongName and FileOpsValidShortName (platformValidateLongName / platformValidateShortName, the rules alone), FileOpsLongNames (platformLongNames), FileOpsShortPath$ (platformShortPath, "" when it does not fit), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$) and jobs (FileOpsJobCreate, FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value. Listings and jobs are handles (small numbers, 0 = none) into a table owned by the library; each records the app that made it, and the shell's app-cleanup hook frees what an app leaves open when it ends. FileOpsJobRunFor(job, ms) steps the job until the time is used or it needs an answer, so a BASIC Timer can run it without blocking the program. A returned string stays valid until the next call returning one (the BASIC runtime copies it at once). Single files are handled by the built-in FILECOPY, KILL, NAME, MKDIR, RMDIR, GETATTR and SETATTR statements. diff --git a/src/libs/kpunch/libdvx/apiref.dhs b/src/libs/kpunch/libdvx/apiref.dhs index 301afd1d..fcbec5c2 100644 --- a/src/libs/kpunch/libdvx/apiref.dhs +++ b/src/libs/kpunch/libdvx/apiref.dhs @@ -5316,7 +5316,7 @@ Describes a filter entry in the file dialog's format dropdown. The glob pattern bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const char *initialDir, const FileFilterT *filters, int32_t filterCount, char *outPath, int32_t outPathSize); .endcode -Display a modal file open/save dialog. Shows a directory listing with parent/drive-letter navigation, a filename input, and an optional filter dropdown. The path box always shows the current folder. Typing "D:" or "D:folder" in the path box changes drive/folder; the file name box also accepts a relative or absolute path ("sub\x.txt", "C:\X\A.TXT") and navigates to its folder. A folder that does not exist shows an "Invalid Path" error. The dialog closes at once if the app that owns it is ended. +Display a modal file open/save dialog. Shows a directory listing with parent/drive-letter navigation, a drive dropdown, a filename input, and an optional filter dropdown. The path box always shows the current folder, and the drive dropdown (platformDriveList, so listing the drives never touches empty floppy or CD-ROM drives) its drive; choosing a drive goes to its root, and a drive that cannot be read shows "Drive X: is not ready." and leaves the dialog where it was. Typing "D:" or "D:folder" in the path box changes drive/folder; the file name box also accepts a relative or absolute path ("sub\x.txt", "C:\X\A.TXT") and navigates to its folder. A folder that does not exist shows an "Invalid Path" error. The dialog closes at once if the app that owns it is ended. .table Parameter Description @@ -6547,6 +6547,14 @@ stb_ds dynamic array. Each widget DXE appends its class pointer via wgtRegisterC .index platformNormalizeFKeyScan .index platformLockRegion .index platformPathIsAbsolute +.index platformPathIsSep +.index platformPathNative +.index platformPathSame +.index platformPathIsUnder +.index platformPathJoin +.index platformPathDirName +.index platformPathTidy +.index Path Separators .index platformPathSibling .index platformReplaceFile .index platformFilterMatch @@ -6960,7 +6968,7 @@ platformValidateLongName and platformValidateShortName are the two DOS name rule platformLongNameChar is true when a long name may hold character c; a file transfer uses it to turn the characters a sender's name cannot keep into _. -platformShortPath writes the short (8.3) form of an existing path into out, for what reads names before a long-name driver loads (AUTOEXEC.BAT) or cannot take blanks (an old program's command line). On a drive with long names DOS gives every part's short name (INT 21h AX=7160h CL=1); elsewhere, and when DOS cannot, path itself is copied. False when out is too small. +platformShortPath writes the short (8.3) form of an existing path into out, for what reads names before a long-name driver loads (AUTOEXEC.BAT) or cannot take blanks (an old program's command line). On a drive with long names DOS gives every part's short name (INT 21h AX=7160h CL=1); elsewhere, and when DOS cannot, path itself is copied (platformPathTidy: "C:/DVX" gives "C:\\DVX"). False when out is too small. .h3 platformStatSize @@ -6986,13 +6994,37 @@ int32_t platformChdir(const char *path); Change the working directory, including drive letter on DOS (switches drive first when the path starts with "X:"). If the chdir fails the previous drive is restored. A drive prefix that is not a letter is rejected. Returns 0 on success, -1 on failure. +.h3 Path separators: platformPathIsSep / platformPathNative / platformPathSame / platformPathIsUnder / platformPathJoin / platformPathDirName / platformPathTidy + +.code +bool platformPathIsSep(char c); +char *platformPathNative(char *path); +bool platformPathSame(const char *a, const char *b); +const char *platformPathIsUnder(const char *path, const char *dir); +bool platformPathJoin(char *out, int32_t outSize, const char *dir, const char *name); +bool platformPathDirName(const char *path, char *out, int32_t outSize); +char *platformPathTidy(char *path); +.endcode + +DVX paths follow DOS rules on every platform: '/' and '\\' are the same separator, so a path a program writes with '/' works on DOS exactly as with '\\', and names compare without case (DOS and the host build also share INI files, projects and program paths). Every path DVX shows uses the native separator, DVX_PATH_SEP ('\\' on DOS). DVX's own path code goes through these functions; a program that splits, compares or joins paths should too, never testing for one separator by hand. + +.list +.item platformPathIsSep: true for '/' and '\\'. +.item platformPathNative: rewrites path's separators as DVX_PATH_SEP, in place, before it is shown (the DOS C library's getcwd and realpath give "c:/dvx"). Returns path. +.item platformPathSame: true when a and b name the same place by DOS rules: separators alike, a trailing one ignored, case ignored. Text only: "." or a relative path is not resolved. +.item platformPathIsUnder: when path is dir or lies below it, the rest of path after dir and its separator ("" for dir itself); NULL otherwise. +.item platformPathJoin: dir and name with one separator between them (none added when dir ends in one), every separator native; an absolute name, or an empty dir, gives name alone. False when it does not fit. +.item platformPathDirName: the folder part of path in native separators: a root keeps its separator ("C:\\X.TXT" gives "C:\\", since "C:" alone is the drive's current folder), "C:X.TXT" gives "C:", a bare name gives ".". False when it does not fit. +.item platformPathTidy: platformPathNative, then each run of separators made one (a leading pair, a network "\\\\server", kept) and a trailing separator dropped unless it is a root's: the form for a path handed to DOS directly, without the C library in between. platformShortPath and the file operations library's attribute calls use it. +.endlist + .h3 platformPathDirEnd .code char *platformPathDirEnd(const char *path); .endcode -Return a pointer to the last directory separator in path, or NULL. On DOS accepts both "/" and "\\". +Return a pointer to the last directory separator in path (platformPathIsSep), or NULL. .h3 platformPathIsAbsolute @@ -7000,7 +7032,7 @@ Return a pointer to the last directory separator in path, or NULL. On DOS accept bool platformPathIsAbsolute(const char *path); .endcode -True for a path rooted at a separator ('/' or '\\') or qualified with a drive letter ("C:..."). Both separators count on every platform. A NULL path is not absolute. +True for a path rooted at a separator ('/' or '\\') or qualified with a drive letter ("C:..."). A NULL path is not absolute. .h3 platformPathBaseName @@ -7008,7 +7040,7 @@ True for a path rooted at a separator ('/' or '\\') or qualified with a drive le const char *platformPathBaseName(const char *path); .endcode -Return a pointer to the leaf (basename) portion of path. +Return a pointer to the leaf (basename) portion of path: after the last separator, or after a drive ("C:A.TXT" gives "A.TXT"). .h3 platformCopyFile @@ -7016,7 +7048,7 @@ Return a pointer to the leaf (basename) portion of path. bool platformCopyFile(const char *srcPath, const char *dstPath); .endcode -Byte-for-byte file copy. Returns false on any open, read, or write failure; a partially written destination is removed so no truncated copy is left behind. +Byte-for-byte file copy. Returns false on any open, read, or write failure; a partially written destination is removed so no truncated copy is left behind. A copy onto itself (platformPathSame) does nothing and returns true: opening the copy would otherwise empty the original first. .h3 platformPathSibling @@ -7104,7 +7136,7 @@ void platformSetLogMemory(void); const char *platformLogMemory(void); .endcode -The loader tests at startup whether the DVX folder can be written to (platformDirIsWritable creates and removes a scratch file in dir) and records the answer with platformSetReadOnly. When DVX runs from a CD-ROM or a write-protected disk (platformIsReadOnly is true): the log goes to DVX.LOG in the TEMP folder (platformTempDir: the TEMP or else TMP variable, NULL when neither is set) or, without one, to memory; the boot-time help rebuild is skipped (its output and counts could not be saved); prefsSave and prefsSaveAs keep a file they cannot write in memory, and prefsLoad reads it back until DVX exits; the shell says so once at startup; and a program that allows several copies runs only once when there is no TEMP folder (a second copy is a temporary copy of the module). platformSetLogMemory sends dvxLog to a buffer holding the latest PLATFORM_LOG_MEM_MAX bytes (the oldest whole lines are dropped), which platformLogMemory returns; platformSetLogPath goes back to a file. SETUP.EXE logs to memory unless /LOG= is given, so it never writes to its own disk. +The loader tests at startup whether the DVX folder can be written to (platformDirIsWritable creates and removes a scratch file in dir) and records the answer with platformSetReadOnly. When DVX runs from a CD-ROM or a write-protected disk (platformIsReadOnly is true): the log goes to DVX.LOG in the TEMP folder (platformTempDir: the TEMP or else TMP variable in native separators, NULL when neither is set) or, without one, to memory; the boot-time help rebuild is skipped (its output and counts could not be saved); prefsSave and prefsSaveAs keep a file they cannot write in memory, and prefsLoad reads it back until DVX exits; the shell says so once at startup; and a program that allows several copies runs only once when there is no TEMP folder (a second copy is a temporary copy of the module). platformSetLogMemory sends dvxLog to a buffer holding the latest PLATFORM_LOG_MEM_MAX bytes (the oldest whole lines are dropped), which platformLogMemory returns; platformSetLogPath goes back to a file. SETUP.EXE logs to memory unless /LOG= is given, so it never writes to its own disk. .h3 platformLineEnding / platformStripLineEndings diff --git a/src/libs/kpunch/libdvx/arch.dhs b/src/libs/kpunch/libdvx/arch.dhs index 906ea1cc..62f4baa1 100644 --- a/src/libs/kpunch/libdvx/arch.dhs +++ b/src/libs/kpunch/libdvx/arch.dhs @@ -401,7 +401,7 @@ Pre-built modal dialog boxes block the caller and run their own event loop via d dvxMessageBox Generic message box with button/icon flags (MB_OK, MB_YESNO, MB_ICONERROR, etc.) dvxErrorBox Shortcut for MB_OK + MB_ICONERROR dvxInfoBox Shortcut for MB_OK + MB_ICONINFO - dvxFileDialog File open/save dialog with directory navigation and filter dropdown + dvxFileDialog File open/save dialog with directory navigation, a drive dropdown and a filter dropdown dvxInputBox Single-line text input with OK/Cancel dvxIntInputBox Integer input with spinner, clamp range, and step dvxChoiceDialog List selection with OK/Cancel diff --git a/src/libs/kpunch/libdvx/dvxApp.c b/src/libs/kpunch/libdvx/dvxApp.c index 6c1be679..03785799 100644 --- a/src/libs/kpunch/libdvx/dvxApp.c +++ b/src/libs/kpunch/libdvx/dvxApp.c @@ -4463,6 +4463,7 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path) { ctx->wallpaperBuf = buf; ctx->wallpaperPitch = pitch; memcpy(ctx->wallpaperPath, newPath, sizeof(ctx->wallpaperPath)); + platformPathNative(ctx->wallpaperPath); dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height); return true; } @@ -4845,7 +4846,7 @@ bool dvxWallpaperFromPrefs(AppContextT *ctx, PrefsHandleT *h) { WallpaperModeE mode = (WallpaperModeE)prefsWallpaperChoices[prefsGetChoice(h, DESKTOP_SECTION, DESKTOP_KEY_MODE, prefsWallpaperChoices, PREFS_WALLPAPER_CHOICE_COUNT, WALLPAPER_MODE_DEFAULT)].value; const char *path = prefsGetString(h, DESKTOP_SECTION, DESKTOP_KEY_WALLPAPER, ""); - if (strcmp(path, ctx->wallpaperPath) != 0) { + if (!platformPathSame(path, ctx->wallpaperPath)) { ctx->wallpaperMode = mode; return dvxSetWallpaper(ctx, path[0] ? path : NULL); } diff --git a/src/libs/kpunch/libdvx/dvxDialog.c b/src/libs/kpunch/libdvx/dvxDialog.c index d16ac1e3..9ba9b3db 100644 --- a/src/libs/kpunch/libdvx/dvxDialog.c +++ b/src/libs/kpunch/libdvx/dvxDialog.c @@ -120,6 +120,12 @@ #define FD_MSG_MAX (DVX_MAX_PATH + 64) // confirmation text: file name + wording #define FD_ENTRY_INITIAL_CAP 64 // initial directory listing capacity (doubles) +// File dialog widgets that keep Enter for themselves (enterOwners slots) +#define FD_OWNER_PATH 0 +#define FD_OWNER_LIST 1 +#define FD_OWNER_FILTER 2 +#define FD_OWNER_DRIVE 3 + // Input box layout constants #define IB_DIALOG_WIDTH 300 #define IB_PADDING 8 @@ -165,7 +171,7 @@ #define DLG_FIELD_GAP 4 // Widgets per dialog that keep Enter for themselves (see dialogOnKey) -#define DLG_ENTER_OWNERS_MAX 3 +#define DLG_ENTER_OWNERS_MAX 4 // ============================================================ @@ -279,6 +285,10 @@ typedef struct { int32_t filterCount; int32_t activeFilter; // index into filters[] const char *filterLabels[FD_MAX_FILTERS]; // dropdown items (borrowed labels) + PlatformDriveT drives[PLATFORM_DRIVE_MAX]; // platformDriveList + char driveLabels[PLATFORM_DRIVE_MAX][PLATFORM_DRIVE_ROOT_MAX]; // "C:" ("/" elsewhere) + const char *driveItems[PLATFORM_DRIVE_MAX]; // drive dropdown items (driveLabels) + int32_t driveCount; FdEntryT *entries; // directory listing, sorted dirs first int32_t entryCount; int32_t entryCap; @@ -287,6 +297,7 @@ typedef struct { WidgetT *pathInput; WidgetT *nameInput; WidgetT *filterDd; + WidgetT *driveDd; char outPath[DVX_MAX_PATH]; // accepted full path } FileDialogStateT; @@ -332,15 +343,17 @@ static bool dialogWindowLive(const AppContextT *ctx, const WindowT *win, static void drawIconCircle(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, uint32_t color); static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t iconType, uint32_t color); static bool fdAcceptFile(FileDialogStateT *fd, const char *name); +static void fdAddDrives(FileDialogStateT *fd, WidgetT *row); static bool fdAddEntry(FileDialogStateT *fd, const char *name, bool isDir); static int32_t fdDirPartLen(const char *name); static void fdDisplayToDirName(const char *display, char *out, int32_t outSize); +static int32_t fdDriveOf(const FileDialogStateT *fd, const char *dir); static int fdEntryCompare(const void *a, const void *b); static const char *fdExtractPattern(const FileFilterT *f, char *buf, int32_t bufSize); static void fdFreeEntries(FileDialogStateT *fd); -static bool fdJoinPath(char *out, size_t outSize, const char *dir, const char *name); static void fdLoadDir(FileDialogStateT *fd); static bool fdNavigate(FileDialogStateT *fd, const char *path); +static void fdOnDriveChange(WidgetT *w); static void fdOnFilterChange(WidgetT *w); static void fdOnListClick(WidgetT *w); static void fdOnListDblClick(WidgetT *w); @@ -1001,7 +1014,7 @@ int32_t dvxErrorBox(AppContextT *ctx, const char *title, const char *message) { // Creates a modal file dialog using the widget system. The layout is: -// - Path input (with Enter-to-navigate) +// - Path input (with Enter-to-navigate) and the drive dropdown // - File listbox (single-click selects, double-click opens/accepts) // - Optional filter dropdown (only shown if filters are provided) // - Filename input (a typed name may include a directory or drive) @@ -1020,13 +1033,16 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha fd.filterCount = filterCount; // Set initial directory. Unknown cwd: resolve accepted names - // relative to '.' rather than the filesystem root. + // relative to '.' rather than the filesystem root. Shown (and + // returned) with the native separators. if (initialDir && initialDir[0]) { snprintf(fd.curDir, sizeof(fd.curDir), "%s", initialDir); } else if (!getcwd(fd.curDir, sizeof(fd.curDir))) { strcpy(fd.curDir, "."); } + platformPathNative(fd.curDir); + // Create dialog window (FD_DIALOG_WIDTH/HEIGHT are the outer size) int32_t contentW = FD_DIALOG_WIDTH - CHROME_TOTAL_SIDE * 2; int32_t contentH = FD_DIALOG_HEIGHT - CHROME_TOTAL_TOP - CHROME_TOTAL_BOTTOM; @@ -1050,19 +1066,21 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha // valid directory string would navigate and rewrite the input // mid-edit (and stat() on each key). if (fd.pathInput) { - fd.pathInput->onKeyDown = fdOnPathKey; - fd.base.enterOwners[0] = fd.pathInput; + fd.pathInput->onKeyDown = fdOnPathKey; + fd.base.enterOwners[FD_OWNER_PATH] = fd.pathInput; } + + fdAddDrives(&fd, pathRow); } // File list fd.fileList = wgtListBox(root); if (fd.fileList) { - fd.fileList->weight = WGT_WEIGHT_FILL; - fd.fileList->onChange = fdOnListClick; - fd.fileList->onDblClick = fdOnListDblClick; - fd.base.enterOwners[1] = fd.fileList; + fd.fileList->weight = WGT_WEIGHT_FILL; + fd.fileList->onChange = fdOnListClick; + fd.fileList->onDblClick = fdOnListDblClick; + fd.base.enterOwners[FD_OWNER_LIST] = fd.fileList; } // Filter row (if filters provided). The dropdown borrows the label @@ -1083,8 +1101,8 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha wgtDropdownSetItems(fd.filterDd, fd.filterLabels, fc); wgtDropdownSetSelected(fd.filterDd, 0); - fd.filterDd->onChange = fdOnFilterChange; - fd.base.enterOwners[2] = fd.filterDd; + fd.filterDd->onChange = fdOnFilterChange; + fd.base.enterOwners[FD_OWNER_FILTER] = fd.filterDd; } } } @@ -1307,7 +1325,7 @@ static bool fdAcceptFile(FileDialogStateT *fd, const char *name) { char fullPath[DVX_MAX_PATH]; - if (!fdJoinPath(fullPath, sizeof(fullPath), fd->curDir, name)) { + if (!platformPathJoin(fullPath, sizeof(fullPath), fd->curDir, name)) { dvxErrorBox(fd->ctx, "Invalid Filename", "The path is too long."); return false; } @@ -1339,6 +1357,41 @@ static bool fdAcceptFile(FileDialogStateT *fd, const char *name) { } +// The drive dropdown at the end of row: every drive platformDriveList +// reports (it never touches the media, so empty floppy and CD-ROM drives +// cost nothing), each shown as its root without the separator ("C:"; +// "/" stays). Nothing when the list is empty. +static void fdAddDrives(FileDialogStateT *fd, WidgetT *row) { + fd->driveCount = platformDriveList(fd->drives, PLATFORM_DRIVE_MAX); + + if (fd->driveCount == 0) { + return; + } + + for (int32_t i = 0; i < fd->driveCount; i++) { + const char *rootPath = fd->drives[i].root; + size_t len = strlen(rootPath); + + if (len > 1 && platformPathIsSep(rootPath[len - 1])) { + len--; + } + + memcpy(fd->driveLabels[i], rootPath, len); + fd->driveLabels[i][len] = '\0'; + fd->driveItems[i] = fd->driveLabels[i]; + } + + wgtLabel(row, "&Drive:"); + fd->driveDd = wgtDropdown(row); + + if (fd->driveDd) { + wgtDropdownSetItems(fd->driveDd, fd->driveItems, fd->driveCount); + fd->driveDd->onChange = fdOnDriveChange; + fd->base.enterOwners[FD_OWNER_DRIVE] = fd->driveDd; + } +} + + // Appends one listing row, growing the array geometrically. Returns // false (listing left consistent) when memory runs out. static bool fdAddEntry(FileDialogStateT *fd, const char *name, bool isDir) { @@ -1373,7 +1426,7 @@ static int32_t fdDirPartLen(const char *name) { int32_t len = 0; for (int32_t i = 0; name[i]; i++) { - if (name[i] == '/' || name[i] == '\\' || (i == 1 && name[i] == ':')) { + if (platformPathIsSep(name[i]) || (i == 1 && name[i] == ':')) { len = i + 1; } } @@ -1396,6 +1449,27 @@ static void fdDisplayToDirName(const char *display, char *out, int32_t outSize) } +// The drive list entry that holds dir: the longest drive label ("C:", +// "/") its full path starts with, case ignored; -1 when none does. +static int32_t fdDriveOf(const FileDialogStateT *fd, const char *dir) { + char canon[PATH_MAX]; + const char *full = realpath(dir, canon) ? canon : dir; + int32_t best = -1; + size_t bestLen = 0; + + for (int32_t i = 0; i < fd->driveCount; i++) { + size_t len = strlen(fd->driveLabels[i]); + + if (len > bestLen && strncasecmp(full, fd->driveLabels[i], len) == 0) { + best = i; + bestLen = len; + } + } + + return best; +} + + // Sort comparator for the directory listing: directories first, then // case-insensitive by name. static int fdEntryCompare(const void *a, const void *b) { @@ -1449,23 +1523,19 @@ static void fdFreeEntries(FileDialogStateT *fd) { } -// Joins dir and name with the platform separator into out. Returns -// false when the result does not fit. -static bool fdJoinPath(char *out, size_t outSize, const char *dir, const char *name) { - int n = snprintf(out, outSize, "%s" DVX_PATH_SEP "%s", dir, name); - - return n >= 0 && (size_t)n < outSize; -} - - // Reads the current directory, applies the active file filter, sorts -// (directories first, then alphabetical), and updates the listbox and -// the path box. The path box is updated first, so it shows the current -// directory even when the listing is empty or unreadable. +// (directories first, then alphabetical), and updates the listbox, the +// path box and the drive dropdown. The path box and drive are updated +// first, so they show the current directory even when the listing is +// empty or unreadable. static void fdLoadDir(FileDialogStateT *fd) { wgtSetText(fd->pathInput, fd->curDir); + if (fd->driveDd) { + wgtDropdownSetSelected(fd->driveDd, fdDriveOf(fd, fd->curDir)); + } + // The listbox holds the listItems array uncopied: drop its reference // before freeing so no early return below leaves it painting freed // memory. @@ -1496,7 +1566,7 @@ static void fdLoadDir(FileDialogStateT *fd) { char fullPath[DVX_MAX_PATH]; struct stat st; - if (!fdJoinPath(fullPath, sizeof(fullPath), fd->curDir, ent->d_name) || stat(fullPath, &st) != 0) { + if (!platformPathJoin(fullPath, sizeof(fullPath), fd->curDir, ent->d_name) || stat(fullPath, &st) != 0) { continue; } @@ -1553,16 +1623,16 @@ static void fdLoadDir(FileDialogStateT *fd) { // bare "D:" meaning that drive's current directory) are used as given; // relative paths are resolved against curDir. realpath canonicalizes // the result (resolving "..", symlinks and drive-relative specs) so the -// path box always shows a clean absolute path. Returns false, leaving +// path box always shows a clean absolute path, in native separators. Returns false, leaving // the dialog unchanged, when the path is not an existing directory. static bool fdNavigate(FileDialogStateT *fd, const char *path) { char resolved[DVX_MAX_PATH]; - bool absolute = (path[0] == '/' || path[0] == '\\' || (path[0] && path[1] == ':')); + bool absolute = platformPathIsAbsolute(path); if (absolute) { snprintf(resolved, sizeof(resolved), "%s", path); - } else if (!fdJoinPath(resolved, sizeof(resolved), fd->curDir, path)) { + } else if (!platformPathJoin(resolved, sizeof(resolved), fd->curDir, path)) { return false; } @@ -1577,11 +1647,33 @@ static bool fdNavigate(FileDialogStateT *fd, const char *path) { } snprintf(fd->curDir, sizeof(fd->curDir), "%s", target); + platformPathNative(fd->curDir); fdLoadDir(fd); return true; } +// Drive dropdown changed: go to that drive's root. A drive that cannot +// be read (no disk in it) says so, and the dropdown shows the current +// folder's drive again. +static void fdOnDriveChange(WidgetT *w) { + FileDialogStateT *fd = (FileDialogStateT *)w->window->userData; + int32_t sel = wgtDropdownGetSelected(w); + + if (sel < 0 || sel >= fd->driveCount) { + return; + } + + if (!fdNavigate(fd, fd->drives[sel].root)) { + char msg[FD_MSG_MAX]; + + snprintf(msg, sizeof(msg), "Drive %s is not ready.", fd->driveLabels[sel]); + wgtDropdownSetSelected(w, fdDriveOf(fd, fd->curDir)); + dvxErrorBox(fd->ctx, "Drive Not Ready", msg); + } +} + + // Filter dropdown changed. static void fdOnFilterChange(WidgetT *w) { @@ -1694,7 +1786,7 @@ static void fdOnOk(WidgetT *w) { char testPath[DVX_MAX_PATH]; struct stat st; - if (fdJoinPath(testPath, sizeof(testPath), fd->curDir, name) && stat(testPath, &st) == 0 && S_ISDIR(st.st_mode)) { + if (platformPathJoin(testPath, sizeof(testPath), fd->curDir, name) && stat(testPath, &st) == 0 && S_ISDIR(st.st_mode)) { fdNavigate(fd, testPath); wgtSetText(fd->nameInput, ""); return; diff --git a/src/libs/kpunch/libdvx/dvxPrefs.c b/src/libs/kpunch/libdvx/dvxPrefs.c index 6bfa644d..d026fc0a 100644 --- a/src/libs/kpunch/libdvx/dvxPrefs.c +++ b/src/libs/kpunch/libdvx/dvxPrefs.c @@ -252,7 +252,7 @@ static bool matchWord(const char *val, const char *const *words, int32_t count) // none is held. static char *memFileCopy(const char *path, int32_t *len) { for (int32_t i = 0; i < arrlen(sMemFiles); i++) { - if (strcasecmp(sMemFiles[i].path, path) == 0) { + if (platformPathSame(sMemFiles[i].path, path)) { char *copy = (char *)malloc((size_t)sMemFiles[i].len + 1); if (copy) { @@ -281,7 +281,7 @@ static bool memFileStore(const char *path, const char *text, int32_t len) { copy[len] = '\0'; for (int32_t i = 0; i < arrlen(sMemFiles); i++) { - if (strcasecmp(sMemFiles[i].path, path) == 0) { + if (platformPathSame(sMemFiles[i].path, path)) { free(sMemFiles[i].text); sMemFiles[i].text = copy; sMemFiles[i].len = len; diff --git a/src/libs/kpunch/libdvx/platform/dvxPlat.h b/src/libs/kpunch/libdvx/platform/dvxPlat.h index dbc3ffca..ce8f63c9 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlat.h +++ b/src/libs/kpunch/libdvx/platform/dvxPlat.h @@ -118,7 +118,8 @@ void platformSetReadOnly(bool readOnly); bool platformDirIsWritable(const char *dir); // The folder for scratch files: the TEMP (or else TMP) environment -// variable; NULL when neither is set. +// variable, in native separators (a copy, valid until the next call); +// NULL when neither is set. const char *platformTempDir(void); // ============================================================ @@ -543,8 +544,9 @@ const char *platformGetSystemInfo(const DisplayT *display); // The short (8.3) form of an existing path, for what reads names before a // long-name driver loads (AUTOEXEC.BAT) or cannot take blanks (a DOS // command line): on a drive with long names DOS gives every part's short -// name; elsewhere, and when DOS cannot, path itself. False when out is -// too small. +// name; elsewhere, and when DOS cannot, path itself (platformPathTidy: +// native separators, so "C:/DVX" works the same). False when out is too +// small. bool platformShortPath(const char *path, char *out, int32_t outSize); struct stat; @@ -603,15 +605,54 @@ int32_t platformChdir(const char *path); // when switching between graphics modes live. void platformVideoFreeBuffers(DisplayT *d); -// Return a pointer to the last directory separator in path, or NULL if -// none is found. On DOS this checks both '/' and '\\' since DJGPP -// accepts either. +// Return a pointer to the last directory separator in path +// (platformPathIsSep), or NULL if none is found. char *platformPathDirEnd(const char *path); +// out = the folder part of path in native separators: everything before +// the last separator, a root keeping its own ("C:\\X.TXT" -> "C:\\", since +// "C:" alone is the drive's current folder), the drive of "C:X.TXT", or +// "." when path names no folder. False when it does not fit. +bool platformPathDirName(const char *path, char *out, int32_t outSize); + // True for a path rooted at a separator ('/' or '\\') or qualified with -// a drive letter ("C:..."). Both separators count on every platform. +// a drive letter ("C:..."). bool platformPathIsAbsolute(const char *path); +// True when c separates the parts of a path: '/' and '\\' alike, on every +// platform, so a path a program writes with '/' works on DOS exactly as +// with '\\' (DOS and the host build also share INI files, projects and +// program paths). The one rule all of DVX's path handling follows; the +// price is that a host file name cannot hold a literal '\\'. +bool platformPathIsSep(char c); + +// When path is dir itself or lies below it, the rest of path after dir +// and its separator ("" for dir itself); NULL otherwise. Compared as +// platformPathSame does. +const char *platformPathIsUnder(const char *path, const char *dir); + +// out = dir and name with one separator between them (none added when +// dir ends in one), every separator native; an absolute name, or an empty +// dir, gives name alone. False when it does not fit in outSize. +bool platformPathJoin(char *out, int32_t outSize, const char *dir, const char *name); + +// Rewrite path's separators as the platform's own (DVX_PATH_SEP), in +// place, before it is shown: on DOS every '/' becomes '\\' (the C +// library hands back "c:/dvx"). Returns path (NULL stays NULL). +char *platformPathNative(char *path); + +// True when a and b name the same place by DOS rules, on every platform +// (DVX's own paths mix spellings such as "APPS/X.APP" and "apps\\x.app"): +// the separators interchangeable, a trailing one ignored, case ignored. +// (Text only: "." or a relative path is not resolved.) +bool platformPathSame(const char *a, const char *b); + +// platformPathNative, then a run of separators made one (a leading pair, +// a network "\\\\server", kept) and a trailing one dropped unless it is a +// root's ("C:\\", "\\"): the form for a path handed to DOS directly, +// without the C library's own clean-up. In place; returns path. +char *platformPathTidy(char *path); + // Scratch-file extension markers for crash-safe rewrites // (platformPathSibling + platformReplaceFile). The sibling's extension is // the marker followed by the leading characters of the original one, so an @@ -638,13 +679,15 @@ bool platformPathSibling(char *out, int32_t outSize, const char *path, const cha // deleted, and prefsLoad falls back to reading it. bool platformReplaceFile(const char *tmpPath, const char *path, const char *bakPath); -// Return a pointer to the leaf (basename) portion of path. Never returns -// NULL -- if the path has no separator, the whole path is the basename. +// Return a pointer to the leaf (basename) portion of path: after the last +// separator, or after a drive ("C:A.TXT" -> "A.TXT"). Never returns +// NULL -- with neither, the whole path is the basename. const char *platformPathBaseName(const char *path); // Byte-for-byte file copy. Returns false on any open/read/write failure; // a partially written destination is removed so no truncated copy is -// left behind. +// left behind. A copy onto itself (platformPathSame) does nothing and +// returns true. bool platformCopyFile(const char *srcPath, const char *dstPath); // Slurp a whole file into a freshly-malloc'd, NUL-terminated buffer. diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c index bf5c6fea..4e149f65 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c +++ b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c @@ -2161,15 +2161,23 @@ void platformRegisterSymOverrides(const PlatformSymOverrideT *entries) { // library's _truename_sfn: that asks with CL=0 (the fully qualified // name), which Windows answers in short names but DOSLFN in long ones. bool platformShortPath(const char *path, char *out, int32_t outSize) { - char sfn[DVX_MAX_PATH]; - size_t len = strlen(path) + 1; - uint32_t dst = __tb + (uint32_t)len; - bool ok = false; + char sfn[DVX_MAX_PATH]; + char full[DVX_MAX_PATH]; + bool ok = false; - if (len <= sizeof(sfn) && platformLongNames(path)) { + // DOS gets the path in its own form ('/' accepted from callers) + if (snprintf(full, sizeof(full), "%s", path) >= (int)sizeof(full)) { + return false; + } + + platformPathTidy(full); + + if (platformLongNames(full)) { + size_t len = strlen(full) + 1; + uint32_t dst = __tb + (uint32_t)len; __dpmi_regs r; - dosmemput(path, len, __tb); + dosmemput(full, len, __tb); memset(&r, 0, sizeof(r)); r.x.ax = DOS_FN_LFN_TRUENAME; r.h.cl = LFN_TRUENAME_SHORT; @@ -2186,7 +2194,7 @@ bool platformShortPath(const char *path, char *out, int32_t outSize) { } } - return snprintf(out, (size_t)outSize, "%s", ok ? sfn : path) < outSize; + return snprintf(out, (size_t)outSize, "%s", ok ? sfn : full) < outSize; } @@ -3424,8 +3432,15 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(platformNormalizeFKeyScan) DXE_EXPORT(platformPathBaseName) DXE_EXPORT(platformPathDirEnd) + DXE_EXPORT(platformPathDirName) DXE_EXPORT(platformPathIsAbsolute) + DXE_EXPORT(platformPathIsSep) + DXE_EXPORT(platformPathIsUnder) + DXE_EXPORT(platformPathJoin) + DXE_EXPORT(platformPathNative) + DXE_EXPORT(platformPathSame) DXE_EXPORT(platformPathSibling) + DXE_EXPORT(platformPathTidy) DXE_EXPORT(platformReadFile) DXE_EXPORT(platformRegisterDxeExports) DXE_EXPORT(platformRegisterSymOverrides) diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c index 3108f0df..077cfe0e 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c +++ b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c @@ -93,6 +93,10 @@ // Scratch file platformDirIsWritable makes and removes #define WRITE_PROBE_NAME "DVXWTEST.$$$" +// "C:" and "C:\\" (platformPathBaseName, platformPathTidy) +#define DOS_DRIVE_SPEC_LEN 2 +#define DOS_ROOT_LEN 3 + // File name rules (platformValidateShortName / platformValidateLongName): // an 8.3 name's parts, and the characters each kind of name cannot hold // (besides control characters) @@ -142,6 +146,8 @@ int32_t dvxTrimRight(char *buf); static const ScanFoldT *findScanFold(int32_t scancode); static bool globMatchLen(const char *pat, int32_t patLen, const char *name); static void logMemAppend(const char *fmt, va_list ap); +static bool pathCharEq(char a, char b); +static int32_t pathTrimmedLen(const char *path); char platformAltScanToChar(int32_t scancode); void platformAppAbort(void); void platformAppExit(int status); @@ -160,8 +166,15 @@ void platformNormalizeKeyEvent(PlatformKeyEventT *evt, int32_ void platformOutOfMemory(size_t bytes); const char *platformPathBaseName(const char *path); char *platformPathDirEnd(const char *path); +bool platformPathDirName(const char *path, char *out, int32_t outSize); bool platformPathIsAbsolute(const char *path); +bool platformPathIsSep(char c); +const char *platformPathIsUnder(const char *path, const char *dir); +bool platformPathJoin(char *out, int32_t outSize, const char *dir, const char *name); +char *platformPathNative(char *path); +bool platformPathSame(const char *a, const char *b); bool platformPathSibling(char *out, int32_t outSize, const char *path, const char *ext); +char *platformPathTidy(char *path); char *platformReadFile(const char *path, int32_t *outLen); bool platformReplaceFile(const char *tmpPath, const char *path, const char *bakPath); void platformSetAppExitHook(PlatformAppExitFnT fn); @@ -530,6 +543,32 @@ static void logMemAppend(const char *fmt, va_list ap) { } +// DVX's path rules are DOS's on every platform: the separators are +// interchangeable and names compare without case. +static bool pathCharEq(char a, char b) { + bool sepA = platformPathIsSep(a); + bool sepB = platformPathIsSep(b); + + if (sepA || sepB) { + return sepA && sepB; + } + + return toupper((unsigned char)a) == toupper((unsigned char)b); +} + + +// Length of path without a trailing separator (a lone "/" becomes 0). +static int32_t pathTrimmedLen(const char *path) { + int32_t len = (int32_t)strlen(path); + + if (len > 0 && platformPathIsSep(path[len - 1])) { + len--; + } + + return len; +} + + char platformAltScanToChar(int32_t scancode) { if (scancode < 0 || scancode >= ALT_SCAN_TABLE_SIZE) { return 0; @@ -620,6 +659,11 @@ int32_t platformChdir(const char *path) { bool platformCopyFile(const char *srcPath, const char *dstPath) { + // Onto itself: opening the copy would empty the original first + if (platformPathSame(srcPath, dstPath)) { + return true; + } + FILE *src = fopen(srcPath, "rb"); if (!src) { @@ -653,13 +697,11 @@ bool platformCopyFile(const char *srcPath, const char *dstPath) { } -// Tokens are the runs between FILTER_SEPARATORS; no copy of the filter is -// made, so its length is not limited. +// Make and remove a probe file in dir. bool platformDirIsWritable(const char *dir) { - char path[DVX_MAX_PATH]; - int32_t n = snprintf(path, sizeof(path), "%s" DVX_PATH_SEP WRITE_PROBE_NAME, dir); + char path[DVX_MAX_PATH]; - if (n <= 0 || n >= (int32_t)sizeof(path)) { + if (!dir || !dir[0] || !platformPathJoin(path, sizeof(path), dir, WRITE_PROBE_NAME)) { return false; } @@ -675,6 +717,8 @@ bool platformDirIsWritable(const char *dir) { } +// Tokens are the runs between FILTER_SEPARATORS; no copy of the filter is +// made, so its length is not limited. bool platformFilterMatch(const char *name, const char *filter) { int32_t allLen = (int32_t)strlen(PLATFORM_FILTER_ALL); bool anyToken = false; @@ -815,44 +859,123 @@ void platformOutOfMemory(size_t bytes) { } +// The name after the last separator, or after a drive ("C:A.TXT"). const char *platformPathBaseName(const char *path) { if (!path) { return ""; } const char *sep = platformPathDirEnd(path); - return sep ? sep + 1 : path; -} - -// Return the last path separator in `path`, or NULL if none. -// -// Platform-specific, because what counts as a separator varies: -// - DJGPP/DOS and Win32 accept both '/' and '\\' -- we have to check -// both and return whichever appears last. -// - Unix-like systems use only '/'. '\\' is a legal filename -// character there, so treating it as a separator would incorrectly -// split paths containing literal backslashes. -char *platformPathDirEnd(const char *path) { -#if defined(__DJGPP__) || defined(_WIN32) || defined(_WIN64) - char *fwd = strrchr(path, '/'); - char *back = strrchr(path, '\\'); - - if (back > fwd) { - return back; + if (sep) { + return sep + 1; } - return fwd; -#else - return strrchr(path, '/'); -#endif + return (isalpha((unsigned char)path[0]) && path[1] == ':') ? path + DOS_DRIVE_SPEC_LEN : path; +} + + +// Return the last path separator in `path` (platformPathIsSep), or NULL +// if none. +char *platformPathDirEnd(const char *path) { + const char *end = NULL; + + for (const char *p = path; *p; p++) { + if (platformPathIsSep(*p)) { + end = p; + } + } + + return (char *)end; +} + + +bool platformPathDirName(const char *path, char *out, int32_t outSize) { + const char *end = platformPathDirEnd(path); + bool drive = isalpha((unsigned char)path[0]) && path[1] == ':'; + int32_t len = 1; + const char *from = "."; + + if (end) { + bool root = end == path || (drive && end == path + DOS_DRIVE_SPEC_LEN); + + from = path; + len = (int32_t)(end - path) + (root ? 1 : 0); + } else if (drive) { + from = path; + len = DOS_DRIVE_SPEC_LEN; + } + + if (len >= outSize) { + return false; + } + + memcpy(out, from, (size_t)len); + out[len] = '\0'; + platformPathNative(out); + return true; } -// DVX paths may use either separator on every platform (INI files and app -// arguments are shared between DOS and the host build), so both count. bool platformPathIsAbsolute(const char *path) { - return path && (path[0] == '/' || path[0] == '\\' || (isalpha((unsigned char)path[0]) && path[1] == ':')); + return path && (platformPathIsSep(path[0]) || (isalpha((unsigned char)path[0]) && path[1] == ':')); +} + + +bool platformPathIsSep(char c) { + return c == '/' || c == '\\'; +} + + +const char *platformPathIsUnder(const char *path, const char *dir) { + if (!path || !dir) { + return NULL; + } + + int32_t len = pathTrimmedLen(dir); + + for (int32_t i = 0; i < len; i++) { + if (!pathCharEq(path[i], dir[i])) { + return NULL; + } + } + + if (path[len] == '\0') { + return path + len; + } + + return platformPathIsSep(path[len]) ? path + len + 1 : NULL; +} + + +bool platformPathJoin(char *out, int32_t outSize, const char *dir, const char *name) { + size_t dirLen = strlen(dir); + bool alone = dirLen == 0 || platformPathIsAbsolute(name); + bool hasSep = dirLen > 0 && platformPathIsSep(dir[dirLen - 1]); + int n = alone ? snprintf(out, (size_t)outSize, "%s", name) : snprintf(out, (size_t)outSize, "%s%s%s", dir, hasSep ? "" : DVX_PATH_SEP, name); + + if (n < 0 || n >= outSize) { + return false; + } + + platformPathNative(out); + return true; +} + + +char *platformPathNative(char *path) { + for (char *p = path; p && *p; p++) { + if (platformPathIsSep(*p)) { + *p = DVX_PATH_SEP[0]; + } + } + + return path; +} + + +bool platformPathSame(const char *a, const char *b) { + return a && b && pathTrimmedLen(a) == pathTrimmedLen(b) && platformPathIsUnder(a, b) != NULL; } @@ -887,6 +1010,35 @@ bool platformPathSibling(char *out, int32_t outSize, const char *path, const cha } +char *platformPathTidy(char *path) { + if (!path) { + return NULL; + } + + platformPathNative(path); + + // A leading pair (a network "\\\\server") stays; any other run is one + int32_t keep = platformPathIsSep(path[0]) && platformPathIsSep(path[1]) ? 2 : 0; + int32_t dst = keep; + + for (int32_t src = keep; path[src]; src++) { + if (!(platformPathIsSep(path[src]) && dst > keep && platformPathIsSep(path[dst - 1]))) { + path[dst++] = path[src]; + } + } + + // A trailing separator goes, except a root's ("\\", "C:\\") + bool root = dst == 1 || (dst == DOS_ROOT_LEN && path[1] == ':'); + + if (dst > 0 && platformPathIsSep(path[dst - 1]) && !root) { + dst--; + } + + path[dst] = '\0'; + return path; +} + + char *platformReadFile(const char *path, int32_t *outLen) { if (outLen) { *outLen = 0; @@ -1025,13 +1177,19 @@ int32_t platformStripLineEndings(char *buf, int32_t len) { const char *platformTempDir(void) { + static char native[DVX_MAX_PATH]; const char *dir = getenv("TEMP"); if (!dir || !dir[0]) { dir = getenv("TMP"); } - return (dir && dir[0]) ? dir : NULL; + if (!dir || !dir[0]) { + return NULL; + } + + snprintf(native, sizeof(native), "%s", dir); + return platformPathNative(native); } diff --git a/src/libs/kpunch/print/printBas.c b/src/libs/kpunch/print/printBas.c index 10157830..b48bc7dd 100644 --- a/src/libs/kpunch/print/printBas.c +++ b/src/libs/kpunch/print/printBas.c @@ -177,7 +177,7 @@ int32_t PrintChoiceDest(void) { // The file name chosen in the last PrintChoose when printing to a file. const char *PrintChoiceFile(void) { - return sChoice.filePath; + return platformPathNative(sChoice.filePath); } diff --git a/src/libs/kpunch/print/printJob.c b/src/libs/kpunch/print/printJob.c index 0678c9da..f0d5974a 100644 --- a/src/libs/kpunch/print/printJob.c +++ b/src/libs/kpunch/print/printJob.c @@ -399,6 +399,7 @@ void printSettingsRead(PrefsHandleT *h, PrintSettingsT *s) { s->page.tabWidth = prefsGetInt(h, PRINT_SECTION, INI_TAB, def.page.tabWidth); s->page.wrap = prefsGetBool(h, PRINT_SECTION, INI_WRAP, def.page.wrap); snprintf(s->filePath, sizeof(s->filePath), "%s", prefsGetString(h, PRINT_SECTION, INI_FILE, def.filePath)); + platformPathNative(s->filePath); snprintf(s->page.header, sizeof(s->page.header), "%s", prefsGetString(h, PRINT_SECTION, INI_HEADER, def.page.header)); snprintf(s->page.footer, sizeof(s->page.footer), "%s", prefsGetString(h, PRINT_SECTION, INI_FOOTER, def.page.footer)); diff --git a/src/libs/kpunch/xfer/xfer.c b/src/libs/kpunch/xfer/xfer.c index 746de49d..ca6bf2bd 100644 --- a/src/libs/kpunch/xfer/xfer.c +++ b/src/libs/kpunch/xfer/xfer.c @@ -129,7 +129,6 @@ static bool drainWhileAsking(XferT *x); static void failOpen(XferT *x, const char *path, bool tellPeer); static int64_t fileSizeOf(const char *path); static bool initCommon(XferT *x, XferProtoE proto, const XferIoT *io, const XferOptionsT *opt, bool sending); -static void joinPath(const char *dir, const char *name, char *out); static bool makeUniquePath(const char *dir, const char *name, char *out); static void mapNamePart(const char *src, int32_t srcLen, char *dst, int32_t dstMax); static bool openReceiveFile(XferT *x, const char *path, bool append, int32_t localSize); @@ -286,16 +285,6 @@ static bool initCommon(XferT *x, XferProtoE proto, const XferIoT *io, const Xfer } -// dir + separator + name (no separator after an empty dir or one that -// already ends in one). -static void joinPath(const char *dir, const char *name, char *out) { - size_t dirLen = strlen(dir); - const char *sep = (dirLen == 0 || dir[dirLen - 1] == '/' || dir[dirLen - 1] == '\\') ? "" : DVX_PATH_SEP; - - snprintf(out, XFER_PATH_MAX, "%s%s%s", dir, sep, name); -} - - // Build dir + name, adding "~N" to the base name until no file or folder // of that name exists. The base is what comes before the extension (the // last dot of a long name, the only one of an 8.3 name) and stays within @@ -320,7 +309,7 @@ static bool makeUniquePath(const char *dir, const char *name, char *out) { int32_t keep = DVX_MIN(baseLen, baseMax - (int32_t)strlen(suffix)); snprintf(cand, sizeof(cand), "%.*s%s%s", (int)keep, name, suffix, ext); - joinPath(dir, cand, out); + platformPathJoin(out, XFER_PATH_MAX, dir, cand); if (fileSizeOf(out) < 0) { return true; @@ -1104,7 +1093,7 @@ void xferFlushOut(XferT *x) { XferOpenE xferOpenReceived(XferT *x, const char *remoteName, int32_t remoteSize, bool canResume, bool resumeRequested) { xferSafeName(remoteName, platformLongNames(x->paths[0]), x->recvName); - joinPath(x->paths[0], x->recvName, x->askPath); + platformPathJoin(x->askPath, sizeof(x->askPath), x->paths[0], x->recvName); x->prog.askRemoteSize = remoteSize; x->prog.askLocalSize = 0; x->prog.askCanResume = false; diff --git a/src/libs/kpunch/xfer/xfer.h b/src/libs/kpunch/xfer/xfer.h index 510058a0..1b7144c7 100644 --- a/src/libs/kpunch/xfer/xfer.h +++ b/src/libs/kpunch/xfer/xfer.h @@ -53,7 +53,7 @@ #include #include -#define XFER_PATH_MAX 260 +#define XFER_PATH_MAX DVX_MAX_PATH #define XFER_NAME_MAX (PLATFORM_LONG_NAME_MAX + 1) // a long name plus NUL (xferSafeName) #define XFER_MSG_MAX 96 #define XFER_MAX_FILES 16 diff --git a/src/libs/kpunch/xfer/xferBas.c b/src/libs/kpunch/xfer/xferBas.c index 50cf81b3..885b67a5 100644 --- a/src/libs/kpunch/xfer/xferBas.c +++ b/src/libs/kpunch/xfer/xferBas.c @@ -327,11 +327,14 @@ int32_t XferErrors(int32_t handle) { } -// The file being transferred, as stored locally ("" for a bad handle) +// The file being transferred, as stored locally, in native separators +// ("" for a bad handle) const char *XferFile(int32_t handle) { + static char file[XFER_PATH_MAX]; const XferProgressT *p = progressOf(handle); - return p ? p->fileName : ""; + snprintf(file, sizeof(file), "%s", p ? p->fileName : ""); + return platformPathNative(file); } diff --git a/src/loader/loaderMain.c b/src/loader/loaderMain.c index 72fdb9a3..6b8f7dce 100644 --- a/src/loader/loaderMain.c +++ b/src/loader/loaderMain.c @@ -842,7 +842,7 @@ static void logStart(void) { if (!platformIsReadOnly()) { snprintf(sLogPath, sizeof(sLogPath), "%s", LOG_PATH); } else if (tmpDir && platformDirIsWritable(tmpDir)) { - snprintf(sLogPath, sizeof(sLogPath), "%s" DVX_PATH_SEP "%s", tmpDir, LOG_PATH); + platformPathJoin(sLogPath, sizeof(sLogPath), tmpDir, LOG_PATH); } if (!sLogPath[0]) { @@ -1166,13 +1166,8 @@ int main(int argc, char *argv[]) { // Change to the directory containing the executable so relative // paths (LIBS/, WIDGETS/, APPS/, CONFIG/) resolve correctly. char exeDir[DVX_MAX_PATH]; - strncpy(exeDir, argv[0], sizeof(exeDir) - 1); - exeDir[sizeof(exeDir) - 1] = '\0'; - char *sep = platformPathDirEnd(exeDir); - - if (sep) { - *sep = '\0'; + if (platformPathDirName(argv[0], exeDir, sizeof(exeDir))) { platformChdir(exeDir); } @@ -1206,7 +1201,7 @@ int main(int argc, char *argv[]) { if (!handles || arrlen(handles) == 0) { splashShutdownIfActive(); - fprintf(stderr, "No modules loaded from %s/ or %s/\n", LIBS_DIR, WIDGET_DIR); + fprintf(stderr, "No modules loaded from %s" DVX_PATH_SEP " or %s" DVX_PATH_SEP "\n", LIBS_DIR, WIDGET_DIR); arrfree(handles); return 1; } diff --git a/src/test/basdev/testBasDev.c b/src/test/basdev/testBasDev.c index d42e86a1..f2b47f08 100644 --- a/src/test/basdev/testBasDev.c +++ b/src/test/basdev/testBasDev.c @@ -221,7 +221,7 @@ static void caseBasicIde(void) { // app), and the STUB carrier CompBuildApp reads (bascomp has one) snprintf(kidDir, sizeof(kidDir), "%s/paride/kid", tTempDir()); snprintf(kidApp, sizeof(kidApp), "%s/kid.app", tTempDir()); - basBuildDirOf(BAS_BUILD_STUB_CARRIER, carrierDir, sizeof(carrierDir)); + platformPathDirName(BAS_BUILD_STUB_CARRIER, carrierDir, sizeof(carrierDir)); platformMkdirRecursive(carrierDir); remove(kidApp); TEST_TRUE(copyKid(kidDir)); diff --git a/src/test/dialogs/dialogsTest.c b/src/test/dialogs/dialogsTest.c index 44e6693c..aa860ddd 100644 --- a/src/test/dialogs/dialogsTest.c +++ b/src/test/dialogs/dialogsTest.c @@ -36,6 +36,7 @@ #include "box/box.h" #include "button/button.h" #include "canvas/canvas.h" +#include "dropdown/dropdown.h" #include "listBox/listBox.h" #include "slider/slider.h" #include "spinner/spinner.h" @@ -213,7 +214,8 @@ typedef enum { FileOpTypeNameE, FileOpDblClickFileE, FileOpSelectDirOpenE, - FileOpEnterInNameE + FileOpEnterInNameE, + FileOpDriveE } FileOpE; typedef struct { @@ -225,6 +227,11 @@ typedef struct { char nameAfter[DVX_MAX_PATH]; int32_t itemsAfter; int32_t itemsBefore; + int32_t driveIdx; // FileOpDriveE: the drive to pick (-1: none) + int32_t driveItems; // drive dropdown: its items, first label, + int32_t driveBefore; // and the selection before / after + int32_t driveAfter; + char driveLabel[MAX_TITLE_LEN]; bool stillModalAfter; bool nestedSeen; char nestedTitle[MAX_TITLE_LEN]; @@ -719,6 +726,26 @@ static void caseFileDialog(void) { TEST_STR_EQ(f.pathAfter, sub); TEST_TRUE(f.stillModalAfter); + // Drives: the host has one, "/", selected for the folder; picking it + // goes to its root, picking none does nothing. + memset(&f, 0, sizeof(f)); + f.op = FileOpDriveE; + f.driveIdx = 0; + TEST_TRUE(!dvxFileDialog(tCtx(), "Open It", FD_OPEN, dir, NULL, 0, out, sizeof(out))); + TEST_EQ(f.driveItems, 1); + TEST_STR_EQ(f.driveLabel, "/"); + TEST_EQ(f.driveBefore, 0); + TEST_EQ(f.driveAfter, 0); + TEST_STR_EQ(f.pathAfter, "/"); + TEST_TRUE(f.stillModalAfter); + + memset(&f, 0, sizeof(f)); + f.op = FileOpDriveE; + f.driveIdx = -1; + TEST_TRUE(!dvxFileDialog(tCtx(), "Open It", FD_OPEN, dir, NULL, 0, out, sizeof(out))); + TEST_STR_EQ(f.pathAfter, real); + TEST_EQ(f.driveAfter, -1); + // Filters: single glob, then switching to the next filter. memset(&f, 0, sizeof(f)); f.op = FileOpFilterNextE; @@ -1522,9 +1549,10 @@ static bool customOk(WindowT *dlg, void *userCtx) { } -// File dialog script. Layout: pathRow(label,input), list, [filterRow -// (label,dropdown)], nameRow(label,input), buttons. Nested confirmation -// boxes (different title) are answered with nestedButton. +// File dialog script. Layout: pathRow(label,input,label,drive +// dropdown), list, [filterRow(label,dropdown)], nameRow(label,input), +// buttons. Nested confirmation boxes (different title) are answered +// with nestedButton. static void fileScript(AppContextT *ctx, WindowT *modal, void *userCtx) { FileScriptT *f = (FileScriptT *)userCtx; @@ -1546,6 +1574,7 @@ static void fileScript(AppContextT *ctx, WindowT *modal, void *userCtx) { WidgetT *root = modal->widgetRoot; WidgetT *pathInput = childAt(childAt(root, 0), 1); + WidgetT *driveDd = childAt(childAt(root, 0), 3); WidgetT *list = childAt(root, 1); bool hasFilter = childAt(root, 4) != NULL; WidgetT *filterDd = hasFilter ? childAt(childAt(root, 2), 1) : NULL; @@ -1554,9 +1583,9 @@ static void fileScript(AppContextT *ctx, WindowT *modal, void *userCtx) { int32_t sx = 0; int32_t sy = 0; - TEST_TRUE(pathInput && list && nameInput && okBtn); + TEST_TRUE(pathInput && driveDd && list && nameInput && okBtn); - if (!pathInput || !list || !nameInput || !okBtn) { + if (!pathInput || !driveDd || !list || !nameInput || !okBtn) { scriptLeave(); return; } @@ -1567,6 +1596,7 @@ static void fileScript(AppContextT *ctx, WindowT *modal, void *userCtx) { if (f->acted) { f->stillModalAfter = true; f->itemsAfter = dvxListBoxApi()->getItemCount(list); + f->driveAfter = wgtDropdownGetSelected(driveDd); snprintf(f->pathAfter, sizeof(f->pathAfter), "%s", wgtGetText(pathInput)); tDialogClickButton(modal, "Cancel"); scriptLeave(); @@ -1575,6 +1605,9 @@ static void fileScript(AppContextT *ctx, WindowT *modal, void *userCtx) { f->acted = true; f->itemsBefore = dvxListBoxApi()->getItemCount(list); + f->driveItems = wgtDropdownGetItemCount(driveDd); + f->driveBefore = wgtDropdownGetSelected(driveDd); + snprintf(f->driveLabel, sizeof(f->driveLabel), "%s", wgtDropdownGetItem(driveDd, 0)); switch (f->op) { case FileOpCancelE: @@ -1639,6 +1672,12 @@ static void fileScript(AppContextT *ctx, WindowT *modal, void *userCtx) { tWidgetCenter(okBtn, &sx, &sy); tClick(sx, sy); break; + + case FileOpDriveE: + wgtSetFocused(driveDd); + wgtDropdownSetSelected(driveDd, f->driveIdx); + driveDd->onChange(driveDd); + break; } scriptLeave(); diff --git a/src/test/dos/fixtures/pathsmk/pathsmk.dbp b/src/test/dos/fixtures/pathsmk/pathsmk.dbp new file mode 100644 index 00000000..4163c10f --- /dev/null +++ b/src/test/dos/fixtures/pathsmk/pathsmk.dbp @@ -0,0 +1,19 @@ +[Project] +Name = Path Smoke +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = DOS harness fixture: paths written with / work, paths shown use \ + +[Modules] +File0 = ../../../../include/basic/fileops.bas +File1 = ../../../../include/basic/shell.bas +Count = 2 + +[Forms] +File0 = pathsmk.frm +Count = 1 + +[Settings] +StartupForm = PathSmoke diff --git a/src/test/dos/fixtures/pathsmk/pathsmk.frm b/src/test/dos/fixtures/pathsmk/pathsmk.frm new file mode 100644 index 00000000..56b9018d --- /dev/null +++ b/src/test/dos/fixtures/pathsmk/pathsmk.frm @@ -0,0 +1,133 @@ +VERSION DVX 1.00 + +Begin Form PathSmoke + Caption = "Path Smoke" + Layout = VBox + AutoSize = False + Resizable = False + Width = 360 + Height = 90 + + Begin Label lblResult + Caption = "Running..." + Weight = 1 + End +End + +' The MIT License (MIT) +' +' Copyright (C) 2026 Scott Duensing +' +' Permission is hereby granted, free of charge, to any person obtaining a copy +' of this software and associated documentation files (the "Software"), to +' deal in the Software without restriction, including without limitation the +' rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +' sell copies of the Software, and to permit persons to whom the Software is +' furnished to do so, subject to the following conditions: +' +' The above copyright notice and this permission notice shall be included in +' all copies or substantial portions of the Software. +' +' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +' FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +' IN THE SOFTWARE. + +' pathsmk -- DOS harness fixture for paths.scr. Started as +' TEST/PATHSMK.APP (a path written with /), it checks that paths a program +' writes with / work exactly as with \ (OPEN, FILELEN, DIR$, NAME, KILL, +' MKDIR, CHDIR, RMDIR, attributes, the FileOps path functions, DIR$ of a +' drive root) and that every path DVX hands back uses \ (CURDIR$, +' App.Path, App.Data, ShellAppPath$, ShellConfigFile$, FileOpsPathJoin$, +' FileOpsPathParent$, FileOpsShortPath$, FileOpsPathNative$). The window +' title reports "PATHS PASS", or "PATHS FAIL ". + +OPTION EXPLICIT + +CONST SLASH = "/" +CONST BACKSLASH = "\" +CONST TEST_FILE = "TEST/PS1.TXT" +CONST TEST_RENAMED = "TEST/PS2.TXT" +CONST TEST_DIR = "TEST/PSDIR" +CONST ATTR_FILE = "TEST//PS3.TXT" + +DIM SHARED failText AS STRING + + +SUB Check(BYVAL ok AS BOOLEAN, BYVAL what AS STRING) + IF NOT ok AND failText = "" THEN + failText = what + END IF +END SUB + + +' A path DVX handed back: the native separator, never /. +SUB CheckNative(BYVAL path AS STRING, BYVAL what AS STRING) + Check path <> "" AND INSTR(path, SLASH) = 0, what + " " + path +END SUB + + +SUB PathSmoke_Load + DIM ch AS INTEGER + DIM home AS STRING + DIM rootSlash AS STRING + DIM rootBack AS STRING + + ' Paths DVX hands back + home = CURDIR$ + CheckNative home, "CURDIR$" + Check INSTR(home, BACKSLASH) > 0, "CURDIR$ has no \ " + home + CheckNative App.Path, "App.Path" + CheckNative App.Data, "App.Data" + CheckNative ShellAppPath$(), "ShellAppPath$" + CheckNative ShellConfigFile$("X.INI"), "ShellConfigFile$" + Check FileOpsPathNative$("C:/A/B.TXT") = "C:\A\B.TXT", "PathNative " + FileOpsPathNative$("C:/A/B.TXT") + Check FileOpsPathJoin$("C:/DVX/", "A/B.TXT") = "C:\DVX\A\B.TXT", "PathJoin " + FileOpsPathJoin$("C:/DVX/", "A/B.TXT") + Check FileOpsPathParent$("C:/DVX/TEST/") = "C:\DVX", "PathParent " + FileOpsPathParent$("C:/DVX/TEST/") + CheckNative FileOpsShortPath$(home + "/TEST"), "ShortPath" + Check FileOpsPathSame("C:/DVX", "c:\dvx\") AND FileOpsPathIsInside("C:/DVX/TEST/X", "C:\DVX"), "PathSame / IsInside" + + ' Paths written with / work as with \ + ch = FREEFILE + OPEN TEST_FILE FOR OUTPUT AS #ch + PRINT #ch, "slash" + CLOSE #ch + Check FILELEN("TEST\PS1.TXT") > 0, "OPEN with /" + Check UCASE$(DIR$("TEST/PS*.TXT")) = "PS1.TXT", "DIR$ with / " + DIR$("TEST/PS*.TXT") + NAME TEST_FILE AS TEST_RENAMED + Check FILELEN("test\ps2.txt") > 0 AND FileOpsIsFolder("TEST/") AND NOT FileOpsIsFolder(TEST_RENAMED), "NAME with /" + KILL TEST_RENAMED + Check DIR$("TEST/PS*.TXT") = "", "KILL with /" + + MKDIR TEST_DIR + CHDIR TEST_DIR + Check UCASE$(RIGHT$(CURDIR$, 11)) = "\TEST\PSDIR", "CHDIR with / " + CURDIR$ + CHDIR home + RMDIR TEST_DIR + Check NOT FileOpsIsFolder(TEST_DIR), "RMDIR with /" + + ' Attributes go to DOS directly: / and a doubled separator work too + ch = FREEFILE + OPEN "TEST\PS3.TXT" FOR OUTPUT AS #ch + CLOSE #ch + Check FileOpsSetAttr(ATTR_FILE, FILEOPS_ATTR_READONLY) AND (FileOpsGetAttr("TEST/PS3.TXT") AND FILEOPS_ATTR_READONLY) <> 0, "SetAttr with /" + Check FileOpsSetAttr(ATTR_FILE, 0) AND FileOpsGetAttr(ATTR_FILE) = 0, "clear attributes with /" + KILL "TEST\PS3.TXT" + + ' DIR$ of a drive root lists the root (not the drive's current folder, + ' which holds DVX.EXE), written either way + rootSlash = DIR$(LEFT$(home, 2) + "/*.*") + rootBack = DIR$(LEFT$(home, 2) + "\*.*") + Check rootSlash = rootBack AND rootBack <> DIR$("*.*"), "DIR$ of the root " + rootSlash + " / " + rootBack + + lblResult.Caption = home + + IF failText = "" THEN + PathSmoke.Caption = "PATHS PASS" + ELSE + PathSmoke.Caption = "PATHS FAIL " + failText + END IF +END SUB diff --git a/src/test/dos/scripts/filedlg.scr b/src/test/dos/scripts/filedlg.scr new file mode 100644 index 00000000..f3e1cbf2 --- /dev/null +++ b/src/test/dos/scripts/filedlg.scr @@ -0,0 +1,32 @@ +# filedlg.scr -- the file dialog's drive drop-down. +# +# Notepad's File > Open dialog has the drive drop-down at the end of the +# path row, showing the folder's drive (C:). Alt+D opens it: every drive +# DOS has, none of them read. A: (an empty floppy drive) says "Drive A: +# is not ready.", and the drop-down shows C: again; the dialog is then +# cancelled. +TIMEOUT 60 +WAITWIN "Program Manager" +MOVE 639 479 +LAUNCH apps/kpunch/notepad/notepad.app +WAITWIN "Untitled - Notepad" +KEY Alt+F O +WAITWIN "Open" +WAIT 5 +SHOT fd-open +KEY Alt+D +WAIT 5 +SHOT fd-list +KEY Up Enter +WAITWIN "Drive Not Ready" +WAIT 5 +SHOT fd-nordy +KEY Enter +WAITNOWIN "Drive Not Ready" +WAIT 5 +SHOT fd-back +KEY Esc +WAITNOWIN "Open" +KEY Alt+F4 +WAITNOWIN "Untitled - Notepad" +QUIT diff --git a/src/test/dos/scripts/paths.scr b/src/test/dos/scripts/paths.scr new file mode 100644 index 00000000..b74a3690 --- /dev/null +++ b/src/test/dos/scripts/paths.scr @@ -0,0 +1,19 @@ +# paths.scr -- paths written with / work on DOS, paths shown use \. +# +# TEST/PATHSMK.APP (fixtures/pathsmk) is started by a path written with /; +# it checks that OPEN, FILELEN, DIR$, NAME, KILL, MKDIR, CHDIR, RMDIR, the +# attribute calls and the FileOps path functions take / as \ (doubled and +# trailing separators too), that DIR$ of "C:/*.*" lists the root, and that +# CURDIR$, App.Path, App.Data, ShellAppPath$, ShellConfigFile$ and the +# FileOps results use \. Its title is "PATHS PASS" only when all held. +TIMEOUT 60 +WAITWIN "Program Manager" +MOVE 639 479 +LAUNCH TEST/PATHSMK.APP +WAITWIN "PATHS *" +WAIT 10 +SHOT paths +ASSERTWIN "PATHS PASS" +KEY Alt+F4 +WAITNOWIN "PATHS *" +QUIT diff --git a/src/test/fileops/fileOpsTest.c b/src/test/fileops/fileOpsTest.c index 71e86f6f..866798b3 100644 --- a/src/test/fileops/fileOpsTest.c +++ b/src/test/fileops/fileOpsTest.c @@ -242,6 +242,8 @@ static void caseBasicPlatform(void) { TEST_EQ(FileOpsPathIsInside(tp("bp"), tp("bp/src")), BAS_FALSE); TEST_EQ(FileOpsPathIsInside(NULL, tp("bp")), BAS_FALSE); TEST_STR_EQ(FileOpsPathSep(), DVX_PATH_SEP); + TEST_STR_EQ(FileOpsPathNative("/a/b.txt"), "/a/b.txt"); + TEST_STR_EQ(FileOpsPathNative(NULL), ""); TEST_EQ(FileOpsFilterMatch("A.TXT", "*.txt"), BAS_TRUE); TEST_EQ(FileOpsFilterMatch("a.bmp", "*.txt;*.bmp"), BAS_TRUE); TEST_EQ(FileOpsFilterMatch("a.png", "*.txt;*.bmp"), BAS_FALSE); @@ -952,7 +954,13 @@ static void casePaths(void) { TEST_TRUE(fileOpsPathJoin(out, sizeof(out), "/a", "b")); TEST_STR_EQ(out, "/a/b"); TEST_TRUE(fileOpsPathJoin(out, sizeof(out), "C:\\", "B")); - TEST_STR_EQ(out, "C:\\B"); + TEST_STR_EQ(out, "C:" DVX_PATH_SEP "B"); + TEST_TRUE(fileOpsPathJoin(out, sizeof(out), "C:/A\\", "B/C")); + TEST_STR_EQ(out, "C:" DVX_PATH_SEP "A" DVX_PATH_SEP "B" DVX_PATH_SEP "C"); + TEST_TRUE(fileOpsPathJoin(out, sizeof(out), "/a", "C:\\X")); + TEST_STR_EQ(out, "C:" DVX_PATH_SEP "X"); + TEST_TRUE(fileOpsPathJoin(out, sizeof(out), "", "x")); + TEST_STR_EQ(out, "x"); TEST_TRUE(!fileOpsPathJoin(small, sizeof(small), "/abc", "def")); TEST_TRUE(fileOpsPathParent("/a/b", out, sizeof(out))); @@ -962,9 +970,11 @@ static void casePaths(void) { TEST_TRUE(fileOpsPathParent("/a", out, sizeof(out))); TEST_STR_EQ(out, "/"); TEST_TRUE(fileOpsPathParent("C:\\DVX", out, sizeof(out))); - TEST_STR_EQ(out, "C:\\"); + TEST_STR_EQ(out, "C:" DVX_PATH_SEP); TEST_TRUE(fileOpsPathParent("C:\\DVX\\APPS", out, sizeof(out))); - TEST_STR_EQ(out, "C:\\DVX"); + TEST_STR_EQ(out, "C:" DVX_PATH_SEP "DVX"); + TEST_TRUE(fileOpsPathParent("C:/DVX/APPS/", out, sizeof(out))); + TEST_STR_EQ(out, "C:" DVX_PATH_SEP "DVX"); TEST_TRUE(!fileOpsPathParent("/", out, sizeof(out))); TEST_TRUE(!fileOpsPathParent("C:\\", out, sizeof(out))); TEST_TRUE(!fileOpsPathParent("name", out, sizeof(out))); diff --git a/src/test/formrt/testFormRt.c b/src/test/formrt/testFormRt.c index ffd42661..b9c136df 100644 --- a/src/test/formrt/testFormRt.c +++ b/src/test/formrt/testFormRt.c @@ -525,20 +525,23 @@ static const WgtMethodDescT sProbeMethods[] = { // Build a shipped BASIC app's .frm with stubs spliced in at the start of // its code section (standing in for the app's commdlg.bas module, so the // dialogs are scripted) and test SUBs appended at the end. The real -// help.bas and print.bas are the module source, as in the apps' projects -// (HelpView resolves to the stub library; the printing functions are only -// declared, the tests never call them). +// fileops.bas, help.bas and print.bas are the module source, as in the +// apps' projects (HelpView resolves to the stub library; the file +// operations and printing functions are only declared, the tests never +// call them). static bool appBuild(FrtAppT *app, const char *frmPath, const char *clsPath, const char *stubs, const char *tests) { int32_t len = 0; + int32_t fopsLen = 0; int32_t helpLen = 0; int32_t printLen = 0; int32_t clsLen = 0; char *frm = platformReadFile(frmPath, &len); + char *fopsBas = platformReadFile(FRT_INCLUDE_DIR "/fileops.bas", &fopsLen); char *helpOnly = platformReadFile(FRT_INCLUDE_DIR "/help.bas", &helpLen); char *printBas = platformReadFile(FRT_INCLUDE_DIR "/print.bas", &printLen); char *cls = clsPath ? platformReadFile(clsPath, &clsLen) : NULL; - size_t helpCap = (size_t)helpLen + (size_t)printLen + (size_t)clsLen + FRT_CLASS_WRAP_LEN; - char *helpBas = (helpOnly && printBas) ? (char *)malloc(helpCap) : NULL; + size_t helpCap = (size_t)fopsLen + (size_t)helpLen + (size_t)printLen + (size_t)clsLen + FRT_CLASS_WRAP_LEN; + char *helpBas = (fopsBas && helpOnly && printBas) ? (char *)malloc(helpCap) : NULL; TEST_TRUE(frm != NULL); TEST_TRUE(helpBas != NULL); @@ -553,9 +556,10 @@ static bool appBuild(FrtAppT *app, const char *frmPath, const char *clsPath, con basExtractClassName(cls, clsLen, clsPath, className, (int32_t)sizeof(className)); } - snprintf(helpBas, helpCap, cls ? "%s\n%s\nBEGINCLASS \"%s\"\n%s\nENDCLASS\n" : "%s\n%s", helpOnly, printBas, className, cls ? cls + basClassCodePos(cls, clsLen) : ""); + snprintf(helpBas, helpCap, cls ? "%s\n%s\n%s\nBEGINCLASS \"%s\"\n%s\nENDCLASS\n" : "%s\n%s\n%s", fopsBas, helpOnly, printBas, className, cls ? cls + basClassCodePos(cls, clsLen) : ""); } + free(fopsBas); free(helpOnly); free(printBas); free(cls); @@ -1174,10 +1178,16 @@ static void caseBuildProject(void) { // Directory helper and project open. char out[DVX_MAX_PATH]; - basBuildDirOf("a/b/c.dbp", out, sizeof(out)); + TEST_TRUE(platformPathDirName("a/b/c.dbp", out, sizeof(out))); TEST_STR_EQ(out, "a/b"); - basBuildDirOf("c.dbp", out, sizeof(out)); + TEST_TRUE(platformPathDirName("c.dbp", out, sizeof(out))); TEST_STR_EQ(out, "."); + TEST_TRUE(platformPathDirName("C:\\c.dbp", out, sizeof(out))); + TEST_STR_EQ(out, "C:" DVX_PATH_SEP); + TEST_TRUE(platformPathDirName("C:c.dbp", out, sizeof(out))); + TEST_STR_EQ(out, "C:"); + TEST_TRUE(platformPathDirName("/c.dbp", out, sizeof(out))); + TEST_STR_EQ(out, "/"); TEST_TRUE(!basBuildOpenProject("frtBuildProj/none.dbp", &prefs, out, sizeof(out))); // An unreadable icon is reported by name, not as a write failure. diff --git a/src/test/ide/testIde.c b/src/test/ide/testIde.c index 46132364..0e7b9d43 100644 --- a/src/test/ide/testIde.c +++ b/src/test/ide/testIde.c @@ -1552,7 +1552,7 @@ static void snapshotBascomp(void) { // relative to the DVX folder (here: the working directory) char carrierDir[DVX_MAX_PATH]; - basBuildDirOf(BAS_BUILD_STUB_CARRIER, carrierDir, sizeof(carrierDir)); + platformPathDirName(BAS_BUILD_STUB_CARRIER, carrierDir, sizeof(carrierDir)); platformMkdirRecursive(carrierDir); TEST_TRUE(tWriteFile(BAS_BUILD_STUB_CARRIER, data, len)); free(data); diff --git a/src/test/lint/analyzerBaseline.txt b/src/test/lint/analyzerBaseline.txt index 1875b321..a60f02dd 100644 --- a/src/test/lint/analyzerBaseline.txt +++ b/src/test/lint/analyzerBaseline.txt @@ -28,6 +28,8 @@ 4 src/apps/kpunch/dvxbasic/runtime/vm.c|execStringOp|use of uninitialized value [CWE-457] [-Wanalyzer-use-of-uninitialized-value] # FP: copy is stored via arrins at m->history[0] (trimmed by the pop loop, freed with the model); stb_ds grow aborts rather than failing. 1 src/libs/kpunch/dvxshell/pmModel.c|pmHistoryAdd|leak of [CWE-401] [-Wanalyzer-malloc-leak] +# FP: allocSlot appends the calloc'd slot to sApps with arrput; the stb_ds OOM hook never returns, so the slot is never dropped (freed with the slot index). +1 src/libs/kpunch/dvxshell/shellApp.c|shellLoadAppInternal|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: name copy is stored in fd->entries[] and freed by the listing free loop. 1 src/libs/kpunch/libdvx/dvxDialog.c|fdAddEntry|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: listItems malloc'd in fdLoadDir is freed by the free(fd->listItems) just above this NULL store; analyzer loses the pointer after the per-entry free loop. diff --git a/src/test/lint/clangTidyBaseline.txt b/src/test/lint/clangTidyBaseline.txt index b614eb7a..2df8cdee 100644 --- a/src/test/lint/clangTidyBaseline.txt +++ b/src/test/lint/clangTidyBaseline.txt @@ -70,8 +70,8 @@ 2 src/libs/kpunch/dvxnet/netResolve.c|clang-analyzer-security.insecureAPI.strcpy|Call to function 'strcpy' is insecure as it does not provide bounding of the memory buffer. Replace unbounded copy functions with analogous functions that support length arguments such as 'strlcpy'. CWE-119 # FP: name is cut to sizeof(name) - sizeof(suffix) before the strcat and the suffix is shorter than its buffer, so it always fits 1 src/libs/kpunch/dvxshell/pmModel.c|clang-analyzer-security.insecureAPI.strcpy|Call to function 'strcat' is insecure as it does not provide bounding of the memory buffer. Replace unbounded copy functions with analogous functions that support length arguments such as 'strlcat'. CWE-119 -# FP: allocSlot scans from slot 1; every slot past 0 is a ShellAppT allocSlot itself calloc'd and appended only when non-NULL -1 src/libs/kpunch/dvxshell/shellApp.c|clang-analyzer-core.NullDereference|Access to field 'state' results in a dereference of a null pointer +# FP: allocSlot and findLoadedPath scan from slot 1; every slot past 0 is a ShellAppT allocSlot itself calloc'd and appended only when non-NULL +2 src/libs/kpunch/dvxshell/shellApp.c|clang-analyzer-core.NullDereference|Access to field 'state' results in a dereference of a null pointer # FP: the app id is passed as the task argument on purpose 1 src/libs/kpunch/dvxshell/shellApp.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities # FP: the BASIC hotkey id is passed as dvxRegisterHotkey's user pointer on purpose diff --git a/src/test/samples/golden/basdemo-debug.app b/src/test/samples/golden/basdemo-debug.app index f153c9a46b7bee39d976f51a6bb6b0a46a82651b..5cbf9836488ca2b4001fbcd23f95cfe978bcba83 100644 GIT binary patch delta 8509 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 K2~T@|SNt3L+oae4 delta 7140 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 IJX3`K0s8ozZU6uP delta 4347 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 KH^!sP{`?n+gwPED diff --git a/src/test/samples/golden/fileMgr-debug.app b/src/test/samples/golden/fileMgr-debug.app index ff18cf2c0f988d009455654a748ccaa51a269a3b..eb8915c059124622231ccc2c65626ba89d590658 100644 GIT binary patch delta 9306 zcmai437A#Im9G2Vt9$S3rlp$(8fY52S-RPzADhZ1&EB*qm-q4zmuTpWi22lHqBCY3 zlS?DGV$kmyb?m`U{oI#GMB~0PX5tPCh>02}?rWjqf?Lo3pSt&UL!58kCs1|j)Ty(d zQ+2)aqSWmdr8b>cGkvk6Lox-z8}e&59-W9tx)q+N_Z_#tCePFx-zXWZOIS7 zL4(XoX?>g0x2pnEEkdM5nS`Bq&$GMk_&?GH<3KS6hA%x4cEdAi7jJI87-5x=)IGtHDh8 zLHa5cs|UhtDxP?oUA92KI?*!oyr5{xQ zZ;Vd*UQXJq^kYEjlF8#rZ&9BZl_AsrrSub+q%wURvY*I(L{}GtGwBQr~NHXW(!#!M+)KgrchPTM0ui zELGIjhy92Y0pXnbX|>Bq!Jz$}O+;!{DeDbIQ=75z0xQ&IF3``Y)$N60pyfD_KZ=&2 zq?S%CXv+`8imC5G9K_mU*D9J(8AfATh4lN8#2uZ{&qfTwsyY3f%00VG-5hTT>WMUV zKcwgPMKa?>w;~Xn3yMW#ndXQK2(s4X_GKyTcK3;yK1$`wc+8|s;o1repv38s)Ue6S zACwvkvnY&6O?{XaPP_G-NNr|k;Dm_kqG^p;g8cB+^>k?A8lvm1D%WijsO>Q2WXMde z!z_yT0qy=C+ptP&EvA>_*RHqiN8|8#C8u8?p|*%n2&Etx*6SDdb&ew<{ElKE)>)MS z;g$U$-N_lfUG;<1KZj=eAtn4m0BKbWu4};hHcmS+ctHk0+$jUCg0s2)CTB>W=IxAI z4f~sSGJ1#V1m3$DFr!o_Xug;6I)U;*1}<=uBoC#U;4n;X!^**l`7o0jg$Wc@UxmFN zWl}@pDfSzh037R`5bY(kx}M`*c03nu9xuHd_E>vat)9Cm-ydYmZcCk%Ke3cqCiYMX zLh>&P^pSZIz+ai!k$*CUbA1p%+L5JKaOA5>zoynWzS*0A{-#_4zRozb|LZ{dACQM}tz&SO;EdLP7j^oCdxdt?f|S#?0l9y>n%$bO zAyQU@@xoYaaGdrS$2YySoKGg-P-~F!E40_?!ae(H8|o0hpQgDWe2qrhI(u1Sf;F+Rd37N~C}0PbTeum+98h zyA7i&&Z2y5wV;S!mxc^$4m${e>gVR^O=gXd`&bykf~2|)8Fnsp)F6KCIc<>sq??%= z=T5yF+yxD!u(k(<{dVe4Xt;AVWC6iy<)l^VnPGl-zJ?cq_q+-Z&JBldpOSApo5v5-ke0lEyU~0D06G+lHmiE-=+(r*(WM+H5Db3dR3kF0EX)R|n%9lwGYo4~kKBI#hiT}JX_>~IR>%H{9 z4zpDkp5YOwLou`FMJgh{lECe<;cLkIjaprvVfUR_wuJ~UB?#ZL=U?NrS@+*XG@JlK z7D{m5G}QBQg1hKDBD-r+ejxqBtGZBwW&-qH@7DD@abUbB1Nip`LV(&QHx9V@sC4Ye z2Iu3_Z${RHpU?{Y$ZN-z2{A zZF2G+Zl$ri7=(pRSPL<$+(KO>f#b#+O}UBr%S#g)Yr+Z_hLWggCiw$gjI|X93mv9+ z0{EULrG89hH<5v^y`nKm406E+W4_jv%ay4U0Fw4lcY~|)V|hTm3ymNiM%zH2kE=^> z!W+m3mZXt(f{9?mj7ZLwtV2aLy0N0p=dLE1ZY}j{8tsfK9n{oRImYFciD^m5jxC+i z)KHyE2*oZlv2;ySvooo*t*J4bvhS9I$O(v8PRw*p*&TOv#SwRy$c1~!5pD{lpo=%d zB@tr`^*GY$n@e4rby{rU%)7X3FxCrhFqtwgbNd1TyW-4OuCeQ=(gmYxM;y()xKLoV zA)?uqm*6|bJt}yD3OKIxdb zIfW8{{qFR5YNP-->N4{$JXmgv7dshh<&4;c&T`#OaQ5qjGsj_bv)aWz?17`yIHtpu zluF-Q(;E5JK4G;s_7+Gb$3TF|6kr|cP}CIZoI}>B3lKWDG;7Sj`4=STrW}ijUZcOm z7`rf85)baV3WcqACHu4Y`)z6Cn1RlQ(mi8_g%>BAkY&;~iY}pUh*+YMsaeH#gvMIBSNi6&LXH^o zxs~1Nyw>~_UPsMyT-%TWE$N+~=XB+Uv}!AK4hOCZBspvououCKW>)Mi<8+LxRK7sPV&VYX#MHBo$dyULa(B<@m{A9oKP8p*M0BEXCHN%nT(o zdIy1!V<;pNFMN2chhl@GGtNUmFr3DFg-^Ljo-`~@j(qQb@0s8Q9Y8aaN!oTeQ#{?z zb3nwlI0t*$=gdqijc*>EneORq>6qpr;h`R^smq-8y4*ovAuANk+=wF(oF|-swFUce zg0)s2wm&C}yx z#RfGOf2`*m=LI=T%nC9Nf-P2!?Vf!Nuv+{lF7j>QL@#-fIkj|ut}*<%=#g=a-a_)a zbRN|08c-nf+C)=QBOs*P+~JdB}@!+5xt?P&T3d8djC%)g0*l ztZ|es>?z)wM7%`0vpt&<&mmRt2f`DQGTcD9&m}>CC&f3^_`UZ$FF2407kave((LmL zX4YFp90P=lsGO){E1KWM5?v+(c=hEnfD2wB1Kb3z^w>)=QJPLKI3J#9u1fUC_J%q8 z>eAGS4dFFja5&em^KdV)0@Oz>fF$kfMKHOZG{tZ>SvPvN$%-BbZt?;q8*^(nnYbOX z>Gd|^Cn?C>?gcx^Ubu;F2=(ORR+n!dcX|k!sd|9ZVsB8Jqr>n@bY!Gm+*-WgQj)lb z$XGccq98uDge&)Q1=HEqcnqPE`nPJ?!x|GRQ%x&FNYvx{VBW;um%E zh!;p-`Z(FZNykR6)0UU3_7?7k6uK_iQZ!GAEx^Pxv6-G_5`vYsdQm%!XZ07tBuxxK z^CCgCT>`~(rZhTgf*u;{V29_@eRn3SDK!~6Eno!0ZpcD*Lp?Ug+tx0@Bsjv9M~uIC zy1~)IoX879^H(ppkIMR+=s4@(S(seDPGw!{NT>OCE+Mx})L9Uhj}UKo=zg2f4xw{@ z+q3-){0Hp_9$RqvWlb{gczpiGXN(Nel5LfjW9>}_>-)5OehdmR?nj@ef%%b_KJFo7glA)%rW34M64#4jNjttO+YrjYCA1fWZ@qDOo|CH$RJ_h57 zb1(%CBS(S6xbiT+wAwO!h^5eMDdZfif&=V8chJ4`K0jd1wDSo-#f!XWlUggRFuTJX zYS$(vYcxo-HVbQ^I$KXl7Fm6x#TL79adVu47+rweoPb7X3+ACy$y4NlP z2=!ALAd|1`8QM0oSMB4Xv-47NPx2Q+mc)#6EgwJg^;AbsOZYg&4-j?u@Iew1ZjAfb zk)=BRbE$0Fpk=2q@}=xZ$vxcp{67WUl7NX1@J0d8PQY@%!Sz$Xb1ax|OIMdRO&<_m<42F0MRQZ+67YnZV^6px z_JmtwPqKjD%<3WLm-_!?scL3T__Q1Vw^-PtW!WY=1DzLr9$cZ4 z+JUQ=*+In!o!&faC#Ok2S2VBt$z}L0ztF)iDZCHcVp2GC1Pfi!7++KyCV)MDVMl|- z-Rnm;{i6BaFRbO$Id){SXt;2nmP`IOzwACY!}KJAN8EAnO*!;8M>M ztFvA*9*~VL%VE*fWupsr(G1HLMnwEJWs}r1E?c;Z8Uu|9+2onwpse^io(Yl{f$8?* z-V@Z4l^aw~(EO~tK3EXCo)qY|W%2v~Nc`eZG)uC&)q%->6n`oxnjdAMbiR8n%c9Mc zmvZ@wpd+(-k<$%&4oOOx zZ82JHli|8-pNx%zMZMUuoF^!@CPgUc3kvmNQPvBJtxOThMS?=xS(J+fWpWPj{i2F+ zE)ksi1n1JM%S!dTZ1kb;N(?r?IP0TSb(LCh(#a<*`Ne6$iqlsevEuZT{`G{-kItDm zx8kYA*p1)3`E!a3OB1|We3C+zDGfcR*?G0J~VI E3stpxssI20 delta 9270 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 OS+RAEQt|}^B>xwSICF;p diff --git a/src/test/samples/golden/fileMgr-release.app b/src/test/samples/golden/fileMgr-release.app index 9368f59d6b2de7418d2785d4508e68df957da761..b54ab8a13a9b814cba6aaa6889abaffb69bcaf69 100644 GIT binary patch delta 4130 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 diff --git a/src/test/samples/golden/iconed-debug.app b/src/test/samples/golden/iconed-debug.app index acf605f4a562a509074c7d73573a9e3c2ffbf37e..9e2c3df57118c75a570e737b276af27a95dc97aa 100644 GIT binary patch delta 5111 zcmai1X>b(B74Dht(ZM1RAdrxRw2=@(<_aK(I04s+LzvUcj#l85IaX)@g=JUXUBH|p z#9_j|WaoWa7h`}a=9FD6{Rm$;II8K!fwqqMXPA9?bD@n!JMc#Y6)4L)+vZdOc z@9Wp^ef{3+?$PO;+R>!;?2=;pg+j*8v(%>=e*71KkT2>QV|pcE_xGT#b-}<(YT%%z zb!kk#t{WRzS3dLa!g+7#NsXqU!(vUoNmyh0ty{McUnZ`@fDj0&h(Cs4kX?p zEMP*)!P|rl7*FAM2@~5!;c7;Lyc~MfhIpDJPdFT1^9$;1=x3!{ilR0VZ0m1gYt9T zd1h#F*-dPf0d5;Si^jK1g2IglOZ5lsCf zC=8103t;QXL)?EHe-BgTzJ`yFa^n!w(I=F8H+AJ>95StA$M^yRZ8SQ<&C6)QQbe~Zz zYo8@k%>Q8qx;o(uU_NNN&tPTE@;o=EpfDzX$~CVNbU_LF8BXtl8vPtjPWW`?F!>9f z+m*}YFI}zAadQ-H3;8Rh@cFcAo5_nDV!n$zAWwQwz zH|gVeqsf1&hWHmZ9*2dbgZ-QPPob3mplVhA&sA=3a%X9EiTwq7g(C$#rVn?xM(c3m zmvp$m-N4By_SqA3S76)~#rh&JyaF$2i(vc${7NQZ#Vy$4ns9f8F5J_Hi*$7o%RN3jH=Lh5h_E(v!t(+d9xrT|WEyfSor2FXl3NvaV-AJCC*%Nk*vMks4hD%Ac`GrKQ!{;@nc63Od9IYt=! z+4dblIRRyx3t+?G4x1#P=@gnQG(CeFgjUGRPt%r-LMzFjIYNUY0mkadO+Y|gctfc6 zaM+lpfXjrC0MsP#Qo<9$7l1brCWFE`Qd46$3%q+cD->*qj%UY0k-kFZqAZQ_Tf#pX zr`<1d@Mu{qN};Puq15IfIPu^vTSA3M*Wt(L@|@Rrnt8!7Je;9Z&*~*`1qke$QMgh# zb4S$JtNyPa`E8NRWr2YxXZ7tmg7N`qkz7dXIzg8xok{D3e;Jx@6vkQX(!N+;_|UH1 zgsRdvRW-e*ZWiX|zBn$oppyXI+FOO=sVyD74QmBmAC!A&9=`0P622D`PA@;z9!&W@ z;Vh`Fv-b-~0lgVjK~lUP1BMO=_;0DuU_&%ac~YhPU{;k1F#f|?H3-VX!gvPdH!*|bW;q2D zRnb_&A4;qm?CJ?vq1JmN;SJ`7c*0uKFHPI=CTmUm^tPDCwZlayHwdOh*G3X58OWI6 zyBE>e7HRE8pqN3~9%scHqv2L7*2~Il+h}LfjGhD0^_fB-foUsp9pQTBM`LS3;kJ#L zY9Q+m#jGrmNX?G6MN~8-Owgo~h{UXTAZkYwDnlu*p(P#-uT5m+r|4Occw4LGrY%km zt@d92=7$ncy@9S;Xs3h~n-@wfU!`pBnG?2Mp|WnXlfm#m1t!*pXtX3rx42ZLVR93jjpeHiIk?6fakioUZl_z4Tw=!F+ z5t}5@%hl{?%e0sk%Bm|c4Xt*o8C@G&o(TzTU~L2(sL}#6&x*%GD|;C=MB?kMnCfwK zAGSg<%!0~0lA*|QE1d0X+S(ZQYI>&K9tSI0(e!8}Qy`b5Ln3>+-vYIfgsLPMQijMd zTMn`4Qxye$Li#-H9xL2FExM*1@_7FG$Pxo?rSo5o2?*_J$3J1Texbk_pIhX7G$F@1 zJYkUjYZn^MZzdG!zjNVG{i^$DIKw9v={GXa1b8z8J0})t`OeD|Yn#jQH94oW7B6$4bB8rWjXjC h@bGUq%chPkfAwy7$Dn^dz@T|i;LHrh@OHw_e*uCv@6P}L delta 3808 zcmai1dvH|c6+hqk_RE7!Ac-*>l1;KEBq1QgFnJaOHwc7>*6Ut22@io_>!YpIK5VCt znVUqNTHEQO*D7|aVp~P=flR9?4aSsB3s!(LGc%0$2}Z!FsVTSNQ1Q)SG!4C>9X8VHo3!VoES-$^}5TZoL3Vcs+2@6QX z8NsE@A7h8#7d)T&f*%&VfUx5*{}I7ugvkTpM+KKN7Wo5$D|kW7_w8dsxP%EJ@B_h> zoB=WLL&29a7XFU}S8)cW*umq1V}On$@=pk^X6!Kkpx}jo2M!!y{G{L-!0G8}h^3T$ zN(i-Na9Ru=5*()*&5(eyPYbRiJS`5M5nNC5QzHMY;0EGPF+XLGs^IX#SnV-xmGXQW zDHx_^ss(D27gTU|;bpsCQiYi!NZBbxoimz2us_RwX|S&<YOQD~Wt1FJ%fQRFQp`iRB8?8d|BbX%Y0?%2xg$8~_R+$D!r*Fy~BYs6VK#?QA?Z zE6D~(tc%6|7>cb5_SPVe>fc+27sL!7hlkr38gn5Y>Q z9Er!M>!=u5MCKOZJbE@p6-B8qn2uLQ5y`b-#r|L>UKiCNULCa7C43X&oTqt0xJo>w z3#1^?kd%`tLocPt6WDU}jz#3F0=@Hwzfv}aG@hslZ8e&;T&yE26J{MGsG#)4jD?sk z1`-+=ND>5F!%HyhNu(G=AdcxGNJg1ghh`8?ahh#|1hRo*+sN%Wp0dpt6xKI)wxIhh z7FdjF8e8;5y~!PF+fdr79KkJSycB6ogzPd5cGq9pw;a>b*=33=7(;snJKmIyssjsX zv)C2rU0y)SU^~S>w`+g!Tzx#+72;LFoAqtc?m4))p)J}o2lqF$M|33F1vS<%^bvlC~cx3czfA30yj%e%+)aNj*<{5GdZuaR%EWp ztE|hZ_?$=canf0y)1iS$IthB(K@qw(ud^YiBPuSZ{#p)L^=N+P+W4(I@EOkqLmHVThZ(1 zz`*WBl?Vg+Y#7jOtj;z1oGcPD1zEbwGZ|yNE`b! z?}X%h_ViW3UIzuCXeW(JGB0P&1ez^#l1bfR?~vBWCR1u~C%UaH`;9QfyYq2;GtAUI z`8ZO(-6uPhvV8BXqx;aLE^-uH?C5@UIW_h{G(%v9;UtGEWd$np9=o5*OsmTE#zW}J z>M~fPAQG<5KN#kJM2;68as$bFw)`ZTtxQXpZIB=`cTkai3SAnbV4n^Zx5Jn;)-xex z2aJ*P0rCXiGMZ9KZQ7WWeta&J>E^~wJDN{4dpK-}=h0kG3&}YAz$YSnLqGPm;6@M(lh0X>oM!C;+%2~mFZ;asx3BFC2v=QPTqqjZI zdEUb<{*$o9Uk(fRN?17is??2KyoSEn1!gx_fKDJ8#nd}{?9bWMYz2OSG8gQxcyC3# zLo^I2FWFT88qMhcruqcB?YxNJvLO!Xb?R)Ih9|>#PDwib-00aOCTM@XnUC$Qd~>~x zXUq+dkhtEtFn}s{@jEo1m`(fd(OtvT)A=(ZyEEQpNX5_2@iS;X$;h7dIX)`*$#S6k z8pehYjt;PDmNvQM-^eD9B+N~aAj9-= zvy8e1i7+2vf&wg=jqoxZ47W7FAE}(DI z<}yjs>E%=C;#2Iv=q7#um=Pbb5 Op^@Zq2Y{2$zyAUhrT8WQ diff --git a/src/test/samples/golden/iconed-release.app b/src/test/samples/golden/iconed-release.app index c0fac3770d610d6f919d6ee825359e823074e5ac..167d6ce10b7706dd92127e1b0f19e1f52b8bbf7f 100644 GIT binary patch delta 2666 zcmaJ?ZERCz6h8N|%UZUvjqkPVTDHl@#B~TnBA|2|(`{@t?FJYP$=&(_jorQF-aD`Z zEh`@)Uj<$j75VU61QA?<8vmH6KS+QWqcPDRCPx3M|M+EMJm+ouwhf6l>Fs%+`@GLN z?{m&=PwhIe_xiT^v;L_8n1bBZf?wf#JUM?!WeU!@A^h#s#rbnoZ32#Kd->-LK5ZXH z&-0%fJo8?l2&PYlt8tS8prys58J>r<{TNO1wVpL1%J6-jei0quw>_bJG|RhMLiB)R3KMmr=nluU-{SJo`3yKc=rVw z-LFfvY+&wcFLI+lQq^Q7j#ji^qeXO;_!>sAUV$vyNi(RuQL@>SJ_p*HaEFqqB59QZ zBiMi3vpz!G!&P|mC{PhixQQhOhN?PG?aC;aHWXPvx)%rtXcr+swx!^REBrg+v>^T^ z*pJ$JVfhy9$26gpC=Jm|=#8TE^+wuCHteNw!8ysOuDl`I+d?hHpIJC8G~eMj7e4CG zP1n1iy+;SZ!Oa~6@E}wOuWfFseZRaozP8zy&bR&p@n}-mBnn5fejU+1ggZ5`xI_$l zToLWWiNs1Hs0`ZWTsd^o6}T>pGC77Z%7mvA?IQ|t1D^NJW_Sw3fPt$Lq2GqJ6a0tf zTMhYIu0S2W7y<1n1nMd%_?X{p>HMFk^{qkW8V|MJ?*D`iKLiSm8NS!lbj*DXlo_xt zAa1xOL#|_kN-^z5?)}1NDtvNPo{TdEA1j-E&hOtQL3W+;_p66rKe zN1$?Fuf4^JJNihXNZM!`I|lVJX3K5Sou=i4b!X??LIDw`$2J*>R5WEfY;0y8PTNCl zEZH5mstPN+iW4ol4W{>x`W-DRj3vl?=WjXcSd*0Iy!ZmkvG!d^oHjghSH`X zr3dt988$=?QiK_}qq-ow!LrPvjnqA=MP`k#WI`WhG<7n`W;85DX{D0_p{Yp|EFBQA z7#)IT)l?BBkwBrFvgRk46LK6&uA{lN+%RCq;@jgaCd)*Ez|sgx7QJLR6UD*_D1Si+ zp|jaiNg!gjimz4=4T)1G)n|@$SxhhKDMT{ zB2hRbh^6HSlr&{iNE7X=f+txb*=3F;(Z{I;i@vW=t6}P|wx_2lCRc)U`L_0%{B*lV z>EKt}TltmtD*j8mhkw^zbtdRhR&sB!m3x9!%2gyc@W(OwjQ0nZs9UP}#-QI#zm_U= zEx*Oj2Nx-K`8UC(%7grGBwViH%PZ&bWgWH37kq8UO6A_vu8yB7)yJB6-|Du;ldAw$ U(LV`Z-J7>YzFq@>|Hb0^4axn6&9m{7meqZMCiO8}soKjXJZL zg$kP1h3G&F7o3F3AgGVk%1`j!e8t^^@-w|KR_PLpx?U3TD|h$&YR>ckfjWByN! zpnNhDCMN+PSE-{)<44&=s2jVDE`<_!%1A4;8lM;`AI*t&D%69UqY;IYcr?0Fg-YSI zXzN@KnuI*)Iacpgn*T=o+ZZLFMnH`QS|tTnOM&(P^F?0j)ASx3j5ReeeMY8_WuQ55a__nQtxaJgIY*=z^10ilNE2dcy1 z6;Pmd@(jw{ENA2NHjvxlDF@S3(2#%;x_|OXdhDJF(P9V)WG8~mQUk+T4f1v61%*v{ z3s`pt8*I4~Y;Kzi=k?4xvh#-dyWk9I4^MA*!x;*ba!DGpe^PIR*wF8_&2s4AaiKZM zYr5YMxkstFxO-tvY3{|_3s+gC=^6sLj|-u}EfoS(2q%KojSbbq|4$CP8_m=H)sLv7 zN!2EmI5q2~90i*yAEAp4WP520DLs$mK#rAi;PM>WPtPy6i7@$Q%9*yGAvsWyPivtL z)Kq~^mNz*XL|g32GJrU@o1Qh>2EBhe0BB0p^9Aj9^UVMN diff --git a/src/test/samples/golden/inc-fileops-debug.app b/src/test/samples/golden/inc-fileops-debug.app index 34dd03cac116db8663e472c5f84d4ce8baeda1e2..254167c067b6c36eec40331e65a4e2dcfc87bb1c 100644 GIT binary patch delta 73 zcmca?`OI>GIHT)kMsYVLVL=AB%$!vJg5rS0k_^AZlFYKy&F+#e%#13Ndt|g_gk%^P PSQsF{B`hLH42cf_^jQzH delta 53 wcmaE6dD(J-IHSvEMsYW$%~6sr%#4DQ=gVlz{Fi26U}1m&m#~N+W*Gz@0B)5D)Bpeg diff --git a/src/test/samples/golden/inc-fileops-release.app b/src/test/samples/golden/inc-fileops-release.app index 95eb2cc1c3c0cd6f8ac2efc4c01e619ca0a0cda2..05089d966cce82a79f6bfc130a7a1dc171440902 100644 GIT binary patch delta 63 zcmbPlxyf>YIHT)kMsYVLVL=AB%$!vJg5rS0k_^AZlFYKy&F+#8%n~ZB3}E0A77-*Q L!@$4{;W7XKDy$8v delta 43 rcmdmFIp1=DIHSvEMsYW$%~6sL%o2jE3}E0A77_Gcnt_2C!esye;HwAA diff --git a/src/test/samples/golden/terminal-debug.app b/src/test/samples/golden/terminal-debug.app index 654bf72e5743d89226c23a333b1719885dbf1679..5e5423e08288c4447cbbed85854ad97f4d497f51 100644 GIT binary patch delta 13376 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 x7=Df4KL^gq86Bq@r-8PGY3+lMUec$4xQH&bY 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 delta 6999 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 diff --git a/src/test/shell/progmanTest.c b/src/test/shell/progmanTest.c index a1db2e43..4127dd40 100644 --- a/src/test/shell/progmanTest.c +++ b/src/test/shell/progmanTest.c @@ -1049,9 +1049,9 @@ static void caseProgScan(void) { TEST_EQ(count, APP_COUNT_TREE - 1); for (int32_t i = 0; i < count; i++) { - TEST_TRUE(!shellPathsMatch(list[i].path, APP_PROGMAN)); + TEST_TRUE(!platformPathSame(list[i].path, APP_PROGMAN)); - if (shellPathsMatch(list[i].path, APP_DVXDEMO)) { + if (platformPathSame(list[i].path, APP_DVXDEMO)) { demo = true; TEST_STR_EQ(list[i].name, "DVX Demo"); TEST_TRUE(list[i].description[0] != '\0'); diff --git a/src/test/unit/testUtil.c b/src/test/unit/testUtil.c index 35753ea1..0cd1cc43 100644 --- a/src/test/unit/testUtil.c +++ b/src/test/unit/testUtil.c @@ -436,9 +436,55 @@ static void testPaths(void) { TEST_PTR_NULL(platformPathDirEnd("c.txt")); TEST_PTR_NULL(platformPathDirEnd("")); - // On a POSIX host a backslash is an ordinary character. - TEST_STR_EQ(platformPathBaseName("a\\b"), "a\\b"); + // Both separators separate on every platform (DVX's path syntax); the + // native form on the host is '/'. A drive also ends a directory part + // for the base name. + TEST_STR_EQ(platformPathBaseName("a\\b"), "b"); + TEST_STR_EQ(platformPathBaseName("C:A.TXT"), "A.TXT"); + TEST_STR_EQ(platformPathBaseName("C:\\A.TXT"), "A.TXT"); TEST_STR_EQ(DVX_PATH_SEP, "/"); + TEST_TRUE(platformPathIsSep('/')); + TEST_TRUE(platformPathIsSep('\\')); + TEST_TRUE(!platformPathIsSep(':')); + + char native[UTIL_SMALL_BUF] = "/a\\b/c"; + + TEST_STR_EQ(platformPathNative(native), "/a/b/c"); + TEST_PTR_NULL(platformPathNative(NULL)); + + // Same / under: DOS rules everywhere -- separators alike, a trailing + // one ignored, case ignored + TEST_TRUE(platformPathSame("C:\\A\\", "C:/A")); + TEST_TRUE(!platformPathSame("C:/A", "C:/AB")); + TEST_TRUE(platformPathSame("/a", "/A")); + TEST_TRUE(!platformPathSame(NULL, "/a")); + TEST_STR_EQ(platformPathIsUnder("C:\\A\\B.TXT", "C:/A/"), "B.TXT"); + TEST_STR_EQ(platformPathIsUnder("/a", "/a"), ""); + TEST_STR_EQ(platformPathIsUnder("/a/b", "/"), "a/b"); + TEST_PTR_NULL(platformPathIsUnder("/ab", "/a")); + TEST_PTR_NULL(platformPathIsUnder("/a", "/a/b")); + TEST_PTR_NULL(platformPathIsUnder(NULL, "/a")); + + char joined[UTIL_SMALL_BUF]; + + TEST_TRUE(platformPathJoin(joined, sizeof(joined), "C:\\", "A\\B")); + TEST_STR_EQ(joined, "C:/A/B"); + TEST_TRUE(platformPathJoin(joined, sizeof(joined), "dir", "\\x")); + TEST_STR_EQ(joined, "/x"); + TEST_TRUE(!platformPathJoin(joined, sizeof(joined), "a-long-folder", "name")); + + // Tidy: one separator per run (a leading pair kept), no trailing one + // but a root's + char tidy[UTIL_SMALL_BUF] = "C://a\\\\b/"; + + TEST_STR_EQ(platformPathTidy(tidy), "C:/a/b"); + snprintf(tidy, sizeof(tidy), "%s", "C:\\"); + TEST_STR_EQ(platformPathTidy(tidy), "C:/"); + snprintf(tidy, sizeof(tidy), "%s", "\\\\srv\\x\\"); + TEST_STR_EQ(platformPathTidy(tidy), "//srv/x"); + snprintf(tidy, sizeof(tidy), "%s", "/"); + TEST_STR_EQ(platformPathTidy(tidy), "/"); + TEST_PTR_NULL(platformPathTidy(NULL)); TEST_STR_EQ(platformLineEnding(), "\n"); TEST_STR_EQ(DVX_SYM("wgtRegister"), "wgtRegister"); diff --git a/src/tools/dvxhlpc.c b/src/tools/dvxhlpc.c index 35a4a927..c6718113 100644 --- a/src/tools/dvxhlpc.c +++ b/src/tools/dvxhlpc.c @@ -131,8 +131,8 @@ typedef struct { } IndexEntryT; typedef struct { - char name[DVX_MAX_PATH]; // filename as written in .image - char path[520]; // imageDir/name + char name[DVX_MAX_PATH]; // filename as written in .image + char path[DVX_MAX_PATH * 2]; // imageDir/name int32_t poolOffset; int32_t fileSize; } ImageRefT; @@ -360,7 +360,7 @@ static void addImageRef(const char *filename) { } ImageRefT *img = &imageRefs[imageCount++]; snprintf(img->name, sizeof(img->name), "%s", filename); - snprintf(img->path, sizeof(img->path), "%s/%s", imageDir, filename); + platformPathJoin(img->path, sizeof(img->path), imageDir, filename); img->poolOffset = 0; img->fileSize = 0; } @@ -1058,11 +1058,11 @@ static void fatalOom(void) { } -// Look an image up by the filename exactly as written in .image (which -// may include a subdirectory). +// Look an image up by the filename as written in .image (which may +// include a subdirectory: '/' and '\\' alike, case ignored). static int32_t findImage(const char *filename) { for (int32_t i = 0; i < imageCount; i++) { - if (strcmp(imageRefs[i].name, filename) == 0) { + if (platformPathSame(imageRefs[i].name, filename)) { return i; } }