From 3c7b93bf7ae748f89c3df3903bc228e5200e17b5 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Tue, 20 Oct 2026 17:24:15 -0500 Subject: [PATCH] Minor paint improvements. Lazy loading of libraries. --- Makefile | 8 +- analyze.sh | 22 + config/README.md | 4 +- cppcheck.sh | 24 + docs/dvx_basic_reference.html | 461 +++- docs/dvx_system_reference.html | 1755 ++++++++++------ dosbox-staging-overrides.conf | 22 + dosbox-x-overrides.conf | 22 + screenshots/86box/parpm-parpm.png | 4 +- screenshots/86box/pkgsign-ps-plan.png | 3 + screenshots/86box/upgdrop-ud-sw.png | 4 +- screenshots/dosbox/cpanel-cp-types.png | 4 +- screenshots/dosbox/hang-ended.png | 4 +- screenshots/dosbox/hang-taskmgr.png | 4 +- screenshots/dosbox/instadd-add-sw.png | 4 +- screenshots/dosbox/instadd-su-what.png | 4 +- screenshots/dosbox/menus-m-design.png | 4 +- screenshots/dosbox/parpm-parpm.png | 4 +- screenshots/dosbox/pkgsign-ps-plan.png | 3 + screenshots/dosbox/player-p-wav.png | 4 +- screenshots/dosbox/sdk-sdktbx.png | 4 +- screenshots/dosbox/shellcmd-sc-back.png | 3 + screenshots/dosbox/sql-sqlsmk.png | 4 +- screenshots/dosbox/ucide-ucide-ds.png | 4 +- screenshots/dosbox/ucide-ucide-tb.png | 4 +- screenshots/dosbox/upgdrop-ud-sw.png | 4 +- server/dvxserver.lua | 22 + server/lib/packages.lua | 22 + server/test/account.lua | 22 + server/test/run.sh | 22 + server/test/serve.sh | 22 + server/test/testPackages.lua | 22 + server/test/testReview.lua | 22 + server/tools/review.lua | 22 + src/apps/kpunch/basdemo/basdemo.dhs | 42 +- src/apps/kpunch/basdemo/shape.cls | 24 + src/apps/kpunch/cpanel/cpSounds.c | 64 +- src/apps/kpunch/cpanel/cpanel.dhs | 42 +- src/apps/kpunch/cpanel/cpanel.res | 22 + src/apps/kpunch/dvxbasic/Makefile | 29 +- src/apps/kpunch/dvxbasic/basBuild.c | 123 +- src/apps/kpunch/dvxbasic/basBuild.h | 6 +- src/apps/kpunch/dvxbasic/basdev/basDebug.c | 67 +- src/apps/kpunch/dvxbasic/basdev/basDebug.h | 19 +- src/apps/kpunch/dvxbasic/basdev/basDevBas.c | 75 +- src/apps/kpunch/dvxbasic/basdev/basHelpMap.c | 10 + src/apps/kpunch/dvxbasic/basdev/basdev.dhs | 151 +- .../dvxbasic/{formrt => basdev}/frmModel.c | 113 +- .../dvxbasic/{formrt => basdev}/frmModel.h | 20 +- .../dvxbasic/{formrt => basdev}/frmModelBas.c | 91 +- src/apps/kpunch/dvxbasic/basrt.dhs | 140 -- src/apps/kpunch/dvxbasic/compiler/basEvents.h | 4 +- src/apps/kpunch/dvxbasic/compiler/codegen.c | 9 +- src/apps/kpunch/dvxbasic/compiler/compiler.c | 28 +- src/apps/kpunch/dvxbasic/compiler/compiler.h | 5 + src/apps/kpunch/dvxbasic/compiler/front.h | 3 + src/apps/kpunch/dvxbasic/compiler/opcodes.h | 223 +- .../kpunch/dvxbasic/compiler/resolveExpr.c | 5 + src/apps/kpunch/dvxbasic/compiler/scope.c | 5 +- src/apps/kpunch/dvxbasic/ctrlover.dhs | 6 +- src/apps/kpunch/dvxbasic/dvxbasic.res | 22 + src/apps/kpunch/dvxbasic/form.dhs | 12 +- src/apps/kpunch/dvxbasic/formrt/basRunHost.c | 18 +- src/apps/kpunch/dvxbasic/formrt/basRunHost.h | 12 +- src/apps/kpunch/dvxbasic/formrt/formCtl.c | 194 +- src/apps/kpunch/dvxbasic/formrt/formDesign.c | 7 +- src/apps/kpunch/dvxbasic/formrt/formGfx.c | 258 ++- src/apps/kpunch/dvxbasic/formrt/formPrinter.c | 57 +- src/apps/kpunch/dvxbasic/formrt/formrt.c | 1865 ++++++++++++----- src/apps/kpunch/dvxbasic/formrt/formrt.h | 159 +- src/apps/kpunch/dvxbasic/ide/ideDesigner.c | 24 +- src/apps/kpunch/dvxbasic/ide/ideDesigner.h | 2 +- src/apps/kpunch/dvxbasic/ide/ideMain.c | 486 +++-- src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c | 2 +- src/apps/kpunch/dvxbasic/ide/idePackage.c | 38 +- src/apps/kpunch/dvxbasic/ide/idePackage.h | 10 +- src/apps/kpunch/dvxbasic/ide/ideProperties.c | 21 +- src/apps/kpunch/dvxbasic/ide/ideToolbox.c | 6 - src/apps/kpunch/dvxbasic/ideguide.dhs | 2 +- src/apps/kpunch/dvxbasic/langref.dhs | 297 ++- src/apps/kpunch/dvxbasic/runtime/basErrors.h | 1 + src/apps/kpunch/dvxbasic/runtime/gfx.c | 86 +- src/apps/kpunch/dvxbasic/runtime/gfx.h | 6 + src/apps/kpunch/dvxbasic/runtime/screen.c | 20 +- src/apps/kpunch/dvxbasic/runtime/values.c | 5 + src/apps/kpunch/dvxbasic/runtime/values.h | 9 +- src/apps/kpunch/dvxbasic/runtime/vm.c | 569 ++++- src/apps/kpunch/dvxbasic/runtime/vm.h | 144 +- src/apps/kpunch/dvxbasic/stub/bascomp.c | 2 +- src/apps/kpunch/dvxbasic/stub/basstub.c | 17 +- src/apps/kpunch/dvxbasic/test_compiler.c | 60 +- src/apps/kpunch/dvxbasic/test_suite.c | 307 ++- src/apps/kpunch/dvxdemo/dvxdemo.c | 11 +- src/apps/kpunch/dvxdemo/dvxdemo.dhs | 42 +- src/apps/kpunch/dvxdemo/dvxdemo.res | 22 + src/apps/kpunch/dvxhelp/dvxhelp.res | 22 + src/apps/kpunch/fileMgr/fileMgr.bas | 56 +- src/apps/kpunch/fileMgr/fileMgr.dhs | 2 +- src/apps/kpunch/fileMgr/fileMgr.frm | 83 +- src/apps/kpunch/fileMgr/fmProps.frm | 18 +- src/apps/kpunch/helpedit/helpedit.dhs | 42 +- src/apps/kpunch/helpedit/helpedit.frm | 142 +- src/apps/kpunch/notepad/notepad.frm | 27 +- src/apps/kpunch/pkgbuild/pbProg.frm | 70 + src/apps/kpunch/pkgbuild/pkgbuild.bas | 55 +- src/apps/kpunch/pkgbuild/pkgbuild.dbp | 3 +- src/apps/kpunch/pkgbuild/pkgbuild.dhs | 4 +- src/apps/kpunch/pkgbuild/pkgbuild.frm | 6 +- src/apps/kpunch/player/player.frm | 11 +- src/apps/kpunch/progman/pmView.c | 72 +- src/apps/kpunch/progman/progman.c | 30 +- src/apps/kpunch/progman/progman.dhs | 44 +- src/apps/kpunch/progman/userHelp.dhs | 2 + src/apps/kpunch/resedit/resedit.dhs | 42 +- src/apps/kpunch/software/software.bas | 48 +- src/apps/kpunch/terminal/terminal.dhs | 5 +- src/apps/kpunch/terminal/terminal.frm | 15 +- src/apps/kpunch/widshow/widshow.dhs | 42 +- src/common.mk | 7 + src/include/basic/comm.bas | 28 +- src/include/basic/conn.bas | 13 +- src/include/basic/designer.bas | 4 + src/include/basic/fileops.bas | 27 +- src/include/basic/game.bas | 13 +- src/include/basic/help.bas | 12 +- src/include/basic/http.bas | 6 +- src/include/basic/joystick.bas | 23 + src/include/basic/music.bas | 9 + src/include/basic/net.bas | 52 +- src/include/basic/pkg.bas | 124 +- src/include/basic/pkgnet.bas | 9 +- src/include/basic/print.bas | 15 +- src/include/basic/resource.bas | 5 +- src/include/basic/security.bas | 105 +- src/include/basic/shell.bas | 35 +- src/include/basic/sound.bas | 149 +- src/include/basic/sql.bas | 7 +- src/include/basic/system.bas | 3 +- src/include/basic/xfer.bas | 18 +- src/installer/Makefile | 8 +- src/installer/pkgBuild.c | 133 +- src/installer/setup.c | 16 +- src/installer/setupCore.c | 4 +- src/libs/kpunch/audio/README.md | 39 + src/libs/kpunch/audio/audio.c | 225 +- src/libs/kpunch/audio/audio.dhs | 37 +- src/libs/kpunch/audio/audio.h | 242 ++- src/libs/kpunch/audio/audioBas.c | 507 ++++- src/libs/kpunch/audio/audioDesk.c | 100 +- src/libs/kpunch/audio/audioDrv.h | 28 + src/libs/kpunch/audio/audioInt.h | 26 +- src/libs/kpunch/audio/audioLoad.c | 387 ++-- src/libs/kpunch/audio/audioMix.c | 22 +- src/libs/kpunch/audio/audioRec.c | 39 +- src/libs/kpunch/audio/audioWt.c | 65 +- src/libs/kpunch/conn/Makefile | 4 +- src/libs/kpunch/conn/conn.c | 193 +- src/libs/kpunch/conn/conn.dep | 2 +- src/libs/kpunch/conn/conn.dhs | 5 +- src/libs/kpunch/conn/conn.h | 6 +- src/libs/kpunch/conn/connBas.c | 93 +- src/libs/kpunch/crypto/Makefile | 80 + src/libs/kpunch/crypto/README.md | 42 + src/libs/kpunch/crypto/crypto.dep | 1 + src/libs/kpunch/crypto/crypto.dhs | 261 +++ .../{serial/security => crypto}/secBas.c | 370 +++- src/libs/kpunch/crypto/secCurve.c | 141 ++ src/libs/kpunch/crypto/secCurve.h | 88 + .../{serial/security => crypto}/secHash.c | 9 +- .../{serial/security => crypto}/secHash.h | 0 src/libs/kpunch/crypto/secSign.c | 53 + .../{serial/security => crypto}/security.c | 276 ++- .../{serial/security => crypto}/security.h | 17 +- .../thirdparty/monocypher/AUTHORS.md | 0 .../thirdparty/monocypher/DVX-VERSION.md | 54 + .../thirdparty/monocypher/LICENCE.md | 0 .../monocypher/dvx-x25519-step.patch | 95 + .../thirdparty/monocypher/src/monocypher.c | 60 + .../thirdparty/monocypher/src/monocypher.h | 17 + .../src/optional/monocypher-ed25519.c | 0 .../src/optional/monocypher-ed25519.h | 0 src/libs/kpunch/dvxnet/Makefile | 17 +- src/libs/kpunch/dvxnet/dvxNet.h | 41 + src/libs/kpunch/dvxnet/dvxnet.dep | 1 + src/libs/kpunch/dvxnet/dvxnet.dhs | 31 +- src/libs/kpunch/dvxnet/netBas.c | 447 +++- src/libs/kpunch/dvxnet/netInt.h | 22 +- src/libs/kpunch/dvxnet/netProto.c | 1 - src/libs/kpunch/dvxnet/netResolve.c | 159 +- src/libs/kpunch/dvxnet/netSock.c | 13 +- src/libs/kpunch/dvxnet/netStack.c | 31 +- src/libs/kpunch/dvxnet/port/lwipopts.h | 49 +- src/libs/kpunch/dvxshell/Makefile | 2 +- src/libs/kpunch/dvxshell/README.md | 11 + src/libs/kpunch/dvxshell/dvxshell.dhs | 29 +- src/libs/kpunch/dvxshell/pmModel.h | 2 +- src/libs/kpunch/dvxshell/shellApp.c | 154 +- src/libs/kpunch/dvxshell/shellApp.h | 47 +- src/libs/kpunch/dvxshell/shellBas.c | 139 +- src/libs/kpunch/dvxshell/shellMain.c | 114 +- src/libs/kpunch/dvxshell/shellProg.c | 277 ++- src/libs/kpunch/dvxshell/shellProg.h | 18 +- src/libs/kpunch/dvxshell/shellProgCache.c | 674 ++++++ src/libs/kpunch/dvxshell/shellProgCache.h | 92 + src/libs/kpunch/dvxshell/shellRun.c | 135 +- src/libs/kpunch/dvxshell/shellRun.h | 11 + src/libs/kpunch/fileops/README.md | 26 + src/libs/kpunch/fileops/fileOps.c | 373 ++-- src/libs/kpunch/fileops/fileOps.h | 30 +- src/libs/kpunch/fileops/fileOpsBas.c | 160 +- src/libs/kpunch/fileops/fileOpsSys.c | 259 ++- src/libs/kpunch/fileops/fileOpsSys.h | 22 +- src/libs/kpunch/fileops/fileops.dhs | 13 +- src/libs/kpunch/game/README.md | 23 + src/libs/kpunch/game/game.c | 6 +- src/libs/kpunch/game/game.h | 3 +- src/libs/kpunch/hlpfile/README.md | 23 + src/libs/kpunch/hlpfile/hlpFile.c | 70 +- src/libs/kpunch/hlpfile/hlpFile.h | 9 +- src/libs/kpunch/http/Makefile | 2 +- src/libs/kpunch/http/http.c | 473 +++-- src/libs/kpunch/http/http.h | 5 + src/libs/kpunch/http/httpBas.c | 10 + src/libs/kpunch/http/httpBas.h | 1 + src/libs/kpunch/joystick/README.md | 27 + src/libs/kpunch/joystick/joyBas.c | 90 +- src/libs/kpunch/joystick/joystick.dhs | 2 +- src/libs/kpunch/joystick/joystick.h | 14 +- src/libs/kpunch/json/json.c | 164 +- src/libs/kpunch/json/json.dhs | 4 +- src/libs/kpunch/json/json.h | 8 +- src/libs/kpunch/libdvx/Makefile | 9 +- src/libs/kpunch/libdvx/README.md | 6 +- src/libs/kpunch/libdvx/apiref.dhs | 222 +- src/libs/kpunch/libdvx/arch.dhs | 103 +- src/libs/kpunch/libdvx/dvxApp.c | 1287 ++++++++---- src/libs/kpunch/libdvx/dvxApp.h | 93 +- src/libs/kpunch/libdvx/dvxBas.c | 47 +- src/libs/kpunch/libdvx/dvxBas.h | 8 + src/libs/kpunch/libdvx/dvxDialog.c | 9 +- src/libs/kpunch/libdvx/dvxDraw.c | 245 ++- src/libs/kpunch/libdvx/dvxDraw.h | 41 + src/libs/kpunch/libdvx/dvxDriver.c | 2 +- src/libs/kpunch/libdvx/dvxEndian.h | 83 + src/libs/kpunch/libdvx/dvxFontFmt.c | 15 + src/libs/kpunch/libdvx/dvxFontFmt.h | 7 + src/libs/kpunch/libdvx/dvxHash.h | 103 + src/libs/kpunch/libdvx/dvxImage.c | 73 +- src/libs/kpunch/libdvx/dvxImgInt.h | 60 + src/libs/kpunch/libdvx/dvxMem.h | 11 + src/libs/kpunch/libdvx/dvxPrefs.c | 213 +- src/libs/kpunch/libdvx/dvxPrefs.h | 7 + src/libs/kpunch/libdvx/dvxRes.h | 5 + .../libdvx/{dvxImageWrite.c => dvxResInt.h} | 33 +- src/libs/kpunch/libdvx/dvxResource.c | 246 ++- src/libs/kpunch/libdvx/dvxResourceBas.c | 237 +++ src/libs/kpunch/libdvx/dvxText.c | 139 +- src/libs/kpunch/libdvx/dvxText.h | 7 + src/libs/kpunch/libdvx/dvxTypes.h | 4 + src/libs/kpunch/libdvx/dvxWgt.h | 22 +- src/libs/kpunch/libdvx/dvxWgtP.h | 14 +- src/libs/kpunch/libdvx/dvxWm.c | 137 +- src/libs/kpunch/libdvx/games.dhs | 1 + src/libs/kpunch/libdvx/platform/dvxMemTrack.c | 12 + src/libs/kpunch/libdvx/platform/dvxPlat.h | 96 +- src/libs/kpunch/libdvx/platform/dvxPlatHost.h | 9 + src/libs/kpunch/libdvx/platform/dvxPlatInt.h | 44 +- .../kpunch/libdvx/platform/dvxPlatformDos.c | 323 ++- .../kpunch/libdvx/platform/dvxPlatformHost.c | 97 +- .../kpunch/libdvx/platform/dvxPlatformIrq.c | 6 + .../kpunch/libdvx/platform/dvxPlatformUtil.c | 418 +++- src/libs/kpunch/libdvx/sdkGuide.dhs | 6 +- src/libs/kpunch/libdvx/widgetCore.c | 74 +- src/libs/kpunch/libdvx/widgetEvent.c | 17 +- src/libs/kpunch/libdvx/widgetIface.c | 20 +- src/libs/kpunch/libdvx/widgetOps.c | 89 +- src/libs/kpunch/listhelp/listHelp.c | 39 + src/libs/kpunch/listhelp/listHelp.h | 4 + src/libs/kpunch/listhelp/listhelp.dhs | 4 +- src/libs/kpunch/music/Makefile | 45 +- src/libs/kpunch/music/README.md | 49 + src/libs/kpunch/music/music.c | 296 ++- src/libs/kpunch/music/music.dhs | 12 +- src/libs/kpunch/music/music.h | 11 +- src/libs/kpunch/music/musicBas.c | 87 +- src/libs/kpunch/music/musicInt.h | 89 +- src/libs/kpunch/music/musicMml.c | 45 +- src/libs/kpunch/music/musicMmlPlay.c | 5 +- src/libs/kpunch/music/musicModHw.c | 43 +- src/libs/kpunch/music/musicOp2.c | 6 +- src/libs/kpunch/music/musicPat.c | 34 +- src/libs/kpunch/music/musicSf2.c | 109 +- src/libs/kpunch/music/musicSmf.c | 22 +- src/libs/kpunch/music/musicSound.c | 159 +- src/libs/kpunch/music/musicWaveLoad.c | 80 +- src/libs/kpunch/music/musicWaveSoft.c | 2 + src/libs/kpunch/music/musicXmp.c | 131 +- src/libs/kpunch/music/musicXmpDrv.c | 226 ++ .../thirdparty/libxmp-lite/DVX-VERSION.md | 8 +- .../dvx-patches/0002-load-hook.patch | 66 + .../thirdparty/libxmp-lite/include/xmp.h | 5 + .../music/thirdparty/libxmp-lite/src/common.h | 2 + .../thirdparty/libxmp-lite/src/control.c | 11 + .../libxmp-lite/src/loaders/sample.c | 5 + src/libs/kpunch/pkg/Makefile | 66 +- src/libs/kpunch/pkg/pkg.dep | 2 +- src/libs/kpunch/pkg/pkg.dhs | 30 +- src/libs/kpunch/pkg/pkgBas.c | 698 +++++- src/libs/kpunch/pkg/pkgBas.h | 27 +- src/libs/kpunch/pkg/pkgCli.c | 6 +- src/libs/kpunch/pkg/pkgDb.c | 2 +- src/libs/kpunch/pkg/pkgDpk.c | 923 +++++--- src/libs/kpunch/pkg/pkgDpk.h | 57 +- src/libs/kpunch/pkg/pkgFormat.c | 74 +- src/libs/kpunch/pkg/pkgFormat.h | 15 +- src/libs/kpunch/pkg/pkgIni.c | 31 +- src/libs/kpunch/pkg/pkgInt.h | 15 +- src/libs/kpunch/pkg/pkgManifest.c | 81 +- src/libs/kpunch/pkg/pkgManifest.h | 5 + src/libs/kpunch/pkg/pkgOp.c | 242 ++- src/libs/kpunch/pkg/pkgOp.h | 22 +- src/libs/kpunch/pkg/pkgPlan.c | 149 +- src/libs/kpunch/pkg/pkgPlan.h | 47 +- src/libs/kpunch/pkg/pkgSign.c | 69 +- src/libs/kpunch/pkg/pkgSign.h | 10 +- src/libs/kpunch/pkg/pkgSignMake.c | 72 + src/libs/kpunch/pkg/pkgTrust.c | 54 + src/libs/kpunch/pkg/pkgTrust.h | 5 + src/libs/kpunch/pkg/pkgWriter.h | 62 + src/libs/kpunch/pkg/pkgWriterDrv.c | 44 + src/libs/kpunch/pkg/pkgWriterMod.c | 180 ++ src/libs/kpunch/pkg/pkgZip.c | 224 +- src/libs/kpunch/pkg/pkgZip.h | 108 +- src/libs/kpunch/pkg/pkgZipJob.c | 445 ++++ src/libs/kpunch/pkg/pkgZipWrite.c | 507 +++-- .../pkg/thirdparty/monocypher/DVX-VERSION.md | 31 - src/libs/kpunch/pkgnet/pkgNet.c | 227 +- src/libs/kpunch/pkgnet/pkgNetBas.c | 31 +- src/libs/kpunch/pkgnet/pkgNetBas.h | 1 + src/libs/kpunch/pkgnet/pkgnet.dep | 2 +- src/libs/kpunch/pkgnet/pkgnet.dhs | 2 +- src/libs/kpunch/print/Makefile | 23 +- src/libs/kpunch/print/README.md | 37 + .../kpunch/print/{ => drivers/ps}/printPs.c | 575 ++--- .../print/{ => drivers/ps}/printPsAfm.c | 22 + src/libs/kpunch/print/print.dhs | 2 +- src/libs/kpunch/print/print.h | 9 + src/libs/kpunch/print/printFont.c | 50 +- src/libs/kpunch/print/printInt.h | 44 +- src/libs/kpunch/print/printJob.c | 37 +- src/libs/kpunch/print/printPsMod.c | 202 ++ src/libs/kpunch/print/printRaster.c | 42 +- src/libs/kpunch/print/printText.c | 22 + src/libs/kpunch/print/printUi.c | 37 +- src/libs/kpunch/seclink/Makefile | 65 + src/libs/kpunch/seclink/README.md | 40 + .../{serial/packet => seclink}/packet.c | 281 ++- .../{serial/packet => seclink}/packet.h | 38 +- .../kpunch/{serial => }/seclink/secLink.c | 481 +++-- .../kpunch/{serial => }/seclink/secLink.h | 50 +- src/libs/kpunch/seclink/seclink.dep | 3 + src/libs/kpunch/seclink/seclink.dhs | 551 +++++ src/libs/kpunch/serial/Makefile | 21 +- src/libs/kpunch/serial/packet/README.md | 27 - src/libs/kpunch/serial/rs232/rs232.c | 162 +- src/libs/kpunch/serial/rs232/rs232.h | 1 + src/libs/kpunch/serial/seclink/README.md | 28 - src/libs/kpunch/serial/security/README.md | 29 - src/libs/kpunch/serial/serial.dhs | 727 +------ src/libs/kpunch/sql/README.md | 25 + src/libs/kpunch/sql/dvxSql.c | 259 ++- src/libs/kpunch/sql/dvxSql.h | 19 +- src/libs/kpunch/sql/dvxSqlVfs.c | 2 +- src/libs/kpunch/sql/dvxsql.dhs | 33 +- .../sql/thirdparty/sqlite3/sqliteOpts.mk | 15 +- src/libs/kpunch/texthelp/textHelp.c | 277 ++- src/libs/kpunch/texthelp/textHelp.h | 22 +- src/libs/kpunch/xfer/xfer.c | 54 +- src/libs/kpunch/xfer/xfer.dhs | 8 +- src/libs/kpunch/xfer/xfer.h | 6 + src/libs/kpunch/xfer/xferBas.c | 45 +- src/libs/kpunch/xfer/xferInt.h | 11 +- src/libs/kpunch/xfer/xferZmodem.c | 144 +- src/loader/Makefile | 17 +- src/loader/README.md | 32 +- src/loader/loaderMain.c | 783 +++++-- src/packages.ini | 17 +- src/savers/kpunch/starfld/starfld.res | 22 + src/sdk/README.TXT | 6 +- src/sdk/templates/basic/tplName.frm | 1 + src/sdk/templates/basicgame/tplName.frm | 1 + src/sdk/templates/control/tplName.ctl | 2 + src/sdk/templates/library/tplName.bas | 1 + src/sdkHeaders.mk | 9 +- src/test/abi/Makefile | 2 +- src/test/abi/abiSizes.txt | 34 +- src/test/audio/audioTest.c | 374 +++- src/test/basdev/testBasDev.c | 65 +- src/test/basic/Makefile | 4 +- src/test/basic/basBench.c | 19 +- src/test/basic/bench/arithFloat.bas | 23 + src/test/basic/bench/arithInt.bas | 23 + src/test/basic/bench/array1d.bas | 23 + src/test/basic/bench/array2d.bas | 23 + src/test/basic/bench/arrayType.bas | 23 + src/test/basic/bench/callByRef.bas | 23 + src/test/basic/bench/callByVal.bas | 23 + src/test/basic/bench/callOptional.bas | 23 + src/test/basic/bench/classCreate.bas | 23 + src/test/basic/bench/classMethods.bas | 23 + src/test/basic/bench/collection.bas | 23 + src/test/basic/bench/fileIo.bas | 23 + src/test/basic/bench/gfxPrims.bas | 23 + src/test/basic/bench/objectGraph.bas | 23 + src/test/basic/bench/recursion.bas | 23 + src/test/basic/bench/strBuild.bas | 23 + src/test/basic/bench/strFuncs.bas | 23 + src/test/basic/bench/variantArith.bas | 23 + src/test/basic/fixtures/arraysMulti.bas | 23 + src/test/basic/fixtures/cintRounding.bas | 23 + src/test/basic/fixtures/classObjects.bas | 23 + src/test/basic/fixtures/commandArgs.bas | 23 + src/test/basic/fixtures/compareText.bas | 23 + src/test/basic/fixtures/constsAndLet.bas | 23 + src/test/basic/fixtures/dataRead.bas | 23 + src/test/basic/fixtures/defTypes.bas | 23 + src/test/basic/fixtures/divByZero.bas | 23 + src/test/basic/fixtures/erlLine.bas | 23 + src/test/basic/fixtures/erlLine.out | 4 +- src/test/basic/fixtures/eventsInterfaces.bas | 23 + src/test/basic/fixtures/fileBinary.bas | 23 + src/test/basic/fixtures/fileInputMulti.bas | 23 + src/test/basic/fixtures/fileRandom.bas | 23 + src/test/basic/fixtures/fileRandomUdt.bas | 23 + src/test/basic/fixtures/fsDirOps.bas | 23 + src/test/basic/fixtures/fsErrors.bas | 23 + src/test/basic/fixtures/gosub.bas | 23 + src/test/basic/fixtures/gosubLocal.bas | 23 + src/test/basic/fixtures/hexOct.bas | 23 + src/test/basic/fixtures/iniReadWrite.bas | 23 + src/test/basic/fixtures/inputStdin.bas | 23 + src/test/basic/fixtures/intOverflow.bas | 23 + src/test/basic/fixtures/lineContinuation.bas | 23 + src/test/basic/fixtures/loopsMisc.bas | 23 + src/test/basic/fixtures/mathFuncs.bas | 23 + src/test/basic/fixtures/noFormRuntime.bas | 23 + src/test/basic/fixtures/onError.bas | 23 + src/test/basic/fixtures/onErrorSub.bas | 23 + src/test/basic/fixtures/optionBase.bas | 23 + src/test/basic/fixtures/parserExt.bas | 23 + src/test/basic/fixtures/printTab.bas | 23 + src/test/basic/fixtures/printUsing.bas | 23 + .../basic/fixtures/printUsingLiterals.bas | 23 + src/test/basic/fixtures/procs.bas | 23 + src/test/basic/fixtures/resumeNoError.bas | 23 + src/test/basic/fixtures/selectCase.bas | 23 + src/test/basic/fixtures/sharedVars.bas | 23 + src/test/basic/fixtures/staticVars.bas | 23 + src/test/basic/fixtures/stepCap.bas | 23 + src/test/basic/fixtures/strAppend.bas | 23 + src/test/basic/fixtures/stringCompare.bas | 23 + src/test/basic/fixtures/stringFuncs.bas | 23 + src/test/basic/fixtures/swapVars.bas | 23 + src/test/basic/fixtures/typedStore.bas | 23 + src/test/basic/fixtures/typedStoreBoolean.bas | 23 + src/test/basic/fixtures/udtNested.bas | 23 + src/test/dialogs/dialogsTest.c | 7 + src/test/dos/fixtures/benchlog/begin.bas | 23 + src/test/dos/fixtures/benchlog/end.bas | 23 + src/test/dos/fixtures/bncurve/bncurve.bas | 93 + src/test/dos/fixtures/bncurve/bncurve.dbp | 12 + src/test/dos/fixtures/dosrun/dosprog.bat | 22 + src/test/dos/fixtures/parpm/parpm.frm | 4 +- src/test/dos/fixtures/prdraw/prdraw.frm | 22 + src/test/dos/fixtures/scrpage/scrpage.bas | 23 + src/test/dos/fixtures/scrtest/scrtest.bas | 23 + src/test/dos/fixtures/shellrun/shellrun.dbp | 18 + src/test/dos/fixtures/shellrun/shellrun.frm | 65 + src/test/dos/fixtures/sqlsmk/sqlsmk.frm | 134 +- src/test/dos/musTest/musTest.c | 77 +- src/test/dos/script/scriptDos.c | 9 + src/test/dos/scripts/addons.scr | 22 + src/test/dos/scripts/apps.scr | 22 + src/test/dos/scripts/awemod.scr | 22 + src/test/dos/scripts/awerom.scr | 22 + src/test/dos/scripts/awewave.scr | 22 + src/test/dos/scripts/banked.scr | 22 + src/test/dos/scripts/basbench.scr | 28 + src/test/dos/scripts/boot.scr | 32 +- src/test/dos/scripts/cpanel.scr | 22 + src/test/dos/scripts/desksnd.scr | 22 + src/test/dos/scripts/docOpen.scr | 22 + src/test/dos/scripts/dosprog.scr | 25 + src/test/dos/scripts/endbas.scr | 22 + src/test/dos/scripts/exit.scr | 22 + src/test/dos/scripts/f1.scr | 22 + src/test/dos/scripts/fileMgr.scr | 22 + src/test/dos/scripts/filedlg.scr | 22 + src/test/dos/scripts/firstrun.scr | 22 + src/test/dos/scripts/fmperf.scr | 22 + src/test/dos/scripts/fonts.scr | 22 + src/test/dos/scripts/fonts8.scr | 22 + src/test/dos/scripts/fullmode.scr | 22 + src/test/dos/scripts/fullscr.scr | 22 + src/test/dos/scripts/gamepad.scr | 22 + src/test/dos/scripts/gus.scr | 22 + src/test/dos/scripts/gusmod.scr | 22 + src/test/dos/scripts/guspat.scr | 22 + src/test/dos/scripts/guswave.scr | 22 + src/test/dos/scripts/guswt.scr | 22 + src/test/dos/scripts/hang.scr | 22 + src/test/dos/scripts/instadd.scr | 22 + src/test/dos/scripts/install.scr | 22 + src/test/dos/scripts/instbase.scr | 22 + src/test/dos/scripts/instcore.scr | 22 + src/test/dos/scripts/joy4axis.scr | 22 + src/test/dos/scripts/joycal.scr | 22 + src/test/dos/scripts/joyempty.scr | 22 + src/test/dos/scripts/joykeys.scr | 22 + src/test/dos/scripts/joymove.scr | 22 + src/test/dos/scripts/joyport.scr | 22 + src/test/dos/scripts/lfn.scr | 22 + src/test/dos/scripts/lptcovox.scr | 22 + src/test/dos/scripts/lptdss.scr | 22 + src/test/dos/scripts/lptst86.scr | 28 + src/test/dos/scripts/lptston.scr | 22 + src/test/dos/scripts/menus.scr | 22 + src/test/dos/scripts/midiin.scr | 22 + src/test/dos/scripts/musfm.scr | 22 + src/test/dos/scripts/musmod.scr | 22 + src/test/dos/scripts/musmpu.scr | 22 + src/test/dos/scripts/net.scr | 22 + src/test/dos/scripts/novbe.scr | 22 + src/test/dos/scripts/parcp.scr | 22 + src/test/dos/scripts/pardemo.scr | 22 + src/test/dos/scripts/pardsgn.scr | 22 + src/test/dos/scripts/parhlp.scr | 22 + src/test/dos/scripts/paride.scr | 22 + src/test/dos/scripts/parpm.scr | 22 + src/test/dos/scripts/paths.scr | 22 + src/test/dos/scripts/pkgbuild.scr | 22 + src/test/dos/scripts/pkgsign.scr | 74 + src/test/dos/scripts/pkgsrv.scr | 22 + src/test/dos/scripts/player.scr | 22 + src/test/dos/scripts/prdjcol.scr | 22 + src/test/dos/scripts/print.scr | 22 + src/test/dos/scripts/prlj300.scr | 22 + src/test/dos/scripts/progcach.scr | 37 + src/test/dos/scripts/progman.scr | 22 + src/test/dos/scripts/prps.scr | 22 + src/test/dos/scripts/record.scr | 22 + src/test/dos/scripts/safevid.scr | 22 + src/test/dos/scripts/saver.scr | 22 + src/test/dos/scripts/sb1.scr | 22 + src/test/dos/scripts/sb2.scr | 22 + src/test/dos/scripts/sbpro.scr | 22 + src/test/dos/scripts/scrpage.scr | 22 + src/test/dos/scripts/scrtest.scr | 22 + src/test/dos/scripts/sdk.scr | 22 + src/test/dos/scripts/sdkgame.scr | 22 + src/test/dos/scripts/seclink.scr | 22 + src/test/dos/scripts/secpass.scr | 22 + src/test/dos/scripts/shellcmd.scr | 48 + src/test/dos/scripts/sndbas.scr | 22 + src/test/dos/scripts/sndload.scr | 22 + src/test/dos/scripts/sndmore.scr | 22 + src/test/dos/scripts/software.scr | 22 + src/test/dos/scripts/softwave.scr | 22 + src/test/dos/scripts/sound.scr | 22 + src/test/dos/scripts/sound2.scr | 22 + src/test/dos/scripts/speaker.scr | 22 + src/test/dos/scripts/spkpcm.scr | 22 + src/test/dos/scripts/sql.scr | 32 +- src/test/dos/scripts/svdbg.scr | 22 + src/test/dos/scripts/telnet.scr | 22 + src/test/dos/scripts/terminal.scr | 22 + src/test/dos/scripts/ucdemo.scr | 22 + src/test/dos/scripts/ucide.scr | 39 +- src/test/dos/scripts/upgdrop.scr | 45 +- src/test/dos/scripts/upgrade.scr | 22 + src/test/dos/scripts/video.scr | 22 + src/test/dos/scripts/welcome.scr | 22 + src/test/dos/scripts/winops.scr | 22 + src/test/dos/scripts/wss.scr | 22 + src/test/dos/scripts/xferperf.scr | 22 + src/test/dos/sndTest/sndTest.c | 76 +- src/test/dvxtest/checkStatics.sh | 22 + src/test/dvxtest/dvxtest.c | 32 +- src/test/dvxtest/dvxtest.mk | 2 +- src/test/dvxtest/hostLookup.c | 91 + src/test/dvxtest/lookupHook.c | 70 + src/test/dvxtest/lookupHook.h | 42 + src/test/fileops/fileMgrTest.c | 66 +- src/test/fileops/fileOpsTest.c | 432 ++++ src/test/formrt/Makefile | 17 +- src/test/formrt/frtHarness.c | 10 +- src/test/formrt/frtStubs.c | 165 +- src/test/formrt/frtStubs.h | 22 +- src/test/formrt/testFontProps.c | 9 +- src/test/formrt/testFormRt.c | 1045 ++++++++- src/test/formrt/testFrmModel.c | 32 +- src/test/formrt/testLibBas.c | 114 +- src/test/formrt/testUcDesign.c | 2 +- src/test/formrt/testUserCtl.c | 207 +- src/test/fuzz/Makefile | 4 +- src/test/fuzz/fuzzFrmModel.c | 2 +- src/test/fuzz/fuzzPacket.c | 4 +- src/test/fuzz/lsan.supp | 22 + src/test/fuzz/ubsan.supp | 22 + src/test/game/gameTest.c | 7 +- src/test/hlpfile/hlpFileTest.c | 106 + src/test/ide/Makefile | 10 +- src/test/ide/fixtures/form1.frm | 24 + src/test/ide/fixtures/main.bas | 23 + src/test/ide/testIde.c | 2 + src/test/ide/testIdeDesign.c | 18 +- src/test/ide/testIdeRegress.c | 44 +- src/test/ide/testStub.c | 1 - src/test/joystick/joystickTest.c | 36 + src/test/lint/Makefile | 33 +- src/test/lint/analyzerBaseline.txt | 26 +- src/test/lint/asciiCheck.sh | 22 + src/test/lint/baseline.txt | 308 +-- src/test/lint/clangTidy.yaml | 22 + src/test/lint/clangTidyBaseline.txt | 26 +- src/test/lint/codespellWords.txt | 2 + src/test/lint/cppcheckBaseline.txt | 30 +- src/test/lint/hostDb.py | 22 + src/test/lint/licenseCheck.sh | 79 + src/test/lint/qaGate.sh | 22 + src/test/lint/staticGate.sh | 26 +- src/test/lint/styleLint.py | 22 + src/test/music/Makefile | 4 +- src/test/music/lsan.supp | 22 + src/test/music/musicFiles.c | 23 + src/test/music/musicFiles.h | 4 + src/test/music/musicTest.c | 231 +- src/test/music/playerTest.c | 7 +- src/test/music/recorderTest.c | 18 +- src/test/net/Makefile | 20 +- src/test/net/netTest.c | 278 ++- src/test/pkg/Makefile | 14 +- src/test/pkg/appDrive.c | 15 +- src/test/pkg/appDrive.h | 4 +- src/test/pkg/pkgServe.c | 2 +- src/test/pkg/pkgTest.c | 17 +- src/test/pkg/pkgTestBas.c | 279 ++- src/test/pkg/pkgTestEngine.c | 132 +- src/test/pkg/pkgTestNet.c | 70 +- src/test/pkg/pkgbuildTest.c | 4 +- src/test/print/Makefile | 2 +- src/test/print/printTest.c | 64 + src/test/samples/golden/basdemo-debug.app | Bin 32116 -> 32168 bytes src/test/samples/golden/basdemo-release.app | Bin 27900 -> 27952 bytes src/test/samples/golden/clock-debug.app | Bin 9823 -> 9904 bytes src/test/samples/golden/clock-release.app | Bin 9422 -> 9503 bytes src/test/samples/golden/fileMgr-debug.app | Bin 34766 -> 35771 bytes src/test/samples/golden/fileMgr-release.app | Bin 29553 -> 30350 bytes src/test/samples/golden/helpedit-debug.app | Bin 13497 -> 14225 bytes src/test/samples/golden/helpedit-release.app | Bin 12365 -> 12923 bytes src/test/samples/golden/iconed-debug.app | Bin 18748 -> 18800 bytes src/test/samples/golden/iconed-release.app | Bin 16153 -> 16205 bytes src/test/samples/golden/imgview-debug.app | Bin 9485 -> 9475 bytes src/test/samples/golden/imgview-release.app | Bin 9066 -> 9056 bytes src/test/samples/golden/inc-comm-debug.app | Bin 6737 -> 6740 bytes src/test/samples/golden/inc-comm-release.app | Bin 6685 -> 6688 bytes src/test/samples/golden/inc-commdlg-debug.app | Bin 6390 -> 6370 bytes .../samples/golden/inc-commdlg-release.app | Bin 6338 -> 6318 bytes .../samples/golden/inc-compiler-debug.app | Bin 6978 -> 6958 bytes .../samples/golden/inc-compiler-release.app | Bin 6926 -> 6906 bytes src/test/samples/golden/inc-conn-debug.app | Bin 6552 -> 6551 bytes src/test/samples/golden/inc-conn-release.app | Bin 6500 -> 6499 bytes src/test/samples/golden/inc-debug-debug.app | Bin 6434 -> 6414 bytes src/test/samples/golden/inc-debug-release.app | Bin 6382 -> 6362 bytes .../samples/golden/inc-designer-debug.app | Bin 6877 -> 6865 bytes .../samples/golden/inc-designer-release.app | Bin 6825 -> 6813 bytes src/test/samples/golden/inc-fileops-debug.app | Bin 7555 -> 7597 bytes .../samples/golden/inc-fileops-release.app | Bin 7503 -> 7545 bytes src/test/samples/golden/inc-font-debug.app | Bin 6368 -> 6348 bytes src/test/samples/golden/inc-font-release.app | Bin 6316 -> 6296 bytes src/test/samples/golden/inc-game-debug.app | Bin 6217 -> 6204 bytes src/test/samples/golden/inc-game-release.app | Bin 6165 -> 6152 bytes src/test/samples/golden/inc-help-debug.app | Bin 6220 -> 6203 bytes src/test/samples/golden/inc-help-release.app | Bin 6168 -> 6151 bytes src/test/samples/golden/inc-hlpfile-debug.app | Bin 6502 -> 6482 bytes .../samples/golden/inc-hlpfile-release.app | Bin 6450 -> 6430 bytes src/test/samples/golden/inc-http-debug.app | Bin 6341 -> 6338 bytes src/test/samples/golden/inc-http-release.app | Bin 6289 -> 6286 bytes .../samples/golden/inc-joystick-debug.app | Bin 6669 -> 6712 bytes .../samples/golden/inc-joystick-release.app | Bin 6617 -> 6660 bytes src/test/samples/golden/inc-json-debug.app | Bin 6265 -> 6245 bytes src/test/samples/golden/inc-json-release.app | Bin 6213 -> 6193 bytes src/test/samples/golden/inc-layout-debug.app | Bin 6187 -> 6167 bytes .../samples/golden/inc-layout-release.app | Bin 6135 -> 6115 bytes .../samples/golden/inc-listhelp-debug.app | Bin 6723 -> 6703 bytes .../samples/golden/inc-listhelp-release.app | Bin 6671 -> 6651 bytes src/test/samples/golden/inc-music-debug.app | Bin 6726 -> 6733 bytes src/test/samples/golden/inc-music-release.app | Bin 6674 -> 6681 bytes src/test/samples/golden/inc-net-debug.app | Bin 6576 -> 6689 bytes src/test/samples/golden/inc-net-release.app | Bin 6524 -> 6637 bytes src/test/samples/golden/inc-pkg-debug.app | Bin 6759 -> 6928 bytes src/test/samples/golden/inc-pkg-release.app | Bin 6707 -> 6876 bytes src/test/samples/golden/inc-pkgnet-debug.app | Bin 6515 -> 6509 bytes .../samples/golden/inc-pkgnet-release.app | Bin 6463 -> 6457 bytes src/test/samples/golden/inc-prefs-debug.app | Bin 6608 -> 6588 bytes src/test/samples/golden/inc-prefs-release.app | Bin 6556 -> 6536 bytes src/test/samples/golden/inc-print-debug.app | Bin 6910 -> 6897 bytes src/test/samples/golden/inc-print-release.app | Bin 6858 -> 6845 bytes .../samples/golden/inc-resource-debug.app | Bin 6304 -> 6285 bytes .../samples/golden/inc-resource-release.app | Bin 6252 -> 6233 bytes .../samples/golden/inc-richtext-debug.app | Bin 6190 -> 6170 bytes .../samples/golden/inc-richtext-release.app | Bin 6138 -> 6118 bytes .../samples/golden/inc-security-debug.app | Bin 6453 -> 6673 bytes .../samples/golden/inc-security-release.app | Bin 6401 -> 6621 bytes src/test/samples/golden/inc-shell-debug.app | Bin 7625 -> 7713 bytes src/test/samples/golden/inc-shell-release.app | Bin 7573 -> 7661 bytes src/test/samples/golden/inc-sound-debug.app | Bin 7411 -> 7643 bytes src/test/samples/golden/inc-sound-release.app | Bin 7359 -> 7591 bytes src/test/samples/golden/inc-sql-debug.app | Bin 6332 -> 6324 bytes src/test/samples/golden/inc-sql-release.app | Bin 6280 -> 6272 bytes src/test/samples/golden/inc-system-debug.app | Bin 7110 -> 7090 bytes .../samples/golden/inc-system-release.app | Bin 7058 -> 7038 bytes src/test/samples/golden/inc-xfer-debug.app | Bin 6486 -> 6499 bytes src/test/samples/golden/inc-xfer-release.app | Bin 6434 -> 6447 bytes src/test/samples/golden/notepad-debug.app | Bin 12270 -> 12525 bytes src/test/samples/golden/notepad-release.app | Bin 11029 -> 11230 bytes src/test/samples/golden/pkgbuild-debug.app | Bin 27270 -> 28847 bytes src/test/samples/golden/pkgbuild-release.app | Bin 24984 -> 26381 bytes src/test/samples/golden/player-debug.app | Bin 23604 -> 23626 bytes src/test/samples/golden/player-release.app | Bin 19799 -> 19830 bytes src/test/samples/golden/recorder-debug.app | Bin 22865 -> 23196 bytes src/test/samples/golden/recorder-release.app | Bin 20075 -> 20406 bytes src/test/samples/golden/resedit-debug.app | Bin 13433 -> 13431 bytes src/test/samples/golden/resedit-release.app | Bin 11892 -> 11890 bytes src/test/samples/golden/software-debug.app | Bin 38881 -> 39428 bytes src/test/samples/golden/software-release.app | Bin 34649 -> 35173 bytes src/test/samples/golden/terminal-debug.app | Bin 47709 -> 48088 bytes src/test/samples/golden/terminal-release.app | Bin 43024 -> 43373 bytes src/test/samples/golden/welcome-debug.app | Bin 79993 -> 80136 bytes src/test/samples/golden/welcome-release.app | Bin 77430 -> 77573 bytes src/test/samples/golden/widshow-debug.app | Bin 14389 -> 14372 bytes src/test/samples/golden/widshow-release.app | Bin 13463 -> 13446 bytes src/test/serial/Makefile | 60 +- src/test/serial/testProxy.c | 47 +- src/test/serial/testSerial.c | 528 +++-- src/test/setup/Makefile | 6 +- src/test/setup/setupTest.c | 59 +- src/test/shell/Makefile | 6 +- src/test/shell/appsTest.c | 271 ++- src/test/shell/loaderTest.c | 133 +- src/test/shell/progmanTest.c | 178 +- src/test/shell/shellFixture.c | 49 +- src/test/shell/shellFixture.h | 14 +- src/test/shell/shellMainTest.c | 6 +- src/test/shell/shellTest.c | 126 ++ src/test/sql/sqlMain.c | 169 +- src/test/terminal/Makefile | 49 +- src/test/terminal/testConn.c | 69 +- src/test/terminal/testConnNoNet.c | 91 + src/test/terminal/testConnProxy.c | 20 +- src/test/terminal/testTelnet.c | 7 +- src/test/terminal/testTermApp.c | 15 +- src/test/terminal/testXfer.c | 91 + src/test/tools/dfnReadTest.c | 25 +- src/test/tools/fixtures/genFixtures.py | 22 + src/test/tools/fixtures/help/badDirective.dhs | 22 + src/test/tools/fixtures/help/brokenLink.dhs | 22 + .../tools/fixtures/help/brokenLinkInc.dhs | 22 + src/test/tools/fixtures/help/codeLink.dhs | 22 + src/test/tools/fixtures/help/extra.dhs | 22 + src/test/tools/fixtures/help/imageNoName.dhs | 22 + src/test/tools/fixtures/help/imgKinds.dhs | 22 + src/test/tools/fixtures/help/inc.dhs | 22 + .../tools/fixtures/help/includeNoFile.dhs | 22 + src/test/tools/fixtures/help/indexNoKey.dhs | 22 + src/test/tools/fixtures/help/itemOutside.dhs | 22 + src/test/tools/fixtures/help/linkA.dhs | 22 + src/test/tools/fixtures/help/linkB.dhs | 22 + src/test/tools/fixtures/help/linkInc.dhs | 22 + src/test/tools/fixtures/help/linkNoId.dhs | 22 + src/test/tools/fixtures/help/listEnd.dhs | 22 + src/test/tools/fixtures/help/main.dhs | 22 + src/test/tools/fixtures/help/missingImage.dhs | 22 + .../tools/fixtures/help/nestedSection.dhs | 22 + src/test/tools/fixtures/help/noteEnd.dhs | 22 + src/test/tools/fixtures/help/outsideTopic.dhs | 22 + src/test/tools/fixtures/help/sectionReset.dhs | 22 + src/test/tools/fixtures/help/selfInc.dhs | 22 + src/test/tools/fixtures/help/topicNoId.dhs | 22 + src/test/tools/fixtures/res/manifest.res | 22 + src/test/tools/hlpcLibTest.c | 105 + src/test/tools/hlpcOomTest.c | 22 + src/test/tools/runTools.sh | 53 +- src/test/unit/testDraw.c | 58 + src/test/unit/testFonts.c | 23 + src/test/unit/testPlatform.c | 190 ++ src/test/unit/testPrefs.c | 111 + src/test/unit/testResource.c | 137 +- src/test/unit/testUtil.c | 131 ++ src/test/web/Makefile | 8 +- src/test/web/httpTest.c | 116 +- src/test/web/jsonTest.c | 47 +- src/test/widgets/testCovListView.c | 90 + src/test/widgets/testCovMisc.c | 4 +- src/test/widgets/testCovPopups.c | 7 +- src/test/widgets/testCovTerm.c | 171 +- src/test/widgets/testCovText.c | 62 +- src/test/widgets/testCovTreeView.c | 52 + src/test/widgets/testData.c | 267 ++- src/test/widgets/testHelpView.c | 12 + src/test/widgets/testInput.c | 44 + src/test/widgets/testMisc.c | 22 +- src/test/widgets/testRange.c | 8 + src/test/wm/wmExcl.c | 88 +- src/test/wm/wmFixes.c | 254 +++ src/test/wm/wmSuite.c | 6 +- src/test/wm/wmSuite2.c | 96 +- src/tools/README.md | 12 +- src/tools/afmWidths.py | 20 +- src/tools/bmp2raw.c | 26 +- src/tools/bmpDraw.c | 33 +- src/tools/dvxResWrite.c | 111 +- src/tools/dvxResWrite.h | 30 +- src/tools/dvxhlpc.c | 64 +- src/tools/dxeGc.sh | 68 + src/tools/hlpcCompile.h | 12 + src/tools/hlpcModule.h | 86 + src/tools/mkfont.c | 3 +- src/tools/proxy/Makefile | 13 +- src/tools/proxy/README.md | 16 +- src/tools/proxy/proxy.c | 17 +- src/tools/proxy/sockShim.c | 15 +- src/tools/proxy/sockShim.h | 13 +- src/widgets/kpunch/Makefile | 2 +- src/widgets/kpunch/ansiTerm/ansiTerm.dhs | 12 +- src/widgets/kpunch/ansiTerm/ansiTerm.h | 25 +- src/widgets/kpunch/ansiTerm/ansiterm.bhs | 32 +- .../ansiTerm/{terminal.dep => ansiterm.dep} | 0 src/widgets/kpunch/ansiTerm/terminal.res | 22 + src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c | 249 ++- src/widgets/kpunch/box/box.h | 4 +- src/widgets/kpunch/box/box.res | 22 + src/widgets/kpunch/box/boxMetrics.h | 26 +- src/widgets/kpunch/box/widgetBox.c | 21 +- src/widgets/kpunch/button/button.h | 4 +- src/widgets/kpunch/button/button.res | 22 + src/widgets/kpunch/canvas/canvas.bhs | 3 + src/widgets/kpunch/canvas/canvas.dhs | 5 + src/widgets/kpunch/canvas/canvas.h | 6 +- src/widgets/kpunch/canvas/canvas.res | 22 + src/widgets/kpunch/canvas/widgetCanvas.c | 160 +- src/widgets/kpunch/checkbox/checkbox.h | 4 +- src/widgets/kpunch/checkbox/checkbox.res | 22 + src/widgets/kpunch/comboBox/comboBox.dhs | 1 + src/widgets/kpunch/comboBox/comboBox.h | 4 +- src/widgets/kpunch/comboBox/combobox.bhs | 3 +- src/widgets/kpunch/comboBox/combobox.res | 22 + src/widgets/kpunch/comboBox/widgetComboBox.c | 13 +- src/widgets/kpunch/dataCtrl/dataCtrl.dhs | 16 +- src/widgets/kpunch/dataCtrl/datactr.res | 22 + src/widgets/kpunch/dataCtrl/datactrl.bhs | 20 +- src/widgets/kpunch/dataCtrl/widgetDataCtrl.c | 296 ++- src/widgets/kpunch/dbGrid/dbGrid.dhs | 2 +- src/widgets/kpunch/dbGrid/dbgrid.bhs | 3 +- src/widgets/kpunch/dbGrid/dbgrid.res | 22 + src/widgets/kpunch/dbGrid/widgetDbGrid.c | 157 +- src/widgets/kpunch/dropdown/dropdown.bhs | 3 +- src/widgets/kpunch/dropdown/dropdown.dhs | 1 + src/widgets/kpunch/dropdown/dropdown.h | 4 +- src/widgets/kpunch/dropdown/dropdown.res | 22 + src/widgets/kpunch/dropdown/widgetDropdown.c | 13 +- src/widgets/kpunch/helpView/helpView.h | 4 +- src/widgets/kpunch/helpView/helpview.res | 22 + src/widgets/kpunch/helpView/widgetHelpView.c | 174 +- src/widgets/kpunch/image/image.bhs | 13 +- src/widgets/kpunch/image/image.dhs | 15 +- src/widgets/kpunch/image/image.h | 6 +- src/widgets/kpunch/image/image.res | 22 + src/widgets/kpunch/image/imageBuf.h | 35 +- src/widgets/kpunch/image/widgetImage.c | 114 +- src/widgets/kpunch/imageButton/imgBtn.h | 6 +- src/widgets/kpunch/imageButton/imgbtn.bhs | 13 +- src/widgets/kpunch/imageButton/imgbtn.dhs | 13 +- src/widgets/kpunch/imageButton/imgbtn.res | 22 + .../kpunch/imageButton/widgetImageButton.c | 47 +- src/widgets/kpunch/label/label.h | 4 +- src/widgets/kpunch/label/label.res | 22 + src/widgets/kpunch/listBox/listBox.h | 4 +- src/widgets/kpunch/listBox/listbox.bhs | 1 + src/widgets/kpunch/listBox/listbox.res | 22 + src/widgets/kpunch/listBox/widgetListBox.c | 56 +- src/widgets/kpunch/listView/listView.dhs | 15 + src/widgets/kpunch/listView/listView.h | 8 +- src/widgets/kpunch/listView/listview.bhs | 12 +- src/widgets/kpunch/listView/listview.res | 22 + src/widgets/kpunch/listView/widgetListView.c | 324 ++- src/widgets/kpunch/progressBar/progress.h | 4 +- src/widgets/kpunch/progressBar/progress.res | 22 + .../kpunch/progressBar/widgetProgressBar.c | 10 +- src/widgets/kpunch/radio/radio.bhs | 2 + src/widgets/kpunch/radio/radio.dhs | 1 + src/widgets/kpunch/radio/radio.h | 4 +- src/widgets/kpunch/radio/radio.res | 22 + src/widgets/kpunch/radio/widgetRadio.c | 12 +- src/widgets/kpunch/richEdit/richEdit.h | 4 +- src/widgets/kpunch/richEdit/richedit.bhs | 18 +- src/widgets/kpunch/richEdit/richedit.dhs | 2 +- src/widgets/kpunch/richEdit/richedit.res | 22 + src/widgets/kpunch/richEdit/widgetRichEdit.c | 44 +- src/widgets/kpunch/richLabel/richLbl.h | 4 +- src/widgets/kpunch/richLabel/richlbl.res | 22 + src/widgets/kpunch/scrollPane/scrlPane.h | 4 +- src/widgets/kpunch/scrollPane/scrlpane.res | 22 + .../kpunch/scrollPane/widgetScrollPane.c | 20 +- src/widgets/kpunch/separator/separatr.bhs | 13 +- src/widgets/kpunch/separator/separatr.dhs | 14 +- src/widgets/kpunch/separator/separatr.h | 14 +- src/widgets/kpunch/separator/separatr.res | 22 + .../kpunch/separator/widgetSeparator.c | 38 +- src/widgets/kpunch/slider/slider.h | 4 +- src/widgets/kpunch/slider/slider.res | 22 + src/widgets/kpunch/spacer/spacer.h | 4 +- src/widgets/kpunch/spacer/spacer.res | 22 + src/widgets/kpunch/spinner/spinner.h | 4 +- src/widgets/kpunch/spinner/spinner.res | 22 + src/widgets/kpunch/splitter/splitter.bhs | 8 + src/widgets/kpunch/splitter/splitter.h | 4 +- src/widgets/kpunch/splitter/splitter.res | 22 + src/widgets/kpunch/statusBar/statBar.h | 4 +- src/widgets/kpunch/statusBar/statbar.bhs | 17 +- src/widgets/kpunch/statusBar/statbar.dhs | 14 +- src/widgets/kpunch/statusBar/statbar.res | 22 + .../kpunch/statusBar/widgetStatusBar.c | 11 +- src/widgets/kpunch/tabControl/tabCtrl.h | 4 +- src/widgets/kpunch/tabControl/tabctrl.bhs | 16 +- src/widgets/kpunch/tabControl/tabctrl.res | 22 + src/widgets/kpunch/textInput/textInpt.h | 4 +- src/widgets/kpunch/textInput/textinpt.bhs | 4 +- src/widgets/kpunch/textInput/textinpt.dhs | 2 +- src/widgets/kpunch/textInput/textinpt.res | 22 + .../kpunch/textInput/widgetTextInput.c | 59 +- src/widgets/kpunch/timer/timer.bhs | 2 + src/widgets/kpunch/timer/timer.dhs | 8 + src/widgets/kpunch/timer/timer.h | 10 +- src/widgets/kpunch/timer/timer.res | 22 + src/widgets/kpunch/timer/widgetTimer.c | 28 +- src/widgets/kpunch/toolbar/toolbar.bhs | 15 +- src/widgets/kpunch/toolbar/toolbar.dhs | 14 +- src/widgets/kpunch/toolbar/toolbar.h | 4 +- src/widgets/kpunch/toolbar/toolbar.res | 22 + src/widgets/kpunch/toolbar/widgetToolbar.c | 11 +- src/widgets/kpunch/treeView/treeView.dhs | 15 +- src/widgets/kpunch/treeView/treeView.h | 4 +- src/widgets/kpunch/treeView/treeview.bhs | 8 + src/widgets/kpunch/treeView/treeview.res | 22 + src/widgets/kpunch/treeView/widgetTreeView.c | 158 +- src/widgets/kpunch/wrapBox/widgetWrapBox.c | 17 +- src/widgets/kpunch/wrapBox/wrapBox.dhs | 2 + src/widgets/kpunch/wrapBox/wrapbox.bhs | 10 +- src/widgets/kpunch/wrapBox/wrapbox.res | 22 + 960 files changed, 41812 insertions(+), 11015 deletions(-) create mode 100644 screenshots/86box/pkgsign-ps-plan.png create mode 100644 screenshots/dosbox/pkgsign-ps-plan.png create mode 100644 screenshots/dosbox/shellcmd-sc-back.png rename src/apps/kpunch/dvxbasic/{formrt => basdev}/frmModel.c (95%) rename src/apps/kpunch/dvxbasic/{formrt => basdev}/frmModel.h (97%) rename src/apps/kpunch/dvxbasic/{formrt => basdev}/frmModelBas.c (93%) create mode 100644 src/apps/kpunch/pkgbuild/pbProg.frm create mode 100644 src/libs/kpunch/audio/README.md create mode 100644 src/libs/kpunch/crypto/Makefile create mode 100644 src/libs/kpunch/crypto/README.md create mode 100644 src/libs/kpunch/crypto/crypto.dep create mode 100644 src/libs/kpunch/crypto/crypto.dhs rename src/libs/kpunch/{serial/security => crypto}/secBas.c (55%) create mode 100644 src/libs/kpunch/crypto/secCurve.c create mode 100644 src/libs/kpunch/crypto/secCurve.h rename src/libs/kpunch/{serial/security => crypto}/secHash.c (96%) rename src/libs/kpunch/{serial/security => crypto}/secHash.h (100%) create mode 100644 src/libs/kpunch/crypto/secSign.c rename src/libs/kpunch/{serial/security => crypto}/security.c (85%) rename src/libs/kpunch/{serial/security => crypto}/security.h (85%) rename src/libs/kpunch/{pkg => crypto}/thirdparty/monocypher/AUTHORS.md (100%) create mode 100644 src/libs/kpunch/crypto/thirdparty/monocypher/DVX-VERSION.md rename src/libs/kpunch/{pkg => crypto}/thirdparty/monocypher/LICENCE.md (100%) create mode 100644 src/libs/kpunch/crypto/thirdparty/monocypher/dvx-x25519-step.patch rename src/libs/kpunch/{pkg => crypto}/thirdparty/monocypher/src/monocypher.c (98%) rename src/libs/kpunch/{pkg => crypto}/thirdparty/monocypher/src/monocypher.h (94%) rename src/libs/kpunch/{pkg => crypto}/thirdparty/monocypher/src/optional/monocypher-ed25519.c (100%) rename src/libs/kpunch/{pkg => crypto}/thirdparty/monocypher/src/optional/monocypher-ed25519.h (100%) create mode 100644 src/libs/kpunch/dvxshell/shellProgCache.c create mode 100644 src/libs/kpunch/dvxshell/shellProgCache.h create mode 100644 src/libs/kpunch/fileops/README.md create mode 100644 src/libs/kpunch/game/README.md create mode 100644 src/libs/kpunch/hlpfile/README.md create mode 100644 src/libs/kpunch/joystick/README.md create mode 100644 src/libs/kpunch/libdvx/dvxEndian.h create mode 100644 src/libs/kpunch/libdvx/dvxHash.h create mode 100644 src/libs/kpunch/libdvx/dvxImgInt.h rename src/libs/kpunch/libdvx/{dvxImageWrite.c => dvxResInt.h} (58%) create mode 100644 src/libs/kpunch/libdvx/dvxResourceBas.c create mode 100644 src/libs/kpunch/music/README.md create mode 100644 src/libs/kpunch/music/musicXmpDrv.c create mode 100644 src/libs/kpunch/music/thirdparty/libxmp-lite/dvx-patches/0002-load-hook.patch create mode 100644 src/libs/kpunch/pkg/pkgSignMake.c create mode 100644 src/libs/kpunch/pkg/pkgWriter.h create mode 100644 src/libs/kpunch/pkg/pkgWriterDrv.c create mode 100644 src/libs/kpunch/pkg/pkgWriterMod.c create mode 100644 src/libs/kpunch/pkg/pkgZipJob.c delete mode 100644 src/libs/kpunch/pkg/thirdparty/monocypher/DVX-VERSION.md create mode 100644 src/libs/kpunch/print/README.md rename src/libs/kpunch/print/{ => drivers/ps}/printPs.c (90%) rename src/libs/kpunch/print/{ => drivers/ps}/printPsAfm.c (94%) create mode 100644 src/libs/kpunch/print/printPsMod.c create mode 100644 src/libs/kpunch/seclink/Makefile create mode 100644 src/libs/kpunch/seclink/README.md rename src/libs/kpunch/{serial/packet => seclink}/packet.c (81%) rename src/libs/kpunch/{serial/packet => seclink}/packet.h (81%) rename src/libs/kpunch/{serial => }/seclink/secLink.c (69%) rename src/libs/kpunch/{serial => }/seclink/secLink.h (83%) create mode 100644 src/libs/kpunch/seclink/seclink.dep create mode 100644 src/libs/kpunch/seclink/seclink.dhs delete mode 100644 src/libs/kpunch/serial/packet/README.md delete mode 100644 src/libs/kpunch/serial/seclink/README.md delete mode 100644 src/libs/kpunch/serial/security/README.md create mode 100644 src/libs/kpunch/sql/README.md create mode 100644 src/test/dos/fixtures/bncurve/bncurve.bas create mode 100644 src/test/dos/fixtures/bncurve/bncurve.dbp create mode 100644 src/test/dos/fixtures/shellrun/shellrun.dbp create mode 100644 src/test/dos/fixtures/shellrun/shellrun.frm create mode 100644 src/test/dos/scripts/pkgsign.scr create mode 100644 src/test/dos/scripts/progcach.scr create mode 100644 src/test/dos/scripts/shellcmd.scr create mode 100644 src/test/dvxtest/hostLookup.c create mode 100644 src/test/dvxtest/lookupHook.c create mode 100644 src/test/dvxtest/lookupHook.h create mode 100755 src/test/lint/licenseCheck.sh create mode 100644 src/test/terminal/testConnNoNet.c create mode 100755 src/tools/dxeGc.sh create mode 100644 src/tools/hlpcModule.h rename src/widgets/kpunch/ansiTerm/{terminal.dep => ansiterm.dep} (100%) diff --git a/Makefile b/Makefile index 1a1f1933..b28feaf3 100644 --- a/Makefile +++ b/Makefile @@ -231,7 +231,7 @@ deploy-sdk: tools apps installer @echo "Building SDK..." @# Start from an empty tree so a header dropped from the lists goes away. rm -rf $(SDKDIR) - @mkdir -p $(SDKDIR)/bin $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/texthelp $(SDKDIR)/include/listhelp $(SDKDIR)/include/richtext $(SDKDIR)/include/fileops $(SDKDIR)/include/hlpfile $(SDKDIR)/include/xfer $(SDKDIR)/include/conn $(SDKDIR)/include/print $(SDKDIR)/include/game $(SDKDIR)/include/joystick $(SDKDIR)/include/audio $(SDKDIR)/include/music $(SDKDIR)/include/pkg $(SDKDIR)/include/json $(SDKDIR)/include/http $(SDKDIR)/include/pkgnet $(SDKDIR)/include/net $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo $(SDKDIR)/samples/basic/widshow + @mkdir -p $(SDKDIR)/bin $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/serial $(SDKDIR)/include/crypto $(SDKDIR)/include/seclink $(SDKDIR)/include/texthelp $(SDKDIR)/include/listhelp $(SDKDIR)/include/richtext $(SDKDIR)/include/fileops $(SDKDIR)/include/hlpfile $(SDKDIR)/include/xfer $(SDKDIR)/include/conn $(SDKDIR)/include/print $(SDKDIR)/include/game $(SDKDIR)/include/joystick $(SDKDIR)/include/audio $(SDKDIR)/include/music $(SDKDIR)/include/pkg $(SDKDIR)/include/json $(SDKDIR)/include/http $(SDKDIR)/include/pkgnet $(SDKDIR)/include/net $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo $(SDKDIR)/samples/basic/widshow cp $(SDK_CORE_H) $(SDKDIR)/include/core/ cp $(SDK_SHELL_H) $(SDKDIR)/include/shell/ cp $(SDK_TASKS_H) $(SDKDIR)/include/tasks/ @@ -259,9 +259,9 @@ deploy-sdk: tools apps installer @for h in $(SDK_NET_STD_H); do \ mkdir -p $(SDKDIR)/include/net/$$(dirname $$h) && cp $(SDK_NET_DIR)/$$h $(SDKDIR)/include/net/$$h || exit 1; \ done - @for h in $(SDK_SERIAL_H); do \ - mkdir -p $(SDKDIR)/include/serial/$$(dirname $$h) && cp $(SDK_SERIAL_DIR)/$$h $(SDKDIR)/include/serial/$$h || exit 1; \ - done + cp $(SDK_SERIAL_H) $(SDKDIR)/include/serial/ + cp $(SDK_CRYPTO_H) $(SDKDIR)/include/crypto/ + cp $(SDK_SECLINK_H) $(SDKDIR)/include/seclink/ @# Widget headers -- one subdir per widget @for h in $(SDK_WIDGET_H); do \ wgt=$$(basename $$(dirname $$h)); \ diff --git a/analyze.sh b/analyze.sh index 4ef750db..eb1ba087 100755 --- a/analyze.sh +++ b/analyze.sh @@ -1,4 +1,26 @@ #!/bin/bash +# 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. + # # analyze.sh -- run the DJGPP build under gcc's -fanalyzer. # diff --git a/config/README.md b/config/README.md index 794b58aa..b2eb3125 100644 --- a/config/README.md +++ b/config/README.md @@ -178,6 +178,8 @@ ignored. Names are case-insensitive. | `dvxshell.dep` | dvxshell.lib | libtasks, libdvx, texthelp, listhelp | | `basrt.dep` | basrt.lib | libtasks, libdvx | | `serial.dep` | serial.lib | libtasks, libdvx | +| `crypto.dep` | crypto.lib | libdvx | +| `seclink.dep` | seclink.lib | crypto, serial, libdvx | ### Widget Dependencies @@ -186,7 +188,7 @@ ignored. Names are case-insensitive. | `textinpt.dep` | textinpt.wgt | texthelp | | `combobox.dep` | combobox.wgt | texthelp, listhelp | | `spinner.dep` | spinner.wgt | texthelp | -| `terminal.dep` | terminal.wgt | texthelp | +| `ansiterm.dep` | ansiterm.wgt | texthelp | | `dropdown.dep` | dropdown.wgt | listhelp | | `listbox.dep` | listbox.wgt | listhelp | | `listview.dep` | listview.wgt | listhelp | diff --git a/cppcheck.sh b/cppcheck.sh index b46d613f..01ccf1d8 100755 --- a/cppcheck.sh +++ b/cppcheck.sh @@ -1,4 +1,26 @@ #!/bin/bash +# 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. + # # cppcheck.sh -- static analysis for the DVX source tree. # @@ -28,6 +50,8 @@ INCLUDES=( -Isrc/libs/kpunch/dvxshell -Isrc/libs/kpunch/taskmgr -Isrc/libs/kpunch/serial + -Isrc/libs/kpunch/crypto + -Isrc/libs/kpunch/seclink -Isrc/widgets/kpunch -Isrc/apps/kpunch/dvxbasic -Isrc/apps/kpunch/dvxbasic/compiler diff --git a/docs/dvx_basic_reference.html b/docs/dvx_basic_reference.html index 992a7e70..a192bb5e 100644 --- a/docs/dvx_basic_reference.html +++ b/docs/dvx_basic_reference.html @@ -196,6 +196,8 @@ img { max-width: 100%; }
  • AddText
  • Alignment
  • Alignment
  • +
  • Alignment
  • +
  • Alignment
  • AND
  • ANSI Terminal
  • App
  • @@ -205,6 +207,7 @@ img { max-width: 100%; }
  • Arrange
  • ARRAY
  • ASC
  • +
  • ASCW
  • ATN
  • Attribute
  • Auto-indent
  • @@ -242,6 +245,7 @@ img { max-width: 100%; }
  • CheckBox
  • CHOOSE
  • CHR$
  • +
  • CHRW$
  • CINT
  • CIRCLE
  • Circle method
  • @@ -290,6 +294,7 @@ img { max-width: 100%; }
  • Common Events
  • Common Methods
  • Common Properties
  • +
  • CommOpenTcp
  • CommRecvEncrypted
  • CommSetPlain
  • CompBuildApp
  • @@ -461,6 +466,7 @@ img { max-width: 100%; }
  • DropDown
  • DVX BASIC
  • dvxArrowUp
  • +
  • dvxFullScreenActive
  • EACH
  • Edit Menu
  • ELSE
  • @@ -600,9 +606,11 @@ img { max-width: 100%; }
  • FRM
  • Full screen
  • FULLSCREEN
  • +
  • FullScreen (form)
  • FUNCTION
  • Function Dropdown
  • game.bas
  • +
  • GameInput
  • GameKeyClear
  • GameKeyDown
  • GameWaitRetrace
  • @@ -613,6 +621,7 @@ img { max-width: 100%; }
  • GetItemTag
  • GetLine
  • GETRED
  • +
  • GetSelected
  • GOSUB
  • GOTO
  • GoToLine
  • @@ -640,6 +649,7 @@ img { max-width: 100%; }
  • HlpSearch
  • Horizontal Layout
  • HostLookup$
  • +
  • HostLookupStart
  • HostName$
  • Hotkeys
  • HOUR
  • @@ -647,6 +657,7 @@ img { max-width: 100%; }
  • http.bas
  • HttpOpen
  • HttpRunFor
  • +
  • HttpSetBodyFile
  • Icon (form)
  • Icon views you draw yourself
  • IDE
  • @@ -684,6 +695,8 @@ img { max-width: 100%; }
  • ISNUMERIC
  • ISOBJECT
  • Item
  • +
  • ItemDepth
  • +
  • ItemParent
  • JOIN$
  • JoyAxis
  • JoyButton
  • @@ -745,6 +758,7 @@ img { max-width: 100%; }
  • LoadFormText
  • LOADFORMTEXT
  • LOADPICTURE
  • +
  • LOADPICTUREDATA
  • LOC
  • Locals Window
  • LOCATE
  • @@ -765,6 +779,8 @@ img { max-width: 100%; }
  • Masked input
  • Master-Detail
  • Max
  • +
  • MaxHeight (form)
  • +
  • MaxWidth (form)
  • MB
  • Me
  • Me in Classes
  • @@ -839,6 +855,7 @@ img { max-width: 100%; }
  • Named Arguments
  • Names
  • net.bas
  • +
  • NetState
  • NEW
  • New (forms)
  • New Class Module
  • @@ -879,6 +896,9 @@ img { max-width: 100%; }
  • Packages
  • Padding
  • Padding
  • +
  • Padding
  • +
  • Padding
  • +
  • Padding
  • PAINT
  • Paint event
  • PaintPicture
  • @@ -894,16 +914,28 @@ img { max-width: 100%; }
  • Picture
  • PictureBox
  • pkg.bas
  • -
  • PkgBuild
  • +
  • PkgBuildStart
  • +
  • PkgCheckData
  • PkgFileOwner$
  • PkgInstallStart
  • PkgKeygen
  • +
  • PkgKeysRows$
  • pkgnet.bas
  • +
  • PkgNetOpenIn
  • PkgOpen
  • +
  • PkgOpenIn
  • PkgPlan
  • PkgRemoveStart
  • PkgRunFor
  • -
  • PkgSign
  • +
  • PkgSignData$
  • +
  • PkgSignStart
  • +
  • PkgUntrustKey
  • +
  • PkgVerifyStart
  • +
  • PkgVersionCompare
  • +
  • PkgZipCreateStart
  • +
  • PkgZipExtractStart
  • +
  • PkgZipListStart
  • +
  • PkgZipRows$
  • PLAY
  • PLAY function
  • PlayNoise
  • @@ -1013,8 +1045,10 @@ img { max-width: 100%; }
  • Remove File
  • RemoveControl
  • RemoveItem
  • +
  • RemoveItem
  • RemoveMenuItem
  • Rename File
  • +
  • Repeat
  • REPL
  • Replace
  • Replace
  • @@ -1084,13 +1118,19 @@ img { max-width: 100%; }
  • ScrollPane
  • ScrollToControl
  • Search
  • +
  • SecAeadLock$
  • +
  • SecAeadUnlock$
  • SecCipherCreate
  • SecDhCreate
  • +
  • SecDhStep
  • +
  • SecEd25519Check
  • +
  • SecEd25519Sign$
  • SecHmac$
  • SECOND
  • SecRandom$
  • SecSha256$
  • security.bas
  • +
  • SecX25519$
  • SEEK
  • SelAlignment
  • SelBold
  • @@ -1109,6 +1149,7 @@ img { max-width: 100%; }
  • SelText
  • SelText
  • SelUnderline
  • +
  • Send Event
  • SendKeys
  • Separator
  • Separator
  • @@ -1123,9 +1164,13 @@ img { max-width: 100%; }
  • SetEvent
  • SetFont
  • SetItemTag
  • +
  • SetItemText
  • SetLineMarker
  • SetPenColor
  • SetPenSize
  • +
  • SetPictureData
  • +
  • SetPictureData
  • +
  • SetPictureData
  • SetRange
  • SetRows
  • SetSelected
  • @@ -1154,12 +1199,15 @@ img { max-width: 100%; }
  • ShellItemKeyValid
  • ShellPriority
  • ShellProgramsFolder$
  • +
  • ShellRunDosWait
  • ShellRunningActivate
  • ShellRunningClose
  • ShellRunningFind
  • ShellRunningId
  • ShellRunningKill
  • ShellRunningName$
  • +
  • ShellRunningPriority
  • +
  • ShellRunningSetPriority
  • ShellSetPriority
  • Shortcut
  • Shortcut Keys
  • @@ -1167,6 +1215,7 @@ img { max-width: 100%; }
  • Show
  • Show
  • ShowTopic
  • +
  • Signatures
  • Signing a package
  • SIN
  • Single
  • @@ -1175,6 +1224,8 @@ img { max-width: 100%; }
  • Sizes in characters
  • SLEEP
  • Slider
  • +
  • SortColumn
  • +
  • SortOrder
  • SOUND
  • Sound
  • sound.bas
  • @@ -1206,6 +1257,9 @@ img { max-width: 100%; }
  • Spacer
  • Spacing
  • Spacing
  • +
  • Spacing
  • +
  • Spacing
  • +
  • Spacing
  • SPC
  • SpinButton
  • Spinner
  • @@ -1217,11 +1271,14 @@ img { max-width: 100%; }
  • SQLClose
  • SQLEof
  • SQLError$
  • +
  • SQLEscape$
  • SQLExec
  • SQLField$
  • SQLFieldCount
  • SQLFieldDbl
  • SQLFieldInt
  • +
  • SQLFieldName$
  • +
  • SQLFieldText$
  • SQLFreeResult
  • SQLite
  • SQLite
  • @@ -1283,8 +1340,11 @@ img { max-width: 100%; }
  • Tag (form)
  • TAN
  • TcpConnect
  • +
  • TcpConnectStart
  • +
  • TcpPeek$
  • TcpPeer$
  • TcpRecv$
  • +
  • TcpSendSome
  • Telnet
  • Terminal
  • Text
  • @@ -1484,6 +1544,7 @@ img { max-width: 100%; }
  • vbYes
  • vbYesNo
  • vbYesNoCancel
  • +
  • Vertical
  • Vertical Layout
  • VIEW
  • View Menu
  • @@ -1526,6 +1587,7 @@ img { max-width: 100%; }
  • XferStatus
  • XOR
  • YEAR
  • +
  • ZIP Files
  • ZMODEM
  • @@ -1978,7 +2040,7 @@ End Sub TreeView Hierarchical tree control. VBox Vertical layout container. WrapBox Flow layout container that wraps items to the next row. -

    Image and ImageButton controls are not in the Toolbox; they can still be written into a .frm file by hand and are then edited in the Properties panel like any other control.

    +

    A new Image or ImageButton starts empty, 32 by 32 pixels so it can be seen and selected: set its Picture property to show a picture.

    Form Designer

    UserControls in the IDE

    Back to Overview

    @@ -3429,6 +3491,7 @@ ErrorHandler: 11 Division by zero 13 Type mismatch (text that is not a number where a number is needed) / not an array / not a TYPE instance 14 Out of string space (no memory to build a string) + 18 User interrupt occurred (a control's code ran too long without returning and was stopped) 20 RESUME without error 26 FOR loop nesting too deep 28 Out of stack space (calls or expressions nested too deeply) @@ -3462,9 +3525,10 @@ ErrorHandler:

    SHELL

    Executes an operating-system command.

    SHELL "command"
    -

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

    +

    When used as a function, SHELL returns the exit code of the command, or -1 when it did not run.

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

    DVX steps aside while the command runs, as it does for a DOS program started from Program Manager: the screen goes back to text, and the command has the keyboard, the mouse, and the hardware to itself. Nothing of DVX runs meanwhile, so the other programs that are running are named first and asked to close (your program stays and waits); choosing No, or a program that keeps a window open, means the command does not run (-1). When the command ends, the desktop comes back and your program goes on. A command that prints and ends at once is gone from the screen as soon as it ends: ShellRunDos (shell.bas) can wait for a key first.

    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.

    @@ -3508,15 +3572,18 @@ PLAY "T160 O3 L8 E D C D E E E4" MF Music foreground: PLAY, SOUND, and BEEP wait until the sound has finished (the default). MB Music background: the program goes on while the music plays; it only waits when more than 32 notes are waiting. I n DVX's own: play the notes that follow on General MIDI instrument n (1 to 128; 1 is the piano, 41 the violin, 74 the flute) through the music synthesizer. I0 goes back to plain tones (the default). + V n Volume of the notes that follow, 0 to 100 percent (default 100), as in QB64. On an instrument it sets how hard the note is played. + @ n Waveform of the tones that follow, as in QB64: @1 square (the default, like the PC speaker), @3 triangle (softer), @5 noise (drums, explosions). QB64's @2 and @4 are not available. X "X" + VARPTR$(s$) plays the commands in s$ at that point (s$ may use X itself, up to 16 levels deep). = Any number above may be written as "=" + VARPTR$(n), n a numeric expression: "T=" + VARPTR$(tempo).

    A string with a mistake in it plays nothing and raises error 5 (Illegal function call). X and "=" work as in QBasic, with VARPTR$ as for DRAW (see the program screen's DRAW): the value is taken when VARPTR$ runs. A whole tune may be at most 32767 characters once X and "=" are filled in.

    verse$ = "L8 E D C D E E E4"
     tempo = 160
     PLAY "T=" + VARPTR$(tempo) + "O3 X" + VARPTR$(verse$) + "X" + VARPTR$(verse$)
    -

    Building the string with + works too, such as PLAY "O" + STR$(n). Without I, the notes are square-wave tones like the PC speaker's; with it, they play on the same synthesizer as MIDI music.

    +

    Building the string with + works too, such as PLAY "O" + STR$(n). Without I, the notes are tones -- square waves like the PC speaker's unless @ chooses another -- and with it, they play on the same synthesizer as MIDI music. The PC speaker plays only square tones at one volume: there V and @ make no difference, except that V0 is silent.

    PLAY "MB T120 L4 CDEF GAB>C"   ' a scale in the background
    -PLAY "I74 O4 L8 GAB>C"         ' on the flute
    +PLAY "I74 O4 L8 GAB>C" ' on the flute +PLAY "@3 V60 L8 CEG>C" ' softer, quieter

    PLAY Function

    PLAY(n) returns how many notes are still waiting to be played in the background (n is ignored). A game can use it to keep its music going:

    If PLAY(0) < 4 Then PLAY "MB " + tune$
    @@ -3582,7 +3649,9 @@ Loop

    FULLSCREEN

    Shows the program's screen on the whole screen, or back in its window. Alt+Enter in the window does the same. While the program has the whole screen, Alt+Tab brings the desktop back (the program keeps running in its window) and switching back to the program returns it to the whole screen. Other programs keep running either way. When the computer has no suitable video mode, the screen stays in its window.

    FULLSCREEN [ON | OFF]
    -FULLSCREEN expression      ' True: whole screen, False: window
    +FULLSCREEN expression ' True: whole screen, False: window +isFull = FULLSCREEN ' True while the program's screen has the whole screen +

    Used as a value, FULLSCREEN tells whether the program's screen has the whole screen (also while the desktop is shown for another program, since the screen comes back with the focus): False when no suitable video mode could be set, or before SCREEN or any drawing made a screen. A form takes the whole screen with its FullScreen property instead.

    Text: CLS, LOCATE, COLOR, CSRLIN, POS, SCREEN( )

    CLS [n]
     LOCATE [row] [, [column] [, cursor]]
    @@ -3824,7 +3893,7 @@ pos = SEEK(channel)           ' Function: get current position
    NAME oldName$ AS newName$
    Name "draft.txt" As "final.txt"

    FILECOPY

    -

    Copies a file. If the destination already exists it is overwritten.

    +

    Copies a file. If the destination already exists it is overwritten. While a big file copies, the rest of DVX and the program's own events go on, as with DoEvents (in a control's code they wait until the copy ends).

    FILECOPY source$, destination$
    FileCopy "data.db", "data.bak"

    MKDIR / RMDIR

    @@ -3857,7 +3926,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. DIR$("C:\*.*") lists the root of C: (DIR$("C:*.*") the drive's current folder).

    +

    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. The pattern uses DOS's wildcards, as the file dialog does: * stands for any characters and ? for one, case does not matter, and *.* is every file, with or without an extension. DIR$("C:\*.*") lists the root of C: (DIR$("C:*.*") the drive's current folder). Looking through a big folder for a rare match lets the rest of DVX and the program's own events go on, as with DoEvents; if one of those events starts a new DIR$ listing, the call returns "" (in a control's code nothing else runs meanwhile).

    cwd$ = CURDIR$
     first$ = DIR$("*.txt")
     WHILE first$ <> ""
    @@ -3873,7 +3942,9 @@ WEND
      Function                                                     Returns        Description
       --------                                                     -------        -----------
       ASC(s$)                                                      Integer        ASCII code of the first character of s$ (error 5 for an empty string)
    +  ASCW(s$)                                                     Long           Unicode code point of the first character of s$ (error 5 for an empty string)
       CHR$(n)                                                      String         Character with ASCII code n
    +  CHRW$(n)                                                     String         Character for Unicode code point n ("?" when DVX has no such character; error 5 outside 0 to &H10FFFF)
       FILTER(arr, match$ [, include [, compare]])                  String array   The elements of arr that contain match$ (or, with include False, that do not)
       FORMAT$(value [, fmt$])                                      String         Formats a number or a date using a format string
       HEX$(n)                                                      String         Hexadecimal representation of n (uppercase, no leading &H)
    @@ -4000,7 +4071,7 @@ Print s$                   ' Hello, DVX--!
    SIN(n) Double Sine of n (n in radians) SQR(n) Double Square root of n TAN(n) Double Tangent of n (n in radians) - TIMER Double Seconds since midnight + TIMER Double Seconds since midnight, to a fraction of a millisecond (it keeps rising between the clock's 55 ms ticks)

    RND follows QBasic: RND or RND(n) with n > 0 returns the next number in the sequence, RND(0) repeats the last number, and RND(n) with n < 0 reseeds the generator from n (the same n always gives the same number). The result is a Double, 0 <= RND < 1.

    INT, FIX, and SGN return an Integer, widening to Long or Double when the value does not fit. EXP, ^, and the other functions raise error 6 (Overflow) when the result leaves the numeric range, and error 5 when it is undefined (for example a negative number to a fractional power).

    Note: Use RANDOMIZE or RANDOMIZE TIMER to seed the generator so you don't get the same sequence each run.
    @@ -4135,7 +4206,7 @@ Print MonthName(Month(Date)); " "; WeekdayName(Weekday(Date), True)Print IIf(score >= 50, "Pass", "Fail") Print Choose(Weekday(Date), "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")

    LOADPICTURE

    -

    LOADPICTURE(file$) reads a picture file (BMP, PNG, JPEG, or GIF) -- or the icon of a DVX program, widget, or library file (.app, .wgt, .lib) -- and returns a Picture object (TypeName "Picture") that can be given to a picture property: a control's DragIcon, the Picture of an Image or ImageButton. Use SET, or a plain assignment, as with a file name; file names still work too. LOADPICTURE() or LOADPICTURE("") gives an empty picture, which clears the property. A missing file raises error 53 (File not found), a file that is not a picture error 481 (Invalid picture), so the check happens where the picture is loaded. Loading the same file again gives the same Picture object. Reading a picture property back gives a Picture object too (TypeName "Picture"), as in Visual Basic; used as a value (printed, joined to text, compared) it stands for its file name, so code that treats the property as a file name keeps working. The name of a program, widget, or library file works wherever a picture file's name does (a Picture property, DragIcon, PaintPicture, a Toolbar's AddButton) and shows its icon; such a file without an icon is not a picture (error 481).

    +

    LOADPICTURE(file$) reads a picture file (BMP, PNG, JPEG, or GIF) -- or the icon of a DVX program, widget, or library file (.app, .wgt, .lib) -- and returns a Picture object (TypeName "Picture") that can be given to a picture property: a control's DragIcon, the Picture of an Image or ImageButton. Use SET, or a plain assignment, as with a file name; file names still work too. LOADPICTURE() or LOADPICTURE("") gives an empty picture, which clears the property. A missing file raises error 53 (File not found), a file that is not a picture error 481 (Invalid picture), so the check happens where the picture is loaded. The check reads the picture's header only (the control that shows it reads the rest): a picture file damaged after its header shows nothing. Loading the same file again gives the same Picture object. Reading a picture property back gives a Picture object too (TypeName "Picture"), as in Visual Basic; used as a value (printed, joined to text, compared) it stands for its file name, so code that treats the property as a file name keeps working. The name of a program, widget, or library file works wherever a picture file's name does (a Picture property, DragIcon, PaintPicture, a Toolbar's AddButton) and shows its icon; such a file without an icon is not a picture (error 481).

    The optional size and colordepth arguments are Visual Basic's: size is vbLPSmall (0), vbLPLarge (1), vbLPSmallShell (2), vbLPLargeShell (3), or vbLPCustom (4, with x and y giving the size), colordepth vbLPDefault (0), vbLPMonochrome (1), vbLPVGAColor (2), or vbLPColor (3). They choose among the images of an icon file; every DVX picture is a single bitmap, so, as for a bitmap in Visual Basic, they are checked and otherwise make no difference. A value outside these, or a negative x or y, raises error 5.

    A Picture object has three properties, which can only be read:

      Property   Type      Description
    @@ -4166,6 +4237,12 @@ Set Label1.DragIcon = LoadPicture()     ' back to the dotted outline
     Dim pic As Object
     Set pic = LoadPicture("logo.bmp")
     Print Me.ScaleX(pic.Width, vbHimetric, vbPixels); "x"; Me.ScaleY(pic.Height, vbHimetric, vbPixels)
    +

    LOADPICTUREDATA

    +

    LOADPICTUREDATA(bytes$) does what LOADPICTURE does for a picture held in a string rather than in a file: the bytes of a BMP, PNG, JPEG, or GIF file, such as HttpBody$ gives for a picture downloaded from the web, or ResGetData$ for a picture resource of a DVX program, widget, or library file (any of its resources, not only its icon). The Picture object it returns goes wherever a LOADPICTURE picture goes: the Picture of an Image, an ImageButton, or a PictureBox, a control's DragIcon, a form's Icon, and PaintPicture; reading such a property back gives the same object. Its Width, Height, and Type are a loaded picture's; used as a value it stands for "", since it has no file name. Bytes that are not a picture raise error 481 (Invalid picture); LOADPICTUREDATA("") gives an empty picture. Giving one to a control that shows no picture (a Label's Picture, for example) stops the program with an error.

    +

    A Picture object made this way keeps its bytes until the program ends, and the same bytes give the same object, so loading a picture again costs nothing. To show many different pictures one after another (a slide show, pictures from the web), use the SetPictureData method of the Image, ImageButton, or PictureBox instead: Image1.SetPictureData bytes$ shows the picture without keeping it, and returns True, or False when the bytes are not a picture.

    +
    Image1.Picture = LoadPictureData(HttpBody$(req))
    +Set Label1.DragIcon = LoadPictureData(ResGetData$("mygame.app", "card"))   ' resource.bas in the project
    +If Not Picture1.SetPictureData(HttpBody$(req)) Then MsgBox "That is not a picture."

    COMMAND$

    COMMAND$ returns the program's arguments exactly as they were given, as in Visual Basic (QBasic turned them into capitals; DVX keeps them as they are). Arguments are separated by blanks; an argument with blanks in it is written in double quotes. COMMAND$(n) picks out one argument with its quotes removed, so a program that opens the document it was started with only needs:

    IF COMMAND$(1) <> "" THEN
    @@ -4671,6 +4748,14 @@ fruit.Remove 1                  ' removes Banana, now the first item

    Put text values in single quotes, as in the examples. Text in double quotes is read as a column name, so "Scott" in place of 'Scott' gives the error "no such column: Scott".

    +

    SQLEscape$

    +

    Returns text with every single quote doubled, the way SQL writes a quote inside a quoted value. Use it on text the user typed (or read from a file) before you put it between quotes in a statement: a name such as O'Brien would otherwise end the value early and break the statement.

    +
    SQLExec db, "INSERT INTO users (name) VALUES ('" & SQLEscape$(name$) & "')"
    +

    SQLExec runs every statement in sql$, one after another, before it returns.

    +

    Long Statements

    +

    A statement can take a while: one that changes many rows, CREATE INDEX on a big table, or the first SQLNext of a query that sorts or groups, which reads every row first. While a statement runs, DVX and the other programs keep going, and your program's events run as they do during DoEvents. Ending the program (End, or closing its last form) stops the statement, and what it changed is undone.

    +

    An event that runs meanwhile cannot use the database the statement is running on: SQLExec, SQLQuery, and SQLNext return False or 0, and SQLError$ says "A statement is still running on this database". Its field functions return "" or 0. SQLFreeResult and SQLClose wait until the statement ends. Other databases can be used as always.

    +

    Code in a control file (a control written in BASIC) does not wait like this: its statements run to the end.

    SQLAffected

    Returns the number of rows affected by the last INSERT, UPDATE, or DELETE.

    count = SQLAffected(db)
    @@ -5093,7 +5178,7 @@ End Sub ---- -------- commdlg.bas File Open/Save dialogs (DlgFileOpenIn$ and DlgFileSaveIn$ start in a folder you name), the folder dialog (DlgFolder$, for a Browse button), input dialogs, choice dialogs, save-prompt dialogs, the color dialog, the font dialog (DlgFont), message boxes with your own buttons (DlgButtons) sql.bas SQLite database access (SQLOpen, SQLQuery, SQLNext, SQLField$, etc.) - comm.bas Serial-port I/O (raw and packet) and optional encrypted links; port settings read back and changed, why a call failed, breaks, ring, and line errors (SerOpen, SerSet, SerLastError, CommOpen, CommHandshakeStep, ...) + comm.bas Serial-port I/O (raw and packet) and optional encrypted links; port settings read back and changed, why a call failed, breaks, ring, and line errors (SerOpen, SerSet, SerLastError, CommOpen, CommOpenTcp, CommHandshakeStep, ...) conn.bas Connections the way Terminal makes them: a serial port, a modem that dials, the secure link to the DVX host proxy, or Telnet, shown on a Terminal control, with capture, answerback, and file transfers (ConnCreate, ConnOpen, ConnPoll, ConnAttach, ConnSendFiles, ...) xfer.bas File transfers with XMODEM, XMODEM-1K, YMODEM, and ZMODEM, on a serial port or a connection, with their progress and questions (XferSendFile, XferStatus, XferBytes, XferAnswer, ...) prefs.bas Settings files (INI): load one, read and change its settings, list the keys of a section, save it; DVX's own settings file, the screen mode it starts in, the names its choices are stored as and its default choices, and choices stored by names of your own (PrefsLoad, PrefsGet$, PrefsSet, PrefsKeyName$, PrefsSaveAs, PrefsDvxIni$, PrefsVideoWidth, PrefsChoiceLabel$, PrefsChoiceDefault, PrefsGetChoiceFrom, ...) @@ -5103,7 +5188,7 @@ End Sub net.bas Network connections: TCP (TcpConnect, TcpListen, TcpSend, TcpRecv$, TcpPeer$, ...), UDP messages and name lookups, any bytes; the network settings and status (NetConfigLoad, NetConfigSet, NetConfigStore, NetRestart, NetStatus$, ...) print.bas Printing: text with the page setup and pictures, with or without the Print dialog; a form; the Page Setup dialog; the printer and page settings; the printer ports (PrintModelLabel$, PrintSettingsStore, PrintPageSetting, PrintChoose, PrintForm, PrintPortStatus, ...) listhelp.bas Lists and icon views you draw yourself (a UserControl, a PictureBox) that behave like ListBox and Program Manager: selection with Shift and Ctrl, the arrow keys in a list or a grid, Ctrl+A, Space, type-ahead, where a dragged item lands, and Program Manager's icon view rules -- its cells, the grid with a scroll bar, names on two lines, dragging and dropping (ListSelCreate, ListSelClick, ListSelNavigate, ListTypeAhead, ListIconMetric, ListIconLabel$, ...) - security.bas Security: random bytes for keys, a key exchange, a cipher, SHA-256 hashes, HMAC signatures, key making, and a safe compare (SecRandom$, SecDhCreate, SecCipherCrypt$, SecSha256$, SecHmac$, SecHkdf$, ...) + security.bas Security: random bytes for keys, key exchanges, ciphers, sealing, SHA-256 hashes, HMAC and public-key signatures, key making, and a safe compare (SecRandom$, SecX25519$, SecAeadLock$, SecEd25519Sign$, SecSha256$, SecHkdf$, ...) game.bas Games: whether a key is being held down right now, forgetting held keys, frames FRAMEWAIT skipped, waiting for the screen's retrace (GameKeyDown, GameKeyClear, FrameMissed, GameWaitRetrace) joystick.bas Joysticks: how many there are, their names, where each stick points, and which buttons are pressed (JoyCount, JoyX, JoyY, JoyButton, ...); setting them up: calibration, dead zone, game port mode, keyboard joystick keys (JoySetCalibration, JoyCalibration, JoyRescan, JoyKeyIndex, JoySettingsStore, ...) sound.bas Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...); sounds loaded once and played with volume, pan, and pitch, sound made by the program as it goes, recording from a microphone or line input (RecordStart, RecordStop, RecordLevel, ...), the FM synthesizer chip, a wavetable card's voices and memory (SoundLoad, SoundPlay, SoundSetPitch, SoundCreate, SoundStreamOpen, SoundStreamWrite, FmAlloc, FmWrite, WtAlloc, WtStart, ...); DVX's own event sounds (SoundPlayEvent), master volume, sound driver, and MIDI settings (SoundEventFile$, SoundSetMasterVolume, SoundSettingsStore, ...) @@ -5116,8 +5201,8 @@ End Sub compiler.bas The BASIC compiler: compile a project or files you give it, find compile errors in their files, list a program's procedures and the language's keywords, build an .app or a control file; put a project's UserControls on a form designer's toolbox; read source text as the compiler does (tokens, code or string or comment, renaming a control in code, WithEvents variables, class Events), F1 help topics; open, change, and save project files and UserControl headers (CompSourceFromProject, CompSourceAdd, CompCompile, CompError$, CompMapFile$, CompProcList$, CompBuildApp, CompDesignControls, CompTokens$, CompRenameInCode$, CompHelpTopic$, ProjOpen, ProjSave, ProjCtlSetHeader$, ...) debug.bas Run a compiled program inside yours and debug it: breakpoints, stepping, its variables, expressions, Immediate-window lines, its PRINT output (RunCreate, RunStart, RunPoll, DbgSetBreakpoints, DbgStepOver, DbgLocals$, DbgEval$, DbgExec$, ...) designer.bas Form designers: the control types, whether each holds other controls and can be placed, the names of new controls, default events, toolbox tooltips and icons, and each type's properties (with their types and value names), methods, and events (with each event's parameters); the names in .frm and class module text; .frm files as data to read, edit, and write, controls copied and pasted as text, and a loaded form written back as .frm text (CtlTypeCount, CtlTypeName$, CtlTypeFlags, CtlPropCount, CtlPropName$, CtlPropType, CtlEventList$, CtlEventParams$, CtlTypeEventParams$, FrmFormName$, FrmControlNames$, ClsClassName$, FrmOpen, FrmText$, FrmCtlAdd, FrmCtlText$, FrmCtlPaste, FrmMenuAdd, FormText$, ...) - shell.bas DVX itself, everything Program Manager and Task Manager do: start programs (DVX's and DOS programs) and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close, or end it), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, the DVX log; Program Manager's program groups with their items, shortcut keys, and icons (ShellRun, ShellRunProgram, ShellRunDos, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...) - pkg.bas Add-on packages: the packages installed in DVX and their details, files, and needs; reading .DPK files and planning what installing them takes; installing and removing as a job with progress; trusting a publisher's key; and making packages, signing them, and making keys (PkgOpen, PkgInstalledRows$, PkgOfferFile, PkgPlan, PkgInstallStart, PkgRunFor, PkgRemoveStart, PkgBuild, PkgSign, PkgKeygen, ...) + shell.bas DVX itself, everything Program Manager and Task Manager do: start programs (DVX's and DOS programs) and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close, or end it, and their priority), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, the DVX log; Program Manager's program groups with their items, shortcut keys, and icons (ShellRun, ShellRunProgram, ShellRunDos, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...) + pkg.bas Add-on packages: the packages installed in DVX and their details, files, and needs; reading .DPK files, installing, removing, making, and signing packages as jobs with progress, and planning what installing them takes; trusting a publisher's key; and making keys (PkgOpen, PkgInstalledRows$, PkgOfferFileStart, PkgPlan, PkgInstallStart, PkgRunFor, PkgRemoveStart, PkgBuildStart, PkgSignStart, PkgKeygen, ...) json.bas JSON: read a document (a web service's reply, a settings file) and find values in it by path such as packages[3].version, and quote text for JSON of your own (JsonParse, JsonText$, JsonNumber, JsonCount, JsonKey$, JsonQuote$, ...) http.bas Web requests: GET a page, a file, or a web service's answer, or POST to one, over the network a little at a time with progress, to memory or to a file (resuming one left part-way), following redirects (HttpOpen, HttpStart, HttpRunFor, HttpStatus, HttpBody$, HttpSaveTo, ...) pkgnet.bas A package server: what it offers, what it has that is newer than what is installed, and fetching packages with what they need, for pkg.bas to install (PkgNetOpen, PkgNetCatalogue, PkgNetUpdates, PkgNetFetch, PkgNetRunFor, PkgNetRows$, ...) @@ -5182,7 +5267,7 @@ If Not DlgInputCancelled() Then SetZoom n End If

    Resources in Files

    -

    With resource.bas in the project, a program reads and writes the named resources DVX keeps at the end of program, widget, and library files: icons, text, and any bytes. ResOpen, ResCount, ResName$, ResType, and ResSize list them; ResGetText$ and ResAddText read and write text; ResAddFile and ResExtract copy a resource from or to a file; ResRemove takes one out. ResGetData$(file, name) gives every byte of a resource as a String (CHR$(0) included), and ResAddData file, name, type, bytes adds or replaces a resource of any bytes (type RES_TYPE_BINARY, RES_TYPE_TEXT, RES_TYPE_ICON, or RES_TYPE_FONT, a .DFN font the program carries; see Fonts). RES_NAME_ICON ("icon32") and RES_NAME_WIDGET_ICON ("icon24") are the names DVX gives a program's and a widget's icon; a name is at most RES_NAME_MAX - 1 characters.

    +

    With resource.bas in the project, a program reads and writes the named resources DVX keeps at the end of program, widget, and library files: icons, text, and any bytes. ResOpen, ResCount, ResName$, ResType, and ResSize list them; ResGetText$ and ResAddText read and write text; ResAddFile and ResExtract copy a resource from or to a file; ResRemove takes one out. ResGetData$(file, name) gives every byte of a resource as a String (CHR$(0) included) -- LoadPictureData(ResGetData$(file, name)) shows a picture resource -- and ResAddData file, name, type, bytes adds or replaces a resource of any bytes (type RES_TYPE_BINARY, RES_TYPE_TEXT, RES_TYPE_ICON, or RES_TYPE_FONT, a .DFN font the program carries; see Fonts). RES_NAME_ICON ("icon32") and RES_NAME_WIDGET_ICON ("icon24") are the names DVX gives a program's and a widget's icon; a name is at most RES_NAME_MAX - 1 characters.

    Dim settings As String
     settings = Chr$(1) + Chr$(0) + Chr$(255)
     
    @@ -5201,7 +5286,11 @@ Else
         Print "Handshake failed"
     End If

    After the handshake, data that arrives unencrypted is ignored unless that channel is opened for it with CommSetPlain link, channel, True.

    -

    CommHandshake waits until the key exchange is done. CommHandshakeStep(link, passphrase) does one step of it and returns at once, so a program can keep working: call it from a Timer until it returns something other than COMM_PENDING -- COMM_OK when the link is ready, else a COMM_ERR_ code. CommLastError(link) gives the COMM_ code of the last CommHandshake, CommHandshakeStep, CommSend, CommSetPlain, or CommPoll, which tells a wrong passphrase (COMM_ERR_AUTH) from a line that does not answer (COMM_ERR_TIMEOUT). CommDropped(link) counts arriving packets thrown away because they were tampered with or came unencrypted where that is refused. Channels are numbered 0 to COMM_MAX_CHANNEL.

    +

    The same link runs over the network: with net.bas in the project too, CommOpenTcp(conn) opens it on a connection from TcpConnect or TcpAccept (both ends do), with every channel, the handshake, and encryption as on a COM port. The connection stays the program's: close it with TcpClose after CommClose.

    +
    conn = TcpConnect("bbs.example.org", 2323)
    +link = CommOpenTcp(conn)
    +If CommHandshake(link, "shared secret") Then CommSend link, "hello", 5, True
    +

    CommHandshake waits until the key exchange is done (up to 30 seconds), and CommSend waits while the link has as much in flight as it can; the program's events run while they wait, as they do in DoEvents. CommHandshakeStep(link, passphrase) does one step of it and returns at once, so a program can keep working: call it from a Timer until it returns something other than COMM_PENDING -- COMM_OK when the link is ready, else a COMM_ERR_ code. CommLastError(link) gives the COMM_ code of the last CommHandshake, CommHandshakeStep, CommSend, CommSetPlain, or CommPoll, which tells a wrong passphrase (COMM_ERR_AUTH) from a line that does not answer (COMM_ERR_TIMEOUT). CommDropped(link) counts arriving packets thrown away because they were tampered with or came unencrypted where that is refused. Channels are numbered 0 to COMM_MAX_CHANNEL.

    Sub Timer1_Timer ()
         Select Case CommHandshakeStep(link, txtPass.Text)
             Case COMM_PENDING
    @@ -5298,12 +5387,12 @@ Sub cmdOK_Click ()
         PrefsClose prefs
     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.

    +

    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. FileOpsListFolders(listing) makes a new listing of just the folders in one, sorted by name, without reading the disk again (handy for a folder tree next to a file list).

    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 nothing can be saved: DVX runs from such a disk and there is no TEMP folder to keep the user folder in. FileOpsTempDir$() is the folder for scratch files ("" when there is none).

    DVX's folders: FileOpsUserDir$() is the user folder (USER in the DVX folder, or the folder DVXUSER names), where everything DVX and its programs save goes; FileOpsUserPath$(name) is a file or folder in it, for example FileOpsUserPath$("DOWNLOAD"); FileOpsUserInTemp() is True when DVX runs from a disk it cannot write to and keeps the user folder in the TEMP folder. A program's own folder there is App.User. The fonts, sounds, themes, desktop pictures, MIDI instrument banks, and screen savers come in two folders each, DVX's own (and add-on packages') in SHARED and the user's in the user folder; the kinds are FILEOPS_KIND_FONTS, FILEOPS_KIND_SOUNDS, FILEOPS_KIND_THEMES, FILEOPS_KIND_WPAPER, FILEOPS_KIND_BANKS, and FILEOPS_KIND_SAVERS. An add-on's files are in a folder of its vendor's inside a kind's folder and are named VENDOR\NAME (for example ACME\SPACE.THM). FileOpsKindDir$(kind, user) is DVX's folder of a kind (user False) or the user's (True); FileOpsKindBrowseDir$(kind) is the one a file dialog starts in (the user's when it exists); FileOpsKindFind$(kind, fileName) finds a file by name, the user's first ("" when there is none); FileOpsKindName$(kind, path) is the name alone of a file in one of the two folders, the way settings store it ("" for any other path); FileOpsKindList(kind) lists both folders as one, sorted by name, a user's file hiding DVX's of the same name (a list for FileOpsListCount, FileOpsListName$, and the rest; FileOpsListFree when done). Programs read DVX's SHARED folder but never write in it.

    -

    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.

    +

    FileOpsCopyTree, FileOpsMoveTree, and FileOpsDeleteTree do a whole folder in one call, as does FileOpsTreeSize. While one works, the rest of DVX goes on and so do the program's own events, as with DoEvents (in a control's code they wait until the call ends); if the program is ended meanwhile, the call stops and returns False. For a long copy, move, or delete with progress and questions, run 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, FileOpsJobFoldersTotal, FileOpsJobPercent, FileOpsJobBytesDone, and FileOpsJobBytesTotal show the progress, and FileOpsJobCancel stops the job. A FILEOPS_MEASURE job only counts: the files (FileOpsJobFilesTotal), the folders inside what was added (FileOpsJobFoldersTotal), and the bytes (FileOpsJobBytesTotal), changing nothing and asking nothing; FileOpsJobUnread says how many folders could not be read. A Properties dialog counts a big folder this way without stopping the program. 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.

    Dim job As Long
     
     Sub cmdCopy_Click ()
    @@ -5330,7 +5419,7 @@ Sub Timer1_Timer ()
     End Sub

    Working with DVX

    With shell.bas in the project, a program works with DVX the way Program Manager does -- a whole desktop can be written in BASIC.

    -

    Starting programs. ShellRun program, args starts a program (args is what it gets as COMMAND$), runs a DOS program, or opens a document in the program set for its type; it returns True when it started (or the DOS program will run). ShellRunProgram(program, args, folder) does the same the way a Program Manager item does: file names in args, and a document named without a folder, are looked for in folder first ("" for none), and it returns the new program's id (SHELL_RUN_DOS for a DOS program, 0 when it did not start; DVX has already said why). DOS programs are .EXE, .COM, and .BAT files (ShellIsDosProgram(path) says whether a file is one). ShellRunDos program, args, folder, pause runs one with DVX stepped aside: once your procedure has returned, DVX names the programs running besides the desktop, this one included, and asks them to close (each asks about unsaved work; No, or a program kept open, cancels the run); the screen then becomes the DOS screen until the program ends, and DVX comes back with the desktop. folder is where it runs ("" for its own folder); pause True shows "Press any key to return to DVX." when it ends. ShellMinimizeApp id runs that program minimized: its windows become icons, including the ones it opens while it starts. ShellRunCommand(commandLine) runs a whole command line (a program, in double quotes when its name has a blank, then its arguments) and returns the id too. ShellRunDialog$(history) shows DVX's Run dialog offering history (earlier commands, newest first, one per line) and returns what the user chose, "" for Cancel. ShellOpenDocument path opens a document in the program set for its type in the Control Panel.

    +

    Starting programs. ShellRun program, args starts a program (args is what it gets as COMMAND$), runs a DOS program, or opens a document in the program set for its type; it returns True when it started (or the DOS program will run). ShellRunProgram(program, args, folder) does the same the way a Program Manager item does: file names in args, and a document named without a folder, are looked for in folder first ("" for none), and it returns the new program's id (SHELL_RUN_DOS for a DOS program, 0 when it did not start; DVX has already said why). DOS programs are .EXE, .COM, and .BAT files (ShellIsDosProgram(path) says whether a file is one). ShellRunDos program, args, folder, pause runs one with DVX stepped aside: once your procedure has returned, DVX names the programs running besides the desktop, this one included, and asks them to close (each asks about unsaved work; No, or a program kept open, cancels the run); the screen then becomes the DOS screen until the program ends, and DVX comes back with the desktop. folder is where it runs ("" for its own folder); pause True shows "Press any key to return to DVX." when it ends. ShellRunDosWait(program, args, folder, pause) does the same and waits for it, as SHELL does: your program is not asked to close, its events run meanwhile, and the DOS program's exit code (its ERRORLEVEL) comes back, -1 when it did not run. ShellMinimizeApp id runs that program minimized: its windows become icons, including the ones it opens while it starts. ShellRunCommand(commandLine) runs a whole command line (a program, in double quotes when its name has a blank, then its arguments) and returns the id too. ShellRunDialog$(history) shows DVX's Run dialog offering history (earlier commands, newest first, one per line) and returns what the user chose, "" for Cancel. ShellOpenDocument path opens a document in the program set for its type in the Control Panel.

    Installed programs. ShellProgramScan(folder, skipPath) finds every program in folder and its subfolders ("" for the APPS folder) and returns a list: ShellProgramCount(list) programs, ShellProgramPath$(list, index) each one's path (index from 0), ShellProgramFree list when done (a list not freed is freed when the program ends). Give ShellAppPath$() as skipPath to leave this program out. ShellProgramName$(path) and ShellProgramDescription$(path) are the name and description a program shows, and ShellIsProgram(path) tells a program from a document. LOADPICTURE of a program gives its icon, so a PictureBox can show a grid of programs with PaintPicture. ShellProgramsFolder$() is the folder the installed programs are in (where ShellProgramScan("") looks), for example to start a Browse for a program there with DlgFileOpenIn$.

    File types. ShellAssocProgram$(extension) tells which program opens files with that extension (with ",arguments" after it when it is given any), ShellAssocCommand$(document) just the program that opens a document. ShellAssocCount() file types are set; ShellAssocExt$(index) and ShellAssocValue$(index) read them (index from 0). ShellAssocSet(extension, program) sets one and ShellAssocRemove(extension) removes one, saved at once; ShellAssocNormalize$(extension) is the extension as stored (".txt" gives "TXT"; "" when it cannot be one).

    Running programs. ShellRunningCount() says how many programs are running, and ShellRunningId(index) is the id of each one (index from 0; the same id ShellRunProgram returns). ShellAppId() is this program's own id. ShellRunningName$(id) and ShellRunningPath$(id) are the name a running program shows and the path of its .app ("" when it is not running), and ShellRunningFind(program) is the id of a running copy of a program, 0 when it is not running. ShellRunningActivate id brings a program to the front the way Task Manager's Switch To does (a minimized window is restored). ShellRunningClose id asks a program to close as if the user had closed its windows: it may ask to save its work and the user may cancel, and it is asked once your code has returned to DVX, so check from a Timer whether it is still running. ShellRunningKill id ends a program at once without asking it, the way Task Manager's End Task does: whatever it had not saved is lost, so keep it for a program that does not respond. ShellAppIsHung(id) is True when a program has been busy for 5 seconds or longer ("Not responding" in Task Manager). A program cannot end itself with ShellRunningKill (END does that). ShellAppMemory(id) is how many bytes of memory a program is using. ShellChangeCount() changes whenever a program starts or ends: check it from a Timer and update what you show when it is not the value you saw last. ShellTaskManager opens the Task Manager, ShellSystemInfo$() describes the computer (several lines), and ShellExitDvx exits DVX: every program is asked to close, each asking about unsaved work, and a program that stays open stops the exit (ask the user first).

    @@ -5388,22 +5477,34 @@ End Sub Loop End Sub

    Add-on Packages

    -

    With pkg.bas in the project, a program installs, removes, and makes add-on packages -- the .DPK files Software installs -- the way Software and Package Builder (both written in BASIC) do. PkgOpen() starts a session (a number; PkgClose ends it), and PkgError$(session) says why the last call that failed did. Lists come back as rows for a ListView's SetRows: cells separated by TAB, each row ending in a line feed.

    +

    With pkg.bas in the project, a program installs, removes, and makes add-on packages -- the .DPK files Software installs -- the way Software and Package Builder (both written in BASIC) do. PkgOpen() starts a session (a number; PkgClose ends it), and PkgError$(session) says why the last call that failed did. PkgOpenIn(root) starts one on another DVX folder (a copy being made ready on another disk, say): its installed packages and the keys its user folder trusts. Lists come back as rows for a ListView's SetRows: cells separated by TAB, each row ending in a line feed.

    What is installed. PkgInstalledRows$ lists every installed package (DVX itself is kpunch.dvx): id, name, version, kind, and summary. A package is named by its id, vendor.name (acme.chess). PkgField$(s, id, field) gives one detail (PKG_FIELD_NAME, PKG_FIELD_VERSION, PKG_FIELD_PUBLISHER, PKG_FIELD_SIGNER_NAME, ...); PkgFilesRows$, PkgDependsRows$, and PkgNeededBy$ its files, what it needs, and what needs it. PkgFileOwner$(s, fileName) is the other way: the id of the package that put a file there (its name from the DVX folder, such as "SHARED\FONTS\KPUNCH\ARIMO.DFN"; "" when none did).

    -

    Installing. PkgOfferFile(s, path) reads a package file and every package beside it (what it may need) as offers, numbered from 0, and returns the number of the one at path (-1 when it cannot be read). PkgOfferField$ and PkgOfferFilesRows$ describe an offer before it is installed; PKG_FIELD_TRUST says whether DVX trusts who signed it. PkgPlan(s, "0") plans installing it with everything it needs, checked against this DVX and the packages installed (False, with PkgError$ saying why, when it cannot be installed); PkgPlanRows$ lists what will go in, PkgPlanBytes the disk space, and PkgPlanRestart whether libraries or widgets are among them (they work from the next time DVX starts). PkgInstallStart(s) then starts the job, and PkgRunFor(s, milliseconds) runs it a little at a time, returning PKG_RUN_BUSY until it is PKG_RUN_DONE or PKG_RUN_FAILED (PkgRunMessage$ says why); PkgRunPercent and PkgRunCurrent$ show how far it is.

    -
    offer = PkgOfferFile(s, path)
    -If offer >= 0 And PkgPlan(s, CStr(offer)) Then
    -    If PkgInstallStart(s) Then
    -        Do
    -            rc = PkgRunFor(s, 60)
    -            pbJob.Value = PkgRunPercent(s)
    -            DoEvents
    -        Loop While rc = PKG_RUN_BUSY
    +

    Jobs. Reading package files, installing, removing, and making and signing packages each take a while (checking a signature alone takes a moment on a 486), so each is a job: a ...Start function starts it, and PkgRunFor(s, milliseconds) runs it a little at a time, returning PKG_RUN_BUSY until it is PKG_RUN_DONE or PKG_RUN_FAILED (PkgRunMessage$ says why); PkgRunPercent and PkgRunCurrent$ show how far it is. A session runs one job at a time.

    +

    Installing. PkgOfferFileStart(s, path) reads a package file and every package beside it (what it may need) as offers, numbered from 0; the one at path is 0, and the job fails when it cannot be read. PkgOfferField$ and PkgOfferFilesRows$ describe an offer before it is installed; PKG_FIELD_TRUST says whether DVX trusts who signed it. PkgPlan(s, "0") plans installing it with everything it needs, checked against this DVX and the packages installed (False, with PkgError$ saying why, when it cannot be installed); PkgPlanRows$ lists what will go in, PkgPlanBytes the disk space, and PkgPlanRestart whether libraries or widgets are among them (they work from the next time DVX starts). PkgInstallStart(s) then starts installing.

    +
    Function RunJob(s As Long) As Long
    +    Dim rc As Long
    +    Do
    +        rc = PkgRunFor(s, 60)
    +        pbJob.Value = PkgRunPercent(s)
    +        DoEvents
    +    Loop While rc = PKG_RUN_BUSY
    +    RunJob = rc
    +End Function
    +
    +If PkgOfferFileStart(s, path) Then
    +    If RunJob(s) = PKG_RUN_DONE And PkgPlan(s, "0") Then
    +        If PkgInstallStart(s) Then
    +            rc = RunJob(s)
    +        End If
         End If
     End If

    Removing. PkgDependents$(s, ids) lists the installed packages that need the ones going (they must go too), and PkgOrphans$(s, ids) those only they needed. PkgRemoveStart(s, ids, removeChanged) starts removing, run with PkgRunFor like an install; files changed since they were installed stay unless removeChanged, and PkgRunKept$ lists them.

    -

    Trust. A publisher signs a package with a key so people can tell it came from them. PkgTrustKey(s, keyPath) trusts a publisher's key file (.KEY) from then on.

    -

    From a package server. With pkgnet.bas in the project too, a program gets packages from a package server the way Software does. PkgNetOpen("") returns a handle on the server DVX's settings name, with the account they give; PkgNetClose frees it. PkgNetServer$, PkgNetPort, PkgNetUser$, and PkgNetPassword$ read those settings (PkgNetUrl$ is the address they make), and PkgNetSaveSettings(server, port, user, password) changes them. A job is started with PkgNetCatalogue (every package the server offers), PkgNetUpdates (what it has that is newer than the packages installed, DVX itself among them), or PkgNetFetch(n, ids) (packages by id, with everything they need that is not installed), and run with PkgNetRunFor(n, milliseconds) until it is PKGNET_DONE or PKGNET_FAILED (PkgNetError$ says why); PkgNetPercent and PkgNetCurrent$ show how far a fetch is. PkgNetRows$ lists the answer (id, name, version, kind, summary, size, and the version installed, if any), and PkgNetField$ and PkgNetDependsRows$ describe one package in it. Each package fetched is checked against the size and fingerprint the server gives, and the server's signature of it comes with it, so it is trusted or not exactly as a package file is. What was fetched is in PkgNetDir$: PkgOfferFolder(s, folder) reads every package there as the offers, and PkgOfferIndex(s, id) gives the number of the one wanted, to plan and install as above; PkgNetClear removes them afterwards. With an account (the settings', or PkgNetAccount(n, email, password) for another server), PkgNetUpload(n, path) publishes a package file for the server's review, and PkgNetMine(n) asks what the account has sent: PkgNetMineRows$ lists it (id, name, version, state, and the review's note); both sign in first.

    +

    Stopping. PkgRunCancel(s) stops a job after the package it is on: one still being unpacked is left as it was, and one whose files are already in is finished first, so keep calling PkgRunFor until it stops saying PKG_RUN_BUSY. A package being made or signed stops at once, and nothing of it is left behind.

    +

    Trust. A publisher signs a package with a key so people can tell it came from them. PkgTrustKey(s, keyPath) trusts a publisher's key file (.KEY) from then on. PkgKeysRows$(s) lists the keys DVX trusts (name, id, and "system" for DVX's own or "user" for one trusted this way), and PkgUntrustKey(s, keyId) stops trusting one of the user's.

    +

    Checking a package. PkgVerifyStart(s, path, keyPath) checks a package file without installing it, a job run with PkgRunFor: every file in it against the size and fingerprint its manifest lists, so a damaged or changed package is found. With keyPath (a .KEY file) the package must also be signed by that key, in it or beside it, or nothing starts.

    +

    Signing any data. PkgSignData$(s, bytes, secretPath) signs any bytes (a file read into a string, a message) with a secret key file, giving the signature as text; PkgCheckData(s, bytes, signature, keyPath) says whether a signature was made over those bytes by the key in keyPath, or with keyPath "" by a key DVX trusts.

    +

    Plain ZIP files. A session also lists, unpacks, and makes ordinary .ZIP files, as jobs run with PkgRunFor: PkgZipListStart(s, zipPath) reads one to its end, every file in it checked; PkgZipExtractStart(s, zipPath, folder) unpacks it into folder (a name that would leave folder stops it); PkgZipCreateStart(s, zipPath, files) makes one from rows of a name in it and the file it comes from (as PkgFolderRows$ gives them). PkgZipRows$(s) lists what the last of them read or packed: name, size, date, and "folder" for a folder.

    +

    From a package server. With pkgnet.bas in the project too, a program gets packages from a package server the way Software does. PkgNetOpen("") returns a handle on the server DVX's settings name, with the account they give; PkgNetClose frees it. PkgNetServer$, PkgNetPort, PkgNetUser$, and PkgNetPassword$ read those settings (PkgNetUrl$ is the address they make), and PkgNetSaveSettings(server, port, user, password) changes them. A job is started with PkgNetCatalogue (every package the server offers), PkgNetUpdates (what it has that is newer than the packages installed, DVX itself among them), or PkgNetFetch(n, ids) (packages by id, with everything they need that is not installed), and run with PkgNetRunFor(n, milliseconds) until it is PKGNET_DONE or PKGNET_FAILED (PkgNetError$ says why); PkgNetPercent and PkgNetCurrent$ show how far a fetch is. PkgNetRows$ lists the answer (id, name, version, kind, summary, size, and the version installed, if any), and PkgNetField$ and PkgNetDependsRows$ describe one package in it (PKG_FIELD_SHA256 is its file's fingerprint, PKG_FIELD_REPO_SIGNED whether the repository signed it). PkgNetOpenIn(server, root, downloadDir) works for another DVX folder and fetches into another folder. Each package fetched is checked against the size and fingerprint the server gives, and the server's signature of it comes with it, so it is trusted or not exactly as a package file is. What was fetched is in PkgNetDir$: PkgOfferFolderStart(s, folder) reads every package there as the offers (a job), and PkgOfferIndex(s, id) gives the number of the one wanted, to plan and install as above; PkgNetClear removes them afterwards. With an account (the settings', or PkgNetAccount(n, email, password) for another server), PkgNetUpload(n, path) publishes a package file for the server's review, and PkgNetMine(n) asks what the account has sent: PkgNetMineRows$ lists it (id, name, version, state, and the review's note); both sign in first.

    n = PkgNetOpen("")
     If PkgNetFetch(n, "acme.chess") Then
         Do
    @@ -5412,13 +5513,14 @@ If PkgNetFetch(n, "acme.chess") Then
             DoEvents
         Loop While rc = PKGNET_BUSY
     End If
    -If rc = PKGNET_DONE And PkgOfferFolder(s, PkgNetDir$(n)) > 0 Then
    +If rc = PKGNET_DONE And PkgOfferFolderStart(s, PkgNetDir$(n)) Then
    +    rc = RunJob(s)
         offer = PkgOfferIndex(s, "acme.chess")
         ' plan and install offer as above, then PkgNetClear n
     End If
    -

    Making packages. PkgBuild(s, template, files, out, secret) builds a package: template is a settings file with a [package] section (id, name, version, kind, and summary, publisher, author, licence, homepage, dvx as wanted) and a [depends] section (id = oldest version for each package it needs); files are rows of a name in the package (from the DVX folder, such as APPS\ACME\CHESS\CHESS.APP) and the file it is made from; secret is the secret key file (.SEC) to sign with, or "". PkgFolderRows$(s, folder, prefix) gives those rows for every file in a folder, and PkgKindFolder$(kind, id) the folder a package of that kind keeps its files in. PkgSign signs a package already built, PkgKeygen(s, name, base) makes a key pair (base.KEY to give out, base.SEC to keep private), and PkgKeyInfo$ reads a key file's name and id. PkgIdValid and PkgVersionValid check an id and a version before building; PkgKindName$ lists the kinds.

    +

    Making packages. PkgBuildStart(s, template, files, out, secret) starts building a package, a job run with PkgRunFor like an install (PkgRunCurrent$ is the file it is on): template is a settings file with a [package] section (id, name, version, kind, and summary, publisher, author, licence, homepage, dvx as wanted) and a [depends] section (id = oldest version for each package it needs); files are rows of a name in the package (from the DVX folder, such as APPS\ACME\CHESS\CHESS.APP) and the file it is made from; secret is the secret key file (.SEC) to sign with, or "". PkgFolderRows$(s, folder, prefix) gives those rows for every file in a folder, and PkgKindFolder$(kind, id) the folder a package of that kind keeps its files in. PkgSignStart(s, path, secret, detached) signs a package already built: into it, a job run the same way, or beside it (NAME.SIG) when detached, done at once. PkgKeygen(s, name, base) makes a key pair (base.KEY to give out, base.SEC to keep private), and PkgKeyInfo$ reads a key file's name and id. PkgIdValid and PkgVersionValid check an id and a version before building; PkgVersionCompare(a, b) says which of two versions is newer (-1, 0, or 1: "1.10.0" is newer than "1.9.2"); PkgKindName$ lists the kinds.

    Web Requests and JSON

    -

    With http.bas in the project, a program asks web servers for pages, files, and web services' answers. HttpOpen(method, url) makes a request ("GET" to fetch, "POST" to send) and returns a handle; HttpClose frees it. Before starting it, HttpAddHeader adds a header, HttpSetBody gives a body to send with its type ("application/json", say), HttpSaveTo writes the answer's body to a file instead of keeping it (with keepPart True, a file left part-way is finished rather than started again), and HttpSetTimeout sets how many seconds without progress mean the server is not answering (30 to start with). HttpStart starts it (False for an address that is not http://; there is no encryption), and HttpRunFor(h, milliseconds) runs it a little at a time, returning HTTP_BUSY until it is HTTP_DONE or HTTP_FAILED:

    +

    With http.bas in the project, a program asks web servers for pages, files, and web services' answers. HttpOpen(method, url) makes a request ("GET" to fetch, "POST" to send) and returns a handle; HttpClose frees it. Before starting it, HttpAddHeader adds a header, HttpSetBody gives a body to send with its type ("application/json", say), HttpSetBodyFile sends a file's bytes instead (a piece at a time, so a file of any size), HttpSaveTo writes the answer's body to a file instead of keeping it (with keepPart True, a file left part-way is finished rather than started again), and HttpSetTimeout sets how many seconds without progress mean the server is not answering (30 to start with). HttpStart starts it (False for an address that is not http://; there is no encryption), and HttpRunFor(h, milliseconds) runs it a little at a time, returning HTTP_BUSY until it is HTTP_DONE or HTTP_FAILED:

    h = HttpOpen("GET", "http://example.com/news.txt")
     IF HttpStart(h) THEN
         DO
    @@ -5431,7 +5533,7 @@ IF rc = HTTP_DONE AND HttpStatus(h) = 200 THEN txtNews.Text = HttpBody$(h)
     IF rc = HTTP_FAILED THEN MsgBox HttpError$(h)
     HttpClose h

    HTTP_DONE means the server answered: HttpStatus is how (200 OK, 404 not found, ...), HttpBody$ what it sent, HttpReplyHeader$ one of its headers, and HttpUrl$ where the answer came from when the server sent the request elsewhere (redirects are followed). HTTP_FAILED means no answer came, and HttpError$ says why. HttpBytes and HttpTotal (-1 until the server says) show progress.

    -

    With json.bas in the project, JsonParse(text) reads JSON -- what most web services answer with -- and returns a handle (0 when it is not JSON: JsonError$ says why and on which line); JsonFree frees it. The other functions find a value by its path from the top: member names and item numbers from 0, such as "packages[3].version". JsonType says what is there (JSON_STRING, JSON_NUMBER, JSON_ARRAY, ..., JSON_NONE for nothing), JsonText$ gives a string, JsonNumber a number, JsonBool True or False, JsonCount how many items an array or members an object has, and JsonKey$(h, path, i) the name of an object's member i. JsonQuote$(text) puts text in quotes with its special characters escaped, for building JSON to send:

    +

    With json.bas in the project, JsonParse(text) reads JSON -- what most web services answer with -- and returns a handle (0 when it is not JSON: JsonError$ says why and on which line); JsonFree frees it. The other functions find a value by its path from the top: member names and item numbers from 0, such as "packages[3].version". JsonType says what is there (JSON_STRING, JSON_NUMBER, JSON_ARRAY, ..., JSON_NONE for nothing), JsonText$ gives a string, JsonNumber a number, JsonBool True or False, JsonCount how many items an array or members an object has, and JsonKey$(h, path, i) the name of an object's member i. JsonQuote$(text) puts text in quotes with its special characters escaped, for building JSON to send. Text comes and goes as DVX characters: an accented letter or a line-drawing character in a reply is the same character in JsonText$ ("?" stands for one DVX does not have), and JsonQuote$ writes such characters so that any web service reads them right:

    h = JsonParse(HttpBody$(req))
     FOR i = 0 TO JsonCount(h, "packages") - 1
         lstPackages.AddItem JsonText$(h, "packages[" & CStr(i) & "].name")
    @@ -5448,7 +5550,7 @@ body = "{" & JsonQuote$("name") & ": " &am
     

    Mouse: SysSetMouse wheelDir, dblClickMs, accel, mickeys, wheelStep applies a whole mouse setup at once: the scroll wheel's direction (1 or -1, the values of PREFS_CHOICE_WHEEL), the double-click time in milliseconds, the acceleration (a value of PREFS_CHOICE_ACCEL), how far the mouse moves for 8 pixels of pointer movement (8 is normal, higher is slower), and the lines scrolled per wheel notch (1 to 10). An accel or mickeys of 0 leaves that one as it is. SysMouseWheelDir, SysMouseDoubleClick, SysMouseAccel, SysMouseMickeys, and SysMouseWheelStep read the setup in effect. MOUSE_WHEEL_STEP_MIN and MOUSE_WHEEL_STEP_MAX are the limits of the wheel step, and MOUSE_WHEEL_STEP_DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS, and MOUSE_SPEED_DEFAULT are DVX's defaults (what a Default button puts back).

    The pointer and the keyboard: SysMouseX() and SysMouseY() are where the mouse pointer is on the screen, and SysMouseButtons() the buttons held (vbLeftButton, vbRightButton, and vbMiddleButton bits) -- also while the pointer is over another program's window, where your MouseMove events do not see it. SysSetMousePos x, y moves the pointer (it is kept on the screen). SysShiftState() is the Shift, Ctrl, and Alt keys held now (vbShiftMask, vbCtrlMask, and vbAltMask bits). SysSendKey(keyCode, shift) types a key into the window that has the focus as if the user pressed it, like SendKeys in other BASICs: keyCode as a KeyDown event gets it -- a character code such as ASC("a") or 13 for Enter, or a vbKey constant such as vbKeyF1 -- and shift the vbShiftMask, vbCtrlMask, and vbAltMask bits held with it (SysSendKey ASC("s"), vbCtrlMask is Ctrl+S). The keys arrive in order, one at a time, once your code has returned to DVX; up to 64 can wait, and SysSendKey returns False when that many are waiting. A program cannot press the mouse buttons: to "click" a control, call its event procedure or set its value.

    Memory: SysMemoryTotalKb() is the computer's memory in KB and SysMemoryFreeKb() how much of it is free (-1 when it cannot be told).

    -

    Libraries: some parts of DVX are optional (Setup's "What to install" page; Software lists each as one package), and so are their libraries: Networking has dvxnet, http, and pkgnet (net.bas, http.bas, pkgnet.bas), Databases has dvxsql (sql.bas), and Communications has conn and xfer (conn.bas, xfer.bas). SysLibraryLoaded(libName) is True when the library a DECLARE LIBRARY line names (any case) is installed and was started with DVX. A program that can do without such a library asks first and leaves out what needs it: calling a function of a library that is not there stops the program with "External function not found". Software, for one, keeps its package server pages off without pkgnet.

    +

    Libraries: some parts of DVX are optional (Setup's "What to install" page; Software lists each as one package), and so are their libraries: Networking has dvxnet, http, and pkgnet (net.bas, http.bas, pkgnet.bas), Databases has dvxsql (sql.bas), and Communications has conn and xfer (conn.bas, xfer.bas). SysLibraryLoaded(libName) is True when the library a DECLARE LIBRARY line names (any case) is installed (DVX loads a library when it is first needed, so asking loads it). A program that can do without such a library asks first and leaves out what needs it: calling a function of a library that is not there stops the program with "External function not found". Software, for one, keeps its package server pages off without pkgnet.

    If SysLibraryLoaded("pkgnet") Then
         handle = PkgNetOpen("")
     Else
    @@ -5555,6 +5657,22 @@ Loop Until quitting
    shipX = shipX + JoyX(1) \ 8192 If JoyButton(1, 1) Then FireShot End Sub
    +

    A program can also hear of every change instead of reading the joystick: JoyWatch joy, True starts keeping that joystick's changes (False stops), and JoyEvent() gives the next one since it was last asked, each once and in order (the last 64 are kept): JOY_EVENT_AXIS (an axis moved), JOY_EVENT_BUTTON_DOWN, JOY_EVENT_BUTTON_UP, JOY_EVENT_HAT, or JOY_EVENT_NONE when nothing has changed. JoyEventJoy() and JoyEventIndex() say which joystick and which axis, button, or hat, and JoyEventValue() an axis's new position or a hat's direction. A button pressed and let go between two Timer ticks still gives both.

    +
    Sub Form1_Load ()
    +    ok = JoyWatch(1, True)
    +End Sub
    +
    +Sub Timer1_Timer ()
    +    Dim kind As Long
    +
    +    kind = JoyEvent()
    +
    +    Do While kind <> JOY_EVENT_NONE
    +        If kind = JOY_EVENT_BUTTON_DOWN And JoyEventIndex() = 1 Then FireShot
    +        If kind = JOY_EVENT_AXIS And JoyEventIndex() = 1 Then shipSpeed = JoyEventValue() \ 8192
    +        kind = JoyEvent()
    +    Loop
    +End Sub

    When no joystick is connected the keyboard stands in for one (the arrow keys, with Left Ctrl, Space, Left Shift, and Z as buttons), and it only answers while one of the program's windows is active. Joysticks are calibrated, and the keyboard joystick's keys chosen, on the Control Panel's Game Controllers tab. QBasic's STICK and STRIG work too, without joystick.bas.

    joystick.bas can also set joysticks up, as the Control Panel does. JoyHats(joy) counts the hats, JoyAxisPresent(joy, axis) is True when an axis answers, JoyConnected(joy) is False once a joystick stops answering, and JoyCanCalibrate(joy) is True for one with analog axes. For calibrating, JoyRawAxis(joy, axis) gives an axis's raw reading and JoyRawButtons(joy) the buttons held (1 for button 1, 2 for button 2, 4 for button 3, ... added up); JoySetCalibration(joy, axis, minV, centerV, maxV) takes the readings at one end, the middle, and the other end, and JoyClearCalibration(joy, axis) lets the axis find its own range again. JoyDeadZone(joy) and JoySetDeadZone(joy, percent) are the dead zone round the middle (0 to 50 percent). JoyChoiceCount, JoyChoiceLabel$, JoyChoice, and JoySetChoice list and change the game port's mode (JOY_CHOICE_GAMEPORT) and when the keyboard joystick stands in (JOY_CHOICE_FALLBACK); JoyKeyCount and JoyKeyName$(index) list the keys the keyboard joystick can use, and JoyKbdSlot(slot) and JoySetKbdSlot(slot, keyIndex) its keys for up, down, left, right, and buttons 1 to 4 (slots 0 to 7). These lists are numbered from 0, so they fill a DropDown in order. JoyKeyIndex(keyName) gives a key's number by its name (-1 when unknown), JoyKeyCode(index) its raw key code, which GameKeyDown in game.bas takes, and JoyKeyNameOf$(rawCode) the name of a raw key code. JoyIsKeyboard(joy) is True for the keyboard standing in for a joystick, and JoyCalibration(joy, axis, part) reads back an axis's calibration: part JOY_CAL_MIN, JOY_CAL_CENTER, or JOY_CAL_MAX of the readings set with JoySetCalibration, or of the range the axis found by itself (0 for a joystick that cannot be calibrated). JoySetting$(key, default) and JoySetSetting key, value reach any other joystick setting; JOY_KEY_POLL_HZ is the key of how often joysticks are read, JOY_POLL_HZ_MIN to JOY_POLL_HZ_MAX times a second (JOY_POLL_HZ_DEFAULT normally), and dead zones go up to JOY_DEADZONE_MAX percent (JOY_DEADZONE_DEFAULT normally). After a new game port mode, JoyRescan() looks for joysticks again; it returns the new count, or -1 while another program is using a joystick. Every change applies at once; JoySettingsStore handle puts them all into a settings handle from prefs.bas for the program to save, and JoySettingsLoad handle takes them back from one, to undo.

    Sub cmdCenter_Click ()
    @@ -5581,8 +5699,17 @@ Sub cmdFire_Click ()
         End If
     End Sub

    WAV files must be uncompressed (8 or 16 bits, mono or stereo); VOC files may be 8- or 16-bit. QBasic's BEEP, SOUND, and PLAY work too, without sound.bas.

    -

    PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator), "running", and "record" (it records sound) are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits", "channels", and "recordsources" (the inputs it records from) are numbers. SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played.

    -

    A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan, and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. SoundPause voice holds a sound's voice where it is, silent, and SoundResume voice goes on from there (a tone cannot be paused). A program's sounds are freed when it ends.

    +

    PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator), "running", and "record" (it records sound) are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits", "channels", and "recordsources" (the inputs it records from) are numbers. SoundDriver$() is the short name of the sound driver playing ("sb", "speaker", ...; "" when none is), and SoundStats(field) what the mixer has done since the device started: "renders" (blocks of sound made), "frames" (samples made), "underruns" (stream samples that were due but not yet written), "clipped" (samples too loud), "dropped" (sounds dropped because too many played at once), and "playing" (voices playing now). SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played (DVX and the program's events go on meanwhile, as in DoEvents; in a control file's code, and once the program is ending, it does not wait).

    +

    A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); it reads and decodes the whole file before it returns, so for a long recording (a minute or more) use MusicLoad and MusicPlay (music.bas), which play a WAV or VOC file as they read it; SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundLoadData(bytes) loads one from the bytes of a WAV or VOC file held in a string, such as ResGetData$ (resource.bas) or HttpBody$ (http.bas) gives. SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds, SoundRate(sound), SoundBits(sound), and SoundChannels(sound) how it was recorded, and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan, and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. SoundPause voice holds a sound's voice where it is, silent, and SoundResume voice goes on from there (a tone cannot be paused). A program's sounds are freed when it ends.

    +

    To hear of every sound ending, call SoundEnded() from a Timer: it gives the next of the program's voices that ended since it was last asked (each once, in the order they ended; the last 64 are kept), or 0 when none has, and SoundEndReason() says why that one ended: SOUND_END_FINISHED (it played to its end), SOUND_END_STOPPED (StopWave, StopAllSounds, or its sound was freed), or SOUND_END_DROPPED (too many sounds played at once and it was the quietest).

    +
    Sub Timer1_Timer ()
    +    Dim v As Long
    +    v = SoundEnded()
    +    Do While v <> 0
    +        If v = engine And SoundEndReason() = SOUND_END_FINISHED Then engine = SoundPlay(idle, 60, 0, True)
    +        v = SoundEnded()
    +    Loop
    +End Sub
    Dim Shared laser As Long
     
     Sub Form1_Load ()
    @@ -5601,13 +5728,18 @@ End Sub
    pending = Mid$(pending, taken + 1) If Len(pending) = 0 Then pending = MakeMoreSound() End Sub
    -

    A program can record sound from the sound card's microphone, line, or CD input into a WAV file. SoundCaps("record") says whether the card can, and SoundCaps("recordsources") which inputs it has (the bit 2 ^ RECORD_MIC, RECORD_LINE, or RECORD_CD set for each). RecordStart(fileName, rate, bits, channels, inputSource) starts recording (0 for 22,050 Hz, 16-bit, mono) and returns True, or False with SoundError$ saying why; the card records at its nearest, which RecordRate(), RecordBits(), and RecordChannels() tell. While it records, RecordLength() is how long the recording is (milliseconds) and RecordLevel() the loudest sound since it was last asked (percent: a level meter); RecordLost() counts samples dropped because DVX was too busy. RecordStop() ends it and returns True when the file was all written; Recording() is True in between. Most sound cards record or play, not both, so nothing plays while a program records, and one program records at a time. A program's recording ends when the program does.

    +

    A melody of tones -- a game's tune or a sound effect -- plays without a gap or a stumble in a note queue. NotesOpen(wave, volume) opens one in a waveform (SOUND_WAVE_SQUARE, SOUND_WAVE_TRIANGLE, or SOUND_WAVE_NOISE) and returns its number, or 0. NotesAdd(queue, hertz, milliseconds) adds a tone (0 hertz for a rest; fractions count) and returns True, or False when 256 are already waiting; each lasts exactly as long as asked, as long as the next is added before the one playing ends. NotesSetTone(queue, wave, volume) changes the waveform and volume (percent of the queue's) of the tones added from then on, so they change in time with the tune. NotesQueued(queue) is how many tones and rests have not finished (0 once it has gone quiet) and NotesSpace(queue) how many more fit; NotesVoice(queue) is its voice, for SoundSetVolume and SoundSetPan. NotesClear queue silences it and forgets what it holds, and NotesClose queue closes it. PLAY's V and @ commands give PLAY tunes a volume and a waveform too.

    +
    q = NotesOpen(SOUND_WAVE_TRIANGLE, 80)
    +For f = 200 To 800 Step 50
    +    ok = NotesAdd(q, f, 15)          ' a rising sweep, 15 ms a step
    +Next f
    +

    A program can record sound from the sound card's microphone, line, or CD input into a WAV file, or into memory. SoundCaps("record") says whether the card can, and SoundCaps("recordsources") which inputs it has (the bit 2 ^ RECORD_MIC, RECORD_LINE, or RECORD_CD set for each). RecordStart(fileName, rate, bits, channels, inputSource) starts recording (0 for 22,050 Hz, 16-bit, mono) and returns True, or False with SoundError$ saying why; the card records at its nearest, which RecordRate(), RecordBits(), and RecordChannels() tell. While it records, RecordLength() is how long the recording is (milliseconds) and RecordLevel() the loudest sound since it was last asked (percent: a level meter); RecordLost() counts samples dropped because DVX was too busy. RecordStop() ends it and returns True when the file was all written; Recording() is True in between. With a file name of "", RecordStart records into memory instead: RecordRead$(maxBytes) takes up to maxBytes of what was recorded and not yet taken, as raw sound data in the recording's format (as SoundCreate and SoundStreamWrite take it), and RecordQueued() is how many bytes are waiting. Take them often, from a Timer: about two seconds are kept, and what does not fit is lost (RecordLost()). A program can show the sound as it comes, change it, or send it over the network. Most sound cards record or play, not both, so nothing plays while a program records, and one program records at a time. A program's recording ends when the program does.

    If RecordStart("NOTE.WAV", 0, 0, 0, RECORD_MIC) Then
         ' ... a Timer shows RecordLength() and RecordLevel()
         ok = RecordStop()
     End If

    On a computer with an FM synthesizer chip (AdLib and most Sound Blasters), a program can play instruments of its own. FmInfo$(field) describes the chip: "description" (like "OPL3 FM synthesizer at 388h", "" when there is none), "voices", "free" (voices no program has), "stereo", and "owned" (one program has the whole chip). FmAlloc() takes one of its two-operator voices (0 when none is free), FmWrite(voice, register, value) sets one of the voice's registers (the FM_ constants: FM_MOD_CHAR, FM_CAR_LEVEL, FM_FNUM_LOW, FM_KEY_BLOCK with FM_KEY_ON, ...; True when it worked), and FmFree voice gives it back. A player of chip music files can take the whole chip with FmRawAcquire() (False while another program has it), set any register with FmRawWrite(register, value) (bank * FM_BANK_REGS + register; the second bank only on an OPL3) and give it back with FmRawRelease. A frequency number for FM_FNUM_LOW and FM_KEY_BLOCK is hertz * 2 ^ 20 / (FM_CHIP_HZ * 2 ^ block).

    -

    On a wavetable sound card (a Gravis UltraSound, a Sound Blaster AWE32 or AWE64), a program can play recorded sounds on the card's own voices from the card's memory. WtInfo$(field) describes the card: "description" ("" when there is none), "voices", "free" (voices no program has), "memory" and "freememory" (bytes), "level" (how many centibels louder a voice at full volume is than the loudest recorded sound the card plays: 0 when it is the same or not known), and what the card does itself ("envelopes", "lfos", "filter", "effects", "8bit", "rom"; True or False as text). WtMemAlloc(bytes, bits) takes memory for samples of 8 or 16 bits (its address, -1 when there is no room), WtUpload(address, pcm, bits) copies 16-bit mono samples there (a binary string, two bytes a sample; True when it worked), and WtMemFree address gives it back. WtAlloc() takes a voice (0 when none is free); WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) plays a sample on it from startFrame (0: its first; loopMode 0 plays once, 1 loops, 3 loops until WtRelease; pitch in cents, 0 for 44,100 samples a second and 1200 an octave higher; attenuation in centibels, 0 loudest and 60 half as loud; pan -100 to 100). WtSetPitch, WtSetAttenuation, and WtSetPan change a playing voice, WtStop silences it, WtActive(voice) is True while it sounds, and WtFree voice gives it back. Everything a program took is given back when it ends.

    +

    On a wavetable sound card (a Gravis UltraSound, a Sound Blaster AWE32 or AWE64), a program can play recorded sounds on the card's own voices from the card's memory. WtInfo$(field) describes the card: "description" ("" when there is none), "voices", "free" (voices no program has), "memory", "freememory" (the largest free block), and "freetotal" (all free, in blocks maybe apart; bytes), "level" (how many centibels louder a voice at full volume is than the loudest recorded sound the card plays: 0 when it is the same or not known), and what the card does itself ("envelopes", "lfos", "filter", "effects", "8bit", "rom"; True or False as text). WtMemAlloc(bytes, bits) takes memory for samples of 8 or 16 bits (its address, -1 when there is no room), WtUpload(address, pcm, bits) copies 16-bit mono samples there (a binary string, two bytes a sample; True when it worked), and WtMemFree address gives it back. WtAlloc() takes a voice (0 when none is free); WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) plays a sample on it from startFrame (0: its first; loopMode 0 plays once, 1 loops, 3 loops until WtRelease; pitch in cents, 0 for 44,100 samples a second and 1200 an octave higher; attenuation in centibels, 0 loudest and 60 half as loud; pan -100 to 100); on a card with a General MIDI sample ROM (WtInfo$("rom")) the address may be WT_ADDR_ROM plus the bytes from the ROM's start. WtSetPitch, WtSetAttenuation, and WtSetPan change a playing voice, WtStop silences it, WtActive(voice) is True while it sounds, and WtFree voice gives it back. WtShape(voice, field, value) shapes the voice's notes as a SoundFont instrument does, one value by name, where the card can (WtInfo$ "envelopes", "lfos", "filter", "effects"): the volume envelope ("voldelay", "volattack", "volhold", "voldecay", "volsustain", "volrelease"), the modulation envelope ("moddelay" to "modrelease", "modenvpitch", "modenvfilter"), the modulation and vibrato LFOs ("modlfodelay", "modlfofreq", "modlfopitch", "modlfofilter", "modlfovolume", "viblfodelay", "viblfofreq", "viblfopitch"), the filter ("filterfc", "filterq"), and the effects ("reverb", "chorus"), in the units sound.bas lists. A value is kept for the voice's next WtStart; the filter, the effects, and the LFO depths also change a note that is sounding. Everything a program took is given back when it ends.

    sound.bas also changes DVX's own sound settings, as the Control Panel's Sounds page does. DVX plays a sound for each of SoundEventCount() events, numbered from 0: SoundEventLabel$(eventIdx) names one ("Start DVX", "Error", ...) and SoundEventKey$(eventIdx) gives its key in DVX.INI. With a settings handle from prefs.bas (PrefsLoad(PrefsDvxIni$())), SoundEventFile$(handle, eventIdx) is the event's sound file ("" for none) and SoundSetEventFile handle, eventIdx, fileName changes it; after the program saves the file, SoundEventsReload makes DVX play the new sounds. SoundTestFile(fileName) plays a file the way DVX plays its event sounds (0 when it cannot, and SoundError$() says why), and SoundFolder$() is the folder of DVX's own sounds. SoundMasterVolume() and SoundSetMasterVolume percent are the volume of all sound from every program. SoundDriverCount() reads the list of sound drivers and SoundDriverName$(index) names each; SoundDriverAuto$() is the setting that means "find the best one". SoundMidiCount() and SoundMidiLabel$(index) list where MIDI music can play, SoundMidiInCount() and SoundMidiInLabel$(index) where MIDI input can come from, and SoundModulesCount() and SoundModulesLabel$(index) where music modules can play. SoundSettingDriver$(handle), SoundSettingMidi(handle), SoundSettingModules(handle), and SoundSettingVolume(handle) read the sound driver, MIDI choice, modules choice, and master volume from the settings, and SoundSettingsStore handle, driver, midi, modules, volume stores them (a new driver or MIDI choice takes effect when DVX next starts, a modules choice with the next music); SoundSettingMidiIn(handle) and SoundSettingMidiInStore handle, midiIn read and store the MIDI input choice (it takes effect when DVX next starts).

    Dim ini As Long
     Dim i As Long
    @@ -5620,7 +5752,7 @@ Next i

    See: BEEP, SOUND, and PLAY

    Music

    With music.bas in the project, a program plays music files: tracker modules (MOD, S3M, XM, and IT files, made of recorded instrument sounds) wherever the computer can play sampled sound (on a Gravis UltraSound's or Sound Blaster AWE's own voices, which takes the computer little time), MIDI files (.MID, types 0 and 1) on the computer's music synthesizer, or on a built-in synthesizer that sounds much the same when there is none, and WAV and VOC sound files wherever the computer can play sampled sound (a long WAV recording is read from the disk as it plays, so it needs no memory). Several programs can play music at once, and everything a program plays stops when it ends.

    -

    MusicLoad(fileName) loads a music file and returns its music number, or MUSIC_FAILED (0) when it could not be loaded; MusicError$() then says why. MusicPlay(music, loopIt) plays it from where it is (the start after loading or stopping); with loopIt True it starts over at the end until it is stopped. MusicStop(music) stops and goes back to the start, MusicPause(music) and MusicResume(music) pause and go on, and MusicPlaying(music) is True while it plays. MusicVolume(music, volume) sets its volume in percent (0 to 100). MusicPosition(music) and MusicLength(music) give the place and the length in milliseconds, and MusicSeek(music, milliseconds) goes to a place (a module goes to the start of the part that holds it). MusicFree(music) stops a music and forgets it.

    +

    MusicLoad(fileName) loads a music file and returns its music number, or MUSIC_FAILED (0) when it could not be loaded; MusicError$() then says why. MusicLoadData(bytes) does the same with the bytes of a music or sound file held in a string (from ResGetData$ or HttpBody$, say). MusicPlay(music, loopIt) plays it from where it is (the start after loading or stopping); with loopIt True it starts over at the end until it is stopped. MusicStop(music) stops and goes back to the start, MusicPause(music) and MusicResume(music) pause and go on, and MusicPlaying(music) is True while it plays. MusicEnded(music) is True once after the music played to its end (not when it was stopped) since MusicPlay started it: a Timer that asks it can start the next tune on time. MusicVolume(music, volume) sets its volume in percent (0 to 100). MusicPosition(music) and MusicLength(music) give the place and the length in milliseconds, and MusicSeek(music, milliseconds) goes to a place (a module goes to the start of the part that holds it). MusicFree(music) stops a music and forgets it.

    MusicLoadMacro(macro) makes a music from a music macro string, the kind PLAY takes, with its notes on the General MIDI instruments its I commands name (the piano before any I): the tune can then be played in the background, paused, stopped, and made quieter like any music. MIDI music plays on the instruments chosen in Control Panel; MusicSetBank music, bank plays one music on another instrument bank, named as in the banks folders ("" is DVX's standard bank), before the music first plays (MusicError$ says when the bank is missing or damaged). A SoundFont (.SF2 or .SBK) gives the recorded instruments for a wavetable card or the built-in SoundFont synthesizer, as can a Gravis UltraSound's own patches: MusicPatchSet$() names them (their ULTRASND.INI, "" when there are none), and MusicRomBank$() names the instruments built into a Sound Blaster AWE card ("" when Creative's software is not installed).

    tune = MusicLoadMacro("T150 I41 O4 L8 CEG>C4")
     MusicSetBank tune, "DMXOPL.OP2"
    @@ -5709,16 +5841,24 @@ Sub Timer1_Timer ()
     End Sub
      Function                              Description
       --------                              -----------
    -  HostLookup$(hostName)                 The address of a name, such as "192.0.2.10", or "" when it is not found.
    +  HostLookup$(hostName)                 The address of a name, such as "192.0.2.10", or "" when it is not found. Waits for the answer; other programs keep running.
    +  HostLookupStart(hostName)             The same without waiting: a lookup handle (0 when too many are under way).
    +  HostLookupPoll(lookup)                NET_BUSY while the lookup goes on, NET_DONE once found, NET_FAILED (NetError$ says why).
    +  HostLookupResult$(lookup)             The address found, once HostLookupPoll says NET_DONE.
    +  HostLookupClose lookup                Free a lookup handle (or give the lookup up).
       HostName$(address)                    The name of an address; the address itself when no name is known for it.
       NetAddress$()                         This computer's address ("" while it has none).
       NetError$()                           Why the last call failed, in plain words.
       TcpConnect(host, port)                Connect; a handle, or 0. Waits up to 20 seconds; other programs keep running.
    +  TcpConnectStart(host, port)           The same without waiting: the connection's handle at once (0 when it cannot start).
    +  TcpConnectPoll(handle)                NET_BUSY while the host is looked up and the connection made, NET_DONE once it is (the handle then works as TcpConnect's), NET_FAILED (close it).
       TcpListen(port)                       Wait for connections on port; a listening handle, or 0.
       TcpAccept(handle)                     A connection that arrived on a listening handle, or 0 when none is waiting.
    -  TcpSend(handle, bytes)                Send text or any other bytes; the bytes sent, or -1.
    +  TcpSend(handle, bytes)                Send text or any other bytes; the bytes sent, or -1. Waits while the connection's buffer is full.
    +  TcpSendSome(handle, bytes)            Send what the connection takes now, never waiting: the bytes taken from the start (0 while it is full), or -1.
       TcpPending(handle)                    Bytes waiting to be read (-1 for a bad handle).
       TcpRecv$(handle)                      What has arrived (up to 4096 bytes), or "".
    +  TcpPeek$(handle)                      What has arrived (up to 4096 bytes), left waiting for the next TcpRecv$: look for a line's end or a message's length first.
       TcpConnected(handle)                  1 while the connection is open, 0 once the other side closed it.
       TcpShutdown(handle)                   Tell the other side nothing more will be sent; what it sends can still be read.
       TcpPeer$(handle)                      "address:port" of the other side ("" when not connected).
    @@ -5732,6 +5872,25 @@ End Sub
    UdpFrom$() "address:port" of the sender of the last UdpRecv$ message. UdpClose handle Close a UDP handle.

    The strings sent and received may hold any bytes, CHR$(0) too, so binary protocols work as well as text. Every connection a program opens is closed for it when the program ends.

    +

    HostLookup$, TcpConnect, and TcpSend wait until they are done. Other programs keep running meanwhile, but yours does not: its windows do not answer, and the first lookup after DVX starts can also wait for the network to get its settings (DHCP). To stay responsive, start the lookup or connection and check on it from a Timer:

    +
    Dim conn As Long
    +
    +Sub cmdConnect_Click ()
    +    conn = TcpConnectStart("bbs.example.org", 23)
    +    Timer1.Enabled = conn <> 0
    +End Sub
    +
    +Sub Timer1_Timer ()
    +    Select Case TcpConnectPoll(conn)
    +        Case NET_DONE
    +            Timer1.Enabled = False
    +            lblStatus.Caption = "Connected"
    +        Case NET_FAILED
    +            Timer1.Enabled = False
    +            lblStatus.Caption = "Could not connect: " & NetError$()
    +            TcpClose conn
    +    End Select
    +End Sub

    A program can also edit the network settings the way the Control Panel's Network page does, and show the network's state. The settings are kept in DVX's settings file, so NetConfigLoad and NetConfigStore take a handle from prefs.bas (add prefs.bas to the project too). NetConfigLoad gives a settings handle (0 when none is free); read and change its fields by name, check them, store them, and restart the network only when they changed:

    Dim prefs As Long
     Dim cfg As Long
    @@ -5770,6 +5929,7 @@ End Sub
    NetVectorCount() / NetVector(index) The packet driver interrupts to choose from: index 0 is 0 (find it), then each one the settings accept. NetRestart() Stop the network and start it with the saved settings. True when it is running. NetStop Stop the network (every program's connections are closed) until NetRestart. + NetState() The network's state as a number: NET_STATE_STOPPED, NET_STATE_NO_DRIVER (no network card driver found), NET_STATE_CONFIGURING (waiting for an address from the network), or NET_STATE_UP. NetStatus$(field) The network's state as text: "message" (in plain words), "up" and "link" ("-1" / "0"), "dhcp", "address", "netmask", "gateway", "dns1", "dns2", "hostname", "driver" (the network card's driver), "mac" (its hardware address), "vector", "sockets", "rxpackets", "rxbytes", "rxdropped", "txpackets", "txbytes". "" for an unknown field.

    Settings handles a program leaves open are freed for it when it ends.

    CommRecvEncrypted(handle, channel) tells whether the data the last CommRecv$ on that channel returned can be trusted. It returns True when every byte of it arrived encrypted and authenticated, and also when that CommRecv$ returned nothing or CommRecv$ has not been called on the channel yet. It returns False when any byte arrived unencrypted, which can only happen before CommHandshake or on a channel opened with CommSetPlain, and for an invalid handle or a channel outside 0-127.

    @@ -5797,7 +5957,7 @@ Sub Timer1_Timer () If ConnZmodemRequest(conn) Then download = ConnReceiveFiles(conn, XFER_ZMODEM, "DOWNLOAD", XFER_EXIST_RENAME, False) End Sub

    File Transfers

    -

    With xfer.bas in the project, a program sends and receives files with XMODEM, XMODEM-1K, YMODEM, or ZMODEM. A transfer is a number: XferSendFile and XferReceive start one on a serial port opened with SerOpen (comm.bas), and ConnSendFiles and ConnReceiveFiles start one on a connection (conn.bas), with the options Terminal has: escaping every control character, asking the receiver to continue interrupted files, and what to do with a file that is already there. On a serial port XferPoll moves the transfer along; on a connection ConnPoll does. XferStatus tells where it is (XFER_RUNNING until it ends with XFER_DONE, XFER_FAILED, or XFER_CANCELLED), and XferBytes, XferSize, XferFile$, XferFileIndex, XferFileCount, XferBatchBytes, XferBatchSize, XferResumedAt, and XferErrors show its progress; XferMessage$ says how it ended. A receive with XFER_EXIST_ASK stops in XFER_ASKING when a file already exists: XferAskSize, XferAskRemoteSize (the size the sender offers, -1 when it did not say), and XferCanResume describe it, and XferAnswer continues with an XFER_EXIST_ choice. XferCancel stops a transfer and XferClose frees it.

    +

    With xfer.bas in the project, a program sends and receives files with XMODEM, XMODEM-1K, YMODEM, or ZMODEM. A transfer is a number: XferSendFile and XferReceive start one on a serial port opened with SerOpen (comm.bas), and ConnSendFiles and ConnReceiveFiles start one on a connection (conn.bas). XferSendFiles and XferReceiveFiles (a serial port) and the Conn ones take the options Terminal has: a batch of files (one path per line), escaping every control character, asking the receiver to continue interrupted files, and what to do with a file that is already there. On a serial port XferPoll moves the transfer along; on a connection ConnPoll does. XferStatus tells where it is (XFER_RUNNING until it ends with XFER_DONE, XFER_FAILED, or XFER_CANCELLED), and XferBytes, XferSize, XferFile$, XferFileIndex, XferFileCount, XferBatchBytes, XferBatchSize, XferResumedAt, and XferErrors show its progress; XferMessage$ says how it ended. A receive with XFER_EXIST_ASK stops in XFER_ASKING when a file already exists: XferAskSize, XferAskRemoteSize (the size the sender offers, -1 when it did not say), and XferCanResume describe it, and XferAnswer continues with an XFER_EXIST_ choice. XferCancel stops a transfer and XferClose frees it.

    XferCrc16(bytes) and XferCrc32(bytes) give the check values the protocols use (CRC-16 as XMODEM computes it, 0 to 65535; the CRC-32 of ZMODEM and ZIP files as a Long, negative when its top bit is set) for any bytes. XferSafeName$(name, folder) gives the name a receive into folder stores a sender's file name as: no folder or device name; a long name, its case kept, where folder's disk takes long names (a character a name cannot hold becomes _), else an 8.3 name of characters every disk accepts.

    Lists You Draw Yourself

    With listhelp.bas in the project, a list or icon view a program draws itself -- in a UserControl or a PictureBox -- follows the same rules as ListBox and ListView. ListSelCreate(multi, count) makes a selection for count items (several at a time when multi is True) and returns a handle; it remembers which items are selected and which one is current (the one the keyboard moves). Items are numbered from 0. Give it the clicks and keys, and ask it what to draw:

    @@ -5858,26 +6018,34 @@ End Sub
      Function                                            Description
       --------                                            -----------
       SecRandom$(count)                                   count random bytes, good for keys and salts (RND is not). SecAddEntropy bytes stirs something unpredictable (key timings, mouse positions) into the generator.
    -  SecDhCreate()                                       A key exchange with fresh keys (a few seconds on a slow computer); a handle, or 0.
    +  SecX25519PublicKey$(secretKey)                      The public key to send the other side, for a secret key of SEC_X25519_KEY_SIZE random bytes (SecRandom$).
    +  SecX25519$(secretKey, otherPublicKey)               The secret both sides now share, which nobody listening can work out ("" for a key the other side should not have sent). About a sixth of a second on a slow computer. Make keys from it with SecHkdf$.
    +  SecX25519Start(secretKey, otherPublicKey)           The same without the wait: SecX25519Step(h) then does a ninth of the work a call, returning SEC_X25519_PENDING until it is done (SEC_OK); SecX25519End$(h) gives the secret (with otherPublicKey "", the public key) and frees the handle.
    +  SecAeadLock$(key, nonce, extra, bytes)              bytes sealed: encrypted with a SEC_AEAD_KEY_SIZE-byte key and a SEC_AEAD_NONCE_SIZE-byte nonce, followed by a tag that proves them, and extra (sent in the clear), unchanged. Use a new nonce for every message: SecRandom$(SEC_AEAD_NONCE_SIZE) is safe.
    +  SecAeadUnlock$(key, nonce, extra, sealed)           What SecAeadLock$ sealed, or "" when anything was changed (or the key, nonce, or extra differs).
    +  SecEd25519KeyPair$(seed)                            A signing key made from SecRandom$(SEC_ED25519_SEED_SIZE); keep it private. SecEd25519PublicKey$(key) is the key to give out.
    +  SecEd25519Sign$(key, bytes)                         The signature of bytes; SecEd25519Check(signature, publicKey, bytes) is True only for exactly those bytes signed with that key.
    +  SecDhCreate()                                       The older key exchange (Diffie-Hellman), with fresh keys (a few seconds on a slow computer); a handle, or 0.
    +  SecDhCreateStart()                                  The same without the wait: SecDhStep(dh) then computes the keys a slice at a time, returning SEC_DH_PENDING until they are ready (SEC_OK).
       SecDhPublicKey$(dh)                                 This side's public key (SEC_DH_KEY_SIZE bytes), to send to the other side.
       SecDhComputeSecret(dh, otherPublicKey)              Combine with the other side's public key: SEC_OK, or SEC_ERR_PARAM for a key that is not acceptable.
    +  SecDhComputeStart(dh, otherPublicKey)               The same a slice at a time: SEC_DH_PENDING (or SEC_ERR_PARAM at once), then SecDhStep as above.
       SecDhSecret$(dh)                                    The secret both sides now share, which nobody listening can work out. Make keys from it with SecHkdf$; never use it as a key itself. SecDhFree dh frees the handle.
    -  SecCipherCreate(key)                                A cipher with a SEC_XTEA_KEY_SIZE-byte key; 0 for a key of another length.
    +  SecCipherCreate(key)                                The older cipher (XTEA), with a SEC_XTEA_KEY_SIZE-byte key; 0 for a key of another length. It does not show tampering: add SecHmac$, or seal instead.
       SecCipherCrypt$(cipher, bytes)                      Encrypt -- or decrypt, the same call -- bytes. SecCipherSetCounter cipher, n sets the position in the key stream; the same key and position must never encrypt two different messages. SecCipherFree cipher frees it.
       SecSha256$(bytes)                                   The SHA-256 hash (SEC_SHA256_SIZE bytes). For data in pieces: SecSha256Open, SecSha256Add h, piece, then SecSha256End$(h), which also frees the handle.
       SecHmac$(key, bytes)                                The HMAC-SHA256 signature of bytes with key; SecHmacOpen(key), SecHmacAdd, and SecHmacEnd$ for pieces.
       SecHkdf$(salt, secret, info, length)                length bytes (up to SEC_HKDF_MAX_OUT) of keys made from secret; a different info gives different keys.
       SecEqual(a, b)                                      True when a and b hold the same bytes, compared in the same time wherever they differ (for signature checks).
    -
    ' Both sides: make keys, swap public keys (here over a TCP connection), agree a cipher key
    -Dim dh As Long, cipher As Long, key As String
    -dh = SecDhCreate()
    -TcpSend conn, SecDhPublicKey$(dh)
    -' ... receive the other side's key into theirs ...
    -If SecDhComputeSecret(dh, theirs) = SEC_OK Then
    -    key = SecHkdf$("", SecDhSecret$(dh), "chat cipher", SEC_XTEA_KEY_SIZE)
    -    cipher = SecCipherCreate(key)
    -End If
    -SecDhFree dh
    +
    ' Both sides: make keys, swap public keys (here over a TCP connection), agree a sealing key
    +Dim mine As String, theirs As String, key As String, nonce As String
    +mine = SecRandom$(SEC_X25519_KEY_SIZE)
    +TcpSend conn, SecX25519PublicKey$(mine)
    +' ... receive the other side's public key into theirs ...
    +key = SecHkdf$("", SecX25519$(mine, theirs), "chat", SEC_AEAD_KEY_SIZE)
    +' Send a message: the nonce, then the sealed bytes
    +nonce = SecRandom$(SEC_AEAD_NONCE_SIZE)
    +TcpSend conn, nonce & SecAeadLock$(key, nonce, "", "Hello")

    Handles a program leaves open are freed, and their keys wiped, when it ends.

    Settings Files

    With prefs.bas in the project, a program keeps its settings in an INI file the way DVX's own programs do. PrefsLoad(path) reads the file once (a file that is not there reads as empty); PrefsGet$, PrefsGetInt, and PrefsGetBool read a key of a section (each with the value to use when it is missing), and PrefsSet, PrefsSetInt, PrefsSetBool, and PrefsRemove change it in memory. PrefsSave writes it back, keeping comments and every line it did not change. To write a whole new file -- dropping sections that are no longer wanted -- fill a PrefsCreate handle and save it with PrefsSaveAs: the old file is replaced only once the new one is completely written. PrefsClose frees a handle. IniRead and IniWrite (built in) do one value at a time and read or write the whole file each time; prefs.bas is the faster choice for many settings.

    @@ -5895,7 +6063,7 @@ PrefsClose ini Next i cboAccel.ListIndex = PrefsChoiceIndexOfValue(PREFS_CHOICE_ACCEL, SysMouseAccel())

    Help Files

    -

    With help.bas in the project, HelpCompile(source, output) compiles a help source file (.dhs) into a help file (.hlp), True when it worked; HelpCompileMessages$() gives the compiler's errors and warnings, one per line, each with the file and line ("error: NOTES.DHS:1: directive .h1 outside of a topic"). HelpView file opens a help file in the DVX Help Viewer (a file named without a folder is looked for beside the program; "" opens the DVX user guide). HelpViewTopic file, topicId opens it at a topic, the way F1 does. A Help Viewer that is already open shows the file in its window instead of a second copy starting.

    +

    With help.bas in the project, HelpCompile(source, output) compiles a help source file (.dhs) into a help file (.hlp), True when it worked; its .image pictures are looked for in the source's folder. HelpCompileMessages$() gives the compiler's errors and warnings, one per line, each with the file and line ("error: NOTES.DHS:1: directive .h1 outside of a topic"). While a big file compiles, the rest of DVX and the program's own events go on, as with DoEvents: one compile runs at a time, so a second HelpCompile meanwhile returns False at once, and the compile stops if the program ends. HelpView file opens a help file in the DVX Help Viewer (a file named without a folder is looked for beside the program; "" opens the DVX user guide). HelpViewTopic file, topicId opens it at a topic, the way F1 does. A Help Viewer that is already open shows the file in its window instead of a second copy starting.

    With hlpfile.bas, a program reads help files itself -- everything a help viewer needs. HlpOpen(path) opens a help file and returns a handle (0 when it is not one); HlpClose closes it (a program's open files are closed when it ends). Topics are numbered from 0: HlpTopicCount, HlpTopicTitle$ and HlpTopicId$ (the name links and HelpTopic use), HlpFindTopic(handle, topicId) (-1 when there is none), and HlpStartTopic(handle, topicId), the topic to show first (topicId when the file has it, else the file's own start page). HlpTocCount, HlpTocTitle$, HlpTocDepth, HlpTocTopic, and HlpTocExpanded give the table of contents in order -- a depth of 0 is the top level, a topic of -1 a heading that only groups others -- ready to fill a TreeView. HlpIndexCount, HlpIndexKeyword$, and HlpIndexTopic give the keyword index (sorted), for a list or DlgChoice. HlpSearch(handle, words) searches the titles, text, and keywords as the Help Viewer does and returns how many topics have every word; HlpSearchTopic(handle, n) gives them.

    To show a topic, put a HelpPane control on the form and call HelpPane1.ShowTopic handle, topic: headings, paragraphs, lists, notes, code, tables, pictures, and links look as they do in the Help Viewer, and the text wraps to the control's width. A click on a link, or Enter on one reached with Tab, fires the HelpPane's LinkClick event with the ID of the topic the link names; the program decides what to do, usually show that topic and remember it for a Back button. A program that draws topics itself reads them with HlpLoadTopic(handle, topic) and HlpRecordType, HlpRecordFlags, HlpRecordText$, and HlpRecordLink$ (the HLP_REC_ constants), saves a picture for LOADPICTURE with HlpRecordImageSave, and wraps text with HlpWrap$.

    Dim hlp As Long
    @@ -6051,7 +6219,7 @@ End Sub
    ToolTipText String R/W Tooltip text shown after the mouse hovers over the control. Empty string or unset = no tooltip. HelpTopic String R/W Help topic opened by F1 while the control has the focus. Empty = the form's HelpTopic. F1 uses the project's help file. DragMode Integer R/W vbManual (0, the default): the program starts a drag with the Drag method. vbAutomatic (1): pressing the left mouse button on the control starts a drag. See Drag and Drop below. - DragIcon Picture R/W Picture shown under the mouse while the control is dragged: a picture file name (BMP, PNG, JPEG, or GIF) or a Picture object from LOADPICTURE (Set works too). Reading it gives a Picture object, which stands for the file name where a value is needed. Empty = a dotted outline the size of the control. + DragIcon Picture R/W Picture shown under the mouse while the control is dragged: a picture file name (BMP, PNG, JPEG, or GIF) or a Picture object from LOADPICTURE or LOADPICTUREDATA (Set works too). Reading it gives a Picture object, which stands for the file name where a value is needed. Empty = a dotted outline the size of the control. Parent Form R The form the control is on (Text1.Parent.Caption). Tag String R/W Any text the program wants to keep with the control (DVX does nothing with it), as in Visual Basic. Also in the .frm file. MousePointer Integer R/W The mouse pointer shown over the control (and the controls inside it that have none of their own): vbDefault (0, the control's own pointer), vbArrow (1), vbCrosshair (2), vbSizeNESW (6), vbSizeNS (7), vbSizeNWSE (8), vbSizeWE (9), vbHourglass (11), or vbNoDrop (12). A form's MousePointer is used where no control has one, and Screen.MousePointer over every form of the program. Change it in MouseMove to show different pointers over parts of a control. Also in the .frm file; another value is an error. @@ -6102,7 +6270,7 @@ End Sub

    With DragMode set to vbAutomatic, pressing the left mouse button on the control starts the drag, and the control gets no Click, MouseDown, or MouseUp from the left button. With DragMode vbManual, start the drag yourself, usually in MouseDown. A drag started while the mouse button is up ends at the next click. During a drag no mouse events other than DragOver and DragDrop are raised.

    Drop targets only react: nothing is moved or copied for you. Use Source in DragDrop to decide what to do, for example read its Name or Caption, or check what kind of control it is with TypeOf Source Is TextBox (see Operators).

    Note that vbCancel is the MsgBox Cancel value (2), which as a Drag action means vbEndDrag; cancel a drag with Drag 0.

    -

    DragIcon takes a file name or a picture loaded with LOADPICTURE: Set Label1.DragIcon = LoadPicture("drag.bmp"). A missing file or one that is not a picture raises its error at LOADPICTURE; a file name that cannot be read when the drag starts shows the outline instead.

    +

    DragIcon takes a file name or a picture loaded with LOADPICTURE or LOADPICTUREDATA: Set Label1.DragIcon = LoadPicture("drag.bmp"). A missing file or one that is not a picture raises its error at LOADPICTURE; a file name that cannot be read when the drag starts shows the outline instead.

    ' Label1 has DragMode = 1 (vbAutomatic) in the form designer.
     Sub List1_DragOver(Source As Control, X As Integer, Y As Integer, State As Integer)
         If State = vbEnter Then Label2.Caption = "Drop to add " & Source.Caption
    @@ -6190,7 +6358,7 @@ End Sub
    vbDstInvert &H550009 NOT destination vbGrayCopy &H1000000 The picture greyed, as DVX shows a disabled picture (DVX only) vbTransparentCopy &H2000000 Every pixel of the picture but the key colour: a colour given after the opcode, else the colour of the picture's top-left pixel (DVX only) -

    vbGrayCopy and vbTransparentCopy copy the picture at its own size (width and height are not used): Picture1.PaintPicture "ship.bmp", x, y, 32, 32, 0, 0, 32, 32, vbTransparentCopy, vbMagenta draws a sprite.

    +

    vbGrayCopy and vbTransparentCopy copy the picture at its own size (width and height are not used): Picture1.PaintPicture "ship.bmp", x, y, 32, 32, 0, 0, 32, 32, vbTransparentCopy, vbMagenta draws a sprite. A picture file (or a LOADPICTURE picture) drawn again and again, a sprite in a game loop say, is read from the disk only the first time while the program runs and the file does not change (about a megabyte of pictures is kept that way).

    Any of the 256 raster operations of Windows can be given by its number; the bits work on the pixel values, so with fewer than 32 bits a pixel the results follow the display's colour format.

    Picture1.PaintPicture Picture2, 0, 0, 32, 32, 0, 0, 32, 32, vbSrcInvert   ' XOR the picture in
     Picture1.PaintPicture Picture2, 0, 0, 32, 32, 0, 0, 32, 32, vbSrcInvert   ' and out again
    @@ -6556,12 +6724,16 @@ End Sub WindowState Integer 0 vbNormal (0), vbMinimized (1, shown only as an icon at the bottom of the screen), or vbMaximized (2, filling the screen). Set on a hidden form (or in the .frm file), it takes effect when the form is shown; reading it then gives the state the form will have. Controls Collection (none) The form's controls (read-only), in the order they were made: Me.Controls.Count, Me.Controls(1) (the first), Me.Controls("Text1"), For Each c In Me.Controls. Elements of a control array are in it too but have no name key. Each use makes a new Collection; changing it does not change the form. DesignMode Boolean False True puts the form in design mode, for a form designer: see Designing Forms below. Not in .frm files. - Icon Picture (DVX's) The picture shown for the window while it is minimized: a picture file name (BMP, PNG, JPEG, or GIF) or a picture from LOADPICTURE (Set works too); "" puts DVX's back. Reading it gives a Picture object. Also in the .frm file. A file that is not a picture is an error. + Icon Picture (DVX's) The picture shown for the window while it is minimized: a picture file name (BMP, PNG, JPEG, or GIF) or a picture from LOADPICTURE or LOADPICTUREDATA (Set works too); "" puts DVX's back. Reading it gives a Picture object. Also in the .frm file. A file that is not a picture is an error. Tag String "" Any text the program wants to keep with the form, as for controls. MousePointer Integer 0 The mouse pointer over the form where its controls have none (see MousePointer under Common Properties). FontName String "System" The form's font: the font of every control on it that has none of its own, and of the form's Print and TextWidth. FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru go with it, as for controls (see Fonts under Common Properties). Also in the .frm file. ActiveControl Control (none) The control of the form that has the keyboard focus (read-only), Nothing when none of them has it: Me.ActiveControl.SelText for an Edit menu's Copy. Screen.ActiveControl is the focused control of the whole program. Not in .frm files. - DesignSelection String "" The name of the control (Text1, or Text1(2) in a control array) whose selection handles are drawn while the form is in design mode; "" for none. Not in .frm files. + DesignSelection String "" The name of the control (Text1, or Text1(2) in a control array) whose selection handles are drawn while the form is in design mode; "" for none. Not in .frm files. + MaxWidth Integer 0 The widest the user can make the window, by dragging or maximizing; 0 is the screen's width. A window wider than a new MaxWidth shrinks to it. Also in the .frm file. + MaxHeight Integer 0 The tallest the window can be made, as MaxWidth; 0 is the screen's height. + GameInput Boolean False True for a game: while the form has the keyboard, every key goes to it -- none to F1 (help), F10 or Alt (the menu bar), menu shortcuts, or DVX's shortcut keys. Only Alt+Tab, Ctrl+Esc, and Alt+F4 still reach DVX. Also in the .frm file. + FullScreen Boolean False True gives the form the whole screen: a video mode as large as the form's inside (its controls and drawing, without the title bar), the form shown on it at the largest whole multiple of its size that fits, centred on black, kept up to date as it changes. The mouse works on it as in the window, and every key goes to it as with GameInput. A hidden form is shown first. When the user switches to another program (Alt+Tab, Ctrl+Esc), or the program shows a message box or another form, the desktop comes back; the form gets the whole screen again with the focus. Reads False when no video mode could be set (the form stays in its window). False gives the desktop back. Run time only; see the FullScreen event.

    Maximizing a window, or restoring a maximized one, changes its size, so Resize fires. Minimizing does not change the size and fires no Resize. A minimized window keeps its maximized state: set WindowState = vbMaximized to bring it back filling the screen, vbNormal to bring it back at its normal size.

    frmTerminal.Show
     frmTerminal.WindowState = vbMinimized   ' starts as an icon
    @@ -6578,6 +6750,7 @@ frmTerminal.WindowState = vbMinimized ' starts as an icon DragDrop Source As Control, X As Integer, Y As Integer Fires when a dragged control is dropped on an empty part of the form. X and Y are where, in the form. DragOver Source As Control, X As Integer, Y As Integer, State As Integer Fires while a dragged control moves over an empty part of the form. State is vbEnter, vbLeave, or vbOver. NewArgs (none) Fires when the running program is handed new arguments: COMMAND$ has changed (see New Arguments below). + FullScreen State As Integer With FullScreen True: dvxFullScreenActive (1) when the form has the whole screen (draw anything that must look new then), dvxFullScreenSuspended (2) when the desktop came back while another window has the focus, dvxFullScreenEnded (0) when the form's video mode could not be set again (FullScreen is False now). DesignMouse Action As Integer, Button As Integer, X As Integer, Y As Integer, CtrlName As String, Handle As Integer In design mode: the mouse was pressed, moved, released, or double-clicked in the form. See Designing Forms below. DesignKey KeyCode As Integer, Shift As Integer In design mode: a key was pressed in the form (KeyCode and Shift as in KeyDown). DesignPropertyChanged CtrlName As String, PropertyName As String In design mode: the code of a UserControl on the form called PropertyChanged. See Designing Forms below. @@ -6802,6 +6975,7 @@ Count = 1
      Property     Type      Description
       ----------   -------   -------------------------------------------
       Cols         Integer   Number of character columns (1-512). Setting Cols or Rows gives the terminal a new size, starting as a new terminal: empty screen and scrollback, modes the host set reset (its colors and connection stay); set the form's AutoSize to True afterwards to fit the window to it.
    +  Music        Boolean   Whether ANSI music sequences (ESC[M, ESC[N, ESC[| followed by PLAY text, as BBSes send) are recognised (True at first): each raises the Music event instead of being shown. False: ESC[M is always Delete Line, and ESC[N and ESC[| do nothing.
       Rows         Integer   Number of character rows (1-512).
       Scrollback   Integer   Maximum number of scrollback lines. Older lines are dropped (default 500). Setting it discards the existing history.

    Type-Specific Methods

    @@ -6809,12 +6983,18 @@ Count = 1 ------ ----------- Clear Clear the terminal screen and reset the cursor to the home position. Copy() Copy the selected text to the clipboard and clear the selection. Returns False when nothing is selected. - Paste Send the clipboard's text to the attached connection as if it were typed (new lines as Enter), as Ctrl+V does. + Paste Send the clipboard's text to the attached connection as if it were typed (new lines as Enter), as Ctrl+V does. With no connection attached it goes to the Send event. Poll Poll the attached communication channel for incoming bytes and feed them into the ANSI parser. ResetColors Back to the standard 16 colors. SetColor index%, color& The color (RGB(), as ForeColor takes it) of text attribute index (0-15, in CGA order: black, blue, green, cyan, red, magenta, brown, light gray, then their bright forms). An index outside 0-15 is ignored. - Write text$ Write a string to the terminal with full ANSI escape processing. -

    No default event. Uses common events only.

    + Write text$ Write a string to the terminal with full ANSI escape processing. Every byte of the string is written, CHR$(0) included (a CHR$(0) itself is padding and shows nothing, as on other terminals), so text read from a connection of the program's own can be written as it came. A very long text appears over the next few moments, a part at a time, so the program and the rest of DVX keep going; Clear after it still clears all of it. +

    Type-Specific Events

    +
      Event                    Description
    +  -----                    -----------
    +  Bell                     A bell character (BEL, CHR$(7)) arrived. The terminal makes no sound itself: the program decides (Beep, a sound of its own, or nothing).
    +  Music (Macro As String)  An ANSI music sequence arrived (Music True): Macro is its PLAY text, to play with PLAY or MusicLoadMacro.
    +  Send (Bytes As String)   The terminal has bytes for the host and no connection is attached: the keys the user typed, a paste, or the terminal's answer to the host asking where the cursor is. A program with a connection of its own (a TCP socket, a modem it drives itself) sends Bytes on. Bytes may hold any character, CHR$(0) included.
    +

    No default event. The common events are raised too.

    Serial Communication

    The conn.bas library connects a Terminal the way the Terminal program does -- a serial port, a modem, the secure link, or Telnet -- with capture, answerback, and file transfers (ConnAttach; see Include Libraries). For a packet link of your own, use the comm.bas library:

    Dim link As Long
    @@ -6827,6 +7007,15 @@ If link <> 0 Then
         End If
     End If

    The passphrase is optional. Both ends must give the same passphrase, or both give none; otherwise CommHandshake returns False. See comm.bas for the full communications API.

    +

    A Connection of Your Own

    +

    Without an attached connection, Write shows what the program received and the Send event hands over what the terminal would send back:

    +
    Sub Sock_Data ()
    +    Terminal1.Write ReceivedText$
    +End Sub
    +
    +Sub Terminal1_Send (Bytes As String)
    +    SendToHost Bytes
    +End Sub

    Example

    Begin Terminal Terminal1
         MinWidth  = 640
    @@ -6955,7 +7144,8 @@ End Sub
    -------- ---- ----------- BorderStyle Enum 0 - None: no frame, the control is exactly the size of the drawing. 1 - FixedSingle (the default): a sunken frame 2 pixels wide around the drawing. Mouse coordinates and drawing positions start inside the frame either way. ImageHeight Integer Height of the drawing in pixels (read-only). After Load it is the loaded image's height. - ImageWidth Integer Width of the drawing in pixels (read-only). After Load it is the loaded image's width. + ImageWidth Integer Width of the drawing in pixels (read-only). After Load it is the loaded image's width. + Picture String The picture file the canvas shows ("" when it shows none). Setting it to a file name, or to LoadPicture(path$), loads that file as Load does; LoadPictureData(bytes$) shows a picture from bytes; setting it to "" (or LoadPicture("")) clears the canvas to white. Clear and Resize wipe the picture, so Picture becomes "". The default property: Picture1 = "logo.bmp" loads it.

    Load and Save return 0 on success and -1 on failure (missing or unreadable file, unsupported format, path that cannot be written). Call them as functions to check the result, or as statements to ignore it. A failed Load leaves the drawing unchanged.

    Type-Specific Methods

      Method                                        Description
    @@ -6972,6 +7162,7 @@ End Sub
    Save(path$) Save the canvas: BMP when path$ ends in .bmp, PNG otherwise. Returns 0 on success, -1 on failure. SetPenColor color% Set the drawing color for subsequent operations. SetPenSize size% Set the pen/brush size in pixels (1 to 255; larger values use 255). + SetPictureData(bytes$ [, name$]) Show the picture held in bytes$ (the bytes of a BMP, PNG, JPEG, or GIF file, such as HttpBody$ or ResGetData$ gives) as Load shows a file, without keeping a Picture object as Picture = LoadPictureData(bytes$) does. Picture then reads name$ ("" without it). Returns True, or False (the drawing unchanged) when the bytes are not a picture. SetPixel x%, y%, color% Set the color of a single pixel.

    Default Event: Click

    Example

    @@ -7016,7 +7207,7 @@ End Sub

    ComboBox

    VB Equivalent: ComboBox -- DVX Widget: combobox (editable text field + drop-down list, default maxLen 256)

    -

    A combination of a single-line text input and a drop-down list. The user can either type a value or pick one from the list. Selecting a list item copies its text into the edit field. Supports the same AddItem / RemoveItem / Clear / List methods as ListBox.

    +

    A combination of a single-line text input and a drop-down list. The user can either type a value or pick one from the list. Selecting a list item copies its text into the edit field. Supports the same AddItem / RemoveItem / Clear / SetItems / List methods as ListBox.

    Its font must be fixed-pitch, every character the same width; the designer's Font dialog offers only those families for it. Given a proportional font it draws in the system font instead.

    Type-Specific Properties

      Property    Type      Description
    @@ -7031,7 +7222,8 @@ End Sub
    Clear Remove all items. List(index%) Return the text of the item at the given index. ListCount() Return the number of items in the list. - RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection (the text is kept). + RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection (the text is kept). + SetItems items$ Replace all the items at once from a "|"-separated list ("Red|Green|Blue"; every "|" separates two items, "" empties the list). Nothing is selected afterwards. The text is kept.

    Default Event: Click

    Example

    Begin ComboBox Combo1
    @@ -7053,6 +7245,13 @@ End Sub

    Data

    VB Equivalent: Data -- DVX Widget: data | Name Prefix: Data

    A data access control that connects to a SQLite database and provides record navigation. Other controls bind to a Data control via their DataSource and DataField properties. The Data control cache is populated by calling Refresh. Move operations and Refresh auto-save the current row if dirty; a move is refused while the save fails, and Refresh discards an edit that cannot be saved. Updates find the record by the key value it was loaded with, so the key column itself can be edited; a key stored as a number in a column without a declared type, or a missing (NULL) key, is still found as long as it identifies exactly one record. Writing a field back with the value it already has is not an edit and does not fire Validate. While a Validate handler runs, MoveFirst, MoveLast, MoveNext, MovePrevious, AddNew, Refresh, and Update on the same Data control are ignored (they would save, and validate, the same record again); Delete is allowed. A RecordSource that is a SELECT query is read-only, and edits made to its records are dropped when the current record changes.

    +

    A big record set is read a part at a time: Refresh reads the first 64 records, and the rest arrive while your program and DVX go on, so RecordCount grows until every record is in. MoveLast, AddNew, Update, and Delete read the rest first, and MoveNext and AbsolutePosition read as far as they need. Until every record is in, other programs (and SQLExec) cannot change that database: their changes fail with "database is locked".

    +

    EOF and BOF work as in Visual Basic, so a loop like this reads every record once:

    +
    Data1.Refresh
    +Do Until Data1.EOF
    +    total = total + Val(Data1.GetField("amount"))
    +    Data1.MoveNext
    +Loop

    See Data Binding for details

    Type-Specific Properties

      Property       Type      Description
    @@ -7061,19 +7260,19 @@ End Sub
    RecordSource String Table name or SQL SELECT query for the recordset. KeyColumn String Primary key column name (used for UPDATE/DELETE operations). Caption String Text displayed on the navigator bar. - BOF Boolean True if the current position is the first record or there are no records (read-only). - EOF Boolean True if the current position is past the last record or there are no records (read-only). + BOF Boolean True after MovePrevious at the first record (which stays the current record), or when there are no records; moving onto a record clears it (read-only). + EOF Boolean True after MoveNext at the last record (which stays the current record), or when there are no records; moving onto a record clears it (read-only). MasterSource String Name of a master Data control (for master-detail binding). MasterField String Column in the master recordset to read for the filter value. DetailField String Column in this recordset that matches the master field. - RecordCount Long The number of records (read-only). + RecordCount Long The number of records read so far: all of them once the rest has arrived, or after MoveLast (read-only). FieldCount Integer The number of columns (read-only). AbsolutePosition Long The current record, 0 for the first (-1 when there are none). Setting it moves there (saving pending changes first, as the Move methods do) and fires Reposition; a value out of range does nothing.

    Type-Specific Methods

      Method         Parameters   Description
       ------------   ----------   -------------------------------------------
       MoveFirst      (none)       Navigate to the first record.
    -  MoveLast       (none)       Navigate to the last record.
    +  MoveLast       (none)       Navigate to the last record (every record is read first).
       MoveNext       (none)       Navigate to the next record.
       MovePrevious   (none)       Navigate to the previous record.
       AddNew         (none)       Append a blank record and move to it.
    @@ -7112,13 +7311,14 @@ End Sub

    DBGrid

    VB Equivalent: DBGrid -- DVX Widget: dbgrid | Name Prefix: DBGrid

    -

    A data-bound grid that displays records from a Data control in a tabular format. Columns auto-populate from the query results. Bind the grid using the DataSource property. Click a column header to sort; drag a column border to resize. The highlighted row is always the bound Data control's current record: selecting a row moves the Data control's cursor, and navigating the Data control moves the highlight. Re-binding keeps the sort order and column settings, and a column width set by dragging a border is kept. Text wider than its column is clipped. The mouse wheel scrolls the rows without changing the current record.

    +

    A data-bound grid that displays records from a Data control in a tabular format. Columns auto-populate from the query results. Bind the grid using the DataSource property. Click a column header to sort; drag a column border to resize. The highlighted row is always the bound Data control's current record: selecting a row moves the Data control's cursor, and navigating the Data control moves the highlight. Re-binding keeps the sort order and column settings, and a column width set by dragging a border is kept. A column's automatic width fits its header and its first 200 records; text wider than its column is clipped. Records that arrive later (a Data control reads a big record set a part at a time) appear as they come, in the grid's sort order. The mouse wheel scrolls the rows without changing the current record.

    Type-Specific Properties

      Property     Type      Description
       ----------   -------   -------------------------------------------
       DataSource   String    Name of the Data control that supplies records.
       GridLines    Boolean   Show or hide grid lines between cells (default: True).
    -  SelectedRow  Integer   Index of the current record's row (-1 = no records). Setting it moves the Data control's cursor; negative values are ignored.
    + SelectedRow Integer Index of the current record's row (-1 = no records). Setting it moves the Data control's cursor; negative values are ignored. + Text String The current record's first visible column ("" with no record), read-only. It is the grid's default property, so Print DBGrid1 prints it. For another field, read the Data control's recordset or the field's bound control.

    Type-Specific Methods

      Method             Parameters                        Description
       ----------------   -------------------------------   -------------------------------------------
    @@ -7157,7 +7357,7 @@ End Sub

    DropDown

    DVX Extension -- DVX Widget: dropdown (non-editable drop-down list)

    -

    A read-only drop-down list. Unlike ComboBox, the user cannot type free text; they can only select from the provided items. Supports AddItem / RemoveItem / Clear / List. Use the Up and Down keys to change the choice. F4, Alt+Down, Space, or Enter opens the list; in the open list use the arrow keys to move and Enter (or F4, Alt+Down) to pick. Esc closes the list without changing the choice.

    +

    A read-only drop-down list. Unlike ComboBox, the user cannot type free text; they can only select from the provided items. Supports AddItem / RemoveItem / Clear / SetItems / List. Use the Up and Down keys to change the choice. F4, Alt+Down, Space, or Enter opens the list; in the open list use the arrow keys to move and Enter (or F4, Alt+Down) to pick. Esc closes the list without changing the choice.

    Type-Specific Properties

      Property    Type      Description
       ---------   -------   -------------------------------------------
    @@ -7169,7 +7369,8 @@ End Sub
    Clear Remove all items. List(index%) Return the text of the item at the given index. ListCount() Return the number of items in the list. - RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection. + RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection. + SetItems items$ Replace all the items at once from a "|"-separated list ("Red|Green|Blue"; every "|" separates two items, "" empties the list). Nothing is selected afterwards.

    Default Event: Change (fires when the user picks a new item; Click fires only when the arrow opens the popup)

    Example

    Begin DropDown DropDown1
    @@ -7234,10 +7435,14 @@ End Sub

    Type-Specific Properties

      Property      Type      Description
       -----------   -------   -------------------------------------------
    -  Picture       Picture   Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget, or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged.
    +  Picture       Picture   Path to an image file to load (or a Picture object from LOADPICTURE or LOADPICTUREDATA); a program, widget, or library file gives its icon. Reads back as a Picture object for the picture shown, which stands for the file name ("" for a picture from bytes) where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged.
       ImageWidth    Integer   Width of the loaded image in pixels (read-only).
       ImageHeight   Integer   Height of the loaded image in pixels (read-only).
    -

    No type-specific methods or events.

    +

    Type-Specific Methods

    +
      Method                           Description
    +  ------                           -----------
    +  SetPictureData(bytes$ [, name$])  Show the picture held in bytes$ (the bytes of a BMP, PNG, JPEG, or GIF file, such as HttpBody$ or ResGetData$ gives), as Picture = LoadPictureData(bytes$) does but without keeping a Picture object, so it suits showing many pictures in turn. Picture then reads name$ ("" without it). Returns True, or False (the picture unchanged) when the bytes are not a picture.
    +

    No type-specific events.

    Default Event: Click

    Example

    Begin Toolbar Toolbar1
    @@ -7261,13 +7466,17 @@ End Sub

    Type-Specific Properties

      Property      Type      Description
       -----------   -------   -------------------------------------------
    -  Picture       Picture   Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget, or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. A disabled Image is drawn grayed (its transparent color stays transparent).
    +  Picture       Picture   Path to an image file to load (or a Picture object from LOADPICTURE or LOADPICTUREDATA); a program, widget, or library file gives its icon. Reads back as a Picture object for the picture shown, which stands for the file name ("" for a picture from bytes) where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. A disabled Image is drawn grayed (its transparent color stays transparent).
       Stretch       Boolean   When True, an image larger than the control is scaled down to fit, keeping its proportions; a smaller image stays at natural size. When False, the image is shown at natural size.
       ImageWidth    Integer   Width of the loaded image in pixels (read-only).
       ImageHeight   Integer   Height of the loaded image in pixels (read-only).
       MaskColor     Long      The transparent colour (an RGB value): with UseMaskColor True, pixels of this colour are not drawn, so the form shows through.
       UseMaskColor  Boolean   True leaves out the pixels of MaskColor (default False).
    -

    No type-specific methods or events.

    +

    Type-Specific Methods

    +
      Method                           Description
    +  ------                           -----------
    +  SetPictureData(bytes$ [, name$])  Show the picture held in bytes$ (the bytes of a BMP, PNG, JPEG, or GIF file, such as HttpBody$ or ResGetData$ gives), as Picture = LoadPictureData(bytes$) does but without keeping a Picture object, so it suits showing many pictures in turn. Picture then reads name$ ("" without it). Returns True, or False (the picture unchanged) when the bytes are not a picture.
    +

    No type-specific events.

    Default Event: Click

    Example

    Begin Image Image1
    @@ -7305,7 +7514,8 @@ End

    Type-Specific Properties

      Property    Type      Description
       ---------   -------   -------------------------------------------
    -  ListIndex   Integer   Index of the currently selected item (-1 = no selection).
    + ListIndex Integer Index of the currently selected item (-1 = no selection). + Text String The selected item ("" with none); the default property, so Print List1 prints it. Setting it selects the first item that is that text (case ignored); with none, the selection stays as it is.

    Type-Specific Methods

      Method            Parameters                                Description
       ---------------   ---------------------------------------   -------------------------------------------
    @@ -7345,9 +7555,11 @@ End Sub

    VB Equivalent: ListView -- DVX Widget: listview

    A multi-column list with column headers. Supports sorting, multi-select, and drag-to-reorder.

    Type-Specific Properties

    -
      Property    Type      R/W   Description
    -  ---------   -------   ---   -------------------------------------------
    -  ListIndex   Integer   R/W   Index of the currently selected row (-1 = none).
    +
      Property     Type      R/W   Description
    +  ----------   -------   ---   -------------------------------------------
    +  ListIndex    Integer   R/W   Index of the currently selected row (-1 = none).
    +  SortColumn   Integer   R/W   The column the list is sorted by, -1 when it is unsorted: what SetSort or the user's last click on a column header chose. Setting it sorts by that column in the current SortOrder (ascending when the list was unsorted); -1 unsorts the list.
    +  SortOrder    Integer   R/W   The order of the sort: 0 = none, 1 = ascending, 2 = descending (a click on the same header again reverses it). Setting it re-sorts the SortColumn in that order; 0 unsorts the list, and an unsorted list stays unsorted.

    Type-Specific Methods

      Method                                        Description
       ------                                        -----------
    @@ -7371,7 +7583,7 @@ End Sub

    Type-Specific Events

      Event         Parameters               Description
       -----------   ----------------------   -------------------------------------------
    -  ColumnClick   Column As Integer        A column header was clicked (Column 0 is the first). Without a handler the list sorts itself by that column's text (the same column again reverses); a program that orders the rows itself calls SetSort -1, 0 in the handler and fills the list in its own order.
    + ColumnClick Column As Integer A column header was clicked (Column 0 is the first). The list has already sorted itself by that column's text (the same column again reverses), with or without a handler; a program that orders the rows itself calls SetSort -1, 0 in the handler (or SetSort for another order) and fills the list in its own order.

    Default Event: Click

    Example

    ListView1.SetColumns "Name,20|Size,10|Type,15"
    @@ -7410,6 +7622,7 @@ ListView2.SetRows "a.txt" & Chr$(9) & "120" & Ch
     

    Type-Specific Methods

      Method        Parameters          Description
       -----------   -----------------   -------------------------------------------
    +  GetSelected   (none)              Returns the index (as OptionIndex counts) of the selected OptionButton of this button's container, or -1 when none is: one call instead of reading every button's Value.
       SetSelected   Index As Integer    Select the OptionButton at the given index within this button's container.

    Default Event: Click

    Common Properties, Events, and Methods

    @@ -7439,15 +7652,15 @@ ListView2.SetRows "a.txt" & Chr$(9) & "120" & Ch CanRedo Boolean An undone edit can be done again (read-only).

    Text (a common property) is the plain text, a line per paragraph; setting it replaces everything with plain text. ReadOnly lets the user select and copy but not change the text.

    Type-Specific Methods

    -
      Method                 Description
    -  ------                 -----------
    -  Undo                   Undo the last edit (typing a run of characters is one edit). Up to 100 edits are kept.
    -  Redo                   Do an undone edit again.
    -  Cut, Copy, Paste       The clipboard. Text copied from a RichEdit pastes back into a RichEdit with its looks; text from elsewhere takes the look of the text typed next.
    -  SelectAll              Select all the text.
    -  Find(text)             Select the next place text is (any case), from the selection on and around past the end; returns where it starts, or -1.
    -  LoadFile(path)         Load a file: RTF when it is RTF, else plain text. Returns 0, or -1 when it cannot be read.
    -  SaveFile(path)         Save the text: as RTF when path ends in .rtf, else as plain text. Returns 0, or -1 when it cannot be written.
    +
      Method                     Description
    +  ------                     -----------
    +  Undo                       Undo the last edit (typing a run of characters is one edit). Up to 100 edits are kept.
    +  Redo                       Do an undone edit again.
    +  Cut, Copy, Paste           The clipboard. Text copied from a RichEdit pastes back into a RichEdit with its looks; text from elsewhere takes the look of the text typed next.
    +  SelectAll                  Select all the text.
    +  Find(text[, matchCase])    Select the next place text is, from the selection on and around past the end; returns where it starts, or -1. Any case unless matchCase is True.
    +  LoadFile(path)             Load a file: RTF when it is RTF, else plain text. Returns 0, or -1 when it cannot be read.
    +  SaveFile(path)             Save the text: as RTF when path ends in .rtf, else as plain text. Returns 0, or -1 when it cannot be written.

    Type-Specific Events

      Event        Parameters   Description
       -----        ----------   -----------
    @@ -7560,8 +7773,12 @@ End Sub

    Line

    VB Equivalent: Line -- DVX Widget: separator

    -

    A visual separator line. The underlying widget supports both horizontal and vertical orientations. A Line placed in a horizontal container (HBox, Toolbar) is vertical; anywhere else it is horizontal.

    -

    No type-specific properties, events, or methods.

    +

    A visual separator line, horizontal or vertical. A Line placed in a horizontal container (HBox, Toolbar) starts vertical; anywhere else it starts horizontal. Set Vertical to turn it the other way, for example a vertical Line between two side-by-side groups inside a VBox.

    +

    Type-Specific Properties

    +
      Property   Type      Description
    +  --------   -------   -------------------------------------------
    +  Vertical   Boolean   True for a vertical line, False for a horizontal one.
    +

    No type-specific events or methods.

    Common Properties, Events, and Methods

    @@ -7623,6 +7840,10 @@ End Sub -------- ------- ------------------------------------------- Position Integer Position of the divider in pixels from the left (or, when Vertical is False, from the top). Vertical Boolean True (the default): panes side by side. False: one pane above the other. Can be set in the designer or at run time. +

    Type-Specific Events

    +
      Event    Description
    +  -----    -----------
    +  Change   The user moved the divider: raised as it is dragged, Position giving where it is. Not raised for a Position set in code.

    Container: Yes

    No default event.

    Common Properties, Events, and Methods

    @@ -7631,8 +7852,14 @@ End Sub

    StatusBar

    VB Equivalent: StatusBar -- DVX Widget: statusbar

    A horizontal container styled as a status bar, typically placed at the bottom of a form. Put Labels (or other controls) inside it as its panes: they sit side by side, each drawn with a sunken-panel border for the classic segmented status-bar look. Give the pane that should take the spare room Weight = 100. Panes can be nested in the designer or the .frm, or added at run time with CreateControl(form, "Label", name$, StatusBar1).

    -

    No type-specific properties, events, or methods.

    Container: Yes

    +

    Type-Specific Properties

    +
      Property    Type      Description
    +  ---------   -------   -------------------------------------------
    +  Alignment   Enum      Where the panes sit along the bar when they do not fill it: Left (the default), Center, or Right.
    +  Padding     Integer   Space in pixels between the edge of the bar and the panes: 1 unless set; 0 is none.
    +  Spacing     Integer   Space in pixels between one pane and the next: 2 unless set; 0 is none.
    +

    No type-specific events or methods.

    Example

    Begin StatusBar Status1
         Begin Label lblStatus
    @@ -7652,16 +7879,19 @@ End

    Type-Specific Properties

      Property   Type      Description
       --------   -------   -------------------------------------------
    -  TabIndex   Integer   Index of the active tab (0-based). Note: this property name collides with the common VB-compatibility TabIndex property, which shadows it at runtime. Use the SetActive method instead to switch tabs.
    + TabIndex Integer Index of the active tab (0-based). Setting it switches tabs as SetActive does.

    Type-Specific Methods

      Method                   Description
       ------                   -----------
       AddPage title$           Add a new tab page with the given title.
       GetActive()              Returns the index of the active tab.
    -  SetActive index%         Switch to the tab at the given index. This is the recommended way to change tabs at runtime (the TabIndex property is shadowed by the common property handler). A negative index selects the first tab and an index past the last tab selects the last tab. Switching closes a drop-down list opened from the page being hidden and moves the focus to the tab strip if it was on a control of the page being hidden. The Click event is not raised for a switch made in code.
    + SetActive index% Switch to the tab at the given index (the same as setting TabIndex). A negative index selects the first tab and an index past the last tab selects the last tab. Switching closes a drop-down list opened from the page being hidden and moves the focus to the tab strip if it was on a control of the page being hidden. No event is raised for a switch made in code. +

    Type-Specific Events

    +
      Event    Description
    +  -----    -----------
    +  Change   The user switched tabs, with the mouse or the keys (not raised for a switch made in code). GetActive (or TabIndex) says which tab is active now.

    Container: Yes

    Default Event: Click

    -
    Warning: The TabIndex property is shadowed by the common property handler at runtime. Use the SetActive method to change tabs programmatically.

    Common Properties, Events, and Methods

    @@ -7744,13 +7974,13 @@ End Sub SetSyntaxMode mode$ Activate syntax highlighting. Modes: "dhs" (help source), "bas" (BASIC, coloured as the DVX BASIC IDE colours it), "custom" (the program colours each line in the Colorize event). Pass "" to disable. SetTabWidth width% Set the tab stop width in characters. SetUseTabChar useChar When True, Tab inserts a tab character; when False, inserts spaces. - Undo Take back the last change (Ctrl+Z); a second Undo puts it back. + Undo Take back the last change (Ctrl+Z; characters typed one after another are one change); a second Undo puts it back.

    Cut, Paste, and Undo do nothing in a read-only area.

    Type-Specific Events

      Event         Parameters                Description
       -----------   -----------------------   -------------------------------------------
       GutterClick   LineNumber As Integer     The line-number gutter (shown by SetShowLineNumbers) was clicked beside a line: toggle a breakpoint there.  A click in the gutter does not move the cursor.
    -  Colorize      LineText As String, Colors As String   With SetSyntaxMode "custom", for every line as it is drawn: Colors comes with one "0" for each character of LineText; set each to the digit of a syntax colour (vbSyntaxText 0 to vbSyntaxType 6, colours as SetSyntaxColor sets them).  Characters it leaves out stay plain.  It runs whenever lines are drawn, so it must be quick and must not change the area.
    + Colorize LineText As String, Colors As String With SetSyntaxMode "custom", for every line as it is drawn: Colors comes with one "0" for each character of LineText; set each to the digit of a syntax colour (vbSyntaxText 0 to vbSyntaxType 6, colours as SetSyntaxColor sets them). Characters it leaves out stay plain. It runs when a line is drawn with text it has not coloured lately (the colours it gave are kept with the text, so a line drawn again unchanged does not ask again), so it must be quick and must not change the area; its colours should depend only on LineText. SetSyntaxMode "custom" again colours every line afresh (after the program changes how it colours).
    TextArea1.SetSyntaxMode "custom"
     
     Sub TextArea1_Colorize (LineText As String, Colors As String)
    @@ -7792,7 +8022,8 @@ End Sub
      Property   Type      Description
       --------   -------   -------------------------------------------
       Enabled    Boolean   True to start the timer, False to stop it.
    -  Interval   Integer   Timer interval in milliseconds. Setting it restarts the countdown. An Interval of 0 (or less) disables the Timer event.
    + Interval Integer Timer interval in milliseconds. Setting it restarts the countdown. An Interval of 0 (or less) disables the Timer event. + Repeat Boolean True (the default) to fire every Interval; False for a one-shot timer that fires once and stops itself (Enabled turns False), until Start or Enabled = True runs it again.

    Type-Specific Methods

      Method   Parameters   Description
       ------   ----------   -------------------------------------------
    @@ -7822,7 +8053,13 @@ End Sub

    VB Equivalent: Toolbar -- DVX Widget: toolbar

    A horizontal container styled as a toolbar, with compact padding and spacing. Place buttons, labels, or other controls inside.

    Container: Yes

    -

    No type-specific properties, events, or methods.

    +

    Type-Specific Properties

    +
      Property    Type      Description
    +  ---------   -------   -------------------------------------------
    +  Alignment   Enum      Where the controls sit along the toolbar when they do not fill it: Left (the default), Center, or Right.
    +  Padding     Integer   Space in pixels between the edge of the toolbar and the controls: 2 unless set; 0 is none.
    +  Spacing     Integer   Space in pixels between one control and the next: 2 unless set; 0 is none.
    +

    No type-specific events or methods.

    Common Properties, Events, and Methods

    @@ -7845,9 +8082,13 @@ End Sub IsExpanded(index%) Returns True if the node at the given index is expanded. IsItemSelected(index%) Returns True if the node at the given index is selected. ItemCount() Returns the total number of nodes (all levels). + ItemDepth(index%) Returns how many nodes the node is under: 0 for a root-level node, 1 for its children, and so on (-1 when there is no such node). + ItemParent(index%) Returns the index of the node's parent, or -1 for a root-level node (or when there is no such node). + RemoveItem index% Remove the node and every node under it. The nodes after them move up. SetExpanded index%, expanded Expand or collapse the node at the given index. Collapsing a node that hides the current item moves the current item to that node. SetItemSelected index%, selected Select or deselect the node at the given index. Without multi-select this sets (or clears) the single selected node. SetItemTag index%, tag$ Keep any text with the node (like VB's Node.Tag): a file path, a key. "" removes it. + SetItemText index%, text$ Change the text of the node at the given index. SetMultiSelect multi Enable or disable multi-select mode. Turning it off clears the multi-selection. SetReorderable reorderable Enable or disable node reordering.

    The Change event fires when the current node changes (a click, the arrow keys, or ListIndex set by the program).

    @@ -7867,9 +8108,11 @@ Print "Items:"; TreeView1.ItemCount()

    DVX Extension -- DVX Widget: wrapbox | Name Prefix: WrapBox

    A container that arranges children in a flowing layout, wrapping to the next row when the available width is exceeded. Similar to CSS flexbox with flex-wrap: wrap.

    Type-Specific Properties

    -
      Property    Type   Description
    -  ---------   ----   -------------------------------------------
    -  Alignment   Enum   Horizontal alignment of items: Left, Center, or Right.
    +
      Property    Type      Description
    +  ---------   -------   -------------------------------------------
    +  Alignment   Enum      Horizontal alignment of items: Left, Center, or Right.
    +  Padding     Integer   Space in pixels between the edge of the box and the items: 2 unless set; 0 is none.
    +  Spacing     Integer   Space in pixels between one item and the next, and between rows: 4 unless set; 0 is none.

    Container: Yes

    Default Event: Click

    Common Properties, Events, and Methods

    diff --git a/docs/dvx_system_reference.html b/docs/dvx_system_reference.html index dc2625a2..33ab9b77 100644 --- a/docs/dvx_system_reference.html +++ b/docs/dvx_system_reference.html @@ -96,6 +96,8 @@ img { max-width: 100%; }
  • dvxRes.h -- Resource System
  • dvxPrefs.h -- Preferences System
  • dvxMem.h -- Memory Tracking
  • +
  • dvxEndian.h -- Byte Order
  • +
  • dvxHash.h -- Hashes and Checksums
  • dvxWgtP.h -- Widget Plugin API
  • platform/dvxPlat.h -- Platform Layer
  • @@ -105,12 +107,15 @@ img { max-width: 100%; }
  • Libraries
  • +
  • Cryptography Library
  • Networking Library
  • +
  • Secure Link Library + +
  • Serial Stack
  • Task Manager
  • @@ -322,6 +330,7 @@ img { max-width: 100%; }
  • AnsiTerm
  • ANSITERM_EVT_BELL
  • ANSITERM_EVT_MUSIC
  • +
  • ANSITERM_EVT_SEND
  • AnsiTermExApiT
  • Anti-aliasing
  • API Reference
  • @@ -360,6 +369,7 @@ img { max-width: 100%; }
  • audioModulesChoices
  • audioNotesAdd
  • audioNotesOpen
  • +
  • audioNotesSetTone
  • AudioNotesT
  • audioOpen
  • audioPercentToVolume
  • @@ -374,6 +384,7 @@ img { max-width: 100%; }
  • audioSampleLoad
  • audioSettingModules
  • audioSettingsStore
  • +
  • audioStreamEnd
  • audioStreamFlush
  • audioStreamOpen
  • audioStreamWrite
  • @@ -403,8 +414,8 @@ img { max-width: 100%; }
  • basDbgExec
  • basDbgLocals
  • basdev
  • -
  • basFontChoiceOf
  • -
  • basFontPropText
  • +
  • basFontChoiceOf
  • +
  • basFontPropText
  • basFormRtTermBind
  • basHelpKeywordTopic
  • BASIC control file
  • @@ -435,7 +446,7 @@ img { max-width: 100%; }
  • basVmStep
  • BasVmT
  • BasVmT
  • -
  • BBS Client
  • +
  • BBS Client
  • Bevel
  • BevelStyleT
  • Binary Interface Rules
  • @@ -499,7 +510,7 @@ img { max-width: 100%; }
  • Crash Recovery
  • Crash Report
  • CRC
  • -
  • CRC-16
  • +
  • CRC-16
  • Critical Error Handler
  • Cross-Compilation
  • Ctrl+Esc
  • @@ -541,12 +552,15 @@ img { max-width: 100%; }
  • Developers
  • DFN
  • DFN
  • +
  • dfnCp437
  • +
  • dfnFromUnicode
  • +
  • dfnTextFromUnicode
  • +
  • dfnTextToUnicode
  • Dialer
  • Dialog Boxes
  • Dialogs
  • Different Version of DVX
  • -
  • Diffie-Hellman
  • -
  • Diffie-Hellman
  • +
  • Diffie-Hellman
  • Dirty Rects
  • dirtyListAdd
  • dirtyListClear
  • @@ -588,7 +602,7 @@ img { max-width: 100%; }
  • drawTextN
  • drawTextScaled
  • drawVLine
  • -
  • DRBG
  • +
  • DRBG
  • Driver Modules
  • Drives
  • Drop-down Lists
  • @@ -607,8 +621,11 @@ img { max-width: 100%; }
  • DVX_BUTTON_BOX_MAX
  • DVX_CONFIG_DIR
  • DVX_DRIVER_HDR
  • +
  • DVX_FNV_BASIS
  • +
  • DVX_HEX_DIGITS
  • DVX_HOTKEY_NONE
  • DVX_HUNG_APP_MS
  • +
  • DVX_MEM_SYSTEM
  • dvxAccelFormat
  • dvxAccelParse
  • dvxAddAccel
  • @@ -635,6 +652,7 @@ img { max-width: 100%; }
  • dvxComp
  • dvxComp.h
  • dvxContentDisplay
  • +
  • dvxCrc16
  • dvxCreateAccelTable
  • dvxCreateWindow
  • dvxCreateWindowCentered
  • @@ -660,6 +678,7 @@ img { max-width: 100%; }
  • dvxDriversLoad
  • dvxDriverUnload
  • dvxDrv.h
  • +
  • dvxEndian.h
  • DvxEntropyHookT
  • dvxErrorBox
  • dvxExclusiveBegin
  • @@ -668,6 +687,7 @@ img { max-width: 100%; }
  • dvxExclusiveDisplay
  • dvxExclusiveEnd
  • dvxExclusiveFlush
  • +
  • dvxExclusiveMirror
  • dvxExclusivePresent8
  • dvxExclusivePresent8
  • dvxExclusiveSetPalette
  • @@ -685,6 +705,8 @@ img { max-width: 100%; }
  • dvxFlowLineOf
  • dvxFlowPaint
  • dvxFlowPaintSel
  • +
  • dvxFnvByte
  • +
  • dvxFnvBytes
  • dvxFont.h
  • dvxFontAddFile
  • dvxFontAddMemory
  • @@ -698,6 +720,7 @@ img { max-width: 100%; }
  • dvxFontFamilyOf
  • dvxFontLicence
  • dvxFontOpen
  • +
  • dvxFontRebuildStep
  • dvxFontRemoveOwner
  • dvxFontRescan
  • dvxFontRetain
  • @@ -705,22 +728,30 @@ img { max-width: 100%; }
  • dvxFontsFromPrefs
  • dvxFontSizes
  • dvxFontSmooth
  • +
  • dvxFontSmoothBegin
  • dvxFontsToPrefs
  • dvxFree
  • dvxFree
  • dvxFreeAccelTable
  • dvxFreeImage
  • +
  • dvxGetBe16
  • +
  • dvxGetBe32
  • dvxGetBlitOps
  • dvxGetColor
  • dvxGetColors
  • dvxGetDisplay
  • dvxGetFont
  • +
  • dvxGetLe16
  • +
  • dvxGetLe32
  • dvxGetVideoModes
  • DVXGM.SF2
  • +
  • dvxHash.h
  • +
  • dvxHexValue
  • dvxHideWindow
  • DvxHotkeyFnT
  • dvxIdleMs
  • dvxImageInfo
  • +
  • dvxImageInfoFromMemory
  • dvxInfoBox
  • dvxInit
  • dvxInputBox
  • @@ -740,6 +771,7 @@ img { max-width: 100%; }
  • dvxMem.h
  • dvxMemGetAppUsage
  • dvxMemResetApp
  • +
  • dvxMemSetApp
  • dvxMessageBox
  • dvxMinimizeWindow
  • dvxMouseFromPrefs
  • @@ -756,6 +788,9 @@ img { max-width: 100%; }
  • dvxPrefs.h
  • dvxPrefsBas.c
  • dvxPromptSave
  • +
  • dvxPutBe32
  • +
  • dvxPutLe16
  • +
  • dvxPutLe32
  • dvxQuit
  • dvxRaiseWindow
  • dvxRealloc
  • @@ -790,6 +825,7 @@ img { max-width: 100%; }
  • dvxSetMouseConfig
  • dvxSetSoundHook
  • dvxSetTitle
  • +
  • dvxSetWaitPump
  • dvxSetWallpaper
  • dvxSetWallpaperMode
  • dvxSetWindowIcon
  • @@ -813,7 +849,9 @@ img { max-width: 100%; }
  • dvxSqlFreeResult
  • dvxSqlNext
  • dvxSqlOpen
  • +
  • DvxSqlPumpFnT
  • dvxSqlQuery
  • +
  • dvxSqlSetPump
  • dvxStrdup
  • dvxText.h
  • dvxTextHash
  • @@ -828,6 +866,7 @@ img { max-width: 100%; }
  • DVXUSER
  • dvxVideo
  • dvxVideo.h
  • +
  • dvxWaitPump
  • dvxWallpaperFromPrefs
  • dvxWallpaperToPrefs
  • dvxWgt.h
  • @@ -842,6 +881,7 @@ img { max-width: 100%; }
  • dxe3gen
  • DxeAppContextT
  • Dynamic Loading
  • +
  • Ed25519
  • EINPROGRESS
  • Empty Floppy Drive
  • EMU8000
  • @@ -879,7 +919,7 @@ img { max-width: 100%; }
  • Font families
  • Font files
  • Font licences
  • -
  • Font properties (C)
  • +
  • Font properties (C)
  • Font sizes
  • Font smoothing
  • Font smoothing
  • @@ -892,13 +932,13 @@ img { max-width: 100%; }
  • FrameMissed
  • FRAMEWAIT
  • FRAMEWAIT
  • -
  • FrmCtrlT
  • -
  • FrmMenuItemT
  • -
  • frmModelFromForm
  • -
  • frmModelLoad
  • -
  • FrmModelT
  • -
  • frmModelText
  • -
  • frmModelWrite
  • +
  • FrmCtrlT
  • +
  • FrmMenuItemT
  • +
  • frmModelFromForm
  • +
  • frmModelLoad
  • +
  • FrmModelT
  • +
  • frmModelText
  • +
  • frmModelWrite
  • Full screen
  • Full screen
  • Full-screen exclusive mode
  • @@ -933,20 +973,20 @@ img { max-width: 100%; }
  • GENMIDI
  • getaddrinfo
  • Getting Started
  • -
  • Go-Back-N
  • +
  • Go-Back-N
  • Gravis UltraSound
  • Gravis UltraSound patches
  • HAL
  • HBox
  • +
  • HDLC
  • HDLC
  • -
  • HDLC
  • Held keys
  • Help
  • Help Menu
  • HelpView
  • HelpViewLinkFnT
  • Hit Testing
  • -
  • HKDF
  • +
  • HKDF
  • hlpfile
  • hlpfile.bas
  • hlpFileForEachRecord
  • @@ -958,7 +998,7 @@ img { max-width: 100%; }
  • hlpFormat.h
  • HlpOpen
  • hlpWrapText
  • -
  • HMAC
  • +
  • HMAC
  • Hot Keys
  • HotkeyAdd
  • HotkeyNext
  • @@ -1103,6 +1143,7 @@ img { max-width: 100%; }
  • Modem
  • Modules
  • Modules on the wavetable synthesizer
  • +
  • Monocypher
  • Mouse
  • Mouse
  • Mouse Click
  • @@ -1150,6 +1191,7 @@ img { max-width: 100%; }
  • net.bas
  • NetConfigT
  • netGetStatus
  • +
  • netLookupStart
  • netPoll
  • netRestart
  • netStart
  • @@ -1180,7 +1222,7 @@ img { max-width: 100%; }
  • Packages
  • Packaging
  • packColor
  • -
  • packet
  • +
  • packet
  • Packet driver
  • Page Setup
  • Palette
  • @@ -1206,13 +1248,13 @@ img { max-width: 100%; }
  • pkgOpInstall
  • pkgOpRemove
  • pkgPlanInstall
  • -
  • pktClose
  • +
  • pktClose
  • pktDrv
  • -
  • pktOpen
  • -
  • pktOpenStream
  • -
  • pktPoll
  • -
  • pktSend
  • -
  • PktStreamT
  • +
  • pktOpen
  • +
  • pktOpenStream
  • +
  • pktPoll
  • +
  • pktSend
  • +
  • PktStreamT
  • Platform Layer
  • Platform Layer
  • PLATFORM_LONG_NAME_MAX
  • @@ -1226,6 +1268,7 @@ img { max-width: 100%; }
  • platformDriveList
  • PlatformDriveT
  • platformEntropyStamp
  • +
  • platformFileStamp
  • platformFilterMatch
  • platformFlushRect
  • platformInjectScan
  • @@ -1250,10 +1293,12 @@ img { max-width: 100%; }
  • platformPathAbsolute
  • platformPathDirName
  • platformPathIsAbsolute
  • +
  • platformPathIsRoot
  • platformPathIsSep
  • platformPathIsUnder
  • platformPathJoin
  • platformPathNative
  • +
  • platformPathParent
  • platformPathSame
  • platformPathSibling
  • platformPathTidy
  • @@ -1265,6 +1310,7 @@ img { max-width: 100%; }
  • platformShutdownHookAdd
  • platformShutdownHookRemove
  • platformSplashNearestColor
  • +
  • PlatformStampT
  • platformStatSize
  • platformSuspendHookAdd
  • platformTempDir
  • @@ -1280,6 +1326,7 @@ img { max-width: 100%; }
  • platformValidateShortName
  • platformVideoInit
  • platformVideoReadBack
  • +
  • platformWalkDir
  • platformWriteFile
  • PlayTone
  • PlayWave
  • @@ -1302,6 +1349,7 @@ img { max-width: 100%; }
  • prefsChoiceIndexOfValue
  • prefsChoiceLabels
  • PrefsChoiceT
  • +
  • prefsDvxIniShared
  • prefsGetBool
  • prefsGetChoice
  • prefsGetInt
  • @@ -1431,35 +1479,40 @@ img { max-width: 100%; }
  • SDK Quick Start
  • sdk.mk
  • Search
  • -
  • secCipherCreate
  • -
  • secDhCreate
  • +
  • secAeadLock
  • +
  • secAeadUnlock
  • +
  • secCipherCreate
  • +
  • secDhCreate
  • +
  • secEd25519Check
  • +
  • secEd25519Sign
  • +
  • secLink
  • secLink
  • -
  • secLink
  • -
  • secLinkClose
  • -
  • secLinkHandshake
  • -
  • secLinkHandshakeStep
  • -
  • secLinkOpen
  • -
  • secLinkOpenStream
  • -
  • secLinkPoll
  • -
  • secLinkSend
  • -
  • secLinkSetChannelPlain
  • -
  • secLinkSetPassphrase
  • -
  • secRngAddEntropy
  • -
  • secRngBytes
  • -
  • secRngLoadSeed
  • -
  • Security
  • -
  • security.bas
  • +
  • secLinkClose
  • +
  • secLinkHandshake
  • +
  • secLinkHandshakeStep
  • +
  • secLinkOpen
  • +
  • secLinkOpenStream
  • +
  • secLinkPoll
  • +
  • secLinkSend
  • +
  • secLinkSetChannelPlain
  • +
  • secLinkSetPassphrase
  • +
  • secRngAddEntropy
  • +
  • secRngBytes
  • +
  • secRngLoadSeed
  • +
  • Security
  • +
  • security.bas
  • +
  • secX25519
  • select
  • Selection
  • Selection Highlight
  • Separator
  • Serial
  • -
  • Serial Example
  • +
  • Serial Example
  • setClipRect
  • Settings
  • Setup
  • SF2
  • -
  • SHA-256
  • +
  • SHA-256
  • SHARED
  • Shared Library
  • shell.bas
  • @@ -1485,6 +1538,7 @@ img { max-width: 100%; }
  • ShellAppPath
  • shellAppSlotCount
  • ShellAppT
  • +
  • shellAppTaskStart
  • ShellArg
  • ShellArgCount
  • ShellArgQuote
  • @@ -1622,6 +1676,7 @@ img { max-width: 100%; }
  • SoundResume
  • Sounds
  • SoundSettingsStore
  • +
  • SoundStreamEnd
  • SoundStreamFlush
  • Spacer
  • Spinner
  • @@ -1801,6 +1856,7 @@ img { max-width: 100%; }
  • wgtCanvasGetDisplay
  • wgtCanvasGetPixel
  • wgtCanvasLoad
  • +
  • wgtCanvasLoadData
  • wgtCanvasResize
  • wgtCanvasSave
  • wgtCanvasSetBorderStyle
  • @@ -1877,9 +1933,11 @@ img { max-width: 100%; }
  • wgtImage
  • wgtImageButton
  • wgtImageButtonFromFile
  • +
  • wgtImageButtonLoadData
  • wgtImageButtonLoadFile
  • wgtImageButtonSetData
  • wgtImageFromFile
  • +
  • wgtImageLoadData
  • wgtImageLoadFile
  • wgtImageSetData
  • wgtImageSetTransparent
  • @@ -1906,6 +1964,8 @@ img { max-width: 100%; }
  • wgtListView
  • wgtListViewClearSelection
  • wgtListViewGetSelected
  • +
  • wgtListViewGetSortCol
  • +
  • wgtListViewGetSortDir
  • wgtListViewSelectAll
  • wgtListViewSetColumns
  • wgtListViewSetData
  • @@ -1957,6 +2017,8 @@ img { max-width: 100%; }
  • wgtScrollPaneSetScrollLeft
  • wgtScrollPaneSetScrollTop
  • wgtSeparator
  • +
  • wgtSeparatorIsVertical
  • +
  • wgtSeparatorSetVertical
  • wgtSetDebugLayout
  • wgtSetEnabled
  • wgtSetEnabled
  • @@ -2020,8 +2082,10 @@ img { max-width: 100%; }
  • wgtTextInputPaste
  • wgtTextInputSetSelText
  • wgtTimer
  • +
  • wgtTimerGetRepeat
  • wgtTimerIsRunning
  • wgtTimerSetInterval
  • +
  • wgtTimerSetRepeat
  • wgtTimerStart
  • wgtTimerStop
  • wgtToolbar
  • @@ -2041,9 +2105,11 @@ img { max-width: 100%; }
  • wgtUpdateCursorBlink
  • wgtUpdateTimers
  • wgtValueBool
  • +
  • wgtValueBytes
  • wgtValueDouble
  • wgtValueFloat
  • wgtValueInt
  • +
  • wgtValueLen
  • wgtValueStr
  • WgtValueT
  • wgtValueToBool
  • @@ -2129,6 +2195,10 @@ img { max-width: 100%; }
  • WrapBox
  • WtInfo$
  • WtStart
  • +
  • X25519
  • +
  • X25519
  • +
  • XChaCha20-Poly1305
  • +
  • XChaCha20-Poly1305
  • xfer
  • xfer.bas
  • xferAnswer
  • @@ -2142,14 +2212,16 @@ img { max-width: 100%; }
  • xferProgress
  • XferProgressT
  • XferReceive
  • +
  • XferReceiveFiles
  • XferSendFile
  • +
  • XferSendFiles
  • +
  • xferSplitList
  • xferStartReceive
  • xferStartSend
  • XM
  • XMODEM
  • -
  • XTEA
  • -
  • XTEA
  • -
  • XTEA-CTR
  • +
  • XTEA
  • +
  • XTEA-CTR
  • YMODEM
  • Your First BASIC Control
  • Your First BASIC Program
  • @@ -2620,6 +2692,7 @@ img { max-width: 100%; }

    When you exit DVX, each program is closed as if you had closed it yourself. If you answer Cancel when a program asks to save its work, DVX stays running. Save or close that program, then choose Exit DVX again.

    Reporting a problem

    DVX writes a record of what happened to the file DVX.LOG, in your USER folder (for example C:\DVX\USER\DVX.LOG). If a program crashes, this file contains details that help the program's author find the problem. The file is started fresh each time DVX starts, so copy it before starting DVX again.

    +

    DVX also remembers the programs' names and icons in PROGRAMS.CAC in the same folder, so Program Manager starts quickly. If a program shows an old name or icon after you changed it, delete PROGRAMS.CAC: DVX makes it again.

    Changing Settings

    Task Manager

    @@ -3007,7 +3080,8 @@ prefsClose(h);
      Function               Purpose
       --------               -------
       dvxMemGetAppUsage      Query current bytes allocated for an app
    -  dvxMemResetApp         Free every tracked allocation charged to an app
    + dvxMemResetApp Start an app's accounting over (nothing is freed) + dvxMemSetApp Make an app the current one (a library's own memory: DVX_MEM_SYSTEM)

    The dvxMemAppIdPtr pointer is set by the shell to &ctx->currentAppId so the allocator always knows which app to charge.

    @@ -3017,10 +3091,10 @@ prefsClose(h);
      Extension  Directory   Purpose                                             Examples
       ---------  ---------   -------                                             --------
       .lib       LIBS/       Core libraries loaded first. Provide infrastructure APIs. Each exports an ABI stamp.  libtasks.lib, libdvx.lib, dvxshell.lib
    -  .wgt       WIDGETS/    Widget type plugins. Each exports wgtRegister() and an ABI stamp.    button.wgt, listview.wgt, terminal.wgt
    +  .wgt       WIDGETS/    Widget type plugins. Each exports wgtRegister() and an ABI stamp.    button.wgt, listview.wgt, ansiterm.wgt
       .app       APPS/*/     Application modules. Each exports appDescriptor (which records its ABI) and appMain(). Loaded on demand by the shell.  progman.app, notepad.app, cpanel.app

    Binary Interface Version

    -

    DVX_ABI_VERSION (dvxTypes.h) is one number for the binary interface between all modules. Each .lib and .wgt exports <baseName>AbiVersion (written by DVX_ABI_STAMP(baseName) in its main source file); each app's AppDescriptorT starts with abiVersion (SHELL_APP_DESCRIPTOR fills it); each widget API struct starts with a WgtApiHeaderT; each widget class carries WGT_CLASS_VERSION, which is the same number. The loader checks every .lib and .wgt right after loading it and stops the boot with a message naming a module whose stamp is missing or different; the shell refuses a mismatched app with a message box; wgtGetApi and wgtRegisterClass refuse mismatched tables and classes. Details always go to DVX.LOG. The bump rule and the size-table test that enforces it are described with DVX_ABI_VERSION in the API reference (dvxTypes.h).

    +

    DVX_ABI_VERSION (dvxTypes.h) is one number for the binary interface between all modules. Each .lib and .wgt exports <baseName>AbiVersion (written by DVX_ABI_STAMP(baseName) in its main source file); each app's AppDescriptorT starts with abiVersion (SHELL_APP_DESCRIPTOR fills it); each widget API struct starts with a WgtApiHeaderT; each widget class carries WGT_CLASS_VERSION, which is the same number. The loader checks every .lib and .wgt right after loading it: at the start it stops the boot with a message naming a module whose stamp is missing or different, and a library loaded later (see Boot Sequence) is refused and logged; the shell refuses a mismatched app with a message box; wgtGetApi and wgtRegisterClass refuse mismatched tables and classes. Details always go to DVX.LOG. The bump rule and the size-table test that enforces it are described with DVX_ABI_VERSION in the API reference (dvxTypes.h).

    Boot Sequence

      dvx.exe (loader)
          |
    @@ -3030,25 +3104,37 @@ prefsClose(h);
    | +-- Read .dep files for each module (dependency base names) | - +-- Topological sort: load modules in dependency order - | - dlopen() with RTLD_GLOBAL - | - ABI stamp checked; a mismatch stops the boot - | - Each .wgt that exports wgtRegister() has it called + +-- Index the names each library exports (its file's DXE header and + | export table only; a hash and the library for each name) | - +-- Find and call shellMain() (exported by dvxshell.lib) + +-- Load, each after the libraries it needs: + | - the libraries with constructors (code that runs as they load: + | audio, music, joystick, the network, Task Manager, texthelp) + | - the library with shellMain (dvxshell.lib) + | - every widget; each .wgt that exports wgtRegister() has it called + | - dlopen() with RTLD_GLOBAL; ABI stamp checked; a mismatch stops + | the boot + | + +-- Find and call shellMain() + | + +-- Every other library loads when something first needs one of + | its names: a module being opened (platformModuleOpen), or a + | dlsym(NULL) (BASIC's DECLARE LIBRARY, dvxLibraryLoaded) | +-- dvxInit() -- video mode, input, font, colors, cursors | +-- Load desktop app (progman.app) | +-- Main loop: - dvxUpdate() -> tsYield() -> shellReapApps() + dvxUpdate() (yields) -> shellReapApps() -> close, exit, + and DOS-run requests -> screen saver

    Application Lifecycle

    Two kinds of DXE apps:

    Callback-only (hasMainLoop = false)

    appMain() creates windows, registers callbacks, and returns. The app lives through GUI callbacks driven by the shell's main loop. Lifecycle ends when the last window is closed. No extra task stack needed -- simpler and cheaper.

    Main-loop (hasMainLoop = true)

    -

    A dedicated cooperative task is created. appMain() runs in that task with its own loop, calling tsYield() to share CPU. Needed for apps with continuous work (terminal emulators, games). Lifecycle ends when appMain() returns.

    +

    A dedicated cooperative task is created. appMain() runs in that task with its own loop, calling tsYield() (or dvxUpdate, which yields) to share CPU. Needed for apps with continuous work (terminal emulators, games). Lifecycle ends when appMain() returns.

    +

    Input is taken on the shell's own task (see Event Model), so every window callback -- a callback-only app's and a main-loop app's alike -- runs there, never on another program's stack; a main-loop app's own loop sees what its callbacks did when it next runs. A BASIC program's events are the exception: its form runtime queues those the shell's task dispatches and runs them on the program's own task at its next turn (DoEvents, its event loop), so a handler that waits (DoEvents, SHELL, a modal form) holds only its own program. Its UserControls' events (their mouse, keys, focus, Resize, and Paint) wait for that turn the same way: a program's code never runs on two tasks at once.

    Crash Recovery

    The platform layer installs signal handlers for SIGSEGV, SIGFPE, SIGILL. On crash, the handler logs platform-specific diagnostics (register dump on DJGPP), then longjmps back to the shell's main loop. The crashed app is killed; other apps and the shell survive. This provides Windows 3.1-style fault tolerance.

    Per-App Memory Tracking

    @@ -3056,16 +3142,16 @@ prefsClose(h);

    Event Model

    -

    DVX uses a cooperative polling model. The main loop (dvxRun / dvxUpdate) runs this cycle each frame:

    +

    DVX uses a cooperative polling model. The main loop (dvxRun / dvxUpdate) runs this cycle each frame. The first four steps run only in a frame that takes input: the context's inputHere hook decides, and the shell lets only its own task take it (or any task, once the shell's has not for a quarter of a second, busy in a long call), so callbacks run on the shell's task; a frame on another task just paints and yields.

    • Poll mouse -- platformMousePoll() returns position and button bitmask. Compare with previous frame for press/release edge detection.
    • Poll keyboard -- platformKeyboardRead() returns ASCII + scancode. Non-blocking; returns false if buffer is empty. Shift/Ctrl/Alt+F-keys arrive as the plain F-key plus modifier flags. If an input entropy hook is installed (dvxSetInputEntropyHook), input events are timestamped and fed to it here.
    • Dispatch to focused window -- the event loop fires window callbacks (onKey, onMouse, etc.) on the focused window. If the window has a widget tree, the widget system's installed handlers dispatch to individual widgets.
    • Tooltips, widget polling (WCLASS_NEEDS_POLL), cursor blink, and widget timers.
    • -
    • Deferred paint flush -- every window whose paintNeeded was raised by wgtInvalidate / wgtInvalidatePaint during the frame is painted once (the highest requested level wins). Windows queued for destruction are skipped.
    • +
    • Deferred paint flush -- every window whose paintNeeded was raised by wgtInvalidate / wgtInvalidatePaint during the frame is painted once (the highest requested level wins). Windows queued for destruction are skipped. After a partial repaint (wgtInvalidatePaint only) the widget paint narrows the window's dirty area to the boxes of the widgets it repainted, joined and widened two pixels (a container that paints its children itself counts as its own box), so only that part of the screen is composited and flushed; a full repaint, an open popup list, and a window that paints itself composite the whole window.
    • Deferred destroy flush -- windows destroyed from inside callbacks are torn down once the dispatch depth is back to zero.
    • Compositor pass -- merge dirty rects, composite, flush to LFB.
    • -
    • Yield -- platformYield() or idle callback.
    • +
    • Yield -- the idle callback after a frame with nothing to paint, the busy callback after one that painted (the shell's run the idle handlers -- audio streams, network, serial pollers -- at least every 20 ms either way, and yield to the other tasks every frame), or platformYield() without them.

    Event Dispatch Chain

      Mouse/Keyboard Input
    @@ -3273,39 +3359,36 @@ prefsClose(h);
    -Wall -Wextra Full warnings

    Directory Layout

      dvxgui/
    -  +-- core/              Core library sources (dvxVideo, dvxDraw, dvxComp, dvxWm, dvxApp, widget infra)
    -  |   +-- platform/      Platform abstraction (dvxPlat.h, dvxPlatformDos.c, dvxPlatformUtil.c)
    -  |   +-- thirdparty/    stb_image, stb_ds, stb_image_write
    -  +-- loader/            Bootstrap loader (dvx.exe)
    -  +-- tasks/             Cooperative task switcher (libtasks.lib)
    -  +-- shell/             DVX Shell (dvxshell.lib)
    -  +-- widgets/           Widget DXE modules (*.wgt), each in its own subdirectory
    -  |   +-- box/           VBox/HBox layout containers
    -  |   +-- button/        Push button
    -  |   +-- textInput/     Text entry field
    -  |   +-- listView/      Multi-column list
    -  |   +-- ...            (29 widget types total)
    -  +-- texthelp/          Shared text editing helpers
    -  +-- listhelp/          Shared dropdown/list helpers
    -  +-- apps/              Application DXE modules (*.app)
    -  |   +-- progman/       Program Manager (desktop)
    -  |   +-- notepad/       Text editor
    -  |   +-- cpanel/        Control Panel
    -  |   +-- imgview/       Image viewer
    -  |   +-- clock/         Clock
    -  |   +-- dvxdemo/       Demo / showcase app
    -  |   +-- dvxbasic/      DVX BASIC compiler and VM
    -  +-- tools/             Build tools (dvxres resource compiler)
    -  +-- rs232/             ISR-driven UART driver
    -  +-- packet/            HDLC framing, CRC-16, sliding window
    -  +-- security/          DH key exchange, XTEA cipher, DRBG RNG
    -  +-- seclink/           Encrypted channel wrapper
    -  +-- serial/            Combined serial stack DXE
    -  +-- proxy/             Linux proxy (emulator <-> secLink <-> telnet)
    -  +-- sql/               SQLite integration
    -  +-- bin/               Build output (dvx.exe, system/, libs/, widgets/, apps/, shared/)
    -  +-- obj/               Intermediate object files
    -  +-- docs/              Documentation
    + +-- src/ + | +-- loader/ Bootstrap loader (dvx.exe) + | +-- libs/kpunch/ Libraries (*.lib), each in its own folder + | | +-- libdvx/ Core: video, drawing, compositor, windows, widget infrastructure + | | | +-- platform/ Platform layer (dvxPlat.h, dvxPlatformDos.c, dvxPlatformUtil.c) + | | | +-- thirdparty/ stb_image, stb_ds, stb_image_write + | | +-- libtasks/ Cooperative task switcher + | | +-- dvxshell/ DVX Shell (Program Manager's host, program loading) + | | +-- texthelp/ Shared text editing helpers + | | +-- listhelp/ Shared dropdown/list helpers + | | +-- serial/ rs232: ISR-driven UART driver + | | +-- crypto/ X25519, XChaCha20-Poly1305, Ed25519 (Monocypher), DH, XTEA, SHA-256/HMAC/HKDF, DRBG + | | +-- seclink/ Packet transport (HDLC, CRC-16, Go-Back-N) and the secure link + | | +-- dvxnet/ TCP/IP (lwIP) and the BSD socket layer + | | +-- pkg/ Add-on packages (ZIP, manifests, signatures, install and remove) + | | +-- sql/ SQLite integration + | | +-- ... audio, music, print, conn, xfer, http, json, and more + | +-- widgets/kpunch/ Widget modules (*.wgt), each in its own folder (35) + | +-- apps/kpunch/ Programs (*.app): progman, notepad, cpanel, dvxbasic, terminal, ... + | +-- savers/kpunch/ Screen savers + | +-- addons/ Add-on package sources (fonts, instrument banks) + | +-- include/basic/ BASIC include files (*.bas) + | +-- installer/ SETUP.EXE and the package tools + | +-- sdk/ SDK templates + | +-- tools/ Build tools (dvxres, dvxhlpc, dxeGc.sh, ...); proxy/ is the Linux secure link proxy + | +-- test/ Test suites (host and DOS) + +-- bin/ Build output (dvx.exe, system/, libs/, widgets/, apps/, shared/) + +-- obj/ Intermediate object files + +-- dist/ Release media and the SDK + +-- docs/ Documentation

    DVX GUI API Reference

    @@ -3328,6 +3411,8 @@ prefsClose(h);
  • dvxRes.h -- DXE-embedded resource system
  • dvxPrefs.h -- INI preferences system
  • dvxMem.h -- Per-app memory tracking
  • +
  • dvxEndian.h -- Byte order loads and stores
  • +
  • dvxHash.h -- Hashes, checksums, and hex digits
  • dvxDrv.h -- Driver modules
  • dvxWgt.h -- Widget system public API
  • dvxWgtP.h -- Widget plugin API
  • @@ -3348,6 +3433,8 @@ prefsClose(h);

    dvxRes.h -- Resource System

    dvxPrefs.h -- Preferences

    dvxMem.h -- Memory Tracking

    +

    dvxEndian.h -- Byte Order

    +

    dvxHash.h -- Hashes and Checksums

    dvxDrv.h -- Driver Modules

    dvxWgt.h -- Widget System

    dvxWgtP.h -- Widget Plugin API

    @@ -3509,7 +3596,7 @@ DVX_ABI_STAMP(mylib);

    Callbacks:

      Callback                                                     Description
       --------                                                     -----------
    -  onPaint(WindowT *win, RectT *dirtyArea)                              Content repaint requested
    +  onPaint(WindowT *win, RectT *dirtyArea)                              Content repaint requested; a painter that changed only part of dirtyArea may narrow it, and only that part is composited
       onKey(WindowT *win, int32_t key, int32_t mod)                        Key press
       onKeyUp(WindowT *win, int32_t scancode, int32_t mod)                 Key release
       onMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons)         Mouse event (content-relative)
    @@ -3799,10 +3886,12 @@ const char *dvxFontFamilyName(int32_t index);

    dvxFontRescan

    void dvxFontRescan(void);

    Read the FONTS folder again on the next call, after fonts were added or removed. The fonts no one holds are unloaded; open fonts stay valid.

    -

    dvxFontSetSmooth / dvxFontSmooth

    +

    dvxFontSetSmooth / dvxFontSmooth / dvxFontSmoothBegin / dvxFontRebuildStep

    void dvxFontSetSmooth(bool smooth);
    -bool dvxFontSmooth(void);
    -

    Smooth fonts where their face has the size smoothed, or stop; dvxFontSmooth says which it is (off at first). The fonts open are built again in place: the same BitmapFontT pointers with new glyphs and a new serial, so code that keeps measurements of a font compares its serial too, and text is laid out again. The fonts no one holds are unloaded. Programs call dvxSetFontSmoothing (dvxApp.h) instead, which follows the user's choice and the screen's depth.

    +bool dvxFontSmooth(void); +void dvxFontSmoothBegin(bool smooth); +bool dvxFontRebuildStep(void); +

    Smooth fonts where their face has the size smoothed, or stop; dvxFontSmooth says which it is (off at first). The fonts open are built again in place: the same BitmapFontT pointers with new glyphs and a new serial, so code that keeps measurements of a font compares its serial too, and text is laid out again. The fonts no one holds are unloaded. dvxFontSetSmooth builds them all before it returns; dvxFontSmoothBegin only switches and marks them, and each dvxFontRebuildStep builds one of them again (15-150 ms a font), false when none is left (the fonts before the fonts with lines, which copy them). Programs call dvxSetFontSmoothing (dvxApp.h) instead, which follows the user's choice and the screen's depth and builds the fonts a frame at a time.

    dvxFontAddFile / dvxFontAddMemory

    bool dvxFontAddFile(int32_t owner, const char *path);
     bool dvxFontAddMemory(int32_t owner, const void *data, uint32_t length, const char *name);
    @@ -3819,6 +3908,12 @@ bool dvxFontAddMemory(int32_t owner, const void *data, uint32_t length, const ch
    char *dvxFontLicence(const BitmapFontT *font);

    The texts that go with the face font was built from: its notice -- the family and style, the fonts it was converted from, and their copyright notices -- then a blank line and its licence. ASCII with CR LF line ends; malloc'd (the caller frees it).

    Returns: The texts; NULL for the system font, and when the font's file cannot be read.

    +

    Code Page 437 and Unicode

    +
    extern const uint16_t dfnCp437[DFN_CHAR_COUNT];
    +int32_t  dfnFromUnicode(uint32_t code);
    +int32_t  dfnTextFromUnicode(uint32_t code);
    +uint32_t dfnTextToUnicode(uint8_t ch);
    +

    Declared in dvxFontFmt.h. DVX text and its fonts are code page 437. dfnCp437[c] is the Unicode code point of character c's glyph (0 for none), so the characters below 32 are the smileys, card suits, and arrows the fonts draw for them; dfnFromUnicode is the character whose glyph a code point is, -1 for none. For text, dfnTextFromUnicode and dfnTextToUnicode keep ASCII (line ends, tabs, and the other controls included) as itself and map the rest through code page 437; dfnTextFromUnicode is -1 for a code point DVX has no character for. The JSON library reads and writes Unicode this way, and BASIC's ChrW$ and AscW are these two.

    dvxFlow.h -- Flowing Text

    @@ -5026,7 +5121,7 @@ void drawFocusRectDots(int32_t x, int32_t y, int32_t w, int32_t h, DrawDotFnT fn

    Color Scheme

    dvxSetColor

    void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b);
    -

    Set a single color by ID. Repacks to native pixel format and invalidates the entire screen.

    +

    Set a single color by ID. Repacks to native pixel format; every window is painted again at the next frame (once, however many colors change before it) and the whole screen composited.

      Parameter   Description
       ---------   -----------
       ctx         Application context
    @@ -5109,7 +5204,7 @@ void dvxWallpaperToPrefs(const AppContextT *ctx, PrefsHandleT *h);

    Text Smoothing

    dvxSetFontSmoothing

    void dvxSetFontSmoothing(AppContextT *ctx, bool on);
    -

    Smooth text, or stop (ctx->fontSmoothing; on after dvxInit). Text is smoothed when on and the screen has more than 8 bits per pixel (dvxFontSetSmooth, dvxText.h), in the faces and sizes that have a smoothed strike; dvxChangeVideoMode applies it again at the new depth. When what is drawn changes, every window is laid out and painted again.

    +

    Smooth text, or stop (ctx->fontSmoothing; on after dvxInit). Text is smoothed when on and the screen has more than 8 bits per pixel (dvxFontSmoothBegin, dvxText.h), in the faces and sizes that have a smoothed strike; dvxChangeVideoMode applies it again at the new depth. The fonts open are built again a font a frame (dvxUpdate calls dvxFontRebuildStep), and every window is laid out and painted again after the last.

    dvxFontsFromPrefs / dvxFontsToPrefs

    void dvxFontsFromPrefs(AppContextT *ctx, PrefsHandleT *h);
     void dvxFontsToPrefs(const AppContextT *ctx, PrefsHandleT *h);
    @@ -5169,6 +5264,12 @@ void dvxBeep(void); void dvxSetSoundHook(DvxSoundHookT fn);

    The function that plays the sounds, set by the sound library when it loads (NULL: silent). Programs do not call it.

    +

    dvxSetWaitPump / dvxWaitPump

    +
    typedef bool (*DvxWaitPumpT)(void);
    +
    +void dvxSetWaitPump(DvxWaitPumpT fn);
    +bool dvxWaitPump(void);
    +

    A library that waits for something (a sound to end, as dvxPlaySoundWait does) calls dvxWaitPump between its checks: DVX goes on meanwhile, and so do the events of the BASIC program that waits, as in DoEvents (the hook the BASIC runtime sets with dvxSetWaitPump). It returns false when the wait should stop: the program is ending, or its code may not wait (a control written in BASIC). Without a hook it yields the processor and returns true.

    Full-Screen Exclusive Mode

    A window's program takes the whole screen in a video mode of its own (a game at 320x200, say) while every other program keeps running: windows keep painting into their own buffers, and the desktop is drawn again when the program lets go. The owner's window must have the focus: when it loses it (Alt+Tab, Ctrl+Esc, a dialog, another window coming to the front) the desktop comes back and onChange hears ExclusiveSuspendedE; when the window has the focus again the mode is set again and onChange hears ExclusiveActiveE (draw the whole frame: the screen and the backbuffer start black). While active every key but Alt+Tab, Ctrl+Esc, and Alt+F4 goes to the window as in game input mode, the mouse cursor is not drawn, onMouse gets full-screen coordinates, dvxScreenshot saves the full screen, and dvxChangeVideoMode fails. Closing the window, the program ending or being ended, and dvxExclusiveEnd give the screen back. See the Writing Games guide.

    typedef enum {
    @@ -5185,6 +5286,10 @@ typedef void (*ExclusiveFnT)(WindowT *win, ExclusiveStateE state, void *userData
     

    dvxExclusiveEnd

    void dvxExclusiveEnd(AppContextT *ctx, WindowT *win);

    Give the screen back: the desktop's mode, palette, and mouse, and a full repaint. onChange is not called. Nothing when win does not have full screen.

    +

    dvxExclusiveMirror

    +
    int32_t dvxExclusiveMirror(AppContextT *ctx, WindowT *win, ExclusiveFnT onChange, void *userData);
    +

    Full screen for a window's own content (a window of widgets, a drawing) rather than a picture the program draws: the smallest mode of at least the content's size in the desktop's pixel format, the content scaled by the largest whole factor that fits and centred on black, and shown again whenever the window paints (any dirty rectangle over its content). The mouse works on the content as in the window -- onMouse gets content coordinates -- and its cursor is drawn, in the shape the content asks for. Keys, suspending, resuming, and giving back are as for dvxExclusiveBegin; the BASIC form runtime's Form.FullScreen is this.

    +

    Returns: as dvxExclusiveBegin; DVX_EXCL_ERR_MODE also when there is no mode in the desktop's pixel format.

    dvxExclusiveDisplay / dvxExclusiveBlitOps

    DisplayT       *dvxExclusiveDisplay(AppContextT *ctx);
     const BlitOpsT *dvxExclusiveBlitOps(AppContextT *ctx);
    @@ -5209,7 +5314,7 @@ bool platformPathAbsolute(const char *path, char *out, int32_t outSize);platformPathAbsolute makes a path absolute against the current folder (it need not exist).

    Optional Libraries: dvxLibraryLoaded

    bool dvxLibraryLoaded(const char *name);
    -

    Whether the library (or widget) name, its file's base name in any case ("pkgnet", "dvxsql"), is loaded: installed and started with DVX, which loads every library and widget it has at start. It looks for the module's ABI stamp (DVX_ABI_STAMP: name and DVX_ABI_SUFFIX), which the loader checked before it took the module; a module that is not there, or did not load, has none. Some parts of DVX are optional (Setup's "What to install" page installs them as bundles: src/packages.ini), so a program that uses one of their libraries only when it is there asks first, and does not import it: an import of a missing library stops the program from loading. BASIC: SysLibraryLoaded (system.bas).

    +

    Whether the library (or widget) name, its file's base name in any case ("pkgnet", "dvxsql"), is installed and loaded. DVX loads a library when something first needs it, so asking loads one that is installed but not loaded yet. It looks for the module's ABI stamp (DVX_ABI_STAMP: name and DVX_ABI_SUFFIX), which the loader checks before it takes the module; a module that is not there, or did not load, has none. Some parts of DVX are optional (Setup's "What to install" page installs them as bundles: src/packages.ini), so a program that uses one of their libraries only when it is there asks first, and does not import it: an import of a missing library stops the program from loading. BASIC: SysLibraryLoaded (system.bas).

    Idle Time: dvxIdleMs

    uint32_t dvxIdleMs(const AppContextT *ctx);

    The milliseconds since the last key or mouse input: a key (pressed, released, or typed), the mouse moving, a button, or the wheel, injected input (a test script, SysSendKey) included, and input that came while DVX was busy and was thrown away. DVX coming back from a DOS program or from full-screen mode counts as input too. The shell's screen saver starts after DVX.INI [desktop] saverWait minutes of it (shellSaver.h); BASIC: SysIdleSeconds. The time is kept in AppContextT.lastInputTime (platformClock ticks).

    @@ -5315,6 +5420,7 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId);

    dvxLoadImage

    uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch);

    Load an image file (BMP, PNG, JPEG, GIF) and convert to the display's native pixel format. A file that is no image but a DVX program, widget, or library with an icon resource gives that icon (dvxResLoadIconData), so every picture that takes a file name can show a program's icon. Caller must free with dvxFreeImage().

    +

    Called from a program's own task (where the task takes no input: a BASIC program), the image loaders decode a big picture a part at a time, a frame of DVX's every 32 KB read, so the other programs go on; none of the caller's code runs meanwhile. On the task that takes the input they decode in one go.

      Parameter     Description
       ---------     -----------
       ctx           Application context
    @@ -5354,12 +5460,15 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId);
    data Buffer to free

    dvxImageInfo

    bool dvxImageInfo(const char *path, int32_t *outW, int32_t *outH);
    -

    Query image dimensions without decoding the full file.

    +

    Query image dimensions without decoding the full file (its header only: a picture damaged after it may still fail to load).

      Parameter     Description
       ---------     -----------
       path          Image file path
       outW, outH    Output: image dimensions

    Returns: true on success; false for an unreadable image or one without pixels.

    +

    dvxImageInfoFromMemory

    +
    bool dvxImageInfoFromMemory(const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH);
    +

    dvxImageInfo for an image's bytes in memory (as LoadPicture checks a picture before a control shows it).

    dvxSaveImage

    int32_t dvxSaveImage(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path);

    Save native-format pixel data to an image file: BMP when path ends in .bmp (case-insensitive), PNG otherwise.

    @@ -6028,13 +6137,14 @@ static const ButtonApiT sApi = {

    WgtValueT

    typedef struct {
         uint8_t type;   // WGT_IFACE_*
    +    int32_t len;    // STRING: its length when it may hold NUL bytes; 0 = up to its NUL
         union {
             int32_t     i;   // INT, BOOL (0 / 1), ENUM
             double      f;   // FLOAT, DOUBLE
             const char *s;   // STRING
         };
     } WgtValueT;
    -

    One property value, method argument or result, or event argument. WGT_IFACE_NONE (5) is "no value" (a void method's result). A STRING value never owns its text: an argument's text belongs to the caller for the duration of the call; a returned one (a property read, a method result) belongs to the widget and stays valid until the next call on that widget or until the widget changes or is destroyed -- copy it to keep it. wgtValueInt, wgtValueBool, wgtValueFloat, wgtValueDouble, and wgtValueStr (NULL becomes "") build values. FLOAT is BASIC's Single, DOUBLE its Double: a DOUBLE property or argument keeps every digit (a value through FLOAT is rounded to a float by a static widget's getter and setter, and the form runtime shows it as a Single); as text a DOUBLE has 15 significant digits.

    +

    One property value, method argument or result, or event argument. WGT_IFACE_NONE (5) is "no value" (a void method's result). A STRING value never owns its text: an argument's text belongs to the caller for the duration of the call; a returned one (a property read, a method result) belongs to the widget and stays valid until the next call on that widget or until the widget changes or is destroyed -- copy it to keep it. wgtValueInt, wgtValueBool, wgtValueFloat, wgtValueDouble, and wgtValueStr (NULL becomes "") build values; wgtValueBytes(s, len) builds a STRING of len bytes that may hold NUL bytes (s[len] must still be a NUL). A BASIC string holds any bytes, so the form runtime passes every string this way, and a STRING's length is wgtValueLen(v): its len, or up to its NUL when len is 0. A widget that takes binary text (Terminal Write) reads that length; one that wants C text reads s, which stops at the first NUL. FLOAT is BASIC's Single, DOUBLE its Double: a DOUBLE property or argument keeps every digit (a value through FLOAT is rounded to a float by a static widget's getter and setter, and the form runtime shows it as a Single); as text a DOUBLE has 15 significant digits.

    wgtValueToInt / wgtValueToBool / wgtValueToFloat / wgtValueToText

    int32_t     wgtValueToInt(const WgtValueT *v);
     bool        wgtValueToBool(const WgtValueT *v);
    @@ -6438,6 +6548,9 @@ if (dvxFontDialog(ctx, NULL, 0, &choice)) {
     
    PrefsHandleT *prefsCreate(void);

    Create an empty preferences handle with no associated file. Use this when building an INI from scratch before calling prefsSaveAs.

    Returns: New handle, or NULL on allocation failure.

    +

    prefsDvxIniShared

    +
    PrefsHandleT *prefsDvxIniShared(void);
    +

    DVX.INI for reading, shared: parsed again only when the file's text changed (its text is read and compared, a fraction of a parse), for code that reads settings often (every sound, every song). Its users do not close it, change it, or keep it past their own call: the next call may read the file again.

    prefsLoad

    PrefsHandleT *prefsLoad(const char *filename);

    Load an INI file into a new handle. If the file does not exist but a backup copy left by an interrupted save does (same name with the extension replaced by ~ and its first two characters, e.g. DVX.~IN), the backup is read instead, so the next save restores the file. Otherwise a missing file gives a valid empty handle. Either way the path is stored so prefsSave can write the file later.

    @@ -6574,7 +6687,7 @@ extern const PrefsChoiceT prefsWheelDirChoices[PREFS_WHEEL_DIR_CHOICE_COUNT];

    dvxMem.h -- Per-App Memory Tracking

    -

    Wraps malloc/free/calloc/realloc/strdup with a 16-byte header that records the owning app ID and allocation size. The Task Manager displays per-app memory usage, and leaks are detected when an app is terminated.

    +

    Wraps malloc/free/calloc/realloc/strdup with a 16-byte header that records the owning app ID and allocation size. The Task Manager displays per-app memory usage. The accounting is all it is: when an app ends its count starts over, and nothing it allocated is freed for it.

    DXE code does NOT need to include this header for tracking to work. The DXE export table maps malloc/free/calloc/realloc/strdup to these wrappers transparently. Call sites that need the explicit dvxXxx names (e.g. dvxMemGetAppUsage) or the dvxMemAppIdPtr declaration should include dvxMem.h directly.

    Globals

    dvxMemAppIdPtr

    @@ -6606,10 +6719,51 @@ extern const PrefsChoiceT prefsWheelDirChoices[PREFS_WHEEL_DIR_CHOICE_COUNT];Returns: Bytes allocated for this app.

    dvxMemResetApp

    void dvxMemResetApp(int32_t appId);
    -

    Free every tracked allocation charged to the given app. Called by the shell after an app exits or crashes to reclaim its memory.

    +

    Start the given app's accounting over: its usage reads 0, and a block it allocated that is freed later is not subtracted again (the block carries the old generation). Nothing is freed: a library's buffers made while the app ran stay valid. The shell calls it when an app ends.

      Parameter   Description
       ---------   -----------
       appId       App ID to flush
    +

    dvxMemSetApp

    +
    int32_t dvxMemSetApp(int32_t appId);
    +

    Make appId the current app (the one what is allocated from now is charged to, as the shell sees it too) and return the one before, to set back afterwards. A library keeps its own memory as DVX's (DVX_MEM_SYSTEM, 0) whichever app's call made it, so the Task Manager does not charge a cache or a font to the program that happened to ask first; the audio library calls a program's callback as that program. Without a shell (host tools) nothing changes and 0 comes back.

    +
    int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
    +// ... allocations that belong to the library ...
    +dvxMemSetApp(prev);
    +
    +
    +

    dvxEndian.h -- Byte Order

    +

    File formats and wire protocols fix their byte order. These inline functions read and write it a byte at a time, so they work at any alignment and on any computer. Header only: include it and call, nothing to link.

    +
    uint16_t dvxGetBe16(const uint8_t *p);
    +uint32_t dvxGetBe32(const uint8_t *p);
    +uint16_t dvxGetLe16(const uint8_t *p);
    +uint32_t dvxGetLe24(const uint8_t *p);
    +uint32_t dvxGetLe32(const uint8_t *p);
    +void     dvxPutBe32(uint8_t *p, uint32_t v);
    +void     dvxPutLe16(uint8_t *p, uint32_t v);
    +void     dvxPutLe32(uint8_t *p, uint32_t v);
    +

    Get reads the value at p, high byte first (Be) or low byte first (Le); Put writes v at p the same way (dvxPutLe16 writes the low 16 bits of v). Cast a load to a signed type for a signed field, for example (int16_t)dvxGetLe16(p) for a 16-bit sample.

    +
    +
    +

    dvxHash.h -- Hashes, Checksums, and Hex Digits

    +

    Small inline functions every part of DVX uses for the same jobs: FNV-1a for hash tables and change detection, CRC-16-CCITT for serial packets and XMODEM / ZMODEM, and hex digits both ways. Header only: include it and call, nothing to link.

    +

    FNV-1a

    +
    #define DVX_FNV_BASIS 2166136261u
    +#define DVX_FNV_PRIME 16777619u
    +
    +uint32_t dvxFnvByte(uint32_t hash, uint8_t byte);
    +uint32_t dvxFnvBytes(uint32_t hash, const void *data, int32_t len);
    +

    Start from DVX_FNV_BASIS and fold in bytes. dvxFnvByte takes one, so a caller can fold case or skip characters as it goes; dvxFnvBytes takes len bytes. Both return the hash carried on.

    +

    CRC-16-CCITT

    +
    uint16_t dvxCrc16(uint16_t crc, const uint8_t *data, int32_t len);
    +

    The CRC of len bytes (polynomial 0x1021, high bit first), carried on from crc, computed a byte at a time without a table. Seed 0 for XMODEM and ZMODEM, 0xFFFF for CCITT-FALSE (the serial packet layer's).

    +

    Hex Digits

    +
    #define DVX_HEX_DIGITS  "0123456789abcdef"
    +#define DVX_HEX_RADIX   16
    +#define DVX_NIBBLE_BITS 4
    +#define DVX_NIBBLE_MASK 0x0F
    +
    +int32_t dvxHexValue(int32_t c);
    +

    DVX_HEX_DIGITS[n] is the digit for the nibble n. dvxHexValue is a digit's value (0 to 15, either case), -1 for any other character.

    dvxWgtP.h -- Widget Plugin API

    @@ -6842,7 +6996,7 @@ PlatformTicksT platformClockHz(void);

    Monotonic tick counter for UI timing (double-click, tooltip delay, cursor blink, timers), advancing platformClockHz() ticks per second. PlatformTicksT is unsigned so "now - then" is wrap-safe: always compare intervals by subtraction, never raw tick values. PLATFORM_MS_TO_TICKS converts milliseconds to ticks at the platform rate, PLATFORM_TICKS_TO_MS ticks to milliseconds (the millisecond clock the conn and xfer engines take is PLATFORM_TICKS_TO_MS(platformClock())).

    platformEntropyStamp

    uint64_t platformEntropyStamp(void);
    -

    Fine-grained, free-running timestamp for input-timing entropy. It is not a clock: units and wrap point are backend-specific. On DOS it returns the CPU time-stamp counter (RDTSC) when CPUID reports one, otherwise the BIOS tick count shifted left 16 bits with the live PIT channel 0 counter (about 0.84 us resolution) in the low 16 bits. Used by dvxSetInputEntropyHook.

    +

    Fine-grained, free-running timestamp for entropy. It is not a clock: units and wrap point are backend-specific. On DOS it reads the live PIT channel 0 counter (about 0.84 us resolution, its own crystal) and returns it in the top 16 bits of the CPU time-stamp counter (RDTSC) when CPUID reports one, so a sample carries the jitter between the two clocks; otherwise the BIOS tick count shifted left 16 bits with the PIT counter in the low 16 bits. Used by dvxSetInputEntropyHook and by the security library's entropy gathering.

    platformMicros

    uint64_t platformMicros(void);

    Monotonic microseconds, the high-resolution clock for games and measurements. On DOS it counts from the start of the timer service (dvxInit): the PIT input clocks the timer interrupt has counted plus the part of the running period, read from the latched channel 0 count (about 0.84 us steps), or on a CPU with a time-stamp counter the TSC cycles since the last interrupt at the rate measured over the interrupt period before it, so the clock never drifts from the PIT and a TSC whose rate changes (power saving, an emulator) does no harm. The host backend returns its fake clock. Not for use inside a timer callback.

    @@ -6991,6 +7145,14 @@ bool platformShortPath(const char *path, char *out, int32_t outSize);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.

    +

    platformFileStamp

    +
    typedef struct {
    +    uint32_t size;      // bytes (a file of 4 GB or more: 0xFFFFFFFF)
    +    uint32_t time;      // its date and time, as the platform keeps them
    +} PlatformStampT;
    +
    +bool platformFileStamp(const char *path, PlatformStampT *out);
    +

    A file's size and time from its folder alone, for telling whether a file read before has changed: compare two stamps for equality only. On DOS it is one directory search; stat makes several DOS calls there, tens of milliseconds each on a slow disk. Elsewhere it is stat, with the time's nanoseconds folded in. False when path is no file, or holds a wildcard. DOS keeps a file's time to two seconds, so a file rewritten at the same size within two seconds keeps its stamp. The shell's program cache checks the programs it keeps with it.

    platformMkdirRecursive

    int32_t platformMkdirRecursive(const char *path);

    Create a directory and all parent directories (like mkdir -p). Existing directories are not an error. Returns 0 on success, -1 on failure (including a path of DVX_MAX_PATH characters or more).

    @@ -7013,6 +7175,8 @@ char *platformPathTidy(char *path);
  • 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.
  • +
  • platformPathParent: the folder holding path, a trailing separator ignored, in native separators ("C:/DVX/APPS/" gives "C:\\DVX", "/a" gives "/"). False for a root, a name without a folder, or when it does not fit.
  • +
  • platformPathIsRoot: true for a drive or file-system root: "C:", "C:\\", "/", or "\\".
  • 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

    @@ -7040,6 +7204,11 @@ bool platformPathSibling(char *out, int32_t outSize, const char *path, const cha

    platformWriteFile

    bool platformWriteFile(const char *path, const void *data, int32_t len);

    Write len bytes of data as the whole of path, all or nothing: into the PLATFORM_TMP_EXT sibling, then swapped in with platformReplaceFile, so a full disk or an I/O error leaves the file as it was. False when it was not done. The preferences system, the IDE's saves, Setup, and the package records use it.

    +

    platformWalkDir

    +
    typedef bool (*PlatformWalkFnT)(const char *path, const char *name, void *ctx);
    +
    +bool platformWalkDir(const char *dir, PlatformWalkFnT fn, void *ctx);
    +

    Visit every file below dir, depth first, entering folders as they come and skipping names that start with '.'. A folder's names are read in full before any is visited (DOS has few file handles), and which are folders comes with them (platformDirEntryIsDir), not from a stat of each. fn gets each file's full path and its name; returning false stops the walk, and platformWalkDir then returns false too. A folder that cannot be read is skipped. The loader's module and help scans and the shell's program list use it.

    platformReadFile

    char *platformReadFile(const char *path, int32_t *outLen);

    Slurp a whole file into a freshly malloc'd, NUL-terminated buffer. Caller frees. Binary-safe: the NUL is past the end of the reported length.

    @@ -7120,7 +7289,7 @@ char **platformKindReadDir(DvxKindE kind); SHARED FONTS, SOUNDS, THEMES, WPAPER, BANKS, and SAVERS: what DVX and add-on packages ship for every program (an add-on's in a folder of its vendor's, such as FONTS\ACME) USER (the user folder) DVX.INI, ASSOC.INI (the user's changes to SYSTEM's), DVX.LOG, the user's own FONTS, SOUNDS, THEMES, WPAPER, BANKS, and SAVERS, KEYS (publishers' keys the user trusts), APPS\<program path> (each program's settings and saved files), and DOWNLOAD

    Only packages put files in SHARED: DVX's own, and add-on packages (pkg.lib), each in its vendor's folders. A program reads it but never writes or installs into it; a program's own files live with it (its DATA folder, or resources inside the .app), under its own vendor and program folder, so two programs' files never collide.

    -

    platformUserInit, called once by the loader, decides the user folder: the folder the DVXUSER environment variable names, else USER in the DVX folder, made when missing. When it cannot be written (DVX on a CD-ROM without DVXUSER), DVXUSER in the TEMP folder is used instead and platformUserInTemp is true; without a TEMP folder nothing can be saved and platformIsReadOnly is true. The first start copies SYSTEM\DVX.INI to the user folder. platformUserDir is the folder (DVXUSER or USER before platformUserInit has decided); platformUserPath joins a name to it; platformDvxIniPath is its DVX.INI. platformUserDirOf is the user folder of a DVX installed in another folder (DVXUSER, else that folder's USER), for Setup.

    +

    platformUserInit, called once by the loader (and not exported to modules), decides the user folder: the folder the DVXUSER environment variable names, else USER in the DVX folder, made when missing. When it cannot be written (DVX on a CD-ROM without DVXUSER), DVXUSER in the TEMP folder is used instead and platformUserInTemp is true; without a TEMP folder nothing can be saved and platformIsReadOnly is true. The first start copies SYSTEM\DVX.INI to the user folder. platformUserDir is the folder (DVXUSER or USER before platformUserInit has decided); platformUserPath joins a name to it; platformDvxIniPath is its DVX.INI. platformUserDirOf is the user folder of a DVX installed in another folder (DVXUSER, else that folder's USER), for Setup.

    platformAppUserDir is a program's folder in the user folder: APPS\KPUNCH\TERMINAL\TERMINAL.APP gives USER\APPS\KPUNCH\TERMINAL; a program outside the DVX folder gives USER\APPS\<its file name without the extension>. It is not made here (the shell's userDir and BASIC's App.User are made on first use).

    SHARED and the user folder have the same six kinds of folder (DvxKindE): fonts, sounds, themes, desktop pictures, MIDI instrument banks, and screen savers. Each may hold folders of add-on vendors (SHARED\FONTS\ACME), one level deep, whose files are named VENDOR\NAME. platformKindName is a kind's folder name; platformKindDir is SHARED's (user false) or the user's (user true); platformKindBrowseDir is where a file dialog for the kind starts (the user's when it exists, else SHARED's). platformKindFind finds a file of a kind by name (NAME or VENDOR\NAME, either separator), the user's first, so a user's file of the same name wins; any other name with a folder in it, or an absolute one, is taken as the path it is (a VENDOR\NAME not in the kind's folders is tried that way too). platformKindNameOf is the other way: a file in one of the kind's folders gives its name alone, or VENDOR\NAME in a vendor's folder, which is what settings store (DVX.INI [sounds], [desktop] wallpaper and saver, and [audio] midiBank), so moving the user folder or DVX keeps them working. platformKindReadDir lists both folders: the user's names (sorted, case ignored), then SHARED's not among them, with each vendor folder's files as VENDOR\NAME (the folders themselves are not listed); free it with dvxReadDirFree. BASIC: FileOpsUserDir$, FileOpsUserPath$, FileOpsUserInTemp, FileOpsKindDir$, FileOpsKindBrowseDir$, FileOpsKindFind$, FileOpsKindName$, and FileOpsKindList (fileops.bas, with FILEOPS_KIND_FONTS to FILEOPS_KIND_SAVERS), and App.User for a program's own folder.

    Read-only media: platformSetReadOnly / platformIsReadOnly / platformDirIsWritable / platformTempDir / platformSetLogMemory / platformLogMemory

    @@ -7165,10 +7334,10 @@ while ((p = dvxNextArg(p, arg, sizeof(arg), &quoted)) != NULL) {

    DXE Module Support

    platformRegisterDxeExports

    void platformRegisterDxeExports(void);
    -

    Register platform and C runtime symbols with the dynamic module loader so DXE modules can resolve them at load time.

    +

    Register platform and C runtime symbols with the dynamic module loader so DXE modules can resolve them at load time. The loader's own, so not exported to modules.

    platformRegisterSymOverrides

    void platformRegisterSymOverrides(const PlatformSymOverrideT *entries);
    -

    Register function pointer overrides for subsequently loaded modules. entries is terminated by {NULL, NULL}. On DOS this wraps dlregsym(). Nothing in DVX itself calls it: it is SDK API for embedders and test harnesses that need to substitute a symbol (e.g. a stub) before loading a module.

    +

    Register function pointer overrides for subsequently loaded modules. entries is terminated by {NULL, NULL}. On DOS this wraps dlregsym(). Nothing in DVX itself calls it: it is SDK API for embedders and test harnesses that need to substitute a symbol (e.g. a stub) before loading a module, so code linked with the platform layer, not a module (it is not exported).

    Interrupt-Safe Memory

    platformLockRegion / platformUnlockRegion

    int32_t platformLockRegion(const void *addr, uint32_t size, bool isCode);
    @@ -7231,7 +7400,7 @@ if (voice < 0) {
     }
     ...
     audioClose(audio);                              // or let the program end
    -

    The first audioOpen starts the sound device and the last audioClose stops it. In between, after AUDIO_IDLE_CLOSE_SEC (3) seconds with nothing playing, the device's output is closed so it costs no CPU time (desktop sounds would otherwise keep it mixing silence), and the next sound opens it again with the same driver before it starts, so nothing of it is lost; audioGetInfo still reports the device. Functions returning int32_t give AUDIO_OK or a voice id (above 0) on success and a negative AUDIO_ERR_* code on failure; audioErrorText says what a code means. audioGetInfo describes the device: the driver's name and description, the mixing rate it achieved, its output bits and channels, the voice limit, and whether it plays sampled sound (hasDac), has a hardware synthesizer (hasSynth), or plays tones on a hardware tone generator (hasTone, the PC speaker). audioGetStats counts mixed blocks and frames, stream underruns, clipped samples, dropped voices, and the voices playing.

    +

    The first audioOpen starts the sound device and the last audioClose stops it; the driver modules it used stay loaded, and the next first audioOpen tries them first (no other driver is loaded or probed) while DVX.INI's driver choice is the same. In between, after AUDIO_IDLE_CLOSE_SEC (10) seconds with nothing playing, the device's output is closed so it costs no CPU time (desktop sounds would otherwise keep it mixing silence), and the next sound opens it again with the same driver before it starts, so nothing of it is lost; audioGetInfo still reports the device. Functions returning int32_t give AUDIO_OK or a voice id (above 0) on success and a negative AUDIO_ERR_* code on failure; audioErrorText says what a code means. audioGetInfo describes the device: the driver's name and description, the mixing rate it achieved, its output bits and channels, the voice limit, and whether it plays sampled sound (hasDac), has a hardware synthesizer (hasSynth), or plays tones on a hardware tone generator (hasTone, the PC speaker). audioGetStats counts mixed blocks and frames, stream underruns, clipped samples, dropped voices, and the voices playing.

    None of the functions may be called from an interrupt (a timer-service callback).

    Samples and Voices

    audioSampleLoad (file), audioSampleLoadMem (memory), and audioSampleCreate (raw PCM in an AudioFormatT: rate, 8 bits unsigned or 16 bits signed, 1 or 2 channels) make an AudioSampleT; audioSampleFree frees it and stops the voices playing it. audioPlay(sample, volume, pan, loop) plays it on a new voice; audioPlayFile loads a file and plays it, freeing the sample when the voice ends.

    @@ -7250,13 +7419,13 @@ audioClose(audio); // or let the program end audioNotesOpen(audio, AUDIO_WAVE_SQUARE, AUDIO_VOLUME_MAX, &tune); audioNotesAdd(tune, 440 * AUDIO_MILLIHZ_PER_HZ, 250000); // A for 1/4 s audioNotesAdd(tune, 0, 50000); // a rest -

    A note queue plays tones and rests one after the other, each lasting exactly as long as asked (its length is counted in output frames from the time queued since the queue last went quiet, so many short notes do not drift). audioNotesAdd(notes, milliHz, us) appends a tone of milliHz thousandths of a hertz (AUDIO_TONE_MIN_HZ to AUDIO_TONE_MAX_HZ, or 0 for a rest) lasting us microseconds; up to AUDIO_NOTES_MAX entries may wait (AUDIO_ERR_BUSY beyond). audioNotesQueued says how many entries have not finished (and how many of them are tones), audioNotesSpace how many more fit, audioNotesClear silences the queue at once. The queue plays on its own voice (audioNotesVoice, for volume and pan) that the voice limit never drops, in the mixer or on the PC speaker's tone generator (never on the FM chip); it is silent while empty. music.lib plays QBasic's PLAY and SOUND through note queues.

    +

    A note queue plays tones and rests one after the other, each lasting exactly as long as asked (its length is counted in output frames from the time queued since the queue last went quiet, so many short notes do not drift). audioNotesAdd(notes, milliHz, us) appends a tone of milliHz thousandths of a hertz (AUDIO_TONE_MIN_HZ to AUDIO_TONE_MAX_HZ, or 0 for a rest) lasting us microseconds; up to AUDIO_NOTES_MAX entries may wait (AUDIO_ERR_BUSY beyond). audioNotesSetTone(notes, wave, level) sets the waveform and loudness (0..AUDIO_VOLUME_MAX, a share of the queue's volume) of the entries added from then on -- each entry plays in its own, so a tune changes them in time -- and a tone generator plays a tone at level 0 as a rest. audioNotesQueued says how many entries have not finished (and how many of them are tones), audioNotesSpace how many more fit, audioNotesClear silences the queue at once. The queue plays on its own voice (audioNotesVoice, for volume and pan) that the voice limit never drops, in the mixer or on the PC speaker's tone generator (never on the FM chip); it is silent while empty. music.lib plays QBasic's PLAY and SOUND through note queues.

    Streams

    AudioFormatT  fmt    = { 22050, 16, 2 };
     AudioStreamT *stream = NULL;
     
     audioStreamOpen(audio, &fmt, 250, fillMusic, &player, &stream);
    -

    audioStreamOpen(ctx, fmt, bufferMs, fill, userData, &stream) opens a stream of any rate from AUDIO_RATE_MIN (4000) to AUDIO_RATE_MAX (48000), 8 or 16 bits, mono or stereo, with a buffer of bufferMs milliseconds (0: 250). It plays at once on its own voice (audioStreamVoice), silence until samples arrive. Without fill the program writes samples with audioStreamWrite, which takes as many frames as fit and returns how many; audioStreamSpace and audioStreamQueued tell how much room and how much sound are left. With fill, the library asks for samples whenever the buffer has room: fill(stream, buf, frames, userData) writes up to frames frames in the stream's format and returns how many. Fill functions run from audioPump and the shell's idle time; a program that keeps the desktop busy every frame should call audioPump once a frame. A stream that runs dry plays silence and counts underruns. audioStreamFlush drops what the stream holds and has not played yet (a seek or a stop is heard at once; the two frames the mixer has read ahead still play). audioStreamClose stops and frees it.

    +

    audioStreamOpen(ctx, fmt, bufferMs, fill, userData, &stream) opens a stream of any rate from AUDIO_RATE_MIN (4000) to AUDIO_RATE_MAX (48000), 8 or 16 bits, mono or stereo, with a buffer of bufferMs milliseconds (0: 250). It plays at once on its own voice (audioStreamVoice), silence until samples arrive. Without fill the program writes samples with audioStreamWrite, which takes as many frames as fit and returns how many; audioStreamSpace and audioStreamQueued tell how much room and how much sound are left. With fill, the library asks for samples whenever the buffer has room: fill(stream, buf, frames, userData) writes up to frames frames in the stream's format and returns how many. Fill functions run from audioPump and the shell's idle time; a program that keeps the desktop busy every frame should call audioPump once a frame. Once a stream holds 100 ms of sound, a pump fills it for 15 ms at most (STREAM_FILL_BUDGET_US) and the next pump goes on, so a fill function that takes its time (a module rendered, a file read) does not hold DVX up after a start, a seek, or a stall. A stream that runs dry plays silence and counts underruns. audioStreamEnd says no more frames will come after those written (a fill function may call it with its last ones): the fill function is not asked again, and once the stream has played what it holds its voice ends as a sample's does (AUDIO_END_FINISHED to the end function), with nothing counted as an underrun; the stream stays open until audioStreamClose. Without it a stream that has played its last frame runs on in silence, counting underruns, until it is closed. audioStreamFlush drops what the stream holds and has not played yet (a seek or a stop is heard at once; the two frames the mixer has read ahead still play). audioStreamClose stops and frees it.

    audioDrv.h also declares, for audio.lib and the libraries built on it (music.lib), audioDecode (a WAV or VOC file in memory as 16-bit frames), audioConvert (8- or 16-bit frames as 16-bit ones), and audioWavParse(head, headBytes, fileBytes, &fmt, &dataAt, &dataBytes) (a WAV file's format and where its frames are, from its first bytes: the frames are clamped to the file and whole; AUDIO_ERR_FORMAT for anything but a RIFF WAVE file, AUDIO_ERR_UNSUPPORTED, AUDIO_ERR_CORRUPT).

    Recording

    AudioFormatT fmt = { 22050, 16, 1 };
    @@ -7290,7 +7459,7 @@ note.addr = addr;
     note.end  = frames;
     note.bits = 16;
     audioWtStart(audio, v, &note);                  // pitch 0: AUDIO_WT_PITCH_RATE (44,100) frames a second
    -

    A wavetable synthesizer (a Gravis UltraSound's GF1, a Sound Blaster AWE32's or AWE64's EMU8000) plays samples from its own memory, and its voices and memory are shared by every program. audioWtGetInfo tells the chip (description, voices, free voices, memory, the largest free block, what it does itself: AUDIO_WT_ENVELOPES, AUDIO_WT_LFOS, AUDIO_WT_FILTER, AUDIO_WT_EFFECTS, AUDIO_WT_8BIT for 8-bit samples, AUDIO_WT_BANK_256K when a 16-bit sample must not cross a 256 KB boundary, AUDIO_WT_ROM for a General MIDI sample ROM; and its level, the centibels a voice at full volume is louder than full-scale digital sound, measured: 0 on a GUS, whose digital sound plays on its own voices, 76 on an AWE). audioWtMemAlloc(ctx, bytes, bits, &addr) takes memory for samples of bits bits (AUDIO_ERR_NO_ROOM when no free block is large enough), audioWtUpload(ctx, addr, frames, count, bits) copies 16-bit frames into it at bits, audioWtMemFree gives it back. audioWtAlloc(ctx) takes a voice (the least recently given back first), audioWtStart(ctx, voice, note) starts it on a note (AudioWtVoiceT: the sample's address, or AUDIO_WT_ADDR_ROM with an address in the ROM, and in frames from there the one it starts from (offset: before the loop or inside it), its loop, and its end; pitch in cents, 0 for AUDIO_WT_PITCH_RATE (44,100) frames a second; attenuation in centibels; pan; the filter, reverb and chorus; both envelopes in timecents; both LFOs; audioWtNoteInit makes a plain one, with no envelope or LFO shaping it, the filter open, no effects, centred, at full volume, for the caller to set the sample on), audioWtUpdate changes what its what bits say (AUDIO_WT_SET_PITCH, _VOLUME, _PAN, _FILTER, _EFFECTS, _LFO), audioWtRelease lets the key up, audioWtStop stops it at once, audioWtActive is false once it is silent, audioWtFree gives it back. The chip does what its flags say; the program does the rest by updating the voice as the note goes on. Closing the context gives back its voices and its memory. The voice calls may be made from a timer callback (music.lib plays SoundFont MIDI and modules that way); the memory calls may not.

    +

    A wavetable synthesizer (a Gravis UltraSound's GF1, a Sound Blaster AWE32's or AWE64's EMU8000) plays samples from its own memory, and its voices and memory are shared by every program. audioWtGetInfo tells the chip (description, voices, free voices, memory, the largest free block, what it does itself: AUDIO_WT_ENVELOPES, AUDIO_WT_LFOS, AUDIO_WT_FILTER, AUDIO_WT_EFFECTS, AUDIO_WT_8BIT for 8-bit samples, AUDIO_WT_BANK_256K when a 16-bit sample must not cross a 256 KB boundary, AUDIO_WT_ROM for a General MIDI sample ROM; and its level, the centibels a voice at full volume is louder than full-scale digital sound, measured: 0 on a GUS, whose digital sound plays on its own voices, 76 on an AWE). audioWtMemAlloc(ctx, bytes, bits, &addr) takes memory for samples of bits bits (AUDIO_ERR_NO_ROOM when no free block is large enough), audioWtUpload(ctx, addr, frames, count, bits) copies 16-bit frames into it at bits, audioWtMemFree gives it back. audioWtAlloc(ctx) takes a voice (the least recently given back first), audioWtStart(ctx, voice, note) starts it on a note (AUDIO_ERR_ARG for a sample in a ROM the chip does not have; AudioWtVoiceT: the sample's address, or AUDIO_WT_ADDR_ROM with an address in the ROM, and in frames from there the one it starts from (offset: before the loop or inside it), its loop, and its end; pitch in cents, 0 for AUDIO_WT_PITCH_RATE (44,100) frames a second; attenuation in centibels; pan; the filter, reverb and chorus; both envelopes in timecents; both LFOs; audioWtNoteInit makes a plain one, with no envelope or LFO shaping it, the filter open, no effects, centred, at full volume, for the caller to set the sample on), audioWtUpdate changes what its what bits say (AUDIO_WT_SET_PITCH, _VOLUME, _PAN, _FILTER, _EFFECTS, _LFO), audioWtRelease lets the key up, audioWtStop stops it at once, audioWtActive is false once it is silent, audioWtFree gives it back. The chip does what its flags say; the program does the rest by updating the voice as the note goes on. Closing the context gives back its voices and its memory. The voice calls may be made from a timer callback (music.lib plays SoundFont MIDI and modules that way); the memory calls may not.

    MIDI Port

    With DVX.INI [audio] midi = mpu401 the mpu401 driver opens an MPU-401 (or compatible, such as a Sound Blaster 16's) in UART mode, for an external synthesizer such as a Roland MT-32 or SC-55. The port belongs to one program at a time: audioMidiAcquire(ctx) (AUDIO_ERR_BUSY when another has it, AUDIO_ERR_NO_SYNTH without a port), audioMidiPut(ctx, byte) sends a byte and returns AUDIO_ERR_BUSY when the port cannot take one yet (send it again later; it may be called from a timer callback), audioMidiRelease gives it back (closing the context does too). audioMidiGetInfo tells whether there is a port and whether it is in use. music.lib uses the port for MIDI music.

    MIDI Input

    @@ -7328,7 +7497,7 @@ audioWtStart(audio, v, &note); // pitch 0: AUDIO_WT_PITCH_R midiBank the OP2 instrument bank for FM MIDI (music.lib): its name in SHARED\BANKS or the user's BANKS folder (an add-on's as VENDOR\NAME), or a path from the DVX folder; not set, the bundled one (Control Panel's Sounds page picks it)

    A settings screen reads and writes the keys it offers through the library, so the key names live in one place (audio.h, Settings; prefs is a copy of DVX.INI the caller loads and saves): audioSettingDriver(prefs) (AUDIO_DRIVER_AUTO when missing), audioSettingMidi(prefs) (an index in audioMidiChoices, auto when missing or unknown), audioSettingMidiIn(prefs) (an index in audioMidiInChoices, none when missing or unknown), audioSettingModules(prefs) (an index in audioModulesChoices, wavetable when missing or unknown), audioSettingVolume(prefs) (percent, clamped to 0..100, 100 when missing), and audioSettingsStore(prefs, driver, midi, modules, volume) writes all four (an empty driver stores auto, a midi index out of range stores auto, a modules index out of range wavetable); audioSettingMidiInStore(prefs, midiIn) writes midiIn (an index out of range stores none), which takes effect when the device next starts. audioMidiChoices[AUDIO_MIDI_CHOICE_COUNT] holds the midi choices in list order, each with its label for people and the name stored (a PrefsChoiceT, dvxPrefs.h); audioMidiInChoices[AUDIO_MIDI_IN_CHOICE_COUNT] the midiIn choices, audioModulesChoices[AUDIO_MODULES_CHOICE_COUNT] the modules choices (wavetable: on a wavetable synthesizer's voices when it is free and the module fits there; software: always mixed in software). audioPercentToVolume(percent) turns a percent into 0..AUDIO_VOLUME_MAX for audioSetMasterVolume. The device reads the driver and volume through the same functions when it starts. The Control Panel's Sounds page uses all of these.

    Desktop Sounds

    -

    The library plays the sounds DVX makes for its events (dvxPlaySound in dvxApp.h): when it loads it sets DVX's sound hook to audioDeskPlay(event, wait). The file of each event is read from DVX.INI [sounds] at the first sound: the key is audioDeskKey(event) and the value a WAV or VOC file, empty or missing for none: a name alone is found in the sound folders (the user folder's SOUNDS first, then SHARED\SOUNDS: platformKindFind), anything else is a path; audioDeskReload makes the next sound read the section again (the Control Panel calls it after saving). The sounds play on a context of the library's own, so no program's end cuts one short, at full volume under the master volume. When no sound device starts, the sounds stay silent without trying again until the next reload. With wait the call returns once the sound has played (at most 5 seconds); after the exit sound further events are quiet (DVX is ending). audioDeskLabel(event) names an event for people, audioDeskPlayFile(path) plays any file the same way (to try one out). audioDeskFile(prefs, event) reads an event's file from a copy of DVX.INI ("" for none) and audioDeskSetFile(prefs, event, path) sets it; the library's own reading of [sounds] goes through audioDeskFile too. The default set, made by DVX's own sound generator at build time, is in SHARED\SOUNDS.

    +

    The library plays the sounds DVX makes for its events (dvxPlaySound in dvxApp.h): when it loads it sets DVX's sound hook to audioDeskPlay(event, wait). The file of each event is read from DVX.INI [sounds] at the first sound: the key is audioDeskKey(event) and the value a WAV or VOC file, empty or missing for none: a name alone is found in the sound folders (the user folder's SOUNDS first, then SHARED\SOUNDS: platformKindFind), anything else is a path; audioDeskReload makes the next sound read the section again (the Control Panel calls it after saving). Each event's file is decoded at its first sound and kept until then, so a window opening or a message box does not read the disk. The sounds play on a context of the library's own, so no program's end cuts one short, at full volume under the master volume. When no sound device starts, the sounds stay silent without trying again until the next reload. With wait the call returns once the sound has played (at most 5 seconds), running dvxWaitPump meanwhile (a BASIC program's events go on); after the exit sound further events are quiet (DVX is ending). audioDeskLabel(event) names an event for people, audioDeskPlayFile(path) plays any file the same way (to try one out). audioDeskFile(prefs, event) reads an event's file from a copy of DVX.INI ("" for none) and audioDeskSetFile(prefs, event, path) sets it; the library's own reading of [sounds] goes through audioDeskFile too. The default set, made by DVX's own sound generator at build time, is in SHARED\SOUNDS.

      Key           Event
       ---           -----
       default       the default beep (dvxBeep; a message box without an icon)
    @@ -7356,24 +7525,33 @@ audioWtStart(audio, v, &note);                  // pitch 0: AUDIO_WT_PITCH_R
     

    audioDriverNames(names, max) lists the short names of the sound drivers installed, the one tried first first (for a settings screen; AUDIO_DRIVERS_MAX is the most DVX's own callers ask for).

    Sound drivers are tried in this order: the one DVX.INI names, then from the highest priority down (Windows Sound System, Sound Blaster, Gravis UltraSound, parallel port DAC, PC speaker, null). The first that starts is used. Synthesizer drivers are opened with the sound device, beside it: the sound device's own first (the GUS's), then of each kind (FM chip, MIDI port, wavetable) the first that finds its hardware, and they close with the device. The sound device's description and hasSynth in AudioInfoT tell whether there is a synthesizer.

    Writing a Driver

    -

    A driver is a driver module (see dvxDrv.h in the API reference): <name>.drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers/<name>/ folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). A driver whose card records also sets AudioDrvInfoT.recSources and gives recordStart(want, source, got) and recordStop: recordStart is called with the output closed and takes the card for recording (its own DMA buffer and interrupt; the format the card records in got), its interrupt then calls audioRecPush(src, bytes) with each block recorded (whole frames; a block that does not fit is dropped and counted), and recordStop gives everything back as close does (sb.drv, wss.drv, and gus.drv use their close for both; the AD1848 codec's routines a Windows Sound System board and an UltraSound MAX share are codec/codecHw.c). While playing, the driver calls audioMixRender(buffer, frames) for each block of output from its own interrupt, or calls audioClockStart to have the timer service drive the mixer (the output is thrown away) when it only plays tones or nothing. Code and data an interrupt touches must be pinned: audioLockSpan pins a module's listed functions and data (a module's code comes before its data, so one span covers them), and the library is built without hot/cold function splitting and without loops turned into library calls. Interrupt code must not call the C library, DVX, or DOS and must not use floating point. audio.lib stops the device from a shutdown hook if DVX ends without closing it.

    +

    A driver is a driver module (see dvxDrv.h in the API reference): <name>.drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers/<name>/ folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest; when the last program closes sound it closes them but keeps their modules for the next device, which opens them again first (so open and close must work more than once). The drivers' names (audioDriverNames) are read once a session. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). A driver whose card records also sets AudioDrvInfoT.recSources and gives recordStart(want, source, got) and recordStop: recordStart is called with the output closed and takes the card for recording (its own DMA buffer and interrupt; the format the card records in got), its interrupt then calls audioRecPush(src, bytes) with each block recorded (whole frames; a block that does not fit is dropped and counted), and recordStop gives everything back as close does (sb.drv, wss.drv, and gus.drv use their close for both; the AD1848 codec's routines a Windows Sound System board and an UltraSound MAX share are codec/codecHw.c). While playing, the driver calls audioMixRender(buffer, frames) for each block of output from its own interrupt, or calls audioClockStart to have the timer service drive the mixer (the output is thrown away) when it only plays tones or nothing. Code and data an interrupt touches must be pinned: audioLockSpan pins a module's listed functions and data (a module's code comes before its data, so one span covers them), and the library is built without hot/cold function splitting and without loops turned into library calls. Interrupt code must not call the C library, DVX, or DOS and must not use floating point. audio.lib stops the device from a shutdown hook if DVX ends without closing it.

    A synthesizer driver exports an AudioSynthDriverT named audioSynth instead, written with AUDIO_SYNTH_DRIVER(name, priority, .kind = AUDIO_SYNTH_FM, AUDIO_SYNTH_MIDI, or AUDIO_SYNTH_WAVETABLE, .open = ..., .close = ..., then .write, .put, or the wavetable functions). open detects the hardware, fills AudioSynthDrvInfoT (description; for FM the voices and whether it is stereo; for a wavetable synthesizer its voices, memory, AUDIO_WT_* flags, and level, 0 when it is as loud as the digital sound or not known) and leaves it silent; close silences it. write(reg, value) sets an FM register (bank x AUDIO_FM_BANK_REGS + index) and must keep the index/data pair together (audioIrqOff / audioIrqRestore); put(byte) sends a MIDI byte, returning false when the port is not ready; a MIDI driver whose port receives also has get(&byte), the next byte the port has (false when none), which the MIDI input's timer calls (mpu401.drv keeps what put finds waiting for get). A wavetable driver has upload(addr, frames, count, bits) (program time; addresses as audioWtMemAlloc gives them) and start, update, release, stop, and active for its voices (numbered from 0, as audio.lib hands them out); it plays a note as AudioWtVoiceT gives it, in its own units. write, put, and the voice functions run at interrupt time too (timer callbacks), so they follow the rules above and are pinned by the driver. A sound driver whose card also has a synthesizer (the GUS) points AudioDrvInfoT.synth at its own AudioSynthDriverT, which is opened before the others.

    BASIC

    src/include/basic/sound.bas (DECLARE LIBRARY "audio"): PlayWave(fileName), PlayWaveLoop(fileName), PlayTone(frequency, milliseconds, volume), PlayNoise(frequency, milliseconds, volume) return a voice number (0 on failure: SoundError$ says why); StopWave(voice), StopAllSounds, SoundPlaying(voice), SoundVolume(percent), SoundInfo$ (the device, e.g. "Sound Blaster 16 (DSP 4.05) at 220h, 22050 Hz, 16-bit stereo"). Volumes are percent. Each BASIC program plays through its own context, closed when it ends.

    For a settings program (the Control Panel's Sounds page): SoundEventCount, SoundEventLabel$(eventIdx), SoundEventKey$(eventIdx) (audioDeskLabel / audioDeskKey; events numbered from 0 in DvxSoundE order); SoundEventFile$(prefs, eventIdx) and SoundSetEventFile(prefs, eventIdx, fileName) (audioDeskFile / audioDeskSetFile); SoundEventsReload (audioDeskReload); SoundTestFile(fileName) (audioDeskPlayFile, a name alone found in the sound folders as the events' files are: a voice number, or 0 with SoundError$); SoundFolder$ (SHARED\SOUNDS, the sounds DVX ships; FileOpsKindDir$ and FileOpsKindList give the user's too); SoundMasterVolume and SoundSetMasterVolume(percent) (audioGetMasterVolume / audioSetMasterVolume in percent); SoundDriverCount (reads audioDriverNames, at most AUDIO_DRIVERS_MAX) and SoundDriverName$(index); SoundDriverAuto$ (AUDIO_DRIVER_AUTO); SoundMidiCount and SoundMidiLabel$(index) (audioMidiChoices); SoundMidiInCount and SoundMidiInLabel$(index) (audioMidiInChoices); SoundModulesCount and SoundModulesLabel$(index) (audioModulesChoices); SoundSettingDriver$(prefs), SoundSettingMidi(prefs), SoundSettingMidiIn(prefs), SoundSettingModules(prefs), SoundSettingVolume(prefs), SoundSettingsStore(prefs, driver, midi, modules, volume), and SoundSettingMidiInStore(prefs, midiIn) (the audioSetting functions). prefs is a prefs.bas handle (prefsBasHandle, dvxPrefs.h); a handle that is not open reads the defaults and stores nothing.

    -

    The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running", "record" True / False; "voices" = voiceLimit, "rate", "bits", "channels", "recordsources"; any case, 0 for another field); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order). Samples: SoundLoad(fileName) (audioSampleLoad) and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped), and SoundSetFrequency(voice, hz) (the audioVoiceSet functions), SoundPause(voice) and SoundResume(voice) (audioVoicePause; a sound's or a stream's voice); the result is kept for SoundError$. Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamFlush (audioStreamFlush), SoundStreamClose. Recording, one per program (audioRecFile): RecordStart(fileName, rate, bits, channels, inputSource) (True / False with SoundError$; 0 for the defaults), RecordStop (audioRecClose; True when the file was all written), Recording, RecordLength (milliseconds recorded, written or waiting), RecordLevel (audioRecPeak), RecordLost, RecordRate, RecordBits, RecordChannels (audioRecFormat), with CONSTs RECORD_MIC, RECORD_LINE, RECORD_CD (AUDIO_REC_*) and SoundCaps "record" and "recordsources". FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS). Wavetable: WtInfo$(field) (audioWtGetInfo: "description", "voices", "free", "memory", "freememory", "level", and the flags "envelopes", "lfos", "filter", "effects", "8bit", "rom" as text), WtMemAlloc(bytes, bits) (an address, -1 for no room), WtUpload(address, pcm AS BINARY STRING, bits) (16-bit mono samples), WtMemFree, WtAlloc, WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) (a plain note: no envelope or LFO, the filter open; pitch in cents, attenuation in centibels, pan -100..100 scaled to AUDIO_WT_PAN_MAX), WtSetPitch, WtSetAttenuation, WtSetPan (audioWtUpdate of the note kept for the voice; True / False), WtRelease, WtStop, WtActive, WtFree. The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned", "input", "inputowned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease; its input: MidiInOpen (True / False with SoundError$), MidiInClose, MidiInMessage (audioMidiInRead: the message packed as status + data1 * 256 + data2 * 65536, AUDIO_MIDI_PACKED_*, or MIDI_NONE, -1), MidiInTime (the ms of the message MidiInMessage gave last), MidiInClock (audioMidiInClock), MidiInLost, with CONSTs MIDI_NOTE_OFF to MIDI_PITCH_BEND, MIDI_KIND_MASK, and MIDI_CHANNEL_MASK. CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*), and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*).

    -

    Sound and stream numbers are 1..64 per kind, valid only for the program that got them; audioClose of a BASIC context forgets its numbers (audioBasForget) as it frees the samples and streams. BINARY STRING parameters arrive as a pointer and a byte count (two C parameters; see the DECLARE LIBRARY rules in the BASIC reference).

    +

    The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running", "record" True / False; "voices" = voiceLimit, "rate", "bits", "channels", "recordsources"; any case, 0 for another field); SoundStats(field) (audioGetStats: "renders", "frames", "underruns", "clipped", "dropped", "playing"); SoundDriver$ (AudioInfoT.driver); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order).

    +

    Samples: SoundLoad(fileName) (audioSampleLoad), SoundLoadData(bytes AS BINARY STRING) (audioSampleLoadMem), and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundRate, SoundBits, and SoundChannels (audioSampleInfo's format), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped), and SoundSetFrequency(voice, hz) (the audioVoiceSet functions), SoundPause(voice) and SoundResume(voice) (audioVoicePause; a sound's or a stream's voice); the result is kept for SoundError$.

    +

    Ends: the program's context has audioSetEndFn set to audioBasEnded, which keeps the last BAS_ENDED_MAX (64) ended voices; SoundEnded gives the program's next one (0 for none) and SoundEndReason the AudioEndE of the one it gave last (CONSTs SOUND_END_*).

    +

    Note queues: NotesOpen(wave, volume) (audioNotesOpen; CONSTs SOUND_WAVE_* for AudioWaveE), NotesAdd(queue, hertz AS DOUBLE, milliseconds AS DOUBLE) (audioNotesAdd in millihertz and microseconds), NotesSetTone(queue, wave, volume) (audioNotesSetTone, the volume a percent of AUDIO_VOLUME_MAX), NotesQueued, NotesSpace, NotesVoice, NotesClear, NotesClose; numbered 1..64 like sounds.

    +

    Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamEnd (audioStreamEnd: no more will be written; the voice ends, SoundEnded tells, once what is held has played), SoundStreamFlush (audioStreamFlush), SoundStreamClose.

    +

    Recording, one per program (audioRecFile, or audioRecOpen into memory when fileName is ""): RecordStart(fileName, rate, bits, channels, inputSource) (True / False with SoundError$; 0 for the defaults), RecordRead$(maxBytes) AS BINARY STRING (audioRecRead of the whole frames that fit, into a buffer the next call reuses) and RecordQueued (audioRecQueued, in bytes), RecordStop (audioRecClose; True when a file was all written), Recording, RecordLength (milliseconds recorded, written or waiting), RecordLevel (audioRecPeak), RecordLost, RecordRate, RecordBits, RecordChannels (audioRecFormat), with CONSTs RECORD_MIC, RECORD_LINE, RECORD_CD (AUDIO_REC_*) and SoundCaps "record" and "recordsources".

    +

    FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS).

    +

    Wavetable: WtInfo$(field) (audioWtGetInfo: "description", "voices", "free", "memory", "freememory" (the largest free block), "freetotal" (freeTotal: all that is free), "level", and the flags "envelopes", "lfos", "filter", "effects", "8bit", "rom" as text), WtMemAlloc(bytes, bits) (an address, -1 for no room), WtUpload(address, pcm AS BINARY STRING, bits) (16-bit mono samples), WtMemFree, WtAlloc (the voice's note made plain: audioWtNoteInit), WtShape(voice, field, value) (an AudioWtVoiceT envelope, LFO, filter, or effects field by name -- "volattack", "modlfofreq", "filterfc", "reverb", ...; int16_t fields clamped -- kept in the voice's note, with audioWtUpdate at once on a sounding note for AUDIO_WT_SET_FILTER, _EFFECTS, and _LFO fields), WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) (the voice's note with the sample and these set; a negative address is AUDIO_WT_ADDR_ROM's, CONST WT_ADDR_ROM; pitch in cents, attenuation in centibels, pan -100..100 scaled to AUDIO_WT_PAN_MAX), WtSetPitch, WtSetAttenuation, WtSetPan (audioWtUpdate of the note kept for the voice; True / False), WtRelease, WtStop, WtActive, WtFree.

    +

    The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned", "input", "inputowned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease; its input: MidiInOpen (True / False with SoundError$), MidiInClose, MidiInMessage (audioMidiInRead: the message packed as status + data1 * 256 + data2 * 65536, AUDIO_MIDI_PACKED_*, or MIDI_NONE, -1), MidiInTime (the ms of the message MidiInMessage gave last), MidiInClock (audioMidiInClock), MidiInLost, with CONSTs MIDI_NOTE_OFF to MIDI_PITCH_BEND, MIDI_KIND_MASK, and MIDI_CHANNEL_MASK.

    +

    CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*), and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*).

    +

    Sound, stream, and note queue numbers are 1..64 per kind, valid only for the program that got them; audioClose of a BASIC context forgets its numbers and ended voices (audioBasForget) as it frees the samples, streams, and queues. BINARY STRING parameters arrive as a pointer and a byte count (two C parameters; see the DECLARE LIBRARY rules in the BASIC reference).

    BASIC Development Library

    -

    basdev.lib is the DVX BASIC compiler and what a development tool builds on it: the build pipeline (basBuild.h), the compile-time Control.Member validator (basValidate.h), the debugger's view of a paused program (basDebug.h), and the SUB / FUNCTION scanner (basProcs.h). The DVX BASIC IDE uses it, and BASIC programs reach it through compiler.bas and debug.bas (basDevBas.c holds their entry points). It also carries the STUB and noicon resources every build inside DVX uses (BAS_BUILD_STUB_CARRIER). It needs basrt.lib, which holds the run host (basRunHost.h) the IDE, the standalone stub, and debug.bas share.

    -

    Headers: apps/dvxbasic/basBuild.h, apps/dvxbasic/basdev/basValidate.h, apps/dvxbasic/basdev/basDebug.h, apps/dvxbasic/basdev/basProcs.h, apps/dvxbasic/basdev/basSource.h, apps/dvxbasic/basdev/basHelpMap.h, apps/dvxbasic/basdev/basProject.h, apps/dvxbasic/formrt/basRunHost.h

    +

    basdev.lib is the DVX BASIC compiler and what a development tool builds on it: the build pipeline (basBuild.h), the compile-time Control.Member validator (basValidate.h), the debugger's view of a paused program (basDebug.h), the SUB / FUNCTION scanner (basProcs.h), and the .frm model (frmModel.h) the IDE's designer works on. The DVX BASIC IDE uses it, and BASIC programs reach it through compiler.bas, debug.bas, and designer.bas's Frm* functions (basDevBas.c and frmModelBas.c hold their entry points). It also carries the STUB and noicon resources every build inside DVX uses (BAS_BUILD_STUB_CARRIER). It needs basrt.lib, which holds the run host (basRunHost.h) the IDE, the standalone stub, and debug.bas share.

    +

    Headers: apps/dvxbasic/basBuild.h, apps/dvxbasic/basdev/basValidate.h, apps/dvxbasic/basdev/basDebug.h, apps/dvxbasic/basdev/basProcs.h, apps/dvxbasic/basdev/basSource.h, apps/dvxbasic/basdev/basHelpMap.h, apps/dvxbasic/basdev/basProject.h, apps/dvxbasic/basdev/frmModel.h, apps/dvxbasic/formrt/basRunHost.h

    Compiling (basBuild.h)

      Function                Description
       --------                -----------
       basBuildAddSource       Append one file to a BasProjectSourceT the way a project is built: a module in BEGINMODULE, a class module's code in BEGINCLASS, a UserControl's code in BEGINCLASS ... USERCONTROL (its header is kept in ctlTexts), a form's code section in BEGINFORM with its controls (the .frm text is kept in frmTexts), a lone program unwrapped. Records the file in the source map.
       basBuildConcatProject   Read every file an open .dbp names (modules, class modules, UserControls, forms) into a BasProjectSourceT through basBuildAddSource.
    -  basBuildCompile         Compile source into a BasModuleT with OPTION EXPLICIT and an optional BasCtrlValidatorT. A BasCompileErrorT says why it failed: BAS_COMPILE_ERROR (line, column, message), BAS_COMPILE_NO_MEMORY, or BAS_COMPILE_NO_MODULE.
    +  basBuildCompile         Compile source into a BasModuleT with OPTION EXPLICIT and an optional BasCtrlValidatorT. A BasCompileErrorT says why it failed: BAS_COMPILE_ERROR (line, column, message), BAS_COMPILE_NO_MEMORY, or BAS_COMPILE_NO_MODULE. An optional unit callback runs after each statement or procedure compiled (basCompilerSetUnitFn), so a long compile can let other work go on.
       basBuildErrorText       A compiler message without its "Line N, Col M: " prefix.
       basBuildMapLine         Line of the concatenated source to file and file line.
       basBuildMapFileLine     File and file line to the line of the concatenated source (names compared as DOS does: any case, / and \ alike).
    @@ -7385,7 +7563,7 @@ audioWtStart(audio, v, &note);                  // pitch 0: AUDIO_WT_PITCH_R
     

    Validating Control Members (basValidate.h)

    A BasValidatorT (zeroed) collects the forms, controls, and menu items of the .frm texts basValidateScanFrm parses; its onCode hook also receives each form's code section, so one parse serves both. basValidateCallbacks fills the BasCtrlValidatorT the compiler takes; pass &validator.compiler as basBuildCompile's liveCompiler so a control name resolves on the form whose code is compiled. Member names are checked against the loaded widget interfaces (WgtIfaceT) and the form runtime's own property tables. badTypes lists controls whose type no loaded widget library registers (BAS_VALIDATE_BAD_TYPE_FMT formats one). The command-line compiler does not validate: no widget library is loaded there.

    Debugging (basDebug.h)

    -

    Every function takes the paused program's VM (or NULL when there is none).

    +

    Every function takes the paused program's VM (or NULL when there is none). The ones that run a snippet (basDbgEval, basDbgExec, basDbgSetVar, basDbgRunSnippet) take a BasDbgPumpT too (NULL for none): its fn(ctx) runs between the snippet's slices, so a long snippet lets other work go on (the IDE shows a frame with input held off; compiler.bas lets the other tasks run).

      Function                Description
       --------                -----------
       basDbgCurProcIdx        The compiled procedure the paused VM is in (-1: module level).
    @@ -7413,11 +7591,106 @@ audioWtStart(audio, v, &note);                  // pitch 0: AUDIO_WT_PITCH_R
     

    The project (.dbp) model: PrjStateT with its file list (PrjFileT: path relative to the project's folder, kind -- PrjFileKindE: module, class module, UserControl ([Controls], .ctl), or form, in compile order -- edit buffer) and settings, prjLoad / prjSave (one table of [Project] / [Settings] keys drives load, save, prjFieldGet, and prjFieldSet; [Project] Type = Control makes a control project, prjIsControlProject), prjNew, prjAddFile, prjRemoveFile, prjRenameFile (disk and project together), prjFileIndexByPath, prjKindForPath (the build's rule, basBuildWrapForPath), prjKindWrap, prjClassName (a class module's or UserControl's class name), prjReadFileText, the fonts the program carries ([Fonts]: prjAddFont, prjRemoveFont, prjFontCount, prjFontPath; the build makes them FONTn font resources, basBuildSpecT fonts), and the source map of the last compile (prjMapLine, prjMapFileLine). The IDE keeps only its Project window and Properties dialog (ideProject.c); compiler.bas has ProjOpen, ProjNew, ProjSave, ProjClose, ProjPath$, ProjFileCount, ProjFile$, ProjFilePath$, ProjFileKind, ProjFileFind, ProjAddFile, ProjRemoveFile, ProjRenameFile, ProjFontCount, ProjFont$, ProjAddFont, ProjRemoveFont, ProjGet$, and ProjSet; CompKeywordClass and CompKeywordReserved expose the keyword table's flags.

    A UserControl's header (the Begin UserControl block of a .ctl) is read and changed key by key, every other line of the text kept: prjCtlKeyAt / prjCtlKeyFind give the keys (PrjCtlKeyT: name and PrjCtlValueE kind -- Name, the control's name on the Begin line, then Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, DefaultEvent), prjCtlHeaderGet the value the control takes (its default when the header leaves the key out), prjCtlHeaderSet a malloc'd copy of the text with one key changed, added before End, or removed (an empty value), written as its kind is (a text in quotes, a color &HBBGGRR&, True or False), and prjCtlNew the text of a new UserControl (its header with Width and Height, Option Explicit, an empty UserControl_Paint written with PRJ_HANDLER_FMT and PRJ_PRIVATE, the form the IDE writes event handlers in). The IDE's File > New UserControl and Properties window use them; compiler.bas has ProjCtlNew$, ProjCtlHeader$, ProjCtlSetHeader$, and ProjCtlKeys$ (the keys and their kinds, prjCtlKeyAt order).

    Running a Program (basRunHost.h, in basrt.lib)

    -

    basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path, App.User (from the program's file: its folder in the user folder), App.Data (the DATA folder beside it), COMMAND$, the print, input, and DoEvents callbacks, the screen window's name, the help file, whether runtime errors show their own box, and whether it is a standalone program (the stub's own: the runtime owns the module, frees it with itself, and is ended through the shell's kill hook, shellRegisterAppKill, when the program is ended by force; the IDE and basdev end their runtimes themselves). basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused, and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs.

    +

    basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path, App.User (from the program's file: its folder in the user folder), App.Data (the DATA folder beside it), COMMAND$, the print, input, and DoEvents callbacks, the screen window's name, the help file, whether runtime errors show their own box, whether it is a standalone program (the stub's own: the runtime owns the module and frees it with itself), and whether it ends with its program (endWithApp: the IDE's runs, on a task of their own). A standalone runtime and one made with endWithApp are ended through the shell's kill hook, shellRegisterAppKill, when the program they run for is ended by force; basdev ends its runs itself. A runtime made on one task and run on another (the IDE makes it in a menu command and runs it on a work task, shellAppTaskStart) is moved with basFormRtAdopt, and basFormRtYield lets the other tasks run once without running any of the program's events. basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused, and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs.

    VM accessors a debugger host uses (vm.h): basVmSetAppPaths, basVmSetBreakpointCallback, basVmSetDebugPaused, basVmResume, basVmStop, basVmClearStepState, basVmGetCurrentLine, basVmGetErrorLine.

    From BASIC

    compiler.bas and debug.bas declare the entry points (see Include Libraries in the BASIC language reference). Source sets, modules, and runs are handles freed with their program. A run is polled: RunPoll runs the child for a bounded time and never waits. A pause in the child's module code returns RUN_PAUSED; a pause inside one of its event handlers (or its forms' Load code, inside RunStart) waits in place while the child pumps DVX events, so the caller's Timer and menus can step, continue, or stop it; RunPoll called from there returns at once. RunFree while the child's code is on the stack stops it at once and releases it at the next library call after that code has unwound.

    CompDesignControls registers a source set's UserControls for the calling program's design forms (basCtlLoadDesign with its runtime, the current one: a child run's own when the child calls it) and holds the module -- one of the caller's own handles; another program's is refused -- until CompDesignRelease, the next CompDesignControls, or the end of that runtime (appEnded for a program, runDestroyHost for a child run), CompModuleFree meanwhile only marking it.

    +

    The .frm Model (frmModel.h)

    +
    +
    +

    The .frm Model

    +

    A form file as data, with no widgets and no windows: the form's properties, its controls (type, name, control array index, parent, the built-in size and state fields, and every other "Key = Value" pair as text), its menu items, and its code section. frmModelLoad reads .frm text (through frmParse) and frmModelWrite writes the canonical text the DVX BASIC IDE saves. The IDE's designer, properties window, menu editor, and copy and paste all work on this model and keep only their live widgets on the side (each control's userData); designer.bas binds it for BASIC programs (the Frm* functions, basdev/frmModelBas.c).

    +

    Header: apps/dvxbasic/basdev/frmModel.h (part of basdev.lib)

    +

    Types

    +
      Type            Description
    +  ----            -----------
    +  FrmModelT       A form: name, caption, width / height / left / top, padding, spacing, windowState, layout, helpTopic, contextMenu, centered / autoSize / resizable, controls (stb_ds array of FrmCtrlT pointers in tree order: each control directly followed by its children), menuItems (stb_ds array), props (other runtime form properties that were set: Icon, Tag, MousePointer, the Font properties ...), code (malloc'd, NULL = none), dirty (changed by an edit since it was made or loaded).
    +  FrmCtrlT        A control: name, typeName, parentName + parentIndex (the parent's control array index, so a child of one member of a container array belongs to that member), index (FRM_NO_INDEX: not in an array), left, top, width (MinWidth), height (MinHeight), maxWidth, maxHeight, weight, dragMode, helpTopic, visible, enabled, props (stb_ds array of FrmPropT: every other key, unquoted), userData (the caller's; the model never reads it and loads, copies, and pastes start it at NULL).
    +  FrmMenuItemT    A menu item: caption ("-" for a separator), name, shortcut, shortcutText, level (0: a menu-bar menu or a popup; 1: its items; 2+: submenus), id (an editor identity, see frmModelMenuReplace), checked, checkable (Checked was written), radioCheck, enabled, visible (False on a level-0 item: a popup menu).
    +  FrmPropT        One stored "Key = Value": name, value.
    +  FrmIntPropT     One built-in integer property (Left, Top, MinWidth alias Width, MinHeight alias Height, MaxWidth, MaxHeight, Weight, DragMode): name, alias, offset into FrmCtrlT, saveRule (FRM_SAVE_ALWAYS / IF_NONZERO / IF_POSITIVE).
    +

    Only values somebody set are stored and written: a property a control has by default (a PictureBox's BorderStyle, a TextBox's MaxLength) is not in the model and not in the file. Values are written in the canonical form for their widget interface type, or for a key the widget does not know, for their type in the runtime's common property table (STRING quoted, BOOL True / False, INT a number, FLOAT and ENUM unquoted); other keys are written as quoted strings. The limits come from the parser's: FRM_MAX_NEST_DEPTH control levels, FRM_MAX_MENU_LEVEL menu levels, and no property line longer than FRM_MAX_LINE_LEN - 1 characters (the writer fails with FRM_WRITE_LINE_TOO_LONG rather than write one the parser would cut).

    +

    Loading and Writing

    +
      Function                                      Description
    +  --------                                      -----------
    +  frmModelNew(name)                             A blank form with the runtime form's defaults.
    +  frmModelLoad(text, len)                       The model of .frm text; NULL when it is not a form (no Begin Form, frmParse refuses it, two controls share a name and index) or out of memory. A Key = Value line is the innermost open block's, also after the blocks inside it, as the runtime reads it; a child names its container by the container's final Index. Of several OptionButtons of one container saved with Value True, the last keeps it.
    +  frmModelFree(m)                               Frees the model, its controls, and menus.
    +  frmModelWrite(m, buf, bufSize)                The .frm text into buf: its length, or FRM_WRITE_TOO_LARGE / FRM_WRITE_LINE_TOO_LONG (never a partial text).
    +  frmModelWriteSize(m)                          A buffer size that always holds the text.
    +  frmModelText(m, &len)                         The text in a new malloc'd string; NULL with len set to the FRM_WRITE_* code.
    +  frmModelSetCode(m, code)                      Replaces the code section.
    +  frmCodeSection(trailing, len, &outLen)        The code section of the text after a form's End (blank lines skipped, the first code line keeps its indent).
    +  frmModelFromForm(rt, form)                    A model of a loaded form as it is now: its properties, its controls in their containers with each property that differs from a new control of the type (compared with one made in a hidden window; the six Font properties of the form and of each control that has a font of its own), its menus; no code. What FormText$ writes.
    +

    Properties

    +
      Function                                              Description
    +  --------                                              -----------
    +  frmModelFormProp(m, name, out, size)                  A form property as text (an unset design property gives "", "0", or "False"); false for run-time-only ones (Visible, DesignMode, DesignSelection, object properties).
    +  frmModelSetFormProp(m, name, value)                   Sets one from text; Name renames the form; an empty value takes an optional one out.
    +  frmCtrlPropText(ctrl, name, out, size)                A control property as the control holds it (Name, Index, the built-in integers, HelpTopic, Visible / Enabled unless the widget defines them, the stored ones).
    +  frmCtrlSetPropText(ctrl, key, value)                  Applies one the way the loader does (a VB TabIndex is dropped unless the widget defines it).
    +  frmModelSetCtrlProp(m, ctrl, name, value)             The same with an editor's rules: Name renames the control array, Index makes an array member, then the OptionButton group rule.
    +  frmCtrlGetProp / frmCtrlSetProp / frmCtrlRemoveProp   The stored properties.
    +  frmIntPropAt / frmIntPropFind / frmIntPropGet / Set   The built-in integer table.
    +  frmIfaceProp(typeName, propName)                      The widget interface property of a type, NULL when none.
    +

    Font Properties

    +

    FontName, FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru (BAS_PROP_FONT*) are in both the form table and the common control table. They are one font, a DvxFontChoiceT (dvxDlg.h: family, size in pixels, FONT_STYLE_* flags), and the widget's font (wgtFont, wgtSetFont) is the only place they live: a read gives the font the object draws with (a form's is its root widget's), a write gives it that font with the one part changed. The .frm loader collects a block's Font properties and opens one font when the block's properties are read, so their order in the file does not matter. Helpers in formrt.h, for loaders and designers that hold them as .frm text:

    +
      Function                                        Description
    +  --------                                        -----------
    +  basFontPropAt(idx)                              The Font property names in .frm order (FontName, FontSize, then the flags), NULL past the end.
    +  basFontPropIs(name)                             True for a Font property name (any case).
    +  basFontChoiceOf(font, out)                      A BitmapFontT as the properties name it (dvxFontFamilyOf, pixelSize, style).
    +  basFontPropText(choice, name, out, size)        One property of a choice as .frm text (the family, the size, True / False).
    +  basFontPropSetText(choice, name, text)          Sets one property of a choice from .frm text.
    +

    Editing Controls

    +
      Function                                                  Description
    +  --------                                                  -----------
    +  frmModelAddCtrl(m, typeName, name, parent)                Adds a control as the last child of parent (NULL: the form); name "" or NULL gives a free one (frmModelAutoName); Caption = its name. NULL when the name is taken or parent cannot hold it.
    +  frmModelRemoveCtrl(m, ctrl)                               Removes ctrl and its subtree.
    +  frmModelRenameCtrl(m, oldName, newName)                   Renames a whole control array; children and DataSource / MasterSource of a renamed Data control follow. Identifier rules are the caller's.
    +  frmModelMakeArrayMember(m, ctrl, index)                   Gives a control array index; its children follow.
    +  frmModelMoveCtrl(m, ctrl, before)                         Moves ctrl with its subtree before a sibling (NULL: after the last).
    +  frmModelReparent(m, ctrl, parent)                         Makes ctrl the last child of another container (refuses cycles, non-containers, and too deep a nesting).
    +  frmModelSibling(m, idx, dir)                              The sibling next to a control (+1 after its subtree, -1 before).
    +  frmModelFindCtrl / CtrlPos / FindParent / CtrlDepth       Lookups.
    +  frmModelIsDescendant / SubtreeEnd / NormalizeOrder        The tree order.
    +  frmModelNameInUse / AutoName / OptionSelected             Names and the OptionButton group rule.
    +  frmModelCtrlText(m, ctrl, &len)                           ctrl and its children as .frm control blocks (a copy for the Clipboard).
    +  frmModelCtrlsFromText(text, len)                          Copied blocks read into a scratch model.
    +  frmModelInsertTree(m, src, srcRoot, parent, name, index)  Moves a subtree out of the scratch model into m: the root takes name / index (NULL: its own name when free, else a free one); a child whose name is taken gets a free one, the members of one array together; children follow their renamed parents.
    +

    Menus

    +

    The frmMenu* functions work on any stb_ds array of FrmMenuItemT (an editor's working copy as well as m->menuItems) and keep the levels a tree.

    +
      Function                                                  Description
    +  --------                                                  -----------
    +  frmMenuItemInit(mi)                                       A blank item (enabled, visible, level 0).
    +  frmMenuInsert(&items, at, level)                          Inserts a blank item; the level is clamped so the menus stay a tree.
    +  frmMenuRemove(&items, idx)                                Removes an item with its subtree.
    +  frmMenuMove(&items, idx, dir)                             Moves an item with its subtree past its previous / next sibling; its new position or FRM_NO_INDEX.
    +  frmMenuIndent(items, idx, delta)                          Indents / outdents an item with its subtree (at most one level below the item above, not past FRM_MAX_MENU_LEVEL).
    +  frmMenuSubtreeEnd / CheckLevels / StripBlank / AutoName   Tree helpers; AutoName makes mnuSep1 ... or "mnu" plus the caption's letters and digits.
    +  frmMenuPropText / frmMenuSetPropText                      Menu properties as text (Checked makes a check item).
    +  frmModelMenuValidate(m, items, reason, size)              The checks before items replace m's menus (levels; every name set, an identifier as the compiler takes one, not the form's or a control's, unique; every Shortcut a key): the first bad item and the reason.
    +  frmModelMenuReplace(m, items)                             Replaces m's menus by a copy; ContextMenu bindings follow renamed items, matched by id (give every item id = position + 1 before editing).
    +  frmModelSetMenuProp(m, idx, key, value)                   Sets an item's property; a Name rename refuses a taken name and ContextMenu bindings follow.
    +
    // Load a form, rename a control, add a label inside a frame, save.
    +FrmModelT *m = frmModelLoad(text, len);
    +
    +if (m) {
    +    int32_t   at     = frmModelFindCtrl(m, "Frame1", FRM_NO_INDEX);
    +    FrmCtrlT *frame  = (at >= 0) ? m->controls[at] : NULL;
    +    FrmCtrlT *lbl    = frmModelAddCtrl(m, "Label", "", frame);
    +    int32_t   outLen = 0;
    +
    +    frmModelRenameCtrl(m, "Command1", "cmdOk");
    +    frmModelSetCtrlProp(m, lbl, "Caption", "Name:");
    +
    +    char *out = frmModelText(m, &outLen);
    +
    +    // write out[0 .. outLen) to the file
    +    free(out);
    +    frmModelFree(m);
    +}

    BASIC Runtime Library

    @@ -7425,7 +7698,6 @@ audioWtStart(audio, v, &note); // pitch 0: AUDIO_WT_PITCH_R

    Headers: apps/dvxbasic/runtime/vm.h, apps/dvxbasic/runtime/values.h

    Value System (values.h)

    Virtual Machine (vm.h)

    -

    The .frm Model (frmModel.h)

    Value System

    @@ -7846,100 +8118,6 @@ void basVmSetAppPaths(BasVmT *vm, const char *path, const char *user, const c debugUdtDefs UDT type definitions for the debugger. debugUdtDefCount Number of debug UDT definitions.
    -
    -

    The .frm Model

    -

    A form file as data, with no widgets and no windows: the form's properties, its controls (type, name, control array index, parent, the built-in size and state fields, and every other "Key = Value" pair as text), its menu items, and its code section. frmModelLoad reads .frm text (through frmParse) and frmModelWrite writes the canonical text the DVX BASIC IDE saves. The IDE's designer, properties window, menu editor, and copy and paste all work on this model and keep only their live widgets on the side (each control's userData); designer.bas binds it for BASIC programs (the Frm* functions, formrt/frmModelBas.c).

    -

    Header: apps/dvxbasic/formrt/frmModel.h (part of basrt.lib)

    -

    Types

    -
      Type            Description
    -  ----            -----------
    -  FrmModelT       A form: name, caption, width / height / left / top, padding, spacing, windowState, layout, helpTopic, contextMenu, centered / autoSize / resizable, controls (stb_ds array of FrmCtrlT pointers in tree order: each control directly followed by its children), menuItems (stb_ds array), props (other runtime form properties that were set: Icon, Tag, MousePointer, the Font properties ...), code (malloc'd, NULL = none), dirty (changed by an edit since it was made or loaded).
    -  FrmCtrlT        A control: name, typeName, parentName + parentIndex (the parent's control array index, so a child of one member of a container array belongs to that member), index (FRM_NO_INDEX: not in an array), left, top, width (MinWidth), height (MinHeight), maxWidth, maxHeight, weight, dragMode, helpTopic, visible, enabled, props (stb_ds array of FrmPropT: every other key, unquoted), userData (the caller's; the model never reads it and loads, copies, and pastes start it at NULL).
    -  FrmMenuItemT    A menu item: caption ("-" for a separator), name, shortcut, shortcutText, level (0: a menu-bar menu or a popup; 1: its items; 2+: submenus), id (an editor identity, see frmModelMenuReplace), checked, checkable (Checked was written), radioCheck, enabled, visible (False on a level-0 item: a popup menu).
    -  FrmPropT        One stored "Key = Value": name, value.
    -  FrmIntPropT     One built-in integer property (Left, Top, MinWidth alias Width, MinHeight alias Height, MaxWidth, MaxHeight, Weight, DragMode): name, alias, offset into FrmCtrlT, saveRule (FRM_SAVE_ALWAYS / IF_NONZERO / IF_POSITIVE).
    -

    Only values somebody set are stored and written: a property a control has by default (a PictureBox's BorderStyle, a TextBox's MaxLength) is not in the model and not in the file. Values are written in the canonical form for their widget interface type, or for a key the widget does not know, for their type in the runtime's common property table (STRING quoted, BOOL True / False, INT a number, FLOAT and ENUM unquoted); other keys are written as quoted strings. The limits come from the parser's: FRM_MAX_NEST_DEPTH control levels, FRM_MAX_MENU_LEVEL menu levels, and no property line longer than FRM_MAX_LINE_LEN - 1 characters (the writer fails with FRM_WRITE_LINE_TOO_LONG rather than write one the parser would cut).

    -

    Loading and Writing

    -
      Function                                      Description
    -  --------                                      -----------
    -  frmModelNew(name)                             A blank form with the runtime form's defaults.
    -  frmModelLoad(text, len)                       The model of .frm text; NULL when it is not a form (no Begin Form, frmParse refuses it, two controls share a name and index) or out of memory. A Key = Value line is the innermost open block's, also after the blocks inside it, as the runtime reads it; a child names its container by the container's final Index. Of several OptionButtons of one container saved with Value True, the last keeps it.
    -  frmModelFree(m)                               Frees the model, its controls, and menus.
    -  frmModelWrite(m, buf, bufSize)                The .frm text into buf: its length, or FRM_WRITE_TOO_LARGE / FRM_WRITE_LINE_TOO_LONG (never a partial text).
    -  frmModelWriteSize(m)                          A buffer size that always holds the text.
    -  frmModelText(m, &len)                         The text in a new malloc'd string; NULL with len set to the FRM_WRITE_* code.
    -  frmModelSetCode(m, code)                      Replaces the code section.
    -  frmCodeSection(trailing, len, &outLen)        The code section of the text after a form's End (blank lines skipped, the first code line keeps its indent).
    -  frmModelFromForm(rt, form)                    A model of a loaded form as it is now: its properties, its controls in their containers with each property that differs from a new control of the type (compared with one made in a hidden window; the six Font properties of the form and of each control that has a font of its own), its menus; no code. What FormText$ writes.
    -

    Properties

    -
      Function                                              Description
    -  --------                                              -----------
    -  frmModelFormProp(m, name, out, size)                  A form property as text (an unset design property gives "", "0", or "False"); false for run-time-only ones (Visible, DesignMode, DesignSelection, object properties).
    -  frmModelSetFormProp(m, name, value)                   Sets one from text; Name renames the form; an empty value takes an optional one out.
    -  frmCtrlPropText(ctrl, name, out, size)                A control property as the control holds it (Name, Index, the built-in integers, HelpTopic, Visible / Enabled unless the widget defines them, the stored ones).
    -  frmCtrlSetPropText(ctrl, key, value)                  Applies one the way the loader does (a VB TabIndex is dropped unless the widget defines it).
    -  frmModelSetCtrlProp(m, ctrl, name, value)             The same with an editor's rules: Name renames the control array, Index makes an array member, then the OptionButton group rule.
    -  frmCtrlGetProp / frmCtrlSetProp / frmCtrlRemoveProp   The stored properties.
    -  frmIntPropAt / frmIntPropFind / frmIntPropGet / Set   The built-in integer table.
    -  frmIfaceProp(typeName, propName)                      The widget interface property of a type, NULL when none.
    -

    Font Properties

    -

    FontName, FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru (BAS_PROP_FONT*) are in both the form table and the common control table. They are one font, a DvxFontChoiceT (dvxDlg.h: family, size in pixels, FONT_STYLE_* flags), and the widget's font (wgtFont, wgtSetFont) is the only place they live: a read gives the font the object draws with (a form's is its root widget's), a write gives it that font with the one part changed. The .frm loader collects a block's Font properties and opens one font when the block's properties are read, so their order in the file does not matter. Helpers in formrt.h, for loaders and designers that hold them as .frm text:

    -
      Function                                        Description
    -  --------                                        -----------
    -  basFontPropAt(idx)                              The Font property names in .frm order (FontName, FontSize, then the flags), NULL past the end.
    -  basFontPropIs(name)                             True for a Font property name (any case).
    -  basFontChoiceOf(font, out)                      A BitmapFontT as the properties name it (dvxFontFamilyOf, pixelSize, style).
    -  basFontPropText(choice, name, out, size)        One property of a choice as .frm text (the family, the size, True / False).
    -  basFontPropSetText(choice, name, text)          Sets one property of a choice from .frm text.
    -

    Editing Controls

    -
      Function                                                  Description
    -  --------                                                  -----------
    -  frmModelAddCtrl(m, typeName, name, parent)                Adds a control as the last child of parent (NULL: the form); name "" or NULL gives a free one (frmModelAutoName); Caption = its name. NULL when the name is taken or parent cannot hold it.
    -  frmModelRemoveCtrl(m, ctrl)                               Removes ctrl and its subtree.
    -  frmModelRenameCtrl(m, oldName, newName)                   Renames a whole control array; children and DataSource / MasterSource of a renamed Data control follow. Identifier rules are the caller's.
    -  frmModelMakeArrayMember(m, ctrl, index)                   Gives a control array index; its children follow.
    -  frmModelMoveCtrl(m, ctrl, before)                         Moves ctrl with its subtree before a sibling (NULL: after the last).
    -  frmModelReparent(m, ctrl, parent)                         Makes ctrl the last child of another container (refuses cycles, non-containers, and too deep a nesting).
    -  frmModelSibling(m, idx, dir)                              The sibling next to a control (+1 after its subtree, -1 before).
    -  frmModelFindCtrl / CtrlPos / FindParent / CtrlDepth       Lookups.
    -  frmModelIsDescendant / SubtreeEnd / NormalizeOrder        The tree order.
    -  frmModelNameInUse / AutoName / OptionSelected             Names and the OptionButton group rule.
    -  frmModelCtrlText(m, ctrl, &len)                           ctrl and its children as .frm control blocks (a copy for the Clipboard).
    -  frmModelCtrlsFromText(text, len)                          Copied blocks read into a scratch model.
    -  frmModelInsertTree(m, src, srcRoot, parent, name, index)  Moves a subtree out of the scratch model into m: the root takes name / index (NULL: its own name when free, else a free one); a child whose name is taken gets a free one, the members of one array together; children follow their renamed parents.
    -

    Menus

    -

    The frmMenu* functions work on any stb_ds array of FrmMenuItemT (an editor's working copy as well as m->menuItems) and keep the levels a tree.

    -
      Function                                                  Description
    -  --------                                                  -----------
    -  frmMenuItemInit(mi)                                       A blank item (enabled, visible, level 0).
    -  frmMenuInsert(&items, at, level)                          Inserts a blank item; the level is clamped so the menus stay a tree.
    -  frmMenuRemove(&items, idx)                                Removes an item with its subtree.
    -  frmMenuMove(&items, idx, dir)                             Moves an item with its subtree past its previous / next sibling; its new position or FRM_NO_INDEX.
    -  frmMenuIndent(items, idx, delta)                          Indents / outdents an item with its subtree (at most one level below the item above, not past FRM_MAX_MENU_LEVEL).
    -  frmMenuSubtreeEnd / CheckLevels / StripBlank / AutoName   Tree helpers; AutoName makes mnuSep1 ... or "mnu" plus the caption's letters and digits.
    -  frmMenuPropText / frmMenuSetPropText                      Menu properties as text (Checked makes a check item).
    -  frmModelMenuValidate(m, items, isIdent, reason, size)     The checks before items replace m's menus (levels; every name set, an identifier by isIdent, not the form's or a control's, unique; every Shortcut a key): the first bad item and the reason.
    -  frmModelMenuReplace(m, items)                             Replaces m's menus by a copy; ContextMenu bindings follow renamed items, matched by id (give every item id = position + 1 before editing).
    -  frmModelSetMenuProp(m, idx, key, value)                   Sets an item's property; a Name rename refuses a taken name and ContextMenu bindings follow.
    -
    // Load a form, rename a control, add a label inside a frame, save.
    -FrmModelT *m = frmModelLoad(text, len);
    -
    -if (m) {
    -    int32_t   at     = frmModelFindCtrl(m, "Frame1", FRM_NO_INDEX);
    -    FrmCtrlT *frame  = (at >= 0) ? m->controls[at] : NULL;
    -    FrmCtrlT *lbl    = frmModelAddCtrl(m, "Label", "", frame);
    -    int32_t   outLen = 0;
    -
    -    frmModelRenameCtrl(m, "Command1", "cmdOk");
    -    frmModelSetCtrlProp(m, lbl, "Caption", "Name:");
    -
    -    char *out = frmModelText(m, &outLen);
    -
    -    // write out[0 .. outLen) to the file
    -    free(out);
    -    frmModelFree(m);
    -}
    -

    Connection Library

    One interface for byte-stream connections: a serial port, a modem with an AT command dialer, the secure link to the DVX host proxy (over a COM port or over the network), and Telnet. Every call except the host name lookup is non-blocking: connOpen starts the connection (dialing, TCP connect, key exchange) and connPoll moves it along, fills the receive buffer, and sends what connWrite queued. Call connPoll as often as you can (every frame). The lookup of a host name waits for the answer, but other programs keep running meanwhile; a dotted address needs no lookup.

    @@ -7971,7 +8149,7 @@ if (m) { ConnT *connCreate(void) Allocate a closed connection. void connDestroy(c) Close and free. void connConfigDefaults(cfg) COM1, 9600 8N1, no flow control, ATZ, ATDT. - int32_t connOpen(c, cfg, nowMs) Start connecting (cfg is copied). CONN_OK, CONN_ERR_PARAM, CONN_ERR_PORT, or CONN_ERR_HOST (the host was not found or refused at once); connStatus explains. Later failures (refused, no answer within 30 seconds, connection lost, host closed it) end the connection with the reason in connStatus. + int32_t connOpen(c, cfg, nowMs) Start connecting (cfg is copied). CONN_OK, CONN_ERR_PARAM, CONN_ERR_PORT, or CONN_ERR_NO_NET (Telnet, or a secure link over the network, without the networking library); connStatus explains. A network host is looked up and connected to while connPoll runs (never waiting inside one call), so later failures (the host not found, refused, no answer within 30 seconds, connection lost, host closed it) end the connection with the reason in connStatus. void connPoll(c, nowMs) Dial, key exchange, receive, send, timers. int32_t connRead(c, buf, max) Take received bytes (0 when none). int32_t connWrite(c, data, len) Queue bytes to send; returns the count accepted (0 while not open or when the 8 KB send buffer is full). @@ -7982,7 +8160,7 @@ if (m) { const char *connStatus(c) What is happening, or why the connection ended, in plain words. uint32_t connStatusSerial(c) Changes whenever the status text changes. void connSetWindowSize(c, cols, rows) The terminal size for Telnet's NAWS (sent now if the host asked for it). - bool connTypeAvailable(type) True for every ConnTypeE value. + bool connTypeAvailable(type) True for every ConnTypeE value but ConnTelnetE when the networking library is not installed (an optional part of DVX; connections over the network then end at once with CONN_ERR_NO_NET). conn.lib does not import the networking library: it looks its calls up when a network connection starts. bool connCanBreak(type) True for serial and modem. const char *connErrorText(err) Text for a connOpen result.

    Example

    @@ -8018,6 +8196,7 @@ n = connRead(c, buf, sizeof(buf)); ConnState, ConnStatus$, ConnStatusSerial connState, connStatus, connStatusSerial. ConnWrite(h, bytes$), ConnRead$(h), ConnPending(h) Bytes out and in without a Terminal: connWrite / connRead with BINARY STRING data, so every byte value passes. ConnCanBreak(type), ConnSendBreak(h) connCanBreak, connSendBreak. + ConnTypeAvailable(type) connTypeAvailable: False for CONN_TELNET when Networking is not installed (a secure link over the network needs it too). ConnClose h, ConnFree h Close (a modem hangs up); close and free the handle. ConnAttach(h, ctrl$), ConnDetach h Show the connection on a Terminal control, and send what is typed there. ConnSetEcho h, on; ConnSetAnswerback h, text$ Local echo (Enter as CR LF); the reply to ENQ. @@ -8028,6 +8207,149 @@ n = connRead(c, buf, sizeof(buf));

    ConnPoll with a Terminal attached does what the C Terminal's heartbeat did: it reads up to 4 KB, appends them to the capture file, answers each ENQ with the answerback (ENQ itself is not shown), stops at a ZMODEM start frame (the rest of what was read is dropped, and ConnZmodemRequest becomes True), and writes the rest to the Terminal. While a transfer started on the connection runs (or waits for XferAnswer), ConnPoll moves it along instead, nothing reaches the Terminal, and typing is dropped; a transfer on a line that is no longer open is cancelled. The receive window of a ZMODEM download on a serial or modem line without flow control is 2 KB, so a slow frame never loses data, unless the program set one with ConnSetXferWindow.

    The Terminal binding goes through the form runtime (basFormRtTermBind, basFormRtTermUnbind, and basFormRtTermForget in basrt, looked up when ConnAttach first needs them): the runtime unbinds a Terminal whose form unloads, and tells the library, so a connection never writes to a freed control. Every handle belongs to the program that made it; whatever it leaves open is closed (a modem hangs up, a capture file is closed, a transfer cancelled) when it ends or is ended.

    +
    +

    Cryptography Library

    +

    Cryptographic primitives for DVX programs and libraries (the secure link, the package library, the network, and the shell's random seed use them): the X25519 key exchange (RFC 7748), XChaCha20-Poly1305 authenticated encryption, Ed25519 signatures (RFC 8032), 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA block cipher in CTR mode, SHA-256, HMAC-SHA256, and HKDF-SHA256, and a SHA-256 based random number generator. X25519, XChaCha20-Poly1305, and Ed25519 are Monocypher's (thirdparty/monocypher, BSD licence); the library keeps only the parts of it these functions use.

    +

    XTEA requires no lookup tables and compiles to approximately 20 instructions per round, making it suitable for 486-class hardware where AES S-box tables would thrash the 8KB cache. SHA-256 needs only 32-bit arithmetic and a 256-byte constant table.

    +

    Loaded as: bin/libs/kpunch/crypto/crypto.lib

    +

    Headers: crypto/security.h, crypto/secHash.h, crypto/secCurve.h

    +

    Constants

    +
      Constant                 Value   Description
    +  --------                 -----   -----------
    +  SEC_DH_KEY_SIZE          128     Size of a DH public key or shared secret in bytes (1024 bits).
    +  SEC_XTEA_KEY_SIZE        16      Size of an XTEA key in bytes (128 bits).
    +  SEC_RNG_SEED_SIZE        32      Bytes stored in a seed file.
    +  SEC_SHA256_SIZE          32      SHA-256 digest size.
    +  SEC_SHA256_BLOCK_SIZE    64      SHA-256 block size.
    +  SEC_HKDF_MAX_OUT         8160    Largest HKDF output (255 digests).
    +  SEC_X25519_KEY_SIZE      32      An X25519 secret key, public key, or shared secret.
    +  SEC_AEAD_KEY_SIZE        32      An XChaCha20-Poly1305 key.
    +  SEC_AEAD_NONCE_SIZE      24      An XChaCha20-Poly1305 nonce.
    +  SEC_AEAD_TAG_SIZE        16      The Poly1305 tag.
    +  SEC_ED25519_SEED_SIZE    32      The random bytes an Ed25519 key pair is made from.
    +  SEC_ED25519_SECRET_SIZE  64      An Ed25519 secret key (the seed, then the public key).
    +  SEC_ED25519_PUBLIC_SIZE  32      An Ed25519 public key.
    +  SEC_ED25519_SIG_SIZE     64      An Ed25519 signature.
    +  SEC_X25519_JOB_WORDS     72      Size of a SecX25519JobT in 32-bit words.
    +  SEC_X25519_PENDING       1       SecX25519Step (BASIC): more of the work to do.
    +

    Error Codes

    +
      Constant           Value   Description
    +  --------           -----   -----------
    +  SEC_SUCCESS        0       Operation succeeded.
    +  SEC_ERR_PARAM      -1      Invalid parameter.
    +  SEC_ERR_NOT_READY  -2      DH context not ready (keys not generated or secret not computed).
    +  SEC_ERR_IO         -3      Seed file could not be read or written.
    +

    RNG Functions

    +

    A SHA-256 hash DRBG over a 32-byte entropy pool. secRngAddEntropy hashes new bytes into the pool and never discards what is already there, so it is safe to call at any time, before or after the first output. Each output block is a hash of the pool and a counter; after every request the pool is re-hashed so earlier output cannot be recomputed from a later state.

    +

    The first secRngBytes call gathers hardware entropy automatically. On DOS that is 256 samples of the PIT counter, each taken after a busy wait sized by the previous sample, with the full time stamp counter on CPUs that have one (Pentium and later), else with the BIOS tick count. On a CPU without a time stamp counter the samples depend mostly on the PIT phase and time of day, which an observer can narrow down, so applications should feed user-input timings to secRngAddEntropy and keep a seed file: call secRngLoadSeed at startup and secRngSaveSeed at shutdown. Host builds read /dev/urandom.

    +

    Under the DVX shell this is automatic: the shell loads the seed file SECLINK.RND in the user folder at startup, feeds the timing of every key press and mouse event to secRngAddEntropy, and saves a fresh seed at shutdown. Applications only need their own calls when they run without the shell.

    +
    void    secRngAddEntropy(const uint8_t *data, int32_t len);
    +void    secRngBytes(uint8_t *buf, int32_t len);
    +int32_t secRngGatherEntropy(uint8_t *buf, int32_t len);
    +int     secRngLoadSeed(const char *path);
    +int     secRngSaveSeed(const char *path);
    +void    secSecureZero(void *ptr, int len);
    +
      Function              Description
    +  --------              -----------
    +  secRngAddEntropy      Hash bytes into the pool (key timings, mouse positions, anything unpredictable).
    +  secRngBytes           Generate pseudorandom bytes.
    +  secRngGatherEntropy   Fill buf with condensed hardware entropy (one fresh gather per 32 bytes). Returns the byte count (0 if a host build cannot read its random device).
    +  secRngLoadSeed        Mix a seed file into the pool, then immediately rewrite it with fresh output so a seed is never used twice. SEC_SUCCESS or SEC_ERR_IO.
    +  secRngSaveSeed        Write SEC_RNG_SEED_SIZE bytes of fresh output for the next run. SEC_SUCCESS or SEC_ERR_IO.
    +  secSecureZero         Zero memory in a way the compiler cannot optimize away (for key material).
    +

    Hash Functions

    +
    void secSha256(const uint8_t *data, int32_t len, uint8_t digest[32]);
    +void secSha256Init(SecSha256T *ctx);
    +void secSha256Update(SecSha256T *ctx, const uint8_t *data, int32_t len);
    +void secSha256Final(SecSha256T *ctx, uint8_t digest[32]);
    +void secHmacSha256(const uint8_t *key, int32_t keyLen, const uint8_t *data, int32_t len, uint8_t mac[32]);
    +void secHmacInit(SecHmacT *h, const uint8_t *key, int32_t keyLen);
    +void secHmacUpdate(SecHmacT *h, const uint8_t *data, int32_t len);
    +void secHmacFinal(SecHmacT *h, uint8_t mac[32]);
    +int  secHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, uint8_t *okm, int32_t okmLen);
    +bool secConstTimeEqual(const void *a, const void *b, int32_t len);
    +

    SHA-256 follows FIPS 180-4, HMAC RFC 2104, and HKDF RFC 5869 (extract and expand; a NULL or empty salt means 32 zero bytes). secHkdf returns SEC_ERR_PARAM for okmLen outside 1..SEC_HKDF_MAX_OUT. secConstTimeEqual compares without revealing where the inputs differ; use it for every MAC check.

    +

    Diffie-Hellman Key Exchange

    +

    1024-bit DH with 256-bit private exponents using the RFC 2409 Group 2 safe prime. The modular exponentiation is a Montgomery ladder with branch-free operand swaps, so its timing does not depend on the private key.

    +
    SecDhT *secDhCreate(void);
    +int     secDhGenerateKeys(SecDhT *dh);
    +int     secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
    +int     secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len);
    +int     secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len);
    +void    secDhDestroy(SecDhT *dh);
    +
      Function            Description
    +  --------            -----------
    +  secDhCreate         Allocate a new DH context.
    +  secDhGenerateKeys   Generate a private exponent and public key from the DRBG.
    +  secDhGetPublicKey   Copy the local public key into buf. *len receives the key size.
    +  secDhComputeSecret  Compute the shared secret from the remote public key. Keys outside [2, p-2] are rejected with SEC_ERR_PARAM (0, 1, and p-1 would force a trivially known secret).
    +  secDhGetSecret      Copy the shared secret (SEC_DH_KEY_SIZE big-endian bytes) into buf. It is input for a key derivation function such as secHkdf, never a key by itself.
    +  secDhDestroy        Securely zero and free the DH context.
    +

    Typical usage order: secDhCreate, secDhGenerateKeys, exchange public keys, secDhComputeSecret, secDhGetSecret, secHkdf, secDhDestroy.

    +

    XTEA-CTR Cipher

    +

    XTEA in counter mode. Encrypt and decrypt are the same operation (XOR with keystream). The counter must never repeat with the same key.

    +
    SecCipherT *secCipherCreate(const uint8_t *key);
    +void        secCipherCrypt(SecCipherT *c, uint8_t *data, int len);
    +void        secCipherSetCounter(SecCipherT *c, uint64_t counter);
    +void        secCipherDestroy(SecCipherT *c);
    +
      Function             Description
    +  --------             -----------
    +  secCipherCreate      Allocate a cipher context with a 128-bit key. Returns NULL on allocation failure or NULL key. Initial counter is zero.
    +  secCipherCrypt       Encrypt or decrypt data in place. The 64-bit counter increments by one every 8 bytes (one XTEA block).
    +  secCipherSetCounter  Position the 64-bit block counter (secLink derives it from the frame sequence number).
    +  secCipherDestroy     Securely zero the cipher context (key and counter state) and free its memory.
    +

    secCipherCreate returns NULL if the key pointer is NULL or allocation fails. secCipherCrypt is a no-op when any of c/data is NULL or len <= 0.

    +

    X25519 Key Exchange

    +

    Elliptic-curve Diffie-Hellman on Curve25519 (crypto/secCurve.h). Each side makes a secret key from 32 random bytes (secRngBytes), sends its public key, and combines its secret key with the other side's public key; both get the same 32-byte secret. One call takes about a sixth of a second on a 486 (fixtures/bncurve on the emulated 486DX2-66: 165 ms), so a program that must stay responsive does it a slice at a time with a SecX25519JobT (the secure link does).

    +
    void secX25519PublicKey(uint8_t *publicKey, const uint8_t *secretKey);
    +bool secX25519(uint8_t *shared, const uint8_t *secretKey, const uint8_t *peerPublic);
    +
    +void secX25519JobStart(SecX25519JobT *job, const uint8_t *secretKey, const uint8_t *peerPublic);
    +bool secX25519JobStep(SecX25519JobT *job);
    +bool secX25519JobResult(SecX25519JobT *job, uint8_t *out);
    +void secX25519JobWipe(SecX25519JobT *job);
    +

    secX25519 is false when the peer's key gives an all-zero secret (one of the few keys that give everyone the same secret: a cheating peer). The secret is input for a key derivation function such as secHkdf, never a key by itself.

    +

    A job is the same computation in nine slices of about 20 ms each on a 486 (eight of the ladder, then the inversion): secX25519JobStart begins it (peerPublic NULL makes the public key of secretKey), secX25519JobStep does a slice and is true once there is nothing left, secX25519JobResult writes the result (false as secX25519 is, or before the job is done) and wipes the job, and secX25519JobWipe drops one. A SecX25519JobT is the caller's (no allocation); the slicing is a small DVX patch to Monocypher (thirdparty/monocypher/DVX-VERSION.md).

    +

    XChaCha20-Poly1305

    +

    Authenticated encryption: the data is encrypted and a 16-byte tag proves it, and some associated data sent in the clear, unchanged.

    +
    void secAeadLock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, uint8_t *tag);
    +bool secAeadUnlock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, const uint8_t *tag);
    +

    Both work in place. secAeadUnlock decrypts only when the tag matches; otherwise it returns false and leaves data as it was. A key and nonce pair must never seal two different messages: a counter in the nonce (the secure link uses its frame number) or a fresh random nonce (24 bytes is enough to pick at random) keeps them apart.

    +

    Ed25519 Signatures

    +

    Public-key signatures (RFC 8032, with SHA-512), as the package library uses for add-on packages.

    +
    void secEd25519KeyPair(uint8_t *secretKey, uint8_t *publicKey, uint8_t *seed);
    +void secEd25519Sign(uint8_t *sig, const uint8_t *secretKey, const uint8_t *msg, int32_t len);
    +bool secEd25519Check(const uint8_t *sig, const uint8_t *publicKey, const uint8_t *msg, int32_t len);
    +

    secEd25519KeyPair makes the 64-byte secret key (the seed followed by the public key) and the public key from 32 random bytes, and wipes the seed. secSign.c holds these three and secCurve.c the rest, so a program that links the sources in (the host proxy) can leave the signatures out.

    +

    BASIC

    +

    src/include/basic/security.bas (DECLARE LIBRARY "crypto") declares the BASIC functions (crypto/secBas.c). Keys, data, and results are BINARY STRING (pointer and length in, a result buffer and length out), so every byte value passes; the result buffer is wiped before it is reused. A handle (1, 2, ...; 0 = none) is a key exchange, a cipher, or an incremental SHA-256 / HMAC; each records the program that made it and the shell's app-cleanup hook frees (and wipes) what a program leaves open.

    +
      BASIC                                          C it calls
    +  -----                                          ----------
    +  SecRandom$(count), SecAddEntropy bytes         secRngBytes; secRngAddEntropy.
    +  SecDhCreate()                                  secDhCreate + secDhGenerateKeys.
    +  SecDhPublicKey$(h), SecDhSecret$(h)            secDhGetPublicKey; secDhGetSecret ("" before SecDhComputeSecret).
    +  SecDhComputeSecret(h, remotePublicKey)         secDhComputeSecret: SEC_OK or a SEC_ERR_ code.
    +  SecDhFree h                                    secDhDestroy.
    +  SecCipherCreate(key)                           secCipherCreate; 0 unless key is SEC_XTEA_KEY_SIZE bytes.
    +  SecCipherCrypt$(h, bytes)                      secCipherCrypt on a copy.
    +  SecCipherSetCounter h, counter                 secCipherSetCounter (a DOUBLE: exact up to 2^53).
    +  SecCipherFree h                                secCipherDestroy.
    +  SecSha256$(bytes); SecSha256Open, SecSha256Add, SecSha256End$  secSha256; secSha256Init / Update / Final (End frees the handle).
    +  SecHmac$(key, bytes); SecHmacOpen, SecHmacAdd, SecHmacEnd$  secHmacSha256; secHmacInit / Update / Final.
    +  SecHkdf$(salt, secret, info, length)           secHkdf ("" for a length outside 1..SEC_HKDF_MAX_OUT).
    +  SecEqual(a, b)                                 secConstTimeEqual (False for different lengths).
    +  SecX25519PublicKey$(secretKey)                 secX25519PublicKey ("" unless the key is SEC_X25519_KEY_SIZE bytes).
    +  SecX25519$(secretKey, otherPublicKey)          secX25519 ("" for a wrong size or a refused key).
    +  SecX25519Start(secretKey, otherPublicKey)      secX25519JobStart in a handle ("" otherPublicKey: the public key); 0 for wrong sizes.
    +  SecX25519Step(h); SecX25519End$(h); SecX25519Free h  secX25519JobStep (SEC_X25519_PENDING, then SEC_OK); secX25519JobResult (frees the handle); secX25519JobWipe.
    +  SecAeadLock$(key, nonce, extra, bytes)         secAeadLock on a copy: the ciphertext, then the tag.
    +  SecAeadUnlock$(key, nonce, extra, sealed)      secAeadUnlock on a copy ("" when the tag does not match).
    +  SecEd25519KeyPair$(seed)                       secEd25519KeyPair: the secret key (the program's seed is not wiped).
    +  SecEd25519PublicKey$(secretKey)                The public key (the secret key's last SEC_ED25519_PUBLIC_SIZE bytes).
    +  SecEd25519Sign$(secretKey, bytes)              secEd25519Sign.
    +  SecEd25519Check(signature, publicKey, bytes)   secEd25519Check (False for wrong sizes).
    +

    The SEC_ CONSTs of security.bas are compared with security.h, secHash.h, and secCurve.h by a formrt test, which also calls every function for real.

    +

    Networking Library

    TCP/IP for DVX programs with the standard BSD socket API: a C programmer who knows sockets writes DVX network code the usual way. IPv4 today; the types and constants for IPv6 (AF_INET6, sockaddr_in6, sockaddr_storage) are defined so code can be written for both.

    @@ -8121,7 +8443,22 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

    Waiting, Polling, and Cooperative Multitasking

    DVX runs programs cooperatively, so a call that has to wait never stops the system: blocking connect, accept, recv, send (with a full buffer), select, poll, and name lookups wait by running the network and then letting the other tasks run (tsYield), again and again, until they can return. Other programs, and other connections, keep working meanwhile.

    The network also runs from the shell's idle time, so connections progress (and data arrives) while a program is busy with other things; every socket call runs it too. A program that polls non-blocking sockets in its own loop needs nothing else.

    -

    Right after the network starts, DHCP needs a moment to get an address. connect, sendto, and name lookups wait for it (up to 20 seconds) instead of failing, so a program started together with DVX can connect at once.

    +

    Right after the network starts, DHCP needs a moment to get an address. Blocking connect and sendto calls, and name lookups, wait for it (up to 20 seconds) instead of failing, so a program started together with DVX can connect at once. A non-blocking socket does not wait.

    +

    A program that must not wait at all, not even inside one call, looks a name up with netLookupStart and netLookupPoll (dvxNet.h) instead of getaddrinfo: each poll only looks, and the lookup waits for DHCP's first address too, so a non-blocking connect to the address it finds goes at once. DVX's own web requests (http) and Terminal connect this way.

    +
    NetLookupT lk;
    +uint32_t   addr;
    +int32_t    rc;
    +
    +netLookupStart(&lk, "bbs.example.org");
    +
    +// Each frame, or from a timer:
    +rc = netLookupPoll(&lk, &addr);
    +
    +if (rc == 0) {
    +    // addr (network order): connect a non-blocking socket to it
    +} else if (rc != NET_LOOKUP_BUSY) {
    +    // gai_strerror(rc) says why
    +}

    Sockets Belong to Programs

    Each socket belongs to the program that created it (accept gives the new socket to the program that accepts). When a program ends, normally or because it was ended or crashed, the shell closes every socket it still had open, and drops its unfinished name lookups.

    @@ -8134,6 +8471,9 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. void netStop(void) Close every socket and stop (the packet driver is released). int32_t netRestart(void) Stop, then start with the saved settings (after they changed). void netPoll(void) Take received frames, run the timers, send. Called from the shell's idle time and by every socket call. + void netLookupStart(NetLookupT *lk, const char *name) Start looking a name (or a dotted address) up without waiting. + int32_t netLookupPoll(NetLookupT *lk, uint32_t *addr) NET_LOOKUP_BUSY, 0 with the address (network order), or an EAI_* code; never waits. + void netLookupCancel(NetLookupT *lk) Give up a lookup that has not ended. void netGetStatus(NetStatusT *status) State (NetStoppedE, NetNoDriverE, NetConfiguringE, NetUpE), plain-words message, addresses, name servers, host name, packet driver name, interrupt and hardware address, packet and byte counters, frames lost while DVX was busy, open sockets. const char *netStateText(int32_t state) Plain words for a state. const char *netErrorText(int32_t err) Plain words for an errno value, network codes included. @@ -8157,13 +8497,13 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Driver pktDrvDos.c, pktRing.c Packet driver calls through DPMI; a real-mode callback receives frames at interrupt time into a 16-slot ring in conventional memory; the stack takes them out later. DOS glue netDos.c Descriptors through DJGPP File System Extensions, fcntl for sockets, the shell hooks (idle poll, app cleanup), orderly shutdown. Settings dvxNet.c DVX.INI, status, errors in plain words. -

    lwIP runs without threads (NO_SYS), configured only by port/lwipopts.h: TCP MSS 1460 with a 4-segment window, 16 TCP connections, 48 KB of stack memory, DHCP, DNS (2 servers), ARP, ICMP (ping answers), IP fragments, loopback. Nothing runs lwIP at interrupt time.

    +

    lwIP runs without threads (NO_SYS), configured only by port/lwipopts.h: TCP MSS 1460 with a 4-segment window, 16 TCP connections, 24 KB of stack memory for data being sent and 20 receive buffers, DHCP, DNS (2 servers), ARP, ICMP (ping answers), IP fragments, loopback. Nothing runs lwIP at interrupt time.

    The receive ring holds 16 frames. A burst larger than that while DVX is busy loses frames (counted as "lost" in the status); TCP sends them again.

    On the host the same sources are built into the "net" test suite: a fake packet driver passes frames through the real ring to a second copy of the stack in another process, which plays DHCP, DNS, and echo server.

    Networking from BASIC

    -

    net.bas declares simple functions over the socket API (netBas.c): HostLookup$, HostName$, NetAddress$, NetError$, TcpConnect, TcpListen, TcpAccept, TcpSend, TcpPending, TcpRecv$, TcpConnected, TcpShutdown, TcpPeer$, TcpLocalPort, TcpSetNoDelay, TcpSetKeepAlive, TcpClose, UdpOpen, UdpSend, UdpRecv$, UdpFrom$, UdpClose. A handle is the socket's descriptor + 1, so 0 means failure. Only TcpConnect (up to 20 seconds) and TcpSend wait; everything else returns at once, so a program polls from a Timer. TcpSend, TcpRecv$, UdpSend, and UdpRecv$ pass their data as BINARY STRING (pointer and length), so every byte value goes through. See the BASIC language reference, Include Libraries.

    +

    net.bas declares simple functions over the socket API (netBas.c): HostLookup$, HostLookupStart / Poll / Result$ / Close, HostName$, NetAddress$, NetError$, TcpConnect, TcpConnectStart / Poll, TcpListen, TcpAccept, TcpSend, TcpSendSome, TcpPending, TcpRecv$, TcpPeek$ (recv with MSG_PEEK), TcpConnected, TcpShutdown, TcpPeer$, TcpLocalPort, TcpSetNoDelay, TcpSetKeepAlive, TcpClose, UdpOpen, UdpSend, UdpRecv$, UdpFrom$, UdpClose. A handle is the socket's descriptor + 1, so 0 means failure. Only TcpConnect (up to 20 seconds), HostLookup$, and TcpSend wait; everything else returns at once, so a program polls from a Timer. The other socket options need no binding: TcpListen sets SO_REUSEADDR and UdpOpen SO_BROADCAST, the stack takes SO_LINGER and the buffer sizes without effect (closing finishes in the background; the buffers are fixed), and no BASIC call waits on a receive, so SO_RCVTIMEO does nothing for it. TcpSend, TcpRecv$, UdpSend, and UdpRecv$ pass their data as BINARY STRING (pointer and length), so every byte value goes through. See the BASIC language reference, Include Libraries.

      BASIC                                   C it calls
       -----                                   ----------
       HostName$(address)                      getnameinfo: the address itself, as this stack does not look names of addresses up; "" when it is not an address.
    @@ -8184,6 +8524,8 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    NetConfigFree(cfg) Free the handle. NetVectorCount / NetVector(i) The packet driver interrupt choices: 0 (find it), then NET_VECTOR_FIRST..NET_VECTOR_LAST. NetRestart() netRestart; True for NET_OK. + netBasStreamRead / netBasStreamWrite A net.bas TCP handle as a byte stream that never waits, for another library's BASIC functions (comm.bas's CommOpenTcp runs the secure packet link over it): bytes read or taken (0 = none now), negative once the connection has closed or failed. + NetState() netGetStatus's state (NetStateE; BASIC's NET_STATE_STOPPED, _NO_DRIVER, _CONFIGURING, _UP). NetStatus$(field) netGetStatus, one field as text: message, up, link, dhcp, address, netmask, gateway, dns1, dns2, hostname, driver, mac, vector, sockets, rxpackets, rxbytes, rxdropped, txpackets, txbytes.

    Settings handles (1 to 16, 0 = none) belong to the program that made them; netCloseAppSockets frees what a program left.

    @@ -8315,7 +8657,9 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. dxeCtx DxeAppContextT * Heap-allocated context (address stable across realloc). taskCodeDepth int32_t * Per task id: SHELL_APP_CALL calls into this app still open on that task's stack. killPending bool A force kill was deferred; shellReapApps finishes it. - faulted bool It crashed: its force kill runs no kill hooks (shellRegisterAppKill). + faulted bool It crashed: its force kill runs no kill hooks (shellRegisterAppKill). + workTaskId uint32_t The task shellAppTaskStart started for it, while that runs; else 0. + workFn, workArg void (*)(void *), void * What that task runs.

    App Lifecycle API

    @@ -8339,7 +8683,7 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

    Gracefully shut down a single app. Calls the app's shutdownFn (if present), destroys all windows belonging to the app, kills its task (if any), releases any widget classes and system-wide hotkeys the app registered and did not unregister (wgtUnregisterAppClasses, dvxUnregisterAppHotkeys), closes the DXE handle, and frees the context. The slot returns to AppStateFreeE.

    shellForceKillApp

    void shellForceKillApp(AppContextT *ctx, ShellAppT *app);
    -

    Forcibly kill an app. Used by the Task Manager "End Task" or as part of crash recovery (which marks the app faulted first). The app's shutdownFn is NOT called: a force kill targets a hung or faulting app whose own code cannot be trusted to run. Instead the libraries' kill hooks (shellRegisterAppKill) end what they run for the app, unless it faulted, then the shell drops every idle and desktop-update callback the app registered, destroys all of the app's windows, kills its task (for main-loop apps), and finally closes the DXE handle.

    +

    Forcibly kill an app. Used by the Task Manager "End Task" or as part of crash recovery (which marks the app faulted first). The app's shutdownFn is NOT called: a force kill targets a hung or faulting app whose own code cannot be trusted to run. Instead the libraries' kill hooks (shellRegisterAppKill) end what they run for the app, unless it faulted, then the shell drops every idle and desktop-update callback the app registered, destroys all of the app's windows, kills its tasks (a main-loop app's, and a work task shellAppTaskStart started), and finally closes the DXE handle.

    Cleanup order: callbacks and windows first (removes them from the compositor), then the task (frees its stack; any dispatch frames still open on it are dropped from the context's dispatch depth), then the widget classes and system-wide hotkeys the app registered (its shutdownFn never got the chance to unregister them), then the DXE handle (unmaps the code). Closing the DXE before destroying windows would cause callbacks into unmapped code.

    If the app's code may still be running underneath the call -- End Task clicked in a dialog the app has open, a kill issued from the app's own task, or from inside a shell call into the app -- the unload is deferred: the app's windows are hidden and queued for destruction, the slot is marked Terminating, and the shell finishes the kill (still without shutdownFn) at the next safe point of its main loop, once that code has returned. Event-handler frames on the app's own task do not hold the kill back, since they end with the task: an app stuck in an endless event handler is killed at once (or at the next main-loop pass).

    shellOpenHelp

    @@ -8426,16 +8770,22 @@ shellUserPath(ctx, "PROGMAN.INI", path, sizeof(path));

    shellDataPath

    void shellDataPath(const DxeAppContextT *ctx, const char *filename, char *outPath, int32_t outSize);

    Build the full path of a file the program ships in the DATA folder beside its .app ("" when it does not fit), e.g. "APPS\ACME\MYGAME\DATA\LEVEL1.DAT". Read-only.

    +

    Program Cache

    +

    Program names, descriptions, and icons come from the programs' own files (their name, description, and icon32 resources), and reading them takes several file opens per program, tens of milliseconds each on a slow disk, every time DVX starts. The shell keeps what it read in PROGRAMS.CAC in the user folder, with each file's size and time (platformFileStamp), and answers from it while a file is unchanged: shellProgramInfo, shellItemIcon, shellItemIcons, shellItemIconFile, and shellProgramIcon all go through it, so Program Manager, File Manager, and BASIC's Shell calls start without opening any program that has not changed.

    +

    A file is checked against its stamp the first time it is asked about after what is installed last changed (platformInstalledChangeCount), and whenever shellProgramScan reads names (the folder is fresh in DOS's buffers then); a program that changed is read again, and a file that is gone loses its entry. A picture is kept as the image file's bytes (a DVX file's icon resource, or a picture file of up to 32 KB) and decoded for the display mode each time; bytes that turn out to be no image are noted as none.

    +

    The file is read on first use. A change is saved three seconds after the last one, 4 KB at a time in the shell's idle time, so no frame waits for the whole write, and what is left at shutdown is saved then; the memory goes after a minute unused. It is DVX's memory (dvxMemSetApp), whichever program's call made it. Nothing is saved when DVX cannot save (platformIsReadOnly). Deleting PROGRAMS.CAC is harmless: it is made again. A file rewritten at the same size within two seconds keeps its stamp (DOS keeps a file's time to two seconds), so a program changed that quickly shows its old name or icon until it changes again.

    Random Seed File

    -

    When the serial library is loaded, the shell feeds keyboard and mouse event timings to its random-number generator (dvxSetInputEntropyHook) and carries generator state across runs in SECLINK.RND in the user folder: the file is read and immediately rewritten at startup, and rewritten again at shutdown. The library is found at run time; without it no seed file is written.

    +

    When the cryptography library is loaded, the shell feeds keyboard and mouse event timings to its random-number generator (dvxSetInputEntropyHook) and carries generator state across runs in SECLINK.RND in the user folder: the file is read and immediately rewritten at startup, and rewritten again at shutdown. The library is found at run time; without it no seed file is written.

    Desktop Callbacks

    -

    The shell wires several AppContextT event hooks (onCtrlEsc, onF1, onTitleChange, idleCallback) during startup and exposes them to other DXEs via function-pointer extern and registration API. Desktop managers register a callback to refresh their display when apps come and go. The Task Manager DXE registers its Ctrl+Esc handler via the shellCtrlEscFn extern pointer (see Task Manager documentation); shellOpenTaskManager is the one way to open it.

    +

    The shell wires several AppContextT event hooks (onCtrlEsc, onF1, onTitleChange, idleCallback, busyCallback, inputHere) during startup and exposes them to other DXEs via function-pointer extern and registration API. Desktop managers register a callback to refresh their display when apps come and go. The Task Manager DXE registers its Ctrl+Esc handler via the shellCtrlEscFn extern pointer (see Task Manager documentation); shellOpenTaskManager is the one way to open it.

    Shell Event Hooks

      AppContextT field    Handler                 Purpose
       -----------------    -------                 -------
       idleCallback         shellIdleDispatch       Runs one frame of Run Minimized (shellMinimizeIdle) and every registered idle handler (see shellRegisterIdle), then yields to cooperative tasks, when dvxUpdate has no input events and no dirty rects.
    +  busyCallback         shellBusyDispatch       After a frame that painted: yields to cooperative tasks, and runs shellIdleDispatch when the idle handlers have not run for 20 ms, so a program that keeps the screen changing never starves the others, audio, or the network.
    +  inputHere            shellInputHere          Input, events, and widget timers are taken on the shell's own task (or on any task once the shell's has not taken them for 250 ms), so callbacks run on the shell's task, never on another program's stack.
       onCtrlEsc            ctrlEscHandler          shellOpenTaskManager.
       onF1                 f1HelpHandler           Opens the focused app's helpFile/helpTopic (or the system help) in the help viewer, reusing a running viewer that sets onNewArgs.
       onTitleChange        titleChangeHandler      Runs shellDesktopUpdate so Program Manager window lists refresh.
    @@ -8447,7 +8797,7 @@ shellUserPath(ctx, "PROGMAN.INI", path, sizeof(path));

    Remove a previously registered callback. Call this before app shutdown to avoid dangling function pointers.

    shellRegisterIdle

    void shellRegisterIdle(void (*fn)(void *ctx), void *ctx);
    -

    Register an idle handler invoked every idle frame by the shell main loop, regardless of which app is foreground. Background pollers (serial and secLink) register here so their connections keep pumping while another app is in front. Registration is idempotent on the (fn, ctx) pair. The registering module MUST unregister before it is unloaded -- app DXEs do this from their _appShutdown hook on a graceful exit. On a force kill (which does not run _appShutdown) the shell drops the app's registrations itself.

    +

    Register an idle handler invoked every idle frame by the shell main loop (and at least every 20 ms while frames keep painting), regardless of which app is foreground. Background pollers (serial and secLink) register here so their connections keep pumping while another app is in front. Registration is idempotent on the (fn, ctx) pair. The registering module MUST unregister before it is unloaded -- app DXEs do this from their _appShutdown hook on a graceful exit. On a force kill (which does not run _appShutdown) the shell drops the app's registrations itself.

    shellUnregisterIdle

    void shellUnregisterIdle(void (*fn)(void *ctx), void *ctx);

    Remove a previously registered idle handler. No-op if the (fn, ctx) pair is absent, so a double-unregister is harmless.

    @@ -8461,6 +8811,9 @@ void shellUnregisterAppCleanup(ShellAppCleanupFnT fn);
    void shellRegisterAppKill(ShellAppCleanupFnT fn);
     void shellUnregisterAppKill(ShellAppCleanupFnT fn);

    Register a function the shell calls with an app's id when the app is ended by force (End Task, a screen saver that would not close, DVX exiting) while none of the app's code is running and its windows are still there -- before the windows go and before the cleanup hooks. A library ends what it runs for the app the way the app's own end would: the BASIC runtime (basrt) frees a standalone program's VM, forms, screen, and module this way, closing its serial ports and its forms' windows, so End Task on a BASIC program leaks nothing. Not called for an app that faulted (its state cannot be trusted), nor for one whose kill had to wait until its code returned (End Task from inside the app's own dialog: its windows went at once). Registration rules as for cleanup hooks.

    +

    shellAppTaskStart

    +
    int32_t shellAppTaskStart(AppContextT *ctx, const char *name, uint32_t stackSize, void (*fn)(void *arg), void *arg);
    +

    Run fn(arg) on a cooperative task of its own (stackSize bytes; 0 for the default) that belongs to the running program (ctx->currentAppId), for a long piece of a callback-only program's work: the DVX BASIC IDE runs each program it starts this way. A callback-only program's code runs inside the shell's main loop, which only reaps programs that ended, carries out close and Exit requests and DOS program starts, and starts the screen saver between frames; work done in a callback holds all of that until it returns. On its own task the work leaves the main loop free. The program is the running one on the task (windows it makes are the program's), it is not reaped while the task runs (even with all its windows closed), and ending it by force (End Task) ends the task. One at a time per program. Returns the task's ID, or -1 when one runs already, no program is running, or no task can be made.

    shellDesktopUpdate

    void shellDesktopUpdate(void);

    Notify all registered desktop callbacks that app state has changed. Called internally by the shell after loading or reaping an app. Can also be called by apps that need to trigger a desktop refresh. Each call also adds one to the change counter (shellDesktopChangeCount).

    @@ -8829,7 +9182,7 @@ int32_t SysLibraryLoaded(const char *name);
  • Paths are made absolute and canonical (relative names join the current directory, backslashes become forward slashes, "." and ".." are resolved), so every spelling of one file is the same database.
  • Locking is in-process: DOS is single user, but two connections inside DVX (two programs, or a Data control and BASIC code) can share a file. Any number may read; one may write. A write while another connection is writing, or a commit while another connection has a query open, fails with "database is locked"; retry after the other side finishes (dvxSqlFreeResult releases a query's read lock). Nothing is locked at the DOS level, so do not share a database with another program outside DVX.
  • Temporary files, when used, are SQLxxxxx.TMP in the directory named by TMPDIR, TEMP, or TMP (else the current directory) and are deleted when closed.
  • -
  • Randomness comes from the serial library's generator (secRngBytes) when that library is loaded, else from platformEntropyStamp mixed with the time of day.
  • +
  • Randomness comes from the cryptography library's generator (secRngBytes) when that library is loaded, else from platformEntropyStamp mixed with the time of day.
  • Databases written by the SQLite 3.5.6 that earlier DVX releases bundled open, read, and write unchanged. Files created now use the current file format, which SQLite 3.5.6 cannot read.

    Handle Model

    @@ -8870,7 +9223,7 @@ int32_t SysLibraryLoaded(const char *name);

    Safe to call with an invalid handle (no-op).

    dvxSqlExec

    bool dvxSqlExec(int32_t db, const char *sql);
    -

    Execute one or more SQL statements that return no result rows. Suitable for DDL (CREATE TABLE, DROP TABLE, etc.) and DML (INSERT, UPDATE, DELETE).

    +

    Execute one or more SQL statements that return no result rows. Suitable for DDL (CREATE TABLE, DROP TABLE, etc.) and DML (INSERT, UPDATE, DELETE). Every statement in sql runs, one after another, within this one call; give the database a pump (dvxSqlSetPump) when that may take long.

      Parameter    Description
       ---------    -----------
       db           Database handle.
    @@ -8890,6 +9243,22 @@ int32_t     SysLibraryLoaded(const char *name);
    --------- ----------- db Database handle.

    Returns the row count, or 0 if the handle is invalid.

    +

    dvxSqlSetPump

    +
    typedef bool (*DvxSqlPumpFnT)(int32_t db);
    +
    +void dvxSqlSetPump(int32_t db, DvxSqlPumpFnT pump);
    +

    Give a database a pump, or take it away (NULL; a database opens without one). While a statement runs on the database (in dvxSqlExec, dvxSqlQuery, or dvxSqlNext, whose first step of a sorted or grouped query reads every row), the pump is called with the database's handle every 1000 steps or so. It can let other work run, for example a frame of DVX's. Returning false stops the statement: the call fails, dvxSqlError says "interrupted", and what the statement changed is undone.

    +
      Parameter    Description
    +  ---------    -----------
    +  db           Database handle.
    +  pump         The function to call, or NULL for none.
    +

    The code a pump runs may use other databases, open and close them, as always. Its calls on the database whose statement is running are refused, as are any calls made from the pump on it:

    + +

    DVX BASIC gives every database a program opens a pump (see SQLOpen in the BASIC reference). The Data control's own connections have none.

    Cursor Operations

    @@ -9047,7 +9416,7 @@ dvxSqlExec(db, sql); fileOpsFormatSize(bytes, out, size) "1,234,567". fileOpsFormatDate(t, out, size) "YYYY-MM-DD HH:MM" in local time. fileOpsParseDate(text, &t) The time fileOpsFormatDate's text stands for ("YYYY-MM-DD HH:MM", optionally ":SS"), local time; false for anything else, a date that does not exist included. - fileOpsTreeSize(path, &bytes, &files, &folders) Total size of a file or of a folder and everything in it. + fileOpsTreeSize(path, &bytes, &files, &folders) Total size of a file or of a folder and everything in it (the folders inside it; a measure job run to its end). False when part of it cannot be read; the totals then cover what was read. 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. @@ -9058,7 +9427,7 @@ dvxSqlExec(db, sql); 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):

    +

    A job copies, moves, deletes, or measures (FileOpsMeasureE: counts the files, folders, and bytes, changing nothing) any number of files and folders (recursively):

    FileOpsJobT *job = fileOpsJobCreate(FileOpsCopyE, "C:\\BACKUP");
     
     fileOpsJobAddSource(job, "C:\\DOCS\\LETTER.TXT");
    @@ -9082,7 +9451,7 @@ for (;;) {
     }
     
     fileOpsJobFree(job);
    -

    Each fileOpsJobStep does one bounded piece of work: read one folder, copy one FILEOPS_COPY_CHUNK of one file, or delete one item. A cooperative program yields between steps.

    +

    Each fileOpsJobStep does one bounded piece of work: read a few dozen entries of a folder (a big folder takes several steps), copy one FILEOPS_COPY_CHUNK of one file, or delete one item. A cooperative program yields between steps.

      Step result              Meaning
       -----------              -------
       FileOpsStepBusyE         More work to do.
    @@ -9090,9 +9459,10 @@ fileOpsJobFree(job);
    FileOpsStepErrorE Something failed; fileOpsJobMessage says why. Answer Retry, Skip, or Cancel. FileOpsStepDoneE Finished. FileOpsStepCancelledE Stopped by Cancel. -

    Until it is answered, the job keeps returning the same question. What the job does:

    +

    Until it is answered, the job keeps returning the same question. The next step tries the item again and applies the answer if it fails again; an item that goes through leaves the answer unused (it is never kept for a later question). Cancel stops at once, without trying again. What the job does:

    @@ -9271,7 +9641,7 @@ httpFree(h); settingsChanged() NULL, or re-read the driver's settings (joySettingGet) after they changed.

    Up to JOY_MAX_DRIVERS drivers in use get their tick called. A driver is loaded again at every probe, so it must not rely on state kept from an earlier load; set its hardware up in probe (or a constructor).

    BASIC

    -

    src/include/basic/joystick.bas declares JoyCount, JoyName$, JoyAxes, JoyButtons, JoyX, JoyY, JoyAxis, JoyButton, and JoyHat. Joystick, axis, button, and hat numbers start at 1; each function reads through the calling program's shared handle.

    +

    src/include/basic/joystick.bas declares JoyCount, JoyName$, JoyAxes, JoyButtons, JoyX, JoyY, JoyAxis, JoyButton, and JoyHat. Joystick, axis, button, and hat numbers start at 1; each function reads through the calling program's shared handle. Events: JoyWatch(joy, watching) sets (or clears) that handle's joySetEventFn, which keeps the program's changes, the last BAS_EVENTS_MAX (64); JoyEvent gives the next as JoyEventKindE + JOY_BAS_EVENT_FIRST (JOY_BAS_EVENT_NONE for none; CONSTs JOY_EVENT_*), and JoyEventJoy, JoyEventIndex (both from 1), and JoyEventValue the one it gave last. joyBasForget drops a program's kept changes when it ends.

    For a settings program (the Control Panel's Game Controllers page) it also declares: JoyHats, JoyAxisPresent(joy, axis), JoyCanCalibrate(joy) (JoyInfoT hatCount, axisPresent, JOY_CAP_CALIBRATE); JoyConnected(joy) (joyRead's connected); JoyRescan (joyRescanApp for the calling program: its own handles and STICK readings go first; -1 while another program has a joystick open); JoyRawAxis(joy, axis) and JoyRawButtons(joy) (joyGetRaw); JoySetCalibration(joy, axis, minV, centerV, maxV) and JoyClearCalibration(joy, axis) (joySetCalibration); JoyDeadZone / JoySetDeadZone (joyGetDeadZone / joySetDeadZone); JoyChoiceCount(which), JoyChoiceLabel$(which, index), JoyChoice(which), JoySetChoice(which, index) with JOY_CHOICE_GAMEPORT = 0 and JOY_CHOICE_FALLBACK = 1 (JoyChoiceE; joyChoices / joyChoiceGet / joyChoiceSet); JoyKeyCount and JoyKeyName$(index); JoyKbdSlotCount, JoyKbdSlot(slot), JoySetKbdSlot(slot, keyIndex) (joyKbdSlotGet / Set); JoySetting$(key, defaultValue) and JoySetSetting(key, value) ("" removes; joySettingGet / Set); JoySettingsLoad(prefs) and JoySettingsStore(prefs) with a prefs.bas handle (prefsBasHandle); JoyCalibration(joy, axis, part) (joyGetCalibration; part JOY_CAL_MIN / JOY_CAL_CENTER / JOY_CAL_MAX, joystick.h; 0 when joyGetCalibration fails); JoyIsKeyboard(joy) (JOY_CAP_KEYBOARD); JoyKeyIndex(keyName), JoyKeyCode(index), JoyKeyNameOf$(rawCode) (joyKeyIndex / joyKeyCode / joyKeyNameOf). CONSTs JOY_DEADZONE_DEFAULT / _MAX, JOY_POLL_HZ_DEFAULT / _MIN / _MAX, and JOY_KEY_POLL_HZ mirror joystick.h. Lists (choices, keys, slots) are numbered from 0; True is -1.

    The BASIC runtime calls joyBasStick(n) and joyBasStrig(n) for QBasic's STICK and STRIG. Joystick A is device 0 (axes 0-1, buttons 0 and 1); joystick B is device 1, or axes 2-3 and buttons 2-3 of device 0 when it is the only device and has more than two axes. STICK(0) reads A's X and remembers A's Y and B's X and Y for STICK(1) to STICK(3) (per program, forgotten when it ends); positions are 1 to 200 ((value - JOY_AXIS_MIN) x 199 / 65535 + 1), 0 without that joystick. Even STRIG numbers use joyButtonWentDown (0 A button 1, 2 B button 1, 4 A button 2, 6 B button 2), odd ones the state joyRead reports; True is -1.

    @@ -9292,7 +9662,7 @@ httpFree(h); jsonCount(doc, node) An array's items or an object's members (0 for anything else). jsonChild(doc, node, index) Item or member index, in order. jsonKey(doc, node) A member's name ("" for anything else). -

    Strings are held decoded: the escapes \n, \t, \", \\, \/, \b, \f, and \r become their characters, \u escapes below 128 their character and others '?'; other bytes, UTF-8 included, pass through. Arrays and objects nest JSON_DEPTH_MAX (64) deep at most.

    +

    Strings are held decoded into DVX text (code page 437): the escapes \n, \t, \", \\, \/, \b, \f, \r, and \u (surrogate pairs included) and UTF-8 sequences become their characters, with '?' for a character DVX does not have; other bytes pass through. Arrays and objects nest JSON_DEPTH_MAX (64) deep at most.

    char      err[JSON_ERR_MAX];
     JsonDocT *doc = jsonParse(text, len, err, sizeof(err));
     
    @@ -9304,7 +9674,7 @@ for (int32_t i = 0; doc && i < jsonCount(doc, jsonFind(doc, 0, "
     
     jsonFree(doc);

    Writing

    -

    jsonQuote(text, out, outSize) writes text as a JSON string -- quotes, escapes, control characters as \u00XX -- and returns the length it needs (as snprintf does), so a NULL out with size 0 measures it.

    +

    jsonQuote(text, out, outSize) writes text as a JSON string -- quotes, escapes, control characters and characters from 128 on as \u escapes of their Unicode code points, so the result is plain ASCII -- and returns the length it needs (as snprintf does), so a NULL out with size 0 measures it.

    Writing Games for DVX

    @@ -9428,6 +9798,7 @@ if (!dvxExclusivePresent8(ctx, fb, 320, 200, 320)) {
  • dvxExclusivePresent8 shows a picture of palette numbers on the whole screen, scaled by the largest whole factor that fits and centred on black, in any pixel format. At 8 bits per pixel the palette is the screen's own: dvxExclusiveSetPalette changes colours at once, so palette animation costs nothing, and a hardware model of up to 16 palettes of 16 colours fits in the 256 entries.
  • To draw yourself, draw into dvxExclusiveDisplay(ctx)->backBuf (DVX's draw functions work on it with dvxExclusiveBlitOps) and call dvxExclusiveFlush. dvxExclusiveWaitRetrace waits for the vertical retrace before a flush, so the frame is not torn.
  • dvxExclusiveEnd gives the screen back. Closing the window, the program ending, and End Task do too.
  • +
  • dvxExclusiveMirror instead shows the window's own content on the whole screen -- its widgets, or what it draws into its content buffer -- in the desktop's pixel format, scaled and centred the same way and kept up to date as the window paints; the mouse works on the content as in the window, with its cursor drawn. A BASIC form does this with FullScreen = True (and hears the changes in its FullScreen event); a BASIC SCREEN program uses FULLSCREEN.
  • @@ -9711,7 +10082,7 @@ End Sub mathx.dhs Developer help, added to the system help Makefile KIND = library

    How the Library Works

    -

    DVX loads every library when it starts, before any widget or program, in the order the .dep files require. Every function that is not static is exported: a program, widget, or library loaded later calls it directly, and DVX connects the call when it loads that module. A library that calls another library's functions lists it in its .dep file.

    +

    DVX loads a library when something first needs it: a program, widget, or library that calls one of its functions (DVX loads the library just before that module), or a lookup by name (dlsym(NULL), a BASIC DECLARE LIBRARY call, dvxLibraryLoaded). Libraries with code that runs as they load (constructors) are loaded when DVX starts, so they can hook into the shell. Every function that is not static is exported: a program, widget, or library calls it directly, and DVX connects the call when it loads that module. A library that calls another library's functions lists it in its .dep file, which DVX loads first. A module that opens another module calls platformModuleOpen (dvxPlat.h), never dlopen, which dvx.exe does not export.

    BASIC programs add mathx.bas to their project. DECLARE LIBRARY looks up each function by exactly the name it declares, without a trailing $ (a $ marks a function returning a string), so mathxGreet$ calls the C function mathxGreet. Keep the declarations and the C functions in step; strings are passed as const char * and returned as const char *.

    A string declared AS STRING is text and stops at the first zero byte. For bytes (network data, samples, encrypted data) declare BINARY STRING: each BINARY STRING parameter becomes two C parameters at its place, the bytes (const uint8_t *, a private copy valid during the call, followed by a zero byte) and their count (int32_t); a BINARY STRING result adds one last C parameter, int32_t *outLen, where the function stores the length of the bytes it returns (the runtime copies them right after the call, so a static buffer is fine; NULL, a negative length, or a length never set give an empty string). The template's tplNameXor shows both:

      // tplNameXor$(BYVAL bytes AS BINARY STRING, BYVAL key AS LONG) AS BINARY STRING
    @@ -9794,7 +10165,7 @@ End Sub
    CTL0, CTL1 ... Each control's Begin UserControl header text, in [Controls] order icon24, name The first control's toolbox picture and tooltip; the second and later have "-2", "-3" ... suffixes, as for any widget file holding several widgets author, version ... The project's metadata; helpfile its help file's name -

    The loader tells such a file from a module by its first bytes: instead of loading it, it hands it, once every library and native widget is loaded, to the BASIC run-time library's basCtlLoadFile (found with dlsym; without the library the file is skipped and logged). basCtlLoadFile registers each control in the file for every program and the loader records the file as theirs, so the toolbox finds their icon24 and name.

    +

    The loader tells such a file from a module by its first bytes: instead of loading it, it hands it, once every native widget is loaded, to the BASIC run-time library's basCtlLoadFile (found with dlsym; without the library the file is skipped and logged). basCtlLoadFile registers each control in the file for every program and the loader records the file as theirs, so the toolbox finds their icon24 and name.

    The Bridge

    Each control is one object of its BASIC class plus a border-less canvas, the UserControl surface its code draws on (the same drawing code as a BASIC PictureBox). The widget class forwards to the class's code: layout sizes the canvas, and between frames a new size runs UserControl_Resize and UserControl_Paint; the mouse, key, wheel, and focus class methods run UserControl_MouseDown and the other handlers. A control file's code runs in one VM shared by every program that uses its controls; it never yields, and a call from another task while it runs code is refused (the paint keeps the old picture, the event is dropped). A BASIC program's own controls ([Controls] in its project) are registered by the program when it starts, owned by it, and run in its own VM.

    Your First BASIC Control

    @@ -9871,7 +10242,7 @@ bin/dvxpkg info NAME.dpk # what the package says<

    Give acme.KEY to the people who install your packages (Software, File > Trust a Publisher's Key) and keep acme.SEC private and backed up: anyone who has it can sign as you, and an update must be signed with the key that signed the version installed. bin/dvxpkg also signs and checks packages and lists their contents; on DVX, SYSTEM/DVXPKG.EXE does the same, and installs and removes packages. The package library and its file format are described under Package Library.

    DVX's Own Packages and Optional Parts

    DVX is packages too: kpunch.dvx (the loader, SYSTEM, the shell, the core libraries and widgets, and the everyday programs), kpunch.basefont (the base fonts), and the optional parts Setup's "What to install" page offers. Each part is a bundle (kind bundle: a package with no files of its own that needs its members) and each member is one module in its own package: kpunch.basic (dvxbasic, basdev), kpunch.data (dvxsql, datactrl, dbgrid), kpunch.net (dvxnet, http, pkgnet), kpunch.comm (terminal, conn, xfer; it needs kpunch.net), kpunch.media (player, recorder), kpunch.devtools (iconed, resedit, helpedit, pkgbuild, widshow), and kpunch.demos (dvxdemo, basdemo). src/packages.ini lists them. The build (src/installer/mkPackages.py) works out what each member needs from the modules its code imports, the libraries its BASIC program declares, and the manifest's uses (a module reached by name), and stops when DVX itself, or a part, would need a part it does not name. A member's files outside its folder are its paths: the DOS tools in SYSTEM go with the part they serve (BASCOMP.EXE with kpunch.basdev, DVXRES.EXE with kpunch.resedit, and DVXHLPC.EXE with kpunch.helpedit, the Help Editor); DVXPKG.EXE stays in DVX itself, to install and remove packages at the DOS prompt.

    -

    A module of yours that cannot work without a part's library names the library's package in PKG_NEEDS (PKG_NEEDS="kpunch.dvxnet=1.0.0"): Software then installs it only where that part is. One that can do without it does not import it: it asks dvxLibraryLoaded (BASIC: SysLibraryLoaded, system.bas) and reaches the library by name only when it is there, as Software does with pkgnet. BASIC's SQL functions do nothing without the Databases part (SQLOpen returns 0).

    +

    A module of yours that cannot work without a part's library names the library's package in PKG_NEEDS (PKG_NEEDS="kpunch.dvxnet=1.0.0"): Software then installs it only where that part is. One that can do without it does not import it: it asks dvxLibraryLoaded (BASIC: SysLibraryLoaded, system.bas), which loads the library when it is installed, and reaches the library by name only when it is there, as Software does with pkgnet. Not importing a library also keeps DVX from loading it before it is needed. BASIC's SQL functions do nothing without the Databases part (SQLOpen returns 0).

    Publishing on the Package Server

    Software's Available and Updates pages list the packages on the DVX package server. To publish one there, make an account on the server (POST /v1/account/register, then the link in the mail), give its email and password in Software's File > Package Server, and choose File > Publish a Package; Software's Published page shows each version's review. From the build machine, sign in (POST /v1/account/login gives a session token) and upload the package with it:

    curl -H "Authorization: Bearer TOKEN" -H "Content-Type: application/octet-stream" \
    @@ -10134,8 +10505,9 @@ void        listItemsFree(ListItemsT *li);
     const char *listItemsGet(const ListItemsT *li, int32_t idx);
     int32_t     listItemsMaxWidth(ListItemsT *li, const BitmapFontT *font);
     bool        listItemsRemove(ListItemsT *li, const BitmapFontT *font, int32_t idx);
    -bool        listItemsSetExternal(ListItemsT *li, const BitmapFontT *font, const char **items, int32_t count);
    -

    listItemsAdd and listItemsRemove first adopt (deep-copy) a caller's array, so they extend or shorten it without modifying it. listItemsSetExternal keeps the previously owned strings until the next adopt, clear, or free, because the new array may point into them, and never reads the caller's previous array (it may be freed already). The mutators measure in font, the widget's own (wgtFont), and return true when the widest item's width changed, i.e. when the widget's minimum width changed and it needs a relayout rather than just a repaint. listItemsGet returns "" for an index out of range.

    +bool listItemsSetExternal(ListItemsT *li, const BitmapFontT *font, const char **items, int32_t count); +bool listItemsSetSpec(ListItemsT *li, const BitmapFontT *font, const char *spec); +

    listItemsAdd and listItemsRemove first adopt (deep-copy) a caller's array, so they extend or shorten it without modifying it. listItemsSetExternal keeps the previously owned strings until the next adopt, clear, or free, because the new array may point into them, and never reads the caller's previous array (it may be freed already). The mutators measure in font, the widget's own (wgtFont), and return true when the widest item's width changed, i.e. when the widget's minimum width changed and it needs a relayout rather than just a repaint. listItemsGet returns "" for an index out of range. listItemsSetSpec replaces the items with the pieces of a "Red|Green|Blue" spec (LIST_ITEMS_SEP; every "|" separates, so "a|" is "a" and an empty item, and "" empties the list): BASIC's SetItems.

    widgetAdoptOwnedStrings(owned, src, count) builds the deep copy that adoption uses (complete before the old strings are freed, so src may alias them) and widgetFreeOwnedStrings(owned) frees every string of an owned array and empties it. ListView uses both for its row-major cell copy.

    ListSelT (selection model)

    Selection state shared by ListBox and ListView. cursor is the current (keyboard) item and anchor the start of Shift ranges; bits holds one byte per item in multi-select mode. Every index is a data index; functions that take a display index also take an optional display-to-data order map (NULL = identity) so a sorted view selects what the user sees as a contiguous range.

    @@ -10182,6 +10554,7 @@ void listReorderPaint(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t to listPopupInit(p) No selection, no hover, closed. listPopupAddItem / listPopupRemoveItem Item list edits; the selection and hover stay on the same item (-1 when it is removed). listPopupClearItems / listPopupSetItems Replace the items; an out-of-range selection becomes -1. + listPopupSetItemsSpec(w, p, spec) Replace the items with a "Red|Green|Blue" spec (listItemsSetSpec); nothing is selected. listPopupOpen(w, p) Open with the selection hovered and scrolled into view (keyboard, mouse, and accelerator all use it). listPopupClose(w, p) WGT_METHOD_CLOSE_POPUP body. listPopupCommit(p) Close and make the hovered item the selection; false when nothing valid is hovered. @@ -10404,7 +10777,7 @@ int32_t listIconScrollDrag(const ListIconGridT *g, int32_t y, int32_t grab);

    Music Library

    Music for DVX programs on whatever sound hardware the computer has: tracker modules (MOD, S3M, XM, IT), Standard MIDI Files (type 0 and 1), WAV and VOC sound files, and live MIDI a program sends itself. Every music belongs to the program that loaded it and is stopped and freed when the program ends, however it ends; any number of programs may play music at the same time.

    -

    Header: music/music.h. Loaded as: bin/libs/kpunch/music/music.lib (needs audio.lib); the General MIDI instrument bank for FM is bin/libs/kpunch/music/genmidi.op2, and the General MIDI SoundFont for wavetable music is an add-on (SHARED\BANKS\KPUNCH\DVXGM.SF2).

    +

    Header: music/music.h. Loaded as: bin/libs/kpunch/music/music.lib (needs audio.lib); the tracker module player (libxmp-lite) is bin/libs/kpunch/music/players/xmp.drv, which music.lib loads when a module is opened and lets go a minute after the last one closes; the General MIDI instrument bank for FM is bin/libs/kpunch/music/genmidi.op2, and the General MIDI SoundFont for wavetable music is an add-on (SHARED\BANKS\KPUNCH\DVXGM.SF2).

    Playing a File

    MusicT *tune = NULL;
     int32_t rc   = musicLoad("THEME.MID", &tune);    // or musicLoadMem(data, size, &tune)
    @@ -10437,7 +10810,7 @@ if (rc != MUSIC_OK) {
     

    When there is no FM chip (or it is busy), MIDI plays on DVX's own software FM synthesizer: an integer two-operator FM chip modelled on the OPL2 (the same log-sine and exponential tables, waveforms 0 to 3, feedback, the chip's envelope rates and key scaling, key scale level, tremolo, and vibrato) with 24 voices, rendering at the device's rate into a stream. It is driven with the same bank and the same register values as the chip, so music sounds alike on both. The host tests compare it with emu8950, an accurate OPL2 emulator, for several patches: pitch, loudness over time, and harmonics agree closely. Cost on a 486: roughly 1 to 2 percent of a 486DX2-66 per sounding voice at 22,050 Hz.

    SoundFonts on a Wavetable Synthesizer

    A wavetable synthesizer (audio.lib's gus and guswt drivers for a Gravis UltraSound, awe for the EMU8000 of a Sound Blaster AWE32 or AWE64) plays recorded instruments from its own memory. Its instruments come from a SoundFont, DVX.INI [audio] soundFont (MUSIC_INI_SOUND_FONT), named as midiBank names an OP2 bank; not set, the standard one (MUSIC_SOUND_FONT_DEFAULT, KPUNCH\DVXGM.SF2: DVX's General MIDI SoundFont add-on, made from FluidR3_GM by mksf, about 3.4 MB). SoundFont 2 files are read in full; SoundFont 1 (.SBK, the AWE32's own) as far as it is known (it has no public specification). soundFont may also name a Gravis UltraSound's ULTRASND.INI by its path, which plays that folder's .PAT patches as instruments; and where the standard SoundFont is not installed, the patches in the folder ULTRADIR names play (musicPatchSetPath), else the AWE's own bank for its sample ROM (musicRomBankPath: SFBANK\SYNTHGM.SF2 or SYNTHGM.SBK in the folder SOUND names, as Creative's drivers installed it). Control Panel's Sounds page lists all of these.

    -

    A song loads only the samples its notes play (the bank selects, programs, and notes are looked at first), each with the frames a chip's interpolation reads after it, into memory the synthesizer gives every program a share of; what a song loaded stays for the next while it fits, and room is made by giving up the samples used longest ago. When the samples still do not fit they are all loaded again smaller: 8-bit where the chip plays it, then at half the rate, then a quarter; at the least, the ones that do not fit are left out. Notes the bank lacks, or whose samples were left out, play on the FM chip or the software synthesizer, as do the notes beyond the synthesizer's voices. One music plays on the wavetable synthesizer at a time (another meanwhile plays on FM); its SoundFont and samples are kept 60 seconds after the last music (MUSIC_FONT_KEEP_SEC). Live MIDI loads a program's samples as it is chosen. A SoundFont made for an AWE's General MIDI ROM (ROM samples) plays from the card's ROM with nothing loaded; on any other synthesizer its notes go to FM.

    +

    A song loads only the samples its notes play (the bank selects, programs, and notes are looked at first), each with the frames a chip's interpolation reads after it, into memory the synthesizer gives every program a share of; what a song loaded stays for the next while it fits, and room is made by giving up the samples used longest ago. When the samples still do not fit (a size they cannot fit at, even in the whole of the synthesizer's memory, is not tried) they are all loaded again smaller: 8-bit where the chip plays it, then at half the rate, then a quarter; at the least, the ones that do not fit are left out. Notes the bank lacks, or whose samples were left out, play on the FM chip or the software synthesizer, as do the notes beyond the synthesizer's voices. One music plays on the wavetable synthesizer at a time (another meanwhile plays on FM); its samples are kept 60 seconds after the last music (MUSIC_FONT_KEEP_SEC), and the SoundFont itself (its headers, a GUS patch set's files) for the whole session, so a later song does not read them again. Live MIDI loads a program's samples once it is chosen, one at a time in the idle time after (a whole instrument at once would hold DVX up), at the best quality that fits beside the samples already loaded; until they are in, its notes play on the FM chip. A SoundFont made for an AWE's General MIDI ROM (ROM samples) plays from the card's ROM with nothing loaded; on any other synthesizer its notes go to FM.

    The AWE runs the SoundFont's envelopes, LFOs, filter, and reverb and chorus sends itself; on the GUS the library runs the envelopes and LFOs (its volume and pitch changed as the note goes on), without a filter or effects.

    The SoundFont Software Synthesizer

    With DVX.INI [audio] midi = softwave, MIDI plays from the SoundFont in software on any device that plays sampled sound: the song's samples are read into memory at their own rates and mixed into the music's stream (linear interpolation, the envelopes and LFOs as on the GUS, no filter or effects), with the software FM synthesizer beside it for the notes the bank lacks. It needs the memory for the song's samples (a General MIDI song of ten instruments and a drum kit about 360 KB of DVXGM's) and more CPU time than FM.

    @@ -10473,19 +10846,19 @@ musicMidiRecordFree(take);

    A MIDI recording keeps channel messages with their times (musicMidiRecordAdd(rec, ms, status, data1, data2), ms from the recording's start; a time before the last message's counts as the last's; MUSIC_ERR_ARG for anything but a channel message) and musicMidiRecordSave(rec, path) writes them as a Standard MIDI File: type 0, one track of 1,000 ticks a quarter note at 60 quarter notes a minute, so a tick is a millisecond. A note still held at the end is let go there, so the file never leaves a note sounding. MUSIC_ERR_WRITE when the file cannot be written (nothing is left of one not all written). musicMidiRecordCount and musicMidiRecordLength (the last message's time) say what it holds; musicMidiRecordFree frees it, and a program's recordings are freed when it ends. musicLoad plays the file. The music macros (below) are written into MIDI files by the same code.

    Modules

    MOD (ProTracker and compatibles), S3M, XM, and IT through libxmp-lite 4.7.3 (thirdparty/libxmp-lite with DVX's patches, listed in its DVX-VERSION.md; formats only, no depackers or ProWizard). Stereo separation is 50 percent (softer than the Amiga's hard left and right). The whole module is held in memory. libxmp works out pitches with floating point, so it plays modules in the shell's idle time, never at interrupt time; on a 486SX (no maths coprocessor) that goes through floating point emulation and is slow. On a stream libxmp mixes every channel in software (linear interpolation, three multiplies a channel a frame).

    -

    On a wavetable synthesizer (musicModHw.c) libxmp mixes nothing: DVX's hardware voice patch has it report each of its voices as every tick starts (the sample, where it is, its rate, volume, pan, and the loop it is in), and only move them on. Those ticks are made up to 400 ms ahead in the idle time, as the synthesizer's notes, and the music timer plays them on time (500 times a second: a note starts within 2 ms of its time). A voice starts on the chip where libxmp starts or moves it (a note, a sample offset, a retrigger, another sample or loop); otherwise its pitch, volume, and pan follow libxmp's each tick, and the chip's own interpolation plays it. The samples are stored as they are (an 8-bit one 8-bit where the chip plays those, as a GUS does), else all 8-bit. The chips loop only forward, so a ping-pong loop is stored unrolled: its way back after its end, each frame where libxmp reads it (an Impulse Tracker module's way back a frame shorter, as Impulse Tracker plays it). What the chips cannot do plays near enough: a ping-pong sustain loop goes forward, a sample played backwards with no loop goes forward, and Impulse Tracker's filters are not heard; a voice the chip has none free for (a program holds the rest) is not heard. A voice at full volume plays 8 dB down, with the chip's level (AudioWtInfoT) taken off too, so a module is about as loud as libxmp mixes it. A pause stops the voices, and the play goes on from the tick that was playing; musicGetPosition is the tick being heard.

    +

    On a wavetable synthesizer (musicModHw.c) libxmp mixes nothing: DVX's hardware voice patch has it report each of its voices as every tick starts (the sample, where it is, its rate, volume, pan, and the loop it is in), and only move them on. Those ticks are made up to 400 ms ahead in the idle time, as the synthesizer's notes, and the music timer plays them on time (500 times a second: a note starts within 2 ms of its time). A voice starts on the chip where libxmp starts or moves it (a note, a sample offset, a retrigger, another sample or loop); otherwise its pitch, volume, and pan follow libxmp's each tick, and the chip's own interpolation plays it. The samples are stored as they are (an 8-bit one 8-bit where the chip plays those, as a GUS does), else all 8-bit; a way of storing them that cannot fit in the synthesizer's free memory is not tried (nothing is uploaded for nothing), and a module that fits in neither plays mixed in software. A big module loads giving way to DVX's other tasks between its samples (a DVX patch to libxmp, 0002-load-hook), and a file of no music kind is refused by its first bytes, without being read whole. A WAV or VOC file plays as it is read. The chips loop only forward, so a ping-pong loop is stored unrolled: its way back after its end, each frame where libxmp reads it (an Impulse Tracker module's way back a frame shorter, as Impulse Tracker plays it). What the chips cannot do plays near enough: a ping-pong sustain loop goes forward, a sample played backwards with no loop goes forward, and Impulse Tracker's filters are not heard; a voice the chip has none free for (a program holds the rest) is not heard. A voice at full volume plays 8 dB down, with the chip's level (AudioWtInfoT) taken off too, so a module is about as loud as libxmp mixes it. A pause stops the voices, and the play goes on from the tick that was playing; musicGetPosition is the tick being heard.

    Measured on an emulated 486DX2-66 at 22,050 Hz, a 4-channel MOD with all four channels sounding takes about 49 percent of the CPU mixed in software and about 14 percent on a Gravis UltraSound's voices, most of that libxmp's own work each tick (about 1.3 ms of the 20).

    Music Macros

    MusicMmlT *tune = NULL;
     
     musicMmlOpen(&tune);
     musicMmlPlay(tune, "MB T140 O3 L8 CDEC CDEC EFG4 EFG4");
    -

    A macro queue plays tunes written in QBasic's music macro language (the commands are listed in music.h and in the BASIC language reference: notes A-G with sharps and flats, N, O, > and <, L, P, dots, T, MN / ML / MS, MF / MB). musicMmlPlay parses the whole string first; a mistake anywhere returns MUSIC_ERR_MACRO and queues nothing. The settings last from one string to the next. musicMmlTone queues a square-wave tone of milliHz thousandths of a hertz (0: silence) for us microseconds (QBasic's SOUND). musicMmlQueued counts the notes not yet played, musicMmlStop silences the queue, musicMmlBusy says whether a caller that follows QBasic should still wait (foreground mode: until everything has played; background: while more than MUSIC_MML_BACKGROUND_MAX notes wait) and moves the queue along. A queue belongs to the program that opened it and is freed when that program ends.

    -

    Timing is QBasic's: a whole note is four beats of 60 / T seconds, each dot adds half again, and a note sounds for 7/8 (MN), all (ML), or 3/4 (MS) of its length. Octave 3 starts at middle C; octave 3's A is 440 Hz. The notes play as square waves on an audio.lib note queue, each to the exact sample (on the PC speaker as tones). DVX's own I command (I1 to I128) plays the notes that follow on that General MIDI instrument: each run of such notes becomes a small MIDI file that plays like any MIDI music (on the synthesizer DVX.INI [audio] midi chooses); I0 returns to tones. Runs play one after the other; when the instruments cannot play (no instrument bank, no device) the run plays as tones.

    +

    A macro queue plays tunes written in QBasic's music macro language (the commands are listed in music.h and in the BASIC language reference: notes A-G with sharps and flats, N, O, > and <, L, P, dots, T, MN / ML / MS, MF / MB, and QB64's V (volume) and @ (waveform: 1 square, 3 triangle, 5 noise)). musicMmlPlay parses the whole string first; a mistake anywhere returns MUSIC_ERR_MACRO and queues nothing. The settings last from one string to the next. musicMmlTone queues a square-wave tone of milliHz thousandths of a hertz (0: silence) for us microseconds (QBasic's SOUND). musicMmlQueued counts the notes not yet played, musicMmlStop silences the queue, musicMmlBusy says whether a caller that follows QBasic should still wait (foreground mode: until everything has played; background: while more than MUSIC_MML_BACKGROUND_MAX notes wait) and moves the queue along. A queue belongs to the program that opened it and is freed when that program ends.

    +

    Timing is QBasic's: a whole note is four beats of 60 / T seconds, each dot adds half again, and a note sounds for 7/8 (MN), all (ML), or 3/4 (MS) of its length. Octave 3 starts at middle C; octave 3's A is 440 Hz. The notes play on an audio.lib note queue, each to the exact sample in its own waveform and volume (audioNotesSetTone; on the PC speaker as square tones, V0 silent); V also scales an instrument's velocity. DVX's own I command (I1 to I128) plays the notes that follow on that General MIDI instrument: each run of such notes becomes a small MIDI file that plays like any MIDI music (on the synthesizer DVX.INI [audio] midi chooses); I0 returns to tones. Runs play one after the other; when the instruments cannot play (no instrument bank, no device) the run plays as tones.

    Several Programs at Once

    Every music plays through one audio context of the library's own; audio.lib mixes all streams, so modules and software MIDI of any number of programs play together. FM voices are shared with every program's tones and FM music; the MPU-401 port serves one music, and so does the wavetable synthesizer (MIDI or a module). When a program ends, its musics stop and are freed and their FM voices and the port are given back; the others go on.

    BASIC

    -

    src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName) and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums), and MusicMidiSend(music, midiMsg) (musicMidiSend of a message packed as MidiInMessage gives it, AUDIO_MIDI_PACKED_*). MIDI recordings: MidiRecordNew (a recording number, 0 when all 8 are in use), MidiRecordAdd(recording, milliseconds, midiMsg), MidiRecordCount, MidiRecordLength, MidiRecordSave(recording, fileName) (True / False with MusicError$), MidiRecordFree. A number only works for the program that got it. MusicLoadMacro(macro) (musicLoadMacro, a music number) and MusicSetBank(music, bank) (musicSetBank, "" the standard bank, a SoundFont or an ULTRASND.INI for wavetable music; MusicError$ says when it is missing or damaged); MusicPatchSet$ and MusicRomBank$ (musicPatchSetPath, musicRomBankPath: "" when there is none). MusicGetVolume(music) (musicGetVolume in percent) and MusicInfo$(music, field) (musicGetInfo: "title", "output", "kind" = "module" / "midi" / "live" / "sound" for MusicKindE, "where" = "" / "stream" / "mpu401" / "fm" / "software" / "wave" / "softwave" for MusicOutputE, "paused" and "looping" as "-1" / "0"; any case, "" for another field). music.bas also declares the raw MIDI port calls of audio.lib (MidiPortInfo$, MidiAcquire, MidiPut, MidiRelease) and its MIDI input (MidiInOpen, MidiInClose, MidiInMessage, MidiInTime, MidiInClock, MidiInLost, and the MIDI_* CONSTs); see the sound library.

    +

    src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName), MusicLoadData(bytes AS BINARY STRING) (musicLoadMem), and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicEnded (True once after the music played to its end: each number's musicSetEndFn sets a flag that MusicPlay and MusicEnded clear), MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums), and MusicMidiSend(music, midiMsg) (musicMidiSend of a message packed as MidiInMessage gives it, AUDIO_MIDI_PACKED_*). MIDI recordings: MidiRecordNew (a recording number, 0 when all 8 are in use), MidiRecordAdd(recording, milliseconds, midiMsg), MidiRecordCount, MidiRecordLength, MidiRecordSave(recording, fileName) (True / False with MusicError$), MidiRecordFree. A number only works for the program that got it. MusicLoadMacro(macro) (musicLoadMacro, a music number) and MusicSetBank(music, bank) (musicSetBank, "" the standard bank, a SoundFont or an ULTRASND.INI for wavetable music; MusicError$ says when it is missing or damaged); MusicPatchSet$ and MusicRomBank$ (musicPatchSetPath, musicRomBankPath: "" when there is none). MusicGetVolume(music) (musicGetVolume in percent) and MusicInfo$(music, field) (musicGetInfo: "title", "output", "kind" = "module" / "midi" / "live" / "sound" for MusicKindE, "where" = "" / "stream" / "mpu401" / "fm" / "software" / "wave" / "softwave" for MusicOutputE, "paused" and "looping" as "-1" / "0"; any case, "" for another field). music.bas also declares the raw MIDI port calls of audio.lib (MidiPortInfo$, MidiAcquire, MidiPut, MidiRelease) and its MIDI input (MidiInOpen, MidiInClose, MidiInMessage, MidiInTime, MidiInClock, MidiInLost, and the MIDI_* CONSTs); see the sound library.

    The BASIC runtime itself calls musicBasBeep, musicBasSound(hz, ticks), musicBasPlay(macro), musicBasPlayCount, and musicBasBusy for the statements BEEP, SOUND, and PLAY and the PLAY(n) function; each BASIC program has its own macro queue. QBasic's X command and "=" numbers (given with VARPTR$) are filled in by the BASIC runtime before musicBasPlay is called, so the macro language itself has neither. SOUND's duration is in clock ticks (18.2 a second) and 0 stops the queue; BEEP is 800 Hz for a quarter second.

    Licences

    libxmp-lite (Extended Module Player Lite), (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr, MIT licence:

    @@ -10500,7 +10873,7 @@ musicMmlPlay(tune, "MB T140 O3 L8 CDEC CDEC EFG4 EFG4");

    The client of a DVX package server: what it offers (its catalogue), what it has that is newer than the packages installed (DVX itself among them), and fetching packages, with everything they need that is not installed, into a download folder; and, with an account on the server, publishing packages and listing what the account has sent. There the package library reads them (pkgOffersInFolder) and plans and installs them like any package file, so a package from the server is checked and trusted exactly as one from a disk. Software uses it for its Available and Updates pages.

    Header: pkgnet/pkgNet.h

    Loaded as: bin/libs/kpunch/pkgnet/pkgnet.lib (it needs the package, HTTP, and JSON libraries)

    -

    BASIC: pkgnet.bas (PkgNetOpen, PkgNetCatalogue, PkgNetUpdates, PkgNetFetch, PkgNetRunFor, PkgNetRows$, ...), with pkg.bas's PkgOfferFolder to plan what was fetched.

    +

    BASIC: pkgnet.bas (PkgNetOpen, PkgNetOpenIn for another DVX folder or download folder, PkgNetCatalogue, PkgNetUpdates, PkgNetFetch, PkgNetRunFor, PkgNetRows$, PkgNetField$ -- PKG_FIELD_SHA256 and PKG_FIELD_REPO_SIGNED give a catalogue entry's sha256 and repoSigned --, ...), with pkg.bas's PkgOfferFolderStart to read what was fetched for planning.

    A Fetch

    PkgNetT *n = pkgNetCreate(server, "", downloadDir);
     const char *ids[] = { "acme.chess" };
    @@ -10544,7 +10917,7 @@ pkgNetFree(n);

    Package Library

    Add-on packages: the .DPK file format, manifests, Ed25519 keys and signatures, the packages installed in a DVX folder, the keys DVX trusts, planning what an install or removal takes, and installing and removing a package a step at a time. Software, Package Builder, the IDE's File > Make Package, DVXPKG.EXE, the build machine's dvxpkg, and Setup all use it.

    Header: pkg/pkg.h (it includes the others below)

    -

    Loaded as: bin/libs/kpunch/pkg/pkg.lib

    +

    Loaded as: bin/libs/kpunch/pkg/pkg.lib, and bin/libs/kpunch/pkg/writer/writer.drv when a package is made (below)

    BASIC: pkg.bas (sessions, offers, plans, install and remove jobs, building, signing, and keys; PkgFileOwner$ is pkgDbOwner).

    A Package

    A package is NAME.DPK: a ZIP read front to back, whose first entry is PACKAGE.INI, the manifest; then PACKAGE.SIG when the publisher signed it; then the files, named relative to the DVX folder with / between parts.

    @@ -10602,7 +10975,8 @@ APPS/ACME/CHESS/CHESS.APP = 61234 <SHA-256, 64 hex digits> pkgDpkBuildFiles(&templ, files, count, out, secret, err, size) The same from a list of PkgDpkFileT (the name in the package, the file it comes from). pkgDpkFolderFiles(folder, prefix, &files) Every file under folder as that list, named prefix/...; pkgDpkFilesFree frees it. pkgDpkReadInfo(path, &info, err, size) The manifest and PACKAGE.SIG, checked; pkgDpkInfoFree. - pkgDpkCheckFiles(path, &info, err, size) Every file's size and SHA-256 against the manifest. + pkgDpkCheckFiles(path, &info, err, size) Every file's size and SHA-256 against the manifest, all at once. + pkgDpkCheckStart(path, &info, err, size) The same as a job: pkgDpkJobStep a piece at a time, pkgDpkJobPercent by bytes read (BASIC's PkgVerifyStart). pkgDpkSign(path, &secret, detached, err, size) Sign into the package, or beside it as NAME.SIG. pkgDpkSigner(&info, detached, keys, count) Which of keys made the signature (or the detached one).
    PkgManifestT templ;
    @@ -10616,7 +10990,21 @@ pkgSecretWipe(&secret);
     pkgManifestFree(&templ);

    Installed Packages and Trust (pkgDb.h, pkgTrust.h)

    Each installed package has a record, SYSTEM/PACKAGES/VENDOR/NAME.INI: its manifest as installed, whether it was chosen or only needed by another, and the key that signed it. pkgDbLoad reads them all; pkgDbFind, pkgDbOwner (who has a file), pkgDbPut, and pkgDbDrop keep them. Setup writes DVX's own record (kpunch.dvx, with no files).

    -

    pkgKeyringLoad reads the keys DVX trusts: SYSTEM/KEYS (DVX's own) and the user's KEYS folder (publishers the user chose); pkgKeyringAdd trusts another. pkgTrustOf says how far a package is trusted (PkgTrustSystemE, PkgTrustUserE, PkgTrustNoneE). The rules the planner keeps: kpunch.* packages must be signed by a SYSTEM key; an upgrade must be signed by the key that signed the installed version (or a SYSTEM key); an unsigned package from a file is installed only after a warning.

    +

    pkgKeyringLoad reads the keys DVX trusts: SYSTEM/KEYS (DVX's own) and the user's KEYS folder (publishers the user chose); pkgKeyringAdd trusts another, and pkgKeyringRemove(ring, userDir, idText, err, size) stops trusting one of the user's (its files in KEYS are deleted; a SYSTEM key stays). pkgTrustOf says how far a package is trusted (PkgTrustSystemE, PkgTrustUserE, PkgTrustNoneE). The rules the planner keeps: kpunch.* packages must be signed by a SYSTEM key; an upgrade must be signed by the key that signed the installed version (or a SYSTEM key); an unsigned package from a file is installed only after a warning.

    +

    ZIP Files (pkgZip.h)

    +

    The reader goes front to back through a ZIP from a read callback (pkgZipFileRead over a FILE *), never seeking; each entry's data goes, inflated, to a write callback in steps, and its size and CRC-32 are checked at its end. pkgZipProblemText says why a step failed, as the end of a sentence. The writer makes a ZIP the reader takes: pkgZipCreate, pkgZipBeginFile or pkgZipBeginMem, pkgZipStep until each is in, pkgZipFinish (pkgZipAbort removes a half-made one). pkgZipDosTime and pkgZipDosStamp turn ZIP dates into times and back.

    +

    A plain ZIP job (PkgZipJobT, pkgZipJob.c) does a whole archive a step at a time, for a program that keeps drawing:

    +
      Function                                                       Description
    +  --------                                                       -----------
    +  pkgZipJobRead(path, dir, err, size)                            Read a ZIP to its end, every entry checked; with dir, unpack each under it (folders made, files dated as in the ZIP; a name with a drive, a leading '/', or ".." fails it).
    +  pkgZipJobCreate(path, names, sources, count, err, size)        Make a ZIP of count files: names[i] in the ZIP from the file sources[i].
    +  pkgZipJobStep(job, maxWork)                                    PkgZipJobMoreE, PkgZipJobDoneE, or PkgZipJobFailedE (pkgZipJobMessage says why).
    +  pkgZipJobEntries(job, &count)                                  The entries read or written so far.
    +  pkgZipJobCurrent, pkgZipJobPercent                             The entry it is on; how far it is (bytes of the ZIP read, or of the files added).
    +  pkgZipJobFree(job)                                             Stop and free it: a ZIP being made, or a file being unpacked, is removed.
    +

    pkgHexEncode and pkgHexDecode (pkgFormat.h) turn bytes into hex digits and back, as manifests, keys, and the package server write SHA-256s and key ids.

    +

    The Package Writer Module

    +

    Writing ZIPs (miniz's deflate) and making keys and signatures (Ed25519 signing) are used only when a package is made, so in DVX they are a module of their own, writer.drv in LIBS\KPUNCH\PKG\WRITER, which pkg.lib loads the first time a program makes a ZIP, a key, or a signature (pkgZipCreate, pkgKeyMake, or pkgSign, directly or through pkgKeyWrite, the .DPK and ZIP jobs, and BASIC's PkgBuildStart, PkgSignStart, PkgKeygen, PkgSignData, and PkgZipCreateStart); it stays until pkg.lib is unloaded. Nothing changes for a program: the calls keep their names (pkgWriterMod.c passes them on; pkgWriter.h has the module's descriptor). Without the module (a damaged installation) pkgZipCreate gives NULL and a key or signature made is all zeros, which nothing accepts; DVX.LOG says the writer is missing. Reading, checking, and installing packages never load it. Programs that link the library in whole (DVXPKG.EXE, the build machine's dvxpkg, the tests) compile pkgZipWrite.c and pkgSignMake.c instead of the module.

    Planning (pkgPlan.h)

    An offer (PkgOfferT) is a package that can be installed: pkgOfferRead reads one file, pkgOffersFor reads a file and every package in its folder (what it may need), pkgOffersInFolder a whole folder. pkgPlanInstall(db, offers, count, wanted, wantedCount, &plan) plans installing the offers wanted with everything they need, in the order they go in, each checked (the DVX and ABI it needs, files another package has, a newer version installed, the signing rules); plan.err says why when it fails, plan.bytes is what it unpacks, and plan.stale lists the add-ons an update of DVX itself would leave unable to load. pkgPlanDependents lists the installed packages that need some going (they go too); pkgPlanOrphans those only they needed.

    Installing and Removing (pkgOp.h)

    @@ -10639,7 +11027,7 @@ dvxpkg list [-r DVXDIR]

    Printing Library

    Printer ports and print jobs, text laid out on pages, pictures for dot-matrix and laser printers, the printer settings, and the common Print and Page Setup dialogs. Notepad, Image Viewer, Icon Editor, the Control Panel's Printer page, and BASIC's LPRINT all print through it; any program can.

    Header: print/print.h

    -

    Loaded as: bin/libs/kpunch/print/print.lib

    +

    Loaded as: bin/libs/kpunch/print/print.lib; the PostScript printers' driver, with their text faces and vector pages, is bin/libs/kpunch/print/drivers/ps.drv, which the library loads the first time a PostScript printer is used (until then printVecBegin is false for every document).

    Settings

    The printer is chosen once for everything, in DVX.INI [printer] (the Control Panel's Printer page and the Page Setup dialog edit it): where output goes (PrintDestE: LPT1 to LPT3, COM1 to COM4, or a file), a serial port's speed and handshake (baud, PrintHandshakeE), the printer model (PrintModelE: text only; Epson 9-pin or 24-pin, each with a black or a color ribbon; HP LaserJet at 150 dpi; HP LaserJet III or DeskJet at 300 dpi; HP DeskJet 500C; HP DeskJet 550C and later; PostScript, black or color; Apple ImageWriter II, black or color), and the page setup (PrintPageSetupT).

      Function                                   Description
    @@ -10854,174 +11242,138 @@ bool  richRtfRead(RichTextT *rt, const char *text, int32_t len);
    const char *TextToRtf(const char *text);

    RtfToText$ is the text of an RTF document, a line per paragraph (text that is not RTF comes back as it is); TextToRtf$ is plain text as RTF in the system font, a paragraph per line. A result stays valid until the next call. The RichEdit control's TextRTF, SelRTF, LoadFile, and SaveFile cover RTF in a control.

    -
    -

    Serial Stack

    -

    The DVX serial/networking stack provides reliable, optionally encrypted communication over RS-232 serial ports. It is composed of four layers, each building on the one below:

    - -

    Loaded as: bin/libs/kpunch/serial/serial.lib

    -

    Layered Architecture

    -
    +--------------------------------------------------+
    -|  Application                                     |
    -|                                                  |
    -|  secLinkSend()  -- send on channel, opt. encrypt |
    -|  secLinkPoll()  -- receive + dispatch            |
    -+--------------------------------------------------+
    -|  secLink  (handshake, channels, cipher + MAC)    |
    -+--------------------------------------------------+
    -|  packet   (HDLC framing, CRC-16, Go-Back-N ARQ)  |
    -+--------------------------------------------------+
    -|  security -- DH, XTEA-CTR, SHA-256/HMAC, DRBG    |
    -|            (used by secLink during handshake)    |
    -+--------------------------------------------------+
    -|  rs232    (ISR-driven UART, 2048-byte rings)     |
    -+--------------------------------------------------+
    -|  UART hardware (8250 / 16450 / 16550 / 16550A)   |
    -+--------------------------------------------------+
    -

    The security layer is invoked by secLink during the Diffie-Hellman handshake and for per-packet cipher operations. It has no runtime coupling to rs232 or packet, so it can be used standalone for cryptographic operations independent of the serial stack.

    -

    Layer 1: RS-232 UART Driver

    -

    Layer 2: Packet Transport

    -

    Layer 3: Security (DH, XTEA, SHA-256)

    -

    Layer 4: Secure Link

    -

    End-to-End Example

    -
    -
    -

    RS-232 UART Driver

    -

    ISR-driven serial port driver supporting up to 4 simultaneous COM ports. A shared ISR drains UART FIFOs into per-port ring buffers (2048 bytes, power-of-2 for fast index wrapping). Flow control (XON/XOFF, RTS/CTS, DTR/DSR) operates within the ISR using watermark thresholds. All ISR data structures are DPMI-locked to prevent page faults. XON/XOFF bytes are queued for the transmit interrupt instead of being written over a byte still in the transmit register, and every cached register (IER, MCR) is changed with interrupts off so the ISR and the application never lose each other's updates.

    -

    Header: rs232/rs232.h

    -

    Port Constants

    -
      Constant          Value   Description
    -  --------          -----   -----------
    -  RS232_COM1        0       First serial port.
    -  RS232_COM2        1       Second serial port.
    -  RS232_COM3        2       Third serial port.
    -  RS232_COM4        3       Fourth serial port.
    -  RS232_NUM_PORTS   4       Total number of supported COM ports.
    -

    Handshake Modes

    + -
    +

    Packet Transport

    Reliable, ordered packet delivery over an RS-232 serial link. Uses HDLC-style byte-stuffed framing for frame delimiting, CRC-16-CCITT for error detection, and Go-Back-N ARQ with a configurable sliding window for retransmission.

    -

    Header: packet/packet.h

    +

    Header: seclink/packet.h

    Constants

      Constant            Value   Description
       --------            -----   -----------
    @@ -11057,19 +11409,21 @@ const char *TextToRtf(const char *text);
    callbackCtx User pointer passed to the callback.

    Returns an opaque PktConnT handle, or NULL on failure (invalid port, duplicate open, or allocation failure).

    pktOpenStream

    -

    Open a packetized connection over any byte stream instead of a COM port -- a TCP socket, for example. The stream is described by two non-blocking calls and a context pointer, copied into the connection. Any number of stream connections may be open.

    +

    Open a packetized connection over any byte stream instead of a COM port -- a TCP socket, for example. The stream is described by non-blocking calls and a context pointer, copied into the connection. Any number of stream connections may be open.

    typedef struct {
         int32_t (*read)(void *ctx, uint8_t *buf, int32_t len);
         int32_t (*write)(void *ctx, const uint8_t *data, int32_t len);
         void     *ctx;
    +    int32_t (*pending)(void *ctx);
     } PktStreamT;
     
     PktConnT *pktOpenStream(const PktStreamT *stream, int windowSize, PktRecvCallbackT callback, void *callbackCtx);
      Member   Description
       ------   -----------
       read     Up to len bytes into buf: the count, 0 when nothing has arrived, negative when the stream is gone (pktPoll then reports PKT_ERR_DISCONNECTED).
    -  write    Bytes taken from data (0 = none right now; a frame is retried until it is all out or nothing was taken for a while), negative when the stream is gone.
    -  ctx      Passed to both.
    + write Bytes taken from data (0 = none right now), negative when the stream is gone. Frames wait in the connection's transmit queue, and each pktPoll hands the stream what it takes; a frame that finds no room in the queue is dropped (a data frame is sent again by its retransmit timer). + ctx Passed to the calls. + pending Bytes taken but not yet on the wire (NULL: the stream does not say). Retransmit timers wait while there are any and their count keeps moving: a count that stays the same for 2 seconds (flow control holding the line) no longer holds them.

    Returns the handle, or NULL for a NULL stream or call, or on allocation failure. pktClose never closes the stream.

    pktClose

    Close a packetized connection and release its registry slot. Does not close the underlying COM port. The caller is responsible for calling rs232Close separately when no longer needed. May be called from inside the receive callback: the connection is then freed when the outermost pktPoll returns (which reports PKT_ERR_DISCONNECTED).

    @@ -11109,236 +11463,13 @@ void pktSetResetCallback(PktConnT *conn, PktResetCallbackT callback);int pktWaitSpace(PktConnT *conn);

    Returns PKT_SUCCESS, PKT_ERR_INVALID_PARAM, PKT_ERR_DISCONNECTED, or PKT_ERR_TIMEOUT after PKT_SEND_TIMEOUT_MS without progress.

    -
    -

    Security (DH, XTEA, SHA-256)

    -

    Cryptographic primitives for the serial stack: 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA block cipher in CTR mode, SHA-256, HMAC-SHA256, and HKDF-SHA256, and a SHA-256 based random number generator.

    -

    XTEA requires no lookup tables and compiles to approximately 20 instructions per round, making it suitable for 486-class hardware where AES S-box tables would thrash the 8KB cache. SHA-256 needs only 32-bit arithmetic and a 256-byte constant table.

    -

    Headers: security/security.h, security/secHash.h

    -

    Constants

    -
      Constant               Value   Description
    -  --------               -----   -----------
    -  SEC_DH_KEY_SIZE        128     Size of a DH public key or shared secret in bytes (1024 bits).
    -  SEC_XTEA_KEY_SIZE      16      Size of an XTEA key in bytes (128 bits).
    -  SEC_RNG_SEED_SIZE      32      Bytes stored in a seed file.
    -  SEC_SHA256_SIZE        32      SHA-256 digest size.
    -  SEC_SHA256_BLOCK_SIZE  64      SHA-256 block size.
    -  SEC_HKDF_MAX_OUT       8160    Largest HKDF output (255 digests).
    -

    Error Codes

    -
      Constant           Value   Description
    -  --------           -----   -----------
    -  SEC_SUCCESS        0       Operation succeeded.
    -  SEC_ERR_PARAM      -1      Invalid parameter.
    -  SEC_ERR_NOT_READY  -2      DH context not ready (keys not generated or secret not computed).
    -  SEC_ERR_IO         -3      Seed file could not be read or written.
    -

    RNG Functions

    -

    A SHA-256 hash DRBG over a 32-byte entropy pool. secRngAddEntropy hashes new bytes into the pool and never discards what is already there, so it is safe to call at any time, before or after the first output. Each output block is a hash of the pool and a counter; after every request the pool is re-hashed so earlier output cannot be recomputed from a later state.

    -

    The first secRngBytes call gathers hardware entropy automatically. On DOS that is 256 PIT samples, each taken after a busy wait sized by the previous sample, plus the full time stamp counter on CPUs that have one (Pentium and later) and the BIOS tick count. On a CPU without a time stamp counter the samples depend mostly on the PIT phase and time of day, which an observer can narrow down, so applications should feed user-input timings to secRngAddEntropy and keep a seed file: call secRngLoadSeed at startup and secRngSaveSeed at shutdown. Host builds read /dev/urandom.

    -

    Under the DVX shell this is automatic when the serial library is installed: the shell loads the seed file SECLINK.RND in the user folder at startup, feeds the timing of every key press and mouse event to secRngAddEntropy, and saves a fresh seed at shutdown. Applications only need their own calls when they run without the shell.

    -
    void    secRngAddEntropy(const uint8_t *data, int32_t len);
    -void    secRngBytes(uint8_t *buf, int32_t len);
    -int32_t secRngGatherEntropy(uint8_t *buf, int32_t len);
    -int     secRngLoadSeed(const char *path);
    -int     secRngSaveSeed(const char *path);
    -void    secSecureZero(void *ptr, int len);
    -
      Function              Description
    -  --------              -----------
    -  secRngAddEntropy      Hash bytes into the pool (key timings, mouse positions, anything unpredictable).
    -  secRngBytes           Generate pseudorandom bytes.
    -  secRngGatherEntropy   Fill buf with condensed hardware entropy (one fresh gather per 32 bytes). Returns the byte count (0 if a host build cannot read its random device).
    -  secRngLoadSeed        Mix a seed file into the pool, then immediately rewrite it with fresh output so a seed is never used twice. SEC_SUCCESS or SEC_ERR_IO.
    -  secRngSaveSeed        Write SEC_RNG_SEED_SIZE bytes of fresh output for the next run. SEC_SUCCESS or SEC_ERR_IO.
    -  secSecureZero         Zero memory in a way the compiler cannot optimize away (for key material).
    -

    Hash Functions

    -
    void secSha256(const uint8_t *data, int32_t len, uint8_t digest[32]);
    -void secSha256Init(SecSha256T *ctx);
    -void secSha256Update(SecSha256T *ctx, const uint8_t *data, int32_t len);
    -void secSha256Final(SecSha256T *ctx, uint8_t digest[32]);
    -void secHmacSha256(const uint8_t *key, int32_t keyLen, const uint8_t *data, int32_t len, uint8_t mac[32]);
    -void secHmacInit(SecHmacT *h, const uint8_t *key, int32_t keyLen);
    -void secHmacUpdate(SecHmacT *h, const uint8_t *data, int32_t len);
    -void secHmacFinal(SecHmacT *h, uint8_t mac[32]);
    -int  secHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, uint8_t *okm, int32_t okmLen);
    -bool secConstTimeEqual(const void *a, const void *b, int32_t len);
    -

    SHA-256 follows FIPS 180-4, HMAC RFC 2104, and HKDF RFC 5869 (extract and expand; a NULL or empty salt means 32 zero bytes). secHkdf returns SEC_ERR_PARAM for okmLen outside 1..SEC_HKDF_MAX_OUT. secConstTimeEqual compares without revealing where the inputs differ; use it for every MAC check.

    -

    Diffie-Hellman Key Exchange

    -

    1024-bit DH with 256-bit private exponents using the RFC 2409 Group 2 safe prime. The modular exponentiation is a Montgomery ladder with branch-free operand swaps, so its timing does not depend on the private key.

    -
    SecDhT *secDhCreate(void);
    -int     secDhGenerateKeys(SecDhT *dh);
    -int     secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
    -int     secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len);
    -int     secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len);
    -void    secDhDestroy(SecDhT *dh);
    -
      Function            Description
    -  --------            -----------
    -  secDhCreate         Allocate a new DH context.
    -  secDhGenerateKeys   Generate a private exponent and public key from the DRBG.
    -  secDhGetPublicKey   Copy the local public key into buf. *len receives the key size.
    -  secDhComputeSecret  Compute the shared secret from the remote public key. Keys outside [2, p-2] are rejected with SEC_ERR_PARAM (0, 1, and p-1 would force a trivially known secret).
    -  secDhGetSecret      Copy the shared secret (SEC_DH_KEY_SIZE big-endian bytes) into buf. It is input for a key derivation function such as secHkdf, never a key by itself.
    -  secDhDestroy        Securely zero and free the DH context.
    -

    Typical usage order: secDhCreate, secDhGenerateKeys, exchange public keys, secDhComputeSecret, secDhGetSecret, secHkdf, secDhDestroy.

    -

    XTEA-CTR Cipher

    -

    XTEA in counter mode. Encrypt and decrypt are the same operation (XOR with keystream). The counter must never repeat with the same key.

    -
    SecCipherT *secCipherCreate(const uint8_t *key);
    -void        secCipherCrypt(SecCipherT *c, uint8_t *data, int len);
    -void        secCipherSetCounter(SecCipherT *c, uint64_t counter);
    -void        secCipherDestroy(SecCipherT *c);
    -
      Function             Description
    -  --------             -----------
    -  secCipherCreate      Allocate a cipher context with a 128-bit key. Returns NULL on allocation failure or NULL key. Initial counter is zero.
    -  secCipherCrypt       Encrypt or decrypt data in place. The 64-bit counter increments by one every 8 bytes (one XTEA block).
    -  secCipherSetCounter  Position the 64-bit block counter (secLink derives it from the frame sequence number).
    -  secCipherDestroy     Securely zero the cipher context (key and counter state) and free its memory.
    -

    secCipherCreate returns NULL if the key pointer is NULL or allocation fails. secCipherCrypt is a no-op when any of c/data is NULL or len <= 0.

    -

    BASIC

    -

    src/include/basic/security.bas (DECLARE LIBRARY "serial") declares the BASIC functions (security/secBas.c). Keys, data, and results are BINARY STRING (pointer and length in, a result buffer and length out), so every byte value passes; the result buffer is wiped before it is reused. A handle (1, 2, ...; 0 = none) is a key exchange, a cipher, or an incremental SHA-256 / HMAC; each records the program that made it and the shell's app-cleanup hook frees (and wipes) what a program leaves open.

    -
      BASIC                                          C it calls
    -  -----                                          ----------
    -  SecRandom$(count), SecAddEntropy bytes         secRngBytes; secRngAddEntropy.
    -  SecDhCreate()                                  secDhCreate + secDhGenerateKeys.
    -  SecDhPublicKey$(h), SecDhSecret$(h)            secDhGetPublicKey; secDhGetSecret ("" before SecDhComputeSecret).
    -  SecDhComputeSecret(h, remotePublicKey)         secDhComputeSecret: SEC_OK or a SEC_ERR_ code.
    -  SecDhFree h                                    secDhDestroy.
    -  SecCipherCreate(key)                           secCipherCreate; 0 unless key is SEC_XTEA_KEY_SIZE bytes.
    -  SecCipherCrypt$(h, bytes)                      secCipherCrypt on a copy.
    -  SecCipherSetCounter h, counter                 secCipherSetCounter (a DOUBLE: exact up to 2^53).
    -  SecCipherFree h                                secCipherDestroy.
    -  SecSha256$(bytes); SecSha256Open, SecSha256Add, SecSha256End$  secSha256; secSha256Init / Update / Final (End frees the handle).
    -  SecHmac$(key, bytes); SecHmacOpen, SecHmacAdd, SecHmacEnd$  secHmacSha256; secHmacInit / Update / Final.
    -  SecHkdf$(salt, secret, info, length)           secHkdf ("" for a length outside 1..SEC_HKDF_MAX_OUT).
    -  SecEqual(a, b)                                 secConstTimeEqual (False for different lengths).
    -

    The SEC_ CONSTs of security.bas are compared with security.h and secHash.h by a formrt test, which also calls every function for real.

    -
    - -
    +

    End-to-End Example

    Below is a complete secure BBS client that opens COM1, runs the authenticated key exchange, and then shuttles terminal traffic over a single encrypted channel. It demonstrates the typical serial stack usage: load the RNG seed file, open the link, set the passphrase, run the handshake, then enter a poll+send loop.

    -
    #include "rs232/rs232.h"
    -#include "packet/packet.h"
    -#include "security/security.h"
    -#include "seclink/secLink.h"
    +
    #include "rs232.h"
    +#include "packet.h"
    +#include "security.h"
    +#include "secLink.h"
     #include <stdio.h>
     #include <string.h>
     
    @@ -11399,11 +11530,177 @@ int main(void) {
     

    Notes:

    • secLinkOpen internally calls rs232Open and pktOpen. secLinkClose tears them down in reverse order.
    • -
    • The DRBG gathers hardware entropy on first use. On a CPU without a time stamp counter that alone is guessable; the seed file and user-input timings (secRngAddEntropy) are what make the DH keys unpredictable.
    • +
    • The DRBG gathers hardware entropy on first use. On a CPU without a time stamp counter that alone is guessable; the seed file and user-input timings (secRngAddEntropy) are what make the X25519 keys unpredictable.
    • secLinkPoll must be called frequently to drain the RX ring buffer, process ACKs, and dispatch received packets to the callback.
    • -
    • For bulk transfers larger than SECLINK_MAX_PAYLOAD (245 bytes), use secLinkSendBuf which splits the data into chunks automatically.
    • +
    • For bulk transfers larger than SECLINK_MAX_PAYLOAD (237 bytes), use secLinkSendBuf which splits the data into chunks automatically.
    +
    +

    Serial Stack

    +

    DVX's serial communications come in layers, in three libraries, each building on the one below:

    +
      +
    • rs232 (the serial library, serial.lib) -- ISR-driven UART driver with ring buffers and flow control
    • +
    • packet (the secure link library, seclink.lib) -- HDLC framing, CRC-16, Go-Back-N ARQ (reliable delivery)
    • +
    • security (the cryptography library, crypto.lib) -- X25519 key exchange, XChaCha20-Poly1305, SHA-256/HMAC/HKDF, SHA-256 DRBG (and Ed25519, Diffie-Hellman, and XTEA-CTR for other uses)
    • +
    • secLink (the secure link library, seclink.lib) -- authenticated handshake, channel multiplexing, per-packet encryption and MAC
    • +
    +

    The serial and cryptography libraries are part of every DVX; the secure link library comes with Communications.

    +

    Loaded as: bin/libs/kpunch/serial/serial.lib (rs232)

    +

    Layered Architecture

    +
    +--------------------------------------------------+
    +|  Application                                     |
    +|                                                  |
    +|  secLinkSend()  -- send on channel, opt. encrypt |
    +|  secLinkPoll()  -- receive + dispatch            |
    ++--------------------------------------------------+
    +|  secLink  (handshake, channels, sealed frames)   |
    ++--------------------------------------------------+
    +|  packet   (HDLC framing, CRC-16, Go-Back-N ARQ)  |
    ++--------------------------------------------------+
    +|  security -- X25519, XChaCha20-Poly1305, DRBG    |
    +|            (used by secLink during handshake)    |
    ++--------------------------------------------------+
    +|  rs232    (ISR-driven UART, 2048-byte rings)     |
    ++--------------------------------------------------+
    +|  UART hardware (8250 / 16450 / 16550 / 16550A)   |
    ++--------------------------------------------------+
    +

    The security layer is invoked by secLink during the X25519 handshake and to seal and open every encrypted packet. It has no runtime coupling to rs232 or packet, so it can be used standalone for cryptographic operations independent of the serial stack.

    +

    Layer 1: RS-232 UART Driver

    +

    Layer 2: Packet Transport

    +

    Layer 3: Cryptography Library

    +

    Layer 4: Secure Link Library

    +

    End-to-End Example

    +
    +
    +

    RS-232 UART Driver

    +

    ISR-driven serial port driver supporting up to 4 simultaneous COM ports. A shared ISR drains UART FIFOs into per-port ring buffers (2048 bytes, power-of-2 for fast index wrapping). An interrupt the interrupt controller misses (its edge lost, seen under a flood in DOSBox, which then stops delivering received bytes) cannot stall a port: rs232Read, rs232WriteBuf, and rs232GetTxBuffered find the UART still holding an interrupt up and turn its interrupt enable off and on, which raises a fresh one, so a port that is polled at all never stays stuck. Flow control (XON/XOFF, RTS/CTS, DTR/DSR) operates within the ISR using watermark thresholds. All ISR data structures are DPMI-locked to prevent page faults. XON/XOFF bytes are queued for the transmit interrupt instead of being written over a byte still in the transmit register, and every cached register (IER, MCR) is changed with interrupts off so the ISR and the application never lose each other's updates.

    +

    Header: serial/rs232.h

    +

    Port Constants

    +
      Constant          Value   Description
    +  --------          -----   -----------
    +  RS232_COM1        0       First serial port.
    +  RS232_COM2        1       Second serial port.
    +  RS232_COM3        2       Third serial port.
    +  RS232_COM4        3       Fourth serial port.
    +  RS232_NUM_PORTS   4       Total number of supported COM ports.
    +

    Handshake Modes

    +
      Constant                  Value   Description
    +  --------                  -----   -----------
    +  RS232_HANDSHAKE_NONE      0       No flow control.
    +  RS232_HANDSHAKE_XONXOFF   1       Software flow control (XON/XOFF characters).
    +  RS232_HANDSHAKE_RTSCTS    2       Hardware flow control via RTS/CTS lines.
    +  RS232_HANDSHAKE_DTRDSR    3       Hardware flow control via DTR/DSR lines.
    +

    UART Types

    +

    Detected automatically by probing the scratch register and FIFO capability.

    +
      Constant            Value   Description
    +  --------            -----   -----------
    +  RS232_UART_UNKNOWN  0       Detection failed.
    +  RS232_UART_8250     1       Original IBM PC UART. No FIFO.
    +  RS232_UART_16450    2       Has scratch register, no FIFO.
    +  RS232_UART_16550    3       Has FIFO but buggy (rare).
    +  RS232_UART_16550A   4       Working 16-byte FIFO. Most common in 486+ hardware.
    +

    Error Codes

    +
      Constant                     Value   Description
    +  --------                     -----   -----------
    +  RS232_SUCCESS                0       Operation succeeded.
    +  RS232_ERR_UNKNOWN            -1      Unknown error.
    +  RS232_ERR_NOT_OPEN           -2      Port is not open.
    +  RS232_ERR_ALREADY_OPEN       -3      Port is already open.
    +  RS232_ERR_NO_UART            -4      No UART detected at port address.
    +  RS232_ERR_INVALID_PORT       -5      Invalid port number.
    +  RS232_ERR_INVALID_BASE       -6      Invalid base I/O address.
    +  RS232_ERR_INVALID_IRQ        -7      Invalid IRQ number.
    +  RS232_ERR_INVALID_BPS        -8      Invalid baud rate.
    +  RS232_ERR_INVALID_DATA       -9      Invalid data bits value.
    +  RS232_ERR_INVALID_PARITY     -10     Invalid parity character.
    +  RS232_ERR_INVALID_STOP       -11     Invalid stop bits value.
    +  RS232_ERR_INVALID_HANDSHAKE  -12     Invalid handshake mode.
    +  RS232_ERR_INVALID_FIFO       -13     Invalid FIFO threshold.
    +  RS232_ERR_NULL_PTR           -14     NULL pointer argument.
    +  RS232_ERR_IRQ_NOT_FOUND      -15     Could not detect IRQ for port.
    +  RS232_ERR_LOCK_MEM           -16     DPMI memory lock failed.
    +  RS232_ERR_NO_MEMORY          -17     No memory for the port's buffers.
    +

    rs232Open

    +

    Open a COM port with the specified line parameters.

    +
    int32_t rs232Open(int32_t com, int32_t bps, int32_t dataBits, char parity, int32_t stopBits, int32_t handshake);
    +
      Parameter   Description
    +  ---------   -----------
    +  com         Port index (RS232_COM1..RS232_COM4).
    +  bps         Baud rate (e.g. 9600, 19200, 38400, 57600, 115200).
    +  dataBits    Data bits per character (5, 6, 7, or 8).
    +  parity      Parity mode: 'N' (none), 'O' (odd), 'E' (even), 'M' (mark), 'S' (space).
    +  stopBits    Stop bits (1 or 2).
    +  handshake   Flow control mode (RS232_HANDSHAKE_*).
    +

    Returns RS232_SUCCESS or a negative error code. The UART type is probed once here (RS232_ERR_NO_UART when nothing answers at the port address); only a 16550A gets its FIFO enabled and is fed 16 bytes per transmit interrupt, every other UART one byte.

    +

    rs232Close

    +

    Close a COM port and release its ISR resources. The modem control lines are left as they are: closing never drops DTR or RTS, so a modem stays connected and the next program to open the port can carry on. To hang up, call rs232SetDtr(com, false) before closing; the line stays down after the close. Opening a port raises DTR and RTS.

    +
    int32_t rs232Close(int32_t com);
    +

    rs232Read

    +

    Non-blocking read from the receive ring buffer.

    +
    int32_t rs232Read(int32_t com, char *data, int32_t len);
    +
      Parameter   Description
    +  ---------   -----------
    +  com         Port index.
    +  data        Buffer to receive data.
    +  len         Maximum bytes to read.
    +

    Returns the number of bytes actually read (may be less than len).

    +

    rs232Write

    +

    Blocking polled write directly to UART transmit holding register. Bypasses the TX ring buffer. Polls LSR_TX_HOLD_EMPTY before writing each byte (the check and the write happen with interrupts off, so a flow-control byte from the ISR is never overwritten), so it takes as long as the bytes take on the wire. Use for small, immediate writes where order matters; the packet layer uses rs232WriteBuf instead, so it never waits.

    +
    int32_t rs232Write(int32_t com, const char *data, int32_t len);
    +

    Returns the number of bytes actually written (may be less than len while flow control holds the line), or a negative error code (RS232_ERR_INVALID_PORT, RS232_ERR_NOT_OPEN, RS232_ERR_NULL_PTR).

    +

    rs232WriteBuf

    +

    Non-blocking write to the transmit ring buffer. The ISR drains buffered data to the UART asynchronously, through the transmit-hold-empty interrupt.

    +
    int32_t rs232WriteBuf(int32_t com, const char *data, int32_t len);
    +

    Returns the number of bytes actually queued (may be less than len if the buffer is full).

    +

    rs232Set

    +

    Change all line parameters on an open port. Internally calls rs232SetBps, rs232SetData, rs232SetParity, rs232SetStop, and rs232SetHandshake in sequence. Returns RS232_SUCCESS on success or the first negative error code encountered.

    +
    int32_t rs232Set(int32_t com, int32_t bps, int32_t dataBits, char parity, int32_t stopBits, int32_t handshake);
    +

    Configuration Getters

    +
      Function                   Returns
    +  --------                   -------
    +  rs232GetBase(com)          I/O base address.
    +  rs232GetBps(com)           Current baud rate (int32_t).
    +  rs232GetData(com)          Data bits setting (5-8).
    +  rs232GetParity(com)        Parity character: 'n', 'o', 'e', 'm', or 's' (lowercase).
    +  rs232GetStop(com)          Stop bits setting (1 or 2).
    +  rs232GetHandshake(com)     Handshake mode (RS232_HANDSHAKE_*).
    +  rs232GetIrq(com)           IRQ number.
    +  rs232GetUartType(com)      UART type probed when the port was opened (RS232_UART_*).
    +

    rs232GetParity returns a lowercase character on success. Callers comparing against uppercase constants must use either both cases or tolower().

    +

    Status Getters

    +
      Function                   Returns
    +  --------                   -------
    +  rs232GetRxBuffered(com)    Bytes waiting in the receive ring buffer.
    +  rs232GetTxBuffered(com)    Bytes waiting in the transmit ring buffer.
    +  rs232GetCts(com)           CTS line state (0 or 1).
    +  rs232GetDcd(com)           DCD (carrier detect) line state (0 or 1), as last reported by the modem status interrupt.
    +  rs232GetDsr(com)           DSR line state (0 or 1).
    +  rs232GetDtr(com)           DTR line state (0 or 1).
    +  rs232GetRts(com)           RTS line state (0 or 1).
    +  rs232GetLsr(com)           Line status register value as last read; RS232_LSR_OVERRUN, RS232_LSR_PARITY_ERR, RS232_LSR_FRAME_ERR, and RS232_LSR_BREAK are its error bits.
    +  rs232GetMcr(com)           Modem control register value.
    +  rs232GetMsr(com)           Modem status register value; RS232_MSR_RI is the ring indicator.
    +

    Configuration Setters

    +
      Function                          Description
    +  --------                          -----------
    +  rs232SetBase(com, base)           Set I/O base address (before open).
    +  rs232SetBps(com, bps)             Change baud rate.
    +  rs232SetData(com, dataBits)       Change data bits.
    +  rs232SetParity(com, parity)       Change parity mode.
    +  rs232SetStop(com, stopBits)       Change stop bits.
    +  rs232SetHandshake(com, handshake) Change flow control mode. Releases any throttle the old mode applied (raises RTS/DTR or sends XON) and starts the transmit side from the live CTS/DSR line.
    +  rs232SetIrq(com, irq)             Move an open port to another IRQ (for a wrong auto-detection). Interrupt enables and DTR/RTS are kept.
    +  rs232SetMcr(com, mcr)             Set modem control register directly.
    +  rs232SetDtr(com, dtr)             Set DTR line state. The state is kept when the port is closed (dropping DTR is how a modem is hung up).
    +  rs232SetBreak(com, on)            Hold the transmit line in the break (spacing) state (on = true) or release it. The caller times the break, typically 250-500 ms.
    +  rs232SetRts(com, rts)             Set RTS line state. The state is kept when the port is closed.
    +  rs232SetFifoThreshold(com, thr)   Set FIFO trigger level (1, 4, 8, or 14 bytes) or 0 to turn the FIFO off. Levels other than 0 return RS232_ERR_INVALID_FIFO unless the UART is a 16550A. rs232Open sets 8 on a 16550A (the 8 free bytes allow 0.7 ms of interrupt latency at 115200 bps; 14 allows only 0.17 ms) and 0 on other UARTs.
    +

    Buffer Management

    +
      Function                   Description
    +  --------                   -----------
    +  rs232ClearRxBuffer(com)    Discard all data in the receive ring buffer.
    +  rs232ClearTxBuffer(com)    Discard all data in the transmit ring buffer.
    +

    Task Manager

    System-level task manager for DVX. Displays all running applications with a six-column ListView (Name, Title, File, Type, Memory, Status) and exposes Switch To, End Task, and Run... operations. Always accessible via Ctrl+Esc regardless of which application is focused. Persists independently of the desktop app (Program Manager).

    @@ -11734,6 +12031,7 @@ if (xferPoll(x, nowMs()) != XferRunningE) { const XferProgressT *xferProgress(x) Progress and outcome. bool xferDetectFeed(d, byte) Feed received terminal bytes; true when a ZMODEM sender's start frame (ZRQINIT) has arrived, so a terminal can start a ZMODEM receive by itself. void xferSafeName(name, longNames, out) Reduce a sender's file name to a safe one: no path, no drive, no DOS device name (CON, PRN, COM1, and so on get a leading _). With longNames (the receiving disk takes long names, platformLongNames) a long name keeping its case, blanks, and dots, where a character a long name cannot hold becomes _ and leading blanks and trailing dots and blanks go (RECEIVED when nothing is left); otherwise an 8.3 name of portable characters in upper case. out holds XFER_NAME_MAX bytes (PLATFORM_LONG_NAME_MAX + 1). + int32_t xferSplitList(list, paths) Split a list of files to send, one path per line (LF or CR LF, empty lines skipped), in place into paths[XFER_MAX_FILES]; the count. ConnSendFiles and XferSendFiles use it. uint16_t xferCrc16(data, len) CRC-16/XMODEM (polynomial 0x1021). uint32_t xferCrc32(data, len) CRC-32 (IEEE 802.3), as ZMODEM uses it. int32_t xferBasNew(&x) For the BASIC functions of other libraries: a new xfer.bas handle owning a new idle transfer (in x) that belongs to the running program; 0 when no handle is free. The library starts the transfer over its own transport; the program follows it with the Xfer functions (the connection library's ConnSendFiles and ConnReceiveFiles work this way). @@ -11760,7 +12058,9 @@ if (xferPoll(x, nowMs()) != XferRunningE) {
      Function                             Description
       --------                             -----------
       XferSendFile(com, proto, path)       Send one file over COM port com (1-4). Returns a handle, or 0.
    -  XferReceive(com, proto, target)      Receive: XMODEM into the file target; YMODEM and ZMODEM into the folder target. Returns a handle, or 0.
    +  XferSendFiles(com, proto, files, escapeCtl, resumeFiles)  Send a batch, one path per line (xferSplitList), with the ZMODEM options ConnSendFiles takes. Returns a handle, or 0.
    +  XferReceive(com, proto, target)      Receive: XMODEM into the file target; YMODEM and ZMODEM into the folder target; a file already there is renamed. Returns a handle, or 0.
    +  XferReceiveFiles(com, proto, target, exists, escapeCtl)  Receive with an XFER_EXIST_ action for files already there (XFER_EXIST_ASK: XFER_ASKING and XferAnswer) and ZMODEM control escaping.
       XferPoll(handle)                     Move the transfer along. Returns XFER_RUNNING, XFER_ASKING, XFER_DONE, XFER_FAILED, or XFER_CANCELLED.
       XferStatus(handle)                   The same, without moving it along (XFER_IDLE for a bad handle).
       XferSending(handle), XferProtocol(handle)  True for a send; the protocol.
    @@ -11880,13 +12180,14 @@ wgtCallMethod(list, "ListCount", NULL, 0, &count);   // count.i ==
     

    WCLASS_FIXED_FONT: it edits by columns, so its font must be fixed-pitch. Any size works (the column math uses the font's charWidth and charHeight); a proportional font set with wgtSetFont, or inherited, is replaced by the system font when it draws.

    The widget renders through two paint paths: a full paint used by the normal widget pipeline, and a fast incremental repaint (wgtAnsiTermRepaint) that pushes dirty rows directly to the window content buffer for low-latency serial echo.

    Header: widgets/ansiTerm.h

    +

    Loaded as: bin/widgets/kpunch/ansiterm/ansiterm.wgt, part of Communications. Without it there is no Terminal control and dvxAnsiTermApi() is NULL, so a program that may run without Communications checks that before it makes a terminal.

    Creation

    WidgetT *term = wgtAnsiTerm(parent, 80, 25);

    API Functions

      Function                                                      Description
       --------                                                      -----------
       WidgetT *wgtAnsiTerm(parent, cols, rows)                      Create an ANSI terminal widget with the given column/row dimensions.
    -  void wgtAnsiTermWrite(w, data, len)                           Write raw bytes into the terminal's ANSI parser. data is a const uint8_t * buffer.
    +  void wgtAnsiTermWrite(w, data, len)                           Write raw bytes into the terminal's ANSI parser. data is a const uint8_t * buffer. Up to 4 KB is parsed at once; the rest of a longer text waits in order and is parsed 4 KB per poll (a frame each), so a long text does not hold DVX up. Clear, a resize, and a new scrollback length parse what waits first.
       void wgtAnsiTermClear(w)                                      Clear the terminal screen and reset the cursor to the home position.
       void wgtAnsiTermSetComm(w, ctx, readFn, writeFn)              Attach a communication channel. readFn and writeFn are I/O callbacks; ctx is passed as their first argument.
       void wgtAnsiTermSetScrollback(w, maxLines)                    Set the maximum number of scrollback lines.
    @@ -11924,17 +12225,18 @@ wgtCallMethod(list, "ListCount", NULL, 0, &count);   // count.i ==
       ------       -----------
       Clear        Clear the terminal screen.
       Copy         Copy the selection (copySelection); returns False when nothing is selected.
    -  Paste        Send the clipboard to the comm write callback as Ctrl+V does (new lines as CR, 64-byte chunks).
    +  Paste        Send the clipboard to the comm write callback as Ctrl+V does (new lines as CR, 64-byte chunks); with no write callback, to the Send event in one piece.
       Poll         Process any pending bytes on the attached communication channel.
       ResetColors  The standard CGA colors again (setPalette NULL).
       SetColor     index, color: one attribute color (0-15) from a BASIC color (&HBBGGRR, converted with wgtBasicColor).
    -  Write        Write a string into the terminal (with ANSI escape processing).
    + Write Write a string into the terminal (with ANSI escape processing). Dynamic (WGT_SIG_DYNAMIC) so every byte of the BASIC string arrives, NUL bytes included (wgtValueLen); the parser ignores a NUL as padding.

    Events

    -

    Besides the common events, two of its own, fired through wgtFireEvent (a C host gets them in onEvent with the names ANSITERM_EVT_MUSIC and ANSITERM_EVT_BELL):

    +

    Besides the common events, three of its own, fired through wgtFireEvent (a C host gets them in onEvent with the names ANSITERM_EVT_MUSIC, ANSITERM_EVT_BELL, and ANSITERM_EVT_SEND):

      Event   Parameters          Description
       -----   ----------          -----------
       Music   Macro As String     An ANSI music string arrived: the BASIC PLAY text between ESC[| or ESC[N, or ESC[MF (MB, MN, ML, MS), and Ctrl+N (0x0E). For the ESC[M form the macro begins with that M command (e.g. "MFT120O4L8CDE"). Line ends are left out; a string longer than 4096 characters, an empty one, or one cut short by ESC fires nothing.
    -  Bell    (none)              A BEL character (Ctrl+G) arrived.
    + Bell (none) A BEL character (Ctrl+G) arrived. + Send Bytes As String Bytes for the host while no comm write callback is set (wgtAnsiTermSetComm): typed keys, a paste, and the replies to ESC[c and ESC[6n / ESC[255n. The argument is wgtValueBytes (read its length with wgtValueLen: it may hold NUL bytes). A reply waits until the byte that asked for it is parsed; keys typed while a Music, Bell, or Send handler runs go when it returns.

    The widget plays nothing itself: a handler plays the macro (BASIC: PLAY Macro; C: the music library's musicMmlPlay). While a Music or Bell handler runs, the terminal takes no input: Poll leaves the bytes in the connection, and Write keeps them, in order, until the handler returns. A tune played in the foreground (MF) therefore holds back the text after it, as on a DOS terminal, while the window stays responsive. Bare ESC[M (no parameter) followed by anything but F, B, N, L, or S is still Delete Line. A handler may close the terminal's window; the input after the event is then dropped.

    Keyboard Shortcuts

    Within the terminal widget:

    @@ -12041,6 +12343,7 @@ btn->onClick = onOkClicked;
    void wgtCanvasSetMouseCallback(w, cb) Set a mouse callback. Signature: void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag). cx/cy are canvas-relative, drag=true on mouse-move during press. int32_t wgtCanvasSave(w, path) Save the canvas buffer to an image file: BMP when path ends in .bmp, PNG otherwise. Returns 0 on success, -1 on error. int32_t wgtCanvasLoad(w, path) Load an image file into the canvas (resizes buffer to match). Returns 0 on success, -1 on error. + int32_t wgtCanvasLoadData(w, data, len, name) Load the picture in len bytes of an image file (as dvxLoadImageFromMemory reads them) as wgtCanvasLoad loads a file; Picture then reads name (NULL: ""). Returns 0 on success, -1 when they are not a picture. void wgtCanvasResize(w, newW, newH) Resize the canvas buffer and relayout. The old content is discarded; the new buffer is white. void wgtCanvasSetBorderStyle(w, style) CANVAS_BORDER_NONE (0) drops the sunken frame, any other value (CANVAS_BORDER_FIXED, 1, the default) shows it. The buffer keeps its size; the widget relays out. int32_t wgtCanvasGetBorderStyle(w) CANVAS_BORDER_FIXED or CANVAS_BORDER_NONE. @@ -12067,14 +12370,16 @@ btn->onClick = onOkClicked;
    setBorderStyle wgtCanvasSetBorderStyle getBorderStyle wgtCanvasGetBorderStyle getDisplay wgtCanvasGetDisplay - drawTextScaled wgtCanvasDrawTextScaled + drawTextScaled wgtCanvasDrawTextScaled + loadData wgtCanvasLoadData

    Properties

    Drawing state (pen color, pen size) is managed by the API functions above. The BASIC interface registers these properties:

      Property      Type      Access      Description
       --------      ----      ------      -----------
       BorderStyle   Enum      Read/Write  None (0) or FixedSingle (1, the default): wgtCanvasSetBorderStyle / wgtCanvasGetBorderStyle.
       ImageHeight   Integer   Read-only   Height of the canvas buffer in pixels (0 when there is no buffer).
    -  ImageWidth    Integer   Read-only   Width of the canvas buffer in pixels (0 when there is no buffer).
    + ImageWidth Integer Read-only Width of the canvas buffer in pixels (0 when there is no buffer). + Picture String Read/Write The file wgtCanvasLoad loaded, or the name wgtCanvasLoadData was given ("" when none; Clear and Resize wipe it). Setting a path loads it; setting "" clears the canvas to white. In BASIC it reads as a Picture object (see LOADPICTURE).

    Methods (BASIC Interface)

      Method                                        Description
       ------                                        -----------
    @@ -12090,6 +12395,7 @@ btn->onClick = onOkClicked;
    Save(path$) Save the canvas: BMP when path$ ends in .bmp, PNG otherwise. Returns 0 on success, -1 on error (WGT_SIG_RET_INT_STR). SetPenColor color% Set the drawing color (&HBBGGRR). SetPenSize size% Set the pen size in pixels (1 to 255). + SetPictureData(bytes$[, name$]) WGT_METHOD_SET_PICTURE_DATA (WGT_SIG_DYNAMIC): wgtCanvasLoadData(bytes, length, name). Returns True when the bytes are a picture. SetPixel x%, y%, color% Set a single pixel.

    Events

      Callback                                          Description
    @@ -12188,7 +12494,8 @@ wgtComboBoxAddItem(cb, "Times");
    Clear Remove all items and clear the selection. List(index%) Return the text of the item at the given index. ListCount() Return the total number of items. - RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection (the text is kept). + RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection (the text is kept). + SetItems items$ Replace all the items at once from a "|"-separated list ("Red|Green|Blue"; every "|" separates two items, "" empties the list). Nothing is selected afterwards. The text is kept.

    Events

      Callback     Description
       --------     -----------
    @@ -12198,8 +12505,8 @@ wgtComboBoxAddItem(cb, "Times");

    DataCtrl

    -

    A VB3-style Data control for database binding. Displays a visible navigation bar (|< < > >|) that connects to a SQLite database via dvxSql* functions. Reads all rows from the RecordSource query into an in-memory cache for bidirectional navigation. Fires a Reposition event each time the current row changes so bound controls can update. Supports master-detail linking between Data controls (via MasterSource / MasterField / DetailField), INSERT via AddNew, and DELETE via Delete. UPDATE and DELETE identify the row by the KeyColumn value it had when it was loaded, so editing the key column changes the key instead of writing to another row; an UPDATE writes only the changed columns and fails (the row stays unsaved) when no database row matches. A key the quoted text match cannot find (an integer in a column without a declared type, a REAL, or NULL) is matched by its text form instead, as long as that names exactly one row. Setting a field to the value it already has is not an edit. With a read-only source, pending edits are dropped when the cursor moves. Refresh saves a pending edit first; if that save fails or is cancelled the edit is discarded. With a SELECT RecordSource the control is read-only: Update and Delete do nothing, and a master-detail filter is applied to the query as a subquery.

    -

    The widget depends on the "dvxsql" library DXE; the navigation logic caches the full result set so there is no round-trip for MoveNext/MovePrev.

    +

    A VB3-style Data control for database binding. Displays a visible navigation bar (|< < > >|) that connects to a SQLite database via dvxSql* functions. Reads the RecordSource query's rows into an in-memory cache for bidirectional navigation: a Refresh reads the first 64, and the widget's polls read the rest, about 20 ms a frame, so a big record set does not hold DVX up. Fires a Reposition event each time the current row changes so bound controls can update. Supports master-detail linking between Data controls (via MasterSource / MasterField / DetailField), INSERT via AddNew, and DELETE via Delete. UPDATE and DELETE identify the row by the KeyColumn value it had when it was loaded, so editing the key column changes the key instead of writing to another row; an UPDATE writes only the changed columns and fails (the row stays unsaved) when no database row matches. A key the quoted text match cannot find (an integer in a column without a declared type, a REAL, or NULL) is matched by its text form instead, as long as that names exactly one row. Setting a field to the value it already has is not an edit. With a read-only source, pending edits are dropped when the cursor moves. Refresh saves a pending edit first; if that save fails or is cancelled the edit is discarded. With a SELECT RecordSource the control is read-only: Update and Delete do nothing, and a master-detail filter is applied to the query as a subquery.

    +

    The widget depends on the "dvxsql" library DXE; the navigation logic caches the result set so there is no round-trip for MoveNext/MovePrev. Until every row is read, the query stays open on a connection of its own, which holds the database's read lock: a write to that database from elsewhere (another program, or SQLExec) fails with "database is locked" until then. What needs the rows does not wait for the polls: MoveNext past the rows read and dataCtrlSetCurrentRow beyond them read on as far as they need, and MoveLast, AddNew, Update, and Delete read the rest first (the control's own writes would be locked out by its query too). The row count grows as rows arrive; a copy that runs out of memory part way frees the cache whole, as a failed Refresh does, and Reposition then fires so bound controls clear.

    Header: widgets/dataCtrl.h

    Creation

    WidgetT *data = wgtDataCtrl(parent);
    @@ -12212,20 +12519,20 @@ wgtComboBoxAddItem(cb, "Times"); void dataCtrlMoveFirst(w) Move the cursor to the first row. void dataCtrlMovePrev(w) Move the cursor to the previous row. void dataCtrlMoveNext(w) Move the cursor to the next row. - void dataCtrlMoveLast(w) Move the cursor to the last row. + void dataCtrlMoveLast(w) Move the cursor to the last row (the record set is read to its end first). const char *dataCtrlGetField(w, colName) Get the value of a column in the current row (case-insensitive). void dataCtrlSetField(w, colName, value) Set the value of a column in the current row. Marks the row dirty unless the value is unchanged. void dataCtrlUpdate(w) Flush pending changes (INSERT or UPDATE) to the database. void dataCtrlAddNew(w) Append a blank row and move the cursor to it. The row is dirty and new until Update is called. void dataCtrlDelete(w) Delete the current row from the cache and the database. void dataCtrlSetMasterValue(w, val) Set the master-detail filter value. Normally managed by the form runtime. - int32_t dataCtrlGetRowCount(w) Get the total number of cached rows. + int32_t dataCtrlGetRowCount(w) Get the number of rows read so far (all of them once the polls have read the rest, or after MoveLast). int32_t dataCtrlGetColCount(w) Get the number of columns in the result set. const char *dataCtrlGetColName(w, col) Get the name of a column by index. const char *dataCtrlGetCellText(w, row, col) Get the text of a specific cell. void dataCtrlSetCurrentRow(w, row) Set the current row by index. Auto-saves the previous row if dirty. int32_t dataCtrlGetCurrentRow(w) Get the current row index (-1 if there are no rows). - uint32_t dataCtrlGetGeneration(w) A counter that changes whenever the cached rows or cell values change (Refresh, AddNew, Delete, SetField) but not on cursor moves. Bound views use it to rebuild derived state only when needed. + uint32_t dataCtrlGetGeneration(w) A counter that changes whenever the cached rows or cell values change (Refresh, AddNew, Delete, SetField) but not on cursor moves, nor when more rows of the record set arrive (only the row count grows then). Bound views use it to rebuild derived state only when needed.

    API Struct (wgtRegisterApi "data")

      Slot                 Function
       ----                 --------
    @@ -12260,15 +12567,15 @@ wgtComboBoxAddItem(cb, "Times");
    MasterField String Read/Write Column in the master control to read for the filter value. DetailField String Read/Write Column in this table to filter by the master value. Caption String Read/Write Text displayed on the navigator bar. - BOF Boolean Read-only True when the cursor is at the first row (or no rows). - EOF Boolean Read-only True when the cursor is past the last row (or no rows). + BOF Boolean Read-only True after a MovePrevious at the first row (the cursor stays there), or when there are no rows. A move onto a row clears it. + EOF Boolean Read-only True after a MoveNext at the last row (the cursor stays there), or when there are no rows. A move onto a row clears it.

    Methods (BASIC Interface)

      Method         Description
       ------         -----------
       AddNew         Append a blank row and move to it.
       Delete         Delete the current row.
       MoveFirst      Move the cursor to the first row.
    -  MoveLast       Move the cursor to the last row.
    +  MoveLast       Move the cursor to the last row (all rows are read first).
       MoveNext       Move the cursor to the next row.
       MovePrevious   Move the cursor to the previous row.
       Refresh        Save a pending edit, then re-execute the query and rebuild the cache.
    @@ -12282,7 +12589,7 @@ wgtComboBoxAddItem(cb, "Times");

    DbGrid

    -

    A database grid widget that displays all records from a Data control in a scrollable, sortable table. Columns auto-populate from the Data control's column names and can be hidden, resized (by dragging a column border), and renamed by the application. Clicking a column header sorts the display ascending/descending. The highlighted row is always the Data control's current row: selecting a row moves the Data control's cursor, and the highlight is read from the cursor whenever the grid paints. The Data control does not repaint the grid itself, so a C application that moves the cursor elsewhere (the Data control's own buttons or API) calls dbGridRefresh (or wgtInvalidatePaint on the grid) from the Data control's onChange; the form runtime does this for BASIC programs. The grid reads directly from the Data control's cached rows, so there is no separate copy of the data. Re-binding the same Data control (as the form runtime does on every Reposition) keeps the sort order and column settings while the column set is unchanged (the sort order and automatic widths are rebuilt when the Data control's rows or values changed), and a width set by dragging a column border is kept like one set with dbGridSetColumnWidth. Cell and header text is clipped to its column.

    +

    A database grid widget that displays all records from a Data control in a scrollable, sortable table. Columns auto-populate from the Data control's column names and can be hidden, resized (by dragging a column border), and renamed by the application. Clicking a column header sorts the display ascending/descending. The highlighted row is always the Data control's current row: selecting a row moves the Data control's cursor, and the highlight is read from the cursor whenever the grid paints. The grid's polls follow the Data control's rows: a change to them, and the rows of a record set that arrive a part at a time (sorted in as they come, in the order a full sort gives), are shown without a call. A cursor move is not: a C application that moves the cursor elsewhere (the Data control's own buttons or API) calls dbGridRefresh (or wgtInvalidatePaint on the grid) from the Data control's onChange; the form runtime does this for BASIC programs. The grid reads directly from the Data control's cached rows, so there is no separate copy of the data. Re-binding the same Data control (as the form runtime does on every Reposition) keeps the sort order and column settings while the column set is unchanged (the sort order and automatic widths are rebuilt when the Data control's rows or values changed), and a width set by dragging a column border is kept like one set with dbGridSetColumnWidth. An automatic column width fits the header and the first 200 rows. Cell and header text is clipped to its column.

    The grid supports alternating row shading, vertical and horizontal scrollbars, keyboard navigation (arrows, Page Up/Down, Home, End), and double-click activation (fires onDblClick). The mouse wheel scrolls the view without moving the record cursor.

    Header: widgets/dbGrid.h

    Creation

    @@ -12374,7 +12681,8 @@ wgtDropdownAddItem(dd, "Blue"); Clear Remove all items. List(index%) Return the text of the item at the given index. ListCount() Return the total number of items. - RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection. + RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection. + SetItems items$ Replace all the items at once from a "|"-separated list ("Red|Green|Blue"; every "|" separates two items, "" empties the list). Nothing is selected afterwards.

    Events

      Callback     Description
       --------     -----------
    @@ -12428,7 +12736,8 @@ btn->onClick = onSaveClicked;
    WidgetT *wgtImageButton(parent, data, w, h, pitch) Create from raw pixel data (in display's native format). WidgetT *wgtImageButtonFromFile(parent, path) Create by loading an image file. void wgtImageButtonSetData(w, data, imgW, imgH, pitch) Replace the image with new raw pixel data. - void wgtImageButtonLoadFile(w, path) Replace the image by loading a new file. + void wgtImageButtonLoadFile(w, path) Replace the image by loading a new file. + bool wgtImageButtonLoadData(w, data, len, name) Replace the image with the picture in len bytes of an image file (as dvxLoadImageFromMemory reads them); Picture then reads name (NULL: ""). False (the image unchanged) when they are not a picture.

    API Struct (wgtRegisterApi "imagebutton")

      Slot         Function
       ----         --------
    @@ -12436,15 +12745,19 @@ btn->onClick = onSaveClicked;
    create wgtImageButton fromFile wgtImageButtonFromFile setData wgtImageButtonSetData - loadFile wgtImageButtonLoadFile + loadFile wgtImageButtonLoadFile + loadData wgtImageButtonLoadData

    Properties (BASIC Interface)

      Property      Type       Access       Description
       --------      ----       ------       -----------
    -  Picture       String     Read/Write   Setting writes a file path (loads image). Reading returns the path of the loaded file (also set by wgtImageButtonFromFile); in BASIC the path comes as a Picture object, which stands for the path where a value is needed (see LOADPICTURE); wgtImageButtonSetData clears it. Setting "" clears the image and the path; a file that cannot be loaded leaves both unchanged.
    +  Picture       String     Read/Write   Setting writes a file path (loads image). Reading returns the path of the loaded file (also set by wgtImageButtonFromFile), or the name wgtImageButtonLoadData was given; in BASIC the path comes as a Picture object, which stands for the path where a value is needed (see LOADPICTURE); wgtImageButtonSetData clears it. Setting "" clears the image and the path; a file that cannot be loaded leaves both unchanged.
       ImageWidth    Integer    Read-only    Width of the currently loaded image in pixels.
       ImageHeight   Integer    Read-only    Height of the currently loaded image in pixels.

    Methods

    -

    No widget-specific BASIC methods. Setting Picture loads a new image.

    +
      Method                           Description
    +  ------                           -----------
    +  SetPictureData(bytes$[, name$])  WGT_METHOD_SET_PICTURE_DATA (WGT_SIG_DYNAMIC): wgtImageButtonLoadData(bytes, length, name). Returns True when the bytes are a picture.
    +

    Setting Picture loads a new image; the BASIC form runtime shows a LOADPICTUREDATA picture through SetPictureData.

    Events

      Callback     Description
       --------     -----------
    @@ -12467,6 +12780,7 @@ btn->onClick = onSaveClicked;
    WidgetT *wgtImageFromFile(parent, path) Create by loading an image file. void wgtImageSetData(w, data, imgW, imgH, pitch) Replace the displayed image with new raw pixel data. void wgtImageLoadFile(w, path) Replace by loading a new file. Static but exposed via the API struct. + bool wgtImageLoadData(w, data, len, name) Replace with the picture in len bytes of an image file (as dvxLoadImageFromMemory reads them); Picture then reads name (NULL: ""). False (the image unchanged) when they are not a picture. void wgtImageSetTransparent(w, hasTransparency, keyColor) Set chroma-key color. Pixels matching keyColor are not drawn.

    API Struct (wgtRegisterApi "image")

      Slot             Function
    @@ -12476,16 +12790,22 @@ btn->onClick = onSaveClicked;
    fromFile wgtImageFromFile setData wgtImageSetData loadFile wgtImageLoadFile - setTransparent wgtImageSetTransparent + setTransparent wgtImageSetTransparent + loadData wgtImageLoadData

    Properties (BASIC Interface)

      Property      Type       Access       Description
       --------      ----       ------       -----------
    -  Picture       String     Read/Write   Setting writes a file path (loads image). Reading returns the path of the loaded file (also set by wgtImageFromFile); in BASIC the path comes as a Picture object, which stands for the path where a value is needed (see LOADPICTURE); wgtImageSetData clears it. Setting "" clears the image and the path; a file that cannot be loaded leaves both unchanged.
    +  Picture       String     Read/Write   Setting writes a file path (loads image). Reading returns the path of the loaded file (also set by wgtImageFromFile), or the name wgtImageLoadData was given; in BASIC the path comes as a Picture object, which stands for the path where a value is needed (see LOADPICTURE); wgtImageSetData clears it. Setting "" clears the image and the path; a file that cannot be loaded leaves both unchanged.
       Stretch       Boolean    Read/Write   When true, an image larger than the widget is scaled down to fit (aspect preserved, nearest neighbour); a smaller image stays at natural size. When false, shown at natural size. Changing it relayouts the widget.
       ImageWidth    Integer    Read-only    Width of the currently loaded image in pixels.
    -  ImageHeight   Integer    Read-only    Height of the currently loaded image in pixels.
    + ImageHeight Integer Read-only Height of the currently loaded image in pixels. + MaskColor Integer Read/Write The transparent colour as a BASIC colour (&HBBGGRR), converted with wgtImageSetTransparent. + UseMaskColor Boolean Read/Write Whether pixels of MaskColor are left out (wgtImageSetTransparent's hasTransparency).

    Methods

    -

    No widget-specific BASIC methods. Setting Picture loads a new image.

    +
      Method                           Description
    +  ------                           -----------
    +  SetPictureData(bytes$[, name$])  WGT_METHOD_SET_PICTURE_DATA (WGT_SIG_DYNAMIC): wgtImageLoadData(bytes, length, name). Returns True when the bytes are a picture.
    +

    Setting Picture loads a new image; the BASIC form runtime shows a LOADPICTUREDATA picture through SetPictureData.

    Events

      Callback     Description
       --------     -----------
    @@ -12633,13 +12953,21 @@ wgtListViewSetData(lv, cells, 2);
    wgtListViewGetSelected(w) Get the index of the selected row (-1 if none). wgtListViewSetSelected(w, idx) Set the selected row by index (outside the list = none) and scroll it into view. wgtListViewSetSort(w, col, dir) Set the sort column and direction. A column outside the defined columns, or ListViewSortNoneE, clears the sort. Ignored while reorder is enabled. + wgtListViewGetSortCol(w) The column the list is sorted by (-1 when unsorted): the last wgtListViewSetSort or header click. + wgtListViewGetSortDir(w) The sort direction (ListViewSortNoneE when unsorted). wgtListViewSetHeaderClickCallback(w, cb) Set a callback for header clicks. Signature: void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir). After it, the ListView fires its own event ColumnClick (Column As Integer, 0-based) with wgtFireEvent, which reaches the widget's onEvent (BASIC: CtrlName_ColumnClick). wgtListViewSetMultiSelect(w, multi) Enable or disable multi-selection. wgtListViewIsItemSelected(w, idx) Check if a specific row is selected. wgtListViewSetItemSelected(w, idx, selected) Select or deselect a specific row. wgtListViewSelectAll(w) Select all rows. wgtListViewClearSelection(w) Deselect all rows. - wgtListViewSetReorderable(w, reorderable) Enable drag-to-reorder of rows. + wgtListViewSetReorderable(w, reorderable) Enable drag-to-reorder of rows. + wgtListViewAddItem(w, text) Add a row with text in its first column (the list keeps its own copy of the cells from then on). + wgtListViewRemoveRow(w, row) Remove a data row. + wgtListViewClear(w) Remove all rows. + wgtListViewGetCell(w, row, col) The text of a cell ("" outside the list). + wgtListViewSetCell(w, row, col, text) Set the text of a cell. + wgtListViewGetRowCount(w) The number of rows.

    Events

      Callback     Description
       --------     -----------
    @@ -12649,7 +12977,9 @@ wgtListViewSetData(lv, cells, 2);

    Properties (BASIC Interface)

      Property     Type       Access       Description
       --------     ----       ------       -----------
    -  ListIndex    Integer    Read/Write   Index of the currently selected row.
    + ListIndex Integer Read/Write Index of the currently selected row. + SortColumn Integer Read/Write The sort column (-1 when unsorted). Setting it sorts by that column in the current order (ascending when unsorted); -1 unsorts. + SortOrder Integer Read/Write The sort order (0 none, 1 ascending, 2 descending). Setting it re-sorts the current column; an unsorted list stays unsorted.

    Methods (BASIC Interface)

      Method                          Description
       ------                          -----------
    @@ -12658,14 +12988,17 @@ wgtListViewSetData(lv, cells, 2);
    ClearSelection Deselect all rows. GetCell(row%, col%) Returns the text of a cell. IsItemSelected(index%) Check if a specific row is selected. + ListCount() Returns the number of rows (the same as RowCount). RemoveItem index% Remove a row by index. Rows after it keep their selection; removing the selected row leaves no selection. RowCount() Returns the number of rows. SelectAll Select all rows. + SelectedRows() Returns the selected rows' numbers separated by spaces ("" when none). SetCell row%, col%, text$ Set the text of a cell. SetColumns spec$ Define columns from a pipe-delimited spec string ("Name,Width|Name,Width"). SetItemSelected idx%, sel Select or deselect a specific row by index. SetMultiSelect multi Enable or disable multi-selection. SetReorderable reorderable Enable or disable drag-to-reorder. + SetRows text$ Replace all rows at once: rows separated by CHR$(10), cells by CHR$(9). SetSort col%, dir% Set the sort column and direction (0=none, 1=ascending, 2=descending). An invalid column or direction clears the sort; ignored while reorder is enabled.

    Default Event

    "Click" (VB basName: ListView).

    @@ -12749,6 +13082,7 @@ WidgetT *r2 = wgtRadio(grp, "Option B");

    Methods (BASIC Interface)

      Method                  Description
       ------                  -----------
    +  GetSelected()           The index of the selected radio button of this button's group, -1 when none (called on a group, wgtRadioGroupGetSelected).
       SetSelected idx%        Select the radio button at index idx% within this button's group (called on a group, within that group).

    Events

      Callback     Description
    @@ -12801,7 +13135,7 @@ wgtRichEditSetSelColor(re, DVX_RGB(0, 0, 160));

    API Struct (wgtRegisterApi "richedit")

    The slots are the functions above in order: create, getRtf, setRtf, getSelRtf, setSelRtf, getSelStart, setSelStart, getSelLength, setSelLength, getSelText, setSelText, getSelFontName, getSelFontSize, setSelFont, getSelStyle, setSelStyle, getSelColor, setSelColor, getSelAlign, setSelAlign, canUndo, canRedo, undo, redo, cut, copy, paste, selectAll, find, loadFile, and saveFile.

    BASIC Interface

    -

    Properties: TextRTF and SelRTF (wgtRichEditGetRtf / SetRtf, GetSelRtf / SetSelRtf), SelStart, SelLength, SelText, SelBold, SelItalic, SelUnderline, SelStrikethru (one FONT_STYLE_* bit of SetSelStyle each), SelColor (a BASIC color; -1 for ForeColor), SelFontName and SelFontSize (wgtRichEditSetSelFont with the other part left alone), SelAlignment, and the read-only CanUndo and CanRedo. Methods: Undo, Redo, Cut, Copy, Paste, SelectAll, Find (any case), LoadFile, and SaveFile. Event: SelChange; default event Change.

    +

    Properties: TextRTF and SelRTF (wgtRichEditGetRtf / SetRtf, GetSelRtf / SetSelRtf), SelStart, SelLength, SelText, SelBold, SelItalic, SelUnderline, SelStrikethru (one FONT_STYLE_* bit of SetSelStyle each), SelColor (a BASIC color; -1 for ForeColor), SelFontName and SelFontSize (wgtRichEditSetSelFont with the other part left alone), SelAlignment, and the read-only CanUndo and CanRedo. Methods: Undo, Redo, Cut, Copy, Paste, SelectAll, Find (text$ and an optional matchCase, any case without it), LoadFile, and SaveFile. Event: SelChange; default event Change.

    Methods Used

    PAINT, CALC_MIN_SIZE (20 by 4 characters of its font, with border and scroll bar), ON_MOUSE, ON_KEY, ON_DRAG_UPDATE, ON_DRAG_END, ON_WHEEL, ON_FOCUS, ON_BLUR, POLL, QUICK_REPAINT, GET_TEXT, SET_TEXT, CLEAR_SELECTION, DESTROY. Flags: WCLASS_FOCUSABLE, WCLASS_NEEDS_POLL.

    @@ -12921,17 +13255,23 @@ WidgetT *content = wgtVBox(sp); -------- ----------- WidgetT *wgtSeparator(parent) Create a separator oriented from the parent: vertical in a horizontal container, horizontal otherwise. WidgetT *wgtHSeparator(parent) Create a horizontal separator line. - WidgetT *wgtVSeparator(parent) Create a vertical separator line. + WidgetT *wgtVSeparator(parent) Create a vertical separator line. + bool wgtSeparatorIsVertical(w) True for a vertical line. + void wgtSeparatorSetVertical(w, v) Turn the line vertical (true) or horizontal (false); the window lays out again.

    API Struct (wgtRegisterApi "separator")

      Slot           Function
       ----           --------
       header         WGT_API_HEADER (ABI version and size, checked by wgtGetApi)
       hSeparator     wgtHSeparator
       vSeparator     wgtVSeparator
    -  create         wgtSeparator
    + create wgtSeparator + isVertical wgtSeparatorIsVertical + setVertical wgtSeparatorSetVertical

    The BASIC Line control is registered as interface "line" with its own one-slot API (create = wgtSeparator).

    -

    Properties

    -

    No widget-specific properties.

    +

    Properties (BASIC Interface)

    +
      Property     Type       Access       Description
    +  --------     ----       ------       -----------
    +  Vertical     Boolean    Read/Write   The orientation (wgtSeparatorIsVertical / wgtSeparatorSetVertical).

    Methods

    No widget-specific methods.

    Events

    @@ -13104,8 +13444,14 @@ wgtLabel(sb, "Ready"); ---- -------- header WGT_API_HEADER (ABI version and size, checked by wgtGetApi) create wgtStatusBar -

    Properties, Methods, Events

    -

    StatusBar has no widget-specific properties, methods, or events. Add child widgets (labels, progress bars, etc.) to populate.

    +

    Properties (BASIC Interface)

    +
      Property     Type                         Access       Description
    +  --------     ----                         ------       -----------
    +  Alignment    Enum (Left, Center, Right)   Read/Write   Where the children sit along the bar when they do not fill it (the align field).
    +  Padding      Integer                      Read/Write   The padding field in pixels (1 unless set; wgtGetPaddingPx / wgtSetPaddingPx).
    +  Spacing      Integer                      Read/Write   The spacing field in pixels (2 unless set; wgtGetSpacingPx / wgtSetSpacingPx).
    +

    Methods, Events

    +

    StatusBar has no widget-specific methods or events. Add child widgets (labels, progress bars, etc.) to populate.

    Default Event

    None. (VB basName: StatusBar.)

    @@ -13178,7 +13524,7 @@ WidgetT *page2 = wgtTabPage(tabs, "Advanced"); bool wgtTextAreaReplaceSelection(w, needle, repl, caseSens, fwd) If the current selection matches needle, replace it, then find the next occurrence in direction fwd. Returns true if anything was replaced or found. void wgtTextAreaSetLineMarker(w, line, gutterRgb, backRgb) Mark 1-based line: a dot of gutterRgb (0xRRGGBB) in the line-number gutter and / or the line's background in backRgb; TEXTAREA_MARKER_NONE leaves a part out, both NONE removes the marker. Markers keep their line numbers when the text changes (the owner moves them). The DVX BASIC IDE marks breakpoints and the paused line this way. void wgtTextAreaClearLineMarkers(w) Remove every line marker. - void wgtTextAreaSetSyntaxMode(w, mode) Use a built-in colorizer: "bas" / "basic" (BASIC: the compiler's keyword list, date literals by the lexer's shape rule, &H / &O and exponent numbers, REM and ' comments; the IDE's code editor uses it), "dhs" / "help" (help source), or "custom" (each painted line goes to the "Colorize" event through wgtFireEvent: args LineText and Colors, a string of one digit per character, "0" = TEXT_SYNTAX_DEFAULT; the host writes the TEXT_SYNTAX_* digits back into Colors; the BASIC Colorize event); NULL or "" turns colouring off. + void wgtTextAreaSetSyntaxMode(w, mode) Use a built-in colorizer: "bas" / "basic" (BASIC: the compiler's keyword list, date literals by the lexer's shape rule, &H / &O and exponent numbers, REM and ' comments; the IDE's code editor uses it), "dhs" / "help" (help source), or "custom" (each painted line goes to the "Colorize" event through wgtFireEvent: args LineText and Colors, a string of one digit per character, "0" = TEXT_SYNTAX_DEFAULT; the host writes the TEXT_SYNTAX_* digits back into Colors; the BASIC Colorize event; the answers are kept with the line text, 64 lines, so a line drawn again unchanged does not fire it again, and setting the mode again drops them); NULL or "" turns colouring off. int32_t wgtTextAreaGetCursorLine(w) Get the current cursor line number. void wgtTextAreaSetGutterClickCallback(w, fn) Set a callback for gutter clicks (e.g. for breakpoint toggling). int32_t wgtTextAreaGetWordAtCursor(w, buf, bufSize) Copy the word under the cursor into buf. Returns its length. @@ -13264,6 +13610,8 @@ tmr->onChange = onTimerTick; void wgtTimerSetInterval(w, intervalMs) Change the timer interval and restart the countdown. An interval of 0 or less never fires. int32_t wgtTimerGetInterval(w) Returns the current interval in milliseconds. bool wgtTimerIsRunning(w) Returns true if the timer is currently running. + bool wgtTimerGetRepeat(w) Returns true for a repeating timer, false for a one-shot. + void wgtTimerSetRepeat(w, repeat) Make the timer repeating or one-shot from its next fire on (a running countdown is kept). void wgtTimerSetEnabled(w, enabled) Start or stop the timer based on enabled flag. void wgtUpdateTimers(void) Global tick function. Called by the event loop to advance all active timers.

    API Struct (wgtRegisterApi "timer")

    @@ -13276,6 +13624,9 @@ tmr->onChange = onTimerTick; setInterval wgtTimerSetInterval isRunning wgtTimerIsRunning updateTimers wgtUpdateTimers + getInterval wgtTimerGetInterval + getRepeat wgtTimerGetRepeat + setRepeat wgtTimerSetRepeat testReset wgtTimerTestReset (present only in host test builds, DVX_HOST_TEST)

    Events

      Callback     Description
    @@ -13285,7 +13636,8 @@ tmr->onChange = onTimerTick;
      Property     Type       Access       Description
       --------     ----       ------       -----------
       Enabled      Boolean    Read/Write   Whether the timer is running. Reading returns the running state; writing starts or stops it.
    -  Interval     Integer    Read/Write   Timer interval in milliseconds. Setting it restarts the countdown; 0 or less disables firing.
    + Interval Integer Read/Write Timer interval in milliseconds. Setting it restarts the countdown; 0 or less disables firing. + Repeat Boolean Read/Write True (the default) for a repeating timer, False for a one-shot (wgtTimerGetRepeat / wgtTimerSetRepeat).

    Methods (BASIC Interface)

      Method       Description
       ------       -----------
    @@ -13316,8 +13668,14 @@ wgtImageButtonFromFile(tb, "icons/open.bmp");
    header WGT_API_HEADER (ABI version and size, checked by wgtGetApi) create wgtToolbar getFace wgtToolbarGetFace -

    Properties, Methods, Events

    -

    Toolbar has no widget-specific properties, methods, or events. Add children (buttons, separators, etc.) to populate.

    +

    Properties (BASIC Interface)

    +
      Property     Type                         Access       Description
    +  --------     ----                         ------       -----------
    +  Alignment    Enum (Left, Center, Right)   Read/Write   Where the children sit along the bar when they do not fill it (the align field).
    +  Padding      Integer                      Read/Write   The padding field in pixels (2 unless set; wgtGetPaddingPx / wgtSetPaddingPx).
    +  Spacing      Integer                      Read/Write   The spacing field in pixels (2 unless set; wgtGetSpacingPx / wgtSetSpacingPx).
    +

    Methods, Events

    +

    Toolbar has no widget-specific methods or events. Add children (buttons, separators, etc.) to populate.

    Default Event

    None. (VB basName: Toolbar.)

    @@ -13356,20 +13714,29 @@ WidgetT *child = wgtTreeItem(root, "Child"); itemIsExpanded wgtTreeItemIsExpanded itemIsSelected wgtTreeItemIsSelected itemSetSelected wgtTreeItemSetSelected -

    Properties

    -

    No widget-specific properties.

    +

    Properties (BASIC Interface)

    +
      Property    Type      Access   Description
    +  --------    ----      ------   -----------
    +  ListIndex   Integer   R/W      Depth-first index of the current node, -1 when none. Setting it selects the node as a click would (Change fires).

    Methods (BASIC Interface)

      Method                                Description
       ------                                -----------
       AddChildItem parentIdx%, text$        Add a child node under the node at the given index.
       AddItem text$                         Add a root-level node.
       Clear                                 Remove all nodes.
    +  GetItemTag(index%)                    Returns the text SetItemTag stored on the node ("" when none).
       GetItemText(index%)                   Returns the text of the node at the given depth-first index.
    +  HasChildren(index%)                   Returns True if the node has nodes under it.
       IsExpanded(index%)                    Returns True if the node is expanded.
       IsItemSelected(index%)                Returns True if the node is selected.
       ItemCount()                           Returns the total number of nodes.
    +  ItemDepth(index%)                     Returns how many nodes the node is under (0 at the root level, -1 for no such node).
    +  ItemParent(index%)                    Returns the index of the node's parent (-1 at the root level or for no such node).
    +  RemoveItem index%                     Remove the node and every node under it (destroys the item widgets).
       SetExpanded index%, expanded          Expand or collapse the node (collapsing moves a hidden selection to the node).
       SetItemSelected index%, selected      Select or deselect the node (single-select: sets or clears the selection).
    +  SetItemTag index%, tag$               Keep a string with the node ("" frees it).
    +  SetItemText index%, text$             Change the node's text.
       SetMultiSelect multi                  Enable or disable multi-selection.
       SetReorderable reorderable            Enable or disable drag-to-reorder.

    Events

    @@ -13408,7 +13775,9 @@ wgtButton(wrap, "Tag 3");

    Properties (BASIC Interface)

      Property     Type                         Access       Description
       --------     ----                         ------       -----------
    -  Alignment    Enum (Left, Center, Right)   Read/Write   Row alignment for rows that do not fill the full width.
    + Alignment Enum (Left, Center, Right) Read/Write Row alignment for rows that do not fill the full width. + Padding Integer Read/Write The padding field in pixels (wgtGetPaddingPx / wgtSetPaddingPx). + Spacing Integer Read/Write The spacing field in pixels (wgtGetSpacingPx / wgtSetSpacingPx).

    Methods

    No widget-specific methods.

    Events

    diff --git a/dosbox-staging-overrides.conf b/dosbox-staging-overrides.conf index 28119d67..9b401ca8 100644 --- a/dosbox-staging-overrides.conf +++ b/dosbox-staging-overrides.conf @@ -1,3 +1,25 @@ +# 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. + [sdl] windowresolution = 1024x768 diff --git a/dosbox-x-overrides.conf b/dosbox-x-overrides.conf index 4863b1b3..4b1eaa14 100644 --- a/dosbox-x-overrides.conf +++ b/dosbox-x-overrides.conf @@ -1,3 +1,25 @@ +# 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. + [sdl] output = surface windowresolution = 1024x768 diff --git a/screenshots/86box/parpm-parpm.png b/screenshots/86box/parpm-parpm.png index a50f176d..4e6a2761 100644 --- a/screenshots/86box/parpm-parpm.png +++ b/screenshots/86box/parpm-parpm.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fc2c96e65240cabcb060dbc0c92e4c59d5424afb2790448e3e66ecffe985f5a -size 17400 +oid sha256:145f93bfbd629a4d85199335402972d315b289fb2c7e8b17148baa5726cf0c37 +size 17276 diff --git a/screenshots/86box/pkgsign-ps-plan.png b/screenshots/86box/pkgsign-ps-plan.png new file mode 100644 index 00000000..59137296 --- /dev/null +++ b/screenshots/86box/pkgsign-ps-plan.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ed7c94a9a57e3302bb292d8e12e446ad2886d76e3f2587ad4e6cbf8f353a4b8 +size 16567 diff --git a/screenshots/86box/upgdrop-ud-sw.png b/screenshots/86box/upgdrop-ud-sw.png index 3ce54a2a..12746a3e 100644 --- a/screenshots/86box/upgdrop-ud-sw.png +++ b/screenshots/86box/upgdrop-ud-sw.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1aaf22316b34f748e761e710a8c5fdfab34595916975dd8a8d4c7b74ec8b21ee -size 17154 +oid sha256:a42a5bb02f585ac5412e94c99e708097b49962ce5ac665626265a6dd72d2ad75 +size 16416 diff --git a/screenshots/dosbox/cpanel-cp-types.png b/screenshots/dosbox/cpanel-cp-types.png index 0b7d27dd..619eb94e 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:2749e1d0ca5ca17f97455a0e6a4200d587f3b0961b135ce80fb37ee586325ef8 -size 24863 +oid sha256:92d8c62871629493d43a2913a148706833ed39aefc38471a0099008f0e3b1f3c +size 24799 diff --git a/screenshots/dosbox/hang-ended.png b/screenshots/dosbox/hang-ended.png index e8dd2143..6ac1b67c 100644 --- a/screenshots/dosbox/hang-ended.png +++ b/screenshots/dosbox/hang-ended.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8e96d2fa598cf563ef80728c0201e51306b0eb9d7ab4169107d33ac62f279bb -size 15155 +oid sha256:05c8ed764345486fd82bdd63033ef83cc9175ffe4968a30a474214b615f325db +size 15166 diff --git a/screenshots/dosbox/hang-taskmgr.png b/screenshots/dosbox/hang-taskmgr.png index 3aba658d..931928bc 100644 --- a/screenshots/dosbox/hang-taskmgr.png +++ b/screenshots/dosbox/hang-taskmgr.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45d1606cf7d9d2da3dc9f7abcccce1f1bea6b55b95fe4a27f0b0c618a8884065 -size 16019 +oid sha256:7682c8fb92700284dfd2c50ea44a5a69f15182c657358da4dc53bd8bbdcacdef +size 16067 diff --git a/screenshots/dosbox/instadd-add-sw.png b/screenshots/dosbox/instadd-add-sw.png index 87001408..6ab52379 100644 --- a/screenshots/dosbox/instadd-add-sw.png +++ b/screenshots/dosbox/instadd-add-sw.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14eba919cb53886f0496c00fb57379b238ca4754dd05bd0a13a49fd135e43599 -size 21515 +oid sha256:a95e657ccefe24dc158cdf198733228926ffd3320afd181575c41eeba7289794 +size 21479 diff --git a/screenshots/dosbox/instadd-su-what.png b/screenshots/dosbox/instadd-su-what.png index b3f2400e..18b84e8e 100644 --- a/screenshots/dosbox/instadd-su-what.png +++ b/screenshots/dosbox/instadd-su-what.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d928454dc96be2d801b4dca486336ca902d461895eaddb436544f9ef2eba25a8 -size 21509 +oid sha256:b29e0eb146dba303b3bb6fc322636bd96f4eb6c2d2b4790ea0216102ced48b7f +size 21513 diff --git a/screenshots/dosbox/menus-m-design.png b/screenshots/dosbox/menus-m-design.png index d54d6915..3bbf9175 100644 --- a/screenshots/dosbox/menus-m-design.png +++ b/screenshots/dosbox/menus-m-design.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1eeb4d3e4cb61600af378d7f36c0f96290f74fe2da8fa14066ef8e779d1459c -size 23804 +oid sha256:72a63bc0029977479b2a9e4cb73caed8aa69c5320af08ac485386281e86452d1 +size 23941 diff --git a/screenshots/dosbox/parpm-parpm.png b/screenshots/dosbox/parpm-parpm.png index 6b631e94..c4e4edb5 100644 --- a/screenshots/dosbox/parpm-parpm.png +++ b/screenshots/dosbox/parpm-parpm.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:698945834aca4b2659a1f02658f2128f135459f9d28a7e10d4d19ce5c22c846d -size 17471 +oid sha256:eb69020b4045dfac048226eebc446cc808418e0592a1f21eb4676f162bb97168 +size 17441 diff --git a/screenshots/dosbox/pkgsign-ps-plan.png b/screenshots/dosbox/pkgsign-ps-plan.png new file mode 100644 index 00000000..59137296 --- /dev/null +++ b/screenshots/dosbox/pkgsign-ps-plan.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ed7c94a9a57e3302bb292d8e12e446ad2886d76e3f2587ad4e6cbf8f353a4b8 +size 16567 diff --git a/screenshots/dosbox/player-p-wav.png b/screenshots/dosbox/player-p-wav.png index 8c4ca0b1..eb7e09fd 100644 --- a/screenshots/dosbox/player-p-wav.png +++ b/screenshots/dosbox/player-p-wav.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81ab1b214f13c292b7034488d9001d405c9fa9fce3b649c412fb98bb4d5ff9da -size 13944 +oid sha256:4155d46360ac8d7eeb050b5ede832b29c4de10595a0b0f7cf0656420a80fa20e +size 13952 diff --git a/screenshots/dosbox/sdk-sdktbx.png b/screenshots/dosbox/sdk-sdktbx.png index 24260f82..0782049d 100644 --- a/screenshots/dosbox/sdk-sdktbx.png +++ b/screenshots/dosbox/sdk-sdktbx.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb2e99f81d2fd46ebf15c15fbd18dc40290b12aef056ce503bee8d34c72e5db8 -size 23699 +oid sha256:d05934c117be8d1d2f3d788ddb55a594af00b607a0acd42ccad013dcfe6a64e9 +size 23835 diff --git a/screenshots/dosbox/shellcmd-sc-back.png b/screenshots/dosbox/shellcmd-sc-back.png new file mode 100644 index 00000000..488088ec --- /dev/null +++ b/screenshots/dosbox/shellcmd-sc-back.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78576367ec2f8053aaad04c8e1205712207ed7fd1d4157e2da84d3c959cdc7f4 +size 17710 diff --git a/screenshots/dosbox/sql-sqlsmk.png b/screenshots/dosbox/sql-sqlsmk.png index 91eecc55..5aab5670 100644 --- a/screenshots/dosbox/sql-sqlsmk.png +++ b/screenshots/dosbox/sql-sqlsmk.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f0385872d52d227121faf4e1453cfbacae6e6be7b9568743fae5d9e53eeed3f -size 16932 +oid sha256:b6cd37e662a61fad0598ae80f40bfb325a7e112091303436901f68f5d63c94f1 +size 17411 diff --git a/screenshots/dosbox/ucide-ucide-ds.png b/screenshots/dosbox/ucide-ucide-ds.png index 26601f99..90670732 100644 --- a/screenshots/dosbox/ucide-ucide-ds.png +++ b/screenshots/dosbox/ucide-ucide-ds.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98eea34512e3f348a3041b12b6ca7ef522f9edcabe494a0f15d2a8e896cf9a1e -size 31561 +oid sha256:75cc5f470d854edf551989cd44d1abf2e6e2b8f44dfc6d23a6890447494754d4 +size 31726 diff --git a/screenshots/dosbox/ucide-ucide-tb.png b/screenshots/dosbox/ucide-ucide-tb.png index 646e96b4..708634f2 100644 --- a/screenshots/dosbox/ucide-ucide-tb.png +++ b/screenshots/dosbox/ucide-ucide-tb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77eaddc942ed51d860f97a5701db753b7010a68fa5d74088922e1d10cc5ef039 -size 31708 +oid sha256:6641873ed5704e3ca54c5353ff07620a35ad31ae787069995d8612f7a635db2b +size 31876 diff --git a/screenshots/dosbox/upgdrop-ud-sw.png b/screenshots/dosbox/upgdrop-ud-sw.png index 3ce54a2a..12746a3e 100644 --- a/screenshots/dosbox/upgdrop-ud-sw.png +++ b/screenshots/dosbox/upgdrop-ud-sw.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1aaf22316b34f748e761e710a8c5fdfab34595916975dd8a8d4c7b74ec8b21ee -size 17154 +oid sha256:a42a5bb02f585ac5412e94c99e708097b49962ce5ac665626265a6dd72d2ad75 +size 16416 diff --git a/server/dvxserver.lua b/server/dvxserver.lua index a1e8ab80..68d72e9c 100644 --- a/server/dvxserver.lua +++ b/server/dvxserver.lua @@ -1,3 +1,25 @@ +-- 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. + -- dvxserver.lua -- the DVX package server: the catalogue of add-on packages that DVX's Software -- program browses, installs, and updates from, and where publishers upload them. -- diff --git a/server/lib/packages.lua b/server/lib/packages.lua index 9fe28757..1d09550f 100644 --- a/server/lib/packages.lua +++ b/server/lib/packages.lua @@ -1,3 +1,25 @@ +-- 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. + -- packages.lua -- the DVX package catalogue: packages, their versions, what each needs, and the -- review that publishes a version. -- diff --git a/server/test/account.lua b/server/test/account.lua index 228fec3b..c9028800 100644 --- a/server/test/account.lua +++ b/server/test/account.lua @@ -1,3 +1,25 @@ +-- 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. + -- account.lua -- an account on the service under test, made as a person makes one: registered, the -- link in its mail followed (read from the mail queue in the database), then signed in. Loaded by -- the suites, and run by serve.sh for the DOS tests' account: diff --git a/server/test/run.sh b/server/test/run.sh index fc2ceeb8..b883bc50 100755 --- a/server/test/run.sh +++ b/server/test/run.sh @@ -1,4 +1,26 @@ #!/bin/bash +# 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. + # run.sh -- the DVX package server's tests, as singemaster's are run: fixtures made, the server # started on a port of its own with a configuration of its own, the suites run against it, the # server stopped. Then both services in one calog process, each answering on its own port. diff --git a/server/test/serve.sh b/server/test/serve.sh index 3bb43a5a..0bd847b5 100755 --- a/server/test/serve.sh +++ b/server/test/serve.sh @@ -1,4 +1,26 @@ #!/bin/bash +# 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. + # serve.sh DATA PORT FIXTURES DVXPKG USER PASSWORD -- the package server for the DOS tests (runDos.sh starts it # for a "# REQUIRES: PKGSERVER" script and stops it by its PID). In DATA, from nothing: # diff --git a/server/test/testPackages.lua b/server/test/testPackages.lua index 2db84cf2..89fe31f5 100644 --- a/server/test/testPackages.lua +++ b/server/test/testPackages.lua @@ -1,3 +1,25 @@ +-- 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. + -- testPackages.lua -- the package server end to end: uploads checked by the package tool and -- refused for the right reasons, review with the repository's signature (made here, as on the -- operator's own computer), the public catalogue, downloads (resumed) that check against the diff --git a/server/test/testReview.lua b/server/test/testReview.lua index a090fadc..9f009a09 100644 --- a/server/test/testReview.lua +++ b/server/test/testReview.lua @@ -1,3 +1,25 @@ +-- 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. + -- testReview.lua -- the operator's review tool (tools/review.lua) against the running service: what -- waits, a fetch and the package tool's word on it, an approval signed with the repository's key -- that DVX would trust, a rejection, withdrawing and publishing again, and a fetched file changed diff --git a/server/tools/review.lua b/server/tools/review.lua index 216d0ba4..db205f72 100644 --- a/server/tools/review.lua +++ b/server/tools/review.lua @@ -1,3 +1,25 @@ +-- 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. + -- review.lua -- the operator's review of uploaded packages, run on the operator's own computer: the -- one place the repository's secret key is kept. It lists what waits, fetches a version and shows -- what it is (the package tool's own check of it), approves a version with the repository's diff --git a/src/apps/kpunch/basdemo/basdemo.dhs b/src/apps/kpunch/basdemo/basdemo.dhs index e7f39983..6d34d94d 100644 --- a/src/apps/kpunch/basdemo/basdemo.dhs +++ b/src/apps/kpunch/basdemo/basdemo.dhs @@ -1,24 +1,24 @@ -// 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. +# 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. # BASIC Demo help. Compiled to basdemo.hlp next to basdemo.app; F1 and # Help > Contents in the BASIC Demo open it. diff --git a/src/apps/kpunch/basdemo/shape.cls b/src/apps/kpunch/basdemo/shape.cls index 5a07ae17..73e0c43a 100644 --- a/src/apps/kpunch/basdemo/shape.cls +++ b/src/apps/kpunch/basdemo/shape.cls @@ -1,6 +1,30 @@ VERSION DVX 1.00 CLASS Attribute VB_Name = "Shape" + +' 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. + + ' A shape for the Objects demo: its kind and size, and the area that ' follows from them. Every new Shape starts as a square of size 1. diff --git a/src/apps/kpunch/cpanel/cpSounds.c b/src/apps/kpunch/cpanel/cpSounds.c index 41330f65..a6cab1f6 100644 --- a/src/apps/kpunch/cpanel/cpSounds.c +++ b/src/apps/kpunch/cpanel/cpSounds.c @@ -233,8 +233,8 @@ static int32_t sChoice[SoundCountE]; // Prototypes // ============================================================ -static void addBankRow(WidgetT *page, const char *label, BankRowT *row, PrefsHandleT *prefs); -static int32_t addBanks(BankRowT *row, PrefsHandleT *prefs); +static void addBankRow(WidgetT *page, const char *label, BankRowT *row, PrefsHandleT *prefs, char **banks, const PkgDbT *db); +static int32_t addBanks(BankRowT *row, PrefsHandleT *prefs, char **banks, const PkgDbT *db); static int32_t addDriver(const char *name, const char *label); static int32_t addFile(const char *path); static void addFound(BankRowT *row, BankFindFnT *find, const char *label); @@ -263,9 +263,10 @@ static void showVolume(void); // A row of banks (its dropdown, and its Test when the music library is -// there). -static void addBankRow(WidgetT *page, const char *label, BankRowT *row, PrefsHandleT *prefs) { - int32_t sel = addBanks(row, prefs); +// there), from the banks folders' files (sorted) and the package records +// (NULL: none). +static void addBankRow(WidgetT *page, const char *label, BankRowT *row, PrefsHandleT *prefs, char **banks, const PkgDbT *db) { + int32_t sel = addBanks(row, prefs, banks, db); WidgetT *box = addRow(page, label); row->drop = wgtDropdown(box); @@ -283,26 +284,20 @@ static void addBankRow(WidgetT *page, const char *label, BankRowT *row, PrefsHan } -// A row's banks: the standard one, those of its kind in the banks folders -// (by the names DVX.INI keeps, shown by bankLabel; the standard one not -// twice), the sound cards' own banks where the row offers them, and -// DVX.INI's own choice when it is none of them (so OK keeps it). Returns -// DVX.INI's choice's item. -static int32_t addBanks(BankRowT *row, PrefsHandleT *prefs) { - char **names = platformKindReadDir(DvxKindBanksE); - PkgDbT db; - bool haveDb = sPkg.load && sPkg.load(&db, ""); - int32_t sel; - +// A row's banks: the standard one, those of its kind among banks (the +// banks folders' files, by the names DVX.INI keeps, shown by bankLabel; +// the standard one not twice), the sound cards' own banks where the row +// offers them, and DVX.INI's own choice when it is none of them (so OK +// keeps it). Returns DVX.INI's choice's item. +static int32_t addBanks(BankRowT *row, PrefsHandleT *prefs, char **banks, const PkgDbT *db) { choiceAdd(&row->choices, "", CPS_BANK_LABEL); - qsort(names, arrlen(names), sizeof(names[0]), compareNames); - for (int32_t i = 0; i < (int32_t)arrlen(names); i++) { - if (bankOfRow(row, names[i]) && strcasecmp(names[i], row->standard) != 0) { + for (int32_t i = 0; i < (int32_t)arrlen(banks); i++) { + if (bankOfRow(row, banks[i]) && strcasecmp(banks[i], row->standard) != 0) { char label[DVX_MAX_PATH]; - bankLabel(names[i], row, haveDb ? &db : NULL, label, sizeof(label)); - choiceSetLabel(&row->choices, choiceAdd(&row->choices, names[i], CPS_BANK_LABEL), label); + bankLabel(banks[i], row, db, label, sizeof(label)); + choiceSetLabel(&row->choices, choiceAdd(&row->choices, banks[i], CPS_BANK_LABEL), label); } } @@ -311,14 +306,7 @@ static int32_t addBanks(BankRowT *row, PrefsHandleT *prefs) { addFound(row, sMus.romBank, CPS_ROM_BANK); } - // Partly loaded when out of memory: freed whenever it was loaded at all - if (sPkg.load) { - sPkg.release(&db); - } - - dvxReadDirFree(names); - sel = choiceAdd(&row->choices, prefsGetString(prefs, AUDIO_INI_SECTION, row->key, ""), CPS_BANK_LABEL); - return sel; + return choiceAdd(&row->choices, prefsGetString(prefs, AUDIO_INI_SECTION, row->key, ""), CPS_BANK_LABEL); } @@ -603,8 +591,22 @@ void cpSoundsBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) { wgtDropdownSetItems(sModDrop, sModLabels, AUDIO_MODULES_CHOICE_COUNT); wgtDropdownSetSelected(sModDrop, sSnd.modules(prefs)); - addBankRow(page, "Instruments:", &sFmBanks, prefs); - addBankRow(page, "SoundFont:", &sSoundFonts, prefs); + // The banks folders and the package records (they name the add-on + // banks), read once for both rows + char **banks = platformKindReadDir(DvxKindBanksE); + PkgDbT db; + bool haveDb = sPkg.load && sPkg.load(&db, ""); + + qsort(banks, arrlen(banks), sizeof(banks[0]), compareNames); + addBankRow(page, "Instruments:", &sFmBanks, prefs, banks, haveDb ? &db : NULL); + addBankRow(page, "SoundFont:", &sSoundFonts, prefs, banks, haveDb ? &db : NULL); + + // Partly loaded when out of memory: freed whenever it was loaded at all + if (sPkg.load) { + sPkg.release(&db); + } + + dvxReadDirFree(banks); AudioInfoT info; char text[CPS_TEXT_BUF]; diff --git a/src/apps/kpunch/cpanel/cpanel.dhs b/src/apps/kpunch/cpanel/cpanel.dhs index 8f1454cf..ea689c85 100644 --- a/src/apps/kpunch/cpanel/cpanel.dhs +++ b/src/apps/kpunch/cpanel/cpanel.dhs @@ -1,24 +1,24 @@ -// 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. +# 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. # Control Panel help. Compiled to cpanel.hlp next to cpanel.app; F1 and # Help > Contents in the Control Panel open it. diff --git a/src/apps/kpunch/cpanel/cpanel.res b/src/apps/kpunch/cpanel/cpanel.res index fee578c1..76bc6d56 100644 --- a/src/apps/kpunch/cpanel/cpanel.res +++ b/src/apps/kpunch/cpanel/cpanel.res @@ -1,3 +1,25 @@ +# 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. + # cpanel.res -- Resource manifest for Control Panel icon32 icon icon32.bmp name text "Control Panel" diff --git a/src/apps/kpunch/dvxbasic/Makefile b/src/apps/kpunch/dvxbasic/Makefile index 68e1b008..52851264 100644 --- a/src/apps/kpunch/dvxbasic/Makefile +++ b/src/apps/kpunch/dvxbasic/Makefile @@ -26,9 +26,10 @@ # basrt.lib -- BASIC runtime library (VM + values + form # runtime + dlregsym init) # basdev.lib -- BASIC development library (compiler, build -# pipeline, validator, debugger; the IDE and -# BASIC programs through compiler.bas / -# debug.bas; standalone stub embedded) +# pipeline, validator, debugger, the .frm +# model; the IDE and BASIC programs through +# compiler.bas / debug.bas / designer.bas's +# Frm*; standalone stub embedded) # dvxbasic.app -- BASIC IDE (UI, links against basrt.lib and # basdev.lib) # basstub.app -- standalone app stub (obj/dvxbasic, embedded @@ -65,23 +66,25 @@ SYSTEMDIR = ../../../../bin/system COMP_SRCS = compiler/lexer.c compiler/ast.c compiler/scope.c compiler/parse.c compiler/parseExpr.c compiler/parseStmt.c compiler/parseDecl.c compiler/resolve.c compiler/resolveExpr.c compiler/typecheck.c compiler/fold.c compiler/gen.c compiler/genExpr.c compiler/genStmt.c compiler/compiler.c compiler/codegen.c compiler/strip.c compiler/obfuscate.c compiler/compact.c VM_SRCS = runtime/vm.c runtime/values.c runtime/dates.c runtime/serialize.c runtime/gfx.c runtime/screen.c # Standalone programs that link the VM also need what libdvx provides it -# at run time on the target (INI access through dvxPrefs). -VM_LINK_SRCS = $(VM_SRCS) $(LIBDVX)/dvxPrefs.c +# at run time on the target (INI access through dvxPrefs, Unicode through +# dvxFontFmt). +VM_LINK_SRCS = $(VM_SRCS) $(LIBDVX)/dvxPrefs.c $(LIBDVX)/dvxFontFmt.c SUPPORT_SRCS = $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/thirdparty/stb_ds_impl.c -RES_SRCS = $(LIBDVX)/dvxResource.c $(LIBDVX)/dvxFontFmt.c $(TOOLSDIR)/dvxResWrite.c +RES_SRCS = $(LIBDVX)/dvxResource.c $(TOOLSDIR)/dvxResWrite.c CLI_SRCS = basBuild.c $(COMP_SRCS) $(VM_LINK_SRCS) $(RES_SRCS) $(SUPPORT_SRCS) # Runtime library objects (VM + values + form runtime + serialization) -RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/dates.o $(OBJDIR)/gfx.o $(OBJDIR)/screen.o $(OBJDIR)/formrt.o $(OBJDIR)/formGfx.o $(OBJDIR)/formPrinter.o $(OBJDIR)/formDesign.o $(OBJDIR)/formCtl.o $(OBJDIR)/frmParser.o $(OBJDIR)/frmModel.o $(OBJDIR)/frmModelBas.o $(OBJDIR)/basNativeDos.o $(OBJDIR)/serialize.o $(OBJDIR)/basRunHost.o +RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/dates.o $(OBJDIR)/gfx.o $(OBJDIR)/screen.o $(OBJDIR)/formrt.o $(OBJDIR)/formGfx.o $(OBJDIR)/formPrinter.o $(OBJDIR)/formDesign.o $(OBJDIR)/formCtl.o $(OBJDIR)/frmParser.o $(OBJDIR)/basNativeDos.o $(OBJDIR)/serialize.o $(OBJDIR)/basRunHost.o RT_TARGETDIR = $(LIBSDIR)/kpunch/basrt RT_TARGET = $(RT_TARGETDIR)/basrt.lib # Compiler objects (the development library) COMP_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(COMP_SRCS))) $(OBJDIR)/basBuild.o -# Development library: the compiler plus the shared validator, debugger -# and proc scanner and the BASIC entry points (basdev/) -DEV_SRCS = basdev/basValidate.c basdev/basDebug.c basdev/basProcs.c basdev/basSource.c basdev/basHelpMap.c basdev/basProject.c basdev/basDevBas.c +# Development library: the compiler plus the shared validator, debugger, +# proc scanner, the .frm model the designer works on, and the BASIC +# entry points (basdev/: compiler.bas, debug.bas, designer.bas's Frm*) +DEV_SRCS = basdev/basValidate.c basdev/basDebug.c basdev/basProcs.c basdev/basSource.c basdev/basHelpMap.c basdev/basProject.c basdev/basDevBas.c basdev/frmModel.c basdev/frmModelBas.c DEV_OBJS = $(COMP_OBJS) $(patsubst basdev/%.c,$(OBJDIR)/%.o,$(DEV_SRCS)) DEV_TARGETDIR = $(LIBSDIR)/kpunch/basdev DEV_TARGET = $(DEV_TARGETDIR)/basdev.lib @@ -311,7 +314,7 @@ clean: host-basrt-clean # of the host build. HOSTRT_DIR = $(HOST_OBJROOT)/basrt HOSTRT_TARGET = $(HOST_OBJROOT)/basrt.a -HOSTRT_SRCS = $(COMP_SRCS) $(VM_SRCS) formrt/formrt.c formrt/formGfx.c formrt/formPrinter.c formrt/formDesign.c formrt/formCtl.c formrt/frmParser.c formrt/frmModel.c formrt/frmModelBas.c formrt/basNativeHost.c formrt/basRunHost.c +HOSTRT_SRCS = $(COMP_SRCS) $(VM_SRCS) formrt/formrt.c formrt/formGfx.c formrt/formPrinter.c formrt/formDesign.c formrt/formCtl.c formrt/frmParser.c basdev/frmModel.c basdev/frmModelBas.c formrt/basNativeHost.c formrt/basRunHost.c HOSTRT_OBJS = $(patsubst %.c,$(HOSTRT_DIR)/%.o,$(notdir $(HOSTRT_SRCS))) HOSTRT_CFLAGS = $(HOST_CFLAGS) -Wno-stringop-truncation $(DVX_INCLUDES) -I../../../widgets/kpunch -I../../../libs/kpunch/dvxshell -I../../../libs/kpunch/libtasks @@ -335,6 +338,10 @@ $(HOSTRT_DIR)/%.o: formrt/%.c $(DVX_MAKEFILES) @mkdir -p $(HOSTRT_DIR) $(HOSTCC) $(HOSTRT_CFLAGS) -c -o $@ $< +$(HOSTRT_DIR)/%.o: basdev/%.c $(DVX_MAKEFILES) + @mkdir -p $(HOSTRT_DIR) + $(HOSTCC) $(HOSTRT_CFLAGS) -c -o $@ $< + host-basrt-clean: rm -rf $(HOSTRT_DIR) $(HOSTRT_TARGET) diff --git a/src/apps/kpunch/dvxbasic/basBuild.c b/src/apps/kpunch/dvxbasic/basBuild.c index b4776db9..85c4d3e0 100644 --- a/src/apps/kpunch/dvxbasic/basBuild.c +++ b/src/apps/kpunch/dvxbasic/basBuild.c @@ -34,7 +34,8 @@ // .frm texts too), compact the bytecode and re-serialize. // 3. Debug builds serialize the debug info. // 4. Extract the STUB resource from selfPath and write it as outPath. -// 5. Append the resource set in canonical order: +// 5. Gather the resource set in memory (a DvxResBatchT, written to +// outPath once at the end) in canonical order: // BAS_RES_NAME (always, falls back to BAS_DEFAULT_APP_NAME) // BAS_RES_AUTHOR / PUBLISHER / VERSION / COPYRIGHT / DESCRIPTION // BAS_RES_ICON32 (project icon, else the NOICON fallback) @@ -47,6 +48,7 @@ // 6. Copy the help file next to outPath. #include "basBuild.h" +#include "../../../tools/dvxResWrite.h" #include "basRes.h" #include "compiler/compiler.h" #include "compiler/compact.h" @@ -121,11 +123,11 @@ static const char *const sWrapEnd[] = { // Function prototypes (alphabetical) static bool appendSource(BasProjectSourceT *ps, const char *text, int32_t len); -static int32_t appendText(const char *path, const char *name, const char *value); +static int32_t appendText(DvxResBatchT *res, const char *name, const char *value); bool basBuildAddSource(BasProjectSourceT *ps, const char *path, const char *text, int32_t len, BasBuildWrapE wrap); const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec); bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, BasBuildWrapE wrap, const char *name, const char *ctrlList, int32_t *textLine, int32_t *textLines); -BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit, const BasCtrlValidatorT *validator, const BasCompilerT **liveCompiler, BasCompileErrorT *err); +BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit, const BasCtrlValidatorT *validator, const BasCompilerT **liveCompiler, BasCompileErrorT *err, void (*unitFn)(void *ctx), void *unitCtx); bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out); const char *basBuildControl(const char *outPath, const BasBuildSpecT *spec); const char *basBuildErrorText(const char *message); @@ -140,11 +142,11 @@ BasBuildWrapE basBuildWrapForPath(const char *path); static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...); static const char *copyHelpFile(const char *outPath, const BasBuildSpecT *spec); static int32_t countLines(const char *text, int32_t len); -static const char *emitControls(const char *outPath, const BasBuildSpecT *spec); -static const char *emitCtlTexts(const char *outPath, const BasBuildSpecT *spec); -static const char *emitFonts(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 const char *emitControls(DvxResBatchT *res, const BasBuildSpecT *spec); +static const char *emitCtlTexts(DvxResBatchT *res, const BasBuildSpecT *spec); +static const char *emitFonts(DvxResBatchT *res, const BasBuildSpecT *spec); +static const char *emitIcon(DvxResBatchT *res, const BasBuildSpecT *spec); +static const char *emitResources(DvxResBatchT *res, 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 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); @@ -194,12 +196,12 @@ static bool appendSource(BasProjectSourceT *ps, const char *text, int32_t len) { } -static int32_t appendText(const char *path, const char *name, const char *value) { +static int32_t appendText(DvxResBatchT *res, const char *name, const char *value) { if (!value || !value[0]) { return 0; } - return dvxResAppend(path, name, DVX_RES_TEXT, value, (uint32_t)strlen(value) + 1); + return dvxResBatchAdd(res, name, DVX_RES_TEXT, value, (uint32_t)strlen(value) + 1); } @@ -349,16 +351,31 @@ const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec) { int32_t dbgLen = 0; const char *failure = prepareModule(spec, frmData, frmLens, obfFrms, frmCount, &modData, &modLen, &dbgData, &dbgLen); + // Every resource is gathered and the file written once + DvxResBatchT res; + bool batched = false; + if (!failure) { failure = writeStub(outPath, spec); } if (!failure) { - failure = emitResources(outPath, spec, modData, modLen, dbgData, dbgLen, frmData, frmLens, obfFrms, frmCount); + batched = dvxResBatchBegin(&res, outPath) == 0; + failure = batched ? NULL : BAS_BUILD_ERR_WRITE_RES; } if (!failure) { - failure = emitFonts(outPath, spec); + failure = emitResources(&res, spec, modData, modLen, dbgData, dbgLen, frmData, frmLens, obfFrms, frmCount); + } + + if (!failure) { + failure = emitFonts(&res, spec); + } + + if (batched && failure) { + dvxResBatchFree(&res); + } else if (batched && dvxResBatchCommit(&res) != 0) { + failure = BAS_BUILD_ERR_WRITE_RES; } if (!failure) { @@ -418,7 +435,7 @@ bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, Ba } -BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit, const BasCtrlValidatorT *validator, const BasCompilerT **liveCompiler, BasCompileErrorT *err) { +BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit, const BasCtrlValidatorT *validator, const BasCompilerT **liveCompiler, BasCompileErrorT *err, void (*unitFn)(void *ctx), void *unitCtx) { memset(err, 0, sizeof(*err)); BasCompilerT *compiler = basCompilerCreate(source, len); @@ -430,6 +447,7 @@ BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit } basCompilerSetOptionExplicit(compiler, optionExplicit); + basCompilerSetUnitFn(compiler, unitFn, unitCtx); if (validator) { basCompilerSetValidator(compiler, validator); @@ -539,16 +557,31 @@ const char *basBuildControl(const char *outPath, const BasBuildSpecT *spec) { int32_t dbgLen = 0; const char *failure = prepareModule(spec, NULL, NULL, NULL, 0, &modData, &modLen, &dbgData, &dbgLen); + // Every resource is gathered and the file written once + DvxResBatchT res; + bool batched = false; + if (!failure) { failure = writeMarker(outPath); } - if (!failure && (dvxResAppend(outPath, BAS_RES_MODULE, DVX_RES_BINARY, modData, (uint32_t)modLen) != 0 || (dbgData && dvxResAppend(outPath, BAS_RES_DEBUG, DVX_RES_BINARY, dbgData, (uint32_t)dbgLen) != 0))) { + if (!failure) { + batched = dvxResBatchBegin(&res, outPath) == 0; + failure = batched ? NULL : BAS_BUILD_ERR_WRITE_RES; + } + + if (!failure && (dvxResBatchAdd(&res, BAS_RES_MODULE, DVX_RES_BINARY, modData, (uint32_t)modLen) != 0 || (dbgData && dvxResBatchAdd(&res, BAS_RES_DEBUG, DVX_RES_BINARY, dbgData, (uint32_t)dbgLen) != 0))) { failure = BAS_BUILD_ERR_WRITE_RES; } if (!failure) { - failure = emitControls(outPath, spec); + failure = emitControls(&res, spec); + } + + if (batched && failure) { + dvxResBatchFree(&res); + } else if (batched && dvxResBatchCommit(&res) != 0) { + failure = BAS_BUILD_ERR_WRITE_RES; } if (!failure) { @@ -765,17 +798,17 @@ static int32_t countLines(const char *text, int32_t len) { // picture (icon24, the header's ToolboxIcon, a file in the project // folder) and its toolbox name (the header's Description, else its own // name), the second and later with "-n" suffixes. -static const char *emitControls(const char *outPath, const BasBuildSpecT *spec) { +static const char *emitControls(DvxResBatchT *res, const BasBuildSpecT *spec) { int32_t rc = 0; - rc |= appendText(outPath, BAS_RES_AUTHOR, spec->author); - rc |= appendText(outPath, BAS_RES_PUBLISHER, spec->publisher); - rc |= appendText(outPath, BAS_RES_VERSION, spec->version); - rc |= appendText(outPath, BAS_RES_COPYRIGHT, spec->copyright); - rc |= appendText(outPath, BAS_RES_DESCRIPTION, spec->description); + rc |= appendText(res, BAS_RES_AUTHOR, spec->author); + rc |= appendText(res, BAS_RES_PUBLISHER, spec->publisher); + rc |= appendText(res, BAS_RES_VERSION, spec->version); + rc |= appendText(res, BAS_RES_COPYRIGHT, spec->copyright); + rc |= appendText(res, BAS_RES_DESCRIPTION, spec->description); if (spec->helpFile && spec->helpFile[0]) { - rc |= appendText(outPath, BAS_RES_HELPFILE, platformPathBaseName(spec->helpFile)); + rc |= appendText(res, BAS_RES_HELPFILE, platformPathBaseName(spec->helpFile)); } for (int32_t i = 0; rc == 0 && i < spec->ctlCount; i++) { @@ -799,7 +832,7 @@ static const char *emitControls(const char *outPath, const BasBuildSpecT *spec) } snprintf(resName, sizeof(resName), (index == BAS_BUILD_CTL_FIRST) ? "%s" : BAS_CTL_RES_SUFFIX_FMT, BAS_CTL_RES_ICON, (int)index); - rc |= dvxResAppend(outPath, resName, DVX_RES_ICON, iconData, (uint32_t)iconLen); + rc |= dvxResBatchAdd(res, resName, DVX_RES_ICON, iconData, (uint32_t)iconLen); free(iconData); } @@ -808,19 +841,19 @@ static const char *emitControls(const char *outPath, const BasBuildSpecT *spec) } snprintf(resName, sizeof(resName), (index == BAS_BUILD_CTL_FIRST) ? "%s" : BAS_CTL_RES_SUFFIX_FMT, BAS_CTL_RES_NAME, (int)index); - rc |= appendText(outPath, resName, value); + rc |= appendText(res, resName, value); } if (rc != 0) { return BAS_BUILD_ERR_WRITE_RES; } - return emitCtlTexts(outPath, spec); + return emitCtlTexts(res, spec); } // The UserControls' header texts, CTL0, CTL1 ... -static const char *emitCtlTexts(const char *outPath, const BasBuildSpecT *spec) { +static const char *emitCtlTexts(DvxResBatchT *res, const BasBuildSpecT *spec) { if (spec->ctlCount > BAS_MAX_CTL_RESOURCES) { return "Too many UserControls in one project."; } @@ -830,7 +863,7 @@ static const char *emitCtlTexts(const char *outPath, const BasBuildSpecT *spec) snprintf(resName, sizeof(resName), BAS_RES_CTL_FMT, (long)i); - if (appendText(outPath, resName, spec->ctlSources[i]) != 0) { + if (appendText(res, resName, spec->ctlSources[i]) != 0) { return BAS_BUILD_ERR_WRITE_RES; } } @@ -841,7 +874,7 @@ static const char *emitCtlTexts(const char *outPath, const BasBuildSpecT *spec) // The fonts the program carries as FONTn resources, each checked to be a // DVX font first -static const char *emitFonts(const char *outPath, const BasBuildSpecT *spec) { +static const char *emitFonts(DvxResBatchT *res, const BasBuildSpecT *spec) { if (spec->fontCount > BAS_BUILD_FONTS_MAX) { snprintf(sFontError, sizeof(sFontError), "A program carries at most %d fonts.", BAS_BUILD_FONTS_MAX); return sFontError; @@ -877,7 +910,7 @@ static const char *emitFonts(const char *outPath, const BasBuildSpecT *spec) { snprintf(name, sizeof(name), BAS_RES_FONT_FMT, (long)i); - int32_t rc = dvxResAppend(outPath, name, DVX_RES_FONT, data, (uint32_t)len); + int32_t rc = dvxResBatchAdd(res, name, DVX_RES_FONT, data, (uint32_t)len); free(data); @@ -895,7 +928,7 @@ static const char *emitFonts(const char *outPath, const BasBuildSpecT *spec) { // Embed the project icon, or the NOICON fallback of the stub carrier // when the project names none. Returns NULL on success, else // an error message (a named icon that cannot be read names the path). -static const char *emitIcon(const char *outPath, const BasBuildSpecT *spec) { +static const char *emitIcon(DvxResBatchT *res, const BasBuildSpecT *spec) { char iconPath[DVX_MAX_PATH]; void *iconData = NULL; int32_t iconLen = 0; @@ -927,13 +960,13 @@ static const char *emitIcon(const char *outPath, const BasBuildSpecT *spec) { } } - int32_t rc = dvxResAppend(outPath, BAS_RES_ICON32, DVX_RES_ICON, iconData, (uint32_t)iconLen); + int32_t rc = dvxResBatchAdd(res, BAS_RES_ICON32, DVX_RES_ICON, iconData, (uint32_t)iconLen); free(iconData); return (rc == 0) ? NULL : BAS_BUILD_ERR_WRITE_RES; } -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 const char *emitResources(DvxResBatchT *res, 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) { // Accumulate every append result so a disk-full or I/O failure on any // resource is reported instead of being silently treated as success. int32_t rc = 0; @@ -941,32 +974,32 @@ static const char *emitResources(const char *outPath, const BasBuildSpecT *spec, // Project metadata. Name is required -- fall back to a generic label // so the compiled app always has something sensible to display. const char *projName = (spec->projName && spec->projName[0]) ? spec->projName : BAS_DEFAULT_APP_NAME; - rc |= appendText(outPath, BAS_RES_NAME, projName); + rc |= appendText(res, BAS_RES_NAME, projName); - rc |= appendText(outPath, BAS_RES_AUTHOR, spec->author); - rc |= appendText(outPath, BAS_RES_PUBLISHER, spec->publisher); - rc |= appendText(outPath, BAS_RES_VERSION, spec->version); - rc |= appendText(outPath, BAS_RES_COPYRIGHT, spec->copyright); - rc |= appendText(outPath, BAS_RES_DESCRIPTION, spec->description); + rc |= appendText(res, BAS_RES_AUTHOR, spec->author); + rc |= appendText(res, BAS_RES_PUBLISHER, spec->publisher); + rc |= appendText(res, BAS_RES_VERSION, spec->version); + rc |= appendText(res, BAS_RES_COPYRIGHT, spec->copyright); + rc |= appendText(res, BAS_RES_DESCRIPTION, spec->description); - const char *iconFailure = emitIcon(outPath, spec); + const char *iconFailure = emitIcon(res, spec); if (iconFailure) { return iconFailure; } if (spec->helpFile && spec->helpFile[0]) { - rc |= appendText(outPath, BAS_RES_HELPFILE, platformPathBaseName(spec->helpFile)); + rc |= appendText(res, BAS_RES_HELPFILE, platformPathBaseName(spec->helpFile)); } if (spec->multiInstance) { - rc |= appendText(outPath, BAS_RES_MULTIINSTANCE, BAS_RES_FLAG_ON); + rc |= appendText(res, BAS_RES_MULTIINSTANCE, BAS_RES_FLAG_ON); } - rc |= dvxResAppend(outPath, BAS_RES_MODULE, DVX_RES_BINARY, modData, (uint32_t)modLen); + rc |= dvxResBatchAdd(res, BAS_RES_MODULE, DVX_RES_BINARY, modData, (uint32_t)modLen); if (dbgData && dbgLen > 0) { - rc |= dvxResAppend(outPath, BAS_RES_DEBUG, DVX_RES_BINARY, dbgData, (uint32_t)dbgLen); + rc |= dvxResBatchAdd(res, BAS_RES_DEBUG, DVX_RES_BINARY, dbgData, (uint32_t)dbgLen); } // Form resources. Release builds embed the obfuscated variant when @@ -1002,7 +1035,7 @@ static const char *emitResources(const char *outPath, const BasBuildSpecT *spec, int32_t formLen = basFindFormEndPos((const char *)data, len); snprintf(resName, sizeof(resName), BAS_RES_FORM_FMT, (long)outIdx); - rc |= dvxResAppend(outPath, resName, DVX_RES_BINARY, data, (uint32_t)formLen); + rc |= dvxResBatchAdd(res, resName, DVX_RES_BINARY, data, (uint32_t)formLen); outIdx++; } @@ -1010,7 +1043,7 @@ static const char *emitResources(const char *outPath, const BasBuildSpecT *spec, return BAS_BUILD_ERR_WRITE_RES; } - return emitCtlTexts(outPath, spec); + return emitCtlTexts(res, spec); } diff --git a/src/apps/kpunch/dvxbasic/basBuild.h b/src/apps/kpunch/dvxbasic/basBuild.h index b9c9c013..2232f2fa 100644 --- a/src/apps/kpunch/dvxbasic/basBuild.h +++ b/src/apps/kpunch/dvxbasic/basBuild.h @@ -191,8 +191,10 @@ typedef struct { // optionExplicit is set and validator (may be NULL) checking // CtrlName.Member references. *liveCompiler (may be NULL) holds the // compiler while it runs: a validator reads its form scope from it. -// NULL on failure, with *err telling why. -BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit, const BasCtrlValidatorT *validator, const BasCompilerT **liveCompiler, BasCompileErrorT *err); +// unitFn(unitCtx) (NULL: none) runs after each unit compiled +// (basCompilerSetUnitFn), for a caller that lets other work go on. NULL +// on failure, with *err telling why. +BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit, const BasCtrlValidatorT *validator, const BasCompilerT **liveCompiler, BasCompileErrorT *err, void (*unitFn)(void *ctx), void *unitCtx); // A compiler message without its "Line N, Col M: " prefix. const char *basBuildErrorText(const char *message); diff --git a/src/apps/kpunch/dvxbasic/basdev/basDebug.c b/src/apps/kpunch/dvxbasic/basdev/basDebug.c index 8f9ba16e..ec89d604 100644 --- a/src/apps/kpunch/dvxbasic/basdev/basDebug.c +++ b/src/apps/kpunch/dvxbasic/basdev/basDebug.c @@ -63,12 +63,12 @@ typedef struct { } DbgCaptureT; // Prototypes (alphabetical) -static bool assignSlot(BasVmT *vm, BasValueT *slot, const char *lhs, int32_t lhsLen, const char *rhs, BasPrintFnT printFn, void *ctx); +static bool assignSlot(BasVmT *vm, BasValueT *slot, const char *lhs, int32_t lhsLen, const char *rhs, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump); int32_t basDbgAppendDropping(char *buf, int32_t cap, int32_t len, const char *text, int32_t textLen); int32_t basDbgCallStack(BasVmT *vm, int32_t curLine, BasDbgRowFnT fn, void *ctx); int32_t basDbgCurProcIdx(const BasVmT *vm); -bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize); -bool basDbgExec(BasVmT *pausedVm, const char *line, BasPrintFnT printFn, void *ctx); +bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize, const BasDbgPumpT *pump); +bool basDbgExec(BasVmT *pausedVm, const char *line, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump); const BasDebugVarT *basDbgFindVar(const BasVmT *vm, const char *name); void basDbgFormatValue(const BasVmT *vm, const BasValueT *v, char *buf, int32_t bufSize); const char *basDbgInstanceTypeName(const BasVmT *vm, const BasObjectT *obj); @@ -76,14 +76,14 @@ bool basDbgIsMangledName(const char *name); int32_t basDbgLocals(BasVmT *vm, BasDbgRowFnT fn, void *ctx); const char *basDbgProcNameForPc(const BasVmT *vm, int32_t pc); BasValueT *basDbgResolvePath(BasVmT *vm, const char *path, const char **endPtr); -BasVmResultE basDbgRunSnippet(BasVmT *vm); -bool basDbgSetVar(BasVmT *pausedVm, const char *path, const char *expr, BasPrintFnT printFn, void *ctx); +BasVmResultE basDbgRunSnippet(BasVmT *vm, const BasDbgPumpT *pump); +bool basDbgSetVar(BasVmT *pausedVm, const char *path, const char *expr, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump); BasVmT *basDbgSnippetVm(BasVmT *pausedVm, const char *src, BasModuleT **outMod, char *err, int32_t errSize, const char *captureName, int32_t *outCaptureIdx); const char *basDbgTypeName(uint8_t dt); BasValueT *basDbgVarSlot(BasVmT *vm, const BasDebugVarT *dv); bool basDbgVarVisible(const BasVmT *vm, const BasDebugVarT *dv, int32_t curProcIdx); static void capturePrint(void *ctx, const char *text, bool newline); -static bool evalRhs(BasVmT *vm, const char *rhs, uint8_t targetType, BasValueT *outVal, BasPrintFnT printFn, void *ctx); +static bool evalRhs(BasVmT *vm, const char *rhs, uint8_t targetType, BasValueT *outVal, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump); static int32_t findMemberVarIdx(const BasVmT *vm, const BasObjectT *obj, const char *name); static int32_t findUdtFieldIdx(const BasModuleT *mod, int32_t typeId, const char *fieldName); static bool isPaused(const BasVmT *vm); @@ -94,12 +94,12 @@ static const char *parseIndexList(const char *p, int32_t *indices, int32_t *out static void printError(BasPrintFnT printFn, void *ctx, const char *msg); static int32_t procIndexForPc(const BasVmT *vm, int32_t pc); static void reportSnippetResult(BasVmT *vm, BasVmResultE result, BasPrintFnT printFn, void *ctx); -static bool tryAssign(BasVmT *vm, const char *line, BasPrintFnT printFn, void *ctx, bool *assigned); +static bool tryAssign(BasVmT *vm, const char *line, BasPrintFnT printFn, void *ctx, bool *assigned, const BasDbgPumpT *pump); // Evaluate rhs as a value of slot's type and store it there. lhs (lhsLen // bytes) names the variable in the confirmation. -static bool assignSlot(BasVmT *vm, BasValueT *slot, const char *lhs, int32_t lhsLen, const char *rhs, BasPrintFnT printFn, void *ctx) { +static bool assignSlot(BasVmT *vm, BasValueT *slot, const char *lhs, int32_t lhsLen, const char *rhs, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump) { // Only scalars can be assigned; the RHS is evaluated as an expression bool scalar = slot->type == BAS_TYPE_INTEGER || slot->type == BAS_TYPE_LONG || slot->type == BAS_TYPE_SINGLE || slot->type == BAS_TYPE_DOUBLE || slot->type == BAS_TYPE_STRING || slot->type == BAS_TYPE_BOOLEAN || slot->type == BAS_TYPE_DATE || slot->type == BAS_TYPE_BYTE || slot->type == BAS_TYPE_CURRENCY; @@ -110,7 +110,7 @@ static bool assignSlot(BasVmT *vm, BasValueT *slot, const char *lhs, int32_t lhs BasValueT newVal; - if (!evalRhs(vm, rhs, slot->type, &newVal, printFn, ctx)) { + if (!evalRhs(vm, rhs, slot->type, &newVal, printFn, ctx, pump)) { return false; } @@ -197,7 +197,7 @@ int32_t basDbgCurProcIdx(const BasVmT *vm) { } -bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize) { +bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize, const BasDbgPumpT *pump) { const char *end = NULL; BasValueT *slot = isPaused(pausedVm) ? basDbgResolvePath(pausedVm, dvxSkipWs(expr), &end) : NULL; @@ -231,7 +231,7 @@ bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize) cap.len = 0; out[0] = '\0'; basVmSetPrintCallback(tvm, capturePrint, &cap); - basDbgRunSnippet(tvm); + basDbgRunSnippet(tvm, pump); // Strip the trailing newline if (cap.len > 0 && out[cap.len - 1] == '\n') { @@ -244,10 +244,10 @@ bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize) } -bool basDbgExec(BasVmT *pausedVm, const char *line, BasPrintFnT printFn, void *ctx) { +bool basDbgExec(BasVmT *pausedVm, const char *line, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump) { bool assigned = false; - if (!line || *line == '\0' || tryAssign(pausedVm, line, printFn, ctx, &assigned)) { + if (!line || *line == '\0' || tryAssign(pausedVm, line, printFn, ctx, &assigned, pump)) { return assigned; } @@ -270,7 +270,7 @@ bool basDbgExec(BasVmT *pausedVm, const char *line, BasPrintFnT printFn, void *c } basVmSetPrintCallback(vm, printFn, ctx); - reportSnippetResult(vm, basDbgRunSnippet(vm), printFn, ctx); + reportSnippetResult(vm, basDbgRunSnippet(vm, pump), printFn, ctx); basVmDestroy(vm); basModuleFree(mod); return false; @@ -298,12 +298,13 @@ const BasDebugVarT *basDbgFindVar(const BasVmT *vm, const char *name) { void basDbgFormatValue(const BasVmT *vm, const BasValueT *v, char *buf, int32_t bufSize) { switch (v->type) { + // Numbers as PRINT and CStr show them case BAS_TYPE_INTEGER: case BAS_TYPE_BYTE: - snprintf(buf, bufSize, "%d", (int)v->intVal); - break; - - case BAS_TYPE_CURRENCY: { + case BAS_TYPE_CURRENCY: + case BAS_TYPE_LONG: + case BAS_TYPE_SINGLE: + case BAS_TYPE_DOUBLE: { BasStringT *text = basValFormatString(*v); snprintf(buf, bufSize, "%s", text->data); @@ -311,18 +312,6 @@ void basDbgFormatValue(const BasVmT *vm, const BasValueT *v, char *buf, int32_t break; } - case BAS_TYPE_LONG: - snprintf(buf, bufSize, "%ld", (long)v->longVal); - break; - - case BAS_TYPE_SINGLE: - snprintf(buf, bufSize, "%.6g", (double)v->sngVal); - break; - - case BAS_TYPE_DOUBLE: - snprintf(buf, bufSize, "%.10g", v->dblVal); - break; - case BAS_TYPE_BOOLEAN: snprintf(buf, bufSize, "%s", v->boolVal ? "True" : "False"); break; @@ -584,12 +573,16 @@ BasValueT *basDbgResolvePath(BasVmT *vm, const char *path, const char **endPtr) } -BasVmResultE basDbgRunSnippet(BasVmT *vm) { +BasVmResultE basDbgRunSnippet(BasVmT *vm, const BasDbgPumpT *pump) { BasVmResultE result = BAS_VM_STEP_LIMIT; basVmSetStepLimit(vm, DBG_SNIPPET_SLICE); for (int32_t slice = 0; slice < DBG_SNIPPET_SLICES && result == BAS_VM_STEP_LIMIT; slice++) { + if (slice > 0 && pump && pump->fn) { + pump->fn(pump->ctx); + } + result = basVmRun(vm); } @@ -597,7 +590,7 @@ BasVmResultE basDbgRunSnippet(BasVmT *vm) { } -bool basDbgSetVar(BasVmT *pausedVm, const char *path, const char *expr, BasPrintFnT printFn, void *ctx) { +bool basDbgSetVar(BasVmT *pausedVm, const char *path, const char *expr, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump) { const char *start = dvxSkipWs(path); const char *end = NULL; BasValueT *slot = isPaused(pausedVm) ? basDbgResolvePath(pausedVm, start, &end) : NULL; @@ -607,7 +600,7 @@ bool basDbgSetVar(BasVmT *pausedVm, const char *path, const char *expr, BasPrint return false; } - return assignSlot(pausedVm, slot, start, (int32_t)(end - start), expr, printFn, ctx); + return assignSlot(pausedVm, slot, start, (int32_t)(end - start), expr, printFn, ctx, pump); } @@ -811,7 +804,7 @@ static void capturePrint(void *ctx, const char *text, bool newline) { // Evaluate the right-hand side of an assignment as a BASIC expression // (it may use the paused program's variables) converted to targetType. // Reports an error to printFn and returns false. -static bool evalRhs(BasVmT *vm, const char *rhs, uint8_t targetType, BasValueT *outVal, BasPrintFnT printFn, void *ctx) { +static bool evalRhs(BasVmT *vm, const char *rhs, uint8_t targetType, BasValueT *outVal, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump) { char src[DBG_RHS_SRC_LEN]; snprintf(src, sizeof(src), "%s %s AS %s: %s = %s", DBG_KW_DIM, DBG_RHS_VAR, basDbgTypeName(targetType), DBG_RHS_VAR, rhs); @@ -826,7 +819,7 @@ static bool evalRhs(BasVmT *vm, const char *rhs, uint8_t targetType, BasValueT * return false; } - BasVmResultE result = basDbgRunSnippet(svm); + BasVmResultE result = basDbgRunSnippet(svm, pump); bool ok = (result == BAS_VM_HALTED || result == BAS_VM_OK) && idx >= 0 && idx < BAS_VM_MAX_GLOBALS; if (ok) { @@ -1002,7 +995,7 @@ static void reportSnippetResult(BasVmT *vm, BasVmResultE result, BasPrintFnT pri // "var = expr" (LET optional) when paused: assigns to the program's // variable. Returns true when line was handled as an assignment (*assigned // tells whether it succeeded); false lets the caller run it as a snippet. -static bool tryAssign(BasVmT *vm, const char *line, BasPrintFnT printFn, void *ctx, bool *assigned) { +static bool tryAssign(BasVmT *vm, const char *line, BasPrintFnT printFn, void *ctx, bool *assigned, const BasDbgPumpT *pump) { if (!isPaused(vm)) { return false; } @@ -1034,6 +1027,6 @@ static bool tryAssign(BasVmT *vm, const char *line, BasPrintFnT printFn, void *c return false; } - *assigned = assignSlot(vm, slot, p, (int32_t)(afterLhs - p), rhs, printFn, ctx); + *assigned = assignSlot(vm, slot, p, (int32_t)(afterLhs - p), rhs, printFn, ctx, pump); return true; } diff --git a/src/apps/kpunch/dvxbasic/basdev/basDebug.h b/src/apps/kpunch/dvxbasic/basdev/basDebug.h index 652b0ffd..ebb5047d 100644 --- a/src/apps/kpunch/dvxbasic/basdev/basDebug.h +++ b/src/apps/kpunch/dvxbasic/basdev/basDebug.h @@ -75,18 +75,26 @@ int32_t basDbgCallStack(BasVmT *vm, int32_t curLine, BasDbgRowFnT fn, void *ctx) // The compiled procedure the paused vm is in, or -1 at module level. int32_t basDbgCurProcIdx(const BasVmT *vm); +// What a snippet calls between its slices (basDbgRunSnippet), so a long +// one lets other work go on: fn(ctx). The calls below take one (NULL: +// none). +typedef struct { + void (*fn)(void *ctx); + void *ctx; +} BasDbgPumpT; + // A watch: a variable path is read directly, anything else is evaluated // as an expression in the paused program (pausedVm NULL: fails). out // receives the value text; false (out undefined) when it cannot be // evaluated. -bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize); +bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize, const BasDbgPumpT *pump); // An Immediate-window line: "var = expr" (LET optional) assigns to the // paused program's variable; a line starting with PRINT, DIM or LET runs // as it is; anything else is printed (PRINT expr). Everything it shows // -- the output, "name = value" after an assignment, "Error: ..." -- goes // to printFn. True when a variable of the program was changed. -bool basDbgExec(BasVmT *pausedVm, const char *line, BasPrintFnT printFn, void *ctx); +bool basDbgExec(BasVmT *pausedVm, const char *line, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump); // The variable called name that is visible where vm is paused, or NULL. const BasDebugVarT *basDbgFindVar(const BasVmT *vm, const char *name); @@ -117,13 +125,14 @@ const char *basDbgProcNameForPc(const BasVmT *vm, int32_t pc); BasValueT *basDbgResolvePath(BasVmT *vm, const char *path, const char **endPtr); // Run a snippet VM (basDbgSnippetVm) in bounded slices, so an endless -// loop cannot hang the host. BAS_VM_STEP_LIMIT means it was abandoned. -BasVmResultE basDbgRunSnippet(BasVmT *vm); +// loop cannot hang the host, with pump between them. BAS_VM_STEP_LIMIT +// means it was abandoned. +BasVmResultE basDbgRunSnippet(BasVmT *vm, const BasDbgPumpT *pump); // Set variable path (see basDbgResolvePath) of the paused program to the // value of expr, converted to its type (scalars only). printFn receives // "path = value" or why it failed. True when it was set. -bool basDbgSetVar(BasVmT *pausedVm, const char *path, const char *expr, BasPrintFnT printFn, void *ctx); +bool basDbgSetVar(BasVmT *pausedVm, const char *path, const char *expr, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump); // Compile src as a snippet and return a VM ready to run it; with a // paused VM, its visible variables are the snippet's globals, holding diff --git a/src/apps/kpunch/dvxbasic/basdev/basDevBas.c b/src/apps/kpunch/dvxbasic/basdev/basDevBas.c index b72c2d5b..2ffedb38 100644 --- a/src/apps/kpunch/dvxbasic/basdev/basDevBas.c +++ b/src/apps/kpunch/dvxbasic/basdev/basDevBas.c @@ -74,18 +74,19 @@ DVX_ABI_STAMP(basdev); -#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup" -#define DEV_OUTPUT_MAX 32768 // PRINT text kept for RunOutput$ (oldest lines dropped) -#define DEV_MESSAGE_LEN (BAS_DBG_VALUE_LEN * 2) -#define DEV_INPUT_PROMPT "Enter value:" -#define DEV_KIND_SUB "Sub" -#define DEV_KIND_FUNCTION "Function" -#define DEV_LINE_SEPS " ,\t\r\n" // between DbgSetBreakpoints line numbers -#define DEV_DECIMAL 10 -#define DEV_KIND_AUTO (-1) // CompSourceAdd: the kind follows the file name -#define DEV_NO_INDEX (-1) // no such keyword / file -#define DEV_TOKEN_ROW_MAX (BAS_MAX_TOKEN_LEN + DEV_MESSAGE_LEN) // one CompTokens$ row -#define DEV_DESIGN_BUSY "The design UserControls' code is running; try again when it is done." +#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup" +#define DEV_OUTPUT_MAX 32768 // PRINT text kept for RunOutput$ (oldest lines dropped) +#define DEV_MESSAGE_LEN (BAS_DBG_VALUE_LEN * 2) +#define DEV_INPUT_PROMPT "Enter value:" +#define DEV_KIND_SUB "Sub" +#define DEV_KIND_FUNCTION "Function" +#define DEV_LINE_SEPS " ,\t\r\n" // between DbgSetBreakpoints line numbers +#define DEV_DECIMAL 10 +#define DEV_KIND_AUTO (-1) // CompSourceAdd: the kind follows the file name +#define DEV_NO_INDEX (-1) // no such keyword / file +#define DEV_TOKEN_ROW_MAX (BAS_MAX_TOKEN_LEN + DEV_MESSAGE_LEN) // one CompTokens$ row +#define DEV_YIELD_MS 50 // a compile or a debugger snippet lets the other tasks run this often (devYield) +#define DEV_DESIGN_BUSY "The design UserControls' code is running; try again when it is done." // The states RunState reports (debug.bas RUN_*) #define RUN_READY 0 @@ -176,15 +177,16 @@ typedef void (*DevCleanupRegFnT)(DevCleanupFnT fn); // Module state // ============================================================ -static DevHandleT *sHandles = NULL; // stb_ds; handle = index + 1 -static bool sCleanupHooked = false; -static BasTextT sText; // the last string result -static char *sCompError = NULL; // stb_ds: why the last Comp* call failed -static int32_t sCompErrorLine = 0; // concatenated line of the last compile error -static int32_t sCompErrorCol = 0; -static int32_t sCompFileLine = 0; // the same line in its file -static char sCompErrorFile[DVX_MAX_PATH]; -static char sDbgMessage[DEV_MESSAGE_LEN]; // the last Dbg* / Run* message +static DevHandleT *sHandles = NULL; // stb_ds; handle = index + 1 +static bool sCleanupHooked = false; +static BasTextT sText; // the last string result +static char *sCompError = NULL; // stb_ds: why the last Comp* call failed +static int32_t sCompErrorLine = 0; // concatenated line of the last compile error +static int32_t sCompErrorCol = 0; +static int32_t sCompFileLine = 0; // the same line in its file +static char sCompErrorFile[DVX_MAX_PATH]; +static PlatformTicksT sYieldAt = 0; // when devYield last let the other tasks run +static char sDbgMessage[DEV_MESSAGE_LEN]; // the last Dbg* / Run* message // ============================================================ // Prototypes @@ -253,6 +255,7 @@ static bool devInput(void *ctx, const char *prompt, char *buf, int32_t bu static void devOnBreak(void *ctx, int32_t line); static void devPrint(void *ctx, const char *text, bool newline); static const char *devTitle(const DevSourceT *s); +static void devYield(void *ctx); static int32_t handleNew(DevKindE kind, void *obj); static void *handleOf(int32_t handle, DevKindE kind); static void handleRelease(int32_t idx); @@ -303,6 +306,9 @@ static void sweepDeferred(void); static void tokenRow(void *ctx, BasSrcKindE kind, int32_t line, int32_t col, int32_t pos, int32_t len); static void withEventsRow(void *ctx, const char *name, const char *className); +// The debugger's snippets let the other tasks run (devYield) +static const BasDbgPumpT sDevPump = { devYield, NULL }; + // Shell app-cleanup hook: free what a program that ended left open (its // code has unwound, so nothing of it is on the stack). Its runtime let @@ -465,7 +471,7 @@ int32_t CompCompile(int32_t src, int32_t optionExplicit, int32_t validate) { } BasCompileErrorT err; - BasModuleT *mod = basBuildCompile(s->ps.source, s->ps.sourceLen, optionExplicit || s->ps.optionExplicit, validate ? &cb : NULL, &v.compiler, &err); + BasModuleT *mod = basBuildCompile(s->ps.source, s->ps.sourceLen, optionExplicit || s->ps.optionExplicit, validate ? &cb : NULL, &v.compiler, &err, devYield, NULL); basValidateFree(&v); @@ -907,7 +913,7 @@ const char *DbgEval(int32_t run, const char *expr) { return ""; } - return basTextSet(&sText, basDbgEval(r->rt->vm, expr, out, sizeof(out)) ? out : BAS_DBG_EVAL_ERROR); + return basTextSet(&sText, basDbgEval(r->rt->vm, expr, out, sizeof(out), &sDevPump) ? out : BAS_DBG_EVAL_ERROR); } @@ -920,7 +926,7 @@ const char *DbgExec(int32_t run, const char *line) { cap.cap = sizeof(out); cap.len = 0; out[0] = '\0'; - basDbgExec(r ? r->rt->vm : NULL, line ? line : "", collectPrint, &cap); + basDbgExec(r ? r->rt->vm : NULL, line ? line : "", collectPrint, &cap, &sDevPump); return basTextSet(&sText, out); } @@ -1033,7 +1039,7 @@ int32_t DbgSetVar(int32_t run, const char *path, const char *expr) { cap.len = 0; sDbgMessage[0] = '\0'; - bool ok = basDbgSetVar(r ? r->rt->vm : NULL, path ? path : "", expr ? expr : "", collectPrint, &cap); + bool ok = basDbgSetVar(r ? r->rt->vm : NULL, path ? path : "", expr ? expr : "", collectPrint, &cap, &sDevPump); dvxTrimRight(sDbgMessage); return ok ? BAS_TRUE : BAS_FALSE; @@ -1111,12 +1117,12 @@ static bool devDoEvents(void *ctx) { return false; } - if (run->parentRt && run->parentRt->eventDepth == 0 && !basFormRtKeepsRunning(run->parentRt)) { + if (run->parentRt && !basFormRtCodeOnStack(run->parentRt) && !basFormRtKeepsRunning(run->parentRt)) { return false; } run->busy++; - bool ok = dvxUpdate(ac); + bool ok = basFormRtUpdate(run->rt); run->busy--; return ok && !run->stopRequested; @@ -1171,6 +1177,19 @@ static const char *devTitle(const DevSourceT *s) { } +// basBuildCompile's unit callback in CompCompile and the debugger's +// snippet pump: every DEV_YIELD_MS the other tasks run (the program's own +// events wait, so its sources and its paused run stay as they are). +static void devYield(void *ctx) { + (void)ctx; + + if (platformClock() - sYieldAt >= PLATFORM_MS_TO_TICKS(DEV_YIELD_MS)) { + basFormRtYield(); + sYieldAt = platformClock(); + } +} + + static int32_t handleNew(DevKindE kind, void *obj) { if (!sCleanupHooked) { DevCleanupRegFnT reg = (DevCleanupRegFnT)dlsym(NULL, DVX_SYM(SHELL_CLEANUP_SYM)); @@ -1789,7 +1808,7 @@ void RunFree(int32_t run) { // Whether the child's code is on the stack (a handler of it, or a call // of this library into it). static bool runOnStack(const DevRunT *run) { - return run->busy > 0 || (run->rt && run->rt->eventDepth > 0); + return run->busy > 0 || (run->rt && basFormRtCodeOnStack(run->rt)); } diff --git a/src/apps/kpunch/dvxbasic/basdev/basHelpMap.c b/src/apps/kpunch/dvxbasic/basdev/basHelpMap.c index a826eaf1..5fcef13d 100644 --- a/src/apps/kpunch/dvxbasic/basdev/basHelpMap.c +++ b/src/apps/kpunch/dvxbasic/basdev/basHelpMap.c @@ -161,8 +161,11 @@ static const BasHelpMapEntryT sHelpMap[] = { // String functions {"Asc", "lang.func.string"}, + {"AscW", "lang.func.string"}, {"Chr", "lang.func.string"}, {"Chr$", "lang.func.string"}, + {"ChrW", "lang.func.string"}, + {"ChrW$", "lang.func.string"}, {"Environ", "lang.func.string"}, {"Environ$", "lang.func.string"}, {"Format", "lang.func.string"}, @@ -224,6 +227,7 @@ static const BasHelpMapEntryT sHelpMap[] = { {"InputBox", "lang.func.misc"}, {"InputBox$", "lang.func.misc"}, {"LoadPicture", "lang.func.misc"}, + {"LoadPictureData", "lang.func.misc"}, {"MsgBox", "lang.func.misc"}, // SQL functions @@ -232,12 +236,18 @@ static const BasHelpMapEntryT sHelpMap[] = { {"SQLEof", "lang.sql"}, {"SQLError", "lang.sql"}, {"SQLError$", "lang.sql"}, + {"SQLEscape", "lang.sql"}, + {"SQLEscape$", "lang.sql"}, {"SQLExec", "lang.sql"}, {"SQLField", "lang.sql"}, {"SQLField$", "lang.sql"}, {"SQLFieldCount", "lang.sql"}, {"SQLFieldDbl", "lang.sql"}, {"SQLFieldInt", "lang.sql"}, + {"SQLFieldName", "lang.sql"}, + {"SQLFieldName$", "lang.sql"}, + {"SQLFieldText", "lang.sql"}, + {"SQLFieldText$", "lang.sql"}, {"SQLFreeResult", "lang.sql"}, {"SQLNext", "lang.sql"}, {"SQLOpen", "lang.sql"}, diff --git a/src/apps/kpunch/dvxbasic/basdev/basdev.dhs b/src/apps/kpunch/dvxbasic/basdev/basdev.dhs index ea9c0bf8..66e210b7 100644 --- a/src/apps/kpunch/dvxbasic/basdev/basdev.dhs +++ b/src/apps/kpunch/dvxbasic/basdev/basdev.dhs @@ -46,9 +46,9 @@ .h1 BASIC Development Library -basdev.lib is the DVX BASIC compiler and what a development tool builds on it: the build pipeline (basBuild.h), the compile-time Control.Member validator (basValidate.h), the debugger's view of a paused program (basDebug.h), and the SUB / FUNCTION scanner (basProcs.h). The DVX BASIC IDE uses it, and BASIC programs reach it through compiler.bas and debug.bas (basDevBas.c holds their entry points). It also carries the STUB and noicon resources every build inside DVX uses (BAS_BUILD_STUB_CARRIER). It needs basrt.lib, which holds the run host (basRunHost.h) the IDE, the standalone stub, and debug.bas share. +basdev.lib is the DVX BASIC compiler and what a development tool builds on it: the build pipeline (basBuild.h), the compile-time Control.Member validator (basValidate.h), the debugger's view of a paused program (basDebug.h), the SUB / FUNCTION scanner (basProcs.h), and the .frm model (frmModel.h) the IDE's designer works on. The DVX BASIC IDE uses it, and BASIC programs reach it through compiler.bas, debug.bas, and designer.bas's Frm* functions (basDevBas.c and frmModelBas.c hold their entry points). It also carries the STUB and noicon resources every build inside DVX uses (BAS_BUILD_STUB_CARRIER). It needs basrt.lib, which holds the run host (basRunHost.h) the IDE, the standalone stub, and debug.bas share. -Headers: apps/dvxbasic/basBuild.h, apps/dvxbasic/basdev/basValidate.h, apps/dvxbasic/basdev/basDebug.h, apps/dvxbasic/basdev/basProcs.h, apps/dvxbasic/basdev/basSource.h, apps/dvxbasic/basdev/basHelpMap.h, apps/dvxbasic/basdev/basProject.h, apps/dvxbasic/formrt/basRunHost.h +Headers: apps/dvxbasic/basBuild.h, apps/dvxbasic/basdev/basValidate.h, apps/dvxbasic/basdev/basDebug.h, apps/dvxbasic/basdev/basProcs.h, apps/dvxbasic/basdev/basSource.h, apps/dvxbasic/basdev/basHelpMap.h, apps/dvxbasic/basdev/basProject.h, apps/dvxbasic/basdev/frmModel.h, apps/dvxbasic/formrt/basRunHost.h .h2 Compiling (basBuild.h) @@ -57,7 +57,7 @@ Headers: apps/dvxbasic/basBuild.h, apps/dvxbasic/basdev/basValidate.h, apps/dvxb -------- ----------- basBuildAddSource Append one file to a BasProjectSourceT the way a project is built: a module in BEGINMODULE, a class module's code in BEGINCLASS, a UserControl's code in BEGINCLASS ... USERCONTROL (its header is kept in ctlTexts), a form's code section in BEGINFORM with its controls (the .frm text is kept in frmTexts), a lone program unwrapped. Records the file in the source map. basBuildConcatProject Read every file an open .dbp names (modules, class modules, UserControls, forms) into a BasProjectSourceT through basBuildAddSource. - basBuildCompile Compile source into a BasModuleT with OPTION EXPLICIT and an optional BasCtrlValidatorT. A BasCompileErrorT says why it failed: BAS_COMPILE_ERROR (line, column, message), BAS_COMPILE_NO_MEMORY, or BAS_COMPILE_NO_MODULE. + basBuildCompile Compile source into a BasModuleT with OPTION EXPLICIT and an optional BasCtrlValidatorT. A BasCompileErrorT says why it failed: BAS_COMPILE_ERROR (line, column, message), BAS_COMPILE_NO_MEMORY, or BAS_COMPILE_NO_MODULE. An optional unit callback runs after each statement or procedure compiled (basCompilerSetUnitFn), so a long compile can let other work go on. basBuildErrorText A compiler message without its "Line N, Col M: " prefix. basBuildMapLine Line of the concatenated source to file and file line. basBuildMapFileLine File and file line to the line of the concatenated source (names compared as DOS does: any case, / and \ alike). @@ -74,7 +74,7 @@ A BasValidatorT (zeroed) collects the forms, controls, and menu items of the .fr .h2 Debugging (basDebug.h) -Every function takes the paused program's VM (or NULL when there is none). +Every function takes the paused program's VM (or NULL when there is none). The ones that run a snippet (basDbgEval, basDbgExec, basDbgSetVar, basDbgRunSnippet) take a BasDbgPumpT too (NULL for none): its fn(ctx) runs between the snippet's slices, so a long snippet lets other work go on (the IDE shows a frame with input held off; compiler.bas lets the other tasks run). .table Function Description @@ -116,7 +116,7 @@ A UserControl's header (the Begin UserControl block of a .ctl) is read and chang .h2 Running a Program (basRunHost.h, in basrt.lib) -basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path, App.User (from the program's file: its folder in the user folder), App.Data (the DATA folder beside it), COMMAND$, the print, input, and DoEvents callbacks, the screen window's name, the help file, whether runtime errors show their own box, and whether it is a standalone program (the stub's own: the runtime owns the module, frees it with itself, and is ended through the shell's kill hook, shellRegisterAppKill, when the program is ended by force; the IDE and basdev end their runtimes themselves). basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused, and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs. +basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path, App.User (from the program's file: its folder in the user folder), App.Data (the DATA folder beside it), COMMAND$, the print, input, and DoEvents callbacks, the screen window's name, the help file, whether runtime errors show their own box, whether it is a standalone program (the stub's own: the runtime owns the module and frees it with itself), and whether it ends with its program (endWithApp: the IDE's runs, on a task of their own). A standalone runtime and one made with endWithApp are ended through the shell's kill hook, shellRegisterAppKill, when the program they run for is ended by force; basdev ends its runs itself. A runtime made on one task and run on another (the IDE makes it in a menu command and runs it on a work task, shellAppTaskStart) is moved with basFormRtAdopt, and basFormRtYield lets the other tasks run once without running any of the program's events. basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused, and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs. VM accessors a debugger host uses (vm.h): basVmSetAppPaths, basVmSetBreakpointCallback, basVmSetDebugPaused, basVmResume, basVmStop, basVmClearStepState, basVmGetCurrentLine, basVmGetErrorLine. @@ -125,3 +125,144 @@ VM accessors a debugger host uses (vm.h): basVmSetAppPaths, basVmSetBreakpointCa compiler.bas and debug.bas declare the entry points (see Include Libraries in the BASIC language reference). Source sets, modules, and runs are handles freed with their program. A run is polled: RunPoll runs the child for a bounded time and never waits. A pause in the child's module code returns RUN_PAUSED; a pause inside one of its event handlers (or its forms' Load code, inside RunStart) waits in place while the child pumps DVX events, so the caller's Timer and menus can step, continue, or stop it; RunPoll called from there returns at once. RunFree while the child's code is on the stack stops it at once and releases it at the next library call after that code has unwound. CompDesignControls registers a source set's UserControls for the calling program's design forms (basCtlLoadDesign with its runtime, the current one: a child run's own when the child calls it) and holds the module -- one of the caller's own handles; another program's is refused -- until CompDesignRelease, the next CompDesignControls, or the end of that runtime (appEnded for a program, runDestroyHost for a child run), CompModuleFree meanwhile only marking it. + +.link lib.basdev.frmmodel The .frm Model (frmModel.h) + +.topic lib.basdev.frmmodel +.title The .frm Model +.toc 1 The .frm Model +.index FrmModelT +.index FrmCtrlT +.index FrmMenuItemT +.index frmModelLoad +.index frmModelWrite +.index frmModelText +.index frmModelFromForm +.index basFontChoiceOf +.index basFontPropText +.index Font properties (C) + +.h1 The .frm Model + +A form file as data, with no widgets and no windows: the form's properties, its controls (type, name, control array index, parent, the built-in size and state fields, and every other "Key = Value" pair as text), its menu items, and its code section. frmModelLoad reads .frm text (through frmParse) and frmModelWrite writes the canonical text the DVX BASIC IDE saves. The IDE's designer, properties window, menu editor, and copy and paste all work on this model and keep only their live widgets on the side (each control's userData); designer.bas binds it for BASIC programs (the Frm* functions, basdev/frmModelBas.c). + +Header: apps/dvxbasic/basdev/frmModel.h (part of basdev.lib) + +.h2 Types + +.table + Type Description + ---- ----------- + FrmModelT A form: name, caption, width / height / left / top, padding, spacing, windowState, layout, helpTopic, contextMenu, centered / autoSize / resizable, controls (stb_ds array of FrmCtrlT pointers in tree order: each control directly followed by its children), menuItems (stb_ds array), props (other runtime form properties that were set: Icon, Tag, MousePointer, the Font properties ...), code (malloc'd, NULL = none), dirty (changed by an edit since it was made or loaded). + FrmCtrlT A control: name, typeName, parentName + parentIndex (the parent's control array index, so a child of one member of a container array belongs to that member), index (FRM_NO_INDEX: not in an array), left, top, width (MinWidth), height (MinHeight), maxWidth, maxHeight, weight, dragMode, helpTopic, visible, enabled, props (stb_ds array of FrmPropT: every other key, unquoted), userData (the caller's; the model never reads it and loads, copies, and pastes start it at NULL). + FrmMenuItemT A menu item: caption ("-" for a separator), name, shortcut, shortcutText, level (0: a menu-bar menu or a popup; 1: its items; 2+: submenus), id (an editor identity, see frmModelMenuReplace), checked, checkable (Checked was written), radioCheck, enabled, visible (False on a level-0 item: a popup menu). + FrmPropT One stored "Key = Value": name, value. + FrmIntPropT One built-in integer property (Left, Top, MinWidth alias Width, MinHeight alias Height, MaxWidth, MaxHeight, Weight, DragMode): name, alias, offset into FrmCtrlT, saveRule (FRM_SAVE_ALWAYS / IF_NONZERO / IF_POSITIVE). +.endtable + +Only values somebody set are stored and written: a property a control has by default (a PictureBox's BorderStyle, a TextBox's MaxLength) is not in the model and not in the file. Values are written in the canonical form for their widget interface type, or for a key the widget does not know, for their type in the runtime's common property table (STRING quoted, BOOL True / False, INT a number, FLOAT and ENUM unquoted); other keys are written as quoted strings. The limits come from the parser's: FRM_MAX_NEST_DEPTH control levels, FRM_MAX_MENU_LEVEL menu levels, and no property line longer than FRM_MAX_LINE_LEN - 1 characters (the writer fails with FRM_WRITE_LINE_TOO_LONG rather than write one the parser would cut). + +.h2 Loading and Writing + +.table + Function Description + -------- ----------- + frmModelNew(name) A blank form with the runtime form's defaults. + frmModelLoad(text, len) The model of .frm text; NULL when it is not a form (no Begin Form, frmParse refuses it, two controls share a name and index) or out of memory. A Key = Value line is the innermost open block's, also after the blocks inside it, as the runtime reads it; a child names its container by the container's final Index. Of several OptionButtons of one container saved with Value True, the last keeps it. + frmModelFree(m) Frees the model, its controls, and menus. + frmModelWrite(m, buf, bufSize) The .frm text into buf: its length, or FRM_WRITE_TOO_LARGE / FRM_WRITE_LINE_TOO_LONG (never a partial text). + frmModelWriteSize(m) A buffer size that always holds the text. + frmModelText(m, &len) The text in a new malloc'd string; NULL with len set to the FRM_WRITE_* code. + frmModelSetCode(m, code) Replaces the code section. + frmCodeSection(trailing, len, &outLen) The code section of the text after a form's End (blank lines skipped, the first code line keeps its indent). + frmModelFromForm(rt, form) A model of a loaded form as it is now: its properties, its controls in their containers with each property that differs from a new control of the type (compared with one made in a hidden window; the six Font properties of the form and of each control that has a font of its own), its menus; no code. What FormText$ writes. +.endtable + +.h2 Properties + +.table + Function Description + -------- ----------- + frmModelFormProp(m, name, out, size) A form property as text (an unset design property gives "", "0", or "False"); false for run-time-only ones (Visible, DesignMode, DesignSelection, object properties). + frmModelSetFormProp(m, name, value) Sets one from text; Name renames the form; an empty value takes an optional one out. + frmCtrlPropText(ctrl, name, out, size) A control property as the control holds it (Name, Index, the built-in integers, HelpTopic, Visible / Enabled unless the widget defines them, the stored ones). + frmCtrlSetPropText(ctrl, key, value) Applies one the way the loader does (a VB TabIndex is dropped unless the widget defines it). + frmModelSetCtrlProp(m, ctrl, name, value) The same with an editor's rules: Name renames the control array, Index makes an array member, then the OptionButton group rule. + frmCtrlGetProp / frmCtrlSetProp / frmCtrlRemoveProp The stored properties. + frmIntPropAt / frmIntPropFind / frmIntPropGet / Set The built-in integer table. + frmIfaceProp(typeName, propName) The widget interface property of a type, NULL when none. +.endtable + +.h2 Font Properties + +FontName, FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru (BAS_PROP_FONT*) are in both the form table and the common control table. They are one font, a DvxFontChoiceT (dvxDlg.h: family, size in pixels, FONT_STYLE_* flags), and the widget's font (wgtFont, wgtSetFont) is the only place they live: a read gives the font the object draws with (a form's is its root widget's), a write gives it that font with the one part changed. The .frm loader collects a block's Font properties and opens one font when the block's properties are read, so their order in the file does not matter. Helpers in formrt.h, for loaders and designers that hold them as .frm text: + +.table + Function Description + -------- ----------- + basFontPropAt(idx) The Font property names in .frm order (FontName, FontSize, then the flags), NULL past the end. + basFontPropIs(name) True for a Font property name (any case). + basFontChoiceOf(font, out) A BitmapFontT as the properties name it (dvxFontFamilyOf, pixelSize, style). + basFontPropText(choice, name, out, size) One property of a choice as .frm text (the family, the size, True / False). + basFontPropSetText(choice, name, text) Sets one property of a choice from .frm text. +.endtable + +.h2 Editing Controls + +.table + Function Description + -------- ----------- + frmModelAddCtrl(m, typeName, name, parent) Adds a control as the last child of parent (NULL: the form); name "" or NULL gives a free one (frmModelAutoName); Caption = its name. NULL when the name is taken or parent cannot hold it. + frmModelRemoveCtrl(m, ctrl) Removes ctrl and its subtree. + frmModelRenameCtrl(m, oldName, newName) Renames a whole control array; children and DataSource / MasterSource of a renamed Data control follow. Identifier rules are the caller's. + frmModelMakeArrayMember(m, ctrl, index) Gives a control array index; its children follow. + frmModelMoveCtrl(m, ctrl, before) Moves ctrl with its subtree before a sibling (NULL: after the last). + frmModelReparent(m, ctrl, parent) Makes ctrl the last child of another container (refuses cycles, non-containers, and too deep a nesting). + frmModelSibling(m, idx, dir) The sibling next to a control (+1 after its subtree, -1 before). + frmModelFindCtrl / CtrlPos / FindParent / CtrlDepth Lookups. + frmModelIsDescendant / SubtreeEnd / NormalizeOrder The tree order. + frmModelNameInUse / AutoName / OptionSelected Names and the OptionButton group rule. + frmModelCtrlText(m, ctrl, &len) ctrl and its children as .frm control blocks (a copy for the Clipboard). + frmModelCtrlsFromText(text, len) Copied blocks read into a scratch model. + frmModelInsertTree(m, src, srcRoot, parent, name, index) Moves a subtree out of the scratch model into m: the root takes name / index (NULL: its own name when free, else a free one); a child whose name is taken gets a free one, the members of one array together; children follow their renamed parents. +.endtable + +.h2 Menus + +The frmMenu* functions work on any stb_ds array of FrmMenuItemT (an editor's working copy as well as m->menuItems) and keep the levels a tree. + +.table + Function Description + -------- ----------- + frmMenuItemInit(mi) A blank item (enabled, visible, level 0). + frmMenuInsert(&items, at, level) Inserts a blank item; the level is clamped so the menus stay a tree. + frmMenuRemove(&items, idx) Removes an item with its subtree. + frmMenuMove(&items, idx, dir) Moves an item with its subtree past its previous / next sibling; its new position or FRM_NO_INDEX. + frmMenuIndent(items, idx, delta) Indents / outdents an item with its subtree (at most one level below the item above, not past FRM_MAX_MENU_LEVEL). + frmMenuSubtreeEnd / CheckLevels / StripBlank / AutoName Tree helpers; AutoName makes mnuSep1 ... or "mnu" plus the caption's letters and digits. + frmMenuPropText / frmMenuSetPropText Menu properties as text (Checked makes a check item). + frmModelMenuValidate(m, items, reason, size) The checks before items replace m's menus (levels; every name set, an identifier as the compiler takes one, not the form's or a control's, unique; every Shortcut a key): the first bad item and the reason. + frmModelMenuReplace(m, items) Replaces m's menus by a copy; ContextMenu bindings follow renamed items, matched by id (give every item id = position + 1 before editing). + frmModelSetMenuProp(m, idx, key, value) Sets an item's property; a Name rename refuses a taken name and ContextMenu bindings follow. +.endtable + +.code +// Load a form, rename a control, add a label inside a frame, save. +FrmModelT *m = frmModelLoad(text, len); + +if (m) { + int32_t at = frmModelFindCtrl(m, "Frame1", FRM_NO_INDEX); + FrmCtrlT *frame = (at >= 0) ? m->controls[at] : NULL; + FrmCtrlT *lbl = frmModelAddCtrl(m, "Label", "", frame); + int32_t outLen = 0; + + frmModelRenameCtrl(m, "Command1", "cmdOk"); + frmModelSetCtrlProp(m, lbl, "Caption", "Name:"); + + char *out = frmModelText(m, &outLen); + + // write out[0 .. outLen) to the file + free(out); + frmModelFree(m); +} +.endcode diff --git a/src/apps/kpunch/dvxbasic/formrt/frmModel.c b/src/apps/kpunch/dvxbasic/basdev/frmModel.c similarity index 95% rename from src/apps/kpunch/dvxbasic/formrt/frmModel.c rename to src/apps/kpunch/dvxbasic/basdev/frmModel.c index 5706390d..8998b16e 100644 --- a/src/apps/kpunch/dvxbasic/formrt/frmModel.c +++ b/src/apps/kpunch/dvxbasic/basdev/frmModel.c @@ -31,11 +31,11 @@ // whether it came from a fresh placement, a loaded file or an edit. #include "frmModel.h" +#include "../compiler/lexer.h" #include "dvxApp.h" #include "stb_ds_wrap.h" #include -#include #include #include #include @@ -77,14 +77,6 @@ static const FrmIntPropT sIntProps[] = { #define FRM_INT_PROP_COUNT DVX_ARRAY_LEN(sIntProps) -// Form properties of the runtime's table that are run-time state, never -// part of a .frm (besides the object-typed ones). -static const char *const sRuntimeOnlyFormProps[] = { - BAS_PROP_VISIBLE, - BAS_PROP_DESIGNMODE, - BAS_PROP_DESIGNSELECTION -}; - // ============================================================ // .frm loading context (used by the loadOn* callbacks) // ============================================================ @@ -123,9 +115,8 @@ typedef struct { static void buildPad(int32_t indent, char *pad); static bool canTakeSubtree(const FrmModelT *m, const FrmCtrlT *parent, int32_t height); static bool ctrlsAmbiguous(const FrmModelT *m); -static int32_t emitClampedV(char *buf, int32_t bufSize, int32_t pos, const char *fmt, va_list args); static int32_t emitControlHead(const FrmCtrlT *ctrl, char *buf, int32_t bufSize, int32_t pos, const char *pad); -static int32_t emitIndented(char *buf, int32_t bufSize, int32_t pos, int32_t indent, const char *fmt, ...); +static int32_t emitEnd(char *buf, int32_t bufSize, int32_t pos, int32_t indent); static int32_t emitPropLine(char *buf, int32_t bufSize, int32_t pos, const char *pad, const char *name, uint8_t type, const char *value); static const BasPropDescT *formExtraProp(const char *name); static bool formFieldText(const FrmModelT *m, const char *name, char *out, int32_t outSize); @@ -139,7 +130,6 @@ bool frmCtrlPropText(const FrmCtrlT *ctrl, const char *nam bool frmCtrlRemoveProp(FrmCtrlT *ctrl, const char *name); void frmCtrlSetProp(FrmCtrlT *ctrl, const char *name, const char *value); void frmCtrlSetPropText(FrmCtrlT *ctrl, const char *key, const char *value); -int32_t frmEmitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...); const WgtPropDescT *frmIfaceProp(const char *typeName, const char *propName); const FrmIntPropT *frmIntPropAt(int32_t idx); const FrmIntPropT *frmIntPropFind(const char *name); @@ -171,7 +161,7 @@ bool frmModelIsDescendant(const FrmModelT *m, const FrmCtr FrmModelT *frmModelLoad(const char *text, int32_t len); void frmModelMakeArrayMember(FrmModelT *m, FrmCtrlT *ctrl, int32_t index); bool frmModelMenuReplace(FrmModelT *m, const FrmMenuItemT *items); -int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, FrmIdentFnT isIdent, char *reason, int32_t reasonSize); +int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, char *reason, int32_t reasonSize); bool frmModelMoveCtrl(FrmModelT *m, FrmCtrlT *ctrl, FrmCtrlT *before); bool frmModelNameInUse(const FrmModelT *m, const char *name, const char *exceptName, bool checkMenus); FrmModelT *frmModelNew(const char *name); @@ -260,36 +250,15 @@ static bool ctrlsAmbiguous(const FrmModelT *m) { } -static int32_t emitClampedV(char *buf, int32_t bufSize, int32_t pos, const char *fmt, va_list args) { - if (!buf || pos < 0 || pos >= bufSize) { - return pos; - } - - int32_t avail = bufSize - pos; - int32_t written = (int32_t)vsnprintf(buf + pos, (size_t)avail, fmt, args); - - if (written < 0) { - return pos; - } - - // Cut: vsnprintf wrote avail - 1 characters and the NUL - if (written >= avail) { - return bufSize - 1; - } - - return pos + written; -} - - // "Begin Type Name" and every property line of one control, without the // closing End so the writer can nest the children first. Interface-typed // keys and the runtime's common properties are written by their type; // only other keys go out as quoted strings. static int32_t emitControlHead(const FrmCtrlT *ctrl, char *buf, int32_t bufSize, int32_t pos, const char *pad) { - pos = frmEmitClamped(buf, bufSize, pos, "%sBegin %s %s\n", pad, ctrl->typeName, ctrl->name); + pos = dvxStrAppendfCut(buf, bufSize, pos, "%sBegin %s %s\n", pad, ctrl->typeName, ctrl->name); if (ctrl->index >= 0) { - pos = frmEmitClamped(buf, bufSize, pos, "%s %s = %" PRId32 "\n", pad, BAS_PROP_INDEX, ctrl->index); + pos = dvxStrAppendfCut(buf, bufSize, pos, "%s %s = %" PRId32 "\n", pad, BAS_PROP_INDEX, ctrl->index); } const char *caption = frmCtrlGetProp(ctrl, BAS_PROP_CAPTION); @@ -315,7 +284,7 @@ static int32_t emitControlHead(const FrmCtrlT *ctrl, char *buf, int32_t bufSize, } if (emit) { - pos = frmEmitClamped(buf, bufSize, pos, "%s %s = %" PRId32 "\n", pad, ip->name, val); + pos = dvxStrAppendfCut(buf, bufSize, pos, "%s %s = %" PRId32 "\n", pad, ip->name, val); } } @@ -360,17 +329,12 @@ static int32_t emitControlHead(const FrmCtrlT *ctrl, char *buf, int32_t bufSize, } -// One formatted line indented to an indent-level block. -static int32_t emitIndented(char *buf, int32_t bufSize, int32_t pos, int32_t indent, const char *fmt, ...) { - char pad[FRM_PAD_BUF]; - va_list args; +// The End line closing an indent-level block. +static int32_t emitEnd(char *buf, int32_t bufSize, int32_t pos, int32_t indent) { + char pad[FRM_PAD_BUF]; buildPad(indent, pad); - pos = frmEmitClamped(buf, bufSize, pos, "%s", pad); - va_start(args, fmt); - pos = emitClampedV(buf, bufSize, pos, fmt, args); - va_end(args); - return frmEmitClamped(buf, bufSize, pos, "\n"); + return dvxStrAppendfCut(buf, bufSize, pos, "%s" FRM_KW_END "\n", pad); } @@ -383,17 +347,17 @@ static int32_t emitPropLine(char *buf, int32_t bufSize, int32_t pos, const char switch (type) { case WGT_IFACE_STRING: - end = frmEmitClamped(buf, bufSize, pos, "%s %s = \"%s\"\n", pad, name, value); + end = dvxStrAppendfCut(buf, bufSize, pos, "%s %s = \"%s\"\n", pad, name, value); break; case WGT_IFACE_BOOL: - end = frmEmitClamped(buf, bufSize, pos, "%s %s = %s\n", pad, name, frmParseBool(value) ? FRM_TEXT_TRUE : FRM_TEXT_FALSE); + end = dvxStrAppendfCut(buf, bufSize, pos, "%s %s = %s\n", pad, name, frmParseBool(value) ? FRM_TEXT_TRUE : FRM_TEXT_FALSE); break; case WGT_IFACE_INT: - end = frmEmitClamped(buf, bufSize, pos, "%s %s = %d\n", pad, name, atoi(value)); + end = dvxStrAppendfCut(buf, bufSize, pos, "%s %s = %d\n", pad, name, atoi(value)); break; default: // FLOAT and ENUM: decimal text or the value name, unquoted - end = frmEmitClamped(buf, bufSize, pos, "%s %s = %s\n", pad, name, value); + end = dvxStrAppendfCut(buf, bufSize, pos, "%s %s = %s\n", pad, name, value); break; } @@ -412,16 +376,10 @@ static const BasPropDescT *formExtraProp(const char *name) { const BasPropDescT *pd = basFormRtFindFormProp(name); char dummy[FRM_INT_TEXT_MAX]; - if (!pd || !pd->writable || pd->type == BAS_PROP_OBJECT || formFieldText(NULL, pd->name, dummy, sizeof(dummy))) { + if (!pd || !pd->writable || pd->type == BAS_PROP_OBJECT || basFormRtFormPropRuntimeOnly(pd->name) || formFieldText(NULL, pd->name, dummy, sizeof(dummy))) { return NULL; } - for (int32_t i = 0; i < DVX_ARRAY_LEN(sRuntimeOnlyFormProps); i++) { - if (strcasecmp(pd->name, sRuntimeOnlyFormProps[i]) == 0) { - return NULL; - } - } - return pd; } @@ -691,16 +649,6 @@ void frmCtrlSetPropText(FrmCtrlT *ctrl, const char *key, const char *value) { } -int32_t frmEmitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) { - va_list args; - - va_start(args, fmt); - pos = emitClampedV(buf, bufSize, pos, fmt, args); - va_end(args); - return pos; -} - - const WgtPropDescT *frmIfaceProp(const char *typeName, const char *propName) { return wgtIfaceFindProp(basCtlIface(typeName), propName); } @@ -1478,7 +1426,7 @@ bool frmModelMenuReplace(FrmModelT *m, const FrmMenuItemT *items) { } -int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, FrmIdentFnT isIdent, char *reason, int32_t reasonSize) { +int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, char *reason, int32_t reasonSize) { int32_t count = (int32_t)arrlen(items); int32_t bad = frmMenuCheckLevels(items); @@ -1497,7 +1445,7 @@ int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, FrmI return i; } - if (isIdent && !isIdent(name)) { + if (!basIsValidIdent(name)) { snprintf(reason, reasonSize, "Menu name must be an identifier (letters, digits, underscores): %s", name); return i; } @@ -1902,9 +1850,9 @@ int32_t frmModelWrite(const FrmModelT *m, char *buf, int32_t bufSize) { return FRM_WRITE_TOO_LARGE; } - int32_t pos = frmEmitClamped(buf, bufSize, 0, FRM_VERSION_LINE); + int32_t pos = dvxStrAppendfCut(buf, bufSize, 0, FRM_VERSION_LINE); - pos = frmEmitClamped(buf, bufSize, pos, FRM_KW_BEGIN " " FRM_KW_FORM " %s\n", m->name); + pos = dvxStrAppendfCut(buf, bufSize, pos, FRM_KW_BEGIN " " FRM_KW_FORM " %s\n", m->name); // Form header in the runtime table's order. Name comes from the // Begin line; Left / Top are implied by Centered and Width / Height @@ -1964,13 +1912,13 @@ int32_t frmModelWrite(const FrmModelT *m, char *buf, int32_t bufSize) { pos = writeControls(m, parents, buf, bufSize, pos, NULL, 1); arrfree(parents); - pos = frmEmitClamped(buf, bufSize, pos, FRM_KW_END "\n"); + pos = dvxStrAppendfCut(buf, bufSize, pos, FRM_KW_END "\n"); if (pos < 0) { return pos; } - // frmEmitClamped parks pos at bufSize - 1 once the buffer is full, so + // dvxStrAppendfCut parks pos at bufSize - 1 once the buffer is full, so // reaching it means the form text was cut short if (pos >= bufSize - 1) { return FRM_WRITE_TOO_LARGE; @@ -2099,12 +2047,15 @@ static void loadOnFormEnd(void *userData, const char *trailingSrc, int32_t trail return; } - ctx->m->code = (char *)malloc((size_t)codeLen + 1); + char *code = (char *)malloc((size_t)codeLen + 1); - if (ctx->m->code) { - memcpy(ctx->m->code, codeStart, codeLen); - ctx->m->code[codeLen] = '\0'; + if (code) { + memcpy(code, codeStart, codeLen); + code[codeLen] = '\0'; } + + free(ctx->m->code); + ctx->m->code = code; } @@ -2360,7 +2311,7 @@ static int32_t writeCtrlTree(const FrmModelT *m, FrmCtrlT **parents, const FrmCt pos = writeControls(m, parents, buf, bufSize, pos, ctrl, indent + 1); } - return frmEmitClamped(buf, bufSize, pos, "%s" FRM_KW_END "\n", pad); + return dvxStrAppendfCut(buf, bufSize, pos, "%s" FRM_KW_END "\n", pad); } @@ -2378,11 +2329,11 @@ static int32_t writeMenus(const FrmModelT *m, char *buf, int32_t bufSize, int32_ // Close blocks back to this item's level while (curLevel > mi->level) { curLevel--; - pos = emitIndented(buf, bufSize, pos, curLevel + 1, FRM_KW_END); + pos = emitEnd(buf, bufSize, pos, curLevel + 1); } buildPad(mi->level + 1, pad); - pos = frmEmitClamped(buf, bufSize, pos, "%s" FRM_KW_BEGIN " " FRM_KW_MENU " %s\n", pad, mi->name); + pos = dvxStrAppendfCut(buf, bufSize, pos, "%s" FRM_KW_BEGIN " " FRM_KW_MENU " %s\n", pad, mi->name); pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_CAPTION, WGT_IFACE_STRING, mi->caption); if (mi->checked || mi->checkable) { @@ -2415,7 +2366,7 @@ static int32_t writeMenus(const FrmModelT *m, char *buf, int32_t bufSize, int32_ while (curLevel > 0) { curLevel--; - pos = emitIndented(buf, bufSize, pos, curLevel + 1, FRM_KW_END); + pos = emitEnd(buf, bufSize, pos, curLevel + 1); } return pos; diff --git a/src/apps/kpunch/dvxbasic/formrt/frmModel.h b/src/apps/kpunch/dvxbasic/basdev/frmModel.h similarity index 97% rename from src/apps/kpunch/dvxbasic/formrt/frmModel.h rename to src/apps/kpunch/dvxbasic/basdev/frmModel.h index 4c65319f..8db45830 100644 --- a/src/apps/kpunch/dvxbasic/formrt/frmModel.h +++ b/src/apps/kpunch/dvxbasic/basdev/frmModel.h @@ -20,7 +20,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -// frmModel.h -- the .frm document model (BASIC run-time library) +// frmModel.h -- the .frm document model (BASIC development library) // // A form file as data, with no widgets and no windows: the form's // properties, its controls (type, name, control array index, parent, @@ -44,8 +44,8 @@ #ifndef DVXBASIC_FRMMODEL_H #define DVXBASIC_FRMMODEL_H -#include "formrt.h" -#include "frmParser.h" +#include "../formrt/formrt.h" +#include "../formrt/frmParser.h" #include #include @@ -179,10 +179,6 @@ typedef struct FrmModelT { bool dirty; // changed by an edit operation since it was made or loaded } FrmModelT; -// Identifier rule for names (the compiler's basIsValidIdent): the model -// lives in the run-time library, which has no compiler. -typedef bool (*FrmIdentFnT)(const char *name); - // ============================================================ // Text // ============================================================ @@ -193,10 +189,6 @@ typedef bool (*FrmIdentFnT)(const char *name); // start and length, or NULL when there is no code. const char *frmCodeSection(const char *trailingSrc, int32_t trailingLen, int32_t *outLen); -// Clamped formatted append into a fixed buffer: returns pos unchanged once -// it has reached bufSize, else the new position, capped at bufSize - 1 -// when the text was cut. -int32_t frmEmitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...); // ============================================================ // Model lifecycle and .frm text @@ -346,7 +338,7 @@ void frmModelRemoveCtrl(FrmModelT *m, FrmCtrlT *ctrl); // newName: their children follow, and DataSource / MasterSource that // named a renamed Data control too. False (nothing changed) when there // is no such control or newName is in use. Identifier rules are the -// caller's (FrmIdentFnT). +// caller's (basIsValidIdent). bool frmModelRenameCtrl(FrmModelT *m, const char *oldName, const char *newName); // Give ctrl control array index index (FRM_NO_INDEX: no array); its @@ -457,10 +449,10 @@ bool frmMenuPropText(const FrmMenuItemT *mi, const char *key, char *out, int32_t bool frmMenuSetPropText(FrmMenuItemT *mi, const char *key, const char *value); // Check a menu array before it replaces m's: the levels, and every name -// is set, an identifier (isIdent; NULL skips this), not the form's or a +// is set, an identifier (the compiler's basIsValidIdent), not the form's or a // control's, not used twice; every Shortcut is a key. Returns the first // bad item with the reason in reason, FRM_NO_INDEX when all are good. -int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, FrmIdentFnT isIdent, char *reason, int32_t reasonSize); +int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, char *reason, int32_t reasonSize); // Replace m's menu items by a copy of items. ContextMenu bindings of the // form and its controls follow renamed items, matched by id (see diff --git a/src/apps/kpunch/dvxbasic/formrt/frmModelBas.c b/src/apps/kpunch/dvxbasic/basdev/frmModelBas.c similarity index 93% rename from src/apps/kpunch/dvxbasic/formrt/frmModelBas.c rename to src/apps/kpunch/dvxbasic/basdev/frmModelBas.c index 538e99b4..93b36930 100644 --- a/src/apps/kpunch/dvxbasic/formrt/frmModelBas.c +++ b/src/apps/kpunch/dvxbasic/basdev/frmModelBas.c @@ -24,7 +24,7 @@ // Frm*, FrmCtl*, FrmMenu*, FormText$) and the model of a live form // (frmModelFromForm) // -// Called through DECLARE LIBRARY "basrt", so the signatures use only +// Called through DECLARE LIBRARY "basdev", so the signatures use only // BASIC-marshalable types: strings and LONG in, LONG or STRING out, True // (-1) / False (0). A model is a handle (1, 2, ...; 0 means none) owned // by the program that opened it and freed when that program ends. @@ -39,7 +39,6 @@ #include "stb_ds_wrap.h" #include "dvxBas.h" -#include #include #include #include @@ -50,16 +49,6 @@ #define FRM_VALUE_MAX FRM_MAX_TEXT #define FRM_SCRATCH_SIZE 64 // the hidden window new controls are made in to read their defaults #define FRM_SCRATCH_TITLE "" -#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup" - -typedef void (*FrmCleanupFnT)(int32_t appId); -typedef void (*FrmCleanupRegFnT)(FrmCleanupFnT fn); - -// One open model and the program it belongs to (m NULL: free slot) -typedef struct { - FrmModelT *m; - int32_t appId; -} BasFrmHandleT; // Building the model of a live form typedef struct { @@ -73,15 +62,12 @@ typedef struct { // Module state // ============================================================ -static BasFrmHandleT *sModels = NULL; // stb_ds; handle = index + 1 -static BasTextT sText; // the last string result -static bool sCleanupHooked = false; +static BasTextT sText; // the last string result // ============================================================ // Prototypes // ============================================================ -static void appEnded(int32_t appId); static BasControlT *ctrlForWidget(const BasFormT *form, const WidgetT *w); static FrmCtrlT *ctrlOf(int32_t handle, int32_t i, FrmModelT **outModel); const char *FormText(const char *formName); @@ -129,18 +115,12 @@ static void liveMenu(FrmLiveT *live, const MenuT *menu, int32_t level) static void liveMenuAdd(FrmLiveT *live, const char *name, const char *label, int32_t level); static const char *liveMenuName(const BasFormT *form, const MenuT *menu); static void liveMenus(FrmLiveT *live); +static void modelFree(void *obj); static FrmModelT *modelOf(int32_t handle); static void valueText(BasValueT v, uint8_t type, char *out, int32_t outSize); +static BasHandlesT sModels = BAS_HANDLES(modelFree, 0); // the open models -// Shell app-cleanup hook: free the models a program that ended left open. -static void appEnded(int32_t appId) { - for (int32_t i = 0; i < (int32_t)arrlen(sModels); i++) { - if (sModels[i].m && sModels[i].appId == appId) { - FrmFree(i + 1); - } - } -} static BasControlT *ctrlForWidget(const BasFormT *form, const WidgetT *w) { @@ -369,10 +349,7 @@ const char *FrmCtlType(int32_t handle, int32_t i) { void FrmFree(int32_t handle) { - if (modelOf(handle)) { - frmModelFree(sModels[handle - 1].m); - sModels[handle - 1].m = NULL; - } + basHandleFree(&sModels, handle); } @@ -571,43 +548,16 @@ const char *FrmText(int32_t handle) { } -// A handle for m (freed when the calling program ends), 0 when m is NULL. +// A handle for m (freed when the calling program ends), 0 when m is NULL +// (or, m freed, when out of memory). static int32_t handleNew(FrmModelT *m) { - if (!m) { - return BAS_NO_HANDLE; + int32_t handle = basHandleAdd(&sModels, m); + + if (handle == BAS_NO_HANDLE) { + frmModelFree(m); } - if (!sCleanupHooked) { - FrmCleanupRegFnT reg = (FrmCleanupRegFnT)dlsym(NULL, DVX_SYM(SHELL_CLEANUP_SYM)); - - sCleanupHooked = true; - - if (reg) { - reg(appEnded); - } - } - - int32_t idx = 0; - - while (idx < (int32_t)arrlen(sModels) && sModels[idx].m) { - idx++; - } - - if (idx == (int32_t)arrlen(sModels)) { - BasFrmHandleT blank; - - memset(&blank, 0, sizeof(blank)); - arrput(sModels, blank); - - if (idx == (int32_t)arrlen(sModels)) { - frmModelFree(m); - return BAS_NO_HANDLE; - } - } - - sModels[idx].m = m; - sModels[idx].appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0; - return idx + 1; + return handle; } @@ -691,6 +641,11 @@ static void liveDefaults(FrmLiveT *live, BasControlT *ctrl, FrmCtrlT *fc) { continue; } + // A LoadPictureData picture has no file for the .frm to name + if (strcasecmp(p->name, BAS_PROP_PICTURE) == 0 && basPictureIsData(val)) { + val[0] = '\0'; + } + if (!wgtIfaceGetPropText(iface, dflt, p, old, sizeof(old)) || strcmp(val, old) != 0) { frmCtrlSetProp(fc, p->name, val); } @@ -762,9 +717,10 @@ static void liveForm(FrmLiveT *live) { continue; } - // Icon reads as a Picture; the .frm holds its file name + // Icon reads as a Picture; the .frm holds its file name (none for a + // LoadPictureData picture) if (strcasecmp(pd->name, BAS_PROP_ICON) == 0) { - frmModelSetFormProp(live->m, pd->name, form->iconPath); + frmModelSetFormProp(live->m, pd->name, basPictureIsData(form->iconPath) ? "" : form->iconPath); continue; } @@ -890,8 +846,13 @@ static void liveMenus(FrmLiveT *live) { } +static void modelFree(void *obj) { + frmModelFree((FrmModelT *)obj); +} + + static FrmModelT *modelOf(int32_t handle) { - return (handle >= 1 && handle <= (int32_t)arrlen(sModels)) ? sModels[handle - 1].m : NULL; + return (FrmModelT *)basHandleGet(&sModels, handle); } diff --git a/src/apps/kpunch/dvxbasic/basrt.dhs b/src/apps/kpunch/dvxbasic/basrt.dhs index b0449212..7ebe8ccd 100644 --- a/src/apps/kpunch/dvxbasic/basrt.dhs +++ b/src/apps/kpunch/dvxbasic/basrt.dhs @@ -39,7 +39,6 @@ Headers: apps/dvxbasic/runtime/vm.h, apps/dvxbasic/runtime/values.h .link lib.basrt.values Value System (values.h) .link lib.basrt.vm Virtual Machine (vm.h) -.link lib.basrt.frmmodel The .frm Model (frmModel.h) .topic lib.basrt.values .title Value System @@ -675,142 +674,3 @@ typedef struct { debugUdtDefs UDT type definitions for the debugger. debugUdtDefCount Number of debug UDT definitions. .endtable - -.topic lib.basrt.frmmodel -.title The .frm Model -.toc 1 The .frm Model -.index FrmModelT -.index FrmCtrlT -.index FrmMenuItemT -.index frmModelLoad -.index frmModelWrite -.index frmModelText -.index frmModelFromForm -.index basFontChoiceOf -.index basFontPropText -.index Font properties (C) - -.h1 The .frm Model - -A form file as data, with no widgets and no windows: the form's properties, its controls (type, name, control array index, parent, the built-in size and state fields, and every other "Key = Value" pair as text), its menu items, and its code section. frmModelLoad reads .frm text (through frmParse) and frmModelWrite writes the canonical text the DVX BASIC IDE saves. The IDE's designer, properties window, menu editor, and copy and paste all work on this model and keep only their live widgets on the side (each control's userData); designer.bas binds it for BASIC programs (the Frm* functions, formrt/frmModelBas.c). - -Header: apps/dvxbasic/formrt/frmModel.h (part of basrt.lib) - -.h2 Types - -.table - Type Description - ---- ----------- - FrmModelT A form: name, caption, width / height / left / top, padding, spacing, windowState, layout, helpTopic, contextMenu, centered / autoSize / resizable, controls (stb_ds array of FrmCtrlT pointers in tree order: each control directly followed by its children), menuItems (stb_ds array), props (other runtime form properties that were set: Icon, Tag, MousePointer, the Font properties ...), code (malloc'd, NULL = none), dirty (changed by an edit since it was made or loaded). - FrmCtrlT A control: name, typeName, parentName + parentIndex (the parent's control array index, so a child of one member of a container array belongs to that member), index (FRM_NO_INDEX: not in an array), left, top, width (MinWidth), height (MinHeight), maxWidth, maxHeight, weight, dragMode, helpTopic, visible, enabled, props (stb_ds array of FrmPropT: every other key, unquoted), userData (the caller's; the model never reads it and loads, copies, and pastes start it at NULL). - FrmMenuItemT A menu item: caption ("-" for a separator), name, shortcut, shortcutText, level (0: a menu-bar menu or a popup; 1: its items; 2+: submenus), id (an editor identity, see frmModelMenuReplace), checked, checkable (Checked was written), radioCheck, enabled, visible (False on a level-0 item: a popup menu). - FrmPropT One stored "Key = Value": name, value. - FrmIntPropT One built-in integer property (Left, Top, MinWidth alias Width, MinHeight alias Height, MaxWidth, MaxHeight, Weight, DragMode): name, alias, offset into FrmCtrlT, saveRule (FRM_SAVE_ALWAYS / IF_NONZERO / IF_POSITIVE). -.endtable - -Only values somebody set are stored and written: a property a control has by default (a PictureBox's BorderStyle, a TextBox's MaxLength) is not in the model and not in the file. Values are written in the canonical form for their widget interface type, or for a key the widget does not know, for their type in the runtime's common property table (STRING quoted, BOOL True / False, INT a number, FLOAT and ENUM unquoted); other keys are written as quoted strings. The limits come from the parser's: FRM_MAX_NEST_DEPTH control levels, FRM_MAX_MENU_LEVEL menu levels, and no property line longer than FRM_MAX_LINE_LEN - 1 characters (the writer fails with FRM_WRITE_LINE_TOO_LONG rather than write one the parser would cut). - -.h2 Loading and Writing - -.table - Function Description - -------- ----------- - frmModelNew(name) A blank form with the runtime form's defaults. - frmModelLoad(text, len) The model of .frm text; NULL when it is not a form (no Begin Form, frmParse refuses it, two controls share a name and index) or out of memory. A Key = Value line is the innermost open block's, also after the blocks inside it, as the runtime reads it; a child names its container by the container's final Index. Of several OptionButtons of one container saved with Value True, the last keeps it. - frmModelFree(m) Frees the model, its controls, and menus. - frmModelWrite(m, buf, bufSize) The .frm text into buf: its length, or FRM_WRITE_TOO_LARGE / FRM_WRITE_LINE_TOO_LONG (never a partial text). - frmModelWriteSize(m) A buffer size that always holds the text. - frmModelText(m, &len) The text in a new malloc'd string; NULL with len set to the FRM_WRITE_* code. - frmModelSetCode(m, code) Replaces the code section. - frmCodeSection(trailing, len, &outLen) The code section of the text after a form's End (blank lines skipped, the first code line keeps its indent). - frmModelFromForm(rt, form) A model of a loaded form as it is now: its properties, its controls in their containers with each property that differs from a new control of the type (compared with one made in a hidden window; the six Font properties of the form and of each control that has a font of its own), its menus; no code. What FormText$ writes. -.endtable - -.h2 Properties - -.table - Function Description - -------- ----------- - frmModelFormProp(m, name, out, size) A form property as text (an unset design property gives "", "0", or "False"); false for run-time-only ones (Visible, DesignMode, DesignSelection, object properties). - frmModelSetFormProp(m, name, value) Sets one from text; Name renames the form; an empty value takes an optional one out. - frmCtrlPropText(ctrl, name, out, size) A control property as the control holds it (Name, Index, the built-in integers, HelpTopic, Visible / Enabled unless the widget defines them, the stored ones). - frmCtrlSetPropText(ctrl, key, value) Applies one the way the loader does (a VB TabIndex is dropped unless the widget defines it). - frmModelSetCtrlProp(m, ctrl, name, value) The same with an editor's rules: Name renames the control array, Index makes an array member, then the OptionButton group rule. - frmCtrlGetProp / frmCtrlSetProp / frmCtrlRemoveProp The stored properties. - frmIntPropAt / frmIntPropFind / frmIntPropGet / Set The built-in integer table. - frmIfaceProp(typeName, propName) The widget interface property of a type, NULL when none. -.endtable - -.h2 Font Properties - -FontName, FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru (BAS_PROP_FONT*) are in both the form table and the common control table. They are one font, a DvxFontChoiceT (dvxDlg.h: family, size in pixels, FONT_STYLE_* flags), and the widget's font (wgtFont, wgtSetFont) is the only place they live: a read gives the font the object draws with (a form's is its root widget's), a write gives it that font with the one part changed. The .frm loader collects a block's Font properties and opens one font when the block's properties are read, so their order in the file does not matter. Helpers in formrt.h, for loaders and designers that hold them as .frm text: - -.table - Function Description - -------- ----------- - basFontPropAt(idx) The Font property names in .frm order (FontName, FontSize, then the flags), NULL past the end. - basFontPropIs(name) True for a Font property name (any case). - basFontChoiceOf(font, out) A BitmapFontT as the properties name it (dvxFontFamilyOf, pixelSize, style). - basFontPropText(choice, name, out, size) One property of a choice as .frm text (the family, the size, True / False). - basFontPropSetText(choice, name, text) Sets one property of a choice from .frm text. -.endtable - -.h2 Editing Controls - -.table - Function Description - -------- ----------- - frmModelAddCtrl(m, typeName, name, parent) Adds a control as the last child of parent (NULL: the form); name "" or NULL gives a free one (frmModelAutoName); Caption = its name. NULL when the name is taken or parent cannot hold it. - frmModelRemoveCtrl(m, ctrl) Removes ctrl and its subtree. - frmModelRenameCtrl(m, oldName, newName) Renames a whole control array; children and DataSource / MasterSource of a renamed Data control follow. Identifier rules are the caller's. - frmModelMakeArrayMember(m, ctrl, index) Gives a control array index; its children follow. - frmModelMoveCtrl(m, ctrl, before) Moves ctrl with its subtree before a sibling (NULL: after the last). - frmModelReparent(m, ctrl, parent) Makes ctrl the last child of another container (refuses cycles, non-containers, and too deep a nesting). - frmModelSibling(m, idx, dir) The sibling next to a control (+1 after its subtree, -1 before). - frmModelFindCtrl / CtrlPos / FindParent / CtrlDepth Lookups. - frmModelIsDescendant / SubtreeEnd / NormalizeOrder The tree order. - frmModelNameInUse / AutoName / OptionSelected Names and the OptionButton group rule. - frmModelCtrlText(m, ctrl, &len) ctrl and its children as .frm control blocks (a copy for the Clipboard). - frmModelCtrlsFromText(text, len) Copied blocks read into a scratch model. - frmModelInsertTree(m, src, srcRoot, parent, name, index) Moves a subtree out of the scratch model into m: the root takes name / index (NULL: its own name when free, else a free one); a child whose name is taken gets a free one, the members of one array together; children follow their renamed parents. -.endtable - -.h2 Menus - -The frmMenu* functions work on any stb_ds array of FrmMenuItemT (an editor's working copy as well as m->menuItems) and keep the levels a tree. - -.table - Function Description - -------- ----------- - frmMenuItemInit(mi) A blank item (enabled, visible, level 0). - frmMenuInsert(&items, at, level) Inserts a blank item; the level is clamped so the menus stay a tree. - frmMenuRemove(&items, idx) Removes an item with its subtree. - frmMenuMove(&items, idx, dir) Moves an item with its subtree past its previous / next sibling; its new position or FRM_NO_INDEX. - frmMenuIndent(items, idx, delta) Indents / outdents an item with its subtree (at most one level below the item above, not past FRM_MAX_MENU_LEVEL). - frmMenuSubtreeEnd / CheckLevels / StripBlank / AutoName Tree helpers; AutoName makes mnuSep1 ... or "mnu" plus the caption's letters and digits. - frmMenuPropText / frmMenuSetPropText Menu properties as text (Checked makes a check item). - frmModelMenuValidate(m, items, isIdent, reason, size) The checks before items replace m's menus (levels; every name set, an identifier by isIdent, not the form's or a control's, unique; every Shortcut a key): the first bad item and the reason. - frmModelMenuReplace(m, items) Replaces m's menus by a copy; ContextMenu bindings follow renamed items, matched by id (give every item id = position + 1 before editing). - frmModelSetMenuProp(m, idx, key, value) Sets an item's property; a Name rename refuses a taken name and ContextMenu bindings follow. -.endtable - -.code -// Load a form, rename a control, add a label inside a frame, save. -FrmModelT *m = frmModelLoad(text, len); - -if (m) { - int32_t at = frmModelFindCtrl(m, "Frame1", FRM_NO_INDEX); - FrmCtrlT *frame = (at >= 0) ? m->controls[at] : NULL; - FrmCtrlT *lbl = frmModelAddCtrl(m, "Label", "", frame); - int32_t outLen = 0; - - frmModelRenameCtrl(m, "Command1", "cmdOk"); - frmModelSetCtrlProp(m, lbl, "Caption", "Name:"); - - char *out = frmModelText(m, &outLen); - - // write out[0 .. outLen) to the file - free(out); - frmModelFree(m); -} -.endcode diff --git a/src/apps/kpunch/dvxbasic/compiler/basEvents.h b/src/apps/kpunch/dvxbasic/compiler/basEvents.h index 1809eee0..840d3e7c 100644 --- a/src/apps/kpunch/dvxbasic/compiler/basEvents.h +++ b/src/apps/kpunch/dvxbasic/compiler/basEvents.h @@ -93,6 +93,7 @@ typedef enum { #define BAS_EVT_DSGNPROPCHG "DesignPropertyChanged" #define BAS_EVT_LINKCLICK "LinkClick" #define BAS_EVT_NEWARGS "NewArgs" +#define BAS_EVT_FULLSCREEN "FullScreen" #define BAS_EVENT_LIST(X) \ X(BAS_EVT_LOAD, BAS_EVT_SCOPE_FORM, "") \ @@ -125,7 +126,8 @@ typedef enum { X(BAS_EVT_DESIGNKEY, BAS_EVT_SCOPE_FORM, "KeyCode As Integer, Shift As Integer") \ X(BAS_EVT_DSGNPROPCHG, BAS_EVT_SCOPE_FORM, "CtrlName As String, PropertyName As String") \ X(BAS_EVT_LINKCLICK, BAS_EVT_SCOPE_WIDGET, NULL) \ - X(BAS_EVT_NEWARGS, BAS_EVT_SCOPE_FORM, "") + X(BAS_EVT_NEWARGS, BAS_EVT_SCOPE_FORM, "") \ + X(BAS_EVT_FULLSCREEN, BAS_EVT_SCOPE_FORM, "State As Integer") // A UserControl (a widget class written in BASIC, a .ctl file): the name // of its own object in its code (UserControl.Refresh), the word after the diff --git a/src/apps/kpunch/dvxbasic/compiler/codegen.c b/src/apps/kpunch/dvxbasic/compiler/codegen.c index 62f0c139..b6aed6f5 100644 --- a/src/apps/kpunch/dvxbasic/compiler/codegen.c +++ b/src/apps/kpunch/dvxbasic/compiler/codegen.c @@ -322,14 +322,7 @@ void basPatch32(BasCodeGenT *cg, int32_t pos, int32_t val) { // FNV-1a over the constant's bytes (case-sensitive: constants are exact). static uint32_t constantHash(const char *text, int32_t len) { - uint32_t h = BAS_FNV1A_OFFSET; - - for (int32_t i = 0; i < len; i++) { - h ^= (uint32_t)(uint8_t)text[i]; - h *= BAS_FNV1A_PRIME; - } - - return h; + return dvxFnvBytes(DVX_FNV_BASIS, text, len); } diff --git a/src/apps/kpunch/dvxbasic/compiler/compiler.c b/src/apps/kpunch/dvxbasic/compiler/compiler.c index 949e54be..f0683ff3 100644 --- a/src/apps/kpunch/dvxbasic/compiler/compiler.c +++ b/src/apps/kpunch/dvxbasic/compiler/compiler.c @@ -248,6 +248,11 @@ static const PredefConstT sPredefConsts[] = { { "dvxArrowLeft", ScrollArrowLeftE }, { "dvxArrowRight", ScrollArrowRightE }, + // A form's FullScreen event's State + { "dvxFullScreenEnded", BAS_FULLSCREEN_ENDED }, + { "dvxFullScreenActive", BAS_FULLSCREEN_ACTIVE }, + { "dvxFullScreenSuspended", BAS_FULLSCREEN_SUSPENDED }, + // ScrollBarsNeeded's answer { "vbSBNone", BAS_SB_NONE }, { "vbHorizontal", BAS_SB_HORIZONTAL }, @@ -301,6 +306,7 @@ int32_t basCompilerErrorLine(const BasCompilerT *c); const char *basCompilerFormScope(const BasCompilerT *c); int32_t basCompilerGlobalSlot(const BasCompilerT *c, const char *name); void basCompilerSetOptionExplicit(BasCompilerT *c, bool on); +void basCompilerSetUnitFn(BasCompilerT *c, void (*fn)(void *ctx), void *ctx); void basCompilerSetValidator(BasCompilerT *c, const BasCtrlValidatorT *v); void basErrorAt(BasCompilerT *c, int32_t line, int32_t col, const char *fmt, ...); void basErrorNode(BasCompilerT *c, const BasNodeT *n, const char *fmt, ...); @@ -441,6 +447,10 @@ bool basCompile(BasCompilerT *c) { arrsetlen(c->symChanges, 0); basResolveUnitEnd(c, unit); basArenaRelease(&c->unitArena, mark); + + if (c->unitFn) { + c->unitFn(c->unitCtx); + } } if (!c->hasError) { @@ -609,6 +619,12 @@ void basCompilerSetOptionExplicit(BasCompilerT *c, bool on) { } +void basCompilerSetUnitFn(BasCompilerT *c, void (*fn)(void *ctx), void *ctx) { + c->unitFn = fn; + c->unitCtx = ctx; +} + + void basCompilerSetValidator(BasCompilerT *c, const BasCtrlValidatorT *v) { c->validator = v; } @@ -734,7 +750,7 @@ static bool buildClasses(const BasCompilerT *c, BasModuleT *mod) { m->typedParams |= basConvertsArg(pi->paramTypes[p], pi->paramFlags[p]) ? (1u << p) : 0u; } - if (pi->paramCount > 0 && !joinParamNames(pi, &m->paramNames)) { + if (!joinParamNames(pi, &m->paramNames)) { basModuleFree(mod); return false; } @@ -1194,11 +1210,17 @@ static bool globalStartsTyped(const BasSymT *s) { // A procedure's parameter names, comma-separated, into a new string -// (*out): what a late-bound call with named arguments reads. False when -// memory runs out. +// (*out; NULL for a procedure without parameters): what a late-bound +// call with named arguments reads. False when memory runs out. static bool joinParamNames(const BasProcInfoT *pi, char **out) { size_t len = 0; + *out = NULL; + + if (pi->paramCount <= 0) { + return true; + } + for (int32_t p = 0; p < pi->paramCount; p++) { len += strlen(pi->paramNames[p]) + 1; } diff --git a/src/apps/kpunch/dvxbasic/compiler/compiler.h b/src/apps/kpunch/dvxbasic/compiler/compiler.h index 1b4e1063..20fd5294 100644 --- a/src/apps/kpunch/dvxbasic/compiler/compiler.h +++ b/src/apps/kpunch/dvxbasic/compiler/compiler.h @@ -118,6 +118,11 @@ int32_t basCompilerGlobalSlot(const BasCompilerT *c, const char *name); // OPTION EXPLICIT for the whole program (the project setting). void basCompilerSetOptionExplicit(BasCompilerT *c, bool on); +// fn(ctx) is called after each unit (a statement or procedure) basCompile +// compiles, so a caller can let other work go on through a long compile; +// NULL calls nothing. +void basCompilerSetUnitFn(BasCompilerT *c, void (*fn)(void *ctx), void *ctx); + // Attaches the validator (borrowed: it must outlive basCompile). void basCompilerSetValidator(BasCompilerT *c, const BasCtrlValidatorT *v); diff --git a/src/apps/kpunch/dvxbasic/compiler/front.h b/src/apps/kpunch/dvxbasic/compiler/front.h index 37adccfd..e1d349ae 100644 --- a/src/apps/kpunch/dvxbasic/compiler/front.h +++ b/src/apps/kpunch/dvxbasic/compiler/front.h @@ -314,6 +314,9 @@ struct BasCompilerS { BasVmT *constVm; // evaluates constant expressions (made on first use) const BasCtrlValidatorT *validator; + + void (*unitFn)(void *ctx); // basCompilerSetUnitFn: after each unit + void *unitCtx; }; // ============================================================ diff --git a/src/apps/kpunch/dvxbasic/compiler/opcodes.h b/src/apps/kpunch/dvxbasic/compiler/opcodes.h index 58108ecf..ad5a936d 100644 --- a/src/apps/kpunch/dvxbasic/compiler/opcodes.h +++ b/src/apps/kpunch/dvxbasic/compiler/opcodes.h @@ -33,6 +33,8 @@ #include #include +#include "dvxEndian.h" + // ============================================================ // Variable scope tags // Emitted in bytecode (e.g. OP_FOR scopeTag byte) and consumed @@ -523,99 +525,103 @@ typedef enum { // OP_BUILTIN function numbers. Part of the bytecode ABI -- append only. typedef enum { - BAS_FN_REPLACE = 0, // REPLACE$(s, find, repl [, start [, count [, compare]]]) - BAS_FN_SPLIT = 1, // SPLIT(s [, delim [, limit [, compare]]]) -> STRING array - BAS_FN_JOIN = 2, // JOIN$(array [, delim]) - BAS_FN_INSTRREV = 3, // INSTRREV(s, find [, start [, compare]]) - BAS_FN_STRCOMP = 4, // STRCOMP(a, b [, compare]) - BAS_FN_STRREVERSE = 5, // STRREVERSE$(s) - BAS_FN_FILTER = 6, // FILTER(array, match [, include [, compare]]) -> STRING array - BAS_FN_LSET = 7, // LSET target = s: (current target, s) -> s left-justified in LEN(target) - BAS_FN_RSET = 8, // RSET target = s: (current target, s) -> s right-justified in LEN(target) - BAS_FN_IIF = 9, // IIF(cond, a, b) - BAS_FN_CHOOSE = 10, // CHOOSE(index, a [, b ...]) - BAS_FN_NOW = 11, // NOW - BAS_FN_DATE = 12, // DATE - BAS_FN_TIME = 13, // TIME - BAS_FN_DATESERIAL = 14, // DATESERIAL(y, m, d) - BAS_FN_TIMESERIAL = 15, // TIMESERIAL(h, m, s) - BAS_FN_DATEVALUE = 16, // DATEVALUE(d) - BAS_FN_TIMEVALUE = 17, // TIMEVALUE(d) - BAS_FN_YEAR = 18, // YEAR(d) - BAS_FN_MONTH = 19, // MONTH(d) - BAS_FN_DAY = 20, // DAY(d) - BAS_FN_WEEKDAY = 21, // WEEKDAY(d [, firstDayOfWeek]) - BAS_FN_HOUR = 22, // HOUR(d) - BAS_FN_MINUTE = 23, // MINUTE(d) - BAS_FN_SECOND = 24, // SECOND(d) - BAS_FN_DATEADD = 25, // DATEADD(interval, n, d) - BAS_FN_DATEDIFF = 26, // DATEDIFF(interval, d1, d2 [, firstDayOfWeek [, firstWeekOfYear]]) - BAS_FN_DATEPART = 27, // DATEPART(interval, d [, firstDayOfWeek [, firstWeekOfYear]]) - BAS_FN_ISDATE = 28, // ISDATE(v) - BAS_FN_MONTHNAME = 29, // MONTHNAME(m [, abbrev]) - BAS_FN_WEEKDAYNAME = 30, // WEEKDAYNAME(w [, abbrev [, firstDayOfWeek]]) - BAS_FN_PARAMARRAY = 31, // (elementType [| BAS_PARAMARRAY_BASE_ONE], v0, v1, ...) -> 0-based (1-based) array of the values: a ParamArray argument, ARRAY(...) + BAS_FN_REPLACE = 0, // REPLACE$(s, find, repl [, start [, count [, compare]]]) + BAS_FN_SPLIT = 1, // SPLIT(s [, delim [, limit [, compare]]]) -> STRING array + BAS_FN_JOIN = 2, // JOIN$(array [, delim]) + BAS_FN_INSTRREV = 3, // INSTRREV(s, find [, start [, compare]]) + BAS_FN_STRCOMP = 4, // STRCOMP(a, b [, compare]) + BAS_FN_STRREVERSE = 5, // STRREVERSE$(s) + BAS_FN_FILTER = 6, // FILTER(array, match [, include [, compare]]) -> STRING array + BAS_FN_LSET = 7, // LSET target = s: (current target, s) -> s left-justified in LEN(target) + BAS_FN_RSET = 8, // RSET target = s: (current target, s) -> s right-justified in LEN(target) + BAS_FN_IIF = 9, // IIF(cond, a, b) + BAS_FN_CHOOSE = 10, // CHOOSE(index, a [, b ...]) + BAS_FN_NOW = 11, // NOW + BAS_FN_DATE = 12, // DATE + BAS_FN_TIME = 13, // TIME + BAS_FN_DATESERIAL = 14, // DATESERIAL(y, m, d) + BAS_FN_TIMESERIAL = 15, // TIMESERIAL(h, m, s) + BAS_FN_DATEVALUE = 16, // DATEVALUE(d) + BAS_FN_TIMEVALUE = 17, // TIMEVALUE(d) + BAS_FN_YEAR = 18, // YEAR(d) + BAS_FN_MONTH = 19, // MONTH(d) + BAS_FN_DAY = 20, // DAY(d) + BAS_FN_WEEKDAY = 21, // WEEKDAY(d [, firstDayOfWeek]) + BAS_FN_HOUR = 22, // HOUR(d) + BAS_FN_MINUTE = 23, // MINUTE(d) + BAS_FN_SECOND = 24, // SECOND(d) + BAS_FN_DATEADD = 25, // DATEADD(interval, n, d) + BAS_FN_DATEDIFF = 26, // DATEDIFF(interval, d1, d2 [, firstDayOfWeek [, firstWeekOfYear]]) + BAS_FN_DATEPART = 27, // DATEPART(interval, d [, firstDayOfWeek [, firstWeekOfYear]]) + BAS_FN_ISDATE = 28, // ISDATE(v) + BAS_FN_MONTHNAME = 29, // MONTHNAME(m [, abbrev]) + BAS_FN_WEEKDAYNAME = 30, // WEEKDAYNAME(w [, abbrev [, firstDayOfWeek]]) + BAS_FN_PARAMARRAY = 31, // (elementType [| BAS_PARAMARRAY_BASE_ONE], v0, v1, ...) -> 0-based (1-based) array of the values: a ParamArray argument, ARRAY(...) // The program screen (runtime/screen.c): statements return 0, which // the compiler pops. Omitted arguments are pushed as 0 and flagged in // a BAS_GIVEN_* / BAS_GFX_* mask pushed last. - BAS_FN_CLS = 32, // CLS [n]: (n, -1 when omitted) - BAS_FN_LOCATE = 33, // LOCATE [row] [, [col] [, cursor]]: (row, col, cursor, given) - BAS_FN_COLOR = 34, // COLOR [fore] [, back]: (fore, back, given) - BAS_FN_SCREEN = 35, // SCREEN mode [, [colorswitch] [, [apage] [, vpage]]]: (mode, colorswitch, apage, vpage, given) - BAS_FN_WIDTH = 36, // WIDTH [cols] [, rows]: (cols, rows, given) - BAS_FN_PSET = 37, // PSET / PRESET [STEP](x, y) [, color]: (x, y, color, flags) - BAS_FN_LINE = 38, // LINE [[STEP](x1, y1)]-[STEP](x2, y2) [, color] [, B|BF] [, style]: (x1, y1, x2, y2, color, style, flags) - BAS_FN_CIRCLE = 39, // CIRCLE [STEP](x, y), r [, color [, start [, end [, aspect]]]]: (x, y, r, color, start, end, aspect, flags) - BAS_FN_PAINT = 40, // PAINT [STEP](x, y) [, paint | tile$ [, border [, background$]]]: (x, y, paint, border, background, flags) - BAS_FN_DRAW = 41, // DRAW macro$ - BAS_FN_PALETTE = 42, // PALETTE [attr, color]: (attr, color, given) - BAS_FN_PALETTE_USING = 43, // PALETTE USING array[(first)]: (array, first, given) - BAS_FN_VIEW = 44, // VIEW [[SCREEN] (x1, y1)-(x2, y2) [, [fill] [, border]]]: (x1, y1, x2, y2, fill, border, flags) - BAS_FN_WINDOW = 45, // WINDOW [[SCREEN] (x1, y1)-(x2, y2)]: (x1, y1, x2, y2, flags) - BAS_FN_FULLSCREEN = 46, // FULLSCREEN [ON | OFF | expr]: (on) - BAS_FN_INKEY = 47, // INKEY$ -> "" / one character / CHR$(0) + scan code - BAS_FN_CSRLIN = 48, // CSRLIN -> cursor row - BAS_FN_POS = 49, // POS(n) -> cursor column - BAS_FN_POINT = 50, // POINT(x, y) -> colour (-1 outside the view); POINT(n) -> the last point - BAS_FN_SCREENFN = 51, // SCREEN(row, col [, colorflag]) -> character code or colour - BAS_FN_LINE_INPUT = 52, // LINE INPUT [prompt$;] v$: (prompt) -> the line typed + BAS_FN_CLS = 32, // CLS [n]: (n, -1 when omitted) + BAS_FN_LOCATE = 33, // LOCATE [row] [, [col] [, cursor]]: (row, col, cursor, given) + BAS_FN_COLOR = 34, // COLOR [fore] [, back]: (fore, back, given) + BAS_FN_SCREEN = 35, // SCREEN mode [, [colorswitch] [, [apage] [, vpage]]]: (mode, colorswitch, apage, vpage, given) + BAS_FN_WIDTH = 36, // WIDTH [cols] [, rows]: (cols, rows, given) + BAS_FN_PSET = 37, // PSET / PRESET [STEP](x, y) [, color]: (x, y, color, flags) + BAS_FN_LINE = 38, // LINE [[STEP](x1, y1)]-[STEP](x2, y2) [, color] [, B|BF] [, style]: (x1, y1, x2, y2, color, style, flags) + BAS_FN_CIRCLE = 39, // CIRCLE [STEP](x, y), r [, color [, start [, end [, aspect]]]]: (x, y, r, color, start, end, aspect, flags) + BAS_FN_PAINT = 40, // PAINT [STEP](x, y) [, paint | tile$ [, border [, background$]]]: (x, y, paint, border, background, flags) + BAS_FN_DRAW = 41, // DRAW macro$ + BAS_FN_PALETTE = 42, // PALETTE [attr, color]: (attr, color, given) + BAS_FN_PALETTE_USING = 43, // PALETTE USING array[(first)]: (array, first, given) + BAS_FN_VIEW = 44, // VIEW [[SCREEN] (x1, y1)-(x2, y2) [, [fill] [, border]]]: (x1, y1, x2, y2, fill, border, flags) + BAS_FN_WINDOW = 45, // WINDOW [[SCREEN] (x1, y1)-(x2, y2)]: (x1, y1, x2, y2, flags) + BAS_FN_FULLSCREEN = 46, // FULLSCREEN [ON | OFF | expr]: (on) + BAS_FN_INKEY = 47, // INKEY$ -> "" / one character / CHR$(0) + scan code + BAS_FN_CSRLIN = 48, // CSRLIN -> cursor row + BAS_FN_POS = 49, // POS(n) -> cursor column + BAS_FN_POINT = 50, // POINT(x, y) -> colour (-1 outside the view); POINT(n) -> the last point + BAS_FN_SCREENFN = 51, // SCREEN(row, col [, colorflag]) -> character code or colour + BAS_FN_LINE_INPUT = 52, // LINE INPUT [prompt$;] v$: (prompt) -> the line typed // Variants and objects - BAS_FN_VARTYPE = 53, // VARTYPE(v) -> vbEmpty .. vbArray + element type - BAS_FN_TYPENAME = 54, // TYPENAME(v) -> "Integer", "Nothing", class name ... - BAS_FN_ISEMPTY = 55, // ISEMPTY(v) - BAS_FN_ISNULL = 56, // ISNULL(v) - BAS_FN_ISNUMERIC = 57, // ISNUMERIC(v) - BAS_FN_ISOBJECT = 58, // ISOBJECT(v) - BAS_FN_ISARRAY = 59, // ISARRAY(v) - BAS_FN_CVAR = 60, // CVAR(v) -> v as it is - BAS_FN_TYPEOF = 61, // TYPEOF obj IS name: (obj, name) -> the object is of that type - BAS_FN_EACH_COUNT = 62, // FOR EACH (compiler-generated): (group) -> items in the array or Collection - BAS_FN_EACH_ITEM = 63, // FOR EACH (compiler-generated): (group, i) -> item i (0-based) - BAS_FN_ISMISSING = 64, // ISMISSING(v) - BAS_FN_LSET_TYPE = 65, // LSET rec1 = rec2 (compiler-generated): (rec1, rec2, rec1 layout, rec2 layout) -> rec1 holding rec2's bytes - BAS_FN_PCOPY = 66, // PCOPY source, destination (screen pages) - BAS_FN_VIEW_PRINT = 67, // VIEW PRINT [top TO bottom]: (top, bottom, given) - BAS_FN_PMAP = 68, // PMAP(coordinate, fn) -> world <-> physical - BAS_FN_VARPTR_STR = 69, // VARPTR$(v) -> v packed for DRAW's X and "=" (BAS_VARPTR_*) - BAS_FN_LOADPICTURE = 70, // LOADPICTURE([file]) -> a Picture object for picture properties (DragIcon, Picture); no file: an empty picture - BAS_FN_RECORD_ASSIGN = 71, // rec = value (compiler-generated): (reference to the target, TYPE value) -> Empty; the value's fields are copied into the record the target holds (VB) - BAS_FN_CLOSE = 72, // CLOSE [#n [, #n ...]] with no channel or several, RESET: (channels...) -> 0; none closes every channel - BAS_FN_FILE_PRINT = 73, // PRINT #n, ... (compiler-generated): (n) -> 0 sends the statement's PRINT output to channel n; () -> 0 ends it - BAS_FN_FILE_WIDTH = 74, // WIDTH #n, w / WIDTH "device", w / WIDTH LPRINT w: (n | device$ | BAS_WIDTH_LPRINT, w) -> 0 - BAS_FN_PEN = 75, // PEN(n) -> the light pen's (the mouse's) state on the program screen (BAS_PEN_*) - BAS_FN_PEN_SET = 76, // PEN ON | OFF | STOP: (BAS_PEN_ON / OFF / STOP) -> 0 + BAS_FN_VARTYPE = 53, // VARTYPE(v) -> vbEmpty .. vbArray + element type + BAS_FN_TYPENAME = 54, // TYPENAME(v) -> "Integer", "Nothing", class name ... + BAS_FN_ISEMPTY = 55, // ISEMPTY(v) + BAS_FN_ISNULL = 56, // ISNULL(v) + BAS_FN_ISNUMERIC = 57, // ISNUMERIC(v) + BAS_FN_ISOBJECT = 58, // ISOBJECT(v) + BAS_FN_ISARRAY = 59, // ISARRAY(v) + BAS_FN_CVAR = 60, // CVAR(v) -> v as it is + BAS_FN_TYPEOF = 61, // TYPEOF obj IS name: (obj, name) -> the object is of that type + BAS_FN_EACH_COUNT = 62, // FOR EACH (compiler-generated): (group) -> items in the array or Collection + BAS_FN_EACH_ITEM = 63, // FOR EACH (compiler-generated): (group, i) -> item i (0-based) + BAS_FN_ISMISSING = 64, // ISMISSING(v) + BAS_FN_LSET_TYPE = 65, // LSET rec1 = rec2 (compiler-generated): (rec1, rec2, rec1 layout, rec2 layout) -> rec1 holding rec2's bytes + BAS_FN_PCOPY = 66, // PCOPY source, destination (screen pages) + BAS_FN_VIEW_PRINT = 67, // VIEW PRINT [top TO bottom]: (top, bottom, given) + BAS_FN_PMAP = 68, // PMAP(coordinate, fn) -> world <-> physical + BAS_FN_VARPTR_STR = 69, // VARPTR$(v) -> v packed for DRAW's X and "=" (BAS_VARPTR_*) + BAS_FN_LOADPICTURE = 70, // LOADPICTURE([file]) -> a Picture object for picture properties (DragIcon, Picture); no file: an empty picture + BAS_FN_RECORD_ASSIGN = 71, // rec = value (compiler-generated): (reference to the target, TYPE value) -> Empty; the value's fields are copied into the record the target holds (VB) + BAS_FN_CLOSE = 72, // CLOSE [#n [, #n ...]] with no channel or several, RESET: (channels...) -> 0; none closes every channel + BAS_FN_FILE_PRINT = 73, // PRINT #n, ... (compiler-generated): (n) -> 0 sends the statement's PRINT output to channel n; () -> 0 ends it + BAS_FN_FILE_WIDTH = 74, // WIDTH #n, w / WIDTH "device", w / WIDTH LPRINT w: (n | device$ | BAS_WIDTH_LPRINT, w) -> 0 + BAS_FN_PEN = 75, // PEN(n) -> the light pen's (the mouse's) state on the program screen (BAS_PEN_*) + BAS_FN_PEN_SET = 76, // PEN ON | OFF | STOP: (BAS_PEN_ON / OFF / STOP) -> 0 // The Err object and Error$ - BAS_FN_ERR_DESCRIPTION = 77, // Err.Description -> the current error's text ("" when none) - BAS_FN_ERR_SOURCE = 78, // Err.Source -> the source Err.Raise gave ("" otherwise) - BAS_FN_ERR_SET = 79, // Err.Clear: () -> 0 clears the error; Err.Number = n: (n) -> 0 sets it (its text the number's) - BAS_FN_ERR_RAISE = 80, // Err.Raise number [, source [, description]] -> raises the error - BAS_FN_ERROR_TEXT = 81, // ERROR$([n]) / ERROR([n]) -> the text of error n (none: the current error's) - BAS_FN_LIKE = 82, // a LIKE pattern: (a, pattern) -> a matches the pattern (Option Compare decides case) - BAS_FN_FILEATTR = 83, // FILEATTR(n, attribute) -> the open mode of file n (BAS_FILEATTR_MODE) or its system handle (BAS_FILEATTR_HANDLE) - BAS_FN_QBCOLOR = 84, // QBCOLOR(n) -> QBasic colour n (0..15) as an RGB value - BAS_FN_NEW_FORM = 85, // New FormName (compiler-generated): (name) -> a new copy of the form (its own window, controls and form-level variables) - BAS_FN_CALLBYNAME = 86, // CALLBYNAME(object, name, callType [, args...]) -> the member's value or result (Empty for vbLet / vbSet) - BAS_FN_LOADFORMTEXT = 87 // LOADFORMTEXT(frmText) -> a form loaded from .frm text + BAS_FN_ERR_DESCRIPTION = 77, // Err.Description -> the current error's text ("" when none) + BAS_FN_ERR_SOURCE = 78, // Err.Source -> the source Err.Raise gave ("" otherwise) + BAS_FN_ERR_SET = 79, // Err.Clear: () -> 0 clears the error; Err.Number = n: (n) -> 0 sets it (its text the number's) + BAS_FN_ERR_RAISE = 80, // Err.Raise number [, source [, description]] -> raises the error + BAS_FN_ERROR_TEXT = 81, // ERROR$([n]) / ERROR([n]) -> the text of error n (none: the current error's) + BAS_FN_LIKE = 82, // a LIKE pattern: (a, pattern) -> a matches the pattern (Option Compare decides case) + BAS_FN_FILEATTR = 83, // FILEATTR(n, attribute) -> the open mode of file n (BAS_FILEATTR_MODE) or its system handle (BAS_FILEATTR_HANDLE) + BAS_FN_QBCOLOR = 84, // QBCOLOR(n) -> QBasic colour n (0..15) as an RGB value + BAS_FN_NEW_FORM = 85, // New FormName (compiler-generated): (name) -> a new copy of the form (its own window, controls and form-level variables) + BAS_FN_CALLBYNAME = 86, // CALLBYNAME(object, name, callType [, args...]) -> the member's value or result (Empty for vbLet / vbSet) + BAS_FN_LOADFORMTEXT = 87, // LOADFORMTEXT(frmText) -> a form loaded from .frm text + BAS_FN_CHRW = 88, // CHRW$(code) -> the DVX (code page 437) character for a Unicode code point ("?" when it has none) + BAS_FN_ASCW = 89, // ASCW(s) -> the Unicode code point of s's first character + BAS_FN_LOADPICTUREDATA = 90, // LOADPICTUREDATA(bytes) -> a Picture object for the bytes of a picture file + BAS_FN_FULLSCREEN_STATE = 91 // FULLSCREEN (a function) -> True while the program screen has (or keeps, behind the desktop) the whole screen } BasBuiltinFnE; // CallByName's call types (VB's vbMethod, vbGet, vbLet, vbSet) and its @@ -698,14 +704,25 @@ typedef enum { #define BAS_VT_ARRAY 8192 // The program-screen functions are numbered BAS_FN_CLS through -// BAS_FN_LINE_INPUT and BAS_FN_PCOPY through BAS_FN_PMAP (the VM hands -// them to runtime/screen.c; see basBuiltinIsScreen). +// BAS_FN_LINE_INPUT, BAS_FN_PCOPY through BAS_FN_PMAP, BAS_FN_PEN +// through BAS_FN_PEN_SET, and BAS_FN_FULLSCREEN_STATE (the VM hands them +// to runtime/screen.c; see basBuiltinIsScreen). #define BAS_FN_SCREEN_FIRST BAS_FN_CLS #define BAS_FN_SCREEN_LAST BAS_FN_LINE_INPUT #define BAS_FN_SCREEN2_FIRST BAS_FN_PCOPY #define BAS_FN_SCREEN2_LAST BAS_FN_PMAP #define BAS_FN_SCREEN3_FIRST BAS_FN_PEN #define BAS_FN_SCREEN3_LAST BAS_FN_PEN_SET +#define BAS_FN_SCREEN4_FIRST BAS_FN_FULLSCREEN_STATE +#define BAS_FN_SCREEN4_LAST BAS_FN_FULLSCREEN_STATE + +// A form's FullScreen event's State (dvxFullScreen* in BASIC): the whole +// screen is the form's, the desktop is back while another window has the +// focus (the form gets it again with the focus), or it is over (no video +// mode could be set again). +#define BAS_FULLSCREEN_ENDED 0 +#define BAS_FULLSCREEN_ACTIVE 1 +#define BAS_FULLSCREEN_SUSPENDED 2 // PEN(n) (QBasic's light pen; the mouse on the program screen) and the // PEN statement's argument. @@ -935,12 +952,13 @@ static inline bool basBuiltinArity(uint8_t fn, int32_t *minArgs, int32_t *maxArg switch (fn) { case BAS_FN_NOW: case BAS_FN_DATE: case BAS_FN_TIME: case BAS_FN_INKEY: case BAS_FN_CSRLIN: case BAS_FN_ERR_DESCRIPTION: - case BAS_FN_ERR_SOURCE: + case BAS_FN_ERR_SOURCE: case BAS_FN_FULLSCREEN_STATE: *minArgs = 0; *maxArgs = 0; return true; case BAS_FN_QBCOLOR: case BAS_FN_NEW_FORM: case BAS_FN_LOADFORMTEXT: + case BAS_FN_CHRW: case BAS_FN_ASCW: case BAS_FN_LOADPICTUREDATA: case BAS_FN_STRREVERSE: case BAS_FN_DATEVALUE: case BAS_FN_TIMEVALUE: case BAS_FN_YEAR: case BAS_FN_MONTH: case BAS_FN_DAY: case BAS_FN_HOUR: case BAS_FN_MINUTE: case BAS_FN_SECOND: @@ -1126,7 +1144,7 @@ static inline bool basBuiltinArity(uint8_t fn, int32_t *minArgs, int32_t *maxArg // True for the program-screen functions (runtime/screen.c). static inline bool basBuiltinIsScreen(uint8_t fn) { - return (fn >= BAS_FN_SCREEN_FIRST && fn <= BAS_FN_SCREEN_LAST) || (fn >= BAS_FN_SCREEN2_FIRST && fn <= BAS_FN_SCREEN2_LAST) || (fn >= BAS_FN_SCREEN3_FIRST && fn <= BAS_FN_SCREEN3_LAST); + return (fn >= BAS_FN_SCREEN_FIRST && fn <= BAS_FN_SCREEN_LAST) || (fn >= BAS_FN_SCREEN2_FIRST && fn <= BAS_FN_SCREEN2_LAST) || (fn >= BAS_FN_SCREEN3_FIRST && fn <= BAS_FN_SCREEN3_LAST) || (fn >= BAS_FN_SCREEN4_FIRST && fn <= BAS_FN_SCREEN4_LAST); } @@ -1369,41 +1387,36 @@ static inline int32_t basOpcodeOperandSize(uint8_t op) { // ============================================================ // // Bytecode is always little-endian regardless of the host; the compiler, -// the compactor and the serializer all go through these. +// the compactor and the serializer all go through these (signed and +// unsigned forms of dvxEndian.h's loads and stores). static inline int16_t basReadI16LE(const uint8_t *p) { - return (int16_t)basReadU16LE(p); + return (int16_t)dvxGetLe16(p); } static inline int32_t basReadI32LE(const uint8_t *p) { - return (int32_t)((uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24)); + return (int32_t)dvxGetLe32(p); } static inline uint16_t basReadU16LE(const uint8_t *p) { - return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); + return dvxGetLe16(p); } static inline void basWriteI16LE(uint8_t *p, int16_t v) { - basWriteU16LE(p, (uint16_t)v); + dvxPutLe16(p, (uint16_t)v); } static inline void basWriteI32LE(uint8_t *p, int32_t v) { - uint32_t u = (uint32_t)v; - - p[0] = (uint8_t)(u & 0xFF); - p[1] = (uint8_t)((u >> 8) & 0xFF); - p[2] = (uint8_t)((u >> 16) & 0xFF); - p[3] = (uint8_t)((u >> 24) & 0xFF); + dvxPutLe32(p, (uint32_t)v); } static inline void basWriteU16LE(uint8_t *p, uint16_t v) { - p[0] = (uint8_t)(v & 0xFF); - p[1] = (uint8_t)((v >> 8) & 0xFF); + dvxPutLe16(p, v); } diff --git a/src/apps/kpunch/dvxbasic/compiler/resolveExpr.c b/src/apps/kpunch/dvxbasic/compiler/resolveExpr.c index ef7e9a91..677dc161 100644 --- a/src/apps/kpunch/dvxbasic/compiler/resolveExpr.c +++ b/src/apps/kpunch/dvxbasic/compiler/resolveExpr.c @@ -98,7 +98,9 @@ static const char *const sArgsWeekdayName[] = { "Weekday", "Abbreviate", "FirstD static const BasBuiltinT sBuiltins[] = { // String functions {"ASC", OP_STR_ASC, 0, 1, 1, BAS_NODE_TYPE_UNKNOWN, BI_PURE, 0, NULL}, + {"ASCW", OP_BUILTIN, BAS_FN_ASCW, 0, 0, BAS_TYPE_LONG, BI_PURE, 0, NULL}, {"CHR$", OP_STR_CHR, 0, 1, 1, BAS_TYPE_STRING, BI_PURE, 0, NULL}, + {"CHRW$", OP_BUILTIN, BAS_FN_CHRW, 0, 0, BAS_TYPE_STRING, BI_PURE, 0, NULL}, {"COMMAND$", OP_COMMAND, 0, 0, 1, BAS_TYPE_STRING, 0, 0, NULL}, {"DATE$", OP_DATE_STR, 0, 0, 0, BAS_TYPE_STRING, 0, 0, NULL}, {"ENVIRON$", OP_ENVIRON, 0, 1, 1, BAS_TYPE_STRING, 0, 0, NULL}, @@ -123,6 +125,7 @@ static const BasBuiltinT sBuiltins[] = { // VB's Variant forms of the string functions: Null text gives Null {"CHR", OP_STR_CHR, 0, 1, 1, BAS_TYPE_STRING, BI_PURE, 0, NULL}, + {"CHRW", OP_BUILTIN, BAS_FN_CHRW, 0, 0, BAS_TYPE_STRING, BI_PURE, 0, NULL}, {"HEX", OP_STR_HEX, 0, 1, 1, BAS_NODE_TYPE_UNKNOWN, BI_PURE, BAS_NULL_ARG_1, NULL}, {"LCASE", OP_STR_LCASE, 0, 1, 1, BAS_NODE_TYPE_UNKNOWN, BI_PURE, BAS_NULL_ARG_1, NULL}, {"LEFT", OP_STR_LEFT, 0, 2, 2, BAS_NODE_TYPE_UNKNOWN, BI_PURE, BAS_NULL_ARG_1, sArgsLeft}, @@ -180,6 +183,7 @@ static const BasBuiltinT sBuiltins[] = { // The program screen {"CSRLIN", OP_BUILTIN, BAS_FN_CSRLIN, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, + {"FULLSCREEN", OP_BUILTIN, BAS_FN_FULLSCREEN_STATE, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, {"INKEY$", OP_BUILTIN, BAS_FN_INKEY, 0, 0, BAS_TYPE_STRING, 0, 0, NULL}, {"PEN", OP_BUILTIN, BAS_FN_PEN, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, {"PMAP", OP_BUILTIN, BAS_FN_PMAP, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, @@ -245,6 +249,7 @@ static const BasBuiltinT sBuiltins[] = { {"ISOBJECT", OP_BUILTIN, BAS_FN_ISOBJECT, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, {"LOADFORMTEXT", OP_BUILTIN, BAS_FN_LOADFORMTEXT, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, {"LOADPICTURE", OP_BUILTIN, BAS_FN_LOADPICTURE, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, + {"LOADPICTUREDATA", OP_BUILTIN, BAS_FN_LOADPICTUREDATA, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, {"TYPENAME", OP_BUILTIN, BAS_FN_TYPENAME, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, {"VARTYPE", OP_BUILTIN, BAS_FN_VARTYPE, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL} }; diff --git a/src/apps/kpunch/dvxbasic/compiler/scope.c b/src/apps/kpunch/dvxbasic/compiler/scope.c index 03d683ca..b397554a 100644 --- a/src/apps/kpunch/dvxbasic/compiler/scope.c +++ b/src/apps/kpunch/dvxbasic/compiler/scope.c @@ -103,11 +103,10 @@ BasSymT *basScopeFindKind(const BasScopeTabT *tab, const char *name, uint8_t kin uint32_t basScopeHash(const char *name) { - uint32_t h = BAS_FNV1A_OFFSET; + uint32_t h = DVX_FNV_BASIS; for (; *name != '\0'; name++) { - h ^= (uint32_t)(uint8_t)basAsciiUpper(*name); - h *= BAS_FNV1A_PRIME; + h = dvxFnvByte(h, (uint8_t)basAsciiUpper(*name)); } return h; diff --git a/src/apps/kpunch/dvxbasic/ctrlover.dhs b/src/apps/kpunch/dvxbasic/ctrlover.dhs index 187b187d..7042a78b 100644 --- a/src/apps/kpunch/dvxbasic/ctrlover.dhs +++ b/src/apps/kpunch/dvxbasic/ctrlover.dhs @@ -92,7 +92,7 @@ Every control in DVX BASIC inherits a set of common properties, events, and meth ToolTipText String R/W Tooltip text shown after the mouse hovers over the control. Empty string or unset = no tooltip. HelpTopic String R/W Help topic opened by F1 while the control has the focus. Empty = the form's HelpTopic. F1 uses the project's help file. DragMode Integer R/W vbManual (0, the default): the program starts a drag with the Drag method. vbAutomatic (1): pressing the left mouse button on the control starts a drag. See Drag and Drop below. - DragIcon Picture R/W Picture shown under the mouse while the control is dragged: a picture file name (BMP, PNG, JPEG, or GIF) or a Picture object from LOADPICTURE (Set works too). Reading it gives a Picture object, which stands for the file name where a value is needed. Empty = a dotted outline the size of the control. + DragIcon Picture R/W Picture shown under the mouse while the control is dragged: a picture file name (BMP, PNG, JPEG, or GIF) or a Picture object from LOADPICTURE or LOADPICTUREDATA (Set works too). Reading it gives a Picture object, which stands for the file name where a value is needed. Empty = a dotted outline the size of the control. Parent Form R The form the control is on (Text1.Parent.Caption). Tag String R/W Any text the program wants to keep with the control (DVX does nothing with it), as in Visual Basic. Also in the .frm file. MousePointer Integer R/W The mouse pointer shown over the control (and the controls inside it that have none of their own): vbDefault (0, the control's own pointer), vbArrow (1), vbCrosshair (2), vbSizeNESW (6), vbSizeNS (7), vbSizeNWSE (8), vbSizeWE (9), vbHourglass (11), or vbNoDrop (12). A form's MousePointer is used where no control has one, and Screen.MousePointer over every form of the program. Change it in MouseMove to show different pointers over parts of a control. Also in the .frm file; another value is an error. @@ -169,7 +169,7 @@ Drop targets only react: nothing is moved or copied for you. Use Source in DragD Note that vbCancel is the MsgBox Cancel value (2), which as a Drag action means vbEndDrag; cancel a drag with Drag 0. -DragIcon takes a file name or a picture loaded with LOADPICTURE: Set Label1.DragIcon = LoadPicture("drag.bmp"). A missing file or one that is not a picture raises its error at LOADPICTURE; a file name that cannot be read when the drag starts shows the outline instead. +DragIcon takes a file name or a picture loaded with LOADPICTURE or LOADPICTUREDATA: Set Label1.DragIcon = LoadPicture("drag.bmp"). A missing file or one that is not a picture raises its error at LOADPICTURE; a file name that cannot be read when the drag starts shows the outline instead. .code ' Label1 has DragMode = 1 (vbAutomatic) in the form designer. @@ -335,7 +335,7 @@ PaintPicture's opcode says how each copied pixel is combined with the pixel alre vbTransparentCopy &H2000000 Every pixel of the picture but the key colour: a colour given after the opcode, else the colour of the picture's top-left pixel (DVX only) .endtable -vbGrayCopy and vbTransparentCopy copy the picture at its own size (width and height are not used): Picture1.PaintPicture "ship.bmp", x, y, 32, 32, 0, 0, 32, 32, vbTransparentCopy, vbMagenta draws a sprite. +vbGrayCopy and vbTransparentCopy copy the picture at its own size (width and height are not used): Picture1.PaintPicture "ship.bmp", x, y, 32, 32, 0, 0, 32, 32, vbTransparentCopy, vbMagenta draws a sprite. A picture file (or a LOADPICTURE picture) drawn again and again, a sprite in a game loop say, is read from the disk only the first time while the program runs and the file does not change (about a megabyte of pictures is kept that way). Any of the 256 raster operations of Windows can be given by its number; the bits work on the pixel values, so with fewer than 32 bits a pixel the results follow the display's colour format. diff --git a/src/apps/kpunch/dvxbasic/dvxbasic.res b/src/apps/kpunch/dvxbasic/dvxbasic.res index 891bc801..d38cc048 100644 --- a/src/apps/kpunch/dvxbasic/dvxbasic.res +++ b/src/apps/kpunch/dvxbasic/dvxbasic.res @@ -1,3 +1,25 @@ +# 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. + # dvxbasic.res -- Resource manifest for DVX BASIC icon32 icon icon32.bmp name text "DVX BASIC" diff --git a/src/apps/kpunch/dvxbasic/form.dhs b/src/apps/kpunch/dvxbasic/form.dhs index ed669675..d4f5d7c5 100644 --- a/src/apps/kpunch/dvxbasic/form.dhs +++ b/src/apps/kpunch/dvxbasic/form.dhs @@ -37,6 +37,11 @@ .index vbModal .index New (forms) .index WindowState +.index MaxWidth (form) +.index MaxHeight (form) +.index GameInput +.index FullScreen (form) +.index dvxFullScreenActive .index vbMinimized .index NewArgs .index COMMAND$ (new arguments) @@ -88,12 +93,16 @@ The Form is the top-level container representing a DVX window. It is declared in WindowState Integer 0 vbNormal (0), vbMinimized (1, shown only as an icon at the bottom of the screen), or vbMaximized (2, filling the screen). Set on a hidden form (or in the .frm file), it takes effect when the form is shown; reading it then gives the state the form will have. Controls Collection (none) The form's controls (read-only), in the order they were made: Me.Controls.Count, Me.Controls(1) (the first), Me.Controls("Text1"), For Each c In Me.Controls. Elements of a control array are in it too but have no name key. Each use makes a new Collection; changing it does not change the form. DesignMode Boolean False True puts the form in design mode, for a form designer: see Designing Forms below. Not in .frm files. - Icon Picture (DVX's) The picture shown for the window while it is minimized: a picture file name (BMP, PNG, JPEG, or GIF) or a picture from LOADPICTURE (Set works too); "" puts DVX's back. Reading it gives a Picture object. Also in the .frm file. A file that is not a picture is an error. + Icon Picture (DVX's) The picture shown for the window while it is minimized: a picture file name (BMP, PNG, JPEG, or GIF) or a picture from LOADPICTURE or LOADPICTUREDATA (Set works too); "" puts DVX's back. Reading it gives a Picture object. Also in the .frm file. A file that is not a picture is an error. Tag String "" Any text the program wants to keep with the form, as for controls. MousePointer Integer 0 The mouse pointer over the form where its controls have none (see MousePointer under Common Properties). FontName String "System" The form's font: the font of every control on it that has none of its own, and of the form's Print and TextWidth. FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru go with it, as for controls (see Fonts under Common Properties). Also in the .frm file. ActiveControl Control (none) The control of the form that has the keyboard focus (read-only), Nothing when none of them has it: Me.ActiveControl.SelText for an Edit menu's Copy. Screen.ActiveControl is the focused control of the whole program. Not in .frm files. DesignSelection String "" The name of the control (Text1, or Text1(2) in a control array) whose selection handles are drawn while the form is in design mode; "" for none. Not in .frm files. + MaxWidth Integer 0 The widest the user can make the window, by dragging or maximizing; 0 is the screen's width. A window wider than a new MaxWidth shrinks to it. Also in the .frm file. + MaxHeight Integer 0 The tallest the window can be made, as MaxWidth; 0 is the screen's height. + GameInput Boolean False True for a game: while the form has the keyboard, every key goes to it -- none to F1 (help), F10 or Alt (the menu bar), menu shortcuts, or DVX's shortcut keys. Only Alt+Tab, Ctrl+Esc, and Alt+F4 still reach DVX. Also in the .frm file. + FullScreen Boolean False True gives the form the whole screen: a video mode as large as the form's inside (its controls and drawing, without the title bar), the form shown on it at the largest whole multiple of its size that fits, centred on black, kept up to date as it changes. The mouse works on it as in the window, and every key goes to it as with GameInput. A hidden form is shown first. When the user switches to another program (Alt+Tab, Ctrl+Esc), or the program shows a message box or another form, the desktop comes back; the form gets the whole screen again with the focus. Reads False when no video mode could be set (the form stays in its window). False gives the desktop back. Run time only; see the FullScreen event. .endtable Maximizing a window, or restoring a maximized one, changes its size, so Resize fires. Minimizing does not change the size and fires no Resize. A minimized window keeps its maximized state: set WindowState = vbMaximized to bring it back filling the screen, vbNormal to bring it back at its normal size. @@ -118,6 +127,7 @@ frmTerminal.WindowState = vbMinimized ' starts as an icon DragDrop Source As Control, X As Integer, Y As Integer Fires when a dragged control is dropped on an empty part of the form. X and Y are where, in the form. DragOver Source As Control, X As Integer, Y As Integer, State As Integer Fires while a dragged control moves over an empty part of the form. State is vbEnter, vbLeave, or vbOver. NewArgs (none) Fires when the running program is handed new arguments: COMMAND$ has changed (see New Arguments below). + FullScreen State As Integer With FullScreen True: dvxFullScreenActive (1) when the form has the whole screen (draw anything that must look new then), dvxFullScreenSuspended (2) when the desktop came back while another window has the focus, dvxFullScreenEnded (0) when the form's video mode could not be set again (FullScreen is False now). DesignMouse Action As Integer, Button As Integer, X As Integer, Y As Integer, CtrlName As String, Handle As Integer In design mode: the mouse was pressed, moved, released, or double-clicked in the form. See Designing Forms below. DesignKey KeyCode As Integer, Shift As Integer In design mode: a key was pressed in the form (KeyCode and Shift as in KeyDown). DesignPropertyChanged CtrlName As String, PropertyName As String In design mode: the code of a UserControl on the form called PropertyChanged. See Designing Forms below. diff --git a/src/apps/kpunch/dvxbasic/formrt/basRunHost.c b/src/apps/kpunch/dvxbasic/formrt/basRunHost.c index a4b0f811..2432ede8 100644 --- a/src/apps/kpunch/dvxbasic/formrt/basRunHost.c +++ b/src/apps/kpunch/dvxbasic/formrt/basRunHost.c @@ -40,7 +40,12 @@ #define RUN_HOST_ERR_FORMRT "Out of memory creating the form runtime." #define RUN_HOST_ERR_OTHER "Runtime error" -// A standalone program's runtime, its module, and the program it runs for +// A program's slice also ends at the next BIOS clock tick (about 55 ms) +// after it began, however few its steps (basVmSetSliceClock) +#define RUN_HOST_SLICE_TICKS 1 + +// A runtime ended with its program by force (standalone or endWithApp), +// its module when it owns it (standalone), and the program it runs for typedef struct { BasFormRtT *rt; BasModuleT *module; @@ -68,11 +73,13 @@ bool basRunHostAlive(const BasFormRtT *rt) { // The shell's kill hook: a program ended by force has its runtimes ended // as its own end would (its windows are still there, none of its code -// runs). +// runs), except that what it drew for the printer is thrown away, not +// printed (that would hold the kill for seconds). static void basRunHostAppKilled(int32_t appId) { for (ptrdiff_t i = arrlen(sHosts) - 1; i >= 0; i--) { if (i < arrlen(sHosts) && sHosts[i].appId == appId) { dvxLog("BASIC: '%s' ended by force; its runtime freed", sHosts[i].rt->title); + basFormRtPrinterKill(sHosts[i].rt); basRunHostDestroy(sHosts[i].rt); } } @@ -116,6 +123,9 @@ BasFormRtT *basRunHostCreate(AppContextT *ctx, BasModuleT *module, const BasRunS basVmSetDoEventsCallback(vm, spec->doEventsFn, spec->doEventsCtx); } + basVmSetSliceClock(vm, platformBiosTicks(), RUN_HOST_SLICE_TICKS); + basVmSetMicrosClock(vm, platformMicros); + // DECLARE LIBRARY functions BasExternCallbacksT extCb; @@ -142,8 +152,8 @@ BasFormRtT *basRunHostCreate(AppContextT *ctx, BasModuleT *module, const BasRunS snprintf(rt->helpFile, sizeof(rt->helpFile), "%s", spec->helpFile); } - if (spec->standalone) { - HostRecT rec = { rt, module, ctx->currentAppId }; + if (spec->standalone || spec->endWithApp) { + HostRecT rec = { rt, spec->standalone ? module : NULL, ctx->currentAppId }; arrput(sHosts, rec); diff --git a/src/apps/kpunch/dvxbasic/formrt/basRunHost.h b/src/apps/kpunch/dvxbasic/formrt/basRunHost.h index e7bcac7d..0596f719 100644 --- a/src/apps/kpunch/dvxbasic/formrt/basRunHost.h +++ b/src/apps/kpunch/dvxbasic/formrt/basRunHost.h @@ -39,6 +39,9 @@ #include #include +// A program's task stack: the standalone stub's, and the IDE's runs. +#define BAS_RUN_STACK_SIZE 65536 + // What a program is run with. Any pointer may be NULL (none / default). typedef struct { const char *appDir; // App.Path; App.Data is its DATA folder (the files it ships) @@ -54,6 +57,7 @@ typedef struct { void *doEventsCtx; bool suppressErrorDialog; // the host reports runtime errors itself bool standalone; // the .app's own program (basstub): see basRunHostCreate + bool endWithApp; // ended with the program it runs for when that is ended by force (the IDE's runs on a task of their own); standalone ones are } BasRunSpecT; // A form's caption saved while " [break]" is appended to it. @@ -71,10 +75,10 @@ bool basRunHostAlive(const BasFormRtT *rt); // A VM running module (borrowed: it must outlive the runtime) and its // form runtime, set up from spec. A standalone program's runtime -// (spec->standalone, once it is made) owns the module, freed with it, and -// is ended as the program's own end would when the program is ended by -// force (the shell's kill hook); the IDE and basdev end theirs -// themselves. Returns the runtime (rt->vm is the +// (spec->standalone, once it is made) owns the module, freed with it. It +// and one made with spec->endWithApp are ended as the program's own end +// would when the program they run for (ctx->currentAppId) is ended by +// force (the shell's kill hook); basdev ends its runs itself. Returns the runtime (rt->vm is the // VM), or NULL with *outError set when memory runs out. A module the VM // refuses (too many globals) leaves rt->vm->module NULL; running it then // fails with the reason in basVmGetError. diff --git a/src/apps/kpunch/dvxbasic/formrt/formCtl.c b/src/apps/kpunch/dvxbasic/formrt/formCtl.c index 02f32d5d..bfae120d 100644 --- a/src/apps/kpunch/dvxbasic/formrt/formCtl.c +++ b/src/apps/kpunch/dvxbasic/formrt/formCtl.c @@ -61,9 +61,11 @@ // Running code: a program's own controls run in its VM with its rules (an // untrapped error ends the program). An installed control file has one VM // shared by every program that uses its controls: it has no DoEvents host -// (its code never yields), a call from another task while it runs code is -// refused (the event is dropped, a paint keeps the old picture; logged -// once), and an untrapped error is shown and the control carries on. +// (its code never yields, so a call that runs too long is stopped and its +// waits are refused: UC_RUN_LIMIT_SLICES), a call from another task while +// it runs code is refused (the event is dropped, a paint keeps the old +// picture; logged once), and an untrapped error is shown and the control +// carries on. // // Lifetime: a widget destroyed while its code runs (a host handler that // unloads the form) stays allocated, cut off from its widgets, until the @@ -79,7 +81,6 @@ #include "dvxPlat.h" #include "thirdparty/stb_ds_wrap.h" -#include #include #include #include @@ -100,6 +101,7 @@ // The mouse event handlers' four arguments, a key handler's two. #define UC_MOUSE_ARGS 4 #define UC_KEY_ARGS 2 +#define UC_EVENT_ARGS_MAX UC_MOUSE_ARGS // WGT_METHOD_ON_MOUSE_EVENT gives a pressed or released button as its // ordinal (1 left, 2 right, 3 middle); BASIC handlers get VB's button @@ -130,6 +132,12 @@ static const int32_t sButtonBits[UC_MOUSE_ORDINALS] = { 0, MOUSE_LEFT, MOUSE_RIG #define UC_PROGRAM_KEY_EXTRA 12 #define UC_TYPE_KEY_MAX (BAS_MAX_IDENT + UC_PROGRAM_KEY_EXTRA) +// An installed control file's code has no event pump, so nothing else runs +// while it does: a call into it (or its module code) that runs more than +// this many slices (about two seconds) is stopped with error 18, and its +// waits (SLEEP ...) are refused (basVmSetRunLimit). +#define UC_RUN_LIMIT_SLICES 40 + // An installed control file: its module, and the runtime and VM its code // runs in (made with its first control). A designer's project controls // (basCtlLoadDesign) are one too. @@ -172,6 +180,14 @@ typedef struct { bool retired; // unregistered (its program ended); freed with its last control } UcClassT; +// An event of a control whose program runs on another task, waiting for +// that program's own (ucDefer, basUcPump), its arguments copied. +typedef struct { + int32_t evt; + int32_t argc; + BasValueT args[UC_EVENT_ARGS_MAX]; +} UcPendingT; + // One control (widget). typedef struct { UcClassT *cls; @@ -192,6 +208,7 @@ typedef struct { bool inPaint; bool dead; // its widget is gone; freed when depth is 0 bool orphan; // its program ended first: no object, no code, only the widget left + UcPendingT *pending; // stb_ds: events from other tasks, oldest first (basUcPump) } UcInstT; static UcClassT **sClasses = NULL; // stb_ds @@ -199,20 +216,20 @@ static uint32_t sProgramKeys = 0; // serials of program-owned ty static UcInstT **sInsts = NULL; // stb_ds static UcFileT **sFiles = NULL; // stb_ds static BasUcPropChangedFnT sPropChanged = NULL; -static uint32_t (*sTaskIdFn)(void) = NULL; -static bool sTaskIdResolved = false; // Function prototypes (alphabetical) int32_t basCtlLoadDesign(const BasFormRtT *rt, BasModuleT *module, const char *const *headers, int32_t count, const char *path); bool basCtlLoadFile(const char *path); bool basCtlUnloadDesign(const BasFormRtT *rt); bool basFormRtRegisterCtl(BasFormRtT *rt, const char *text, int32_t len); +bool basUcBusy(void); bool basUcHeaderParse(const char *text, int32_t len, BasUcHeaderT *out); bool basUcIconPath(const WgtIfaceT *iface, char *out, int32_t outSize); const BasUcHeaderT *basUcIfaceHeader(const WgtIfaceT *iface); bool basUcIfaceVisible(const BasFormRtT *rt, const WgtIfaceT *iface); bool basUcIsControlType(const char *typeName); const WgtIfaceT *basUcOwnIface(const BasFormRtT *rt, const char *typeName); +void basUcPump(void); BasControlT *basUcResolveSurface(const BasFormRtT *rt, void *ref); const char *basUcResolveType(const BasFormRtT *rt, const char *typeName); void basUcRuntimeEnd(BasFormRtT *rt); @@ -227,6 +244,7 @@ static bool ucCallEvent(UcInstT *inst, int32_t evt, const BasValueT * static const BasClassInfoT *ucClassInfo(const UcClassT *cls); static UcClassT *ucClassOf(const WgtIfaceT *iface); static WidgetT *ucCreate(WidgetT *parent, const WgtIfaceT *iface); +static bool ucDefer(UcInstT *inst, int32_t evt, const BasValueT *args, int32_t argc); static void ucDestroy(WidgetT *w); static void ucDropObject(UcInstT *inst); static bool ucDynCall(WidgetT *w, int32_t methodIdx, const WgtValueT *args, int32_t argc, WgtValueT *ret); @@ -238,6 +256,7 @@ static void ucEventHost(void *ctx, const BasObjectT *obj, int32_t eve static bool ucFileReady(UcFileT *file, WidgetT *parent); static UcInstT *ucFindInst(const BasObjectT *obj); static void ucFocus(WidgetT *w); +static bool ucForeign(const UcInstT *inst); static void ucFreeClass(UcClassT *cls); static void ucFreeInst(UcInstT *inst); static void ucHold(UcInstT *inst); @@ -256,7 +275,9 @@ static const char *ucNameTaken(const BasFormRtT *rt, const UcFileT *file, co static UcClassT *ucOwnClass(const BasFormRtT *rt, const char *name); static const BasFormRtT *ucOwner(const UcClassT *cls); static void ucPaintNow(UcInstT *inst); +static void ucPendingFree(UcPendingT *ev); static void ucPoll(WidgetT *w, WindowT *win); +static void ucPollNow(UcInstT *inst); static bool ucPrivate(const UcClassT *cls); static bool ucPrivateIface(const WgtIfaceT *iface); static UcClassT *ucRegister(BasFormRtT *rt, UcFileT *file, const BasModuleT *module, const BasUcHeaderT *header); @@ -264,7 +285,6 @@ static bool ucRelease(UcInstT *inst); static void ucRetireClass(UcClassT *cls); static BasFormRtT *ucRt(const UcClassT *cls); static UcInstT *ucSurfaceInst(const BasFormRtT *rt, const void *ref); -static uint32_t ucTaskId(void); static WgtValueT ucToWgt(UcInstT *inst, BasValueT v, uint8_t want); static bool ucUserMode(const UcInstT *inst); static bool ucWheel(WidgetT *w, int32_t delta); @@ -429,6 +449,19 @@ bool basFormRtRegisterCtl(BasFormRtT *rt, const char *text, int32_t len) { } +// A control has events from another task waiting for its program's turn +// (basFormRtBusy). +bool basUcBusy(void) { + for (int32_t i = 0; i < (int32_t)arrlen(sInsts); i++) { + if (arrlen(sInsts[i]->pending) > 0) { + return true; + } + } + + return false; +} + + bool basUcHeaderParse(const char *text, int32_t len, BasUcHeaderT *out) { char value[FRM_MAX_LINE_LEN]; @@ -520,6 +553,46 @@ const WgtIfaceT *basUcOwnIface(const BasFormRtT *rt, const char *typeName) { } +// The program's turn on its own task (basFormRtUpdate): the events its +// controls got on other tasks run, oldest first, then their Resize and +// Paint. Every control taken is held, so one a handler destroys stays +// readable until all have run. +void basUcPump(void) { + UcInstT **todo = NULL; + + for (int32_t i = 0; i < (int32_t)arrlen(sInsts); i++) { + const BasFormRtT *rt = ucRt(sInsts[i]->cls); + + if (!sInsts[i]->dead && !sInsts[i]->orphan && rt && rt->ownTask == basFormRtTaskId()) { + ucHold(sInsts[i]); + arrput(todo, sInsts[i]); + } + } + + for (int32_t i = 0; i < (int32_t)arrlen(todo); i++) { + UcInstT *inst = todo[i]; + + while (arrlen(inst->pending) > 0 && !inst->dead) { + UcPendingT ev = inst->pending[0]; + + arrdel(inst->pending, 0); + ucCallEvent(inst, ev.evt, ev.args, ev.argc); + ucPendingFree(&ev); + } + + if (!inst->dead) { + ucPollNow(inst); + } + } + + for (int32_t i = 0; i < (int32_t)arrlen(todo); i++) { + ucRelease(todo[i]); + } + + arrfree(todo); +} + + BasControlT *basUcResolveSurface(const BasFormRtT *rt, void *ref) { UcInstT *inst = ucSurfaceInst(rt, ref); @@ -835,7 +908,16 @@ static bool ucCallEvent(UcInstT *inst, int32_t evt, const BasValueT *args, int32 int32_t proc = ucClassInfo(inst->cls)->ucProcs[evt]; BasFormRtT *prev = NULL; - if (proc < 0 || inst->dead || !inst->obj || !ucEnter(inst, &prev)) { + if (proc < 0 || inst->dead || !inst->obj) { + return false; + } + + // Its program's code runs only on its program's task + if (ucForeign(inst)) { + return ucDefer(inst, evt, args, argc); + } + + if (!ucEnter(inst, &prev)) { return false; } @@ -943,6 +1025,38 @@ static WidgetT *ucCreate(WidgetT *parent, const WgtIfaceT *iface) { } +// An event of a control whose program runs on another task waits for +// that program's turn (basUcPump); a newer MouseMove replaces one still +// waiting. True: it will run. +static bool ucDefer(UcInstT *inst, int32_t evt, const BasValueT *args, int32_t argc) { + UcPendingT *ev = NULL; + + for (int32_t i = 0; evt == BAS_UC_EVT_MOUSEMOVE && i < (int32_t)arrlen(inst->pending); i++) { + if (inst->pending[i].evt == evt) { + ev = &inst->pending[i]; + ucPendingFree(ev); + } + } + + if (!ev) { + UcPendingT fresh; + + memset(&fresh, 0, sizeof(fresh)); + arrput(inst->pending, fresh); + ev = &arrlast(inst->pending); + } + + ev->evt = evt; + ev->argc = DVX_MIN(argc, UC_EVENT_ARGS_MAX); + + for (int32_t i = 0; i < ev->argc; i++) { + ev->args[i] = basValCopy(args[i]); + } + + return true; +} + + // The widget is being destroyed (its canvas already is): UserControl_ // Terminate, then the object goes. While code of it runs, the control // stays allocated, cut off from the widgets, until the calls return. The @@ -1084,7 +1198,7 @@ static bool ucEnter(UcInstT *inst, BasFormRtT **outPrev) { } if (file) { - uint32_t task = ucTaskId(); + uint32_t task = basFormRtTaskId(); if (file->depth > 0 && file->task != task) { if (!file->refusedLogged) { @@ -1230,6 +1344,8 @@ static bool ucFileReady(UcFileT *file, WidgetT *parent) { } basVmSetEventHost(file->rt->vm, ucEventHost, NULL); + basVmSetRunLimit(file->rt->vm, UC_RUN_LIMIT_SLICES); + file->rt->ownTask = BAS_RT_ANY_TASK; // A designer's controls never run the module-level code: it is the // program's start @@ -1278,6 +1394,16 @@ static void ucFocus(WidgetT *w) { } +// The control's program runs on another task: its code runs on that +// task alone (two tasks in one VM would interleave their calls). An +// installed file's runs on any (ucEnter refuses a second while it runs). +static bool ucForeign(const UcInstT *inst) { + const BasFormRtT *rt = ucRt(inst->cls); + + return rt && rt->ownTask != BAS_RT_ANY_TASK && rt->ownTask != basFormRtTaskId(); +} + + // A class's memory (out of the registry, its controls gone). static void ucFreeClass(UcClassT *cls) { for (int32_t i = 0; i < (int32_t)arrlen(cls->texts); i++) { @@ -1319,6 +1445,11 @@ static void ucFreeInst(UcInstT *inst) { basStringUnref(inst->hold); } + for (int32_t i = 0; i < (int32_t)arrlen(inst->pending); i++) { + ucPendingFree(&inst->pending[i]); + } + + arrfree(inst->pending); basFormRtDrawFree(&inst->surface); UcClassT *cls = inst->cls; @@ -1647,21 +1778,42 @@ static void ucPaintNow(UcInstT *inst) { } -// Between frames: a size the layout gave the control makes a canvas of -// that size (cleared to its BackColor), then Resize and Paint; a Refresh -// that came during Paint paints again. +// A waiting event's copied arguments go. +static void ucPendingFree(UcPendingT *ev) { + for (int32_t i = 0; i < ev->argc; i++) { + basValRelease(&ev->args[i]); + } + + ev->argc = 0; +} + + +// The widget poll between frames (ucPollNow). +// cppcheck-suppress constParameterCallback ; a widget class method: WidgetClassT fixes the signature static void ucPoll(WidgetT *w, WindowT *win) { UcInstT *inst = ucInstOf(w); (void)win; - if (!inst) { + // On another task than its program's, its program's turn does it + // (basUcPump) + if (!inst || ucForeign(inst)) { return; } ucHold(inst); + ucPollNow(inst); + ucRelease(inst); +} - if (inst->sizeDirty && w->w > 0 && w->h > 0) { + +// ucPoll's work (the caller holds inst): a new size makes a canvas of +// that size, then Resize and Paint; a Refresh that came during Paint +// paints again. +static void ucPollNow(UcInstT *inst) { + WidgetT *w = inst->self; + + if (inst->sizeDirty && w && w->w > 0 && w->h > 0) { inst->sizeDirty = false; inst->lastW = w->w; inst->lastH = w->h; @@ -1674,8 +1826,6 @@ static void ucPoll(WidgetT *w, WindowT *win) { if (inst->paintPending && !inst->dead) { ucPaintNow(inst); } - - ucRelease(inst); } @@ -1831,18 +1981,6 @@ static UcInstT *ucSurfaceInst(const BasFormRtT *rt, const void *ref) { } -// The running task (the task switcher's, found at run time; 0 without -// one). -static uint32_t ucTaskId(void) { - if (!sTaskIdResolved) { - sTaskIdResolved = true; - sTaskIdFn = (uint32_t (*)(void))dlsym(NULL, DVX_SYM("tsCurrentId")); - } - - return sTaskIdFn ? sTaskIdFn() : 0; -} - - // A BASIC value as the widget value the host gets: its text held by the // control until the next value it gives. static WgtValueT ucToWgt(UcInstT *inst, BasValueT v, uint8_t want) { diff --git a/src/apps/kpunch/dvxbasic/formrt/formDesign.c b/src/apps/kpunch/dvxbasic/formrt/formDesign.c index fda082f3..ff4d4dd3 100644 --- a/src/apps/kpunch/dvxbasic/formrt/formDesign.c +++ b/src/apps/kpunch/dvxbasic/formrt/formDesign.c @@ -967,8 +967,9 @@ const char *CtlTypeEventParams(const char *typeName, const char *eventName) { // designer.bas: CTL_FLAG_CONTAINER (holds controls) and CTL_FLAG_DESIGNABLE -// (can be created with CreateControl, so placed in a designer); -1 for an -// unknown type. +// (can be created with CreateControl, so placed in a designer: every type +// can, an Image or ImageButton with an empty picture); -1 for an unknown +// type. int32_t CtlTypeFlags(const char *typeName) { const WgtIfaceT *iface = basCtlIface(typeName); @@ -976,7 +977,7 @@ int32_t CtlTypeFlags(const char *typeName) { return BAS_CTL_TYPE_UNKNOWN; } - return (iface->isContainer ? BAS_CTL_FLAG_CONTAINER : 0) | ((iface->createSig != WGT_CREATE_PARENT_DATA) ? BAS_CTL_FLAG_DESIGNABLE : 0); + return (iface->isContainer ? BAS_CTL_FLAG_CONTAINER : 0) | BAS_CTL_FLAG_DESIGNABLE; } diff --git a/src/apps/kpunch/dvxbasic/formrt/formGfx.c b/src/apps/kpunch/dvxbasic/formrt/formGfx.c index f1512b27..02d0792b 100644 --- a/src/apps/kpunch/dvxbasic/formrt/formGfx.c +++ b/src/apps/kpunch/dvxbasic/formrt/formGfx.c @@ -67,6 +67,8 @@ #include #include #include +#include +#include #define SCREEN_SCALE_MAX 3 // largest whole-number window scale #define SCREEN_PIXEL_SINGLE 1 // a framebuffer pixel one window pixel across / down (square) @@ -117,6 +119,13 @@ #define DRAW_ACCEL_ENABLED_ARG 1 // PrintAccel text [, enabled] #define DRAW_ACCEL_MAX_ARGS 2 #define DRAW_PAINT_KEY_ARG 10 // PaintPicture ... , vbTransparentCopy, key colour +#define DRAW_NO_COLUMN (-1) // PaintPicture: a destination column with no source pixel + +// PaintPicture's pictures kept decoded for the runtime (drawPictureLoad): +// at most this many bytes, and a file changed this recently is decoded +// again (FAT times are 2 seconds apart) +#define DRAW_PICS_KEEP_BYTES (1024 * 1024) +#define DRAW_PICS_TIME_SEC 2 // The scale units (BAS_SCALE_*) in twips: 15 to a pixel, as VB on a // standard (96 DPI) display, whatever the real display is; characters are @@ -299,7 +308,9 @@ static bool drawIsSurface(const BasControlT *ctrl); static bool drawMeasure(BasDrawT *d, const BitmapFontT *font, DrawMethodE method, const BasValueT *args, int32_t argc, BasValueT *result); static void drawPaintKeyed(BasFormRtT *rt, BasDrawT *d, WidgetT *src, const uint8_t *pixels, int32_t pitch, uint32_t opcode, const BasValueT *args, int32_t argc, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy); static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, BasValueT *args, int32_t argc); -static bool drawPictureOf(BasFormRtT *rt, BasFormT *form, BasValueT v, WidgetT **src, uint8_t **pixels, int32_t *w, int32_t *h, int32_t *pitch, const BasGfxT **scale); +static void drawPaintRows(BasFormRtT *rt, BasDrawT *d, WidgetT *src, const uint8_t *pixels, int32_t pitch, int32_t srcW, int32_t srcH, uint32_t opcode, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh); +static uint8_t *drawPictureLoad(BasFormRtT *rt, const char *path, const BasPictureT *pic, int32_t *w, int32_t *h, int32_t *pitch, bool *owned); +static bool drawPictureOf(BasFormRtT *rt, BasFormT *form, BasValueT v, WidgetT **src, uint8_t **pixels, int32_t *w, int32_t *h, int32_t *pitch, const BasGfxT **scale, bool *owned); static int32_t drawPixelSpan(const BasGfxT *g, double length, bool across); static void drawPrint(BasDrawT *d, const BitmapFontT *font, BasValueT value, int32_t mode); static uint32_t drawRasterOp(uint32_t rop, uint32_t pattern, uint32_t source, uint32_t dest); @@ -520,6 +531,16 @@ BasDrawT *basFormRtDrawNew(AppContextT *ctx, const BasGfxSurfaceT *surf, double } +void basFormRtDrawPicsFree(BasFormRtT *rt) { + for (int32_t i = 0; i < (int32_t)arrlen(rt->drawPics); i++) { + dvxFreeImage(rt->drawPics[i].pixels); + } + + arrfree(rt->drawPics); + rt->drawPicsBytes = 0; +} + + // Columns and lines count charW x charH pixel cells from the left edge, // CurrentX / CurrentY kept in the scale; text moves CurrentX by the width // emit drew it at. @@ -530,24 +551,27 @@ uint8_t *basFormRtDrawPictureRgb(BasFormRtT *rt, BasFormT *form, BasValueT pic, uint8_t *pixels = NULL; WidgetT *src = NULL; int32_t pitch = 0; - int32_t bpp = rt->ctx->display.format.bytesPerPixel; + bool owned = false; + DisplayT canvas; - if (!drawPictureOf(rt, form, pic, &src, &pixels, w, h, &pitch, scale)) { + if (!drawPictureOf(rt, form, pic, &src, &pixels, w, h, &pitch, scale, &owned)) { return NULL; } - uint8_t *rgb = (uint8_t *)malloc((size_t)*w * (size_t)*h * DRAW_RGB_BYTES); + // A PictureBox's (or form's) pixels are its canvas's, in the display's + // format either way + const uint8_t *buf = pixels; - for (int32_t y = 0; rgb && y < *h; y++) { - for (int32_t x = 0; x < *w; x++) { - uint8_t *p = rgb + ((size_t)y * (size_t)*w + (size_t)x) * DRAW_RGB_BYTES; - uint32_t pixel = pixels ? readPixel(pixels + y * pitch + x * bpp, bpp) : wgtCanvasGetPixel(src, x, y); - - unpackColor(&rt->ctx->display, pixel, &p[0], &p[1], &p[2]); - } + if (!buf && wgtCanvasGetDisplay(src, &canvas)) { + buf = canvas.backBuf; + pitch = canvas.pitch; } - dvxFreeImage(pixels); + uint8_t *rgb = buf ? dvxBufferToRgb(&rt->ctx->display, buf, *w, *h, pitch) : NULL; + + if (owned) { + dvxFreeImage(pixels); + } if (!rgb) { basFormRtRuntimeError(rt, "Out of memory", "There is not enough memory for the picture."); @@ -1117,34 +1141,37 @@ static void drawPaintKeyed(BasFormRtT *rt, BasDrawT *d, WidgetT *src, const uint // destination (nearest pixel), each pixel combined with the one there and // the FillColor pattern by the raster operation opcode (vbSrcCopy when // left out; vbSrcAnd, vbSrcPaint, vbSrcInvert ... -- any of Windows' 256). +// A plain copy at the source's size is one rectangle copy; otherwise each +// destination column's source column is worked out once and a row is +// built and copied whole, the canvas buffers read directly. static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, BasValueT *args, int32_t argc) { uint8_t *pixels = NULL; WidgetT *src = NULL; const BasGfxT *srcScale = NULL; + bool owned = false; int32_t srcW = 0; int32_t srcH = 0; int32_t pitch = 0; - int32_t bpp = rt->ctx->display.format.bytesPerPixel; if (argc < DRAW_PAINT_MIN_ARGS) { basFormRtRuntimeError(rt, "PaintPicture needs a picture, x, and y", "Usage: PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, srcHeight [, opcode]]]"); return; } - if (!drawPictureOf(rt, ctrl->form, args[0], &src, &pixels, &srcW, &srcH, &pitch, &srcScale)) { + if (!drawPictureOf(rt, ctrl->form, args[0], &src, &pixels, &srcW, &srcH, &pitch, &srcScale, &owned)) { return; } // The destination in this object's scale, the source rectangle in the // source PictureBox's (an image file's in pixels) - int32_t sx = 0; - int32_t sy = 0; - int32_t sw = srcW; - int32_t sh = srcH; - int32_t dx = 0; - int32_t dy = 0; - int32_t dw = 0; - int32_t dh = 0; + int32_t sx = 0; + int32_t sy = 0; + int32_t sw = srcW; + int32_t sh = srcH; + int32_t dx = 0; + int32_t dy = 0; + int32_t dw = 0; + int32_t dh = 0; if (argc >= DRAW_PAINT_SRC_ARGS) { basFormRtDrawToPixels(srcScale, basValToNumber(args[5]), basValToNumber(args[6]), basValToNumber(args[7]), basValToNumber(args[8]), &sx, &sy, &sw, &sh); @@ -1162,62 +1189,195 @@ static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, Bas // DVX's copies: greyed, or colour-keyed (at the source's size) if (opcode == BAS_ROP_GRAYCOPY || opcode == BAS_ROP_TRANSPCOPY) { drawPaintKeyed(rt, d, src, pixels, pitch, opcode, args, argc, sx, sy, sw, sh, dx, dy); + } else { + drawPaintRows(rt, d, src, pixels, pitch, srcW, srcH, opcode, sx, sy, sw, sh, dx, dy, dw, dh); + } + + if (owned) { dvxFreeImage(pixels); + } +} + + +// drawPaintPicture's raster-op copy onto the canvas, its source the image +// pixels or else the PictureBox src's canvas. +static void drawPaintRows(BasFormRtT *rt, BasDrawT *d, WidgetT *src, const uint8_t *pixels, int32_t pitch, int32_t srcW, int32_t srcH, uint32_t opcode, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh) { + const BlitOpsT *ops = &rt->ctx->blitOps; + int32_t bpp = ops->bytesPerPixel; + DisplayT dst; + DisplayT srcDisp; + + if (!pixels) { + if (!src || !wgtCanvasGetDisplay(src, &srcDisp)) { + return; + } + + pixels = srcDisp.backBuf; + pitch = srcDisp.pitch; + } + + if (sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0 || !wgtCanvasGetDisplay(d->canvas, &dst)) { + return; + } + + uint32_t rop = (opcode >> BAS_ROP_TABLE_SHIFT) & BAS_ROP_TABLE_MASK; + bool copy = rop == (BAS_ROP_SRCCOPY >> BAS_ROP_TABLE_SHIFT); + + // A plain copy at the source's size: one rectangle copy (rectCopy clips + // to the canvas), the source rectangle kept inside the source + if (copy && dw == sw && dh == sh) { + int32_t cutX = DVX_MAX(0, -sx); + int32_t cutY = DVX_MAX(0, -sy); + int32_t w = DVX_MIN(sw - cutX, srcW - (sx + cutX)); + int32_t h = DVX_MIN(sh - cutY, srcH - (sy + cutY)); + + if (w > 0 && h > 0) { + rectCopy(&dst, ops, dx + cutX, dy + cutY, pixels, pitch, sx + cutX, sy + cutY, w, h); + wgtInvalidatePaint(d->canvas); + } + return; } - uint32_t rop = (opcode >> BAS_ROP_TABLE_SHIFT) & BAS_ROP_TABLE_MASK; - bool copy = rop == (BAS_ROP_SRCCOPY >> BAS_ROP_TABLE_SHIFT); uint32_t pattern = surfToPixel(d, d->gfx.fillColor); uint32_t mask = (bpp >= (int32_t)sizeof(uint32_t)) ? UINT32_MAX : ((1u << (bpp * DRAW_BITS_PER_BYTE)) - 1); + int32_t *cols = (int32_t *)malloc((size_t)dw * sizeof(int32_t)); + uint8_t *row = (uint8_t *)malloc((size_t)dw * (size_t)bpp); - for (int32_t y = 0; y < dh && sw > 0 && sh > 0; y++) { + if (!cols || !row) { + free(cols); + free(row); + return; + } + + for (int32_t x = 0; x < dw; x++) { + int32_t rx = sx + (int32_t)((int64_t)x * sw / dw); + + cols[x] = (rx >= 0 && rx < srcW) ? rx : DRAW_NO_COLUMN; + } + + for (int32_t y = 0; y < dh; y++) { int32_t py = dy + y; int32_t ry = sy + (int32_t)((int64_t)y * sh / dh); - if (py < 0 || py >= d->gfx.surf.height || ry < 0 || ry >= srcH) { + if (py < 0 || py >= dst.height || ry < 0 || ry >= srcH) { continue; } + const uint8_t *srcRow = pixels + (size_t)ry * (size_t)pitch; + const uint8_t *dstRow = dst.backBuf + (size_t)py * (size_t)dst.pitch; + for (int32_t x = 0; x < dw; x++) { - int32_t px = dx + x; - int32_t rx = sx + (int32_t)((int64_t)x * sw / dw); + int32_t px = dx + x; + bool inside = px >= 0 && px < dst.width; + uint32_t under = (inside && (!copy || cols[x] == DRAW_NO_COLUMN)) ? readPixel(dstRow + px * bpp, bpp) : 0; + uint32_t pixel = under; - if (px < 0 || px >= d->gfx.surf.width || rx < 0 || rx >= srcW) { - continue; + if (cols[x] != DRAW_NO_COLUMN) { + pixel = readPixel(srcRow + cols[x] * bpp, bpp); + pixel = copy ? pixel : drawRasterOp(rop, pattern, pixel, under) & mask; } - uint32_t pixel = pixels ? readPixel(pixels + ry * pitch + rx * bpp, bpp) : wgtCanvasGetPixel(src, rx, ry); - - if (!copy) { - pixel = drawRasterOp(rop, pattern, pixel, wgtCanvasGetPixel(d->canvas, px, py)) & mask; - } - - surfSpan(d, px, px, py, pixel); + memcpy(row + x * bpp, &pixel, (size_t)bpp); } + + rectCopy(&dst, ops, dx, py, row, dw * bpp, 0, 0, dw, 1); } - dvxFreeImage(pixels); + free(cols); + free(row); + wgtInvalidatePaint(d->canvas); +} + + +// The pixels of a picture file (path) or of a LoadPictureData picture +// (pic) in the display format, as dvxLoadImage gives them: those kept +// from an earlier call while the file is unchanged (its size and time, +// and not changed in the last DRAW_PICS_TIME_SEC; a picture's bytes never +// change) and the display's pixel size the same, else decoded and, while +// there is room (DRAW_PICS_KEEP_BYTES for the runtime), kept: a picture +// drawn every frame is decoded once. *owned: the caller frees them (not +// kept). +static uint8_t *drawPictureLoad(BasFormRtT *rt, const char *path, const BasPictureT *pic, int32_t *w, int32_t *h, int32_t *pitch, bool *owned) { + struct stat st; + int32_t bpp = rt->ctx->display.format.bytesPerPixel; + bool stable = true; + + memset(&st, 0, sizeof(st)); + + if (!pic) { + stable = stat(path, &st) == 0 && (int64_t)time(NULL) - (int64_t)st.st_mtime > DRAW_PICS_TIME_SEC; + } + + *owned = true; + + for (int32_t i = 0; i < (int32_t)arrlen(rt->drawPics); i++) { + BasDrawPicT *p = &rt->drawPics[i]; + + if (p->picture != pic || (!pic && !platformPathSame(p->path, path))) { + continue; + } + + if (stable && p->size == (int64_t)st.st_size && p->time == (int64_t)st.st_mtime && p->bpp == bpp) { + *w = p->w; + *h = p->h; + *pitch = p->pitch; + *owned = false; + return p->pixels; + } + + rt->drawPicsBytes -= p->pitch * p->h; + dvxFreeImage(p->pixels); + arrdel(rt->drawPics, i); + break; + } + + uint8_t *pixels = pic ? dvxLoadImageFromMemory(rt->ctx, pic->data, pic->dataLen, w, h, pitch) : dvxLoadImage(rt->ctx, path, w, h, pitch); + int32_t bytes = pixels ? *pitch * *h : 0; + + if (!pixels || !stable || rt->drawPicsBytes + bytes > DRAW_PICS_KEEP_BYTES) { + return pixels; + } + + BasDrawPicT kept; + + memset(&kept, 0, sizeof(kept)); + kept.pixels = pixels; + kept.picture = pic; + kept.size = (int64_t)st.st_size; + kept.time = (int64_t)st.st_mtime; + kept.w = *w; + kept.h = *h; + kept.pitch = *pitch; + kept.bpp = bpp; + snprintf(kept.path, sizeof(kept.path), "%s", pic ? "" : path); + arrput(rt->drawPics, kept); + rt->drawPicsBytes += bytes; + *owned = false; + return pixels; } // PaintPicture's picture: a PictureBox (a Control variable, or its name, // looked up from form first), else an image file (named, or a LOADPICTURE -// Picture object) loaded into *pixels in the display format (free with -// dvxFreeImage). *scale is the PictureBox's scale (NULL: pixels). False, -// a runtime error raised, when there is none. -static bool drawPictureOf(BasFormRtT *rt, BasFormT *form, BasValueT v, WidgetT **src, uint8_t **pixels, int32_t *w, int32_t *h, int32_t *pitch, const BasGfxT **scale) { - const char *path = basFormRtPicturePath(rt, v); - BasStringT *name = path ? basStringNew(path, (int32_t)strlen(path)) : (v.type == BAS_TYPE_OBJECT ? NULL : basValFormatString(v)); - void *ref = name ? basFormRtFindCtrl(rt, form, name->data) : v.objVal; - BasControlT *pic = basFormRtResolveCtrl(rt, ref); +// Picture object) or a LOADPICTUREDATA Picture object as *pixels in the +// display format (drawPictureLoad; *owned: free them with dvxFreeImage). +// *scale is the PictureBox's scale (NULL: pixels). False, a runtime error +// raised, when there is none. +static bool drawPictureOf(BasFormRtT *rt, BasFormT *form, BasValueT v, WidgetT **src, uint8_t **pixels, int32_t *w, int32_t *h, int32_t *pitch, const BasGfxT **scale, bool *owned) { + const BasPictureT *bytes = basFormRtPictureData(rt, v); + const char *path = basFormRtPicturePath(rt, v); + BasStringT *name = path ? basStringNew(path, (int32_t)strlen(path)) : (v.type == BAS_TYPE_OBJECT ? NULL : basValFormatString(v)); + void *ref = name ? basFormRtFindCtrl(rt, form, name->data) : (bytes ? NULL : v.objVal); + BasControlT *pic = basFormRtResolveCtrl(rt, ref); *pixels = NULL; + *owned = false; *src = pic && drawIsSurface(pic) && (!pic->form || pic != &pic->form->formCtrl) ? pic->widget : NULL; *scale = *src && pic->draw ? &pic->draw->gfx : NULL; - if (!*src && name) { - *pixels = dvxLoadImage(rt->ctx, name->data, w, h, pitch); + if (!*src && (name || bytes)) { + *pixels = drawPictureLoad(rt, name ? name->data : NULL, bytes, w, h, pitch, owned); } if (name) { @@ -1225,7 +1385,7 @@ static bool drawPictureOf(BasFormRtT *rt, BasFormT *form, BasValueT v, WidgetT * } if (!*pixels && (!*src || !canvasSize(*src, w, h))) { - basFormRtRuntimeError(rt, "PaintPicture has no picture", "The picture must be a PictureBox (its name, or a Control variable holding it), a BMP, PNG, JPEG, or GIF image file, a program, widget or library file (its icon), or a picture from LoadPicture."); + basFormRtRuntimeError(rt, "PaintPicture has no picture", "The picture must be a PictureBox (its name, or a Control variable holding it), a BMP, PNG, JPEG, or GIF image file, a program, widget or library file (its icon), or a picture from LoadPicture or LoadPictureData."); return false; } diff --git a/src/apps/kpunch/dvxbasic/formrt/formPrinter.c b/src/apps/kpunch/dvxbasic/formrt/formPrinter.c index 0248db9b..849201ac 100644 --- a/src/apps/kpunch/dvxbasic/formrt/formPrinter.c +++ b/src/apps/kpunch/dvxbasic/formrt/formPrinter.c @@ -300,6 +300,7 @@ static bool sApiTried; void basFormRtPrinterEnd(BasFormRtT *rt); BasValueT basFormRtPrinterGetProp(BasFormRtT *rt, const char *name); bool basFormRtPrinterIs(const void *ref); +void basFormRtPrinterKill(BasFormRtT *rt); BasValueT basFormRtPrinterMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc); void *basFormRtPrinterRef(void); void basFormRtPrinterSetProp(BasFormRtT *rt, const char *name, BasValueT value); @@ -309,6 +310,7 @@ static void prEndDoc(BasFormRtT *rt, BasPrinterT *p); static int32_t prFind(const char *const *names, int32_t count, const char *name); static int32_t prFont(BasPrinterT *p); static void prFontsClose(BasPrinterT *p); +static void prFree(BasFormRtT *rt); static BasPrinterT *prGet(BasFormRtT *rt); static void prHome(BasPrinterT *p); static void prKillDoc(BasPrinterT *p); @@ -323,7 +325,7 @@ static void prPicture(BasFormRtT *rt, BasPrinterT *p, BasValueT *args, in static void prPictureBand(const PrOpT *op, const PrBandT *b); static bool prPrintPage(BasFormRtT *rt, BasPrinterT *p); static void prShape(BasFormRtT *rt, BasPrinterT *p, PrOpKindE kind, BasValueT *args, int32_t argc); -static void prShapeAgain(const BasPrinterT *p, const PrOpT *op, const BasGfxSurfaceT *surf); +static void prShapeAgain(const BasPrinterT *p, const PrOpT *op, const BasGfxSurfaceT *surf, int32_t bandY0, int32_t bandY1); static int32_t prShapeDo(BasGfxT *g, PrOpKindE kind, const BasValueT *args); static int32_t prText(void *ctx, const char *text, int32_t len, int32_t x, int32_t y); static double prTwipsX(const BasPrinterT *p); @@ -347,9 +349,7 @@ void basFormRtPrinterEnd(BasFormRtT *rt) { } prEndDoc(rt, p); - basFormRtDrawDelete(p->draw); - free(p); - rt->printer = NULL; + prFree(rt); } @@ -426,6 +426,18 @@ bool basFormRtPrinterIs(const void *ref) { } +void basFormRtPrinterKill(BasFormRtT *rt) { + BasPrinterT *p = rt->printer; + + if (!p) { + return; + } + + prKillDoc(p); + prFree(rt); +} + + BasValueT basFormRtPrinterMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc) { BasValueT result = basValInteger(0); BasPrinterT *p = prGet(rt); @@ -742,6 +754,14 @@ static void prFontsClose(BasPrinterT *p) { // The runtime's Printer, made on first use; NULL (a runtime error raised) // when printing is not installed. +// The program's Printer state gone (its document printed or abandoned). +static void prFree(BasFormRtT *rt) { + basFormRtDrawDelete(rt->printer->draw); + free(rt->printer); + rt->printer = NULL; +} + + static BasPrinterT *prGet(BasFormRtT *rt) { const PrApiT *api = prApi(); @@ -916,7 +936,7 @@ static bool prPageBand(void *ctx, uint8_t *rgb, int32_t width, int32_t y0, int32 } else if (op->kind == PrOpPictureE) { prPictureBand(op, &b); } else { - prShapeAgain(p, op, &surf); + prShapeAgain(p, op, &surf, y0, last); } } @@ -1021,14 +1041,11 @@ static void prPictureBand(const PrOpT *op, const PrBandT *b) { int32_t x1 = op->u.picture.dx + op->u.picture.dw < b->width ? op->u.picture.dx + op->u.picture.dw : b->width; for (int32_t y = top; y <= bottom; y++) { - int32_t ry = op->u.picture.sy + (int32_t)((int64_t)(y - op->u.picture.dy) * op->u.picture.sh / op->u.picture.dh); - uint8_t *dst = b->rgb + (int64_t)(y - b->y0) * b->width * PR_RGB_BYTES; + int32_t ry = op->u.picture.sy + (int32_t)((int64_t)(y - op->u.picture.dy) * op->u.picture.sh / op->u.picture.dh); + uint8_t *dst = b->rgb + ((int64_t)(y - b->y0) * b->width + x0) * PR_RGB_BYTES; + const uint8_t *src = op->u.picture.rgb + ((int64_t)ry * op->u.picture.w + op->u.picture.sx) * PR_RGB_BYTES; - for (int32_t x = x0; x < x1; x++) { - int32_t rx = op->u.picture.sx + (int32_t)((int64_t)(x - op->u.picture.dx) * op->u.picture.sw / op->u.picture.dw); - - memcpy(dst + (int64_t)x * PR_RGB_BYTES, op->u.picture.rgb + ((int64_t)ry * op->u.picture.w + rx) * PR_RGB_BYTES, PR_RGB_BYTES); - } + drawScaleRow(dst, src, PR_RGB_BYTES, x0 - op->u.picture.dx, x1 - x0, op->u.picture.dw, op->u.picture.sw, false, 0); } } @@ -1114,14 +1131,18 @@ static void prShape(BasFormRtT *rt, BasPrinterT *p, PrOpKindE kind, BasValueT *a } -// A kept shape done again on surf, in the drawing state it was done in. -static void prShapeAgain(const BasPrinterT *p, const PrOpT *op, const BasGfxSurfaceT *surf) { +// A kept shape done again on surf, in the drawing state it was done in, +// only its rows bandY0..bandY1 (a band's: the fills and pen dots of the +// rest are not worked out at all). +static void prShapeAgain(const BasPrinterT *p, const PrOpT *op, const BasGfxSurfaceT *surf, int32_t bandY0, int32_t bandY1) { BasGfxT g = p->states[op->u.shape.state]; BasValueT args[BAS_GFX_CIRCLE_ARGS]; - g.surf = *surf; - g.lastX = op->u.shape.lastX; - g.lastY = op->u.shape.lastY; + g.surf = *surf; + g.lastX = op->u.shape.lastX; + g.lastY = op->u.shape.lastY; + g.bandY0 = bandY0; + g.bandY1 = bandY1; for (int32_t a = 0; a < op->u.shape.argc; a++) { args[a] = basValDouble(op->u.shape.args[a]); @@ -1238,7 +1259,7 @@ static bool prVecPage(BasPrinterT *p) { } else if (op->kind == PrOpPictureE) { api->vecImage(p->doc, op->u.picture.rgb, op->u.picture.w, op->u.picture.h, op->u.picture.sx, op->u.picture.sy, op->u.picture.sw, op->u.picture.sh, op->u.picture.dx, op->u.picture.dy, op->u.picture.dw, op->u.picture.dh); } else { - prShapeAgain(p, op, &surf); + prShapeAgain(p, op, &surf, 0, surf.height - 1); } } diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.c b/src/apps/kpunch/dvxbasic/formrt/formrt.c index 409d3546..e35cf0f1 100644 --- a/src/apps/kpunch/dvxbasic/formrt/formrt.c +++ b/src/apps/kpunch/dvxbasic/formrt/formrt.c @@ -34,7 +34,8 @@ #include "../runtime/gfx.h" #include "../runtime/serialize.h" #include "../../../../libs/kpunch/serial/rs232/rs232.h" -#include "../../../../libs/kpunch/serial/seclink/secLink.h" +#include "../../../../libs/kpunch/seclink/secLink.h" +#include "../../../../libs/kpunch/dvxnet/dvxNet.h" #include "../../../../libs/kpunch/game/game.h" #include "../../../../libs/kpunch/joystick/joystick.h" #include "../../../../libs/kpunch/music/music.h" @@ -46,6 +47,7 @@ #include "dvxWgtP.h" #include "dvxWm.h" #include "shellApp.h" +#include "shellRun.h" // Widget headers below publish inline accessor macros over // dvx*Api() -> wgtGetApi(...) lookups, so they're lightweight // compile-time type-safety over a runtime-dynamic dispatch -- no @@ -58,7 +60,7 @@ #include "dataCtrl/dataCtrl.h" #include "dbGrid/dbGrid.h" #include "frmParser.h" -#include "../../../../tools/hlpcCompile.h" +#include "../../../../tools/hlpcModule.h" #include "thirdparty/stb_ds_wrap.h" #include @@ -97,6 +99,8 @@ DVX_ABI_STAMP(basrt); #define BAS_ERR_SUMMARY "Runtime error" // when the error names none #define BAS_ERR_UNKNOWN "Unknown error" #define BAS_SQL_MISSING "Databases are not installed: add the Databases part of DVX with Setup (Upgrade or repair)." // SQLError$ without SQLite +#define BAS_SQL_ESC_GROWTH 2 // SQLEscape$: a quote becomes two +#define FORM_NO_MAX 0 // MaxWidth / MaxHeight: no limit but the screen // dlsym name buffer: DVX_SYM_PREFIX + the longest BASIC identifier. #define BAS_MANGLED_NAME_LEN (BAS_MAX_IDENT + 2) @@ -134,6 +138,8 @@ DVX_ABI_STAMP(basrt); #define BAS_FORM_TYPE_NAME "Form" // TypeName / TypeOf of a form #define BAS_WINSTATE_NONE (-1) // BasFormT.frmWindowState: no WindowState waits for Show #define BAS_PICTURE_TYPE_NAME "Picture" // TypeName of a LoadPicture object +#define BAS_PICTURE_NAME_MAX 16 // a LoadPictureData picture's name: the mark, its place, and a NUL +#define BAS_PICTURE_NAME_RADIX 10 // its place is decimal #define BAS_PIC_PROP_HEIGHT "Height" // Picture object properties (read-only) #define BAS_PIC_PROP_TYPE "Type" #define BAS_PIC_PROP_WIDTH "Width" @@ -153,19 +159,57 @@ DVX_ABI_STAMP(basrt); #define BAS_DRAG_OVER_ARGS 4 #define BAS_DRAG_STATE_NONE (-1) -// ResGetText result cap (static return buffer). -#define BAS_RES_TEXT_LEN 1024 - -// HelpCompile's messages (HelpCompileMessages$) +// HelpCompile's messages (HelpCompileMessages$), and how often a compile +// lets the program's events and DVX go on (helpCompileTick) #define HELP_MESSAGE_SEP '\n' #define HELP_COMPILER_MISSING "The help compiler is not available, or no input or output file was given." +#define HELP_PUMP_MS 50 -// Module-level form runtime pointer for onFormClose callback -static BasFormRtT *sFormRt = NULL; +// The runtime whose code runs, one per task (curRt): a program's code +// that yields (DoEvents, a wait, a modal dialog) finds its own runtime +// current when it resumes, whatever another task's program set meanwhile. +typedef struct { + uint32_t task; + BasFormRtT *rt; +} TaskRtT; + +static TaskRtT *sTaskRts = NULL; // stb_ds // How many programs' code calls are on the stack (handlers, sliced // module code): a runtime created inside one does not become current. static int32_t sCodeDepth = 0; +// The task switcher's tsCurrentId and tsYield, found at run time +// (basFormRtTaskId, basFormRtYield). +static uint32_t (*sTaskIdFn)(void) = NULL; +static bool sTaskIdResolved = false; +static void (*sYieldFn)(void) = NULL; +static bool sYieldResolved = false; + +// Events waiting in every runtime's queue (basFormRtBusy), and every +// runtime (stb_ds, oldest first): a pump runs the waiting events of all +// those of its task (a program's own, and one it runs: basdev's child). +static int32_t sQueuedTotal = 0; +static BasFormRtT **sRuntimes = NULL; + +// Events of these kinds report a state, so a newer one waiting for the +// program's turn replaces an older one of the same control instead of +// queueing behind it (a timer that fired twice while the program was +// busy runs once, as in VB). +static const char *const sQueueLatest[] = { + BAS_EVT_MOUSEMOVE, + BAS_EVT_PAINT, + BAS_EVT_RESIZE, + BAS_EVT_TIMER +}; + +// A form's life: these run where they are fired, as part of what fires +// them (an unload must not wait for the program's turn). +static const char *const sQueueNever[] = { + BAS_EVT_LOAD, + BAS_EVT_QUERYUNLOAD, + BAS_EVT_UNLOAD +}; + // True only while frmParse is running inside basFormRtLoadFrm. The // parser calls basFormRtLoadForm via onFormBegin to create the bare form // before it starts firing onCtrlBegin callbacks; while this is set that @@ -189,6 +233,9 @@ static bool sNewFormCopy = false; // default number. static bool sLastInputBoxCancelled = false; +// The BIOS tick of the last pump a long SQL statement made (sqlPump) +static uint32_t sSqlPumpTick = 0; + // The font the last DlgFont call ended on, which DlgFontName$ / // DlgFontSize / DlgFontStyle give: the one chosen, or after Cancel the // one it started at. @@ -259,25 +306,25 @@ static bool sDragCleanupHooked = false; // ============================================================ typedef struct { - int (*open)(int, int32_t, int, char, int, int); - int (*close)(int); - int (*read)(int, char *, int); - int (*write)(int, const char *, int); - int (*writeBuf)(int, const char *, int); - int (*getRxBuffered)(int); - int (*clearRxBuffer)(int); - int (*getUartType)(int); - int (*getBase)(int); - int (*getIrq)(int); - int (*setBase)(int, int); - int (*setIrq)(int, int); - int (*getCts)(int); - int (*getDcd)(int); - int (*getDsr)(int); - int (*getDtr)(int); - int (*getRts)(int); - int (*setDtr)(int, bool); - int (*setRts)(int, bool); + __typeof__(rs232Open) *open; + __typeof__(rs232Close) *close; + __typeof__(rs232Read) *read; + __typeof__(rs232Write) *write; + __typeof__(rs232WriteBuf) *writeBuf; + __typeof__(rs232GetRxBuffered) *getRxBuffered; + __typeof__(rs232ClearRxBuffer) *clearRxBuffer; + __typeof__(rs232GetUartType) *getUartType; + __typeof__(rs232GetBase) *getBase; + __typeof__(rs232GetIrq) *getIrq; + __typeof__(rs232SetBase) *setBase; + __typeof__(rs232SetIrq) *setIrq; + __typeof__(rs232GetCts) *getCts; + __typeof__(rs232GetDcd) *getDcd; + __typeof__(rs232GetDsr) *getDsr; + __typeof__(rs232GetDtr) *getDtr; + __typeof__(rs232GetRts) *getRts; + __typeof__(rs232SetDtr) *setDtr; + __typeof__(rs232SetRts) *setRts; __typeof__(rs232ClearTxBuffer) *clearTxBuffer; __typeof__(rs232GetBps) *getBps; __typeof__(rs232GetData) *getData; @@ -291,6 +338,11 @@ typedef struct { __typeof__(rs232SetBreak) *setBreak; } SerApiT; +// A modem line's reader and setter (rs232GetCts / rs232SetDtr and their +// kin: serLineGet, serLineSet) +typedef __typeof__(rs232GetCts) SerLineGetFnT; +typedef __typeof__(rs232SetDtr) SerLineSetFnT; + static SerApiT sSerApi; static bool sSerApiResolved = false; @@ -301,11 +353,12 @@ static bool sSerApiResolved = false; #define SER_LINE_ERRORS (RS232_LSR_OVERRUN | RS232_LSR_PARITY_ERR | RS232_LSR_FRAME_ERR | RS232_LSR_BREAK) typedef struct { - bool opened; // SerOpen succeeded and SerClose has not run - bool attached; - WidgetT *term; - int32_t com; // 0-based COM port index - int32_t lastError; // RS232_ result of the last SerOpen / SerSet + bool opened; // SerOpen succeeded and SerClose has not run + bool attached; + WidgetT *term; + int32_t com; // 0-based COM port index + int32_t lastError; // RS232_ result of the last SerOpen / SerSet + BasFormRtT *owner; // the program that opened or attached it (basFormRtSerialShutdown) } SerAttachT; static SerAttachT sSerAttach[RS232_NUM_PORTS]; @@ -322,18 +375,20 @@ static TermBindT *sTermBinds = NULL; // stb_ds // SecLink function pointers (resolved lazily) typedef struct { - void *(*open)(int, int32_t, int, char, int, int, void (*)(void *, const uint8_t *, int, uint8_t, bool), void *); - void (*close)(void *); - int (*handshake)(void *); - bool (*isReady)(void *); - int (*poll)(void *); - int (*send)(void *, const uint8_t *, int, uint8_t, bool, bool); - int (*sendBuf)(void *, const uint8_t *, int, uint8_t, bool); - int (*getPending)(void *); - int (*setPassphrase)(void *, const char *); - int (*setChannelPlain)(void *, uint8_t, bool); - int (*handshakeStep)(void *); - int32_t (*getDropped)(void *); + __typeof__(secLinkOpen) *open; + __typeof__(secLinkClose) *close; + __typeof__(secLinkHandshakeStart) *handshakeStart; + __typeof__(secLinkIsReady) *isReady; + __typeof__(secLinkPoll) *poll; + __typeof__(secLinkSend) *send; + __typeof__(secLinkGetPending) *getPending; + __typeof__(secLinkSetPassphrase) *setPassphrase; + __typeof__(secLinkSetChannelPlain) *setChannelPlain; + __typeof__(secLinkHandshakeStep) *handshakeStep; + __typeof__(secLinkGetDropped) *getDropped; + __typeof__(secLinkOpenStream) *openStream; + __typeof__(netBasStreamRead) *tcpRead; // CommOpenTcp (NULL without networking) + __typeof__(netBasStreamWrite) *tcpWrite; } CommSecLinkApiT; static CommSecLinkApiT sCommApi; @@ -348,9 +403,9 @@ typedef struct { static PrintApiT sPrintApi; -// The music, joystick and game libraries' functions behind BEEP, SOUND, -// PLAY, STICK, STRIG and FRAMEWAIT (deviceBind; a group stays NULL when -// its library is not loaded) +// The music, joystick, game, and shell libraries' functions behind BEEP, +// SOUND, PLAY, STICK, STRIG, FRAMEWAIT, and SHELL (deviceBind; a group +// stays NULL when its library is not loaded) typedef struct { __typeof__(musicBasBeep) *beep; __typeof__(musicBasBusy) *busy; @@ -361,6 +416,9 @@ typedef struct { __typeof__(joyBasStrig) *strig; __typeof__(gameFrameInit) *frameInit; __typeof__(gameFrameNext) *frameNext; + __typeof__(shellRunDosFor) *dosFor; + __typeof__(shellRunDosDone) *dosDone; + __typeof__(shellSplitCommand) *dosSplit; } DeviceApiT; static DeviceApiT sDeviceApi; @@ -392,7 +450,10 @@ typedef struct { int32_t termChannel; // channel for terminal I/O bool termEncrypt; // encrypt terminal traffic bool stepping; // CommHandshakeStep exchange running + bool waiting; // CommHandshake or CommSend running (the program's events with it) int32_t lastError; // SECLINK_ result of the last call + int32_t tcp; // CommOpenTcp's net.bas handle (0 on a COM port) + BasFormRtT *owner; // the program that opened it (basFormRtSerialShutdown) } CommSlotT; // Dynamic array of heap-allocated comm slots (stb_ds), allocated on @@ -401,14 +462,8 @@ typedef struct { // keeping handles stable. static CommSlotT **sCommSlots = NULL; -// hlpcCompile function pointer (resolved lazily); typed from the -// hlpcCompile.h prototype so the signature has one source of truth. -typedef __typeof__(hlpcCompile) *HlpcCompileFnT; -typedef __typeof__(hlpcSetMessageFn) *HlpcSetMessageFnT; - -static HlpcCompileFnT sHlpcCompile = NULL; -static HlpcSetMessageFnT sHlpcSetMessage = NULL; -static bool sHlpcResolved = false; +static bool sHelpCompiling = false; // a HelpCompile runs (the compiler does one at a time) +static PlatformTicksT sHelpPumpAt = 0; // when helpCompileTick last let DVX go on static char *sHelpMessages = NULL; // stb_ds: the last HelpCompile's messages, one line each // shellOpenHelp function pointer (resolved lazily); typed from the @@ -430,13 +485,6 @@ static ShellIdleFnT sShellUnregisterIdle = NULL; static bool sShellIdleResolved = false; -// Maximum number of simultaneously open resource handles -#define RES_MAX_HANDLES 8 - -static DvxResHandleT *sResHandles[RES_MAX_HANDLES]; - -// The last ResGetData result, valid until the next call -static uint8_t *sResData = NULL; // ============================================================ @@ -528,6 +576,10 @@ typedef enum { FORM_PROP_FONTUNDERLINE, FORM_PROP_FONTSTRIKETHRU, FORM_PROP_ACTIVECONTROL, + FORM_PROP_MAXWIDTH, + FORM_PROP_MAXHEIGHT, + FORM_PROP_GAMEINPUT, + FORM_PROP_FULLSCREEN, FORM_PROP_COUNT } BasFormPropE; @@ -561,6 +613,18 @@ static const BasPropDescT sFormProps[FORM_PROP_COUNT] = { [FORM_PROP_FONTUNDERLINE] = { BAS_PROP_FONTUNDERLINE, WGT_IFACE_BOOL, true, true }, [FORM_PROP_FONTSTRIKETHRU] = { BAS_PROP_FONTSTRIKETHRU, WGT_IFACE_BOOL, true, true }, [FORM_PROP_ACTIVECONTROL] = { BAS_PROP_ACTIVECONTROL, BAS_PROP_OBJECT, true, false }, + [FORM_PROP_MAXWIDTH] = { BAS_PROP_MAXWIDTH, WGT_IFACE_INT, true, true }, + [FORM_PROP_MAXHEIGHT] = { BAS_PROP_MAXHEIGHT, WGT_IFACE_INT, true, true }, + [FORM_PROP_GAMEINPUT] = { BAS_PROP_GAMEINPUT, WGT_IFACE_BOOL, true, true }, + [FORM_PROP_FULLSCREEN] = { BAS_PROP_FULLSCREEN, WGT_IFACE_BOOL, true, true }, +}; + +// The form properties that are run-time state (basFormRtFormPropRuntimeOnly) +static const char *const sRuntimeOnlyFormProps[] = { + BAS_PROP_VISIBLE, + BAS_PROP_DESIGNMODE, + BAS_PROP_DESIGNSELECTION, + BAS_PROP_FULLSCREEN }; // Common control property ids (every non-menu control, consulted after @@ -782,11 +846,14 @@ const char *basFontPropAt(int32_t idx); bool basFontPropIs(const char *name); bool basFontPropSetText(DvxFontChoiceT *choice, const char *name, const char *text); bool basFontPropText(const DvxFontChoiceT *choice, const char *name, char *out, int32_t outSize); +void basFormRtAdopt(BasFormRtT *rt); void basFormRtBindVm(BasFormRtT *rt); +bool basFormRtBusy(void); BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc); BasFormRtT *basFormRtCodeEnter(BasFormRtT *rt); void basFormRtCodeFailed(BasFormRtT *rt); void basFormRtCodeLeave(BasFormRtT *rt, BasFormRtT *prev); +bool basFormRtCodeOnStack(const BasFormRtT *rt); int32_t basFormRtCommonMethodId(const char *name); const char *basFormRtCommonMethodName(int32_t id); const BasPropDescT *basFormRtCommonProps(int32_t *count); @@ -810,6 +877,7 @@ bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName static bool basFormRtFireEventWithCancel(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName); const char *basFormRtFormMethodAt(int32_t idx); int32_t basFormRtFormMethodCount(void); +bool basFormRtFormPropRuntimeOnly(const char *name); const BasPropDescT *basFormRtFormProps(int32_t *count); BasValueT basFormRtFromWgt(const WgtValueT *v); BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName); @@ -823,22 +891,25 @@ void *basFormRtLoadForm(void *ctx, const char *formName); void *basFormRtLoadFormText(void *ctx, const char *text); BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen); void *basFormRtLoadPicture(void *ctx, const char *path, int32_t *outErr); +void *basFormRtLoadPictureData(void *ctx, const char *data, int32_t len, int32_t *outErr); bool basFormRtMenuLabel(const char *caption, const char *shortcut, const char *shortcutText, char *out, int32_t outSize, int32_t *outKey, int32_t *outMods); const BasPropDescT *basFormRtMenuProps(int32_t *count); int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title); void basFormRtNewArgs(BasFormRtT *rt, const char *args); void *basFormRtNewForm(void *ctx, const char *formName); +BasPictureT *basFormRtPictureData(const BasFormRtT *rt, BasValueT v); const char *basFormRtPicturePath(const BasFormRtT *rt, BasValueT v); void basFormRtRegisterFrm(BasFormRtT *rt, const char *formName, const char *source, int32_t sourceLen); void basFormRtRemoveCtrl(void *ctx, void *formRef, const char *ctrlName); BasControlT *basFormRtResolveCtrl(BasFormRtT *rt, void *ref); void basFormRtRunSimple(BasFormRtT *rt); void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *detailFmt, ...); -void basFormRtSerialShutdown(void); +void basFormRtSerialShutdown(const BasFormRtT *rt); BasFormRtT *basFormRtSetCurrent(BasFormRtT *rt); void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const char *handlerName); void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value); void basFormRtShowForm(void *ctx, void *formRef, bool modal); +uint32_t basFormRtTaskId(void); static void basFormRtTeardownForm(BasFormRtT *rt, BasFormT *form); WidgetT *basFormRtTermBind(const char *ctrlName, void *ctx, BasTermWriteFnT writeFn, BasTermUnbindFnT onUnbind); void basFormRtTermForget(void *ctx); @@ -847,17 +918,21 @@ void basFormRtTestReset(void); WgtValueT basFormRtToWgt(BasValueT v, uint8_t want, BasStringT **hold); const char *basFormRtTypeName(void *ctx, void *objRef); void basFormRtUnloadForm(void *ctx, void *formRef); +bool basFormRtUpdate(BasFormRtT *rt); bool basFormRtWantsNewArgs(BasFormRtT *rt); +void basFormRtYield(void); static BasValueT basFromWgt(const WgtValueT *v); static BasValueT callCommonMethod(BasFormRtT *rt, BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc); static bool callEventProc(BasFormRtT *rt, BasFormT *form, const BasProcEntryT *proc, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outArgCount); static BasValueT callIfaceMethod(BasFormRtT *rt, BasControlT *ctrl, const WgtMethodDescT *m, BasValueT *args, int32_t argc); static BasValueT clipboardMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc); +static int32_t commAddSlot(CommSlotT *slot); void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt); void CommClose(int32_t handle); void CommDetach(int32_t handle); int32_t CommDropped(int32_t handle); static CommSlotT *commGetSlot(int32_t handle); +static bool commGoOn(BasFormRtT *rt, int32_t handle, PlatformTicksT since, uint32_t limitMs); int32_t CommHandshake(int32_t handle, const char *passphrase); int32_t CommHandshakeStep(int32_t handle, const char *passphrase); static void commIdlePoll(void *ctx); @@ -866,6 +941,7 @@ int32_t CommLastError(int32_t handle); static int32_t commNote(CommSlotT *slot, int32_t rc); static void commOnRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted); int32_t CommOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake); +int32_t CommOpenTcp(int32_t tcp); int32_t CommPending(int32_t handle); int32_t CommPoll(int32_t handle); const uint8_t *CommRecv(int32_t handle, int32_t channel, int32_t *outLen); @@ -873,11 +949,16 @@ int32_t CommRecvEncrypted(int32_t handle, int32_t channel); static bool commResolveApi(void); int32_t CommSend(int32_t handle, const uint8_t *data, int32_t len, int32_t channel, int32_t encrypt); int32_t CommSetPlain(int32_t handle, int32_t channel, int32_t plain); +static int32_t commTcpRead(void *ctx, uint8_t *buf, int32_t len); +static int32_t commTcpWrite(void *ctx, const uint8_t *data, int32_t len); static int32_t commTermRead(void *ctx, uint8_t *buf, int32_t maxLen); static int32_t commTermWrite(void *ctx, const uint8_t *data, int32_t len); static BasValueT contentBoxProp(BasFormRtT *rt, BasFormT *form, const char *propName, const BasValueT *value); static const char *ctrlDisplayName(const BasControlT *ctrl); static BasControlT *ctrlFromWidget(WidgetT *w, BasFormRtT **outRt); +static BasFormRtT *curRt(void); +static void curRtForget(const BasFormRtT *rt); +static BasFormRtT *curRtSet(BasFormRtT *rt); static const char *defaultPropOf(BasFormRtT *rt, void *ref); static void destroyFormNow(BasFormRtT *rt, BasFormT *form); static void detachFormForUnload(BasFormRtT *rt, BasFormT *form); @@ -887,6 +968,9 @@ static bool deviceFrameBusy(void *ctx); static void deviceFrameStart(void *ctx, int32_t hz); static bool devicePlay(void *ctx, const char *macro); static int32_t devicePlayCount(void *ctx); +static bool deviceShellBusy(void *ctx); +static int32_t deviceShellResult(void *ctx); +static bool deviceShellStart(void *ctx, const char *command); static void deviceSound(void *ctx, double hz, double ticks); static bool deviceSoundBusy(void *ctx); static int32_t deviceStick(void *ctx, int32_t n); @@ -922,20 +1006,22 @@ static void dragHookForms(BasFormRtT *rt, bool hook); static void dragOnKey(WindowT *win, int32_t key, int32_t mod); static void dragOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons); static void dragOverUpdate(void); -static bool dragShowIcon(AppContextT *ctx, const char *path); +static bool dragShowIcon(const BasFormRtT *rt, AppContextT *ctx, const char *path); static BasControlT *dragTargetAt(BasFormRtT *rt); static const BasProcEntryT *eventHandler(BasFormRtT *rt, const BasControlT *obj, const char *ctrlName, const char *eventName, bool *outOverridden); +static bool eventProcFits(const BasProcEntryT *proc, int32_t argCount); +static bool eventWanted(BasFormRtT *rt, const BasControlT *obj, const char *ctrlName, const char *eventName, int32_t argCount); static const char *fileDialog(const char *title, const char *filter, int32_t flags, const char *folder); static BasControlT *findButtonWith(BasFormT *form, const char *propName); static BasControlT *findCtrlByName(const char *name); static BasControlT *findCtrlByWidget(BasFormT *form, const WidgetT *w); static BasControlT *findCtrlInForm(BasFormT *form, const char *name); -static WidgetT *findCtrlWidgetByName(const char *name); static BasFormT *findFormByWindow(const BasFormRtT *rt, const WindowT *win); static BasControlT *findMenuProxyInForm(BasFormT *form, const char *name); static BasControlT *findNamedObjectInForm(BasFormT *form, const char *name); static BasFrmPopupMenuT *findPopupMenu(BasFormT *form, const char *name); static const BasPropDescT *findPropDesc(const BasPropDescT *table, int32_t count, const char *name); +static WidgetT *findTermWidget(const char *name); static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount); static void fireCtrlEventOut(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outArgCount); static bool fireEventArgsOut(BasFormRtT *rt, BasFormT *form, BasControlT *obj, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outArgCount); @@ -946,7 +1032,9 @@ static const BasFontPropT *fontPropFind(const char *name); static BasValueT fontPropGet(const WidgetT *w, const char *name); static bool fontPropSet(WidgetT *w, const char *name, BasValueT value); static void formatPropNames(const BasPropDescT *table, int32_t count, bool forWrite, char *buf, int32_t bufSize); +static void formClose(BasFormRtT *rt, BasFormT *form); static BasControlT *formEventObject(BasFormT *form, const char *ctrlName); +static void formFullChanged(WindowT *win, ExclusiveStateE state, void *userData); static bool formMethod(BasFormRtT *rt, BasFormT *frm, const char *name, BasValueT *args, int32_t argc); static BasFormT *formOfWindow(const WindowT *win, BasFormRtT **outRt); static void formRtCommIdleSync(void); @@ -973,6 +1061,7 @@ static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *pr int32_t HelpCompile(const char *inputFile, const char *outputFile); static void helpCompileMessage(void *ctx, const char *line); const char *HelpCompileMessages(void); +static bool helpCompileTick(void *ctx); void HelpView(const char *hlpFile); static void helpViewOpen(const char *hlpFile, const char *topicId); void HelpViewTopic(const char *hlpFile, const char *topicId); @@ -1013,10 +1102,14 @@ static void onWidgetScroll(WidgetT *w, int32_t delta); static bool onWidgetValidate(WidgetT *w); static int32_t parseFileFilters(const char *filter, FileFilterT **outFilters, char *buf, int32_t bufSize); static BasPictureT *pictureAdd(BasFormRtT *rt, const char *path, int32_t width, int32_t height); +static BasPictureT *pictureByDataName(const BasFormRtT *rt, const char *path); +static void pictureDataName(const BasFormRtT *rt, const BasPictureT *pic, char *out, int32_t outSize); static char *pictureFileRead(const char *path, int32_t *outLen); static BasPictureT *pictureFind(const BasFormRtT *rt, const void *ref); static BasPictureT *pictureFor(BasFormRtT *rt, const char *path); static BasValueT pictureProp(BasFormRtT *rt, const BasPictureT *pic, const char *propName); +static void pictureShowData(BasFormRtT *rt, BasControlT *ctrl, const char *propName, const BasPictureT *pic); +static bool pictureTakesData(const BasControlT *ctrl, const char *propName); static bool pointerCursor(int32_t pointer, int32_t *outCursor); static void popupMenuAdd(BasFormT *form, const char *name, MenuT *menu, bool ownsMenu); static const char *popupMenuNameFor(const BasFormT *form, const MenuT *menu); @@ -1027,18 +1120,15 @@ static bool printerWrite(void *ctx, void *job, const char *data, int32_t len); int32_t PrintForm(const char *formName); int32_t PrintFormDialog(const char *formName); static int32_t printFormWindow(const char *formName, bool askFirst); +static void queueArgsFree(BasQueuedEventT *ev); +static void queueDrain(BasFormRtT *rt); +static bool queueEvent(BasFormRtT *rt, BasFormT *form, BasControlT *obj, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount, BasQueuedKindE kind); +static bool queueForeign(const BasFormRtT *rt, const char *eventName); +static void queueFree(BasFormRtT *rt); +static bool queueLive(BasFormRtT *rt, const BasQueuedEventT *ev); static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl); static void removeCtrlTree(BasFormRtT *rt, BasFormT *form, BasControlT *ctrl); -int32_t ResAddData(const char *path, const char *name, int32_t type, const uint8_t *data, int32_t len); -int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile); -int32_t ResAddText(const char *path, const char *name, const char *text); -void ResClose(int32_t handle); -int32_t ResCount(int32_t handle); -int32_t ResExtract(const char *path, const char *name, const char *outFile); -const uint8_t *ResGetData(const char *path, const char *name, int32_t *outLen); -const char *ResGetText(const char *path, const char *name); static void resizeIfaceBuffer(BasControlT *ctrl); -const char *ResName(int32_t handle, int32_t index); static BasControlT *resolveCtrlArg(BasFormRtT *rt, BasFormT *form, BasValueT arg); static BasControlT *resolveCtrlRef(BasFormRtT *rt, void *ref); static BasControlT *resolveCtrlRefIn(BasFormT **forms, void *ref); @@ -1046,15 +1136,11 @@ static BasFormT *resolveFormRef(BasFormRtT *rt, void *ref); static MenuItemT *resolveMenuItem(BasFormT *form, int32_t menuId, bool *fromBar); static BasFormT *resolveOwningForm(BasFormRtT *rt, BasFormT *eventForm, const BasProcEntryT *proc); static bool resolveShellIdle(void); -int32_t ResOpen(const char *path); -static void *resReadNamed(const char *path, const char *name, uint32_t *outSize); -int32_t ResRemove(const char *path, const char *name); -int32_t ResSize(int32_t handle, int32_t index); -int32_t ResType(int32_t handle, int32_t index); static bool rtCallHandler(BasFormRtT *rt, BasFormT *eventForm, const BasProcEntryT *proc, int32_t codeAddr, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outArgCount); static void rtEventEnter(BasFormRtT *rt); static void rtEventLeave(BasFormRtT *rt); static const BasProcEntryT *rtFindProc(BasFormRtT *rt, const char *name); +static bool rtPump(BasFormRtT *rt); static BasVmResultE runSliced(BasFormRtT *rt); static void runtimeErrorEnd(BasFormRtT *rt, const char *summary, const char *details); static BasValueT screenMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc); @@ -1083,8 +1169,8 @@ int32_t SerGetUart(int32_t com); static void serIdlePoll(void *ctx); int32_t SerLastError(int32_t com); int32_t SerLineErrors(int32_t com); -static int32_t serLineGet(int (*const *get)(int), int32_t com); -static void serLineSet(int (*const *set)(int, bool), int32_t com, int32_t on); +static int32_t serLineGet(SerLineGetFnT *const *get, int32_t com); +static void serLineSet(SerLineSetFnT *const *set, int32_t com, int32_t on); static int32_t serNote(int32_t com, int32_t rc); int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake); static char serParity(const char *parity); @@ -1103,7 +1189,9 @@ static int32_t serValueGet(int32_t (*const *get)(int32_t), int32_t com); int32_t SerWrite(int32_t com, const uint8_t *data, int32_t len); static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT value); static void setControlString(char *dst, int32_t dstSize, BasValueT value); +static void setFormFullScreen(BasFormRtT *rt, BasFormT *frm, bool on); static void setFormIcon(BasFormRtT *rt, BasFormT *frm, BasValueT value); +static void setFormMax(BasFormRtT *rt, WindowT *win, bool across, int32_t limit); static void setFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName, BasValueT value); static void setHeapString(char **dst, BasValueT value); static bool setIfaceProp(BasFormRtT *rt, const WgtIfaceT *iface, WidgetT *w, const char *propName, BasValueT value); @@ -1121,6 +1209,7 @@ static const BasSqlCallbacksT *sqlApi(void); void SQLClose(int32_t db); int32_t SQLEof(int32_t rs); const char *SQLError(int32_t db); +const char *SQLEscape(const char *text); int32_t SQLExec(int32_t db, const char *sql); const char *SQLField(int32_t rs, const char *colName); int32_t SQLFieldCount(int32_t rs); @@ -1131,6 +1220,7 @@ const char *SQLFieldText(int32_t rs, int32_t col); void SQLFreeResult(int32_t rs); int32_t SQLNext(int32_t rs); int32_t SQLOpen(const char *path); +static bool sqlPump(int32_t db); int32_t SQLQuery(int32_t db, const char *sql); static void sqlResolveApi(BasSqlCallbacksT *sql); static int32_t termBindIndex(const void *ctx); @@ -1138,6 +1228,7 @@ static void unloadFormNow(BasFormRtT *rt, BasFormT *form); static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl); static void upperCopy(char *dst, int32_t dstSize, const char *src); static void vmErrorEnd(BasFormRtT *rt); +static bool waitPump(void); static WgtValueT wgtFromBas(BasValueT v, uint8_t want, BasStringT **hold); static bool widgetHasAncestor(const WidgetT *w, const WidgetT *ancestor); static bool windowLive(const AppContextT *ctx, const WindowT *win, int32_t winId); @@ -1326,7 +1417,7 @@ BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const cha memset(&result, 0, sizeof(result)); if (oom) { - basFormRtRuntimeError(sFormRt, "Out of memory", "Out of memory marshalling arguments for external function '%s'.", funcName ? funcName : "?"); + basFormRtRuntimeError(curRt(), "Out of memory", "Out of memory marshalling arguments for external function '%s'.", funcName ? funcName : "?"); } else { BasNativeRetE ret = BasNativeRetIntE; @@ -1442,32 +1533,40 @@ bool basFontPropText(const DvxFontChoiceT *choice, const char *name, char *out, } +void basFormRtAdopt(BasFormRtT *rt) { + curRtForget(rt); + rt->ownTask = basFormRtTaskId(); + curRtSet(rt); +} + + void basFormRtBindVm(BasFormRtT *rt) { BasUiCallbacksT ui; memset(&ui, 0, sizeof(ui)); - ui.getProp = basFormRtGetProp; - ui.setProp = basFormRtSetProp; - ui.callMethod = basFormRtCallMethod; - ui.createCtrl = basFormRtCreateCtrl; - ui.findCtrl = basFormRtFindCtrl; - ui.findCtrlIdx = basFormRtFindCtrlIdx; - ui.loadForm = basFormRtLoadForm; - ui.unloadForm = basFormRtUnloadForm; - ui.showForm = basFormRtShowForm; - ui.hideForm = basFormRtHideForm; - ui.msgBox = basFormRtMsgBox; - ui.inputBox = basFormRtInputBox; - ui.createForm = basFormRtCreateForm; - ui.newForm = basFormRtNewForm; - ui.createCtrlEx = basFormRtCreateCtrlEx; - ui.setEvent = basFormRtSetEvent; - ui.removeCtrl = basFormRtRemoveCtrl; - ui.typeName = basFormRtTypeName; - ui.loadPicture = basFormRtLoadPicture; - ui.loadFormText = basFormRtLoadFormText; - ui.defaultProp = basFormRtDefaultProp; - ui.ctx = rt; + ui.getProp = basFormRtGetProp; + ui.setProp = basFormRtSetProp; + ui.callMethod = basFormRtCallMethod; + ui.createCtrl = basFormRtCreateCtrl; + ui.findCtrl = basFormRtFindCtrl; + ui.findCtrlIdx = basFormRtFindCtrlIdx; + ui.loadForm = basFormRtLoadForm; + ui.unloadForm = basFormRtUnloadForm; + ui.showForm = basFormRtShowForm; + ui.hideForm = basFormRtHideForm; + ui.msgBox = basFormRtMsgBox; + ui.inputBox = basFormRtInputBox; + ui.createForm = basFormRtCreateForm; + ui.newForm = basFormRtNewForm; + ui.createCtrlEx = basFormRtCreateCtrlEx; + ui.setEvent = basFormRtSetEvent; + ui.removeCtrl = basFormRtRemoveCtrl; + ui.typeName = basFormRtTypeName; + ui.loadPicture = basFormRtLoadPicture; + ui.loadPictureData = basFormRtLoadPictureData; + ui.loadFormText = basFormRtLoadFormText; + ui.defaultProp = basFormRtDefaultProp; + ui.ctx = rt; basVmSetUiCallbacks(rt->vm, &ui); printerBind(rt); @@ -1477,6 +1576,17 @@ void basFormRtBindVm(BasFormRtT *rt) { } +bool basFormRtBusy(void) { + for (int32_t i = 0; i < (int32_t)arrlen(sRuntimes); i++) { + if (sRuntimes[i]->formsLoading) { + return true; + } + } + + return sCodeDepth > 0 || sQueuedTotal > 0 || basUcBusy(); +} + + BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc) { BasFormRtT *rt = (BasFormRtT *)ctx; size_t len = strlen(methodName); @@ -1573,9 +1683,8 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasFormRtT *basFormRtCodeEnter(BasFormRtT *rt) { - BasFormRtT *prev = sFormRt; + BasFormRtT *prev = curRtSet(rt); - sFormRt = rt; sCodeDepth++; rtEventEnter(rt); return prev; @@ -1591,11 +1700,17 @@ void basFormRtCodeFailed(BasFormRtT *rt) { void basFormRtCodeLeave(BasFormRtT *rt, BasFormRtT *prev) { sCodeDepth--; - sFormRt = prev; + curRtSet(prev); rtEventLeave(rt); } +// eventDepth counts the queue's bracket too (BasFormRtT.queueHeld). +bool basFormRtCodeOnStack(const BasFormRtT *rt) { + return rt->eventDepth > (rt->queueHeld ? 1 : 0); +} + + int32_t basFormRtCommonMethodId(const char *name) { for (int32_t i = 0; i < BAS_CM_COUNT; i++) { if (strcasecmp(sCommonMethodNames[i], name) == 0) { @@ -1625,12 +1740,15 @@ BasFormRtT *basFormRtCreate(AppContextT *ctx, BasVmT *vm, BasModuleT *module) { return NULL; } - rt->ctx = ctx; - rt->vm = vm; - rt->module = module; + rt->ctx = ctx; + rt->vm = vm; + rt->module = module; + rt->ownTask = basFormRtTaskId(); + arrput(sRuntimes, rt); + dvxSetWaitPump(waitPump); if (sCodeDepth == 0) { - sFormRt = rt; + curRtSet(rt); } basFormRtBindVm(rt); @@ -1855,15 +1973,16 @@ WindowT *basFormRtCreateFormWindow(AppContextT *ctx, const char *title, const ch } -// BasUiCallbacksT.defaultProp: a Picture's default value is its file name -// (BAS_PIC_PROP_DEFAULT), a control's its VB default property -// (sDefaultProps) when it has that property; forms, menus and the -// Clipboard have none. +// The runtime the library-wide externs act for now (NULL: none). BasFormRtT *basFormRtCurrent(void) { - return sFormRt; + return curRt(); } +// BasUiCallbacksT.defaultProp: a Picture's default value is its file name +// (BAS_PIC_PROP_DEFAULT), a control's its VB default property +// (sDefaultProps) when it has that property; forms, menus, and the +// Clipboard have none. const char *basFormRtDefaultProp(void *ctx, void *objRef) { BasFormRtT *rt = (BasFormRtT *)ctx; @@ -1894,8 +2013,13 @@ void basFormRtDestroy(BasFormRtT *rt) { return; } - if (sFormRt == rt) { - sFormRt = NULL; + curRtForget(rt); + + for (int32_t i = 0; i < (int32_t)arrlen(sRuntimes); i++) { + if (sRuntimes[i] == rt) { + arrdel(sRuntimes, i); + break; + } } if (sDrag.rt == rt) { @@ -1912,10 +2036,10 @@ void basFormRtDestroy(BasFormRtT *rt) { dvxSetBusy(rt->ctx, false); } - // Close serial/secLink and drop the idle pollers before tearing down the - // forms whose terminal widgets they reference. Also invoked from the - // stub's _appShutdown hook so a force-killed app gets the same cleanup. - basFormRtSerialShutdown(); + // Close this program's serial ports and secure links (and drop the + // idle pollers when nothing is left) before tearing down the forms + // whose terminal widgets they reference. + basFormRtSerialShutdown(rt); for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) { destroyFormNow(rt, rt->forms[i]); @@ -1929,6 +2053,8 @@ void basFormRtDestroy(BasFormRtT *rt) { // reaches basFormRtDestroy; there the shell's per-app window scan // reclaims the windows before dlclose. destroyFormNow (not // unloadFormNow) so nothing re-touches vm fields on a dying runtime. + queueFree(rt); + while (arrlen(rt->pendingCtrlFree) > 0) { BasControlT *ctrl = arrpop(rt->pendingCtrlFree); @@ -1956,10 +2082,12 @@ void basFormRtDestroy(BasFormRtT *rt) { arrfree(rt->frmCache); for (int32_t i = 0; i < (int32_t)arrlen(rt->pictures); i++) { + free(rt->pictures[i]->data); free(rt->pictures[i]); } arrfree(rt->pictures); + basFormRtDrawPicsFree(rt); free(rt->pendingArgs); free(rt); } @@ -1971,7 +2099,7 @@ void basFormRtEventLoop(BasFormRtT *rt) { } while (rt->ctx->running && basFormRtKeepsRunning(rt)) { - if (!dvxUpdate(rt->ctx)) { + if (!basFormRtUpdate(rt)) { break; } @@ -2109,6 +2237,17 @@ int32_t basFormRtFormMethodCount(void) { } +bool basFormRtFormPropRuntimeOnly(const char *name) { + for (int32_t i = 0; i < DVX_ARRAY_LEN(sRuntimeOnlyFormProps); i++) { + if (strcasecmp(name, sRuntimeOnlyFormProps[i]) == 0) { + return true; + } + } + + return false; +} + + const BasPropDescT *basFormRtFormProps(int32_t *count) { *count = FORM_PROP_COUNT; return sFormProps; @@ -2417,9 +2556,13 @@ void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName) { return; } - // Load .frm text cache entries (skip if already loaded) + // Load .frm text cache entries (skip if already loaded), the other + // tasks running between them + rt->formsLoading = true; + for (int32_t i = 0; i < rt->frmCacheCount; i++) { basFormRtLoadForm(rt, rt->frmCache[i].formName); + basFormRtYield(); } // Show the startup form (named or first) @@ -2439,6 +2582,8 @@ void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName) { basFormRtShowForm(rt, startup, false); } + + rt->formsLoading = false; } @@ -2877,7 +3022,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 (platformPathSame(rt->pictures[i]->path, path)) { + if (!rt->pictures[i]->data && platformPathSame(rt->pictures[i]->path, path)) { return rt->pictures[i]; } } @@ -2895,19 +3040,15 @@ void *basFormRtLoadPicture(void *ctx, const char *path, int32_t *outErr) { return NULL; } - int32_t pitch = 0; - bool hasKey = false; - uint32_t key = 0; - uint8_t *pixels = dvxLoadImageAlpha(rt->ctx, (const uint8_t *)data, len, &w, &h, &pitch, &hasKey, &key); + // Its header: the control showing it decodes it + bool picture = dvxImageInfoFromMemory((const uint8_t *)data, len, &w, &h); free(data); - if (!pixels) { + if (!picture) { *outErr = BAS_ERR_INVALID_PICTURE; return NULL; } - - dvxFreeImage(pixels); } BasPictureT *pic = pictureAdd(rt, path, w, h); @@ -2920,6 +3061,49 @@ void *basFormRtLoadPicture(void *ctx, const char *path, int32_t *outErr) { } +// Each picture made from bytes keeps them for the runtime's life, as the +// file pictures keep their names: the same bytes give the same object. +void *basFormRtLoadPictureData(void *ctx, const char *data, int32_t len, int32_t *outErr) { + BasFormRtT *rt = (BasFormRtT *)ctx; + + if (len <= 0) { + return basFormRtLoadPicture(ctx, "", outErr); + } + + for (int32_t i = 0; i < (int32_t)arrlen(rt->pictures); i++) { + BasPictureT *pic = rt->pictures[i]; + + if (pic->data && pic->dataLen == len && memcmp(pic->data, data, (size_t)len) == 0) { + return pic; + } + } + + int32_t w = 0; + int32_t h = 0; + + if (!dvxImageInfoFromMemory((const uint8_t *)data, len, &w, &h)) { + *outErr = BAS_ERR_INVALID_PICTURE; + return NULL; + } + + // The NUL after the bytes lets them go to a widget as one WgtValueT + uint8_t *copy = (uint8_t *)malloc((size_t)len + 1); + BasPictureT *pic = copy ? pictureAdd(rt, "", w, h) : NULL; + + if (!pic) { + free(copy); + *outErr = BAS_ERR_OUT_OF_MEMORY; + return NULL; + } + + memcpy(copy, data, (size_t)len); + copy[len] = '\0'; + pic->data = copy; + pic->dataLen = len; + return pic; +} + + bool basFormRtMenuLabel(const char *caption, const char *shortcut, const char *shortcutText, char *out, int32_t outSize, int32_t *outKey, int32_t *outMods) { char keyText[BAS_MENU_KEY_TEXT_MAX]; bool known = true; @@ -2987,10 +3171,18 @@ void *basFormRtNewForm(void *ctx, const char *formName) { } +BasPictureT *basFormRtPictureData(const BasFormRtT *rt, BasValueT v) { + BasPictureT *pic = (v.type == BAS_TYPE_OBJECT) ? pictureFind(rt, v.objVal) : NULL; + + return (pic && pic->data) ? pic : NULL; +} + + +// NULL for a picture from bytes (LoadPictureData): it has no file const char *basFormRtPicturePath(const BasFormRtT *rt, BasValueT v) { const BasPictureT *pic = (v.type == BAS_TYPE_OBJECT) ? pictureFind(rt, v.objVal) : NULL; - return pic ? pic->path : NULL; + return (pic && !pic->data) ? pic->path : NULL; } @@ -3112,44 +3304,39 @@ void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *deta } -// Release all serial/secLink resources and unregister both idle pollers from -// the shell. Safe to call repeatedly. The pollers live in this shared -// runtime DXE but iterate per-app slots/terminals, so they MUST be dropped -// when a BASIC app goes away -- on normal exit via basFormRtDestroy, and on -// force-kill (where basFormRtDestroy never runs) via the stub's _appShutdown. -// NOTE: closes every open connection/port, which is correct for the single -// foreground BASIC app; concurrent BASIC instances sharing these module-scope -// slots is a separate, pre-existing limitation. -void basFormRtSerialShutdown(void) { +// Close the connections and serial ports the program rt opened (NULL: +// every one) and drop the idle pollers once nothing is left for them. +// Safe to call repeatedly. The slots and ports are shared by every BASIC +// program in this runtime library, so a program's end (basFormRtDestroy, +// which a force kill reaches through basRunHost's kill hook) leaves the +// other programs' links and ports open. +void basFormRtSerialShutdown(const BasFormRtT *rt) { for (int32_t i = 0; i < (int32_t)arrlen(sCommSlots); i++) { - if (sCommSlots[i]) { + if (sCommSlots[i] && (!rt || sCommSlots[i]->owner == rt)) { CommClose(i + 1); } } - arrfree(sCommSlots); - sCommSlots = NULL; + if (!rt) { + arrfree(sCommSlots); + sCommSlots = NULL; + } if (sSerApiResolved && sSerApi.close) { for (int32_t i = 0; i < RS232_NUM_PORTS; i++) { - if (sSerAttach[i].opened || sSerAttach[i].attached) { + if ((sSerAttach[i].opened || sSerAttach[i].attached) && (!rt || sSerAttach[i].owner == rt)) { SerClose(i + 1); } } } - if (sShellUnregisterIdle) { - sShellUnregisterIdle(commIdlePoll, NULL); - sShellUnregisterIdle(serIdlePoll, NULL); - } + formRtCommIdleSync(); + formRtSerIdleSync(); } BasFormRtT *basFormRtSetCurrent(BasFormRtT *rt) { - BasFormRtT *prev = sFormRt; - - sFormRt = rt; - return prev; + return curRtSet(rt); } @@ -3293,7 +3480,7 @@ void basFormRtShowForm(void *ctx, void *formRef, bool modal) { bool running = true; while (running && !form->detached && form->window->visible && !rt->terminated) { - running = dvxUpdate(rt->ctx); + running = basFormRtUpdate(rt); } if (rt->ctx->modalWindow == NULL || rt->ctx->modalWindow == form->window) { @@ -3305,12 +3492,22 @@ void basFormRtShowForm(void *ctx, void *formRef, bool modal) { } +// The task running now (tsCurrentId; 0 without the task switcher). +uint32_t basFormRtTaskId(void) { + if (!sTaskIdResolved) { + sTaskIdResolved = true; + sTaskIdFn = (uint32_t (*)(void))dlsym(NULL, DVX_SYM("tsCurrentId")); + } + + return sTaskIdFn ? sTaskIdFn() : 0; +} + + // Free ALL per-form heap resources: form variables, controls (with // tooltips), menuIdMap proxies + array, and popup menus. Does NOT // destroy the window, does NOT remove the form from rt->forms, and does -// NOT free the BasFormT itself -- callers differ on those. Single -// source of truth for the three formerly-diverged teardown sites -// (basFormRtDestroy, basFormRtUnloadForm, onFormClose). +// NOT free the BasFormT itself: destroyFormNow, its caller, destroys the +// window next and frees the form. static void basFormRtTeardownForm(BasFormRtT *rt, BasFormT *form) { rt->refCacheKey = NULL; basFormRtDesignForget(form); @@ -3335,8 +3532,16 @@ static void basFormRtTeardownForm(BasFormRtT *rt, BasFormT *form) { form->formVarCount = 0; } + // The window goes after the controls: its widgets (a timer's tick, a + // UserControl's Terminate code that gives way) must find none for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { - freeControl(form->controls[i]); + BasControlT *ctrl = form->controls[i]; + + if (ctrl->widget && ctrl->widget->userData == ctrl) { + ctrl->widget->userData = NULL; + } + + freeControl(ctrl); } arrfree(form->controls); @@ -3384,27 +3589,27 @@ static void basFormRtTeardownForm(BasFormRtT *rt, BasFormT *form) { // Bind a library's byte stream to the Terminal control ctrlName (see // formrt.h). NULL when the running program has no Terminal of that name. WidgetT *basFormRtTermBind(const char *ctrlName, void *ctx, BasTermWriteFnT writeFn, BasTermUnbindFnT onUnbind) { - BasControlT *ctrl = findCtrlByName(ctrlName); + WidgetT *term = findTermWidget(ctrlName); - if (!ctrl || !ctrl->widget || strcasecmp(ctrl->typeName, BAS_TERMINAL_TYPE) != 0) { + if (!term) { return NULL; } // One binding per terminal and per stream, as SerAttach / CommAttach basFormRtTermUnbind(ctx); - if (formRtDetachTermsForWidget(ctrl->widget)) { + if (formRtDetachTermsForWidget(term)) { formRtSerIdleSync(); } TermBindT bind; - bind.term = ctrl->widget; + bind.term = term; bind.ctx = ctx; bind.onUnbind = onUnbind; arrput(sTermBinds, bind); - wgtAnsiTermSetComm(ctrl->widget, ctx, NULL, writeFn); - return ctrl->widget; + wgtAnsiTermSetComm(term, ctx, NULL, writeFn); + return term; } @@ -3435,17 +3640,8 @@ void basFormRtTermUnbind(void *ctx) { // Serial/comm state goes through the normal shutdown path first so open // ports and idle pollers are released rather than leaked. void basFormRtTestReset(void) { - basFormRtSerialShutdown(); + basFormRtSerialShutdown(NULL); - for (int32_t i = 0; i < RES_MAX_HANDLES; i++) { - if (sResHandles[i]) { - dvxResClose(sResHandles[i]); - sResHandles[i] = NULL; - } - } - - free(sResData); - sResData = NULL; memset(sSerAttach, 0, sizeof(sSerAttach)); memset(&sSerApi, 0, sizeof(sSerApi)); arrfree(sTermBinds); @@ -3456,16 +3652,21 @@ void basFormRtTestReset(void) { sCommApiResolved = false; arrfree(sHelpMessages); sHelpMessages = NULL; - sHlpcCompile = NULL; - sHlpcSetMessage = NULL; - sHlpcResolved = false; + sHelpCompiling = false; sShellOpenHelp = NULL; sShellOpenHelpResolved = false; sShellRegisterIdle = NULL; sShellUnregisterIdle = NULL; sShellIdleResolved = false; - sFormRt = NULL; + arrfree(sTaskRts); sCodeDepth = 0; + sTaskIdFn = NULL; + sTaskIdResolved = false; + sYieldFn = NULL; + sYieldResolved = false; + sQueuedTotal = 0; + arrfree(sRuntimes); + sRuntimes = NULL; sLoadingFrm = false; sLastInputBoxCancelled = false; memset(&sDrag, 0, sizeof(sDrag)); @@ -3552,6 +3753,22 @@ void basFormRtUnloadForm(void *ctx, void *formRef) { } +// The waiting events of every runtime of this task run (rt's, and those +// of programs it runs), then a frame. +bool basFormRtUpdate(BasFormRtT *rt) { + uint32_t task = basFormRtTaskId(); + + for (int32_t i = 0; i < (int32_t)arrlen(sRuntimes); i++) { + if (sRuntimes[i]->ownTask == task) { + queueDrain(sRuntimes[i]); + } + } + + basUcPump(); + return dvxUpdate(rt->ctx); +} + + bool basFormRtWantsNewArgs(BasFormRtT *rt) { for (int32_t i = 0; rt && i < (int32_t)arrlen(rt->forms); i++) { BasFormT *form = rt->forms[i]; @@ -3566,13 +3783,30 @@ bool basFormRtWantsNewArgs(BasFormRtT *rt) { } +void basFormRtYield(void) { + if (!sYieldResolved) { + sYieldResolved = true; + sYieldFn = (void (*)(void))dlsym(NULL, DVX_SYM("tsYield")); + } + + if (sYieldFn) { + sYieldFn(); + } +} + + // A widget value as a BASIC value: INT / ENUM Long, BOOL Boolean, FLOAT // Single (the BASIC type of WGT_IFACE_FLOAT), DOUBLE Double, STRING a new -// string. +// string of its bytes (NUL bytes included: wgtValueLen). static BasValueT basFromWgt(const WgtValueT *v) { switch (v->type) { - case WGT_IFACE_STRING: - return basValStringFromC(v->s ? v->s : ""); + case WGT_IFACE_STRING: { + BasValueT out = zeroValue(); + + out.type = BAS_TYPE_STRING; + out.strVal = basStringNew(v->s ? v->s : "", wgtValueLen(v)); + return out; + } case WGT_IFACE_INT: case WGT_IFACE_ENUM: @@ -4122,7 +4356,7 @@ static BasValueT callCommonMethod(BasFormRtT *rt, BasControlT *ctrl, const char // are skipped. outArgs, when given, are what the handler's leading // parameters refer to (ByRef event parameters such as Cancel). static bool callEventProc(BasFormRtT *rt, BasFormT *form, const BasProcEntryT *proc, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outArgCount) { - if (!proc || proc->isFunction) { + if (!eventProcFits(proc, argCount)) { return false; } @@ -4130,10 +4364,6 @@ static bool callEventProc(BasFormRtT *rt, BasFormT *form, const BasProcEntryT *p return rtCallHandler(rt, form, proc, proc->codeAddr, NULL, 0, NULL, 0); } - if (proc->paramCount != argCount) { - return false; - } - return rtCallHandler(rt, form, proc, proc->codeAddr, args, argCount, outArgs, outArgCount); } @@ -4221,15 +4451,44 @@ static BasValueT clipboardMethod(BasFormRtT *rt, const char *name, BasValueT *ar } +// A handle for a slot whose link CommOpen or CommOpenTcp opened: a hole +// a close left, else one more. 0 (the slot freed) when the link did not +// open. +static int32_t commAddSlot(CommSlotT *slot) { + if (!slot->link) { + free(slot); + return 0; + } + + int32_t handle = 0; + + for (int32_t i = 0; i < (int32_t)arrlen(sCommSlots) && handle == 0; i++) { + if (!sCommSlots[i]) { + handle = i + 1; + } + } + + if (handle == 0) { + arrput(sCommSlots, NULL); + handle = (int32_t)arrlen(sCommSlots); + } + + slot->active = true; + slot->owner = curRt(); + sCommSlots[handle - 1] = slot; + return handle; +} + + void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt) { CommSlotT *slot = commGetSlot(handle); - if (!slot || !sFormRt || !termCtrlName) { + if (!slot || !curRt() || !termCtrlName) { return; } - // Find the terminal widget by control name on any loaded form - WidgetT *termWidget = findCtrlWidgetByName(termCtrlName); + // The Terminal control of that name on any loaded form + WidgetT *termWidget = findTermWidget(termCtrlName); if (!termWidget) { return; @@ -4329,14 +4588,35 @@ static CommSlotT *commGetSlot(int32_t handle) { } +// A Comm call that waits (CommHandshake, CommSend) goes on: the link is +// polled, the program's events run (as a DoEvents loop would run them), +// and it is still open, the program is not ending, and limitMs has not +// passed since since. +static bool commGoOn(BasFormRtT *rt, int32_t handle, PlatformTicksT since, uint32_t limitMs) { + CommSlotT *slot = commGetSlot(handle); + + if (!slot || platformClock() - since >= PLATFORM_MS_TO_TICKS(limitMs)) { + return false; + } + + sCommApi.poll(slot->link); + return !rt || (rtPump(rt) && commGetSlot(handle) != NULL); +} + + // Key exchange, optionally authenticated by a shared passphrase (NULL or // "" = none; both sides must match). Returns True on success, False on // any failure, including a passphrase mismatch or a peer that uses a -// passphrase when this side does not (or the reverse). +// passphrase when this side does not (or the reverse). It waits up to +// SECLINK_HANDSHAKE_TIMEOUT_MS, a step at a time with the program's +// events between steps (a ready link is re-keyed). int32_t CommHandshake(int32_t handle, const char *passphrase) { - CommSlotT *slot = commGetSlot(handle); + CommSlotT *slot = commGetSlot(handle); + BasFormRtT *rt = basFormRtCurrent(); + PlatformTicksT since = platformClock(); + int32_t rc; - if (!slot || !sCommApi.handshake || !sCommApi.setPassphrase) { + if (!slot || slot->waiting || !sCommApi.handshakeStart || !sCommApi.handshakeStep || !sCommApi.setPassphrase) { return 0; } @@ -4346,7 +4626,27 @@ int32_t CommHandshake(int32_t handle, const char *passphrase) { return 0; } - return (commNote(slot, sCommApi.handshake(slot->link)) == SECLINK_SUCCESS) ? -1 : 0; + slot->waiting = true; + rc = commNote(slot, sCommApi.handshakeStart(slot->link)); + + while (rc == SECLINK_PENDING) { + if (!commGoOn(rt, handle, since, SECLINK_HANDSHAKE_TIMEOUT_MS)) { + rc = SECLINK_ERR_TIMEOUT; + break; + } + + slot = commGetSlot(handle); + rc = commNote(slot, sCommApi.handshakeStep(slot->link)); + } + + slot = commGetSlot(handle); + + if (slot != NULL) { + commNote(slot, rc); + slot->waiting = false; + } + + return (rc == SECLINK_SUCCESS) ? -1 : 0; } @@ -4457,28 +4757,7 @@ static void commOnRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, int32_t CommOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake) { - if (!commResolveApi()) { - return 0; - } - - // Reuse an empty slot (hole from a previous close) if possible. - // Otherwise grow the array by one pointer slot. - int32_t handle = 0; - int32_t n = (int32_t)arrlen(sCommSlots); - - for (int32_t i = 0; i < n; i++) { - if (!sCommSlots[i]) { - handle = i + 1; - break; - } - } - - if (handle == 0) { - arrput(sCommSlots, NULL); - handle = (int32_t)arrlen(sCommSlots); - } - - CommSlotT *slot = (CommSlotT *)calloc(1, sizeof(CommSlotT)); + CommSlotT *slot = commResolveApi() ? (CommSlotT *)calloc(1, sizeof(CommSlotT)) : NULL; if (!slot) { return 0; @@ -4491,15 +4770,27 @@ int32_t CommOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity } slot->link = sCommApi.open(com, baud, dataBits, parityChar, stopBits, handshake, commOnRecv, slot); + return commAddSlot(slot); +} - if (!slot->link) { - free(slot); + +// The packet link over a net.bas TCP connection (TcpConnect, TcpAccept) +// instead of a COM port: every channel, the handshake and encryption, as +// CommOpen's. The connection stays the program's: TcpClose it after +// CommClose. 0 when networking is not installed or the link cannot open. +int32_t CommOpenTcp(int32_t tcp) { + bool ready = commResolveApi() && sCommApi.openStream && sCommApi.tcpRead && sCommApi.tcpWrite && tcp > 0; + CommSlotT *slot = ready ? (CommSlotT *)calloc(1, sizeof(CommSlotT)) : NULL; + + if (!slot) { return 0; } - slot->active = true; - sCommSlots[handle - 1] = slot; - return handle; + PktStreamT stream = { commTcpRead, commTcpWrite, slot, NULL }; + + slot->tcp = tcp; + slot->link = sCommApi.openStream(&stream, commOnRecv, slot); + return commAddSlot(slot); } @@ -4576,39 +4867,70 @@ static bool commResolveApi(void) { return sCommApi.open != NULL; } - sCommApiResolved = true; - sCommApi.open = dlsym(NULL, DVX_SYM("secLinkOpen")); - sCommApi.close = dlsym(NULL, DVX_SYM("secLinkClose")); - sCommApi.handshake = dlsym(NULL, DVX_SYM("secLinkHandshake")); - sCommApi.isReady = dlsym(NULL, DVX_SYM("secLinkIsReady")); - sCommApi.poll = dlsym(NULL, DVX_SYM("secLinkPoll")); - sCommApi.send = dlsym(NULL, DVX_SYM("secLinkSend")); - sCommApi.sendBuf = dlsym(NULL, DVX_SYM("secLinkSendBuf")); - sCommApi.getPending = dlsym(NULL, DVX_SYM("secLinkGetPending")); - + sCommApiResolved = true; + sCommApi.open = dlsym(NULL, DVX_SYM("secLinkOpen")); + sCommApi.close = dlsym(NULL, DVX_SYM("secLinkClose")); + sCommApi.handshakeStart = dlsym(NULL, DVX_SYM("secLinkHandshakeStart")); + sCommApi.isReady = dlsym(NULL, DVX_SYM("secLinkIsReady")); + sCommApi.poll = dlsym(NULL, DVX_SYM("secLinkPoll")); + sCommApi.send = dlsym(NULL, DVX_SYM("secLinkSend")); + sCommApi.getPending = dlsym(NULL, DVX_SYM("secLinkGetPending")); sCommApi.setPassphrase = dlsym(NULL, DVX_SYM("secLinkSetPassphrase")); sCommApi.setChannelPlain = dlsym(NULL, DVX_SYM("secLinkSetChannelPlain")); sCommApi.handshakeStep = dlsym(NULL, DVX_SYM("secLinkHandshakeStep")); sCommApi.getDropped = dlsym(NULL, DVX_SYM("secLinkGetDropped")); + sCommApi.openStream = dlsym(NULL, DVX_SYM("secLinkOpenStream")); + sCommApi.tcpRead = dlsym(NULL, DVX_SYM("netBasStreamRead")); + sCommApi.tcpWrite = dlsym(NULL, DVX_SYM("netBasStreamWrite")); return sCommApi.open != NULL; } -// Send data (a BINARY STRING: every byte value) on channel. +// Send data (a BINARY STRING: every byte value) on channel, a frame at a +// time as the link takes it: while its window is full the link is polled +// and the program's events run (as a DoEvents loop would run them). False +// when the far side takes nothing for PKT_SEND_TIMEOUT_MS. int32_t CommSend(int32_t handle, const uint8_t *data, int32_t len, int32_t channel, int32_t encrypt) { - CommSlotT *slot = commGetSlot(handle); + CommSlotT *slot = commGetSlot(handle); + BasFormRtT *rt = basFormRtCurrent(); + PlatformTicksT since = platformClock(); + int32_t sent = 0; + int32_t rc = SECLINK_SUCCESS; - if (!slot || !data || !sCommApi.sendBuf || channel < 0 || channel >= SECLINK_NUM_CHANNELS) { + if (!slot || slot->waiting || !data || !sCommApi.send || !sCommApi.poll || channel < 0 || channel >= SECLINK_NUM_CHANNELS) { return 0; } - if (len == 0) { - return -1; + slot->waiting = true; + + while (sent < len) { + int32_t chunk = DVX_MIN(len - sent, SECLINK_MAX_PAYLOAD); + + rc = sCommApi.send(slot->link, data + sent, chunk, (uint8_t)channel, encrypt != 0, false); + + // The window is full: its acknowledgements come with the polls + if (rc == SECLINK_ERR_SEND && commGoOn(rt, handle, since, PKT_SEND_TIMEOUT_MS)) { + slot = commGetSlot(handle); + continue; + } + + if (rc != SECLINK_SUCCESS) { + break; + } + + sent += chunk; + since = platformClock(); } - int32_t rc = commNote(slot, sCommApi.sendBuf(slot->link, data, len, (uint8_t)channel, encrypt != 0)); - return (rc == 0) ? -1 : 0; + slot = commGetSlot(handle); + + if (slot != NULL) { + commNote(slot, rc); + slot->waiting = false; + } + + return (rc == SECLINK_SUCCESS) ? -1 : 0; } @@ -4626,6 +4948,17 @@ int32_t CommSetPlain(int32_t handle, int32_t channel, int32_t plain) { } +// CommOpenTcp's byte stream: the slot's net.bas connection. +static int32_t commTcpRead(void *ctx, uint8_t *buf, int32_t len) { + return sCommApi.tcpRead(((const CommSlotT *)ctx)->tcp, buf, len); +} + + +static int32_t commTcpWrite(void *ctx, const uint8_t *data, int32_t len) { + return sCommApi.tcpWrite(((const CommSlotT *)ctx)->tcp, data, len); +} + + // AnsiTerm read callback: drains the terminal channel's ring buffer static int32_t commTermRead(void *ctx, uint8_t *buf, int32_t maxLen) { CommSlotT *slot = (CommSlotT *)ctx; @@ -4711,6 +5044,57 @@ static BasControlT *ctrlFromWidget(WidgetT *w, BasFormRtT **outRt) { } +// The runtime current on this task, or NULL. +static BasFormRtT *curRt(void) { + uint32_t task = basFormRtTaskId(); + + for (int32_t i = 0; i < (int32_t)arrlen(sTaskRts); i++) { + if (sTaskRts[i].task == task) { + return sTaskRts[i].rt; + } + } + + return NULL; +} + + +// rt is ending: no task has it current any more. +static void curRtForget(const BasFormRtT *rt) { + for (int32_t i = (int32_t)arrlen(sTaskRts) - 1; i >= 0; i--) { + if (sTaskRts[i].rt == rt) { + arrdelswap(sTaskRts, i); + } + } +} + + +// Make rt (NULL: none) current on this task; the one it replaces. +static BasFormRtT *curRtSet(BasFormRtT *rt) { + uint32_t task = basFormRtTaskId(); + TaskRtT slot = { task, rt }; + + for (int32_t i = 0; i < (int32_t)arrlen(sTaskRts); i++) { + if (sTaskRts[i].task == task) { + BasFormRtT *prev = sTaskRts[i].rt; + + if (rt) { + sTaskRts[i].rt = rt; + } else { + arrdelswap(sTaskRts, i); + } + + return prev; + } + } + + if (rt) { + arrput(sTaskRts, slot); + } + + return NULL; +} + + // The default property of the control ref names (a store or read with no // property name: Text1 = "x"); NULL after a runtime error when it has none. static const char *defaultPropOf(BasFormRtT *rt, void *ref) { @@ -4803,6 +5187,9 @@ static void deviceBind(BasFormRtT *rt) { sDeviceApi.strig = (__typeof__(sDeviceApi.strig))dlsym(NULL, DVX_SYM("joyBasStrig")); sDeviceApi.frameInit = (__typeof__(sDeviceApi.frameInit))dlsym(NULL, DVX_SYM("gameFrameInit")); sDeviceApi.frameNext = (__typeof__(sDeviceApi.frameNext))dlsym(NULL, DVX_SYM("gameFrameNext")); + sDeviceApi.dosFor = (__typeof__(sDeviceApi.dosFor))dlsym(NULL, DVX_SYM("shellRunDosFor")); + sDeviceApi.dosDone = (__typeof__(sDeviceApi.dosDone))dlsym(NULL, DVX_SYM("shellRunDosDone")); + sDeviceApi.dosSplit = (__typeof__(sDeviceApi.dosSplit))dlsym(NULL, DVX_SYM("shellSplitCommand")); memset(&cb, 0, sizeof(cb)); if (sDeviceApi.beep && sDeviceApi.busy && sDeviceApi.play && sDeviceApi.playCount && sDeviceApi.sound) { @@ -4823,6 +5210,12 @@ static void deviceBind(BasFormRtT *rt) { cb.frameBusy = deviceFrameBusy; } + if (sDeviceApi.dosFor && sDeviceApi.dosDone && sDeviceApi.dosSplit) { + cb.shellStart = deviceShellStart; + cb.shellBusy = deviceShellBusy; + cb.shellResult = deviceShellResult; + } + cb.ctx = rt; basVmSetDeviceCallbacks(rt->vm, &cb); } @@ -4861,6 +5254,38 @@ static int32_t devicePlayCount(void *ctx) { } +// SHELL: until the shell's main loop has run the command (or refused it). +static bool deviceShellBusy(void *ctx) { + BasFormRtT *rt = (BasFormRtT *)ctx; + + return !sDeviceApi.dosDone(rt->shellAppId, &rt->shellRc); +} + + +static int32_t deviceShellResult(void *ctx) { + return ((const BasFormRtT *)ctx)->shellRc; +} + + +// SHELL command: the shell runs it with DVX stepped aside for this +// program, which waits (shellRunDosFor: the other programs are asked to +// close first); a bare command (DIR) goes to the command interpreter. +static bool deviceShellStart(void *ctx, const char *command) { + BasFormRtT *rt = (BasFormRtT *)ctx; + char program[DVX_MAX_PATH]; + char args[SHELL_ARGS_MAX]; + + if (!rt->ctx || !sDeviceApi.dosSplit(command, program, (int32_t)sizeof(program), args, (int32_t)sizeof(args))) { + return false; + } + + rt->shellAppId = rt->ctx->currentAppId; + rt->shellRc = -1; + sDeviceApi.dosFor(rt->shellAppId, program, args, NULL, false); + return true; +} + + static void deviceSound(void *ctx, double hz, double ticks) { (void)ctx; sDeviceApi.sound(hz, ticks); @@ -4890,7 +5315,9 @@ static int32_t deviceStrig(void *ctx, int32_t n) { // closing the box the last), or -1 for no buttons or more than // DVX_BUTTON_BOX_MAX. icon is a MsgBox icon value (vbQuestion ...) or 0. int32_t DlgButtons(const char *title, const char *message, const char *buttons, int32_t icon) { - if (!sFormRt || !buttons) { + BasFormRtT *rt = curRt(); + + if (!rt || !buttons) { return -1; } @@ -4905,12 +5332,14 @@ int32_t DlgButtons(const char *title, const char *message, const char *buttons, return -1; } - return dvxButtonBox(sFormRt->ctx, title, message ? message : "", icon & MB_ICON_MASK, labels, count); + return dvxButtonBox(rt->ctx, title, message ? message : "", icon & MB_ICON_MASK, labels, count); } int32_t DlgChoice(const char *title, const char *prompt, const char *items, int32_t defaultIdx) { - if (!sFormRt || !items) { + BasFormRtT *rt = curRt(); + + if (!rt || !items) { return -1; } @@ -4926,7 +5355,7 @@ int32_t DlgChoice(const char *title, const char *prompt, const char *items, int3 int32_t count = (buf && ptrs) ? splitPipeList(buf, ptrs, maxItems) : 0; int32_t chosen = -1; - if (count > 0 && !dvxChoiceDialog(sFormRt->ctx, title, prompt, ptrs, count, defaultIdx, &chosen)) { + if (count > 0 && !dvxChoiceDialog(rt->ctx, title, prompt, ptrs, count, defaultIdx, &chosen)) { chosen = -1; } @@ -4939,9 +5368,10 @@ int32_t DlgChoice(const char *title, const char *prompt, const char *items, int3 // The color dialog: the chosen color as RGB() makes it, or -1 when // cancelled. defaultColor is the starting color. int32_t DlgColor(const char *title, int32_t defaultColor) { - uint32_t rgb = wgtBasicColor((uint32_t)defaultColor) & BAS_RGB_MASK; + uint32_t rgb = wgtBasicColor((uint32_t)defaultColor) & BAS_RGB_MASK; + BasFormRtT *rt = curRt(); - if (sFormRt && dvxColorDialog(sFormRt->ctx, title, &rgb)) { + if (rt && dvxColorDialog(rt->ctx, title, &rgb)) { return (int32_t)wgtBasicColor(rgb); } @@ -4988,7 +5418,9 @@ int32_t DlgFont(const char *title, const char *family, int32_t size, int32_t sty sDlgFont.size = size; sDlgFont.style = (uint32_t)style & (FONT_STYLE_FACE | FONT_STYLE_LINES); - return (sFormRt && dvxFontDialog(sFormRt->ctx, title, (uint32_t)flags, &sDlgFont)) ? -1 : 0; + BasFormRtT *rt = curRt(); + + return (rt && dvxFontDialog(rt->ctx, title, (uint32_t)flags, &sDlgFont)) ? -1 : 0; } @@ -5008,7 +5440,9 @@ int32_t DlgFontStyle(void) { const char *DlgInputBox(const char *title, const char *prompt, const char *defaultText) { - if (!sFormRt) { + BasFormRtT *rt = curRt(); + + if (!rt) { sLastInputBoxCancelled = true; return ""; } @@ -5016,7 +5450,7 @@ const char *DlgInputBox(const char *title, const char *prompt, const char *defau static char buf[BAS_INPUTBOX_BUF_LEN]; buf[0] = '\0'; - if (dvxInputBox(sFormRt->ctx, title, prompt, defaultText, buf, sizeof(buf))) { + if (dvxInputBox(rt->ctx, title, prompt, defaultText, buf, sizeof(buf))) { sLastInputBoxCancelled = false; return buf; } @@ -5039,19 +5473,22 @@ int32_t DlgIntInput(const char *title, const char *prompt, int32_t defaultVal, i // The integer input box with the spinner moving by stepVal; the default // value when cancelled (DlgInputCancelled tells). int32_t DlgIntInputStep(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal, int32_t stepVal) { - int32_t result = defaultVal; + int32_t result = defaultVal; + BasFormRtT *rt = curRt(); - sLastInputBoxCancelled = !sFormRt || !dvxIntInputBox(sFormRt->ctx, title, prompt, defaultVal, minVal, maxVal, stepVal, &result); + sLastInputBoxCancelled = !rt || !dvxIntInputBox(rt->ctx, title, prompt, defaultVal, minVal, maxVal, stepVal, &result); return sLastInputBoxCancelled ? defaultVal : result; } int32_t DlgPromptSave(const char *title) { - if (!sFormRt) { + BasFormRtT *rt = curRt(); + + if (!rt) { return DVX_SAVE_NO; } - return dvxPromptSave(sFormRt->ctx, title); + return dvxPromptSave(rt->ctx, title); } @@ -5109,7 +5546,7 @@ static void dragBegin(BasFormRtT *rt, BasControlT *ctrl) { sDrag.prevButtons = ctx->mouseButtons; dragHookForms(rt, true); - if (!ctrl->dragIcon || !dragShowIcon(ctx, ctrl->dragIcon)) { + if (!ctrl->dragIcon || !dragShowIcon(rt, ctx, ctrl->dragIcon)) { dvxDragOutlineShow(ctx, w->w, w->h, win->x + win->contentX + w->x - ctx->mouseX, win->y + win->contentY + w->y - ctx->mouseY); } @@ -5309,23 +5746,26 @@ static void dragOverUpdate(void) { } -// Show the DragIcon picture at path as the drag feedback; false when it -// cannot be read (the outline is shown instead). -static bool dragShowIcon(AppContextT *ctx, const char *path) { - int32_t len = 0; - char *data = pictureFileRead(path, &len); - int32_t w = 0; - int32_t h = 0; - int32_t pitch = 0; - bool hasKey = false; - uint32_t key = 0; +// Show the DragIcon picture at path (a file, or a LoadPictureData +// picture's name) as the drag feedback; false when it cannot be read (the +// outline is shown instead). +static bool dragShowIcon(const BasFormRtT *rt, AppContextT *ctx, const char *path) { + const BasPictureT *pic = pictureByDataName(rt, path); + int32_t len = pic ? pic->dataLen : 0; + char *read = pic ? NULL : pictureFileRead(path, &len); + const uint8_t *bytes = pic ? pic->data : (const uint8_t *)read; + int32_t w = 0; + int32_t h = 0; + int32_t pitch = 0; + bool hasKey = false; + uint32_t key = 0; - if (!data) { + if (!bytes) { return false; } - sDrag.icon = dvxLoadImageAlpha(ctx, (const uint8_t *)data, len, &w, &h, &pitch, &hasKey, &key); - free(data); + sDrag.icon = dvxLoadImageAlpha(ctx, bytes, len, &w, &h, &pitch, &hasKey, &key); + free(read); if (!sDrag.icon) { return false; @@ -5389,6 +5829,21 @@ static const BasProcEntryT *eventHandler(BasFormRtT *rt, const BasControlT *obj, } +// The rule callEventProc calls a handler by: a SUB taking the event's +// arguments, or none. +static bool eventProcFits(const BasProcEntryT *proc, int32_t argCount) { + return proc && !proc->isFunction && (proc->paramCount == 0 || proc->paramCount == argCount); +} + + +// True when the event has a handler that would run (eventProcFits). +static bool eventWanted(BasFormRtT *rt, const BasControlT *obj, const char *ctrlName, const char *eventName, int32_t argCount) { + bool overridden; + + return eventProcFits(eventHandler(rt, obj, ctrlName, eventName, &overridden), argCount); +} + + // The file dialogs of commdlg.bas: flags FD_OPEN, FD_SAVE, or FD_FOLDER, // starting in folder (NULL or "" = the current folder), titled title ("" // = the dialog's own). The chosen path, or "" when cancelled. @@ -5396,15 +5851,16 @@ static const char *fileDialog(const char *title, const char *filter, int32_t fla static char path[DVX_MAX_PATH]; FileFilterT *filters = NULL; char filterBuf[BAS_FILTER_BUF_LEN]; + BasFormRtT *rt = curRt(); path[0] = '\0'; - if (!sFormRt) { + if (!rt) { return path; } int32_t count = parseFileFilters(filter, &filters, filterBuf, sizeof(filterBuf)); - bool ok = dvxFileDialog(sFormRt->ctx, (title && title[0]) ? title : NULL, flags, (folder && folder[0]) ? folder : NULL, filters, count, path, sizeof(path)); + bool ok = dvxFileDialog(rt->ctx, (title && title[0]) ? title : NULL, flags, (folder && folder[0]) ? folder : NULL, filters, count, path, sizeof(path)); free(filters); @@ -5436,12 +5892,14 @@ static BasControlT *findButtonWith(BasFormT *form, const char *propName) { // The running program's control called name, on any of its loaded forms // (the first one found), or NULL. static BasControlT *findCtrlByName(const char *name) { - if (!sFormRt || !name) { + BasFormRtT *rt = curRt(); + + if (!rt || !name) { return NULL; } - for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) { - BasControlT *hit = findCtrlInForm(sFormRt->forms[i], name); + for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) { + BasControlT *hit = findCtrlInForm(rt->forms[i], name); if (hit) { return hit; @@ -5466,7 +5924,7 @@ static BasControlT *findCtrlByWidget(BasFormT *form, const WidgetT *w) { // Case-insensitive scan of a single form's controls by bare name. The // shared core of every unindexed control lookup (basFormRtFindCtrl, -// basFormRtRemoveCtrl, findCtrlWidgetByName). Control-array elements +// basFormRtRemoveCtrl, findCtrlByName). Control-array elements // are never matched here: an unindexed reference to an array is an // error in VB, and matching one would hit an arbitrary element. static BasControlT *findCtrlInForm(BasFormT *form, const char *name) { @@ -5480,15 +5938,6 @@ static BasControlT *findCtrlInForm(BasFormT *form, const char *name) { } -// Finds a control's live widget by name across every loaded form. Shared by -// CommAttach/SerAttach to locate the terminal widget to bind a serial channel. -static WidgetT *findCtrlWidgetByName(const char *name) { - BasControlT *ctrl = findCtrlByName(name); - - return ctrl ? ctrl->widget : NULL; -} - - // rt's loaded form owning this window, or NULL (also when rt is NULL). // Shared by every window callback (onForm*) and the drag. static BasFormT *findFormByWindow(const BasFormRtT *rt, const WindowT *win) { @@ -5574,6 +6023,17 @@ static const BasPropDescT *findPropDesc(const BasPropDescT *table, int32_t count } +// The live widget of the running program's Terminal control called name +// (BAS_TERMINAL_TYPE), else NULL: a control of another type is no +// terminal, and without the terminal widget no control is one. Shared by +// SerAttach, CommAttach, and basFormRtTermBind. +static WidgetT *findTermWidget(const char *name) { + BasControlT *ctrl = findCtrlByName(name); + + return ctrl && strcasecmp(ctrl->typeName, BAS_TERMINAL_TYPE) == 0 ? ctrl->widget : NULL; +} + + static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount) { fireCtrlEventOut(rt, ctrl, eventName, args, argCount, NULL, 0); } @@ -5596,6 +6056,15 @@ static void fireCtrlEventOut(BasFormRtT *rt, BasControlT *ctrl, const char *even return; } + // Dispatched on another task: the program's own task runs it + if (!outArgs && queueForeign(rt, eventName)) { + if (eventWanted(rt, ctrl, ctrl->name, eventName, argCount + (ctrl->index >= 0 ? 1 : 0))) { + queueEvent(rt, ctrl->form, ctrl, ctrl->name, eventName, args, argCount, BasQueuedCtrlEventE); + } + + return; + } + // Re-entrancy guard: reject only same-event re-entry (runSubLoop // pumps events during long handlers; without this, a lingering // mouse-up can re-deliver Click on top of itself). But allow @@ -5702,6 +6171,11 @@ static bool fireEventArgsOut(BasFormRtT *rt, BasFormT *form, BasControlT *obj, c return false; } + // Dispatched on another task: the program's own task runs it + if (!outArgs && queueForeign(rt, eventName)) { + return eventWanted(rt, obj, ctrlName, eventName, argCount) && queueEvent(rt, form, obj, ctrlName, eventName, args, argCount, BasQueuedEventE); + } + bool overridden; const BasProcEntryT *proc = eventHandler(rt, obj, ctrlName, eventName, &overridden); @@ -5836,6 +6310,18 @@ static void formatPropNames(const BasPropDescT *table, int32_t count, bool forWr } +// A form's window was closed: the form is unloaded (QueryUnload may keep +// it); with no form left the program's code stops. A committed unload is +// detached (removed from rt->forms) even when its frees are deferred. +static void formClose(BasFormRtT *rt, BasFormT *form) { + basFormRtUnloadForm(rt, form); + + if (arrlen(rt->forms) == 0 && rt->vm) { + rt->vm->running = false; + } +} + + // The object whose SetEvent overrides apply to a by-name form-level // event: the form's own formCtrl, an already-created menu proxy (SetEvent // on a menu name creates one), or NULL. @@ -5858,6 +6344,28 @@ static BasControlT *formEventObject(BasFormT *form, const char *ctrlName) { } +// A full-screen form's screen came, went behind the desktop, or ended: +// its FullScreen event (State: dvxFullScreenActive, dvxFullScreenSuspended, +// or dvxFullScreenEnded). +static void formFullChanged(WindowT *win, ExclusiveStateE state, void *userData) { + BasFormRtT *rt = (BasFormRtT *)userData; + BasFormT *form = findFormByWindow(rt, win); + int32_t code = BAS_FULLSCREEN_ENDED; + + if (state == ExclusiveActiveE) { + code = BAS_FULLSCREEN_ACTIVE; + } else if (state == ExclusiveSuspendedE) { + code = BAS_FULLSCREEN_SUSPENDED; + } + + if (form) { + BasValueT arg = basValInteger((int16_t)code); + + basFormRtFireEventArgs(rt, form, form->name, BAS_EVT_FULLSCREEN, &arg, 1); + } +} + + // The form's own methods that reach the runtime as method calls: // FitWidth / FitHeight (shrink-wrap one dimension to the controls, as // AutoSize does both) and SaveScreenshot path$ (the window's picture to @@ -6047,7 +6555,9 @@ static void formRtSerIdleSync(void) { // ended when it was called) since the program started waiting at its // current rate (FRAMEWAIT's own pacer, GameFramePacerT.missed). int32_t FrameMissed(void) { - return (sFormRt && sFormRt->frameHz > 0) ? (int32_t)sFormRt->framePacer.missed : 0; + BasFormRtT *rt = curRt(); + + return (rt && rt->frameHz > 0) ? (int32_t)rt->framePacer.missed : 0; } @@ -6378,8 +6888,17 @@ static void frmLoadOnFormProp(void *userData, const char *key, const char *value case FORM_PROP_ICON: case FORM_PROP_TAG: - case FORM_PROP_MOUSEPOINTER: { - BasValueT v = (pd->type == WGT_IFACE_INT) ? basValLong(num) : basValStringFromC(text); + case FORM_PROP_MOUSEPOINTER: + case FORM_PROP_MAXWIDTH: + case FORM_PROP_MAXHEIGHT: + case FORM_PROP_GAMEINPUT: { + BasValueT v; + + if (pd->type == WGT_IFACE_STRING) { + v = basValStringFromC(text); + } else { + v = (pd->type == WGT_IFACE_BOOL) ? basValBool(flag) : basValLong(num); + } setFormProp(ctx->rt, frm, pd->name, v); basValRelease(&v); @@ -6401,6 +6920,7 @@ static void frmLoadOnFormProp(void *userData, const char *key, const char *value case FORM_PROP_DESIGNMODE: case FORM_PROP_DESIGNSELECTION: case FORM_PROP_ACTIVECONTROL: + case FORM_PROP_FULLSCREEN: case FORM_PROP_COUNT: // Name comes from the Begin line; the others are runtime-only. break; @@ -6603,7 +7123,7 @@ static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *ha case CTRL_PROP_PARENT: case CTRL_PROP_CONTAINER: - return basFormRtDesignCtrlProp(sFormRt, ctrl, pd->name); + return basFormRtDesignCtrlProp(curRt(), ctrl, pd->name); case CTRL_PROP_TAG: return basValStringFromC(ctrl->tag ? ctrl->tag : ""); @@ -6711,6 +7231,18 @@ static BasValueT getFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName case FORM_PROP_ACTIVECONTROL: return activeControlOf(rt, frm); + case FORM_PROP_MAXWIDTH: + return basValLong((win && win->maxW != WM_MAX_FROM_SCREEN) ? win->maxW : FORM_NO_MAX); + + case FORM_PROP_MAXHEIGHT: + return basValLong((win && win->maxH != WM_MAX_FROM_SCREEN) ? win->maxH : FORM_NO_MAX); + + case FORM_PROP_GAMEINPUT: + return basValBool(win && win->gameInput); + + case FORM_PROP_FULLSCREEN: + return basValBool(win && rt->ctx->exclusive.owner == win); + case FORM_PROP_COUNT: break; } @@ -6736,30 +7268,45 @@ static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *pr // The help compiler's errors and warnings are kept for -// HelpCompileMessages$ (the last compile's only). +// HelpCompileMessages$ (the last compile's only). .image names are +// looked for beside the source. The compile lets the program's events +// and DVX go on (helpCompileTick), so another HelpCompile (this program's +// or another's) meanwhile is refused at once, the messages left alone. int32_t HelpCompile(const char *inputFile, const char *outputFile) { - if (!sHlpcResolved) { - sHlpcResolved = true; - sHlpcCompile = dlsym(NULL, DVX_SYM("hlpcCompile")); - sHlpcSetMessage = dlsym(NULL, DVX_SYM("hlpcSetMessageFn")); + if (sHelpCompiling) { + return 0; } // cppcheck-suppress unsignedLessThanZero ; stb_ds arrsetlen(a, 0) compares the capacity with 0 arrsetlen(sHelpMessages, 0); - if (!sHlpcCompile || !sHlpcSetMessage || !inputFile || !outputFile) { + HlpcModuleT hlpc; + + if (!inputFile || !outputFile || !hlpcModuleOpen(&hlpc)) { helpCompileMessage(NULL, HELP_COMPILER_MISSING); return 0; } const char *inputs[1]; + char imgDir[DVX_MAX_PATH]; + inputs[0] = inputFile; - sHlpcSetMessage(helpCompileMessage, NULL); + if (!platformPathDirName(inputFile, imgDir, sizeof(imgDir))) { + snprintf(imgDir, sizeof(imgDir), "."); + } - int32_t rc = sHlpcCompile(inputs, 1, outputFile, NULL, NULL, HLPC_QUIET, NULL, NULL); + sHelpCompiling = true; + sHelpPumpAt = platformClock(); + hlpc.setMessage(helpCompileMessage, NULL); + hlpc.setTick(helpCompileTick, curRt()); - sHlpcSetMessage(NULL, NULL); + int32_t rc = hlpc.compile(inputs, 1, outputFile, imgDir, NULL, HLPC_QUIET, NULL, NULL); + + hlpc.setTick(NULL, NULL); + hlpc.setMessage(NULL, NULL); + hlpcModuleClose(&hlpc); + sHelpCompiling = false; return (rc == 0) ? -1 : 0; } @@ -6784,6 +7331,24 @@ const char *HelpCompileMessages(void) { } +// The help compiler's tick (hlpcSetTickFn): every HELP_PUMP_MS the +// program's events and DVX go on, as DoEvents does (not in a control +// file's code, which must not wait); false (stop the compile) once the +// program is ending. +static bool helpCompileTick(void *ctx) { + BasFormRtT *rt = (BasFormRtT *)ctx; + + if (!rt || rt->vm->runLimit > 0 || platformClock() - sHelpPumpAt < PLATFORM_MS_TO_TICKS(HELP_PUMP_MS)) { + return !rt || !rt->terminated; + } + + bool goOn = rtPump(rt); + + sHelpPumpAt = platformClock(); + return goOn; +} + + // Open hlpFile in the Help Viewer (HelpViewTopic with no topic). void HelpView(const char *hlpFile) { helpViewOpen(hlpFile, ""); @@ -6800,7 +7365,9 @@ static void helpViewOpen(const char *hlpFile, const char *topicId) { sShellOpenHelp = dlsym(NULL, DVX_SYM("shellOpenHelp")); } - if (!sShellOpenHelp || !sFormRt || !sFormRt->ctx || !hlpFile || !topicId) { + BasFormRtT *rt = curRt(); + + if (!sShellOpenHelp || !rt || !rt->ctx || !hlpFile || !topicId) { return; } @@ -6810,8 +7377,8 @@ static void helpViewOpen(const char *hlpFile, const char *topicId) { if (!hlpFile[0]) { snprintf(resolved, sizeof(resolved), "%s", DVX_SYSTEM_HELP_FILE_LITERAL); - } else if (!hasDir && sFormRt->vm && sFormRt->vm->appPath[0]) { - snprintf(resolved, sizeof(resolved), "%s" DVX_PATH_SEP "%s", sFormRt->vm->appPath, hlpFile); + } else if (!hasDir && rt->vm && rt->vm->appPath[0]) { + snprintf(resolved, sizeof(resolved), "%s" DVX_PATH_SEP "%s", rt->vm->appPath, hlpFile); } else { snprintf(resolved, sizeof(resolved), "%s", hlpFile); } @@ -6819,7 +7386,7 @@ static void helpViewOpen(const char *hlpFile, const char *topicId) { const char *quote = strchr(resolved, ' ') ? "\"" : ""; snprintf(args, sizeof(args), "%s%s%s%s%s", quote, resolved, quote, topicId[0] ? " " : "", topicId); - sShellOpenHelp(sFormRt->ctx, args); + sShellOpenHelp(rt->ctx, args); } @@ -7181,6 +7748,8 @@ static void onFormActivate(WindowT *win) { // basFormRtUnloadForm so the close box takes the exact same // QueryUnload/Unload/teardown path (including the `unloading` // re-entrancy guard and deferred frees) as the UNLOAD statement. +// A form's window closed: on another task (the shell's frame took the +// close) the program's own task unloads it at its turn. static void onFormClose(WindowT *win) { BasFormRtT *rt; BasFormT *form = formOfWindow(win, &rt); @@ -7189,14 +7758,12 @@ static void onFormClose(WindowT *win) { return; } - basFormRtUnloadForm(rt, form); - - // If no forms are left, stop the VM. A QueryUnload cancel leaves the - // form in rt->forms, so this stays false then; a committed unload is - // detached (removed) even when its frees are deferred. - if (arrlen(rt->forms) == 0 && rt->vm) { - rt->vm->running = false; + if (queueForeign(rt, NULL)) { + queueEvent(rt, form, NULL, form->name, "", NULL, 0, BasQueuedCloseE); + return; } + + formClose(rt, form); } @@ -7652,17 +8219,17 @@ static int32_t parseFileFilters(const char *filter, FileFilterT **outFilters, ch } -// Name of the form's popup menu that owns this MenuT, or "" (also for -// NULL, i.e. no context menu set). -// A new Picture object of width x height pixels for file path, kept in -// rt->pictures for the runtime's life. NULL when memory runs out. +// A new Picture object of width x height pixels for file path (no bytes), +// kept in rt->pictures for the runtime's life. NULL when memory runs out. static BasPictureT *pictureAdd(BasFormRtT *rt, const char *path, int32_t width, int32_t height) { size_t pathSize = strlen(path) + 1; BasPictureT *pic = (BasPictureT *)malloc(sizeof(BasPictureT) + pathSize); if (pic) { - pic->width = width; - pic->height = height; + pic->width = width; + pic->height = height; + pic->data = NULL; + pic->dataLen = 0; memcpy(pic->path, path, pathSize); arrput(rt->pictures, pic); } @@ -7671,6 +8238,34 @@ static BasPictureT *pictureAdd(BasFormRtT *rt, const char *path, int32_t width, } +// The LoadPictureData picture whose name (pictureDataName) path is, else +// NULL. +static BasPictureT *pictureByDataName(const BasFormRtT *rt, const char *path) { + char *end = NULL; + long index = (basPictureIsData(path) && isdigit((unsigned char)path[1])) ? strtol(path + 1, &end, BAS_PICTURE_NAME_RADIX) : -1; + + if (index < 0 || *end != '\0' || index >= (long)arrlen(rt->pictures)) { + return NULL; + } + + return rt->pictures[index]->data ? rt->pictures[index] : NULL; +} + + +// The name a property showing the LoadPictureData picture pic holds: +// BAS_PICTURE_DATA_MARK and its place in rt->pictures (no file is named +// so). +static void pictureDataName(const BasFormRtT *rt, const BasPictureT *pic, char *out, int32_t outSize) { + int32_t index = 0; + + while (index < (int32_t)arrlen(rt->pictures) && rt->pictures[index] != pic) { + index++; + } + + snprintf(out, (size_t)outSize, "%c%" PRId32, BAS_PICTURE_DATA_MARK, index); +} + + // The bytes of a picture file, or of the icon a DVX program, widget or // library file carries (dvxResLoadIconData), for dvxLoadImageAlpha. NULL // when the file cannot be read. Free with free(). @@ -7703,6 +8298,12 @@ static BasPictureT *pictureFind(const BasFormRtT *rt, const void *ref) { // one LoadPicture made for it, else a new one of the picture's size (0 x 0 // when the file cannot be read any more). NULL when memory runs out. static BasPictureT *pictureFor(BasFormRtT *rt, const char *path) { + BasPictureT *named = pictureByDataName(rt, path); + + if (named) { + return named; + } + int32_t err = 0; BasPictureT *pic = (BasPictureT *)basFormRtLoadPicture(rt, path, &err); @@ -7714,8 +8315,8 @@ static BasPictureT *pictureFor(BasFormRtT *rt, const char *path) { // HiMetric units (0.01 mm, through the ScaleMode conversions: VB's 15 // twips a pixel, so a pixel is about 26.46), and Type (vbPicTypeBitmap, // vbPicTypeNone for the empty picture); its default value (no name) is -// its file name. Handle is not offered: DVX pictures have no system -// handle. +// its file name ("" for one from bytes). Handle is not offered: DVX +// pictures have no system handle. static BasValueT pictureProp(BasFormRtT *rt, const BasPictureT *pic, const char *propName) { if (strcmp(propName, BAS_PIC_PROP_DEFAULT) == 0) { return basValStringFromC(pic->path); @@ -7730,7 +8331,7 @@ static BasValueT pictureProp(BasFormRtT *rt, const BasPictureT *pic, const char } if (strcasecmp(propName, BAS_PIC_PROP_TYPE) == 0) { - return basValInteger(pic->path[0] ? BAS_PIC_TYPE_BITMAP : BAS_PIC_TYPE_NONE); + return basValInteger((pic->path[0] || pic->data) ? BAS_PIC_TYPE_BITMAP : BAS_PIC_TYPE_NONE); } basFormRtRuntimeError(rt, @@ -7741,6 +8342,76 @@ static BasValueT pictureProp(BasFormRtT *rt, const BasPictureT *pic, const char } +// Show the LoadPictureData picture pic as ctrl's propName (one +// pictureTakesData allows), the property holding the picture's name +// (pictureDataName): a form's Icon, DragIcon (the drag shows it), or the +// Picture of a control that takes picture bytes +// (WGT_METHOD_SET_PICTURE_DATA). A runtime error is raised when it +// cannot be shown. +static void pictureShowData(BasFormRtT *rt, BasControlT *ctrl, const char *propName, const BasPictureT *pic) { + char name[BAS_PICTURE_NAME_MAX]; + + pictureDataName(rt, pic, name, (int32_t)sizeof(name)); + + if (strcasecmp(propName, BAS_PROP_DRAGICON) == 0) { + free(ctrl->dragIcon); + ctrl->dragIcon = strdup(name); + return; + } + + if (ctrl->form && ctrl == &ctrl->form->formCtrl) { + BasFormT *frm = ctrl->form; + int32_t w = 0; + int32_t h = 0; + int32_t pitch = 0; + uint8_t *pixels = frm->window ? dvxLoadImageFromMemory(rt->ctx, pic->data, pic->dataLen, &w, &h, &pitch) : NULL; + + if (frm->window && !pixels) { + basFormRtRuntimeError(rt, "Icon not loaded", "Form: %s\nThere is not enough memory for the picture.", frm->name); + return; + } + + snprintf(frm->iconPath, sizeof(frm->iconPath), "%s", name); + + if (frm->window) { + wmSetIconData(frm->window, pixels, w, h, pitch); + } + + return; + } + + const WgtMethodDescT *m = ctrl->iface ? ifaceFindMethod(ctrl->iface, WGT_METHOD_SET_PICTURE_DATA, WGT_SIG_DYNAMIC) : NULL; + WgtValueT args[WGT_PICTURE_DATA_ARGS_MAX]; + WgtValueT ret; + + if (!m) { + basFormRtRuntimeError(rt, + "Picture from LoadPictureData not shown", + "Control: %s\nType: %s\nOnly an Image, an ImageButton, or a PictureBox shows a picture made from bytes.", + ctrlDisplayName(ctrl), ctrl->typeName); + return; + } + + args[0] = wgtValueBytes((const char *)pic->data, pic->dataLen); + args[1] = wgtValueStr(name); + + if (!wgtIfaceCallMethod(ctrl->iface, ctrl->widget, m, args, WGT_PICTURE_DATA_ARGS_MAX, &ret) || !wgtValueToBool(&ret)) { + basFormRtRuntimeError(rt, "Picture not loaded", "Control: %s\nThere is not enough memory for the picture.", ctrlDisplayName(ctrl)); + } +} + + +// Whether propName of ctrl takes a LoadPictureData picture: a form's +// Icon, a control's Picture or DragIcon. +static bool pictureTakesData(const BasControlT *ctrl, const char *propName) { + if (ctrl->form && ctrl == &ctrl->form->formCtrl) { + return strcasecmp(propName, BAS_PROP_ICON) == 0; + } + + return strcasecmp(propName, BAS_PROP_PICTURE) == 0 || strcasecmp(propName, BAS_PROP_DRAGICON) == 0; +} + + // The DVX pointer (CURSOR_*) a MousePointer value shows; false for a // value DVX has no pointer for. static bool pointerCursor(int32_t pointer, int32_t *outCursor) { @@ -7772,6 +8443,8 @@ static void popupMenuAdd(BasFormT *form, const char *name, MenuT *menu, bool own } +// Name of the form's popup menu that owns this MenuT, or "" (also for +// NULL, i.e. no context menu set). static const char *popupMenuNameFor(const BasFormT *form, const MenuT *menu) { if (menu && form) { for (int32_t i = 0; i < form->popupMenuCount; i++) { @@ -7847,11 +8520,12 @@ int32_t PrintFormDialog(const char *formName) { static int32_t printFormWindow(const char *formName, bool askFirst) { - BasFormT *form = NULL; + BasFormRtT *rt = curRt(); + BasFormT *form = NULL; - for (int32_t i = 0; sFormRt && formName && i < (int32_t)arrlen(sFormRt->forms); i++) { - if (strcasecmp(sFormRt->forms[i]->name, formName) == 0) { - form = sFormRt->forms[i]; + for (int32_t i = 0; rt && formName && i < (int32_t)arrlen(rt->forms); i++) { + if (strcasecmp(rt->forms[i]->name, formName) == 0) { + form = rt->forms[i]; break; } } @@ -7860,7 +8534,171 @@ static int32_t printFormWindow(const char *formName, bool askFirst) { return 0; } - return sPrintApi.window(sFormRt->ctx, form->window, askFirst) ? -1 : 0; + return sPrintApi.window(rt->ctx, form->window, askFirst) ? -1 : 0; +} + + +// A waiting event's copied arguments go. +static void queueArgsFree(BasQueuedEventT *ev) { + for (int32_t i = 0; i < ev->argCount; i++) { + basValRelease(&ev->args[i]); + } + + free(ev->args); + ev->args = NULL; + ev->argCount = 0; +} + + +// The events that waited for the program's own task run, oldest first +// (a DoEvents in one of their handlers runs the next, as VB's does). Each +// runs in its own eventDepth bracket, so the queue's goes once the last +// is taken. A form or object gone meanwhile has its events dropped. +static void queueDrain(BasFormRtT *rt) { + while (arrlen(rt->queued) > 0 && !rt->terminated && !queueForeign(rt, NULL)) { + BasQueuedEventT ev = rt->queued[0]; + + arrdel(rt->queued, 0); + sQueuedTotal--; + rtEventEnter(rt); + + if (arrlen(rt->queued) == 0 && rt->queueHeld) { + rt->queueHeld = false; + rtEventLeave(rt); + } + + // Booked as the program's handler (a Task Manager's "Not + // responding" counts from here, as for a window callback) + int32_t appId = rt->ctx->currentAppId; + + dvxAppCallEnter(appId); + + if (queueLive(rt, &ev)) { + switch (ev.kind) { + case BasQueuedCtrlEventE: + fireCtrlEventOut(rt, ev.obj, ev.eventName, ev.args, ev.argCount, NULL, 0); + break; + + case BasQueuedCloseE: + formClose(rt, ev.form); + break; + + case BasQueuedEventE: + fireEventArgsOut(rt, ev.form, ev.obj, ev.ctrlName, ev.eventName, ev.args, ev.argCount, NULL, 0); + break; + } + } + + dvxAppCallLeave(appId); + queueArgsFree(&ev); + rtEventLeave(rt); + } +} + + +// Keep an event for the program's own task, its arguments copied; a newer +// state (sQueueLatest) replaces one of the same object still waiting. The +// first to wait takes the queue's eventDepth bracket. True (it will run). +static bool queueEvent(BasFormRtT *rt, BasFormT *form, BasControlT *obj, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount, BasQueuedKindE kind) { + BasQueuedEventT *slot = NULL; + + for (int32_t i = 0; !slot && i < DVX_ARRAY_LEN(sQueueLatest); i++) { + for (int32_t j = (int32_t)arrlen(rt->queued) - 1; j >= 0 && strcasecmp(eventName, sQueueLatest[i]) == 0; j--) { + BasQueuedEventT *ev = &rt->queued[j]; + + if (ev->form == form && ev->obj == obj && strcasecmp(ev->eventName, eventName) == 0 && strcasecmp(ev->ctrlName, ctrlName) == 0) { + slot = ev; + break; + } + } + } + + if (slot) { + queueArgsFree(slot); + } else { + BasQueuedEventT ev; + + memset(&ev, 0, sizeof(ev)); + ev.form = form; + ev.obj = obj; + ev.kind = kind; + snprintf(ev.ctrlName, sizeof(ev.ctrlName), "%s", ctrlName); + snprintf(ev.eventName, sizeof(ev.eventName), "%s", eventName); + arrput(rt->queued, ev); + slot = &arrlast(rt->queued); + sQueuedTotal++; + + if (!rt->queueHeld) { + rt->queueHeld = true; + rtEventEnter(rt); + } + } + + slot->args = (argCount > 0) ? (BasValueT *)malloc(sizeof(BasValueT) * argCount) : NULL; + + for (int32_t i = 0; slot->args && i < argCount; i++) { + slot->args[i] = basValCopy(args[i]); + } + + slot->argCount = slot->args ? argCount : 0; + return true; +} + + +// True when this runs on a task other than the program's own, so the +// event (eventName; NULL: any) must wait for the program's turn: not one +// of a form's life (sQueueNever). +static bool queueForeign(const BasFormRtT *rt, const char *eventName) { + for (int32_t i = 0; eventName && i < DVX_ARRAY_LEN(sQueueNever); i++) { + if (strcasecmp(eventName, sQueueNever[i]) == 0) { + return false; + } + } + + return rt->ownTask != BAS_RT_ANY_TASK && basFormRtTaskId() != rt->ownTask; +} + + +// The runtime ends: its waiting events go unrun. +static void queueFree(BasFormRtT *rt) { + for (int32_t i = 0; i < (int32_t)arrlen(rt->queued); i++) { + queueArgsFree(&rt->queued[i]); + } + + sQueuedTotal -= (int32_t)arrlen(rt->queued); + arrfree(rt->queued); + rt->queued = NULL; + rt->queueHeld = false; +} + + +// A waiting event's form is still the program's and not unloading, and +// its object (if any) still one of the program's. A Timer stopped +// meanwhile fires no more (as in VB): its waiting tick goes too. +static bool queueLive(BasFormRtT *rt, const BasQueuedEventT *ev) { + BasFormT *form = resolveFormRef(rt, ev->form); + + if (!form || form != ev->form || form->detached) { + return false; + } + + if (!ev->obj) { + return true; + } + + if (resolveCtrlRef(rt, ev->obj) != ev->obj) { + return false; + } + + bool handled = false; + + if (strcasecmp(ev->eventName, BAS_EVT_TIMER) == 0 && ev->obj->iface && ev->obj->widget) { + BasValueT on = getIfaceProp(ev->obj->iface, ev->obj->widget, BAS_PROP_ENABLED, &handled); + + return !handled || basValIsTruthy(on); + } + + return true; } @@ -7952,119 +8790,6 @@ static void removeCtrlTree(BasFormRtT *rt, BasFormT *form, BasControlT *ctrl) { } -// ResAddData: a resource of any bytes (BINARY STRING); True on success. -int32_t ResAddData(const char *path, const char *name, int32_t type, const uint8_t *data, int32_t len) { - if (!path || !name || !data || len < 0) { - return 0; - } - - return dvxResAppend(path, name, (uint32_t)type, data, (uint32_t)len) == 0 ? -1 : 0; -} - - -int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile) { - if (!srcFile) { - return 0; - } - - int32_t fileSize = 0; - uint8_t *buf = (uint8_t *)platformReadFile(srcFile, &fileSize); - int32_t result = (buf && fileSize > 0) ? ResAddData(path, name, type, buf, fileSize) : 0; - - free(buf); - return result; -} - - -int32_t ResAddText(const char *path, const char *name, const char *text) { - if (!text) { - text = ""; - } - - return ResAddData(path, name, DVX_RES_TEXT, (const uint8_t *)text, (int32_t)strlen(text) + 1); -} - - -void ResClose(int32_t handle) { - int32_t idx = handle - 1; - - if (idx < 0 || idx >= RES_MAX_HANDLES || !sResHandles[idx]) { - return; - } - - dvxResClose(sResHandles[idx]); - sResHandles[idx] = NULL; -} - - -int32_t ResCount(int32_t handle) { - int32_t idx = handle - 1; - - if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx]) { - return (int32_t)sResHandles[idx]->entryCount; - } - - return 0; -} - - -int32_t ResExtract(const char *path, const char *name, const char *outFile) { - if (!outFile) { - return 0; - } - - uint32_t size = 0; - void *data = resReadNamed(path, name, &size); - - if (!data) { - return 0; - } - - FILE *f = fopen(outFile, "wb"); - - if (!f) { - free(data); - return 0; - } - - int32_t result = (fwrite(data, 1, size, f) == size) ? -1 : 0; - fclose(f); - free(data); - return result; -} - - -// ResGetData$: every byte of a resource (BINARY STRING); "" when it is -// not there. -const uint8_t *ResGetData(const char *path, const char *name, int32_t *outLen) { - uint32_t size = 0; - - free(sResData); - sResData = (uint8_t *)resReadNamed(path, name, &size); - *outLen = sResData ? (int32_t)size : 0; - return sResData; -} - - -const char *ResGetText(const char *path, const char *name) { - static char sBuf[BAS_RES_TEXT_LEN]; - uint32_t size = 0; - void *data = resReadNamed(path, name, &size); - - sBuf[0] = '\0'; - - if (data) { - uint32_t copyLen = DVX_MIN(size, (uint32_t)sizeof(sBuf) - 1); - - memcpy(sBuf, data, copyLen); - sBuf[copyLen] = '\0'; - free(data); - } - - return sBuf; -} - - // Widgets with a pixel buffer (Canvas, Image, PictureBox) expose // Resize(w, h) so the buffer can follow the layout size once Width and // Height are known. Runs after .frm parsing and on every runtime @@ -8092,18 +8817,6 @@ static void resizeIfaceBuffer(BasControlT *ctrl) { } -const char *ResName(int32_t handle, int32_t index) { - int32_t idx = handle - 1; - - if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] && - index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) { - return sResHandles[idx]->entries[index].name; - } - - return ""; -} - - // A control given as an argument: the control (an object) or its name on // form; NULL when it is neither. static BasControlT *resolveCtrlArg(BasFormRtT *rt, BasFormT *form, BasValueT arg) { @@ -8258,82 +8971,6 @@ static bool resolveShellIdle(void) { } -int32_t ResOpen(const char *path) { - if (!path) { - return 0; - } - - for (int32_t i = 0; i < RES_MAX_HANDLES; i++) { - if (!sResHandles[i]) { - sResHandles[i] = dvxResOpen(path); - - if (sResHandles[i]) { - return i + 1; - } - - return 0; - } - } - - return 0; -} - - -// A named resource of the file at path, read in one go (malloc'd, the -// caller frees); NULL when the file or the resource is not there. -static void *resReadNamed(const char *path, const char *name, uint32_t *outSize) { - *outSize = 0; - - if (!path || !name) { - return NULL; - } - - DvxResHandleT *h = dvxResOpen(path); - - if (!h) { - return NULL; - } - - void *data = dvxResRead(h, name, outSize); - - dvxResClose(h); - return data; -} - - -int32_t ResRemove(const char *path, const char *name) { - if (!path || !name) { - return 0; - } - - return dvxResRemove(path, name) == 0 ? -1 : 0; -} - - -int32_t ResSize(int32_t handle, int32_t index) { - int32_t idx = handle - 1; - - if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] && - index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) { - return (int32_t)sResHandles[idx]->entries[index].size; - } - - return 0; -} - - -int32_t ResType(int32_t handle, int32_t index) { - int32_t idx = handle - 1; - - if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] && - index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) { - return (int32_t)sResHandles[idx]->entries[index].type; - } - - return 0; -} - - // The single VM-entry wrapper for firing BASIC handler code. Owns, in // order: the prevForm/prevVars/prevVarCount save, the owning-form vars // binding, the rtEventEnter/rtEventLeave pairing, and the restore -- @@ -8482,6 +9119,13 @@ static const BasProcEntryT *rtFindProc(BasFormRtT *rt, const char *name) { } +// One pump for code that waits in a program (DoEvents' own): DVX and the +// program's waiting events run. False once the program is ending. +static bool rtPump(BasFormRtT *rt) { + return basFormRtUpdate(rt) && !rt->terminated; +} + + // Runs the VM in step slices, giving DVX a turn between them, until the // program halts or fails (an error is shown). Returns the last result. static BasVmResultE runSliced(BasFormRtT *rt) { @@ -8491,16 +9135,15 @@ static BasVmResultE runSliced(BasFormRtT *rt) { do { // This program's code runs (see rtCallHandler); the dvxUpdate // between slices may run other programs' handlers. - BasFormRtT *prevRt = sFormRt; + BasFormRtT *prevRt = curRtSet(rt); - sFormRt = rt; sCodeDepth++; - result = basVmRun(vm); + result = basVmRun(vm); sCodeDepth--; - sFormRt = prevRt; + curRtSet(prevRt); if (result == BAS_VM_STEP_LIMIT) { - if (!dvxUpdate(rt->ctx)) { + if (!basFormRtUpdate(rt)) { break; } } else if (result != BAS_VM_HALTED && result != BAS_VM_OK) { @@ -8704,7 +9347,7 @@ static void screenSetProp(BasFormRtT *rt, const char *propName, BasValueT value) void SerAttach(int32_t com, const char *termCtrlName) { - if (!serResolveApi() || !sFormRt || !termCtrlName) { + if (!serResolveApi() || !curRt() || !termCtrlName) { return; } @@ -8714,8 +9357,8 @@ void SerAttach(int32_t com, const char *termCtrlName) { return; } - // Find the terminal widget by control name - WidgetT *termWidget = findCtrlWidgetByName(termCtrlName); + // The Terminal control of that name on any loaded form + WidgetT *termWidget = findTermWidget(termCtrlName); if (!termWidget) { return; @@ -8732,6 +9375,7 @@ void SerAttach(int32_t com, const char *termCtrlName) { sSerAttach[idx].com = idx; sSerAttach[idx].term = termWidget; sSerAttach[idx].attached = true; + sSerAttach[idx].owner = curRt(); wgtAnsiTermSetComm(termWidget, &sSerAttach[idx], serTermRead, serTermWrite); @@ -8946,7 +9590,7 @@ int32_t SerLineErrors(int32_t com) { // A modem line of an open port as BASIC's True / False (False on an error // or a port that is not open). get points at the sSerApi field, read // after serResolveApi has filled it. -static int32_t serLineGet(int (*const *get)(int), int32_t com) { +static int32_t serLineGet(SerLineGetFnT *const *get, int32_t com) { if (!serResolveApi() || !*get) { return 0; } @@ -8957,7 +9601,7 @@ static int32_t serLineGet(int (*const *get)(int), int32_t com) { // Raise (on nonzero) or drop a modem line of an open port. The line keeps // its state when the port is closed. -static void serLineSet(int (*const *set)(int, bool), int32_t com, int32_t on) { +static void serLineSet(SerLineSetFnT *const *set, int32_t com, int32_t on) { if (!serResolveApi() || !*set) { return; } @@ -8994,6 +9638,7 @@ int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, // attached to a terminal. if (idx >= 0 && idx < RS232_NUM_PORTS) { sSerAttach[idx].opened = true; + sSerAttach[idx].owner = curRt(); } return SER_BASIC_TRUE; @@ -9023,7 +9668,7 @@ static bool serResolveApi(void) { return sSerApi.open != NULL; } - sSerApiResolved = true; + sSerApiResolved = true; sSerApi.open = dlsym(NULL, DVX_SYM("rs232Open")); sSerApi.close = dlsym(NULL, DVX_SYM("rs232Close")); sSerApi.read = dlsym(NULL, DVX_SYM("rs232Read")); @@ -9126,8 +9771,9 @@ static int32_t serTermWrite(void *ctx, const uint8_t *data, int32_t len) { return 0; } + // What the port's buffer took (the terminal sends the rest later) int32_t rc = sSerApi.writeBuf(sa->com, (const char *)data, len); - return (rc >= 0) ? len : 0; + return (rc >= 0) ? rc : 0; } @@ -9191,7 +9837,7 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val resizeIfaceBuffer(ctrl); if (w->minW != old) { - firePaintResized(sFormRt, ctrl); + firePaintResized(curRt(), ctrl); } break; @@ -9206,7 +9852,7 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val resizeIfaceBuffer(ctrl); if (w->minH != old) { - firePaintResized(sFormRt, ctrl); + firePaintResized(curRt(), ctrl); } break; @@ -9256,7 +9902,7 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val case CTRL_PROP_FONTUNDERLINE: case CTRL_PROP_FONTSTRIKETHRU: if (!fontPropSet(w, pd->name, value)) { - basFormRtRuntimeError(sFormRt, "Out of memory", "Control: %s\nProperty: %s\nThere is not enough memory for the font.", ctrlDisplayName(ctrl), pd->name); + basFormRtRuntimeError(curRt(), "Out of memory", "Control: %s\nProperty: %s\nThere is not enough memory for the font.", ctrlDisplayName(ctrl), pd->name); } break; @@ -9311,12 +9957,12 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val break; case CTRL_PROP_MOUSEPOINTER: - setMousePointer(sFormRt, &ctrl->mousePointer, value); + setMousePointer(curRt(), &ctrl->mousePointer, value); break; case CTRL_PROP_CONTAINER: // A failed move has reported its own error - basFormRtMoveControl(sFormRt, ctrl, (value.type == BAS_TYPE_OBJECT) ? value.objVal : NULL, NULL); + basFormRtMoveControl(curRt(), ctrl, (value.type == BAS_TYPE_OBJECT) ? value.objVal : NULL, NULL); break; case CTRL_PROP_NAME: @@ -9339,6 +9985,26 @@ static void setControlString(char *dst, int32_t dstSize, BasValueT value) { } +// FullScreen = on: the form's window takes the whole screen, showing its +// own content (dvxExclusiveMirror; a hidden form is shown first), or +// gives it back. When there is no video mode for it the form stays in +// its window (FullScreen reads False). +static void setFormFullScreen(BasFormRtT *rt, BasFormT *frm, bool on) { + if (!on) { + dvxExclusiveEnd(rt->ctx, frm->window); + return; + } + + if (!frm->window->visible) { + basFormRtShowForm(rt, frm, false); + } + + int32_t rc = dvxExclusiveMirror(rt->ctx, frm->window, formFullChanged, rt); + + dvxLog("basrt: form %s full screen %s", frm->name, rc == DVX_EXCL_OK ? "on" : "unavailable"); +} + + // Form.Icon = file$ or LoadPicture(file$): the picture of the minimized // window ("" or LoadPicture() puts DVX's back). static void setFormIcon(BasFormRtT *rt, BasFormT *frm, BasValueT value) { @@ -9364,6 +10030,24 @@ static void setFormIcon(BasFormRtT *rt, BasFormT *frm, BasValueT value) { } +// MaxWidth (across) or MaxHeight = limit: how far the form's window can +// be resized or maximized (FORM_NO_MAX or less: the screen's size); a +// window larger than the new limit shrinks to it. +static void setFormMax(BasFormRtT *rt, WindowT *win, bool across, int32_t limit) { + int32_t max = (limit > FORM_NO_MAX) ? limit : WM_MAX_FROM_SCREEN; + + if (across) { + win->maxW = max; + } else { + win->maxH = max; + } + + if (limit > FORM_NO_MAX && (across ? win->w : win->h) > limit) { + dvxResizeWindow(rt->ctx, win, across ? limit : win->w, across ? win->h : limit); + } +} + + // FormName.Prop = value: window geometry/state plus the BasFormT flags. static void setFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName, BasValueT value) { WindowT *win = frm->window; @@ -9513,6 +10197,25 @@ static void setFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName, Bas setCommonProp(&frm->formCtrl, pd->name, value); break; + case FORM_PROP_MAXWIDTH: + case FORM_PROP_MAXHEIGHT: + if (hasWin) { + setFormMax(rt, win, pd == &sFormProps[FORM_PROP_MAXWIDTH], num); + } + break; + + case FORM_PROP_GAMEINPUT: + if (hasWin) { + win->gameInput = flag; + } + break; + + case FORM_PROP_FULLSCREEN: + if (hasWin) { + setFormFullScreen(rt, frm, flag); + } + break; + case FORM_PROP_NAME: case FORM_PROP_LAYOUT: case FORM_PROP_CONTROLS: @@ -9711,6 +10414,14 @@ static void setPropValue(BasFormRtT *rt, void *ctrlRef, const char *propName, Ba return; } + // A LoadPictureData picture for Picture, DragIcon, or a form's Icon + BasPictureT *bytesPic = pictureTakesData(ctrl, propName) ? basFormRtPictureData(rt, value) : NULL; + + if (bytesPic) { + pictureShowData(rt, ctrl, propName, bytesPic); + return; + } + // Drawing properties of forms and PictureBoxes if (basFormRtDrawSetProp(rt, ctrl, propName, value)) { return; @@ -9881,16 +10592,18 @@ int32_t SQLAffected(int32_t db) { // The SQL callback table of the running VM, or an all-NULL table when // there is none, so each SQL* wrapper checks just its own entry. A VM -// whose host bound no SQL callbacks (a compiled program's stub; only the -// IDE binds them) gets the dvxsql library's functions by name. +// whose host bound no SQL callbacks (basVmSetSqlCallbacks: no host does, +// only the tests) gets the dvxsql library's functions by name. static const BasSqlCallbacksT *sqlApi(void) { static const BasSqlCallbacksT sNoSql; - if (!sFormRt || !sFormRt->vm) { + BasFormRtT *rt = curRt(); + + if (!rt || !rt->vm) { return &sNoSql; } - BasSqlCallbacksT *sql = &sFormRt->vm->sql; + BasSqlCallbacksT *sql = &rt->vm->sql; if (!sql->sqlOpen) { sqlResolveApi(sql); @@ -9934,6 +10647,25 @@ const char *SQLError(int32_t db) { } +// SQLEscape$(text$): text with every ' doubled (dvxSqlEscape), to put +// between single quotes in a statement. The text stays valid until the +// next call. +const char *SQLEscape(const char *text) { + static char *out = NULL; // stb_ds array, reused + + const BasSqlCallbacksT *api = sqlApi(); + int32_t len = text ? (int32_t)strlen(text) : 0; + + if (!api->sqlEscape) { + return ""; + } + + // At most every character doubled, and the NUL + arrsetlen(out, len * BAS_SQL_ESC_GROWTH + 1); + return api->sqlEscape(text ? text : "", out, (int32_t)arrlen(out)) < 0 ? "" : out; +} + + int32_t SQLExec(int32_t db, const char *sql) { const BasSqlCallbacksT *api = sqlApi(); @@ -10036,6 +10768,7 @@ int32_t SQLNext(int32_t rs) { } +// A program's database pumps while a statement runs long (sqlPump). int32_t SQLOpen(const char *path) { const BasSqlCallbacksT *api = sqlApi(); @@ -10043,7 +10776,37 @@ int32_t SQLOpen(const char *path) { return 0; } - return api->sqlOpen(path); + int32_t db = api->sqlOpen(path); + + if (db > 0 && api->sqlSetPump) { + api->sqlSetPump(db, sqlPump); + } + + return db; +} + + +// The pump of a program's databases (dvxSqlSetPump): a statement running +// into a new BIOS tick (~55 ms) lets DVX and the program's events run, as +// DoEvents does (not in a control file's code, which must not wait), and +// is stopped when the program ends. +static bool sqlPump(int32_t db) { + BasFormRtT *rt = curRt(); + + (void)db; + + if (!rt) { + return true; + } + + uint32_t now = *platformBiosTicks(); + + if (rt->terminated || now == sSqlPumpTick || rt->vm->runLimit > 0) { + return !rt->terminated; + } + + sSqlPumpTick = now; + return rtPump(rt); } @@ -10075,6 +10838,8 @@ static void sqlResolveApi(BasSqlCallbacksT *sql) { sql->sqlFieldDbl = dlsym(NULL, DVX_SYM("dvxSqlFieldDbl")); sql->sqlFreeResult = dlsym(NULL, DVX_SYM("dvxSqlFreeResult")); sql->sqlAffectedRows = dlsym(NULL, DVX_SYM("dvxSqlAffectedRows")); + sql->sqlSetPump = dlsym(NULL, DVX_SYM("dvxSqlSetPump")); + sql->sqlEscape = dlsym(NULL, DVX_SYM("dvxSqlEscape")); } @@ -10189,6 +10954,22 @@ static void vmErrorEnd(BasFormRtT *rt) { } +// The wait pump (dvxSetWaitPump): a library waiting in a program's code +// (SoundPlayEvent with wait, say) pumps as DoEvents does; the wait stops +// when the program ends, and a control file's code does not wait. Code +// that is no program's (the shell's) only yields. +static bool waitPump(void) { + BasFormRtT *rt = curRt(); + + if (!rt) { + platformYield(); + return true; + } + + return rt->vm->runLimit <= 0 && rtPump(rt); +} + + // A BASIC value as a widget value of type want, with the BASIC // conversions (basValToNumber, basValIsTruthy, basValFormatString). // WGT_IFACE_NONE keeps the value's own kind (a dynamic method's @@ -10228,7 +11009,7 @@ static WgtValueT wgtFromBas(BasValueT v, uint8_t want, BasStringT **hold) { switch (want) { case WGT_IFACE_STRING: *hold = basValFormatString(v); - return wgtValueStr((*hold)->data); + return wgtValueBytes((*hold)->data, (*hold)->len); case WGT_IFACE_BOOL: return wgtValueBool(basValIsTruthy(v)); diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.h b/src/apps/kpunch/dvxbasic/formrt/formrt.h index 0e4e221f..c51ed6a3 100644 --- a/src/apps/kpunch/dvxbasic/formrt/formrt.h +++ b/src/apps/kpunch/dvxbasic/formrt/formrt.h @@ -61,7 +61,7 @@ typedef struct BasScreenWinT BasScreenWinT; // the program screen's window (f // (vm.h) with the compiler, so name-keyed lookups never truncate. // Longest menu Shortcut / ShortcutText ("Ctrl+Alt+Shift+Backspace"), and // what separates it from the caption in an item's label ("&Open\tCtrl+O"). -#define BAS_MENU_KEY_TEXT_MAX 32 +#define BAS_MENU_KEY_TEXT_MAX DVX_ACCEL_TEXT_MAX #define BAS_MENU_KEY_SEP '\t' // Window size for a form that gives no Width/Height; the IDE designer @@ -115,6 +115,8 @@ typedef struct BasScreenWinT BasScreenWinT; // the program screen's window (f #define BAS_PROP_FONTSTRIKETHRU "FontStrikethru" #define BAS_PROP_FONTUNDERLINE "FontUnderline" #define BAS_PROP_FORECOLOR "ForeColor" +#define BAS_PROP_FULLSCREEN "FullScreen" +#define BAS_PROP_GAMEINPUT "GameInput" #define BAS_PROP_HEIGHT "Height" #define BAS_PROP_HELPTOPIC "HelpTopic" #define BAS_PROP_ICON BAS_FORM_ICON @@ -189,6 +191,10 @@ const BasPropDescT *basFormRtFindCommonProp(const char *name); const BasPropDescT *basFormRtFindFormProp(const char *name); const BasPropDescT *basFormRtFindMenuProp(const char *name); +// True for a form property that is run-time state, never part of a .frm +// (Visible, DesignMode, FullScreen ...; the object-typed ones aside). +bool basFormRtFormPropRuntimeOnly(const char *name); + // ============================================================ // Font properties // ============================================================ @@ -409,6 +415,28 @@ typedef struct { char handlerName[BAS_MAX_IDENT]; // e.g. "HandleOkClick" } BasEventOverrideT; +// A LoadPicture or LoadPictureData Picture object: the picture's size in +// pixels (0 x 0 for the empty picture), and its file name ("" for the +// empty picture) or, from LoadPictureData, the bytes of its picture file +// (data, dataLen bytes and a NUL after them; NULL for a file's picture). +// A property showing one from bytes holds its name instead of a file's: +// BAS_PICTURE_DATA_MARK and its place in BasFormRtT.pictures. +#define BAS_PICTURE_DATA_MARK '*' + +// True when a picture property's text names a LoadPictureData picture +// rather than a file (no file name starts with the mark). +static inline bool basPictureIsData(const char *path) { + return path[0] == BAS_PICTURE_DATA_MARK; +} + +typedef struct { + int32_t width; + int32_t height; + uint8_t *data; + int32_t dataLen; + char path[]; +} BasPictureT; + typedef struct BasControlT { char name[BAS_MAX_IDENT]; // VB control name (e.g. "Command1") char typeName[BAS_MAX_IDENT]; // VB type name (e.g. "CommandButton") @@ -524,13 +552,47 @@ typedef struct { int32_t frmSourceLen; } BasFrmCacheT; -// A LoadPicture Picture object: the picture's size in pixels (0 x 0 for -// the empty picture) and its file name ("" for the empty picture). +// A picture PaintPicture decoded, kept for the next call (formGfx's +// drawPictureLoad): its pixels in the display format then, and the file's +// size and time when it was read -- or the LoadPictureData picture it is +// (picture; path ""). typedef struct { - int32_t width; - int32_t height; - char path[]; -} BasPictureT; + uint8_t *pixels; + const BasPictureT *picture; + int64_t size; + int64_t time; + int32_t w; + int32_t h; + int32_t pitch; + int32_t bpp; + char path[DVX_MAX_PATH]; +} BasDrawPicT; + +// The task of a runtime whose code runs on any task (BasFormRtT.ownTask): +// an installed control file's, shared by every program. +#define BAS_RT_ANY_TASK UINT32_MAX + +// What a waiting event is: an event, a control's event (it runs through +// the control-event path: Index argument, re-entry guard), or a form's +// window closed (the form is unloaded, QueryUnload asked first). +typedef enum { + BasQueuedEventE, + BasQueuedCtrlEventE, + BasQueuedCloseE +} BasQueuedKindE; + +// An event another task dispatched for a program, kept until the +// program's own task runs it (BasFormRtT.queued): what was fired, the +// arguments copied. +typedef struct { + BasFormT *form; + BasControlT *obj; // the control, form object, or menu proxy (NULL: none) + BasValueT *args; // copies (NULL: none), released once it has run + int32_t argCount; + BasQueuedKindE kind; + char ctrlName[BAS_MAX_IDENT]; + char eventName[BAS_MAX_IDENT]; +} BasQueuedEventT; typedef struct { AppContextT *ctx; // DVX app context @@ -545,6 +607,9 @@ typedef struct { // loop exits at the next pump so the app doesn't stumble forward // with a halted VM. bool terminated; + // basFormRtLoadAllForms is loading the program's forms (the other + // tasks run between them): it counts as busy (basFormRtBusy). + bool formsLoading; // Set true by hosts that want to handle runtime-error reporting // themselves (the IDE shows the error in the output pane and // navigates the editor to the line). When true, @@ -587,6 +652,9 @@ typedef struct { GameFramePacerT framePacer; int32_t frameHz; uint64_t frameDueUs; + // SHELL: the program waiting for its DOS run, and how the run ended + int32_t shellAppId; + int32_t shellRc; // The program screen's window (made by the first console or graphics // statement) and its caption: hosts set title to the program's name. BasScreenWinT *screenWin; @@ -595,6 +663,9 @@ typedef struct { // (stb_ds array of heap objects; the object is that pointer), kept // until the runtime is destroyed. BasPictureT **pictures; + // PaintPicture's decoded picture files (stb_ds), and their bytes + BasDrawPicT *drawPics; + int32_t drawPicsBytes; // Screen.MousePointer = vbHourglass is in force: the busy cursor is // put back to the arrow when the runtime is destroyed. bool pointerBusy; @@ -608,6 +679,18 @@ typedef struct { int32_t screenPointer; // The Printer object's pages and document (NULL until it is used). BasPrinterT *printer; + // A program's handlers run on its own task (the one that made the + // runtime; BAS_RT_ANY_TASK: any). An event another task dispatched + // (the shell's frame takes the input), and a form's window closed + // there, wait in queued (stb_ds, oldest first) for the program's next + // turn (basFormRtUpdate) -- except an event whose handler answers + // through its arguments (Cancel) and Load / QueryUnload / Unload, + // which run where they are fired. While any wait, queueHeld holds one + // eventDepth bracket so the forms and controls they name are not + // freed. + uint32_t ownTask; + BasQueuedEventT *queued; + bool queueHeld; } BasFormRtT; // ============================================================ @@ -625,6 +708,38 @@ void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName); // Called after bytecode halts to keep the app alive for event handlers. void basFormRtEventLoop(BasFormRtT *rt); +// One turn of a program's event loop on its own task: the events another +// task dispatched for it run first (in order), then a DVX frame. Every +// pump of a running program (the stub's DoEvents, the IDE's, the +// runtime's own loops) is one. False when DVX is shutting down. +bool basFormRtUpdate(BasFormRtT *rt); + +// The running task (the task switcher's, found at run time; 0 without +// one). +uint32_t basFormRtTaskId(void); + +// Let the other tasks run once (no event of this program runs): between +// the steps of a long call of the program's (starting it, a compile). +void basFormRtYield(void); + +// rt's code runs on the calling task from now on (it was made on another: +// the IDE makes a program's runtime in a menu command, then runs it on a +// task of its own): the events other tasks dispatch for it wait for its +// turn here, and it is this task's current runtime. +void basFormRtAdopt(BasFormRtT *rt); + +// True while any program's BASIC code is running (a handler waiting in +// DoEvents or a dialog box included), has events waiting for its turn +// (its UserControls' too), or is loading its forms at its start: a host +// that dispatched an event and waits for the program to be done with it +// (a test driving a program) waits until this is false. +bool basFormRtBusy(void); + +// True while a handler of rt's (or a native bridge holding its forms and +// controls) is on some stack: its eventDepth beyond the bracket its +// waiting events hold. +bool basFormRtCodeOnStack(const BasFormRtT *rt); + // Simple non-debug execution: run bytecode in slices with dvxUpdate // between slices, then enter the event loop. Used by the standalone stub. void basFormRtRunSimple(BasFormRtT *rt); @@ -658,9 +773,11 @@ WindowT *basFormRtScreenWindow(const BasFormRtT *rt); // AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight). bool basFormRtIsDrawMember(const char *name, bool isProp); -// Close all serial/secLink resources and unregister the idle pollers. -// Called by basFormRtDestroy and by the stub's _appShutdown force-kill hook. -void basFormRtSerialShutdown(void); +// Close the serial ports and secure links (comm.bas) the program rt +// opened, NULL every one, and drop the idle pollers once none is left. +// basFormRtDestroy calls it for its program (a force kill reaches that +// through basRunHost's kill hook), so other programs' stay open. +void basFormRtSerialShutdown(const BasFormRtT *rt); #ifdef DVX_HOST_TEST // Test-only: clear every file-scope static so a host test process can run @@ -955,6 +1072,15 @@ bool basUcIsControlType(const char *typeName); // host's design controls. NULL when it has none. const WgtIfaceT *basUcOwnIface(const BasFormRtT *rt, const char *typeName); +// A UserControl has events from another task waiting for its program's +// turn (basFormRtBusy counts them). +bool basUcBusy(void); + +// A program's turn on its own task (basFormRtUpdate): its UserControls' +// events that came on other tasks (their code runs on its program's task +// only), then their Resize and Paint. +void basUcPump(void); + // The runtime is being destroyed: its UserControl classes and the ones it // registered for design are unregistered (their widgets are gone with the // forms). @@ -1046,15 +1172,21 @@ void basFormRtDrawClear(BasFormRtT *rt, BasControlT *ctrl); // Free a control's drawing state. void basFormRtDrawFree(BasControlT *ctrl); +// Free PaintPicture's kept picture files (the runtime is ending). +void basFormRtDrawPicsFree(BasFormRtT *rt); + // The object is about to be repainted (shown, resized, Refresh): true // when its Paint event should fire -- a form or PictureBox whose // AutoRedraw is not True. One whose program set AutoRedraw = False is // cleared to its BackColor first (VB); by default drawings are kept. bool basFormRtDrawWantsPaint(BasControlT *ctrl); +// The Picture object LOADPICTUREDATA made that v is, else NULL. +BasPictureT *basFormRtPictureData(const BasFormRtT *rt, BasValueT v); + // The file name of a Picture object LOADPICTURE made ("" for an empty // picture), NULL when v is not one. Picture properties and PaintPicture -// take either a file name or such an object. +// take a file name or a Picture object of either kind. const char *basFormRtPicturePath(const BasFormRtT *rt, BasValueT v); // The live control an object reference names, or NULL. @@ -1082,6 +1214,11 @@ void basFormRtPrinterSetProp(BasFormRtT *rt, const char *name, BasValueT va // and the Printer's state freed. void basFormRtPrinterEnd(BasFormRtT *rt); +// The program was ended by force: a page drawn is thrown away and a +// document open abandoned (KillDoc) -- nothing is printed in the kill -- +// and the Printer's state freed. +void basFormRtPrinterKill(BasFormRtT *rt); + // ============================================================ // Between formrt.c and formDesign.c // ============================================================ diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c index 5c0cad31..e39dff4c 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c +++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c @@ -46,6 +46,7 @@ // ============================================================ #define MIN_CTRL_SIZE 8 +#define EMPTY_PICTURE_SIZE 32 // a new Image or ImageButton's size: it has none until it has a picture #define DSGN_NO_SELECTION (-1) // selectedIdx: the form itself // ============================================================ @@ -114,18 +115,12 @@ static WidgetT *containerChildParent(FrmCtrlT *pc) { // Create a real DVX widget for design-time display through libdvx's // wgtCreateByName (the project's own UserControls first, the designer's -// registration of them: basUcResolveType); design-time refuses -// WGT_CREATE_PARENT_DATA widgets (Image/ImageButton) since we have no -// pixel data. +// registration of them: basUcResolveType). An Image or ImageButton starts +// with an empty picture; its Picture property fills it. static WidgetT *createDesignWidget(const char *vbTypeName, WidgetT *parent) { - const char *wgtName = basUcResolveType(NULL, vbTypeName); - const WgtIfaceT *iface = wgtGetIface(wgtName); + const char *wgtName = basUcResolveType(NULL, vbTypeName); - if (!wgtName || (iface && iface->createSig == WGT_CREATE_PARENT_DATA)) { - return NULL; - } - - return wgtCreateByName(wgtName, parent); + return wgtName ? wgtCreateByName(wgtName, parent) : NULL; } @@ -534,6 +529,15 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) { return; } + // A control made with an empty picture (Image, ImageButton) has no size + // of its own until it has one: a size to see and select it by + const WgtIfaceT *placed = basCtlIface(cp->typeName); + + if (placed && placed->createSig == WGT_CREATE_PARENT_DATA) { + cp->width = EMPTY_PICTURE_SIZE; + cp->height = EMPTY_PICTURE_SIZE; + } + if (parentWidget) { dsgnCreateControlWidget(ds->form, cp, parentWidget); } diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h index ed2a8089..e4c8966c 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h +++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h @@ -36,7 +36,7 @@ #include "dvxWgt.h" #include "../compiler/lexer.h" #include "../formrt/formrt.h" -#include "../formrt/frmModel.h" +#include "../basdev/frmModel.h" #include "stb_ds_wrap.h" diff --git a/src/apps/kpunch/dvxbasic/ide/ideMain.c b/src/apps/kpunch/dvxbasic/ide/ideMain.c index 110cb79a..d11bec6b 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideMain.c +++ b/src/apps/kpunch/dvxbasic/ide/ideMain.c @@ -92,26 +92,28 @@ // Constants // ============================================================ -#define IDE_MAX_SOURCE PRJ_MAX_SOURCE // editor capacity and largest loadable project file -#define IDE_MAX_OUTPUT 32768 -#define IDE_SOURCE_MARGIN 64 -#define IDE_FULL_NAME_BUF (BAS_MAX_IDENT * 2) // "Obj.Evt" / "Obj_Evt" combined names -#define IDE_TITLE_BUF (PRJ_MAX_NAME + 32) // "DVX BASIC - [name] *" -#define IDE_FORM_TITLE_BUF (FRM_MAX_TEXT + 32) // "caption [Design] *" -#define IDE_MSG_BUF (DVX_MAX_PATH + 32) // path plus short message -#define IDE_REPORT_BUF (DVX_MAX_PATH + BAS_MAX_IDENT * 4) // runtime error report -#define IDE_PREF_KEY_BUF 32 // "colorN" style preference keys -#define IDE_IMM_LINE_BUF 512 // one Immediate-window line -#define IDE_SKELETON_BUF 512 // generated "Sub ... End Sub" skeleton -#define IDE_EVENT_PARAMS_BUF 256 // an Event declaration's parameter list text -#define IDE_RENAME_TITLE "Rename File" -#define IDE_VALUE_BUF 256 // formatted variable value / watch text -#define IDE_FIND_TEXT_BUF 256 // find / replace text -#define IDE_LINE_NUM_BUF 16 // decimal line number text -#define IDE_RGB_LABEL_BUF 16 // "Green 255" -#define IDE_TAB_WIDTH_BUF 8 // tab width text input -#define IDE_OUTPUT_LINE_RESERVE 256 // headroom kept in sOutputBuf per error line -#define IDE_LOG_LINE_BUF 256 // one pending PRINT line for Output-to-log +#define IDE_MAX_SOURCE PRJ_MAX_SOURCE // editor capacity and largest loadable project file +#define IDE_MAX_OUTPUT 32768 +#define IDE_RUN_TASK_NAME "DVX BASIC program" // the task a run's program runs on (runModule) +#define IDE_BUSY_PUMP_MS 50 // a compile, a debugger snippet, or a Replace All lets DVX go on this often (ideBusyPump) +#define IDE_SOURCE_MARGIN 64 +#define IDE_FULL_NAME_BUF (BAS_MAX_IDENT * 2) // "Obj.Evt" / "Obj_Evt" combined names +#define IDE_TITLE_BUF (PRJ_MAX_NAME + 32) // "DVX BASIC - [name] *" +#define IDE_FORM_TITLE_BUF (FRM_MAX_TEXT + 32) // "caption [Design] *" +#define IDE_MSG_BUF (DVX_MAX_PATH + 32) // path plus short message +#define IDE_REPORT_BUF (DVX_MAX_PATH + BAS_MAX_IDENT * 4) // runtime error report +#define IDE_PREF_KEY_BUF 32 // "colorN" style preference keys +#define IDE_IMM_LINE_BUF 512 // one Immediate-window line +#define IDE_SKELETON_BUF 512 // generated "Sub ... End Sub" skeleton +#define IDE_EVENT_PARAMS_BUF 256 // an Event declaration's parameter list text +#define IDE_RENAME_TITLE "Rename File" +#define IDE_VALUE_BUF 256 // formatted variable value / watch text +#define IDE_FIND_TEXT_BUF 256 // find / replace text +#define IDE_LINE_NUM_BUF 16 // decimal line number text +#define IDE_RGB_LABEL_BUF 16 // "Green 255" +#define IDE_TAB_WIDTH_BUF 8 // tab width text input +#define IDE_OUTPUT_LINE_RESERVE 256 // headroom kept in sOutputBuf per error line +#define IDE_LOG_LINE_BUF 256 // one pending PRINT line for Output-to-log #define IDE_MAIN_TITLE "DVX BASIC" #define IDE_GENERAL_SECTION "(General)" @@ -301,9 +303,23 @@ typedef enum { DbgIdleE, // no program loaded DbgRunningE, // program executing DbgPausedE, // stopped at breakpoint/step - DbgStoppingE // Stop requested; runModule has not unwound yet + DbgStoppingE // Stop requested; runBody has not unwound yet } IdeDebugStateE; +// shellAppTaskStart, found at run time (the host tests have no shell) +typedef __typeof__(shellAppTaskStart) *IdeTaskStartFnT; + +// The run in progress (runStart, runBody): the IDE windows it hid, and +// the owner of the fonts it added +typedef struct { + bool hadFormWin; + bool hadToolbox; + bool hadProps; + bool hadCodeWin; + bool hadPrjWin; + int32_t fontOwner; +} IdeRunT; + typedef struct { int32_t fileIdx; // project file index int32_t codeLine; // line within file's code section (1-based) @@ -480,6 +496,8 @@ static bool hasProject(void); static bool hasUnsavedData(void); static void helpQueryHandler(void *ctx); static void helpSetCtrlTopic(const char *typeName); +static void ideBusyPump(void *ctx); +static void ideBusyPumpEnd(void); static void ideErrorBox(const char *msg); static void idePump(void); static void immPrintCallback(void *ctx, const char *text, bool newline); @@ -609,10 +627,13 @@ static void renameProjectFile(int32_t fileIdx); static bool renameProjectFileTo(int32_t fileIdx, const char *newName); static int32_t replaceInSections(const char *objFilter, bool caseSensitive); static void reportUnreadable(const char *path); +static void runBody(void); static void runCached(void); static void runFontsAdd(int32_t owner); static void runModule(BasModuleT *mod); static BasVmResultE runProgramSlices(BasFormRtT *rt); +static bool runStart(BasModuleT *mod); +static void runTaskEntry(void *arg); static bool saveActiveFile(void); static bool saveAllModified(void); static bool saveCurProc(bool discardSkeleton); @@ -707,6 +728,12 @@ static WindowT *sLastFocusWin = NULL; // last focused non-toolbar window static char sOutputBuf[IDE_MAX_OUTPUT]; static int32_t sOutputLen = 0; +static PlatformTicksT sBusyPumpAt = 0; // when ideBusyPump last let DVX go on +static bool sBusyPumpBusy = false; // ideBusyPump set the busy state (ideBusyPumpEnd clears it) +static const BasDbgPumpT sBusyPump = { ideBusyPump, NULL }; // the debugger's snippets pump through it + +static IdeRunT sRun; + // Procedure view state -- the editor shows one procedure at a time. // Each procedure is stored in its own malloc'd buffer. The editor // swaps directly between buffers with no splicing needed. @@ -1638,10 +1665,10 @@ static bool compileProject(void) { IdeConcatE concat = concatProject(&project, &validatorCtx, true, &sProject.sourceMap, &failIdx); if (concat == ConcatUnreadableE) { - sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nCannot read %s (missing or larger than %d bytes).\n", sProject.files[failIdx].path, (int)IDE_MAX_SOURCE); + sOutputLen = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nCannot read %s (missing or larger than %d bytes).\n", sProject.files[failIdx].path, (int)IDE_MAX_SOURCE); showCompileError("Compilation failed: cannot read file."); } else if (concat == ConcatTooLargeE) { - sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nProject source exceeds %d bytes at %s.\n", (int)IDE_MAX_SOURCE, sProject.files[failIdx].path); + sOutputLen = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nProject source exceeds %d bytes at %s.\n", (int)IDE_MAX_SOURCE, sProject.files[failIdx].path); showCompileError("Compilation failed: source too large."); } else if (concat == ConcatFailedE) { setStatus(project.error); @@ -1674,14 +1701,14 @@ static bool compileProject(void) { // DXE. Those would silently drop the control at runtime and // produce a misleading "control not found" error on first access. if (arrlen(validatorCtx.badTypes) > 0) { - int32_t n = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nUnknown widget type(s) in .frm files:\n"); + int32_t n = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nUnknown widget type(s) in .frm files:\n"); for (int32_t i = 0; i < (int32_t)arrlen(validatorCtx.badTypes) && n < IDE_MAX_OUTPUT - IDE_OUTPUT_LINE_RESERVE; i++) { BasBadTypeT *b = &validatorCtx.badTypes[i]; - n = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, n, " " BAS_VALIDATE_BAD_TYPE_FMT "\n", b->formName, b->ctrlName, b->typeName); + n = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, n, " " BAS_VALIDATE_BAD_TYPE_FMT "\n", b->formName, b->ctrlName, b->typeName); } - n = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, n, "\n" BAS_VALIDATE_BAD_TYPE_HINT "\n"); + n = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, n, "\n" BAS_VALIDATE_BAD_TYPE_HINT "\n"); sOutputLen = n; showCompileError("Compilation failed: unknown widget type."); basBuildFreeProject(&project); @@ -1697,7 +1724,7 @@ static bool compileProject(void) { basValidateCallbacks(&validatorCtx, &validator); - BasModuleT *mod = basBuildCompile(src, srcLen, sProject.optionExplicit, &validator, &validatorCtx.compiler, &err); + BasModuleT *mod = basBuildCompile(src, srcLen, sProject.optionExplicit, &validator, &validatorCtx.compiler, &err, ideBusyPump, NULL); basBuildFreeProject(&project); basValidateFree(&validatorCtx); @@ -1748,9 +1775,9 @@ static bool compileProject(void) { const char *msg = basBuildErrorText(err.message); if (procName[0]) { - sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", procName, (int)procLine, msg); + sOutputLen = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", procName, (int)procLine, msg); } else { - sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", errFile, (int)errLocalLine, msg); + sOutputLen = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", errFile, (int)errLocalLine, msg); } showCompileError("Compilation failed."); @@ -2027,8 +2054,8 @@ static void debugStartOrResume(int32_t cmd) { } -// debugStop -- request the running program to stop. runModule's loops and -// doEventsCallback observe sStopRequested and unwind; until runModule has +// debugStop -- request the running program to stop. runBody's loops and +// doEventsCallback observe sStopRequested and unwind; until runBody has // returned the state stays DbgStoppingE, so nothing can start a new run // or free the module the unwinding VM is still executing. static void debugStop(void) { @@ -2136,7 +2163,7 @@ static void designCtlRefresh(void) { if (count > 0 && concatProject(&ps, NULL, false, &map, &failIdx) == ConcatOkE) { BasCompileErrorT err; - module = basBuildCompile(ps.source, ps.sourceLen, sProject.optionExplicit, NULL, NULL, &err); + module = basBuildCompile(ps.source, ps.sourceLen, sProject.optionExplicit, NULL, NULL, &err, NULL, NULL); } basBuildFreeProject(&ps); @@ -2560,15 +2587,15 @@ static void ensureProjectWindow(bool raise) { } -// evaluateImmediate -- compile and execute a single line from the -// Immediate window. If the line doesn't start with a statement keyword, -// wrap it in PRINT so expressions produce visible output. - // evaluateImmediate -- run a line typed into the Immediate window (an // assignment to the paused program's variable, a statement, or an // expression to print); an assignment refreshes the debug windows. static void evaluateImmediate(const char *expr) { - if (basDbgExec(sDbgState == DbgPausedE ? sVm : NULL, expr, immPrintCallback, NULL)) { + bool assigned = basDbgExec(sDbgState == DbgPausedE ? sVm : NULL, expr, immPrintCallback, NULL, &sBusyPump); + + ideBusyPumpEnd(); + + if (assigned) { updateLocalsWindow(); updateWatchWindow(); } @@ -3570,6 +3597,39 @@ static void helpSetCtrlTopic(const char *typeName) { } +// ideBusyPump -- basBuildCompile's unit callback, the debugger's snippet +// pump (sBusyPump), and Replace All's between procedures: a frame every +// IDE_BUSY_PUMP_MS, so DVX goes on through a long compile, snippet, or +// replace. The busy state keeps the user's input out meanwhile; set here +// when the caller had not (ideBusyPumpEnd clears it). +static void ideBusyPump(void *ctx) { + (void)ctx; + + if (platformClock() - sBusyPumpAt < PLATFORM_MS_TO_TICKS(IDE_BUSY_PUMP_MS)) { + return; + } + + if (!sAc->busy) { + dvxSetBusy(sAc, true); + sBusyPumpBusy = true; + } + + dvxUpdate(sAc); + sBusyPumpAt = platformClock(); +} + + +// ideBusyPumpEnd -- after a step ideBusyPump may have pumped: the busy +// state it set goes. +static void ideBusyPumpEnd(void) { + if (sBusyPumpBusy) { + dvxSetBusy(sAc, false); + } + + sBusyPumpBusy = false; +} + + // ideErrorBox -- modal error message. The busy state swallows all input, // so it is cleared first or the box could never be dismissed. static void ideErrorBox(const char *msg) { @@ -3579,10 +3639,15 @@ static void ideErrorBox(const char *msg) { // idePump -- one pass of the GUI event loop while a program runs, with -// its buffered output shown first +// its buffered output shown first (and the events waiting for it run) static void idePump(void) { flushOutput(); - dvxUpdate(sAc); + + if (sDbgFormRt) { + basFormRtUpdate(sDbgFormRt); + } else { + dvxUpdate(sAc); + } } @@ -4310,7 +4375,7 @@ static void makePackage(void) { return; } - const char *result = idePackageMake(sAc, &sProject, makeOutput); + const char *result = idePackageMake(sAc, &sProject, makeOutput, setStatus); if (result) { setStatus(result); @@ -4609,7 +4674,7 @@ static void onBreakpointHit(void *ctx, int32_t line) { // handler finishes; that is not a real pause point (every genuine pause -- // breakpoint or Step Into/Over/Out -- arrives with a positive line), so // ignore it. Otherwise Continue would re-pause and require a second press. - // After Stop, a handler still run while runModule unwinds never pauses: + // After Stop, a handler still run while runBody unwinds never pauses: // the state stays Stopping and no debugger UI is driven. if (line < 0 || sStopRequested || sDbgState == DbgStoppingE) { return; @@ -4745,8 +4810,8 @@ static void onClose(WindowT *win) { // win itself is destroyed last, below, once nothing refers to it sWin = NULL; - // A running program is still executing sCachedModule (this close - // arrived through its event pump); runModule frees it on exit. + // A running program is still executing sCachedModule (on its task, or + // this close arrived through its event pump); runBody frees it on exit. if (sCachedModule && !sVm) { basModuleFree(sCachedModule); sCachedModule = NULL; @@ -5643,6 +5708,9 @@ static void onReplaceAll(WidgetT *w) { } } + // A long replace let DVX go on (replaceInSections pumps per procedure) + ideBusyPumpEnd(); + char statusBuf[BAS_MAX_IDENT]; snprintf(statusBuf, sizeof(statusBuf), "%d replacement(s) made.", (int)totalCount); setStatus(statusBuf); @@ -6830,6 +6898,7 @@ static int32_t replaceInSections(const char *objFilter, bool caseSensitive) { showProc(idx); count += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSensitive); + ideBusyPump(NULL); } arrfree(targets); @@ -6846,144 +6915,12 @@ static void reportUnreadable(const char *path) { } -static void runCached(void) { - if (!sCachedModule) { - setStatus("No compiled program. Press F5 to compile first."); - return; - } - - // The forms are registered from the project buffers - if (!stashCurrentFile()) { - return; - } - - clearOutput(); - runModule(sCachedModule); -} - - -// The project's fonts added for owner (the IDE) while a program runs; one -// that cannot be added is reported in the Output window -static void runFontsAdd(int32_t owner) { - for (int32_t i = 0; i < prjFontCount(&sProject); i++) { - char path[DVX_MAX_PATH]; - - platformPathJoin(path, sizeof(path), sProject.projectDir, prjFontPath(&sProject, i)); - - if (!dvxFontAddFile(owner, path)) { - char msg[DVX_MAX_PATH + IDE_MSG_BUF]; - - snprintf(msg, sizeof(msg), "Cannot use the font %s.\n", path); - outputAppend(msg, (int32_t)strlen(msg)); - } - } -} - - -static void runModule(BasModuleT *mod) { - // Never nest a run: a stopped program may still be unwinding - if (sVm) { - return; - } - - if (prjIsControlProject(&sProject)) { - setStatus(IDE_CTL_NO_RUN); - return; - } - - // The program's VM and form runtime. In the IDE, App.Path is the - // project's folder, App.Data its DATA folder (what the program will - // ship), and App.User the user folder the project's program gets - // once installed beside it; the program's screen window carries - // the project's name and F1 opens the project's help file. The IDE - // reports runtime errors through the Output pane and jumps the editor - // to the faulting line, so the form runtime's modal error box is - // suppressed (its nested event pump raced with events still queued - // by the program: "press OK twice"). - char helpPath[DVX_MAX_PATH] = ""; - const char *hostError = NULL; - BasRunSpecT spec; - - if (sProject.helpFile[0]) { - platformPathJoin(helpPath, sizeof(helpPath), sProject.projectDir, sProject.helpFile); - } - - memset(&spec, 0, sizeof(spec)); - spec.appDir = sProject.projectDir; - spec.appFile = sProject.projectPath; - spec.args = sProject.commandArgs; - spec.title = sProject.name; - spec.helpFile = helpPath[0] ? helpPath : NULL; - spec.printFn = printCallback; - spec.inputFn = inputCallback; - spec.doEventsFn = doEventsCallback; - spec.suppressErrorDialog = true; - - BasFormRtT *formRt = basRunHostCreate(sAc, mod, &spec, &hostError); - - if (!formRt) { - setStatus(hostError); - return; - } - - BasVmT *vm = formRt->vm; - - setStatus("Running..."); - closeFindDialog(); - - // Hide the designer, code and project windows while the program runs; - // they are restored (where they were visible) when it ends. - bool hadFormWin = sFormWin && sFormWin->visible; - bool hadToolbox = sToolboxWin && sToolboxWin->visible; - bool hadProps = sPropsWin && sPropsWin->visible; - bool hadCodeWin = sCodeWin && sCodeWin->visible; - bool hadPrjWin = sProjectWin && sProjectWin->visible; - - if (sFormWin) { - dvxHideWindow(sAc, sFormWin); - } - - if (sToolboxWin) { - dvxHideWindow(sAc, sToolboxWin); - } - - if (sPropsWin) { - dvxHideWindow(sAc, sPropsWin); - } - - if (sCodeWin) { - dvxHideWindow(sAc, sCodeWin); - } - - if (sProjectWin) { - dvxHideWindow(sAc, sProjectWin); - } - - basVmSetBreakpointCallback(vm, onBreakpointHit, NULL); - - sVm = vm; - sDbgFormRt = formRt; - sDbgModule = mod; - sDbgState = DbgRunningE; - sStopRequested = false; - - updateProjectMenuState(); - - // Set breakpoints BEFORE loading forms so breakpoints in form - // init code (module-level statements inside BEGINFORM) fire. - if (sDbgEnabled) { - syncVmBreakpoints(); - - if (sDbgBreakOnStart) { - basVmStepInto(vm); - } - } - - // The project's fonts, for the program run here as for the built one - // (added for the IDE until the run ends) - int32_t fontOwner = sAc->currentAppId; - - runFontsAdd(fontOwner); +// runBody -- the rest of a run (runModule), on the task the program runs +// on: its forms loaded and shown, its code run, then its event loop until +// it ends; the error it ended on reported, the IDE's windows restored. +static void runBody(void) { + BasFormRtT *formRt = sDbgFormRt; + BasVmT *vm = sVm; // Load forms from project files (AFTER breakpoints are set so // init code breakpoints fire), then show the startup form. @@ -7094,7 +7031,7 @@ static void runModule(BasModuleT *mod) { // The fonts the run added (the project's and any the program added): // the IDE carries none of its own - dvxFontRemoveOwner(fontOwner); + dvxFontRemoveOwner(sRun.fontOwner); // If the IDE was closed while the program was running, skip all UI // updates -- the windows are already destroyed -- and release the @@ -7128,23 +7065,23 @@ static void runModule(BasModuleT *mod) { dvxRaiseWindow(sAc, sCodeWin); } } else { - if (hadFormWin && sFormWin) { + if (sRun.hadFormWin && sFormWin) { dvxShowWindow(sAc, sFormWin); } - if (hadToolbox && sToolboxWin) { + if (sRun.hadToolbox && sToolboxWin) { dvxShowWindow(sAc, sToolboxWin); } - if (hadProps && sPropsWin) { + if (sRun.hadProps && sPropsWin) { dvxShowWindow(sAc, sPropsWin); } - if (hadCodeWin && sCodeWin) { + if (sRun.hadCodeWin && sCodeWin) { dvxShowWindow(sAc, sCodeWin); } - if (hadPrjWin && sProjectWin) { + if (sRun.hadPrjWin && sProjectWin) { dvxShowWindow(sAc, sProjectWin); } } @@ -7154,6 +7091,58 @@ static void runModule(BasModuleT *mod) { } +static void runCached(void) { + if (!sCachedModule) { + setStatus("No compiled program. Press F5 to compile first."); + return; + } + + // The forms are registered from the project buffers + if (!stashCurrentFile()) { + return; + } + + clearOutput(); + runModule(sCachedModule); +} + + +// The project's fonts added for owner (the IDE) while a program runs; one +// that cannot be added is reported in the Output window +static void runFontsAdd(int32_t owner) { + for (int32_t i = 0; i < prjFontCount(&sProject); i++) { + char path[DVX_MAX_PATH]; + + platformPathJoin(path, sizeof(path), sProject.projectDir, prjFontPath(&sProject, i)); + + if (!dvxFontAddFile(owner, path)) { + char msg[DVX_MAX_PATH + IDE_MSG_BUF]; + + snprintf(msg, sizeof(msg), "Cannot use the font %s.\n", path); + outputAppend(msg, (int32_t)strlen(msg)); + } + } +} + + +// runModule -- run mod: set up here (runStart), then run (runBody) on a +// task of the IDE's own (shellAppTaskStart), so the shell's main loop +// goes on while the program does -- programs that end are reaped, Exit +// and DOS requests carried out, the screen saver started. Without one +// (the host tests have no shell) the program runs here, to its end. +static void runModule(BasModuleT *mod) { + if (!runStart(mod)) { + return; + } + + IdeTaskStartFnT taskStart = (IdeTaskStartFnT)dlsym(NULL, DVX_SYM("shellAppTaskStart")); + + if (!taskStart || taskStart(sAc, IDE_RUN_TASK_NAME, BAS_RUN_STACK_SIZE, runTaskEntry, NULL) < 0) { + runBody(); + } +} + + // runProgramSlices -- run the program in step slices, yielding to DVX // between them, pausing at breakpoints, until it halts, fails (its // message and line left in the VM, see basRunHostSlice) or is stopped. @@ -7195,6 +7184,126 @@ static BasVmResultE runProgramSlices(BasFormRtT *rt) { } +// runStart -- the first part of a run (runModule): the program's runtime +// made, the IDE's windows hidden, breakpoints set (and stepping when the +// run breaks on start), the project's fonts added. False when it cannot +// start. +static bool runStart(BasModuleT *mod) { + // Never nest a run: a stopped program may still be unwinding + if (sVm) { + return false; + } + + if (prjIsControlProject(&sProject)) { + setStatus(IDE_CTL_NO_RUN); + return false; + } + + // The program's VM and form runtime. In the IDE, App.Path is the + // project's folder, App.Data its DATA folder (what the program will + // ship), and App.User the user folder the project's program gets + // once installed beside it; the program's screen window carries + // the project's name and F1 opens the project's help file. The IDE + // reports runtime errors through the Output pane and jumps the editor + // to the faulting line, so the form runtime's modal error box is + // suppressed (its nested event pump raced with events still queued + // by the program: "press OK twice"). + char helpPath[DVX_MAX_PATH] = ""; + const char *hostError = NULL; + BasRunSpecT spec; + + if (sProject.helpFile[0]) { + platformPathJoin(helpPath, sizeof(helpPath), sProject.projectDir, sProject.helpFile); + } + + memset(&spec, 0, sizeof(spec)); + spec.appDir = sProject.projectDir; + spec.appFile = sProject.projectPath; + spec.args = sProject.commandArgs; + spec.title = sProject.name; + spec.helpFile = helpPath[0] ? helpPath : NULL; + spec.printFn = printCallback; + spec.inputFn = inputCallback; + spec.doEventsFn = doEventsCallback; + spec.suppressErrorDialog = true; + + BasFormRtT *formRt = basRunHostCreate(sAc, mod, &spec, &hostError); + + if (!formRt) { + setStatus(hostError); + return false; + } + + BasVmT *vm = formRt->vm; + + setStatus("Running..."); + closeFindDialog(); + + // Hide the designer, code and project windows while the program runs; + // they are restored (where they were visible) when it ends. + sRun.hadFormWin = sFormWin && sFormWin->visible; + sRun.hadToolbox = sToolboxWin && sToolboxWin->visible; + sRun.hadProps = sPropsWin && sPropsWin->visible; + sRun.hadCodeWin = sCodeWin && sCodeWin->visible; + sRun.hadPrjWin = sProjectWin && sProjectWin->visible; + + if (sFormWin) { + dvxHideWindow(sAc, sFormWin); + } + + if (sToolboxWin) { + dvxHideWindow(sAc, sToolboxWin); + } + + if (sPropsWin) { + dvxHideWindow(sAc, sPropsWin); + } + + if (sCodeWin) { + dvxHideWindow(sAc, sCodeWin); + } + + if (sProjectWin) { + dvxHideWindow(sAc, sProjectWin); + } + + basVmSetBreakpointCallback(vm, onBreakpointHit, NULL); + + sVm = vm; + sDbgFormRt = formRt; + sDbgModule = mod; + sDbgState = DbgRunningE; + sStopRequested = false; + + updateProjectMenuState(); + + // Set breakpoints BEFORE loading forms so breakpoints in form + // init code (module-level statements inside BEGINFORM) fire. + if (sDbgEnabled) { + syncVmBreakpoints(); + + if (sDbgBreakOnStart) { + basVmStepInto(vm); + } + } + + // The project's fonts, for the program run here as for the built one + // (added for the IDE until the run ends) + sRun.fontOwner = sAc->currentAppId; + runFontsAdd(sRun.fontOwner); + return true; +} + + +// runTaskEntry -- runModule's task: the program's runtime moves to it, +// then the run goes on (runBody). +static void runTaskEntry(void *arg) { + (void)arg; + basFormRtAdopt(sDbgFormRt); + runBody(); +} + + // saveActiveFile -- write the active file (editor/designer state first // stashed into its buffer). Returns false when nothing was written. static bool saveActiveFile(void) { @@ -9017,7 +9126,7 @@ static void updateWatchWindow(void) { valBuf[0] = '\0'; - if (sDbgState == DbgPausedE && sVm && sDbgModule && !basDbgEval(sVm, sWatchExprs[i], valBuf, sizeof(valBuf))) { + if (sDbgState == DbgPausedE && sVm && sDbgModule && !basDbgEval(sVm, sWatchExprs[i], valBuf, sizeof(valBuf), &sBusyPump)) { snprintf(valBuf, sizeof(valBuf), "%s", BAS_DBG_EVAL_ERROR); } @@ -9025,6 +9134,7 @@ static void updateWatchWindow(void) { listRowsAdd(&sWatchRows, valBuf); } + ideBusyPumpEnd(); listRowsCommit(&sWatchRows, sWatchList, count); } diff --git a/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c b/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c index ea2a0c53..2cf45404 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c +++ b/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c @@ -525,7 +525,7 @@ static void onOk(WidgetT *w) { frmMenuStripBlank(&sMed.items); char reason[MED_MSG_BUF]; - int32_t badIdx = frmModelMenuValidate(sMed.form, sMed.items, basIsValidIdent, reason, sizeof(reason)); + int32_t badIdx = frmModelMenuValidate(sMed.form, sMed.items, reason, sizeof(reason)); if (badIdx >= 0) { dvxErrorBox(sMed.ctx, MED_DLG_TITLE, reason); diff --git a/src/apps/kpunch/dvxbasic/ide/idePackage.c b/src/apps/kpunch/dvxbasic/ide/idePackage.c index ac079a0a..0dce4c29 100644 --- a/src/apps/kpunch/dvxbasic/ide/idePackage.c +++ b/src/apps/kpunch/dvxbasic/ide/idePackage.c @@ -60,6 +60,7 @@ #define PKG_NEED_SEP "," // between the needs of [Package] Needs #define PKG_NEED_BLANKS " \t" // between a need's id and version #define PKG_ID_DEFAULT_VENDOR "my" // a package name's vendor when the project has no publisher +#define PKG_SLICE_MS 50 // making the package between frames // The keys offered to sign with: their files and the list's lines typedef struct { @@ -74,7 +75,8 @@ typedef struct { static bool askId(AppContextT *ctx, PrjStateT *prj); static bool chooseKey(AppContextT *ctx, PrjStateT *prj, char *secretPath, int32_t secretSize); -const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build); +const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build, IdeStatusFnT status); +static const char *jobRun(AppContextT *ctx, PkgDpkJobT *job, IdeStatusFnT status); static void keysList(KeyListT *keys); static bool manifestOf(const PrjStateT *prj, PkgManifestT *m); static const char *say(const char *fmt, ...) __attribute__((format(printf, 1, 2))); @@ -149,7 +151,7 @@ static bool chooseKey(AppContextT *ctx, PrjStateT *prj, char *secretPath, int32_ } -const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build) { +const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build, IdeStatusFnT status) { static const FileFilterT filters[] = { { "Packages (*.dpk)" }, { "All Files (*.*)" } @@ -166,6 +168,7 @@ const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build) PkgManifestT m; PkgSecretT secret; PkgDpkFileT *files = NULL; + PkgDpkJobT *job = NULL; const char *failure = NULL; bool control = prjIsControlProject(prj); bool ok; @@ -217,8 +220,9 @@ const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build) failure = say("%s", err); } - if (!failure && !pkgDpkBuildFiles(&m, files, (int32_t)arrlen(files), outPath, secretPath[0] ? &secret : NULL, err, sizeof(err))) { - failure = say("%s", err); + if (!failure) { + job = pkgDpkBuildStart(&m, files, (int32_t)arrlen(files), outPath, secretPath[0] ? &secret : NULL, err, sizeof(err)); + failure = job ? jobRun(ctx, job, status) : say("%s", err); } pkgSecretWipe(&secret); @@ -234,6 +238,32 @@ const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build) } +// Run the package job (freed) to its end, a slice at a time with a frame +// of DVX between slices (the busy cursor up: input waits) and how far it +// is on the status line: NULL when the package is made, else why not. +static const char *jobRun(AppContextT *ctx, PkgDpkJobT *job, IdeStatusFnT status) { + PkgDpkStepE step = PkgDpkMoreE; + const char *failure; + + dvxSetBusy(ctx, true); + + while (step == PkgDpkMoreE) { + PlatformTicksT end = platformClock() + PLATFORM_MS_TO_TICKS(PKG_SLICE_MS); + + while ((step = pkgDpkJobStep(job, PKG_DPK_STEP)) == PkgDpkMoreE && platformClock() < end) { + } + + status(say("Making the package: %d%%", (int)pkgDpkJobPercent(job))); + dvxUpdate(ctx); + } + + dvxSetBusy(ctx, false); + failure = step == PkgDpkDoneE ? NULL : say("%s", pkgDpkJobMessage(job)); + pkgDpkJobFree(job); + return failure; +} + + // The user's secret keys (the KEYS folder of the user folder), by the // names in them, after "Not signed". static void keysList(KeyListT *keys) { diff --git a/src/apps/kpunch/dvxbasic/ide/idePackage.h b/src/apps/kpunch/dvxbasic/ide/idePackage.h index e11b74af..1f903d79 100644 --- a/src/apps/kpunch/dvxbasic/ide/idePackage.h +++ b/src/apps/kpunch/dvxbasic/ide/idePackage.h @@ -45,12 +45,16 @@ // release: NULL when it is built, else why not. typedef const char *(*IdeBuildFnT)(const char *outPath, bool release); +// Show text on the IDE's status line. +typedef void (*IdeStatusFnT)(const char *text); + // Make the project's package: its package name asked for when the // project has none (and kept in it, as the key chosen is), the package // file and the key to sign with asked for, the program built by build // into a scratch folder, and the package made from it and the project's -// details ([Project] and [Package]). The status line to show: the package -// made, or why not; NULL when the user cancelled. -const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build); +// details ([Project] and [Package]), a step at a time with DVX kept going +// between steps and status showing how far it is. The status line to +// show: the package made, or why not; NULL when the user cancelled. +const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build, IdeStatusFnT status); #endif // IDE_PACKAGE_H diff --git a/src/apps/kpunch/dvxbasic/ide/ideProperties.c b/src/apps/kpunch/dvxbasic/ide/ideProperties.c index 0fffd44b..6efa8b30 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideProperties.c +++ b/src/apps/kpunch/dvxbasic/ide/ideProperties.c @@ -39,6 +39,7 @@ #include "../basdev/basSource.h" #include "../formrt/formrt.h" #include "../formrt/frmParser.h" +#include "../../../../libs/kpunch/sql/dvxSql.h" #include "dvxDlg.h" #include "dvxWm.h" #include "box/box.h" @@ -140,14 +141,14 @@ static const PrpFontWordT sFontWords[] = { // here; the column-name and table-name probes resolve the subset they use. // ============================================================ -typedef int32_t (*SqlOpenFnT)(const char *); -typedef void (*SqlCloseFnT)(int32_t); -typedef int32_t (*SqlQueryFnT)(int32_t, const char *); -typedef int32_t (*SqlFieldCountFnT)(int32_t); -typedef const char *(*SqlFieldNameFnT)(int32_t, int32_t); -typedef bool (*SqlNextFnT)(int32_t); -typedef const char *(*SqlFieldTextFnT)(int32_t, int32_t); -typedef void (*SqlFreeResultFnT)(int32_t); +typedef __typeof__(dvxSqlOpen) *SqlOpenFnT; +typedef __typeof__(dvxSqlClose) *SqlCloseFnT; +typedef __typeof__(dvxSqlQuery) *SqlQueryFnT; +typedef __typeof__(dvxSqlFieldCount) *SqlFieldCountFnT; +typedef __typeof__(dvxSqlFieldName) *SqlFieldNameFnT; +typedef __typeof__(dvxSqlNext) *SqlNextFnT; +typedef __typeof__(dvxSqlFieldText) *SqlFieldTextFnT; +typedef __typeof__(dvxSqlFreeResult) *SqlFreeResultFnT; // ============================================================ // Module state @@ -215,11 +216,11 @@ static void addFontRow(const WidgetT *w) { } basFontChoiceOf(wgtFont(w), &font); - pos = frmEmitClamped(text, sizeof(text), 0, "%s, %" PRId32, font.family, font.size); + pos = dvxStrAppendfCut(text, sizeof(text), 0, "%s, %" PRId32, font.family, font.size); for (int32_t i = 0; i < DVX_ARRAY_LEN(sFontWords); i++) { if (font.style & sFontWords[i].style) { - pos = frmEmitClamped(text, sizeof(text), pos, ", %s", sFontWords[i].word); + pos = dvxStrAppendfCut(text, sizeof(text), pos, ", %s", sFontWords[i].word); } } diff --git a/src/apps/kpunch/dvxbasic/ide/ideToolbox.c b/src/apps/kpunch/dvxbasic/ide/ideToolbox.c index 6d174812..20acad0f 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideToolbox.c +++ b/src/apps/kpunch/dvxbasic/ide/ideToolbox.c @@ -161,12 +161,6 @@ WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) { break; } - // Types that need pixel data at creation (Image, ImageButton) - // cannot be placed at design time. - if (iface->createSig == WGT_CREATE_PARENT_DATA) { - continue; - } - // Add tool entry TbxToolEntryT entry; memset(&entry, 0, sizeof(entry)); diff --git a/src/apps/kpunch/dvxbasic/ideguide.dhs b/src/apps/kpunch/dvxbasic/ideguide.dhs index abf5dff1..059ad670 100644 --- a/src/apps/kpunch/dvxbasic/ideguide.dhs +++ b/src/apps/kpunch/dvxbasic/ideguide.dhs @@ -779,7 +779,7 @@ Click a tool to select it (the active tool name is stored in the designer state) WrapBox Flow layout container that wraps items to the next row. .endtable -Image and ImageButton controls are not in the Toolbox; they can still be written into a .frm file by hand and are then edited in the Properties panel like any other control. +A new Image or ImageButton starts empty, 32 by 32 pixels so it can be seen and selected: set its Picture property to show a picture. .link ide.designer Form Designer .link ide.usercontrols UserControls in the IDE diff --git a/src/apps/kpunch/dvxbasic/langref.dhs b/src/apps/kpunch/dvxbasic/langref.dhs index 7a58b616..42a744b1 100644 --- a/src/apps/kpunch/dvxbasic/langref.dhs +++ b/src/apps/kpunch/dvxbasic/langref.dhs @@ -1760,6 +1760,7 @@ ErrorHandler: 11 Division by zero 13 Type mismatch (text that is not a number where a number is needed) / not an array / not a TYPE instance 14 Out of string space (no memory to build a string) + 18 User interrupt occurred (a control's code ran too long without returning and was stopped) 20 RESUME without error 26 FOR loop nesting too deep 28 Out of stack space (calls or expressions nested too deeply) @@ -1800,13 +1801,15 @@ Executes an operating-system command. SHELL "command" .endcode -When used as a function, SHELL returns the exit code of the command. +When used as a function, SHELL returns the exit code of the command, or -1 when it did not run. .code Shell "DIR /B" exitCode = Shell("COPY A.TXT B.TXT") .endcode +DVX steps aside while the command runs, as it does for a DOS program started from Program Manager: the screen goes back to text, and the command has the keyboard, the mouse, and the hardware to itself. Nothing of DVX runs meanwhile, so the other programs that are running are named first and asked to close (your program stays and waits); choosing No, or a program that keeps a window open, means the command does not run (-1). When the command ends, the desktop comes back and your program goes on. A command that prints and ends at once is gone from the screen as soon as it ends: ShellRunDos (shell.bas) can wait for a key first. + 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 @@ -1897,6 +1900,8 @@ PLAY "T160 O3 L8 E D C D E E E4" MF Music foreground: PLAY, SOUND, and BEEP wait until the sound has finished (the default). MB Music background: the program goes on while the music plays; it only waits when more than 32 notes are waiting. I n DVX's own: play the notes that follow on General MIDI instrument n (1 to 128; 1 is the piano, 41 the violin, 74 the flute) through the music synthesizer. I0 goes back to plain tones (the default). + V n Volume of the notes that follow, 0 to 100 percent (default 100), as in QB64. On an instrument it sets how hard the note is played. + @ n Waveform of the tones that follow, as in QB64: @1 square (the default, like the PC speaker), @3 triangle (softer), @5 noise (drums, explosions). QB64's @2 and @4 are not available. X "X" + VARPTR$(s$) plays the commands in s$ at that point (s$ may use X itself, up to 16 levels deep). = Any number above may be written as "=" + VARPTR$(n), n a numeric expression: "T=" + VARPTR$(tempo). .endtable @@ -1909,11 +1914,12 @@ tempo = 160 PLAY "T=" + VARPTR$(tempo) + "O3 X" + VARPTR$(verse$) + "X" + VARPTR$(verse$) .endcode -Building the string with + works too, such as PLAY "O" + STR$(n). Without I, the notes are square-wave tones like the PC speaker's; with it, they play on the same synthesizer as MIDI music. +Building the string with + works too, such as PLAY "O" + STR$(n). Without I, the notes are tones -- square waves like the PC speaker's unless @ chooses another -- and with it, they play on the same synthesizer as MIDI music. The PC speaker plays only square tones at one volume: there V and @ make no difference, except that V0 is silent. .code PLAY "MB T120 L4 CDEF GAB>C" ' a scale in the background PLAY "I74 O4 L8 GAB>C" ' on the flute +PLAY "@3 V60 L8 CEG>C" ' softer, quieter .endcode .h2 PLAY Function @@ -2057,8 +2063,11 @@ Shows the program's screen on the whole screen, or back in its window. Alt+Enter .code FULLSCREEN [ON | OFF] FULLSCREEN expression ' True: whole screen, False: window +isFull = FULLSCREEN ' True while the program's screen has the whole screen .endcode +Used as a value, FULLSCREEN tells whether the program's screen has the whole screen (also while the desktop is shown for another program, since the screen comes back with the focus): False when no suitable video mode could be set, or before SCREEN or any drawing made a screen. A form takes the whole screen with its FullScreen property instead. + .h2 Text: CLS, LOCATE, COLOR, CSRLIN, POS, SCREEN( ) .code @@ -2516,7 +2525,7 @@ Name "draft.txt" As "final.txt" .h2 FILECOPY -Copies a file. If the destination already exists it is overwritten. +Copies a file. If the destination already exists it is overwritten. While a big file copies, the rest of DVX and the program's own events go on, as with DoEvents (in a control's code they wait until the copy ends). .code FILECOPY source$, destination$ @@ -2584,7 +2593,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. DIR$("C:\*.*") lists the root of C: (DIR$("C:*.*") the drive's current folder). +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. The pattern uses DOS's wildcards, as the file dialog does: * stands for any characters and ? for one, case does not matter, and *.* is every file, with or without an extension. DIR$("C:\*.*") lists the root of C: (DIR$("C:*.*") the drive's current folder). Looking through a big folder for a rare match lets the rest of DVX and the program's own events go on, as with DoEvents; if one of those events starts a new DIR$ listing, the call returns "" (in a control's code nothing else runs meanwhile). .code cwd$ = CURDIR$ @@ -2608,7 +2617,9 @@ bytes = FILELEN(filename$) .toc 1 Built-in Functions .toc 2 String Functions .index ASC +.index ASCW .index CHR$ +.index CHRW$ .index FORMAT$ .index HEX$ .index INSTR @@ -2640,7 +2651,9 @@ bytes = FILELEN(filename$) Function Returns Description -------- ------- ----------- ASC(s$) Integer ASCII code of the first character of s$ (error 5 for an empty string) + ASCW(s$) Long Unicode code point of the first character of s$ (error 5 for an empty string) CHR$(n) String Character with ASCII code n + CHRW$(n) String Character for Unicode code point n ("?" when DVX has no such character; error 5 outside 0 to &H10FFFF) FILTER(arr, match$ [, include [, compare]]) String array The elements of arr that contain match$ (or, with include False, that do not) FORMAT$(value [, fmt$]) String Formats a number or a date using a format string HEX$(n) String Hexadecimal representation of n (uppercase, no leading &H) @@ -2840,7 +2853,7 @@ If length is omitted, MID$ replaces up to LEN(replacement$) characters; a length SIN(n) Double Sine of n (n in radians) SQR(n) Double Square root of n TAN(n) Double Tangent of n (n in radians) - TIMER Double Seconds since midnight + TIMER Double Seconds since midnight, to a fraction of a millisecond (it keeps rising between the clock's 55 ms ticks) .endtable RND follows QBasic: RND or RND(n) with n > 0 returns the next number in the sequence, RND(0) repeats the last number, and RND(n) with n < 0 reseeds the generator from n (the same n always gives the same number). The result is a Double, 0 <= RND < 1. @@ -3091,6 +3104,7 @@ File sizes and positions are Longs, so they stop at 2 GB (2,147,483,647 bytes). .index IIF .index CHOOSE .index LOADPICTURE +.index LOADPICTUREDATA .h1 Miscellaneous Functions @@ -3099,7 +3113,7 @@ File sizes and positions are Longs, so they stop at 2 GB (2,147,483,647 bytes). -------- ------- ----------- DATE$ String Current date as "MM-DD-YYYY" TIME$ String Current time as "HH:MM:SS" (24-hour) - TIMER Double Seconds since midnight + TIMER Double Seconds since midnight, to a fraction of a millisecond ENVIRON$(name$) String Value of the environment variable; empty string if not set COMMAND$ String The arguments the program was started with; empty string if none COMMAND$(n) String The nth argument (1 = first) without its quotes; empty string when there are fewer; COMMAND$(0) is the same as COMMAND$ @@ -3123,7 +3137,7 @@ Print Choose(Weekday(Date), "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat") .h2 LOADPICTURE -LOADPICTURE(file$) reads a picture file (BMP, PNG, JPEG, or GIF) -- or the icon of a DVX program, widget, or library file (.app, .wgt, .lib) -- and returns a Picture object (TypeName "Picture") that can be given to a picture property: a control's DragIcon, the Picture of an Image or ImageButton. Use SET, or a plain assignment, as with a file name; file names still work too. LOADPICTURE() or LOADPICTURE("") gives an empty picture, which clears the property. A missing file raises error 53 (File not found), a file that is not a picture error 481 (Invalid picture), so the check happens where the picture is loaded. Loading the same file again gives the same Picture object. Reading a picture property back gives a Picture object too (TypeName "Picture"), as in Visual Basic; used as a value (printed, joined to text, compared) it stands for its file name, so code that treats the property as a file name keeps working. The name of a program, widget, or library file works wherever a picture file's name does (a Picture property, DragIcon, PaintPicture, a Toolbar's AddButton) and shows its icon; such a file without an icon is not a picture (error 481). +LOADPICTURE(file$) reads a picture file (BMP, PNG, JPEG, or GIF) -- or the icon of a DVX program, widget, or library file (.app, .wgt, .lib) -- and returns a Picture object (TypeName "Picture") that can be given to a picture property: a control's DragIcon, the Picture of an Image or ImageButton. Use SET, or a plain assignment, as with a file name; file names still work too. LOADPICTURE() or LOADPICTURE("") gives an empty picture, which clears the property. A missing file raises error 53 (File not found), a file that is not a picture error 481 (Invalid picture), so the check happens where the picture is loaded. The check reads the picture's header only (the control that shows it reads the rest): a picture file damaged after its header shows nothing. Loading the same file again gives the same Picture object. Reading a picture property back gives a Picture object too (TypeName "Picture"), as in Visual Basic; used as a value (printed, joined to text, compared) it stands for its file name, so code that treats the property as a file name keeps working. The name of a program, widget, or library file works wherever a picture file's name does (a Picture property, DragIcon, PaintPicture, a Toolbar's AddButton) and shows its icon; such a file without an icon is not a picture (error 481). The optional size and colordepth arguments are Visual Basic's: size is vbLPSmall (0), vbLPLarge (1), vbLPSmallShell (2), vbLPLargeShell (3), or vbLPCustom (4, with x and y giving the size), colordepth vbLPDefault (0), vbLPMonochrome (1), vbLPVGAColor (2), or vbLPColor (3). They choose among the images of an icon file; every DVX picture is a single bitmap, so, as for a bitmap in Visual Basic, they are checked and otherwise make no difference. A value outside these, or a negative x or y, raises error 5. @@ -3149,6 +3163,18 @@ Set pic = LoadPicture("logo.bmp") Print Me.ScaleX(pic.Width, vbHimetric, vbPixels); "x"; Me.ScaleY(pic.Height, vbHimetric, vbPixels) .endcode +.h2 LOADPICTUREDATA + +LOADPICTUREDATA(bytes$) does what LOADPICTURE does for a picture held in a string rather than in a file: the bytes of a BMP, PNG, JPEG, or GIF file, such as HttpBody$ gives for a picture downloaded from the web, or ResGetData$ for a picture resource of a DVX program, widget, or library file (any of its resources, not only its icon). The Picture object it returns goes wherever a LOADPICTURE picture goes: the Picture of an Image, an ImageButton, or a PictureBox, a control's DragIcon, a form's Icon, and PaintPicture; reading such a property back gives the same object. Its Width, Height, and Type are a loaded picture's; used as a value it stands for "", since it has no file name. Bytes that are not a picture raise error 481 (Invalid picture); LOADPICTUREDATA("") gives an empty picture. Giving one to a control that shows no picture (a Label's Picture, for example) stops the program with an error. + +A Picture object made this way keeps its bytes until the program ends, and the same bytes give the same object, so loading a picture again costs nothing. To show many different pictures one after another (a slide show, pictures from the web), use the SetPictureData method of the Image, ImageButton, or PictureBox instead: Image1.SetPictureData bytes$ shows the picture without keeping it, and returns True, or False when the bytes are not a picture. + +.code +Image1.Picture = LoadPictureData(HttpBody$(req)) +Set Label1.DragIcon = LoadPictureData(ResGetData$("mygame.app", "card")) ' resource.bas in the project +If Not Picture1.SetPictureData(HttpBody$(req)) Then MsgBox "That is not a picture." +.endcode + .h2 COMMAND$ COMMAND$ returns the program's arguments exactly as they were given, as in Visual Basic (QBasic turned them into capitals; DVX keeps them as they are). Arguments are separated by blanks; an argument with blanks in it is written in double quotes. COMMAND$(n) picks out one argument with its quotes removed, so a program that opens the document it was started with only needs: @@ -3996,6 +4022,9 @@ Through a variable declared As Collection the members are checked when the progr .index SQLFieldCount .index SQLFreeResult .index SQLError$ +.index SQLEscape$ +.index SQLFieldName$ +.index SQLFieldText$ .index SQLite .h1 SQL Functions @@ -4044,6 +4073,24 @@ ok = SQLExec(db, "DELETE FROM users WHERE id = 5") Put text values in single quotes, as in the examples. Text in double quotes is read as a column name, so "Scott" in place of 'Scott' gives the error "no such column: Scott". +.h3 SQLEscape$ + +Returns text with every single quote doubled, the way SQL writes a quote inside a quoted value. Use it on text the user typed (or read from a file) before you put it between quotes in a statement: a name such as O'Brien would otherwise end the value early and break the statement. + +.code +SQLExec db, "INSERT INTO users (name) VALUES ('" & SQLEscape$(name$) & "')" +.endcode + +SQLExec runs every statement in sql$, one after another, before it returns. + +.h3 Long Statements + +A statement can take a while: one that changes many rows, CREATE INDEX on a big table, or the first SQLNext of a query that sorts or groups, which reads every row first. While a statement runs, DVX and the other programs keep going, and your program's events run as they do during DoEvents. Ending the program (End, or closing its last form) stops the statement, and what it changed is undone. + +An event that runs meanwhile cannot use the database the statement is running on: SQLExec, SQLQuery, and SQLNext return False or 0, and SQLError$ says "A statement is still running on this database". Its field functions return "" or 0. SQLFreeResult and SQLClose wait until the statement ends. Other databases can be used as always. + +Code in a control file (a control written in BASIC) does not wait like this: its statements run to the end. + .h3 SQLAffected Returns the number of rows affected by the last INSERT, UPDATE, or DELETE. @@ -4920,15 +4967,21 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic .index CommHandshake .index CommRecvEncrypted .index CommSetPlain +.index CommOpenTcp .index print.bas .index PrintTextDialog .index PrintPictureDialog .index PrintPageSetup .index net.bas .index TcpConnect +.index TcpConnectStart .index TcpRecv$ +.index TcpPeek$ +.index NetState +.index TcpSendSome .index UdpSend .index HostLookup$ +.index HostLookupStart .index game.bas .index GameKeyDown .index GameKeyClear @@ -5138,10 +5191,17 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic .index security.bas .index SecRandom$ .index SecDhCreate +.index SecDhStep .index SecCipherCreate .index SecSha256$ .index SecHmac$ +.index SecX25519$ +.index SecAeadLock$ +.index SecAeadUnlock$ +.index SecEd25519Sign$ +.index SecEd25519Check .index Encryption +.index Signatures .index SerSet .index SerLastError .index CommHandshakeStep @@ -5175,6 +5235,9 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic .index ShellRunningActivate .index ShellRunningClose .index ShellRunningKill +.index ShellRunningPriority +.index ShellRunDosWait +.index ShellRunningSetPriority .index ShellAppIsHung .index ShellAppMemory .index ShellAppId @@ -5244,9 +5307,22 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic .index PkgInstallStart .index PkgRunFor .index PkgRemoveStart -.index PkgBuild -.index PkgSign +.index PkgBuildStart +.index PkgSignStart .index PkgKeygen +.index PkgOpenIn +.index PkgKeysRows$ +.index PkgUntrustKey +.index PkgVerifyStart +.index PkgSignData$ +.index PkgCheckData +.index PkgZipListStart +.index PkgZipExtractStart +.index PkgZipCreateStart +.index PkgZipRows$ +.index PkgVersionCompare +.index PkgNetOpenIn +.index ZIP Files .index json.bas .index http.bas .index pkgnet.bas @@ -5254,6 +5330,7 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic .index Web requests .index HttpOpen .index HttpRunFor +.index HttpSetBodyFile .index JsonParse .h1 Include Libraries @@ -5265,7 +5342,7 @@ DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory ---- -------- commdlg.bas File Open/Save dialogs (DlgFileOpenIn$ and DlgFileSaveIn$ start in a folder you name), the folder dialog (DlgFolder$, for a Browse button), input dialogs, choice dialogs, save-prompt dialogs, the color dialog, the font dialog (DlgFont), message boxes with your own buttons (DlgButtons) sql.bas SQLite database access (SQLOpen, SQLQuery, SQLNext, SQLField$, etc.) - comm.bas Serial-port I/O (raw and packet) and optional encrypted links; port settings read back and changed, why a call failed, breaks, ring, and line errors (SerOpen, SerSet, SerLastError, CommOpen, CommHandshakeStep, ...) + comm.bas Serial-port I/O (raw and packet) and optional encrypted links; port settings read back and changed, why a call failed, breaks, ring, and line errors (SerOpen, SerSet, SerLastError, CommOpen, CommOpenTcp, CommHandshakeStep, ...) conn.bas Connections the way Terminal makes them: a serial port, a modem that dials, the secure link to the DVX host proxy, or Telnet, shown on a Terminal control, with capture, answerback, and file transfers (ConnCreate, ConnOpen, ConnPoll, ConnAttach, ConnSendFiles, ...) xfer.bas File transfers with XMODEM, XMODEM-1K, YMODEM, and ZMODEM, on a serial port or a connection, with their progress and questions (XferSendFile, XferStatus, XferBytes, XferAnswer, ...) prefs.bas Settings files (INI): load one, read and change its settings, list the keys of a section, save it; DVX's own settings file, the screen mode it starts in, the names its choices are stored as and its default choices, and choices stored by names of your own (PrefsLoad, PrefsGet$, PrefsSet, PrefsKeyName$, PrefsSaveAs, PrefsDvxIni$, PrefsVideoWidth, PrefsChoiceLabel$, PrefsChoiceDefault, PrefsGetChoiceFrom, ...) @@ -5275,7 +5352,7 @@ DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory net.bas Network connections: TCP (TcpConnect, TcpListen, TcpSend, TcpRecv$, TcpPeer$, ...), UDP messages and name lookups, any bytes; the network settings and status (NetConfigLoad, NetConfigSet, NetConfigStore, NetRestart, NetStatus$, ...) print.bas Printing: text with the page setup and pictures, with or without the Print dialog; a form; the Page Setup dialog; the printer and page settings; the printer ports (PrintModelLabel$, PrintSettingsStore, PrintPageSetting, PrintChoose, PrintForm, PrintPortStatus, ...) listhelp.bas Lists and icon views you draw yourself (a UserControl, a PictureBox) that behave like ListBox and Program Manager: selection with Shift and Ctrl, the arrow keys in a list or a grid, Ctrl+A, Space, type-ahead, where a dragged item lands, and Program Manager's icon view rules -- its cells, the grid with a scroll bar, names on two lines, dragging and dropping (ListSelCreate, ListSelClick, ListSelNavigate, ListTypeAhead, ListIconMetric, ListIconLabel$, ...) - security.bas Security: random bytes for keys, a key exchange, a cipher, SHA-256 hashes, HMAC signatures, key making, and a safe compare (SecRandom$, SecDhCreate, SecCipherCrypt$, SecSha256$, SecHmac$, SecHkdf$, ...) + security.bas Security: random bytes for keys, key exchanges, ciphers, sealing, SHA-256 hashes, HMAC and public-key signatures, key making, and a safe compare (SecRandom$, SecX25519$, SecAeadLock$, SecEd25519Sign$, SecSha256$, SecHkdf$, ...) game.bas Games: whether a key is being held down right now, forgetting held keys, frames FRAMEWAIT skipped, waiting for the screen's retrace (GameKeyDown, GameKeyClear, FrameMissed, GameWaitRetrace) joystick.bas Joysticks: how many there are, their names, where each stick points, and which buttons are pressed (JoyCount, JoyX, JoyY, JoyButton, ...); setting them up: calibration, dead zone, game port mode, keyboard joystick keys (JoySetCalibration, JoyCalibration, JoyRescan, JoyKeyIndex, JoySettingsStore, ...) sound.bas Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...); sounds loaded once and played with volume, pan, and pitch, sound made by the program as it goes, recording from a microphone or line input (RecordStart, RecordStop, RecordLevel, ...), the FM synthesizer chip, a wavetable card's voices and memory (SoundLoad, SoundPlay, SoundSetPitch, SoundCreate, SoundStreamOpen, SoundStreamWrite, FmAlloc, FmWrite, WtAlloc, WtStart, ...); DVX's own event sounds (SoundPlayEvent), master volume, sound driver, and MIDI settings (SoundEventFile$, SoundSetMasterVolume, SoundSettingsStore, ...) @@ -5288,8 +5365,8 @@ DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory compiler.bas The BASIC compiler: compile a project or files you give it, find compile errors in their files, list a program's procedures and the language's keywords, build an .app or a control file; put a project's UserControls on a form designer's toolbox; read source text as the compiler does (tokens, code or string or comment, renaming a control in code, WithEvents variables, class Events), F1 help topics; open, change, and save project files and UserControl headers (CompSourceFromProject, CompSourceAdd, CompCompile, CompError$, CompMapFile$, CompProcList$, CompBuildApp, CompDesignControls, CompTokens$, CompRenameInCode$, CompHelpTopic$, ProjOpen, ProjSave, ProjCtlSetHeader$, ...) debug.bas Run a compiled program inside yours and debug it: breakpoints, stepping, its variables, expressions, Immediate-window lines, its PRINT output (RunCreate, RunStart, RunPoll, DbgSetBreakpoints, DbgStepOver, DbgLocals$, DbgEval$, DbgExec$, ...) designer.bas Form designers: the control types, whether each holds other controls and can be placed, the names of new controls, default events, toolbox tooltips and icons, and each type's properties (with their types and value names), methods, and events (with each event's parameters); the names in .frm and class module text; .frm files as data to read, edit, and write, controls copied and pasted as text, and a loaded form written back as .frm text (CtlTypeCount, CtlTypeName$, CtlTypeFlags, CtlPropCount, CtlPropName$, CtlPropType, CtlEventList$, CtlEventParams$, CtlTypeEventParams$, FrmFormName$, FrmControlNames$, ClsClassName$, FrmOpen, FrmText$, FrmCtlAdd, FrmCtlText$, FrmCtlPaste, FrmMenuAdd, FormText$, ...) - shell.bas DVX itself, everything Program Manager and Task Manager do: start programs (DVX's and DOS programs) and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close, or end it), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, the DVX log; Program Manager's program groups with their items, shortcut keys, and icons (ShellRun, ShellRunProgram, ShellRunDos, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...) - pkg.bas Add-on packages: the packages installed in DVX and their details, files, and needs; reading .DPK files and planning what installing them takes; installing and removing as a job with progress; trusting a publisher's key; and making packages, signing them, and making keys (PkgOpen, PkgInstalledRows$, PkgOfferFile, PkgPlan, PkgInstallStart, PkgRunFor, PkgRemoveStart, PkgBuild, PkgSign, PkgKeygen, ...) + shell.bas DVX itself, everything Program Manager and Task Manager do: start programs (DVX's and DOS programs) and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close, or end it, and their priority), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, the DVX log; Program Manager's program groups with their items, shortcut keys, and icons (ShellRun, ShellRunProgram, ShellRunDos, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...) + pkg.bas Add-on packages: the packages installed in DVX and their details, files, and needs; reading .DPK files, installing, removing, making, and signing packages as jobs with progress, and planning what installing them takes; trusting a publisher's key; and making keys (PkgOpen, PkgInstalledRows$, PkgOfferFileStart, PkgPlan, PkgInstallStart, PkgRunFor, PkgRemoveStart, PkgBuildStart, PkgSignStart, PkgKeygen, ...) json.bas JSON: read a document (a web service's reply, a settings file) and find values in it by path such as packages[3].version, and quote text for JSON of your own (JsonParse, JsonText$, JsonNumber, JsonCount, JsonKey$, JsonQuote$, ...) http.bas Web requests: GET a page, a file, or a web service's answer, or POST to one, over the network a little at a time with progress, to memory or to a file (resuming one left part-way), following redirects (HttpOpen, HttpStart, HttpRunFor, HttpStatus, HttpBody$, HttpSaveTo, ...) pkgnet.bas A package server: what it offers, what it has that is newer than what is installed, and fetching packages with what they need, for pkg.bas to install (PkgNetOpen, PkgNetCatalogue, PkgNetUpdates, PkgNetFetch, PkgNetRunFor, PkgNetRows$, ...) @@ -5386,7 +5463,7 @@ End If .h2 Resources in Files -With resource.bas in the project, a program reads and writes the named resources DVX keeps at the end of program, widget, and library files: icons, text, and any bytes. ResOpen, ResCount, ResName$, ResType, and ResSize list them; ResGetText$ and ResAddText read and write text; ResAddFile and ResExtract copy a resource from or to a file; ResRemove takes one out. ResGetData$(file, name) gives every byte of a resource as a String (CHR$(0) included), and ResAddData file, name, type, bytes adds or replaces a resource of any bytes (type RES_TYPE_BINARY, RES_TYPE_TEXT, RES_TYPE_ICON, or RES_TYPE_FONT, a .DFN font the program carries; see Fonts). RES_NAME_ICON ("icon32") and RES_NAME_WIDGET_ICON ("icon24") are the names DVX gives a program's and a widget's icon; a name is at most RES_NAME_MAX - 1 characters. +With resource.bas in the project, a program reads and writes the named resources DVX keeps at the end of program, widget, and library files: icons, text, and any bytes. ResOpen, ResCount, ResName$, ResType, and ResSize list them; ResGetText$ and ResAddText read and write text; ResAddFile and ResExtract copy a resource from or to a file; ResRemove takes one out. ResGetData$(file, name) gives every byte of a resource as a String (CHR$(0) included) -- LoadPictureData(ResGetData$(file, name)) shows a picture resource -- and ResAddData file, name, type, bytes adds or replaces a resource of any bytes (type RES_TYPE_BINARY, RES_TYPE_TEXT, RES_TYPE_ICON, or RES_TYPE_FONT, a .DFN font the program carries; see Fonts). RES_NAME_ICON ("icon32") and RES_NAME_WIDGET_ICON ("icon24") are the names DVX gives a program's and a widget's icon; a name is at most RES_NAME_MAX - 1 characters. .code Dim settings As String @@ -5418,7 +5495,15 @@ End If After the handshake, data that arrives unencrypted is ignored unless that channel is opened for it with CommSetPlain link, channel, True. -CommHandshake waits until the key exchange is done. CommHandshakeStep(link, passphrase) does one step of it and returns at once, so a program can keep working: call it from a Timer until it returns something other than COMM_PENDING -- COMM_OK when the link is ready, else a COMM_ERR_ code. CommLastError(link) gives the COMM_ code of the last CommHandshake, CommHandshakeStep, CommSend, CommSetPlain, or CommPoll, which tells a wrong passphrase (COMM_ERR_AUTH) from a line that does not answer (COMM_ERR_TIMEOUT). CommDropped(link) counts arriving packets thrown away because they were tampered with or came unencrypted where that is refused. Channels are numbered 0 to COMM_MAX_CHANNEL. +The same link runs over the network: with net.bas in the project too, CommOpenTcp(conn) opens it on a connection from TcpConnect or TcpAccept (both ends do), with every channel, the handshake, and encryption as on a COM port. The connection stays the program's: close it with TcpClose after CommClose. + +.code +conn = TcpConnect("bbs.example.org", 2323) +link = CommOpenTcp(conn) +If CommHandshake(link, "shared secret") Then CommSend link, "hello", 5, True +.endcode + +CommHandshake waits until the key exchange is done (up to 30 seconds), and CommSend waits while the link has as much in flight as it can; the program's events run while they wait, as they do in DoEvents. CommHandshakeStep(link, passphrase) does one step of it and returns at once, so a program can keep working: call it from a Timer until it returns something other than COMM_PENDING -- COMM_OK when the link is ready, else a COMM_ERR_ code. CommLastError(link) gives the COMM_ code of the last CommHandshake, CommHandshakeStep, CommSend, CommSetPlain, or CommPoll, which tells a wrong passphrase (COMM_ERR_AUTH) from a line that does not answer (COMM_ERR_TIMEOUT). CommDropped(link) counts arriving packets thrown away because they were tampered with or came unencrypted where that is refused. Channels are numbered 0 to COMM_MAX_CHANNEL. .code Sub Timer1_Timer () @@ -5550,7 +5635,7 @@ End Sub .h2 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. +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. FileOpsListFolders(listing) makes a new listing of just the folders in one, sorted by name, without reading the disk again (handy for a folder tree next to a file list). 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. @@ -5560,7 +5645,7 @@ Drives: FileOpsDriveList$ (the drive letters), FileOpsDriveType (FILEOPS_DRIVE_F DVX's folders: FileOpsUserDir$() is the user folder (USER in the DVX folder, or the folder DVXUSER names), where everything DVX and its programs save goes; FileOpsUserPath$(name) is a file or folder in it, for example FileOpsUserPath$("DOWNLOAD"); FileOpsUserInTemp() is True when DVX runs from a disk it cannot write to and keeps the user folder in the TEMP folder. A program's own folder there is App.User. The fonts, sounds, themes, desktop pictures, MIDI instrument banks, and screen savers come in two folders each, DVX's own (and add-on packages') in SHARED and the user's in the user folder; the kinds are FILEOPS_KIND_FONTS, FILEOPS_KIND_SOUNDS, FILEOPS_KIND_THEMES, FILEOPS_KIND_WPAPER, FILEOPS_KIND_BANKS, and FILEOPS_KIND_SAVERS. An add-on's files are in a folder of its vendor's inside a kind's folder and are named VENDOR\NAME (for example ACME\SPACE.THM). FileOpsKindDir$(kind, user) is DVX's folder of a kind (user False) or the user's (True); FileOpsKindBrowseDir$(kind) is the one a file dialog starts in (the user's when it exists); FileOpsKindFind$(kind, fileName) finds a file by name, the user's first ("" when there is none); FileOpsKindName$(kind, path) is the name alone of a file in one of the two folders, the way settings store it ("" for any other path); FileOpsKindList(kind) lists both folders as one, sorted by name, a user's file hiding DVX's of the same name (a list for FileOpsListCount, FileOpsListName$, and the rest; FileOpsListFree when done). Programs read DVX's SHARED folder but never write in it. -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. +FileOpsCopyTree, FileOpsMoveTree, and FileOpsDeleteTree do a whole folder in one call, as does FileOpsTreeSize. While one works, the rest of DVX goes on and so do the program's own events, as with DoEvents (in a control's code they wait until the call ends); if the program is ended meanwhile, the call stops and returns False. For a long copy, move, or delete with progress and questions, run 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, FileOpsJobFoldersTotal, FileOpsJobPercent, FileOpsJobBytesDone, and FileOpsJobBytesTotal show the progress, and FileOpsJobCancel stops the job. A FILEOPS_MEASURE job only counts: the files (FileOpsJobFilesTotal), the folders inside what was added (FileOpsJobFoldersTotal), and the bytes (FileOpsJobBytesTotal), changing nothing and asking nothing; FileOpsJobUnread says how many folders could not be read. A Properties dialog counts a big folder this way without stopping the program. 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. .code Dim job As Long @@ -5593,7 +5678,7 @@ End Sub With shell.bas in the project, a program works with DVX the way Program Manager does -- a whole desktop can be written in BASIC. -Starting programs. ShellRun program, args starts a program (args is what it gets as COMMAND$), runs a DOS program, or opens a document in the program set for its type; it returns True when it started (or the DOS program will run). ShellRunProgram(program, args, folder) does the same the way a Program Manager item does: file names in args, and a document named without a folder, are looked for in folder first ("" for none), and it returns the new program's id (SHELL_RUN_DOS for a DOS program, 0 when it did not start; DVX has already said why). DOS programs are .EXE, .COM, and .BAT files (ShellIsDosProgram(path) says whether a file is one). ShellRunDos program, args, folder, pause runs one with DVX stepped aside: once your procedure has returned, DVX names the programs running besides the desktop, this one included, and asks them to close (each asks about unsaved work; No, or a program kept open, cancels the run); the screen then becomes the DOS screen until the program ends, and DVX comes back with the desktop. folder is where it runs ("" for its own folder); pause True shows "Press any key to return to DVX." when it ends. ShellMinimizeApp id runs that program minimized: its windows become icons, including the ones it opens while it starts. ShellRunCommand(commandLine) runs a whole command line (a program, in double quotes when its name has a blank, then its arguments) and returns the id too. ShellRunDialog$(history) shows DVX's Run dialog offering history (earlier commands, newest first, one per line) and returns what the user chose, "" for Cancel. ShellOpenDocument path opens a document in the program set for its type in the Control Panel. +Starting programs. ShellRun program, args starts a program (args is what it gets as COMMAND$), runs a DOS program, or opens a document in the program set for its type; it returns True when it started (or the DOS program will run). ShellRunProgram(program, args, folder) does the same the way a Program Manager item does: file names in args, and a document named without a folder, are looked for in folder first ("" for none), and it returns the new program's id (SHELL_RUN_DOS for a DOS program, 0 when it did not start; DVX has already said why). DOS programs are .EXE, .COM, and .BAT files (ShellIsDosProgram(path) says whether a file is one). ShellRunDos program, args, folder, pause runs one with DVX stepped aside: once your procedure has returned, DVX names the programs running besides the desktop, this one included, and asks them to close (each asks about unsaved work; No, or a program kept open, cancels the run); the screen then becomes the DOS screen until the program ends, and DVX comes back with the desktop. folder is where it runs ("" for its own folder); pause True shows "Press any key to return to DVX." when it ends. ShellRunDosWait(program, args, folder, pause) does the same and waits for it, as SHELL does: your program is not asked to close, its events run meanwhile, and the DOS program's exit code (its ERRORLEVEL) comes back, -1 when it did not run. ShellMinimizeApp id runs that program minimized: its windows become icons, including the ones it opens while it starts. ShellRunCommand(commandLine) runs a whole command line (a program, in double quotes when its name has a blank, then its arguments) and returns the id too. ShellRunDialog$(history) shows DVX's Run dialog offering history (earlier commands, newest first, one per line) and returns what the user chose, "" for Cancel. ShellOpenDocument path opens a document in the program set for its type in the Control Panel. Installed programs. ShellProgramScan(folder, skipPath) finds every program in folder and its subfolders ("" for the APPS folder) and returns a list: ShellProgramCount(list) programs, ShellProgramPath$(list, index) each one's path (index from 0), ShellProgramFree list when done (a list not freed is freed when the program ends). Give ShellAppPath$() as skipPath to leave this program out. ShellProgramName$(path) and ShellProgramDescription$(path) are the name and description a program shows, and ShellIsProgram(path) tells a program from a document. LOADPICTURE of a program gives its icon, so a PictureBox can show a grid of programs with PaintPicture. ShellProgramsFolder$() is the folder the installed programs are in (where ShellProgramScan("") looks), for example to start a Browse for a program there with DlgFileOpenIn$. @@ -5673,30 +5758,47 @@ End Sub .h2 Add-on Packages -With pkg.bas in the project, a program installs, removes, and makes add-on packages -- the .DPK files Software installs -- the way Software and Package Builder (both written in BASIC) do. PkgOpen() starts a session (a number; PkgClose ends it), and PkgError$(session) says why the last call that failed did. Lists come back as rows for a ListView's SetRows: cells separated by TAB, each row ending in a line feed. +With pkg.bas in the project, a program installs, removes, and makes add-on packages -- the .DPK files Software installs -- the way Software and Package Builder (both written in BASIC) do. PkgOpen() starts a session (a number; PkgClose ends it), and PkgError$(session) says why the last call that failed did. PkgOpenIn(root) starts one on another DVX folder (a copy being made ready on another disk, say): its installed packages and the keys its user folder trusts. Lists come back as rows for a ListView's SetRows: cells separated by TAB, each row ending in a line feed. What is installed. PkgInstalledRows$ lists every installed package (DVX itself is kpunch.dvx): id, name, version, kind, and summary. A package is named by its id, vendor.name (acme.chess). PkgField$(s, id, field) gives one detail (PKG_FIELD_NAME, PKG_FIELD_VERSION, PKG_FIELD_PUBLISHER, PKG_FIELD_SIGNER_NAME, ...); PkgFilesRows$, PkgDependsRows$, and PkgNeededBy$ its files, what it needs, and what needs it. PkgFileOwner$(s, fileName) is the other way: the id of the package that put a file there (its name from the DVX folder, such as "SHARED\FONTS\KPUNCH\ARIMO.DFN"; "" when none did). -Installing. PkgOfferFile(s, path) reads a package file and every package beside it (what it may need) as offers, numbered from 0, and returns the number of the one at path (-1 when it cannot be read). PkgOfferField$ and PkgOfferFilesRows$ describe an offer before it is installed; PKG_FIELD_TRUST says whether DVX trusts who signed it. PkgPlan(s, "0") plans installing it with everything it needs, checked against this DVX and the packages installed (False, with PkgError$ saying why, when it cannot be installed); PkgPlanRows$ lists what will go in, PkgPlanBytes the disk space, and PkgPlanRestart whether libraries or widgets are among them (they work from the next time DVX starts). PkgInstallStart(s) then starts the job, and PkgRunFor(s, milliseconds) runs it a little at a time, returning PKG_RUN_BUSY until it is PKG_RUN_DONE or PKG_RUN_FAILED (PkgRunMessage$ says why); PkgRunPercent and PkgRunCurrent$ show how far it is. +Jobs. Reading package files, installing, removing, and making and signing packages each take a while (checking a signature alone takes a moment on a 486), so each is a job: a ...Start function starts it, and PkgRunFor(s, milliseconds) runs it a little at a time, returning PKG_RUN_BUSY until it is PKG_RUN_DONE or PKG_RUN_FAILED (PkgRunMessage$ says why); PkgRunPercent and PkgRunCurrent$ show how far it is. A session runs one job at a time. + +Installing. PkgOfferFileStart(s, path) reads a package file and every package beside it (what it may need) as offers, numbered from 0; the one at path is 0, and the job fails when it cannot be read. PkgOfferField$ and PkgOfferFilesRows$ describe an offer before it is installed; PKG_FIELD_TRUST says whether DVX trusts who signed it. PkgPlan(s, "0") plans installing it with everything it needs, checked against this DVX and the packages installed (False, with PkgError$ saying why, when it cannot be installed); PkgPlanRows$ lists what will go in, PkgPlanBytes the disk space, and PkgPlanRestart whether libraries or widgets are among them (they work from the next time DVX starts). PkgInstallStart(s) then starts installing. .code -offer = PkgOfferFile(s, path) -If offer >= 0 And PkgPlan(s, CStr(offer)) Then - If PkgInstallStart(s) Then - Do - rc = PkgRunFor(s, 60) - pbJob.Value = PkgRunPercent(s) - DoEvents - Loop While rc = PKG_RUN_BUSY +Function RunJob(s As Long) As Long + Dim rc As Long + Do + rc = PkgRunFor(s, 60) + pbJob.Value = PkgRunPercent(s) + DoEvents + Loop While rc = PKG_RUN_BUSY + RunJob = rc +End Function + +If PkgOfferFileStart(s, path) Then + If RunJob(s) = PKG_RUN_DONE And PkgPlan(s, "0") Then + If PkgInstallStart(s) Then + rc = RunJob(s) + End If End If End If .endcode Removing. PkgDependents$(s, ids) lists the installed packages that need the ones going (they must go too), and PkgOrphans$(s, ids) those only they needed. PkgRemoveStart(s, ids, removeChanged) starts removing, run with PkgRunFor like an install; files changed since they were installed stay unless removeChanged, and PkgRunKept$ lists them. -Trust. A publisher signs a package with a key so people can tell it came from them. PkgTrustKey(s, keyPath) trusts a publisher's key file (.KEY) from then on. +Stopping. PkgRunCancel(s) stops a job after the package it is on: one still being unpacked is left as it was, and one whose files are already in is finished first, so keep calling PkgRunFor until it stops saying PKG_RUN_BUSY. A package being made or signed stops at once, and nothing of it is left behind. -From a package server. With pkgnet.bas in the project too, a program gets packages from a package server the way Software does. PkgNetOpen("") returns a handle on the server DVX's settings name, with the account they give; PkgNetClose frees it. PkgNetServer$, PkgNetPort, PkgNetUser$, and PkgNetPassword$ read those settings (PkgNetUrl$ is the address they make), and PkgNetSaveSettings(server, port, user, password) changes them. A job is started with PkgNetCatalogue (every package the server offers), PkgNetUpdates (what it has that is newer than the packages installed, DVX itself among them), or PkgNetFetch(n, ids) (packages by id, with everything they need that is not installed), and run with PkgNetRunFor(n, milliseconds) until it is PKGNET_DONE or PKGNET_FAILED (PkgNetError$ says why); PkgNetPercent and PkgNetCurrent$ show how far a fetch is. PkgNetRows$ lists the answer (id, name, version, kind, summary, size, and the version installed, if any), and PkgNetField$ and PkgNetDependsRows$ describe one package in it. Each package fetched is checked against the size and fingerprint the server gives, and the server's signature of it comes with it, so it is trusted or not exactly as a package file is. What was fetched is in PkgNetDir$: PkgOfferFolder(s, folder) reads every package there as the offers, and PkgOfferIndex(s, id) gives the number of the one wanted, to plan and install as above; PkgNetClear removes them afterwards. With an account (the settings', or PkgNetAccount(n, email, password) for another server), PkgNetUpload(n, path) publishes a package file for the server's review, and PkgNetMine(n) asks what the account has sent: PkgNetMineRows$ lists it (id, name, version, state, and the review's note); both sign in first. +Trust. A publisher signs a package with a key so people can tell it came from them. PkgTrustKey(s, keyPath) trusts a publisher's key file (.KEY) from then on. PkgKeysRows$(s) lists the keys DVX trusts (name, id, and "system" for DVX's own or "user" for one trusted this way), and PkgUntrustKey(s, keyId) stops trusting one of the user's. + +Checking a package. PkgVerifyStart(s, path, keyPath) checks a package file without installing it, a job run with PkgRunFor: every file in it against the size and fingerprint its manifest lists, so a damaged or changed package is found. With keyPath (a .KEY file) the package must also be signed by that key, in it or beside it, or nothing starts. + +Signing any data. PkgSignData$(s, bytes, secretPath) signs any bytes (a file read into a string, a message) with a secret key file, giving the signature as text; PkgCheckData(s, bytes, signature, keyPath) says whether a signature was made over those bytes by the key in keyPath, or with keyPath "" by a key DVX trusts. + +Plain ZIP files. A session also lists, unpacks, and makes ordinary .ZIP files, as jobs run with PkgRunFor: PkgZipListStart(s, zipPath) reads one to its end, every file in it checked; PkgZipExtractStart(s, zipPath, folder) unpacks it into folder (a name that would leave folder stops it); PkgZipCreateStart(s, zipPath, files) makes one from rows of a name in it and the file it comes from (as PkgFolderRows$ gives them). PkgZipRows$(s) lists what the last of them read or packed: name, size, date, and "folder" for a folder. + +From a package server. With pkgnet.bas in the project too, a program gets packages from a package server the way Software does. PkgNetOpen("") returns a handle on the server DVX's settings name, with the account they give; PkgNetClose frees it. PkgNetServer$, PkgNetPort, PkgNetUser$, and PkgNetPassword$ read those settings (PkgNetUrl$ is the address they make), and PkgNetSaveSettings(server, port, user, password) changes them. A job is started with PkgNetCatalogue (every package the server offers), PkgNetUpdates (what it has that is newer than the packages installed, DVX itself among them), or PkgNetFetch(n, ids) (packages by id, with everything they need that is not installed), and run with PkgNetRunFor(n, milliseconds) until it is PKGNET_DONE or PKGNET_FAILED (PkgNetError$ says why); PkgNetPercent and PkgNetCurrent$ show how far a fetch is. PkgNetRows$ lists the answer (id, name, version, kind, summary, size, and the version installed, if any), and PkgNetField$ and PkgNetDependsRows$ describe one package in it (PKG_FIELD_SHA256 is its file's fingerprint, PKG_FIELD_REPO_SIGNED whether the repository signed it). PkgNetOpenIn(server, root, downloadDir) works for another DVX folder and fetches into another folder. Each package fetched is checked against the size and fingerprint the server gives, and the server's signature of it comes with it, so it is trusted or not exactly as a package file is. What was fetched is in PkgNetDir$: PkgOfferFolderStart(s, folder) reads every package there as the offers (a job), and PkgOfferIndex(s, id) gives the number of the one wanted, to plan and install as above; PkgNetClear removes them afterwards. With an account (the settings', or PkgNetAccount(n, email, password) for another server), PkgNetUpload(n, path) publishes a package file for the server's review, and PkgNetMine(n) asks what the account has sent: PkgNetMineRows$ lists it (id, name, version, state, and the review's note); both sign in first. .code n = PkgNetOpen("") @@ -5707,17 +5809,18 @@ If PkgNetFetch(n, "acme.chess") Then DoEvents Loop While rc = PKGNET_BUSY End If -If rc = PKGNET_DONE And PkgOfferFolder(s, PkgNetDir$(n)) > 0 Then +If rc = PKGNET_DONE And PkgOfferFolderStart(s, PkgNetDir$(n)) Then + rc = RunJob(s) offer = PkgOfferIndex(s, "acme.chess") ' plan and install offer as above, then PkgNetClear n End If .endcode -Making packages. PkgBuild(s, template, files, out, secret) builds a package: template is a settings file with a [package] section (id, name, version, kind, and summary, publisher, author, licence, homepage, dvx as wanted) and a [depends] section (id = oldest version for each package it needs); files are rows of a name in the package (from the DVX folder, such as APPS\ACME\CHESS\CHESS.APP) and the file it is made from; secret is the secret key file (.SEC) to sign with, or "". PkgFolderRows$(s, folder, prefix) gives those rows for every file in a folder, and PkgKindFolder$(kind, id) the folder a package of that kind keeps its files in. PkgSign signs a package already built, PkgKeygen(s, name, base) makes a key pair (base.KEY to give out, base.SEC to keep private), and PkgKeyInfo$ reads a key file's name and id. PkgIdValid and PkgVersionValid check an id and a version before building; PkgKindName$ lists the kinds. +Making packages. PkgBuildStart(s, template, files, out, secret) starts building a package, a job run with PkgRunFor like an install (PkgRunCurrent$ is the file it is on): template is a settings file with a [package] section (id, name, version, kind, and summary, publisher, author, licence, homepage, dvx as wanted) and a [depends] section (id = oldest version for each package it needs); files are rows of a name in the package (from the DVX folder, such as APPS\ACME\CHESS\CHESS.APP) and the file it is made from; secret is the secret key file (.SEC) to sign with, or "". PkgFolderRows$(s, folder, prefix) gives those rows for every file in a folder, and PkgKindFolder$(kind, id) the folder a package of that kind keeps its files in. PkgSignStart(s, path, secret, detached) signs a package already built: into it, a job run the same way, or beside it (NAME.SIG) when detached, done at once. PkgKeygen(s, name, base) makes a key pair (base.KEY to give out, base.SEC to keep private), and PkgKeyInfo$ reads a key file's name and id. PkgIdValid and PkgVersionValid check an id and a version before building; PkgVersionCompare(a, b) says which of two versions is newer (-1, 0, or 1: "1.10.0" is newer than "1.9.2"); PkgKindName$ lists the kinds. .h2 Web Requests and JSON -With http.bas in the project, a program asks web servers for pages, files, and web services' answers. HttpOpen(method, url) makes a request ("GET" to fetch, "POST" to send) and returns a handle; HttpClose frees it. Before starting it, HttpAddHeader adds a header, HttpSetBody gives a body to send with its type ("application/json", say), HttpSaveTo writes the answer's body to a file instead of keeping it (with keepPart True, a file left part-way is finished rather than started again), and HttpSetTimeout sets how many seconds without progress mean the server is not answering (30 to start with). HttpStart starts it (False for an address that is not http://; there is no encryption), and HttpRunFor(h, milliseconds) runs it a little at a time, returning HTTP_BUSY until it is HTTP_DONE or HTTP_FAILED: +With http.bas in the project, a program asks web servers for pages, files, and web services' answers. HttpOpen(method, url) makes a request ("GET" to fetch, "POST" to send) and returns a handle; HttpClose frees it. Before starting it, HttpAddHeader adds a header, HttpSetBody gives a body to send with its type ("application/json", say), HttpSetBodyFile sends a file's bytes instead (a piece at a time, so a file of any size), HttpSaveTo writes the answer's body to a file instead of keeping it (with keepPart True, a file left part-way is finished rather than started again), and HttpSetTimeout sets how many seconds without progress mean the server is not answering (30 to start with). HttpStart starts it (False for an address that is not http://; there is no encryption), and HttpRunFor(h, milliseconds) runs it a little at a time, returning HTTP_BUSY until it is HTTP_DONE or HTTP_FAILED: .code h = HttpOpen("GET", "http://example.com/news.txt") @@ -5735,7 +5838,7 @@ HttpClose h HTTP_DONE means the server answered: HttpStatus is how (200 OK, 404 not found, ...), HttpBody$ what it sent, HttpReplyHeader$ one of its headers, and HttpUrl$ where the answer came from when the server sent the request elsewhere (redirects are followed). HTTP_FAILED means no answer came, and HttpError$ says why. HttpBytes and HttpTotal (-1 until the server says) show progress. -With json.bas in the project, JsonParse(text) reads JSON -- what most web services answer with -- and returns a handle (0 when it is not JSON: JsonError$ says why and on which line); JsonFree frees it. The other functions find a value by its path from the top: member names and item numbers from 0, such as "packages[3].version". JsonType says what is there (JSON_STRING, JSON_NUMBER, JSON_ARRAY, ..., JSON_NONE for nothing), JsonText$ gives a string, JsonNumber a number, JsonBool True or False, JsonCount how many items an array or members an object has, and JsonKey$(h, path, i) the name of an object's member i. JsonQuote$(text) puts text in quotes with its special characters escaped, for building JSON to send: +With json.bas in the project, JsonParse(text) reads JSON -- what most web services answer with -- and returns a handle (0 when it is not JSON: JsonError$ says why and on which line); JsonFree frees it. The other functions find a value by its path from the top: member names and item numbers from 0, such as "packages[3].version". JsonType says what is there (JSON_STRING, JSON_NUMBER, JSON_ARRAY, ..., JSON_NONE for nothing), JsonText$ gives a string, JsonNumber a number, JsonBool True or False, JsonCount how many items an array or members an object has, and JsonKey$(h, path, i) the name of an object's member i. JsonQuote$(text) puts text in quotes with its special characters escaped, for building JSON to send. Text comes and goes as DVX characters: an accented letter or a line-drawing character in a reply is the same character in JsonText$ ("?" stands for one DVX does not have), and JsonQuote$ writes such characters so that any web service reads them right: .code h = JsonParse(HttpBody$(req)) @@ -5766,7 +5869,7 @@ The pointer and the keyboard: SysMouseX() and SysMouseY() are where the mouse po Memory: SysMemoryTotalKb() is the computer's memory in KB and SysMemoryFreeKb() how much of it is free (-1 when it cannot be told). -Libraries: some parts of DVX are optional (Setup's "What to install" page; Software lists each as one package), and so are their libraries: Networking has dvxnet, http, and pkgnet (net.bas, http.bas, pkgnet.bas), Databases has dvxsql (sql.bas), and Communications has conn and xfer (conn.bas, xfer.bas). SysLibraryLoaded(libName) is True when the library a DECLARE LIBRARY line names (any case) is installed and was started with DVX. A program that can do without such a library asks first and leaves out what needs it: calling a function of a library that is not there stops the program with "External function not found". Software, for one, keeps its package server pages off without pkgnet. +Libraries: some parts of DVX are optional (Setup's "What to install" page; Software lists each as one package), and so are their libraries: Networking has dvxnet, http, and pkgnet (net.bas, http.bas, pkgnet.bas), Databases has dvxsql (sql.bas), and Communications has conn and xfer (conn.bas, xfer.bas). SysLibraryLoaded(libName) is True when the library a DECLARE LIBRARY line names (any case) is installed (DVX loads a library when it is first needed, so asking loads it). A program that can do without such a library asks first and leaves out what needs it: calling a function of a library that is not there stops the program with "External function not found". Software, for one, keeps its package server pages off without pkgnet. .code If SysLibraryLoaded("pkgnet") Then @@ -5915,6 +6018,26 @@ Sub Timer1_Timer () End Sub .endcode +A program can also hear of every change instead of reading the joystick: JoyWatch joy, True starts keeping that joystick's changes (False stops), and JoyEvent() gives the next one since it was last asked, each once and in order (the last 64 are kept): JOY_EVENT_AXIS (an axis moved), JOY_EVENT_BUTTON_DOWN, JOY_EVENT_BUTTON_UP, JOY_EVENT_HAT, or JOY_EVENT_NONE when nothing has changed. JoyEventJoy() and JoyEventIndex() say which joystick and which axis, button, or hat, and JoyEventValue() an axis's new position or a hat's direction. A button pressed and let go between two Timer ticks still gives both. + +.code +Sub Form1_Load () + ok = JoyWatch(1, True) +End Sub + +Sub Timer1_Timer () + Dim kind As Long + + kind = JoyEvent() + + Do While kind <> JOY_EVENT_NONE + If kind = JOY_EVENT_BUTTON_DOWN And JoyEventIndex() = 1 Then FireShot + If kind = JOY_EVENT_AXIS And JoyEventIndex() = 1 Then shipSpeed = JoyEventValue() \ 8192 + kind = JoyEvent() + Loop +End Sub +.endcode + When no joystick is connected the keyboard stands in for one (the arrow keys, with Left Ctrl, Space, Left Shift, and Z as buttons), and it only answers while one of the program's windows is active. Joysticks are calibrated, and the keyboard joystick's keys chosen, on the Control Panel's Game Controllers tab. QBasic's STICK and STRIG work too, without joystick.bas. joystick.bas can also set joysticks up, as the Control Panel does. JoyHats(joy) counts the hats, JoyAxisPresent(joy, axis) is True when an axis answers, JoyConnected(joy) is False once a joystick stops answering, and JoyCanCalibrate(joy) is True for one with analog axes. For calibrating, JoyRawAxis(joy, axis) gives an axis's raw reading and JoyRawButtons(joy) the buttons held (1 for button 1, 2 for button 2, 4 for button 3, ... added up); JoySetCalibration(joy, axis, minV, centerV, maxV) takes the readings at one end, the middle, and the other end, and JoyClearCalibration(joy, axis) lets the axis find its own range again. JoyDeadZone(joy) and JoySetDeadZone(joy, percent) are the dead zone round the middle (0 to 50 percent). JoyChoiceCount, JoyChoiceLabel$, JoyChoice, and JoySetChoice list and change the game port's mode (JOY_CHOICE_GAMEPORT) and when the keyboard joystick stands in (JOY_CHOICE_FALLBACK); JoyKeyCount and JoyKeyName$(index) list the keys the keyboard joystick can use, and JoyKbdSlot(slot) and JoySetKbdSlot(slot, keyIndex) its keys for up, down, left, right, and buttons 1 to 4 (slots 0 to 7). These lists are numbered from 0, so they fill a DropDown in order. JoyKeyIndex(keyName) gives a key's number by its name (-1 when unknown), JoyKeyCode(index) its raw key code, which GameKeyDown in game.bas takes, and JoyKeyNameOf$(rawCode) the name of a raw key code. JoyIsKeyboard(joy) is True for the keyboard standing in for a joystick, and JoyCalibration(joy, axis, part) reads back an axis's calibration: part JOY_CAL_MIN, JOY_CAL_CENTER, or JOY_CAL_MAX of the readings set with JoySetCalibration, or of the range the axis found by itself (0 for a joystick that cannot be calibrated). JoySetting$(key, default) and JoySetSetting key, value reach any other joystick setting; JOY_KEY_POLL_HZ is the key of how often joysticks are read, JOY_POLL_HZ_MIN to JOY_POLL_HZ_MAX times a second (JOY_POLL_HZ_DEFAULT normally), and dead zones go up to JOY_DEADZONE_MAX percent (JOY_DEADZONE_DEFAULT normally). After a new game port mode, JoyRescan() looks for joysticks again; it returns the new count, or -1 while another program is using a joystick. Every change applies at once; JoySettingsStore handle puts them all into a settings handle from prefs.bas for the program to save, and JoySettingsLoad handle takes them back from one, to undo. @@ -5954,9 +6077,22 @@ End Sub WAV files must be uncompressed (8 or 16 bits, mono or stereo); VOC files may be 8- or 16-bit. QBasic's BEEP, SOUND, and PLAY work too, without sound.bas. -PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator), "running", and "record" (it records sound) are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits", "channels", and "recordsources" (the inputs it records from) are numbers. SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played. +PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator), "running", and "record" (it records sound) are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits", "channels", and "recordsources" (the inputs it records from) are numbers. SoundDriver$() is the short name of the sound driver playing ("sb", "speaker", ...; "" when none is), and SoundStats(field) what the mixer has done since the device started: "renders" (blocks of sound made), "frames" (samples made), "underruns" (stream samples that were due but not yet written), "clipped" (samples too loud), "dropped" (sounds dropped because too many played at once), and "playing" (voices playing now). SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played (DVX and the program's events go on meanwhile, as in DoEvents; in a control file's code, and once the program is ending, it does not wait). -A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan, and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. SoundPause voice holds a sound's voice where it is, silent, and SoundResume voice goes on from there (a tone cannot be paused). A program's sounds are freed when it ends. +A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); it reads and decodes the whole file before it returns, so for a long recording (a minute or more) use MusicLoad and MusicPlay (music.bas), which play a WAV or VOC file as they read it; SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundLoadData(bytes) loads one from the bytes of a WAV or VOC file held in a string, such as ResGetData$ (resource.bas) or HttpBody$ (http.bas) gives. SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds, SoundRate(sound), SoundBits(sound), and SoundChannels(sound) how it was recorded, and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan, and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. SoundPause voice holds a sound's voice where it is, silent, and SoundResume voice goes on from there (a tone cannot be paused). A program's sounds are freed when it ends. + +To hear of every sound ending, call SoundEnded() from a Timer: it gives the next of the program's voices that ended since it was last asked (each once, in the order they ended; the last 64 are kept), or 0 when none has, and SoundEndReason() says why that one ended: SOUND_END_FINISHED (it played to its end), SOUND_END_STOPPED (StopWave, StopAllSounds, or its sound was freed), or SOUND_END_DROPPED (too many sounds played at once and it was the quietest). + +.code +Sub Timer1_Timer () + Dim v As Long + v = SoundEnded() + Do While v <> 0 + If v = engine And SoundEndReason() = SOUND_END_FINISHED Then engine = SoundPlay(idle, 60, 0, True) + v = SoundEnded() + Loop +End Sub +.endcode .code Dim Shared laser As Long @@ -5983,7 +6119,16 @@ Sub Timer1_Timer () End Sub .endcode -A program can record sound from the sound card's microphone, line, or CD input into a WAV file. SoundCaps("record") says whether the card can, and SoundCaps("recordsources") which inputs it has (the bit 2 ^ RECORD_MIC, RECORD_LINE, or RECORD_CD set for each). RecordStart(fileName, rate, bits, channels, inputSource) starts recording (0 for 22,050 Hz, 16-bit, mono) and returns True, or False with SoundError$ saying why; the card records at its nearest, which RecordRate(), RecordBits(), and RecordChannels() tell. While it records, RecordLength() is how long the recording is (milliseconds) and RecordLevel() the loudest sound since it was last asked (percent: a level meter); RecordLost() counts samples dropped because DVX was too busy. RecordStop() ends it and returns True when the file was all written; Recording() is True in between. Most sound cards record or play, not both, so nothing plays while a program records, and one program records at a time. A program's recording ends when the program does. +A melody of tones -- a game's tune or a sound effect -- plays without a gap or a stumble in a note queue. NotesOpen(wave, volume) opens one in a waveform (SOUND_WAVE_SQUARE, SOUND_WAVE_TRIANGLE, or SOUND_WAVE_NOISE) and returns its number, or 0. NotesAdd(queue, hertz, milliseconds) adds a tone (0 hertz for a rest; fractions count) and returns True, or False when 256 are already waiting; each lasts exactly as long as asked, as long as the next is added before the one playing ends. NotesSetTone(queue, wave, volume) changes the waveform and volume (percent of the queue's) of the tones added from then on, so they change in time with the tune. NotesQueued(queue) is how many tones and rests have not finished (0 once it has gone quiet) and NotesSpace(queue) how many more fit; NotesVoice(queue) is its voice, for SoundSetVolume and SoundSetPan. NotesClear queue silences it and forgets what it holds, and NotesClose queue closes it. PLAY's V and @ commands give PLAY tunes a volume and a waveform too. + +.code +q = NotesOpen(SOUND_WAVE_TRIANGLE, 80) +For f = 200 To 800 Step 50 + ok = NotesAdd(q, f, 15) ' a rising sweep, 15 ms a step +Next f +.endcode + +A program can record sound from the sound card's microphone, line, or CD input into a WAV file, or into memory. SoundCaps("record") says whether the card can, and SoundCaps("recordsources") which inputs it has (the bit 2 ^ RECORD_MIC, RECORD_LINE, or RECORD_CD set for each). RecordStart(fileName, rate, bits, channels, inputSource) starts recording (0 for 22,050 Hz, 16-bit, mono) and returns True, or False with SoundError$ saying why; the card records at its nearest, which RecordRate(), RecordBits(), and RecordChannels() tell. While it records, RecordLength() is how long the recording is (milliseconds) and RecordLevel() the loudest sound since it was last asked (percent: a level meter); RecordLost() counts samples dropped because DVX was too busy. RecordStop() ends it and returns True when the file was all written; Recording() is True in between. With a file name of "", RecordStart records into memory instead: RecordRead$(maxBytes) takes up to maxBytes of what was recorded and not yet taken, as raw sound data in the recording's format (as SoundCreate and SoundStreamWrite take it), and RecordQueued() is how many bytes are waiting. Take them often, from a Timer: about two seconds are kept, and what does not fit is lost (RecordLost()). A program can show the sound as it comes, change it, or send it over the network. Most sound cards record or play, not both, so nothing plays while a program records, and one program records at a time. A program's recording ends when the program does. .code If RecordStart("NOTE.WAV", 0, 0, 0, RECORD_MIC) Then @@ -5994,7 +6139,7 @@ End If On a computer with an FM synthesizer chip (AdLib and most Sound Blasters), a program can play instruments of its own. FmInfo$(field) describes the chip: "description" (like "OPL3 FM synthesizer at 388h", "" when there is none), "voices", "free" (voices no program has), "stereo", and "owned" (one program has the whole chip). FmAlloc() takes one of its two-operator voices (0 when none is free), FmWrite(voice, register, value) sets one of the voice's registers (the FM_ constants: FM_MOD_CHAR, FM_CAR_LEVEL, FM_FNUM_LOW, FM_KEY_BLOCK with FM_KEY_ON, ...; True when it worked), and FmFree voice gives it back. A player of chip music files can take the whole chip with FmRawAcquire() (False while another program has it), set any register with FmRawWrite(register, value) (bank * FM_BANK_REGS + register; the second bank only on an OPL3) and give it back with FmRawRelease. A frequency number for FM_FNUM_LOW and FM_KEY_BLOCK is hertz * 2 ^ 20 / (FM_CHIP_HZ * 2 ^ block). -On a wavetable sound card (a Gravis UltraSound, a Sound Blaster AWE32 or AWE64), a program can play recorded sounds on the card's own voices from the card's memory. WtInfo$(field) describes the card: "description" ("" when there is none), "voices", "free" (voices no program has), "memory" and "freememory" (bytes), "level" (how many centibels louder a voice at full volume is than the loudest recorded sound the card plays: 0 when it is the same or not known), and what the card does itself ("envelopes", "lfos", "filter", "effects", "8bit", "rom"; True or False as text). WtMemAlloc(bytes, bits) takes memory for samples of 8 or 16 bits (its address, -1 when there is no room), WtUpload(address, pcm, bits) copies 16-bit mono samples there (a binary string, two bytes a sample; True when it worked), and WtMemFree address gives it back. WtAlloc() takes a voice (0 when none is free); WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) plays a sample on it from startFrame (0: its first; loopMode 0 plays once, 1 loops, 3 loops until WtRelease; pitch in cents, 0 for 44,100 samples a second and 1200 an octave higher; attenuation in centibels, 0 loudest and 60 half as loud; pan -100 to 100). WtSetPitch, WtSetAttenuation, and WtSetPan change a playing voice, WtStop silences it, WtActive(voice) is True while it sounds, and WtFree voice gives it back. Everything a program took is given back when it ends. +On a wavetable sound card (a Gravis UltraSound, a Sound Blaster AWE32 or AWE64), a program can play recorded sounds on the card's own voices from the card's memory. WtInfo$(field) describes the card: "description" ("" when there is none), "voices", "free" (voices no program has), "memory", "freememory" (the largest free block), and "freetotal" (all free, in blocks maybe apart; bytes), "level" (how many centibels louder a voice at full volume is than the loudest recorded sound the card plays: 0 when it is the same or not known), and what the card does itself ("envelopes", "lfos", "filter", "effects", "8bit", "rom"; True or False as text). WtMemAlloc(bytes, bits) takes memory for samples of 8 or 16 bits (its address, -1 when there is no room), WtUpload(address, pcm, bits) copies 16-bit mono samples there (a binary string, two bytes a sample; True when it worked), and WtMemFree address gives it back. WtAlloc() takes a voice (0 when none is free); WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) plays a sample on it from startFrame (0: its first; loopMode 0 plays once, 1 loops, 3 loops until WtRelease; pitch in cents, 0 for 44,100 samples a second and 1200 an octave higher; attenuation in centibels, 0 loudest and 60 half as loud; pan -100 to 100); on a card with a General MIDI sample ROM (WtInfo$("rom")) the address may be WT_ADDR_ROM plus the bytes from the ROM's start. WtSetPitch, WtSetAttenuation, and WtSetPan change a playing voice, WtStop silences it, WtActive(voice) is True while it sounds, and WtFree voice gives it back. WtShape(voice, field, value) shapes the voice's notes as a SoundFont instrument does, one value by name, where the card can (WtInfo$ "envelopes", "lfos", "filter", "effects"): the volume envelope ("voldelay", "volattack", "volhold", "voldecay", "volsustain", "volrelease"), the modulation envelope ("moddelay" to "modrelease", "modenvpitch", "modenvfilter"), the modulation and vibrato LFOs ("modlfodelay", "modlfofreq", "modlfopitch", "modlfofilter", "modlfovolume", "viblfodelay", "viblfofreq", "viblfopitch"), the filter ("filterfc", "filterq"), and the effects ("reverb", "chorus"), in the units sound.bas lists. A value is kept for the voice's next WtStart; the filter, the effects, and the LFO depths also change a note that is sounding. Everything a program took is given back when it ends. sound.bas also changes DVX's own sound settings, as the Control Panel's Sounds page does. DVX plays a sound for each of SoundEventCount() events, numbered from 0: SoundEventLabel$(eventIdx) names one ("Start DVX", "Error", ...) and SoundEventKey$(eventIdx) gives its key in DVX.INI. With a settings handle from prefs.bas (PrefsLoad(PrefsDvxIni$())), SoundEventFile$(handle, eventIdx) is the event's sound file ("" for none) and SoundSetEventFile handle, eventIdx, fileName changes it; after the program saves the file, SoundEventsReload makes DVX play the new sounds. SoundTestFile(fileName) plays a file the way DVX plays its event sounds (0 when it cannot, and SoundError$() says why), and SoundFolder$() is the folder of DVX's own sounds. SoundMasterVolume() and SoundSetMasterVolume percent are the volume of all sound from every program. SoundDriverCount() reads the list of sound drivers and SoundDriverName$(index) names each; SoundDriverAuto$() is the setting that means "find the best one". SoundMidiCount() and SoundMidiLabel$(index) list where MIDI music can play, SoundMidiInCount() and SoundMidiInLabel$(index) where MIDI input can come from, and SoundModulesCount() and SoundModulesLabel$(index) where music modules can play. SoundSettingDriver$(handle), SoundSettingMidi(handle), SoundSettingModules(handle), and SoundSettingVolume(handle) read the sound driver, MIDI choice, modules choice, and master volume from the settings, and SoundSettingsStore handle, driver, midi, modules, volume stores them (a new driver or MIDI choice takes effect when DVX next starts, a modules choice with the next music); SoundSettingMidiIn(handle) and SoundSettingMidiInStore handle, midiIn read and store the MIDI input choice (it takes effect when DVX next starts). @@ -6015,7 +6160,7 @@ Next i With music.bas in the project, a program plays music files: tracker modules (MOD, S3M, XM, and IT files, made of recorded instrument sounds) wherever the computer can play sampled sound (on a Gravis UltraSound's or Sound Blaster AWE's own voices, which takes the computer little time), MIDI files (.MID, types 0 and 1) on the computer's music synthesizer, or on a built-in synthesizer that sounds much the same when there is none, and WAV and VOC sound files wherever the computer can play sampled sound (a long WAV recording is read from the disk as it plays, so it needs no memory). Several programs can play music at once, and everything a program plays stops when it ends. -MusicLoad(fileName) loads a music file and returns its music number, or MUSIC_FAILED (0) when it could not be loaded; MusicError$() then says why. MusicPlay(music, loopIt) plays it from where it is (the start after loading or stopping); with loopIt True it starts over at the end until it is stopped. MusicStop(music) stops and goes back to the start, MusicPause(music) and MusicResume(music) pause and go on, and MusicPlaying(music) is True while it plays. MusicVolume(music, volume) sets its volume in percent (0 to 100). MusicPosition(music) and MusicLength(music) give the place and the length in milliseconds, and MusicSeek(music, milliseconds) goes to a place (a module goes to the start of the part that holds it). MusicFree(music) stops a music and forgets it. +MusicLoad(fileName) loads a music file and returns its music number, or MUSIC_FAILED (0) when it could not be loaded; MusicError$() then says why. MusicLoadData(bytes) does the same with the bytes of a music or sound file held in a string (from ResGetData$ or HttpBody$, say). MusicPlay(music, loopIt) plays it from where it is (the start after loading or stopping); with loopIt True it starts over at the end until it is stopped. MusicStop(music) stops and goes back to the start, MusicPause(music) and MusicResume(music) pause and go on, and MusicPlaying(music) is True while it plays. MusicEnded(music) is True once after the music played to its end (not when it was stopped) since MusicPlay started it: a Timer that asks it can start the next tune on time. MusicVolume(music, volume) sets its volume in percent (0 to 100). MusicPosition(music) and MusicLength(music) give the place and the length in milliseconds, and MusicSeek(music, milliseconds) goes to a place (a module goes to the start of the part that holds it). MusicFree(music) stops a music and forgets it. MusicLoadMacro(macro) makes a music from a music macro string, the kind PLAY takes, with its notes on the General MIDI instruments its I commands name (the piano before any I): the tune can then be played in the background, paused, stopped, and made quieter like any music. MIDI music plays on the instruments chosen in Control Panel; MusicSetBank music, bank plays one music on another instrument bank, named as in the banks folders ("" is DVX's standard bank), before the music first plays (MusicError$ says when the bank is missing or damaged). A SoundFont (.SF2 or .SBK) gives the recorded instruments for a wavetable card or the built-in SoundFont synthesizer, as can a Gravis UltraSound's own patches: MusicPatchSet$() names them (their ULTRASND.INI, "" when there are none), and MusicRomBank$() names the instruments built into a Sound Blaster AWE card ("" when Creative's software is not installed). @@ -6129,16 +6274,24 @@ End Sub .table Function Description -------- ----------- - HostLookup$(hostName) The address of a name, such as "192.0.2.10", or "" when it is not found. + HostLookup$(hostName) The address of a name, such as "192.0.2.10", or "" when it is not found. Waits for the answer; other programs keep running. + HostLookupStart(hostName) The same without waiting: a lookup handle (0 when too many are under way). + HostLookupPoll(lookup) NET_BUSY while the lookup goes on, NET_DONE once found, NET_FAILED (NetError$ says why). + HostLookupResult$(lookup) The address found, once HostLookupPoll says NET_DONE. + HostLookupClose lookup Free a lookup handle (or give the lookup up). HostName$(address) The name of an address; the address itself when no name is known for it. NetAddress$() This computer's address ("" while it has none). NetError$() Why the last call failed, in plain words. TcpConnect(host, port) Connect; a handle, or 0. Waits up to 20 seconds; other programs keep running. + TcpConnectStart(host, port) The same without waiting: the connection's handle at once (0 when it cannot start). + TcpConnectPoll(handle) NET_BUSY while the host is looked up and the connection made, NET_DONE once it is (the handle then works as TcpConnect's), NET_FAILED (close it). TcpListen(port) Wait for connections on port; a listening handle, or 0. TcpAccept(handle) A connection that arrived on a listening handle, or 0 when none is waiting. - TcpSend(handle, bytes) Send text or any other bytes; the bytes sent, or -1. + TcpSend(handle, bytes) Send text or any other bytes; the bytes sent, or -1. Waits while the connection's buffer is full. + TcpSendSome(handle, bytes) Send what the connection takes now, never waiting: the bytes taken from the start (0 while it is full), or -1. TcpPending(handle) Bytes waiting to be read (-1 for a bad handle). TcpRecv$(handle) What has arrived (up to 4096 bytes), or "". + TcpPeek$(handle) What has arrived (up to 4096 bytes), left waiting for the next TcpRecv$: look for a line's end or a message's length first. TcpConnected(handle) 1 while the connection is open, 0 once the other side closed it. TcpShutdown(handle) Tell the other side nothing more will be sent; what it sends can still be read. TcpPeer$(handle) "address:port" of the other side ("" when not connected). @@ -6155,6 +6308,29 @@ End Sub The strings sent and received may hold any bytes, CHR$(0) too, so binary protocols work as well as text. Every connection a program opens is closed for it when the program ends. +HostLookup$, TcpConnect, and TcpSend wait until they are done. Other programs keep running meanwhile, but yours does not: its windows do not answer, and the first lookup after DVX starts can also wait for the network to get its settings (DHCP). To stay responsive, start the lookup or connection and check on it from a Timer: + +.code +Dim conn As Long + +Sub cmdConnect_Click () + conn = TcpConnectStart("bbs.example.org", 23) + Timer1.Enabled = conn <> 0 +End Sub + +Sub Timer1_Timer () + Select Case TcpConnectPoll(conn) + Case NET_DONE + Timer1.Enabled = False + lblStatus.Caption = "Connected" + Case NET_FAILED + Timer1.Enabled = False + lblStatus.Caption = "Could not connect: " & NetError$() + TcpClose conn + End Select +End Sub +.endcode + A program can also edit the network settings the way the Control Panel's Network page does, and show the network's state. The settings are kept in DVX's settings file, so NetConfigLoad and NetConfigStore take a handle from prefs.bas (add prefs.bas to the project too). NetConfigLoad gives a settings handle (0 when none is free); read and change its fields by name, check them, store them, and restart the network only when they changed: .code @@ -6198,6 +6374,7 @@ End Sub NetVectorCount() / NetVector(index) The packet driver interrupts to choose from: index 0 is 0 (find it), then each one the settings accept. NetRestart() Stop the network and start it with the saved settings. True when it is running. NetStop Stop the network (every program's connections are closed) until NetRestart. + NetState() The network's state as a number: NET_STATE_STOPPED, NET_STATE_NO_DRIVER (no network card driver found), NET_STATE_CONFIGURING (waiting for an address from the network), or NET_STATE_UP. NetStatus$(field) The network's state as text: "message" (in plain words), "up" and "link" ("-1" / "0"), "dhcp", "address", "netmask", "gateway", "dns1", "dns2", "hostname", "driver" (the network card's driver), "mac" (its hardware address), "vector", "sockets", "rxpackets", "rxbytes", "rxdropped", "txpackets", "txbytes". "" for an unknown field. .endtable @@ -6239,7 +6416,7 @@ End Sub .h2 File Transfers -With xfer.bas in the project, a program sends and receives files with XMODEM, XMODEM-1K, YMODEM, or ZMODEM. A transfer is a number: XferSendFile and XferReceive start one on a serial port opened with SerOpen (comm.bas), and ConnSendFiles and ConnReceiveFiles start one on a connection (conn.bas), with the options Terminal has: escaping every control character, asking the receiver to continue interrupted files, and what to do with a file that is already there. On a serial port XferPoll moves the transfer along; on a connection ConnPoll does. XferStatus tells where it is (XFER_RUNNING until it ends with XFER_DONE, XFER_FAILED, or XFER_CANCELLED), and XferBytes, XferSize, XferFile$, XferFileIndex, XferFileCount, XferBatchBytes, XferBatchSize, XferResumedAt, and XferErrors show its progress; XferMessage$ says how it ended. A receive with XFER_EXIST_ASK stops in XFER_ASKING when a file already exists: XferAskSize, XferAskRemoteSize (the size the sender offers, -1 when it did not say), and XferCanResume describe it, and XferAnswer continues with an XFER_EXIST_ choice. XferCancel stops a transfer and XferClose frees it. +With xfer.bas in the project, a program sends and receives files with XMODEM, XMODEM-1K, YMODEM, or ZMODEM. A transfer is a number: XferSendFile and XferReceive start one on a serial port opened with SerOpen (comm.bas), and ConnSendFiles and ConnReceiveFiles start one on a connection (conn.bas). XferSendFiles and XferReceiveFiles (a serial port) and the Conn ones take the options Terminal has: a batch of files (one path per line), escaping every control character, asking the receiver to continue interrupted files, and what to do with a file that is already there. On a serial port XferPoll moves the transfer along; on a connection ConnPoll does. XferStatus tells where it is (XFER_RUNNING until it ends with XFER_DONE, XFER_FAILED, or XFER_CANCELLED), and XferBytes, XferSize, XferFile$, XferFileIndex, XferFileCount, XferBatchBytes, XferBatchSize, XferResumedAt, and XferErrors show its progress; XferMessage$ says how it ended. A receive with XFER_EXIST_ASK stops in XFER_ASKING when a file already exists: XferAskSize, XferAskRemoteSize (the size the sender offers, -1 when it did not say), and XferCanResume describe it, and XferAnswer continues with an XFER_EXIST_ choice. XferCancel stops a transfer and XferClose frees it. XferCrc16(bytes) and XferCrc32(bytes) give the check values the protocols use (CRC-16 as XMODEM computes it, 0 to 65535; the CRC-32 of ZMODEM and ZIP files as a Long, negative when its top bit is set) for any bytes. XferSafeName$(name, folder) gives the name a receive into folder stores a sender's file name as: no folder or device name; a long name, its case kept, where folder's disk takes long names (a character a name cannot hold becomes _), else an 8.3 name of characters every disk accepts. @@ -6321,11 +6498,20 @@ With security.bas in the project, a program has the building blocks of DVX's enc Function Description -------- ----------- SecRandom$(count) count random bytes, good for keys and salts (RND is not). SecAddEntropy bytes stirs something unpredictable (key timings, mouse positions) into the generator. - SecDhCreate() A key exchange with fresh keys (a few seconds on a slow computer); a handle, or 0. + SecX25519PublicKey$(secretKey) The public key to send the other side, for a secret key of SEC_X25519_KEY_SIZE random bytes (SecRandom$). + SecX25519$(secretKey, otherPublicKey) The secret both sides now share, which nobody listening can work out ("" for a key the other side should not have sent). About a sixth of a second on a slow computer. Make keys from it with SecHkdf$. + SecX25519Start(secretKey, otherPublicKey) The same without the wait: SecX25519Step(h) then does a ninth of the work a call, returning SEC_X25519_PENDING until it is done (SEC_OK); SecX25519End$(h) gives the secret (with otherPublicKey "", the public key) and frees the handle. + SecAeadLock$(key, nonce, extra, bytes) bytes sealed: encrypted with a SEC_AEAD_KEY_SIZE-byte key and a SEC_AEAD_NONCE_SIZE-byte nonce, followed by a tag that proves them, and extra (sent in the clear), unchanged. Use a new nonce for every message: SecRandom$(SEC_AEAD_NONCE_SIZE) is safe. + SecAeadUnlock$(key, nonce, extra, sealed) What SecAeadLock$ sealed, or "" when anything was changed (or the key, nonce, or extra differs). + SecEd25519KeyPair$(seed) A signing key made from SecRandom$(SEC_ED25519_SEED_SIZE); keep it private. SecEd25519PublicKey$(key) is the key to give out. + SecEd25519Sign$(key, bytes) The signature of bytes; SecEd25519Check(signature, publicKey, bytes) is True only for exactly those bytes signed with that key. + SecDhCreate() The older key exchange (Diffie-Hellman), with fresh keys (a few seconds on a slow computer); a handle, or 0. + SecDhCreateStart() The same without the wait: SecDhStep(dh) then computes the keys a slice at a time, returning SEC_DH_PENDING until they are ready (SEC_OK). SecDhPublicKey$(dh) This side's public key (SEC_DH_KEY_SIZE bytes), to send to the other side. SecDhComputeSecret(dh, otherPublicKey) Combine with the other side's public key: SEC_OK, or SEC_ERR_PARAM for a key that is not acceptable. + SecDhComputeStart(dh, otherPublicKey) The same a slice at a time: SEC_DH_PENDING (or SEC_ERR_PARAM at once), then SecDhStep as above. SecDhSecret$(dh) The secret both sides now share, which nobody listening can work out. Make keys from it with SecHkdf$; never use it as a key itself. SecDhFree dh frees the handle. - SecCipherCreate(key) A cipher with a SEC_XTEA_KEY_SIZE-byte key; 0 for a key of another length. + SecCipherCreate(key) The older cipher (XTEA), with a SEC_XTEA_KEY_SIZE-byte key; 0 for a key of another length. It does not show tampering: add SecHmac$, or seal instead. SecCipherCrypt$(cipher, bytes) Encrypt -- or decrypt, the same call -- bytes. SecCipherSetCounter cipher, n sets the position in the key stream; the same key and position must never encrypt two different messages. SecCipherFree cipher frees it. SecSha256$(bytes) The SHA-256 hash (SEC_SHA256_SIZE bytes). For data in pieces: SecSha256Open, SecSha256Add h, piece, then SecSha256End$(h), which also frees the handle. SecHmac$(key, bytes) The HMAC-SHA256 signature of bytes with key; SecHmacOpen(key), SecHmacAdd, and SecHmacEnd$ for pieces. @@ -6334,16 +6520,15 @@ With security.bas in the project, a program has the building blocks of DVX's enc .endtable .code -' Both sides: make keys, swap public keys (here over a TCP connection), agree a cipher key -Dim dh As Long, cipher As Long, key As String -dh = SecDhCreate() -TcpSend conn, SecDhPublicKey$(dh) -' ... receive the other side's key into theirs ... -If SecDhComputeSecret(dh, theirs) = SEC_OK Then - key = SecHkdf$("", SecDhSecret$(dh), "chat cipher", SEC_XTEA_KEY_SIZE) - cipher = SecCipherCreate(key) -End If -SecDhFree dh +' Both sides: make keys, swap public keys (here over a TCP connection), agree a sealing key +Dim mine As String, theirs As String, key As String, nonce As String +mine = SecRandom$(SEC_X25519_KEY_SIZE) +TcpSend conn, SecX25519PublicKey$(mine) +' ... receive the other side's public key into theirs ... +key = SecHkdf$("", SecX25519$(mine, theirs), "chat", SEC_AEAD_KEY_SIZE) +' Send a message: the nonce, then the sealed bytes +nonce = SecRandom$(SEC_AEAD_NONCE_SIZE) +TcpSend conn, nonce & SecAeadLock$(key, nonce, "", "Hello") .endcode Handles a program leaves open are freed, and their keys wiped, when it ends. @@ -6376,7 +6561,7 @@ cboAccel.ListIndex = PrefsChoiceIndexOfValue(PREFS_CHOICE_ACCEL, SysMouseAccel() .h2 Help Files -With help.bas in the project, HelpCompile(source, output) compiles a help source file (.dhs) into a help file (.hlp), True when it worked; HelpCompileMessages$() gives the compiler's errors and warnings, one per line, each with the file and line ("error: NOTES.DHS:1: directive .h1 outside of a topic"). HelpView file opens a help file in the DVX Help Viewer (a file named without a folder is looked for beside the program; "" opens the DVX user guide). HelpViewTopic file, topicId opens it at a topic, the way F1 does. A Help Viewer that is already open shows the file in its window instead of a second copy starting. +With help.bas in the project, HelpCompile(source, output) compiles a help source file (.dhs) into a help file (.hlp), True when it worked; its .image pictures are looked for in the source's folder. HelpCompileMessages$() gives the compiler's errors and warnings, one per line, each with the file and line ("error: NOTES.DHS:1: directive .h1 outside of a topic"). While a big file compiles, the rest of DVX and the program's own events go on, as with DoEvents: one compile runs at a time, so a second HelpCompile meanwhile returns False at once, and the compile stops if the program ends. HelpView file opens a help file in the DVX Help Viewer (a file named without a folder is looked for beside the program; "" opens the DVX user guide). HelpViewTopic file, topicId opens it at a topic, the way F1 does. A Help Viewer that is already open shows the file in its window instead of a second copy starting. With hlpfile.bas, a program reads help files itself -- everything a help viewer needs. HlpOpen(path) opens a help file and returns a handle (0 when it is not one); HlpClose closes it (a program's open files are closed when it ends). Topics are numbered from 0: HlpTopicCount, HlpTopicTitle$ and HlpTopicId$ (the name links and HelpTopic use), HlpFindTopic(handle, topicId) (-1 when there is none), and HlpStartTopic(handle, topicId), the topic to show first (topicId when the file has it, else the file's own start page). HlpTocCount, HlpTocTitle$, HlpTocDepth, HlpTocTopic, and HlpTocExpanded give the table of contents in order -- a depth of 0 is the top level, a topic of -1 a heading that only groups others -- ready to fill a TreeView. HlpIndexCount, HlpIndexKeyword$, and HlpIndexTopic give the keyword index (sorted), for a list or DlgChoice. HlpSearch(handle, words) searches the titles, text, and keywords as the Help Viewer does and returns how many topics have every word; HlpSearchTopic(handle, n) gives them. diff --git a/src/apps/kpunch/dvxbasic/runtime/basErrors.h b/src/apps/kpunch/dvxbasic/runtime/basErrors.h index 801feb8e..33f72527 100644 --- a/src/apps/kpunch/dvxbasic/runtime/basErrors.h +++ b/src/apps/kpunch/dvxbasic/runtime/basErrors.h @@ -46,6 +46,7 @@ #define BAS_ERR_DIV_BY_ZERO 11 #define BAS_ERR_TYPE_MISMATCH 13 #define BAS_ERR_OUT_OF_STRING 14 +#define BAS_ERR_INTERRUPTED 18 // User interrupt occurred: code stopped for running too long (basVmSetRunLimit) #define BAS_ERR_RESUME_WITHOUT_ERR 20 #define BAS_ERR_FOR_NESTING 26 #define BAS_ERR_OUT_OF_STACK 28 diff --git a/src/apps/kpunch/dvxbasic/runtime/gfx.c b/src/apps/kpunch/dvxbasic/runtime/gfx.c index 9bd60a35..ba0686f8 100644 --- a/src/apps/kpunch/dvxbasic/runtime/gfx.c +++ b/src/apps/kpunch/dvxbasic/runtime/gfx.c @@ -70,6 +70,8 @@ typedef struct { uint32_t border; // pixel the fill stops at const BasGfxTileT *tile; // NULL: colour fill uint8_t *done; // tile fill: one bit per view pixel + uint32_t *row; // a row of the view read at once (rowRead) + uint32_t *near; // and the run above or below it } GfxFloodT; // An ellipse outline (CIRCLE): its centre and radii in pixels, the arc @@ -133,7 +135,7 @@ static int32_t drawNumber(const char *s, int32_t len, int32_t *pos, bool allowS static void ellipse(BasGfxT *g, const GfxEllipseT *e); static void ellipseFill(BasGfxT *g, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t pixel); static void ellipsePoint(BasGfxT *g, const GfxEllipseT *e, int32_t dx, int32_t dy); -static bool fillable(const GfxFloodT *f, int32_t x, int32_t y); +static bool fillable(const GfxFloodT *f, uint32_t pixel, int32_t x, int32_t y); static void fillRect(BasGfxT *g, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t pixel, int32_t style); static void fillSpan(BasGfxT *g, int32_t y, int32_t x0, int32_t x1, uint32_t pixel, int32_t style); static int32_t floodFill(BasGfxT *g, int32_t x, int32_t y, uint32_t paint, uint32_t border, const BasGfxTileT *tile); @@ -151,6 +153,9 @@ static int32_t penSize(const BasGfxT *g); static void plot(BasGfxT *g, int32_t x, int32_t y, uint32_t pixel); static void plotPixel(BasGfxT *g, int32_t x, int32_t y, uint32_t pixel); static void pointArg(const BasGfxT *g, BasValueT x, BasValueT y, bool step, double *outX, double *outY); +static void rowRead(const BasGfxT *g, int32_t x0, int32_t x1, int32_t y, uint32_t *out); +static int32_t rowsBottom(const BasGfxT *g); +static int32_t rowsTop(const BasGfxT *g); static double scaleLen(const BasGfxT *g, double len); static bool shapeFilled(const BasGfxT *g); static double spanX(const BasGfxT *g); @@ -335,6 +340,7 @@ void basGfxInit(BasGfxT *g, const BasGfxSurfaceT *surf, uint32_t fore, uint32_t g->surf = *surf; g->viewX1 = surf->width - 1; g->viewY1 = surf->height - 1; + g->bandY1 = BAS_GFX_BAND_ALL; g->fore = fore; g->back = back; g->fillStyle = BAS_GFX_FILL_TRANSPARENT; @@ -1174,9 +1180,8 @@ static void ellipsePoint(BasGfxT *g, const GfxEllipseT *e, int32_t dx, int32_t d } -static bool fillable(const GfxFloodT *f, int32_t x, int32_t y) { - uint32_t pixel = f->g->surf.get(f->g->surf.ctx, x, y); - +// Whether the flood takes the pixel at (x, y), whose value is pixel. +static bool fillable(const GfxFloodT *f, uint32_t pixel, int32_t x, int32_t y) { if (pixel == f->border) { return false; } @@ -1202,8 +1207,8 @@ static void fillRect(BasGfxT *g, int32_t x0, int32_t y0, int32_t x1, int32_t y1, return; } - top = top < g->viewY0 ? g->viewY0 : top; - bottom = bottom > g->viewY1 ? g->viewY1 : bottom; + top = top < rowsTop(g) ? rowsTop(g) : top; + bottom = bottom > rowsBottom(g) ? rowsBottom(g) : bottom; for (int32_t y = top; y <= bottom; y++) { fillSpan(g, y, x0, x1, pixel, style); @@ -1222,7 +1227,7 @@ static void fillSpan(BasGfxT *g, int32_t y, int32_t x0, int32_t x1, uint32_t pix int32_t left = x0 < x1 ? x0 : x1; int32_t right = x0 < x1 ? x1 : x0; - if (y < g->viewY0 || y > g->viewY1) { + if (y < rowsTop(g) || y > rowsBottom(g)) { return; } @@ -1239,8 +1244,10 @@ static void fillSpan(BasGfxT *g, int32_t y, int32_t x0, int32_t x1, uint32_t pix // Scan-line flood fill inside the view: everything reachable from (x, y) // that is not the border colour (nor, for a colour, already that colour). +// Rows are read a run at a time (rowRead), not a pixel at a time. static int32_t floodFill(BasGfxT *g, int32_t x, int32_t y, uint32_t paint, uint32_t border, const BasGfxTileT *tile) { GfxSeedT *seeds = NULL; + int32_t width = g->viewX1 - g->viewX0 + 1; GfxFloodT f; f.g = g; @@ -1248,15 +1255,20 @@ static int32_t floodFill(BasGfxT *g, int32_t x, int32_t y, uint32_t paint, uint3 f.border = border; f.tile = tile; f.done = NULL; + f.row = (uint32_t *)malloc((size_t)width * sizeof(uint32_t)); + f.near = (uint32_t *)malloc((size_t)width * sizeof(uint32_t)); if (tile) { - size_t pixels = (size_t)(g->viewX1 - g->viewX0 + 1) * (size_t)(g->viewY1 - g->viewY0 + 1); + size_t pixels = (size_t)width * (size_t)(g->viewY1 - g->viewY0 + 1); f.done = (uint8_t *)calloc(pixels / GFX_BITS_PER_BYTE + 1, 1); + } - if (!f.done) { - return BAS_ERR_OUT_OF_MEMORY; - } + if (!f.row || !f.near || (tile && !f.done)) { + free(f.row); + free(f.near); + free(f.done); + return BAS_ERR_OUT_OF_MEMORY; } arrput(seeds, ((GfxSeedT){ x, y })); @@ -1264,18 +1276,20 @@ static int32_t floodFill(BasGfxT *g, int32_t x, int32_t y, uint32_t paint, uint3 while (arrlen(seeds) > 0) { GfxSeedT s = arrpop(seeds); - if (!fillable(&f, s.x, s.y)) { + rowRead(g, g->viewX0, g->viewX1, s.y, f.row); + + if (!fillable(&f, f.row[s.x - g->viewX0], s.x, s.y)) { continue; } int32_t left = s.x; int32_t right = s.x; - while (left > g->viewX0 && fillable(&f, left - 1, s.y)) { + while (left > g->viewX0 && fillable(&f, f.row[left - 1 - g->viewX0], left - 1, s.y)) { left--; } - while (right < g->viewX1 && fillable(&f, right + 1, s.y)) { + while (right < g->viewX1 && fillable(&f, f.row[right + 1 - g->viewX0], right + 1, s.y)) { right++; } @@ -1288,8 +1302,10 @@ static int32_t floodFill(BasGfxT *g, int32_t x, int32_t y, uint32_t paint, uint3 bool inRun = false; + rowRead(g, left, right, ny, f.near); + for (int32_t nx = left; nx <= right; nx++) { - bool can = fillable(&f, nx, ny); + bool can = fillable(&f, f.near[nx - left], nx, ny); if (can && !inRun) { arrput(seeds, ((GfxSeedT){ nx, ny })); @@ -1301,6 +1317,8 @@ static int32_t floodFill(BasGfxT *g, int32_t x, int32_t y, uint32_t paint, uint3 } arrfree(seeds); + free(f.row); + free(f.near); free(f.done); return 0; } @@ -1361,12 +1379,12 @@ static bool hatchOn(int32_t style, int32_t x, int32_t y) { } -// A horizontal span clipped to the view. +// A horizontal span clipped to the view (and band). static void hspan(BasGfxT *g, int32_t y, int32_t x0, int32_t x1, uint32_t pixel) { int32_t left = x0 < x1 ? x0 : x1; int32_t right = x0 < x1 ? x1 : x0; - if (y < g->viewY0 || y > g->viewY1) { + if (y < rowsTop(g) || y > rowsBottom(g)) { return; } @@ -1380,7 +1398,7 @@ static void hspan(BasGfxT *g, int32_t y, int32_t x0, int32_t x1, uint32_t pixel) static bool inView(const BasGfxT *g, int32_t x, int32_t y) { - return x >= g->viewX0 && x <= g->viewX1 && y >= g->viewY0 && y <= g->viewY1; + return x >= g->viewX0 && x <= g->viewX1 && y >= rowsTop(g) && y <= rowsBottom(g); } @@ -1507,7 +1525,11 @@ static void plot(BasGfxT *g, int32_t x, int32_t y, uint32_t pixel) { return; } - for (int32_t dy = -r; dy <= r; dy++) { + // Only the disc's rows that are drawn (a band of a page: most are not) + int32_t from = y - r < rowsTop(g) ? rowsTop(g) - y : -r; + int32_t to = y + r > rowsBottom(g) ? rowsBottom(g) - y : r; + + for (int32_t dy = from; dy <= to; dy++) { int32_t half = basGfxRound(sqrt((double)(r * r - dy * dy))); hspan(g, y + dy, x - half, x + half, pixel); @@ -1529,6 +1551,32 @@ static void pointArg(const BasGfxT *g, BasValueT x, BasValueT y, bool step, doub } +// Pixels x0..x1 of row y into out: the surface's row read, or a pixel at +// a time. +static void rowRead(const BasGfxT *g, int32_t x0, int32_t x1, int32_t y, uint32_t *out) { + if (g->surf.getRow) { + g->surf.getRow(g->surf.ctx, x0, x1, y, out); + return; + } + + for (int32_t x = x0; x <= x1; x++) { + out[x - x0] = g->surf.get(g->surf.ctx, x, y); + } +} + + +// The last row drawn: the view's, or the band's above it. +static int32_t rowsBottom(const BasGfxT *g) { + return g->viewY1 < g->bandY1 ? g->viewY1 : g->bandY1; +} + + +// The first row drawn: the view's, or the band's below it. +static int32_t rowsTop(const BasGfxT *g) { + return g->viewY0 > g->bandY0 ? g->viewY0 : g->bandY0; +} + + // A length in logical x units -> pixels. static double scaleLen(const BasGfxT *g, double len) { if (!g->world) { diff --git a/src/apps/kpunch/dvxbasic/runtime/gfx.h b/src/apps/kpunch/dvxbasic/runtime/gfx.h index bbcfdd3a..67cc8fab 100644 --- a/src/apps/kpunch/dvxbasic/runtime/gfx.h +++ b/src/apps/kpunch/dvxbasic/runtime/gfx.h @@ -107,6 +107,7 @@ typedef struct { uint32_t colorMax; // largest colour number it takes (0: any value, e.g. RGB) void (*span)(void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t pixel); uint32_t (*get)(const void *ctx, int32_t x, int32_t y); + void (*getRow)(const void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t *out); // pixels x0..x1 of row y (NULL: get a pixel at a time) uint32_t (*toPixel)(void *ctx, uint32_t color); // NULL: colours are pixels uint32_t (*toColor)(const void *ctx, uint32_t pixel); // NULL: pixels are colours const BasGfxVectorT *vec; // NULL: shapes are spans @@ -120,6 +121,9 @@ typedef struct { uint32_t colors[BAS_GFX_TILE_MAX_H][BAS_GFX_TILE_MAX_W]; } BasGfxTileT; +// BasGfxT.bandY1 when no band limits the rows drawn +#define BAS_GFX_BAND_ALL INT32_MAX + // Drawing state of one surface. typedef struct { BasGfxSurfaceT surf; @@ -127,6 +131,8 @@ typedef struct { int32_t viewY0; int32_t viewX1; int32_t viewY1; + int32_t bandY0; // rows drawn at all, with the view's: a printed page's band being drawn; 0..BAS_GFX_BAND_ALL otherwise + int32_t bandY1; bool viewAbsolute; // VIEW SCREEN: coordinates are not moved to the view's corner bool world; // WINDOW or a VB Scale is on: logical units are mapped onto the view bool worldEdges; // VB Scale: the width covers the view's pixels edge to edge (WINDOW: first to last pixel centre) diff --git a/src/apps/kpunch/dvxbasic/runtime/screen.c b/src/apps/kpunch/dvxbasic/runtime/screen.c index f78cc1a7..87d5ad5f 100644 --- a/src/apps/kpunch/dvxbasic/runtime/screen.c +++ b/src/apps/kpunch/dvxbasic/runtime/screen.c @@ -286,6 +286,7 @@ static BasScreenT *screenEnsure(BasVmT *vm); static void scrollUp(BasScreenT *scr); static int32_t setMode(BasScreenT *scr, int32_t mode, int32_t cols, int32_t rows); static uint32_t surfGet(const void *ctx, int32_t x, int32_t y); +static void surfGetRow(const void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t *out); static void surfSpan(void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t pixel); static uint8_t textBackPixel(const BasScreenT *scr); static void textRedraw(BasScreenT *scr, bool blinkOnly); @@ -340,7 +341,7 @@ int32_t basScreenExec(BasVmT *vm, uint8_t fn, BasValueT *args, int32_t argc, Bas *result = basValInteger(0); // LINE INPUT and POS work without a screen (the host's input and - // output); everything else makes one. + // output); everything else but FULLSCREEN's state makes one. if (fn == BAS_FN_LINE_INPUT) { return execLineInput(vm, args[0], result); } @@ -350,6 +351,12 @@ int32_t basScreenExec(BasVmT *vm, uint8_t fn, BasValueT *args, int32_t argc, Bas return 0; } + // FULLSCREEN's state makes no screen either + if (fn == BAS_FN_FULLSCREEN_STATE) { + *result = basValBool(vm->screen && vm->screen->fullScreen); + return 0; + } + BasScreenT *scr = screenEnsure(vm); if (!scr) { @@ -1563,6 +1570,7 @@ static int32_t setMode(BasScreenT *scr, int32_t mode, int32_t cols, int32_t rows surf.colorMax = (uint32_t)info->colors - 1; surf.span = surfSpan; surf.get = surfGet; + surf.getRow = surfGetRow; scr->fbAll = fb; scr->charsAll = chars; @@ -1610,6 +1618,16 @@ static uint32_t surfGet(const void *ctx, int32_t x, int32_t y) { } +static void surfGetRow(const void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t *out) { + const BasScreenT *scr = (const BasScreenT *)ctx; + const uint8_t *row = scr->fb + y * scr->width; + + for (int32_t x = x0; x <= x1; x++) { + out[x - x0] = row[x]; + } +} + + static void surfSpan(void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t pixel) { BasScreenT *scr = (BasScreenT *)ctx; diff --git a/src/apps/kpunch/dvxbasic/runtime/values.c b/src/apps/kpunch/dvxbasic/runtime/values.c index dd56b82d..6b5ae52e 100644 --- a/src/apps/kpunch/dvxbasic/runtime/values.c +++ b/src/apps/kpunch/dvxbasic/runtime/values.c @@ -456,6 +456,11 @@ void basObjectClear(BasObjectT *obj) { obj->itemCount = 0; obj->itemCap = 0; + free(obj->keyIndex); + obj->keyIndex = NULL; + obj->keyIndexCap = 0; + obj->keyIndexUsed = 0; + for (int32_t i = 0; i < fieldCount; i++) { basValRelease(&fields[i]); } diff --git a/src/apps/kpunch/dvxbasic/runtime/values.h b/src/apps/kpunch/dvxbasic/runtime/values.h index fcc80a66..92422e0a 100644 --- a/src/apps/kpunch/dvxbasic/runtime/values.h +++ b/src/apps/kpunch/dvxbasic/runtime/values.h @@ -36,6 +36,7 @@ #include #include "../compiler/opcodes.h" +#include "dvxHash.h" // ============================================================ // Reference-counted string @@ -318,11 +319,6 @@ typedef struct { BasObjectT *next; } BasObjLinkT; -// FNV-1a parameters shared by the constant-pool interning hash, the -// case-insensitive symbol hash (compiler/scope.c) and Collection keys. -#define BAS_FNV1A_OFFSET 0x811C9DC5u -#define BAS_FNV1A_PRIME 0x01000193u - // One Collection item: an optional key (NULL = none), its hash (a lookup // compares the text only when the hashes match) and the value. typedef struct { @@ -339,6 +335,9 @@ struct BasObjectS { int32_t itemCap; BasValueT *fields; // class instance: member variables BasCollItemT *items; // Collection: items in order + int32_t *keyIndex; // Collection: item position + 1 by key hash, open addressing (NULL = not built) + int32_t keyIndexCap; // its slots (a power of two) + int32_t keyIndexUsed; // keyed items in it BasObjFinalizeFnT finalize; // Class_Terminate hook (NULL = free at once) void *finalizeCtx; BasObjLinkT links[BAS_OBJ_LIST_COUNT]; // places on the owner's lists (BasObjListE) diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.c b/src/apps/kpunch/dvxbasic/runtime/vm.c index 9814e9d9..68e4b626 100644 --- a/src/apps/kpunch/dvxbasic/runtime/vm.c +++ b/src/apps/kpunch/dvxbasic/runtime/vm.c @@ -26,7 +26,7 @@ // basVmStep() call, or runs until completion via basVmRun(). // PRINT/INPUT, forms and extern calls go through host-provided // callbacks; file, directory and path statements use the C library -// (stdio, dirent, fnmatch, stat) and the DVX platform helpers. +// (stdio, dirent, stat) and the DVX platform helpers. #include "vm.h" #include "../compiler/opcodes.h" @@ -36,10 +36,10 @@ #include "dvxPrefs.h" #include "screen.h" #include "dvxBas.h" +#include "dvxFontFmt.h" #include #include -#include #include #include #include @@ -161,8 +161,16 @@ #define BAS_SECS_PER_HOUR 3600.0 #define BAS_SECS_PER_MINUTE 60.0 #define BAS_USEC_PER_SEC 1000000.0 +#define BAS_USEC_PER_SEC_INT 1000000 +#define BAS_CLOCK_TICK_US ((int64_t)PLATFORM_US_PER_SEC * PLATFORM_PIT_BIOS_DIVISOR / PLATFORM_PIT_HZ) // a BIOS tick, the DOS wall clock's step +#define BAS_TIMER_NO_READING (-1) // BasVmT.timerWall before TIMER's first reading #define BAS_SECS_PER_DAY 86400.0 +// CHRW$: the highest Unicode code point, and the character for one code +// page 437 does not have. +#define BAS_UNICODE_MAX 0x10FFFF +#define BAS_CHRW_NONE '?' + // struct tm counts years from 1900. #define BAS_TM_YEAR_BASE 1900 @@ -183,6 +191,22 @@ // between checks (microseconds). #define BAS_WAIT_POLL_USEC 1000 +// A long FileCopy or Dir$ lets the program's events run (DoEvents) every +// BAS_PUMP_SEC, Dir$ looking every BAS_DIR_PUMP_ENTRIES entries it passes +// over (not in a control's code) +#define BAS_PUMP_SEC 0.05 +#define BAS_DIR_PUMP_ENTRIES 64 + +// IniRead / IniWrite's kept file: a file changed this recently is read +// again (FAT times are 2 seconds apart), and the size kept for no file +#define BAS_INI_TIME_SEC 2 +#define BAS_INI_NO_FILE (-1) + +// A VM with a run limit (basVmSetRunLimit): why a run or call was stopped +// (error 18), and why a wait was refused (error 5). +#define BAS_RUNAWAY_MSG "The code ran too long without returning and was stopped" +#define BAS_NO_WAIT_MSG "A control's code cannot wait (SLEEP, FRAMEWAIT, or SHELL)" + // RND: rand() scaled by RAND_MAX + 1 so the result stays below 1.0, and // the multiplier that spreads a negative RND(n) argument's fraction into // the reseed value (RND(-1.5) and RND(-1) must seed differently). @@ -222,9 +246,15 @@ #define BAS_PLAY_TEXT_MAX 32767 #define BAS_PLAY_NUMBER_CHARS 12 -// How a Collection's item array grows. +// How a Collection's item array grows, and its key index: at least +// BAS_COLL_INDEX_MIN slots, BAS_COLL_INDEX_ROOM per key when built, and +// built again once a key would take more than one slot in +// BAS_COLL_INDEX_LOAD #define BAS_COLL_MIN_CAP 4 #define BAS_COLL_GROWTH 2 +#define BAS_COLL_INDEX_MIN 16 +#define BAS_COLL_INDEX_ROOM 4 +#define BAS_COLL_INDEX_LOAD 2 #define BAS_COLL_KEY_ARGS 2 // Add item, key // TYPENAME / TYPEOF names that are not class or control names, and the @@ -315,6 +345,13 @@ typedef struct { int32_t len; } BasPlayTextT; +// FileCopy's pump between pieces (fileCopyGoOn) +typedef struct { + BasVmT *vm; + double pumpAt; // wall seconds of the last pump + bool stopped; // the program was asked to end meanwhile +} BasCopyPumpT; + // Output sink for PUT: writes to fp or into mem (up to memCap bytes), or // with neither only counts the bytes (a RANDOM PUT measures its record // before writing it). @@ -392,8 +429,11 @@ static bool closeChannel(BasVmT *vm, int32_t channel); static bool collectCycles(BasVmT *vm, int32_t returnPc); static BasVmResultE collectionAccess(BasVmT *vm, BasObjectT *coll, int32_t member, BasValueT *args, int32_t argc, int32_t op); static int32_t collectionAdd(BasObjectT *coll, BasValueT *args, int32_t argc); -static int32_t collectionFind(const BasObjectT *coll, BasValueT index, int32_t *outPos); -static int32_t collectionKeyPos(const BasObjectT *coll, const BasStringT *key); +static int32_t collectionFind(BasObjectT *coll, BasValueT index, int32_t *outPos); +static void collectionIndexDrop(BasObjectT *coll); +static void collectionIndexPut(BasObjectT *coll, int32_t pos); +static bool collectionIndexReady(BasObjectT *coll); +static int32_t collectionKeyPos(BasObjectT *coll, const BasStringT *key); static BasStringT *commandArg(const char *args, int32_t n); static bool conversionFailed(void); static BasVmResultE convertValue(BasVmT *vm, uint8_t op, BasValueT *val); @@ -402,7 +442,8 @@ static bool debugChecksDue(const BasVmT *vm); static void defaultPrint(void *ctx, const char *text, bool newline); static void derefArg(BasValueT *arg); static void dirClose(BasVmT *vm); -static const char *dirNext(BasVmT *vm); +static const char *dirNext(BasVmT *vm, bool *stopped); +static BasVmResultE dirPush(BasVmT *vm); static void discardFrame(BasVmT *vm); static void discardResult(BasVmT *vm, int32_t depth, int32_t sp); static bool dispatchError(BasVmT *vm, BasVmResultE result, int32_t floorDepth, int32_t faultPc); @@ -436,6 +477,7 @@ static BasVmResultE execObjMember(BasVmT *vm, uint8_t op); static BasVmResultE execPrint(BasVmT *vm); static BasVmResultE execRaiseEvent(BasVmT *vm); static BasVmResultE execReturn(BasVmT *vm, bool withValue); +static BasVmResultE execShell(BasVmT *vm); static BasVmResultE execStoreVar(BasVmT *vm, uint8_t op); static BasVmResultE execStrAppend(BasVmT *vm); static BasVmResultE execStringOp(BasVmT *vm, uint8_t op); @@ -463,6 +505,7 @@ static bool fastType(uint8_t type); static bool fastValues(const BasValueT *v, int32_t count); static int32_t fetchI32(const uint8_t *p); static uint16_t fetchU16(const uint8_t *p); +static bool fileCopyGoOn(void *ctx); static int32_t fileWidth(BasVmT *vm, BasValueT target, int32_t width); static BasStringT *fillString(char ch, int32_t count); static int32_t filterScan(const BasArrayT *arr, const BasStringT *match, bool keep, bool ignoreCase, BasArrayT *out); @@ -483,7 +526,8 @@ static void gcScan(BasGcT *gc, int32_t idx, bool mark); static bool hostCallFinish(BasVmT *vm, BasVmResultE rc, int32_t savedPc, int32_t savedDepth, bool savedRunning, int32_t savedSp, BasValueT *result); static bool hostCallReady(const BasVmT *vm); static int32_t indexArray(const BasArrayT *arr, BasValueT *args, int32_t argc, int32_t *outIdx); -static BasValueT iniRead(const char *path, const char *sec, const char *key, BasStringT *defStr); +static PrefsHandleT *iniPrefs(BasVmT *vm, const char *path); +static BasValueT iniRead(BasVmT *vm, const char *path, const char *sec, const char *key, BasStringT *defStr); static BasVmResultE iniWrite(BasVmT *vm, const char *path, const char *sec, const char *key, const char *val); static BasValueT integralValue(double v); static BasValueT intResult(int32_t v); @@ -504,7 +548,7 @@ static bool layoutNext(const BasStringT *layout, int32_t *pos, BasLayo static bool leaveFast(BasVmT *vm, uint8_t op, int32_t safeEnd); static int32_t likeElement(const char *p, int32_t pLen, int32_t pos, uint8_t ch, bool text, bool *hit); static int32_t likeMatch(const char *s, int32_t sLen, const char *p, int32_t pLen, bool text); -static void localNow(struct tm *out, int32_t *outUsec); +static void localNow(BasVmT *vm, struct tm *out, int32_t *outUsec); static BasVmResultE logicalNull(BasVmT *vm, uint8_t op, BasValueT a, BasValueT b); static BasValueT logicalResult(int32_t r, bool isBool, bool isLong); static void logicalSet(BasValueT *v, int32_t r, bool isBool, bool isLong); @@ -579,6 +623,7 @@ static void releaseVmState(BasVmT *vm); static int32_t replaceScan(const BasStringT *s, int32_t start, const BasStringT *find, const BasStringT *repl, int32_t count, bool ignoreCase, char *out); static void restoreFormContext(BasVmT *vm, BasCallFrameT *frame); static BasVmResultE roundedOperand(BasVmT *vm, BasValueT v, int32_t *out); +static bool runaway(BasVmT *vm, int32_t *slices); static BasVmResultE runFast(BasVmT *vm, int32_t budget, int32_t floorDepth, int32_t *outSteps, int32_t *outPc); static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool savedRunning); static void runtimeError(BasVmT *vm, int32_t errNum, const char *msg); @@ -589,6 +634,7 @@ static bool sendSpool(BasVmT *vm, BasFileChannelT *ch); static void sinkFill(BasPutSinkT *sink, char c, int32_t count); static void sinkWrite(BasPutSinkT *sink, const void *data, int32_t len); static bool sleepSeconds(BasVmT *vm, int32_t secs); +static void sliceBegin(BasVmT *vm); static int32_t sourceRead(BasGetSourceT *src, void *dst, int32_t len); static int32_t splitScan(const BasStringT *s, const BasStringT *delim, int32_t limit, bool ignoreCase, BasArrayT *out); static BasVmResultE startTerminate(BasVmT *vm); @@ -597,6 +643,7 @@ static void storeValue(BasValueT *target, BasValueT val); static int32_t stringFind(const BasStringT *haystack, int32_t start, const BasStringT *needle, bool ignoreCase); static bool stringMatchAt(const BasStringT *haystack, int32_t i, const BasStringT *needle, bool ignoreCase); static BasValueT strValue(BasStringT *s); +static bool subLoopFail(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool savedRunning); static void sweepObjects(BasVmT *vm); static const char *typeNameOf(const BasVmT *vm, uint8_t type, int32_t typeId); static BasUdtT *udtFromLayout(BasVmT *vm, const BasStringT *layout, int32_t *pos, int32_t typeId, int32_t fieldCount, int32_t depth); @@ -611,6 +658,7 @@ static bool valueless(const BasValueT *v); static int32_t valueOperands(const BasVmT *vm); static int32_t varptrString(BasValueT v, BasValueT *out); static int32_t varTypeOf(uint8_t type); +static bool waitRefused(BasVmT *vm); static bool waitWhile(BasVmT *vm, bool (*busy)(void *ctx)); static double wallSeconds(void); static bool wholeOrCurrency(uint8_t type); @@ -1021,6 +1069,7 @@ void basVmDestroy(BasVmT *vm) { } dirClose(vm); + prefsClose((PrefsHandleT *)vm->iniPrefs); basScreenDestroy(vm->screen); free(vm->commandArgs); free(vm->externCache); @@ -1280,8 +1329,11 @@ BasVmResultE basVmRun(BasVmT *vm) { return BAS_VM_HALTED; } + int32_t slices = 0; // a run limit's count (no step limit: one run to the end) + vm->running = true; vm->stepCount = 0; + sliceBegin(vm); while (vm->running) { // Check step limit: the host gets a turn, with the screen shown @@ -1290,14 +1342,19 @@ BasVmResultE basVmRun(BasVmT *vm) { return BAS_VM_STEP_LIMIT; } - // savedPc: where the instruction that failed started, for RESUME - int32_t budget = (vm->stepLimit > 0) ? vm->stepLimit - vm->stepCount : BAS_RUN_UNLIMITED; + // savedPc: where the instruction that failed started, for RESUME. + // A run limit without a step limit counts the run in slices. + int32_t budget = (vm->stepLimit > 0) ? vm->stepLimit - vm->stepCount : (vm->runLimit > 0 ? BAS_VM_DEFAULT_STEP_SLICE : BAS_RUN_UNLIMITED); int32_t steps = 0; int32_t savedPc = vm->pc; BasVmResultE result = runFast(vm, budget, BAS_NO_FLOOR_DEPTH, &steps, &savedPc); if (vm->stepLimit > 0) { vm->stepCount += steps; + } else if (result == BAS_VM_OK && vm->running && runaway(vm, &slices)) { + vm->running = false; + basScreenPresent(vm); + return BAS_VM_ERROR; } if (result != BAS_VM_OK) { @@ -1419,6 +1476,12 @@ void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx) { } +void basVmSetMicrosClock(BasVmT *vm, uint64_t (*micros)(void)) { + vm->microsFn = micros; + vm->timerWall = BAS_TIMER_NO_READING; +} + + void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx) { vm->printFn = fn; vm->printCtx = ctx; @@ -1430,11 +1493,22 @@ void basVmSetPrinterCallbacks(BasVmT *vm, const BasPrinterCallbacksT *printer) { } +void basVmSetRunLimit(BasVmT *vm, int32_t slices) { + vm->runLimit = slices; +} + + void basVmSetScreenHost(BasVmT *vm, const BasScreenHostT *host) { vm->screenHost = *host; } +void basVmSetSliceClock(BasVmT *vm, const volatile uint32_t *clock, uint32_t span) { + vm->sliceClock = clock; + vm->sliceSpan = span; +} + + void basVmSetSqlCallbacks(BasVmT *vm, const BasSqlCallbacksT *sql) { vm->sql = *sql; } @@ -2605,7 +2679,7 @@ static BasVmResultE basVmStepExec(BasVmT *vm) { struct tm t; int32_t usec; - localNow(&t, &usec); + localNow(vm, &t, &usec); double secs = (double)t.tm_hour * BAS_SECS_PER_HOUR + (double)t.tm_min * BAS_SECS_PER_MINUTE + (double)t.tm_sec + (double)usec / BAS_USEC_PER_SEC; @@ -2621,7 +2695,7 @@ static BasVmResultE basVmStepExec(BasVmT *vm) { struct tm t; char buf[BAS_DATE_STR_LEN]; - localNow(&t, NULL); + localNow(vm, &t, NULL); snprintf(buf, sizeof(buf), "%02d-%02d-%04d", t.tm_mon + 1, t.tm_mday, t.tm_year + 1900); if (!push(vm, basValStringFromC(buf))) { @@ -2636,7 +2710,7 @@ static BasVmResultE basVmStepExec(BasVmT *vm) { struct tm t; char buf[BAS_TIME_STR_LEN]; - localNow(&t, NULL); + localNow(vm, &t, NULL); snprintf(buf, sizeof(buf), "%02d:%02d:%02d", t.tm_hour, t.tm_min, t.tm_sec); if (!push(vm, basValStringFromC(buf))) { @@ -2656,6 +2730,10 @@ static BasVmResultE basVmStepExec(BasVmT *vm) { int32_t secs = basValToInt32(val); basValRelease(&val); + if (secs > 0 && waitRefused(vm)) { + return BAS_VM_ERROR; + } + if (!sleepSeconds(vm, secs)) { vm->running = false; return BAS_VM_HALTED; @@ -3311,25 +3389,10 @@ static BasVmResultE basVmStepExec(BasVmT *vm) { // ============================================================ case OP_SHELL: { - BasValueT cmdVal; + BasVmResultE shellRes = execShell(vm); - if (!pop(vm, &cmdVal)) { - return BAS_VM_STACK_UNDERFLOW; - } - - BasValueT cmdStr = basValToString(cmdVal); - basValRelease(&cmdVal); - - int32_t result = 0; - - if (cmdStr.strVal && cmdStr.strVal->len > 0) { - result = system(cmdStr.strVal->data); - } - - basValRelease(&cmdStr); - - if (!push(vm, basValInteger((int16_t)result))) { - return BAS_VM_STACK_OVERFLOW; + if (shellRes != BAS_VM_OK) { + return shellRes; } break; @@ -4132,7 +4195,7 @@ static BasVmResultE basVmStepExec(BasVmT *vm) { BasStringT *secStr = basValFormatString(secVal); BasStringT *keyStr = basValFormatString(keyVal); BasStringT *defStr = basValFormatString(defVal); - BasValueT resultVal = iniRead(fileStr->data, secStr->data, keyStr->data, defStr); + BasValueT resultVal = iniRead(vm, fileStr->data, secStr->data, keyStr->data, defStr); basStringUnref(fileStr); basStringUnref(secStr); @@ -4294,7 +4357,7 @@ static int32_t builtinDate(uint8_t fn, BasValueT *args, BasValueT *out) { double datePart = 0.0; double timePart = 0.0; - localNow(&t, NULL); + localNow(NULL, &t, NULL); if (fn != BAS_FN_TIME && !basDateSerial(t.tm_year + BAS_TM_YEAR_BASE, t.tm_mon + 1, t.tm_mday, &datePart)) { return BAS_ERR_OVERFLOW; @@ -4994,6 +5057,31 @@ static int32_t builtinString(BasVmT *vm, uint8_t fn, BasValueT *args, int32_t ar return 0; } + case BAS_FN_ASCW: { + BasStringT *s = argString(&args[0]); + + if (s->len == 0) { + return BAS_ERR_ILLEGAL_FUNC_CALL; + } + + *out = basValLong((int32_t)dfnTextToUnicode((uint8_t)s->data[0])); + return 0; + } + + case BAS_FN_CHRW: { + double code = basValToNumber(args[0]); + + if (code < 0 || code > BAS_UNICODE_MAX) { + return BAS_ERR_ILLEGAL_FUNC_CALL; + } + + int32_t c = dfnTextFromUnicode((uint32_t)code); + char ch = (c >= 0) ? (char)c : BAS_CHRW_NONE; + + *out = strValue(basStringNew(&ch, 1)); + return 0; + } + case BAS_FN_STRREVERSE: { BasStringT *s = argString(&args[0]); BasStringT *result = basStringAlloc(s->len + 1); @@ -5081,6 +5169,23 @@ static int32_t builtinString(BasVmT *vm, uint8_t fn, BasValueT *args, int32_t ar return 0; } + case BAS_FN_LOADPICTUREDATA: { + if (!vm->ui.loadPictureData) { + return BAS_ERR_NO_FORM_RUNTIME; + } + + BasStringT *bytes = argString(&args[0]); + int32_t picErr = 0; + void *pic = vm->ui.loadPictureData(vm->ui.ctx, bytes->data, bytes->len, &picErr); + + if (!pic) { + return picErr; + } + + *out = basValObject(pic); + return 0; + } + case BAS_FN_NEW_FORM: if (!vm->ui.newForm) { return BAS_ERR_NO_FORM_RUNTIME; @@ -5552,6 +5657,7 @@ static BasVmResultE collectionAccess(BasVmT *vm, BasObjectT *coll, int32_t membe coll->itemCount--; memmove(&coll->items[pos], &coll->items[pos + 1], (size_t)(coll->itemCount - pos) * sizeof(BasCollItemT)); + collectionIndexDrop(coll); basStringUnref(gone.key); basValRelease(&gone.value); } @@ -5623,6 +5729,15 @@ static int32_t collectionAdd(BasObjectT *coll, BasValueT *args, int32_t argc) { coll->items[pos].keyHash = hasKey ? keyHash(args[1].strVal) : 0; coll->items[pos].value = valueDetach(basValCopy(args[0])); coll->itemCount++; + + // An item added at the end keeps the key index (while it has room); + // one put before others moves their positions + if (pos < coll->itemCount - 1 || (hasKey && coll->keyIndex && (coll->keyIndexUsed + 1) * BAS_COLL_INDEX_LOAD > coll->keyIndexCap)) { + collectionIndexDrop(coll); + } else if (hasKey && coll->keyIndex) { + collectionIndexPut(coll, pos); + } + return 0; } @@ -5630,7 +5745,7 @@ static int32_t collectionAdd(BasObjectT *coll, BasValueT *args, int32_t argc) { // The 0-based position of the item index (1-based) or key (a string, // matched without regard to case) names. Returns 0 and sets *outPos, // else the BASIC error number. -static int32_t collectionFind(const BasObjectT *coll, BasValueT index, int32_t *outPos) { +static int32_t collectionFind(BasObjectT *coll, BasValueT index, int32_t *outPos) { if (index.type == BAS_TYPE_STRING) { int32_t pos = collectionKeyPos(coll, index.strVal); @@ -5657,10 +5772,85 @@ static int32_t collectionFind(const BasObjectT *coll, BasValueT index, int32_t * } -// The position of the item with key (case ignored), or -1. -static int32_t collectionKeyPos(const BasObjectT *coll, const BasStringT *key) { +// Forget the key index (the next lookup builds it again): positions moved. +static void collectionIndexDrop(BasObjectT *coll) { + free(coll->keyIndex); + coll->keyIndex = NULL; + coll->keyIndexCap = 0; + coll->keyIndexUsed = 0; +} + + +// Put keyed item pos in the key index (which has room). +static void collectionIndexPut(BasObjectT *coll, int32_t pos) { + uint32_t mask = (uint32_t)coll->keyIndexCap - 1; + uint32_t slot = coll->items[pos].keyHash & mask; + + while (coll->keyIndex[slot] != 0) { + slot = (slot + 1) & mask; + } + + coll->keyIndex[slot] = pos + 1; + coll->keyIndexUsed++; +} + + +// The key index, built when missing; false when there is no memory for it +// (the caller looks through the items instead). +static bool collectionIndexReady(BasObjectT *coll) { + if (coll->keyIndex) { + return true; + } + + int32_t keyed = 0; + int32_t cap = BAS_COLL_INDEX_MIN; + + for (int32_t i = 0; i < coll->itemCount; i++) { + keyed += coll->items[i].key ? 1 : 0; + } + + while (cap < keyed * BAS_COLL_INDEX_ROOM) { + cap *= 2; + } + + coll->keyIndex = (int32_t *)calloc((size_t)cap, sizeof(int32_t)); + + if (!coll->keyIndex) { + return false; + } + + coll->keyIndexCap = cap; + coll->keyIndexUsed = 0; + + for (int32_t i = 0; i < coll->itemCount; i++) { + if (coll->items[i].key) { + collectionIndexPut(coll, i); + } + } + + return true; +} + + +// The position of the item with key (case ignored), or -1: through the +// key index, so a big keyed collection is not read item by item. +static int32_t collectionKeyPos(BasObjectT *coll, const BasStringT *key) { uint32_t hash = keyHash(key); + if (collectionIndexReady(coll)) { + uint32_t mask = (uint32_t)coll->keyIndexCap - 1; + + for (uint32_t slot = hash & mask; coll->keyIndex[slot] != 0; slot = (slot + 1) & mask) { + const BasCollItemT *item = &coll->items[coll->keyIndex[slot] - 1]; + + if (item->keyHash == hash && basStringCompareCI(item->key, key) == 0) { + return coll->keyIndex[slot] - 1; + } + } + + return -1; + } + for (int32_t i = 0; i < coll->itemCount; i++) { if (coll->items[i].keyHash == hash && coll->items[i].key && basStringCompareCI(coll->items[i].key, key) == 0) { return i; @@ -5799,27 +5989,39 @@ static void dirClose(BasVmT *vm) { // Next Dir$ match. Like QB/VB Dir$ without a directory attribute, only -// files are returned: ".", ".." and subdirectories are skipped. -static const char *dirNext(BasVmT *vm) { +// files are returned: ".", ".." and subdirectories are skipped. NULL at +// the end, or when the program's events (run along the way) started a new +// Dir$ or said stop (*stopped). +static const char *dirNext(BasVmT *vm, bool *stopped) { if (!vm->dirHandle) { return NULL; } + DIR *dir = (DIR *)vm->dirHandle; + int32_t passed = 0; struct dirent *ent; - while ((ent = readdir((DIR *)vm->dirHandle)) != NULL) { + while ((ent = readdir(dir)) != NULL) { + // A long run of entries passed over lets the program's events run; + // one that starts a new Dir$ meanwhile ends this one + if (++passed % BAS_DIR_PUMP_ENTRIES == 0 && vm->runLimit <= 0 && vm->doEventsFn) { + *stopped = !basVmDoEvents(vm); + + if (*stopped || vm->dirHandle != dir) { + return NULL; + } + } + if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) { continue; } - if (fnmatch(vm->dirPattern, ent->d_name, FNM_CASEFOLD) != 0) { + // DOS's wildcards, as the file dialog has them: "*.*" is every name + if (strcmp(vm->dirPattern, PLATFORM_FILTER_ALL) != 0 && !platformGlobMatch(vm->dirPattern, ent->d_name)) { continue; } - char full[DVX_MAX_PATH]; - struct stat st; - - if (platformPathJoin(full, sizeof(full), vm->dirPath, ent->d_name) && stat(full, &st) == 0 && S_ISDIR(st.st_mode)) { + if (platformDirEntryIsDir(vm->dirPath, ent->d_name, ent->d_type)) { continue; } @@ -5831,6 +6033,21 @@ static const char *dirNext(BasVmT *vm) { } +// Dir$'s next name on the stack ("" when there is none); the program +// halts if its events said stop meanwhile. +static BasVmResultE dirPush(BasVmT *vm) { + bool stopped = false; + const char *name = dirNext(vm, &stopped); + + if (stopped) { + vm->running = false; + return BAS_VM_HALTED; + } + + return push(vm, basValStringFromC(name)) ? BAS_VM_OK : BAS_VM_STACK_OVERFLOW; +} + + // Releases the top call frame without returning through it: its locals // are released, its FOR frames dropped and the caller's statement // boundary restored. Used when an error unwinds frames (dispatchError, @@ -6236,6 +6453,9 @@ static const char *errorText(int32_t errNum) { case BAS_ERR_OUT_OF_STRING: return "Out of string space"; + case BAS_ERR_INTERRUPTED: + return "User interrupt occurred"; + case BAS_ERR_RESUME_WITHOUT_ERR: return "RESUME without error"; @@ -6939,6 +7159,10 @@ static BasVmResultE execDevice(BasVmT *vm, uint8_t op) { return BAS_VM_ERROR; } + if (waitRefused(vm)) { + return BAS_VM_ERROR; + } + d->frameStart(d->ctx, hz); if (!waitWhile(vm, d->frameBusy)) { @@ -6977,8 +7201,9 @@ static BasVmResultE execDevice(BasVmT *vm, uint8_t op) { } } - // BEEP, SOUND and PLAY wait while the music is in the foreground. - if (d->soundBusy && !waitWhile(vm, d->soundBusy)) { + // BEEP, SOUND and PLAY wait while the music is in the foreground (not + // in code that must not block: it plays on as background music). + if (d->soundBusy && vm->runLimit <= 0 && !waitWhile(vm, d->soundBusy)) { vm->running = false; return BAS_VM_HALTED; } @@ -7722,11 +7947,22 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) { return BAS_VM_STACK_UNDERFLOW; } - bool copied = platformCopyFile(srcStr.strVal->data, dstStr.strVal->data); + BasCopyPumpT pump; + + pump.vm = vm; + pump.pumpAt = wallSeconds(); + pump.stopped = false; + + bool copied = platformCopyFileWith(srcStr.strVal->data, dstStr.strVal->data, fileCopyGoOn, &pump); basValRelease(&srcStr); basValRelease(&dstStr); + if (pump.stopped) { + vm->running = false; + return BAS_VM_HALTED; + } + if (!copied) { runtimeError(vm, BAS_ERR_FILE_NOT_FOUND, "File not found or cannot create destination"); return BAS_VM_FILE_ERROR; @@ -7856,21 +8092,11 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) { } vm->dirHandle = opendir(vm->dirPath); - - if (!push(vm, basValStringFromC(dirNext(vm)))) { - return BAS_VM_STACK_OVERFLOW; - } - - break; + return dirPush(vm); } - case OP_FS_DIR_NEXT: { - if (!push(vm, basValStringFromC(dirNext(vm)))) { - return BAS_VM_STACK_OVERFLOW; - } - - break; - } + case OP_FS_DIR_NEXT: + return dirPush(vm); case OP_FS_FILELEN: { BasValueT fnStr; @@ -8561,6 +8787,46 @@ static BasVmResultE execReturn(BasVmT *vm, bool withValue) { } +// SHELL command: run it with DVX stepped aside through the host (keeping +// its events running until the command is over) and push its exit code; +// outside DVX, system(). An empty command does nothing (0). +static BasVmResultE execShell(BasVmT *vm) { + BasValueT cmdVal; + const BasDeviceCallbacksT *d = &vm->device; + int32_t result = 0; + + if (!pop(vm, &cmdVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT cmdStr = basValToString(cmdVal); + + basValRelease(&cmdVal); + + if (cmdStr.strVal && cmdStr.strVal->len > 0 && waitRefused(vm)) { + basValRelease(&cmdStr); + return BAS_VM_ERROR; + } + + if (cmdStr.strVal && cmdStr.strVal->len > 0) { + if (!d->shellStart) { + result = system(cmdStr.strVal->data); + } else if (!d->shellStart(d->ctx, cmdStr.strVal->data)) { + result = -1; + } else if (!waitWhile(vm, d->shellBusy)) { + basValRelease(&cmdStr); + vm->running = false; + return BAS_VM_HALTED; + } else { + result = d->shellResult(d->ctx); + } + } + + basValRelease(&cmdStr); + return push(vm, basValInteger((int16_t)result)) ? BAS_VM_OK : BAS_VM_STACK_OVERFLOW; +} + + // OP_STORE_LOCAL / OP_STORE_GLOBAL [uint16 idx] and their _CONV forms // [uint16 idx] [uint8 conv], which first run conversion opcode conv on the // value: pops the value into the variable (a local holding a reference @@ -9448,6 +9714,23 @@ static inline uint16_t fetchU16(const uint8_t *p) { } +// FileCopy between pieces: every BAS_PUMP_SEC the program's events run, +// as DoEvents (not in a control's code); false (the copy stops) once the +// program is asked to end. +static bool fileCopyGoOn(void *ctx) { + BasCopyPumpT *pump = (BasCopyPumpT *)ctx; + double now = wallSeconds(); + + if (pump->vm->runLimit > 0 || !pump->vm->doEventsFn || now - pump->pumpAt < BAS_PUMP_SEC) { + return true; + } + + pump->pumpAt = now; + pump->stopped = !basVmDoEvents(pump->vm); + return !pump->stopped; +} + + // WIDTH for a file or device (BAS_FN_FILE_WIDTH): target is a channel, // BAS_WIDTH_LPRINT or a device -- SCRN: (the screen's WIDTH), LPT1: .. // LPT3: or PRN (a channel opened on it later). width 1 .. 255; 255 is no @@ -10139,15 +10422,36 @@ static int32_t indexArray(const BasArrayT *arr, BasValueT *args, int32_t argc, i } +// The file IniRead and IniWrite work on, parsed: the one kept from the +// last call when it is the same file and unchanged since (its size and +// time the same, and not changed in the last BAS_INI_TIME_SEC seconds, +// which a FAT time cannot tell apart), else read again and kept. NULL +// when out of memory. +static PrefsHandleT *iniPrefs(BasVmT *vm, const char *path) { + struct stat st; + bool have = stat(path, &st) == 0; + int64_t size = have ? (int64_t)st.st_size : BAS_INI_NO_FILE; + int64_t when = have ? (int64_t)st.st_mtime : 0; + + if (vm->iniPrefs && platformPathSame(path, vm->iniPath) && size == vm->iniSize && when == vm->iniTime && (!have || (int64_t)time(NULL) - when > BAS_INI_TIME_SEC)) { + return (PrefsHandleT *)vm->iniPrefs; + } + + prefsClose((PrefsHandleT *)vm->iniPrefs); + vm->iniPrefs = prefsLoad(path); + vm->iniSize = size; + vm->iniTime = when; + snprintf(vm->iniPath, sizeof(vm->iniPath), "%s", path); + return (PrefsHandleT *)vm->iniPrefs; +} + + // IniRead: the value of key in [sec] of path through dvxPrefs, or a // copy of defStr when the file, section or key is missing. -static BasValueT iniRead(const char *path, const char *sec, const char *key, BasStringT *defStr) { - PrefsHandleT *h = prefsLoad(path); - const char *value = prefsGetString(h, sec, key, NULL); - BasValueT result = value ? basValStringFromC(value) : basValString(defStr); +static BasValueT iniRead(BasVmT *vm, const char *path, const char *sec, const char *key, BasStringT *defStr) { + const char *value = prefsGetString(iniPrefs(vm, path), sec, key, NULL); - prefsClose(h); - return result; + return value ? basValStringFromC(value) : basValString(defStr); } @@ -10157,7 +10461,7 @@ static BasValueT iniRead(const char *path, const char *sec, const char *key, Bas // end of the file. A file that exists but cannot be read is never // rewritten, and a failed save leaves the old file intact (error 75). static BasVmResultE iniWrite(BasVmT *vm, const char *path, const char *sec, const char *key, const char *val) { - PrefsHandleT *h = prefsLoad(path); + PrefsHandleT *h = iniPrefs(vm, path); if (!h) { runtimeError(vm, BAS_ERR_OUT_OF_MEMORY, "Out of memory"); @@ -10166,9 +10470,12 @@ static BasVmResultE iniWrite(BasVmT *vm, const char *path, const char *sec, cons prefsSetString(h, sec, key, val); + // What was saved is read back next time (its new size and time are + // not known here); what failed to save is not kept either bool saved = prefsSave(h); prefsClose(h); + vm->iniPrefs = NULL; if (!saved) { runtimeError(vm, BAS_ERR_PATH_FILE_ACCESS, "Cannot write INI file"); @@ -10428,11 +10735,10 @@ static inline int32_t jumpTarget(const BasVmT *vm, int32_t offset) { // A Collection key's hash: FNV-1a of its text with case folded as // basStringCompareCI folds it, so keys that match have equal hashes. static uint32_t keyHash(const BasStringT *key) { - uint32_t h = BAS_FNV1A_OFFSET; + uint32_t h = DVX_FNV_BASIS; for (int32_t i = 0; i < key->len; i++) { - h ^= (uint32_t)toupper((unsigned char)key->data[i]); - h *= BAS_FNV1A_PRIME; + h = dvxFnvByte(h, (uint8_t)toupper((unsigned char)key->data[i])); } return h; @@ -10457,15 +10763,15 @@ static bool layoutNext(const BasStringT *layout, int32_t *pos, BasLayoutEntryT * return false; } - out->fixedLen = d[*pos] | (d[*pos + 1] << 8); + out->fixedLen = basReadU16LE(d + *pos); *pos += 2; } else if (out->type == BAS_TYPE_UDT) { if (*pos + 4 > layout->len) { return false; } - out->typeId = d[*pos] | (d[*pos + 1] << 8); - out->fieldCount = d[*pos + 2] | (d[*pos + 3] << 8); + out->typeId = basReadU16LE(d + *pos); + out->fieldCount = basReadU16LE(d + *pos + 2); *pos += 4; } @@ -10628,13 +10934,30 @@ static int32_t likeMatch(const char *s, int32_t sLen, const char *p, int32_t pLe // Local wall-clock time, with the sub-second part in microseconds when -// outUsec is non-NULL. Shared by TIMER, DATE$ and TIME$. -static void localNow(struct tm *out, int32_t *outUsec) { +// outUsec is non-NULL. Shared by TIMER, DATE$, TIME$ and NOW (vm NULL: +// whole seconds only). With a fine +// clock (basVmSetMicrosClock) the time since the wall clock's reading last +// moved is added -- never a whole BIOS tick, its step on DOS -- so TIMER +// keeps rising in fine steps and never passes the next reading. +static void localNow(BasVmT *vm, struct tm *out, int32_t *outUsec) { struct timeval tv; gettimeofday(&tv, NULL); - time_t secs = tv.tv_sec; + int64_t wall = (int64_t)tv.tv_sec * BAS_USEC_PER_SEC_INT + tv.tv_usec; + + if (vm && vm->microsFn) { + uint64_t now = vm->microsFn(); + + if (wall != vm->timerWall) { + vm->timerWall = wall; + vm->timerSeen = now; + } + + wall += DVX_MIN((int64_t)(now - vm->timerSeen), BAS_CLOCK_TICK_US - 1); + } + + time_t secs = (time_t)(wall / BAS_USEC_PER_SEC_INT); struct tm *local = localtime(&secs); if (local) { @@ -10644,7 +10967,7 @@ static void localNow(struct tm *out, int32_t *outUsec) { } if (outUsec) { - *outUsec = (int32_t)tv.tv_usec; + *outUsec = (int32_t)(wall % BAS_USEC_PER_SEC_INT); } } @@ -11378,13 +11701,16 @@ static bool operandDefaults(BasVmT *vm, int32_t count, BasVmResultE *rc) { BasValueT *v = &vm->stack[slot]; if (v->type == BAS_TYPE_OBJECT && v->objVal != NULL && vm->ui.defaultProp != NULL && vm->ui.getProp != NULL) { + // A default can be an object with a default of its own (an + // Image's Picture, whose default is its file name) const char *name = vm->ui.defaultProp(vm->ui.ctx, v->objVal); - if (name != NULL) { + while (name != NULL) { BasValueT val = vm->ui.getProp(vm->ui.ctx, v->objVal, name); basValRelease(v); - *v = val; + *v = val; + name = (v->type == BAS_TYPE_OBJECT && v->objVal != NULL) ? vm->ui.defaultProp(vm->ui.ctx, v->objVal) : NULL; } continue; @@ -12696,6 +13022,18 @@ static BasVmResultE roundedOperand(BasVmT *vm, BasValueT v, int32_t *out) { // labels for unused and the label addresses in the table for Booleans. // cppcheck-suppress-begin [unusedLabelConfiguration, assignBoolToPointer] __attribute__((optimize("no-gcse", "omit-frame-pointer", "align-labels=16"))) +// basVmSetRunLimit: one more slice of a run or call; true (error 18 set) +// when that is more than the limit. +static bool runaway(BasVmT *vm, int32_t *slices) { + if (vm->runLimit <= 0 || ++*slices <= vm->runLimit) { + return false; + } + + runtimeError(vm, BAS_ERR_INTERRUPTED, BAS_RUNAWAY_MSG); + return true; +} + + static BasVmResultE runFast(BasVmT *vm, int32_t budget, int32_t floorDepth, int32_t *outSteps, int32_t *outPc) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Woverride-init" @@ -12780,6 +13118,7 @@ static BasVmResultE runFast(BasVmT *vm, int32_t budget, int32_t floorDepth, int3 int32_t left = budget; int32_t startPc = 0; bool lineHooks = false; + bool timed = vm->sliceClock != NULL && budget != BAS_RUN_UNLIMITED; uint8_t op = OP_NOP; BasVmResultE rc = BAS_VM_OK; @@ -13605,6 +13944,11 @@ fastAfter: return BAS_VM_OK; } + // The slice's time is up (a long call): it ends as its steps would + if (timed && *vm->sliceClock - vm->sliceStart >= vm->sliceSpan) { + left = 0; + } + FAST_LOAD(); FAST_NEXT(); @@ -13652,8 +13996,11 @@ fastResume: // event handlers. static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool savedRunning) { int32_t stepsSinceYield = 0; + int32_t slices = 0; // a run limit's count bool hadBreakpoint = false; + sliceBegin(vm); + while (vm->running && vm->callDepth > savedCallDepth) { int32_t steps = 0; int32_t stepPc = vm->pc; // where a failing instruction started, for ON ERROR dispatch @@ -13681,18 +14028,13 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool if (!vm->doEventsFn(vm->doEventsCtx)) { // Host is shutting down: drop the paused frames like // every other exit path before handing back. - while (vm->callDepth > savedCallDepth) { - discardFrame(vm); - } - - vm->pc = savedPc; - vm->running = savedRunning; - return false; + return subLoopFail(vm, savedPc, savedCallDepth, savedRunning); } } // User resumed -- continue executing the sub stepsSinceYield = 0; + sliceBegin(vm); continue; } @@ -13705,27 +14047,24 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool continue; } - // Untrapped: the frames still hold refcounted locals; discard - // them so force-restoring callDepth cannot orphan them above - // releaseVmState's reach. Not popCallFrame: it would clear the - // error state the host reads. - while (vm->callDepth > savedCallDepth) { - discardFrame(vm); - } - - vm->pc = savedPc; - vm->running = savedRunning; - return false; + return subLoopFail(vm, savedPc, savedCallDepth, savedRunning); } - // Yield periodically to keep the GUI responsive + // Yield periodically to keep the GUI responsive; code with a run + // limit that has run too long is stopped (ON ERROR cannot trap it) if (stepsSinceYield >= BAS_VM_DEFAULT_STEP_SLICE) { stepsSinceYield = 0; + if (runaway(vm, &slices)) { + return subLoopFail(vm, savedPc, savedCallDepth, savedRunning); + } + if (!basVmDoEvents(vm)) { vm->running = false; break; } + + sliceBegin(vm); } } @@ -13912,6 +14251,12 @@ static bool sleepSeconds(BasVmT *vm, int32_t secs) { } +// A slice starts now: the slice clock's reading it is measured from. +static void sliceBegin(BasVmT *vm) { + vm->sliceStart = vm->sliceClock ? *vm->sliceClock : 0; +} + + // Reads up to len bytes into dst (none from a source with neither a file // nor memory). Returns how many there were. static int32_t sourceRead(BasGetSourceT *src, void *dst, int32_t len) { @@ -14094,6 +14439,22 @@ static BasValueT strValue(BasStringT *s) { } +// runSubLoop's way out without finishing the call (an untrapped error, a +// run limit, the host shutting down): the frames still hold refcounted +// locals; they are discarded so force-restoring callDepth cannot orphan +// them above releaseVmState's reach (not popCallFrame: it would clear the +// error state the host reads). Returns false, runSubLoop's failure. +static bool subLoopFail(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool savedRunning) { + while (vm->callDepth > savedCallDepth) { + discardFrame(vm); + } + + vm->pc = savedPc; + vm->running = savedRunning; + return false; +} + + // Releases every object the VM made that is still alive when it is // destroyed. Each object is held while all of them let go of what they // hold (breaking reference cycles), then the holds are dropped: objects @@ -14692,6 +15053,18 @@ static int32_t varTypeOf(uint8_t type) { } +// basVmSetRunLimit: code that must not block refuses to wait (error 5 +// set: true). +static bool waitRefused(BasVmT *vm) { + if (vm->runLimit <= 0) { + return false; + } + + runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, BAS_NO_WAIT_MSG); + return true; +} + + // Keep the host's events running until busy says the wait is over (or // just check again after a pause without an event pump). False when the // host asks the program to stop. diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.h b/src/apps/kpunch/dvxbasic/runtime/vm.h index 7f2bda2f..14ca05e4 100644 --- a/src/apps/kpunch/dvxbasic/runtime/vm.h +++ b/src/apps/kpunch/dvxbasic/runtime/vm.h @@ -41,6 +41,7 @@ #include "values.h" #include "../compiler/basEvents.h" #include "../compiler/opcodes.h" +#include "../../../../libs/kpunch/sql/dvxSql.h" #include "dvxTypes.h" #include "dvxPlat.h" @@ -228,56 +229,65 @@ typedef const char *(*BasUiDefaultPropFnT)(void *ctx, void *objRef); // cannot be read. typedef void *(*BasUiLoadPictureFnT)(void *ctx, const char *path, int32_t *outErr); +// LoadPictureData(bytes): a Picture object for the len bytes of a picture +// file ("" = an empty picture; the same bytes give the same object). NULL +// with *outErr set to a BASIC error (invalid picture, out of memory) when +// they are not a picture DVX can show. +typedef void *(*BasUiLoadPictureDataFnT)(void *ctx, const char *data, int32_t len, int32_t *outErr); + // LoadFormText(text): a form loaded from .frm text (its own copy when a // form of its name is loaded), NULL when the text does not load. typedef void *(*BasUiLoadFormTextFnT)(void *ctx, const char *text); // Collected UI callbacks typedef struct { - BasUiGetPropFnT getProp; - BasUiSetPropFnT setProp; - BasUiCallMethodFnT callMethod; - BasUiCreateCtrlFnT createCtrl; - BasUiFindCtrlFnT findCtrl; - BasUiFindCtrlIdxFnT findCtrlIdx; - BasUiLoadFormFnT loadForm; - BasUiUnloadFormFnT unloadForm; - BasUiShowFormFnT showForm; - BasUiHideFormFnT hideForm; - BasUiMsgBoxFnT msgBox; - BasUiInputBoxFnT inputBox; - BasUiCreateFormFnT createForm; - BasUiNewFormFnT newForm; - BasUiCreateCtrlExFnT createCtrlEx; - BasUiSetEventFnT setEvent; - BasUiRemoveCtrlFnT removeCtrl; - BasUiTypeNameFnT typeName; - BasUiLoadPictureFnT loadPicture; - BasUiDefaultPropFnT defaultProp; - BasUiLoadFormTextFnT loadFormText; - void *ctx; // passed as first arg to all callbacks + BasUiGetPropFnT getProp; + BasUiSetPropFnT setProp; + BasUiCallMethodFnT callMethod; + BasUiCreateCtrlFnT createCtrl; + BasUiFindCtrlFnT findCtrl; + BasUiFindCtrlIdxFnT findCtrlIdx; + BasUiLoadFormFnT loadForm; + BasUiUnloadFormFnT unloadForm; + BasUiShowFormFnT showForm; + BasUiHideFormFnT hideForm; + BasUiMsgBoxFnT msgBox; + BasUiInputBoxFnT inputBox; + BasUiCreateFormFnT createForm; + BasUiNewFormFnT newForm; + BasUiCreateCtrlExFnT createCtrlEx; + BasUiSetEventFnT setEvent; + BasUiRemoveCtrlFnT removeCtrl; + BasUiTypeNameFnT typeName; + BasUiLoadPictureFnT loadPicture; + BasUiDefaultPropFnT defaultProp; + BasUiLoadFormTextFnT loadFormText; + BasUiLoadPictureDataFnT loadPictureData; + void *ctx; // passed as first arg to all callbacks } BasUiCallbacksT; // ============================================================ -// SQL callbacks (host-provided) +// SQL callbacks (host-provided: the dvxsql library's functions, by type) // ============================================================ typedef struct { - int32_t (*sqlOpen)(const char *path); - void (*sqlClose)(int32_t db); - bool (*sqlExec)(int32_t db, const char *sql); - const char *(*sqlError)(int32_t db); - int32_t (*sqlQuery)(int32_t db, const char *sql); - bool (*sqlNext)(int32_t rs); - bool (*sqlEof)(int32_t rs); - int32_t (*sqlFieldCount)(int32_t rs); - const char *(*sqlFieldName)(int32_t rs, int32_t col); - const char *(*sqlFieldText)(int32_t rs, int32_t col); - const char *(*sqlFieldByName)(int32_t rs, const char *name); - int32_t (*sqlFieldInt)(int32_t rs, int32_t col); - double (*sqlFieldDbl)(int32_t rs, int32_t col); - void (*sqlFreeResult)(int32_t rs); - int32_t (*sqlAffectedRows)(int32_t db); + __typeof__(dvxSqlOpen) *sqlOpen; + __typeof__(dvxSqlClose) *sqlClose; + __typeof__(dvxSqlExec) *sqlExec; + __typeof__(dvxSqlError) *sqlError; + __typeof__(dvxSqlQuery) *sqlQuery; + __typeof__(dvxSqlNext) *sqlNext; + __typeof__(dvxSqlEof) *sqlEof; + __typeof__(dvxSqlFieldCount) *sqlFieldCount; + __typeof__(dvxSqlFieldName) *sqlFieldName; + __typeof__(dvxSqlFieldText) *sqlFieldText; + __typeof__(dvxSqlFieldByName) *sqlFieldByName; + __typeof__(dvxSqlFieldInt) *sqlFieldInt; + __typeof__(dvxSqlFieldDbl) *sqlFieldDbl; + __typeof__(dvxSqlFreeResult) *sqlFreeResult; + __typeof__(dvxSqlAffectedRows) *sqlAffectedRows; + __typeof__(dvxSqlSetPump) *sqlSetPump; + __typeof__(dvxSqlEscape) *sqlEscape; } BasSqlCallbacksT; // ============================================================ @@ -313,9 +323,13 @@ typedef struct { // soundBusy says the statement must still wait (foreground music); after // frameStart(hz) (the host's frame clock moves on to its next frame at hz // frames a second) likewise while frameBusy says that frame has not -// begun. Without beep / sound / play the statements do nothing and -// PLAY(n) is 0, without stick / strig they read 0, and FRAMEWAIT without -// frameStart raises "Device unavailable". +// begun. SHELL hands its command line to shellStart (the host runs it +// with DVX stepped aside: false when it cannot), keeps the host's events +// running while shellBusy, and returns shellResult (the exit code, -1 when +// it did not run). Without beep / sound / play the statements do nothing +// and PLAY(n) is 0, without stick / strig they read 0, FRAMEWAIT without +// frameStart raises "Device unavailable", and SHELL without shellStart +// runs the command with system() (BASIC outside DVX: the runner, BASCOMP). // QBasic's ranges: SOUND frequency (Hz) and duration (clock ticks, 18.2 // a second), STICK(0..3), STRIG(0..7) @@ -335,6 +349,9 @@ typedef struct { int32_t (*strig)(void *ctx, int32_t n); void (*frameStart)(void *ctx, int32_t hz); bool (*frameBusy)(void *ctx); + bool (*shellStart)(void *ctx, const char *command); + bool (*shellBusy)(void *ctx); + int32_t (*shellResult)(void *ctx); void *ctx; } BasDeviceCallbacksT; @@ -837,6 +854,17 @@ typedef struct { void *inputCtx; BasDoEventsFnT doEventsFn; void *doEventsCtx; + + // The slice clock (basVmSetSliceClock): a counter the host's timer + // interrupt advances, read after each instruction run out of line + // (calls, properties, built-ins); NULL: slices count steps only. + const volatile uint32_t *sliceClock; + uint32_t sliceSpan; // its counts a slice may last + uint32_t sliceStart; // its value when the slice began + uint64_t (*microsFn)(void); // basVmSetMicrosClock: TIMER's fine clock (NULL: the wall clock alone) + int64_t timerWall; // TIMER: the wall clock's last reading, in microseconds (-1: none yet) + uint64_t timerSeen; // microsFn when it came + int32_t runLimit; // slices one run or call may last, waits refused (basVmSetRunLimit; 0: no limit) BasBreakpointFnT breakpointFn; void *breakpointCtx; BasEventHostFnT eventHostFn; // UserControl events to the host (NULL: none) @@ -907,6 +935,12 @@ typedef struct { void *dirHandle; // DIR * (opaque here; cast in vm.c) char dirPattern[DVX_MAX_PATH]; // filename glob for Dir$ char dirPath[DVX_MAX_PATH]; // directory being enumerated + + // IniRead / IniWrite's last file, kept parsed (iniPrefs in vm.c) + void *iniPrefs; // PrefsHandleT * (NULL = none kept) + char iniPath[DVX_MAX_PATH]; + int64_t iniSize; // its size and time when kept (-1: there was no file) + int64_t iniTime; } BasVmT; // ============================================================ @@ -955,6 +989,30 @@ void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx); void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx); void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx); +// For a VM with no event pump that must never keep the CPU (an installed +// control file's, shared by every program): a run, or a call into it (an +// event), that lasts more than slices slices is stopped with error 18, +// which ON ERROR cannot trap; SLEEP, FRAMEWAIT, and SHELL raise error 5 +// instead of blocking, and BEEP, SOUND, and PLAY do not wait for +// foreground music. 0: no limit (the default). +void basVmSetRunLimit(BasVmT *vm, int32_t slices); + +// Give the VM a clock for its slices: clock is a counter the host's timer +// interrupt advances (reading it costs one load), span how far it may move +// in one slice. A slice (a basVmRun with a step limit, or the stretch of +// an event handler between two doEvents) then also ends once the clock has +// moved span, at the first instruction run out of line after that, so a +// loop of long calls (a file copy, a SQL statement) gives the host its turn +// as often as a loop of quick instructions does. NULL: steps only. +void basVmSetSliceClock(BasVmT *vm, const volatile uint32_t *clock, uint32_t span); + +// A fine monotonic clock in microseconds (platformMicros) for TIMER: the +// wall clock moves in coarse steps on DOS (a BIOS tick, about 55 ms), and +// the time since it last moved is added from this, never a whole step, so +// TIMER rises smoothly and never passes the wall clock's next reading. +// NULL: the wall clock alone. +void basVmSetMicrosClock(BasVmT *vm, uint64_t (*micros)(void)); + // Set the printer callbacks (LPRINT, OPEN "LPTn:"). void basVmSetPrinterCallbacks(BasVmT *vm, const BasPrinterCallbacksT *printer); @@ -965,8 +1023,10 @@ void basVmSetDeviceCallbacks(BasVmT *vm, const BasDeviceCallbacksT *device); // Set the program screen callbacks. void basVmSetScreenHost(BasVmT *vm, const BasScreenHostT *host); -// Set UI callbacks (for form/control system). +// Set the SQL callbacks (the dvxsql library's functions). void basVmSetSqlCallbacks(BasVmT *vm, const BasSqlCallbacksT *sql); + +// Set UI callbacks (for form/control system). void basVmSetUiCallbacks(BasVmT *vm, const BasUiCallbacksT *ui); // Set external library callbacks (for DECLARE LIBRARY support). diff --git a/src/apps/kpunch/dvxbasic/stub/bascomp.c b/src/apps/kpunch/dvxbasic/stub/bascomp.c index 78684fba..e935b2e6 100644 --- a/src/apps/kpunch/dvxbasic/stub/bascomp.c +++ b/src/apps/kpunch/dvxbasic/stub/bascomp.c @@ -181,7 +181,7 @@ int main(int argc, char **argv) { printf("Compiling %d file(s)...\n", (int)fileCount); BasCompileErrorT err; - BasModuleT *mod = basBuildCompile(project.source, project.sourceLen, project.optionExplicit, NULL, NULL, &err); + BasModuleT *mod = basBuildCompile(project.source, project.sourceLen, project.optionExplicit, NULL, NULL, &err, NULL, NULL); if (!mod) { const char *errPath = NULL; diff --git a/src/apps/kpunch/dvxbasic/stub/basstub.c b/src/apps/kpunch/dvxbasic/stub/basstub.c index 979cb7d9..948c111b 100644 --- a/src/apps/kpunch/dvxbasic/stub/basstub.c +++ b/src/apps/kpunch/dvxbasic/stub/basstub.c @@ -51,13 +51,11 @@ // App descriptor // ============================================================ -#define STUB_STACK_SIZE 65536 - SHELL_APP_DESCRIPTOR( .name = BAS_DEFAULT_APP_NAME, .hasMainLoop = true, .multiInstance = false, - .stackSize = STUB_STACK_SIZE, + .stackSize = BAS_RUN_STACK_SIZE, .priority = TS_PRIORITY_NORMAL ); @@ -71,7 +69,6 @@ static BasFormRtT *sRt = NULL; // live while appMain runs the program (F1 que // Function prototypes (alphabetical; appMain last) -void appShutdown(void); static bool stubDoEvents(void *ctx); static void stubHelpQuery(void *ctx); static bool stubInput(void *ctx, const char *prompt, char *buf, int32_t bufSize); @@ -80,16 +77,6 @@ static void stubPrint(void *ctx, const char *text, bool newline); int32_t appMain(DxeAppContextT *ctx); -// Resolved by the shell as _appShutdown and called when the app is reaped -// after appMain returned (which already ran basFormRtDestroy, so this -// finds the serial ports closed). A force kill runs none of the app's -// code: basrt's kill hook (basRunHostAppKilled) ends the runtime then, -// serial ports and idle pollers included, while its windows are there. -void appShutdown(void) { - basFormRtSerialShutdown(); -} - - static bool stubDoEvents(void *ctx) { (void)ctx; @@ -97,7 +84,7 @@ static bool stubDoEvents(void *ctx) { return false; } - return dvxUpdate(sAc); + return sRt ? basFormRtUpdate(sRt) : dvxUpdate(sAc); } diff --git a/src/apps/kpunch/dvxbasic/test_compiler.c b/src/apps/kpunch/dvxbasic/test_compiler.c index 23bec59a..b1917963 100644 --- a/src/apps/kpunch/dvxbasic/test_compiler.c +++ b/src/apps/kpunch/dvxbasic/test_compiler.c @@ -7100,8 +7100,9 @@ static void testScreenModesPages(void) { // Phase 1 library functions: the string functions (REPLACE$, SPLIT, JOIN$, -// INSTRREV, STRCOMP, STRREVERSE$, FILTER, LSET / RSET, IIF, CHOOSE) and -// the DATE type with its functions, literals, arithmetic and Format$. +// INSTRREV, STRCOMP, STRREVERSE$, CHRW$, ASCW, FILTER, LSET / RSET, IIF, +// CHOOSE) and the DATE type with its functions, literals, arithmetic and +// Format$. static void testStringsAndDates(void) { runProgram("Replace$", "PRINT Replace$(\"aXbXc\", \"X\", \"-\")\n" @@ -7164,6 +7165,25 @@ static void testStringsAndDates(void) { "apple-banana-cherry\napple banana cherry\nxyz\n1,22,333\n[]\n0 banana\napple+cherry\nbanana\n-1 \n" ); + runProgram("Dir$ takes DOS's wildcards: *.* is every file, [ is a plain character", + "MKDIR \"dirwild\"\n" + "OPEN \"dirwild/plain\" FOR OUTPUT AS #1: CLOSE #1\n" + "OPEN \"dirwild/a[1].txt\" FOR OUTPUT AS #1: CLOSE #1\n" + "OPEN \"dirwild/b.TXT\" FOR OUTPUT AS #1: CLOSE #1\n" + "n = 0\n" + "f$ = Dir$(\"dirwild/*.*\")\n" + "DO WHILE f$ <> \"\"\n" + " n = n + 1\n" + " f$ = Dir$()\n" + "LOOP\n" + "PRINT n; Dir$(\"dirwild/a[1]*\"); \" \"; Dir$(\"dirwild/B.t?t\")\n" + "KILL \"dirwild/plain\"\n" + "KILL \"dirwild/a[1].txt\"\n" + "KILL \"dirwild/b.TXT\"\n" + "RMDIR \"dirwild\"\n", + "3 a[1].txt b.TXT\n" + ); + runProgram("InStrRev, StrComp and StrReverse$", "PRINT InStrRev(\"abcabc\", \"bc\")\n" "PRINT InStrRev(\"abcabc\", \"bc\", 4)\n" @@ -7178,6 +7198,26 @@ static void testStringsAndDates(void) { "5 \n2 \n0 \n3 \n2 \n0 \n0 \n-1 1 0 \n-1 0 -1 \ncba[]\n" ); + runProgram("FULLSCREEN reads the program screen's state without making one", + "PRINT FULLSCREEN; NOT FULLSCREEN\n", + "False True \n" + ); + + runProgram("ChrW$ and AscW: Unicode code points as DVX text", + "c = 130\n" + "PRINT AscW(\"A\"); AscW(Chr$(10)); AscW(Chr$(c)); AscW(Chr$(196))\n" + "PRINT Asc(ChrW$(&HE9)); Asc(ChrW(&H2500)); Asc(ChrW$(65)); Asc(ChrW$(&H263A))\n" + "PRINT ChrW$(&H4E2D); LEN(ChrW$(&H1F600))\n" + "e$ = \"\"\n" + "ON ERROR GOTO h\n" + "PRINT AscW(e$)\n" + "PRINT ChrW$(-1)\n" + "PRINT ChrW$(&H110000)\n" + "END\n" + "h: PRINT \"err\"; ERR: RESUME NEXT\n", + "65 10 233 9472 \n130 196 65 1 \n?1 \nerr5 \nerr5 \nerr5 \n" + ); + runProgram("Drag and clipboard constants; Control, Form and Object types", "PRINT vbManual; vbAutomatic; vbBeginDrag; vbEndDrag\n" "PRINT vbEnter; vbLeave; vbOver; vbCFText\n" @@ -8716,7 +8756,9 @@ int main(void) { } printf(found ? "PASS: OP_CALL_EXTERN emitted\n" : "FAIL: OP_CALL_EXTERN not emitted\n"); - if (!found) { gFailCount++; } + if (!found) { + gFailCount++; + } basModuleFree(mod); } else { printf("MODULE BUILD FAILED\n"); @@ -8792,7 +8834,9 @@ int main(void) { printf("Calling Command1_Click via basVmCallSub: "); bool callOk = basVmCallSub(vm, p->codeAddr); printf("result=%s\n", callOk ? "ok" : "FAIL"); - if (!callOk) { gFailCount++; } + if (!callOk) { + gFailCount++; + } } basVmDestroy(vm); @@ -9835,10 +9879,14 @@ int main(void) { const BasProcEntryT *p1 = basModuleFindProc(mod, "Form1_Load"); const BasProcEntryT *p2 = basModuleFindProc(mod, "Command1_Click"); - if (p1) { basVmCallSub(vm, p1->codeAddr); } + if (p1) { + basVmCallSub(vm, p1->codeAddr); + } else { printf("Form1_Load NOT FOUND\n"); } - if (p2) { basVmCallSub(vm, p2->codeAddr); } + if (p2) { + basVmCallSub(vm, p2->codeAddr); + } else { printf("Command1_Click NOT FOUND\n"); } basVmDestroy(vm); diff --git a/src/apps/kpunch/dvxbasic/test_suite.c b/src/apps/kpunch/dvxbasic/test_suite.c index 83c3210f..b9823456 100644 --- a/src/apps/kpunch/dvxbasic/test_suite.c +++ b/src/apps/kpunch/dvxbasic/test_suite.c @@ -41,6 +41,8 @@ #include "dvxRes.h" #include +#include +#include #include #include "compiler/strip.h" #include "runtime/serialize.h" @@ -73,6 +75,8 @@ // Failure detail text of a check. #define TEST_DETAIL_LEN 512 +#define SLICE_SPAN 2 // testSliceClock: clock counts a slice may last +#define SLICE_STEPS 1000000 // testSliceClock: far more steps than the slice runs #define TEST_IMPL_CORRUPTIONS 3 // testInterfaceMaps: bad entry, short map, bad interface #define TEST_ENUM_CORRUPTIONS 7 // testUserControlEnums: index past the table, below none, on a method, in another class, an Enum without members, an empty name, two names alike #define TEST_ENUM_WIRE_NAME "\x06\x00" "Circle" // the member Circle on the wire: uint16 length, then the name @@ -80,6 +84,13 @@ #define TEST_ENUM_WIRE_NUL 5 // a byte of the name, made NUL #define TS_BIG_FILE "ts_big.tmp" // a sparse file past 2 GB (testBattery) #define TS_BIG_SIZE 3000000000LL // bytes: a FAT32 file may reach 4 GB - 1 +#define TS_COMPILE_UNITS 4 // testCompileUnits: two assignments, a SUB, a PRINT +#define TS_DIR_PUMP_DIR "ts_dirp" // testDirPumps' folder +#define TS_DIR_PUMP_FILES 200 // files in it Dir$ passes over +#define TS_DIR_PUMPS_MIN 2 // pumps at least: 200 passed over in two calls, a pump per 64 +#define TS_PUMPS_MANY 1000 // a countdown no test runs out +#define TS_INI_FILE "ts_inic.tmp" // testIniCache's file +#define TS_INI_SETTLED_SEC 3600 // its time: an hour old, past the 2 s a FAT time cannot tell apart static CaptureT *sUiCap = NULL; static char sUiRefs[UI_STUB_REF_COUNT][UI_STUB_REF_LEN]; @@ -238,6 +249,7 @@ static bool deviceSoundBusyStub(void *ctx); static void deviceSoundStub(void *ctx, double hz, double ticks); static int32_t deviceStickStub(void *ctx, int32_t n); static int32_t deviceStrigStub(void *ctx, int32_t n); +static void dirPumpFiles(bool make); static bool doEventsAbortStub(void *ctx); static bool doEventsCountdownStub(void *ctx); static bool doEventsResumeStub(void *ctx); @@ -248,6 +260,7 @@ static void handEmit16(HandCodeT *h, int16_t v); static void handEmit32(HandCodeT *h, int32_t v); static void handEmit8(HandCodeT *h, uint8_t b); static void handEmitU16(HandCodeT *h, uint16_t v); +static bool iniRewriteStub(void *ctx); static bool inputStub(void *ctx, const char *prompt, char *buf, int32_t bufSize); static int32_t memberIndex(const BasClassInfoT *cls, const char *memberName, uint8_t kind); static MiniCtrlT *miniAddCtrl(MiniFormT *f, const char *name, const char *typeName); @@ -256,6 +269,7 @@ static void miniFireCtrlEvent(MiniRtT *rt, MiniCtrlT *ctrl, const char *eventNam static MiniFormT *miniResolveOwningForm(MiniRtT *rt, const BasProcEntryT *proc); static void miniSetEvent(MiniCtrlT *ctrl, const char *eventName, const char *handlerName); static bool moduleHasOpcode(const BasModuleT *mod, uint8_t op); +static void printTickStub(void *ctx, const char *text, bool newline); static void reportFail(const char *name, const char *detail); static void reportFailDiff(const char *name, const char *expected, const char *got); static void reportPass(const char *name); @@ -267,9 +281,11 @@ static void testBytecode(const char *name, const char *source, const uint8_t *ex static void testClassRoundTrip(const char *name); static void testCommandArgs(const char *name); static void testCompileError(const char *name, const char *source, const char *needle); +static void testCompileUnits(void); static void testDebuggerApi(void); static void testDebugInfoRoundTrip(const char *name); static void testDeviceStatements(void); +static void testDirPumps(void); static void testDispatchEq(const char *name, const TestDispatchT *d, const char *expected); static void testEarlyBoundOpcodes(void); static void testEq(const char *name, const char *source, const char *expected); @@ -278,6 +294,7 @@ static void testHandAssembled(void); static void testHandBattery(void); static void testHandRun(const char *name, const HandCodeT *h, BasStringT **consts, int32_t constCount, int32_t localCount, const char *expected); static void testHandRunError(const char *name, const HandCodeT *h, BasStringT **consts, int32_t constCount, int32_t localCount, const char *needle); +static void testIniCache(void); static void testInputCallback(const char *name); static void testInterfaceMaps(const char *name); static void testLexerTokens(const char *name, const char *source, const ExpectedTokenT *expected, int32_t expectedCount); @@ -290,6 +307,7 @@ static void testRuntimeError(const char *name, const char *source, const char *n static void testSerializeCorrupt(const char *name); static void testSharedFormScope(const char *name); static void testSleepPumpsEvents(const char *name); +static void testSliceClock(void); static void testStrAppend(void); static void testSubEq(const char *name, const char *source, const char *subName, const char *expected); static void testUiEq(const char *name, const char *source, const char *expected); @@ -314,6 +332,12 @@ static void uiStubSetEvent(void *ctx, void *ctrlRef, const char *eventName, cons static void uiStubSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value); static void uiStubShowForm(void *ctx, void *formRef, bool modal); static void uiStubUnloadForm(void *ctx, void *formRef); +static void unitCountStub(void *ctx); + + +// testSliceClock's slice clock (printTickStub moves it) and its prints +static volatile uint32_t sSliceTicks = 0; +static int32_t sSlicePrints = 0; static const BasUiCallbacksT sUiStubs = { @@ -482,6 +506,31 @@ static int32_t deviceStrigStub(void *ctx, int32_t n) { } +// testDirPumps' files: TS_DIR_PUMP_FILES that do not match and one that +// does, made (empty) or removed. +static void dirPumpFiles(bool make) { + char path[TEST_DETAIL_LEN]; + + for (int32_t i = 0; i <= TS_DIR_PUMP_FILES; i++) { + if (i < TS_DIR_PUMP_FILES) { + snprintf(path, sizeof(path), TS_DIR_PUMP_DIR "/f%03d.dat", (int)i); + } else { + snprintf(path, sizeof(path), TS_DIR_PUMP_DIR "/match.zzz"); + } + + FILE *f = make ? fopen(path, "w") : NULL; + + if (f) { + fclose(f); + } + + if (!make) { + remove(path); + } + } +} + + // DoEvents stub that tells the VM to stop (host shutting down). static bool doEventsAbortStub(void *ctx) { (void)ctx; @@ -573,6 +622,38 @@ static void handEmitU16(HandCodeT *h, uint16_t v) { } +// testIniCache's DoEvents: the nth call rewrites the file -- the same size +// and time, then another size, then the same size at the time now. +static bool iniRewriteStub(void *ctx) { + static const char *texts[] = { "[A]\nk=2\n", "[A]\nk=33\n", "[A]\nk=44\n" }; + int32_t *calls = (int32_t *)ctx; + + if (*calls >= (int32_t)(sizeof(texts) / sizeof(texts[0]))) { + return false; + } + + FILE *f = fopen(TS_INI_FILE, "w"); + + if (!f) { + return false; + } + + fputs(texts[*calls], f); + fclose(f); + + if (*calls < 2) { + struct utimbuf settled; + + settled.actime = time(NULL) - TS_INI_SETTLED_SEC; + settled.modtime = settled.actime; + utime(TS_INI_FILE, &settled); + } + + (*calls)++; + return true; +} + + // INPUT stub: logs the prompt, answers "42" on the first call and // cancels every later one. static bool inputStub(void *ctx, const char *prompt, char *buf, int32_t bufSize) { @@ -797,6 +878,18 @@ static bool moduleHasOpcode(const BasModuleT *mod, uint8_t op) { } +// testSliceClock's print callback: each line moves the slice clock. +static void printTickStub(void *ctx, const char *text, bool newline) { + (void)ctx; + (void)text; + + if (newline) { + sSliceTicks++; + sSlicePrints++; + } +} + + static void reportFail(const char *name, const char *detail) { printf("FAIL %s\n", name); @@ -1435,6 +1528,26 @@ static void testCompileError(const char *name, const char *source, const char *n } +// basBuildCompile's unit callback runs after every unit compiled (a +// statement, or a whole procedure), and the module is the same as +// without it. +static void testCompileUnits(void) { + const char *src = "a = 1\nb = 2\nSUB S\n PRINT 1\n PRINT 2\nEND SUB\nPRINT a + b\n"; + BasCompileErrorT err; + int32_t units = 0; + char detail[TEST_DETAIL_LEN]; + BasModuleT *mod = basBuildCompile(src, (int32_t)strlen(src), false, NULL, NULL, &err, unitCountStub, &units); + BasModuleT *plain = basBuildCompile(src, (int32_t)strlen(src), false, NULL, NULL, &err, NULL, NULL); + + snprintf(detail, sizeof(detail), "units=%d", (int)units); + expectTrue("compile-unit-callback", mod && plain && units == TS_COMPILE_UNITS && mod->codeLen == plain->codeLen && memcmp(mod->code, plain->code, (size_t)mod->codeLen) == 0, detail); + basModuleFree(mod); + basModuleFree(plain); +} + + +// Breakpoints, step into/over/out, run to cursor, and the runSubLoop +// pause/resume/abort protocol used by the IDE debugger. static void testDebuggerApi(void) { const char *source = "DIM x AS INTEGER\n" @@ -1623,6 +1736,9 @@ static void testDebuggerApi(void) { } +// Debug-info side channel: serialize the debugVars table, wipe it on a +// second compile of the same source, deserialize, and compare. Also +// drives the truncated-stream rejection paths. static void testDebugInfoRoundTrip(const char *name) { const char *source = "DIM g AS INTEGER\n" @@ -1728,9 +1844,6 @@ static void testDebugInfoRoundTrip(const char *name) { } -// Debug-info side channel: serialize the debugVars table, wipe it on a -// second compile of the same source, deserialize, and compare. Also -// drives the truncated-stream rejection paths. // BEEP, SOUND, PLAY, PLAY(n), STICK, STRIG and FRAMEWAIT reach the device // callbacks with QBasic's ranges checked, and wait (pumping events) while // the host says so. @@ -1775,8 +1888,42 @@ static void testDeviceStatements(void) { } -// Breakpoints, step into/over/out, run to cursor, and the runSubLoop -// pause/resume/abort protocol used by the IDE debugger. +// Dir$ lets the program's events run every BAS_DIR_PUMP_ENTRIES entries it +// passes over, and halts the program when they say stop. +static void testDirPumps(void) { + mkdir(TS_DIR_PUMP_DIR, 0755); + dirPumpFiles(true); + + BasModuleT *mod = compileSource("dir-pumps", "n = 0\nf$ = DIR$(\"" TS_DIR_PUMP_DIR "/*.zzz\")\nDO WHILE f$ <> \"\"\n n = n + 1\n f$ = DIR$\nLOOP\nPRINT n\n"); + + for (int32_t stop = 0; mod && stop <= 1; stop++) { + BasVmT *vm = basVmCreate(); + CaptureT cap; + int32_t countdown = stop ? 0 : TS_PUMPS_MANY; + char detail[TEST_DETAIL_LEN]; + + captureReset(&cap); + basVmLoadModule(vm, mod); + basVmSetPrintCallback(vm, capturePrint, &cap); + basVmSetDoEventsCallback(vm, doEventsCountdownStub, &countdown); + + BasVmResultE r = basVmRun(vm); + + snprintf(detail, sizeof(detail), "r=%d pumps=%d out=[%s]", (int)r, (int)(TS_PUMPS_MANY - countdown), cap.buf); + + if (stop) { + expectTrue("dir-pumps-stop-halts", r == BAS_VM_HALTED && cap.len == 0, detail); + } else { + expectTrue("dir-pumps-pass-over", strcmp(cap.buf, "1 \n") == 0 && TS_PUMPS_MANY - countdown >= TS_DIR_PUMPS_MIN, detail); + } + + basVmDestroy(vm); + } + + basModuleFree(mod); + dirPumpFiles(false); + rmdir(TS_DIR_PUMP_DIR); +} static void testDispatchEq(const char *name, const TestDispatchT *d, const char *expected) { @@ -2390,6 +2537,53 @@ static void testHandRunError(const char *name, const HandCodeT *h, BasStringT ** } +// IniRead keeps the file parsed while its size and time are the same and +// it is older than a FAT time can tell apart; a new size or a new time +// reads it again. +static void testIniCache(void) { + FILE *f = fopen(TS_INI_FILE, "w"); + + if (f) { + fputs("[A]\nk=1\n", f); + fclose(f); + } + + struct utimbuf settled; + + settled.actime = time(NULL) - TS_INI_SETTLED_SEC; + settled.modtime = settled.actime; + utime(TS_INI_FILE, &settled); + + BasModuleT *mod = compileSource("ini-cache", + "PRINT INIREAD(\"" TS_INI_FILE "\", \"A\", \"k\", \"?\")\n" + "DOEVENTS\n" + "PRINT INIREAD(\"" TS_INI_FILE "\", \"A\", \"k\", \"?\")\n" + "DOEVENTS\n" + "PRINT INIREAD(\"" TS_INI_FILE "\", \"A\", \"k\", \"?\")\n" + "DOEVENTS\n" + "PRINT INIREAD(\"" TS_INI_FILE "\", \"A\", \"k\", \"?\")\n"); + + if (mod) { + BasVmT *vm = basVmCreate(); + CaptureT cap; + int32_t calls = 0; + char detail[TEST_DETAIL_LEN]; + + captureReset(&cap); + basVmLoadModule(vm, mod); + basVmSetPrintCallback(vm, capturePrint, &cap); + basVmSetDoEventsCallback(vm, iniRewriteStub, &calls); + basVmRun(vm); + snprintf(detail, sizeof(detail), "calls=%d out=[%s]", (int)calls, cap.buf); + expectTrue("ini-cache-kept-and-reread", calls == 3 && strcmp(cap.buf, "1\n1\n33\n44\n") == 0, detail); + basVmDestroy(vm); + basModuleFree(mod); + } + + remove(TS_INI_FILE); +} + + // INPUT through the host callback: prompt text, an accepted line, and a // cancelled read (empty result). static void testInputCallback(const char *name) { @@ -3635,6 +3829,49 @@ static void testSleepPumpsEvents(const char *name) { } +// The slice clock: a slice ends once the host's clock has moved its span, +// at the first instruction run out of line after that, however few steps +// it has run -- here each PRINT line moves the clock one count, as a long +// native call would. Without a clock the same slice runs to its steps. +static void testSliceClock(void) { + BasModuleT *mod = compileSource("slice-clock", "DIM i AS LONG\nFOR i = 1 TO 1000000\n PRINT i\nNEXT i\n"); + + if (!mod) { + return; + } + + BasVmT *vm = basVmCreate(); + char detail[TEST_DETAIL_LEN]; + + basVmLoadModule(vm, mod); + basVmSetPrintCallback(vm, printTickStub, NULL); + basVmSetStepLimit(vm, SLICE_STEPS); + basVmSetSliceClock(vm, &sSliceTicks, SLICE_SPAN); + sSliceTicks = 0; + sSlicePrints = 0; + + BasVmResultE r = basVmRun(vm); + + snprintf(detail, sizeof(detail), "r=%d prints=%d", (int)r, (int)sSlicePrints); + expectTrue("slice-clock-ends-slice", r == BAS_VM_STEP_LIMIT && sSlicePrints == SLICE_SPAN, detail); + + // The next slice measures from its own start + sSlicePrints = 0; + r = basVmRun(vm); + snprintf(detail, sizeof(detail), "r=%d prints=%d", (int)r, (int)sSlicePrints); + expectTrue("slice-clock-next-slice", r == BAS_VM_STEP_LIMIT && sSlicePrints == SLICE_SPAN, detail); + + basVmSetSliceClock(vm, NULL, 0); + sSlicePrints = 0; + r = basVmRun(vm); + snprintf(detail, sizeof(detail), "r=%d prints=%d", (int)r, (int)sSlicePrints); + expectTrue("slice-clock-none-steps", r == BAS_VM_STEP_LIMIT && sSlicePrints > SLICE_SPAN, detail); + + basVmDestroy(vm); + basModuleFree(mod); +} + + // "s = s + x" / "s = s & x" on a STRING variable compile to OP_STR_APPEND // (in-place growth); anything whose operand could observe or change the // variable keeps the ordinary load/concat/store form. Both forms must @@ -4349,6 +4586,12 @@ static void uiStubUnloadForm(void *ctx, void *formRef) { } +// testCompileUnits' unit callback: counts. +static void unitCountStub(void *ctx) { + (*(int32_t *)ctx)++; +} + + int main(void) { printf("DVX BASIC Regression Suite\n"); printf("==========================\n"); @@ -5136,6 +5379,20 @@ int main(void) { "IF t >= 0 THEN PRINT \"ok\"\n", "ok\n"); + // --- TIMER keeps rising in fine steps: readings in a busy loop never + // go back, and some differ by less than a 55 ms clock tick --- + TEST_EQ("timer-fine", + "DIM a AS DOUBLE\nDIM b AS DOUBLE\nDIM i AS LONG\nDIM fine AS LONG\nDIM back AS LONG\n" + "a = TIMER\n" + "FOR i = 1 TO 20000\n" + " b = TIMER\n" + " IF b < a AND a - b < 1 THEN back = back + 1\n" + " IF b > a AND b - a < 0.05 THEN fine = fine + 1\n" + " a = b\n" + "NEXT i\n" + "PRINT back; fine > 0\n", + "0 True \n"); + // --- Integer overflow promotes to LONG --- TEST_EQ("int-to-long-promotion", "DIM a AS INTEGER\nDIM b AS INTEGER\n" @@ -7777,9 +8034,48 @@ int main(void) { "KILL \"ts_dird/f1.tmp\"\nRMDIR \"ts_dird/sub\"\nRMDIR \"ts_dird\"\n", "[f1.tmp]\n"); + // FileCopy copies a file of many pieces whole (each piece may let the + // program's events run). + TEST_EQ("filecopy-many-pieces", + "OPEN \"ts_fc.tmp\" FOR OUTPUT AS #1\n" + "FOR i = 1 TO 2000\n PRINT #1, \"line\"; i; STRING$(40, \"x\")\nNEXT i\n" + "CLOSE #1\n" + "FILECOPY \"ts_fc.tmp\", \"ts_fc2.tmp\"\n" + "PRINT FILELEN(\"ts_fc.tmp\") = FILELEN(\"ts_fc2.tmp\"); FILELEN(\"ts_fc2.tmp\") > 65536\n" + "OPEN \"ts_fc2.tmp\" FOR INPUT AS #1\n" + "DO WHILE NOT EOF(1)\n LINE INPUT #1, l$\nLOOP\n" + "CLOSE #1\n" + "PRINT LEFT$(l$, 9)\n" + "KILL \"ts_fc.tmp\"\nKILL \"ts_fc2.tmp\"\n", + "True True \nline2000 \n"); + + // Keyed Collection lookups through the key index: built on the first + // lookup, kept as keys are added (and rebuilt as it fills), rebuilt + // after a removal or an insert before the end; keys ignore case. + TEST_EQ("collection-key-index", + "DIM c AS NEW Collection\nDIM i AS LONG\nDIM s AS LONG\n" + "FOR i = 1 TO 5\n c.Add i, \"k\" & i\nNEXT i\n" + "PRINT c(\"k3\")\n" + "FOR i = 6 TO 100\n c.Add i, \"k\" & i\nNEXT i\n" + "FOR i = 1 TO 100\n s = s + c(\"K\" & i)\nNEXT i\n" + "PRINT s; c.Count\n" + "c.Remove \"k50\"\nc.Remove 1\n" + "PRINT c.Count; c(\"k51\"); c(1); c(49)\n" + "c.Add 777, \"new\", 1\n" + "PRINT c(1); c(\"NEW\"); c(\"k100\"); c(\"k2\")\n" + "ON ERROR GOTO EH\n" + "c.Add 1, \"K2\"\n" + "x = c(\"k50\")\n" + "PRINT \"end\"\nEND\n" + "EH:\nPRINT \"err\"; ERR\nRESUME NEXT\n", + "3 \n5050 100 \n98 51 2 51 \n777 777 100 2 \nerr457 \nerr5 \nend\n"); + // SLEEP keeps pumping the host's events and stops when told to. testSleepPumpsEvents("sleep-pumps-events"); + testSliceClock(); testDeviceStatements(); + testDirPumps(); + testIniCache(); testStrAppend(); testObfuscateCaption("obfuscate-caption-kept"); testObfuscateKeepsNames("obfuscate-keeps-names-name", "SUB Total_Click\n PRINT Total.Name\nEND SUB\n"); @@ -7799,6 +8095,7 @@ int main(void) { testHandBattery(); testOpcodeGuards(); testBasBuild(); + testCompileUnits(); // Coverage battery: debugger, externs, serialization, values API. testDebuggerApi(); diff --git a/src/apps/kpunch/dvxdemo/dvxdemo.c b/src/apps/kpunch/dvxdemo/dvxdemo.c index 724f8ea6..baf3f859 100644 --- a/src/apps/kpunch/dvxdemo/dvxdemo.c +++ b/src/apps/kpunch/dvxdemo/dvxdemo.c @@ -1115,7 +1115,11 @@ static void setupMainWindow(void) { MenuT *viewMenu = wmAddMenu(bar, "&View"); if (viewMenu) { - wmAddMenuItem(viewMenu, "&Terminal", CMD_VIEW_TERM); + // The terminal widget is part of Communications + if (dvxAnsiTermApi()) { + wmAddMenuItem(viewMenu, "&Terminal", CMD_VIEW_TERM); + } + wmAddMenuItem(viewMenu, "&Controls", CMD_VIEW_CTRL); wmAddMenuSeparator(viewMenu); wmAddMenuCheckItem(viewMenu, "Tool&bar", CMD_VIEW_TOOLBAR, true); @@ -1222,7 +1226,12 @@ static void setupMainWindow(void) { // supported attributes (bold, reverse, blink), 8+8 colors, background // colors, and CP437 box-drawing/graphic characters. The terminal is // minimized on creation to avoid cluttering the initial demo view. +// Nothing without the terminal widget (part of Communications). static void setupTerminalWindow(void) { + if (!dvxAnsiTermApi()) { + return; + } + WindowT *win = dvxCreateWindow(sAc, "ANSI Terminal", DEMO_TERM_X, DEMO_TERM_Y, DEMO_TERM_W, DEMO_TERM_H, true); if (!win) { diff --git a/src/apps/kpunch/dvxdemo/dvxdemo.dhs b/src/apps/kpunch/dvxdemo/dvxdemo.dhs index 93263053..7c2f0bec 100644 --- a/src/apps/kpunch/dvxdemo/dvxdemo.dhs +++ b/src/apps/kpunch/dvxdemo/dvxdemo.dhs @@ -1,24 +1,24 @@ -// 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. +# 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. # DVX Demo help. Compiled to dvxdemo.hlp next to dvxdemo.app; F1 and # Help > Contents in the DVX Information window open it. diff --git a/src/apps/kpunch/dvxdemo/dvxdemo.res b/src/apps/kpunch/dvxdemo/dvxdemo.res index dc033e3b..6030f379 100644 --- a/src/apps/kpunch/dvxdemo/dvxdemo.res +++ b/src/apps/kpunch/dvxdemo/dvxdemo.res @@ -1,3 +1,25 @@ +# 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. + # dvxdemo.res -- Resource manifest for DVX Demo icon32 icon icon32.bmp name text "DVX Demo" diff --git a/src/apps/kpunch/dvxhelp/dvxhelp.res b/src/apps/kpunch/dvxhelp/dvxhelp.res index 539a1654..df6100a9 100644 --- a/src/apps/kpunch/dvxhelp/dvxhelp.res +++ b/src/apps/kpunch/dvxhelp/dvxhelp.res @@ -1,3 +1,25 @@ +# 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. + # dvxhelp.res -- Resource manifest for DVX Help Viewer icon32 icon icon32.bmp name text "DVX Help" diff --git a/src/apps/kpunch/fileMgr/fileMgr.bas b/src/apps/kpunch/fileMgr/fileMgr.bas index 6d232314..9702379b 100644 --- a/src/apps/kpunch/fileMgr/fileMgr.bas +++ b/src/apps/kpunch/fileMgr/fileMgr.bas @@ -35,7 +35,9 @@ ' the variables of the form whose code it is, and a finished job shows ' the main window's folder again. ' -' The Properties dialog leaves its answer here too. +' The Properties dialog leaves its answer here too, and counts the size +' of the selection here: its timer works on a measure job a slice at a +' time (fmProps.frm), so a big folder does not stop DVX. OPTION EXPLICIT @@ -66,12 +68,24 @@ DIM SHARED fmJobCount AS LONG DIM SHARED fmPropsAccepted AS BOOLEAN DIM SHARED fmPropsAttr AS LONG +' Its count of the selection: the measure job (0 once done), the selected +' items that are folders, and whether one could not be found +DIM SHARED fmPropsJob AS LONG +DIM SHARED fmPropsFolders AS LONG +DIM SHARED fmPropsMissing AS BOOLEAN + SUB ShowError(BYVAL title AS STRING, BYVAL message AS STRING) MsgBox message, vbOKOnly + vbCritical, title END SUB +' "1 file", "3 files": count things, thing in the singular. +FUNCTION CountOf$(BYVAL count AS LONG, BYVAL thing AS STRING) AS STRING + CountOf$ = CStr(count) & " " & thing & IIF(count = 1, "", "s") +END FUNCTION + + ' The running job whose window is called windowName, or Nothing. FUNCTION FindJob(BYVAL windowName AS STRING) AS FmJob DIM job AS FmJob @@ -213,6 +227,44 @@ END SUB ' Progress line, file count and bar of the job's window. SUB JobShowProgress(BYVAL job AS FmJob) job.Window.lblWhat.Caption = job.Verb & " " & FileOpsJobCurrent$(job.Handle) - job.Window.lblCounts.Caption = CStr(FileOpsJobFilesDone(job.Handle)) & " of " & CStr(FileOpsJobFilesTotal(job.Handle)) & " files" + job.Window.lblCounts.Caption = CStr(FileOpsJobFilesDone(job.Handle)) & " of " & CountOf$(FileOpsJobFilesTotal(job.Handle), "file") job.Window.pbJob.Value = FileOpsJobPercent(job.Handle) END SUB + + +' ============================================================ +' The Properties dialog's count +' ============================================================ + +' The size and contents of the selection in the Properties dialog: so far +' while counting, then (done) all of it, the job freed and the timer +' stopped. The dialog grows when a line gets longer. +SUB PropsShowCount(BYVAL done AS BOOLEAN) + DIM size AS STRING + DIM contains AS STRING + DIM grow AS BOOLEAN + + size = "Size: " & FileOpsFormatSize$(FileOpsJobBytesTotal(fmPropsJob)) & " bytes" + contains = "Contains: " & CountOf$(FileOpsJobFilesTotal(fmPropsJob), "file") & ", " & CountOf$(FileOpsJobFoldersTotal(fmPropsJob) + fmPropsFolders, "folder") + + IF NOT done THEN + size = size & " so far" + contains = contains & " so far" + ELSEIF fmPropsMissing OR FileOpsJobUnread(fmPropsJob) > 0 THEN + size = size & " (some items could not be read)" + END IF + + grow = Len(size) > Len(PropsForm.lblSize.Caption) OR Len(contains) > Len(PropsForm.lblContains.Caption) + PropsForm.lblSize.Caption = size + PropsForm.lblContains.Caption = contains + + IF grow THEN + PropsForm.AutoSize = True + END IF + + IF done THEN + PropsForm.tmrCount.Enabled = False + FileOpsJobFree fmPropsJob + fmPropsJob = 0 + END IF +END SUB diff --git a/src/apps/kpunch/fileMgr/fileMgr.dhs b/src/apps/kpunch/fileMgr/fileMgr.dhs index 0ed22e22..1ff4a095 100644 --- a/src/apps/kpunch/fileMgr/fileMgr.dhs +++ b/src/apps/kpunch/fileMgr/fileMgr.dhs @@ -216,7 +216,7 @@ Files and disks bigger than 2 GB (a FAT32 disk) show their full size. .h1 Properties -Select one or more items and choose File > Properties or press Alt+Enter. The Properties window shows the name, the folder it is in, the total size (for a folder, everything in it) with the number of files and folders, and the date the item was last changed. +Select one or more items and choose File > Properties or press Alt+Enter. The Properties window shows the name, the folder it is in, the total size (for a folder, everything in it) with the number of files and folders, and the date the item was last changed. A big folder takes a moment to count: the size and the numbers grow, marked "so far", until the count is done, and you can change the attributes or close the window meanwhile. The Attributes boxes can be changed; click OK to apply them to every selected item, or Cancel to leave them as they were. diff --git a/src/apps/kpunch/fileMgr/fileMgr.frm b/src/apps/kpunch/fileMgr/fileMgr.frm index 67d17645..2d01b2ee 100644 --- a/src/apps/kpunch/fileMgr/fileMgr.frm +++ b/src/apps/kpunch/fileMgr/fileMgr.frm @@ -513,14 +513,27 @@ SUB UpdateTitle END SUB -' "N items" or "S of N selected (B bytes)", and the free space of the -' folder's drive. +' The free space of the folder's drive. Asking DOS takes a while on a +' floppy or a network drive, so it is asked on navigate and refresh only. +SUB UpdateFree + DIM freeBytes AS DOUBLE + + freeBytes = FileOpsFreeBytes(curFolder) + + IF freeBytes >= 0 THEN + lblFree.Caption = FileOpsFormatSize$(freeBytes) & " bytes free" + ELSE + lblFree.Caption = "" + END IF +END SUB + + +' "N items" or "S of N selected (B bytes)". SUB UpdateStatus DIM rows AS STRING DIM row AS VARIANT DIM selected AS LONG DIM bytes AS DOUBLE - DIM freeBytes AS DOUBLE rows = fmList.SelectedRows() @@ -534,15 +547,7 @@ SUB UpdateStatus IF selected > 0 THEN fmStatus.Caption = CStr(selected) & " of " & CStr(FileOpsListCount(listing)) & " selected (" & FileOpsFormatSize$(bytes) & " bytes)" ELSE - fmStatus.Caption = CStr(FileOpsListCount(listing)) & " items" - END IF - - freeBytes = FileOpsFreeBytes(curFolder) - - IF freeBytes >= 0 THEN - lblFree.Caption = FileOpsFormatSize$(freeBytes) & " bytes free" - ELSE - lblFree.Caption = "" + fmStatus.Caption = CountOf$(FileOpsListCount(listing), "item") END IF END SUB @@ -611,7 +616,7 @@ FUNCTION Navigate(BYVAL path AS STRING, BYVAL fromTree AS BOOLEAN) AS BOOLEAN fmPath.Text = curFolder UpdateTitle - UpdateStatus + UpdateFree IF fromTree THEN item = fmTree.ListIndex @@ -655,12 +660,13 @@ SUB FmRefresh FreeListing fmList.SetRows "" + UpdateStatus END IF SelectByName keep TreeRebuild UpdateTitle - UpdateStatus + UpdateFree END SUB @@ -727,7 +733,8 @@ END FUNCTION ' Add folder's sub-folders (sorted by name) under the tree item parentItem, ' which has none yet, so they follow it. Returns the item of the one at -' expandPath, -1 when none. +' expandPath, -1 when none. The current folder's come from its listing, +' which already holds them, instead of reading the disk again. FUNCTION TreeAddChildren(BYVAL parentItem AS LONG, BYVAL folder AS STRING, BYVAL expandPath AS STRING) AS LONG DIM subList AS LONG DIM count AS LONG @@ -737,7 +744,12 @@ FUNCTION TreeAddChildren(BYVAL parentItem AS LONG, BYVAL folder AS STRING, BYVAL DIM found AS LONG found = -1 - subList = FileOpsList(folder, "") + + IF listing <> 0 AND FileOpsPathSame(folder, curFolder) THEN + subList = FileOpsListFolders(listing) + ELSE + subList = FileOpsList(folder, "") + END IF IF subList = 0 THEN TreeAddChildren = found @@ -1163,14 +1175,12 @@ END SUB ' Size, dates and editable attributes of the selection. Attribute changes ' apply to every selected item. +' The dialog counts the size itself, from its timer (fmProps.frm), with a +' measure job of the selection started here. SUB CmdProperties DIM paths AS Collection DIM path AS VARIANT DIM first AS STRING - DIM bytes AS DOUBLE - DIM files AS LONG - DIM folders AS LONG - DIM allRead AS BOOLEAN DIM attr AS LONG DIM location AS STRING DIM modified AS STRING @@ -1183,16 +1193,15 @@ SUB CmdProperties EXIT SUB END IF - allRead = True + fmPropsJob = FileOpsJobCreate(FILEOPS_MEASURE, "") + fmPropsFolders = 0 + fmPropsMissing = False FOR EACH path IN paths - allRead = FileOpsTreeSize(path) AND allRead - bytes = bytes + FileOpsTreeBytes() - files = files + FileOpsTreeFiles() - folders = folders + FileOpsTreeFolders() - - IF FileOpsIsFolder(path) THEN - folders = folders + 1 + IF NOT FileOpsJobAddSource(fmPropsJob, path) THEN + fmPropsMissing = True + ELSEIF FileOpsIsFolder(path) THEN + fmPropsFolders = fmPropsFolders + 1 END IF NEXT path @@ -1218,14 +1227,7 @@ SUB CmdProperties END IF PropsForm.lblLocation.Caption = "Location: " & location - - IF allRead THEN - PropsForm.lblSize.Caption = "Size: " & FileOpsFormatSize$(bytes) & " bytes" - ELSE - PropsForm.lblSize.Caption = "Size: " & FileOpsFormatSize$(bytes) & " bytes (some items could not be read)" - END IF - - PropsForm.lblContains.Caption = "Contains: " & CStr(files) & " files, " & CStr(folders) & " folders" + PropsShowCount False IF paths.Count = 1 AND modified <> "" THEN PropsForm.lblModified.Caption = "Modified: " & modified @@ -1239,9 +1241,12 @@ SUB CmdProperties PropsForm.chkArchive.Value = (attr AND FILEOPS_ATTR_ARCHIVE) <> 0 ' Fit the dialog to what it now says and centre it, then wait for it - fmPropsAccepted = False - PropsForm.AutoSize = True - PropsForm.Centered = True + ' while its timer counts + fmPropsAccepted = False + PropsForm.AutoSize = True + PropsForm.Centered = True + PropsForm.tmrCount.Interval = FM_JOB_TICK + PropsForm.tmrCount.Enabled = True PropsForm.Show vbModal IF NOT fmPropsAccepted THEN diff --git a/src/apps/kpunch/fileMgr/fmProps.frm b/src/apps/kpunch/fileMgr/fmProps.frm index 23153ad7..1af2ec96 100644 --- a/src/apps/kpunch/fileMgr/fmProps.frm +++ b/src/apps/kpunch/fileMgr/fmProps.frm @@ -20,6 +20,9 @@ Begin Form PropsForm Begin Label lblModified Caption = "" End + Begin Timer tmrCount + Enabled = False + End Begin Frame fraAttributes Caption = "Attributes" Begin CheckBox chkReadOnly @@ -78,7 +81,8 @@ End ' fmProps.frm -- the File Manager's Properties dialog: what the selection ' is (the File Manager fills in the labels and ticks the attributes before -' showing it with Show vbModal) and its Read-only, Hidden, System and +' showing it with Show vbModal; tmrCount counts its size meanwhile, a +' slice of a measure job at a time) and its Read-only, Hidden, System and ' Archive attributes. OK leaves the ticked attributes in fmPropsAttr and ' sets fmPropsAccepted (fileMgr.bas); Cancel, Escape and the close box ' leave fmPropsAccepted False. Its buttons are the size of the buttons of @@ -114,3 +118,15 @@ END SUB SUB cmdCancel_Click Unload PropsForm END SUB + + +' Closed before the count is done: the count stops. +SUB PropsForm_Unload + FileOpsJobFree fmPropsJob + fmPropsJob = 0 +END SUB + + +SUB tmrCount_Timer + PropsShowCount FileOpsJobRunFor(fmPropsJob, FM_SLICE_MS) <> FILEOPS_STEP_BUSY +END SUB diff --git a/src/apps/kpunch/helpedit/helpedit.dhs b/src/apps/kpunch/helpedit/helpedit.dhs index 826b94a4..f2194bab 100644 --- a/src/apps/kpunch/helpedit/helpedit.dhs +++ b/src/apps/kpunch/helpedit/helpedit.dhs @@ -1,24 +1,24 @@ -// 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. +# 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. # Help Editor help. Compiled to helpedit.hlp next to helpedit.app; F1 # and Help > Contents in the Help Editor open it. The help source diff --git a/src/apps/kpunch/helpedit/helpedit.frm b/src/apps/kpunch/helpedit/helpedit.frm index 04c4d86c..4803a8ba 100644 --- a/src/apps/kpunch/helpedit/helpedit.frm +++ b/src/apps/kpunch/helpedit/helpedit.frm @@ -98,16 +98,23 @@ End CONST HELP_FILE = "helpedit.hlp" CONST VIEWER_HELP = "\kpunch\dvxhelp\dvxhelp.hlp" CONST FORMAT_TOPIC = "help.format" -CONST HASH_MUL = 31 -CONST HASH_MOD = 16777213 + +' Loaded text is gathered in chunks of about this many bytes before being +' appended to the whole source, so a big file is not rebuilt per line; +' saved text is written this many bytes at a time +CONST OPEN_CHUNK_BYTES = 4096 +CONST SAVE_CHUNK_BYTES = 16384 + +' Room left in the editor past a loaded source for typing +CONST EDIT_ROOM_BYTES = 65536 DIM SHARED currentFile AS STRING DIM SHARED hlpFile AS STRING -DIM SHARED savedHash AS LONG +DIM SHARED savedText AS STRING DIM SHARED dirty AS INTEGER currentFile = "" hlpFile = "" -savedHash = 0 +savedText = "" dirty = 0 Load HelpEdit @@ -121,7 +128,7 @@ Editor.SetCaptureTabs True Editor.SetTabWidth 2 ' Start with a template -Editor.Text = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10) + "This is your help file. Edit the .dhs source here," + CHR$(10) + "then click Compile and Preview to see the result." + CHR$(10) +Editor.Text = NewSource$() MarkClean LblStatus.Caption = "Ready." @@ -133,23 +140,10 @@ IF COMMAND$(1) <> "" THEN END IF -' A hash of the editor's text (kept below HASH_MOD so h * HASH_MUL -' always fits a LONG). -FUNCTION TextHash() AS LONG - DIM h AS LONG - DIM t AS STRING - DIM i AS LONG - t = Editor.Text - h = 0 - FOR i = 1 TO LEN(t) - h = (h * HASH_MUL + ASC(MID$(t, i, 1))) MOD HASH_MOD - NEXT i - TextHash = h -END FUNCTION - - +' The text differs from what was last saved or loaded (typing it back as +' it was is not a change). FUNCTION IsDirty() AS INTEGER - IsDirty = (TextHash() <> savedHash) + IsDirty = (Editor.Text <> savedText) END FUNCTION @@ -187,7 +181,7 @@ END SUB SUB MarkClean dirty = 0 - savedHash = TextHash() + savedText = Editor.Text UpdateTitle END SUB @@ -206,13 +200,18 @@ SUB mnuNew_Click END IF currentFile = "" hlpFile = "" - Editor.Text = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10) + Editor.Text = NewSource$() MarkClean LblStatus.Caption = "New file." - UpdateTitle END SUB +' The source a new file starts with. +FUNCTION NewSource$() AS STRING + NewSource$ = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10) + "This is your help file. Edit the .dhs source here," + CHR$(10) + "then click Compile and Preview to see the result." + CHR$(10) +END FUNCTION + + SUB mnuOpen_Click IF AskSave() THEN EXIT SUB @@ -227,15 +226,20 @@ SUB mnuOpen_Click END SUB -' Loads the help source in path; a file that cannot be read is reported -' and the current text stays. +' Loads the help source in path; a file that cannot be read, or is too +' big for the editor, is reported and the current text stays. The +' editor is made big enough for it (and some typing). SUB OpenSource(path AS STRING) DIM f AS INTEGER DIM opened AS BOOLEAN + DIM firstLine AS BOOLEAN DIM text AS STRING + DIM chunk AS STRING DIM ln AS STRING - opened = False - text = "" + opened = False + firstLine = True + text = "" + chunk = "" ON ERROR GOTO openFailed f = FREEFILE @@ -243,19 +247,35 @@ SUB OpenSource(path AS STRING) opened = True DO WHILE NOT EOF(f) LINE INPUT #f, ln - IF text <> "" THEN - text = text + CHR$(10) + IF firstLine THEN + firstLine = False + ELSE + chunk = chunk + CHR$(10) + END IF + chunk = chunk + ln + IF LEN(chunk) >= OPEN_CHUNK_BYTES THEN + text = text + chunk + chunk = "" END IF - text = text + ln LOOP CLOSE #f + text = text + chunk + + ' The editor holds what MaxLength allows, at most its own limit + IF LEN(text) + EDIT_ROOM_BYTES > Editor.MaxLength THEN + Editor.MaxLength = LEN(text) + EDIT_ROOM_BYTES + END IF + + IF LEN(text) > Editor.MaxLength THEN + MsgBox path + " is too big for the Help Editor." + EXIT SUB + END IF Editor.Text = text currentFile = path hlpFile = "" MarkClean LblStatus.Caption = path - UpdateTitle EXIT SUB openFailed: @@ -272,16 +292,46 @@ SUB FileSave EXIT SUB END IF - OPEN currentFile FOR OUTPUT AS #1 - PRINT #1, Editor.Text - CLOSE #1 + IF NOT WriteSource(currentFile) THEN + EXIT SUB + END IF MarkClean LblStatus.Caption = "Saved: " + currentFile - UpdateTitle END SUB +' Writes the editor text to path, SAVE_CHUNK_BYTES at a time (a big +' source does not stop DVX for the whole write). False (after telling +' the user) on failure. +FUNCTION WriteSource(path AS STRING) AS BOOLEAN + DIM f AS INTEGER + DIM opened AS BOOLEAN + DIM text AS STRING + DIM at AS LONG + WriteSource = False + opened = False + text = Editor.Text + ON ERROR GOTO writeFailed + f = FREEFILE + OPEN path FOR OUTPUT AS #f + opened = True + FOR at = 1 TO LEN(text) STEP SAVE_CHUNK_BYTES + PRINT #f, MID$(text, at, SAVE_CHUNK_BYTES); + NEXT at + PRINT #f, "" + CLOSE #f + WriteSource = True + EXIT FUNCTION + +writeFailed: + IF opened THEN + CLOSE #f + END IF + MsgBox "Could not save " + path + " (error" + STR$(ERR) + ")." +END FUNCTION + + SUB FileSaveAs DIM path AS STRING path = DlgFileSave$("Save Help Source", "Help Source (*.dhs)|All Files (*.*)") @@ -317,11 +367,11 @@ SUB DoCompile LblStatus.Caption = "Save cancelled." EXIT SUB END IF - ELSE - OPEN currentFile FOR OUTPUT AS #1 - PRINT #1, Editor.Text - CLOSE #1 + ELSEIF WriteSource(currentFile) THEN MarkClean + ELSE + LblStatus.Caption = "Not compiled: the source could not be saved." + EXIT SUB END IF ' Derive .hlp filename from .dhs filename @@ -336,15 +386,23 @@ SUB DoCompile hlpFile = baseName + ".hlp" - LblStatus.Caption = "Compiling..." - DoEvents + ' The compile lets this program's events run: no second one meanwhile + LblStatus.Caption = "Compiling..." + mnuCompile.Enabled = False + mnuPreview.Enabled = False IF HelpCompile(currentFile, hlpFile) THEN LblStatus.Caption = "Compiled: " + hlpFile ELSE LblStatus.Caption = "Compile failed!" hlpFile = "" + IF HelpCompileMessages$() <> "" THEN + MsgBox HelpCompileMessages$(), vbOKOnly + vbExclamation, "Compile failed" + END IF END IF + + mnuCompile.Enabled = True + mnuPreview.Enabled = True END SUB diff --git a/src/apps/kpunch/notepad/notepad.frm b/src/apps/kpunch/notepad/notepad.frm index 6ab70f63..7717034d 100644 --- a/src/apps/kpunch/notepad/notepad.frm +++ b/src/apps/kpunch/notepad/notepad.frm @@ -106,6 +106,13 @@ CONST HELP_FILE = "notepad.hlp" ' appended to the whole document, so large files are not rebuilt per line. CONST OPEN_CHUNK_BYTES = 4096 +' Saved text is written this many bytes at a time, so a big file does not +' stop DVX for the whole write (the program yields between pieces). +CONST SAVE_CHUNK_BYTES = 16384 + +' Room left in the editor past a loaded file for typing +CONST EDIT_ROOM_BYTES = 65536 + DIM SHARED currentFile AS STRING DIM SHARED dirty AS BOOLEAN currentFile = "" @@ -218,13 +225,19 @@ END FUNCTION FUNCTION WriteTextFile(path AS STRING) AS BOOLEAN DIM f AS INTEGER DIM opened AS BOOLEAN + DIM text AS STRING + DIM at AS LONG WriteTextFile = False opened = False + text = Editor.Text ON ERROR GOTO writeFailed f = FREEFILE OPEN path FOR OUTPUT AS #f opened = True - PRINT #f, Editor.Text + FOR at = 1 TO LEN(text) STEP SAVE_CHUNK_BYTES + PRINT #f, MID$(text, at, SAVE_CHUNK_BYTES); + NEXT at + PRINT #f, "" CLOSE #f WriteTextFile = True EXIT FUNCTION @@ -278,6 +291,18 @@ SUB OpenFile(path AS STRING) IF NOT ReadTextFile(path, text) THEN EXIT SUB END IF + + ' The editor holds what MaxLength allows, at most its own limit: a + ' file it cannot hold whole is not opened (saving it would cut it) + IF LEN(text) + EDIT_ROOM_BYTES > Editor.MaxLength THEN + Editor.MaxLength = LEN(text) + EDIT_ROOM_BYTES + END IF + + IF LEN(text) > Editor.MaxLength THEN + MsgBox path + " is too big for Notepad." + EXIT SUB + END IF + Editor.Text = text currentFile = path MarkClean diff --git a/src/apps/kpunch/pkgbuild/pbProg.frm b/src/apps/kpunch/pkgbuild/pbProg.frm new file mode 100644 index 00000000..ffb8e08d --- /dev/null +++ b/src/apps/kpunch/pkgbuild/pbProg.frm @@ -0,0 +1,70 @@ +VERSION DVX 1.00 +Begin Form ProgForm + Caption = "Building" + Layout = VBox + AutoSize = False + Resizable = False + Centered = True + Width = 360 + Height = 130 + Begin Label lblWhat + Caption = "" + Weight = 1 + End + Begin ProgressBar pbJob + End + Begin HBox boxCancel + Alignment = Center + Begin CommandButton cmdProgCancel + Caption = "&Cancel" + Cancel = True + MinWidth = 80 + MinHeight = 24 + End + 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. + + +' pbProg.frm -- the progress of building a package: PbRunJob +' (pkgbuild.bas) runs the build and keeps this window up to date, and +' closes it when the build ends. Cancel (and the close box) stop the +' build; nothing of the package is left behind. + +OPTION EXPLICIT + + +SUB cmdProgCancel_Click + PbCancel +END SUB + + +SUB ProgForm_QueryUnload(Cancel AS INTEGER) + ' Still building: stop it, and close when it has + IF pbBuilding THEN + PbCancel + Cancel = 1 + END IF +END SUB diff --git a/src/apps/kpunch/pkgbuild/pkgbuild.bas b/src/apps/kpunch/pkgbuild/pkgbuild.bas index b377cb60..cf08b235 100644 --- a/src/apps/kpunch/pkgbuild/pkgbuild.bas +++ b/src/apps/kpunch/pkgbuild/pkgbuild.bas @@ -29,7 +29,7 @@ ' file dvxpkg build takes too), plus a [builder] section with the folder ' of files, how that folder is laid out, and the key. Building writes the template beside ' the new package for a moment and hands it, the files, and the key to -' PkgBuild. +' PkgBuildStart, then runs the build with its progress window up. OPTION EXPLICIT @@ -48,6 +48,7 @@ CONST PB_FIRST_VERSION = "1.0.0" CONST PB_NOT_SIGNED = "Not signed" CONST PB_PAGE_PACKAGE = 0 ' the tabs: the Package page CONST PB_PAGE_FILES = 3 ' the Files page +CONST PB_SLICE_MS = 60 ' building between screen updates ' The template: the manifest's sections and keys, and the builder's own CONST PB_SECT_PACKAGE = "package" @@ -80,6 +81,11 @@ DIM SHARED pbKeys AS STRING ' PbState$ when the template was last opened or saved DIM SHARED pbSaved AS STRING +' A build is running (its progress window is up), and its Cancel was +' pressed +DIM SHARED pbBuilding AS BOOLEAN +DIM SHARED pbCancelled AS BOOLEAN + ' Build the package: checked first, then saved where the user says, from ' the template, the files, and the key. @@ -89,7 +95,7 @@ SUB PbBuild DIM rows AS STRING DIM ok AS BOOLEAN - IF NOT PbCheck() THEN + IF pbBuilding OR NOT PbCheck() THEN EXIT SUB END IF @@ -114,7 +120,8 @@ SUB PbBuild EXIT SUB END IF - ok = PkgBuild(pbSession, tmplPath, rows, outPath, PbKeyPath$()) + ' The template is read as the build starts + ok = PkgBuildStart(pbSession, tmplPath, rows, outPath, PbKeyPath$()) KILL tmplPath IF NOT ok THEN @@ -122,12 +129,28 @@ SUB PbBuild EXIT SUB END IF + IF PbRunJob() <> PKG_RUN_DONE THEN + IF NOT pbCancelled THEN + PbError "The package cannot be built. " & PkgRunMessage$(pbSession) + END IF + + EXIT SUB + END IF + BuilderForm.lblStatus.Caption = "Built " & outPath ShellLog "Package Builder: built " & outPath MsgBox "The package is built:" & PB_EOL & PB_EOL & outPath, vbOKOnly + vbInformation, PB_TITLE END SUB +' Stop the build the progress window shows (nothing of the package is +' left behind). +SUB PbCancel + pbCancelled = True + PkgRunCancel pbSession +END SUB + + ' What a package must have before it is built; False (and a word why) ' when something is missing or wrong. FUNCTION PbCheck() AS BOOLEAN @@ -456,6 +479,7 @@ SUB PbNewKey EXIT SUB END IF + ShellLog "Package Builder: key made " & keyBase & PB_SEC_EXT PbKeysFill keyBase & PB_SEC_EXT MsgBox "Your key is made." & PB_EOL & PB_EOL & "Give " & FileOpsBaseName$(keyBase & PB_KEY_EXT) & " to the people who install your packages: it tells DVX that they are yours. Keep " & FileOpsBaseName$(keyBase & PB_SEC_EXT) & " private: anyone who has it can sign packages as you.", vbOKOnly + vbInformation, PB_TITLE END SUB @@ -486,6 +510,31 @@ END SUB ' Save the template: to pbPath, or where the user says when it has none ' or asAs. False when it was not saved. +' Run the build just started, a slice at a time, with the progress window +' up; its Cancel stops the build. How it ended (PKG_RUN_*). +FUNCTION PbRunJob() AS LONG + DIM rc AS LONG + + Load ProgForm + pbBuilding = True + pbCancelled = False + ProgForm.lblWhat.Caption = "Building" + ProgForm.pbJob.Value = 0 + ProgForm.Show + + DO + rc = PkgRunFor(pbSession, PB_SLICE_MS) + ProgForm.lblWhat.Caption = Trim$("Building " & PkgRunCurrent$(pbSession)) + ProgForm.pbJob.Value = PkgRunPercent(pbSession) + DoEvents + LOOP WHILE rc = PKG_RUN_BUSY + + pbBuilding = False + Unload ProgForm + PbRunJob = rc +END FUNCTION + + FUNCTION PbSave(BYVAL asAs AS BOOLEAN) AS BOOLEAN DIM path AS STRING diff --git a/src/apps/kpunch/pkgbuild/pkgbuild.dbp b/src/apps/kpunch/pkgbuild/pkgbuild.dbp index 65d7e33f..46c486a1 100644 --- a/src/apps/kpunch/pkgbuild/pkgbuild.dbp +++ b/src/apps/kpunch/pkgbuild/pkgbuild.dbp @@ -19,7 +19,8 @@ Count = 7 [Forms] File0 = pkgbuild.frm -Count = 1 +File1 = pbProg.frm +Count = 2 [Settings] StartupForm = BuilderForm diff --git a/src/apps/kpunch/pkgbuild/pkgbuild.dhs b/src/apps/kpunch/pkgbuild/pkgbuild.dhs index 14424e99..15e55140 100644 --- a/src/apps/kpunch/pkgbuild/pkgbuild.dhs +++ b/src/apps/kpunch/pkgbuild/pkgbuild.dhs @@ -44,6 +44,8 @@ Describe the package on the five pages of the window, then choose Build Package .item Signing: the key that shows the package came from you. .endlist +While the package is built, a window shows how far it has got; choose Cancel to stop, and nothing of the package is left behind. You can go on using DVX while it builds. + What you fill in can be saved as a template (File > Save) and opened again (File > Open) to build the next version. .link pkgbuild.package The Package page @@ -158,4 +160,4 @@ DVXPKG build CHESS.INI FOLDER CHESS.DPK MYKEY.SEC where FOLDER is laid out like the DVX folder. DVXPKG with no arguments lists its other commands: signing a package, checking one, showing what is in one, making keys, and installing, removing, and listing packages. -Programs make packages with the package library (pkg.bas): PkgBuild, PkgSign, PkgKeygen, and the functions around them. +Programs make packages with the package library (pkg.bas): PkgBuildStart and PkgSignStart (jobs run a little at a time with PkgRunFor, as installing is), PkgKeygen, and the functions around them. diff --git a/src/apps/kpunch/pkgbuild/pkgbuild.frm b/src/apps/kpunch/pkgbuild/pkgbuild.frm index aec51661..3825d277 100644 --- a/src/apps/kpunch/pkgbuild/pkgbuild.frm +++ b/src/apps/kpunch/pkgbuild/pkgbuild.frm @@ -329,7 +329,11 @@ END SUB SUB BuilderForm_QueryUnload(Cancel AS INTEGER) - IF NOT PbSaveFirst() THEN + ' Building: stop it first (close again when it has) + IF pbBuilding THEN + PbCancel + Cancel = 1 + ELSEIF NOT PbSaveFirst() THEN Cancel = 1 END IF END SUB diff --git a/src/apps/kpunch/player/player.frm b/src/apps/kpunch/player/player.frm index 80663aa5..11fba1c0 100644 --- a/src/apps/kpunch/player/player.frm +++ b/src/apps/kpunch/player/player.frm @@ -1117,15 +1117,10 @@ SUB lstTracks_DblClick END SUB -' Enter plays the selected track, Del takes it out of the list. +' Del takes the selected track out of the list. (Enter plays it: a list +' box's Enter is a double-click.) SUB lstTracks_KeyDown(KeyCode AS INTEGER, Shift AS INTEGER) - IF Shift <> 0 OR lstTracks.ListIndex < 0 THEN - EXIT SUB - END IF - - IF KeyCode = vbKeyReturn THEN - PlayTrack lstTracks.ListIndex - ELSEIF KeyCode = vbKeyDelete THEN + IF Shift = 0 AND KeyCode = vbKeyDelete AND lstTracks.ListIndex >= 0 THEN RemoveTrack lstTracks.ListIndex END IF END SUB diff --git a/src/apps/kpunch/progman/pmView.c b/src/apps/kpunch/progman/pmView.c index ca30c98e..1a7f9b61 100644 --- a/src/apps/kpunch/progman/pmView.c +++ b/src/apps/kpunch/progman/pmView.c @@ -34,8 +34,10 @@ #include "listhelp/listHelp.h" #include "dvxApp.h" #include "dvxDraw.h" +#include "dvxPlat.h" #include "dvxVideo.h" #include "dvxWgtP.h" +#include "taskSwch.h" #include #include @@ -49,6 +51,15 @@ #define PV_BUTTON_RIGHT 2 // onMouseDown button number of the right button #define PV_CELL_PARTS 2 // a cell's column and row (navigate's buffer) +// Icon decoding one paint may spend (at least one icon); the cells after +// it show placeholders until a later frame (viewPoll). While programs +// run, a frame that decoded is followed by as long again without decoding +// (iconTurn): on a 486 such a frame (the decode, the paint, and the flush) +// takes 100 ms or more, and the shell's priority gives it several turns in +// a row, so back-to-back decoding frames kept the programs from running +// for most of a second. +#define PV_ICON_BUDGET_US 20000 + // Program Manager's views as the icon view rules name them static const ListIconViewE sIconView[PmViewCountE] = { [PmViewLargeE] = ListIconLargeE, @@ -59,22 +70,27 @@ static const ListIconViewE sIconView[PmViewCountE] = { typedef struct { PmModelT *model; const PmViewCallbacksT *cb; - PmViewEntryT *entries; // stb_ds copy + PmViewEntryT *entries; // stb_ds copy const char *emptyText; + uint64_t paintUs; // when the paint under way began (platformMicros) + uint64_t iconDueUs; // decoding may go on from then (iconTurn) int32_t selected; int32_t scrollRow; int32_t lastCols; - int32_t dragEntry; // pressed entry that may be dragged, or -1 + int32_t dragEntry; // pressed entry that may be dragged, or -1 int32_t dropCol; int32_t dropRow; int32_t pressX; int32_t pressY; int32_t sbDragOff; + int32_t paintIcons; // icons the paint under way has loaded bool manual; bool canDrag; bool dragging; bool sbDragging; - bool placeDirty; // entries changed: tell onColumns again + bool placeDirty; // entries changed: tell onColumns again + bool iconsPending; // a paint left icons for the next frame (viewPoll) + bool iconsDecoded; // the last paint decoded: viewPoll starts the rest } PmViewDataT; static int32_t sTypeId = -1; @@ -89,6 +105,7 @@ static void ensureVisible(WidgetT *w, const ListIconGridT *g, int32_t e); static void entryCell(const PmViewDataT *d, int32_t e, int32_t cols, int32_t *col, int32_t *row); static PmItemT *entryItem(const PmViewDataT *d, int32_t e); static int32_t hitEntry(const WidgetT *w, const ListIconGridT *g, int32_t vx, int32_t vy, int32_t *outCol, int32_t *outRow); +static bool iconTurn(const PmViewDataT *d); static void manualCell(const PmItemT *item, int32_t *col, int32_t *row); static int32_t manualRows(const PmViewDataT *d); static int32_t navigate(const PmViewDataT *d, const ListIconGridT *g, int32_t key); @@ -113,11 +130,12 @@ static void viewOnKey(WidgetT *w, int32_t key, int32_t mod); static void viewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); static bool viewOnWheel(WidgetT *w, int32_t delta); static void viewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +static void viewPoll(WidgetT *w, WindowT *win); static void viewSync(WidgetT *w, const BitmapFontT *font, ListIconGridT *g); static const WidgetClassT sClassView = { .version = WGT_CLASS_VERSION, - .flags = WCLASS_FOCUSABLE, + .flags = WCLASS_FOCUSABLE | WCLASS_NEEDS_POLL, .handlers = { [WGT_METHOD_PAINT] = (void *)viewPaint, [WGT_METHOD_CALC_MIN_SIZE] = (void *)viewCalcMinSize, @@ -128,6 +146,7 @@ static const WidgetClassT sClassView = { [WGT_METHOD_ON_DRAG_END] = (void *)viewDragEnd, [WGT_METHOD_ON_WHEEL] = (void *)viewOnWheel, [WGT_METHOD_GET_TEXT] = (void *)viewGetText, + [WGT_METHOD_POLL] = (void *)viewPoll, } }; @@ -154,7 +173,13 @@ static void drawCell(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapF uint32_t bg = selected ? colors->menuHighlightBg : colors->contentBg; if (!item->iconTried && vd->cb->loadIcon) { - vd->cb->loadIcon(item); + if (iconTurn(vd)) { + vd->cb->loadIcon(item); + vd->paintIcons++; + vd->iconsDecoded = true; + } else { + vd->iconsPending = true; + } } const uint8_t *picture = large ? item->icon : item->iconSmall; @@ -293,6 +318,16 @@ static int32_t hitEntry(const WidgetT *w, const ListIconGridT *g, int32_t vx, in } +// Whether the paint under way may decode an icon: the first once the +// rest after the last decoding frame is over, more while the paint's +// budget lasts. +static bool iconTurn(const PmViewDataT *d) { + uint64_t now = platformMicros(); + + return d->paintIcons == 0 ? now >= d->iconDueUs : now - d->paintUs < PV_ICON_BUDGET_US; +} + + // An item's own cell (manual layout; a negative col / row counts as 0). static void manualCell(const PmItemT *item, int32_t *col, int32_t *row) { *col = DVX_MAX(item->col, 0); @@ -696,7 +731,9 @@ static void viewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap ListIconGridT g; viewSync(w, font, &g); - vd->scrollRow = clampInt(vd->scrollRow, 0, g.maxScroll); + vd->scrollRow = clampInt(vd->scrollRow, 0, g.maxScroll); + vd->paintUs = platformMicros(); + vd->paintIcons = 0; drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); // Intersect the widget's inside with the clip already in force. @@ -742,6 +779,29 @@ static void viewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap } +// WGT_METHOD_POLL, at the start of a frame: while programs run, a frame +// that decoded (the paint, the composite, and the other tasks' turns +// since) is followed by as long a rest; a paint that left icons is +// followed by another once decoding may go on, until every icon shown is +// in. +static void viewPoll(WidgetT *w, WindowT *win) { + PmViewDataT *vd = (PmViewDataT *)w->data; + uint64_t now = platformMicros(); + + (void)win; + + if (vd->iconsDecoded) { + vd->iconsDecoded = false; + vd->iconDueUs = tsActiveCount() > 1 ? now + (now - vd->paintUs) : now; + } + + if (vd->iconsPending && now >= vd->iconDueUs) { + vd->iconsPending = false; + wgtInvalidatePaint(w); + } +} + + // Geometry, first telling Program Manager the column count whenever it // changed or the entries were replaced (it places unplaced items then). // Nothing is reported before the first layout gives the view a width. diff --git a/src/apps/kpunch/progman/progman.c b/src/apps/kpunch/progman/progman.c index 4ea17412..56e82168 100644 --- a/src/apps/kpunch/progman/progman.c +++ b/src/apps/kpunch/progman/progman.c @@ -167,6 +167,7 @@ static char sIniPath[DVX_MAX_PATH]; static char sFilter[PM_SEARCH_MAX]; static bool sStartupPending = true; static bool sScanning = false; +static uint32_t sInstalledSeen = 0; // platformInstalledChangeCount at the last scan static void (*sPrevOnFocus)(WindowT *win) = NULL; // ============================================================ @@ -738,11 +739,17 @@ static void launchItem(const PmItemT *item) { } -// The shell's item icon rule (shellItemIcon), at both sizes. +// The shell's item icon rule (shellItemIcons), at both sizes from one +// decode. static void loadItemIcon(PmItemT *item) { - item->iconTried = true; - item->icon = shellItemIcon(sAc, item->iconPath, item->program, SHELL_PROG_ICON, &item->iconPitch); - item->iconSmall = shellItemIcon(sAc, item->iconPath, item->program, SHELL_PROG_ICON_SMALL, &item->iconSmallPitch); + ShellIconsT icons; + + shellItemIcons(sAc, item->iconPath, item->program, SHELL_PROG_ICON, SHELL_PROG_ICON_SMALL, &icons); + item->iconTried = true; + item->icon = icons.icon; + item->iconPitch = icons.iconPitch; + item->iconSmall = icons.iconSmall; + item->iconSmallPitch = icons.iconSmallPitch; } @@ -791,13 +798,21 @@ static void onPmClose(WindowT *win) { } -// Back at the front: pick up programs installed or removed meanwhile. +// Back at the front: pick up programs installed or removed meanwhile, +// looking only when something may have changed (a scan reads the whole +// programs folder). static void onPmFocus(WindowT *win) { if (sPrevOnFocus) { sPrevOnFocus(win); } - if (!sScanning && pmInstalledChanged(&sModel, sCtx->appPath)) { + if (sScanning || platformInstalledChangeCount() == sInstalledSeen) { + return; + } + + sInstalledSeen = platformInstalledChangeCount(); + + if (pmInstalledChanged(&sModel, sCtx->appPath)) { scanInstalled(false); refreshView(); } @@ -1101,7 +1116,8 @@ static void scanInstalled(bool pump) { return; } - sScanning = true; + sScanning = true; + sInstalledSeen = platformInstalledChangeCount(); int32_t count = pmInstalledScan(&sModel, sCtx->appPath, pump ? scanProgress : NULL, NULL); diff --git a/src/apps/kpunch/progman/progman.dhs b/src/apps/kpunch/progman/progman.dhs index 382aca5c..906ccd8c 100644 --- a/src/apps/kpunch/progman/progman.dhs +++ b/src/apps/kpunch/progman/progman.dhs @@ -1,24 +1,24 @@ -// 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. +# 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. # Program Manager help. Compiled to progman.hlp next to progman.app; F1 # and Help > Contents in the Program Manager open it. The user guide @@ -70,7 +70,7 @@ A group is a set of program icons you put together, such as Games or Writing. Th .h2 Installed Programs -The first group, Installed Programs, is kept up to date for you: it always shows every program installed in DVX, in alphabetical order. A newly installed program appears there by itself (the Program Manager looks again whenever its window comes to the front, and View > Refresh looks at once), and a removed program disappears. You cannot change this group, but you can copy its icons into your own groups with File > Copy. +The first group, Installed Programs, is kept up to date for you: it always shows every program installed in DVX, in alphabetical order. A newly installed program appears there by itself (when its window comes to the front after a package was installed or removed, files were copied, moved, or deleted, or a DOS program ran, the Program Manager looks again; View > Refresh looks at once), and a removed program disappears. You cannot change this group, but you can copy its icons into your own groups with File > Copy. .h2 Main and Startup diff --git a/src/apps/kpunch/progman/userHelp.dhs b/src/apps/kpunch/progman/userHelp.dhs index 7f2c7bc0..05fe500c 100644 --- a/src/apps/kpunch/progman/userHelp.dhs +++ b/src/apps/kpunch/progman/userHelp.dhs @@ -964,6 +964,8 @@ When you exit DVX, each program is closed as if you had closed it yourself. If y DVX writes a record of what happened to the file DVX.LOG, in your USER folder (for example C:\DVX\USER\DVX.LOG). If a program crashes, this file contains details that help the program's author find the problem. The file is started fresh each time DVX starts, so copy it before starting DVX again. +DVX also remembers the programs' names and icons in PROGRAMS.CAC in the same folder, so Program Manager starts quickly. If a program shows an old name or icon after you changed it, delete PROGRAMS.CAC: DVX makes it again. + .link user.settings Changing Settings .link user.taskmgr Task Manager diff --git a/src/apps/kpunch/resedit/resedit.dhs b/src/apps/kpunch/resedit/resedit.dhs index 33ea97e7..2eae3d60 100644 --- a/src/apps/kpunch/resedit/resedit.dhs +++ b/src/apps/kpunch/resedit/resedit.dhs @@ -1,24 +1,24 @@ -// 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. +# 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. # Resource Editor help. Compiled to resedit.hlp next to resedit.app; F1 # and Help > Contents in the Resource Editor open it. diff --git a/src/apps/kpunch/software/software.bas b/src/apps/kpunch/software/software.bas index 737aaf15..8f9ef95c 100644 --- a/src/apps/kpunch/software/software.bas +++ b/src/apps/kpunch/software/software.bas @@ -145,7 +145,12 @@ SUB SwFetchInstall(BYVAL net AS LONG, BYVAL ids AS STRING, BYVAL what AS STRING) EXIT SUB END IF - IF PkgOfferFolder(swSession, PkgNetDir$(net)) <= 0 THEN + IF NOT SwRead(PkgOfferFolderStart(swSession, PkgNetDir$(net)), what & " cannot be installed: ") THEN + PkgNetClear net + EXIT SUB + END IF + + IF PkgOfferCount(swSession) = 0 THEN SwError what & " cannot be installed: what was fetched cannot be read." PkgNetClear net EXIT SUB @@ -195,16 +200,10 @@ END FUNCTION ' Install the package file path, with what it needs from the packages ' beside it: what will happen is shown first, then it runs. SUB SwInstallFile(BYVAL path AS STRING) - DIM offer AS LONG - - offer = PkgOfferFile(swSession, path) - - IF offer < 0 THEN - SwError path & " cannot be installed: " & PkgError$(swSession) - EXIT SUB + ' The file is offer 0 + IF SwRead(PkgOfferFileStart(swSession, path), path & " cannot be installed: ") THEN + SwInstallOffers "0", PkgOfferField$(swSession, 0, PKG_FIELD_NAME), path END IF - - SwInstallOffers CStr(offer), PkgOfferField$(swSession, offer, PKG_FIELD_NAME), path END SUB @@ -438,28 +437,42 @@ END FUNCTION ' Publish the package file path on the package server with the ' settings' account: True when the server took it in for review. FUNCTION SwPublish(BYVAL path AS STRING) AS BOOLEAN - DIM offer AS LONG DIM what AS STRING - offer = PkgOfferFile(swSession, path) - - IF offer < 0 THEN - SwError path & " cannot be published: " & PkgError$(swSession) + ' The file is offer 0 + IF NOT SwRead(PkgOfferFileStart(swSession, path), path & " cannot be published: ") THEN EXIT FUNCTION END IF - what = PkgOfferField$(swSession, offer, PKG_FIELD_NAME) & " " & PkgOfferField$(swSession, offer, PKG_FIELD_VERSION) + what = PkgOfferField$(swSession, 0, PKG_FIELD_NAME) & " " & PkgOfferField$(swSession, 0, PKG_FIELD_VERSION) IF NOT SwNetRun(swMine, PkgNetUpload(swMine, path), "Publishing") THEN EXIT FUNCTION END IF - ShellLog "Software: published " & PkgOfferField$(swSession, offer, PKG_FIELD_ID) & " " & PkgOfferField$(swSession, offer, PKG_FIELD_VERSION) + ShellLog "Software: published " & PkgOfferField$(swSession, 0, PKG_FIELD_ID) & " " & PkgOfferField$(swSession, 0, PKG_FIELD_VERSION) MsgBox what & " is on the package server, waiting for its review. The Published page shows how the review goes.", vbOKOnly + vbInformation, SW_TITLE SwPublish = True END FUNCTION +' Read packages as the offers (started: whether the reading did) with the +' progress window up. True when they were read; else failText and why +' are shown, unless it was stopped. +FUNCTION SwRead(BYVAL started AS BOOLEAN, BYVAL failText AS STRING) AS BOOLEAN + IF NOT started THEN + SwError failText & "another job is running." + EXIT FUNCTION + END IF + + IF SwRunJob("Reading") = PKG_RUN_DONE THEN + SwRead = True + ELSEIF NOT swCancelled THEN + SwError failText & PkgRunMessage$(swSession) + END IF +END FUNCTION + + ' Remove the installed package id, after asking about what needs it (it ' goes too, or nothing does), what only it needed (offered), and files ' changed since they were installed (kept unless the user says). @@ -542,6 +555,7 @@ FUNCTION SwRunJob(BYVAL verb AS STRING) AS LONG Load ProgForm swJob = SW_JOB_PKG + swCancelled = False ProgForm.Caption = verb ProgForm.lblWhat.Caption = verb ProgForm.pbJob.Value = 0 diff --git a/src/apps/kpunch/terminal/terminal.dhs b/src/apps/kpunch/terminal/terminal.dhs index 14a51007..3436ac46 100644 --- a/src/apps/kpunch/terminal/terminal.dhs +++ b/src/apps/kpunch/terminal/terminal.dhs @@ -82,7 +82,7 @@ Settings > Session Settings... changes the current session. Changes to the conne Setting Meaning ------- ------- Name The name shown in the phone book and the window title. - Connect with Serial port (a cable to the other computer), Modem (dials a phone number), Secure link (the encrypted link to the DVX secure-link proxy), or Telnet (not available yet). + Connect with Serial port (a cable to the other computer), Modem (dials a phone number), Secure link (the encrypted link to the DVX secure-link proxy), or Telnet (over the network; it needs Networking installed). Port COM1 to COM4. Speed (bps) 300 to 115200 bits per second. Both sides must use the same speed. Data bits 5 to 8; almost always 8. @@ -124,7 +124,7 @@ A passphrase typed on this tab is saved scrambled in Terminal's settings file, s Port Its port number: usually 23 for Telnet. A secure-link proxy usually listens on 2323. .endtable -Telnet uses these settings, and so does the secure link when "Connect over the network" is ticked on the Secure Link tab. The network itself is set up in the Control Panel (Network page). +Telnet uses these settings, and so does the secure link when "Connect over the network" is ticked on the Secure Link tab. Both need Networking, a part of DVX you choose when you install it (Setup can add it later). The network itself is set up in the Control Panel (Network page). .h2 Screen Tab @@ -365,6 +365,7 @@ On a serial port or modem without flow control, ZMODEM downloads automatically a The modem did not answer Check that the modem is on and connected to the chosen port, and that the setup command is right. The passphrase does not match the host's Use exactly the same passphrase as the secure-link proxy, or none on both sides. The host did not answer the secure link Check that the secure-link proxy is running and that the speed matches. + Networking is not installed Telnet and the secure link over the network need Networking: run Setup and add it. The host ... was not found Check the host name, and that a name server is set up (Control Panel, Network page). ... refused the connection on port ... The computer is there but nothing answers on that port: check the port number. ... did not answer Check the address, and that the network is working (Control Panel, Network page, Status...). diff --git a/src/apps/kpunch/terminal/terminal.frm b/src/apps/kpunch/terminal/terminal.frm index b9991582..8f776e84 100644 --- a/src/apps/kpunch/terminal/terminal.frm +++ b/src/apps/kpunch/terminal/terminal.frm @@ -399,20 +399,27 @@ END SUB SUB ConnectSession DIM host AS STRING DIM pass AS STRING + DIM overNet AS BOOLEAN IF transferring THEN EXIT SUB END IF ' The host and port are for Telnet, and for a secure link only when it - ' is set to go over the network. - host = session.host + ' is set to go over the network; both need Networking installed. + overNet = (session.connType = CONN_TELNET) OR (session.connType = CONN_SECURE AND session.secureNet) + host = session.host - IF session.connType = CONN_SECURE AND NOT session.secureNet THEN + IF NOT overNet THEN host = "" END IF - IF (session.connType = CONN_TELNET OR (session.connType = CONN_SECURE AND session.secureNet)) AND host = "" THEN + IF overNet AND NOT ConnTypeAvailable(CONN_TELNET) THEN + MsgBox "Telnet and the secure link over the network need Networking, which is not installed. Setup can add it.", vbOKOnly + vbInformation, TERM_TITLE + EXIT SUB + END IF + + IF overNet AND host = "" THEN MsgBox "Enter the host name on the Network page of Session Settings first.", vbOKOnly + vbInformation, TERM_TITLE EXIT SUB END IF diff --git a/src/apps/kpunch/widshow/widshow.dhs b/src/apps/kpunch/widshow/widshow.dhs index 0ff0e879..e050c063 100644 --- a/src/apps/kpunch/widshow/widshow.dhs +++ b/src/apps/kpunch/widshow/widshow.dhs @@ -1,24 +1,24 @@ -// 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. +# 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. # Widget Showcase help. Compiled to widshow.hlp next to widshow.app; F1 # and Help > Contents in the Widget Showcase open it. diff --git a/src/common.mk b/src/common.mk index bb2ee279..a3ea9872 100644 --- a/src/common.mk +++ b/src/common.mk @@ -98,6 +98,13 @@ endif CC = $(DJGPP_PREFIX)/bin/$(DJGPP_TRIPLE)-gcc DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/$(DJGPP_TRIPLE) $(DVX_ROOT)/src/tools/dxeGen.sh $(DJGPP_PREFIX)/$(DJGPP_TRIPLE)/bin/dxe3gen EXE2COFF = $(DJGPP_PREFIX)/$(DJGPP_TRIPLE)/bin/exe2coff +# Pre-link a module with third-party code down to what its own objects +# reach (tools/dxeGc.sh): $(DXE_GC) OUT OWN.o... -- ALL.o..., the objects +# built with $(DXE_GC_CFLAGS). Functions only: -fdata-sections would put +# this COFF target's zero-filled variables in data sections, stored in +# the module file (lwIP's buffer pools alone are about 100 KB). +DXE_GC = $(DVX_ROOT)/src/tools/dxeGc.sh $(DJGPP_PREFIX)/bin/$(DJGPP_TRIPLE) +DXE_GC_CFLAGS = -ffunction-sections # ---- Flags ---- WARNFLAGS = -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation diff --git a/src/include/basic/comm.bas b/src/include/basic/comm.bas index 5e5333f5..c65a2644 100644 --- a/src/include/basic/comm.bas +++ b/src/include/basic/comm.bas @@ -74,6 +74,7 @@ CONST SER_ERR_INVALID_FIFO = -13 CONST SER_ERR_NULL_PTR = -14 CONST SER_ERR_IRQ_NOT_FOUND = -15 CONST SER_ERR_LOCK_MEM = -16 +CONST SER_ERR_NO_MEMORY = -17 ' Errors seen on received data (SerLineErrors; add them up) CONST SER_LINE_OVERRUN = &H02 @@ -86,7 +87,6 @@ CONST SER_LINE_BREAK = &H10 CONST COMM_OK = 0 CONST COMM_ERR_PARAM = -1 CONST COMM_ERR_SERIAL = -2 -CONST COMM_ERR_ALLOC = -3 CONST COMM_ERR_HANDSHAKE = -4 CONST COMM_ERR_NOT_READY = -5 CONST COMM_ERR_SEND = -6 @@ -97,7 +97,9 @@ CONST COMM_PENDING = 1 ' The highest channel number (channels are 0 to COMM_MAX_CHANNEL) CONST COMM_MAX_CHANNEL = 127 -DECLARE LIBRARY "basrt" +' The library named is the one these need (the serial ports and secure +' links); BASIC finds the functions by name. +DECLARE LIBRARY "serial" ' ========================================================= ' UART Detection and Configuration @@ -201,6 +203,11 @@ DECLARE LIBRARY "basrt" ' Send a break: hold the line in the break state (True), then release ' it (False). DECLARE SUB SerSetBreak(BYVAL com AS LONG, BYVAL state AS LONG) +END DECLARE + +' The packet and encrypted links are the secure link library's (part of +' Communications): without it CommOpen and CommOpenTcp return 0. +DECLARE LIBRARY "seclink" ' ========================================================= ' Packet Serial (reliable delivery + channel multiplexing) @@ -209,11 +216,21 @@ DECLARE LIBRARY "basrt" ' Open a packet serial link. Returns a handle (>0), or 0 on failure. DECLARE FUNCTION CommOpen(BYVAL com AS LONG, BYVAL baud AS LONG, BYVAL dataBits AS LONG, BYVAL parity AS STRING, BYVAL stopBits AS LONG, BYVAL handshake AS LONG) AS LONG + ' Open the same link over a network connection instead of a COM port: + ' tcp is a connection handle from net.bas (TcpConnect or TcpAccept; + ' the other side opens its link the same way). Every channel, the + ' handshake, and encryption work as on a COM port. The connection + ' stays yours: close it with TcpClose after CommClose. Returns a + ' handle (>0), or 0 (the network part of DVX not installed). + DECLARE FUNCTION CommOpenTcp(BYVAL tcp AS LONG) AS LONG + ' Close a packet serial link. DECLARE SUB CommClose(BYVAL handle AS LONG) - ' Send data on a logical channel (0 to COMM_MAX_CHANNEL). - ' encrypt: True to encrypt (requires CommHandshake first). + ' Send data on a logical channel (0 to COMM_MAX_CHANNEL), waiting while + ' the link's window is full (the program's events run meanwhile, as + ' with DoEvents); False when the far side takes nothing for 15 + ' seconds. encrypt: True to encrypt (requires CommHandshake first). DECLARE FUNCTION CommSend(BYVAL handle AS LONG, BYVAL payload AS BINARY STRING, BYVAL channel AS LONG, BYVAL encrypt AS LONG) AS LONG ' Receive pending data from a specific channel. Returns "" if none. @@ -245,7 +262,8 @@ DECLARE LIBRARY "basrt" ' Encryption (authenticated key exchange) ' ========================================================= - ' Perform the key exchange. Blocks until both sides complete, then + ' Perform the key exchange. Waits until both sides complete (up to 30 + ' seconds; the program's events run meanwhile, as with DoEvents), then ' CommSend with encrypt=True encrypts. passphrase is optional: both ' sides must give the same one, or both none. Returns False on a ' mismatch, when only one side uses a passphrase, or on a failed link. diff --git a/src/include/basic/conn.bas b/src/include/basic/conn.bas index e94254b6..9f0588a2 100644 --- a/src/include/basic/conn.bas +++ b/src/include/basic/conn.bas @@ -67,10 +67,10 @@ CONST CONN_OPENING = 1 CONST CONN_OPEN = 2 ' ConnOpen results -CONST CONN_OK = 0 -CONST CONN_ERR_PARAM = -1 -CONST CONN_ERR_PORT = -3 -CONST CONN_ERR_HOST = -4 +CONST CONN_OK = 0 +CONST CONN_ERR_PARAM = -1 +CONST CONN_ERR_PORT = -3 +CONST CONN_ERR_NO_NET = -4 ' Flow control (ConnSetSerial) CONST CONN_FLOW_NONE = 0 @@ -160,6 +160,11 @@ DECLARE LIBRARY "conn" ' and modems). DECLARE FUNCTION ConnCanBreak(BYVAL connType AS LONG) AS LONG + ' True when connections of a type can be made here: every type but + ' CONN_TELNET, which needs Networking installed (a secure link over the + ' network needs it too; without it ConnOpen gives CONN_ERR_NO_NET). + DECLARE FUNCTION ConnTypeAvailable(BYVAL connType AS LONG) AS LONG + ' Hold the line in the break state for a moment. True when it did. DECLARE FUNCTION ConnSendBreak(BYVAL handle AS LONG) AS LONG diff --git a/src/include/basic/designer.bas b/src/include/basic/designer.bas index 11b0c9c9..deb8341b 100644 --- a/src/include/basic/designer.bas +++ b/src/include/basic/designer.bas @@ -147,7 +147,11 @@ DECLARE LIBRARY "basrt" DECLARE FUNCTION FrmControlNames$(BYVAL frmText AS STRING) AS STRING DECLARE FUNCTION FrmCode$(BYVAL frmText AS STRING) AS STRING DECLARE FUNCTION ClsClassName$(BYVAL clsText AS STRING, BYVAL path AS STRING) AS STRING +END DECLARE +' The form model (the Frm* functions with a handle, and FormText$) is the +' BASIC development library's, part of DVX BASIC. +DECLARE LIBRARY "basdev" ' A form model from .frm text, from a file, or a new blank form; ' 0 when the text is not a form. FrmFree frees it. DECLARE FUNCTION FrmOpen(BYVAL frmText AS STRING) AS LONG diff --git a/src/include/basic/fileops.bas b/src/include/basic/fileops.bas index db0fea9e..b6baaa81 100644 --- a/src/include/basic/fileops.bas +++ b/src/include/basic/fileops.bas @@ -41,9 +41,10 @@ CONST FILEOPS_SORT_TYPE = 2 CONST FILEOPS_SORT_DATE = 3 ' FileOpsJobCreate operations -CONST FILEOPS_COPY = 0 -CONST FILEOPS_MOVE = 1 -CONST FILEOPS_DELETE = 2 +CONST FILEOPS_COPY = 0 +CONST FILEOPS_MOVE = 1 +CONST FILEOPS_DELETE = 2 +CONST FILEOPS_MEASURE = 3 ' FileOpsJobRunFor results CONST FILEOPS_STEP_BUSY = 0 ' more to do @@ -199,6 +200,11 @@ DECLARE LIBRARY "fileops" ' -1 when there is none. DECLARE FUNCTION FileOpsListFind(BYVAL list AS LONG, BYVAL itemName AS STRING) AS LONG + ' A new listing of a listing's folders alone, sorted by name, made + ' without reading the disk again (the listing itself is unchanged); 0 + ' when out of memory. FileOpsListFree it when done. + DECLARE FUNCTION FileOpsListFolders(BYVAL list AS LONG) AS LONG + ' An entry's name, whether it is a folder, and its size in bytes. DECLARE FUNCTION FileOpsListName$(BYVAL list AS LONG, BYVAL index AS LONG) AS STRING DECLARE FUNCTION FileOpsListIsFolder(BYVAL list AS LONG, BYVAL index AS LONG) AS LONG @@ -324,10 +330,14 @@ DECLARE LIBRARY "fileops" ' A byte count with thousands separators, "1,234,567". DECLARE FUNCTION FileOpsFormatSize$(BYVAL bytes AS DOUBLE) AS STRING - ' ---- Jobs: copy, move, or delete with progress and questions ---- + ' ---- Jobs: copy, move, delete, or measure with progress and questions ---- - ' Start a FILEOPS_COPY, FILEOPS_MOVE (into destFolder), or - ' FILEOPS_DELETE job. Returns a job handle, 0 when out of memory. + ' Start a FILEOPS_COPY, FILEOPS_MOVE (into destFolder), FILEOPS_DELETE, + ' or FILEOPS_MEASURE job. A measure only counts the files, folders, + ' and bytes (FileOpsJobFilesTotal, FileOpsJobFoldersTotal, and + ' FileOpsJobBytesTotal) without changing anything or asking anything; + ' FileOpsJobUnread says how many folders it could not read. Returns a + ' job handle, 0 when out of memory. DECLARE FUNCTION FileOpsJobCreate(BYVAL op AS LONG, BYVAL destFolder AS STRING) AS LONG ' Add a file or folder (full path) before the job runs. False when it @@ -353,10 +363,13 @@ DECLARE LIBRARY "fileops" DECLARE SUB FileOpsJobCancel(BYVAL job AS LONG) ' Progress: the path being worked on, files done and found so far, - ' and percent done (0-100). + ' folders found so far inside the job's own files and folders, folders + ' a measure could not read (all or part), and percent done (0-100). DECLARE FUNCTION FileOpsJobCurrent$(BYVAL job AS LONG) AS STRING DECLARE FUNCTION FileOpsJobFilesDone(BYVAL job AS LONG) AS LONG DECLARE FUNCTION FileOpsJobFilesTotal(BYVAL job AS LONG) AS LONG + DECLARE FUNCTION FileOpsJobFoldersTotal(BYVAL job AS LONG) AS LONG + DECLARE FUNCTION FileOpsJobUnread(BYVAL job AS LONG) AS LONG DECLARE FUNCTION FileOpsJobPercent(BYVAL job AS LONG) AS LONG ' Progress in bytes: the size of the files found so far, and how much diff --git a/src/include/basic/game.bas b/src/include/basic/game.bas index b80b7e60..9949831b 100644 --- a/src/include/basic/game.bas +++ b/src/include/basic/game.bas @@ -65,12 +65,15 @@ DECLARE LIBRARY "game" ' program's windows is active. DECLARE SUB GameKeyClear() - ' How many frames FRAMEWAIT has skipped because the program was too - ' slow to draw them in time, since it began waiting at its present - ' rate (0 before the first FRAMEWAIT). - DECLARE FUNCTION FrameMissed() AS LONG - ' Wait until the screen starts drawing its next picture from the top; ' a full-screen game that draws right after it does not tear. DECLARE SUB GameWaitRetrace() END DECLARE + +' FRAMEWAIT's own counter, kept by the BASIC runtime +DECLARE LIBRARY "basrt" + ' How many frames FRAMEWAIT has skipped because the program was too + ' slow to draw them in time, since it began waiting at its present + ' rate (0 before the first FRAMEWAIT). + DECLARE FUNCTION FrameMissed() AS LONG +END DECLARE diff --git a/src/include/basic/help.bas b/src/include/basic/help.bas index a5cd67a5..ab185296 100644 --- a/src/include/basic/help.bas +++ b/src/include/basic/help.bas @@ -29,9 +29,15 @@ ' ' Usage: Add this file to your project. -DECLARE LIBRARY "basrt" - ' Compile a .dhs source file to a binary .hlp file. - ' Returns True on success; HelpCompileMessages$ says what was wrong. +' The library named is the one these need (the shell's help viewer); +' BASIC finds the functions by name. +DECLARE LIBRARY "dvxshell" + ' Compile a .dhs source file to a binary .hlp file (its .image + ' pictures are looked for beside it). Returns True on success; + ' HelpCompileMessages$ says what was wrong. While it compiles, DVX + ' and your program's events go on, as with DoEvents; a second + ' HelpCompile meanwhile (yours or another program's) returns False at + ' once, and the compile stops if your program ends. DECLARE FUNCTION HelpCompile(BYVAL inputFile AS STRING, BYVAL outputFile AS STRING) AS INTEGER ' The errors and warnings of the last HelpCompile, one per line diff --git a/src/include/basic/http.bas b/src/include/basic/http.bas index 4ba45b9c..b10bebde 100644 --- a/src/include/basic/http.bas +++ b/src/include/basic/http.bas @@ -30,7 +30,7 @@ ' HttpOpen(method, url) makes a request and returns a number ("handle", ' 0 when out of memory); close it with HttpClose when done (whatever a ' program leaves open is closed when it ends). Set it up (HttpAddHeader, -' HttpSetBody, HttpSaveTo, HttpSetTimeout), HttpStart it, then call +' HttpSetBody or HttpSetBodyFile, HttpSaveTo, HttpSetTimeout), HttpStart it, then call ' HttpRunFor until it is HTTP_DONE or HTTP_FAILED: ' ' h = HttpOpen("GET", "http://example.com/news.txt") @@ -60,12 +60,14 @@ DECLARE LIBRARY "http" DECLARE SUB HttpClose(BYVAL h AS LONG) ' Before starting: a header to send, a body to send (with its type, - ' such as "application/json"), a file to save the reply's body in + ' such as "application/json"; or a file's bytes, sent a piece at a + ' time, so a file of any size), a file to save the reply's body in ' (with keepPart True, a part already there is kept and only the rest ' fetched), and the seconds without progress before giving up (30 to ' start with). DECLARE FUNCTION HttpAddHeader(BYVAL h AS LONG, BYVAL headerName AS STRING, BYVAL value AS STRING) AS LONG DECLARE FUNCTION HttpSetBody(BYVAL h AS LONG, BYVAL body AS BINARY STRING, BYVAL contentType AS STRING) AS LONG + DECLARE FUNCTION HttpSetBodyFile(BYVAL h AS LONG, BYVAL path AS STRING, BYVAL contentType AS STRING) AS LONG DECLARE SUB HttpSaveTo(BYVAL h AS LONG, BYVAL path AS STRING, BYVAL keepPart AS LONG) DECLARE SUB HttpSetTimeout(BYVAL h AS LONG, BYVAL seconds AS LONG) diff --git a/src/include/basic/joystick.bas b/src/include/basic/joystick.bas index b37a69cb..820a43ca 100644 --- a/src/include/basic/joystick.bas +++ b/src/include/basic/joystick.bas @@ -49,6 +49,13 @@ CONST JOY_HAT_CENTERED = -1 CONST JOY_AXIS_MIN = -32768 CONST JOY_AXIS_MAX = 32767 +' What changed, as JoyEvent tells it +CONST JOY_EVENT_NONE = 0 +CONST JOY_EVENT_AXIS = 1 +CONST JOY_EVENT_BUTTON_DOWN = 2 +CONST JOY_EVENT_BUTTON_UP = 3 +CONST JOY_EVENT_HAT = 4 + ' The listed settings for JoyChoice and friends (the library's own ' numbers, which do not change): the game port's mode, and when the ' keyboard joystick stands in for a missing one. @@ -108,6 +115,22 @@ DECLARE LIBRARY "joystick" ' False when the joystick stopped answering (unplugged). DECLARE FUNCTION JoyConnected(BYVAL joy AS LONG) AS LONG + ' Changes as they happen, instead of reading the joystick: JoyWatch + ' joy, True starts keeping that joystick's changes (False stops), and + ' JoyEvent() gives the next one since it was last asked (each once, + ' in order; the last 64 are kept): JOY_EVENT_AXIS (an axis moved), + ' JOY_EVENT_BUTTON_DOWN, JOY_EVENT_BUTTON_UP, JOY_EVENT_HAT, or + ' JOY_EVENT_NONE when nothing has changed. JoyEventJoy() and + ' JoyEventIndex() say which joystick and which axis, button, or hat + ' (from 1), and JoyEventValue() an axis's new position or a hat's + ' direction. Call JoyEvent from a Timer until it gives + ' JOY_EVENT_NONE. JoyWatch is True when there is such a joystick. + DECLARE FUNCTION JoyWatch(BYVAL joy AS LONG, BYVAL watching AS LONG) AS LONG + DECLARE FUNCTION JoyEvent() AS LONG + DECLARE FUNCTION JoyEventJoy() AS LONG + DECLARE FUNCTION JoyEventIndex() AS LONG + DECLARE FUNCTION JoyEventValue() AS LONG + ' ---- Setting joysticks up ---- ' Look for joysticks again (one plugged in later, a new game port diff --git a/src/include/basic/music.bas b/src/include/basic/music.bas index 09c3fcb6..4f24aed0 100644 --- a/src/include/basic/music.bas +++ b/src/include/basic/music.bas @@ -75,6 +75,10 @@ DECLARE LIBRARY "music" ' until MusicPlay. DECLARE FUNCTION MusicLoad(BYVAL fileName AS STRING) AS LONG + ' The same from the bytes of a music or sound file, such as + ' ResGetData$ or HttpBody$ gives. + DECLARE FUNCTION MusicLoadData(BYVAL bytes AS BINARY STRING) AS LONG + ' A MIDI music made from a music macro string (as PLAY takes it): its ' notes play on the General MIDI instruments its I commands name, the ' piano before any I. Nothing plays until MusicPlay. @@ -112,6 +116,11 @@ DECLARE LIBRARY "music" ' True while the music is playing (not stopped, finished, or paused). DECLARE FUNCTION MusicPlaying(BYVAL music AS LONG) AS LONG + ' True once after the music played to its end (not when stopped) + ' since MusicPlay started it: call it from a Timer to start the next + ' tune. + DECLARE FUNCTION MusicEnded(BYVAL music AS LONG) AS LONG + ' Set the volume of this music (percent). DECLARE SUB MusicVolume(BYVAL music AS LONG, BYVAL volume AS LONG) diff --git a/src/include/basic/net.bas b/src/include/basic/net.bas index ad374b86..9e4f2c31 100644 --- a/src/include/basic/net.bas +++ b/src/include/basic/net.bas @@ -26,9 +26,11 @@ ' Control Panel (Network page). Handles are numbers; 0 means the call ' failed, and NetError$ then says why in plain words. ' -' TcpConnect and TcpSend wait until they are done (other programs keep -' running meanwhile). Everything else returns at once, so a program stays -' event driven by checking from a Timer: +' HostLookup$, TcpConnect, and TcpSend wait until they are done (other +' programs keep running meanwhile, but yours waits). Everything else +' returns at once, so a program stays event driven by checking from a +' Timer, and those three have forms that do not wait either: +' HostLookupStart, TcpConnectStart, and TcpSendSome. ' ' Sub Timer1_Timer () ' IF TcpPending(conn) > 0 THEN PRINT TcpRecv$(conn) @@ -43,12 +45,34 @@ ' ' Usage: Add this file to your project. +' HostLookupPoll and TcpConnectPoll results +CONST NET_BUSY = 0 ' still going: check again later (from a Timer) +CONST NET_DONE = 1 +CONST NET_FAILED = 2 ' NetError$ says why + +' NetState results +CONST NET_STATE_STOPPED = 0 ' not started, or stopped (NetStop) +CONST NET_STATE_NO_DRIVER = 1 ' started, but no network card driver was found +CONST NET_STATE_CONFIGURING = 2 ' waiting for an address from the network +CONST NET_STATE_UP = 3 ' has an address: ready + DECLARE LIBRARY "dvxnet" ' The address of hostName (like "bbs.example.org"), as text such as ' "192.0.2.10", or "" when it cannot be found. DECLARE FUNCTION HostLookup$(BYVAL hostName AS STRING) AS STRING + ' The same without waiting: HostLookupStart gives a lookup handle (0 + ' when too many are under way), HostLookupPoll says NET_BUSY, NET_DONE, + ' or NET_FAILED, HostLookupResult$ gives the address once it is + ' NET_DONE, and HostLookupClose frees the handle (or gives the lookup + ' up). The first lookup after DVX starts may wait for the network's + ' settings (DHCP) too. + DECLARE FUNCTION HostLookupStart(BYVAL hostName AS STRING) AS LONG + DECLARE FUNCTION HostLookupPoll(BYVAL lookup AS LONG) AS LONG + DECLARE FUNCTION HostLookupResult$(BYVAL lookup AS LONG) AS STRING + DECLARE SUB HostLookupClose(BYVAL lookup AS LONG) + ' The name of an address such as "192.0.2.10"; the address itself ' when no name is known for it, "" when it is not an address. DECLARE FUNCTION HostName$(BYVAL address AS STRING) AS STRING @@ -63,6 +87,14 @@ DECLARE LIBRARY "dvxnet" ' or 0 when the connection could not be made (waits up to 20 seconds). DECLARE FUNCTION TcpConnect(BYVAL host AS STRING, BYVAL port AS LONG) AS LONG + ' The same without waiting: TcpConnectStart gives the connection's + ' handle at once (0 when it cannot start), and TcpConnectPoll says + ' NET_BUSY while the host is looked up and the connection made, + ' NET_DONE once it is (the handle is then like TcpConnect's), or + ' NET_FAILED (close it with TcpClose). + DECLARE FUNCTION TcpConnectStart(BYVAL host AS STRING, BYVAL port AS LONG) AS LONG + DECLARE FUNCTION TcpConnectPoll(BYVAL handle AS LONG) AS LONG + ' Wait for connections on port. Returns a listening handle, or 0. DECLARE FUNCTION TcpListen(BYVAL port AS LONG) AS LONG @@ -74,12 +106,22 @@ DECLARE LIBRARY "dvxnet" ' of bytes sent, or -1. DECLARE FUNCTION TcpSend(BYVAL handle AS LONG, BYVAL bytes AS BINARY STRING) AS LONG + ' Send what the connection takes now, never waiting: the number of + ' bytes taken from the start of bytes (0 while it is full: send the + ' rest later), or -1. + DECLARE FUNCTION TcpSendSome(BYVAL handle AS LONG, BYVAL bytes AS BINARY STRING) AS LONG + ' How many bytes have arrived and wait to be read (-1 for a bad handle). DECLARE FUNCTION TcpPending(BYVAL handle AS LONG) AS LONG ' What has arrived (up to 4096 bytes), or "" when nothing has. DECLARE FUNCTION TcpRecv$(BYVAL handle AS LONG) AS BINARY STRING + ' What has arrived (up to 4096 bytes), left waiting for the next + ' TcpRecv$: look for the end of a line, or a message's length, before + ' taking any of it. "" when nothing has. + DECLARE FUNCTION TcpPeek$(BYVAL handle AS LONG) AS BINARY STRING + ' 1 while the connection is open, 0 once the other side closed it ' (text may still be waiting: read it with TcpRecv$ first). DECLARE FUNCTION TcpConnected(BYVAL handle AS LONG) AS LONG @@ -179,4 +221,8 @@ DECLARE LIBRARY "dvxnet" ' "rxpackets", "rxbytes", "rxdropped", "txpackets", "txbytes" ("" for ' an unknown field). DECLARE FUNCTION NetStatus$(BYVAL field AS STRING) AS STRING + + ' The network's state as a number: NET_STATE_STOPPED, + ' NET_STATE_NO_DRIVER, NET_STATE_CONFIGURING, or NET_STATE_UP. + DECLARE FUNCTION NetState() AS LONG END DECLARE diff --git a/src/include/basic/pkg.bas b/src/include/basic/pkg.bas index f47226da..f6ca0e28 100644 --- a/src/include/basic/pkg.bas +++ b/src/include/basic/pkg.bas @@ -24,9 +24,9 @@ ' pkg.bas -- Add-on Package Library for DVX BASIC ' Lists the packages installed in DVX, reads add-on packages (.DPK files), ' plans what installing them takes (the packages they need, checked -' against this DVX, its other packages, and the keys DVX trusts), and -' installs and removes packages as a job a program runs a little at a -' time with progress; and makes packages, signs them, and makes keys. +' against this DVX, its other packages, and the keys DVX trusts), +' installs and removes packages, and makes and signs packages, each as a +' job a program runs a little at a time with progress; and makes keys. ' Usage: Add this file to your project. ' ' A session (PkgOpen) is a number ("handle", 0 = none); close it with @@ -55,6 +55,8 @@ CONST PKG_FIELD_EXPLICIT = 13 ' installed: "yes" chosen, "no" only needed CONST PKG_FIELD_TRUST = 14 ' offer: "system" (DVX's own key), "user" (a key you trust), "none" CONST PKG_FIELD_WHERE = 15 ' offer: its file CONST PKG_FIELD_SIZE = 16 ' the bytes of its files +CONST PKG_FIELD_SHA256 = 17 ' catalogue (PkgNetField$): its package file's SHA-256, as hex +CONST PKG_FIELD_REPO_SIGNED = 18 ' catalogue (PkgNetField$): "yes" when the repository signed it, else "no" ' PkgRunFor results CONST PKG_RUN_BUSY = 0 ' more to do @@ -68,6 +70,11 @@ DECLARE LIBRARY "pkg" ' handle, or 0 when out of memory. DECLARE FUNCTION PkgOpen() AS LONG + ' A session on another DVX folder (root; "" is this DVX's), such as a + ' copy being made ready on another disk: its installed packages and + ' the keys its user folder trusts. 0 when its records cannot be read. + DECLARE FUNCTION PkgOpenIn(BYVAL root AS STRING) AS LONG + ' Close a session (a running job stops; one whose files are already in ' place is finished first). DECLARE SUB PkgClose(BYVAL s AS LONG) @@ -100,15 +107,17 @@ DECLARE LIBRARY "pkg" ' ---- Packages from files ---- - ' Read the package file path, and every other package in its folder - ' (what it may need), as the offers (the ones before are gone). - ' Returns path's offer number (0), or -1: PkgError$ says why. - DECLARE FUNCTION PkgOfferFile(BYVAL s AS LONG, BYVAL path AS STRING) AS LONG + ' Start reading the package file path, and every other package in its + ' folder (what it may need), as the offers (the ones before are + ' gone): a job, run with PkgRunFor (checking a package's signature + ' takes a moment). Path's offer is 0; the job fails when path cannot + ' be read (PkgRunMessage$ says why). False while a job runs. + DECLARE FUNCTION PkgOfferFileStart(BYVAL s AS LONG, BYVAL path AS STRING) AS LONG - ' Read every package file in folder as the offers (the ones before - ' are gone; one that cannot be read is left out): how many, or -1 - ' while a job runs. What pkgnet.bas fetched is planned this way. - DECLARE FUNCTION PkgOfferFolder(BYVAL s AS LONG, BYVAL folder AS STRING) AS LONG + ' Start reading every package file in folder as the offers, the same + ' way (one that cannot be read is left out; PkgOfferCount says how + ' many there were). What pkgnet.bas fetched is planned this way. + DECLARE FUNCTION PkgOfferFolderStart(BYVAL s AS LONG, BYVAL folder AS STRING) AS LONG ' How many offers there are, and the number of the newest offer of ' the package id (-1 when none is). @@ -122,9 +131,17 @@ DECLARE LIBRARY "pkg" DECLARE FUNCTION PkgOfferDependsRows$(BYVAL s AS LONG, BYVAL offer AS LONG) AS STRING ' Trust the key file keyPath (NAME.KEY) from now on. Read the offers - ' again (PkgOfferFile) to see their new trust. + ' again (PkgOfferFileStart) to see their new trust. DECLARE FUNCTION PkgTrustKey(BYVAL s AS LONG, BYVAL keyPath AS STRING) AS LONG + ' The keys DVX trusts, as rows: name, id, and "system" (DVX's own) or + ' "user" (one you chose to trust with PkgTrustKey). + DECLARE FUNCTION PkgKeysRows$(BYVAL s AS LONG) AS STRING + + ' Stop trusting the key with id keyId (as PkgKeysRows$ gives it): it + ' leaves your KEYS folder. DVX's own keys stay. True when done. + DECLARE FUNCTION PkgUntrustKey(BYVAL s AS LONG, BYVAL keyId AS STRING) AS LONG + ' ---- Planning ---- ' Plan installing the offers numbered in offers ("0" or "0 3") with @@ -160,7 +177,9 @@ DECLARE LIBRARY "pkg" ' Start installing the plan, or removing the installed packages ids ' (one per line, in that order; files changed since they were ' installed stay unless removeChanged). False when a job is running - ' (or, installing, there is no plan). + ' (or, installing, there is no plan). Reading packages, and building + ' and signing one, are jobs too (PkgOfferFileStart, + ' PkgOfferFolderStart, PkgBuildStart, PkgSignStart). DECLARE FUNCTION PkgInstallStart(BYVAL s AS LONG) AS LONG DECLARE FUNCTION PkgRemoveStart(BYVAL s AS LONG, BYVAL ids AS STRING, BYVAL removeChanged AS LONG) AS LONG @@ -168,7 +187,8 @@ DECLARE LIBRARY "pkg" ' PKG_RUN_FAILED. Call it again (from a Timer, say) while BUSY. DECLARE FUNCTION PkgRunFor(BYVAL s AS LONG, BYVAL milliseconds AS LONG) AS LONG - ' Progress: percent done (0-100) and the package it is on. + ' Progress: percent done (0-100) and the package it is on (reading: + ' the package file; building or signing: the file in it). DECLARE FUNCTION PkgRunPercent(BYVAL s AS LONG) AS LONG DECLARE FUNCTION PkgRunCurrent$(BYVAL s AS LONG) AS STRING @@ -179,8 +199,11 @@ DECLARE LIBRARY "pkg" ' were installed, one per line. DECLARE FUNCTION PkgRunKept$(BYVAL s AS LONG) AS STRING - ' Stop the job after the package it is on (one still being unpacked - ' is left as it was). + ' Stop the job: reading, building, or signing at once (nothing read; + ' nothing left behind); + ' installing or removing after the package it is on (one still being + ' unpacked is left as it was; one whose files are in is finished first, + ' so keep calling PkgRunFor while it says PKG_RUN_BUSY). DECLARE SUB PkgRunCancel(BYVAL s AS LONG) ' ---- Making packages ---- @@ -200,28 +223,45 @@ DECLARE LIBRARY "pkg" DECLARE FUNCTION PkgIdPart$(BYVAL text AS STRING) AS STRING DECLARE FUNCTION PkgVersionValid(BYVAL text AS STRING) AS LONG + ' -1 when version a is older than b, 0 when they are the same, 1 when + ' a is newer: "1.10.0" is newer than "1.9.2", which comparing the + ' texts gets wrong. A text that is not a version counts as older than + ' any version. + DECLARE FUNCTION PkgVersionCompare(BYVAL a AS STRING, BYVAL b AS STRING) AS LONG + ' This DVX's version (a package's oldest DVX) and the ABI its programs, ' libraries, and widgets are made for (a package's abi). DECLARE FUNCTION PkgDvxVersion$() AS STRING DECLARE FUNCTION PkgDvxAbi() AS LONG - ' Every file under folder as the files for PkgBuild: rows of its name + ' Every file under folder as the files for PkgBuildStart: rows of its name ' in the package (prefix, then its name under folder; "" names it from ' folder, laid out like the DVX folder) and the file, in name order. ' "" with the reason in PkgError$ when folder cannot be read. DECLARE FUNCTION PkgFolderRows$(BYVAL s AS LONG, BYVAL folder AS STRING, BYVAL prefix AS STRING) AS STRING - ' Build the package outPath from templatePath (a PACKAGE.INI without - ' [files] or format; other sections are ignored; a program, library, - ' or widget with no abi gets PkgDvxAbi's) and files (rows of a name - ' in the package TAB the file it is made from), signed with the secret - ' key file secretPath unless it is "". True, or False: PkgError$ says - ' why. - DECLARE FUNCTION PkgBuild(BYVAL s AS LONG, BYVAL templatePath AS STRING, BYVAL files AS STRING, BYVAL outPath AS STRING, BYVAL secretPath AS STRING) AS LONG + ' Start building the package outPath from templatePath (a PACKAGE.INI + ' without [files] or format; other sections are ignored; a program, + ' library, or widget with no abi gets PkgDvxAbi's) and files (rows of + ' a name in the package TAB the file it is made from), signed with the + ' secret key file secretPath unless it is "": a job, run with + ' PkgRunFor. False when it cannot start (PkgError$ says why) or a job + ' is running. + DECLARE FUNCTION PkgBuildStart(BYVAL s AS LONG, BYVAL templatePath AS STRING, BYVAL files AS STRING, BYVAL outPath AS STRING, BYVAL secretPath AS STRING) AS LONG - ' Sign the package dpkPath with the secret key file secretPath: into it, - ' or beside it (NAME.SIG) when detached. - DECLARE FUNCTION PkgSign(BYVAL s AS LONG, BYVAL dpkPath AS STRING, BYVAL secretPath AS STRING, BYVAL detached AS LONG) AS LONG + ' Start signing the package dpkPath with the secret key file + ' secretPath: into it (a job, run with PkgRunFor), or beside it + ' (NAME.SIG) when detached (done at once: PkgRunFor says PKG_RUN_DONE). + ' False as PkgBuildStart. + DECLARE FUNCTION PkgSignStart(BYVAL s AS LONG, BYVAL dpkPath AS STRING, BYVAL secretPath AS STRING, BYVAL detached AS LONG) AS LONG + + ' Check the package dpkPath without installing it: every file against + ' its manifest (each file's size and SHA-256, so a damaged or changed + ' package is found), a job run with PkgRunFor like an install. With + ' keyPath (a NAME.KEY file) the package must also be signed by that key + ' -- in it or beside it (NAME.SIG) -- or nothing starts. False with the + ' reason in PkgError$ (none while a job is running). + DECLARE FUNCTION PkgVerifyStart(BYVAL s AS LONG, BYVAL dpkPath AS STRING, BYVAL keyPath AS STRING) AS LONG ' Make a key pair called keyName: basePath.KEY, the public key to give out ' (and to trust: PkgTrustKey), and basePath.SEC, the secret key to sign @@ -231,4 +271,34 @@ DECLARE LIBRARY "pkg" ' The name and id of the key in a key file (.KEY) or secret key file ' (.SEC) as a row; "" with the reason in PkgError$ when it is neither. DECLARE FUNCTION PkgKeyInfo$(BYVAL s AS LONG, BYVAL path AS STRING) AS STRING + + ' Sign any bytes (a file read into a string, a message) with the + ' secret key file secretPath: the signature as text, to keep or send + ' with them; "" with the reason in PkgError$. + DECLARE FUNCTION PkgSignData$(BYVAL s AS LONG, BYVAL bytes AS BINARY STRING, BYVAL secretPath AS STRING) AS STRING + + ' True when signature (PkgSignData$'s text, or a .SIG file's) was made + ' over bytes by the key in the key file keyPath -- or, with keyPath "", + ' by a key DVX trusts. False with the reason in PkgError$. + DECLARE FUNCTION PkgCheckData(BYVAL s AS LONG, BYVAL bytes AS BINARY STRING, BYVAL signature AS STRING, BYVAL keyPath AS STRING) AS LONG + + ' ---- Plain ZIP archives (jobs run with PkgRunFor, like an install) ---- + + ' Read the ZIP zipPath to its end, every file in it checked; then + ' PkgZipRows$ lists what is in it. + DECLARE FUNCTION PkgZipListStart(BYVAL s AS LONG, BYVAL zipPath AS STRING) AS LONG + + ' Unpack the ZIP zipPath into folder (made as needed; a file already + ' there is replaced). A name in the ZIP that would leave folder stops + ' it. + DECLARE FUNCTION PkgZipExtractStart(BYVAL s AS LONG, BYVAL zipPath AS STRING, BYVAL folder AS STRING) AS LONG + + ' Make the ZIP zipPath from files: rows of a name in the ZIP and the + ' file it is made from, TAB between them (PkgFolderRows$ gives them + ' for a folder). + DECLARE FUNCTION PkgZipCreateStart(BYVAL s AS LONG, BYVAL zipPath AS STRING, BYVAL files AS STRING) AS LONG + + ' What the last ZIP job listed, unpacked, or packed, as rows: name, + ' size, date (YYYY-MM-DD HH:MM), and "folder" for a folder. + DECLARE FUNCTION PkgZipRows$(BYVAL s AS LONG) AS STRING END DECLARE diff --git a/src/include/basic/pkgnet.bas b/src/include/basic/pkgnet.bas index cab64831..9f73f601 100644 --- a/src/include/basic/pkgnet.bas +++ b/src/include/basic/pkgnet.bas @@ -25,7 +25,7 @@ ' that is newer than the packages installed (DVX itself among them), and ' fetches packages, with what they need that is not installed, into a ' download folder. With an account on the server, it also publishes -' packages there and lists what the account has sent. There pkg.bas reads them (PkgOfferFolder), plans +' packages there and lists what the account has sent. There pkg.bas reads them (PkgOfferFolderStart), plans ' installing them, and installs them as it does a package file. Every ' package fetched is checked against the size and fingerprint (SHA-256) ' the server gives, and the server's signature of it is fetched beside @@ -62,6 +62,11 @@ DECLARE LIBRARY "pkgnet" ' memory. An account for a handle on another server: ' PkgNetAccount(n, email, password). DECLARE FUNCTION PkgNetOpen(BYVAL server AS STRING) AS LONG + + ' PkgNetOpen for another DVX folder (root, "" for this DVX: what is + ' installed there decides the updates), fetching into downloadDir ("" + ' for the user folder's DOWNLOAD). + DECLARE FUNCTION PkgNetOpenIn(BYVAL server AS STRING, BYVAL root AS STRING, BYVAL downloadDir AS STRING) AS LONG DECLARE SUB PkgNetClose(BYVAL n AS LONG) DECLARE SUB PkgNetAccount(BYVAL n AS LONG, BYVAL user AS STRING, BYVAL password AS STRING) @@ -124,7 +129,7 @@ DECLARE LIBRARY "pkgnet" DECLARE FUNCTION PkgNetField$(BYVAL n AS LONG, BYVAL id AS STRING, BYVAL field AS LONG) AS STRING DECLARE FUNCTION PkgNetDependsRows$(BYVAL n AS LONG, BYVAL id AS STRING) AS STRING - ' The download folder (for pkg.bas's PkgOfferFolder after a fetch), + ' The download folder (for pkg.bas's PkgOfferFolderStart after a fetch), ' and removing what the last fetch put there (once it is installed, ' or not wanted). DECLARE FUNCTION PkgNetDir$(BYVAL n AS LONG) AS STRING diff --git a/src/include/basic/print.bas b/src/include/basic/print.bas index a426e3e6..25495a73 100644 --- a/src/include/basic/print.bas +++ b/src/include/basic/print.bas @@ -157,12 +157,6 @@ DECLARE LIBRARY "print" DECLARE FUNCTION PrintChoiceFirst() AS LONG DECLARE FUNCTION PrintChoiceLast() AS LONG - ' Print what a form shows (it must be loaded), scaled to the page, on - ' a printer model that prints pictures; PrintFormDialog shows the - ' Print dialog first. True when it was printed. - DECLARE FUNCTION PrintForm(BYVAL formName AS STRING) AS LONG - DECLARE FUNCTION PrintFormDialog(BYVAL formName AS STRING) AS LONG - ' ---- The printer ports themselves (LPT1 is 1, up to 3) ' True when the computer has printer port LPTn. @@ -177,6 +171,15 @@ DECLARE LIBRARY "print" DECLARE FUNCTION PrintStatusProblem$(BYVAL status AS LONG, BYVAL afterWrite AS LONG, BYVAL where AS STRING) AS STRING END DECLARE +' Printing a form, done by the BASIC runtime (which has the forms) +DECLARE LIBRARY "basrt" + ' Print what a form shows (it must be loaded), scaled to the page, on + ' a printer model that prints pictures; PrintFormDialog shows the + ' Print dialog first. True when it was printed. + DECLARE FUNCTION PrintForm(BYVAL formName AS STRING) AS LONG + DECLARE FUNCTION PrintFormDialog(BYVAL formName AS STRING) AS LONG +END DECLARE + ' The state of a printer (PrintPortStatus) CONST PRINT_STATUS_TIMEOUT = &H01 CONST PRINT_STATUS_IO_ERROR = &H08 diff --git a/src/include/basic/resource.bas b/src/include/basic/resource.bas index eb1a152b..06e4149a 100644 --- a/src/include/basic/resource.bas +++ b/src/include/basic/resource.bas @@ -39,9 +39,10 @@ CONST RES_NAME_ICON = "icon32" CONST RES_NAME_WIDGET_ICON = "icon24" CONST RES_NAME_MAX = 32 -DECLARE LIBRARY "basrt" +DECLARE LIBRARY "libdvx" ' Open a resource file for reading. Returns a handle (> 0) - ' or 0 on failure. Close with ResClose when done. + ' or 0 on failure. Close with ResClose when done (whatever a + ' program leaves open is closed when it ends). DECLARE FUNCTION ResOpen(BYVAL path AS STRING) AS LONG ' Close a resource handle. diff --git a/src/include/basic/security.bas b/src/include/basic/security.bas index 3ec92e1f..c0546c1b 100644 --- a/src/include/basic/security.bas +++ b/src/include/basic/security.bas @@ -23,10 +23,11 @@ ' security.bas -- Security for DVX BASIC ' ' The building blocks the encrypted serial link uses, for your own -' protocols and files: random bytes for keys, a key exchange (two -' programs agree on a secret over a line anyone can read), a cipher, -' SHA-256 hashes, HMAC signatures, key stretching (HKDF), and a compare -' that does not give away where two codes differ. +' protocols and files: random bytes for keys, key exchanges (two +' programs agree on a secret over a line anyone can read), ciphers, +' sealing (encryption that also proves nothing was changed), SHA-256 +' hashes, HMAC and public-key signatures, key stretching (HKDF), and a +' compare that does not give away where two codes differ. ' ' Keys, data, and results are strings that may hold any bytes, CHR$(0) ' too. A key exchange, cipher, hash, or HMAC in progress is a handle (0 @@ -34,25 +35,39 @@ ' and SecHmacEnd$ free theirs). Whatever a program leaves open is freed ' when it ends. ' -' A secret from SecDhSecret$ is not a key: make keys from it with -' SecHkdf$. The same key and counter must never encrypt two different -' messages: move the counter on (SecCipherSetCounter) or make a new key. +' A secret from SecDhSecret$ or SecX25519$ is not a key: make keys from +' it with SecHkdf$. The same key and counter must never encrypt two +' different messages: move the counter on (SecCipherSetCounter) or make +' a new key. The same goes for a sealing key and nonce: a fresh +' SecRandom$(SEC_AEAD_NONCE_SIZE) for every message is safe. ' ' Usage: Add this file to your project. ' Sizes in bytes -CONST SEC_DH_KEY_SIZE = 128 ' a key exchange public key and secret -CONST SEC_XTEA_KEY_SIZE = 16 ' a cipher key -CONST SEC_SHA256_SIZE = 32 ' a SHA-256 hash or HMAC -CONST SEC_HKDF_MAX_OUT = 8160 ' the most SecHkdf$ makes at once +CONST SEC_DH_KEY_SIZE = 128 ' a key exchange public key and secret +CONST SEC_XTEA_KEY_SIZE = 16 ' a cipher key +CONST SEC_SHA256_SIZE = 32 ' a SHA-256 hash or HMAC +CONST SEC_HKDF_MAX_OUT = 8160 ' the most SecHkdf$ makes at once +CONST SEC_X25519_KEY_SIZE = 32 ' an X25519 secret key, public key, or shared secret +CONST SEC_AEAD_KEY_SIZE = 32 ' a sealing key +CONST SEC_AEAD_NONCE_SIZE = 24 ' a sealing nonce +CONST SEC_AEAD_TAG_SIZE = 16 ' what sealing adds to a message +CONST SEC_ED25519_SEED_SIZE = 32 ' the random bytes a signing key is made from +CONST SEC_ED25519_SECRET_SIZE = 64 ' a signing (secret) key +CONST SEC_ED25519_PUBLIC_SIZE = 32 ' a signature checking (public) key +CONST SEC_ED25519_SIG_SIZE = 64 ' a signature -' Results of SecDhComputeSecret +' Results of SecDhComputeSecret, SecDhComputeStart, and SecDhStep CONST SEC_OK = 0 CONST SEC_ERR_PARAM = -1 CONST SEC_ERR_NOT_READY = -2 CONST SEC_ERR_IO = -3 +CONST SEC_DH_PENDING = 1 ' SecDhStep: more of the work to do -DECLARE LIBRARY "serial" +' Result of SecX25519Step while there is more to do (then SEC_OK) +CONST SEC_X25519_PENDING = 1 + +DECLARE LIBRARY "crypto" ' count random bytes, good for keys and salts (RND is not). DECLARE FUNCTION SecRandom$(BYVAL count AS LONG) AS BINARY STRING @@ -70,6 +85,15 @@ DECLARE LIBRARY "serial" ' computer). Returns a handle, or 0. DECLARE FUNCTION SecDhCreate() AS LONG + ' The same without the wait: SecDhCreateStart gives the handle at once, + ' and SecDhStep then computes its keys a slice at a time (call it from + ' a Timer until it stops saying SEC_DH_PENDING; SEC_OK once they are + ' ready). SecDhComputeStart starts the secret the same way (checking + ' the other side's key at once), and SecDhStep finishes it. + DECLARE FUNCTION SecDhCreateStart() AS LONG + DECLARE FUNCTION SecDhComputeStart(BYVAL handle AS LONG, BYVAL remotePublicKey AS BINARY STRING) AS LONG + DECLARE FUNCTION SecDhStep(BYVAL handle AS LONG) AS LONG + ' This side's public key (SEC_DH_KEY_SIZE bytes), to send. DECLARE FUNCTION SecDhPublicKey$(BYVAL handle AS LONG) AS BINARY STRING @@ -83,6 +107,31 @@ DECLARE LIBRARY "serial" DECLARE SUB SecDhFree(BYVAL handle AS LONG) + ' ---- Key exchange (X25519): the same idea, much faster + ' Each side makes a secret key (SecRandom$(SEC_X25519_KEY_SIZE)), + ' sends its SecX25519PublicKey$ to the other, and gives what it got to + ' SecX25519$; both then have the same secret. Each call takes about a + ' tenth of a second on a slow computer. + + ' The public key of secretKey ("" for a key of another length). + DECLARE FUNCTION SecX25519PublicKey$(BYVAL secretKey AS BINARY STRING) AS BINARY STRING + + ' The shared secret (SEC_X25519_KEY_SIZE bytes): "" for a key of + ' another length, or a public key that would give everyone the same + ' secret (the other side is cheating). + DECLARE FUNCTION SecX25519$(BYVAL secretKey AS BINARY STRING, BYVAL otherPublicKey AS BINARY STRING) AS BINARY STRING + + ' The same without the wait: SecX25519Start gives a handle at once + ' (with otherPublicKey "" it makes the public key of secretKey), and + ' SecX25519Step does a ninth of the work a call (call it from a Timer + ' until it stops saying SEC_X25519_PENDING; SEC_OK once it is done). + ' SecX25519End$ then gives what SecX25519$ (or SecX25519PublicKey$) + ' would have, and frees the handle; SecX25519Free drops one. + DECLARE FUNCTION SecX25519Start(BYVAL secretKey AS BINARY STRING, BYVAL otherPublicKey AS BINARY STRING) AS LONG + DECLARE FUNCTION SecX25519Step(BYVAL handle AS LONG) AS LONG + DECLARE FUNCTION SecX25519End$(BYVAL handle AS LONG) AS BINARY STRING + DECLARE SUB SecX25519Free(BYVAL handle AS LONG) + ' ---- Cipher (XTEA in counter mode: encrypting and decrypting are the ' same call) @@ -100,6 +149,19 @@ DECLARE LIBRARY "serial" DECLARE SUB SecCipherFree(BYVAL handle AS LONG) + ' ---- Sealing (XChaCha20-Poly1305): encrypt and prove it unchanged + + ' bytes encrypted with key (SEC_AEAD_KEY_SIZE bytes) and nonce + ' (SEC_AEAD_NONCE_SIZE), followed by a SEC_AEAD_TAG_SIZE-byte tag + ' that also covers extra (sent in the clear, "" for none); "" for a + ' key or nonce of another length. + DECLARE FUNCTION SecAeadLock$(BYVAL key AS BINARY STRING, BYVAL nonce AS BINARY STRING, BYVAL extra AS BINARY STRING, BYVAL bytes AS BINARY STRING) AS BINARY STRING + + ' What SecAeadLock$ sealed, opened again: "" when anything differs + ' from the sealing (the key, nonce, extra, or one sealed byte). A + ' sealed message of no bytes also opens to "". + DECLARE FUNCTION SecAeadUnlock$(BYVAL key AS BINARY STRING, BYVAL nonce AS BINARY STRING, BYVAL extra AS BINARY STRING, BYVAL sealed AS BINARY STRING) AS BINARY STRING + ' ---- Hashes and signatures ' The SHA-256 hash of bytes (SEC_SHA256_SIZE bytes). @@ -125,6 +187,23 @@ DECLARE LIBRARY "serial" ' out of range. DECLARE FUNCTION SecHkdf$(BYVAL salt AS BINARY STRING, BYVAL secret AS BINARY STRING, BYVAL info AS BINARY STRING, BYVAL length AS LONG) AS BINARY STRING + ' Public-key signatures (Ed25519): whoever has the public key can + ' check that the secret key's owner signed exactly these bytes. + + ' A secret key (SEC_ED25519_SECRET_SIZE bytes) made from + ' SecRandom$(SEC_ED25519_SEED_SIZE); "" for a seed of another length. + ' Keep it private. + DECLARE FUNCTION SecEd25519KeyPair$(BYVAL seed AS BINARY STRING) AS BINARY STRING + + ' The public key of a secret key, to give out. + DECLARE FUNCTION SecEd25519PublicKey$(BYVAL secretKey AS BINARY STRING) AS BINARY STRING + + ' The signature of bytes (SEC_ED25519_SIG_SIZE bytes). + DECLARE FUNCTION SecEd25519Sign$(BYVAL secretKey AS BINARY STRING, BYVAL bytes AS BINARY STRING) AS BINARY STRING + + ' True when signature is publicKey's owner's signature of bytes. + DECLARE FUNCTION SecEd25519Check(BYVAL signature AS BINARY STRING, BYVAL publicKey AS BINARY STRING, BYVAL bytes AS BINARY STRING) AS LONG + ' True when a and b hold the same bytes. Takes the same time wherever ' they differ, so a signature check gives nothing away. DECLARE FUNCTION SecEqual(BYVAL a AS BINARY STRING, BYVAL b AS BINARY STRING) AS LONG diff --git a/src/include/basic/shell.bas b/src/include/basic/shell.bas index 02e0d602..f6a37b19 100644 --- a/src/include/basic/shell.bas +++ b/src/include/basic/shell.bas @@ -27,10 +27,11 @@ ' open documents, run command lines and the Run dialog, the installed ' programs with their names and descriptions, file types (which program ' opens which kind of document), the running programs (find one, bring -' it to the front, ask it to close, or end it), command lines split into -' their parts, this program's priority, keys that work in every program, -' Task Manager, system information, the desktop and the program started -' after it, Exit DVX, this program's settings folder, and the DVX log +' it to the front, ask it to close, or end it, and their priority), +' command lines split into their parts, this program's priority, keys +' that work in every program, Task Manager, system information, the +' desktop and the program started after it, Exit DVX, this program's +' settings folder, and the DVX log ' (DVX.LOG); and Program Manager's program groups (PROGMAN.INI) with ' their items, shortcut keys, and icons. ' Usage: Add this file to your project. @@ -87,6 +88,13 @@ DECLARE LIBRARY "dvxshell" ' can be read. DECLARE SUB ShellRunDos(BYVAL program AS STRING, BYVAL args AS STRING, BYVAL folder AS STRING, BYVAL pause AS LONG) + ' ShellRunDos, waiting for the DOS program to end, as SHELL does: this + ' program is not asked to close, and its events run meanwhile. The + ' DOS program's exit code (its ERRORLEVEL), or -1 when it did not run + ' (the other programs were kept open, it was not found, or it could + ' not start). + DECLARE FUNCTION ShellRunDosWait(BYVAL program AS STRING, BYVAL args AS STRING, BYVAL folder AS STRING, BYVAL pause AS LONG) AS LONG + ' Show the Run dialog. history is the commands to offer, newest ' first, one per line (separated by CHR$(10)). Returns the command ' line the user chose, or "" when the dialog was cancelled; run it @@ -211,6 +219,13 @@ DECLARE LIBRARY "dvxshell" ' when it was ended. DECLARE FUNCTION ShellRunningKill(BYVAL appId AS LONG) AS LONG + ' A running program's priority, as ShellPriority is this program's + ' (-1 when it has no task of its own: some built-in programs share + ' DVX's); ShellRunningSetPriority sets it, as Task Manager could. True + ' when it was set. + DECLARE FUNCTION ShellRunningPriority(BYVAL appId AS LONG) AS LONG + DECLARE FUNCTION ShellRunningSetPriority(BYVAL appId AS LONG, BYVAL priority AS LONG) AS LONG + ' True when a running program is not responding: it has been busy in ' one of its event procedures for 5 seconds or longer. DECLARE FUNCTION ShellAppIsHung(BYVAL appId AS LONG) AS LONG @@ -224,6 +239,18 @@ DECLARE LIBRARY "dvxshell" ' from the value you saw last. DECLARE FUNCTION ShellChangeCount() AS LONG + ' Say that what is installed may have changed: call it after your + ' program puts programs, fonts, sounds, or the like in DVX's folders + ' (installing a package does it by itself). Program Manager then + ' looks for new programs when it next comes to the front. + DECLARE SUB ShellInstalledChanged() + + ' A number that changes whenever what is installed may have changed + ' (a package installed or removed, a DOS program run, a file copy, + ' move, or delete finished, or ShellInstalledChanged): check it and + ' look again only when it differs from the value you saw last. + DECLARE FUNCTION ShellInstalledCount() AS LONG + ' Open Task Manager (what Ctrl+Esc does). DECLARE SUB ShellTaskManager() diff --git a/src/include/basic/sound.bas b/src/include/basic/sound.bas index 0b086c3c..62b46b0d 100644 --- a/src/include/basic/sound.bas +++ b/src/include/basic/sound.bas @@ -46,6 +46,17 @@ ' Voice number returned when a sound could not be played CONST SOUND_FAILED = 0 +' Why a voice ended (SoundEndReason): it played to its end, it was +' stopped, or the most voices were playing and it was the quietest +CONST SOUND_END_FINISHED = 0 +CONST SOUND_END_STOPPED = 1 +CONST SOUND_END_DROPPED = 2 + +' The waveforms of a note queue (NotesOpen, NotesSetTone) +CONST SOUND_WAVE_SQUARE = 0 +CONST SOUND_WAVE_TRIANGLE = 1 +CONST SOUND_WAVE_NOISE = 2 + ' DVX's event sounds, for SoundPlayEvent and the SoundEvent... functions CONST SOUND_EVENT_DEFAULT = 0 CONST SOUND_EVENT_START = 1 @@ -121,6 +132,11 @@ CONST FM_ADDITIVE = &H01 ' + register; bank 1 only on an OPL3) CONST FM_BANK_REGS = &H100 +' A sample in the wavetable synthesizer's General MIDI ROM (WtInfo$ +' "rom"): WtStart's address is WT_ADDR_ROM plus its bytes from the ROM's +' start +CONST WT_ADDR_ROM = &H80000000 + DECLARE LIBRARY "audio" ' Play a WAV or VOC file once. DECLARE FUNCTION PlayWave(BYVAL fileName AS STRING) AS LONG @@ -137,6 +153,14 @@ DECLARE LIBRARY "audio" ' True while the sound is still playing, else False. DECLARE FUNCTION SoundPlaying(BYVAL voice AS LONG) AS LONG + ' The next of this program's voices that ended since you last asked + ' (each once, in the order they ended; the last 64 are kept), 0 when + ' none has: call it from a Timer to hear of every sound ending. + ' SoundEndReason says why the voice SoundEnded gave last ended + ' (SOUND_END_...). + DECLARE FUNCTION SoundEnded() AS LONG + DECLARE FUNCTION SoundEndReason() AS LONG + ' Play a tone of frequency hertz (20 to 20000) for milliseconds (0: until ' StopWave) at volume percent. DECLARE FUNCTION PlayTone(BYVAL frequency AS LONG, BYVAL milliseconds AS LONG, BYVAL volume AS LONG) AS LONG @@ -152,6 +176,10 @@ DECLARE LIBRARY "audio" ' at 220h, 22050 Hz, 16-bit stereo", or "No sound device". DECLARE FUNCTION SoundInfo$() AS STRING + ' The short name of the sound driver playing ("sb", "speaker", ...; + ' "" when none is), the same names SoundDriverName$ lists. + DECLARE FUNCTION SoundDriver$() AS STRING + ' Why the last sound could not be played. DECLARE FUNCTION SoundError$() AS STRING @@ -169,16 +197,29 @@ DECLARE LIBRARY "audio" ' 2 ^ RECORD_... set for each) -- numbers. DECLARE FUNCTION SoundCaps(BYVAL field AS STRING) AS LONG + ' What the mixer has done since the device started, by name (any + ' case): "renders" (blocks of sound made), "frames" (samples made), + ' "underruns" (stream samples that were due but not yet written), + ' "clipped" (samples too loud), "dropped" (sounds the voice limit + ' dropped or refused), "playing" (voices playing now). + DECLARE FUNCTION SoundStats(BYVAL field AS STRING) AS LONG + ' Play one of DVX's event sounds (SOUND_EVENT_...), the one the - ' settings choose; with wait True, return once it has played. + ' settings choose; with wait True, return once it has played (the + ' program's events run meanwhile, as in DoEvents). DECLARE SUB SoundPlayEvent(BYVAL eventIdx AS LONG, BYVAL wait AS LONG) ' ---- Loaded sounds ---- ' Load a WAV or VOC file once, to play it as often as you like. - ' Returns a sound number, or 0 (SoundError$ says why). + ' Returns a sound number, or 0 (SoundError$ says why). The whole file + ' is read first: play a long recording with MusicLoad (music.bas). DECLARE FUNCTION SoundLoad(BYVAL fileName AS STRING) AS LONG + ' The same from the bytes of a WAV or VOC file, such as ResGetData$ + ' or HttpBody$ gives. + DECLARE FUNCTION SoundLoadData(BYVAL bytes AS BINARY STRING) AS LONG + ' Make a sound from raw sound data: 8-bit (0 to 255, 128 is silence) ' or 16-bit (two bytes each, low byte first) samples, mono or stereo ' (left, right, left, ...), at rate hertz. Returns a sound number, @@ -188,8 +229,12 @@ DECLARE LIBRARY "audio" ' Stop a sound's voices and forget it. DECLARE SUB SoundFree(BYVAL snd AS LONG) - ' How long the sound is (milliseconds). + ' How long the sound is (milliseconds), and how it was recorded: its + ' rate (hertz), bits (8 or 16), and channels (1 or 2). DECLARE FUNCTION SoundLength(BYVAL snd AS LONG) AS LONG + DECLARE FUNCTION SoundRate(BYVAL snd AS LONG) AS LONG + DECLARE FUNCTION SoundBits(BYVAL snd AS LONG) AS LONG + DECLARE FUNCTION SoundChannels(BYVAL snd AS LONG) AS LONG ' Play a sound on a new voice at volume percent and pan ' (SOUND_PAN_LEFT to SOUND_PAN_RIGHT), once or over and over. @@ -228,6 +273,11 @@ DECLARE LIBRARY "audio" ' SoundPause, and SoundResume. DECLARE FUNCTION SoundStreamVoice(BYVAL stream AS LONG) AS LONG + ' You will write no more: the stream's voice ends once it has played + ' what it holds (SoundEnded tells, as for a sound), with no gap + ' counted. Close it afterwards with SoundStreamClose. + DECLARE SUB SoundStreamEnd(BYVAL stream AS LONG) + ' Drop what the stream holds and has not played yet (to jump to ' another place in what you play: what you write next is heard at ' once). @@ -236,23 +286,60 @@ DECLARE LIBRARY "audio" ' Stop the stream and close it. DECLARE SUB SoundStreamClose(BYVAL stream AS LONG) + ' ---- Note queues: melodies timed exactly ---- + + ' Open a queue of tones in a waveform (SOUND_WAVE_...) at volume + ' percent. Tones and rests added to it play one after the other, + ' each lasting exactly as long as asked, as long as the next is added + ' before the one playing ends. Returns a queue number, or 0. + DECLARE FUNCTION NotesOpen(BYVAL wave AS LONG, BYVAL volume AS LONG) AS LONG + + ' Add a tone of hertz (20 to 20000; 0 for a rest) lasting + ' milliseconds; fractions count. True when it was added, False when + ' 256 are waiting (or the hertz are out of range). + DECLARE FUNCTION NotesAdd(BYVAL queue AS LONG, BYVAL hertz AS DOUBLE, BYVAL milliseconds AS DOUBLE) AS LONG + + ' The waveform and volume (percent of the queue's) of the tones added + ' from now on. True when it worked. + DECLARE FUNCTION NotesSetTone(BYVAL queue AS LONG, BYVAL wave AS LONG, BYVAL volume AS LONG) AS LONG + + ' Tones and rests not yet finished (0 once the queue has gone quiet), + ' and how many more can be added now. + DECLARE FUNCTION NotesQueued(BYVAL queue AS LONG) AS LONG + DECLARE FUNCTION NotesSpace(BYVAL queue AS LONG) AS LONG + + ' The queue's voice, for SoundSetVolume and SoundSetPan. + DECLARE FUNCTION NotesVoice(BYVAL queue AS LONG) AS LONG + + ' Silence the queue and forget what it holds; close it. + DECLARE SUB NotesClear(BYVAL queue AS LONG) + DECLARE SUB NotesClose(BYVAL queue AS LONG) + ' ---- Recording ---- ' ' Sound from the card's microphone, line, or CD input into a WAV - ' file. Most sound cards record or play, not both, so while a - ' program records nothing plays, and one program records at a time. + ' file, or into memory for the program to take as it comes. Most + ' sound cards record or play, not both, so while a program records + ' nothing plays, and one program records at a time. ' SoundCaps("record") says whether the card can. ' Start recording from input (RECORD_MIC, RECORD_LINE, RECORD_CD) - ' into the WAV file fileName (made, or replaced) at rate, bits (8 or - ' 16), and channels (1 or 2); 0 for 22,050 Hz, 16-bit, mono. The - ' card records at its nearest: RecordRate, RecordBits, and - ' RecordChannels say what it chose. True when it started; False with - ' SoundError$ saying why. + ' into the WAV file fileName (made, or replaced) -- or, with fileName + ' "", into memory for RecordRead$ -- at rate, bits (8 or 16), and + ' channels (1 or 2); 0 for 22,050 Hz, 16-bit, mono. The card records + ' at its nearest: RecordRate, RecordBits, and RecordChannels say what + ' it chose. True when it started; False with SoundError$ saying why. DECLARE FUNCTION RecordStart(BYVAL fileName AS STRING, BYVAL rate AS LONG, BYVAL bits AS LONG, BYVAL channels AS LONG, BYVAL inputSource AS LONG) AS LONG - ' Stop recording: the file is complete. True when it was all - ' written. + ' Recording into memory: up to maxBytes of what was recorded and not + ' yet taken (whole samples, in the recording's format, as SoundCreate + ' and SoundStreamWrite take them), "" when nothing is waiting; and how + ' many bytes are waiting. Take them often: about two seconds are + ' kept, and what does not fit is lost (RecordLost). + DECLARE FUNCTION RecordRead$(BYVAL maxBytes AS LONG) AS BINARY STRING + DECLARE FUNCTION RecordQueued() AS LONG + + ' Stop recording: a file is complete. True when it was all written. DECLARE FUNCTION RecordStop() AS LONG ' True while the program records. @@ -295,7 +382,8 @@ DECLARE LIBRARY "audio" ' The synthesizer by name (any case): "description" (like "Gravis ' UltraSound wavetable at 220h (26 voices, 1019 KB)", "" when there is ' none), "voices", "free" (voices no program has), "memory" (bytes of - ' sample memory), "freememory" (the largest free block), and what it + ' sample memory), "freememory" (the largest free block), "freetotal" + ' (the bytes free in all, in blocks maybe apart), and what it ' does itself: "envelopes", "lfos", "filter", "effects", "8bit", ' "rom" -- numbers and True / False as text. DECLARE FUNCTION WtInfo$(BYVAL field AS STRING) AS STRING @@ -309,12 +397,13 @@ DECLARE LIBRARY "audio" DECLARE SUB WtMemFree(BYVAL address AS LONG) ' Take a voice (0 when none is free) and start it on a sample in the - ' memory: from address, endFrame samples long, playing from startFrame - ' (0: its first), its loop from loopStart up to loopEnd (loopMode 0 - ' plays once, 1 loops, 3 loops until released), stored at bits; pitch in cents (0 plays 44,100 samples a - ' second, 1200 an octave higher), attenuation in centibels (0 loudest, - ' 60 half as loud, 1440 silent), pan -100 (left) to 100 (right). True - ' when it started. WtSetPitch, WtSetAttenuation and WtSetPan change + ' memory (or, from WT_ADDR_ROM on, in the ROM): from address, endFrame + ' samples long, playing from startFrame (0: its first), its loop from + ' loopStart up to loopEnd (loopMode 0 plays once, 1 loops, 3 loops + ' until released), stored at bits; pitch in cents (0 plays 44,100 + ' samples a second, 1200 an octave higher), attenuation in centibels + ' (0 loudest, 60 half as loud, 1440 silent), pan -100 (left) to 100 + ' (right), shaped as WtShape set. True when it started. WtSetPitch, WtSetAttenuation and WtSetPan change ' it, WtRelease lets a note looping until released play out, WtStop ' silences it, WtActive is True while it sounds, and WtFree gives the ' voice back. @@ -328,6 +417,28 @@ DECLARE LIBRARY "audio" DECLARE FUNCTION WtActive(BYVAL voice AS LONG) AS LONG DECLARE SUB WtFree(BYVAL voice AS LONG) + ' Shape a voice's notes, one value by name (any case), as a SoundFont + ' instrument does: kept for the voice's next WtStart, and changed at + ' once on a sounding note where the chip can (the filter, the + ' effects, the LFO depths). Used where the chip has them (WtInfo$ + ' "envelopes", "lfos", "filter", "effects"): + ' "voldelay", "volattack", "volhold", "voldecay", "volrelease": the + ' volume envelope's times, in timecents (1200 times the base-2 + ' logarithm of seconds; -12000 for none), "volsustain" its sustain + ' (centibels below the peak); "moddelay" to "modrelease" the + ' modulation envelope's (its sustain in tenths of a percent below + ' the peak), "modenvpitch" and "modenvfilter" how far it moves the + ' pitch and the filter (cents); "modlfodelay" (timecents), + ' "modlfofreq" (absolute cents: 8.176 Hz times 2 ^ (cents / 1200)), + ' "modlfopitch", "modlfofilter" (cents), and "modlfovolume" + ' (centibels): the modulation LFO; "viblfodelay", "viblfofreq", and + ' "viblfopitch": the vibrato LFO; "filterfc" (absolute cents, 13500 + ' open) and "filterq" (centibels): the low-pass filter; "reverb" and + ' "chorus" (tenths of a percent). A new voice's notes are plain: + ' no envelope or LFO, the filter open, no effects. True when it + ' worked. + DECLARE FUNCTION WtShape(BYVAL voice AS LONG, BYVAL field AS STRING, BYVAL value AS LONG) AS LONG + ' ---- DVX's own sounds and sound settings ---- ' How many events DVX plays a sound for (numbered 0 to diff --git a/src/include/basic/sql.bas b/src/include/basic/sql.bas index 697338ca..a4680de6 100644 --- a/src/include/basic/sql.bas +++ b/src/include/basic/sql.bas @@ -32,7 +32,9 @@ ' ' Usage: Add this file to your project. -DECLARE LIBRARY "basrt" +' The library named is the one these need (a program that includes this +' file needs the Databases part); BASIC finds the functions by name. +DECLARE LIBRARY "dvxsql" ' Open a database file. Returns a handle (>0), or 0 on failure. DECLARE FUNCTION SQLOpen(BYVAL path AS STRING) AS LONG @@ -77,4 +79,7 @@ DECLARE LIBRARY "basrt" ' Return the number of rows affected by the last INSERT/UPDATE/DELETE. DECLARE FUNCTION SQLAffected(BYVAL db AS LONG) AS LONG + + ' Return text with every single quote doubled, to put between quotes. + DECLARE FUNCTION SQLEscape$(BYVAL text AS STRING) AS STRING END DECLARE diff --git a/src/include/basic/system.bas b/src/include/basic/system.bas index ec2bc2c8..e97d7323 100644 --- a/src/include/basic/system.bas +++ b/src/include/basic/system.bas @@ -255,7 +255,8 @@ DECLARE LIBRARY "dvxshell" ' ---- Libraries ---- ' True when the library DECLARE LIBRARY names as libName ("pkgnet", - ' "dvxsql" ...; any case) is installed and was started with DVX. + ' "dvxsql" ...; any case) is installed. DVX loads a library when it is + ' first needed, so asking loads it. ' Some parts of DVX are optional (Setup's "What to install" page, and ' Software): a program using one of their libraries asks first, and ' leaves out what needs it, since calling a function of a library that diff --git a/src/include/basic/xfer.bas b/src/include/basic/xfer.bas index 75062300..5e1674c4 100644 --- a/src/include/basic/xfer.bas +++ b/src/include/basic/xfer.bas @@ -76,11 +76,25 @@ DECLARE LIBRARY "xfer" ' or 0 when the file cannot be opened or the port number is wrong. DECLARE FUNCTION XferSendFile(BYVAL com AS LONG, BYVAL proto AS LONG, BYVAL path AS STRING) AS LONG + ' Send several files as one batch: files has one path per line + ' (separate them with CHR$(10); up to XFER_MAX_FILES; XMODEM sends + ' only the first). escapeCtl <> 0 escapes every control character + ' (ZMODEM, for links that eat them); resumeFiles <> 0 asks a ZMODEM + ' receiver to continue files it already has part of. + DECLARE FUNCTION XferSendFiles(BYVAL com AS LONG, BYVAL proto AS LONG, BYVAL files AS STRING, BYVAL escapeCtl AS LONG, BYVAL resumeFiles AS LONG) AS LONG + ' Receive. XMODEM: target is the file to create. YMODEM and ZMODEM: - ' target is the folder the files go into (the sender names them). - ' Returns a handle, or 0 when the transfer cannot start. + ' target is the folder the files go into (the sender names them). A + ' file that is already there is kept and the new one stored under + ' another name. Returns a handle, or 0 when the transfer cannot start. DECLARE FUNCTION XferReceive(BYVAL com AS LONG, BYVAL proto AS LONG, BYVAL target AS STRING) AS LONG + ' Receive as XferReceive does, choosing what happens to a file that + ' is already there: exists is an XFER_EXIST_ action (XFER_EXIST_ASK + ' waits in XFER_ASKING for XferAnswer). escapeCtl <> 0 asks a ZMODEM + ' sender to escape every control character. + DECLARE FUNCTION XferReceiveFiles(BYVAL com AS LONG, BYVAL proto AS LONG, BYVAL target AS STRING, BYVAL exists AS LONG, BYVAL escapeCtl AS LONG) AS LONG + ' Move the transfer along. Returns XFER_RUNNING until it ends. DECLARE FUNCTION XferPoll(BYVAL handle AS LONG) AS LONG diff --git a/src/installer/Makefile b/src/installer/Makefile index 3d75978b..104b8138 100644 --- a/src/installer/Makefile +++ b/src/installer/Makefile @@ -47,7 +47,7 @@ KPUNCH = ../libs/kpunch WIDGETS = ../widgets/kpunch PKGLIB = $(KPUNCH)/pkg MINIZ = $(PKGLIB)/thirdparty/miniz -MONO = $(PKGLIB)/thirdparty/monocypher/src +MONO = $(KPUNCH)/crypto/thirdparty/monocypher/src LICENSE = ../../LICENSE.txt OBJDIR = ../../obj/installer @@ -79,7 +79,7 @@ LDFLAGS = -s -Wl,--gc-sections -lm # Setup's own sources; the core, fileops, and the text and list helpers # as they are -SETUP_SRCS = setup.c setupCore.c pkgRead.c $(PKGLIB)/pkgDb.c $(PKGLIB)/pkgDpk.c $(PKGLIB)/pkgFormat.c $(PKGLIB)/pkgIni.c $(PKGLIB)/pkgManifest.c $(PKGLIB)/pkgOp.c $(PKGLIB)/pkgPlan.c $(PKGLIB)/pkgSign.c $(PKGLIB)/pkgTrust.c $(PKGLIB)/pkgZip.c $(MINIZ)/miniz.c $(MONO)/monocypher.c $(MONO)/optional/monocypher-ed25519.c $(KPUNCH)/serial/security/secHash.c $(KPUNCH)/serial/security/security.c +SETUP_SRCS = setup.c setupCore.c pkgRead.c $(PKGLIB)/pkgDb.c $(PKGLIB)/pkgDpk.c $(PKGLIB)/pkgFormat.c $(PKGLIB)/pkgIni.c $(PKGLIB)/pkgManifest.c $(PKGLIB)/pkgOp.c $(PKGLIB)/pkgPlan.c $(PKGLIB)/pkgSign.c $(PKGLIB)/pkgTrust.c $(PKGLIB)/pkgZip.c $(MINIZ)/miniz.c $(MONO)/monocypher.c $(MONO)/optional/monocypher-ed25519.c $(KPUNCH)/crypto/secHash.c $(KPUNCH)/crypto/secSign.c $(KPUNCH)/crypto/security.c CORE_SRCS = $(wildcard $(LIBDVX)/*.c) $(LIBDVX)/platform/dvxPlatformDos.c $(LIBDVX)/platform/dvxPlatformIrq.c $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/platform/dvxMemTrack.c $(LIBDVX)/thirdparty/stb_ds_impl.c ../tools/dvxResWrite.c $(wildcard $(KPUNCH)/fileops/*.c) $(KPUNCH)/texthelp/textHelp.c $(KPUNCH)/listhelp/listHelp.c # The widgets Setup uses: /: (wgtRegister -> wgtRegister); @@ -93,11 +93,11 @@ WGT_OBJS = $(foreach w,$(SETUP_WGTS),$(DOSOBJ)/wgt/$(notdir $(firstword $(subs CORE_LIB = $(DOSOBJ)/setupCore.a # The package tool (host) -TOOL_SRCS = pkgTool.c pkgBuild.c $(PKGLIB)/pkgFormat.c $(PKGLIB)/pkgCli.c $(PKGLIB)/pkgDb.c $(PKGLIB)/pkgDpk.c $(PKGLIB)/pkgIni.c $(PKGLIB)/pkgManifest.c $(PKGLIB)/pkgOp.c $(PKGLIB)/pkgPlan.c $(PKGLIB)/pkgSign.c $(PKGLIB)/pkgTrust.c $(PKGLIB)/pkgZip.c $(PKGLIB)/pkgZipWrite.c $(MINIZ)/miniz.c $(MONO)/monocypher.c $(MONO)/optional/monocypher-ed25519.c $(KPUNCH)/serial/security/secHash.c $(KPUNCH)/serial/security/security.c $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/thirdparty/stb_ds_impl.c +TOOL_SRCS = pkgTool.c pkgBuild.c $(PKGLIB)/pkgFormat.c $(PKGLIB)/pkgCli.c $(PKGLIB)/pkgDb.c $(PKGLIB)/pkgDpk.c $(PKGLIB)/pkgIni.c $(PKGLIB)/pkgManifest.c $(PKGLIB)/pkgOp.c $(PKGLIB)/pkgPlan.c $(PKGLIB)/pkgSign.c $(PKGLIB)/pkgSignMake.c $(PKGLIB)/pkgTrust.c $(PKGLIB)/pkgZip.c $(PKGLIB)/pkgZipWrite.c $(MINIZ)/miniz.c $(MONO)/monocypher.c $(MONO)/optional/monocypher-ed25519.c $(KPUNCH)/crypto/secHash.c $(KPUNCH)/crypto/secSign.c $(KPUNCH)/crypto/security.c $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/thirdparty/stb_ds_impl.c TOOL_OBJS = $(patsubst %.c,$(HOSTOBJ)/%.o,$(notdir $(TOOL_SRCS))) TOOL_FLAGS = $(HOST_TOOL_CFLAGS) -I. -I$(PKGLIB) -I$(MINIZ) -I$(MONO) -I$(KPUNCH) -I$(LIBDVX) -I$(LIBDVX)/platform -I$(LIBDVX)/thirdparty -vpath %.c . $(PKGLIB) $(MINIZ) $(MONO) $(MONO)/optional $(KPUNCH)/serial/security $(LIBDVX)/platform $(LIBDVX)/thirdparty +vpath %.c . $(PKGLIB) $(MINIZ) $(MONO) $(MONO)/optional $(KPUNCH)/crypto $(LIBDVX)/platform $(LIBDVX)/thirdparty .PHONY: all clean diff --git a/src/installer/pkgBuild.c b/src/installer/pkgBuild.c index 7e310f02..e27f55d4 100644 --- a/src/installer/pkgBuild.c +++ b/src/installer/pkgBuild.c @@ -37,8 +37,12 @@ // Names found under the package folder, relative, '/'-separated; folders // end with '/' +// The names a folder's tree gives a ZIP, and where each is on disk +// (relative to the folder: a folder only case apart from one listed +// before goes under that one's name, see scanFolder) typedef struct { char **names; + char **paths; int32_t count; int32_t cap; } NameListT; @@ -46,6 +50,7 @@ typedef struct { // A name and the tier the rules put it in, for the ZIP's order typedef struct { const char *name; + const char *path; int32_t tier; } TieredNameT; @@ -53,18 +58,31 @@ typedef struct { // Prototypes // ============================================================ -static int byTierName(const void *a, const void *b); -static int32_t entryTier(const mz_zip_archive_file_stat *st); -static bool listAdd(NameListT *list, const char *name); -static void listFree(NameListT *list); -static bool loadFile(const char *path, uint8_t **data, uint32_t *size); -bool pkgBuildAppended(const char *exePath, const char *zipPath, const char *outPath, char *err, int32_t errSize); -bool pkgBuildInfo(const char *zipPath, PkgHeaderT *info, char *err, int32_t errSize); -int32_t pkgBuildSplit(const char *zipPath, const char *outDir, uint32_t firstRoom, uint32_t diskRoom, char *err, int32_t errSize); -bool pkgBuildZip(const char *folder, const char *zipPath, const PkgTierRuleT *rules, int32_t ruleCount, char *err, int32_t errSize); -static bool scanFolder(const char *root, const char *rel, NameListT *list, char *err, int32_t errSize); -static int32_t tierOf(const char *name, const PkgTierRuleT *rules, int32_t ruleCount); -static bool writePart(const char *path, const uint8_t *data, uint32_t len, const PkgHeaderT *h, const uint8_t *prefix, uint32_t prefixLen); +static int byDosName(const void *a, const void *b); +static int byTierName(const void *a, const void *b); +static int32_t entryTier(const mz_zip_archive_file_stat *st); +static bool listAdd(NameListT *list, const char *name, const char *path); +static const char *listFindCase(const NameListT *list, const char *name); +static void listFree(NameListT *list); +static bool loadFile(const char *path, uint8_t **data, uint32_t *size); +bool pkgBuildAppended(const char *exePath, const char *zipPath, const char *outPath, char *err, int32_t errSize); +bool pkgBuildInfo(const char *zipPath, PkgHeaderT *info, char *err, int32_t errSize); +int32_t pkgBuildSplit(const char *zipPath, const char *outDir, uint32_t firstRoom, uint32_t diskRoom, char *err, int32_t errSize); +bool pkgBuildZip(const char *folder, const char *zipPath, const PkgTierRuleT *rules, int32_t ruleCount, char *err, int32_t errSize); +static bool scanFolder(const char *root, const char *rel, const char *diskRel, NameListT *list, char *err, int32_t errSize); +static int32_t tierOf(const char *name, const PkgTierRuleT *rules, int32_t ruleCount); +static bool writePart(const char *path, const uint8_t *data, uint32_t len, const PkgHeaderT *h, const uint8_t *prefix, uint32_t prefixLen); + + +// Names as DOS compares them (case ignored), a lowercase spelling before +// an uppercase one (scanFolder: the release tree's own spelling wins). +static int byDosName(const void *a, const void *b) { + const char *x = *(const char *const *)a; + const char *y = *(const char *const *)b; + int c = strcasecmp(x, y); + + return c ? c : -strcmp(x, y); +} static int byTierName(const void *a, const void *b) { @@ -83,7 +101,7 @@ static int32_t entryTier(const mz_zip_archive_file_stat *st) { } -static bool listAdd(NameListT *list, const char *name) { +static bool listAdd(NameListT *list, const char *name, const char *path) { if (list->count == list->cap) { int32_t cap = list->cap + LIST_GROW; char **names = (char **)realloc(list->names, (size_t)cap * sizeof(char *)); @@ -93,12 +111,23 @@ static bool listAdd(NameListT *list, const char *name) { } list->names = names; + + char **paths = (char **)realloc(list->paths, (size_t)cap * sizeof(char *)); + + if (!paths) { + return false; + } + + list->paths = paths; list->cap = cap; } list->names[list->count] = strdup(name); + list->paths[list->count] = strdup(path); - if (!list->names[list->count]) { + if (!list->names[list->count] || !list->paths[list->count]) { + free(list->names[list->count]); + free(list->paths[list->count]); return false; } @@ -107,12 +136,26 @@ static bool listAdd(NameListT *list, const char *name) { } +// The name listed that is name but for case (DOS's same name), or NULL. +static const char *listFindCase(const NameListT *list, const char *name) { + for (int32_t i = 0; i < list->count; i++) { + if (strcasecmp(list->names[i], name) == 0) { + return list->names[i]; + } + } + + return NULL; +} + + static void listFree(NameListT *list) { for (int32_t i = 0; i < list->count; i++) { free(list->names[i]); + free(list->paths[i]); } free(list->names); + free(list->paths); memset(list, 0, sizeof(*list)); } @@ -330,7 +373,7 @@ bool pkgBuildZip(const char *folder, const char *zipPath, const PkgTierRuleT *ru memset(&list, 0, sizeof(list)); memset(&zip, 0, sizeof(zip)); - if (!scanFolder(folder, "", &list, err, errSize)) { + if (!scanFolder(folder, "", "", &list, err, errSize)) { listFree(&list); return false; } @@ -347,6 +390,7 @@ bool pkgBuildZip(const char *folder, const char *zipPath, const PkgTierRuleT *ru for (int32_t i = 0; i < list.count; i++) { order[i].name = list.names[i]; + order[i].path = list.paths[i]; order[i].tier = tierOf(list.names[i], rules, ruleCount); } @@ -368,7 +412,7 @@ bool pkgBuildZip(const char *folder, const char *zipPath, const PkgTierRuleT *ru } else { char full[PATH_MAX_LEN]; - snprintf(full, sizeof(full), "%s/%s", folder, name); + snprintf(full, sizeof(full), "%s/%s", folder, order[i].path); ok = mz_zip_writer_add_file(&zip, name, full, &comment, commentLen, MZ_UBER_COMPRESSION | MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE | MZ_ZIP_FLAG_ASCII_FILENAME); } @@ -394,11 +438,17 @@ bool pkgBuildZip(const char *folder, const char *zipPath, const PkgTierRuleT *ru } -// Add every file and folder under root/rel (rel "" or ending in '/'). -static bool scanFolder(const char *root, const char *rel, NameListT *list, char *err, int32_t errSize) { +// Add every file and folder under root/diskRel, named rel in the ZIP (both +// "" or ending in '/'), in DOS name order (byDosName). A folder only case +// apart from one listed already is that folder on DOS (SYSTEM beside +// system: the package tool writes DVX's own spelling into a tree copied +// from Linux): what it holds goes under the spelling listed (the +// lowercase one when there is one). Two files only case apart are +// refused, as DOS cannot hold both. +static bool scanFolder(const char *root, const char *rel, const char *diskRel, NameListT *list, char *err, int32_t errSize) { char dirPath[PATH_MAX_LEN]; - snprintf(dirPath, sizeof(dirPath), "%s/%s", root, rel); + snprintf(dirPath, sizeof(dirPath), "%s/%s", root, diskRel); DIR *d = opendir(dirPath); @@ -407,40 +457,63 @@ static bool scanFolder(const char *root, const char *rel, NameListT *list, char return false; } + NameListT entries; bool ok = true; struct dirent *e; + memset(&entries, 0, sizeof(entries)); + while (ok && (e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") != 0 && strcmp(e->d_name, "..") != 0) { + ok = listAdd(&entries, e->d_name, e->d_name); + } + } + + closedir(d); + + if (!ok) { + snprintf(err, errSize, "out of memory reading %s", dirPath); + } else if (entries.count > 0) { + qsort(entries.names, (size_t)entries.count, sizeof(char *), byDosName); + } + + for (int32_t i = 0; ok && i < entries.count; i++) { + const char *leaf = entries.names[i]; char name[PKG_NAME_MAX]; + char path[PATH_MAX_LEN]; char full[PATH_MAX_LEN]; struct stat st; - if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) { - continue; - } - - if (snprintf(name, sizeof(name), "%s%s", rel, e->d_name) >= (int)sizeof(name) || !pkgNameIsValid(name)) { - snprintf(err, errSize, "%s%s is not a DOS 8.3 name", rel, e->d_name); + if (snprintf(name, sizeof(name), "%s%s", rel, leaf) >= (int)sizeof(name) || !pkgNameIsValid(name)) { + snprintf(err, errSize, "%s%s is not a DOS 8.3 name", rel, leaf); ok = false; break; } - snprintf(full, sizeof(full), "%s/%s", root, name); + snprintf(path, sizeof(path), "%s%s", diskRel, leaf); + snprintf(full, sizeof(full), "%s/%s", root, path); if (stat(full, &st) != 0) { snprintf(err, errSize, "cannot read %s", full); ok = false; } else if (S_ISDIR(st.st_mode)) { - char sub[PKG_NAME_MAX]; + char sub[PKG_NAME_MAX]; + char diskSub[PATH_MAX_LEN]; + const char *same; snprintf(sub, sizeof(sub), "%s%c", name, PKG_NAME_SEP); - ok = listAdd(list, sub) && scanFolder(root, sub, list, err, errSize); + snprintf(diskSub, sizeof(diskSub), "%s%c", path, PKG_NAME_SEP); + same = listFindCase(list, sub); + ok = (same || listAdd(list, sub, diskSub)) && scanFolder(root, same ? same : sub, diskSub, list, err, errSize); + } else if (S_ISREG(st.st_mode) && listFindCase(list, name)) { + snprintf(err, errSize, "%s and %s differ only in case: DOS cannot hold both", listFindCase(list, name), path); + ok = false; } else if (S_ISREG(st.st_mode)) { - ok = listAdd(list, name); + ok = listAdd(list, name, path); } } - closedir(d); + listFree(&entries); return ok; } diff --git a/src/installer/setup.c b/src/installer/setup.c index 9814436c..b95ebacb 100644 --- a/src/installer/setup.c +++ b/src/installer/setup.c @@ -1437,23 +1437,31 @@ static void onTile(WidgetT *w) { // Step op (status says what it does) in slices like the unpacking, until -// it is done or has failed, or Cancel stops it (PkgOpRunningE). +// it is done or has failed. Cancel stops it (PkgOpRunningE): at once +// while it unpacks; once its files are in, after the rest it must do, +// still a slice at a time (then it is done). static PkgOpStateE opRun(PkgOpT *op, const char *status) { - SliceT slice = { platformClock(), platformClock() }; + SliceT slice = { platformClock(), platformClock() }; + bool stopAsked = false; PkgOpStateE state; wgtSetText(sSetup.status, status); - for (state = pkgOpStep(op); state == PkgOpRunningE && !sSetup.cancelRequested; state = pkgOpStep(op)) { + for (state = pkgOpStep(op); state == PkgOpRunningE; state = pkgOpStep(op)) { uint32_t done; uint32_t total; + if (sSetup.cancelRequested && !stopAsked) { + pkgOpStop(op); + stopAsked = true; + } + pkgOpProgress(op, &done, &total); wgtProgressBarSetValue(sSetup.bar, total ? (int32_t)((uint64_t)done * SETUP_PERCENT / total) : SETUP_PERCENT); sliceIdle(&slice); } - return state; + return (stopAsked && state == PkgOpFailedE) ? PkgOpRunningE : state; } diff --git a/src/installer/setupCore.c b/src/installer/setupCore.c index 9426bfeb..3355c68e 100644 --- a/src/installer/setupCore.c +++ b/src/installer/setupCore.c @@ -420,7 +420,9 @@ bool setupAddonOp(PkgDbT *db, const PkgKeyringT *ring, const SetupAddonT *addon, *op = NULL; if (chosen) { - *op = pkgOpInstall(db, addon->offer.where, ring, true); + // Trusted as the installed DVX's keys say: it was found before + // they were in + *op = pkgOpInstallFile(db, addon->offer.where, ring, true); return true; } diff --git a/src/libs/kpunch/audio/README.md b/src/libs/kpunch/audio/README.md new file mode 100644 index 00000000..010fc9a6 --- /dev/null +++ b/src/libs/kpunch/audio/README.md @@ -0,0 +1,39 @@ +# audio -- Sound + +Sound for DVX programs on whatever sound hardware the computer has: +samples from WAV and VOC files or memory, tones, streams, note queues, +wavetable voices, recording, and MIDI input, all through one mixer +shared by every program; what a program opened is stopped and released +when it ends. BASIC reaches it through SOUND / BEEP and +src/include/basic/sound.bas. + +## Documentation + +Full reference is in `audio.dhs` (this directory), compiled into the DVX +System Reference. Open via the Help Viewer app, or read +`docs/dvx_system_reference.html` after `make`. + +## Source Files + +- `audio.h`, `audioInt.h`, `audioDrv.h` -- public, internal, and driver + API +- `audio.c` -- contexts, voices, samples, streams, and the sound device +- `audioMix.c` -- the mixer: everything audio.lib runs at interrupt time +- `audioLoad.c` -- WAV and VOC loading +- `audioSynth.c` -- FM voices shared by every program, the whole FM chip + for one, the MIDI port, tones on FM +- `audioWt.c` -- the wavetable synthesizer: voices and sample memory + shared by every program +- `audioRec.c` -- recording +- `audioMidiIn.c` -- MIDI input +- `audioSet.c` -- the [audio] settings in DVX.INI +- `audioDesk.c` -- desktop sounds: what DVX plays for its own events +- `audioShell.c` -- audio.lib's hooks into the shell +- `audioBas.c` -- BASIC entry points (sound.bas) +- `drivers/` -- the sound card driver modules (sb, wss, gus, guswt, awe, + opl, mpu401, lpt, speaker, null) +- `codec/`, `gus/`, `isa/`, `math/` -- pieces the drivers share + +## Build + +`make -C src/libs/kpunch/audio` (invoked by the top-level `make`). diff --git a/src/libs/kpunch/audio/audio.c b/src/libs/kpunch/audio/audio.c index ff6ca260..90065d03 100644 --- a/src/libs/kpunch/audio/audio.c +++ b/src/libs/kpunch/audio/audio.c @@ -34,11 +34,16 @@ // The device is opened when the first context opens and closed with the // last one. In between, its output is closed after IDLE_CLOSE_US without // a voice (a desktop with sound effects would otherwise mix silence all -// the time) and opened again, with the same driver, when a voice starts. Drivers are the driver modules in AUDIO_DRV_DIR (dvxDrv.h); -// they are loaded, sorted by priority and tried in turn (DVX.INI [audio] -// driver first), and all but the one that starts are unloaded again. Synth +// the time) and opened again, with the same driver, when a voice starts. +// Drivers are the driver modules in AUDIO_DRV_DIR (dvxDrv.h); they are +// loaded, sorted by priority and tried in turn (DVX.INI [audio] driver +// first), and all but the one that starts are unloaded again. Synth // drivers (an FM chip, a MIDI port) come from the same folder: the first -// of each kind that finds its hardware is kept beside the device. +// of each kind that finds its hardware is kept beside the device. The +// modules of a device that closed stay loaded (sParked): the next device +// tries them first while DVX.INI makes the same driver choice, instead of +// loading and probing every driver again. The drivers' names are read +// once. #include "audioInt.h" #include "stb_ds.h" @@ -68,6 +73,13 @@ DVX_ABI_STAMP(audio); // Frames a pull stream is asked for at a time #define STREAM_FILL_FRAMES 1024 +// A pump fills pull streams for STREAM_FILL_BUDGET_US at most once each +// holds STREAM_SAFE_MS of sound (the next pump fills the rest): a burst of +// a stream's whole ring after a start, a seek or a stall would take the +// CPU for a few hundred milliseconds +#define STREAM_FILL_BUDGET_US 15000 +#define STREAM_SAFE_MS 100 + #define MS_PER_SEC 1000 #define BITS_PER_BYTE 8 #define BITS_8 8 @@ -99,6 +111,8 @@ struct AudioNotesT { uint64_t usQueued; // time queued since the queue last went quiet ... uint64_t framesQueued; // ... and the frames it was rounded to int32_t voice; + int32_t level; // the next entries' share of the volume (audioNotesSetTone) + uint8_t wave; // ... and their waveform }; struct AudioSampleT { @@ -155,8 +169,18 @@ typedef struct { bool suspended; // output closed for silence (the driver stays chosen) bool fmTones; // square / triangle tones on FM voices bool recording; // the card records (audioDevRecordBegin): no output, no new voice + char want[AUDIO_NAME_MAX]; // DVX.INI's driver choice it opened under } AudioDevT; +// The modules of the last device, kept loaded after it closed (devClose): +// the DVX.INI driver choice it opened under, its sound driver, and its +// synth drivers (desc NULL: none). +typedef struct { + char want[AUDIO_NAME_MAX]; + DvxDriverModT sound; + DvxDriverModT synths[AUDIO_SYNTH_KINDS]; +} ParkedT; + static const char *sErrorText[AUDIO_ERR_COUNT] = { "No error", "Invalid argument", @@ -176,6 +200,7 @@ static const char *sErrorText[AUDIO_ERR_COUNT] = { }; static AudioDevT sDev; +static ParkedT sParked; static AudioCtxT *sCtxs = NULL; static AudioSampleT *sSamples = NULL; static AudioStreamT *sStreams = NULL; @@ -189,15 +214,19 @@ static uint32_t sDropped = 0; static bool sPumping = false; static bool sStreamsDirty = false; +// The sound drivers' names (audioDriverNames), read once: -1 until then +static char sDrvNames[AUDIO_DRIVERS_MAX][AUDIO_NAME_MAX]; +static int32_t sDrvNameCount = -1; + // ============================================================ // Prototypes // ============================================================ static int32_t appCurrent(void); void audioAppEnded(int32_t appId); -int32_t audioAppEnter(int32_t appId); AudioCtxT *audioBasicContext(void); void audioClose(AudioCtxT *ctx); +void audioDevForget(void); int32_t audioDevRecordBegin(int32_t source, const AudioDriverT **drv); void audioDevRecordEnd(void); int32_t audioDriverNames(char names[][AUDIO_NAME_MAX], int32_t max); @@ -212,6 +241,7 @@ void audioNotesClear(AudioNotesT *notes); void audioNotesClose(AudioNotesT *notes); int32_t audioNotesOpen(AudioCtxT *ctx, AudioWaveE wave, int32_t volume, AudioNotesT **out); uint32_t audioNotesQueued(const AudioNotesT *notes, uint32_t *tones); +int32_t audioNotesSetTone(AudioNotesT *notes, AudioWaveE wave, int32_t level); uint32_t audioNotesSpace(const AudioNotesT *notes); int32_t audioNotesVoice(const AudioNotesT *notes); AudioCtxT *audioOpen(void); @@ -229,6 +259,7 @@ void audioSetVolume(AudioCtxT *ctx, int32_t volume); void audioShutdown(void); void audioStopAll(AudioCtxT *ctx); void audioStreamClose(AudioStreamT *stream); +void audioStreamEnd(AudioStreamT *stream); void audioStreamFlush(AudioStreamT *stream); int32_t audioStreamOpen(AudioCtxT *ctx, const AudioFormatT *fmt, uint32_t bufferMs, AudioFillFnT fill, void *userData, AudioStreamT **out); uint32_t audioStreamQueued(const AudioStreamT *stream); @@ -252,6 +283,7 @@ static void devConfig(PrefsHandleT *prefs, AudioDrvConfigT * static void devEmergencyStop(void); static void devIdle(void); static bool devOpen(void); +static bool devOpenParked(const char *want, const AudioDrvConfigT *cfg); static void devOpenSynths(DvxDriverModT *mods, const AudioDrvConfigT *cfg); static bool devResume(void); static bool devTry(DvxDriverModT *mods, int32_t index, const AudioDrvConfigT *cfg); @@ -262,11 +294,12 @@ static void gainsUpdate(int32_t slot); static bool infoUsable(const AudioDrvInfoT *info); static int32_t lookupVoice(const AudioCtxT *ctx, int32_t voice); static int32_t makeSample(AudioCtxT *ctx, int16_t *pcm, uint32_t frames, const AudioFormatT *fmt, AudioSampleT **out); +static void namesNote(DvxDriverModT *mods); static void reclaimSlots(void); static void releaseSlot(int32_t slot); static const AudioDriverT *soundOf(const DvxDriverModT *mod); static void stepUpdate(int32_t slot); -static void streamFill(AudioStreamT *s); +static void streamFill(AudioStreamT *s, uint64_t until); static const AudioSynthDriverT *synthOf(const DvxDriverModT *mod); static bool synthTry(const AudioSynthDriverT *syn, const AudioDrvConfigT *cfg); static int32_t voiceBegin(AudioCtxT *owner, int32_t volume, int32_t pan, uint8_t kind); @@ -300,19 +333,6 @@ void audioAppEnded(int32_t appId) { } -// Make appId the current program and return the one before (for calling -// a program's callback as that program). -int32_t audioAppEnter(int32_t appId) { - int32_t prev = appCurrent(); - - if (dvxMemAppIdPtr) { - *dvxMemAppIdPtr = appId; - } - - return prev; -} - - // The calling BASIC program's context, opened on first use. AudioCtxT *audioBasicContext(void) { int32_t appId = appCurrent(); @@ -327,6 +347,7 @@ AudioCtxT *audioBasicContext(void) { if (ctx) { ctx->basic = true; + audioSetEndFn(ctx, audioBasEnded, NULL); } return ctx; @@ -414,6 +435,17 @@ void audioClose(AudioCtxT *ctx) { } +void audioDevForget(void) { + dvxDriverUnload(&sParked.sound); + + for (int32_t k = 0; k < AUDIO_SYNTH_KINDS; k++) { + dvxDriverUnload(&sParked.synths[k]); + } + + memset(&sParked, 0, sizeof(sParked)); +} + + // The card for recording from source: its output closed (as the silence // timeout does) and new voices refused until audioDevRecordEnd. int32_t audioDevRecordBegin(int32_t source, const AudioDriverT **drv) { @@ -454,17 +486,22 @@ void audioDevRecordEnd(void) { } +// Read once (devOpen's full load notes them as well): a driver installed +// meanwhile is used from the next start anyway. int32_t audioDriverNames(char names[][AUDIO_NAME_MAX], int32_t max) { - DvxDriverModT *mods = dvxDriversLoad(AUDIO_DRV_DIR, &sDrvClass); - int32_t count = 0; + if (sDrvNameCount < 0) { + DvxDriverModT *mods = dvxDriversLoad(AUDIO_DRV_DIR, &sDrvClass); - for (int32_t i = 0; i < (int32_t)arrlen(mods) && count < max; i++) { - if (soundOf(&mods[i])) { - snprintf(names[count++], AUDIO_NAME_MAX, "%s", mods[i].desc->name); - } + namesNote(mods); + dvxDriversFree(mods); + } + + int32_t count = DVX_MIN(max, sDrvNameCount); + + for (int32_t i = 0; i < count; i++) { + snprintf(names[i], AUDIO_NAME_MAX, "%s", sDrvNames[i]); } - dvxDriversFree(mods); return count; } @@ -558,9 +595,12 @@ int32_t audioNotesAdd(AudioNotesT *notes, uint32_t milliHz, uint32_t us) { uint64_t end = (notes->usQueued + us) * rate / PLATFORM_US_PER_SEC; MixNoteT *e = &q->data[q->head & q->mask]; - e->hz = (mhz + AUDIO_MILLIHZ_PER_HZ / 2) / AUDIO_MILLIHZ_PER_HZ; + // A silent tone is a rest for a tone generator + e->hz = notes->level > 0 ? (mhz + AUDIO_MILLIHZ_PER_HZ / 2) / AUDIO_MILLIHZ_PER_HZ : 0; e->phaseStep = (uint32_t)(((uint64_t)mhz << PHASE_SHIFT) / ((uint64_t)rate * AUDIO_MILLIHZ_PER_HZ)); e->frames = (uint32_t)(end - notes->framesQueued); + e->level = (uint16_t)notes->level; + e->wave = notes->wave; notes->usQueued += us; notes->framesQueued = end; __atomic_signal_fence(__ATOMIC_RELEASE); @@ -635,6 +675,8 @@ int32_t audioNotesOpen(AudioCtxT *ctx, AudioWaveE wave, int32_t volume, AudioNot n->ring.data = n->data; n->ring.mask = AUDIO_NOTES_MAX - 1; n->owner = ctx; + n->level = AUDIO_VOLUME_MAX; + n->wave = (uint8_t)wave; n->next = sNoteQueues; sNoteQueues = n; @@ -679,6 +721,17 @@ uint32_t audioNotesQueued(const AudioNotesT *notes, uint32_t *tones) { } +int32_t audioNotesSetTone(AudioNotesT *notes, AudioWaveE wave, int32_t level) { + if (!notes || (wave != AUDIO_WAVE_SQUARE && wave != AUDIO_WAVE_TRIANGLE && wave != AUDIO_WAVE_NOISE) || level < 0 || level > AUDIO_VOLUME_MAX) { + return AUDIO_ERR_ARG; + } + + notes->wave = (uint8_t)wave; + notes->level = level; + return AUDIO_OK; +} + + uint32_t audioNotesSpace(const AudioNotesT *notes) { return notes->ring.mask + 1 - (notes->ring.head - notes->ring.tail); } @@ -767,15 +820,16 @@ void audioPump(void) { deliverNotes(); audioRecIdle(); - AudioStreamT *s = sStreams; + AudioStreamT *s = sStreams; + uint64_t until = platformMicros() + STREAM_FILL_BUDGET_US; while (s) { AudioStreamT *next = s->next; sStreamsDirty = false; - if (s->fill) { - streamFill(s); + if (s->fill && !s->ring.ended) { + streamFill(s, until); } s = sStreamsDirty ? sStreams : next; @@ -911,6 +965,7 @@ void audioShutdown(void) { audioClose(sCtxs); } + audioDevForget(); arrfree(sNotes); sNotes = NULL; } @@ -961,6 +1016,13 @@ void audioStreamClose(AudioStreamT *stream) { // With interrupts off: the mixer moves the tail. +void audioStreamEnd(AudioStreamT *stream) { + if (stream) { + stream->ring.ended = true; + } +} + + void audioStreamFlush(AudioStreamT *stream) { if (!stream) { return; @@ -1125,7 +1187,7 @@ void audioSuspend(bool suspending) { return; } - PrefsHandleT *prefs = prefsLoad(platformDvxIniPath()); + PrefsHandleT *prefs = prefsDvxIniShared(); AudioDrvConfigT cfg; devConfig(prefs, &cfg); @@ -1140,7 +1202,6 @@ void audioSuspend(bool suspending) { } } - prefsClose(prefs); } @@ -1301,10 +1362,10 @@ static void deliverNotes(void) { } if (live && ctx->endFn) { - int32_t prev = audioAppEnter(ctx->appId); + int32_t prev = dvxMemSetApp(ctx->appId); ctx->endFn(ctx, notes[i].voice, notes[i].reason, ctx->endData); - audioAppEnter(prev); + dvxMemSetApp(prev); } } @@ -1312,6 +1373,7 @@ static void deliverNotes(void) { } +// The modules stay loaded (sParked) for the next device to try first. static void devClose(void) { if (!sDev.mod.desc) { return; @@ -1325,13 +1387,16 @@ static void devClose(void) { } audioMixLock(false); - dvxDriverUnload(&sDev.mod); + snprintf(sParked.want, sizeof(sParked.want), "%s", sDev.want); + sParked.sound = sDev.mod; memset(&sDev, 0, sizeof(sDev)); dvxLog("Audio: sound device closed"); } -// Every context is closed, so no program holds a voice or the port. +// Every context is closed, so no program holds a voice or the port. The +// synth modules are parked with the device's (the card's own synthesizer +// has none). static void devCloseSynths(void) { audioFmDetach(); audioMidiDetach(); @@ -1342,7 +1407,10 @@ static void devCloseSynths(void) { for (int32_t k = 0; k < AUDIO_SYNTH_KINDS; k++) { if (sDev.synths[k].desc) { synthOf(&sDev.synths[k])->close(); - dvxDriverUnload(&sDev.synths[k]); + } + + if (sDev.synths[k].handle) { + sParked.synths[k] = sDev.synths[k]; } } } @@ -1415,7 +1483,7 @@ static void devIdle(void) { static bool devOpen(void) { - PrefsHandleT *prefs = prefsLoad(platformDvxIniPath()); + PrefsHandleT *prefs = prefsDvxIniShared(); AudioDrvConfigT cfg; devConfig(prefs, &cfg); @@ -1423,12 +1491,20 @@ static bool devOpen(void) { sMaster = audioPercentToVolume(audioSettingVolume(prefs)); sDev.fmTones = prefsGetBool(prefs, AUDIO_INI_SECTION, INI_FM_TONES, true); - const char *want = audioSettingDriver(prefs); + const char *want = audioSettingDriver(prefs); + + if (devOpenParked(want, &cfg)) { + snprintf(sDev.want, sizeof(sDev.want), "%s", want); + return true; + } + DvxDriverModT *mods = dvxDriversLoad(AUDIO_DRV_DIR, &sDrvClass); int32_t count = (int32_t)arrlen(mods); int32_t chosen = -1; int32_t tried = -1; + namesNote(mods); + if (strcasecmp(want, AUDIO_DRIVER_AUTO) != 0) { for (int32_t i = 0; i < count; i++) { if (soundOf(&mods[i]) && strcasecmp(mods[i].desc->name, want) == 0) { @@ -1450,10 +1526,10 @@ static bool devOpen(void) { if (chosen >= 0) { devOpenSynths(mods, &cfg); + snprintf(sDev.want, sizeof(sDev.want), "%s", want); } dvxDriversFree(mods); - prefsClose(prefs); if (chosen < 0) { dvxLog("Audio: no sound driver could be started"); @@ -1464,6 +1540,41 @@ static bool devOpen(void) { } +// The parked modules of the last device, while DVX.INI still makes the +// same driver choice: its sound driver first (true when it started; its +// synthesizers come back with it). Every parked module is let go +// otherwise. +static bool devOpenParked(const char *want, const AudioDrvConfigT *cfg) { + DvxDriverModT *mods = NULL; + bool ok = false; + + // A module handed to mods is no longer parked (mods unloads or keeps it) + if (sParked.sound.desc && strcasecmp(sParked.want, want) == 0) { + arrput(mods, sParked.sound); + memset(&sParked.sound, 0, sizeof(sParked.sound)); + ok = devTry(mods, 0, cfg); + dvxDriversFree(mods); + mods = NULL; + } + + if (ok) { + for (int32_t k = 0; k < AUDIO_SYNTH_KINDS; k++) { + if (sParked.synths[k].desc) { + arrput(mods, sParked.synths[k]); + memset(&sParked.synths[k], 0, sizeof(sParked.synths[k])); + } + } + + devOpenSynths(mods, cfg); + dvxDriversFree(mods); + dvxLog("Audio: the last device's driver started again"); + } + + audioDevForget(); + return ok; +} + + // The FM chip and the MIDI port: of each kind the first synth driver // (highest priority first) that finds its hardware. Kept modules are // taken out of mods so dvxDriversFree leaves them loaded. @@ -1495,7 +1606,7 @@ static void devOpenSynths(DvxDriverModT *mods, const AudioDrvConfigT *cfg) { // a new format (DVX.INI changed meanwhile) only needs the mixer reset. static bool devResume(void) { const AudioDriverT *drv = soundOf(&sDev.mod); - PrefsHandleT *prefs = prefsLoad(platformDvxIniPath()); + PrefsHandleT *prefs = prefsDvxIniShared(); AudioDrvConfigT cfg; AudioDrvInfoT info; @@ -1504,8 +1615,6 @@ static bool devResume(void) { bool opened = drv->open(&cfg, &info); - prefsClose(prefs); - if (!opened || !infoUsable(&info)) { dvxLog("Audio: driver '%s' could not open the output again", drv->hdr.name); @@ -1675,6 +1784,22 @@ static int32_t makeSample(AudioCtxT *ctx, int16_t *pcm, uint32_t frames, const A } +// The sound drivers' names in mods (a full load), when not read yet. +static void namesNote(DvxDriverModT *mods) { + if (sDrvNameCount >= 0) { + return; + } + + sDrvNameCount = 0; + + for (int32_t i = 0; i < (int32_t)arrlen(mods) && sDrvNameCount < AUDIO_DRIVERS_MAX; i++) { + if (soundOf(&mods[i])) { + snprintf(sDrvNames[sDrvNameCount++], AUDIO_NAME_MAX, "%s", mods[i].desc->name); + } + } +} + + // Free every ended slot, queueing a notification for owners that want // one. static void reclaimSlots(void) { @@ -1755,16 +1880,18 @@ static void stepUpdate(int32_t slot) { // Ask the fill function for as much as the ring has room for, as the -// program owning the stream. -static void streamFill(AudioStreamT *s) { +// program owning the stream; once it holds STREAM_SAFE_MS, no longer than +// until (platformMicros). +static void streamFill(AudioStreamT *s, uint64_t until) { uint32_t space = audioStreamSpace(s); + uint32_t safe = s->fmt.rate * STREAM_SAFE_MS / MS_PER_SEC; while (space > 0) { uint32_t ask = space < STREAM_FILL_FRAMES ? space : STREAM_FILL_FRAMES; - int32_t prev = audioAppEnter(s->owner->appId); + int32_t prev = dvxMemSetApp(s->owner->appId); uint32_t got = s->fill(s, s->scratch, ask, s->fillData); - audioAppEnter(prev); + dvxMemSetApp(prev); // The callback closed a stream (maybe this one): stop here. if (sStreamsDirty || got == 0) { @@ -1773,7 +1900,7 @@ static void streamFill(AudioStreamT *s) { audioStreamWrite(s, s->scratch, got < ask ? got : ask); - if (got < ask) { + if (got < ask || s->ring.ended || (audioStreamQueued(s) >= safe && platformMicros() >= until)) { break; } @@ -1782,12 +1909,12 @@ static void streamFill(AudioStreamT *s) { } +// The synth driver of a module (NULL: none, or a sound driver). static const AudioSynthDriverT *synthOf(const DvxDriverModT *mod) { return mod->kind == DRV_KIND_SYNTH ? (const AudioSynthDriverT *)mod->desc : NULL; } -// The synth driver of a module (NULL: none, or a sound driver). // Open a synthesizer and attach it as its kind; false when it found no // device. static bool synthTry(const AudioSynthDriverT *syn, const AudioDrvConfigT *cfg) { diff --git a/src/libs/kpunch/audio/audio.dhs b/src/libs/kpunch/audio/audio.dhs index c6f82cc0..c611763c 100644 --- a/src/libs/kpunch/audio/audio.dhs +++ b/src/libs/kpunch/audio/audio.dhs @@ -35,12 +35,14 @@ .index audioStreamOpen .index audioStreamWrite .index audioStreamFlush +.index audioStreamEnd .index audioVoicePause .index audioWavParse .index audioDecode .index SoundPause .index SoundResume .index SoundStreamFlush +.index SoundStreamEnd .index audioSampleLoad .index audioGetInfo .index AudioInfoT @@ -66,6 +68,7 @@ .index OPL .index MPU-401 .index audioNotesOpen +.index audioNotesSetTone .index audioNotesAdd .index AudioNotesT .index audioDeskPlay @@ -130,7 +133,7 @@ if (voice < 0) { audioClose(audio); // or let the program end .endcode -The first audioOpen starts the sound device and the last audioClose stops it. In between, after AUDIO_IDLE_CLOSE_SEC (3) seconds with nothing playing, the device's output is closed so it costs no CPU time (desktop sounds would otherwise keep it mixing silence), and the next sound opens it again with the same driver before it starts, so nothing of it is lost; audioGetInfo still reports the device. Functions returning int32_t give AUDIO_OK or a voice id (above 0) on success and a negative AUDIO_ERR_* code on failure; audioErrorText says what a code means. audioGetInfo describes the device: the driver's name and description, the mixing rate it achieved, its output bits and channels, the voice limit, and whether it plays sampled sound (hasDac), has a hardware synthesizer (hasSynth), or plays tones on a hardware tone generator (hasTone, the PC speaker). audioGetStats counts mixed blocks and frames, stream underruns, clipped samples, dropped voices, and the voices playing. +The first audioOpen starts the sound device and the last audioClose stops it; the driver modules it used stay loaded, and the next first audioOpen tries them first (no other driver is loaded or probed) while DVX.INI's driver choice is the same. In between, after AUDIO_IDLE_CLOSE_SEC (10) seconds with nothing playing, the device's output is closed so it costs no CPU time (desktop sounds would otherwise keep it mixing silence), and the next sound opens it again with the same driver before it starts, so nothing of it is lost; audioGetInfo still reports the device. Functions returning int32_t give AUDIO_OK or a voice id (above 0) on success and a negative AUDIO_ERR_* code on failure; audioErrorText says what a code means. audioGetInfo describes the device: the driver's name and description, the mixing rate it achieved, its output bits and channels, the voice limit, and whether it plays sampled sound (hasDac), has a hardware synthesizer (hasSynth), or plays tones on a hardware tone generator (hasTone, the PC speaker). audioGetStats counts mixed blocks and frames, stream underruns, clipped samples, dropped voices, and the voices playing. None of the functions may be called from an interrupt (a timer-service callback). @@ -164,7 +167,7 @@ audioNotesAdd(tune, 440 * AUDIO_MILLIHZ_PER_HZ, 250000); // A for 1/4 s audioNotesAdd(tune, 0, 50000); // a rest .endcode -A note queue plays tones and rests one after the other, each lasting exactly as long as asked (its length is counted in output frames from the time queued since the queue last went quiet, so many short notes do not drift). audioNotesAdd(notes, milliHz, us) appends a tone of milliHz thousandths of a hertz (AUDIO_TONE_MIN_HZ to AUDIO_TONE_MAX_HZ, or 0 for a rest) lasting us microseconds; up to AUDIO_NOTES_MAX entries may wait (AUDIO_ERR_BUSY beyond). audioNotesQueued says how many entries have not finished (and how many of them are tones), audioNotesSpace how many more fit, audioNotesClear silences the queue at once. The queue plays on its own voice (audioNotesVoice, for volume and pan) that the voice limit never drops, in the mixer or on the PC speaker's tone generator (never on the FM chip); it is silent while empty. music.lib plays QBasic's PLAY and SOUND through note queues. +A note queue plays tones and rests one after the other, each lasting exactly as long as asked (its length is counted in output frames from the time queued since the queue last went quiet, so many short notes do not drift). audioNotesAdd(notes, milliHz, us) appends a tone of milliHz thousandths of a hertz (AUDIO_TONE_MIN_HZ to AUDIO_TONE_MAX_HZ, or 0 for a rest) lasting us microseconds; up to AUDIO_NOTES_MAX entries may wait (AUDIO_ERR_BUSY beyond). audioNotesSetTone(notes, wave, level) sets the waveform and loudness (0..AUDIO_VOLUME_MAX, a share of the queue's volume) of the entries added from then on -- each entry plays in its own, so a tune changes them in time -- and a tone generator plays a tone at level 0 as a rest. audioNotesQueued says how many entries have not finished (and how many of them are tones), audioNotesSpace how many more fit, audioNotesClear silences the queue at once. The queue plays on its own voice (audioNotesVoice, for volume and pan) that the voice limit never drops, in the mixer or on the PC speaker's tone generator (never on the FM chip); it is silent while empty. music.lib plays QBasic's PLAY and SOUND through note queues. .h2 Streams @@ -175,7 +178,7 @@ AudioStreamT *stream = NULL; audioStreamOpen(audio, &fmt, 250, fillMusic, &player, &stream); .endcode -audioStreamOpen(ctx, fmt, bufferMs, fill, userData, &stream) opens a stream of any rate from AUDIO_RATE_MIN (4000) to AUDIO_RATE_MAX (48000), 8 or 16 bits, mono or stereo, with a buffer of bufferMs milliseconds (0: 250). It plays at once on its own voice (audioStreamVoice), silence until samples arrive. Without fill the program writes samples with audioStreamWrite, which takes as many frames as fit and returns how many; audioStreamSpace and audioStreamQueued tell how much room and how much sound are left. With fill, the library asks for samples whenever the buffer has room: fill(stream, buf, frames, userData) writes up to frames frames in the stream's format and returns how many. Fill functions run from audioPump and the shell's idle time; a program that keeps the desktop busy every frame should call audioPump once a frame. A stream that runs dry plays silence and counts underruns. audioStreamFlush drops what the stream holds and has not played yet (a seek or a stop is heard at once; the two frames the mixer has read ahead still play). audioStreamClose stops and frees it. +audioStreamOpen(ctx, fmt, bufferMs, fill, userData, &stream) opens a stream of any rate from AUDIO_RATE_MIN (4000) to AUDIO_RATE_MAX (48000), 8 or 16 bits, mono or stereo, with a buffer of bufferMs milliseconds (0: 250). It plays at once on its own voice (audioStreamVoice), silence until samples arrive. Without fill the program writes samples with audioStreamWrite, which takes as many frames as fit and returns how many; audioStreamSpace and audioStreamQueued tell how much room and how much sound are left. With fill, the library asks for samples whenever the buffer has room: fill(stream, buf, frames, userData) writes up to frames frames in the stream's format and returns how many. Fill functions run from audioPump and the shell's idle time; a program that keeps the desktop busy every frame should call audioPump once a frame. Once a stream holds 100 ms of sound, a pump fills it for 15 ms at most (STREAM_FILL_BUDGET_US) and the next pump goes on, so a fill function that takes its time (a module rendered, a file read) does not hold DVX up after a start, a seek, or a stall. A stream that runs dry plays silence and counts underruns. audioStreamEnd says no more frames will come after those written (a fill function may call it with its last ones): the fill function is not asked again, and once the stream has played what it holds its voice ends as a sample's does (AUDIO_END_FINISHED to the end function), with nothing counted as an underrun; the stream stays open until audioStreamClose. Without it a stream that has played its last frame runs on in silence, counting underruns, until it is closed. audioStreamFlush drops what the stream holds and has not played yet (a seek or a stop is heard at once; the two frames the mixer has read ahead still play). audioStreamClose stops and frees it. audioDrv.h also declares, for audio.lib and the libraries built on it (music.lib), audioDecode (a WAV or VOC file in memory as 16-bit frames), audioConvert (8- or 16-bit frames as 16-bit ones), and audioWavParse(head, headBytes, fileBytes, &fmt, &dataAt, &dataBytes) (a WAV file's format and where its frames are, from its first bytes: the frames are clamped to the file and whole; AUDIO_ERR_FORMAT for anything but a RIFF WAVE file, AUDIO_ERR_UNSUPPORTED, AUDIO_ERR_CORRUPT). @@ -228,7 +231,7 @@ note.bits = 16; audioWtStart(audio, v, ¬e); // pitch 0: AUDIO_WT_PITCH_RATE (44,100) frames a second .endcode -A wavetable synthesizer (a Gravis UltraSound's GF1, a Sound Blaster AWE32's or AWE64's EMU8000) plays samples from its own memory, and its voices and memory are shared by every program. audioWtGetInfo tells the chip (description, voices, free voices, memory, the largest free block, what it does itself: AUDIO_WT_ENVELOPES, AUDIO_WT_LFOS, AUDIO_WT_FILTER, AUDIO_WT_EFFECTS, AUDIO_WT_8BIT for 8-bit samples, AUDIO_WT_BANK_256K when a 16-bit sample must not cross a 256 KB boundary, AUDIO_WT_ROM for a General MIDI sample ROM; and its level, the centibels a voice at full volume is louder than full-scale digital sound, measured: 0 on a GUS, whose digital sound plays on its own voices, 76 on an AWE). audioWtMemAlloc(ctx, bytes, bits, &addr) takes memory for samples of bits bits (AUDIO_ERR_NO_ROOM when no free block is large enough), audioWtUpload(ctx, addr, frames, count, bits) copies 16-bit frames into it at bits, audioWtMemFree gives it back. audioWtAlloc(ctx) takes a voice (the least recently given back first), audioWtStart(ctx, voice, note) starts it on a note (AudioWtVoiceT: the sample's address, or AUDIO_WT_ADDR_ROM with an address in the ROM, and in frames from there the one it starts from (offset: before the loop or inside it), its loop, and its end; pitch in cents, 0 for AUDIO_WT_PITCH_RATE (44,100) frames a second; attenuation in centibels; pan; the filter, reverb and chorus; both envelopes in timecents; both LFOs; audioWtNoteInit makes a plain one, with no envelope or LFO shaping it, the filter open, no effects, centred, at full volume, for the caller to set the sample on), audioWtUpdate changes what its what bits say (AUDIO_WT_SET_PITCH, _VOLUME, _PAN, _FILTER, _EFFECTS, _LFO), audioWtRelease lets the key up, audioWtStop stops it at once, audioWtActive is false once it is silent, audioWtFree gives it back. The chip does what its flags say; the program does the rest by updating the voice as the note goes on. Closing the context gives back its voices and its memory. The voice calls may be made from a timer callback (music.lib plays SoundFont MIDI and modules that way); the memory calls may not. +A wavetable synthesizer (a Gravis UltraSound's GF1, a Sound Blaster AWE32's or AWE64's EMU8000) plays samples from its own memory, and its voices and memory are shared by every program. audioWtGetInfo tells the chip (description, voices, free voices, memory, the largest free block, what it does itself: AUDIO_WT_ENVELOPES, AUDIO_WT_LFOS, AUDIO_WT_FILTER, AUDIO_WT_EFFECTS, AUDIO_WT_8BIT for 8-bit samples, AUDIO_WT_BANK_256K when a 16-bit sample must not cross a 256 KB boundary, AUDIO_WT_ROM for a General MIDI sample ROM; and its level, the centibels a voice at full volume is louder than full-scale digital sound, measured: 0 on a GUS, whose digital sound plays on its own voices, 76 on an AWE). audioWtMemAlloc(ctx, bytes, bits, &addr) takes memory for samples of bits bits (AUDIO_ERR_NO_ROOM when no free block is large enough), audioWtUpload(ctx, addr, frames, count, bits) copies 16-bit frames into it at bits, audioWtMemFree gives it back. audioWtAlloc(ctx) takes a voice (the least recently given back first), audioWtStart(ctx, voice, note) starts it on a note (AUDIO_ERR_ARG for a sample in a ROM the chip does not have; AudioWtVoiceT: the sample's address, or AUDIO_WT_ADDR_ROM with an address in the ROM, and in frames from there the one it starts from (offset: before the loop or inside it), its loop, and its end; pitch in cents, 0 for AUDIO_WT_PITCH_RATE (44,100) frames a second; attenuation in centibels; pan; the filter, reverb and chorus; both envelopes in timecents; both LFOs; audioWtNoteInit makes a plain one, with no envelope or LFO shaping it, the filter open, no effects, centred, at full volume, for the caller to set the sample on), audioWtUpdate changes what its what bits say (AUDIO_WT_SET_PITCH, _VOLUME, _PAN, _FILTER, _EFFECTS, _LFO), audioWtRelease lets the key up, audioWtStop stops it at once, audioWtActive is false once it is silent, audioWtFree gives it back. The chip does what its flags say; the program does the rest by updating the voice as the note goes on. Closing the context gives back its voices and its memory. The voice calls may be made from a timer callback (music.lib plays SoundFont MIDI and modules that way); the memory calls may not. .h2 MIDI Port @@ -284,7 +287,7 @@ A settings screen reads and writes the keys it offers through the library, so th .h2 Desktop Sounds -The library plays the sounds DVX makes for its events (dvxPlaySound in dvxApp.h): when it loads it sets DVX's sound hook to audioDeskPlay(event, wait). The file of each event is read from DVX.INI [sounds] at the first sound: the key is audioDeskKey(event) and the value a WAV or VOC file, empty or missing for none: a name alone is found in the sound folders (the user folder's SOUNDS first, then SHARED\SOUNDS: platformKindFind), anything else is a path; audioDeskReload makes the next sound read the section again (the Control Panel calls it after saving). The sounds play on a context of the library's own, so no program's end cuts one short, at full volume under the master volume. When no sound device starts, the sounds stay silent without trying again until the next reload. With wait the call returns once the sound has played (at most 5 seconds); after the exit sound further events are quiet (DVX is ending). audioDeskLabel(event) names an event for people, audioDeskPlayFile(path) plays any file the same way (to try one out). audioDeskFile(prefs, event) reads an event's file from a copy of DVX.INI ("" for none) and audioDeskSetFile(prefs, event, path) sets it; the library's own reading of [sounds] goes through audioDeskFile too. The default set, made by DVX's own sound generator at build time, is in SHARED\SOUNDS. +The library plays the sounds DVX makes for its events (dvxPlaySound in dvxApp.h): when it loads it sets DVX's sound hook to audioDeskPlay(event, wait). The file of each event is read from DVX.INI [sounds] at the first sound: the key is audioDeskKey(event) and the value a WAV or VOC file, empty or missing for none: a name alone is found in the sound folders (the user folder's SOUNDS first, then SHARED\SOUNDS: platformKindFind), anything else is a path; audioDeskReload makes the next sound read the section again (the Control Panel calls it after saving). Each event's file is decoded at its first sound and kept until then, so a window opening or a message box does not read the disk. The sounds play on a context of the library's own, so no program's end cuts one short, at full volume under the master volume. When no sound device starts, the sounds stay silent without trying again until the next reload. With wait the call returns once the sound has played (at most 5 seconds), running dvxWaitPump meanwhile (a BASIC program's events go on); after the exit sound further events are quiet (DVX is ending). audioDeskLabel(event) names an event for people, audioDeskPlayFile(path) plays any file the same way (to try one out). audioDeskFile(prefs, event) reads an event's file from a copy of DVX.INI ("" for none) and audioDeskSetFile(prefs, event, path) sets it; the library's own reading of [sounds] goes through audioDeskFile too. The default set, made by DVX's own sound generator at build time, is in SHARED\SOUNDS. .table Key Event @@ -323,7 +326,7 @@ Sound drivers are tried in this order: the one DVX.INI names, then from the high .h2 Writing a Driver -A driver is a driver module (see dvxDrv.h in the API reference): .drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers// folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). A driver whose card records also sets AudioDrvInfoT.recSources and gives recordStart(want, source, got) and recordStop: recordStart is called with the output closed and takes the card for recording (its own DMA buffer and interrupt; the format the card records in got), its interrupt then calls audioRecPush(src, bytes) with each block recorded (whole frames; a block that does not fit is dropped and counted), and recordStop gives everything back as close does (sb.drv, wss.drv, and gus.drv use their close for both; the AD1848 codec's routines a Windows Sound System board and an UltraSound MAX share are codec/codecHw.c). While playing, the driver calls audioMixRender(buffer, frames) for each block of output from its own interrupt, or calls audioClockStart to have the timer service drive the mixer (the output is thrown away) when it only plays tones or nothing. Code and data an interrupt touches must be pinned: audioLockSpan pins a module's listed functions and data (a module's code comes before its data, so one span covers them), and the library is built without hot/cold function splitting and without loops turned into library calls. Interrupt code must not call the C library, DVX, or DOS and must not use floating point. audio.lib stops the device from a shutdown hook if DVX ends without closing it. +A driver is a driver module (see dvxDrv.h in the API reference): .drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers// folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest; when the last program closes sound it closes them but keeps their modules for the next device, which opens them again first (so open and close must work more than once). The drivers' names (audioDriverNames) are read once a session. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). A driver whose card records also sets AudioDrvInfoT.recSources and gives recordStart(want, source, got) and recordStop: recordStart is called with the output closed and takes the card for recording (its own DMA buffer and interrupt; the format the card records in got), its interrupt then calls audioRecPush(src, bytes) with each block recorded (whole frames; a block that does not fit is dropped and counted), and recordStop gives everything back as close does (sb.drv, wss.drv, and gus.drv use their close for both; the AD1848 codec's routines a Windows Sound System board and an UltraSound MAX share are codec/codecHw.c). While playing, the driver calls audioMixRender(buffer, frames) for each block of output from its own interrupt, or calls audioClockStart to have the timer service drive the mixer (the output is thrown away) when it only plays tones or nothing. Code and data an interrupt touches must be pinned: audioLockSpan pins a module's listed functions and data (a module's code comes before its data, so one span covers them), and the library is built without hot/cold function splitting and without loops turned into library calls. Interrupt code must not call the C library, DVX, or DOS and must not use floating point. audio.lib stops the device from a shutdown hook if DVX ends without closing it. A synthesizer driver exports an AudioSynthDriverT named audioSynth instead, written with AUDIO_SYNTH_DRIVER(name, priority, .kind = AUDIO_SYNTH_FM, AUDIO_SYNTH_MIDI, or AUDIO_SYNTH_WAVETABLE, .open = ..., .close = ..., then .write, .put, or the wavetable functions). open detects the hardware, fills AudioSynthDrvInfoT (description; for FM the voices and whether it is stereo; for a wavetable synthesizer its voices, memory, AUDIO_WT_* flags, and level, 0 when it is as loud as the digital sound or not known) and leaves it silent; close silences it. write(reg, value) sets an FM register (bank x AUDIO_FM_BANK_REGS + index) and must keep the index/data pair together (audioIrqOff / audioIrqRestore); put(byte) sends a MIDI byte, returning false when the port is not ready; a MIDI driver whose port receives also has get(&byte), the next byte the port has (false when none), which the MIDI input's timer calls (mpu401.drv keeps what put finds waiting for get). A wavetable driver has upload(addr, frames, count, bits) (program time; addresses as audioWtMemAlloc gives them) and start, update, release, stop, and active for its voices (numbered from 0, as audio.lib hands them out); it plays a note as AudioWtVoiceT gives it, in its own units. write, put, and the voice functions run at interrupt time too (timer callbacks), so they follow the rules above and are pinned by the driver. A sound driver whose card also has a synthesizer (the GUS) points AudioDrvInfoT.synth at its own AudioSynthDriverT, which is opened before the others. @@ -333,6 +336,24 @@ src/include/basic/sound.bas (DECLARE LIBRARY "audio"): PlayWave(fileName), PlayW For a settings program (the Control Panel's Sounds page): SoundEventCount, SoundEventLabel$(eventIdx), SoundEventKey$(eventIdx) (audioDeskLabel / audioDeskKey; events numbered from 0 in DvxSoundE order); SoundEventFile$(prefs, eventIdx) and SoundSetEventFile(prefs, eventIdx, fileName) (audioDeskFile / audioDeskSetFile); SoundEventsReload (audioDeskReload); SoundTestFile(fileName) (audioDeskPlayFile, a name alone found in the sound folders as the events' files are: a voice number, or 0 with SoundError$); SoundFolder$ (SHARED\SOUNDS, the sounds DVX ships; FileOpsKindDir$ and FileOpsKindList give the user's too); SoundMasterVolume and SoundSetMasterVolume(percent) (audioGetMasterVolume / audioSetMasterVolume in percent); SoundDriverCount (reads audioDriverNames, at most AUDIO_DRIVERS_MAX) and SoundDriverName$(index); SoundDriverAuto$ (AUDIO_DRIVER_AUTO); SoundMidiCount and SoundMidiLabel$(index) (audioMidiChoices); SoundMidiInCount and SoundMidiInLabel$(index) (audioMidiInChoices); SoundModulesCount and SoundModulesLabel$(index) (audioModulesChoices); SoundSettingDriver$(prefs), SoundSettingMidi(prefs), SoundSettingMidiIn(prefs), SoundSettingModules(prefs), SoundSettingVolume(prefs), SoundSettingsStore(prefs, driver, midi, modules, volume), and SoundSettingMidiInStore(prefs, midiIn) (the audioSetting functions). prefs is a prefs.bas handle (prefsBasHandle, dvxPrefs.h); a handle that is not open reads the defaults and stores nothing. -The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running", "record" True / False; "voices" = voiceLimit, "rate", "bits", "channels", "recordsources"; any case, 0 for another field); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order). Samples: SoundLoad(fileName) (audioSampleLoad) and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped), and SoundSetFrequency(voice, hz) (the audioVoiceSet functions), SoundPause(voice) and SoundResume(voice) (audioVoicePause; a sound's or a stream's voice); the result is kept for SoundError$. Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamFlush (audioStreamFlush), SoundStreamClose. Recording, one per program (audioRecFile): RecordStart(fileName, rate, bits, channels, inputSource) (True / False with SoundError$; 0 for the defaults), RecordStop (audioRecClose; True when the file was all written), Recording, RecordLength (milliseconds recorded, written or waiting), RecordLevel (audioRecPeak), RecordLost, RecordRate, RecordBits, RecordChannels (audioRecFormat), with CONSTs RECORD_MIC, RECORD_LINE, RECORD_CD (AUDIO_REC_*) and SoundCaps "record" and "recordsources". FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS). Wavetable: WtInfo$(field) (audioWtGetInfo: "description", "voices", "free", "memory", "freememory", "level", and the flags "envelopes", "lfos", "filter", "effects", "8bit", "rom" as text), WtMemAlloc(bytes, bits) (an address, -1 for no room), WtUpload(address, pcm AS BINARY STRING, bits) (16-bit mono samples), WtMemFree, WtAlloc, WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) (a plain note: no envelope or LFO, the filter open; pitch in cents, attenuation in centibels, pan -100..100 scaled to AUDIO_WT_PAN_MAX), WtSetPitch, WtSetAttenuation, WtSetPan (audioWtUpdate of the note kept for the voice; True / False), WtRelease, WtStop, WtActive, WtFree. The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned", "input", "inputowned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease; its input: MidiInOpen (True / False with SoundError$), MidiInClose, MidiInMessage (audioMidiInRead: the message packed as status + data1 * 256 + data2 * 65536, AUDIO_MIDI_PACKED_*, or MIDI_NONE, -1), MidiInTime (the ms of the message MidiInMessage gave last), MidiInClock (audioMidiInClock), MidiInLost, with CONSTs MIDI_NOTE_OFF to MIDI_PITCH_BEND, MIDI_KIND_MASK, and MIDI_CHANNEL_MASK. CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*), and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*). +The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running", "record" True / False; "voices" = voiceLimit, "rate", "bits", "channels", "recordsources"; any case, 0 for another field); SoundStats(field) (audioGetStats: "renders", "frames", "underruns", "clipped", "dropped", "playing"); SoundDriver$ (AudioInfoT.driver); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order). -Sound and stream numbers are 1..64 per kind, valid only for the program that got them; audioClose of a BASIC context forgets its numbers (audioBasForget) as it frees the samples and streams. BINARY STRING parameters arrive as a pointer and a byte count (two C parameters; see the DECLARE LIBRARY rules in the BASIC reference). +Samples: SoundLoad(fileName) (audioSampleLoad), SoundLoadData(bytes AS BINARY STRING) (audioSampleLoadMem), and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundRate, SoundBits, and SoundChannels (audioSampleInfo's format), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped), and SoundSetFrequency(voice, hz) (the audioVoiceSet functions), SoundPause(voice) and SoundResume(voice) (audioVoicePause; a sound's or a stream's voice); the result is kept for SoundError$. + +Ends: the program's context has audioSetEndFn set to audioBasEnded, which keeps the last BAS_ENDED_MAX (64) ended voices; SoundEnded gives the program's next one (0 for none) and SoundEndReason the AudioEndE of the one it gave last (CONSTs SOUND_END_*). + +Note queues: NotesOpen(wave, volume) (audioNotesOpen; CONSTs SOUND_WAVE_* for AudioWaveE), NotesAdd(queue, hertz AS DOUBLE, milliseconds AS DOUBLE) (audioNotesAdd in millihertz and microseconds), NotesSetTone(queue, wave, volume) (audioNotesSetTone, the volume a percent of AUDIO_VOLUME_MAX), NotesQueued, NotesSpace, NotesVoice, NotesClear, NotesClose; numbered 1..64 like sounds. + +Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamEnd (audioStreamEnd: no more will be written; the voice ends, SoundEnded tells, once what is held has played), SoundStreamFlush (audioStreamFlush), SoundStreamClose. + +Recording, one per program (audioRecFile, or audioRecOpen into memory when fileName is ""): RecordStart(fileName, rate, bits, channels, inputSource) (True / False with SoundError$; 0 for the defaults), RecordRead$(maxBytes) AS BINARY STRING (audioRecRead of the whole frames that fit, into a buffer the next call reuses) and RecordQueued (audioRecQueued, in bytes), RecordStop (audioRecClose; True when a file was all written), Recording, RecordLength (milliseconds recorded, written or waiting), RecordLevel (audioRecPeak), RecordLost, RecordRate, RecordBits, RecordChannels (audioRecFormat), with CONSTs RECORD_MIC, RECORD_LINE, RECORD_CD (AUDIO_REC_*) and SoundCaps "record" and "recordsources". + +FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS). + +Wavetable: WtInfo$(field) (audioWtGetInfo: "description", "voices", "free", "memory", "freememory" (the largest free block), "freetotal" (freeTotal: all that is free), "level", and the flags "envelopes", "lfos", "filter", "effects", "8bit", "rom" as text), WtMemAlloc(bytes, bits) (an address, -1 for no room), WtUpload(address, pcm AS BINARY STRING, bits) (16-bit mono samples), WtMemFree, WtAlloc (the voice's note made plain: audioWtNoteInit), WtShape(voice, field, value) (an AudioWtVoiceT envelope, LFO, filter, or effects field by name -- "volattack", "modlfofreq", "filterfc", "reverb", ...; int16_t fields clamped -- kept in the voice's note, with audioWtUpdate at once on a sounding note for AUDIO_WT_SET_FILTER, _EFFECTS, and _LFO fields), WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) (the voice's note with the sample and these set; a negative address is AUDIO_WT_ADDR_ROM's, CONST WT_ADDR_ROM; pitch in cents, attenuation in centibels, pan -100..100 scaled to AUDIO_WT_PAN_MAX), WtSetPitch, WtSetAttenuation, WtSetPan (audioWtUpdate of the note kept for the voice; True / False), WtRelease, WtStop, WtActive, WtFree. + +The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned", "input", "inputowned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease; its input: MidiInOpen (True / False with SoundError$), MidiInClose, MidiInMessage (audioMidiInRead: the message packed as status + data1 * 256 + data2 * 65536, AUDIO_MIDI_PACKED_*, or MIDI_NONE, -1), MidiInTime (the ms of the message MidiInMessage gave last), MidiInClock (audioMidiInClock), MidiInLost, with CONSTs MIDI_NOTE_OFF to MIDI_PITCH_BEND, MIDI_KIND_MASK, and MIDI_CHANNEL_MASK. + +CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*), and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*). + +Sound, stream, and note queue numbers are 1..64 per kind, valid only for the program that got them; audioClose of a BASIC context forgets its numbers and ended voices (audioBasForget) as it frees the samples, streams, and queues. BINARY STRING parameters arrive as a pointer and a byte count (two C parameters; see the DECLARE LIBRARY rules in the BASIC reference). diff --git a/src/libs/kpunch/audio/audio.h b/src/libs/kpunch/audio/audio.h index f7237ccc..cb655b30 100644 --- a/src/libs/kpunch/audio/audio.h +++ b/src/libs/kpunch/audio/audio.h @@ -311,6 +311,14 @@ void audioStreamClose(AudioStreamT *stream); uint32_t audioStreamQueued(const AudioStreamT *stream); uint32_t audioStreamSpace(const AudioStreamT *stream); +// No more frames will come after those written (a fill function may call +// it with its last ones, which are still played): the fill function is +// not asked again, and once the stream has played what it holds its +// voice ends as a sample's does (the end function gets +// AUDIO_END_FINISHED), with nothing counted as an underrun. The stream +// stays open until audioStreamClose. NULL is ignored. +void audioStreamEnd(AudioStreamT *stream); + // Drop the frames buffered and not yet played (a seek or a stop is heard // at once; a fill function fills the room again). NULL is ignored. void audioStreamFlush(AudioStreamT *stream); @@ -420,6 +428,12 @@ void audioNotesClear(AudioNotesT *notes); // than rests. uint32_t audioNotesQueued(const AudioNotesT *notes, uint32_t *tones); +// The waveform and loudness (0..AUDIO_VOLUME_MAX, a share of the queue's +// volume) of the entries added from now on; the queue starts in its own +// waveform at AUDIO_VOLUME_MAX. A tone generator plays a tone at 0 as a +// rest and ignores the rest. +int32_t audioNotesSetTone(AudioNotesT *notes, AudioWaveE wave, int32_t level); + // Entries that can still be added. uint32_t audioNotesSpace(const AudioNotesT *notes); @@ -693,6 +707,7 @@ typedef struct { uint32_t freeVoices; uint32_t memory; // bytes of sample memory uint32_t freeMemory; // the largest free block + uint32_t freeTotal; // the bytes free in all (in blocks maybe apart) uint32_t flags; // AUDIO_WT_* int32_t level; // centibels a voice at full volume is louder than full-scale digital sound (measured; 0: as loud, or not known) bool present; @@ -712,7 +727,8 @@ void audioWtNoteInit(AudioWtVoiceT *v); int32_t audioWtAlloc(AudioCtxT *ctx); int32_t audioWtFree(const AudioCtxT *ctx, int32_t voice); -// audioWtStart starts the voice on a note; Update changes it (what: +// audioWtStart starts the voice on a note (AUDIO_ERR_ARG for a sample in +// a ROM the chip does not have); Update changes it (what: // AUDIO_WT_SET_*); Release lets the key up (the chip's envelopes release, // a sample that loops while the key is down plays out); Stop stops it at // once. audioWtActive is false once the voice has fallen silent (its @@ -841,105 +857,129 @@ void audioSettingMidiInStore(PrefsHandleT *prefs, int32_t midiIn); // its context. SoundCreate, SoundStreamWrite and WtUpload take BINARY // STRING PCM (bytes and byte count). The Wt... functions are the // wavetable synthesizer's in its own units (cents, centibels; pan -// -100..100; WtMemAlloc -1 for no room), each started note plain (no -// envelope or LFO, the filter open). The Midi... functions are declared -// in music.bas. -int32_t FmAlloc(void); -void FmFree(int32_t voice); -const char *FmInfo(const char *field); -int32_t FmRawAcquire(void); -void FmRawRelease(void); -int32_t FmRawWrite(int32_t reg, int32_t value); -int32_t FmWrite(int32_t voice, int32_t reg, int32_t value); -int32_t MidiAcquire(void); -int32_t MidiInClock(void); -void MidiInClose(void); -int32_t MidiInLost(void); -int32_t MidiInMessage(void); -int32_t MidiInOpen(void); -int32_t MidiInTime(void); -const char *MidiPortInfo(const char *field); -int32_t MidiPut(int32_t byteValue); -void MidiRelease(void); -int32_t PlayNoise(int32_t frequency, int32_t milliseconds, int32_t volume); -int32_t PlayTone(int32_t frequency, int32_t milliseconds, int32_t volume); -int32_t PlayTriangle(int32_t frequency, int32_t milliseconds, int32_t volume); -int32_t PlayWave(const char *fileName); -int32_t PlayWaveLoop(const char *fileName); -int32_t RecordBits(void); -int32_t RecordChannels(void); -int32_t Recording(void); -int32_t RecordLength(void); -int32_t RecordLevel(void); -int32_t RecordLost(void); -int32_t RecordRate(void); -int32_t RecordStart(const char *fileName, int32_t rate, int32_t bits, int32_t channels, int32_t source); -int32_t RecordStop(void); -int32_t SoundCaps(const char *field); -int32_t SoundCreate(const uint8_t *pcm, int32_t len, int32_t rate, int32_t bits, int32_t channels); -const char *SoundDriverAuto(void); -int32_t SoundDriverCount(void); -const char *SoundDriverName(int32_t index); -const char *SoundError(void); -int32_t SoundEventCount(void); -const char *SoundEventFile(int32_t prefs, int32_t eventIdx); -const char *SoundEventKey(int32_t eventIdx); -const char *SoundEventLabel(int32_t eventIdx); -void SoundEventsReload(void); -const char *SoundFolder(void); -void SoundFree(int32_t sound); -int32_t SoundGetVolume(void); -const char *SoundInfo(void); -int32_t SoundLength(int32_t sound); -int32_t SoundLoad(const char *fileName); -int32_t SoundMasterVolume(void); -int32_t SoundMidiCount(void); -int32_t SoundMidiInCount(void); -const char *SoundMidiInLabel(int32_t index); -const char *SoundMidiLabel(int32_t index); -int32_t SoundModulesCount(void); -const char *SoundModulesLabel(int32_t index); -void SoundPause(int32_t voice); -int32_t SoundPlay(int32_t sound, int32_t volume, int32_t pan, int32_t loopIt); -void SoundPlayEvent(int32_t eventIdx, int32_t wait); -int32_t SoundPlaying(int32_t voice); -void SoundResume(int32_t voice); -void SoundSetEventFile(int32_t prefs, int32_t eventIdx, const char *fileName); -void SoundSetFrequency(int32_t voice, int32_t frequency); -void SoundSetMasterVolume(int32_t percent); -void SoundSetPan(int32_t voice, int32_t pan); -void SoundSetPitch(int32_t voice, int32_t percent); -const char *SoundSettingDriver(int32_t prefs); -int32_t SoundSettingMidi(int32_t prefs); -int32_t SoundSettingMidiIn(int32_t prefs); -void SoundSettingMidiInStore(int32_t prefs, int32_t midiIn); -int32_t SoundSettingModules(int32_t prefs); -void SoundSettingsStore(int32_t prefs, const char *driver, int32_t midi, int32_t modules, int32_t volume); -int32_t SoundSettingVolume(int32_t prefs); -void SoundSetVolume(int32_t voice, int32_t volume); -void SoundStreamClose(int32_t stream); -void SoundStreamFlush(int32_t stream); -int32_t SoundStreamOpen(int32_t rate, int32_t bits, int32_t channels, int32_t bufferMs); -int32_t SoundStreamQueued(int32_t stream); -int32_t SoundStreamSpace(int32_t stream); -int32_t SoundStreamVoice(int32_t stream); -int32_t SoundStreamWrite(int32_t stream, const uint8_t *pcm, int32_t len); -int32_t SoundTestFile(const char *fileName); -void SoundVolume(int32_t volume); -void StopAllSounds(void); -void StopWave(int32_t voice); -int32_t WtActive(int32_t voice); -int32_t WtAlloc(void); -void WtFree(int32_t voice); -const char *WtInfo(const char *field); -int32_t WtMemAlloc(int32_t bytes, int32_t bits); -void WtMemFree(int32_t address); -void WtRelease(int32_t voice); -int32_t WtSetAttenuation(int32_t voice, int32_t attenuation); -int32_t WtSetPan(int32_t voice, int32_t pan); -int32_t WtSetPitch(int32_t voice, int32_t pitch); -int32_t WtStart(int32_t voice, int32_t address, int32_t startFrame, int32_t endFrame, int32_t loopStart, int32_t loopEnd, int32_t loopMode, int32_t bits, int32_t pitch, int32_t attenuation, int32_t pan); -void WtStop(int32_t voice); -int32_t WtUpload(int32_t address, const uint8_t *pcm, int32_t len, int32_t bits); +// -100..100; WtMemAlloc -1 for no room), a voice's notes plain (no +// envelope or LFO, the filter open) until WtShape shapes them. +// SoundEnded gives a program's ended voices (audioBasEnded keeps them), +// RecordStart with no file name records into memory for RecordRead +// (BINARY STRING: bytes and *outLen), and the Notes... functions are +// note queues by number. The Midi... functions are declared in +// music.bas. +int32_t FmAlloc(void); +void FmFree(int32_t voice); +const char *FmInfo(const char *field); +int32_t FmRawAcquire(void); +void FmRawRelease(void); +int32_t FmRawWrite(int32_t reg, int32_t value); +int32_t FmWrite(int32_t voice, int32_t reg, int32_t value); +int32_t MidiAcquire(void); +int32_t MidiInClock(void); +void MidiInClose(void); +int32_t MidiInLost(void); +int32_t MidiInMessage(void); +int32_t MidiInOpen(void); +int32_t MidiInTime(void); +const char *MidiPortInfo(const char *field); +int32_t MidiPut(int32_t byteValue); +void MidiRelease(void); +int32_t NotesAdd(int32_t queue, double hz, double milliseconds); +void NotesClear(int32_t queue); +void NotesClose(int32_t queue); +int32_t NotesOpen(int32_t wave, int32_t volume); +int32_t NotesQueued(int32_t queue); +int32_t NotesSetTone(int32_t queue, int32_t wave, int32_t volume); +int32_t NotesSpace(int32_t queue); +int32_t NotesVoice(int32_t queue); +int32_t PlayNoise(int32_t frequency, int32_t milliseconds, int32_t volume); +int32_t PlayTone(int32_t frequency, int32_t milliseconds, int32_t volume); +int32_t PlayTriangle(int32_t frequency, int32_t milliseconds, int32_t volume); +int32_t PlayWave(const char *fileName); +int32_t PlayWaveLoop(const char *fileName); +int32_t RecordBits(void); +int32_t RecordChannels(void); +int32_t Recording(void); +int32_t RecordLength(void); +int32_t RecordLevel(void); +int32_t RecordLost(void); +int32_t RecordQueued(void); +int32_t RecordRate(void); +const uint8_t *RecordRead(int32_t maxBytes, int32_t *outLen); +int32_t RecordStart(const char *fileName, int32_t rate, int32_t bits, int32_t channels, int32_t source); +int32_t RecordStop(void); +int32_t SoundBits(int32_t sound); +int32_t SoundCaps(const char *field); +int32_t SoundChannels(int32_t sound); +int32_t SoundCreate(const uint8_t *pcm, int32_t len, int32_t rate, int32_t bits, int32_t channels); +const char *SoundDriver(void); +const char *SoundDriverAuto(void); +int32_t SoundDriverCount(void); +const char *SoundDriverName(int32_t index); +int32_t SoundEnded(void); +int32_t SoundEndReason(void); +const char *SoundError(void); +int32_t SoundEventCount(void); +const char *SoundEventFile(int32_t prefs, int32_t eventIdx); +const char *SoundEventKey(int32_t eventIdx); +const char *SoundEventLabel(int32_t eventIdx); +void SoundEventsReload(void); +const char *SoundFolder(void); +void SoundFree(int32_t sound); +int32_t SoundGetVolume(void); +const char *SoundInfo(void); +int32_t SoundLength(int32_t sound); +int32_t SoundLoad(const char *fileName); +int32_t SoundLoadData(const uint8_t *data, int32_t len); +int32_t SoundMasterVolume(void); +int32_t SoundMidiCount(void); +int32_t SoundMidiInCount(void); +const char *SoundMidiInLabel(int32_t index); +const char *SoundMidiLabel(int32_t index); +int32_t SoundModulesCount(void); +const char *SoundModulesLabel(int32_t index); +void SoundPause(int32_t voice); +int32_t SoundPlay(int32_t sound, int32_t volume, int32_t pan, int32_t loopIt); +void SoundPlayEvent(int32_t eventIdx, int32_t wait); +int32_t SoundPlaying(int32_t voice); +int32_t SoundRate(int32_t sound); +void SoundResume(int32_t voice); +void SoundSetEventFile(int32_t prefs, int32_t eventIdx, const char *fileName); +void SoundSetFrequency(int32_t voice, int32_t frequency); +void SoundSetMasterVolume(int32_t percent); +void SoundSetPan(int32_t voice, int32_t pan); +void SoundSetPitch(int32_t voice, int32_t percent); +const char *SoundSettingDriver(int32_t prefs); +int32_t SoundSettingMidi(int32_t prefs); +int32_t SoundSettingMidiIn(int32_t prefs); +void SoundSettingMidiInStore(int32_t prefs, int32_t midiIn); +int32_t SoundSettingModules(int32_t prefs); +void SoundSettingsStore(int32_t prefs, const char *driver, int32_t midi, int32_t modules, int32_t volume); +int32_t SoundSettingVolume(int32_t prefs); +void SoundSetVolume(int32_t voice, int32_t volume); +int32_t SoundStats(const char *field); +void SoundStreamClose(int32_t stream); +void SoundStreamEnd(int32_t stream); +void SoundStreamFlush(int32_t stream); +int32_t SoundStreamOpen(int32_t rate, int32_t bits, int32_t channels, int32_t bufferMs); +int32_t SoundStreamQueued(int32_t stream); +int32_t SoundStreamSpace(int32_t stream); +int32_t SoundStreamVoice(int32_t stream); +int32_t SoundStreamWrite(int32_t stream, const uint8_t *pcm, int32_t len); +int32_t SoundTestFile(const char *fileName); +void SoundVolume(int32_t volume); +void StopAllSounds(void); +void StopWave(int32_t voice); +int32_t WtActive(int32_t voice); +int32_t WtAlloc(void); +void WtFree(int32_t voice); +const char *WtInfo(const char *field); +int32_t WtMemAlloc(int32_t bytes, int32_t bits); +void WtMemFree(int32_t address); +void WtRelease(int32_t voice); +int32_t WtSetAttenuation(int32_t voice, int32_t attenuation); +int32_t WtSetPan(int32_t voice, int32_t pan); +int32_t WtSetPitch(int32_t voice, int32_t pitch); +int32_t WtShape(int32_t voice, const char *field, int32_t value); +int32_t WtStart(int32_t voice, int32_t address, int32_t startFrame, int32_t endFrame, int32_t loopStart, int32_t loopEnd, int32_t loopMode, int32_t bits, int32_t pitch, int32_t attenuation, int32_t pan); +void WtStop(int32_t voice); +int32_t WtUpload(int32_t address, const uint8_t *pcm, int32_t len, int32_t bits); #endif // AUDIO_LIB_H diff --git a/src/libs/kpunch/audio/audioBas.c b/src/libs/kpunch/audio/audioBas.c index 593c839c..4e8ca707 100644 --- a/src/libs/kpunch/audio/audioBas.c +++ b/src/libs/kpunch/audio/audioBas.c @@ -39,18 +39,22 @@ #include "audioInt.h" #include "dvxBas.h" +#include #include +#include #include #include #define PERCENT_MAX 100 -#define BAS_INFO_MAX (AUDIO_DESC_MAX + 48) #define BAS_SLOTS 64 #define MS_PER_SEC 1000 #define BITS_PER_BYTE 8 #define BYTE_MAX_VALUE 0xFF #define SAMPLE_BYTES 2 // WtUpload's samples: 16-bit +#define BAS_ENDED_MAX 64 // ended voices kept for SoundEnded (the oldest go first) +#define BAS_MILLI 1000 // NotesAdd: hertz to millihertz, milliseconds to microseconds + #define BAS_WT_MEM_NONE (-1) // WtMemAlloc: no room #define BAS_WT_BITS_8 8 #define BAS_WT_BITS_16 16 @@ -63,6 +67,17 @@ #define BAS_PITCH_MIN (AUDIO_PITCH_MIN * PERCENT_MAX / AUDIO_PITCH_NORMAL) #define BAS_PITCH_MAX (AUDIO_PITCH_MAX * PERCENT_MAX / AUDIO_PITCH_NORMAL) +// SoundStats fields +typedef enum { + StatRendersE, + StatFramesE, + StatUnderrunsE, + StatClippedE, + StatDroppedE, + StatPlayingE, + StatCountE +} SoundStatE; + // SoundCaps fields typedef enum { CapDacE, @@ -96,6 +111,7 @@ typedef enum { WtFreeE, WtMemoryE, WtFreeMemoryE, + WtFreeTotalE, WtLevelE, WtEnvelopesE, WtLfosE, @@ -124,6 +140,22 @@ typedef struct { int32_t voice; } BasWtNoteT; +// A voice of a BASIC program that ended, until SoundEnded gives it +typedef struct { + const AudioCtxT *ctx; + int32_t voice; + int32_t reason; // AudioEndE +} BasEndedT; + +// What WtShape sets in a voice's note (AudioWtVoiceT), and what of a +// sounding note audioWtUpdate changes with it (0: the next WtStart) +typedef struct { + const char *name; + size_t offset; + uint8_t size; // int16_t or int32_t + uint32_t what; +} BasWtShapeT; + // A numbered sound or stream of a BASIC program. typedef struct { void *obj; // AudioSampleT / AudioStreamT; NULL: free @@ -132,18 +164,56 @@ typedef struct { uint32_t frameBytes; // streams: bytes per frame of its format } BasSlotT; +static const char *const sStatNames[StatCountE] = { "renders", "frames", "underruns", "clipped", "dropped", "playing" }; static const char *const sCapNames[CapCountE] = { "dac", "synth", "tone", "voices", "running", "rate", "bits", "channels", "record", "recordsources" }; static const char *const sFmNames[FmFieldCountE] = { "description", "voices", "free", "stereo", "owned" }; static const char *const sMidiNames[MidiFieldCountE] = { "description", "present", "owned", "input", "inputowned" }; -static const char *const sWtNames[WtFieldCountE] = { "description", "voices", "free", "memory", "freememory", "level", "envelopes", "lfos", "filter", "effects", "8bit", "rom" }; +static const char *const sWtNames[WtFieldCountE] = { "description", "voices", "free", "memory", "freememory", "freetotal", "level", "envelopes", "lfos", "filter", "effects", "8bit", "rom" }; static const uint32_t sWtFlags[WtFieldCountE] = { [WtEnvelopesE] = AUDIO_WT_ENVELOPES, [WtLfosE] = AUDIO_WT_LFOS, [WtFilterE] = AUDIO_WT_FILTER, [WtEffectsE] = AUDIO_WT_EFFECTS, [Wt8BitE] = AUDIO_WT_8BIT, [WtRomE] = AUDIO_WT_ROM }; -static char sInfo[BAS_INFO_MAX]; +#define WT_SHAPE(name, field, what) { name, offsetof(AudioWtVoiceT, field), sizeof(((AudioWtVoiceT *)0)->field), what } + +static const BasWtShapeT sWtShapes[] = { + WT_SHAPE("voldelay", volEnv[AUDIO_WT_ENV_DELAY], 0), + WT_SHAPE("volattack", volEnv[AUDIO_WT_ENV_ATTACK], 0), + WT_SHAPE("volhold", volEnv[AUDIO_WT_ENV_HOLD], 0), + WT_SHAPE("voldecay", volEnv[AUDIO_WT_ENV_DECAY], 0), + WT_SHAPE("volsustain", volEnv[AUDIO_WT_ENV_SUSTAIN], 0), + WT_SHAPE("volrelease", volEnv[AUDIO_WT_ENV_RELEASE], 0), + WT_SHAPE("moddelay", modEnv[AUDIO_WT_ENV_DELAY], 0), + WT_SHAPE("modattack", modEnv[AUDIO_WT_ENV_ATTACK], 0), + WT_SHAPE("modhold", modEnv[AUDIO_WT_ENV_HOLD], 0), + WT_SHAPE("moddecay", modEnv[AUDIO_WT_ENV_DECAY], 0), + WT_SHAPE("modsustain", modEnv[AUDIO_WT_ENV_SUSTAIN], 0), + WT_SHAPE("modrelease", modEnv[AUDIO_WT_ENV_RELEASE], 0), + WT_SHAPE("modenvpitch", modEnvToPitch, 0), + WT_SHAPE("modenvfilter", modEnvToFilter, 0), + WT_SHAPE("modlfodelay", modLfoDelay, 0), + WT_SHAPE("modlfofreq", modLfoFreq, 0), + WT_SHAPE("modlfopitch", modLfoToPitch, AUDIO_WT_SET_LFO), + WT_SHAPE("modlfofilter", modLfoToFilter, AUDIO_WT_SET_LFO), + WT_SHAPE("modlfovolume", modLfoToVolume, AUDIO_WT_SET_LFO), + WT_SHAPE("viblfodelay", vibLfoDelay, 0), + WT_SHAPE("viblfofreq", vibLfoFreq, 0), + WT_SHAPE("viblfopitch", vibLfoToPitch, AUDIO_WT_SET_LFO), + WT_SHAPE("filterfc", filterFc, AUDIO_WT_SET_FILTER), + WT_SHAPE("filterq", filterQ, AUDIO_WT_SET_FILTER), + WT_SHAPE("reverb", reverb, AUDIO_WT_SET_EFFECTS), + WT_SHAPE("chorus", chorus, AUDIO_WT_SET_EFFECTS) +}; + +static BasTextT sText; // the last string result static int32_t sLastError = AUDIO_OK; static char sDrivers[AUDIO_DRIVERS_MAX][AUDIO_NAME_MAX]; static int32_t sDriverCount = -1; // -1: not read yet static BasSlotT sSamples[BAS_SLOTS]; static BasSlotT sStreams[BAS_SLOTS]; +static BasSlotT sQueues[BAS_SLOTS]; +static BasEndedT sEnded[BAS_ENDED_MAX]; +static int32_t sEndedCount = 0; +static int32_t sEndReason = AUDIO_END_FINISHED; // of the voice SoundEnded gave last +static uint8_t *sReadBuf = NULL; // RecordRead$'s bytes (kept, grown) +static uint32_t sReadCap = 0; static BasWtNoteT sWtNotes[AUDIO_WT_VOICES_MAX]; static uint32_t sMidiInMs = 0; // the time of the message MidiInMessage gave last static AudioRecT *sRec = NULL; // the recording a BASIC program runs (one at a time in DVX) @@ -153,14 +223,17 @@ static AudioCtxT *sRecCtx = NULL; // ... and that program's context // Prototypes // ============================================================ +void audioBasEnded(AudioCtxT *ctx, int32_t voice, AudioEndE reason, void *userData); void audioBasForget(const AudioCtxT *ctx); static int32_t basAppId(void); static int32_t basField(const char *field, const char *const *names, int32_t count); static int32_t basNumber(BasSlotT *slots, void *obj, const AudioCtxT *ctx, uint32_t frameBytes); static int32_t basOk(int32_t rc); static int32_t basPercent(int32_t volume); +static AudioNotesT *basQueue(int32_t queue); static AudioRecT *basRec(AudioFormatT *fmt); static int32_t basResult(int32_t rc); +static bool basSampleFormat(int32_t sound, AudioFormatT *fmt); static int32_t basScale(int32_t percent, int32_t lo, int32_t hi, int32_t unit); static BasSlotT *basSlot(BasSlotT *slots, int32_t handle); static const char *basText(int32_t value); @@ -170,6 +243,7 @@ static void basVoiceResult(int32_t rc); static int32_t basWave(const char *fileName, bool loop); static BasWtNoteT *basWtNote(const AudioCtxT *ctx, int32_t voice); static int32_t basWtSet(int32_t voice, uint32_t what, int32_t value); +static BasWtNoteT *basWtTake(const AudioCtxT *ctx, int32_t voice); int32_t FmAlloc(void); void FmFree(int32_t voice); const char *FmInfo(const char *field); @@ -187,6 +261,14 @@ int32_t MidiInTime(void); const char *MidiPortInfo(const char *field); int32_t MidiPut(int32_t byteValue); void MidiRelease(void); +int32_t NotesAdd(int32_t queue, double hz, double milliseconds); +void NotesClear(int32_t queue); +void NotesClose(int32_t queue); +int32_t NotesOpen(int32_t wave, int32_t volume); +int32_t NotesQueued(int32_t queue); +int32_t NotesSetTone(int32_t queue, int32_t wave, int32_t volume); +int32_t NotesSpace(int32_t queue); +int32_t NotesVoice(int32_t queue); int32_t PlayNoise(int32_t frequency, int32_t milliseconds, int32_t volume); int32_t PlayTone(int32_t frequency, int32_t milliseconds, int32_t volume); int32_t PlayTriangle(int32_t frequency, int32_t milliseconds, int32_t volume); @@ -198,14 +280,21 @@ int32_t Recording(void); int32_t RecordLength(void); int32_t RecordLevel(void); int32_t RecordLost(void); +int32_t RecordQueued(void); int32_t RecordRate(void); +const uint8_t *RecordRead(int32_t maxBytes, int32_t *outLen); int32_t RecordStart(const char *fileName, int32_t rate, int32_t bits, int32_t channels, int32_t source); int32_t RecordStop(void); +int32_t SoundBits(int32_t sound); int32_t SoundCaps(const char *field); +int32_t SoundChannels(int32_t sound); int32_t SoundCreate(const uint8_t *pcm, int32_t len, int32_t rate, int32_t bits, int32_t channels); +const char *SoundDriver(void); const char *SoundDriverAuto(void); int32_t SoundDriverCount(void); const char *SoundDriverName(int32_t index); +int32_t SoundEnded(void); +int32_t SoundEndReason(void); const char *SoundError(void); int32_t SoundEventCount(void); const char *SoundEventFile(int32_t prefs, int32_t eventIdx); @@ -218,6 +307,7 @@ int32_t SoundGetVolume(void); const char *SoundInfo(void); int32_t SoundLength(int32_t sound); int32_t SoundLoad(const char *fileName); +int32_t SoundLoadData(const uint8_t *data, int32_t len); int32_t SoundMasterVolume(void); int32_t SoundMidiCount(void); int32_t SoundMidiInCount(void); @@ -229,6 +319,7 @@ void SoundPause(int32_t voice); int32_t SoundPlay(int32_t sound, int32_t volume, int32_t pan, int32_t loopIt); void SoundPlayEvent(int32_t eventIdx, int32_t wait); int32_t SoundPlaying(int32_t voice); +int32_t SoundRate(int32_t sound); void SoundResume(int32_t voice); void SoundSetEventFile(int32_t prefs, int32_t eventIdx, const char *fileName); void SoundSetFrequency(int32_t voice, int32_t frequency); @@ -243,7 +334,9 @@ int32_t SoundSettingModules(int32_t prefs); void SoundSettingsStore(int32_t prefs, const char *driver, int32_t midi, int32_t modules, int32_t volume); int32_t SoundSettingVolume(int32_t prefs); void SoundSetVolume(int32_t voice, int32_t volume); +int32_t SoundStats(const char *field); void SoundStreamClose(int32_t stream); +void SoundStreamEnd(int32_t stream); void SoundStreamFlush(int32_t stream); int32_t SoundStreamOpen(int32_t rate, int32_t bits, int32_t channels, int32_t bufferMs); int32_t SoundStreamQueued(int32_t stream); @@ -264,11 +357,29 @@ void WtRelease(int32_t voice); int32_t WtSetAttenuation(int32_t voice, int32_t attenuation); int32_t WtSetPan(int32_t voice, int32_t pan); int32_t WtSetPitch(int32_t voice, int32_t pitch); +int32_t WtShape(int32_t voice, const char *field, int32_t value); int32_t WtStart(int32_t voice, int32_t address, int32_t startFrame, int32_t endFrame, int32_t loopStart, int32_t loopEnd, int32_t loopMode, int32_t bits, int32_t pitch, int32_t attenuation, int32_t pan); void WtStop(int32_t voice); int32_t WtUpload(int32_t address, const uint8_t *pcm, int32_t len, int32_t bits); +// A voice of ctx ended: kept for SoundEnded (the oldest dropped when +// BAS_ENDED_MAX are waiting). +void audioBasEnded(AudioCtxT *ctx, int32_t voice, AudioEndE reason, void *userData) { + (void)userData; + + if (sEndedCount == BAS_ENDED_MAX) { + memmove(&sEnded[0], &sEnded[1], (size_t)(BAS_ENDED_MAX - 1) * sizeof(sEnded[0])); + sEndedCount--; + } + + sEnded[sEndedCount].ctx = ctx; + sEnded[sEndedCount].voice = voice; + sEnded[sEndedCount].reason = (int32_t)reason; + sEndedCount++; +} + + // ctx is closing (it frees its samples and streams itself): forget their // numbers. void audioBasForget(const AudioCtxT *ctx) { @@ -287,6 +398,18 @@ void audioBasForget(const AudioCtxT *ctx) { sStreams[i].obj = NULL; sStreams[i].ctx = NULL; } + + if (sQueues[i].ctx == ctx) { + sQueues[i].obj = NULL; + sQueues[i].ctx = NULL; + } + } + + for (int32_t i = sEndedCount - 1; i >= 0; i--) { + if (sEnded[i].ctx == ctx) { + memmove(&sEnded[i], &sEnded[i + 1], (size_t)(sEndedCount - i - 1) * sizeof(sEnded[0])); + sEndedCount--; + } } for (int32_t i = 0; i < AUDIO_WT_VOICES_MAX; i++) { @@ -352,6 +475,14 @@ static int32_t basPercent(int32_t volume) { } +// The calling program's note queue with this number, else NULL. +static AudioNotesT *basQueue(int32_t queue) { + BasSlotT *s = basSlot(sQueues, queue); + + return s ? (AudioNotesT *)s->obj : NULL; +} + + // The calling program's recording and its format, or NULL (fmt zeroed). static AudioRecT *basRec(AudioFormatT *fmt) { memset(fmt, 0, sizeof(*fmt)); @@ -372,6 +503,23 @@ static int32_t basResult(int32_t rc) { } +// The calling program's sound's format, false (fmt zeroed) for no such +// sound. +static bool basSampleFormat(int32_t sound, AudioFormatT *fmt) { + BasSlotT *s = basSlot(sSamples, sound); + uint32_t frames = 0; + + memset(fmt, 0, sizeof(*fmt)); + + if (!s) { + return false; + } + + audioSampleInfo((const AudioSampleT *)s->obj, fmt, &frames); + return true; +} + + // percent clamped to lo..hi, in the library's units (unit per // PERCENT_MAX). static int32_t basScale(int32_t percent, int32_t lo, int32_t hi, int32_t unit) { @@ -395,8 +543,7 @@ static BasSlotT *basSlot(BasSlotT *slots, int32_t handle) { // A number or a truth value as text, in the buffer the next call reuses. static const char *basText(int32_t value) { - snprintf(sInfo, sizeof(sInfo), "%ld", (long)value); - return sInfo; + return basTextFormat(&sText, "%ld", (long)value); } @@ -472,6 +619,22 @@ static int32_t basWtSet(int32_t voice, uint32_t what, int32_t value) { } +// A note slot for voice of ctx, its note plain; NULL when every slot is +// taken. +static BasWtNoteT *basWtTake(const AudioCtxT *ctx, int32_t voice) { + for (int32_t i = 0; i < AUDIO_WT_VOICES_MAX; i++) { + if (!sWtNotes[i].ctx) { + sWtNotes[i].ctx = ctx; + sWtNotes[i].voice = voice; + audioWtNoteInit(&sWtNotes[i].note); + return &sWtNotes[i]; + } + } + + return NULL; +} + + // A shared two-operator FM voice (FmFree gives it back), or 0. int32_t FmAlloc(void) { AudioCtxT *ctx = audioBasicContext(); @@ -498,8 +661,7 @@ const char *FmInfo(const char *field) { switch ((FmFieldE)basField(field, sFmNames, FmFieldCountE)) { case FmDescriptionE: - snprintf(sInfo, sizeof(sInfo), "%s", info.description); - return sInfo; + return basTextSet(&sText, info.description); case FmVoicesE: return basText((int32_t)info.voices); @@ -631,8 +793,7 @@ const char *MidiPortInfo(const char *field) { switch ((MidiFieldE)basField(field, sMidiNames, MidiFieldCountE)) { case MidiDescriptionE: - snprintf(sInfo, sizeof(sInfo), "%s", info.description); - return sInfo; + return basTextSet(&sText, info.description); case MidiPresentE: return basText(basTruth(info.present)); @@ -676,6 +837,83 @@ void MidiRelease(void) { } +// Append a tone of hz (0: a rest) lasting milliseconds; True when it was +// added (False when the queue is full or hz is out of range). +int32_t NotesAdd(int32_t queue, double hz, double milliseconds) { + AudioNotesT *notes = basQueue(queue); + + if (!notes || hz < 0 || milliseconds < 0) { + return basOk(AUDIO_ERR_ARG); + } + + return basOk(audioNotesAdd(notes, (uint32_t)(hz * BAS_MILLI + 0.5), (uint32_t)(milliseconds * BAS_MILLI + 0.5))); +} + + +void NotesClear(int32_t queue) { + audioNotesClear(basQueue(queue)); +} + + +void NotesClose(int32_t queue) { + BasSlotT *s = basSlot(sQueues, queue); + + if (s) { + audioNotesClose((AudioNotesT *)s->obj); + s->obj = NULL; + s->ctx = NULL; + } +} + + +// A queue of tones in wave (SOUND_WAVE_*) at volume percent: its number, +// or 0 (SoundError$ says why). +int32_t NotesOpen(int32_t wave, int32_t volume) { + AudioCtxT *ctx = audioBasicContext(); + AudioNotesT *notes = NULL; + + if (!ctx) { + return basResult(AUDIO_ERR_NO_DEVICE); + } + + int32_t rc = audioNotesOpen(ctx, (AudioWaveE)wave, audioPercentToVolume(volume), ¬es); + + return rc < 0 ? basResult(rc) : basNumber(sQueues, notes, ctx, 0); +} + + +// Entries not yet finished (playing or waiting; 0 once it has gone quiet). +int32_t NotesQueued(int32_t queue) { + AudioNotesT *notes = basQueue(queue); + + return notes ? (int32_t)audioNotesQueued(notes, NULL) : 0; +} + + +// The waveform and volume (percent of the queue's) of the tones added +// from now on; True when it worked. +int32_t NotesSetTone(int32_t queue, int32_t wave, int32_t volume) { + AudioNotesT *notes = basQueue(queue); + + return basOk(notes ? audioNotesSetTone(notes, (AudioWaveE)wave, basScale(volume, 0, PERCENT_MAX, AUDIO_VOLUME_MAX)) : AUDIO_ERR_ARG); +} + + +int32_t NotesSpace(int32_t queue) { + AudioNotesT *notes = basQueue(queue); + + return notes ? (int32_t)audioNotesSpace(notes) : 0; +} + + +// The queue's voice, for SoundSetVolume and SoundSetPan. +int32_t NotesVoice(int32_t queue) { + AudioNotesT *notes = basQueue(queue); + + return notes ? audioNotesVoice(notes) : 0; +} + + int32_t PlayNoise(int32_t frequency, int32_t milliseconds, int32_t volume) { return basTone(AUDIO_WAVE_NOISE, frequency, milliseconds, volume); } @@ -754,6 +992,15 @@ int32_t RecordLost(void) { } +// Bytes recorded into memory and not yet taken by RecordRead$. +int32_t RecordQueued(void) { + AudioFormatT fmt; + AudioRecT *rec = basRec(&fmt); + + return rec ? (int32_t)(audioRecQueued(rec) * fmt.channels * (fmt.bits / BITS_PER_BYTE)) : 0; +} + + // The rate the card records at (the nearest it has to the one asked). int32_t RecordRate(void) { AudioFormatT fmt; @@ -763,6 +1010,38 @@ int32_t RecordRate(void) { } +// Up to maxBytes of what a recording into memory has taken (whole frames, +// in its format), oldest first; "" when nothing is waiting. +const uint8_t *RecordRead(int32_t maxBytes, int32_t *outLen) { + AudioFormatT fmt; + AudioRecT *rec = basRec(&fmt); + uint32_t frameBytes = fmt.channels * (fmt.bits / BITS_PER_BYTE); + uint32_t frames = (rec && frameBytes > 0 && maxBytes > 0) ? (uint32_t)maxBytes / frameBytes : 0; + + *outLen = 0; + frames = rec ? DVX_MIN(frames, audioRecQueued(rec)) : 0; + + if (frames * frameBytes > sReadCap) { + uint8_t *grown = (uint8_t *)realloc(sReadBuf, frames * frameBytes); + + if (!grown) { + sLastError = AUDIO_ERR_NO_MEMORY; + return (const uint8_t *)""; + } + + sReadBuf = grown; + sReadCap = frames * frameBytes; + } + + if (frames == 0) { + return (const uint8_t *)""; + } + + *outLen = (int32_t)(audioRecRead(rec, sReadBuf, frames) * frameBytes); + return sReadBuf; +} + + // Record from source (RECORD_MIC, RECORD_LINE, RECORD_CD) into a WAV file // in rate, bits and channels (0: the default). True when it started; // False with SoundError$ saying why. @@ -783,7 +1062,8 @@ int32_t RecordStart(const char *fileName, int32_t rate, int32_t bits, int32_t ch fmt.bits = (uint32_t)bits; fmt.channels = (uint32_t)channels; - int32_t rc = audioRecFile(ctx, fileName, &fmt, source, &rec); + // No file name: into memory, for RecordRead$ + int32_t rc = (fileName && fileName[0]) ? audioRecFile(ctx, fileName, &fmt, source, &rec) : audioRecOpen(ctx, &fmt, source, 0, &rec); if (rc == AUDIO_OK) { sRec = rec; @@ -809,6 +1089,16 @@ int32_t RecordStop(void) { } +// A loaded sound's recorded bits (8 or 16), channels (1 or 2) and rate +// (hertz); 0 for no such sound. +int32_t SoundBits(int32_t sound) { + AudioFormatT fmt; + + basSampleFormat(sound, &fmt); + return (int32_t)fmt.bits; +} + + // What the sound device can do (opens the program's context, so the // device is started): "dac", "synth", "tone", "running", "record" True / // False; "voices", "rate", "bits", "channels" numbers; "recordsources" @@ -857,6 +1147,14 @@ int32_t SoundCaps(const char *field) { } +int32_t SoundChannels(int32_t sound) { + AudioFormatT fmt; + + basSampleFormat(sound, &fmt); + return (int32_t)fmt.channels; +} + + // A sound made from raw PCM bytes (8-bit unsigned or 16-bit signed // little-endian, stereo interleaved left, right); a trailing partial // frame is ignored. @@ -888,6 +1186,17 @@ int32_t SoundCreate(const uint8_t *pcm, int32_t len, int32_t rate, int32_t bits, } +// The short name of the sound driver running ("sb", "speaker" ...; "" +// when none runs). +const char *SoundDriver(void) { + AudioInfoT info; + + audioBasicContext(); + audioGetInfo(&info); + return basTextSet(&sText, info.driver); +} + + // The driver name DVX.INI uses for "find the best one". const char *SoundDriverAuto(void) { return AUDIO_DRIVER_AUTO; @@ -912,6 +1221,33 @@ const char *SoundDriverName(int32_t index) { } +// The next voice of the calling program that ended since it was last +// asked (each once, in the order they ended), 0 when none has. +int32_t SoundEnded(void) { + const AudioCtxT *ctx = audioBasicContext(); + + for (int32_t i = 0; ctx && i < sEndedCount; i++) { + if (sEnded[i].ctx == ctx) { + int32_t voice = sEnded[i].voice; + + sEndReason = sEnded[i].reason; + memmove(&sEnded[i], &sEnded[i + 1], (size_t)(sEndedCount - i - 1) * sizeof(sEnded[0])); + sEndedCount--; + return voice; + } + } + + return 0; +} + + +// Why the voice SoundEnded gave last ended: SOUND_END_FINISHED, +// SOUND_END_STOPPED or SOUND_END_DROPPED. +int32_t SoundEndReason(void) { + return sEndReason; +} + + const char *SoundError(void) { return audioErrorText(sLastError); } @@ -926,10 +1262,10 @@ int32_t SoundEventCount(void) { // name alone (a file in the sound folders) or a path, in native // separators. const char *SoundEventFile(int32_t prefs, int32_t eventIdx) { - static char file[DVX_MAX_PATH]; + char file[DVX_MAX_PATH]; snprintf(file, sizeof(file), "%s", audioDeskFile(prefsBasHandle(prefs), (DvxSoundE)eventIdx)); - return platformPathNative(file); + return basTextSet(&sText, platformPathNative(file)); } @@ -958,9 +1294,9 @@ void SoundEventsReload(void) { // The folder of DVX's own sound files (SHARED\SOUNDS; the user's are in // FileOpsKindDir$(FILEOPS_KIND_SOUNDS, True)). const char *SoundFolder(void) { - static char dir[DVX_MAX_PATH]; + char dir[DVX_MAX_PATH]; - return platformKindDir(dir, sizeof(dir), DvxKindSoundsE, false) ? dir : ""; + return basTextSet(&sText, platformKindDir(dir, sizeof(dir), DvxKindSoundsE, false) ? dir : ""); } @@ -992,12 +1328,10 @@ const char *SoundInfo(void) { audioGetInfo(&info); if (!info.running) { - snprintf(sInfo, sizeof(sInfo), "No sound device"); - } else { - snprintf(sInfo, sizeof(sInfo), "%s, %lu Hz, %lu-bit %s", info.description, (unsigned long)info.rate, (unsigned long)info.bits, info.channels > 1 ? "stereo" : "mono"); + return basTextSet(&sText, "No sound device"); } - return sInfo; + return basTextFormat(&sText, "%s, %lu Hz, %lu-bit %s", info.description, (unsigned long)info.rate, (unsigned long)info.bits, info.channels > 1 ? "stereo" : "mono"); } @@ -1035,6 +1369,22 @@ int32_t SoundLoad(const char *fileName) { } +// A WAV or VOC sound from the bytes of its file (a resource, an HTTP +// reply): its number, or 0 (SoundError$ says why). +int32_t SoundLoadData(const uint8_t *data, int32_t len) { + AudioCtxT *ctx = audioBasicContext(); + AudioSampleT *sample = NULL; + + if (!ctx) { + return basResult(AUDIO_ERR_NO_DEVICE); + } + + int32_t rc = audioSampleLoadMem(ctx, data, len > 0 ? (uint32_t)len : 0, &sample); + + return rc < 0 ? basResult(rc) : basNumber(sSamples, sample, ctx, 0); +} + + // The master volume of all sound, percent. int32_t SoundMasterVolume(void) { return basPercent(audioGetMasterVolume()); @@ -1114,6 +1464,14 @@ int32_t SoundPlaying(int32_t voice) { } +int32_t SoundRate(int32_t sound) { + AudioFormatT fmt; + + basSampleFormat(sound, &fmt); + return (int32_t)fmt.rate; +} + + void SoundResume(int32_t voice) { AudioCtxT *ctx = audioBasicContext(); @@ -1212,6 +1570,42 @@ void SoundSetVolume(int32_t voice, int32_t volume) { } +// The mixer's counters since the device started, by name (any case): +// "renders", "frames", "underruns", "clipped", "dropped", "playing"; 0 +// for another name. +int32_t SoundStats(const char *field) { + AudioStatsT stats; + + audioBasicContext(); + audioGetStats(&stats); + + switch ((SoundStatE)basField(field, sStatNames, StatCountE)) { + case StatRendersE: + return (int32_t)stats.renders; + + case StatFramesE: + return (int32_t)stats.frames; + + case StatUnderrunsE: + return (int32_t)stats.underruns; + + case StatClippedE: + return (int32_t)stats.clipped; + + case StatDroppedE: + return (int32_t)stats.dropped; + + case StatPlayingE: + return (int32_t)stats.playing; + + case StatCountE: + break; + } + + return 0; +} + + void SoundStreamClose(int32_t stream) { BasSlotT *s = basSlot(sStreams, stream); @@ -1224,6 +1618,17 @@ void SoundStreamClose(int32_t stream) { // What the stream holds and has not played yet dropped. +// No more will be written: the stream's voice ends once it has played +// what it holds (audioStreamEnd). +void SoundStreamEnd(int32_t stream) { + BasSlotT *s = basSlot(sStreams, stream); + + if (s) { + audioStreamEnd((AudioStreamT *)s->obj); + } +} + + void SoundStreamFlush(int32_t stream) { BasSlotT *s = basSlot(sStreams, stream); @@ -1344,10 +1749,17 @@ int32_t WtActive(int32_t voice) { } +// A voice, its note plain (no envelope or LFO shapes it) until WtShape +// sets one. int32_t WtAlloc(void) { - AudioCtxT *ctx = audioBasicContext(); + AudioCtxT *ctx = audioBasicContext(); + int32_t voice = ctx ? audioWtAlloc(ctx) : AUDIO_ERR_NO_DEVICE; - return basResult(ctx ? audioWtAlloc(ctx) : AUDIO_ERR_NO_DEVICE); + if (voice > 0) { + basWtTake(ctx, voice); + } + + return basResult(voice); } @@ -1373,8 +1785,7 @@ const char *WtInfo(const char *field) { switch ((WtFieldE)f) { case WtDescriptionE: - snprintf(sInfo, sizeof(sInfo), "%s", info.description); - return sInfo; + return basTextSet(&sText, info.description); case WtVoicesE: return basText((int32_t)info.voices); @@ -1388,6 +1799,9 @@ const char *WtInfo(const char *field) { case WtFreeMemoryE: return basText((int32_t)info.freeMemory); + case WtFreeTotalE: + return basText((int32_t)info.freeTotal); + case WtLevelE: return basText(info.level); @@ -1441,6 +1855,40 @@ int32_t WtSetPitch(int32_t voice, int32_t pitch) { } +// One value of the shape of a voice's notes, by name (any case; see +// sound.bas): kept for its next WtStart, and changed at once on a note +// sounding where the chip can (filter, effects, LFO depths). True when +// it worked. +int32_t WtShape(int32_t voice, const char *field, int32_t value) { + AudioCtxT *ctx = audioBasicContext(); + BasWtNoteT *w = basWtNote(ctx, voice); + int32_t f = -1; + + for (int32_t i = 0; field && i < DVX_ARRAY_LEN(sWtShapes); i++) { + if (strcasecmp(field, sWtShapes[i].name) == 0) { + f = i; + } + } + + if (!w || f < 0) { + return basOk(w ? AUDIO_ERR_ARG : AUDIO_ERR_VOICE); + } + + const BasWtShapeT *sh = &sWtShapes[f]; + uint8_t *at = (uint8_t *)&w->note + sh->offset; + + if (sh->size == sizeof(int16_t)) { + int16_t v16 = (int16_t)DVX_MAX(INT16_MIN, DVX_MIN(value, INT16_MAX)); + + memcpy(at, &v16, sizeof(v16)); + } else { + memcpy(at, &value, sizeof(value)); + } + + return basOk((sh->what && audioWtActive(ctx, voice)) ? audioWtUpdate(ctx, voice, &w->note, sh->what) : AUDIO_OK); +} + + // A plain note (audioWtNoteInit) on a sample in the memory, kept for the // Set functions. int32_t WtStart(int32_t voice, int32_t address, int32_t startFrame, int32_t endFrame, int32_t loopStart, int32_t loopEnd, int32_t loopMode, int32_t bits, int32_t pitch, int32_t attenuation, int32_t pan) { @@ -1451,21 +1899,20 @@ int32_t WtStart(int32_t voice, int32_t address, int32_t startFrame, int32_t endF return basOk(AUDIO_ERR_NO_DEVICE); } - if (address < 0 || startFrame < 0 || startFrame > endFrame || loopStart < 0 || loopEnd < loopStart || loopEnd > endFrame || (loopMode != AUDIO_WT_LOOP_NONE && loopMode != AUDIO_WT_LOOP_ON && loopMode != AUDIO_WT_LOOP_RELEASE) || (bits != BAS_WT_BITS_8 && bits != BAS_WT_BITS_16)) { + // A negative address is in the ROM (WT_ADDR_ROM + bytes from its start) + if (startFrame < 0 || startFrame > endFrame || loopStart < 0 || loopEnd < loopStart || loopEnd > endFrame || (loopMode != AUDIO_WT_LOOP_NONE && loopMode != AUDIO_WT_LOOP_ON && loopMode != AUDIO_WT_LOOP_RELEASE) || (bits != BAS_WT_BITS_8 && bits != BAS_WT_BITS_16)) { return basOk(AUDIO_ERR_ARG); } - for (int32_t i = 0; !w && i < AUDIO_WT_VOICES_MAX; i++) { - w = sWtNotes[i].ctx ? NULL : &sWtNotes[i]; - } + w = w ? w : basWtTake(ctx, voice); if (!w) { return basOk(AUDIO_ERR_NO_VOICE); } + // The sample and the note's own values; the shape (WtShape) stays AudioWtVoiceT *n = &w->note; - audioWtNoteInit(n); n->addr = (uint32_t)address; n->offset = (uint32_t)startFrame; n->end = (uint32_t)endFrame; @@ -1477,11 +1924,7 @@ int32_t WtStart(int32_t voice, int32_t address, int32_t startFrame, int32_t endF n->attenuation = attenuation < 0 ? 0 : (attenuation > AUDIO_WT_ATTEN_MAX ? AUDIO_WT_ATTEN_MAX : attenuation); n->pan = basScale(pan, BAS_PAN_MIN, BAS_PAN_MAX, AUDIO_WT_PAN_MAX); - int32_t rc = audioWtStart(ctx, voice, n); - - w->ctx = rc == AUDIO_OK ? ctx : NULL; - w->voice = voice; - return basOk(rc); + return basOk(audioWtStart(ctx, voice, n)); } diff --git a/src/libs/kpunch/audio/audioDesk.c b/src/libs/kpunch/audio/audioDesk.c index c599f625..413da9c9 100644 --- a/src/libs/kpunch/audio/audioDesk.c +++ b/src/libs/kpunch/audio/audioDesk.c @@ -24,15 +24,17 @@ // // DVX calls audioDeskPlay (the sound hook, set by audioDeskInit) for an // event; the event's file comes from DVX.INI [sounds], read at the first -// sound and again after audioDeskReload. The sounds play on a context -// opened for no program, so no program's ending cuts one short. Once the -// exit sound has played DVX is ending: later events (windows closing as -// the programs are ended) stay quiet. +// sound and again after audioDeskReload, and is decoded once (kept until +// then: a window opening or a message box does not read the disk). The +// sounds play on a context opened for no program, so no program's ending +// cuts one short. Once the exit sound has played DVX is ending: later +// events (windows closing as the programs are ended) stay quiet. #include "audioInt.h" #include "dvxPrefs.h" #include +#include // Longest a caller waits for a sound to play (dvxPlaySoundWait) #define DESK_WAIT_MAX_US (5 * PLATFORM_US_PER_SEC) @@ -54,11 +56,12 @@ static const DeskEventT sEvents[SoundCountE] = { [SoundWindowCloseE] = { "windowClose", "Close window" } }; -static AudioCtxT *sDeskCtx = NULL; -static char sFiles[SoundCountE][DVX_MAX_PATH]; -static bool sLoaded = false; -static bool sEnding = false; -static bool sNoDevice = false; // no sound device started: not tried again until a reload +static AudioCtxT *sDeskCtx = NULL; +static char sFiles[SoundCountE][DVX_MAX_PATH]; +static AudioSampleT *sSounds[SoundCountE]; // decoded (NULL: not yet, or it did not load) +static bool sLoaded = false; +static bool sEnding = false; +static bool sNoDevice = false; // no sound device started: not tried again until a reload // ============================================================ // Prototypes @@ -73,8 +76,11 @@ int32_t audioDeskPlayFile(const char *path); void audioDeskReload(void); void audioDeskSetFile(PrefsHandleT *prefs, DvxSoundE event, const char *path); void audioDeskShutdown(void); -static bool eventValid(DvxSoundE event); -static void filesLoad(void); +static bool deskOpen(void); +static int32_t deskPlay(DvxSoundE event); +static bool eventValid(DvxSoundE event); +static void filesLoad(void); +static void soundsFree(void); const char *audioDeskFile(PrefsHandleT *prefs, DvxSoundE event) { @@ -109,7 +115,7 @@ void audioDeskPlay(DvxSoundE event, bool wait) { return; } - int32_t voice = audioDeskPlayFile(sFiles[event]); + int32_t voice = deskPlay(event); sEnding = event == SoundExitE; @@ -121,24 +127,21 @@ void audioDeskPlay(DvxSoundE event, bool wait) { while (audioVoiceIsPlaying(sDeskCtx, voice) && platformMicros() - start < DESK_WAIT_MAX_US) { audioPump(); - platformYield(); + + if (!dvxWaitPump()) { + return; + } } } // Everything is done as no program, so the memory a sound takes is the -// library's own. When no sound device starts (audioOpen logs why) the -// sounds stay silent without trying again at every event. +// library's own. int32_t audioDeskPlayFile(const char *path) { - int32_t prev = audioAppEnter(0); + int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); int32_t rc = AUDIO_ERR_NO_DEVICE; - if (!sDeskCtx && !sNoDevice) { - sDeskCtx = audioOpen(); - sNoDevice = !sDeskCtx; - } - - if (sDeskCtx) { + if (deskOpen()) { rc = audioPlayFile(sDeskCtx, path, AUDIO_VOLUME_MAX, AUDIO_PAN_CENTER, false); if (rc < 0) { @@ -146,12 +149,14 @@ int32_t audioDeskPlayFile(const char *path) { } } - audioAppEnter(prev); + dvxMemSetApp(prev); return rc; } +// The decoded sounds go too: the next event reads its file again. void audioDeskReload(void) { + soundsFree(); sLoaded = false; sNoDevice = false; } @@ -168,12 +173,49 @@ void audioDeskSetFile(PrefsHandleT *prefs, DvxSoundE event, const char *path) { // take the hook back. void audioDeskShutdown(void) { dvxSetSoundHook(NULL); + memset(sSounds, 0, sizeof(sSounds)); sDeskCtx = NULL; sLoaded = false; sNoDevice = false; } +// The desktop's context, opened at the first sound (as no program: the +// caller entered app 0). When no sound device starts (audioOpen logs +// why) the sounds stay silent without trying again at every event. +static bool deskOpen(void) { + if (!sDeskCtx && !sNoDevice) { + sDeskCtx = audioOpen(); + sNoDevice = !sDeskCtx; + } + + return sDeskCtx != NULL; +} + + +// The event's sound, decoded at its first play and kept, played as no +// program. The voice, or an error (logged). +static int32_t deskPlay(DvxSoundE event) { + int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); + int32_t rc = AUDIO_ERR_NO_DEVICE; + + if (deskOpen()) { + rc = sSounds[event] ? AUDIO_OK : audioSampleLoad(sDeskCtx, sFiles[event], &sSounds[event]); + + if (rc == AUDIO_OK) { + rc = audioPlay(sSounds[event], AUDIO_VOLUME_MAX, AUDIO_PAN_CENTER, false); + } + + if (rc < 0) { + dvxLog("Audio: desktop sound %s: %s", sFiles[event], audioErrorText(rc)); + } + } + + dvxMemSetApp(prev); + return rc; +} + + static bool eventValid(DvxSoundE event) { return (int32_t)event >= 0 && event < SoundCountE; } @@ -184,7 +226,7 @@ static void filesLoad(void) { return; } - PrefsHandleT *prefs = prefsLoad(platformDvxIniPath()); + PrefsHandleT *prefs = prefsDvxIniShared(); // A name is found in the sound folders; one not found is kept as it // is, so playing it logs what is missing @@ -196,6 +238,14 @@ static void filesLoad(void) { } } - prefsClose(prefs); sLoaded = true; } + + +// Let the decoded sounds go (their voices stop). +static void soundsFree(void) { + for (int32_t i = 0; i < SoundCountE; i++) { + audioSampleFree(sSounds[i]); + sSounds[i] = NULL; + } +} diff --git a/src/libs/kpunch/audio/audioDrv.h b/src/libs/kpunch/audio/audioDrv.h index 790a83fe..845e0397 100644 --- a/src/libs/kpunch/audio/audioDrv.h +++ b/src/libs/kpunch/audio/audioDrv.h @@ -281,6 +281,34 @@ void audioConvert(const void *src, int16_t *dst, uint32_t count, uint32_t bit int32_t audioDecode(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t *frames, AudioFormatT *fmt); int32_t audioWavParse(const uint8_t *head, uint32_t headBytes, uint32_t fileBytes, AudioFormatT *fmt, uint32_t *dataAt, uint32_t *dataBytes); +// A VOC file's sound a part at a time, for a caller that plays it as it +// reads it (audioDecode walks it the same way): read gives len bytes of +// the file from at (returns how many it read). audioVocBegin checks the +// file's header (AUDIO_ERR_FORMAT: not a VOC file). Each audioVocNext +// gives the next part: *count samples of the sound from byte *at +// (AudioVocDataE), *count samples of silence, or the end; fmt is the +// format the parts so far set (haveFmt), the same for all of them. +typedef enum { + AudioVocDataE, + AudioVocSilenceE, + AudioVocEndE +} AudioVocPartE; + +typedef struct { + uint32_t (*read)(void *ctx, uint32_t at, uint8_t *buf, uint32_t len); + void *ctx; + uint32_t size; // the file's bytes + uint32_t pos; // the next block's header + AudioFormatT fmt; + uint32_t extRate; // an extended block's format, for the next sound block + uint32_t extChans; + bool extPending; + bool haveFmt; +} AudioVocT; + +int32_t audioVocBegin(AudioVocT *v); +int32_t audioVocNext(AudioVocT *v, AudioVocPartE *kind, uint32_t *at, uint32_t *count); + // Interrupts off / back as they were, around the few instructions that // must not be split by an interrupt (an FM chip's index/data pair, a // table an interrupt also changes). Inline, so code that runs at diff --git a/src/libs/kpunch/audio/audioInt.h b/src/libs/kpunch/audio/audioInt.h index 5d51a835..bb15d0a0 100644 --- a/src/libs/kpunch/audio/audioInt.h +++ b/src/libs/kpunch/audio/audioInt.h @@ -38,8 +38,9 @@ // Frames mixed per pass (the accumulator's length) // Silence after which the device's output is closed until the next voice -// (audio.c devIdle) -#define AUDIO_IDLE_CLOSE_SEC 3 +// (audio.c devIdle): opening it again costs a sound 25-60 ms (the card set +// up, its first interrupt awaited), so sounds a few seconds apart keep it +#define AUDIO_IDLE_CLOSE_SEC 10 #define MIX_CHUNK_FRAMES 256 #define MIX_ACC_CHANNELS 2 @@ -84,16 +85,20 @@ typedef struct { volatile uint32_t head; // written by the program side volatile uint32_t tail; // advanced by the mixer volatile bool fed; // samples have arrived at least once + volatile bool ended; // no more will come (audioStreamEnd): empty is the end } AudioRingT; // A note queue (audioNotesOpen): tones and rests played one after the // other. An entry is a tone of phaseStep (hz for a tone generator) for -// frames output frames; hz 0 is a rest. head counts entries written, +// frames output frames, in wave at level (a share of the voice's volume, +// 0..AUDIO_VOLUME_MAX); hz 0 is a rest. head counts entries written, // tail entries the mixer has started. typedef struct { uint32_t phaseStep; uint32_t hz; uint32_t frames; + uint16_t level; + uint8_t wave; } MixNoteT; typedef struct { @@ -119,6 +124,7 @@ typedef struct { volatile int32_t gainL; // 0..AUDIO_VOLUME_MAX: volume x pan x context x master volatile int32_t gainR; uint32_t phase; // tones: waveform position (32-bit turn) + uint32_t level; // note queues: the playing entry's share of the volume (0..AUDIO_VOLUME_MAX) volatile uint32_t phaseStep; // tones: phase added per output frame uint32_t lfsr; // noise shift register volatile uint32_t remain; // timed tones, note queues: output frames left @@ -170,6 +176,11 @@ typedef struct { extern AudioRecRingT *volatile audioRecRing; +// Let go of the last device's parked driver modules (audio.c): the next +// device detects from scratch, as at DVX's start (the library's unload, +// and tests that give DVX.INI new settings). +void audioDevForget(void); + // Mixer (audioMix.c). Prime loads a voice's first two source frames // before it is marked playing; Lock pins the mixer's code and data; // Reset empties it for a device with this format. @@ -217,11 +228,12 @@ bool audioWtLock(bool lock); // The library's end of a program's life, of the shell's idle time, of a // DOS program run with DVX stepped aside (audioSuspend, a platform suspend // hook) and of its own unloading (audio.c; audioShell.c hooks them up), -// the calling BASIC program's context, and making appId the current -// program (returns the one before). audioBasForget (audioBas.c) drops the -// BASIC numbers of a closing BASIC context's sounds and streams. +// and the calling BASIC program's context (a program's callback is called +// as that program: dvxMemSetApp). audioBasForget (audioBas.c) drops the +// BASIC numbers of a closing BASIC context's sounds and streams, and +// audioBasEnded keeps a BASIC context's ended voices for SoundEnded. void audioAppEnded(int32_t appId); -int32_t audioAppEnter(int32_t appId); +void audioBasEnded(AudioCtxT *ctx, int32_t voice, AudioEndE reason, void *userData); void audioBasForget(const AudioCtxT *ctx); AudioCtxT *audioBasicContext(void); void audioIdle(void *unused); diff --git a/src/libs/kpunch/audio/audioLoad.c b/src/libs/kpunch/audio/audioLoad.c index 3992a98b..08127edd 100644 --- a/src/libs/kpunch/audio/audioLoad.c +++ b/src/libs/kpunch/audio/audioLoad.c @@ -42,6 +42,7 @@ // format. #include "audioInt.h" +#include "dvxEndian.h" #include #include @@ -88,6 +89,7 @@ #define VOC_TC16_BASE 65536 #define VOC_TC16_CLOCK 256000000 #define VOC_MODE_STEREO 1 +#define VOC_BODY_MAX VOC_NEW_HEADER_BYTES // a block's bytes read for its fields // Sample formats #define BITS_8 8 @@ -97,17 +99,12 @@ #define U8_SCALE 256 // 8-bit sample to 16-bit #define CHANNELS_MAX 2 #define BYTE_MASK 0xFF -#define SHIFT_8 8 -#define SHIFT_16 16 -#define SHIFT_24 24 // VOC output while it is assembled typedef struct { int16_t *pcm; uint32_t count; // samples stored uint32_t cap; // samples allocated - AudioFormatT fmt; - bool haveFmt; // a sound or silence block set the format } VocOutT; // ============================================================ @@ -116,15 +113,15 @@ typedef struct { void audioConvert(const void *src, int16_t *dst, uint32_t count, uint32_t bits); int32_t audioDecode(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t *frames, AudioFormatT *fmt); +int32_t audioVocBegin(AudioVocT *v); +int32_t audioVocNext(AudioVocT *v, AudioVocPartE *kind, uint32_t *at, uint32_t *count); int32_t audioWavParse(const uint8_t *head, uint32_t headBytes, uint32_t fileBytes, AudioFormatT *fmt, uint32_t *dataAt, uint32_t *dataBytes); static int32_t checkFormat(const AudioFormatT *fmt); static int32_t decodeVoc(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t *frames, AudioFormatT *fmt); static int32_t decodeWav(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t *frames, AudioFormatT *fmt); -static uint32_t rd16(const uint8_t *p); -static uint32_t rd24(const uint8_t *p); -static uint32_t rd32(const uint8_t *p); -static int32_t vocAppend(VocOutT *out, const uint8_t *src, uint32_t bytes, uint32_t silence); -static int32_t vocSetFormat(VocOutT *out, uint32_t rate, uint32_t bits, uint32_t channels); +static int32_t vocAppend(VocOutT *out, const uint8_t *src, uint32_t count, uint32_t bits); +static uint32_t vocMemRead(void *ctx, uint32_t at, uint8_t *buf, uint32_t len); +static int32_t vocSetFormat(AudioVocT *v, uint32_t rate, uint32_t bits, uint32_t channels); void audioConvert(const void *src, int16_t *dst, uint32_t count, uint32_t bits) { @@ -139,7 +136,7 @@ void audioConvert(const void *src, int16_t *dst, uint32_t count, uint32_t bits) } for (uint32_t i = 0; i < count; i++) { - dst[i] = (int16_t)rd16(&s[i * 2]); + dst[i] = (int16_t)dvxGetLe16(&s[i * 2]); } } @@ -160,6 +157,161 @@ int32_t audioDecode(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t } +int32_t audioVocBegin(AudioVocT *v) { + uint8_t head[VOC_HEADER_MIN_BYTES]; + + if (v->size < VOC_HEADER_MIN_BYTES || v->read(v->ctx, 0, head, VOC_HEADER_MIN_BYTES) != VOC_HEADER_MIN_BYTES || memcmp(head, VOC_SIGNATURE, VOC_SIGNATURE_BYTES) != 0) { + return v->size < VOC_HEADER_MIN_BYTES ? AUDIO_ERR_CORRUPT : AUDIO_ERR_FORMAT; + } + + uint32_t headerBytes = dvxGetLe16(head + VOC_HEADER_SIZE_OFFSET); + uint32_t version = dvxGetLe16(head + VOC_VERSION_OFFSET); + + if (dvxGetLe16(head + VOC_CHECK_OFFSET) != ((~version + VOC_CHECK_MAGIC) & 0xFFFF) || headerBytes < VOC_HEADER_MIN_BYTES || headerBytes > v->size) { + return AUDIO_ERR_CORRUPT; + } + + v->pos = headerBytes; + v->haveFmt = false; + v->extPending = false; + return AUDIO_OK; +} + + +// A missing terminator block ends the file where the data ends. Blocks +// that hold no sound (an extended block's format, markers, text, repeats) +// are passed over. +int32_t audioVocNext(AudioVocT *v, AudioVocPartE *kind, uint32_t *at, uint32_t *count) { + *kind = AudioVocEndE; + *at = 0; + *count = 0; + + for (;;) { + uint8_t hdr[VOC_BLOCK_HEADER_BYTES]; + uint8_t body[VOC_BODY_MAX]; + + if (v->pos >= v->size) { + return AUDIO_OK; + } + + if (v->read(v->ctx, v->pos, hdr, 1) != 1) { + return AUDIO_ERR_CORRUPT; + } + + if (hdr[0] == VOC_BLOCK_END) { + return AUDIO_OK; + } + + if (v->size - v->pos < VOC_BLOCK_HEADER_BYTES || v->read(v->ctx, v->pos, hdr, VOC_BLOCK_HEADER_BYTES) != VOC_BLOCK_HEADER_BYTES) { + return AUDIO_ERR_CORRUPT; + } + + uint32_t type = hdr[0]; + uint32_t len = dvxGetLe24(hdr + 1); + uint32_t start = v->pos + VOC_BLOCK_HEADER_BYTES; + + if (len > v->size - start) { + return AUDIO_ERR_CORRUPT; + } + + uint32_t fields = len < VOC_BODY_MAX ? len : VOC_BODY_MAX; + + if (v->read(v->ctx, start, body, fields) != fields) { + return AUDIO_ERR_CORRUPT; + } + + v->pos = start + len; + + switch (type) { + case VOC_BLOCK_SOUND: { + if (len < VOC_SOUND_HEADER_BYTES) { + return AUDIO_ERR_CORRUPT; + } + + uint32_t rate = VOC_TC_CLOCK / (VOC_TC_BASE - body[0]); + uint32_t chans = 1; + + if (v->extPending) { + rate = v->extRate; + chans = v->extChans; + v->extPending = false; + } + + if (body[1] != VOC_CODEC_PCM8) { + return AUDIO_ERR_UNSUPPORTED; + } + + int32_t rc = vocSetFormat(v, rate, BITS_8, chans); + + *kind = AudioVocDataE; + *at = start + VOC_SOUND_HEADER_BYTES; + *count = len - VOC_SOUND_HEADER_BYTES; + return rc; + } + + case VOC_BLOCK_CONTINUE: + if (!v->haveFmt) { + return AUDIO_ERR_CORRUPT; + } + + *kind = AudioVocDataE; + *at = start; + *count = len / (v->fmt.bits / BITS_PER_BYTE); + return AUDIO_OK; + + case VOC_BLOCK_SILENCE: { + if (len < VOC_SILENCE_BYTES) { + return AUDIO_ERR_CORRUPT; + } + + int32_t rc = v->haveFmt ? AUDIO_OK : vocSetFormat(v, VOC_TC_CLOCK / (VOC_TC_BASE - body[2]), BITS_8, 1); + + *kind = AudioVocSilenceE; + *count = (dvxGetLe16(body) + 1) * v->fmt.channels; + return rc; + } + + case VOC_BLOCK_EXTENDED: + if (len < VOC_EXTENDED_BYTES) { + return AUDIO_ERR_CORRUPT; + } + + if (body[3] > VOC_MODE_STEREO || body[2] != VOC_CODEC_PCM8) { + return AUDIO_ERR_UNSUPPORTED; + } + + v->extChans = (uint32_t)body[3] + 1; + v->extRate = VOC_TC16_CLOCK / (VOC_TC16_BASE - dvxGetLe16(body)) / v->extChans; + v->extPending = true; + break; + + case VOC_BLOCK_NEW_SOUND: { + if (len < VOC_NEW_HEADER_BYTES) { + return AUDIO_ERR_CORRUPT; + } + + uint32_t codec = dvxGetLe16(body + 6); + uint32_t bits = body[4]; + + if (!((codec == VOC_CODEC_PCM8 && bits == BITS_8) || (codec == VOC_CODEC_PCM16 && bits == BITS_16))) { + return AUDIO_ERR_UNSUPPORTED; + } + + int32_t rc = vocSetFormat(v, dvxGetLe32(body), bits, body[5]); + + *kind = AudioVocDataE; + *at = start + VOC_NEW_HEADER_BYTES; + *count = (len - VOC_NEW_HEADER_BYTES) / (bits / BITS_PER_BYTE); + return rc; + } + + default: + break; + } + } +} + + // The chunks walked from the start (a chunk that claims more than the // file has is damaged, but for the data chunk: a recorder that stopped // early leaves one, and what is there plays). The 'fmt ' chunk must lie @@ -179,7 +331,7 @@ int32_t audioWavParse(const uint8_t *head, uint32_t headBytes, uint32_t fileByte while (pos + CHUNK_HEADER_BYTES <= headBytes && !(fmtChunk && haveData)) { const uint8_t *id = head + pos; - uint32_t len = rd32(head + pos + CHUNK_SIZE_OFFSET); + uint32_t len = dvxGetLe32(head + pos + CHUNK_SIZE_OFFSET); uint32_t avail = fileBytes - pos - CHUNK_HEADER_BYTES; if (memcmp(id, "data", CHUNK_ID_BYTES) == 0) { @@ -208,15 +360,15 @@ int32_t audioWavParse(const uint8_t *head, uint32_t headBytes, uint32_t fileByte return AUDIO_ERR_CORRUPT; } - uint32_t tag = rd16(fmtChunk); + uint32_t tag = dvxGetLe16(fmtChunk); if (tag == WAV_TAG_EXTENSIBLE && fmtBytes >= WAV_EXT_FMT_BYTES) { - tag = rd16(fmtChunk + WAV_EXT_SUBFORMAT); + tag = dvxGetLe16(fmtChunk + WAV_EXT_SUBFORMAT); } - fmt->channels = rd16(fmtChunk + WAV_FMT_CHANNELS); - fmt->rate = rd32(fmtChunk + WAV_FMT_RATE); - fmt->bits = rd16(fmtChunk + WAV_FMT_BITS); + fmt->channels = dvxGetLe16(fmtChunk + WAV_FMT_CHANNELS); + fmt->rate = dvxGetLe32(fmtChunk + WAV_FMT_RATE); + fmt->bits = dvxGetLe16(fmtChunk + WAV_FMT_BITS); if (tag != WAV_TAG_PCM || checkFormat(fmt) != AUDIO_OK) { return AUDIO_ERR_UNSUPPORTED; @@ -224,7 +376,7 @@ int32_t audioWavParse(const uint8_t *head, uint32_t headBytes, uint32_t fileByte uint32_t frameBytes = fmt->channels * fmt->bits / BITS_PER_BYTE; - if (rd16(fmtChunk + WAV_FMT_BLOCK_ALIGN) != frameBytes || *dataBytes < frameBytes) { + if (dvxGetLe16(fmtChunk + WAV_FMT_BLOCK_ALIGN) != frameBytes || *dataBytes < frameBytes) { return AUDIO_ERR_CORRUPT; } @@ -243,139 +395,25 @@ static int32_t checkFormat(const AudioFormatT *fmt) { static int32_t decodeVoc(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t *frames, AudioFormatT *fmt) { - if (size < VOC_HEADER_MIN_BYTES) { - return AUDIO_ERR_CORRUPT; - } - - uint32_t headerBytes = rd16(data + VOC_HEADER_SIZE_OFFSET); - uint32_t version = rd16(data + VOC_VERSION_OFFSET); - - if (rd16(data + VOC_CHECK_OFFSET) != ((~version + VOC_CHECK_MAGIC) & 0xFFFF) || headerBytes < VOC_HEADER_MIN_BYTES || headerBytes > size) { - return AUDIO_ERR_CORRUPT; - } - - VocOutT out; - uint32_t pos = headerBytes; - uint32_t extRate = 0; - uint32_t extChans = 0; - bool extPending = false; - int32_t rc = AUDIO_OK; + AudioVocT v; + VocOutT out; + AudioVocPartE kind = AudioVocDataE; + uint32_t at; + uint32_t count; + memset(&v, 0, sizeof(v)); memset(&out, 0, sizeof(out)); + v.read = vocMemRead; + v.ctx = (void *)data; + v.size = size; - // A missing terminator block ends the file where the data ends. - while (rc == AUDIO_OK && pos < size && data[pos] != VOC_BLOCK_END) { - if (size - pos < VOC_BLOCK_HEADER_BYTES) { - rc = AUDIO_ERR_CORRUPT; - break; - } + int32_t rc = audioVocBegin(&v); - uint32_t type = data[pos]; - uint32_t len = rd24(data + pos + 1); - const uint8_t *body = data + pos + VOC_BLOCK_HEADER_BYTES; - - if (len > size - pos - VOC_BLOCK_HEADER_BYTES) { - rc = AUDIO_ERR_CORRUPT; - break; - } - - switch (type) { - case VOC_BLOCK_SOUND: { - if (len < VOC_SOUND_HEADER_BYTES) { - rc = AUDIO_ERR_CORRUPT; - break; - } - - uint32_t rate = VOC_TC_CLOCK / (VOC_TC_BASE - body[0]); - uint32_t chans = 1; - - if (extPending) { - rate = extRate; - chans = extChans; - extPending = false; - } - - if (body[1] != VOC_CODEC_PCM8) { - rc = AUDIO_ERR_UNSUPPORTED; - break; - } - - rc = vocSetFormat(&out, rate, BITS_8, chans); - - if (rc == AUDIO_OK) { - rc = vocAppend(&out, body + VOC_SOUND_HEADER_BYTES, len - VOC_SOUND_HEADER_BYTES, 0); - } - - break; - } - - case VOC_BLOCK_CONTINUE: - rc = out.haveFmt ? vocAppend(&out, body, len, 0) : AUDIO_ERR_CORRUPT; - break; - - case VOC_BLOCK_SILENCE: - if (len < VOC_SILENCE_BYTES) { - rc = AUDIO_ERR_CORRUPT; - break; - } - - if (!out.haveFmt) { - rc = vocSetFormat(&out, VOC_TC_CLOCK / (VOC_TC_BASE - body[2]), BITS_8, 1); - } - - if (rc == AUDIO_OK) { - rc = vocAppend(&out, NULL, 0, rd16(body) + 1); - } - - break; - - case VOC_BLOCK_EXTENDED: - if (len < VOC_EXTENDED_BYTES || body[3] > VOC_MODE_STEREO) { - rc = len < VOC_EXTENDED_BYTES ? AUDIO_ERR_CORRUPT : AUDIO_ERR_UNSUPPORTED; - break; - } - - extChans = (uint32_t)body[3] + 1; - extRate = VOC_TC16_CLOCK / (VOC_TC16_BASE - rd16(body)) / extChans; - extPending = true; - - if (body[2] != VOC_CODEC_PCM8) { - rc = AUDIO_ERR_UNSUPPORTED; - } - - break; - - case VOC_BLOCK_NEW_SOUND: { - if (len < VOC_NEW_HEADER_BYTES) { - rc = AUDIO_ERR_CORRUPT; - break; - } - - uint32_t codec = rd16(body + 6); - uint32_t bits = body[4]; - - if (!((codec == VOC_CODEC_PCM8 && bits == BITS_8) || (codec == VOC_CODEC_PCM16 && bits == BITS_16))) { - rc = AUDIO_ERR_UNSUPPORTED; - break; - } - - rc = vocSetFormat(&out, rd32(body), bits, body[5]); - - if (rc == AUDIO_OK) { - rc = vocAppend(&out, body + VOC_NEW_HEADER_BYTES, len - VOC_NEW_HEADER_BYTES, 0); - } - - break; - } - - default: - break; - } - - pos += VOC_BLOCK_HEADER_BYTES + len; + while (rc == AUDIO_OK && (rc = audioVocNext(&v, &kind, &at, &count)) == AUDIO_OK && kind != AudioVocEndE) { + rc = vocAppend(&out, kind == AudioVocDataE ? data + at : NULL, count, v.fmt.bits); } - if (rc == AUDIO_OK && (!out.haveFmt || out.count < out.fmt.channels)) { + if (rc == AUDIO_OK && (!v.haveFmt || out.count < v.fmt.channels)) { rc = AUDIO_ERR_CORRUPT; } @@ -385,8 +423,8 @@ static int32_t decodeVoc(const uint8_t *data, uint32_t size, int16_t **pcm, uint } *pcm = out.pcm; - *frames = out.count / out.fmt.channels; - *fmt = out.fmt; + *frames = out.count / v.fmt.channels; + *fmt = v.fmt; return AUDIO_OK; } @@ -414,37 +452,18 @@ static int32_t decodeWav(const uint8_t *data, uint32_t size, int16_t **pcm, uint } -static uint32_t rd16(const uint8_t *p) { - return (uint32_t)p[0] | ((uint32_t)p[1] << SHIFT_8); -} - - -static uint32_t rd24(const uint8_t *p) { - return rd16(p) | ((uint32_t)p[2] << SHIFT_16); -} - - -static uint32_t rd32(const uint8_t *p) { - return rd24(p) | ((uint32_t)p[3] << SHIFT_24); -} - - -// Append bytes of sound in the current format, or silence frames of -// silence (src NULL). A stereo frame may continue in the next block, so -// only the total is cut to whole frames (decodeVoc). -static int32_t vocAppend(VocOutT *out, const uint8_t *src, uint32_t bytes, uint32_t silence) { - uint32_t add = src ? bytes / (out->fmt.bits / BITS_PER_BYTE) : silence * out->fmt.channels; - +// count samples of src (bits each) added to out; silence when src is NULL. +static int32_t vocAppend(VocOutT *out, const uint8_t *src, uint32_t count, uint32_t bits) { // An empty block adds nothing; before the first sound out->pcm is // still NULL, and neither NULL + 0 nor memset(NULL, ..) is defined. - if (add == 0) { + if (count == 0) { return AUDIO_OK; } - if (out->count + add > out->cap) { - uint32_t cap = out->cap ? out->cap : add; + if (out->count + count > out->cap) { + uint32_t cap = out->cap ? out->cap : count; - while (cap < out->count + add) { + while (cap < out->count + count) { cap *= 2; } @@ -459,30 +478,38 @@ static int32_t vocAppend(VocOutT *out, const uint8_t *src, uint32_t bytes, uint3 } if (src) { - audioConvert(src, out->pcm + out->count, add, out->fmt.bits); + audioConvert(src, out->pcm + out->count, count, bits); } else { - memset(out->pcm + out->count, 0, add * sizeof(int16_t)); + memset(out->pcm + out->count, 0, count * sizeof(int16_t)); } - out->count += add; + out->count += count; return AUDIO_OK; } +// audioVoc's read for a VOC image in memory (ctx: its bytes; the walker +// reads only within v->size). +static uint32_t vocMemRead(void *ctx, uint32_t at, uint8_t *buf, uint32_t len) { + memcpy(buf, (const uint8_t *)ctx + at, len); + return len; +} + + // The first sound or silence block sets the format; a later one that // differs is refused. -static int32_t vocSetFormat(VocOutT *out, uint32_t rate, uint32_t bits, uint32_t channels) { +static int32_t vocSetFormat(AudioVocT *v, uint32_t rate, uint32_t bits, uint32_t channels) { AudioFormatT fmt = { rate, bits, channels }; if (checkFormat(&fmt) != AUDIO_OK) { return AUDIO_ERR_UNSUPPORTED; } - if (!out->haveFmt) { - out->fmt = fmt; - out->haveFmt = true; + if (!v->haveFmt) { + v->fmt = fmt; + v->haveFmt = true; return AUDIO_OK; } - return (fmt.rate == out->fmt.rate && fmt.bits == out->fmt.bits && fmt.channels == out->fmt.channels) ? AUDIO_OK : AUDIO_ERR_UNSUPPORTED; + return (fmt.rate == v->fmt.rate && fmt.bits == v->fmt.bits && fmt.channels == v->fmt.channels) ? AUDIO_OK : AUDIO_ERR_UNSUPPORTED; } diff --git a/src/libs/kpunch/audio/audioMix.c b/src/libs/kpunch/audio/audioMix.c index 70b5b8da..20922a73 100644 --- a/src/libs/kpunch/audio/audioMix.c +++ b/src/libs/kpunch/audio/audioMix.c @@ -311,8 +311,9 @@ static void endVoice(MixVoiceT *v, AudioEndE reason) { // Fetch the source frame after next into next. False when a sample that -// does not loop has no more frames; a stream with nothing buffered gives -// silence (counted as an underrun once it has had samples). A source has +// does not loop has no more frames, or an ended stream (audioStreamEnd) +// has played what it held; a stream with nothing buffered gives silence +// (counted as an underrun once it has had samples). A source has // one or two channels (audio.c checks): a frame's index is a shift, not // a multiply. static bool loadNext(MixVoiceT *v) { @@ -320,6 +321,10 @@ static bool loadNext(MixVoiceT *v) { AudioRingT *r = v->ring; if (r->head == r->tail) { + if (r->ended) { + return false; + } + v->nextL = 0; v->nextR = 0; @@ -358,10 +363,8 @@ static bool loadNext(MixVoiceT *v) { // A note queue plays its entries back to back, each for exactly its // frames; with none left it is silent (toneHz 0) until more arrive. static void mixNotes(MixVoiceT *v, int32_t *acc, uint32_t n) { - MixNoteRingT *q = v->notes; - int32_t gainL = v->gainL; - int32_t gainR = v->gainR; - uint32_t i = 0; + MixNoteRingT *q = v->notes; + uint32_t i = 0; while (i < n) { if (v->remain == 0) { @@ -374,11 +377,16 @@ static void mixNotes(MixVoiceT *v, int32_t *acc, uint32_t n) { v->phaseStep = e->phaseStep; v->toneHz = e->hz; v->remain = e->frames; + v->wave = e->wave; + v->level = e->level; q->tail++; continue; } - uint32_t k = v->remain < n - i ? v->remain : n - i; + // The entry's level of the voice's gain + int32_t gainL = (int32_t)(((uint32_t)v->gainL * v->level) >> MIX_GAIN_SHIFT); + int32_t gainR = (int32_t)(((uint32_t)v->gainR * v->level) >> MIX_GAIN_SHIFT); + uint32_t k = v->remain < n - i ? v->remain : n - i; for (uint32_t j = 0; j < k && v->toneHz; j++) { int32_t smp = toneSample(v); diff --git a/src/libs/kpunch/audio/audioRec.c b/src/libs/kpunch/audio/audioRec.c index 45a26ec0..2116f131 100644 --- a/src/libs/kpunch/audio/audioRec.c +++ b/src/libs/kpunch/audio/audioRec.c @@ -35,6 +35,7 @@ // voices that were held play on. #include "audioInt.h" +#include "dvxEndian.h" #include #include @@ -55,10 +56,6 @@ #define PERCENT 100 #define U8_BIAS 128 #define U8_SCALE 256 // an 8-bit sample's step in 16-bit terms -#define BYTE_SHIFT 8 -#define BYTE_MASK 0xFF -#define WORD_SHIFT 16 -#define WORD_MASK 0xFFFF #define WORD_BYTES 2 // WAV: RIFF, the fmt chunk (PCM), the data chunk's header @@ -111,8 +108,6 @@ uint32_t audioRecRead(AudioRecT *rec, void *pcm, uint32_t frames); void audioRecSuspend(void); void audioWavHeader(uint8_t *out, const AudioFormatT *fmt, uint32_t dataBytes); static uint32_t frameBytesOf(const AudioFormatT *fmt); -static void put16(uint8_t *p, uint32_t v); -static void put32(uint8_t *p, uint32_t v); static void putTag(uint8_t *p, const char *tag); static AudioRecT *recAlloc(uint32_t bytes); static int32_t recBegin(AudioCtxT *ctx, AudioFormatT *fmt, int32_t source, uint32_t bufferMs, const char *path, AudioRecT **out); @@ -241,18 +236,18 @@ void audioWavHeader(uint8_t *out, const AudioFormatT *fmt, uint32_t dataBytes) { uint32_t align = frameBytesOf(fmt); putTag(out, "RIFF"); - put32(out + WAV_AT_RIFF_SIZE, dataBytes + WAV_RIFF_EXTRA); + dvxPutLe32(out + WAV_AT_RIFF_SIZE, dataBytes + WAV_RIFF_EXTRA); putTag(out + WAV_AT_WAVE, "WAVE"); putTag(out + WAV_AT_FMT, "fmt "); - put32(out + WAV_AT_FMT_SIZE, WAV_FMT_BYTES); - put16(out + WAV_AT_FORMAT, WAV_FORMAT_PCM); - put16(out + WAV_AT_CHANNELS, fmt->channels); - put32(out + WAV_AT_RATE, fmt->rate); - put32(out + WAV_AT_BYTE_RATE, fmt->rate * align); - put16(out + WAV_AT_ALIGN, align); - put16(out + WAV_AT_BITS, fmt->bits); + dvxPutLe32(out + WAV_AT_FMT_SIZE, WAV_FMT_BYTES); + dvxPutLe16(out + WAV_AT_FORMAT, WAV_FORMAT_PCM); + dvxPutLe16(out + WAV_AT_CHANNELS, fmt->channels); + dvxPutLe32(out + WAV_AT_RATE, fmt->rate); + dvxPutLe32(out + WAV_AT_BYTE_RATE, fmt->rate * align); + dvxPutLe16(out + WAV_AT_ALIGN, align); + dvxPutLe16(out + WAV_AT_BITS, fmt->bits); putTag(out + WAV_AT_DATA, "data"); - put32(out + WAV_AT_DATA_SIZE, dataBytes); + dvxPutLe32(out + WAV_AT_DATA_SIZE, dataBytes); } @@ -261,18 +256,6 @@ static uint32_t frameBytesOf(const AudioFormatT *fmt) { } -static void put16(uint8_t *p, uint32_t v) { - p[0] = (uint8_t)(v & BYTE_MASK); - p[1] = (uint8_t)((v >> BYTE_SHIFT) & BYTE_MASK); -} - - -static void put32(uint8_t *p, uint32_t v) { - put16(p, v & WORD_MASK); - put16(p + WORD_BYTES, v >> WORD_SHIFT); -} - - // A chunk's four-character tag (no terminator). static void putTag(uint8_t *p, const char *tag) { for (int32_t i = 0; i < FOURCC_BYTES; i++) { @@ -436,7 +419,7 @@ static void recPeak(AudioRecT *rec, const uint8_t *p, uint32_t bytes) { if (rec->fmt.bits == BITS_16) { for (uint32_t i = 0; i + 1 < bytes; i += WORD_BYTES) { - int32_t v = (int16_t)(p[i] | (p[i + 1] << BYTE_SHIFT)); + int32_t v = (int16_t)dvxGetLe16(p + i); v = v < 0 ? -v : v; peak = v > peak ? v : peak; diff --git a/src/libs/kpunch/audio/audioWt.c b/src/libs/kpunch/audio/audioWt.c index ef812a9e..f70a3732 100644 --- a/src/libs/kpunch/audio/audioWt.c +++ b/src/libs/kpunch/audio/audioWt.c @@ -37,13 +37,14 @@ // aligned, and a 16-bit one inside a 256 KB boundary where the chip asks. #include "audioInt.h" +#include "stb_ds.h" #include #include #define WT_ID_STRIDE 64 #define WT_GEN_MAX (INT32_MAX / WT_ID_STRIDE - 1) -#define WT_BLOCKS_MAX 1024 +#define WT_BLOCKS_MAX 1024 // blocks of memory given out at once #define WT_ALIGN 32 // bytes a block starts on #define WT_BANK_BYTES 0x40000U // AUDIO_WT_BANK_256K #define BITS_8 8 @@ -75,8 +76,7 @@ typedef struct { } WtChipT; static WtChipT sWt; -static WtBlockT sBlocks[WT_BLOCKS_MAX]; // in use, by address -static uint32_t sBlockCount; +static WtBlockT *sBlocks = NULL; // stb_ds: in use, by address // ============================================================ // Prototypes @@ -101,7 +101,6 @@ int32_t audioWtUpload(AudioCtxT *ctx, uint32_t addr, const int16_t *frame static int32_t blockOf(const AudioCtxT *ctx, uint32_t addr, uint32_t bytes); static void giveBack(uint32_t voice); static int32_t lookup(const AudioCtxT *ctx, int32_t voice); -static void removeBlock(uint32_t i); // Interrupt-safe. @@ -151,7 +150,7 @@ int32_t audioWtAlloc(AudioCtxT *ctx) { void audioWtAttach(const AudioSynthDriverT *drv, const AudioSynthDrvInfoT *info) { memset(&sWt, 0, sizeof(sWt)); - sBlockCount = 0; + arrfree(sBlocks); sWt.drv = drv; sWt.count = info->voices < AUDIO_WT_VOICES_MAX ? info->voices : AUDIO_WT_VOICES_MAX; sWt.memory = info->memory; @@ -173,9 +172,9 @@ void audioWtCtxClosed(AudioCtxT *ctx) { audioIrqRestore(flags); - for (uint32_t i = sBlockCount; i > 0; i--) { - if (sBlocks[i - 1].owner == ctx) { - removeBlock(i - 1); + for (int32_t i = (int32_t)arrlen(sBlocks) - 1; i >= 0; i--) { + if (sBlocks[i].owner == ctx) { + arrdel(sBlocks, i); } } } @@ -183,7 +182,7 @@ void audioWtCtxClosed(AudioCtxT *ctx) { void audioWtDetach(void) { memset(&sWt, 0, sizeof(sWt)); - sBlockCount = 0; + arrfree(sBlocks); } @@ -203,6 +202,7 @@ int32_t audioWtFree(const AudioCtxT *ctx, int32_t voice) { void audioWtGetInfo(AudioWtInfoT *info) { uint32_t largest = 0; + uint32_t total = 0; uint32_t at = 0; memset(info, 0, sizeof(*info)); @@ -212,17 +212,21 @@ void audioWtGetInfo(AudioWtInfoT *info) { } // The largest gap between the blocks in use - for (uint32_t i = 0; i <= sBlockCount; i++) { - uint32_t next = i < sBlockCount ? sBlocks[i].addr : sWt.memory; + uint32_t count = (uint32_t)arrlen(sBlocks); + + for (uint32_t i = 0; i <= count; i++) { + uint32_t next = i < count ? sBlocks[i].addr : sWt.memory; largest = next - at > largest ? next - at : largest; - at = i < sBlockCount ? sBlocks[i].addr + sBlocks[i].bytes : at; + total += next - at; + at = i < count ? sBlocks[i].addr + sBlocks[i].bytes : at; } snprintf(info->description, sizeof(info->description), "%s", sWt.description); info->voices = sWt.count; info->memory = sWt.memory; info->freeMemory = largest; + info->freeTotal = total; info->flags = sWt.flags; info->level = sWt.level; info->present = true; @@ -265,14 +269,16 @@ int32_t audioWtMemAlloc(AudioCtxT *ctx, uint32_t bytes, uint32_t bits, uint32_t return AUDIO_ERR_NO_SYNTH; } - if (sBlockCount >= WT_BLOCKS_MAX || (bankRule && bytes > WT_BANK_BYTES)) { + uint32_t count = (uint32_t)arrlen(sBlocks); + + if (count >= WT_BLOCKS_MAX || (bankRule && bytes > WT_BANK_BYTES)) { return AUDIO_ERR_NO_ROOM; } uint32_t gapStart = 0; - for (uint32_t i = 0; i <= sBlockCount; i++) { - uint32_t gapEnd = i < sBlockCount ? sBlocks[i].addr : sWt.memory; + for (uint32_t i = 0; i <= count; i++) { + uint32_t gapEnd = i < count ? sBlocks[i].addr : sWt.memory; uint32_t start = (gapStart + WT_ALIGN - 1) / WT_ALIGN * WT_ALIGN; if (bankRule && start / WT_BANK_BYTES != (start + bytes - 1) / WT_BANK_BYTES) { @@ -280,16 +286,14 @@ int32_t audioWtMemAlloc(AudioCtxT *ctx, uint32_t bytes, uint32_t bits, uint32_t } if (start <= gapEnd && bytes <= gapEnd - start) { - memmove(&sBlocks[i + 1], &sBlocks[i], (sBlockCount - i) * sizeof(WtBlockT)); - sBlocks[i].owner = ctx; - sBlocks[i].addr = start; - sBlocks[i].bytes = bytes; - sBlockCount++; + WtBlockT block = { ctx, start, bytes }; + + arrins(sBlocks, i, block); *addr = start; return AUDIO_OK; } - gapStart = i < sBlockCount ? sBlocks[i].addr + sBlocks[i].bytes : gapStart; + gapStart = i < count ? sBlocks[i].addr + sBlocks[i].bytes : gapStart; } return AUDIO_ERR_NO_ROOM; @@ -297,9 +301,9 @@ int32_t audioWtMemAlloc(AudioCtxT *ctx, uint32_t bytes, uint32_t bits, uint32_t int32_t audioWtMemFree(AudioCtxT *ctx, uint32_t addr) { - for (uint32_t i = 0; i < sBlockCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(sBlocks); i++) { if (sBlocks[i].owner == ctx && sBlocks[i].addr == addr) { - removeBlock(i); + arrdel(sBlocks, i); return AUDIO_OK; } } @@ -342,6 +346,11 @@ int32_t audioWtStart(const AudioCtxT *ctx, int32_t voice, const AudioWtVoiceT *v return AUDIO_ERR_VOICE; } + // A sample in a ROM the chip does not have + if ((v->addr & AUDIO_WT_ADDR_ROM) && !(sWt.flags & AUDIO_WT_ROM)) { + return AUDIO_ERR_ARG; + } + sWt.drv->start((uint32_t)i, v); return AUDIO_OK; } @@ -389,11 +398,11 @@ int32_t audioWtUpload(AudioCtxT *ctx, uint32_t addr, const int16_t *frames, uint // The block of ctx holding addr .. addr + bytes - 1 (its index), -1 when // none does. static int32_t blockOf(const AudioCtxT *ctx, uint32_t addr, uint32_t bytes) { - for (uint32_t i = 0; i < sBlockCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(sBlocks); i++) { const WtBlockT *b = &sBlocks[i]; if (b->owner == ctx && addr >= b->addr && bytes <= b->bytes && addr - b->addr <= b->bytes - bytes) { - return (int32_t)i; + return i; } } @@ -422,9 +431,3 @@ static int32_t lookup(const AudioCtxT *ctx, int32_t voice) { return i; } - - -static void removeBlock(uint32_t i) { - memmove(&sBlocks[i], &sBlocks[i + 1], (sBlockCount - i - 1) * sizeof(WtBlockT)); - sBlockCount--; -} diff --git a/src/libs/kpunch/conn/Makefile b/src/libs/kpunch/conn/Makefile index 1822bbb9..55e57ade 100644 --- a/src/libs/kpunch/conn/Makefile +++ b/src/libs/kpunch/conn/Makefile @@ -26,12 +26,12 @@ # behind one interface (see conn.h) -- and their BASIC face (connBas.c, # src/include/basic/conn.bas), which runs transfers with the transfer # library and writes to Terminal controls. It sits on the serial library -# (rs232, secLink) and the networking library (BSD sockets: its SDK +# (rs232), the secure link library, and the networking library (BSD sockets: its SDK # headers come first on the include path). include ../../../common.mk -CFLAGS = $(DOS_CFLAGS) -I. -I../dvxnet/include -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty -I../../../widgets/kpunch +CFLAGS = $(DOS_CFLAGS) -I. -I.. -I../dvxnet/include -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty -I../../../widgets/kpunch OBJDIR = ../../../../obj/conn LIBSDIR = ../../../../bin/libs diff --git a/src/libs/kpunch/conn/conn.c b/src/libs/kpunch/conn/conn.c index 585b7608..132d5812 100644 --- a/src/libs/kpunch/conn/conn.c +++ b/src/libs/kpunch/conn/conn.c @@ -24,18 +24,24 @@ // per connection type; the common code owns the receive and send buffers, // the status text and the break timer. Telnet and the secure link over // the network use the standard BSD socket API (the networking library on -// DOS), always non-blocking, so connPoll never waits. +// DOS), always non-blocking, so connPoll never waits. The networking +// library is an optional part: its calls are looked up when a network +// connection starts (netFind), and without them such a connection ends at +// once saying so, while serial, modem, and secure links over a COM port +// work as ever. #include "conn.h" #include "connTelnet.h" +#include "dvxPlat.h" #include "dvxTypes.h" +#include "dvxnet/dvxNet.h" #include "../serial/rs232/rs232.h" -#include "../serial/seclink/secLink.h" +#include "../seclink/secLink.h" #include +#include #include #include -#include #include #include #include @@ -63,6 +69,7 @@ DVX_ABI_STAMP(conn); #define DEFAULT_ROWS 25 #define MS_PER_SEC 1000 #define COM_NUMBER_BASE 1 // RS232_COM1 is shown as COM1 +#define NO_NET_TEXT "Networking is not installed" // Receive room the secure link needs before it is polled: one poll can // deliver everything the UART buffered (secLink has already acknowledged @@ -121,12 +128,28 @@ struct ConnS { bool portOpen; bool withPass; // secure link: a passphrase authenticates it bool tcpConnecting; // secure link over the network: TCP not up yet + bool looking; // Telnet / secure link over the network: the host being looked up + NetLookupT lookup; int32_t sock; // Telnet / secure link over the network int32_t cols; // window size for Telnet (NAWS) int32_t rows; TelnetT telnet; }; +// The networking library's calls (see netFind) +typedef struct { + __typeof__(&connect) connect; + __typeof__(&fcntl) fcntl; + __typeof__(&getsockopt) getsockopt; + __typeof__(&netLookupCancel) lookupCancel; + __typeof__(&netLookupPoll) lookupPoll; + __typeof__(&netLookupStart) lookupStart; + __typeof__(&poll) poll; + __typeof__(&recv) recv; + __typeof__(&send) send; + __typeof__(&socket) socket; +} NetCallsT; + // Modem result lines, matched at the start of a line (any case) typedef enum { ResultOkE, @@ -162,6 +185,7 @@ static void modemLine(ConnT *c, uint32_t nowMs); static int32_t modemOpen(ConnT *c, uint32_t nowMs); static void modemPoll(ConnT *c, uint32_t nowMs); static void modemRetry(ConnT *c, const char *reason, uint32_t nowMs); +static bool netFind(void); static void rs232Text(int32_t rc, char *out, int32_t outSize); static void rxPut(ConnT *c, const uint8_t *data, int32_t len); static int32_t rxSpace(const ConnT *c); @@ -181,9 +205,10 @@ static int32_t serialSend(ConnT *c, const uint8_t *data, int32_t len); static void setStatus(ConnT *c, const char *fmt, ...); static int32_t sockRead(void *ctx, uint8_t *buf, int32_t len); static int32_t sockWrite(void *ctx, const uint8_t *data, int32_t len); -static int32_t tcpBegin(ConnT *c, uint32_t nowMs); +static int32_t tcpBegin(ConnT *c); static int32_t tcpCheck(ConnT *c, uint32_t nowMs); static void tcpClose(ConnT *c); +static bool tcpConnect(ConnT *c, uint32_t addr, uint32_t nowMs); static void tcpFailed(ConnT *c, int32_t err); static void telnetClose(ConnT *c); static int32_t telnetOpen(ConnT *c, uint32_t nowMs); @@ -194,6 +219,9 @@ static bool timeReached(uint32_t nowMs, uint32_t deadline); static void txQueue(ConnT *c, const uint8_t *data, int32_t len); static void txQueueText(ConnT *c, const char *text); +static NetCallsT sNet; +static bool sNetFound = false; // every call of sNet was found + static const ConnOpsT sOps[ConnTypeCountE] = { [ConnSerialE] = { true, serialOpen, serialPoll, serialSend, serialClose, serialPending }, [ConnModemE] = { true, modemOpen, modemPoll, serialSend, serialClose, serialPending }, @@ -267,8 +295,8 @@ const char *connErrorText(int32_t err) { case CONN_ERR_PORT: return "The port could not be opened"; - case CONN_ERR_HOST: - return "The host could not be reached"; + case CONN_ERR_NO_NET: + return NO_NET_TEXT; default: return "The connection could not be opened"; @@ -398,7 +426,7 @@ uint32_t connStatusSerial(const ConnT *c) { bool connTypeAvailable(int32_t type) { - return type >= 0 && type < ConnTypeCountE; + return type >= 0 && type < ConnTypeCountE && (type != ConnTelnetE || netFind()); } @@ -440,7 +468,7 @@ static void fail(ConnT *c, const char *fmt, ...) { // Send what the Telnet filter queued, as far as the socket takes it. static void flushNet(ConnT *c) { while (c->telnet.outLen > 0) { - ssize_t n = send(c->sock, c->telnet.out, (size_t)c->telnet.outLen, MSG_DONTWAIT); + ssize_t n = sNet.send(c->sock, c->telnet.out, (size_t)c->telnet.outLen, MSG_DONTWAIT); if (n <= 0) { return; @@ -653,6 +681,32 @@ static void modemRetry(ConnT *c, const char *reason, uint32_t nowMs) { } +// The networking library's calls, looked up by name (it is an optional +// part, so conn.lib does not import them); true once every one is found. +// A call not found yet is looked for again at the next network +// connection. fcntl and poll are the C library's too, but a lookup finds +// a loaded module's name before dvx.exe's, so these are the networking +// library's own (the ones that know its sockets). +static bool netFind(void) { + if (sNetFound) { + return true; + } + + sNet.connect = dlsym(NULL, DVX_SYM("connect")); + sNet.fcntl = dlsym(NULL, DVX_SYM("fcntl")); + sNet.getsockopt = dlsym(NULL, DVX_SYM("getsockopt")); + sNet.lookupCancel = dlsym(NULL, DVX_SYM("netLookupCancel")); + sNet.lookupPoll = dlsym(NULL, DVX_SYM("netLookupPoll")); + sNet.lookupStart = dlsym(NULL, DVX_SYM("netLookupStart")); + sNet.poll = dlsym(NULL, DVX_SYM("poll")); + sNet.recv = dlsym(NULL, DVX_SYM("recv")); + sNet.send = dlsym(NULL, DVX_SYM("send")); + sNet.socket = dlsym(NULL, DVX_SYM("socket")); + sNetFound = sNet.connect && sNet.fcntl && sNet.getsockopt && sNet.lookupCancel && sNet.lookupPoll && sNet.lookupStart && sNet.poll && sNet.recv && sNet.send && sNet.socket; + return sNetFound; +} + + // Plain words for an rs232 error code. static void rs232Text(int32_t rc, char *out, int32_t outSize) { const char *text; @@ -718,7 +772,7 @@ static void secureClose(ConnT *c) { // starts once the TCP connection is up; see securePoll). static int32_t secureOpen(ConnT *c, uint32_t nowMs) { if (c->cfg.host[0] != '\0') { - int32_t rc = tcpBegin(c, nowMs); + int32_t rc = tcpBegin(c); if (rc == CONN_OK) { c->tcpConnecting = true; @@ -749,7 +803,7 @@ static int32_t securePending(const ConnT *c) { static void securePoll(ConnT *c, uint32_t nowMs) { if (c->tcpConnecting) { - PktStreamT stream = { sockRead, sockWrite, c }; + PktStreamT stream = { sockRead, sockWrite, c, NULL }; if (tcpCheck(c, nowMs) <= 0) { return; @@ -776,10 +830,10 @@ static void securePoll(ConnT *c, uint32_t nowMs) { setStatus(c, "Secure link established%s", c->withPass ? " (passphrase checked)" : ""); } else if (rc == SECLINK_ERR_AUTH) { fail(c, "The passphrase does not match the host's"); + } else if (rc == SECLINK_ERR_TIMEOUT || (rc == SECLINK_PENDING && timeReached(nowMs, c->deadline))) { + fail(c, "The host did not answer the secure link"); } else if (rc != SECLINK_PENDING) { fail(c, "The secure link could not be set up"); - } else if (timeReached(nowMs, c->deadline)) { - fail(c, "The host did not answer the secure link"); } return; @@ -926,7 +980,7 @@ static void setStatus(ConnT *c, const char *fmt, ...) { // Socket as a secLink byte stream (see PktStreamT). static int32_t sockRead(void *ctx, uint8_t *buf, int32_t len) { - ssize_t n = recv(((ConnT *)ctx)->sock, buf, (size_t)len, MSG_DONTWAIT); + ssize_t n = sNet.recv(((ConnT *)ctx)->sock, buf, (size_t)len, MSG_DONTWAIT); if (n == 0) { return -1; @@ -941,7 +995,7 @@ static int32_t sockRead(void *ctx, uint8_t *buf, int32_t len) { static int32_t sockWrite(void *ctx, const uint8_t *data, int32_t len) { - ssize_t n = send(((ConnT *)ctx)->sock, data, (size_t)len, MSG_DONTWAIT); + ssize_t n = sNet.send(((ConnT *)ctx)->sock, data, (size_t)len, MSG_DONTWAIT); if (n < 0) { return (errno == EWOULDBLOCK) ? 0 : -1; @@ -951,64 +1005,58 @@ static int32_t sockWrite(void *ctx, const uint8_t *data, int32_t len) { } -// Look the host up and start a non-blocking TCP connection to it. -static int32_t tcpBegin(ConnT *c, uint32_t nowMs) { - struct addrinfo hints; - struct addrinfo *res = NULL; - char port[PORT_TEXT_MAX]; - int rc; - +// Start looking the host up (tcpCheck connects once it is found). +static int32_t tcpBegin(ConnT *c) { if (c->cfg.host[0] == '\0') { fail(c, "No host name is set"); return CONN_ERR_PARAM; } - memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_INET; - hints.ai_socktype = SOCK_STREAM; - snprintf(port, sizeof(port), "%d", (int)c->cfg.tcpPort); - - if (getaddrinfo(c->cfg.host, port, &hints, &res) != 0 || !res) { - fail(c, "The host %s was not found", c->cfg.host); - return CONN_ERR_HOST; + if (!netFind()) { + fail(c, NO_NET_TEXT); + return CONN_ERR_NO_NET; } - c->sock = socket(AF_INET, SOCK_STREAM, 0); - - if (c->sock < 0) { - freeaddrinfo(res); - tcpFailed(c, errno); - return CONN_ERR_HOST; - } - - fcntl(c->sock, F_SETFL, O_NONBLOCK); - rc = connect(c->sock, res->ai_addr, res->ai_addrlen); - freeaddrinfo(res); - - if (rc != 0 && errno != EINPROGRESS) { - tcpFailed(c, errno); - return CONN_ERR_HOST; - } - - c->state = ConnOpeningE; - c->deadline = nowMs + TCP_CONNECT_MS; + sNet.lookupStart(&c->lookup, c->cfg.host); + c->looking = true; + c->state = ConnOpeningE; return CONN_OK; } // Is the TCP connection up? 1 yes, 0 not yet, -1 it failed (and the -// connection has ended with the reason). +// connection has ended with the reason). First the host is looked up (it +// ends by itself: DHCP's first address and the DNS server each have their +// limit), then a non-blocking connection is started to it. static int32_t tcpCheck(ConnT *c, uint32_t nowMs) { struct pollfd p; int err = 0; socklen_t len = sizeof(err); + if (c->looking) { + uint32_t addr; + int32_t rc = sNet.lookupPoll(&c->lookup, &addr); + + if (rc == NET_LOOKUP_BUSY) { + return 0; + } + + c->looking = false; + + if (rc != 0) { + fail(c, "The host %s was not found", c->cfg.host); + return -1; + } + + return tcpConnect(c, addr, nowMs) ? 0 : -1; + } + p.fd = c->sock; p.events = POLLOUT; p.revents = 0; - if (poll(&p, 1, 0) > 0) { - getsockopt(c->sock, SOL_SOCKET, SO_ERROR, &err, &len); + if (sNet.poll(&p, 1, 0) > 0) { + sNet.getsockopt(c->sock, SOL_SOCKET, SO_ERROR, &err, &len); if (err == 0 && !(p.revents & (POLLERR | POLLHUP))) { return 1; @@ -1027,7 +1075,15 @@ static int32_t tcpCheck(ConnT *c, uint32_t nowMs) { } +// The socket goes, and a lookup under way (either is only there when the +// networking library is). static void tcpClose(ConnT *c) { + if (c->looking) { + sNet.lookupCancel(&c->lookup); + } + + c->looking = false; + if (c->sock == NO_SOCKET) { return; } @@ -1037,6 +1093,36 @@ static void tcpClose(ConnT *c) { } +// A socket, and a non-blocking connection to the host (at addr, network +// order) started. False (the connection ended with the reason) when it +// cannot be. +static bool tcpConnect(ConnT *c, uint32_t addr, uint32_t nowMs) { + struct sockaddr_in to; + + memset(&to, 0, sizeof(to)); + to.sin_family = AF_INET; + to.sin_port = htons((uint16_t)c->cfg.tcpPort); + to.sin_addr.s_addr = addr; + c->sock = sNet.socket(AF_INET, SOCK_STREAM, 0); + + if (c->sock < 0) { + c->sock = NO_SOCKET; + tcpFailed(c, errno); + return false; + } + + sNet.fcntl(c->sock, F_SETFL, O_NONBLOCK); + + if (sNet.connect(c->sock, (struct sockaddr *)&to, sizeof(to)) != 0 && errno != EINPROGRESS) { + tcpFailed(c, errno); + return false; + } + + c->deadline = nowMs + TCP_CONNECT_MS; + return true; +} + + // End the connection with a plain-words reason for a socket error. static void tcpFailed(ConnT *c, int32_t err) { switch (err) { @@ -1069,8 +1155,9 @@ static void telnetClose(ConnT *c) { static int32_t telnetOpen(ConnT *c, uint32_t nowMs) { int32_t rc; + (void)nowMs; telnetInit(&c->telnet, c->cols, c->rows); - rc = tcpBegin(c, nowMs); + rc = tcpBegin(c); if (rc == CONN_OK) { setStatus(c, "Connecting to %s port %d", c->cfg.host, (int)c->cfg.tcpPort); @@ -1109,7 +1196,7 @@ static void telnetPoll(ConnT *c, uint32_t nowMs) { break; } - n = recv(c->sock, in, (size_t)((room < READ_CHUNK) ? room : READ_CHUNK), MSG_DONTWAIT); + n = sNet.recv(c->sock, in, (size_t)((room < READ_CHUNK) ? room : READ_CHUNK), MSG_DONTWAIT); if (n == 0) { fail(c, "%s closed the connection", c->cfg.host); diff --git a/src/libs/kpunch/conn/conn.dep b/src/libs/kpunch/conn/conn.dep index 215a451c..7e5825c5 100644 --- a/src/libs/kpunch/conn/conn.dep +++ b/src/libs/kpunch/conn/conn.dep @@ -1,5 +1,5 @@ -dvxnet serial +seclink xfer libtasks libdvx diff --git a/src/libs/kpunch/conn/conn.dhs b/src/libs/kpunch/conn/conn.dhs index 6de169b7..0865fc39 100644 --- a/src/libs/kpunch/conn/conn.dhs +++ b/src/libs/kpunch/conn/conn.dhs @@ -77,7 +77,7 @@ The Telnet protocol itself is a separate filter (connTelnet.c) that only moves b ConnT *connCreate(void) Allocate a closed connection. void connDestroy(c) Close and free. void connConfigDefaults(cfg) COM1, 9600 8N1, no flow control, ATZ, ATDT. - int32_t connOpen(c, cfg, nowMs) Start connecting (cfg is copied). CONN_OK, CONN_ERR_PARAM, CONN_ERR_PORT, or CONN_ERR_HOST (the host was not found or refused at once); connStatus explains. Later failures (refused, no answer within 30 seconds, connection lost, host closed it) end the connection with the reason in connStatus. + int32_t connOpen(c, cfg, nowMs) Start connecting (cfg is copied). CONN_OK, CONN_ERR_PARAM, CONN_ERR_PORT, or CONN_ERR_NO_NET (Telnet, or a secure link over the network, without the networking library); connStatus explains. A network host is looked up and connected to while connPoll runs (never waiting inside one call), so later failures (the host not found, refused, no answer within 30 seconds, connection lost, host closed it) end the connection with the reason in connStatus. void connPoll(c, nowMs) Dial, key exchange, receive, send, timers. int32_t connRead(c, buf, max) Take received bytes (0 when none). int32_t connWrite(c, data, len) Queue bytes to send; returns the count accepted (0 while not open or when the 8 KB send buffer is full). @@ -88,7 +88,7 @@ The Telnet protocol itself is a separate filter (connTelnet.c) that only moves b const char *connStatus(c) What is happening, or why the connection ended, in plain words. uint32_t connStatusSerial(c) Changes whenever the status text changes. void connSetWindowSize(c, cols, rows) The terminal size for Telnet's NAWS (sent now if the host asked for it). - bool connTypeAvailable(type) True for every ConnTypeE value. + bool connTypeAvailable(type) True for every ConnTypeE value but ConnTelnetE when the networking library is not installed (an optional part of DVX; connections over the network then end at once with CONN_ERR_NO_NET). conn.lib does not import the networking library: it looks its calls up when a network connection starts. bool connCanBreak(type) True for serial and modem. const char *connErrorText(err) Text for a connOpen result. .endtable @@ -142,6 +142,7 @@ Add conn.bas to a BASIC project (and xfer.bas for transfers): connBas.c gives BA ConnState, ConnStatus$, ConnStatusSerial connState, connStatus, connStatusSerial. ConnWrite(h, bytes$), ConnRead$(h), ConnPending(h) Bytes out and in without a Terminal: connWrite / connRead with BINARY STRING data, so every byte value passes. ConnCanBreak(type), ConnSendBreak(h) connCanBreak, connSendBreak. + ConnTypeAvailable(type) connTypeAvailable: False for CONN_TELNET when Networking is not installed (a secure link over the network needs it too). ConnClose h, ConnFree h Close (a modem hangs up); close and free the handle. ConnAttach(h, ctrl$), ConnDetach h Show the connection on a Terminal control, and send what is typed there. ConnSetEcho h, on; ConnSetAnswerback h, text$ Local echo (Enter as CR LF); the reply to ENQ. diff --git a/src/libs/kpunch/conn/conn.h b/src/libs/kpunch/conn/conn.h index 13c4bee5..71377f26 100644 --- a/src/libs/kpunch/conn/conn.h +++ b/src/libs/kpunch/conn/conn.h @@ -71,7 +71,7 @@ #define CONN_OK 0 #define CONN_ERR_PARAM (-1) #define CONN_ERR_PORT (-3) -#define CONN_ERR_HOST (-4) // host not found or not reachable +#define CONN_ERR_NO_NET (-4) // Telnet or a secure link over the network, without the networking library // Secure link: the channel that carries terminal data #define CONN_SECURE_CHANNEL 0 @@ -158,7 +158,9 @@ const char *connStatus(const ConnT *c); // Changes whenever the status text changes (so a caller can redraw it) uint32_t connStatusSerial(const ConnT *c); -// True for every ConnTypeE value (all types are available). +// True for a ConnTypeE value that can be used here: every one but Telnet, +// which needs the networking library (an optional part of DVX; a secure +// link over the network does too, and then ends with CONN_ERR_NO_NET). bool connTypeAvailable(int32_t type); // Queue bytes to send. Returns the count accepted (0 while not open or diff --git a/src/libs/kpunch/conn/connBas.c b/src/libs/kpunch/conn/connBas.c index a3c28650..8d1b139a 100644 --- a/src/libs/kpunch/conn/connBas.c +++ b/src/libs/kpunch/conn/connBas.c @@ -63,11 +63,11 @@ #include #define BAS_COM_FIRST 1 // BASIC numbers COM ports from 1 -#define READ_MAX 4096 // received bytes moved to the screen per ConnPoll +#define READ_MAX 4096 // received bytes ConnRead$ gives at most +#define SHOW_PIECE 256 // received bytes moved to the screen at a time +#define SHOW_BUDGET_MS 20 // ... until a ConnPoll has spent this long on them #define ANSWERBACK_MAX 64 #define FILES_MAX (XFER_MAX_FILES * XFER_PATH_MAX) -#define FILE_LIST_SEP '\n' -#define FILE_LIST_CR '\r' #define ENQ 0x05 #define TERM_BIND_SYM "basFormRtTermBind" #define TERM_FORGET_SYM "basFormRtTermForget" @@ -135,6 +135,7 @@ void ConnSetXferWindow(int32_t handle, int32_t bytes); int32_t ConnState(int32_t handle); const char *ConnStatus(int32_t handle); int32_t ConnStatusSerial(int32_t handle); +int32_t ConnTypeAvailable(int32_t type); int32_t ConnWrite(int32_t handle, const uint8_t *data, int32_t len); int32_t ConnZmodemRequest(int32_t handle); static void echoLocal(BasConnT *b, const uint8_t *data, int32_t len); @@ -531,6 +532,13 @@ int32_t ConnStatusSerial(int32_t handle) { } +// True when connections of type can be made here (Telnet needs +// Networking). +int32_t ConnTypeAvailable(int32_t type) { + return connTypeAvailable(type) ? BAS_TRUE : BAS_FALSE; +} + + // Queue bytes (a BINARY STRING) to send. Returns the bytes accepted (0 // while not open). int32_t ConnWrite(int32_t handle, const uint8_t *data, int32_t len) { @@ -599,34 +607,42 @@ static XferT *runningTransfer(BasConnT *b) { // with the answerback and watch for a ZMODEM sender starting (the rest of // what was read with its start frame is not shown). static void showIncoming(BasConnT *b) { - int32_t n = connRead(b->conn, sReadBuf, (int32_t)sizeof(sReadBuf)); - int32_t out = 0; + uint32_t start = nowMs(); + bool seen = false; // a ZMODEM start in this poll: the rest is the transfer's - if (n <= 0) { - return; - } + // A piece at a time, for a while: a flood (bare line feeds scroll the + // whole screen each) waits in the connection for the next poll + do { + int32_t n = connRead(b->conn, sReadBuf, SHOW_PIECE); + int32_t out = 0; - if (b->capture) { - fwrite(sReadBuf, 1, (size_t)n, b->capture); - } - - for (int32_t i = 0; i < n; i++) { - uint8_t ch = sReadBuf[i]; - - if (xferDetectFeed(&b->detect, ch)) { - b->zmodemSeen = true; - break; + if (n <= 0) { + return; } - if (ch == ENQ) { - connWrite(b->conn, (const uint8_t *)b->answerback, (int32_t)strlen(b->answerback)); - continue; + if (b->capture) { + fwrite(sReadBuf, 1, (size_t)n, b->capture); } - sReadBuf[out++] = ch; - } + for (int32_t i = 0; i < n && !seen; i++) { + uint8_t ch = sReadBuf[i]; - wgtAnsiTermWrite(b->term, sReadBuf, out); + if (xferDetectFeed(&b->detect, ch)) { + b->zmodemSeen = true; + seen = true; + break; + } + + if (ch == ENQ) { + connWrite(b->conn, (const uint8_t *)b->answerback, (int32_t)strlen(b->answerback)); + continue; + } + + sReadBuf[out++] = ch; + } + + wgtAnsiTermWrite(b->term, sReadBuf, out); + } while (!seen && nowMs() - start < SHOW_BUDGET_MS); } @@ -668,36 +684,9 @@ static int32_t startTransfer(int32_t handle, bool sending, int32_t proto, const if (sending) { static char list[FILES_MAX]; const char *paths[XFER_MAX_FILES] = { NULL }; - int32_t count = 0; - // One path per line (a line feed at the very end starts none) snprintf(list, sizeof(list), "%s", files ? files : ""); - - for (char *p = list; *p && count < XFER_MAX_FILES; ) { - char *end = strchr(p, FILE_LIST_SEP); - - if (end) { - *end = '\0'; - } - - size_t len = strlen(p); - - if (len > 0 && p[len - 1] == FILE_LIST_CR) { - p[len - 1] = '\0'; - } - - if (*p) { - paths[count++] = p; - } - - if (!end) { - break; - } - - p = end + 1; - } - - ok = xferStartSend(x, (XferProtoE)proto, &io, &opt, paths, count, nowMs()); + ok = xferStartSend(x, (XferProtoE)proto, &io, &opt, paths, xferSplitList(list, paths), nowMs()); } else { ok = xferStartReceive(x, (XferProtoE)proto, &io, &opt, target ? target : "", nowMs()); } diff --git a/src/libs/kpunch/crypto/Makefile b/src/libs/kpunch/crypto/Makefile new file mode 100644 index 00000000..c6c79e59 --- /dev/null +++ b/src/libs/kpunch/crypto/Makefile @@ -0,0 +1,80 @@ +# 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. + +# Cryptography Library Makefile for DJGPP cross-compilation +# +# crypto.lib: Diffie-Hellman, XTEA-CTR, the random number generator +# (security.c), SHA-256, HMAC and HKDF (secHash.c), X25519 and +# XChaCha20-Poly1305 (secCurve.c), Ed25519 (secSign.c), and their BASIC +# entry points (secBas.c, security.bas). Monocypher (thirdparty) is +# built in; the library keeps only what its own objects reach of it +# (DXE_GC) and exports its own functions only. The package library, the +# network, the shell's seed file, and the secure link use it. + +include ../../../common.mk + +MONO = thirdparty/monocypher/src +CFLAGS = $(DOS_CFLAGS) -I. -I$(MONO) -I../libdvx -I../libdvx/platform -I../libtasks + +OBJDIR = ../../../../obj/crypto +LIBSDIR = ../../../../bin/libs + +SRCS = $(wildcard *.c) +OWN_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) +MONO_OBJS = $(OBJDIR)/monocypher.o $(OBJDIR)/monocypher-ed25519.o +OBJS = $(OWN_OBJS) $(MONO_OBJS) +GC_OBJ = $(OBJDIR)/cryptoGc.o +TARGETDIR = $(LIBSDIR)/kpunch/crypto +TARGET = $(TARGETDIR)/crypto.lib + +vpath %.c . $(MONO) $(MONO)/optional + +.PHONY: all clean + +all: $(TARGET) $(TARGETDIR)/crypto.dep + +$(TARGETDIR)/crypto.dep: crypto.dep | $(TARGETDIR) + sed 's/$$/\r/' $< > $@ + +$(GC_OBJ): $(OBJS) + $(DXE_GC) $@ $(OWN_OBJS) -- $(OBJS) + +$(TARGET): $(GC_OBJ) | $(TARGETDIR) + $(DXE3GEN) -o $(TARGETDIR)/crypto.dxe -U $(GC_OBJ) + mv $(TARGETDIR)/crypto.dxe $@ + +$(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(MONO_OBJS): $(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR) + $(CC) $(CFLAGS) $(DXE_GC_CFLAGS) -c -o $@ $< + +$(OBJDIR): + mkdir -p $(OBJDIR) + +$(TARGETDIR): + mkdir -p $(TARGETDIR) + +-include $(OBJS:.o=.d) + +clean: + rm -rf $(OBJDIR) $(TARGETDIR) diff --git a/src/libs/kpunch/crypto/README.md b/src/libs/kpunch/crypto/README.md new file mode 100644 index 00000000..45ac5ad4 --- /dev/null +++ b/src/libs/kpunch/crypto/README.md @@ -0,0 +1,42 @@ +# crypto -- Cryptography + +Compact cryptographic primitives: the X25519 key exchange (RFC 7748), +XChaCha20-Poly1305 authenticated encryption, Ed25519 signatures (RFC +8032), 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA +block cipher in CTR mode, SHA-256 / HMAC-SHA256 / HKDF-SHA256 for key +derivation and message authentication, and a SHA-256 DRBG with an +entropy pool and seed file. X25519, XChaCha20-Poly1305, and Ed25519 +are Monocypher's (thirdparty/monocypher, BSD licence, see its +DVX-VERSION.md); the library keeps only the parts of it these functions +use. The secure link, the package library, the network, and the +shell's random seed use it; BASIC reaches it through +src/include/basic/security.bas. + +## Documentation + +Full reference is in `crypto.dhs` (this directory), compiled into the +DVX System Reference -- open via the Help Viewer app, or read +`docs/dvx_system_reference.html` after `make`. + +Topics covered: DH handshake API, XTEA-CTR encrypt/decrypt, hashing, +DRBG entropy pool and seed file, remote-key validation range, the BASIC +functions. + +## Source Files + +- `security.h` -- public API (DH, XTEA-CTR, DRBG) +- `security.c` -- implementation (bignum, Montgomery ladder, XTEA, DRBG) +- `secHash.h` -- SHA-256, HMAC-SHA256, HKDF-SHA256 API +- `secHash.c` -- hash implementation +- `secCurve.h` -- X25519, XChaCha20-Poly1305, and Ed25519 API +- `secCurve.c` -- X25519 and XChaCha20-Poly1305 over Monocypher +- `secSign.c` -- Ed25519 over Monocypher (apart, so the proxy can + leave it out) +- `secBas.c` -- BASIC entry points (security.bas) +- `thirdparty/monocypher/` -- Monocypher 4.0.3 with one small DVX patch + (X25519 a slice at a time; its DVX-VERSION.md) + +## Build + +Built into `crypto.lib` by `make -C src/libs/kpunch/crypto` (invoked by +the top-level `make`). diff --git a/src/libs/kpunch/crypto/crypto.dep b/src/libs/kpunch/crypto/crypto.dep new file mode 100644 index 00000000..d613d36a --- /dev/null +++ b/src/libs/kpunch/crypto/crypto.dep @@ -0,0 +1 @@ +libdvx diff --git a/src/libs/kpunch/crypto/crypto.dhs b/src/libs/kpunch/crypto/crypto.dhs new file mode 100644 index 00000000..96106622 --- /dev/null +++ b/src/libs/kpunch/crypto/crypto.dhs @@ -0,0 +1,261 @@ +# 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. + +.section Technical Reference (for Developers)|Libraries +.topic lib.crypto +.title Cryptography Library +.toc 0 Cryptography Library +.index Security +.index Diffie-Hellman +.index XTEA +.index XTEA-CTR +.index SHA-256 +.index HMAC +.index HKDF +.index DRBG +.index X25519 +.index XChaCha20-Poly1305 +.index Ed25519 +.index Monocypher +.index secX25519 +.index secAeadLock +.index secAeadUnlock +.index secEd25519Check +.index secEd25519Sign +.index secDhCreate +.index secCipherCreate +.index secRngBytes +.index secRngAddEntropy +.index secRngLoadSeed +.index security.bas + +.h1 Cryptography Library + +Cryptographic primitives for DVX programs and libraries (the secure link, the package library, the network, and the shell's random seed use them): the X25519 key exchange (RFC 7748), XChaCha20-Poly1305 authenticated encryption, Ed25519 signatures (RFC 8032), 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA block cipher in CTR mode, SHA-256, HMAC-SHA256, and HKDF-SHA256, and a SHA-256 based random number generator. X25519, XChaCha20-Poly1305, and Ed25519 are Monocypher's (thirdparty/monocypher, BSD licence); the library keeps only the parts of it these functions use. + +XTEA requires no lookup tables and compiles to approximately 20 instructions per round, making it suitable for 486-class hardware where AES S-box tables would thrash the 8KB cache. SHA-256 needs only 32-bit arithmetic and a 256-byte constant table. + +Loaded as: bin/libs/kpunch/crypto/crypto.lib + +Headers: crypto/security.h, crypto/secHash.h, crypto/secCurve.h + +.h2 Constants + +.table + Constant Value Description + -------- ----- ----------- + SEC_DH_KEY_SIZE 128 Size of a DH public key or shared secret in bytes (1024 bits). + SEC_XTEA_KEY_SIZE 16 Size of an XTEA key in bytes (128 bits). + SEC_RNG_SEED_SIZE 32 Bytes stored in a seed file. + SEC_SHA256_SIZE 32 SHA-256 digest size. + SEC_SHA256_BLOCK_SIZE 64 SHA-256 block size. + SEC_HKDF_MAX_OUT 8160 Largest HKDF output (255 digests). + SEC_X25519_KEY_SIZE 32 An X25519 secret key, public key, or shared secret. + SEC_AEAD_KEY_SIZE 32 An XChaCha20-Poly1305 key. + SEC_AEAD_NONCE_SIZE 24 An XChaCha20-Poly1305 nonce. + SEC_AEAD_TAG_SIZE 16 The Poly1305 tag. + SEC_ED25519_SEED_SIZE 32 The random bytes an Ed25519 key pair is made from. + SEC_ED25519_SECRET_SIZE 64 An Ed25519 secret key (the seed, then the public key). + SEC_ED25519_PUBLIC_SIZE 32 An Ed25519 public key. + SEC_ED25519_SIG_SIZE 64 An Ed25519 signature. + SEC_X25519_JOB_WORDS 72 Size of a SecX25519JobT in 32-bit words. + SEC_X25519_PENDING 1 SecX25519Step (BASIC): more of the work to do. +.endtable + +.h2 Error Codes + +.table + Constant Value Description + -------- ----- ----------- + SEC_SUCCESS 0 Operation succeeded. + SEC_ERR_PARAM -1 Invalid parameter. + SEC_ERR_NOT_READY -2 DH context not ready (keys not generated or secret not computed). + SEC_ERR_IO -3 Seed file could not be read or written. +.endtable + +.h2 RNG Functions + +A SHA-256 hash DRBG over a 32-byte entropy pool. secRngAddEntropy hashes new bytes into the pool and never discards what is already there, so it is safe to call at any time, before or after the first output. Each output block is a hash of the pool and a counter; after every request the pool is re-hashed so earlier output cannot be recomputed from a later state. + +The first secRngBytes call gathers hardware entropy automatically. On DOS that is 256 samples of the PIT counter, each taken after a busy wait sized by the previous sample, with the full time stamp counter on CPUs that have one (Pentium and later), else with the BIOS tick count. On a CPU without a time stamp counter the samples depend mostly on the PIT phase and time of day, which an observer can narrow down, so applications should feed user-input timings to secRngAddEntropy and keep a seed file: call secRngLoadSeed at startup and secRngSaveSeed at shutdown. Host builds read /dev/urandom. + +Under the DVX shell this is automatic: the shell loads the seed file SECLINK.RND in the user folder at startup, feeds the timing of every key press and mouse event to secRngAddEntropy, and saves a fresh seed at shutdown. Applications only need their own calls when they run without the shell. + +.code +void secRngAddEntropy(const uint8_t *data, int32_t len); +void secRngBytes(uint8_t *buf, int32_t len); +int32_t secRngGatherEntropy(uint8_t *buf, int32_t len); +int secRngLoadSeed(const char *path); +int secRngSaveSeed(const char *path); +void secSecureZero(void *ptr, int len); +.endcode + +.table + Function Description + -------- ----------- + secRngAddEntropy Hash bytes into the pool (key timings, mouse positions, anything unpredictable). + secRngBytes Generate pseudorandom bytes. + secRngGatherEntropy Fill buf with condensed hardware entropy (one fresh gather per 32 bytes). Returns the byte count (0 if a host build cannot read its random device). + secRngLoadSeed Mix a seed file into the pool, then immediately rewrite it with fresh output so a seed is never used twice. SEC_SUCCESS or SEC_ERR_IO. + secRngSaveSeed Write SEC_RNG_SEED_SIZE bytes of fresh output for the next run. SEC_SUCCESS or SEC_ERR_IO. + secSecureZero Zero memory in a way the compiler cannot optimize away (for key material). +.endtable + +.h2 Hash Functions + +.code +void secSha256(const uint8_t *data, int32_t len, uint8_t digest[32]); +void secSha256Init(SecSha256T *ctx); +void secSha256Update(SecSha256T *ctx, const uint8_t *data, int32_t len); +void secSha256Final(SecSha256T *ctx, uint8_t digest[32]); +void secHmacSha256(const uint8_t *key, int32_t keyLen, const uint8_t *data, int32_t len, uint8_t mac[32]); +void secHmacInit(SecHmacT *h, const uint8_t *key, int32_t keyLen); +void secHmacUpdate(SecHmacT *h, const uint8_t *data, int32_t len); +void secHmacFinal(SecHmacT *h, uint8_t mac[32]); +int secHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, uint8_t *okm, int32_t okmLen); +bool secConstTimeEqual(const void *a, const void *b, int32_t len); +.endcode + +SHA-256 follows FIPS 180-4, HMAC RFC 2104, and HKDF RFC 5869 (extract and expand; a NULL or empty salt means 32 zero bytes). secHkdf returns SEC_ERR_PARAM for okmLen outside 1..SEC_HKDF_MAX_OUT. secConstTimeEqual compares without revealing where the inputs differ; use it for every MAC check. + +.h2 Diffie-Hellman Key Exchange + +1024-bit DH with 256-bit private exponents using the RFC 2409 Group 2 safe prime. The modular exponentiation is a Montgomery ladder with branch-free operand swaps, so its timing does not depend on the private key. + +.code +SecDhT *secDhCreate(void); +int secDhGenerateKeys(SecDhT *dh); +int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len); +int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len); +int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len); +void secDhDestroy(SecDhT *dh); +.endcode + +.table + Function Description + -------- ----------- + secDhCreate Allocate a new DH context. + secDhGenerateKeys Generate a private exponent and public key from the DRBG. + secDhGetPublicKey Copy the local public key into buf. *len receives the key size. + secDhComputeSecret Compute the shared secret from the remote public key. Keys outside [2, p-2] are rejected with SEC_ERR_PARAM (0, 1, and p-1 would force a trivially known secret). + secDhGetSecret Copy the shared secret (SEC_DH_KEY_SIZE big-endian bytes) into buf. It is input for a key derivation function such as secHkdf, never a key by itself. + secDhDestroy Securely zero and free the DH context. +.endtable + +Typical usage order: secDhCreate, secDhGenerateKeys, exchange public keys, secDhComputeSecret, secDhGetSecret, secHkdf, secDhDestroy. + +.h2 XTEA-CTR Cipher + +XTEA in counter mode. Encrypt and decrypt are the same operation (XOR with keystream). The counter must never repeat with the same key. + +.code +SecCipherT *secCipherCreate(const uint8_t *key); +void secCipherCrypt(SecCipherT *c, uint8_t *data, int len); +void secCipherSetCounter(SecCipherT *c, uint64_t counter); +void secCipherDestroy(SecCipherT *c); +.endcode + +.table + Function Description + -------- ----------- + secCipherCreate Allocate a cipher context with a 128-bit key. Returns NULL on allocation failure or NULL key. Initial counter is zero. + secCipherCrypt Encrypt or decrypt data in place. The 64-bit counter increments by one every 8 bytes (one XTEA block). + secCipherSetCounter Position the 64-bit block counter (secLink derives it from the frame sequence number). + secCipherDestroy Securely zero the cipher context (key and counter state) and free its memory. +.endtable + +secCipherCreate returns NULL if the key pointer is NULL or allocation fails. secCipherCrypt is a no-op when any of c/data is NULL or len <= 0. + +.h2 X25519 Key Exchange + +Elliptic-curve Diffie-Hellman on Curve25519 (crypto/secCurve.h). Each side makes a secret key from 32 random bytes (secRngBytes), sends its public key, and combines its secret key with the other side's public key; both get the same 32-byte secret. One call takes about a sixth of a second on a 486 (fixtures/bncurve on the emulated 486DX2-66: 165 ms), so a program that must stay responsive does it a slice at a time with a SecX25519JobT (the secure link does). + +.code +void secX25519PublicKey(uint8_t *publicKey, const uint8_t *secretKey); +bool secX25519(uint8_t *shared, const uint8_t *secretKey, const uint8_t *peerPublic); + +void secX25519JobStart(SecX25519JobT *job, const uint8_t *secretKey, const uint8_t *peerPublic); +bool secX25519JobStep(SecX25519JobT *job); +bool secX25519JobResult(SecX25519JobT *job, uint8_t *out); +void secX25519JobWipe(SecX25519JobT *job); +.endcode + +secX25519 is false when the peer's key gives an all-zero secret (one of the few keys that give everyone the same secret: a cheating peer). The secret is input for a key derivation function such as secHkdf, never a key by itself. + +A job is the same computation in nine slices of about 20 ms each on a 486 (eight of the ladder, then the inversion): secX25519JobStart begins it (peerPublic NULL makes the public key of secretKey), secX25519JobStep does a slice and is true once there is nothing left, secX25519JobResult writes the result (false as secX25519 is, or before the job is done) and wipes the job, and secX25519JobWipe drops one. A SecX25519JobT is the caller's (no allocation); the slicing is a small DVX patch to Monocypher (thirdparty/monocypher/DVX-VERSION.md). + +.h2 XChaCha20-Poly1305 + +Authenticated encryption: the data is encrypted and a 16-byte tag proves it, and some associated data sent in the clear, unchanged. + +.code +void secAeadLock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, uint8_t *tag); +bool secAeadUnlock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, const uint8_t *tag); +.endcode + +Both work in place. secAeadUnlock decrypts only when the tag matches; otherwise it returns false and leaves data as it was. A key and nonce pair must never seal two different messages: a counter in the nonce (the secure link uses its frame number) or a fresh random nonce (24 bytes is enough to pick at random) keeps them apart. + +.h2 Ed25519 Signatures + +Public-key signatures (RFC 8032, with SHA-512), as the package library uses for add-on packages. + +.code +void secEd25519KeyPair(uint8_t *secretKey, uint8_t *publicKey, uint8_t *seed); +void secEd25519Sign(uint8_t *sig, const uint8_t *secretKey, const uint8_t *msg, int32_t len); +bool secEd25519Check(const uint8_t *sig, const uint8_t *publicKey, const uint8_t *msg, int32_t len); +.endcode + +secEd25519KeyPair makes the 64-byte secret key (the seed followed by the public key) and the public key from 32 random bytes, and wipes the seed. secSign.c holds these three and secCurve.c the rest, so a program that links the sources in (the host proxy) can leave the signatures out. + +.h2 BASIC + +src/include/basic/security.bas (DECLARE LIBRARY "crypto") declares the BASIC functions (crypto/secBas.c). Keys, data, and results are BINARY STRING (pointer and length in, a result buffer and length out), so every byte value passes; the result buffer is wiped before it is reused. A handle (1, 2, ...; 0 = none) is a key exchange, a cipher, or an incremental SHA-256 / HMAC; each records the program that made it and the shell's app-cleanup hook frees (and wipes) what a program leaves open. + +.table + BASIC C it calls + ----- ---------- + SecRandom$(count), SecAddEntropy bytes secRngBytes; secRngAddEntropy. + SecDhCreate() secDhCreate + secDhGenerateKeys. + SecDhPublicKey$(h), SecDhSecret$(h) secDhGetPublicKey; secDhGetSecret ("" before SecDhComputeSecret). + SecDhComputeSecret(h, remotePublicKey) secDhComputeSecret: SEC_OK or a SEC_ERR_ code. + SecDhFree h secDhDestroy. + SecCipherCreate(key) secCipherCreate; 0 unless key is SEC_XTEA_KEY_SIZE bytes. + SecCipherCrypt$(h, bytes) secCipherCrypt on a copy. + SecCipherSetCounter h, counter secCipherSetCounter (a DOUBLE: exact up to 2^53). + SecCipherFree h secCipherDestroy. + SecSha256$(bytes); SecSha256Open, SecSha256Add, SecSha256End$ secSha256; secSha256Init / Update / Final (End frees the handle). + SecHmac$(key, bytes); SecHmacOpen, SecHmacAdd, SecHmacEnd$ secHmacSha256; secHmacInit / Update / Final. + SecHkdf$(salt, secret, info, length) secHkdf ("" for a length outside 1..SEC_HKDF_MAX_OUT). + SecEqual(a, b) secConstTimeEqual (False for different lengths). + SecX25519PublicKey$(secretKey) secX25519PublicKey ("" unless the key is SEC_X25519_KEY_SIZE bytes). + SecX25519$(secretKey, otherPublicKey) secX25519 ("" for a wrong size or a refused key). + SecX25519Start(secretKey, otherPublicKey) secX25519JobStart in a handle ("" otherPublicKey: the public key); 0 for wrong sizes. + SecX25519Step(h); SecX25519End$(h); SecX25519Free h secX25519JobStep (SEC_X25519_PENDING, then SEC_OK); secX25519JobResult (frees the handle); secX25519JobWipe. + SecAeadLock$(key, nonce, extra, bytes) secAeadLock on a copy: the ciphertext, then the tag. + SecAeadUnlock$(key, nonce, extra, sealed) secAeadUnlock on a copy ("" when the tag does not match). + SecEd25519KeyPair$(seed) secEd25519KeyPair: the secret key (the program's seed is not wiped). + SecEd25519PublicKey$(secretKey) The public key (the secret key's last SEC_ED25519_PUBLIC_SIZE bytes). + SecEd25519Sign$(secretKey, bytes) secEd25519Sign. + SecEd25519Check(signature, publicKey, bytes) secEd25519Check (False for wrong sizes). +.endtable + +The SEC_ CONSTs of security.bas are compared with security.h, secHash.h, and secCurve.h by a formrt test, which also calls every function for real. diff --git a/src/libs/kpunch/serial/security/secBas.c b/src/libs/kpunch/crypto/secBas.c similarity index 55% rename from src/libs/kpunch/serial/security/secBas.c rename to src/libs/kpunch/crypto/secBas.c index d01ae9da..2550ef4b 100644 --- a/src/libs/kpunch/serial/security/secBas.c +++ b/src/libs/kpunch/crypto/secBas.c @@ -23,9 +23,10 @@ // secBas.c -- DVX BASIC entry points of the security library // (src/include/basic/security.bas) // -// Random bytes, the Diffie-Hellman key exchange, the XTEA-CTR cipher, -// SHA-256, HMAC-SHA256, HKDF and a constant-time compare, over the same -// functions secLink and C programs use. Called through DECLARE LIBRARY: +// Random bytes, the Diffie-Hellman and X25519 key exchanges, the XTEA-CTR +// cipher, XChaCha20-Poly1305, Ed25519 signatures, SHA-256, HMAC-SHA256, +// HKDF and a constant-time compare, over the same functions secLink, the +// package library and C programs use. Called through DECLARE LIBRARY: // keys, data and results are BINARY STRING (pointer + length in, a result // buffer + length out; claudeDocs/round2/binaryStrings.md), so every byte // value passes. A result stays valid until the next call that returns @@ -36,133 +37,112 @@ // or HMAC in progress. Each handle records the program that made it; the // shell's app-cleanup hook frees (and wipes) what a program leaves open. +#include "secCurve.h" #include "security.h" #include "secHash.h" -#include "dvxMem.h" #include "dvxPlat.h" #include "dvxBas.h" -#include #include #include #include -#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup" +// ABI stamp checked by the loader (see DVX_ABI_VERSION). +DVX_ABI_STAMP(crypto); typedef enum { - BasSecFreeE, BasSecDhE, BasSecCipherE, BasSecSha256E, - BasSecHmacE + BasSecHmacE, + BasSecX25519E } BasSecKindE; -// One handle: what it holds and the program it belongs to +// What one handle holds (the handle table records its program) typedef struct { - void *obj; // SecDhT, SecCipherT, SecSha256T or SecHmacT - BasSecKindE kind; // BasSecFreeE: the handle is free - int32_t appId; + void *obj; // SecDhT, SecCipherT, SecSha256T, SecHmacT or SecX25519JobT + BasSecKindE kind; } BasSecT; -// The shell's app-cleanup registry (shellApp.h), looked up at run time: -// this library loads before the shell, and the host tests may have none. -typedef void (*BasCleanupFnT)(int32_t appId); -typedef void (*BasCleanupRegFnT)(BasCleanupFnT fn); - -static BasSecT *sHandles = NULL; // handle = index + 1 -static int32_t sHandleCount = 0; -static bool sCleanupHooked = false; -static uint8_t *sOut = NULL; // the last result -static int32_t sOutCap = 0; +static uint8_t *sOut = NULL; // the last result +static int32_t sOutCap = 0; // ============================================================ // Prototypes // ============================================================ -static void appEnded(int32_t appId); static int32_t handleNew(BasSecKindE kind, void *obj); static void *handleObj(int32_t handle, BasSecKindE kind); static void objFree(BasSecKindE kind, void *obj); static const uint8_t *result(int32_t len, int32_t *outLen); void SecAddEntropy(const uint8_t *data, int32_t len); +const uint8_t *SecAeadLock(const uint8_t *key, int32_t keyLen, const uint8_t *nonce, int32_t nonceLen, const uint8_t *ad, int32_t adLen, const uint8_t *plain, int32_t len, int32_t *outLen); +const uint8_t *SecAeadUnlock(const uint8_t *key, int32_t keyLen, const uint8_t *nonce, int32_t nonceLen, const uint8_t *ad, int32_t adLen, const uint8_t *sealed, int32_t len, int32_t *outLen); int32_t SecCipherCreate(const uint8_t *key, int32_t len); const uint8_t *SecCipherCrypt(int32_t handle, const uint8_t *data, int32_t len, int32_t *outLen); void SecCipherFree(int32_t handle); void SecCipherSetCounter(int32_t handle, double counter); int32_t SecDhComputeSecret(int32_t handle, const uint8_t *remotePub, int32_t len); +int32_t SecDhComputeStart(int32_t handle, const uint8_t *remotePub, int32_t len); int32_t SecDhCreate(void); +int32_t SecDhCreateStart(void); void SecDhFree(int32_t handle); const uint8_t *SecDhPublicKey(int32_t handle, int32_t *outLen); const uint8_t *SecDhSecret(int32_t handle, int32_t *outLen); +int32_t SecDhStep(int32_t handle); +int32_t SecEd25519Check(const uint8_t *sig, int32_t sigLen, const uint8_t *publicKey, int32_t publicLen, const uint8_t *msg, int32_t len); +const uint8_t *SecEd25519KeyPair(const uint8_t *seed, int32_t len, int32_t *outLen); +const uint8_t *SecEd25519PublicKey(const uint8_t *secretKey, int32_t len, int32_t *outLen); +const uint8_t *SecEd25519Sign(const uint8_t *secretKey, int32_t secretLen, const uint8_t *msg, int32_t len, int32_t *outLen); int32_t SecEqual(const uint8_t *a, int32_t aLen, const uint8_t *b, int32_t bLen); -static void secFree(int32_t handle); const uint8_t *SecHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, int32_t length, int32_t *outLen); const uint8_t *SecHmac(const uint8_t *key, int32_t keyLen, const uint8_t *data, int32_t len, int32_t *outLen); void SecHmacAdd(int32_t handle, const uint8_t *data, int32_t len); const uint8_t *SecHmacEnd(int32_t handle, int32_t *outLen); int32_t SecHmacOpen(const uint8_t *key, int32_t len); const uint8_t *SecRandom(int32_t count, int32_t *outLen); +static void secRelease(void *obj); const uint8_t *SecSha256(const uint8_t *data, int32_t len, int32_t *outLen); void SecSha256Add(int32_t handle, const uint8_t *data, int32_t len); const uint8_t *SecSha256End(int32_t handle, int32_t *outLen); int32_t SecSha256Open(void); +const uint8_t *SecX25519(const uint8_t *secretKey, int32_t secretLen, const uint8_t *peerPublic, int32_t peerLen, int32_t *outLen); +const uint8_t *SecX25519End(int32_t handle, int32_t *outLen); +void SecX25519Free(int32_t handle); +const uint8_t *SecX25519PublicKey(const uint8_t *secretKey, int32_t len, int32_t *outLen); +int32_t SecX25519Start(const uint8_t *secretKey, int32_t secretLen, const uint8_t *peerPublic, int32_t peerLen); +int32_t SecX25519Step(int32_t handle); +static BasHandlesT sHandles = BAS_HANDLES(secRelease, 0); // BasSecT -// Shell app-cleanup hook: free what a program that ended left open. -static void appEnded(int32_t appId) { - for (int32_t i = 0; i < sHandleCount; i++) { - if (sHandles[i].kind != BasSecFreeE && sHandles[i].appId == appId) { - secFree(i + 1); - } - } -} // A handle for obj (owned by it from now on), or 0 (obj freed) when out of // memory. static int32_t handleNew(BasSecKindE kind, void *obj) { - int32_t idx = 0; + BasSecT *h = (BasSecT *)malloc(sizeof(*h)); + int32_t handle = BAS_NO_HANDLE; - if (!sCleanupHooked) { - BasCleanupRegFnT reg = (BasCleanupRegFnT)dlsym(NULL, DVX_SYM(SHELL_CLEANUP_SYM)); - - sCleanupHooked = true; - - if (reg) { - reg(appEnded); - } + if (h) { + h->obj = obj; + h->kind = kind; + handle = basHandleAdd(&sHandles, h); } - while (idx < sHandleCount && sHandles[idx].kind != BasSecFreeE) { - idx++; + if (handle == BAS_NO_HANDLE) { + objFree(kind, obj); + free(h); } - if (idx == sHandleCount) { - BasSecT *grown = (BasSecT *)realloc(sHandles, (size_t)(sHandleCount + 1) * sizeof(BasSecT)); - - if (!grown) { - objFree(kind, obj); - return BAS_NO_HANDLE; - } - - sHandles = grown; - sHandleCount++; - } - - sHandles[idx].obj = obj; - sHandles[idx].kind = kind; - sHandles[idx].appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0; - return idx + 1; + return handle; } // What handle holds when it is a kind handle, else NULL. static void *handleObj(int32_t handle, BasSecKindE kind) { - if (handle <= BAS_NO_HANDLE || handle > sHandleCount || sHandles[handle - 1].kind != kind) { - return NULL; - } + const BasSecT *h = (const BasSecT *)basHandleGet(&sHandles, handle); - return sHandles[handle - 1].obj; + return (h && h->kind == kind) ? h->obj : NULL; } @@ -187,7 +167,9 @@ static void objFree(BasSecKindE kind, void *obj) { free(obj); break; - case BasSecFreeE: + case BasSecX25519E: + secX25519JobWipe((SecX25519JobT *)obj); + free(obj); break; } } @@ -228,6 +210,45 @@ void SecAddEntropy(const uint8_t *data, int32_t len) { } +// XChaCha20-Poly1305: plain encrypted with key (SEC_AEAD_KEY_SIZE bytes) +// and nonce (SEC_AEAD_NONCE_SIZE), the tag over it and ad after it; "" +// for a key or nonce of another length. +const uint8_t *SecAeadLock(const uint8_t *key, int32_t keyLen, const uint8_t *nonce, int32_t nonceLen, const uint8_t *ad, int32_t adLen, const uint8_t *plain, int32_t len, int32_t *outLen) { + *outLen = 0; + + if (keyLen != SEC_AEAD_KEY_SIZE || nonceLen != SEC_AEAD_NONCE_SIZE || len < 0 || !result(len + SEC_AEAD_TAG_SIZE, outLen)) { + return NULL; + } + + memcpy(sOut, plain, (size_t)len); + secAeadLock(key, nonce, ad, adLen, sOut, len, sOut + len); + return sOut; +} + + +// What SecAeadLock sealed, decrypted: "" when the tag does not match (the +// key, nonce, ad, or sealed bytes differ from the sealing) or a size is +// wrong. +const uint8_t *SecAeadUnlock(const uint8_t *key, int32_t keyLen, const uint8_t *nonce, int32_t nonceLen, const uint8_t *ad, int32_t adLen, const uint8_t *sealed, int32_t len, int32_t *outLen) { + int32_t plainLen = len - SEC_AEAD_TAG_SIZE; + + *outLen = 0; + + if (keyLen != SEC_AEAD_KEY_SIZE || nonceLen != SEC_AEAD_NONCE_SIZE || plainLen < 0 || !result(plainLen, outLen)) { + return NULL; + } + + memcpy(sOut, sealed, (size_t)plainLen); + + if (!secAeadUnlock(key, nonce, ad, adLen, sOut, plainLen, sealed + plainLen)) { + *outLen = 0; + return NULL; + } + + return sOut; +} + + // An XTEA-CTR cipher with key (SEC_XTEA_KEY_SIZE bytes), counter 0. 0 // for a key of another length. int32_t SecCipherCreate(const uint8_t *key, int32_t len) { @@ -266,7 +287,7 @@ const uint8_t *SecCipherCrypt(int32_t handle, const uint8_t *data, int32_t len, void SecCipherFree(int32_t handle) { if (handleObj(handle, BasSecCipherE)) { - secFree(handle); + basHandleFree(&sHandles, handle); } } @@ -295,6 +316,19 @@ int32_t SecDhComputeSecret(int32_t handle, const uint8_t *remotePub, int32_t len } +// The same a slice at a time: SEC_DH_PENDING (SecDhStep computes it), or +// SEC_ERR_PARAM for a bad handle or an unacceptable key (at once). +int32_t SecDhComputeStart(int32_t handle, const uint8_t *remotePub, int32_t len) { + SecDhT *dh = (SecDhT *)handleObj(handle, BasSecDhE); + + if (!dh || !remotePub) { + return SEC_ERR_PARAM; + } + + return secDhComputeStart(dh, remotePub, len); +} + + // A key exchange with fresh keys (secDhCreate + secDhGenerateKeys; can take // a few seconds on a slow computer). 0 when it could not be made. int32_t SecDhCreate(void) { @@ -313,9 +347,23 @@ int32_t SecDhCreate(void) { } +// A key exchange whose keys SecDhStep computes a slice at a time. 0 when +// it could not be made. +int32_t SecDhCreateStart(void) { + SecDhT *dh = secDhCreate(); + + if (!dh) { + return BAS_NO_HANDLE; + } + + secDhGenerateStart(dh); + return handleNew(BasSecDhE, dh); +} + + void SecDhFree(int32_t handle) { if (handleObj(handle, BasSecDhE)) { - secFree(handle); + basHandleFree(&sHandles, handle); } } @@ -366,6 +414,71 @@ const uint8_t *SecDhSecret(int32_t handle, int32_t *outLen) { } +// A slice of the keys or secret being computed: SEC_DH_PENDING while more +// is left, SEC_OK once ready, SEC_ERR_NOT_READY when nothing is. +int32_t SecDhStep(int32_t handle) { + SecDhT *dh = (SecDhT *)handleObj(handle, BasSecDhE); + + return dh ? secDhStep(dh) : SEC_ERR_PARAM; +} + + +// True when sig is publicKey's Ed25519 signature of msg. +int32_t SecEd25519Check(const uint8_t *sig, int32_t sigLen, const uint8_t *publicKey, int32_t publicLen, const uint8_t *msg, int32_t len) { + if (sigLen != SEC_ED25519_SIG_SIZE || publicLen != SEC_ED25519_PUBLIC_SIZE) { + return BAS_FALSE; + } + + return secEd25519Check(sig, publicKey, msg, len) ? BAS_TRUE : BAS_FALSE; +} + + +// An Ed25519 secret key (SEC_ED25519_SECRET_SIZE bytes, the public key in +// its last SEC_ED25519_PUBLIC_SIZE) from seed (SEC_ED25519_SEED_SIZE +// random bytes); "" for a seed of another length. +const uint8_t *SecEd25519KeyPair(const uint8_t *seed, int32_t len, int32_t *outLen) { + uint8_t copy[SEC_ED25519_SEED_SIZE]; + + *outLen = 0; + + if (len != SEC_ED25519_SEED_SIZE || !result(SEC_ED25519_SECRET_SIZE, outLen)) { + return NULL; + } + + // The key pair wipes the seed it is given; the program's stays + memcpy(copy, seed, sizeof(copy)); + secEd25519KeyPair(sOut, sOut + SEC_ED25519_SEED_SIZE, copy); + return sOut; +} + + +// The public key of an Ed25519 secret key; "" for a key of another length. +const uint8_t *SecEd25519PublicKey(const uint8_t *secretKey, int32_t len, int32_t *outLen) { + *outLen = 0; + + if (len != SEC_ED25519_SECRET_SIZE || !result(SEC_ED25519_PUBLIC_SIZE, outLen)) { + return NULL; + } + + memcpy(sOut, secretKey + SEC_ED25519_SEED_SIZE, SEC_ED25519_PUBLIC_SIZE); + return sOut; +} + + +// The Ed25519 signature (SEC_ED25519_SIG_SIZE bytes) of msg; "" for a +// secret key of another length. +const uint8_t *SecEd25519Sign(const uint8_t *secretKey, int32_t secretLen, const uint8_t *msg, int32_t len, int32_t *outLen) { + *outLen = 0; + + if (secretLen != SEC_ED25519_SECRET_SIZE || !result(SEC_ED25519_SIG_SIZE, outLen)) { + return NULL; + } + + secEd25519Sign(sOut, secretKey, msg, len); + return sOut; +} + + // True when a and b hold the same bytes, compared in a time that does not // depend on where they differ (for MACs and passwords). int32_t SecEqual(const uint8_t *a, int32_t aLen, const uint8_t *b, int32_t bLen) { @@ -377,16 +490,6 @@ int32_t SecEqual(const uint8_t *a, int32_t aLen, const uint8_t *b, int32_t bLen) } -// Free (and wipe) what handle holds. -static void secFree(int32_t handle) { - BasSecT *h = &sHandles[handle - 1]; - - objFree(h->kind, h->obj); - h->obj = NULL; - h->kind = BasSecFreeE; -} - - // HKDF-SHA256: length bytes (1 to SEC_HKDF_MAX_OUT) of key material from // ikm, with salt ("" = none) and info; "" for a length out of range. const uint8_t *SecHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, int32_t length, int32_t *outLen) { @@ -445,7 +548,7 @@ const uint8_t *SecHmacEnd(int32_t handle, int32_t *outLen) { secHmacFinal(h, sOut); } - secFree(handle); + basHandleFree(&sHandles, handle); return out; } @@ -479,6 +582,15 @@ const uint8_t *SecRandom(int32_t count, int32_t *outLen) { } +// sHandles' freeFn: free (and wipe) what a handle held. +static void secRelease(void *obj) { + BasSecT *h = (BasSecT *)obj; + + objFree(h->kind, h->obj); + free(h); +} + + // SHA-256 of data (SEC_SHA256_SIZE bytes). const uint8_t *SecSha256(const uint8_t *data, int32_t len, int32_t *outLen) { const uint8_t *out = result(SEC_SHA256_SIZE, outLen); @@ -518,7 +630,7 @@ const uint8_t *SecSha256End(int32_t handle, int32_t *outLen) { secSha256Final(ctx, sOut); } - secFree(handle); + basHandleFree(&sHandles, handle); return out; } @@ -534,3 +646,105 @@ int32_t SecSha256Open(void) { secSha256Init(ctx); return handleNew(BasSecSha256E, ctx); } + + +// The X25519 secret both sides share: this side's secret key and the +// other side's public key (SEC_X25519_KEY_SIZE bytes each); "" for a key +// of another length, or a public key that gives everyone the same secret. +const uint8_t *SecX25519(const uint8_t *secretKey, int32_t secretLen, const uint8_t *peerPublic, int32_t peerLen, int32_t *outLen) { + *outLen = 0; + + if (secretLen != SEC_X25519_KEY_SIZE || peerLen != SEC_X25519_KEY_SIZE || !result(SEC_X25519_KEY_SIZE, outLen)) { + return NULL; + } + + if (!secX25519(sOut, secretKey, peerPublic)) { + *outLen = 0; + return NULL; + } + + return sOut; +} + + +// What a SecX25519Start computed, once SecX25519Step said SEC_OK: "" +// before then, for a bad handle, or for a refused public key. Frees the +// handle. +const uint8_t *SecX25519End(int32_t handle, int32_t *outLen) { + SecX25519JobT *job = (SecX25519JobT *)handleObj(handle, BasSecX25519E); + uint8_t out[SEC_X25519_KEY_SIZE]; + bool ok; + + *outLen = 0; + + if (!job) { + return NULL; + } + + ok = secX25519JobResult(job, out); + basHandleFree(&sHandles, handle); + + if (!ok || !result(SEC_X25519_KEY_SIZE, outLen)) { + secSecureZero(out, sizeof(out)); + return NULL; + } + + memcpy(sOut, out, SEC_X25519_KEY_SIZE); + secSecureZero(out, sizeof(out)); + return sOut; +} + + +void SecX25519Free(int32_t handle) { + if (handleObj(handle, BasSecX25519E)) { + basHandleFree(&sHandles, handle); + } +} + + +// The X25519 public key of a secret key (SEC_X25519_KEY_SIZE random +// bytes); "" for a key of another length. +const uint8_t *SecX25519PublicKey(const uint8_t *secretKey, int32_t len, int32_t *outLen) { + *outLen = 0; + + if (len != SEC_X25519_KEY_SIZE || !result(SEC_X25519_KEY_SIZE, outLen)) { + return NULL; + } + + secX25519PublicKey(sOut, secretKey); + return sOut; +} + + +// SecX25519$ (or, for an empty peerPublic, SecX25519PublicKey$) a slice +// at a time: a handle that SecX25519Step works on and SecX25519End$ +// finishes; 0 for a key of another length. +int32_t SecX25519Start(const uint8_t *secretKey, int32_t secretLen, const uint8_t *peerPublic, int32_t peerLen) { + SecX25519JobT *job; + + if (secretLen != SEC_X25519_KEY_SIZE || (peerLen != 0 && peerLen != SEC_X25519_KEY_SIZE)) { + return BAS_NO_HANDLE; + } + + job = (SecX25519JobT *)malloc(sizeof(*job)); + + if (!job) { + return BAS_NO_HANDLE; + } + + secX25519JobStart(job, secretKey, peerLen ? peerPublic : NULL); + return handleNew(BasSecX25519E, job); +} + + +// A slice of a SecX25519Start: SEC_X25519_PENDING while there is more, +// then SEC_SUCCESS; SEC_ERR_PARAM for a bad handle. +int32_t SecX25519Step(int32_t handle) { + SecX25519JobT *job = (SecX25519JobT *)handleObj(handle, BasSecX25519E); + + if (!job) { + return SEC_ERR_PARAM; + } + + return secX25519JobStep(job) ? SEC_SUCCESS : SEC_X25519_PENDING; +} diff --git a/src/libs/kpunch/crypto/secCurve.c b/src/libs/kpunch/crypto/secCurve.c new file mode 100644 index 00000000..97b62810 --- /dev/null +++ b/src/libs/kpunch/crypto/secCurve.c @@ -0,0 +1,141 @@ +// 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. + +// secCurve.c -- the X25519 key exchange and XChaCha20-Poly1305 (see +// secCurve.h), over Monocypher. + +#include "secCurve.h" +#include "monocypher.h" + +#include +#include + +// Ladder bits a slice does: eight slices, then the inversion (about as +// much work as one) +#define JOB_SLICE_BITS 32 + +// A job is a SecX25519JobT on the outside; it holds the ladder state and +// whether the ladder is through +typedef struct { + crypto_x25519_ctx ctx; + uint8_t out[SEC_X25519_KEY_SIZE]; + bool laddered; + bool done; +} JobT; + +typedef char secCurveJobFits[(sizeof(SecX25519JobT) >= sizeof(JobT)) ? 1 : -1]; + +static const uint8_t sBasePoint[SEC_X25519_KEY_SIZE] = { 9 }; + +// ============================================================ +// Prototypes +// ============================================================ + +static bool allZero(const uint8_t *key); +void secAeadLock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, uint8_t *tag); +bool secAeadUnlock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, const uint8_t *tag); +bool secX25519(uint8_t *shared, const uint8_t *secretKey, const uint8_t *peerPublic); +bool secX25519JobResult(SecX25519JobT *job, uint8_t *out); +void secX25519JobStart(SecX25519JobT *job, const uint8_t *secretKey, const uint8_t *peerPublic); +bool secX25519JobStep(SecX25519JobT *job); +void secX25519JobWipe(SecX25519JobT *job); +void secX25519PublicKey(uint8_t *publicKey, const uint8_t *secretKey); + + +// The all-zero test ORs every byte so its time does not depend on where +// a byte is set. +static bool allZero(const uint8_t *key) { + uint8_t any = 0; + + for (int32_t i = 0; i < SEC_X25519_KEY_SIZE; i++) { + any |= key[i]; + } + + return any == 0; +} + + +// Monocypher encrypts and decrypts in place when the two buffers are one. +void secAeadLock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, uint8_t *tag) { + crypto_aead_lock(data, tag, key, nonce, ad, (size_t)adLen, data, (size_t)len); +} + + +bool secAeadUnlock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, const uint8_t *tag) { + return crypto_aead_unlock(data, tag, key, nonce, ad, (size_t)adLen, data, (size_t)len) == 0; +} + + +bool secX25519(uint8_t *shared, const uint8_t *secretKey, const uint8_t *peerPublic) { + crypto_x25519(shared, secretKey, peerPublic); + return !allZero(shared); +} + + +bool secX25519JobResult(SecX25519JobT *job, uint8_t *out) { + JobT *j = (JobT *)job; + bool ok = j->done && !allZero(j->out); + + if (j->done) { + memcpy(out, j->out, SEC_X25519_KEY_SIZE); + } + + secX25519JobWipe(job); + return ok; +} + + +void secX25519JobStart(SecX25519JobT *job, const uint8_t *secretKey, const uint8_t *peerPublic) { + JobT *j = (JobT *)job; + + secX25519JobWipe(job); + crypto_x25519_start(&j->ctx, secretKey, peerPublic ? peerPublic : sBasePoint); +} + + +// The ladder a slice at a time, then the inversion as a slice of its own. +bool secX25519JobStep(SecX25519JobT *job) { + JobT *j = (JobT *)job; + + if (j->done) { + return true; + } + + if (!j->laddered) { + j->laddered = crypto_x25519_step(&j->ctx, JOB_SLICE_BITS) != 0; + return false; + } + + crypto_x25519_finish(&j->ctx, j->out); + j->done = true; + return true; +} + + +void secX25519JobWipe(SecX25519JobT *job) { + crypto_wipe(job, sizeof(*job)); +} + + +void secX25519PublicKey(uint8_t *publicKey, const uint8_t *secretKey) { + crypto_x25519_public_key(publicKey, secretKey); +} diff --git a/src/libs/kpunch/crypto/secCurve.h b/src/libs/kpunch/crypto/secCurve.h new file mode 100644 index 00000000..491025f5 --- /dev/null +++ b/src/libs/kpunch/crypto/secCurve.h @@ -0,0 +1,88 @@ +// 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. + +// Curve25519 and XChaCha20-Poly1305 for the security library, on +// Monocypher (thirdparty/monocypher): the X25519 key exchange (RFC 7748), +// XChaCha20-Poly1305 authenticated encryption, and Ed25519 signatures +// (RFC 8032). secCurve.c holds the key exchange and the encryption (what +// the secure link needs), secSign.c the signatures (the package library). +// +// An X25519 takes about a sixth of a second on a 486, so a caller that +// must stay responsive does it a slice at a time (SecX25519JobT; secLink +// does). + +#ifndef SECCURVE_H +#define SECCURVE_H + +#include +#include + +#define SEC_X25519_KEY_SIZE 32 // secret, public, and shared +#define SEC_AEAD_KEY_SIZE 32 +#define SEC_AEAD_NONCE_SIZE 24 +#define SEC_AEAD_TAG_SIZE 16 +#define SEC_ED25519_SEED_SIZE 32 +#define SEC_ED25519_SECRET_SIZE 64 // the seed, then the public key +#define SEC_ED25519_PUBLIC_SIZE 32 +#define SEC_ED25519_SIG_SIZE 64 +#define SEC_X25519_JOB_WORDS 72 // room for the ladder state and its result +#define SEC_X25519_PENDING 1 // SecX25519Step (BASIC): more of the work to do + +// An X25519 under way (secX25519Job*): opaque, the caller's to keep +typedef struct { + uint32_t state[SEC_X25519_JOB_WORDS]; +} SecX25519JobT; + +// XChaCha20-Poly1305. secAeadLock encrypts data (len bytes, in place) +// and writes the tag over it and ad (adLen bytes, sent in the clear); +// secAeadUnlock checks the tag and decrypts in place only when it +// matches, else leaves data as it was and returns false. A key and +// nonce pair must never lock two different messages; a random nonce +// (secRngBytes) is safe. +void secAeadLock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, uint8_t *tag); +bool secAeadUnlock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, const uint8_t *tag); + +// Ed25519 (RFC 8032, SHA-512). secEd25519KeyPair makes the secret and +// public key from 32 random bytes; it wipes seed. secEd25519Check is +// true when sig is the key's signature of msg. +bool secEd25519Check(const uint8_t *sig, const uint8_t *publicKey, const uint8_t *msg, int32_t len); +void secEd25519KeyPair(uint8_t *secretKey, uint8_t *publicKey, uint8_t *seed); +void secEd25519Sign(uint8_t *sig, const uint8_t *secretKey, const uint8_t *msg, int32_t len); + +// X25519. secretKey is 32 random bytes (secRngBytes). secX25519 is +// false when the peer's key is one of the few that give everyone the same +// secret (an all-zero result): a peer that sends one is cheating. Make +// keys from the shared secret with a KDF (secHkdf); never use it as one. +bool secX25519(uint8_t *shared, const uint8_t *secretKey, const uint8_t *peerPublic); +void secX25519PublicKey(uint8_t *publicKey, const uint8_t *secretKey); + +// The same a slice at a time, each about a ninth of the work (about 20 ms +// on a 486): secX25519JobStart begins one (peerPublic NULL: the public +// key of secretKey), secX25519JobStep does a slice and is true once +// there is nothing left, and secX25519JobResult writes the result (false +// as secX25519 is) and wipes the job. secX25519JobWipe drops one. +bool secX25519JobResult(SecX25519JobT *job, uint8_t *out); +void secX25519JobStart(SecX25519JobT *job, const uint8_t *secretKey, const uint8_t *peerPublic); +bool secX25519JobStep(SecX25519JobT *job); +void secX25519JobWipe(SecX25519JobT *job); + +#endif diff --git a/src/libs/kpunch/serial/security/secHash.c b/src/libs/kpunch/crypto/secHash.c similarity index 96% rename from src/libs/kpunch/serial/security/secHash.c rename to src/libs/kpunch/crypto/secHash.c index 07e17e64..d122a860 100644 --- a/src/libs/kpunch/serial/security/secHash.c +++ b/src/libs/kpunch/crypto/secHash.c @@ -31,6 +31,7 @@ #include #include "secHash.h" #include "security.h" +#include "../libdvx/dvxEndian.h" // ======================================================================== @@ -40,6 +41,7 @@ #define SHA256_ROUNDS 64 #define SHA256_STATE_WORDS 8 #define SHA256_BLOCK_WORDS 16 +#define SHA256_WORD_BYTES 4 // the message and digest are big-endian words // Padding: a 0x80 marker byte, zeros, then the 64-bit bit length #define SHA256_PAD_MARKER 0x80 @@ -242,10 +244,7 @@ void secSha256Final(SecSha256T *ctx, uint8_t digest[SEC_SHA256_SIZE]) { sha256Block(ctx->state, ctx->buf); for (int32_t i = 0; i < SHA256_STATE_WORDS; i++) { - digest[i * 4] = (uint8_t)(ctx->state[i] >> 24); - digest[i * 4 + 1] = (uint8_t)(ctx->state[i] >> 16); - digest[i * 4 + 2] = (uint8_t)(ctx->state[i] >> 8); - digest[i * 4 + 3] = (uint8_t)(ctx->state[i]); + dvxPutBe32(digest + i * SHA256_WORD_BYTES, ctx->state[i]); } secSecureZero(ctx, sizeof(*ctx)); @@ -299,7 +298,7 @@ static void sha256Block(uint32_t state[SHA256_STATE_WORDS], const uint8_t *block uint32_t h = state[7]; for (int32_t i = 0; i < SHA256_BLOCK_WORDS; i++) { - w[i] = ((uint32_t)block[i * 4] << 24) | ((uint32_t)block[i * 4 + 1] << 16) | ((uint32_t)block[i * 4 + 2] << 8) | (uint32_t)block[i * 4 + 3]; + w[i] = dvxGetBe32(block + i * SHA256_WORD_BYTES); } for (int32_t i = SHA256_BLOCK_WORDS; i < SHA256_ROUNDS; i++) { diff --git a/src/libs/kpunch/serial/security/secHash.h b/src/libs/kpunch/crypto/secHash.h similarity index 100% rename from src/libs/kpunch/serial/security/secHash.h rename to src/libs/kpunch/crypto/secHash.h diff --git a/src/libs/kpunch/crypto/secSign.c b/src/libs/kpunch/crypto/secSign.c new file mode 100644 index 00000000..bf29c649 --- /dev/null +++ b/src/libs/kpunch/crypto/secSign.c @@ -0,0 +1,53 @@ +// 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. + +// secSign.c -- Ed25519 signatures (see secCurve.h), over Monocypher's +// RFC 8032 flavour (SHA-512, optional/monocypher-ed25519.c). + +#include "secCurve.h" +#include "monocypher.h" +#include "optional/monocypher-ed25519.h" + +#include + +// ============================================================ +// Prototypes +// ============================================================ + +bool secEd25519Check(const uint8_t *sig, const uint8_t *publicKey, const uint8_t *msg, int32_t len); +void secEd25519KeyPair(uint8_t *secretKey, uint8_t *publicKey, uint8_t *seed); +void secEd25519Sign(uint8_t *sig, const uint8_t *secretKey, const uint8_t *msg, int32_t len); + + +bool secEd25519Check(const uint8_t *sig, const uint8_t *publicKey, const uint8_t *msg, int32_t len) { + return crypto_ed25519_check(sig, publicKey, msg, (size_t)len) == 0; +} + + +void secEd25519KeyPair(uint8_t *secretKey, uint8_t *publicKey, uint8_t *seed) { + crypto_ed25519_key_pair(secretKey, publicKey, seed); +} + + +void secEd25519Sign(uint8_t *sig, const uint8_t *secretKey, const uint8_t *msg, int32_t len) { + crypto_ed25519_sign(sig, secretKey, msg, (size_t)len); +} diff --git a/src/libs/kpunch/serial/security/security.c b/src/libs/kpunch/crypto/security.c similarity index 85% rename from src/libs/kpunch/serial/security/security.c rename to src/libs/kpunch/crypto/security.c index 4c524923..ac25708c 100644 --- a/src/libs/kpunch/serial/security/security.c +++ b/src/libs/kpunch/crypto/security.c @@ -42,13 +42,12 @@ #include #include #include -#ifdef __DJGPP__ -#include -#include -#include -#endif #include "security.h" #include "secHash.h" +#include "../libdvx/dvxEndian.h" +#ifdef __DJGPP__ +#include "dvxPlat.h" +#endif // ======================================================================== @@ -59,6 +58,7 @@ #define BN_WORDS (BN_BITS / 32) #define BN_BYTES (BN_BITS / 8) #define BN_WORD_BITS 32 +#define BN_WORD_BYTES (BN_WORD_BITS / 8) // DH wire size (SEC_DH_KEY_SIZE) and bignum byte width (BN_BYTES) must agree; // they describe the same 1024-bit value and are used interchangeably. typedef char secDhKeySizeMatch[(BN_BYTES == SEC_DH_KEY_SIZE) ? 1 : -1]; @@ -79,6 +79,9 @@ typedef char secDhKeySizeMatch[(BN_BYTES == SEC_DH_KEY_SIZE) ? 1 : -1]; // Smallest valid remote public key (keys must lie in [2, p-2]) #define DH_MIN_PUBLIC 2 +// Ladder steps (exponent bits) secDhStep does: a sixteenth of the work +#define DH_STEP_BITS 16 + #define XTEA_ROUNDS 32 #define XTEA_DELTA 0x9E3779B9 #define XTEA_BLOCK_SIZE 8 // XTEA operates on 8-byte (two-uint32_t) blocks @@ -93,21 +96,11 @@ typedef char secDhKeySizeMatch[(BN_BYTES == SEC_DH_KEY_SIZE) ? 1 : -1]; #define RNG_COUNTER_BYTES 8 #ifdef __DJGPP__ -// Hardware entropy sources for the DRBG pool -#define PIT_CMD_PORT 0x43 // PIT mode/command register -#define PIT_CH0_PORT 0x40 // PIT channel 0 data port -#define PIT_LATCH_CMD 0x00 // Latch counter command for channel 0 -#define BIOS_TICK_ADDR 0x46C // BIOS tick count linear address (18.2 Hz) - -// PIT samples hashed per 32 bytes of gathered entropy. Each sample is +// Entropy stamps hashed per 32 bytes of gathered entropy. Each sample is // taken after a busy wait whose length depends on the previous reading, // so sampling instants accumulate interrupt, bus and cache jitter. #define RNG_GATHER_SAMPLES 256 #define RNG_SPIN_MASK 0x3F - -#define EFLAGS_ID 0x00200000u // toggleable only when CPUID exists -#define CPUID_LEAF_FEATURES 1 -#define CPUID_EDX_TSC 0x00000010u // time stamp counter present #else // Operating system random device used by host builds #define RNG_OS_DEVICE "/dev/urandom" @@ -122,10 +115,21 @@ typedef struct { uint32_t w[BN_WORDS]; } BigNumT; +// What a SecDhT is computing a step at a time +typedef enum { + DhIdleE, + DhKeysE, // the public key: g^private + DhSecretE // the shared secret: remote^private +} DhOpE; + struct SecDhS { BigNumT privateKey; BigNumT publicKey; BigNumT sharedSecret; + BigNumT r0; // the ladder under way (Montgomery form) + BigNumT r1; + int32_t bit; // its next exponent bit (counting down) + DhOpE op; bool hasKeys; bool hasSecret; }; @@ -141,14 +145,6 @@ typedef struct { bool gathered; } RngStateT; -#ifdef __DJGPP__ -typedef struct { - uint32_t tscLo; - uint32_t tscHi; - uint16_t pit; -} EntropySampleT; -#endif - // ======================================================================== // Static globals @@ -195,7 +191,8 @@ static void bnClear(BigNumT *a); static int bnCmp(const BigNumT *a, const BigNumT *b); static void bnCondSwap(BigNumT *a, BigNumT *b, uint32_t swap); static void bnFromBytes(BigNumT *a, const uint8_t *buf); -static void bnModExp(BigNumT *result, const BigNumT *base, const BigNumT *exp); +static void bnModExpStart(SecDhT *dh, const BigNumT *base); +static bool bnModExpStep(SecDhT *dh, int32_t bits, BigNumT *result); static void bnMontMul(BigNumT *result, const BigNumT *a, const BigNumT *b); static void bnSet(BigNumT *a, uint32_t val); static int bnShiftLeft1(BigNumT *a); @@ -203,9 +200,6 @@ static int bnSub(BigNumT *result, const BigNumT *a, const BigNumT *b); static void bnToBytes(uint8_t *buf, const BigNumT *a); static uint32_t computeM0Inv(uint32_t m0); static void computeR2(BigNumT *r2, const BigNumT *m); -#ifdef __DJGPP__ -static bool cpuHasTsc(void); -#endif static void dhInit(void); static void gatherBlock(uint8_t digest[SEC_SHA256_SIZE]); static void rngHashCounter(uint8_t out[SEC_SHA256_SIZE], uint8_t tag); @@ -214,11 +208,14 @@ void secCipherCrypt(SecCipherT *c, uint8_t *data, int len); void secCipherDestroy(SecCipherT *c); void secCipherSetCounter(SecCipherT *c, uint64_t counter); int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len); +int secDhComputeStart(SecDhT *dh, const uint8_t *remotePub, int len); SecDhT *secDhCreate(void); void secDhDestroy(SecDhT *dh); int secDhGenerateKeys(SecDhT *dh); +int secDhGenerateStart(SecDhT *dh); int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len); int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len); +int secDhStep(SecDhT *dh); void secRngAddEntropy(const uint8_t *data, int32_t len); void secRngBytes(uint8_t *buf, int32_t len); int32_t secRngGatherEntropy(uint8_t *buf, int32_t len); @@ -272,14 +269,13 @@ static void bnCondSwap(BigNumT *a, BigNumT *b, uint32_t swap) { static void bnFromBytes(BigNumT *a, const uint8_t *buf) { for (int i = 0; i < BN_WORDS; i++) { - int j = (BN_WORDS - 1 - i) * 4; - - a->w[i] = ((uint32_t)buf[j] << 24) | ((uint32_t)buf[j + 1] << 16) | ((uint32_t)buf[j + 2] << 8) | (uint32_t)buf[j + 3]; + a->w[i] = dvxGetBe32(buf + (BN_WORDS - 1 - i) * BN_WORD_BYTES); } } -// Modular exponentiation mod the DH prime using Montgomery multiplication. +// Modular exponentiation mod the DH prime using Montgomery multiplication, +// to the private key's power, a slice of its bits at a time. // // Montgomery multiplication replaces expensive modular reduction (division // by a 1024-bit number) with cheaper additions and right-shifts, at the @@ -290,33 +286,45 @@ static void bnFromBytes(BigNumT *a, const uint8_t *buf) { // Montgomery ladder over a fixed DH_PRIVATE_BITS exponent bits: every bit // costs exactly one multiply and one square, and the operands are chosen // with a branch-free conditional swap, so neither the operation count nor -// the memory access pattern depends on the exponent. Invariant: r1 = r0 * -// base. Exponents are private keys, always below 2^DH_PRIVATE_BITS. -static void bnModExp(BigNumT *result, const BigNumT *base, const BigNumT *exp) { - BigNumT r0; - BigNumT r1; +// the memory access pattern depends on the exponent (nor does where the +// slices end: a fixed number of bits each). Invariant: r1 = r0 * base. +// Private keys are always below 2^DH_PRIVATE_BITS. +static void bnModExpStart(SecDhT *dh, const BigNumT *base) { BigNumT one; bnSet(&one, 1); // r0 = 1 and r1 = base, both in Montgomery form - bnMontMul(&r0, &one, &sDhR2); - bnMontMul(&r1, base, &sDhR2); + bnMontMul(&dh->r0, &one, &sDhR2); + bnMontMul(&dh->r1, base, &sDhR2); + dh->bit = DH_PRIVATE_BITS - 1; +} - for (int i = DH_PRIVATE_BITS - 1; i >= 0; i--) { - uint32_t bit = (uint32_t)bnBit(exp, i); - bnCondSwap(&r0, &r1, bit); - bnMontMul(&r1, &r0, &r1); - bnMontMul(&r0, &r0, &r0); - bnCondSwap(&r0, &r1, bit); +// Up to bits more of the ladder; true once it is done, the power in result +// (back from Montgomery form) and the ladder's state wiped. +static bool bnModExpStep(SecDhT *dh, int32_t bits, BigNumT *result) { + BigNumT one; + + for (; bits > 0 && dh->bit >= 0; bits--, dh->bit--) { + uint32_t bit = (uint32_t)bnBit(&dh->privateKey, dh->bit); + + bnCondSwap(&dh->r0, &dh->r1, bit); + bnMontMul(&dh->r1, &dh->r0, &dh->r1); + bnMontMul(&dh->r0, &dh->r0, &dh->r0); + bnCondSwap(&dh->r0, &dh->r1, bit); + } + + if (dh->bit >= 0) { + return false; } // Convert back from Montgomery form: result = r0 * 1 * R^(-1) mod m - bnMontMul(result, &r0, &one); - - secSecureZero(&r0, sizeof(r0)); - secSecureZero(&r1, sizeof(r1)); + bnSet(&one, 1); + bnMontMul(result, &dh->r0, &one); + secSecureZero(&dh->r0, sizeof(dh->r0)); + secSecureZero(&dh->r1, sizeof(dh->r1)); + return true; } @@ -420,12 +428,7 @@ static int bnSub(BigNumT *result, const BigNumT *a, const BigNumT *b) { static void bnToBytes(uint8_t *buf, const BigNumT *a) { for (int i = 0; i < BN_WORDS; i++) { - int j = (BN_WORDS - 1 - i) * 4; - uint32_t w = a->w[i]; - buf[j] = (uint8_t)(w >> 24); - buf[j + 1] = (uint8_t)(w >> 16); - buf[j + 2] = (uint8_t)(w >> 8); - buf[j + 3] = (uint8_t)(w); + dvxPutBe32(buf + (BN_WORDS - 1 - i) * BN_WORD_BYTES, a->w[i]); } } @@ -463,47 +466,6 @@ static void computeR2(BigNumT *r2, const BigNumT *m) { } -#ifdef __DJGPP__ -// RDTSC exists when CPUID exists (EFLAGS.ID toggles) and leaf 1 reports -// the TSC feature. A 386 or an early 486 has neither. -static bool cpuHasTsc(void) { - uint32_t before; - uint32_t after; - uint32_t eax; - uint32_t ebx; - uint32_t ecx; - uint32_t edx; - - __asm__ __volatile__ ( - "pushfl\n\t" - "popl %%eax\n\t" - "movl %%eax, %0\n\t" - "xorl %2, %%eax\n\t" - "pushl %%eax\n\t" - "popfl\n\t" - "pushfl\n\t" - "popl %%eax\n\t" - "movl %%eax, %1\n\t" - "pushl %0\n\t" - "popfl" - : "=&r"(before), "=&r"(after) - : "i"(EFLAGS_ID) - : "eax", "cc" - ); - - if (((before ^ after) & EFLAGS_ID) == 0) { - return false; - } - - __asm__ __volatile__ ("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) : "a"(CPUID_LEAF_FEATURES)); - - return (edx & CPUID_EDX_TSC) != 0; -} - - -#endif - - static void dhInit(void) { if (sDhInited) { return; @@ -516,51 +478,30 @@ static void dhInit(void) { #ifdef __DJGPP__ -// Condense RNG_GATHER_SAMPLES hardware samples into one digest. Each -// sample is a latched PIT channel 0 reading (1.19 MHz) plus, on CPUs that -// have one, the full time stamp counter. The busy wait before each sample -// is sized by the previous PIT reading, so every sampling instant depends -// on all the jitter before it; the BIOS tick count anchors the digest to -// the time of day. The TSC alone contributes the unknown uptime at CPU -// clock resolution, far beyond what an attacker can enumerate. +// Condense RNG_GATHER_SAMPLES platform entropy stamps into one digest. +// Each is the latched PIT channel 0 counter (1.19 MHz, its own crystal) +// with the time stamp counter on CPUs that have one, else with the BIOS +// tick count. The busy wait before each sample is sized by the previous +// one, so every sampling instant depends on all the jitter before it. +// The TSC alone contributes the unknown uptime at CPU clock resolution, +// far beyond what an attacker can enumerate. static void gatherBlock(uint8_t digest[SEC_SHA256_SIZE]) { - SecSha256T ctx; - EntropySampleT sample; - uint32_t ticks; - bool hasTsc; - uint16_t prev; + SecSha256T ctx; + uint64_t sample = 0; secSha256Init(&ctx); - ticks = _farpeekl(_dos_ds, BIOS_TICK_ADDR); - secSha256Update(&ctx, (const uint8_t *)&ticks, sizeof(ticks)); - - hasTsc = cpuHasTsc(); - prev = 0; - memset(&sample, 0, sizeof(sample)); - for (int32_t i = 0; i < RNG_GATHER_SAMPLES; i++) { - volatile uint32_t spin = (uint32_t)(prev & RNG_SPIN_MASK) + 1; + volatile uint32_t spin = (uint32_t)(sample & RNG_SPIN_MASK) + 1; while (spin > 0) { spin--; } - outportb(PIT_CMD_PORT, PIT_LATCH_CMD); - sample.pit = inportb(PIT_CH0_PORT); - sample.pit |= (uint16_t)(inportb(PIT_CH0_PORT) << 8); - - if (hasTsc) { - __asm__ __volatile__ ("rdtsc" : "=a"(sample.tscLo), "=d"(sample.tscHi)); - } - + sample = platformEntropyStamp(); secSha256Update(&ctx, (const uint8_t *)&sample, sizeof(sample)); - prev = sample.pit; } - ticks = _farpeekl(_dos_ds, BIOS_TICK_ADDR); - secSha256Update(&ctx, (const uint8_t *)&ticks, sizeof(ticks)); - secSha256Final(&ctx, digest); secSecureZero(&sample, sizeof(sample)); } @@ -659,12 +600,23 @@ void secCipherSetCounter(SecCipherT *c, uint64_t counter) { } -// Compute the shared secret from the remote side's public key. +int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len) { + int rc = secDhComputeStart(dh, remotePub, len); + + while (rc == SEC_DH_PENDING) { + rc = secDhStep(dh); + } + + return rc; +} + + +// Start computing the shared secret from the remote side's public key. // Validates that the remote key is in [2, p-2] to prevent small-subgroup // attacks (keys of 0, 1, or p-1 would produce trivially guessable secrets). // The shared secret is remote^private mod p, which both sides compute // independently and arrive at the same value (the DH property). -int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len) { +int secDhComputeStart(SecDhT *dh, const uint8_t *remotePub, int len) { BigNumT remote; BigNumT low; BigNumT high; @@ -693,12 +645,13 @@ int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len) { return SEC_ERR_PARAM; } - // shared = remote^private mod p - bnModExp(&dh->sharedSecret, &remote, &dh->privateKey); - dh->hasSecret = true; + // shared = remote^private mod p, by secDhStep + bnModExpStart(dh, &remote); + dh->op = DhSecretE; + dh->hasSecret = false; secSecureZero(&remote, sizeof(remote)); - return SEC_SUCCESS; + return SEC_DH_PENDING; } @@ -718,10 +671,21 @@ void secDhDestroy(SecDhT *dh) { } -// Generate a DH keypair: random 256-bit private key, then compute +int secDhGenerateKeys(SecDhT *dh) { + int rc = secDhGenerateStart(dh); + + while (rc == SEC_DH_PENDING) { + rc = secDhStep(dh); + } + + return rc; +} + + +// Start a DH keypair: random 256-bit private key now, then (secDhStep) // public = g^private mod p. The private key is only 256 bits (not 1024) // to keep exponentiation fast on 486-class hardware. -int secDhGenerateKeys(SecDhT *dh) { +int secDhGenerateStart(SecDhT *dh) { if (!dh) { return SEC_ERR_PARAM; } @@ -737,13 +701,12 @@ int secDhGenerateKeys(SecDhT *dh) { // trivial). dh->privateKey.w[DH_PRIVATE_WORDS - 1] |= DH_PRIVATE_TOP_BIT; - // public = g^private mod p - bnModExp(&dh->publicKey, &sDhGenerator, &dh->privateKey); - - dh->hasKeys = true; + bnModExpStart(dh, &sDhGenerator); + dh->op = DhKeysE; + dh->hasKeys = false; dh->hasSecret = false; - return SEC_SUCCESS; + return SEC_DH_PENDING; } @@ -786,6 +749,37 @@ int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len) { // Hash the new bytes into the pool: pool = SHA-256(pool || tag || data). // Nothing already in the pool is lost, so this is safe to call at any // time and with any data, including data an attacker may know. +int secDhStep(SecDhT *dh) { + if (!dh) { + return SEC_ERR_PARAM; + } + + switch (dh->op) { + case DhKeysE: + if (!bnModExpStep(dh, DH_STEP_BITS, &dh->publicKey)) { + return SEC_DH_PENDING; + } + + dh->hasKeys = true; + break; + + case DhSecretE: + if (!bnModExpStep(dh, DH_STEP_BITS, &dh->sharedSecret)) { + return SEC_DH_PENDING; + } + + dh->hasSecret = true; + break; + + case DhIdleE: + return SEC_ERR_NOT_READY; + } + + dh->op = DhIdleE; + return SEC_SUCCESS; +} + + void secRngAddEntropy(const uint8_t *data, int32_t len) { SecSha256T ctx; uint8_t tag = RNG_TAG_ADD; diff --git a/src/libs/kpunch/serial/security/security.h b/src/libs/kpunch/crypto/security.h similarity index 85% rename from src/libs/kpunch/serial/security/security.h rename to src/libs/kpunch/crypto/security.h index 1b7cb17a..4e760ae6 100644 --- a/src/libs/kpunch/serial/security/security.h +++ b/src/libs/kpunch/crypto/security.h @@ -44,9 +44,9 @@ // RNG: // SHA-256 DRBG over an entropy pool. Adding entropy never discards what // is already in the pool. The first output auto-gathers hardware -// entropy (on DOS: hundreds of jittered PIT samples, the CPU time stamp -// counter when present, and the BIOS tick; elsewhere the OS random -// device). Callers should also feed user-input timings with +// entropy (on DOS: hundreds of jittered PIT samples, each with the CPU +// time stamp counter when present, else the BIOS tick; elsewhere the OS +// random device). Callers should also feed user-input timings with // secRngAddEntropy and carry entropy across runs with a seed file // (secRngLoadSeed at startup, secRngSaveSeed at shutdown). @@ -66,6 +66,7 @@ #define SEC_ERR_PARAM (-1) #define SEC_ERR_NOT_READY (-2) #define SEC_ERR_IO (-3) +#define SEC_DH_PENDING 1 // secDhStep: more of the exponentiation to do // Opaque types typedef struct SecDhS SecDhT; @@ -92,12 +93,22 @@ void secSecureZero(void *ptr, int len); // public key must lie in [2, p-2]. secDhGetSecret returns the shared // secret as SEC_DH_KEY_SIZE big-endian bytes for a KDF (never use it as a // key directly). +// +// An exponentiation takes about a second on a 486, so it can also be done +// a slice at a time: secDhGenerateStart or secDhComputeStart starts one +// (checking what it is given at once), and secDhStep does a slice of it +// (an eighth to a sixteenth of the work) until it says SEC_SUCCESS. +// secDhGenerateKeys and secDhComputeSecret do the whole of it at once. +// Starting another drops one under way. int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len); +int secDhComputeStart(SecDhT *dh, const uint8_t *remotePub, int len); SecDhT *secDhCreate(void); void secDhDestroy(SecDhT *dh); int secDhGenerateKeys(SecDhT *dh); +int secDhGenerateStart(SecDhT *dh); int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len); int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len); +int secDhStep(SecDhT *dh); // XTEA cipher in CTR mode. CTR mode turns a block cipher into a stream // cipher: encrypt an incrementing counter with the key to produce a diff --git a/src/libs/kpunch/pkg/thirdparty/monocypher/AUTHORS.md b/src/libs/kpunch/crypto/thirdparty/monocypher/AUTHORS.md similarity index 100% rename from src/libs/kpunch/pkg/thirdparty/monocypher/AUTHORS.md rename to src/libs/kpunch/crypto/thirdparty/monocypher/AUTHORS.md diff --git a/src/libs/kpunch/crypto/thirdparty/monocypher/DVX-VERSION.md b/src/libs/kpunch/crypto/thirdparty/monocypher/DVX-VERSION.md new file mode 100644 index 00000000..704d39cf --- /dev/null +++ b/src/libs/kpunch/crypto/thirdparty/monocypher/DVX-VERSION.md @@ -0,0 +1,54 @@ +# Monocypher for DVX + +| Item | Value | +|---|---| +| Project | Monocypher, a small, auditable crypto library, https://monocypher.org | +| Version | 4.0.3 (2026-06-15; fixes a timing leak in EdDSA/Ed25519 signing) | +| Download | https://monocypher.org/download/monocypher-4.0.3.tar.gz | +| Tarball SHA-512 | 40904ada5c7ee4f7741733e38b69a30a4b0561cbffba5ffe7c2dce16136d540251ec0d9056ff606510d3b5b708fb8a40db7e0870d4a0b2dc17ba2bfb880f8965 (matches the .sha512 published beside the download) | +| Files kept | src/monocypher.c, src/monocypher.h, src/optional/monocypher-ed25519.c, src/optional/monocypher-ed25519.h, LICENCE.md, AUTHORS.md -- byte for byte as in the tarball, in the same folders, but for the patch below. Not kept: doc/, tests/, the makefile, README, CHANGELOG. | +| Licence | Dual: 2-clause BSD or CC-0 (LICENCE.md); DVX takes the BSD licence. Keep the notice with the source. | +| Patches | One: dvx-x25519-step.patch (this folder), 2026-10-20 | + +## Patches + +`dvx-x25519-step.patch` adds `crypto_x25519_ctx` and +`crypto_x25519_start`, `crypto_x25519_step`, and `crypto_x25519_finish` +(monocypher.h, and after `crypto_x25519_public_key` in monocypher.c): the +Montgomery ladder of `scalarmult()` with its state kept in the caller's +context, so it can run a number of bits at a time. The steps are +scalarmult()'s, in the same order (the result is crypto_x25519()'s; the +X25519 vectors in src/test/serial check both). Why: one X25519 takes +165 ms on a 486DX2-66, too long to hold up every other program; +secCurve.c's SecX25519JobT does it in nine slices of about 20 ms. Nothing +else in Monocypher is changed. + +To reapply after an update: from this folder, `patch -p1 < +dvx-x25519-step.patch` (or by hand: the patch only adds lines), then +rebuild and run the tests below. + +## Where it is used + +- `crypto.lib` (src/libs/kpunch/crypto): the one copy in DVX. + secCurve.c wraps X25519 and XChaCha20-Poly1305 (the secure link's + protocol 3, security.bas), secSign.c wraps Ed25519 (the SHA-512 flavour + in optional/), which checks and makes the signatures of add-on packages + for the package library and its writer module. The library keeps only + what those wrappers reach (DXE_GC) and exports the wrappers alone. +- Programs that link the sources in rather than load crypto.lib: the + package tool (`bin/host/dvxpkg`, and DVXPKG.EXE in DVX), SETUP.EXE, the + host proxy (`bin/host/secproxy`, secCurve.c only), and the host tests. +- Host tests: RFC 7748 and the XChaCha20-Poly1305 draft's vectors + (src/test/serial), an RFC 8032 vector (src/test/pkg). + +It compiles with the normal DVX warning flags for DJGPP and the host; no +flags are relaxed for it. + +## Updating + +1. Download the new tarball, check its SHA-512 against the .sha512 file + on the download page, record the new version and hash here. +2. Replace the six files above, nothing else, and reapply the patch. +3. Rebuild (`make`), run `make -C src/test/serial` (the X25519 and + XChaCha20-Poly1305 vectors and the secure link), `make -C src/test/pkg` + (the RFC 8032 vector must still match), and `make -C src/test/setup`. diff --git a/src/libs/kpunch/pkg/thirdparty/monocypher/LICENCE.md b/src/libs/kpunch/crypto/thirdparty/monocypher/LICENCE.md similarity index 100% rename from src/libs/kpunch/pkg/thirdparty/monocypher/LICENCE.md rename to src/libs/kpunch/crypto/thirdparty/monocypher/LICENCE.md diff --git a/src/libs/kpunch/crypto/thirdparty/monocypher/dvx-x25519-step.patch b/src/libs/kpunch/crypto/thirdparty/monocypher/dvx-x25519-step.patch new file mode 100644 index 00000000..33ce0b6d --- /dev/null +++ b/src/libs/kpunch/crypto/thirdparty/monocypher/dvx-x25519-step.patch @@ -0,0 +1,95 @@ +--- a/src/monocypher.h ++++ b/src/monocypher.h +@@ -195,6 +195,23 @@ + const uint8_t your_secret_key [32], + const uint8_t their_public_key [32]); + ++// DVX patch (DVX-VERSION.md): crypto_x25519() a slice at a time, for ++// computers slow enough that one X-25519 holds up everything else. ++// start, then step until it returns 1, then finish (which wipes ctx). ++typedef struct { ++ int32_t x1[10], x2[10], z2[10], x3[10], z3[10]; ++ uint8_t scalar[32]; ++ int swap; ++ int pos; ++} crypto_x25519_ctx; ++ ++void crypto_x25519_start (crypto_x25519_ctx *ctx, ++ const uint8_t your_secret_key [32], ++ const uint8_t their_public_key [32]); ++int crypto_x25519_step (crypto_x25519_ctx *ctx, int nb_bits); ++void crypto_x25519_finish(crypto_x25519_ctx *ctx, ++ uint8_t raw_shared_secret[32]); ++ + // Conversion to EdDSA + void crypto_x25519_to_eddsa(uint8_t eddsa[32], const uint8_t x25519[32]); + +--- a/src/monocypher.c ++++ b/src/monocypher.c +@@ -1593,6 +1593,66 @@ + crypto_x25519(public_key, secret_key, base_point); + } + ++// DVX patch (DVX-VERSION.md): the ladder of scalarmult() kept in ctx, so ++// it can run nb_bits at a time. The same steps as scalarmult(), in the ++// same order, so the result is crypto_x25519()'s. ++void crypto_x25519_start(crypto_x25519_ctx *ctx, ++ const u8 your_secret_key [32], ++ const u8 their_public_key[32]) ++{ ++ crypto_eddsa_trim_scalar(ctx->scalar, your_secret_key); ++ fe_frombytes(ctx->x1, their_public_key); ++ fe_1(ctx->x2); fe_0(ctx->z2); ++ fe_copy(ctx->x3, ctx->x1); fe_1(ctx->z3); ++ ctx->swap = 0; ++ ctx->pos = 254; ++} ++ ++int crypto_x25519_step(crypto_x25519_ctx *ctx, int nb_bits) ++{ ++ fe t0, t1; ++ for (; nb_bits > 0 && ctx->pos >= 0; nb_bits--, ctx->pos--) { ++ int b = scalar_bit(ctx->scalar, ctx->pos); ++ ctx->swap ^= b; ++ fe_cswap(ctx->x2, ctx->x3, ctx->swap); ++ fe_cswap(ctx->z2, ctx->z3, ctx->swap); ++ ctx->swap = b; ++ ++ fe_sub(t0, ctx->x3, ctx->z3); ++ fe_sub(t1, ctx->x2, ctx->z2); ++ fe_add(ctx->x2, ctx->x2, ctx->z2); ++ fe_add(ctx->z2, ctx->x3, ctx->z3); ++ fe_mul(ctx->z3, t0, ctx->x2); ++ fe_mul(ctx->z2, ctx->z2, t1); ++ fe_sq (t0, t1); ++ fe_sq (t1, ctx->x2); ++ fe_add(ctx->x3, ctx->z3, ctx->z2); ++ fe_sub(ctx->z2, ctx->z3, ctx->z2); ++ fe_mul(ctx->x2, t1, t0); ++ fe_sub(t1, t1, t0); ++ fe_sq (ctx->z2, ctx->z2); ++ fe_mul_small(ctx->z3, t1, 121666); ++ fe_sq (ctx->x3, ctx->x3); ++ fe_add(t0, t0, ctx->z3); ++ fe_mul(ctx->z3, ctx->x1, ctx->z2); ++ fe_mul(ctx->z2, t1, t0); ++ } ++ WIPE_BUFFER(t0); ++ WIPE_BUFFER(t1); ++ return ctx->pos < 0; ++} ++ ++void crypto_x25519_finish(crypto_x25519_ctx *ctx, ++ u8 raw_shared_secret[32]) ++{ ++ fe_cswap(ctx->x2, ctx->x3, ctx->swap); ++ fe_cswap(ctx->z2, ctx->z3, ctx->swap); ++ fe_invert(ctx->z2, ctx->z2); ++ fe_mul(ctx->x2, ctx->x2, ctx->z2); ++ fe_tobytes(raw_shared_secret, ctx->x2); ++ crypto_wipe(ctx, sizeof(*ctx)); ++} ++ + /////////////////////////// + /// Arithmetic modulo L /// + /////////////////////////// diff --git a/src/libs/kpunch/pkg/thirdparty/monocypher/src/monocypher.c b/src/libs/kpunch/crypto/thirdparty/monocypher/src/monocypher.c similarity index 98% rename from src/libs/kpunch/pkg/thirdparty/monocypher/src/monocypher.c rename to src/libs/kpunch/crypto/thirdparty/monocypher/src/monocypher.c index fe5bbdc8..527b9114 100644 --- a/src/libs/kpunch/pkg/thirdparty/monocypher/src/monocypher.c +++ b/src/libs/kpunch/crypto/thirdparty/monocypher/src/monocypher.c @@ -1593,6 +1593,66 @@ void crypto_x25519_public_key(u8 public_key[32], crypto_x25519(public_key, secret_key, base_point); } +// DVX patch (DVX-VERSION.md): the ladder of scalarmult() kept in ctx, so +// it can run nb_bits at a time. The same steps as scalarmult(), in the +// same order, so the result is crypto_x25519()'s. +void crypto_x25519_start(crypto_x25519_ctx *ctx, + const u8 your_secret_key [32], + const u8 their_public_key[32]) +{ + crypto_eddsa_trim_scalar(ctx->scalar, your_secret_key); + fe_frombytes(ctx->x1, their_public_key); + fe_1(ctx->x2); fe_0(ctx->z2); + fe_copy(ctx->x3, ctx->x1); fe_1(ctx->z3); + ctx->swap = 0; + ctx->pos = 254; +} + +int crypto_x25519_step(crypto_x25519_ctx *ctx, int nb_bits) +{ + fe t0, t1; + for (; nb_bits > 0 && ctx->pos >= 0; nb_bits--, ctx->pos--) { + int b = scalar_bit(ctx->scalar, ctx->pos); + ctx->swap ^= b; + fe_cswap(ctx->x2, ctx->x3, ctx->swap); + fe_cswap(ctx->z2, ctx->z3, ctx->swap); + ctx->swap = b; + + fe_sub(t0, ctx->x3, ctx->z3); + fe_sub(t1, ctx->x2, ctx->z2); + fe_add(ctx->x2, ctx->x2, ctx->z2); + fe_add(ctx->z2, ctx->x3, ctx->z3); + fe_mul(ctx->z3, t0, ctx->x2); + fe_mul(ctx->z2, ctx->z2, t1); + fe_sq (t0, t1); + fe_sq (t1, ctx->x2); + fe_add(ctx->x3, ctx->z3, ctx->z2); + fe_sub(ctx->z2, ctx->z3, ctx->z2); + fe_mul(ctx->x2, t1, t0); + fe_sub(t1, t1, t0); + fe_sq (ctx->z2, ctx->z2); + fe_mul_small(ctx->z3, t1, 121666); + fe_sq (ctx->x3, ctx->x3); + fe_add(t0, t0, ctx->z3); + fe_mul(ctx->z3, ctx->x1, ctx->z2); + fe_mul(ctx->z2, t1, t0); + } + WIPE_BUFFER(t0); + WIPE_BUFFER(t1); + return ctx->pos < 0; +} + +void crypto_x25519_finish(crypto_x25519_ctx *ctx, + u8 raw_shared_secret[32]) +{ + fe_cswap(ctx->x2, ctx->x3, ctx->swap); + fe_cswap(ctx->z2, ctx->z3, ctx->swap); + fe_invert(ctx->z2, ctx->z2); + fe_mul(ctx->x2, ctx->x2, ctx->z2); + fe_tobytes(raw_shared_secret, ctx->x2); + crypto_wipe(ctx, sizeof(*ctx)); +} + /////////////////////////// /// Arithmetic modulo L /// /////////////////////////// diff --git a/src/libs/kpunch/pkg/thirdparty/monocypher/src/monocypher.h b/src/libs/kpunch/crypto/thirdparty/monocypher/src/monocypher.h similarity index 94% rename from src/libs/kpunch/pkg/thirdparty/monocypher/src/monocypher.h rename to src/libs/kpunch/crypto/thirdparty/monocypher/src/monocypher.h index 291b8b9e..203346cc 100644 --- a/src/libs/kpunch/pkg/thirdparty/monocypher/src/monocypher.h +++ b/src/libs/kpunch/crypto/thirdparty/monocypher/src/monocypher.h @@ -195,6 +195,23 @@ void crypto_x25519(uint8_t raw_shared_secret[32], const uint8_t your_secret_key [32], const uint8_t their_public_key [32]); +// DVX patch (DVX-VERSION.md): crypto_x25519() a slice at a time, for +// computers slow enough that one X-25519 holds up everything else. +// start, then step until it returns 1, then finish (which wipes ctx). +typedef struct { + int32_t x1[10], x2[10], z2[10], x3[10], z3[10]; + uint8_t scalar[32]; + int swap; + int pos; +} crypto_x25519_ctx; + +void crypto_x25519_start (crypto_x25519_ctx *ctx, + const uint8_t your_secret_key [32], + const uint8_t their_public_key [32]); +int crypto_x25519_step (crypto_x25519_ctx *ctx, int nb_bits); +void crypto_x25519_finish(crypto_x25519_ctx *ctx, + uint8_t raw_shared_secret[32]); + // Conversion to EdDSA void crypto_x25519_to_eddsa(uint8_t eddsa[32], const uint8_t x25519[32]); diff --git a/src/libs/kpunch/pkg/thirdparty/monocypher/src/optional/monocypher-ed25519.c b/src/libs/kpunch/crypto/thirdparty/monocypher/src/optional/monocypher-ed25519.c similarity index 100% rename from src/libs/kpunch/pkg/thirdparty/monocypher/src/optional/monocypher-ed25519.c rename to src/libs/kpunch/crypto/thirdparty/monocypher/src/optional/monocypher-ed25519.c diff --git a/src/libs/kpunch/pkg/thirdparty/monocypher/src/optional/monocypher-ed25519.h b/src/libs/kpunch/crypto/thirdparty/monocypher/src/optional/monocypher-ed25519.h similarity index 100% rename from src/libs/kpunch/pkg/thirdparty/monocypher/src/optional/monocypher-ed25519.h rename to src/libs/kpunch/crypto/thirdparty/monocypher/src/optional/monocypher-ed25519.h diff --git a/src/libs/kpunch/dvxnet/Makefile b/src/libs/kpunch/dvxnet/Makefile index 06443dd2..947e361a 100644 --- a/src/libs/kpunch/dvxnet/Makefile +++ b/src/libs/kpunch/dvxnet/Makefile @@ -40,7 +40,13 @@ LIBSDIR = ../../../../bin/libs SRCS = dvxNet.c netBas.c netDos.c netOrder.c netProto.c netResolve.c netSock.c netStack.c pktDrvDos.c pktRing.c LWIP_SRCS = $(wildcard $(LWIP_DIR)/core/*.c) $(wildcard $(LWIP_DIR)/core/ipv4/*.c) $(LWIP_DIR)/netif/ethernet.c -OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) $(patsubst $(LWIP_DIR)/%.c,$(OBJDIR)/lwip/%.o,$(LWIP_SRCS)) +OWN_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) +OBJS = $(OWN_OBJS) $(patsubst $(LWIP_DIR)/%.c,$(OBJDIR)/lwip/%.o,$(LWIP_SRCS)) + +# What the library's own objects (whole: the packet driver's interrupt +# code is laid out by file) reach of lwIP is all dvxnet.lib keeps, and it +# exports its own functions only (DXE_GC) +GC_OBJ = $(OBJDIR)/dvxnetGc.o TARGETDIR = $(LIBSDIR)/kpunch/dvxnet TARGET = $(TARGETDIR)/dvxnet.lib @@ -51,8 +57,11 @@ all: $(TARGET) $(TARGETDIR)/dvxnet.dep $(TARGETDIR)/dvxnet.dep: dvxnet.dep | $(TARGETDIR) sed 's/$$/\r/' $< > $@ -$(TARGET): $(OBJS) | $(TARGETDIR) - $(DXE3GEN) -o $(TARGETDIR)/dvxnet.dxe -U $(OBJS) +$(GC_OBJ): $(OBJS) + $(DXE_GC) $@ $(OWN_OBJS) -- $(OBJS) + +$(TARGET): $(GC_OBJ) | $(TARGETDIR) + $(DXE3GEN) -o $(TARGETDIR)/dvxnet.dxe -U $(GC_OBJ) mv $(TARGETDIR)/dvxnet.dxe $@ $(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR) @@ -60,7 +69,7 @@ $(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR) $(OBJDIR)/lwip/%.o: $(LWIP_DIR)/%.c $(DVX_MAKEFILES) @mkdir -p $(dir $@) - $(CC) $(CFLAGS) -c -o $@ $< + $(CC) $(CFLAGS) $(DXE_GC_CFLAGS) -c -o $@ $< $(OBJDIR): mkdir -p $(OBJDIR) diff --git a/src/libs/kpunch/dvxnet/dvxNet.h b/src/libs/kpunch/dvxnet/dvxNet.h index 558b67a7..5094b33e 100644 --- a/src/libs/kpunch/dvxnet/dvxNet.h +++ b/src/libs/kpunch/dvxnet/dvxNet.h @@ -50,6 +50,11 @@ #define NET_MAC_TEXT_MAX 18 // "00:11:22:33:44:55" + NUL #define NET_DRIVER_NAME_MAX 32 #define NET_MESSAGE_MAX 96 +#define NET_LOOKUP_NAME_MAX 256 // a DNS name and NUL + +// netLookupPoll while a lookup goes on (else 0, or an EAI_* code of +// , all at or below 0) +#define NET_LOOKUP_BUSY 1 // Packet driver software interrupt range searched when vector is 0 #define NET_VECTOR_AUTO 0 @@ -102,6 +107,20 @@ typedef struct { char message[NET_MESSAGE_MAX]; // plain-words state, e.g. "Waiting for a DHCP server" } NetStatusT; +// A name lookup that does not wait, as getaddrinfo does: netLookupStart, +// then netLookupPoll (from a Timer, or each frame) until it stops saying +// NET_LOOKUP_BUSY. It waits for DHCP's first address too, so a +// connection started after it goes at once. +typedef struct { + char name[NET_LOOKUP_NAME_MAX]; + int32_t owner; // the app it is for + int32_t slot; // the query sent (0: none) + int32_t result; // NET_LOOKUP_BUSY, or how it ended + uint32_t addr; // network order, once found + uint32_t since; // when its present wait began (ms) + bool awaiting; // waiting for DHCP's first address +} NetLookupT; + // Close every socket the app with this id still has open, and free the // BASIC network settings handles it left (the shell does this when an // app ends or is ended; callable by anyone). @@ -141,6 +160,20 @@ const char *netErrorText(int32_t err); // Current state, addresses and counters. void netGetStatus(NetStatusT *status); +// Give up a lookup that has not ended (its query is dropped); one that +// has is left as it is. +void netLookupCancel(NetLookupT *lk); + +// One look at a lookup: NET_LOOKUP_BUSY, 0 with *addr (network order), +// or an EAI_* code (; gai_strerror says it in words). Once +// ended it says the same again. +int32_t netLookupPoll(NetLookupT *lk, uint32_t *addr); + +// Start looking up name, a host name or a dotted address (which needs no +// lookup, only the wait for DHCP's first address). The stack starts +// itself. +void netLookupStart(NetLookupT *lk, const char *name); + // Let the stack do its work: take received frames, run timers, send. // The shell calls this every idle frame and every waiting socket call // calls it, so programs rarely need to. @@ -161,4 +194,12 @@ const char *netStateText(int32_t state); // Stop the stack and close every socket. void netStop(void); +// A net.bas TCP handle (TcpConnect, TcpAccept) as a byte stream that +// never waits, for another library's BASIC functions (comm.bas's +// CommOpenTcp runs the secure packet link over it): the bytes read (0 = +// none now) or taken (0 = no room now); negative once the connection has +// closed or failed. +int32_t netBasStreamRead(int32_t handle, uint8_t *buf, int32_t len); +int32_t netBasStreamWrite(int32_t handle, const uint8_t *data, int32_t len); + #endif diff --git a/src/libs/kpunch/dvxnet/dvxnet.dep b/src/libs/kpunch/dvxnet/dvxnet.dep index a9c88ea7..105be3c8 100644 --- a/src/libs/kpunch/dvxnet/dvxnet.dep +++ b/src/libs/kpunch/dvxnet/dvxnet.dep @@ -1,3 +1,4 @@ dvxshell libtasks libdvx +crypto diff --git a/src/libs/kpunch/dvxnet/dvxnet.dhs b/src/libs/kpunch/dvxnet/dvxnet.dhs index 6da7e123..e9e6115a 100644 --- a/src/libs/kpunch/dvxnet/dvxnet.dhs +++ b/src/libs/kpunch/dvxnet/dvxnet.dhs @@ -161,6 +161,7 @@ Sockets are real descriptors below FD_SETSIZE (256), at most 32 at a time. dup() .index Cooperative multitasking .index tsYield .index netPoll +.index netLookupStart .h1 Waiting, Polling, and Cooperative Multitasking @@ -168,7 +169,26 @@ DVX runs programs cooperatively, so a call that has to wait never stops the syst The network also runs from the shell's idle time, so connections progress (and data arrives) while a program is busy with other things; every socket call runs it too. A program that polls non-blocking sockets in its own loop needs nothing else. -Right after the network starts, DHCP needs a moment to get an address. connect, sendto, and name lookups wait for it (up to 20 seconds) instead of failing, so a program started together with DVX can connect at once. +Right after the network starts, DHCP needs a moment to get an address. Blocking connect and sendto calls, and name lookups, wait for it (up to 20 seconds) instead of failing, so a program started together with DVX can connect at once. A non-blocking socket does not wait. + +A program that must not wait at all, not even inside one call, looks a name up with netLookupStart and netLookupPoll (dvxNet.h) instead of getaddrinfo: each poll only looks, and the lookup waits for DHCP's first address too, so a non-blocking connect to the address it finds goes at once. DVX's own web requests (http) and Terminal connect this way. + +.code +NetLookupT lk; +uint32_t addr; +int32_t rc; + +netLookupStart(&lk, "bbs.example.org"); + +// Each frame, or from a timer: +rc = netLookupPoll(&lk, &addr); + +if (rc == 0) { + // addr (network order): connect a non-blocking socket to it +} else if (rc != NET_LOOKUP_BUSY) { + // gai_strerror(rc) says why +} +.endcode .h2 Sockets Belong to Programs @@ -195,6 +215,9 @@ The settings live in DVX.INI, section [network]: dhcp (true/false), address, net void netStop(void) Close every socket and stop (the packet driver is released). int32_t netRestart(void) Stop, then start with the saved settings (after they changed). void netPoll(void) Take received frames, run the timers, send. Called from the shell's idle time and by every socket call. + void netLookupStart(NetLookupT *lk, const char *name) Start looking a name (or a dotted address) up without waiting. + int32_t netLookupPoll(NetLookupT *lk, uint32_t *addr) NET_LOOKUP_BUSY, 0 with the address (network order), or an EAI_* code; never waits. + void netLookupCancel(NetLookupT *lk) Give up a lookup that has not ended. void netGetStatus(NetStatusT *status) State (NetStoppedE, NetNoDriverE, NetConfiguringE, NetUpE), plain-words message, addresses, name servers, host name, packet driver name, interrupt and hardware address, packet and byte counters, frames lost while DVX was busy, open sockets. const char *netStateText(int32_t state) Plain words for a state. const char *netErrorText(int32_t err) Plain words for an errno value, network codes included. @@ -229,7 +252,7 @@ Name servers set in the settings are used even with DHCP; left empty, the DHCP s Settings dvxNet.c DVX.INI, status, errors in plain words. .endtable -lwIP runs without threads (NO_SYS), configured only by port/lwipopts.h: TCP MSS 1460 with a 4-segment window, 16 TCP connections, 48 KB of stack memory, DHCP, DNS (2 servers), ARP, ICMP (ping answers), IP fragments, loopback. Nothing runs lwIP at interrupt time. +lwIP runs without threads (NO_SYS), configured only by port/lwipopts.h: TCP MSS 1460 with a 4-segment window, 16 TCP connections, 24 KB of stack memory for data being sent and 20 receive buffers, DHCP, DNS (2 servers), ARP, ICMP (ping answers), IP fragments, loopback. Nothing runs lwIP at interrupt time. The receive ring holds 16 frames. A burst larger than that while DVX is busy loses frames (counted as "lost" in the status); TCP sends them again. @@ -242,7 +265,7 @@ On the host the same sources are built into the "net" test suite: a fake packet .h1 Networking from BASIC -net.bas declares simple functions over the socket API (netBas.c): HostLookup$, HostName$, NetAddress$, NetError$, TcpConnect, TcpListen, TcpAccept, TcpSend, TcpPending, TcpRecv$, TcpConnected, TcpShutdown, TcpPeer$, TcpLocalPort, TcpSetNoDelay, TcpSetKeepAlive, TcpClose, UdpOpen, UdpSend, UdpRecv$, UdpFrom$, UdpClose. A handle is the socket's descriptor + 1, so 0 means failure. Only TcpConnect (up to 20 seconds) and TcpSend wait; everything else returns at once, so a program polls from a Timer. TcpSend, TcpRecv$, UdpSend, and UdpRecv$ pass their data as BINARY STRING (pointer and length), so every byte value goes through. See the BASIC language reference, Include Libraries. +net.bas declares simple functions over the socket API (netBas.c): HostLookup$, HostLookupStart / Poll / Result$ / Close, HostName$, NetAddress$, NetError$, TcpConnect, TcpConnectStart / Poll, TcpListen, TcpAccept, TcpSend, TcpSendSome, TcpPending, TcpRecv$, TcpPeek$ (recv with MSG_PEEK), TcpConnected, TcpShutdown, TcpPeer$, TcpLocalPort, TcpSetNoDelay, TcpSetKeepAlive, TcpClose, UdpOpen, UdpSend, UdpRecv$, UdpFrom$, UdpClose. A handle is the socket's descriptor + 1, so 0 means failure. Only TcpConnect (up to 20 seconds), HostLookup$, and TcpSend wait; everything else returns at once, so a program polls from a Timer. The other socket options need no binding: TcpListen sets SO_REUSEADDR and UdpOpen SO_BROADCAST, the stack takes SO_LINGER and the buffer sizes without effect (closing finishes in the background; the buffers are fixed), and no BASIC call waits on a receive, so SO_RCVTIMEO does nothing for it. TcpSend, TcpRecv$, UdpSend, and UdpRecv$ pass their data as BINARY STRING (pointer and length), so every byte value goes through. See the BASIC language reference, Include Libraries. .table BASIC C it calls @@ -269,6 +292,8 @@ The network settings and status (what the Control Panel's Network page shows) ar NetConfigFree(cfg) Free the handle. NetVectorCount / NetVector(i) The packet driver interrupt choices: 0 (find it), then NET_VECTOR_FIRST..NET_VECTOR_LAST. NetRestart() netRestart; True for NET_OK. + netBasStreamRead / netBasStreamWrite A net.bas TCP handle as a byte stream that never waits, for another library's BASIC functions (comm.bas's CommOpenTcp runs the secure packet link over it): bytes read or taken (0 = none now), negative once the connection has closed or failed. + NetState() netGetStatus's state (NetStateE; BASIC's NET_STATE_STOPPED, _NO_DRIVER, _CONFIGURING, _UP). NetStatus$(field) netGetStatus, one field as text: message, up, link, dhcp, address, netmask, gateway, dns1, dns2, hostname, driver, mac, vector, sockets, rxpackets, rxbytes, rxdropped, txpackets, txbytes. .endtable diff --git a/src/libs/kpunch/dvxnet/netBas.c b/src/libs/kpunch/dvxnet/netBas.c index a9f08b8b..c5d8c1de 100644 --- a/src/libs/kpunch/dvxnet/netBas.c +++ b/src/libs/kpunch/dvxnet/netBas.c @@ -24,8 +24,12 @@ // (src/include/basic/net.bas), written over the ordinary BSD socket API. // // A BASIC handle is the socket's descriptor + 1, so 0 means "failed". -// Everything but TcpConnect and TcpSend returns at once, so a program -// polls from a Timer (TcpPending, TcpRecv$, TcpAccept, UdpRecv$). What +// Everything but HostLookup$, TcpConnect, and TcpSend returns at once, so +// a program polls from a Timer (TcpPending, TcpRecv$, TcpAccept, +// UdpRecv$); those three have forms that do not wait either +// (HostLookupStart, TcpConnectStart, TcpSendSome), whose lookups and +// connections under way are kept here, for the program that started +// them. What // is sent and received is a BINARY STRING (pointer + length both ways, // claudeDocs/round2/binaryStrings.md), so every byte value passes. // Sockets belong to the program and are closed for it when it ends. @@ -41,6 +45,7 @@ #include "netInt.h" #include "dvxTypes.h" #include "dvxBas.h" +#include "thirdparty/stb_ds_wrap.h" #include #include @@ -48,6 +53,7 @@ #include #include #include +#include #include #include #include @@ -64,6 +70,9 @@ #define BAS_ENDPOINT_MAX (INET_ADDRSTRLEN + 8) #define BAS_PORT_MAX 65535 #define BAS_CONFIG_MAX 16 +#define BAS_WAIT_MAX 16 // lookups and connections under way at once +#define BAS_NO_FD (-1) +#define MS_PER_SEC 1000 #define BAS_FIELD_MAX NET_MESSAGE_MAX #define BAS_DNS_PREFIX "dns" #define BAS_DNS_FIRST 1 // "dns1" is the first name server @@ -75,12 +84,22 @@ // One settings handle typedef struct { - bool used; - int32_t appId; NetConfigT cfg; // being edited NetConfigT loaded; // as NetConfigLoad read it } BasNetConfigT; +// A lookup (HostLookupStart) or a connection (TcpConnectStart) under way +typedef struct { + int32_t handle; // its lookup handle + NetLookupT lookup; + int32_t fd; // a connection's socket; BAS_NO_FD for a lookup + BasNetWaitE result; + uint16_t port; // a connection's + bool connecting; // the connection is started (the lookup is done) + uint32_t addr; // found (network order) + uint32_t deadline; // when the connection gives up (netStackNow) +} BasNetWaitT; + // A text field of NetConfigT / NetStatusT by name typedef struct { const char *name; @@ -105,8 +124,7 @@ static const BasTextFieldT sStatusFields[] = { { "message", offsetof(NetStatusT, message), NET_MESSAGE_MAX } }; -static BasNetConfigT sConfigs[BAS_CONFIG_MAX]; -static char sField[BAS_FIELD_MAX]; +static char sField[BAS_FIELD_MAX]; static uint8_t sRecv[BAS_RECV_MAX]; static char sLookup[NI_MAXHOST]; @@ -126,9 +144,16 @@ static int32_t dnsIndex(const char *field); static const char *endpointText(const struct sockaddr_in *addr); static int32_t handleFd(int32_t handle); const char *HostLookup(const char *name); +void HostLookupClose(int32_t lookup); +int32_t HostLookupPoll(int32_t lookup); +const char *HostLookupResult(int32_t lookup); +int32_t HostLookupStart(const char *name); const char *HostName(const char *address); +static BasNetWaitT *lookupOf(int32_t lookup); const char *NetAddress(void); void netBasAppEnded(int32_t appId); +int32_t netBasStreamRead(int32_t handle, uint8_t *buf, int32_t len); +int32_t netBasStreamWrite(int32_t handle, const uint8_t *data, int32_t len); int32_t NetConfigChanged(int32_t config); void NetConfigFree(int32_t config); const char *NetConfigGet(int32_t config, const char *field); @@ -138,24 +163,30 @@ int32_t NetConfigSet(int32_t config, const char *field, const char int32_t NetConfigStore(int32_t config, int32_t prefs); const char *NetError(void); int32_t NetRestart(void); +int32_t NetState(void); const char *NetStatus(const char *field); void NetStop(void); int32_t NetVector(int32_t index); int32_t NetVectorCount(void); static int32_t newSocket(int32_t type); static int32_t noteError(void); +static const uint8_t *recvSome(int32_t handle, int flags, int32_t *outLen); static bool resolve(const char *host, int32_t port, struct sockaddr_in *out); static int32_t setFlag(int32_t handle, int level, int name, int32_t on); int32_t TcpAccept(int32_t handle); void TcpClose(int32_t handle); int32_t TcpConnect(const char *host, int32_t port); int32_t TcpConnected(int32_t handle); +int32_t TcpConnectPoll(int32_t handle); +int32_t TcpConnectStart(const char *host, int32_t port); int32_t TcpListen(int32_t port); int32_t TcpLocalPort(int32_t handle); +const uint8_t *TcpPeek(int32_t handle, int32_t *outLen); const char *TcpPeer(int32_t handle); int32_t TcpPending(int32_t handle); const uint8_t *TcpRecv(int32_t handle, int32_t *outLen); int32_t TcpSend(int32_t handle, const uint8_t *data, int32_t len); +int32_t TcpSendSome(int32_t handle, const uint8_t *data, int32_t len); int32_t TcpSetKeepAlive(int32_t handle, int32_t on); int32_t TcpSetNoDelay(int32_t handle, int32_t on); int32_t TcpShutdown(int32_t handle); @@ -165,6 +196,15 @@ const char *UdpFrom(void); int32_t UdpOpen(int32_t port); const uint8_t *UdpRecv(int32_t handle, int32_t *outLen); int32_t UdpSend(int32_t handle, const char *host, int32_t port, const uint8_t *data, int32_t len); +static void waitEnd(BasNetWaitT *w, BasNetWaitE result, int32_t err); +static void waitFree(BasNetWaitT *w); +static BasNetWaitT *waitNew(void); +static BasNetWaitT *waitOfFd(int32_t fd); +static void waitRelease(void *obj); +static void waitStep(BasNetWaitT *w); + +static BasHandlesT sConfigs = BAS_HANDLES(free, BAS_CONFIG_MAX); // BasNetConfigT +static BasHandlesT sWaits = BAS_HANDLES(waitRelease, BAS_WAIT_MAX); // BasNetWaitT // A yes / no field as text: "-1" (True) or "0" (False), so VAL gives @@ -176,11 +216,7 @@ static const char *basBoolText(bool value) { // The settings handle's entry, or NULL when it is not open. static BasNetConfigT *configOf(int32_t handle) { - if (handle < 1 || handle > BAS_CONFIG_MAX || !sConfigs[handle - 1].used) { - return NULL; - } - - return &sConfigs[handle - 1]; + return (BasNetConfigT *)basHandleGet(&sConfigs, handle); } @@ -232,6 +268,57 @@ const char *HostLookup(const char *name) { } +// Give up (or free) a lookup. +void HostLookupClose(int32_t lookup) { + waitFree(lookupOf(lookup)); +} + + +// BasNetBusyE while the lookup goes on; BasNetDoneE once the address is +// found (HostLookupResult$); BasNetFailedE (NetError$ says why). +int32_t HostLookupPoll(int32_t lookup) { + BasNetWaitT *w = lookupOf(lookup); + + if (!w) { + sLastError = EINVAL; + return BasNetFailedE; + } + + waitStep(w); + return w->result; +} + + +// The address a finished lookup found ("192.0.2.10"), "" before that. +const char *HostLookupResult(int32_t lookup) { + const BasNetWaitT *w = lookupOf(lookup); + struct in_addr in; + + sLookup[0] = '\0'; + + if (w && w->result == BasNetDoneE) { + in.s_addr = w->addr; + inet_ntop(AF_INET, &in, sLookup, sizeof(sLookup)); + } + + return sLookup; +} + + +// Start looking name up without waiting: a lookup handle (poll it with +// HostLookupPoll), or 0 when too many are under way. +int32_t HostLookupStart(const char *name) { + BasNetWaitT *w = waitNew(); + + if (!w) { + return 0; + } + + netLookupStart(&w->lookup, name ? name : ""); + return w->handle; +} + + // The name of an address ("192.0.2.10"), through getnameinfo: the // address itself when no name is known for it, "" when address is not // one (NetError$ says why). @@ -259,6 +346,14 @@ const char *HostName(const char *address) { } +// A lookup handle's entry (not a connection's), or NULL. +static BasNetWaitT *lookupOf(int32_t lookup) { + BasNetWaitT *w = (BasNetWaitT *)basHandleGet(&sWaits, lookup); + + return (w && w->fd == BAS_NO_FD) ? w : NULL; +} + + const char *NetAddress(void) { NetStatusT st; @@ -269,13 +364,38 @@ const char *NetAddress(void) { } -// Free what an app that ended left open (netCloseAppSockets). +// Free what an app that ended left open (netCloseAppSockets closes its +// sockets). void netBasAppEnded(int32_t appId) { - for (int32_t i = 0; i < BAS_CONFIG_MAX; i++) { - if (sConfigs[i].used && sConfigs[i].appId == appId) { - sConfigs[i].used = false; - } + basHandlesAppEnded(&sConfigs, appId); + basHandlesAppEnded(&sWaits, appId); +} + + +int32_t netBasStreamRead(int32_t handle, uint8_t *buf, int32_t len) { + ssize_t n = recv(handleFd(handle), buf, (size_t)len, MSG_DONTWAIT); + + // 0 bytes from a read that asked for some: the other side closed it + if (n == 0) { + return -1; } + + if (n < 0) { + return (errno == EWOULDBLOCK) ? 0 : -1; + } + + return (int32_t)n; +} + + +int32_t netBasStreamWrite(int32_t handle, const uint8_t *data, int32_t len) { + ssize_t n = send(handleFd(handle), data, (size_t)len, MSG_DONTWAIT); + + if (n < 0) { + return (errno == EWOULDBLOCK) ? 0 : -1; + } + + return (int32_t)n; } @@ -288,11 +408,7 @@ int32_t NetConfigChanged(int32_t config) { void NetConfigFree(int32_t config) { - BasNetConfigT *c = configOf(config); - - if (c) { - c->used = false; - } + basHandleFree(&sConfigs, config); } @@ -325,17 +441,22 @@ const char *NetConfigGet(int32_t config, const char *field) { // A handle on the [network] settings of a prefs.bas handle (the // defaults for 0); 0 when every handle is in use. int32_t NetConfigLoad(int32_t prefs) { - for (int32_t i = 0; i < BAS_CONFIG_MAX; i++) { - if (!sConfigs[i].used) { - netConfigFromPrefs(prefsBasHandle(prefs), &sConfigs[i].cfg); - sConfigs[i].loaded = sConfigs[i].cfg; - sConfigs[i].appId = netSockAppId(); - sConfigs[i].used = true; - return i + 1; - } + BasNetConfigT *c = (BasNetConfigT *)malloc(sizeof(*c)); + + if (!c) { + return BAS_NO_HANDLE; } - return 0; + netConfigFromPrefs(prefsBasHandle(prefs), &c->cfg); + c->loaded = c->cfg; + + int32_t handle = basHandleAdd(&sConfigs, c); + + if (handle == BAS_NO_HANDLE) { + free(c); + } + + return handle; } @@ -417,6 +538,15 @@ int32_t NetRestart(void) { } +// The network's state as a number (NetStateE: BASIC's NET_STATE_*). +int32_t NetState(void) { + NetStatusT st; + + netGetStatus(&st); + return st.state; +} + + // The network's state as text: "message" (in plain words), "up", "link", // "dhcp" ("-1" / "0"), "address", "netmask", "gateway", "dns1", "dns2", // "hostname", "driver", "mac", "vector", "sockets", "rxpackets", @@ -513,6 +643,24 @@ static int32_t noteError(void) { } +// Up to BAS_RECV_MAX bytes of what has arrived, never waiting (flags: +// MSG_PEEK leaves them there). *outLen 0 when nothing has. +static const uint8_t *recvSome(int32_t handle, int flags, int32_t *outLen) { + ssize_t n = recv(handleFd(handle), sRecv, BAS_RECV_MAX, flags | MSG_DONTWAIT); + + if (n < 0) { + if (errno != EWOULDBLOCK) { + noteError(); + } + + n = 0; + } + + *outLen = (int32_t)n; + return sRecv; +} + + static bool resolve(const char *host, int32_t port, struct sockaddr_in *out) { struct addrinfo hints; struct addrinfo *res = NULL; @@ -572,7 +720,9 @@ int32_t TcpAccept(int32_t handle) { } +// Close it (a connection still being made stops). void TcpClose(int32_t handle) { + waitFree(waitOfFd(handleFd(handle))); close(handleFd(handle)); } @@ -624,6 +774,66 @@ int32_t TcpConnected(int32_t handle) { } +// BasNetBusyE while the connection TcpConnectStart started is being made; +// BasNetDoneE once it is (the handle then works as TcpConnect's); +// BasNetFailedE (NetError$ says why; close it). +int32_t TcpConnectPoll(int32_t handle) { + BasNetWaitT *w = waitOfFd(handleFd(handle)); + + if (!w) { + return TcpConnected(handle) ? BasNetDoneE : BasNetFailedE; + } + + waitStep(w); + + BasNetWaitE result = w->result; + + if (result != BasNetBusyE) { + waitFree(w); + } + + return result; +} + + +// Start connecting to host on port without waiting: the connection's +// handle at once (poll it with TcpConnectPoll), or 0 when it cannot +// start (NetError$). +int32_t TcpConnectStart(const char *host, int32_t port) { + BasNetWaitT *w; + int32_t handle; + int on = 1; + + if (!host || port <= 0 || port > BAS_PORT_MAX) { + sLastError = EINVAL; + return 0; + } + + w = waitNew(); + + if (!w) { + return 0; + } + + handle = newSocket(SOCK_STREAM); + + if (handle == 0 || ioctl(handleFd(handle), FIONBIO, &on) != 0) { + if (handle != 0) { + noteError(); + close(handleFd(handle)); + } + + waitFree(w); + return 0; + } + + w->fd = handleFd(handle); + w->port = (uint16_t)port; + netLookupStart(&w->lookup, host); + return handle; +} + + int32_t TcpListen(int32_t port) { struct sockaddr_in addr; int on = 1; @@ -671,6 +881,14 @@ int32_t TcpLocalPort(int32_t handle) { } +// What has arrived (up to BAS_RECV_MAX bytes), left waiting for the next +// TcpRecv$: a program can look for the end of a line or a message's +// length before it takes any of it. "" when nothing has. +const uint8_t *TcpPeek(int32_t handle, int32_t *outLen) { + return recvSome(handle, MSG_PEEK, outLen); +} + + // "address:port" of the other side of a connection, "" when it is not // connected. const char *TcpPeer(int32_t handle) { @@ -701,18 +919,7 @@ int32_t TcpPending(int32_t handle) { // What has arrived (up to BAS_RECV_MAX bytes), "" when nothing has. const uint8_t *TcpRecv(int32_t handle, int32_t *outLen) { - ssize_t n = recv(handleFd(handle), sRecv, BAS_RECV_MAX, MSG_DONTWAIT); - - if (n < 0) { - if (errno != EWOULDBLOCK) { - noteError(); - } - - n = 0; - } - - *outLen = (int32_t)n; - return sRecv; + return recvSome(handle, 0, outLen); } @@ -730,6 +937,24 @@ int32_t TcpSend(int32_t handle, const uint8_t *data, int32_t len) { } +// Send what the connection takes now, never waiting: the bytes taken (0 +// while its buffer is full), -1 on failure. +int32_t TcpSendSome(int32_t handle, const uint8_t *data, int32_t len) { + ssize_t n = send(handleFd(handle), data, (size_t)len, MSG_DONTWAIT); + + if (n >= 0) { + return (int32_t)n; + } + + if (errno == EWOULDBLOCK) { + return 0; + } + + noteError(); + return -1; +} + + // Keep-alive probes on an idle connection (SO_KEEPALIVE): True when set. int32_t TcpSetKeepAlive(int32_t handle, int32_t on) { return setFlag(handle, SOL_SOCKET, SO_KEEPALIVE, on); @@ -860,3 +1085,141 @@ int32_t UdpSend(int32_t handle, const char *host, int32_t port, const uint8_t *d return (int32_t)n; } + + +// The wait is over: result, with the error NetError$ gives (0: none). +static void waitEnd(BasNetWaitT *w, BasNetWaitE result, int32_t err) { + w->result = result; + + if (err) { + sLastError = err; + } +} + + +// Free a wait and its handle. NULL is ignored. +static void waitFree(BasNetWaitT *w) { + if (w) { + basHandleFree(&sWaits, w->handle); + } +} + + +// A new wait, with its handle, for the calling program; NULL (noted) +// when too many are under way. +static BasNetWaitT *waitNew(void) { + BasNetWaitT *w = (BasNetWaitT *)calloc(1, sizeof(*w)); + + if (w) { + w->fd = BAS_NO_FD; + w->result = BasNetBusyE; + w->handle = basHandleAdd(&sWaits, w); + + if (w->handle == BAS_NO_HANDLE) { + free(w); + w = NULL; + } + } + + if (!w) { + sLastError = EMFILE; + } + + return w; +} + + +// The connection under way on socket fd, or NULL. +static BasNetWaitT *waitOfFd(int32_t fd) { + for (int32_t i = 0; fd != BAS_NO_FD && i < (int32_t)arrlen(sWaits.objs); i++) { + BasNetWaitT *w = (BasNetWaitT *)sWaits.objs[i]; + + if (w && w->fd == fd) { + return w; + } + } + + return NULL; +} + + +// sWaits' freeFn: a lookup under way is given up. +static void waitRelease(void *obj) { + BasNetWaitT *w = (BasNetWaitT *)obj; + + netLookupCancel(&w->lookup); + free(w); +} + + +// One look at a wait: its lookup, then (a connection) the connecting +// within BAS_CONNECT_SECS; never waiting. Done, a connection's socket +// works as a blocking one again, as TcpConnect's does. +static void waitStep(BasNetWaitT *w) { + struct sockaddr_in to; + struct pollfd p; + int err = 0; + int off = 0; + socklen_t len = sizeof(err); + + if (w->result != BasNetBusyE) { + return; + } + + if (!w->connecting) { + int32_t rc = netLookupPoll(&w->lookup, &w->addr); + + if (rc == NET_LOOKUP_BUSY) { + return; + } + + if (rc != 0) { + waitEnd(w, BasNetFailedE, EHOSTUNREACH); + return; + } + + if (w->fd == BAS_NO_FD) { + waitEnd(w, BasNetDoneE, 0); + return; + } + + memset(&to, 0, sizeof(to)); + to.sin_family = AF_INET; + to.sin_port = htons(w->port); + to.sin_addr.s_addr = w->addr; + w->connecting = true; + w->deadline = netStackNow() + BAS_CONNECT_SECS * MS_PER_SEC; + + if (connect(w->fd, (struct sockaddr *)&to, sizeof(to)) == 0) { + ioctl(w->fd, FIONBIO, &off); + waitEnd(w, BasNetDoneE, 0); + return; + } + + if (errno != EINPROGRESS) { + waitEnd(w, BasNetFailedE, errno); + return; + } + } + + p.fd = w->fd; + p.events = POLLOUT; + p.revents = 0; + + if (poll(&p, 1, 0) > 0) { + getsockopt(w->fd, SOL_SOCKET, SO_ERROR, &err, &len); + + if (err == 0 && !(p.revents & (POLLERR | POLLHUP))) { + ioctl(w->fd, FIONBIO, &off); + waitEnd(w, BasNetDoneE, 0); + } else { + waitEnd(w, BasNetFailedE, err ? err : ECONNREFUSED); + } + + return; + } + + if ((int32_t)(netStackNow() - w->deadline) >= 0) { + waitEnd(w, BasNetFailedE, ETIMEDOUT); + } +} diff --git a/src/libs/kpunch/dvxnet/netInt.h b/src/libs/kpunch/dvxnet/netInt.h index 4ac73263..f0e04ad0 100644 --- a/src/libs/kpunch/dvxnet/netInt.h +++ b/src/libs/kpunch/dvxnet/netInt.h @@ -69,6 +69,10 @@ typedef enum { // Largest UDP payload accepted for sending #define PROTO_UDP_MAX 8192 +// Datagrams a UDP socket holds unread (more are dropped); each holds a +// receive buffer (PBUF_POOL_SIZE, lwipopts.h) +#define PROTO_DGRAM_QUEUE 16 + // Protocol object (defined in netProto.c) typedef struct NetProtoS NetProtoT; @@ -77,6 +81,9 @@ typedef struct { uint16_t port; // host byte order } NetEndpointT; +// The longest wait for DHCP's first address +#define NET_DHCP_WAIT_MS 20000 + // ---- netStack.c ---- // Start the stack with cfg (NET_OK or NET_ERR_*). Already running: NET_OK. @@ -105,6 +112,10 @@ void netStackWaitStep(void); // DVX can connect at once. Returns at once otherwise. void netStackAwaitAddress(void); +// True while DHCP is still getting the first address (netLookupPoll +// waits for it a look at a time). +bool netStackAwaitingAddress(void); + // Milliseconds since an arbitrary start (lwIP's sys_now). uint32_t netStackNow(void); @@ -182,7 +193,16 @@ void protoResolveDropOwner(int32_t owner); // ---- netBas.c ---- -// Free the network settings handles (NetConfigLoad) an app left. +// HostLookupPoll's and TcpConnectPoll's results: net.bas's NET_BUSY, +// NET_DONE, and NET_FAILED, which must not change +typedef enum { + BasNetBusyE, + BasNetDoneE, + BasNetFailedE +} BasNetWaitE; + +// Free the network settings handles (NetConfigLoad) and the lookups and +// connections under way (HostLookupStart, TcpConnectStart) an app left. void netBasAppEnded(int32_t appId); // ---- netSock.c ---- diff --git a/src/libs/kpunch/dvxnet/netProto.c b/src/libs/kpunch/dvxnet/netProto.c index 7b74086f..362ce8ae 100644 --- a/src/libs/kpunch/dvxnet/netProto.c +++ b/src/libs/kpunch/dvxnet/netProto.c @@ -46,7 +46,6 @@ #include #include -#define PROTO_DGRAM_QUEUE 16 #define PROTO_BACKLOG_MAX 16 #define PROTO_RESOLVE_SLOTS 8 #define PROTO_TCP_CHUNK_MAX 0xFFFF // tcp_write / tcp_recved take a u16_t diff --git a/src/libs/kpunch/dvxnet/netResolve.c b/src/libs/kpunch/dvxnet/netResolve.c index 425705f0..02966204 100644 --- a/src/libs/kpunch/dvxnet/netResolve.c +++ b/src/libs/kpunch/dvxnet/netResolve.c @@ -22,9 +22,10 @@ // netResolve.c -- name lookups and address text (see netInt.h): // getaddrinfo & co., gethostbyname/gethostbyaddr, gethostname and the -// inet_* conversions. Names go to lwIP's DNS resolver; the caller waits -// in netStackWaitStep, so other programs run meanwhile. There is no -// reverse lookup: an address's "name" is its dotted text. +// inet_* conversions, and lookups that do not wait (netLookupStart). +// Names go to lwIP's DNS resolver; getaddrinfo's caller waits in +// netStackWaitStep, so other programs run meanwhile. There is no reverse +// lookup: an address's "name" is its dotted text. #include "netInt.h" @@ -102,7 +103,23 @@ static char sNtoa[INET_ADDRSTRLEN]; // ============================================================ static struct hostent *fillHost(const char *name, uint32_t addr); +void freeaddrinfo(struct addrinfo *res); +const char *gai_strerror(int err); +int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); +struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type); +struct hostent *gethostbyname(const char *name); +int gethostname(char *name, NetNameLenT len); +int getnameinfo(const struct sockaddr *addr, socklen_t addrLen, char *host, socklen_t hostLen, char *serv, socklen_t servLen, int flags); +in_addr_t inet_addr(const char *text); +int inet_aton(const char *text, struct in_addr *addr); +char *inet_ntoa(struct in_addr addr); +const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); +int inet_pton(int af, const char *src, void *dst); +static int32_t lookupEnd(NetLookupT *lk, int32_t result, uint32_t *addr); static int32_t lookupName(const char *name, uint32_t *addr); +void netLookupCancel(NetLookupT *lk); +int32_t netLookupPoll(NetLookupT *lk, uint32_t *addr); +void netLookupStart(NetLookupT *lk, const char *name); static bool parsePort(const char *service, bool numericOnly, uint16_t *port); @@ -473,46 +490,122 @@ int inet_pton(int af, const char *src, void *dst) { } -// Name lookup: 0 with *addr (network order), or an EAI_* code. +// The lookup is over, with result (0: found, *addr given). +static int32_t lookupEnd(NetLookupT *lk, int32_t result, uint32_t *addr) { + lk->slot = 0; + lk->result = result; + + if (result == 0) { + *addr = lk->addr; + } + + return result; +} + + +// Name lookup, waiting (other tasks run meanwhile): 0 with *addr +// (network order), or an EAI_* code. A dotted address is no lookup and +// no wait. static int32_t lookupName(const char *name, uint32_t *addr) { - int32_t slot; - int32_t rc; - uint32_t start; + NetLookupT lk; + int32_t rc; if (netParseAddress(name, addr)) { return 0; } - netStart(); - - if (!netStackRunning()) { - return EAI_FAIL; - } - - // The DNS servers usually come from DHCP. - netStackAwaitAddress(); - slot = protoResolveStart(name, netSockAppId(), addr); - - if (slot == 0) { - return 0; - } - - if (slot < 0) { - return (slot == -ENOMEM) ? EAI_MEMORY : EAI_NONAME; - } - - start = netStackNow(); - - while ((rc = protoResolvePoll(slot, addr)) == -EINPROGRESS) { - if (netStackNow() - start >= RESOLVE_TIMEOUT_MS) { - protoResolveAbandon(slot); - return EAI_AGAIN; - } + netLookupStart(&lk, name); + while ((rc = netLookupPoll(&lk, addr)) == NET_LOOKUP_BUSY) { netStackWaitStep(); } - return (rc == 0) ? 0 : EAI_NONAME; + return rc; +} + + +void netLookupCancel(NetLookupT *lk) { + if (lk->result != NET_LOOKUP_BUSY) { + return; + } + + if (lk->slot > 0) { + protoResolveAbandon(lk->slot); + } + + lk->slot = 0; + lk->result = EAI_AGAIN; +} + + +// First DHCP's address (the DNS servers usually come with it), then the +// query, each waited for a look at a time. +int32_t netLookupPoll(NetLookupT *lk, uint32_t *addr) { + int32_t rc; + + if (lk->result != NET_LOOKUP_BUSY) { + return lookupEnd(lk, lk->result, addr); + } + + netStackPoll(); + + if (lk->awaiting) { + if (netStackAwaitingAddress() && netStackNow() - lk->since < NET_DHCP_WAIT_MS) { + return NET_LOOKUP_BUSY; + } + + lk->awaiting = false; + + if (netParseAddress(lk->name, &lk->addr)) { + return lookupEnd(lk, 0, addr); + } + + lk->slot = protoResolveStart(lk->name, lk->owner, &lk->addr); + lk->since = netStackNow(); + + if (lk->slot == 0) { + return lookupEnd(lk, 0, addr); + } + + if (lk->slot < 0) { + return lookupEnd(lk, (lk->slot == -ENOMEM) ? EAI_MEMORY : EAI_NONAME, addr); + } + } + + rc = protoResolvePoll(lk->slot, &lk->addr); + + if (rc != -EINPROGRESS) { + return lookupEnd(lk, (rc == 0) ? 0 : EAI_NONAME, addr); + } + + if (netStackNow() - lk->since >= RESOLVE_TIMEOUT_MS) { + protoResolveAbandon(lk->slot); + return lookupEnd(lk, EAI_AGAIN, addr); + } + + return NET_LOOKUP_BUSY; +} + + +void netLookupStart(NetLookupT *lk, const char *name) { + memset(lk, 0, sizeof(*lk)); + lk->owner = netSockAppId(); + lk->result = NET_LOOKUP_BUSY; + + if (!name || !name[0] || snprintf(lk->name, sizeof(lk->name), "%s", name) >= (int)sizeof(lk->name)) { + lk->result = EAI_NONAME; + return; + } + + netStart(); + + if (!netStackRunning()) { + lk->result = EAI_FAIL; + return; + } + + lk->awaiting = true; + lk->since = netStackNow(); } diff --git a/src/libs/kpunch/dvxnet/netSock.c b/src/libs/kpunch/dvxnet/netSock.c index 85b8f454..1a261b0f 100644 --- a/src/libs/kpunch/dvxnet/netSock.c +++ b/src/libs/kpunch/dvxnet/netSock.c @@ -163,7 +163,12 @@ int connect(int fd, const struct sockaddr *addr, socklen_t addrLen) { return fail(err); } - netStackAwaitAddress(); + // A non-blocking one does not wait for DHCP's first address either + // (netLookupPoll does) + if (!s->nonBlocking) { + netStackAwaitAddress(); + } + rc = protoConnect(s->proto, &ep); if (rc != -EINPROGRESS) { @@ -816,7 +821,11 @@ static ssize_t sendCommon(int fd, const void *buf, size_t len, int flags, const if (s->type == SOCK_DGRAM) { int32_t n; - netStackAwaitAddress(); + // Only a send that may wait waits for DHCP's first address + if (!s->nonBlocking && !(flags & MSG_DONTWAIT)) { + netStackAwaitAddress(); + } + n = protoSend(s->proto, (const uint8_t *)buf, (int32_t)len, to ? &ep : NULL); return (n < 0) ? fail(-n) : (ssize_t)n; diff --git a/src/libs/kpunch/dvxnet/netStack.c b/src/libs/kpunch/dvxnet/netStack.c index 9a5aa210..08631394 100644 --- a/src/libs/kpunch/dvxnet/netStack.c +++ b/src/libs/kpunch/dvxnet/netStack.c @@ -35,6 +35,7 @@ #include "pktDrv.h" #include "dvxPlat.h" #include "taskSwch.h" +#include "../crypto/security.h" #include "lwip/dhcp.h" #include "lwip/dns.h" @@ -56,10 +57,6 @@ #define DIAG_MAX 160 #define IF_NAME0 'e' #define IF_NAME1 'n' -#define NET_DHCP_WAIT_MS 20000 -#define RAND_SHIFT_A 13 // xorshift32 (Marsaglia) shifts -#define RAND_SHIFT_B 17 -#define RAND_SHIFT_C 5 static bool sLwipReady = false; static bool sRunning = false; @@ -74,7 +71,6 @@ static uint32_t sTxPackets = 0; static uint32_t sRxBytes = 0; static uint32_t sTxBytes = 0; static uint32_t sRxNoBuf = 0; -static uint32_t sRandState = 0; static uint32_t sClockLast = 0; static uint32_t sClockMs = 0; static uint64_t sClockRem = 0; @@ -156,12 +152,17 @@ void netStackAssert(const char *msg, const char *file, int32_t line) { void netStackAwaitAddress(void) { uint32_t start = netStackNow(); - while (sRunning && sHaveDriver && sCfg.dhcp && ip4_addr_isany_val(*netif_ip4_addr(&sNetif)) && netStackNow() - start < NET_DHCP_WAIT_MS) { + while (netStackAwaitingAddress() && netStackNow() - start < NET_DHCP_WAIT_MS) { netStackWaitStep(); } } +bool netStackAwaitingAddress(void) { + return sRunning && sHaveDriver && sCfg.dhcp && ip4_addr_isany_val(*netif_ip4_addr(&sNetif)); +} + + void netStackDiag(const char *fmt, ...) { char text[DIAG_MAX]; va_list ap; @@ -229,20 +230,14 @@ void netStackPoll(void) { } -// xorshift32 stirred with the platform's entropy stamp: DNS query ids, -// DHCP transaction ids and local ports need to be hard to guess, not -// cryptographic. +// DNS query ids, DHCP transaction ids, local ports, and TCP initial +// sequence numbers, from the security library's generator: values an +// observer could predict from earlier ones would let it forge DNS answers +// or inject into a connection. uint32_t netStackRand(void) { - uint32_t x = sRandState ^ (uint32_t)platformEntropyStamp(); + uint32_t x; - if (x == 0) { - x = 1; - } - - x ^= x << RAND_SHIFT_A; - x ^= x >> RAND_SHIFT_B; - x ^= x << RAND_SHIFT_C; - sRandState = x; + secRngBytes((uint8_t *)&x, (int32_t)sizeof(x)); return x; } diff --git a/src/libs/kpunch/dvxnet/port/lwipopts.h b/src/libs/kpunch/dvxnet/port/lwipopts.h index 4041233e..2f9a8964 100644 --- a/src/libs/kpunch/dvxnet/port/lwipopts.h +++ b/src/libs/kpunch/dvxnet/port/lwipopts.h @@ -53,16 +53,34 @@ #define MEMP_MEM_MALLOC 0 // Pointer size: 4 on DOS, 8 for the 64-bit host test build. #define MEM_ALIGNMENT __SIZEOF_POINTER__ -#define MEM_SIZE (48 * 1024) -#define MEMP_NUM_PBUF 32 + +// Sized against measured peaks (2026-10-20): the host suite (a 256 KB +// TCP transfer with both ends on loopback, a 64 KB one and a 40-datagram +// burst over the fake wire) and the DOS network scripts (a DHCP lease, a +// lookup, an echo, a telnet session, and the package server's downloads +// and upload). The heap holds sent data until it is acknowledged (host +// peak 12,688 bytes, DOS 872); the receive pool holds each frame until +// the program reads it (host 15, DOS 1). A full heap makes a send wait, +// and a full pool drops a frame (TCP sends it again). +// Four connections' full send buffers +#define MEM_SIZE (24 * 1024) +// Our sends always copy, so only lwIP's own references (peak 0) +#define MEMP_NUM_PBUF 8 +// DHCP, DNS (a port per lookup in flight, up to DNS_TABLE_SIZE), and +// programs' sockets (peak 2) #define MEMP_NUM_UDP_PCB 12 +// Closed connections wait in TIME_WAIT, reclaimed when the pool runs out +// (peak 13, the package server's requests) #define MEMP_NUM_TCP_PCB 16 #define MEMP_NUM_TCP_PCB_LISTEN 8 -#define MEMP_NUM_TCP_SEG 48 +// Two connections' full send queues (peak 4) +#define MEMP_NUM_TCP_SEG (2 * TCP_SND_QUEUELEN) +// An 8 KB datagram (PROTO_UDP_MAX) is six fragments #define MEMP_NUM_REASSDATA 4 #define MEMP_NUM_FRAG_PBUF 8 -#define MEMP_NUM_ARP_QUEUE 16 -#define PBUF_POOL_SIZE 32 +// A full datagram queue (PROTO_DGRAM_QUEUE, netProto.c) and a full TCP +// window +#define PBUF_POOL_SIZE 20 // ---- Protocols ---- #define LWIP_IPV4 1 @@ -115,8 +133,25 @@ #define CHECKSUM_CHECK_UDP 1 #define CHECKSUM_CHECK_TCP 1 -// ---- Statistics and debugging off (DVX keeps its own traffic counters) ---- +// ---- Statistics off (DVX keeps its own traffic counters). The host test +// suite builds with NET_POOL_STATS to count each pool's peak use and +// failed allocations against the sizes above. Debugging stays off by +// leaving LWIP_DEBUG undefined: lwIP tests it with #ifdef, so defining it +// as 0 would turn it on. ---- +#ifdef NET_POOL_STATS +#define LWIP_STATS 1 +#define MEM_STATS 1 +#define MEMP_STATS 1 +#define LINK_STATS 0 +#define ETHARP_STATS 0 +#define IP_STATS 0 +#define IPFRAG_STATS 0 +#define ICMP_STATS 0 +#define UDP_STATS 0 +#define TCP_STATS 0 +#define SYS_STATS 0 +#else #define LWIP_STATS 0 -#define LWIP_DEBUG 0 +#endif #endif diff --git a/src/libs/kpunch/dvxshell/Makefile b/src/libs/kpunch/dvxshell/Makefile index a1364623..af082367 100644 --- a/src/libs/kpunch/dvxshell/Makefile +++ b/src/libs/kpunch/dvxshell/Makefile @@ -37,7 +37,7 @@ WPAPERDIR = $(SHAREDDIR)/wpaper BANKDIR = $(SHAREDDIR)/banks KEYSDIR = $(SYSTEMDIR)/KEYS -SRCS = shellMain.c shellApp.c shellBas.c shellSysBas.c shellInfo.c shellAssoc.c shellProg.c shellRun.c shellSaver.c pmModel.c pmModelBas.c +SRCS = shellMain.c shellApp.c shellBas.c shellSysBas.c shellInfo.c shellAssoc.c shellProg.c shellProgCache.c shellRun.c shellSaver.c pmModel.c pmModelBas.c OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) TARGETDIR = $(LIBSDIR)/kpunch/dvxshell TARGET = $(TARGETDIR)/dvxshell.lib diff --git a/src/libs/kpunch/dvxshell/README.md b/src/libs/kpunch/dvxshell/README.md index fb69f99b..97ab9ea2 100644 --- a/src/libs/kpunch/dvxshell/README.md +++ b/src/libs/kpunch/dvxshell/README.md @@ -20,16 +20,27 @@ program discovery, running programs and documents, and file associations. - `shellApp.h` -- application contract (AppDescriptorT, DxeAppContextT) - `shellInf.h` -- shell-internal interfaces exposed to apps +- `pmModel.h` -- program groups and items (PROGMAN.INI), one model for + every desktop +- `shellSaver.h` -- the screen saver - `shellAssoc.h` -- file associations (SYSTEM/ASSOC.INI, with the user's changes in USER/ASSOC.INI laid over it) - `shellProg.h` -- installed programs: discovery, names, icons +- `shellProgCache.h` -- the program cache (PROGRAMS.CAC in the user + folder): names, descriptions, and pictures kept between runs - `shellRun.h` -- running programs, documents, and command lines; the Run dialog - `shellApp.c` -- app registry, load, unload, lifecycle - `shellAssoc.c` -- file association store - `shellInfo.c` -- system info service - `shellMain.c` -- shell entry point, main loop, crash handler - `shellProg.c` -- installed program discovery and icons +- `shellProgCache.c` -- the program cache, checked by file stamps and + saved a chunk at a time in the idle time - `shellRun.c` -- command lines, shellRunProgram, Run dialog +- `shellSaver.c` -- the screen saver: wait, start, and stop +- `pmModel.c` -- program groups model and PROGMAN.INI +- `shellBas.c`, `shellSysBas.c`, `pmModelBas.c` -- BASIC entry points + (shell.bas, system.bas, and the groups model) ## Build diff --git a/src/libs/kpunch/dvxshell/dvxshell.dhs b/src/libs/kpunch/dvxshell/dvxshell.dhs index 099754c6..85ccf917 100644 --- a/src/libs/kpunch/dvxshell/dvxshell.dhs +++ b/src/libs/kpunch/dvxshell/dvxshell.dhs @@ -228,6 +228,8 @@ Per-app slot in the shell's app table. Slot 0 is reserved for the shell itself; taskCodeDepth int32_t * Per task id: SHELL_APP_CALL calls into this app still open on that task's stack. killPending bool A force kill was deferred; shellReapApps finishes it. faulted bool It crashed: its force kill runs no kill hooks (shellRegisterAppKill). + workTaskId uint32_t The task shellAppTaskStart started for it, while that runs; else 0. + workFn, workArg void (*)(void *), void * What that task runs. .endtable .topic shell.lifecycle @@ -306,7 +308,7 @@ Gracefully shut down a single app. Calls the app's shutdownFn (if present), dest void shellForceKillApp(AppContextT *ctx, ShellAppT *app); .endcode -Forcibly kill an app. Used by the Task Manager "End Task" or as part of crash recovery (which marks the app faulted first). The app's shutdownFn is NOT called: a force kill targets a hung or faulting app whose own code cannot be trusted to run. Instead the libraries' kill hooks (shellRegisterAppKill) end what they run for the app, unless it faulted, then the shell drops every idle and desktop-update callback the app registered, destroys all of the app's windows, kills its task (for main-loop apps), and finally closes the DXE handle. +Forcibly kill an app. Used by the Task Manager "End Task" or as part of crash recovery (which marks the app faulted first). The app's shutdownFn is NOT called: a force kill targets a hung or faulting app whose own code cannot be trusted to run. Instead the libraries' kill hooks (shellRegisterAppKill) end what they run for the app, unless it faulted, then the shell drops every idle and desktop-update callback the app registered, destroys all of the app's windows, kills its tasks (a main-loop app's, and a work task shellAppTaskStart started), and finally closes the DXE handle. Cleanup order: callbacks and windows first (removes them from the compositor), then the task (frees its stack; any dispatch frames still open on it are dropped from the context's dispatch depth), then the widget classes and system-wide hotkeys the app registered (its shutdownFn never got the chance to unregister them), then the DXE handle (unmaps the code). Closing the DXE before destroying windows would cause callbacks into unmapped code. @@ -507,9 +509,17 @@ void shellDataPath(const DxeAppContextT *ctx, const char *filename, char *outPat Build the full path of a file the program ships in the DATA folder beside its .app ("" when it does not fit), e.g. "APPS\ACME\MYGAME\DATA\LEVEL1.DAT". Read-only. +.h2 Program Cache + +Program names, descriptions, and icons come from the programs' own files (their name, description, and icon32 resources), and reading them takes several file opens per program, tens of milliseconds each on a slow disk, every time DVX starts. The shell keeps what it read in PROGRAMS.CAC in the user folder, with each file's size and time (platformFileStamp), and answers from it while a file is unchanged: shellProgramInfo, shellItemIcon, shellItemIcons, shellItemIconFile, and shellProgramIcon all go through it, so Program Manager, File Manager, and BASIC's Shell calls start without opening any program that has not changed. + +A file is checked against its stamp the first time it is asked about after what is installed last changed (platformInstalledChangeCount), and whenever shellProgramScan reads names (the folder is fresh in DOS's buffers then); a program that changed is read again, and a file that is gone loses its entry. A picture is kept as the image file's bytes (a DVX file's icon resource, or a picture file of up to 32 KB) and decoded for the display mode each time; bytes that turn out to be no image are noted as none. + +The file is read on first use. A change is saved three seconds after the last one, 4 KB at a time in the shell's idle time, so no frame waits for the whole write, and what is left at shutdown is saved then; the memory goes after a minute unused. It is DVX's memory (dvxMemSetApp), whichever program's call made it. Nothing is saved when DVX cannot save (platformIsReadOnly). Deleting PROGRAMS.CAC is harmless: it is made again. A file rewritten at the same size within two seconds keeps its stamp (DOS keeps a file's time to two seconds), so a program changed that quickly shows its old name or icon until it changes again. + .h2 Random Seed File -When the serial library is loaded, the shell feeds keyboard and mouse event timings to its random-number generator (dvxSetInputEntropyHook) and carries generator state across runs in SECLINK.RND in the user folder: the file is read and immediately rewritten at startup, and rewritten again at shutdown. The library is found at run time; without it no seed file is written. +When the cryptography library is loaded, the shell feeds keyboard and mouse event timings to its random-number generator (dvxSetInputEntropyHook) and carries generator state across runs in SECLINK.RND in the user folder: the file is read and immediately rewritten at startup, and rewritten again at shutdown. The library is found at run time; without it no seed file is written. .topic shell.desktop .title Desktop Callbacks @@ -522,6 +532,7 @@ When the serial library is loaded, the shell feeds keyboard and mouse event timi .index shellUnregisterAppCleanup .index shellRegisterAppKill .index shellUnregisterAppKill +.index shellAppTaskStart .index ShellAppCleanupFnT .index shellDesktopUpdate .index shellDesktopChangeCount @@ -530,7 +541,7 @@ When the serial library is loaded, the shell feeds keyboard and mouse event timi .h1 Desktop Callbacks -The shell wires several AppContextT event hooks (onCtrlEsc, onF1, onTitleChange, idleCallback) during startup and exposes them to other DXEs via function-pointer extern and registration API. Desktop managers register a callback to refresh their display when apps come and go. The Task Manager DXE registers its Ctrl+Esc handler via the shellCtrlEscFn extern pointer (see Task Manager documentation); shellOpenTaskManager is the one way to open it. +The shell wires several AppContextT event hooks (onCtrlEsc, onF1, onTitleChange, idleCallback, busyCallback, inputHere) during startup and exposes them to other DXEs via function-pointer extern and registration API. Desktop managers register a callback to refresh their display when apps come and go. The Task Manager DXE registers its Ctrl+Esc handler via the shellCtrlEscFn extern pointer (see Task Manager documentation); shellOpenTaskManager is the one way to open it. .h2 Shell Event Hooks @@ -538,6 +549,8 @@ The shell wires several AppContextT event hooks (onCtrlEsc, onF1, onTitleChange, AppContextT field Handler Purpose ----------------- ------- ------- idleCallback shellIdleDispatch Runs one frame of Run Minimized (shellMinimizeIdle) and every registered idle handler (see shellRegisterIdle), then yields to cooperative tasks, when dvxUpdate has no input events and no dirty rects. + busyCallback shellBusyDispatch After a frame that painted: yields to cooperative tasks, and runs shellIdleDispatch when the idle handlers have not run for 20 ms, so a program that keeps the screen changing never starves the others, audio, or the network. + inputHere shellInputHere Input, events, and widget timers are taken on the shell's own task (or on any task once the shell's has not taken them for 250 ms), so callbacks run on the shell's task, never on another program's stack. onCtrlEsc ctrlEscHandler shellOpenTaskManager. onF1 f1HelpHandler Opens the focused app's helpFile/helpTopic (or the system help) in the help viewer, reusing a running viewer that sets onNewArgs. onTitleChange titleChangeHandler Runs shellDesktopUpdate so Program Manager window lists refresh. @@ -565,7 +578,7 @@ Remove a previously registered callback. Call this before app shutdown to avoid void shellRegisterIdle(void (*fn)(void *ctx), void *ctx); .endcode -Register an idle handler invoked every idle frame by the shell main loop, regardless of which app is foreground. Background pollers (serial and secLink) register here so their connections keep pumping while another app is in front. Registration is idempotent on the (fn, ctx) pair. The registering module MUST unregister before it is unloaded -- app DXEs do this from their _appShutdown hook on a graceful exit. On a force kill (which does not run _appShutdown) the shell drops the app's registrations itself. +Register an idle handler invoked every idle frame by the shell main loop (and at least every 20 ms while frames keep painting), regardless of which app is foreground. Background pollers (serial and secLink) register here so their connections keep pumping while another app is in front. Registration is idempotent on the (fn, ctx) pair. The registering module MUST unregister before it is unloaded -- app DXEs do this from their _appShutdown hook on a graceful exit. On a force kill (which does not run _appShutdown) the shell drops the app's registrations itself. .h2 shellUnregisterIdle @@ -595,6 +608,14 @@ void shellUnregisterAppKill(ShellAppCleanupFnT fn); Register a function the shell calls with an app's id when the app is ended by force (End Task, a screen saver that would not close, DVX exiting) while none of the app's code is running and its windows are still there -- before the windows go and before the cleanup hooks. A library ends what it runs for the app the way the app's own end would: the BASIC runtime (basrt) frees a standalone program's VM, forms, screen, and module this way, closing its serial ports and its forms' windows, so End Task on a BASIC program leaks nothing. Not called for an app that faulted (its state cannot be trusted), nor for one whose kill had to wait until its code returned (End Task from inside the app's own dialog: its windows went at once). Registration rules as for cleanup hooks. +.h2 shellAppTaskStart + +.code +int32_t shellAppTaskStart(AppContextT *ctx, const char *name, uint32_t stackSize, void (*fn)(void *arg), void *arg); +.endcode + +Run fn(arg) on a cooperative task of its own (stackSize bytes; 0 for the default) that belongs to the running program (ctx->currentAppId), for a long piece of a callback-only program's work: the DVX BASIC IDE runs each program it starts this way. A callback-only program's code runs inside the shell's main loop, which only reaps programs that ended, carries out close and Exit requests and DOS program starts, and starts the screen saver between frames; work done in a callback holds all of that until it returns. On its own task the work leaves the main loop free. The program is the running one on the task (windows it makes are the program's), it is not reaped while the task runs (even with all its windows closed), and ending it by force (End Task) ends the task. One at a time per program. Returns the task's ID, or -1 when one runs already, no program is running, or no task can be made. + .h2 shellDesktopUpdate .code diff --git a/src/libs/kpunch/dvxshell/pmModel.h b/src/libs/kpunch/dvxshell/pmModel.h index 1d09142a..d3ba604e 100644 --- a/src/libs/kpunch/dvxshell/pmModel.h +++ b/src/libs/kpunch/dvxshell/pmModel.h @@ -54,7 +54,7 @@ #define PM_NAME_MAX SHELL_APP_NAME_MAX #define PM_ARGS_MAX 256 -#define PM_HOTKEY_TEXT 24 // "Ctrl+Alt+Shift+F12" plus NUL, rounded up +#define PM_HOTKEY_TEXT DVX_ACCEL_TEXT_MAX #define PM_GROUP_MAX 64 // user groups kept from PROGMAN.INI #define PM_ITEM_MAX 256 // items kept per group #define PM_HISTORY_MAX 10 // File > Run... commands remembered diff --git a/src/libs/kpunch/dvxshell/shellApp.c b/src/libs/kpunch/dvxshell/shellApp.c index f3150b0a..76e12c34 100644 --- a/src/libs/kpunch/dvxshell/shellApp.c +++ b/src/libs/kpunch/dvxshell/shellApp.c @@ -32,7 +32,6 @@ #include "stb_ds_wrap.h" #include -#include #include #include #include @@ -41,9 +40,6 @@ #include #include "dvxMem.h" -// File copy chunk size for multi-instance temp copies -#define COPY_BUF_SIZE 32768 - // dispatchDepth while a callback dispatched by a top-level dvxUpdate runs // (e.g. the Task Manager's End Task button). Anything deeper means some // other callback -- possibly the kill target's own modal loop -- is still @@ -84,12 +80,13 @@ static int32_t allocSlot(void); static int32_t appCodeDepth(const ShellAppT *app); static bool appCodeMayBeLive(const AppContextT *ctx, const ShellAppT *app); static bool appCounted(const ShellAppT *app); +static uint32_t appOwnTask(const ShellAppT *app); static int32_t *appTaskCodeSlot(ShellAppT *app, uint32_t taskId); static int32_t appTaskDispatchShare(const ShellAppT *app); static void appTaskWrapper(void *arg); static WindowT *appTopWindow(const AppContextT *ctx, int32_t appId); +static void appWorkTaskWrapper(void *arg); static void cleanupTempFile(ShellAppT *app); -static int32_t copyFile(const char *src, const char *dst); static void defaultHelpFile(DxeAppContextT *dxeCtx); static void deferForceKill(AppContextT *ctx, ShellAppT *app); static void deliverNewArgs(AppContextT *ctx, ShellAppT *app, const char *args); @@ -108,8 +105,11 @@ void shellAppCallEnter(ShellAppT *app); void shellAppCallLeave(ShellAppT *app); void shellAppCodeForgetTask(uint32_t taskId); ShellAppT *shellAppFindByPath(const char *path); +int32_t shellAppGetPriority(int32_t appId); void shellAppInit(void); +bool shellAppSetPriority(int32_t appId, int32_t priority); int32_t shellAppSlotCount(void); +int32_t shellAppTaskStart(AppContextT *ctx, const char *name, uint32_t stackSize, void (*fn)(void *arg), void *arg); #ifdef DVX_HOST_TEST void shellAppTestReset(void); #endif @@ -191,7 +191,7 @@ static bool appCodeMayBeLive(const AppContextT *ctx, const ShellAppT *app) { return true; } - if (app->hasMainLoop && app->mainTaskId > 0 && tsCurrentId() == app->mainTaskId) { + if ((app->hasMainLoop && app->mainTaskId > 0 && tsCurrentId() == app->mainTaskId) || (app->workTaskId > 0 && tsCurrentId() == app->workTaskId)) { return true; } @@ -206,6 +206,18 @@ static bool appCounted(const ShellAppT *app) { } +// The task that is the program's own, for its priority: its main loop's, +// else its shellAppTaskStart task while one runs; 0 for none (a callback +// program runs on the shell's task). +static uint32_t appOwnTask(const ShellAppT *app) { + if (app->hasMainLoop && app->mainTaskId != TS_MAIN_TASK_ID) { + return app->mainTaskId; + } + + return app->workTaskId; +} + + // Grows the app's per-task call table on demand (zero-filled). static int32_t *appTaskCodeSlot(ShellAppT *app, uint32_t taskId) { while (arrlen(app->taskCodeDepth) <= (ptrdiff_t)taskId) { @@ -216,14 +228,20 @@ static int32_t *appTaskCodeSlot(ShellAppT *app, uint32_t taskId) { } -// Dispatch frames open on the app's own task, which tsKill discards along -// with the task (0 for a callback-only app). +// Dispatch frames open on the app's own tasks (its main loop's, its work +// task's), which tsKill discards along with them. static int32_t appTaskDispatchShare(const ShellAppT *app) { - if (!app->hasMainLoop || app->mainTaskId == 0) { - return 0; + int32_t share = 0; + + if (app->hasMainLoop && app->mainTaskId > 0) { + share += shellTaskDepthShare(app->mainTaskId); } - return shellTaskDepthShare(app->mainTaskId); + if (app->workTaskId > 0) { + share += shellTaskDepthShare(app->workTaskId); + } + + return share; } @@ -273,6 +291,19 @@ static WindowT *appTopWindow(const AppContextT *ctx, int32_t appId) { } +// Task entry of shellAppTaskStart's work: the program is the running one +// for the task's life (as appTaskWrapper does), and workTaskId clears as +// the task ends (its slot may be handed out again). +static void appWorkTaskWrapper(void *arg) { + ShellAppT *app = sApps[(int32_t)(intptr_t)arg]; + + app->dxeCtx->shellCtx->currentAppId = app->appId; + app->workFn(app->workArg); + app->dxeCtx->shellCtx->currentAppId = 0; + app->workTaskId = 0; +} + + // Delete the temp file for a multi-instance app copy, if one exists. static void cleanupTempFile(ShellAppT *app) { if (app->tempPath[0] != '\0') { @@ -282,46 +313,6 @@ static void cleanupTempFile(ShellAppT *app) { } -// Binary file copy. Returns 0 on success, -1 on failure. -static int32_t copyFile(const char *src, const char *dst) { - FILE *in = fopen(src, "rb"); - - if (!in) { - dvxLog("copyFile: failed to open source '%s' (errno=%d)", src, errno); - return -1; - } - - FILE *out = fopen(dst, "wb"); - - if (!out) { - dvxLog("copyFile: failed to create dest '%s' (errno=%d)", dst, errno); - fclose(in); - return -1; - } - - // Static rather than on-stack: shellLoadApp runs on app task stacks as - // small as TS_DEFAULT_STACK_SIZE (32KB), which a 32KB stack frame would - // overflow. The cooperative switcher is single-threaded, so a shared - // copy buffer is safe. - static char buf[COPY_BUF_SIZE]; - size_t n; - - while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { - if (fwrite(buf, 1, n, out) != n) { - dvxLog("copyFile: write failed '%s' -> '%s' (errno=%d)", src, dst, errno); - fclose(in); - fclose(out); - remove(dst); - return -1; - } - } - - fclose(in); - fclose(out); - return 0; -} - - // Force kill while the app's code may still be on a stack (see // appCodeMayBeLive). Its windows are queued for destruction -- hidden at // once, so modal loops over them see them go, but their memory stays valid @@ -488,9 +479,18 @@ static void forceKillNow(AppContextT *ctx, ShellAppT *app) { } -// Kill an app's cooperative task if it has one and it is still alive. -// Shared by shellForceKillApp and shellReapApp. +// Kill an app's cooperative tasks (its main loop's, its work task) that +// are still alive. Shared by shellForceKillApp and shellReapApp. static void killAppTask(ShellAppT *app) { + if (app->workTaskId > 0) { + if (tsGetState(app->workTaskId) != TaskStateTerminated) { + shellTaskDepthForget(app->workTaskId); + tsKill(app->workTaskId); + } + + app->workTaskId = 0; + } + if (app->hasMainLoop && app->mainTaskId > 0) { if (tsGetState(app->mainTaskId) != TaskStateTerminated) { shellTaskDepthForget(app->mainTaskId); @@ -646,6 +646,15 @@ ShellAppT *shellAppFindByPath(const char *path) { } +int32_t shellAppGetPriority(int32_t appId) { + const ShellAppT *app = shellGetApp(appId); + uint32_t task = (app && appCounted(app)) ? appOwnTask(app) : 0; + int32_t pri = task ? tsGetPriority(task) : TS_ERR_PARAM; + + return pri >= TS_OK ? pri : SHELL_NO_PRIORITY; +} + + void shellAppInit(void) { // Seed slot 0 (reserved for the shell). Slots are heap-allocated so // their addresses stay stable as the sApps index array grows. @@ -654,11 +663,37 @@ void shellAppInit(void) { } +bool shellAppSetPriority(int32_t appId, int32_t priority) { + const ShellAppT *app = shellGetApp(appId); + uint32_t task = (app && appCounted(app)) ? appOwnTask(app) : 0; + + return task && tsSetPriority(task, priority) == TS_OK; +} + + int32_t shellAppSlotCount(void) { return arrlen(sApps); } +int32_t shellAppTaskStart(AppContextT *ctx, const char *name, uint32_t stackSize, void (*fn)(void *arg), void *arg) { + int32_t appId = ctx->currentAppId; + ShellAppT *app = (appId > 0 && appId < arrlen(sApps)) ? sApps[appId] : NULL; + + if (!app || app->state != AppStateRunningE || app->workTaskId != 0 || !fn) { + return -1; + } + + app->workFn = fn; + app->workArg = arg; + + int32_t taskId = tsCreate(name, appWorkTaskWrapper, (void *)(intptr_t)appId, stackSize, TS_PRIORITY_NORMAL); + + app->workTaskId = (taskId > 0) ? (uint32_t)taskId : 0; + return taskId > 0 ? taskId : -1; +} + + #ifdef DVX_HOST_TEST // Test seam: release every slot (the caller has already terminated the // apps) so the next shellAppInit starts from an empty table. @@ -799,8 +834,10 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch return refuseSecondCopy(ctx, path); } - if (copyFile(path, tempPath) != 0) { + if (!platformCopyFile(path, tempPath)) { char msg[SHELL_MSG_MAX]; + + dvxLog("Shell: cannot copy %s to %s for another copy of it", path, tempPath); snprintf(msg, sizeof(msg), "Failed to create instance copy of %s.", platformPathBaseName(path)); dvxErrorBox(ctx, NULL, msg); return -1; @@ -812,8 +849,8 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch // Show hourglass during the load (dlopen + symbol resolution + appMain) dvxSetBusy(ctx, true); - // Load the DXE - void *handle = dlopen(loadPath, RTLD_NOW | RTLD_GLOBAL); + // Load the DXE (and any library it needs that is not loaded yet) + void *handle = platformModuleOpen(loadPath, RTLD_NOW | RTLD_GLOBAL); if (!handle) { char msg[SHELL_MSG_MAX]; @@ -1103,8 +1140,9 @@ bool shellReapApps(AppContextT *ctx) { // Callback-only apps terminate when their last window closes. // They have no main loop to set AppStateTerminatingE, so we - // detect termination by checking for zero remaining windows. - if (sApps[i]->state == AppStateRunningE && !sApps[i]->hasMainLoop) { + // detect termination by checking for zero remaining windows -- + // once their work task (shellAppTaskStart) has ended. + if (sApps[i]->state == AppStateRunningE && !sApps[i]->hasMainLoop && sApps[i]->workTaskId == 0) { bool hasWindow = false; for (int32_t w = 0; w < ctx->stack.count; w++) { diff --git a/src/libs/kpunch/dvxshell/shellApp.h b/src/libs/kpunch/dvxshell/shellApp.h index aa4a3ace..c5e63bf9 100644 --- a/src/libs/kpunch/dvxshell/shellApp.h +++ b/src/libs/kpunch/dvxshell/shellApp.h @@ -75,6 +75,9 @@ // Use in AppDescriptorT.stackSize to get the default task stack size #define SHELL_STACK_DEFAULT 0 +// shellAppGetPriority of a program with no task of its own +#define SHELL_NO_PRIORITY (-1) + typedef struct { uint32_t abiVersion; // DVX_ABI_VERSION at build time; must stay first char name[SHELL_APP_NAME_MAX]; @@ -156,6 +159,9 @@ typedef struct { int32_t *taskCodeDepth; // stb_ds by task id: shell calls into this app in progress on that task's stack (see SHELL_APP_CALL) bool killPending; // force kill requested while its code was live; released by shellReapApps bool faulted; // it crashed: its kill runs no kill hooks (shellRegisterAppKill) + uint32_t workTaskId; // shellAppTaskStart's task while it runs, else 0 + void (*workFn)(void *arg); + void *workArg; } ShellAppT; // Bracket every direct shell call into an app's code (appMain of a @@ -296,6 +302,17 @@ bool shellActivateApp(AppContextT *ctx, int32_t appId); // Get an active app slot loaded from this path (platformPathSame), or NULL. ShellAppT *shellAppFindByPath(const char *path); +// A running program's priority (TS_PRIORITY_LOW .. TS_PRIORITY_HIGH): that +// of its own task (its main loop's, or its shellAppTaskStart task's while +// that runs). SHELL_NO_PRIORITY when it has none -- a callback program +// runs on the shell's task -- or appId is not running (or starting). +int32_t shellAppGetPriority(int32_t appId); + +// Set a running program's priority (its own task's, as above): how many +// turns it gets beside the others. False when it has no task of its own, +// is not running, or priority is out of range. +bool shellAppSetPriority(int32_t appId, int32_t priority); + // Total number of app slots (for iteration bounds) int32_t shellAppSlotCount(void); @@ -369,11 +386,12 @@ void shellDesktopUpdate(void); // differs from the value it saw last. int32_t shellDesktopChangeCount(void); -// Register an idle handler called every idle frame by the shell main loop, -// regardless of which app is foreground. Serial/secLink pollers register -// here so background connections keep pumping. Idempotent on the (fn, ctx) -// pair. The registering module MUST unregister before it is unloaded (for -// app DXEs, from the _appShutdown hook) or the handler will dangle. +// Register an idle handler called every idle frame by the shell main loop +// (and at least every 20 ms while frames keep painting), regardless of which +// app is foreground. Serial/secLink pollers register here so background +// connections keep pumping. Idempotent on the (fn, ctx) pair. The +// registering module MUST unregister before it is unloaded (for app DXEs, +// from the _appShutdown hook) or the handler will dangle. void shellRegisterIdle(void (*fn)(void *ctx), void *ctx); // Remove a previously registered idle handler. No-op if absent, so a @@ -416,6 +434,16 @@ void shellRegisterAppKill(ShellAppCleanupFnT fn); // Remove a kill hook (no-op if absent). void shellUnregisterAppKill(ShellAppCleanupFnT fn); +// Run fn(arg) on a task of its own (stackSize bytes; 0: the default) that +// belongs to the running program (ctx->currentAppId), for a long piece of +// a callback-only program's work (the IDE's program runs): the shell's +// main loop goes on meanwhile -- programs that ended are reaped, close and +// exit requests carried out, the screen saver started. The program is +// the running one on the task, is not reaped while the task runs, and +// ending it by force ends the task. One at a time per program. Returns +// the task's ID, or -1 (one runs already, no program, or no task). +int32_t shellAppTaskStart(AppContextT *ctx, const char *name, uint32_t stackSize, void (*fn)(void *arg), void *arg); + // ============================================================ // Ctrl+Esc handler (set by taskmgr library) // ============================================================ @@ -500,6 +528,11 @@ const char *ShellCommandProgram(const char *command); // Exit DVX (every program is asked to close first). void ShellExitDvx(void); +// What is installed may have changed (platformInstalledChanged), and its +// count (platformInstalledChangeCount). +void ShellInstalledChanged(void); +int32_t ShellInstalledCount(void); + // Copies of this program running, this one included. int32_t ShellInstanceCount(void); @@ -576,6 +609,10 @@ int32_t ShellRunningKill(int32_t appId); const char *ShellRunningName(int32_t appId); const char *ShellRunningPath(int32_t appId); +// Another program's priority (shellAppGetPriority / shellAppSetPriority). +int32_t ShellRunningPriority(int32_t appId); +int32_t ShellRunningSetPriority(int32_t appId, int32_t priority); + // shellRunProgram: the new program's id, or 0. int32_t ShellRunProgram(const char *program, const char *args, const char *folder); diff --git a/src/libs/kpunch/dvxshell/shellBas.c b/src/libs/kpunch/dvxshell/shellBas.c index c681d051..75eae418 100644 --- a/src/libs/kpunch/dvxshell/shellBas.c +++ b/src/libs/kpunch/dvxshell/shellBas.c @@ -48,10 +48,9 @@ #define BAS_HISTORY_SEP '\n' #define BAS_LINE_END '\r' #define HOTKEY_QUEUE_MAX 16 // presses kept per program that has not asked yet -#define ACCEL_TEXT_MAX 32 #define ARG_NONE (-1) // argWalk: only count -#define PRIORITY_NONE (-1) // ShellPriority without a task of its own #define BAS_RUN_DOS (-1) // shell.bas SHELL_RUN_DOS: a DOS program will run +#define RUN_DOS_NONE (-1) // ShellRunDosWait: the DOS program did not run // A program list from ShellProgramScan and the program it belongs to. typedef struct { @@ -81,6 +80,7 @@ typedef struct { static BasProgListT *sProgLists = NULL; // stb_ds; handle = index + 1 static BasHotkeyT *sHotkeys = NULL; // stb_ds static BasHotkeyPressT *sPresses = NULL; // stb_ds, oldest first +static BasTextT sText; // the last string result static int32_t sLastHotkeyId = 0; // ============================================================ @@ -99,7 +99,6 @@ int32_t HotkeyNext(void); static void hotkeyPressed(void *userCtx); void HotkeyRemove(int32_t id); const char *HotkeyText(const char *keyText); -static const ShellAppT *mainTaskApp(void); static const char *orEmpty(const char *text); static BasProgListT *progListOf(int32_t handle); static ShellAppT *runningApp(int32_t appId); @@ -123,6 +122,8 @@ int32_t ShellChangeCount(void); const char *ShellCommandArgs(const char *command); const char *ShellCommandProgram(const char *command); void ShellExitDvx(void); +void ShellInstalledChanged(void); +int32_t ShellInstalledCount(void); int32_t ShellInstanceCount(void); int32_t ShellIsDosProgram(const char *path); int32_t ShellIsProgram(const char *path); @@ -142,6 +143,7 @@ int32_t ShellRun(const char *program, const char *args); int32_t ShellRunCommand(const char *command); const char *ShellRunDialog(const char *history); void ShellRunDos(const char *program, const char *args, const char *folder, int32_t pause); +int32_t ShellRunDosWait(const char *program, const char *args, const char *folder, int32_t pause); int32_t ShellRunningActivate(int32_t appId); int32_t ShellRunningClose(int32_t appId); int32_t ShellRunningCount(void); @@ -150,6 +152,8 @@ int32_t ShellRunningId(int32_t index); int32_t ShellRunningKill(int32_t appId); const char *ShellRunningName(int32_t appId); const char *ShellRunningPath(int32_t appId); +int32_t ShellRunningPriority(int32_t appId); +int32_t ShellRunningSetPriority(int32_t appId, int32_t priority); int32_t ShellRunProgram(const char *program, const char *args, const char *folder); int32_t ShellSetDesktop(const char *program); int32_t ShellSetPriority(int32_t priority); @@ -333,9 +337,9 @@ void HotkeyRemove(int32_t id) { // keyText read and written back by the same code menus use. const char *HotkeyText(const char *keyText) { - static char text[ACCEL_TEXT_MAX]; - int32_t key = 0; - int32_t mods = 0; + char text[DVX_ACCEL_TEXT_MAX]; + int32_t key = 0; + int32_t mods = 0; text[0] = '\0'; @@ -343,16 +347,7 @@ const char *HotkeyText(const char *keyText) { dvxAccelFormat(key, mods, text, sizeof(text)); } - return text; -} - - -// This program when it runs as a task of its own (every BASIC program -// does), else NULL. -static const ShellAppT *mainTaskApp(void) { - const ShellAppT *app = currentApp(); - - return (app && app->hasMainLoop && app->mainTaskId != TS_MAIN_TASK_ID) ? app : NULL; + return basTextSet(&sText, text); } @@ -441,10 +436,10 @@ const char *ShellAppPath(void) { // Argument index of a command line (0 = the program), without its quotes; // "" past the last. const char *ShellArg(const char *command, int32_t index) { - static char arg[SHELL_ARGS_MAX]; + char arg[SHELL_ARGS_MAX]; argWalk(command, index, arg, (int32_t)sizeof(arg)); - return arg; + return basTextSet(&sText, arg); } @@ -460,27 +455,27 @@ int32_t ShellArgCount(const char *command) { // it holds blanks, so ShellArg$ and COMMAND$(n) read it back whole; "" // when it is too long for one. const char *ShellArgQuote(const char *arg) { - static char quoted[SHELL_ARGS_MAX]; + char quoted[SHELL_ARGS_MAX]; if (dvxArgAppend(quoted, (int32_t)sizeof(quoted), 0, arg ? arg : "") < 0) { quoted[0] = '\0'; } - return quoted; + return basTextSet(&sText, quoted); } // The program that opens document, without the association's argument // template ("" when none). const char *ShellAssocCommand(const char *document) { - static char program[DVX_MAX_PATH]; - char args[SHELL_ARGS_MAX]; + char program[DVX_MAX_PATH]; + char args[SHELL_ARGS_MAX]; if (!shellAssocCommand(document, program, sizeof(program), args, sizeof(args))) { program[0] = '\0'; } - return program; + return basTextSet(&sText, program); } @@ -490,37 +485,37 @@ int32_t ShellAssocCount(void) { const char *ShellAssocExt(int32_t index) { - static char extension[SHELL_ASSOC_EXT_MAX]; - char value[DVX_MAX_PATH + SHELL_ARGS_MAX]; + char extension[SHELL_ASSOC_EXT_MAX]; + char value[DVX_MAX_PATH + SHELL_ARGS_MAX]; if (!shellAssocGet(index, extension, sizeof(extension), value, sizeof(value))) { extension[0] = '\0'; } - return extension; + return basTextSet(&sText, extension); } const char *ShellAssocNormalize(const char *extension) { - static char normal[SHELL_ASSOC_EXT_MAX]; + char normal[SHELL_ASSOC_EXT_MAX]; if (!shellAssocNormalize(extension, normal)) { normal[0] = '\0'; } - return normal; + return basTextSet(&sText, normal); } // The program set to open files with this extension ("" when none). const char *ShellAssocProgram(const char *extension) { - static char program[DVX_MAX_PATH]; + char program[DVX_MAX_PATH]; if (!shellAssocLookup(extension, program, sizeof(program))) { program[0] = '\0'; } - return program; + return basTextSet(&sText, program); } @@ -535,14 +530,14 @@ int32_t ShellAssocSet(const char *extension, const char *value) { const char *ShellAssocValue(int32_t index) { - static char value[DVX_MAX_PATH + SHELL_ARGS_MAX]; - char extension[SHELL_ASSOC_EXT_MAX]; + char value[DVX_MAX_PATH + SHELL_ARGS_MAX]; + char extension[SHELL_ASSOC_EXT_MAX]; if (!shellAssocGet(index, extension, sizeof(extension), value, sizeof(value))) { value[0] = '\0'; } - return value; + return basTextSet(&sText, value); } @@ -553,21 +548,21 @@ int32_t ShellChangeCount(void) { // The arguments part of a command line, as ShellRunCommand passes them. const char *ShellCommandArgs(const char *command) { - static char args[SHELL_ARGS_MAX]; - char program[DVX_MAX_PATH]; + char args[SHELL_ARGS_MAX]; + char program[DVX_MAX_PATH]; shellSplitCommand(orEmpty(command), program, (int32_t)sizeof(program), args, (int32_t)sizeof(args)); - return args; + return basTextSet(&sText, args); } // The program part of a command line, as ShellRunCommand starts it. const char *ShellCommandProgram(const char *command) { - static char program[DVX_MAX_PATH]; - char args[SHELL_ARGS_MAX]; + char program[DVX_MAX_PATH]; + char args[SHELL_ARGS_MAX]; shellSplitCommand(orEmpty(command), program, (int32_t)sizeof(program), args, (int32_t)sizeof(args)); - return program; + return basTextSet(&sText, program); } @@ -576,6 +571,20 @@ void ShellExitDvx(void) { } +// What is installed may have changed (platformInstalledChanged): a +// program that put files in the programs folder says so, and Program +// Manager looks again when it comes to the front. +void ShellInstalledChanged(void) { + platformInstalledChanged(); +} + + +// platformInstalledChangeCount, wrapped into a Long. +int32_t ShellInstalledCount(void) { + return (int32_t)platformInstalledChangeCount(); +} + + // How many copies of this program are running, this one included. int32_t ShellInstanceCount(void) { const ShellAppT *self = currentApp(); @@ -608,10 +617,10 @@ int32_t ShellIsProgram(const char *path) { // else the program that opens the document; "" when none gives one. // LOADPICTURE of the file shows the icon. const char *ShellItemIcon(const char *iconPath, const char *path) { - static char file[DVX_MAX_PATH]; + char file[DVX_MAX_PATH]; shellItemIconFile(shellContext(), orEmpty(iconPath), orEmpty(path), file, (int32_t)sizeof(file)); - return platformPathNative(file); + return basTextSet(&sText, platformPathNative(file)); } @@ -635,10 +644,7 @@ int32_t ShellOpenDocument(const char *path) { // This program's priority (TS_PRIORITY_LOW .. TS_PRIORITY_HIGH), -1 when // it has no task of its own. int32_t ShellPriority(void) { - const ShellAppT *app = mainTaskApp(); - int32_t pri = app ? tsGetPriority(app->mainTaskId) : TS_ERR_PARAM; - - return pri >= TS_OK ? pri : PRIORITY_NONE; + return shellAppGetPriority(ShellAppId()); } @@ -742,7 +748,7 @@ int32_t ShellRunCommand(const char *command) { // history holds the remembered commands newest first, one per line. const char *ShellRunDialog(const char *history) { - static char command[SHELL_RUN_CMD_MAX]; + char command[SHELL_RUN_CMD_MAX]; char *copy = strdup(orEmpty(history)); const char **lines = NULL; @@ -774,7 +780,7 @@ const char *ShellRunDialog(const char *history) { arrfree(lines); free(copy); - return command; + return basTextSet(&sText, command); } @@ -785,6 +791,27 @@ void ShellRunDos(const char *program, const char *args, const char *folder, int3 } +// ShellRunDos waiting for the DOS program, as SHELL does (this program +// is not asked to close, and its events run meanwhile, as in DoEvents): +// the program's exit code, -1 when it did not run (the other programs +// were kept, it was not found or could not start) or this program is +// ending. +int32_t ShellRunDosWait(const char *program, const char *args, const char *folder, int32_t pause) { + int32_t appId = currentAppId(); + int32_t rc = RUN_DOS_NONE; + + shellRunDosFor(appId, program, args, folder, pause != BAS_FALSE); + + while (!shellRunDosDone(appId, &rc)) { + if (!dvxWaitPump()) { + return RUN_DOS_NONE; + } + } + + return rc; +} + + // Bring a program to the front (Task Manager's Switch To). True when it // has a window to show. int32_t ShellRunningActivate(int32_t appId) { @@ -859,6 +886,20 @@ const char *ShellRunningPath(int32_t appId) { } +// Another program's priority, as ShellPriority is this one's; -1 when it +// has no task of its own or is not running. +int32_t ShellRunningPriority(int32_t appId) { + return shellAppGetPriority(appId); +} + + +// Set another program's priority, as ShellSetPriority does this one's. +// True when it was set. +int32_t ShellRunningSetPriority(int32_t appId, int32_t priority) { + return shellAppSetPriority(appId, priority) ? BAS_TRUE : BAS_FALSE; +} + + // A program, or a document opened by its file type, with file names in // args found in folder ("" for none). The new program's id, or 0 (DVX // has said why). @@ -877,9 +918,7 @@ int32_t ShellSetDesktop(const char *program) { // Set this program's priority (TS_PRIORITY_LOW .. TS_PRIORITY_HIGH). // True when it was set. int32_t ShellSetPriority(int32_t priority) { - const ShellAppT *app = mainTaskApp(); - - return (app && tsSetPriority(app->mainTaskId, priority) == TS_OK) ? BAS_TRUE : BAS_FALSE; + return shellAppSetPriority(ShellAppId(), priority) ? BAS_TRUE : BAS_FALSE; } diff --git a/src/libs/kpunch/dvxshell/shellMain.c b/src/libs/kpunch/dvxshell/shellMain.c index a2d6d3da..9baf379c 100644 --- a/src/libs/kpunch/dvxshell/shellMain.c +++ b/src/libs/kpunch/dvxshell/shellMain.c @@ -66,6 +66,7 @@ #include "shellApp.h" #include "shellInf.h" +#include "shellProgCache.h" #include "shellRun.h" #include "shellSaver.h" #include "dvxDlg.h" @@ -75,7 +76,7 @@ #include "dvxPrefs.h" #include "dvxPlat.h" #include "stb_ds_wrap.h" -#include "../serial/security/security.h" // SEC_SUCCESS only; resolved at run time +#include "../crypto/security.h" // SEC_SUCCESS only; resolved at run time #include #include @@ -88,7 +89,7 @@ // ABI stamp checked by the loader (see DVX_ABI_VERSION). DVX_ABI_STAMP(dvxshell); -// Random-seed file carried across runs for the serial library's RNG +// Random-seed file carried across runs for the cryptography library's RNG #define SHELL_SEED_NAME "SECLINK.RND" // in the user folder // Exit DVX: how long an app may take to close a window it was asked to @@ -99,6 +100,16 @@ DVX_ABI_STAMP(dvxshell); #define SHELL_EXIT_TITLE "Exit DVX" #define SHELL_CLOSE_TITLE "Close Program" +// While frames keep painting (no idle frame comes), the idle handlers +// still run this often: audio streams, network polls, and serial +// pollers must not wait for the screen to settle. +#define SHELL_BUSY_IDLE_US 20000 + +// Input is the shell task's (shellInputHere) unless it has not polled for +// this long (busy in a long call): then any task's frame takes it, so the +// desktop never stops answering. +#define SHELL_INPUT_STALE_US 250000 + // The not-responding question: while exiting DVX, and for one program #define SHELL_STUCK_EXIT_ASK "'%s' is not responding to the request to close. End it and continue exiting DVX?" #define SHELL_STUCK_CLOSE_ASK "'%s' is not responding to the request to close. End it?" @@ -190,9 +201,10 @@ static int32_t *sTaskDepth = NULL; // stb_ds, indexed by task id static uint32_t sRunningTask = 0; // task whose context was last restored static int32_t sDepthBase = 0; // dispatchDepth when that task was switched in -// Serial-library RNG entry points, resolved at run time (the library is -// optional and nothing links against it). secRngAddEntropy has exactly -// the DvxEntropyHookT signature and is registered directly. +// Serial-library RNG entry points, resolved at run time (the shell does +// not link against the library, and the host tests have none). +// secRngAddEntropy has exactly the DvxEntropyHookT signature and is +// registered directly. typedef int (*RngSeedFileFnT)(const char *path); static RngSeedFileFnT sRngSaveSeed = NULL; @@ -201,6 +213,11 @@ static char sSeedPath[DVX_MAX_PATH]; // SHELL_SEED_NAME in the user // Set by shellRequestExit, acted on at the top of the main loop. static bool sExitRequested = false; +// platformMicros when the idle handlers last ran (shellBusyDispatch), and +// when the shell task last took input (shellInputHere). +static uint64_t sIdleWalkAt = 0; +static uint64_t sInputPolledAt = 0; + // Programs asked to close (shellRequestAppClose), oldest first: stb_ds of // app ids, carried out at the top of the main loop. static int32_t *sCloseRequests = NULL; @@ -234,6 +251,7 @@ static bool exitWindowGone(const AppContextT *ctx, int32_t winId); static void f1HelpHandler(void *ctx); static const char *findSwitch(int argc, char *argv[], const char *name); static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData); +static void shellBusyDispatch(void *ctx); bool shellCloseAllApps(AppContextT *ctx, int32_t keepAppId); bool shellCloseApp(AppContextT *ctx, int32_t appId); AppContextT *shellContext(void); @@ -241,6 +259,7 @@ int32_t shellDesktopChangeCount(void); void shellDesktopUpdate(void); static void shellFrame(AppContextT *ctx); static void shellIdleDispatch(void *ctx); +static bool shellInputHere(void *ctx); int shellMain(int argc, char *argv[]); void shellPurgeAppCallbacks(int32_t appId); void shellRegisterDesktopUpdate(void (*updateFn)(void)); @@ -373,7 +392,7 @@ static void desktopEnded(int32_t appId) { } -// When the serial library is loaded: feed it input timings and restore the +// When the cryptography library is loaded: feed it input timings and restore the // seed saved by the previous run (secRngLoadSeed rewrites the file at once). static void entropyInit(void) { RngSeedFileFnT loadSeed = (RngSeedFileFnT)dlsym(NULL, DVX_SYM("secRngLoadSeed")); @@ -615,6 +634,20 @@ static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData) { // asked running; apps already closed stay closed. A dialog box still // open (its nested event loop is on some stack) is raised and the exit // refused, so it can be answered first. +// Installed as the context's busyCallback: after a frame that painted, +// give the other tasks their turn (a program pumping dvxUpdate in its own +// loop -- a progress loop, DoEvents, a modal dialog -- would otherwise +// keep the CPU for as long as the screen changes), and run the idle +// handlers at least every SHELL_BUSY_IDLE_US. +static void shellBusyDispatch(void *ctx) { + if (platformMicros() - sIdleWalkAt >= SHELL_BUSY_IDLE_US) { + shellIdleDispatch(ctx); + } else if (tsActiveCount() > 1) { + tsYield(); + } +} + + bool shellCloseAllApps(AppContextT *ctx, int32_t keepAppId) { if (closeRefusedForDialog(ctx)) { return false; @@ -680,18 +713,13 @@ void shellDesktopUpdate(void) { // One main-loop iteration. dvxUpdate processes input, dispatches -// callbacks, composites and flushes. The explicit yield gives app tasks -// CPU time even during busy frames: a flurry of mouse moves could -// otherwise keep dvxUpdate from ever reaching its idle-callback yield. -// Reaping happens here, at the top level, not inside any callback or -// compositor operation. +// callbacks, composites and flushes, and yields to the app tasks through +// the idle or busy callback, whichever the frame was. Reaping happens +// here, at the top level, not inside any callback or compositor +// operation. static void shellFrame(AppContextT *ctx) { dvxUpdate(ctx); - if (tsActiveCount() > 1) { - tsYield(); - } - if (shellReapApps(ctx)) { shellDesktopUpdate(); } @@ -699,16 +727,19 @@ static void shellFrame(AppContextT *ctx) { // Installed as the context's single idleCallback. dvxUpdate calls this when -// all events are processed and nothing is dirty. We walk the idle handler -// list (serial/secLink pollers register here), then perform the cooperative -// yield unconditionally as the tail step -- so a registered poller can never -// suppress task switching the way the old single-slot scheme did, where BASIC -// overwrote idleYield outright and never restored it. The tsActiveCount > 1 -// check avoids a scheduler scan when the shell is the only task. Handlers -// unregistered mid-walk (e.g. via a nested modal dvxUpdate) are only marked -// dead until the outermost walk ends (see sIdleWalkDepth), so no handler is -// skipped and arrlen is re-read so none is indexed past the array. +// all events are processed and nothing is dirty (shellBusyDispatch too, at +// least every SHELL_BUSY_IDLE_US while frames keep painting). We walk the +// idle handler list (serial/secLink pollers register here), then perform the +// cooperative yield unconditionally as the tail step -- so a registered +// poller can never suppress task switching the way the old single-slot scheme +// did, where BASIC overwrote idleYield outright and never restored it. The +// tsActiveCount > 1 check avoids a scheduler scan when the shell is the only +// task. Handlers unregistered mid-walk (e.g. via a nested modal dvxUpdate) +// are only marked dead until the outermost walk ends (see sIdleWalkDepth), so +// no handler is skipped and arrlen is re-read so none is indexed past the +// array. static void shellIdleDispatch(void *ctx) { + sIdleWalkAt = platformMicros(); shellMinimizeIdle((AppContextT *)ctx); sIdleWalkDepth++; @@ -730,6 +761,27 @@ static void shellIdleDispatch(void *ctx) { } +// Installed as the context's inputHere: input, events, and widget timers +// are taken on the shell's own task, so a callback program (Program +// Manager, Control Panel, the IDE) runs on the shell task and never on a +// BASIC program's stack, and a BASIC program's events reach its own task +// (the form runtime queues those dispatched elsewhere). Another task's +// frame takes them only once the shell task has not for +// SHELL_INPUT_STALE_US. +static bool shellInputHere(void *ctx) { + uint64_t now = platformMicros(); + + (void)ctx; + + if (tsCurrentId() == TS_MAIN_TASK_ID) { + sInputPolledAt = now; + return true; + } + + return now - sInputPolledAt >= SHELL_INPUT_STALE_US; +} + + int shellMain(int argc, char *argv[]) { dvxLog("DVX Shell starting..."); @@ -794,9 +846,12 @@ int shellMain(int argc, char *argv[]) { } // Set callbacks AFTER dvxInit (which memsets the context to zero). - // The idle callback runs when dvxUpdate has no work to do; it is the - // main mechanism for giving app tasks CPU time in quiet periods. + // The idle callback runs when dvxUpdate has no work to do, the busy + // callback after a frame that painted: between them app tasks get + // CPU time on every frame. sCtx.idleCallback = shellIdleDispatch; + sCtx.busyCallback = shellBusyDispatch; + sCtx.inputHere = shellInputHere; sCtx.idleCtx = &sCtx; sCtx.onCtrlEsc = ctrlEscHandler; sCtx.ctrlEscCtx = &sCtx; @@ -810,6 +865,7 @@ int shellMain(int argc, char *argv[]) { dvxLog("Selected: %ldx%ld %ldbpp (pitch %ld)", (long)sCtx.display.width, (long)sCtx.display.height, (long)sCtx.display.format.bitsPerPixel, (long)sCtx.display.pitch); entropyInit(); + shellRegisterIdle(shellProgCacheIdle, NULL); // Gather system information (CPU, memory, video, drives, etc.) // after dvxInit so the display fields have real values to report. @@ -1148,6 +1204,7 @@ static void shellTeardown(void) { platformSetAppExitHook(NULL); sRecoveryArmed = false; entropyShutdown(); + shellProgCacheShutdown(); tsShutdown(); dvxShutdown(&sCtx); prefsClose(sPrefs); @@ -1178,15 +1235,20 @@ void shellTestBind(AppContextT *ctx) { sDepthBase = 0; sDesktopWalkDepth = 0; sIdleWalkDepth = 0; + sIdleWalkAt = 0; + sInputPolledAt = 0; sExitRequested = false; sCtxRef = ctx ? ctx : &sCtx; shellAppTestReset(); + shellProgCacheDrop(); if (ctx) { shellAppInit(); dvxMemAppIdPtr = &ctx->currentAppId; tsSetContextHooks(appIdSave, appIdRestore); ctx->idleCallback = shellIdleDispatch; + ctx->busyCallback = shellBusyDispatch; + ctx->inputHere = shellInputHere; ctx->idleCtx = ctx; ctx->onF1 = f1HelpHandler; ctx->f1Ctx = ctx; diff --git a/src/libs/kpunch/dvxshell/shellProg.c b/src/libs/kpunch/dvxshell/shellProg.c index ed88f456..be1cb919 100644 --- a/src/libs/kpunch/dvxshell/shellProg.c +++ b/src/libs/kpunch/dvxshell/shellProg.c @@ -21,16 +21,21 @@ // IN THE SOFTWARE. // shellProg.c -- Installed programs (see shellProg.h) +// +// What is read from program and picture files (names, descriptions, +// pictures) goes through the program cache (shellProgCache.h), so a file +// that has not changed is not opened again. #include "shellProg.h" #include "shellAssoc.h" +#include "shellProgCache.h" #include "dvxPlat.h" +#include "dvxRes.h" #include #include #include #include -#include #include "dvxMem.h" #include "stb_ds_wrap.h" @@ -38,45 +43,98 @@ #define PROG_RES_NAME "name" #define PROG_RES_DESC "description" +// shellProgramScan's walk: what it skips and reads, and the list +typedef struct { + const char *skipPath; + bool readInfo; + ShellProgressFnT progress; + void *progressCtx; + ShellProgramT **list; +} ScanWalkT; + // ============================================================ // Prototypes // ============================================================ -static uint8_t *itemIconPick(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch, char *chosen, int32_t chosenSize); -static void scanDir(const char *dir, const char *skipPath, bool readInfo, ShellProgressFnT progress, void *progressCtx, ShellProgramT **list); +static uint8_t *iconFit(const AppContextT *ctx, uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, int32_t edge, int32_t *outPitch); +static uint8_t *iconScale(const AppContextT *ctx, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, int32_t edge, int32_t *outPitch); +static uint8_t *itemIconPick(AppContextT *ctx, const char *iconPath, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch, char *chosen, int32_t chosenSize); +static uint8_t *pictureLoad(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch); +static bool scanVisit(const char *path, const char *name, void *ctx); bool shellIsDosProgram(const char *path); bool shellIsProgram(const char *path); uint8_t *shellItemIcon(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch); bool shellItemIconFile(AppContextT *ctx, const char *iconPath, const char *path, char *out, int32_t outSize); +bool shellItemIcons(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t smallEdge, ShellIconsT *out); uint8_t *shellProgramIcon(AppContextT *ctx, const char *path, int32_t edge, int32_t *outPitch); void shellProgramInfo(const char *path, ShellProgramT *out); void shellProgramListFree(ShellProgramT *list); int32_t shellProgramScan(const char *dir, const char *skipPath, bool readInfo, ShellProgressFnT progress, void *progressCtx, ShellProgramT **outList); +// The picture src (owned: it is returned or freed) at edge x edge pixels: +// itself when it is that size already, else a scaled copy (iconScale). +static uint8_t *iconFit(const AppContextT *ctx, uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, int32_t edge, int32_t *outPitch) { + if (!src || (srcW == edge && srcH == edge)) { + *outPitch = srcPitch; + return src; + } + + uint8_t *dst = iconScale(ctx, src, srcW, srcH, srcPitch, edge, outPitch); + + dvxFreeImage(src); + return dst; +} + + +// A copy of the picture src at edge x edge pixels, nearest pixel (icons +// are small, and a whole number of source pixels per target pixel is not +// needed). NULL when out of memory. +static uint8_t *iconScale(const AppContextT *ctx, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, int32_t edge, int32_t *outPitch) { + int32_t bpp = ctx->display.format.bytesPerPixel; + int32_t dstPitch = edge * bpp; + uint8_t *dst = (uint8_t *)malloc((size_t)dstPitch * (size_t)edge); + + if (dst) { + for (int32_t y = 0; y < edge; y++) { + const uint8_t *srcRow = src + (y * srcH / edge) * srcPitch; + + for (int32_t x = 0; x < edge; x++) { + memcpy(dst + y * dstPitch + x * bpp, srcRow + (x * srcW / edge) * bpp, (size_t)bpp); + } + } + } + + *outPitch = dstPitch; + return dst; +} + + // The item icon rule: the picture file iconPath (NULL or "" for none), // else path's own picture, else for a DOS program SHELL_DOS_ICON, else -// for a document the icon of the program its file type opens in. chosen (chosenSize bytes, may be NULL) gets -// the file the icon came from ("" for none). -static uint8_t *itemIconPick(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch, char *chosen, int32_t chosenSize) { +// for a document the icon of the program its file type opens in, decoded +// at its own size (dvxFreeImage it; NULL when there is none). chosen +// (chosenSize bytes, may be NULL) gets the file the icon came from ("" +// for none). +static uint8_t *itemIconPick(AppContextT *ctx, const char *iconPath, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch, char *chosen, int32_t chosenSize) { char opener[DVX_MAX_PATH]; char args[SHELL_ARGS_MAX]; const char *from = (iconPath && iconPath[0]) ? iconPath : NULL; - uint8_t *icon = from ? shellProgramIcon(ctx, from, edge, outPitch) : NULL; + uint8_t *icon = from ? pictureLoad(ctx, from, outW, outH, outPitch) : NULL; - if (!icon) { + if (!icon && path && path[0]) { from = path; - icon = shellProgramIcon(ctx, from, edge, outPitch); + icon = pictureLoad(ctx, from, outW, outH, outPitch); } if (!icon && shellIsDosProgram(path)) { from = SHELL_DOS_ICON; - icon = shellProgramIcon(ctx, from, edge, outPitch); + icon = pictureLoad(ctx, from, outW, outH, outPitch); } if (!icon && path && !shellIsProgram(path) && shellAssocCommand(path, opener, sizeof(opener), args, sizeof(args))) { from = opener; - icon = shellProgramIcon(ctx, from, edge, outPitch); + icon = pictureLoad(ctx, from, outW, outH, outPitch); } if (chosen && chosenSize > 0) { @@ -87,49 +145,66 @@ static uint8_t *itemIconPick(AppContextT *ctx, const char *iconPath, const char } -// The names are read in full before anything is opened: DOS has few file -// handles, and each program's resources are opened while listing. -static void scanDir(const char *dir, const char *skipPath, bool readInfo, ShellProgressFnT progress, void *progressCtx, ShellProgramT **list) { - char **names = dvxReadDir(dir); +// path's picture as dvxLoadImage decodes it (NULL: none), its bytes from +// the program cache: a file known to have none, or whose bytes are kept, +// is not opened. A picture too big to keep is loaded from its file each +// time. A file is noted as having none only when its bytes are no image +// (a decode that ran out of memory is not). +static uint8_t *pictureLoad(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch) { + uint8_t *bytes = NULL; + uint32_t len = 0; + int32_t w = 0; + int32_t h = 0; + uint8_t *pixels; + bool none; - if (!names) { - return; + if (!shellProgCachePicture(path, &bytes, &len)) { + pixels = dvxLoadImage(ctx, path, outW, outH, outPitch); + none = !pixels && !dvxImageInfo(path, &w, &h); + } else { + pixels = bytes ? dvxLoadImageFromMemory(ctx, bytes, (int32_t)len, outW, outH, outPitch) : NULL; + none = bytes && !pixels && !dvxImageInfoFromMemory(bytes, (int32_t)len, &w, &h); } - for (int32_t i = 0; i < arrlen(names); i++) { - char path[DVX_MAX_PATH]; - struct stat st; - - 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 && platformPathSame(path, skipPath))) { - continue; - } - - ShellProgramT prog; - - if (readInfo) { - shellProgramInfo(path, &prog); - } else { - memset(&prog, 0, sizeof(prog)); - snprintf(prog.path, sizeof(prog.path), "%s", path); - } - - arrput(*list, prog); - - if (progress) { - progress(progressCtx); - } + if (none) { + shellProgCacheNoPicture(path); } - dvxReadDirFree(names); + free(bytes); + return pixels; +} + + +// A file of shellProgramScan's walk (platformWalkDir: the names are read +// in full before anything is opened): a program, unless it is skipPath. +// Reading names checks each program against the program cache while its +// folder is fresh, so the cache answers for the files that have not +// changed. +static bool scanVisit(const char *path, const char *name, void *ctx) { + ScanWalkT *walk = (ScanWalkT *)ctx; + ShellProgramT prog; + + (void)name; + + if (!shellIsProgram(path) || (walk->skipPath && platformPathSame(path, walk->skipPath))) { + return true; + } + + if (walk->readInfo) { + shellProgCacheCheck(path); + shellProgramInfo(path, &prog); + } else { + memset(&prog, 0, sizeof(prog)); + snprintf(prog.path, sizeof(prog.path), "%s", path); + } + + arrput(*walk->list, prog); + + if (walk->progress) { + walk->progress(walk->progressCtx); + } + + return true; } @@ -153,56 +228,64 @@ bool shellIsProgram(const char *path) { uint8_t *shellItemIcon(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch) { - return itemIconPick(ctx, iconPath, path, edge, outPitch, NULL, 0); + int32_t w = 0; + int32_t h = 0; + int32_t pitch = 0; + + if (!ctx || edge <= 0 || !outPitch) { + return NULL; + } + + return iconFit(ctx, itemIconPick(ctx, iconPath, path, &w, &h, &pitch, NULL, 0), w, h, pitch, edge, outPitch); } bool shellItemIconFile(AppContextT *ctx, const char *iconPath, const char *path, char *out, int32_t outSize) { + int32_t w = 0; + int32_t h = 0; int32_t pitch = 0; - uint8_t *icon = itemIconPick(ctx, iconPath, path, SHELL_PROG_ICON_SMALL, &pitch, out, outSize); + uint8_t *icon = ctx ? itemIconPick(ctx, iconPath, path, &w, &h, &pitch, out, outSize) : NULL; dvxFreeImage(icon); return icon != NULL; } -// Nearest-pixel scaling: a whole number of source pixels per target pixel -// is not needed, and icons are small. +// One decode for both sizes. +bool shellItemIcons(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t smallEdge, ShellIconsT *out) { + int32_t w = 0; + int32_t h = 0; + int32_t pitch = 0; + + memset(out, 0, sizeof(*out)); + + if (!ctx || edge <= 0 || smallEdge <= 0) { + return false; + } + + uint8_t *src = itemIconPick(ctx, iconPath, path, &w, &h, &pitch, NULL, 0); + + if (!src) { + return false; + } + + out->iconSmall = iconScale(ctx, src, w, h, pitch, smallEdge, &out->iconSmallPitch); + out->icon = iconFit(ctx, src, w, h, pitch, edge, &out->iconPitch); + return out->icon != NULL && out->iconSmall != NULL; +} + + +// A picture file, or a program's own icon. uint8_t *shellProgramIcon(AppContextT *ctx, const char *path, int32_t edge, int32_t *outPitch) { - int32_t srcW = 0; - int32_t srcH = 0; - int32_t srcPitch = 0; - uint8_t *src = NULL; + int32_t w = 0; + int32_t h = 0; + int32_t pitch = 0; if (!ctx || !path || !path[0] || edge <= 0 || !outPitch) { return NULL; } - // A picture file, or a program's own icon - src = dvxLoadImage(ctx, path, &srcW, &srcH, &srcPitch); - - if (!src || (srcW == edge && srcH == edge)) { - *outPitch = srcPitch; - return src; - } - - int32_t bpp = ctx->display.format.bytesPerPixel; - int32_t dstPitch = edge * bpp; - uint8_t *dst = (uint8_t *)malloc((size_t)dstPitch * (size_t)edge); - - if (dst) { - for (int32_t y = 0; y < edge; y++) { - const uint8_t *srcRow = src + (y * srcH / edge) * srcPitch; - - for (int32_t x = 0; x < edge; x++) { - memcpy(dst + y * dstPitch + x * bpp, srcRow + (x * srcW / edge) * bpp, (size_t)bpp); - } - } - } - - dvxFreeImage(src); - *outPitch = dstPitch; - return dst; + return iconFit(ctx, pictureLoad(ctx, path, &w, &h, &pitch), w, h, pitch, edge, outPitch); } @@ -218,10 +301,25 @@ void shellProgramInfo(const char *path, ShellProgramT *out) { out->name[nameLen] = '\0'; out->name[0] = (char)toupper((unsigned char)out->name[0]); - if (shellIsProgram(path)) { - dvxResLoadText(path, PROG_RES_NAME, out->name, sizeof(out->name)); - dvxResLoadText(path, PROG_RES_DESC, out->description, sizeof(out->description)); + if (!shellIsProgram(path) || shellProgCacheInfo(path, out)) { + return; } + + // One resource directory read for both texts + DvxResHandleT *res = dvxResOpen(path); + + if (res) { + char name[sizeof(out->name)]; + + if (dvxResReadText(res, PROG_RES_NAME, name, sizeof(name))) { + snprintf(out->name, sizeof(out->name), "%s", name); + } + + dvxResReadText(res, PROG_RES_DESC, out->description, sizeof(out->description)); + dvxResClose(res); + } + + shellProgCacheSetInfo(path, out); } @@ -234,7 +332,14 @@ int32_t shellProgramScan(const char *dir, const char *skipPath, bool readInfo, S ShellProgramT *list = NULL; if (outList) { - scanDir(dir ? dir : SHELL_PROG_DIR, skipPath, readInfo, progress, progressCtx, &list); + ScanWalkT walk; + + walk.skipPath = skipPath; + walk.readInfo = readInfo; + walk.progress = progress; + walk.progressCtx = progressCtx; + walk.list = &list; + platformWalkDir(dir ? dir : SHELL_PROG_DIR, scanVisit, &walk); *outList = list; } diff --git a/src/libs/kpunch/dvxshell/shellProg.h b/src/libs/kpunch/dvxshell/shellProg.h index 6ac284d6..d537d96c 100644 --- a/src/libs/kpunch/dvxshell/shellProg.h +++ b/src/libs/kpunch/dvxshell/shellProg.h @@ -26,7 +26,9 @@ // description come from its "name" and "description" resources (the file // name when it has none) and its picture from its "icon32" resource. // Program Manager's Installed Programs group, File Manager and setup -// programs all read programs through these calls. +// programs all read programs through these calls. What they read is kept +// in the program cache (PROGRAMS.CAC in the user folder: shellProgCache.h), +// so a program that has not changed is not opened again. #ifndef SHELL_PROG_H #define SHELL_PROG_H @@ -62,6 +64,15 @@ typedef struct { char description[SHELL_PROG_DESC_MAX]; } ShellProgramT; +// An item's icon at two sizes (shellItemIcons), each freed with +// dvxFreeImage. +typedef struct { + uint8_t *icon; + uint8_t *iconSmall; + int32_t iconPitch; + int32_t iconSmallPitch; +} ShellIconsT; + // Called once per program found by shellProgramScan (e.g. to keep the // screen responsive during a long scan). typedef void (*ShellProgressFnT)(void *ctx); @@ -88,6 +99,11 @@ uint8_t *shellItemIcon(AppContextT *ctx, const char *iconPath, const char *path, // BASIC's LOADPICTURE) loads from that file. bool shellItemIconFile(AppContextT *ctx, const char *iconPath, const char *path, char *out, int32_t outSize); +// shellItemIcon at edge and at smallEdge from one decode of the picture +// (Program Manager's large and small views). False when there is none +// (out empty) or memory ran out (what was made is in out). +bool shellItemIcons(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t smallEdge, ShellIconsT *out); + // A picture edge x edge pixels in display format: a program's icon32 // resource, or any image file DVX can load (BMP, PNG, JPEG, GIF), scaled // to fit. NULL when there is none. Free with dvxFreeImage. diff --git a/src/libs/kpunch/dvxshell/shellProgCache.c b/src/libs/kpunch/dvxshell/shellProgCache.c new file mode 100644 index 00000000..3ce9fef7 --- /dev/null +++ b/src/libs/kpunch/dvxshell/shellProgCache.c @@ -0,0 +1,674 @@ +// 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. + +// shellProgCache.c -- the program cache (see shellProgCache.h) +// +// The file: CACHE_MAGIC, a 16-bit version, 16 bits kept 0, the entry +// count (32 bits), then each entry: its path (a 16-bit length and the +// bytes), its file's stamp (size and time, 32 bits each), its flags (8 +// bits), with CACHE_FLAG_INFO the name and description (an 8-bit length +// and the bytes each), and with CACHE_FLAG_PICTURE the picture (a 32-bit +// length and the bytes; 0: none). Numbers are little-endian. A file +// that does not read as this is ignored, and replaced at the next save. + +#include "shellProgCache.h" +#include "dvxApp.h" +#include "dvxEndian.h" +#include "dvxMem.h" +#include "dvxPlat.h" + +#include +#include +#include +#include "stb_ds_wrap.h" + +#define CACHE_MAGIC "DVXPCACH" +#define CACHE_MAGIC_LEN 8 +#define CACHE_VERSION 1 +#define CACHE_VERSION_AT CACHE_MAGIC_LEN +#define CACHE_COUNT_AT (CACHE_MAGIC_LEN + 4) +#define CACHE_HEAD_LEN (CACHE_MAGIC_LEN + 8) +#define CACHE_FLAG_INFO 0x01 // the name and description +#define CACHE_FLAG_PICTURE 0x02 // the picture is known (bytes, or none) +#define CACHE_MAX_BYTES 262144 // a saved cache at most: entries not asked about this run go first +#define LEN8_BYTES 1 +#define LEN16_BYTES 2 +#define LEN32_BYTES 4 +#define FLAG_BYTES 1 +#define STAMP_BYTES (2 * LEN32_BYTES) +#define US_PER_MS 1000 + +typedef struct { + char *path; + PlatformStampT stamp; + uint32_t checkedCount; // platformInstalledChangeCount when checked + uint8_t *picture; // the image file's bytes (pictureLen), NULL for none + uint32_t pictureLen; + uint8_t flags; // CACHE_FLAG_* + bool checked; // against its file, this run + bool used; // asked about this run + char name[SHELL_APP_NAME_MAX]; + char description[SHELL_PROG_DESC_MAX]; +} CacheEntryT; + +// A save under way: data (len bytes) going to tmpPath, pos of them written +typedef struct { + FILE *f; + uint8_t *data; + uint32_t len; + uint32_t pos; + char tmpPath[DVX_MAX_PATH]; +} CacheSaveT; + +// What parsing a saved cache has left +typedef struct { + const uint8_t *p; + uint32_t left; +} ReadT; + +// ============================================================ +// Prototypes +// ============================================================ + +static bool cacheParse(const uint8_t *data, uint32_t len); +static void cacheRead(void); +static void changed(void); +static void entriesFree(void); +static uint32_t entryBytes(const CacheEntryT *e); +static CacheEntryT *entryChecked(const char *path, bool now); +static void entryClear(CacheEntryT *e); +static int32_t entryIndex(const char *path); +static void entryRemove(int32_t i); +static uint8_t *formatCache(uint32_t *outLen); +static void formatKeep(bool used, int32_t **keep, uint32_t *total); +static bool parseEntry(ReadT *r, CacheEntryT *e); +static bool pictureRead(CacheEntryT *e); +static uint8_t *putEntry(uint8_t *w, const CacheEntryT *e); +static uint8_t *putText(uint8_t *w, const char *text); +static void saveBegin(void); +static void saveEnd(bool written); +static void saveStep(void); +void shellProgCacheCheck(const char *path); +void shellProgCacheDrop(void); +void shellProgCacheIdle(void *ctx); +bool shellProgCacheInfo(const char *path, ShellProgramT *out); +void shellProgCacheNoPicture(const char *path); +bool shellProgCachePicture(const char *path, uint8_t **outData, uint32_t *outLen); +void shellProgCacheSetInfo(const char *path, const ShellProgramT *info); +void shellProgCacheShutdown(void); +static const uint8_t *take(ReadT *r, uint32_t n); +static bool takeText(ReadT *r, char *out, int32_t outSize); + +// ============================================================ +// Module state +// ============================================================ + +static CacheEntryT *sEntries = NULL; // stb_ds +static bool sLoaded = false; // the saved cache has been read +static bool sDirty = false; // changed since the last save began +static uint64_t sChangedUs = 0; // platformMicros of the last change +static uint64_t sUsedUs = 0; // platformMicros of the last question +static CacheSaveT sSave; + + +static bool cacheParse(const uint8_t *data, uint32_t len) { + ReadT r = { data, len }; + const uint8_t *head = take(&r, CACHE_HEAD_LEN); + + if (!head || memcmp(head, CACHE_MAGIC, CACHE_MAGIC_LEN) != 0 || dvxGetLe16(head + CACHE_VERSION_AT) != CACHE_VERSION) { + return false; + } + + uint32_t count = dvxGetLe32(head + CACHE_COUNT_AT); + + for (uint32_t i = 0; i < count; i++) { + CacheEntryT e; + + if (!parseEntry(&r, &e)) { + return false; + } + + arrput(sEntries, e); + } + + return r.left == 0; +} + + +// The saved cache, once, on first use. +static void cacheRead(void) { + char path[DVX_MAX_PATH]; + int32_t len = 0; + uint8_t *data = platformUserPath(path, sizeof(path), SHELL_PROG_CACHE_NAME) ? (uint8_t *)platformReadFile(path, &len) : NULL; + + sLoaded = true; + + if (data && !cacheParse(data, (uint32_t)len)) { + entriesFree(); + dvxLog("Shell: %s is no program cache; it is made again", path); + } + + free(data); +} + + +static void changed(void) { + sDirty = true; + sChangedUs = platformMicros(); +} + + +static void entriesFree(void) { + for (int32_t i = 0; i < (int32_t)arrlen(sEntries); i++) { + free(sEntries[i].path); + free(sEntries[i].picture); + } + + arrfree(sEntries); + sEntries = NULL; +} + + +// What e takes in the saved cache. +static uint32_t entryBytes(const CacheEntryT *e) { + uint32_t n = LEN16_BYTES + (uint32_t)strlen(e->path) + STAMP_BYTES + FLAG_BYTES; + + if (e->flags & CACHE_FLAG_INFO) { + n += 2 * LEN8_BYTES + (uint32_t)strlen(e->name) + (uint32_t)strlen(e->description); + } + + if (e->flags & CACHE_FLAG_PICTURE) { + n += LEN32_BYTES + e->pictureLen; + } + + return n; +} + + +// path's entry, made when it has none, checked against its file when it +// has not been since what is installed last changed, or always when now. +// A file that changed loses what was kept for it; NULL (and no entry) +// when path is no file. +static CacheEntryT *entryChecked(const char *path, bool now) { + PlatformStampT stamp; + uint32_t count = platformInstalledChangeCount(); + + if (!path || !path[0]) { + return NULL; + } + + if (!sLoaded) { + cacheRead(); + } + + sUsedUs = platformMicros(); + + int32_t i = entryIndex(path); + + if (i >= 0 && !now && sEntries[i].checked && sEntries[i].checkedCount == count) { + sEntries[i].used = true; + return &sEntries[i]; + } + + if (!platformFileStamp(path, &stamp)) { + if (i >= 0) { + entryRemove(i); + changed(); + } + + return NULL; + } + + if (i < 0) { + CacheEntryT e; + + memset(&e, 0, sizeof(e)); + e.path = strdup(path); + e.stamp = stamp; + + if (!e.path) { + return NULL; + } + + arrput(sEntries, e); + i = (int32_t)arrlen(sEntries) - 1; + } else if (sEntries[i].stamp.size != stamp.size || sEntries[i].stamp.time != stamp.time) { + entryClear(&sEntries[i]); + sEntries[i].stamp = stamp; + changed(); + } + + sEntries[i].checked = true; + sEntries[i].checkedCount = count; + sEntries[i].used = true; + return &sEntries[i]; +} + + +// Forget what was kept for e's file. +static void entryClear(CacheEntryT *e) { + free(e->picture); + e->picture = NULL; + e->pictureLen = 0; + e->flags = 0; + e->name[0] = '\0'; + e->description[0] = '\0'; +} + + +static int32_t entryIndex(const char *path) { + for (int32_t i = 0; i < (int32_t)arrlen(sEntries); i++) { + if (platformPathSame(sEntries[i].path, path)) { + return i; + } + } + + return -1; +} + + +static void entryRemove(int32_t i) { + free(sEntries[i].path); + free(sEntries[i].picture); + arrdel(sEntries, i); +} + + +// The cache as saved: the entries asked about this run, then the others +// while the whole stays within CACHE_MAX_BYTES (malloc'd; NULL when out of +// memory). +static uint8_t *formatCache(uint32_t *outLen) { + int32_t *keep = NULL; + uint32_t total = CACHE_HEAD_LEN; + + formatKeep(true, &keep, &total); + formatKeep(false, &keep, &total); + + uint8_t *data = (uint8_t *)malloc(total); + + if (data) { + uint8_t *w = data + CACHE_HEAD_LEN; + + memcpy(data, CACHE_MAGIC, CACHE_MAGIC_LEN); + dvxPutLe16(data + CACHE_VERSION_AT, CACHE_VERSION); + dvxPutLe16(data + CACHE_VERSION_AT + LEN16_BYTES, 0); + dvxPutLe32(data + CACHE_COUNT_AT, (uint32_t)arrlen(keep)); + + for (int32_t k = 0; k < (int32_t)arrlen(keep); k++) { + w = putEntry(w, &sEntries[keep[k]]); + } + } + + arrfree(keep); + *outLen = total; + return data; +} + + +// The entries (asked about this run, or not) that keep something, onto +// keep; the ones not asked about only while the total stays within +// CACHE_MAX_BYTES. +static void formatKeep(bool used, int32_t **keep, uint32_t *total) { + for (int32_t i = 0; i < (int32_t)arrlen(sEntries); i++) { + const CacheEntryT *e = &sEntries[i]; + uint32_t n = entryBytes(e); + + if (e->flags && e->used == used && (used || *total + n <= CACHE_MAX_BYTES)) { + *total += n; + arrput(*keep, i); + } + } +} + + +static bool parseEntry(ReadT *r, CacheEntryT *e) { + memset(e, 0, sizeof(*e)); + + const uint8_t *lenAt = take(r, LEN16_BYTES); + uint32_t pathLen = lenAt ? dvxGetLe16(lenAt) : 0; + const uint8_t *path = (pathLen > 0 && pathLen < DVX_MAX_PATH) ? take(r, pathLen) : NULL; + const uint8_t *fixed = path ? take(r, STAMP_BYTES + FLAG_BYTES) : NULL; + + if (!fixed) { + return false; + } + + e->stamp.size = dvxGetLe32(fixed); + e->stamp.time = dvxGetLe32(fixed + LEN32_BYTES); + e->flags = fixed[STAMP_BYTES]; + + if ((e->flags & CACHE_FLAG_INFO) && !(takeText(r, e->name, (int32_t)sizeof(e->name)) && takeText(r, e->description, (int32_t)sizeof(e->description)))) { + return false; + } + + const uint8_t *picLenAt = (e->flags & CACHE_FLAG_PICTURE) ? take(r, LEN32_BYTES) : NULL; + uint32_t picLen = picLenAt ? dvxGetLe32(picLenAt) : 0; + const uint8_t *picture = picLen > 0 ? take(r, picLen) : NULL; + + if (((e->flags & CACHE_FLAG_PICTURE) && !picLenAt) || (picLen > 0 && !picture)) { + return false; + } + + e->path = (char *)malloc(pathLen + 1); + e->picture = picture ? (uint8_t *)malloc(picLen) : NULL; + + if (!e->path || (picture && !e->picture)) { + free(e->path); + free(e->picture); + return false; + } + + memcpy(e->path, path, pathLen); + e->path[pathLen] = '\0'; + + if (picture) { + memcpy(e->picture, picture, picLen); + e->pictureLen = picLen; + } + + return true; +} + + +// e's picture bytes from its file: a DVX file's icon resource, else the +// file itself when it is small enough (bytes that are no image are found +// out by the decode: shellProgCacheNoPicture). False, and nothing kept, +// for a bigger file. +static bool pictureRead(CacheEntryT *e) { + uint32_t len = 0; + uint8_t *bytes = (uint8_t *)dvxResLoadIconData(e->path, &len); + + if (!bytes) { + int32_t fileLen = 0; + + if (e->stamp.size > SHELL_PROG_CACHE_PICTURE_MAX) { + return false; + } + + bytes = (uint8_t *)platformReadFile(e->path, &fileLen); + len = bytes ? (uint32_t)fileLen : 0; + } + + e->picture = bytes; + e->pictureLen = len; + e->flags |= CACHE_FLAG_PICTURE; + changed(); + return true; +} + + +static uint8_t *putEntry(uint8_t *w, const CacheEntryT *e) { + uint32_t pathLen = (uint32_t)strlen(e->path); + + dvxPutLe16(w, pathLen); + memcpy(w + LEN16_BYTES, e->path, pathLen); + w += LEN16_BYTES + pathLen; + dvxPutLe32(w, e->stamp.size); + dvxPutLe32(w + LEN32_BYTES, e->stamp.time); + w[STAMP_BYTES] = e->flags; + w += STAMP_BYTES + FLAG_BYTES; + + if (e->flags & CACHE_FLAG_INFO) { + w = putText(w, e->name); + w = putText(w, e->description); + } + + if (e->flags & CACHE_FLAG_PICTURE) { + dvxPutLe32(w, e->pictureLen); + memcpy(w + LEN32_BYTES, e->picture, e->pictureLen); + w += LEN32_BYTES + e->pictureLen; + } + + return w; +} + + +// A name or description: its length (it fits a byte: SHELL_APP_NAME_MAX, +// SHELL_PROG_DESC_MAX) and its bytes. +static uint8_t *putText(uint8_t *w, const char *text) { + uint32_t len = (uint32_t)strlen(text); + + w[0] = (uint8_t)len; + memcpy(w + LEN8_BYTES, text, len); + return w + LEN8_BYTES + len; +} + + +// The cache formatted and the scratch file opened; the idle time writes +// it (saveStep). +static void saveBegin(void) { + char path[DVX_MAX_PATH]; + + sDirty = false; + + if (platformIsReadOnly() || !platformUserPath(path, sizeof(path), SHELL_PROG_CACHE_NAME) || !platformPathSibling(sSave.tmpPath, sizeof(sSave.tmpPath), path, PLATFORM_TMP_EXT)) { + return; + } + + sSave.data = formatCache(&sSave.len); + sSave.pos = 0; + sSave.f = sSave.data ? fopen(sSave.tmpPath, "wb") : NULL; + + if (!sSave.f) { + saveEnd(false); + } +} + + +// The scratch file closed and, when all of it was written, put in place +// of the saved cache (platformReplaceFile); else it goes. +static void saveEnd(bool written) { + char path[DVX_MAX_PATH]; + char bakPath[DVX_MAX_PATH]; + bool ok = written; + + if (sSave.f && fclose(sSave.f) != 0) { + ok = false; + } + + sSave.f = NULL; + ok = ok && platformUserPath(path, sizeof(path), SHELL_PROG_CACHE_NAME) && platformPathSibling(bakPath, sizeof(bakPath), path, PLATFORM_BAK_EXT) && platformReplaceFile(sSave.tmpPath, path, bakPath); + + if (!ok) { + remove(sSave.tmpPath); + dvxLog("Shell: the program cache could not be saved"); + } + + free(sSave.data); + sSave.data = NULL; +} + + +static void saveStep(void) { + uint32_t n = sSave.len - sSave.pos; + + if (n > SHELL_PROG_CACHE_SAVE_CHUNK) { + n = SHELL_PROG_CACHE_SAVE_CHUNK; + } + + if (fwrite(sSave.data + sSave.pos, 1, n, sSave.f) != n) { + saveEnd(false); + return; + } + + sSave.pos += n; + + if (sSave.pos == sSave.len) { + saveEnd(true); + } +} + + +void shellProgCacheCheck(const char *path) { + int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); + + entryChecked(path, true); + dvxMemSetApp(prev); +} + + +void shellProgCacheDrop(void) { + if (sSave.f) { + fclose(sSave.f); + sSave.f = NULL; + remove(sSave.tmpPath); + } + + free(sSave.data); + sSave.data = NULL; + entriesFree(); + sLoaded = false; + sDirty = false; +} + + +void shellProgCacheIdle(void *ctx) { + uint64_t now = platformMicros(); + int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); + + (void)ctx; + + if (sSave.f) { + saveStep(); + } else if (sDirty && now - sChangedUs >= (uint64_t)SHELL_PROG_CACHE_SAVE_MS * US_PER_MS) { + saveBegin(); + } else if (!sDirty && sLoaded && now - sUsedUs >= (uint64_t)SHELL_PROG_CACHE_DROP_MS * US_PER_MS) { + shellProgCacheDrop(); + } + + dvxMemSetApp(prev); +} + + +bool shellProgCacheInfo(const char *path, ShellProgramT *out) { + int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); + CacheEntryT *e = entryChecked(path, false); + bool hit = e && (e->flags & CACHE_FLAG_INFO); + + if (hit) { + snprintf(out->name, sizeof(out->name), "%s", e->name); + snprintf(out->description, sizeof(out->description), "%s", e->description); + } + + dvxMemSetApp(prev); + return hit; +} + + +// Only a change is saved: none known already is left alone. +void shellProgCacheNoPicture(const char *path) { + int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); + CacheEntryT *e = entryChecked(path, false); + + if (e && (e->picture || !(e->flags & CACHE_FLAG_PICTURE))) { + free(e->picture); + e->picture = NULL; + e->pictureLen = 0; + e->flags |= CACHE_FLAG_PICTURE; + changed(); + } + + dvxMemSetApp(prev); +} + + +// A copy, so the caller's decode never holds the cache's own bytes (it may +// let other tasks run, which may ask the cache too). +bool shellProgCachePicture(const char *path, uint8_t **outData, uint32_t *outLen) { + int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); + CacheEntryT *e = entryChecked(path, false); + bool known = e && ((e->flags & CACHE_FLAG_PICTURE) || pictureRead(e)); + uint8_t *copy = (known && e->picture) ? (uint8_t *)malloc(e->pictureLen) : NULL; + + if (copy) { + memcpy(copy, e->picture, e->pictureLen); + } + + *outData = copy; + *outLen = copy ? e->pictureLen : 0; + dvxMemSetApp(prev); + return known && (copy || !e->picture); +} + + +void shellProgCacheSetInfo(const char *path, const ShellProgramT *info) { + int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); + CacheEntryT *e = entryChecked(path, false); + + if (e) { + snprintf(e->name, sizeof(e->name), "%s", info->name); + snprintf(e->description, sizeof(e->description), "%s", info->description); + e->flags |= CACHE_FLAG_INFO; + changed(); + } + + dvxMemSetApp(prev); +} + + +// A save under way is finished first; a change since it began is saved +// after it. +void shellProgCacheShutdown(void) { + int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); + + while (sSave.f || sDirty) { + if (sSave.f) { + saveStep(); + } else { + saveBegin(); + } + } + + shellProgCacheDrop(); + dvxMemSetApp(prev); +} + + +// The next n bytes of r (NULL, and nothing taken, when fewer are left). +static const uint8_t *take(ReadT *r, uint32_t n) { + const uint8_t *at = r->p; + + if (n > r->left) { + return NULL; + } + + r->p += n; + r->left -= n; + return at; +} + + +// A text putText wrote, into out (outSize bytes); false when it is not +// there whole or does not fit. +static bool takeText(ReadT *r, char *out, int32_t outSize) { + const uint8_t *lenAt = take(r, LEN8_BYTES); + uint32_t len = lenAt ? lenAt[0] : 0; + const uint8_t *text = (lenAt && (int32_t)len < outSize) ? take(r, len) : NULL; + + if (!text) { + return false; + } + + memcpy(out, text, len); + out[len] = '\0'; + return true; +} diff --git a/src/libs/kpunch/dvxshell/shellProgCache.h b/src/libs/kpunch/dvxshell/shellProgCache.h new file mode 100644 index 00000000..b8c86787 --- /dev/null +++ b/src/libs/kpunch/dvxshell/shellProgCache.h @@ -0,0 +1,92 @@ +// 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. + +// shellProgCache.h -- the program cache (dvxshell's own: see shellProg.c) +// +// Reading a program's name, description, and icon takes several file +// opens, tens of milliseconds each on a slow disk, every time DVX starts. +// The program cache keeps what was read, with each file's stamp +// (platformFileStamp), in SHELL_PROG_CACHE_NAME in the user folder, and +// answers from it while the file is unchanged. shellProgramInfo, the item +// icon calls, and shellProgramIcon go through it, so Program Manager, File +// Manager, and BASIC programs gain it without a change. +// +// A file is checked against its stamp (one directory search) the first +// time it is asked about after what is installed last changed +// (platformInstalledChangeCount), and whenever the program scan passes it +// (shellProgCacheCheck, while its folder is fresh in DOS's buffers); a +// file that changed is read again, and one that is gone loses its entry. +// A picture is kept as the image file's bytes (a DVX file's icon resource, +// or a picture file up to SHELL_PROG_CACHE_PICTURE_MAX), decoded for the +// display mode each time. +// +// The file is read on first use. A change is saved SHELL_PROG_CACHE_SAVE_MS +// after the last, SHELL_PROG_CACHE_SAVE_CHUNK bytes at a time in the shell's +// idle time (shellProgCacheIdle), so no frame waits for the whole write; +// shellProgCacheShutdown saves what is left. The memory goes once the +// cache has gone SHELL_PROG_CACHE_DROP_MS unused. It is DVX's memory, +// whichever program's call made it. Nothing is saved when DVX cannot save +// (platformIsReadOnly). + +#ifndef SHELL_PROG_CACHE_H +#define SHELL_PROG_CACHE_H + +#include "shellProg.h" + +#include +#include + +#define SHELL_PROG_CACHE_NAME "PROGRAMS.CAC" +#define SHELL_PROG_CACHE_PICTURE_MAX 32768 // a picture file's bytes kept at most +#define SHELL_PROG_CACHE_SAVE_MS 3000 // a change is saved this long after the last +#define SHELL_PROG_CACHE_SAVE_CHUNK 4096 // bytes written per idle call +#define SHELL_PROG_CACHE_DROP_MS 60000 // the memory goes after this long unused + +// Check path against its file now (the program scan). +void shellProgCacheCheck(const char *path); + +// Free the cache without saving (a save under way is abandoned). +void shellProgCacheDrop(void); + +// The shell's idle time: a save under way goes on, a change old enough +// starts one, and a cache unused long enough is freed. +void shellProgCacheIdle(void *ctx); + +// path's name and description from the cache; false when it has none. +bool shellProgCacheInfo(const char *path, ShellProgramT *out); + +// path has no picture (the bytes kept for it were no image). +void shellProgCacheNoPicture(const char *path); + +// path's picture bytes in *outData (malloc'd, the caller frees; NULL when +// path has none), read and kept when the cache did not have them. False +// when the cache cannot say (no such file, or a picture file too big to +// keep): load the file itself. +bool shellProgCachePicture(const char *path, uint8_t **outData, uint32_t *outLen); + +// Keep path's name and description. +void shellProgCacheSetInfo(const char *path, const ShellProgramT *info); + +// Save what is left and free the cache (the shell's end). +void shellProgCacheShutdown(void); + +#endif // SHELL_PROG_CACHE_H diff --git a/src/libs/kpunch/dvxshell/shellRun.c b/src/libs/kpunch/dvxshell/shellRun.c index 2afca658..af201e69 100644 --- a/src/libs/kpunch/dvxshell/shellRun.c +++ b/src/libs/kpunch/dvxshell/shellRun.c @@ -62,21 +62,31 @@ typedef struct { int32_t framesLeft; } RunMinimizeT; -// A DOS program waiting for the main loop (shellRunDos) +// A DOS program waiting for the main loop (shellRunDos), and the program +// that waits for it to end (shellRunDosFor; 0 when none) typedef struct { - bool pending; - bool pause; - char program[DVX_MAX_PATH]; - char args[SHELL_ARGS_MAX]; - char folder[DVX_MAX_PATH]; + bool pending; + bool pause; + int32_t waiter; + char program[DVX_MAX_PATH]; + char args[SHELL_ARGS_MAX]; + char folder[DVX_MAX_PATH]; } RunDosT; +// How the run a program waits for ended (shellRunDosDone) +typedef struct { + int32_t waiter; // its app id; 0 when none waits + bool done; + int32_t rc; // the exit code; -1 when it did not run +} RunDosWaitT; + // ============================================================ // Module state // ============================================================ static RunMinimizeT *sMinimize = NULL; // stb_ds static RunDosT sDosRun; +static RunDosWaitT sDosWait; // ============================================================ // Prototypes @@ -87,12 +97,16 @@ static void minimizeEnded(int32_t appId); static void minimizeWindows(AppContextT *ctx, int32_t appId); static void runDlgBrowse(WidgetT *w); static bool runDlgOk(WindowT *dlg, void *userCtx); +static void runDosFinish(int32_t waiter, int32_t rc); +static void runDosRecord(int32_t waiter, const char *program, const char *args, const char *folder, bool pause); void shellMinimizeApp(AppContextT *ctx, int32_t appId); void shellMinimizeIdle(AppContextT *ctx); void shellResolveArgs(const char *folder, const char *args, char *out, int32_t outSize); int32_t shellRunCommand(AppContextT *ctx, const char *command); bool shellRunDialog(AppContextT *ctx, const char *const *history, int32_t historyCount, char *outCommand, int32_t outSize); void shellRunDos(const char *program, const char *args, const char *folder, bool pause); +bool shellRunDosDone(int32_t appId, int32_t *rc); +void shellRunDosFor(int32_t appId, const char *program, const char *args, const char *folder, bool pause); void shellRunDosRequest(AppContextT *ctx, int32_t keepAppId); int32_t shellRunProgram(AppContextT *ctx, const char *program, const char *args, const char *folder); bool shellSplitCommand(const char *command, char *program, int32_t programSize, char *args, int32_t argsSize); @@ -162,6 +176,37 @@ static bool runDlgOk(WindowT *dlg, void *userCtx) { } +// The run waiter waits for is over: rc its exit code (-1: it did not run). +static void runDosFinish(int32_t waiter, int32_t rc) { + if (waiter && sDosWait.waiter == waiter) { + sDosWait.done = true; + sDosWait.rc = rc; + } +} + + +// Record a run for the main loop. A run waiting already is replaced (its +// waiter, if any, is told it did not run). +static void runDosRecord(int32_t waiter, const char *program, const char *args, const char *folder, bool pause) { + if (sDosRun.pending) { + runDosFinish(sDosRun.waiter, -1); + } + + snprintf(sDosRun.program, sizeof(sDosRun.program), "%s", program); + snprintf(sDosRun.args, sizeof(sDosRun.args), "%s", args ? args : ""); + snprintf(sDosRun.folder, sizeof(sDosRun.folder), "%s", folder ? folder : ""); + sDosRun.pause = pause; + sDosRun.waiter = waiter; + sDosRun.pending = true; + + if (waiter) { + sDosWait.waiter = waiter; + sDosWait.done = false; + sDosWait.rc = -1; + } +} + + // A second call for the same program starts its frames again. void shellMinimizeApp(AppContextT *ctx, int32_t appId) { RunMinimizeT entry = { appId, SHELL_MINIMIZE_FRAMES }; @@ -272,27 +317,55 @@ bool shellRunDialog(AppContextT *ctx, const char *const *history, int32_t histor // is not safe from the caller's event handler); a request made while one // waits replaces it. void shellRunDos(const char *program, const char *args, const char *folder, bool pause) { - if (!program || !program[0]) { + if (program && program[0]) { + runDosRecord(0, program, args, folder, pause); + } +} + + +// A waiter's own run is over once shellRunDosRequest has carried it out +// or refused it; one replaced by another request is over too (not run). +bool shellRunDosDone(int32_t appId, int32_t *rc) { + if (sDosWait.waiter != appId) { + *rc = -1; + return true; + } + + if (!sDosWait.done) { + return false; + } + + *rc = sDosWait.rc; + sDosWait.waiter = 0; + return true; +} + + +// Nothing to run is over at once. +void shellRunDosFor(int32_t appId, const char *program, const char *args, const char *folder, bool pause) { + if (program && program[0]) { + runDosRecord(appId, program, args, folder, pause); return; } - snprintf(sDosRun.program, sizeof(sDosRun.program), "%s", program); - snprintf(sDosRun.args, sizeof(sDosRun.args), "%s", args ? args : ""); - snprintf(sDosRun.folder, sizeof(sDosRun.folder), "%s", folder ? folder : ""); - sDosRun.pause = pause; - sDosRun.pending = true; + sDosWait.waiter = appId; + sDosWait.done = true; + sDosWait.rc = -1; } // A program named with a folder must exist (a bare name is looked for on // the PATH by the command interpreter). The programs running besides the -// desktop are named and asked to close first; No, or one that keeps its -// window, cancels the run. +// desktop and the program waiting for the run are named and asked to +// close first; No, or one that keeps its window, cancels the run. The +// waiter is told how it ended (shellRunDosDone). void shellRunDosRequest(AppContextT *ctx, int32_t keepAppId) { char names[SHELL_MSG_MAX]; char msg[SHELL_MSG_MAX]; - int32_t pos = 0; - int32_t count = 0; + int32_t pos = 0; + int32_t count = 0; + int32_t *others = NULL; // stb_ds: their ids + int32_t rc = -1; struct stat st; if (!sDosRun.pending) { @@ -308,6 +381,7 @@ void shellRunDosRequest(AppContextT *ctx, int32_t keepAppId) { if (base != run.program && stat(run.program, &st) != 0) { snprintf(msg, sizeof(msg), RUN_NOT_FOUND, run.program); dvxErrorBox(ctx, RUN_DOS_TITLE, msg); + runDosFinish(run.waiter, rc); return; } @@ -316,28 +390,43 @@ void shellRunDosRequest(AppContextT *ctx, int32_t keepAppId) { const ShellAppT *app = shellGetApp(id); ShellProgramT info; - if (id == keepAppId || !app) { + if (id == keepAppId || id == run.waiter || !app) { continue; } shellProgramInfo(app->path, &info); pos = dvxStrAppendf(names, sizeof(names), pos, RUN_DOS_LINE, info.name[0] ? info.name : app->name); + arrput(others, id); count++; } - if (count > 0) { - snprintf(msg, sizeof(msg), RUN_DOS_ASK, base, names); + bool agreed = count == 0; - if (dvxMessageBox(ctx, RUN_DOS_TITLE, msg, MB_YESNO | MB_ICONQUESTION) != ID_YES || !shellCloseAllApps(ctx, keepAppId)) { - dvxLog("Shell: %s not run: programs kept running", run.program); - return; + if (!agreed) { + snprintf(msg, sizeof(msg), RUN_DOS_ASK, base, names); + agreed = dvxMessageBox(ctx, RUN_DOS_TITLE, msg, MB_YESNO | MB_ICONQUESTION) == ID_YES; + + for (int32_t i = 0; agreed && i < (int32_t)arrlen(others); i++) { + agreed = shellCloseApp(ctx, others[i]); } } - if (dvxRunDos(ctx, run.program, run.args, run.folder, run.pause) < 0) { + arrfree(others); + + if (!agreed) { + dvxLog("Shell: %s not run: programs kept running", run.program); + runDosFinish(run.waiter, rc); + return; + } + + rc = dvxRunDos(ctx, run.program, run.args, run.folder, run.pause); + + if (rc < 0) { snprintf(msg, sizeof(msg), RUN_DOS_FAILED, base, strerror(errno)); dvxErrorBox(ctx, RUN_DOS_TITLE, msg); } + + runDosFinish(run.waiter, rc); } diff --git a/src/libs/kpunch/dvxshell/shellRun.h b/src/libs/kpunch/dvxshell/shellRun.h index 3d567a2e..c0021190 100644 --- a/src/libs/kpunch/dvxshell/shellRun.h +++ b/src/libs/kpunch/dvxshell/shellRun.h @@ -88,6 +88,17 @@ int32_t shellRunProgram(AppContextT *ctx, const char *program, const char *args, // made while one waits replaces it. void shellRunDos(const char *program, const char *args, const char *folder, bool pause); +// Whether the run the program appId waits for (shellRunDosFor) is over; +// then *rc is its exit code, -1 when it did not run (the other programs +// were kept, it was not found, it could not start, or another request +// replaced it). +bool shellRunDosDone(int32_t appId, int32_t *rc); + +// shellRunDos for a program that waits for the run to end (BASIC's +// SHELL): appId is not asked to close, and shellRunDosDone says when the +// run is over and how it ended. +void shellRunDosFor(int32_t appId, const char *program, const char *args, const char *folder, bool pause); + // Main-loop step: carry out a waiting shellRunDos; keepAppId is the // desktop, which stays. Pumps the event loop: shell top level only. void shellRunDosRequest(AppContextT *ctx, int32_t keepAppId); diff --git a/src/libs/kpunch/fileops/README.md b/src/libs/kpunch/fileops/README.md new file mode 100644 index 00000000..26a38f0f --- /dev/null +++ b/src/libs/kpunch/fileops/README.md @@ -0,0 +1,26 @@ +# fileops -- File Operations + +Files and folders for DVX programs: drive and disk details, attributes +and dates, folder listings with a wildcard filter, sorted, copying, +moving, and deleting whole trees a step at a time with progress (so the +other programs keep running), name checks by DOS rules, and the path +helpers. The File Manager is built on it; BASIC reaches it through +src/include/basic/fileops.bas. + +## Documentation + +Full reference is in `fileops.dhs` (this directory), compiled into the +DVX System Reference. Open via the Help Viewer app, or read +`docs/dvx_system_reference.html` after `make`. + +## Source Files + +- `fileOps.h` -- public API +- `fileOps.c` -- listings, trees, attributes, names, paths +- `fileOpsSys.h`, `fileOpsSys.c` -- the system calls behind attributes + and dates +- `fileOpsBas.c` -- BASIC entry points (fileops.bas) + +## Build + +`make -C src/libs/kpunch/fileops` (invoked by the top-level `make`). diff --git a/src/libs/kpunch/fileops/fileOps.c b/src/libs/kpunch/fileops/fileOps.c index b09d5e12..85975ed7 100644 --- a/src/libs/kpunch/fileops/fileOps.c +++ b/src/libs/kpunch/fileops/fileOps.c @@ -71,6 +71,10 @@ #define NEW_DIR_MODE 0777 #define NO_PARENT (-1) +// Folder entries one scan step reads at most (a step stays short on a +// big folder) +#define SCAN_STEP_ENTRIES 32 + // fileOpsPathNormalize: the "C" of "C:\..." (upper case on every disk) #define DOS_DRIVE_LETTER_LEN 1 @@ -110,6 +114,8 @@ struct FileOpsJobS { FileOpsAnswerE answer; FileOpsItemT *items; // stb_ds array, breadth-first int32_t scanPos; // next item the scan looks at + int32_t scanBase; // items before scanPos's were read (a Retry drops the rest) + FileOpsSysDirT *scanDir; // the folder scanPos is, while it is being read int32_t pos; // item being processed in the current phase FILE *in; // file being copied FILE *out; @@ -119,11 +125,14 @@ struct FileOpsJobS { bool overwriteAll; // "All" given to an overwrite question bool readOnlyAll; // "All" given to a read-only question bool cancelled; + bool ran; // a step ran: files may have changed uint8_t clearAttr; // attribute bits cleared on every copied file char destDir[DVX_MAX_PATH]; char message[FILEOPS_MSG_MAX]; int32_t filesTotal; int32_t filesDone; + int32_t foldersTotal; + int32_t unread; // measure: items that could not be read uint64_t bytesTotal; uint64_t bytesDone; const char *current; // path being worked on (points into items) @@ -136,22 +145,6 @@ typedef struct { bool ok; // false: out of memory } ListScanT; -// scanFolder's scan: the job and the folder item being read. -typedef struct { - FileOpsJobT *job; - int32_t idx; - bool ok; -} FolderScanT; - -// treeSizeDir's scan: the folder and the totals. -typedef struct { - const char *dir; - uint64_t *bytes; - int32_t *files; - int32_t *folders; - bool ok; // false: something could not be read -} SizeScanT; - // ============================================================ // Prototypes // ============================================================ @@ -166,8 +159,10 @@ static const char *errorText(int32_t err); static void keepAncestors(FileOpsJobT *job, int32_t idx); static bool listScanEntry(const FileOpsSysEntryT *e, void *ctx); static int32_t percentOf(const FileOpsJobT *job); -static bool scanFolder(FileOpsJobT *job, int32_t idx); -static bool scanFolderEntry(const FileOpsSysEntryT *e, void *ctx); +static int32_t phasePos(const FileOpsJobT *job); +static void scanClose(FileOpsJobT *job); +static bool scanEntry(FileOpsJobT *job, int32_t idx, const FileOpsSysEntryT *e); +static bool scanStep(FileOpsJobT *job, int32_t idx, char *problem, int32_t problemSize); static void skipItem(FileOpsJobT *job, int32_t idx); static int32_t statItem(const char *path, struct stat *st); static FileOpsStepE stepRemove(FileOpsJobT *job); @@ -175,9 +170,6 @@ static FileOpsStepE stepScan(FileOpsJobT *job); static FileOpsStepE stepTransfer(FileOpsJobT *job); static FileOpsStepE stepTransferFile(FileOpsJobT *job, FileOpsItemT *it); static bool takeAnswer(FileOpsJobT *job, FileOpsQuestionE question, FileOpsAnswerE *answer); -static bool treeSizeDir(const char *dir, uint64_t *bytes, int32_t *files, int32_t *folders); -static bool treeSizeEntry(const FileOpsSysEntryT *e, void *ctx); -static bool treeSizeWalk(const char *path, uint64_t *bytes, int32_t *files, int32_t *folders); // Sort parameters for compareEntries (qsort has no context argument) static FileOpsSortE sSortCol = FileOpsSortNameE; @@ -234,6 +226,7 @@ static FileOpsStepE askUser(FileOpsJobT *job, FileOpsQuestionE question, FileOps static FileOpsStepE cancelJob(FileOpsJobT *job) { closeCopy(job, true); + scanClose(job); job->cancelled = true; job->question = FileOpsQuestionNoneE; job->phase = FileOpsPhaseDoneE; @@ -376,7 +369,7 @@ void fileOpsFormatSize(uint64_t bytes, char *out, int32_t outSize) { bool fileOpsJobAddSource(FileOpsJobT *job, const char *path) { char dst[DVX_MAX_PATH]; - if (job->op == FileOpsDeleteE) { + if (job->op == FileOpsDeleteE || job->op == FileOpsMeasureE) { return addItem(job, path, NULL, NO_PARENT); } @@ -424,12 +417,18 @@ FileOpsJobT *fileOpsJobCreate(FileOpsOpE op, const char *destDir) { } +// A copy, move or delete that ran may have changed what is installed. void fileOpsJobFree(FileOpsJobT *job) { if (!job) { return; } closeCopy(job, true); + scanClose(job); + + if (job->ran && job->op != FileOpsMeasureE) { + platformInstalledChanged(); + } for (int32_t i = 0; i < (int32_t)arrlen(job->items); i++) { free(job->items[i].src); @@ -453,12 +452,14 @@ FileOpsOpE fileOpsJobOp(const FileOpsJobT *job) { void fileOpsJobProgress(const FileOpsJobT *job, FileOpsProgressT *out) { - out->filesTotal = job->filesTotal; - out->filesDone = job->filesDone; - out->bytesTotal = job->bytesTotal; - out->bytesDone = job->bytesDone; - out->percent = percentOf(job); - out->current = job->current ? job->current : ""; + out->filesTotal = job->filesTotal; + out->filesDone = job->filesDone; + out->foldersTotal = job->foldersTotal; + out->unread = job->unread; + out->bytesTotal = job->bytesTotal; + out->bytesDone = job->bytesDone; + out->percent = percentOf(job); + out->current = job->current ? job->current : ""; } @@ -494,7 +495,9 @@ FileOpsStepE fileOpsJobStep(FileOpsJobT *job) { return job->cancelled ? FileOpsStepCancelledE : FileOpsStepDoneE; } - if (job->cancelled) { + // Cancel, also as the answer to a question, stops at once (the item is + // not tried again first) + if (job->cancelled || (job->answered && job->answer == FileOpsAnswerCancelE)) { return cancelJob(job); } @@ -502,7 +505,11 @@ FileOpsStepE fileOpsJobStep(FileOpsJobT *job) { return job->question == FileOpsQuestionErrorE ? FileOpsStepErrorE : FileOpsStepAskE; } - FileOpsStepE result = FileOpsStepBusyE; + FileOpsPhaseE phase = job->phase; + int32_t at = phasePos(job); + FileOpsStepE result = FileOpsStepBusyE; + + job->ran = true; switch (job->phase) { case FileOpsPhaseScanE: @@ -521,6 +528,13 @@ FileOpsStepE fileOpsJobStep(FileOpsJobT *job) { break; } + // An answer still waiting was for an item that went through when tried + // again; it is not kept for a later question + if (job->answered && (job->phase != phase || phasePos(job) != at)) { + job->answered = false; + job->question = FileOpsQuestionNoneE; + } + if (result == FileOpsStepBusyE && job->phase == FileOpsPhaseDoneE) { result = FileOpsStepDoneE; } @@ -654,11 +668,7 @@ bool fileOpsPathIsInside(const char *path, const char *dir) { bool fileOpsPathIsRoot(const char *path) { - if (platformPathIsSep(path[0]) && path[1] == '\0') { - return true; - } - - return path[0] && path[1] == ':' && (path[2] == '\0' || (platformPathIsSep(path[2]) && path[3] == '\0')); + return platformPathIsRoot(path); } @@ -697,39 +707,7 @@ bool fileOpsPathNormalize(const char *path, char *out, int32_t outSize) { bool fileOpsPathParent(const char *path, char *out, int32_t outSize) { - if (fileOpsPathIsRoot(path)) { - return false; - } - - 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 (platformPathIsSep(path[i])) { - sep = i; - } - } - - if (sep < 0) { - return false; - } - - // Keep the separator of a root ("/", "C:\"). - bool rootSep = sep == 0 || (sep == 2 && path[1] == ':'); - int32_t keep = rootSep ? sep + 1 : sep; - - if (keep >= outSize) { - return false; - } - - memcpy(out, path, (size_t)keep); - out[keep] = '\0'; - platformPathNative(out); - return true; + return platformPathParent(path, out, outSize); } @@ -772,12 +750,16 @@ bool fileOpsRename(const char *dir, const char *oldName, const char *newName, ch } +// A measure job run to its end (it asks nothing). bool fileOpsTreeSize(const char *path, uint64_t *bytes, int32_t *files, int32_t *folders) { - *bytes = 0; - *files = 0; - *folders = 0; + FileOpsJobT *job = fileOpsJobCreate(FileOpsMeasureE, NULL); + bool ok = job && fileOpsJobAddSource(job, path) && fileOpsJobRun(job, NULL) == FileOpsStepDoneE && job->unread == 0; - return treeSizeWalk(path, bytes, files, folders); + *bytes = job ? job->bytesTotal : 0; + *files = job ? job->filesTotal : 0; + *folders = job ? job->foldersTotal : 0; + fileOpsJobFree(job); + return ok; } @@ -863,40 +845,68 @@ static int32_t percentOf(const FileOpsJobT *job) { } -// Append the contents of folder item idx, in one pass over the folder. -// False when it cannot be read (or a path would be too long). -static bool scanFolder(FileOpsJobT *job, int32_t idx) { - FolderScanT scan; - - scan.job = job; - scan.idx = idx; - scan.ok = true; - - // The item array may move while appending, but the path string stays. - return fileOpsSysScan(job->items[idx].src, false, scanFolderEntry, &scan) && scan.ok; +// The item the job's phase is on. +static int32_t phasePos(const FileOpsJobT *job) { + return job->phase == FileOpsPhaseScanE ? job->scanPos : job->pos; } -// One entry of the folder scanFolder reads: an item under it. -static bool scanFolderEntry(const FileOpsSysEntryT *e, void *ctx) { - FolderScanT *scan = (FolderScanT *)ctx; - const FileOpsItemT *dirItem = &scan->job->items[scan->idx]; +// Stop reading the folder the scan is on, if any. +static void scanClose(FileOpsJobT *job) { + fileOpsSysClose(job->scanDir); + job->scanDir = NULL; +} + + +// One entry of folder item idx: an item under it, or for a measure a file +// counted (only folders need visiting). False (errno set) when its path +// would be too long or memory runs out. +static bool scanEntry(FileOpsJobT *job, int32_t idx, const FileOpsSysEntryT *e) { + const FileOpsItemT *dirItem = &job->items[idx]; char src[DVX_MAX_PATH]; char dst[DVX_MAX_PATH]; - bool fits = fileOpsPathJoin(src, sizeof(src), dirItem->src, e->name); + + if (job->op == FileOpsMeasureE && !e->isDir) { + job->filesTotal++; + job->bytesTotal += e->size; + return true; + } + + bool fits = fileOpsPathJoin(src, sizeof(src), dirItem->src, e->name); if (fits && dirItem->dst) { fits = fileOpsPathJoin(dst, sizeof(dst), dirItem->dst, e->name); } if (!fits) { - errno = ENAMETOOLONG; - scan->ok = false; + errno = ENAMETOOLONG; return false; } - scan->ok = appendItem(scan->job, src, dirItem->dst ? dst : NULL, scan->idx, e->isDir, e->size); - return scan->ok; + // The item array may move while appending, so dirItem is not used after + return appendItem(job, src, dirItem->dst ? dst : NULL, idx, e->isDir, e->size); +} + + +// Read up to SCAN_STEP_ENTRIES more entries of folder item idx (the open +// job->scanDir). True while there are more; false at the end (the folder +// closed) or with the reason in problem. +static bool scanStep(FileOpsJobT *job, int32_t idx, char *problem, int32_t problemSize) { + FileOpsSysEntryT e; + + for (int32_t n = 0; n < SCAN_STEP_ENTRIES; n++) { + if (!fileOpsSysNext(job->scanDir, &e)) { + scanClose(job); + return false; + } + + if (!scanEntry(job, idx, &e)) { + snprintf(problem, (size_t)problemSize, "Cannot read %s.\n%s", job->items[idx].src, errorText(errno)); + return false; + } + } + + return true; } @@ -928,7 +938,7 @@ static FileOpsStepE stepRemove(FileOpsJobT *job) { return FileOpsStepBusyE; } - int32_t idx = job->pos; + int32_t idx = job->pos; FileOpsItemT *it = &job->items[idx]; FileOpsAnswerE answer; @@ -958,9 +968,6 @@ static FileOpsStepE stepRemove(FileOpsJobT *job) { job->readOnlyAll = true; break; - case FileOpsAnswerCancelE: - return cancelJob(job); - default: it->approved = true; break; @@ -980,10 +987,6 @@ static FileOpsStepE stepRemove(FileOpsJobT *job) { return askUser(job, FileOpsQuestionErrorE, FileOpsStepErrorE, "Cannot delete %s.\n%s", it->src, errorText(e)); } - if (answer == FileOpsAnswerCancelE) { - return cancelJob(job); - } - if (answer == FileOpsAnswerRetryE) { return FileOpsStepBusyE; } @@ -1004,70 +1007,93 @@ static FileOpsStepE stepRemove(FileOpsJobT *job) { // Scan one item: check a top-level source, try a whole-item rename for a -// move, read a folder's contents, or count a file. +// move, read a piece of a folder's contents, or count a file or folder. +// A measure asks nothing: what cannot be read is skipped and counted. static FileOpsStepE stepScan(FileOpsJobT *job) { int32_t count = (int32_t)arrlen(job->items); if (job->scanPos >= count) { - bool isDelete = job->op == FileOpsDeleteE; + if (job->op == FileOpsMeasureE) { + job->phase = FileOpsPhaseDoneE; + } else if (job->op == FileOpsDeleteE) { + job->phase = FileOpsPhaseRemoveE; + job->pos = count - 1; + } else { + job->phase = FileOpsPhaseTransferE; + job->pos = 0; + } - job->phase = isDelete ? FileOpsPhaseRemoveE : FileOpsPhaseTransferE; - job->pos = isDelete ? count - 1 : 0; return FileOpsStepBusyE; } - int32_t idx = job->scanPos; - FileOpsItemT *it = &job->items[idx]; + int32_t idx = job->scanPos; + FileOpsItemT *it = &job->items[idx]; + bool more = false; FileOpsAnswerE answer; - char problem[FILEOPS_MSG_MAX]; + char problem[FILEOPS_MSG_MAX]; job->current = it->src; problem[0] = '\0'; - if (it->parent == NO_PARENT && it->dst) { - struct stat st; + if (job->scanDir) { + more = scanStep(job, idx, problem, sizeof(problem)); + } else { + job->scanBase = count; - if (fileOpsPathSame(it->src, it->dst)) { - if (job->op == FileOpsMoveE) { - // Moving an item to where it already is: nothing to do. - it->skipped = true; + if (it->parent == NO_PARENT && it->dst) { + struct stat st; + + if (fileOpsPathSame(it->src, it->dst)) { + if (job->op == FileOpsMoveE) { + // Moving an item to where it already is: nothing to do. + it->skipped = true; + job->scanPos++; + return FileOpsStepBusyE; + } + + snprintf(problem, sizeof(problem), "%s cannot be copied onto itself.", it->src); + } else if (it->isDir && fileOpsPathIsInside(it->dst, it->src)) { + snprintf(problem, sizeof(problem), "%s cannot be %s into itself.", it->src, job->op == FileOpsMoveE ? "moved" : "copied"); + } else if (job->op == FileOpsMoveE && stat(it->dst, &st) != 0 && rename(it->src, it->dst) == 0) { + it->moved = true; job->scanPos++; return FileOpsStepBusyE; } - - snprintf(problem, sizeof(problem), "%s cannot be copied onto itself.", it->src); - } else if (it->isDir && fileOpsPathIsInside(it->dst, it->src)) { - snprintf(problem, sizeof(problem), "%s cannot be %s into itself.", it->src, job->op == FileOpsMoveE ? "moved" : "copied"); - } else if (job->op == FileOpsMoveE && stat(it->dst, &st) != 0 && rename(it->src, it->dst) == 0) { - it->moved = true; - job->scanPos++; - return FileOpsStepBusyE; } - } - if (!problem[0] && it->isDir && !scanFolder(job, idx)) { - snprintf(problem, sizeof(problem), "Cannot read %s.\n%s", job->items[idx].src, errorText(errno)); + if (!problem[0] && it->isDir) { + job->scanDir = fileOpsSysOpen(it->src, false); + more = job->scanDir != NULL; + + if (!more) { + snprintf(problem, sizeof(problem), "Cannot read %s.\n%s", it->src, errorText(errno)); + } + } } if (problem[0]) { - if (!takeAnswer(job, FileOpsQuestionErrorE, &answer)) { - return askUser(job, FileOpsQuestionErrorE, FileOpsStepErrorE, "%s", problem); - } + scanClose(job); - if (answer == FileOpsAnswerCancelE) { - return cancelJob(job); - } - - if (answer == FileOpsAnswerRetryE) { - // Drop anything a partial folder read appended. - while ((int32_t)arrlen(job->items) > count) { + if (job->op == FileOpsMeasureE) { + // What was read stays counted; only a folder read fails here + job->unread++; + job->foldersTotal += job->items[idx].parent != NO_PARENT ? 1 : 0; + } else { + // Drop anything a partial folder read appended + while ((int32_t)arrlen(job->items) > job->scanBase) { FileOpsItemT last = arrpop(job->items); free(last.src); free(last.dst); } - return FileOpsStepBusyE; + if (!takeAnswer(job, FileOpsQuestionErrorE, &answer)) { + return askUser(job, FileOpsQuestionErrorE, FileOpsStepErrorE, "%s", problem); + } + + if (answer == FileOpsAnswerRetryE) { + return FileOpsStepBusyE; + } } skipItem(job, idx); @@ -1075,11 +1101,17 @@ static FileOpsStepE stepScan(FileOpsJobT *job) { return FileOpsStepBusyE; } + if (more) { + return FileOpsStepBusyE; + } + it = &job->items[idx]; if (!it->isDir) { job->filesTotal++; job->bytesTotal += it->size; + } else if (it->parent != NO_PARENT) { + job->foldersTotal++; } job->scanPos++; @@ -1095,7 +1127,7 @@ static FileOpsStepE stepTransfer(FileOpsJobT *job) { return FileOpsStepBusyE; } - int32_t idx = job->pos; + int32_t idx = job->pos; FileOpsItemT *it = &job->items[idx]; FileOpsAnswerE answer; @@ -1125,10 +1157,6 @@ static FileOpsStepE stepTransfer(FileOpsJobT *job) { return askUser(job, FileOpsQuestionErrorE, FileOpsStepErrorE, "Cannot create folder %s.\n%s", it->dst, errorText(e)); } - if (answer == FileOpsAnswerCancelE) { - return cancelJob(job); - } - if (answer == FileOpsAnswerRetryE) { return FileOpsStepBusyE; } @@ -1145,9 +1173,9 @@ static FileOpsStepE stepTransfer(FileOpsJobT *job) { // files), copy one chunk, or finish it (dates, attributes, and for a move // removing the source). static FileOpsStepE stepTransferFile(FileOpsJobT *job, FileOpsItemT *it) { - int32_t idx = job->pos; + int32_t idx = job->pos; FileOpsAnswerE answer; - char problem[FILEOPS_MSG_MAX]; + char problem[FILEOPS_MSG_MAX]; problem[0] = '\0'; @@ -1172,9 +1200,6 @@ static FileOpsStepE stepTransferFile(FileOpsJobT *job, FileOpsItemT *it) { job->overwriteAll = true; break; - case FileOpsAnswerCancelE: - return cancelJob(job); - default: it->approved = true; break; @@ -1269,10 +1294,6 @@ static FileOpsStepE stepTransferFile(FileOpsJobT *job, FileOpsItemT *it) { return askUser(job, FileOpsQuestionErrorE, FileOpsStepErrorE, "%s", problem); } - if (answer == FileOpsAnswerCancelE) { - return cancelJob(job); - } - if (answer == FileOpsAnswerRetryE) { return FileOpsStepBusyE; } @@ -1301,57 +1322,3 @@ static bool takeAnswer(FileOpsJobT *job, FileOpsQuestionE question, FileOpsAnswe job->question = FileOpsQuestionNoneE; return true; } - - -// Add everything in folder dir to the totals, one pass per folder. -static bool treeSizeDir(const char *dir, uint64_t *bytes, int32_t *files, int32_t *folders) { - SizeScanT scan; - - scan.dir = dir; - scan.bytes = bytes; - scan.files = files; - scan.folders = folders; - scan.ok = true; - - return fileOpsSysScan(dir, false, treeSizeEntry, &scan) && scan.ok; -} - - -// One entry of a folder treeSizeDir reads: a file's size, or a folder -// counted and walked. -static bool treeSizeEntry(const FileOpsSysEntryT *e, void *ctx) { - SizeScanT *scan = (SizeScanT *)ctx; - char child[DVX_MAX_PATH]; - - if (!e->isDir) { - *scan->bytes += e->size; - (*scan->files)++; - return true; - } - - (*scan->folders)++; - - if (!fileOpsPathJoin(child, sizeof(child), scan->dir, e->name) || !treeSizeDir(child, scan->bytes, scan->files, scan->folders)) { - scan->ok = false; - } - - return true; -} - - -// Add path (and, for a folder, everything below it) to the totals. -static bool treeSizeWalk(const char *path, uint64_t *bytes, int32_t *files, int32_t *folders) { - struct stat st; - - if (statItem(path, &st) != 0) { - return false; - } - - if (!S_ISDIR(st.st_mode)) { - *bytes += platformStatSize(&st); - (*files)++; - return true; - } - - return treeSizeDir(path, bytes, files, folders); -} diff --git a/src/libs/kpunch/fileops/fileOps.h b/src/libs/kpunch/fileops/fileOps.h index b50297d9..17c8c6d7 100644 --- a/src/libs/kpunch/fileops/fileOps.h +++ b/src/libs/kpunch/fileops/fileOps.h @@ -41,10 +41,11 @@ // Jobs recursive copy, move and delete of any number of // files and folders, with progress, Cancel, "file // exists" / "read-only" questions (Yes/No/All/Cancel) -// and error recovery (Retry/Skip/Cancel). +// and error recovery (Retry/Skip/Cancel); and measure +// (count files, folders and bytes, changing nothing). // // A job is advanced by fileOpsJobStep, one bounded piece of work per call -// (one folder read, one chunk of one file, one delete), so a cooperative +// (part of a folder read, one chunk of one file, one delete), so a cooperative // program can yield, repaint progress and honour Cancel between steps. // When a step needs the user it returns FileOpsStepAskE or // FileOpsStepErrorE; fileOpsJobMessage says what happened and the job @@ -52,7 +53,8 @@ // alternative: it runs a job to the end, calling back for progress and // questions. // -// A job works in phases: scan (read source folders, total the bytes), +// A job works in phases: scan (read source folders, total the bytes; all a +// measure does, asking nothing: a folder it cannot read is counted unread), // transfer (copy/move: create folders, copy files in chunks keeping their // dates and attributes; a move removes each source file once its copy is // complete, or renames a whole top-level item in one go when the @@ -110,7 +112,8 @@ typedef enum { typedef enum { FileOpsCopyE, FileOpsMoveE, - FileOpsDeleteE + FileOpsDeleteE, + FileOpsMeasureE // count files, folders and bytes; nothing changes } FileOpsOpE; typedef enum { @@ -134,6 +137,8 @@ typedef enum { typedef struct { int32_t filesTotal; // files found so far by the scan int32_t filesDone; + int32_t foldersTotal; // folders found so far inside the sources + int32_t unread; // measure: folders that could not be read (all of it or part) uint64_t bytesTotal; uint64_t bytesDone; int32_t percent; // 0..FILEOPS_PERCENT_FULL @@ -255,8 +260,8 @@ void fileOpsJobClearAttr(FileOpsJobT *job, uint8_t attr); // file) and returns FileOpsStepCancelledE. void fileOpsJobCancel(FileOpsJobT *job); -// Start a job. destDir is the target folder (ignored for delete). NULL -// when out of memory. +// Start a job. destDir is the target folder (ignored for delete and +// measure). NULL when out of memory. FileOpsJobT *fileOpsJobCreate(FileOpsOpE op, const char *destDir); // Free a job, closing any open files and removing a partial copy. @@ -324,9 +329,10 @@ int32_t FileOpsIsFolder(const char *path); // platformDirIsWritable. int32_t FileOpsIsWritable(const char *dir); -// Jobs: create (op 0 copy, 1 move, 2 delete), add sources, then run in -// time slices (FileOpsJobRunFor returns a FileOpsStepE), answering Ask / -// Error steps; progress as files done / total, percent and current path. +// Jobs: create (op 0 copy, 1 move, 2 delete, 3 measure), add sources, +// then run in time slices (FileOpsJobRunFor returns a FileOpsStepE), +// answering Ask / Error steps; progress as files done / total, folders +// inside the sources, unread folders (measure), percent and current path. int32_t FileOpsJobAddSource(int32_t job, const char *path); void FileOpsJobAnswer(int32_t job, int32_t answer); double FileOpsJobBytesDone(int32_t job); @@ -337,10 +343,12 @@ int32_t FileOpsJobCreate(int32_t op, const char *destDir); const char *FileOpsJobCurrent(int32_t job); int32_t FileOpsJobFilesDone(int32_t job); int32_t FileOpsJobFilesTotal(int32_t job); +int32_t FileOpsJobFoldersTotal(int32_t job); void FileOpsJobFree(int32_t job); const char *FileOpsJobMessage(int32_t job); int32_t FileOpsJobPercent(int32_t job); int32_t FileOpsJobRunFor(int32_t job, int32_t milliseconds); +int32_t FileOpsJobUnread(int32_t job); // The DVX folders of a kind of file (dvxPlat.h DvxKindE, BASIC // FILEOPS_KIND_*): platformKindBrowseDir, platformKindDir, @@ -354,10 +362,12 @@ const char *FileOpsKindName(int32_t kind, const char *path); // Listings: read a folder (sorted by name, folders first), sort it, read // its entries, or all of them as ListView SetRows text (name, size, type, -// date per row). +// date per row); FileOpsListFolders copies a listing's folders alone, +// sorted by name, without reading the disk. int32_t FileOpsList(const char *dir, const char *filter); int32_t FileOpsListCount(int32_t list); int32_t FileOpsListFind(int32_t list, const char *name); +int32_t FileOpsListFolders(int32_t list); void FileOpsListFree(int32_t list); int32_t FileOpsListIsFolder(int32_t list, int32_t index); const char *FileOpsListName(int32_t list, int32_t index); diff --git a/src/libs/kpunch/fileops/fileOpsBas.c b/src/libs/kpunch/fileops/fileOpsBas.c index bad36a18..48bb2a83 100644 --- a/src/libs/kpunch/fileops/fileOpsBas.c +++ b/src/libs/kpunch/fileops/fileOpsBas.c @@ -35,6 +35,7 @@ // and removes a partly copied one. #include "fileOps.h" +#include "dvxApp.h" #include "dvxPlat.h" #include "dvxTypes.h" #include "stb_ds_wrap.h" @@ -54,10 +55,14 @@ #define LIST_NOT_FOUND (-1) #define BYTES_PER_KB 1024 #define DRIVE_TEXT_MAX 2 // a drive letter and its NUL +#define TREE_PUMP_MS 50 // a whole-tree call lets DVX go on this often typedef struct { - bool yesToAll; // replace existing / delete read-only files - int32_t problems; // errors that skipped an item + bool yesToAll; // replace existing / delete read-only files + bool pumped; // dvxWaitPump has let DVX go on + bool noPump; // dvxWaitPump said stop: no more + int32_t problems; // errors that skipped an item + PlatformTicksT pumpAt; // when DVX last went on (once pumped) } BasRunT; // One handle: a listing or a job. @@ -72,9 +77,9 @@ typedef struct { // ============================================================ static char sError[FILEOPS_MSG_MAX]; // why the last MakeDir / Rename / ReplaceFile / Valid* failed -static uint64_t sTreeBytes = 0; // the last FileOpsTreeSize -static int32_t sTreeFiles = 0; -static int32_t sTreeFolders = 0; +static uint64_t sTreeBytes = 0; // the last FileOpsTreeSize +static int32_t sTreeFiles = 0; +static int32_t sTreeFolders = 0; // ============================================================ // Prototypes @@ -90,7 +95,9 @@ static bool jobProgress(int32_t job, FileOpsProgressT *p); static const FileOpsEntryT *listEntry(int32_t list, int32_t index); static BasHandleT *listOf(int32_t list); static int32_t nameResult(const char *problem); +static bool runJob(FileOpsJobT *job, const char *src, bool yesToAll, FileOpsProgressT *out); static int32_t runTree(FileOpsOpE op, const char *src, const char *destDir, bool yesToAll); +static bool treeProgress(FileOpsJobT *job, void *ctx); // The listings and jobs, and the last string result static BasHandlesT sHandles = BAS_HANDLES(handleFree, 0); @@ -303,10 +310,10 @@ void FileOpsJobClearAttr(int32_t job, int32_t attr) { } -// A copy (0), move (1) or delete (2) into destDir; 0 for another op or -// when out of memory. +// A copy (0), move (1) or delete (2) into destDir, or a measure (3); 0 +// for another op or when out of memory. int32_t FileOpsJobCreate(int32_t op, const char *destDir) { - if (op < FileOpsCopyE || op > FileOpsDeleteE) { + if (op < FileOpsCopyE || op > FileOpsMeasureE) { return BAS_NO_HANDLE; } @@ -349,6 +356,14 @@ int32_t FileOpsJobFilesTotal(int32_t job) { } +int32_t FileOpsJobFoldersTotal(int32_t job) { + FileOpsProgressT p; + + jobProgress(job, &p); + return p.foldersTotal; +} + + void FileOpsJobFree(int32_t job) { if (jobOf(job)) { basHandleFree(&sHandles, job); @@ -392,6 +407,14 @@ int32_t FileOpsJobRunFor(int32_t job, int32_t milliseconds) { } +int32_t FileOpsJobUnread(int32_t job) { + FileOpsProgressT p; + + jobProgress(job, &p); + return p.unread; +} + + // The folder a file dialog for a kind starts in: the user's when it // exists, else SHARED's; "" for a kind out of range. const char *FileOpsKindBrowseDir(int32_t kind) { @@ -513,6 +536,47 @@ int32_t FileOpsListFind(int32_t list, const char *name) { } +// list's folders alone, sorted by name, as a new listing (list stays as +// it is); 0 when list is not a listing or memory runs out. +int32_t FileOpsListFolders(int32_t list) { + BasHandleT *hd = listOf(list); + FileOpsEntryT *entries = NULL; + + if (!hd) { + return BAS_NO_HANDLE; + } + + for (int32_t i = 0; i < hd->count; i++) { + FileOpsEntryT e = hd->entries[i]; + + if (!e.isDir) { + continue; + } + + e.name = strdup(e.name); + + if (!e.name) { + fileOpsListFree(entries, (int32_t)arrlen(entries)); + return BAS_NO_HANDLE; + } + + arrput(entries, e); + } + + int32_t count = (int32_t)arrlen(entries); + + fileOpsListSort(entries, count, FileOpsSortNameE, false); + + int32_t handle = handleNew(entries, count, NULL); + + if (handle == BAS_NO_HANDLE) { + fileOpsListFree(entries, count); + } + + return handle; +} + + void FileOpsListFree(int32_t list) { if (listOf(list)) { basHandleFree(&sHandles, list); @@ -774,9 +838,16 @@ int32_t FileOpsTreeFolders(void) { // Totals of a file, or of a folder and everything in it (not counting the // folder itself), for FileOpsTreeBytes / Files / Folders. True when every -// part could be read. +// part could be read. A measure job, as fileOpsTreeSize, but letting DVX +// go on as it works (runJob). int32_t FileOpsTreeSize(const char *path) { - return fileOpsTreeSize(path, &sTreeBytes, &sTreeFiles, &sTreeFolders) ? BAS_TRUE : BAS_FALSE; + FileOpsProgressT p; + bool ok = runJob(fileOpsJobCreate(FileOpsMeasureE, NULL), path, false, &p) && p.unread == 0; + + sTreeBytes = p.bytesTotal; + sTreeFiles = p.filesTotal; + sTreeFolders = p.foldersTotal; + return ok ? BAS_TRUE : BAS_FALSE; } @@ -908,18 +979,67 @@ static int32_t nameResult(const char *problem) { } -// Run one job over one source; True only when nothing failed. -static int32_t runTree(FileOpsOpE op, const char *src, const char *destDir, bool yesToAll) { - FileOpsJobT *job = fileOpsJobCreate(op, destDir); +// Run job (NULL when out of memory) over source src to its end and free +// it; true only when it finished with nothing skipped. out (unless NULL) +// gets its totals (current is ""). The job is a handle while it runs, so +// it is freed if the program is ended during a pump (treeProgress). +static bool runJob(FileOpsJobT *job, const char *src, bool yesToAll, FileOpsProgressT *out) { + int32_t handle = job ? handleNew(NULL, 0, job) : BAS_NO_HANDLE; - if (!job) { - return BAS_FALSE; + if (out) { + memset(out, 0, sizeof(*out)); + out->current = ""; } - BasRunT run = { yesToAll, 0 }; - FileOpsCallbacksT cb = { NULL, basAnswer, &run }; - bool ok = fileOpsJobAddSource(job, src) && fileOpsJobRun(job, &cb) == FileOpsStepDoneE && run.problems == 0; + if (handle == BAS_NO_HANDLE) { + fileOpsJobFree(job); + return false; + } - fileOpsJobFree(job); - return ok ? BAS_TRUE : BAS_FALSE; + BasRunT run; + + memset(&run, 0, sizeof(run)); + run.yesToAll = yesToAll; + + FileOpsCallbacksT cb = { treeProgress, basAnswer, &run }; + bool ok = fileOpsJobAddSource(job, src) && fileOpsJobRun(job, &cb) == FileOpsStepDoneE && run.problems == 0; + + if (out) { + fileOpsJobProgress(job, out); + out->current = ""; + } + + basHandleFree(&sHandles, handle); + return ok; +} + + +// A whole-tree copy, move or delete of src. +static int32_t runTree(FileOpsOpE op, const char *src, const char *destDir, bool yesToAll) { + return runJob(fileOpsJobCreate(op, destDir), src, yesToAll, NULL) ? BAS_TRUE : BAS_FALSE; +} + + +// A whole-tree call's progress: at the first step and then every +// TREE_PUMP_MS DVX and the program's events go on (dvxWaitPump, as +// DoEvents). When the first pump says stop (code that may not wait) the +// job runs to its end without pumps; when a later one does, the program +// is ending and the job is cancelled. +static bool treeProgress(FileOpsJobT *job, void *ctx) { + BasRunT *run = (BasRunT *)ctx; + + (void)job; + + if (run->noPump || (run->pumped && platformClock() - run->pumpAt < PLATFORM_MS_TO_TICKS(TREE_PUMP_MS))) { + return true; + } + + if (dvxWaitPump()) { + run->pumped = true; + run->pumpAt = platformClock(); + return true; + } + + run->noPump = true; + return !run->pumped; } diff --git a/src/libs/kpunch/fileops/fileOpsSys.c b/src/libs/kpunch/fileops/fileOpsSys.c index 25360f62..854d02ff 100644 --- a/src/libs/kpunch/fileops/fileOpsSys.c +++ b/src/libs/kpunch/fileops/fileOpsSys.c @@ -40,7 +40,9 @@ #include "fileOpsSys.h" #include "dvxPlat.h" +#include #include +#include #include #include @@ -82,6 +84,18 @@ #define DOS_TIME_HOUR_MASK 0x1F #define DOS_TIME_MIN_MASK 0x3F #define DOS_TIME_SEC_MASK 0x1F +#define MINUTES_PER_HOUR 60 +#define SECONDS_PER_MINUTE 60 + +// dosTime's cache: DOS_TIME_CACHE_SLOTS hours, direct mapped by a +// Fibonacci hash of the key (date in the high half, the hour's bits of the +// time in the low, DOS_TIME_KEY_USED set: an empty slot is 0) +#define DOS_TIME_CACHE_BITS 5 +#define DOS_TIME_CACHE_SLOTS (1 << DOS_TIME_CACHE_BITS) +#define DOS_TIME_HASH_MUL 2654435761u +#define DOS_TIME_HASH_BITS 32 // the width of the hash (uint32_t) +#define DOS_TIME_KEY_DATE_SHIFT 16 +#define DOS_TIME_KEY_USED 1u // Find first / find next: every file and folder (not the volume label), // hidden and system ones too, as readdir lists them @@ -99,6 +113,35 @@ #endif +// ============================================================ +// Types +// ============================================================ + +#ifdef __DJGPP__ +// The hours dosTime has converted during one scan: the time of the start +// of each. A folder's entries mostly share a few dates, and mktime, which +// searches for the time it is given, costs far more than reading an entry. +typedef struct { + uint32_t key[DOS_TIME_CACHE_SLOTS]; + time_t start[DOS_TIME_CACHE_SLOTS]; +} DosTimeCacheT; +#endif + +// A folder being read (fileOpsSysOpen). +struct FileOpsSysDirS { +#ifdef __DJGPP__ + struct ffblk ff; // the search, and the entry it is on + DosTimeCacheT times; + int rc; // the last find first / find next: 0 = ff holds an entry + bool taken; // ff's entry was reported: find the next one first + bool lower; // 8.3 names in lower case (FNCASE not set) +#else + DIR *dir; + char path[DVX_MAX_PATH]; + bool followLinks; +#endif +}; + // ============================================================ // Prototypes // ============================================================ @@ -106,7 +149,8 @@ #ifdef __DJGPP__ static void attrCall(const char *path, bool set, __dpmi_regs *r); static bool dosInt(uint32_t intNo, __dpmi_regs *r); -static time_t dosTime(uint16_t date, uint16_t time); +static time_t dosMktime(uint16_t date, int32_t hour, int32_t minute, int32_t second); +static time_t dosTime(DosTimeCacheT *cache, uint16_t date, uint16_t time); static bool putPath(const char *path, __dpmi_regs *r); #endif @@ -148,21 +192,51 @@ static bool dosInt(uint32_t intNo, __dpmi_regs *r) { #ifdef __DJGPP__ -// A DOS packed date and time as local time, exactly as the C library's -// stat converts them (mktime decides daylight saving time). -static time_t dosTime(uint16_t date, uint16_t time) { +// A DOS date with this hour, minute and second as local time, as the C +// library's stat converts them: mktime normalizes fields out of range and +// decides daylight saving time. +static time_t dosMktime(uint16_t date, int32_t hour, int32_t minute, int32_t second) { struct tm tm; memset(&tm, 0, sizeof(tm)); tm.tm_isdst = -1; - tm.tm_sec = (time & DOS_TIME_SEC_MASK) * DOS_TIME_SEC_DIVISOR; - tm.tm_min = (time >> DOS_TIME_MIN_SHIFT) & DOS_TIME_MIN_MASK; - tm.tm_hour = (time >> DOS_TIME_HOUR_SHIFT) & DOS_TIME_HOUR_MASK; + tm.tm_sec = second; + tm.tm_min = minute; + tm.tm_hour = hour; tm.tm_mday = date & DOS_DATE_DAY_MASK; tm.tm_mon = ((date >> DOS_DATE_MONTH_SHIFT) & DOS_DATE_MONTH_MASK) - 1; tm.tm_year = (date >> DOS_DATE_YEAR_SHIFT) + DOS_YEAR_BASE - TM_YEAR_BASE; return mktime(&tm); } + + +// A DOS packed date and time as local time (dosMktime): the start of its +// hour, from cache or converted once and kept there, plus the minutes and +// seconds. Clocks change on the hour, so that is mktime's answer but in a +// zone that moves them by half an hour; a minute or second out of range +// (which may fall in the next hour) is converted whole. +static time_t dosTime(DosTimeCacheT *cache, uint16_t date, uint16_t time) { + int32_t hour = (time >> DOS_TIME_HOUR_SHIFT) & DOS_TIME_HOUR_MASK; + int32_t minute = (time >> DOS_TIME_MIN_SHIFT) & DOS_TIME_MIN_MASK; + int32_t second = (time & DOS_TIME_SEC_MASK) * DOS_TIME_SEC_DIVISOR; + uint32_t key = ((uint32_t)date << DOS_TIME_KEY_DATE_SHIFT) | ((uint32_t)hour << DOS_TIME_HOUR_SHIFT) | DOS_TIME_KEY_USED; + uint32_t slot = (key * DOS_TIME_HASH_MUL) >> (DOS_TIME_HASH_BITS - DOS_TIME_CACHE_BITS); + + if (minute >= MINUTES_PER_HOUR || second >= SECONDS_PER_MINUTE) { + return dosMktime(date, hour, minute, second); + } + + if (cache->key[slot] != key) { + cache->key[slot] = key; + cache->start[slot] = dosMktime(date, hour, 0, 0); + } + + if (cache->start[slot] == (time_t)-1) { + return (time_t)-1; + } + + return cache->start[slot] + minute * SECONDS_PER_MINUTE + second; +} #endif @@ -279,42 +353,49 @@ bool fileOpsSetTime(const char *path, time_t mtime) { } -bool fileOpsSysScan(const char *dir, bool followLinks, FileOpsScanFnT fn, void *ctx) { - bool more = true; +// A search not run to its end is run to it, so the C library closes a +// long-name search handle (it has no call that closes one early). +void fileOpsSysClose(FileOpsSysDirT *d) { + if (!d) { + return; + } #ifdef __DJGPP__ - char pattern[DVX_MAX_PATH]; - struct ffblk ff; - bool lower = !_preserve_fncase(); - - (void)followLinks; - - if (!fileOpsPathJoin(pattern, sizeof(pattern), dir, DOS_FIND_ALL)) { - return false; + while (d->rc == 0) { + d->rc = findnext(&d->ff); } +#else + closedir(d->dir); +#endif - int rc = findfirst(pattern, &ff, DOS_FIND_ATTR); + free(d); +} - // Nothing found: an empty root folder, or no folder at all - if (rc != 0) { - struct stat st; - return stat(dir, &st) == 0 && S_ISDIR(st.st_mode); - } +bool fileOpsSysNext(FileOpsSysDirT *d, FileOpsSysEntryT *e) { +#ifdef __DJGPP__ + for (;;) { + if (d->taken) { + d->rc = findnext(&d->ff); + d->taken = false; + } - // The search runs to its end even after the callback said stop, so a - // long-name search handle is always closed by the library. - for (; rc == 0; rc = findnext(&ff)) { - char *name = ff.ff_name; + if (d->rc != 0) { + return false; + } + + char *name = d->ff.ff_name; char shortName[DOS_SHORT_NAME_MAX]; - if (!more || strcmp(name, ".") == 0 || strcmp(name, "..") == 0) { + d->taken = true; + + if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) { continue; } // readdir's rule: a name that is its own short (8.3) name is shown // in lower case - if (lower && strcmp(_lfn_gen_short_fname(name, shortName), name) == 0) { + if (d->lower && strcmp(_lfn_gen_short_fname(name, shortName), name) == 0) { for (char *c = name; *c; c++) { if (*c >= 'A' && *c <= 'Z') { *c = (char)(*c - 'A' + 'a'); @@ -322,26 +403,16 @@ bool fileOpsSysScan(const char *dir, bool followLinks, FileOpsScanFnT fn, void * } } - FileOpsSysEntryT e; - - e.name = name; - e.isDir = (ff.ff_attrib & FA_DIREC) != 0; - e.size = e.isDir ? 0 : (uint32_t)ff.ff_fsize; // DOS sizes are unsigned (up to 4 GB - 1) - e.mtime = dosTime(ff.ff_fdate, ff.ff_ftime); - more = fn(&e, ctx); + e->name = name; + e->isDir = (d->ff.ff_attrib & FA_DIREC) != 0; + e->size = e->isDir ? 0 : (uint32_t)d->ff.ff_fsize; // DOS sizes are unsigned (up to 4 GB - 1) + e->mtime = dosTime(&d->times, d->ff.ff_fdate, d->ff.ff_ftime); + return true; } - - return true; #else - DIR *d = opendir(dir); - - if (!d) { - return false; - } - struct dirent *ent; - while (more && (ent = readdir(d)) != NULL) { + while ((ent = readdir(d->dir)) != NULL) { char path[DVX_MAX_PATH]; struct stat st; @@ -349,25 +420,103 @@ bool fileOpsSysScan(const char *dir, bool followLinks, FileOpsScanFnT fn, void * continue; } - if (!fileOpsPathJoin(path, sizeof(path), dir, ent->d_name) || (followLinks ? stat(path, &st) : lstat(path, &st)) != 0) { + if (!fileOpsPathJoin(path, sizeof(path), d->path, ent->d_name) || (d->followLinks ? stat(path, &st) : lstat(path, &st)) != 0) { continue; } - FileOpsSysEntryT e; - - e.name = ent->d_name; - e.isDir = S_ISDIR(st.st_mode); - e.size = e.isDir ? 0 : platformStatSize(&st); - e.mtime = st.st_mtime; - more = fn(&e, ctx); + e->name = ent->d_name; + e->isDir = S_ISDIR(st.st_mode); + e->size = e->isDir ? 0 : platformStatSize(&st); + e->mtime = st.st_mtime; + return true; } - closedir(d); - return true; + return false; #endif } +FileOpsSysDirT *fileOpsSysOpen(const char *dir, bool followLinks) { + FileOpsSysDirT *d = (FileOpsSysDirT *)calloc(1, sizeof(FileOpsSysDirT)); + + if (!d) { + errno = ENOMEM; + return NULL; + } + +#ifdef __DJGPP__ + char pattern[DVX_MAX_PATH]; + + (void)followLinks; + d->lower = !_preserve_fncase(); + + if (!fileOpsPathJoin(pattern, sizeof(pattern), dir, DOS_FIND_ALL)) { + free(d); + errno = ENAMETOOLONG; + return NULL; + } + + d->rc = findfirst(pattern, &d->ff, DOS_FIND_ATTR); + + // Nothing found: an empty root folder, or no folder at all + if (d->rc != 0) { + struct stat st; + int32_t err = ENOTDIR; + + if (stat(dir, &st) != 0) { + err = errno; + } else if (S_ISDIR(st.st_mode)) { + err = 0; + } + + if (err != 0) { + free(d); + errno = err; + return NULL; + } + } +#else + if (snprintf(d->path, sizeof(d->path), "%s", dir) >= (int)sizeof(d->path)) { + free(d); + errno = ENAMETOOLONG; + return NULL; + } + + d->followLinks = followLinks; + d->dir = opendir(dir); + + if (!d->dir) { + int32_t err = errno; + + free(d); + errno = err; + return NULL; + } +#endif + + return d; +} + + +bool fileOpsSysScan(const char *dir, bool followLinks, FileOpsScanFnT fn, void *ctx) { + FileOpsSysDirT *d = fileOpsSysOpen(dir, followLinks); + FileOpsSysEntryT e; + + if (!d) { + return false; + } + + while (fileOpsSysNext(d, &e)) { + if (!fn(&e, ctx)) { + break; + } + } + + fileOpsSysClose(d); + return true; +} + + #ifdef __DJGPP__ // Copy a NUL-terminated path into the transfer buffer and point DS:DX // at it, in the form DOS takes (platformPathTidy: the C library is not in diff --git a/src/libs/kpunch/fileops/fileOpsSys.h b/src/libs/kpunch/fileops/fileOpsSys.h index 5d89b619..8863be13 100644 --- a/src/libs/kpunch/fileops/fileOpsSys.h +++ b/src/libs/kpunch/fileops/fileOpsSys.h @@ -31,17 +31,31 @@ #include #include -// One folder entry as fileOpsSysScan reports it. +// One folder entry as fileOpsSysScan and fileOpsSysNext report it. typedef struct { - const char *name; // leaf name, valid during the callback + const char *name; // leaf name: valid during the callback, or until the next fileOpsSysNext uint64_t size; // bytes (0 for a folder) time_t mtime; // last modification bool isDir; } FileOpsSysEntryT; +// A folder being read a piece at a time (fileOpsSysOpen). +typedef struct FileOpsSysDirS FileOpsSysDirT; + // Called for each entry; false stops the scan (no more calls). typedef bool (*FileOpsScanFnT)(const FileOpsSysEntryT *e, void *ctx); +// Stop reading a folder and free d (NULL is ignored). On DOS a search +// not at its end is first run to it, which costs the rest of the folder. +void fileOpsSysClose(FileOpsSysDirT *d); + +// The next entry of d into e; false at the end. +bool fileOpsSysNext(FileOpsSysDirT *d, FileOpsSysEntryT *e); + +// Start reading folder dir for fileOpsSysNext, which reports what +// fileOpsSysScan does. NULL (errno set) when it cannot be read. +FileOpsSysDirT *fileOpsSysOpen(const char *dir, bool followLinks); + // Every entry of dir except "." and ".." (hidden ones too) with its size, // date and kind. DOS reads them in one pass of the folder (find first / // find next, which report all of that), not one stat per entry -- a stat @@ -49,7 +63,7 @@ typedef bool (*FileOpsScanFnT)(const FileOpsSysEntryT *e, void *ctx); // names come back as readdir gives them (8.3 names in lower case unless // FNCASE is set). Elsewhere readdir and stat (lstat unless followLinks: // a link is then an entry of its own); an entry that cannot be read -// there is left out. False when dir cannot be read. -bool fileOpsSysScan(const char *dir, bool followLinks, FileOpsScanFnT fn, void *ctx); +// there is left out. False (errno set) when dir cannot be read. +bool fileOpsSysScan(const char *dir, bool followLinks, FileOpsScanFnT fn, void *ctx); #endif // FILE_OPS_SYS_H diff --git a/src/libs/kpunch/fileops/fileops.dhs b/src/libs/kpunch/fileops/fileops.dhs index 1afc00ba..15491132 100644 --- a/src/libs/kpunch/fileops/fileops.dhs +++ b/src/libs/kpunch/fileops/fileops.dhs @@ -72,7 +72,7 @@ Disks past 2 GB report their real size and free space: platformDiskSpace asks DO fileOpsFormatSize(bytes, out, size) "1,234,567". fileOpsFormatDate(t, out, size) "YYYY-MM-DD HH:MM" in local time. fileOpsParseDate(text, &t) The time fileOpsFormatDate's text stands for ("YYYY-MM-DD HH:MM", optionally ":SS"), local time; false for anything else, a date that does not exist included. - fileOpsTreeSize(path, &bytes, &files, &folders) Total size of a file or of a folder and everything in it. + fileOpsTreeSize(path, &bytes, &files, &folders) Total size of a file or of a folder and everything in it (the folders inside it; a measure job run to its end). False when part of it cannot be read; the totals then cover what was read. 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. @@ -86,7 +86,7 @@ Disks past 2 GB report their real size and free space: platformDiskSpace asks DO .h2 Jobs -A job copies, moves, or deletes any number of files and folders (recursively): +A job copies, moves, deletes, or measures (FileOpsMeasureE: counts the files, folders, and bytes, changing nothing) any number of files and folders (recursively): .code FileOpsJobT *job = fileOpsJobCreate(FileOpsCopyE, "C:\\BACKUP"); @@ -114,7 +114,7 @@ for (;;) { fileOpsJobFree(job); .endcode -Each fileOpsJobStep does one bounded piece of work: read one folder, copy one FILEOPS_COPY_CHUNK of one file, or delete one item. A cooperative program yields between steps. +Each fileOpsJobStep does one bounded piece of work: read a few dozen entries of a folder (a big folder takes several steps), copy one FILEOPS_COPY_CHUNK of one file, or delete one item. A cooperative program yields between steps. .table Step result Meaning @@ -126,10 +126,11 @@ Each fileOpsJobStep does one bounded piece of work: read one folder, copy one FI FileOpsStepCancelledE Stopped by Cancel. .endtable -Until it is answered, the job keeps returning the same question. What the job does: +Until it is answered, the job keeps returning the same question. The next step tries the item again and applies the answer if it fails again; an item that goes through leaves the answer unused (it is never kept for a later question). Cancel stops at once, without trying again. What the job does: .list -.item Scan: each folder is read once and its files are counted, so fileOpsJobProgress (FileOpsProgressT: files and bytes done and total, percent, current path) is accurate while the transfer runs. +.item Scan: each folder is read once and its files are counted, so fileOpsJobProgress (FileOpsProgressT: files and bytes done and total, folders inside the sources, percent, current path) is accurate while the transfer runs. A folder that fails part way keeps nothing of what was read before the question. +.item Measure: only the scan, and it asks nothing: a folder it cannot read is skipped and counted in FileOpsProgressT.unread (what was read of it stays counted). .item Copy: folders are created before their contents, files are copied in chunks and keep their date and attributes (plus Archive). A read-only destination is made writable before it is replaced. .item Move: a top-level item whose destination does not exist yet is renamed in one go. Otherwise each file is copied and then its source removed, and the emptied source folders are removed at the end. A folder that still holds skipped items is kept. .item Delete: files first, deepest first, then folders; read-only files ask first. @@ -143,6 +144,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)), DVX's folders (FileOpsUserDir$ (platformUserDir), FileOpsUserPath$ (platformUserPath, "" when it does not fit), FileOpsUserInTemp (platformUserInTemp), and for the kinds of DvxKindE (CONSTs FILEOPS_KIND_*) FileOpsKindDir$ (platformKindDir), FileOpsKindBrowseDir$ (platformKindBrowseDir), FileOpsKindFind$ (platformKindFind), FileOpsKindName$ (platformKindNameOf), and FileOpsKindList (platformKindReadDir as a listing, sorted by name; "" or 0 for a kind out of range)), 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. +src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree: a job run to its end, letting DVX and the program's events go on through dvxWaitPump at the first step and every 50 ms after; when the first pump says stop the job runs on without pumps, when a later one does the program is ending and the call is cancelled), 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)), DVX's folders (FileOpsUserDir$ (platformUserDir), FileOpsUserPath$ (platformUserPath, "" when it does not fit), FileOpsUserInTemp (platformUserInTemp), and for the kinds of DvxKindE (CONSTs FILEOPS_KIND_*) FileOpsKindDir$ (platformKindDir), FileOpsKindBrowseDir$ (platformKindBrowseDir), FileOpsKindFind$ (platformKindFind), FileOpsKindName$ (platformKindNameOf), and FileOpsKindList (platformKindReadDir as a listing, sorted by name; "" or 0 for a kind out of range)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListFolders (a new listing of a listing's folders, by name, from memory), 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 (a measure job pumping as the whole-tree calls do) with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$), and jobs (FileOpsJobCreate (FILEOPS_COPY, FILEOPS_MOVE, FILEOPS_DELETE, or FILEOPS_MEASURE), FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobFoldersTotal, FileOpsJobUnread, 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/game/README.md b/src/libs/kpunch/game/README.md new file mode 100644 index 00000000..6da25984 --- /dev/null +++ b/src/libs/kpunch/game/README.md @@ -0,0 +1,23 @@ +# game -- Game Helpers + +Helpers for programs that run like games inside a DVX window: frame +pacing on the high-resolution clock (yielding while it waits), held +keys, timer-service callbacks owned by the program, and nearest-colour +lookups on a 256-colour desktop. BASIC reaches it through FRAMEWAIT and +src/include/basic/game.bas. + +## Documentation + +Full reference is in `game.dhs` (this directory), compiled into the DVX +System Reference. Open via the Help Viewer app, or read +`docs/dvx_system_reference.html` after `make`. + +## Source Files + +- `game.h` -- public API +- `game.c` -- frames, keys, timers, colours +- `gameBas.c` -- BASIC entry points (game.bas) + +## Build + +`make -C src/libs/kpunch/game` (invoked by the top-level `make`). diff --git a/src/libs/kpunch/game/game.c b/src/libs/kpunch/game/game.c index a50471cf..949b1222 100644 --- a/src/libs/kpunch/game/game.c +++ b/src/libs/kpunch/game/game.c @@ -108,13 +108,15 @@ uint64_t gameFrameNext(GameFramePacerT *p, uint32_t *missed) { } +// Yields at least once, even when the frame is already due: a game too +// slow for its rate (every frame late) must not keep the CPU for good. uint32_t gameFrameWait(GameFramePacerT *p) { uint32_t missed = 0; uint64_t due = gameFrameNext(p, &missed); - while (platformMicros() < due) { + do { frameYield(); - } + } while (platformMicros() < due); return missed; } diff --git a/src/libs/kpunch/game/game.h b/src/libs/kpunch/game/game.h index d651c5b9..47ac0c38 100644 --- a/src/libs/kpunch/game/game.h +++ b/src/libs/kpunch/game/game.h @@ -81,7 +81,8 @@ void gameFrameInit(GameFramePacerT *p, uint32_t hz); // number of frames missed: 0 when on time, else the frames that already // ended while the caller was busy, which are skipped rather than caught // up. A frame that starts late but within its own time counts as on -// time and returns at once. +// time. The other programs get a turn even then (a game too slow for its +// rate never keeps the CPU for good). uint32_t gameFrameWait(GameFramePacerT *p); // gameFrameWait without the waiting, for a caller that waits its own way diff --git a/src/libs/kpunch/hlpfile/README.md b/src/libs/kpunch/hlpfile/README.md new file mode 100644 index 00000000..ebe75c41 --- /dev/null +++ b/src/libs/kpunch/hlpfile/README.md @@ -0,0 +1,23 @@ +# hlpfile -- Help File Reader + +Reads compiled DVX help files (.hlp): topics, the table of contents, the +keyword index, a topic's content records, images, and full-text search. +The Help Viewer, the HelpPane widget, and BASIC's hlpfile.bas all read +help files through it. + +## Documentation + +Full reference is in `hlpfile.dhs` (this directory), compiled into the +DVX System Reference. Open via the Help Viewer app, or read +`docs/dvx_system_reference.html` after `make`. + +## Source Files + +- `hlpFile.h` -- public API (HlpFileT handles) +- `hlpFormat.h` -- the .hlp file layout, shared with the help compiler +- `hlpFile.c` -- the reader and search +- `hlpFileBas.c` -- BASIC entry points (hlpfile.bas) + +## Build + +`make -C src/libs/kpunch/hlpfile` (invoked by the top-level `make`). diff --git a/src/libs/kpunch/hlpfile/hlpFile.c b/src/libs/kpunch/hlpfile/hlpFile.c index c580af22..11b6ddf3 100644 --- a/src/libs/kpunch/hlpfile/hlpFile.c +++ b/src/libs/kpunch/hlpfile/hlpFile.c @@ -35,6 +35,7 @@ #include "dvxPlat.h" #include "dvxTypes.h" +#include #include #include #include @@ -54,8 +55,11 @@ struct HlpFileT { char *stringTable; HlpTocEntryT *tocEntries; HlpIndexEntryT *indexEntries; - uint8_t *searchData; // search section, NULL if absent + uint8_t *searchData; // search section, NULL if absent (or not read yet) uint32_t trigramCount; + bool searchTried; // searchIndexLoad has run (the first search) + void *cache; // hlpFileSetCache + void (*cacheFree)(void *cache); }; // Search words for topicMatchRecord. @@ -72,6 +76,7 @@ typedef struct { static bool containsNoCase(const char *hay, const char *needle); static const HlpTrigramEntryT *findTrigram(const HlpFileT *f, const uint8_t *tri); +void *hlpFileCache(const HlpFileT *f); void hlpFileClose(HlpFileT *f); int32_t hlpFileFindTopic(const HlpFileT *f, const char *topicId); bool hlpFileForEachRecord(HlpFileT *f, int32_t topic, HlpRecordFnT fn, void *userData); @@ -80,6 +85,7 @@ int32_t hlpFileIndexCount(const HlpFileT *f); HlpFileT *hlpFileOpen(const char *path); uint8_t *hlpFileReadImage(HlpFileT *f, const HlpImageRefT *ref, uint32_t *outSize); int32_t hlpFileSearch(HlpFileT *f, const char *query, uint8_t *matches); +void hlpFileSetCache(HlpFileT *f, void *cache, void (*cacheFree)(void *cache)); int32_t hlpFileStartTopic(const HlpFileT *f, const char *topicId); const char *hlpFileString(const HlpFileT *f, uint32_t offset); const HlpTocEntryT *hlpFileToc(const HlpFileT *f, int32_t i); @@ -92,17 +98,27 @@ bool hlpRecordLink(const HlpRecordHdrT *hdr, const cha char *hlpWrapText(const char *text, int32_t cols); static bool readSearchIndex(HlpFileT *f); static bool readTable(HlpFileT *f, void **out, uint32_t offset, uint32_t count, uint32_t entrySize); +static void searchIndexLoad(HlpFileT *f); static int32_t searchRun(HlpFileT *f, HlpSearchT *search, const char *wholeQuery, uint8_t *matches, uint8_t *inPosting); static bool topicMatchRecord(const HlpRecordHdrT *hdr, const char *payload, void *userData); static void trigramFilter(const HlpFileT *f, const HlpSearchT *search, uint8_t *matches, uint8_t *inPosting, bool *titlesOnly); -// Case-insensitive substring test. +// Case-insensitive substring test. Only a place holding the needle's +// first letter (either case) is compared whole, so most of the text is +// passed over with two compares a character. static bool containsNoCase(const char *hay, const char *needle) { size_t needleLen = strlen(needle); + if (needleLen == 0) { + return hay[0] != '\0'; + } + + char lower = (char)tolower((unsigned char)needle[0]); + char upper = (char)toupper((unsigned char)needle[0]); + for (const char *p = hay; *p; p++) { - if (strncasecmp(p, needle, needleLen) == 0) { + if ((*p == lower || *p == upper) && strncasecmp(p, needle, needleLen) == 0) { return true; } } @@ -137,11 +153,18 @@ static const HlpTrigramEntryT *findTrigram(const HlpFileT *f, const uint8_t *tri } +void *hlpFileCache(const HlpFileT *f) { + return f ? f->cache : NULL; +} + + void hlpFileClose(HlpFileT *f) { if (!f) { return; } + hlpFileSetCache(f, NULL, NULL); + if (f->fp) { fclose(f->fp); } @@ -277,14 +300,6 @@ HlpFileT *hlpFileOpen(const char *path) { f->stringTable[f->header.stringTableSize - 1] = '\0'; } - // A missing or damaged search index only makes searching slower - // (every topic is scanned), so it does not fail the open. - if (!readSearchIndex(f)) { - free(f->searchData); - f->searchData = NULL; - f->trigramCount = 0; - } - return f; } @@ -359,6 +374,20 @@ int32_t hlpFileSearch(HlpFileT *f, const char *query, uint8_t *matches) { } +void hlpFileSetCache(HlpFileT *f, void *cache, void (*cacheFree)(void *cache)) { + if (!f) { + return; + } + + if (f->cacheFree) { + f->cacheFree(f->cache); + } + + f->cache = cache; + f->cacheFree = cacheFree; +} + + int32_t hlpFileStartTopic(const HlpFileT *f, const char *topicId) { if (!f) { return -1; @@ -563,6 +592,24 @@ static bool readTable(HlpFileT *f, void **out, uint32_t offset, uint32_t count, } +// The search section, read on the first search, so opening a file does +// not pay for it. A missing or damaged one only makes searching slower +// (every topic is scanned), so it is dropped. +static void searchIndexLoad(HlpFileT *f) { + if (f->searchTried) { + return; + } + + f->searchTried = true; + + if (!readSearchIndex(f)) { + free(f->searchData); + f->searchData = NULL; + f->trigramCount = 0; + } +} + + // hlpFileSearch with the words split: the trigram filter, then each // candidate checked word by word, then the keyword index. static int32_t searchRun(HlpFileT *f, HlpSearchT *search, const char *wholeQuery, uint8_t *matches, uint8_t *inPosting) { @@ -570,6 +617,7 @@ static int32_t searchRun(HlpFileT *f, HlpSearchT *search, const char *wholeQuery int32_t matchCount = 0; bool titlesOnly = false; + searchIndexLoad(f); memset(matches, 0, topicCount); if (search->termCount == 0) { diff --git a/src/libs/kpunch/hlpfile/hlpFile.h b/src/libs/kpunch/hlpfile/hlpFile.h index 1fc692a6..e0084eb6 100644 --- a/src/libs/kpunch/hlpfile/hlpFile.h +++ b/src/libs/kpunch/hlpfile/hlpFile.h @@ -51,7 +51,14 @@ typedef bool (*HlpRecordFnT)(const HlpRecordHdrT *hdr, const char *payload, void // The help file behind a BASIC handle (hlpfile.bas HlpOpen), or NULL. HlpFileT *hlpFileBasHandle(int32_t handle); -// Close the file and free everything read from it (NULL is ignored). +// What a caller keeps with an open file (a help view's decoded pictures, +// say): cacheFree frees it when the file closes, or when another cache +// is set. hlpFileCache is NULL when there is none. +void *hlpFileCache(const HlpFileT *f); +void hlpFileSetCache(HlpFileT *f, void *cache, void (*cacheFree)(void *cache)); + +// Close the file and free everything read from it, and its cache (NULL is +// ignored). void hlpFileClose(HlpFileT *f); // Index of the topic whose ID is topicId, or -1. diff --git a/src/libs/kpunch/http/Makefile b/src/libs/kpunch/http/Makefile index 9b24e22e..ab9a4e3a 100644 --- a/src/libs/kpunch/http/Makefile +++ b/src/libs/kpunch/http/Makefile @@ -27,7 +27,7 @@ include ../../../common.mk -CFLAGS = $(DOS_CFLAGS) -I. -I../dvxnet/include -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty +CFLAGS = $(DOS_CFLAGS) -I. -I.. -I../dvxnet/include -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty OBJDIR = ../../../../obj/http LIBSDIR = ../../../../bin/libs diff --git a/src/libs/kpunch/http/http.c b/src/libs/kpunch/http/http.c index f764048d..424d8d42 100644 --- a/src/libs/kpunch/http/http.c +++ b/src/libs/kpunch/http/http.c @@ -34,10 +34,10 @@ #include "http.h" #include "dvxTypes.h" #include "dvxPlat.h" +#include "dvxnet/dvxNet.h" #include "stb_ds_wrap.h" #include -#include #include #include #include @@ -98,7 +98,8 @@ typedef enum { StepIdleE, - StepConnectE, + StepConnectE, // a connection to start (fd NO_SOCKET) or under way + StepLookupE, // the host's address being looked up StepSendE, StepHeadE, StepBodyE, @@ -126,11 +127,18 @@ struct HttpS { HttpUrlT target; char **headers; // stb_ds of "Name: value" (strdup'd) char *body; // the request's body (malloc'd), or NULL - int32_t bodyLen; + int32_t bodyLen; // its bytes, or the body file's + char bodyPath[DVX_MAX_PATH]; // the body sent from this file ("": none) + FILE *bodyFile; // ... while it is sent + int32_t bodySent; // its bytes taken by the socket + int32_t bodyBufLen; // read from it + int32_t bodyBufPos; // of those, taken + char bodyBuf[READ_STEP]; char savePath[DVX_MAX_PATH]; // "": the body to memory bool resume; uint32_t timeoutMs; StepE step; + NetLookupT lookup; // StepLookupE int fd; uint32_t lastMs; // when something last happened char *out; // stb_ds: the request as sent @@ -151,6 +159,8 @@ struct HttpS { int32_t redirects; char headerValue[HEADER_VALUE_MAX]; char err[HTTP_ERR_MAX]; + int32_t polling; // httpPoll calls under way + bool freeAsked; // httpFree asked meanwhile: done when they return }; DVX_ABI_STAMP(http); @@ -160,6 +170,8 @@ DVX_ABI_STAMP(http); // ============================================================ static bool bodyFeed(HttpT *h, const char *data, int32_t len); +static void bodyFileClose(HttpT *h); +static bool bodyFileSend(HttpT *h); static bool bodyStart(HttpT *h); static bool bodyWrite(HttpT *h, const char *data, int32_t len); static bool buildRequest(HttpT *h); @@ -171,8 +183,9 @@ static HttpStateE fail(HttpT *h, const char *fmt, ...) __attribute__((format(pr static HttpStateE finish(HttpT *h); static const char *headerFind(const char *head, const char *name, char *out, int32_t outSize); static bool headParse(HttpT *h, int32_t *headLen); -static bool hostConnect(HttpT *h); +static bool hostConnect(HttpT *h, uint32_t addr); const char *httpBody(const HttpT *h, int32_t *len); +bool httpBodyFile(HttpT *h, const char *path, const char *contentType); bool httpBodySet(HttpT *h, const void *data, int32_t len, const char *contentType); HttpT *httpCreate(const char *method, const char *url); void httpFree(HttpT *h); @@ -189,6 +202,7 @@ const char *httpUrl(const HttpT *h); bool httpUrlParse(const char *url, HttpUrlT *out, char *err, int32_t errSize); static bool isRedirect(int32_t status); static bool locationResolve(const HttpT *h, const char *loc, char *out, int32_t outSize); +static HttpStateE pollStep(HttpT *h, uint32_t nowMs); static void replyReset(HttpT *h); static bool socketReady(int fd, short events); static bool textAdd(char **buf, const char *fmt, ...) __attribute__((format(printf, 2, 3))); @@ -233,6 +247,56 @@ static bool bodyFeed(HttpT *h, const char *data, int32_t len) { } +// The body file is closed (it is read from its start when sent again). +static void bodyFileClose(HttpT *h) { + if (h->bodyFile) { + fclose(h->bodyFile); + } + + h->bodyFile = NULL; + h->bodySent = 0; + h->bodyBufLen = 0; + h->bodyBufPos = 0; +} + + +// A piece of the body file sent: what the socket takes of the piece read +// last, the next piece read once it took all of it. False when the file +// cannot be read (or ends before the length the request gave). +static bool bodyFileSend(HttpT *h) { + if (h->bodyBufPos == h->bodyBufLen) { + int32_t want = h->bodyLen - h->bodySent; + size_t n; + + if (!h->bodyFile) { + h->bodyFile = fopen(h->bodyPath, "rb"); + } + + n = h->bodyFile ? fread(h->bodyBuf, 1, (size_t)(want < (int32_t)sizeof(h->bodyBuf) ? want : (int32_t)sizeof(h->bodyBuf)), h->bodyFile) : 0; + + if (n == 0) { + return false; + } + + h->bodyBufLen = (int32_t)n; + h->bodyBufPos = 0; + } + + ssize_t sent = send(h->fd, h->bodyBuf + h->bodyBufPos, (size_t)(h->bodyBufLen - h->bodyBufPos), MSG_NOSIGNAL); + + if (sent < 0 && !wouldBlock(errno)) { + return false; + } + + if (sent > 0) { + h->bodyBufPos += (int32_t)sent; + h->bodySent += (int32_t)sent; + } + + return true; +} + + // The head is read: where the body goes, how it comes, and how much of // it there is. False (h failed) when the file cannot be opened. static bool bodyStart(HttpT *h) { @@ -302,6 +366,7 @@ static bool buildRequest(HttpT *h) { bool ok; arrfree(h->out); + bodyFileClose(h); h->sent = 0; ok = textAdd(&h->out, "%s %s HTTP/1.1" CRLF, h->method, h->target.path); @@ -321,13 +386,14 @@ static bool buildRequest(HttpT *h) { ok = ok && textAdd(&h->out, "%s" CRLF, h->headers[i]); } - if (h->body || strcmp(h->method, METHOD_POST) == 0 || strcmp(h->method, METHOD_PUT) == 0) { + if (h->body || h->bodyPath[0] || strcmp(h->method, METHOD_POST) == 0 || strcmp(h->method, METHOD_PUT) == 0) { ok = ok && textAdd(&h->out, FIELD_LENGTH ": %ld" CRLF, (long)h->bodyLen); } ok = ok && textAdd(&h->out, CRLF); - if (ok && h->bodyLen > 0) { + // A body file is sent after the head, a piece at a time + if (ok && h->body && h->bodyLen > 0) { memcpy(arraddnptr(h->out, h->bodyLen), h->body, (size_t)h->bodyLen); } @@ -403,7 +469,10 @@ static bool chunkFeed(HttpT *h, const char *data, int32_t len, int32_t *used) { } +// The socket goes, and a lookup under way. static void closeSocket(HttpT *h) { + netLookupCancel(&h->lookup); + if (h->fd != NO_SOCKET) { close(h->fd); h->fd = NO_SOCKET; @@ -413,6 +482,8 @@ static void closeSocket(HttpT *h) { // Look the host up and start connecting (StepConnectE then waits for the // connection). False when h failed. +// The request made, and the host's address looked up (pollStep waits for +// it a poll at a time). static bool connectStart(HttpT *h) { replyReset(h); @@ -421,7 +492,9 @@ static bool connectStart(HttpT *h) { return false; } - return hostConnect(h); + netLookupStart(&h->lookup, h->target.host); + h->step = StepLookupE; + return true; } @@ -455,6 +528,7 @@ static HttpStateE fail(HttpT *h, const char *fmt, ...) { va_end(ap); h->step = StepFailedE; closeSocket(h); + bodyFileClose(h); if (h->file) { fclose(h->file); @@ -569,37 +643,28 @@ static bool headParse(HttpT *h, int32_t *headLen) { } -// The host's address, a socket, and a connection started. -static bool hostConnect(HttpT *h) { - struct addrinfo hints; - struct addrinfo *found = NULL; - char port[NUMBER_TEXT_MAX]; - unsigned long on = 1; - int rc; +// A socket, and a connection to the host (at addr, network order) +// started. +static bool hostConnect(HttpT *h, uint32_t addr) { + struct sockaddr_in to; + unsigned long on = 1; + int rc; - memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_INET; - hints.ai_socktype = SOCK_STREAM; - snprintf(port, sizeof(port), "%u", (unsigned)h->target.port); - - if (getaddrinfo(h->target.host, port, &hints, &found) != 0 || !found) { - fail(h, "Cannot find %s on the network.", h->target.host); - return false; - } - - h->fd = socket(AF_INET, SOCK_STREAM, 0); + memset(&to, 0, sizeof(to)); + to.sin_family = AF_INET; + to.sin_port = htons(h->target.port); + to.sin_addr.s_addr = addr; + h->fd = socket(AF_INET, SOCK_STREAM, 0); if (h->fd < 0 || ioctl(h->fd, FIONBIO, &on) != 0) { int32_t err = errno; - freeaddrinfo(found); - h->fd = NO_SOCKET; + closeSocket(h); fail(h, "Cannot connect to %s: %s.", h->target.host, errnoText(err)); return false; } - rc = connect(h->fd, found->ai_addr, found->ai_addrlen); - freeaddrinfo(found); + rc = connect(h->fd, (struct sockaddr *)&to, sizeof(to)); if (rc != 0 && errno != EINPROGRESS && !wouldBlock(errno)) { fail(h, "Cannot connect to %s: %s.", h->target.host, errnoText(errno)); @@ -620,6 +685,21 @@ const char *httpBody(const HttpT *h, int32_t *len) { } +bool httpBodyFile(HttpT *h, const char *path, const char *contentType) { + struct stat st; + + if (!path || stat(path, &st) != 0 || !S_ISREG(st.st_mode) || (uint64_t)st.st_size > INT32_MAX || snprintf(h->bodyPath, sizeof(h->bodyPath), "%s", path) >= (int)sizeof(h->bodyPath)) { + h->bodyPath[0] = '\0'; + return false; + } + + free(h->body); + h->body = NULL; + h->bodyLen = (int32_t)st.st_size; + return !contentType || !contentType[0] || httpHeaderAdd(h, FIELD_TYPE, contentType); +} + + bool httpBodySet(HttpT *h, const void *data, int32_t len, const char *contentType) { char *copy = (char *)malloc(len > 0 ? (size_t)len : 1); @@ -629,7 +709,8 @@ bool httpBodySet(HttpT *h, const void *data, int32_t len, const char *contentTyp memcpy(copy, data, len > 0 ? (size_t)len : 0); free(h->body); - h->body = copy; + h->bodyPath[0] = '\0'; + h->body = copy; h->bodyLen = len > 0 ? len : 0; return !contentType || !contentType[0] || httpHeaderAdd(h, FIELD_TYPE, contentType); } @@ -656,6 +737,11 @@ void httpFree(HttpT *h) { return; } + if (h->polling > 0) { + h->freeAsked = true; + return; + } + closeSocket(h); if (h->file) { @@ -712,150 +798,26 @@ const char *httpMessage(const HttpT *h) { } +// A free asked for during a poll (a wait for the network inside it lets +// other code run) is carried out when the poll returns, which then +// reports failure: the request outlives the code still using it. HttpStateE httpPoll(HttpT *h, uint32_t nowMs) { - if (!h || h->step == StepIdleE || h->step == StepFailedE) { + if (!h) { return HttpFailedE; } - if (h->step == StepDoneE) { - return HttpDoneE; + h->polling++; + + HttpStateE st = pollStep(h, nowMs); + + h->polling--; + + if (h->polling == 0 && h->freeAsked) { + httpFree(h); + return HttpFailedE; } - if (h->step == StepConnectE && h->fd == NO_SOCKET) { - if (!connectStart(h)) { - return HttpFailedE; - } - - h->lastMs = nowMs; - } - - if (h->step == StepConnectE) { - if (socketReady(h->fd, POLLOUT)) { - int soErr = 0; - socklen_t len = sizeof(soErr); - - if (getsockopt(h->fd, SOL_SOCKET, SO_ERROR, &soErr, &len) != 0 || soErr != 0) { - return fail(h, "Cannot connect to %s: %s.", h->target.host, errnoText(soErr ? soErr : errno)); - } - - h->step = StepSendE; - h->lastMs = nowMs; - } - } - - if (h->step == StepSendE && socketReady(h->fd, POLLOUT)) { - ssize_t n = send(h->fd, h->out + h->sent, (size_t)(arrlen(h->out) - h->sent), MSG_NOSIGNAL); - - if (n < 0 && !wouldBlock(errno)) { - return fail(h, "Cannot send to %s: %s.", h->target.host, errnoText(errno)); - } - - if (n > 0) { - h->sent += (int32_t)n; - h->lastMs = nowMs; - } - - if (h->sent == (int32_t)arrlen(h->out)) { - h->step = StepHeadE; - } - } - - if ((h->step == StepHeadE || h->step == StepBodyE) && socketReady(h->fd, POLLIN)) { - char buf[READ_STEP]; - ssize_t n = recv(h->fd, buf, sizeof(buf), 0); - - if (n < 0 && !wouldBlock(errno)) { - return fail(h, "Lost the connection to %s: %s.", h->target.host, errnoText(errno)); - } - - if (n == 0 && h->step == StepHeadE) { - return fail(h, "%s closed the connection without answering.", h->target.host); - } - - if (n == 0) { - if (h->bodyMode != BodyCloseE) { - return fail(h, "%s closed the connection before the whole reply came.", h->target.host); - } - - h->step = StepDoneE; - } - - if (n > 0) { - h->lastMs = nowMs; - - if (h->step == StepHeadE) { - int32_t headLen = 0; - - memcpy(arraddnptr(h->head, n), buf, (size_t)n); - - if (!headParse(h, &headLen)) { - return HttpFailedE; - } - - if (headLen > 0) { - char loc[HEADER_VALUE_MAX]; - char next[HTTP_URL_MAX]; - int32_t rest = (int32_t)arrlen(h->head) - headLen; - - // The body's first bytes, then the head alone (NUL-ended): - // they came in this read, so they fit in buf - if (rest > 0 && rest <= (int32_t)sizeof(buf)) { - memcpy(buf, h->head + headLen, (size_t)rest); - } - - arrsetlen(h->head, headLen); - textEnd(&h->head); - - // A redirect: the same request (a GET after 303, or - // after 301 and 302 of a POST) to where it points - if (isRedirect(h->status) && headerFind(h->head, FIELD_LOCATION, loc, sizeof(loc))) { - if (h->redirects >= HTTP_REDIRECTS_MAX) { - return fail(h, "%s sent the request elsewhere too many times.", h->target.host); - } - - if (!locationResolve(h, loc, next, sizeof(next)) || !httpUrlParse(next, &h->target, h->err, sizeof(h->err))) { - return fail(h, "%s sent the request to %s, which is not an http:// address.", h->target.host, loc); - } - - h->redirects++; - snprintf(h->url, sizeof(h->url), "%s", next); - - if (h->status == STATUS_SEE_OTHER || ((h->status == STATUS_MOVED || h->status == STATUS_FOUND) && strcmp(h->method, METHOD_POST) == 0)) { - snprintf(h->method, sizeof(h->method), "%s", METHOD_GET); - free(h->body); - h->body = NULL; - h->bodyLen = 0; - } - - closeSocket(h); - h->step = StepConnectE; - h->lastMs = nowMs; - return HttpBusyE; - } - - if (!bodyStart(h)) { - return HttpFailedE; - } - - if (h->step == StepBodyE && rest > 0 && !bodyFeed(h, buf, rest)) { - return HttpFailedE; - } - } - } else if (!bodyFeed(h, buf, (int32_t)n)) { - return HttpFailedE; - } - } - } - - if (h->step == StepDoneE) { - return finish(h); - } - - if (nowMs - h->lastMs > h->timeoutMs) { - return fail(h, "%s did not answer in time (%lu seconds).", h->target.host, (unsigned long)(h->timeoutMs / MS_PER_SECOND)); - } - - return HttpBusyE; + return st; } @@ -996,6 +958,181 @@ static bool locationResolve(const HttpT *h, const char *loc, char *out, int32_t } +// httpPoll's work: one step of the request. +static HttpStateE pollStep(HttpT *h, uint32_t nowMs) { + if (h->step == StepIdleE || h->step == StepFailedE) { + return HttpFailedE; + } + + if (h->step == StepDoneE) { + return HttpDoneE; + } + + if (h->step == StepConnectE && h->fd == NO_SOCKET && !connectStart(h)) { + return HttpFailedE; + } + + // The lookup ends itself (DHCP's first address and the DNS server + // each have their limit); the timeout counts from its end + if (h->step == StepLookupE) { + uint32_t addr; + int32_t rc = netLookupPoll(&h->lookup, &addr); + + if (rc == NET_LOOKUP_BUSY) { + return HttpBusyE; + } + + if (rc != 0) { + return fail(h, "Cannot find %s on the network.", h->target.host); + } + + if (!hostConnect(h, addr)) { + return HttpFailedE; + } + + h->lastMs = nowMs; + } + + if (h->step == StepConnectE) { + if (socketReady(h->fd, POLLOUT)) { + int soErr = 0; + socklen_t len = sizeof(soErr); + + if (getsockopt(h->fd, SOL_SOCKET, SO_ERROR, &soErr, &len) != 0 || soErr != 0) { + return fail(h, "Cannot connect to %s: %s.", h->target.host, errnoText(soErr ? soErr : errno)); + } + + h->step = StepSendE; + h->lastMs = nowMs; + } + } + + // The head (and a body in memory), then a body file's pieces + if (h->step == StepSendE && socketReady(h->fd, POLLOUT)) { + int32_t before = h->sent + h->bodySent; + + if (h->sent < (int32_t)arrlen(h->out)) { + ssize_t n = send(h->fd, h->out + h->sent, (size_t)(arrlen(h->out) - h->sent), MSG_NOSIGNAL); + + if (n < 0 && !wouldBlock(errno)) { + return fail(h, "Cannot send to %s: %s.", h->target.host, errnoText(errno)); + } + + h->sent += n > 0 ? (int32_t)n : 0; + } else if (h->bodyPath[0] && !bodyFileSend(h)) { + return fail(h, "Cannot send %s to %s.", h->bodyPath, h->target.host); + } + + if (h->sent + h->bodySent > before) { + h->lastMs = nowMs; + } + + if (h->sent == (int32_t)arrlen(h->out) && (!h->bodyPath[0] || h->bodySent == h->bodyLen)) { + bodyFileClose(h); + h->step = StepHeadE; + } + } + + if ((h->step == StepHeadE || h->step == StepBodyE) && socketReady(h->fd, POLLIN)) { + char buf[READ_STEP]; + ssize_t n = recv(h->fd, buf, sizeof(buf), 0); + + if (n < 0 && !wouldBlock(errno)) { + return fail(h, "Lost the connection to %s: %s.", h->target.host, errnoText(errno)); + } + + if (n == 0 && h->step == StepHeadE) { + return fail(h, "%s closed the connection without answering.", h->target.host); + } + + if (n == 0) { + if (h->bodyMode != BodyCloseE) { + return fail(h, "%s closed the connection before the whole reply came.", h->target.host); + } + + h->step = StepDoneE; + } + + if (n > 0) { + h->lastMs = nowMs; + + if (h->step == StepHeadE) { + int32_t headLen = 0; + + memcpy(arraddnptr(h->head, n), buf, (size_t)n); + + if (!headParse(h, &headLen)) { + return HttpFailedE; + } + + if (headLen > 0) { + char loc[HEADER_VALUE_MAX]; + char next[HTTP_URL_MAX]; + int32_t rest = (int32_t)arrlen(h->head) - headLen; + + // The body's first bytes, then the head alone (NUL-ended): + // they came in this read, so they fit in buf + if (rest > 0 && rest <= (int32_t)sizeof(buf)) { + memcpy(buf, h->head + headLen, (size_t)rest); + } + + arrsetlen(h->head, headLen); + textEnd(&h->head); + + // A redirect: the same request (a GET after 303, or + // after 301 and 302 of a POST) to where it points + if (isRedirect(h->status) && headerFind(h->head, FIELD_LOCATION, loc, sizeof(loc))) { + if (h->redirects >= HTTP_REDIRECTS_MAX) { + return fail(h, "%s sent the request elsewhere too many times.", h->target.host); + } + + if (!locationResolve(h, loc, next, sizeof(next)) || !httpUrlParse(next, &h->target, h->err, sizeof(h->err))) { + return fail(h, "%s sent the request to %s, which is not an http:// address.", h->target.host, loc); + } + + h->redirects++; + snprintf(h->url, sizeof(h->url), "%s", next); + + if (h->status == STATUS_SEE_OTHER || ((h->status == STATUS_MOVED || h->status == STATUS_FOUND) && strcmp(h->method, METHOD_POST) == 0)) { + snprintf(h->method, sizeof(h->method), "%s", METHOD_GET); + free(h->body); + h->body = NULL; + h->bodyLen = 0; + h->bodyPath[0] = '\0'; + } + + closeSocket(h); + h->step = StepConnectE; + h->lastMs = nowMs; + return HttpBusyE; + } + + if (!bodyStart(h)) { + return HttpFailedE; + } + + if (h->step == StepBodyE && rest > 0 && !bodyFeed(h, buf, rest)) { + return HttpFailedE; + } + } + } else if (!bodyFeed(h, buf, (int32_t)n)) { + return HttpFailedE; + } + } + } + + if (h->step == StepDoneE) { + return finish(h); + } + + if (nowMs - h->lastMs > h->timeoutMs) { + return fail(h, "%s did not answer in time (%lu seconds).", h->target.host, (unsigned long)(h->timeoutMs / MS_PER_SECOND)); + } + + return HttpBusyE; +} + + // Forget the reply so far (a new connection: the first, or a redirect's). static void replyReset(HttpT *h) { arrfree(h->head); diff --git a/src/libs/kpunch/http/http.h b/src/libs/kpunch/http/http.h index 9b2c2e94..fdd3fa5d 100644 --- a/src/libs/kpunch/http/http.h +++ b/src/libs/kpunch/http/http.h @@ -80,6 +80,11 @@ typedef struct HttpS HttpT; // counted). "" when it went to a file, or before the reply. const char *httpBody(const HttpT *h, int32_t *len); +// Send the file at path as the request's body, of contentType, a piece +// at a time as the socket takes it (never all in memory). False when it +// is not a file that can be sent. +bool httpBodyFile(HttpT *h, const char *path, const char *contentType); + // Send data (copied) as the request's body, of contentType. bool httpBodySet(HttpT *h, const void *data, int32_t len, const char *contentType); diff --git a/src/libs/kpunch/http/httpBas.c b/src/libs/kpunch/http/httpBas.c index baaee49b..f32b4f5c 100644 --- a/src/libs/kpunch/http/httpBas.c +++ b/src/libs/kpunch/http/httpBas.c @@ -52,6 +52,7 @@ const char *HttpReplyHeader(int32_t h, const char *name); int32_t HttpRunFor(int32_t h, int32_t milliseconds); void HttpSaveTo(int32_t h, const char *path, int32_t resume); int32_t HttpSetBody(int32_t h, const uint8_t *data, int32_t len, const char *contentType); +int32_t HttpSetBodyFile(int32_t h, const char *path, const char *contentType); void HttpSetTimeout(int32_t h, int32_t seconds); int32_t HttpStart(int32_t h); int32_t HttpStatus(int32_t h); @@ -158,6 +159,15 @@ int32_t HttpSetBody(int32_t h, const uint8_t *data, int32_t len, const char *con } +// Send the file at path as the request's body, of contentType (a piece +// at a time: a file of any size). False when it is not a file. +int32_t HttpSetBodyFile(int32_t h, const char *path, const char *contentType) { + HttpT *r = requestOf(h); + + return (r && httpBodyFile(r, path, contentType)) ? BAS_TRUE : BAS_FALSE; +} + + // Give up after seconds without progress. void HttpSetTimeout(int32_t h, int32_t seconds) { HttpT *r = requestOf(h); diff --git a/src/libs/kpunch/http/httpBas.h b/src/libs/kpunch/http/httpBas.h index 6b9dea59..f798e1e1 100644 --- a/src/libs/kpunch/http/httpBas.h +++ b/src/libs/kpunch/http/httpBas.h @@ -39,6 +39,7 @@ const char *HttpReplyHeader(int32_t h, const char *name); int32_t HttpRunFor(int32_t h, int32_t milliseconds); void HttpSaveTo(int32_t h, const char *path, int32_t resume); int32_t HttpSetBody(int32_t h, const uint8_t *data, int32_t len, const char *contentType); +int32_t HttpSetBodyFile(int32_t h, const char *path, const char *contentType); void HttpSetTimeout(int32_t h, int32_t seconds); int32_t HttpStart(int32_t h); int32_t HttpStatus(int32_t h); diff --git a/src/libs/kpunch/joystick/README.md b/src/libs/kpunch/joystick/README.md new file mode 100644 index 00000000..1c946198 --- /dev/null +++ b/src/libs/kpunch/joystick/README.md @@ -0,0 +1,27 @@ +# joystick -- Game Controllers + +Game controllers for DVX programs behind one API whatever the hardware: +the devices the drivers found, calibrated reading or events, calibration +kept in DVX.INI [joystick] (set by the Control Panel), and driver +modules loaded when a program first asks. Handles belong to the program +that opened them. BASIC reaches it through STICK / STRIG and +src/include/basic/joystick.bas. + +## Documentation + +Full reference is in `joystick.dhs` (this directory), compiled into the +DVX System Reference. Open via the Help Viewer app, or read +`docs/dvx_system_reference.html` after `make`. + +## Source Files + +- `joystick.h`, `joyInt.h` -- public and internal API +- `joystick.c` -- devices, handles, calibration, events +- `joyShell.c` -- the ties to the shell: the event idle handler and the + app cleanup hook +- `joyBas.c` -- BASIC entry points (joystick.bas) +- `drivers/` -- the controller driver modules + +## Build + +`make -C src/libs/kpunch/joystick` (invoked by the top-level `make`). diff --git a/src/libs/kpunch/joystick/joyBas.c b/src/libs/kpunch/joystick/joyBas.c index 3ec7a9b7..f1b593bb 100644 --- a/src/libs/kpunch/joystick/joyBas.c +++ b/src/libs/kpunch/joystick/joyBas.c @@ -44,6 +44,7 @@ #define QB_POS_MAX 200 #define QB_STRIGS 8 // STRIG(0)..STRIG(7) #define QB_LATCH_SLOTS 32 // programs whose STICK(0) readings are kept +#define BAS_EVENTS_MAX 64 // joystick changes kept for JoyEvent (the oldest go first) // A program's positions as its last STICK(0) read them (appId 0: free). typedef struct { @@ -51,12 +52,23 @@ typedef struct { int32_t pos[QB_LATCHED]; } StickLatchT; -static StickLatchT sLatches[QB_LATCH_SLOTS]; +// A change of a joystick a program watches (JoyWatch), until JoyEvent +// gives it +typedef struct { + int32_t appId; + JoyEventT evt; +} BasJoyEventT; + +static StickLatchT sLatches[QB_LATCH_SLOTS]; +static BasJoyEventT sEvents[BAS_EVENTS_MAX]; +static int32_t sEventCount = 0; +static JoyEventT sLastEvent; // the change JoyEvent gave last // ============================================================ // Prototypes // ============================================================ +static void basEvent(int32_t handle, const JoyEventT *evt, void *userData); static int32_t basHandle(int32_t joy); static bool basInfo(int32_t joy, JoyInfoT *info); static bool basRaw(int32_t joy, JoyRawT *raw); @@ -79,6 +91,10 @@ int32_t JoyClearCalibration(int32_t joy, int32_t axis); int32_t JoyConnected(int32_t joy); int32_t JoyCount(void); int32_t JoyDeadZone(int32_t joy); +int32_t JoyEvent(void); +int32_t JoyEventIndex(void); +int32_t JoyEventJoy(void); +int32_t JoyEventValue(void); int32_t JoyHat(int32_t joy, int32_t hat); int32_t JoyHats(int32_t joy); int32_t JoyIsKeyboard(int32_t joy); @@ -101,6 +117,7 @@ void JoySetSetting(const char *key, const char *value); const char *JoySetting(const char *key, const char *defaultValue); void JoySettingsLoad(int32_t prefs); void JoySettingsStore(int32_t prefs); +int32_t JoyWatch(int32_t joy, int32_t watching); int32_t JoyX(int32_t joy); int32_t JoyY(int32_t joy); static StickLatchT *latchOf(int32_t appId, bool create); @@ -108,6 +125,23 @@ static bool qbStick(int32_t stick, int32_t *device, int32_t *first); static int32_t qbStickPos(int32_t stick, int32_t axis); +// A watched joystick changed: kept for JoyEvent (the call comes as the +// program that owns the handle). +static void basEvent(int32_t handle, const JoyEventT *evt, void *userData) { + (void)handle; + (void)userData; + + if (sEventCount == BAS_EVENTS_MAX) { + memmove(&sEvents[0], &sEvents[1], (size_t)(BAS_EVENTS_MAX - 1) * sizeof(sEvents[0])); + sEventCount--; + } + + sEvents[sEventCount].appId = shellContext()->currentAppId; + sEvents[sEventCount].evt = *evt; + sEventCount++; +} + + // The calling program's handle on joystick joy (1-based), -1 if none. static int32_t basHandle(int32_t joy) { return joyOpenShared(shellContext(), joy - 1); @@ -167,6 +201,13 @@ void joyBasForget(int32_t appId) { if (l) { l->appId = 0; } + + for (int32_t i = sEventCount - 1; i >= 0; i--) { + if (sEvents[i].appId == appId) { + memmove(&sEvents[i], &sEvents[i + 1], (size_t)(sEventCount - i - 1) * sizeof(sEvents[0])); + sEventCount--; + } + } } @@ -313,6 +354,44 @@ int32_t JoyDeadZone(int32_t joy) { } +// The calling program's next joystick change since it last asked (each +// once, in order): JOY_EVENT_AXIS, _BUTTON_DOWN, _BUTTON_UP or _HAT, with +// JoyEventJoy, JoyEventIndex and JoyEventValue describing it; JOY_EVENT_NONE +// when there is none. +int32_t JoyEvent(void) { + int32_t appId = shellContext()->currentAppId; + + for (int32_t i = 0; i < sEventCount; i++) { + if (sEvents[i].appId == appId) { + sLastEvent = sEvents[i].evt; + memmove(&sEvents[i], &sEvents[i + 1], (size_t)(sEventCount - i - 1) * sizeof(sEvents[0])); + sEventCount--; + return (int32_t)sLastEvent.kind + JOY_BAS_EVENT_FIRST; + } + } + + return JOY_BAS_EVENT_NONE; +} + + +// What the change JoyEvent gave last was about: the joystick, the axis, +// button or hat (numbered from 1), and an axis's new position or a hat's +// direction. +int32_t JoyEventIndex(void) { + return sLastEvent.index + 1; +} + + +int32_t JoyEventJoy(void) { + return sLastEvent.device + 1; +} + + +int32_t JoyEventValue(void) { + return sLastEvent.value; +} + + int32_t JoyHat(int32_t joy, int32_t hat) { JoyStateT st; @@ -474,6 +553,15 @@ void JoySettingsStore(int32_t prefs) { } +// Start (watching True) or stop keeping joy's changes for JoyEvent; True +// when there is such a joystick. +int32_t JoyWatch(int32_t joy, int32_t watching) { + int32_t handle = basHandle(joy); + + return basTruth(handle >= 0 && joySetEventFn(handle, watching != BAS_FALSE ? basEvent : NULL, NULL)); +} + + int32_t JoyX(int32_t joy) { return JoyAxis(joy, 1); } diff --git a/src/libs/kpunch/joystick/joystick.dhs b/src/libs/kpunch/joystick/joystick.dhs index a3daef7a..361db0f2 100644 --- a/src/libs/kpunch/joystick/joystick.dhs +++ b/src/libs/kpunch/joystick/joystick.dhs @@ -161,7 +161,7 @@ Up to JOY_MAX_DRIVERS drivers in use get their tick called. A driver is loaded a .h2 BASIC -src/include/basic/joystick.bas declares JoyCount, JoyName$, JoyAxes, JoyButtons, JoyX, JoyY, JoyAxis, JoyButton, and JoyHat. Joystick, axis, button, and hat numbers start at 1; each function reads through the calling program's shared handle. +src/include/basic/joystick.bas declares JoyCount, JoyName$, JoyAxes, JoyButtons, JoyX, JoyY, JoyAxis, JoyButton, and JoyHat. Joystick, axis, button, and hat numbers start at 1; each function reads through the calling program's shared handle. Events: JoyWatch(joy, watching) sets (or clears) that handle's joySetEventFn, which keeps the program's changes, the last BAS_EVENTS_MAX (64); JoyEvent gives the next as JoyEventKindE + JOY_BAS_EVENT_FIRST (JOY_BAS_EVENT_NONE for none; CONSTs JOY_EVENT_*), and JoyEventJoy, JoyEventIndex (both from 1), and JoyEventValue the one it gave last. joyBasForget drops a program's kept changes when it ends. For a settings program (the Control Panel's Game Controllers page) it also declares: JoyHats, JoyAxisPresent(joy, axis), JoyCanCalibrate(joy) (JoyInfoT hatCount, axisPresent, JOY_CAP_CALIBRATE); JoyConnected(joy) (joyRead's connected); JoyRescan (joyRescanApp for the calling program: its own handles and STICK readings go first; -1 while another program has a joystick open); JoyRawAxis(joy, axis) and JoyRawButtons(joy) (joyGetRaw); JoySetCalibration(joy, axis, minV, centerV, maxV) and JoyClearCalibration(joy, axis) (joySetCalibration); JoyDeadZone / JoySetDeadZone (joyGetDeadZone / joySetDeadZone); JoyChoiceCount(which), JoyChoiceLabel$(which, index), JoyChoice(which), JoySetChoice(which, index) with JOY_CHOICE_GAMEPORT = 0 and JOY_CHOICE_FALLBACK = 1 (JoyChoiceE; joyChoices / joyChoiceGet / joyChoiceSet); JoyKeyCount and JoyKeyName$(index); JoyKbdSlotCount, JoyKbdSlot(slot), JoySetKbdSlot(slot, keyIndex) (joyKbdSlotGet / Set); JoySetting$(key, defaultValue) and JoySetSetting(key, value) ("" removes; joySettingGet / Set); JoySettingsLoad(prefs) and JoySettingsStore(prefs) with a prefs.bas handle (prefsBasHandle); JoyCalibration(joy, axis, part) (joyGetCalibration; part JOY_CAL_MIN / JOY_CAL_CENTER / JOY_CAL_MAX, joystick.h; 0 when joyGetCalibration fails); JoyIsKeyboard(joy) (JOY_CAP_KEYBOARD); JoyKeyIndex(keyName), JoyKeyCode(index), JoyKeyNameOf$(rawCode) (joyKeyIndex / joyKeyCode / joyKeyNameOf). CONSTs JOY_DEADZONE_DEFAULT / _MAX, JOY_POLL_HZ_DEFAULT / _MIN / _MAX, and JOY_KEY_POLL_HZ mirror joystick.h. Lists (choices, keys, slots) are numbered from 0; True is -1. diff --git a/src/libs/kpunch/joystick/joystick.h b/src/libs/kpunch/joystick/joystick.h index 949f35c9..fcabb4fa 100644 --- a/src/libs/kpunch/joystick/joystick.h +++ b/src/libs/kpunch/joystick/joystick.h @@ -189,6 +189,11 @@ typedef struct { int32_t value; } JoyEventT; +// JoyEvent's answers in BASIC (joystick.bas JOY_EVENT_*): none, else the +// JoyEventKindE plus JOY_BAS_EVENT_FIRST +#define JOY_BAS_EVENT_NONE 0 +#define JOY_BAS_EVENT_FIRST 1 + // Called in the program's context (not at interrupt time): DVX calls are fine. typedef void (*JoyEventFnT)(int32_t handle, const JoyEventT *evt, void *userData); @@ -394,7 +399,9 @@ int32_t joyBasStrig(int32_t n); // ============================================================ // // Joystick, axis, button and hat numbers start at 1; every function reads -// through the calling program's shared handle. +// through the calling program's shared handle. JoyWatch sets that +// handle's event function to keep the program's changes (the last 64) +// for JoyEvent. int32_t JoyAxes(int32_t joy); int32_t JoyAxis(int32_t joy, int32_t axis); @@ -410,6 +417,10 @@ int32_t JoyClearCalibration(int32_t joy, int32_t axis); int32_t JoyConnected(int32_t joy); int32_t JoyCount(void); int32_t JoyDeadZone(int32_t joy); +int32_t JoyEvent(void); +int32_t JoyEventIndex(void); +int32_t JoyEventJoy(void); +int32_t JoyEventValue(void); int32_t JoyHat(int32_t joy, int32_t hat); int32_t JoyHats(int32_t joy); int32_t JoyIsKeyboard(int32_t joy); @@ -432,6 +443,7 @@ void JoySetSetting(const char *key, const char *value); const char *JoySetting(const char *key, const char *defaultValue); void JoySettingsLoad(int32_t prefs); void JoySettingsStore(int32_t prefs); +int32_t JoyWatch(int32_t joy, int32_t watching); int32_t JoyX(int32_t joy); int32_t JoyY(int32_t joy); diff --git a/src/libs/kpunch/json/json.c b/src/libs/kpunch/json/json.c index 01893a36..266694d0 100644 --- a/src/libs/kpunch/json/json.c +++ b/src/libs/kpunch/json/json.c @@ -29,7 +29,9 @@ // by offset because the pool moves as it grows. #include "json.h" +#include "dvxFontFmt.h" #include "dvxTypes.h" +#include "dvxHash.h" #include "stb_ds_wrap.h" #include @@ -39,11 +41,21 @@ #define NO_TEXT (-1) // a node without a name or text #define HEX_DIGITS_U 4 // the digits of a \u escape -#define HEX_BASE 16 #define DECIMAL_BASE 10 -#define ASCII_LIMIT 0x80 // \u escapes below this become their character +#define ASCII_LIMIT 0x80 // characters from here on are written as \u escapes #define CONTROL_LIMIT 0x20 // characters below this need escaping -#define UNKNOWN_CHAR '?' // what other \u escapes become +#define UNKNOWN_CHAR '?' // what a character DVX does not have becomes +#define ESCAPE_U_LEAD 2 // the \u before an escape's digits +#define SURROGATE_HIGH 0xD800 // the first of a surrogate pair's first half +#define SURROGATE_LOW 0xDC00 // the first of a surrogate pair's second half +#define SURROGATE_LAST 0xDFFF +#define SURROGATE_BITS 10 // the bits of the code point each half holds +#define SURROGATE_BASE 0x10000 // the first code point a pair stands for +#define UTF8_CONT_MASK 0xC0 // a UTF-8 continuation byte is 10xxxxxx +#define UTF8_CONT_TAG 0x80 +#define UTF8_CONT_BITS 6 +#define UTF8_CONT_DATA 0x3F +#define UNICODE_MAX 0x10FFFF #define NUMBER_TEXT_MAX 64 // longest number read #define WORD_TRUE "true" #define WORD_FALSE "false" @@ -52,7 +64,7 @@ #define PATH_INDEX_OPEN '[' #define PATH_INDEX_CLOSE ']' #define PATH_STOPS ".[" // what ends a member name in a path -#define ESCAPE_TEXT_MAX 8 // the longest escape jsonQuote writes, \u00XX, and a NUL +#define ESCAPE_TEXT_MAX 8 // the longest escape jsonQuote writes, \uXXXX, and a NUL typedef struct { JsonTypeE type; @@ -63,6 +75,17 @@ typedef struct { bool truth; } NodeT; +// The lead bytes of UTF-8 sequences: a lead whose mask bits are tag +// starts one of more continuation bytes, the lead holding its data bits; +// a code point below min is overlong (not UTF-8). +typedef struct { + uint8_t mask; + uint8_t tag; + uint8_t data; + int32_t more; + uint32_t min; +} Utf8LeadT; + struct JsonDocS { NodeT *nodes; // stb_ds; node 0 is the root char *pool; // stb_ds: NUL-terminated strings @@ -80,12 +103,20 @@ typedef struct { DVX_ABI_STAMP(json); +static const Utf8LeadT sUtf8Leads[] = { + { 0xE0, 0xC0, 0x1F, 1, 0x80 }, + { 0xF0, 0xE0, 0x0F, 2, 0x800 }, + { 0xF8, 0xF0, 0x07, 3, 0x10000 } +}; + // ============================================================ // Prototypes // ============================================================ +static unsigned char charOf(uint32_t code); +static int32_t escapeU(ParserT *ps); static bool fail(ParserT *ps, const char *fmt, ...) __attribute__((format(printf, 2, 3))); -static int32_t hexValue(char c); +static int32_t hexQuad(ParserT *ps); bool jsonBool(const JsonDocT *doc, int32_t node); int32_t jsonChild(const JsonDocT *doc, int32_t node, int32_t index); int32_t jsonCount(const JsonDocT *doc, int32_t node); @@ -108,6 +139,40 @@ static int32_t parseValue(ParserT *ps, int32_t depth); static bool parseWord(ParserT *ps, const char *word); static void quotePut(char *out, int32_t outSize, int32_t *n, const char *piece); static void skipSpace(ParserT *ps); +static unsigned char utf8Char(ParserT *ps, unsigned char lead); + + +// The character a code point is in DVX text (UNKNOWN_CHAR when DVX has +// none) +static unsigned char charOf(uint32_t code) { + int32_t c = dfnTextFromUnicode(code); + + return (c >= 0) ? (unsigned char)c : (unsigned char)UNKNOWN_CHAR; +} + + +// A \u escape's code point, after its \u (a surrogate pair's second half, +// \u and all, taken with its first); -1 when its four hex digits are not +// there. +static int32_t escapeU(ParserT *ps) { + int32_t code = hexQuad(ps); + + if (code >= SURROGATE_HIGH && code < SURROGATE_LOW && ps->end - ps->p >= ESCAPE_U_LEAD && ps->p[0] == '\\' && ps->p[1] == 'u') { + const char *mark = ps->p; + + ps->p += ESCAPE_U_LEAD; + + int32_t low = hexQuad(ps); + + if (low >= SURROGATE_LOW && low <= SURROGATE_LAST) { + return SURROGATE_BASE + ((code - SURROGATE_HIGH) << SURROGATE_BITS) + (low - SURROGATE_LOW); + } + + ps->p = mark; + } + + return code; +} static bool fail(ParserT *ps, const char *fmt, ...) { @@ -126,20 +191,23 @@ static bool fail(ParserT *ps, const char *fmt, ...) { } -static int32_t hexValue(char c) { - if (c >= '0' && c <= '9') { - return c - '0'; +// Four hex digits, the code a \u escape holds; -1 when they are not +// there. +static int32_t hexQuad(ParserT *ps) { + int32_t code = 0; + + for (int32_t i = 0; i < HEX_DIGITS_U; i++) { + int32_t digit = ps->p < ps->end ? dvxHexValue((uint8_t)*ps->p) : -1; + + if (digit < 0) { + return -1; + } + + code = code * DVX_HEX_RADIX + digit; + ps->p++; } - if (c >= 'a' && c <= 'f') { - return c - 'a' + DECIMAL_BASE; - } - - if (c >= 'A' && c <= 'F') { - return c - 'A' + DECIMAL_BASE; - } - - return -1; + return code; } @@ -292,8 +360,8 @@ int32_t jsonQuote(const char *text, char *out, int32_t outSize) { break; default: - if (*c < CONTROL_LIMIT) { - snprintf(esc, sizeof(esc), "\\u%04x", (unsigned)*c); + if (*c < CONTROL_LIMIT || *c >= ASCII_LIMIT) { + snprintf(esc, sizeof(esc), "\\u%04x", (unsigned)dfnTextToUnicode(*c)); } else { esc[0] = (char)*c; esc[1] = '\0'; @@ -516,7 +584,9 @@ static bool parseString(ParserT *ps, int32_t *offset) { return fail(ps, "a string holds a line end or another control character"); } - if (c == '\\') { + if (c >= ASCII_LIMIT) { + c = utf8Char(ps, c); + } else if (c == '\\') { if (ps->p >= ps->end) { break; } @@ -550,20 +620,13 @@ static bool parseString(ParserT *ps, int32_t *offset) { break; case 'u': { - int32_t code = 0; + int32_t code = escapeU(ps); - for (int32_t i = 0; i < HEX_DIGITS_U; i++) { - int32_t digit = ps->p < ps->end ? hexValue(*ps->p) : -1; - - if (digit < 0) { - return fail(ps, "a \\u escape needs four hex digits"); - } - - code = code * HEX_BASE + digit; - ps->p++; + if (code < 0) { + return fail(ps, "a \\u escape needs four hex digits"); } - c = code < ASCII_LIMIT ? (unsigned char)code : (unsigned char)UNKNOWN_CHAR; + c = charOf((uint32_t)code); break; } @@ -693,3 +756,42 @@ static void skipSpace(ParserT *ps) { ps->p++; } } + + +// A byte from 0x80 on: when it leads a UTF-8 sequence, the sequence's +// character (UNKNOWN_CHAR when DVX has none), the sequence taken; else +// the byte itself, as DVX text. +static unsigned char utf8Char(ParserT *ps, unsigned char lead) { + for (int32_t k = 0; k < DVX_ARRAY_LEN(sUtf8Leads); k++) { + const Utf8LeadT *u = &sUtf8Leads[k]; + + if ((lead & u->mask) != u->tag) { + continue; + } + + if (ps->end - ps->p < u->more) { + return lead; + } + + uint32_t code = lead & u->data; + + for (int32_t i = 0; i < u->more; i++) { + unsigned char b = (unsigned char)ps->p[i]; + + if ((b & UTF8_CONT_MASK) != UTF8_CONT_TAG) { + return lead; + } + + code = (code << UTF8_CONT_BITS) | (b & UTF8_CONT_DATA); + } + + if (code < u->min || code > UNICODE_MAX || (code >= SURROGATE_HIGH && code <= SURROGATE_LAST)) { + return lead; + } + + ps->p += u->more; + return charOf(code); + } + + return lead; +} diff --git a/src/libs/kpunch/json/json.dhs b/src/libs/kpunch/json/json.dhs index 785a220d..d1bfd1da 100644 --- a/src/libs/kpunch/json/json.dhs +++ b/src/libs/kpunch/json/json.dhs @@ -56,7 +56,7 @@ jsonParse(text, len, err, errSize) reads a document and returns it (NULL when it jsonKey(doc, node) A member's name ("" for anything else). .endtable -Strings are held decoded: the escapes \n, \t, \", \\, \/, \b, \f, and \r become their characters, \u escapes below 128 their character and others '?'; other bytes, UTF-8 included, pass through. Arrays and objects nest JSON_DEPTH_MAX (64) deep at most. +Strings are held decoded into DVX text (code page 437): the escapes \n, \t, \", \\, \/, \b, \f, \r, and \u (surrogate pairs included) and UTF-8 sequences become their characters, with '?' for a character DVX does not have; other bytes pass through. Arrays and objects nest JSON_DEPTH_MAX (64) deep at most. .code char err[JSON_ERR_MAX]; @@ -73,4 +73,4 @@ jsonFree(doc); .h2 Writing -jsonQuote(text, out, outSize) writes text as a JSON string -- quotes, escapes, control characters as \u00XX -- and returns the length it needs (as snprintf does), so a NULL out with size 0 measures it. +jsonQuote(text, out, outSize) writes text as a JSON string -- quotes, escapes, control characters and characters from 128 on as \u escapes of their Unicode code points, so the result is plain ASCII -- and returns the length it needs (as snprintf does), so a NULL out with size 0 measures it. diff --git a/src/libs/kpunch/json/json.h b/src/libs/kpunch/json/json.h index fb7de20c..fbc4c0a6 100644 --- a/src/libs/kpunch/json/json.h +++ b/src/libs/kpunch/json/json.h @@ -33,10 +33,10 @@ // // Members are matched by exact name (the first when a name repeats). A // name holding '.' or '[' is reached with jsonChild instead. Strings are -// held decoded: \n, \t, \", \\, \/, \b, \f, \r, and \u escapes below 128 -// become their characters, \u escapes of other characters become '?', and -// other bytes (UTF-8 included) pass through unchanged. Numbers are -// doubles. +// held decoded into DVX text (code page 437): \n, \t, \", \\, \/, \b, +// \f, \r, \u escapes (surrogate pairs included), and UTF-8 sequences +// become their characters, '?' for a character DVX does not have; other +// bytes pass through unchanged. Numbers are doubles. #ifndef JSON_H #define JSON_H diff --git a/src/libs/kpunch/libdvx/Makefile b/src/libs/kpunch/libdvx/Makefile index f51e2461..71ef08ee 100644 --- a/src/libs/kpunch/libdvx/Makefile +++ b/src/libs/kpunch/libdvx/Makefile @@ -33,10 +33,9 @@ CFLAGS = $(DOS_CFLAGS) -I. -Iplatform -I../libtasks -Ithirdparty OBJDIR = ../../../../obj/core LIBSDIR = ../../../../bin/libs -# Core sources. dvxPrefs.c is not among them on DOS: the loader needs it -# before libdvx loads, so dvx.exe links it and exports prefs* to every DXE. -SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxImage.c dvxImageWrite.c dvxDriver.c \ - dvxApp.c dvxBas.c dvxDialog.c dvxPrefsBas.c dvxResource.c dvxText.c dvxTextBas.c dvxFontFmt.c dvxFlow.c \ +# Core sources +SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxImage.c dvxDriver.c \ + dvxApp.c dvxBas.c dvxDialog.c dvxPrefs.c dvxPrefsBas.c dvxResource.c dvxResourceBas.c dvxText.c dvxTextBas.c dvxFontFmt.c dvxFlow.c \ widgetClass.c widgetCore.c widgetScrollbar.c \ widgetLayout.c widgetEvent.c widgetOps.c widgetIface.c @@ -57,7 +56,7 @@ TARGET = $(TARGETDIR)/libdvx.lib HOSTOBJDIR = $(HOST_OBJROOT) HOSTTARGET = $(HOSTOBJDIR)/libdvx.a HOSTCFLAGS = $(HOST_CFLAGS) -I. -Iplatform -I../libtasks -Ithirdparty -HOST_SRCS = $(SRCS) dvxPrefs.c platform/dvxPlatformHost.c platform/dvxPlatformIrq.c platform/dvxMemTrack.c platform/dvxPlatformUtil.c thirdparty/stb_ds_impl.c +HOST_SRCS = $(SRCS) platform/dvxPlatformHost.c platform/dvxPlatformIrq.c platform/dvxMemTrack.c platform/dvxPlatformUtil.c thirdparty/stb_ds_impl.c HOST_OBJS = $(patsubst %.c,$(HOSTOBJDIR)/libdvx/%.o,$(HOST_SRCS) $(notdir $(TOOL_SRCS))) .PHONY: all clean host host-clean diff --git a/src/libs/kpunch/libdvx/README.md b/src/libs/kpunch/libdvx/README.md index 39c389e7..63e603ad 100644 --- a/src/libs/kpunch/libdvx/README.md +++ b/src/libs/kpunch/libdvx/README.md @@ -31,13 +31,15 @@ Public headers live alongside implementation: - `dvxDlg.h` / `dvxDialog.c` -- message/input/choice/file dialogs - `dvxRes.h` / `dvxResource.c` -- .res resource system - `dvxMem.h` -- memory allocation wrappers -- `dvxPrefs.h` / `.c` -- INI-format preferences (on DOS compiled into dvx.exe, not libdvx.lib, and exported to every module) +- `dvxEndian.h` -- little- and big-endian loads and stores (header only) +- `dvxHash.h` -- FNV-1a, CRC-16, and hex digits (header only) +- `dvxPrefs.h` / `.c` -- INI-format preferences - `dvxFont.h` -- bundled 8x16 bitmap font - `dvxCur.h` -- mouse cursor bitmaps - `dvxPal.h` -- 16-color CGA palette - `widgetCore.c` / `widgetLayout.c` / `widgetEvent.c` / `widgetClass.c` -- widget runtime helpers shared across all widget plugins -- `dvxImage.c` / `dvxImageWrite.c` -- image loader/writer (BMP, PNG via stb) +- `dvxImage.c` -- image codec (stb_image / stb_image_write, static, behind dvxImgInt.h) - `platform/dvxPlat.h` -- platform-layer interface (implemented per target) ## Build diff --git a/src/libs/kpunch/libdvx/apiref.dhs b/src/libs/kpunch/libdvx/apiref.dhs index eb13085a..d45e47ee 100644 --- a/src/libs/kpunch/libdvx/apiref.dhs +++ b/src/libs/kpunch/libdvx/apiref.dhs @@ -52,6 +52,8 @@ The DVX GUI is built as a five-layer architecture. Each layer is defined in its .item dvxRes.h -- DXE-embedded resource system .item dvxPrefs.h -- INI preferences system .item dvxMem.h -- Per-app memory tracking +.item dvxEndian.h -- Byte order loads and stores +.item dvxHash.h -- Hashes, checksums, and hex digits .item dvxDrv.h -- Driver modules .item dvxWgt.h -- Widget system public API .item dvxWgtP.h -- Widget plugin API @@ -73,6 +75,8 @@ The DVX GUI is built as a five-layer architecture. Each layer is defined in its .link api.resource dvxRes.h -- Resource System .link api.prefs dvxPrefs.h -- Preferences .link api.mem dvxMem.h -- Memory Tracking +.link api.endian dvxEndian.h -- Byte Order +.link api.hash dvxHash.h -- Hashes and Checksums .link api.driver dvxDrv.h -- Driver Modules .link api.widget dvxWgt.h -- Widget System .link api.widgetplugin dvxWgtP.h -- Widget Plugin API @@ -333,7 +337,7 @@ Callbacks: .table Callback Description -------- ----------- - onPaint(WindowT *win, RectT *dirtyArea) Content repaint requested + onPaint(WindowT *win, RectT *dirtyArea) Content repaint requested; a painter that changed only part of dirtyArea may narrow it, and only that part is composited onKey(WindowT *win, int32_t key, int32_t mod) Key press onKeyUp(WindowT *win, int32_t scancode, int32_t mod) Key release onMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) Mouse event (content-relative) @@ -646,6 +650,10 @@ Legacy alias for backward compatibility with code that predates multi-cursor sup .index Font .index CP437 .index BitmapFontT +.index dfnCp437 +.index dfnFromUnicode +.index dfnTextFromUnicode +.index dfnTextToUnicode .h1 dvxFont.h -- Embedded Bitmap Font Data @@ -703,6 +711,8 @@ const BitmapFontT struct describing the 8x16 font, ready to pass to drawText/dra .index dvxFontRescan .index dvxFontRetain .index dvxFontSetSmooth +.index dvxFontSmoothBegin +.index dvxFontRebuildStep .index dvxFontSmooth .index dvxFontAddFile .index dvxFontAddMemory @@ -822,14 +832,16 @@ void dvxFontRescan(void); Read the FONTS folder again on the next call, after fonts were added or removed. The fonts no one holds are unloaded; open fonts stay valid. -.h2 dvxFontSetSmooth / dvxFontSmooth +.h2 dvxFontSetSmooth / dvxFontSmooth / dvxFontSmoothBegin / dvxFontRebuildStep .code void dvxFontSetSmooth(bool smooth); bool dvxFontSmooth(void); +void dvxFontSmoothBegin(bool smooth); +bool dvxFontRebuildStep(void); .endcode -Smooth fonts where their face has the size smoothed, or stop; dvxFontSmooth says which it is (off at first). The fonts open are built again in place: the same BitmapFontT pointers with new glyphs and a new serial, so code that keeps measurements of a font compares its serial too, and text is laid out again. The fonts no one holds are unloaded. Programs call dvxSetFontSmoothing (dvxApp.h) instead, which follows the user's choice and the screen's depth. +Smooth fonts where their face has the size smoothed, or stop; dvxFontSmooth says which it is (off at first). The fonts open are built again in place: the same BitmapFontT pointers with new glyphs and a new serial, so code that keeps measurements of a font compares its serial too, and text is laid out again. The fonts no one holds are unloaded. dvxFontSetSmooth builds them all before it returns; dvxFontSmoothBegin only switches and marks them, and each dvxFontRebuildStep builds one of them again (15-150 ms a font), false when none is left (the fonts before the fonts with lines, which copy them). Programs call dvxSetFontSmoothing (dvxApp.h) instead, which follows the user's choice and the screen's depth and builds the fonts a frame at a time. .h2 dvxFontAddFile / dvxFontAddMemory @@ -870,6 +882,17 @@ The texts that go with the face font was built from: its notice -- the family an Returns: The texts; NULL for the system font, and when the font's file cannot be read. +.h2 Code Page 437 and Unicode + +.code +extern const uint16_t dfnCp437[DFN_CHAR_COUNT]; +int32_t dfnFromUnicode(uint32_t code); +int32_t dfnTextFromUnicode(uint32_t code); +uint32_t dfnTextToUnicode(uint8_t ch); +.endcode + +Declared in dvxFontFmt.h. DVX text and its fonts are code page 437. dfnCp437[c] is the Unicode code point of character c's glyph (0 for none), so the characters below 32 are the smileys, card suits, and arrows the fonts draw for them; dfnFromUnicode is the character whose glyph a code point is, -1 for none. For text, dfnTextFromUnicode and dfnTextToUnicode keep ASCII (line ends, tabs, and the other controls included) as itself and map the rest through code page 437; dfnTextFromUnicode is -1 for a code point DVX has no character for. The JSON library reads and writes Unicode this way, and BASIC's ChrW$ and AscW are these two. + .topic api.flow .title dvxFlow.h -- Flowing Text .toc 1 dvxFlow.h -- Flowing Text @@ -2799,6 +2822,9 @@ Install an icon that is already in display pixel format (for example from dvxLoa .index dvxLoadImageFromMemory .index dvxFreeImage .index dvxImageInfo +.index dvxImageInfoFromMemory +.index dvxSetWaitPump +.index dvxWaitPump .index dvxSaveImage .index dvxScreenshot .index dvxWindowScreenshot @@ -2825,6 +2851,7 @@ Install an icon that is already in display pixel format (for example from dvxLoa .index platformPathAbsolute .index DOS programs .index dvxExclusiveEnd +.index dvxExclusiveMirror .index dvxExclusiveDisplay .index dvxExclusiveBlitOps .index dvxExclusiveFlush @@ -3397,7 +3424,7 @@ Returns: Pointer to the VideoModeInfoT array. void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b); .endcode -Set a single color by ID. Repacks to native pixel format and invalidates the entire screen. +Set a single color by ID. Repacks to native pixel format; every window is painted again at the next frame (once, however many colors change before it) and the whole screen composited. .table Parameter Description @@ -3577,7 +3604,7 @@ The [desktop] wallpaper and mode keys of an INI handle (mode through prefsWallpa void dvxSetFontSmoothing(AppContextT *ctx, bool on); .endcode -Smooth text, or stop (ctx->fontSmoothing; on after dvxInit). Text is smoothed when on and the screen has more than 8 bits per pixel (dvxFontSetSmooth, dvxText.h), in the faces and sizes that have a smoothed strike; dvxChangeVideoMode applies it again at the new depth. When what is drawn changes, every window is laid out and painted again. +Smooth text, or stop (ctx->fontSmoothing; on after dvxInit). Text is smoothed when on and the screen has more than 8 bits per pixel (dvxFontSmoothBegin, dvxText.h), in the faces and sizes that have a smoothed strike; dvxChangeVideoMode applies it again at the new depth. The fonts open are built again a font a frame (dvxUpdate calls dvxFontRebuildStep), and every window is laid out and painted again after the last. .h3 dvxFontsFromPrefs / dvxFontsToPrefs @@ -3683,6 +3710,17 @@ void dvxSetSoundHook(DvxSoundHookT fn); The function that plays the sounds, set by the sound library when it loads (NULL: silent). Programs do not call it. +.h3 dvxSetWaitPump / dvxWaitPump + +.code +typedef bool (*DvxWaitPumpT)(void); + +void dvxSetWaitPump(DvxWaitPumpT fn); +bool dvxWaitPump(void); +.endcode + +A library that waits for something (a sound to end, as dvxPlaySoundWait does) calls dvxWaitPump between its checks: DVX goes on meanwhile, and so do the events of the BASIC program that waits, as in DoEvents (the hook the BASIC runtime sets with dvxSetWaitPump). It returns false when the wait should stop: the program is ending, or its code may not wait (a control written in BASIC). Without a hook it yields the processor and returns true. + .h2 Full-Screen Exclusive Mode A window's program takes the whole screen in a video mode of its own (a game at 320x200, say) while every other program keeps running: windows keep painting into their own buffers, and the desktop is drawn again when the program lets go. The owner's window must have the focus: when it loses it (Alt+Tab, Ctrl+Esc, a dialog, another window coming to the front) the desktop comes back and onChange hears ExclusiveSuspendedE; when the window has the focus again the mode is set again and onChange hears ExclusiveActiveE (draw the whole frame: the screen and the backbuffer start black). While active every key but Alt+Tab, Ctrl+Esc, and Alt+F4 goes to the window as in game input mode, the mouse cursor is not drawn, onMouse gets full-screen coordinates, dvxScreenshot saves the full screen, and dvxChangeVideoMode fails. Closing the window, the program ending or being ended, and dvxExclusiveEnd give the screen back. See the Writing Games guide. @@ -3715,6 +3753,16 @@ void dvxExclusiveEnd(AppContextT *ctx, WindowT *win); Give the screen back: the desktop's mode, palette, and mouse, and a full repaint. onChange is not called. Nothing when win does not have full screen. +.h3 dvxExclusiveMirror + +.code +int32_t dvxExclusiveMirror(AppContextT *ctx, WindowT *win, ExclusiveFnT onChange, void *userData); +.endcode + +Full screen for a window's own content (a window of widgets, a drawing) rather than a picture the program draws: the smallest mode of at least the content's size in the desktop's pixel format, the content scaled by the largest whole factor that fits and centred on black, and shown again whenever the window paints (any dirty rectangle over its content). The mouse works on the content as in the window -- onMouse gets content coordinates -- and its cursor is drawn, in the shape the content asks for. Keys, suspending, resuming, and giving back are as for dvxExclusiveBegin; the BASIC form runtime's Form.FullScreen is this. + +Returns: as dvxExclusiveBegin; DVX_EXCL_ERR_MODE also when there is no mode in the desktop's pixel format. + .h3 dvxExclusiveDisplay / dvxExclusiveBlitOps .code @@ -3775,7 +3823,7 @@ platformPathAbsolute makes a path absolute against the current folder (it need n bool dvxLibraryLoaded(const char *name); .endcode -Whether the library (or widget) name, its file's base name in any case ("pkgnet", "dvxsql"), is loaded: installed and started with DVX, which loads every library and widget it has at start. It looks for the module's ABI stamp (DVX_ABI_STAMP: name and DVX_ABI_SUFFIX), which the loader checked before it took the module; a module that is not there, or did not load, has none. Some parts of DVX are optional (Setup's "What to install" page installs them as bundles: src/packages.ini), so a program that uses one of their libraries only when it is there asks first, and does not import it: an import of a missing library stops the program from loading. BASIC: SysLibraryLoaded (system.bas). +Whether the library (or widget) name, its file's base name in any case ("pkgnet", "dvxsql"), is installed and loaded. DVX loads a library when something first needs it, so asking loads one that is installed but not loaded yet. It looks for the module's ABI stamp (DVX_ABI_STAMP: name and DVX_ABI_SUFFIX), which the loader checks before it takes the module; a module that is not there, or did not load, has none. Some parts of DVX are optional (Setup's "What to install" page installs them as bundles: src/packages.ini), so a program that uses one of their libraries only when it is there asks first, and does not import it: an import of a missing library stops the program from loading. BASIC: SysLibraryLoaded (system.bas). .h2 Idle Time: dvxIdleMs @@ -4005,6 +4053,8 @@ uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, i Load an image file (BMP, PNG, JPEG, GIF) and convert to the display's native pixel format. A file that is no image but a DVX program, widget, or library with an icon resource gives that icon (dvxResLoadIconData), so every picture that takes a file name can show a program's icon. Caller must free with dvxFreeImage(). +Called from a program's own task (where the task takes no input: a BASIC program), the image loaders decode a big picture a part at a time, a frame of DVX's every 32 KB read, so the other programs go on; none of the caller's code runs meanwhile. On the task that takes the input they decode in one go. + .table Parameter Description --------- ----------- @@ -4078,7 +4128,7 @@ Free a pixel buffer returned by dvxLoadImage() or dvxLoadImageFromMemory(). bool dvxImageInfo(const char *path, int32_t *outW, int32_t *outH); .endcode -Query image dimensions without decoding the full file. +Query image dimensions without decoding the full file (its header only: a picture damaged after it may still fail to load). .table Parameter Description @@ -4089,6 +4139,14 @@ Query image dimensions without decoding the full file. Returns: true on success; false for an unreadable image or one without pixels. +.h3 dvxImageInfoFromMemory + +.code +bool dvxImageInfoFromMemory(const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH); +.endcode + +dvxImageInfo for an image's bytes in memory (as LoadPicture checks a picture before a control shows it). + .h3 dvxSaveImage .code @@ -4445,6 +4503,8 @@ A library that has drivers puts them in drivers// in its source folder; it .index wgtValueDouble .index WGT_IFACE_DOUBLE .index wgtValueStr +.index wgtValueBytes +.index wgtValueLen .index wgtValueToInt .index wgtValueToBool .index wgtValueToFloat @@ -5410,6 +5470,7 @@ A widget class whose properties and methods are not C functions with fixed signa .code typedef struct { uint8_t type; // WGT_IFACE_* + int32_t len; // STRING: its length when it may hold NUL bytes; 0 = up to its NUL union { int32_t i; // INT, BOOL (0 / 1), ENUM double f; // FLOAT, DOUBLE @@ -5418,7 +5479,7 @@ typedef struct { } WgtValueT; .endcode -One property value, method argument or result, or event argument. WGT_IFACE_NONE (5) is "no value" (a void method's result). A STRING value never owns its text: an argument's text belongs to the caller for the duration of the call; a returned one (a property read, a method result) belongs to the widget and stays valid until the next call on that widget or until the widget changes or is destroyed -- copy it to keep it. wgtValueInt, wgtValueBool, wgtValueFloat, wgtValueDouble, and wgtValueStr (NULL becomes "") build values. FLOAT is BASIC's Single, DOUBLE its Double: a DOUBLE property or argument keeps every digit (a value through FLOAT is rounded to a float by a static widget's getter and setter, and the form runtime shows it as a Single); as text a DOUBLE has 15 significant digits. +One property value, method argument or result, or event argument. WGT_IFACE_NONE (5) is "no value" (a void method's result). A STRING value never owns its text: an argument's text belongs to the caller for the duration of the call; a returned one (a property read, a method result) belongs to the widget and stays valid until the next call on that widget or until the widget changes or is destroyed -- copy it to keep it. wgtValueInt, wgtValueBool, wgtValueFloat, wgtValueDouble, and wgtValueStr (NULL becomes "") build values; wgtValueBytes(s, len) builds a STRING of len bytes that may hold NUL bytes (s[len] must still be a NUL). A BASIC string holds any bytes, so the form runtime passes every string this way, and a STRING's length is wgtValueLen(v): its len, or up to its NUL when len is 0. A widget that takes binary text (Terminal Write) reads that length; one that wants C text reads s, which stops at the first NUL. FLOAT is BASIC's Single, DOUBLE its Double: a DOUBLE property or argument keeps every digit (a value through FLOAT is rounded to a float by a static widget's getter and setter, and the form runtime shows it as a Single); as text a DOUBLE has 15 significant digits. .h3 wgtValueToInt / wgtValueToBool / wgtValueToFloat / wgtValueToText @@ -6161,6 +6222,7 @@ Returns: 0 on success, -1 if not found, the name is too long, or I/O failed. .index Preferences .index PrefsHandleT .index prefsLoad +.index prefsDvxIniShared .index prefsSave .index prefsGetString .index prefsGetInt @@ -6220,6 +6282,14 @@ Create an empty preferences handle with no associated file. Use this when buildi Returns: New handle, or NULL on allocation failure. +.h3 prefsDvxIniShared + +.code +PrefsHandleT *prefsDvxIniShared(void); +.endcode + +DVX.INI for reading, shared: parsed again only when the file's text changed (its text is read and compared, a fraction of a parse), for code that reads settings often (every sound, every song). Its users do not close it, change it, or keep it past their own call: the next call may read the file again. + .h3 prefsLoad .code @@ -6506,10 +6576,12 @@ Return the index of the choice named by the key's value; the index of defaultNam .index dvxStrdup .index dvxMemGetAppUsage .index dvxMemResetApp +.index dvxMemSetApp +.index DVX_MEM_SYSTEM .h1 dvxMem.h -- Per-App Memory Tracking -Wraps malloc/free/calloc/realloc/strdup with a 16-byte header that records the owning app ID and allocation size. The Task Manager displays per-app memory usage, and leaks are detected when an app is terminated. +Wraps malloc/free/calloc/realloc/strdup with a 16-byte header that records the owning app ID and allocation size. The Task Manager displays per-app memory usage. The accounting is all it is: when an app ends its count starts over, and nothing it allocated is freed for it. DXE code does NOT need to include this header for tracking to work. The DXE export table maps malloc/free/calloc/realloc/strdup to these wrappers transparently. Call sites that need the explicit dvxXxx names (e.g. dvxMemGetAppUsage) or the dvxMemAppIdPtr declaration should include dvxMem.h directly. @@ -6589,7 +6661,7 @@ Returns: Bytes allocated for this app. void dvxMemResetApp(int32_t appId); .endcode -Free every tracked allocation charged to the given app. Called by the shell after an app exits or crashes to reclaim its memory. +Start the given app's accounting over: its usage reads 0, and a block it allocated that is freed later is not subtracted again (the block carries the old generation). Nothing is freed: a library's buffers made while the app ran stay valid. The shell calls it when an app ends. .table Parameter Description @@ -6597,6 +6669,97 @@ Free every tracked allocation charged to the given app. Called by the shell afte appId App ID to flush .endtable +.h3 dvxMemSetApp + +.code +int32_t dvxMemSetApp(int32_t appId); +.endcode + +Make appId the current app (the one what is allocated from now is charged to, as the shell sees it too) and return the one before, to set back afterwards. A library keeps its own memory as DVX's (DVX_MEM_SYSTEM, 0) whichever app's call made it, so the Task Manager does not charge a cache or a font to the program that happened to ask first; the audio library calls a program's callback as that program. Without a shell (host tools) nothing changes and 0 comes back. + +.code +int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); +// ... allocations that belong to the library ... +dvxMemSetApp(prev); +.endcode + +.topic api.endian +.title dvxEndian.h -- Byte Order +.toc 1 dvxEndian.h -- Byte Order +.index dvxEndian.h +.index dvxGetLe16 +.index dvxGetLe32 +.index dvxGetBe16 +.index dvxGetBe32 +.index dvxPutLe16 +.index dvxPutLe32 +.index dvxPutBe32 + +.h1 dvxEndian.h -- Byte Order + +File formats and wire protocols fix their byte order. These inline functions read and write it a byte at a time, so they work at any alignment and on any computer. Header only: include it and call, nothing to link. + +.code +uint16_t dvxGetBe16(const uint8_t *p); +uint32_t dvxGetBe32(const uint8_t *p); +uint16_t dvxGetLe16(const uint8_t *p); +uint32_t dvxGetLe24(const uint8_t *p); +uint32_t dvxGetLe32(const uint8_t *p); +void dvxPutBe32(uint8_t *p, uint32_t v); +void dvxPutLe16(uint8_t *p, uint32_t v); +void dvxPutLe32(uint8_t *p, uint32_t v); +.endcode + +Get reads the value at p, high byte first (Be) or low byte first (Le); Put writes v at p the same way (dvxPutLe16 writes the low 16 bits of v). Cast a load to a signed type for a signed field, for example (int16_t)dvxGetLe16(p) for a 16-bit sample. + +.topic api.hash +.title dvxHash.h -- Hashes and Checksums +.toc 1 dvxHash.h -- Hashes and Checksums +.index dvxHash.h +.index dvxCrc16 +.index dvxFnvByte +.index dvxFnvBytes +.index dvxHexValue +.index DVX_FNV_BASIS +.index DVX_HEX_DIGITS + +.h1 dvxHash.h -- Hashes, Checksums, and Hex Digits + +Small inline functions every part of DVX uses for the same jobs: FNV-1a for hash tables and change detection, CRC-16-CCITT for serial packets and XMODEM / ZMODEM, and hex digits both ways. Header only: include it and call, nothing to link. + +.h2 FNV-1a + +.code +#define DVX_FNV_BASIS 2166136261u +#define DVX_FNV_PRIME 16777619u + +uint32_t dvxFnvByte(uint32_t hash, uint8_t byte); +uint32_t dvxFnvBytes(uint32_t hash, const void *data, int32_t len); +.endcode + +Start from DVX_FNV_BASIS and fold in bytes. dvxFnvByte takes one, so a caller can fold case or skip characters as it goes; dvxFnvBytes takes len bytes. Both return the hash carried on. + +.h2 CRC-16-CCITT + +.code +uint16_t dvxCrc16(uint16_t crc, const uint8_t *data, int32_t len); +.endcode + +The CRC of len bytes (polynomial 0x1021, high bit first), carried on from crc, computed a byte at a time without a table. Seed 0 for XMODEM and ZMODEM, 0xFFFF for CCITT-FALSE (the serial packet layer's). + +.h2 Hex Digits + +.code +#define DVX_HEX_DIGITS "0123456789abcdef" +#define DVX_HEX_RADIX 16 +#define DVX_NIBBLE_BITS 4 +#define DVX_NIBBLE_MASK 0x0F + +int32_t dvxHexValue(int32_t c); +.endcode + +DVX_HEX_DIGITS[n] is the digit for the nibble n. dvxHexValue is a digit's value (0 to 15, either case), -1 for any other character. + .topic api.widgetplugin .title dvxWgtP.h -- Widget Plugin API .toc 1 dvxWgtP.h -- Widget Plugin API @@ -7054,6 +7217,8 @@ stb_ds dynamic array. Each widget DXE appends its class pointer via wgtRegisterC .index platformPathTidy .index Path Separators .index platformPathSibling +.index platformPathParent +.index platformPathIsRoot .index platformReplaceFile .index platformFilterMatch .index platformDriveList @@ -7070,6 +7235,8 @@ stb_ds dynamic array. Each widget DXE appends its class pointer via wgtRegisterC .index platformValidateShortName .index platformLongNameChar .index platformStatSize +.index platformFileStamp +.index PlatformStampT .index platformShortPath .index PLATFORM_LONG_NAME_MAX .index Long File Names @@ -7198,7 +7365,7 @@ Monotonic tick counter for UI timing (double-click, tooltip delay, cursor blink, uint64_t platformEntropyStamp(void); .endcode -Fine-grained, free-running timestamp for input-timing entropy. It is not a clock: units and wrap point are backend-specific. On DOS it returns the CPU time-stamp counter (RDTSC) when CPUID reports one, otherwise the BIOS tick count shifted left 16 bits with the live PIT channel 0 counter (about 0.84 us resolution) in the low 16 bits. Used by dvxSetInputEntropyHook. +Fine-grained, free-running timestamp for entropy. It is not a clock: units and wrap point are backend-specific. On DOS it reads the live PIT channel 0 counter (about 0.84 us resolution, its own crystal) and returns it in the top 16 bits of the CPU time-stamp counter (RDTSC) when CPUID reports one, so a sample carries the jitter between the two clocks; otherwise the BIOS tick count shifted left 16 bits with the PIT counter in the low 16 bits. Used by dvxSetInputEntropyHook and by the security library's entropy gathering. .h3 platformMicros @@ -7502,6 +7669,19 @@ 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. +.h3 platformFileStamp + +.code +typedef struct { + uint32_t size; // bytes (a file of 4 GB or more: 0xFFFFFFFF) + uint32_t time; // its date and time, as the platform keeps them +} PlatformStampT; + +bool platformFileStamp(const char *path, PlatformStampT *out); +.endcode + +A file's size and time from its folder alone, for telling whether a file read before has changed: compare two stamps for equality only. On DOS it is one directory search; stat makes several DOS calls there, tens of milliseconds each on a slow disk. Elsewhere it is stat, with the time's nanoseconds folded in. False when path is no file, or holds a wildcard. DOS keeps a file's time to two seconds, so a file rewritten at the same size within two seconds keeps its stamp. The shell's program cache checks the programs it keeps with it. + .h3 platformMkdirRecursive .code @@ -7539,6 +7719,8 @@ DVX paths follow DOS rules on every platform: '/' and '\\' are the same separato .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 platformPathParent: the folder holding path, a trailing separator ignored, in native separators ("C:/DVX/APPS/" gives "C:\\DVX", "/a" gives "/"). False for a root, a name without a folder, or when it does not fit. +.item platformPathIsRoot: true for a drive or file-system root: "C:", "C:\\", "/", or "\\". .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 @@ -7604,6 +7786,18 @@ bool platformWriteFile(const char *path, const void *data, int32_t len); Write len bytes of data as the whole of path, all or nothing: into the PLATFORM_TMP_EXT sibling, then swapped in with platformReplaceFile, so a full disk or an I/O error leaves the file as it was. False when it was not done. The preferences system, the IDE's saves, Setup, and the package records use it. +.index platformWalkDir + +.h3 platformWalkDir + +.code +typedef bool (*PlatformWalkFnT)(const char *path, const char *name, void *ctx); + +bool platformWalkDir(const char *dir, PlatformWalkFnT fn, void *ctx); +.endcode + +Visit every file below dir, depth first, entering folders as they come and skipping names that start with '.'. A folder's names are read in full before any is visited (DOS has few file handles), and which are folders comes with them (platformDirEntryIsDir), not from a stat of each. fn gets each file's full path and its name; returning false stops the walk, and platformWalkDir then returns false too. A folder that cannot be read is skipped. The loader's module and help scans and the shell's program list use it. + .h3 platformReadFile .code @@ -7715,7 +7909,7 @@ The DVX folder (the working directory) holds the program, which Setup installs, Only packages put files in SHARED: DVX's own, and add-on packages (pkg.lib), each in its vendor's folders. A program reads it but never writes or installs into it; a program's own files live with it (its DATA folder, or resources inside the .app), under its own vendor and program folder, so two programs' files never collide. -platformUserInit, called once by the loader, decides the user folder: the folder the DVXUSER environment variable names, else USER in the DVX folder, made when missing. When it cannot be written (DVX on a CD-ROM without DVXUSER), DVXUSER in the TEMP folder is used instead and platformUserInTemp is true; without a TEMP folder nothing can be saved and platformIsReadOnly is true. The first start copies SYSTEM\DVX.INI to the user folder. platformUserDir is the folder (DVXUSER or USER before platformUserInit has decided); platformUserPath joins a name to it; platformDvxIniPath is its DVX.INI. platformUserDirOf is the user folder of a DVX installed in another folder (DVXUSER, else that folder's USER), for Setup. +platformUserInit, called once by the loader (and not exported to modules), decides the user folder: the folder the DVXUSER environment variable names, else USER in the DVX folder, made when missing. When it cannot be written (DVX on a CD-ROM without DVXUSER), DVXUSER in the TEMP folder is used instead and platformUserInTemp is true; without a TEMP folder nothing can be saved and platformIsReadOnly is true. The first start copies SYSTEM\DVX.INI to the user folder. platformUserDir is the folder (DVXUSER or USER before platformUserInit has decided); platformUserPath joins a name to it; platformDvxIniPath is its DVX.INI. platformUserDirOf is the user folder of a DVX installed in another folder (DVXUSER, else that folder's USER), for Setup. platformAppUserDir is a program's folder in the user folder: APPS\KPUNCH\TERMINAL\TERMINAL.APP gives USER\APPS\KPUNCH\TERMINAL; a program outside the DVX folder gives USER\APPS\. It is not made here (the shell's userDir and BASIC's App.User are made on first use). @@ -7807,7 +8001,7 @@ Append arg to the command line in buf at pos as one argument that dvxNextArg rea void platformRegisterDxeExports(void); .endcode -Register platform and C runtime symbols with the dynamic module loader so DXE modules can resolve them at load time. +Register platform and C runtime symbols with the dynamic module loader so DXE modules can resolve them at load time. The loader's own, so not exported to modules. .h3 platformRegisterSymOverrides @@ -7815,7 +8009,7 @@ Register platform and C runtime symbols with the dynamic module loader so DXE mo void platformRegisterSymOverrides(const PlatformSymOverrideT *entries); .endcode -Register function pointer overrides for subsequently loaded modules. entries is terminated by {NULL, NULL}. On DOS this wraps dlregsym(). Nothing in DVX itself calls it: it is SDK API for embedders and test harnesses that need to substitute a symbol (e.g. a stub) before loading a module. +Register function pointer overrides for subsequently loaded modules. entries is terminated by {NULL, NULL}. On DOS this wraps dlregsym(). Nothing in DVX itself calls it: it is SDK API for embedders and test harnesses that need to substitute a symbol (e.g. a stub) before loading a module, so code linked with the platform layer, not a module (it is not exported). .h2 Interrupt-Safe Memory diff --git a/src/libs/kpunch/libdvx/arch.dhs b/src/libs/kpunch/libdvx/arch.dhs index ed6d5577..ccf5d54a 100644 --- a/src/libs/kpunch/libdvx/arch.dhs +++ b/src/libs/kpunch/libdvx/arch.dhs @@ -554,7 +554,8 @@ Explicit use (e.g. in the Task Manager) can include dvxMem.h to call: Function Purpose -------- ------- dvxMemGetAppUsage Query current bytes allocated for an app - dvxMemResetApp Free every tracked allocation charged to an app + dvxMemResetApp Start an app's accounting over (nothing is freed) + dvxMemSetApp Make an app the current one (a library's own memory: DVX_MEM_SYSTEM) .endtable The dvxMemAppIdPtr pointer is set by the shell to &ctx->currentAppId so the allocator always knows which app to charge. @@ -577,7 +578,7 @@ DVX uses DJGPP's DXE3 (Dynamic eXtension) format for all loadable modules. DXE3 Extension Directory Purpose Examples --------- --------- ------- -------- .lib LIBS/ Core libraries loaded first. Provide infrastructure APIs. Each exports an ABI stamp. libtasks.lib, libdvx.lib, dvxshell.lib - .wgt WIDGETS/ Widget type plugins. Each exports wgtRegister() and an ABI stamp. button.wgt, listview.wgt, terminal.wgt + .wgt WIDGETS/ Widget type plugins. Each exports wgtRegister() and an ABI stamp. button.wgt, listview.wgt, ansiterm.wgt .app APPS/*/ Application modules. Each exports appDescriptor (which records its ABI) and appMain(). Loaded on demand by the shell. progman.app, notepad.app, cpanel.app .endtable @@ -586,7 +587,7 @@ DVX uses DJGPP's DXE3 (Dynamic eXtension) format for all loadable modules. DXE3 .index DVX_ABI_VERSION .index ABI stamp -DVX_ABI_VERSION (dvxTypes.h) is one number for the binary interface between all modules. Each .lib and .wgt exports AbiVersion (written by DVX_ABI_STAMP(baseName) in its main source file); each app's AppDescriptorT starts with abiVersion (SHELL_APP_DESCRIPTOR fills it); each widget API struct starts with a WgtApiHeaderT; each widget class carries WGT_CLASS_VERSION, which is the same number. The loader checks every .lib and .wgt right after loading it and stops the boot with a message naming a module whose stamp is missing or different; the shell refuses a mismatched app with a message box; wgtGetApi and wgtRegisterClass refuse mismatched tables and classes. Details always go to DVX.LOG. The bump rule and the size-table test that enforces it are described with DVX_ABI_VERSION in the API reference (dvxTypes.h). +DVX_ABI_VERSION (dvxTypes.h) is one number for the binary interface between all modules. Each .lib and .wgt exports AbiVersion (written by DVX_ABI_STAMP(baseName) in its main source file); each app's AppDescriptorT starts with abiVersion (SHELL_APP_DESCRIPTOR fills it); each widget API struct starts with a WgtApiHeaderT; each widget class carries WGT_CLASS_VERSION, which is the same number. The loader checks every .lib and .wgt right after loading it: at the start it stops the boot with a message naming a module whose stamp is missing or different, and a library loaded later (see Boot Sequence) is refused and logged; the shell refuses a mismatched app with a message box; wgtGetApi and wgtRegisterClass refuse mismatched tables and classes. Details always go to DVX.LOG. The bump rule and the size-table test that enforces it are described with DVX_ABI_VERSION in the API reference (dvxTypes.h). .h2 Boot Sequence @@ -601,19 +602,30 @@ DVX_ABI_VERSION (dvxTypes.h) is one number for the binary interface between all | +-- Read .dep files for each module (dependency base names) | - +-- Topological sort: load modules in dependency order - | - dlopen() with RTLD_GLOBAL - | - ABI stamp checked; a mismatch stops the boot - | - Each .wgt that exports wgtRegister() has it called + +-- Index the names each library exports (its file's DXE header and + | export table only; a hash and the library for each name) | - +-- Find and call shellMain() (exported by dvxshell.lib) + +-- Load, each after the libraries it needs: + | - the libraries with constructors (code that runs as they load: + | audio, music, joystick, the network, Task Manager, texthelp) + | - the library with shellMain (dvxshell.lib) + | - every widget; each .wgt that exports wgtRegister() has it called + | - dlopen() with RTLD_GLOBAL; ABI stamp checked; a mismatch stops + | the boot + | + +-- Find and call shellMain() + | + +-- Every other library loads when something first needs one of + | its names: a module being opened (platformModuleOpen), or a + | dlsym(NULL) (BASIC's DECLARE LIBRARY, dvxLibraryLoaded) | +-- dvxInit() -- video mode, input, font, colors, cursors | +-- Load desktop app (progman.app) | +-- Main loop: - dvxUpdate() -> tsYield() -> shellReapApps() + dvxUpdate() (yields) -> shellReapApps() -> close, exit, + and DOS-run requests -> screen saver .endcode .h2 Application Lifecycle @@ -626,7 +638,9 @@ appMain() creates windows, registers callbacks, and returns. The app lives throu .h3 Main-loop (hasMainLoop = true) -A dedicated cooperative task is created. appMain() runs in that task with its own loop, calling tsYield() to share CPU. Needed for apps with continuous work (terminal emulators, games). Lifecycle ends when appMain() returns. +A dedicated cooperative task is created. appMain() runs in that task with its own loop, calling tsYield() (or dvxUpdate, which yields) to share CPU. Needed for apps with continuous work (terminal emulators, games). Lifecycle ends when appMain() returns. + +Input is taken on the shell's own task (see Event Model), so every window callback -- a callback-only app's and a main-loop app's alike -- runs there, never on another program's stack; a main-loop app's own loop sees what its callbacks did when it next runs. A BASIC program's events are the exception: its form runtime queues those the shell's task dispatches and runs them on the program's own task at its next turn (DoEvents, its event loop), so a handler that waits (DoEvents, SHELL, a modal form) holds only its own program. Its UserControls' events (their mouse, keys, focus, Resize, and Paint) wait for that turn the same way: a program's code never runs on two tasks at once. .h2 Crash Recovery @@ -652,17 +666,17 @@ All allocations route through dvxMalloc/dvxFree wrappers that prepend a 16-byte .h1 Event Model -DVX uses a cooperative polling model. The main loop (dvxRun / dvxUpdate) runs this cycle each frame: +DVX uses a cooperative polling model. The main loop (dvxRun / dvxUpdate) runs this cycle each frame. The first four steps run only in a frame that takes input: the context's inputHere hook decides, and the shell lets only its own task take it (or any task, once the shell's has not for a quarter of a second, busy in a long call), so callbacks run on the shell's task; a frame on another task just paints and yields. .list .item Poll mouse -- platformMousePoll() returns position and button bitmask. Compare with previous frame for press/release edge detection. .item Poll keyboard -- platformKeyboardRead() returns ASCII + scancode. Non-blocking; returns false if buffer is empty. Shift/Ctrl/Alt+F-keys arrive as the plain F-key plus modifier flags. If an input entropy hook is installed (dvxSetInputEntropyHook), input events are timestamped and fed to it here. .item Dispatch to focused window -- the event loop fires window callbacks (onKey, onMouse, etc.) on the focused window. If the window has a widget tree, the widget system's installed handlers dispatch to individual widgets. .item Tooltips, widget polling (WCLASS_NEEDS_POLL), cursor blink, and widget timers. -.item Deferred paint flush -- every window whose paintNeeded was raised by wgtInvalidate / wgtInvalidatePaint during the frame is painted once (the highest requested level wins). Windows queued for destruction are skipped. +.item Deferred paint flush -- every window whose paintNeeded was raised by wgtInvalidate / wgtInvalidatePaint during the frame is painted once (the highest requested level wins). Windows queued for destruction are skipped. After a partial repaint (wgtInvalidatePaint only) the widget paint narrows the window's dirty area to the boxes of the widgets it repainted, joined and widened two pixels (a container that paints its children itself counts as its own box), so only that part of the screen is composited and flushed; a full repaint, an open popup list, and a window that paints itself composite the whole window. .item Deferred destroy flush -- windows destroyed from inside callbacks are torn down once the dispatch depth is back to zero. .item Compositor pass -- merge dirty rects, composite, flush to LFB. -.item Yield -- platformYield() or idle callback. +.item Yield -- the idle callback after a frame with nothing to paint, the busy callback after one that painted (the shell's run the idle handlers -- audio streams, network, serial pollers -- at least every 20 ms either way, and yield to the other tasks every frame), or platformYield() without them. .endlist .h2 Event Dispatch Chain @@ -1043,37 +1057,34 @@ The -E flag specifies exported symbols (prefixed with underscore per DJGPP conve .code dvxgui/ - +-- core/ Core library sources (dvxVideo, dvxDraw, dvxComp, dvxWm, dvxApp, widget infra) - | +-- platform/ Platform abstraction (dvxPlat.h, dvxPlatformDos.c, dvxPlatformUtil.c) - | +-- thirdparty/ stb_image, stb_ds, stb_image_write - +-- loader/ Bootstrap loader (dvx.exe) - +-- tasks/ Cooperative task switcher (libtasks.lib) - +-- shell/ DVX Shell (dvxshell.lib) - +-- widgets/ Widget DXE modules (*.wgt), each in its own subdirectory - | +-- box/ VBox/HBox layout containers - | +-- button/ Push button - | +-- textInput/ Text entry field - | +-- listView/ Multi-column list - | +-- ... (29 widget types total) - +-- texthelp/ Shared text editing helpers - +-- listhelp/ Shared dropdown/list helpers - +-- apps/ Application DXE modules (*.app) - | +-- progman/ Program Manager (desktop) - | +-- notepad/ Text editor - | +-- cpanel/ Control Panel - | +-- imgview/ Image viewer - | +-- clock/ Clock - | +-- dvxdemo/ Demo / showcase app - | +-- dvxbasic/ DVX BASIC compiler and VM - +-- tools/ Build tools (dvxres resource compiler) - +-- rs232/ ISR-driven UART driver - +-- packet/ HDLC framing, CRC-16, sliding window - +-- security/ DH key exchange, XTEA cipher, DRBG RNG - +-- seclink/ Encrypted channel wrapper - +-- serial/ Combined serial stack DXE - +-- proxy/ Linux proxy (emulator <-> secLink <-> telnet) - +-- sql/ SQLite integration - +-- bin/ Build output (dvx.exe, system/, libs/, widgets/, apps/, shared/) - +-- obj/ Intermediate object files - +-- docs/ Documentation + +-- src/ + | +-- loader/ Bootstrap loader (dvx.exe) + | +-- libs/kpunch/ Libraries (*.lib), each in its own folder + | | +-- libdvx/ Core: video, drawing, compositor, windows, widget infrastructure + | | | +-- platform/ Platform layer (dvxPlat.h, dvxPlatformDos.c, dvxPlatformUtil.c) + | | | +-- thirdparty/ stb_image, stb_ds, stb_image_write + | | +-- libtasks/ Cooperative task switcher + | | +-- dvxshell/ DVX Shell (Program Manager's host, program loading) + | | +-- texthelp/ Shared text editing helpers + | | +-- listhelp/ Shared dropdown/list helpers + | | +-- serial/ rs232: ISR-driven UART driver + | | +-- crypto/ X25519, XChaCha20-Poly1305, Ed25519 (Monocypher), DH, XTEA, SHA-256/HMAC/HKDF, DRBG + | | +-- seclink/ Packet transport (HDLC, CRC-16, Go-Back-N) and the secure link + | | +-- dvxnet/ TCP/IP (lwIP) and the BSD socket layer + | | +-- pkg/ Add-on packages (ZIP, manifests, signatures, install and remove) + | | +-- sql/ SQLite integration + | | +-- ... audio, music, print, conn, xfer, http, json, and more + | +-- widgets/kpunch/ Widget modules (*.wgt), each in its own folder (35) + | +-- apps/kpunch/ Programs (*.app): progman, notepad, cpanel, dvxbasic, terminal, ... + | +-- savers/kpunch/ Screen savers + | +-- addons/ Add-on package sources (fonts, instrument banks) + | +-- include/basic/ BASIC include files (*.bas) + | +-- installer/ SETUP.EXE and the package tools + | +-- sdk/ SDK templates + | +-- tools/ Build tools (dvxres, dvxhlpc, dxeGc.sh, ...); proxy/ is the Linux secure link proxy + | +-- test/ Test suites (host and DOS) + +-- bin/ Build output (dvx.exe, system/, libs/, widgets/, apps/, shared/) + +-- obj/ Intermediate object files + +-- dist/ Release media and the SDK + +-- docs/ Documentation .endcode diff --git a/src/libs/kpunch/libdvx/dvxApp.c b/src/libs/kpunch/libdvx/dvxApp.c index a47dd392..6b9ec719 100644 --- a/src/libs/kpunch/libdvx/dvxApp.c +++ b/src/libs/kpunch/libdvx/dvxApp.c @@ -59,6 +59,8 @@ #include "dvxRes.h" #include "dvxText.h" #include "dvxPrefs.h" +#include "dvxImgInt.h" +#include "dvxResInt.h" #include "thirdparty/stb_ds_wrap.h" #include @@ -71,8 +73,6 @@ #include #include -#include "thirdparty/stb_image_wrap.h" -#include "thirdparty/stb_image_write_wrap.h" // ABI stamp checked by the loader (see DVX_ABI_VERSION). DVX_ABI_STAMP(libdvx); @@ -107,12 +107,20 @@ DVX_ABI_STAMP(libdvx); #define ACCEL_TEXT_CTRL "Ctrl" #define ACCEL_TEXT_ALT "Alt" #define ACCEL_TEXT_SHIFT "Shift" -#define ACCEL_TEXT_MAX 32 // longest key text taken apart #define ACCEL_FKEY_PREFIX 'F' #define ACCEL_FKEY_FMT "F%ld" #define ACCEL_FKEY_COUNT 12 #define ACCEL_FKEY_BASE 10 +// A frame's composite (compositeDirty) stops once COMPOSITE_BUDGET_US has +// gone and keeps what is left of the dirty rects for the next frame, so a +// full-screen repaint on a slow bus (0.2 - 1 s at 1024 x 768 x 32 on ISA) +// is spread over frames, in bands of COMPOSITE_BAND_ROWS, the other tasks +// running between; any other composite (compositeAndFlush) runs to the end. +#define COMPOSITE_BUDGET_US 40000 +#define COMPOSITE_BAND_ROWS 64 +#define COMPOSITE_NO_BUDGET 0 + // Cursor dirty region: covers all cursor shapes (16x16 with hotspot at 0,0 or 7,7) #define CURSOR_HOTSPOT_X 7 #define CURSOR_HOTSPOT_Y 7 @@ -132,6 +140,13 @@ DVX_ABI_STAMP(libdvx); // Wallpaper scaler yields to the event loop every N rows (power of two) #define WALLPAPER_YIELD_ROWS 32 +// The wallpaper's decoded picture is kept for its next build (a wallpaper +// mode or video mode change) when it takes no more than this +#define WALLPAPER_KEEP_BYTES (2560 * 1024) + +// Image decodes that pump (imageDecode): a frame every so many bytes read +#define IMAGE_PUMP_BYTES (32 * 1024) + // Ordered dithering (4x4 Bayer) is applied in the 15/16-bit modes #define BAYER_SIZE 4 #define BAYER_MASK (BAYER_SIZE - 1) @@ -155,6 +170,7 @@ DVX_ABI_STAMP(libdvx); // Screenshot format selection and messages #define BMP_EXT ".bmp" + #define SCREENSHOT_NOMEM_MSG "Not enough memory to capture the image." #define SCREENSHOT_FAIL_MSG "The image could not be saved." @@ -210,21 +226,21 @@ DVX_ABI_STAMP(libdvx); // window callback, then restores the previous value. This ensures // allocations made during menu handlers, close handlers, paint, // etc. are attributed to the correct app. The call is also booked as -// the app being busy (appCallEnter/appCallLeave, see dvxAppIsHung); the +// the app being busy (dvxAppCallEnter/dvxAppCallLeave, see dvxAppIsHung); the // app id is captured first because the callback may destroy the window. #define WIN_CALLBACK(ctx, win, call) do { \ int32_t _prevAppId = (ctx)->currentAppId; \ int32_t _appId = (win)->appId; \ (ctx)->currentAppId = _appId; \ - appCallEnter(_appId); \ + dvxAppCallEnter(_appId); \ call; \ - appCallLeave(_appId); \ + dvxAppCallLeave(_appId); \ (ctx)->currentAppId = _prevAppId; \ } while (0) // Per-channel expansion tables for converting a direct-color pixel to -// 24-bit RGB (see bufferToRgb). +// 24-bit RGB (see dvxBufferToRgb). typedef struct { int32_t shift[RGB_CHANNELS]; uint32_t mask[RGB_CHANNELS]; @@ -247,6 +263,18 @@ typedef struct { uint32_t keyColor; } DragFeedbackT; +// An image decoded through stb's read callbacks (imageDecode): a file +// (fp) or bytes (data, len, read at pos), and the context pumped every +// IMAGE_PUMP_BYTES read (NULL: none) +typedef struct { + FILE *fp; + const uint8_t *data; + int32_t len; + int32_t pos; + AppContextT *pumpCtx; + int32_t sincePump; +} ImageSrcT; + // ============================================================ // Prototypes @@ -256,11 +284,8 @@ static int32_t accelKeyFromKeystroke(int32_t scancode, int32_t ascii, int32_t ke static void activatePopupItem(AppContextT *ctx, int32_t itemIdx); static void addSysMenuItem(AppContextT *ctx, const char *label, int32_t cmd, bool enabled); static void addSysMenuSeparator(AppContextT *ctx); -static void appCallEnter(int32_t appId); static int32_t appCallFind(int32_t appId); -static void appCallLeave(int32_t appId); static void blurFocusedWindow(AppContextT *ctx); -static uint8_t *bufferToRgb(const DisplayT *d, const uint8_t *buf, int32_t w, int32_t h, int32_t pitch); static void buildChannelLut(ChannelLutT *lut, const PixelFormatT *fmt); static uint8_t *buildWallpaperBuf(AppContextT *ctx, const uint8_t *rgb, int32_t imgW, int32_t imgH, WallpaperModeE mode, int32_t *outPitch); static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph, bool *hasCheck); @@ -275,7 +300,10 @@ static void closeSysMenu(AppContextT *ctx); static void closeWindowTransients(AppContextT *ctx, const WindowT *win); static uint32_t *colorSlot(ColorSchemeT *cs, ColorIdE id); static void compositeAndFlush(AppContextT *ctx); +static void compositeDirty(AppContextT *ctx, uint64_t budgetUs); +static void compositeRect(AppContextT *ctx, RectT *dr, const int32_t *visibleIdx, int32_t visibleCount); static void consumeMousePress(AppContextT *ctx); +static int32_t contentCursorShape(AppContextT *ctx, WindowT *win, int32_t cx, int32_t cy); static uint8_t *convertRgbToNative(const DisplayT *d, const uint8_t *src, int32_t channels, int32_t w, int32_t h, uint32_t keyColor, bool *outHasAlpha, int32_t *outPitch); static int32_t countVisibleWindows(const AppContextT *ctx); static int32_t cursorForResizeEdge(int32_t edge); @@ -302,8 +330,13 @@ static void enumModeCb(int32_t w, int32_t h, int32_t bpp, void *userData); static bool exclusiveEnter(AppContextT *ctx); static void exclusiveLeave(AppContextT *ctx); static void exclusiveLutSet(ExclusiveStateT *ex, int32_t first, int32_t count); +static void exclusiveMirrorDirty(AppContextT *ctx, const DirtyListT *dl); +static void exclusiveMirrorMouse(AppContextT *ctx); +static int32_t exclusiveMirrorPlace(AppContextT *ctx, RectT *place); +static void exclusiveMirrorShow(AppContextT *ctx, const RectT *r); static void exclusiveMouse(AppContextT *ctx); static void exclusiveNotify(AppContextT *ctx, WindowT *win, ExclusiveFnT fn, void *userData, ExclusiveStateE state); +static int32_t exclusiveStart(AppContextT *ctx, WindowT *win, int32_t width, int32_t height, int32_t bpp, ExclusiveFnT onChange, void *userData, bool mirror); static void exclusiveSync(AppContextT *ctx); static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd); static WindowT *findWindowById(AppContextT *ctx, int32_t id); @@ -315,18 +348,25 @@ static void flushWindowPaint(AppContextT *ctx, WindowT *win); static WindowT *focusedWindow(const AppContextT *ctx); static void focusTopmostAlive(AppContextT *ctx); static void focusWindowAt(AppContextT *ctx, int32_t idx); -static bool fontSmoothSet(const AppContextT *ctx); +static void fontSmoothSet(const AppContextT *ctx); static void handleKbMoveResizeKey(AppContextT *ctx, int32_t scancode, int32_t ascii); static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons); static void handlePopupKey(AppContextT *ctx, int32_t scancode, int32_t ascii); static void handleRightButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons); static bool handleSysMenuKey(AppContextT *ctx, int32_t scancode, int32_t ascii); static void handleTabKey(AppContextT *ctx, WindowT *win, int32_t key, int32_t shiftFlags); +static uint8_t *imageDecode(ImageSrcT *src, int *imgW, int *imgH, int channels); +static bool imageInfoTake(int w, int h, int32_t *outW, int32_t *outH); +static AppContextT *imagePumpCtx(const AppContextT *ctx); +static int imageSrcEof(void *user); +static int imageSrcRead(void *user, char *out, int size); +static void imageSrcSkip(void *user, int n); static void initColorScheme(AppContextT *ctx); static void interactiveSaveBuffer(AppContextT *ctx, const char *title, const uint8_t *src, int32_t w, int32_t h, int32_t pitch); static void interactiveScreenshot(AppContextT *ctx); static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win); static void invalidateAllWindows(AppContextT *ctx); +static void invalidateAllWindowsLater(AppContextT *ctx); static bool isFocusCandidate(const WindowT *win); static bool isModalWindow(const AppContextT *ctx, const WindowT *win); static void keyFocusSync(AppContextT *ctx); @@ -361,7 +401,6 @@ static void scrollWidgetIntoView(AppContextT *ctx, WindowT *win, WidgetT *w); static void sendMouseToWindow(AppContextT *ctx, WindowT *win, int32_t mx, int32_t my, int32_t buttons); static void setWindowScroll(AppContextT *ctx, WindowT *win, ScrollbarT *sb, int32_t value); static const char *splitMenuLabel(const char *label, char leftBuf[MAX_MENU_LABEL]); -static inline void storePixel(uint8_t *row, int32_t x, int32_t bytesPerPx, uint32_t px); static bool takesGameKeys(const AppContextT *ctx, const WindowT *win); static int32_t tileSlotPos(int32_t slot, int32_t tile, int32_t screen); static void toggleMaximize(AppContextT *ctx, WindowT *win); @@ -458,6 +497,20 @@ static int32_t sEntropyLen = 0; static DvxSoundHookT sSoundHook = NULL; static int32_t sOpenSoundWinId = 0; +// dvxSetWaitPump (NULL: waits only yield) +static DvxWaitPumpT sWaitPump = NULL; + +// The wallpaper's decoded picture (RGB) and its file, kept while it is +// the wallpaper (see WALLPAPER_KEEP_BYTES); NULL: none kept +static uint8_t *sWallRgb = NULL; +static int sWallRgbW = 0; +static int sWallRgbH = 0; +static char sWallRgbPath[DVX_MAX_PATH]; + +// Fonts are being built again for a smoothing change (dvxUpdate steps +// them): every window is laid out again after the last +static bool sFontsRebuilding = false; + // The drag feedback shown (see DragFeedbackT); one mouse, so one for the // whole desktop. static DragFeedbackT sDragFeedback; @@ -637,21 +690,6 @@ static void addSysMenuSeparator(AppContextT *ctx) { } -// Book the start of a window callback of appId (see WIN_CALLBACK). -static void appCallEnter(int32_t appId) { - int32_t idx = appCallFind(appId); - - if (idx < 0) { - AppCallT call = { appId, 0, platformClock() }; - - arrput(sAppCalls, call); - idx = (int32_t)arrlen(sAppCalls) - 1; - } - - sAppCalls[idx].depth++; -} - - // Index of appId's entry in sAppCalls, or -1. static int32_t appCallFind(int32_t appId) { for (int32_t i = 0; i < (int32_t)arrlen(sAppCalls); i++) { @@ -664,17 +702,6 @@ static int32_t appCallFind(int32_t appId) { } -// Book the end of a window callback of appId. The entry may already be -// gone (dvxAppHungForget when the app was ended inside the callback). -static void appCallLeave(int32_t appId) { - int32_t idx = appCallFind(appId); - - if (idx >= 0 && --sAppCalls[idx].depth == 0) { - arrdel(sAppCalls, idx); - } -} - - // Drops window-manager focus from the focused window, firing its onBlur // under its own app id, and leaves focusedIdx at -1. Shared by the // attributed focus switch, dvxHideWindow and dvxMinimizeWindow. @@ -696,64 +723,6 @@ static void blurFocusedWindow(AppContextT *ctx) { } -// ============================================================ -// bufferToRgb -- convert native pixel format to 24-bit RGB -// ============================================================ -// -// Screenshots must produce standard RGB data for stb_image_write, but the -// backbuffer uses whatever native pixel format VESA gave us (8/16/32bpp). -// The 8bpp path uses the VGA palette for lookup. Direct-color channels -// are widened through per-channel lookup tables built once per call with -// bit replication, so full-scale values reach 255 (a 5-bit 31 becomes -// 255, not 248). The pixel-width branch is hoisted out of the pixel loop. - -static uint8_t *bufferToRgb(const DisplayT *d, const uint8_t *buf, int32_t w, int32_t h, int32_t pitch) { - uint8_t *rgb = (uint8_t *)malloc((size_t)w * h * RGB_CHANNELS); - - if (!rgb) { - return NULL; - } - - uint8_t *dst = rgb; - - if (d->format.bitsPerPixel == PALETTE_BPP) { - for (int32_t y = 0; y < h; y++) { - const uint8_t *row = buf + y * pitch; - - for (int32_t x = 0; x < w; x++) { - memcpy(dst, d->palette + row[x] * RGB_CHANNELS, RGB_CHANNELS); - dst += RGB_CHANNELS; - } - } - - return rgb; - } - - ChannelLutT lut; - buildChannelLut(&lut, &d->format); - - bool wide = (d->format.bytesPerPixel == sizeof(uint32_t)); - - for (int32_t y = 0; y < h; y++) { - const uint8_t *row = buf + y * pitch; - - if (wide) { - for (int32_t x = 0; x < w; x++) { - pixelToRgb(&lut, ((const uint32_t *)row)[x], dst); - dst += RGB_CHANNELS; - } - } else { - for (int32_t x = 0; x < w; x++) { - pixelToRgb(&lut, ((const uint16_t *)row)[x], dst); - dst += RGB_CHANNELS; - } - } - } - - return rgb; -} - - // Fills the per-channel expansion tables for a direct-color format: every // n-bit channel value maps to 8 bits by replicating its bits from the top // down, so the maximum value maps to 255. @@ -811,7 +780,7 @@ static uint8_t *buildWallpaperBuf(AppContextT *ctx, const uint8_t *rgb, int32_t // Fill with the desktop color first (the border area in center mode): // one row pixel by pixel, then replicate it. for (int32_t x = 0; x < screenW; x++) { - storePixel(buf, x, bytesPerPx, ctx->colors.desktop); + pixelPut(buf + x * bytesPerPx, ctx->colors.desktop, bytesPerPx); } for (int32_t y = 1; y < screenH; y++) { @@ -869,7 +838,7 @@ static uint8_t *buildWallpaperBuf(AppContextT *ctx, const uint8_t *rgb, int32_t int32_t g = (p00[1] * w00 + p10[1] * w10 + p01[1] * w01 + p11[1] * w11) >> FP_INT_SHIFT; int32_t b = (p00[2] * w00 + p10[2] * w10 + p01[2] * w01 + p11[2] * w11) >> FP_INT_SHIFT; - storePixel(dst, x, bytesPerPx, packDithered(d, r, g, b, y, x, dither)); + pixelPut(dst + x * bytesPerPx, packDithered(d, r, g, b, y, x, dither), bytesPerPx); } } @@ -888,7 +857,7 @@ static uint8_t *buildWallpaperBuf(AppContextT *ctx, const uint8_t *rgb, int32_t for (int32_t x = 0; x < screenW; x++) { const uint8_t *src = srcRow + srcX * RGB_CHANNELS; - storePixel(dst, x, bytesPerPx, packDithered(d, src[0], src[1], src[2], y, x, dither)); + pixelPut(dst + x * bytesPerPx, packDithered(d, src[0], src[1], src[2], y, x, dither), bytesPerPx); srcX++; if (srcX == imgW) { @@ -923,7 +892,7 @@ static uint8_t *buildWallpaperBuf(AppContextT *ctx, const uint8_t *rgb, int32_t int32_t dx = offX + sx; const uint8_t *src = srcRow + sx * RGB_CHANNELS; - storePixel(dst, dx, bytesPerPx, packDithered(d, src[0], src[1], src[2], dy, dx, dither)); + pixelPut(dst + dx * bytesPerPx, packDithered(d, src[0], src[1], src[2], dy, dx, dither), bytesPerPx); } } } @@ -1211,15 +1180,27 @@ static uint32_t *colorSlot(ColorSchemeT *cs, ColorIdE id) { // clipped by the draw layer. static void compositeAndFlush(AppContextT *ctx) { - DisplayT *d = &ctx->display; - BlitOpsT *ops = &ctx->blitOps; - DirtyListT *dl = &ctx->dirty; - WindowStackT *ws = &ctx->stack; + compositeDirty(ctx, COMPOSITE_NO_BUDGET); +} + + +// The dirty rects composited and flushed, in bands of COMPOSITE_BAND_ROWS +// when budgetUs is set: once that has gone, what is left stays in the +// dirty list for the next frame (COMPOSITE_NO_BUDGET: all of it now). +static void compositeDirty(AppContextT *ctx, uint64_t budgetUs) { + DisplayT *d = &ctx->display; + DirtyListT *dl = &ctx->dirty; + WindowStackT *ws = &ctx->stack; + uint64_t start = platformMicros(); // A full-screen program has the screen: nothing is drawn (windows keep // painting into their own buffers), and giving the screen back // repaints all of it. if (ctx->exclusive.active) { + if (ctx->exclusive.mirror) { + exclusiveMirrorDirty(ctx, dl); + } + dirtyListClear(dl); return; } @@ -1241,122 +1222,52 @@ static void compositeAndFlush(AppContextT *ctx) { } for (int32_t i = 0; i < dl->count; i++) { - RectT *dr = &dl->rects[i]; + RectT whole = dl->rects[i]; // Clip dirty rect to screen bounds - if (dr->x < 0) { - dr->w += dr->x; - dr->x = 0; + if (whole.x < 0) { + whole.w += whole.x; + whole.x = 0; } - if (dr->y < 0) { - dr->h += dr->y; - dr->y = 0; + if (whole.y < 0) { + whole.h += whole.y; + whole.y = 0; } - if (dr->x + dr->w > d->width) { - dr->w = d->width - dr->x; + if (whole.x + whole.w > d->width) { + whole.w = d->width - whole.x; } - if (dr->y + dr->h > d->height) { - dr->h = d->height - dr->y; + if (whole.y + whole.h > d->height) { + whole.h = d->height - whole.y; } - if (dr->w <= 0 || dr->h <= 0) { - continue; - } + while (whole.w > 0 && whole.h > 0) { + RectT band = whole; - // Set clip rect to this dirty rect - setClipRect(d, dr->x, dr->y, dr->w, dr->h); + band.h = budgetUs ? DVX_MIN(whole.h, COMPOSITE_BAND_ROWS) : whole.h; + compositeRect(ctx, &band, visibleIdx, visibleCount); + whole.y += band.h; + whole.h -= band.h; - // Occlusion: find the topmost window whose frame covers the whole - // dirty rect; nothing below it can show through. - int32_t firstVi = 0; - bool covered = false; + // Out of time: the rest waits for the next frame + if (budgetUs && platformMicros() - start >= budgetUs && (whole.h > 0 || i + 1 < dl->count)) { + int32_t kept = 0; - for (int32_t vi = visibleCount - 1; vi >= 0; vi--) { - if (windowCoversRect(ws->windows[visibleIdx[vi]], dr)) { - firstVi = vi; - covered = true; - break; - } - } - - if (!covered) { - // 1. Draw desktop background (wallpaper or solid color) - if (ctx->wallpaperBuf) { - int32_t bpp = d->format.bytesPerPixel; - - for (int32_t row = dr->y; row < dr->y + dr->h; row++) { - ops->spanCopy(d->backBuf + row * d->pitch + dr->x * bpp, ctx->wallpaperBuf + row * ctx->wallpaperPitch + dr->x * bpp, dr->w); + if (whole.w > 0 && whole.h > 0) { + dl->rects[kept++] = whole; } - } else { - rectFill(d, ops, dr->x, dr->y, dr->w, dr->h, ctx->colors.desktop); - } - // 2. Draw minimized window icons (under all windows) - wmDrawMinimizedIcons(d, ops, &ctx->colors, ws, dr); - } + for (int32_t j = i + 1; j < dl->count; j++) { + dl->rects[kept++] = dl->rects[j]; + } - // 3. Walk pre-filtered visible window list bottom-to-top - for (int32_t vi = firstVi; vi < visibleCount; vi++) { - WindowT *win = ws->windows[visibleIdx[vi]]; - - // Check if window intersects this dirty rect - RectT winRect = {win->x, win->y, win->w, win->h}; - RectT isect; - - if (!rectIntersect(dr, &winRect, &isect)) { - continue; - } - - wmDrawChrome(d, ops, &ctx->font, &ctx->colors, win, dr); - wmDrawContent(d, ops, win, dr); - - if (win->vScroll || win->hScroll) { - wmDrawScrollbars(d, ops, &ctx->colors, win, dr); + dl->count = kept; + resetClipRect(d); + return; } } - - // 4. Draw popup menu if active (all levels) - if (ctx->popup.active) { - // Draw parent levels first (bottom to top) - for (int32_t lvl = 0; lvl < ctx->popup.depth; lvl++) { - PopupLevelT *pl = &ctx->popup.parentStack[lvl]; - drawPopupLevel(ctx, d, ops, pl->menu, pl->popupX, pl->popupY, pl->popupW, pl->popupH, pl->hoverItem, pl->hasCheck, dr); - } - - // Draw current (deepest) level - drawPopupLevel(ctx, d, ops, ctx->popup.menu, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH, ctx->popup.hoverItem, ctx->popup.hasCheck, dr); - } - - // 4b. Draw system menu if active - if (ctx->sysMenu.active) { - drawSysMenu(ctx, d, ops, dr); - } - - // 5. Draw tooltip - if (ctx->tooltipText) { - RectT ttRect = { ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH }; - RectT ttIsect; - - if (rectIntersect(dr, &ttRect, &ttIsect)) { - rectFill(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH, ctx->colors.menuBg); - drawRectOutline(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH, ctx->colors.contentFg); - drawText(d, ops, &ctx->font, ctx->tooltipX + TOOLTIP_PAD, ctx->tooltipY + TOOLTIP_PAD, ctx->tooltipText, ctx->colors.menuFg, ctx->colors.menuBg, true); - } - } - - // 5b. Draw the drag outline or picture - if (sDragFeedback.active) { - drawDragFeedback(ctx, dr); - } - - // 6. Draw cursor - drawCursorAt(ctx, ctx->mouseX, ctx->mouseY); - - // 7. Flush this dirty rect to LFB - flushRect(d, dr); } resetClipRect(d); @@ -1364,6 +1275,108 @@ static void compositeAndFlush(AppContextT *ctx) { } +// One dirty rect (on the screen) composited: the desktop where no window +// covers it all, the windows bottom to top, menus, the tooltip, drag +// feedback, the cursor; then flushed to the screen. +static void compositeRect(AppContextT *ctx, RectT *dr, const int32_t *visibleIdx, int32_t visibleCount) { + DisplayT *d = &ctx->display; + BlitOpsT *ops = &ctx->blitOps; + WindowStackT *ws = &ctx->stack; + + // Set clip rect to this dirty rect + setClipRect(d, dr->x, dr->y, dr->w, dr->h); + + // Occlusion: find the topmost window whose frame covers the whole + // dirty rect; nothing below it can show through. + int32_t firstVi = 0; + bool covered = false; + + for (int32_t vi = visibleCount - 1; vi >= 0; vi--) { + if (windowCoversRect(ws->windows[visibleIdx[vi]], dr)) { + firstVi = vi; + covered = true; + break; + } + } + + if (!covered) { + // 1. Draw desktop background (wallpaper or solid color) + if (ctx->wallpaperBuf) { + int32_t bpp = d->format.bytesPerPixel; + + for (int32_t row = dr->y; row < dr->y + dr->h; row++) { + ops->spanCopy(d->backBuf + row * d->pitch + dr->x * bpp, ctx->wallpaperBuf + row * ctx->wallpaperPitch + dr->x * bpp, dr->w); + } + } else { + rectFill(d, ops, dr->x, dr->y, dr->w, dr->h, ctx->colors.desktop); + } + + // 2. Draw minimized window icons (under all windows) + wmDrawMinimizedIcons(d, ops, &ctx->colors, ws, dr); + } + + // 3. Walk pre-filtered visible window list bottom-to-top + for (int32_t vi = firstVi; vi < visibleCount; vi++) { + WindowT *win = ws->windows[visibleIdx[vi]]; + + // Check if window intersects this dirty rect + RectT winRect = {win->x, win->y, win->w, win->h}; + RectT isect; + + if (!rectIntersect(dr, &winRect, &isect)) { + continue; + } + + wmDrawChrome(d, ops, &ctx->font, &ctx->colors, win, dr); + wmDrawContent(d, ops, win, dr); + + if (win->vScroll || win->hScroll) { + wmDrawScrollbars(d, ops, &ctx->colors, win, dr); + } + } + + // 4. Draw popup menu if active (all levels) + if (ctx->popup.active) { + // Draw parent levels first (bottom to top) + for (int32_t lvl = 0; lvl < ctx->popup.depth; lvl++) { + PopupLevelT *pl = &ctx->popup.parentStack[lvl]; + drawPopupLevel(ctx, d, ops, pl->menu, pl->popupX, pl->popupY, pl->popupW, pl->popupH, pl->hoverItem, pl->hasCheck, dr); + } + + // Draw current (deepest) level + drawPopupLevel(ctx, d, ops, ctx->popup.menu, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH, ctx->popup.hoverItem, ctx->popup.hasCheck, dr); + } + + // 4b. Draw system menu if active + if (ctx->sysMenu.active) { + drawSysMenu(ctx, d, ops, dr); + } + + // 5. Draw tooltip + if (ctx->tooltipText) { + RectT ttRect = { ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH }; + RectT ttIsect; + + if (rectIntersect(dr, &ttRect, &ttIsect)) { + rectFill(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH, ctx->colors.menuBg); + drawRectOutline(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH, ctx->colors.contentFg); + drawText(d, ops, &ctx->font, ctx->tooltipX + TOOLTIP_PAD, ctx->tooltipY + TOOLTIP_PAD, ctx->tooltipText, ctx->colors.menuFg, ctx->colors.menuBg, true); + } + } + + // 5b. Draw the drag outline or picture + if (sDragFeedback.active) { + drawDragFeedback(ctx, dr); + } + + // 6. Draw cursor + drawCursorAt(ctx, ctx->mouseX, ctx->mouseY); + + // 7. Flush this dirty rect to LFB + flushRect(d, dr); +} + + // Consumes the current left-button press: marks the edge already-seen and // swallows the matching release. Called before a press handler pumps a // nested dvxUpdate loop (sys-menu command, popup-menu command) so the @@ -1383,6 +1396,32 @@ static void consumeMousePress(AppContextT *ctx) { // threshold are replaced by keyColor and *outHasAlpha is set. Returns a // malloc'd buffer (pitch in *outPitch) or NULL on allocation failure. +// The cursor win's content asks for at content point (cx, cy): its +// onCursorQuery's, else the widget's under it -- one inside a clipping +// container (a text box in a ScrollPane) first, then the container itself +// (a Splitter's divider) -- else the arrow. +static int32_t contentCursorShape(AppContextT *ctx, WindowT *win, int32_t cx, int32_t cy) { + int32_t shape = 0; + + if (win->onCursorQuery) { + WIN_CALLBACK(ctx, win, shape = win->onCursorQuery(win, cx, cy)); + } + + if (shape <= 0 && win->widgetRoot) { + WidgetT *target = widgetCallbackTarget(win->widgetRoot, cx, cy); + WidgetT *hit = widgetHitTest(win->widgetRoot, cx, cy); + + shape = target ? wclsGetCursorShape(target, cx, cy) : 0; + + if (shape <= 0 && hit && hit != target) { + shape = wclsGetCursorShape(hit, cx, cy); + } + } + + return (shape > 0) ? shape : CURSOR_ARROW; +} + + static uint8_t *convertRgbToNative(const DisplayT *d, const uint8_t *src, int32_t channels, int32_t w, int32_t h, uint32_t keyColor, bool *outHasAlpha, int32_t *outPitch) { int32_t bpp = d->format.bytesPerPixel; int32_t pitch = w * bpp; @@ -1407,7 +1446,7 @@ static uint8_t *convertRgbToNative(const DisplayT *d, const uint8_t *src, int32_ color = packColor(d, px[0], px[1], px[2]); } - storePixel(dstRow, x, bpp, color); + pixelPut(dstRow + x * bpp, color, bpp); } } @@ -1812,6 +1851,14 @@ static void dispatchEvents(AppContextT *ctx) { ctx->modalWindow = NULL; } + // A modal dialog keeps the top and the focus: a window another + // program shows or raises while the dialog waits (its task runs every + // frame) goes behind it, or keys typed for the dialog would reach that + // window. + if (ctx->modalWindow && focusedWindow(ctx) != ctx->modalWindow) { + raiseAndFocus(ctx, windowIndex(ctx, ctx->modalWindow)); + } + // Full screen: the mouse is the program's alone. if (ctx->exclusive.active) { exclusiveMouse(ctx); @@ -2591,7 +2638,7 @@ void dvxAccelFormat(int32_t key, int32_t modifiers, char *out, int32_t outSize) // or digit, F1 to F12, or a name from sAccelKeyNames (case does not // matter, blanks around the parts are ignored). bool dvxAccelParse(const char *text, int32_t *outKey, int32_t *outMods) { - char buf[ACCEL_TEXT_MAX]; + char buf[DVX_ACCEL_TEXT_MAX]; int32_t mods = 0; int32_t key = 0; @@ -2684,6 +2731,32 @@ void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmd } +// Book the start of a handler of appId (a window callback: WIN_CALLBACK). +void dvxAppCallEnter(int32_t appId) { + int32_t idx = appCallFind(appId); + + if (idx < 0) { + AppCallT call = { appId, 0, platformClock() }; + + arrput(sAppCalls, call); + idx = (int32_t)arrlen(sAppCalls) - 1; + } + + sAppCalls[idx].depth++; +} + + +// Book the end of a handler of appId. The entry may already be gone +// (dvxAppHungForget when the app was ended inside the handler). +void dvxAppCallLeave(int32_t appId) { + int32_t idx = appCallFind(appId); + + if (idx >= 0 && --sAppCalls[idx].depth == 0) { + arrdel(sAppCalls, idx); + } +} + + void dvxAppHungForget(int32_t appId) { int32_t idx = appCallFind(appId); @@ -2718,13 +2791,20 @@ void dvxApplyColorScheme(AppContextT *ctx) { #ifdef DVX_HOST_TEST void dvxAppTestReset(void) { - sKeyPressedBtn = NULL; - sCursorBlinkFn = NULL; - sTooltipGen = 0; - sEntropyHook = NULL; - sEntropyLen = 0; - sSoundHook = NULL; - sOpenSoundWinId = 0; + free(sWallRgb); + sKeyPressedBtn = NULL; + sCursorBlinkFn = NULL; + sTooltipGen = 0; + sEntropyHook = NULL; + sEntropyLen = 0; + sSoundHook = NULL; + sWaitPump = NULL; + sWallRgb = NULL; + sWallRgbW = 0; + sWallRgbH = 0; + sFontsRebuilding = false; + sOpenSoundWinId = 0; + sWallRgbPath[0] = '\0'; memset(sEntropyBuf, 0, sizeof(sEntropyBuf)); arrfree(sHotkeys); sHotkeys = NULL; @@ -2742,6 +2822,64 @@ void dvxBeep(void) { } +// ============================================================ +// dvxBufferToRgb -- convert native pixel format to 24-bit RGB +// ============================================================ +// +// Screenshots must produce standard RGB data for stb_image_write, but the +// backbuffer uses whatever native pixel format VESA gave us (8/16/32bpp). +// The 8bpp path uses the VGA palette for lookup. Direct-color channels +// are widened through per-channel lookup tables built once per call with +// bit replication, so full-scale values reach 255 (a 5-bit 31 becomes +// 255, not 248). The pixel-width branch is hoisted out of the pixel loop. + +uint8_t *dvxBufferToRgb(const DisplayT *d, const uint8_t *buf, int32_t w, int32_t h, int32_t pitch) { + uint8_t *rgb = (uint8_t *)malloc((size_t)w * h * RGB_CHANNELS); + + if (!rgb) { + return NULL; + } + + uint8_t *dst = rgb; + + if (d->format.bitsPerPixel == PALETTE_BPP) { + for (int32_t y = 0; y < h; y++) { + const uint8_t *row = buf + y * pitch; + + for (int32_t x = 0; x < w; x++) { + memcpy(dst, d->palette + row[x] * RGB_CHANNELS, RGB_CHANNELS); + dst += RGB_CHANNELS; + } + } + + return rgb; + } + + ChannelLutT lut; + buildChannelLut(&lut, &d->format); + + bool wide = (d->format.bytesPerPixel == sizeof(uint32_t)); + + for (int32_t y = 0; y < h; y++) { + const uint8_t *row = buf + y * pitch; + + if (wide) { + for (int32_t x = 0; x < w; x++) { + pixelToRgb(&lut, ((const uint32_t *)row)[x], dst); + dst += RGB_CHANNELS; + } + } else { + for (int32_t x = 0; x < w; x++) { + pixelToRgb(&lut, ((const uint16_t *)row)[x], dst); + dst += RGB_CHANNELS; + } + } + } + + return rgb; +} + + // ============================================================ // dvxCascadeWindows // ============================================================ @@ -2874,8 +3012,8 @@ int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t request ctx->mouseY = ctx->display.height - 1; } - // Smoothed text or not at the new depth (the loop below lays out and - // paints every window) + // Smoothed text or not at the new depth (the fonts are built again in + // the frames after; the loop below lays out and paints every window) fontSmoothSet(ctx); // Clamp and reallocate all window content buffers @@ -3251,42 +3389,7 @@ void dvxDragOutlineShow(AppContextT *ctx, int32_t w, int32_t h, int32_t offX, in // suspends and resumes full screen as the focus leaves and comes back. int32_t dvxExclusiveBegin(AppContextT *ctx, WindowT *win, int32_t width, int32_t height, int32_t bpp, ExclusiveFnT onChange, void *userData) { - ExclusiveStateT *ex = &ctx->exclusive; - - if (!win || windowIndex(ctx, win) < 0 || win->destroyPending || width <= 0 || height <= 0) { - return DVX_EXCL_ERR_ARGS; - } - - if (ex->owner && ex->owner != win) { - return DVX_EXCL_ERR_BUSY; - } - - // A new size for the owner: let go of the old mode first. - exclusiveLeave(ctx); - - if (!ex->owner) { - dvxGeneratePalette(ex->palette); - } - - ex->owner = win; - ex->reqW = width; - ex->reqH = height; - ex->reqBpp = bpp; - ex->onChange = onChange; - ex->userData = userData; - - if (win->minimized) { - dvxRestoreWindow(ctx, win); - } - - raiseAndFocus(ctx, windowIndex(ctx, win)); - - if (!exclusiveEnter(ctx)) { - ex->owner = NULL; - return DVX_EXCL_ERR_MODE; - } - - return DVX_EXCL_OK; + return exclusiveStart(ctx, win, width, height, bpp, onChange, userData, false); } @@ -3328,6 +3431,11 @@ void dvxExclusiveFlush(AppContextT *ctx, const RectT *r) { } +int32_t dvxExclusiveMirror(AppContextT *ctx, WindowT *win, ExclusiveFnT onChange, void *userData) { + return win ? exclusiveStart(ctx, win, win->contentW, win->contentH, ctx->display.format.bitsPerPixel, onChange, userData, true) : DVX_EXCL_ERR_ARGS; +} + + // Whole-factor scaling, one source row expanded once and copied for the // repeated rows. The backbuffer is cleared (borders black) whenever the // picture's place changes. The whole screen is flushed, so a @@ -3372,7 +3480,7 @@ bool dvxExclusivePresent8(AppContextT *ctx, const uint8_t *src, int32_t srcW, in uint32_t px = bpp == 1 ? row[sx] : ex->lut[row[sx]]; for (int32_t k = 0; k < scale && x < outW; k++, x++) { - storePixel(dst, x, bpp, px); + pixelPut(dst + x * bpp, px, bpp); } } } @@ -3587,40 +3695,18 @@ uint32_t dvxIdleMs(const AppContextT *ctx) { bool dvxImageInfo(const char *path, int32_t *outW, int32_t *outH) { - if (!path) { - return false; - } + int w = 0; + int h = 0; - int w = 0; - int h = 0; - int comp = 0; + return path && dvxImgInfoFile(path, &w, &h) && imageInfoTake(w, h, outW, outH); +} - if (!stbi_info(path, &w, &h, &comp)) { - return false; - } - // stb_image gives a top-down BMP's height as stored, negative, and - // loads that image at its magnitude. INT_MIN has none (the load - // refuses that height as well). - if (h < 0 && h != INT_MIN) { - h = -h; - } +bool dvxImageInfoFromMemory(const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH) { + int w = 0; + int h = 0; - // An image without pixels does not load (rejectEmptyImage), so it has - // no size either. - if (w <= 0 || h <= 0) { - return false; - } - - if (outW) { - *outW = w; - } - - if (outH) { - *outH = h; - } - - return true; + return data && dataLen > 0 && dvxImgInfoMemory(data, dataLen, &w, &h) && imageInfoTake(w, h, outW, outH); } @@ -3803,7 +3889,8 @@ void dvxInvalidateWindow(AppContextT *ctx, WindowT *win) { // // A loaded module exports its ABI stamp (DVX_ABI_STAMP: its name, in // lower case, and DVX_ABI_SUFFIX), which the loader checked as it loaded -// it; a module that did not load has none to find. +// it; a module that did not load has none to find. Looking for it loads +// a library that is installed but not loaded yet (the loader's resolver). bool dvxLibraryLoaded(const char *name) { char lower[LIBRARY_NAME_MAX]; @@ -3839,18 +3926,30 @@ uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, i return NULL; } - int imgW; - int imgH; - int channels; - uint8_t *rgb = rejectEmptyImage(stbi_load(path, &imgW, &imgH, &channels, RGB_CHANNELS), &imgW, &imgH); + int imgW; + int imgH; + ImageSrcT src; + + memset(&src, 0, sizeof(src)); + src.fp = fopen(path, "rb"); + src.pumpCtx = imagePumpCtx(ctx); + + uint8_t *rgb = src.fp ? imageDecode(&src, &imgW, &imgH, RGB_CHANNELS) : NULL; if (rgb) { + fclose(src.fp); return finishLoadImage(&ctx->display, rgb, imgW, imgH, outW, outH, outPitch); } - // A DVX program, widget or library file: its icon - uint32_t size = 0; - uint8_t *icon = (uint8_t *)dvxResLoadIconData(path, &size); + // A DVX program, widget or library file: its icon, read from the file + // already open + uint32_t size = 0; + uint8_t *icon = (uint8_t *)dvxResIconRead(src.fp, &size); + + if (src.fp) { + fclose(src.fp); + } + uint8_t *pixels = icon ? dvxLoadImageFromMemory(ctx, icon, (int32_t)size, outW, outH, outPitch) : NULL; free(icon); @@ -3865,10 +3964,16 @@ uint8_t *dvxLoadImageAlpha(const AppContextT *ctx, const uint8_t *data, int32_t const DisplayT *d = &ctx->display; - int imgW; - int imgH; - int channels; - uint8_t *rgba = rejectEmptyImage(stbi_load_from_memory(data, dataLen, &imgW, &imgH, &channels, RGBA_CHANNELS), &imgW, &imgH); + int imgW; + int imgH; + ImageSrcT src; + + memset(&src, 0, sizeof(src)); + src.data = data; + src.len = dataLen; + src.pumpCtx = imagePumpCtx(ctx); + + uint8_t *rgba = imageDecode(&src, &imgW, &imgH, RGBA_CHANNELS); if (!rgba) { return NULL; @@ -3878,7 +3983,7 @@ uint8_t *dvxLoadImageAlpha(const AppContextT *ctx, const uint8_t *data, int32_t int32_t pitch; uint8_t *buf = convertRgbToNative(d, rgba, RGBA_CHANNELS, imgW, imgH, keyColor, outHasAlpha, &pitch); - stbi_image_free(rgba); + free(rgba); if (!buf) { return NULL; @@ -3909,10 +4014,16 @@ uint8_t *dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int return NULL; } - int imgW; - int imgH; - int channels; - uint8_t *rgb = rejectEmptyImage(stbi_load_from_memory(data, dataLen, &imgW, &imgH, &channels, RGB_CHANNELS), &imgW, &imgH); + int imgW; + int imgH; + ImageSrcT src; + + memset(&src, 0, sizeof(src)); + src.data = data; + src.len = dataLen; + src.pumpCtx = imagePumpCtx(ctx); + + uint8_t *rgb = imageDecode(&src, &imgW, &imgH, RGB_CHANNELS); return finishLoadImage(&ctx->display, rgb, imgW, imgH, outW, outH, outPitch); } @@ -4221,20 +4332,15 @@ uint8_t *dvxResLoadIcon(AppContextT *ctx, const char *dxePath, const char *resNa } +// One open of the file for the directory and the picture. void *dvxResLoadIconData(const char *dxePath, uint32_t *outSize) { - DvxResHandleT *res = dxePath ? dvxResOpen(dxePath) : NULL; + FILE *f = dxePath ? fopen(dxePath, "rb") : NULL; + void *data = dvxResIconRead(f, outSize); - if (!res) { - return NULL; + if (f) { + fclose(f); } - void *data = dvxResRead(res, DVX_RES_NAME_ICON, outSize); - - if (!data) { - data = dvxResRead(res, DVX_RES_NAME_WIDGET_ICON, outSize); - } - - dvxResClose(res); return data; } @@ -4245,24 +4351,10 @@ bool dvxResLoadText(const char *dxePath, const char *resName, char *buf, int32_t } DvxResHandleT *res = dvxResOpen(dxePath); + bool ok = res && dvxResReadText(res, resName, buf, bufSize); - if (!res) { - return false; - } - - uint32_t size = 0; - void *data = dvxResRead(res, resName, &size); dvxResClose(res); - - if (!data) { - return false; - } - - int32_t copyLen = (int32_t)size < bufSize - 1 ? (int32_t)size : bufSize - 1; - memcpy(buf, data, copyLen); - buf[copyLen] = '\0'; - free(data); - return true; + return ok; } @@ -4353,7 +4445,9 @@ int32_t dvxRunDos(AppContextT *ctx, const char *program, const char *args, const if (rc < 0) { dvxLog("DOS: %s could not be run: %s", command, strerror(err)); } else { + // It may have installed or removed anything dvxLog("DOS: back from %s", command); + platformInstalledChanged(); } platformKeyStateClear(); @@ -4483,7 +4577,7 @@ void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b) uint32_t packed = packColor(&ctx->display, r, g, b); *colorSlot(&ctx->colors, id) = packed; - invalidateAllWindows(ctx); + invalidateAllWindowsLater(ctx); } @@ -4491,15 +4585,12 @@ void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b) // dvxSetFontSmoothing // ============================================================ // -// The open fonts are built again in place, so text measured with them -// is laid out again: every window is. +// The open fonts are built again in place, a frame at a time, so text +// measured with them is laid out again: every window is, after the last. void dvxSetFontSmoothing(AppContextT *ctx, bool on) { ctx->fontSmoothing = on; - - if (fontSmoothSet(ctx)) { - invalidateAllWindows(ctx); - } + fontSmoothSet(ctx); } @@ -4551,7 +4642,9 @@ void dvxSetSoundHook(DvxSoundHookT fn) { // ============================================================ void dvxSetTitle(AppContextT *ctx, WindowT *win, const char *title) { - if (!win) { + // The title the window already has changes nothing (no repaint, no + // title-change notice for a caption set on every pass of a loop) + if (!win || (title && strcmp(win->title, title) == 0)) { return; } @@ -4566,6 +4659,15 @@ void dvxSetTitle(AppContextT *ctx, WindowT *win, const char *title) { } +// ============================================================ +// dvxSetWaitPump +// ============================================================ + +void dvxSetWaitPump(DvxWaitPumpT fn) { + sWaitPump = fn; +} + + // ============================================================ // dvxSetWallpaper // ============================================================ @@ -4573,8 +4675,10 @@ void dvxSetTitle(AppContextT *ctx, WindowT *win, const char *title) { // Loads and renders into locals; the previous wallpaper (buffer and // path) is only replaced once the new one is complete, so a bad path or // an out-of-memory build leaves the current wallpaper untouched. The -// builder pumps dvxUpdate; if a timer or idle callback changed the video -// mode meanwhile, the buffer is sized for the old mode and is rebuilt. +// decode and the builder pump dvxUpdate; if a timer or idle callback +// changed the video mode meanwhile, the buffer is sized for the old mode +// and is rebuilt. The decoded picture is kept (sWallRgb) while it is the +// wallpaper, so a wallpaper mode or video mode change only builds. bool dvxSetWallpaper(AppContextT *ctx, const char *path) { // buildWallpaperBuf pumps dvxUpdate to keep the UI alive, and a timer @@ -4589,6 +4693,8 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path) { if (!path) { free(ctx->wallpaperBuf); + free(sWallRgb); + sWallRgb = NULL; ctx->wallpaperBuf = NULL; ctx->wallpaperPitch = 0; ctx->wallpaperPath[0] = '\0'; @@ -4603,17 +4709,27 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path) { bool wasBusy = ctx->busy; dvxSetBusy(ctx, true); + sBuilding = true; - int imgW; - int imgH; - int channels; - uint8_t *rgb = rejectEmptyImage(stbi_load(newPath, &imgW, &imgH, &channels, RGB_CHANNELS), &imgW, &imgH); - uint8_t *buf = NULL; - int32_t pitch = 0; + int imgW = sWallRgbW; + int imgH = sWallRgbH; + bool kept = sWallRgb && platformPathSame(sWallRgbPath, newPath); + uint8_t *rgb = kept ? sWallRgb : NULL; + uint8_t *buf = NULL; + int32_t pitch = 0; + ImageSrcT src; + + memset(&src, 0, sizeof(src)); + src.pumpCtx = ctx; + + src.fp = kept ? NULL : fopen(newPath, "rb"); + + if (src.fp != NULL) { + rgb = imageDecode(&src, &imgW, &imgH, RGB_CHANNELS); + fclose(src.fp); + } if (rgb) { - sBuilding = true; - for (;;) { int32_t builtW = ctx->display.width; int32_t builtH = ctx->display.height; @@ -4628,10 +4744,20 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path) { free(buf); } - sBuilding = false; - stbi_image_free(rgb); + // Kept for the next build when it is the wallpaper now and small + // enough; another picture kept before goes + if (!kept && buf && (int64_t)imgW * imgH * RGB_CHANNELS <= WALLPAPER_KEEP_BYTES) { + free(sWallRgb); + sWallRgb = rgb; + sWallRgbW = imgW; + sWallRgbH = imgH; + snprintf(sWallRgbPath, sizeof(sWallRgbPath), "%s", newPath); + } else if (!kept) { + free(rgb); + } } + sBuilding = false; dvxSetBusy(ctx, wasBusy); if (!buf) { @@ -4754,7 +4880,9 @@ void dvxShutdown(AppContextT *ctx) { ctx->dirty.cap = 0; free(ctx->wallpaperBuf); + free(sWallRgb); ctx->wallpaperBuf = NULL; + sWallRgb = NULL; arrfree(ctx->videoModes); ctx->videoModes = NULL; videoShutdown(&ctx->display); @@ -4957,20 +5085,35 @@ bool dvxUpdate(AppContextT *ctx) { // happens once every callback frame has unwound. ctx->dispatchDepth++; - pollMouse(ctx); - pollKeyboard(ctx); - dispatchEvents(ctx); - exclusiveSync(ctx); - updateTooltip(ctx); - pollWidgets(ctx); + // Input, events, and timers only where the host takes them (inputHere) + bool input = !ctx->inputHere || ctx->inputHere(ctx->idleCtx); - if (sCursorBlinkFn) { - sCursorBlinkFn(); + if (input) { + pollMouse(ctx); + pollKeyboard(ctx); + dispatchEvents(ctx); + exclusiveSync(ctx); + updateTooltip(ctx); + pollWidgets(ctx); + + if (sCursorBlinkFn) { + sCursorBlinkFn(); + } + + wgtUpdateTimers(); } - wgtUpdateTimers(); playOpenSound(ctx); + // A font built again for a smoothing change; every window laid out + // and painted again after the last + if (dvxFontRebuildStep()) { + sFontsRebuilding = true; + } else if (sFontsRebuilding) { + sFontsRebuilding = false; + invalidateAllWindows(ctx); + } + // Flush deferred paints. paintNeeded is set by wgtInvalidatePaint // (PARTIAL) or wgtInvalidate (FULL). Multiple calls per frame are // batched into one paint -- the highest level wins. This loop runs @@ -4997,18 +5140,24 @@ bool dvxUpdate(AppContextT *ctx) { } if (ctx->dirty.count > 0) { - compositeAndFlush(ctx); + compositeDirty(ctx, COMPOSITE_BUDGET_US); + + if (ctx->busyCallback) { + ctx->busyCallback(ctx->idleCtx); + } } else if (ctx->idleCallback) { ctx->idleCallback(ctx->idleCtx); } else { platformYield(); } - releaseKeyPressedButton(ctx); + if (input) { + releaseKeyPressedButton(ctx); - ctx->prevMouseX = ctx->mouseX; - ctx->prevMouseY = ctx->mouseY; - ctx->prevMouseButtons = ctx->mouseButtons; + ctx->prevMouseX = ctx->mouseX; + ctx->prevMouseY = ctx->mouseY; + ctx->prevMouseButtons = ctx->mouseButtons; + } return ctx->running; } @@ -5021,6 +5170,20 @@ bool dvxUpdate(AppContextT *ctx) { // A missing wallpaper key means none. The image is only built again // when the path or the mode differs from the current one. +// ============================================================ +// dvxWaitPump +// ============================================================ + +bool dvxWaitPump(void) { + if (sWaitPump) { + return sWaitPump(); + } + + platformYield(); + return true; +} + + bool dvxWallpaperFromPrefs(AppContextT *ctx, PrefsHandleT *h) { WallpaperModeE mode = (WallpaperModeE)prefsWallpaperChoices[prefsGetChoice(h, DVX_DESKTOP_SECTION, DESKTOP_KEY_MODE, prefsWallpaperChoices, PREFS_WALLPAPER_CHOICE_COUNT, WALLPAPER_MODE_DEFAULT)].value; const char *value = prefsGetString(h, DVX_DESKTOP_SECTION, DESKTOP_KEY_WALLPAPER, ""); @@ -5147,10 +5310,16 @@ static void enumModeCb(int32_t w, int32_t h, int32_t bpp, void *userData) { // Set the owner's mode and hand it the screen. False (the desktop is back // and repainted) when no mode could be set. static bool exclusiveEnter(AppContextT *ctx) { - ExclusiveStateT *ex = &ctx->exclusive; - DisplayT *d = &ex->display; + ExclusiveStateT *ex = &ctx->exclusive; + DisplayT *d = &ex->display; + bool set = platformVideoExclusiveBegin(d, ex->reqW, ex->reqH, ex->reqBpp) == 0; - if (platformVideoExclusiveBegin(d, ex->reqW, ex->reqH, ex->reqBpp) != 0) { + // A mirror copies the window's pixels as they are + if (set && ex->mirror && memcmp(&d->format, &ctx->display.format, sizeof(d->format)) != 0) { + set = false; + } + + if (!set) { dvxLog("Video: no %ldx%ld full-screen mode", (long)ex->reqW, (long)ex->reqH); platformVideoExclusiveEnd(d, &ctx->display); dirtyListInit(&ctx->dirty); @@ -5193,6 +5362,14 @@ static bool exclusiveEnter(AppContextT *ctx) { ctx->prevMouseY = ctx->mouseY; ex->active = true; + + if (ex->mirror) { + RectT all = { 0, 0, d->width, d->height }; + + memset(&ex->mirrorCursor, 0, sizeof(ex->mirrorCursor)); + exclusiveMirrorShow(ctx, &all); + } + exclusiveNotify(ctx, ex->owner, ex->onChange, ex->userData, ExclusiveActiveE); return true; } @@ -5228,7 +5405,153 @@ static void exclusiveLutSet(ExclusiveStateT *ex, int32_t first, int32_t count) { } -// Full screen: any mouse change goes to the owner in screen coordinates. +// A mirror: the part of the owner's content the frame's dirty rectangles +// (screen coordinates of the desktop) cover, shown again. +static void exclusiveMirrorDirty(AppContextT *ctx, const DirtyListT *dl) { + WindowT *win = ctx->exclusive.owner; + RectT content = { win->x + win->contentX, win->y + win->contentY, win->contentW, win->contentH }; + int32_t x0 = INT32_MAX; + int32_t y0 = INT32_MAX; + int32_t x1 = INT32_MIN; + int32_t y1 = INT32_MIN; + + for (int32_t i = 0; i < dl->count; i++) { + RectT part; + + if (rectIntersect(&dl->rects[i], &content, &part)) { + x0 = DVX_MIN(x0, part.x); + y0 = DVX_MIN(y0, part.y); + x1 = DVX_MAX(x1, part.x + part.w); + y1 = DVX_MAX(y1, part.y + part.h); + } + } + + if (x1 <= x0) { + return; + } + + RectT place; + int32_t scale = exclusiveMirrorPlace(ctx, &place); + RectT shown = { place.x + (x0 - content.x) * scale, place.y + (y0 - content.y) * scale, (x1 - x0) * scale, (y1 - y0) * scale }; + + exclusiveMirrorShow(ctx, &shown); +} + + +// A mirror: the mouse in the owner's content coordinates, and its cursor +// (the shape the content asks for) moved. +static void exclusiveMirrorMouse(AppContextT *ctx) { + ExclusiveStateT *ex = &ctx->exclusive; + WindowT *win = ex->owner; + RectT place; + int32_t scale = exclusiveMirrorPlace(ctx, &place); + int32_t cx = (ctx->mouseX - place.x) / scale; + int32_t cy = (ctx->mouseY - place.y) / scale; + + if (ctx->mouseX != ctx->prevMouseX || ctx->mouseY != ctx->prevMouseY) { + RectT old = ex->mirrorCursor; + + ctx->cursorId = ctx->busy ? CURSOR_BUSY : contentCursorShape(ctx, win, cx, cy); + exclusiveMirrorShow(ctx, &old); + + const CursorT *cur = &ctx->cursors[ctx->cursorId]; + RectT now = { ctx->mouseX - cur->hotX, ctx->mouseY - cur->hotY, cur->width, cur->height }; + + exclusiveMirrorShow(ctx, &now); + } + + if (win->onMouse && !win->destroyPending) { + WIN_CALLBACK(ctx, win, win->onMouse(win, cx, cy, ctx->mouseButtons)); + } +} + + +// Where a mirror shows the owner's content on the full-screen display +// (*place) and its scale: the largest whole factor that fits, centered. +static int32_t exclusiveMirrorPlace(AppContextT *ctx, RectT *place) { + ExclusiveStateT *ex = &ctx->exclusive; + DisplayT *d = &ex->display; + WindowT *win = ex->owner; + int32_t srcW = DVX_MAX(1, win->contentW); + int32_t srcH = DVX_MAX(1, win->contentH); + int32_t scale = DVX_MAX(1, DVX_MIN(d->width / srcW, d->height / srcH)); + + place->w = DVX_MIN(srcW * scale, d->width); + place->h = DVX_MIN(srcH * scale, d->height); + place->x = (d->width - place->w) / 2; + place->y = (d->height - place->h) / 2; + return scale; +} + + +// A mirror: screen rectangle r drawn again from the owner's content +// (black around it) -- all of the screen when the content's place moved +// (a new size) -- the mouse cursor on top, and flushed. +static void exclusiveMirrorShow(AppContextT *ctx, const RectT *r) { + ExclusiveStateT *ex = &ctx->exclusive; + DisplayT *d = &ex->display; + WindowT *win = ex->owner; + RectT screen = { 0, 0, d->width, d->height }; + RectT clip; + RectT place; + int32_t scale = exclusiveMirrorPlace(ctx, &place); + int32_t bpp = d->format.bytesPerPixel; + + if (memcmp(&place, &ex->presentRect, sizeof(place)) != 0) { + ex->presentRect = place; + r = &screen; + } + + if (!win->contentBuf || !rectIntersect(r, &screen, &clip)) { + return; + } + + // Each row: black, the content's pixels (each repeated scale times), + // black + int32_t right = clip.x + clip.w; + + for (int32_t y = clip.y; y < clip.y + clip.h; y++) { + uint8_t *dst = d->backBuf + (size_t)y * (size_t)d->pitch; + bool inRows = y >= place.y && y < place.y + place.h; + int32_t in0 = inRows ? DVX_MAX(clip.x, place.x) : right; + int32_t in1 = inRows ? DVX_MAX(in0, DVX_MIN(right, place.x + place.w)) : right; + + memset(dst + (size_t)clip.x * (size_t)bpp, 0, (size_t)(in0 - clip.x) * (size_t)bpp); + memset(dst + (size_t)in1 * (size_t)bpp, 0, (size_t)(right - in1) * (size_t)bpp); + + if (in1 <= in0) { + continue; + } + + const uint8_t *src = win->contentBuf + (size_t)((y - place.y) / scale) * (size_t)win->contentPitch; + + if (scale == 1) { + memcpy(dst + (size_t)in0 * (size_t)bpp, src + (size_t)(in0 - place.x) * (size_t)bpp, (size_t)(in1 - in0) * (size_t)bpp); + continue; + } + + for (int32_t x = in0; x < in1; x++) { + pixelPut(dst + x * bpp, pixelGet(src + (x - place.x) / scale * bpp, bpp), bpp); + } + } + + const CursorT *cur = &ctx->cursors[ctx->cursorId]; + RectT cursor = { ctx->mouseX - cur->hotX, ctx->mouseY - cur->hotY, cur->width, cur->height }; + RectT over; + + if (rectIntersect(&cursor, &clip, &over)) { + setClipRect(d, clip.x, clip.y, clip.w, clip.h); + drawMaskedBitmap(d, &ex->blitOps, cursor.x, cursor.y, cur->width, cur->height, cur->andMask, cur->xorData, ctx->colors.cursorFg, ctx->colors.cursorBg); + resetClipRect(d); + } + + ex->mirrorCursor = cursor; + platformVideoExclusiveFlush(d, &clip); +} + + +// Full screen: any mouse change goes to the owner in screen coordinates +// (a mirror's in content coordinates, exclusiveMirrorMouse). static void exclusiveMouse(AppContextT *ctx) { WindowT *win = ctx->exclusive.owner; @@ -5236,6 +5559,11 @@ static void exclusiveMouse(AppContextT *ctx) { return; } + if (ctx->exclusive.mirror) { + exclusiveMirrorMouse(ctx); + return; + } + if (win->onMouse && !win->destroyPending) { WIN_CALLBACK(ctx, win, win->onMouse(win, ctx->mouseX, ctx->mouseY, ctx->mouseButtons)); } @@ -5250,6 +5578,49 @@ static void exclusiveNotify(AppContextT *ctx, WindowT *win, ExclusiveFnT fn, voi } +// dvxExclusiveBegin and dvxExclusiveMirror: win takes the whole screen +// (a mirror of its content when mirror is set). +static int32_t exclusiveStart(AppContextT *ctx, WindowT *win, int32_t width, int32_t height, int32_t bpp, ExclusiveFnT onChange, void *userData, bool mirror) { + ExclusiveStateT *ex = &ctx->exclusive; + + if (!win || windowIndex(ctx, win) < 0 || win->destroyPending || width <= 0 || height <= 0) { + return DVX_EXCL_ERR_ARGS; + } + + if (ex->owner && ex->owner != win) { + return DVX_EXCL_ERR_BUSY; + } + + // A new size for the owner: let go of the old mode first. + exclusiveLeave(ctx); + + if (!ex->owner) { + dvxGeneratePalette(ex->palette); + } + + ex->owner = win; + ex->reqW = width; + ex->reqH = height; + ex->reqBpp = bpp; + ex->onChange = onChange; + ex->userData = userData; + ex->mirror = mirror; + + if (win->minimized) { + dvxRestoreWindow(ctx, win); + } + + raiseAndFocus(ctx, windowIndex(ctx, win)); + + if (!exclusiveEnter(ctx)) { + ex->owner = NULL; + return DVX_EXCL_ERR_MODE; + } + + return DVX_EXCL_OK; +} + + // Once a frame: full screen follows the owner's focus. Losing it (Alt+Tab, // Ctrl+Esc, a dialog, minimizing) suspends; getting it back sets the mode // again, and when that fails full screen is over. @@ -5384,7 +5755,7 @@ static uint8_t *finishLoadImage(const DisplayT *d, uint8_t *rgb, int32_t imgW, i int32_t pitch; uint8_t *buf = convertRgbToNative(d, rgb, RGB_CHANNELS, imgW, imgH, 0, NULL, &pitch); - stbi_image_free(rgb); + free(rgb); if (!buf) { return NULL; @@ -5498,18 +5869,28 @@ static void flushPendingDestroys(AppContextT *ctx) { // wgtInvalidatePaint PAINT_PARTIAL) now. Shared by the dvxUpdate flush // and the content-buffer readers that must not see a stale buffer. A // deferred-destroyed window is skipped: its app state may be torn down. +// A painter that narrowed the area (widgetOnPaint after a partial pass) +// changed only that part, and only that part is composited; else the +// whole window is. static void flushWindowPaint(AppContextT *ctx, WindowT *win) { if (win->destroyPending || !win->paintNeeded || !win->onPaint) { return; } RectT fullRect = {0, 0, win->contentW, win->contentH}; - WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect)); + RectT area = fullRect; + + WIN_CALLBACK(ctx, win, win->onPaint(win, &area)); // widgetOnPaint clears paintNeeded for widget windows. // For raw-paint windows, clear it here. win->paintNeeded = PAINT_NONE; win->iconNeedsRefresh = true; - dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); + + if (area.x == fullRect.x && area.y == fullRect.y && area.w == fullRect.w && area.h == fullRect.h) { + dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); + } else if (area.w > 0 && area.h > 0) { + dvxInvalidateRect(ctx, win, area.x, area.y, area.w, area.h); + } } @@ -5568,17 +5949,11 @@ static void focusWindowAt(AppContextT *ctx, int32_t idx) { } -// Smooth the fonts (dvxFontSetSmooth) when the user wants it and the -// screen has direct color; true when that changed what text looks like -static bool fontSmoothSet(const AppContextT *ctx) { - bool smooth = ctx->fontSmoothing && ctx->display.format.bitsPerPixel >= FONT_SMOOTH_MIN_BPP; - - if (smooth == dvxFontSmooth()) { - return false; - } - - dvxFontSetSmooth(smooth); - return true; +// Smooth the fonts when the user wants it and the screen has direct +// color: the fonts open are built again a frame at a time (dvxUpdate, +// dvxFontRebuildStep), and every window laid out again after the last. +static void fontSmoothSet(const AppContextT *ctx) { + dvxFontSmoothBegin(ctx->fontSmoothing && ctx->display.format.bitsPerPixel >= FONT_SMOOTH_MIN_BPP); } @@ -6172,6 +6547,105 @@ static void handleTabKey(AppContextT *ctx, WindowT *win, int32_t key, int32_t sh // illusion. The dark charcoal active title bar distinguishes it from // GEOS's blue, giving DVX its own identity. +// Decode an image through stb's read callbacks from src (channels per +// pixel asked for); NULL when it is no image (or one without pixels). +static uint8_t *imageDecode(ImageSrcT *src, int *imgW, int *imgH, int channels) { + static const DvxImgIoT io = { imageSrcRead, imageSrcSkip, imageSrcEof }; + + return rejectEmptyImage(dvxImgDecode(&io, src, imgW, imgH, channels), imgW, imgH); +} + + +// An image's size as stb_image's info gives it (w, h), for dvxImageInfo: +// a top-down BMP's height comes as stored, negative, and it loads at its +// magnitude (INT_MIN has none: the load refuses that height as well); an +// image without pixels does not load (rejectEmptyImage), so it has no +// size either. +static bool imageInfoTake(int w, int h, int32_t *outW, int32_t *outH) { + if (h < 0 && h != INT_MIN) { + h = -h; + } + + if (w <= 0 || h <= 0) { + return false; + } + + if (outW) { + *outW = w; + } + + if (outH) { + *outH = h; + } + + return true; +} + + +// The context an image decode pumps (a frame every IMAGE_PUMP_BYTES) for a +// caller on ctx: only on a task that takes no input (a program on its own +// task), where a frame runs the other tasks and none of the caller's +// code; NULL elsewhere (a frame there could run the caller's own handlers +// inside its call). +static AppContextT *imagePumpCtx(const AppContextT *ctx) { + return (ctx->inputHere && !ctx->inputHere(ctx->idleCtx)) ? (AppContextT *)ctx : NULL; +} + + +static int imageSrcEof(void *user) { + const ImageSrcT *src = (const ImageSrcT *)user; + + return src->fp ? (feof(src->fp) || ferror(src->fp)) : src->pos >= src->len; +} + + +// stb_image's read: from the file or the bytes; a frame pumped every +// IMAGE_PUMP_BYTES when the source has a context to pump. +static int imageSrcRead(void *user, char *out, int size) { + ImageSrcT *src = (ImageSrcT *)user; + int got; + + if (src->fp) { + got = (int)fread(out, 1, (size_t)size, src->fp); + } else { + got = DVX_MIN(size, src->len - src->pos); + memcpy(out, src->data + src->pos, (size_t)got); + src->pos += got; + } + + src->sincePump += got; + + if (src->pumpCtx && src->sincePump >= IMAGE_PUMP_BYTES) { + src->sincePump = 0; + dvxUpdate(src->pumpCtx); + } + + return got; +} + + +// stb_image's skip: n bytes on (or back, when negative). A file reads a +// byte after the seek and puts it back, as stb's own stdio skip does: a +// seek clears the end-of-file flag, and one past the end with no read +// after it would leave imageSrcEof false for ever (a truncated JPEG then +// looped in stb's marker scan). +static void imageSrcSkip(void *user, int n) { + ImageSrcT *src = (ImageSrcT *)user; + + if (src->fp) { + fseek(src->fp, n, SEEK_CUR); + + int ch = fgetc(src->fp); + + if (ch != EOF) { + ungetc(ch, src->fp); + } + } else { + src->pos = clampInt(src->pos + n, 0, src->len); + } +} + + static void initColorScheme(AppContextT *ctx) { // Load defaults into the RGB source array, then pack all at once memcpy(ctx->colorRgb, sDefaultColors, sizeof(sDefaultColors)); @@ -6270,6 +6744,18 @@ static void invalidateAllWindows(AppContextT *ctx) { } +// Every window painted in full at the next frame's paint (flushWindowPaint) +// and the whole screen composited: however many changes come before that +// (a program setting every colour, one call each), one paint. +static void invalidateAllWindowsLater(AppContextT *ctx) { + for (int32_t i = 0; i < ctx->stack.count; i++) { + ctx->stack.windows[i]->paintNeeded = PAINT_FULL; + } + + dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height); +} + + // True when a window can take keyboard focus: visible, not minimized // (a minimized window is only an icon) and not queued for destroy. The // single rule for Alt+Tab and every focus-successor choice. @@ -7035,7 +7521,7 @@ static void refreshMinimizedIcons(AppContextT *ctx) { static uint8_t *rejectEmptyImage(uint8_t *pixels, const int *imgW, const int *imgH) { if (pixels && (*imgW <= 0 || *imgH <= 0)) { - stbi_image_free(pixels); + free(pixels); return NULL; } @@ -7131,20 +7617,14 @@ static int32_t saveImageAs(const DisplayT *fmt, const uint8_t *data, int32_t w, return -1; } - uint8_t *rgb = bufferToRgb(fmt, data, w, h, pitch); + uint8_t *rgb = dvxBufferToRgb(fmt, data, w, h, pitch); if (!rgb) { return -1; } const char *ext = strrchr(path, '.'); - int ok; - - if (ext && strcasecmp(ext, BMP_EXT) == 0) { - ok = stbi_write_bmp(path, w, h, RGB_CHANNELS, rgb); - } else { - ok = stbi_write_png(path, w, h, RGB_CHANNELS, rgb, w * RGB_CHANNELS); - } + bool ok = dvxImgWrite(path, ext && strcasecmp(ext, BMP_EXT) == 0, w, h, rgb); free(rgb); @@ -7278,18 +7758,6 @@ static const char *splitMenuLabel(const char *label, char leftBuf[MAX_MENU_LABEL } -// Stores one packed pixel at column x of a row in the display format. -static inline void storePixel(uint8_t *row, int32_t x, int32_t bytesPerPx, uint32_t px) { - if (bytesPerPx == sizeof(uint16_t)) { - ((uint16_t *)row)[x] = (uint16_t)px; - } else if (bytesPerPx == sizeof(uint32_t)) { - ((uint32_t *)row)[x] = px; - } else { - row[x] = (uint8_t)px; - } -} - - // Keys go to win as in game input mode: it asked for them, or it has full // screen (menus there could not be seen). static bool takesGameKeys(const AppContextT *ctx, const WindowT *win) { @@ -7377,32 +7845,7 @@ static void updateCursorShape(AppContextT *ctx) { int32_t cy; windowContentPoint(win, mx, my, &cx, &cy); - - if (win->onCursorQuery) { - int32_t shape; - WIN_CALLBACK(ctx, win, shape = win->onCursorQuery(win, cx, cy)); - - if (shape > 0) { - newCursor = shape; - } - } - - // The widget under the cursor inside a clipping container (a - // text box in a ScrollPane) decides first, then the container - // itself (a Splitter's divider). - if (newCursor == CURSOR_ARROW && win->widgetRoot) { - WidgetT *target = widgetCallbackTarget(win->widgetRoot, cx, cy); - WidgetT *hit = widgetHitTest(win->widgetRoot, cx, cy); - int32_t shape = target ? wclsGetCursorShape(target, cx, cy) : 0; - - if (shape <= 0 && hit && hit != target) { - shape = wclsGetCursorShape(hit, cx, cy); - } - - if (shape > 0) { - newCursor = shape; - } - } + newCursor = contentCursorShape(ctx, win, cx, cy); } } diff --git a/src/libs/kpunch/libdvx/dvxApp.h b/src/libs/kpunch/libdvx/dvxApp.h index 9cca1d15..0827419a 100644 --- a/src/libs/kpunch/libdvx/dvxApp.h +++ b/src/libs/kpunch/libdvx/dvxApp.h @@ -65,6 +65,7 @@ typedef void (*ExclusiveFnT)(WindowT *win, ExclusiveStateE state, void *userData typedef struct { WindowT *owner; // window holding full screen (NULL: none) bool active; // the screen is the owner's now (false: suspended) + bool mirror; // the owner's content is shown (dvxExclusiveMirror) int32_t reqW; // what the owner asked for int32_t reqH; int32_t reqBpp; @@ -74,7 +75,8 @@ typedef struct { BlitOpsT blitOps; // span operations for display's format uint8_t palette[PAL_ENTRIES * PAL_CHANNELS]; // the owner's palette (the DAC at 8 bpp) uint32_t lut[PAL_ENTRIES]; // palette index -> pixel, above 8 bpp - RectT presentRect; // where dvxExclusivePresent8 last put the picture + RectT presentRect; // where dvxExclusivePresent8 or the mirror last put the picture + RectT mirrorCursor; // where the mirror last drew the mouse cursor int32_t savedMouseX; // desktop mouse position to go back to int32_t savedMouseY; } ExclusiveStateT; @@ -137,7 +139,17 @@ typedef struct AppContextT { // The idle callback allows the host application (e.g. the DVX shell) // to do background work (poll serial ports, service DXE apps) during // frames where no GUI events occurred, instead of just yielding the CPU. + // The busy callback runs after a frame that painted instead (never + // both), so the host's background work and cooperative yield go on + // while the screen keeps changing. Both are given idleCtx. void (*idleCallback)(void *ctx); // called instead of yield when non-NULL + void (*busyCallback)(void *ctx); // called after a painted frame when non-NULL + // Whether this frame takes input (polls the mouse and keyboard, + // dispatches events, fires widget timers): the shell lets only its own + // task do it, so a program's callbacks never run on another program's + // stack. A frame that does not just paints. NULL: every frame does. + // Given idleCtx. + bool (*inputHere)(void *ctx); void *idleCtx; WindowT *modalWindow; // if non-NULL, only this window receives input // Deferred window destruction. Destroying a window from inside a @@ -225,8 +237,9 @@ void dvxMouseToPrefs(const AppContextT *ctx, PrefsHandleT *h); // Smooth (anti-alias) text, or stop: on (the default) it is smoothed at // more than 8 bits per pixel, in the faces and sizes the FONTS folder -// has smoothed. Every window is laid out and painted again when what is -// drawn changes; a video mode change applies it to the new depth. +// has smoothed. The fonts open are built again a frame at a time, and +// every window is laid out and painted again after the last; a video mode +// change applies it to the new depth. void dvxSetFontSmoothing(AppContextT *ctx, bool on); // Apply the DVX.INI [fonts] settings in h: smoothing (on when missing). @@ -282,6 +295,16 @@ void dvxPlaySoundWait(DvxSoundE event); // The default beep: dvxPlaySound(SoundDefaultE). void dvxBeep(void); +// Waiting in a library (for a sound to end, say): dvxWaitPump between the +// checks lets DVX go on, and with it the events of the BASIC program that +// waits (the hook the BASIC runtime sets), as DoEvents does. False: stop +// waiting (the program is ending, or its code may not wait). Without a +// hook it only yields the processor. +typedef bool (*DvxWaitPumpT)(void); + +void dvxSetWaitPump(DvxWaitPumpT fn); +bool dvxWaitPump(void); + // ============================================================ // Full-screen exclusive mode // ============================================================ @@ -324,6 +347,15 @@ DisplayT *dvxExclusiveDisplay(AppContextT *ctx); // have full screen. void dvxExclusiveEnd(AppContextT *ctx, WindowT *win); +// Take the whole screen for win's own content (a window of controls, a +// drawing): the smallest mode of at least its content's size in the +// desktop's pixel format, the content scaled by the largest whole factor +// that fits and centered on black, and shown again as the window paints. +// The mouse works on the content as in the window (onMouse gets content +// coordinates) and its cursor is drawn. Otherwise as dvxExclusiveBegin; +// DVX_EXCL_ERR_MODE also when there is no mode in the desktop's format. +int32_t dvxExclusiveMirror(AppContextT *ctx, WindowT *win, ExclusiveFnT onChange, void *userData); + // Copy a rectangle of the backbuffer to the screen (NULL: all of it). void dvxExclusiveFlush(AppContextT *ctx, const RectT *r); @@ -362,8 +394,9 @@ void dvxExclusiveWaitRetrace(void); int32_t dvxRunDos(AppContextT *ctx, const char *program, const char *args, const char *folder, bool pause); // Whether the library (or widget) name -- its file's base name, any case, -// as DECLARE LIBRARY names it: "pkgnet", "dvxsql" -- is loaded, that is -// installed and started with DVX (the loader loads every one at start). +// as DECLARE LIBRARY names it: "pkgnet", "dvxsql" -- is installed and +// loaded. The loader loads a library when something first needs it, so +// asking loads one that is installed and not loaded yet. // A program using a library of an optional part of DVX (Networking, // Databases, ...) asks before it calls one of its functions. bool dvxLibraryLoaded(const char *name); @@ -383,8 +416,9 @@ uint32_t dvxIdleMs(const AppContextT *ctx); // Color scheme // ============================================================ -// Set a single color by ID. Repacks to native pixel format and -// invalidates the entire screen. +// Set a single color by ID. Repacks to native pixel format; every +// window is painted again at the next frame (once, however many colors +// change before it) and the whole screen composited. void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b); // Get a color's RGB values by ID. @@ -458,10 +492,11 @@ void dvxShutdown(AppContextT *ctx); // becomes false (typically from dvxQuit() or a close callback). void dvxRun(AppContextT *ctx); -// Process exactly one frame of the event loop. Provided for applications -// that need to integrate the GUI into their own main loop (e.g. the DVX -// shell polling serial ports between frames). Returns false when the GUI -// wants to exit. +// Process exactly one frame of the event loop: input, events, and timers +// (where the context takes them: inputHere), paints, the composite, and the +// idle or busy callback. Provided for applications that need to integrate +// the GUI into their own main loop (e.g. the DVX shell polling serial ports +// between frames). Returns false when the GUI wants to exit. bool dvxUpdate(AppContextT *ctx); // Create a window at an explicit screen position. The window is raised @@ -599,7 +634,10 @@ int32_t dvxWindowScreenshot(AppContextT *ctx, WindowT *win, const char *path); // Down, End, Enter, Esc, Home, Ins, Left, PgDn, PgUp, Right, Space, Tab, // Up); "" for key 0. dvxAccelParse reads such text back (case does not // matter; Delete and Insert are also accepted); false, with *outKey 0, -// for anything else. +// for anything else. DVX_ACCEL_TEXT_MAX holds the longest such text +// ("Ctrl+Alt+Shift+Backspace") with its NUL. +#define DVX_ACCEL_TEXT_MAX 32 + void dvxAccelFormat(int32_t key, int32_t modifiers, char *out, int32_t outSize); bool dvxAccelParse(const char *text, int32_t *outKey, int32_t *outMods); @@ -660,6 +698,13 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId); // goes away, even from inside one of its callbacks). void dvxAppHungForget(int32_t appId); +// Book a handler of appId as running (dvxAppIsHung counts from the +// outermost one's start), and its end. Window callbacks are booked by +// DVX itself; a host that runs an app's handlers outside them (the BASIC +// runtime, running events on the program's own task) brackets each. +void dvxAppCallEnter(int32_t appId); +void dvxAppCallLeave(int32_t appId); + // Window arrangement functions -- operate on all visible, non-minimized // windows. These mirror the classic Windows 3.x "Window" menu commands. @@ -695,10 +740,25 @@ uint8_t *dvxLoadImageAlpha(const AppContextT *ctx, const uint8_t *data, int32_t // Free a pixel buffer returned by dvxLoadImage. void dvxFreeImage(uint8_t *data); -// Query image dimensions without decoding the full file. -// Returns true on success, false if the file can't be read. +// The image loaders decode a big picture (seconds for a large JPEG) a +// part at a time when called from a program's own task: DVX and the other +// programs run between the parts, none of the caller's code does. On the +// task that takes the input (C programs' callbacks) they decode in one go. + +// Query image dimensions without decoding the full file (its header +// only: a damaged picture may still fail to load). Returns true on +// success, false if the file can't be read or is no picture. bool dvxImageInfo(const char *path, int32_t *outW, int32_t *outH); +// dvxImageInfo for a picture's bytes in memory. +bool dvxImageInfoFromMemory(const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH); + +// Native-format pixels (in d's format: a buffer as dvxLoadImage gives it, +// a window's content, a canvas's) as R, G, B bytes, malloc'd (NULL when +// out of memory). Table driven: the fast way to hand the screen's pixels +// to code that wants RGB (image files, printing). +uint8_t *dvxBufferToRgb(const DisplayT *d, const uint8_t *buf, int32_t w, int32_t h, int32_t pitch); + // Save native-format pixel data to an image file: BMP when path ends in // .bmp (any case), PNG otherwise. The pixel data must be in the display's // native format (as returned by dvxLoadImage or captured from a content @@ -765,9 +825,10 @@ uint8_t *dvxResLoadIcon(AppContextT *ctx, const char *dxePath, const char *resNa // neither (or is no DVX file). Free with free(); *outSize gets the size. void *dvxResLoadIconData(const char *dxePath, uint32_t *outSize); -// Load a text resource into a caller-provided buffer. +// Load a text resource into a caller-provided buffer (dvxResReadText). // Returns true on success. The text is null-terminated and truncated -// to fit bufSize. Returns false if the resource is not found. +// to fit bufSize. Returns false if the resource is not found (buf is +// left alone) or cannot be read (buf is then ""). bool dvxResLoadText(const char *dxePath, const char *resName, char *buf, int32_t bufSize); // Load a raw binary resource. Returns a malloc'd buffer that the diff --git a/src/libs/kpunch/libdvx/dvxBas.c b/src/libs/kpunch/libdvx/dvxBas.c index c92a9f38..c5084754 100644 --- a/src/libs/kpunch/libdvx/dvxBas.c +++ b/src/libs/kpunch/libdvx/dvxBas.c @@ -30,6 +30,8 @@ #include "thirdparty/stb_ds_wrap.h" #include +#include +#include #include #define SHELL_CLEANUP_SYM "shellRegisterAppCleanup" @@ -50,9 +52,11 @@ static void appEnded(int32_t appId); int32_t basHandleAdd(BasHandlesT *t, void *obj); void basHandleFree(BasHandlesT *t, int32_t handle); void *basHandleGet(const BasHandlesT *t, int32_t handle); +void basHandlesAppEnded(BasHandlesT *t, int32_t appId); void basTextAppend(BasTextT *t, const char *s); void basTextAppendLen(BasTextT *t, const char *s, int32_t len); const char *basTextEnd(BasTextT *t); +const char *basTextFormat(BasTextT *t, const char *fmt, ...); void basTextRow(BasTextT *t, const char *const *cells, int32_t count); const char *basTextSet(BasTextT *t, const char *s); void basTextStart(BasTextT *t); @@ -61,11 +65,7 @@ void basTextStart(BasTextT *t); // The shell's app cleanup: what the program that ended still holds. static void appEnded(int32_t appId) { for (int32_t t = 0; t < (int32_t)arrlen(sTables); t++) { - for (int32_t i = 0; i < (int32_t)arrlen(sTables[t]->objs); i++) { - if (sTables[t]->objs[i] && sTables[t]->owners[i] == appId) { - basHandleFree(sTables[t], i + 1); - } - } + basHandlesAppEnded(sTables[t], appId); } } @@ -141,6 +141,15 @@ void *basHandleGet(const BasHandlesT *t, int32_t handle) { } +void basHandlesAppEnded(BasHandlesT *t, int32_t appId) { + for (int32_t i = 0; i < (int32_t)arrlen(t->objs); i++) { + if (t->objs[i] && t->owners[i] == appId) { + basHandleFree(t, i + 1); + } + } +} + + void basTextAppend(BasTextT *t, const char *s) { basTextAppendLen(t, s, (int32_t)strlen(s)); } @@ -166,6 +175,34 @@ const char *basTextEnd(BasTextT *t) { } +// Measured first, then formatted straight into the result. +const char *basTextFormat(BasTextT *t, const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + int32_t n = (int32_t)vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + + basTextStart(t); + + if (n < 0) { + return basTextEnd(t); + } + + arrsetlen(t->buf, n + 1); + + if ((int32_t)arrlen(t->buf) != n + 1) { + arrsetlen(t->buf, 0); + return ""; + } + + va_start(ap, fmt); + vsnprintf(t->buf, (size_t)n + 1, fmt, ap); + va_end(ap); + return t->buf; +} + + void basTextRow(BasTextT *t, const char *const *cells, int32_t count) { for (int32_t i = 0; i < count; i++) { if (i > 0) { diff --git a/src/libs/kpunch/libdvx/dvxBas.h b/src/libs/kpunch/libdvx/dvxBas.h index 5e1f2852..c73df2c3 100644 --- a/src/libs/kpunch/libdvx/dvxBas.h +++ b/src/libs/kpunch/libdvx/dvxBas.h @@ -71,6 +71,11 @@ int32_t basHandleAdd(BasHandlesT *t, void *obj); // Free handle's object (freeFn) and the handle; a bad handle is ignored. void basHandleFree(BasHandlesT *t, int32_t handle); +// Free every handle of t the program appId holds. The shell's app +// cleanup does this for every table; a library that has its own end hook +// can call it too. +void basHandlesAppEnded(BasHandlesT *t, int32_t appId); + // The object of handle, or NULL for a bad handle. void *basHandleGet(const BasHandlesT *t, int32_t handle); @@ -88,6 +93,9 @@ void basTextRow(BasTextT *t, const char *const *cells, int32_t count); // The result built: a NUL-terminated copy ("" when out of memory). const char *basTextEnd(BasTextT *t); +// printf-style text as the result ("" when out of memory). +const char *basTextFormat(BasTextT *t, const char *fmt, ...) __attribute__((format(printf, 2, 3))); + // s (NULL: "") as the result: basTextStart, basTextAppend, basTextEnd. const char *basTextSet(BasTextT *t, const char *s); diff --git a/src/libs/kpunch/libdvx/dvxDialog.c b/src/libs/kpunch/libdvx/dvxDialog.c index c5f566bc..3a255c1f 100644 --- a/src/libs/kpunch/libdvx/dvxDialog.c +++ b/src/libs/kpunch/libdvx/dvxDialog.c @@ -1779,14 +1779,7 @@ static void fdLoadDir(FileDialogStateT *fd) { continue; } - char fullPath[DVX_MAX_PATH]; - struct stat st; - - if (!platformPathJoin(fullPath, sizeof(fullPath), fd->curDir, ent->d_name) || stat(fullPath, &st) != 0) { - continue; - } - - bool isDir = S_ISDIR(st.st_mode); + bool isDir = platformDirEntryIsDir(fd->curDir, ent->d_name, ent->d_type); // Apply filter to files only; a folder dialog lists no files if (!isDir && ((fd->flags & FD_FOLDER) || !platformFilterMatch(ent->d_name, pattern))) { diff --git a/src/libs/kpunch/libdvx/dvxDraw.c b/src/libs/kpunch/libdvx/dvxDraw.c index fc661fc3..c2daabf6 100644 --- a/src/libs/kpunch/libdvx/dvxDraw.c +++ b/src/libs/kpunch/libdvx/dvxDraw.c @@ -152,7 +152,7 @@ static inline void nibbleRow16(uint8_t *dst, const NibbleTableT *t, uint8_t bi static inline void nibbleRow32(uint8_t *dst, const NibbleTableT *t, uint8_t bits); static inline void nibbleRow8(uint8_t *dst, const NibbleTableT *t, uint8_t bits); static void nibbleTableSet(NibbleTableT *t, uint32_t fg, uint32_t bg, int32_t bpp); -static inline void putPixel(uint8_t *dst, uint32_t color, int32_t bpp); +static inline void scaleStep(int32_t *q, int32_t *r, int32_t qStep, int32_t rStep, int32_t dstW); static void termCellColors(uint8_t attr, const uint32_t *palette, bool blinkVisible, bool cursor, uint32_t *fg, uint32_t *bg); // Bit lookup tables for glyph and mask rendering. On a 486, a variable @@ -312,10 +312,18 @@ static inline void clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t * int32_t rx2 = rx1 + *w; int32_t ry2 = ry1 + *h; - if (__builtin_expect(rx1 < d->clipX, 0)) { rx1 = d->clipX; } - if (__builtin_expect(ry1 < d->clipY, 0)) { ry1 = d->clipY; } - if (__builtin_expect(rx2 > cx2, 0)) { rx2 = cx2; } - if (__builtin_expect(ry2 > cy2, 0)) { ry2 = cy2; } + if (__builtin_expect(rx1 < d->clipX, 0)) { + rx1 = d->clipX; + } + if (__builtin_expect(ry1 < d->clipY, 0)) { + ry1 = d->clipY; + } + if (__builtin_expect(rx2 > cx2, 0)) { + rx2 = cx2; + } + if (__builtin_expect(ry2 > cy2, 0)) { + ry2 = cy2; + } *x = rx1; *y = ry1; @@ -507,13 +515,21 @@ static int32_t drawCharFixed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT int32_t rowStart = 0; int32_t rowEnd = chh; - if (y < clipY1) { rowStart = clipY1 - y; } - if (y + chh > clipY2) { rowEnd = clipY2 - y; } + if (y < clipY1) { + rowStart = clipY1 - y; + } + if (y + chh > clipY2) { + rowEnd = clipY2 - y; + } int32_t colStart = 0; int32_t colEnd = cw; - if (x < clipX1) { colStart = clipX1 - x; } - if (x + cw > clipX2) { colEnd = clipX2 - x; } + if (x < clipX1) { + colStart = clipX1 - x; + } + if (x + cw > clipX2) { + colEnd = clipX2 - x; + } // Unclipped fast path: when the character cell is fully within the // clip rect we can skip per-pixel clip checks and use the fully @@ -644,7 +660,7 @@ static int32_t drawCharFixed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT // ============================================================ // // Draws a dotted (every-other-pixel) rectangle to indicate keyboard -// focus, matching the Windows/Motif convention. Uses putPixel per +// focus, matching the Windows/Motif convention. Uses pixelPut per // dot rather than spanFill because the alternating pattern can't be // expressed as a span fill (which writes uniform color). // @@ -769,7 +785,7 @@ static void drawGlyphBits(DisplayT *d, const BlitOpsT *ops, const uint8_t *bits, for (int32_t col = 0; col < GLYPH_COLS; col++) { if ((byte & sGlyphBit[col]) && px + col >= clipX1 && px + col < clipX2) { - putPixel(line + (px + col) * bpp, fg, bpp); + pixelPut(line + (px + col) * bpp, fg, bpp); } } } @@ -806,7 +822,7 @@ static void drawGlyphGray(DisplayT *d, const BlitOpsT *ops, const uint8_t *bits, } if (level == FONT_GRAY_FULL || (bpp == 1 && level >= FONT_GRAY_HALF)) { - putPixel(dst, fg, bpp); + pixelPut(dst, fg, bpp); } else if (bpp > 1) { uint32_t back = (bpp == 2) ? *(const uint16_t *)dst : *(const uint32_t *)dst; @@ -816,7 +832,7 @@ static void drawGlyphGray(DisplayT *d, const BlitOpsT *ops, const uint8_t *bits, have[level] = true; } - putPixel(dst, lastOut[level], bpp); + pixelPut(dst, lastOut[level], bpp); } } } @@ -920,13 +936,21 @@ void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, in int32_t rowStart = 0; int32_t rowEnd = h; - if (y < clipY1) { rowStart = clipY1 - y; } - if (y + h > clipY2) { rowEnd = clipY2 - y; } + if (y < clipY1) { + rowStart = clipY1 - y; + } + if (y + h > clipY2) { + rowEnd = clipY2 - y; + } int32_t colStart = 0; int32_t colEnd = w; - if (x < clipX1) { colStart = clipX1 - x; } - if (x + w > clipX2) { colEnd = clipX2 - x; } + if (x < clipX1) { + colStart = clipX1 - x; + } + if (x + w > clipX2) { + colEnd = clipX2 - x; + } // The mask rows index sMaskBit[col], so a width above the table size // would read past it. Today the only caller passes width 16, but @@ -1016,6 +1040,98 @@ void drawRectOutline(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int } +// Clipped once (row and column ranges), each visible row's source row +// found exactly and drawn by drawScaleRow. +void drawScaled(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW, int32_t dstH, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, bool keyed, uint32_t key) { + int32_t bpp = d->format.bytesPerPixel; + int32_t col0 = DVX_MAX(0, d->clipX - dstX); + int32_t col1 = DVX_MIN(dstW, d->clipX + d->clipW - dstX); + int32_t row0 = DVX_MAX(0, d->clipY - dstY); + int32_t row1 = DVX_MIN(dstH, d->clipY + d->clipH - dstY); + + if (dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 || col0 >= col1) { + return; + } + + for (int32_t y = row0; y < row1; y++) { + const uint8_t *srcRow = src + (int64_t)((int64_t)y * srcH / dstH) * srcPitch; + uint8_t *dstRow = d->backBuf + (int64_t)(dstY + y) * d->pitch + (int64_t)(dstX + col0) * bpp; + + drawScaleRow(dstRow, srcRow, bpp, col0, col1 - col0, dstW, srcW, keyed, key); + } +} + + +// The source pixel walks (q, and r over dstW) one destination pixel on, +// so no pixel costs a divide; the switch runs once per row. +void drawScaleRow(uint8_t *dst, const uint8_t *src, int32_t bytesPerPx, int32_t first, int32_t count, int32_t dstW, int32_t srcW, bool keyed, uint32_t key) { + uint64_t start = (uint64_t)first * (uint64_t)srcW; + int32_t q = (int32_t)(start / (uint64_t)dstW); + int32_t r = (int32_t)(start % (uint64_t)dstW); + int32_t qStep = srcW / dstW; + int32_t rStep = srcW % dstW; + + switch (bytesPerPx) { + case 1: + for (int32_t i = 0; i < count; i++) { + if (!keyed || src[q] != (uint8_t)key) { + dst[i] = src[q]; + } + + scaleStep(&q, &r, qStep, rStep, dstW); + } + + break; + + case 2: { + const uint16_t *s16 = (const uint16_t *)src; + uint16_t *d16 = (uint16_t *)dst; + + for (int32_t i = 0; i < count; i++) { + if (!keyed || s16[q] != (uint16_t)key) { + d16[i] = s16[q]; + } + + scaleStep(&q, &r, qStep, rStep, dstW); + } + + break; + } + + case 4: { + const uint32_t *s32 = (const uint32_t *)src; + uint32_t *d32 = (uint32_t *)dst; + + for (int32_t i = 0; i < count; i++) { + if (!keyed || s32[q] != key) { + d32[i] = s32[q]; + } + + scaleStep(&q, &r, qStep, rStep, dstW); + } + + break; + } + + default: + for (int32_t i = 0; i < count; i++) { + const uint8_t *px = src + (int64_t)q * bytesPerPx; + uint32_t v = 0; + + memcpy(&v, px, (size_t)bytesPerPx); + + if (!keyed || v != key) { + memcpy(dst + (int64_t)i * bytesPerPx, px, (size_t)bytesPerPx); + } + + scaleStep(&q, &r, qStep, rStep, dstW); + } + + break; + } +} + + // Axis-agnostic scrollbar painter shared by the window manager and the // widget toolkit. Lays down a sunken trough bevel, an arrow button at // each end, and a raised thumb at (barW + thumbPos) along the track. @@ -1025,7 +1141,6 @@ void drawRectOutline(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int // ends, and finally the thumb bevel sits in the middle. A thumbSize of // 0 (or less) suppresses the thumb, used when the range is too small or // the track is too short to display one. - void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, ScrollbarOrientE orient, int32_t x, int32_t y, int32_t length, int32_t barW, int32_t thumbPos, int32_t thumbSize) { BevelStyleT troughBevel = BEVEL_TROUGH(colors); BevelStyleT btnBevel = BEVEL_SB_BUTTON(colors); @@ -1127,8 +1242,12 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3 // Vertical clip for glyph scanlines int32_t rowStart = 0; int32_t rowEnd = ch; - if (y < clipY1) { rowStart = clipY1 - y; } - if (y + ch > clipY2) { rowEnd = clipY2 - y; } + if (y < clipY1) { + rowStart = clipY1 - y; + } + if (y + ch > clipY2) { + rowEnd = clipY2 - y; + } // Horizontal clip: find first and last visible column int32_t rowW = cols * cw; @@ -1145,7 +1264,9 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3 if (x + rowW > clipX2) { lastCol = (clipX2 - x + cw - 1) / cw; - if (lastCol > cols) { lastCol = cols; } + if (lastCol > cols) { + lastCol = cols; + } } // Per-column clip for partially visible edge cells @@ -1388,9 +1509,18 @@ static void drawTextLines(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *f // Horizontal clipping is done at the character level (firstChar/ // lastChar) to avoid iterating invisible characters, with per-pixel // edge clipping only for the partially visible first and last chars. +// A row wholly above or below the clip rect (its underline and strike +// lines included) is passed over before anything is measured: a long +// document draws only what shows. void drawTextN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t count, uint32_t fg, uint32_t bg, bool opaque) { - if (count <= 0) { + int32_t bottom = y + font->charHeight; + + if (font->style & FONT_STYLE_LINES) { + bottom = DVX_MAX(bottom, y + DVX_MAX(font->underlineY, font->strikeY) + font->lineThick); + } + + if (count <= 0 || bottom <= d->clipY || y >= d->clipY + d->clipH) { return; } @@ -1432,8 +1562,12 @@ static void drawTextNFixed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT * // Vertical clip for glyph scanlines int32_t rowStart = 0; int32_t rowEnd = ch; - if (y < clipY1) { rowStart = clipY1 - y; } - if (y + ch > clipY2) { rowEnd = clipY2 - y; } + if (y < clipY1) { + rowStart = clipY1 - y; + } + if (y + ch > clipY2) { + rowEnd = clipY2 - y; + } // Horizontal clip: find first and last visible column (character index) int32_t firstChar = 0; @@ -1445,7 +1579,9 @@ static void drawTextNFixed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT * if (x + totalW > clipX2) { lastChar = (clipX2 - x + cw - 1) / cw; - if (lastChar > count) { lastChar = count; } + if (lastChar > count) { + lastChar = count; + } } // Per-pixel clip for partially visible edge characters @@ -1461,8 +1597,12 @@ static void drawTextNFixed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT * int32_t fillX1 = x + firstChar * cw; int32_t fillX2 = x + lastChar * cw; - if (fillX1 < clipX1) { fillX1 = clipX1; } - if (fillX2 > clipX2) { fillX2 = clipX2; } + if (fillX1 < clipX1) { + fillX1 = clipX1; + } + if (fillX2 > clipX2) { + fillX2 = clipX2; + } int32_t fillW = fillX2 - fillX1; @@ -1756,9 +1896,15 @@ void drawVLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t h int32_t y1 = y; int32_t y2 = y + h; - if (y1 < d->clipY) { y1 = d->clipY; } - if (y2 > d->clipY + d->clipH) { y2 = d->clipY + d->clipH; } - if (y1 >= y2) { return; } + if (y1 < d->clipY) { + y1 = d->clipY; + } + if (y2 > d->clipY + d->clipH) { + y2 = d->clipY + d->clipH; + } + if (y1 >= y2) { + return; + } int32_t bpp = ops->bytesPerPixel; uint8_t *dst = d->backBuf + y1 * d->pitch + x * bpp; @@ -1810,7 +1956,7 @@ static void focusDot(void *ctx, int32_t x, int32_t y) { const DisplayT *d = dc->d; if (x >= d->clipX && x < d->clipX + d->clipW && y >= d->clipY && y < d->clipY + d->clipH) { - putPixel(d->backBuf + y * d->pitch + x * dc->bpp, dc->color, dc->bpp); + pixelPut(d->backBuf + y * d->pitch + x * dc->bpp, dc->color, dc->bpp); } } @@ -1995,7 +2141,7 @@ static inline void nibbleRow8(uint8_t *dst, const NibbleTableT *t, uint8_t bits) // t's patterns for fg on bg at bpp bytes a pixel (kept when they already -// are): each nibble's pixels written as putPixel writes them +// are): each nibble's pixels written as pixelPut writes them static void nibbleTableSet(NibbleTableT *t, uint32_t fg, uint32_t bg, int32_t bpp) { if (t->valid && t->fg == fg && t->bg == bg && t->bpp == bpp) { return; @@ -2005,7 +2151,7 @@ static void nibbleTableSet(NibbleTableT *t, uint32_t fg, uint32_t bg, int32_t bp uint8_t px[NIBBLE_WORDS_MAX * sizeof(uint32_t)]; for (int32_t p = 0; p < NIBBLE_BITS; p++) { - putPixel(px + p * bpp, ((n << p) & NIBBLE_LEFT_BIT) ? fg : bg, bpp); + pixelPut(px + p * bpp, ((n << p) & NIBBLE_LEFT_BIT) ? fg : bg, bpp); } memcpy(t->words[n], px, (size_t)(NIBBLE_BITS * bpp)); @@ -2018,27 +2164,6 @@ static void nibbleTableSet(NibbleTableT *t, uint32_t fg, uint32_t bg, int32_t bp } -// ============================================================ -// putPixel -// ============================================================ -// -// Writes a single pixel at an already-computed buffer address, for -// drawFocusRect's alternating dot pattern and nibbleTableSet. -// Marked static inline so it compiles to a direct store at the -// call site with no function call overhead. The bpp chain here -// is acceptable because focus rect drawing is infrequent. - -static inline void putPixel(uint8_t *dst, uint32_t color, int32_t bpp) { - if (bpp == 2) { - *(uint16_t *)dst = (uint16_t)color; - } else if (bpp == 4) { - *(uint32_t *)dst = color; - } else { - *dst = (uint8_t)color; - } -} - - // ============================================================ // rectCopy // ============================================================ @@ -2305,6 +2430,18 @@ void rectFill(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, // The colors of a terminal cell of attribute attr: palette foreground // and background, the foreground hidden in a blink's off phase, the two // swapped under the cursor +// drawScaleRow's step: the next destination pixel's source pixel. +static inline void scaleStep(int32_t *q, int32_t *r, int32_t qStep, int32_t rStep, int32_t dstW) { + *q += qStep; + *r += rStep; + + if (*r >= dstW) { + *r -= dstW; + (*q)++; + } +} + + static void termCellColors(uint8_t attr, const uint32_t *palette, bool blinkVisible, bool cursor, uint32_t *fg, uint32_t *bg) { *fg = palette[attr & TERM_ATTR_FG_MASK]; *bg = palette[(attr >> TERM_ATTR_BG_SHIFT) & TERM_ATTR_BG_MASK]; diff --git a/src/libs/kpunch/libdvx/dvxDraw.h b/src/libs/kpunch/libdvx/dvxDraw.h index 472e067c..f4db9111 100644 --- a/src/libs/kpunch/libdvx/dvxDraw.h +++ b/src/libs/kpunch/libdvx/dvxDraw.h @@ -198,10 +198,51 @@ void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *co // drawScrollbarArrow glyph pointing dir centred on it. void drawScrollbarArrowButton(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, ScrollArrowDirE dir); +// Nearest-neighbour scaling of a srcW x srcH picture in d's pixel format +// (srcPitch bytes a row) to dstW x dstH at (dstX, dstY), clipped; with +// keyed, source pixels equal to key are not drawn. Pixel (x, y) is the +// source pixel (x * srcW / dstW, y * srcH / dstH), exactly. +void drawScaled(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW, int32_t dstH, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, bool keyed, uint32_t key); + +// Nearest-neighbour scaling of one row of pixels (bytesPerPx 1 to 4): dst +// gets count pixels, pixel i the source pixel (first + i) * srcW / dstW, +// exactly, stepped without a divide per pixel. With keyed, a source pixel +// equal to key is not copied (dst keeps its own). +void drawScaleRow(uint8_t *dst, const uint8_t *src, int32_t bytesPerPx, int32_t first, int32_t count, int32_t dstW, int32_t srcW, bool keyed, uint32_t key); + // Horizontal line (1px tall rectangle fill, but with simpler clipping). void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, uint32_t color); // Vertical line (1px wide, drawn pixel-by-pixel down each scanline). void drawVLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t h, uint32_t color); +// One packed pixel of bpp bytes (1, 2 or 4: a display's format) written +// at dst, or read from src. +static inline uint32_t pixelGet(const uint8_t *src, int32_t bpp); +static inline void pixelPut(uint8_t *dst, uint32_t color, int32_t bpp); + + +static inline uint32_t pixelGet(const uint8_t *src, int32_t bpp) { + if (bpp == sizeof(uint16_t)) { + return *(const uint16_t *)src; + } + + if (bpp == sizeof(uint32_t)) { + return *(const uint32_t *)src; + } + + return *src; +} + + +static inline void pixelPut(uint8_t *dst, uint32_t color, int32_t bpp) { + if (bpp == sizeof(uint16_t)) { + *(uint16_t *)dst = (uint16_t)color; + } else if (bpp == sizeof(uint32_t)) { + *(uint32_t *)dst = color; + } else { + *dst = (uint8_t)color; + } +} + #endif // DVX_DRAW_H diff --git a/src/libs/kpunch/libdvx/dvxDriver.c b/src/libs/kpunch/libdvx/dvxDriver.c index 4ebebdc0..e2b77cde 100644 --- a/src/libs/kpunch/libdvx/dvxDriver.c +++ b/src/libs/kpunch/libdvx/dvxDriver.c @@ -84,7 +84,7 @@ DvxDriverModT *dvxDriversLoad(const char *dir, const DvxDriverClassT *cls) { } snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", dir, names[i]); - mod.handle = dlopen(path, RTLD_NOW); + mod.handle = platformModuleOpen(path, RTLD_NOW); if (!mod.handle) { dvxLog("Drivers: %s did not load: %s", path, dlerror()); diff --git a/src/libs/kpunch/libdvx/dvxEndian.h b/src/libs/kpunch/libdvx/dvxEndian.h new file mode 100644 index 00000000..768b76f4 --- /dev/null +++ b/src/libs/kpunch/libdvx/dvxEndian.h @@ -0,0 +1,83 @@ +// 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. + +// dvxEndian.h -- Byte order: little- and big-endian loads and stores +// +// File formats and wire protocols fix their byte order; these read and +// write it a byte at a time, so they work on any host and at any +// alignment. Header only, so the core, the libraries, the tools, and the +// Linux proxy use the same code without linking anything. + +#ifndef DVX_ENDIAN_H +#define DVX_ENDIAN_H + +#include + +#define DVX_BYTE_BITS 8 +#define DVX_BYTE_MASK 0xFFu + + +static inline uint16_t dvxGetBe16(const uint8_t *p) { + return (uint16_t)((p[0] << DVX_BYTE_BITS) | p[1]); +} + + +static inline uint32_t dvxGetBe32(const uint8_t *p) { + return ((uint32_t)dvxGetBe16(p) << (2 * DVX_BYTE_BITS)) | dvxGetBe16(p + 2); +} + + +static inline uint16_t dvxGetLe16(const uint8_t *p) { + return (uint16_t)(p[0] | (p[1] << DVX_BYTE_BITS)); +} + + +static inline uint32_t dvxGetLe24(const uint8_t *p) { + return (uint32_t)dvxGetLe16(p) | ((uint32_t)p[2] << (2 * DVX_BYTE_BITS)); +} + + +static inline uint32_t dvxGetLe32(const uint8_t *p) { + return dvxGetLe24(p) | ((uint32_t)p[3] << (3 * DVX_BYTE_BITS)); +} + + +static inline void dvxPutBe32(uint8_t *p, uint32_t v) { + p[0] = (uint8_t)(v >> (3 * DVX_BYTE_BITS)); + p[1] = (uint8_t)(v >> (2 * DVX_BYTE_BITS)); + p[2] = (uint8_t)(v >> DVX_BYTE_BITS); + p[3] = (uint8_t)(v & DVX_BYTE_MASK); +} + + +static inline void dvxPutLe16(uint8_t *p, uint32_t v) { + p[0] = (uint8_t)(v & DVX_BYTE_MASK); + p[1] = (uint8_t)((v >> DVX_BYTE_BITS) & DVX_BYTE_MASK); +} + + +static inline void dvxPutLe32(uint8_t *p, uint32_t v) { + dvxPutLe16(p, v); + dvxPutLe16(p + 2, v >> (2 * DVX_BYTE_BITS)); +} + +#endif // DVX_ENDIAN_H diff --git a/src/libs/kpunch/libdvx/dvxFontFmt.c b/src/libs/kpunch/libdvx/dvxFontFmt.c index a2de4525..ed891b2c 100644 --- a/src/libs/kpunch/libdvx/dvxFontFmt.c +++ b/src/libs/kpunch/libdvx/dvxFontFmt.c @@ -32,6 +32,9 @@ #include #include +// Below this, DVX text and Unicode agree +#define DFN_ASCII_LIMIT 0x80 + // A source being read: its bytes (a font in memory), else its file and // where the font starts in it; the font's length typedef struct { @@ -71,6 +74,8 @@ bool dfnLoadStrike(const DfnSourceT *src, const DfnFileT *f, int32_t inde char *dfnLoadText(const DfnSourceT *src, const DfnFileT *f, DfnTextE which, char *err, int32_t errSize); bool dfnOpen(const DfnSourceT *src, DfnFileT *out, char *err, int32_t errSize); void dfnSourceFile(DfnSourceT *src, const char *path); +int32_t dfnTextFromUnicode(uint32_t code); +uint32_t dfnTextToUnicode(uint8_t ch); static bool fail(char *err, int32_t errSize, const DfnSourceT *src, const char *why); static bool inFont(uint32_t length, uint32_t offset, uint32_t bytes); static bool readAt(const DfnReaderT *r, uint32_t offset, void *buf, uint32_t len); @@ -230,6 +235,16 @@ void dfnSourceFile(DfnSourceT *src, const char *path) { } +int32_t dfnTextFromUnicode(uint32_t code) { + return (code < DFN_ASCII_LIMIT) ? (int32_t)code : dfnFromUnicode(code); +} + + +uint32_t dfnTextToUnicode(uint8_t ch) { + return (ch < DFN_ASCII_LIMIT) ? ch : dfnCp437[ch]; +} + + // The reason, with the font named. Always false. static bool fail(char *err, int32_t errSize, const DfnSourceT *src, const char *why) { if (err && errSize > 0) { diff --git a/src/libs/kpunch/libdvx/dvxFontFmt.h b/src/libs/kpunch/libdvx/dvxFontFmt.h index 48b2c585..6c25f6b6 100644 --- a/src/libs/kpunch/libdvx/dvxFontFmt.h +++ b/src/libs/kpunch/libdvx/dvxFontFmt.h @@ -154,6 +154,13 @@ typedef struct { extern const uint16_t dfnCp437[DFN_CHAR_COUNT]; int32_t dfnFromUnicode(uint32_t code); +// DVX text as Unicode: ASCII (line ends, tabs, and the other controls +// included) is itself, and the rest is its code page 437 character's +// code point. dfnTextFromUnicode is -1 for a code point DVX has no +// character for. +int32_t dfnTextFromUnicode(uint32_t code); +uint32_t dfnTextToUnicode(uint8_t ch); + // Free a strike dfnLoadStrike filled (safe on a zeroed one). void dfnFreeStrike(DfnStrikeDataT *s); diff --git a/src/libs/kpunch/libdvx/dvxHash.h b/src/libs/kpunch/libdvx/dvxHash.h new file mode 100644 index 00000000..33e6b4db --- /dev/null +++ b/src/libs/kpunch/libdvx/dvxHash.h @@ -0,0 +1,103 @@ +// 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. + +// dvxHash.h -- Small hashes, checksums, and hex digits shared across DVX +// +// FNV-1a (32 bits) for hash tables and change detection, CRC-16-CCITT +// for the serial packet layer and file transfers, and hex digits both +// ways. Header only, so the core, the libraries, the tools, and the +// Linux proxy use the same code without linking anything. + +#ifndef DVX_HASH_H +#define DVX_HASH_H + +#include + +#define DVX_FNV_BASIS 2166136261u // FNV-1a start value +#define DVX_FNV_PRIME 16777619u + +#define DVX_CRC16_WIDTH 16 // register bits +#define DVX_CRC16_BYTE_BITS 8 // the register takes a byte at a time +#define DVX_CRC16_TAP_HI 12 // the x^12 and x^5 terms of 0x1021 +#define DVX_CRC16_TAP_LO 5 +#define DVX_CRC16_LOW_BYTE 0xFFu + +// DVX_HEX_DIGITS[n] is nibble n's digit; dvxHexValue reads one back +#define DVX_HEX_DIGITS "0123456789abcdef" +#define DVX_HEX_RADIX 16 +#define DVX_HEX_LETTER_VAL 10 // the value of 'a' and 'A' +#define DVX_NIBBLE_BITS 4 +#define DVX_NIBBLE_MASK 0x0F + + +// One CRC-16-CCITT step per byte (polynomial 0x1021, MSB first) without +// a table: the shifts are the polynomial's terms. Carry crc on from the +// previous call; seed 0 for XMODEM/ZMODEM, 0xFFFF for CCITT-FALSE. +static inline uint16_t dvxCrc16(uint16_t crc, const uint8_t *data, int32_t len) { + for (int32_t i = 0; i < len; i++) { + crc = (uint16_t)((crc >> DVX_CRC16_BYTE_BITS) | (crc << DVX_CRC16_BYTE_BITS)); + crc ^= data[i]; + crc ^= (uint16_t)((crc & DVX_CRC16_LOW_BYTE) >> (DVX_CRC16_WIDTH - DVX_CRC16_TAP_HI)); + crc ^= (uint16_t)(crc << DVX_CRC16_TAP_HI); + crc ^= (uint16_t)((crc & DVX_CRC16_LOW_BYTE) << DVX_CRC16_TAP_LO); + } + + return crc; +} + + +// One FNV-1a step: fold a byte into hash. Start from DVX_FNV_BASIS. +static inline uint32_t dvxFnvByte(uint32_t hash, uint8_t byte) { + return (hash ^ byte) * DVX_FNV_PRIME; +} + + +// FNV-1a over len bytes, carried on from hash. +static inline uint32_t dvxFnvBytes(uint32_t hash, const void *data, int32_t len) { + const uint8_t *p = (const uint8_t *)data; + + for (int32_t i = 0; i < len; i++) { + hash = dvxFnvByte(hash, p[i]); + } + + return hash; +} + + +// A hex digit's value, either case; -1 for any other character. +static inline int32_t dvxHexValue(int32_t c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + + if (c >= 'a' && c <= 'f') { + return c - 'a' + DVX_HEX_LETTER_VAL; + } + + if (c >= 'A' && c <= 'F') { + return c - 'A' + DVX_HEX_LETTER_VAL; + } + + return -1; +} + +#endif // DVX_HASH_H diff --git a/src/libs/kpunch/libdvx/dvxImage.c b/src/libs/kpunch/libdvx/dvxImage.c index 70d93a4a..dbf71edd 100644 --- a/src/libs/kpunch/libdvx/dvxImage.c +++ b/src/libs/kpunch/libdvx/dvxImage.c @@ -19,16 +19,15 @@ // 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. - -// dvxImage.c -- stb_image implementation for DVX GUI +// dvxImage.c -- libdvx's image codec: stb_image and stb_image_write // -// This file exists solely to instantiate the stb_image implementation. -// stb_image is a single-header library: you #define STB_IMAGE_IMPLEMENTATION -// in exactly one .c file to generate the actual function bodies. Putting -// this in its own translation unit keeps the stb code isolated so it: -// 1. Compiles once, not in every file that #includes stb_image.h -// 2. Can have its own warning suppressions without affecting project code -// 3. Doesn't slow down incremental rebuilds (only recompiles if stb changes) +// Both single-header libraries are compiled here, static (STB_IMAGE_STATIC, +// STB_IMAGE_WRITE_STATIC), behind the few calls of dvxImgInt.h: a module +// exports every global, so exported stb functions would all be kept, +// while static ones DVX never calls are dropped (about 21 KB: the other +// loaders and writers, the float paths). Kept in a translation unit of +// its own so the stb code compiles once and its warning suppressions +// touch nothing else. // // stb_image was chosen over libpng/libjpeg because: // - Single header, no external dependencies -- critical for DJGPP cross-compile @@ -37,8 +36,18 @@ // - Small code footprint suitable for DOS targets // // STBI_ONLY_* defines strip out support for unused formats (TGA, PSD, HDR, -// PIC, PNM) to reduce binary size. STBI_NO_SIMD is required because DJGPP -// targets 486/Pentium which lack SSE; on the Linux/SDL build it's harmless. +// PIC, PNM) and STBI_NO_LINEAR the float conversions. STBI_NO_SIMD is +// required because DJGPP targets 486/Pentium which lack SSE; on the host +// build it's harmless. + +#include "dvxImgInt.h" + +// PNG saving: stb's defaults (level 8, all five row filters tried on +// every row) take 3-5 times as long for files a few percent smaller. +// Level 5 is the least stb takes; Paeth is the one filter used. +#define PNG_SAVE_LEVEL 5 +#define PNG_SAVE_FILTER_PAETH 4 +#define RGB_CHANNELS 3 #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-function" @@ -48,7 +57,49 @@ #define STBI_ONLY_JPEG #define STBI_ONLY_GIF #define STBI_NO_SIMD +#define STBI_NO_LINEAR +#define STB_IMAGE_STATIC #define STB_IMAGE_IMPLEMENTATION #include "thirdparty/stb_image_wrap.h" +#define STB_IMAGE_WRITE_STATIC +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "thirdparty/stb_image_write_wrap.h" + #pragma GCC diagnostic pop + + +uint8_t *dvxImgDecode(const DvxImgIoT *io, void *user, int *w, int *h, int channels) { + stbi_io_callbacks cb; + int had; + + cb.read = io->read; + cb.skip = io->skip; + cb.eof = io->eof; + return stbi_load_from_callbacks(&cb, user, w, h, &had, channels); +} + + +bool dvxImgInfoFile(const char *path, int *w, int *h) { + int comp; + + return stbi_info(path, w, h, &comp) != 0; +} + + +bool dvxImgInfoMemory(const uint8_t *data, int32_t len, int *w, int *h) { + int comp; + + return stbi_info_from_memory(data, len, w, h, &comp) != 0; +} + + +bool dvxImgWrite(const char *path, bool bmp, int32_t w, int32_t h, const uint8_t *rgb) { + if (bmp) { + return stbi_write_bmp(path, w, h, RGB_CHANNELS, rgb) != 0; + } + + stbi_write_png_compression_level = PNG_SAVE_LEVEL; + stbi_write_force_png_filter = PNG_SAVE_FILTER_PAETH; + return stbi_write_png(path, w, h, RGB_CHANNELS, rgb, w * RGB_CHANNELS) != 0; +} diff --git a/src/libs/kpunch/libdvx/dvxImgInt.h b/src/libs/kpunch/libdvx/dvxImgInt.h new file mode 100644 index 00000000..323c15d5 --- /dev/null +++ b/src/libs/kpunch/libdvx/dvxImgInt.h @@ -0,0 +1,60 @@ +// 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. + +// dvxImgInt.h -- libdvx's image codec, private to libdvx +// +// stb_image and stb_image_write are compiled static in dvxImage.c, so +// only what DVX calls is kept (about 21 KB less than exporting all of +// both). These are its calls; dvxApp.c's image functions and the host +// platform's screen capture go through them. + +#ifndef DVX_IMG_INT_H +#define DVX_IMG_INT_H + +#include +#include + +// The reads a decode makes (stb_image's callbacks): size bytes into out +// (how many there were), n bytes on or back, and whether the end is +// reached +typedef struct { + int (*read)(void *user, char *out, int size); + void (*skip)(void *user, int n); + int (*eof)(void *user); +} DvxImgIoT; + +// Decode an image read through io, channels per pixel (malloc'd, the +// caller frees); NULL when it is not a BMP, PNG, JPEG, or GIF image. *w +// and *h are its size. +uint8_t *dvxImgDecode(const DvxImgIoT *io, void *user, int *w, int *h, int channels); + +// An image's size from its header, the file at path or len bytes at +// data; false when it is no image. *h may come back negative (a +// top-down BMP). +bool dvxImgInfoFile(const char *path, int *w, int *h); +bool dvxImgInfoMemory(const uint8_t *data, int32_t len, int *w, int *h); + +// Write w x h 24-bit RGB pixels to path as a BMP (bmp) or a PNG +// (compression and filter DVX's own); false when it was not written. +bool dvxImgWrite(const char *path, bool bmp, int32_t w, int32_t h, const uint8_t *rgb); + +#endif // DVX_IMG_INT_H diff --git a/src/libs/kpunch/libdvx/dvxMem.h b/src/libs/kpunch/libdvx/dvxMem.h index b4aa436b..8314f74f 100644 --- a/src/libs/kpunch/libdvx/dvxMem.h +++ b/src/libs/kpunch/libdvx/dvxMem.h @@ -36,6 +36,9 @@ #include #include +// The program id of DVX itself (the shell and the libraries' own memory) +#define DVX_MEM_SYSTEM 0 + extern int32_t *dvxMemAppIdPtr; void *dvxCalloc(size_t nmemb, size_t size); @@ -43,6 +46,14 @@ void dvxFree(void *ptr); void *dvxMalloc(size_t size); uint32_t dvxMemGetAppUsage(int32_t appId); void dvxMemResetApp(int32_t appId); + +// Make appId the current program (the one what is allocated from now is +// charged to, as the shell sees it too) and return the one before, to be +// set back: a library keeps its own memory as DVX_MEM_SYSTEM's, or calls +// a program's callback as that program. Without a shell (host tools) +// nothing changes and 0 comes back. +int32_t dvxMemSetApp(int32_t appId); + void *dvxRealloc(void *ptr, size_t size); char *dvxStrdup(const char *s); diff --git a/src/libs/kpunch/libdvx/dvxPrefs.c b/src/libs/kpunch/libdvx/dvxPrefs.c index 60609d3a..f4062088 100644 --- a/src/libs/kpunch/libdvx/dvxPrefs.c +++ b/src/libs/kpunch/libdvx/dvxPrefs.c @@ -30,8 +30,10 @@ #include "dvxPrefs.h" #include "dvxTypes.h" +#include "dvxHash.h" #include "platform/dvxPlat.h" +#include #include #include #include @@ -70,9 +72,21 @@ typedef struct { struct PrefsHandleT { PrefsEntryT *entries; // stb_ds dynamic array char *filePath; // path used by prefsSave + int32_t *index; // the lookup index (indexFind): entry + 1 per slot, 0 empty; NULL: not built + int32_t indexCap; // its slots (a power of two) + int32_t indexUsed; bool loadFailed; // file exists but could not be read: prefsSave refuses }; +// The lookup index (indexFind) holds the first entry of each key (by its +// section and name) and of each section header (by its name), hashed with +// the names folded to lower case. Built on the first lookup, kept up as +// entries are appended, dropped when one is inserted before the end or +// removed (built again on the next lookup). +#define PREFS_INDEX_MIN 64 // slots at least +#define PREFS_INDEX_ROOM 4 // slots per entry when built +#define PREFS_INDEX_LOAD 2 // built again once more than 1 / 2 full + // A settings file saved while DVX's folder cannot be written to // (platformIsReadOnly): kept here until DVX exits, and read back by // prefsLoad in place of the file on disk. @@ -117,6 +131,12 @@ static PrefsMemFileT *sMemFiles = NULL; // prefsSetSecret calls so far: no two in one run share a salt static uint32_t sSecretCalls = 0; +// The shared DVX.INI (prefsDvxIniShared) and the text it was read from +// (NULL: there was none) +static PrefsHandleT *sDvxIni = NULL; +static char *sDvxIniText = NULL; +static int32_t sDvxIniLen = 0; + // ============================================================ // Shared preference vocabularies @@ -147,10 +167,17 @@ const PrefsChoiceT prefsWheelDirChoices[PREFS_WHEEL_DIR_CHOICE_COUNT] = { static void addEntry(PrefsHandleT *h, PrefsLineE kind, const char *section, const char *key, const char *value, const char *raw); static void appendText(char **buf, const char *a, const char *b, const char *c); +static uint32_t entryHash(PrefsLineE kind, const char *section, const char *key); +static bool entryIs(const PrefsEntryT *e, PrefsLineE kind, const char *section, const char *key); static int32_t findEntry(PrefsHandleT *h, const char *section, const char *key); static int32_t findSection(PrefsHandleT *h, const char *section); static char *formatText(const PrefsHandleT *h); static void freeEntry(PrefsEntryT *e); +static void indexAppended(PrefsHandleT *h); +static void indexDrop(PrefsHandleT *h); +static int32_t indexFind(PrefsHandleT *h, PrefsLineE kind, const char *section, const char *key); +static void indexPut(PrefsHandleT *h, int32_t entry); +static bool indexReady(PrefsHandleT *h); static bool matchWord(const char *val, const char *const *words, int32_t count); static char *memFileCopy(const char *path, int32_t *len); static bool memFileStore(const char *path, const char *text, int32_t len); @@ -179,6 +206,7 @@ static void addEntry(PrefsHandleT *h, PrefsLineE kind, const char *section, cons } arrput(h->entries, e); + indexAppended(h); } @@ -194,29 +222,39 @@ static void appendText(char **buf, const char *a, const char *b, const char *c) } -static int32_t findEntry(PrefsHandleT *h, const char *section, const char *key) { - for (int32_t i = 0; i < arrlen(h->entries); i++) { - PrefsEntryT *e = &h->entries[i]; +// The hash the lookup index files an entry under: a key's section and +// name, a section header's name, folded to lower case. +static uint32_t entryHash(PrefsLineE kind, const char *section, const char *key) { + uint32_t hash = dvxFnvByte(DVX_FNV_BASIS, (uint8_t)kind); - if (e->kind == PrefsLineKeyE && strcasecmp(e->section, section) == 0 && strcasecmp(e->key, key) == 0) { - return i; - } + for (const char *p = section; *p; p++) { + hash = dvxFnvByte(hash, (uint8_t)tolower((uint8_t)*p)); } - return -1; + for (const char *p = (kind == PrefsLineKeyE) ? key : ""; *p; p++) { + hash = dvxFnvByte(hash, (uint8_t)tolower((uint8_t)*p)); + } + + return hash; } +// e is the key (kind PrefsLineKeyE) section / key, or the header of +// section (PrefsLineSectionE), names compared in any case. +static bool entryIs(const PrefsEntryT *e, PrefsLineE kind, const char *section, const char *key) { + return e->kind == kind && strcasecmp(e->section, section) == 0 && (kind != PrefsLineKeyE || strcasecmp(e->key, key) == 0); +} + + +// The first entry of key in section, or -1. +static int32_t findEntry(PrefsHandleT *h, const char *section, const char *key) { + return indexFind(h, PrefsLineKeyE, section, key); +} + + +// The first header of section, or -1. static int32_t findSection(PrefsHandleT *h, const char *section) { - for (int32_t i = 0; i < arrlen(h->entries); i++) { - PrefsEntryT *e = &h->entries[i]; - - if (e->kind == PrefsLineSectionE && strcasecmp(e->section, section) == 0) { - return i; - } - } - - return -1; + return indexFind(h, PrefsLineSectionE, section, NULL); } @@ -254,6 +292,112 @@ static void freeEntry(PrefsEntryT *e) { } +// The last entry was appended: into a built index while it has room +// (else the index goes, built again bigger on the next lookup). +static void indexAppended(PrefsHandleT *h) { + if (!h->index) { + return; + } + + if ((h->indexUsed + 1) * PREFS_INDEX_LOAD > h->indexCap) { + indexDrop(h); + return; + } + + indexPut(h, (int32_t)arrlen(h->entries) - 1); +} + + +// The entries moved (one inserted before the end, or removed): the index +// is built again on the next lookup. +static void indexDrop(PrefsHandleT *h) { + free(h->index); + h->index = NULL; + h->indexCap = 0; + h->indexUsed = 0; +} + + +// The first entry of a key (kind PrefsLineKeyE) or a section header +// (PrefsLineSectionE), or -1. Through the lookup index; through every +// entry when there is no memory for one. +static int32_t indexFind(PrefsHandleT *h, PrefsLineE kind, const char *section, const char *key) { + if (!indexReady(h)) { + for (int32_t i = 0; i < arrlen(h->entries); i++) { + if (entryIs(&h->entries[i], kind, section, key)) { + return i; + } + } + + return -1; + } + + uint32_t mask = (uint32_t)h->indexCap - 1; + + for (uint32_t slot = entryHash(kind, section, key) & mask; h->index[slot] != 0; slot = (slot + 1) & mask) { + if (entryIs(&h->entries[h->index[slot] - 1], kind, section, key)) { + return h->index[slot] - 1; + } + } + + return -1; +} + + +// File entry in the index (which has room) unless it is not a key or a +// header, or an earlier entry has its names (the one lookups return). +static void indexPut(PrefsHandleT *h, int32_t entry) { + const PrefsEntryT *e = &h->entries[entry]; + + if (e->kind == PrefsLineOtherE) { + return; + } + + uint32_t mask = (uint32_t)h->indexCap - 1; + uint32_t slot = entryHash(e->kind, e->section, e->key) & mask; + + for (; h->index[slot] != 0; slot = (slot + 1) & mask) { + if (entryIs(&h->entries[h->index[slot] - 1], e->kind, e->section, e->key)) { + return; + } + } + + h->index[slot] = entry + 1; + h->indexUsed++; +} + + +// The lookup index, built when missing; false when there is no memory +// for it. +static bool indexReady(PrefsHandleT *h) { + if (h->index) { + return true; + } + + int32_t count = (int32_t)arrlen(h->entries); + int32_t cap = PREFS_INDEX_MIN; + + while (cap < count * PREFS_INDEX_ROOM) { + cap *= 2; + } + + h->index = (int32_t *)calloc((size_t)cap, sizeof(int32_t)); + + if (!h->index) { + return false; + } + + h->indexCap = cap; + h->indexUsed = 0; + + for (int32_t i = 0; i < count; i++) { + indexPut(h, i); + } + + return true; +} + + // True when val equals one of words, ignoring case. static bool matchWord(const char *val, const char *const *words, int32_t count) { for (int32_t i = 0; i < count; i++) { @@ -401,6 +545,7 @@ void prefsClose(PrefsHandleT *h) { } arrfree(h->entries); + indexDrop(h); free(h->filePath); free(h); } @@ -412,6 +557,33 @@ PrefsHandleT *prefsCreate(void) { } +// The file's text (as prefsLoad reads it: one held in memory first, see +// prefsSaveAs) is compared with the text the handle was read from: a +// file's time cannot tell (FAT keeps it to 2 s), and reading it costs a +// small part of parsing it. +PrefsHandleT *prefsDvxIniShared(void) { + const char *path = platformDvxIniPath(); + int32_t len = 0; + char *text = memFileCopy(path, &len); + + if (!text) { + text = platformReadFile(path, &len); + } + + if (sDvxIni && (text ? sDvxIniText && len == sDvxIniLen && memcmp(text, sDvxIniText, (size_t)len) == 0 : !sDvxIniText)) { + free(text); + return sDvxIni; + } + + prefsClose(sDvxIni); + free(sDvxIniText); + sDvxIni = prefsLoad(path); + sDvxIniText = text; + sDvxIniLen = len; + return sDvxIni; +} + + bool prefsGetBool(PrefsHandleT *h, const char *section, const char *key, bool defaultVal) { const char *val = prefsGetString(h, section, key, NULL); @@ -612,6 +784,7 @@ void prefsRemove(PrefsHandleT *h, const char *section, const char *key) { if (idx >= 0) { freeEntry(&h->entries[idx]); arrdel(h->entries, idx); + indexDrop(h); } } @@ -771,7 +944,15 @@ void prefsSetString(PrefsHandleT *h, const char *section, const char *key, const return; } + bool atEnd = insertAt == arrlen(h->entries); + arrins(h->entries, insertAt, newEntry); + + if (atEnd) { + indexAppended(h); + } else { + indexDrop(h); + } } diff --git a/src/libs/kpunch/libdvx/dvxPrefs.h b/src/libs/kpunch/libdvx/dvxPrefs.h index 5ece74fc..e8959cc0 100644 --- a/src/libs/kpunch/libdvx/dvxPrefs.h +++ b/src/libs/kpunch/libdvx/dvxPrefs.h @@ -81,6 +81,13 @@ bool prefsSaveAs(PrefsHandleT *h, const char *filename); // Release all memory held by the handle. void prefsClose(PrefsHandleT *h); +// DVX.INI (platformDvxIniPath) for reading, shared: parsed again only +// when the file's text changed, so code that reads settings often (every +// sound, every song) does not parse the file each time. Its users do not +// close it, change it, or keep it past their own call (the next call may +// read the file again). +PrefsHandleT *prefsDvxIniShared(void); + // Retrieve a string value. Returns defaultVal if the key is not present. // The returned pointer is valid until the key is modified or prefsClose. const char *prefsGetString(PrefsHandleT *h, const char *section, const char *key, const char *defaultVal); diff --git a/src/libs/kpunch/libdvx/dvxRes.h b/src/libs/kpunch/libdvx/dvxRes.h index 8324e003..9ebad32d 100644 --- a/src/libs/kpunch/libdvx/dvxRes.h +++ b/src/libs/kpunch/libdvx/dvxRes.h @@ -99,6 +99,11 @@ DvxResHandleT *dvxResOpen(const char *path); // Caller must free the returned buffer. void *dvxResRead(DvxResHandleT *h, const char *name, uint32_t *outSize); +// Read a text resource into buf (up to bufSize - 1 bytes, NUL added). +// False when there is no such resource (buf is left alone) or it cannot +// be read (buf is then ""). +bool dvxResReadText(DvxResHandleT *h, const char *name, char *buf, int32_t bufSize); + // Find a resource by name and return its directory entry. // Returns NULL if not found. The returned pointer is valid until // dvxResClose is called. diff --git a/src/libs/kpunch/libdvx/dvxImageWrite.c b/src/libs/kpunch/libdvx/dvxResInt.h similarity index 58% rename from src/libs/kpunch/libdvx/dvxImageWrite.c rename to src/libs/kpunch/libdvx/dvxResInt.h index 276e8ef3..14a876ab 100644 --- a/src/libs/kpunch/libdvx/dvxImageWrite.c +++ b/src/libs/kpunch/libdvx/dvxResInt.h @@ -20,23 +20,24 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -// dvxImageWrite.c -- stb_image_write implementation for DVX GUI +// dvxResInt.h -- libdvx's resource reads on a file already open, private +// to libdvx // -// Companion to dvxImage.c: instantiates stb_image_write for PNG output -// (used by dvxScreenshot and dvxWindowScreenshot). Same rationale as -// dvxImage.c for using stb -- zero external dependencies, single header, -// public domain. Kept in a separate translation unit from the read side -// so projects that don't need screenshot support can omit this file and -// save the code size. -// -// STBI_WRITE_NO_SIMD disables SSE codepaths for the same reason as -// STBI_NO_SIMD in dvxImage.c: the DOS target lacks SSE support. +// On DOS opening a file is most of what reading a program's icon costs +// (each open walks the path's folders on disk), so dvxLoadImage tries the +// file as a picture and, when it is none, reads the icon from the same +// open stream. -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-function" +#ifndef DVX_RES_INT_H +#define DVX_RES_INT_H -#define STBI_WRITE_NO_SIMD -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include "thirdparty/stb_image_write_wrap.h" +#include +#include -#pragma GCC diagnostic pop +// The icon of the DVX program, widget, or library file open as f (NULL is +// allowed), as the bytes of its image file: its DVX_RES_NAME_ICON +// resource, else DVX_RES_NAME_WIDGET_ICON. NULL when it has neither. +// Free with free(); *outSize gets the size. Moves f's position. +void *dvxResIconRead(FILE *f, uint32_t *outSize); + +#endif // DVX_RES_INT_H diff --git a/src/libs/kpunch/libdvx/dvxResource.c b/src/libs/kpunch/libdvx/dvxResource.c index 08b1629e..715ede62 100644 --- a/src/libs/kpunch/libdvx/dvxResource.c +++ b/src/libs/kpunch/libdvx/dvxResource.c @@ -26,6 +26,7 @@ // block is located by reading the footer at the end of the file. #include "dvxRes.h" +#include "dvxResInt.h" #include "../../../tools/dvxResWrite.h" #include @@ -37,7 +38,78 @@ // Prototypes // ============================================================ -static int dvxResEntryCmp(const void *a, const void *b); +static DvxResDirEntryT *dirRead(FILE *f, uint32_t *outCount); +int32_t dvxResAppend(const char *path, const char *name, uint32_t type, const void *data, uint32_t dataSize); +void dvxResClose(DvxResHandleT *h); +static int dvxResEntryCmp(const void *a, const void *b); +const DvxResDirEntryT *dvxResFind(DvxResHandleT *h, const char *name); +void *dvxResIconRead(FILE *f, uint32_t *outSize); +DvxResHandleT *dvxResOpen(const char *path); +void *dvxResRead(DvxResHandleT *h, const char *name, uint32_t *outSize); +bool dvxResReadText(DvxResHandleT *h, const char *name, char *buf, int32_t bufSize); +int32_t dvxResRemove(const char *path, const char *name); +static void *entryRead(FILE *f, const DvxResDirEntryT *entry, uint32_t *outSize); + +// The pictures dvxResIconRead looks for, in order +static const char *const sIconNames[] = { DVX_RES_NAME_ICON, DVX_RES_NAME_WIDGET_ICON }; + + +// The directory of the resource block of f (malloc'd, names terminated; +// *outCount entries), or NULL when f has none. +static DvxResDirEntryT *dirRead(FILE *f, uint32_t *outCount) { + DvxResFooterT footer; + + // Measure the file so the directory size read from the (possibly + // corrupt/hostile) footer can be bounds-checked before allocating. + if (fseek(f, 0, SEEK_END) != 0) { + return NULL; + } + + int64_t fileSize = ftell(f); + + // Read footer from end of file + if (fseek(f, -(int32_t)sizeof(DvxResFooterT), SEEK_END) != 0 || fread(&footer, sizeof(footer), 1, f) != 1) { + return NULL; + } + + if (footer.magic != DVX_RES_MAGIC || footer.entryCount == 0) { + return NULL; + } + + // Reject an entryCount that can't fit in the file: this both stops a + // size_t overflow in the malloc below (entryCount * 48 wraps on a + // 32-bit target) and prevents the fread from overrunning a tiny + // allocation when the footer is corrupt. + if (fileSize < 0 || (int64_t)footer.dirOffset > fileSize || (int64_t)footer.entryCount > fileSize / (int64_t)sizeof(DvxResDirEntryT)) { + return NULL; + } + + // Read directory + if (fseek(f, footer.dirOffset, SEEK_SET) != 0) { + return NULL; + } + + DvxResDirEntryT *entries = (DvxResDirEntryT *)malloc(footer.entryCount * sizeof(DvxResDirEntryT)); + + if (!entries) { + return NULL; + } + + if (fread(entries, sizeof(DvxResDirEntryT), footer.entryCount, f) != footer.entryCount) { + free(entries); + return NULL; + } + + // Force-terminate names read from the file before any strcmp (qsort / + // bsearch); a corrupt unterminated name would otherwise read past the + // field, and past the allocation for the last entry. + for (uint32_t i = 0; i < footer.entryCount; i++) { + entries[i].name[DVX_RES_NAME_MAX - 1] = '\0'; + } + + *outCount = footer.entryCount; + return entries; +} int32_t dvxResAppend(const char *path, const char *name, uint32_t type, const void *data, uint32_t dataSize) { @@ -70,88 +142,47 @@ const DvxResDirEntryT *dvxResFind(DvxResHandleT *h, const char *name) { DvxResDirEntryT key; memset(&key, 0, sizeof(key)); memcpy(key.name, name, strlen(name) + 1); - return (const DvxResDirEntryT *)bsearch(&key, h->entries, h->entryCount, - sizeof(DvxResDirEntryT), dvxResEntryCmp); + return (const DvxResDirEntryT *)bsearch(&key, h->entries, h->entryCount, sizeof(DvxResDirEntryT), dvxResEntryCmp); +} + + +// One directory read and one data read on the stream the caller has open +// (dvxResOpen and dvxResRead would open the file twice more). +void *dvxResIconRead(FILE *f, uint32_t *outSize) { + uint32_t count = 0; + DvxResDirEntryT *entries = f ? dirRead(f, &count) : NULL; + void *data = NULL; + + for (int32_t n = 0; entries && !data && n < DVX_ARRAY_LEN(sIconNames); n++) { + for (uint32_t i = 0; i < count; i++) { + if (strcmp(entries[i].name, sIconNames[n]) == 0) { + data = entryRead(f, &entries[i], outSize); + break; + } + } + } + + free(entries); + return data; } DvxResHandleT *dvxResOpen(const char *path) { - if (!path) { - return NULL; - } - - FILE *f = fopen(path, "rb"); + FILE *f = path ? fopen(path, "rb") : NULL; if (!f) { return NULL; } - // Measure the file so the directory size read from the (possibly - // corrupt/hostile) footer can be bounds-checked before allocating. - if (fseek(f, 0, SEEK_END) != 0) { - fclose(f); - return NULL; - } - - int64_t fileSize = ftell(f); - - // Read footer from end of file - if (fseek(f, -(int32_t)sizeof(DvxResFooterT), SEEK_END) != 0) { - fclose(f); - return NULL; - } - - DvxResFooterT footer; - - if (fread(&footer, sizeof(footer), 1, f) != 1) { - fclose(f); - return NULL; - } - - if (footer.magic != DVX_RES_MAGIC || footer.entryCount == 0) { - fclose(f); - return NULL; - } - - // Reject an entryCount that can't fit in the file: this both stops a - // size_t overflow in the malloc below (entryCount * 48 wraps on a - // 32-bit target) and prevents the fread from overrunning a tiny - // allocation when the footer is corrupt. - if (fileSize < 0 || - (int64_t)footer.dirOffset > fileSize || - (int64_t)footer.entryCount > fileSize / (int64_t)sizeof(DvxResDirEntryT)) { - fclose(f); - return NULL; - } - - // Read directory - if (fseek(f, footer.dirOffset, SEEK_SET) != 0) { - fclose(f); - return NULL; - } - - DvxResDirEntryT *entries = (DvxResDirEntryT *)malloc(footer.entryCount * sizeof(DvxResDirEntryT)); - - if (!entries) { - fclose(f); - return NULL; - } - - if (fread(entries, sizeof(DvxResDirEntryT), footer.entryCount, f) != footer.entryCount) { - free(entries); - fclose(f); - return NULL; - } - - // Force-terminate names read from the file before any strcmp (qsort / - // bsearch); a corrupt unterminated name would otherwise read past the - // field, and past the allocation for the last entry. - for (uint32_t i = 0; i < footer.entryCount; i++) { - entries[i].name[DVX_RES_NAME_MAX - 1] = '\0'; - } + uint32_t count = 0; + DvxResDirEntryT *entries = dirRead(f, &count); fclose(f); + if (!entries) { + return NULL; + } + DvxResHandleT *h = (DvxResHandleT *)malloc(sizeof(DvxResHandleT)); if (!h) { @@ -162,7 +193,7 @@ DvxResHandleT *dvxResOpen(const char *path) { strncpy(h->path, path, sizeof(h->path) - 1); h->path[sizeof(h->path) - 1] = '\0'; h->entries = entries; - h->entryCount = footer.entryCount; + h->entryCount = count; // Sort once so dvxResFind can bsearch. qsort(h->entries, h->entryCount, sizeof(DvxResDirEntryT), dvxResEntryCmp); @@ -173,42 +204,35 @@ DvxResHandleT *dvxResOpen(const char *path) { void *dvxResRead(DvxResHandleT *h, const char *name, uint32_t *outSize) { const DvxResDirEntryT *entry = dvxResFind(h, name); - - if (!entry) { - return NULL; - } - - FILE *f = fopen(h->path, "rb"); + FILE *f = entry ? fopen(h->path, "rb") : NULL; if (!f) { return NULL; } - if (fseek(f, entry->offset, SEEK_SET) != 0) { - fclose(f); - return NULL; - } - - void *buf = malloc(entry->size); - - if (!buf) { - fclose(f); - return NULL; - } - - if (fread(buf, 1, entry->size, f) != entry->size) { - free(buf); - fclose(f); - return NULL; - } + void *buf = entryRead(f, entry, outSize); fclose(f); + return buf; +} - if (outSize) { - *outSize = entry->size; + +// Straight into buf: no buffer of the resource's own size. +bool dvxResReadText(DvxResHandleT *h, const char *name, char *buf, int32_t bufSize) { + const DvxResDirEntryT *entry = (h && buf && bufSize > 0) ? dvxResFind(h, name) : NULL; + FILE *f = entry ? fopen(h->path, "rb") : NULL; + + if (!f) { + return false; } - return buf; + size_t room = (size_t)bufSize - 1; + size_t want = entry->size < room ? entry->size : room; + bool ok = fseek(f, entry->offset, SEEK_SET) == 0 && fread(buf, 1, want, f) == want; + + fclose(f); + buf[ok ? want : 0] = '\0'; + return ok; } @@ -258,3 +282,29 @@ int32_t dvxResRemove(const char *path, const char *name) { free(layout.entries); return result; } + + +// The data of entry (malloc'd; *outSize, when given, gets its size), or +// NULL when it cannot be read. +static void *entryRead(FILE *f, const DvxResDirEntryT *entry, uint32_t *outSize) { + if (fseek(f, entry->offset, SEEK_SET) != 0) { + return NULL; + } + + void *buf = malloc(entry->size); + + if (!buf) { + return NULL; + } + + if (fread(buf, 1, entry->size, f) != entry->size) { + free(buf); + return NULL; + } + + if (outSize) { + *outSize = entry->size; + } + + return buf; +} diff --git a/src/libs/kpunch/libdvx/dvxResourceBas.c b/src/libs/kpunch/libdvx/dvxResourceBas.c new file mode 100644 index 00000000..8e917f15 --- /dev/null +++ b/src/libs/kpunch/libdvx/dvxResourceBas.c @@ -0,0 +1,237 @@ +// 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. + +// dvxResourceBas.c -- DVX BASIC entry points of the resource functions +// (src/include/basic/resource.bas) +// +// Called through DECLARE LIBRARY, so the signatures use only BASIC- +// marshalable types; True (-1) / False (0) for yes / no. A returned +// string stays valid until the next call that returns one (the BASIC +// runtime copies it at once). Handles from ResOpen (1, 2, ...; 0 means +// none) belong to the program that opened them, as many as it likes; +// whatever a program leaves open is closed when it ends. + +#include "dvxRes.h" +#include "dvxPlat.h" +#include "dvxBas.h" + +#include +#include +#include +#include + +// ============================================================ +// Prototypes +// ============================================================ + +int32_t ResAddData(const char *path, const char *name, int32_t type, const uint8_t *data, int32_t len); +int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile); +int32_t ResAddText(const char *path, const char *name, const char *text); +void ResClose(int32_t handle); +int32_t ResCount(int32_t handle); +static const DvxResDirEntryT *resEntry(int32_t handle, int32_t index); +int32_t ResExtract(const char *path, const char *name, const char *outFile); +static void resFree(void *obj); +const uint8_t *ResGetData(const char *path, const char *name, int32_t *outLen); +const char *ResGetText(const char *path, const char *name); +const char *ResName(int32_t handle, int32_t index); +int32_t ResOpen(const char *path); +static void *resReadNamed(const char *path, const char *name, uint32_t *outSize); +int32_t ResRemove(const char *path, const char *name); +int32_t ResSize(int32_t handle, int32_t index); +int32_t ResType(int32_t handle, int32_t index); + +static BasHandlesT sFiles = BAS_HANDLES(resFree, 0); // the open resource files +static BasTextT sText; // the last string result +static uint8_t *sData = NULL; // the last ResGetData$ bytes + + +// ResAddData: a resource of any bytes (BINARY STRING); True on success. +int32_t ResAddData(const char *path, const char *name, int32_t type, const uint8_t *data, int32_t len) { + if (!path || !name || !data || len < 0) { + return BAS_FALSE; + } + + return dvxResAppend(path, name, (uint32_t)type, data, (uint32_t)len) == 0 ? BAS_TRUE : BAS_FALSE; +} + + +int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile) { + if (!srcFile) { + return BAS_FALSE; + } + + int32_t fileSize = 0; + uint8_t *buf = (uint8_t *)platformReadFile(srcFile, &fileSize); + int32_t result = (buf && fileSize > 0) ? ResAddData(path, name, type, buf, fileSize) : BAS_FALSE; + + free(buf); + return result; +} + + +int32_t ResAddText(const char *path, const char *name, const char *text) { + if (!text) { + text = ""; + } + + return ResAddData(path, name, DVX_RES_TEXT, (const uint8_t *)text, (int32_t)strlen(text) + 1); +} + + +void ResClose(int32_t handle) { + basHandleFree(&sFiles, handle); +} + + +int32_t ResCount(int32_t handle) { + DvxResHandleT *h = (DvxResHandleT *)basHandleGet(&sFiles, handle); + + return h ? (int32_t)h->entryCount : 0; +} + + +// Entry index of the file open as handle, or NULL. +static const DvxResDirEntryT *resEntry(int32_t handle, int32_t index) { + DvxResHandleT *h = (DvxResHandleT *)basHandleGet(&sFiles, handle); + + return (h && index >= 0 && index < (int32_t)h->entryCount) ? &h->entries[index] : NULL; +} + + +int32_t ResExtract(const char *path, const char *name, const char *outFile) { + if (!outFile) { + return BAS_FALSE; + } + + uint32_t size = 0; + void *data = resReadNamed(path, name, &size); + + if (!data) { + return BAS_FALSE; + } + + FILE *f = fopen(outFile, "wb"); + + if (!f) { + free(data); + return BAS_FALSE; + } + + int32_t result = (fwrite(data, 1, size, f) == size) ? BAS_TRUE : BAS_FALSE; + + fclose(f); + free(data); + return result; +} + + +static void resFree(void *obj) { + dvxResClose((DvxResHandleT *)obj); +} + + +// ResGetData$: every byte of a resource (BINARY STRING); "" when it is +// not there. +const uint8_t *ResGetData(const char *path, const char *name, int32_t *outLen) { + uint32_t size = 0; + + free(sData); + sData = (uint8_t *)resReadNamed(path, name, &size); + *outLen = sData ? (int32_t)size : 0; + return sData; +} + + +// ResGetText$: a resource's text, up to its first NUL; "" when it is not +// there. +const char *ResGetText(const char *path, const char *name) { + uint32_t size = 0; + char *data = (char *)resReadNamed(path, name, &size); + uint32_t len = 0; + + while (data && len < size && data[len] != '\0') { + len++; + } + + basTextStart(&sText); + basTextAppendLen(&sText, data ? data : "", (int32_t)len); + free(data); + return basTextEnd(&sText); +} + + +const char *ResName(int32_t handle, int32_t index) { + const DvxResDirEntryT *e = resEntry(handle, index); + + return e ? e->name : ""; +} + + +int32_t ResOpen(const char *path) { + return path ? basHandleAdd(&sFiles, dvxResOpen(path)) : BAS_NO_HANDLE; +} + + +// A named resource of the file at path, read in one go (malloc'd, the +// caller frees); NULL when the file or the resource is not there. +static void *resReadNamed(const char *path, const char *name, uint32_t *outSize) { + *outSize = 0; + + if (!path || !name) { + return NULL; + } + + DvxResHandleT *h = dvxResOpen(path); + + if (!h) { + return NULL; + } + + void *data = dvxResRead(h, name, outSize); + + dvxResClose(h); + return data; +} + + +int32_t ResRemove(const char *path, const char *name) { + if (!path || !name) { + return BAS_FALSE; + } + + return dvxResRemove(path, name) == 0 ? BAS_TRUE : BAS_FALSE; +} + + +int32_t ResSize(int32_t handle, int32_t index) { + const DvxResDirEntryT *e = resEntry(handle, index); + + return e ? (int32_t)e->size : 0; +} + + +int32_t ResType(int32_t handle, int32_t index) { + const DvxResDirEntryT *e = resEntry(handle, index); + + return e ? (int32_t)e->type : 0; +} diff --git a/src/libs/kpunch/libdvx/dvxText.c b/src/libs/kpunch/libdvx/dvxText.c index 78ee7b7d..08467b1f 100644 --- a/src/libs/kpunch/libdvx/dvxText.c +++ b/src/libs/kpunch/libdvx/dvxText.c @@ -176,6 +176,7 @@ typedef struct FontEntryT { int32_t refs; uint32_t lastUse; // sUseClock when last given up struct FontEntryT *next; + bool stale; // made for the other smoothing (dvxFontSmoothBegin) } FontEntryT; // What a font is built from (entryBuild) @@ -251,12 +252,14 @@ const char *dvxFontFamilyName(int32_t index); const char *dvxFontFamilyOf(const BitmapFontT *font); char *dvxFontLicence(const BitmapFontT *font); BitmapFontT *dvxFontOpen(const char *family, int32_t pixelSize, uint32_t style); +bool dvxFontRebuildStep(void); void dvxFontRemoveOwner(int32_t owner); void dvxFontRescan(void); BitmapFontT *dvxFontRetain(BitmapFontT *font); void dvxFontSetSmooth(bool smooth); int32_t dvxFontSizes(const char *family, uint32_t style, int32_t *sizes, int32_t max); bool dvxFontSmooth(void); +void dvxFontSmoothBegin(bool smooth); static FontEntryT *entryBuild(int32_t owner, const FontPickT *pick, uint32_t style); static void entryFinish(FontEntryT *e, const BuildT *b); static void entryFree(FontEntryT *e); @@ -281,8 +284,6 @@ static void idleTrim(int32_t keep); static bool isBox(int32_t ch); static void levelRect(uint8_t *px, int32_t w, int32_t h, int32_t x0, int32_t y0, int32_t x1, int32_t y1); static void levelsFrom(BuildT *b, const FontGlyphT *g, const uint8_t *bits, int32_t bpp); -static int32_t memAsSystem(void); -static void memRestore(int32_t prev); static int32_t nearestSize(const FontFaceT *a, const FontFaceT *b, int32_t pixelSize); static void ownerDetach(int32_t owner, const char *family); static bool pickFaces(int32_t owner, const char *family, uint32_t style, const FontFaceT **mine, const FontFaceT **base, uint32_t *synth); @@ -524,10 +525,44 @@ BitmapFontT *dvxFontOpen(const char *family, int32_t pixelSize, uint32_t style) } +// The fonts first, then the fonts with lines, which copy them. +bool dvxFontRebuildStep(void) { + FontEntryT *font = NULL; + FontEntryT *lines = NULL; + + for (FontEntryT *e = sEntries; e && !font; e = e->next) { + if (e->stale && !e->base) { + font = e; + } else if (e->stale && !lines) { + lines = e; + } + } + + FontEntryT *next = font ? font : lines; + + if (!next) { + return false; + } + + int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); + + next->stale = false; + + if (next->base) { + entryLinesCopy(next, next->font.style & FONT_STYLE_LINES); + } else { + entryRebuild(next); + } + + dvxMemSetApp(prev); + return true; +} + + // Every face added for owner forgotten (for OWNER_ALL, the added ones: // the installed faces stay), then the families left with none void dvxFontRemoveOwner(int32_t owner) { - int32_t prev = memAsSystem(); + int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); for (int32_t i = (int32_t)arrlen(sFamilies) - 1; i > SYSTEM_FAMILY_INDEX; i--) { if (sFamilies[i].owner == owner) { @@ -538,19 +573,19 @@ void dvxFontRemoveOwner(int32_t owner) { ownerDetach(owner, NULL); sViewOwner = OWNER_GONE; - memRestore(prev); + dvxMemSetApp(prev); } // Read the FONTS folder again on the next call; unload the fonts no one // holds (added families stay) void dvxFontRescan(void) { - int32_t prev = memAsSystem(); + int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); idleTrim(0); sScanned = false; sViewOwner = OWNER_GONE; - memRestore(prev); + dvxMemSetApp(prev); } @@ -566,38 +601,13 @@ BitmapFontT *dvxFontRetain(BitmapFontT *font) { // Fonts smoothed (anti-aliased) where their face has the size smoothed: -// the idle fonts are unloaded and the held ones built again in place -- -// the fonts first, then the fonts with lines that copy them +// the idle fonts are unloaded and the held ones built again in place +// (dvxFontSmoothBegin, then every dvxFontRebuildStep). void dvxFontSetSmooth(bool smooth) { - if (smooth == sSmooth) { - return; + dvxFontSmoothBegin(smooth); + + while (dvxFontRebuildStep()) { } - - int32_t prev = memAsSystem(); - FontEntryT **held = NULL; - - sSmooth = smooth; - scanFonts(); - idleTrim(0); - - for (FontEntryT *e = sEntries; e; e = e->next) { - arrput(held, e); - } - - for (int32_t i = 0; i < arrlen(held); i++) { - if (!held[i]->base) { - entryRebuild(held[i]); - } - } - - for (int32_t i = 0; i < arrlen(held); i++) { - if (held[i]->base) { - entryLinesCopy(held[i], held[i]->font.style & FONT_STYLE_LINES); - } - } - - arrfree(held); - memRestore(prev); } @@ -653,6 +663,26 @@ bool dvxFontSmooth(void) { } +// The idle fonts are unloaded; the held ones wait for dvxFontRebuildStep. +void dvxFontSmoothBegin(bool smooth) { + if (smooth == sSmooth) { + return; + } + + int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); + + sSmooth = smooth; + scanFonts(); + idleTrim(0); + + for (FontEntryT *e = sEntries; e; e = e->next) { + e->stale = true; + } + + dvxMemSetApp(prev); +} + + // The font pick says (its face, NULL: the system font) in style for // owner, from the size's gray strike when the pick is gray, built and // added to the cache with one holder; NULL when out of memory. A face @@ -870,6 +900,7 @@ static FontEntryT *entryLines(int32_t owner, const FontPickT *pick, uint32_t sty e->base = (FontEntryT *)base->priv; e->refs = 1; e->owner = owner; + e->stale = e->base->stale; entryLinesCopy(e, style & FONT_STYLE_LINES); snprintf(e->family, sizeof(e->family), "%s", pick->family); e->next = sEntries; @@ -982,7 +1013,7 @@ static bool faceAdd(int32_t owner, const DfnSourceT *src) { return false; } - int32_t prev = memAsSystem(); + int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); int32_t fam = familyMake(file.header.family, owner); bool ok = faceMerge(&sFamilies[fam].faces[file.header.style & FONT_STYLE_FACE], src, &file); @@ -992,7 +1023,7 @@ static bool faceAdd(int32_t owner, const DfnSourceT *src) { familyDropEmpty(fam); sViewOwner = OWNER_GONE; - memRestore(prev); + dvxMemSetApp(prev); return ok; } @@ -1189,10 +1220,10 @@ static BitmapFontT *fontOpen(int32_t owner, const char *family, int32_t pixelSiz } } - int32_t prev = memAsSystem(); + int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); FontEntryT *e = (style & FONT_STYLE_LINES) ? entryLines(owner, &pick, style) : entryBuild(owner, &pick, style); - memRestore(prev); + dvxMemSetApp(prev); return e ? &e->font : NULL; } @@ -1315,26 +1346,6 @@ static void levelsFrom(BuildT *b, const FontGlyphT *g, const uint8_t *bits, int3 } -// Make the system (app 0) the owner of what is allocated until -// memRestore(the value returned) -static int32_t memAsSystem(void) { - int32_t prev = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0; - - if (dvxMemAppIdPtr) { - *dvxMemAppIdPtr = 0; - } - - return prev; -} - - -static void memRestore(int32_t prev) { - if (dvxMemAppIdPtr) { - *dvxMemAppIdPtr = prev; - } -} - - // The size of the two faces (either may be NULL) nearest pixelSize, the // smaller of two as near static int32_t nearestSize(const FontFaceT *a, const FontFaceT *b, int32_t pixelSize) { @@ -1426,7 +1437,7 @@ static void scanFonts(void) { return; } - int32_t prev = memAsSystem(); + int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); char **names = platformKindReadDir(DvxKindFontsE); char cwd[DVX_MAX_PATH]; bool haveCwd = getcwd(cwd, sizeof(cwd)) != NULL; @@ -1497,7 +1508,7 @@ static void scanFonts(void) { } dvxReadDirFree(names); - memRestore(prev); + dvxMemSetApp(prev); } @@ -1659,7 +1670,7 @@ static void viewFor(int32_t owner) { return; } - int32_t prev = memAsSystem(); + int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM); arrsetlen(sView, 0); @@ -1673,7 +1684,7 @@ static void viewFor(int32_t owner) { qsort(sView + 1, (size_t)(arrlen(sView) - 1), sizeof(sView[0]), viewCompare); sViewOwner = owner; - memRestore(prev); + dvxMemSetApp(prev); } diff --git a/src/libs/kpunch/libdvx/dvxText.h b/src/libs/kpunch/libdvx/dvxText.h index 3e7ac42a..bed07914 100644 --- a/src/libs/kpunch/libdvx/dvxText.h +++ b/src/libs/kpunch/libdvx/dvxText.h @@ -114,6 +114,13 @@ BitmapFontT *dvxFontRetain(BitmapFontT *font); void dvxFontSetSmooth(bool smooth); bool dvxFontSmooth(void); +// dvxFontSetSmooth a font at a time (building a font takes 15-150 ms): +// Begin switches and marks the fonts open; each RebuildStep builds one +// of them again, false when none is left (dvxUpdate steps a font a frame +// and lays every window out again after the last). +bool dvxFontRebuildStep(void); +void dvxFontSmoothBegin(bool smooth); + // Read the FONTS folder again on the next call (after fonts were added or // removed); the fonts no one holds are unloaded. Open fonts stay valid. // Families added for programs stay. diff --git a/src/libs/kpunch/libdvx/dvxTypes.h b/src/libs/kpunch/libdvx/dvxTypes.h index 118e137e..ce96fdee 100644 --- a/src/libs/kpunch/libdvx/dvxTypes.h +++ b/src/libs/kpunch/libdvx/dvxTypes.h @@ -883,6 +883,10 @@ typedef struct WindowT { // low (no queuing/dispatching overhead) and matches the callback-driven // model that DOS programs expect. The widget system installs its own // handlers here via wgtInitWindow() and dispatches to individual widgets. + // onPaint's dirtyArea is the content to paint (content coordinates); + // a painter that changed only part of it may narrow it to that part, + // and only that part is composited (the widget paint does, after a + // partial repaint). void *userData; void (*onPaint)(struct WindowT *win, RectT *dirtyArea); void (*onKey)(struct WindowT *win, int32_t key, int32_t mod); diff --git a/src/libs/kpunch/libdvx/dvxWgt.h b/src/libs/kpunch/libdvx/dvxWgt.h index b70de196..c6aaa3cf 100644 --- a/src/libs/kpunch/libdvx/dvxWgt.h +++ b/src/libs/kpunch/libdvx/dvxWgt.h @@ -240,7 +240,10 @@ typedef struct WidgetClassT { // text: an argument's text belongs to the caller for the duration of the // call; a returned one (property get, method result) belongs to the // widget and stays valid until the next call on that widget or until -// the widget changes or is destroyed -- copy it to keep it. +// the widget changes or is destroyed -- copy it to keep it. A STRING's +// s always ends in a NUL; len, when above 0, is its length in bytes and +// may count NUL bytes before that one (a BASIC string holds any bytes: +// wgtValueBytes, wgtValueLen). 0 means up to the first NUL. #define WGT_IFACE_STRING 0 #define WGT_IFACE_INT 1 @@ -252,6 +255,7 @@ typedef struct WidgetClassT { typedef struct { uint8_t type; // WGT_IFACE_* + int32_t len; // STRING only: its bytes when it may hold NUL bytes; 0 = up to its NUL union { int32_t i; double f; @@ -259,8 +263,10 @@ typedef struct { }; } WgtValueT; -// Value constructors (wgtValueStr turns NULL into ""). +// Value constructors (wgtValueStr turns NULL into ""). wgtValueBytes +// takes len bytes of s, which may include NUL bytes; s[len] must be a NUL. WgtValueT wgtValueBool(bool b); +WgtValueT wgtValueBytes(const char *s, int32_t len); WgtValueT wgtValueDouble(double f); WgtValueT wgtValueFloat(double f); WgtValueT wgtValueInt(int32_t i); @@ -280,6 +286,9 @@ double wgtValueToFloat(const WgtValueT *v); int32_t wgtValueToInt(const WgtValueT *v); const char *wgtValueToText(const WgtValueT *v, char *buf, int32_t bufSize); +// A STRING value's length in bytes: its len, else up to its NUL. +int32_t wgtValueLen(const WgtValueT *v); + // The .frm / property-sheet boolean rule: "True" or "-1" is true, "False" // or "0" false (case-insensitive), anything else defaultValue. bool wgtParseBool(const char *text, bool defaultValue); @@ -1039,6 +1048,15 @@ typedef struct { void *fn; // function pointer } WgtMethodDescT; +// The method a control showing a picture (Image, ImageButton, PictureBox) +// offers for the bytes of a picture file, WGT_SIG_DYNAMIC: a STRING of +// any bytes and, optionally, the name its Picture property reads back +// afterwards; True when they are a picture DVX can show. The BASIC form +// runtime calls it to show a LoadPictureData picture. +#define WGT_METHOD_SET_PICTURE_DATA "SetPictureData" +#define WGT_PICTURE_DATA_ARGS_MIN 1 +#define WGT_PICTURE_DATA_ARGS_MAX 2 + // Event descriptor. params is the handler's parameter list as BASIC // writes it ("Topic As String", "" for none) -- the one place a widget's // own event parameters are written down (IDE handler stubs, designer.bas diff --git a/src/libs/kpunch/libdvx/dvxWgtP.h b/src/libs/kpunch/libdvx/dvxWgtP.h index 152b6af9..f155db2e 100644 --- a/src/libs/kpunch/libdvx/dvxWgtP.h +++ b/src/libs/kpunch/libdvx/dvxWgtP.h @@ -218,9 +218,13 @@ WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y); // Scrollbar helpers. widgetScrollbarThumb yields a zero-size thumb when // trackLen or totalSize is <= 0 and clamps scrollPos, so callers need no -// guards of their own. +// guards of their own. widgetScrollbarThumbMin is the same rule with the +// smallest thumb given (a bar's own width, so it is never shorter than +// square): widgetScrollbarThumb is it with SB_MIN_THUMB, and the window +// manager's scrollbars use it with SCROLLBAR_WIDTH. void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize); int32_t widgetScrollbarThumbDragScroll(int32_t trackLen, int32_t total, int32_t visible, int32_t relMouse, int32_t maxScroll); +void widgetScrollbarThumbMin(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t minThumb, int32_t *thumbPos, int32_t *thumbSize); // ============================================================ // Scrollbar drawing and hit testing (widgetScrollbar.c) @@ -279,7 +283,13 @@ void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value); // ============================================================ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +bool widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); + +// What the last partial wgtPaint pass changed (content coordinates): the +// boxes of the widgets it repainted, joined and widened a little; false +// when it repainted nothing. widgetOnPaint narrows the window's dirty +// area to it. +bool widgetPaintedArea(RectT *out); // A display onto w's window content buffer, clipped to w and its // ancestors, for a quick repaint (WGT_METHOD_QUICK_REPAINT) outside the diff --git a/src/libs/kpunch/libdvx/dvxWm.c b/src/libs/kpunch/libdvx/dvxWm.c index 8c5f2a79..8d6a50d4 100644 --- a/src/libs/kpunch/libdvx/dvxWm.c +++ b/src/libs/kpunch/libdvx/dvxWm.c @@ -59,6 +59,7 @@ #include "dvxFont.h" #include "dvxComp.h" #include "dvxWgt.h" +#include "dvxWgtP.h" #include "dvxPlat.h" #include @@ -127,7 +128,6 @@ static void copyLabel(char *dst, const char *src, size_t cap); static void drawBorderFrame(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t w, int32_t h, const RectT *well, bool fillCorner); static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win); static void drawResizeBreaks(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, WindowT *win); -static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW, int32_t dstH, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, int32_t bpp); static void drawTitleBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win); static void drawTitleGadget(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size); static void freeMenuItemSubMenu(MenuItemT *item); @@ -484,91 +484,6 @@ static void drawResizeBreaks(DisplayT *d, const BlitOpsT *ops, const ColorScheme } -// Nearest-neighbor scale blit used to render minimized window icons. -// Icons are ICON_SIZE x ICON_SIZE (64x64); the source can be the window's -// content buffer (live thumbnail) or a loaded icon image of any size. -// -// Optimization strategy for 486/Pentium: -// - Fixed-point 16.16 reciprocals replace per-pixel division with -// multiply+shift. Division is 40+ cycles on 486; multiply is 13. -// - Source coordinate lookup tables (srcXTab/srcYTab) are pre-computed -// once per call and indexed in the inner loop, eliminating repeated -// multiply+shift per pixel. -// - Static lookup table buffers avoid stack frame growth (ICON_TOTAL_SIZE=68 -// entries * 4 bytes = 272 bytes per table that would otherwise be allocated -// on every call during the compositing loop). Sized to the largest dstW/dstH -// ever passed (a provided icon scaled to ICON_TOTAL_SIZE), not just ICON_SIZE. -// - The bpp switch is outside the column loop so the branch is taken -// once per row rather than once per pixel. -// - Clipping is done once up front by adjusting row/col start/end ranges, -// so the inner loop runs with zero per-pixel clip checks. - -static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW, int32_t dstH, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, int32_t bpp) { - int32_t rowStart = 0; - int32_t rowEnd = dstH; - int32_t colStart = 0; - int32_t colEnd = dstW; - - if (dstY < d->clipY) { - rowStart = d->clipY - dstY; - } - - if (dstY + dstH > d->clipY + d->clipH) { - rowEnd = d->clipY + d->clipH - dstY; - } - - if (dstX < d->clipX) { - colStart = d->clipX - dstX; - } - - if (dstX + dstW > d->clipX + d->clipW) { - colEnd = d->clipX + d->clipW - dstX; - } - - if (rowStart >= rowEnd || colStart >= colEnd) { - return; - } - - static int32_t srcXTab[ICON_TOTAL_SIZE]; - static int32_t srcYTab[ICON_TOTAL_SIZE]; - int32_t visibleCols = colEnd - colStart; - int32_t visibleRows = rowEnd - rowStart; - - // Fixed-point 16.16 scale factors - uint32_t recipW = ((uint32_t)srcW << 16) / (uint32_t)dstW; - uint32_t recipH = ((uint32_t)srcH << 16) / (uint32_t)dstH; - - // Pre-multiply X offsets by bpp so the inner loop does a single array index - for (int32_t dx = 0; dx < visibleCols; dx++) { - srcXTab[dx] = (int32_t)(((uint32_t)(colStart + dx) * recipW) >> 16) * bpp; - } - - for (int32_t dy = 0; dy < visibleRows; dy++) { - srcYTab[dy] = (int32_t)(((uint32_t)(rowStart + dy) * recipH) >> 16); - } - - for (int32_t dy = rowStart; dy < rowEnd; dy++) { - int32_t sy = srcYTab[dy - rowStart]; - uint8_t *dstRow = d->backBuf + (dstY + dy) * d->pitch + (dstX + colStart) * bpp; - const uint8_t *srcRow = src + sy * srcPitch; - - if (bpp == 1) { - for (int32_t dx = 0; dx < visibleCols; dx++) { - dstRow[dx] = srcRow[srcXTab[dx]]; - } - } else if (bpp == 2) { - for (int32_t dx = 0; dx < visibleCols; dx++) { - *(uint16_t *)(dstRow + dx * 2) = *(const uint16_t *)(srcRow + srcXTab[dx]); - } - } else { - for (int32_t dx = 0; dx < visibleCols; dx++) { - *(uint32_t *)(dstRow + dx * 4) = *(const uint32_t *)(srcRow + srcXTab[dx]); - } - } - } -} - - // Renders the title bar: background fill, close gadget (left), minimize // and maximize gadgets (right), and centered title text. // @@ -874,21 +789,11 @@ static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32 // The WM scrollbar thumb rule, shared by the window scrollbars and the -// standalone ones (scrollbarAtThumb). Thumb size is proportional to the visible page -// relative to the total scrollable range: trackLen * pageSize / -// (range + pageSize). This gives a thumb that shrinks as content grows, -// matching Motif/Windows scrollbars. The size is clamped up to -// SCROLLBAR_WIDTH so it stays grabbable with huge content, then down to -// the track so it never spills over the arrow buttons of a short bar; the -// value is clamped into [min, max] so the thumb stays inside the track. -// -// The int64_t casts prevent overflow: trackLen can be ~400px, pageSize and -// range can be arbitrarily large (e.g. a 10000-line text buffer), and -// 400 * 10000 exceeds int32_t range. -// -// When range <= 0 (all content visible), the thumb fills the entire track, -// visually indicating there's nothing to scroll. - +// standalone ones (scrollbarAtThumb): the widget scrollbars' rule +// (widgetScrollbarThumbMin, page out of range + page, the thumb never +// shorter than the bar is wide), except that when range <= 0 (all content +// visible) the thumb fills the entire track, visually indicating there's +// nothing to scroll. static void thumbGeometry(int32_t trackLen, int32_t min, int32_t max, int32_t value, int32_t pageSize, int32_t *thumbPos, int32_t *thumbSize) { int32_t range = max - min; @@ -898,17 +803,7 @@ static void thumbGeometry(int32_t trackLen, int32_t min, int32_t max, int32_t va return; } - int32_t size = (int32_t)(((int64_t)pageSize * trackLen) / ((int64_t)range + pageSize)); - - size = DVX_MAX(size, SCROLLBAR_WIDTH); - size = DVX_MIN(size, trackLen); - - // Map value to pixel offset: value maps linearly to the range - // [0, trackLen - thumbSize] - int32_t clamped = DVX_MIN(DVX_MAX(value, min), max); - - *thumbSize = size; - *thumbPos = (int32_t)(((int64_t)(clamped - min) * (trackLen - size)) / range); + widgetScrollbarThumbMin(trackLen, range + pageSize, pageSize, value - min, SCROLLBAR_WIDTH, thumbPos, thumbSize); } @@ -1558,12 +1453,9 @@ void wmDrawMinimizedIcons(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT * continue; } - int32_t bpp = d->format.bytesPerPixel; - if (win->iconData && win->iconW > 0 && win->iconH > 0) { // Provided icon image -- draw edge-to-edge, no bevel - drawScaledRect(d, ix, iy, ICON_TOTAL_SIZE, ICON_TOTAL_SIZE, - win->iconData, win->iconW, win->iconH, win->iconPitch, bpp); + drawScaled(d, ix, iy, ICON_TOTAL_SIZE, ICON_TOTAL_SIZE, win->iconData, win->iconW, win->iconH, win->iconPitch, false, 0); } else { // Live thumbnail or grey fill -- draw with beveled border BevelStyleT bevel; @@ -1577,8 +1469,7 @@ void wmDrawMinimizedIcons(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT * int32_t contentY = iy + ICON_BORDER; if (win->contentBuf && win->contentW > 0 && win->contentH > 0) { - drawScaledRect(d, contentX, contentY, ICON_SIZE, ICON_SIZE, - win->contentBuf, win->contentW, win->contentH, win->contentPitch, bpp); + drawScaled(d, contentX, contentY, ICON_SIZE, ICON_SIZE, win->contentBuf, win->contentW, win->contentH, win->contentPitch, false, 0); } else { rectFill(d, ops, contentX, contentY, ICON_SIZE, ICON_SIZE, colors->windowFace); } @@ -2270,7 +2161,9 @@ void wmRaiseWindow(WindowStackT *stack, DirtyListT *dl, int32_t idx) { // The buffer is initialized to white so any area the app doesn't paint // shows a clean background rather than garbage. In direct-color modes an // all-ones byte fill is white; in 8-bit modes 0xFF would be palette index -// 255 (a reserved black entry), so the white palette index is used. +// 255 (a reserved black entry), so the white palette index is used. A +// widget window is not filled: its full paint, which follows, clears every +// pixel (a maximize or a resize step then clears once, not twice). int32_t wmReallocContentBuf(WindowT *win, const DisplayT *d) { free(win->contentBuf); @@ -2289,9 +2182,11 @@ int32_t wmReallocContentBuf(WindowT *win, const DisplayT *d) { return -1; } - uint8_t clearByte = (d->format.bytesPerPixel == 1) ? (uint8_t)packColor(d, CONTENT_CLEAR_RGB, CONTENT_CLEAR_RGB, CONTENT_CLEAR_RGB) : CONTENT_CLEAR_BYTE; + if (win->onPaint != widgetOnPaint) { + uint8_t clearByte = (d->format.bytesPerPixel == 1) ? (uint8_t)packColor(d, CONTENT_CLEAR_RGB, CONTENT_CLEAR_RGB, CONTENT_CLEAR_RGB) : CONTENT_CLEAR_BYTE; - memset(win->contentBuf, clearByte, bufSize); + memset(win->contentBuf, clearByte, bufSize); + } return 0; } diff --git a/src/libs/kpunch/libdvx/games.dhs b/src/libs/kpunch/libdvx/games.dhs index b0ba0bb1..7564ca51 100644 --- a/src/libs/kpunch/libdvx/games.dhs +++ b/src/libs/kpunch/libdvx/games.dhs @@ -240,6 +240,7 @@ if (!dvxExclusivePresent8(ctx, fb, 320, 200, 320)) { .item dvxExclusivePresent8 shows a picture of palette numbers on the whole screen, scaled by the largest whole factor that fits and centred on black, in any pixel format. At 8 bits per pixel the palette is the screen's own: dvxExclusiveSetPalette changes colours at once, so palette animation costs nothing, and a hardware model of up to 16 palettes of 16 colours fits in the 256 entries. .item To draw yourself, draw into dvxExclusiveDisplay(ctx)->backBuf (DVX's draw functions work on it with dvxExclusiveBlitOps) and call dvxExclusiveFlush. dvxExclusiveWaitRetrace waits for the vertical retrace before a flush, so the frame is not torn. .item dvxExclusiveEnd gives the screen back. Closing the window, the program ending, and End Task do too. +.item dvxExclusiveMirror instead shows the window's own content on the whole screen -- its widgets, or what it draws into its content buffer -- in the desktop's pixel format, scaled and centred the same way and kept up to date as the window paints; the mouse works on the content as in the window, with its cursor drawn. A BASIC form does this with FullScreen = True (and hears the changes in its FullScreen event); a BASIC SCREEN program uses FULLSCREEN. .endlist .topic games.sound diff --git a/src/libs/kpunch/libdvx/platform/dvxMemTrack.c b/src/libs/kpunch/libdvx/platform/dvxMemTrack.c index ec0f767d..d44866eb 100644 --- a/src/libs/kpunch/libdvx/platform/dvxMemTrack.c +++ b/src/libs/kpunch/libdvx/platform/dvxMemTrack.c @@ -198,6 +198,18 @@ void dvxMemResetApp(int32_t appId) { } +int32_t dvxMemSetApp(int32_t appId) { + if (!dvxMemAppIdPtr) { + return DVX_MEM_SYSTEM; + } + + int32_t prev = *dvxMemAppIdPtr; + + *dvxMemAppIdPtr = appId; + return prev; +} + + void *dvxRealloc(void *ptr, size_t size) { if (!ptr) { return dvxMalloc(size); diff --git a/src/libs/kpunch/libdvx/platform/dvxPlat.h b/src/libs/kpunch/libdvx/platform/dvxPlat.h index 65fd6782..63c8325d 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlat.h +++ b/src/libs/kpunch/libdvx/platform/dvxPlat.h @@ -112,6 +112,12 @@ const char *platformLogMemory(void); bool platformIsReadOnly(void); void platformSetReadOnly(bool readOnly); +// Whether the entry name that readdir gave for folder dir, with its +// d_type type, is a folder: from type when it tells, else (unknown, or +// a link) from stat. A stat per entry costs a search of the folder on +// DOS, so a listing that asks it for every entry takes quadratic time. +bool platformDirEntryIsDir(const char *dir, const char *name, int32_t type); + // True when a file can be created in dir (a scratch file is made and // removed again). bool platformDirIsWritable(const char *dir); @@ -232,6 +238,14 @@ bool platformKindNameOf(DvxKindE kind, const char *path, char *out, int32_t outS // dvxReadDirFree). char **platformKindReadDir(DvxKindE kind); +// What is installed (programs, and the files of the kinds above) may have +// changed: a package went in or out, a DOS program ran, a file job ended. +// platformInstalledChangeCount moves on (wrapping around) each time, so a +// program that shows what is installed (Program Manager) looks again only +// when it differs from the count it saw last. +uint32_t platformInstalledChangeCount(void); +void platformInstalledChanged(void); + // Notes the loader leaves for the shell to show once the desktop is up // (an add-on that was not loaded, and why); each text is copied. They // hold PLATFORM_BOOT_NOTES_MAX bytes in all (the NUL counted); past that a @@ -260,10 +274,10 @@ typedef uint32_t PlatformTicksT; PlatformTicksT platformClock(void); PlatformTicksT platformClockHz(void); -// Fine-grained, free-running timestamp for input-timing entropy (not a -// clock: units and wrap point are backend-specific). DOS returns the TSC -// when the CPU has one, else BIOS ticks with the PIT counter in the low -// 16 bits. +// Fine-grained, free-running timestamp for entropy (not a clock: units +// and wrap point are backend-specific). DOS returns the TSC with the PIT +// counter in its top 16 bits when the CPU has a TSC, else BIOS ticks with +// the PIT counter in the low 16 bits. uint64_t platformEntropyStamp(void); // Convert a millisecond interval to ticks at the platform rate. @@ -301,6 +315,13 @@ uint64_t platformEntropyStamp(void); // for use inside a timer callback. uint64_t platformMicros(void); +// The BIOS clock ticks (PLATFORM_PIT_HZ / PLATFORM_PIT_BIOS_DIVISOR, about +// 18.2 a second) since the timer service started, as the word the timer +// interrupt advances: reading it costs one load, for a check made too often +// for platformMicros (the BASIC VM's slice clock). Wraps; compare by +// subtraction. +const volatile uint32_t *platformBiosTicks(void); + // A timer callback. It runs INSIDE THE TIMER INTERRUPT, with interrupts // disabled, on a small interrupt stack: keep it to a few dozen // microseconds, do not call the C library, DVX or DOS, do not use floating @@ -703,7 +724,7 @@ int32_t platformBiosScanCode(int32_t key, int32_t mod); // Gather hardware information (CPU, clock, memory, DOS/DPMI version, // video, mouse, disk drives) and return as a pre-formatted text string. // The display pointer provides the current video mode info. Returns a -// pointer to a static buffer valid for the lifetime of the process. +// pointer valid until the next call ("" when out of memory). // On DOS this uses CPUID, RDTSC, DPMI, INT 21h, INT 33h, and VBE. const char *platformGetSystemInfo(const DisplayT *display); @@ -724,6 +745,19 @@ bool platformShortPath(const char *path, char *out, int32_t outSize); struct stat; +// A file's size and time as its folder lists them, for telling whether a +// file read before has changed (compare two for equality only). +typedef struct { + uint32_t size; // bytes (a file of 4 GB or more: 0xFFFFFFFF) + uint32_t time; // its date and time, as the platform keeps them +} PlatformStampT; + +// The stamp of the file at path, from its folder alone: on DOS one +// directory search (stat makes several calls there, tens of milliseconds +// on a slow disk), elsewhere stat. False when path is no file (or has a +// wildcard). +bool platformFileStamp(const char *path, PlatformStampT *out); + // The size stat reported for a file. DOS sizes are 32-bit unsigned (a // FAT32 file reaches 4 GB - 1) where the DOS C library's st_size is a // signed 32-bit off_t, so a file past 2 GB reads as negative there. @@ -797,6 +831,9 @@ bool platformPathDirName(const char *path, char *out, int32_t outSize); // a drive letter ("C:..."). bool platformPathIsAbsolute(const char *path); +// True for a drive or file-system root: "C:", "C:\\", "/", or "\\". +bool platformPathIsRoot(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 @@ -819,6 +856,11 @@ bool platformPathJoin(char *out, int32_t outSize, const char *dir, const char *n // library hands back "c:/dvx"). Returns path (NULL stays NULL). char *platformPathNative(char *path); +// out = the folder holding path (a trailing separator ignored), native +// separators: "C:\\DVX" for "C:/DVX/APPS/", "/" for "/a". False for a +// root, a name without a folder, or when it does not fit. +bool platformPathParent(const char *path, char *out, int32_t outSize); + // 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. @@ -863,6 +905,16 @@ bool platformReplaceFile(const char *tmpPath, const char *path, const char *bakP // done. bool platformWriteFile(const char *path, const void *data, int32_t len); +// Visit every file below dir, depth first, folders entered as they come +// and names starting with '.' skipped. A folder's names are read in full +// before any is visited (DOS has few file handles), and which are folders +// comes with them (platformDirEntryIsDir). fn gets each file's full path +// and its name; returning false stops the walk, and then so does +// platformWalkDir. A folder that cannot be read is skipped. +typedef bool (*PlatformWalkFnT)(const char *path, const char *name, void *ctx); + +bool platformWalkDir(const char *dir, PlatformWalkFnT fn, void *ctx); + // 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. @@ -874,6 +926,11 @@ const char *platformPathBaseName(const char *path); // returns true. bool platformCopyFile(const char *srcPath, const char *dstPath); +// platformCopyFile asking goOn(ctx) between pieces (a caller that lets +// other work go on while a big file copies); false from goOn stops the +// copy, which then fails like any other (its partial copy removed). +bool platformCopyFileWith(const char *srcPath, const char *dstPath, bool (*goOn)(void *ctx), void *ctx); + // Slurp a whole file into a freshly-malloc'd, NUL-terminated buffer. // Returns NULL on error (file missing, OOM, read truncated). On success, // *outLen (if non-NULL) receives the byte length (not counting the NUL). @@ -891,6 +948,12 @@ const char *dvxSkipWs(const char *s); // as pos propagates, collapsing a chain of appends to one truncation check. int32_t dvxStrAppendf(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) __attribute__((format(printf, 4, 5))); +// As dvxStrAppendf, but text that does not fit is cut to what does: the +// position returned is then bufSize - 1. A pos outside the buffer (a +// caller's negative code included) or a vsnprintf failure comes back +// unchanged. +int32_t dvxStrAppendfCut(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) __attribute__((format(printf, 4, 5))); + // Strip trailing ' ', '\t', '\r', '\n' from buf in place. Returns the // new length. buf may be NULL or empty. int32_t dvxTrimRight(char *buf); @@ -1018,6 +1081,29 @@ int32_t platformStripLineEndings(char *buf, int32_t len); #endif #define DVX_SYM(name) DVX_SYM_PREFIX name +// Calls fn with each name a DXE3 module file exports (as the file has +// it, DJGPP's leading underscore and all) and sets *runsAtLoad when the +// module has code that runs as it loads (constructors). False when path +// is not a readable, whole DXE3 file. The loader indexes the libraries +// it leaves for later with it. +typedef void (*PlatformDxeNameFnT)(void *ctx, const char *name); +bool platformDxeExports(const char *path, PlatformDxeNameFnT fn, void *ctx, bool *runsAtLoad); + +// Open a module, as dlopen does, the way the loader opens one: a library +// not loaded yet that exports a name the module needs is loaded first +// (with the libraries its .dep file names). A module calls this, never +// dlopen (dvx.exe does not export dlopen). On hosts without DXE it is +// dlopen. +void *platformModuleOpen(const char *path, int32_t mode); + +// The loader's: what platformModuleOpen calls (until it is set, dlopen), +// and the last-resort resolver for a name that dvx.exe and the loaded +// modules lack (DJGPP's _dlsymresolver; nothing on the host). +typedef void *(*PlatformModuleOpenFnT)(const char *path, int32_t mode); +typedef void *(*PlatformSymbolResolverFnT)(const char *name); +void platformSetModuleOpen(PlatformModuleOpenFnT fn); +void platformSetSymbolResolver(PlatformSymbolResolverFnT fn); + // Register platform and C runtime symbols with the dynamic module // loader so that DXE modules can resolve them at load time. On DOS // this calls dlregsym() with the full DJGPP libc/libm/libgcc/platform diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatHost.h b/src/libs/kpunch/libdvx/platform/dvxPlatHost.h index 5790a75e..d28e7a16 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlatHost.h +++ b/src/libs/kpunch/libdvx/platform/dvxPlatHost.h @@ -86,6 +86,15 @@ void hostClockAdvanceUs(uint64_t us); // ignored while hostClockSetRealTime(true) is in effect. void hostClockSet(uint32_t ms); +// Make every platformClock read advance the fake clock by ms milliseconds +// once it has been read (0, the default, leaves the clock alone), so code +// that checks the time between pieces of work sees time pass. +void hostClockSetReadStep(uint32_t ms); + +// The same for platformMicros: every read advances the fake clock by us +// microseconds once it has been read (0, the default: none). +void hostClockSetMicrosStep(uint32_t us); + // Switch between the fake clock (default, deterministic) and wall time // for interactive use. Entering real-time mode continues from the // current fake value; leaving it freezes at the current wall value. diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatInt.h b/src/libs/kpunch/libdvx/platform/dvxPlatInt.h index c8427d32..783edb6e 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlatInt.h +++ b/src/libs/kpunch/libdvx/platform/dvxPlatInt.h @@ -23,8 +23,9 @@ // dvxPlatInt.h -- what the platform layer's own files share // -// Private to the platform layer (not in the SDK): the DOS interrupt and -// real-mode facts dvxPlatformDos.c and dvxPlatformUtil.c both use, and +// Private to the platform layer (not in the SDK): the DOS interrupt, +// real-mode, timer, and CPU facts dvxPlatformDos.c and dvxPlatformUtil.c +// both use, and // the host's root and current folder (dvxPlatformHost.c and // dvxPlatformUtil.c). @@ -47,4 +48,43 @@ #define HOST_ROOT "/" #define HOST_CURRENT_DIR "." +#ifdef __DJGPP__ +#include +#include + +// BIOS timer tick counter at 0040:006C (linear 0x46C) +#define BIOS_TICK_ADDR 0x46C + +// PIT channel 0 (the system timer): command port, data port, the command +// that latches the count, and the count's width +#define PIT_CMD_PORT 0x43 +#define PIT_CH0_PORT 0x40 +#define PIT_LATCH_CH0 0x00 +#define PIT_COUNT_BITS 16 + +// EFLAGS ID bit: toggleable only when CPUID exists +#define EFLAGS_ID_BIT 0x200000U + +// CPUID leaf 1 and its EDX time stamp counter bit +#define CPUID_LEAF_FEATURES 1 +#define CPUID_FEAT_TSC (1U << 4) + +#define TSC_HI_SHIFT 32 // rdtsc: EDX holds the high dword + +// CPUID exists; the CPU has a time stamp counter (probed once). In +// dvxPlatformUtil.c, so a DOS tool that links only it has them too. +bool platformHasCpuid(void); +bool platformHasTsc(void); + +// The time stamp counter (the caller knows the CPU has one: +// platformHasTsc). Inline, so the timer interrupt can read it. +static inline __attribute__((always_inline)) uint64_t platformReadTsc(void) { + uint32_t lo; + uint32_t hi; + + __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi)); + return ((uint64_t)hi << TSC_HI_SHIFT) | lo; +} +#endif + #endif // DVX_PLAT_INT_H diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c index 13d22e14..260bc50d 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c +++ b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c @@ -55,8 +55,6 @@ #include "dvxPlatInt.h" #include "dvxPlatIsr.h" #include "dvxPal.h" -#include "dvxPrefs.h" -#include "../../../../tools/hlpcCompile.h" #include #include @@ -80,6 +78,7 @@ #include #include #include +#include // DJGPP-specific headers -- this is the ONLY file that includes these #include @@ -101,23 +100,18 @@ // RDTSC calibration: measure over this many BIOS timer ticks #define CLOCK_MEAS_TICKS 3 -// BIOS timer tick counter at 0040:006C (linear 0x46C). It counts at -// 1193182 / 65536 Hz (54925.4 us per tick) and wraps to 0 at midnight -// after BIOS_TICKS_PER_DAY ticks. -#define BIOS_TICK_ADDR 0x46C +// The BIOS timer tick counter (BIOS_TICK_ADDR) counts at 1193182 / 65536 +// Hz (54925.4 us per tick) and wraps to 0 at midnight after +// BIOS_TICKS_PER_DAY ticks. #define BIOS_TICK_US 54925ULL #define BIOS_TICKS_PER_DAY 0x1800B0UL -// PIT channel 0 (the system timer): command port, data port, and the -// command bytes the timer service uses: latch the count, rate generator -// (mode 2, low byte then high byte) while the service runs, and the BIOS's -// own square wave (mode 3) to hand the PIT back. A count of 0 means 65536. -#define PIT_CMD_PORT 0x43 -#define PIT_CH0_PORT 0x40 -#define PIT_LATCH_CH0 0x00 +// PIT channel 0 (PIT_CMD_PORT, PIT_CH0_PORT): the command bytes the timer +// service uses beside the latch, rate generator (mode 2, low byte then +// high byte) while the service runs, and the BIOS's own square wave +// (mode 3) to hand the PIT back. A count of 0 means 65536. #define PIT_CH0_RATE_GEN 0x34 #define PIT_CH0_SQUARE_WAVE 0x36 -#define PIT_COUNT_BITS 16 #define BYTE_BITS 8 #define DWORD_SHIFT 2 // bytes to doublewords #define DWORD_MASK 3 // the bytes past the last whole doubleword @@ -135,19 +129,14 @@ // CPU interrupt flag in EFLAGS (platformIrqSave / platformIrqRestore) #define EFLAGS_IF_BIT 0x200U -#define TSC_HI_SHIFT 32 // rdtsc: EDX holds the high dword // Real-mode interrupt vectors #define INT_VIDEO 0x10 #define INT_KEYBOARD 0x16 #define INT_MOUSE 0x33 -// EFLAGS ID bit: toggleable only when CPUID exists -#define EFLAGS_ID_BIT 0x200000U - // CPUID leaves, EAX(1) field layout, and EDX(1) feature bits #define CPUID_LEAF_VENDOR 0 -#define CPUID_LEAF_FEATURES 1 #define CPUID_EXT_BASE 0x80000000U #define CPUID_BRAND_FIRST 0x80000002U #define CPUID_BRAND_LAST 0x80000004U @@ -164,7 +153,6 @@ #define CPUID_FAMILY_EXTENDED 0x0F #define CPUID_FAMILY_P6 6 #define CPUID_FEAT_FPU (1U << 0) -#define CPUID_FEAT_TSC (1U << 4) #define CPUID_FEAT_MMX (1U << 23) #define CPUID_FEAT_SSE (1U << 25) #define CPUID_FEAT_SSE2 (1U << 26) @@ -247,6 +235,8 @@ #define MOUSE_FN_RESET 0x0000 #define MOUSE_FN_GET_STATE 0x0003 #define MOUSE_FN_SET_POS 0x0004 +#define MOUSE_FN_PRESS_INFO 0x0005 // presses of button BX since the last call (BX), counter cleared +#define MOUSE_FN_RELEASE_INFO 0x0006 // releases likewise #define MOUSE_FN_SET_XRANGE 0x0007 #define MOUSE_FN_SET_YRANGE 0x0008 #define MOUSE_FN_READ_MICKEYS 0x000B @@ -257,6 +247,7 @@ #define MOUSE_WHEEL_SIG 0x574D // 'WM' returned by the CuteMouse WheelAPI #define MOUSE_WHEEL_PRESENT 0x0001 // CX bit 0: wheel physically present #define MOUSE_WHEEL_SHIFT 8 // BH of fn 03h BX holds the wheel counter +#define MOUSE_BUTTONS 3 // left, right, middle: fn 05h / 06h's button numbers 0..2 #define MOUSE_SETTING_UNSET (-1) // INT 16h enhanced keyboard functions @@ -415,6 +406,13 @@ typedef struct { uint8_t bluePos; } VbeModeT; +// A mode's shape, all platformVideoEnumModes compares +typedef struct { + int32_t width; + int32_t height; + int32_t bpp; +} ModeKeyT; + // A banked mode's window (platformBankChunk): where it is in the address // space, its size and step, and which part of video memory it shows. typedef struct { @@ -472,14 +470,12 @@ static void bankSet(uint8_t window, uint32_t winStart); static void biosKeyFlush(void); static uint8_t biosVideoMode(void); static inline void copySpan(uint8_t *dst, const uint8_t *src, uint32_t bytes); -static bool cpuHasTsc(void); static void critErrorOwn(bool ours); static int32_t desktopModeRestore(const DisplayT *desktop); static void dosBreakSet(uint16_t state); static uint32_t estimateClockMhz(void); static uint32_t farPtrToLinear(uint32_t addr); static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t preferredBpp, ModeScoreFnT score, uint16_t *outMode, VbeModeT *outInfo, DisplayT *d); -static bool hasCpuid(void); static void installCritErrorHandler(void); static void int8Handler(void); static void int9Handler(void); @@ -489,11 +485,11 @@ static int32_t mapLfb(DisplayT *d, uint32_t physAddr, LfbMapT *m static int32_t modeStart(uint16_t mode, const VbeModeT *m, DisplayT *d, LfbMapT *map, BankT *bank); static void modeXCopy(const DisplayT *d, const RectT *r, uint32_t pageOffset); static void modeXSet(void); +static int32_t mouseMissedEdges(int32_t buttons); static uint32_t pagesToKb(unsigned long pages); static uint32_t pitLatchElapsed(void); static void platformCrashHandler(int sig); int32_t platformDosRun(const char *command, const char *folder, bool pause, const DisplayT *desktop); -ISR_INLINE uint64_t readTsc(void); static bool regionFromNear(const void *addr, uint32_t size, bool isCode, __dpmi_meminfo *out); static int32_t scoreExclusive(const VbeModeT *m, int32_t requestedW, int32_t requestedH, int32_t preferredBpp); static int32_t scoreMode(const VbeModeT *m, int32_t requestedW, int32_t requestedH, int32_t preferredBpp); @@ -530,21 +526,15 @@ static __dpmi_raddr sCritVectorDos; // DOS's, given back while a DOS pr static uint16_t sBreakDos = DOS_BREAK_OFF; static bool sPlatformInitDone = false; -// TSC availability for platformEntropyStamp, probed on first use. -typedef enum { - TscUnknownE = 0, - TscAbsentE = 1, - TscPresentE = 2 -} TscStateE; - -static TscStateE sTscState = TscUnknownE; - // Wheel state: set by platformMouseWheelInit, read by platformMousePoll // and platformGetSystemInfo. Declared here (above all functions) so // every function in the file can see them. static bool sHasMouseWheel = false; static int32_t sLastWheelDelta = 0; +// The buttons platformMousePoll last reported (mouseMissedEdges). +static int32_t sMouseLastButtons = 0; + // Software-tracked mouse cursor. sMouseRangeW/H are the clamp bounds, set to // the VESA resolution in platformMouseInit. Position comes from one of two // schemes selected at RUNTIME (sMouseMode): @@ -603,8 +593,9 @@ static int32_t sInjectHead = 0; static int32_t sInjectCount = 0; static InjectMouseT sInjectLast; -// System information static buffer -static char sSysInfoBuf[PLATFORM_SYSINFO_MAX]; +// The system information text (platformGetSystemInfo): built in a +// PLATFORM_SYSINFO_MAX buffer, then cut to its length +static char *sSysInfoBuf = NULL; static int32_t sSysInfoPos = 0; // Per-app memory tracking (dvxMalloc & co.) lives in dvxMemTrack.c. The @@ -772,29 +763,6 @@ static inline void copySpan(uint8_t *dst, const uint8_t *src, uint32_t bytes) { } -// Whether the CPU has a time-stamp counter (CPUID feature bit), probed -// once. -static bool cpuHasTsc(void) { - if (sTscState == TscUnknownE) { - sTscState = TscAbsentE; - - if (hasCpuid()) { - uint32_t eax; - uint32_t ebx; - uint32_t ecx; - uint32_t edx; - - __asm__ __volatile__ ("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) : "a"(CPUID_LEAF_FEATURES)); - - if (edx & CPUID_FEAT_TSC) { - sTscState = TscPresentE; - } - } - } - - return sTscState == TscPresentE; -} - // INT 24h: DVX's handler (critical errors fail the call), or DOS's own // while a DOS program runs. @@ -852,7 +820,7 @@ static uint32_t estimateClockMhz(void) { // spin } - uint64_t tsc1 = readTsc(); + uint64_t tsc1 = platformReadTsc(); uint32_t tickStart = _farpeekl(_dos_ds, BIOS_TICK_ADDR); uint32_t now = tickStart; @@ -866,7 +834,7 @@ static uint32_t estimateClockMhz(void) { continue; } - uint64_t delta = readTsc() - tsc1; + uint64_t delta = platformReadTsc() - tsc1; return (uint32_t)(delta / (CLOCK_MEAS_TICKS * BIOS_TICK_US)); } @@ -962,41 +930,6 @@ static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t pref } -// Check if the CPUID instruction is available. -// The CPUID instruction exists if bit 21 (ID flag) of EFLAGS can be -// toggled. On a 386 this bit is hardwired to 0. On a 486 without -// CPUID it's also hardwired. Only if we can flip it does CPUID exist. - -static bool hasCpuid(void) { - uint32_t before; - uint32_t after; - - __asm__ __volatile__ ( - "pushfl\n\t" - "popl %%eax\n\t" - "movl %%eax, %0\n\t" - "xorl %2, %%eax\n\t" - "pushl %%eax\n\t" - "popfl\n\t" - "pushfl\n\t" - "popl %%eax\n\t" - "movl %%eax, %1\n\t" - : "=r"(before), "=r"(after) - : "i"(EFLAGS_ID_BIT) - : "eax" - ); - - __asm__ __volatile__ ( - "pushl %0\n\t" - "popfl" - : - : "r"(before) - ); - - return ((before ^ after) & EFLAGS_ID_BIT) != 0; -} - - // Point the real-mode INT 24h vector at sCritStub, copied into a paragraph // of DOS memory, so every critical error fails the DOS call. Given back // to DOS's handler only while a DOS program runs (critErrorOwn): DOS @@ -1037,7 +970,7 @@ static void int8Handler(void) { TimerServiceT *t = &platformTimer; if (t->tscPresent) { - uint64_t now = readTsc(); + uint64_t now = platformReadTsc(); if (t->tscAtIrq != 0) { t->tscPeriod = now - t->tscAtIrq; @@ -1246,6 +1179,45 @@ static void modeXSet(void) { } +// The input is polled once a frame of the shell, so a click can begin and +// end between two polls, or a double-click's release and second press +// can: the button state alone then misses them. The driver's counters +// (fn 05h / 06h, cleared by each read) catch them, reported as one state +// more -- the other half of the edge comes with the next poll. +static int32_t mouseMissedEdges(int32_t buttons) { + __dpmi_regs r; + + for (int32_t b = 0; b < MOUSE_BUTTONS; b++) { + int32_t bit = 1 << b; + + memset(&r, 0, sizeof(r)); + r.x.ax = MOUSE_FN_PRESS_INFO; + r.x.bx = (uint16_t)b; + __dpmi_int(INT_MOUSE, &r); + + int32_t presses = r.x.bx; + + memset(&r, 0, sizeof(r)); + r.x.ax = MOUSE_FN_RELEASE_INFO; + r.x.bx = (uint16_t)b; + __dpmi_int(INT_MOUSE, &r); + + int32_t releases = r.x.bx; + bool down = (buttons & bit) != 0; + bool wasDown = (sMouseLastButtons & bit) != 0; + + if (!down && !wasDown && presses > 0) { + buttons |= bit; + } else if (down && wasDown && releases > 0) { + buttons &= ~bit; + } + } + + sMouseLastButtons = buttons; + return buttons; +} + + // Convert a DPMI page count to KB, mapping the "unavailable" sentinel to 0. static uint32_t pagesToKb(unsigned long pages) { if (pages == DPMI_INFO_UNAVAILABLE) { @@ -1434,23 +1406,6 @@ int32_t platformDriveList(PlatformDriveT *out, int32_t max) { } -// High-resolution timestamp for input-timing entropy: the TSC when the CPU -// has one, else the BIOS tick count with the live PIT channel 0 counter -// (~0.84 us resolution) in the low 16 bits. -uint64_t platformEntropyStamp(void) { - if (cpuHasTsc()) { - return readTsc(); - } - - outportb(PIT_CMD_PORT, PIT_LATCH_CH0); - - uint32_t countLo = inportb(PIT_CH0_PORT); - uint32_t countHi = inportb(PIT_CH0_PORT); - uint32_t ticks = _farpeekl(_dos_ds, BIOS_TICK_ADDR); - - return ((uint64_t)ticks << PIT_COUNT_BITS) | (countHi << BYTE_BITS) | countLo; -} - // Copies a dirty rectangle from the system RAM backbuffer to video // memory. This is the critical path for display updates -- the @@ -1524,7 +1479,14 @@ const char *platformGetSystemInfo(const DisplayT *display) { __dpmi_version_ret dpmiVer; bool haveDpmiVer = (__dpmi_get_version(&dpmiVer) == 0); - sSysInfoPos = 0; + free(sSysInfoBuf); + sSysInfoBuf = (char *)malloc(PLATFORM_SYSINFO_MAX); + sSysInfoPos = 0; + + if (!sSysInfoBuf) { + return ""; + } + sSysInfoBuf[0] = '\0'; sysInfoAppend("DVX System Information"); @@ -1538,7 +1500,7 @@ const char *platformGetSystemInfo(const DisplayT *display) { sysInfoAppend("CPU Type: %d86", dpmiVer.cpu); } - if (!hasCpuid()) { + if (!platformHasCpuid()) { sysInfoAppend("(no CPUID support)"); } else { uint32_t maxFunc; @@ -1774,6 +1736,9 @@ const char *platformGetSystemInfo(const DisplayT *display) { sysInfoAppend("=== Disk Drives ==="); sysInfoDrives(); + char *fit = (char *)realloc(sSysInfoBuf, (size_t)sSysInfoPos + 1); + + sSysInfoBuf = fit ? fit : sSysInfoBuf; return sSysInfoBuf; } @@ -1806,6 +1771,13 @@ void platformInit(void) { // 4. Critical errors fail the call instead of prompting installCritErrorHandler(); + + // stat() without what DVX never reads: inode numbers, execute bits (the + // C library opens a file of an unknown extension to read its magic), + // a folder's entry count as its size, the root's time stamp, and + // fstat's write bit. Its default does all of them, a few DOS calls + // more per stat on every listing. + _djstat_flags |= _STAT_INODE | _STAT_EXEC_EXT | _STAT_EXEC_MAGIC | _STAT_DIRSIZE | _STAT_ROOT_TIME | _STAT_WRITEBIT; } @@ -2060,7 +2032,7 @@ uint64_t platformMicros(void) { uint32_t elapsed = 0; if (t->tscPeriod != 0) { - elapsed = timerTscElapsed(readTsc(), t->tscAtIrq, t->tscPeriod, t->tscPeriodDivisor, t->divisor); + elapsed = timerTscElapsed(platformReadTsc(), t->tscAtIrq, t->tscPeriod, t->tscPeriodDivisor, t->divisor); } else { elapsed = pitLatchElapsed(); } @@ -2106,12 +2078,13 @@ uint64_t platformMicros(void) { void platformMouseInit(int32_t screenW, int32_t screenH) { __dpmi_regs r; - sMouseRangeW = screenW; - sMouseRangeH = screenH; - sCurX = screenW / 2; - sCurY = screenH / 2; - sMouseMode = MouseModeMickeyE; - sPromoteCounter = 0; + sMouseRangeW = screenW; + sMouseRangeH = screenH; + sCurX = screenW / 2; + sCurY = screenH / 2; + sMouseMode = MouseModeMickeyE; + sPromoteCounter = 0; + sMouseLastButtons = 0; // Function 00h: reset driver, detect mouse hardware (BX = buttons). memset(&r, 0, sizeof(r)); @@ -2231,6 +2204,10 @@ void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) { sCurY += (int16_t)r.x.dx; } + if (!sInjectMouse) { + *buttons = mouseMissedEdges(*buttons); + } + if (sInjectCount > 0) { sInjectLast = sInjectQueue[sInjectHead]; sInjectHead = (sInjectHead + 1) % INJECT_MOUSE_MAX; @@ -2380,6 +2357,13 @@ void platformRegisterSymOverrides(const PlatformSymOverrideT *entries) { } +// DJGPP's last resort for a name no registered table and no loaded +// module has (dlopen asks it for each import, dlsym(NULL) for its name). +void platformSetSymbolResolver(PlatformSymbolResolverFnT fn) { + _dlsymresolver = fn; +} + + // INT 21h AX=7160h CL=1, the long-name call for the short form of an // existing path (CH=0: a SUBSTed drive as its true path). Not the C // library's _truename_sfn: that asks with CL=0 (the fully qualified @@ -2717,7 +2701,7 @@ void platformTimerInit(void) { memset(&platformTimer, 0, sizeof(platformTimer)); platformTimer.divisor = PLATFORM_PIT_BIOS_DIVISOR; - platformTimer.tscPresent = cpuHasTsc(); + platformTimer.tscPresent = platformHasTsc(); uint32_t state = platformIrqSave(); @@ -2746,7 +2730,7 @@ void platformTimerProgram(uint32_t divisor) { outportb(PIT_CH0_PORT, (divisor >> BYTE_BITS) & BYTE_MASK); if (platformTimer.tscPresent) { - platformTimer.tscAtIrq = readTsc(); + platformTimer.tscAtIrq = platformReadTsc(); } timerRestartPeriod(&platformTimer, elapsed, divisor); @@ -2804,11 +2788,16 @@ void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void * return; } - // Static: this can run on an app task's small stack. - static VbeModeT seen[MAX_VBE_MODES]; - uint16_t modes[MAX_VBE_MODES]; - int32_t seenCount = 0; - int32_t modeCount = vbeReadModeList(modes); + // The shapes reported so far, on the heap: this can run on an app + // task's small stack + uint16_t modes[MAX_VBE_MODES]; + int32_t seenCount = 0; + int32_t modeCount = vbeReadModeList(modes); + ModeKeyT *seen = (ModeKeyT *)malloc((size_t)(modeCount > 0 ? modeCount : 1) * sizeof(ModeKeyT)); + + if (!seen) { + return; + } for (int32_t i = 0; i < modeCount; i++) { VbeModeT m; @@ -2830,9 +2819,14 @@ void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void * continue; } - seen[seenCount++] = m; + seen[seenCount].width = m.width; + seen[seenCount].height = m.height; + seen[seenCount].bpp = m.bpp; + seenCount++; cb(m.width, m.height, m.bpp, userData); } + + free(seen); } @@ -3164,15 +3158,6 @@ void platformYield(void) { } -// Time-stamp counter (the caller knows the CPU has one: cpuHasTsc). -ISR_INLINE uint64_t readTsc(void) { - uint32_t lo; - uint32_t hi; - - __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi)); - return ((uint64_t)hi << TSC_HI_SHIFT) | lo; -} - // The linear region of a near pointer range in this program's CS (isCode) // or DS space, for the DPMI lock/unlock calls. @@ -3603,9 +3588,15 @@ static void videoError(const char *fmt, ...) { // listed here and registered via dlregsym() before any dlopen(). // // Three categories: -// 1. Platform functions (all platform*() from this file) +// 1. Platform functions (every platform*() a module may call: the +// loader's own start-up ones, platformRegisterDxeExports, +// platformRegisterSymOverrides, platformUserInit, platformSetLogPath, +// platformSetLogMemory, platformBootNoteAdd, platformSetReadOnly, +// platformSetModuleOpen, platformSetSymbolResolver, and +// platformDxeExports, are not exported) // 2. C runtime (libc, libm, libgcc, DJGPP internals) -// 3. DXE3 runtime (dlopen/dlsym/dlclose/dlerror/dlregsym) +// 3. DXE3 runtime (dlsym/dlclose/dlerror/dlregsym; not dlopen: a module +// opens one with platformModuleOpen, which loads what it needs) // DJGPP stdio internals extern FILE __dj_stdin; @@ -3668,62 +3659,39 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(dvxMemAppIdPtr) DXE_EXPORT(dvxMemGetAppUsage) DXE_EXPORT(dvxMemResetApp) + DXE_EXPORT(dvxMemSetApp) DXE_EXPORT(dvxNextArg) DXE_EXPORT(dvxReadDir) DXE_EXPORT(dvxReadDirFree) DXE_EXPORT(dvxRealloc) DXE_EXPORT(dvxSkipWs) DXE_EXPORT(dvxStrAppendf) + DXE_EXPORT(dvxStrAppendfCut) DXE_EXPORT(dvxStrdup) DXE_EXPORT(dvxTrimRight) - // --- help compiler (dvxhlpc.c linked into dvx.exe; BASIC HelpCompile) --- - DXE_EXPORT(hlpcCompile) - DXE_EXPORT(hlpcSetMessageFn) - // --- preferences (dvxPrefs.c linked into dvx.exe; the only resident copy) --- - DXE_EXPORT(prefsAccelChoices) - DXE_EXPORT(prefsChoiceIndex) - DXE_EXPORT(prefsChoiceIndexOfValue) - DXE_EXPORT(prefsChoiceLabels) - DXE_EXPORT(prefsClose) - DXE_EXPORT(prefsCreate) - DXE_EXPORT(prefsGetBool) - DXE_EXPORT(prefsGetChoice) - DXE_EXPORT(prefsGetInt) - DXE_EXPORT(prefsGetSecret) - DXE_EXPORT(prefsGetString) - DXE_EXPORT(prefsGetVideoMode) - DXE_EXPORT(prefsKeyName) - DXE_EXPORT(prefsLoad) - DXE_EXPORT(prefsRemove) - DXE_EXPORT(prefsSave) - DXE_EXPORT(prefsSaveAs) - DXE_EXPORT(prefsSetBool) - DXE_EXPORT(prefsSetInt) - DXE_EXPORT(prefsSetSecret) - DXE_EXPORT(prefsSetString) - DXE_EXPORT(prefsSetVideoMode) - DXE_EXPORT(prefsWallpaperChoices) - DXE_EXPORT(prefsWheelDirChoices) // --- platform --- DXE_EXPORT(platformAltScanToChar) DXE_EXPORT(platformAppUserDir) DXE_EXPORT(platformBiosScanCode) + DXE_EXPORT(platformBiosTicks) DXE_EXPORT(platformBootDrive) - DXE_EXPORT(platformBootNoteAdd) DXE_EXPORT(platformBootNotes) DXE_EXPORT(platformChdir) DXE_EXPORT(platformClock) DXE_EXPORT(platformClockHz) DXE_EXPORT(platformCopyFile) + DXE_EXPORT(platformCopyFileWith) + DXE_EXPORT(platformDirEntryIsDir) DXE_EXPORT(platformDirIsWritable) DXE_EXPORT(platformDiskSpace) DXE_EXPORT(platformDosRun) DXE_EXPORT(platformDriveList) DXE_EXPORT(platformDvxIniPath) DXE_EXPORT(platformEntropyStamp) + DXE_EXPORT(platformFileStamp) DXE_EXPORT(platformFilterMatch) DXE_EXPORT(platformFlushRect) DXE_EXPORT(platformGetMemoryInfo) @@ -3735,6 +3703,8 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(platformInjectMouse) DXE_EXPORT(platformInjectScan) DXE_EXPORT(platformInstallCrashHandler) + DXE_EXPORT(platformInstalledChangeCount) + DXE_EXPORT(platformInstalledChanged) DXE_EXPORT(platformIrqRestore) DXE_EXPORT(platformIrqSave) DXE_EXPORT(platformIsReadOnly) @@ -3760,6 +3730,7 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(platformLongNames) DXE_EXPORT(platformMicros) DXE_EXPORT(platformMkdirRecursive) + DXE_EXPORT(platformModuleOpen) DXE_EXPORT(platformMouseInit) DXE_EXPORT(platformMousePoll) DXE_EXPORT(platformMouseSetAccel) @@ -3768,26 +3739,23 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(platformMouseWheelInit) DXE_EXPORT(platformMouseWheelPoll) DXE_EXPORT(platformNormalizeFKeyScan) + DXE_EXPORT(platformPathAbsolute) DXE_EXPORT(platformPathBaseName) DXE_EXPORT(platformPathDirEnd) - DXE_EXPORT(platformPathAbsolute) DXE_EXPORT(platformPathDirName) DXE_EXPORT(platformPathIsAbsolute) + DXE_EXPORT(platformPathIsRoot) DXE_EXPORT(platformPathIsSep) DXE_EXPORT(platformPathIsUnder) DXE_EXPORT(platformPathJoin) DXE_EXPORT(platformPathNative) + DXE_EXPORT(platformPathParent) DXE_EXPORT(platformPathSame) DXE_EXPORT(platformPathSibling) DXE_EXPORT(platformPathTidy) DXE_EXPORT(platformReadFile) - DXE_EXPORT(platformRegisterDxeExports) - DXE_EXPORT(platformRegisterSymOverrides) DXE_EXPORT(platformReplaceFile) DXE_EXPORT(platformSetAppExitHook) - DXE_EXPORT(platformSetLogMemory) - DXE_EXPORT(platformSetLogPath) - DXE_EXPORT(platformSetReadOnly) DXE_EXPORT(platformShortPath) DXE_EXPORT(platformShutdownHookAdd) DXE_EXPORT(platformShutdownHookRemove) @@ -3812,7 +3780,6 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(platformUnlockRegion) DXE_EXPORT(platformUserDir) DXE_EXPORT(platformUserDirOf) - DXE_EXPORT(platformUserInit) DXE_EXPORT(platformUserInTemp) DXE_EXPORT(platformUserPath) DXE_EXPORT(platformValidateFilename) @@ -3829,6 +3796,7 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(platformVideoSetPalette) DXE_EXPORT(platformVideoShutdown) DXE_EXPORT(platformVideoWaitRetrace) + DXE_EXPORT(platformWalkDir) DXE_EXPORT(platformWriteFile) DXE_EXPORT(platformYield) @@ -3944,6 +3912,7 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(remove) DXE_EXPORT(rename) DXE_EXPORT(rewind) + DXE_EXPORT(setvbuf) DXE_EXPORT(tmpfile) DXE_EXPORT(tmpnam) @@ -3967,6 +3936,7 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(realpath) DXE_EXPORT(stat) DXE_EXPORT(unlink) + DXE_EXPORT(utime) // --- time --- DXE_EXPORT(asctime) @@ -4067,7 +4037,6 @@ DXE_EXPORT_TABLE(sDxeExportTable) // --- DXE3 runtime --- DXE_EXPORT(dlclose) DXE_EXPORT(dlerror) - DXE_EXPORT(dlopen) DXE_EXPORT(dlregsym) DXE_EXPORT(dlsym) diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c b/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c index 9e64e264..b55fd5c6 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c +++ b/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c @@ -46,13 +46,14 @@ // Everything test-facing is declared in dvxPlatHost.h; everything DVX // proper sees is the dvxPlat.h contract. +#include "dvxApp.h" +#include "dvxImgInt.h" #include "dvxPlat.h" #include "dvxPlatHost.h" #include "dvxPlatInt.h" #include "dvxPlatIsr.h" #include "dvxPal.h" -#include "thirdparty/stb_image_write_wrap.h" #include #include @@ -91,9 +92,6 @@ // BIOS shift-state byte: only the low 8 bits are meaningful. #define MODIFIER_MASK 0xFF -// PNG output is 3 bytes per pixel (RGB). -#define PNG_COMPONENTS 3 - // Kilobytes per byte divisor for platformGetMemoryInfo. #define BYTES_PER_KB 1024 @@ -166,6 +164,8 @@ static uint32_t sTicks = 0; static uint64_t sMicros = 0; static uint32_t sTickRemUs = 0; static uint32_t sYieldStepUs = 0; // hostClockSetYieldStep +static uint32_t sReadStepMs = 0; // hostClockSetReadStep +static uint32_t sMicrosStepUs = 0; // hostClockSetMicrosStep static bool sRealTime = false; static uint64_t sRealTimeBase = WALL_CLOCK_UNSET; // wall us when real-time mode began static uint32_t sRealTimeStart = 0; // fake ticks when real-time mode began @@ -323,6 +323,16 @@ void hostClockSet(uint32_t ms) { } +void hostClockSetMicrosStep(uint32_t us) { + sMicrosStepUs = us; +} + + +void hostClockSetReadStep(uint32_t ms) { + sReadStepMs = ms; +} + + void hostClockSetRealTime(bool realTime) { if (realTime == sRealTime) { return; @@ -402,56 +412,19 @@ bool hostSavePng(const char *path) { return false; } - size_t rowBytes = (size_t)d->width * PNG_COMPONENTS; - uint8_t *rgb = (uint8_t *)malloc(rowBytes * (size_t)d->height); + // In the palette the screen shows, which a full-screen program may + // have set (platformVideoSetPalette), not the desktop's + DisplayT shown = *d; + + shown.palette = sPalette; + + uint8_t *rgb = dvxBufferToRgb(&shown, d->lfb, d->width, d->height, d->pitch); if (!rgb) { return false; } - const PixelFormatT *f = &d->format; - - for (int32_t y = 0; y < d->height; y++) { - const uint8_t *src = d->lfb + (size_t)y * (size_t)d->pitch; - uint8_t *dst = rgb + (size_t)y * rowBytes; - - for (int32_t x = 0; x < d->width; x++) { - uint32_t pixel; - - switch (f->bytesPerPixel) { - case 1: - pixel = src[x]; - break; - case 2: - pixel = ((const uint16_t *)src)[x]; - break; - default: - pixel = ((const uint32_t *)src)[x]; - break; - } - - if (f->bitsPerPixel == HOST_BPP_8) { - const uint8_t *entry = sPalette + (pixel & (PAL_ENTRIES - 1)) * PAL_CHANNELS; - - dst[0] = entry[0]; - dst[1] = entry[1]; - dst[2] = entry[2]; - } else { - uint32_t r = (pixel & f->redMask) >> f->redShift; - uint32_t g = (pixel & f->greenMask) >> f->greenShift; - uint32_t b = (pixel & f->blueMask) >> f->blueShift; - - // Scale each field up to 8 bits by replicating its top bits. - dst[0] = (uint8_t)((r << (8 - f->redBits)) | (r >> (2 * f->redBits - 8))); - dst[1] = (uint8_t)((g << (8 - f->greenBits)) | (g >> (2 * f->greenBits - 8))); - dst[2] = (uint8_t)((b << (8 - f->blueBits)) | (b >> (2 * f->blueBits - 8))); - } - - dst += PNG_COMPONENTS; - } - } - - bool ok = stbi_write_png(path, d->width, d->height, PNG_COMPONENTS, rgb, (int)rowBytes) != 0; + bool ok = dvxImgWrite(path, false, d->width, d->height, rgb); free(rgb); return ok; @@ -479,12 +452,20 @@ char platformBootDrive(void) { } +// The fake clock moves on by the read step after each read +// (hostClockSetReadStep). PlatformTicksT platformClock(void) { if (sRealTime) { return sRealTimeStart + (uint32_t)((wallClockUs() - sRealTimeBase) / US_PER_MS); } - return sTicks; + PlatformTicksT now = sTicks; + + if (sReadStepMs > 0) { + hostClockAdvance((int32_t)sReadStepMs); + } + + return now; } @@ -722,12 +703,19 @@ bool platformLongNames(const char *path) { } +// (hostClockSetMicrosStep). uint64_t platformMicros(void) { if (sRealTime) { return sRealTimeMicros + (wallClockUs() - sRealTimeBase); } - return sMicros; + uint64_t now = sMicros; + + if (sMicrosStepUs > 0) { + hostClockAdvanceUs(sMicrosStepUs); + } + + return now; } @@ -796,6 +784,13 @@ void platformRegisterSymOverrides(const PlatformSymOverrideT *entries) { } +// The host's dynamic linker has no last resort to hook: a name it cannot +// find stays missing. +void platformSetSymbolResolver(PlatformSymbolResolverFnT fn) { + (void)fn; +} + + // Host file systems have no short names: path itself. bool platformShortPath(const char *path, char *out, int32_t outSize) { return snprintf(out, (size_t)outSize, "%s", path) < outSize; diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformIrq.c b/src/libs/kpunch/libdvx/platform/dvxPlatformIrq.c index 00821a4f..605a8c86 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlatformIrq.c +++ b/src/libs/kpunch/libdvx/platform/dvxPlatformIrq.c @@ -46,6 +46,7 @@ typedef void (*HookFnT)(void); static bool hookAdd(HookFnT *table, int32_t count, HookFnT fn); static void hookRemove(HookFnT *table, int32_t count, HookFnT fn); +const volatile uint32_t *platformBiosTicks(void); void platformInjectScan(uint8_t scan); bool platformKeyEventRead(RawKeyEventT *evt); bool platformKeyIsDown(int32_t rawCode); @@ -111,6 +112,11 @@ static void hookRemove(HookFnT *table, int32_t count, HookFnT fn) { } +const volatile uint32_t *platformBiosTicks(void) { + return &platformTimer.biosTicks; +} + + void platformInjectScan(uint8_t scan) { uint32_t state = platformIrqSave(); diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c index d1fc8e1f..4fc70284 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c +++ b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c @@ -30,10 +30,12 @@ #include "dvxPlat.h" #include "dvxPlatInt.h" +#include "dvxEndian.h" #include "thirdparty/stb_ds_wrap.h" #include #include +#include #include #include #include @@ -48,6 +50,7 @@ #include #include #include +#include #include #else #include @@ -56,6 +59,17 @@ // Permission bits for directories created by platformMkdirRecursive (rwxr-xr-x). #define MKDIR_MODE 0755 +// platformFileStamp: what makes a path a pattern, the files a DOS search +// finds (not folders), and where DOS's date goes in the stamp +#define STAMP_WILDCARDS "*?" +#define STAMP_FIND_ATTR (FA_RDONLY | FA_HIDDEN | FA_SYSTEM | FA_ARCH) +#define DOS_DATE_SHIFT 16 + +// platformEntropyStamp on DOS: the PIT count goes in the TSC's top bits, +// which stay 0 for years of uptime; the latched count reads low byte first +#define ENTROPY_PIT_SHIFT 48 +#define PIT_BYTE_BITS 8 + // platformDiskSpace on DOS: INT 21h AH=36h (at most 2 GB), and the FAT32 // call 7303h with its record (the version word at +2 goes in as 0) in the // transfer buffer after the "X:\" root @@ -130,6 +144,18 @@ #define NAME_CHAR_FIRST '!' // an 8.3 name holds '!'..'~' only #define NAME_CHAR_LAST '~' +// The DXE3 file header (DJGPP sys/dxe.h, dxe3_header): its size and the +// fields platformDxeExports reads. An export entry is a 32-bit offset, +// then the name and its NUL. +#define DXE3_MAGIC 0x31455844u // "DXE1", which every DXE file starts with +#define DXE3_HEADER_BYTES 80 +#define DXE3_EXP_COUNT_OFS 28 +#define DXE3_EXP_TABLE_OFS 32 +#define DXE3_EXP_SIZE_OFS 36 +#define DXE3_INIT_OFS 72 +#define DXE3_NO_INIT 0xFFFFFFFFu // _init of a module without constructors +#define DXE3_SYM_OFFSET_SIZE 4 +#define DXE3_EXP_SIZE_MAX (1024u * 1024u) // ============================================================ @@ -150,6 +176,23 @@ typedef struct { int32_t higherMods; } ScanFoldT; +// A name platformWalkDir read, and whether it is a folder +typedef struct { + char *name; + bool isDir; +} WalkEntT; + +#ifdef __DJGPP__ +// The time stamp counter's presence, probed on first use (platformHasTsc) +typedef enum { + TscUnknownE = 0, + TscAbsentE = 1, + TscPresentE = 2 +} TscStateE; + +static TscStateE sTscState = TscUnknownE; +#endif + // ============================================================ // Prototypes @@ -165,6 +208,7 @@ char **dvxReadDir(const char *dirPath); void dvxReadDirFree(char **entries); const char *dvxSkipWs(const char *s); int32_t dvxStrAppendf(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...); +int32_t dvxStrAppendfCut(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...); int32_t dvxTrimRight(char *buf); static const ScanFoldT *findScanFold(int32_t scancode); static bool globMatchLen(const char *pat, int32_t patLen, const char *name); @@ -184,11 +228,21 @@ void platformBootNoteAdd(const char *text); const char *platformBootNotes(void); int32_t platformChdir(const char *path); bool platformCopyFile(const char *srcPath, const char *dstPath); +bool platformCopyFileWith(const char *srcPath, const char *dstPath, bool (*goOn)(void *ctx), void *ctx); bool platformDirIsWritable(const char *dir); bool platformDiskSpace(const char *path, uint64_t *totalBytes, uint64_t *freeBytes); const char *platformDvxIniPath(void); +bool platformDxeExports(const char *path, PlatformDxeNameFnT fn, void *ctx, bool *runsAtLoad); +#ifdef __DJGPP__ +uint64_t platformEntropyStamp(void); +#endif +bool platformFileStamp(const char *path, PlatformStampT *out); bool platformFilterMatch(const char *name, const char *filter); bool platformGlobMatch(const char *pattern, const char *name); +#ifdef __DJGPP__ +bool platformHasCpuid(void); +bool platformHasTsc(void); +#endif bool platformIsReadOnly(void); bool platformKindBrowseDir(char *out, int32_t outSize, DvxKindE kind); bool platformKindDir(char *out, int32_t outSize, DvxKindE kind, bool user); @@ -199,6 +253,7 @@ char **platformKindReadDir(DvxKindE kind); const char *platformLogMemory(void); bool platformLongNameChar(char c); int32_t platformMkdirRecursive(const char *path); +void *platformModuleOpen(const char *path, int32_t mode); int32_t platformNormalizeFKeyScan(int32_t scancode); void platformNormalizeKeyEvent(PlatformKeyEventT *evt, int32_t liveMods); void platformOutOfMemory(size_t bytes); @@ -207,10 +262,12 @@ 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 platformPathIsRoot(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 platformPathParent(const char *path, char *out, int32_t outSize); 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); @@ -219,6 +276,7 @@ bool platformReplaceFile(const char *tmpPath, const char *pat void platformSetAppExitHook(PlatformAppExitFnT fn); void platformSetLogMemory(void); void platformSetLogPath(const char *path); +void platformSetModuleOpen(PlatformModuleOpenFnT fn); void platformSetReadOnly(bool readOnly); uint64_t platformStatSize(const struct stat *st); int32_t platformStripLineEndings(char *buf, int32_t len); @@ -230,7 +288,9 @@ bool platformUserInTemp(void); bool platformUserPath(char *out, int32_t outSize, const char *rel); const char *platformValidateLongName(const char *name); const char *platformValidateShortName(const char *name); +bool platformWalkDir(const char *dir, PlatformWalkFnT fn, void *ctx); bool platformWriteFile(const char *path, const void *data, int32_t len); +static int32_t strAppendV(char *buf, int32_t bufSize, int32_t pos, bool cut, const char *fmt, va_list args); // ============================================================ @@ -241,13 +301,18 @@ bool platformWriteFile(const char *path, const void *data, in // (platformSetLogMemory). static const char *sLogPath = DVX_LOG_FILE; -// The memory log and its length (without the NUL) -static char sLogMem[PLATFORM_LOG_MEM_MAX]; +// The memory log (PLATFORM_LOG_MEM_MAX bytes, made by the first +// platformSetLogMemory) and its length (without the NUL) +static char *sLogMem = NULL; static int32_t sLogMemLen = 0; // Nothing can be saved: no writable user folder (platformUserInit) static bool sReadOnly = false; +// How often what is installed changed (platformInstalledChanged; wraps +// around) +static uint32_t sInstalledChanges = 0; + // The loader's notes for the shell, one per line, and whether they ran out // of room (platformBootNoteAdd) static char sBootNotes[PLATFORM_BOOT_NOTES_MAX]; @@ -273,6 +338,9 @@ static const char *const sDeviceNames[] = { // Where exit() and abort() of loaded modules go first (platformAppExit). static PlatformAppExitFnT sAppExitHook = NULL; +// The loader's module opening (platformModuleOpen), set at its start +static PlatformModuleOpenFnT sModuleOpen = NULL; + // Every modifier-specific scancode run the keyboard path folds. static const ScanFoldT sScanFolds[] = { { FKEY_SCAN_SHIFT_F1, FKEY_LOW_COUNT, KEY_SCAN_F1, KEY_MOD_SHIFT, 0, KEY_MOD_CTRL | KEY_MOD_ALT }, @@ -491,23 +559,23 @@ const char *dvxSkipWs(const char *s) { // as pos propagates, so a chain of appends collapses to a single // truncation check on the final return value. int32_t dvxStrAppendf(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) { - if (!buf || pos < 0 || pos >= bufSize) { - return -1; - } - va_list args; - int32_t avail = bufSize - pos; - int32_t written; va_start(args, fmt); - written = (int32_t)vsnprintf(buf + pos, (size_t)avail, fmt, args); + pos = strAppendV(buf, bufSize, pos, false, fmt, args); va_end(args); + return pos; +} - if (written < 0 || written >= avail) { - return -1; - } - return pos + written; +// As dvxStrAppendf, but text that does not fit is cut to what does. +int32_t dvxStrAppendfCut(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) { + va_list args; + + va_start(args, fmt); + pos = strAppendV(buf, bufSize, pos, true, fmt, args); + va_end(args); + return pos; } @@ -633,6 +701,10 @@ static void logMemAppend(const char *fmt, va_list ap) { line[n++] = '\n'; + if (!sLogMem) { + return; + } + int32_t over = sLogMemLen + n - (PLATFORM_LOG_MEM_MAX - 1); if (over > 0) { @@ -841,6 +913,11 @@ int32_t platformChdir(const char *path) { bool platformCopyFile(const char *srcPath, const char *dstPath) { + return platformCopyFileWith(srcPath, dstPath, NULL, NULL); +} + + +bool platformCopyFileWith(const char *srcPath, const char *dstPath, bool (*goOn)(void *ctx), void *ctx) { // Onto itself: opening the copy would empty the original first if (platformPathSame(srcPath, dstPath)) { return true; @@ -864,7 +941,7 @@ bool platformCopyFile(const char *srcPath, const char *dstPath) { bool ok = true; while (ok && (n = fread(buf, 1, sizeof(buf), src)) > 0) { - ok = (fwrite(buf, 1, n, dst) == n); + ok = (fwrite(buf, 1, n, dst) == n) && (!goOn || goOn(ctx)); } ok = ok && !ferror(src); @@ -879,6 +956,24 @@ bool platformCopyFile(const char *srcPath, const char *dstPath) { } +// DOS's readdir always knows (from the find data it already has); a host +// file system may say unknown, and a link has to be followed. +bool platformDirEntryIsDir(const char *dir, const char *name, int32_t type) { + char path[DVX_MAX_PATH]; + struct stat st; + + if (type == DT_DIR) { + return true; + } + + if (type != DT_UNKNOWN && type != DT_LNK) { + return false; + } + + return platformPathJoin(path, sizeof(path), dir, name) && stat(path, &st) == 0 && S_ISDIR(st.st_mode); +} + + // Make and remove a probe file in dir. bool platformDirIsWritable(const char *dir) { char path[DVX_MAX_PATH]; @@ -987,6 +1082,113 @@ const char *platformDvxIniPath(void) { } +// One read of the header and one of the export table; the names are +// handed out as the file has them (DJGPP's leading underscore and all). +bool platformDxeExports(const char *path, PlatformDxeNameFnT fn, void *ctx, bool *runsAtLoad) { + uint8_t hdr[DXE3_HEADER_BYTES]; + uint8_t *table = NULL; + FILE *f = fopen(path, "rb"); + uint32_t count = 0; + uint32_t size = 0; + uint32_t pos = 0; + bool ok; + + if (!f) { + return false; + } + + ok = fread(hdr, 1, sizeof(hdr), f) == sizeof(hdr) && dvxGetLe32(hdr) == DXE3_MAGIC; + + if (ok) { + count = dvxGetLe32(hdr + DXE3_EXP_COUNT_OFS); + size = dvxGetLe32(hdr + DXE3_EXP_SIZE_OFS); + ok = size <= DXE3_EXP_SIZE_MAX && (table = (uint8_t *)malloc(size + 1)) != NULL; + } + + ok = ok && fseek(f, (long)dvxGetLe32(hdr + DXE3_EXP_TABLE_OFS), SEEK_SET) == 0 && fread(table, 1, size, f) == size; + fclose(f); + + for (uint32_t i = 0; ok && i < count; i++) { + const char *name = (const char *)table + pos + DXE3_SYM_OFFSET_SIZE; + size_t left = pos + DXE3_SYM_OFFSET_SIZE < size ? size - pos - DXE3_SYM_OFFSET_SIZE : 0; + size_t len = left ? strnlen(name, left) : 0; + + // A name runs to its NUL inside the table, or the file is damaged + ok = len < left; + + if (ok) { + fn(ctx, name); + pos += DXE3_SYM_OFFSET_SIZE + (uint32_t)len + 1; + } + } + + if (ok) { + *runsAtLoad = dvxGetLe32(hdr + DXE3_INIT_OFS) != DXE3_NO_INIT; + } + + free(table); + return ok; +} + + +#ifdef __DJGPP__ +// High-resolution timestamp for entropy: the live PIT channel 0 counter +// (~0.84 us, its own crystal) with the TSC when the CPU has one, so a +// sample carries the jitter between the two clocks; else with the BIOS +// tick count above it. The host's is in dvxPlatformHost.c. +uint64_t platformEntropyStamp(void) { + outportb(PIT_CMD_PORT, PIT_LATCH_CH0); + + uint64_t pit = inportb(PIT_CH0_PORT); + + pit |= (uint64_t)inportb(PIT_CH0_PORT) << PIT_BYTE_BITS; + + if (platformHasTsc()) { + return platformReadTsc() ^ (pit << ENTROPY_PIT_SHIFT); + } + + return ((uint64_t)_farpeekl(_dos_ds, BIOS_TICK_ADDR) << PIT_COUNT_BITS) | pit; +} +#endif + + +// DOS: the directory search's own entry (DOS keeps the date in the high +// half). A long-name search keeps a handle open until a findnext finds +// nothing more, so one is made. Elsewhere the modification time's +// seconds and nanoseconds folded together, so a file rewritten within a +// second still changes its stamp. +bool platformFileStamp(const char *path, PlatformStampT *out) { + if (!path || strpbrk(path, STAMP_WILDCARDS)) { + return false; + } + +#ifdef __DJGPP__ + struct ffblk ff; + + if (findfirst(path, &ff, STAMP_FIND_ATTR) != 0) { + return false; + } + + out->size = (uint32_t)ff.ff_fsize; + out->time = ((uint32_t)ff.ff_fdate << DOS_DATE_SHIFT) | ff.ff_ftime; + findnext(&ff); + return true; +#else + struct stat st; + + if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) { + return false; + } + + uint64_t size = platformStatSize(&st); + + out->size = size > UINT32_MAX ? UINT32_MAX : (uint32_t)size; + out->time = (uint32_t)st.st_mtime ^ (uint32_t)st.st_mtim.tv_nsec; + return true; +#endif +} + + // 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) { @@ -1023,6 +1225,77 @@ bool platformGlobMatch(const char *pattern, const char *name) { } +#ifdef __DJGPP__ +// The CPUID instruction exists if bit 21 (ID flag) of EFLAGS can be +// toggled. On a 386 this bit is hardwired to 0. On a 486 without +// CPUID it's also hardwired. Only if we can flip it does CPUID exist. +bool platformHasCpuid(void) { + uint32_t before; + uint32_t after; + + __asm__ __volatile__ ( + "pushfl\n\t" + "popl %%eax\n\t" + "movl %%eax, %0\n\t" + "xorl %2, %%eax\n\t" + "pushl %%eax\n\t" + "popfl\n\t" + "pushfl\n\t" + "popl %%eax\n\t" + "movl %%eax, %1\n\t" + : "=r"(before), "=r"(after) + : "i"(EFLAGS_ID_BIT) + : "eax" + ); + + __asm__ __volatile__ ( + "pushl %0\n\t" + "popfl" + : + : "r"(before) + ); + + return ((before ^ after) & EFLAGS_ID_BIT) != 0; +} +#endif + + +#ifdef __DJGPP__ +// RDTSC exists when CPUID's leaf 1 reports the time stamp counter. A 386 +// or an early 486 has neither. +bool platformHasTsc(void) { + if (sTscState == TscUnknownE) { + sTscState = TscAbsentE; + + if (platformHasCpuid()) { + uint32_t eax; + uint32_t ebx; + uint32_t ecx; + uint32_t edx; + + __asm__ __volatile__ ("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) : "a"(CPUID_LEAF_FEATURES)); + + if (edx & CPUID_FEAT_TSC) { + sTscState = TscPresentE; + } + } + } + + return sTscState == TscPresentE; +} +#endif + + +uint32_t platformInstalledChangeCount(void) { + return sInstalledChanges; +} + + +void platformInstalledChanged(void) { + sInstalledChanges++; +} + + bool platformIsReadOnly(void) { return sReadOnly; } @@ -1158,7 +1431,7 @@ char **platformKindReadDir(DvxKindE kind) { const char *platformLogMemory(void) { - return sLogMem; + return sLogMem ? sLogMem : ""; } @@ -1214,6 +1487,13 @@ int32_t platformMkdirRecursive(const char *path) { } +// The loader's way once it has set it (platformSetModuleOpen); until then +// (the loader itself starting, the host's tests) dlopen. +void *platformModuleOpen(const char *path, int32_t mode) { + return sModuleOpen ? sModuleOpen(path, mode) : dlopen(path, (int)mode); +} + + // Fold a modifier-specific BIOS scancode (Shift/Ctrl/Alt+Fn, Alt+grey // navigation key) onto the plain key's scancode. int32_t platformNormalizeFKeyScan(int32_t scancode) { @@ -1332,6 +1612,15 @@ bool platformPathIsAbsolute(const char *path) { } +bool platformPathIsRoot(const char *path) { + if (platformPathIsSep(path[0]) && path[1] == '\0') { + return true; + } + + return path[0] && path[1] == ':' && (path[2] == '\0' || (platformPathIsSep(path[2]) && path[3] == '\0')); +} + + bool platformPathIsSep(char c) { return c == '/' || c == '\\'; } @@ -1384,6 +1673,22 @@ char *platformPathNative(char *path) { } +// The trimmed path's folder: platformPathDirName, once it is known to +// have one. +bool platformPathParent(const char *path, char *out, int32_t outSize) { + char dir[DVX_MAX_PATH]; + int32_t len = pathTrimmedLen(path); + + if (platformPathIsRoot(path) || len >= (int32_t)sizeof(dir)) { + return false; + } + + memcpy(dir, path, (size_t)len); + dir[len] = '\0'; + return platformPathDirEnd(dir) && platformPathDirName(dir, out, outSize); +} + + bool platformPathSame(const char *a, const char *b) { return a && b && pathTrimmedLen(a) == pathTrimmedLen(b) && platformPathIsUnder(a, b) != NULL; } @@ -1467,7 +1772,10 @@ char *platformReadFile(const char *path, int32_t *outLen) { long size = ftell(f); - if (size < 0) { + // More than its length can say with room for the terminator, or a + // folder (on Linux it opens, and its end is LONG_MAX: INT32_MAX where + // long is 32 bits) + if (size < 0 || size >= INT32_MAX) { fclose(f); return NULL; } @@ -1546,6 +1854,10 @@ void platformSetAppExitHook(PlatformAppExitFnT fn) { void platformSetLogMemory(void) { + if (!sLogMem) { + sLogMem = (char *)calloc(1, PLATFORM_LOG_MEM_MAX); + } + sLogPath = NULL; } @@ -1555,6 +1867,11 @@ void platformSetLogPath(const char *path) { } +void platformSetModuleOpen(PlatformModuleOpenFnT fn) { + sModuleOpen = fn; +} + + void platformSetReadOnly(bool readOnly) { sReadOnly = readOnly; } @@ -1751,6 +2068,49 @@ const char *platformValidateShortName(const char *name) { } +bool platformWalkDir(const char *dir, PlatformWalkFnT fn, void *ctx) { + DIR *d = opendir(dir); + WalkEntT *names = NULL; + bool goOn = true; + + if (!d) { + return true; + } + + for (struct dirent *ent = readdir(d); ent; ent = readdir(d)) { + WalkEntT e; + + if (ent->d_name[0] == '.') { + continue; + } + + e.isDir = platformDirEntryIsDir(dir, ent->d_name, ent->d_type); + e.name = strdup(ent->d_name); + + if (e.name) { + arrput(names, e); + } + } + + closedir(d); + + for (int32_t i = 0; goOn && i < (int32_t)arrlen(names); i++) { + char path[DVX_MAX_PATH]; + + if (platformPathJoin(path, sizeof(path), dir, names[i].name)) { + goOn = names[i].isDir ? platformWalkDir(path, fn, ctx) : fn(path, names[i].name, ctx); + } + } + + for (int32_t i = 0; i < (int32_t)arrlen(names); i++) { + free(names[i].name); + } + + arrfree(names); + return goOn; +} + + bool platformWriteFile(const char *path, const void *data, int32_t len) { char tmpPath[DVX_MAX_PATH]; char bakPath[DVX_MAX_PATH]; @@ -1776,3 +2136,29 @@ bool platformWriteFile(const char *path, const void *data, int32_t len) { return platformReplaceFile(tmpPath, path, bakPath); } + + +// The appends' shared core: fmt/args into buf at pos. Returns the new +// position. Text that does not fit returns -1, or with cut the position +// of the cut (bufSize - 1); a pos outside the buffer or a vsnprintf +// failure returns -1, or with cut pos unchanged. +static int32_t strAppendV(char *buf, int32_t bufSize, int32_t pos, bool cut, const char *fmt, va_list args) { + int32_t fail = cut ? pos : -1; + + if (!buf || pos < 0 || pos >= bufSize) { + return fail; + } + + int32_t avail = bufSize - pos; + int32_t written = (int32_t)vsnprintf(buf + pos, (size_t)avail, fmt, args); + + if (written < 0) { + return fail; + } + + if (written >= avail) { + return cut ? bufSize - 1 : -1; + } + + return pos + written; +} diff --git a/src/libs/kpunch/libdvx/sdkGuide.dhs b/src/libs/kpunch/libdvx/sdkGuide.dhs index 6ef0cdad..40d64a7b 100644 --- a/src/libs/kpunch/libdvx/sdkGuide.dhs +++ b/src/libs/kpunch/libdvx/sdkGuide.dhs @@ -301,7 +301,7 @@ The build makes mathx.lib and mathx.hlp (a preview of the help). The project hol .h2 How the Library Works -DVX loads every library when it starts, before any widget or program, in the order the .dep files require. Every function that is not static is exported: a program, widget, or library loaded later calls it directly, and DVX connects the call when it loads that module. A library that calls another library's functions lists it in its .dep file. +DVX loads a library when something first needs it: a program, widget, or library that calls one of its functions (DVX loads the library just before that module), or a lookup by name (dlsym(NULL), a BASIC DECLARE LIBRARY call, dvxLibraryLoaded). Libraries with code that runs as they load (constructors) are loaded when DVX starts, so they can hook into the shell. Every function that is not static is exported: a program, widget, or library calls it directly, and DVX connects the call when it loads that module. A library that calls another library's functions lists it in its .dep file, which DVX loads first. A module that opens another module calls platformModuleOpen (dvxPlat.h), never dlopen, which dvx.exe does not export. BASIC programs add mathx.bas to their project. DECLARE LIBRARY looks up each function by exactly the name it declares, without a trailing $ (a $ marks a function returning a string), so mathxGreet$ calls the C function mathxGreet. Keep the declarations and the C functions in step; strings are passed as const char * and returned as const char *. @@ -451,7 +451,7 @@ A control project builds NAME.wgt, a file that holds only resources: author, version ... The project's metadata; helpfile its help file's name .endtable -The loader tells such a file from a module by its first bytes: instead of loading it, it hands it, once every library and native widget is loaded, to the BASIC run-time library's basCtlLoadFile (found with dlsym; without the library the file is skipped and logged). basCtlLoadFile registers each control in the file for every program and the loader records the file as theirs, so the toolbox finds their icon24 and name. +The loader tells such a file from a module by its first bytes: instead of loading it, it hands it, once every native widget is loaded, to the BASIC run-time library's basCtlLoadFile (found with dlsym; without the library the file is skipped and logged). basCtlLoadFile registers each control in the file for every program and the loader records the file as theirs, so the toolbox finds their icon24 and name. .h2 The Bridge @@ -593,7 +593,7 @@ Give acme.KEY to the people who install your packages (Software, File > Trust a DVX is packages too: kpunch.dvx (the loader, SYSTEM, the shell, the core libraries and widgets, and the everyday programs), kpunch.basefont (the base fonts), and the optional parts Setup's "What to install" page offers. Each part is a bundle (kind bundle: a package with no files of its own that needs its members) and each member is one module in its own package: kpunch.basic (dvxbasic, basdev), kpunch.data (dvxsql, datactrl, dbgrid), kpunch.net (dvxnet, http, pkgnet), kpunch.comm (terminal, conn, xfer; it needs kpunch.net), kpunch.media (player, recorder), kpunch.devtools (iconed, resedit, helpedit, pkgbuild, widshow), and kpunch.demos (dvxdemo, basdemo). src/packages.ini lists them. The build (src/installer/mkPackages.py) works out what each member needs from the modules its code imports, the libraries its BASIC program declares, and the manifest's uses (a module reached by name), and stops when DVX itself, or a part, would need a part it does not name. A member's files outside its folder are its paths: the DOS tools in SYSTEM go with the part they serve (BASCOMP.EXE with kpunch.basdev, DVXRES.EXE with kpunch.resedit, and DVXHLPC.EXE with kpunch.helpedit, the Help Editor); DVXPKG.EXE stays in DVX itself, to install and remove packages at the DOS prompt. -A module of yours that cannot work without a part's library names the library's package in PKG_NEEDS (PKG_NEEDS="kpunch.dvxnet=1.0.0"): Software then installs it only where that part is. One that can do without it does not import it: it asks dvxLibraryLoaded (BASIC: SysLibraryLoaded, system.bas) and reaches the library by name only when it is there, as Software does with pkgnet. BASIC's SQL functions do nothing without the Databases part (SQLOpen returns 0). +A module of yours that cannot work without a part's library names the library's package in PKG_NEEDS (PKG_NEEDS="kpunch.dvxnet=1.0.0"): Software then installs it only where that part is. One that can do without it does not import it: it asks dvxLibraryLoaded (BASIC: SysLibraryLoaded, system.bas), which loads the library when it is installed, and reaches the library by name only when it is there, as Software does with pkgnet. Not importing a library also keeps DVX from loading it before it is needed. BASIC's SQL functions do nothing without the Databases part (SQLOpen returns 0). .h2 Publishing on the Package Server diff --git a/src/libs/kpunch/libdvx/widgetCore.c b/src/libs/kpunch/libdvx/widgetCore.c index 1146301e..80470383 100644 --- a/src/libs/kpunch/libdvx/widgetCore.c +++ b/src/libs/kpunch/libdvx/widgetCore.c @@ -881,42 +881,8 @@ void widgetRemoveChild(WidgetT *parent, WidgetT *child) { } -// Calculates thumb position and size for a scrollbar track. -// Used by both the WM-level scrollbars and widget-internal scrollbars -// (ListBox, TreeView, etc.) to maintain consistent scrollbar behavior. -// -// The thumb size is proportional to visibleSize/totalSize -- a larger -// visible area means a larger thumb, giving visual feedback about how -// much content is scrollable. SB_MIN_THUMB prevents the thumb from -// becoming too small to grab with a mouse. - void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize) { - // No track or no content: no thumb. Callers need no guard of their own. - if (trackLen <= 0 || totalSize <= 0) { - *thumbPos = 0; - *thumbSize = 0; - return; - } - - // 64-bit products: pixel scroll ranges (a tree or text area of several - // hundred thousand 16px rows) overflow int32 once multiplied by the track. - *thumbSize = (int32_t)(((int64_t)trackLen * visibleSize) / totalSize); - - if (*thumbSize < SB_MIN_THUMB) { - *thumbSize = SB_MIN_THUMB; - } - - if (*thumbSize > trackLen) { - *thumbSize = trackLen; - } - - int32_t maxScroll = totalSize - visibleSize; - - if (maxScroll > 0) { - *thumbPos = (int32_t)(((int64_t)(trackLen - *thumbSize) * clampInt(scrollPos, 0, maxScroll)) / maxScroll); - } else { - *thumbPos = 0; - } + widgetScrollbarThumbMin(trackLen, totalSize, visibleSize, scrollPos, SB_MIN_THUMB, thumbPos, thumbSize); } @@ -949,6 +915,44 @@ int32_t widgetScrollbarThumbDragScroll(int32_t trackLen, int32_t total, int32_t } +// Calculates thumb position and size for a scrollbar track, for the +// widget scrollbars (ListBox, TreeView, etc.) and the window manager's +// alike, so every scrollbar behaves the same. +// +// The thumb size is proportional to visibleSize/totalSize -- a larger +// visible area means a larger thumb, giving visual feedback about how +// much content is scrollable. minThumb keeps the thumb from becoming too +// small to grab with a mouse; it never spills past the track. +void widgetScrollbarThumbMin(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t minThumb, int32_t *thumbPos, int32_t *thumbSize) { + // No track or no content: no thumb. Callers need no guard of their own. + if (trackLen <= 0 || totalSize <= 0) { + *thumbPos = 0; + *thumbSize = 0; + return; + } + + // 64-bit products: pixel scroll ranges (a tree or text area of several + // hundred thousand 16px rows) overflow int32 once multiplied by the track. + *thumbSize = (int32_t)(((int64_t)trackLen * visibleSize) / totalSize); + + if (*thumbSize < minThumb) { + *thumbSize = minThumb; + } + + if (*thumbSize > trackLen) { + *thumbSize = trackLen; + } + + int32_t maxScroll = totalSize - visibleSize; + + if (maxScroll > 0) { + *thumbPos = (int32_t)(((int64_t)(trackLen - *thumbSize) * clampInt(scrollPos, 0, maxScroll)) / maxScroll); + } else { + *thumbPos = 0; + } +} + + // Moves keyboard focus to w (NULL clears focus). Clears the previous // widget's selection, repaints both widgets, and fires the blur/focus // callbacks via widgetFireFocusChange. A no-op when w already has focus. diff --git a/src/libs/kpunch/libdvx/widgetEvent.c b/src/libs/kpunch/libdvx/widgetEvent.c index 99b13635..ff3f2b9a 100644 --- a/src/libs/kpunch/libdvx/widgetEvent.c +++ b/src/libs/kpunch/libdvx/widgetEvent.c @@ -850,7 +850,6 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y // the paint is needed (e.g. cursor blink, selection change). void widgetOnPaint(WindowT *win, RectT *dirtyArea) { - (void)dirtyArea; WidgetT *root = win->widgetRoot; if (!root) { @@ -909,7 +908,21 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) { wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors, full); // Paint overlay popups (dropdown/combobox) - widgetPaintOverlays(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors); + bool overlay = widgetPaintOverlays(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors); + + // A partial pass narrows the dirty area (when the caller gave one) to + // what it changed, so only that reaches the screen (an open popup may + // reach past its widget's box: then the whole of it) + RectT painted; + + if (dirtyArea && !full && !overlay) { + if (!widgetPaintedArea(&painted) || !rectIntersect(dirtyArea, &painted, &painted)) { + painted.w = 0; + painted.h = 0; + } + + *dirtyArea = painted; + } } diff --git a/src/libs/kpunch/libdvx/widgetIface.c b/src/libs/kpunch/libdvx/widgetIface.c index c695f586..743116c3 100644 --- a/src/libs/kpunch/libdvx/widgetIface.c +++ b/src/libs/kpunch/libdvx/widgetIface.c @@ -294,7 +294,8 @@ static void coerceValue(WgtValueT *v, uint8_t type, char *buf, int32_t bufSize) switch (type) { case WGT_IFACE_STRING: - out.s = wgtValueToText(v, buf, bufSize); + out.s = wgtValueToText(v, buf, bufSize); + out.len = (v->type == WGT_IFACE_STRING) ? v->len : 0; break; case WGT_IFACE_INT: @@ -769,6 +770,14 @@ WgtValueT wgtValueBool(bool b) { } +WgtValueT wgtValueBytes(const char *s, int32_t len) { + WgtValueT v = wgtValueStr(s); + + v.len = (s && len > 0) ? len : 0; + return v; +} + + WgtValueT wgtValueDouble(double f) { WgtValueT v = zeroOf(WGT_IFACE_DOUBLE); @@ -793,6 +802,15 @@ WgtValueT wgtValueInt(int32_t i) { } +int32_t wgtValueLen(const WgtValueT *v) { + if (v->type != WGT_IFACE_STRING) { + return 0; + } + + return v->len > 0 ? v->len : (int32_t)strlen(v->s ? v->s : ""); +} + + WgtValueT wgtValueStr(const char *s) { WgtValueT v = zeroOf(WGT_IFACE_STRING); diff --git a/src/libs/kpunch/libdvx/widgetOps.c b/src/libs/kpunch/libdvx/widgetOps.c index 2d33e06b..88ac1734 100644 --- a/src/libs/kpunch/libdvx/widgetOps.c +++ b/src/libs/kpunch/libdvx/widgetOps.c @@ -41,6 +41,17 @@ static bool sFullRepaint = false; +// What a partial paint pass changed (widgetPaintedArea): the union of the +// boxes of the widgets it repainted, each widened by PAINTED_MARGIN for +// what a widget draws at its very edge. A container that paints its +// children itself counts as its own box (sPaintNesting: its children's +// boxes may be in its own coordinates). +static RectT sPainted; +static bool sPaintedSome = false; +static int32_t sPaintNesting = 0; + +#define PAINTED_MARGIN 2 + // Knuth multiplicative hash constant (2^32 / golden ratio). #define KNUTH_HASH_MUL 2654435761u @@ -50,6 +61,7 @@ static bool sFullRepaint = false; // ============================================================ static void debugContainerBorder(WidgetT *w, DisplayT *d, const BlitOpsT *ops); +static void paintedAdd(const WidgetT *w); static bool pressableHitTest(const WidgetT *w, const WidgetT *root, int32_t x, int32_t y); static WidgetT *wgtFindImpl(WidgetT *w, const char *name); static bool widgetDropFocusWithin(WidgetT *w); @@ -89,6 +101,33 @@ static void debugContainerBorder(WidgetT *w, DisplayT *d, const BlitOpsT *ops) { // Shared hit test for pressable widgets: true when (x,y) falls within // the widget bounds and the drag originated in the same window. +// A partial pass repainted w: its box (widened) joins what the pass +// changed, unless a container that paints its children is painting it. +static void paintedAdd(const WidgetT *w) { + if (sPaintNesting > 0) { + return; + } + + int32_t x0 = w->x - PAINTED_MARGIN; + int32_t y0 = w->y - PAINTED_MARGIN; + int32_t x1 = w->x + w->w + PAINTED_MARGIN; + int32_t y1 = w->y + w->h + PAINTED_MARGIN; + + if (sPaintedSome) { + x0 = DVX_MIN(x0, sPainted.x); + y0 = DVX_MIN(y0, sPainted.y); + x1 = DVX_MAX(x1, sPainted.x + sPainted.w); + y1 = DVX_MAX(y1, sPainted.y + sPainted.h); + } + + sPainted.x = x0; + sPainted.y = y0; + sPainted.w = x1 - x0; + sPainted.h = y1 - y0; + sPaintedSome = true; +} + + static bool pressableHitTest(const WidgetT *w, const WidgetT *root, int32_t x, int32_t y) { return w->window == root->window && x >= w->x && x < w->x + w->w && @@ -383,7 +422,9 @@ void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT return; } - sFullRepaint = fullRepaint; + sFullRepaint = fullRepaint; + sPaintedSome = false; + sPaintNesting = 0; widgetPaintOne(root, d, ops, font, colors); sFullRepaint = false; } @@ -475,13 +516,24 @@ void wgtSetReadOnly(WidgetT *w, bool readOnly) { // Polymorphic text setter. Dispatches to the type-specific setText // via vtable, then does a full invalidation because changing text -// can change the widget's minimum size (triggering relayout). - +// can change the widget's minimum size (triggering relayout). A caption +// (WCLASS_HAS_TEXT) the widget already has changes nothing, so a loop +// that sets a caption on every pass does not relayout and repaint on +// every frame; an editing widget's setText also resets its caret and +// selection, so it always runs. void wgtSetText(WidgetT *w, const char *text) { if (!w) { return; } + if (w->wclass && (w->wclass->flags & WCLASS_HAS_TEXT) && wclsHas(w, WGT_METHOD_GET_TEXT)) { + const char *cur = wclsGetText(w); + + if (cur && strcmp(cur, text ? text : "") == 0) { + return; + } + } + wclsSetText(w, text); wgtInvalidate(w); @@ -566,11 +618,22 @@ static void widgetNotifyChildChanged(WidgetT *w) { #ifdef DVX_HOST_TEST void widgetOpsTestReset(void) { - sFullRepaint = false; + sFullRepaint = false; + sPaintedSome = false; + sPaintNesting = 0; + memset(&sPainted, 0, sizeof(sPainted)); } #endif +// What the last partial paint pass (wgtPaint) changed, in content +// coordinates; false when it repainted nothing. +bool widgetPaintedArea(RectT *out) { + *out = sPainted; + return sPaintedSome; +} + + // Recursive paint walker. For each visible widget: // 1. Call the widget's paint function (if any) via vtable. // 2. If the widget has WCLASS_PAINTS_CHILDREN, stop recursion -- @@ -620,11 +683,17 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo // all descendants must repaint on the fresh background. bool savedFull = sFullRepaint; + if (!savedFull) { + paintedAdd(w); + } + if (w->paintDirty) { sFullRepaint = true; } + sPaintNesting++; wclsPaint(w, d, ops, font, colors); + sPaintNesting--; sFullRepaint = savedFull; w->paintDirty = false; w->childDirty = false; @@ -639,6 +708,10 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo w->paintDirty = false; if (dirty) { + if (!sFullRepaint) { + paintedAdd(w); + } + wclsPaint(w, d, ops, font, colors); } @@ -657,14 +730,15 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo // Paints popup overlays (open dropdowns/comboboxes) on top of // the widget tree. Called AFTER the main paint pass so popups // always render above all other widgets regardless of tree position. +// True when one was painted (it may reach past its widget's box). // // Only one popup can be open at a time (tracked by sOpenPopup). // The tree-root ownership check prevents a popup from one window // being painted into a different window's content buffer. -void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { +bool widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { if (!sOpenPopup) { - return; + return false; } // Verify the popup belongs to this widget tree @@ -675,10 +749,11 @@ void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const } if (check != root) { - return; + return false; } wclsPaintOverlay(sOpenPopup, d, ops, font, colors); + return true; } diff --git a/src/libs/kpunch/listhelp/listHelp.c b/src/libs/kpunch/listhelp/listHelp.c index 6526a1c7..783ec124 100644 --- a/src/libs/kpunch/listhelp/listHelp.c +++ b/src/libs/kpunch/listhelp/listHelp.c @@ -66,6 +66,7 @@ const char *listItemsGet(const ListItemsT *li, int32_t idx); int32_t listItemsMaxWidth(ListItemsT *li, const BitmapFontT *font); bool listItemsRemove(ListItemsT *li, const BitmapFontT *font, int32_t idx); bool listItemsSetExternal(ListItemsT *li, const BitmapFontT *font, const char **items, int32_t count); +bool listItemsSetSpec(ListItemsT *li, const BitmapFontT *font, const char *spec); static void listItemsSync(ListItemsT *li); void listPopupAddItem(WidgetT *w, ListPopupT *p, const char *text); void listPopupCalcMinSize(WidgetT *w, const BitmapFontT *font, ListPopupT *p); @@ -84,6 +85,7 @@ void listPopupOpen(WidgetT *w, ListPopupT *p); void listPopupPaint(const WidgetT *w, const ListPopupT *p, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void listPopupRemoveItem(WidgetT *w, ListPopupT *p, int32_t idx); void listPopupSetItems(WidgetT *w, ListPopupT *p, const char **items, int32_t count); +void listPopupSetItemsSpec(WidgetT *w, ListPopupT *p, const char *spec); int32_t listPopupVisibleCount(int32_t itemCount); void listReorderCancel(WidgetT *w, int32_t *pDragIdx, int32_t *pDropIdx); int32_t listReorderDest(int32_t from, int32_t to, int32_t count); @@ -591,6 +593,32 @@ bool listItemsSetExternal(ListItemsT *li, const BitmapFontT *font, const char ** } +// Replace the items with the pieces of spec between LIST_ITEMS_SEP +// characters (every one separates: "a|" is "a" and an empty item; "" or +// NULL leaves the list empty): BASIC's SetItems "Red|Green|Blue". +bool listItemsSetSpec(ListItemsT *li, const BitmapFontT *font, const char *spec) { + bool widthChanged = listItemsClear(li); + char *buf = (spec && *spec) ? strdup(spec) : NULL; + + for (char *tok = buf; tok; ) { + char *sep = strchr(tok, LIST_ITEMS_SEP); + + if (sep) { + *sep = '\0'; + } + + if (listItemsAdd(li, font, tok)) { + widthChanged = true; + } + + tok = sep ? sep + 1 : NULL; + } + + free(buf); + return widthChanged; +} + + // Repoint the active array at the owned storage after it changed (arrput // may have reallocated it). static void listItemsSync(ListItemsT *li) { @@ -860,6 +888,17 @@ void listPopupSetItems(WidgetT *w, ListPopupT *p, const char **items, int32_t co } +// SetItems from a "Red|Green|Blue" spec (listItemsSetSpec): a new list, +// so nothing is selected, as after listPopupClearItems. +void listPopupSetItemsSpec(WidgetT *w, ListPopupT *p, const char *spec) { + bool widthChanged = listItemsSetSpec(&p->items, wgtFont(w), spec); + + p->selectedIdx = -1; + p->hoverIdx = -1; + listPopupItemsChanged(w, p, widthChanged); +} + + // Rows the popup shows: the item count clamped to [1, DROPDOWN_MAX_VISIBLE]. int32_t listPopupVisibleCount(int32_t itemCount) { return clampInt(itemCount, 1, DROPDOWN_MAX_VISIBLE); diff --git a/src/libs/kpunch/listhelp/listHelp.h b/src/libs/kpunch/listhelp/listHelp.h index 276c88bc..047b8520 100644 --- a/src/libs/kpunch/listhelp/listHelp.h +++ b/src/libs/kpunch/listhelp/listHelp.h @@ -45,6 +45,8 @@ #define POPUP_SCROLLBAR_W SCROLLBAR_WIDTH // Thickness of the drag-reorder insertion line. #define LIST_REORDER_LINE_H 2 +// Separates the items of a SetItems spec ("Red|Green|Blue"). +#define LIST_ITEMS_SEP '|' // ============================================================ // Owned-string item list @@ -78,6 +80,7 @@ const char *listItemsGet(const ListItemsT *li, int32_t idx); int32_t listItemsMaxWidth(ListItemsT *li, const BitmapFontT *font); bool listItemsRemove(ListItemsT *li, const BitmapFontT *font, int32_t idx); bool listItemsSetExternal(ListItemsT *li, const BitmapFontT *font, const char **items, int32_t count); +bool listItemsSetSpec(ListItemsT *li, const BitmapFontT *font, const char *spec); // Rebuild an internally-owned stb_ds string array as a deep copy of an // external source array. The copy is built before the old strings are @@ -181,6 +184,7 @@ void listPopupOpen(WidgetT *w, ListPopupT *p); void listPopupPaint(const WidgetT *w, const ListPopupT *p, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void listPopupRemoveItem(WidgetT *w, ListPopupT *p, int32_t idx); void listPopupSetItems(WidgetT *w, ListPopupT *p, const char **items, int32_t count); +void listPopupSetItemsSpec(WidgetT *w, ListPopupT *p, const char *spec); int32_t listPopupVisibleCount(int32_t itemCount); // ============================================================ diff --git a/src/libs/kpunch/listhelp/listhelp.dhs b/src/libs/kpunch/listhelp/listhelp.dhs index f743231c..c5b082ae 100644 --- a/src/libs/kpunch/listhelp/listhelp.dhs +++ b/src/libs/kpunch/listhelp/listhelp.dhs @@ -85,9 +85,10 @@ const char *listItemsGet(const ListItemsT *li, int32_t idx); int32_t listItemsMaxWidth(ListItemsT *li, const BitmapFontT *font); bool listItemsRemove(ListItemsT *li, const BitmapFontT *font, int32_t idx); bool listItemsSetExternal(ListItemsT *li, const BitmapFontT *font, const char **items, int32_t count); +bool listItemsSetSpec(ListItemsT *li, const BitmapFontT *font, const char *spec); .endcode -listItemsAdd and listItemsRemove first adopt (deep-copy) a caller's array, so they extend or shorten it without modifying it. listItemsSetExternal keeps the previously owned strings until the next adopt, clear, or free, because the new array may point into them, and never reads the caller's previous array (it may be freed already). The mutators measure in font, the widget's own (wgtFont), and return true when the widest item's width changed, i.e. when the widget's minimum width changed and it needs a relayout rather than just a repaint. listItemsGet returns "" for an index out of range. +listItemsAdd and listItemsRemove first adopt (deep-copy) a caller's array, so they extend or shorten it without modifying it. listItemsSetExternal keeps the previously owned strings until the next adopt, clear, or free, because the new array may point into them, and never reads the caller's previous array (it may be freed already). The mutators measure in font, the widget's own (wgtFont), and return true when the widest item's width changed, i.e. when the widget's minimum width changed and it needs a relayout rather than just a repaint. listItemsGet returns "" for an index out of range. listItemsSetSpec replaces the items with the pieces of a "Red|Green|Blue" spec (LIST_ITEMS_SEP; every "|" separates, so "a|" is "a" and an empty item, and "" empties the list): BASIC's SetItems. widgetAdoptOwnedStrings(owned, src, count) builds the deep copy that adoption uses (complete before the old strings are freed, so src may alias them) and widgetFreeOwnedStrings(owned) frees every string of an owned array and empties it. ListView uses both for its row-major cell copy. @@ -155,6 +156,7 @@ typedef struct { listPopupInit(p) No selection, no hover, closed. listPopupAddItem / listPopupRemoveItem Item list edits; the selection and hover stay on the same item (-1 when it is removed). listPopupClearItems / listPopupSetItems Replace the items; an out-of-range selection becomes -1. + listPopupSetItemsSpec(w, p, spec) Replace the items with a "Red|Green|Blue" spec (listItemsSetSpec); nothing is selected. listPopupOpen(w, p) Open with the selection hovered and scrolled into view (keyboard, mouse, and accelerator all use it). listPopupClose(w, p) WGT_METHOD_CLOSE_POPUP body. listPopupCommit(p) Close and make the hovered item the selection; false when nothing valid is hovered. diff --git a/src/libs/kpunch/music/Makefile b/src/libs/kpunch/music/Makefile index a07bac2a..89224d59 100644 --- a/src/libs/kpunch/music/Makefile +++ b/src/libs/kpunch/music/Makefile @@ -22,13 +22,17 @@ # Music Library Makefile for DJGPP cross-compilation # -# music.lib (see music.h): libxmp-lite (thirdparty/libxmp-lite, with -# DVX's patches from its dvx-patches folder, built with warnings off, as -# its own options say below) and our own sources. The files that run at interrupt time (ISR_SRCS) are linked -# last, so musicLock pins one span from their first function to the end -# of the library's data; gcc must not move parts of them elsewhere or -# turn loops into library calls. The OP2 instrument bank is copied -# beside the library. +# music.lib (see music.h) is our own sources but musicXmp.c. The files +# that run at interrupt time (ISR_SRCS) are linked last, so musicLock pins +# one span from their first function to the end of the library's data; +# gcc must not move parts of them elsewhere or turn loops into library +# calls. The OP2 instrument bank is copied beside the library. +# +# players/xmp.drv, the module player music.lib loads when a tracker +# module is opened (musicXmpDrv.c), is musicXmp.c and libxmp-lite +# (thirdparty/libxmp-lite, with DVX's patches from its dvx-patches +# folder, built with warnings off, as its own options say below): only +# what musicXmp.c reaches of libxmp is kept (DXE_GC). include ../../../common.mk @@ -36,10 +40,11 @@ XMP_DIR = thirdparty/libxmp-lite XMP_SRCS = $(wildcard $(XMP_DIR)/src/*.c) $(wildcard $(XMP_DIR)/src/loaders/*.c) XMP_HDRS = $(wildcard $(XMP_DIR)/include/*.h $(XMP_DIR)/src/*.h $(XMP_DIR)/src/loaders/*.h) XMP_OPTS = -DLIBXMP_CORE_PLAYER -DLIBXMP_NO_PROWIZARD -DLIBXMP_NO_DEPACKERS -DBUILDING_STATIC -I$(XMP_DIR)/include -I$(XMP_DIR)/src -XMP_CFLAGS = $(filter-out $(WARNFLAGS) $(DEPFLAGS),$(DOS_CFLAGS)) -w -fno-analyzer $(XMP_OPTS) +XMP_CFLAGS = $(filter-out $(WARNFLAGS) $(DEPFLAGS),$(DOS_CFLAGS)) -w -fno-analyzer $(DXE_GC_CFLAGS) $(XMP_OPTS) ISR_SRCS = musicCents.c musicFm.c musicModTick.c musicMpu.c musicWave.c musicSeq.c -MAIN_SRCS = $(filter-out $(ISR_SRCS),$(wildcard *.c)) +PLAYER_SRC = musicXmp.c +MAIN_SRCS = $(filter-out $(ISR_SRCS) $(PLAYER_SRC),$(wildcard *.c)) ISR_CFLAGS = -fno-reorder-blocks-and-partition -fno-reorder-functions -fno-tree-loop-distribute-patterns CFLAGS = $(DOS_CFLAGS) $(ISR_CFLAGS) -I. -I$(XMP_DIR)/include -I../audio -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty -I../libtasks -I../dvxshell @@ -48,13 +53,17 @@ LIBSDIR = ../../../../bin/libs TARGETDIR = $(LIBSDIR)/kpunch/music TARGET = $(TARGETDIR)/music.lib BANK = $(TARGETDIR)/genmidi.op2 +PLAYERDIR = $(TARGETDIR)/players +PLAYER = $(PLAYERDIR)/xmp.drv -XMP_OBJS = $(patsubst $(XMP_DIR)/%.c,$(OBJDIR)/xmp/%.o,$(XMP_SRCS)) -OBJS = $(XMP_OBJS) $(patsubst %.c,$(OBJDIR)/%.o,$(MAIN_SRCS) $(ISR_SRCS)) +XMP_OBJS = $(patsubst $(XMP_DIR)/%.c,$(OBJDIR)/xmp/%.o,$(XMP_SRCS)) +OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(MAIN_SRCS) $(ISR_SRCS)) +PLAYER_OBJ = $(OBJDIR)/$(PLAYER_SRC:.c=.o) +GC_OBJ = $(OBJDIR)/xmpGc.o .PHONY: all clean -all: $(TARGET) $(TARGETDIR)/music.dep $(BANK) +all: $(TARGET) $(TARGETDIR)/music.dep $(BANK) $(PLAYER) $(TARGETDIR)/music.dep: music.dep | $(TARGETDIR) sed 's/$$/\r/' $< > $@ @@ -66,6 +75,13 @@ $(TARGET): $(OBJS) | $(TARGETDIR) $(DXE3GEN) -o $(TARGETDIR)/music.dxe -U $(OBJS) mv $(TARGETDIR)/music.dxe $@ +$(GC_OBJ): $(PLAYER_OBJ) $(XMP_OBJS) + $(DXE_GC) $@ $(PLAYER_OBJ) -- $(PLAYER_OBJ) $(XMP_OBJS) + +$(PLAYER): $(GC_OBJ) | $(PLAYERDIR) + $(DXE3GEN) -o $(PLAYERDIR)/xmp.dxe -U $(GC_OBJ) + mv $(PLAYERDIR)/xmp.dxe $@ + $(OBJDIR)/xmp/%.o: $(XMP_DIR)/%.c $(XMP_HDRS) $(DVX_MAKEFILES) @mkdir -p $(dir $@) $(CC) $(XMP_CFLAGS) -c -o $@ $< @@ -79,7 +95,10 @@ $(OBJDIR): $(TARGETDIR): mkdir -p $(TARGETDIR) --include $(patsubst %.c,$(OBJDIR)/%.d,$(MAIN_SRCS) $(ISR_SRCS)) +$(PLAYERDIR): + mkdir -p $(PLAYERDIR) + +-include $(patsubst %.c,$(OBJDIR)/%.d,$(MAIN_SRCS) $(ISR_SRCS) $(PLAYER_SRC)) clean: rm -rf $(OBJDIR) $(TARGETDIR) diff --git a/src/libs/kpunch/music/README.md b/src/libs/kpunch/music/README.md new file mode 100644 index 00000000..d256c5c3 --- /dev/null +++ b/src/libs/kpunch/music/README.md @@ -0,0 +1,49 @@ +# music -- Music + +Music for DVX programs: tracker modules (MOD, S3M, XM, IT, through +libxmp-lite), WAV and VOC files streamed as they play, Standard MIDI +Files on the best synthesizer there is (an external MIDI synthesizer, +the card's wavetable or FM, or DVX's own), and PLAY strings. BASIC +reaches it through PLAY and src/include/basic/music.bas. + +## Documentation + +Full reference is in `music.dhs` (this directory), compiled into the DVX +System Reference. Open via the Help Viewer app, or read +`docs/dvx_system_reference.html` after `make`. + +## Source Files + +- `music.h`, `musicInt.h` -- public and internal API +- `music.c` -- musics, where they play, programs, the public functions +- `musicShell.c` -- music.lib's hooks into the shell +- `musicXmpDrv.c` -- tracker modules: loads the module player when a + module is opened, and unloads it once none has been for a minute +- `musicXmp.c` -- the module player, `players/xmp.drv`: tracker modules + (MOD, S3M, XM, IT) through libxmp-lite, not part of music.lib +- `musicModHw.c`, `musicModTick.c` -- tracker modules on a wavetable + synthesizer's voices, played by the music timer +- `musicSound.c` -- WAV and VOC files as music +- `musicSmf.c`, `musicSeq.c` -- Standard MIDI Files; the sequencer, its + event ring, and the music timer +- `musicMpu.c` -- MIDI out of the MPU-401 port +- `musicFm.c`, `musicSynth.c`, `musicOp2.c` -- General MIDI on FM + voices, the software FM synthesizer, and OP2 (GENMIDI) banks +- `musicWave.c`, `musicWaveLoad.c`, `musicWaveSoft.c` -- General MIDI on + wavetable voices (SoundFont), its samples in the synthesizer's memory, + and the same in software +- `musicSf2.c`, `musicPat.c`, `musicFound.c` -- SoundFont 2 banks, + Gravis UltraSound patch sets, and the banks a card's own software + installed +- `musicMml.c`, `musicMmlPlay.c` -- PLAY's music macro language and its + queues +- `musicMidiRec.c` -- recording MIDI into a Standard MIDI File +- `musicCents.c` -- cents and powers of two in fixed point, loudness as + attenuation +- `musicBas.c` -- BASIC entry points (music.bas) +- `thirdparty/libxmp-lite` -- module playback (MIT; local patches + tracked beside it) + +## Build + +`make -C src/libs/kpunch/music` (invoked by the top-level `make`). diff --git a/src/libs/kpunch/music/music.c b/src/libs/kpunch/music/music.c index a6a921b1..6930de2f 100644 --- a/src/libs/kpunch/music/music.c +++ b/src/libs/kpunch/music/music.c @@ -51,7 +51,9 @@ // from song to song (a song loads only what it lacks; a module's samples // that do not fit beside them put them out); the library's context, // which owns that memory, stays open FONT_KEEP_US after the last music -// for the next one. +// for the next one. The SoundFont's bank itself (a GUS patch set's +// files, a SoundFont's headers: seconds to read) is kept for the session: +// only its samples go with the context. // // The state a player is in lives in its IsrPlayerT for every kind of // music, so there is one place to look: the timer, the fill functions @@ -64,6 +66,7 @@ #include "musicInt.h" #include "stb_ds.h" +#include "taskSwch.h" #include #include @@ -80,13 +83,13 @@ DVX_ABI_STAMP(music); #define SW_VOICES SYNTH_VOICES_MAX #define STREAM_BITS 16 #define STREAM_CHANNELS 2 -#define US_PER_SEC 1000000ULL -#define US_PER_MS 1000 #define MS_PER_SEC 1000 #define GAIN_SHIFT 8 #define TIMER_NONE (-1) #define ERROR_TEXT_UNKNOWN "Unknown error" #define FONT_KEEP_US (MUSIC_FONT_KEEP_SEC * US_PER_SEC) +#define LOAD_HEAD_BYTES 4096 // read to tell a MIDI file by its start +#define LOAD_CHUNK_BYTES (64 * 1024) // a music file is read this much at a time #define GM_BANK 0 // General MIDI's instruments; live MIDI starts every channel ... #define GM_PROGRAM_FIRST 0 // ... on the first (and channel 10 on the standard kit) @@ -99,12 +102,20 @@ typedef struct { char name[DVX_MAX_PATH]; // as DVX.INI's soundFont names it } WaveFontT; -// The SoundFont software synthesizer's, one per music: its own copy of -// the SoundFont's headers, the song's samples in memory, where each is +// A SoundFont's bank parsed for the software synthesizer, shared by the +// musics that use it (softBankHold); the last one read is kept (sSoftBank) typedef struct { - WaveBankT bank; + WaveBankT bank; + char name[DVX_MAX_PATH]; + int32_t refs; +} SoftBankT; + +// The SoundFont software synthesizer's, one per music: the SoundFont's +// headers (shared), the song's samples in memory, where each is +typedef struct { + SoftBankT *shared; WaveSoftT soft; - WaveLoadedT *loaded; // per sample of bank + WaveLoadedT *loaded; // per sample of the bank } SoftFontT; struct MusicT { @@ -138,6 +149,7 @@ struct MusicT { bool atEnd; // a module played to its end: its next play starts over bool draining; // rendered to its end, not yet heard to it bool fmChip; // hwSynth plays (the music, or the wavetable synthesizer's overflow) + bool waveLoading; // live programs' samples still loading (waveLiveStep, a musicPump each) char soundFont[DVX_MAX_PATH]; // musicSetBank's SoundFont; "": DVX.INI's }; @@ -161,6 +173,7 @@ static MusicBankT *sBank = NULL; // DVX.INI's choice as last loaded (a re static MusicT *sMpuUser = NULL; static int32_t sTimerId = TIMER_NONE; static WaveFontT *sWave = NULL; // the SoundFont loaded for the wavetable synthesizer +static SoftBankT *sSoftBank = NULL; // the SoundFont the software synthesizer read last (softBankHold) static MusicT *sWaveUser = NULL; // the music on it static uint64_t sQuietSince = 0; // when the last music went (sCtx kept for sWave) static char sFontFailed[DVX_MAX_PATH]; // the SoundFont last found missing or damaged (said once) @@ -196,6 +209,7 @@ void musicFree(MusicT *music); void musicGetInfo(const MusicT *music, MusicInfoT *info); uint32_t musicGetPosition(const MusicT *music); int32_t musicGetVolume(const MusicT *music); +void musicGiveWay(uint64_t *last); void musicIdle(void *unused); bool musicIsPlaying(const MusicT *music); int32_t musicLoad(const char *path, MusicT **out); @@ -224,9 +238,12 @@ static int32_t openSoftware(MusicT *m); static int32_t openStream(MusicT *m, uint32_t bufferMs, AudioFillFnT fill); static void outputClose(MusicT *m); static int32_t outputOpen(MusicT *m); +static char *readGivingWay(const char *path, int32_t *outLen); static void rewindToStart(MusicT *m); static int32_t sendMidi(MusicT *m, int32_t channel, uint8_t type, int32_t data1, int32_t data2); static void silence(MusicT *m); +static SoftBankT *softBankHold(const char *want, int32_t *err); +static void softBankRelease(SoftBankT *sb); static int32_t softLoad(MusicT *m, SoftFontT *sf, const char *want); static int32_t softOpen(MusicT *m, const char *font); static uint32_t soundFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData); @@ -238,8 +255,10 @@ static void timerUpdate(void); static bool waveBankLock(const WaveBankT *bank, bool lock); static bool waveClaim(AudioWtInfoT *info); static void waveDrop(MusicT *m); +static void waveFontEmpty(void); static void waveFontFree(WaveFontT *font); static int32_t waveFontUse(const char *name); +static void waveLiveStep(MusicT *m); static void waveNeedsSong(const MusicT *m, const WaveBankT *bank, uint8_t *need); static int32_t waveOpen(MusicT *m, const char *font, MidiSinkT *overflow); static void waveProgram(MusicT *m, int32_t channel, int32_t program); @@ -341,11 +360,12 @@ static MusicBankT *bankShared(const char *name, bool current, int32_t *err) { } -// The library's context closed, and the SoundFont with it (its samples -// are in the context's memory). +// The library's context closed, and the SoundFont's samples with it +// (they are in the context's memory); its bank, read (a GUS patch set's +// files, a SoundFont's headers) at the cost of seconds, stays for the next +// song. static void ctxClose(void) { - waveFontFree(sWave); - sWave = NULL; + waveFontEmpty(); audioClose(sCtx); sCtx = NULL; } @@ -368,7 +388,7 @@ static int32_t ctxEnsure(void) { // instrument bank only the MPU-401 port can play it. The banks are the // ones musicSetBank chose, else DVX.INI's choices as they are now. static int32_t decideMidi(MusicT *m) { - PrefsHandleT *prefs = prefsLoad(platformDvxIniPath()); + PrefsHandleT *prefs = prefsDvxIniShared(); const char *want = audioMidiChoices[audioSettingMidi(prefs)].iniName; int32_t mode = mpuModeChoices[prefsGetChoice(prefs, AUDIO_INI_SECTION, MUSIC_INI_MPU_MODE, mpuModeChoices, MPU_MODE_COUNT, mpuModeChoices[MPU_MODE_GM].iniName)].value; bool mpu = strcasecmp(want, AUDIO_MIDI_MPU401) == 0; @@ -383,7 +403,6 @@ static int32_t decideMidi(MusicT *m) { snprintf(bank, sizeof(bank), "%s", prefsGetString(prefs, AUDIO_INI_SECTION, MUSIC_INI_BANK, "")); snprintf(font, sizeof(font), "%s", m->soundFont[0] ? m->soundFont : prefsGetString(prefs, AUDIO_INI_SECTION, MUSIC_INI_SOUND_FONT, "")); - prefsClose(prefs); if (mpu && !sMpuUser && isrSlotFree() && audioMidiAcquire(sCtx) == AUDIO_OK) { sMpuUser = m; @@ -631,12 +650,10 @@ static uint32_t moduleFill(AudioStreamT *stream, void *buf, uint32_t frames, voi // (moduleWave) and DVX.INI [audio] modules does not say software, else on // a stream mixed in software. static int32_t moduleOpen(MusicT *m) { - PrefsHandleT *prefs = prefsLoad(platformDvxIniPath()); + PrefsHandleT *prefs = prefsDvxIniShared(); const char *want = audioModulesChoices[audioSettingModules(prefs)].iniName; bool soft = strcasecmp(want, AUDIO_MODULES_SOFTWARE) == 0; - prefsClose(prefs); - if (!soft && moduleWave(m) == MUSIC_OK) { return MUSIC_OK; } @@ -660,9 +677,8 @@ static int32_t moduleWave(MusicT *m) { int32_t rc = modHwOpen(sCtx, m->xmp, gainOf(m), &m->isr.mod); if (rc == MODHW_ERR_ROOM && sWave) { - waveFontFree(sWave); - sWave = NULL; - rc = modHwOpen(sCtx, m->xmp, gainOf(m), &m->isr.mod); + waveFontEmpty(); + rc = modHwOpen(sCtx, m->xmp, gainOf(m), &m->isr.mod); } if (rc != MUSIC_OK) { @@ -852,11 +868,23 @@ int32_t musicGetVolume(const MusicT *music) { } -// After a while with no music the SoundFont's samples are let go. +// tsYield: on a program's own task DVX's frames run (none of the +// program's code); on the shell's, the other tasks. +void musicGiveWay(uint64_t *last) { + if (platformMicros() - *last >= MUSIC_GIVE_WAY_US) { + tsYield(); + *last = platformMicros(); + } +} + + +// After a while with no music the SoundFont's samples are let go, and +// the module player after a while unused. void musicIdle(void *unused) { (void)unused; musicMmlIdle(); musicPump(); + xmpIdle(); if (!sMusics && sCtx && platformMicros() - sQuietSince >= FONT_KEEP_US) { ctxClose(); @@ -869,7 +897,9 @@ bool musicIsPlaying(const MusicT *music) { } -// A WAV is read as it plays (soundOpenFile); anything else is read whole. +// A WAV is read as it plays (soundOpenFile); a MIDI file or a module is +// read whole, giving way as it is, once its head says it is one (a file +// of another kind is refused without being read). int32_t musicLoad(const char *path, MusicT **out) { SoundSrcT *src = NULL; int32_t len = 0; @@ -891,7 +921,19 @@ int32_t musicLoad(const char *path, MusicT **out) { return rc; } - char *data = platformReadFile(path, &len); + uint8_t head[LOAD_HEAD_BYTES]; + FILE *fp = fopen(path, "rb"); + size_t got = fp ? fread(head, 1, sizeof(head), fp) : 0; + + if (fp) { + fclose(fp); + } + + if (!midiIsFile(head, (uint32_t)got) && !xmpIsModule(path)) { + return fp ? MUSIC_ERR_FORMAT : MUSIC_ERR_OPEN; + } + + char *data = readGivingWay(path, &len); if (!data) { return MUSIC_ERR_OPEN; @@ -960,7 +1002,9 @@ int32_t musicLoadMem(const void *data, uint32_t size, MusicT **out) { return rc == MUSIC_OK ? soundMusic(src, out) : rc; } - bool midi = midiIsFile(bytes, size); + bool midi = midiIsFile(bytes, size); + XmpPlayerT *xmp = NULL; + AudioInfoT info; rc = ctxEnsure(); @@ -968,9 +1012,21 @@ int32_t musicLoadMem(const void *data, uint32_t size, MusicT **out) { return rc; } + // A module loads (giving way) before its music is made: no other + // task meets a music that is half there + if (!midi) { + audioGetInfo(&info); + rc = xmpOpen(bytes, size, info.rate, STREAM_CHANNELS, &xmp); + + if (rc != MUSIC_OK) { + return rc; + } + } + MusicT *m = musicNew(midi ? MUSIC_KIND_MIDI : MUSIC_KIND_MODULE); if (!m) { + xmpClose(xmp); rc = MUSIC_ERR_NO_MEMORY; } else if (midi) { rc = midiParse(bytes, size, &m->file); @@ -984,11 +1040,8 @@ int32_t musicLoadMem(const void *data, uint32_t size, MusicT **out) { m->isr.seq.count = m->file.count; m->isr.seq.length = m->file.length; } else { - AudioInfoT info; - - audioGetInfo(&info); m->rate = info.rate; - rc = xmpOpen(bytes, size, m->rate, STREAM_CHANNELS, &m->xmp); + m->xmp = xmp; } if (m && rc != MUSIC_OK) { @@ -1178,6 +1231,10 @@ void musicPump(void) { if (m->isr.mod && m->isr.state == PLAY_PLAYING) { modHwFill(m->isr.mod, m->xmp, m->loop); } + + if (m->waveLoading) { + waveLiveStep(m); + } } while (again) { @@ -1321,8 +1378,8 @@ int32_t musicSetVolume(MusicT *music, int32_t volume) { } -// The library is being unloaded: free every music, the banks, the -// context. +// The library is being unloaded: free every music, the module player, +// the banks, the context. void musicShutdown(void) { musicMmlShutdown(); @@ -1330,12 +1387,22 @@ void musicShutdown(void) { musicFree(sMusics); } + xmpUnload(); + if (sCtx) { ctxClose(); } + waveFontFree(sWave); bankRelease(sBank); + sWave = NULL; sBank = NULL; + + if (sSoftBank) { + waveBankFree(&sSoftBank->bank); + free(sSoftBank); + sSoftBank = NULL; + } } @@ -1497,6 +1564,37 @@ static int32_t outputOpen(MusicT *m) { } +// A file read whole, LOAD_CHUNK_BYTES at a time, giving way between +// (musicGiveWay); NULL when it cannot be read. Free with free(). +static char *readGivingWay(const char *path, int32_t *outLen) { + FILE *fp = fopen(path, "rb"); + long size = fp && fseek(fp, 0, SEEK_END) == 0 ? ftell(fp) : -1; + char *data = size > 0 && size < INT32_MAX && fseek(fp, 0, SEEK_SET) == 0 ? (char *)malloc((size_t)size) : NULL; + long done = 0; + uint64_t last = platformMicros(); + + while (data && done < size) { + size_t want = (size_t)DVX_MIN((long)LOAD_CHUNK_BYTES, size - done); + + if (fread(data + done, 1, want, fp) != want) { + free(data); + data = NULL; + break; + } + + done += (long)want; + musicGiveWay(&last); + } + + if (fp) { + fclose(fp); + } + + *outLen = (int32_t)done; + return data; +} + + // A module or a sound back to its start (a module's frames queued for the // synthesizer dropped). static void rewindToStart(MusicT *m) { @@ -1556,28 +1654,82 @@ static void silence(MusicT *m) { } +// The SoundFont want names, read once: the bank kept (sSoftBank) is +// shared while it is the one wanted; another is read in its place (a kept +// one no music holds goes; one still held goes with its last music). +// NULL, *err set, when it cannot be read. +static SoftBankT *softBankHold(const char *want, int32_t *err) { + if (sSoftBank && strcasecmp(sSoftBank->name, want) == 0) { + sSoftBank->refs++; + *err = MUSIC_OK; + return sSoftBank; + } + + SoftBankT *sb = (SoftBankT *)calloc(1, sizeof(SoftBankT)); + + *err = sb ? fontLoad(want, &sb->bank) : MUSIC_ERR_NO_MEMORY; + + if (*err != MUSIC_OK) { + if (sb) { + waveBankFree(&sb->bank); + } + + free(sb); + return NULL; + } + + snprintf(sb->name, sizeof(sb->name), "%s", want); + sb->refs = 1; + + if (sSoftBank && sSoftBank->refs == 0) { + waveBankFree(&sSoftBank->bank); + free(sSoftBank); + } + + sSoftBank = sb; + return sb; +} + + +// A music's hold given up; a bank no longer kept goes with its last. +static void softBankRelease(SoftBankT *sb) { + if (!sb) { + return; + } + + sb->refs--; + + if (sb->refs == 0 && sb != sSoftBank) { + waveBankFree(&sb->bank); + free(sb); + } +} + + // softOpen's SoftFontT sf filled: the SoundFont want names, the music's // renderer, the song's samples, the backend. static int32_t softLoad(MusicT *m, SoftFontT *sf, const char *want) { - int32_t rc = fontLoad(want, &sf->bank); + int32_t rc; WaveBackendT be; + sf->shared = softBankHold(want, &rc); + if (rc == MUSIC_OK) { - rc = waveSynthNew(m, sf->bank.sampleCount); + rc = waveSynthNew(m, sf->shared->bank.sampleCount); } if (rc == MUSIC_OK) { - sf->loaded = (WaveLoadedT *)calloc(sf->bank.sampleCount ? sf->bank.sampleCount : 1, sizeof(WaveLoadedT)); + sf->loaded = (WaveLoadedT *)calloc(sf->shared->bank.sampleCount ? sf->shared->bank.sampleCount : 1, sizeof(WaveLoadedT)); rc = sf->loaded ? MUSIC_OK : MUSIC_ERR_NO_MEMORY; } if (rc == MUSIC_OK) { - for (uint32_t i = 0; i < sf->bank.sampleCount; i++) { + for (uint32_t i = 0; i < sf->shared->bank.sampleCount; i++) { sf->loaded[i].addr = WAVE_NOT_LOADED; } - waveNeedsSong(m, &sf->bank, m->waveNeed); - rc = waveSoftLoad(&sf->soft, &sf->bank, sf->loaded, m->waveNeed); + waveNeedsSong(m, &sf->shared->bank, m->waveNeed); + rc = waveSoftLoad(&sf->soft, &sf->shared->bank, sf->loaded, m->waveNeed); } if (rc != MUSIC_OK) { @@ -1585,7 +1737,7 @@ static int32_t softLoad(MusicT *m, SoftFontT *sf, const char *want) { } waveSoftBackend(&sf->soft, m->rate, &be); - waveSynthInit(m->wave, &be, &sf->bank, sf->loaded, &m->swSynth->sink, MUSIC_VOLUME_MAX); + waveSynthInit(m->wave, &be, &sf->shared->bank, sf->loaded, &m->swSynth->sink, MUSIC_VOLUME_MAX); return MUSIC_OK; } @@ -1796,16 +1948,34 @@ static void waveDrop(MusicT *m) { if (m->softFont) { waveSoftFree(&m->softFont->soft); - waveBankFree(&m->softFont->bank); + softBankRelease(m->softFont->shared); free(m->softFont->loaded); } free(m->softFont); free(m->wave); free(m->waveNeed); - m->softFont = NULL; - m->wave = NULL; - m->waveNeed = NULL; + m->softFont = NULL; + m->wave = NULL; + m->waveNeed = NULL; + m->waveLoading = false; +} + + +// The SoundFont's samples out of the synthesizer's memory; its bank stays +// (sWave: the next song that uses it loads only its samples), unless an +// empty cache cannot be made for it. +static void waveFontEmpty(void) { + if (!sWave) { + return; + } + + waveCacheFree(sCtx, &sWave->cache); + + if (waveCacheInit(&sWave->cache, &sWave->bank) != MUSIC_OK) { + waveFontFree(sWave); + sWave = NULL; + } } @@ -1868,6 +2038,23 @@ static int32_t waveFontUse(const char *name) { } +// A step of a live program's samples loading on the wavetable +// synthesizer (waveProgram), one sample a musicPump: a song's worth taken +// at once would hold DVX up for half a second. +static void waveLiveStep(MusicT *m) { + AudioWtInfoT info; + bool more = false; + + audioWtGetInfo(&info); + + if (waveLoadStep(sCtx, &sWave->bank, &sWave->cache, m->waveNeed, info.flags, &more) != MUSIC_OK) { + dvxLog("Music: a sample of a program chosen does not fit in the synthesizer's memory (or could not be read); its notes play on the FM synthesizer"); + } + + m->waveLoading = more; +} + + // The samples the music needs from bank: the song's, or for live MIDI // the first program and the standard kit (the rest as they are chosen). static void waveNeedsSong(const MusicT *m, const WaveBankT *bank, uint8_t *need) { @@ -1927,38 +2114,25 @@ static int32_t waveOpen(MusicT *m, const char *font, MidiSinkT *overflow) { // A program chosen live on a wavetable synthesizer: its samples loaded -// when some are not there yet (on the chip the notes sounding are -// silenced first, as samples may move to make room). +// when some are not there yet, a sample a musicPump (waveLiveStep; its +// notes play on FM until they are). In software they join the others in +// memory at once. static void waveProgram(MusicT *m, int32_t channel, int32_t program) { SoftFontT *sf = m->softFont; - const WaveBankT *bank = sf ? &sf->bank : &sWave->bank; + const WaveBankT *bank = sf ? &sf->shared->bank : &sWave->bank; uint16_t bankNum = channel == MIDI_PERCUSSION ? SF_DRUM_BANK : m->wave->channels[channel].bankMsb; - AudioWtInfoT info; - uint32_t missing = 0; if (waveNeedsPreset(bank, bankNum, (uint8_t)program, m->waveNeed) == 0) { return; } - // In software the samples join the others in memory - if (sf) { - if (waveSoftLoad(&sf->soft, bank, sf->loaded, m->waveNeed) != MUSIC_OK) { - dvxLog("Music: program %" PRId32 "'s samples could not be read; its notes play on the FM synthesizer", program); - } - + if (!sf) { + m->waveLoading = true; return; } - // Its notes off before samples move in the synthesizer's memory (the - // timer may be sending) - uint32_t flags = platformIrqSave(); - - m->wave->sink.silence(&m->wave->sink); - platformIrqRestore(flags); - audioWtGetInfo(&info); - - if (waveLoadNeeded(sCtx, &sWave->bank, &sWave->cache, m->waveNeed, info.flags, &missing) != MUSIC_OK || missing > 0) { - dvxLog("Music: not every sample of program %" PRId32 " fits in the synthesizer's memory; those notes play on the FM synthesizer", program); + if (waveSoftLoad(&sf->soft, bank, sf->loaded, m->waveNeed) != MUSIC_OK) { + dvxLog("Music: program %" PRId32 "'s samples could not be read; its notes play on the FM synthesizer", program); } } diff --git a/src/libs/kpunch/music/music.dhs b/src/libs/kpunch/music/music.dhs index b6a5a279..ce8c6f1d 100644 --- a/src/libs/kpunch/music/music.dhs +++ b/src/libs/kpunch/music/music.dhs @@ -85,7 +85,7 @@ Music for DVX programs on whatever sound hardware the computer has: tracker modules (MOD, S3M, XM, IT), Standard MIDI Files (type 0 and 1), WAV and VOC sound files, and live MIDI a program sends itself. Every music belongs to the program that loaded it and is stopped and freed when the program ends, however it ends; any number of programs may play music at the same time. -Header: music/music.h. Loaded as: bin/libs/kpunch/music/music.lib (needs audio.lib); the General MIDI instrument bank for FM is bin/libs/kpunch/music/genmidi.op2, and the General MIDI SoundFont for wavetable music is an add-on (SHARED\BANKS\KPUNCH\DVXGM.SF2). +Header: music/music.h. Loaded as: bin/libs/kpunch/music/music.lib (needs audio.lib); the tracker module player (libxmp-lite) is bin/libs/kpunch/music/players/xmp.drv, which music.lib loads when a module is opened and lets go a minute after the last one closes; the General MIDI instrument bank for FM is bin/libs/kpunch/music/genmidi.op2, and the General MIDI SoundFont for wavetable music is an add-on (SHARED\BANKS\KPUNCH\DVXGM.SF2). .h2 Playing a File @@ -141,7 +141,7 @@ When there is no FM chip (or it is busy), MIDI plays on DVX's own software FM sy A wavetable synthesizer (audio.lib's gus and guswt drivers for a Gravis UltraSound, awe for the EMU8000 of a Sound Blaster AWE32 or AWE64) plays recorded instruments from its own memory. Its instruments come from a SoundFont, DVX.INI [audio] soundFont (MUSIC_INI_SOUND_FONT), named as midiBank names an OP2 bank; not set, the standard one (MUSIC_SOUND_FONT_DEFAULT, KPUNCH\DVXGM.SF2: DVX's General MIDI SoundFont add-on, made from FluidR3_GM by mksf, about 3.4 MB). SoundFont 2 files are read in full; SoundFont 1 (.SBK, the AWE32's own) as far as it is known (it has no public specification). soundFont may also name a Gravis UltraSound's ULTRASND.INI by its path, which plays that folder's .PAT patches as instruments; and where the standard SoundFont is not installed, the patches in the folder ULTRADIR names play (musicPatchSetPath), else the AWE's own bank for its sample ROM (musicRomBankPath: SFBANK\SYNTHGM.SF2 or SYNTHGM.SBK in the folder SOUND names, as Creative's drivers installed it). Control Panel's Sounds page lists all of these. -A song loads only the samples its notes play (the bank selects, programs, and notes are looked at first), each with the frames a chip's interpolation reads after it, into memory the synthesizer gives every program a share of; what a song loaded stays for the next while it fits, and room is made by giving up the samples used longest ago. When the samples still do not fit they are all loaded again smaller: 8-bit where the chip plays it, then at half the rate, then a quarter; at the least, the ones that do not fit are left out. Notes the bank lacks, or whose samples were left out, play on the FM chip or the software synthesizer, as do the notes beyond the synthesizer's voices. One music plays on the wavetable synthesizer at a time (another meanwhile plays on FM); its SoundFont and samples are kept 60 seconds after the last music (MUSIC_FONT_KEEP_SEC). Live MIDI loads a program's samples as it is chosen. A SoundFont made for an AWE's General MIDI ROM (ROM samples) plays from the card's ROM with nothing loaded; on any other synthesizer its notes go to FM. +A song loads only the samples its notes play (the bank selects, programs, and notes are looked at first), each with the frames a chip's interpolation reads after it, into memory the synthesizer gives every program a share of; what a song loaded stays for the next while it fits, and room is made by giving up the samples used longest ago. When the samples still do not fit (a size they cannot fit at, even in the whole of the synthesizer's memory, is not tried) they are all loaded again smaller: 8-bit where the chip plays it, then at half the rate, then a quarter; at the least, the ones that do not fit are left out. Notes the bank lacks, or whose samples were left out, play on the FM chip or the software synthesizer, as do the notes beyond the synthesizer's voices. One music plays on the wavetable synthesizer at a time (another meanwhile plays on FM); its samples are kept 60 seconds after the last music (MUSIC_FONT_KEEP_SEC), and the SoundFont itself (its headers, a GUS patch set's files) for the whole session, so a later song does not read them again. Live MIDI loads a program's samples once it is chosen, one at a time in the idle time after (a whole instrument at once would hold DVX up), at the best quality that fits beside the samples already loaded; until they are in, its notes play on the FM chip. A SoundFont made for an AWE's General MIDI ROM (ROM samples) plays from the card's ROM with nothing loaded; on any other synthesizer its notes go to FM. The AWE runs the SoundFont's envelopes, LFOs, filter, and reverb and chorus sends itself; on the GUS the library runs the envelopes and LFOs (its volume and pitch changed as the note goes on), without a filter or effects. @@ -199,7 +199,7 @@ A MIDI recording keeps channel messages with their times (musicMidiRecordAdd(rec MOD (ProTracker and compatibles), S3M, XM, and IT through libxmp-lite 4.7.3 (thirdparty/libxmp-lite with DVX's patches, listed in its DVX-VERSION.md; formats only, no depackers or ProWizard). Stereo separation is 50 percent (softer than the Amiga's hard left and right). The whole module is held in memory. libxmp works out pitches with floating point, so it plays modules in the shell's idle time, never at interrupt time; on a 486SX (no maths coprocessor) that goes through floating point emulation and is slow. On a stream libxmp mixes every channel in software (linear interpolation, three multiplies a channel a frame). -On a wavetable synthesizer (musicModHw.c) libxmp mixes nothing: DVX's hardware voice patch has it report each of its voices as every tick starts (the sample, where it is, its rate, volume, pan, and the loop it is in), and only move them on. Those ticks are made up to 400 ms ahead in the idle time, as the synthesizer's notes, and the music timer plays them on time (500 times a second: a note starts within 2 ms of its time). A voice starts on the chip where libxmp starts or moves it (a note, a sample offset, a retrigger, another sample or loop); otherwise its pitch, volume, and pan follow libxmp's each tick, and the chip's own interpolation plays it. The samples are stored as they are (an 8-bit one 8-bit where the chip plays those, as a GUS does), else all 8-bit. The chips loop only forward, so a ping-pong loop is stored unrolled: its way back after its end, each frame where libxmp reads it (an Impulse Tracker module's way back a frame shorter, as Impulse Tracker plays it). What the chips cannot do plays near enough: a ping-pong sustain loop goes forward, a sample played backwards with no loop goes forward, and Impulse Tracker's filters are not heard; a voice the chip has none free for (a program holds the rest) is not heard. A voice at full volume plays 8 dB down, with the chip's level (AudioWtInfoT) taken off too, so a module is about as loud as libxmp mixes it. A pause stops the voices, and the play goes on from the tick that was playing; musicGetPosition is the tick being heard. +On a wavetable synthesizer (musicModHw.c) libxmp mixes nothing: DVX's hardware voice patch has it report each of its voices as every tick starts (the sample, where it is, its rate, volume, pan, and the loop it is in), and only move them on. Those ticks are made up to 400 ms ahead in the idle time, as the synthesizer's notes, and the music timer plays them on time (500 times a second: a note starts within 2 ms of its time). A voice starts on the chip where libxmp starts or moves it (a note, a sample offset, a retrigger, another sample or loop); otherwise its pitch, volume, and pan follow libxmp's each tick, and the chip's own interpolation plays it. The samples are stored as they are (an 8-bit one 8-bit where the chip plays those, as a GUS does), else all 8-bit; a way of storing them that cannot fit in the synthesizer's free memory is not tried (nothing is uploaded for nothing), and a module that fits in neither plays mixed in software. A big module loads giving way to DVX's other tasks between its samples (a DVX patch to libxmp, 0002-load-hook), and a file of no music kind is refused by its first bytes, without being read whole. A WAV or VOC file plays as it is read. The chips loop only forward, so a ping-pong loop is stored unrolled: its way back after its end, each frame where libxmp reads it (an Impulse Tracker module's way back a frame shorter, as Impulse Tracker plays it). What the chips cannot do plays near enough: a ping-pong sustain loop goes forward, a sample played backwards with no loop goes forward, and Impulse Tracker's filters are not heard; a voice the chip has none free for (a program holds the rest) is not heard. A voice at full volume plays 8 dB down, with the chip's level (AudioWtInfoT) taken off too, so a module is about as loud as libxmp mixes it. A pause stops the voices, and the play goes on from the tick that was playing; musicGetPosition is the tick being heard. Measured on an emulated 486DX2-66 at 22,050 Hz, a 4-channel MOD with all four channels sounding takes about 49 percent of the CPU mixed in software and about 14 percent on a Gravis UltraSound's voices, most of that libxmp's own work each tick (about 1.3 ms of the 20). @@ -212,9 +212,9 @@ musicMmlOpen(&tune); musicMmlPlay(tune, "MB T140 O3 L8 CDEC CDEC EFG4 EFG4"); .endcode -A macro queue plays tunes written in QBasic's music macro language (the commands are listed in music.h and in the BASIC language reference: notes A-G with sharps and flats, N, O, > and <, L, P, dots, T, MN / ML / MS, MF / MB). musicMmlPlay parses the whole string first; a mistake anywhere returns MUSIC_ERR_MACRO and queues nothing. The settings last from one string to the next. musicMmlTone queues a square-wave tone of milliHz thousandths of a hertz (0: silence) for us microseconds (QBasic's SOUND). musicMmlQueued counts the notes not yet played, musicMmlStop silences the queue, musicMmlBusy says whether a caller that follows QBasic should still wait (foreground mode: until everything has played; background: while more than MUSIC_MML_BACKGROUND_MAX notes wait) and moves the queue along. A queue belongs to the program that opened it and is freed when that program ends. +A macro queue plays tunes written in QBasic's music macro language (the commands are listed in music.h and in the BASIC language reference: notes A-G with sharps and flats, N, O, > and <, L, P, dots, T, MN / ML / MS, MF / MB, and QB64's V (volume) and @ (waveform: 1 square, 3 triangle, 5 noise)). musicMmlPlay parses the whole string first; a mistake anywhere returns MUSIC_ERR_MACRO and queues nothing. The settings last from one string to the next. musicMmlTone queues a square-wave tone of milliHz thousandths of a hertz (0: silence) for us microseconds (QBasic's SOUND). musicMmlQueued counts the notes not yet played, musicMmlStop silences the queue, musicMmlBusy says whether a caller that follows QBasic should still wait (foreground mode: until everything has played; background: while more than MUSIC_MML_BACKGROUND_MAX notes wait) and moves the queue along. A queue belongs to the program that opened it and is freed when that program ends. -Timing is QBasic's: a whole note is four beats of 60 / T seconds, each dot adds half again, and a note sounds for 7/8 (MN), all (ML), or 3/4 (MS) of its length. Octave 3 starts at middle C; octave 3's A is 440 Hz. The notes play as square waves on an audio.lib note queue, each to the exact sample (on the PC speaker as tones). DVX's own I command (I1 to I128) plays the notes that follow on that General MIDI instrument: each run of such notes becomes a small MIDI file that plays like any MIDI music (on the synthesizer DVX.INI [audio] midi chooses); I0 returns to tones. Runs play one after the other; when the instruments cannot play (no instrument bank, no device) the run plays as tones. +Timing is QBasic's: a whole note is four beats of 60 / T seconds, each dot adds half again, and a note sounds for 7/8 (MN), all (ML), or 3/4 (MS) of its length. Octave 3 starts at middle C; octave 3's A is 440 Hz. The notes play on an audio.lib note queue, each to the exact sample in its own waveform and volume (audioNotesSetTone; on the PC speaker as square tones, V0 silent); V also scales an instrument's velocity. DVX's own I command (I1 to I128) plays the notes that follow on that General MIDI instrument: each run of such notes becomes a small MIDI file that plays like any MIDI music (on the synthesizer DVX.INI [audio] midi chooses); I0 returns to tones. Runs play one after the other; when the instruments cannot play (no instrument bank, no device) the run plays as tones. .h2 Several Programs at Once @@ -222,7 +222,7 @@ Every music plays through one audio context of the library's own; audio.lib mixe .h2 BASIC -src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName) and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums), and MusicMidiSend(music, midiMsg) (musicMidiSend of a message packed as MidiInMessage gives it, AUDIO_MIDI_PACKED_*). MIDI recordings: MidiRecordNew (a recording number, 0 when all 8 are in use), MidiRecordAdd(recording, milliseconds, midiMsg), MidiRecordCount, MidiRecordLength, MidiRecordSave(recording, fileName) (True / False with MusicError$), MidiRecordFree. A number only works for the program that got it. MusicLoadMacro(macro) (musicLoadMacro, a music number) and MusicSetBank(music, bank) (musicSetBank, "" the standard bank, a SoundFont or an ULTRASND.INI for wavetable music; MusicError$ says when it is missing or damaged); MusicPatchSet$ and MusicRomBank$ (musicPatchSetPath, musicRomBankPath: "" when there is none). MusicGetVolume(music) (musicGetVolume in percent) and MusicInfo$(music, field) (musicGetInfo: "title", "output", "kind" = "module" / "midi" / "live" / "sound" for MusicKindE, "where" = "" / "stream" / "mpu401" / "fm" / "software" / "wave" / "softwave" for MusicOutputE, "paused" and "looping" as "-1" / "0"; any case, "" for another field). music.bas also declares the raw MIDI port calls of audio.lib (MidiPortInfo$, MidiAcquire, MidiPut, MidiRelease) and its MIDI input (MidiInOpen, MidiInClose, MidiInMessage, MidiInTime, MidiInClock, MidiInLost, and the MIDI_* CONSTs); see the sound library. +src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName), MusicLoadData(bytes AS BINARY STRING) (musicLoadMem), and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicEnded (True once after the music played to its end: each number's musicSetEndFn sets a flag that MusicPlay and MusicEnded clear), MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums), and MusicMidiSend(music, midiMsg) (musicMidiSend of a message packed as MidiInMessage gives it, AUDIO_MIDI_PACKED_*). MIDI recordings: MidiRecordNew (a recording number, 0 when all 8 are in use), MidiRecordAdd(recording, milliseconds, midiMsg), MidiRecordCount, MidiRecordLength, MidiRecordSave(recording, fileName) (True / False with MusicError$), MidiRecordFree. A number only works for the program that got it. MusicLoadMacro(macro) (musicLoadMacro, a music number) and MusicSetBank(music, bank) (musicSetBank, "" the standard bank, a SoundFont or an ULTRASND.INI for wavetable music; MusicError$ says when it is missing or damaged); MusicPatchSet$ and MusicRomBank$ (musicPatchSetPath, musicRomBankPath: "" when there is none). MusicGetVolume(music) (musicGetVolume in percent) and MusicInfo$(music, field) (musicGetInfo: "title", "output", "kind" = "module" / "midi" / "live" / "sound" for MusicKindE, "where" = "" / "stream" / "mpu401" / "fm" / "software" / "wave" / "softwave" for MusicOutputE, "paused" and "looping" as "-1" / "0"; any case, "" for another field). music.bas also declares the raw MIDI port calls of audio.lib (MidiPortInfo$, MidiAcquire, MidiPut, MidiRelease) and its MIDI input (MidiInOpen, MidiInClose, MidiInMessage, MidiInTime, MidiInClock, MidiInLost, and the MIDI_* CONSTs); see the sound library. The BASIC runtime itself calls musicBasBeep, musicBasSound(hz, ticks), musicBasPlay(macro), musicBasPlayCount, and musicBasBusy for the statements BEEP, SOUND, and PLAY and the PLAY(n) function; each BASIC program has its own macro queue. QBasic's X command and "=" numbers (given with VARPTR$) are filled in by the BASIC runtime before musicBasPlay is called, so the macro language itself has neither. SOUND's duration is in clock ticks (18.2 a second) and 0 stops the queue; BEEP is 800 Hz for a quarter second. diff --git a/src/libs/kpunch/music/music.h b/src/libs/kpunch/music/music.h index a8e71bb0..d9ba699b 100644 --- a/src/libs/kpunch/music/music.h +++ b/src/libs/kpunch/music/music.h @@ -332,7 +332,11 @@ void musicMidiRecordFree(MusicMidiRecordT *rec); // may wait) // I n DVX's own: n 1-128 plays the notes that follow on General // MIDI instrument n (1 is the piano) on the music synthesizer, -// I0 (the default) square-wave tones again +// I0 (the default) tones again +// V n QB64's: volume of the notes that follow, 0-100 percent +// (default 100; an instrument's velocity too) +// @ n QB64's: the tones' waveform, 1 square (the default), 3 +// triangle, 5 noise // The X (execute a string) command and VARPTR$ parameters are not part // of the language here: BASIC's PLAY fills them in before the string // reaches the library. The settings stay from one call to the next. @@ -390,7 +394,8 @@ int32_t musicBasSound(double hz, double ticks); // Each BASIC program's musics are numbered from 1 (0 = MUSIC_FAILED; // MusicError$ says why), and its MIDI recordings the same way. Volumes // are percent (0..100). A MIDI message is packed as MidiInMessage gives -// it (AUDIO_MIDI_PACKED_*). +// it (AUDIO_MIDI_PACKED_*). MusicLoadData takes BINARY STRING bytes; +// MusicEnded is True once after a music played to its end. void MidiRecordAdd(int32_t handle, int32_t milliseconds, int32_t midiMsg); int32_t MidiRecordCount(int32_t handle); void MidiRecordFree(int32_t handle); @@ -398,12 +403,14 @@ int32_t MidiRecordLength(int32_t handle); int32_t MidiRecordNew(void); int32_t MidiRecordSave(int32_t handle, const char *fileName); void MusicControl(int32_t handle, int32_t channel, int32_t controller, int32_t value); +int32_t MusicEnded(int32_t handle); const char *MusicError(void); void MusicFree(int32_t handle); int32_t MusicGetVolume(int32_t handle); const char *MusicInfo(int32_t handle, const char *field); int32_t MusicLength(int32_t handle); int32_t MusicLoad(const char *fileName); +int32_t MusicLoadData(const uint8_t *data, int32_t len); int32_t MusicLoadMacro(const char *macro); int32_t MusicMidiOpen(void); void MusicMidiSend(int32_t handle, int32_t midiMsg); diff --git a/src/libs/kpunch/music/musicBas.c b/src/libs/kpunch/music/musicBas.c index 91d02f89..225be3a6 100644 --- a/src/libs/kpunch/music/musicBas.c +++ b/src/libs/kpunch/music/musicBas.c @@ -45,7 +45,6 @@ #define BYTE_MASK 0xFF #define BAS_FIRST_CHANNEL 1 #define PERCENT_MAX 100 -#define BAS_INFO_MAX (MUSIC_TITLE_MAX + MUSIC_DESC_MAX) // QBasic's clock tick (SOUND durations): 65536 PIT clocks of 1,193,182 Hz #define QB_TICK_US (65536.0 * 1000000.0 / 1193182.0) @@ -56,6 +55,7 @@ typedef struct { MusicT *music; int32_t appId; + bool ended; // played to its end since MusicEnded or MusicPlay last asked } BasSlotT; typedef struct { @@ -83,7 +83,7 @@ static const char *const sWhereWords[] = { "", "stream", "mpu401", "fm" static BasSlotT sSlots[BAS_SLOTS]; static BasRecordSlotT sRecords[BAS_RECORD_SLOTS]; static int32_t sLastError = MUSIC_OK; -static char sInfo[BAS_INFO_MAX]; +static BasTextT sText; // the last string result // ============================================================ // Prototypes @@ -91,10 +91,12 @@ static char sInfo[BAS_INFO_MAX]; static int32_t basAdd(MusicT *music, int32_t rc); static int32_t basAppId(void); +static void basEnded(MusicT *music, void *userData); static MusicT *basGet(int32_t handle); static uint8_t basPacked(int32_t midiMsg, int32_t shift); static MusicMidiRecordT *basRecord(int32_t handle); static int32_t basResult(int32_t rc); +static BasSlotT *basSlot(int32_t handle); static const char *basWord(const char *const *words, int32_t count, int32_t value); void MidiRecordAdd(int32_t handle, int32_t milliseconds, int32_t midiMsg); int32_t MidiRecordCount(int32_t handle); @@ -109,12 +111,14 @@ int32_t musicBasPlay(const char *macro); int32_t musicBasPlayCount(void); int32_t musicBasSound(double hz, double ticks); void MusicControl(int32_t handle, int32_t channel, int32_t controller, int32_t value); +int32_t MusicEnded(int32_t handle); const char *MusicError(void); void MusicFree(int32_t handle); int32_t MusicGetVolume(int32_t handle); const char *MusicInfo(int32_t handle, const char *field); int32_t MusicLength(int32_t handle); int32_t MusicLoad(const char *fileName); +int32_t MusicLoadData(const uint8_t *data, int32_t len); int32_t MusicLoadMacro(const char *macro); int32_t MusicMidiOpen(void); void MusicMidiSend(int32_t handle, int32_t midiMsg); @@ -147,7 +151,9 @@ static int32_t basAdd(MusicT *music, int32_t rc) { if (!sSlots[i].music) { sSlots[i].music = music; sSlots[i].appId = appId; + sSlots[i].ended = false; sLastError = MUSIC_OK; + musicSetEndFn(music, basEnded, &sSlots[i]); return i + 1; } } @@ -163,15 +169,19 @@ static int32_t basAppId(void) { } +// A music of a BASIC program played to its end (MusicEnded). +static void basEnded(MusicT *music, void *userData) { + (void)music; + ((BasSlotT *)userData)->ended = true; +} + + // The calling program's music with this number, else NULL (and the error // is noted). static MusicT *basGet(int32_t handle) { - if (handle < 1 || handle > BAS_SLOTS || !sSlots[handle - 1].music || sSlots[handle - 1].appId != basAppId()) { - sLastError = MUSIC_ERR_ARG; - return NULL; - } + BasSlotT *s = basSlot(handle); - return sSlots[handle - 1].music; + return s ? s->music : NULL; } @@ -200,6 +210,18 @@ static int32_t basResult(int32_t rc) { } +// The calling program's music with this number, else NULL (and the +// error is noted). +static BasSlotT *basSlot(int32_t handle) { + if (handle < 1 || handle > BAS_SLOTS || !sSlots[handle - 1].music || sSlots[handle - 1].appId != basAppId()) { + sLastError = MUSIC_ERR_ARG; + return NULL; + } + + return &sSlots[handle - 1]; +} + + // The word for an enum value ("" out of range). static const char *basWord(const char *const *words, int32_t count, int32_t value) { return (value >= 0 && value < count) ? words[value] : ""; @@ -318,6 +340,20 @@ void MusicControl(int32_t handle, int32_t channel, int32_t controller, int32_t v } +// True once after the music played to its end (not when stopped), since +// MusicPlay started it or MusicEnded last said so. +int32_t MusicEnded(int32_t handle) { + BasSlotT *s = basSlot(handle); + bool ended = s && s->ended; + + if (s) { + s->ended = false; + } + + return ended ? BAS_TRUE : BAS_FALSE; +} + + const char *MusicError(void) { return musicErrorText(sLastError); } @@ -365,12 +401,10 @@ const char *MusicInfo(int32_t handle, const char *field) { switch ((MusicInfoFieldE)idx) { case InfoTitleE: - snprintf(sInfo, sizeof(sInfo), "%s", info.title); - return sInfo; + return basTextSet(&sText, info.title); case InfoOutputE: - snprintf(sInfo, sizeof(sInfo), "%s", info.output); - return sInfo; + return basTextSet(&sText, info.output); case InfoKindE: return basWord(sKindWords, DVX_ARRAY_LEN(sKindWords), info.kind); @@ -379,12 +413,10 @@ const char *MusicInfo(int32_t handle, const char *field) { return basWord(sWhereWords, DVX_ARRAY_LEN(sWhereWords), info.where); case InfoPausedE: - snprintf(sInfo, sizeof(sInfo), "%d", info.paused ? BAS_TRUE : BAS_FALSE); - return sInfo; + return basTextFormat(&sText, "%d", info.paused ? BAS_TRUE : BAS_FALSE); case InfoLoopingE: - snprintf(sInfo, sizeof(sInfo), "%d", info.looping ? BAS_TRUE : BAS_FALSE); - return sInfo; + return basTextFormat(&sText, "%d", info.looping ? BAS_TRUE : BAS_FALSE); default: return ""; @@ -408,6 +440,15 @@ int32_t MusicLoad(const char *fileName) { } +// A music from the bytes of its file (a resource, an HTTP reply). +int32_t MusicLoadData(const uint8_t *data, int32_t len) { + MusicT *m = NULL; + int32_t rc = musicLoadMem(data, len > 0 ? (uint32_t)len : 0, &m); + + return basAdd(m, rc); +} + + int32_t MusicLoadMacro(const char *macro) { MusicT *m = NULL; int32_t rc = musicLoadMacro(macro, &m); @@ -442,13 +483,13 @@ void MusicNoteOn(int32_t handle, int32_t channel, int32_t note, int32_t velocity // The UltraSound's own patches' ULTRASND.INI (musicPatchSetPath), or "". const char *MusicPatchSet(void) { - static char path[DVX_MAX_PATH]; + char path[DVX_MAX_PATH]; if (!musicPatchSetPath(path, sizeof(path))) { path[0] = '\0'; } - return path; + return basTextSet(&sText, path); } @@ -463,7 +504,13 @@ void MusicPitchBend(int32_t handle, int32_t channel, int32_t bend) { void MusicPlay(int32_t handle, int32_t loopIt) { - sLastError = musicPlay(basGet(handle), loopIt != BAS_FALSE); + BasSlotT *s = basSlot(handle); + + if (s) { + s->ended = false; + } + + sLastError = musicPlay(s ? s->music : NULL, loopIt != BAS_FALSE); } @@ -489,13 +536,13 @@ void MusicResume(int32_t handle) { // The AWE's own ROM bank (musicRomBankPath), or "". const char *MusicRomBank(void) { - static char path[DVX_MAX_PATH]; + char path[DVX_MAX_PATH]; if (!musicRomBankPath(path, sizeof(path))) { path[0] = '\0'; } - return path; + return basTextSet(&sText, path); } diff --git a/src/libs/kpunch/music/musicInt.h b/src/libs/kpunch/music/musicInt.h index 90c3c056..34f5cc46 100644 --- a/src/libs/kpunch/music/musicInt.h +++ b/src/libs/kpunch/music/musicInt.h @@ -47,7 +47,9 @@ // musicWaveLoad.c a SoundFont's samples in the synthesizer's memory // musicWaveSoft.c the SoundFont software synthesizer (WaveSoftT) // musicCents.c cents, powers of two, and attenuation in fixed point -// musicXmp.c tracker modules through libxmp-lite +// musicXmpDrv.c tracker modules: the player module, loaded when one +// is opened +// musicXmp.c ... that module (xmp.drv): libxmp-lite behind it // musicSound.c WAV and VOC files (SoundSrcT): a WAV read from the // disk as it plays, a VOC decoded into memory // musicModHw.c a module on a wavetable synthesizer's voices: its @@ -75,6 +77,10 @@ #include "music.h" #include "audioDrv.h" +// Microseconds +#define US_PER_SEC 1000000ULL +#define US_PER_MS 1000 + // ============================================================ // MIDI // ============================================================ @@ -450,10 +456,14 @@ int32_t patLoadSet(const char *iniPath, WaveBankT *bank); // A file or folder is at path (musicFound.c). bool musicFileThere(const char *path); +// The file the bank reads last read (waveBankRead keeps it open for the +// next: a song's samples come from one or a few files) closed; a load +// that read samples calls it once done. +void waveBankClose(void); + // The bank's memory given back (the bank cleared). void waveBankFree(WaveBankT *bank); - // frames frames of sample data from frame first into out (16-bit); // false when they cannot be read. bool waveBankRead(const WaveBankT *bank, uint32_t first, uint32_t frames, int16_t *out); @@ -727,7 +737,8 @@ void waveSoftRender(WaveSoftT *soft, int16_t *out, uint32_t frames, uint32_t // ============================================================ // Seconds the library keeps the SoundFont's samples in the synthesizer -// after the last music goes (music.c), for the next one. +// after the last music goes (music.c), for the next one (its bank is +// kept for the session). #define MUSIC_FONT_KEEP_SEC 60 // The bank's samples in the synthesizer's memory, kept from song to song @@ -760,6 +771,13 @@ uint32_t waveNeedsPreset(const WaveBankT *bank, uint16_t bankNum, uint8_t progra // cannot be read); MUSIC_ERR_NO_MEMORY. int32_t waveLoadNeeded(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, const uint8_t *need, uint32_t flags, uint32_t *missing); +// One needed sample that is missing loaded (the ROM ones first), at the +// best quality that fits beside the loaded ones, which stay where they +// are; *more when others are still missing. One that fits nowhere (or +// cannot be read) is cleared from need: MUSIC_ERR_NO_DEVICE (or the +// read's error). +int32_t waveLoadStep(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, uint8_t *need, uint32_t flags, bool *more); + // ============================================================ // The MPU-401 port (musicMpu.c) // ============================================================ @@ -864,7 +882,7 @@ void synthRender(SynthChipT *chip, int16_t *out, uint32_t frames, uint32_t ch void synthWrite(SynthChipT *chip, int32_t voice, uint8_t reg, uint8_t value); // ============================================================ -// Tracker modules (musicXmp.c) +// Tracker modules (musicXmpDrv.c; the player module, musicXmp.c) // ============================================================ typedef struct XmpPlayerT XmpPlayerT; @@ -911,14 +929,17 @@ typedef void (*XmpHwFnT)(void *user, const XmpHwVoiceT *voice); // module is ready to render at rate (Hz) in channels (1 or 2) 16-bit // frames. xmpRender renders frames frames; false once a song that does // not loop has ended (the rest of out is silence). Times in ms. +// xmpIsModule: the file at path is a module libxmp plays (its head read +// only). xmpOpen gives way (musicGiveWay) between the samples it loads. void xmpClose(XmpPlayerT *player); +bool xmpIsModule(const char *path); uint32_t xmpLength(XmpPlayerT *player); int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out); uint32_t xmpPosition(XmpPlayerT *player); bool xmpRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop); void xmpRestart(XmpPlayerT *player); void xmpSeek(XmpPlayerT *player, uint32_t ms); -const char *xmpTitle(XmpPlayerT *player); +const char *xmpTitle(const XmpPlayerT *player); // The module's tracks and samples (false: no sample there), and the // frames its ping-pong loops are shortened by on the way back (Impulse @@ -938,6 +959,41 @@ bool xmpHwFrame(XmpPlayerT *player, bool loop, uint32_t *us); void xmpHwOff(XmpPlayerT *player); bool xmpHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user); +// The player module (xmp.drv, the only one in MUSIC_XMP_DIR: musicXmp.c +// and libxmp-lite) is loaded by the first xmpIsModule or xmpOpen; without +// it no file is a module (xmpOpen: MUSIC_ERR_FORMAT). xmpIdle +// (musicIdle) unloads it MUSIC_XMP_KEEP_SEC after its last use with no +// player open, so songs in a row load it once; xmpUnload (musicShutdown, +// every player closed) unloads it now. +void xmpIdle(void); +void xmpUnload(void); + +#define MUSIC_XMP_DIR "libs" DVX_PATH_SEP "kpunch" DVX_PATH_SEP "music" DVX_PATH_SEP "players" +#define MUSIC_XMP_SYMBOL "musicXmpDriver" +#define MUSIC_XMP_KEEP_SEC 60 + +// What the module exports as MUSIC_XMP_SYMBOL: the functions behind the +// xmp* calls above, which musicXmpDrv.c makes through it. +typedef struct { + DvxDriverHeaderT header; + __typeof__(xmpBidiAdjust) *bidiAdjust; + __typeof__(xmpChannels) *channels; + __typeof__(xmpClose) *close; + __typeof__(xmpHwFrame) *hwFrame; + __typeof__(xmpHwOff) *hwOff; + __typeof__(xmpHwOn) *hwOn; + __typeof__(xmpIsModule) *isModule; + __typeof__(xmpLength) *length; + __typeof__(xmpOpen) *open; + __typeof__(xmpPosition) *position; + __typeof__(xmpRender) *render; + __typeof__(xmpRestart) *restart; + __typeof__(xmpSample) *sample; + __typeof__(xmpSampleCount) *sampleCount; + __typeof__(xmpSeek) *seek; + __typeof__(xmpTitle) *title; +} XmpDriverT; + // ============================================================ // Sound files (musicSound.c) // ============================================================ @@ -945,10 +1001,10 @@ bool xmpHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user typedef struct SoundSrcT SoundSrcT; // A WAV or VOC file as the frames of a sound music. soundOpenFile reads -// a WAV named by its path as it plays (only its start now); soundOpenMem -// decodes a WAV or VOC file's bytes into memory. MUSIC_OK, -// MUSIC_ERR_FORMAT (not a file the call takes: soundOpenFile takes only -// WAV), _OPEN, _CORRUPT, _NO_MEMORY. soundRead gives up to frames 16-bit +// a WAV or VOC named by its path as it plays (only its start now, and a +// VOC's block headers); soundOpenMem decodes a WAV or VOC file's bytes +// into memory. MUSIC_OK, MUSIC_ERR_FORMAT (not a WAV or VOC file), +// _OPEN, _CORRUPT, _NO_MEMORY. soundRead gives up to frames 16-bit // frames from the place it is at (fewer at the end), soundSeek moves it. // Program time only. uint32_t soundChannels(const SoundSrcT *s); @@ -1134,6 +1190,9 @@ bool musicLock(bool lock); #define MML_PROGRAM_PIANO 1 // the acoustic grand piano, General MIDI's first #define MML_PROGRAMS 128 +// Volume (V): percent of the full level (an instrument's velocity too) +#define MML_VOLUME_MAX 100 + // The settings a macro changes, which last from one string to the next. typedef struct { int32_t octave; @@ -1141,6 +1200,8 @@ typedef struct { int32_t tempo; // quarter notes a minute int32_t onEighths; int32_t program; + int32_t volume; // 0..MML_VOLUME_MAX (V) + int32_t wave; // AudioWaveE of the tones (@) bool background; } MmlStateT; @@ -1151,9 +1212,11 @@ typedef struct { uint32_t soundUs; // the part that sounds int32_t midiNote; // MIDI note number (instruments) int32_t program; // MML_PROGRAM_TONE or the instrument + int32_t volume; // 0..MML_VOLUME_MAX + int32_t wave; // AudioWaveE (tones) } MmlNoteT; -// The defaults (O4 L4 T120 MN MF I0). +// The defaults (O4 L4 T120 MN MF I0 V100 @1). void mmlStateInit(MmlStateT *st); // Append the notes of macro to *notes (stb_ds array) and update st; @@ -1180,6 +1243,12 @@ int32_t musicAppCurrent(void); void musicAppEnded(int32_t appId); int32_t musicAppEnter(int32_t appId); void musicBasForget(int32_t appId); + +// Long work at program time (a big module loading, a file read) gives +// way: DVX's other tasks run once MUSIC_GIVE_WAY_US has passed since +// *last (a platformMicros time the caller started at; set again). +#define MUSIC_GIVE_WAY_US 20000 +void musicGiveWay(uint64_t *last); void musicIdle(void *unused); void musicShutdown(void); void musicSuspend(bool suspending); diff --git a/src/libs/kpunch/music/musicMml.c b/src/libs/kpunch/music/musicMml.c index 4eea5993..e720e14d 100644 --- a/src/libs/kpunch/music/musicMml.c +++ b/src/libs/kpunch/music/musicMml.c @@ -27,7 +27,8 @@ // a note of length n a 1/n of that, each dot half as long again; the // sounding part is 7/8 (MN), all (ML) or 3/4 (MS) of it, the rest is // silence. Octave n's C is MIDI note 24 + 12n (octave 3 starts at middle -// C), and its A (octave 3) is 440 Hz. +// C), and its A (octave 3) is 440 Hz. QB64's V (volume, percent) and @ +// (the tones' waveform: 1 square, 3 triangle, 5 noise) are taken too. #include "musicInt.h" #include "stb_ds.h" @@ -65,6 +66,17 @@ static const uint32_t sOctave0MilliHz[MML_SEMITONES] = { // Semitone of A..G within the octave static const int32_t sLetterSemitone[] = { 9, 11, 0, 2, 4, 5, 7 }; +// The waveforms @n names (QB64's numbers; 2 sawtooth and 4 sine DVX does +// not make) +static const struct { + int32_t number; + AudioWaveE wave; +} sMmlWaves[] = { + { 1, AUDIO_WAVE_SQUARE }, + { 3, AUDIO_WAVE_TRIANGLE }, + { 5, AUDIO_WAVE_NOISE } +}; + // MIDI file: 0.1 ms ticks (10,000 ticks a quarter note of 1,000,000 us) #define SMF_DIVISION 10000 #define SMF_TEMPO_US 1000000 @@ -80,6 +92,7 @@ static int32_t countDots(const char **p); int32_t mmlParse(MmlStateT *st, const char *macro, MmlNoteT **notes); int32_t mmlSmfBuild(const MmlNoteT *notes, int32_t count, uint8_t **out, uint32_t *size); void mmlStateInit(MmlStateT *st); +static bool mmlWave(int32_t number, int32_t *wave); static uint32_t pitchMilliHz(int32_t pitch); static bool readNumber(const char **p, int32_t *out); static bool readRanged(const char **p, int32_t lo, int32_t hi, int32_t *out); @@ -100,6 +113,8 @@ static void addNote(MmlNoteT **notes, const MmlStateT *st, int32_t pitch, int32_ n.milliHz = pitch < 0 ? 0 : pitchMilliHz(pitch); n.midiNote = pitch < 0 ? 0 : MML_MIDI_BASE + pitch; n.program = st->program; + n.volume = st->volume; + n.wave = st->wave; arrput(*notes, n); } @@ -206,6 +221,14 @@ int32_t mmlParse(MmlStateT *st, const char *macro, MmlNoteT **notes) { ok = readRanged(&p, MML_PROGRAM_TONE, MML_PROGRAMS, &s.program); break; + case 'V': + ok = readRanged(&p, 0, MML_VOLUME_MAX, &s.volume); + break; + + case '@': + ok = readNumber(&p, &n) && mmlWave(n, &s.wave); + break; + case 'M': c = toupper((unsigned char)*p); p = *p ? p + 1 : p; @@ -278,11 +301,12 @@ int32_t mmlSmfBuild(const MmlNoteT *notes, int32_t count, uint8_t **out, uint32_ lastTick = onAt; } - if (n->milliHz) { + // V scales the velocity (a note at V0 is silent) + if (n->milliHz && n->volume > 0) { midiPutVlq(&trk, onAt - lastTick); arrput(trk, MIDI_NOTE_ON); arrput(trk, (uint8_t)n->midiNote); - arrput(trk, SMF_VELOCITY); + arrput(trk, (uint8_t)DVX_MAX(1, SMF_VELOCITY * n->volume / MML_VOLUME_MAX)); midiPutVlq(&trk, offAt - onAt); arrput(trk, MIDI_NOTE_OFF); arrput(trk, (uint8_t)n->midiNote); @@ -308,10 +332,25 @@ void mmlStateInit(MmlStateT *st) { st->tempo = MML_TEMPO_DEFAULT; st->onEighths = MML_ON_NORMAL; st->program = MML_PROGRAM_TONE; + st->volume = MML_VOLUME_MAX; + st->wave = AUDIO_WAVE_SQUARE; st->background = false; } +// The waveform QB64's @number names, false for one DVX does not make. +static bool mmlWave(int32_t number, int32_t *wave) { + for (int32_t i = 0; i < DVX_ARRAY_LEN(sMmlWaves); i++) { + if (sMmlWaves[i].number == number) { + *wave = (int32_t)sMmlWaves[i].wave; + return true; + } + } + + return false; +} + + static uint32_t pitchMilliHz(int32_t pitch) { return sOctave0MilliHz[pitch % MML_SEMITONES] << (pitch / MML_SEMITONES); } diff --git a/src/libs/kpunch/music/musicMmlPlay.c b/src/libs/kpunch/music/musicMmlPlay.c index 38581fbe..7d0e9f78 100644 --- a/src/libs/kpunch/music/musicMmlPlay.c +++ b/src/libs/kpunch/music/musicMmlPlay.c @@ -46,7 +46,6 @@ // Note queue entries one note may take (the tone and the silence after it) #define MML_ENTRIES_NOTE 2 -#define US_PER_MS 1000 // A run of notes played the same way. typedef struct { @@ -325,6 +324,8 @@ int32_t musicMmlTone(MusicMmlT *mml, uint32_t milliHz, uint32_t us) { n.us = us; n.soundUs = milliHz ? us : 0; n.program = MML_PROGRAM_TONE; + n.volume = MML_VOLUME_MAX; + n.wave = AUDIO_WAVE_SQUARE; partAdd(mml, &n, 1, false); pump(mml); return MUSIC_OK; @@ -402,6 +403,8 @@ static bool tonesFeed(MusicMmlT *m, MmlPartT *part) { while (m->tones && part->next < count && audioNotesSpace(m->tones) >= MML_ENTRIES_NOTE) { const MmlNoteT *n = &part->notes[part->next++]; + audioNotesSetTone(m->tones, (AudioWaveE)n->wave, n->volume * AUDIO_VOLUME_MAX / MML_VOLUME_MAX); + if (n->milliHz && audioNotesAdd(m->tones, n->milliHz, n->soundUs) == AUDIO_OK) { if (n->us > n->soundUs) { audioNotesAdd(m->tones, 0, n->us - n->soundUs); diff --git a/src/libs/kpunch/music/musicModHw.c b/src/libs/kpunch/music/musicModHw.c index a2691957..b8a2e131 100644 --- a/src/libs/kpunch/music/musicModHw.c +++ b/src/libs/kpunch/music/musicModHw.c @@ -55,7 +55,6 @@ #define RATE_FIX 16 // a rate's sixteenths count in its pitch #define VOL_SHIFT 3 // XMP_HW_VOL_MAX >> VOL_SHIFT = WAVE_STEP_MAX #define LEVEL_CB 80 // a voice at full volume 8 dB down: as loud as libxmp mixes it (measured on a GUS alone) -#define US_PER_MS 1000 // ============================================================ // Prototypes @@ -71,6 +70,7 @@ uint32_t modHwPosition(const ModHwT *mod); void modHwSetGain(ModHwT *mod, int32_t gain); void modHwSilence(ModHwT *mod); static void modHwVoice(void *user, const XmpHwVoiceT *v); +static uint32_t sampleFrames(const ModHwT *mod, const XmpSampleT *xs, uint32_t *body, uint32_t *back); static int32_t sampleLoad(ModHwT *mod, const XmpSampleT *xs, uint32_t i, uint32_t bits); static void samplesFree(ModHwT *mod); static int32_t samplesLoad(ModHwT *mod, XmpPlayerT *xmp, uint32_t flags); @@ -288,16 +288,26 @@ static void modHwVoice(void *user, const XmpHwVoiceT *v) { } +// The frames sample xs is stored as (sampleLoad): *body of its own (to a +// ping-pong loop's end), then *back of the loop's way back. +static uint32_t sampleFrames(const ModHwT *mod, const XmpSampleT *xs, uint32_t *body, uint32_t *back) { + uint32_t span = xs->loopEnd - xs->loopStart; + + *back = xs->bidi && span > mod->bidiAdjust ? span - mod->bidiAdjust : 0; + *body = *back ? xs->loopEnd : xs->frames; + return *body + *back; +} + + // Sample i into the synthesizer's memory at bits: its frames (a ping-pong // loop unrolled), then WAVE_GUARD_FRAMES the chip's interpolation reads // past the end (the loop again, where the stored frames end with it; // else silence). MODHW_ERR_ROOM when it does not fit. static int32_t sampleLoad(ModHwT *mod, const XmpSampleT *xs, uint32_t i, uint32_t bits) { ModHwSampleT *s = &mod->samples[i]; - uint32_t span = xs->loopEnd - xs->loopStart; - uint32_t back = xs->bidi && span > mod->bidiAdjust ? span - mod->bidiAdjust : 0; - uint32_t body = back ? xs->loopEnd : xs->frames; - uint32_t frames = body + back; + uint32_t body; + uint32_t back; + uint32_t frames = sampleFrames(mod, xs, &body, &back); uint32_t repeat = xs->loops && (back || xs->loopEnd == xs->frames) ? frames - xs->loopStart : 0; int16_t *pcm = (int16_t *)malloc((frames + WAVE_GUARD_FRAMES) * sizeof(int16_t)); uint32_t addr = 0; @@ -358,13 +368,30 @@ static void samplesFree(ModHwT *mod) { // Every sample as it is (8-bit ones 8-bit where the chip plays those), // else (some do not fit) all 8-bit where it does; a sample libxmp has no -// data for is left out. +// data for is left out. A pass whose samples cannot fit in the memory +// free (all of it, blocks apart or not) is passed over, nothing uploaded. static int32_t samplesLoad(ModHwT *mod, XmpPlayerT *xmp, uint32_t flags) { - bool chip8 = (flags & AUDIO_WT_8BIT) != 0; - int32_t rc = MODHW_ERR_ROOM; + bool chip8 = (flags & AUDIO_WT_8BIT) != 0; + int32_t rc = MODHW_ERR_ROOM; + AudioWtInfoT info; + + audioWtGetInfo(&info); for (uint32_t pass = 0; pass < (chip8 ? 2U : 1U) && rc == MODHW_ERR_ROOM; pass++) { XmpSampleT xs; + uint64_t bytes = 0; + uint32_t body; + uint32_t back; + + for (uint32_t i = 0; i < mod->sampleCount; i++) { + if (xmpSample(xmp, i, &xs)) { + bytes += (uint64_t)(sampleFrames(mod, &xs, &body, &back) + WAVE_GUARD_FRAMES) * ((chip8 && (pass > 0 || !xs.bits16) ? BITS_8 : BITS_16) / BYTE_BITS); + } + } + + if (bytes > info.freeTotal) { + continue; + } rc = MUSIC_OK; samplesFree(mod); diff --git a/src/libs/kpunch/music/musicOp2.c b/src/libs/kpunch/music/musicOp2.c index e429c4f2..2ac19d5d 100644 --- a/src/libs/kpunch/music/musicOp2.c +++ b/src/libs/kpunch/music/musicOp2.c @@ -32,6 +32,7 @@ // All numbers little-endian. #include "musicInt.h" +#include "dvxEndian.h" #include @@ -43,7 +44,6 @@ #define OP2_FINE_TUNE_AT 2 #define OP2_FIXED_NOTE_AT 3 #define OP2_NOTE_OFFSET_AT 14 -#define BYTE_BITS 8 // Byte offsets within a voice enum { @@ -79,7 +79,7 @@ int32_t op2Parse(const uint8_t *data, uint32_t size, Op2BankT *bank) { const uint8_t *p = data + OP2_SIGNATURE_BYTES + i * OP2_INSTR_BYTES; Op2InstrumentT *in = &bank->instruments[i]; - in->flags = (uint16_t)(p[0] | (p[1] << BYTE_BITS)); + in->flags = dvxGetLe16(p); in->fineTune = p[OP2_FINE_TUNE_AT]; in->fixedNote = p[OP2_FIXED_NOTE_AT]; @@ -104,5 +104,5 @@ static void readVoice(const uint8_t *p, Op2VoiceT *v) { v->carSustain = p[VOICE_CAR_SUSTAIN]; v->carWave = p[VOICE_CAR_WAVE]; v->carLevel = (uint8_t)(p[VOICE_CAR_KSL] | p[VOICE_CAR_LEVEL]); - v->noteOffset = (int16_t)(p[OP2_NOTE_OFFSET_AT] | (p[OP2_NOTE_OFFSET_AT + 1] << BYTE_BITS)); + v->noteOffset = (int16_t)dvxGetLe16(p + OP2_NOTE_OFFSET_AT); } diff --git a/src/libs/kpunch/music/musicPat.c b/src/libs/kpunch/music/musicPat.c index 49a5c959..ed9ef818 100644 --- a/src/libs/kpunch/music/musicPat.c +++ b/src/libs/kpunch/music/musicPat.c @@ -54,6 +54,7 @@ // pan balance 0..15 #include "musicInt.h" +#include "dvxEndian.h" #include "stb_ds.h" #include @@ -129,7 +130,6 @@ #define RATE_RANGE_STEP 8 // each range step: 8 times slower #define FRAME_NS 38873 // a GF1 frame at 24 voices #define NS_PER_US 1000 -#define US_PER_SEC 1000000 #define LFO_BASE_MILLIHZ 8176 // 0 absolute cents #define LFO_MILLIHZ_BASE 50 // 0.05 Hz #define LFO_MILLIHZ_NUM 2333 // + 0.02333 Hz a step @@ -179,8 +179,6 @@ typedef struct { // ============================================================ static int32_t keyCents(uint32_t milliHz); -static uint32_t le16(const uint8_t *p); -static uint32_t le32(const uint8_t *p); static int32_t lfoCents(uint8_t rate); static void patAddWave(PatBuildT *b, const PatWaveT *w, uint16_t file, uint8_t keyLo, uint8_t keyHi); static bool patFind(PrefsHandleT *ini, const char *section, const char *dir, int32_t key, char *path); @@ -195,16 +193,6 @@ static int32_t keyCents(uint32_t milliHz) { } -static uint32_t le16(const uint8_t *p) { - return (uint32_t)p[0] | (uint32_t)p[1] << 8; -} - - -static uint32_t le32(const uint8_t *p) { - return le16(p) | le16(p + 2) << 16; -} - - // An LFO rate byte (0.05 Hz + 0.0233 Hz a step) in absolute cents. static int32_t lfoCents(uint8_t rate) { return waveRatioCents(LFO_MILLIHZ_BASE + rate * LFO_MILLIHZ_NUM / LFO_MILLIHZ_DEN, LFO_BASE_MILLIHZ); @@ -481,7 +469,7 @@ static int32_t patRead(const char *path, PatWaveT **waves) { return MUSIC_ERR_BANK; } - uint32_t count = le16(head + PAT_WAVES_AT); + uint32_t count = dvxGetLe16(head + PAT_WAVES_AT); long at = PAT_HEADER_BYTES + PAT_INSTR_BYTES + PAT_LAYER_BYTES; for (uint32_t i = 0; i < count && rc == MUSIC_OK; i++) { @@ -494,13 +482,13 @@ static int32_t patRead(const char *path, PatWaveT **waves) { memset(&w, 0, sizeof(w)); w.dataAt = (uint32_t)(at + PAT_WAVE_BYTES); - w.size = le32(wh + W_SIZE); - w.loopStart = le32(wh + W_LOOP_START); - w.loopEnd = le32(wh + W_LOOP_END); - w.rate = (uint16_t)le16(wh + W_RATE); - w.low = le32(wh + W_LOW); - w.high = le32(wh + W_HIGH); - w.root = le32(wh + W_ROOT); + w.size = dvxGetLe32(wh + W_SIZE); + w.loopStart = dvxGetLe32(wh + W_LOOP_START); + w.loopEnd = dvxGetLe32(wh + W_LOOP_END); + w.rate = dvxGetLe16(wh + W_RATE); + w.low = dvxGetLe32(wh + W_LOW); + w.high = dvxGetLe32(wh + W_HIGH); + w.root = dvxGetLe32(wh + W_ROOT); w.balance = wh[W_BALANCE] > BALANCE_MAX ? BALANCE_MAX : wh[W_BALANCE]; w.tremRate = wh[W_TREM_RATE]; w.tremDepth = wh[W_TREM_DEPTH]; @@ -508,8 +496,8 @@ static int32_t patRead(const char *path, PatWaveT **waves) { w.vibRate = wh[W_VIB_RATE]; w.vibDepth = wh[W_VIB_DEPTH]; w.modes = wh[W_MODES]; - w.scaleKey = (uint16_t)le16(wh + W_SCALE_FREQ); - w.scaleFactor = (uint16_t)le16(wh + W_SCALE_FACTOR); + w.scaleKey = dvxGetLe16(wh + W_SCALE_FREQ); + w.scaleFactor = dvxGetLe16(wh + W_SCALE_FACTOR); memcpy(w.envRate, wh + W_ENV_RATE, ENV_POINTS); memcpy(w.envLevel, wh + W_ENV_OFFSET, ENV_POINTS); at += PAT_WAVE_BYTES + (long)w.size; diff --git a/src/libs/kpunch/music/musicSf2.c b/src/libs/kpunch/music/musicSf2.c index 90e9def7..0b1fb6b5 100644 --- a/src/libs/kpunch/music/musicSf2.c +++ b/src/libs/kpunch/music/musicSf2.c @@ -56,6 +56,7 @@ // specification is not public; this follows what is known of the files. #include "musicInt.h" +#include "dvxEndian.h" #include #include @@ -251,6 +252,11 @@ static const GenRuleT sGenRules[SF_GEN_COUNT] = { [60] = { 0, 0, 0, false } }; +// The file waveBankRead read last, kept open for the next read +// (waveBankClose closes it) +static FILE *sReadFp = NULL; +static char sReadPath[DVX_MAX_PATH]; + // ============================================================ // Prototypes // ============================================================ @@ -260,8 +266,7 @@ static bool bagRange(const HydraT *h, int32_t bagArray, uint32_t bag, ui static int32_t buildRegions(const HydraT *h, WaveBankT *bank); static bool findHydra(const uint8_t *pdta, uint32_t size, bool sf1, HydraT *h); static int32_t instrumentRegions(const HydraT *h, const WaveBankT *bank, uint32_t inst, const ZoneT *preset, uint32_t *count, WaveRegionT **regions); -static uint32_t le16(const uint8_t *p); -static uint32_t le32(const uint8_t *p); +static FILE *readFile(const char *path); static int32_t readHydra(SourceT *src, uint32_t offset, uint32_t size, bool sf1, WaveBankT *bank); static int32_t readSamples(const HydraT *h, WaveBankT *bank); static void regionGens(bool sf1, const ZoneT *inst, const ZoneT *preset, int16_t *out); @@ -275,6 +280,7 @@ int32_t sf2Load(const char *path, WaveBankT *bank); int32_t sf2Parse(const uint8_t *data, uint32_t size, WaveBankT *bank); static bool sourcesRead(const WaveBankT *bank, uint32_t first, uint32_t frames, int16_t *out); static bool srcRead(SourceT *src, uint32_t offset, void *out, uint32_t len); +void waveBankClose(void); void waveBankFree(WaveBankT *bank); bool waveBankRead(const WaveBankT *bank, uint32_t first, uint32_t frames, int16_t *out); static bool zoneHas(const HydraT *h, int32_t genArray, uint32_t first, uint32_t last, uint32_t oper); @@ -285,10 +291,10 @@ static bool zoneHas(const HydraT *h, int32_t genArray, uint32_t first, u static void applyGens(const HydraT *h, int32_t genArray, uint32_t first, uint32_t last, ZoneT *zone) { for (uint32_t g = first; g < last; g++) { const uint8_t *rec = h->at[genArray] + g * GEN_BYTES; - uint32_t oper = le16(rec + GEN_OPER); + uint32_t oper = dvxGetLe16(rec + GEN_OPER); if (oper < SF_GEN_COUNT) { - zone->gen[oper] = (int16_t)le16(rec + GEN_AMOUNT); + zone->gen[oper] = (int16_t)dvxGetLe16(rec + GEN_AMOUNT); zone->set[oper] = 1; } } @@ -300,8 +306,8 @@ static void applyGens(const HydraT *h, int32_t genArray, uint32_t first, uint32_ static bool bagRange(const HydraT *h, int32_t bagArray, uint32_t bag, uint32_t *first, uint32_t *last) { int32_t genArray = bagArray == H_PBAG ? H_PGEN : H_IGEN; - *first = le16(h->at[bagArray] + bag * BAG_BYTES + BAG_GEN); - *last = le16(h->at[bagArray] + (bag + 1) * BAG_BYTES + BAG_GEN); + *first = dvxGetLe16(h->at[bagArray] + bag * BAG_BYTES + BAG_GEN); + *last = dvxGetLe16(h->at[bagArray] + (bag + 1) * BAG_BYTES + BAG_GEN); return *first <= *last && *last <= h->count[genArray]; } @@ -318,15 +324,15 @@ static int32_t buildRegions(const HydraT *h, WaveBankT *bank) { for (uint32_t p = 0; p < presets; p++) { const uint8_t *rec = h->at[H_PHDR] + p * PHDR_BYTES; - uint32_t bagFirst = le16(rec + PHDR_BAG); - uint32_t bagLast = le16(rec + PHDR_BYTES + PHDR_BAG); + uint32_t bagFirst = dvxGetLe16(rec + PHDR_BAG); + uint32_t bagLast = dvxGetLe16(rec + PHDR_BYTES + PHDR_BAG); WavePresetT *out = &bank->presets[p]; ZoneT global; memcpy(out->name, rec, SF_NAME_LEN); out->name[SF_NAME_LEN] = '\0'; - out->program = (uint16_t)le16(rec + PHDR_PRESET); - out->bank = (uint16_t)le16(rec + PHDR_BANK); + out->program = dvxGetLe16(rec + PHDR_PRESET); + out->bank = dvxGetLe16(rec + PHDR_BANK); out->first = bank->regionCount; if (bagFirst > bagLast || bagLast >= h->count[H_PBAG]) { @@ -386,7 +392,7 @@ static bool findHydra(const uint8_t *pdta, uint32_t size, bool sf1, HydraT *h) { h->sf1 = sf1; for (uint32_t at = 0; at + RIFF_HEADER_BYTES <= size;) { - uint32_t len = le32(pdta + at + RIFF_ID_BYTES); + uint32_t len = dvxGetLe32(pdta + at + RIFF_ID_BYTES); uint32_t body = at + RIFF_HEADER_BYTES; if (len > size - body) { @@ -423,8 +429,8 @@ static bool findHydra(const uint8_t *pdta, uint32_t size, bool sf1, HydraT *h) { // its ranges), appended to *regions. static int32_t instrumentRegions(const HydraT *h, const WaveBankT *bank, uint32_t inst, const ZoneT *preset, uint32_t *count, WaveRegionT **regions) { const uint8_t *rec = h->at[H_INST] + inst * INST_BYTES; - uint32_t bagFirst = le16(rec + INST_BAG); - uint32_t bagLast = le16(rec + INST_BYTES + INST_BAG); + uint32_t bagFirst = dvxGetLe16(rec + INST_BAG); + uint32_t bagLast = dvxGetLe16(rec + INST_BYTES + INST_BAG); ZoneT global; if (bagFirst > bagLast || bagLast >= h->count[H_IBAG]) { @@ -502,13 +508,17 @@ static int32_t instrumentRegions(const HydraT *h, const WaveBankT *bank, uint32_ } -static uint32_t le16(const uint8_t *p) { - return (uint32_t)p[0] | ((uint32_t)p[1] << 8); -} +// path opened for waveBankRead, or the file already open when it is path +// (one is kept until waveBankClose). +static FILE *readFile(const char *path) { + if (sReadFp && strcmp(sReadPath, path) == 0) { + return sReadFp; + } - -static uint32_t le32(const uint8_t *p) { - return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); + waveBankClose(); + sReadFp = fopen(path, "rb"); + snprintf(sReadPath, sizeof(sReadPath), "%s", path); + return sReadFp; } @@ -553,11 +563,11 @@ static int32_t readSamples(const HydraT *h, WaveBankT *bank) { if (h->sf1) { const uint8_t *rec = h->at[H_SHDR] + i * SHDR1_BYTES; - rom = rom && le32(rec + SHDR1_START) != 0; - s->start = le32(rec + SHDR1_START); - s->end = le32(rec + SHDR1_END); - s->loopStart = le32(rec + SHDR1_LOOP_START) + SF1_LOOP_START_ADD; - s->loopEnd = le32(rec + SHDR1_LOOP_END) + SF1_LOOP_END_ADD; + rom = rom && dvxGetLe32(rec + SHDR1_START) != 0; + s->start = dvxGetLe32(rec + SHDR1_START); + s->end = dvxGetLe32(rec + SHDR1_END); + s->loopStart = dvxGetLe32(rec + SHDR1_LOOP_START) + SF1_LOOP_START_ADD; + s->loopEnd = dvxGetLe32(rec + SHDR1_LOOP_END) + SF1_LOOP_END_ADD; s->rate = SF1_RATE; s->rootKey = SF1_ROOT; s->correction = 0; @@ -565,14 +575,14 @@ static int32_t readSamples(const HydraT *h, WaveBankT *bank) { } else { const uint8_t *rec = h->at[H_SHDR] + i * SHDR_BYTES; - s->start = le32(rec + SHDR_START); - s->end = le32(rec + SHDR_END); - s->loopStart = le32(rec + SHDR_LOOP_START); - s->loopEnd = le32(rec + SHDR_LOOP_END); - s->rate = le32(rec + SHDR_RATE); + s->start = dvxGetLe32(rec + SHDR_START); + s->end = dvxGetLe32(rec + SHDR_END); + s->loopStart = dvxGetLe32(rec + SHDR_LOOP_START); + s->loopEnd = dvxGetLe32(rec + SHDR_LOOP_END); + s->rate = dvxGetLe32(rec + SHDR_RATE); s->rootKey = rec[SHDR_PITCH] > MIDI_TOP ? UNPITCHED_ROOT : rec[SHDR_PITCH]; s->correction = (int8_t)rec[SHDR_CORRECTION]; - s->type = (uint16_t)le16(rec + SHDR_TYPE); + s->type = dvxGetLe16(rec + SHDR_TYPE); } if (!(s->type & SF_SAMPLE_ROM) && (s->start > s->end || s->end > bank->dataFrames)) { @@ -766,7 +776,7 @@ static int32_t sf2From(SourceT *src, WaveBankT *bank) { return MUSIC_ERR_BANK; } - uint32_t end = RIFF_HEADER_BYTES + le32(head + RIFF_ID_BYTES); + uint32_t end = RIFF_HEADER_BYTES + dvxGetLe32(head + RIFF_ID_BYTES); end = end < src->size ? end : src->size; @@ -778,7 +788,7 @@ static int32_t sf2From(SourceT *src, WaveBankT *bank) { return MUSIC_ERR_BANK; } - len = le32(list + RIFF_ID_BYTES); + len = dvxGetLe32(list + RIFF_ID_BYTES); if (len > end - at - RIFF_HEADER_BYTES) { return MUSIC_ERR_BANK; @@ -797,7 +807,7 @@ static int32_t sf2From(SourceT *src, WaveBankT *bank) { return MUSIC_ERR_BANK; } - ckLen = le32(ck + RIFF_ID_BYTES); + ckLen = dvxGetLe32(ck + RIFF_ID_BYTES); if (ckLen > bodyEnd - sub - RIFF_HEADER_BYTES) { return MUSIC_ERR_BANK; @@ -806,7 +816,7 @@ static int32_t sf2From(SourceT *src, WaveBankT *bank) { if (memcmp(list + RIFF_HEADER_BYTES, "INFO", RIFF_TYPE_BYTES) == 0 && memcmp(ck, "ifil", RIFF_ID_BYTES) == 0 && ckLen >= IFIL_BYTES) { uint8_t ver[IFIL_BYTES]; - version = srcRead(src, sub + RIFF_HEADER_BYTES, ver, sizeof(ver)) ? le16(ver) : 0; + version = srcRead(src, sub + RIFF_HEADER_BYTES, ver, sizeof(ver)) ? dvxGetLe16(ver) : 0; } else if (memcmp(list + RIFF_HEADER_BYTES, "sdta", RIFF_TYPE_BYTES) == 0 && memcmp(ck, "smpl", RIFF_ID_BYTES) == 0) { bank->dataOffset = sub + RIFF_HEADER_BYTES; bank->dataFrames = ckLen / SAMPLE_BYTES; @@ -908,16 +918,12 @@ static bool sourcesRead(const WaveBankT *bank, uint32_t first, uint32_t frames, return false; } - FILE *f = fopen(bank->files[src->file], "rb"); + FILE *f = readFile(bank->files[src->file]); long at = (long)src->offset + (long)(from - src->first) * (long)bytes; bool ok = f && fseek(f, at, SEEK_SET) == 0 && fread(raw, bytes, count, f) == count; - if (f) { - fclose(f); - } - for (uint32_t k = 0; ok && k < count; k++) { - uint32_t v = bytes == SAMPLE_BYTES ? le16(raw + k * SAMPLE_BYTES) : (uint32_t)raw[k] << BYTE_BITS; + uint32_t v = bytes == SAMPLE_BYTES ? dvxGetLe16(raw + k * SAMPLE_BYTES) : (uint32_t)raw[k] << BYTE_BITS; out[from - first + k] = (int16_t)(src->format & WAVE_SRC_UNSIGNED ? v ^ SIGN_FLIP : v); } @@ -947,6 +953,16 @@ static bool srcRead(SourceT *src, uint32_t offset, void *out, uint32_t len) { } +void waveBankClose(void) { + if (sReadFp) { + fclose(sReadFp); + } + + sReadFp = NULL; + sReadPath[0] = '\0'; +} + + void waveBankFree(WaveBankT *bank) { free(bank->presets); free(bank->regions); @@ -970,25 +986,20 @@ bool waveBankRead(const WaveBankT *bank, uint32_t first, uint32_t frames, int16_ if (bank->memory) { for (uint32_t i = 0; i < frames; i++) { - out[i] = (int16_t)le16(bank->memory + offset + i * SAMPLE_BYTES); + out[i] = (int16_t)dvxGetLe16(bank->memory + offset + i * SAMPLE_BYTES); } return true; } - FILE *f = fopen(bank->path, "rb"); - bool ok = f && fseek(f, (long)offset, SEEK_SET) == 0 && fread(out, SAMPLE_BYTES, frames, f) == frames; + FILE *f = readFile(bank->path); - if (f) { - fclose(f); - } - - return ok; + return f && fseek(f, (long)offset, SEEK_SET) == 0 && fread(out, SAMPLE_BYTES, frames, f) == frames; } // Whether generators first .. last - 1 hold oper (a zone's instrument or // sample: its last generator, by the specification). static bool zoneHas(const HydraT *h, int32_t genArray, uint32_t first, uint32_t last, uint32_t oper) { - return last > first && le16(h->at[genArray] + (last - 1) * GEN_BYTES + GEN_OPER) == oper; + return last > first && dvxGetLe16(h->at[genArray] + (last - 1) * GEN_BYTES + GEN_OPER) == oper; } diff --git a/src/libs/kpunch/music/musicSmf.c b/src/libs/kpunch/music/musicSmf.c index 8b4ade1d..837eec02 100644 --- a/src/libs/kpunch/music/musicSmf.c +++ b/src/libs/kpunch/music/musicSmf.c @@ -44,6 +44,7 @@ // recordings, the events put into a byte array by midiPut.... #include "musicInt.h" +#include "dvxEndian.h" #include "stb_ds.h" #include @@ -71,7 +72,6 @@ #define META_END_OF_TRACK 0x2F #define META_TEMPO_BYTES 3 #define TEMPO_DEFAULT_US 500000 // 120 beats a minute -#define US_PER_SEC 1000000ULL #define BYTE_BITS 8 #define BYTE_MASK 0xFF #define CHUNK_LEN_BYTES 4 @@ -99,8 +99,6 @@ typedef struct { // Prototypes // ============================================================ -static uint32_t be16(const uint8_t *p); -static uint32_t be32(const uint8_t *p); static bool findSmf(const uint8_t *data, uint32_t size, const uint8_t **smf, uint32_t *smfSize); static bool idIs(const uint8_t *p, const char *id); int32_t midiFileBuild(const uint8_t *trk, uint32_t trkLen, uint32_t division, uint8_t **out, uint32_t *size); @@ -117,14 +115,6 @@ static uint32_t readTrack(SmfCursorT c, uint32_t track, RawEventT **raw, uint8_t static bool readVlq(SmfCursorT *c, uint32_t *value); -static uint32_t be16(const uint8_t *p) { - return ((uint32_t)p[0] << BYTE_BITS) | p[1]; -} - - -static uint32_t be32(const uint8_t *p) { - return (be16(p) << (BYTE_BITS * 2)) | be16(p + 2); -} // The SMF inside data: the data itself, or a RIFF RMID file's "data" @@ -143,7 +133,7 @@ static bool findSmf(const uint8_t *data, uint32_t size, const uint8_t **smf, uin uint32_t at = RIFF_HEADER_BYTES; while (at + CHUNK_HEADER_BYTES <= size) { - uint32_t len = data[at + 4] | ((uint32_t)data[at + 5] << BYTE_BITS) | ((uint32_t)data[at + 6] << (BYTE_BITS * 2)) | ((uint32_t)data[at + 7] << (BYTE_BITS * 3)); + uint32_t len = dvxGetLe32(data + at + CHUNK_ID_BYTES); if (idIs(data + at, "data")) { uint32_t avail = size - at - CHUNK_HEADER_BYTES; @@ -223,14 +213,14 @@ int32_t midiParse(const uint8_t *data, uint32_t size, MidiFileT *out) { return MUSIC_ERR_FORMAT; } - uint32_t hdrLen = be32(smf + CHUNK_ID_BYTES); + uint32_t hdrLen = dvxGetBe32(smf + CHUNK_ID_BYTES); if (hdrLen < SMF_HEADER_BYTES || smfSize < CHUNK_HEADER_BYTES + SMF_HEADER_BYTES) { return MUSIC_ERR_CORRUPT; } - uint32_t type = be16(smf + CHUNK_HEADER_BYTES); - uint32_t division = be16(smf + CHUNK_HEADER_BYTES + 4); + uint32_t type = dvxGetBe16(smf + CHUNK_HEADER_BYTES); + uint32_t division = dvxGetBe16(smf + CHUNK_HEADER_BYTES + 4); if (type > SMF_TYPE_MAX) { return MUSIC_ERR_FORMAT; @@ -258,7 +248,7 @@ int32_t midiParse(const uint8_t *data, uint32_t size, MidiFileT *out) { uint32_t endTick = 0; while (at + CHUNK_HEADER_BYTES <= smfSize) { - uint32_t len = be32(smf + at + CHUNK_ID_BYTES); + uint32_t len = dvxGetBe32(smf + at + CHUNK_ID_BYTES); uint32_t avail = smfSize - at - CHUNK_HEADER_BYTES; if (len > avail) { diff --git a/src/libs/kpunch/music/musicSound.c b/src/libs/kpunch/music/musicSound.c index 43d15666..ff394ad1 100644 --- a/src/libs/kpunch/music/musicSound.c +++ b/src/libs/kpunch/music/musicSound.c @@ -27,9 +27,12 @@ // its first SOUND_HEAD_MAX bytes (audioWavParse finds its format and // where its sound is), and the stream's fill reads the frames it needs, a // buffer at a time, at program time; a long recording needs no memory. -// A VOC file (its blocks may hold silences and continue one another), or -// a sound file given as bytes, is decoded into 16-bit frames in memory -// (audioDecode; not pinned, as nothing reads it at interrupt time). +// A VOC file named by its path is read as it plays too: its blocks (which +// may hold silences and continue one another) are walked once at loading +// to count its frames (audioVocNext, their headers only), then again as +// it plays. A sound file given as bytes is decoded into 16-bit frames in +// memory (audioDecode; not pinned, as nothing reads it at interrupt +// time). #include "musicInt.h" @@ -42,13 +45,18 @@ #define BYTE_BITS 8 struct SoundSrcT { - FILE *fp; // a WAV read as it plays (NULL: pcm) - int16_t *pcm; // decoded frames (NULL: fp) - uint8_t *raw; // fp's frames as the file has them, SOUND_READ_FRAMES at a time - AudioFormatT fmt; // as the file has it (bits 8 or 16) - uint32_t dataAt; // fp: where the frames start - uint32_t frames; - uint32_t pos; // the next frame read + FILE *fp; // a WAV or VOC read as it plays (NULL: pcm) + int16_t *pcm; // decoded frames (NULL: fp) + uint8_t *raw; // fp's frames as the file has them, SOUND_READ_FRAMES at a time + AudioFormatT fmt; // as the file has it (bits 8 or 16) + uint32_t dataAt; // a WAV: where the frames start + uint32_t frames; + uint32_t pos; // the next frame read + AudioVocT voc; // a VOC's blocks, walked as it plays + AudioVocPartE part; // ... the part being read + uint32_t partAt; // ... its next sample's byte (data) + uint32_t partLeft; // ... its samples not read yet + bool isVoc; }; // ============================================================ @@ -65,8 +73,12 @@ int32_t soundOpenMem(const uint8_t *data, uint32_t size, SoundSrcT **out uint32_t soundRate(const SoundSrcT *s); uint32_t soundRead(SoundSrcT *s, int16_t *out, uint32_t frames); static uint32_t soundReadFile(SoundSrcT *s, int16_t *out, uint32_t frames); +static uint32_t soundReadVoc(SoundSrcT *s, int16_t *out, uint32_t frames); void soundSeek(SoundSrcT *s, uint32_t frame); uint32_t soundTell(const SoundSrcT *s); +static int32_t vocOpen(SoundSrcT *s); +static uint32_t vocRead(void *ctx, uint32_t at, uint8_t *buf, uint32_t len); +static bool vocSkip(SoundSrcT *s, uint32_t samples); uint32_t soundChannels(const SoundSrcT *s) { @@ -152,6 +164,13 @@ int32_t soundOpenFile(const char *path, SoundSrcT **out) { rc = fread(head, 1, headBytes, s->fp) == headBytes ? audioWavParse(head, headBytes, (uint32_t)size, &s->fmt, &s->dataAt, &dataBytes) : AUDIO_ERR_CORRUPT; free(head); + // Not a WAV: a VOC, maybe + if (rc == AUDIO_ERR_FORMAT) { + s->voc.size = (uint32_t)size; + rc = vocOpen(s); + dataBytes = s->frames * soundFrameBytes(s); + } + if (rc != AUDIO_OK) { soundClose(s); return soundError(rc); @@ -201,6 +220,10 @@ uint32_t soundRate(const SoundSrcT *s) { uint32_t soundRead(SoundSrcT *s, int16_t *out, uint32_t frames) { uint32_t n = frames < s->frames - s->pos ? frames : s->frames - s->pos; + if (s->isVoc) { + return soundReadVoc(s, out, n); + } + if (s->fp) { return soundReadFile(s, out, n); } @@ -233,10 +256,58 @@ static uint32_t soundReadFile(SoundSrcT *s, int16_t *out, uint32_t frames) { } +// A VOC's parts in turn: sound read from the file, silence as 0s; a file +// that ends (or fails) before the frames its walk counted ends there. +static uint32_t soundReadVoc(SoundSrcT *s, int16_t *out, uint32_t frames) { + uint32_t chans = s->fmt.channels; + uint32_t want = frames * chans; + uint32_t done = 0; + uint32_t bytesOf = s->fmt.bits / BYTE_BITS; + + while (done < want) { + if (s->partLeft == 0) { + if (audioVocNext(&s->voc, &s->part, &s->partAt, &s->partLeft) != AUDIO_OK || s->part == AudioVocEndE) { + break; + } + + continue; + } + + uint32_t n = DVX_MIN(DVX_MIN(want - done, s->partLeft), SOUND_READ_FRAMES * chans); + + if (s->part == AudioVocSilenceE) { + memset(out + done, 0, n * sizeof(int16_t)); + } else if (vocRead(s, s->partAt, s->raw, n * bytesOf) == n * bytesOf) { + audioConvert(s->raw, out + done, n, s->fmt.bits); + s->partAt += n * bytesOf; + } else { + break; + } + + s->partLeft -= n; + done += n; + } + + if (done < want) { + s->frames = s->pos + done / chans; + } + + s->pos += done / chans; + return done / chans; +} + + +// A VOC's blocks are walked from the start to the frame. void soundSeek(SoundSrcT *s, uint32_t frame) { s->pos = frame < s->frames ? frame : s->frames; - if (s->fp) { + if (s->isVoc) { + s->partLeft = 0; + + if (audioVocBegin(&s->voc) != AUDIO_OK || !vocSkip(s, s->pos * s->fmt.channels)) { + s->frames = s->pos; + } + } else if (s->fp) { uint32_t at = s->dataAt + s->pos * soundFrameBytes(s); fseek(s->fp, (long)at, SEEK_SET); @@ -247,3 +318,69 @@ void soundSeek(SoundSrcT *s, uint32_t frame) { uint32_t soundTell(const SoundSrcT *s) { return s->pos; } + + +// A VOC file (s->fp, s->voc.size its bytes): its blocks walked once, their +// headers only, for its format and frame count. +static int32_t vocOpen(SoundSrcT *s) { + AudioVocPartE kind = AudioVocDataE; + uint32_t at; + uint32_t count; + uint32_t samples = 0; + + s->isVoc = true; + s->voc.read = vocRead; + s->voc.ctx = s; + + int32_t rc = audioVocBegin(&s->voc); + + while (rc == AUDIO_OK && (rc = audioVocNext(&s->voc, &kind, &at, &count)) == AUDIO_OK && kind != AudioVocEndE) { + samples += count; + } + + if (rc == AUDIO_OK && (!s->voc.haveFmt || samples < s->voc.fmt.channels)) { + rc = AUDIO_ERR_CORRUPT; + } + + s->fmt = s->voc.fmt; + s->frames = rc == AUDIO_OK ? samples / s->fmt.channels : 0; + return rc; +} + + +// audioVoc's read: from the file (ctx: the SoundSrcT). +static uint32_t vocRead(void *ctx, uint32_t at, uint8_t *buf, uint32_t len) { + SoundSrcT *s = (SoundSrcT *)ctx; + + if (fseek(s->fp, (long)at, SEEK_SET) != 0) { + return 0; + } + + return (uint32_t)fread(buf, 1, len, s->fp); +} + + +// Pass samples samples of a VOC walked from its start (the part reached +// left part-read); false when it ends first. +static bool vocSkip(SoundSrcT *s, uint32_t samples) { + while (samples > 0) { + if (s->partLeft == 0) { + if (audioVocNext(&s->voc, &s->part, &s->partAt, &s->partLeft) != AUDIO_OK || s->part == AudioVocEndE) { + return false; + } + + continue; + } + + uint32_t n = DVX_MIN(samples, s->partLeft); + + if (s->part == AudioVocDataE) { + s->partAt += n * (s->fmt.bits / BYTE_BITS); + } + + s->partLeft -= n; + samples -= n; + } + + return true; +} diff --git a/src/libs/kpunch/music/musicWaveLoad.c b/src/libs/kpunch/music/musicWaveLoad.c index 917ef442..2e521bb3 100644 --- a/src/libs/kpunch/music/musicWaveLoad.c +++ b/src/libs/kpunch/music/musicWaveLoad.c @@ -77,12 +77,14 @@ static int32_t loadAt(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, static int32_t loadOne(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, const uint8_t *need, uint32_t sample, const QualityT *q); static uint32_t loadRom(const WaveBankT *bank, WaveCacheT *cache, const uint8_t *need, uint32_t flags); static uint32_t markPreset(const WaveBankT *bank, const WavePresetT *preset, int32_t key, int32_t velocity, uint8_t *need); +static bool qualityFits(const WaveBankT *bank, const WaveCacheT *cache, const uint8_t *need, const QualityT *q); static bool qualityPlays(const QualityT *q, uint32_t flags); static uint32_t storedFrames(const WaveBankT *bank, uint32_t sample); static void unload(AudioCtxT *ctx, WaveCacheT *cache, uint32_t sample); void waveCacheFree(AudioCtxT *ctx, WaveCacheT *cache); int32_t waveCacheInit(WaveCacheT *cache, const WaveBankT *bank); int32_t waveLoadNeeded(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, const uint8_t *need, uint32_t flags, uint32_t *missing); +int32_t waveLoadStep(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, uint8_t *need, uint32_t flags, bool *more); void waveNeedsFile(const WaveBankT *bank, const MidiFileT *file, uint8_t *need); uint32_t waveNeedsPreset(const WaveBankT *bank, uint16_t bankNum, uint8_t program, uint8_t *need); @@ -243,6 +245,35 @@ static uint32_t markPreset(const WaveBankT *bank, const WavePresetT *preset, int // The chip plays samples at q's bits. +// The needed samples that are not in memory at quality q fit beside the +// ones that are (all of the synthesizer's memory else free: the most it +// could have). +static bool qualityFits(const WaveBankT *bank, const WaveCacheT *cache, const uint8_t *need, const QualityT *q) { + AudioWtInfoT info; + uint64_t want = 0; + uint64_t kept = 0; + + audioWtGetInfo(&info); + + for (uint32_t i = 0; i < cache->count; i++) { + const WaveLoadedT *ld = &cache->loaded[i]; + bool here = ld->addr != WAVE_NOT_LOADED && !(ld->addr & AUDIO_WT_ADDR_ROM) && ld->bits == q->bits && ld->shift == q->shift; + + if (!need[i] || (bank->samples[i].type & SF_SAMPLE_ROM)) { + continue; + } + + if (here) { + kept += (uint64_t)ld->frames * (q->bits / BYTE_BITS); + } else { + want += (uint64_t)(storedFrames(bank, i) >> q->shift) * (q->bits / BYTE_BITS); + } + } + + return kept + want <= info.memory; +} + + static bool qualityPlays(const QualityT *q, uint32_t flags) { return q->bits == BITS_16 || (flags & AUDIO_WT_8BIT) != 0; } @@ -330,12 +361,16 @@ int32_t waveLoadNeeded(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, last--; } + // A quality whose samples cannot fit even in an empty synthesizer is + // passed over (not read and loaded for nothing); the least is always + // tried (it leaves out what does not fit) for (int32_t qi = 0; qi <= last && rc == MUSIC_ERR_NO_DEVICE; qi++) { - if (qualityPlays(&sQualities[qi], flags)) { + if (qualityPlays(&sQualities[qi], flags) && (qi == last || qualityFits(bank, cache, need, &sQualities[qi]))) { rc = loadAt(ctx, bank, cache, need, &sQualities[qi], qi == last, missing); } } + waveBankClose(); *missing += romLeft; // None fitted even at the least @@ -343,6 +378,49 @@ int32_t waveLoadNeeded(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, } +// The ROM samples, then one missing sample at the best quality the chip +// plays that it fits at (room made of samples not needed; the loaded ones +// stay where they are, so nothing sounding moves). One that fits at none, +// or cannot be read, is no longer needed: MUSIC_ERR_NO_DEVICE (or the +// read's error), and its notes play on FM. +int32_t waveLoadStep(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, uint8_t *need, uint32_t flags, bool *more) { + int32_t next = -1; + bool left = false; + int32_t rc = MUSIC_ERR_NO_DEVICE; + + loadRom(bank, cache, need, flags); + + for (uint32_t i = 0; i < cache->count; i++) { + if (need[i] && !(bank->samples[i].type & SF_SAMPLE_ROM) && cache->loaded[i].addr == WAVE_NOT_LOADED) { + left = left || next >= 0; + next = next >= 0 ? next : (int32_t)i; + } + } + + *more = left; + + if (next < 0) { + return MUSIC_OK; + } + + cache->lastUse[next] = cache->songs; + + for (int32_t qi = 0; qi < QUALITY_COUNT && rc == MUSIC_ERR_NO_DEVICE; qi++) { + if (qualityPlays(&sQualities[qi], flags)) { + rc = loadOne(ctx, bank, cache, need, (uint32_t)next, &sQualities[qi]); + } + } + + waveBankClose(); + + if (rc != MUSIC_OK) { + need[next] = 0; + } + + return rc; +} + + void waveNeedsFile(const WaveBankT *bank, const MidiFileT *file, uint8_t *need) { uint8_t program[MUSIC_MIDI_CHANNELS]; uint8_t bankMsb[MUSIC_MIDI_CHANNELS]; diff --git a/src/libs/kpunch/music/musicWaveSoft.c b/src/libs/kpunch/music/musicWaveSoft.c index f77df3bd..98a61717 100644 --- a/src/libs/kpunch/music/musicWaveSoft.c +++ b/src/libs/kpunch/music/musicWaveSoft.c @@ -199,6 +199,7 @@ int32_t waveSoftLoad(WaveSoftT *soft, const WaveBankT *bank, WaveLoadedT *loaded soft->arena = grown; if (!waveBankRead(bank, s->start, frames, soft->arena + soft->frames)) { + waveBankClose(); return MUSIC_ERR_OPEN; } @@ -210,6 +211,7 @@ int32_t waveSoftLoad(WaveSoftT *soft, const WaveBankT *bank, WaveLoadedT *loaded soft->frames += frames; } + waveBankClose(); return MUSIC_OK; } diff --git a/src/libs/kpunch/music/musicXmp.c b/src/libs/kpunch/music/musicXmp.c index b092866f..b0487347 100644 --- a/src/libs/kpunch/music/musicXmp.c +++ b/src/libs/kpunch/music/musicXmp.c @@ -22,6 +22,9 @@ // musicXmp.c -- tracker modules (MOD, S3M, XM, IT) through libxmp-lite // +// The player module, xmp.drv, with libxmp-lite (most of what music.lib +// would otherwise hold): music.lib loads it when a module is opened +// (musicXmpDrv.c) and calls it through the descriptor it exports. // libxmp-lite (thirdparty/libxmp-lite, with DVX's patches) loads the // module from memory and renders 16-bit frames at the stream's rate with // linear interpolation; the stream's fill function calls xmpRender at @@ -48,7 +51,7 @@ struct XmpPlayerT { xmp_context ctx; uint32_t channels; uint32_t rate; - int32_t voices; // libxmp's voices before xmpHwOn + int32_t voices; // libxmp's voices before playerHwOn XmpHwFnT hwFn; // NULL: rendering void *hwUser; char title[MUSIC_TITLE_MAX]; @@ -58,31 +61,54 @@ struct XmpPlayerT { // Prototypes // ============================================================ -uint32_t xmpBidiAdjust(XmpPlayerT *player); -uint32_t xmpChannels(XmpPlayerT *player); -void xmpClose(XmpPlayerT *player); -bool xmpHwFrame(XmpPlayerT *player, bool loop, uint32_t *us); -static void xmpHwHook(void *user, const struct xmp_hw_voice *v); -void xmpHwOff(XmpPlayerT *player); -bool xmpHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user); -uint32_t xmpLength(XmpPlayerT *player); -int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out); -uint32_t xmpPosition(XmpPlayerT *player); -bool xmpRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop); -void xmpRestart(XmpPlayerT *player); -bool xmpSample(XmpPlayerT *player, uint32_t i, XmpSampleT *out); -uint32_t xmpSampleCount(XmpPlayerT *player); -void xmpSeek(XmpPlayerT *player, uint32_t ms); -static bool xmpStart(XmpPlayerT *player); -const char *xmpTitle(XmpPlayerT *player); +static uint32_t playerBidiAdjust(XmpPlayerT *player); +static uint32_t playerChannels(XmpPlayerT *player); +static void playerClose(XmpPlayerT *player); +static bool playerHwFrame(XmpPlayerT *player, bool loop, uint32_t *us); +static void playerHwHook(void *user, const struct xmp_hw_voice *v); +static void playerHwOff(XmpPlayerT *player); +static bool playerHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user); +static bool playerIsModule(const char *path); +static uint32_t playerLength(XmpPlayerT *player); +static void playerLoadHook(void *user); +static int32_t playerOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out); +static uint32_t playerPosition(XmpPlayerT *player); +static bool playerRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop); +static void playerRestart(XmpPlayerT *player); +static bool playerSample(XmpPlayerT *player, uint32_t i, XmpSampleT *out); +static uint32_t playerSampleCount(XmpPlayerT *player); +static void playerSeek(XmpPlayerT *player, uint32_t ms); +static bool playerStart(XmpPlayerT *player); +static const char *playerTitle(const XmpPlayerT *player); + +const XmpDriverT musicXmpDriver = { + DVX_DRIVER_HDR(XmpDriverT, "xmp", 0), + .bidiAdjust = playerBidiAdjust, + .channels = playerChannels, + .close = playerClose, + .hwFrame = playerHwFrame, + .hwOff = playerHwOff, + .hwOn = playerHwOn, + .isModule = playerIsModule, + .length = playerLength, + .open = playerOpen, + .position = playerPosition, + .render = playerRender, + .restart = playerRestart, + .sample = playerSample, + .sampleCount = playerSampleCount, + .seek = playerSeek, + .title = playerTitle +}; -uint32_t xmpBidiAdjust(XmpPlayerT *player) { + +static uint32_t playerBidiAdjust(XmpPlayerT *player) { return (uint32_t)xmp_hw_bidir_adjust(player->ctx); } -uint32_t xmpChannels(XmpPlayerT *player) { +static uint32_t playerChannels(XmpPlayerT *player) { struct xmp_module_info mi; xmp_get_module_info(player->ctx, &mi); @@ -90,7 +116,7 @@ uint32_t xmpChannels(XmpPlayerT *player) { } -void xmpClose(XmpPlayerT *player) { +static void playerClose(XmpPlayerT *player) { if (!player) { return; } @@ -102,7 +128,7 @@ void xmpClose(XmpPlayerT *player) { } -bool xmpHwFrame(XmpPlayerT *player, bool loop, uint32_t *us) { +static bool playerHwFrame(XmpPlayerT *player, bool loop, uint32_t *us) { struct xmp_frame_info fi; int rc = xmp_play_frame(player->ctx); @@ -114,7 +140,7 @@ bool xmpHwFrame(XmpPlayerT *player, bool loop, uint32_t *us) { // libxmp's voice as XmpHwVoiceT, to the music library's function. -static void xmpHwHook(void *user, const struct xmp_hw_voice *v) { +static void playerHwHook(void *user, const struct xmp_hw_voice *v) { XmpPlayerT *p = (XmpPlayerT *)user; XmpHwVoiceT hv; @@ -135,43 +161,48 @@ static void xmpHwHook(void *user, const struct xmp_hw_voice *v) { // Back to rendering, where the song was. -void xmpHwOff(XmpPlayerT *player) { - uint32_t ms = xmpPosition(player); +static void playerHwOff(XmpPlayerT *player) { + uint32_t ms = playerPosition(player); player->hwFn = NULL; xmp_set_hw_voices(player->ctx, NULL, NULL); xmp_end_player(player->ctx); xmp_set_player(player->ctx, XMP_PLAYER_VOICES, player->voices); - xmpStart(player); - xmpSeek(player, ms); + playerStart(player); + playerSeek(player, ms); } // Started again (where the song was) with the voices and the hook: the // voice count is set before libxmp starts playing. -bool xmpHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user) { - uint32_t ms = xmpPosition(player); +static bool playerHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user) { + uint32_t ms = playerPosition(player); player->hwFn = fn; player->hwUser = user; xmp_end_player(player->ctx); xmp_set_player(player->ctx, XMP_PLAYER_VOICES, (int)voices); - xmp_set_hw_voices(player->ctx, xmpHwHook, player); + xmp_set_hw_voices(player->ctx, playerHwHook, player); - if (!xmpStart(player)) { - xmpHwOff(player); + if (!playerStart(player)) { + playerHwOff(player); return false; } // Nothing is mixed: no interpolation spares libxmp copying each // loop's ends at every turn of it xmp_set_player(player->ctx, XMP_PLAYER_INTERP, XMP_INTERP_NEAREST); - xmpSeek(player, ms); + playerSeek(player, ms); return true; } -uint32_t xmpLength(XmpPlayerT *player) { +static bool playerIsModule(const char *path) { + return xmp_test_module(path, NULL) == 0; +} + + +static uint32_t playerLength(XmpPlayerT *player) { struct xmp_frame_info fi; xmp_get_frame_info(player->ctx, &fi); @@ -179,7 +210,13 @@ uint32_t xmpLength(XmpPlayerT *player) { } -int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out) { +// libxmp's load hook (xmp_set_load_hook, DVX patch): before each sample. +static void playerLoadHook(void *user) { + musicGiveWay((uint64_t *)user); +} + + +static int32_t playerOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out) { *out = NULL; XmpPlayerT *p = (XmpPlayerT *)calloc(1, sizeof(XmpPlayerT)); @@ -199,8 +236,14 @@ int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t chan xmp_set_player(p->ctx, XMP_PLAYER_DEFPAN, XMP_DEFAULT_PAN); + uint64_t last = platformMicros(); + + xmp_set_load_hook(p->ctx, playerLoadHook, &last); + int rc = xmp_load_module_from_memory(p->ctx, data, (long)size); + xmp_set_load_hook(p->ctx, NULL, NULL); + if (rc != 0) { xmp_free_context(p->ctx); free(p); @@ -212,7 +255,7 @@ int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t chan return rc == -XMP_ERROR_SYSTEM ? MUSIC_ERR_NO_MEMORY : MUSIC_ERR_CORRUPT; } - if (!xmpStart(p)) { + if (!playerStart(p)) { xmp_release_module(p->ctx); xmp_free_context(p->ctx); free(p); @@ -230,7 +273,7 @@ int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t chan } -uint32_t xmpPosition(XmpPlayerT *player) { +static uint32_t playerPosition(XmpPlayerT *player) { struct xmp_frame_info fi; xmp_get_frame_info(player->ctx, &fi); @@ -238,7 +281,7 @@ uint32_t xmpPosition(XmpPlayerT *player) { } -bool xmpRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop) { +static bool playerRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop) { int bytes = (int)(frames * player->channels * XMP_BYTES_16); int rc = xmp_play_buffer(player->ctx, out, bytes, loop ? XMP_LOOP_FOREVER : XMP_LOOP_ONCE); @@ -247,13 +290,13 @@ bool xmpRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop) { // Back to the start, with the loop count and the render buffer reset. -void xmpRestart(XmpPlayerT *player) { +static void playerRestart(XmpPlayerT *player) { xmp_restart_module(player->ctx); xmp_play_buffer(player->ctx, NULL, 0, 0); } -bool xmpSample(XmpPlayerT *player, uint32_t i, XmpSampleT *out) { +static bool playerSample(XmpPlayerT *player, uint32_t i, XmpSampleT *out) { struct xmp_module_info mi; xmp_get_module_info(player->ctx, &mi); @@ -280,7 +323,7 @@ bool xmpSample(XmpPlayerT *player, uint32_t i, XmpSampleT *out) { } -uint32_t xmpSampleCount(XmpPlayerT *player) { +static uint32_t playerSampleCount(XmpPlayerT *player) { struct xmp_module_info mi; xmp_get_module_info(player->ctx, &mi); @@ -288,7 +331,7 @@ uint32_t xmpSampleCount(XmpPlayerT *player) { } -void xmpSeek(XmpPlayerT *player, uint32_t ms) { +static void playerSeek(XmpPlayerT *player, uint32_t ms) { xmp_seek_time(player->ctx, (int)ms); xmp_play_buffer(player->ctx, NULL, 0, 0); } @@ -296,7 +339,7 @@ void xmpSeek(XmpPlayerT *player, uint32_t ms) { // The player started at the stream's rate and format, linear // interpolation (what xmp_start_player resets is set again). -static bool xmpStart(XmpPlayerT *player) { +static bool playerStart(XmpPlayerT *player) { if (xmp_start_player(player->ctx, (int)player->rate, player->channels == 1 ? XMP_FORMAT_MONO : 0) != 0) { return false; } @@ -306,6 +349,6 @@ static bool xmpStart(XmpPlayerT *player) { } -const char *xmpTitle(XmpPlayerT *player) { +static const char *playerTitle(const XmpPlayerT *player) { return player->title; } diff --git a/src/libs/kpunch/music/musicXmpDrv.c b/src/libs/kpunch/music/musicXmpDrv.c new file mode 100644 index 00000000..6260c31c --- /dev/null +++ b/src/libs/kpunch/music/musicXmpDrv.c @@ -0,0 +1,226 @@ +// 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. + +// musicXmpDrv.c -- tracker modules: the player module, loaded on demand +// +// libxmp-lite and musicXmp.c, most of what the library would otherwise +// hold, are a module of their own (xmp.drv in MUSIC_XMP_DIR, a driver +// module: dvxDrv.h) that exports one XmpDriverT. The xmp* calls the rest +// of the library makes are here, through it: the first xmpIsModule or +// xmpOpen loads it, and xmpIdle unloads it once no player is open and it +// has not been used for MUSIC_XMP_KEEP_SEC. A build that links +// musicXmp.c in (the host tests) has the descriptor already. A player +// exists only while the module is loaded, so its calls need no check. + +#include "musicInt.h" +#include "dvxPlat.h" +#include "stb_ds_wrap.h" + +#include + +#define XMP_KEEP_US (MUSIC_XMP_KEEP_SEC * US_PER_SEC) + +// ============================================================ +// Prototypes +// ============================================================ + +static bool driverValid(const DvxDriverHeaderT *desc, int32_t kind); +uint32_t xmpBidiAdjust(XmpPlayerT *player); +uint32_t xmpChannels(XmpPlayerT *player); +void xmpClose(XmpPlayerT *player); +static const XmpDriverT *xmpDriver(void); +bool xmpHwFrame(XmpPlayerT *player, bool loop, uint32_t *us); +void xmpHwOff(XmpPlayerT *player); +bool xmpHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user); +void xmpIdle(void); +bool xmpIsModule(const char *path); +uint32_t xmpLength(XmpPlayerT *player); +int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out); +uint32_t xmpPosition(XmpPlayerT *player); +bool xmpRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop); +void xmpRestart(XmpPlayerT *player); +bool xmpSample(XmpPlayerT *player, uint32_t i, XmpSampleT *out); +uint32_t xmpSampleCount(XmpPlayerT *player); +void xmpSeek(XmpPlayerT *player, uint32_t ms); +const char *xmpTitle(const XmpPlayerT *player); +void xmpUnload(void); + +static const char *const sDrvSymbols[] = { DVX_SYM(MUSIC_XMP_SYMBOL) }; +static const uint32_t sDrvSizes[] = { sizeof(XmpDriverT) }; +static const DvxDriverClassT sDrvClass = { sDrvSymbols, sDrvSizes, 1, driverValid }; + +static DvxDriverModT sMod = { NULL, NULL, 0 }; // desc NULL: not loaded +static const XmpDriverT *sDrv = NULL; // sMod's descriptor +static int32_t sPlayers = 0; // open players +static uint64_t sLastUse = 0; // platformMicros of the last xmpIsModule / xmpOpen / xmpClose + + +static bool driverValid(const DvxDriverHeaderT *desc, int32_t kind) { + (void)kind; + + const XmpDriverT *d = (const XmpDriverT *)desc; + + return d->bidiAdjust && d->channels && d->close && d->hwFrame && d->hwOff && d->hwOn && d->isModule && d->length && d->open && d->position && d->render && d->restart && d->sample && d->sampleCount && d->seek && d->title; +} + + +uint32_t xmpBidiAdjust(XmpPlayerT *player) { + return sDrv->bidiAdjust(player); +} + + +uint32_t xmpChannels(XmpPlayerT *player) { + return sDrv->channels(player); +} + + +void xmpClose(XmpPlayerT *player) { + if (!player) { + return; + } + + sDrv->close(player); + sPlayers--; + sLastUse = platformMicros(); +} + + +// The player module, loaded if it is not (NULL: it is not there). +static const XmpDriverT *xmpDriver(void) { + sLastUse = platformMicros(); + + if (sDrv) { + return sDrv; + } + + const DvxDriverHeaderT *linked = (const DvxDriverHeaderT *)dlsym(NULL, sDrvSymbols[0]); + + if (linked) { + sMod.desc = linked; + } else { + DvxDriverModT *mods = dvxDriversLoad(MUSIC_XMP_DIR, &sDrvClass); + + if (arrlen(mods) > 0) { + sMod = dvxDriverKeep(mods, 0); + } + + dvxDriversFree(mods); + } + + sDrv = (const XmpDriverT *)sMod.desc; + return sDrv; +} + + +bool xmpHwFrame(XmpPlayerT *player, bool loop, uint32_t *us) { + return sDrv->hwFrame(player, loop, us); +} + + +void xmpHwOff(XmpPlayerT *player) { + sDrv->hwOff(player); +} + + +bool xmpHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user) { + return sDrv->hwOn(player, voices, fn, user); +} + + +void xmpIdle(void) { + if (sPlayers == 0 && platformMicros() - sLastUse >= XMP_KEEP_US) { + xmpUnload(); + } +} + + +bool xmpIsModule(const char *path) { + const XmpDriverT *drv = xmpDriver(); + + return drv && drv->isModule(path); +} + + +uint32_t xmpLength(XmpPlayerT *player) { + return sDrv->length(player); +} + + +int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out) { + const XmpDriverT *drv = xmpDriver(); + + *out = NULL; + + if (!drv) { + return MUSIC_ERR_FORMAT; + } + + int32_t rc = drv->open(data, size, rate, channels, out); + + if (rc == MUSIC_OK) { + sPlayers++; + } + + return rc; +} + + +uint32_t xmpPosition(XmpPlayerT *player) { + return sDrv->position(player); +} + + +bool xmpRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop) { + return sDrv->render(player, out, frames, loop); +} + + +void xmpRestart(XmpPlayerT *player) { + sDrv->restart(player); +} + + +bool xmpSample(XmpPlayerT *player, uint32_t i, XmpSampleT *out) { + return sDrv->sample(player, i, out); +} + + +uint32_t xmpSampleCount(XmpPlayerT *player) { + return sDrv->sampleCount(player); +} + + +void xmpSeek(XmpPlayerT *player, uint32_t ms) { + sDrv->seek(player, ms); +} + + +const char *xmpTitle(const XmpPlayerT *player) { + return sDrv->title(player); +} + + +// Unloaded now (a linked-in player stays; it is found again). +void xmpUnload(void) { + dvxDriverUnload(&sMod); + sDrv = NULL; +} diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/DVX-VERSION.md b/src/libs/kpunch/music/thirdparty/libxmp-lite/DVX-VERSION.md index f33df121..3554a61a 100644 --- a/src/libs/kpunch/music/thirdparty/libxmp-lite/DVX-VERSION.md +++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/DVX-VERSION.md @@ -8,7 +8,7 @@ | Tarball SHA-256 | e199da3f7552f5ac688a091b8d28ea3c9ffd2beb845d15aac17d4d264f851d71 (262,671 bytes) | | Files kept | src/*.c, src/*.h, src/loaders/*.c, src/loaders/*.h, include/libxmp-lite/xmp.h (as include/xmp.h), README (it holds the licence), test/test.it and test/test.xm (used by the host tests) -- byte for byte as in the tarball but for the patches below. Not kept: the build files (configure, CMake, the Makefiles, including src/Makefile and src/loaders/Makefile), jni, the rest of test/. | | Licence | MIT ("Extended Module Player Lite, Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr", in README). Keep the notice with the source; reproduced in the music library's developer help (topic lib.music) for binaries. | -| Patches | dvx-patches/0001-hardware-voices.patch (see Patches) | +| Patches | dvx-patches/0001-hardware-voices.patch, dvx-patches/0002-load-hook.patch (see Patches) | ## Build options @@ -51,6 +51,12 @@ patch" in the sources. frames the loaded module's ping-pong loops are shortened by on the way back (Impulse Tracker's 1). music.lib (musicModHw.c) plays modules on a wavetable synthesizer's voices that way. +- `0002-load-hook.patch` -- `xmp_set_load_hook(ctx, fn, user)` + (include/xmp.h, src/control.c, src/common.h: `load_hook` and + `load_user` in `struct module_data`): src/loaders/sample.c's + `libxmp_load_sample` calls the hook before each sample loads. + music.lib (musicXmp.c) gives way to DVX's other tasks from it while a + big module loads. ## Updating diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/dvx-patches/0002-load-hook.patch b/src/libs/kpunch/music/thirdparty/libxmp-lite/dvx-patches/0002-load-hook.patch new file mode 100644 index 00000000..97454f5c --- /dev/null +++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/dvx-patches/0002-load-hook.patch @@ -0,0 +1,66 @@ +--- a/include/xmp.h ++++ b/include/xmp.h +@@ -368,6 +368,10 @@ + + typedef void (*xmp_hw_voice_fn)(void *, const struct xmp_hw_voice *); + ++/* DVX patch (load hook): called before each sample of a module loads, so ++ * a caller can let other work run while a big module loads. */ ++typedef void (*xmp_load_hook_fn)(void *); ++ + struct xmp_callbacks { + unsigned long (*read_func)(void *dest, unsigned long len, + unsigned long nmemb, void *priv); +@@ -424,6 +428,7 @@ + LIBXMP_EXPORT int xmp_set_player (xmp_context, int, int); + LIBXMP_EXPORT int xmp_set_hw_voices (xmp_context, xmp_hw_voice_fn, void *); /* DVX patch */ + LIBXMP_EXPORT int xmp_hw_bidir_adjust (xmp_context); /* DVX patch */ ++LIBXMP_EXPORT int xmp_set_load_hook (xmp_context, xmp_load_hook_fn, void *); /* DVX patch */ + LIBXMP_EXPORT int xmp_get_player (xmp_context, int); + LIBXMP_EXPORT int xmp_set_instrument_path (xmp_context, const char *); + +--- a/src/common.h ++++ b/src/common.h +@@ -552,6 +552,8 @@ + uint8 **scan_cnt; /* scan counters */ + struct extra_sample_data *xtra; + struct midi_macro_data *midi; ++ xmp_load_hook_fn load_hook; /* DVX patch: before each sample loads (NULL: none) */ ++ void *load_user; + int compare_vblank; + }; + +--- a/src/control.c ++++ b/src/control.c +@@ -423,6 +423,17 @@ + return 0; + } + ++/* DVX patch: the load hook (see xmp.h); a NULL fn is none. */ ++int xmp_set_load_hook(xmp_context opaque, xmp_load_hook_fn fn, void *user) ++{ ++ struct context_data *ctx = (struct context_data *)opaque; ++ struct module_data *m = &ctx->m; ++ ++ m->load_hook = fn; ++ m->load_user = user; ++ return 0; ++} ++ + /* DVX patch: the frames a ping-pong loop's way back is shortened by in + * the loaded module (Impulse Tracker's 1, else 0): a loop of n frames + * takes 2n minus this to play there and back. */ +--- a/src/loaders/sample.c ++++ b/src/loaders/sample.c +@@ -186,6 +186,11 @@ + int framelen; + int bytelen, extralen, i; + ++ /* DVX patch: the load hook (xmp_set_load_hook) */ ++ if (m->load_hook) { ++ m->load_hook(m->load_user); ++ } ++ + #ifndef LIBXMP_CORE_PLAYER + /* Adlib FM patches */ + if (flags & SAMPLE_FLAG_ADLIB) { diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/include/xmp.h b/src/libs/kpunch/music/thirdparty/libxmp-lite/include/xmp.h index 65d7fef4..ac728eb6 100644 --- a/src/libs/kpunch/music/thirdparty/libxmp-lite/include/xmp.h +++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/include/xmp.h @@ -368,6 +368,10 @@ struct xmp_hw_voice { typedef void (*xmp_hw_voice_fn)(void *, const struct xmp_hw_voice *); +/* DVX patch (load hook): called before each sample of a module loads, so + * a caller can let other work run while a big module loads. */ +typedef void (*xmp_load_hook_fn)(void *); + struct xmp_callbacks { unsigned long (*read_func)(void *dest, unsigned long len, unsigned long nmemb, void *priv); @@ -424,6 +428,7 @@ LIBXMP_EXPORT int xmp_channel_vol (xmp_context, int, int); LIBXMP_EXPORT int xmp_set_player (xmp_context, int, int); LIBXMP_EXPORT int xmp_set_hw_voices (xmp_context, xmp_hw_voice_fn, void *); /* DVX patch */ LIBXMP_EXPORT int xmp_hw_bidir_adjust (xmp_context); /* DVX patch */ +LIBXMP_EXPORT int xmp_set_load_hook (xmp_context, xmp_load_hook_fn, void *); /* DVX patch */ LIBXMP_EXPORT int xmp_get_player (xmp_context, int); LIBXMP_EXPORT int xmp_set_instrument_path (xmp_context, const char *); diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/common.h b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/common.h index 042bafaf..3139b31a 100644 --- a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/common.h +++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/common.h @@ -552,6 +552,8 @@ struct module_data { uint8 **scan_cnt; /* scan counters */ struct extra_sample_data *xtra; struct midi_macro_data *midi; + xmp_load_hook_fn load_hook; /* DVX patch: before each sample loads (NULL: none) */ + void *load_user; int compare_vblank; }; diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/control.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/control.c index 9412de9b..6c6e5289 100644 --- a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/control.c +++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/control.c @@ -423,6 +423,17 @@ int xmp_set_hw_voices(xmp_context opaque, xmp_hw_voice_fn fn, void *user) return 0; } +/* DVX patch: the load hook (see xmp.h); a NULL fn is none. */ +int xmp_set_load_hook(xmp_context opaque, xmp_load_hook_fn fn, void *user) +{ + struct context_data *ctx = (struct context_data *)opaque; + struct module_data *m = &ctx->m; + + m->load_hook = fn; + m->load_user = user; + return 0; +} + /* DVX patch: the frames a ping-pong loop's way back is shortened by in * the loaded module (Impulse Tracker's 1, else 0): a loop of n frames * takes 2n minus this to play there and back. */ diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/loaders/sample.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/loaders/sample.c index d7a4c0c9..786f5485 100644 --- a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/loaders/sample.c +++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/loaders/sample.c @@ -186,6 +186,11 @@ int libxmp_load_sample(struct module_data *m, HIO_HANDLE *f, int flags, struct x int framelen; int bytelen, extralen, i; + /* DVX patch: the load hook (xmp_set_load_hook) */ + if (m->load_hook) { + m->load_hook(m->load_user); + } + #ifndef LIBXMP_CORE_PLAYER /* Adlib FM patches */ if (flags & SAMPLE_FLAG_ADLIB) { diff --git a/src/libs/kpunch/pkg/Makefile b/src/libs/kpunch/pkg/Makefile index 58ece387..76b58e63 100644 --- a/src/libs/kpunch/pkg/Makefile +++ b/src/libs/kpunch/pkg/Makefile @@ -26,52 +26,82 @@ # writer, manifests, keys and Ed25519 signatures, .DPK files, the # installed-package records, the keyring, planning, and the stepwise # install and remove operations. miniz (inflate and -# deflate, no archive API) and Monocypher (with its Ed25519 file) are -# built in; SETUP.EXE and the package tool compile the same sources. +# deflate, no archive API) is built in; Ed25519 is the cryptography +# library's (secCurve.h). SETUP.EXE and the package tool compile the +# same sources, and crypto's with Monocypher. +# The ZIP writer and the key and signature making (deflate and Ed25519 +# signing) are writer/writer.drv, the package writer module pkg.lib loads +# when a package is made (pkgWriter.h, pkgWriterMod.c). # Also SYSTEM\DVXPKG.EXE, the package tool on DOS (pkgDosMain.c over # pkgCli.c), a program of its own like DVXRES.EXE. include ../../../common.mk MINIZ = thirdparty/miniz -MONO = thirdparty/monocypher/src -CFLAGS = $(DOS_CFLAGS) -I. -I.. -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty -I$(MINIZ) -I$(MONO) $(PKG_MINIZ_FLAGS) +MONO = ../crypto/thirdparty/monocypher/src +CFLAGS = $(DOS_CFLAGS) $(DXE_GC_CFLAGS) -I. -I.. -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty -I$(MINIZ) -I$(MONO) $(PKG_MINIZ_FLAGS) OBJDIR = ../../../../obj/pkg LIBSDIR = ../../../../bin/libs -SRCS = pkgBas.c pkgDb.c pkgDpk.c pkgFormat.c pkgIni.c pkgManifest.c pkgOp.c pkgPlan.c pkgSign.c pkgTrust.c pkgZip.c pkgZipWrite.c $(MINIZ)/miniz.c $(MONO)/monocypher.c $(MONO)/optional/monocypher-ed25519.c -OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(SRCS))) +# What the library, the writer module, and the tool share +CORE_SRCS = pkgDb.c pkgDpk.c pkgFormat.c pkgIni.c pkgManifest.c pkgOp.c pkgPlan.c pkgSign.c pkgTrust.c pkgZip.c pkgZipJob.c +THIRD_SRCS = $(MINIZ)/miniz.c +WRITER_SRCS = pkgSignMake.c pkgZipWrite.c +SRCS = pkgBas.c pkgWriterMod.c $(CORE_SRCS) $(THIRD_SRCS) +OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(SRCS))) +# pkg's own objects: what they reach of miniz is all pkg.lib keeps +# (DXE_GC) +OWN_OBJS = $(filter $(OBJDIR)/pkg%.o,$(OBJS)) +GC_OBJ = $(OBJDIR)/pkgGc.o TARGETDIR = $(LIBSDIR)/kpunch/pkg TARGET = $(TARGETDIR)/pkg.lib +# The writer module: its descriptor is its only root (DXE_GC), so it keeps +# what the writer reaches of miniz and exports nothing else +WRITERDIR = $(TARGETDIR)/writer +WRITER = $(WRITERDIR)/writer.drv +WRITER_DESC = $(OBJDIR)/pkgWriterDrv.o +WRITER_OBJS = $(WRITER_DESC) $(patsubst %.c,$(OBJDIR)/%.o,$(WRITER_SRCS) $(notdir $(THIRD_SRCS))) +WRITER_GC = $(OBJDIR)/writerGc.o + SYSTEMDIR = ../../../../bin/system TOOL = $(SYSTEMDIR)/DVXPKG.EXE TOOLOBJDIR = $(OBJDIR)/tool -TOOL_SRCS = pkgDosMain.c pkgCli.c $(filter-out pkgBas.c,$(SRCS)) ../serial/security/secHash.c ../serial/security/security.c ../libdvx/platform/dvxPlatformUtil.c ../libdvx/thirdparty/stb_ds_impl.c +TOOL_SRCS = pkgDosMain.c pkgCli.c $(CORE_SRCS) $(WRITER_SRCS) $(THIRD_SRCS) ../crypto/secHash.c ../crypto/secSign.c ../crypto/security.c $(MONO)/monocypher.c $(MONO)/optional/monocypher-ed25519.c ../libdvx/platform/dvxPlatformUtil.c ../libdvx/thirdparty/stb_ds_impl.c TOOL_OBJS = $(patsubst %.c,$(TOOLOBJDIR)/%.o,$(notdir $(TOOL_SRCS))) -vpath %.c . $(MINIZ) $(MONO) $(MONO)/optional ../serial/security ../libdvx/platform ../libdvx/thirdparty +vpath %.c . $(MINIZ) $(MONO) $(MONO)/optional ../crypto ../libdvx/platform ../libdvx/thirdparty .PHONY: all clean -all: $(TARGET) $(TARGETDIR)/pkg.dep $(TOOL) +all: $(TARGET) $(TARGETDIR)/pkg.dep $(WRITER) $(TOOL) $(TARGETDIR)/pkg.dep: pkg.dep | $(TARGETDIR) sed 's/$$/\r/' $< > $@ -$(TARGET): $(OBJS) | $(TARGETDIR) - $(DXE3GEN) -o $(TARGETDIR)/pkg.dxe -U $(OBJS) +$(GC_OBJ): $(OBJS) + $(DXE_GC) $@ $(OWN_OBJS) -- $(OBJS) + +$(TARGET): $(GC_OBJ) | $(TARGETDIR) + $(DXE3GEN) -o $(TARGETDIR)/pkg.dxe -U $(GC_OBJ) mv $(TARGETDIR)/pkg.dxe $@ +$(WRITER_GC): $(WRITER_OBJS) + $(DXE_GC) $@ $(WRITER_DESC) -- $(WRITER_OBJS) + +$(WRITER): $(WRITER_GC) | $(WRITERDIR) + $(DXE3GEN) -o $(WRITERDIR)/writer.dxe -U $(WRITER_GC) + mv $(WRITERDIR)/writer.dxe $@ + $(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< -# DOS executable: link (stripped: it ships on the floppies), convert to -# COFF, prepend the DPMI stub +# DOS executable: link (stripped, and only what it reaches: it ships on +# the floppies), convert to COFF, prepend the DPMI stub $(TOOL): $(TOOL_OBJS) | $(SYSTEMDIR) - $(CC) -s -o $(SYSTEMDIR)/dvxpkg.exe $^ -lm + $(CC) -s -Wl,--gc-sections -o $(SYSTEMDIR)/dvxpkg.exe $^ -lm $(EXE2COFF) $(SYSTEMDIR)/dvxpkg.exe cat $(CWSDSTUB) $(SYSTEMDIR)/dvxpkg > $@ rm -f $(SYSTEMDIR)/dvxpkg $(SYSTEMDIR)/dvxpkg.exe @@ -85,10 +115,10 @@ $(TOOLOBJDIR) $(SYSTEMDIR): $(OBJDIR): mkdir -p $(OBJDIR) -$(TARGETDIR): - mkdir -p $(TARGETDIR) +$(TARGETDIR) $(WRITERDIR): + mkdir -p $@ --include $(OBJS:.o=.d) $(TOOL_OBJS:.o=.d) +-include $(OBJS:.o=.d) $(WRITER_OBJS:.o=.d) $(TOOL_OBJS:.o=.d) clean: - rm -rf $(OBJS) $(OBJS:.o=.d) $(TARGET) $(TARGETDIR)/pkg.dep $(TARGETDIR) $(TOOLOBJDIR) $(TOOL) + rm -rf $(OBJS) $(OBJS:.o=.d) $(WRITER_OBJS) $(WRITER_OBJS:.o=.d) $(GC_OBJ) $(WRITER_GC) $(TARGET) $(TARGETDIR)/pkg.dep $(TARGETDIR) $(TOOLOBJDIR) $(TOOL) diff --git a/src/libs/kpunch/pkg/pkg.dep b/src/libs/kpunch/pkg/pkg.dep index ffb0ed6e..0063f87e 100644 --- a/src/libs/kpunch/pkg/pkg.dep +++ b/src/libs/kpunch/pkg/pkg.dep @@ -1,2 +1,2 @@ libdvx -serial +crypto diff --git a/src/libs/kpunch/pkg/pkg.dhs b/src/libs/kpunch/pkg/pkg.dhs index d34fe636..f7c9b118 100644 --- a/src/libs/kpunch/pkg/pkg.dhs +++ b/src/libs/kpunch/pkg/pkg.dhs @@ -43,7 +43,7 @@ Add-on packages: the .DPK file format, manifests, Ed25519 keys and signatures, t Header: pkg/pkg.h (it includes the others below) -Loaded as: bin/libs/kpunch/pkg/pkg.lib +Loaded as: bin/libs/kpunch/pkg/pkg.lib, and bin/libs/kpunch/pkg/writer/writer.drv when a package is made (below) BASIC: pkg.bas (sessions, offers, plans, install and remove jobs, building, signing, and keys; PkgFileOwner$ is pkgDbOwner). @@ -124,7 +124,8 @@ A key pair is NAME.KEY (public: id, name, public key) and NAME.SEC (secret: id, pkgDpkBuildFiles(&templ, files, count, out, secret, err, size) The same from a list of PkgDpkFileT (the name in the package, the file it comes from). pkgDpkFolderFiles(folder, prefix, &files) Every file under folder as that list, named prefix/...; pkgDpkFilesFree frees it. pkgDpkReadInfo(path, &info, err, size) The manifest and PACKAGE.SIG, checked; pkgDpkInfoFree. - pkgDpkCheckFiles(path, &info, err, size) Every file's size and SHA-256 against the manifest. + pkgDpkCheckFiles(path, &info, err, size) Every file's size and SHA-256 against the manifest, all at once. + pkgDpkCheckStart(path, &info, err, size) The same as a job: pkgDpkJobStep a piece at a time, pkgDpkJobPercent by bytes read (BASIC's PkgVerifyStart). pkgDpkSign(path, &secret, detached, err, size) Sign into the package, or beside it as NAME.SIG. pkgDpkSigner(&info, detached, keys, count) Which of keys made the signature (or the detached one). .endtable @@ -145,7 +146,30 @@ pkgManifestFree(&templ); Each installed package has a record, SYSTEM/PACKAGES/VENDOR/NAME.INI: its manifest as installed, whether it was chosen or only needed by another, and the key that signed it. pkgDbLoad reads them all; pkgDbFind, pkgDbOwner (who has a file), pkgDbPut, and pkgDbDrop keep them. Setup writes DVX's own record (kpunch.dvx, with no files). -pkgKeyringLoad reads the keys DVX trusts: SYSTEM/KEYS (DVX's own) and the user's KEYS folder (publishers the user chose); pkgKeyringAdd trusts another. pkgTrustOf says how far a package is trusted (PkgTrustSystemE, PkgTrustUserE, PkgTrustNoneE). The rules the planner keeps: kpunch.* packages must be signed by a SYSTEM key; an upgrade must be signed by the key that signed the installed version (or a SYSTEM key); an unsigned package from a file is installed only after a warning. +pkgKeyringLoad reads the keys DVX trusts: SYSTEM/KEYS (DVX's own) and the user's KEYS folder (publishers the user chose); pkgKeyringAdd trusts another, and pkgKeyringRemove(ring, userDir, idText, err, size) stops trusting one of the user's (its files in KEYS are deleted; a SYSTEM key stays). pkgTrustOf says how far a package is trusted (PkgTrustSystemE, PkgTrustUserE, PkgTrustNoneE). The rules the planner keeps: kpunch.* packages must be signed by a SYSTEM key; an upgrade must be signed by the key that signed the installed version (or a SYSTEM key); an unsigned package from a file is installed only after a warning. + +.h2 ZIP Files (pkgZip.h) + +The reader goes front to back through a ZIP from a read callback (pkgZipFileRead over a FILE *), never seeking; each entry's data goes, inflated, to a write callback in steps, and its size and CRC-32 are checked at its end. pkgZipProblemText says why a step failed, as the end of a sentence. The writer makes a ZIP the reader takes: pkgZipCreate, pkgZipBeginFile or pkgZipBeginMem, pkgZipStep until each is in, pkgZipFinish (pkgZipAbort removes a half-made one). pkgZipDosTime and pkgZipDosStamp turn ZIP dates into times and back. + +A plain ZIP job (PkgZipJobT, pkgZipJob.c) does a whole archive a step at a time, for a program that keeps drawing: + +.table + Function Description + -------- ----------- + pkgZipJobRead(path, dir, err, size) Read a ZIP to its end, every entry checked; with dir, unpack each under it (folders made, files dated as in the ZIP; a name with a drive, a leading '/', or ".." fails it). + pkgZipJobCreate(path, names, sources, count, err, size) Make a ZIP of count files: names[i] in the ZIP from the file sources[i]. + pkgZipJobStep(job, maxWork) PkgZipJobMoreE, PkgZipJobDoneE, or PkgZipJobFailedE (pkgZipJobMessage says why). + pkgZipJobEntries(job, &count) The entries read or written so far. + pkgZipJobCurrent, pkgZipJobPercent The entry it is on; how far it is (bytes of the ZIP read, or of the files added). + pkgZipJobFree(job) Stop and free it: a ZIP being made, or a file being unpacked, is removed. +.endtable + +pkgHexEncode and pkgHexDecode (pkgFormat.h) turn bytes into hex digits and back, as manifests, keys, and the package server write SHA-256s and key ids. + +.h2 The Package Writer Module + +Writing ZIPs (miniz's deflate) and making keys and signatures (Ed25519 signing) are used only when a package is made, so in DVX they are a module of their own, writer.drv in LIBS\KPUNCH\PKG\WRITER, which pkg.lib loads the first time a program makes a ZIP, a key, or a signature (pkgZipCreate, pkgKeyMake, or pkgSign, directly or through pkgKeyWrite, the .DPK and ZIP jobs, and BASIC's PkgBuildStart, PkgSignStart, PkgKeygen, PkgSignData, and PkgZipCreateStart); it stays until pkg.lib is unloaded. Nothing changes for a program: the calls keep their names (pkgWriterMod.c passes them on; pkgWriter.h has the module's descriptor). Without the module (a damaged installation) pkgZipCreate gives NULL and a key or signature made is all zeros, which nothing accepts; DVX.LOG says the writer is missing. Reading, checking, and installing packages never load it. Programs that link the library in whole (DVXPKG.EXE, the build machine's dvxpkg, the tests) compile pkgZipWrite.c and pkgSignMake.c instead of the module. .h2 Planning (pkgPlan.h) diff --git a/src/libs/kpunch/pkg/pkgBas.c b/src/libs/kpunch/pkg/pkgBas.c index 96ef0eb0..0b0592d5 100644 --- a/src/libs/kpunch/pkg/pkgBas.c +++ b/src/libs/kpunch/pkg/pkgBas.c @@ -25,36 +25,47 @@ // // A session (PkgOpen) holds the packages installed in the DVX folder, the // keys DVX trusts, the packages read from a folder (offers), the last -// install plan, and one running job (installing the plan, or removing -// packages). Lists come back as ListView rows: cells separated by TAB, -// each row ending in LF. Making packages (PkgBuild, PkgSign, PkgKeygen) -// needs a session only for PkgError$. +// install plan, and one running job (reading packages as the offers, +// installing the plan, removing packages, building, signing, or checking +// a package, or listing, unpacking, or making a plain ZIP). Lists come +// back as ListView rows: cells separated by TAB, each row ending in LF. +// Making a key (PkgKeygen) needs a session only for PkgError$. #include "pkgBas.h" +#include "pkgInt.h" #include "pkg.h" #include "dvxBas.h" #include "dvxPlat.h" -#include "monocypher.h" +#include "crypto/security.h" #include "stb_ds_wrap.h" #include #include #include #include +#include -#define ROW_ENDS "\n\r" // what may end a row PkgBuild is given +#define ROW_ENDS "\n\r" // what may end a row PkgBuildStart is given #define ID_SEP "\n" // between the ids a function takes or gives #define OFFER_SEP " ," // between PkgPlan's offer numbers #define NUMBER_TEXT_MAX 24 #define PERCENT_MAX 100 -#define YES_TEXT "yes" -#define NO_TEXT "no" +#define KEY_SYSTEM_TEXT "system" // PkgKeysRows$: DVX's own key +#define KEY_USER_TEXT "user" // one the user trusts +#define ZIP_FOLDER_TEXT "folder" // PkgZipRows$: an entry that is a folder +#define ZIP_DATE_FORMAT "%Y-%m-%d %H:%M" +#define ZIP_DATE_MAX 20 #define PKG_SEP '/' // between the parts of a name in a package typedef enum { JobNoneE, JobInstallE, - JobRemoveE + JobRemoveE, + JobBuildE, // dpk: building a package + JobSignE, // dpk: signing one + JobReadE, // scan: reading packages as the offers + JobVerifyE, // dpk: checking a package's files + JobZipE // zip: a plain ZIP listed, unpacked, or made } JobE; typedef struct { @@ -66,6 +77,11 @@ typedef struct { char *staleRows; // the plan's stale add-ons, as rows (malloc'd) JobE job; PkgOpT *op; + PkgDpkJobT *dpk; // building or signing + PkgOfferScanT scan; // reading + PkgZipJobT *zip; // a ZIP job + PkgZipEntryT *zipEntries; // stb_ds: what the last ZIP job listed, unpacked, or packed + bool cancelAsked; // PkgRunCancel: the job ends once its operation can stop int32_t jobAt; // the plan step or removal running char **removeIds; // stb_ds of strdup'd ids bool removeChanged; @@ -74,6 +90,7 @@ typedef struct { uint32_t jobBytes; // bytes of the whole job (estimated for removal) PkgRunE result; char current[PKG_TEXT_MAX]; + char userDir[DVX_MAX_PATH]; // the user folder of the session's DVX folder: its trusted keys char err[PKG_ERR_MAX]; } SessionT; @@ -86,12 +103,14 @@ static uint32_t filesBytes(const PkgManifestT *m); static bool filesParse(SessionT *ss, const char *rows, PkgDpkFileT **files); static void idsFree(char **ids); static char **idsSplit(const char *text); +static void jobBegin(SessionT *ss, JobE job, uint32_t bytes); static void jobEnd(SessionT *ss, PkgRunE result); static bool jobNext(SessionT *ss); static char *nameCopy(const char *text, size_t len); static const PkgOfferT *offerOf(const SessionT *ss, int32_t offer); static void offersFree(SessionT *ss); -int32_t PkgBuild(int32_t s, const char *templatePath, const char *files, const char *outPath, const char *secretPath); +int32_t PkgBuildStart(int32_t s, const char *templatePath, const char *files, const char *outPath, const char *secretPath); +int32_t PkgCheckData(int32_t s, const uint8_t *data, int32_t len, const char *signature, const char *keyPath); void PkgClose(int32_t s); const char *PkgDependents(int32_t s, const char *ids); const char *PkgDependsRows(int32_t s, const char *id); @@ -108,17 +127,19 @@ const char *PkgInstalledRows(int32_t s); int32_t PkgInstallStart(int32_t s); int32_t PkgKeygen(int32_t s, const char *keyName, const char *basePath); const char *PkgKeyInfo(int32_t s, const char *path); +const char *PkgKeysRows(int32_t s); const char *PkgKindFolder(const char *kind, const char *id); const char *PkgKindName(int32_t kind); const char *PkgNeededBy(int32_t s, const char *id); int32_t PkgOfferCount(int32_t s); const char *PkgOfferDependsRows(int32_t s, int32_t offer); const char *PkgOfferField(int32_t s, int32_t offer, int32_t field); -int32_t PkgOfferFile(int32_t s, const char *path); const char *PkgOfferFilesRows(int32_t s, int32_t offer); -int32_t PkgOfferFolder(int32_t s, const char *dir); +int32_t PkgOfferFileStart(int32_t s, const char *path); +int32_t PkgOfferFolderStart(int32_t s, const char *dir); int32_t PkgOfferIndex(int32_t s, const char *id); int32_t PkgOpen(void); +int32_t PkgOpenIn(const char *root); const char *PkgOrphans(int32_t s, const char *ids); int32_t PkgPlan(int32_t s, const char *offers); double PkgPlanBytes(int32_t s); @@ -132,19 +153,33 @@ int32_t PkgRunFor(int32_t s, int32_t milliseconds); const char *PkgRunKept(int32_t s); const char *PkgRunMessage(int32_t s); int32_t PkgRunPercent(int32_t s); -int32_t PkgSign(int32_t s, const char *dpkPath, const char *secretPath, int32_t detached); +const char *PkgSignData(int32_t s, const uint8_t *data, int32_t len, const char *secretPath); +int32_t PkgSignStart(int32_t s, const char *dpkPath, const char *secretPath, int32_t detached); int32_t PkgTrustKey(int32_t s, const char *keyPath); +int32_t PkgUntrustKey(int32_t s, const char *keyId); +int32_t PkgVerifyStart(int32_t s, const char *dpkPath, const char *keyPath); +int32_t PkgVersionCompare(const char *a, const char *b); int32_t PkgVersionValid(const char *text); +int32_t PkgZipCreateStart(int32_t s, const char *zipPath, const char *files); +int32_t PkgZipExtractStart(int32_t s, const char *zipPath, const char *folder); +int32_t PkgZipListStart(int32_t s, const char *zipPath); +const char *PkgZipRows(int32_t s); static void planForget(SessionT *ss); static bool restartKind(PkgKindE kind); static int32_t resultOf(SessionT *ss, bool ok); static void rowsDepends(const SessionT *ss, const PkgManifestT *m); static void rowsFiles(const PkgManifestT *m); static void rowsPackages(const PkgInstalledT *const *pkgs); +static void runDpk(SessionT *ss); +static void runOp(SessionT *ss); +static void runRead(SessionT *ss); +static void runZip(SessionT *ss); static void sessionFree(void *obj); static SessionT *sessionOf(int32_t s); +static bool signedBy(SessionT *ss, const char *dpkPath, const PkgDpkInfoT *info, const char *keyPath); static const PkgKeyT *signerKey(const SessionT *ss, const uint8_t *id); static char *textCopy(const char *text, size_t len); +static bool zipStart(SessionT *ss, PkgZipJobT *job); // ============================================================ // Module state @@ -237,7 +272,7 @@ static const char *fieldOf(const SessionT *ss, const PkgManifestT *m, int32_t fi return basTextSet(&sText, key ? key->name : ""); case PkgFieldExplicitE: - return basTextSet(&sText, inst ? (inst->explicitly ? YES_TEXT : NO_TEXT) : ""); + return basTextSet(&sText, inst ? (inst->explicitly ? PKG_BAS_YES : PKG_BAS_NO) : ""); case PkgFieldTrustE: return basTextSet(&sText, offer ? trustNames[offer->trust] : ""); @@ -268,7 +303,7 @@ static uint32_t filesBytes(const PkgManifestT *m) { } -// PkgBuild's files: rows of name TAB file (blank rows skipped) onto *files +// PkgBuildStart's files: rows of name TAB file (blank rows skipped) onto *files // (pkgDpkFilesFree). False with the reason in ss->err. static bool filesParse(SessionT *ss, const char *rows, PkgDpkFileT **files) { const char *p = rows ? rows : ""; @@ -292,7 +327,7 @@ static bool filesParse(SessionT *ss, const char *rows, PkgDpkFileT **files) { if (!f.name || !f.source) { free(f.name); free(f.source); - snprintf(ss->err, sizeof(ss->err), "There is not enough memory."); + snprintf(ss->err, sizeof(ss->err), PKG_ERR_NO_MEMORY); return false; } @@ -336,12 +371,44 @@ static char **idsSplit(const char *text) { } +// Start a job of bytes (for its progress; a package job counts its own). +static void jobBegin(SessionT *ss, JobE job, uint32_t bytes) { + idsFree(ss->kept); + ss->kept = NULL; + ss->err[0] = '\0'; + ss->current[0] = '\0'; + ss->job = job; + ss->jobAt = 0; + ss->doneBefore = 0; + ss->jobBytes = bytes; + ss->cancelAsked = false; + ss->result = PkgRunBusyE; +} + + // The job is over (done, or failed with ss->err said): an operation still -// open is stopped (or finished, once its files are in), and an install's -// plan is used up. +// open is stopped (or finished, once its files are in), a package being +// built or signed is removed, packages being read are dropped, and an +// install's plan is used up. static void jobEnd(SessionT *ss, PkgRunE result) { + if (ss->zip) { + int32_t count = 0; + const PkgZipEntryT *entries = pkgZipJobEntries(ss->zip, &count); + + arrsetlen(ss->zipEntries, count); + + if (count > 0) { + memcpy(ss->zipEntries, entries, (size_t)count * sizeof(*entries)); + } + } + pkgOpFree(ss->op); - ss->op = NULL; + pkgDpkJobFree(ss->dpk); + pkgZipJobFree(ss->zip); + pkgOfferScanFree(&ss->scan); + ss->op = NULL; + ss->dpk = NULL; + ss->zip = NULL; if (ss->job == JobInstallE) { planForget(ss); @@ -364,7 +431,7 @@ static bool jobNext(SessionT *ss) { const PkgOfferT *o = &ss->offers[st->offer]; name = o->manifest.name; - ss->op = pkgOpInstall(&ss->db, o->where, &ss->ring, st->explicitly); + ss->op = pkgOpInstall(&ss->db, o, st->explicitly); } else if (ss->job == JobRemoveE && ss->jobAt < (int32_t)arrlen(ss->removeIds)) { const PkgInstalledT *inst = pkgDbFind(&ss->db, ss->removeIds[ss->jobAt]); @@ -379,7 +446,7 @@ static bool jobNext(SessionT *ss) { snprintf(ss->current, sizeof(ss->current), "%s", name); if (!ss->op) { - snprintf(ss->err, sizeof(ss->err), "There is not enough memory."); + snprintf(ss->err, sizeof(ss->err), PKG_ERR_NO_MEMORY); } return ss->op != NULL; @@ -419,28 +486,70 @@ static void offersFree(SessionT *ss) { } -// Build the package outPath from the template templatePath +// Start building the package outPath from the template templatePath // (pkgTemplateRead; code with no abi gets this DVX's) and files: rows of // a name in the package (from the DVX folder) TAB the file it is made -// from (PkgFolderRows$). Signed with the secret key file secretPath -// unless it is "". True, or False with the reason in PkgError$. -int32_t PkgBuild(int32_t s, const char *templatePath, const char *files, const char *outPath, const char *secretPath) { +// from (PkgFolderRows$); run it with PkgRunFor. Signed with the secret +// key file secretPath unless it is "". False with the reason in +// PkgError$ (none when a job is running). +int32_t PkgBuildStart(int32_t s, const char *templatePath, const char *files, const char *outPath, const char *secretPath) { SessionT *ss = sessionOf(s); PkgDpkFileT *list = NULL; + PkgDpkJobT *job = NULL; bool keyed = secretPath && secretPath[0]; PkgManifestT templ; PkgSecretT secret; - bool ok; - if (!ss || !templatePath || !outPath || !pkgTemplateRead(templatePath, &templ, ss->err, sizeof(ss->err))) { + if (!ss || ss->job != JobNoneE || !templatePath || !outPath || !pkgTemplateRead(templatePath, &templ, ss->err, sizeof(ss->err))) { return BAS_FALSE; } - ok = filesParse(ss, files, &list) && (!keyed || pkgSecretRead(secretPath, &secret, ss->err, sizeof(ss->err))); - ok = ok && pkgDpkBuildFiles(&templ, list, (int32_t)arrlen(list), outPath, keyed ? &secret : NULL, ss->err, sizeof(ss->err)); + if (filesParse(ss, files, &list) && (!keyed || pkgSecretRead(secretPath, &secret, ss->err, sizeof(ss->err)))) { + job = pkgDpkBuildStart(&templ, list, (int32_t)arrlen(list), outPath, keyed ? &secret : NULL, ss->err, sizeof(ss->err)); + } + pkgSecretWipe(&secret); pkgDpkFilesFree(list); pkgManifestFree(&templ); + + if (job) { + jobBegin(ss, JobBuildE, 0); + ss->dpk = job; + } + + return resultOf(ss, job != NULL); +} + + +// True when signature (PkgSignData$'s text, or a .SIG file's) was made +// over data by the key in the key file keyPath, or with keyPath "" by a +// key DVX trusts. False with the reason in PkgError$. +int32_t PkgCheckData(int32_t s, const uint8_t *data, int32_t len, const char *signature, const char *keyPath) { + SessionT *ss = sessionOf(s); + PkgSigT sig; + PkgKeyT key; + bool ok = ss && signature && pkgSigParse(signature, (int32_t)strlen(signature), &sig, ss->err, sizeof(ss->err)); + + if (ok && keyPath && keyPath[0]) { + ok = pkgKeyRead(keyPath, &key, ss->err, sizeof(ss->err)); + + if (ok && !pkgSigCheck(&sig, &key, data, len)) { + snprintf(ss->err, sizeof(ss->err), "The signature was not made by %s over these bytes.", keyPath); + ok = false; + } + } else if (ok) { + bool trusted = false; + + for (int32_t i = 0; i < (int32_t)arrlen(ss->ring.keys) && !trusted; i++) { + trusted = pkgSigCheck(&sig, &ss->ring.keys[i], data, len); + } + + if (!trusted) { + snprintf(ss->err, sizeof(ss->err), "The signature was not made over these bytes by a key DVX trusts."); + ok = false; + } + } + return resultOf(ss, ok); } @@ -539,7 +648,7 @@ const char *PkgFilesRows(int32_t s, const char *id) { // Every file under folder (but a PACKAGE.INI or PACKAGE.SIG at its top) as -// PkgBuild's files: rows of its name in the package (prefix, then its name +// PkgBuildStart's files: rows of its name in the package (prefix, then its name // under folder; from folder itself when prefix is "") and the file, in // name order. "" with the reason in PkgError$ when folder cannot be read. const char *PkgFolderRows(int32_t s, const char *folder, const char *prefix) { @@ -612,14 +721,7 @@ int32_t PkgInstallStart(int32_t s) { return BAS_FALSE; } - idsFree(ss->kept); - ss->kept = NULL; - ss->err[0] = '\0'; - ss->job = JobInstallE; - ss->jobAt = 0; - ss->doneBefore = 0; - ss->jobBytes = ss->plan.bytes; - ss->result = PkgRunBusyE; + jobBegin(ss, JobInstallE, ss->plan.bytes); if (!jobNext(ss)) { jobEnd(ss, ss->err[0] ? PkgRunFailedE : PkgRunDoneE); @@ -648,7 +750,7 @@ int32_t PkgKeygen(int32_t s, const char *keyName, const char *basePath) { } ok = ok && pkgKeyWrite(seed, keyName, basePath, NULL, ss->err, sizeof(ss->err)); - crypto_wipe(seed, sizeof(seed)); + secSecureZero(seed, sizeof(seed)); return resultOf(ss, ok); } @@ -689,6 +791,25 @@ const char *PkgKeyInfo(int32_t s, const char *path) { } +// The keys DVX trusts, as rows: name, id (hex), and "system" (DVX's own, +// in SYSTEM\KEYS) or "user" (one the user chose to trust). +const char *PkgKeysRows(int32_t s) { + const SessionT *ss = sessionOf(s); + + basTextStart(&sText); + + for (int32_t i = 0; ss && i < (int32_t)arrlen(ss->ring.keys); i++) { + char id[PKG_KEY_ID_TEXT]; + const char *cells[] = { ss->ring.keys[i].name, id, i < ss->ring.systemCount ? KEY_SYSTEM_TEXT : KEY_USER_TEXT }; + + pkgKeyIdText(ss->ring.keys[i].id, id); + basTextRow(&sText, cells, DVX_ARRAY_LEN(cells)); + } + + return basTextEnd(&sText); +} + + // The folder a package id of kind ("app", ...) keeps its files in, from // the DVX folder (APPS\\VENDOR\\NAME for a program); "" for bundle and // system (no folder of their own), or a bad kind or id. @@ -761,24 +882,6 @@ const char *PkgOfferField(int32_t s, int32_t offer, int32_t field) { } -// Read the package at path, and every other package in its folder (what -// it may need), as the offers (the ones before are gone, and the plan). -// The number of path's offer (0), or -1: PkgError$ says why. -int32_t PkgOfferFile(int32_t s, const char *path) { - SessionT *ss = sessionOf(s); - - // A running install reads the plan's offers - if (!ss || !path || ss->job != JobNoneE) { - return -1; - } - - offersFree(ss); - planForget(ss); - ss->offers = pkgOffersFor(path, &ss->ring, ss->err, sizeof(ss->err)); - return ss->offers ? 0 : -1; -} - - // The files of offer (the rows of PkgFilesRows). const char *PkgOfferFilesRows(int32_t s, int32_t offer) { const PkgOfferT *o = offerOf(sessionOf(s), offer); @@ -793,18 +896,41 @@ const char *PkgOfferFilesRows(int32_t s, int32_t offer) { } -// Every package in dir as the offers: how many, or -1 while a job runs. -int32_t PkgOfferFolder(int32_t s, const char *dir) { +// Start reading the package at path, and every other package in its +// folder (what it may need), as the offers: a job (run with PkgRunFor; +// the ones before are gone, and the plan); path's offer is 0. It fails +// when path cannot be read (PkgRunMessage$ says why). False while a job +// runs (an install reads the plan's offers). +int32_t PkgOfferFileStart(int32_t s, const char *path) { SessionT *ss = sessionOf(s); + char dir[DVX_MAX_PATH]; - if (!ss || !dir || ss->job != JobNoneE) { - return -1; + if (!ss || !path || ss->job != JobNoneE) { + return BAS_FALSE; } offersFree(ss); planForget(ss); - ss->offers = pkgOffersInFolder(dir, &ss->ring); - return (int32_t)arrlen(ss->offers); + jobBegin(ss, JobReadE, 0); + pkgOfferScanStart(&ss->scan, platformPathDirName(path, dir, sizeof(dir)) ? dir : NULL, path, &ss->ring); + return BAS_TRUE; +} + + +// Start reading every package in dir as the offers, as +// PkgOfferFileStart; PkgOfferCount says how many there were. +int32_t PkgOfferFolderStart(int32_t s, const char *dir) { + SessionT *ss = sessionOf(s); + + if (!ss || !dir || ss->job != JobNoneE) { + return BAS_FALSE; + } + + offersFree(ss); + planForget(ss); + jobBegin(ss, JobReadE, 0); + pkgOfferScanStart(&ss->scan, dir, NULL, &ss->ring); + return BAS_TRUE; } @@ -828,20 +954,36 @@ int32_t PkgOfferIndex(int32_t s, const char *id) { // A session of the DVX folder's packages and the keys DVX trusts: its // handle, or 0 when out of memory. int32_t PkgOpen(void) { + return PkgOpenIn(""); +} + + +// A session on the DVX folder root ("" = this DVX's; another one, such as +// a copy being made ready on another disk, as DVXPKG's -r does), with the +// keys its user folder trusts. 0 when its records cannot be read. +int32_t PkgOpenIn(const char *root) { SessionT *ss = (SessionT *)calloc(1, sizeof(SessionT)); int32_t handle; + if (!root) { + root = ""; + } + if (!ss) { return BAS_NO_HANDLE; } - if (!pkgDbLoad(&ss->db, "")) { + if (!pkgDbLoad(&ss->db, root) || (root[0] && !platformUserDirOf(ss->userDir, sizeof(ss->userDir), root))) { pkgDbFree(&ss->db); free(ss); return BAS_NO_HANDLE; } - pkgKeyringLoad(&ss->ring, "", platformUserDir()); + if (!root[0]) { + snprintf(ss->userDir, sizeof(ss->userDir), "%s", platformUserDir()); + } + + pkgKeyringLoad(&ss->ring, root, ss->userDir); ss->result = PkgRunDoneE; handle = basHandleAdd(&sSessions, ss); @@ -953,7 +1095,7 @@ const char *PkgPlanRows(int32_t s) { const PkgOfferT *o = &ss->offers[st->offer]; const PkgManifestT *m = &o->manifest; char version[PKG_VERSION_MAX]; - const char *cells[] = { m->id, m->name, version, actNames[st->act], st->explicitly ? YES_TEXT : NO_TEXT, trustNames[o->trust] }; + const char *cells[] = { m->id, m->name, version, actNames[st->act], st->explicitly ? PKG_BAS_YES : PKG_BAS_NO, trustNames[o->trust] }; pkgVersionFormat(m->version, version, sizeof(version)); basTextRow(&sText, cells, DVX_ARRAY_LEN(cells)); @@ -982,16 +1124,9 @@ int32_t PkgRemoveStart(int32_t s, const char *ids, int32_t removeChanged) { return BAS_FALSE; } - idsFree(ss->kept); - ss->kept = NULL; - ss->err[0] = '\0'; + jobBegin(ss, JobRemoveE, 0); ss->removeIds = idsSplit(ids); ss->removeChanged = removeChanged != BAS_FALSE; - ss->job = JobRemoveE; - ss->jobAt = 0; - ss->doneBefore = 0; - ss->jobBytes = 0; - ss->result = PkgRunBusyE; for (int32_t i = 0; i < (int32_t)arrlen(ss->removeIds); i++) { const PkgInstalledT *inst = pkgDbFind(&ss->db, ss->removeIds[i]); @@ -1007,19 +1142,31 @@ int32_t PkgRemoveStart(int32_t s, const char *ids, int32_t removeChanged) { } -// Stop the job after the package it is on (one being unpacked is left as -// it was; one whose files are in is finished). +// Stop the job: reading packages, or a package being built or signed, at +// once (nothing read; nothing left behind); an install or removal after +// the package it is on (one being +// unpacked is left as it was; one whose files are in is finished, by +// PkgRunFor, which says PKG_RUN_BUSY until then). void PkgRunCancel(int32_t s) { SessionT *ss = sessionOf(s); - if (ss && ss->job != JobNoneE) { - snprintf(ss->err, sizeof(ss->err), "Stopped before it was done."); + if (!ss || ss->job == JobNoneE) { + return; + } + + snprintf(ss->err, sizeof(ss->err), "Stopped before it was done."); + ss->cancelAsked = true; + + if (ss->op) { + pkgOpStop(ss->op); + } else { jobEnd(ss, PkgRunFailedE); } } -// The name of the package the job is on. +// The name of the package the job is on (building or signing: of the file +// in it). const char *PkgRunCurrent(int32_t s) { SessionT *ss = sessionOf(s); @@ -1039,33 +1186,14 @@ int32_t PkgRunFor(int32_t s, int32_t milliseconds) { } while (ss->job != JobNoneE) { - PkgOpStateE st = pkgOpStep(ss->op); - uint32_t done; - uint32_t total; - - if (st == PkgOpFailedE) { - snprintf(ss->err, sizeof(ss->err), "%s: %s", ss->current, pkgOpMessage(ss->op)); - jobEnd(ss, PkgRunFailedE); - } else if (st == PkgOpDoneE) { - char *const *kept = pkgOpKept(ss->op); - - for (int32_t i = 0; i < (int32_t)arrlen(kept); i++) { - char *copy = strdup(kept[i]); - - if (copy) { - arrput(ss->kept, copy); - } - } - - pkgOpProgress(ss->op, &done, &total); - ss->doneBefore += total; - pkgOpFree(ss->op); - ss->op = NULL; - ss->jobAt++; - - if (!jobNext(ss)) { - jobEnd(ss, ss->err[0] ? PkgRunFailedE : PkgRunDoneE); - } + if (ss->job == JobReadE) { + runRead(ss); + } else if (ss->dpk) { + runDpk(ss); + } else if (ss->zip) { + runZip(ss); + } else { + runOp(ss); } if (platformClock() >= end) { @@ -1113,6 +1241,18 @@ int32_t PkgRunPercent(int32_t s) { return ss->result == PkgRunDoneE ? PERCENT_MAX : 0; } + if (ss->job == JobReadE) { + return pkgOfferScanPercent(&ss->scan); + } + + if (ss->dpk) { + return pkgDpkJobPercent(ss->dpk); + } + + if (ss->zip) { + return pkgZipJobPercent(ss->zip); + } + if (ss->op) { pkgOpProgress(ss->op, &done, &total); } @@ -1125,20 +1265,64 @@ int32_t PkgRunPercent(int32_t s) { } -// Sign the package dpkPath with the secret key file secretPath: into it, -// or beside it (NAME.SIG) when detached. True, or False with the reason -// in PkgError$. -int32_t PkgSign(int32_t s, const char *dpkPath, const char *secretPath, int32_t detached) { - SessionT *ss = sessionOf(s); +// Sign data with the secret key file secretPath: the signature as a +// signature file's text (save it, or send it with the data); "" with the +// reason in PkgError$. +const char *PkgSignData(int32_t s, const uint8_t *data, int32_t len, const char *secretPath) { + SessionT *ss = sessionOf(s); PkgSecretT secret; - bool ok; + PkgSigT sig; + char *text = NULL; + int32_t textLen; - if (!ss || !dpkPath || !secretPath) { + if (ss && secretPath && pkgSecretRead(secretPath, &secret, ss->err, sizeof(ss->err))) { + pkgSign(&secret, data, len, &sig); + pkgSecretWipe(&secret); + text = pkgSigFormat(&sig, &textLen); + } + + const char *out = basTextSet(&sText, text ? text : ""); + + resultOf(ss, text != NULL); + free(text); + return out; +} + + +// Start signing the package dpkPath with the secret key file secretPath: +// into it (the package rewritten), or beside it (NAME.SIG) when detached +// (done at once); run it with PkgRunFor. False with the reason in +// PkgError$ (none when a job is running). +int32_t PkgSignStart(int32_t s, const char *dpkPath, const char *secretPath, int32_t detached) { + SessionT *ss = sessionOf(s); + PkgDpkJobT *job = NULL; + PkgSecretT secret; + bool ok; + + if (!ss || ss->job != JobNoneE || !dpkPath || !secretPath) { return BAS_FALSE; } - ok = pkgSecretRead(secretPath, &secret, ss->err, sizeof(ss->err)) && pkgDpkSign(dpkPath, &secret, detached != BAS_FALSE, ss->err, sizeof(ss->err)); + ok = pkgSecretRead(secretPath, &secret, ss->err, sizeof(ss->err)); + + if (ok && detached != BAS_FALSE) { + ok = pkgDpkSign(dpkPath, &secret, true, ss->err, sizeof(ss->err)); + } else if (ok) { + job = pkgDpkSignStart(dpkPath, &secret, ss->err, sizeof(ss->err)); + ok = job != NULL; + } + pkgSecretWipe(&secret); + + if (ok) { + jobBegin(ss, JobSignE, 0); + ss->dpk = job; + + if (!job) { + jobEnd(ss, PkgRunDoneE); + } + } + return resultOf(ss, ok); } @@ -1148,7 +1332,72 @@ int32_t PkgSign(int32_t s, const char *dpkPath, const char *secretPath, int32_t int32_t PkgTrustKey(int32_t s, const char *keyPath) { SessionT *ss = sessionOf(s); - return resultOf(ss, ss && keyPath && pkgKeyringAdd(&ss->ring, platformUserDir(), keyPath, ss->err, sizeof(ss->err))); + return resultOf(ss, ss && keyPath && pkgKeyringAdd(&ss->ring, ss->userDir, keyPath, ss->err, sizeof(ss->err))); +} + + +// Stop trusting the key with id keyId (hex, as PkgKeysRows$ gives it): +// its files leave the user's KEYS folder. DVX's own keys stay. Offers +// read before keep the trust they had: read them again. +int32_t PkgUntrustKey(int32_t s, const char *keyId) { + SessionT *ss = sessionOf(s); + + return resultOf(ss, ss && pkgKeyringRemove(&ss->ring, ss->userDir, keyId, ss->err, sizeof(ss->err))); +} + + +// Start checking the package at dpkPath without installing it: every file +// against its manifest (size and SHA-256), a job run with PkgRunFor. With +// keyPath (a key file) it must also be signed by that key, in it or +// beside it (NAME.SIG), or nothing starts. False with the reason in +// PkgError$ (none when a job is running). +int32_t PkgVerifyStart(int32_t s, const char *dpkPath, const char *keyPath) { + SessionT *ss = sessionOf(s); + PkgDpkInfoT info; + PkgDpkJobT *job = NULL; + + if (!ss || ss->job != JobNoneE || !dpkPath) { + return BAS_FALSE; + } + + if (!pkgDpkReadInfo(dpkPath, &info, ss->err, sizeof(ss->err))) { + return BAS_FALSE; + } + + bool ok = !keyPath || !keyPath[0] || signedBy(ss, dpkPath, &info, keyPath); + + if (ok) { + job = pkgDpkCheckStart(dpkPath, &info, ss->err, sizeof(ss->err)); + ok = job != NULL; + } + + pkgDpkInfoFree(&info); + + if (ok) { + jobBegin(ss, JobVerifyE, 0); + ss->dpk = job; + } + + return resultOf(ss, ok); +} + + +// -1 when version a is older than b, 0 when the same, 1 when newer +// (1.10.0 is newer than 1.9.2). A text that is not a version counts as +// older than any version. +int32_t PkgVersionCompare(const char *a, const char *b) { + PkgVersionT va; + PkgVersionT vb; + bool okA = a && pkgVersionParse(a, &va); + bool okB = b && pkgVersionParse(b, &vb); + + if (!okA || !okB) { + return (okA ? 1 : 0) - (okB ? 1 : 0); + } + + int32_t cmp = pkgVersionCompare(va, vb); + + return (cmp > 0) - (cmp < 0); } @@ -1161,6 +1410,97 @@ int32_t PkgVersionValid(const char *text) { } +// Start making a plain ZIP at zipPath from files: rows of a name in the +// ZIP and the file it is made from, TAB between them (PkgFolderRows$ +// gives them for a folder), a job run with PkgRunFor. False with the +// reason in PkgError$ (none while a job is running). +int32_t PkgZipCreateStart(int32_t s, const char *zipPath, const char *files) { + SessionT *ss = sessionOf(s); + PkgDpkFileT *list = NULL; + PkgZipJobT *job = NULL; + + if (!ss || ss->job != JobNoneE || !zipPath) { + return BAS_FALSE; + } + + if (filesParse(ss, files, &list)) { + const char **names = NULL; + const char **sources = NULL; + + for (int32_t i = 0; i < (int32_t)arrlen(list); i++) { + arrput(names, list[i].name); + arrput(sources, list[i].source); + } + + job = pkgZipJobCreate(zipPath, names, sources, (int32_t)arrlen(list), ss->err, sizeof(ss->err)); + arrfree(names); + arrfree(sources); + } + + pkgDpkFilesFree(list); + return resultOf(ss, zipStart(ss, job)); +} + + +// Start unpacking the plain ZIP at zipPath into folder (made as needed; +// a file already there is replaced), a job run with PkgRunFor: each +// entry's size and CRC are checked, and a name that would leave folder +// stops it. False with the reason in PkgError$. +int32_t PkgZipExtractStart(int32_t s, const char *zipPath, const char *folder) { + SessionT *ss = sessionOf(s); + + if (!ss || ss->job != JobNoneE || !zipPath || !folder) { + return BAS_FALSE; + } + + return resultOf(ss, zipStart(ss, pkgZipJobRead(zipPath, folder, ss->err, sizeof(ss->err)))); +} + + +// Start reading the plain ZIP at zipPath to its end, every entry checked +// (PkgZipRows$ lists them after), a job run with PkgRunFor. False with +// the reason in PkgError$. +int32_t PkgZipListStart(int32_t s, const char *zipPath) { + SessionT *ss = sessionOf(s); + + if (!ss || ss->job != JobNoneE || !zipPath) { + return BAS_FALSE; + } + + return resultOf(ss, zipStart(ss, pkgZipJobRead(zipPath, NULL, ss->err, sizeof(ss->err)))); +} + + +// What the last ZIP job listed, unpacked, or packed (as far as it got), +// as rows: name ('/' between parts), size, date (YYYY-MM-DD HH:MM), and +// "folder" for a folder entry. +const char *PkgZipRows(int32_t s) { + const SessionT *ss = sessionOf(s); + + basTextStart(&sText); + + for (int32_t i = 0; ss && i < (int32_t)arrlen(ss->zipEntries); i++) { + const PkgZipEntryT *e = &ss->zipEntries[i]; + time_t when = pkgZipDosTime(e->dosDate, e->dosTime); + struct tm *t = localtime(&when); + char size[NUMBER_TEXT_MAX]; + char date[ZIP_DATE_MAX]; + + snprintf(size, sizeof(size), "%lu", (unsigned long)e->size); + + if (!t || strftime(date, sizeof(date), ZIP_DATE_FORMAT, t) == 0) { + date[0] = '\0'; + } + + const char *cells[] = { e->name, size, date, e->isDir ? ZIP_FOLDER_TEXT : "" }; + + basTextRow(&sText, cells, DVX_ARRAY_LEN(cells)); + } + + return basTextEnd(&sText); +} + + static void planForget(SessionT *ss) { pkgPlanFree(&ss->plan); free(ss->staleRows); @@ -1224,6 +1564,95 @@ static void rowsPackages(const PkgInstalledT *const *pkgs) { // A session (the handles' freeFn). +// One step of the package being built or signed. +static void runDpk(SessionT *ss) { + PkgDpkStepE st = pkgDpkJobStep(ss->dpk, PKG_DPK_STEP); + + snprintf(ss->current, sizeof(ss->current), "%s", pkgDpkJobCurrent(ss->dpk)); + + if (st == PkgDpkFailedE) { + snprintf(ss->err, sizeof(ss->err), "%s", pkgDpkJobMessage(ss->dpk)); + jobEnd(ss, PkgRunFailedE); + } else if (st == PkgDpkDoneE) { + jobEnd(ss, PkgRunDoneE); + } +} + + +// One step of the install or removal's operation; once it is over, the +// next one starts (none when the job was asked to stop: ss->err says so). +static void runOp(SessionT *ss) { + PkgOpStateE st = pkgOpStep(ss->op); + uint32_t done; + uint32_t total; + + if (st == PkgOpFailedE) { + if (!ss->cancelAsked) { + snprintf(ss->err, sizeof(ss->err), "%s: %s", ss->current, pkgOpMessage(ss->op)); + } + + jobEnd(ss, PkgRunFailedE); + return; + } + + if (st != PkgOpDoneE) { + return; + } + + char *const *kept = pkgOpKept(ss->op); + + for (int32_t i = 0; i < (int32_t)arrlen(kept); i++) { + char *copy = strdup(kept[i]); + + if (copy) { + arrput(ss->kept, copy); + } + } + + pkgOpProgress(ss->op, &done, &total); + ss->doneBefore += total; + pkgOpFree(ss->op); + ss->op = NULL; + ss->jobAt++; + + if (ss->cancelAsked) { + jobEnd(ss, PkgRunFailedE); + } else if (!jobNext(ss)) { + jobEnd(ss, ss->err[0] ? PkgRunFailedE : PkgRunDoneE); + } +} + + +// One package read as an offer; once all are, they are the offers. +static void runRead(SessionT *ss) { + PkgScanStepE st = pkgOfferScanStep(&ss->scan, ss->err, sizeof(ss->err)); + + snprintf(ss->current, sizeof(ss->current), "%s", ss->scan.current); + + if (st == PkgScanFailedE) { + jobEnd(ss, PkgRunFailedE); + } else if (st == PkgScanDoneE) { + ss->offers = ss->scan.offers; + ss->scan.offers = NULL; + jobEnd(ss, PkgRunDoneE); + } +} + + +static void runZip(SessionT *ss) { + PkgZipJobStepE st = pkgZipJobStep(ss->zip, PKG_DPK_STEP); + + snprintf(ss->current, sizeof(ss->current), "%s", pkgZipJobCurrent(ss->zip)); + + if (st == PkgZipJobFailedE) { + snprintf(ss->err, sizeof(ss->err), "%s", pkgZipJobMessage(ss->zip)); + jobEnd(ss, PkgRunFailedE); + } else if (st == PkgZipJobDoneE) { + jobEnd(ss, PkgRunDoneE); + } +} + + static void sessionFree(void *obj) { SessionT *ss = (SessionT *)obj; @@ -1233,6 +1662,7 @@ static void sessionFree(void *obj) { idsFree(ss->kept); pkgKeyringFree(&ss->ring); pkgDbFree(&ss->db); + arrfree(ss->zipEntries); free(ss); } @@ -1242,6 +1672,30 @@ static SessionT *sessionOf(int32_t s) { } +// True when the package at dpkPath (info its manifest and own signature) +// is signed by the key in the key file keyPath, in it or beside it +// (NAME.SIG); else false with the reason in ss->err. +static bool signedBy(SessionT *ss, const char *dpkPath, const PkgDpkInfoT *info, const char *keyPath) { + PkgKeyT key; + PkgSigT detached; + char sigPath[DVX_MAX_PATH]; + char why[PKG_ERR_MAX]; + + if (!pkgKeyRead(keyPath, &key, ss->err, sizeof(ss->err))) { + return false; + } + + bool hasDetached = platformPathSibling(sigPath, sizeof(sigPath), dpkPath, PKG_SIG_EXT) && pkgSigRead(sigPath, &detached, why, sizeof(why)); + + if (!pkgDpkSigner(info, hasDetached ? &detached : NULL, &key, 1)) { + snprintf(ss->err, sizeof(ss->err), "%s is not signed by %s.", dpkPath, keyPath); + return false; + } + + return true; +} + + // The key DVX trusts with id, or NULL. static const PkgKeyT *signerKey(const SessionT *ss, const uint8_t *id) { for (int32_t i = 0; ss && i < (int32_t)arrlen(ss->ring.keys); i++) { @@ -1266,3 +1720,17 @@ static char *textCopy(const char *text, size_t len) { return copy; } + + +// A ZIP job started (job NULL: it could not, ss->err says why): the last +// one's rows forgotten. +static bool zipStart(SessionT *ss, PkgZipJobT *job) { + if (!job) { + return false; + } + + arrsetlen(ss->zipEntries, 0); + jobBegin(ss, JobZipE, 0); + ss->zip = job; + return true; +} diff --git a/src/libs/kpunch/pkg/pkgBas.h b/src/libs/kpunch/pkg/pkgBas.h index 2c73bd94..12d0bd25 100644 --- a/src/libs/kpunch/pkg/pkgBas.h +++ b/src/libs/kpunch/pkg/pkgBas.h @@ -49,9 +49,17 @@ typedef enum { PkgFieldTrustE, // offer: system / user / none PkgFieldWhereE, // offer: its file PkgFieldSizeE, // the bytes of its files + PkgFieldSha256E, // catalogue: its package file's SHA-256 (hex), else "" + PkgFieldRepoSignedE, // catalogue: yes when the repository signed it, else no ("" elsewhere) PkgFieldCountE } PkgFieldE; +// The yes / no of PkgFieldExplicitE, PkgFieldRepoSignedE, and the plan's +// rows +#define PKG_BAS_YES "yes" +#define PKG_BAS_NO "no" + + // PKG_RUN_* typedef enum { PkgRunBusyE, @@ -59,7 +67,8 @@ typedef enum { PkgRunFailedE } PkgRunE; -int32_t PkgBuild(int32_t s, const char *templatePath, const char *files, const char *outPath, const char *secretPath); +int32_t PkgBuildStart(int32_t s, const char *templatePath, const char *files, const char *outPath, const char *secretPath); +int32_t PkgCheckData(int32_t s, const uint8_t *data, int32_t len, const char *signature, const char *keyPath); void PkgClose(int32_t s); const char *PkgDependents(int32_t s, const char *ids); const char *PkgDependsRows(int32_t s, const char *id); @@ -76,17 +85,19 @@ const char *PkgInstalledRows(int32_t s); int32_t PkgInstallStart(int32_t s); int32_t PkgKeygen(int32_t s, const char *keyName, const char *basePath); const char *PkgKeyInfo(int32_t s, const char *path); +const char *PkgKeysRows(int32_t s); const char *PkgKindFolder(const char *kind, const char *id); const char *PkgKindName(int32_t kind); const char *PkgNeededBy(int32_t s, const char *id); int32_t PkgOfferCount(int32_t s); const char *PkgOfferDependsRows(int32_t s, int32_t offer); const char *PkgOfferField(int32_t s, int32_t offer, int32_t field); -int32_t PkgOfferFile(int32_t s, const char *path); const char *PkgOfferFilesRows(int32_t s, int32_t offer); -int32_t PkgOfferFolder(int32_t s, const char *dir); +int32_t PkgOfferFileStart(int32_t s, const char *path); +int32_t PkgOfferFolderStart(int32_t s, const char *dir); int32_t PkgOfferIndex(int32_t s, const char *id); int32_t PkgOpen(void); +int32_t PkgOpenIn(const char *root); const char *PkgOrphans(int32_t s, const char *ids); int32_t PkgPlan(int32_t s, const char *offers); double PkgPlanBytes(int32_t s); @@ -100,8 +111,16 @@ int32_t PkgRunFor(int32_t s, int32_t milliseconds); const char *PkgRunKept(int32_t s); const char *PkgRunMessage(int32_t s); int32_t PkgRunPercent(int32_t s); -int32_t PkgSign(int32_t s, const char *dpkPath, const char *secretPath, int32_t detached); +const char *PkgSignData(int32_t s, const uint8_t *data, int32_t len, const char *secretPath); +int32_t PkgSignStart(int32_t s, const char *dpkPath, const char *secretPath, int32_t detached); int32_t PkgTrustKey(int32_t s, const char *keyPath); +int32_t PkgUntrustKey(int32_t s, const char *keyId); +int32_t PkgVerifyStart(int32_t s, const char *dpkPath, const char *keyPath); +int32_t PkgVersionCompare(const char *a, const char *b); int32_t PkgVersionValid(const char *text); +int32_t PkgZipCreateStart(int32_t s, const char *zipPath, const char *files); +int32_t PkgZipExtractStart(int32_t s, const char *zipPath, const char *folder); +int32_t PkgZipListStart(int32_t s, const char *zipPath); +const char *PkgZipRows(int32_t s); #endif // PKG_BAS_H diff --git a/src/libs/kpunch/pkg/pkgCli.c b/src/libs/kpunch/pkg/pkgCli.c index 6f139bc6..b7eb5b88 100644 --- a/src/libs/kpunch/pkg/pkgCli.c +++ b/src/libs/kpunch/pkg/pkgCli.c @@ -28,7 +28,7 @@ #include "pkgOp.h" #include "pkgPlan.h" #include "dvxPlat.h" -#include "monocypher.h" +#include "crypto/security.h" #include "stb_ds_wrap.h" #include @@ -286,7 +286,7 @@ static int cmdInstall(const CliOptsT *o) { for (int32_t i = 0; planned && i < (int32_t)arrlen(plan.steps); i++) { const PkgOfferT *offer = &offers[plan.steps[i].offer]; - planned = runOp(pkgOpInstall(&db, offer->where, &ring, plan.steps[i].explicitly), offer->manifest.id) == EXIT_OK; + planned = runOp(pkgOpInstall(&db, offer, plan.steps[i].explicitly), offer->manifest.id) == EXIT_OK; rc = planned ? EXIT_OK : EXIT_FAILED; } @@ -315,7 +315,7 @@ static int cmdKeygen(const char *name, const char *base, PkgRandomFnT random) { } ok = pkgKeyWrite(seed, name, base, &key, err, sizeof(err)); - crypto_wipe(seed, sizeof(seed)); + secSecureZero(seed, sizeof(seed)); if (!ok) { fprintf(stderr, "dvxpkg: %s\n", err); diff --git a/src/libs/kpunch/pkg/pkgDb.c b/src/libs/kpunch/pkg/pkgDb.c index c74f7d39..639e2359 100644 --- a/src/libs/kpunch/pkg/pkgDb.c +++ b/src/libs/kpunch/pkg/pkgDb.c @@ -330,7 +330,7 @@ bool pkgDbPut(PkgDbT *db, const PkgManifestT *m, bool explicitly, const uint8_t text = pkgDbFormat(m, explicitly, signer, &len); if (!text) { - return fail(err, errSize, "There is not enough memory."); + return fail(err, errSize, PKG_ERR_NO_MEMORY); } // Kept as read back, so the entry is the record exactly diff --git a/src/libs/kpunch/pkg/pkgDpk.c b/src/libs/kpunch/pkg/pkgDpk.c index 1cfc3c68..e9f830d3 100644 --- a/src/libs/kpunch/pkg/pkgDpk.c +++ b/src/libs/kpunch/pkg/pkgDpk.c @@ -27,10 +27,11 @@ #include "pkgInt.h" #include "dvxPlat.h" #include "dvxTypes.h" -#include "serial/security/secHash.h" +#include "crypto/secHash.h" #include "stb_ds_wrap.h" #include +#include #include #include #include @@ -39,12 +40,12 @@ #include #include -#define READ_CHUNK 16384 // bytes hashed at a time #define ENTRY_STEP 32768 // work per pkgZipData call (no one waits here) +#define HASH_SHARE 10 // of a build's progress, the hashing (deflating takes far longer) +#define PERCENT_MAX 100 #define SIG_TEXT_MAX 1024 // largest PACKAGE.SIG read #define TEMP_EXT ".$PK" // the package being rewritten by pkgDpkSign #define PATH_SEP_TEXT "/" -#define DOT '.' // A ZIP entry's data into memory typedef struct { @@ -53,32 +54,180 @@ typedef struct { uint32_t cap; } MemSinkT; +typedef enum { + DpkHashE, // building: each file's size and SHA-256 + DpkManifestE, // building: the manifest made (and signed), the package started + DpkHeadE, // the manifest (and signature) into the package + DpkFilesE, // building: each file into the package + DpkCopyE, // signing: each file of the package signed copied in + DpkCheckE, // checking: each file of the package hashed against the manifest + DpkFinishE, // the central directory (signing: the package replaced) + DpkDoneE, + DpkFailedE +} DpkStageE; + +struct PkgDpkJobT { + PkgManifestT m; // building: the manifest made; checking: the one checked against + bool *seen; // checking: each of m's files found (malloc'd) + PkgDpkFileT *files; // building: copies, in name order (stb_ds) + PkgSecretT secret; + char *text; // the manifest as written + char *sigText; // its signature, when signed + char *path; // the package written + char *signing; // signing: the package signed (path replaces it) + char *outReal; // building: path's real path, when it is there already + FILE *src; // the file being hashed, or the package being signed + PkgZipWriterT *w; + PkgZipT *zip; // signing: the package signed, read + SecSha256T sha; + uint64_t bytes; // building: bytes of the files hashed + uint64_t added; // building: bytes of the files added + uint32_t hashed; // bytes of the file being hashed so far + uint32_t total; // signing, checking: the size of the package read + int32_t textLen; + int32_t sigLen; + int32_t at; // building: the file being hashed or added + DpkStageE stage; + bool keyed; + bool entryOpen; // an entry begun: being added, copied, or passed over + bool entryKeep; // signing: the entry is copied + char current[PKG_NAME_MAX]; + char err[PKG_ERR_MAX]; + uint8_t buf[PKG_DPK_STEP]; +}; + // ============================================================ // Prototypes // ============================================================ +static void checkEnd(PkgDpkJobT *job); +static void checkStep(PkgDpkJobT *job, int32_t maxWork); static int compareFiles(const void *a, const void *b); +static void copyStep(PkgDpkJobT *job, int32_t maxWork); static bool fail(char *err, int32_t errSize, const char *fmt, ...) __attribute__((format(printf, 3, 4))); -static int32_t fileRead(void *ctx, uint8_t *buf, int32_t len); -static bool hashFile(const char *path, uint32_t *size, uint8_t sha[PKG_SHA256_SIZE]); +static void filesStep(PkgDpkJobT *job, int32_t maxWork); +static void finishStep(PkgDpkJobT *job); +static void hashEnd(PkgDpkJobT *job); +static void hashStep(PkgDpkJobT *job, int32_t maxWork); static bool hashWrite(void *ctx, const uint8_t *data, int32_t len); +static void headBegin(PkgDpkJobT *job); +static void headStep(PkgDpkJobT *job, int32_t maxWork); +static bool isOutput(const PkgDpkJobT *job, const char *source); +static void jobFail(PkgDpkJobT *job, const char *fmt, ...) __attribute__((format(printf, 2, 3))); +static bool jobRun(PkgDpkJobT *job, char *err, int32_t errSize); static bool joinName(char *out, int32_t outSize, const char *dir, const char *name); +static void manifestStep(PkgDpkJobT *job); static bool memWrite(void *ctx, const uint8_t *data, int32_t len); bool pkgDpkBuild(const PkgManifestT *templ, const char *folder, const char *outPath, const PkgSecretT *secret, char *err, int32_t errSize); bool pkgDpkBuildFiles(const PkgManifestT *templ, const PkgDpkFileT *files, int32_t count, const char *outPath, const PkgSecretT *secret, char *err, int32_t errSize); +PkgDpkJobT *pkgDpkBuildStart(const PkgManifestT *templ, const PkgDpkFileT *files, int32_t count, const char *outPath, const PkgSecretT *secret, char *err, int32_t errSize); bool pkgDpkCheckFiles(const char *path, const PkgDpkInfoT *info, char *err, int32_t errSize); +PkgDpkJobT *pkgDpkCheckStart(const char *path, const PkgDpkInfoT *info, char *err, int32_t errSize); void pkgDpkFilesFree(PkgDpkFileT *files); bool pkgDpkFolderFiles(const char *folder, const char *prefix, PkgDpkFileT **files); void pkgDpkInfoFree(PkgDpkInfoT *info); +const char *pkgDpkJobCurrent(const PkgDpkJobT *job); +void pkgDpkJobFree(PkgDpkJobT *job); +const char *pkgDpkJobMessage(const PkgDpkJobT *job); +int32_t pkgDpkJobPercent(const PkgDpkJobT *job); +PkgDpkStepE pkgDpkJobStep(PkgDpkJobT *job, int32_t maxWork); bool pkgDpkReadInfo(const char *path, PkgDpkInfoT *info, char *err, int32_t errSize); bool pkgDpkSign(const char *path, const PkgSecretT *secret, bool detached, char *err, int32_t errSize); const PkgKeyT *pkgDpkSigner(const PkgDpkInfoT *info, const PkgSigT *detached, const PkgKeyT *keys, int32_t count); +PkgDpkJobT *pkgDpkSignStart(const char *path, const PkgSecretT *secret, char *err, int32_t errSize); static bool readEntry(PkgZipT *zip, const PkgZipEntryT *e, uint32_t max, MemSinkT *sink); -static bool sameFile(const char *a, const char *b); -static bool siblingPath(const char *path, const char *ext, char *out, int32_t outSize); static void sortFiles(PkgDpkFileT *files); static bool walk(const char *folder, const char *rel, const char *prefix, PkgDpkFileT **files); -static const char *zipReason(const PkgZipT *zip); + + +// Checking: the package read to its end; a file the manifest lists and +// the package does not have fails it. +static void checkEnd(PkgDpkJobT *job) { + for (int32_t i = 0; i < (int32_t)arrlen(job->m.files); i++) { + if (!job->seen[i]) { + jobFail(job, "%s is in the manifest but not in the package.", job->m.files[i].name); + return; + } + } + + job->stage = DpkDoneE; +} + + +// Checking: the next entry's header, or a piece of its data (hashed when +// it is one of the package's files, else passed over: the manifest and +// signature); at its end its size and SHA-256 against the manifest's. +static void checkStep(PkgDpkJobT *job, int32_t maxWork) { + if (!job->entryOpen) { + PkgZipEntryT e; + PkgZipStepE step = pkgZipNext(job->zip, &e); + + if (step == PkgZipEndE) { + checkEnd(job); + return; + } + + if (step != PkgZipEntryE) { + jobFail(job, "%s", pkgZipProblemText(job->zip)); + return; + } + + if (e.isDir) { + return; + } + + snprintf(job->current, sizeof(job->current), "%s", e.name); + job->entryKeep = strcasecmp(e.name, PKG_MANIFEST_NAME) != 0 && strcasecmp(e.name, PKG_SIG_NAME) != 0; + job->entryOpen = true; + + if (!job->entryKeep) { + return; + } + + const PkgFileT *file = pkgManifestFindFile(&job->m, e.name); + + if (!file) { + jobFail(job, "%s is in the package but not in its manifest.", e.name); + return; + } + + job->at = (int32_t)(file - job->m.files); + + if (job->seen[job->at]) { + jobFail(job, "%s is in the package twice.", e.name); + return; + } + + job->seen[job->at] = true; + job->hashed = e.size; + secSha256Init(&job->sha); + return; + } + + PkgZipStepE data = job->entryKeep ? pkgZipData(job->zip, hashWrite, &job->sha, maxWork) : pkgZipData(job->zip, NULL, NULL, maxWork); + + if (data == PkgZipMoreE) { + return; + } + + job->entryOpen = false; + + if (data != PkgZipFileDoneE) { + jobFail(job, "%s: %s", job->current, pkgZipProblemText(job->zip)); + return; + } + + if (job->entryKeep) { + const PkgFileT *file = &job->m.files[job->at]; + uint8_t sha[PKG_SHA256_SIZE]; + + secSha256Final(&job->sha, sha); + + if (job->hashed != file->size || memcmp(sha, file->sha256, PKG_SHA256_SIZE) != 0) { + jobFail(job, "%s is not the file the manifest lists (it was changed or damaged).", job->current); + } + } +} // Two files to build by name. @@ -87,6 +236,49 @@ static int compareFiles(const void *a, const void *b) { } +// Signing: each file of the package signed copied in as it is packed +// (its manifest and an old signature passed over: the new ones are in). +static void copyStep(PkgDpkJobT *job, int32_t maxWork) { + PkgZipStepE step; + + if (!job->entryOpen) { + PkgZipEntryT e; + + step = pkgZipNext(job->zip, &e); + + if (step == PkgZipEndE) { + job->stage = DpkFinishE; + return; + } + + if (step != PkgZipEntryE) { + jobFail(job, "%s cannot be rewritten: %s", job->signing, pkgZipProblemText(job->zip)); + return; + } + + snprintf(job->current, sizeof(job->current), "%s", e.name); + job->entryKeep = !e.isDir && strcasecmp(e.name, PKG_MANIFEST_NAME) != 0 && strcasecmp(e.name, PKG_SIG_NAME) != 0; + job->entryOpen = true; + + if (job->entryKeep && !pkgZipBeginCopy(job->w, &e)) { + jobFail(job, "Cannot write %s.", job->path); + } + + return; + } + + step = job->entryKeep ? pkgZipDataPacked(job->zip, pkgZipCopyWrite, job->w, maxWork) : pkgZipData(job->zip, NULL, NULL, maxWork); + + if (step == PkgZipFailE && pkgZipProblem(job->zip) == PkgZipWriteFailedE) { + jobFail(job, "Cannot write %s.", job->path); + } else if (step == PkgZipFailE) { + jobFail(job, "%s cannot be rewritten: %s", job->signing, pkgZipProblemText(job->zip)); + } + + job->entryOpen = step == PkgZipMoreE; +} + + static bool fail(char *err, int32_t errSize, const char *fmt, ...) { va_list ap; @@ -97,38 +289,137 @@ static bool fail(char *err, int32_t errSize, const char *fmt, ...) { } -static int32_t fileRead(void *ctx, uint8_t *buf, int32_t len) { - FILE *f = (FILE *)ctx; - size_t n = fread(buf, 1, (size_t)len, f); +// Building: each file into the package, a piece at a time. +static void filesStep(PkgDpkJobT *job, int32_t maxWork) { + if (job->at == (int32_t)arrlen(job->files)) { + job->stage = DpkFinishE; + return; + } - return (n == 0 && ferror(f)) ? -1 : (int32_t)n; + const PkgDpkFileT *file = &job->files[job->at]; + + if (!job->entryOpen) { + snprintf(job->current, sizeof(job->current), "%s", file->name); + job->entryOpen = pkgZipBeginFile(job->w, file->name, file->source); + + if (!job->entryOpen) { + jobFail(job, "Cannot add %s to %s.", file->source, job->path); + } + + return; + } + + PkgZipStepE step = pkgZipStep(job->w, maxWork); + + if (step == PkgZipFailE) { + jobFail(job, "Cannot add %s to %s.", file->source, job->path); + } else if (step == PkgZipFileDoneE) { + const PkgFileT *listed = pkgManifestFindFile(&job->m, file->name); + + job->added += listed ? listed->size : 0; + job->entryOpen = false; + job->at++; + } } -static bool hashFile(const char *path, uint32_t *size, uint8_t sha[PKG_SHA256_SIZE]) { - uint8_t buf[READ_CHUNK]; - SecSha256T ctx; - uint64_t total = 0; - size_t n; - FILE *f = fopen(path, "rb"); +// The central directory written (signing: the package signed replaced by +// the copy). +static void finishStep(PkgDpkJobT *job) { + PkgZipWriterT *w = job->w; - if (!f) { - return false; + job->w = NULL; + + if (!pkgZipFinish(w)) { + jobFail(job, "Cannot write %s.", job->path); + return; } - secSha256Init(&ctx); + if (job->signing) { + pkgZipClose(job->zip); + fclose(job->src); + job->zip = NULL; + job->src = NULL; - while ((n = fread(buf, 1, sizeof(buf), f)) > 0) { - secSha256Update(&ctx, buf, (int32_t)n); - total += n; + if (remove(job->signing) != 0 || rename(job->path, job->signing) != 0) { + jobFail(job, "Cannot replace %s (the signed copy is %s).", job->signing, job->path); + return; + } } - bool ok = !ferror(f) && total <= UINT32_MAX; + job->stage = DpkDoneE; +} - fclose(f); - secSha256Final(&ctx, sha); - *size = (uint32_t)total; - return ok; + +// The file being hashed is read through: its size and SHA-256 into the +// manifest. +static void hashEnd(PkgDpkJobT *job) { + const PkgDpkFileT *file = &job->files[job->at]; + bool bad = ferror(job->src) != 0; + uint8_t sha[PKG_SHA256_SIZE]; + + fclose(job->src); + job->src = NULL; + secSha256Final(&job->sha, sha); + + if (bad) { + jobFail(job, "Cannot read %s.", file->source); + } else if (!pkgManifestAddFile(&job->m, file->name, job->hashed, sha)) { + jobFail(job, PKG_ERR_NO_MEMORY); + } else { + job->bytes += job->hashed; + job->at++; + } +} + + +// Building: each file's size and SHA-256, a piece at a time (the package +// being written, when it is there already, is not one of them). +static void hashStep(PkgDpkJobT *job, int32_t maxWork) { + if (!job->src) { + if (job->at == (int32_t)arrlen(job->files)) { + job->at = 0; + job->stage = DpkManifestE; + return; + } + + PkgDpkFileT *file = &job->files[job->at]; + + if (isOutput(job, file->source)) { + free(file->name); + free(file->source); + arrdel(job->files, job->at); + return; + } + + snprintf(job->current, sizeof(job->current), "%s", file->name); + job->src = fopen(file->source, "rb"); + job->hashed = 0; + secSha256Init(&job->sha); + + if (!job->src) { + jobFail(job, "Cannot read %s.", file->source); + return; + } + } + + for (int32_t work = 0; work == 0 || work < maxWork; ) { + size_t n = fread(job->buf, 1, sizeof(job->buf), job->src); + + if (n == 0) { + hashEnd(job); + return; + } + + if (n > UINT32_MAX - job->hashed) { + jobFail(job, "%s is too big for a package.", job->files[job->at].source); + return; + } + + secSha256Update(&job->sha, job->buf, (int32_t)n); + job->hashed += (uint32_t)n; + work += (int32_t)n; + } } @@ -138,6 +429,90 @@ static bool hashWrite(void *ctx, const uint8_t *data, int32_t len) { } +// The package started: the signature made (when there is a key) and the +// manifest's entry begun (headStep adds them). +static void headBegin(PkgDpkJobT *job) { + if (job->keyed) { + PkgSigT sig; + + pkgSign(&job->secret, (const uint8_t *)job->text, job->textLen, &sig); + job->sigText = pkgSigFormat(&sig, &job->sigLen); + + if (!job->sigText) { + jobFail(job, PKG_ERR_NO_MEMORY); + return; + } + } + + job->w = pkgZipCreate(job->path); + + if (!job->w) { + jobFail(job, "Cannot create %s.", job->path); + } else if (!pkgZipBeginMem(job->w, PKG_MANIFEST_NAME, job->text, (uint32_t)job->textLen, time(NULL))) { + jobFail(job, "Cannot write %s.", job->path); + } else { + job->stage = DpkHeadE; + } +} + + +// The manifest into the package, a piece at a time, then the signature +// (small: at once). +static void headStep(PkgDpkJobT *job, int32_t maxWork) { + PkgZipStepE step = pkgZipStep(job->w, maxWork); + + if (step == PkgZipMoreE) { + return; + } + + if (step == PkgZipFailE || (job->sigText && !pkgZipAddMem(job->w, PKG_SIG_NAME, job->sigText, (uint32_t)job->sigLen, time(NULL)))) { + jobFail(job, "Cannot write %s.", job->path); + return; + } + + job->stage = job->signing ? DpkCopyE : DpkFilesE; +} + + +// True when source is the package being written (only when that is there +// already; only a file of its name is looked at closely). Not by inode +// numbers: DVX's stat does not compute them (platformInit). +static bool isOutput(const PkgDpkJobT *job, const char *source) { + char real[PATH_MAX]; + + return job->outReal && platformPathSame(platformPathBaseName(source), platformPathBaseName(job->outReal)) && realpath(source, real) && platformPathSame(real, job->outReal); +} + + +static void jobFail(PkgDpkJobT *job, const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + vsnprintf(job->err, sizeof(job->err), fmt, ap); + va_end(ap); + job->stage = DpkFailedE; +} + + +// Run job (freed) to its end at once: false with the reason in err (a +// NULL job did not start: err says why already). +static bool jobRun(PkgDpkJobT *job, char *err, int32_t errSize) { + PkgDpkStepE step; + + if (!job) { + return false; + } + + while ((step = pkgDpkJobStep(job, INT32_MAX)) == PkgDpkMoreE) { + } + + bool ok = step == PkgDpkDoneE || fail(err, errSize, "%s", job->err); + + pkgDpkJobFree(job); + return ok; +} + + // dir/name ('/' between them; name alone when dir is ""). static bool joinName(char *out, int32_t outSize, const char *dir, const char *name) { int32_t n = dir[0] ? snprintf(out, (size_t)outSize, "%s" PATH_SEP_TEXT "%s", dir, name) : snprintf(out, (size_t)outSize, "%s", name); @@ -146,6 +521,24 @@ static bool joinName(char *out, int32_t outSize, const char *dir, const char *na } +// Building: the manifest checked and made, and the package started. +static void manifestStep(PkgDpkJobT *job) { + if (!pkgManifestCheck(&job->m, job->err, sizeof(job->err))) { + job->stage = DpkFailedE; + return; + } + + job->text = pkgManifestFormat(&job->m, &job->textLen); + + if (!job->text) { + jobFail(job, PKG_ERR_NO_MEMORY); + return; + } + + headBegin(job); +} + + static bool memWrite(void *ctx, const uint8_t *data, int32_t len) { MemSinkT *sink = (MemSinkT *)ctx; @@ -171,174 +564,101 @@ bool pkgDpkBuild(const PkgManifestT *templ, const char *folder, const char *outP bool pkgDpkBuildFiles(const PkgManifestT *templ, const PkgDpkFileT *files, int32_t count, const char *outPath, const PkgSecretT *secret, char *err, int32_t errSize) { - PkgManifestT m; - PkgDpkFileT *sorted = NULL; // the files' entries (not their text) copied - char *text = NULL; - int32_t len = 0; - bool ok = true; + return jobRun(pkgDpkBuildStart(templ, files, count, outPath, secret, err, errSize), err, errSize); +} - // The template's fields and dependencies (code made for this DVX when - // it does not say); the files are these - m = *templ; - m.format = PKG_MANIFEST_FORMAT; - m.abi = (pkgKindIsCode(m.kind) && m.abi <= 0) ? DVX_ABI_VERSION : m.abi; - m.files = NULL; - m.depends = NULL; - for (int32_t i = 0; i < (int32_t)arrlen(templ->depends); i++) { - arrput(m.depends, templ->depends[i]); - } +PkgDpkJobT *pkgDpkBuildStart(const PkgManifestT *templ, const PkgDpkFileT *files, int32_t count, const char *outPath, const PkgSecretT *secret, char *err, int32_t errSize) { + PkgDpkJobT *job = (PkgDpkJobT *)calloc(1, sizeof(PkgDpkJobT)); + char real[PATH_MAX]; + bool ok = job != NULL; - // In name order (the package being written is never one of them) - for (int32_t i = 0; i < count; i++) { - if (!sameFile(files[i].source, outPath)) { - arrput(sorted, files[i]); - } - } + if (ok) { + // The template's fields and dependencies (code made for this DVX + // when it does not say); the files are these + job->m = *templ; + job->m.format = PKG_MANIFEST_FORMAT; + job->m.abi = (pkgKindIsCode(job->m.kind) && job->m.abi <= 0) ? DVX_ABI_VERSION : job->m.abi; + job->m.files = NULL; + job->m.depends = NULL; + job->stage = DpkHashE; + job->keyed = secret != NULL; + job->path = strdup(outPath); + ok = job->path != NULL; - sortFiles(sorted); - - for (int32_t i = 0; ok && i < (int32_t)arrlen(sorted); i++) { - uint8_t sha[PKG_SHA256_SIZE]; - uint32_t size = 0; - - if (!hashFile(sorted[i].source, &size, sha)) { - ok = fail(err, errSize, "Cannot read %s.", sorted[i].source); - } else if (!pkgManifestAddFile(&m, sorted[i].name, size, sha)) { - ok = fail(err, errSize, "There is not enough memory."); - } - } - - ok = ok && pkgManifestCheck(&m, err, errSize); - text = ok ? pkgManifestFormat(&m, &len) : NULL; - - if (ok && !text) { - ok = fail(err, errSize, "There is not enough memory."); - } - - PkgZipWriterT *w = ok ? pkgZipCreate(outPath) : NULL; - - if (ok && !w) { - ok = fail(err, errSize, "Cannot create %s.", outPath); - } - - if (ok && !pkgZipAddMem(w, PKG_MANIFEST_NAME, text, (uint32_t)len, time(NULL))) { - ok = fail(err, errSize, "Cannot write %s.", outPath); - } - - if (ok && secret) { - PkgSigT sig; - int32_t sigLen = 0; - char *sigText = NULL; - - pkgSign(secret, (const uint8_t *)text, len, &sig); - sigText = pkgSigFormat(&sig, &sigLen); - - if (!sigText || !pkgZipAddMem(w, PKG_SIG_NAME, sigText, (uint32_t)sigLen, time(NULL))) { - ok = fail(err, errSize, "Cannot write %s.", outPath); + if (secret) { + job->secret = *secret; } - free(sigText); - } - - for (int32_t i = 0; ok && i < (int32_t)arrlen(sorted); i++) { - if (!pkgZipAddFile(w, sorted[i].name, sorted[i].source)) { - ok = fail(err, errSize, "Cannot add %s to %s.", sorted[i].source, outPath); + if (ok && realpath(outPath, real)) { + job->outReal = strdup(real); + ok = job->outReal != NULL; } + + for (int32_t i = 0; i < (int32_t)arrlen(templ->depends); i++) { + arrput(job->m.depends, templ->depends[i]); + } + + for (int32_t i = 0; ok && i < count; i++) { + PkgDpkFileT f = { strdup(files[i].name), strdup(files[i].source) }; + + ok = f.name && f.source; + + if (ok) { + arrput(job->files, f); + } else { + free(f.name); + free(f.source); + } + } + + sortFiles(job->files); } - if (w && ok && !pkgZipFinish(w)) { - ok = fail(err, errSize, "Cannot write %s.", outPath); - } else if (w && !ok) { - pkgZipAbort(w); + if (!ok) { + pkgDpkJobFree(job); + fail(err, errSize, PKG_ERR_NO_MEMORY); + return NULL; } - free(text); - arrfree(sorted); - pkgManifestFree(&m); - return ok; + return job; } bool pkgDpkCheckFiles(const char *path, const PkgDpkInfoT *info, char *err, int32_t errSize) { - const PkgManifestT *m = &info->manifest; - int32_t count = (int32_t)arrlen(m->files); - bool *seen = (bool *)calloc((size_t)count + 1, sizeof(bool)); - FILE *f = seen ? fopen(path, "rb") : NULL; - PkgZipT *zip = f ? pkgZipOpen(fileRead, f) : NULL; - bool ok = zip != NULL; - PkgZipEntryT e; - PkgZipStepE step = PkgZipFailE; + return jobRun(pkgDpkCheckStart(path, info, err, errSize), err, errSize); +} - if (!ok) { + +PkgDpkJobT *pkgDpkCheckStart(const char *path, const PkgDpkInfoT *info, char *err, int32_t errSize) { + PkgDpkJobT *job = (PkgDpkJobT *)calloc(1, sizeof(PkgDpkJobT)); + struct stat st; + + if (!job) { + fail(err, errSize, PKG_ERR_NO_MEMORY); + return NULL; + } + + job->seen = (bool *)calloc((size_t)arrlen(info->manifest.files) + 1, sizeof(bool)); + + if (!job->seen || !pkgManifestCopy(&job->m, &info->manifest)) { + fail(err, errSize, PKG_ERR_NO_MEMORY); + pkgDpkJobFree(job); + return NULL; + } + + job->total = stat(path, &st) == 0 ? (uint32_t)st.st_size : 0; + job->src = fopen(path, "rb"); + job->zip = job->src ? pkgZipOpen(pkgZipFileRead, job->src) : NULL; + + if (!job->zip) { fail(err, errSize, "Cannot read %s.", path); + pkgDpkJobFree(job); + return NULL; } - while (ok && (step = pkgZipNext(zip, &e)) == PkgZipEntryE) { - if (e.isDir) { - continue; - } - - // The manifest and signature: not files of the package - if (strcasecmp(e.name, PKG_MANIFEST_NAME) == 0 || strcasecmp(e.name, PKG_SIG_NAME) == 0) { - ok = pkgZipData(zip, NULL, NULL, INT32_MAX) == PkgZipFileDoneE || fail(err, errSize, "%s", zipReason(zip)); - continue; - } - - const PkgFileT *file = pkgManifestFindFile(m, e.name); - - if (!file) { - ok = fail(err, errSize, "%s is in the package but not in its manifest.", e.name); - break; - } - - int32_t index = (int32_t)(file - m->files); - - if (seen[index]) { - ok = fail(err, errSize, "%s is in the package twice.", e.name); - break; - } - - seen[index] = true; - - SecSha256T ctx; - uint8_t sha[PKG_SHA256_SIZE]; - PkgZipStepE data; - - secSha256Init(&ctx); - - while ((data = pkgZipData(zip, hashWrite, &ctx, ENTRY_STEP)) == PkgZipMoreE) { - } - - secSha256Final(&ctx, sha); - - if (data != PkgZipFileDoneE) { - ok = fail(err, errSize, "%s: %s", e.name, zipReason(zip)); - } else if (e.size != file->size || memcmp(sha, file->sha256, PKG_SHA256_SIZE) != 0) { - ok = fail(err, errSize, "%s is not the file the manifest lists (it was changed or damaged).", e.name); - } - } - - if (ok && step == PkgZipFailE) { - ok = fail(err, errSize, "%s", zipReason(zip)); - } - - for (int32_t i = 0; ok && i < count; i++) { - if (!seen[i]) { - ok = fail(err, errSize, "%s is in the manifest but not in the package.", m->files[i].name); - } - } - - if (zip) { - pkgZipClose(zip); - } - - if (f) { - fclose(f); - } - - free(seen); - return ok; + job->stage = DpkCheckE; + return job; } @@ -368,9 +688,113 @@ void pkgDpkInfoFree(PkgDpkInfoT *info) { } +const char *pkgDpkJobCurrent(const PkgDpkJobT *job) { + return job->current; +} + + +void pkgDpkJobFree(PkgDpkJobT *job) { + if (!job) { + return; + } + + pkgZipAbort(job->w); + + if (job->zip) { + pkgZipClose(job->zip); + } + + if (job->src) { + fclose(job->src); + } + + pkgSecretWipe(&job->secret); + pkgManifestFree(&job->m); + free(job->seen); + pkgDpkFilesFree(job->files); + free(job->text); + free(job->sigText); + free(job->path); + free(job->signing); + free(job->outReal); + free(job); +} + + +const char *pkgDpkJobMessage(const PkgDpkJobT *job) { + return job->err; +} + + +// Building: the hashing by files, then the rest by bytes added; signing +// and checking: by bytes of the package read. +int32_t pkgDpkJobPercent(const PkgDpkJobT *job) { + int32_t count = (int32_t)arrlen(job->files); + + if (job->stage == DpkDoneE) { + return PERCENT_MAX; + } + + if (job->signing || job->seen) { + return job->total && job->zip ? (int32_t)((uint64_t)pkgZipOffset(job->zip) * PERCENT_MAX / job->total) : 0; + } + + if (job->stage == DpkHashE) { + return count ? job->at * HASH_SHARE / count : 0; + } + + uint64_t done = job->added + (job->stage == DpkFilesE ? pkgZipAdded(job->w) : 0); + + return HASH_SHARE + (job->bytes ? (int32_t)(done * (PERCENT_MAX - HASH_SHARE) / job->bytes) : 0); +} + + +PkgDpkStepE pkgDpkJobStep(PkgDpkJobT *job, int32_t maxWork) { + switch (job->stage) { + case DpkHashE: + hashStep(job, maxWork); + break; + + case DpkManifestE: + manifestStep(job); + break; + + case DpkHeadE: + headStep(job, maxWork); + break; + + case DpkFilesE: + filesStep(job, maxWork); + break; + + case DpkCopyE: + copyStep(job, maxWork); + break; + + case DpkCheckE: + checkStep(job, maxWork); + break; + + case DpkFinishE: + finishStep(job); + break; + + case DpkDoneE: + case DpkFailedE: + break; + } + + if (job->stage == DpkDoneE) { + return PkgDpkDoneE; + } + + return job->stage == DpkFailedE ? PkgDpkFailedE : PkgDpkMoreE; +} + + bool pkgDpkReadInfo(const char *path, PkgDpkInfoT *info, char *err, int32_t errSize) { FILE *f = fopen(path, "rb"); - PkgZipT *zip = f ? pkgZipOpen(fileRead, f) : NULL; + PkgZipT *zip = f ? pkgZipOpen(pkgZipFileRead, f) : NULL; MemSinkT text = { NULL, 0, 0 }; PkgZipEntryT e; bool ok = zip != NULL; @@ -382,7 +806,7 @@ bool pkgDpkReadInfo(const char *path, PkgDpkInfoT *info, char *err, int32_t errS } else if (pkgZipNext(zip, &e) != PkgZipEntryE || e.isDir || strcasecmp(e.name, PKG_MANIFEST_NAME) != 0) { ok = fail(err, errSize, "%s is not a DVX package (it does not start with %s).", path, PKG_MANIFEST_NAME); } else if (!readEntry(zip, &e, PKG_MANIFEST_MAX, &text)) { - ok = fail(err, errSize, "The manifest of %s cannot be read: %s", path, zipReason(zip)); + ok = fail(err, errSize, "The manifest of %s cannot be read: %s", path, pkgZipProblemText(zip)); } else if (!pkgManifestParse((const char *)text.buf, (int32_t)text.len, &info->manifest, err, errSize)) { ok = false; } else if (!pkgManifestCheck(&info->manifest, err, errSize)) { @@ -436,6 +860,10 @@ bool pkgDpkSign(const char *path, const PkgSecretT *secret, bool detached, char char other[DVX_MAX_PATH]; bool ok; + if (!detached) { + return jobRun(pkgDpkSignStart(path, secret, err, errSize), err, errSize); + } + if (!pkgDpkReadInfo(path, &info, err, errSize)) { return false; } @@ -445,55 +873,18 @@ bool pkgDpkSign(const char *path, const PkgSecretT *secret, bool detached, char ok = sigText != NULL; if (!ok) { - fail(err, errSize, "There is not enough memory."); + fail(err, errSize, PKG_ERR_NO_MEMORY); } - if (ok && !siblingPath(path, detached ? PKG_SIG_EXT : TEMP_EXT, other, sizeof(other))) { + if (ok && !platformPathSibling(other, sizeof(other), path, PKG_SIG_EXT)) { ok = fail(err, errSize, "The path of %s is too long.", path); } - if (ok && detached) { + if (ok) { FILE *f = fopen(other, "wb"); ok = f && fwrite(sigText, 1, (size_t)sigLen, f) == (size_t)sigLen; ok = (f && fclose(f) == 0 && ok) || fail(err, errSize, "Cannot write %s.", other); - } else if (ok) { - // Rewrite the package: the manifest, the new signature, then every - // file as it was (an old signature left out) - FILE *f = fopen(path, "rb"); - PkgZipT *zip = f ? pkgZipOpen(fileRead, f) : NULL; - PkgZipWriterT *w = zip ? pkgZipCreate(other) : NULL; - PkgZipEntryT e; - PkgZipStepE step = PkgZipFailE; - - ok = w && pkgZipAddMem(w, PKG_MANIFEST_NAME, info.text, (uint32_t)info.textLen, time(NULL)) && pkgZipAddMem(w, PKG_SIG_NAME, sigText, (uint32_t)sigLen, time(NULL)); - - while (ok && (step = pkgZipNext(zip, &e)) == PkgZipEntryE) { - MemSinkT data = { NULL, 0, 0 }; - bool keep = !e.isDir && strcasecmp(e.name, PKG_MANIFEST_NAME) != 0 && strcasecmp(e.name, PKG_SIG_NAME) != 0; - - ok = readEntry(zip, &e, e.size, &data) && (!keep || pkgZipAddMem(w, e.name, data.buf, data.len, pkgZipDosTime(e.dosDate, e.dosTime))); - free(data.buf); - } - - ok = ok && step == PkgZipEndE; - - if (zip) { - pkgZipClose(zip); - } - - if (f) { - fclose(f); - } - - if (!ok) { - pkgZipAbort(w); - ok = fail(err, errSize, "Cannot rewrite %s.", path); - } else if (!pkgZipFinish(w)) { - ok = fail(err, errSize, "Cannot write %s.", other); - } else { - ok = (remove(path) == 0 && rename(other, path) == 0) || fail(err, errSize, "Cannot replace %s (the signed copy is %s).", path, other); - } } free(sigText); @@ -520,6 +911,61 @@ const PkgKeyT *pkgDpkSigner(const PkgDpkInfoT *info, const PkgSigT *detached, co } +PkgDpkJobT *pkgDpkSignStart(const char *path, const PkgSecretT *secret, char *err, int32_t errSize) { + PkgDpkJobT *job = (PkgDpkJobT *)calloc(1, sizeof(PkgDpkJobT)); + PkgDpkInfoT info; + char other[DVX_MAX_PATH]; + struct stat st; + + if (!job) { + fail(err, errSize, PKG_ERR_NO_MEMORY); + return NULL; + } + + if (!pkgDpkReadInfo(path, &info, err, errSize)) { + free(job); + return NULL; + } + + // The manifest's bytes as they were signed; the rest of what was read + // goes + job->text = info.text; + job->textLen = info.textLen; + info.text = NULL; + pkgDpkInfoFree(&info); + + job->secret = *secret; + job->keyed = true; + job->signing = strdup(path); + job->total = stat(path, &st) == 0 ? (uint32_t)st.st_size : 0; + job->src = fopen(path, "rb"); + job->zip = job->src ? pkgZipOpen(pkgZipFileRead, job->src) : NULL; + + if (!job->signing || !job->zip) { + fail(err, errSize, "Cannot read %s.", path); + } else if (!platformPathSibling(other, sizeof(other), path, TEMP_EXT)) { + fail(err, errSize, "The path of %s is too long.", path); + } else { + job->path = strdup(other); + + if (!job->path) { + fail(err, errSize, PKG_ERR_NO_MEMORY); + } else { + headBegin(job); + + if (job->stage == DpkHeadE) { + return job; + } + + fail(err, errSize, "%s", job->err); + } + } + + pkgDpkJobFree(job); + return NULL; +} + + // The whole of entry e (at most max bytes) into a new buffer in sink (the // caller frees sink->buf). static bool readEntry(PkgZipT *zip, const PkgZipEntryT *e, uint32_t max, MemSinkT *sink) { @@ -544,26 +990,6 @@ static bool readEntry(PkgZipT *zip, const PkgZipEntryT *e, uint32_t max, MemSink } -// True when a and b name the same existing file. -static bool sameFile(const char *a, const char *b) { - struct stat sa; - struct stat sb; - - return stat(a, &sa) == 0 && stat(b, &sb) == 0 && sa.st_ino == sb.st_ino && sa.st_dev == sb.st_dev && sa.st_ino != 0; -} - - -// path with its extension (if any) replaced by ext. -static bool siblingPath(const char *path, const char *ext, char *out, int32_t outSize) { - const char *base = platformPathBaseName(path); - const char *dot = strrchr(base, DOT); - int32_t keep = dot ? (int32_t)(dot - path) : (int32_t)strlen(path); - int32_t n = snprintf(out, (size_t)outSize, "%.*s%s", (int)keep, path, ext); - - return n > 0 && n < outSize; -} - - // files (stb_ds) in name order (qsort is not given an empty one: it // would be NULL). static void sortFiles(PkgDpkFileT *files) { @@ -627,20 +1053,3 @@ static bool walk(const char *folder, const char *rel, const char *prefix, PkgDpk closedir(d); return ok; } - - -static const char *zipReason(const PkgZipT *zip) { - switch (pkgZipProblem(zip)) { - case PkgZipReadFailedE: - return "it cannot be read."; - - case PkgZipUnreadableE: - return "it uses a form DVX cannot read."; - - case PkgZipWriteFailedE: - return "there is not enough memory, or a part is too big."; - - default: - return "it is damaged."; - } -} diff --git a/src/libs/kpunch/pkg/pkgDpk.h b/src/libs/kpunch/pkg/pkgDpk.h index 0b2110dc..7cbedb1f 100644 --- a/src/libs/kpunch/pkg/pkgDpk.h +++ b/src/libs/kpunch/pkg/pkgDpk.h @@ -43,6 +43,19 @@ #define PKG_MANIFEST_MAX (256 * 1024) // largest manifest read +#define PKG_DPK_STEP 4096 // work a job step: bytes hashed, deflated, or copied + +// A package being built or signed a step at a time (pkgDpkBuildStart, +// pkgDpkSignStart), so a program can draw progress and stay responsive +// between steps +typedef struct PkgDpkJobT PkgDpkJobT; + +typedef enum { + PkgDpkMoreE, // more to do + PkgDpkDoneE, // the package is written + PkgDpkFailedE // it is not (pkgDpkJobMessage says why; nothing is left behind) +} PkgDpkStepE; + // What a package says about itself (its first entries) typedef struct { PkgManifestT manifest; @@ -62,19 +75,27 @@ typedef struct { // Build outPath from a template manifest (its [files], if any, is // replaced; a program, library, or widget with no abi gets this DVX's, // DVX_ABI_VERSION) and the files under folder (pkgDpkFolderFiles, no -// prefix), signed with secret when not NULL. False with the reason in -// err (and no file left behind). +// prefix), signed with secret when not NULL, all at once. False with the +// reason in err (and no file left behind). bool pkgDpkBuild(const PkgManifestT *templ, const char *folder, const char *outPath, const PkgSecretT *secret, char *err, int32_t errSize); // The same from files (count of them, any order; one that is outPath is // left out). bool pkgDpkBuildFiles(const PkgManifestT *templ, const PkgDpkFileT *files, int32_t count, const char *outPath, const PkgSecretT *secret, char *err, int32_t errSize); +// Start building as pkgDpkBuildFiles (what it is given is copied); run it +// with pkgDpkJobStep. NULL with the reason in err. +PkgDpkJobT *pkgDpkBuildStart(const PkgManifestT *templ, const PkgDpkFileT *files, int32_t count, const char *outPath, const PkgSecretT *secret, char *err, int32_t errSize); + // Check every file of the package at path against info's manifest: each -// entry listed, of its size and SHA-256, and each listed file there. -// False with the reason in err. +// entry listed, of its size and SHA-256, and each listed file there, all +// at once (pkgDpkCheckStart run to its end). False with the reason in err. bool pkgDpkCheckFiles(const char *path, const PkgDpkInfoT *info, char *err, int32_t errSize); +// Start checking as pkgDpkCheckFiles (info's manifest is copied); run it +// with pkgDpkJobStep. NULL with the reason in err. +PkgDpkJobT *pkgDpkCheckStart(const char *path, const PkgDpkInfoT *info, char *err, int32_t errSize); + void pkgDpkFilesFree(PkgDpkFileT *files); // Every file under folder (but a PACKAGE.INI or PACKAGE.SIG at its top) @@ -86,16 +107,38 @@ bool pkgDpkFolderFiles(const char *folder, const char *prefix, PkgDpkF void pkgDpkInfoFree(PkgDpkInfoT *info); +// The name (in the package) of the file the job is on ("" when none). +const char *pkgDpkJobCurrent(const PkgDpkJobT *job); + +// Stop the job if it is not done (the package it was writing removed), +// and free it. NULL is ignored. +void pkgDpkJobFree(PkgDpkJobT *job); + +// Why the job failed ("" when it did not). +const char *pkgDpkJobMessage(const PkgDpkJobT *job); + +// How far the job is, 0 to 100. +int32_t pkgDpkJobPercent(const PkgDpkJobT *job); + +// Run the job for about maxWork bytes of work (at least one piece): +// PkgDpkMoreE, PkgDpkDoneE, or PkgDpkFailedE; then the same again. +PkgDpkStepE pkgDpkJobStep(PkgDpkJobT *job, int32_t maxWork); + // Read the manifest (and the publisher's signature, if any) of the // package at path, and check the manifest (pkgManifestCheck). False with // the reason in err (nothing to free). bool pkgDpkReadInfo(const char *path, PkgDpkInfoT *info, char *err, int32_t errSize); -// Sign the package at path with secret: into it (PACKAGE.SIG, replacing -// one that is there; the package is rewritten) or, when detached, as -// NAME.SIG beside it. False with the reason in err. +// Sign the package at path with secret, all at once: into it +// (PACKAGE.SIG, replacing one that is there; the package is rewritten, +// its files copied as they are packed) or, when detached, as NAME.SIG +// beside it. False with the reason in err. bool pkgDpkSign(const char *path, const PkgSecretT *secret, bool detached, char *err, int32_t errSize); +// Start signing the package at path into it, as pkgDpkSign; run it with +// pkgDpkJobStep. NULL with the reason in err. +PkgDpkJobT *pkgDpkSignStart(const char *path, const PkgSecretT *secret, char *err, int32_t errSize); + // The one of keys (count of them) that made info's signature or the // detached one (NULL: none), or NULL when neither is by any of them. const PkgKeyT *pkgDpkSigner(const PkgDpkInfoT *info, const PkgSigT *detached, const PkgKeyT *keys, int32_t count); diff --git a/src/libs/kpunch/pkg/pkgFormat.c b/src/libs/kpunch/pkg/pkgFormat.c index e566598f..769d9b36 100644 --- a/src/libs/kpunch/pkg/pkgFormat.c +++ b/src/libs/kpunch/pkg/pkgFormat.c @@ -49,10 +49,6 @@ _Static_assert(PKG_HEADER_SIZE == HDR_CRC + HDR_CRC_SIZE, "PKG_HEADER_SIZE is th #define TIER_BYTES 8 #define TIER_BYTES_HI 12 -#define BYTE_BITS 8 -#define BYTE_MASK 0xFF -#define HALF_BITS 16 -#define HALF_MASK 0xFFFFu #define WORD_BITS 32 // The separator of local paths (DVX_PATH_SEP, without needing dvxPlat.h: @@ -71,8 +67,6 @@ _Static_assert(PKG_HEADER_SIZE == HDR_CRC + HDR_CRC_SIZE, "PKG_HEADER_SIZE is th // ============================================================ static bool dosPartIsValid(const char *part, int32_t len); -uint16_t pkgGet16(const uint8_t *p); -uint32_t pkgGet32(const uint8_t *p); bool pkgHeaderDecode(const uint8_t *in, PkgHeaderT *h); void pkgHeaderEncode(const PkgHeaderT *h, uint8_t *out); bool pkgHeaderRead(FILE *f, PkgHeaderT *h, uint32_t *dataStart); @@ -80,8 +74,6 @@ bool pkgLocalPath(const char *dir, const char *relName, char *out, in bool pkgNameIsValid(const char *name); bool pkgPartName(int32_t disk, char *out, int32_t outSize); int32_t pkgPartNumber(const char *name); -void pkgPut16(uint8_t *p, uint16_t v); -void pkgPut32(uint8_t *p, uint32_t v); // One name part: base of 1-8 characters, optional '.' and 1-3 more. @@ -110,31 +102,21 @@ static bool dosPartIsValid(const char *part, int32_t len) { } -uint16_t pkgGet16(const uint8_t *p) { - return (uint16_t)(p[0] | (p[1] << BYTE_BITS)); -} - - -uint32_t pkgGet32(const uint8_t *p) { - return (uint32_t)pkgGet16(p) | ((uint32_t)pkgGet16(p + sizeof(uint16_t)) << HALF_BITS); -} - - bool pkgHeaderDecode(const uint8_t *in, PkgHeaderT *h) { - if (memcmp(in, PKG_MAGIC, PKG_MAGIC_SIZE) != 0 || pkgGet16(in + HDR_VERSION) != PKG_FORMAT_VERSION) { + if (memcmp(in, PKG_MAGIC, PKG_MAGIC_SIZE) != 0 || dvxGetLe16(in + HDR_VERSION) != PKG_FORMAT_VERSION) { return false; } - if ((uint32_t)mz_crc32(MZ_CRC32_INIT, in, HDR_CRC) != pkgGet32(in + HDR_CRC)) { + if ((uint32_t)mz_crc32(MZ_CRC32_INIT, in, HDR_CRC) != dvxGetLe32(in + HDR_CRC)) { return false; } - h->disk = pkgGet16(in + HDR_DISK); - h->diskCount = pkgGet16(in + HDR_DISK_COUNT); - h->setId = pkgGet32(in + HDR_SET_ID); - h->partOffset = pkgGet32(in + HDR_PART_OFFSET); - h->partLength = pkgGet32(in + HDR_PART_LENGTH); - h->zipSize = pkgGet32(in + HDR_ZIP_SIZE); + h->disk = dvxGetLe16(in + HDR_DISK); + h->diskCount = dvxGetLe16(in + HDR_DISK_COUNT); + h->setId = dvxGetLe32(in + HDR_SET_ID); + h->partOffset = dvxGetLe32(in + HDR_PART_OFFSET); + h->partLength = dvxGetLe32(in + HDR_PART_LENGTH); + h->zipSize = dvxGetLe32(in + HDR_ZIP_SIZE); // Each tier ends where the one before it does or later, inside the ZIP bool ok = h->disk >= 1 && h->disk <= h->diskCount && h->diskCount <= PKG_MAX_DISKS && h->partOffset <= h->zipSize && h->partLength <= h->zipSize - h->partOffset; @@ -144,9 +126,9 @@ bool pkgHeaderDecode(const uint8_t *in, PkgHeaderT *h) { const uint8_t *p = in + HDR_TIERS + t * HDR_TIER_SIZE; PkgTierT *tier = &h->tiers[t]; - tier->end = pkgGet32(p + TIER_END); - tier->fileCount = pkgGet32(p + TIER_FILES); - tier->bytes = (uint64_t)pkgGet32(p + TIER_BYTES) | ((uint64_t)pkgGet32(p + TIER_BYTES_HI) << WORD_BITS); + tier->end = dvxGetLe32(p + TIER_END); + tier->fileCount = dvxGetLe32(p + TIER_FILES); + tier->bytes = (uint64_t)dvxGetLe32(p + TIER_BYTES) | ((uint64_t)dvxGetLe32(p + TIER_BYTES_HI) << WORD_BITS); ok = ok && tier->end >= prev.end && tier->end <= h->zipSize && tier->fileCount >= prev.fileCount && tier->bytes >= prev.bytes; prev = *tier; } @@ -158,25 +140,25 @@ bool pkgHeaderDecode(const uint8_t *in, PkgHeaderT *h) { void pkgHeaderEncode(const PkgHeaderT *h, uint8_t *out) { memset(out, 0, PKG_HEADER_SIZE); memcpy(out, PKG_MAGIC, PKG_MAGIC_SIZE); - pkgPut16(out + HDR_VERSION, PKG_FORMAT_VERSION); - pkgPut16(out + HDR_DISK, h->disk); - pkgPut16(out + HDR_DISK_COUNT, h->diskCount); - pkgPut32(out + HDR_SET_ID, h->setId); - pkgPut32(out + HDR_PART_OFFSET, h->partOffset); - pkgPut32(out + HDR_PART_LENGTH, h->partLength); - pkgPut32(out + HDR_ZIP_SIZE, h->zipSize); + dvxPutLe16(out + HDR_VERSION, PKG_FORMAT_VERSION); + dvxPutLe16(out + HDR_DISK, h->disk); + dvxPutLe16(out + HDR_DISK_COUNT, h->diskCount); + dvxPutLe32(out + HDR_SET_ID, h->setId); + dvxPutLe32(out + HDR_PART_OFFSET, h->partOffset); + dvxPutLe32(out + HDR_PART_LENGTH, h->partLength); + dvxPutLe32(out + HDR_ZIP_SIZE, h->zipSize); for (int32_t t = 0; t < PKG_TIERS; t++) { uint8_t *p = out + HDR_TIERS + t * HDR_TIER_SIZE; const PkgTierT *tier = &h->tiers[t]; - pkgPut32(p + TIER_END, tier->end); - pkgPut32(p + TIER_FILES, tier->fileCount); - pkgPut32(p + TIER_BYTES, (uint32_t)tier->bytes); - pkgPut32(p + TIER_BYTES_HI, (uint32_t)(tier->bytes >> WORD_BITS)); + dvxPutLe32(p + TIER_END, tier->end); + dvxPutLe32(p + TIER_FILES, tier->fileCount); + dvxPutLe32(p + TIER_BYTES, (uint32_t)tier->bytes); + dvxPutLe32(p + TIER_BYTES_HI, (uint32_t)(tier->bytes >> WORD_BITS)); } - pkgPut32(out + HDR_CRC, (uint32_t)mz_crc32(MZ_CRC32_INIT, out, HDR_CRC)); + dvxPutLe32(out + HDR_CRC, (uint32_t)mz_crc32(MZ_CRC32_INIT, out, HDR_CRC)); } @@ -276,13 +258,3 @@ int32_t pkgPartNumber(const char *name) { } -void pkgPut16(uint8_t *p, uint16_t v) { - p[0] = (uint8_t)(v & BYTE_MASK); - p[1] = (uint8_t)(v >> BYTE_BITS); -} - - -void pkgPut32(uint8_t *p, uint32_t v) { - pkgPut16(p, (uint16_t)(v & HALF_MASK)); - pkgPut16(p + sizeof(uint16_t), (uint16_t)(v >> HALF_BITS)); -} diff --git a/src/libs/kpunch/pkg/pkgFormat.h b/src/libs/kpunch/pkg/pkgFormat.h index 06b78490..bf0cde07 100644 --- a/src/libs/kpunch/pkg/pkgFormat.h +++ b/src/libs/kpunch/pkg/pkgFormat.h @@ -58,6 +58,8 @@ #include #include +#include "dvxEndian.h" + // Part header: magic, format version, then the fields of PkgHeaderT and // a CRC-32 of everything before it. Little-endian. #define PKG_HEADER_SIZE 68 @@ -172,10 +174,13 @@ bool pkgPartName(int32_t disk, char *out, int32_t outSize); // one. int32_t pkgPartNumber(const char *name); -// Little-endian field access (shared with the ZIP reader and writer) -uint16_t pkgGet16(const uint8_t *p); -uint32_t pkgGet32(const uint8_t *p); -void pkgPut16(uint8_t *p, uint16_t v); -void pkgPut32(uint8_t *p, uint32_t v); +// Exactly n bytes from n * PKG_HEX_PER_BYTE hex digits (either case) and +// nothing more (a manifest's SHA-256, a key's id); bytes as lower-case hex +// digits (out holds n * PKG_HEX_PER_BYTE + 1). +#define PKG_HEX_PER_BYTE 2 + +bool pkgHexDecode(const char *text, uint8_t *bytes, int32_t n); +void pkgHexEncode(const uint8_t *bytes, int32_t n, char *out); + #endif // PKG_FORMAT_H diff --git a/src/libs/kpunch/pkg/pkgIni.c b/src/libs/kpunch/pkg/pkgIni.c index 335fdd06..05e2b8c8 100644 --- a/src/libs/kpunch/pkg/pkgIni.c +++ b/src/libs/kpunch/pkg/pkgIni.c @@ -23,17 +23,12 @@ // pkgIni.c -- the package library's INI reader and hex (see pkgInt.h) #include "pkgInt.h" +#include "dvxHash.h" #include #include -#define HEX_DIGITS "0123456789abcdef" -#define HEX_RADIX 16 -#define HEX_PER_BYTE 2 -#define HEX_LETTER_BASE 10 // the value of 'a' #define SECTION_BRACKETS 2 // '[' and ']' -#define NIBBLE_BITS 4 -#define NIBBLE_MASK 0x0F #define COMMENT_SEMICOLON ';' #define COMMENT_HASH '#' #define SECTION_OPEN '[' @@ -44,7 +39,6 @@ // Prototypes // ============================================================ -static int32_t hexValue(char c); bool pkgHexDecode(const char *text, uint8_t *bytes, int32_t n); void pkgHexEncode(const uint8_t *bytes, int32_t n, char *out); PkgIniStepE pkgIniNext(PkgIniT *ini); @@ -52,30 +46,21 @@ void pkgIniStart(PkgIniT *ini, const char *text, int32_t len); static bool trimCopy(char *out, int32_t outSize, const char *from, int32_t len); -static int32_t hexValue(char c) { - if (c >= '0' && c <= '9') { - return c - '0'; - } - - c = (char)tolower((unsigned char)c); - return (c >= 'a' && c <= 'f') ? c - 'a' + HEX_LETTER_BASE : -1; -} - bool pkgHexDecode(const char *text, uint8_t *bytes, int32_t n) { - if ((int32_t)strlen(text) != n * HEX_PER_BYTE) { + if ((int32_t)strlen(text) != n * PKG_HEX_PER_BYTE) { return false; } for (int32_t i = 0; i < n; i++) { - int32_t hi = hexValue(text[i * HEX_PER_BYTE]); - int32_t lo = hexValue(text[i * HEX_PER_BYTE + 1]); + int32_t hi = dvxHexValue((uint8_t)text[i * PKG_HEX_PER_BYTE]); + int32_t lo = dvxHexValue((uint8_t)text[i * PKG_HEX_PER_BYTE + 1]); if (hi < 0 || lo < 0) { return false; } - bytes[i] = (uint8_t)(hi * HEX_RADIX + lo); + bytes[i] = (uint8_t)(hi * DVX_HEX_RADIX + lo); } return true; @@ -84,11 +69,11 @@ bool pkgHexDecode(const char *text, uint8_t *bytes, int32_t n) { void pkgHexEncode(const uint8_t *bytes, int32_t n, char *out) { for (int32_t i = 0; i < n; i++) { - out[i * HEX_PER_BYTE] = HEX_DIGITS[bytes[i] >> NIBBLE_BITS]; - out[i * HEX_PER_BYTE + 1] = HEX_DIGITS[bytes[i] & NIBBLE_MASK]; + out[i * PKG_HEX_PER_BYTE] = DVX_HEX_DIGITS[bytes[i] >> DVX_NIBBLE_BITS]; + out[i * PKG_HEX_PER_BYTE + 1] = DVX_HEX_DIGITS[bytes[i] & DVX_NIBBLE_MASK]; } - out[n * HEX_PER_BYTE] = '\0'; + out[n * PKG_HEX_PER_BYTE] = '\0'; } diff --git a/src/libs/kpunch/pkg/pkgInt.h b/src/libs/kpunch/pkg/pkgInt.h index be7bf04e..6c9bd57b 100644 --- a/src/libs/kpunch/pkg/pkgInt.h +++ b/src/libs/kpunch/pkg/pkgInt.h @@ -22,12 +22,13 @@ // pkgInt.h -- the package library's own helpers: reading the small INI // texts of manifests, keys, and signatures (one strict reader for all of -// them), and hex. +// them), and the messages its parts share. #ifndef PKG_INT_H #define PKG_INT_H #include "pkgFormat.h" +#include "pkgSign.h" #include #include @@ -35,6 +36,8 @@ #define PKG_INI_SECTION_MAX 32 #define PKG_INI_VALUE_MAX 160 +#define PKG_ERR_NO_MEMORY "There is not enough memory." + typedef enum { PkgIniSectionE, // a [section] line: ini->section PkgIniKeyE, // a key = value line: ini->key, ini->value (trimmed) @@ -52,16 +55,14 @@ typedef struct { char value[PKG_INI_VALUE_MAX]; } PkgIniT; -// Bytes as lower-case hex digits (out holds 2 * n + 1). -void pkgHexEncode(const uint8_t *bytes, int32_t n, char *out); - -// Exactly n bytes from 2 * n hex digits (either case) and nothing more. -bool pkgHexDecode(const char *text, uint8_t *bytes, int32_t n); - // The next section or key line (blank lines and lines starting with ';' // or '#' are skipped; a line ends at CR LF or LF). PkgIniStepE pkgIniNext(PkgIniT *ini); void pkgIniStart(PkgIniT *ini, const char *text, int32_t len); +// A key's id: the start of the SHA-256 of its public key (pkgSign.c; +// pkgSignMake.c uses it too). +void pkgKeyIdOf(const uint8_t publicKey[PKG_PUBLIC_SIZE], uint8_t id[PKG_KEY_ID_SIZE]); + #endif // PKG_INT_H diff --git a/src/libs/kpunch/pkg/pkgManifest.c b/src/libs/kpunch/pkg/pkgManifest.c index 309f7573..7d5080e9 100644 --- a/src/libs/kpunch/pkg/pkgManifest.c +++ b/src/libs/kpunch/pkg/pkgManifest.c @@ -51,7 +51,6 @@ DVX_ABI_STAMP(pkg); #define TEXT_FIRST 512 // the formatted manifest's first capacity #define LINE_MAX (PKG_NAME_MAX + PKG_SHA256_SIZE * 2 + 32) #define EOL "\r\n" -#define SHA_HEX_SIZE (PKG_SHA256_SIZE * 2 + 1) #define PATH_SEP_TEXT "/" // The keys of [package] (sPackageKeys order) @@ -120,12 +119,14 @@ static const char *const sSystemOwnDirs[] = { PKG_DB_DIR, PKG_KEYS_DIR }; // Prototypes // ============================================================ +static int compareNames(const void *a, const void *b); static void copyText(char *out, int32_t outSize, const char *value, int32_t line, bool *ok, char *err, int32_t errSize); static bool fail(char *err, int32_t errSize, const char *fmt, ...) __attribute__((format(printf, 3, 4))); static int32_t folderText(char *out, int32_t outSize, const char *root, const char *vendor, const char *pkgName); static bool idCharValid(char c); static bool idPartValid(const char *part, int32_t len); static bool inFolder(const char *name, const char *root, const char *vendor, const char *pkgName); +static bool listedTwice(const PkgManifestT *m, const char **twice); static bool parseFileLine(PkgManifestT *m, const PkgIniT *ini, char *err, int32_t errSize); static bool parsePackageKey(PkgManifestT *m, const PkgIniT *ini, uint32_t *seen, char *err, int32_t errSize); static bool parseText(const char *text, int32_t len, bool template, PkgManifestT *m, char *err, int32_t errSize); @@ -142,6 +143,7 @@ bool pkgKindParse(const char *text, PkgKindE *kind); void pkgManifestAddDepend(PkgManifestT *m, const char *id, PkgVersionT min); bool pkgManifestAddFile(PkgManifestT *m, const char *name, uint32_t size, const uint8_t sha256[PKG_SHA256_SIZE]); bool pkgManifestCheck(const PkgManifestT *m, char *err, int32_t errSize); +bool pkgManifestCopy(PkgManifestT *dst, const PkgManifestT *src); const PkgFileT *pkgManifestFindFile(const PkgManifestT *m, const char *name); char *pkgManifestFormat(const PkgManifestT *m, int32_t *outLen); void pkgManifestFree(PkgManifestT *m); @@ -156,6 +158,12 @@ static void textAdd(TextT *t, const char *fmt, ...) __attribute__((format // A text value into its field; a value too long fails the parse. +// Two file names, as DOS compares them (for qsort). +static int compareNames(const void *a, const void *b) { + return strcasecmp(*(const char *const *)a, *(const char *const *)b); +} + + static void copyText(char *out, int32_t outSize, const char *value, int32_t line, bool *ok, char *err, int32_t errSize) { if ((int32_t)strlen(value) >= outSize) { *ok = fail(err, errSize, "Line %d: the value is too long (at most %d characters).", (int)line, (int)outSize - 1); @@ -225,8 +233,37 @@ static bool inFolder(const char *name, const char *root, const char *vendor, con // A [files] line: name = size sha256. +// *twice: a name m lists twice (as DOS compares names), or NULL: the names +// sorted, so the same ones are side by side. False when out of memory. +static bool listedTwice(const PkgManifestT *m, const char **twice) { + int32_t count = (int32_t)arrlen(m->files); + const char **names = (const char **)malloc((size_t)(count ? count : 1) * sizeof(*names)); + + *twice = NULL; + + if (!names) { + return false; + } + + for (int32_t i = 0; i < count; i++) { + names[i] = m->files[i].name; + } + + qsort(names, (size_t)count, sizeof(*names), compareNames); + + for (int32_t i = 1; i < count && !*twice; i++) { + if (strcasecmp(names[i - 1], names[i]) == 0) { + *twice = names[i]; + } + } + + free(names); + return true; +} + + static bool parseFileLine(PkgManifestT *m, const PkgIniT *ini, char *err, int32_t errSize) { - char hex[SHA_HEX_SIZE]; + char hex[PKG_SHA256_TEXT]; uint8_t sha[PKG_SHA256_SIZE]; uint32_t size = 0; char *end = NULL; @@ -244,7 +281,7 @@ static bool parseFileLine(PkgManifestT *m, const PkgIniT *ini, char *err, int32_ snprintf(hex, sizeof(hex), "%s", end); - if (strlen(end) != SHA_HEX_SIZE - 1 || !pkgHexDecode(hex, sha, PKG_SHA256_SIZE)) { + if (strlen(end) != PKG_SHA256_TEXT - 1 || !pkgHexDecode(hex, sha, PKG_SHA256_SIZE)) { return fail(err, errSize, "Line %d: the SHA-256 of %s is not 64 hex digits.", (int)ini->line, ini->key); } @@ -591,12 +628,16 @@ bool pkgManifestCheck(const PkgManifestT *m, char *err, int32_t errSize) { if (!pkgFileAllowed(m, name)) { return fail(err, errSize, "This %s package may not put a file at %s.", pkgKindName(m->kind), name); } + } - for (int32_t j = 0; j < i; j++) { - if (strcasecmp(m->files[j].name, name) == 0) { - return fail(err, errSize, "%s is listed twice.", name); - } - } + const char *twice = NULL; + + if (!listedTwice(m, &twice)) { + return fail(err, errSize, PKG_ERR_NO_MEMORY); + } + + if (twice) { + return fail(err, errSize, "%s is listed twice.", twice); } for (int32_t i = 0; i < (int32_t)arrlen(m->depends); i++) { @@ -621,6 +662,28 @@ bool pkgManifestCheck(const PkgManifestT *m, char *err, int32_t errSize) { } +bool pkgManifestCopy(PkgManifestT *dst, const PkgManifestT *src) { + *dst = *src; + dst->files = NULL; + dst->depends = NULL; + + for (int32_t i = 0; i < (int32_t)arrlen(src->depends); i++) { + arrput(dst->depends, src->depends[i]); + } + + for (int32_t i = 0; i < (int32_t)arrlen(src->files); i++) { + const PkgFileT *f = &src->files[i]; + + if (!pkgManifestAddFile(dst, f->name, f->size, f->sha256)) { + pkgManifestFree(dst); + return false; + } + } + + return true; +} + + const PkgFileT *pkgManifestFindFile(const PkgManifestT *m, const char *name) { for (int32_t i = 0; i < (int32_t)arrlen(m->files); i++) { if (strcasecmp(m->files[i].name, name) == 0) { @@ -688,7 +751,7 @@ char *pkgManifestFormat(const PkgManifestT *m, int32_t *outLen) { } for (int32_t i = 0; i < (int32_t)arrlen(m->files); i++) { - char hex[SHA_HEX_SIZE]; + char hex[PKG_SHA256_TEXT]; pkgHexEncode(m->files[i].sha256, PKG_SHA256_SIZE, hex); textAdd(&t, "%s = %" PRIu32 " %s" EOL, m->files[i].name, m->files[i].size, hex); diff --git a/src/libs/kpunch/pkg/pkgManifest.h b/src/libs/kpunch/pkg/pkgManifest.h index c5e6c326..04e28216 100644 --- a/src/libs/kpunch/pkg/pkgManifest.h +++ b/src/libs/kpunch/pkg/pkgManifest.h @@ -78,6 +78,7 @@ #define PKG_TEXT_MAX 128 // name, summary, publisher, ... #define PKG_VERSION_MAX 18 // "65535.65535.65535" and NUL #define PKG_SHA256_SIZE 32 +#define PKG_SHA256_TEXT (PKG_SHA256_SIZE * PKG_HEX_PER_BYTE + 1) // a SHA-256 as hex, with the NUL #define PKG_SYSTEM_ID "kpunch.dvx" // the one `system` package: DVX itself #define PKG_BASE_FONTS_ID "kpunch.basefont" // DVX's base set of fonts (src/fonts/PACKAGE.INI) #define PKG_DVX_NAME "DVX" // its manifest (pkgDvxManifest) @@ -151,6 +152,10 @@ bool pkgManifestAddFile(PkgManifestT *m, const char *name, uint32_t size, // False with the reason in err. bool pkgManifestCheck(const PkgManifestT *m, char *err, int32_t errSize); +// dst as a copy of src, with its own files and dependencies. False (dst +// freed) when out of memory. +bool pkgManifestCopy(PkgManifestT *dst, const PkgManifestT *src); + // The file named name (any case), or NULL. const PkgFileT *pkgManifestFindFile(const PkgManifestT *m, const char *name); diff --git a/src/libs/kpunch/pkg/pkgOp.c b/src/libs/kpunch/pkg/pkgOp.c index ca7029b9..78c671a4 100644 --- a/src/libs/kpunch/pkg/pkgOp.c +++ b/src/libs/kpunch/pkg/pkgOp.c @@ -25,10 +25,11 @@ // (pkgOp.h) #include "pkgOp.h" +#include "pkgInt.h" #include "pkgPlan.h" #include "pkgZip.h" #include "dvxPlat.h" -#include "serial/security/secHash.h" +#include "crypto/secHash.h" #include "stb_ds_wrap.h" #include @@ -44,6 +45,7 @@ #define OP_FILES_MAX 99999 // what those names can number #define OP_NAME_MAX 16 // "PKG00000.$$$" and NUL #define OP_KB 1024 +#define OP_SWAP_STEP 8 // files swapped in, put back, or cleared a step typedef enum { OpInstallE, @@ -53,6 +55,8 @@ typedef enum { typedef enum { StageUnpackE, StageSwapE, + StageUndoE, // the swap failed (msg says why): every file as it was + StageAsideE, // swapped in: the files it replaced go StageDropE, // install: the old version's leftovers; remove: the package's files StageTidyE, StageRecordE, @@ -78,6 +82,9 @@ struct PkgOpT { SecSha256T outSha; uint32_t outSize; bool *scratch; // per manifest file: its scratch file is there + bool *aside; // per manifest file: the file it replaces is set aside + bool *placed; // per manifest file: it is in place + int32_t swapAt; // the manifest file the swap (or its undoing or clearing) is on PkgFileT *drop; // files to check and delete (stb_ds; names owned) int32_t dropAt; FILE *hashFile; // the one being checked @@ -99,24 +106,27 @@ static void addDirs(PkgOpT *op, const char *name); static int compareDirs(const void *a, const void *b); static bool destPath(const PkgOpT *op, const char *name, char *out, int32_t outSize); static bool dropAdd(PkgOpT *op, const PkgFileT *file); -static int32_t fileRead(void *ctx, uint8_t *buf, int32_t len); static PkgOpStateE opFail(PkgOpT *op, const char *fmt, ...) __attribute__((format(printf, 2, 3))); static PkgOpT *opNew(PkgDbT *db, OpKindE kind); void pkgOpFree(PkgOpT *op); -PkgOpT *pkgOpInstall(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool explicitly); +PkgOpT *pkgOpInstall(PkgDbT *db, const PkgOfferT *offer, bool explicitly); +PkgOpT *pkgOpInstallFile(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool explicitly); char *const *pkgOpKept(const PkgOpT *op); const char *pkgOpMessage(const PkgOpT *op); void pkgOpProgress(const PkgOpT *op, uint32_t *done, uint32_t *total); PkgOpT *pkgOpRemove(PkgDbT *db, const char *id, bool removeChanged); PkgOpStateE pkgOpStep(PkgOpT *op); +void pkgOpStop(PkgOpT *op); static bool scratchPath(const PkgOpT *op, int32_t index, const char *fmt, char *out, int32_t outSize); static bool scratchWrite(void *ctx, const uint8_t *data, int32_t len); +static PkgOpStateE stepAside(PkgOpT *op); static PkgOpStateE stepDrop(PkgOpT *op); static PkgOpStateE stepRecord(PkgOpT *op); static PkgOpStateE stepSwap(PkgOpT *op); +static PkgOpStateE stepUndo(PkgOpT *op); static PkgOpStateE stepUnpack(PkgOpT *op); static void tidy(PkgOpT *op); -static const char *zipReason(const PkgZipT *zip); +static PkgOpStateE undoBegin(PkgOpT *op, const char *fmt, ...) __attribute__((format(printf, 2, 3))); // The folders of name from its own down to its vendor's folder (the first @@ -196,14 +206,6 @@ static bool dropAdd(PkgOpT *op, const PkgFileT *file) { } -static int32_t fileRead(void *ctx, uint8_t *buf, int32_t len) { - FILE *f = (FILE *)ctx; - size_t n = fread(buf, 1, (size_t)len, f); - - return (n == 0 && ferror(f)) ? -1 : (int32_t)n; -} - - // Stop: nothing of a package that was not swapped in is left (its scratch // files, and the folders they leave empty). static PkgOpStateE opFail(PkgOpT *op, const char *fmt, ...) { @@ -274,11 +276,7 @@ void pkgOpFree(PkgOpT *op) { return; } - // Stopped while unpacking: nothing changed. Once swapped in, the - // rest is done, so no package is left half installed. - if (op->stage == StageUnpackE) { - opFail(op, "Stopped."); - } + pkgOpStop(op); while (pkgOpStep(op) == PkgOpRunningE) { } @@ -300,14 +298,15 @@ void pkgOpFree(PkgOpT *op) { arrfree(op->dirs); pkgManifestFree(&op->manifest); free(op->scratch); + free(op->aside); + free(op->placed); free(op->buf); free(op); } -PkgOpT *pkgOpInstall(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool explicitly) { +PkgOpT *pkgOpInstall(PkgDbT *db, const PkgOfferT *offer, bool explicitly) { PkgOpT *op = opNew(db, OpInstallE); - PkgOfferT offer; const PkgInstalledT *old; int32_t count; uint64_t diskTotal = 0; @@ -317,26 +316,28 @@ PkgOpT *pkgOpInstall(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool return NULL; } - if (!pkgOfferRead(path, ring, &offer, op->msg, sizeof(op->msg))) { - op->stage = StageFailedE; + pkgManifestFree(&op->manifest); + + if (!pkgManifestCopy(&op->manifest, &offer->manifest)) { + opFail(op, PKG_ERR_NO_MEMORY); return op; } - pkgManifestFree(&op->manifest); - op->manifest = offer.manifest; op->explicitly = explicitly; - op->hasSigner = offer.hasSigner; - memcpy(op->signer, offer.signer, PKG_KEY_ID_SIZE); + op->hasSigner = offer->hasSigner; + memcpy(op->signer, offer->signer, PKG_KEY_ID_SIZE); snprintf(op->id, sizeof(op->id), "%s", op->manifest.id); count = (int32_t)arrlen(op->manifest.files); op->scratch = (bool *)calloc((size_t)count + 1, sizeof(bool)); + op->aside = (bool *)calloc((size_t)count + 1, sizeof(bool)); + op->placed = (bool *)calloc((size_t)count + 1, sizeof(bool)); - if (!op->scratch) { - opFail(op, "There is not enough memory."); + if (!op->scratch || !op->aside || !op->placed) { + opFail(op, PKG_ERR_NO_MEMORY); return op; } - if (pkgTrustIsDvxId(op->id) && offer.trust != PkgTrustSystemE) { + if (pkgTrustIsDvxId(op->id) && offer->trust != PkgTrustSystemE) { opFail(op, "%s says it is part of DVX, but it is not signed by DVX's key.", op->manifest.name); return op; } @@ -358,7 +359,7 @@ PkgOpT *pkgOpInstall(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool for (int32_t i = 0; i < (int32_t)arrlen(old->manifest.files); i++) { if (!pkgManifestFindFile(&op->manifest, old->manifest.files[i].name) && !dropAdd(op, &old->manifest.files[i])) { - opFail(op, "There is not enough memory."); + opFail(op, PKG_ERR_NO_MEMORY); return op; } } @@ -370,11 +371,11 @@ PkgOpT *pkgOpInstall(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool return op; } - op->zipFile = fopen(path, "rb"); - op->zip = op->zipFile ? pkgZipOpen(fileRead, op->zipFile) : NULL; + op->zipFile = fopen(offer->where, "rb"); + op->zip = op->zipFile ? pkgZipOpen(pkgZipFileRead, op->zipFile) : NULL; if (!op->zip) { - opFail(op, "Cannot read %s.", path); + opFail(op, "Cannot read %s.", offer->where); return op; } @@ -383,6 +384,27 @@ PkgOpT *pkgOpInstall(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool } +PkgOpT *pkgOpInstallFile(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool explicitly) { + PkgOfferT offer; + char err[PKG_ERR_MAX]; + PkgOpT *op; + + if (pkgOfferRead(path, ring, &offer, err, sizeof(err))) { + op = pkgOpInstall(db, &offer, explicitly); + pkgOfferFree(&offer); + return op; + } + + op = opNew(db, OpInstallE); + + if (op) { + opFail(op, "%s", err); + } + + return op; +} + + char *const *pkgOpKept(const PkgOpT *op) { return op->kept; } @@ -422,7 +444,7 @@ PkgOpT *pkgOpRemove(PkgDbT *db, const char *id, bool removeChanged) { for (int32_t i = 0; i < (int32_t)arrlen(rec->manifest.files); i++) { if (!dropAdd(op, &rec->manifest.files[i])) { - opFail(op, "There is not enough memory."); + opFail(op, PKG_ERR_NO_MEMORY); return op; } } @@ -440,6 +462,12 @@ PkgOpStateE pkgOpStep(PkgOpT *op) { case StageSwapE: return stepSwap(op); + case StageUndoE: + return stepUndo(op); + + case StageAsideE: + return stepAside(op); + case StageDropE: return stepDrop(op); @@ -460,6 +488,15 @@ PkgOpStateE pkgOpStep(PkgOpT *op) { } +// Stopped while unpacking: nothing changed. Once swapped in, the rest is +// done, so no package is left half installed. +void pkgOpStop(PkgOpT *op) { + if (op->stage == StageUnpackE) { + opFail(op, "Stopped."); + } +} + + // The scratch (or set-aside) name of the manifest's file index, in the // folder it goes to. static bool scratchPath(const PkgOpT *op, int32_t index, const char *fmt, char *out, int32_t outSize) { @@ -481,6 +518,28 @@ static bool scratchWrite(void *ctx, const uint8_t *data, int32_t len) { } +// Swapped in: the files it replaced, set aside until now, go, +// OP_SWAP_STEP at a time. +static PkgOpStateE stepAside(PkgOpT *op) { + int32_t count = (int32_t)arrlen(op->manifest.files); + int32_t end = op->swapAt + OP_SWAP_STEP; + + for (; op->swapAt < count && op->swapAt < end; op->swapAt++) { + char bak[DVX_MAX_PATH]; + + if (op->aside[op->swapAt] && scratchPath(op, op->swapAt, OP_ASIDE_FMT, bak, sizeof(bak))) { + remove(bak); + } + } + + if (op->swapAt == count) { + op->stage = StageDropE; + } + + return PkgOpRunningE; +} + + // Check one file to delete a step at a time: unchanged (or all are to go) // it is deleted; changed it is kept. A file another package has is left. static PkgOpStateE stepDrop(PkgOpT *op) { @@ -577,97 +636,93 @@ static PkgOpStateE stepRecord(PkgOpT *op) { } op->stage = StageDoneE; + platformInstalledChanged(); return PkgOpDoneE; } -// Every unpacked file in place at once: the files they replace go aside -// first, and come back if a swap fails. +// Swap the unpacked files in, OP_SWAP_STEP at a time: the file each +// replaces set aside, then it is put in its place. Should one fail, every +// file goes back as it was (stepUndo). static PkgOpStateE stepSwap(PkgOpT *op) { - int32_t count = (int32_t)arrlen(op->manifest.files); - bool *aside = (bool *)calloc((size_t)count + 1, sizeof(bool)); - bool *placed = (bool *)calloc((size_t)count + 1, sizeof(bool)); - char failed[PKG_ERR_MAX]; - bool ok = aside && placed; + int32_t count = (int32_t)arrlen(op->manifest.files); + int32_t end = op->swapAt + OP_SWAP_STEP; - snprintf(failed, sizeof(failed), "There is not enough memory."); - - for (int32_t i = 0; ok && i < count; i++) { + for (; op->swapAt < count && op->swapAt < end; op->swapAt++) { + int32_t i = op->swapAt; + const char *name = op->manifest.files[i].name; char dest[DVX_MAX_PATH]; char scratch[DVX_MAX_PATH]; char bak[DVX_MAX_PATH]; struct stat st; - if (!destPath(op, op->manifest.files[i].name, dest, sizeof(dest)) || !scratchPath(op, i, OP_SCRATCH_FMT, scratch, sizeof(scratch)) || !scratchPath(op, i, OP_ASIDE_FMT, bak, sizeof(bak))) { - snprintf(failed, sizeof(failed), "The path of %s is too long.", op->manifest.files[i].name); - ok = false; - break; + if (!destPath(op, name, dest, sizeof(dest)) || !scratchPath(op, i, OP_SCRATCH_FMT, scratch, sizeof(scratch)) || !scratchPath(op, i, OP_ASIDE_FMT, bak, sizeof(bak))) { + return undoBegin(op, "The path of %s is too long.", name); } if (stat(dest, &st) == 0) { if (!S_ISREG(st.st_mode)) { - snprintf(failed, sizeof(failed), "%s is a folder.", dest); - ok = false; - break; + return undoBegin(op, "%s is a folder.", dest); } remove(bak); if (rename(dest, bak) != 0) { - snprintf(failed, sizeof(failed), "Cannot move %s aside.", dest); - ok = false; - break; + return undoBegin(op, "Cannot move %s aside.", dest); } - aside[i] = true; + op->aside[i] = true; } if (rename(scratch, dest) != 0) { - snprintf(failed, sizeof(failed), "Cannot put %s in place.", dest); - ok = false; - break; + return undoBegin(op, "Cannot put %s in place.", dest); } - placed[i] = true; + op->placed[i] = true; op->scratch[i] = false; } - // Undo: the new files out, the old ones back - for (int32_t i = 0; !ok && aside && placed && i < count; i++) { - char dest[DVX_MAX_PATH]; - char bak[DVX_MAX_PATH]; + if (op->swapAt == count) { + op->swapAt = 0; + op->stage = StageAsideE; + } + + return PkgOpRunningE; +} + + +// The swap failed (op->msg says why): the new files out and the old ones +// back, OP_SWAP_STEP at a time; then the operation fails. +static PkgOpStateE stepUndo(PkgOpT *op) { + int32_t count = (int32_t)arrlen(op->manifest.files); + int32_t end = op->swapAt + OP_SWAP_STEP; + + for (; op->swapAt < count && op->swapAt < end; op->swapAt++) { + int32_t i = op->swapAt; + char dest[DVX_MAX_PATH]; + char bak[DVX_MAX_PATH]; if (!destPath(op, op->manifest.files[i].name, dest, sizeof(dest)) || !scratchPath(op, i, OP_ASIDE_FMT, bak, sizeof(bak))) { continue; } - if (placed[i]) { + if (op->placed[i]) { remove(dest); } - if (aside[i]) { + if (op->aside[i]) { rename(bak, dest); } } - // Done: the old files are not needed now - for (int32_t i = 0; ok && i < count; i++) { - char bak[DVX_MAX_PATH]; - - if (aside[i] && scratchPath(op, i, OP_ASIDE_FMT, bak, sizeof(bak))) { - remove(bak); - } + if (op->swapAt < count) { + return PkgOpRunningE; } - free(aside); - free(placed); + char why[PKG_ERR_MAX]; - if (!ok) { - return opFail(op, "%s", failed); - } - - op->stage = StageDropE; - return PkgOpRunningE; + snprintf(why, sizeof(why), "%s", op->msg); + return opFail(op, "%s", why); } @@ -691,7 +746,7 @@ static PkgOpStateE stepUnpack(PkgOpT *op) { op->out = NULL; if (st != PkgZipFileDoneE) { - return opFail(op, "%s: %s", file->name, zipReason(op->zip)); + return opFail(op, "%s: %s", file->name, pkgZipProblemText(op->zip)); } secSha256Final(&op->outSha, sha); @@ -727,7 +782,7 @@ static PkgOpStateE stepUnpack(PkgOpT *op) { } if (st != PkgZipEntryE) { - return opFail(op, "The package: %s", zipReason(op->zip)); + return opFail(op, "The package: %s", pkgZipProblemText(op->zip)); } if (e.isDir) { @@ -736,7 +791,7 @@ static PkgOpStateE stepUnpack(PkgOpT *op) { // The manifest and signature: read past if (strcasecmp(e.name, PKG_MANIFEST_NAME) == 0 || strcasecmp(e.name, PKG_SIG_NAME) == 0) { - return pkgZipData(op->zip, NULL, NULL, INT32_MAX) == PkgZipFileDoneE ? PkgOpRunningE : opFail(op, "The package: %s", zipReason(op->zip)); + return pkgZipData(op->zip, NULL, NULL, INT32_MAX) == PkgZipFileDoneE ? PkgOpRunningE : opFail(op, "The package: %s", pkgZipProblemText(op->zip)); } const PkgFileT *file = pkgManifestFindFile(&op->manifest, e.name); @@ -790,18 +845,15 @@ static void tidy(PkgOpT *op) { } -static const char *zipReason(const PkgZipT *zip) { - switch (pkgZipProblem(zip)) { - case PkgZipReadFailedE: - return "it cannot be read."; +// The swap failed, saying why: put everything back (stepUndo) from the +// first file. +static PkgOpStateE undoBegin(PkgOpT *op, const char *fmt, ...) { + va_list ap; - case PkgZipWriteFailedE: - return "it cannot be written (is the disk full?)."; - - case PkgZipUnreadableE: - return "it uses a form DVX cannot read."; - - default: - return "it is damaged."; - } + va_start(ap, fmt); + vsnprintf(op->msg, sizeof(op->msg), fmt, ap); + va_end(ap); + op->swapAt = 0; + op->stage = StageUndoE; + return PkgOpRunningE; } diff --git a/src/libs/kpunch/pkg/pkgOp.h b/src/libs/kpunch/pkg/pkgOp.h index aa1e8f7a..579580de 100644 --- a/src/libs/kpunch/pkg/pkgOp.h +++ b/src/libs/kpunch/pkg/pkgOp.h @@ -43,12 +43,13 @@ #define PKG_OP_H #include "pkgDb.h" +#include "pkgPlan.h" #include "pkgTrust.h" #include #include -#define PKG_OP_STEP 32768 // bytes unpacked or checked a step +#define PKG_OP_STEP 16384 // bytes unpacked or checked a step typedef enum { PkgOpRunningE, @@ -62,10 +63,17 @@ typedef struct PkgOpT PkgOpT; // after, the rest of it is done first. void pkgOpFree(PkgOpT *op); -// Install the package at path into db's DVX folder, trusted as ring says -// (a kpunch.* package must be signed by a SYSTEM key). explicitly: asked -// for (an upgrade keeps a package that was). NULL when out of memory. -PkgOpT *pkgOpInstall(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool explicitly); +// Install offer (pkgOfferRead: its manifest, and the trust checked then; +// a kpunch.* package must be signed by a SYSTEM key) into db's DVX +// folder; each file unpacked from offer->where is checked against that +// manifest. explicitly: asked for (an upgrade keeps a package that +// was). NULL when out of memory. +PkgOpT *pkgOpInstall(PkgDbT *db, const PkgOfferT *offer, bool explicitly); + +// The same from the package at path, read (pkgOfferRead) and trusted as +// ring says now (for an offer read before ring was). The op fails at +// once, saying why, when it cannot be read. NULL when out of memory. +PkgOpT *pkgOpInstallFile(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool explicitly); // The files a removal or upgrade left because they changed since they // were installed (relative names, an stb_ds array owned by the op). @@ -84,4 +92,8 @@ PkgOpT *pkgOpRemove(PkgDbT *db, const char *id, bool removeChanged); // One step: PkgOpRunningE until it is done or has failed. PkgOpStateE pkgOpStep(PkgOpT *op); +// Ask op to stop: before its files are swapped in it fails at once +// (nothing changed); after, its steps run on to the end. +void pkgOpStop(PkgOpT *op); + #endif // PKG_OP_H diff --git a/src/libs/kpunch/pkg/pkgPlan.c b/src/libs/kpunch/pkg/pkgPlan.c index 2d8aabdc..393c1c2c 100644 --- a/src/libs/kpunch/pkg/pkgPlan.c +++ b/src/libs/kpunch/pkg/pkgPlan.c @@ -24,6 +24,7 @@ // pkgPlan.c -- planning installs and removals (pkgPlan.h) #include "pkgPlan.h" +#include "pkgInt.h" #include "dvxTypes.h" #include "stb_ds_wrap.h" @@ -33,6 +34,8 @@ #include #include +#define PERCENT_MAX 100 + typedef enum { VisitNoneE, VisitActiveE, // its dependencies are being planned @@ -66,6 +69,10 @@ static bool needs(const PkgManifestT *m, const char *id); static bool offerHasFile(const PkgOfferT *offer, const char *name); void pkgOfferFree(PkgOfferT *offer); bool pkgOfferRead(const char *path, const PkgKeyringT *ring, PkgOfferT *offer, char *err, int32_t errSize); +void pkgOfferScanFree(PkgOfferScanT *scan); +int32_t pkgOfferScanPercent(const PkgOfferScanT *scan); +void pkgOfferScanStart(PkgOfferScanT *scan, const char *dir, const char *first, const PkgKeyringT *ring); +PkgScanStepE pkgOfferScanStep(PkgOfferScanT *scan, char *err, int32_t errSize); PkgOfferT *pkgOffersFor(const char *path, const PkgKeyringT *ring, char *err, int32_t errSize); void pkgOffersFree(PkgOfferT *offers); PkgOfferT *pkgOffersInFolder(const char *dir, const PkgKeyringT *ring); @@ -74,6 +81,7 @@ void pkgPlanFree(PkgPlanT *plan); bool pkgPlanInstall(const PkgDbT *db, const PkgOfferT *offers, int32_t offerCount, const int32_t *wanted, int32_t wantedCount, PkgPlanT *plan); const PkgInstalledT **pkgPlanOrphans(const PkgDbT *db, const char *const *ids, int32_t count); static void planStale(PlanT *p); +static PkgOfferT *scanAll(const char *dir, const char *first, const PkgKeyringT *ring, char *err, int32_t errSize); static bool visitOffer(PlanT *p, int32_t i, bool explicitly); @@ -307,35 +315,88 @@ bool pkgOfferRead(const char *path, const PkgKeyringT *ring, PkgOfferT *offer, c } -PkgOfferT *pkgOffersFor(const char *path, const PkgKeyringT *ring, char *err, int32_t errSize) { - PkgOfferT mine; - PkgOfferT *offers = NULL; - PkgOfferT *others; - char dir[DVX_MAX_PATH]; +void pkgOfferScanFree(PkgOfferScanT *scan) { + pkgOffersFree(scan->offers); + dvxReadDirFree(scan->names); + free(scan->dir); + free(scan->first); + free(scan->skip); + memset(scan, 0, sizeof(*scan)); +} - if (!pkgOfferRead(path, ring, &mine, err, errSize)) { - return NULL; + +// By the folder's names gone through (the first package counts as none). +int32_t pkgOfferScanPercent(const PkgOfferScanT *scan) { + int32_t count = (int32_t)arrlen(scan->names); + + if (count == 0) { + return scan->first ? 0 : PERCENT_MAX; } - arrput(offers, mine); + return scan->at * PERCENT_MAX / count; +} - if (!platformPathDirName(path, dir, sizeof(dir))) { - return offers; + +void pkgOfferScanStart(PkgOfferScanT *scan, const char *dir, const char *first, const PkgKeyringT *ring) { + memset(scan, 0, sizeof(*scan)); + scan->ring = ring; + + if (dir && dir[0]) { + scan->dir = strdup(dir); + scan->names = scan->dir ? dvxReadDir(dir) : NULL; } - // The folder's others (path itself is there too: once is enough) - others = pkgOffersInFolder(dir, ring); + if (first) { + scan->first = strdup(first); + scan->skip = strdup(platformPathBaseName(first)); + } +} - for (int32_t i = 0; i < (int32_t)arrlen(others); i++) { - if (strcasecmp(platformPathBaseName(others[i].where), platformPathBaseName(path)) == 0) { - pkgOfferFree(&others[i]); - } else { - arrput(offers, others[i]); + +PkgScanStepE pkgOfferScanStep(PkgOfferScanT *scan, char *err, int32_t errSize) { + PkgOfferT offer; + + if (scan->first) { + snprintf(scan->current, sizeof(scan->current), "%s", platformPathBaseName(scan->first)); + + if (!pkgOfferRead(scan->first, scan->ring, &offer, err, errSize)) { + return PkgScanFailedE; } + + arrput(scan->offers, offer); + free(scan->first); + scan->first = NULL; + return PkgScanMoreE; } - arrfree(others); - return offers; + while (scan->at < (int32_t)arrlen(scan->names)) { + const char *name = scan->names[scan->at++]; + char path[DVX_MAX_PATH]; + char why[PKG_ERR_MAX]; + + if (!dvxHasExt(name, PKG_EXT) || (scan->skip && strcasecmp(name, scan->skip) == 0) || !platformPathJoin(path, sizeof(path), scan->dir, name)) { + continue; + } + + snprintf(scan->current, sizeof(scan->current), "%s", name); + + if (pkgOfferRead(path, scan->ring, &offer, why, sizeof(why))) { + arrput(scan->offers, offer); + } else { + dvxLog("Packages: %s skipped: %s", path, why); + } + + return PkgScanMoreE; + } + + return PkgScanDoneE; +} + + +PkgOfferT *pkgOffersFor(const char *path, const PkgKeyringT *ring, char *err, int32_t errSize) { + char dir[DVX_MAX_PATH]; + + return scanAll(platformPathDirName(path, dir, sizeof(dir)) ? dir : NULL, path, ring, err, errSize); } @@ -349,27 +410,9 @@ void pkgOffersFree(PkgOfferT *offers) { PkgOfferT *pkgOffersInFolder(const char *dir, const PkgKeyringT *ring) { - PkgOfferT *offers = NULL; - char **names = dvxReadDir(dir); + char err[PKG_ERR_MAX]; - for (int32_t i = 0; i < (int32_t)arrlen(names); i++) { - char path[DVX_MAX_PATH]; - char err[PKG_ERR_MAX]; - PkgOfferT offer; - - if (!dvxHasExt(names[i], PKG_EXT) || !platformPathJoin(path, sizeof(path), dir, names[i])) { - continue; - } - - if (pkgOfferRead(path, ring, &offer, err, sizeof(err))) { - arrput(offers, offer); - } else { - dvxLog("Packages: %s skipped: %s", path, err); - } - } - - dvxReadDirFree(names); - return offers; + return scanAll(dir, NULL, ring, err, sizeof(err)); } @@ -438,7 +481,7 @@ bool pkgPlanInstall(const PkgDbT *db, const PkgOfferT *offers, int32_t offerCoun p.dvx.patch = DVX_VERSION_PATCH; if (!p.visit) { - return fail(&p, "There is not enough memory."); + return fail(&p, PKG_ERR_NO_MEMORY); } if (dvx) { @@ -509,8 +552,6 @@ const PkgInstalledT **pkgPlanOrphans(const PkgDbT *db, const char *const *ids, i } -// After a DVX update to another ABI: the installed code it does not -// replace that will not load. static void planStale(PlanT *p) { for (int32_t i = 0; p->abi != DVX_ABI_VERSION && i < (int32_t)arrlen(p->db->pkgs); i++) { const PkgInstalledT *pkg = &p->db->pkgs[i]; @@ -522,6 +563,30 @@ static void planStale(PlanT *p) { } +// After a DVX update to another ABI: the installed code it does not +// replace that will not load. +// A scan run to its end at once: its offers, or NULL with the reason in +// err when first cannot be read. +static PkgOfferT *scanAll(const char *dir, const char *first, const PkgKeyringT *ring, char *err, int32_t errSize) { + PkgOfferScanT scan; + PkgScanStepE step; + PkgOfferT *offers = NULL; + + pkgOfferScanStart(&scan, dir, first, ring); + + while ((step = pkgOfferScanStep(&scan, err, errSize)) == PkgScanMoreE) { + } + + if (step == PkgScanDoneE) { + offers = scan.offers; + scan.offers = NULL; + } + + pkgOfferScanFree(&scan); + return offers; +} + + // Plan offer i after everything it needs that is not installed. static bool visitOffer(PlanT *p, int32_t i, bool explicitly) { const PkgManifestT *m = &p->offers[i].manifest; diff --git a/src/libs/kpunch/pkg/pkgPlan.h b/src/libs/kpunch/pkg/pkgPlan.h index 6bfc1b4f..3a750f53 100644 --- a/src/libs/kpunch/pkg/pkgPlan.h +++ b/src/libs/kpunch/pkg/pkgPlan.h @@ -62,6 +62,26 @@ typedef enum { PkgActReinstallE // the same version again (a repair) } PkgActE; +// Reading packages as offers one at a time (pkgOfferScanStart), so a +// program can draw progress between them: checking a signature is slow +// on a 486 +typedef struct { + PkgOfferT *offers; // stb_ds array: those read so far (first's first) + char **names; // the folder's files (dvxReadDir) + const PkgKeyringT *ring; + char *dir; + char *first; // read first (NULL once it is, or for none) + char *skip; // its name: not read again from the folder + int32_t at; // the next of names + char current[DVX_MAX_PATH]; // the name of the file read last +} PkgOfferScanT; + +typedef enum { + PkgScanMoreE, + PkgScanDoneE, + PkgScanFailedE // the first package cannot be read +} PkgScanStepE; + typedef struct { PkgActE act; int32_t offer; // index in the offers @@ -83,15 +103,34 @@ void pkgOfferFree(PkgOfferT *offer); bool pkgOfferRead(const char *path, const PkgKeyringT *ring, PkgOfferT *offer, char *err, int32_t errSize); // The package at path as the first offer, then every other package in -// its folder (what it may need): an stb_ds array (pkgOffersFree), or NULL -// with the reason in err when path itself cannot be read. +// its folder (what it may need), all at once (pkgOfferScanStart): an +// stb_ds array (pkgOffersFree), or NULL with the reason in err when path +// itself cannot be read. PkgOfferT *pkgOffersFor(const char *path, const PkgKeyringT *ring, char *err, int32_t errSize); +// Free what scan holds: its offers too, unless taken (scan->offers set +// NULL). +void pkgOfferScanFree(PkgOfferScanT *scan); + +// How far scan is, 0 to 100. +int32_t pkgOfferScanPercent(const PkgOfferScanT *scan); + +// Start reading every package (*.DPK) in dir as offers (none when dir is +// NULL or ""), the one at first before them when first is not NULL (offer +// 0; not read again from dir), trusted as ring says (it must stay while +// the scan runs). Free it with pkgOfferScanFree. +void pkgOfferScanStart(PkgOfferScanT *scan, const char *dir, const char *first, const PkgKeyringT *ring); + +// Read the next package: PkgScanMoreE, PkgScanDoneE, or PkgScanFailedE +// (first cannot be read: the reason in err). A package of the folder +// that does not read is passed over (and logged). +PkgScanStepE pkgOfferScanStep(PkgOfferScanT *scan, char *err, int32_t errSize); + // Free an stb_ds array of offers and each offer in it. void pkgOffersFree(PkgOfferT *offers); -// Every package (*.DPK) in dir that reads, as offers: an stb_ds array -// (pkgOffersFree). +// Every package (*.DPK) in dir that reads, as offers, all at once: an +// stb_ds array (pkgOffersFree). PkgOfferT *pkgOffersInFolder(const char *dir, const PkgKeyringT *ring); // The installed packages that need any of ids (count of them), directly diff --git a/src/libs/kpunch/pkg/pkgSign.c b/src/libs/kpunch/pkg/pkgSign.c index 8f3ee20e..ccd151a4 100644 --- a/src/libs/kpunch/pkg/pkgSign.c +++ b/src/libs/kpunch/pkg/pkgSign.c @@ -28,10 +28,9 @@ #include "pkgInt.h" #include "dvxTypes.h" #include "dvxPlat.h" -#include "monocypher.h" -#include "optional/monocypher-ed25519.h" -#include "serial/security/secHash.h" -#include "serial/security/security.h" +#include "crypto/secCurve.h" +#include "crypto/secHash.h" +#include "crypto/security.h" #include #include @@ -48,7 +47,6 @@ #define KEY_SEED "seed" #define KEY_KEY "key" #define KEY_SIG "sig" -#define EXPANDED_SIZE 64 // Monocypher's secret key: the seed and the public key #define HEX_PER_BYTE 2 #define TEXT_SLACK 96 // the brackets, keys, ' = ', and line ends #define TEXT_MAX (PKG_KEY_NAME_MAX + PKG_SIG_SIZE * HEX_PER_BYTE + TEXT_SLACK) @@ -69,11 +67,10 @@ typedef struct { // ============================================================ static bool fail(char *err, int32_t errSize, const char *fmt, ...) __attribute__((format(printf, 3, 4))); -static void idOf(const uint8_t publicKey[PKG_PUBLIC_SIZE], uint8_t id[PKG_KEY_ID_SIZE]); static bool parseFields(const char *text, int32_t len, const char *section, FieldT *fields, int32_t count, char *err, int32_t errSize); char *pkgKeyFormat(const PkgKeyT *key, int32_t *outLen); +void pkgKeyIdOf(const uint8_t publicKey[PKG_PUBLIC_SIZE], uint8_t id[PKG_KEY_ID_SIZE]); void pkgKeyIdText(const uint8_t id[PKG_KEY_ID_SIZE], char *out); -void pkgKeyMake(const uint8_t seed[PKG_SEED_SIZE], const char *name, PkgKeyT *key, PkgSecretT *secret); bool pkgKeyParse(const char *text, int32_t len, PkgKeyT *key, char *err, int32_t errSize); bool pkgKeyRead(const char *path, PkgKeyT *key, char *err, int32_t errSize); bool pkgKeyWrite(const uint8_t seed[PKG_SEED_SIZE], const char *name, const char *base, PkgKeyT *key, char *err, int32_t errSize); @@ -84,7 +81,6 @@ bool pkgSecretRead(const char *path, PkgSecretT *secret, char *err, int3 void pkgSecretWipe(PkgSecretT *secret); bool pkgSigCheck(const PkgSigT *sig, const PkgKeyT *key, const uint8_t *msg, int32_t len); char *pkgSigFormat(const PkgSigT *sig, int32_t *outLen); -void pkgSign(const PkgSecretT *secret, const uint8_t *msg, int32_t len, PkgSigT *sig); bool pkgSigParse(const char *text, int32_t len, PkgSigT *sig, char *err, int32_t errSize); bool pkgSigRead(const char *path, PkgSigT *sig, char *err, int32_t errSize); static char *textOf(int32_t *outLen, const char *fmt, ...) __attribute__((format(printf, 2, 3))); @@ -100,14 +96,6 @@ static bool fail(char *err, int32_t errSize, const char *fmt, ...) { } -static void idOf(const uint8_t publicKey[PKG_PUBLIC_SIZE], uint8_t id[PKG_KEY_ID_SIZE]) { - uint8_t digest[SEC_SHA256_SIZE]; - - secSha256(publicKey, PKG_PUBLIC_SIZE, digest); - memcpy(id, digest, PKG_KEY_ID_SIZE); -} - - // The keys of one [section] into fields; every field must come, once. // Other keys are ignored (from a newer DVX). static bool parseFields(const char *text, int32_t len, const char *section, FieldT *fields, int32_t count, char *err, int32_t errSize) { @@ -165,24 +153,16 @@ char *pkgKeyFormat(const PkgKeyT *key, int32_t *outLen) { } -void pkgKeyIdText(const uint8_t id[PKG_KEY_ID_SIZE], char *out) { - pkgHexEncode(id, PKG_KEY_ID_SIZE, out); +void pkgKeyIdOf(const uint8_t publicKey[PKG_PUBLIC_SIZE], uint8_t id[PKG_KEY_ID_SIZE]) { + uint8_t digest[SEC_SHA256_SIZE]; + + secSha256(publicKey, PKG_PUBLIC_SIZE, digest); + memcpy(id, digest, PKG_KEY_ID_SIZE); } -void pkgKeyMake(const uint8_t seed[PKG_SEED_SIZE], const char *name, PkgKeyT *key, PkgSecretT *secret) { - uint8_t expanded[EXPANDED_SIZE]; - uint8_t seedCopy[PKG_SEED_SIZE]; - - // Monocypher wipes the seed it is given - memcpy(seedCopy, seed, PKG_SEED_SIZE); - crypto_ed25519_key_pair(expanded, key->publicKey, seedCopy); - crypto_wipe(expanded, sizeof(expanded)); - idOf(key->publicKey, key->id); - snprintf(key->name, sizeof(key->name), "%s", name); - memcpy(secret->name, key->name, sizeof(secret->name)); - memcpy(secret->id, key->id, PKG_KEY_ID_SIZE); - memcpy(secret->seed, seed, PKG_SEED_SIZE); +void pkgKeyIdText(const uint8_t id[PKG_KEY_ID_SIZE], char *out) { + pkgHexEncode(id, PKG_KEY_ID_SIZE, out); } @@ -200,7 +180,7 @@ bool pkgKeyParse(const char *text, int32_t len, PkgKeyT *key, char *err, int32_t return false; } - idOf(key->publicKey, id); + pkgKeyIdOf(key->publicKey, id); if (memcmp(id, key->id, PKG_KEY_ID_SIZE) != 0) { return fail(err, errSize, "The key's id is not its public key's."); @@ -239,7 +219,7 @@ bool pkgKeyWrite(const uint8_t seed[PKG_SEED_SIZE], const char *name, const char pkgSecretWipe(&secret); if (text) { - crypto_wipe(text, (size_t)len); + secSecureZero(text, len); free(text); } @@ -273,7 +253,7 @@ bool pkgRandomBytes(uint8_t *buf, int32_t len) { } secRngAddEntropy(pool, n); - crypto_wipe(pool, sizeof(pool)); + secSecureZero(pool, sizeof(pool)); secRngBytes(buf, len); return true; } @@ -287,7 +267,7 @@ char *pkgSecretFormat(const PkgSecretT *secret, int32_t *outLen) { pkgHexEncode(secret->id, PKG_KEY_ID_SIZE, id); pkgHexEncode(secret->seed, PKG_SEED_SIZE, seed); text = textOf(outLen, "[" SECTION_SECRET "]" EOL KEY_NAME " = %s" EOL KEY_ID " = %s" EOL KEY_SEED " = %s" EOL, secret->name, id, seed); - crypto_wipe(seed, sizeof(seed)); + secSecureZero(seed, sizeof(seed)); return text; } @@ -310,7 +290,7 @@ bool pkgSecretRead(const char *path, PkgSecretT *secret, char *err, int32_t errS bool ok = text ? pkgSecretParse(text, len, secret, err, errSize) : fail(err, errSize, "Cannot read %s.", path); if (text) { - crypto_wipe(text, (size_t)len); + secSecureZero(text, len); free(text); } @@ -319,12 +299,12 @@ bool pkgSecretRead(const char *path, PkgSecretT *secret, char *err, int32_t errS void pkgSecretWipe(PkgSecretT *secret) { - crypto_wipe(secret, sizeof(*secret)); + secSecureZero(secret, sizeof(*secret)); } bool pkgSigCheck(const PkgSigT *sig, const PkgKeyT *key, const uint8_t *msg, int32_t len) { - return memcmp(sig->keyId, key->id, PKG_KEY_ID_SIZE) == 0 && crypto_ed25519_check(sig->sig, key->publicKey, msg, (size_t)len) == 0; + return memcmp(sig->keyId, key->id, PKG_KEY_ID_SIZE) == 0 && secEd25519Check(sig->sig, key->publicKey, msg, len); } @@ -338,19 +318,6 @@ char *pkgSigFormat(const PkgSigT *sig, int32_t *outLen) { } -void pkgSign(const PkgSecretT *secret, const uint8_t *msg, int32_t len, PkgSigT *sig) { - uint8_t expanded[EXPANDED_SIZE]; - uint8_t publicKey[PKG_PUBLIC_SIZE]; - uint8_t seedCopy[PKG_SEED_SIZE]; - - memcpy(seedCopy, secret->seed, PKG_SEED_SIZE); - crypto_ed25519_key_pair(expanded, publicKey, seedCopy); - crypto_ed25519_sign(sig->sig, expanded, msg, (size_t)len); - crypto_wipe(expanded, sizeof(expanded)); - idOf(publicKey, sig->keyId); -} - - bool pkgSigParse(const char *text, int32_t len, PkgSigT *sig, char *err, int32_t errSize) { FieldT fields[] = { { KEY_KEY, NULL, 0, sig->keyId, PKG_KEY_ID_SIZE }, diff --git a/src/libs/kpunch/pkg/pkgSign.h b/src/libs/kpunch/pkg/pkgSign.h index d45f6066..eae3614e 100644 --- a/src/libs/kpunch/pkg/pkgSign.h +++ b/src/libs/kpunch/pkg/pkgSign.h @@ -22,7 +22,7 @@ // pkgSign.h -- the keys and signatures that make a package trusted // -// A signature is Ed25519 (Monocypher) over the bytes of a package's +// A signature is Ed25519 (the cryptography library, secCurve.h) over the bytes of a package's // manifest (PACKAGE.INI), which lists every file's size and SHA-256, so // it covers the whole package. It travels in the package (PACKAGE.SIG, // the publisher's) or beside it (NAME.SIG, or the catalogue's: the DVX @@ -48,6 +48,8 @@ #ifndef PKG_SIGN_H #define PKG_SIGN_H +#include "../crypto/secCurve.h" + #include #include @@ -56,9 +58,9 @@ #define PKG_SECRET_EXT ".SEC" #define PKG_SIG_EXT ".SIG" #define PKG_KEY_ID_SIZE 8 -#define PKG_PUBLIC_SIZE 32 -#define PKG_SEED_SIZE 32 -#define PKG_SIG_SIZE 64 +#define PKG_PUBLIC_SIZE SEC_ED25519_PUBLIC_SIZE +#define PKG_SEED_SIZE SEC_ED25519_SEED_SIZE +#define PKG_SIG_SIZE SEC_ED25519_SIG_SIZE #define PKG_KEY_NAME_MAX 64 #define PKG_KEY_ID_TEXT (PKG_KEY_ID_SIZE * 2 + 1) // as hex, with the NUL diff --git a/src/libs/kpunch/pkg/pkgSignMake.c b/src/libs/kpunch/pkg/pkgSignMake.c new file mode 100644 index 00000000..164d0223 --- /dev/null +++ b/src/libs/kpunch/pkg/pkgSignMake.c @@ -0,0 +1,72 @@ +// 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. + +// pkgSignMake.c -- making keys and signatures (see pkgSign.h): the +// Ed25519 key pair and signing, which only building and signing packages +// need. In DVX they are the package writer module's (pkgWriter.h), so +// the signing code is loaded only when a package is made; the tools that +// link the package library in whole (DVXPKG.EXE, the release tools) have +// them as they are. + +#include "pkgSign.h" +#include "pkgInt.h" +#include "crypto/secCurve.h" +#include "crypto/security.h" + +#include +#include + +// ============================================================ +// Prototypes +// ============================================================ + +void pkgKeyMake(const uint8_t seed[PKG_SEED_SIZE], const char *name, PkgKeyT *key, PkgSecretT *secret); +void pkgSign(const PkgSecretT *secret, const uint8_t *msg, int32_t len, PkgSigT *sig); + + +void pkgKeyMake(const uint8_t seed[PKG_SEED_SIZE], const char *name, PkgKeyT *key, PkgSecretT *secret) { + uint8_t expanded[SEC_ED25519_SECRET_SIZE]; + uint8_t seedCopy[PKG_SEED_SIZE]; + + // The key pair wipes the seed it is given + memcpy(seedCopy, seed, PKG_SEED_SIZE); + secEd25519KeyPair(expanded, key->publicKey, seedCopy); + secSecureZero(expanded, sizeof(expanded)); + pkgKeyIdOf(key->publicKey, key->id); + snprintf(key->name, sizeof(key->name), "%s", name); + memcpy(secret->name, key->name, sizeof(secret->name)); + memcpy(secret->id, key->id, PKG_KEY_ID_SIZE); + memcpy(secret->seed, seed, PKG_SEED_SIZE); +} + + +void pkgSign(const PkgSecretT *secret, const uint8_t *msg, int32_t len, PkgSigT *sig) { + uint8_t expanded[SEC_ED25519_SECRET_SIZE]; + uint8_t publicKey[PKG_PUBLIC_SIZE]; + uint8_t seedCopy[PKG_SEED_SIZE]; + + memcpy(seedCopy, secret->seed, PKG_SEED_SIZE); + secEd25519KeyPair(expanded, publicKey, seedCopy); + secEd25519Sign(sig->sig, expanded, msg, len); + secSecureZero(expanded, sizeof(expanded)); + pkgKeyIdOf(publicKey, sig->keyId); +} diff --git a/src/libs/kpunch/pkg/pkgTrust.c b/src/libs/kpunch/pkg/pkgTrust.c index 89d675e1..6040cd33 100644 --- a/src/libs/kpunch/pkg/pkgTrust.c +++ b/src/libs/kpunch/pkg/pkgTrust.c @@ -44,6 +44,7 @@ static void loadFolder(PkgKeyringT *ring, const char *dir); bool pkgKeyringAdd(PkgKeyringT *ring, const char *userDir, const char *path, char *err, int32_t errSize); void pkgKeyringFree(PkgKeyringT *ring); void pkgKeyringLoad(PkgKeyringT *ring, const char *root, const char *userDir); +bool pkgKeyringRemove(PkgKeyringT *ring, const char *userDir, const char *idText, char *err, int32_t errSize); bool pkgTrustIsDvxId(const char *id); PkgTrustE pkgTrustOf(const PkgKeyringT *ring, const PkgDpkInfoT *info, const PkgSigT *detached, const PkgKeyT **signer); @@ -129,6 +130,59 @@ void pkgKeyringLoad(PkgKeyringT *ring, const char *root, const char *userDir) { } +bool pkgKeyringRemove(PkgKeyringT *ring, const char *userDir, const char *idText, char *err, int32_t errSize) { + char dir[DVX_MAX_PATH]; + char text[PKG_KEY_ID_TEXT]; + int32_t found = -1; + + for (int32_t i = 0; i < (int32_t)arrlen(ring->keys) && found < 0; i++) { + pkgKeyIdText(ring->keys[i].id, text); + + if (idText && strcasecmp(text, idText) == 0) { + found = i; + } + } + + if (found < 0) { + return fail(err, errSize, "No key with the id %s is trusted.", idText ? idText : ""); + } + + if (found < ring->systemCount) { + return fail(err, errSize, "The key %s is DVX's own, and stays trusted.", ring->keys[found].name); + } + + if (!platformPathJoin(dir, sizeof(dir), userDir, PKG_KEYS_DIR)) { + return fail(err, errSize, "The path of the KEYS folder is too long."); + } + + // Every file of the key: one copied in under another name too + char **names = dvxReadDir(dir); + bool ok = true; + + for (int32_t i = 0; i < (int32_t)arrlen(names); i++) { + char path[DVX_MAX_PATH]; + char why[PKG_ERR_MAX]; + PkgKeyT key; + + if (!dvxHasExt(names[i], PKG_KEY_EXT) || !platformPathJoin(path, sizeof(path), dir, names[i]) || !pkgKeyRead(path, &key, why, sizeof(why))) { + continue; + } + + if (memcmp(key.id, ring->keys[found].id, PKG_KEY_ID_SIZE) == 0 && remove(path) != 0) { + ok = fail(err, errSize, "Cannot delete %s.", path); + } + } + + dvxReadDirFree(names); + + if (ok) { + arrdel(ring->keys, found); + } + + return ok; +} + + bool pkgTrustIsDvxId(const char *id) { char vendor[PKG_ID_PART_MAX + 1]; char name[PKG_ID_PART_MAX + 1]; diff --git a/src/libs/kpunch/pkg/pkgTrust.h b/src/libs/kpunch/pkg/pkgTrust.h index 17d70c14..9fcb4280 100644 --- a/src/libs/kpunch/pkg/pkgTrust.h +++ b/src/libs/kpunch/pkg/pkgTrust.h @@ -63,6 +63,11 @@ bool pkgKeyringAdd(PkgKeyringT *ring, const char *userDir, const char *path void pkgKeyringFree(PkgKeyringT *ring); +// Stop trusting the key with id idText (hex, pkgKeyIdText): its files in +// the user's KEYS folder (userDir) are deleted and it leaves ring. A +// SYSTEM key is DVX's own and stays. False with the reason. +bool pkgKeyringRemove(PkgKeyringT *ring, const char *userDir, const char *idText, char *err, int32_t errSize); + // Read the keys in root's SYSTEM\KEYS and userDir's KEYS (NULL: none). A // file that is not a key is skipped and logged. void pkgKeyringLoad(PkgKeyringT *ring, const char *root, const char *userDir); diff --git a/src/libs/kpunch/pkg/pkgWriter.h b/src/libs/kpunch/pkg/pkgWriter.h new file mode 100644 index 00000000..9403e317 --- /dev/null +++ b/src/libs/kpunch/pkg/pkgWriter.h @@ -0,0 +1,62 @@ +// 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. + +// pkgWriter.h -- the package writer module: writing package ZIPs (miniz's +// deflate) and making keys and signatures (Ed25519 signing), which only +// programs that make packages use (the Package Builder, the IDE's Make +// Package, BASIC's PkgBuildStart and the rest). In DVX they are a module +// of their own, writer.drv in PKG_WRITER_DIR (a driver module: dvxDrv.h), +// that exports one PkgWriterModT; pkg.lib's calls of these functions are +// through it (pkgWriterMod.c), and the first one loads it. The tools that +// link the package library in whole (DVXPKG.EXE, the release tools, the +// host tests) compile pkgZipWrite.c and pkgSignMake.c instead. + +#ifndef PKG_WRITER_H +#define PKG_WRITER_H + +#include "pkgSign.h" +#include "pkgZip.h" +#include "dvxDrv.h" +#include "dvxPlat.h" + +#define PKG_WRITER_DIR "libs" DVX_PATH_SEP "kpunch" DVX_PATH_SEP "pkg" DVX_PATH_SEP "writer" +#define PKG_WRITER_SYMBOL "pkgWriterModule" + +// What the module exports as PKG_WRITER_SYMBOL: the package library's +// functions of the same names +typedef struct { + DvxDriverHeaderT hdr; + __typeof__(&pkgKeyMake) keyMake; + __typeof__(&pkgSign) sign; + __typeof__(&pkgZipAbort) zipAbort; + __typeof__(&pkgZipAdded) zipAdded; + __typeof__(&pkgZipAddMem) zipAddMem; + __typeof__(&pkgZipBeginCopy) zipBeginCopy; + __typeof__(&pkgZipBeginFile) zipBeginFile; + __typeof__(&pkgZipBeginMem) zipBeginMem; + __typeof__(&pkgZipCopyWrite) zipCopyWrite; + __typeof__(&pkgZipCreate) zipCreate; + __typeof__(&pkgZipFinish) zipFinish; + __typeof__(&pkgZipStep) zipStep; +} PkgWriterModT; + +#endif // PKG_WRITER_H diff --git a/src/libs/kpunch/pkg/pkgWriterDrv.c b/src/libs/kpunch/pkg/pkgWriterDrv.c new file mode 100644 index 00000000..e0de4c71 --- /dev/null +++ b/src/libs/kpunch/pkg/pkgWriterDrv.c @@ -0,0 +1,44 @@ +// 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. + +// pkgWriterDrv.c -- the package writer module's descriptor (see +// pkgWriter.h): writer.drv is this, pkgZipWrite.c, pkgSignMake.c, and what +// they reach of miniz and Monocypher; everything else it calls is +// pkg.lib's. + +#include "pkgWriter.h" + +const PkgWriterModT pkgWriterModule = { + DVX_DRIVER_HDR(PkgWriterModT, "writer", 0), + .keyMake = pkgKeyMake, + .sign = pkgSign, + .zipAbort = pkgZipAbort, + .zipAdded = pkgZipAdded, + .zipAddMem = pkgZipAddMem, + .zipBeginCopy = pkgZipBeginCopy, + .zipBeginFile = pkgZipBeginFile, + .zipBeginMem = pkgZipBeginMem, + .zipCopyWrite = pkgZipCopyWrite, + .zipCreate = pkgZipCreate, + .zipFinish = pkgZipFinish, + .zipStep = pkgZipStep +}; diff --git a/src/libs/kpunch/pkg/pkgWriterMod.c b/src/libs/kpunch/pkg/pkgWriterMod.c new file mode 100644 index 00000000..bd8a6b04 --- /dev/null +++ b/src/libs/kpunch/pkg/pkgWriterMod.c @@ -0,0 +1,180 @@ +// 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. + +// pkgWriterMod.c -- the package library's calls of the package writer +// module (see pkgWriter.h): writing package ZIPs and making keys and +// signatures. The first pkgZipCreate, pkgKeyMake, or pkgSign loads the +// module, and it stays until the library is unloaded. A ZIP writer exists +// only while the module is loaded (pkgZipCreate made it), so the calls on +// one need no check. Without the module (a damaged installation) nothing +// is written: pkgZipCreate gives NULL, and a key or signature made is all +// zeros, which nothing accepts. + +#include "pkgWriter.h" +#include "stb_ds_wrap.h" + +#include + +// ============================================================ +// Prototypes +// ============================================================ + +static bool driverValid(const DvxDriverHeaderT *desc, int32_t kind); +void pkgKeyMake(const uint8_t seed[PKG_SEED_SIZE], const char *name, PkgKeyT *key, PkgSecretT *secret); +void pkgSign(const PkgSecretT *secret, const uint8_t *msg, int32_t len, PkgSigT *sig); +static const PkgWriterModT *pkgWriter(void); +static void pkgWriterUnload(void) __attribute__((destructor)); +void pkgZipAbort(PkgZipWriterT *w); +uint32_t pkgZipAdded(const PkgZipWriterT *w); +bool pkgZipAddMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when); +bool pkgZipBeginCopy(PkgZipWriterT *w, const PkgZipEntryT *e); +bool pkgZipBeginFile(PkgZipWriterT *w, const char *name, const char *path); +bool pkgZipBeginMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when); +bool pkgZipCopyWrite(void *ctx, const uint8_t *data, int32_t len); +PkgZipWriterT *pkgZipCreate(const char *path); +bool pkgZipFinish(PkgZipWriterT *w); +PkgZipStepE pkgZipStep(PkgZipWriterT *w, int32_t maxWork); + +static const char *const sDrvSymbols[] = { DVX_SYM(PKG_WRITER_SYMBOL) }; +static const uint32_t sDrvSizes[] = { sizeof(PkgWriterModT) }; +static const DvxDriverClassT sDrvClass = { sDrvSymbols, sDrvSizes, 1, driverValid }; + +static DvxDriverModT sMod = { NULL, NULL, 0 }; // desc NULL: not loaded +static const PkgWriterModT *sWriter = NULL; // sMod's descriptor + + +static bool driverValid(const DvxDriverHeaderT *desc, int32_t kind) { + (void)kind; + + const PkgWriterModT *m = (const PkgWriterModT *)desc; + + return m->keyMake && m->sign && m->zipAbort && m->zipAdded && m->zipAddMem && m->zipBeginCopy && m->zipBeginFile && m->zipBeginMem && m->zipCopyWrite && m->zipCreate && m->zipFinish && m->zipStep; +} + + +void pkgKeyMake(const uint8_t seed[PKG_SEED_SIZE], const char *name, PkgKeyT *key, PkgSecretT *secret) { + const PkgWriterModT *writer = pkgWriter(); + + if (writer) { + writer->keyMake(seed, name, key, secret); + return; + } + + memset(key, 0, sizeof(*key)); + memset(secret, 0, sizeof(*secret)); +} + + +void pkgSign(const PkgSecretT *secret, const uint8_t *msg, int32_t len, PkgSigT *sig) { + const PkgWriterModT *writer = pkgWriter(); + + if (writer) { + writer->sign(secret, msg, len, sig); + return; + } + + memset(sig, 0, sizeof(*sig)); +} + + +// The module, loaded if it is not (NULL: it is not there, logged). +static const PkgWriterModT *pkgWriter(void) { + if (sWriter) { + return sWriter; + } + + DvxDriverModT *mods = dvxDriversLoad(PKG_WRITER_DIR, &sDrvClass); + + if (arrlen(mods) > 0) { + sMod = dvxDriverKeep(mods, 0); + } + + dvxDriversFree(mods); + sWriter = (const PkgWriterModT *)sMod.desc; + + if (!sWriter) { + dvxLog("Packages: the package writer is missing from %s", PKG_WRITER_DIR); + } + + return sWriter; +} + + +// The library is being unloaded. +static void pkgWriterUnload(void) { + dvxDriverUnload(&sMod); + sWriter = NULL; +} + + +void pkgZipAbort(PkgZipWriterT *w) { + if (w) { + sWriter->zipAbort(w); + } +} + + +uint32_t pkgZipAdded(const PkgZipWriterT *w) { + return sWriter->zipAdded(w); +} + + +bool pkgZipAddMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when) { + return sWriter->zipAddMem(w, name, data, len, when); +} + + +bool pkgZipBeginCopy(PkgZipWriterT *w, const PkgZipEntryT *e) { + return sWriter->zipBeginCopy(w, e); +} + + +bool pkgZipBeginFile(PkgZipWriterT *w, const char *name, const char *path) { + return sWriter->zipBeginFile(w, name, path); +} + + +bool pkgZipBeginMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when) { + return sWriter->zipBeginMem(w, name, data, len, when); +} + + +bool pkgZipCopyWrite(void *ctx, const uint8_t *data, int32_t len) { + return sWriter->zipCopyWrite(ctx, data, len); +} + + +PkgZipWriterT *pkgZipCreate(const char *path) { + const PkgWriterModT *writer = pkgWriter(); + + return writer ? writer->zipCreate(path) : NULL; +} + + +bool pkgZipFinish(PkgZipWriterT *w) { + return sWriter->zipFinish(w); +} + + +PkgZipStepE pkgZipStep(PkgZipWriterT *w, int32_t maxWork) { + return sWriter->zipStep(w, maxWork); +} diff --git a/src/libs/kpunch/pkg/pkgZip.c b/src/libs/kpunch/pkg/pkgZip.c index d8f0457b..08f57a09 100644 --- a/src/libs/kpunch/pkg/pkgZip.c +++ b/src/libs/kpunch/pkg/pkgZip.c @@ -27,6 +27,7 @@ #include "pkgZip.h" #include "miniz.h" +#include #include #include @@ -69,27 +70,122 @@ struct PkgZipT { tinfl_decompressor inflator; uint8_t dict[TINFL_LZ_DICT_SIZE]; uint32_t dictPos; + uint32_t outPos; // in dict: what the last inflate made, not yet handed out + uint32_t outLeft; + bool inflated; // the inflate is done (what it made may still be handed out) }; // ============================================================ // Prototypes // ============================================================ +static PkgZipStepE dataStep(PkgZipT *z, PkgZipWriteFnT write, void *ctx, int32_t maxWork, bool packed); static bool fill(PkgZipT *z); static bool finishEntry(PkgZipT *z); void pkgZipClose(PkgZipT *z); PkgZipStepE pkgZipData(PkgZipT *z, PkgZipWriteFnT write, void *ctx, int32_t maxWork); +PkgZipStepE pkgZipDataPacked(PkgZipT *z, PkgZipWriteFnT write, void *ctx, int32_t maxWork); uint32_t pkgZipDoneBytes(const PkgZipT *z); +void pkgZipDosStamp(time_t when, uint16_t *date, uint16_t *time); time_t pkgZipDosTime(uint16_t date, uint16_t time); +int32_t pkgZipFileRead(void *ctx, uint8_t *buf, int32_t len); PkgZipStepE pkgZipNext(PkgZipT *z, PkgZipEntryT *entry); uint32_t pkgZipOffset(const PkgZipT *z); PkgZipT *pkgZipOpen(PkgZipReadFnT read, void *ctx); PkgZipProblemE pkgZipProblem(const PkgZipT *z); +const char *pkgZipProblemText(const PkgZipT *z); static bool readBytes(PkgZipT *z, uint8_t *buf, uint32_t len); static bool takeOut(PkgZipT *z, PkgZipWriteFnT write, void *ctx, const uint8_t *data, uint32_t len); static PkgZipStepE zipFail(PkgZipT *z, PkgZipProblemE problem); +// Up to maxWork bytes of the entry's data read and written (at least one +// piece): unpacked, or as they are packed when packed (still unpacked, to +// check them). An inflate makes up to a dictionary's worth at once, so +// what it made is handed out a piece at a time, and it is given no more +// input than the work left. +static PkgZipStepE dataStep(PkgZipT *z, PkgZipWriteFnT write, void *ctx, int32_t maxWork, bool packed) { + if (z->problem != PkgZipNoProblemE) { + return PkgZipFailE; + } + + if (z->state != ZipDataE) { + return PkgZipFileDoneE; + } + + for (int32_t work = 0; work == 0 || work < maxWork; ) { + uint32_t room = work < maxWork ? (uint32_t)(maxWork - work) : 1; + bool done; + + if (z->outLeft > 0) { + uint32_t n = z->outLeft < room ? z->outLeft : room; + + if (!takeOut(z, packed ? NULL : write, ctx, z->dict + z->outPos, n)) { + return PkgZipFailE; + } + + z->outPos += n; + z->outLeft -= n; + work += (int32_t)n; + done = z->outLeft == 0 && z->inflated; + } else { + if (z->packedLeft > 0 && !fill(z)) { + return PkgZipFailE; + } + + uint32_t avail = z->inLen - z->inPos; + + if (avail > z->packedLeft) { + avail = z->packedLeft; + } + + if (avail > room) { + avail = room; + } + + if (z->entry.method == PKG_ZIP_STORED) { + if (!takeOut(z, write, ctx, z->in + z->inPos, avail)) { + return PkgZipFailE; + } + + z->inPos += avail; + z->packedLeft -= avail; + work += (int32_t)avail; + done = z->packedLeft == 0; + } else { + size_t inBytes = avail; + size_t outBytes = TINFL_LZ_DICT_SIZE - z->dictPos; + mz_uint32 flags = z->packedLeft > avail ? TINFL_FLAG_HAS_MORE_INPUT : 0; + tinfl_status status = tinfl_decompress(&z->inflator, z->in + z->inPos, &inBytes, z->dict, z->dict + z->dictPos, &outBytes, flags); + + if (packed && inBytes > 0 && write && !write(ctx, z->in + z->inPos, (int32_t)inBytes)) { + return zipFail(z, PkgZipWriteFailedE); + } + + if (status < TINFL_STATUS_DONE || (status == TINFL_STATUS_NEEDS_MORE_INPUT && z->packedLeft == inBytes)) { + return zipFail(z, PkgZipDamagedE); + } + + z->inPos += (uint32_t)inBytes; + z->packedLeft -= (uint32_t)inBytes; + z->outPos = z->dictPos; + z->outLeft = (uint32_t)outBytes; + z->dictPos = (z->dictPos + (uint32_t)outBytes) & (TINFL_LZ_DICT_SIZE - 1); + z->inflated = status == TINFL_STATUS_DONE; + work += (int32_t)inBytes + 1; // a call that only makes output still counts + done = z->outLeft == 0 && z->inflated; + } + } + + if (done) { + return finishEntry(z) ? PkgZipFileDoneE : PkgZipFailE; + } + } + + return PkgZipMoreE; +} + + // Make sure the input buffer has a byte. False (problem set) when the // read callback stops or the bytes end. static bool fill(PkgZipT *z) { @@ -129,64 +225,12 @@ void pkgZipClose(PkgZipT *z) { PkgZipStepE pkgZipData(PkgZipT *z, PkgZipWriteFnT write, void *ctx, int32_t maxWork) { - if (z->problem != PkgZipNoProblemE) { - return PkgZipFailE; - } + return dataStep(z, write, ctx, maxWork, false); +} - if (z->state != ZipDataE) { - return PkgZipFileDoneE; - } - for (int32_t work = 0; work < maxWork; ) { - if (z->packedLeft > 0 && !fill(z)) { - return PkgZipFailE; - } - - uint32_t avail = z->inLen - z->inPos; - bool done; - - if (avail > z->packedLeft) { - avail = z->packedLeft; - } - - if (z->entry.method == PKG_ZIP_STORED) { - if (!takeOut(z, write, ctx, z->in + z->inPos, avail)) { - return PkgZipFailE; - } - - z->inPos += avail; - z->packedLeft -= avail; - work += (int32_t)avail; - done = z->packedLeft == 0; - } else { - size_t inBytes = avail; - size_t outBytes = TINFL_LZ_DICT_SIZE - z->dictPos; - mz_uint32 flags = z->packedLeft > avail ? TINFL_FLAG_HAS_MORE_INPUT : 0; - tinfl_status status = tinfl_decompress(&z->inflator, z->in + z->inPos, &inBytes, z->dict, z->dict + z->dictPos, &outBytes, flags); - - z->inPos += (uint32_t)inBytes; - z->packedLeft -= (uint32_t)inBytes; - work += (int32_t)(inBytes + outBytes); - - if (outBytes > 0 && !takeOut(z, write, ctx, z->dict + z->dictPos, (uint32_t)outBytes)) { - return PkgZipFailE; - } - - z->dictPos = (z->dictPos + (uint32_t)outBytes) & (TINFL_LZ_DICT_SIZE - 1); - - if (status < TINFL_STATUS_DONE || (status == TINFL_STATUS_NEEDS_MORE_INPUT && z->packedLeft == 0)) { - return zipFail(z, PkgZipDamagedE); - } - - done = status == TINFL_STATUS_DONE; - } - - if (done) { - return finishEntry(z) ? PkgZipFileDoneE : PkgZipFailE; - } - } - - return PkgZipMoreE; +PkgZipStepE pkgZipDataPacked(PkgZipT *z, PkgZipWriteFnT write, void *ctx, int32_t maxWork) { + return dataStep(z, write, ctx, maxWork, true); } @@ -195,6 +239,26 @@ uint32_t pkgZipDoneBytes(const PkgZipT *z) { } +// A time as the ZIP's DOS date and time (1980 .. 2107, two-second steps). +void pkgZipDosStamp(time_t when, uint16_t *date, uint16_t *time) { + struct tm *t = localtime(&when); + int32_t year = t ? t->tm_year + TM_YEAR_BASE - DOS_YEAR_BASE : 0; + + if (!t || year < 0) { + *date = (uint16_t)((1 << DOS_MONTH_SHIFT) | 1); + *time = 0; + return; + } + + if (year > DOS_YEAR_MASK) { + year = DOS_YEAR_MASK; + } + + *date = (uint16_t)((year << DOS_YEAR_SHIFT) | ((t->tm_mon + 1) << DOS_MONTH_SHIFT) | t->tm_mday); + *time = (uint16_t)((t->tm_hour << DOS_HOUR_SHIFT) | (t->tm_min << DOS_MINUTE_SHIFT) | (t->tm_sec / DOS_SECOND_UNIT)); +} + + time_t pkgZipDosTime(uint16_t date, uint16_t time) { struct tm t; @@ -210,6 +274,14 @@ time_t pkgZipDosTime(uint16_t date, uint16_t time) { } +int32_t pkgZipFileRead(void *ctx, uint8_t *buf, int32_t len) { + FILE *f = (FILE *)ctx; + size_t n = fread(buf, 1, (size_t)len, f); + + return (n == 0 && ferror(f)) ? -1 : (int32_t)n; +} + + PkgZipStepE pkgZipNext(PkgZipT *z, PkgZipEntryT *entry) { uint8_t hdr[PKG_ZIP_LOCAL_SIZE]; @@ -230,7 +302,7 @@ PkgZipStepE pkgZipNext(PkgZipT *z, PkgZipEntryT *entry) { return PkgZipFailE; } - uint32_t sig = pkgGet32(hdr); + uint32_t sig = dvxGetLe32(hdr); if (sig == PKG_ZIP_CENTRAL_SIG || sig == PKG_ZIP_EOCD_SIG) { z->state = ZipEndE; @@ -246,17 +318,17 @@ PkgZipStepE pkgZipNext(PkgZipT *z, PkgZipEntryT *entry) { } PkgZipEntryT e; - uint16_t flags = pkgGet16(hdr + PKG_ZIP_LH_FLAGS); - uint16_t nameLen = pkgGet16(hdr + PKG_ZIP_LH_NAME_LEN); - uint16_t extraLen = pkgGet16(hdr + PKG_ZIP_LH_EXTRA_LEN); + uint16_t flags = dvxGetLe16(hdr + PKG_ZIP_LH_FLAGS); + uint16_t nameLen = dvxGetLe16(hdr + PKG_ZIP_LH_NAME_LEN); + uint16_t extraLen = dvxGetLe16(hdr + PKG_ZIP_LH_EXTRA_LEN); memset(&e, 0, sizeof(e)); - e.method = pkgGet16(hdr + PKG_ZIP_LH_METHOD); - e.dosTime = pkgGet16(hdr + PKG_ZIP_LH_TIME); - e.dosDate = pkgGet16(hdr + PKG_ZIP_LH_DATE); - e.crc = pkgGet32(hdr + PKG_ZIP_LH_CRC); - e.packedSize = pkgGet32(hdr + PKG_ZIP_LH_PACKED); - e.size = pkgGet32(hdr + PKG_ZIP_LH_SIZE); + e.method = dvxGetLe16(hdr + PKG_ZIP_LH_METHOD); + e.dosTime = dvxGetLe16(hdr + PKG_ZIP_LH_TIME); + e.dosDate = dvxGetLe16(hdr + PKG_ZIP_LH_DATE); + e.crc = dvxGetLe32(hdr + PKG_ZIP_LH_CRC); + e.packedSize = dvxGetLe32(hdr + PKG_ZIP_LH_PACKED); + e.size = dvxGetLe32(hdr + PKG_ZIP_LH_SIZE); if ((flags & (PKG_ZIP_FLAG_DESC | PKG_ZIP_FLAG_CRYPT)) || (e.method != PKG_ZIP_STORED && e.method != PKG_ZIP_DEFLATED) || nameLen == 0 || nameLen >= PKG_NAME_MAX) { return zipFail(z, PkgZipUnreadableE); @@ -294,6 +366,9 @@ PkgZipStepE pkgZipNext(PkgZipT *z, PkgZipEntryT *entry) { z->sizeDone = 0; z->crc = (uint32_t)MZ_CRC32_INIT; z->dictPos = 0; + z->outPos = 0; + z->outLeft = 0; + z->inflated = false; z->state = e.isDir ? ZipHeaderE : ZipDataE; tinfl_init(&z->inflator); *entry = e; @@ -326,6 +401,23 @@ PkgZipProblemE pkgZipProblem(const PkgZipT *z) { } +const char *pkgZipProblemText(const PkgZipT *z) { + switch (pkgZipProblem(z)) { + case PkgZipReadFailedE: + return "it cannot be read."; + + case PkgZipWriteFailedE: + return "a part of it could not be kept (the disk is full, or memory is short)."; + + case PkgZipUnreadableE: + return "it uses a form DVX cannot read."; + + default: + return "it is damaged."; + } +} + + // Exactly len bytes of the stream. static bool readBytes(PkgZipT *z, uint8_t *buf, uint32_t len) { while (len > 0) { diff --git a/src/libs/kpunch/pkg/pkgZip.h b/src/libs/kpunch/pkg/pkgZip.h index 3c0aa01d..ca229dde 100644 --- a/src/libs/kpunch/pkg/pkgZip.h +++ b/src/libs/kpunch/pkg/pkgZip.h @@ -34,7 +34,9 @@ // Writing makes a ZIP the reader takes: each local header carries its // sizes, each file is deflated (or stored when deflating does not make it // smaller), names are relative with '/', and the central directory comes -// last. Files are added in the order given (a .DPK's manifest first). +// last. Files are added in the order given (a .DPK's manifest first), a +// step of a given amount of work at a time, as they are read; an entry of +// another ZIP can be copied in as it is packed. #ifndef PKG_ZIP_H #define PKG_ZIP_H @@ -56,8 +58,8 @@ typedef bool (*PkgZipWriteFnT)(void *ctx, const uint8_t *data, int32_t len); typedef enum { PkgZipEntryE, // pkgZipNext: an entry's header was read PkgZipEndE, // pkgZipNext: the central directory: no more entries - PkgZipMoreE, // pkgZipData: more of the entry to come - PkgZipFileDoneE, // pkgZipData: the entry is complete and checked + PkgZipMoreE, // pkgZipData, pkgZipStep: more of the entry to come + PkgZipFileDoneE, // pkgZipData: the entry is complete and checked; pkgZipStep: it is in PkgZipFailE // either: see pkgZipProblem } PkgZipStepE; @@ -95,12 +97,22 @@ void pkgZipClose(PkgZipT *z); // done at once. PkgZipStepE pkgZipData(PkgZipT *z, PkgZipWriteFnT write, void *ctx, int32_t maxWork); +// As pkgZipData, but write takes the entry's bytes as they are packed +// (to copy the entry into another ZIP: pkgZipBeginCopy); they are still +// unpacked to check the entry's size and CRC-32. +PkgZipStepE pkgZipDataPacked(PkgZipT *z, PkgZipWriteFnT write, void *ctx, int32_t maxWork); + // Bytes of the current entry's data written so far (for progress). uint32_t pkgZipDoneBytes(const PkgZipT *z); -// The time a ZIP date and time stand for (local time). +// The time a ZIP date and time stand for (local time), and a time as a +// ZIP date and time (1980 to 2107, two-second steps). +void pkgZipDosStamp(time_t when, uint16_t *date, uint16_t *time); time_t pkgZipDosTime(uint16_t date, uint16_t time); +// A PkgZipReadFnT over a stdio file (ctx the FILE *). +int32_t pkgZipFileRead(void *ctx, uint8_t *buf, int32_t len); + // The next entry's header (after the previous entry's data is done): // PkgZipEntryE with *entry filled, PkgZipEndE at the central directory, // or PkgZipFailE. @@ -115,6 +127,10 @@ PkgZipT *pkgZipOpen(PkgZipReadFnT read, void *ctx); // Why the last step failed. PkgZipProblemE pkgZipProblem(const PkgZipT *z); +// That reason in plain words, as the end of a sentence ("it is +// damaged."): what the package library's messages say after a name. +const char *pkgZipProblemText(const PkgZipT *z); + // ------------------------------------------------------------ // Writing // ------------------------------------------------------------ @@ -122,19 +138,91 @@ PkgZipProblemE pkgZipProblem(const PkgZipT *z); // Remove a half-made ZIP (the writer is freed). void pkgZipAbort(PkgZipWriterT *w); -// Add a file from disk under name. False on a read or write error, or -// when the file is too big for the format (4 GB). -bool pkgZipAddFile(PkgZipWriterT *w, const char *name, const char *path); - -// Add len bytes under name, dated when. +// Add len bytes under name, dated when, all at once (pkgZipBeginMem and +// pkgZipStep until it is in). bool pkgZipAddMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when); +// Bytes of the source of the entry being added taken so far (for +// progress). +uint32_t pkgZipAdded(const PkgZipWriterT *w); + +// Start copying in e, the entry of another ZIP just read by pkgZipNext, +// as it is packed: its packed bytes go through pkgZipCopyWrite (with the +// writer as ctx), as pkgZipDataPacked hands them; the entry is in after +// the last of them. False when it is a folder, its name will not do, or +// the ZIP would be too big for the format (4 GB). +bool pkgZipBeginCopy(PkgZipWriterT *w, const PkgZipEntryT *e); + +// Start adding the file at path under name, dated as it is; pkgZipStep +// adds it. False when it cannot be read, the name will not do, or the +// ZIP would be too big for the format (4 GB). +bool pkgZipBeginFile(PkgZipWriterT *w, const char *name, const char *path); + +// Start adding len bytes under name, dated when; pkgZipStep adds them +// (data must stay until it is done). False as pkgZipBeginFile. +bool pkgZipBeginMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when); + +// pkgZipBeginCopy's PkgZipWriteFnT: len more packed bytes of the entry +// (ctx the writer). False on a write error or more than the entry has. +bool pkgZipCopyWrite(void *ctx, const uint8_t *data, int32_t len); + // Start a ZIP at path (replaced if there is one). NULL when it cannot be // made or out of memory. PkgZipWriterT *pkgZipCreate(const char *path); // Write the central directory and close the file (the writer is freed). -// False on a write error (the file is removed). +// False on a write error or an entry not all in (the file is removed). bool pkgZipFinish(PkgZipWriterT *w); +// Add up to maxWork bytes of the entry begun (at least one piece): +// PkgZipMoreE, PkgZipFileDoneE once it is in, or PkgZipFailE on a read or +// write error (then pkgZipAbort). +PkgZipStepE pkgZipStep(PkgZipWriterT *w, int32_t maxWork); + +// ------------------------------------------------------------ +// Plain ZIP archives a step at a time (pkgZipJob.c) +// ------------------------------------------------------------ + +// A ZIP listed (every entry read and checked), unpacked into a folder, or +// made from files, in steps of a given amount of work, so a program can +// draw progress and stay responsive between them (pkg.bas's PkgZip*). +typedef struct PkgZipJobT PkgZipJobT; + +typedef enum { + PkgZipJobMoreE, // more to do + PkgZipJobDoneE, + PkgZipJobFailedE // pkgZipJobMessage says why (a ZIP being made is removed) +} PkgZipJobStepE; + +// Make a ZIP at path from count files: names[i] in the ZIP (relative, '/' +// between parts) from the file sources[i] (both copied). NULL with the +// reason in err. +PkgZipJobT *pkgZipJobCreate(const char *path, const char *const *names, const char *const *sources, int32_t count, char *err, int32_t errSize); + +// The name of the entry the job is on ("" when none). +const char *pkgZipJobCurrent(const PkgZipJobT *job); + +// The entries read or written so far (*count of them). +const PkgZipEntryT *pkgZipJobEntries(const PkgZipJobT *job, int32_t *count); + +// Stop the job if it is not done (a ZIP being made is removed; a file +// being unpacked is removed), and free it. NULL is ignored. +void pkgZipJobFree(PkgZipJobT *job); + +// Why the job failed ("" when it did not). +const char *pkgZipJobMessage(const PkgZipJobT *job); + +// How far the job is, 0 to 100. +int32_t pkgZipJobPercent(const PkgZipJobT *job); + +// Read the ZIP at path to its end, each entry's size and CRC-32 checked; +// with dir (not NULL) each entry is also written under it (folders made, +// files dated as in the ZIP; an existing file is replaced). A name that +// would leave dir (a drive, a leading '/', "..") fails it. NULL with the +// reason in err. +PkgZipJobT *pkgZipJobRead(const char *path, const char *dir, char *err, int32_t errSize); + +// Run the job for about maxWork bytes of work (at least one piece). +PkgZipJobStepE pkgZipJobStep(PkgZipJobT *job, int32_t maxWork); + #endif // PKG_ZIP_H diff --git a/src/libs/kpunch/pkg/pkgZipJob.c b/src/libs/kpunch/pkg/pkgZipJob.c new file mode 100644 index 00000000..f1afd3e9 --- /dev/null +++ b/src/libs/kpunch/pkg/pkgZipJob.c @@ -0,0 +1,445 @@ +// 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. + +// pkgZipJob.c -- plain ZIP archives a step at a time: listed (every +// entry read and checked), unpacked into a folder, or made from files, +// with the package library's ZIP reader and writer (pkgZip.c, +// pkgZipWrite.c). A step does at most a given amount of work, so a +// program draws progress and stays responsive between steps. + +#include "pkgZip.h" +#include "pkgInt.h" +#include "pkgManifest.h" +#include "dvxPlat.h" +#include "stb_ds_wrap.h" + +#include +#include +#include +#include +#include +#include + +#define PERCENT_MAX 100 +#define NAME_SEP '/' +#define NAME_UP ".." +#define DRIVE_SEP ':' +#define BACKSLASH '\\' + +struct PkgZipJobT { + PkgZipT *zip; // reading + FILE *src; // reading: the ZIP + PkgZipWriterT *w; // writing + char *dir; // reading: the folder unpacked into (NULL: listing) + char **names; // writing: stb_ds of the names in the ZIP (strdup'd) + char **sources; // writing: the files they come from (strdup'd) + PkgZipEntryT *entries; // stb_ds: read or written so far + FILE *out; // unpacking: the file being written + uint64_t total; // bytes of the ZIP (reading) or of the files (writing) + uint64_t done; // writing: bytes of the files already in + int32_t at; // writing: the file being added + bool entryOpen; // an entry begun: being read or added + PkgZipJobStepE state; + PkgZipEntryT entry; // the one begun + char outPath[DVX_MAX_PATH]; + char current[PKG_NAME_MAX]; + char err[PKG_ERR_MAX]; +}; + +// ============================================================ +// Prototypes +// ============================================================ + +static bool fail(char *err, int32_t errSize, const char *fmt, ...) __attribute__((format(printf, 3, 4))); +static void jobFail(PkgZipJobT *job, const char *fmt, ...) __attribute__((format(printf, 2, 3))); +static bool nameSafe(const char *name); +static bool outBegin(PkgZipJobT *job); +static void outEnd(PkgZipJobT *job, bool keep); +static bool outWrite(void *ctx, const uint8_t *data, int32_t len); +PkgZipJobT *pkgZipJobCreate(const char *path, const char *const *names, const char *const *sources, int32_t count, char *err, int32_t errSize); +const char *pkgZipJobCurrent(const PkgZipJobT *job); +const PkgZipEntryT *pkgZipJobEntries(const PkgZipJobT *job, int32_t *count); +void pkgZipJobFree(PkgZipJobT *job); +const char *pkgZipJobMessage(const PkgZipJobT *job); +int32_t pkgZipJobPercent(const PkgZipJobT *job); +PkgZipJobT *pkgZipJobRead(const char *path, const char *dir, char *err, int32_t errSize); +PkgZipJobStepE pkgZipJobStep(PkgZipJobT *job, int32_t maxWork); +static void readStep(PkgZipJobT *job, int32_t maxWork); +static void writeStep(PkgZipJobT *job, int32_t maxWork); + + +static bool fail(char *err, int32_t errSize, const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + vsnprintf(err, (size_t)errSize, fmt, ap); + va_end(ap); + return false; +} + + +static void jobFail(PkgZipJobT *job, const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + vsnprintf(job->err, sizeof(job->err), fmt, ap); + va_end(ap); + job->state = PkgZipJobFailedE; +} + + +// A name that stays inside the folder it is unpacked into: relative, no +// drive, no backslash, and no ".." part. +static bool nameSafe(const char *name) { + if (!name[0] || name[0] == NAME_SEP || strchr(name, DRIVE_SEP) || strchr(name, BACKSLASH)) { + return false; + } + + for (const char *p = name; *p; ) { + const char *end = strchr(p, NAME_SEP); + size_t len = end ? (size_t)(end - p) : strlen(p); + + if (len == strlen(NAME_UP) && strncmp(p, NAME_UP, len) == 0) { + return false; + } + + p = end ? end + 1 : p + len; + } + + return true; +} + + +// Unpacking: the entry begun's folder made (its parents too), or its file +// opened for writing. False with the reason said. +static bool outBegin(PkgZipJobT *job) { + char parent[DVX_MAX_PATH]; + + if (!nameSafe(job->entry.name)) { + jobFail(job, "%s would be unpacked outside %s.", job->entry.name, job->dir); + return false; + } + + if (!platformPathJoin(job->outPath, sizeof(job->outPath), job->dir, job->entry.name)) { + jobFail(job, "The path of %s in %s is too long.", job->entry.name, job->dir); + return false; + } + + platformPathNative(job->outPath); + + if (job->entry.isDir) { + if (platformMkdirRecursive(job->outPath) != 0) { + jobFail(job, "Cannot make the folder %s.", job->outPath); + return false; + } + + return true; + } + + platformPathDirName(job->outPath, parent, sizeof(parent)); + job->out = (!parent[0] || platformMkdirRecursive(parent) == 0) ? fopen(job->outPath, "wb") : NULL; + + if (!job->out) { + jobFail(job, "Cannot write %s.", job->outPath); + return false; + } + + return true; +} + + +// Unpacking: the file written closed, dated as in the ZIP when kept, else +// removed (a half-unpacked file is not left behind). +static void outEnd(PkgZipJobT *job, bool keep) { + if (!job->out) { + return; + } + + bool ok = fclose(job->out) == 0 && keep; + + job->out = NULL; + + if (!ok) { + remove(job->outPath); + return; + } + + struct utimbuf when; + + when.actime = pkgZipDosTime(job->entry.dosDate, job->entry.dosTime); + when.modtime = when.actime; + utime(job->outPath, &when); +} + + +// A PkgZipWriteFnT: unpacked bytes into the open file (none for a list). +static bool outWrite(void *ctx, const uint8_t *data, int32_t len) { + PkgZipJobT *job = (PkgZipJobT *)ctx; + + return !job->out || fwrite(data, 1, (size_t)len, job->out) == (size_t)len; +} + + +PkgZipJobT *pkgZipJobCreate(const char *path, const char *const *names, const char *const *sources, int32_t count, char *err, int32_t errSize) { + PkgZipJobT *job = (PkgZipJobT *)calloc(1, sizeof(PkgZipJobT)); + + if (!job) { + fail(err, errSize, PKG_ERR_NO_MEMORY); + return NULL; + } + + for (int32_t i = 0; i < count; i++) { + char *name = strdup(names[i]); + char *source = strdup(sources[i]); + struct stat st; + + arrput(job->names, name); + arrput(job->sources, source); + + if (!name || !source) { + fail(err, errSize, PKG_ERR_NO_MEMORY); + pkgZipJobFree(job); + return NULL; + } + + if (stat(source, &st) != 0 || !S_ISREG(st.st_mode)) { + fail(err, errSize, "Cannot read %s.", source); + pkgZipJobFree(job); + return NULL; + } + + job->total += (uint64_t)st.st_size; + } + + job->w = pkgZipCreate(path); + + if (!job->w) { + fail(err, errSize, "Cannot create %s.", path); + pkgZipJobFree(job); + return NULL; + } + + return job; +} + + +const char *pkgZipJobCurrent(const PkgZipJobT *job) { + return job->current; +} + + +const PkgZipEntryT *pkgZipJobEntries(const PkgZipJobT *job, int32_t *count) { + *count = (int32_t)arrlen(job->entries); + return job->entries; +} + + +void pkgZipJobFree(PkgZipJobT *job) { + if (!job) { + return; + } + + outEnd(job, false); + pkgZipAbort(job->w); + + if (job->zip) { + pkgZipClose(job->zip); + } + + if (job->src) { + fclose(job->src); + } + + for (int32_t i = 0; i < (int32_t)arrlen(job->names); i++) { + free(job->names[i]); + free(job->sources[i]); + } + + arrfree(job->names); + arrfree(job->sources); + arrfree(job->entries); + free(job->dir); + free(job); +} + + +const char *pkgZipJobMessage(const PkgZipJobT *job) { + return job->err; +} + + +// Reading: by bytes of the ZIP read; writing: by bytes of the files added. +int32_t pkgZipJobPercent(const PkgZipJobT *job) { + if (job->state == PkgZipJobDoneE) { + return PERCENT_MAX; + } + + if (!job->total) { + return 0; + } + + uint64_t done = job->zip ? pkgZipOffset(job->zip) : job->done + (job->entryOpen ? pkgZipAdded(job->w) : 0); + + return (int32_t)(done * PERCENT_MAX / job->total); +} + + +PkgZipJobT *pkgZipJobRead(const char *path, const char *dir, char *err, int32_t errSize) { + PkgZipJobT *job = (PkgZipJobT *)calloc(1, sizeof(PkgZipJobT)); + struct stat st; + + if (job && dir) { + job->dir = strdup(dir); + } + + if (!job || (dir && !job->dir)) { + free(job); + fail(err, errSize, PKG_ERR_NO_MEMORY); + return NULL; + } + + job->total = stat(path, &st) == 0 ? (uint64_t)st.st_size : 0; + job->src = fopen(path, "rb"); + job->zip = job->src ? pkgZipOpen(pkgZipFileRead, job->src) : NULL; + + if (!job->zip) { + fail(err, errSize, "Cannot read %s.", path); + pkgZipJobFree(job); + return NULL; + } + + return job; +} + + +PkgZipJobStepE pkgZipJobStep(PkgZipJobT *job, int32_t maxWork) { + if (job->state == PkgZipJobMoreE) { + if (job->zip) { + readStep(job, maxWork); + } else { + writeStep(job, maxWork); + } + } + + return job->state; +} + + +// Reading: the next entry's header (and, unpacking, its folder or file +// begun), or a piece of its data; at its end it is checked (and its file +// closed) and listed. +static void readStep(PkgZipJobT *job, int32_t maxWork) { + if (!job->entryOpen) { + PkgZipStepE step = pkgZipNext(job->zip, &job->entry); + + if (step == PkgZipEndE) { + job->current[0] = '\0'; + job->state = PkgZipJobDoneE; + return; + } + + if (step != PkgZipEntryE) { + jobFail(job, "%s", pkgZipProblemText(job->zip)); + return; + } + + snprintf(job->current, sizeof(job->current), "%s", job->entry.name); + job->entryOpen = !job->dir || outBegin(job); + return; + } + + PkgZipStepE data = pkgZipData(job->zip, outWrite, job, maxWork); + + if (data == PkgZipMoreE) { + return; + } + + job->entryOpen = false; + + if (data == PkgZipFailE && pkgZipProblem(job->zip) == PkgZipWriteFailedE) { + jobFail(job, "Cannot write %s. The disk may be full.", job->outPath); + } else if (data == PkgZipFailE) { + jobFail(job, "%s: %s", job->current, pkgZipProblemText(job->zip)); + } + + outEnd(job, data == PkgZipFileDoneE); + + if (data == PkgZipFileDoneE) { + arrput(job->entries, job->entry); + } +} + + +// Writing: the next file begun, or a piece of it added; after the last, +// the ZIP finished. +static void writeStep(PkgZipJobT *job, int32_t maxWork) { + if (!job->entryOpen) { + if (job->at >= (int32_t)arrlen(job->names)) { + PkgZipWriterT *w = job->w; + + job->w = NULL; + job->current[0] = '\0'; + + if (pkgZipFinish(w)) { + job->state = PkgZipJobDoneE; + } else { + jobFail(job, "Cannot finish the ZIP. The disk may be full."); + } + + return; + } + + snprintf(job->current, sizeof(job->current), "%s", job->names[job->at]); + + if (!pkgZipBeginFile(job->w, job->names[job->at], job->sources[job->at])) { + jobFail(job, "Cannot add %s as %s.", job->sources[job->at], job->names[job->at]); + return; + } + + job->entryOpen = true; + return; + } + + PkgZipStepE step = pkgZipStep(job->w, maxWork); + + if (step == PkgZipMoreE) { + return; + } + + job->entryOpen = false; + + if (step != PkgZipFileDoneE) { + jobFail(job, "Cannot add %s.", job->sources[job->at]); + return; + } + + struct stat st; + PkgZipEntryT e; + + memset(&e, 0, sizeof(e)); + snprintf(e.name, sizeof(e.name), "%s", job->names[job->at]); + e.size = stat(job->sources[job->at], &st) == 0 ? (uint32_t)st.st_size : 0; + pkgZipDosStamp(st.st_mtime, &e.dosDate, &e.dosTime); + arrput(job->entries, e); + job->done += e.size; + job->at++; +} diff --git a/src/libs/kpunch/pkg/pkgZipWrite.c b/src/libs/kpunch/pkg/pkgZipWrite.c index 4373f664..bc0c4dd8 100644 --- a/src/libs/kpunch/pkg/pkgZipWrite.c +++ b/src/libs/kpunch/pkg/pkgZipWrite.c @@ -19,11 +19,11 @@ // 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. - -// pkgZipWrite.c -- writing a package ZIP (see pkgZip.h): each file whole -// in memory, deflated with miniz's tdefl at its best setting (stored when -// that is no smaller), its local header written with the sizes, the -// central directory at the end. +// pkgZipWrite.c -- writing a package ZIP (see pkgZip.h): each file read +// a chunk at a time and deflated as it goes by miniz's tdefl at its best +// setting, its local header written first and completed when its data is +// in (the data written again, stored, when deflating does not make it +// smaller); the central directory at the end. #include "pkgZip.h" #include "miniz.h" @@ -40,15 +40,15 @@ // The central directory's first capacity, doubled as it fills #define ZIP_DIR_FIRST 16 -// DOS date and time fields -#define DOS_YEAR_SHIFT 9 -#define DOS_YEAR_BASE 1980 -#define DOS_YEAR_MAX 127 -#define DOS_MONTH_SHIFT 5 -#define DOS_HOUR_SHIFT 11 -#define DOS_MINUTE_SHIFT 5 -#define DOS_SECOND_UNIT 2 -#define TM_YEAR_BASE 1900 +// Bytes of an entry's source read (and deflated) at a time +#define ZIP_CHUNK 4096 + +typedef enum { + ZipIdleE, // no entry being added + ZipDeflateE, // the entry's source read and deflated + ZipStoreE, // the entry's source read and written as it is + ZipCopyE // an entry of another ZIP written as it is packed +} ZipAddE; typedef struct { char *name; @@ -62,46 +62,121 @@ typedef struct { } ZipDirEntryT; struct PkgZipWriterT { - FILE *f; - char *path; - ZipDirEntryT *entries; // in the order added - int32_t count; - int32_t cap; - uint32_t offset; // bytes written - bool failed; + FILE *f; + char *path; + ZipDirEntryT *entries; // in the order added (the one being added last) + FILE *src; // the source of the entry being added (NULL: mem) + const uint8_t *mem; // its bytes, when it is added from memory + tdefl_compressor *deflator; // made for the first deflated entry + int32_t count; + int32_t cap; + uint32_t offset; // where the next byte goes + uint32_t srcLen; + uint32_t srcDone; // bytes of the source taken + uint32_t left; // ZipCopyE: packed bytes still to come + ZipAddE adding; + bool failed; + bool storedAfter; // deflating came to the source's size: stored instead + uint8_t buf[ZIP_CHUNK]; }; // ============================================================ // Prototypes // ============================================================ -static void dosStamp(time_t when, uint16_t *date, uint16_t *time); -static void freeWriter(PkgZipWriterT *w); -void pkgZipAbort(PkgZipWriterT *w); -bool pkgZipAddFile(PkgZipWriterT *w, const char *name, const char *path); -bool pkgZipAddMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when); -PkgZipWriterT *pkgZipCreate(const char *path); -bool pkgZipFinish(PkgZipWriterT *w); -static bool writeBytes(PkgZipWriterT *w, const void *data, uint32_t len); +static PkgZipStepE entryEnd(PkgZipWriterT *w); +static ZipDirEntryT *entryNew(PkgZipWriterT *w, const char *name, uint32_t dataMax); +static void freeWriter(PkgZipWriterT *w); +static void headerFill(const ZipDirEntryT *e, uint8_t hdr[PKG_ZIP_LOCAL_SIZE]); +static bool headerWrite(PkgZipWriterT *w, const ZipDirEntryT *e); +void pkgZipAbort(PkgZipWriterT *w); +uint32_t pkgZipAdded(const PkgZipWriterT *w); +bool pkgZipAddMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when); +bool pkgZipBeginCopy(PkgZipWriterT *w, const PkgZipEntryT *e); +bool pkgZipBeginFile(PkgZipWriterT *w, const char *name, const char *path); +bool pkgZipBeginMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when); +bool pkgZipCopyWrite(void *ctx, const uint8_t *data, int32_t len); +PkgZipWriterT *pkgZipCreate(const char *path); +bool pkgZipFinish(PkgZipWriterT *w); +PkgZipStepE pkgZipStep(PkgZipWriterT *w, int32_t maxWork); +static mz_bool putDeflated(const void *buf, int len, void *user); +static bool sourceBegin(PkgZipWriterT *w, const char *name, time_t when, uint32_t len); +static const uint8_t *sourceRead(PkgZipWriterT *w, uint32_t len); +static bool storeInstead(PkgZipWriterT *w); +static bool writeBytes(PkgZipWriterT *w, const void *data, uint32_t len); -// A time as the ZIP's DOS date and time (1980 .. 2107, two-second steps). -static void dosStamp(time_t when, uint16_t *date, uint16_t *time) { - struct tm *t = localtime(&when); - int32_t year = t ? t->tm_year + TM_YEAR_BASE - DOS_YEAR_BASE : 0; +// The entry being added from a source is in: its source closed and its +// local header completed (a copied entry's was complete from the start). +static PkgZipStepE entryEnd(PkgZipWriterT *w) { + ZipDirEntryT *e = &w->entries[w->count - 1]; + bool copied = w->adding == ZipCopyE; + uint8_t hdr[PKG_ZIP_LOCAL_SIZE]; - if (!t || year < 0) { - *date = (uint16_t)((1 << DOS_MONTH_SHIFT) | 1); - *time = 0; - return; + if (w->src) { + fclose(w->src); } - if (year > DOS_YEAR_MAX) { - year = DOS_YEAR_MAX; + w->src = NULL; + w->mem = NULL; + w->adding = ZipIdleE; + + if (copied) { + return PkgZipFileDoneE; } - *date = (uint16_t)((year << DOS_YEAR_SHIFT) | ((t->tm_mon + 1) << DOS_MONTH_SHIFT) | t->tm_mday); - *time = (uint16_t)((t->tm_hour << DOS_HOUR_SHIFT) | (t->tm_min << DOS_MINUTE_SHIFT) | (t->tm_sec / DOS_SECOND_UNIT)); + e->size = w->srcLen; + e->packed = e->method == PKG_ZIP_STORED ? w->srcLen : e->packed; + headerFill(e, hdr); + + if (fseek(w->f, (long)e->offset, SEEK_SET) != 0 || fwrite(hdr, 1, sizeof(hdr), w->f) != sizeof(hdr) || fseek(w->f, (long)w->offset, SEEK_SET) != 0) { + w->failed = true; + return PkgZipFailE; + } + + return PkgZipFileDoneE; +} + + +// A new entry named name, at most dataMax bytes of data, onto the central +// directory (its offset set; the caller fills the rest). NULL, leaving +// the writer as it was, when the name will not do, the ZIP would be too +// big, or out of memory. +static ZipDirEntryT *entryNew(PkgZipWriterT *w, const char *name, uint32_t dataMax) { + size_t nameLen = strlen(name); + + if (w->failed || w->adding != ZipIdleE || nameLen == 0 || nameLen >= PKG_NAME_MAX || w->count >= (int32_t)ZIP_MAX_ENTRIES) { + return NULL; + } + + if ((uint64_t)w->offset + PKG_ZIP_LOCAL_SIZE + nameLen + dataMax > ZIP_MAX_OFFSET) { + return NULL; + } + + if (w->count == w->cap) { + int32_t cap = w->cap ? w->cap * 2 : ZIP_DIR_FIRST; + ZipDirEntryT *grow = (ZipDirEntryT *)realloc(w->entries, (size_t)cap * sizeof(ZipDirEntryT)); + + if (!grow) { + return NULL; + } + + w->entries = grow; + w->cap = cap; + } + + ZipDirEntryT *e = &w->entries[w->count]; + + memset(e, 0, sizeof(*e)); + e->name = strdup(name); + e->offset = w->offset; + + if (!e->name) { + return NULL; + } + + w->count++; + return e; } @@ -110,12 +185,41 @@ static void freeWriter(PkgZipWriterT *w) { free(w->entries[i].name); } + if (w->src) { + fclose(w->src); + } + + free(w->deflator); free(w->entries); free(w->path); free(w); } +// e's local header (its name follows it). +static void headerFill(const ZipDirEntryT *e, uint8_t hdr[PKG_ZIP_LOCAL_SIZE]) { + memset(hdr, 0, PKG_ZIP_LOCAL_SIZE); + dvxPutLe32(hdr, PKG_ZIP_LOCAL_SIG); + dvxPutLe16(hdr + PKG_ZIP_LH_NEEDED, PKG_ZIP_VERSION); + dvxPutLe16(hdr + PKG_ZIP_LH_METHOD, e->method); + dvxPutLe16(hdr + PKG_ZIP_LH_TIME, e->dosTime); + dvxPutLe16(hdr + PKG_ZIP_LH_DATE, e->dosDate); + dvxPutLe32(hdr + PKG_ZIP_LH_CRC, e->crc); + dvxPutLe32(hdr + PKG_ZIP_LH_PACKED, e->packed); + dvxPutLe32(hdr + PKG_ZIP_LH_SIZE, e->size); + dvxPutLe16(hdr + PKG_ZIP_LH_NAME_LEN, (uint16_t)strlen(e->name)); +} + + +// e's local header and name, at the end. +static bool headerWrite(PkgZipWriterT *w, const ZipDirEntryT *e) { + uint8_t hdr[PKG_ZIP_LOCAL_SIZE]; + + headerFill(e, hdr); + return writeBytes(w, hdr, sizeof(hdr)) && writeBytes(w, e->name, (uint32_t)strlen(e->name)); +} + + void pkgZipAbort(PkgZipWriterT *w) { if (!w) { return; @@ -130,95 +234,102 @@ void pkgZipAbort(PkgZipWriterT *w) { } -bool pkgZipAddFile(PkgZipWriterT *w, const char *name, const char *path) { +// A file stored after deflating it did no good has been read through +// once already: it counts as done. +uint32_t pkgZipAdded(const PkgZipWriterT *w) { + if (w->adding == ZipIdleE || w->adding == ZipCopyE) { + return 0; + } + + return w->storedAfter ? w->srcLen : w->srcDone; +} + + +bool pkgZipAddMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when) { + PkgZipStepE step; + + if (!pkgZipBeginMem(w, name, data, len, when)) { + return false; + } + + while ((step = pkgZipStep(w, INT32_MAX)) == PkgZipMoreE) { + } + + return step == PkgZipFileDoneE; +} + + +bool pkgZipBeginCopy(PkgZipWriterT *w, const PkgZipEntryT *e) { + ZipDirEntryT *d = e->isDir ? NULL : entryNew(w, e->name, e->packedSize); + + if (!d) { + return false; + } + + d->crc = e->crc; + d->packed = e->packedSize; + d->size = e->size; + d->method = e->method; + d->dosDate = e->dosDate; + d->dosTime = e->dosTime; + w->left = e->packedSize; + w->adding = w->left ? ZipCopyE : ZipIdleE; + return headerWrite(w, d); +} + + +bool pkgZipBeginFile(PkgZipWriterT *w, const char *name, const char *path) { struct stat st; if (stat(path, &st) != 0 || (uint64_t)st.st_size > ZIP_MAX_OFFSET) { return false; } - uint32_t len = (uint32_t)st.st_size; - uint8_t *data = (uint8_t *)malloc(len ? len : 1); - FILE *f = data ? fopen(path, "rb") : NULL; - bool ok = f && fread(data, 1, len, f) == len; + FILE *src = fopen(path, "rb"); - if (f) { - fclose(f); + if (src == NULL) { + return false; } - ok = ok && pkgZipAddMem(w, name, data, len, st.st_mtime); - free(data); - return ok; + if (!sourceBegin(w, name, st.st_mtime, (uint32_t)st.st_size)) { + fclose(src); + return false; + } + + w->src = src; + return true; } -bool pkgZipAddMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when) { - size_t nameLen = strlen(name); - size_t packed = 0; - int flags = (int)tdefl_create_comp_flags_from_zip_params(MZ_UBER_COMPRESSION, -MZ_DEFAULT_WINDOW_BITS, MZ_DEFAULT_STRATEGY); - void *deflated = len ? tdefl_compress_mem_to_heap(data, len, &packed, flags) : NULL; - uint8_t hdr[PKG_ZIP_LOCAL_SIZE]; - - if (w->failed || nameLen == 0 || nameLen >= PKG_NAME_MAX || w->count >= (int32_t)ZIP_MAX_ENTRIES) { - free(deflated); +bool pkgZipBeginMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when) { + if (!sourceBegin(w, name, when, len)) { return false; } - // Stored when deflating does not make it smaller - bool stored = !deflated || packed >= len; - const void *body = stored ? data : deflated; - uint32_t bodyLen = stored ? len : (uint32_t)packed; + w->mem = (const uint8_t *)data; + return true; +} - if ((uint64_t)w->offset + PKG_ZIP_LOCAL_SIZE + nameLen + bodyLen > ZIP_MAX_OFFSET) { - free(deflated); + +bool pkgZipCopyWrite(void *ctx, const uint8_t *data, int32_t len) { + PkgZipWriterT *w = (PkgZipWriterT *)ctx; + + if (w->adding != ZipCopyE || len < 0 || (uint32_t)len > w->left) { + w->failed = true; return false; } - if (w->count == w->cap) { - int32_t cap = w->cap ? w->cap * 2 : ZIP_DIR_FIRST; - ZipDirEntryT *grow = (ZipDirEntryT *)realloc(w->entries, (size_t)cap * sizeof(ZipDirEntryT)); - - if (!grow) { - free(deflated); - return false; - } - - w->entries = grow; - w->cap = cap; - } - - ZipDirEntryT *e = &w->entries[w->count]; - - memset(e, 0, sizeof(*e)); - e->name = strdup(name); - e->crc = (uint32_t)mz_crc32(MZ_CRC32_INIT, (const uint8_t *)data, len); - e->packed = bodyLen; - e->size = len; - e->offset = w->offset; - e->method = stored ? PKG_ZIP_STORED : PKG_ZIP_DEFLATED; - dosStamp(when, &e->dosDate, &e->dosTime); - - if (!e->name) { - free(deflated); + if (!writeBytes(w, data, (uint32_t)len)) { return false; } - w->count++; - memset(hdr, 0, sizeof(hdr)); - pkgPut32(hdr, PKG_ZIP_LOCAL_SIG); - pkgPut16(hdr + PKG_ZIP_LH_NEEDED, PKG_ZIP_VERSION); - pkgPut16(hdr + PKG_ZIP_LH_METHOD, e->method); - pkgPut16(hdr + PKG_ZIP_LH_TIME, e->dosTime); - pkgPut16(hdr + PKG_ZIP_LH_DATE, e->dosDate); - pkgPut32(hdr + PKG_ZIP_LH_CRC, e->crc); - pkgPut32(hdr + PKG_ZIP_LH_PACKED, e->packed); - pkgPut32(hdr + PKG_ZIP_LH_SIZE, e->size); - pkgPut16(hdr + PKG_ZIP_LH_NAME_LEN, (uint16_t)nameLen); + w->left -= (uint32_t)len; - bool ok = writeBytes(w, hdr, sizeof(hdr)) && writeBytes(w, name, (uint32_t)nameLen) && writeBytes(w, body, bodyLen); + if (w->left == 0) { + entryEnd(w); + } - free(deflated); - return ok; + return true; } @@ -244,23 +355,28 @@ PkgZipWriterT *pkgZipCreate(const char *path) { bool pkgZipFinish(PkgZipWriterT *w) { uint32_t dirStart = w->offset; + // An entry not all in cannot be listed + if (w->adding != ZipIdleE) { + w->failed = true; + } + for (int32_t i = 0; i < w->count && !w->failed; i++) { const ZipDirEntryT *e = &w->entries[i]; uint8_t rec[PKG_ZIP_CENTRAL_SIZE]; size_t nameLen = strlen(e->name); memset(rec, 0, sizeof(rec)); - pkgPut32(rec, PKG_ZIP_CENTRAL_SIG); - pkgPut16(rec + PKG_ZIP_CD_MADE_BY, PKG_ZIP_VERSION); - pkgPut16(rec + PKG_ZIP_CD_NEEDED, PKG_ZIP_VERSION); - pkgPut16(rec + PKG_ZIP_CD_METHOD, e->method); - pkgPut16(rec + PKG_ZIP_CD_TIME, e->dosTime); - pkgPut16(rec + PKG_ZIP_CD_DATE, e->dosDate); - pkgPut32(rec + PKG_ZIP_CD_CRC, e->crc); - pkgPut32(rec + PKG_ZIP_CD_PACKED, e->packed); - pkgPut32(rec + PKG_ZIP_CD_SIZE, e->size); - pkgPut16(rec + PKG_ZIP_CD_NAME_LEN, (uint16_t)nameLen); - pkgPut32(rec + PKG_ZIP_CD_OFFSET, e->offset); + dvxPutLe32(rec, PKG_ZIP_CENTRAL_SIG); + dvxPutLe16(rec + PKG_ZIP_CD_MADE_BY, PKG_ZIP_VERSION); + dvxPutLe16(rec + PKG_ZIP_CD_NEEDED, PKG_ZIP_VERSION); + dvxPutLe16(rec + PKG_ZIP_CD_METHOD, e->method); + dvxPutLe16(rec + PKG_ZIP_CD_TIME, e->dosTime); + dvxPutLe16(rec + PKG_ZIP_CD_DATE, e->dosDate); + dvxPutLe32(rec + PKG_ZIP_CD_CRC, e->crc); + dvxPutLe32(rec + PKG_ZIP_CD_PACKED, e->packed); + dvxPutLe32(rec + PKG_ZIP_CD_SIZE, e->size); + dvxPutLe16(rec + PKG_ZIP_CD_NAME_LEN, (uint16_t)nameLen); + dvxPutLe32(rec + PKG_ZIP_CD_OFFSET, e->offset); writeBytes(w, rec, sizeof(rec)); writeBytes(w, e->name, (uint32_t)nameLen); } @@ -268,11 +384,11 @@ bool pkgZipFinish(PkgZipWriterT *w) { uint8_t eocd[PKG_ZIP_EOCD_SIZE]; memset(eocd, 0, sizeof(eocd)); - pkgPut32(eocd, PKG_ZIP_EOCD_SIG); - pkgPut16(eocd + PKG_ZIP_EOCD_DISK_ENTRIES, (uint16_t)w->count); - pkgPut16(eocd + PKG_ZIP_EOCD_ENTRIES, (uint16_t)w->count); - pkgPut32(eocd + PKG_ZIP_EOCD_DIR_SIZE, w->offset - dirStart); - pkgPut32(eocd + PKG_ZIP_EOCD_DIR_OFFSET, dirStart); + dvxPutLe32(eocd, PKG_ZIP_EOCD_SIG); + dvxPutLe16(eocd + PKG_ZIP_EOCD_DISK_ENTRIES, (uint16_t)w->count); + dvxPutLe16(eocd + PKG_ZIP_EOCD_ENTRIES, (uint16_t)w->count); + dvxPutLe32(eocd + PKG_ZIP_EOCD_DIR_SIZE, w->offset - dirStart); + dvxPutLe32(eocd + PKG_ZIP_EOCD_DIR_OFFSET, dirStart); writeBytes(w, eocd, sizeof(eocd)); FILE *f = w->f; @@ -290,6 +406,153 @@ bool pkgZipFinish(PkgZipWriterT *w) { } +// At least one chunk of the source each call (none for a copied entry: +// its bytes come through pkgZipCopyWrite). +PkgZipStepE pkgZipStep(PkgZipWriterT *w, int32_t maxWork) { + if (w->failed) { + return PkgZipFailE; + } + + if (w->adding == ZipIdleE) { + return PkgZipFileDoneE; + } + + if (w->adding == ZipCopyE) { + return PkgZipMoreE; + } + + for (int32_t work = 0; ; ) { + ZipDirEntryT *e = &w->entries[w->count - 1]; + + if (w->adding == ZipStoreE && w->srcDone == w->srcLen) { + return entryEnd(w); + } + + if (work > 0 && work >= maxWork) { + return PkgZipMoreE; + } + + uint32_t n = w->srcLen - w->srcDone; + const uint8_t *in; + + if (n > ZIP_CHUNK) { + n = ZIP_CHUNK; + } + + in = sourceRead(w, n); + + if (!in) { + w->failed = true; + return PkgZipFailE; + } + + e->crc = (uint32_t)mz_crc32(e->crc, in, n); + w->srcDone += n; + work += (int32_t)n; + + if (w->adding == ZipStoreE) { + if (!writeBytes(w, in, n)) { + return PkgZipFailE; + } + + continue; + } + + tdefl_status status = tdefl_compress_buffer(w->deflator, in, n, w->srcDone == w->srcLen ? TDEFL_FINISH : TDEFL_NO_FLUSH); + + if (w->storedAfter) { + if (!storeInstead(w)) { + return PkgZipFailE; + } + } else if (status == TDEFL_STATUS_DONE) { + return entryEnd(w); + } else if (status != TDEFL_STATUS_OKAY) { + w->failed = true; + return PkgZipFailE; + } + } +} + + +// tdefl's output: written, unless it comes to the source's size (stored +// instead: deflating stops). So what was written is always shorter than +// the stored data written over it. +static mz_bool putDeflated(const void *buf, int len, void *user) { + PkgZipWriterT *w = (PkgZipWriterT *)user; + ZipDirEntryT *e = &w->entries[w->count - 1]; + + if ((uint64_t)e->packed + (uint32_t)len >= w->srcLen) { + w->storedAfter = true; + return MZ_FALSE; + } + + e->packed += (uint32_t)len; + return writeBytes(w, buf, (uint32_t)len) ? MZ_TRUE : MZ_FALSE; +} + + +// Start an entry of len bytes from a source (set by the caller): its +// local header written with what is known, completed by entryEnd. +static bool sourceBegin(PkgZipWriterT *w, const char *name, time_t when, uint32_t len) { + int flags = (int)tdefl_create_comp_flags_from_zip_params(MZ_UBER_COMPRESSION, -MZ_DEFAULT_WINDOW_BITS, MZ_DEFAULT_STRATEGY); + ZipDirEntryT *e = entryNew(w, name, len); + + if (!e) { + return false; + } + + // Nothing to deflate in an empty one + e->crc = (uint32_t)MZ_CRC32_INIT; + e->method = len ? PKG_ZIP_DEFLATED : PKG_ZIP_STORED; + w->adding = len ? ZipDeflateE : ZipStoreE; + w->srcLen = len; + w->srcDone = 0; + w->storedAfter = false; + pkgZipDosStamp(when, &e->dosDate, &e->dosTime); + + if (len && !w->deflator) { + w->deflator = (tdefl_compressor *)malloc(sizeof(tdefl_compressor)); + } + + if (len && (!w->deflator || tdefl_init(w->deflator, putDeflated, w, flags) != TDEFL_STATUS_OKAY)) { + w->failed = true; + return false; + } + + return headerWrite(w, e); +} + + +// The next len bytes of the source; NULL when it cannot be read. +static const uint8_t *sourceRead(PkgZipWriterT *w, uint32_t len) { + if (!w->src) { + return w->mem + w->srcDone; + } + + return fread(w->buf, 1, len, w->src) == len ? w->buf : NULL; +} + + +// Deflating came to the source's size: write it again from its start, +// stored, over what was deflated. +static bool storeInstead(PkgZipWriterT *w) { + ZipDirEntryT *e = &w->entries[w->count - 1]; + uint32_t data = e->offset + PKG_ZIP_LOCAL_SIZE + (uint32_t)strlen(e->name); + + if ((w->src && fseek(w->src, 0, SEEK_SET) != 0) || fseek(w->f, (long)data, SEEK_SET) != 0) { + w->failed = true; + return false; + } + + e->crc = (uint32_t)MZ_CRC32_INIT; + e->method = PKG_ZIP_STORED; + w->offset = data; + w->srcDone = 0; + w->adding = ZipStoreE; + return true; +} + + static bool writeBytes(PkgZipWriterT *w, const void *data, uint32_t len) { if (w->failed || (len > 0 && fwrite(data, 1, len, w->f) != len)) { w->failed = true; diff --git a/src/libs/kpunch/pkg/thirdparty/monocypher/DVX-VERSION.md b/src/libs/kpunch/pkg/thirdparty/monocypher/DVX-VERSION.md deleted file mode 100644 index 2bda8a3a..00000000 --- a/src/libs/kpunch/pkg/thirdparty/monocypher/DVX-VERSION.md +++ /dev/null @@ -1,31 +0,0 @@ -# Monocypher for DVX - -| Item | Value | -|---|---| -| Project | Monocypher, a small, auditable crypto library, https://monocypher.org | -| Version | 4.0.3 (2026-06-15; fixes a timing leak in EdDSA/Ed25519 signing) | -| Download | https://monocypher.org/download/monocypher-4.0.3.tar.gz | -| Tarball SHA-512 | 40904ada5c7ee4f7741733e38b69a30a4b0561cbffba5ffe7c2dce16136d540251ec0d9056ff606510d3b5b708fb8a40db7e0870d4a0b2dc17ba2bfb880f8965 (matches the .sha512 published beside the download) | -| Files kept | src/monocypher.c, src/monocypher.h, src/optional/monocypher-ed25519.c, src/optional/monocypher-ed25519.h, LICENCE.md, AUTHORS.md -- byte for byte as in the tarball, in the same folders. Not kept: doc/, tests/, the makefile, README, CHANGELOG. | -| Licence | Dual: 2-clause BSD or CC-0 (LICENCE.md); DVX takes the BSD licence. Keep the notice with the source. | -| Patches | None | - -## Where it is used - -- `pkg.lib` (src/libs/kpunch/pkg): Ed25519 (the SHA-512 flavour in - optional/) checks the signatures of add-on packages (pkgSign.c); - crypto_wipe clears key material. -- The package tool (`bin/host/dvxpkg`, and DVXPKG.EXE in DVX): makes keys - and signs packages. -- Host tests (src/test/pkg): an RFC 8032 vector checks the wiring. - -It compiles with the normal DVX warning flags for DJGPP and the host; no -flags are relaxed for it. - -## Updating - -1. Download the new tarball, check its SHA-512 against the .sha512 file - on the download page, record the new version and hash here. -2. Replace the six files above, nothing else. -3. Rebuild (`make`), run `make -C src/test/pkg` (the RFC 8032 vector must - still match) and `make -C src/test/setup`. diff --git a/src/libs/kpunch/pkgnet/pkgNet.c b/src/libs/kpunch/pkgnet/pkgNet.c index fe5b22ab..ddfa5387 100644 --- a/src/libs/kpunch/pkgnet/pkgNet.c +++ b/src/libs/kpunch/pkgnet/pkgNet.c @@ -44,7 +44,8 @@ #include "dvxPrefs.h" #include "dvxPlat.h" #include "dvxTypes.h" -#include "serial/security/secHash.h" +#include "crypto/secHash.h" +#include "crypto/security.h" #include "stb_ds_wrap.h" #include @@ -146,6 +147,9 @@ struct PkgNetS { PkgNetMineT *mine; // stb_ds: what the account has sent char current[PKG_TEXT_MAX]; char err[PKGNET_ERR_MAX]; + int32_t polling; // pkgNetPoll calls under way + bool cancelAsked; // pkgNetCancel asked meanwhile: done when they return + bool freeAsked; // pkgNetFree likewise }; DVX_ABI_STAMP(pkgnet); @@ -169,7 +173,6 @@ static bool entryRead(const JsonDocT *doc, int32_t node, PkgNetEn static bool failed(PkgNetT *n, const char *fmt, ...); static bool fetchPlan(PkgNetT *n); static uint32_t fileSize(const char *path); -static bool hexRead(const char *text, uint8_t *out, int32_t outLen); static void itemAdd(PkgNetT *n, const PkgNetEntryT *e); static void itemApiPath(const PkgNetT *n, const char *api, char *out, int32_t outSize); static const PkgNetEntryT *itemEntry(const PkgNetT *n); @@ -203,14 +206,15 @@ PkgNetStateE pkgNetPoll(PkgNetT *n, uint32_t nowMs); void pkgNetProgress(const PkgNetT *n, uint32_t *done, uint32_t *total); bool pkgNetUpdatesStart(PkgNetT *n, uint32_t nowMs); bool pkgNetUploadStart(PkgNetT *n, const char *path, uint32_t nowMs); +static PkgNetStateE pollStep(PkgNetT *n, uint32_t nowMs); static bool requestBody(PkgNetT *n, const void *data, int32_t len, const char *type); +static bool requestBodyFile(PkgNetT *n, const char *path, const char *type); static void requestGo(PkgNetT *n, uint32_t nowMs); static bool requestMake(PkgNetT *n, const char *method, const char *path, StageE stage); static void signatureAnswer(PkgNetT *n, uint32_t nowMs); static PkgNetStateE stateOf(const PkgNetT *n); static void textCopy(char *out, int32_t outSize, const char *text); static void updatesBody(PkgNetT *n, char **body); -static void wipe(void *p, size_t len); // What the server said to an upload or a list of the account's versions: @@ -247,20 +251,12 @@ static void accountNext(PkgNetT *n, uint32_t nowMs) { snprintf(auth, sizeof(auth), "%s%s", BEARER, n->token); + // The package is sent from its file as the server takes it if (n->job == JobUploadE) { - int32_t len = 0; - char *data = platformReadFile(n->upload, &len); - - if (!data) { - failed(n, "%s cannot be read.", n->upload); - return; - } - - if (requestMake(n, METHOD_POST, API_UPLOAD, StageUploadE) && httpHeaderAdd(n->req, HEADER_AUTH, auth) && requestBody(n, data, len, TYPE_PACKAGE)) { + if (requestMake(n, METHOD_POST, API_UPLOAD, StageUploadE) && httpHeaderAdd(n->req, HEADER_AUTH, auth) && requestBodyFile(n, n->upload, TYPE_PACKAGE)) { requestGo(n, nowMs); } - free(data); return; } @@ -486,7 +482,7 @@ static bool entryRead(const JsonDocT *doc, int32_t node, PkgNetEntryT *e) { return false; } - if (!hexRead(jsonText(doc, jsonFind(doc, node, "sha256")), e->sha256, PKG_SHA256_SIZE) || size < 0 || size > SIZE_MAX_JSON) { + if (!pkgHexDecode(jsonText(doc, jsonFind(doc, node, "sha256")), e->sha256, PKG_SHA256_SIZE) || size < 0 || size > SIZE_MAX_JSON) { return false; } @@ -581,27 +577,6 @@ static uint32_t fileSize(const char *path) { } -// outLen bytes from text, two hex digits each. False when text is not -// exactly that. -static bool hexRead(const char *text, uint8_t *out, int32_t outLen) { - if ((int32_t)strlen(text) != outLen * 2) { - return false; - } - - for (int32_t i = 0; i < outLen; i++) { - unsigned int byte; - - if (sscanf(text + i * 2, "%2x", &byte) != 1) { - return false; - } - - out[i] = (uint8_t)byte; - } - - return true; -} - - // Add e to what the fetch downloads (once), its files named from its id // and version. static void itemAdd(PkgNetT *n, const PkgNetEntryT *e) { @@ -761,13 +736,13 @@ static void loginStart(PkgNetT *n, uint32_t nowMs) { jsonQuote(n->user, user, sizeof(user)); jsonQuote(n->password, password, sizeof(password)); len = snprintf(body, sizeof(body), "{\"email\":%s,\"password\":%s}", user, password); - wipe(password, sizeof(password)); + secSecureZero(password, (int)sizeof(password)); if (requestMake(n, METHOD_POST, API_LOGIN, StageLoginE) && requestBody(n, body, len, TYPE_JSON)) { requestGo(n, nowMs); } - wipe(body, sizeof(body)); + secSecureZero(body, (int)sizeof(body)); } @@ -826,12 +801,17 @@ void pkgNetAccountSet(PkgNetT *n, const char *user, const char *password) { if (n) { textCopy(n->user, sizeof(n->user), user ? user : ""); textCopy(n->password, sizeof(n->password), password ? password : ""); - wipe(n->token, sizeof(n->token)); + secSecureZero(n->token, (int)sizeof(n->token)); } } void pkgNetCancel(PkgNetT *n) { + if (n && n->polling > 0) { + n->cancelAsked = true; + return; + } + if (n && busy(n)) { jobForget(n); failed(n, "Stopped before it finished."); @@ -1001,13 +981,18 @@ void pkgNetFree(PkgNetT *n) { return; } + if (n->polling > 0) { + n->freeAsked = true; + return; + } + jobForget(n); pkgDbFree(&n->db); entriesFree(&n->catalogue); entriesFree(&n->updates); arrfree(n->mine); - wipe(n->password, sizeof(n->password)); - wipe(n->token, sizeof(n->token)); + secSecureZero(n->password, (int)sizeof(n->password)); + secSecureZero(n->token, (int)sizeof(n->token)); free(n); } @@ -1033,13 +1018,98 @@ bool pkgNetMineStart(PkgNetT *n, uint32_t nowMs) { } +// A cancel or free asked for during a poll (a wait for the network inside +// it lets other code run) is carried out when the poll returns: the +// request and the items it works on outlive the code still using them. +// After a free the poll reports failure. PkgNetStateE pkgNetPoll(PkgNetT *n, uint32_t nowMs) { - HttpStateE st; - if (!n) { return PkgNetFailedE; } + n->polling++; + + PkgNetStateE st = pollStep(n, nowMs); + + n->polling--; + + if (n->polling == 0 && n->freeAsked) { + n->freeAsked = false; + pkgNetFree(n); + return PkgNetFailedE; + } + + if (n->polling == 0 && n->cancelAsked) { + n->cancelAsked = false; + pkgNetCancel(n); + st = stateOf(n); + } + + return st; +} + + +void pkgNetProgress(const PkgNetT *n, uint32_t *done, uint32_t *total) { + uint32_t got = 0; + uint32_t size = 0; + + *done = 0; + *total = 0; + + if (!n || n->job != JobFetchE) { + return; + } + + *done = n->doneBytes; + *total = n->totalBytes; + + if (n->stage == StagePackageE) { + httpProgress(n->req, &got, &size); + *done += got; + } else if (n->stage == StageCheckE || n->stage == StageSignatureE) { + *done += itemEntry(n)->size; + } + + if (*done > *total) { + *done = *total; + } +} + + +bool pkgNetUpdatesStart(PkgNetT *n, uint32_t nowMs) { + char *body = NULL; + + if (!jobStart(n, JobUpdatesE)) { + return false; + } + + updatesBody(n, &body); + + if (requestMake(n, METHOD_POST, API_UPDATES, StageUpdatesE) && requestBody(n, body, (int32_t)arrlen(body), TYPE_JSON)) { + requestGo(n, nowMs); + } + + arrfree(body); + return true; +} + + +bool pkgNetUploadStart(PkgNetT *n, const char *path, uint32_t nowMs) { + if (!jobStart(n, JobUploadE)) { + return false; + } + + textCopy(n->upload, sizeof(n->upload), path ? path : ""); + textCopy(n->current, sizeof(n->current), platformPathBaseName(n->upload)); + accountNext(n, nowMs); + return true; +} + + +// pkgNetPoll's work: one step of the job. +static PkgNetStateE pollStep(PkgNetT *n, uint32_t nowMs) { + HttpStateE st; + if (n->stage == StageCheckE) { checkStep(n, nowMs); return stateOf(n); @@ -1104,63 +1174,6 @@ PkgNetStateE pkgNetPoll(PkgNetT *n, uint32_t nowMs) { } -void pkgNetProgress(const PkgNetT *n, uint32_t *done, uint32_t *total) { - uint32_t got = 0; - uint32_t size = 0; - - *done = 0; - *total = 0; - - if (!n || n->job != JobFetchE) { - return; - } - - *done = n->doneBytes; - *total = n->totalBytes; - - if (n->stage == StagePackageE) { - httpProgress(n->req, &got, &size); - *done += got; - } else if (n->stage == StageCheckE || n->stage == StageSignatureE) { - *done += itemEntry(n)->size; - } - - if (*done > *total) { - *done = *total; - } -} - - -bool pkgNetUpdatesStart(PkgNetT *n, uint32_t nowMs) { - char *body = NULL; - - if (!jobStart(n, JobUpdatesE)) { - return false; - } - - updatesBody(n, &body); - - if (requestMake(n, METHOD_POST, API_UPDATES, StageUpdatesE) && requestBody(n, body, (int32_t)arrlen(body), TYPE_JSON)) { - requestGo(n, nowMs); - } - - arrfree(body); - return true; -} - - -bool pkgNetUploadStart(PkgNetT *n, const char *path, uint32_t nowMs) { - if (!jobStart(n, JobUploadE)) { - return false; - } - - textCopy(n->upload, sizeof(n->upload), path ? path : ""); - textCopy(n->current, sizeof(n->current), platformPathBaseName(n->upload)); - accountNext(n, nowMs); - return true; -} - - // A body for the request made (data copied), of type. False (the job // failed) when out of memory. static bool requestBody(PkgNetT *n, const void *data, int32_t len, const char *type) { @@ -1168,6 +1181,12 @@ static bool requestBody(PkgNetT *n, const void *data, int32_t len, const char *t } +// The request's body from the file at path, sent as the server takes it. +static bool requestBodyFile(PkgNetT *n, const char *path, const char *type) { + return httpBodyFile(n->req, path, type) || failed(n, "%s cannot be read.", path); +} + + // Start the request made (with its headers and body). static void requestGo(PkgNetT *n, uint32_t nowMs) { if (!httpStart(n->req, nowMs)) { @@ -1261,13 +1280,3 @@ static void updatesBody(PkgNetT *n, char **body) { arrput(*body, ']'); arrput(*body, '}'); } - - -// Overwrite len bytes at p (a password, a token) before the memory goes. -static void wipe(void *p, size_t len) { - volatile uint8_t *b = (volatile uint8_t *)p; - - for (size_t i = 0; i < len; i++) { - b[i] = 0; - } -} diff --git a/src/libs/kpunch/pkgnet/pkgNetBas.c b/src/libs/kpunch/pkgnet/pkgNetBas.c index 53f1e7c7..14aea3e5 100644 --- a/src/libs/kpunch/pkgnet/pkgNetBas.c +++ b/src/libs/kpunch/pkgnet/pkgNetBas.c @@ -71,6 +71,7 @@ const char *PkgNetField(int32_t h, const char *id, int32_t field) int32_t PkgNetMine(int32_t h); const char *PkgNetMineRows(int32_t h); int32_t PkgNetOpen(const char *server); +int32_t PkgNetOpenIn(const char *server, const char *root, const char *downloadDir); const char *PkgNetPassword(void); int32_t PkgNetPercent(int32_t h); int32_t PkgNetPort(void); @@ -290,6 +291,16 @@ const char *PkgNetField(int32_t h, const char *id, int32_t field) { snprintf(text, sizeof(text), "%lu", (unsigned long)e->size); break; + case PkgFieldSha256E: { + char hex[PKG_SHA256_TEXT]; + + pkgHexEncode(e->sha256, PKG_SHA256_SIZE, hex); + return basTextSet(&sText, hex); + } + + case PkgFieldRepoSignedE: + return basTextSet(&sText, e->repoSigned ? PKG_BAS_YES : PKG_BAS_NO); + default: break; } @@ -328,22 +339,36 @@ const char *PkgNetMineRows(int32_t h) { // names, with its account), fetching into the user folder's DOWNLOAD: 0 // when out of memory. int32_t PkgNetOpen(const char *server) { + return PkgNetOpenIn(server, "", ""); +} + + +// PkgNetOpen for the DVX folder root ("" = this DVX's: what is installed +// there decides the updates), fetching into downloadDir ("" = the user +// folder's DOWNLOAD). +int32_t PkgNetOpenIn(const char *server, const char *root, const char *downloadDir) { PkgNetConfigT c; char url[PKGNET_SERVER_MAX]; char dir[DVX_MAX_PATH]; PkgNetT *n; int32_t h; - if (!platformUserPath(dir, sizeof(dir), PKGNET_DIR)) { + if (downloadDir && downloadDir[0]) { + snprintf(dir, sizeof(dir), "%s", downloadDir); + } else if (!platformUserPath(dir, sizeof(dir), PKGNET_DIR)) { return BAS_NO_HANDLE; } + if (!root) { + root = ""; + } + if (server && server[0]) { - n = pkgNetCreate(server, "", dir); + n = pkgNetCreate(server, root, dir); } else { pkgNetConfigRead(&c); pkgNetConfigUrl(&c, url, sizeof(url)); - n = pkgNetCreate(url, "", dir); + n = pkgNetCreate(url, root, dir); pkgNetAccountSet(n, c.user, c.password); } diff --git a/src/libs/kpunch/pkgnet/pkgNetBas.h b/src/libs/kpunch/pkgnet/pkgNetBas.h index 2167d8b9..6894106f 100644 --- a/src/libs/kpunch/pkgnet/pkgNetBas.h +++ b/src/libs/kpunch/pkgnet/pkgNetBas.h @@ -44,6 +44,7 @@ const char *PkgNetField(int32_t h, const char *id, int32_t field); int32_t PkgNetMine(int32_t h); const char *PkgNetMineRows(int32_t h); int32_t PkgNetOpen(const char *server); +int32_t PkgNetOpenIn(const char *server, const char *root, const char *downloadDir); const char *PkgNetPassword(void); int32_t PkgNetPercent(int32_t h); int32_t PkgNetPort(void); diff --git a/src/libs/kpunch/pkgnet/pkgnet.dep b/src/libs/kpunch/pkgnet/pkgnet.dep index 447da455..cd605ebe 100644 --- a/src/libs/kpunch/pkgnet/pkgnet.dep +++ b/src/libs/kpunch/pkgnet/pkgnet.dep @@ -1,5 +1,5 @@ libdvx -serial +crypto pkg http json diff --git a/src/libs/kpunch/pkgnet/pkgnet.dhs b/src/libs/kpunch/pkgnet/pkgnet.dhs index e9e6dde7..9d925e76 100644 --- a/src/libs/kpunch/pkgnet/pkgnet.dhs +++ b/src/libs/kpunch/pkgnet/pkgnet.dhs @@ -38,7 +38,7 @@ Header: pkgnet/pkgNet.h Loaded as: bin/libs/kpunch/pkgnet/pkgnet.lib (it needs the package, HTTP, and JSON libraries) -BASIC: pkgnet.bas (PkgNetOpen, PkgNetCatalogue, PkgNetUpdates, PkgNetFetch, PkgNetRunFor, PkgNetRows$, ...), with pkg.bas's PkgOfferFolder to plan what was fetched. +BASIC: pkgnet.bas (PkgNetOpen, PkgNetOpenIn for another DVX folder or download folder, PkgNetCatalogue, PkgNetUpdates, PkgNetFetch, PkgNetRunFor, PkgNetRows$, PkgNetField$ -- PKG_FIELD_SHA256 and PKG_FIELD_REPO_SIGNED give a catalogue entry's sha256 and repoSigned --, ...), with pkg.bas's PkgOfferFolderStart to read what was fetched for planning. .h2 A Fetch diff --git a/src/libs/kpunch/print/Makefile b/src/libs/kpunch/print/Makefile index 0a3d74f0..de03e5a3 100644 --- a/src/libs/kpunch/print/Makefile +++ b/src/libs/kpunch/print/Makefile @@ -23,8 +23,11 @@ # Printing Library Makefile for DJGPP cross-compilation # # Printing library (print.lib): printer ports and jobs, text pages, -# picture drivers (ESC/P, PCL), the Print and Page Setup dialogs and the -# BASIC functions. See print.h. +# picture drivers (ESC/P, PCL, ImageWriter), the Print and Page Setup +# dialogs and the BASIC functions. See print.h. Every drivers// +# directory is a driver module built into drivers/.drv beside the +# library (the PostScript printers', drivers/ps: printPsMod.c loads it +# when one is first used). include ../../../common.mk @@ -38,9 +41,13 @@ OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) TARGETDIR = $(LIBSDIR)/kpunch/print TARGET = $(TARGETDIR)/print.lib +DRVDIR = $(TARGETDIR)/drivers +DRV_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(wildcard drivers/*/*.c)) +DRV_TARGETS = $(patsubst %,$(DRVDIR)/%.drv,$(DVX_DRIVERS)) + .PHONY: all clean -all: $(TARGET) $(TARGETDIR)/print.dep +all: $(TARGET) $(TARGETDIR)/print.dep $(DRV_TARGETS) $(TARGETDIR)/print.dep: print.dep | $(TARGETDIR) sed 's/$$/\r/' $< > $@ @@ -49,7 +56,10 @@ $(TARGET): $(OBJS) | $(TARGETDIR) $(DXE3GEN) -o $(TARGETDIR)/print.dxe -U $(OBJS) mv $(TARGETDIR)/print.dxe $@ +$(foreach d,$(DVX_DRIVERS),$(eval $(call DVX_DRIVER_RULE,$d))) + $(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR) + @mkdir -p $(dir $@) $(CC) $(CFLAGS) -c -o $@ $< $(OBJDIR): @@ -58,7 +68,10 @@ $(OBJDIR): $(TARGETDIR): mkdir -p $(TARGETDIR) --include $(OBJS:.o=.d) +$(DRVDIR): + mkdir -p $(DRVDIR) + +-include $(OBJS:.o=.d) $(DRV_OBJS:.o=.d) clean: - rm -rf $(OBJS) $(OBJS:.o=.d) $(TARGET) $(TARGETDIR)/print.dep $(TARGETDIR) + rm -rf $(OBJDIR) $(TARGETDIR) diff --git a/src/libs/kpunch/print/README.md b/src/libs/kpunch/print/README.md new file mode 100644 index 00000000..be044927 --- /dev/null +++ b/src/libs/kpunch/print/README.md @@ -0,0 +1,37 @@ +# print -- Printing + +Printing for every DVX program: jobs to a printer port or a file, +written a piece at a time so the other programs keep running, with Retry +/ Cancel when the printer is busy, out of paper, or offline; plain text +laid out on pages; and pictures and drawings (BASIC's Printer object) on +dot matrix (ESC/P, ImageWriter), LaserJet (PCL), and PostScript +printers. BASIC reaches it through LPRINT, the Printer object, and +src/include/basic/print.bas. + +## Documentation + +Full reference is in `print.dhs` (this directory), compiled into the DVX +System Reference. Open via the Help Viewer app, or read +`docs/dvx_system_reference.html` after `make`. + +## Source Files + +- `print.h`, `printInt.h` -- public and internal API +- `printJob.c` -- jobs, ports, the busy and error questions +- `printText.c` -- plain text on pages +- `printFont.c` -- text on a page at printer sizes +- `printRaster.c` -- documents of picture pages and the page pipeline + every printer driver is fed by +- `printEscp.c`, `printIw.c`, `printPcl.c` -- dot matrix (ESC/P), + ImageWriter, and LaserJet (PCL) output +- `drivers/ps/printPs.c`, `drivers/ps/printPsAfm.c` -- PostScript output + and its font metrics: the module `drivers/ps.drv`, loaded when a + PostScript printer is first used +- `printPsMod.c` -- loads that module; the calls into it +- `printUi.c` -- dialogs and ready-made printing (windows, pictures) +- `printSys.c` -- the printer ports (the BIOS printer service on DOS) +- `printBas.c` -- BASIC entry points (print.bas) + +## Build + +`make -C src/libs/kpunch/print` (invoked by the top-level `make`). diff --git a/src/libs/kpunch/print/printPs.c b/src/libs/kpunch/print/drivers/ps/printPs.c similarity index 90% rename from src/libs/kpunch/print/printPs.c rename to src/libs/kpunch/print/drivers/ps/printPs.c index 0d801f69..ca25a086 100644 --- a/src/libs/kpunch/print/printPs.c +++ b/src/libs/kpunch/print/drivers/ps/printPs.c @@ -22,6 +22,10 @@ // printPs.c -- Printing library: the PostScript drivers (see printInt.h) // +// With printPsAfm.c the module drivers/ps.drv, which print.lib loads the +// first time a PostScript printer is used (printPsMod.c): what the +// library calls is in the PrintPsModT it exports. +// // PostScript Level 2, pictures at 300 dpi: each band of a page an // imagemask of its black dots. // PostScript color each band a colorimage of 4 bits a dot (cyan, @@ -45,6 +49,7 @@ // (colour, width, dashes) and font are only sent when they change. #include "printInt.h" +#include "dvxEndian.h" #include "dvxFontFmt.h" #include "dvxText.h" @@ -200,22 +205,12 @@ static void colorRow(const PrintPlanesT *planes, int32_t r, uint8_t *out); static bool dashOn(uint16_t dash, int32_t i); static int32_t hatchFirst(int32_t lo); static bool nameHas(const char *name, const char *const *words, int32_t count); -double printPsAscent(const PrintFontT *font); -int32_t printPsFaceOf(const char *family, uint32_t style); int32_t printPsLatin1(uint8_t ch); -double printPsWidth(const PrintFontT *font, const char *text, int32_t len); -bool printVecBegin(PrintDocT *doc); -void printVecDot(PrintDocT *doc, int32_t x, int32_t y, uint32_t color, int32_t width); -void printVecEllipse(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, int32_t width, uint16_t dash, bool arc, double start, double end); -void printVecEllipseFill(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, PrintFillE fill); -bool printVecEnd(PrintDocT *doc); -void printVecImage(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh); -void printVecLine(PrintDocT *doc, double x0, double y0, double x1, double y1, uint32_t color, int32_t width, uint16_t dash); -void printVecRect(PrintDocT *doc, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t color, PrintFillE fill); -void printVecText(PrintDocT *doc, const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color); +static double psAscent(const PrintFontT *font); static bool psBand(const PrintDriverT *drv, PrintJobT *job, const PrintPlanesT *planes); static bool psDocBegin(const PrintDriverT *drv, PrintJobT *job); static bool psDocEnd(const PrintDriverT *drv, PrintJobT *job); +static int32_t psFaceOf(const char *family, uint32_t style); static bool psPageBegin(const PrintDriverT *drv, PrintJobT *job, int32_t widthDots, int32_t heightDots); static bool psPageEnd(const PrintDriverT *drv, PrintJobT *job); static bool psString(PrintJobT *job, const char *text, int32_t len); @@ -224,6 +219,16 @@ static bool psTextEnd(PrintJobT *job); static bool psTextLine(PrintJobT *job, const char *text, int32_t len); static bool psTextPageBegin(PrintJobT *job); static bool psTextPageEnd(PrintJobT *job); +static bool psVecBegin(PrintDocT *doc); +static void psVecDot(PrintDocT *doc, int32_t x, int32_t y, uint32_t color, int32_t width); +static void psVecEllipse(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, int32_t width, uint16_t dash, bool arc, double start, double end); +static void psVecEllipseFill(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, PrintFillE fill); +static bool psVecEnd(PrintDocT *doc); +static void psVecImage(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh); +static void psVecLine(PrintDocT *doc, double x0, double y0, double x1, double y1, uint32_t color, int32_t width, uint16_t dash); +static void psVecRect(PrintDocT *doc, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t color, PrintFillE fill); +static void psVecText(PrintDocT *doc, const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color); +static double psWidth(const PrintFontT *font, const char *text, int32_t len); static void vecColor(PrintDocT *doc, uint32_t color); static void vecDashes(PrintDocT *doc, uint16_t dash, double unit, double phase); static void vecFill(PrintDocT *doc, const char *shape, PrintFillE fill, int32_t x0, int32_t y0, int32_t x1, int32_t y1); @@ -234,18 +239,36 @@ static void vecPrintf(PrintDocT *doc, const char *fmt, ...) __attribute__((fo static const PrintTextOpsT sPsText = { psTextBegin, psTextPageBegin, psTextLine, psTextPageEnd, psTextEnd }; -const PrintDriverT printDrvPostScript = { +static const PrintDriverT sDrvMono = { PS_DPI, PS_DPI, PS_DPI * PRINT_PAGE_WIDTH_IN, PS_DPI * PRINT_PAGE_HEIGHT_IN, PS_BAND_ROWS, PRINT_DITHER_LARGE, 1, printInksBlack, "", "", PRINT_LINES_LASER, &sPsText, &sPsMono, psDocBegin, psPageBegin, psBand, psPageEnd, psDocEnd, true }; -const PrintDriverT printDrvPostScriptColor = { +static const PrintDriverT sDrvColor = { PS_DPI, PS_DPI, PS_DPI * PRINT_PAGE_WIDTH_IN, PS_DPI * PRINT_PAGE_HEIGHT_IN, PS_BAND_ROWS, PRINT_DITHER_LARGE, PRINT_PLANES_MAX, printInksKcmy, "", "", PRINT_LINES_LASER, &sPsText, &sPsColor, psDocBegin, psPageBegin, psBand, psPageEnd, psDocEnd, true }; +const PrintPsModT printPsModule = { + DVX_DRIVER_HDR(PrintPsModT, "ps", 0), + .mono = &sDrvMono, + .color = &sDrvColor, + .ascent = psAscent, + .faceOf = psFaceOf, + .width = psWidth, + .vecBegin = psVecBegin, + .vecDot = psVecDot, + .vecEllipse = psVecEllipse, + .vecEllipseFill = psVecEllipseFill, + .vecEnd = psVecEnd, + .vecImage = psVecImage, + .vecLine = psVecLine, + .vecRect = psVecRect, + .vecText = psVecText +}; + static void a85Byte(A85T *a, uint8_t b) { a->group[a->n++] = b; @@ -275,7 +298,7 @@ static bool a85End(A85T *a) { // The group as five characters (z for four zero bytes); a short group of // count bytes as count + 1 characters. static void a85Group(A85T *a, int32_t count) { - uint32_t v = ((uint32_t)a->group[0] << 24) | ((uint32_t)a->group[1] << 16) | ((uint32_t)a->group[2] << 8) | a->group[3]; + uint32_t v = dvxGetBe32(a->group); char c[A85_CHARS]; a->n = 0; @@ -375,32 +398,6 @@ static bool nameHas(const char *name, const char *const *words, int32_t count) { } -// The baseline below the top of a line: the ascender, and half the -// leading beyond the ascender and descender. -double printPsAscent(const PrintFontT *font) { - const PrintPsFaceT *face = &printPsFaces[font->psFace]; - int32_t extra = PRINT_PS_LEADING - (face->ascender - face->descender); - - return (face->ascender + extra / 2.0) * font->psSizeY / PRINT_PS_UNITS; -} - - -int32_t printPsFaceOf(const char *family, uint32_t style) { - const char *name = family && family[0] ? family : FONT_SYSTEM_FAMILY; - int32_t face = PRINT_PS_SANS; - - if (dvxFontFamilyFixed(name) || nameHas(name, sMonoWords, DVX_ARRAY_LEN(sMonoWords))) { - face = PRINT_PS_MONO; - } else if (nameHas(name, sSerifWords, DVX_ARRAY_LEN(sSerifWords)) && !nameHas(name, sSansWords, DVX_ARRAY_LEN(sSansWords))) { - face = PRINT_PS_SERIF; - } - - face += (style & FONT_STYLE_BOLD) ? PRINT_PS_BOLD : 0; - face += (style & FONT_STYLE_ITALIC) ? PRINT_PS_SLANT : 0; - return face; -} - - int32_t printPsLatin1(uint8_t ch) { uint32_t u = dfnCp437[ch]; @@ -428,243 +425,13 @@ int32_t printPsLatin1(uint8_t ch) { } -// Dots across len characters of text in a printer's face (not rounded). -double printPsWidth(const PrintFontT *font, const char *text, int32_t len) { - const PrintPsFaceT *face = &printPsFaces[font->psFace]; - int32_t sum = 0; - - for (int32_t i = 0; i < len; i++) { - sum += face->widths[printPsLatin1((uint8_t)text[i]) - PRINT_PS_FIRST_CHAR]; - } - - return sum * font->psSizeX / PRINT_PS_UNITS; -} - - -// The page started, clipped to the printable area, with round line ends -// and corners; the pen and font as a page starts. -bool printVecBegin(PrintDocT *doc) { - if (!doc || !doc->ok || !doc->drv->vector || doc->vecPage) { - return false; - } - - const PrintDriverT *drv = doc->drv; - - doc->ok = drv->pageBegin(drv, doc->job, drv->pageWidthDots, drv->pageHeightDots); - vecPrintf(doc, PS_VEC_PAGE_FMT, drv->pageWidthDots, drv->pageHeightDots); - doc->vecPage = doc->ok; - doc->vecColor = PS_BLACK; - doc->vecWidth = 1; - doc->vecDashed = false; - doc->vecFace = PRINT_PS_NONE; - return doc->vecPage; -} - - -void printVecDot(PrintDocT *doc, int32_t x, int32_t y, uint32_t color, int32_t width) { - if (!vecOn(doc)) { - return; - } - - vecColor(doc, color); - vecPrintf(doc, "%.6g %.6g %.6g dvxD\n", x + DOT_CENTRE, y + DOT_CENTRE, (width < 1 ? 1 : width) / 2.0); -} - - -// The outline through the dots' centres, dashed from angle 0 (an arc's -// path starts its start along the pattern); an ellipse with no width or -// height is the line it has become. -void printVecEllipse(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, int32_t width, uint16_t dash, bool arc, double start, double end) { - if (!vecOn(doc) || dash == DASH_NONE) { - return; - } - - if (rx <= 0 || ry <= 0) { - printVecLine(doc, cx - rx, cy - ry, cx + rx, cy + ry, color, width, dash); - return; - } - - vecColor(doc, color); - vecPen(doc, width, dash != DASH_SOLID); - - if (dash != DASH_SOLID) { - vecDashes(doc, dash, 1.0, arc ? start * (rx + ry) / 2.0 : 0.0); - } - - if (arc) { - vecPrintf(doc, "%.6g %.6g %" PRId32 " %" PRId32 " %.6g %.6g dvxEA stroke\n", cx + DOT_CENTRE, cy + DOT_CENTRE, rx, ry, start * DEGREES_PER_RADIAN, end * DEGREES_PER_RADIAN); - } else { - vecPrintf(doc, "%.6g %.6g %" PRId32 " %" PRId32 " dvxE stroke\n", cx + DOT_CENTRE, cy + DOT_CENTRE, rx, ry); - } -} - - -// Out to the edges of the dots rx and ry from the centre. -void printVecEllipseFill(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, PrintFillE fill) { - char shape[PS_VEC_CMD_MAX]; - - if (!vecOn(doc)) { - return; - } - - rx = rx < 0 ? -rx : rx; - ry = ry < 0 ? -ry : ry; - vecColor(doc, color); - snprintf(shape, sizeof(shape), "%.6g %.6g %.6g %.6g dvxE", cx + DOT_CENTRE, cy + DOT_CENTRE, rx + DOT_CENTRE, ry + DOT_CENTRE); - vecFill(doc, shape, fill, cx - rx, cy - ry, cx + rx, cy + ry); -} - - -bool printVecEnd(PrintDocT *doc) { - if (!doc || !doc->vecPage) { - return false; - } - - doc->vecPage = false; - doc->ok = doc->ok && doc->drv->pageEnd(doc->drv, doc->job) && !printJobCancelled(doc->job); - return doc->ok; -} - - -// The part of the picture as an image of its own dots, grey on a -// black-and-white printer, the rows packed and in ASCII85. -void printVecImage(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh) { - if (!vecOn(doc) || !rgb || sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0 || sx < 0 || sy < 0 || sx + sw > w || sy + sh > h) { - return; - } - - const PsParamsT *pp = (const PsParamsT *)doc->drv->params; - int32_t comps = pp->color ? RGB_BYTES : GRAY_BYTES; - int32_t rowLen = sw * comps; - uint8_t *row = (uint8_t *)malloc((size_t)rowLen); - uint8_t *packed = (uint8_t *)malloc((size_t)PRINT_PACK_MAX(rowLen)); - uint8_t eod = RLE_EOD; - A85T a; - - if (!row || !packed) { - free(row); - free(packed); - doc->ok = false; - return; - } - - memset(&a, 0, sizeof(a)); - a.job = doc->job; - a.ok = true; - vecPrintf(doc, "%" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " dvxImg\n", dx, dy, dw, dh, sw, sh, comps); - - for (int32_t r = 0; a.ok && doc->ok && r < sh; r++) { - const uint8_t *src = rgb + ((int64_t)(sy + r) * w + sx) * RGB_BYTES; - - if (pp->color) { - memcpy(row, src, (size_t)rowLen); - } else { - for (int32_t i = 0; i < sw; i++) { - row[i] = (uint8_t)printLuma(src + i * RGB_BYTES); - } - } - - a85Put(&a, packed, printPackBits(row, rowLen, packed)); - } - - a85Put(&a, &eod, 1); - doc->ok = doc->ok && a.ok && a85End(&a); - free(row); - free(packed); -} - - -// A solid line through the dots' centres, round at the ends; a dashed -// one in steps of its longer side in dots, a bit of dash each from the -// top one, each step centred on its dot (the ends half a step out, as -// dots), square at the ends. -void printVecLine(PrintDocT *doc, double x0, double y0, double x1, double y1, uint32_t color, int32_t width, uint16_t dash) { - if (!vecOn(doc) || dash == DASH_NONE || !(fabs(x0) <= DBL_MAX && fabs(y0) <= DBL_MAX && fabs(x1) <= DBL_MAX && fabs(y1) <= DBL_MAX)) { - return; - } - - x0 += DOT_CENTRE; - y0 += DOT_CENTRE; - x1 += DOT_CENTRE; - y1 += DOT_CENTRE; - vecColor(doc, color); - - if (dash == DASH_SOLID) { - vecPen(doc, width, false); - vecPrintf(doc, "%.6g %.6g %.6g %.6g dvxL\n", x0, y0, x1, y1); - return; - } - - double ax = fabs(floor(x1) - floor(x0)); - double ay = fabs(floor(y1) - floor(y0)); - double steps = ax > ay ? ax : ay; - - if (steps == 0.0) { - if (dash & DASH_TOP) { - printVecDot(doc, (int32_t)floor(x0), (int32_t)floor(y0), color, width); - } - - return; - } - - double len = sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)); - double unit = len / steps; - double ex = (x1 - x0) / len * unit * DOT_CENTRE; - double ey = (y1 - y0) / len * unit * DOT_CENTRE; - - vecPen(doc, width, true); - vecDashes(doc, dash, unit, 0.0); - vecPrintf(doc, "%.6g %.6g %.6g %.6g dvxL\n", x0 - ex, y0 - ey, x1 + ex, y1 + ey); -} - - -void printVecRect(PrintDocT *doc, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t color, PrintFillE fill) { - int32_t left = x0 < x1 ? x0 : x1; - int32_t right = x0 < x1 ? x1 : x0; - int32_t top = y0 < y1 ? y0 : y1; - int32_t bottom = y0 < y1 ? y1 : y0; - char shape[PS_VEC_CMD_MAX]; - - if (!vecOn(doc)) { - return; - } - - vecColor(doc, color); - snprintf(shape, sizeof(shape), "%" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " dvxBox", left, top, right - left + 1, bottom - top + 1); - vecFill(doc, shape, fill, left, top, right, bottom); -} - - -// The text at its baseline, then its underline and strike-through as -// the face has them (the strike-through half the x-height up). -void printVecText(PrintDocT *doc, const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color) { - if (!vecOn(doc) || !font || font->psFace == PRINT_PS_NONE || !text || len <= 0) { - return; - } - +// The baseline below the top of a line: the ascender, and half the +// leading beyond the ascender and descender. +static double psAscent(const PrintFontT *font) { const PrintPsFaceT *face = &printPsFaces[font->psFace]; - double base = y + printPsAscent(font); - double wide = printPsWidth(font, text, len); - double thick = face->underlineThick * font->psSizeY / PRINT_PS_UNITS; - double lines[TEXT_LINES_MAX]; - int32_t count = 0; + int32_t extra = PRINT_PS_LEADING - (face->ascender - face->descender); - vecColor(doc, color); - vecFont(doc, font); - doc->ok = doc->ok && psString(doc->job, text, len); - vecPrintf(doc, " %" PRId32 " %.6g dvxT\n", x, base); - - if (font->style & FONT_STYLE_UNDERLINE) { - lines[count++] = base - face->underlinePos * font->psSizeY / PRINT_PS_UNITS; - } - - if (font->style & FONT_STYLE_STRIKE) { - lines[count++] = base - face->xHeight / 2.0 * font->psSizeY / PRINT_PS_UNITS; - } - - for (int32_t i = 0; i < count; i++) { - vecPrintf(doc, "%" PRId32 " %.6g %.6g %.6g rectfill\n", x, lines[i] - thick / 2.0, wide, thick); - } + return (face->ascender + extra / 2.0) * font->psSizeY / PRINT_PS_UNITS; } @@ -726,6 +493,22 @@ static bool psDocEnd(const PrintDriverT *drv, PrintJobT *job) { } +static int32_t psFaceOf(const char *family, uint32_t style) { + const char *name = family && family[0] ? family : FONT_SYSTEM_FAMILY; + int32_t face = PRINT_PS_SANS; + + if (dvxFontFamilyFixed(name) || nameHas(name, sMonoWords, DVX_ARRAY_LEN(sMonoWords))) { + face = PRINT_PS_MONO; + } else if (nameHas(name, sSerifWords, DVX_ARRAY_LEN(sSerifWords)) && !nameHas(name, sSansWords, DVX_ARRAY_LEN(sSansWords))) { + face = PRINT_PS_SERIF; + } + + face += (style & FONT_STYLE_BOLD) ? PRINT_PS_BOLD : 0; + face += (style & FONT_STYLE_ITALIC) ? PRINT_PS_SLANT : 0; + return face; +} + + static bool psPageBegin(const PrintDriverT *drv, PrintJobT *job, int32_t widthDots, int32_t heightDots) { char cmd[PS_CMD_MAX]; @@ -807,6 +590,246 @@ static bool psTextPageEnd(PrintJobT *job) { } +// The page started, clipped to the printable area, with round line ends +// and corners; the pen and font as a page starts. +static bool psVecBegin(PrintDocT *doc) { + if (!doc || !doc->ok || !doc->drv->vector || doc->vecPage) { + return false; + } + + const PrintDriverT *drv = doc->drv; + + doc->ok = drv->pageBegin(drv, doc->job, drv->pageWidthDots, drv->pageHeightDots); + vecPrintf(doc, PS_VEC_PAGE_FMT, drv->pageWidthDots, drv->pageHeightDots); + doc->vecPage = doc->ok; + doc->vecColor = PS_BLACK; + doc->vecWidth = 1; + doc->vecDashed = false; + doc->vecFace = PRINT_PS_NONE; + return doc->vecPage; +} + + +static void psVecDot(PrintDocT *doc, int32_t x, int32_t y, uint32_t color, int32_t width) { + if (!vecOn(doc)) { + return; + } + + vecColor(doc, color); + vecPrintf(doc, "%.6g %.6g %.6g dvxD\n", x + DOT_CENTRE, y + DOT_CENTRE, (width < 1 ? 1 : width) / 2.0); +} + + +// The outline through the dots' centres, dashed from angle 0 (an arc's +// path starts its start along the pattern); an ellipse with no width or +// height is the line it has become. +static void psVecEllipse(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, int32_t width, uint16_t dash, bool arc, double start, double end) { + if (!vecOn(doc) || dash == DASH_NONE) { + return; + } + + if (rx <= 0 || ry <= 0) { + psVecLine(doc, cx - rx, cy - ry, cx + rx, cy + ry, color, width, dash); + return; + } + + vecColor(doc, color); + vecPen(doc, width, dash != DASH_SOLID); + + if (dash != DASH_SOLID) { + vecDashes(doc, dash, 1.0, arc ? start * (rx + ry) / 2.0 : 0.0); + } + + if (arc) { + vecPrintf(doc, "%.6g %.6g %" PRId32 " %" PRId32 " %.6g %.6g dvxEA stroke\n", cx + DOT_CENTRE, cy + DOT_CENTRE, rx, ry, start * DEGREES_PER_RADIAN, end * DEGREES_PER_RADIAN); + } else { + vecPrintf(doc, "%.6g %.6g %" PRId32 " %" PRId32 " dvxE stroke\n", cx + DOT_CENTRE, cy + DOT_CENTRE, rx, ry); + } +} + + +// Out to the edges of the dots rx and ry from the centre. +static void psVecEllipseFill(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, PrintFillE fill) { + char shape[PS_VEC_CMD_MAX]; + + if (!vecOn(doc)) { + return; + } + + rx = rx < 0 ? -rx : rx; + ry = ry < 0 ? -ry : ry; + vecColor(doc, color); + snprintf(shape, sizeof(shape), "%.6g %.6g %.6g %.6g dvxE", cx + DOT_CENTRE, cy + DOT_CENTRE, rx + DOT_CENTRE, ry + DOT_CENTRE); + vecFill(doc, shape, fill, cx - rx, cy - ry, cx + rx, cy + ry); +} + + +static bool psVecEnd(PrintDocT *doc) { + if (!doc || !doc->vecPage) { + return false; + } + + doc->vecPage = false; + doc->ok = doc->ok && doc->drv->pageEnd(doc->drv, doc->job) && !printJobCancelled(doc->job); + return doc->ok; +} + + +// The part of the picture as an image of its own dots, grey on a +// black-and-white printer, the rows packed and in ASCII85. +static void psVecImage(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh) { + if (!vecOn(doc) || !rgb || sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0 || sx < 0 || sy < 0 || sx + sw > w || sy + sh > h) { + return; + } + + const PsParamsT *pp = (const PsParamsT *)doc->drv->params; + int32_t comps = pp->color ? RGB_BYTES : GRAY_BYTES; + int32_t rowLen = sw * comps; + uint8_t *row = (uint8_t *)malloc((size_t)rowLen); + uint8_t *packed = (uint8_t *)malloc((size_t)PRINT_PACK_MAX(rowLen)); + uint8_t eod = RLE_EOD; + A85T a; + + if (!row || !packed) { + free(row); + free(packed); + doc->ok = false; + return; + } + + memset(&a, 0, sizeof(a)); + a.job = doc->job; + a.ok = true; + vecPrintf(doc, "%" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " dvxImg\n", dx, dy, dw, dh, sw, sh, comps); + + for (int32_t r = 0; a.ok && doc->ok && r < sh; r++) { + const uint8_t *src = rgb + ((int64_t)(sy + r) * w + sx) * RGB_BYTES; + + if (pp->color) { + memcpy(row, src, (size_t)rowLen); + } else { + for (int32_t i = 0; i < sw; i++) { + row[i] = (uint8_t)printLuma(src + i * RGB_BYTES); + } + } + + a85Put(&a, packed, printPackBits(row, rowLen, packed)); + } + + a85Put(&a, &eod, 1); + doc->ok = doc->ok && a.ok && a85End(&a); + free(row); + free(packed); +} + + +// A solid line through the dots' centres, round at the ends; a dashed +// one in steps of its longer side in dots, a bit of dash each from the +// top one, each step centred on its dot (the ends half a step out, as +// dots), square at the ends. +static void psVecLine(PrintDocT *doc, double x0, double y0, double x1, double y1, uint32_t color, int32_t width, uint16_t dash) { + if (!vecOn(doc) || dash == DASH_NONE || !(fabs(x0) <= DBL_MAX && fabs(y0) <= DBL_MAX && fabs(x1) <= DBL_MAX && fabs(y1) <= DBL_MAX)) { + return; + } + + x0 += DOT_CENTRE; + y0 += DOT_CENTRE; + x1 += DOT_CENTRE; + y1 += DOT_CENTRE; + vecColor(doc, color); + + if (dash == DASH_SOLID) { + vecPen(doc, width, false); + vecPrintf(doc, "%.6g %.6g %.6g %.6g dvxL\n", x0, y0, x1, y1); + return; + } + + double ax = fabs(floor(x1) - floor(x0)); + double ay = fabs(floor(y1) - floor(y0)); + double steps = ax > ay ? ax : ay; + + if (steps == 0.0) { + if (dash & DASH_TOP) { + psVecDot(doc, (int32_t)floor(x0), (int32_t)floor(y0), color, width); + } + + return; + } + + double len = sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)); + double unit = len / steps; + double ex = (x1 - x0) / len * unit * DOT_CENTRE; + double ey = (y1 - y0) / len * unit * DOT_CENTRE; + + vecPen(doc, width, true); + vecDashes(doc, dash, unit, 0.0); + vecPrintf(doc, "%.6g %.6g %.6g %.6g dvxL\n", x0 - ex, y0 - ey, x1 + ex, y1 + ey); +} + + +static void psVecRect(PrintDocT *doc, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t color, PrintFillE fill) { + int32_t left = x0 < x1 ? x0 : x1; + int32_t right = x0 < x1 ? x1 : x0; + int32_t top = y0 < y1 ? y0 : y1; + int32_t bottom = y0 < y1 ? y1 : y0; + char shape[PS_VEC_CMD_MAX]; + + if (!vecOn(doc)) { + return; + } + + vecColor(doc, color); + snprintf(shape, sizeof(shape), "%" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " dvxBox", left, top, right - left + 1, bottom - top + 1); + vecFill(doc, shape, fill, left, top, right, bottom); +} + + +// The text at its baseline, then its underline and strike-through as +// the face has them (the strike-through half the x-height up). +static void psVecText(PrintDocT *doc, const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color) { + if (!vecOn(doc) || !font || font->psFace == PRINT_PS_NONE || !text || len <= 0) { + return; + } + + const PrintPsFaceT *face = &printPsFaces[font->psFace]; + double base = y + psAscent(font); + double wide = psWidth(font, text, len); + double thick = face->underlineThick * font->psSizeY / PRINT_PS_UNITS; + double lines[TEXT_LINES_MAX]; + int32_t count = 0; + + vecColor(doc, color); + vecFont(doc, font); + doc->ok = doc->ok && psString(doc->job, text, len); + vecPrintf(doc, " %" PRId32 " %.6g dvxT\n", x, base); + + if (font->style & FONT_STYLE_UNDERLINE) { + lines[count++] = base - face->underlinePos * font->psSizeY / PRINT_PS_UNITS; + } + + if (font->style & FONT_STYLE_STRIKE) { + lines[count++] = base - face->xHeight / 2.0 * font->psSizeY / PRINT_PS_UNITS; + } + + for (int32_t i = 0; i < count; i++) { + vecPrintf(doc, "%" PRId32 " %.6g %.6g %.6g rectfill\n", x, lines[i] - thick / 2.0, wide, thick); + } +} + + +// Dots across len characters of text in a printer's face (not rounded). +static double psWidth(const PrintFontT *font, const char *text, int32_t len) { + const PrintPsFaceT *face = &printPsFaces[font->psFace]; + int32_t sum = 0; + + for (int32_t i = 0; i < len; i++) { + sum += face->widths[printPsLatin1((uint8_t)text[i]) - PRINT_PS_FIRST_CHAR]; + } + + return sum * font->psSizeX / PRINT_PS_UNITS; +} + + // The colour, sent when it changed: grey (its brightness) on a // black-and-white printer. static void vecColor(PrintDocT *doc, uint32_t color) { diff --git a/src/libs/kpunch/print/printPsAfm.c b/src/libs/kpunch/print/drivers/ps/printPsAfm.c similarity index 94% rename from src/libs/kpunch/print/printPsAfm.c rename to src/libs/kpunch/print/drivers/ps/printPsAfm.c index fd429326..7d3273f5 100644 --- a/src/libs/kpunch/print/printPsAfm.c +++ b/src/libs/kpunch/print/drivers/ps/printPsAfm.c @@ -1,3 +1,25 @@ +// 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. + // printPsAfm.c -- Printing library: the widths of the PostScript // printers' own text faces (printInt.h), made by src/tools/afmWidths.py // -- do not edit. diff --git a/src/libs/kpunch/print/print.dhs b/src/libs/kpunch/print/print.dhs index 7442bff7..f6f4a44a 100644 --- a/src/libs/kpunch/print/print.dhs +++ b/src/libs/kpunch/print/print.dhs @@ -51,7 +51,7 @@ Printer ports and print jobs, text laid out on pages, pictures for dot-matrix an Header: print/print.h -Loaded as: bin/libs/kpunch/print/print.lib +Loaded as: bin/libs/kpunch/print/print.lib; the PostScript printers' driver, with their text faces and vector pages, is bin/libs/kpunch/print/drivers/ps.drv, which the library loads the first time a PostScript printer is used (until then printVecBegin is false for every document). .h2 Settings diff --git a/src/libs/kpunch/print/print.h b/src/libs/kpunch/print/print.h index 96abefe9..4c04ae8d 100644 --- a/src/libs/kpunch/print/print.h +++ b/src/libs/kpunch/print/print.h @@ -98,6 +98,9 @@ // Bytes sent to a port between two calls of the idle hook #define PRINT_YIELD_BYTES 256 +// The longest a job's work goes without the idle hook (printJobPoll) +#define PRINT_POLL_MS 50 + // Page setup limits and defaults. linesPerPage 0 means the printer // model's usual page length (PRINT_LINES_STANDARD, or PRINT_LINES_LASER // for a laser printer). @@ -374,6 +377,12 @@ bool printJobPuts(PrintJobT *job, const char *text); // True once the job was cancelled. bool printJobCancelled(const PrintJobT *job); +// The idle hook (other programs run, Cancel is seen) once PRINT_POLL_MS +// has passed since it last ran: work that sends little -- a white band, a +// page laid out, a picture converted -- calls this as it goes. False when +// the job is cancelled. +bool printJobPoll(PrintJobT *job); + // Bytes sent so far. uint32_t printJobBytes(const PrintJobT *job); diff --git a/src/libs/kpunch/print/printFont.c b/src/libs/kpunch/print/printFont.c index adf7afbb..06060951 100644 --- a/src/libs/kpunch/print/printFont.c +++ b/src/libs/kpunch/print/printFont.c @@ -61,6 +61,8 @@ #define ROUND_HALF 0.5 #define FIX_ONE 65536 // glyphSmoothSpans: strike positions in 16.16 fixed point #define FIX_HALF (FIX_ONE / 2) +#define LEVELS_PAD 2 // glyphRowLevels: blank columns each side of a strike row +#define LEVELS_MAX (UINT8_MAX + 2 * LEVELS_PAD) // One glyph as a bitmap: the system font's cells and a loaded font's // glyphs alike @@ -92,6 +94,7 @@ static int64_t fixFloor(int64_t v); static bool glyphInk(const GlyphT *g, const uint8_t *row, int32_t col); static int32_t glyphLevel(const GlyphT *g, int32_t col, int32_t row); static bool glyphOf(const BitmapFontT *font, uint8_t ch, GlyphT *g); +static void glyphRowLevels(const GlyphT *g, int32_t row, uint8_t *out); static void glyphSmoothSpans(const PrintFontT *pf, const GlyphT *g, int32_t x, int32_t y, int32_t pen, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx); static void glyphSpans(const PrintFontT *pf, const GlyphT *g, int32_t x, int32_t y, int32_t pen, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx); static void lineSpans(const PrintFontT *pf, int32_t x, int32_t y, int32_t row, int32_t thick, int32_t width, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx); @@ -137,13 +140,9 @@ static bool glyphInk(const GlyphT *g, const uint8_t *row, int32_t col) { } -// How much ink the glyph pixel at col, row holds, 0 .. FONT_GRAY_FULL (a -// 1-bit glyph's ink is full); 0 outside the glyph. +// How much ink the glyph pixel at col, row (inside the glyph) holds, +// 0 .. FONT_GRAY_FULL (a 1-bit glyph's ink is full). static int32_t glyphLevel(const GlyphT *g, int32_t col, int32_t row) { - if (col < 0 || row < 0 || col >= g->width || row >= g->height) { - return 0; - } - const uint8_t *bits = g->bits + row * g->rowBytes; if (g->gray) { @@ -189,11 +188,26 @@ static bool glyphOf(const BitmapFontT *font, uint8_t ch, GlyphT *g) { } +// Strike row row's ink levels: out[c + LEVELS_PAD] is column c's, the +// pad columns each side (and every column of a row outside the glyph) +// none. +static void glyphRowLevels(const GlyphT *g, int32_t row, uint8_t *out) { + memset(out, 0, (size_t)g->width + 2 * LEVELS_PAD); + + for (int32_t c = 0; row >= 0 && row < g->height && c < g->width; c++) { + out[c + LEVELS_PAD] = (uint8_t)glyphLevel(g, c, row); + } +} + + // A glyph scaled up both ways, as glyphSpans: each dot's centre taken // back to the strike, its ink the four pixels around it weighed by // nearness (bilinear), a dot at least half covered drawn. The strike -// position of a row is worked out once; across, it steps by the strike -// pixels a dot covers. +// position of a row is worked out once, with the two strike rows' levels +// (glyphRowLevels: no bounds checks per dot); across, it steps by the +// strike pixels a dot covers. The weighing is done a row at a time, +// (1 - fy) * row r's blend + fy * row r + 1's: the same sum, exactly, in +// fewer and narrower multiplies. static void glyphSmoothSpans(const PrintFontT *pf, const GlyphT *g, int32_t x, int32_t y, int32_t pen, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx) { int32_t rowTop = pf->font->ascent - g->top; int32_t colLeft = pen + g->left; @@ -212,15 +226,23 @@ static void glyphSmoothSpans(const PrintFontT *pf, const GlyphT *g, int32_t x, i int64_t v = (int64_t)(2 * (dy - y) + 1) * pf->size * FIX_ONE / (2 * pf->dotsY) - (int64_t)rowTop * FIX_ONE - FIX_HALF; int64_t vBase = fixFloor(v); int32_t r = (int32_t)(vBase / FIX_ONE); - int64_t fy = v - vBase; + int32_t fy = (int32_t)(v - vBase); int64_t u = startU; int32_t run = -1; + uint8_t lv0[LEVELS_MAX]; + uint8_t lv1[LEVELS_MAX]; + + glyphRowLevels(g, r, lv0); + glyphRowLevels(g, r + 1, lv1); for (int32_t dx = x0; dx <= x1; dx++, u += stepX) { int64_t uBase = fixFloor(u); int32_t c = (int32_t)(uBase / FIX_ONE); - int64_t fx = u - uBase; - int64_t ink = (FIX_ONE - fx) * (FIX_ONE - fy) * glyphLevel(g, c, r) + fx * (FIX_ONE - fy) * glyphLevel(g, c + 1, r) + (FIX_ONE - fx) * fy * glyphLevel(g, c, r + 1) + fx * fy * glyphLevel(g, c + 1, r + 1); + int32_t fx = (int32_t)(u - uBase); + int32_t i = (c < -LEVELS_PAD ? -LEVELS_PAD : (c > g->width ? g->width : c)) + LEVELS_PAD; + int32_t inkR0 = (FIX_ONE - fx) * lv0[i] + fx * lv0[i + 1]; + int32_t inkR1 = (FIX_ONE - fx) * lv1[i] + fx * lv1[i + 1]; + int64_t ink = (int64_t)(FIX_ONE - fy) * inkR0 + (int64_t)fy * inkR1; bool on = 2 * ink >= full; if (on && run < 0) { @@ -361,10 +383,10 @@ PrintFontT *printFontOpen(const PrintPageInfoT *page, const char *family, int32_ pf->dotsX = pf->dotsX < 1 ? 1 : pf->dotsX; pf->dotsY = pf->dotsY < 1 ? 1 : pf->dotsY; pf->style = style; - pf->psFace = PRINT_PS_NONE; + pf->psFace = page->vector ? printPsFaceOf(family, style) : PRINT_PS_NONE; - if (page->vector) { - pf->psFace = printPsFaceOf(family, style); + // A printer's face (none without the PostScript module: a strike then) + if (pf->psFace != PRINT_PS_NONE) { pf->psSizeX = (double)decipoints * page->dpiX / DECIPOINTS_PER_INCH; pf->psSizeY = (double)decipoints * page->dpiY / DECIPOINTS_PER_INCH; return pf; diff --git a/src/libs/kpunch/print/printInt.h b/src/libs/kpunch/print/printInt.h index 7293a829..608308f8 100644 --- a/src/libs/kpunch/print/printInt.h +++ b/src/libs/kpunch/print/printInt.h @@ -26,6 +26,8 @@ #define PRINT_INT_H #include "print.h" +#include "dvxDrv.h" +#include "dvxPlat.h" // Port 0 is called LPT1 #define PRINT_PORT_NUMBER_BASE 1 @@ -181,8 +183,8 @@ extern const PrintPsFaceT printPsFaces[PRINT_PS_FACES]; // The face of printPsFaces a family and style (FONT_STYLE_BOLD, _ITALIC) // print in: Courier for a fixed-pitch family (or one named Mono or -// Courier), Times for a serif one (its name says so), else Helvetica -// (printPs.c). +// Courier), Times for a serif one (its name says so), else Helvetica; +// PRINT_PS_NONE without the PostScript module (printPsMod.c). int32_t printPsFaceOf(const char *family, uint32_t style); // A CP437 character as ISOLatin1Encoding has it: Latin-1 letters and @@ -192,10 +194,41 @@ int32_t printPsLatin1(uint8_t ch); // A printer's face (PrintFontT.psFace): dots from the top of a line to // its baseline, and across len characters of text, not rounded -// (printPs.c). +// (printPsMod.c). double printPsAscent(const PrintFontT *font); double printPsWidth(const PrintFontT *font, const char *text, int32_t len); +// The PostScript printers' module, drivers/ps.drv (printPs.c and +// printPsAfm.c, a driver module: dvxDrv.h), loaded by printPsMod.c the +// first time a PostScript printer is used and kept until the library is +// unloaded. It exports a PrintPsModT as PRINT_PS_SYMBOL: its two +// drivers and the functions behind printPsAscent, printPsFaceOf, +// printPsWidth, and the printVec* calls. +#define PRINT_DRV_DIR "libs" DVX_PATH_SEP "kpunch" DVX_PATH_SEP "print" DVX_PATH_SEP "drivers" +#define PRINT_PS_SYMBOL "printPsModule" + +typedef struct { + DvxDriverHeaderT header; + const PrintDriverT *mono; + const PrintDriverT *color; + __typeof__(printPsAscent) *ascent; + __typeof__(printPsFaceOf) *faceOf; + __typeof__(printPsWidth) *width; + __typeof__(printVecBegin) *vecBegin; + __typeof__(printVecDot) *vecDot; + __typeof__(printVecEllipse) *vecEllipse; + __typeof__(printVecEllipseFill) *vecEllipseFill; + __typeof__(printVecEnd) *vecEnd; + __typeof__(printVecImage) *vecImage; + __typeof__(printVecLine) *vecLine; + __typeof__(printVecRect) *vecRect; + __typeof__(printVecText) *vecText; +} PrintPsModT; + +// A PostScript model's driver (color: the colour one), the module loaded +// if it is not; NULL without it (logged). +const PrintDriverT *printDriverPs(bool color); + // The one ink of a black-and-white printer, and the four of a printer // with black (printRaster.c) extern const PrintInkE printInksBlack[1]; @@ -212,10 +245,9 @@ extern const PrintDriverT printDrvImageWriter; extern const PrintDriverT printDrvImageWriterColor; extern const PrintDriverT printDrvLaserJet; extern const PrintDriverT printDrvLaserJet300; -extern const PrintDriverT printDrvPostScript; -extern const PrintDriverT printDrvPostScriptColor; -// The picture driver of a model, NULL for PrintModelTextE. +// The picture driver of a model, NULL for PrintModelTextE (and for a +// PostScript one without its module, which then prints as text). const PrintDriverT *printDriver(PrintModelE model); // One page through drv, widthDots x heightDots: fn fills each band in RGB diff --git a/src/libs/kpunch/print/printJob.c b/src/libs/kpunch/print/printJob.c index d80d36fa..470ae79e 100644 --- a/src/libs/kpunch/print/printJob.c +++ b/src/libs/kpunch/print/printJob.c @@ -63,15 +63,16 @@ DVX_ABI_STAMP(print); #define PRINT_FILE_YIELD_BYTES 4096 struct PrintJobS { - PrintHooksT hooks; - FILE *fp; // file destination, else NULL - int32_t port; // port destination - uint32_t bytes; - int32_t sinceIdle; // bytes since the idle hook ran - int32_t idleEvery; - bool cancelled; - bool idled; // the idle hook ran during this write - char where[PRINT_WHERE_MAX]; + PrintHooksT hooks; + FILE *fp; // file destination, else NULL + int32_t port; // port destination + uint32_t bytes; + int32_t sinceIdle; // bytes since the idle hook ran + int32_t idleEvery; + PlatformTicksT idleAt; // when the idle hook last ran (printJobPoll) + bool cancelled; + bool idled; // the idle hook ran during this write + char where[PRINT_WHERE_MAX]; }; const PrefsChoiceT printDestChoices[PrintDestCountE] = { @@ -132,6 +133,7 @@ uint32_t printJobBytes(const PrintJobT *job); bool printJobCancelled(const PrintJobT *job); bool printJobClose(PrintJobT *job); PrintJobT *printJobOpen(PrintDestE dest, const char *filePath, const PrintHooksT *hooks, char *err, int32_t errSize); +bool printJobPoll(PrintJobT *job); bool printJobPuts(PrintJobT *job, const char *text); bool printJobWrite(PrintJobT *job, const void *data, int32_t len); const char *printModelLabel(PrintModelE model); @@ -181,6 +183,7 @@ static int32_t baudOffered(int32_t baud) { static bool idle(PrintJobT *job) { job->idled = true; job->sinceIdle = 0; + job->idleAt = platformClock(); if (!job->hooks.idle) { tsYield(); @@ -321,6 +324,7 @@ PrintJobT *printJobOpen(PrintDestE dest, const char *filePath, const PrintHooksT job->hooks = *hooks; } + job->idleAt = platformClock(); printDestName(dest, filePath, job->where, sizeof(job->where)); if (dest == PrintDestFileE) { @@ -332,6 +336,10 @@ PrintJobT *printJobOpen(PrintDestE dest, const char *filePath, const PrintHooksT free(job); return NULL; } + + // Flushed in pieces of the idle cadence: the C library's 16 KB + // writes take most of a second on a floppy + setvbuf(job->fp, NULL, _IOFBF, PRINT_FILE_YIELD_BYTES); } else { job->port = (int32_t)dest; job->idleEvery = PRINT_YIELD_BYTES; @@ -357,6 +365,15 @@ PrintJobT *printJobOpen(PrintDestE dest, const char *filePath, const PrintHooksT } +bool printJobPoll(PrintJobT *job) { + if (!job || job->cancelled) { + return false; + } + + return platformClock() - job->idleAt < PLATFORM_MS_TO_TICKS(PRINT_POLL_MS) || idle(job); +} + + bool printJobPuts(PrintJobT *job, const char *text) { return printJobWrite(job, text, (int32_t)strlen(text)); } @@ -382,6 +399,8 @@ bool printJobWrite(PrintJobT *job, const void *data, int32_t len) { } } + ok = ok && printJobPoll(job); + if (job->idled && job->hooks.finished) { job->hooks.finished(job->hooks.ctx, false); } diff --git a/src/libs/kpunch/print/printPsMod.c b/src/libs/kpunch/print/printPsMod.c new file mode 100644 index 00000000..de98e2dc --- /dev/null +++ b/src/libs/kpunch/print/printPsMod.c @@ -0,0 +1,202 @@ +// 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. + +// printPsMod.c -- Printing library: the PostScript printers' module +// +// The PostScript drivers, their text faces, and vector pages (printPs.c, +// printPsAfm.c) are a module of their own, drivers/ps.drv in +// PRINT_DRV_DIR, that exports one PrintPsModT (printInt.h). The first +// PostScript printer used loads it (printDriverPs, or printPsFaceOf for +// a vector page's fonts), and it stays until the library is unloaded: +// the drivers it hands out are its own. The library's and programs' +// calls are here, through it. A printer face or a vector page exists +// only on a PostScript document, which the module made, so printPsAscent +// and printPsWidth need no check; printVec* may be called on any +// document and do nothing (false) without the module. A build that +// links printPs.c in (the host tests) has the descriptor already. + +#include "printInt.h" +#include "dvxApp.h" +#include "stb_ds_wrap.h" + +#include + +// ============================================================ +// Prototypes +// ============================================================ + +static bool driverValid(const DvxDriverHeaderT *desc, int32_t kind); +const PrintDriverT *printDriverPs(bool color); +double printPsAscent(const PrintFontT *font); +int32_t printPsFaceOf(const char *family, uint32_t style); +static const PrintPsModT *printPsMod(void); +static void printPsModUnload(void) __attribute__((destructor)); +double printPsWidth(const PrintFontT *font, const char *text, int32_t len); +bool printVecBegin(PrintDocT *doc); +void printVecDot(PrintDocT *doc, int32_t x, int32_t y, uint32_t color, int32_t width); +void printVecEllipse(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, int32_t width, uint16_t dash, bool arc, double start, double end); +void printVecEllipseFill(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, PrintFillE fill); +bool printVecEnd(PrintDocT *doc); +void printVecImage(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh); +void printVecLine(PrintDocT *doc, double x0, double y0, double x1, double y1, uint32_t color, int32_t width, uint16_t dash); +void printVecRect(PrintDocT *doc, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t color, PrintFillE fill); +void printVecText(PrintDocT *doc, const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color); + +static const char *const sDrvSymbols[] = { DVX_SYM(PRINT_PS_SYMBOL) }; +static const uint32_t sDrvSizes[] = { sizeof(PrintPsModT) }; +static const DvxDriverClassT sDrvClass = { sDrvSymbols, sDrvSizes, 1, driverValid }; + +static DvxDriverModT sMod = { NULL, NULL, 0 }; // desc NULL: not loaded +static const PrintPsModT *sPs = NULL; // sMod's descriptor + + +static bool driverValid(const DvxDriverHeaderT *desc, int32_t kind) { + (void)kind; + + const PrintPsModT *m = (const PrintPsModT *)desc; + + return m->mono && m->color && m->ascent && m->faceOf && m->width && m->vecBegin && m->vecDot && m->vecEllipse && m->vecEllipseFill && m->vecEnd && m->vecImage && m->vecLine && m->vecRect && m->vecText; +} + + +const PrintDriverT *printDriverPs(bool color) { + const PrintPsModT *ps = printPsMod(); + + if (!ps) { + return NULL; + } + + return color ? ps->color : ps->mono; +} + + +double printPsAscent(const PrintFontT *font) { + return sPs->ascent(font); +} + + +int32_t printPsFaceOf(const char *family, uint32_t style) { + const PrintPsModT *ps = printPsMod(); + + return ps ? ps->faceOf(family, style) : PRINT_PS_NONE; +} + + +// The module, loaded if it is not (NULL: it is not there, logged). +static const PrintPsModT *printPsMod(void) { + if (sPs) { + return sPs; + } + + const DvxDriverHeaderT *linked = (const DvxDriverHeaderT *)dlsym(NULL, sDrvSymbols[0]); + + if (linked) { + sMod.desc = linked; + } else { + DvxDriverModT *mods = dvxDriversLoad(PRINT_DRV_DIR, &sDrvClass); + + if (arrlen(mods) > 0) { + sMod = dvxDriverKeep(mods, 0); + } + + dvxDriversFree(mods); + } + + sPs = (const PrintPsModT *)sMod.desc; + + if (!sPs) { + dvxLog("Print: the PostScript driver is missing from %s", PRINT_DRV_DIR); + } + + return sPs; +} + + +// The library is being unloaded (a linked-in module stays). +static void printPsModUnload(void) { + dvxDriverUnload(&sMod); + sPs = NULL; +} + + +double printPsWidth(const PrintFontT *font, const char *text, int32_t len) { + return sPs->width(font, text, len); +} + + +bool printVecBegin(PrintDocT *doc) { + return sPs && sPs->vecBegin(doc); +} + + +void printVecDot(PrintDocT *doc, int32_t x, int32_t y, uint32_t color, int32_t width) { + if (sPs) { + sPs->vecDot(doc, x, y, color, width); + } +} + + +void printVecEllipse(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, int32_t width, uint16_t dash, bool arc, double start, double end) { + if (sPs) { + sPs->vecEllipse(doc, cx, cy, rx, ry, color, width, dash, arc, start, end); + } +} + + +void printVecEllipseFill(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, PrintFillE fill) { + if (sPs) { + sPs->vecEllipseFill(doc, cx, cy, rx, ry, color, fill); + } +} + + +bool printVecEnd(PrintDocT *doc) { + return sPs && sPs->vecEnd(doc); +} + + +void printVecImage(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh) { + if (sPs) { + sPs->vecImage(doc, rgb, w, h, sx, sy, sw, sh, dx, dy, dw, dh); + } +} + + +void printVecLine(PrintDocT *doc, double x0, double y0, double x1, double y1, uint32_t color, int32_t width, uint16_t dash) { + if (sPs) { + sPs->vecLine(doc, x0, y0, x1, y1, color, width, dash); + } +} + + +void printVecRect(PrintDocT *doc, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t color, PrintFillE fill) { + if (sPs) { + sPs->vecRect(doc, x0, y0, x1, y1, color, fill); + } +} + + +void printVecText(PrintDocT *doc, const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color) { + if (sPs) { + sPs->vecText(doc, font, x, y, text, len, color); + } +} diff --git a/src/libs/kpunch/print/printRaster.c b/src/libs/kpunch/print/printRaster.c index d5c00429..37fc659b 100644 --- a/src/libs/kpunch/print/printRaster.c +++ b/src/libs/kpunch/print/printRaster.c @@ -55,6 +55,7 @@ #define TOP_BIT 0x80 #define RGB_BYTES 3 #define WHITE 0xFF +#define PRINT_SUB_ROWS 8 // rows filled and dithered between two polls #define BRIGHTNESS_LEVELS 256 // Luma weights (ITU-R BT.601, per mille) @@ -122,7 +123,7 @@ static const uint8_t sPlaneShift[PRINT_PLANES_MAX][2] = { // Prototypes // ============================================================ -static void bandToPlanes(const PrintDriverT *drv, InkModeE mode, const uint8_t *rgb, int32_t y0, PrintPlanesT *pl); +static void bandToPlanes(const PrintDriverT *drv, InkModeE mode, const uint8_t *rgb, int32_t y0, PrintPlanesT *pl, int32_t rowFrom, int32_t rowTo); static InkModeE inkModeOf(const PrintDriverT *drv); static int32_t inkValue(InkModeE mode, PrintInkE ink, const uint8_t *px); static bool pictureBand(void *ctx, uint8_t *rgb, int32_t width, int32_t y0, int32_t rows); @@ -141,13 +142,13 @@ bool printPlaneDot(const PrintPlanesT *pl, int32_t plane, int32_t static int32_t threshold(int32_t size, int32_t plane, int32_t x, int32_t y); -// The band's colours as the driver's inks, dithered (the planes come -// clear). Each row's thresholds are worked out once: the pattern repeats -// every ditherSize dots across. -static void bandToPlanes(const PrintDriverT *drv, InkModeE mode, const uint8_t *rgb, int32_t y0, PrintPlanesT *pl) { +// The band's rows rowFrom..rowTo - 1 as the driver's inks, dithered (the +// planes come clear). Each row's thresholds are worked out once: the +// pattern repeats every ditherSize dots across. +static void bandToPlanes(const PrintDriverT *drv, InkModeE mode, const uint8_t *rgb, int32_t y0, PrintPlanesT *pl, int32_t rowFrom, int32_t rowTo) { int32_t mask = drv->ditherSize - 1; - for (int32_t r = 0; r < pl->rows; r++) { + for (int32_t r = rowFrom; r < rowTo; r++) { uint8_t th[PRINT_PLANES_MAX][PRINT_DITHER_LARGE]; const uint8_t *src = rgb + (int64_t)r * pl->widthDots * RGB_BYTES; @@ -222,11 +223,8 @@ static bool pictureBand(void *ctx, uint8_t *rgb, int32_t width, int32_t y0, int3 for (int32_t r = 0; r < rows; r++) { const uint8_t *src = pic->rgb + (int64_t)((int64_t)(y0 + r) * pic->h / pic->outH) * pic->pitch; - uint8_t *dst = rgb + (int64_t)r * width * RGB_BYTES; - for (int32_t x = 0; x < width; x++) { - memcpy(dst + x * RGB_BYTES, src + (int64_t)x * pic->w / pic->outW * RGB_BYTES, RGB_BYTES); - } + drawScaleRow(rgb + (int64_t)r * width * RGB_BYTES, src, RGB_BYTES, 0, width, pic->outW, pic->w, false, 0); } return true; @@ -347,10 +345,10 @@ const PrintDriverT *printDriver(PrintModelE model) { return &printDrvDeskJetColor; case PrintModelPostScriptE: - return &printDrvPostScript; + return printDriverPs(false); case PrintModelPostScriptColorE: - return &printDrvPostScriptColor; + return printDriverPs(true); case PrintModelImageWriterE: return &printDrvImageWriter; @@ -450,16 +448,26 @@ bool printPageRun(PrintJobT *job, const PrintDriverT *drv, int32_t widthDots, in pl.rows = heightDots - y0 < drv->bandRows ? heightDots - y0 : drv->bandRows; pl.y0 = y0; memset(rgb, WHITE, (size_t)widthDots * (size_t)pl.rows * RGB_BYTES); - ok = fn(ctx, rgb, widthDots, y0, pl.rows); - for (int32_t p = 0; ok && p < pl.count; p++) { + for (int32_t p = 0; p < pl.count; p++) { memset(pl.bits[p], 0, planeBytes); } - if (ok) { - bandToPlanes(drv, mode, rgb, y0, &pl); - ok = drv->band(drv, job, &pl); + // Filled and dithered PRINT_SUB_ROWS rows at a time, the job polled + // between: a band of a 300 dpi page is a lot of work, and a white + // one sends almost nothing that would let the others run + for (int32_t s = 0; ok && s < pl.rows; s += PRINT_SUB_ROWS) { + int32_t n = pl.rows - s < PRINT_SUB_ROWS ? pl.rows - s : PRINT_SUB_ROWS; + + ok = fn(ctx, rgb + (size_t)s * (size_t)widthDots * RGB_BYTES, widthDots, y0 + s, n) && printJobPoll(job); + + if (ok) { + bandToPlanes(drv, mode, rgb, y0, &pl, s, s + n); + ok = printJobPoll(job); + } } + + ok = ok && drv->band(drv, job, &pl); } ok = ok && drv->pageEnd(drv, job); diff --git a/src/libs/kpunch/print/printText.c b/src/libs/kpunch/print/printText.c index 3e0aaad3..30398394 100644 --- a/src/libs/kpunch/print/printText.c +++ b/src/libs/kpunch/print/printText.c @@ -34,6 +34,7 @@ // lines instead of the plain text. #include "printInt.h" +#include "taskSwch.h" #include #include @@ -55,6 +56,7 @@ #define STAMP_TEXT_MAX 20 #define DATE_FORMAT "%Y-%m-%d" #define TIME_FORMAT "%H:%M" +#define LAYOUT_POLL_BYTES 16384 // text laid out between two polls (layoutPoll) // One layout pass typedef struct { @@ -94,6 +96,7 @@ static void bodyLine(TextOutT *o, const char *text, int32_t len); static void endSource(TextOutT *o, LineT *ln); static void flushLine(TextOutT *o, LineT *ln); static void layout(TextOutT *o, const char *text, int32_t len); +static bool layoutPoll(TextOutT *o); static void lineReset(LineT *ln); static void outBytes(TextOutT *o, const char *data, int32_t len); static void outHeader(TextOutT *o, const char *fmt); @@ -157,6 +160,10 @@ static void layout(TextOutT *o, const char *text, int32_t len) { for (int32_t i = 0; i < len; i++) { char ch = text[i]; + if ((i + 1) % LAYOUT_POLL_BYTES == 0 && !layoutPoll(o)) { + return; + } + if (ch == '\r') { continue; } @@ -212,6 +219,21 @@ static void layout(TextOutT *o, const char *text, int32_t len) { } +// A long text lays out its pages without printing a byte (the page count, +// the pages before the range): the job is polled as it goes, and without +// one the others get a turn. False (o->ok false too) when the job was +// cancelled. +static bool layoutPoll(TextOutT *o) { + if (!o->job) { + tsYield(); + return true; + } + + o->ok = o->ok && printJobPoll(o->job); + return o->ok; +} + + static void lineReset(LineT *ln) { ln->n = 0; ln->col = 0; diff --git a/src/libs/kpunch/print/printUi.c b/src/libs/kpunch/print/printUi.c index cc2ac227..b1a4e0df 100644 --- a/src/libs/kpunch/print/printUi.c +++ b/src/libs/kpunch/print/printUi.c @@ -62,7 +62,6 @@ #define UI_PAGES_ALL 0 #define UI_PAGES_RANGE 1 #define RGB_BYTES 3 -#define PIXEL_BYTE_BITS 8 // A job's user interface state (the hooks' context) typedef struct { @@ -134,7 +133,6 @@ static void progressHide(UiRunT *run); static void progressShow(UiRunT *run); static bool setupDlgOk(WindowT *dlg, void *userCtx); static void textOnlyBox(AppContextT *ctx); -static uint8_t *toRgb(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch); static PrintAnswerE uiAsk(void *ctx, const char *message); static void uiFinished(void *ctx, bool closing); static bool uiIdle(void *ctx); @@ -486,7 +484,7 @@ bool printUiImageFile(AppContextT *ctx, const char *path, const char *title, boo return false; } - uint8_t *rgb = toRgb(ctx, data, w, h, pitch); + uint8_t *rgb = dvxBufferToRgb(&ctx->display, data, w, h, pitch); dvxFreeImage(data); @@ -573,7 +571,7 @@ bool printUiWindow(AppContextT *ctx, WindowT *win, bool askFirst) { return false; } - uint8_t *rgb = toRgb(ctx, win->contentBuf, win->contentW, win->contentH, win->contentPitch); + uint8_t *rgb = dvxBufferToRgb(&ctx->display, win->contentBuf, win->contentW, win->contentH, win->contentPitch); if (!rgb) { dvxErrorBox(ctx, UI_TITLE, "Not enough memory to print the window."); @@ -682,37 +680,6 @@ static void textOnlyBox(AppContextT *ctx) { } -// The display-format pixels of a picture or window as R, G, B bytes -// (NULL when out of memory). -static uint8_t *toRgb(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch) { - const DisplayT *d = &ctx->display; - int32_t bpp = d->format.bytesPerPixel; - uint8_t *rgb = (uint8_t *)malloc((size_t)w * (size_t)h * RGB_BYTES); - - if (!rgb) { - return NULL; - } - - for (int32_t y = 0; y < h; y++) { - const uint8_t *src = data + (size_t)y * (size_t)pitch; - uint8_t *dst = rgb + (size_t)y * (size_t)w * RGB_BYTES; - - for (int32_t x = 0; x < w; x++) { - uint32_t pixel = 0; - - for (int32_t b = 0; b < bpp; b++) { - pixel |= (uint32_t)src[x * bpp + b] << (b * PIXEL_BYTE_BITS); - } - - unpackColor(d, pixel, &dst[0], &dst[1], &dst[2]); - dst += RGB_BYTES; - } - } - - return rgb; -} - - static PrintAnswerE uiAsk(void *ctx, const char *message) { UiRunT *run = (UiRunT *)ctx; diff --git a/src/libs/kpunch/seclink/Makefile b/src/libs/kpunch/seclink/Makefile new file mode 100644 index 00000000..bf2f7e23 --- /dev/null +++ b/src/libs/kpunch/seclink/Makefile @@ -0,0 +1,65 @@ +# 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. + +# Secure Link Library Makefile for DJGPP cross-compilation +# +# seclink.lib: reliable packets over a serial port or any byte stream +# (packet.c: HDLC framing, CRC-16, Go-Back-N) and the encrypted, +# multi-channel link on top of them (secLink.c), over serial.lib's UART +# driver and crypto.lib's ciphers. Part of Communications. + +include ../../../common.mk + +CFLAGS = $(DOS_CFLAGS) -I. -I../libdvx -I../libdvx/platform -I../libtasks + +OBJDIR = ../../../../obj/seclink +LIBSDIR = ../../../../bin/libs + +SRCS = $(wildcard *.c) +OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) +TARGETDIR = $(LIBSDIR)/kpunch/seclink +TARGET = $(TARGETDIR)/seclink.lib + +.PHONY: all clean + +all: $(TARGET) $(TARGETDIR)/seclink.dep + +$(TARGETDIR)/seclink.dep: seclink.dep | $(TARGETDIR) + sed 's/$$/\r/' $< > $@ + +$(TARGET): $(OBJS) | $(TARGETDIR) + $(DXE3GEN) -o $(TARGETDIR)/seclink.dxe -U $(OBJS) + mv $(TARGETDIR)/seclink.dxe $@ + +$(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR): + mkdir -p $(OBJDIR) + +$(TARGETDIR): + mkdir -p $(TARGETDIR) + +-include $(OBJS:.o=.d) + +clean: + rm -rf $(OBJDIR) $(TARGETDIR) diff --git a/src/libs/kpunch/seclink/README.md b/src/libs/kpunch/seclink/README.md new file mode 100644 index 00000000..29c35e55 --- /dev/null +++ b/src/libs/kpunch/seclink/README.md @@ -0,0 +1,40 @@ +# seclink -- Secure Link + +Reliable packets and the encrypted link on top of them, over a serial +port (the serial library's rs232 driver) or any byte stream, such as a +network connection. Part of Communications. + +- packet: a reliable, ordered transport. HDLC-style byte stuffing + (FLAG=0x7E, ESC=0x7D), CRC-16-CCITT integrity, and Go-Back-N + sliding-window retransmission. +- secLink: the multi-channel encrypted link: optional shared-passphrase + authentication, forward-secret X25519 handshake with key + confirmation, XChaCha20-Poly1305 on every encrypted packet (the + cryptography library), + up to 128 channels multiplexed on one link, and bulk send with + retransmit handled underneath. + +The Linux proxy (src/tools/proxy) builds the same sources against a +socket shim in place of the rs232 driver. + +## Documentation + +Full reference is in `seclink.dhs` (this directory), compiled into the +DVX System Reference -- open via the Help Viewer app, or read +`docs/dvx_system_reference.html` after `make`. + +Topics covered: protocol, open/close, passphrase, handshake, send/poll, +plaintext channels, error codes, the packet transport (frame format, +window size, retransmit timer, CRC), and an end-to-end example. + +## Source Files + +- `secLink.h` -- public API and error codes +- `secLink.c` -- implementation (handshake, channel mux, bulk send) +- `packet.h` -- packet transport API and error codes +- `packet.c` -- implementation (framing, CRC, sliding window) + +## Build + +Built into `seclink.lib` by `make -C src/libs/kpunch/seclink` (invoked +by the top-level `make`). diff --git a/src/libs/kpunch/serial/packet/packet.c b/src/libs/kpunch/seclink/packet.c similarity index 81% rename from src/libs/kpunch/serial/packet/packet.c rename to src/libs/kpunch/seclink/packet.c index 64a8d2bc..5221deb1 100644 --- a/src/libs/kpunch/serial/packet/packet.c +++ b/src/libs/kpunch/seclink/packet.c @@ -44,7 +44,9 @@ #include #include #include "packet.h" -#include "../rs232/rs232.h" +#include "../serial/rs232/rs232.h" +#include "../libdvx/dvxEndian.h" +#include "../libdvx/dvxHash.h" // ======================================================================== @@ -83,15 +85,22 @@ // Per-connection buffer for draining the serial port in pktPoll #define RX_READ_CHUNK 128 -// A frame write stalled by flow control (XOFF, CTS low) is retried until -// the port accepts bytes again or this long passes without progress. -#define FRAME_WRITE_TIMEOUT_MS 2000 +// Frames stuffed for the wire wait in a queue the port or stream takes +// from as it can (never waited for): room for a full window and the +// control frames between them +#define TX_QUEUE_FRAMES (PKT_MAX_WINDOW + 2) +#define TX_QUEUE_SIZE (TX_QUEUE_FRAMES * MAX_STUFFED_SIZE) // Retransmit timeout. 500ms is conservative for a local serial link (RTT // is < 1ms) but accounts for the remote side being busy. On a real BBS // connection through the proxy, the round-trip includes TCP latency. #define RETRANSMIT_TIMEOUT_MS 500 +// A wire whose untaken bytes have not moved for this long is not counted +// busy (flow control holding the line, a stuck port): the retransmit and +// link timers run again, as when frames were written byte by byte +#define WIRE_STALL_MS 2000 + // CRC-16-CCITT initial register value (RFC 1662 / HDLC convention). #define CRC_INIT_VALUE 0xFFFF @@ -139,6 +148,14 @@ struct PktConnS { TxSlotT txSlots[PKT_MAX_WINDOW]; int txCount; // number of slots in use int txHead; // physical index of oldest unacked slot + clock_t lastHeard; // the peer's last good frame (or the window's first frame) + clock_t wireMoved; // when the bytes on their way to the wire last changed (wireBusy) + int32_t wireBytes; // how many there were then + + // Stuffed frames not yet taken by the port or stream (circular) + uint8_t txQueue[TX_QUEUE_SIZE]; + int txQHead; // the first byte not taken + int txQLen; // Receive state (Go-Back-N receiver: only accepts in-order frames) uint8_t rxExpectSeq; // next expected sequence number @@ -173,56 +190,12 @@ struct PktConnS { static PktConnT *sOpenConns[RS232_NUM_PORTS]; -// ======================================================================== -// CRC-16-CCITT lookup table -// ======================================================================== - -// Regenerated from the polynomial (previous table had 42 corrupted -// entries, breaking the guaranteed error-detection properties). To -// verify: table[i ^ j] == table[i] ^ table[j] must hold for all i, j. -static const uint16_t sCrcTable[256] = { - 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, - 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, - 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, - 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, - 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, - 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, - 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, - 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, - 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, - 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, - 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, - 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, - 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, - 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, - 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, - 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, - 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, - 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, - 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, - 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, - 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, - 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, - 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, - 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, - 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, - 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, - 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, - 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, - 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, - 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, - 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, - 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0 -}; - - // ======================================================================== // Prototypes (alphabetical) // ======================================================================== static int32_t ackAdvance(PktConnT *conn, uint8_t seq); static PktConnT *connCreate(const PktStreamT *stream, int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx); -static uint16_t crcCalc(const uint8_t *data, int len); bool pktCanSend(PktConnT *conn); void pktClose(PktConnT *conn); int pktGetPending(PktConnT *conn); @@ -233,18 +206,23 @@ int pktReset(PktConnT *conn); int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block); void pktSetResetCallback(PktConnT *conn, PktResetCallbackT callback); int pktWaitSpace(PktConnT *conn); +static int32_t portPending(void *ctx); static int32_t portRead(void *ctx, uint8_t *buf, int32_t len); static int32_t portWrite(void *ctx, const uint8_t *data, int32_t len); static bool processFrame(PktConnT *conn, const uint8_t *frame, int len); static void resetSequenceState(PktConnT *conn); -static void retransmitCheck(PktConnT *conn); +static bool retransmitCheck(PktConnT *conn); static bool rxProcessByte(PktConnT *conn, uint8_t byte); static void sendAck(PktConnT *conn, uint8_t seq); -static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len); +static bool sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len); static void sendNakOnce(PktConnT *conn); static void sendRst(PktConnT *conn, uint8_t marker); static int seqInWindow(uint8_t seq, uint8_t base, int size); +static void txFlush(PktConnT *conn); +static bool txPut(PktConnT *conn, const uint8_t *bytes, int len); +static bool txRoom(const PktConnT *conn); static int txSlotIndex(PktConnT *conn, uint8_t seq); +static bool wireBusy(PktConnT *conn); // Returns how many outstanding slots a cumulative ACK frees, or -1 if the @@ -289,26 +267,12 @@ static PktConnT *connCreate(const PktStreamT *stream, int com, int windowSize, P } -// Table-driven CRC-16-CCITT. Processing one byte per iteration with a -// 256-entry table is ~10x faster than bit-by-bit on a 486. The table -// costs 512 bytes of .rodata -- a worthwhile trade for a function called -// on every frame received and transmitted. -static uint16_t crcCalc(const uint8_t *data, int len) { - uint16_t crc = CRC_INIT_VALUE; - - for (int i = 0; i < len; i++) { - crc = (crc << 8) ^ sCrcTable[(crc >> 8) ^ data[i]]; - } - - return crc; -} - - +// A free slot, and room in the queue for its frame. bool pktCanSend(PktConnT *conn) { if (!conn) { return false; } - return conn->txCount < conn->windowSize; + return conn->txCount < conn->windowSize && txRoom(conn); } @@ -356,10 +320,11 @@ PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *call return NULL; } - port.read = portRead; - port.write = portWrite; - port.ctx = (void *)(intptr_t)com; - conn = connCreate(&port, com, windowSize, callback, callbackCtx); + port.read = portRead; + port.write = portWrite; + port.pending = portPending; + port.ctx = (void *)(intptr_t)com; + conn = connCreate(&port, com, windowSize, callback, callbackCtx); if (conn) { sOpenConns[com] = conn; @@ -395,6 +360,7 @@ int pktPoll(PktConnT *conn) { } conn->pollDepth++; + txFlush(conn); while (!conn->closePending) { uint8_t byte; @@ -417,7 +383,8 @@ int pktPoll(PktConnT *conn) { } if (!conn->closePending && !disconnected) { - retransmitCheck(conn); + disconnected = retransmitCheck(conn); + txFlush(conn); } conn->pollDepth--; @@ -468,10 +435,16 @@ int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block) { if (rc != PKT_SUCCESS) { return rc; } - } else if (conn->txCount >= conn->windowSize) { + } else if (!pktCanSend(conn)) { return PKT_ERR_TX_FULL; } + // The first frame of an empty window: the link's silence counts from + // here + if (conn->txCount == 0) { + conn->lastHeard = clock(); + } + // Store in retransmit buffer slot = &conn->txSlots[(conn->txHead + conn->txCount) % PKT_MAX_WINDOW]; memcpy(slot->data, data, len); @@ -514,7 +487,7 @@ int pktWaitSpace(PktConnT *conn) { limit = (clock_t)PKT_SEND_TIMEOUT_MS * CLOCKS_PER_SEC / 1000; lastCount = conn->txCount; - while (conn->txCount >= conn->windowSize) { + while (!pktCanSend(conn)) { if (pktPoll(conn) == PKT_ERR_DISCONNECTED) { return PKT_ERR_DISCONNECTED; } @@ -524,7 +497,7 @@ int pktWaitSpace(PktConnT *conn) { } lastCount = conn->txCount; - if (conn->txCount >= conn->windowSize && clock() - start >= limit) { + if (!pktCanSend(conn) && clock() - start >= limit) { return PKT_ERR_TIMEOUT; } } @@ -547,14 +520,21 @@ int pktWaitSpace(PktConnT *conn) { // // For NAK frames: the receiver wants us to retransmit from a specific // sequence. Go-Back-N retransmits that frame AND all subsequent ones. -// A COM port as a stream (ctx is the port index). +// A COM port as a stream (ctx is the port index): writes go into its +// transmit ring, which its interrupt handler sends (flow control and +// XON/XOFF included); what the ring still holds is pending. +static int32_t portPending(void *ctx) { + return rs232GetTxBuffered((int32_t)(intptr_t)ctx); +} + + static int32_t portRead(void *ctx, uint8_t *buf, int32_t len) { return rs232Read((int32_t)(intptr_t)ctx, (char *)buf, len); } static int32_t portWrite(void *ctx, const uint8_t *data, int32_t len) { - return rs232Write((int32_t)(intptr_t)ctx, (const char *)data, len); + return rs232WriteBuf((int32_t)(intptr_t)ctx, (const char *)data, len); } @@ -571,14 +551,16 @@ static bool processFrame(PktConnT *conn, const uint8_t *frame, int len) { } // Verify CRC - calcCrc = crcCalc(frame, len - CRC_SIZE); - rxCrc = frame[len - 2] | ((uint16_t)frame[len - 1] << 8); + calcCrc = dvxCrc16(CRC_INIT_VALUE, frame, len - CRC_SIZE); + rxCrc = dvxGetLe16(frame + len - CRC_SIZE); if (calcCrc != rxCrc) { // CRC mismatch -- request retransmit of what we expect sendNakOnce(conn); return false; } + conn->lastHeard = clock(); + seq = frame[0]; type = frame[1]; payloadLen = frame[2]; @@ -644,8 +626,10 @@ static bool processFrame(PktConnT *conn, const uint8_t *frame, int len) { // Retransmit this slot and all after it (go-back-N) for (int i = idx; i < conn->txCount; i++) { TxSlotT *slot = &conn->txSlots[(conn->txHead + i) % PKT_MAX_WINDOW]; - sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len); - slot->timer = clock(); + + if (sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len)) { + slot->timer = clock(); + } } } break; @@ -701,18 +685,40 @@ static void resetSequenceState(PktConnT *conn) { // timeout. This handles the case where an ACK or NAK was lost -- without // this, the connection would stall forever. Each slot is retransmitted // independently and its timer is reset, creating exponential backoff -// behavior naturally (each retransmit resets the timer). -static void retransmitCheck(PktConnT *conn) { +// behavior naturally (each retransmit resets the timer). The timers only +// run once the wire is idle: until then the window is still going out (a +// window takes a second at 9600 bps). True when the link is lost: the +// peer said nothing for PKT_LINK_TIMEOUT_MS with frames waiting for it. +static bool retransmitCheck(PktConnT *conn) { clock_t now = clock(); clock_t timeout = (clock_t)RETRANSMIT_TIMEOUT_MS * CLOCKS_PER_SEC / 1000; + clock_t lost = (clock_t)PKT_LINK_TIMEOUT_MS * CLOCKS_PER_SEC / 1000; + + if (conn->txCount == 0) { + return false; + } + + if (wireBusy(conn)) { + for (int i = 0; i < conn->txCount; i++) { + conn->txSlots[(conn->txHead + i) % PKT_MAX_WINDOW].timer = now; + } + + conn->lastHeard = now; + return false; + } + + if (now - conn->lastHeard >= lost) { + return true; + } for (int i = 0; i < conn->txCount; i++) { TxSlotT *slot = &conn->txSlots[(conn->txHead + i) % PKT_MAX_WINDOW]; - if (now - slot->timer >= timeout) { - sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len); + if (now - slot->timer >= timeout && sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len)) { slot->timer = now; } } + + return false; } @@ -779,19 +785,19 @@ static void sendAck(PktConnT *conn, uint8_t seq) { } -// Build a raw frame, compute CRC, byte-stuff it, and transmit. -// The stuffed buffer can be up to 2x the raw size (every byte might need -// escaping) plus the flags. This is stack-allocated because frames are small -// and we're not in a deeply recursive call path. -static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len) { +// Build a raw frame, compute CRC, byte-stuff it, and queue it whole for +// the wire (txFlush sends what the port or stream takes now). The stuffed +// buffer can be up to 2x the raw size (every byte might need escaping) +// plus the flags. This is stack-allocated because frames are small and +// we're not in a deeply recursive call path. False when the queue has no +// room: the frame is dropped (a data frame's retransmit timer resends it; +// a lost ACK or NAK is recovered by the peer's). +static bool sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len) { uint8_t raw[MAX_FRAME_SIZE]; uint8_t stuffed[MAX_STUFFED_SIZE]; int rawLen; uint16_t crc; int out; - int sent; - clock_t start; - clock_t limit; // Build raw frame: SEQ + TYPE + LEN + PAYLOAD raw[0] = seq; @@ -803,9 +809,8 @@ static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t * rawLen = HEADER_SIZE + len; // Append CRC - crc = crcCalc(raw, rawLen); - raw[rawLen] = (uint8_t)(crc & 0xFF); - raw[rawLen + 1] = (uint8_t)((crc >> 8) & 0xFF); + crc = dvxCrc16(CRC_INIT_VALUE, raw, rawLen); + dvxPutLe16(raw + rawLen, crc); rawLen += CRC_SIZE; // Byte-stuff into output buffer with leading flag @@ -826,27 +831,12 @@ static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t * // Trailing flag to close the frame immediately stuffed[out++] = FLAG_BYTE; - // Send via the port or stream. rs232Write stops early while flow - // control holds the line (XOFF, CTS low), a socket while its buffer is - // full; keep writing the rest so a frame is never cut short, but give - // up if nothing is accepted for a while (the retransmit timer recovers - // the frame). - sent = 0; - start = clock(); - limit = (clock_t)FRAME_WRITE_TIMEOUT_MS * CLOCKS_PER_SEC / 1000; - while (sent < out) { - int n = conn->stream.write(conn->stream.ctx, stuffed + sent, out - sent); - - if (n < 0) { - break; - } - if (n > 0) { - sent += n; - start = clock(); - } else if (clock() - start >= limit) { - break; - } + if (!txPut(conn, stuffed, out)) { + return false; } + + txFlush(conn); + return true; } @@ -877,6 +867,51 @@ static int seqInWindow(uint8_t seq, uint8_t base, int size) { } +// Send what the port or stream takes of the queue now (never waiting: flow +// control holding the line, or a full socket, leaves the rest for the next +// poll). +static void txFlush(PktConnT *conn) { + while (conn->txQLen > 0) { + int run = TX_QUEUE_SIZE - conn->txQHead; + int n; + + if (run > conn->txQLen) { + run = conn->txQLen; + } + + n = conn->stream.write(conn->stream.ctx, conn->txQueue + conn->txQHead, run); + + if (n <= 0) { + break; + } + + conn->txQHead = (conn->txQHead + n) % TX_QUEUE_SIZE; + conn->txQLen -= n; + } +} + + +// A whole stuffed frame onto the queue; false when it does not fit. +static bool txPut(PktConnT *conn, const uint8_t *bytes, int len) { + if (TX_QUEUE_SIZE - conn->txQLen < len) { + return false; + } + + for (int i = 0; i < len; i++) { + conn->txQueue[(conn->txQHead + conn->txQLen + i) % TX_QUEUE_SIZE] = bytes[i]; + } + + conn->txQLen += len; + return true; +} + + +// Room on the queue for the largest frame. +static bool txRoom(const PktConnT *conn) { + return TX_QUEUE_SIZE - conn->txQLen >= MAX_STUFFED_SIZE; +} + + static int txSlotIndex(PktConnT *conn, uint8_t seq) { uint8_t diff = seq - conn->txAckSeq; if (diff >= (uint8_t)conn->txCount) { @@ -884,3 +919,19 @@ static int txSlotIndex(PktConnT *conn, uint8_t seq) { } return diff; } + + +// Bytes still on their way to the wire, queued here or taken by a port +// that has not sent them yet, and moving: a count that has not changed +// for WIRE_STALL_MS is not busy. +static bool wireBusy(PktConnT *conn) { + int32_t bytes = conn->txQLen + (conn->stream.pending ? conn->stream.pending(conn->stream.ctx) : 0); + clock_t now = clock(); + + if (bytes != conn->wireBytes) { + conn->wireBytes = bytes; + conn->wireMoved = now; + } + + return bytes > 0 && now - conn->wireMoved < (clock_t)WIRE_STALL_MS * CLOCKS_PER_SEC / 1000; +} diff --git a/src/libs/kpunch/serial/packet/packet.h b/src/libs/kpunch/seclink/packet.h similarity index 81% rename from src/libs/kpunch/serial/packet/packet.h rename to src/libs/kpunch/seclink/packet.h index 4fdc1913..aef6592b 100644 --- a/src/libs/kpunch/serial/packet/packet.h +++ b/src/libs/kpunch/seclink/packet.h @@ -42,9 +42,9 @@ // net for electrical noise, not a routine error recovery mechanism. // // CRC-16-CCITT: -// Standard polynomial used in HDLC/X.25. Table-driven for speed -// (256-entry lookup table trades 512 bytes of ROM for ~10x faster CRC -// than the bit-by-bit method, important on a 486). +// Standard polynomial used in HDLC/X.25, register seeded with 0xFFFF. +// Computed a byte at a time by dvxCrc16 (libdvx's dvxHash.h), the code +// file transfers use too. #ifndef PACKET_H #define PACKET_H @@ -75,6 +75,10 @@ // long (a silent or vanished peer must not hang the caller forever). #define PKT_SEND_TIMEOUT_MS 15000 +// The link is lost (pktPoll says PKT_ERR_DISCONNECTED) when the peer sends +// nothing this long while frames wait for it and the wire is idle. +#define PKT_LINK_TIMEOUT_MS 30000 + // Callback for received packets // ctx: user context pointer // data: payload (valid only during callback) @@ -91,15 +95,18 @@ typedef void (*PktResetCallbackT)(void *ctx); typedef struct PktConnS PktConnT; // A byte stream to run the packet layer over instead of a COM port (a TCP -// socket, for example). Both calls must return at once. -// read: up to len bytes into buf; the count, 0 when none has arrived, -// negative when the stream is gone -// write: bytes taken from data (0 = none right now), negative when the -// stream is gone +// socket, for example). Every call must return at once. +// read: up to len bytes into buf; the count, 0 when none has arrived, +// negative when the stream is gone +// write: bytes taken from data (0 = none right now), negative when the +// stream is gone +// pending: bytes taken but not yet on the wire (NULL: it does not say); +// retransmit timers wait for them typedef struct { int32_t (*read)(void *ctx, uint8_t *buf, int32_t len); int32_t (*write)(void *ctx, const uint8_t *data, int32_t len); void *ctx; + int32_t (*pending)(void *ctx); } PktStreamT; @@ -121,16 +128,21 @@ PktConnT *pktOpenStream(const PktStreamT *stream, int windowSize, PktRecvCallbac // PKT_ERR_DISCONNECTED). void pktClose(PktConnT *conn); -// Send a packet. Returns PKT_SUCCESS or error. +// Send a packet. Returns PKT_SUCCESS or error. Its frame is queued and +// goes out as the port or stream takes it (pktPoll keeps it going); the +// window is full while the queue has no room for another frame. // Blocks if the transmit window is full and block is true (see // pktWaitSpace for the bound). Returns PKT_ERR_TX_FULL if window is full // and block is false. int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block); -// Poll for incoming data, process received frames, handle retransmits. -// Must be called frequently (e.g. in your main loop). May be re-entered -// from the receive callback; nested calls continue the same byte stream. -// Returns the number of valid data packets delivered to the callback. +// Poll for incoming data, process received frames, handle retransmits, +// and send what the port or stream takes of the queued frames (never +// waiting). Must be called frequently (e.g. in your main loop). May be +// re-entered from the receive callback; nested calls continue the same +// byte stream. Returns the number of valid data packets delivered to the +// callback, or PKT_ERR_DISCONNECTED (the port or stream is gone, or the +// link is lost: PKT_LINK_TIMEOUT_MS). int pktPoll(PktConnT *conn); // Reset the connection state (sequence numbers, buffers). diff --git a/src/libs/kpunch/serial/seclink/secLink.c b/src/libs/kpunch/seclink/secLink.c similarity index 69% rename from src/libs/kpunch/serial/seclink/secLink.c rename to src/libs/kpunch/seclink/secLink.c index 102700bd..02358174 100644 --- a/src/libs/kpunch/serial/seclink/secLink.c +++ b/src/libs/kpunch/seclink/secLink.c @@ -23,21 +23,26 @@ // Secure serial link -- ties rs232, packet, and security into one API // -// Packet types (first byte of every packet): -// HELLO [type][version][flags][DH public key, 128 bytes] +// Packet types (first byte of every packet), protocol version 3: +// HELLO [type][version][flags][X25519 public key, 32 bytes] // CONFIRM [type][HMAC-SHA256, 32 bytes] // PLAIN [type][channel][payload] -// CIPHER [type][channel][XTEA-CTR ciphertext][truncated HMAC, 8 bytes] +// CIPHER [type][channel][XChaCha20 ciphertext][Poly1305 tag, 16 bytes] // // Handshake: -// 1. Each side sends HELLO with a fresh DH public key. A HELLO is stored -// whenever it arrives, so a peer that starts first is never missed. +// 1. Each side sends HELLO with a fresh X25519 public key. A HELLO is +// stored whenever it arrives, so a peer that starts first is never +// missed. An X25519 takes about a sixth of a second on a 486, so a +// poll does a slice of one (curveStep, about 20 ms there): our public +// key, then (once both HELLOs are here) the shared secret. Our HELLO +// goes out once our key is ready. // 2. With both keys present each side checks the version and that both -// or neither use a passphrase, computes the DH secret and runs +// or neither use a passphrase, computes the shared secret (a peer key +// that gives an all-zero secret fails the exchange) and runs // HKDF-SHA256(salt = SHA-256(passphrase) or zeros, ikm = secret, -// info = label || lowerPub || higherPub). The output is split into -// an XTEA key and an HMAC key per direction plus a confirmation key; -// the side with the lower public key sends with the "low" keys. +// info = label || lowerPub || higherPub). The output is split into a +// sending key per direction plus a confirmation key; the side with +// the lower public key sends with the "low" key. // 3. Each side sends CONFIRM = HMAC(confirmKey, SHA-256(lowerHello || // higherHello) || role). A wrong passphrase or a tampered HELLO makes // the peer's CONFIRM mismatch and the handshake fails with @@ -49,7 +54,7 @@ // previous HELLO) means the peer is re-keying: we answer with a fresh // HELLO and CONFIRM from secLinkPoll. A re-key HELLO whose version or // passphrase flag cannot match is dropped without touching the running -// session. The DH private key is destroyed as soon as the session keys +// session. The X25519 secret key is wiped as soon as the session keys // are derived (forward secrecy). // // Clear frames: @@ -60,12 +65,13 @@ // on the line can cause, so neither can downgrade a protected channel. // // Encrypted frames: -// Encrypt-then-MAC. Each direction numbers its encrypted frames with an -// implicit 64-bit sequence (never sent); the MAC covers type, channel, -// sequence and ciphertext, and the CTR counter for a frame starts at -// seq * CTR_BLOCKS_PER_FRAME, so every frame has its own keystream range -// and a dropped frame does not desynchronize later ones. A replayed or -// injected frame is checked against the wrong sequence number and +// XChaCha20-Poly1305 with the type and channel bytes as associated data. +// Each direction numbers its encrypted frames with an implicit 64-bit +// sequence (never sent); the nonce is that number, little-endian, then +// zeros, so no two frames share one and a frame sealed for one sequence +// number does not open under another. Every frame is sealed on its own, +// so a dropped frame does not desynchronize later ones, and a replayed +// or injected frame is checked against the wrong sequence number and // dropped. #include @@ -74,10 +80,15 @@ #include #include #include "secLink.h" -#include "../rs232/rs232.h" -#include "../packet/packet.h" -#include "../security/security.h" -#include "../security/secHash.h" +#include "../serial/rs232/rs232.h" +#include "packet.h" +#include "../crypto/secCurve.h" +#include "../crypto/security.h" +#include "../crypto/secHash.h" +#include "../libdvx/dvxTypes.h" + +// ABI stamp checked by the loader (see DVX_ABI_VERSION). +DVX_ABI_STAMP(seclink); // ======================================================================== @@ -96,7 +107,7 @@ #define HELLO_VERSION_OFS 1 #define HELLO_FLAGS_OFS 2 #define HELLO_PUB_OFS 3 -#define HELLO_SIZE (HELLO_PUB_OFS + SEC_DH_KEY_SIZE) +#define HELLO_SIZE (HELLO_PUB_OFS + SEC_X25519_KEY_SIZE) #define HELLO_FLAG_PASS 0x01 // CONFIRM layout @@ -111,32 +122,20 @@ #define ROLE_HIGH 0x02 // HKDF info label; the two public keys (lower first) follow it -#define KDF_LABEL "DVX secLink v2" +#define KDF_LABEL "DVX secLink v3" #define KDF_LABEL_LEN ((int32_t)sizeof(KDF_LABEL) - 1) -#define KDF_INFO_SIZE (KDF_LABEL_LEN + 2 * SEC_DH_KEY_SIZE) +#define KDF_INFO_SIZE (KDF_LABEL_LEN + 2 * SEC_X25519_KEY_SIZE) -// HKDF output layout -#define MAC_KEY_SIZE SEC_SHA256_SIZE -#define KEY_ENC_LOW_OFS 0 -#define KEY_ENC_HIGH_OFS (KEY_ENC_LOW_OFS + SEC_XTEA_KEY_SIZE) -#define KEY_MAC_LOW_OFS (KEY_ENC_HIGH_OFS + SEC_XTEA_KEY_SIZE) -#define KEY_MAC_HIGH_OFS (KEY_MAC_LOW_OFS + MAC_KEY_SIZE) -#define KEY_CONFIRM_OFS (KEY_MAC_HIGH_OFS + MAC_KEY_SIZE) +// HKDF output layout: each side's sending key, then the confirmation key +#define KEY_LOW_OFS 0 +#define KEY_HIGH_OFS (KEY_LOW_OFS + SEC_AEAD_KEY_SIZE) +#define KEY_CONFIRM_OFS (KEY_HIGH_OFS + SEC_AEAD_KEY_SIZE) #define KEY_BLOCK_SIZE (KEY_CONFIRM_OFS + SEC_SHA256_SIZE) -// MAC input: frame header, then the big-endian sequence number -#define SEQ_BYTES 8 +// The nonce: the frame's sequence number in its first bytes +#define NONCE_SEQ_BYTES 8 -// Each frame owns 2^CTR_FRAME_SHIFT keystream blocks (32 blocks = 256 -// bytes, more than any payload) -#define CTR_FRAME_SHIFT 5 -#define XTEA_BLOCK_BYTES 8 -typedef char secLinkCtrRangeFits[((1 << CTR_FRAME_SHIFT) * XTEA_BLOCK_BYTES >= PKT_MAX_PAYLOAD) ? 1 : -1]; - -// Handshake wait bound. The peer may need several seconds for its DH -// keypair on slow hardware, but a silent peer (proxy or BBS not running) -// must not hang the single-threaded caller forever. -#define HANDSHAKE_TIMEOUT_MS 30000 +#define MS_PER_SEC 1000 // ======================================================================== @@ -155,29 +154,35 @@ struct SecLinkS { uint64_t txSeq; uint64_t rxSeq; PktConnT *pkt; - SecDhT *dh; - SecCipherT *txCipher; - SecCipherT *rxCipher; SecLinkRecvT userCallback; + clock_t exchangeStart; // when our current exchange began void *userCtx; int32_t dropped; int32_t depth; // nesting of public calls (deferred close) int32_t com; SecLinkStateE state; int32_t failRc; // result reported for StateErrorE - SecHmacT txMac; // HMAC state primed with the send MAC key - SecHmacT rxMac; // HMAC state primed with the receive MAC key + SecX25519JobT job; // the X25519 under way (curveStep) + uint8_t secretKey[SEC_X25519_KEY_SIZE]; // ours, for the exchange under way + uint8_t txKey[SEC_AEAD_KEY_SIZE]; + uint8_t rxKey[SEC_AEAD_KEY_SIZE]; uint8_t passSalt[SEC_SHA256_SIZE]; uint8_t peerConfirm[SEC_SHA256_SIZE]; + uint8_t earlyConfirm[SEC_SHA256_SIZE]; // the peer's, come before our secret was computed uint8_t myHello[HELLO_SIZE]; uint8_t peerHello[HELLO_SIZE]; uint8_t myConfirm[CONFIRM_SIZE]; bool plainChannel[SECLINK_NUM_CHANNELS]; bool hasPass; + bool haveKeys; // session keys (txKey, rxKey) derived bool havePeerHello; // stored HELLO not yet used + bool keysPending; // our public key still to compute (our HELLO not ready) + bool secretPending; // the shared secret still to compute + bool jobRunning; // job holds the X25519 of keysPending or secretPending bool helloPending; // our HELLO still to be queued bool confirmPending; // our CONFIRM still to be queued bool peerConfirmed; + bool haveEarlyConfirm; bool closePending; bool established; // a session was READY once; clear frames stay refused until close }; @@ -188,8 +193,10 @@ struct SecLinkS { // ======================================================================== static void beginExchange(SecLinkT *link); -static void computeMac(const SecHmacT *primed, uint64_t seq, const uint8_t *hdr, const uint8_t *body, int32_t len, uint8_t *tag); -static void deriveKeys(SecLinkT *link); +static void confirmCheck(SecLinkT *link, const uint8_t *mac); +static void curveStep(SecLinkT *link); +static void deriveFinish(SecLinkT *link, const uint8_t *secret); +static void deriveStart(SecLinkT *link); static void destroyLink(SecLinkT *link); static void dropSession(SecLinkT *link); static void enterCall(SecLinkT *link); @@ -202,15 +209,18 @@ static void internalReset(void *ctx); static int leaveCall(SecLinkT *link, int32_t rc); static SecLinkT *linkCreate(int32_t com, SecLinkRecvT callback, void *ctx); static SecLinkT *linkFinish(SecLinkT *link); +static void nonceOf(uint64_t seq, uint8_t *nonce); static void onCipher(SecLinkT *link, const uint8_t *data, int len); static void onConfirm(SecLinkT *link, const uint8_t *data, int len); static void onHello(SecLinkT *link, const uint8_t *data, int len); static void onPlain(SecLinkT *link, const uint8_t *data, int len); +static bool peerSilent(const SecLinkT *link); static int pollOnce(SecLinkT *link); void secLinkClose(SecLinkT *link); int32_t secLinkGetDropped(SecLinkT *link); int secLinkGetPending(SecLinkT *link); int secLinkHandshake(SecLinkT *link); +int secLinkHandshakeStart(SecLinkT *link); int secLinkHandshakeStep(SecLinkT *link); bool secLinkIsReady(SecLinkT *link); SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx); @@ -226,140 +236,121 @@ int secLinkSetPassphrase(SecLinkT *link, const char *pass); // Static functions (alphabetical) // ======================================================================== -// Generate a fresh keypair and queue our HELLO. If the peer's HELLO is -// already stored the keys are derived right away. +// Start a fresh key pair: curveStep computes its public key and queues +// our HELLO (and then, with the peer's HELLO already stored, the secret +// is next). static void beginExchange(SecLinkT *link) { - int pubLen = SEC_DH_KEY_SIZE; // secDhGetPublicKey takes an int - - secDhDestroy(link->dh); - link->dh = secDhCreate(); - if (!link->dh) { - failHandshake(link, SECLINK_ERR_ALLOC); - return; - } - - // Cannot fail for a non-NULL context - secDhGenerateKeys(link->dh); - secDhGetPublicKey(link->dh, link->myHello + HELLO_PUB_OFS, &pubLen); + secRngBytes(link->secretKey, SEC_X25519_KEY_SIZE); + secX25519JobWipe(&link->job); + link->jobRunning = false; + link->exchangeStart = clock(); link->myHello[TYPE_OFS] = TYPE_HELLO; link->myHello[HELLO_VERSION_OFS] = SECLINK_PROTOCOL_VERSION; link->myHello[HELLO_FLAGS_OFS] = link->hasPass ? HELLO_FLAG_PASS : 0; link->state = StateHandshakeE; - link->helloPending = true; + link->keysPending = true; + link->secretPending = false; + link->helloPending = false; link->confirmPending = false; link->peerConfirmed = false; +} + + +// The peer's CONFIRM MAC against the one expected: confirmed, or the +// exchange fails (a wrong passphrase, or tampering). +static void confirmCheck(SecLinkT *link, const uint8_t *mac) { + if (!secConstTimeEqual(mac, link->peerConfirm, SEC_SHA256_SIZE)) { + failHandshake(link, SECLINK_ERR_AUTH); + return; + } + + link->peerConfirmed = true; flushHandshake(link); - - if (link->havePeerHello) { - deriveKeys(link); - } } -// tag = first SECLINK_MAC_SIZE bytes of HMAC(macKey, hdr || seq || body), -// where hdr is the type and channel bytes and seq is big-endian. primed -// already holds the state after the key blocks (set up once per session), -// so a frame costs no key-pad compressions. -static void computeMac(const SecHmacT *primed, uint64_t seq, const uint8_t *hdr, const uint8_t *body, int32_t len, uint8_t *tag) { - SecHmacT h = *primed; - uint8_t seqBytes[SEQ_BYTES]; - uint8_t full[SEC_SHA256_SIZE]; +// A slice of the X25519 under way, the slow part of an exchange: our +// public key (then our HELLO is queued and, with the peer's HELLO here, +// the secret is next), or the shared secret (then the session keys are +// derived). +static void curveStep(SecLinkT *link) { + uint8_t out[SEC_X25519_KEY_SIZE]; + bool ok; - for (int32_t i = 0; i < SEQ_BYTES; i++) { - seqBytes[i] = (uint8_t)(seq >> ((SEQ_BYTES - 1 - i) * 8)); + if (!link->keysPending && !link->secretPending) { + return; } - secHmacUpdate(&h, hdr, SECLINK_FRAME_HDR_SIZE); - secHmacUpdate(&h, seqBytes, SEQ_BYTES); - secHmacUpdate(&h, body, len); - secHmacFinal(&h, full); + if (!link->jobRunning) { + secX25519JobStart(&link->job, link->secretKey, link->keysPending ? NULL : link->peerHello + HELLO_PUB_OFS); + link->jobRunning = true; + } - memcpy(tag, full, SECLINK_MAC_SIZE); - secSecureZero(full, sizeof(full)); + if (!secX25519JobStep(&link->job)) { + return; + } + + link->jobRunning = false; + ok = secX25519JobResult(&link->job, out); + + if (link->keysPending) { + link->keysPending = false; + link->helloPending = true; + memcpy(link->myHello + HELLO_PUB_OFS, out, SEC_X25519_KEY_SIZE); + flushHandshake(link); + + if (link->havePeerHello) { + deriveStart(link); + } + + return; + } + + link->secretPending = false; + + if (!ok) { + failHandshake(link, SECLINK_ERR_HANDSHAKE); + } else { + deriveFinish(link, out); + } + + secSecureZero(out, sizeof(out)); } -// Both HELLOs are present (ours in myHello, the peer's in peerHello): -// validate, derive the session keys and queue our CONFIRM. The new keys -// replace any previous session's immediately; the peer's frames under the -// old keys all arrived before its HELLO (the packet layer is in order). -static void deriveKeys(SecLinkT *link) { - uint8_t secret[SEC_DH_KEY_SIZE]; +// The secret is computed: derive the session keys and queue our CONFIRM. +// The new keys replace any previous session's immediately; the peer's +// frames under the old keys all arrived before its HELLO (the packet layer +// is in order). +static void deriveFinish(SecLinkT *link, const uint8_t *secret) { uint8_t info[KDF_INFO_SIZE]; uint8_t keys[KEY_BLOCK_SIZE]; uint8_t transcript[SEC_SHA256_SIZE]; uint8_t roleMsg[SEC_SHA256_SIZE + 1]; SecSha256T sha; - SecCipherT *tx; - SecCipherT *rx; const uint8_t *lowHello; const uint8_t *highHello; - int secretLen = SEC_DH_KEY_SIZE; // secDhGetSecret takes an int - int32_t cmp; bool weAreLow; - bool peerHasPass; - link->havePeerHello = false; - - if (link->peerHello[HELLO_VERSION_OFS] != SECLINK_PROTOCOL_VERSION) { - failHandshake(link, SECLINK_ERR_HANDSHAKE); - return; - } - - // No downgrade: a passphrase on either side requires one on both. Our - // own flag is read from the HELLO we sent, the same one the peer sees - // and the transcript binds. - peerHasPass = (link->peerHello[HELLO_FLAGS_OFS] & HELLO_FLAG_PASS) != 0; - if (peerHasPass != ((link->myHello[HELLO_FLAGS_OFS] & HELLO_FLAG_PASS) != 0)) { - failHandshake(link, SECLINK_ERR_AUTH); - return; - } - - // Identical keys mean our own HELLO was reflected back at us. - cmp = memcmp(link->myHello + HELLO_PUB_OFS, link->peerHello + HELLO_PUB_OFS, SEC_DH_KEY_SIZE); - if (cmp == 0) { - failHandshake(link, SECLINK_ERR_HANDSHAKE); - return; - } - weAreLow = cmp < 0; + weAreLow = memcmp(link->myHello + HELLO_PUB_OFS, link->peerHello + HELLO_PUB_OFS, SEC_X25519_KEY_SIZE) < 0; lowHello = weAreLow ? link->myHello : link->peerHello; highHello = weAreLow ? link->peerHello : link->myHello; - if (secDhComputeSecret(link->dh, link->peerHello + HELLO_PUB_OFS, SEC_DH_KEY_SIZE) != SEC_SUCCESS || secDhGetSecret(link->dh, secret, &secretLen) != SEC_SUCCESS) { - failHandshake(link, SECLINK_ERR_HANDSHAKE); - return; - } - - // Private key no longer needed (forward secrecy) - secDhDestroy(link->dh); - link->dh = 0; + // Secret key no longer needed (forward secrecy) + secSecureZero(link->secretKey, SEC_X25519_KEY_SIZE); memcpy(info, KDF_LABEL, KDF_LABEL_LEN); - memcpy(info + KDF_LABEL_LEN, lowHello + HELLO_PUB_OFS, SEC_DH_KEY_SIZE); - memcpy(info + KDF_LABEL_LEN + SEC_DH_KEY_SIZE, highHello + HELLO_PUB_OFS, SEC_DH_KEY_SIZE); - secHkdf(link->hasPass ? link->passSalt : 0, link->hasPass ? SEC_SHA256_SIZE : 0, secret, secretLen, info, KDF_INFO_SIZE, keys, KEY_BLOCK_SIZE); - secSecureZero(secret, sizeof(secret)); + memcpy(info + KDF_LABEL_LEN, lowHello + HELLO_PUB_OFS, SEC_X25519_KEY_SIZE); + memcpy(info + KDF_LABEL_LEN + SEC_X25519_KEY_SIZE, highHello + HELLO_PUB_OFS, SEC_X25519_KEY_SIZE); + secHkdf(link->hasPass ? link->passSalt : 0, link->hasPass ? SEC_SHA256_SIZE : 0, secret, SEC_X25519_KEY_SIZE, info, KDF_INFO_SIZE, keys, KEY_BLOCK_SIZE); - tx = secCipherCreate(keys + (weAreLow ? KEY_ENC_LOW_OFS : KEY_ENC_HIGH_OFS)); - rx = secCipherCreate(keys + (weAreLow ? KEY_ENC_HIGH_OFS : KEY_ENC_LOW_OFS)); - if (!tx || !rx) { - secCipherDestroy(tx); - secCipherDestroy(rx); - secSecureZero(keys, sizeof(keys)); - failHandshake(link, SECLINK_ERR_ALLOC); - return; - } - - secCipherDestroy(link->txCipher); - secCipherDestroy(link->rxCipher); - link->txCipher = tx; - link->rxCipher = rx; - secHmacInit(&link->txMac, keys + (weAreLow ? KEY_MAC_LOW_OFS : KEY_MAC_HIGH_OFS), MAC_KEY_SIZE); - secHmacInit(&link->rxMac, keys + (weAreLow ? KEY_MAC_HIGH_OFS : KEY_MAC_LOW_OFS), MAC_KEY_SIZE); - link->txSeq = 0; - link->rxSeq = 0; + memcpy(link->txKey, keys + (weAreLow ? KEY_LOW_OFS : KEY_HIGH_OFS), SEC_AEAD_KEY_SIZE); + memcpy(link->rxKey, keys + (weAreLow ? KEY_HIGH_OFS : KEY_LOW_OFS), SEC_AEAD_KEY_SIZE); + link->haveKeys = true; + link->txSeq = 0; + link->rxSeq = 0; // Transcript binds both complete HELLOs (versions, flags, keys) secSha256Init(&sha); @@ -381,6 +372,47 @@ static void deriveKeys(SecLinkT *link) { link->peerConfirmed = false; link->confirmPending = true; flushHandshake(link); + + // The peer finished first: its CONFIRM waited for ours + if (link->haveEarlyConfirm) { + link->haveEarlyConfirm = false; + confirmCheck(link, link->earlyConfirm); + } +} + + +// Both HELLOs are present (ours in myHello, the peer's in peerHello): +// validate, then curveStep computes the secret (then deriveFinish). A +// newer HELLO from the peer before then is the one used: a secret under +// way with the older one starts over. +static void deriveStart(SecLinkT *link) { + bool peerHasPass; + + link->havePeerHello = false; + + if (link->peerHello[HELLO_VERSION_OFS] != SECLINK_PROTOCOL_VERSION) { + failHandshake(link, SECLINK_ERR_HANDSHAKE); + return; + } + + // No downgrade: a passphrase on either side requires one on both. Our + // own flag is read from the HELLO we sent, the same one the peer sees + // and the transcript binds. + peerHasPass = (link->peerHello[HELLO_FLAGS_OFS] & HELLO_FLAG_PASS) != 0; + if (peerHasPass != ((link->myHello[HELLO_FLAGS_OFS] & HELLO_FLAG_PASS) != 0)) { + failHandshake(link, SECLINK_ERR_AUTH); + return; + } + + // Identical keys mean our own HELLO was reflected back at us. + if (memcmp(link->myHello + HELLO_PUB_OFS, link->peerHello + HELLO_PUB_OFS, SEC_X25519_KEY_SIZE) == 0) { + failHandshake(link, SECLINK_ERR_HANDSHAKE); + return; + } + + secX25519JobWipe(&link->job); + link->jobRunning = false; + link->secretPending = true; } @@ -401,19 +433,21 @@ static void destroyLink(SecLinkT *link) { // Forget all key material and any exchange in progress. static void dropSession(SecLinkT *link) { - secDhDestroy(link->dh); - secCipherDestroy(link->txCipher); - secCipherDestroy(link->rxCipher); - link->dh = 0; - link->txCipher = 0; - link->rxCipher = 0; - link->havePeerHello = false; - link->helloPending = false; - link->confirmPending = false; - link->peerConfirmed = false; - secSecureZero(&link->txMac, sizeof(link->txMac)); - secSecureZero(&link->rxMac, sizeof(link->rxMac)); + secX25519JobWipe(&link->job); + link->jobRunning = false; + link->haveKeys = false; + link->keysPending = false; + link->secretPending = false; + link->haveEarlyConfirm = false; + link->havePeerHello = false; + link->helloPending = false; + link->confirmPending = false; + link->peerConfirmed = false; + secSecureZero(link->secretKey, SEC_X25519_KEY_SIZE); + secSecureZero(link->txKey, SEC_AEAD_KEY_SIZE); + secSecureZero(link->rxKey, SEC_AEAD_KEY_SIZE); secSecureZero(link->peerConfirm, SEC_SHA256_SIZE); + secSecureZero(link->earlyConfirm, SEC_SHA256_SIZE); secSecureZero(link->myConfirm, CONFIRM_SIZE); } @@ -546,9 +580,6 @@ static int leaveCall(SecLinkT *link, int32_t rc) { } -// Encrypted data: verify the MAC against the next receive sequence number, -// then decrypt. The sequence advances for every encrypted frame so a -// rejected (tampered or injected) frame does not shift the frames after it. // A new link (no packet layer yet); com is its port, or -1 for a stream. static SecLinkT *linkCreate(int32_t com, SecLinkRecvT callback, void *ctx) { SecLinkT *link = (SecLinkT *)calloc(1, sizeof(SecLinkT)); @@ -580,31 +611,40 @@ static SecLinkT *linkFinish(SecLinkT *link) { } +// The nonce of the frame numbered seq: the number, little-endian, then zeros. +static void nonceOf(uint64_t seq, uint8_t *nonce) { + memset(nonce, 0, SEC_AEAD_NONCE_SIZE); + + for (int32_t i = 0; i < NONCE_SEQ_BYTES; i++) { + nonce[i] = (uint8_t)(seq >> (i * 8)); + } +} + + +// Encrypted data: check the tag against the next receive sequence number, +// decrypting only when it matches. The sequence advances for every +// encrypted frame so a rejected (tampered or injected) frame does not +// shift the frames after it. static void onCipher(SecLinkT *link, const uint8_t *data, int len) { - uint8_t plain[PKT_MAX_PAYLOAD]; - uint8_t tag[SECLINK_MAC_SIZE]; - uint64_t seq; - int32_t bodyLen = len - SECLINK_FRAME_HDR_SIZE - SECLINK_MAC_SIZE; - uint8_t channel; + uint8_t plain[PKT_MAX_PAYLOAD]; + uint8_t nonce[SEC_AEAD_NONCE_SIZE]; + int32_t bodyLen = len - SECLINK_FRAME_HDR_SIZE - SECLINK_MAC_SIZE; + uint8_t channel; - if (!link->rxCipher || bodyLen < 0) { + if (!link->haveKeys || bodyLen < 0) { link->dropped++; return; } - seq = link->rxSeq++; - computeMac(&link->rxMac, seq, data, data + SECLINK_FRAME_HDR_SIZE, bodyLen, tag); + nonceOf(link->rxSeq++, nonce); channel = data[DATA_CHANNEL_OFS]; + memcpy(plain, data + SECLINK_FRAME_HDR_SIZE, bodyLen); - if (!secConstTimeEqual(tag, data + SECLINK_FRAME_HDR_SIZE + bodyLen, SECLINK_MAC_SIZE) || channel > SECLINK_MAX_CHANNEL) { + if (channel > SECLINK_MAX_CHANNEL || !secAeadUnlock(link->rxKey, nonce, data, SECLINK_FRAME_HDR_SIZE, plain, bodyLen, data + SECLINK_FRAME_HDR_SIZE + bodyLen)) { link->dropped++; return; } - memcpy(plain, data + SECLINK_FRAME_HDR_SIZE, bodyLen); - secCipherSetCounter(link->rxCipher, seq << CTR_FRAME_SHIFT); - secCipherCrypt(link->rxCipher, plain, bodyLen); - if (link->userCallback) { link->userCallback(link->userCtx, plain, bodyLen, channel, true); } @@ -615,19 +655,21 @@ static void onCipher(SecLinkT *link, const uint8_t *data, int len) { // The peer's CONFIRM is only meaningful while we wait for it; a mismatch // means a different passphrase or a tampered HELLO. +// A CONFIRM that comes before our secret is computed (the peer was +// faster) is kept until deriveFinish can check it. static void onConfirm(SecLinkT *link, const uint8_t *data, int len) { + if (len == CONFIRM_SIZE && link->state == StateHandshakeE && link->secretPending) { + memcpy(link->earlyConfirm, data + CONFIRM_MAC_OFS, SEC_SHA256_SIZE); + link->haveEarlyConfirm = true; + return; + } + if (link->state != StateConfirmE || len != CONFIRM_SIZE) { link->dropped++; return; } - if (!secConstTimeEqual(data + CONFIRM_MAC_OFS, link->peerConfirm, SEC_SHA256_SIZE)) { - failHandshake(link, SECLINK_ERR_AUTH); - return; - } - - link->peerConfirmed = true; - flushHandshake(link); + confirmCheck(link, data + CONFIRM_MAC_OFS); } @@ -646,8 +688,9 @@ static void onHello(SecLinkT *link, const uint8_t *data, int len) { memcpy(link->peerHello, data, HELLO_SIZE); link->havePeerHello = true; - if (link->state == StateHandshakeE) { - deriveKeys(link); + // Our public key still to compute: curveStep goes on to the secret + if (link->state == StateHandshakeE && !link->keysPending) { + deriveStart(link); } else if (rekey) { beginExchange(link); } @@ -668,7 +711,7 @@ static void onPlain(SecLinkT *link, const uint8_t *data, int len) { } channel = data[DATA_CHANNEL_OFS]; - protectedLink = link->established || link->rxCipher; + protectedLink = link->established || link->haveKeys; if (channel > SECLINK_MAX_CHANNEL || (protectedLink && !link->plainChannel[channel])) { link->dropped++; return; @@ -680,6 +723,17 @@ static void onPlain(SecLinkT *link, const uint8_t *data, int len) { } +// The line died while we still waited for the peer's HELLO, and as late +// as the packet layer gives up on a peer that says nothing: the peer never +// answered. A dead port, or a stream the other end closed, ends the +// exchange sooner and is a failure, not silence. +static bool peerSilent(const SecLinkT *link) { + clock_t waited = clock() - link->exchangeStart; + + return link->state == StateHandshakeE && !link->havePeerHello && !link->secretPending && waited >= (clock_t)PKT_LINK_TIMEOUT_MS * CLOCKS_PER_SEC / MS_PER_SEC; +} + + // One receive pass plus pending handshake output. Packet-layer codes are // mapped so callers only ever see SECLINK_* errors. static int pollOnce(SecLinkT *link) { @@ -690,6 +744,7 @@ static int pollOnce(SecLinkT *link) { } if (!link->closePending) { + curveStep(link); flushHandshake(link); } @@ -745,19 +800,11 @@ int secLinkHandshake(SecLinkT *link) { enterCall(link); - // An exchange already in flight (a timed-out earlier call, or one the - // peer started) is resumed rather than restarted, so the peer never - // sees two competing HELLOs from us. A ready link is re-keyed. - if (link->state != StateHandshakeE && link->state != StateConfirmE) { - beginExchange(link); - } - - // The wait is bounded: pktPoll only reports a disconnect for a closed - // port, never for an open but silent line. + // The wait is bounded: the packet layer reports a silent line as lost + // only after PKT_LINK_TIMEOUT_MS. start = clock(); - timeout = (clock_t)HANDSHAKE_TIMEOUT_MS * CLOCKS_PER_SEC / 1000; - - rc = handshakeStatus(link); + timeout = (clock_t)SECLINK_HANDSHAKE_TIMEOUT_MS * CLOCKS_PER_SEC / MS_PER_SEC; + rc = secLinkHandshakeStart(link); while (rc == SECLINK_PENDING) { if (clock() - start >= timeout) { @@ -771,10 +818,31 @@ int secLinkHandshake(SecLinkT *link) { } +// An exchange already in flight (a timed-out earlier call, or one the peer +// started) is resumed rather than restarted, so the peer never sees two +// competing HELLOs from us. A ready link is re-keyed. +int secLinkHandshakeStart(SecLinkT *link) { + if (!link) { + return SECLINK_ERR_PARAM; + } + + enterCall(link); + + if (link->state != StateHandshakeE && link->state != StateConfirmE) { + beginExchange(link); + } + + return leaveCall(link, handshakeStatus(link)); +} + + // Non-blocking form: starts an exchange on an idle link, then polls once. -// A failed exchange stays failed (secLinkHandshake starts a new one). The same poll may deliver the peer's CONFIRM and then its -// hang-up (a proxy exits as soon as it sees a passphrase mismatch): the -// exchange's outcome wins over the disconnect. +// A failed exchange stays failed (secLinkHandshake starts a new one). The +// same poll may deliver the peer's CONFIRM and then its hang-up (a proxy +// exits as soon as it sees a passphrase mismatch): the exchange's outcome +// wins over the disconnect. A line that went dead with the peer silent +// all along is SECLINK_ERR_TIMEOUT (peerSilent), any other dead line +// SECLINK_ERR_HANDSHAKE. int secLinkHandshakeStep(SecLinkT *link) { if (!link) { return SECLINK_ERR_PARAM; @@ -787,9 +855,11 @@ int secLinkHandshakeStep(SecLinkT *link) { } if (pollOnce(link) == SECLINK_ERR_SERIAL && handshakeStatus(link) == SECLINK_PENDING) { + int32_t rc = peerSilent(link) ? SECLINK_ERR_TIMEOUT : SECLINK_ERR_HANDSHAKE; + dropSession(link); link->state = StateInitE; - return leaveCall(link, SECLINK_ERR_HANDSHAKE); + return leaveCall(link, rc); } if (link->closePending) { @@ -897,11 +967,10 @@ int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, b frameLen = SECLINK_FRAME_HDR_SIZE + len; if (encrypt) { - uint64_t seq = link->txSeq++; + uint8_t nonce[SEC_AEAD_NONCE_SIZE]; - secCipherSetCounter(link->txCipher, seq << CTR_FRAME_SHIFT); - secCipherCrypt(link->txCipher, buf + SECLINK_FRAME_HDR_SIZE, len); - computeMac(&link->txMac, seq, buf, buf + SECLINK_FRAME_HDR_SIZE, len, buf + frameLen); + nonceOf(link->txSeq++, nonce); + secAeadLock(link->txKey, nonce, buf, SECLINK_FRAME_HDR_SIZE, buf + SECLINK_FRAME_HDR_SIZE, len, buf + frameLen); frameLen += SECLINK_MAC_SIZE; } diff --git a/src/libs/kpunch/serial/seclink/secLink.h b/src/libs/kpunch/seclink/secLink.h similarity index 83% rename from src/libs/kpunch/serial/seclink/secLink.h rename to src/libs/kpunch/seclink/secLink.h index 26e9c2dd..78842f4a 100644 --- a/src/libs/kpunch/serial/seclink/secLink.h +++ b/src/libs/kpunch/seclink/secLink.h @@ -26,30 +26,31 @@ // three layers into one: // rs232 -- ISR-driven UART I/O with ring buffers // packet -- HDLC framing + CRC-16 + Go-Back-N ARQ (reliable delivery) -// security -- DH key exchange, XTEA-CTR, SHA-256/HMAC/HKDF, DRBG +// security -- X25519, XChaCha20-Poly1305, SHA-256/HMAC/HKDF, DRBG // // Usage: // 1. secLinkOpen() -- opens COM port, sets up packet framing // 2. secLinkSetPassphrase() -- optional shared secret (both sides or none) -// 3. secLinkHandshake() -- authenticated DH key exchange +// 3. secLinkHandshake() -- authenticated X25519 key exchange // 4. secLinkSend() -- send data (optionally encrypted) on a channel // 5. secLinkPoll() -- receive, verify, decrypt, deliver to callback // 6. secLinkClose() -- tear everything down // -// Protocol (version 2): +// Protocol (version 3): // Every packet starts with a type byte. Handshake messages (HELLO, // CONFIRM) are never delivered to the application and application data // is never taken as key material. HELLO carries the protocol version, -// flags (passphrase in use) and the DH public key; CONFIRM carries an -// HMAC over the handshake transcript that proves both sides derived the -// same keys (and, with a passphrase, that both know it). Session keys -// come from HKDF-SHA256 over the DH secret, salted with SHA-256 of the -// passphrase, and are independent per direction. +// flags (passphrase in use) and the X25519 public key; CONFIRM carries +// an HMAC over the handshake transcript that proves both sides derived +// the same keys (and, with a passphrase, that both know it). Session +// keys come from HKDF-SHA256 over the X25519 secret, salted with +// SHA-256 of the passphrase, and are independent per direction. // -// Encrypted data frames are XTEA-CTR encrypted and carry an 8-byte -// truncated HMAC-SHA256 over the frame type, channel, an implicit 64-bit -// per-direction sequence number and the ciphertext. Frames with a bad -// MAC (tampered, replayed or injected) are dropped and counted. +// Encrypted data frames are XChaCha20-Poly1305: the type and channel +// bytes are authenticated, the payload encrypted, and the nonce is an +// implicit 64-bit per-direction sequence number, so a 16-byte tag +// covers all of it. Frames with a bad tag (tampered, replayed or +// injected) are dropped and counted. // // Passphrase: // Without a passphrase on either side the link is encrypted and @@ -77,13 +78,13 @@ #include #include -#include "../packet/packet.h" +#include "packet.h" +#include "../crypto/secCurve.h" // Error codes #define SECLINK_SUCCESS 0 #define SECLINK_ERR_PARAM (-1) #define SECLINK_ERR_SERIAL (-2) -#define SECLINK_ERR_ALLOC (-3) #define SECLINK_ERR_HANDSHAKE (-4) #define SECLINK_ERR_NOT_READY (-5) #define SECLINK_ERR_SEND (-6) @@ -91,14 +92,19 @@ #define SECLINK_ERR_AUTH (-8) // passphrase mismatch or tampering #define SECLINK_PENDING 1 // secLinkHandshakeStep: exchange still running +// secLinkHandshake gives up after this long. The peer may be slow to +// answer on slow hardware, but a silent peer (proxy or BBS not running) +// must not hang the caller forever. +#define SECLINK_HANDSHAKE_TIMEOUT_MS 30000 + // Wire protocol version carried in HELLO -#define SECLINK_PROTOCOL_VERSION 2 +#define SECLINK_PROTOCOL_VERSION 3 // Data frame header: type byte + channel byte #define SECLINK_FRAME_HDR_SIZE 2 -// Truncated HMAC-SHA256 tag appended to every encrypted data frame -#define SECLINK_MAC_SIZE 8 +// Poly1305 tag appended to every encrypted data frame +#define SECLINK_MAC_SIZE SEC_AEAD_TAG_SIZE // Max payload per send (packet max minus frame header and MAC). The same // limit applies to clear and encrypted sends so callers need one constant. @@ -158,12 +164,20 @@ int secLinkGetPending(SecLinkT *link); // ready (re-key) or has its own handshake pending. int secLinkHandshake(SecLinkT *link); +// Start an exchange as secLinkHandshake does (one in flight is resumed; a +// ready link is re-keyed; a failed one starts over), not waiting: run it +// with secLinkHandshakeStep, giving up when the caller's limit +// (SECLINK_HANDSHAKE_TIMEOUT_MS, say) passes. Returns as the step does. +int secLinkHandshakeStart(SecLinkT *link); + // Non-blocking handshake for callers with their own event loop: starts an // exchange on an idle link (never opened one, or dropped by a peer reset), // polls once and returns SECLINK_SUCCESS once the link is ready, // SECLINK_PENDING while the exchange is still running, or the error that // ended it (as secLinkHandshake; it keeps being returned, and -// secLinkHandshake starts over). Call it again after SECLINK_PENDING. +// secLinkHandshake or secLinkHandshakeStart starts over). A line the +// packet layer gave up on while the peer had not answered at all is +// SECLINK_ERR_TIMEOUT. Call it again after SECLINK_PENDING. int secLinkHandshakeStep(SecLinkT *link); // Returns true if handshake is complete and link is ready for data. diff --git a/src/libs/kpunch/seclink/seclink.dep b/src/libs/kpunch/seclink/seclink.dep new file mode 100644 index 00000000..a31094b4 --- /dev/null +++ b/src/libs/kpunch/seclink/seclink.dep @@ -0,0 +1,3 @@ +crypto +serial +libdvx diff --git a/src/libs/kpunch/seclink/seclink.dhs b/src/libs/kpunch/seclink/seclink.dhs new file mode 100644 index 00000000..80dace5f --- /dev/null +++ b/src/libs/kpunch/seclink/seclink.dhs @@ -0,0 +1,551 @@ +# 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. + +.section Technical Reference (for Developers)|Libraries +.topic lib.seclink +.title Secure Link Library +.toc 0 Secure Link Library +.index secLink +.index secLinkOpen +.index secLinkOpenStream +.index secLinkClose +.index secLinkHandshake +.index secLinkHandshakeStep +.index secLinkSend +.index secLinkPoll +.index secLinkSetPassphrase +.index secLinkSetChannelPlain + +.h1 Secure Link Library + +Top-level API composing all three lower layers (rs232, packet, security) into a single interface. Provides an authenticated key exchange, channel multiplexing (0-127), and per-packet encryption with integrity protection over a reliable serial link. + +Loaded as: bin/libs/kpunch/seclink/seclink.lib (part of Communications; needs the serial and cryptography libraries). The packet transport below it is in the same library. + +Header: seclink/secLink.h + +.link lib.seclink.packet Packet Transport +.link lib.seclink.example End-to-End Example +.link lib.serial.rs232 RS-232 UART Driver (the serial library) +.link lib.crypto Cryptography Library + +.h2 Protocol (version 3) + +Every packet starts with a type byte: + +.table + Type Layout Purpose + ---- ------ ------- + HELLO type, version, flags, X25519 public key (32 bytes) Starts a key exchange. Flag bit 0: passphrase in use. + CONFIRM type, HMAC-SHA256 (32 bytes) Proves both sides derived the same keys. + PLAIN type, channel, payload Clear application data. + CIPHER type, channel, ciphertext, tag (16 bytes) Encrypted, authenticated application data. +.endtable + +Handshake packets are never delivered to the application, and application data is never taken as key material. A HELLO is kept whenever it arrives, so the side that starts first is never missed. + +Key exchange: X25519 (RFC 7748). An X25519 takes about a sixth of a second on a 486, so each poll computes a ninth of one (about 20 ms there): first the side's public key, then the shared secret once both HELLOs are in, so the handshake never holds up other programs for long. A peer key that gives an all-zero secret fails the exchange (SECLINK_ERR_HANDSHAKE). + +Key derivation: HKDF-SHA256 with salt = SHA-256(passphrase) (32 zero bytes without one), input = the X25519 shared secret, and info = "DVX secLink v3" followed by the lower then the higher public key. The output supplies a 32-byte sending key for each direction plus a confirmation key; the side with the lower public key sends with the "low" key. Directions never share a key. + +Confirmation: each side sends HMAC(confirmation key, SHA-256(lower HELLO + higher HELLO) + role byte). A different passphrase, a passphrase on only one side, or a modified HELLO makes the check fail on both sides with SECLINK_ERR_AUTH. A side that has a passphrase never accepts a peer without one. + +Encrypted frames: XChaCha20-Poly1305. Each direction numbers its encrypted frames with an implicit 64-bit sequence number that is never sent; a frame's nonce is that number, little-endian, followed by 16 zero bytes. The type and channel bytes are authenticated (associated data), the payload is encrypted, and the 16-byte Poly1305 tag follows it. Every frame is sealed on its own, so tampered, replayed, or injected frames fail the tag, are dropped and counted (secLinkGetDropped), and later frames still verify. + +Without a passphrase the link is encrypted and integrity-protected but not authenticated: an active attacker in the middle could run two separate exchanges. Use the same passphrase on both sides to rule that out. + +Clear frames are accepted only until the first session is established. From then on, until secLinkClose, a clear frame is delivered only on a channel marked plaintext with secLinkSetChannelPlain -- even after the session was dropped by a packet-layer reset or a failed re-key, which someone on the line can cause; so neither can turn a protected channel into a clear one. A re-key HELLO on a running session whose protocol version or passphrase flag cannot match is dropped (and counted) without ending the session. + +.h2 Constants + +.table + Constant Value Description + -------- ----- ----------- + SECLINK_PROTOCOL_VERSION 3 Version carried in HELLO; a mismatch fails the handshake. + SECLINK_FRAME_HDR_SIZE 2 Type byte plus channel byte. + SECLINK_MAC_SIZE 16 Poly1305 tag on encrypted frames (SEC_AEAD_TAG_SIZE). + SECLINK_MAX_PAYLOAD 237 Maximum payload per send (PKT_MAX_PAYLOAD minus header and tag), clear or encrypted. + SECLINK_MAX_CHANNEL 127 Highest valid channel number. + SECLINK_NUM_CHANNELS 128 Array size needed to hold one slot per channel (SECLINK_MAX_CHANNEL + 1). + SECLINK_CHAN_BUF_SIZE 4096 Recommended per-channel receive buffer size for callers that buffer inbound data between the receive callback and an application read. +.endtable + +.h2 Error Codes + +.table + Constant Value Description + -------- ----- ----------- + SECLINK_SUCCESS 0 Operation succeeded. + SECLINK_ERR_PARAM -1 Invalid parameter (NULL pointer, bad channel, bad length). + SECLINK_ERR_SERIAL -2 Serial port closed or disconnected (secLinkPoll). + SECLINK_ERR_HANDSHAKE -4 Handshake failed: invalid public key, protocol version mismatch, reflected HELLO, a dead port or a line that died once the peer answered, or the peer reset mid-exchange. + SECLINK_ERR_NOT_READY -5 No session keys yet (encrypted send attempted). + SECLINK_ERR_SEND -6 Packet send failed, the peer stopped acknowledging, or a non-blocking send found the window full. + SECLINK_ERR_TIMEOUT -7 The peer did not complete the handshake within 30 seconds (secLinkHandshake; the exchange stays pending), or the line went silent before it answered at all (secLinkHandshakeStep). + SECLINK_ERR_AUTH -8 Passphrase mismatch, passphrase on only one side, or tampering. + SECLINK_PENDING 1 secLinkHandshakeStep only: the exchange is still running. +.endtable + +.h2 SecLinkRecvT + +Receive callback type. Delivers plaintext with the channel number. encrypted is true when the frame arrived encrypted and its tag verified, false for a clear frame. + +.code +typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted); +.endcode + +.h2 secLinkOpen + +Open a secure serial link. Opens the COM port via rs232Open and creates a packet layer (default window size) with an internal receive dispatcher. The handshake must be performed separately via secLinkHandshake before encrypted data can be sent. + +.code +SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx); +.endcode + +.table + Parameter Description + --------- ----------- + com COM port index (RS232_COM1..RS232_COM4). + bps Baud rate. + dataBits Data bits per character (5-8). + parity Parity mode ('N', 'O', 'E', 'M', 'S', or lowercase). + stopBits Stop bits (1 or 2). + handshake Flow control mode (RS232_HANDSHAKE_*). + callback Called when a packet is received, after verification and decryption. The data pointer is valid only during the callback. + ctx User pointer passed to the callback. +.endtable + +Returns an opaque SecLinkT handle, or NULL on failure (allocation failure, rs232Open error, or pktOpen error). On failure all partially initialized resources are cleaned up. + +.h2 secLinkOpenStream + +Open a secure link over a byte stream (see pktOpenStream) instead of a COM port, e.g. a TCP connection to the host proxy's listening port. Everything else -- handshake, passphrase, channels, sending, and polling -- is the same as for a COM port link. secLinkClose does not close the stream; the caller closes it afterwards. + +.code +SecLinkT *secLinkOpenStream(const PktStreamT *stream, SecLinkRecvT callback, void *ctx); +.endcode + +.h2 secLinkClose + +Close the link and free all associated resources: wipe the key material, close the packet layer and the COM port, and free the link. Safe to call with a NULL link (no-op). Safe to call from inside the receive callback: the close then happens when the outermost secLink call returns, and no further packets are delivered. + +.code +void secLinkClose(SecLinkT *link); +.endcode + +.h2 secLinkSetPassphrase + +Set the shared passphrase for the next handshake. NULL or "" clears it. Only SHA-256 of the text is kept. Set it before calling secLinkHandshake; both sides must use the same passphrase, or both none. An exchange already in progress (for example one left pending by a timed-out secLinkHandshake) was started with the old setting, so it is abandoned: the next secLinkHandshake starts a fresh one. + +.code +int secLinkSetPassphrase(SecLinkT *link, const char *pass); +.endcode + +Returns SECLINK_SUCCESS or SECLINK_ERR_PARAM for a NULL link. + +.h2 secLinkHandshake + +Run the authenticated X25519 key exchange. Blocks, polling the packet layer, until both CONFIRM messages have been checked, or until 30 seconds pass. + +.code +int secLinkHandshake(SecLinkT *link); +.endcode + +Behavior: + +.list +.item A fresh X25519 key pair is made from the DRBG and our HELLO is sent. If the peer's HELLO already arrived (it started first), the shared secret follows on the next poll. +.item The X25519 secret key is wiped as soon as the session keys are derived (forward secrecy). +.item After SECLINK_ERR_TIMEOUT the exchange stays pending: it still completes during secLinkPoll if the peer shows up, and calling secLinkHandshake again resumes the wait without sending a new key. +.item On a ready link it negotiates fresh session keys. A peer that is already ready (or has an exchange pending) answers automatically from secLinkPoll. +.item Encrypted traffic under the previous keys that was sent before the new HELLO is still accepted; encrypted sends are refused (SECLINK_ERR_NOT_READY) until the new exchange completes. +.endlist + +Returns SECLINK_SUCCESS, SECLINK_ERR_PARAM, SECLINK_ERR_HANDSHAKE, SECLINK_ERR_TIMEOUT, or SECLINK_ERR_AUTH. When the peer confirms (or rejects) and hangs up in the same poll -- a proxy exits as soon as it sees a passphrase mismatch -- the exchange's outcome is reported, not the disconnect. + +.h2 secLinkHandshakeStep + +Non-blocking handshake for callers that run their own event loop. + +.code +int secLinkHandshakeStep(SecLinkT *link); +.endcode + +Starts an exchange on an idle link (never handshaken, or dropped by a peer reset), polls once, and returns SECLINK_SUCCESS once the link is ready, SECLINK_PENDING (1) while the exchange is still running, or the error that ended it (the same codes as secLinkHandshake). SECLINK_ERR_TIMEOUT here means the packet layer gave up on the line (PKT_LINK_TIMEOUT_MS) while the peer had not answered at all; the caller's own limit is its to keep. A failed exchange keeps returning its error; secLinkHandshake starts a new one. + +.h2 secLinkSend + +Send data on a channel, optionally encrypted and authenticated. + +.code +int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block); +.endcode + +.table + Parameter Description + --------- ----------- + link Connection handle from secLinkOpen. + data Plaintext payload. + len Payload length. Must be in 1..SECLINK_MAX_PAYLOAD. + channel Logical channel number (0..SECLINK_MAX_CHANNEL). + encrypt If true, seal (encrypt and tag) before sending (requires a completed handshake). + block If true, wait (pumping the receive callback) until the transmit window has space. If false, return SECLINK_ERR_SEND immediately when the window is full. +.endtable + +Returns SECLINK_SUCCESS on success or a negative error code (SECLINK_ERR_PARAM, SECLINK_ERR_NOT_READY, SECLINK_ERR_SEND). + +A frame takes its sequence number only once window space is guaranteed, and is queued without pumping in between, so frames always reach the wire in sequence order. The receive callback may itself send (blocking or not), including encrypted. + +Once session keys exist the receiver drops clear frames unless it marked the channel plaintext with secLinkSetChannelPlain. + +.h2 secLinkSendBuf + +Send an arbitrarily large buffer by splitting into SECLINK_MAX_PAYLOAD chunks. Always blocks until all data is sent. + +.code +int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt); +.endcode + +Returns SECLINK_SUCCESS or the first error encountered. + +.h2 secLinkSetChannelPlain + +Allow (plain = true) or refuse (plain = false, the default) clear frames on a channel once session keys exist. Before the first handshake clear frames are always accepted. + +.code +int secLinkSetChannelPlain(SecLinkT *link, uint8_t channel, bool plain); +.endcode + +Returns SECLINK_SUCCESS or SECLINK_ERR_PARAM for a bad link or channel. + +.h2 secLinkPoll + +Poll for incoming data. Drains the serial port, processes frames and retransmits, completes pending handshake steps, verifies and decrypts data frames, and forwards them to the callback. + +.code +int secLinkPoll(SecLinkT *link); +.endcode + +Returns the number of packets delivered by the packet layer this call, SECLINK_ERR_PARAM if link is NULL, or SECLINK_ERR_SERIAL when the port is closed or disconnected. Must be called frequently in the main loop. + +If the remote packet layer resets, the session keys are dropped (the link is no longer ready) and a new handshake is needed. Clear frames stay refused on protected channels (see Protocol). + +.h2 secLinkIsReady + +Check whether the handshake is complete and the link is ready for encrypted traffic. Cleartext sends do not require the link to be ready. + +.code +bool secLinkIsReady(SecLinkT *link); +.endcode + +.h2 secLinkGetDropped + +Number of received frames dropped since open: bad tag (tampered, replayed, or injected), clear frames on a protected channel, encrypted frames without session keys, and malformed frames. + +.code +int32_t secLinkGetDropped(SecLinkT *link); +.endcode + +Returns the count or SECLINK_ERR_PARAM when link is NULL. + +.h2 secLinkGetPending + +Get the number of unacknowledged packets in the transmit window. Delegates directly to pktGetPending on the underlying packet connection. Useful for non-blocking send loops to decide when to throttle. + +.code +int secLinkGetPending(SecLinkT *link); +.endcode + +Returns the pending count on success or SECLINK_ERR_PARAM when link is NULL. + +.topic lib.seclink.packet +.title Packet Transport +.toc 1 Packet Transport +.index packet +.index HDLC +.index CRC-16 +.index Go-Back-N +.index pktOpen +.index pktOpenStream +.index PktStreamT +.index pktClose +.index pktSend +.index pktPoll + +.h1 Packet Transport + +Reliable, ordered packet delivery over an RS-232 serial link. Uses HDLC-style byte-stuffed framing for frame delimiting, CRC-16-CCITT for error detection, and Go-Back-N ARQ with a configurable sliding window for retransmission. + +Header: seclink/packet.h + +.h2 Constants + +.table + Constant Value Description + -------- ----- ----------- + PKT_MAX_PAYLOAD 255 Maximum bytes per packet payload. + PKT_DEFAULT_WINDOW 4 Default sliding window size (unacknowledged frames in flight). + PKT_MAX_WINDOW 8 Maximum sliding window size. +.endtable + +.h2 Error Codes + +.table + Constant Value Description + -------- ----- ----------- + PKT_SUCCESS 0 Operation succeeded. + PKT_ERR_INVALID_PARAM -1 Invalid parameter (NULL handle, bad data pointer, length out of range). + PKT_ERR_TX_FULL -2 Transmit window is full (non-blocking send only). + PKT_ERR_DISCONNECTED -3 Serial port returned a negative read (disconnect/error), or the connection was closed from its own callback. + PKT_ERR_TIMEOUT -4 A blocking send got no acknowledgement for PKT_SEND_TIMEOUT_MS (15000 ms). +.endtable + +.h2 PktRecvCallbackT + +Callback type for received packets. + +.code +typedef void (*PktRecvCallbackT)(void *ctx, + const uint8_t *data, int len); +.endcode + +.table + Parameter Description + --------- ----------- + ctx User context pointer (passed to pktOpen). + data Payload bytes (valid only during the callback). + len Payload length. +.endtable + +.h2 pktOpen + +Open a packetized connection over an already-open COM port. Only one packet connection may exist on a given COM port at a time; a second pktOpen call on the same port returns NULL to avoid silent framing corruption. + +.code +PktConnT *pktOpen(int com, int windowSize, + PktRecvCallbackT callback, void *callbackCtx); +.endcode + +.table + Parameter Description + --------- ----------- + com COM port index (RS232_COM1..RS232_COM4). Must already be open via rs232Open. + windowSize Sliding window size (1..PKT_MAX_WINDOW). Pass 0 for PKT_DEFAULT_WINDOW. Values greater than PKT_MAX_WINDOW are clamped. + callback Called when a complete, CRC-verified, in-order packet is received. The data pointer is valid only during the callback. + callbackCtx User pointer passed to the callback. +.endtable + +Returns an opaque PktConnT handle, or NULL on failure (invalid port, duplicate open, or allocation failure). + +.h2 pktOpenStream + +Open a packetized connection over any byte stream instead of a COM port -- a TCP socket, for example. The stream is described by non-blocking calls and a context pointer, copied into the connection. Any number of stream connections may be open. + +.code +typedef struct { + int32_t (*read)(void *ctx, uint8_t *buf, int32_t len); + int32_t (*write)(void *ctx, const uint8_t *data, int32_t len); + void *ctx; + int32_t (*pending)(void *ctx); +} PktStreamT; + +PktConnT *pktOpenStream(const PktStreamT *stream, int windowSize, PktRecvCallbackT callback, void *callbackCtx); +.endcode + +.table + Member Description + ------ ----------- + read Up to len bytes into buf: the count, 0 when nothing has arrived, negative when the stream is gone (pktPoll then reports PKT_ERR_DISCONNECTED). + write Bytes taken from data (0 = none right now), negative when the stream is gone. Frames wait in the connection's transmit queue, and each pktPoll hands the stream what it takes; a frame that finds no room in the queue is dropped (a data frame is sent again by its retransmit timer). + ctx Passed to the calls. + pending Bytes taken but not yet on the wire (NULL: the stream does not say). Retransmit timers wait while there are any and their count keeps moving: a count that stays the same for 2 seconds (flow control holding the line) no longer holds them. +.endtable + +Returns the handle, or NULL for a NULL stream or call, or on allocation failure. pktClose never closes the stream. + +.h2 pktClose + +Close a packetized connection and release its registry slot. Does not close the underlying COM port. The caller is responsible for calling rs232Close separately when no longer needed. May be called from inside the receive callback: the connection is then freed when the outermost pktPoll returns (which reports PKT_ERR_DISCONNECTED). + +.code +void pktClose(PktConnT *conn); +.endcode + +.h2 pktSend + +Send a data packet. The payload is copied into an internal retransmit slot before transmission, so the caller may reuse the supplied buffer immediately. + +.code +int pktSend(PktConnT *conn, const uint8_t *data, + int len, bool block); +.endcode + +.table + Parameter Description + --------- ----------- + conn Connection handle from pktOpen. + data Payload bytes (1..PKT_MAX_PAYLOAD). + len Payload length. + block If true, wait for transmit window space via pktWaitSpace. If false, return PKT_ERR_TX_FULL immediately when the window is full. +.endtable + +Returns PKT_SUCCESS on success, PKT_ERR_INVALID_PARAM for NULL/out-of-range input, PKT_ERR_TX_FULL for a non-blocking call with a full window, PKT_ERR_DISCONNECTED if the serial port drops during a blocking wait, or PKT_ERR_TIMEOUT if the peer stops acknowledging. A frame the port accepts only partially (flow control) is completed before the call returns. + +.h2 pktPoll + +Poll for incoming data, process received frames, and handle retransmits. Must be called frequently in the main loop. The callback is invoked synchronously for each complete, CRC-verified, in-order data frame and may call pktSend or pktPoll again: received bytes are consumed from a per-connection buffer, so a nested call continues the same byte stream in order. After a gap in the sequence only one NAK is sent until the missing frame arrives (go-back-N resends the whole window on one NAK). + +.code +int pktPoll(PktConnT *conn); +.endcode + +Returns the number of valid data packets delivered to the callback this call, or PKT_ERR_INVALID_PARAM if conn is NULL, or PKT_ERR_DISCONNECTED if the serial layer returned a negative read. + +.h2 pktReset + +Reset the local connection state (sequence numbers, transmit slots, RX state machine) and send a RST frame to the remote side. The remote's RST handler clears its own state (discarding its unacknowledged frames), runs its reset callback, and answers with a RST reply. A RST reply that answers no reset of ours is ignored. Useful for recovering from a desynchronized connection. + +.code +int pktReset(PktConnT *conn); +.endcode + +Returns PKT_SUCCESS on success or PKT_ERR_INVALID_PARAM if conn is NULL. + +.h2 pktCanSend + +Check whether there is room in the transmit window for another packet. Returns false when conn is NULL or the transmit window is full. + +.code +bool pktCanSend(PktConnT *conn); +.endcode + +.h2 pktGetPending + +Get the number of unacknowledged packets currently in the transmit window. Ranges from 0 (all sent packets acknowledged) to windowSize (window full). + +.code +int pktGetPending(PktConnT *conn); +.endcode + +Returns the pending count on success, or PKT_ERR_INVALID_PARAM if conn is NULL. + +.h2 pktSetResetCallback + +Register a callback run when the remote side resets the connection (its unacknowledged frames are gone at that point, so an upper layer with per-frame state must resynchronize). The callback receives the ctx passed to pktOpen. NULL removes it. + +.code +typedef void (*PktResetCallbackT)(void *ctx); +void pktSetResetCallback(PktConnT *conn, PktResetCallbackT callback); +.endcode + +.h2 pktWaitSpace + +Poll until the transmit window has room. The wait restarts whenever an acknowledgement frees a slot, so only a peer that stops acknowledging times out. + +.code +int pktWaitSpace(PktConnT *conn); +.endcode + +Returns PKT_SUCCESS, PKT_ERR_INVALID_PARAM, PKT_ERR_DISCONNECTED, or PKT_ERR_TIMEOUT after PKT_SEND_TIMEOUT_MS without progress. + +.topic lib.seclink.example +.title End-to-End Example +.toc 1 End-to-End Example +.index Serial Example +.index BBS Client + +.h1 End-to-End Example + +Below is a complete secure BBS client that opens COM1, runs the authenticated key exchange, and then shuttles terminal traffic over a single encrypted channel. It demonstrates the typical serial stack usage: load the RNG seed file, open the link, set the passphrase, run the handshake, then enter a poll+send loop. + +.code +#include "rs232.h" +#include "packet.h" +#include "security.h" +#include "secLink.h" +#include +#include + +#define CHAN_TERMINAL 0 + +#define SEED_FILE "SECLINK.RND" + +static void onPacket(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted) { + (void)ctx; + if (channel == CHAN_TERMINAL && encrypted) { + fwrite(data, 1, len, stdout); + fflush(stdout); + } +} + + +int main(void) { + // Carry entropy over from the last run (hardware entropy is + // gathered automatically; feed key timings with secRngAddEntropy). + secRngLoadSeed(SEED_FILE); + + // Open the link at 115200 8N1 with RTS/CTS flow control. + SecLinkT *link = secLinkOpen(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_RTSCTS, onPacket, NULL); + if (!link) { + fprintf(stderr, "secLinkOpen failed\n"); + return 1; + } + + // Authenticated key exchange. The proxy must use the same passphrase. + secLinkSetPassphrase(link, "shared secret words"); + if (secLinkHandshake(link) != SECLINK_SUCCESS) { + fprintf(stderr, "Handshake failed\n"); + secLinkClose(link); + return 1; + } + + // Send an encrypted banner and then relay keyboard input. + const char *hello = "Hello, secure BBS!\r\n"; + secLinkSend(link, (const uint8_t *)hello, (int)strlen(hello), CHAN_TERMINAL, true, true); + + while (!feof(stdin)) { + // Deliver any incoming packets to onPacket(). + secLinkPoll(link); + + // If the user typed something, forward it encrypted. + int ch = getchar(); + if (ch == EOF) { + break; + } + uint8_t byte = (uint8_t)ch; + secLinkSend(link, &byte, 1, CHAN_TERMINAL, true, true); + } + + secLinkClose(link); + secRngSaveSeed(SEED_FILE); + return 0; +} +.endcode + +Notes: + +.list +.item secLinkOpen internally calls rs232Open and pktOpen. secLinkClose tears them down in reverse order. +.item The DRBG gathers hardware entropy on first use. On a CPU without a time stamp counter that alone is guessable; the seed file and user-input timings (secRngAddEntropy) are what make the X25519 keys unpredictable. +.item secLinkPoll must be called frequently to drain the RX ring buffer, process ACKs, and dispatch received packets to the callback. +.item For bulk transfers larger than SECLINK_MAX_PAYLOAD (237 bytes), use secLinkSendBuf which splits the data into chunks automatically. +.endlist diff --git a/src/libs/kpunch/serial/Makefile b/src/libs/kpunch/serial/Makefile index bb96528a..40a881bb 100644 --- a/src/libs/kpunch/serial/Makefile +++ b/src/libs/kpunch/serial/Makefile @@ -20,22 +20,21 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -# DVX Serial Stack Makefile for DJGPP cross-compilation +# Serial Library Makefile for DJGPP cross-compilation # -# Builds serial.lib -- combines rs232, packet, security, and seclink -# into a single DXE library loaded by the DVX system. +# serial.lib: the interrupt-driven UART driver (rs232/). The packets and +# secure link over it are seclink.lib (../seclink), the cryptography +# crypto.lib (../crypto). include ../../../common.mk -CFLAGS = $(DOS_CFLAGS) -Irs232 -Ipacket -Isecurity -Iseclink -I../libdvx -I../libdvx/platform -I../libtasks +CFLAGS = $(DOS_CFLAGS) -Irs232 -I../libdvx -I../libdvx/platform -I../libtasks -OBJDIR = ../../../../obj/serial -LIBSDIR = ../../../../bin/libs +OBJDIR = ../../../../obj/serial +LIBSDIR = ../../../../bin/libs -# One object per source; the source's subdirectory is dropped (the names -# are unique across rs232/, packet/, security/ and seclink/). -SRCS = rs232/rs232.c packet/packet.c security/secHash.c security/security.c security/secBas.c seclink/secLink.c -OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(SRCS))) +SRCS = rs232/rs232.c +OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(SRCS))) TARGETDIR = $(LIBSDIR)/kpunch/serial TARGET = $(TARGETDIR)/serial.lib @@ -64,4 +63,4 @@ $(TARGETDIR): -include $(OBJS:.o=.d) clean: - rm -rf $(OBJS) $(OBJS:.o=.d) $(TARGET) $(TARGETDIR)/serial.dep $(TARGETDIR) + rm -rf $(OBJDIR) $(TARGETDIR) diff --git a/src/libs/kpunch/serial/packet/README.md b/src/libs/kpunch/serial/packet/README.md deleted file mode 100644 index 80612027..00000000 --- a/src/libs/kpunch/serial/packet/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# packet -- Reliable HDLC Transport - -A reliable, ordered serial-transport layer. HDLC-style byte stuffing -(FLAG=0x7E, ESC=0x7D), CRC-16-CCITT integrity, and Go-Back-N sliding- -window retransmission over any byte-oriented link. Sits on top of the -rs232 driver (or the Linux socket shim used by the proxy). - -## Documentation - -Full reference is in the combined serial stack doc: -`../serial.dhs` (the `packet` section). It compiles into the DVX System -Reference -- open via the Help Viewer app, or read -`docs/dvx_system_reference.html` after `make`. - -Topics covered: connection open/close, send/poll, frame format, window -size, retransmit timer, CRC polynomial and initial value, error codes, -callback contract. - -## Source Files - -- `packet.h` -- public API and error codes -- `packet.c` -- implementation (framing, CRC, sliding window) - -## Build - -Built into `serial.lib` by `make -C src/libs/kpunch/serial` (invoked by -the top-level `make`). diff --git a/src/libs/kpunch/serial/rs232/rs232.c b/src/libs/kpunch/serial/rs232/rs232.c index a041e767..e9c2bad4 100644 --- a/src/libs/kpunch/serial/rs232/rs232.c +++ b/src/libs/kpunch/serial/rs232/rs232.c @@ -32,6 +32,8 @@ #include "rs232.h" #include "dvxPlat.h" +#include + // ABI stamp checked by the loader (see DVX_ABI_VERSION). DVX_ABI_STAMP(serial); @@ -48,8 +50,8 @@ DVX_ABI_STAMP(serial); #define OUTPW(a, w) (outportw((a), (w)), (w)) // Buffer sizes must be power of 2 so (index & MASK) wraps correctly. -// 2048 bytes balances memory usage (8KB per port for both buffers) against -// burst tolerance at 115200 bps. +// 2048 bytes balances memory usage (4 KB a port for both buffers, taken +// when it opens) against burst tolerance at 115200 bps. #define RX_BUFFER_BITS 11 // 2048 #define TX_BUFFER_BITS 11 // 2048 #define RX_BUFFER_SIZE (1L << RX_BUFFER_BITS) @@ -104,6 +106,11 @@ DVX_ABI_STAMP(serial); #define IRQ_NONE 0xFF #define IRQ_COUNT 16 +// findIrq: line-status reads to wait for the transmitter to empty before +// the UART counts as stuck (each an ISA bus cycle, about a microsecond: +// some 0.2 s, past one character at 110 bps) +#define FIND_IRQ_TX_POLLS 200000 + // Max valid UART port base (12-bit I/O address). #define UART_MAX_BASE 0xFFF @@ -124,7 +131,9 @@ DVX_ABI_STAMP(serial); #define PIC_MASTER 0x20 #define PIC_SLAVE 0xA0 #define PIC_EOI 0x20 +#define PIC_SPECIFIC_EOI 0x60 // OCW2: end the in-service level in its low bits #define PIC_RR 0x02 +#define PIC_RIS 0x01 // OCW3 with PIC_RR: read the in-service register #define PIC_MASTER_IRQ_MAX 7 // highest IRQ on the master 8259 #define PIC_CASCADE_IRQ 2 // master input the slave is wired to #define SLAVE_IRQ_BASE 8 // IRQ8 is slave bit 0 (IRQ8..15 -> slave IMR bit 0..7) @@ -316,8 +325,8 @@ typedef struct { uint8_t txCtrl; // XON/XOFF waiting for THR (TX_CTRL_NONE if none) uint8_t txBurst; // bytes the THR/FIFO takes per THRE interrupt uint8_t uartType; // RS232_UART_* probed at open - uint8_t rxBuff[(uint16_t)RX_BUFFER_SIZE]; - uint8_t txBuff[(uint16_t)TX_BUFFER_SIZE]; + uint8_t *rxBuff; // RX_BUFFER_SIZE bytes, allocated and locked while open + uint8_t *txBuff; // TX_BUFFER_SIZE bytes, the same uint32_t base; uint32_t dlatch; uint32_t rxHead; @@ -411,7 +420,11 @@ static void irqRestore(uint32_t flags); static uint32_t irqSave(void); static void mcrUpdate(Rs232StateT *port, uint8_t bits, bool set); static int32_t oneBitIndex(uint8_t mask); +static void picHealIrq(uint8_t irq); static uint8_t picReadIrr(uint16_t port); +static uint8_t picReadIsr(uint16_t port); +static void portBuffersFree(Rs232StateT *port); +static int32_t portBuffersGet(Rs232StateT *port); static int32_t probeUartType(Rs232StateT *port); static void queueTxCtrl(Rs232StateT *port, uint8_t ctrl); static void removeIrqHandler(int32_t irq); @@ -451,6 +464,7 @@ int32_t rs232SetRts(int32_t com, bool rts); int32_t rs232SetStop(int32_t com, int32_t stopBits); int32_t rs232Write(int32_t com, const char *data, int32_t len); int32_t rs232WriteBuf(int32_t com, const char *data, int32_t len); +static void uartRearm(Rs232StateT *port); // ======================================================================== @@ -748,8 +762,12 @@ static int32_t findIrq(Rs232StateT *com) { UART_WRITE_MCR(com, (UART_READ_MCR(com) & MCR_LINES) | MCR_OUT2); UART_WRITE_FCR(com, 0); - // Wait until TX hold register is empty - while ((UART_READ_LSR(com) & LSR_TX_HOLD_EMPTY) == 0) { + // Wait until TX hold register is empty; a transmitter that never + // empties (auto-CTS held off, a broken device) is no IRQ found + for (int32_t polls = 0; (UART_READ_LSR(com) & LSR_TX_HOLD_EMPTY) == 0; polls++) { + if (polls >= FIND_IRQ_TX_POLLS) { + return RS232_ERR_IRQ_NOT_FOUND; + } } flags = irqSave(); @@ -881,12 +899,86 @@ static int32_t oneBitIndex(uint8_t mask) { } +// With interrupts off, outside the handler: irq shown in service at the +// PIC had its end of interrupt never sent (seen in DOSBox under a serial +// flood), and the PIC then holds that IRQ and every lower one back for +// good. A specific EOI ends it (and the slave's cascade input on the +// master when it is the slave's). +static void picHealIrq(uint8_t irq) { + bool slave = irq > PIC_MASTER_IRQ_MAX; + uint16_t pic = slave ? PIC_SLAVE : PIC_MASTER; + uint8_t level = slave ? (uint8_t)(irq - SLAVE_IRQ_BASE) : irq; + + if (!(picReadIsr(pic) & (1 << level))) { + return; + } + + PIC_WRITE_OCW2(pic, PIC_SPECIFIC_EOI | level); + + if (slave && (picReadIsr(PIC_MASTER) & (1 << PIC_CASCADE_IRQ))) { + PIC_WRITE_OCW2(PIC_MASTER, PIC_SPECIFIC_EOI | PIC_CASCADE_IRQ); + } +} + + static uint8_t picReadIrr(uint16_t port) { PIC_WRITE_OCW3(port, PIC_RR); return inportb(port); } +// The in-service register; the PIC is left reading IRR (picReadIrr's +// mode, its default). +static uint8_t picReadIsr(uint16_t port) { + PIC_WRITE_OCW3(port, PIC_RR | PIC_RIS); + + uint8_t isr = inportb(port); + + PIC_WRITE_OCW3(port, PIC_RR); + return isr; +} + + +// Unlock and free the buffers portBuffersGet gave a port (its interrupt +// is off). +static void portBuffersFree(Rs232StateT *port) { + platformUnlockRegion(port->rxBuff, RX_BUFFER_SIZE, false); + platformUnlockRegion(port->txBuff, TX_BUFFER_SIZE, false); + free(port->rxBuff); + free(port->txBuff); + port->rxBuff = NULL; + port->txBuff = NULL; +} + + +// A port's buffers, taken as it opens and locked for its interrupt +// handler, so a port that is never opened costs no memory. The port has +// them only when both are locked. +static int32_t portBuffersGet(Rs232StateT *port) { + uint8_t *rx = (uint8_t *)calloc(1, RX_BUFFER_SIZE); + uint8_t *tx = (uint8_t *)calloc(1, TX_BUFFER_SIZE); + int32_t rc = RS232_ERR_NO_MEMORY; + + if (rx && tx) { + rc = RS232_ERR_LOCK_MEM; + + if (platformLockRegion(rx, RX_BUFFER_SIZE, false) == 0) { + if (platformLockRegion(tx, TX_BUFFER_SIZE, false) == 0) { + port->rxBuff = rx; + port->txBuff = tx; + return RS232_SUCCESS; + } + + platformUnlockRegion(rx, RX_BUFFER_SIZE, false); + } + } + + free(rx); + free(tx); + return rc; +} + + // Detect UART type by probing hardware features. The detection sequence: // 1. An unpopulated address reads 0xFF everywhere: no UART // 2. Write/read scratch register -- 8250 doesn't have one @@ -1010,6 +1102,7 @@ int32_t rs232Close(int32_t com) { irqRestore(flags); detachIrq(port); + portBuffersFree(port); return RS232_SUCCESS; } @@ -1240,6 +1333,7 @@ int32_t rs232GetTxBuffered(int32_t com) { flags = irqSave(); count = TX_COUNT(port); + uartRearm(port); irqRestore(flags); return count; @@ -1287,6 +1381,11 @@ int32_t rs232Open(int32_t com, int32_t bps, int32_t dataBits, char parity, int32 if (port->uartType == RS232_UART_UNKNOWN) { return RS232_ERR_NO_UART; } + rc = portBuffersGet(port); + if (rc != RS232_SUCCESS) { + return rc; + } + port->txBurst = port->uartType == RS232_UART_16550A ? FIFO_SIZE : NO_FIFO_BURST; port->rxFlowOn = 1; port->txFlowOn = 1; @@ -1305,6 +1404,7 @@ int32_t rs232Open(int32_t com, int32_t bps, int32_t dataBits, char parity, int32 rc = attachIrq(port, irq); if (rc != RS232_SUCCESS) { + portBuffersFree(port); return rc; } port->isOpen = 1; @@ -1362,6 +1462,7 @@ int32_t rs232Read(int32_t com, char *data, int32_t len) { } } + uartRearm(port); irqRestore(flags); return i; @@ -1668,13 +1769,13 @@ int32_t rs232SetStop(int32_t com, int32_t stopBits) { } -// Blocking polled write: busy-waits on LSR_TX_HOLD_EMPTY for each byte. -// This bypasses the TX ring buffer entirely and writes directly to the UART. -// Used by the packet layer for frame transmission where we want guaranteed -// delivery order and don't want ISR-driven TX reordering complications. -// The empty check and the write happen with interrupts off so the ISR -// cannot slip an XON/XOFF into THR between them. Stops early (returning -// the count written) while flow control holds the line. +// Blocking polled write: busy-waits on LSR_TX_HOLD_EMPTY for each byte, so +// it takes as long as the bytes take on the wire (a caller that must not +// wait uses rs232WriteBuf, as the packet layer does). This bypasses the +// TX ring buffer entirely and writes directly to the UART. The empty +// check and the write happen with interrupts off so the ISR cannot slip +// an XON/XOFF into THR between them. Stops early (returning the count +// written) while flow control holds the line. int32_t rs232Write(int32_t com, const char *data, int32_t len) { int32_t rc; Rs232StateT *port = getOpenPort(com, &rc); @@ -1726,12 +1827,37 @@ int32_t rs232WriteBuf(int32_t com, const char *data, int32_t len) { TX_WRITE(port, data[i]); } - // If there's data to send, enable TX_HOLD_EMPTY interrupt - if (!TX_EMPTY(port)) { - UART_WRITE_IER(port, port->ier | IER_TX_HOLD_EMPTY); - } - + uartRearm(port); irqRestore(flags); return i; } + + +// With interrupts off, outside the handler: bytes in the ring get the +// TX-hold-empty interrupt enabled. The port's IRQ left in service at the +// PIC is ended (picHealIrq). A UART that holds an interrupt up had its +// edge missed by the PIC: the interrupt enable is turned off and on, +// which drops the UART's line and raises it again for a fresh edge. An +// interrupt merely waiting for interrupts to come back on is unharmed +// (reading IIR may clear a TX-hold-empty one, which the enable raises +// again). Readers, writers, and the pending count all do this, so a +// port polled at all never stays stuck (DOSBox stops delivering received +// bytes until the guest reads them). +static void uartRearm(Rs232StateT *port) { + if (!TX_EMPTY(port) && port->txFlowOn && !(port->ier & IER_TX_HOLD_EMPTY)) { + UART_WRITE_IER(port, port->ier | IER_TX_HOLD_EMPTY); + return; + } + + if (port->irq != IRQ_NONE) { + picHealIrq(port->irq); + } + + if (!(UART_READ_IIR(port) & IIR_NO_INT_PENDING)) { + uint8_t ier = port->ier; + + UART_WRITE_IER(port, 0); + UART_WRITE_IER(port, ier); + } +} diff --git a/src/libs/kpunch/serial/rs232/rs232.h b/src/libs/kpunch/serial/rs232/rs232.h index 2a57cf90..d815159c 100644 --- a/src/libs/kpunch/serial/rs232/rs232.h +++ b/src/libs/kpunch/serial/rs232/rs232.h @@ -93,6 +93,7 @@ #define RS232_ERR_NULL_PTR (-14) #define RS232_ERR_IRQ_NOT_FOUND (-15) #define RS232_ERR_LOCK_MEM (-16) +#define RS232_ERR_NO_MEMORY (-17) // Line status bits (rs232GetLsr): errors seen on received data #define RS232_LSR_OVERRUN 0x02 diff --git a/src/libs/kpunch/serial/seclink/README.md b/src/libs/kpunch/serial/seclink/README.md deleted file mode 100644 index a5e6d2d5..00000000 --- a/src/libs/kpunch/serial/seclink/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# seclink -- Secure Serial Link - -The top-level API for the DVX serial stack. Wraps the packet transport -and the security primitives to provide a multi-channel encrypted byte -link: optional shared-passphrase authentication, forward-secret DH -handshake with key confirmation, per-packet encryption with HMAC -integrity, up to 128 channels multiplexed on a single physical port, -and bulk send with retransmit handled underneath. - -## Documentation - -Full reference is in the combined serial stack doc: -`../serial.dhs` (the `seclink` section). It compiles into the DVX -System Reference -- open via the Help Viewer app, or read -`docs/dvx_system_reference.html` after `make`. - -Topics covered: protocol, open/close, passphrase, handshake, send/poll, -plaintext channels, error codes, typical end-to-end usage. - -## Source Files - -- `secLink.h` -- public API and error codes -- `secLink.c` -- implementation (handshake, channel mux, bulk send) - -## Build - -Built into `serial.lib` by `make -C src/libs/kpunch/serial` (invoked by -the top-level `make`). diff --git a/src/libs/kpunch/serial/security/README.md b/src/libs/kpunch/serial/security/README.md deleted file mode 100644 index 3cf55232..00000000 --- a/src/libs/kpunch/serial/security/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# security -- Crypto Primitives - -Compact cryptographic primitives used by the secLink wrapper: 1024-bit -Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA block cipher in CTR -mode for session encryption, SHA-256 / HMAC-SHA256 / HKDF-SHA256 for key -derivation and message authentication, and a SHA-256 DRBG with an -entropy pool and seed file. No external dependencies. - -## Documentation - -Full reference is in the combined serial stack doc: -`../serial.dhs` (the `security` section). It compiles into the DVX -System Reference -- open via the Help Viewer app, or read -`docs/dvx_system_reference.html` after `make`. - -Topics covered: DH handshake API, XTEA-CTR encrypt/decrypt, hashing, -DRBG entropy pool and seed file, remote-key validation range. - -## Source Files - -- `security.h` -- public API (DH, XTEA-CTR, DRBG) -- `security.c` -- implementation (bignum, Montgomery ladder, XTEA, DRBG) -- `secHash.h` -- SHA-256, HMAC-SHA256, HKDF-SHA256 API -- `secHash.c` -- hash implementation - -## Build - -Built into `serial.lib` by `make -C src/libs/kpunch/serial` (invoked by -the top-level `make`). diff --git a/src/libs/kpunch/serial/serial.dhs b/src/libs/kpunch/serial/serial.dhs index 352af9e8..60ca9dfe 100644 --- a/src/libs/kpunch/serial/serial.dhs +++ b/src/libs/kpunch/serial/serial.dhs @@ -27,22 +27,24 @@ .index Serial .index RS-232 .index HDLC -.index XTEA -.index Diffie-Hellman +.index X25519 +.index XChaCha20-Poly1305 .index secLink .h1 Serial Stack -The DVX serial/networking stack provides reliable, optionally encrypted communication over RS-232 serial ports. It is composed of four layers, each building on the one below: +DVX's serial communications come in layers, in three libraries, each building on the one below: .list -.item rs232 -- ISR-driven UART driver with ring buffers and flow control -.item packet -- HDLC framing, CRC-16, Go-Back-N ARQ (reliable delivery) -.item security -- 1024-bit Diffie-Hellman key exchange, XTEA-CTR cipher, SHA-256/HMAC/HKDF, SHA-256 DRBG -.item secLink -- Convenience wrapper: authenticated handshake, channel multiplexing, per-packet encryption and MAC +.item rs232 (the serial library, serial.lib) -- ISR-driven UART driver with ring buffers and flow control +.item packet (the secure link library, seclink.lib) -- HDLC framing, CRC-16, Go-Back-N ARQ (reliable delivery) +.item security (the cryptography library, crypto.lib) -- X25519 key exchange, XChaCha20-Poly1305, SHA-256/HMAC/HKDF, SHA-256 DRBG (and Ed25519, Diffie-Hellman, and XTEA-CTR for other uses) +.item secLink (the secure link library, seclink.lib) -- authenticated handshake, channel multiplexing, per-packet encryption and MAC .endlist -Loaded as: bin/libs/kpunch/serial/serial.lib +The serial and cryptography libraries are part of every DVX; the secure link library comes with Communications. + +Loaded as: bin/libs/kpunch/serial/serial.lib (rs232) .h2 Layered Architecture @@ -53,11 +55,11 @@ Loaded as: bin/libs/kpunch/serial/serial.lib | secLinkSend() -- send on channel, opt. encrypt | | secLinkPoll() -- receive + dispatch | +--------------------------------------------------+ -| secLink (handshake, channels, cipher + MAC) | +| secLink (handshake, channels, sealed frames) | +--------------------------------------------------+ | packet (HDLC framing, CRC-16, Go-Back-N ARQ) | +--------------------------------------------------+ -| security -- DH, XTEA-CTR, SHA-256/HMAC, DRBG | +| security -- X25519, XChaCha20-Poly1305, DRBG | | (used by secLink during handshake) | +--------------------------------------------------+ | rs232 (ISR-driven UART, 2048-byte rings) | @@ -66,13 +68,13 @@ Loaded as: bin/libs/kpunch/serial/serial.lib +--------------------------------------------------+ .endcode -The security layer is invoked by secLink during the Diffie-Hellman handshake and for per-packet cipher operations. It has no runtime coupling to rs232 or packet, so it can be used standalone for cryptographic operations independent of the serial stack. +The security layer is invoked by secLink during the X25519 handshake and to seal and open every encrypted packet. It has no runtime coupling to rs232 or packet, so it can be used standalone for cryptographic operations independent of the serial stack. .link lib.serial.rs232 Layer 1: RS-232 UART Driver -.link lib.serial.packet Layer 2: Packet Transport -.link lib.serial.security Layer 3: Security (DH, XTEA, SHA-256) -.link lib.serial.seclink Layer 4: Secure Link -.link lib.serial.example End-to-End Example +.link lib.seclink.packet Layer 2: Packet Transport +.link lib.crypto Layer 3: Cryptography Library +.link lib.seclink Layer 4: Secure Link Library +.link lib.seclink.example End-to-End Example .topic lib.serial.rs232 .title RS-232 UART Driver @@ -89,9 +91,9 @@ The security layer is invoked by secLink during the Diffie-Hellman handshake and .h1 RS-232 UART Driver -ISR-driven serial port driver supporting up to 4 simultaneous COM ports. A shared ISR drains UART FIFOs into per-port ring buffers (2048 bytes, power-of-2 for fast index wrapping). Flow control (XON/XOFF, RTS/CTS, DTR/DSR) operates within the ISR using watermark thresholds. All ISR data structures are DPMI-locked to prevent page faults. XON/XOFF bytes are queued for the transmit interrupt instead of being written over a byte still in the transmit register, and every cached register (IER, MCR) is changed with interrupts off so the ISR and the application never lose each other's updates. +ISR-driven serial port driver supporting up to 4 simultaneous COM ports. A shared ISR drains UART FIFOs into per-port ring buffers (2048 bytes, power-of-2 for fast index wrapping). An interrupt the interrupt controller misses (its edge lost, seen under a flood in DOSBox, which then stops delivering received bytes) cannot stall a port: rs232Read, rs232WriteBuf, and rs232GetTxBuffered find the UART still holding an interrupt up and turn its interrupt enable off and on, which raises a fresh one, so a port that is polled at all never stays stuck. Flow control (XON/XOFF, RTS/CTS, DTR/DSR) operates within the ISR using watermark thresholds. All ISR data structures are DPMI-locked to prevent page faults. XON/XOFF bytes are queued for the transmit interrupt instead of being written over a byte still in the transmit register, and every cached register (IER, MCR) is changed with interrupts off so the ISR and the application never lose each other's updates. -Header: rs232/rs232.h +Header: serial/rs232.h .h2 Port Constants @@ -152,6 +154,7 @@ Detected automatically by probing the scratch register and FIFO capability. RS232_ERR_NULL_PTR -14 NULL pointer argument. RS232_ERR_IRQ_NOT_FOUND -15 Could not detect IRQ for port. RS232_ERR_LOCK_MEM -16 DPMI memory lock failed. + RS232_ERR_NO_MEMORY -17 No memory for the port's buffers. .endtable .h2 rs232Open @@ -203,7 +206,7 @@ Returns the number of bytes actually read (may be less than len). .h2 rs232Write -Blocking polled write directly to UART transmit holding register. Bypasses the TX ring buffer. Polls LSR_TX_HOLD_EMPTY before writing each byte (the check and the write happen with interrupts off, so a flow-control byte from the ISR is never overwritten). Use for small, immediate writes where order matters (the packet layer uses this for frame transmission). +Blocking polled write directly to UART transmit holding register. Bypasses the TX ring buffer. Polls LSR_TX_HOLD_EMPTY before writing each byte (the check and the write happen with interrupts off, so a flow-control byte from the ISR is never overwritten), so it takes as long as the bytes take on the wire. Use for small, immediate writes where order matters; the packet layer uses rs232WriteBuf instead, so it never waits. .code int32_t rs232Write(int32_t com, const char *data, int32_t len); @@ -213,7 +216,7 @@ Returns the number of bytes actually written (may be less than len while flow co .h2 rs232WriteBuf -Non-blocking write to the transmit ring buffer. The ISR drains buffered data to the UART asynchronously. +Non-blocking write to the transmit ring buffer. The ISR drains buffered data to the UART asynchronously, through the transmit-hold-empty interrupt. .code int32_t rs232WriteBuf(int32_t com, const char *data, int32_t len); @@ -290,689 +293,3 @@ rs232GetParity returns a lowercase character on success. Callers comparing again rs232ClearRxBuffer(com) Discard all data in the receive ring buffer. rs232ClearTxBuffer(com) Discard all data in the transmit ring buffer. .endtable - -.topic lib.serial.packet -.title Packet Transport -.toc 1 Packet Transport -.index packet -.index HDLC -.index CRC-16 -.index Go-Back-N -.index pktOpen -.index pktOpenStream -.index PktStreamT -.index pktClose -.index pktSend -.index pktPoll - -.h1 Packet Transport - -Reliable, ordered packet delivery over an RS-232 serial link. Uses HDLC-style byte-stuffed framing for frame delimiting, CRC-16-CCITT for error detection, and Go-Back-N ARQ with a configurable sliding window for retransmission. - -Header: packet/packet.h - -.h2 Constants - -.table - Constant Value Description - -------- ----- ----------- - PKT_MAX_PAYLOAD 255 Maximum bytes per packet payload. - PKT_DEFAULT_WINDOW 4 Default sliding window size (unacknowledged frames in flight). - PKT_MAX_WINDOW 8 Maximum sliding window size. -.endtable - -.h2 Error Codes - -.table - Constant Value Description - -------- ----- ----------- - PKT_SUCCESS 0 Operation succeeded. - PKT_ERR_INVALID_PARAM -1 Invalid parameter (NULL handle, bad data pointer, length out of range). - PKT_ERR_TX_FULL -2 Transmit window is full (non-blocking send only). - PKT_ERR_DISCONNECTED -3 Serial port returned a negative read (disconnect/error), or the connection was closed from its own callback. - PKT_ERR_TIMEOUT -4 A blocking send got no acknowledgement for PKT_SEND_TIMEOUT_MS (15000 ms). -.endtable - -.h2 PktRecvCallbackT - -Callback type for received packets. - -.code -typedef void (*PktRecvCallbackT)(void *ctx, - const uint8_t *data, int len); -.endcode - -.table - Parameter Description - --------- ----------- - ctx User context pointer (passed to pktOpen). - data Payload bytes (valid only during the callback). - len Payload length. -.endtable - -.h2 pktOpen - -Open a packetized connection over an already-open COM port. Only one packet connection may exist on a given COM port at a time; a second pktOpen call on the same port returns NULL to avoid silent framing corruption. - -.code -PktConnT *pktOpen(int com, int windowSize, - PktRecvCallbackT callback, void *callbackCtx); -.endcode - -.table - Parameter Description - --------- ----------- - com COM port index (RS232_COM1..RS232_COM4). Must already be open via rs232Open. - windowSize Sliding window size (1..PKT_MAX_WINDOW). Pass 0 for PKT_DEFAULT_WINDOW. Values greater than PKT_MAX_WINDOW are clamped. - callback Called when a complete, CRC-verified, in-order packet is received. The data pointer is valid only during the callback. - callbackCtx User pointer passed to the callback. -.endtable - -Returns an opaque PktConnT handle, or NULL on failure (invalid port, duplicate open, or allocation failure). - -.h2 pktOpenStream - -Open a packetized connection over any byte stream instead of a COM port -- a TCP socket, for example. The stream is described by two non-blocking calls and a context pointer, copied into the connection. Any number of stream connections may be open. - -.code -typedef struct { - int32_t (*read)(void *ctx, uint8_t *buf, int32_t len); - int32_t (*write)(void *ctx, const uint8_t *data, int32_t len); - void *ctx; -} PktStreamT; - -PktConnT *pktOpenStream(const PktStreamT *stream, int windowSize, PktRecvCallbackT callback, void *callbackCtx); -.endcode - -.table - Member Description - ------ ----------- - read Up to len bytes into buf: the count, 0 when nothing has arrived, negative when the stream is gone (pktPoll then reports PKT_ERR_DISCONNECTED). - write Bytes taken from data (0 = none right now; a frame is retried until it is all out or nothing was taken for a while), negative when the stream is gone. - ctx Passed to both. -.endtable - -Returns the handle, or NULL for a NULL stream or call, or on allocation failure. pktClose never closes the stream. - -.h2 pktClose - -Close a packetized connection and release its registry slot. Does not close the underlying COM port. The caller is responsible for calling rs232Close separately when no longer needed. May be called from inside the receive callback: the connection is then freed when the outermost pktPoll returns (which reports PKT_ERR_DISCONNECTED). - -.code -void pktClose(PktConnT *conn); -.endcode - -.h2 pktSend - -Send a data packet. The payload is copied into an internal retransmit slot before transmission, so the caller may reuse the supplied buffer immediately. - -.code -int pktSend(PktConnT *conn, const uint8_t *data, - int len, bool block); -.endcode - -.table - Parameter Description - --------- ----------- - conn Connection handle from pktOpen. - data Payload bytes (1..PKT_MAX_PAYLOAD). - len Payload length. - block If true, wait for transmit window space via pktWaitSpace. If false, return PKT_ERR_TX_FULL immediately when the window is full. -.endtable - -Returns PKT_SUCCESS on success, PKT_ERR_INVALID_PARAM for NULL/out-of-range input, PKT_ERR_TX_FULL for a non-blocking call with a full window, PKT_ERR_DISCONNECTED if the serial port drops during a blocking wait, or PKT_ERR_TIMEOUT if the peer stops acknowledging. A frame the port accepts only partially (flow control) is completed before the call returns. - -.h2 pktPoll - -Poll for incoming data, process received frames, and handle retransmits. Must be called frequently in the main loop. The callback is invoked synchronously for each complete, CRC-verified, in-order data frame and may call pktSend or pktPoll again: received bytes are consumed from a per-connection buffer, so a nested call continues the same byte stream in order. After a gap in the sequence only one NAK is sent until the missing frame arrives (go-back-N resends the whole window on one NAK). - -.code -int pktPoll(PktConnT *conn); -.endcode - -Returns the number of valid data packets delivered to the callback this call, or PKT_ERR_INVALID_PARAM if conn is NULL, or PKT_ERR_DISCONNECTED if the serial layer returned a negative read. - -.h2 pktReset - -Reset the local connection state (sequence numbers, transmit slots, RX state machine) and send a RST frame to the remote side. The remote's RST handler clears its own state (discarding its unacknowledged frames), runs its reset callback, and answers with a RST reply. A RST reply that answers no reset of ours is ignored. Useful for recovering from a desynchronized connection. - -.code -int pktReset(PktConnT *conn); -.endcode - -Returns PKT_SUCCESS on success or PKT_ERR_INVALID_PARAM if conn is NULL. - -.h2 pktCanSend - -Check whether there is room in the transmit window for another packet. Returns false when conn is NULL or the transmit window is full. - -.code -bool pktCanSend(PktConnT *conn); -.endcode - -.h2 pktGetPending - -Get the number of unacknowledged packets currently in the transmit window. Ranges from 0 (all sent packets acknowledged) to windowSize (window full). - -.code -int pktGetPending(PktConnT *conn); -.endcode - -Returns the pending count on success, or PKT_ERR_INVALID_PARAM if conn is NULL. - -.h2 pktSetResetCallback - -Register a callback run when the remote side resets the connection (its unacknowledged frames are gone at that point, so an upper layer with per-frame state must resynchronize). The callback receives the ctx passed to pktOpen. NULL removes it. - -.code -typedef void (*PktResetCallbackT)(void *ctx); -void pktSetResetCallback(PktConnT *conn, PktResetCallbackT callback); -.endcode - -.h2 pktWaitSpace - -Poll until the transmit window has room. The wait restarts whenever an acknowledgement frees a slot, so only a peer that stops acknowledging times out. - -.code -int pktWaitSpace(PktConnT *conn); -.endcode - -Returns PKT_SUCCESS, PKT_ERR_INVALID_PARAM, PKT_ERR_DISCONNECTED, or PKT_ERR_TIMEOUT after PKT_SEND_TIMEOUT_MS without progress. - -.topic lib.serial.security -.title Security (DH, XTEA, SHA-256) -.toc 1 Security (DH, XTEA, SHA-256) -.index Security -.index Diffie-Hellman -.index XTEA -.index XTEA-CTR -.index SHA-256 -.index HMAC -.index HKDF -.index DRBG -.index secDhCreate -.index secCipherCreate -.index secRngBytes -.index secRngAddEntropy -.index secRngLoadSeed -.index security.bas - -.h1 Security (DH, XTEA, SHA-256) - -Cryptographic primitives for the serial stack: 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA block cipher in CTR mode, SHA-256, HMAC-SHA256, and HKDF-SHA256, and a SHA-256 based random number generator. - -XTEA requires no lookup tables and compiles to approximately 20 instructions per round, making it suitable for 486-class hardware where AES S-box tables would thrash the 8KB cache. SHA-256 needs only 32-bit arithmetic and a 256-byte constant table. - -Headers: security/security.h, security/secHash.h - -.h2 Constants - -.table - Constant Value Description - -------- ----- ----------- - SEC_DH_KEY_SIZE 128 Size of a DH public key or shared secret in bytes (1024 bits). - SEC_XTEA_KEY_SIZE 16 Size of an XTEA key in bytes (128 bits). - SEC_RNG_SEED_SIZE 32 Bytes stored in a seed file. - SEC_SHA256_SIZE 32 SHA-256 digest size. - SEC_SHA256_BLOCK_SIZE 64 SHA-256 block size. - SEC_HKDF_MAX_OUT 8160 Largest HKDF output (255 digests). -.endtable - -.h2 Error Codes - -.table - Constant Value Description - -------- ----- ----------- - SEC_SUCCESS 0 Operation succeeded. - SEC_ERR_PARAM -1 Invalid parameter. - SEC_ERR_NOT_READY -2 DH context not ready (keys not generated or secret not computed). - SEC_ERR_IO -3 Seed file could not be read or written. -.endtable - -.h2 RNG Functions - -A SHA-256 hash DRBG over a 32-byte entropy pool. secRngAddEntropy hashes new bytes into the pool and never discards what is already there, so it is safe to call at any time, before or after the first output. Each output block is a hash of the pool and a counter; after every request the pool is re-hashed so earlier output cannot be recomputed from a later state. - -The first secRngBytes call gathers hardware entropy automatically. On DOS that is 256 PIT samples, each taken after a busy wait sized by the previous sample, plus the full time stamp counter on CPUs that have one (Pentium and later) and the BIOS tick count. On a CPU without a time stamp counter the samples depend mostly on the PIT phase and time of day, which an observer can narrow down, so applications should feed user-input timings to secRngAddEntropy and keep a seed file: call secRngLoadSeed at startup and secRngSaveSeed at shutdown. Host builds read /dev/urandom. - -Under the DVX shell this is automatic when the serial library is installed: the shell loads the seed file SECLINK.RND in the user folder at startup, feeds the timing of every key press and mouse event to secRngAddEntropy, and saves a fresh seed at shutdown. Applications only need their own calls when they run without the shell. - -.code -void secRngAddEntropy(const uint8_t *data, int32_t len); -void secRngBytes(uint8_t *buf, int32_t len); -int32_t secRngGatherEntropy(uint8_t *buf, int32_t len); -int secRngLoadSeed(const char *path); -int secRngSaveSeed(const char *path); -void secSecureZero(void *ptr, int len); -.endcode - -.table - Function Description - -------- ----------- - secRngAddEntropy Hash bytes into the pool (key timings, mouse positions, anything unpredictable). - secRngBytes Generate pseudorandom bytes. - secRngGatherEntropy Fill buf with condensed hardware entropy (one fresh gather per 32 bytes). Returns the byte count (0 if a host build cannot read its random device). - secRngLoadSeed Mix a seed file into the pool, then immediately rewrite it with fresh output so a seed is never used twice. SEC_SUCCESS or SEC_ERR_IO. - secRngSaveSeed Write SEC_RNG_SEED_SIZE bytes of fresh output for the next run. SEC_SUCCESS or SEC_ERR_IO. - secSecureZero Zero memory in a way the compiler cannot optimize away (for key material). -.endtable - -.h2 Hash Functions - -.code -void secSha256(const uint8_t *data, int32_t len, uint8_t digest[32]); -void secSha256Init(SecSha256T *ctx); -void secSha256Update(SecSha256T *ctx, const uint8_t *data, int32_t len); -void secSha256Final(SecSha256T *ctx, uint8_t digest[32]); -void secHmacSha256(const uint8_t *key, int32_t keyLen, const uint8_t *data, int32_t len, uint8_t mac[32]); -void secHmacInit(SecHmacT *h, const uint8_t *key, int32_t keyLen); -void secHmacUpdate(SecHmacT *h, const uint8_t *data, int32_t len); -void secHmacFinal(SecHmacT *h, uint8_t mac[32]); -int secHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, uint8_t *okm, int32_t okmLen); -bool secConstTimeEqual(const void *a, const void *b, int32_t len); -.endcode - -SHA-256 follows FIPS 180-4, HMAC RFC 2104, and HKDF RFC 5869 (extract and expand; a NULL or empty salt means 32 zero bytes). secHkdf returns SEC_ERR_PARAM for okmLen outside 1..SEC_HKDF_MAX_OUT. secConstTimeEqual compares without revealing where the inputs differ; use it for every MAC check. - -.h2 Diffie-Hellman Key Exchange - -1024-bit DH with 256-bit private exponents using the RFC 2409 Group 2 safe prime. The modular exponentiation is a Montgomery ladder with branch-free operand swaps, so its timing does not depend on the private key. - -.code -SecDhT *secDhCreate(void); -int secDhGenerateKeys(SecDhT *dh); -int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len); -int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len); -int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len); -void secDhDestroy(SecDhT *dh); -.endcode - -.table - Function Description - -------- ----------- - secDhCreate Allocate a new DH context. - secDhGenerateKeys Generate a private exponent and public key from the DRBG. - secDhGetPublicKey Copy the local public key into buf. *len receives the key size. - secDhComputeSecret Compute the shared secret from the remote public key. Keys outside [2, p-2] are rejected with SEC_ERR_PARAM (0, 1, and p-1 would force a trivially known secret). - secDhGetSecret Copy the shared secret (SEC_DH_KEY_SIZE big-endian bytes) into buf. It is input for a key derivation function such as secHkdf, never a key by itself. - secDhDestroy Securely zero and free the DH context. -.endtable - -Typical usage order: secDhCreate, secDhGenerateKeys, exchange public keys, secDhComputeSecret, secDhGetSecret, secHkdf, secDhDestroy. - -.h2 XTEA-CTR Cipher - -XTEA in counter mode. Encrypt and decrypt are the same operation (XOR with keystream). The counter must never repeat with the same key. - -.code -SecCipherT *secCipherCreate(const uint8_t *key); -void secCipherCrypt(SecCipherT *c, uint8_t *data, int len); -void secCipherSetCounter(SecCipherT *c, uint64_t counter); -void secCipherDestroy(SecCipherT *c); -.endcode - -.table - Function Description - -------- ----------- - secCipherCreate Allocate a cipher context with a 128-bit key. Returns NULL on allocation failure or NULL key. Initial counter is zero. - secCipherCrypt Encrypt or decrypt data in place. The 64-bit counter increments by one every 8 bytes (one XTEA block). - secCipherSetCounter Position the 64-bit block counter (secLink derives it from the frame sequence number). - secCipherDestroy Securely zero the cipher context (key and counter state) and free its memory. -.endtable - -secCipherCreate returns NULL if the key pointer is NULL or allocation fails. secCipherCrypt is a no-op when any of c/data is NULL or len <= 0. - -.h2 BASIC - -src/include/basic/security.bas (DECLARE LIBRARY "serial") declares the BASIC functions (security/secBas.c). Keys, data, and results are BINARY STRING (pointer and length in, a result buffer and length out), so every byte value passes; the result buffer is wiped before it is reused. A handle (1, 2, ...; 0 = none) is a key exchange, a cipher, or an incremental SHA-256 / HMAC; each records the program that made it and the shell's app-cleanup hook frees (and wipes) what a program leaves open. - -.table - BASIC C it calls - ----- ---------- - SecRandom$(count), SecAddEntropy bytes secRngBytes; secRngAddEntropy. - SecDhCreate() secDhCreate + secDhGenerateKeys. - SecDhPublicKey$(h), SecDhSecret$(h) secDhGetPublicKey; secDhGetSecret ("" before SecDhComputeSecret). - SecDhComputeSecret(h, remotePublicKey) secDhComputeSecret: SEC_OK or a SEC_ERR_ code. - SecDhFree h secDhDestroy. - SecCipherCreate(key) secCipherCreate; 0 unless key is SEC_XTEA_KEY_SIZE bytes. - SecCipherCrypt$(h, bytes) secCipherCrypt on a copy. - SecCipherSetCounter h, counter secCipherSetCounter (a DOUBLE: exact up to 2^53). - SecCipherFree h secCipherDestroy. - SecSha256$(bytes); SecSha256Open, SecSha256Add, SecSha256End$ secSha256; secSha256Init / Update / Final (End frees the handle). - SecHmac$(key, bytes); SecHmacOpen, SecHmacAdd, SecHmacEnd$ secHmacSha256; secHmacInit / Update / Final. - SecHkdf$(salt, secret, info, length) secHkdf ("" for a length outside 1..SEC_HKDF_MAX_OUT). - SecEqual(a, b) secConstTimeEqual (False for different lengths). -.endtable - -The SEC_ CONSTs of security.bas are compared with security.h and secHash.h by a formrt test, which also calls every function for real. - -.topic lib.serial.seclink -.title Secure Link -.toc 1 Secure Link -.index secLink -.index secLinkOpen -.index secLinkOpenStream -.index secLinkClose -.index secLinkHandshake -.index secLinkHandshakeStep -.index secLinkSend -.index secLinkPoll -.index secLinkSetPassphrase -.index secLinkSetChannelPlain - -.h1 Secure Link - -Top-level API composing all three lower layers (rs232, packet, security) into a single interface. Provides an authenticated key exchange, channel multiplexing (0-127), and per-packet encryption with integrity protection over a reliable serial link. - -Header: seclink/secLink.h - -.h2 Protocol (version 2) - -Every packet starts with a type byte: - -.table - Type Layout Purpose - ---- ------ ------- - HELLO type, version, flags, DH public key (128 bytes) Starts a key exchange. Flag bit 0: passphrase in use. - CONFIRM type, HMAC-SHA256 (32 bytes) Proves both sides derived the same keys. - PLAIN type, channel, payload Clear application data. - CIPHER type, channel, ciphertext, MAC (8 bytes) Encrypted, authenticated application data. -.endtable - -Handshake packets are never delivered to the application, and application data is never taken as key material. A HELLO is kept whenever it arrives, so the side that starts first is never missed. - -Key derivation: HKDF-SHA256 with salt = SHA-256(passphrase) (32 zero bytes without one), input = the DH shared secret, and info = "DVX secLink v2" followed by the lower then the higher public key. The output supplies an XTEA key and an HMAC key for each direction plus a confirmation key; the side with the lower public key sends with the "low" keys. Directions never share a key. - -Confirmation: each side sends HMAC(confirmation key, SHA-256(lower HELLO + higher HELLO) + role byte). A different passphrase, a passphrase on only one side, or a modified HELLO makes the check fail on both sides with SECLINK_ERR_AUTH. A side that has a passphrase never accepts a peer without one. - -Encrypted frames: encrypt-then-MAC. Each direction numbers its encrypted frames with an implicit 64-bit sequence number that is never sent. The MAC is the first 8 bytes of HMAC-SHA256 over type, channel, big-endian sequence number, and ciphertext, and the CTR counter for a frame starts at sequence * 32, so every frame has its own keystream range. Tampered, replayed, or injected frames fail the MAC, are dropped and counted (secLinkGetDropped); later frames still verify. - -Without a passphrase the link is encrypted and integrity-protected but not authenticated: an active attacker in the middle could run two separate exchanges. Use the same passphrase on both sides to rule that out. - -Clear frames are accepted only until the first session is established. From then on, until secLinkClose, a clear frame is delivered only on a channel marked plaintext with secLinkSetChannelPlain -- even after the session was dropped by a packet-layer reset or a failed re-key, which someone on the line can cause; so neither can turn a protected channel into a clear one. A re-key HELLO on a running session whose protocol version or passphrase flag cannot match is dropped (and counted) without ending the session. - -.h2 Constants - -.table - Constant Value Description - -------- ----- ----------- - SECLINK_PROTOCOL_VERSION 2 Version carried in HELLO; a mismatch fails the handshake. - SECLINK_FRAME_HDR_SIZE 2 Type byte plus channel byte. - SECLINK_MAC_SIZE 8 Truncated MAC on encrypted frames. - SECLINK_MAX_PAYLOAD 245 Maximum payload per send (PKT_MAX_PAYLOAD minus header and MAC), clear or encrypted. - SECLINK_MAX_CHANNEL 127 Highest valid channel number. - SECLINK_NUM_CHANNELS 128 Array size needed to hold one slot per channel (SECLINK_MAX_CHANNEL + 1). - SECLINK_CHAN_BUF_SIZE 4096 Recommended per-channel receive buffer size for callers that buffer inbound data between the receive callback and an application read. -.endtable - -.h2 Error Codes - -.table - Constant Value Description - -------- ----- ----------- - SECLINK_SUCCESS 0 Operation succeeded. - SECLINK_ERR_PARAM -1 Invalid parameter (NULL pointer, bad channel, bad length). - SECLINK_ERR_SERIAL -2 Serial port closed or disconnected (secLinkPoll). - SECLINK_ERR_ALLOC -3 Memory allocation failed. - SECLINK_ERR_HANDSHAKE -4 Handshake failed: invalid public key, protocol version mismatch, reflected HELLO, dead line, or the peer reset mid-exchange. - SECLINK_ERR_NOT_READY -5 No session keys yet (encrypted send attempted). - SECLINK_ERR_SEND -6 Packet send failed, the peer stopped acknowledging, or a non-blocking send found the window full. - SECLINK_ERR_TIMEOUT -7 The peer did not complete the handshake within 30 seconds (the exchange stays pending). - SECLINK_ERR_AUTH -8 Passphrase mismatch, passphrase on only one side, or tampering. - SECLINK_PENDING 1 secLinkHandshakeStep only: the exchange is still running. -.endtable - -.h2 SecLinkRecvT - -Receive callback type. Delivers plaintext with the channel number. encrypted is true when the frame arrived encrypted and its MAC verified, false for a clear frame. - -.code -typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted); -.endcode - -.h2 secLinkOpen - -Open a secure serial link. Opens the COM port via rs232Open and creates a packet layer (default window size) with an internal receive dispatcher. The handshake must be performed separately via secLinkHandshake before encrypted data can be sent. - -.code -SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx); -.endcode - -.table - Parameter Description - --------- ----------- - com COM port index (RS232_COM1..RS232_COM4). - bps Baud rate. - dataBits Data bits per character (5-8). - parity Parity mode ('N', 'O', 'E', 'M', 'S', or lowercase). - stopBits Stop bits (1 or 2). - handshake Flow control mode (RS232_HANDSHAKE_*). - callback Called when a packet is received, after verification and decryption. The data pointer is valid only during the callback. - ctx User pointer passed to the callback. -.endtable - -Returns an opaque SecLinkT handle, or NULL on failure (allocation failure, rs232Open error, or pktOpen error). On failure all partially initialized resources are cleaned up. - -.h2 secLinkOpenStream - -Open a secure link over a byte stream (see pktOpenStream) instead of a COM port, e.g. a TCP connection to the host proxy's listening port. Everything else -- handshake, passphrase, channels, sending, and polling -- is the same as for a COM port link. secLinkClose does not close the stream; the caller closes it afterwards. - -.code -SecLinkT *secLinkOpenStream(const PktStreamT *stream, SecLinkRecvT callback, void *ctx); -.endcode - -.h2 secLinkClose - -Close the link and free all associated resources: securely destroy the ciphers and key material, destroy any remaining DH context, close the packet layer and the COM port, and free the link. Safe to call with a NULL link (no-op). Safe to call from inside the receive callback: the close then happens when the outermost secLink call returns, and no further packets are delivered. - -.code -void secLinkClose(SecLinkT *link); -.endcode - -.h2 secLinkSetPassphrase - -Set the shared passphrase for the next handshake. NULL or "" clears it. Only SHA-256 of the text is kept. Set it before calling secLinkHandshake; both sides must use the same passphrase, or both none. An exchange already in progress (for example one left pending by a timed-out secLinkHandshake) was started with the old setting, so it is abandoned: the next secLinkHandshake starts a fresh one. - -.code -int secLinkSetPassphrase(SecLinkT *link, const char *pass); -.endcode - -Returns SECLINK_SUCCESS or SECLINK_ERR_PARAM for a NULL link. - -.h2 secLinkHandshake - -Run the authenticated DH key exchange. Blocks, polling the packet layer, until both CONFIRM messages have been checked, or until 30 seconds pass. - -.code -int secLinkHandshake(SecLinkT *link); -.endcode - -Behavior: - -.list -.item A fresh DH key pair is generated from the DRBG and our HELLO is sent. If the peer's HELLO already arrived (it started first), the keys are derived at once. -.item The DH private key is destroyed as soon as the session keys are derived (forward secrecy). -.item After SECLINK_ERR_TIMEOUT the exchange stays pending: it still completes during secLinkPoll if the peer shows up, and calling secLinkHandshake again resumes the wait without sending a new key. -.item On a ready link it negotiates fresh session keys. A peer that is already ready (or has an exchange pending) answers automatically from secLinkPoll. -.item Encrypted traffic under the previous keys that was sent before the new HELLO is still accepted; encrypted sends are refused (SECLINK_ERR_NOT_READY) until the new exchange completes. -.endlist - -Returns SECLINK_SUCCESS, SECLINK_ERR_PARAM, SECLINK_ERR_ALLOC, SECLINK_ERR_HANDSHAKE, SECLINK_ERR_TIMEOUT, or SECLINK_ERR_AUTH. When the peer confirms (or rejects) and hangs up in the same poll -- a proxy exits as soon as it sees a passphrase mismatch -- the exchange's outcome is reported, not the disconnect. - -.h2 secLinkHandshakeStep - -Non-blocking handshake for callers that run their own event loop. - -.code -int secLinkHandshakeStep(SecLinkT *link); -.endcode - -Starts an exchange on an idle link (never handshaken, or dropped by a peer reset), polls once, and returns SECLINK_SUCCESS once the link is ready, SECLINK_PENDING (1) while the exchange is still running, or the error that ended it (the same codes as secLinkHandshake, without SECLINK_ERR_TIMEOUT). A failed exchange keeps returning its error; secLinkHandshake starts a new one. - -.h2 secLinkSend - -Send data on a channel, optionally encrypted and MACed. - -.code -int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block); -.endcode - -.table - Parameter Description - --------- ----------- - link Connection handle from secLinkOpen. - data Plaintext payload. - len Payload length. Must be in 1..SECLINK_MAX_PAYLOAD. - channel Logical channel number (0..SECLINK_MAX_CHANNEL). - encrypt If true, encrypt and MAC before sending (requires a completed handshake). - block If true, wait (pumping the receive callback) until the transmit window has space. If false, return SECLINK_ERR_SEND immediately when the window is full. -.endtable - -Returns SECLINK_SUCCESS on success or a negative error code (SECLINK_ERR_PARAM, SECLINK_ERR_NOT_READY, SECLINK_ERR_SEND). - -A frame takes its sequence number only once window space is guaranteed, and is queued without pumping in between, so frames always reach the wire in sequence order. The receive callback may itself send (blocking or not), including encrypted. - -Once session keys exist the receiver drops clear frames unless it marked the channel plaintext with secLinkSetChannelPlain. - -.h2 secLinkSendBuf - -Send an arbitrarily large buffer by splitting into SECLINK_MAX_PAYLOAD chunks. Always blocks until all data is sent. - -.code -int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt); -.endcode - -Returns SECLINK_SUCCESS or the first error encountered. - -.h2 secLinkSetChannelPlain - -Allow (plain = true) or refuse (plain = false, the default) clear frames on a channel once session keys exist. Before the first handshake clear frames are always accepted. - -.code -int secLinkSetChannelPlain(SecLinkT *link, uint8_t channel, bool plain); -.endcode - -Returns SECLINK_SUCCESS or SECLINK_ERR_PARAM for a bad link or channel. - -.h2 secLinkPoll - -Poll for incoming data. Drains the serial port, processes frames and retransmits, completes pending handshake steps, verifies and decrypts data frames, and forwards them to the callback. - -.code -int secLinkPoll(SecLinkT *link); -.endcode - -Returns the number of packets delivered by the packet layer this call, SECLINK_ERR_PARAM if link is NULL, or SECLINK_ERR_SERIAL when the port is closed or disconnected. Must be called frequently in the main loop. - -If the remote packet layer resets, the session keys are dropped (the link is no longer ready) and a new handshake is needed. Clear frames stay refused on protected channels (see Protocol). - -.h2 secLinkIsReady - -Check whether the handshake is complete and the link is ready for encrypted traffic. Cleartext sends do not require the link to be ready. - -.code -bool secLinkIsReady(SecLinkT *link); -.endcode - -.h2 secLinkGetDropped - -Number of received frames dropped since open: bad MAC (tampered, replayed, or injected), clear frames on a protected channel, encrypted frames without session keys, and malformed frames. - -.code -int32_t secLinkGetDropped(SecLinkT *link); -.endcode - -Returns the count or SECLINK_ERR_PARAM when link is NULL. - -.h2 secLinkGetPending - -Get the number of unacknowledged packets in the transmit window. Delegates directly to pktGetPending on the underlying packet connection. Useful for non-blocking send loops to decide when to throttle. - -.code -int secLinkGetPending(SecLinkT *link); -.endcode - -Returns the pending count on success or SECLINK_ERR_PARAM when link is NULL. - -.topic lib.serial.example -.title End-to-End Example -.toc 1 End-to-End Example -.index Serial Example -.index BBS Client - -.h1 End-to-End Example - -Below is a complete secure BBS client that opens COM1, runs the authenticated key exchange, and then shuttles terminal traffic over a single encrypted channel. It demonstrates the typical serial stack usage: load the RNG seed file, open the link, set the passphrase, run the handshake, then enter a poll+send loop. - -.code -#include "rs232/rs232.h" -#include "packet/packet.h" -#include "security/security.h" -#include "seclink/secLink.h" -#include -#include - -#define CHAN_TERMINAL 0 - -#define SEED_FILE "SECLINK.RND" - -static void onPacket(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted) { - (void)ctx; - if (channel == CHAN_TERMINAL && encrypted) { - fwrite(data, 1, len, stdout); - fflush(stdout); - } -} - - -int main(void) { - // Carry entropy over from the last run (hardware entropy is - // gathered automatically; feed key timings with secRngAddEntropy). - secRngLoadSeed(SEED_FILE); - - // Open the link at 115200 8N1 with RTS/CTS flow control. - SecLinkT *link = secLinkOpen(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_RTSCTS, onPacket, NULL); - if (!link) { - fprintf(stderr, "secLinkOpen failed\n"); - return 1; - } - - // Authenticated key exchange. The proxy must use the same passphrase. - secLinkSetPassphrase(link, "shared secret words"); - if (secLinkHandshake(link) != SECLINK_SUCCESS) { - fprintf(stderr, "Handshake failed\n"); - secLinkClose(link); - return 1; - } - - // Send an encrypted banner and then relay keyboard input. - const char *hello = "Hello, secure BBS!\r\n"; - secLinkSend(link, (const uint8_t *)hello, (int)strlen(hello), CHAN_TERMINAL, true, true); - - while (!feof(stdin)) { - // Deliver any incoming packets to onPacket(). - secLinkPoll(link); - - // If the user typed something, forward it encrypted. - int ch = getchar(); - if (ch == EOF) { - break; - } - uint8_t byte = (uint8_t)ch; - secLinkSend(link, &byte, 1, CHAN_TERMINAL, true, true); - } - - secLinkClose(link); - secRngSaveSeed(SEED_FILE); - return 0; -} -.endcode - -Notes: - -.list -.item secLinkOpen internally calls rs232Open and pktOpen. secLinkClose tears them down in reverse order. -.item The DRBG gathers hardware entropy on first use. On a CPU without a time stamp counter that alone is guessable; the seed file and user-input timings (secRngAddEntropy) are what make the DH keys unpredictable. -.item secLinkPoll must be called frequently to drain the RX ring buffer, process ACKs, and dispatch received packets to the callback. -.item For bulk transfers larger than SECLINK_MAX_PAYLOAD (245 bytes), use secLinkSendBuf which splits the data into chunks automatically. -.endlist diff --git a/src/libs/kpunch/sql/README.md b/src/libs/kpunch/sql/README.md new file mode 100644 index 00000000..c5071df0 --- /dev/null +++ b/src/libs/kpunch/sql/README.md @@ -0,0 +1,25 @@ +# sql -- Databases (dvxsql) + +SQLite databases for DVX programs: connections and result sets by +integer handle, statements that let the other programs run while they +work, and a DOS file layer (8.3 names, DVX's random source). The Data +control and DBGrid use it; BASIC reaches it through +src/include/basic/sql.bas. Part of the optional Databases part. + +## Documentation + +Full reference is in `dvxsql.dhs` (this directory), compiled into the +DVX System Reference. Open via the Help Viewer app, or read +`docs/dvx_system_reference.html` after `make`. + +## Source Files + +- `dvxSql.h` -- public API +- `dvxSql.c` -- connections, statements, result sets, the pump +- `dvxSqlVfs.c` -- SQLite's file layer on DOS +- `thirdparty/sqlite3` -- SQLite (public domain), built with the options + in sqliteOpts.mk + +## Build + +`make -C src/libs/kpunch/sql` (invoked by the top-level `make`). diff --git a/src/libs/kpunch/sql/dvxSql.c b/src/libs/kpunch/sql/dvxSql.c index 68af96af..d6b40ce6 100644 --- a/src/libs/kpunch/sql/dvxSql.c +++ b/src/libs/kpunch/sql/dvxSql.c @@ -30,6 +30,7 @@ #include "../libdvx/dvxTypes.h" #include "thirdparty/sqlite3/sqlite3.h" +#include #include #include #include @@ -50,17 +51,24 @@ DVX_ABI_STAMP(dvxsql); #define ERR_OUT_OF_MEMORY "Out of memory growing handle table" #define ERR_NO_PATH "No database path" #define ERR_NO_MEMORY "Out of memory" +#define ERR_RUNNING "A statement is still running on this database" +#define SQL_PROGRESS_OPS 1000 // statement steps between calls of a database's pump typedef struct { - sqlite3 *db; - char lastError[SQL_ERROR_SIZE]; - int32_t affectedRows; + sqlite3 *db; + DvxSqlPumpFnT pump; // dvxSqlSetPump (NULL: none) + char lastError[SQL_ERROR_SIZE]; + int32_t affectedRows; + bool running; // a statement runs: its pump may run other code + bool closeAfter; // dvxSqlClose came while it ran + bool freesPending; // a dvxSqlFreeResult came while it ran } DbEntryT; typedef struct { sqlite3_stmt *stmt; - int32_t dbHandle; // which database owns this cursor - bool eof; // true after sqlite3_step returns SQLITE_DONE + int32_t dbHandle; // which database owns this cursor + bool eof; // true after sqlite3_step returns SQLITE_DONE + bool freeAfter; // dvxSqlFreeResult came while its database ran a statement } CursorEntryT; // Dynamically grown handle tables. sDbsCount/sCursorsCount track allocated slots. @@ -78,6 +86,7 @@ static char sOpenError[SQL_ERROR_SIZE]; // Prototypes // ============================================================ +static DbEntryT *dbSlot(int32_t db); int32_t dvxSqlAffectedRows(int32_t db); void dvxSqlClose(int32_t db); bool dvxSqlEof(int32_t rs); @@ -94,10 +103,25 @@ void dvxSqlFreeResult(int32_t rs); bool dvxSqlNext(int32_t rs); int32_t dvxSqlOpen(const char *path); int32_t dvxSqlQuery(int32_t db, const char *sql); +void dvxSqlSetPump(int32_t db, DvxSqlPumpFnT pump); +static void finalizeCursor(CursorEntryT *cur); static CursorEntryT *getCursor(int32_t handle); static DbEntryT *getDb(int32_t handle); static void *growTable(void *table, int32_t *count, size_t entrySize, int32_t initialCount); +static int progressCb(void *arg); +static CursorEntryT *readCursor(int32_t handle); static void recordError(char *buf, sqlite3 *db); +static DbEntryT *runBegin(int32_t db); +static void runEnd(int32_t db); + + +// The slot of a database the caller knows is open: one runBegin claimed +// (a dvxSqlClose waits for runEnd), or a live cursor's (closing a +// database finalizes its cursors first). Indexed again after every +// statement because the pump may have grown the table. +static DbEntryT *dbSlot(int32_t db) { + return &sDbs[db - 1]; +} int32_t dvxSqlAffectedRows(int32_t db) { @@ -111,6 +135,7 @@ int32_t dvxSqlAffectedRows(int32_t db) { } +// Closed when the statement running on it (if one is) ends. void dvxSqlClose(int32_t db) { DbEntryT *entry = getDb(db); @@ -118,11 +143,15 @@ void dvxSqlClose(int32_t db) { return; } + if (entry->running) { + entry->closeAfter = true; + return; + } + // Close any open cursors belonging to this database for (int32_t i = 0; i < sCursorsCount; i++) { if (sCursors[i].stmt && sCursors[i].dbHandle == db) { - sqlite3_finalize(sCursors[i].stmt); - memset(&sCursors[i], 0, sizeof(CursorEntryT)); + finalizeCursor(&sCursors[i]); } } @@ -150,6 +179,7 @@ const char *dvxSqlError(int32_t db) { if (db == 0 && sOpenError[0]) { return sOpenError; } + if (!entry) { return ERR_INVALID_HANDLE; } @@ -187,26 +217,35 @@ int32_t dvxSqlEscape(const char *src, char *dst, int32_t dstSize) { } +// Every statement in sql, one after another, in this one call (its pump +// runs between steps). bool dvxSqlExec(int32_t db, const char *sql) { - DbEntryT *entry = getDb(db); + DbEntryT *entry = sql ? runBegin(db) : NULL; - if (!entry || !sql) { + if (!entry) { return false; } - if (sqlite3_exec(entry->db, sql, NULL, NULL, NULL) != SQLITE_OK) { - recordError(entry->lastError, entry->db); - return false; + sqlite3 *conn = entry->db; + bool ok = sqlite3_exec(conn, sql, NULL, NULL, NULL) == SQLITE_OK; + + // The pump may have grown the table: the entry is looked up again + entry = dbSlot(db); + + if (ok) { + entry->lastError[0] = '\0'; + entry->affectedRows = sqlite3_changes(conn); + } else { + recordError(entry->lastError, conn); } - entry->lastError[0] = '\0'; - entry->affectedRows = sqlite3_changes(entry->db); - return true; + runEnd(db); + return ok; } const char *dvxSqlFieldByName(int32_t rs, const char *name) { - CursorEntryT *cur = getCursor(rs); + CursorEntryT *cur = readCursor(rs); if (!cur || !name) { return ""; @@ -228,7 +267,7 @@ const char *dvxSqlFieldByName(int32_t rs, const char *name) { int32_t dvxSqlFieldCount(int32_t rs) { - CursorEntryT *cur = getCursor(rs); + CursorEntryT *cur = readCursor(rs); if (!cur) { return 0; @@ -239,7 +278,7 @@ int32_t dvxSqlFieldCount(int32_t rs) { double dvxSqlFieldDbl(int32_t rs, int32_t col) { - CursorEntryT *cur = getCursor(rs); + CursorEntryT *cur = readCursor(rs); if (!cur) { return 0.0; @@ -250,7 +289,7 @@ double dvxSqlFieldDbl(int32_t rs, int32_t col) { int32_t dvxSqlFieldInt(int32_t rs, int32_t col) { - CursorEntryT *cur = getCursor(rs); + CursorEntryT *cur = readCursor(rs); if (!cur) { return 0; @@ -261,7 +300,7 @@ int32_t dvxSqlFieldInt(int32_t rs, int32_t col) { const char *dvxSqlFieldName(int32_t rs, int32_t col) { - CursorEntryT *cur = getCursor(rs); + CursorEntryT *cur = readCursor(rs); if (!cur) { return ""; @@ -273,7 +312,7 @@ const char *dvxSqlFieldName(int32_t rs, int32_t col) { const char *dvxSqlFieldText(int32_t rs, int32_t col) { - CursorEntryT *cur = getCursor(rs); + CursorEntryT *cur = readCursor(rs); if (!cur) { return ""; @@ -284,6 +323,7 @@ const char *dvxSqlFieldText(int32_t rs, int32_t col) { } +// Freed when the statement running on its database (if one is) ends. void dvxSqlFreeResult(int32_t rs) { CursorEntryT *cur = getCursor(rs); @@ -291,11 +331,20 @@ void dvxSqlFreeResult(int32_t rs) { return; } - sqlite3_finalize(cur->stmt); - memset(cur, 0, sizeof(CursorEntryT)); + DbEntryT *entry = getDb(cur->dbHandle); + + if (entry && entry->running) { + cur->freeAfter = true; + entry->freesPending = true; + return; + } + + finalizeCursor(cur); } +// False at the end, on an error (dvxSqlError says), and while another +// statement runs on the cursor's database. bool dvxSqlNext(int32_t rs) { CursorEntryT *cur = getCursor(rs); @@ -303,24 +352,32 @@ bool dvxSqlNext(int32_t rs) { return false; } + int32_t db = cur->dbHandle; + DbEntryT *entry = runBegin(db); + + if (!entry) { + return false; + } + int32_t rc = sqlite3_step(cur->stmt); - if (rc == SQLITE_ROW) { - return true; - } + // The pump may have grown the tables or freed this cursor (done by + // runEnd): both are looked up again + entry = dbSlot(db); + cur = &sCursors[rs - 1]; // Anything other than SQLITE_DONE is a real step error, not clean EOF; // record it so callers don't mistake a failure for end-of-results. - if (rc != SQLITE_DONE) { - DbEntryT *db = getDb(cur->dbHandle); - - if (db) { - recordError(db->lastError, db->db); + if (rc != SQLITE_ROW) { + if (rc != SQLITE_DONE) { + recordError(entry->lastError, entry->db); } + + cur->eof = true; } - cur->eof = true; - return false; + runEnd(db); + return rc == SQLITE_ROW && getCursor(rs) != NULL; } @@ -362,6 +419,8 @@ int32_t dvxSqlOpen(const char *path) { return 0; } + // The pump (none until dvxSqlSetPump) runs every SQL_PROGRESS_OPS steps + sqlite3_progress_handler(sDbs[slot].db, SQL_PROGRESS_OPS, progressCb, (void *)(intptr_t)(slot + 1)); sOpenError[0] = '\0'; sDbs[slot].lastError[0] = '\0'; sDbs[slot].affectedRows = 0; @@ -370,9 +429,37 @@ int32_t dvxSqlOpen(const char *path) { int32_t dvxSqlQuery(int32_t db, const char *sql) { - DbEntryT *entry = getDb(db); + DbEntryT *entry = sql ? runBegin(db) : NULL; - if (!entry || !sql) { + if (!entry) { + return 0; + } + + // Preparing can read the schema, which runs statements (and the pump, + // which may take a free slot): the slot is found afterwards + sqlite3_stmt *stmt = NULL; + bool ok = sqlite3_prepare_v2(entry->db, sql, -1, &stmt, NULL) == SQLITE_OK; + + entry = dbSlot(db); + + // A dvxSqlClose from the pump: the statement goes before the database + if (!ok || entry->closeAfter) { + if (!ok) { + recordError(entry->lastError, entry->db); + } + + sqlite3_finalize(stmt); + runEnd(db); + return 0; + } + + runEnd(db); + + // Empty or comment-only SQL prepares to no statement. Storing that NULL + // would leave the slot looking free, so the next query would reuse it + // and two live handles would alias one cursor. + if (!stmt) { + snprintf(entry->lastError, sizeof(entry->lastError), "%s", ERR_EMPTY_STATEMENT); return 0; } @@ -391,26 +478,12 @@ int32_t dvxSqlQuery(int32_t db, const char *sql) { slot = sCursorsCount; sCursors = (CursorEntryT *)growTable(sCursors, &sCursorsCount, sizeof(CursorEntryT), CURSOR_TABLE_INITIAL); if (slot == sCursorsCount) { + sqlite3_finalize(stmt); snprintf(entry->lastError, sizeof(entry->lastError), "%s", ERR_OUT_OF_MEMORY); return 0; } } - sqlite3_stmt *stmt = NULL; - - if (sqlite3_prepare_v2(entry->db, sql, -1, &stmt, NULL) != SQLITE_OK) { - recordError(entry->lastError, entry->db); - return 0; - } - - // Empty or comment-only SQL prepares to no statement. Storing that NULL - // would leave the slot looking free, so the next query would reuse it - // and two live handles would alias one cursor. - if (!stmt) { - snprintf(entry->lastError, sizeof(entry->lastError), "%s", ERR_EMPTY_STATEMENT); - return 0; - } - entry->lastError[0] = '\0'; sCursors[slot].stmt = stmt; sCursors[slot].dbHandle = db; @@ -419,12 +492,29 @@ int32_t dvxSqlQuery(int32_t db, const char *sql) { } +void dvxSqlSetPump(int32_t db, DvxSqlPumpFnT pump) { + DbEntryT *entry = getDb(db); + + if (entry) { + entry->pump = pump; + } +} + + +// Finalize a cursor's statement and free its slot. +static void finalizeCursor(CursorEntryT *cur) { + sqlite3_finalize(cur->stmt); + memset(cur, 0, sizeof(CursorEntryT)); +} + + +// A live cursor (one freed while its database ran a statement is gone). static CursorEntryT *getCursor(int32_t handle) { if (handle <= 0 || handle > sCursorsCount) { return NULL; } - if (!sCursors[handle - 1].stmt) { + if (!sCursors[handle - 1].stmt || sCursors[handle - 1].freeAfter) { return NULL; } @@ -462,6 +552,30 @@ static void *growTable(void *table, int32_t *count, size_t entrySize, int32_t in } +// sqlite3_progress_handler's callback, every SQL_PROGRESS_OPS steps of a +// statement: the database's pump. Non-zero stops the statement. The +// entry is not touched after the pump, which may grow the table. +static int progressCb(void *arg) { + int32_t db = (int32_t)(intptr_t)arg; + DbEntryT *entry = getDb(db); + + return (entry && entry->pump && !entry->pump(db)) ? 1 : 0; +} + + +// A cursor whose values can be read: not while its database runs a +// statement (it may be this cursor's, between steps). +static CursorEntryT *readCursor(int32_t handle) { + CursorEntryT *cur = getCursor(handle); + + if (!cur || dbSlot(cur->dbHandle)->running) { + return NULL; + } + + return cur; +} + + // Copy db's last error into buf (SQL_ERROR_SIZE bytes). A file error // also names the operating-system reason, e.g. "unable to open database // file (No such file or directory)". @@ -475,3 +589,46 @@ static void recordError(char *buf, sqlite3 *db) { snprintf(buf, SQL_ERROR_SIZE, "%s", sqlite3_errmsg(db)); } } + + +// Mark db running a statement: its entry, or NULL (refused, the error +// recorded) for a bad handle or while another statement runs on it (its +// pump runs other code, which may call back in). +static DbEntryT *runBegin(int32_t db) { + DbEntryT *entry = getDb(db); + + if (!entry) { + return NULL; + } + + if (entry->running) { + snprintf(entry->lastError, sizeof(entry->lastError), "%s", ERR_RUNNING); + return NULL; + } + + entry->running = true; + return entry; +} + + +// The statement on db ended: the frees and the close asked for while it +// ran are done now. +static void runEnd(int32_t db) { + DbEntryT *entry = dbSlot(db); + + entry->running = false; + + if (entry->freesPending) { + entry->freesPending = false; + + for (int32_t i = 0; i < sCursorsCount; i++) { + if (sCursors[i].stmt && sCursors[i].dbHandle == db && sCursors[i].freeAfter) { + finalizeCursor(&sCursors[i]); + } + } + } + + if (entry->closeAfter) { + dvxSqlClose(db); + } +} diff --git a/src/libs/kpunch/sql/dvxSql.h b/src/libs/kpunch/sql/dvxSql.h index 367279ea..5ed48987 100644 --- a/src/libs/kpunch/sql/dvxSql.h +++ b/src/libs/kpunch/sql/dvxSql.h @@ -44,8 +44,9 @@ int32_t dvxSqlOpen(const char *path); // Close a database and free its resources. void dvxSqlClose(int32_t db); -// Execute a SQL statement that returns no results (DDL, INSERT, UPDATE, DELETE). -// Returns true on success, false on error. +// Execute SQL that returns no results (DDL, INSERT, UPDATE, DELETE): every +// statement in sql, one after another, in this one call. Returns true on +// success, false on error. bool dvxSqlExec(int32_t db, const char *sql); // Return the last error message for a database handle. For handle 0 it @@ -55,6 +56,20 @@ const char *dvxSqlError(int32_t db); // Return the number of rows affected by the last INSERT/UPDATE/DELETE. int32_t dvxSqlAffectedRows(int32_t db); +// A database's pump (dvxSqlSetPump): called with the database's handle +// every so many steps of a statement running on it (dvxSqlExec, +// dvxSqlQuery, dvxSqlNext), so a long one can let other work run. +// Returning false stops the statement: the call fails ("interrupted"). +typedef bool (*DvxSqlPumpFnT)(int32_t db); + +// Give db a pump (NULL: none, as a database opens). While a statement +// runs on a database, calls the pump makes on that same database are +// refused: dvxSqlExec, dvxSqlQuery, and dvxSqlNext fail ("A statement is +// still running on this database"), the field functions of its cursors +// return "" or 0, and dvxSqlClose and dvxSqlFreeResult are carried out +// when the statement ends. Other databases can be used as always. +void dvxSqlSetPump(int32_t db, DvxSqlPumpFnT pump); + // ============================================================ // Result set (cursor) operations // ============================================================ diff --git a/src/libs/kpunch/sql/dvxSqlVfs.c b/src/libs/kpunch/sql/dvxSqlVfs.c index fb59262f..740270b9 100644 --- a/src/libs/kpunch/sql/dvxSqlVfs.c +++ b/src/libs/kpunch/sql/dvxSqlVfs.c @@ -37,7 +37,7 @@ // with no extension (TESTDB) would still get TESTDB-journal, which DOS // truncates to eight characters; vfsMapName gives those TESTDB.NAL. // - Temporary files get 8.3 names (SQLxxxxx.TMP) in TMPDIR, TEMP or TMP. -// - Randomness comes from the serial library's DRBG when it is loaded +// - Randomness comes from the cryptography library's DRBG when it is loaded // (looked up at run time, so there is no load dependency), else from // platformEntropyStamp mixed with the time of day. diff --git a/src/libs/kpunch/sql/dvxsql.dhs b/src/libs/kpunch/sql/dvxsql.dhs index 3075e66e..d3213352 100644 --- a/src/libs/kpunch/sql/dvxsql.dhs +++ b/src/libs/kpunch/sql/dvxsql.dhs @@ -67,7 +67,7 @@ The DOS port is SQLite's SQLITE_OS_OTHER build with a small DVX file layer, the .item Paths are made absolute and canonical (relative names join the current directory, backslashes become forward slashes, "." and ".." are resolved), so every spelling of one file is the same database. .item Locking is in-process: DOS is single user, but two connections inside DVX (two programs, or a Data control and BASIC code) can share a file. Any number may read; one may write. A write while another connection is writing, or a commit while another connection has a query open, fails with "database is locked"; retry after the other side finishes (dvxSqlFreeResult releases a query's read lock). Nothing is locked at the DOS level, so do not share a database with another program outside DVX. .item Temporary files, when used, are SQLxxxxx.TMP in the directory named by TMPDIR, TEMP, or TMP (else the current directory) and are deleted when closed. -.item Randomness comes from the serial library's generator (secRngBytes) when that library is loaded, else from platformEntropyStamp mixed with the time of day. +.item Randomness comes from the cryptography library's generator (secRngBytes) when that library is loaded, else from platformEntropyStamp mixed with the time of day. .endlist Databases written by the SQLite 3.5.6 that earlier DVX releases bundled open, read, and write unchanged. Files created now use the current file format, which SQLite 3.5.6 cannot read. @@ -106,6 +106,8 @@ Database and cursor handles are int32_t values. A successful open or query retur .index dvxSqlExec .index dvxSqlError .index dvxSqlAffectedRows +.index dvxSqlSetPump +.index DvxSqlPumpFnT .h2 Database Operations @@ -147,7 +149,7 @@ Safe to call with an invalid handle (no-op). bool dvxSqlExec(int32_t db, const char *sql); .endcode -Execute one or more SQL statements that return no result rows. Suitable for DDL (CREATE TABLE, DROP TABLE, etc.) and DML (INSERT, UPDATE, DELETE). +Execute one or more SQL statements that return no result rows. Suitable for DDL (CREATE TABLE, DROP TABLE, etc.) and DML (INSERT, UPDATE, DELETE). Every statement in sql runs, one after another, within this one call; give the database a pump (dvxSqlSetPump) when that may take long. .table Parameter Description @@ -190,6 +192,33 @@ Return the number of rows inserted, updated, or deleted by the last dvxSqlExec c Returns the row count, or 0 if the handle is invalid. +.h3 dvxSqlSetPump + +.code +typedef bool (*DvxSqlPumpFnT)(int32_t db); + +void dvxSqlSetPump(int32_t db, DvxSqlPumpFnT pump); +.endcode + +Give a database a pump, or take it away (NULL; a database opens without one). While a statement runs on the database (in dvxSqlExec, dvxSqlQuery, or dvxSqlNext, whose first step of a sorted or grouped query reads every row), the pump is called with the database's handle every 1000 steps or so. It can let other work run, for example a frame of DVX's. Returning false stops the statement: the call fails, dvxSqlError says "interrupted", and what the statement changed is undone. + +.table + Parameter Description + --------- ----------- + db Database handle. + pump The function to call, or NULL for none. +.endtable + +The code a pump runs may use other databases, open and close them, as always. Its calls on the database whose statement is running are refused, as are any calls made from the pump on it: + +.list +.item dvxSqlExec, dvxSqlQuery, and dvxSqlNext fail, and dvxSqlError says "A statement is still running on this database". +.item The field functions of that database's cursors return "" or 0. +.item dvxSqlFreeResult and dvxSqlClose are carried out when the running statement ends. The cursor or database can no longer be used from then on. +.endlist + +DVX BASIC gives every database a program opens a pump (see SQLOpen in the BASIC reference). The Data control's own connections have none. + .topic sql.cursor .title Cursor Operations .toc 1 Cursor Operations diff --git a/src/libs/kpunch/sql/thirdparty/sqlite3/sqliteOpts.mk b/src/libs/kpunch/sql/thirdparty/sqlite3/sqliteOpts.mk index d2f60d8b..c923d43e 100644 --- a/src/libs/kpunch/sql/thirdparty/sqlite3/sqliteOpts.mk +++ b/src/libs/kpunch/sql/thirdparty/sqlite3/sqliteOpts.mk @@ -23,11 +23,11 @@ SQLITE_OPTS += -DSQLITE_DEFAULT_MEMSTATUS=0 SQLITE_OPTS += -DSQLITE_OMIT_WAL SQLITE_OPTS += -DSQLITE_MAX_MMAP_SIZE=0 -# Features DVX does not use. +# Features DVX does not use. (The progress callback stays: a database's +# pump runs from it, dvxSqlSetPump.) SQLITE_OPTS += -DSQLITE_OMIT_LOAD_EXTENSION SQLITE_OPTS += -DSQLITE_OMIT_SHARED_CACHE SQLITE_OPTS += -DSQLITE_OMIT_DEPRECATED -SQLITE_OPTS += -DSQLITE_OMIT_PROGRESS_CALLBACK SQLITE_OPTS += -DSQLITE_OMIT_DECLTYPE SQLITE_OPTS += -DSQLITE_OMIT_DESERIALIZE SQLITE_OPTS += -DSQLITE_OMIT_GET_TABLE @@ -35,6 +35,17 @@ SQLITE_OPTS += -DSQLITE_OMIT_JSON SQLITE_OPTS += -DSQLITE_OMIT_UTF16 SQLITE_OPTS += -DSQLITE_UNTESTABLE +# Size: hooks DVX never calls (trace and authorizer callbacks, +# sqlite3_complete, the introspection pragmas such as function_list). +# Not taken: OMIT_COMPILEOPTION_DIAGS (the SQL suite checks this list +# through sqlite3_compileoption_used), OMIT_AUTOINIT (dvxSql relies on +# sqlite3_open initializing), and MAX_EXPR_DEPTH=0 (the depth limit keeps +# a deeply nested expression from overflowing a task's stack). +SQLITE_OPTS += -DSQLITE_OMIT_TRACE +SQLITE_OPTS += -DSQLITE_OMIT_AUTHORIZATION +SQLITE_OPTS += -DSQLITE_OMIT_COMPLETE +SQLITE_OPTS += -DSQLITE_OMIT_INTROSPECTION_PRAGMAS + # Recommended strictness: "text" is an identifier, never a string, and # LIKE does not look inside BLOBs. SQLITE_OPTS += -DSQLITE_DQS=0 diff --git a/src/libs/kpunch/texthelp/textHelp.c b/src/libs/kpunch/texthelp/textHelp.c index 5426ce49..175b2eae 100644 --- a/src/libs/kpunch/texthelp/textHelp.c +++ b/src/libs/kpunch/texthelp/textHelp.c @@ -92,10 +92,12 @@ void textEditLineCacheInit(TextEditLineCacheT *lc, int32_t tabWidth); void textEditLineCacheNotifyDelete(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t deleteLen, bool deletedNewline); void textEditLineCacheNotifyInsert(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t insertLen); static void textEditLineCacheRebuild(TextEditLineCacheT *lc, const char *buf, int32_t len); +static bool textEditLineCacheRoom(TextEditLineCacheT *lc, int32_t lineCount); void textEditLineCacheSetTabWidth(TextEditLineCacheT *lc, int32_t tabWidth); int32_t textEditLineCount(TextEditLineCacheT *lc, const char *buf, int32_t len); int32_t textEditLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row); int32_t textEditLineStart(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row); +static int32_t textEditLineVisLen(const TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t start, int32_t end); static bool textEditMatchAt(const char *buf, int32_t pos, const char *needle, int32_t needleLen, bool caseSensitive); int32_t textEditMaxLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len); void textEditMoveCursor(int32_t newPos, bool shift, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd); @@ -481,11 +483,10 @@ void textEditLineCacheInit(TextEditLineCacheT *lc, int32_t tabWidth) { // Incrementally update the cache after deleting bytes at `off`. // Caller passes the POST-delete buffer and length, and whether the // removed bytes contained a newline (the post-delete buffer cannot -// tell). Falls back to a full rebuild when a newline was removed or -// the deletion is longer than one byte. +// tell). One byte is patched in: a newline joins its line and the next. +// Falls back to a full rebuild for a longer deletion. void textEditLineCacheNotifyDelete(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t deleteLen, bool deletedNewline) { - // Only single-byte, same-line deletes are patched incrementally. - if (lc->cachedLines < 0 || deleteLen != 1 || deletedNewline) { + if (lc->cachedLines < 0 || deleteLen != 1) { textEditLineCacheDirty(lc); return; } @@ -498,6 +499,18 @@ void textEditLineCacheNotifyDelete(TextEditLineCacheT *lc, const char *buf, int3 } } + // A newline gone: the line below joins this one + if (deletedNewline) { + if (line + 1 >= lc->cachedLines) { + textEditLineCacheDirty(lc); + return; + } + + memmove(&lc->lineOffsets[line + 1], &lc->lineOffsets[line + 2], (size_t)(lc->cachedLines - line - 1) * sizeof(int32_t)); + memmove(&lc->lineVisLens[line + 1], &lc->lineVisLens[line + 2], (size_t)(lc->cachedLines - line - 2) * sizeof(int32_t)); + lc->cachedLines--; + } + for (int32_t i = line + 1; i <= lc->cachedLines; i++) { lc->lineOffsets[i] -= deleteLen; } @@ -510,45 +523,37 @@ void textEditLineCacheNotifyDelete(TextEditLineCacheT *lc, const char *buf, int3 return; } - for (int32_t i = lineOff; i < nextOff && i < len; i++) { + for (int32_t i = lineOff; i < nextOff - 1 && i < len; i++) { if (buf[i] == '\n') { - if (i < nextOff - 1) { - textEditLineCacheDirty(lc); - return; - } + textEditLineCacheDirty(lc); + return; } } - int32_t tabW = lc->tabWidth; - int32_t vc = 0; - - for (int32_t i = lineOff; i < nextOff && i < len && buf[i] != '\n'; i++) { - if (buf[i] == '\t') { - vc += tabW - (vc % tabW); - } else { - vc++; - } - } - - lc->lineVisLens[line] = vc; + lc->lineVisLens[line] = textEditLineVisLen(lc, buf, len, lineOff, nextOff); lc->cachedMaxLL = -1; } // Incrementally update the cache after inserting bytes at `off`. -// Caller passes the POST-insert buffer and length. Falls back to a -// full rebuild if the insertion contains newlines. +// Caller passes the POST-insert buffer and length. Newlines in the +// insert (Enter, a paste) add their lines to the table: the text is not +// scanned again. void textEditLineCacheNotifyInsert(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t insertLen) { if (lc->cachedLines < 0 || insertLen <= 0) { textEditLineCacheDirty(lc); return; } + int32_t added = 0; + for (int32_t i = off; i < off + insertLen && i < len; i++) { - if (buf[i] == '\n') { - textEditLineCacheDirty(lc); - return; - } + added += buf[i] == '\n'; + } + + if (!textEditLineCacheRoom(lc, lc->cachedLines + added)) { + textEditLineCacheDirty(lc); + return; } int32_t line = 0; @@ -559,30 +564,38 @@ void textEditLineCacheNotifyInsert(TextEditLineCacheT *lc, const char *buf, int3 } } - for (int32_t i = line + 1; i <= lc->cachedLines; i++) { + // The later lines move along by the insert and down by the lines it + // adds, whose starts follow its newlines + memmove(&lc->lineOffsets[line + 1 + added], &lc->lineOffsets[line + 1], (size_t)(lc->cachedLines - line) * sizeof(int32_t)); + memmove(&lc->lineVisLens[line + 1 + added], &lc->lineVisLens[line + 1], (size_t)(lc->cachedLines - 1 - line) * sizeof(int32_t)); + lc->cachedLines += added; + + for (int32_t i = line + 1 + added; i <= lc->cachedLines; i++) { lc->lineOffsets[i] += insertLen; } - int32_t lineOff = lc->lineOffsets[line]; - int32_t lineEnd = lc->lineOffsets[line + 1]; - int32_t tabW = lc->tabWidth; - int32_t vc = 0; + int32_t at = line + 1; - for (int32_t i = lineOff; i < lineEnd && buf[i] != '\n'; i++) { - if (buf[i] == '\t') { - vc += tabW - (vc % tabW); - } else { - vc++; + for (int32_t i = off; i < off + insertLen && i < len; i++) { + if (buf[i] == '\n') { + lc->lineOffsets[at++] = i + 1; } } - lc->lineVisLens[line] = vc; + int32_t widest = 0; + + for (int32_t l = line; l <= line + added; l++) { + lc->lineVisLens[l] = textEditLineVisLen(lc, buf, len, lc->lineOffsets[l], lc->lineOffsets[l + 1]); + widest = DVX_MAX(widest, lc->lineVisLens[l]); + } // cachedMaxLL == -1 is the "unknown/needs-recompute" sentinel. An insert - // can only grow the edited line's visual length, so only grow a KNOWN max - // and never clobber the sentinel with a single line's length. - if (lc->cachedMaxLL >= 0 && vc > lc->cachedMaxLL) { - lc->cachedMaxLL = vc; + // within a line can only grow it, so only grow a KNOWN max; a split line + // may have been the longest, so the max is found again. + if (added > 0) { + lc->cachedMaxLL = -1; + } else if (lc->cachedMaxLL >= 0 && widest > lc->cachedMaxLL) { + lc->cachedMaxLL = widest; } } @@ -599,33 +612,10 @@ static void textEditLineCacheRebuild(TextEditLineCacheT *lc, const char *buf, in } } - int32_t needed = lineCount + 1; - - if (needed > lc->lineOffsetCap) { - int32_t newCap = needed + TEXT_EDIT_LINE_CAP_GROWTH; - int32_t *tmp = (int32_t *)realloc(lc->lineOffsets, newCap * sizeof(int32_t)); - - if (!tmp) { - // Leave the cache marked invalid so a later call retries. - lc->cachedLines = -1; - return; - } - - lc->lineOffsets = tmp; - lc->lineOffsetCap = newCap; - } - - if (lineCount > lc->lineVisLenCap) { - int32_t newCap = lineCount + TEXT_EDIT_LINE_CAP_GROWTH; - int32_t *tmp2 = (int32_t *)realloc(lc->lineVisLens, newCap * sizeof(int32_t)); - - if (!tmp2) { - lc->cachedLines = -1; - return; - } - - lc->lineVisLens = tmp2; - lc->lineVisLenCap = newCap; + if (!textEditLineCacheRoom(lc, lineCount)) { + // Leave the cache marked invalid so a later call retries. + lc->cachedLines = -1; + return; } int32_t line = 0; @@ -664,6 +654,37 @@ static void textEditLineCacheRebuild(TextEditLineCacheT *lc, const char *buf, in } +// Room in the tables for lineCount lines (and the end sentinel); false +// when out of memory. +static bool textEditLineCacheRoom(TextEditLineCacheT *lc, int32_t lineCount) { + if (lineCount + 1 > lc->lineOffsetCap) { + int32_t newCap = lineCount + 1 + TEXT_EDIT_LINE_CAP_GROWTH; + int32_t *tmp = (int32_t *)realloc(lc->lineOffsets, newCap * sizeof(int32_t)); + + if (!tmp) { + return false; + } + + lc->lineOffsets = tmp; + lc->lineOffsetCap = newCap; + } + + if (lineCount > lc->lineVisLenCap) { + int32_t newCap = lineCount + TEXT_EDIT_LINE_CAP_GROWTH; + int32_t *tmp = (int32_t *)realloc(lc->lineVisLens, newCap * sizeof(int32_t)); + + if (!tmp) { + return false; + } + + lc->lineVisLens = tmp; + lc->lineVisLenCap = newCap; + } + + return true; +} + + void textEditLineCacheSetTabWidth(TextEditLineCacheT *lc, int32_t tabWidth) { int32_t newW = tabWidth > 0 ? tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH; @@ -715,14 +736,33 @@ int32_t textEditLineStart(TextEditLineCacheT *lc, const char *buf, int32_t len, } -// True if needle (needleLen bytes) occurs at buf + pos. Caller -// guarantees pos + needleLen is within the buffer. -static bool textEditMatchAt(const char *buf, int32_t pos, const char *needle, int32_t needleLen, bool caseSensitive) { - if (caseSensitive) { - return memcmp(buf + pos, needle, needleLen) == 0; +// The tab-expanded columns of the line from start to end (its newline +// not counted). +static int32_t textEditLineVisLen(const TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t start, int32_t end) { + int32_t tabW = lc->tabWidth; + int32_t vc = 0; + + for (int32_t i = start; i < end && i < len && buf[i] != '\n'; i++) { + if (buf[i] == '\t') { + vc += tabW - (vc % tabW); + } else { + vc++; + } } - return strncasecmp(buf + pos, needle, needleLen) == 0; + return vc; +} + + +// True if needle (needleLen bytes) occurs at buf + pos. Caller +// guarantees pos + needleLen is within the buffer. +// The first letters are compared before the rest: most places differ there. +static bool textEditMatchAt(const char *buf, int32_t pos, const char *needle, int32_t needleLen, bool caseSensitive) { + if (caseSensitive) { + return buf[pos] == needle[0] && memcmp(buf + pos, needle, needleLen) == 0; + } + + return tolower((uint8_t)buf[pos]) == tolower((uint8_t)needle[0]) && strncasecmp(buf + pos, needle, needleLen) == 0; } @@ -839,6 +879,7 @@ bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth) { te->buf[0] = '\0'; te->undoBuf[0] = '\0'; te->undoLen = TEXT_EDIT_UNDO_NONE; + te->runEnd = TEXT_EDIT_NO_RUN; te->selAnchor = -1; te->selCursor = -1; textEditLineCacheInit(&te->lines, tabWidth); @@ -943,7 +984,7 @@ bool textEditMultiReplaceSel(TextEditMultiT *te, const char *text, int32_t textL memmove(te->buf + off + insert, te->buf + off, te->len - off + 1); memcpy(te->buf + off, text, insert); te->len += insert; - textEditLineCacheDirty(lc); + textEditLineCacheNotifyInsert(lc, te->buf, te->len, off, insert); textEditOffToRowCol(lc, te->buf, te->len, off + insert, &te->cursorRow, &te->cursorCol); } @@ -1024,10 +1065,12 @@ void textEditOffToRowCol(TextEditLineCacheT *lc, const char *buf, int32_t len, i } -// Replaces every occurrence of needle with replacement. Records a -// single undo snapshot before mutating. Clamps the cursor to the -// post-replace buffer length and clears any selection. Returns the -// count of replacements made. +// Replaces every occurrence of needle with replacement (until one would +// not fit in bufSize). Records a single undo snapshot before mutating, +// and builds the new text from it in one pass (from a copy when there is +// no undo buffer), not shifting the rest of the text per match. Clamps +// the cursor to the post-replace buffer length and clears any selection. +// Returns the count of replacements made (0 when out of memory). int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor) { if (!needle || !needle[0] || !replacement) { return 0; @@ -1037,35 +1080,50 @@ int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, i int32_t replLen = strlen(replacement); int32_t delta = replLen - needleLen; int32_t count = 0; + int32_t srcLen = *pLen; - if (needleLen > *pLen) { + if (needleLen > srcLen) { return 0; } - textEditSaveUndo(buf, *pLen, textEditRowColToOff(lc, buf, *pLen, *pCursorRow, *pCursorCol), undoBuf, pUndoLen, pUndoCursor, bufSize); + textEditSaveUndo(buf, srcLen, textEditRowColToOff(lc, buf, srcLen, *pCursorRow, *pCursorCol), undoBuf, pUndoLen, pUndoCursor, bufSize); - int32_t pos = 0; + char *copy = undoBuf ? NULL : (char *)malloc((size_t)srcLen + 1); + const char *src = undoBuf ? undoBuf : copy; - while (pos + needleLen <= *pLen) { - if (textEditMatchAt(buf, pos, needle, needleLen, caseSensitive)) { - if (*pLen + delta >= bufSize) { - break; - } - - if (delta != 0) { - memmove(buf + pos + replLen, buf + pos + needleLen, *pLen - pos - needleLen); - } - - memcpy(buf + pos, replacement, replLen); - *pLen += delta; - buf[*pLen] = '\0'; - count++; - pos += replLen; - } else { - pos++; - } + if (!src) { + return 0; } + if (copy) { + memcpy(copy, buf, (size_t)srcLen); + } + + int32_t out = 0; // bytes of the new text in buf + int32_t runStart = 0; // the source bytes not yet copied start here + int32_t newLen = srcLen; + + for (int32_t in = 0; in + needleLen <= srcLen; ) { + if (newLen + delta >= bufSize || !textEditMatchAt(src, in, needle, needleLen, caseSensitive)) { + in++; + continue; + } + + memcpy(buf + out, src + runStart, (size_t)(in - runStart)); + out += in - runStart; + memcpy(buf + out, replacement, (size_t)replLen); + out += replLen; + in += needleLen; + runStart = in; + newLen += delta; + count++; + } + + memcpy(buf + out, src + runStart, (size_t)(srcLen - runStart)); + *pLen = out + srcLen - runStart; + buf[*pLen] = '\0'; + free(copy); + if (count > 0) { *pSelAnchor = -1; *pSelCursor = -1; @@ -1640,6 +1698,10 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditMult bool shift = (mod & KEY_MOD_SHIFT) != 0; int32_t tabW = opts->tabWidth > 0 ? opts->tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH; bool changed = false; + int32_t runEnd = te->runEnd; + + // Any key but a character typed on at the run's end ends the run + te->runEnd = TEXT_EDIT_NO_RUN; #define CUR_OFF() textEditRowColToOff(lc, buf, *pLen, *pRow, *pCol) #define FROM_OFF(o) textEditOffToRowCol(lc, buf, *pLen, (o), pRow, pCol) @@ -1711,7 +1773,7 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditMult buf[off] = '\n'; memcpy(buf + off + 1, indentBuf, indent); *pLen += 1 + indent; - textEditLineCacheDirty(lc); + textEditLineCacheNotifyInsert(lc, buf, *pLen, off, 1 + indent); (*pRow)++; *pCol = indent; *pDesiredCol = indent; @@ -1789,7 +1851,14 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditMult if (key >= KEY_ASCII_PRINT_FIRST && key <= KEY_ASCII_PRINT_LAST) { if (*pLen < bufSize - 1) { - textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize); + int32_t at = CUR_OFF(); + + // A run of typing is one undo step: the slot keeps the + // text from before its first key (not copied per key) + if (HAS_SEL() || runEnd != at || te->runLen != *pLen || *pUndoLen == TEXT_EDIT_UNDO_NONE || *pUndoCursor != te->runCursor) { + textEditSaveUndo(buf, *pLen, at, undoBuf, pUndoLen, pUndoCursor, bufSize); + te->runCursor = *pUndoCursor; + } if (HAS_SEL()) { DELETE_SEL(); @@ -1804,6 +1873,8 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditMult (*pCol)++; *pDesiredCol = *pCol; changed = true; + te->runEnd = off + 1; + te->runLen = *pLen; } goto done; diff --git a/src/libs/kpunch/texthelp/textHelp.h b/src/libs/kpunch/texthelp/textHelp.h index 25731e68..db9bb763 100644 --- a/src/libs/kpunch/texthelp/textHelp.h +++ b/src/libs/kpunch/texthelp/textHelp.h @@ -22,10 +22,12 @@ // textHelp.h -- Public API for the shared text editing infrastructure library // -// Declares clipboard operations, multi-click detection, word boundary -// logic, cross-widget selection clearing, and the single-line text -// editing engine shared across widget DXEs (TextInput, Spinner, -// ComboBox, AnsiTerm, etc.). +// Declares the text editing pieces shared across widget DXEs (TextInput, +// Spinner, ComboBox, AnsiTerm, etc.): the cursor blink, cross-widget +// selection clearing, word boundaries and visual columns, the single-line +// editing engine (copy, cursor moves, undo), the multi-line line cache, +// editor, painting, and scrolling, and syntax-coloured text drawing. The +// clipboard and multi-click counting are libdvx's (dvxWgtP.h). #ifndef TEXT_HELP_H #define TEXT_HELP_H @@ -54,6 +56,9 @@ // wholesale (SetText) so undo never restores unrelated content. #define TEXT_EDIT_UNDO_NONE (-1) +// TextEditMultiT.runEnd when the undo slot holds no typing run. +#define TEXT_EDIT_NO_RUN (-1) + // ============================================================ // Cursor blink // ============================================================ @@ -295,6 +300,15 @@ typedef struct TextEditMultiT { int32_t undoLen; int32_t undoCursor; + // A run of typed characters is one undo step: while the cursor stays + // where the last one went (runEnd), the text runLen long and the + // slot's cursor runCursor (no other edit since), the next key keeps + // the slot's snapshot instead of copying the text again. + // TEXT_EDIT_NO_RUN: none. + int32_t runEnd; + int32_t runLen; + int32_t runCursor; + TextEditLineCacheT lines; } TextEditMultiT; diff --git a/src/libs/kpunch/xfer/xfer.c b/src/libs/kpunch/xfer/xfer.c index ca6bf2bd..66142cc3 100644 --- a/src/libs/kpunch/xfer/xfer.c +++ b/src/libs/kpunch/xfer/xfer.c @@ -45,13 +45,10 @@ // ABI stamp checked by the loader (see DVX_ABI_VERSION). DVX_ABI_STAMP(xfer); -#define CRC16_POLY 0x1021 -#define CRC16_TOP 0x8000 #define CRC32_POLY 0xEDB88320U #define CRC_TABLE_SIZE 256 #define BITS_PER_BYTE 8 #define BYTE_MASK 0xFF -#define CRC_HI_SHIFT 8 // Packet layout #define OFS_START 0 @@ -112,6 +109,10 @@ static const uint8_t sDetectSeq[] = { '*', '*', 0x18, 'B', '0', '0' }; // A file the transfer counters (Longs) cannot carry #define TOO_LARGE_MSG "A file larger than 2 GB cannot be transferred" +// xferSplitList: one path per line +#define LIST_SEP '\n' +#define LIST_CR '\r' + static uint32_t sCrc32Table[CRC_TABLE_SIZE]; static bool sCrc32Ready = false; @@ -156,7 +157,7 @@ static void appendCheck(XferT *x, int32_t dataLen) { if (x->useCrc) { uint16_t crc = xferCrc16(data, dataLen); - x->out[x->outLen++] = (uint8_t)(crc >> CRC_HI_SHIFT); + x->out[x->outLen++] = (uint8_t)(crc >> BITS_PER_BYTE); x->out[x->outLen++] = (uint8_t)(crc & BYTE_MASK); return; } @@ -170,6 +171,7 @@ static int32_t blockSizeOf(uint8_t start) { } +// CRC-32 (ZMODEM's, reflected) a byte at a time. CRC-16 is dvxCrc16. static void buildCrc32Table(void) { for (uint32_t i = 0; i < CRC_TABLE_SIZE; i++) { uint32_t c = i; @@ -193,7 +195,7 @@ static bool checkOk(const XferT *x, int32_t dataLen) { if (x->useCrc) { uint16_t crc = xferCrc16(data, dataLen); - return check[0] == (uint8_t)(crc >> CRC_HI_SHIFT) && check[1] == (uint8_t)(crc & BYTE_MASK); + return check[0] == (uint8_t)(crc >> BITS_PER_BYTE) && check[1] == (uint8_t)(crc & BYTE_MASK); } return check[0] == checksum8(data, dataLen); @@ -968,20 +970,7 @@ void xferCloseFile(XferT *x, bool keep) { uint16_t xferCrc16(const uint8_t *data, int32_t len) { - return xferCrc16Add(0, data, len); -} - - -uint16_t xferCrc16Add(uint16_t crc, const uint8_t *data, int32_t len) { - for (int32_t i = 0; i < len; i++) { - crc ^= (uint16_t)(data[i] << CRC_HI_SHIFT); - - for (int32_t bit = 0; bit < BITS_PER_BYTE; bit++) { - crc = (crc & CRC16_TOP) ? (uint16_t)((crc << 1) ^ CRC16_POLY) : (uint16_t)(crc << 1); - } - } - - return crc; + return dvxCrc16(XFER_CRC16_INIT, data, len); } @@ -1294,6 +1283,33 @@ bool xferSetFileBytes(XferT *x, int64_t bytes) { } +int32_t xferSplitList(char *list, const char **paths) { + int32_t count = 0; + + for (char *p = list; p && *p && count < XFER_MAX_FILES; ) { + char *end = strchr(p, LIST_SEP); + + if (end) { + *end = '\0'; + } + + size_t len = strlen(p); + + if (len > 0 && p[len - 1] == LIST_CR) { + p[len - 1] = '\0'; + } + + if (*p) { + paths[count++] = p; + } + + p = end ? end + 1 : NULL; + } + + return count; +} + + bool xferStartReceive(XferT *x, XferProtoE proto, const XferIoT *io, const XferOptionsT *opt, const char *target, uint32_t nowMs) { if (!initCommon(x, proto, io, opt, false)) { return false; diff --git a/src/libs/kpunch/xfer/xfer.dhs b/src/libs/kpunch/xfer/xfer.dhs index 1e3159f2..1272b7e7 100644 --- a/src/libs/kpunch/xfer/xfer.dhs +++ b/src/libs/kpunch/xfer/xfer.dhs @@ -117,6 +117,7 @@ if (xferPoll(x, nowMs()) != XferRunningE) { const XferProgressT *xferProgress(x) Progress and outcome. bool xferDetectFeed(d, byte) Feed received terminal bytes; true when a ZMODEM sender's start frame (ZRQINIT) has arrived, so a terminal can start a ZMODEM receive by itself. void xferSafeName(name, longNames, out) Reduce a sender's file name to a safe one: no path, no drive, no DOS device name (CON, PRN, COM1, and so on get a leading _). With longNames (the receiving disk takes long names, platformLongNames) a long name keeping its case, blanks, and dots, where a character a long name cannot hold becomes _ and leading blanks and trailing dots and blanks go (RECEIVED when nothing is left); otherwise an 8.3 name of portable characters in upper case. out holds XFER_NAME_MAX bytes (PLATFORM_LONG_NAME_MAX + 1). + int32_t xferSplitList(list, paths) Split a list of files to send, one path per line (LF or CR LF, empty lines skipped), in place into paths[XFER_MAX_FILES]; the count. ConnSendFiles and XferSendFiles use it. uint16_t xferCrc16(data, len) CRC-16/XMODEM (polynomial 0x1021). uint32_t xferCrc32(data, len) CRC-32 (IEEE 802.3), as ZMODEM uses it. int32_t xferBasNew(&x) For the BASIC functions of other libraries: a new xfer.bas handle owning a new idle transfer (in x) that belongs to the running program; 0 when no handle is free. The library starts the transfer over its own transport; the program follows it with the Xfer functions (the connection library's ConnSendFiles and ConnReceiveFiles work this way). @@ -160,6 +161,9 @@ Interoperability: the engines are tested against lrz and lsz (lrzsz 0.12.20) in .index xfer.bas .index XferSendFile .index XferReceive +.index XferSendFiles +.index XferReceiveFiles +.index xferSplitList .index XferPoll .h1 File Transfers from BASIC @@ -172,7 +176,9 @@ Transfers over a connection (conn.bas: ConnSendFiles, ConnReceiveFiles) are the Function Description -------- ----------- XferSendFile(com, proto, path) Send one file over COM port com (1-4). Returns a handle, or 0. - XferReceive(com, proto, target) Receive: XMODEM into the file target; YMODEM and ZMODEM into the folder target. Returns a handle, or 0. + XferSendFiles(com, proto, files, escapeCtl, resumeFiles) Send a batch, one path per line (xferSplitList), with the ZMODEM options ConnSendFiles takes. Returns a handle, or 0. + XferReceive(com, proto, target) Receive: XMODEM into the file target; YMODEM and ZMODEM into the folder target; a file already there is renamed. Returns a handle, or 0. + XferReceiveFiles(com, proto, target, exists, escapeCtl) Receive with an XFER_EXIST_ action for files already there (XFER_EXIST_ASK: XFER_ASKING and XferAnswer) and ZMODEM control escaping. XferPoll(handle) Move the transfer along. Returns XFER_RUNNING, XFER_ASKING, XFER_DONE, XFER_FAILED, or XFER_CANCELLED. XferStatus(handle) The same, without moving it along (XFER_IDLE for a bad handle). XferSending(handle), XferProtocol(handle) True for a send; the protocol. diff --git a/src/libs/kpunch/xfer/xfer.h b/src/libs/kpunch/xfer/xfer.h index 1b7144c7..786a6508 100644 --- a/src/libs/kpunch/xfer/xfer.h +++ b/src/libs/kpunch/xfer/xfer.h @@ -181,6 +181,12 @@ const XferProgressT *xferProgress(const XferT *x); // bytes. void xferSafeName(const char *name, bool longNames, char *out); +// Split a list of files to send, one path per line (LF or CR LF; empty +// lines are skipped), in place: paths[] (XFER_MAX_FILES entries) points +// into list. Returns how many, at most XFER_MAX_FILES (the rest are +// left out). +int32_t xferSplitList(char *list, const char **paths); + // BASIC handles (xferBas.c, src/include/basic/xfer.bas) for the BASIC // functions of other libraries, which start transfers over their own // transports: a new handle owning a new idle transfer (in *outX) that diff --git a/src/libs/kpunch/xfer/xferBas.c b/src/libs/kpunch/xfer/xferBas.c index fe30fc48..c17cfe4b 100644 --- a/src/libs/kpunch/xfer/xferBas.c +++ b/src/libs/kpunch/xfer/xferBas.c @@ -23,10 +23,10 @@ // xferBas.c -- BASIC face of the transfer library (src/include/basic/ // xfer.bas): transfers by handle (1, 2, ...; 0 means none). // -// XferSendFile and XferReceive run over a serial port a BASIC program -// opened with SerOpen (COM ports are numbered 1-4 as in the rest of the -// BASIC serial functions); there an incoming file that already exists is -// never replaced but stored under a new name. The BASIC functions of +// XferSendFile(s) and XferReceive(Files) run over a serial port a BASIC +// program opened with SerOpen (COM ports are numbered 1-4 as in the rest +// of the BASIC serial functions); XferReceive stores an incoming file that +// already exists under a new name, XferReceiveFiles does what it is told. The BASIC functions of // other libraries start transfers over their own transports and hand out // the same handles (xferBasNew; the connection library's ConnSendFiles and // ConnReceiveFiles), so the progress, question and end functions below @@ -85,9 +85,11 @@ const char *XferMessage(int32_t handle); int32_t XferPoll(int32_t handle); int32_t XferProtocol(int32_t handle); int32_t XferReceive(int32_t com, int32_t proto, const char *target); +int32_t XferReceiveFiles(int32_t com, int32_t proto, const char *target, int32_t exists, int32_t escapeCtl); int32_t XferResumedAt(int32_t handle); const char *XferSafeName(const char *fileName, const char *folder); int32_t XferSendFile(int32_t com, int32_t proto, const char *path); +int32_t XferSendFiles(int32_t com, int32_t proto, const char *files, int32_t escapeCtl, int32_t resumeFiles); int32_t XferSending(int32_t handle); int32_t XferSize(int32_t handle); int32_t XferStatus(int32_t handle); @@ -357,11 +359,23 @@ int32_t XferProtocol(int32_t handle) { // Receive: XMODEM into the file target, YMODEM / ZMODEM into the folder // target. Returns a handle, or 0 when the transfer cannot start. int32_t XferReceive(int32_t com, int32_t proto, const char *target) { + return XferReceiveFiles(com, proto, target, XferExistRenameE, BAS_FALSE); +} + + +// Receive as XferReceive does, with exists (an XFER_EXIST_ action: what +// happens to a file that is already there; XFER_EXIST_ASK waits in +// XFER_ASKING for XferAnswer) and escapeCtl (ask a ZMODEM sender to escape +// every control character). Returns a handle, or 0. +int32_t XferReceiveFiles(int32_t com, int32_t proto, const char *target, int32_t exists, int32_t escapeCtl) { XferIoT io; XferOptionsT opt; int32_t handle = startOnPort(com, &io, &opt); - if (handle && !xferStartReceive(byHandle(handle)->x, (XferProtoE)proto, &io, &opt, target, PLATFORM_TICKS_TO_MS(platformClock()))) { + opt.exists = (exists >= XferExistAskE && exists < XferExistCountE) ? (XferExistE)exists : XferExistRenameE; + opt.escapeCtl = escapeCtl != BAS_FALSE; + + if (handle && !xferStartReceive(byHandle(handle)->x, (XferProtoE)proto, &io, &opt, target ? target : "", PLATFORM_TICKS_TO_MS(platformClock()))) { XferClose(handle); return BAS_NO_HANDLE; } @@ -391,15 +405,30 @@ const char *XferSafeName(const char *fileName, const char *folder) { // Send one file. Returns a handle, or 0 when the transfer cannot start. int32_t XferSendFile(int32_t com, int32_t proto, const char *path) { + return XferSendFiles(com, proto, path, BAS_FALSE, BAS_FALSE); +} + + +// Send files, one path per line (xferSplitList; XMODEM sends the first), +// as a batch. escapeCtl escapes every ZMODEM control character; +// resumeFiles asks a ZMODEM receiver to continue files it has a part of. +// Returns a handle, or 0 when the transfer cannot start. +int32_t XferSendFiles(int32_t com, int32_t proto, const char *files, int32_t escapeCtl, int32_t resumeFiles) { XferIoT io; XferOptionsT opt; - int32_t handle = startOnPort(com, &io, &opt); + const char *paths[XFER_MAX_FILES] = { NULL }; + char *list = strdup(files ? files : ""); + int32_t handle = list ? startOnPort(com, &io, &opt) : BAS_NO_HANDLE; - if (handle && !xferStartSend(byHandle(handle)->x, (XferProtoE)proto, &io, &opt, &path, 1, PLATFORM_TICKS_TO_MS(platformClock()))) { + opt.escapeCtl = escapeCtl != BAS_FALSE; + opt.resume = resumeFiles != BAS_FALSE; + + if (handle && !xferStartSend(byHandle(handle)->x, (XferProtoE)proto, &io, &opt, paths, xferSplitList(list, paths), PLATFORM_TICKS_TO_MS(platformClock()))) { XferClose(handle); - return BAS_NO_HANDLE; + handle = BAS_NO_HANDLE; } + free(list); return handle; } diff --git a/src/libs/kpunch/xfer/xferInt.h b/src/libs/kpunch/xfer/xferInt.h index c4d8aa06..a5a3367d 100644 --- a/src/libs/kpunch/xfer/xferInt.h +++ b/src/libs/kpunch/xfer/xferInt.h @@ -27,6 +27,8 @@ #define XFERINT_H #include "xfer.h" +#include "dvxHash.h" +#include "dvxEndian.h" #include @@ -52,6 +54,9 @@ #define XFER_PKT_CRC 2 #define XFER_PKT_SUM 1 +// CRC-16 register start (dvxCrc16, XMODEM's and ZMODEM's) +#define XFER_CRC16_INIT 0 + // CRC-32 register start (xferCrc32Add runs without the final inversion) #define XFER_CRC32_INIT 0xFFFFFFFFU @@ -124,6 +129,8 @@ typedef struct { uint32_t ackPos; // sender: highest position the receiver acknowledged uint32_t sinceSync; // sender: bytes since the last ZCRCW uint32_t sinceQuery; // sender: bytes since the last ZCRCQ + uint32_t crcLeft; // sender: bytes of a ZCRC request's range still to CRC + uint32_t crcValue; // ... and its CRC so far int32_t blockLen; // sender: subpacket size (shrinks on errors, grows back) int32_t goodRun; // sender: subpackets sent since the last error uint8_t dataCrc[XFER_ZM_CRC_MAX]; @@ -136,6 +143,7 @@ typedef struct { bool escCtl; // we escape every control character bool ignoreCtl; // we asked for escaped controls: drop raw ones bool waitAck; // sender: paused for ZACK + bool crcRunning; // sender: a ZCRC request's CRC being worked out (crcStep) bool frameEnded; // sender: the last subpacket ended the frame (ZCRCW) } XferZmT; @@ -180,8 +188,7 @@ XferOpenE xferApplyExist(XferT *x, XferExistE action); // Close the current file; keep false deletes it. void xferCloseFile(XferT *x, bool keep); -// Running CRCs (no final inversion for CRC-32) -uint16_t xferCrc16Add(uint16_t crc, const uint8_t *data, int32_t len); +// Running CRC-32 (no final inversion); CRC-16 runs on with dvxCrc16 uint32_t xferCrc32Add(uint32_t crc, const uint8_t *data, int32_t len); // Give up: cancel the peer, then end with XferFailedE. diff --git a/src/libs/kpunch/xfer/xferZmodem.c b/src/libs/kpunch/xfer/xferZmodem.c index a7398e49..1e11f9a6 100644 --- a/src/libs/kpunch/xfer/xferZmodem.c +++ b/src/libs/kpunch/xfer/xferZmodem.c @@ -76,9 +76,6 @@ #define ESC_OK_BITS 0x40 #define BYTE_MASK 0xFF #define BITS_PER_BYTE 8 -#define NIBBLE_BITS 4 -#define NIBBLE_MASK 0x0F -#define HEX_ALPHA_VALUE 10 // Frame types #define ZRQINIT 0 @@ -160,6 +157,7 @@ #define BLOCK_MIN 64 #define GROW_AFTER 8 #define SUBPACKETS_PER_POLL 4 +#define CRC_READS_PER_POLL 2 // ZCRC reply: file reads (XFER_ZM_DATA_MAX each) CRCed a poll #define OO_COUNT 2 #define HEX_TAIL 2 // CR and LF after a hex header #define COMMAND_REFUSED 1 @@ -168,7 +166,6 @@ #define OVER_AND_OUT "OO" #define FILE_INFO_MAX (XFER_NAME_MAX + XFER_INFO_FIELDS_MAX) #define FILE_MODE_TEXT "100644" -#define HEX_DIGITS "0123456789abcdef" #define DECIMAL 10 #define OCTAL 8 @@ -190,15 +187,13 @@ static const uint8_t sZeroHdr[HDR_LEN]; // Prototypes // ============================================================ +static void crcStep(XferT *x, uint32_t nowMs); static void dataByte(XferT *x, uint8_t b, uint32_t nowMs); static void dataDone(XferT *x, bool good, uint32_t nowMs); static int32_t decode(XferZmT *zm, uint8_t b); static void fileInfo(XferT *x, uint32_t nowMs); -static uint32_t hdrToPos(const uint8_t *hdr); static void headerDone(XferT *x, uint32_t nowMs); -static int32_t hexValue(uint8_t c); static void nextSendFile(XferT *x, uint32_t nowMs); -static void posToHdr(uint32_t pos, uint8_t *hdr); static void putEsc(XferT *x, uint8_t b); static void putHex(XferT *x, uint8_t v); static void putRaw(XferT *x, uint8_t b); @@ -214,7 +209,7 @@ static void recvHeader(XferT *x, uint8_t type, const uint8_t *hdr, uint32_t static void reposition(XferT *x, uint32_t pos, bool afterError, uint32_t nowMs); static void resendSend(XferT *x); static void resetOut(XferT *x); -static void sendCrcReply(XferT *x, uint32_t len); +static void sendCrcReply(XferT *x, uint32_t len, uint32_t nowMs); static void sendHeader(XferT *x, uint8_t type, const uint8_t *hdr, uint32_t nowMs); static bool sendRetry(XferT *x, uint32_t nowMs); static void sendZrinit(XferT *x); @@ -222,6 +217,38 @@ static void skipSendFile(XferT *x, uint32_t nowMs); static void startData(XferT *x); +// A ZCRC request's next pieces CRCed (the receiver waits for the reply: +// the deadline moves on meanwhile); once the range is done (or the file +// ends before it), the reply goes and the file is back at its start. +static void crcStep(XferT *x, uint32_t nowMs) { + uint8_t hdr[HDR_LEN]; + + for (int32_t i = 0; i < CRC_READS_PER_POLL && x->zm.crcLeft > 0; i++) { + size_t want = (x->zm.crcLeft < XFER_ZM_DATA_MAX) ? x->zm.crcLeft : XFER_ZM_DATA_MAX; + size_t got = fread(x->pkt, 1, want, x->fp); + + if (got == 0) { + x->zm.crcLeft = 0; + break; + } + + x->zm.crcValue = xferCrc32Add(x->zm.crcValue, x->pkt, (int32_t)got); + x->zm.crcLeft -= (uint32_t)got; + } + + if (x->zm.crcLeft > 0) { + x->deadline = nowMs + XFER_REPLY_MS; + return; + } + + x->zm.crcRunning = false; + fseek(x->fp, 0, SEEK_SET); + dvxPutLe32(hdr, ~x->zm.crcValue); + queueHexHeader(x, ZCRC, hdr); + xferFlushOut(x); +} + + // One byte of a data subpacket: data until ZDLE + frame end, then the CRC. static void dataByte(XferT *x, uint8_t b, uint32_t nowMs) { XferZmT *zm = &x->zm; @@ -268,11 +295,11 @@ static void dataByte(XferT *x, uint8_t b, uint32_t nowMs) { if (zm->dataCrc32) { uint32_t crc = ~xferCrc32Add(xferCrc32Add(XFER_CRC32_INIT, x->pkt, x->pktLen), &end, 1); - ok = hdrToPos(zm->dataCrc) == crc; + ok = dvxGetLe32(zm->dataCrc) == crc; } else { - uint16_t crc = xferCrc16Add(xferCrc16Add(0, x->pkt, x->pktLen), &end, 1); + uint16_t crc = dvxCrc16(dvxCrc16(XFER_CRC16_INIT, x->pkt, x->pktLen), &end, 1); - ok = zm->dataCrc[0] == (uint8_t)(crc >> BITS_PER_BYTE) && zm->dataCrc[1] == (uint8_t)(crc & BYTE_MASK); + ok = dvxGetBe16(zm->dataCrc) == crc; } dataDone(x, ok, nowMs); @@ -432,11 +459,6 @@ static void fileInfo(XferT *x, uint32_t nowMs) { } -static uint32_t hdrToPos(const uint8_t *hdr) { - return (uint32_t)hdr[0] | ((uint32_t)hdr[1] << BITS_PER_BYTE) | ((uint32_t)hdr[2] << (2 * BITS_PER_BYTE)) | ((uint32_t)hdr[3] << (3 * BITS_PER_BYTE)); -} - - // A complete header: check its CRC and hand it to the sender or receiver. static void headerDone(XferT *x, uint32_t nowMs) { XferZmT *zm = &x->zm; @@ -445,11 +467,11 @@ static void headerDone(XferT *x, uint32_t nowMs) { zm->rdState = RD_SEEK; if (zm->rdCrc32) { - ok = hdrToPos(zm->rdHdr + HDR_BODY) == xferCrc32(zm->rdHdr, HDR_BODY); + ok = dvxGetLe32(zm->rdHdr + HDR_BODY) == xferCrc32(zm->rdHdr, HDR_BODY); } else { uint16_t crc = xferCrc16(zm->rdHdr, HDR_BODY); - ok = zm->rdHdr[HDR_BODY] == (uint8_t)(crc >> BITS_PER_BYTE) && zm->rdHdr[HDR_BODY + 1] == (uint8_t)(crc & BYTE_MASK); + ok = dvxGetBe16(zm->rdHdr + HDR_BODY) == crc; } // A damaged header is dropped: a lost request is repeated on timeout, @@ -469,23 +491,6 @@ static void headerDone(XferT *x, uint32_t nowMs) { } -static int32_t hexValue(uint8_t c) { - if (c >= '0' && c <= '9') { - return c - '0'; - } - - if (c >= 'a' && c <= 'f') { - return c - 'a' + HEX_ALPHA_VALUE; - } - - if (c >= 'A' && c <= 'F') { - return c - 'A' + HEX_ALPHA_VALUE; - } - - return -1; -} - - // Offer the next file of the batch, or end the session. static void nextSendFile(XferT *x, uint32_t nowMs) { x->tries = 0; @@ -511,13 +516,6 @@ static void nextSendFile(XferT *x, uint32_t nowMs) { } -static void posToHdr(uint32_t pos, uint8_t *hdr) { - for (int32_t i = 0; i < HDR_LEN; i++) { - hdr[i] = (uint8_t)(pos >> (i * BITS_PER_BYTE)); - } -} - - // Queue a byte, escaped when the link could eat it. static void putEsc(XferT *x, uint8_t b) { XferZmT *zm = &x->zm; @@ -543,8 +541,8 @@ static void putEsc(XferT *x, uint8_t b) { static void putHex(XferT *x, uint8_t v) { - putRaw(x, (uint8_t)HEX_DIGITS[v >> NIBBLE_BITS]); - putRaw(x, (uint8_t)HEX_DIGITS[v & NIBBLE_MASK]); + putRaw(x, (uint8_t)DVX_HEX_DIGITS[v >> DVX_NIBBLE_BITS]); + putRaw(x, (uint8_t)DVX_HEX_DIGITS[v & DVX_NIBBLE_MASK]); } @@ -570,7 +568,7 @@ static void queueBinHeader(XferT *x, uint8_t type, const uint8_t *hdr) { if (x->zm.txCrc32) { uint8_t crc[CRC32_LEN]; - posToHdr(xferCrc32(body, HDR_BODY), crc); + dvxPutLe32(crc, xferCrc32(body, HDR_BODY)); for (int32_t i = 0; i < CRC32_LEN; i++) { putEsc(x, crc[i]); @@ -597,13 +595,13 @@ static void queueData(XferT *x, const uint8_t *data, int32_t len, uint8_t end) { if (x->zm.txCrc32) { uint8_t crc[CRC32_LEN]; - posToHdr(~xferCrc32Add(xferCrc32Add(XFER_CRC32_INIT, data, len), &end, 1), crc); + dvxPutLe32(crc, ~xferCrc32Add(xferCrc32Add(XFER_CRC32_INIT, data, len), &end, 1)); for (int32_t i = 0; i < CRC32_LEN; i++) { putEsc(x, crc[i]); } } else { - uint16_t crc = xferCrc16Add(xferCrc16Add(0, data, len), &end, 1); + uint16_t crc = dvxCrc16(dvxCrc16(XFER_CRC16_INIT, data, len), &end, 1); putEsc(x, (uint8_t)(crc >> BITS_PER_BYTE)); putEsc(x, (uint8_t)(crc & BYTE_MASK)); @@ -721,7 +719,7 @@ static void queueNextSubpacket(XferT *x, uint32_t nowMs) { static void queuePosHeader(XferT *x, uint8_t type, uint32_t pos, bool hex) { uint8_t hdr[HDR_LEN]; - posToHdr(pos, hdr); + dvxPutLe32(hdr, pos); if (hex) { queueHexHeader(x, type, hdr); @@ -769,7 +767,7 @@ static void recvError(XferT *x, uint32_t nowMs) { static void recvHeader(XferT *x, uint8_t type, const uint8_t *hdr, uint32_t nowMs) { XferZmT *zm = &x->zm; - uint32_t pos = hdrToPos(hdr); + uint32_t pos = dvxGetLe32(hdr); bool inFile = x->phase == XferPhZrWaitE || x->phase == XferPhZrDataE; switch (type) { @@ -937,42 +935,26 @@ static void resetOut(XferT *x) { } -// Answer a ZCRC request with the CRC-32 of the first len bytes of the -// file (0 = all of it). -static void sendCrcReply(XferT *x, uint32_t len) { - uint32_t crc = XFER_CRC32_INIT; - uint32_t left = (len == 0) ? (uint32_t)x->prog.fileSize : len; - uint8_t hdr[HDR_LEN]; - +// The receiver asked for the CRC of the file's first len bytes (0: all of +// it; crash recovery): crcStep works it out a piece a poll, then replies. +static void sendCrcReply(XferT *x, uint32_t len, uint32_t nowMs) { + x->zm.crcLeft = (len == 0) ? (uint32_t)x->prog.fileSize : len; + x->zm.crcValue = XFER_CRC32_INIT; + x->zm.crcRunning = true; fseek(x->fp, 0, SEEK_SET); - - while (left > 0) { - size_t want = (left < XFER_ZM_DATA_MAX) ? left : XFER_ZM_DATA_MAX; - size_t got = fread(x->pkt, 1, want, x->fp); - - if (got == 0) { - break; - } - - crc = xferCrc32Add(crc, x->pkt, (int32_t)got); - left -= (uint32_t)got; - } - - fseek(x->fp, 0, SEEK_SET); - posToHdr(~crc, hdr); - queueHexHeader(x, ZCRC, hdr); + crcStep(x, nowMs); } static void sendHeader(XferT *x, uint8_t type, const uint8_t *hdr, uint32_t nowMs) { XferZmT *zm = &x->zm; - uint32_t pos = hdrToPos(hdr); + uint32_t pos = dvxGetLe32(hdr); bool inFile = x->phase == XferPhZsFileE || x->phase == XferPhZsDataE || x->phase == XferPhZsEofE; switch (type) { case ZRINIT: if (x->phase == XferPhZsInitE) { - zm->rxBufLen = hdr[ZP0] | (hdr[ZP1] << BITS_PER_BYTE); + zm->rxBufLen = dvxGetLe16(hdr + ZP0); zm->txCrc32 = (hdr[ZF0] & CANFC32) != 0; zm->escCtl = zm->escCtl || (hdr[ZF0] & ESCCTL) != 0; @@ -1057,7 +1039,7 @@ static void sendHeader(XferT *x, uint8_t type, const uint8_t *hdr, uint32_t nowM case ZCRC: if (x->phase == XferPhZsFileE) { - sendCrcReply(x, pos); + sendCrcReply(x, pos, nowMs); } break; @@ -1111,8 +1093,7 @@ static void sendZrinit(XferT *x) { window = 0; } - hdr[ZP0] = (uint8_t)(window & BYTE_MASK); - hdr[ZP1] = (uint8_t)(window >> BITS_PER_BYTE); + dvxPutLe16(hdr + ZP0, (uint32_t)window); hdr[ZF0] = CANFDX | CANOVIO | CANFC32 | (x->opt.escapeCtl ? ESCCTL : 0); queueHexHeader(x, ZRINIT, hdr); } @@ -1234,7 +1215,7 @@ void xferZmByte(XferT *x, uint8_t b, uint32_t nowMs) { break; case RD_HEX: { - int32_t v = hexValue(b); + int32_t v = dvxHexValue(b); if (v < 0) { zm->rdState = RD_SEEK; @@ -1247,7 +1228,7 @@ void xferZmByte(XferT *x, uint8_t b, uint32_t nowMs) { break; } - zm->rdHdr[zm->rdCount++] = (uint8_t)((zm->rdHexHigh << NIBBLE_BITS) | v); + zm->rdHdr[zm->rdCount++] = (uint8_t)((zm->rdHexHigh << DVX_NIBBLE_BITS) | v); zm->rdHexHigh = -1; if (zm->rdCount == zm->rdNeed) { @@ -1291,6 +1272,11 @@ void xferZmByte(XferT *x, uint8_t b, uint32_t nowMs) { // Sender: stream data while the window and the transport allow. void xferZmPump(XferT *x, uint32_t nowMs) { + if (x->zm.crcRunning) { + crcStep(x, nowMs); + return; + } + if (!x->prog.sending || x->phase != XferPhZsDataE || x->zm.waitAck) { return; } diff --git a/src/loader/Makefile b/src/loader/Makefile index cda9a2f9..10043de9 100644 --- a/src/loader/Makefile +++ b/src/loader/Makefile @@ -41,12 +41,16 @@ BINDIR = ../../bin SRCS = loaderMain.c OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) -POBJS = $(POBJDIR)/dvxPlatformDos.o $(POBJDIR)/dvxPlatformIrq.o $(POBJDIR)/dvxPlatformUtil.o $(POBJDIR)/dvxMemTrack.o $(POBJDIR)/dvxPrefs.o $(OBJDIR)/dvxhlpc.o +POBJS = $(POBJDIR)/dvxPlatformDos.o $(POBJDIR)/dvxPlatformIrq.o $(POBJDIR)/dvxPlatformUtil.o $(POBJDIR)/dvxMemTrack.o TARGET = $(BINDIR)/dvx.exe +# The help compiler as a module of its own (tools/hlpcModule.h), loaded +# only while help compiles: the boot rebuild and BASIC's HelpCompile +HLPC = $(BINDIR)/system/HLPC.DXE + .PHONY: all clean -all: $(TARGET) +all: $(TARGET) $(HLPC) $(TARGET): $(OBJS) $(POBJS) | $(BINDIR) $(CC) $(CFLAGS) -o $@ $(OBJS) $(POBJS) $(LDFLAGS) -Wl,-Map=$(BINDIR)/dvx.map @@ -60,10 +64,11 @@ $(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR) $(POBJDIR)/%.o: $(LIBDVX)/platform/%.c $(DVX_MAKEFILES) | $(POBJDIR) $(CC) $(CFLAGS) -c -o $@ $< -$(POBJDIR)/%.o: $(LIBDVX)/%.c $(DVX_MAKEFILES) | $(POBJDIR) - $(CC) $(CFLAGS) -c -o $@ $< - # The help compiler's library entry point (no main) for help rebuilds. +$(HLPC): $(OBJDIR)/dvxhlpc.o | $(BINDIR) + @mkdir -p $(dir $@) + $(DXE3GEN) -o $@ -U $< + $(OBJDIR)/dvxhlpc.o: ../tools/dvxhlpc.c $(DVX_MAKEFILES) | $(OBJDIR) $(CC) $(CFLAGS) -DHLPC_NO_MAIN -c -o $@ $< @@ -79,4 +84,4 @@ $(BINDIR): -include $(OBJS:.o=.d) $(POBJS:.o=.d) clean: - rm -f $(OBJS) $(POBJS) $(OBJS:.o=.d) $(POBJS:.o=.d) $(TARGET) $(BINDIR)/dvx.map + rm -f $(OBJS) $(POBJS) $(OBJS:.o=.d) $(POBJS:.o=.d) $(OBJDIR)/dvxhlpc.o $(OBJDIR)/dvxhlpc.d $(TARGET) $(HLPC) $(BINDIR)/dvx.map diff --git a/src/loader/README.md b/src/loader/README.md index 7ba05dbe..7cc21f2d 100644 --- a/src/loader/README.md +++ b/src/loader/README.md @@ -31,7 +31,8 @@ On launch, `dvx.exe` performs these steps in order: 6. Scan and load all modules in two phases (see below), updating the splash progress bar as each module loads. 7. Check whether help files need rebuilding (the module set changed - since the last boot); if so, run the integrated help compiler with + since the last boot); if so, load the help compiler module + (`SYSTEM/HLPC.DXE`) and run it with `HLPC_LENIENT_LINKS`, so a link into help that was removed along with its module becomes plain text instead of failing the rebuild. The compiler's warnings and errors go to `DVX.LOG` (stderr is @@ -67,6 +68,7 @@ texthelp.lib (deps: libtasks, libdvx) listhelp.lib (deps: libtasks, libdvx) dvxshell.lib (deps: libtasks, libdvx, texthelp, listhelp) taskmgr.lib (deps: dvxshell, libtasks, libdvx, texthelp, listhelp) +crypto.lib (deps: libdvx) serial.lib (deps: libtasks, libdvx) dvxsql.lib (deps: libtasks, libdvx) basrt.lib (deps: libtasks, libdvx) @@ -133,7 +135,7 @@ void dvxLog(const char *fmt, ...); `STB_DS_IMPLEMENTATION` is compiled into the loader so the implementation is shared across the loader's translation units -(`loaderMain.o`, `dvxPrefs.o`). The loader uses plain `realloc` / +(`loaderMain.o` and the platform objects). The loader uses plain `realloc` / `free` for stb_ds; per-app memory tracking applies to DXE allocations via the export table mappings described below. @@ -152,8 +154,9 @@ the export table. This includes: * Crash handling -- signal handler installation, register dump logging * System utilities -- memory info, directory creation, path helpers -* Preferences -- the `prefs*` INI API from `dvxPrefs.c` (the only - resident copy; `libdvx.lib` does not contain it) + +The `prefs*` INI API is libdvx's (`dvxPrefs.c`); the loader finds the +few it needs for the help rebuild by name once libdvx is loaded. ### Per-App Memory Tracking @@ -173,15 +176,17 @@ the tracked wrapper runs instead. The Task Manager reads per-app usage via `dvxMemGetAppUsage()`, and `dvxMemResetApp()` zeros the counter when an app is reaped. -### Integrated Help Compilation +### Help Rebuild at Boot -The help compiler (`dvxhlpc.c`) is built a second time as an object -file with `-DHLPC_NO_MAIN` and linked into the loader. On boot, the -loader compares the currently-loaded module set against the last-run -state recorded in `DVX.INI`. If it has changed, the loader -re-compiles the affected help files before starting the shell, so +The help compiler (`dvxhlpc.c`) is built a second time with +`-DHLPC_NO_MAIN` as a module, `SYSTEM/HLPC.DXE`, so dvx.exe does not +carry it. On boot, the loader compares the currently-loaded module set +against the last-run state recorded in `DVX.INI`. If it has changed, +the loader opens the module (`tools/hlpcModule.h`), re-compiles the +affected help files before starting the shell, and closes it again, so newly-installed widgets and libraries contribute to the system help -without a manual step. +without a manual step. A missing module is logged and the rebuild is +tried at the next start. ### Splash Screen @@ -214,5 +219,6 @@ The loader is cross-compiled with the DJGPP toolchain final `dvx.exe` runs stand-alone on DOS without a separate DPMI host. Linked translation units: `loaderMain.c` + `dvxPlatformDos.c` + -`dvxPlatformUtil.c` + `dvxPrefs.c` + `dvxhlpc.c` (as -`-DHLPC_NO_MAIN`). +`dvxPlatformIrq.c` + `dvxPlatformUtil.c` + `dvxMemTrack.c`. The help +compiler (`dvxhlpc.c`, `-DHLPC_NO_MAIN`) is built beside it as +`SYSTEM/HLPC.DXE`. diff --git a/src/loader/loaderMain.c b/src/loader/loaderMain.c index d6a42b01..0ef2e640 100644 --- a/src/loader/loaderMain.c +++ b/src/loader/loaderMain.c @@ -22,23 +22,35 @@ // loaderMain.c -- DVX bootstrap loader entry point // -// Loads all DXE modules from two directories: -// libs/ *.lib -- core libraries (libtasks, libdvx, dvxshell) +// Finds the DXE modules in two directories: +// libs/ *.lib -- libraries (libtasks, libdvx, dvxshell, ...) // widgets/ *.wgt -- widget type plugins (box, button, listview, etc.) // // Each module may have a .dep file (same base name, .dep extension) -// listing base names of modules that must be loaded before it. -// The loader resolves the dependency graph and loads in topological -// order. Each module must export an ABI stamp (DVX_ABI_STAMP) equal to -// the loader's DVX_ABI_VERSION or the boot stops with a message naming -// it. After loading, any module that exports wgtRegister() has -// it called. Finally, the loader finds and calls shellMain(). +// listing base names of modules that must be loaded before it. Each +// module must export an ABI stamp (DVX_ABI_STAMP) equal to the loader's +// DVX_ABI_VERSION. Any module that exports wgtRegister() has it called +// once it is loaded. Finally, the loader finds and calls shellMain(). +// +// At the start the loader loads every widget, the shell, and the +// libraries that run code as they load (constructors: they hook into +// the shell), each with what it needs; one of DVX's own that fails stops +// the boot with a message naming it. Every other library is loaded when +// something first needs it: an index of the names the libraries export +// (read from their files, not loaded) lets DJGPP's last-resort resolver +// find the library behind a name dvx.exe and the loaded modules lack. +// While a module is being opened the resolver only marks that library +// (dlopen cannot be entered again from inside itself); the open fails, +// moduleOpen loads what was marked, and opens the module again. A +// dlsym(NULL) from a module loads the library at once. Modules open +// modules with platformModuleOpen, which is moduleOpen. #include "stddclmr.h" #include "dvxPlat.h" #include "dvxPrefs.h" #include "dvxRes.h" -#include "../tools/hlpcCompile.h" +#include "dvxHash.h" +#include "../tools/hlpcModule.h" #include #include @@ -126,9 +138,19 @@ typedef struct { void *handle; bool loaded; bool addOn; - bool skipped; // an add-on that could not be loaded + bool skipped; // not loaded and not to be tried (an add-on, or a later load, that failed) + bool runsAtLoad; // a library with constructors: loaded at the start + bool wanted; // a module being opened needs one of its names + bool loading; // being loaded (a module that needs it now cannot have it) } ModuleT; +// One name a library exports, in the index: its hash and the library +// (index into sLibs). Sorted by hash. +typedef struct { + uint32_t hash; + uint16_t lib; +} ExportT; + // Whether a module's dependencies let it load now (depsCheck) typedef enum { DepsReadyE, // every one is loaded @@ -150,6 +172,29 @@ typedef struct { typedef void (*HcfEntryFnT)(const char *key, const char *val, const char *exclude, void *user); +// collectGlobFiles' walk: the files it gathers and the names they match +typedef struct { + char ***files; + const char *glob; + const char *exclude; +} GlobWalkT; + +// scanDir's walk: the module extension and the modules found +typedef struct { + const char *ext; + ModuleT **mods; +} ModWalkT; + +// The settings functions helpRecompileIfNeeded uses: libdvx's, found by +// name once it is loaded (dvx.exe links no copy of its own) +typedef struct { + __typeof__(prefsLoad) *load; + __typeof__(prefsGetInt) *getInt; + __typeof__(prefsSetInt) *setInt; + __typeof__(prefsSave) *save; + __typeof__(prefsClose) *close; +} LoaderPrefsT; + // wgtIface* accessors exported by libdvx, resolved once after it loads. typedef int32_t (*IfaceCountFnT)(void); typedef const void *(*IfaceAtFnT)(int32_t, const char **); @@ -178,6 +223,19 @@ static uint8_t sSplashBarFg = SPLASH_VGA_GREY_FG; static int32_t sLibCount = 0; static int32_t sWidgetCount = 0; +// Every library found (kept for the session: the ones not loaded yet load +// on demand), the index of their names, and every module loaded, in load +// order (closed in reverse at exit) +static ModuleT *sLibs = NULL; +static ExportT *sExports = NULL; +static void **sLoaded = NULL; + +// moduleOpen calls under way: the resolver only marks libraries then +static int32_t sOpenDepth = 0; + +// The start is over: a library that fails to load no longer stops DVX +static bool sStarted = false; + static IfaceCountFnT sIfaceCount = NULL; static IfaceAtFnT sIfaceAt = NULL; static IfaceGetPathFnT sIfaceGetPath = NULL; @@ -197,23 +255,33 @@ static char sLogPath[DVX_MAX_PATH] = LOG_PATH; static bool abiStampOk(void *handle, const char *path, const char *baseName); static void collectGlobFiles(char ***outFiles, const char *pattern, const char *excludePattern); -static void collectHelpJobs(const char *dirPath, HelpJobT **jobs); static DepsE depsCheck(const ModuleT *mod, const ModuleT *mods, const ModuleT *others, char *why, int32_t whySize); +static void exportAdd(void *ctx, const char *name); +static int exportCompare(const void *a, const void *b); +static int32_t exportFirst(const char *name, uint32_t *hash); +static void exportsBuild(void); +static void exportsPrune(void); static void extractBaseName(const char *path, const char *ext, char *out, int32_t outSize); static const ModuleT *findModule(const ModuleT *mods, const ModuleT *others, const char *name); -static void *findSymbol(void **handles, const char *symbol); static void freeHelpJobs(HelpJobT *jobs); static void freeMods(ModuleT *mods); static void freeStrings(char **strs); -static void handlesAdd(void ***handles, const ModuleT *mods); +static bool globVisit(const char *path, const char *name, void *ctx); static void hcfForEachEntry(const char *hcfPath, HcfEntryFnT cb, void *user); static void helpJobCallback(const char *key, const char *val, const char *exclude, void *user); +static bool helpJobVisit(const char *path, const char *name, void *ctx); static void helpRecompileIfNeeded(void); -static void helpSaveCounts(PrefsHandleT *ini); +static void helpSaveCounts(const LoaderPrefsT *prefs, PrefsHandleT *ini); static void hlpcProgressCallback(void *ctx, int32_t current, int32_t total); static bool isResourceOnly(const char *path); -static void **loadAllModules(bool safeStart); -static void loadBasicControls(void); +static ModuleT *libExporting(const char *name); +static void libFail(ModuleT *lib, const char *why); +static ModuleT *libFind(const char *name); +static bool libLoad(ModuleT *lib); +static bool libsWant(const char *name); +static bool loadAllModules(bool safeStart); +static void loadBasicControls(CtlLoadFnT loadFn); +static bool loaderPrefs(LoaderPrefsT *prefs); static void loadInOrder(ModuleT *mods, const ModuleT *others); static void loadStuck(ModuleT *mods); static void logAndReadDeps(ModuleT *mods); @@ -221,15 +289,18 @@ static void logStart(void); static int32_t markAddOns(ModuleT **mods, const char *ownDir, bool safeStart); static void moduleFail(ModuleT *mod, const char *why); static void moduleLoad(ModuleT *mod); +static void *moduleOpen(const char *path, int32_t mode); +static bool moduleVisit(const char *path, const char *name, void *ctx); static void readDeps(ModuleT *mod); static void recordIfacePaths(const char *modPath); +static void *resolveLater(const char *name); static bool safeStartAsked(int argc, char *argv[]); static void scanDir(const char *dirPath, const char *ext, ModuleT **mods); static void skipDuplicates(ModuleT *libs, ModuleT *widgets); static void splashDrawScreen(void); static void splashShutdownIfActive(void); static void splashUpdateProgress(void); -static void *startTestScript(int argc, char *argv[], void ***handles); +static void *startTestScript(int argc, char *argv[]); static void stderrRestore(int32_t savedFd); static int32_t stderrToLog(void); int main(int argc, char *argv[]); @@ -259,115 +330,28 @@ static bool abiStampOk(void *handle, const char *path, const char *baseName) { } -// Collect matching filenames into an stb_ds array of strdup'd paths. +// Collect the files below pattern's folder whose names match its last +// part (and not excludePattern) into an stb_ds array of strdup'd paths. static void collectGlobFiles(char ***outFiles, const char *pattern, const char *excludePattern) { - char dirPart[DVX_MAX_PATH]; - const char *globPart = NULL; + char dirPart[DVX_MAX_PATH]; + GlobWalkT walk; snprintf(dirPart, sizeof(dirPart), "%s", pattern); char *lastSep = platformPathDirEnd(dirPart); + walk.files = outFiles; + walk.exclude = excludePattern; + if (lastSep) { - *lastSep = '\0'; - globPart = lastSep + 1; + *lastSep = '\0'; + walk.glob = lastSep + 1; } else { - globPart = pattern; + walk.glob = pattern; dirPart[0] = '.'; dirPart[1] = '\0'; } - char **names = dvxReadDir(dirPart); - - if (!names) { - return; - } - - int32_t nEntries = (int32_t)arrlen(names); - - for (int32_t i = 0; i < nEntries; i++) { - // Skip hidden files (dvxReadDir already strips "." and "..") - if (names[i][0] == '.') { - continue; - } - - char fullPath[DVX_MAX_PATH]; - snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPart, names[i]); - - struct stat st; - - if (stat(fullPath, &st) != 0) { - continue; - } - - if (S_ISDIR(st.st_mode)) { - char subPattern[DVX_MAX_PATH]; - snprintf(subPattern, sizeof(subPattern), "%s" DVX_PATH_SEP "%s", fullPath, globPart); - collectGlobFiles(outFiles, subPattern, excludePattern); - } else if (platformGlobMatch(globPart, names[i])) { - if (!excludePattern || !platformGlobMatch(excludePattern, names[i])) { - char *copy = strdup(fullPath); - - if (copy) { - arrput(*outFiles, copy); - } - } - } - } - - dvxReadDirFree(names); -} - - -// Recursively walk dirPath and parse every .hcf into a HelpJobT. Jobs -// with no output= or no matching sources are dropped here, so every -// entry in *jobs is a real compile. -static void collectHelpJobs(const char *dirPath, HelpJobT **jobs) { - char **names = dvxReadDir(dirPath); - - if (!names) { - return; - } - - int32_t nEntries = (int32_t)arrlen(names); - - for (int32_t i = 0; i < nEntries; i++) { - if (names[i][0] == '.') { - continue; - } - - char fullPath[DVX_MAX_PATH]; - snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPath, names[i]); - - struct stat st; - - if (stat(fullPath, &st) != 0) { - continue; - } - - if (S_ISDIR(st.st_mode)) { - collectHelpJobs(fullPath, jobs); - continue; - } - - if (!dvxHasExt(names[i], ".hcf")) { - continue; - } - - HelpJobT job; - - memset(&job, 0, sizeof(job)); - snprintf(job.hcfPath, sizeof(job.hcfPath), "%s", fullPath); - snprintf(job.hcfDir, sizeof(job.hcfDir), "%s", dirPath); - hcfForEachEntry(fullPath, helpJobCallback, &job); - - if (job.outputFile[0] && arrlen(job.inputFiles) > 0) { - arrput(*jobs, job); - } else { - freeStrings(job.inputFiles); - } - } - - dvxReadDirFree(names); + platformWalkDir(dirPart, globVisit, &walk); } @@ -384,11 +368,12 @@ static DepsE depsCheck(const ModuleT *mod, const ModuleT *mods, const ModuleT *o return DepsFailedE; } - if (dep->skipped) { + if (dep->skipped || (!dep->loaded && libFind(dep->baseName) && !libLoad(libFind(dep->baseName)))) { snprintf(why, (size_t)whySize, "it needs %s, which was not loaded", dep->path); return DepsFailedE; } + // A library is loaded now; a widget may still be waiting its turn if (!dep->loaded) { state = DepsWaitE; } @@ -398,6 +383,82 @@ static DepsE depsCheck(const ModuleT *mod, const ModuleT *mods, const ModuleT *o } +// One name a library exports, into the index (ctx: the library's index). +static void exportAdd(void *ctx, const char *name) { + ExportT e; + + e.hash = dvxFnvBytes(DVX_FNV_BASIS, name, (int32_t)strlen(name)); + e.lib = (uint16_t)(uintptr_t)ctx; + arrput(sExports, e); +} + + +static int exportCompare(const void *a, const void *b) { + uint32_t ha = ((const ExportT *)a)->hash; + uint32_t hb = ((const ExportT *)b)->hash; + + return (ha > hb) - (ha < hb); +} + + +// The first index entry with name's hash (into *hash), or the index's +// length when none: the entries for name, if any, run on from there. +static int32_t exportFirst(const char *name, uint32_t *hash) { + int32_t lo = 0; + int32_t hi = (int32_t)arrlen(sExports); + + *hash = dvxFnvBytes(DVX_FNV_BASIS, name, (int32_t)strlen(name)); + + while (lo < hi) { + int32_t mid = lo + (hi - lo) / 2; + + if (sExports[mid].hash < *hash) { + lo = mid + 1; + } else { + hi = mid; + } + } + + return lo; +} + + +// Index the names every library exports and note which run code as they +// load, one read of each file's header and export table. A file that is +// not a whole DXE stops DVX when it is one of its own (as a failed load +// does); an add-on is skipped. +static void exportsBuild(void) { + for (int32_t i = 0; i < arrlen(sLibs); i++) { + ModuleT *lib = &sLibs[i]; + + if (!lib->skipped && !platformDxeExports(lib->path, exportAdd, (void *)(uintptr_t)i, &lib->runsAtLoad)) { + moduleFail(lib, LOADER_WHY_OPEN); + } + } + + // qsort may not be given NULL, which an empty index is + if (sExports) { + qsort(sExports, (size_t)arrlen(sExports), sizeof(ExportT), exportCompare); + } +} + + +// Once DVX has started only the libraries not loaded yet need their +// names: the rest of the index is given back. +static void exportsPrune(void) { + ExportT *kept = NULL; + + for (int32_t i = 0; i < arrlen(sExports); i++) { + if (!sLibs[sExports[i].lib].loaded && !sLibs[sExports[i].lib].skipped) { + arrput(kept, sExports[i]); + } + } + + arrfree(sExports); + sExports = kept; +} + + // Module base name: the leaf name, minus ext, lowercased, truncated to fit. static void extractBaseName(const char *path, const char *ext, char *out, int32_t outSize) { const char *start = platformPathBaseName(path); @@ -437,19 +498,6 @@ static const ModuleT *findModule(const ModuleT *mods, const ModuleT *others, con } -static void *findSymbol(void **handles, const char *symbol) { - for (int32_t i = 0; i < arrlen(handles); i++) { - void *sym = dlsym(handles[i], symbol); - - if (sym) { - return sym; - } - } - - return NULL; -} - - static void freeHelpJobs(HelpJobT *jobs) { for (int32_t i = 0; i < arrlen(jobs); i++) { freeStrings(jobs[i].inputFiles); @@ -478,13 +526,19 @@ static void freeStrings(char **strs) { } -// The handles of the modules that loaded code, onto handles. -static void handlesAdd(void ***handles, const ModuleT *mods) { - for (int32_t i = 0; i < arrlen(mods); i++) { - if (mods[i].handle) { - arrput(*handles, mods[i].handle); +// A file of collectGlobFiles' walk, kept when its name matches. +static bool globVisit(const char *path, const char *name, void *ctx) { + GlobWalkT *walk = (GlobWalkT *)ctx; + + if (platformGlobMatch(walk->glob, name) && (!walk->exclude || !platformGlobMatch(walk->exclude, name))) { + char *copy = strdup(path); + + if (copy) { + arrput(*walk->files, copy); } } + + return true; } @@ -543,7 +597,7 @@ static void hcfForEachEntry(const char *hcfPath, HcfEntryFnT cb, void *user) { } -// Callback for collectHelpJobs: accumulate output=, imagedir=, and expanded source= entries. +// Callback for helpJobVisit's .hcf: accumulate output=, imagedir=, and expanded source= entries. static void helpJobCallback(const char *key, const char *val, const char *exclude, void *user) { HelpJobT *job = (HelpJobT *)user; @@ -557,6 +611,32 @@ static void helpJobCallback(const char *key, const char *val, const char *exclud } +// Parse a .hcf found by the APPS walk into a HelpJobT. Jobs with no +// output= or no matching sources are dropped here, so every entry in +// *jobs is a real compile. +static bool helpJobVisit(const char *path, const char *name, void *ctx) { + HelpJobT **jobs = (HelpJobT **)ctx; + HelpJobT job; + + if (!dvxHasExt(name, ".hcf")) { + return true; + } + + memset(&job, 0, sizeof(job)); + snprintf(job.hcfPath, sizeof(job.hcfPath), "%s", path); + platformPathDirName(path, job.hcfDir, sizeof(job.hcfDir)); + hcfForEachEntry(path, helpJobCallback, &job); + + if (job.outputFile[0] && arrlen(job.inputFiles) > 0) { + arrput(*jobs, job); + } else { + freeStrings(job.inputFiles); + } + + return true; +} + + // Recompile every help file under APPS when the installed lib/widget // counts differ from the ones recorded in [help]. The counts are only // recorded when every compile succeeded, so a failed .hlp is retried on @@ -571,26 +651,33 @@ static void helpRecompileIfNeeded(void) { return; } - PrefsHandleT *ini = prefsLoad(platformDvxIniPath()); + LoaderPrefsT prefs; + + if (!loaderPrefs(&prefs)) { + dvxLog("helpRecompile: libdvx's settings functions are not there"); + return; + } + + PrefsHandleT *ini = prefs.load(platformDvxIniPath()); if (!ini) { dvxLog("helpRecompile: cannot load INI"); return; } - int32_t storedLibs = prefsGetInt(ini, "help", "libCount", HELP_COUNT_UNSET); - int32_t storedWidgets = prefsGetInt(ini, "help", "widgetCount", HELP_COUNT_UNSET); + int32_t storedLibs = prefs.getInt(ini, "help", "libCount", HELP_COUNT_UNSET); + int32_t storedWidgets = prefs.getInt(ini, "help", "widgetCount", HELP_COUNT_UNSET); // First startup: no [help] section. Save counts, skip recompile. if (storedLibs == HELP_COUNT_UNSET && storedWidgets == HELP_COUNT_UNSET) { dvxLog("helpRecompile: first startup, saving initial counts (libs=%d widgets=%d)", (int)sLibCount, (int)sWidgetCount); - helpSaveCounts(ini); + helpSaveCounts(&prefs, ini); return; } // Counts unchanged: no recompile needed if (storedLibs == sLibCount && storedWidgets == sWidgetCount) { - prefsClose(ini); + prefs.close(ini); return; } @@ -599,7 +686,7 @@ static void helpRecompileIfNeeded(void) { HelpJobT *jobs = NULL; int32_t totalSteps = 0; - collectHelpJobs(DVX_APPS_DIR, &jobs); + platformWalkDir(DVX_APPS_DIR, helpJobVisit, &jobs); for (int32_t i = 0; i < arrlen(jobs); i++) { totalSteps += hlpcProgressTotal((int32_t)arrlen(jobs[i].inputFiles)); @@ -608,7 +695,16 @@ static void helpRecompileIfNeeded(void) { if (totalSteps == 0) { dvxLog("helpRecompile: no help sources found"); freeHelpJobs(jobs); - helpSaveCounts(ini); + helpSaveCounts(&prefs, ini); + return; + } + + HlpcModuleT hlpc; + + if (!hlpcModuleOpen(&hlpc)) { + dvxLog("helpRecompile: the help compiler (%s) cannot be loaded; trying again next start", HLPC_MODULE); + freeHelpJobs(jobs); + prefs.close(ini); return; } @@ -627,7 +723,7 @@ static void helpRecompileIfNeeded(void) { dvxLog("helpRecompile: %s -> %s (%d files)", job->hcfPath, job->outputFile, (int)inputCount); - int32_t rc = hlpcCompile((const char **)job->inputFiles, inputCount, job->outputFile, imgDir, NULL, HLPC_QUIET | HLPC_LENIENT_LINKS, hlpcProgressCallback, NULL); + int32_t rc = hlpc.compile((const char **)job->inputFiles, inputCount, job->outputFile, imgDir, NULL, HLPC_QUIET | HLPC_LENIENT_LINKS, hlpcProgressCallback, NULL); if (rc != 0) { dvxLog("helpRecompile: FAILED (rc=%d)", (int)rc); @@ -636,34 +732,35 @@ static void helpRecompileIfNeeded(void) { } stderrRestore(savedErr); + hlpcModuleClose(&hlpc); freeHelpJobs(jobs); if (failures > 0) { - int32_t retries = prefsGetInt(ini, "help", "retries", 0) + 1; + int32_t retries = prefs.getInt(ini, "help", "retries", 0) + 1; if (retries < HELP_MAX_RETRIES) { dvxLog("helpRecompile: %d compile(s) failed; counts not saved so the next boot retries (%d of %d)", (int)failures, (int)retries, HELP_MAX_RETRIES); - prefsSetInt(ini, "help", "retries", retries); - prefsSave(ini); - prefsClose(ini); + prefs.setInt(ini, "help", "retries", retries); + prefs.save(ini); + prefs.close(ini); return; } dvxLog("helpRecompile: %d compile(s) still failing after %d boots; giving up until the modules change", (int)failures, HELP_MAX_RETRIES); } - prefsSetInt(ini, "help", "retries", 0); - helpSaveCounts(ini); + prefs.setInt(ini, "help", "retries", 0); + helpSaveCounts(&prefs, ini); dvxLog("helpRecompile: done"); } // Record the current lib/widget counts in [help], then save and close ini. -static void helpSaveCounts(PrefsHandleT *ini) { - prefsSetInt(ini, "help", "libCount", sLibCount); - prefsSetInt(ini, "help", "widgetCount", sWidgetCount); - prefsSave(ini); - prefsClose(ini); +static void helpSaveCounts(const LoaderPrefsT *prefs, PrefsHandleT *ini) { + prefs->setInt(ini, "help", "libCount", sLibCount); + prefs->setInt(ini, "help", "widgetCount", sWidgetCount); + prefs->save(ini); + prefs->close(ini); } @@ -694,52 +791,208 @@ static bool isResourceOnly(const char *path) { } -static void **loadAllModules(bool safeStart) { - void **handles = NULL; - ModuleT *libs = NULL; - ModuleT *widgets = NULL; +// The library the index has name in, or NULL (its file has a name with +// the same hash: the caller's dlsym on it tells). +static ModuleT *libExporting(const char *name) { + uint32_t hash; + int32_t first = exportFirst(name, &hash); - scanDir(DVX_LIBS_DIR, ".lib", &libs); + return first < arrlen(sExports) && sExports[first].hash == hash ? &sLibs[sExports[first].lib] : NULL; +} + + +// A library that cannot be loaded: at the start as any module +// (moduleFail: one of DVX's own stops it); later it is logged and not +// tried again, and whatever needed it does without. +static void libFail(ModuleT *lib, const char *why) { + if (!sStarted) { + moduleFail(lib, why); + return; + } + + dvxLog(" FAILED: %s: %s", lib->path, why); + lib->skipped = true; +} + + +// The library named name (any case), or NULL. +static ModuleT *libFind(const char *name) { + for (int32_t i = 0; i < arrlen(sLibs); i++) { + if (strcasecmp(sLibs[i].baseName, name) == 0) { + return &sLibs[i]; + } + } + + return NULL; +} + + +// Load a library (first the libraries its .dep file names) the way every +// module opens: moduleOpen. Its memory is DVX's, not the program's whose +// call needed it. One that cannot be loaded: libFail. +static bool libLoad(ModuleT *lib) { + char why[LOADER_WHY_MAX]; + int32_t prevApp; + + if (lib->loaded) { + return true; + } + + if (lib->skipped || lib->loading) { + return false; + } + + lib->loading = true; + + for (int32_t d = 0; d < arrlen(lib->deps); d++) { + ModuleT *dep = libFind(lib->deps[d]); + + if (!dep || !libLoad(dep)) { + lib->loading = false; + snprintf(why, sizeof(why), dep ? "it needs %s, which was not loaded" : "it needs %s, which is not installed", dep ? dep->path : lib->deps[d]); + libFail(lib, why); + return false; + } + } + + dvxLog("Loading: %s", lib->path); + prevApp = dvxMemSetApp(DVX_MEM_SYSTEM); + lib->handle = moduleOpen(lib->path, RTLD_NOW | RTLD_GLOBAL); + lib->loading = false; + + if (!lib->handle) { + const char *err = dlerror(); + + dvxMemSetApp(prevApp); + dvxLog(" dlopen: %s", err ? err : "no reason given"); + libFail(lib, LOADER_WHY_OPEN); + return false; + } + + if (!abiStampOk(lib->handle, lib->path, lib->baseName)) { + dvxMemSetApp(prevApp); + dlclose(lib->handle); + lib->handle = NULL; + libFail(lib, "it was made for a different version of DVX"); + return false; + } + + lib->loaded = true; + arrput(sLoaded, lib->handle); + + // The BASIC library: the controls written in BASIC wait for it + if (arrlen(sResOnlyWidgets) > 0 && dlsym(lib->handle, DVX_SYM(CTL_LOAD_SYMBOL))) { + loadBasicControls((CtlLoadFnT)dlsym(lib->handle, DVX_SYM(CTL_LOAD_SYMBOL))); + } + + dvxMemSetApp(prevApp); + + if (!sStarted) { + sSplashLoaded++; + splashUpdateProgress(); + } + + return true; +} + + +// Mark the libraries not loaded yet that export name (a module being +// opened needs it); false when none does. +static bool libsWant(const char *name) { + uint32_t hash; + bool found = false; + + for (int32_t i = exportFirst(name, &hash); i < arrlen(sExports) && sExports[i].hash == hash; i++) { + ModuleT *lib = &sLibs[sExports[i].lib]; + + if (!lib->loaded && !lib->skipped && !lib->loading) { + lib->wanted = true; + found = true; + } + } + + return found; +} + + +// The start: index every library, load the ones that run code as they +// load, the shell, and every widget (each with what it needs), and leave +// the rest for later. True when anything loaded. +static bool loadAllModules(bool safeStart) { + ModuleT *widgets = NULL; + ModuleT *shell; + ModuleT *basic; + int32_t later = 0; + + scanDir(DVX_LIBS_DIR, ".lib", &sLibs); scanDir(DVX_WIDGETS_DIR, ".wgt", &widgets); // Counted before a safe start leaves the add-ons out, so it does not // look like a change to helpRecompileIfNeeded - sLibCount = (int32_t)arrlen(libs); + sLibCount = (int32_t)arrlen(sLibs); sWidgetCount = (int32_t)arrlen(widgets); - if (markAddOns(&libs, DVX_LIBS_DIR DVX_PATH_SEP DVX_VENDOR_DIR, safeStart) + markAddOns(&widgets, DVX_WIDGETS_DIR DVX_PATH_SEP DVX_VENDOR_DIR, safeStart) > 0) { + if (markAddOns(&sLibs, DVX_LIBS_DIR DVX_PATH_SEP DVX_VENDOR_DIR, safeStart) + markAddOns(&widgets, DVX_WIDGETS_DIR DVX_PATH_SEP DVX_VENDOR_DIR, safeStart) > 0) { platformBootNoteAdd(LOADER_SAFE_NOTE); } - logAndReadDeps(libs); + logAndReadDeps(sLibs); logAndReadDeps(widgets); - sSplashTotal = (int32_t)(arrlen(libs) + arrlen(widgets)); - skipDuplicates(libs, widgets); + sSplashTotal = (int32_t)(arrlen(sLibs) + arrlen(widgets)); + skipDuplicates(sLibs, widgets); + exportsBuild(); + platformSetSymbolResolver(resolveLater); + platformSetModuleOpen(moduleOpen); - // Libraries in dependency order, then widgets (every library is there) - loadInOrder(libs, widgets); - handlesAdd(&handles, libs); - loadInOrder(widgets, libs); - handlesAdd(&handles, widgets); - freeMods(libs); + for (int32_t i = 0; i < arrlen(sLibs); i++) { + if (sLibs[i].runsAtLoad) { + libLoad(&sLibs[i]); + } + } + + // The library with shellMain, whatever its name, before the widgets + shell = libExporting(DVX_SYM("shellMain")); + + if (shell) { + libLoad(shell); + } + + loadInOrder(widgets, sLibs); freeMods(widgets); - // Controls written in BASIC, now that every native widget is there - loadBasicControls(); + // Controls written in BASIC, now that every native widget is there, + // when the BASIC library is loaded already; else when it loads + basic = libExporting(DVX_SYM(CTL_LOAD_SYMBOL)); - return handles; + if (!basic) { + loadBasicControls((CtlLoadFnT)dlsym(NULL, DVX_SYM(CTL_LOAD_SYMBOL))); + } else if (basic->loaded) { + loadBasicControls((CtlLoadFnT)dlsym(basic->handle, DVX_SYM(CTL_LOAD_SYMBOL))); + } + + sStarted = true; + sSplashLoaded = sSplashTotal; + splashUpdateProgress(); + + for (int32_t i = 0; i < arrlen(sLibs); i++) { + later += !sLibs[i].loaded && !sLibs[i].skipped ? 1 : 0; + } + + dvxLog("Loader: %d modules loaded; %d libraries load when first used", (int)arrlen(sLoaded), (int)later); + exportsPrune(); + return arrlen(sLoaded) > 0; } -// Hands every resource-only widget file to the BASIC run-time library -// (it registers the file's controls) and records the file as theirs for -// the toolbox. Without the library the files are skipped, not fatal. -static void loadBasicControls(void) { - CtlLoadFnT loadFn = (CtlLoadFnT)dlsym(NULL, DVX_SYM(CTL_LOAD_SYMBOL)); - +// Hands every resource-only widget file to the BASIC run-time library's +// loadFn (it registers the file's controls) and records the file as +// theirs for the toolbox: when that library loads (libLoad), since only +// BASIC can use them. Without the library (loadFn NULL) the files are +// skipped, not fatal. +static void loadBasicControls(CtlLoadFnT loadFn) { for (int32_t i = 0; i < arrlen(sResOnlyWidgets); i++) { if (!loadFn) { - dvxLog("Skipping %s: the BASIC run-time library is not loaded", sResOnlyWidgets[i]); + dvxLog("Skipping %s: the BASIC run-time library is not installed", sResOnlyWidgets[i]); } else if (loadFn(sResOnlyWidgets[i])) { recordIfacePaths(sResOnlyWidgets[i]); } else { @@ -752,6 +1005,18 @@ static void loadBasicControls(void) { } +// The settings functions of the loaded libdvx; false when one is missing. +static bool loaderPrefs(LoaderPrefsT *prefs) { + prefs->load = (__typeof__(prefsLoad) *)dlsym(NULL, DVX_SYM("prefsLoad")); + prefs->getInt = (__typeof__(prefsGetInt) *)dlsym(NULL, DVX_SYM("prefsGetInt")); + prefs->setInt = (__typeof__(prefsSetInt) *)dlsym(NULL, DVX_SYM("prefsSetInt")); + prefs->save = (__typeof__(prefsSave) *)dlsym(NULL, DVX_SYM("prefsSave")); + prefs->close = (__typeof__(prefsClose) *)dlsym(NULL, DVX_SYM("prefsClose")); + + return prefs->load && prefs->getInt && prefs->setInt && prefs->save && prefs->close; +} + + // Loads the modules whose dependencies are loaded, pass after pass, until // each one is loaded or skipped or the rest wait on each other. A // dependency is found among mods and others (the other kind of module). @@ -951,7 +1216,7 @@ static void moduleLoad(ModuleT *mod) { return; } - mod->handle = dlopen(mod->path, RTLD_NOW | RTLD_GLOBAL); + mod->handle = moduleOpen(mod->path, RTLD_NOW | RTLD_GLOBAL); // The loader's own words go to the log; the note says it plainly if (!mod->handle) { @@ -969,6 +1234,8 @@ static void moduleLoad(ModuleT *mod) { return; } + arrput(sLoaded, mod->handle); + RegFnT regFn = (RegFnT)dlsym(mod->handle, DVX_SYM("wgtRegister")); if (regFn) { @@ -982,6 +1249,62 @@ static void moduleLoad(ModuleT *mod) { } +// dlopen, with what the module needs: when the open fails for names that +// libraries not loaded yet export (the resolver marked them), those are +// loaded and the module is opened again. NULL (dlerror says why) when +// nothing more can be loaded for it. platformModuleOpen is this. +static void *moduleOpen(const char *path, int32_t mode) { + for (;;) { + ModuleT **wanted = NULL; + void *handle; + int32_t loaded = 0; + + sOpenDepth++; + handle = dlopen(path, (int)mode); + sOpenDepth--; + + if (handle) { + return handle; + } + + // Taken before loading: those loads open modules (and mark) too + for (int32_t i = 0; i < arrlen(sLibs); i++) { + if (sLibs[i].wanted) { + sLibs[i].wanted = false; + arrput(wanted, &sLibs[i]); + } + } + + for (int32_t i = 0; i < arrlen(wanted); i++) { + loaded += libLoad(wanted[i]) ? 1 : 0; + } + + arrfree(wanted); + + if (loaded == 0) { + return NULL; + } + } +} + + +// A file of scanDir's walk: a module when it has the extension. +static bool moduleVisit(const char *path, const char *name, void *ctx) { + ModWalkT *walk = (ModWalkT *)ctx; + + if (dvxHasExt(name, walk->ext)) { + ModuleT mod; + + memset(&mod, 0, sizeof(mod)); + snprintf(mod.path, sizeof(mod.path), "%s", path); + extractBaseName(path, walk->ext, mod.baseName, sizeof(mod.baseName)); + arrput(*walk->mods, mod); + } + + return true; +} + + // The .dep file has the same path as the module but with a .dep // extension. Each line is a dependency base name. Empty lines // and lines starting with # are ignored. @@ -1075,6 +1398,33 @@ static void recordIfacePaths(const char *modPath) { } +// DJGPP's last resort (platformSetSymbolResolver) for a name that neither +// dvx.exe nor a loaded module has: dlopen asks it for each import, +// dlsym(NULL) for its name. During a moduleOpen it marks the library that +// has the name (dlopen is under way and must not be entered again) and +// gives nothing; otherwise it loads that library and gives the name's +// address. +static void *resolveLater(const char *name) { + uint32_t hash; + + if (sOpenDepth > 0) { + libsWant(name); + return NULL; + } + + for (int32_t i = exportFirst(name, &hash); i < arrlen(sExports) && sExports[i].hash == hash; i++) { + ModuleT *lib = &sLibs[sExports[i].lib]; + void *sym = libLoad(lib) ? dlsym(lib->handle, name) : NULL; + + if (sym) { + return sym; + } + } + + return NULL; +} + + // A safe start (DVX_SWITCH_SAFE, or Shift held): no add-ons are loaded. // The shell reads the same, for safe video mode. static bool safeStartAsked(int argc, char *argv[]) { @@ -1089,37 +1439,11 @@ static bool safeStartAsked(int argc, char *argv[]) { static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) { - // Collect all entries first, close the handle, then process. - // DOS has limited file handles; keeping a DIR open during - // recursion or stat() causes intermittent failures. - char **names = dvxReadDir(dirPath); + ModWalkT walk; - if (!names) { - return; - } - - int32_t count = (int32_t)arrlen(names); - - for (int32_t i = 0; i < count; i++) { - char path[DVX_MAX_PATH]; - snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", dirPath, names[i]); - - if (dvxHasExt(names[i], ext)) { - ModuleT mod; - memset(&mod, 0, sizeof(mod)); - snprintf(mod.path, sizeof(mod.path), "%s", path); - extractBaseName(path, ext, mod.baseName, sizeof(mod.baseName)); - arrput(*mods, mod); - } else { - struct stat st; - - if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - scanDir(path, ext, mods); - } - } - } - - dvxReadDirFree(names); + walk.ext = ext; + walk.mods = mods; + platformWalkDir(dirPath, moduleVisit, &walk); } @@ -1195,7 +1519,7 @@ static void splashUpdateProgress(void) { // before the shell starts. The handle joins the module list so it is // closed with the rest at exit. Returns the module, or NULL when no // script was asked for or the module could not be used. -static void *startTestScript(int argc, char *argv[], void ***handles) { +static void *startTestScript(int argc, char *argv[]) { size_t switchLen = strlen(SCRIPT_SWITCH); for (int32_t i = 1; i < argc; i++) { @@ -1203,7 +1527,7 @@ static void *startTestScript(int argc, char *argv[], void ***handles) { continue; } - void *handle = dlopen(SCRIPT_MODULE, RTLD_NOW | RTLD_GLOBAL); + void *handle = moduleOpen(SCRIPT_MODULE, RTLD_NOW | RTLD_GLOBAL); if (!handle) { const char *err = dlerror(); @@ -1211,7 +1535,7 @@ static void *startTestScript(int argc, char *argv[], void ***handles) { return NULL; } - arrput(*handles, handle); + arrput(sLoaded, handle); ScriptStartFnT start = (ScriptStartFnT)dlsym(handle, DVX_SYM("scriptStart")); @@ -1312,16 +1636,13 @@ int main(int argc, char *argv[]) { platformRegisterDxeExports(); dvxLog("Platform exports registered."); - // Load all modules from libs/ and widgets/ in dependency order. - // Each module may have a .dep file specifying load-before deps. - // Widget modules that export wgtRegister() get it called. An add-on - // that cannot load is skipped (and none load in a safe start). - void **handles = loadAllModules(safeStartAsked(argc, argv)); - - if (!handles || arrlen(handles) == 0) { + // The widgets, the shell, and the libraries that run code as they + // load, each with what it needs (see the top of this file); the other + // libraries load when first needed. An add-on that cannot load is + // skipped (and none load in a safe start). + if (!loadAllModules(safeStartAsked(argc, argv))) { splashShutdownIfActive(); fprintf(stderr, "No modules loaded from %s" DVX_PATH_SEP " or %s" DVX_PATH_SEP "\n", DVX_LIBS_DIR, DVX_WIDGETS_DIR); - arrfree(handles); return 1; } @@ -1330,22 +1651,22 @@ int main(int argc, char *argv[]) { // Find and call shellMain from whichever module exports it typedef int (*ShellMainFnT)(int, char **); - ShellMainFnT shellMain = (ShellMainFnT)findSymbol(handles, DVX_SYM("shellMain")); + ShellMainFnT shellMain = (ShellMainFnT)dlsym(NULL, DVX_SYM("shellMain")); if (!shellMain) { dvxLog("ERROR: No module exports shellMain"); splashShutdownIfActive(); - for (int32_t i = arrlen(handles) - 1; i >= 0; i--) { - dlclose(handles[i]); + for (int32_t i = arrlen(sLoaded) - 1; i >= 0; i--) { + dlclose(sLoaded[i]); } - arrfree(handles); + arrfree(sLoaded); return 1; } - void *scriptModule = startTestScript(argc, argv, &handles); + void *scriptModule = startTestScript(argc, argv); int result = shellMain(argc, argv); @@ -1360,10 +1681,12 @@ int main(int argc, char *argv[]) { } // Clean up in reverse load order - for (int32_t i = arrlen(handles) - 1; i >= 0; i--) { - dlclose(handles[i]); + for (int32_t i = arrlen(sLoaded) - 1; i >= 0; i--) { + dlclose(sLoaded[i]); } - arrfree(handles); + arrfree(sLoaded); + arrfree(sExports); + freeMods(sLibs); return result; } diff --git a/src/packages.ini b/src/packages.ini index 0616bed4..b5313162 100644 --- a/src/packages.ini +++ b/src/packages.ini @@ -59,9 +59,8 @@ members = kpunch.dvxnet kpunch.http kpunch.pkgnet [kpunch.comm] name = Communications -summary = The Terminal: modems, serial and secure links, Telnet, and file transfers. -members = kpunch.terminal kpunch.conn kpunch.xfer -needs = kpunch.net +summary = The Terminal: modems, serial and secure links, Telnet (with Networking), and file transfers. +members = kpunch.terminal kpunch.ansiterm kpunch.conn kpunch.seclink kpunch.xfer [kpunch.media] name = Media @@ -100,6 +99,7 @@ summary = SQLite databases for BASIC programs. kind = widget name = Data Control summary = The Data control: a table or query of a database, record by record. +uses = kpunch.dvxsql [kpunch.dbgrid] kind = widget @@ -126,12 +126,23 @@ summary = Software's link to the package server: updates and new software. kind = app name = Terminal summary = Modems, serial and secure links, Telnet, and file transfers. +uses = kpunch.ansiterm + +[kpunch.ansiterm] +kind = widget +name = Terminal Control +summary = The Terminal control: an ANSI terminal for BBSes and serial links. [kpunch.conn] kind = library name = Connections summary = Serial, modem, secure-link, and Telnet connections for programs. +[kpunch.seclink] +kind = library +name = Secure Link +summary = Reliable packets and encrypted links over serial ports and the network, for programs. + [kpunch.xfer] kind = library name = File Transfer diff --git a/src/savers/kpunch/starfld/starfld.res b/src/savers/kpunch/starfld/starfld.res index eb14c535..24b0cb00 100644 --- a/src/savers/kpunch/starfld/starfld.res +++ b/src/savers/kpunch/starfld/starfld.res @@ -1,3 +1,25 @@ +# 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. + # starfld.res -- Resource manifest for the Starfield screen saver name text "Starfield" author text "Scott Duensing" diff --git a/src/sdk/README.TXT b/src/sdk/README.TXT index 0be67119..3fb97e04 100644 --- a/src/sdk/README.TXT +++ b/src/sdk/README.TXT @@ -58,7 +58,11 @@ Directory Structure shell/ Shell/app loading API tasks/ Cooperative task switching API sql/ SQLite database wrapper API - serial/ Serial port, packet, security and secure link APIs + serial/ Serial ports: the interrupt-driven UART driver + crypto/ Cryptography: Diffie-Hellman, XTEA-CTR, SHA-256, HMAC, + HKDF, and the random number generator + seclink/ Reliable packets and the encrypted, multi-channel secure + link over a serial port or any byte stream texthelp/ Shared text editing helpers for widgets listhelp/ Shared list and dropdown helpers for widgets richtext/ Styled text and RTF in and out (RichEdit widget) diff --git a/src/sdk/templates/basic/tplName.frm b/src/sdk/templates/basic/tplName.frm index 58643110..30c4779f 100644 --- a/src/sdk/templates/basic/tplName.frm +++ b/src/sdk/templates/basic/tplName.frm @@ -34,6 +34,7 @@ Begin Form TplNameForm End End + ' tplName.frm -- the main form of TplName. ' ' Help > Contents (and F1) opens tplName.hlp, compiled from tplName.dhs diff --git a/src/sdk/templates/basicgame/tplName.frm b/src/sdk/templates/basicgame/tplName.frm index 40f4235b..ba619a83 100644 --- a/src/sdk/templates/basicgame/tplName.frm +++ b/src/sdk/templates/basicgame/tplName.frm @@ -40,6 +40,7 @@ Begin Form TplNameForm End End + ' tplName.frm -- TplName, a DVX BASIC game. ' ' The game loop runs in tmrStart_Timer: move, draw, then FRAMEWAIT, diff --git a/src/sdk/templates/control/tplName.ctl b/src/sdk/templates/control/tplName.ctl index 6a8b20c1..194c9ef0 100644 --- a/src/sdk/templates/control/tplName.ctl +++ b/src/sdk/templates/control/tplName.ctl @@ -8,6 +8,8 @@ Begin UserControl TplName Description = "TplName" DefaultEvent = "Click" End + + ' tplName.ctl -- the TplName control, written in DVX BASIC: a button-like ' box that shows its Caption and counts the clicks it gets. Every ' program can use it once tplName.wgt is in DVX's WIDGETS folder: diff --git a/src/sdk/templates/library/tplName.bas b/src/sdk/templates/library/tplName.bas index 9dd6229a..0611d9ff 100644 --- a/src/sdk/templates/library/tplName.bas +++ b/src/sdk/templates/library/tplName.bas @@ -1,3 +1,4 @@ + ' tplName.bas -- DVX BASIC declarations for the TplName library. ' ' Add this file to a BASIC project to call the library. diff --git a/src/sdkHeaders.mk b/src/sdkHeaders.mk index 9b2d42c8..fcbba7d0 100644 --- a/src/sdkHeaders.mk +++ b/src/sdkHeaders.mk @@ -30,7 +30,7 @@ # SDK_ROOT is the repository root as seen from the including Makefile, # with a trailing slash (empty at the top level). -SDK_CORE_H = $(addprefix $(SDK_ROOT)src/libs/kpunch/libdvx/,dvxApp.h dvxTypes.h dvxWgt.h dvxWgtP.h dvxWm.h dvxDraw.h dvxVideo.h dvxComp.h dvxPrefs.h dvxDlg.h dvxRes.h dvxFont.h dvxText.h dvxFlow.h dvxCur.h dvxPal.h dvxMem.h dvxDrv.h platform/dvxPlat.h) +SDK_CORE_H = $(addprefix $(SDK_ROOT)src/libs/kpunch/libdvx/,dvxApp.h dvxTypes.h dvxWgt.h dvxWgtP.h dvxWm.h dvxDraw.h dvxVideo.h dvxComp.h dvxPrefs.h dvxDlg.h dvxRes.h dvxFont.h dvxText.h dvxFlow.h dvxCur.h dvxPal.h dvxMem.h dvxEndian.h dvxHash.h dvxDrv.h platform/dvxPlat.h) SDK_SHELL_H = $(addprefix $(SDK_ROOT)src/libs/kpunch/dvxshell/,shellApp.h shellAssoc.h shellInf.h shellProg.h shellRun.h shellSaver.h pmModel.h) $(SDK_ROOT)src/libs/kpunch/taskmgr/shellTaskMgr.h SDK_TASKS_H = $(SDK_ROOT)src/libs/kpunch/libtasks/taskSwch.h SDK_SQL_H = $(SDK_ROOT)src/libs/kpunch/sql/dvxSql.h @@ -50,10 +50,9 @@ SDK_JSON_H = $(SDK_ROOT)src/libs/kpunch/json/json.h SDK_HTTP_H = $(SDK_ROOT)src/libs/kpunch/http/http.h SDK_PKGNET_H = $(SDK_ROOT)src/libs/kpunch/pkgnet/pkgNet.h SDK_PKG_H = $(addprefix $(SDK_ROOT)src/libs/kpunch/pkg/,pkg.h pkgFormat.h pkgZip.h pkgManifest.h pkgSign.h pkgDpk.h pkgTrust.h pkgDb.h pkgPlan.h pkgOp.h) -# Serial headers keep their subdirectories (secLink.h includes -# ../packet/packet.h), so they are listed relative to SDK_SERIAL_DIR. -SDK_SERIAL_DIR = $(SDK_ROOT)src/libs/kpunch/serial -SDK_SERIAL_H = rs232/rs232.h packet/packet.h security/security.h security/secHash.h seclink/secLink.h +SDK_SERIAL_H = $(SDK_ROOT)src/libs/kpunch/serial/rs232/rs232.h +SDK_CRYPTO_H = $(addprefix $(SDK_ROOT)src/libs/kpunch/crypto/,security.h secHash.h secCurve.h) +SDK_SECLINK_H = $(addprefix $(SDK_ROOT)src/libs/kpunch/seclink/,packet.h secLink.h) SDK_WIDGET_PRIVATE_H = $(addprefix $(SDK_ROOT)src/widgets/kpunch/,box/boxMetrics.h checkbox/checkGeom.h image/imageBuf.h label/alignNames.h) SDK_WIDGET_H = $(filter-out $(SDK_WIDGET_PRIVATE_H),$(wildcard $(SDK_ROOT)src/widgets/kpunch/*/*.h)) # ---- Networking (dvxnet; added with Phase 4) ---- diff --git a/src/test/abi/Makefile b/src/test/abi/Makefile index 68e7a6d6..9577da09 100644 --- a/src/test/abi/Makefile +++ b/src/test/abi/Makefile @@ -44,7 +44,7 @@ include ../../sdkHeaders.mk OBJDIR = $(HOST_OBJROOT)/test/abi TABLE = abiSizes.txt -HEADERS = $(SDK_CORE_H) $(SDK_SHELL_H) $(SDK_TASKS_H) $(SDK_SQL_H) $(SDK_TEXTHELP_H) $(SDK_LISTHELP_H) $(SDK_RICHTEXT_H) $(SDK_FILEOPS_H) $(SDK_HLPFILE_H) $(SDK_XFER_H) $(SDK_CONN_H) $(SDK_PRINT_H) $(SDK_GAME_H) $(SDK_JOYSTICK_H) $(SDK_AUDIO_H) $(SDK_MUSIC_H) $(SDK_PKG_H) $(SDK_JSON_H) $(SDK_HTTP_H) $(SDK_PKGNET_H) $(addprefix $(SDK_SERIAL_DIR)/,$(SDK_SERIAL_H)) $(SDK_WIDGET_H) $(SDK_NET_H) +HEADERS = $(SDK_CORE_H) $(SDK_SHELL_H) $(SDK_TASKS_H) $(SDK_SQL_H) $(SDK_TEXTHELP_H) $(SDK_LISTHELP_H) $(SDK_RICHTEXT_H) $(SDK_FILEOPS_H) $(SDK_HLPFILE_H) $(SDK_XFER_H) $(SDK_CONN_H) $(SDK_PRINT_H) $(SDK_GAME_H) $(SDK_JOYSTICK_H) $(SDK_AUDIO_H) $(SDK_MUSIC_H) $(SDK_PKG_H) $(SDK_JSON_H) $(SDK_HTTP_H) $(SDK_PKGNET_H) $(SDK_SERIAL_H) $(SDK_CRYPTO_H) $(SDK_SECLINK_H) $(SDK_WIDGET_H) $(SDK_NET_H) ABI_FLAGS = $(WARNFLAGS) -march=i486 -I$(DVX_ROOT)/src/libs/kpunch/libdvx/thirdparty .PHONY: all check update clean diff --git a/src/test/abi/abiSizes.txt b/src/test/abi/abiSizes.txt index da6e363c..04047805 100644 --- a/src/test/abi/abiSizes.txt +++ b/src/test/abi/abiSizes.txt @@ -7,7 +7,7 @@ AccelEntryT 12 AccelTableT 12 AnsiTermApiT 36 AnsiTermExApiT 20 -AppContextT 3484 +AppContextT 3508 AppDescriptorT 80 AudioDriverT 36 AudioDrvConfigT 16 @@ -21,14 +21,15 @@ AudioMidiMsgT 8 AudioStatsT 24 AudioSynthDriverT 64 AudioSynthDrvInfoT 84 -AudioWtInfoT 92 +AudioVocT 40 +AudioWtInfoT 96 AudioWtVoiceT 96 BevelStyleT 16 BitmapFontT 64 BlitOpsT 12 BoxApiT 20 ButtonApiT 12 -CanvasApiT 84 +CanvasApiT 88 CheckboxApiT 20 ColorSchemeT 80 ComboBoxApiT 52 @@ -47,11 +48,11 @@ DvxResDirEntryT 48 DvxResFooterT 16 DvxResHandleT 268 DxeAppContextT 2216 -ExclusiveStateT 1940 +ExclusiveStateT 1956 FileFilterT 4 FileOpsCallbacksT 12 FileOpsEntryT 20 -FileOpsProgressT 32 +FileOpsProgressT 40 FontGlyphT 6 FontStampT 8 GameFramePacerT 24 @@ -66,8 +67,8 @@ HlpTocEntryT 8 HlpTopicDirT 20 HlpTrigramEntryT 8 HttpUrlT 642 -ImageApiT 28 -ImageButtonApiT 24 +ImageApiT 32 +ImageButtonApiT 28 JoyAxisCalT 12 JoyDriverT 40 JoyEventT 16 @@ -82,12 +83,13 @@ ListIconGridT 80 ListItemsT 28 ListPopupT 44 ListSelT 16 -ListViewApiT 84 +ListViewApiT 92 ListViewColT 12 MenuBarT 20 MenuItemT 48 MusicInfoT 148 NetConfigT 152 +NetLookupT 280 NetStatusT 328 PixelFormatT 44 PkgDbT 264 @@ -103,6 +105,7 @@ PkgManifestT 820 PkgNetConfigT 386 PkgNetEntryT 860 PkgNetMineT 308 +PkgOfferScanT 288 PkgOfferT 1104 PkgPlanT 432 PkgSecretT 104 @@ -111,9 +114,10 @@ PkgStepT 12 PkgTierT 16 PkgVersionT 6 PkgZipEntryT 280 -PktStreamT 12 +PktStreamT 16 PlatformDriveT 8 PlatformKeyEventT 12 +PlatformStampT 8 PlatformSymOverrideT 8 PlatformTimerStatsT 16 PmGroupT 72 @@ -143,8 +147,10 @@ ScrollPaneApiT 40 ScrollbarT 32 SecHmacT 216 SecSha256T 108 -SeparatorApiT 20 -ShellAppT 624 +SecX25519JobT 288 +SeparatorApiT 28 +ShellAppT 636 +ShellIconsT 16 ShellProgramT 452 SliderApiT 20 SpacerApiT 12 @@ -156,11 +162,11 @@ SysMenuStateT 432 TabControlApiT 24 TextEditLineCacheT 28 TextEditMultiOptionsT 8 -TextEditMultiT 80 +TextEditMultiT 92 TextEditPaintHooksT 36 TextEditSingleT 40 TextInputApiT 144 -TimerApiT 32 +TimerApiT 44 ToolbarApiT 16 TreeViewApiT 48 VideoModeInfoT 12 @@ -172,7 +178,7 @@ WgtIfaceT 56 WgtMethodDescT 12 WgtPropDescT 20 WgtSigInfoT 7 -WgtValueT 12 +WgtValueT 16 WidgetClassT 136 WidgetT 236 WindowStackT 60 diff --git a/src/test/audio/audioTest.c b/src/test/audio/audioTest.c index 273a19df..9fdf2e1a 100644 --- a/src/test/audio/audioTest.c +++ b/src/test/audio/audioTest.c @@ -70,6 +70,7 @@ #include "dvxtest.h" #include "dvxPlat.h" #include "dvxPlatHost.h" +#include "dvxEndian.h" #include "dvxPlatIsr.h" #include "audioInt.h" #include "sbCalc.h" @@ -168,6 +169,12 @@ #define DESK_SOUND_DIR DVX_SHARED_DIR DVX_PATH_SEP DVX_SOUNDS_DIR // the shipped sounds #define DESK_SOUND DESK_SOUND_DIR DVX_PATH_SEP "ding.wav" #define YIELD_STEP_US 1000 +#define DESK_ONCE_NAME "once.wav" // a sound only in the sounds folder +#define DESK_ONCE DESK_SOUND_DIR DVX_PATH_SEP DESK_ONCE_NAME +#define FILL_SLOW_MS 10 // fillSlow: the clock a call takes +#define FILL_SLOW_BUFFER_MS 1000 +#define FILL_SAFE_MS 100 // what a pump fills at the least (audio.c STREAM_SAFE_MS) +#define FILL_SLOW_PUMPS 64 #define DRIVER_NAMES_MAX 8 #define DRIVERS_FAKE 5 // the fake drivers that load (alpha .. mono8) #define MIDI_FM_INDEX 2 @@ -183,9 +190,12 @@ #define BAS_PCM_MS 100 #define BAS_PCM_LEVEL 200 // an 8-bit sample above the middle (128) #define BAS_STREAM_FRAMES 100 +#define BAS_NOTE_MS 10 // a note queue's tone (milliseconds) #define BAS_RENDER_FRAMES 32 #define BAS_PITCH_DOUBLE 200 #define BAS_WT_FRAMES 4 // WtUpload's samples in the BASIC test +#define BAS_WT_ATTACK_TC (-1200) // WtShape: half a second's attack (timecents) +#define BAS_WT_FILTER_FC 8000 // ... and a filter (absolute cents) #define BAS_FM_REG 0xB0 // FM_KEY_BLOCK #define BAS_FM_RAW_REG 0xB3 #define BAS_MIDI_NOTE_ON 0x90 @@ -223,6 +233,8 @@ static bool sGoldenDirty = false; static bool sUpdate = false; static int32_t sHookRuns = 0; static uint32_t sFillCalls = 0; +static int32_t sWaitPumps = 0; // waitPumpCount calls +static bool sWaitGoOn = true; // what waitPumpCount answers static int16_t sFillValue = 0; static uint8_t sFmRegs[FM_REGS]; static uint32_t sFmWrites = 0; @@ -247,6 +259,7 @@ static char sCapture[BAS_CAPTURE_MAX]; // ============================================================ static void aweTests(void); +static void basicGapTests(void); static void basicLiveTests(void); static void basicRecordTests(void); static void basicSampleTests(void); @@ -275,6 +288,7 @@ void fakeWtUpdate(uint32_t voice, const AudioWtVoiceT *v, uint32_t wh bool fakeWtUpload(uint32_t addr, const int16_t *frames, uint32_t count, uint32_t bits); static void fileTests(void); static uint32_t fillRamp(AudioStreamT *stream, void *buf, uint32_t frames, void *userData); +static uint32_t fillSlow(AudioStreamT *stream, void *buf, uint32_t frames, void *userData); static void fmRawTests(void); static void fmTests(void); static void fmToneTests(void); @@ -295,8 +309,6 @@ static void onEnd(AudioCtxT *ctx, int32_t voice, AudioEndE reason, void *use static int32_t outPeak(uint32_t frames); static void platformTests(void); static void programTests(void); -static void put16(uint8_t *p, uint32_t v); -static void put32(uint8_t *p, uint32_t v); static void recordTests(void); static void render(uint32_t frames); static void saveGoldens(void); @@ -307,6 +319,8 @@ static void settingsTests(void); static void suspendTests(void); static void toneTests(void); static void voiceTests(void); +static bool waitPumpCount(void); +static void writeIni(const char *extra); static void wssTests(void); static void wtTests(void); @@ -371,6 +385,113 @@ static void aweTests(void) { } +// sound.bas: a sound from the bytes of a file and its format, the +// driver's name, the mixer's counters, each ended voice once +// (SoundEnded), recording into memory (RecordRead$), and note queues +// whose tones change waveform in turn. +static void basicGapTests(void) { + static uint8_t pcm8[PCM_FRAMES]; + static uint8_t wav[WAV_MAX]; + int16_t block[REC_BLOCK_FRAMES]; + int32_t len = 0; + + tCase("basic: a sound from bytes, its format, the driver, the counters"); + setIni("driver = gamma\n"); + sAppId = APP_A; + memset(pcm8, BAS_PCM_LEVEL, sizeof(pcm8)); + + uint32_t n = makeWav(wav, 1, 1, RATE_HALF, 8, 1, pcm8, PCM_FRAMES, false); + int32_t snd = SoundLoadData(wav, (int32_t)n); + + TEST_TRUE(snd > 0); + TEST_EQ(SoundRate(snd), RATE_HALF); + TEST_EQ(SoundBits(snd), 8); + TEST_EQ(SoundChannels(snd), 1); + TEST_EQ(SoundLength(snd), PCM_FRAMES * MS_PER_SEC / RATE_HALF); + TEST_EQ(SoundLoadData((const uint8_t *)"junk", 4), 0); + TEST_STR_EQ(SoundError(), audioErrorText(AUDIO_ERR_FORMAT)); + TEST_EQ(SoundRate(snd + 1), 0); + TEST_STR_EQ(SoundDriver(), "gamma"); + + tCase("basic: SoundEnded gives each ended voice once, with its reason"); + int32_t once = SoundPlay(snd, PERCENT_FULL, 0, 0); + int32_t loop = SoundPlay(snd, PERCENT_FULL, 0, 1); + + TEST_EQ(SoundStats("Playing"), 2); + TEST_EQ(SoundStats("nonsense"), 0); + TEST_EQ(SoundEnded(), 0); + render(PCM_FRAMES * (RATE_DEV / RATE_HALF) + BAS_RENDER_FRAMES); + audioPump(); + TEST_TRUE(SoundStats("renders") > 0); + TEST_EQ(SoundEnded(), once); + TEST_EQ(SoundEndReason(), AUDIO_END_FINISHED); + TEST_EQ(SoundEnded(), 0); + StopWave(loop); + audioPump(); + + // Another program's ends are not this one's + sAppId = APP_B; + TEST_EQ(SoundEnded(), 0); + sAppId = APP_A; + TEST_EQ(SoundEnded(), loop); + TEST_EQ(SoundEndReason(), AUDIO_END_STOPPED); + + tCase("basic: recording into memory"); + for (int32_t i = 0; i < REC_BLOCK_FRAMES; i++) { + block[i] = (int16_t)i; + } + + TEST_EQ(RecordStart("", 0, 0, 0, AUDIO_REC_MIC), BAS_TRUE); + TEST_EQ(RecordQueued(), 0); + audioRecPush(block, sizeof(block)); + audioPump(); + TEST_EQ(RecordQueued(), (int32_t)sizeof(block)); + + // Whole samples only: three bytes give one 16-bit sample + const uint8_t *got = RecordRead(3, &len); + + TEST_EQ(len, (int32_t)sizeof(int16_t)); + got = RecordRead((int32_t)sizeof(block), &len); + TEST_EQ(len, (int32_t)(sizeof(block) - sizeof(int16_t))); + TEST_TRUE(memcmp(got, block + 1, (size_t)len) == 0); + TEST_EQ(RecordQueued(), 0); + RecordRead((int32_t)sizeof(block), &len); + TEST_EQ(len, 0); + TEST_EQ(RecordStop(), BAS_TRUE); + RecordRead((int32_t)sizeof(block), &len); + TEST_EQ(len, 0); + + tCase("basic: note queues, the waveform changing from one tone to the next"); + int32_t q = NotesOpen(AUDIO_WAVE_TRIANGLE, PERCENT_HALF); + + TEST_TRUE(q > 0); + TEST_EQ(NotesAdd(q, TONE_HZ + 0.5, BAS_NOTE_MS), BAS_TRUE); + TEST_EQ(NotesSetTone(q, AUDIO_WAVE_NOISE, PERCENT_HALF), BAS_TRUE); + TEST_EQ(NotesAdd(q, TONE_HZ, BAS_NOTE_MS), BAS_TRUE); + TEST_EQ(NotesAdd(q, 1, BAS_NOTE_MS), 0); + TEST_EQ(NotesSetTone(q, AUDIO_WAVE_NOISE + 1, PERCENT_HALF), 0); + TEST_EQ(NotesQueued(q), 2); + TEST_EQ(NotesSpace(q), AUDIO_NOTES_MAX - 2); + + int32_t slot = (NotesVoice(q) - 1) % AUDIO_VOICES_MAX; + + render(BAS_RENDER_FRAMES); + TEST_EQ(audioMixer.voices[slot].wave, AUDIO_WAVE_TRIANGLE); + TEST_EQ(audioMixer.voices[slot].level, AUDIO_VOLUME_MAX); + render(RATE_DEV * BAS_NOTE_MS / MS_PER_SEC); + TEST_EQ(audioMixer.voices[slot].wave, AUDIO_WAVE_NOISE); + TEST_EQ(audioMixer.voices[slot].level, AUDIO_VOLUME_MAX / 2); + NotesClear(q); + TEST_EQ(NotesQueued(q), 0); + NotesClose(q); + TEST_EQ(NotesQueued(q), 0); + TEST_STR_EQ(SoundError(), "Invalid argument"); + audioAppEnded(APP_A); + audioAppEnded(APP_B); + sAppId = 0; +} + + // sound.bas and music.bas compiled with a program whose DECLARE LIBRARY // calls reach the library for real: BINARY STRING data with zero bytes // arrives whole (its length shows in SoundLength and the bytes a stream @@ -400,7 +521,8 @@ static void basicLiveTests(void) { "PRINT SoundLength(s); SoundPlay(s, 100, SOUND_PAN_LEFT, 0) > 0\n" "st = SoundStreamOpen(SOUND_RATE_DEFAULT, 16, 1, 0)\n" "PRINT SoundStreamWrite(st, CHR$(0) + CHR$(0) + CHR$(0) + CHR$(7) + CHR$(0)); SoundStreamQueued(st)\n" - "PRINT SoundCaps(\"rate\"); FmInfo$(\"voices\"); \"/\"; MidiPortInfo$(\"present\")\n", soundBas, musicBas); + "PRINT SoundCaps(\"rate\"); FmInfo$(\"voices\"); \"/\"; MidiPortInfo$(\"present\")\n" + "PRINT SoundDriver$(); SoundLoadData(\"junk\"); NotesOpen(SOUND_WAVE_TRIANGLE, 50) > 0; LEN(RecordRead$(10)); WT_ADDR_ROM\n", soundBas, musicBas); BasCompilerT *compiler = basCompilerCreate(src, (int32_t)strlen(src)); BasModuleT *mod = NULL; @@ -430,7 +552,7 @@ static void basicLiveTests(void) { basNativeHostSetLive(false); TEST_TRUE(rr == BAS_VM_HALTED || rr == BAS_VM_OK); - TEST_STR_EQ(sCapture, "100 True \n4 2 \n22050 9/-1\n"); + TEST_STR_EQ(sCapture, "100 True \n4 2 \n22050 9/-1\ngamma0 True 0 -2147483648 \n"); } basVmDestroy(vm); @@ -721,6 +843,18 @@ static void basicStreamTests(void) { // A flush drops what is queued; the stream stays open. SoundStreamFlush(h); TEST_EQ(SoundStreamQueued(h), 0); + + // SoundStreamEnd: the voice ends once what is held has played, and + // SoundEnded says so + voice = SoundStreamVoice(h); + SoundSetVolume(voice, PERCENT_FULL); + TEST_EQ(SoundStreamWrite(h, (const uint8_t *)pcm, (int32_t)sizeof(pcm)), (int32_t)sizeof(pcm)); + SoundStreamEnd(h); + SoundStreamEnd(BAS_BAD_HANDLE); + render(BAS_STREAM_FRAMES + BAS_RENDER_FRAMES); + audioPump(); + TEST_EQ(SoundEnded(), voice); + TEST_EQ(SoundEndReason(), AUDIO_END_FINISHED); TEST_TRUE(SoundStreamSpace(h) > 0); SoundStreamClose(h); TEST_EQ(SoundStreamQueued(h), 0); @@ -796,6 +930,7 @@ static void basicSynthTests(void) { snprintf(text, sizeof(text), "%d", WT_FAKE_MEMORY); TEST_STR_EQ(WtInfo("memory"), text); TEST_STR_EQ(WtInfo("freememory"), text); + TEST_STR_EQ(WtInfo("FreeTotal"), text); TEST_STR_EQ(WtInfo("level"), "0"); TEST_STR_EQ(WtInfo("envelopes"), "-1"); TEST_STR_EQ(WtInfo("8bit"), "0"); @@ -836,6 +971,21 @@ static void basicSynthTests(void) { TEST_EQ(WtSetPan(wv, -100), BAS_TRUE); TEST_EQ(sWtVoice[0].pan, -AUDIO_WT_PAN_MAX); TEST_EQ(sWtVoice[0].pitch, 1300); + + // The voice's shape: kept for its next note, a filter change sent to + // the sounding one at once + TEST_EQ(WtShape(wv, "VolAttack", BAS_WT_ATTACK_TC), BAS_TRUE); + TEST_EQ(WtShape(wv, "filterfc", BAS_WT_FILTER_FC), BAS_TRUE); + TEST_EQ(sWtWhat, AUDIO_WT_SET_FILTER); + TEST_EQ(sWtVoice[0].filterFc, BAS_WT_FILTER_FC); + TEST_EQ(WtShape(wv, "nonsense", 0), 0); + TEST_EQ(WtShape(wv + 1, "reverb", 0), 0); + TEST_EQ(WtStart(wv, wtAddr, 0, BAS_WT_FRAMES, 0, 0, AUDIO_WT_LOOP_ON, 16, 0, 0, 0), BAS_TRUE); + TEST_EQ(sWtVoice[0].volEnv[AUDIO_WT_ENV_ATTACK], BAS_WT_ATTACK_TC); + TEST_EQ(sWtVoice[0].filterFc, BAS_WT_FILTER_FC); + + // A sample in a ROM the chip does not have + TEST_EQ(WtStart(wv, (int32_t)AUDIO_WT_ADDR_ROM, 0, BAS_WT_FRAMES, 0, 0, AUDIO_WT_LOOP_NONE, 16, 0, 0, 0), 0); TEST_EQ(WtSetPitch(wv + 1, 0), 0); sWtReleased = 0; WtRelease(wv); @@ -1078,6 +1228,28 @@ static void deskTests(void) { dvxBeep(); audioGetStats(&st); TEST_EQ(st.playing, 1); + + // Decoded once: the file gone, the event still sounds; a reload reads + // the file again (and stops what played the old one) + fp = fopen(DESK_ONCE, "wb"); + TEST_TRUE(fp && fwrite(wav, 1, n, fp) == n); + + if (fp) { + fclose(fp); + } + + writeIni("driver = gamma\n[sounds]\ndefault = " DESK_ONCE_NAME "\n"); + audioDeskReload(); + dvxBeep(); + TEST_EQ(remove(DESK_ONCE), 0); + dvxBeep(); + audioGetStats(&st); + TEST_EQ(st.playing, 2); + audioDeskReload(); + dvxBeep(); + audioPump(); + audioGetStats(&st); + TEST_EQ(st.playing, 0); TEST_TRUE(audioDeskPlayFile(DESK_SOUND) > 0); TEST_EQ(audioDeskPlayFile("missing.wav"), AUDIO_ERR_OPEN); audioShutdown(); @@ -1097,6 +1269,19 @@ static void deskTests(void) { dvxPlaySound(SoundErrorE); audioGetStats(&st); TEST_EQ(st.playing, 0); + + tCase("desktop: a wait runs the wait pump, which can stop it"); + audioDeskInit(); + dvxSetWaitPump(waitPumpCount); + sWaitPumps = 0; + sWaitGoOn = true; + dvxPlaySoundWait(SoundErrorE); + TEST_TRUE(sWaitPumps > 1); + sWaitPumps = 0; + sWaitGoOn = false; + dvxPlaySoundWait(SoundErrorE); + TEST_EQ(sWaitPumps, 1); + dvxSetWaitPump(NULL); hostClockSetYieldStep(0); audioShutdown(); dvxPlaySound(SoundErrorE); @@ -1185,7 +1370,29 @@ static void driverTests(void) { TEST_STR_EQ(sEvents[1], "beta open"); audioClose(ctx); + tCase("drivers: the last device's driver comes back first while DVX.INI's choice stands"); + ctx = audioOpen(); + audioClose(ctx); + eventsClear(); + ctx = audioOpen(); + audioGetInfo(&info); + TEST_STR_EQ(info.driver, "gamma"); + TEST_STR_EQ(sEvents[0], "gamma open"); + audioClose(ctx); + + // Another choice: detection again + setIni("driver = delta\n"); + ctx = audioOpen(); + audioClose(ctx); + writeIni("driver = beta\n"); + eventsClear(); + ctx = audioOpen(); + audioGetInfo(&info); + TEST_STR_EQ(sEvents[0], "beta open"); + audioClose(ctx); + tCase("drivers: no drivers at all"); + audioDevForget(); TEST_EQ(chdir(".."), 0); TEST_TRUE(audioOpen() == NULL); audioGetInfo(&info); @@ -1319,7 +1526,6 @@ bool fakeWtUpload(uint32_t addr, const int16_t *frames, uint32_t count, uint32_t } - static void fileTests(void) { static uint8_t wav[WAV_MAX]; uint8_t pcm8[PCM_FRAMES]; @@ -1380,14 +1586,14 @@ static void fileTests(void) { // No data chunk; an fmt chunk longer than the file; a short fmt chunk. n = makeWav(wav, 1, 1, RATE_HALF, 8, 1, pcm8, PCM_FRAMES, false); TEST_EQ(audioDecode(wav, 36, &out, &frames, &fmt), AUDIO_ERR_CORRUPT); - put32(wav + 16, 400); + dvxPutLe32(wav + 16, 400); TEST_EQ(audioDecode(wav, n, &out, &frames, &fmt), AUDIO_ERR_CORRUPT); - put32(wav + 16, 12); + dvxPutLe32(wav + 16, 12); TEST_EQ(audioDecode(wav, n, &out, &frames, &fmt), AUDIO_ERR_CORRUPT); // A data chunk claiming more than the file holds plays what is there. n = makeWav(wav, 1, 1, RATE_HALF, 8, 1, pcm8, PCM_FRAMES, false); - put32(wav + 40, 100000); + dvxPutLe32(wav + 40, 100000); TEST_EQ(audioDecode(wav, n, &out, &frames, &fmt), AUDIO_OK); TEST_EQ(frames, PCM_FRAMES); free(out); @@ -1534,6 +1740,14 @@ static uint32_t fillRamp(AudioStreamT *stream, void *buf, uint32_t frames, void } +// fillRamp's frames, FILL_SLOW_MS of the clock a call (a decoder that +// takes its time). +static uint32_t fillSlow(AudioStreamT *stream, void *buf, uint32_t frames, void *userData) { + hostClockAdvance(FILL_SLOW_MS); + return fillRamp(stream, buf, frames, userData); +} + + static void fmRawTests(void) { AudioFmInfoT info; @@ -1998,9 +2212,9 @@ static uint32_t makeVoc(uint8_t *buf, uint16_t checksumDelta, const uint8_t *blo const uint32_t version = 0x010A; memcpy(buf, "Creative Voice File\x1A", 20); - put16(buf + 20, 26); - put16(buf + 22, version); - put16(buf + 24, ((~version + 0x1234) & 0xFFFF) + checksumDelta); + dvxPutLe16(buf + 20, 26); + dvxPutLe16(buf + 22, version); + dvxPutLe16(buf + 24, ((~version + 0x1234) & 0xFFFF) + checksumDelta); memcpy(buf + 26, blocks, blockBytes); return 26 + blockBytes; } @@ -2017,32 +2231,32 @@ static uint32_t makeWav(uint8_t *buf, uint32_t tag, uint32_t channels, uint32_t if (listFirst) { memcpy(buf + pos, "LIST", 4); - put32(buf + pos + 4, 5); + dvxPutLe32(buf + pos + 4, 5); memcpy(buf + pos + 8, "INFOx\0", 6); pos += 14; } memcpy(buf + pos, "fmt ", 4); - put32(buf + pos + 4, fmtBytes); - put16(buf + pos + 8, tag); - put16(buf + pos + 10, channels); - put32(buf + pos + 12, rate); - put32(buf + pos + 16, rate * align); - put16(buf + pos + 20, align); - put16(buf + pos + 22, bits); + dvxPutLe32(buf + pos + 4, fmtBytes); + dvxPutLe16(buf + pos + 8, tag); + dvxPutLe16(buf + pos + 10, channels); + dvxPutLe32(buf + pos + 12, rate); + dvxPutLe32(buf + pos + 16, rate * align); + dvxPutLe16(buf + pos + 20, align); + dvxPutLe16(buf + pos + 22, bits); if (tag == 0xFFFE) { memset(buf + pos + 24, 0, 24); - put16(buf + pos + 24, 22); - put16(buf + pos + 32, 1); + dvxPutLe16(buf + pos + 24, 22); + dvxPutLe16(buf + pos + 32, 1); } pos += 8 + fmtBytes; memcpy(buf + pos, "data", 4); - put32(buf + pos + 4, dataBytes); + dvxPutLe32(buf + pos + 4, dataBytes); memcpy(buf + pos + 8, data, dataBytes); pos += 8 + dataBytes; - put32(buf + 4, pos - 8); + dvxPutLe32(buf + 4, pos - 8); return pos; } @@ -2352,6 +2566,49 @@ static void mixerTests(void) { audioStreamClose(stream); } + tCase("mixer: an ended stream's voice ends once it has played, with no underrun"); + { + AudioStreamT *stream = NULL; + + // The cases before ended voices of their own: their notes go first + audioPump(); + audioSetEndFn(ctx, onEnd, NULL); + sNoteCount = 0; + TEST_EQ(audioStreamOpen(ctx, &mono16, 0, NULL, NULL, &stream), AUDIO_OK); + TEST_EQ(audioStreamWrite(stream, ramp, PCM_FRAMES), PCM_FRAMES); + audioStreamEnd(stream); + audioStreamEnd(NULL); + audioGetStats(&st); + + uint32_t under = st.underruns; + + render(PCM_FRAMES * 2); + audioGetStats(&st); + TEST_EQ(st.underruns, under); + TEST_TRUE(!audioVoiceIsPlaying(ctx, audioStreamVoice(stream))); + audioPump(); + TEST_EQ(sNoteCount, 1); + TEST_EQ(sNotes[0].reason, AUDIO_END_FINISHED); + audioStreamClose(stream); + + // A fill function that ends its stream is not asked again + sFillCalls = 0; + sFillValue = 0; + TEST_EQ(audioStreamOpen(ctx, &mono16, AUDIO_STREAM_MS_MIN, fillRamp, NULL, &stream), AUDIO_OK); + audioPump(); + TEST_TRUE(sFillCalls > 0); + audioStreamEnd(stream); + + uint32_t calls = sFillCalls; + + render(RATE_DEV * AUDIO_STREAM_MS_MIN / MS_PER_SEC * 2); + audioPump(); + TEST_EQ(sFillCalls, calls); + TEST_TRUE(!audioVoiceIsPlaying(ctx, audioStreamVoice(stream))); + audioStreamClose(stream); + audioSetEndFn(ctx, NULL, NULL); + } + tCase("mixer: a pulled stream is filled by audioPump"); { AudioStreamT *stream = NULL; @@ -2372,6 +2629,27 @@ static void mixerTests(void) { audioStreamClose(stream); } + tCase("mixer: a slow pulled stream is filled a part a pump once it has enough"); + { + AudioStreamT *stream = NULL; + uint32_t calls; + + TEST_EQ(audioStreamOpen(ctx, &mono16, FILL_SLOW_BUFFER_MS, fillSlow, NULL, &stream), AUDIO_OK); + sFillCalls = 0; + audioPump(); + calls = sFillCalls; + TEST_TRUE(audioStreamSpace(stream) > 0); + TEST_TRUE(audioStreamQueued(stream) >= RATE_DEV * FILL_SAFE_MS / MS_PER_SEC); + + for (int32_t i = 0; i < FILL_SLOW_PUMPS && audioStreamSpace(stream) > 0; i++) { + audioPump(); + } + + TEST_EQ(audioStreamSpace(stream), 0); + TEST_TRUE(sFillCalls > calls); + audioStreamClose(stream); + } + tCase("mixer: a paused voice holds its place, a flushed stream drops its frames"); { AudioStreamT *stream = NULL; @@ -2670,18 +2948,6 @@ static void programTests(void) { } -static void put16(uint8_t *p, uint32_t v) { - p[0] = (uint8_t)(v & 0xFF); - p[1] = (uint8_t)((v >> 8) & 0xFF); -} - - -static void put32(uint8_t *p, uint32_t v) { - put16(p, v & 0xFFFF); - put16(p + 2, v >> 16); -} - - // Recording with the gamma fake (it records from the microphone and the // line; the test pushes the blocks the card's interrupt would): the card // changes hands and comes back, the frames arrive in order with their @@ -2982,13 +3248,10 @@ static void sbTests(void) { // lines. // Tones stay in the mixer unless extra says otherwise (the fake FM chip // is always there). +// DVX.INI as DVX starts with it: no driver modules parked from before. static void setIni(const char *extra) { - FILE *f = fopen(platformDvxIniPath(), "w"); - - if (f) { - fprintf(f, "[audio]\n%s%s", strstr(extra, "fmTones") ? "" : "fmTones = false\n", extra); - fclose(f); - } + writeIni(extra); + audioDevForget(); } @@ -3236,7 +3499,6 @@ static void toneTests(void) { } - static void voiceTests(void) { AudioCtxT *ctx; AudioCtxT *other; @@ -3319,6 +3581,27 @@ static void voiceTests(void) { } +// The wait pump of a desktop sound waited for: counted, the clock moved +// (platformYield), and sWaitGoOn the answer. +static bool waitPumpCount(void) { + sWaitPumps++; + platformYield(); + return sWaitGoOn; +} + + +// Write DVX.INI's [audio] (fmTones off unless extra sets it), as the +// settings change while DVX runs. +static void writeIni(const char *extra) { + FILE *f = fopen(platformDvxIniPath(), "w"); + + if (f) { + fprintf(f, "[audio]\n%s%s", strstr(extra, "fmTones") ? "" : "fmTones = false\n", extra); + fclose(f); + } +} + + // The Windows Sound System board's configuration register (wssCalc.c). static void wssTests(void) { uint8_t value = 0; @@ -3434,6 +3717,12 @@ static void wtTests(void) { TEST_EQ(audioWtUpload(a, addr + 998, ramp, 2, 16), AUDIO_ERR_ARG); TEST_EQ(audioWtMemFree(b, addr), AUDIO_ERR_ARG); TEST_EQ(audioWtMemFree(a, addr), AUDIO_OK); + + // Free in two gaps (before and after b's block): in all more than the + // largest, all but b's block + audioWtGetInfo(&info); + TEST_TRUE(info.freeTotal > info.freeMemory); + TEST_TRUE(info.freeTotal >= WT_FAKE_MEMORY - 1024 && info.freeTotal <= WT_FAKE_MEMORY - 1000); TEST_EQ(audioWtMemAlloc(a, 500, 16, &addr), AUDIO_OK); TEST_EQ(addr, 0); @@ -3493,6 +3782,7 @@ int main(void) { basicSampleTests(); basicRecordTests(); basicStreamTests(); + basicGapTests(); fmTests(); midiTests(); midiInTests(); diff --git a/src/test/basdev/testBasDev.c b/src/test/basdev/testBasDev.c index 25058d88..7ae830f2 100644 --- a/src/test/basdev/testBasDev.c +++ b/src/test/basdev/testBasDev.c @@ -211,6 +211,7 @@ static bool includeConstIs(const char *text, const char *name, const char static const char *includeConstValue(const char *text, const char *name); static int32_t keywordIndex(const char *word); static BasModuleT *loadProject(const char *dbpPath, BasProjectSourceT *ps, BasValidatorT *validator); +static void pumpCount(void *ctx); static bool resHas(DvxResHandleT *res, const char *name, const char *want); static void textPrint(void *ctx, const char *text, bool newline); static void textRow(void *ctx, const char *const *cells, int32_t cellCount); @@ -394,12 +395,12 @@ static void caseCDebugger(void) { // Watch: a variable path is read directly, an expression is evaluated char value[BAS_DBG_VALUE_LEN]; - TEST_TRUE(basDbgEval(vm, "n", value, sizeof(value))); + TEST_TRUE(basDbgEval(vm, "n", value, sizeof(value), NULL)); TEST_STR_EQ(value, "4"); - TEST_TRUE(basDbgEval(vm, "total + n * 10", value, sizeof(value))); + TEST_TRUE(basDbgEval(vm, "total + n * 10", value, sizeof(value), NULL)); TEST_EQ(atoi(value), 40); - TEST_TRUE(!basDbgEval(vm, "nosuch(", value, sizeof(value))); - TEST_TRUE(!basDbgEval(NULL, "n", value, sizeof(value))); + TEST_TRUE(!basDbgEval(vm, "nosuch(", value, sizeof(value), NULL)); + TEST_TRUE(!basDbgEval(NULL, "n", value, sizeof(value), NULL)); // [break] on the program's form captions while paused basRunHostTagBreak(rt, &titles, true); @@ -410,23 +411,40 @@ static void caseCDebugger(void) { // Immediate: assignment, statement, expression, error; and SetVar memset(&out, 0, sizeof(out)); - TEST_TRUE(basDbgExec(vm, "total = 3", textPrint, &out)); + TEST_TRUE(basDbgExec(vm, "total = 3", textPrint, &out, NULL)); TEST_STR_EQ(out.text, "total = 3\n"); memset(&out, 0, sizeof(out)); - TEST_TRUE(!basDbgExec(vm, "PRINT total * 2", textPrint, &out)); + TEST_TRUE(!basDbgExec(vm, "PRINT total * 2", textPrint, &out, NULL)); TEST_EQ(atoi(out.text), 6); memset(&out, 0, sizeof(out)); - TEST_TRUE(!basDbgExec(NULL, "6 * 7", textPrint, &out)); + TEST_TRUE(!basDbgExec(NULL, "6 * 7", textPrint, &out, NULL)); TEST_EQ(atoi(out.text), 42); memset(&out, 0, sizeof(out)); - basDbgExec(vm, "label = ", textPrint, &out); + basDbgExec(vm, "label = ", textPrint, &out, NULL); TEST_TRUE(strncmp(out.text, "Error: ", strlen("Error: ")) == 0); memset(&out, 0, sizeof(out)); - TEST_TRUE(basDbgSetVar(vm, "total", "total + 4", textPrint, &out)); + TEST_TRUE(basDbgSetVar(vm, "total", "total + 4", textPrint, &out, NULL)); TEST_STR_EQ(out.text, "total = 7\n"); memset(&out, 0, sizeof(out)); - TEST_TRUE(!basDbgSetVar(vm, "nosuch", "1", textPrint, &out)); - TEST_TRUE(!basDbgSetVar(vm, "label", "1 +", textPrint, &out)); + TEST_TRUE(!basDbgSetVar(vm, "nosuch", "1", textPrint, &out, NULL)); + TEST_TRUE(!basDbgSetVar(vm, "label", "1 +", textPrint, &out, NULL)); + + // A snippet's pump runs between its slices: not for a short one, for + // a long one, and for an endless one until it is abandoned + int32_t pumps = 0; + BasDbgPumpT pump = { pumpCount, &pumps }; + + TEST_TRUE(basDbgEval(vm, "n", value, sizeof(value), &pump)); + TEST_EQ(pumps, 0); + memset(&out, 0, sizeof(out)); + TEST_TRUE(!basDbgExec(vm, "DIM m AS LONG: FOR k = 1 TO 20000: m = m + 1: NEXT: PRINT m", textPrint, &out, &pump)); + TEST_EQ(atoi(out.text), 20000); + TEST_TRUE(pumps > 0); + pumps = 0; + memset(&out, 0, sizeof(out)); + basDbgExec(vm, "DIM m AS LONG: DO: LOOP", textPrint, &out, &pump); + TEST_TRUE(strstr(out.text, "did not finish") != NULL); + TEST_TRUE(pumps > 1); // Step over: the breakpoint line ran once (7 + 1) basVmStepOver(vm); @@ -437,7 +455,7 @@ static void caseCDebugger(void) { } while (result == BAS_VM_STEP_LIMIT); TEST_EQ(result, BAS_VM_BREAKPOINT); - TEST_TRUE(basDbgEval(vm, "total", value, sizeof(value))); + TEST_TRUE(basDbgEval(vm, "total", value, sizeof(value), NULL)); TEST_STR_EQ(value, "8"); // Run free to the end: 8 + 2 + 3 + 4 @@ -554,7 +572,7 @@ static void caseDesignControls(void) { // The calling program: a runtime made current BasCompileErrorT err; - BasModuleT *hostMod = basBuildCompile(host, (int32_t)strlen(host), false, NULL, NULL, &err); + BasModuleT *hostMod = basBuildCompile(host, (int32_t)strlen(host), false, NULL, NULL, &err, NULL, NULL); const char *hostError = NULL; BasRunSpecT spec; @@ -708,6 +726,7 @@ static void caseIncludeConsts(void) { { "fileops.bas", "FILEOPS_COPY", FileOpsCopyE }, { "fileops.bas", "FILEOPS_MOVE", FileOpsMoveE }, { "fileops.bas", "FILEOPS_DELETE", FileOpsDeleteE }, + { "fileops.bas", "FILEOPS_MEASURE", FileOpsMeasureE }, { "fileops.bas", "FILEOPS_STEP_BUSY", FileOpsStepBusyE }, { "fileops.bas", "FILEOPS_STEP_ASK", FileOpsStepAskE }, { "fileops.bas", "FILEOPS_STEP_ERROR", FileOpsStepErrorE }, @@ -747,6 +766,13 @@ static void caseIncludeConsts(void) { { "sound.bas", "RECORD_MIC", AUDIO_REC_MIC }, { "sound.bas", "RECORD_LINE", AUDIO_REC_LINE }, { "sound.bas", "RECORD_CD", AUDIO_REC_CD }, + { "sound.bas", "SOUND_END_FINISHED", AUDIO_END_FINISHED }, + { "sound.bas", "SOUND_END_STOPPED", AUDIO_END_STOPPED }, + { "sound.bas", "SOUND_END_DROPPED", AUDIO_END_DROPPED }, + { "sound.bas", "SOUND_WAVE_SQUARE", AUDIO_WAVE_SQUARE }, + { "sound.bas", "SOUND_WAVE_TRIANGLE", AUDIO_WAVE_TRIANGLE }, + { "sound.bas", "SOUND_WAVE_NOISE", AUDIO_WAVE_NOISE }, + { "sound.bas", "WT_ADDR_ROM", (int32_t)AUDIO_WT_ADDR_ROM }, { "music.bas", "MIDI_NONE", AUDIO_MIDI_PACKED_NONE }, { "sound.bas", "FM_VOICES_MAX", AUDIO_FM_VOICES_MAX }, { "sound.bas", "FM_CHIP_HZ", AUDIO_FM_CHIP_HZ }, @@ -774,6 +800,11 @@ static void caseIncludeConsts(void) { { "sound.bas", "FM_ADDITIVE", AUDIO_FM_ADDITIVE }, { "sound.bas", "FM_BANK_REGS", AUDIO_FM_BANK_REGS }, { "joystick.bas", "JOY_CAL_MIN", JOY_CAL_MIN }, + { "joystick.bas", "JOY_EVENT_NONE", JOY_BAS_EVENT_NONE }, + { "joystick.bas", "JOY_EVENT_AXIS", JoyEventAxisE + JOY_BAS_EVENT_FIRST }, + { "joystick.bas", "JOY_EVENT_BUTTON_DOWN", JoyEventButtonDownE + JOY_BAS_EVENT_FIRST }, + { "joystick.bas", "JOY_EVENT_BUTTON_UP", JoyEventButtonUpE + JOY_BAS_EVENT_FIRST }, + { "joystick.bas", "JOY_EVENT_HAT", JoyEventHatE + JOY_BAS_EVENT_FIRST }, { "joystick.bas", "JOY_CAL_CENTER", JOY_CAL_CENTER }, { "joystick.bas", "JOY_CAL_MAX", JOY_CAL_MAX }, { "joystick.bas", "JOY_DEADZONE_DEFAULT", JOY_DEADZONE_DEFAULT }, @@ -1418,7 +1449,7 @@ static BasModuleT *loadProject(const char *dbpPath, BasProjectSourceT *ps, BasVa basValidateScanFrm(validator, ps->frmTexts[i], (int32_t)strlen(ps->frmTexts[i]), NULL, NULL); } - mod = basBuildCompile(ps->source, ps->sourceLen, ps->optionExplicit, validator ? &cb : NULL, validator ? &validator->compiler : NULL, &err); + mod = basBuildCompile(ps->source, ps->sourceLen, ps->optionExplicit, validator ? &cb : NULL, validator ? &validator->compiler : NULL, &err, NULL, NULL); if (!mod) { printf(" compile: %s\n", err.message); @@ -1428,6 +1459,12 @@ static BasModuleT *loadProject(const char *dbpPath, BasProjectSourceT *ps, BasVa } +// A snippet pump that counts its calls. +static void pumpCount(void *ctx) { + (*(int32_t *)ctx)++; +} + + // True when res has resource name and, unless want is NULL, it is the // text want (with its NUL, as the build writes text). static bool resHas(DvxResHandleT *res, const char *name, const char *want) { diff --git a/src/test/basic/Makefile b/src/test/basic/Makefile index 8b8d7a85..61d5a4dd 100644 --- a/src/test/basic/Makefile +++ b/src/test/basic/Makefile @@ -90,7 +90,7 @@ FUZZ_MEM_CAP = ulimit -v $$(($(FUZZ_MEM_MB) * 1024)) endif COMPILER_SRCS = $(DVXBASIC)/compiler/lexer.c $(DVXBASIC)/compiler/ast.c $(DVXBASIC)/compiler/scope.c $(DVXBASIC)/compiler/parse.c $(DVXBASIC)/compiler/parseExpr.c $(DVXBASIC)/compiler/parseStmt.c $(DVXBASIC)/compiler/parseDecl.c $(DVXBASIC)/compiler/resolve.c $(DVXBASIC)/compiler/resolveExpr.c $(DVXBASIC)/compiler/typecheck.c $(DVXBASIC)/compiler/fold.c $(DVXBASIC)/compiler/gen.c $(DVXBASIC)/compiler/genExpr.c $(DVXBASIC)/compiler/genStmt.c $(DVXBASIC)/compiler/compiler.c $(DVXBASIC)/compiler/codegen.c -RUNTIME_SRCS = $(DVXBASIC)/runtime/vm.c $(DVXBASIC)/runtime/values.c $(DVXBASIC)/runtime/dates.c $(DVXBASIC)/runtime/serialize.c $(DVXBASIC)/runtime/gfx.c $(DVXBASIC)/runtime/screen.c $(LIBDVX)/dvxPrefs.c +RUNTIME_SRCS = $(DVXBASIC)/runtime/vm.c $(DVXBASIC)/runtime/values.c $(DVXBASIC)/runtime/dates.c $(DVXBASIC)/runtime/serialize.c $(DVXBASIC)/runtime/gfx.c $(DVXBASIC)/runtime/screen.c $(LIBDVX)/dvxPrefs.c $(LIBDVX)/dvxFontFmt.c SUPPORT_SRCS = $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/thirdparty/stb_ds_impl.c FUZZ_COMPILE_SRCS = fuzzCompile.c fuzzCommon.c $(COMPILER_SRCS) $(RUNTIME_SRCS) $(SUPPORT_SRCS) @@ -170,7 +170,7 @@ BENCH_OLD = bench/baseline.txt BENCH_NEW = $(BENCH_RESULTS) BENCH_CFLAGS = $(HOST_TOOL_CFLAGS) -Wno-stringop-truncation -D_GNU_SOURCE -DBAS_BENCH $(INCLUDES) BENCH_LDFLAGS = -Wl,--wrap=malloc,--wrap=calloc,--wrap=realloc,--wrap=free,--wrap=strdup -BENCH_SRCS = basBench.c $(COMPILER_SRCS) $(DVXBASIC)/compiler/strip.c $(DVXBASIC)/compiler/obfuscate.c $(DVXBASIC)/compiler/compact.c $(DVXBASIC)/basBuild.c $(RUNTIME_SRCS) $(LIBDVX)/dvxResource.c $(LIBDVX)/dvxFontFmt.c ../../tools/dvxResWrite.c $(SUPPORT_SRCS) +BENCH_SRCS = basBench.c $(COMPILER_SRCS) $(DVXBASIC)/compiler/strip.c $(DVXBASIC)/compiler/obfuscate.c $(DVXBASIC)/compiler/compact.c $(DVXBASIC)/basBuild.c $(RUNTIME_SRCS) $(LIBDVX)/dvxResource.c ../../tools/dvxResWrite.c $(SUPPORT_SRCS) BENCH_OBJS = $(patsubst %.c,$(BENCH_OBJDIR)/%.o,$(notdir $(BENCH_SRCS))) BENCH_PROGS = $(sort $(wildcard bench/*.bas)) BENCH_SAMPLES = $(sort $(shell find $(DVXBASIC)/.. -name "*.dbp")) diff --git a/src/test/basic/basBench.c b/src/test/basic/basBench.c index 0979ba80..cd8c6b6d 100644 --- a/src/test/basic/basBench.c +++ b/src/test/basic/basBench.c @@ -72,6 +72,7 @@ #include "runtime/screen.h" #include "basBuild.h" #include "dvxPrefs.h" +#include "dvxHash.h" #include "dvxPlat.h" #include "dvxTypes.h" @@ -108,8 +109,6 @@ #define BENCH_NS_PER_MS 1000000.0 #define BENCH_NS_PER_SEC 1000000000ULL #define BENCH_PERCENT 100.0 -#define BENCH_FNV_OFFSET 2166136261u -#define BENCH_FNV_PRIME 16777619u #define BENCH_TEXT_GROW 65536 // generated source buffer growth step #define BENCH_DIR_MODE 0755 #define BENCH_SAMPLES_ROW "compile.samples" @@ -198,7 +197,6 @@ static BasModuleT *compileMeasured(const char *source, int32_t len, bool optionE static void finishRow(BenchRowT *row, const BenchSampleT *samples, double *ms, int32_t count); static char *generateLarge(int32_t procs, int32_t *outLen); static char *generateMany(int32_t procs, int32_t *outLen); -static uint32_t hashBytes(uint32_t hash, const uint8_t *data, int32_t len); static bool hostDoEvents(void *ctx); static bool hostInput(void *ctx, const char *prompt, char *buf, int32_t bufSize); static void hostPrint(void *ctx, const char *text, bool newline); @@ -340,10 +338,10 @@ static void captureScreen(const BasVmT *vm) { return; } - uint32_t hash = hashBytes(BENCH_FNV_OFFSET, scr->fb, scr->width * scr->height); + uint32_t hash = dvxFnvBytes(DVX_FNV_BASIS, scr->fb, scr->width * scr->height); if (scr->chars) { - hash = hashBytes(hash, scr->chars, scr->rows * scr->cols); + hash = dvxFnvBytes(hash, scr->chars, scr->rows * scr->cols); } char line[BENCH_LINE_LEN]; @@ -599,17 +597,6 @@ static char *generateMany(int32_t procs, int32_t *outLen) { } -// FNV-1a -static uint32_t hashBytes(uint32_t hash, const uint8_t *data, int32_t len) { - for (int32_t i = 0; i < len; i++) { - hash ^= data[i]; - hash *= BENCH_FNV_PRIME; - } - - return hash; -} - - static bool hostDoEvents(void *ctx) { (void)ctx; return true; diff --git a/src/test/basic/bench/arithFloat.bas b/src/test/basic/bench/arithFloat.bas index 204d8d99..cf8ae6d1 100644 --- a/src/test/basic/bench/arithFloat.bas +++ b/src/test/basic/bench/arithFloat.bas @@ -1,3 +1,26 @@ +' 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. + + ' arithFloat -- SINGLE and DOUBLE arithmetic and math functions in a loop. ' Prints a rounded checksum so the line does not depend on the last bit. DIM i AS LONG diff --git a/src/test/basic/bench/arithInt.bas b/src/test/basic/bench/arithInt.bas index 6c07cf96..02cda657 100644 --- a/src/test/basic/bench/arithInt.bas +++ b/src/test/basic/bench/arithInt.bas @@ -1,3 +1,26 @@ +' 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. + + ' arithInt -- integer and long arithmetic in tight loops: + - * \ MOD, ' comparisons and a FOR loop with an IF. Prints a checksum. DIM i AS LONG diff --git a/src/test/basic/bench/array1d.bas b/src/test/basic/bench/array1d.bas index b43f03b1..af3a118b 100644 --- a/src/test/basic/bench/array1d.bas +++ b/src/test/basic/bench/array1d.bas @@ -1,3 +1,26 @@ +' 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. + + ' array1d -- one-dimensional array access: a sieve of Eratosthenes run ' several times over an INTEGER array, plus a LONG array prefix sum. CONST LIMIT = 20000 diff --git a/src/test/basic/bench/array2d.bas b/src/test/basic/bench/array2d.bas index 50afe0e7..a052853c 100644 --- a/src/test/basic/bench/array2d.bas +++ b/src/test/basic/bench/array2d.bas @@ -1,3 +1,26 @@ +' 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. + + ' array2d -- two-dimensional array access: integer matrix multiply. CONST N = 40 DIM a(N, N) AS LONG diff --git a/src/test/basic/bench/arrayType.bas b/src/test/basic/bench/arrayType.bas index 5538be51..b3451d73 100644 --- a/src/test/basic/bench/arrayType.bas +++ b/src/test/basic/bench/arrayType.bas @@ -1,3 +1,26 @@ +' 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. + + ' arrayType -- arrays of a TYPE: field reads and writes on elements ' (particle update loop) and a nested TYPE field. TYPE VecT diff --git a/src/test/basic/bench/callByRef.bas b/src/test/basic/bench/callByRef.bas index f37258e2..7691a1ec 100644 --- a/src/test/basic/bench/callByRef.bas +++ b/src/test/basic/bench/callByRef.bas @@ -1,3 +1,26 @@ +' 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. + + ' callByRef -- ByRef arguments: a plain variable, an array element, a ' TYPE field, a TYPE field of an array element and a string. Until ' redesign Phase 1 a TYPE field reached the SUB by value (c.hits and diff --git a/src/test/basic/bench/callByVal.bas b/src/test/basic/bench/callByVal.bas index faf2e3ed..b656f0f3 100644 --- a/src/test/basic/bench/callByVal.bas +++ b/src/test/basic/bench/callByVal.bas @@ -1,3 +1,26 @@ +' 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. + + ' callByVal -- SUB and FUNCTION calls with ByVal arguments (numbers and ' a string). FUNCTION AddMul(BYVAL a AS LONG, BYVAL b AS LONG) AS LONG diff --git a/src/test/basic/bench/callOptional.bas b/src/test/basic/bench/callOptional.bas index d736c9b6..f1b1b558 100644 --- a/src/test/basic/bench/callOptional.bas +++ b/src/test/basic/bench/callOptional.bas @@ -1,3 +1,26 @@ +' 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. + + ' callOptional -- calls with Optional parameters given and omitted, and ' ParamArray calls with several arguments. FUNCTION Scale(BYVAL v AS LONG, OPTIONAL BYVAL mul AS LONG, OPTIONAL BYVAL add AS LONG) AS LONG diff --git a/src/test/basic/bench/classCreate.bas b/src/test/basic/bench/classCreate.bas index 22bcb825..152af857 100644 --- a/src/test/basic/bench/classCreate.bas +++ b/src/test/basic/bench/classCreate.bas @@ -1,3 +1,26 @@ +' 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. + + ' classCreate -- object creation and release: NEW, Set, Nothing, ' Class_Initialize and Class_Terminate on every object. DIM SHARED made AS LONG diff --git a/src/test/basic/bench/classMethods.bas b/src/test/basic/bench/classMethods.bas index 4cb6ab2e..46383073 100644 --- a/src/test/basic/bench/classMethods.bas +++ b/src/test/basic/bench/classMethods.bas @@ -1,3 +1,26 @@ +' 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. + + ' classMethods -- method calls, Property Get/Let and public member ' variable access on a class instance (declared As the class and As ' Object). diff --git a/src/test/basic/bench/collection.bas b/src/test/basic/bench/collection.bas index 6fdfe377..750f3e1c 100644 --- a/src/test/basic/bench/collection.bas +++ b/src/test/basic/bench/collection.bas @@ -1,3 +1,26 @@ +' 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. + + ' collection -- Collection Add (with and without keys), Item by index and ' by key, Count, Remove and For Each. DIM col AS Collection diff --git a/src/test/basic/bench/fileIo.bas b/src/test/basic/bench/fileIo.bas index 4dba339b..70bf5578 100644 --- a/src/test/basic/bench/fileIo.bas +++ b/src/test/basic/bench/fileIo.bas @@ -1,3 +1,26 @@ +' 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. + + ' fileIo -- sequential and binary file I/O in App.User: PRINT # / LINE ' INPUT #, WRITE # / INPUT #, PUT / GET of numbers and strings. The ' files are deleted at the end. diff --git a/src/test/basic/bench/gfxPrims.bas b/src/test/basic/bench/gfxPrims.bas index 2830a8a4..b1b83a8e 100644 --- a/src/test/basic/bench/gfxPrims.bas +++ b/src/test/basic/bench/gfxPrims.bas @@ -1,3 +1,26 @@ +' 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. + + ' gfxPrims -- graphics primitives into the SCREEN 13 framebuffer: PSET, ' LINE (plain, B, BF), CIRCLE, PAINT and POINT. The harness checks a CRC ' of the framebuffer. diff --git a/src/test/basic/bench/objectGraph.bas b/src/test/basic/bench/objectGraph.bas index e3e5884a..d0c9da5c 100644 --- a/src/test/basic/bench/objectGraph.bas +++ b/src/test/basic/bench/objectGraph.bas @@ -1,3 +1,26 @@ +' 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. + + ' objectGraph -- VM memory for a large object graph: a tree of class ' instances, each holding a Collection of children and a string, built, ' walked and released. diff --git a/src/test/basic/bench/recursion.bas b/src/test/basic/bench/recursion.bas index 7b79491f..32c69c6b 100644 --- a/src/test/basic/bench/recursion.bas +++ b/src/test/basic/bench/recursion.bas @@ -1,3 +1,26 @@ +' 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. + + ' recursion -- wide and deep recursion: Fibonacci and Ackermann (kept ' within the VM's call depth limit). FUNCTION Fib(BYVAL n AS LONG) AS LONG diff --git a/src/test/basic/bench/strBuild.bas b/src/test/basic/bench/strBuild.bas index 1752adff..b93f23b0 100644 --- a/src/test/basic/bench/strBuild.bas +++ b/src/test/basic/bench/strBuild.bas @@ -1,3 +1,26 @@ +' 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. + + ' strBuild -- building strings by repeated concatenation (+ and &) and ' MID$ / LEFT$ over the result. DIM i AS LONG diff --git a/src/test/basic/bench/strFuncs.bas b/src/test/basic/bench/strFuncs.bas index 66652096..d865272e 100644 --- a/src/test/basic/bench/strFuncs.bas +++ b/src/test/basic/bench/strFuncs.bas @@ -1,3 +1,26 @@ +' 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. + + ' strFuncs -- the common string functions: INSTR, MID$, LEFT$, RIGHT$, ' UCASE$, LCASE$, LTRIM$/RTRIM$, STR$, VAL, Replace$ and string compares. DIM i AS LONG diff --git a/src/test/basic/bench/variantArith.bas b/src/test/basic/bench/variantArith.bas index 0ebc8836..b512eb6c 100644 --- a/src/test/basic/bench/variantArith.bas +++ b/src/test/basic/bench/variantArith.bas @@ -1,3 +1,26 @@ +' 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. + + ' variantArith -- arithmetic, concatenation and comparisons on Variants ' holding integers, doubles and strings. DIM a AS Variant diff --git a/src/test/basic/fixtures/arraysMulti.bas b/src/test/basic/fixtures/arraysMulti.bas index efed4bf8..f5a45a82 100644 --- a/src/test/basic/fixtures/arraysMulti.bas +++ b/src/test/basic/fixtures/arraysMulti.bas @@ -1,3 +1,26 @@ +' 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. + + Dim m(2, 3) As Integer Dim i As Integer Dim j As Integer diff --git a/src/test/basic/fixtures/cintRounding.bas b/src/test/basic/fixtures/cintRounding.bas index 9431af85..0d6f81de 100644 --- a/src/test/basic/fixtures/cintRounding.bas +++ b/src/test/basic/fixtures/cintRounding.bas @@ -1,3 +1,26 @@ +' 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. + + Print CInt(2.5); CInt(3.5); CInt(-2.5); CInt(-3.5); CInt(0.5); CInt(1.5) Print CInt(2.4999); CInt(2.5001); CInt(-0.5) Print CLng(2.5); CLng(3.5); CLng(2147483646.5) diff --git a/src/test/basic/fixtures/classObjects.bas b/src/test/basic/fixtures/classObjects.bas index 16a984d6..40a5b786 100644 --- a/src/test/basic/fixtures/classObjects.bas +++ b/src/test/basic/fixtures/classObjects.bas @@ -1,3 +1,26 @@ +' 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. + + ' Class modules, Collection and FOR EACH: a plain run and the stripped, ' compacted run must agree (class set-up code, OP_CALL_ME, Terminate). DIM inv AS NEW Inventory diff --git a/src/test/basic/fixtures/commandArgs.bas b/src/test/basic/fixtures/commandArgs.bas index 1a1bcaac..896bd279 100644 --- a/src/test/basic/fixtures/commandArgs.bas +++ b/src/test/basic/fixtures/commandArgs.bas @@ -1,3 +1,26 @@ +' 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. + + ' COMMAND$ returns the launch arguments (basrun --args). PRINT "["; COMMAND$; "]" PRINT "["; COMMAND$(1); "]" diff --git a/src/test/basic/fixtures/compareText.bas b/src/test/basic/fixtures/compareText.bas index 88f9d05d..b56bbc45 100644 --- a/src/test/basic/fixtures/compareText.bas +++ b/src/test/basic/fixtures/compareText.bas @@ -1,3 +1,26 @@ +' 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. + + Option Compare Text Print "abc" = "ABC"; "a" < "B"; Instr("Hello", "L") Select Case "HELLO" diff --git a/src/test/basic/fixtures/constsAndLet.bas b/src/test/basic/fixtures/constsAndLet.bas index 10db2ae7..4c44bc78 100644 --- a/src/test/basic/fixtures/constsAndLet.bas +++ b/src/test/basic/fixtures/constsAndLet.bas @@ -1,3 +1,26 @@ +' 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. + + Const MAXV = 100 Const PI = 3.14159 Const APPNAME = "DVX" diff --git a/src/test/basic/fixtures/dataRead.bas b/src/test/basic/fixtures/dataRead.bas index b98651a0..6390dda8 100644 --- a/src/test/basic/fixtures/dataRead.bas +++ b/src/test/basic/fixtures/dataRead.bas @@ -1,3 +1,26 @@ +' 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. + + Dim a As Integer Dim b As Double Dim s As String diff --git a/src/test/basic/fixtures/defTypes.bas b/src/test/basic/fixtures/defTypes.bas index 806a0348..79b5141e 100644 --- a/src/test/basic/fixtures/defTypes.bas +++ b/src/test/basic/fixtures/defTypes.bas @@ -1,3 +1,26 @@ +' 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. + + DefInt I-K DefLng L DefSng S diff --git a/src/test/basic/fixtures/divByZero.bas b/src/test/basic/fixtures/divByZero.bas index 23c039a3..61bcab1f 100644 --- a/src/test/basic/fixtures/divByZero.bas +++ b/src/test/basic/fixtures/divByZero.bas @@ -1,3 +1,26 @@ +' 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. + + Print "before" Print 1 \ 0 Print "after" diff --git a/src/test/basic/fixtures/erlLine.bas b/src/test/basic/fixtures/erlLine.bas index 53b7ee56..df4bd7dc 100644 --- a/src/test/basic/fixtures/erlLine.bas +++ b/src/test/basic/fixtures/erlLine.bas @@ -1,3 +1,26 @@ +' 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. + + ' ERL reports the line of the current error and resets after RESUME. On Error GoTo Handler Dim v As Integer diff --git a/src/test/basic/fixtures/erlLine.out b/src/test/basic/fixtures/erlLine.out index 749ca9fd..2f531be0 100644 --- a/src/test/basic/fixtures/erlLine.out +++ b/src/test/basic/fixtures/erlLine.out @@ -1,4 +1,4 @@ -11 4 +11 27 after0 -7 6 +7 29 end0 diff --git a/src/test/basic/fixtures/eventsInterfaces.bas b/src/test/basic/fixtures/eventsInterfaces.bas index 557d6da3..3aeb61e7 100644 --- a/src/test/basic/fixtures/eventsInterfaces.bas +++ b/src/test/basic/fixtures/eventsInterfaces.bas @@ -1,3 +1,26 @@ +' 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. + + ' Events (WithEvents / RaiseEvent), Implements, Optional Variant ' arguments, ParamArray by reference and cycle collection DIM WithEvents w AS Worker diff --git a/src/test/basic/fixtures/fileBinary.bas b/src/test/basic/fixtures/fileBinary.bas index 7e04aaa8..c8abdd4f 100644 --- a/src/test/basic/fixtures/fileBinary.bas +++ b/src/test/basic/fixtures/fileBinary.bas @@ -1,3 +1,26 @@ +' 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. + + Dim f As String Dim buf As String Dim i As Integer diff --git a/src/test/basic/fixtures/fileInputMulti.bas b/src/test/basic/fixtures/fileInputMulti.bas index 1236abc4..80757ae8 100644 --- a/src/test/basic/fixtures/fileInputMulti.bas +++ b/src/test/basic/fixtures/fileInputMulti.bas @@ -1,3 +1,26 @@ +' 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. + + Dim f As String Dim a As String Dim b As Integer diff --git a/src/test/basic/fixtures/fileRandom.bas b/src/test/basic/fixtures/fileRandom.bas index c71d9d98..5e709cff 100644 --- a/src/test/basic/fixtures/fileRandom.bas +++ b/src/test/basic/fixtures/fileRandom.bas @@ -1,3 +1,26 @@ +' 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. + + Dim i As Integer Dim l As Long Dim d As Double diff --git a/src/test/basic/fixtures/fileRandomUdt.bas b/src/test/basic/fixtures/fileRandomUdt.bas index bb0a0b97..fb8bba43 100644 --- a/src/test/basic/fixtures/fileRandomUdt.bas +++ b/src/test/basic/fixtures/fileRandomUdt.bas @@ -1,3 +1,26 @@ +' 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. + + ' GET/PUT with TYPE variables: packed fields, fixed strings padded, ' variable strings length-prefixed, RANDOM records padded to LEN. Type PointT diff --git a/src/test/basic/fixtures/fsDirOps.bas b/src/test/basic/fixtures/fsDirOps.bas index 85bc22f7..e1efe521 100644 --- a/src/test/basic/fixtures/fsDirOps.bas +++ b/src/test/basic/fixtures/fsDirOps.bas @@ -1,3 +1,26 @@ +' 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. + + ' CHDIR / CURDIR$ / MKDIR / RMDIR / DIR$ / KILL / NAME / FILECOPY / FILELEN / GETATTR / SETATTR Dim root As String Dim sub1 As String diff --git a/src/test/basic/fixtures/fsErrors.bas b/src/test/basic/fixtures/fsErrors.bas index ef9bf3c0..c08e746c 100644 --- a/src/test/basic/fixtures/fsErrors.bas +++ b/src/test/basic/fixtures/fsErrors.bas @@ -1,3 +1,26 @@ +' 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. + + ' File system error numbers On Error GoTo Handler Dim root As String diff --git a/src/test/basic/fixtures/gosub.bas b/src/test/basic/fixtures/gosub.bas index e15e9297..dda5b45f 100644 --- a/src/test/basic/fixtures/gosub.bas +++ b/src/test/basic/fixtures/gosub.bas @@ -1,3 +1,26 @@ +' 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. + + Dim total As Integer Dim i As Integer GoSub Init diff --git a/src/test/basic/fixtures/gosubLocal.bas b/src/test/basic/fixtures/gosubLocal.bas index 10077bbe..e41021d3 100644 --- a/src/test/basic/fixtures/gosubLocal.bas +++ b/src/test/basic/fixtures/gosubLocal.bas @@ -1,3 +1,26 @@ +' 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. + + ' GOSUB/RETURN inside a SUB or FUNCTION uses that procedure's labels and ' its own return stack. Dim r As Integer diff --git a/src/test/basic/fixtures/hexOct.bas b/src/test/basic/fixtures/hexOct.bas index fa3a4d28..2b89192e 100644 --- a/src/test/basic/fixtures/hexOct.bas +++ b/src/test/basic/fixtures/hexOct.bas @@ -1,3 +1,26 @@ +' 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. + + Print Hex$(255); Hex$(0); Hex$(4096); Hex$(65535); Hex$(-1) Print Oct$(8); Oct$(0); Oct$(64); Oct$(511) Print Hex$(65536); Hex$(2147483647) diff --git a/src/test/basic/fixtures/iniReadWrite.bas b/src/test/basic/fixtures/iniReadWrite.bas index 44480701..d0a29a97 100644 --- a/src/test/basic/fixtures/iniReadWrite.bas +++ b/src/test/basic/fixtures/iniReadWrite.bas @@ -1,3 +1,26 @@ +' 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. + + Dim ini As String ini = App.User & "/test.ini" Print "[" & IniRead(ini, "S", "K", "def") & "]" diff --git a/src/test/basic/fixtures/inputStdin.bas b/src/test/basic/fixtures/inputStdin.bas index 747a471e..d07d3e7e 100644 --- a/src/test/basic/fixtures/inputStdin.bas +++ b/src/test/basic/fixtures/inputStdin.bas @@ -1,3 +1,26 @@ +' 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. + + Dim who As String Dim age As Integer Dim x As Double diff --git a/src/test/basic/fixtures/intOverflow.bas b/src/test/basic/fixtures/intOverflow.bas index 5bed4f1d..18c4e702 100644 --- a/src/test/basic/fixtures/intOverflow.bas +++ b/src/test/basic/fixtures/intOverflow.bas @@ -1,3 +1,26 @@ +' 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. + + Dim i As Integer Dim j As Integer i = 30000 diff --git a/src/test/basic/fixtures/lineContinuation.bas b/src/test/basic/fixtures/lineContinuation.bas index 6d8d7b03..fac2f499 100644 --- a/src/test/basic/fixtures/lineContinuation.bas +++ b/src/test/basic/fixtures/lineContinuation.bas @@ -1,3 +1,26 @@ +' 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. + + Dim total As Integer total = 1 + _ 2 + _ diff --git a/src/test/basic/fixtures/loopsMisc.bas b/src/test/basic/fixtures/loopsMisc.bas index 77314e84..c06dd8c8 100644 --- a/src/test/basic/fixtures/loopsMisc.bas +++ b/src/test/basic/fixtures/loopsMisc.bas @@ -1,3 +1,26 @@ +' 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. + + Dim i As Integer Dim n As Integer For i = 10 To 1 Step -3 diff --git a/src/test/basic/fixtures/mathFuncs.bas b/src/test/basic/fixtures/mathFuncs.bas index 4625b3ea..0690079c 100644 --- a/src/test/basic/fixtures/mathFuncs.bas +++ b/src/test/basic/fixtures/mathFuncs.bas @@ -1,3 +1,26 @@ +' 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. + + Print Abs(-3); Sgn(-2); Sgn(0); Sgn(9); Sqr(16); Int(Exp(0)) Print Int(Sin(0)); Int(Cos(0)); Int(Log(1)); Int(Atn(0)); Int(Tan(0)) Print 7 Mod 3; -7 Mod 3; 7 \ 2; -7 \ 2; 2 ^ 10; 2 ^ 0.5 diff --git a/src/test/basic/fixtures/noFormRuntime.bas b/src/test/basic/fixtures/noFormRuntime.bas index 177b4926..dec94619 100644 --- a/src/test/basic/fixtures/noFormRuntime.bas +++ b/src/test/basic/fixtures/noFormRuntime.bas @@ -1,3 +1,26 @@ +' 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. + + ' Form and control statements need the form runtime; without one they ' raise error 1000 (trappable) instead of silently doing nothing. On Error GoTo Handler diff --git a/src/test/basic/fixtures/onError.bas b/src/test/basic/fixtures/onError.bas index 5c502814..cdc05d77 100644 --- a/src/test/basic/fixtures/onError.bas +++ b/src/test/basic/fixtures/onError.bas @@ -1,3 +1,26 @@ +' 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. + + Dim a As Integer Dim b As Integer Dim tries As Integer diff --git a/src/test/basic/fixtures/onErrorSub.bas b/src/test/basic/fixtures/onErrorSub.bas index 97b4fc53..50382dc5 100644 --- a/src/test/basic/fixtures/onErrorSub.bas +++ b/src/test/basic/fixtures/onErrorSub.bas @@ -1,3 +1,26 @@ +' 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. + + Sub Risky(n As Integer) On Error GoTo Local Dim x As Integer diff --git a/src/test/basic/fixtures/optionBase.bas b/src/test/basic/fixtures/optionBase.bas index cf254afb..d08240f5 100644 --- a/src/test/basic/fixtures/optionBase.bas +++ b/src/test/basic/fixtures/optionBase.bas @@ -1,3 +1,26 @@ +' 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. + + Option Base 1 Dim a(3) As Integer Dim m(2, 2) As Integer diff --git a/src/test/basic/fixtures/parserExt.bas b/src/test/basic/fixtures/parserExt.bas index c0340e2e..f9adf146 100644 --- a/src/test/basic/fixtures/parserExt.bas +++ b/src/test/basic/fixtures/parserExt.bas @@ -1,3 +1,26 @@ +' 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. + + ' Nested TYPE fields on array elements, array parameters, SWAP of ' array elements, the BYREF keyword and DIM name() dynamic arrays. Type PointT diff --git a/src/test/basic/fixtures/printTab.bas b/src/test/basic/fixtures/printTab.bas index ff808472..112ecc2b 100644 --- a/src/test/basic/fixtures/printTab.bas +++ b/src/test/basic/fixtures/printTab.bas @@ -1,3 +1,26 @@ +' 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. + + Print "a"; Tab(5); "b"; Tab(10); "c" Print "abcdefgh"; Tab(3); "x" Print Tab(4); "y" diff --git a/src/test/basic/fixtures/printUsing.bas b/src/test/basic/fixtures/printUsing.bas index 63ae1be8..39a51486 100644 --- a/src/test/basic/fixtures/printUsing.bas +++ b/src/test/basic/fixtures/printUsing.bas @@ -1,3 +1,26 @@ +' 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. + + Print Using "###.##"; 3.14159 Print Using "#,###"; 1234567 Print Using "$$#,##0.00"; 1234567.89 diff --git a/src/test/basic/fixtures/printUsingLiterals.bas b/src/test/basic/fixtures/printUsingLiterals.bas index 43901087..1f09468f 100644 --- a/src/test/basic/fixtures/printUsingLiterals.bas +++ b/src/test/basic/fixtures/printUsingLiterals.bas @@ -1,3 +1,26 @@ +' 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. + + ' PRINT USING keeps literal text between fields, floats the sign next ' to the digits and reuses the format when the values outnumber fields. Print Using "& likes &"; "Scott"; "pizza" diff --git a/src/test/basic/fixtures/procs.bas b/src/test/basic/fixtures/procs.bas index 70f95fc8..c563d042 100644 --- a/src/test/basic/fixtures/procs.bas +++ b/src/test/basic/fixtures/procs.bas @@ -1,3 +1,26 @@ +' 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. + + Function Fact(n As Long) As Long If n <= 1 Then Fact = 1 diff --git a/src/test/basic/fixtures/resumeNoError.bas b/src/test/basic/fixtures/resumeNoError.bas index c53c37d8..52d6901f 100644 --- a/src/test/basic/fixtures/resumeNoError.bas +++ b/src/test/basic/fixtures/resumeNoError.bas @@ -1,3 +1,26 @@ +' 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. + + Print "before" Resume Next Print "after" diff --git a/src/test/basic/fixtures/selectCase.bas b/src/test/basic/fixtures/selectCase.bas index a68d41da..8e6276b9 100644 --- a/src/test/basic/fixtures/selectCase.bas +++ b/src/test/basic/fixtures/selectCase.bas @@ -1,3 +1,26 @@ +' 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. + + Dim i As Integer Dim s As String For i = 0 To 12 Step 3 diff --git a/src/test/basic/fixtures/sharedVars.bas b/src/test/basic/fixtures/sharedVars.bas index 30dcf7f4..6c2536f6 100644 --- a/src/test/basic/fixtures/sharedVars.bas +++ b/src/test/basic/fixtures/sharedVars.bas @@ -1,3 +1,26 @@ +' 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. + + ' Module variables are visible in a SUB/FUNCTION only when SHARED. Dim Shared count As Integer Dim hidden As Integer diff --git a/src/test/basic/fixtures/staticVars.bas b/src/test/basic/fixtures/staticVars.bas index 63dc0031..bd566406 100644 --- a/src/test/basic/fixtures/staticVars.bas +++ b/src/test/basic/fixtures/staticVars.bas @@ -1,3 +1,26 @@ +' 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. + + Sub Counter() Static n As Integer Dim local As Integer diff --git a/src/test/basic/fixtures/stepCap.bas b/src/test/basic/fixtures/stepCap.bas index 6e4d3363..c87e0369 100644 --- a/src/test/basic/fixtures/stepCap.bas +++ b/src/test/basic/fixtures/stepCap.bas @@ -1,3 +1,26 @@ +' 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. + + Dim i As Long Do i = i + 1 diff --git a/src/test/basic/fixtures/strAppend.bas b/src/test/basic/fixtures/strAppend.bas index 9e43d879..a07a4daa 100644 --- a/src/test/basic/fixtures/strAppend.bas +++ b/src/test/basic/fixtures/strAppend.bas @@ -1,3 +1,26 @@ +' 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. + + ' s = s + x and s = s & x append in place; results match the long form. Dim s As String Dim t As String diff --git a/src/test/basic/fixtures/stringCompare.bas b/src/test/basic/fixtures/stringCompare.bas index 60bec01c..9d671f74 100644 --- a/src/test/basic/fixtures/stringCompare.bas +++ b/src/test/basic/fixtures/stringCompare.bas @@ -1,3 +1,26 @@ +' 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. + + Print "abc" < "abd"; "abc" = "ABC"; "a" > "B" Print "abc" = "abc"; "" = ""; "abc" <> "abc " Dim s As String diff --git a/src/test/basic/fixtures/stringFuncs.bas b/src/test/basic/fixtures/stringFuncs.bas index 88382787..6e8a80c1 100644 --- a/src/test/basic/fixtures/stringFuncs.bas +++ b/src/test/basic/fixtures/stringFuncs.bas @@ -1,3 +1,26 @@ +' 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. + + Dim s As String Dim big As String s = "Hello" & Chr$(0) & "World" diff --git a/src/test/basic/fixtures/swapVars.bas b/src/test/basic/fixtures/swapVars.bas index 99e98112..0ad5bf2e 100644 --- a/src/test/basic/fixtures/swapVars.bas +++ b/src/test/basic/fixtures/swapVars.bas @@ -1,3 +1,26 @@ +' 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. + + Dim a As Integer Dim b As Integer Dim s As String diff --git a/src/test/basic/fixtures/typedStore.bas b/src/test/basic/fixtures/typedStore.bas index 521f7a23..9835953c 100644 --- a/src/test/basic/fixtures/typedStore.bas +++ b/src/test/basic/fixtures/typedStore.bas @@ -1,3 +1,26 @@ +' 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. + + Dim i As Integer Dim l As Long Dim s As Single diff --git a/src/test/basic/fixtures/typedStoreBoolean.bas b/src/test/basic/fixtures/typedStoreBoolean.bas index 6c8ddc9e..838f3634 100644 --- a/src/test/basic/fixtures/typedStoreBoolean.bas +++ b/src/test/basic/fixtures/typedStoreBoolean.bas @@ -1,3 +1,26 @@ +' 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. + + ' A Boolean variable coerces stores to True/False; DIM without a type is ' Single; a variable that is never declared keeps the assigned type. Dim b As Boolean diff --git a/src/test/basic/fixtures/udtNested.bas b/src/test/basic/fixtures/udtNested.bas index 5edb8486..9706a8ec 100644 --- a/src/test/basic/fixtures/udtNested.bas +++ b/src/test/basic/fixtures/udtNested.bas @@ -1,3 +1,26 @@ +' 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. + + Type PointT x As Integer y As Integer diff --git a/src/test/dialogs/dialogsTest.c b/src/test/dialogs/dialogsTest.c index 9e7d648d..34663aef 100644 --- a/src/test/dialogs/dialogsTest.c +++ b/src/test/dialogs/dialogsTest.c @@ -2055,8 +2055,15 @@ static void uniformScript(AppContextT *ctx, WindowT *modal, void *userCtx) { tType("nope"); tClick(other->x + other->w / 2, other->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT / 2); tAltTab(); + + // Another program's window shown while the dialog waits goes + // behind it (keys typed for the dialog must not reach it) + WindowT *late = dvxCreateWindow(ctx, "Late", OTHER_WIN_X, OTHER_WIN_Y, OTHER_WIN_W, OTHER_WIN_H, false); + + tPump(1); s->capturedInt = (ctx->stack.windows[ctx->stack.count - 1] == modal) ? 1 : 0; s->capturedInt2 = modal->focused ? 1 : 0; + dvxDestroyWindow(ctx, late); } if (s->goldenName) { diff --git a/src/test/dos/fixtures/benchlog/begin.bas b/src/test/dos/fixtures/benchlog/begin.bas index 7cb8d2fb..c2071188 100644 --- a/src/test/dos/fixtures/benchlog/begin.bas +++ b/src/test/dos/fixtures/benchlog/begin.bas @@ -1,3 +1,26 @@ +' 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. + + ' begin.bas -- first module of the DOS benchmark fixtures (bn*/bn*.dbp, ' scripts/basbench.scr): logs the start to DVX.LOG and starts the clock. ' The benchmark program from src/test/basic/bench follows unchanged, then diff --git a/src/test/dos/fixtures/benchlog/end.bas b/src/test/dos/fixtures/benchlog/end.bas index ba0a9c70..82026fe9 100644 --- a/src/test/dos/fixtures/benchlog/end.bas +++ b/src/test/dos/fixtures/benchlog/end.bas @@ -1,3 +1,26 @@ +' 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. + + ' end.bas -- last module of the DOS benchmark fixtures: logs the time ' since begin.bas started the clock ("BENCH: done ms"). dvxLog "BENCH: done" + STR$(CLNG((TIMER - benchT0) * 1000)) + " ms" diff --git a/src/test/dos/fixtures/bncurve/bncurve.bas b/src/test/dos/fixtures/bncurve/bncurve.bas new file mode 100644 index 00000000..7555a4d5 --- /dev/null +++ b/src/test/dos/fixtures/bncurve/bncurve.bas @@ -0,0 +1,93 @@ +' 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. + +' bncurve.bas -- times the cryptography library's X25519, sealing +' (XChaCha20-Poly1305), and Ed25519 on DOS (scripts/basbench.scr) into +' DVX.LOG. A slice of an X25519 (SecX25519Step) is what one poll of the +' secure link's handshake costs; a sealed full frame is what each +' encrypted packet costs. +DECLARE LIBRARY "basrt" + DECLARE SUB dvxLog(BYVAL msg AS STRING) +END DECLARE + +CONST CURVE_ROUNDS = 8 +CONST SEAL_ROUNDS = 64 +CONST SIGN_ROUNDS = 4 +CONST FRAME_BYTES = 237 ' a full secure link payload (SECLINK_MAX_PAYLOAD) +CONST MS_PER_SEC = 1000 + +DIM t0 AS DOUBLE +DIM i AS LONG +DIM mine AS STRING +DIM theirs AS STRING +DIM key AS STRING +DIM nonce AS STRING +DIM frame AS STRING +DIM sealed AS STRING +DIM signKey AS STRING +DIM signature AS STRING +DIM job AS LONG +DIM slices AS LONG + +mine = SecRandom$(SEC_X25519_KEY_SIZE) +theirs = SecX25519PublicKey$(SecRandom$(SEC_X25519_KEY_SIZE)) +dvxLog "BENCH: start" + +t0 = TIMER +FOR i = 1 TO CURVE_ROUNDS + key = SecX25519$(mine, theirs) +NEXT i +dvxLog "BENCH: x25519" + STR$(CLNG((TIMER - t0) * MS_PER_SEC / CURVE_ROUNDS)) + " ms each" + +slices = 0 +t0 = TIMER +FOR i = 1 TO CURVE_ROUNDS + job = SecX25519Start(mine, theirs) + DO + slices = slices + 1 + LOOP WHILE SecX25519Step(job) = SEC_X25519_PENDING + key = SecX25519End$(job) +NEXT i +dvxLog "BENCH: x25519 slice" + STR$(CLNG((TIMER - t0) * MS_PER_SEC / slices)) + " ms each," + STR$(slices / CURVE_ROUNDS) + " a key" + +key = SecHkdf$("", key, "bench", SEC_AEAD_KEY_SIZE) +nonce = SecRandom$(SEC_AEAD_NONCE_SIZE) +frame = STRING$(FRAME_BYTES, 65) +t0 = TIMER +FOR i = 1 TO SEAL_ROUNDS + sealed = SecAeadLock$(key, nonce, "", frame) +NEXT i +dvxLog "BENCH: seal" + STR$(CLNG((TIMER - t0) * MS_PER_SEC * 10 / SEAL_ROUNDS)) + " ms per 10 frames" + +signKey = SecEd25519KeyPair$(SecRandom$(SEC_ED25519_SEED_SIZE)) +t0 = TIMER +FOR i = 1 TO SIGN_ROUNDS + signature = SecEd25519Sign$(signKey, frame) +NEXT i +dvxLog "BENCH: ed25519 sign" + STR$(CLNG((TIMER - t0) * MS_PER_SEC / SIGN_ROUNDS)) + " ms each" + +t0 = TIMER +FOR i = 1 TO SIGN_ROUNDS + IF NOT SecEd25519Check(signature, SecEd25519PublicKey$(signKey), frame) THEN dvxLog "BENCH: ed25519 check FAILED" +NEXT i +dvxLog "BENCH: ed25519 check" + STR$(CLNG((TIMER - t0) * MS_PER_SEC / SIGN_ROUNDS)) + " ms each" +dvxLog "BENCH: done" diff --git a/src/test/dos/fixtures/bncurve/bncurve.dbp b/src/test/dos/fixtures/bncurve/bncurve.dbp new file mode 100644 index 00000000..f904dca3 --- /dev/null +++ b/src/test/dos/fixtures/bncurve/bncurve.dbp @@ -0,0 +1,12 @@ +[Project] +Name = Bench curve +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = DOS harness fixture: X25519, sealing, and Ed25519 timed into DVX.LOG + +[Modules] +File0 = ../../../../include/basic/security.bas +File1 = bncurve.bas +Count = 2 diff --git a/src/test/dos/fixtures/dosrun/dosprog.bat b/src/test/dos/fixtures/dosrun/dosprog.bat index b8ca3ef0..7249c115 100644 --- a/src/test/dos/fixtures/dosrun/dosprog.bat +++ b/src/test/dos/fixtures/dosrun/dosprog.bat @@ -1,3 +1,25 @@ @ECHO OFF +REM The MIT License (MIT) +REM +REM Copyright (C) 2026 Scott Duensing +REM +REM Permission is hereby granted, free of charge, to any person obtaining a copy +REM of this software and associated documentation files (the "Software"), to +REM deal in the Software without restriction, including without limitation the +REM rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +REM sell copies of the Software, and to permit persons to whom the Software is +REM furnished to do so, subject to the following conditions: +REM +REM The above copyright notice and this permission notice shall be included in +REM all copies or substantial portions of the Software. +REM +REM THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +REM IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +REM FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +REM AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +REM LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +REM FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +REM IN THE SOFTWARE. + REM dosprog.scr: run by DVX from TEST\DOSRUN.APP, in its own folder (TEST) ECHO ran %1>DOSRAN.TXT diff --git a/src/test/dos/fixtures/parpm/parpm.frm b/src/test/dos/fixtures/parpm/parpm.frm index e0e6f07b..04a1aeb5 100644 --- a/src/test/dos/fixtures/parpm/parpm.frm +++ b/src/test/dos/fixtures/parpm/parpm.frm @@ -76,14 +76,14 @@ End ' outcome for the script: "PARPM PASS programs" once every step ' worked and the key was pressed, "PARPM FAIL " otherwise. Started ' with the argument "child" it is the second copy: it closes itself after -' a moment. +' a few seconds. OPTION EXPLICIT CONST TAG = "PARPM " CONST CHILD_ARG = "child" CONST CHILD_TITLE = "Parity Child" -CONST CHILD_TICKS = 5 +CONST CHILD_TICKS = 30 ' the second copy's life (3 s): longer than the parent takes to count it on a slow machine CONST WAIT_TICKS = 300 CONST HOT_KEY = "Ctrl+Alt+P" CONST GROUPS_FILE = "groups.ini" diff --git a/src/test/dos/fixtures/prdraw/prdraw.frm b/src/test/dos/fixtures/prdraw/prdraw.frm index 806aed36..c4b0716b 100644 --- a/src/test/dos/fixtures/prdraw/prdraw.frm +++ b/src/test/dos/fixtures/prdraw/prdraw.frm @@ -20,6 +20,28 @@ Begin Form PrDraw 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. + ' One page on the printer DVX.INI names, drawn with the Printer object: ' a title and a line of text in two families, a thick rule, a box filled diff --git a/src/test/dos/fixtures/scrpage/scrpage.bas b/src/test/dos/fixtures/scrpage/scrpage.bas index 2886def9..c74b8b5f 100644 --- a/src/test/dos/fixtures/scrpage/scrpage.bas +++ b/src/test/dos/fixtures/scrpage/scrpage.bas @@ -1,3 +1,26 @@ +' 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. + + ' scrpage.bas -- DOS harness fixture for scripts/scrpage.scr ' ' The program screen's other modes: SCREEN 7 pages (drawing on a hidden diff --git a/src/test/dos/fixtures/scrtest/scrtest.bas b/src/test/dos/fixtures/scrtest/scrtest.bas index e09d5bad..4def22a7 100644 --- a/src/test/dos/fixtures/scrtest/scrtest.bas +++ b/src/test/dos/fixtures/scrtest/scrtest.bas @@ -1,3 +1,26 @@ +' 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. + + ' scrtest.bas -- DOS harness fixture for scripts/scrtest.scr ' ' The program screen: console text (CLS, COLOR, LOCATE, PRINT with TAB diff --git a/src/test/dos/fixtures/shellrun/shellrun.dbp b/src/test/dos/fixtures/shellrun/shellrun.dbp new file mode 100644 index 00000000..5687fc35 --- /dev/null +++ b/src/test/dos/fixtures/shellrun/shellrun.dbp @@ -0,0 +1,18 @@ +[Project] +Name = Shell Run +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = DOS harness fixture: runs a DOS command with SHELL and waits for it (shellcmd.scr) + +[Modules] +File0 = ../../../../include/basic/shell.bas +Count = 1 + +[Forms] +File0 = shellrun.frm +Count = 1 + +[Settings] +StartupForm = ShellRun diff --git a/src/test/dos/fixtures/shellrun/shellrun.frm b/src/test/dos/fixtures/shellrun/shellrun.frm new file mode 100644 index 00000000..b38b7271 --- /dev/null +++ b/src/test/dos/fixtures/shellrun/shellrun.frm @@ -0,0 +1,65 @@ +VERSION DVX 1.00 + +Begin Form ShellRun + Caption = "Shell Run" + Layout = VBox + AutoSize = False + Resizable = False + Width = 200 + Height = 60 + + Begin Label lblResult + Caption = "Running TEST\DOSPROG.BAT" + Weight = 1 + End + Begin Label lblWait + Caption = "" + Visible = False + End + Begin Timer tmrRun + Interval = 100 + Enabled = True + 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. + + +' Runs TEST\DOSPROG.BAT with "world" through SHELL once the window is up +' (shellcmd.scr): DVX steps aside, the program waits, and goes on with the +' exit code, which it shows and logs. Then the same with ShellRunDosWait +' (its code in the hidden lblWait and the log). + +SUB tmrRun_Timer + DIM rc AS INTEGER + DIM waited AS LONG + + tmrRun.Enabled = False + rc = Shell("TEST\DOSPROG.BAT world") + waited = ShellRunDosWait("TEST\DOSPROG.BAT", "again", "", False) + lblWait.Caption = "ShellRunDosWait returned " & CStr(waited) + ShellLog "ShellRun: ShellRunDosWait returned " & CStr(waited) + lblResult.Caption = "SHELL returned " & CStr(rc) + ShellLog "ShellRun: SHELL returned " & CStr(rc) +END SUB diff --git a/src/test/dos/fixtures/sqlsmk/sqlsmk.frm b/src/test/dos/fixtures/sqlsmk/sqlsmk.frm index ff366ccb..3375bc5e 100644 --- a/src/test/dos/fixtures/sqlsmk/sqlsmk.frm +++ b/src/test/dos/fixtures/sqlsmk/sqlsmk.frm @@ -1,17 +1,28 @@ VERSION DVX 1.00 Begin Form SqlSmoke - Caption = "SQL Smoke" + Caption = "Smoke Test" Layout = VBox AutoSize = False Resizable = False - Width = 320 + Width = 400 Height = 90 Begin Label lblResult Caption = "Running..." Weight = 1 End + Begin Timer tmrStart + Interval = 100 + Enabled = False + End + Begin Timer tmrTick + Interval = 50 + Enabled = False + End + Begin Data datBig + Visible = False + End End ' The MIT License (MIT) @@ -38,15 +49,30 @@ End ' sqlsmk -- DOS harness fixture. Creates TEST\SMOKE.DB, inserts three ' rows in a transaction, reads them back and sums a REAL column through -' SQLFieldDbl (a DECLARE LIBRARY function returning DOUBLE). The window -' title reports the outcome for the script: "SQL PASS 3 rows 3.25" or -' "SQL FAIL ". +' SQLFieldDbl (a DECLARE LIBRARY function returning DOUBLE). Then, from a +' Timer: a long statement (made longer until it is long enough) during +' which the program's Timer events run and are refused the busy database, +' and a Data control whose record set arrives a part at a time (the first +' DATA_FIRST rows at Refresh, the rest while the program waits), walked +' with a Do Until EOF loop. The window title reports the outcome for the +' script once every step has run: "SQL PASS 3 rows 3.25 long data 64/1000 +' loop 1000" or "SQL FAIL ". OPTION EXPLICIT CONST DB_PATH = "TEST\SMOKE.DB" CONST EXPECT_ROWS = 3 CONST EXPECT_SUM = 3.25 +CONST LONG_FIRST = 20000 ' rows the long statement counts at first +CONST LONG_MAX = 5120000 ' and at most +CONST BIG_ROWS = 1000 ' the Data control's record set +CONST DATA_FIRST = 64 ' the rows a Refresh reads +CONST WAIT_LIMIT = 5000 ' DoEvents calls to wait for the rest + +DIM SHARED gDb AS LONG +DIM SHARED gInSql AS BOOLEAN +DIM SHARED gDuring AS LONG +DIM SHARED gRefused AS BOOLEAN SUB Report(BYVAL ok AS BOOLEAN, BYVAL what AS STRING) @@ -105,5 +131,101 @@ SUB SqlSmoke_Load SQLClose db passed = (rows = EXPECT_ROWS AND total = EXPECT_SUM) - Report passed, LTRIM$(STR$(rows)) + " rows " + LTRIM$(STR$(total)) + + IF NOT passed THEN + Report False, LTRIM$(STR$(rows)) + " rows " + LTRIM$(STR$(total)) + EXIT SUB + END IF + + ' The title says "SQL" only once every step has reported + lblResult.Caption = LTRIM$(STR$(rows)) + " rows " + LTRIM$(STR$(total)) + tmrStart.Enabled = True +END SUB + + +SUB tmrStart_Timer + DIM size AS LONG + DIM ok AS BOOLEAN + DIM first AS LONG + DIM waits AS LONG + DIM loops AS LONG + DIM last AS STRING + DIM sums AS STRING + + tmrStart.Enabled = False + sums = lblResult.Caption + gDb = SQLOpen(DB_PATH) + + ' A statement long enough that the Timer's events run during it + size = LONG_FIRST + tmrTick.Enabled = True + + DO + gInSql = True + ok = SQLExec(gDb, "SELECT count(*) FROM (WITH RECURSIVE c(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM c WHERE x < " + LTRIM$(STR$(size)) + ") SELECT x FROM c)") + gInSql = False + size = size * 2 + LOOP WHILE ok AND gDuring = 0 AND size <= LONG_MAX + + tmrTick.Enabled = False + + IF NOT ok THEN + Report False, "long: " + SQLError$(gDb) + EXIT SUB + END IF + + IF gDuring = 0 THEN + Report False, "no events ran during a long statement" + EXIT SUB + END IF + + IF NOT gRefused THEN + Report False, "the busy database was not refused" + EXIT SUB + END IF + + ok = SQLExec(gDb, "DROP TABLE IF EXISTS big") + ok = ok AND SQLExec(gDb, "CREATE TABLE big AS WITH RECURSIVE c(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM c WHERE x < " + LTRIM$(STR$(BIG_ROWS)) + ") SELECT x FROM c") + + IF NOT ok THEN + Report False, "big: " + SQLError$(gDb) + EXIT SUB + END IF + + SQLClose gDb + + ' The first rows at Refresh, the rest while the program waits + datBig.DatabaseName = DB_PATH + datBig.RecordSource = "big" + datBig.Refresh + first = datBig.RecordCount + + DO WHILE datBig.RecordCount < BIG_ROWS AND waits < WAIT_LIMIT + DoEvents + waits = waits + 1 + LOOP + + ' Do Until EOF reads every record once (VB's EOF) + datBig.MoveFirst + + DO UNTIL datBig.EOF + loops = loops + 1 + datBig.MoveNext + LOOP + + datBig.MoveLast + last = datBig.GetField("x") + + ok = (first = DATA_FIRST AND datBig.RecordCount = BIG_ROWS AND loops = BIG_ROWS AND last = LTRIM$(STR$(BIG_ROWS))) + Report ok, sums + " long data " + LTRIM$(STR$(first)) + "/" + LTRIM$(STR$(datBig.RecordCount)) + " loop " + LTRIM$(STR$(loops)) +END SUB + + +' During the long statement only: the program's events run, and the +' database the statement runs on is refused +SUB tmrTick_Timer + IF gInSql THEN + gDuring = gDuring + 1 + gRefused = NOT SQLExec(gDb, "SELECT 1") AND INSTR(SQLError$(gDb), "still running") > 0 + END IF END SUB diff --git a/src/test/dos/musTest/musTest.c b/src/test/dos/musTest/musTest.c index e725b8a6..d1688850 100644 --- a/src/test/dos/musTest/musTest.c +++ b/src/test/dos/musTest/musTest.c @@ -52,6 +52,7 @@ // MUSTEST: load: units, % of the CPU). #include "dvxApp.h" +#include "dvxEndian.h" #include "audio/audio.h" #include "music/music.h" #include "shellApp.h" @@ -235,8 +236,6 @@ static uint32_t makeRomFont(uint8_t *buf); static uint32_t midEvent(uint8_t *buf, uint32_t at, uint32_t delta, uint8_t status, uint8_t data1, uint8_t data2, bool twoData); static int64_t nowMs(void); static void onEnd(MusicT *music, void *userData); -static void put16(uint8_t *p, uint32_t v); -static void put32(uint8_t *p, uint32_t v); static void putCell(uint8_t *pattern, int32_t row, int32_t channel, uint32_t period, uint8_t effect, uint8_t param); static uint32_t riffChunk(uint8_t *buf, uint32_t at, const char *id, const uint8_t *body, uint32_t len); static uint32_t riffListClose(uint8_t *buf, uint32_t at, uint32_t from); @@ -442,8 +441,8 @@ static uint32_t makeRomFont(uint8_t *buf) { // INFO: the version; sdta: no samples - put16(rec, SF_VERSION_MAJOR); - put16(rec + 2, SF_VERSION_MINOR); + dvxPutLe16(rec, SF_VERSION_MAJOR); + dvxPutLe16(rec + 2, SF_VERSION_MINOR); list = at; at = riffListOpen(buf, at, "INFO"); at = riffChunk(buf, at, "ifil", rec, SF_GEN_BYTES); @@ -458,45 +457,45 @@ static uint32_t makeRomFont(uint8_t *buf) { at = riffListOpen(buf, at, "pdta"); memset(rec, 0, sizeof(rec)); memcpy(rec, "Flute", 5); - put16(rec + SF_PHDR_PRESET, MID_FLUTE); + dvxPutLe16(rec + SF_PHDR_PRESET, MID_FLUTE); memcpy(rec + SF_PHDR_BYTES, "EOP", 3); - put16(rec + SF_PHDR_BYTES + SF_PHDR_BAG, 1); + dvxPutLe16(rec + SF_PHDR_BYTES + SF_PHDR_BAG, 1); at = riffChunk(buf, at, "phdr", rec, SF_PHDR_BYTES * 2); memset(rec, 0, sizeof(rec)); - put16(rec + SF_BAG_BYTES, 1); + dvxPutLe16(rec + SF_BAG_BYTES, 1); at = riffChunk(buf, at, "pbag", rec, SF_BAG_BYTES * 2); memset(rec, 0, sizeof(rec)); at = riffChunk(buf, at, "pmod", rec, SF_MOD_BYTES); - put16(rec, SF_GEN_INSTRUMENT); + dvxPutLe16(rec, SF_GEN_INSTRUMENT); at = riffChunk(buf, at, "pgen", rec, SF_GEN_BYTES * 2); memset(rec, 0, sizeof(rec)); memcpy(rec, "Flute", 5); memcpy(rec + SF_INST_BYTES, "EOI", 3); - put16(rec + SF_INST_BYTES + SF_INST_BAG, 1); + dvxPutLe16(rec + SF_INST_BYTES + SF_INST_BAG, 1); at = riffChunk(buf, at, "inst", rec, SF_INST_BYTES * 2); memset(rec, 0, sizeof(rec)); - put16(rec + SF_BAG_BYTES, 2); + dvxPutLe16(rec + SF_BAG_BYTES, 2); at = riffChunk(buf, at, "ibag", rec, SF_BAG_BYTES * 2); memset(rec, 0, sizeof(rec)); at = riffChunk(buf, at, "imod", rec, SF_MOD_BYTES); - put16(rec, SF_GEN_SAMPLE_MODES); - put16(rec + 2, SF_LOOP_ON); - put16(rec + SF_GEN_BYTES, SF_GEN_SAMPLE_ID); + dvxPutLe16(rec, SF_GEN_SAMPLE_MODES); + dvxPutLe16(rec + 2, SF_LOOP_ON); + dvxPutLe16(rec + SF_GEN_BYTES, SF_GEN_SAMPLE_ID); at = riffChunk(buf, at, "igen", rec, SF_GEN_BYTES * 3); memset(rec, 0, sizeof(rec)); memcpy(rec, "flutec4", 7); - put32(rec + SF_SHDR_START, ROM_FLUTE_START); - put32(rec + SF_SHDR_END, ROM_FLUTE_END); - put32(rec + SF_SHDR_LOOP, ROM_FLUTE_LOOP); - put32(rec + SF_SHDR_LOOP_END, ROM_FLUTE_LOOP_END); - put32(rec + SF_SHDR_RATE, ROM_RATE); + dvxPutLe32(rec + SF_SHDR_START, ROM_FLUTE_START); + dvxPutLe32(rec + SF_SHDR_END, ROM_FLUTE_END); + dvxPutLe32(rec + SF_SHDR_LOOP, ROM_FLUTE_LOOP); + dvxPutLe32(rec + SF_SHDR_LOOP_END, ROM_FLUTE_LOOP_END); + dvxPutLe32(rec + SF_SHDR_RATE, ROM_RATE); rec[SF_SHDR_KEY] = ROM_ROOT; rec[SF_SHDR_CORRECTION] = (uint8_t)(int8_t)ROM_CORRECTION; - put16(rec + SF_SHDR_TYPE, ROM_SAMPLE_TYPE); + dvxPutLe16(rec + SF_SHDR_TYPE, ROM_SAMPLE_TYPE); memcpy(rec + SF_SHDR_BYTES, "EOS", 3); at = riffChunk(buf, at, "shdr", rec, SF_SHDR_BYTES * 2); at = riffListClose(buf, at, list); - put32(buf + RIFF_ID_BYTES, at - RIFF_HEAD_BYTES); + dvxPutLe32(buf + RIFF_ID_BYTES, at - RIFF_HEAD_BYTES); return at; } @@ -530,18 +529,6 @@ static void onEnd(MusicT *music, void *userData) { } -static void put16(uint8_t *p, uint32_t v) { - p[0] = (uint8_t)v; - p[1] = (uint8_t)(v >> 8); -} - - -static void put32(uint8_t *p, uint32_t v) { - put16(p, v); - put16(p + 2, v >> 16); -} - - static void putCell(uint8_t *pattern, int32_t row, int32_t channel, uint32_t period, uint8_t effect, uint8_t param) { uint8_t *cell = pattern + row * MOD_ROW_BYTES + channel * MOD_CELL_BYTES; @@ -556,7 +543,7 @@ static void putCell(uint8_t *pattern, int32_t row, int32_t channel, uint32_t per // chunk here has an odd length). static uint32_t riffChunk(uint8_t *buf, uint32_t at, const char *id, const uint8_t *body, uint32_t len) { memcpy(buf + at, id, RIFF_ID_BYTES); - put32(buf + at + RIFF_ID_BYTES, len); + dvxPutLe32(buf + at + RIFF_ID_BYTES, len); memcpy(buf + at + RIFF_HEAD_BYTES, body, len); return at + RIFF_HEAD_BYTES + len; } @@ -565,7 +552,7 @@ static uint32_t riffChunk(uint8_t *buf, uint32_t at, const char *id, const uint8 // The LIST opened at from ends at at: its id and length. static uint32_t riffListClose(uint8_t *buf, uint32_t at, uint32_t from) { memcpy(buf + from, "LIST", RIFF_ID_BYTES); - put32(buf + from + RIFF_ID_BYTES, at - from - RIFF_HEAD_BYTES); + dvxPutLe32(buf + from + RIFF_ID_BYTES, at - from - RIFF_HEAD_BYTES); return at; } @@ -601,21 +588,21 @@ static bool writePatchSet(void) { memset(pat, 0, sizeof(pat)); memcpy(pat, "GF1PATCH110", 11); - put16(pat + PAT_WAVES_AT, 1); - put32(w + PAT_W_SIZE, PAT_FRAMES * PAT_FRAME_BYTES); - put32(w + PAT_W_LOOP_START, PAT_LOOP * PAT_FRAME_BYTES); - put32(w + PAT_W_LOOP_END, PAT_FRAMES * PAT_FRAME_BYTES); - put16(w + PAT_W_RATE, PAT_RATE); - put32(w + PAT_W_LOW, PAT_LOW_MILLIHZ); - put32(w + PAT_W_HIGH, PAT_HIGH_MILLIHZ); - put32(w + PAT_W_ROOT, PAT_ROOT_MILLIHZ); + dvxPutLe16(pat + PAT_WAVES_AT, 1); + dvxPutLe32(w + PAT_W_SIZE, PAT_FRAMES * PAT_FRAME_BYTES); + dvxPutLe32(w + PAT_W_LOOP_START, PAT_LOOP * PAT_FRAME_BYTES); + dvxPutLe32(w + PAT_W_LOOP_END, PAT_FRAMES * PAT_FRAME_BYTES); + dvxPutLe16(w + PAT_W_RATE, PAT_RATE); + dvxPutLe32(w + PAT_W_LOW, PAT_LOW_MILLIHZ); + dvxPutLe32(w + PAT_W_HIGH, PAT_HIGH_MILLIHZ); + dvxPutLe32(w + PAT_W_ROOT, PAT_ROOT_MILLIHZ); w[PAT_W_BALANCE] = PAT_BALANCE; w[PAT_W_MODES] = PAT_MODES; - put16(w + PAT_W_SCALE_KEY, PAT_PIVOT); - put16(w + PAT_W_SCALE_FACTOR, PAT_SEMITONE); + dvxPutLe16(w + PAT_W_SCALE_KEY, PAT_PIVOT); + dvxPutLe16(w + PAT_W_SCALE_FACTOR, PAT_SEMITONE); for (int32_t i = 0; i < PAT_FRAMES; i++) { - put16(data + i * PAT_FRAME_BYTES, (uint32_t)(uint16_t)(int16_t)(PAT_AMPLITUDE * sin(MOD_TWO_PI * i / PAT_PERIOD))); + dvxPutLe16(data + i * PAT_FRAME_BYTES, (uint32_t)(uint16_t)(int16_t)(PAT_AMPLITUDE * sin(MOD_TWO_PI * i / PAT_PERIOD))); } return platformMkdirRecursive(MUS_PAT_MIDI_DIR) == 0 && writeFile(MUS_PAT_INI, (const uint8_t *)MUS_PAT_MAP, (uint32_t)strlen(MUS_PAT_MAP)) && writeFile(MUS_PAT_FILE, pat, sizeof(pat)); diff --git a/src/test/dos/script/scriptDos.c b/src/test/dos/script/scriptDos.c index 128661c9..068cf79d 100644 --- a/src/test/dos/script/scriptDos.c +++ b/src/test/dos/script/scriptDos.c @@ -35,6 +35,7 @@ #include "scriptRun.h" #include "shellApp.h" +#include "taskSwch.h" #include #include @@ -229,6 +230,14 @@ static void scriptFinish(AppContextT *ctx, bool pass, const char *detail) { static void scriptIdle(void *unused) { (void)unused; + // A step per frame of the shell's own task, which takes the input: the + // idle handlers also run in other programs' frames, where a step's + // input would pile up ahead of the shell's next poll (a click's mouse + // samples, read one a frame, behind the keys after it). + if (tsCurrentId() != TS_MAIN_TASK_ID) { + return; + } + // Idle frames only happen once the shell has set the GUI up. sRun.env.ctx = shellContext(); diff --git a/src/test/dos/scripts/addons.scr b/src/test/dos/scripts/addons.scr index 63de3dc9..47cc7e51 100644 --- a/src/test/dos/scripts/addons.scr +++ b/src/test/dos/scripts/addons.scr @@ -1,3 +1,25 @@ +# 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. + # addons.scr -- add-ons that cannot load are skipped, not fatal. # # A damaged add-on library and an add-on widget that needs a library that diff --git a/src/test/dos/scripts/apps.scr b/src/test/dos/scripts/apps.scr index 1e860d63..e1b89ba3 100644 --- a/src/test/dos/scripts/apps.scr +++ b/src/test/dos/scripts/apps.scr @@ -1,3 +1,25 @@ +# 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. + # apps.scr -- open and close every bundled program. # # Each program is started by path, must open its main window (captured diff --git a/src/test/dos/scripts/awemod.scr b/src/test/dos/scripts/awemod.scr index 372642ca..6a56bf89 100644 --- a/src/test/dos/scripts/awemod.scr +++ b/src/test/dos/scripts/awemod.scr @@ -1,3 +1,25 @@ +# 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. + # awemod.scr -- a tracker module on the Sound Blaster AWE32's EMU8000, # recorded. # diff --git a/src/test/dos/scripts/awerom.scr b/src/test/dos/scripts/awerom.scr index decc42d1..d8de476c 100644 --- a/src/test/dos/scripts/awerom.scr +++ b/src/test/dos/scripts/awerom.scr @@ -1,3 +1,25 @@ +# 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. + # awerom.scr -- a MIDI file on the Sound Blaster AWE32's sample ROM, # recorded. # diff --git a/src/test/dos/scripts/awewave.scr b/src/test/dos/scripts/awewave.scr index c10766e9..79f34cbd 100644 --- a/src/test/dos/scripts/awewave.scr +++ b/src/test/dos/scripts/awewave.scr @@ -1,3 +1,25 @@ +# 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. + # awewave.scr -- a MIDI file on the Sound Blaster AWE32's EMU8000, # recorded. # diff --git a/src/test/dos/scripts/banked.scr b/src/test/dos/scripts/banked.scr index 848dbfe4..61feb56d 100644 --- a/src/test/dos/scripts/banked.scr +++ b/src/test/dos/scripts/banked.scr @@ -1,3 +1,25 @@ +# 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. + # banked.scr -- a video card without a linear frame buffer: DVX writes # through the 64 KB window. # diff --git a/src/test/dos/scripts/basbench.scr b/src/test/dos/scripts/basbench.scr index 61e67d5b..74bd0652 100644 --- a/src/test/dos/scripts/basbench.scr +++ b/src/test/dos/scripts/basbench.scr @@ -1,3 +1,25 @@ +# 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. + # basbench.scr -- DOS timing anchor for the BASIC benchmarks. # # Runs five of the host benchmark programs (src/test/basic/bench) inside @@ -9,6 +31,9 @@ # recorded in claudeDocs/basicRedesignPlan.md; nothing here fails on a # slow run. The programs run as compiled apps in the shell (release # build, step slices with DoEvents between), not in the host harness. +# Last, fixtures/bncurve times the cryptography library's X25519 (one +# poll of the secure link's handshake), a sealed full frame, and Ed25519 +# ("BENCH: x25519 ms each" and so on). # ONLY: 86box # LOG: BENCH: done TIMEOUT 120 @@ -29,5 +54,8 @@ WAITLOG "BENCH: done" 900 LOG "BENCHRUN objectGraph" LAUNCH TEST/BNOBJ.APP WAITLOG "BENCH: done" 900 +LOG "BENCHRUN curve" +LAUNCH TEST/BNCURVE.APP +WAITLOG "BENCH: ed25519 check" 900 WAIT 30 QUIT diff --git a/src/test/dos/scripts/boot.scr b/src/test/dos/scripts/boot.scr index 461edd71..0223e8e7 100644 --- a/src/test/dos/scripts/boot.scr +++ b/src/test/dos/scripts/boot.scr @@ -1,8 +1,38 @@ +# 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. + # boot.scr -- DVX boots to the desktop. # # The desktop (Program Manager) must open, the pointer is parked in the # bottom-right corner so it is in a known place, and the whole screen is -# compared with the golden. +# compared with the golden. The desktop alone needs none of the big +# libraries, so DVX leaves them on disk (they load when first used): the +# BASIC run time and IDE libraries, the database, and the package library +# must not be loaded, and the loader says how many it left. +# LOG: libraries load when first used +# NOLOG: Loading: LIBS\kpunch\basrt\basrt.lib +# NOLOG: Loading: LIBS\kpunch\basdev\basdev.lib +# NOLOG: Loading: LIBS\kpunch\dvxsql\dvxsql.lib +# NOLOG: Loading: LIBS\kpunch\pkg\pkg.lib TIMEOUT 120 WAITWIN "Program Manager" MOVE 639 479 diff --git a/src/test/dos/scripts/cpanel.scr b/src/test/dos/scripts/cpanel.scr index 808e212d..9943ec80 100644 --- a/src/test/dos/scripts/cpanel.scr +++ b/src/test/dos/scripts/cpanel.scr @@ -1,3 +1,25 @@ +# 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. + # cpanel.scr -- every Control Panel tab fits a 640x480 screen. # # The window is sized to its largest tab; on each tab the whole window, diff --git a/src/test/dos/scripts/desksnd.scr b/src/test/dos/scripts/desksnd.scr index 647237fc..ccd450e3 100644 --- a/src/test/dos/scripts/desksnd.scr +++ b/src/test/dos/scripts/desksnd.scr @@ -1,3 +1,25 @@ +# 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. + # desksnd.scr -- desktop sounds and BASIC's BEEP, SOUND and PLAY, recorded. # # The shipped desktop sounds stay on (# KEEP: sounds): the start sound diff --git a/src/test/dos/scripts/docOpen.scr b/src/test/dos/scripts/docOpen.scr index 151f03e3..4e941f8d 100644 --- a/src/test/dos/scripts/docOpen.scr +++ b/src/test/dos/scripts/docOpen.scr @@ -1,3 +1,25 @@ +# 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. + # docOpen.scr -- a document opens in its program with the file loaded. # # File > Run... with a text file (the shipped association list) starts diff --git a/src/test/dos/scripts/dosprog.scr b/src/test/dos/scripts/dosprog.scr index 98e175aa..1ded8bcf 100644 --- a/src/test/dos/scripts/dosprog.scr +++ b/src/test/dos/scripts/dosprog.scr @@ -1,3 +1,25 @@ +# 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. + # dosprog.scr -- a DOS program run from DVX: DVX steps aside and comes back. # # TEST\DOSRUN.APP (fixtures/dosrun) asks for TEST\DOSPROG.BAT with @@ -14,6 +36,9 @@ TIMEOUT 60 WAITWIN "Program Manager" MOVE 639 479 +# Program Manager's icons come in a few a frame (all of them before the +# dialog covers them, as the shot has them) +WAIT 60 LAUNCH TEST/DOSRUN.APP WAITWIN "Run DOS Program" WAIT 10 diff --git a/src/test/dos/scripts/endbas.scr b/src/test/dos/scripts/endbas.scr index 36589166..2c797cb6 100644 --- a/src/test/dos/scripts/endbas.scr +++ b/src/test/dos/scripts/endbas.scr @@ -1,3 +1,25 @@ +# 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. + # endbas.scr -- End Task on a BASIC program: the BASIC runtime's kill # hook ends the program's VM, forms, and module while its window is # still there (the log says so), then the shell unloads it and DVX goes diff --git a/src/test/dos/scripts/exit.scr b/src/test/dos/scripts/exit.scr index 99a8bb5b..93545840 100644 --- a/src/test/dos/scripts/exit.scr +++ b/src/test/dos/scripts/exit.scr @@ -1,3 +1,25 @@ +# 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. + # exit.scr -- Exit DVX with an unsaved Notepad document. # # The first exit is cancelled at Notepad's "Save changes?" prompt: DVX must diff --git a/src/test/dos/scripts/f1.scr b/src/test/dos/scripts/f1.scr index 0639f99d..1ba31374 100644 --- a/src/test/dos/scripts/f1.scr +++ b/src/test/dos/scripts/f1.scr @@ -1,3 +1,25 @@ +# 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. + # f1.scr -- F1 opens the help for every program. # # Program Manager first (the desktop), then each program: start it, press diff --git a/src/test/dos/scripts/fileMgr.scr b/src/test/dos/scripts/fileMgr.scr index d160509a..757f0397 100644 --- a/src/test/dos/scripts/fileMgr.scr +++ b/src/test/dos/scripts/fileMgr.scr @@ -1,3 +1,25 @@ +# 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. + # fileMgr.scr -- File Manager smoke test. # # The File Manager opens on the DVX folder (drive bar probed without any diff --git a/src/test/dos/scripts/filedlg.scr b/src/test/dos/scripts/filedlg.scr index f3e1cbf2..88dfe741 100644 --- a/src/test/dos/scripts/filedlg.scr +++ b/src/test/dos/scripts/filedlg.scr @@ -1,3 +1,25 @@ +# 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. + # filedlg.scr -- the file dialog's drive drop-down. # # Notepad's File > Open dialog has the drive drop-down at the end of the diff --git a/src/test/dos/scripts/firstrun.scr b/src/test/dos/scripts/firstrun.scr index 494caa5c..b0d7f1d9 100644 --- a/src/test/dos/scripts/firstrun.scr +++ b/src/test/dos/scripts/firstrun.scr @@ -1,3 +1,25 @@ +# 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. + # firstrun.scr -- the shell starts the Welcome window at the first start. # KEEP: welcome TIMEOUT 120 diff --git a/src/test/dos/scripts/fmperf.scr b/src/test/dos/scripts/fmperf.scr index e326de7d..3e97ffa8 100644 --- a/src/test/dos/scripts/fmperf.scr +++ b/src/test/dos/scripts/fmperf.scr @@ -1,3 +1,25 @@ +# 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. + # fmperf.scr -- File Manager timing on a large folder. # # TEST\BIGDIR.APP (fixtures/bigdir) makes TEST\BIG, 400 files and 20 diff --git a/src/test/dos/scripts/fonts.scr b/src/test/dos/scripts/fonts.scr index 760f9bad..1b82d86e 100644 --- a/src/test/dos/scripts/fonts.scr +++ b/src/test/dos/scripts/fonts.scr @@ -1,3 +1,25 @@ +# 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. + # fonts.scr -- fonts on the real DOS build at the test mode's 16 bits per # pixel, where text is smoothed: the BASIC Demo's Fonts window (a # RichLabel, a label in a font of its own, and a RichEdit), DVX Demo's diff --git a/src/test/dos/scripts/fonts8.scr b/src/test/dos/scripts/fonts8.scr index 21f34600..13acfc43 100644 --- a/src/test/dos/scripts/fonts8.scr +++ b/src/test/dos/scripts/fonts8.scr @@ -1,3 +1,25 @@ +# 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. + # fonts8.scr -- the same as fonts.scr at 8 bits per pixel, where text is # not smoothed: the 1-bit glyphs of every font (the smoothed sizes too). # VIDEO: 640 480 8 diff --git a/src/test/dos/scripts/fullmode.scr b/src/test/dos/scripts/fullmode.scr index 1e9806fc..52fb69cd 100644 --- a/src/test/dos/scripts/fullmode.scr +++ b/src/test/dos/scripts/fullmode.scr @@ -1,3 +1,25 @@ +# 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. + # fullmode.scr -- every kind of full-screen mode shows its picture. # # FULLSCREEN puts the script's own window on the whole screen with a test diff --git a/src/test/dos/scripts/fullscr.scr b/src/test/dos/scripts/fullscr.scr index 4599893c..a590ecaa 100644 --- a/src/test/dos/scripts/fullscr.scr +++ b/src/test/dos/scripts/fullscr.scr @@ -1,3 +1,25 @@ +# 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. + # fullscr.scr -- a game on the whole screen while another program runs on. # # TEST\TICKER.APP (fixtures/ticker) counts to 100 in its caption, ten a diff --git a/src/test/dos/scripts/gamepad.scr b/src/test/dos/scripts/gamepad.scr index 4240f2ad..88b9b12d 100644 --- a/src/test/dos/scripts/gamepad.scr +++ b/src/test/dos/scripts/gamepad.scr @@ -1,3 +1,25 @@ +# 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. + # gamepad.scr -- the game platform: game input, held keys, the timer # service, frame pacing, exit() in a program, and the DOS clock. # diff --git a/src/test/dos/scripts/gus.scr b/src/test/dos/scripts/gus.scr index 0ceb231e..77e7511c 100644 --- a/src/test/dos/scripts/gus.scr +++ b/src/test/dos/scripts/gus.scr @@ -1,3 +1,25 @@ +# 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. + # gus.scr -- sound through a Gravis UltraSound, recorded. # # A Gravis UltraSound alone (# AUDIO-CARD: no Sound Blaster), found by diff --git a/src/test/dos/scripts/gusmod.scr b/src/test/dos/scripts/gusmod.scr index 14e56c1c..86e95dcb 100644 --- a/src/test/dos/scripts/gusmod.scr +++ b/src/test/dos/scripts/gusmod.scr @@ -1,3 +1,25 @@ +# 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. + # gusmod.scr -- a tracker module on the Gravis UltraSound's own voices, # recorded. # diff --git a/src/test/dos/scripts/guspat.scr b/src/test/dos/scripts/guspat.scr index 18422d98..020a69dd 100644 --- a/src/test/dos/scripts/guspat.scr +++ b/src/test/dos/scripts/guspat.scr @@ -1,3 +1,25 @@ +# 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. + # guspat.scr -- a MIDI file on a Gravis UltraSound patch set, recorded. # # A Gravis UltraSound alone (# AUDIO-CARD) and no SoundFont add-on. diff --git a/src/test/dos/scripts/guswave.scr b/src/test/dos/scripts/guswave.scr index d4057576..253e8e12 100644 --- a/src/test/dos/scripts/guswave.scr +++ b/src/test/dos/scripts/guswave.scr @@ -1,3 +1,25 @@ +# 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. + # guswave.scr -- a MIDI file on the Gravis UltraSound's wavetable # synthesizer, recorded. # diff --git a/src/test/dos/scripts/guswt.scr b/src/test/dos/scripts/guswt.scr index 9447f924..cd10409c 100644 --- a/src/test/dos/scripts/guswt.scr +++ b/src/test/dos/scripts/guswt.scr @@ -1,3 +1,25 @@ +# 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. + # guswt.scr -- a MIDI file on a Gravis UltraSound beside a Sound Blaster, # recorded. # diff --git a/src/test/dos/scripts/hang.scr b/src/test/dos/scripts/hang.scr index defc9efc..eefa890d 100644 --- a/src/test/dos/scripts/hang.scr +++ b/src/test/dos/scripts/hang.scr @@ -1,3 +1,25 @@ +# 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. + # hang.scr -- End Task on a program that stopped responding. # # TEST\HANG.APP (fixtures/hang) loops forever once its button is pressed. diff --git a/src/test/dos/scripts/instadd.scr b/src/test/dos/scripts/instadd.scr index 2638e8d1..56041854 100644 --- a/src/test/dos/scripts/instadd.scr +++ b/src/test/dos/scripts/instadd.scr @@ -1,3 +1,25 @@ +# 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. + # instadd.scr -- SETUP.EXE installs DVX with an add-on from the CD. # # As instbase.scr, from the CD image, with /ADDONS=MOREFONT: Setup offers diff --git a/src/test/dos/scripts/install.scr b/src/test/dos/scripts/install.scr index 10ef2c2e..437579ac 100644 --- a/src/test/dos/scripts/install.scr +++ b/src/test/dos/scripts/install.scr @@ -1,3 +1,25 @@ +# 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. + # install.scr -- SETUP.EXE installs DVX from the CD image (86Box) or the # floppy set (DOSBox), then the installed copy starts. # diff --git a/src/test/dos/scripts/instbase.scr b/src/test/dos/scripts/instbase.scr index a5bdb6cf..5047fe3d 100644 --- a/src/test/dos/scripts/instbase.scr +++ b/src/test/dos/scripts/instbase.scr @@ -1,3 +1,25 @@ +# 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. + # instbase.scr -- SETUP.EXE installs DVX with only the base set of fonts. # # As install.scr, from the CD image, with /FONTS=BASE: Setup unpacks DVX diff --git a/src/test/dos/scripts/instcore.scr b/src/test/dos/scripts/instcore.scr index 23f788c4..ffd74874 100644 --- a/src/test/dos/scripts/instcore.scr +++ b/src/test/dos/scripts/instcore.scr @@ -1,3 +1,25 @@ +# 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. + # instcore.scr -- SETUP.EXE installs DVX's core only: no optional parts. # # As install.scr, from the CD image, with /PARTS=NONE: Setup leaves out diff --git a/src/test/dos/scripts/joy4axis.scr b/src/test/dos/scripts/joy4axis.scr index becaf4e9..bed8965b 100644 --- a/src/test/dos/scripts/joy4axis.scr +++ b/src/test/dos/scripts/joy4axis.scr @@ -1,3 +1,25 @@ +# 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. + # joy4axis.scr -- a four-axis joystick on the game port, read as one # joystick with all four axes and buttons ([joystick] gameport.mode # fourAxes, set in the staged DVX.INI with # INI). DOSBox only (see diff --git a/src/test/dos/scripts/joycal.scr b/src/test/dos/scripts/joycal.scr index a3a94823..cec39ef7 100644 --- a/src/test/dos/scripts/joycal.scr +++ b/src/test/dos/scripts/joycal.scr @@ -1,3 +1,25 @@ +# 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. + # joycal.scr -- the Control Panel's Game Controllers page calibrating a # joystick with its wizard. DOSBox only: the emulated joystick is moved # with host keys (see joymove.scr and joyDrive.py). The stick rests for diff --git a/src/test/dos/scripts/joyempty.scr b/src/test/dos/scripts/joyempty.scr index 529da5ba..aef341ed 100644 --- a/src/test/dos/scripts/joyempty.scr +++ b/src/test/dos/scripts/joyempty.scr @@ -1,3 +1,25 @@ +# 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. + # joyempty.scr -- a game port with nothing plugged into it. 86Box only: # with a joystick configured (# JOYSTICK) but no host joystick to drive # it, 86Box has a game port whose axes never answer, like a real port diff --git a/src/test/dos/scripts/joykeys.scr b/src/test/dos/scripts/joykeys.scr index 5ef58111..87dc8a93 100644 --- a/src/test/dos/scripts/joykeys.scr +++ b/src/test/dos/scripts/joykeys.scr @@ -1,3 +1,25 @@ +# 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. + # joykeys.scr -- the joystick library with no joystick: the game port # probe finds no axes and the keyboard joystick stands in for one. # diff --git a/src/test/dos/scripts/joymove.scr b/src/test/dos/scripts/joymove.scr index 0be93454..e927f904 100644 --- a/src/test/dos/scripts/joymove.scr +++ b/src/test/dos/scripts/joymove.scr @@ -1,3 +1,25 @@ +# 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. + # joymove.scr -- the game port driver with the emulated joystick moving. # # DOSBox only: its mapper binds host keys to the emulated joystick and diff --git a/src/test/dos/scripts/joyport.scr b/src/test/dos/scripts/joyport.scr index 9051dd0a..e498de3d 100644 --- a/src/test/dos/scripts/joyport.scr +++ b/src/test/dos/scripts/joyport.scr @@ -1,3 +1,25 @@ +# 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. + # joyport.scr -- the game port driver on the emulator's game port with a # two-axis joystick configured (# JOYSTICK). DOSBox only: with no host # joystick its emulated stick rests in the middle with no button pressed diff --git a/src/test/dos/scripts/lfn.scr b/src/test/dos/scripts/lfn.scr index 5efc6244..f7912a53 100644 --- a/src/test/dos/scripts/lfn.scr +++ b/src/test/dos/scripts/lfn.scr @@ -1,3 +1,25 @@ +# 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. + # lfn.scr -- long file names and a big FAT32 disk (86Box only). # # DOSLFN gives DOS long file names; D: is an 8 GB FAT32 disk holding a diff --git a/src/test/dos/scripts/lptcovox.scr b/src/test/dos/scripts/lptcovox.scr index 5d8b3d60..ecd690ba 100644 --- a/src/test/dos/scripts/lptcovox.scr +++ b/src/test/dos/scripts/lptcovox.scr @@ -1,3 +1,25 @@ +# 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. + # lptcovox.scr -- sound through a Covox Speech Thing, recorded. # # No sound card; a Covox on LPT1 (# AUDIO-CARD). A Covox cannot be diff --git a/src/test/dos/scripts/lptdss.scr b/src/test/dos/scripts/lptdss.scr index c5dd1523..4c9a27df 100644 --- a/src/test/dos/scripts/lptdss.scr +++ b/src/test/dos/scripts/lptdss.scr @@ -1,3 +1,25 @@ +# 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. + # lptdss.scr -- sound through a Disney Sound Source, recorded. # # No sound card; a Disney Sound Source on LPT1 (# AUDIO-CARD), which diff --git a/src/test/dos/scripts/lptst86.scr b/src/test/dos/scripts/lptst86.scr index b1e24b1a..234bce2a 100644 --- a/src/test/dos/scripts/lptst86.scr +++ b/src/test/dos/scripts/lptst86.scr @@ -1,3 +1,25 @@ +# 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. + # lptst86.scr -- sound through 86Box's stereo LPT DAC, recorded. # # No sound card; 86Box's "Stereo LPT DAC" on LPT1 (# AUDIO-CARD ston1), @@ -15,6 +37,12 @@ TIMEOUT 120 WAITWIN "Program Manager" MOVE 639 479 +# Program Manager's icons are all in before the timeline starts: on this +# 486 a frame that loads one takes about 100 ms (it holds the program's +# turn that long; the shell rests between such frames while programs +# run), which with a streamed sound's own lag can pass the timeline's +# 150 ms (it did in 3 of 9 runs without this wait) +WAIT 60 LAUNCH TEST/SNDTEST.APP full WAITLOG "SNDTEST: full done" 90 QUIT diff --git a/src/test/dos/scripts/lptston.scr b/src/test/dos/scripts/lptston.scr index 3f02d6b6..8aa800bc 100644 --- a/src/test/dos/scripts/lptston.scr +++ b/src/test/dos/scripts/lptston.scr @@ -1,3 +1,25 @@ +# 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. + # lptston.scr -- sound through a Stereo-on-1, recorded. # # No sound card; a Stereo-on-1 on LPT1 (# AUDIO-CARD), which lpt.drv diff --git a/src/test/dos/scripts/menus.scr b/src/test/dos/scripts/menus.scr index 2c3d07a3..ae2666fe 100644 --- a/src/test/dos/scripts/menus.scr +++ b/src/test/dos/scripts/menus.scr @@ -1,3 +1,25 @@ +# 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. + # menus.scr -- menus and keyboard accelerators. # # Notepad: Alt+letter opens a menu, the item letter picks from it. diff --git a/src/test/dos/scripts/midiin.scr b/src/test/dos/scripts/midiin.scr index 3835a72f..c5b44a43 100644 --- a/src/test/dos/scripts/midiin.scr +++ b/src/test/dos/scripts/midiin.scr @@ -1,3 +1,25 @@ +# 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. + # midiin.scr -- a MIDI keyboard into the Sound Recorder: DVX.INI [audio] # midiIn = mpu401 opens the Sound Blaster 16's MPU-401 port for input. # MIDI Keyboard (Options, K) can be chosen; Record opens the port's diff --git a/src/test/dos/scripts/musfm.scr b/src/test/dos/scripts/musfm.scr index 9f119683..e063b84a 100644 --- a/src/test/dos/scripts/musfm.scr +++ b/src/test/dos/scripts/musfm.scr @@ -1,3 +1,25 @@ +# 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. + # musfm.scr -- a MIDI file on the FM chip (the Sound Blaster 16's OPL3), # recorded. # diff --git a/src/test/dos/scripts/musmod.scr b/src/test/dos/scripts/musmod.scr index 3f73ed9d..4e104088 100644 --- a/src/test/dos/scripts/musmod.scr +++ b/src/test/dos/scripts/musmod.scr @@ -1,3 +1,25 @@ +# 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. + # musmod.scr -- a tracker module through music.lib, recorded. # # TEST\MUSTEST.APP (src/test/dos/musTest) writes a MOD (a sine sample: diff --git a/src/test/dos/scripts/musmpu.scr b/src/test/dos/scripts/musmpu.scr index 8d1cbdc1..98d14419 100644 --- a/src/test/dos/scripts/musmpu.scr +++ b/src/test/dos/scripts/musmpu.scr @@ -1,3 +1,25 @@ +# 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. + # musmpu.scr -- a MIDI file on the MPU-401 port, recorded. # # DVX.INI [audio] midi = mpu401: TEST\MUSTEST.APP (src/test/dos/musTest) diff --git a/src/test/dos/scripts/net.scr b/src/test/dos/scripts/net.scr index 75b30eb4..aa831c9a 100644 --- a/src/test/dos/scripts/net.scr +++ b/src/test/dos/scripts/net.scr @@ -1,3 +1,25 @@ +# 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. + # net.scr -- TCP/IP through the NE2000 packet driver. # # REQUIRES: NIC diff --git a/src/test/dos/scripts/novbe.scr b/src/test/dos/scripts/novbe.scr index f3752e76..ab3fd9f3 100644 --- a/src/test/dos/scripts/novbe.scr +++ b/src/test/dos/scripts/novbe.scr @@ -1,3 +1,25 @@ +# 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. + # novbe.scr -- DVX on a video card without any VBE refuses to start. # # The GUI never comes up, so the whole script is WAITEXIT: DVX exiting is diff --git a/src/test/dos/scripts/parcp.scr b/src/test/dos/scripts/parcp.scr index bee329b1..2980f8c1 100644 --- a/src/test/dos/scripts/parcp.scr +++ b/src/test/dos/scripts/parcp.scr @@ -1,3 +1,25 @@ +# 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. + # parcp.scr -- what Control Panel does, from a BASIC program. # # TEST\PARCP.APP (fixtures/parcp) changes the screen mode and back, sets diff --git a/src/test/dos/scripts/pardemo.scr b/src/test/dos/scripts/pardemo.scr index 06e7412a..5c65ac5c 100644 --- a/src/test/dos/scripts/pardemo.scr +++ b/src/test/dos/scripts/pardemo.scr @@ -1,3 +1,25 @@ +# 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. + # pardemo.scr -- what DVX Demo shows, written in BASIC. # # TEST\PARDEMO.APP (fixtures/pardemo; its pictures in TEST\PARDEMO) has diff --git a/src/test/dos/scripts/pardsgn.scr b/src/test/dos/scripts/pardsgn.scr index 38e1cdf5..c85f57cc 100644 --- a/src/test/dos/scripts/pardsgn.scr +++ b/src/test/dos/scripts/pardsgn.scr @@ -1,3 +1,25 @@ +# 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. + # pardsgn.scr -- what a form designer and code editor written in BASIC do. # # TEST\PARDSGN.APP (fixtures/pardsgn) lists the control types and their diff --git a/src/test/dos/scripts/parhlp.scr b/src/test/dos/scripts/parhlp.scr index 66fa58d6..6e980795 100644 --- a/src/test/dos/scripts/parhlp.scr +++ b/src/test/dos/scripts/parhlp.scr @@ -1,3 +1,25 @@ +# 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. + # parhlp.scr -- what a Help Viewer written in BASIC needs. # # TEST\PARHLP.APP (fixtures/parhlp) compiles TEST\PARHLP\PARHLP.DHS with diff --git a/src/test/dos/scripts/paride.scr b/src/test/dos/scripts/paride.scr index 70dd973d..2b937a03 100644 --- a/src/test/dos/scripts/paride.scr +++ b/src/test/dos/scripts/paride.scr @@ -1,3 +1,25 @@ +# 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. + # paride.scr -- a BASIC program that compiles, debugs and builds another. # # TEST\PARIDE.APP (fixtures/paride) uses compiler.bas and debug.bas (the diff --git a/src/test/dos/scripts/parpm.scr b/src/test/dos/scripts/parpm.scr index d5c197f4..d020c3c7 100644 --- a/src/test/dos/scripts/parpm.scr +++ b/src/test/dos/scripts/parpm.scr @@ -1,3 +1,25 @@ +# 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. + # parpm.scr -- what a Program Manager written in BASIC needs. # # TEST\PARPM.APP (fixtures/parpm) scans the installed programs and reads diff --git a/src/test/dos/scripts/paths.scr b/src/test/dos/scripts/paths.scr index 32e3eec9..1bfd79a4 100644 --- a/src/test/dos/scripts/paths.scr +++ b/src/test/dos/scripts/paths.scr @@ -1,3 +1,25 @@ +# 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. + # paths.scr -- paths written with / work on DOS, paths shown use \. # # TEST/PATHSMK.APP (fixtures/pathsmk) is started by a path written with /; diff --git a/src/test/dos/scripts/pkgbuild.scr b/src/test/dos/scripts/pkgbuild.scr index 659def4e..93d89c79 100644 --- a/src/test/dos/scripts/pkgbuild.scr +++ b/src/test/dos/scripts/pkgbuild.scr @@ -1,3 +1,25 @@ +# 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. + # pkgbuild.scr -- Package Builder opens a template (a copy of DVX's own # Clock as a package of its own), shows its files where they go, and # builds the package unsigned. diff --git a/src/test/dos/scripts/pkgsign.scr b/src/test/dos/scripts/pkgsign.scr new file mode 100644 index 00000000..69b7d0a3 --- /dev/null +++ b/src/test/dos/scripts/pkgsign.scr @@ -0,0 +1,74 @@ +# 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. + +# pkgsign.scr -- Package Builder makes a key (Signing, New Key...) and +# builds a package signed with it; Software, opening that package, shows +# it as signed by the key (one of the user's own, so trusted) and installs +# it. The key pair and the signature are the package writer module's +# (LIBS\KPUNCH\PKG\WRITER\WRITER.DRV, loaded when they are first made). +# STAGE: pkgbuild/clock.ini DOWNLOAD/CLOCK.INI +# LOG: Package Builder: key made +# LOG: Package Builder: built +# LOG: Software: installed from +# FILE: USER/KEYS/TESTKEY.KEY +# FILE: USER/KEYS/TESTKEY.SEC +TIMEOUT 240 +WAITWIN "Program Manager" +MOVE 639 479 +LAUNCH apps/kpunch/pkgbuild/pkgbuild.app DOWNLOAD\CLOCK.INI +WAITWIN "Package Builder" +WAIT 10 +# The Signing page's New Key...: "Test Key", saved as USER\KEYS\TESTKEY +KEY Alt+S +WAIT 5 +KEY Alt+E +WAITWIN "New Key" +TYPE "Test Key" +KEY Enter +WAITWIN "Save the New Key As" +KEY Alt+N +TYPE "TESTKEY" +KEY Enter +WAITLOG "Package Builder: key made" +WAIT 10 +KEY Enter +WAIT 10 +# Build Package...: signed with the new key, which the list now shows +KEY Alt+B +WAIT 20 +WAITWIN "Build Package" +KEY Alt+N +TYPE "DOWNLOAD\CLOCKS.DPK" +KEY Enter +WAITLOG "Package Builder: built" +WAIT 20 +KEY Enter +# Software: the package is signed by Test Key +LAUNCH apps/kpunch/software/software.app DOWNLOAD\CLOCKS.DPK +WAITWIN "Install Software" +WAIT 10 +SHOT ps-plan +KEY Enter +WAIT 60 +KEY Enter +WAITLOG "Software: installed from" +QUIT diff --git a/src/test/dos/scripts/pkgsrv.scr b/src/test/dos/scripts/pkgsrv.scr index b0297a88..a9de12b7 100644 --- a/src/test/dos/scripts/pkgsrv.scr +++ b/src/test/dos/scripts/pkgsrv.scr @@ -1,3 +1,25 @@ +# 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. + # pkgsrv.scr -- Software and the package server on the host (server/test/ # serve.sh): the Available page read from it, Star installed from it (the # Sky it needs is installed already), then the Updates page: Sky 1.1 and diff --git a/src/test/dos/scripts/player.scr b/src/test/dos/scripts/player.scr index 69d0d418..d4acff26 100644 --- a/src/test/dos/scripts/player.scr +++ b/src/test/dos/scripts/player.scr @@ -1,3 +1,25 @@ +# 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. + # player.scr -- Media Player plays a WAV file, a module, a MIDI file, and # a VOC file. # diff --git a/src/test/dos/scripts/prdjcol.scr b/src/test/dos/scripts/prdjcol.scr index d5268a6d..8a979d44 100644 --- a/src/test/dos/scripts/prdjcol.scr +++ b/src/test/dos/scripts/prdjcol.scr @@ -1,3 +1,25 @@ +# 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. + # prdjcol.scr -- the Printer object on the DeskJet 550C: the page in # four PCL planes (black, cyan, magenta, yellow). # diff --git a/src/test/dos/scripts/print.scr b/src/test/dos/scripts/print.scr index 19b63b2d..7633e0ff 100644 --- a/src/test/dos/scripts/print.scr +++ b/src/test/dos/scripts/print.scr @@ -1,3 +1,25 @@ +# 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. + # print.scr -- print a text file from Notepad to a file. # # Notepad opens TEST\PRTEST.TXT (fixtures/files), File > Print prints it diff --git a/src/test/dos/scripts/prlj300.scr b/src/test/dos/scripts/prlj300.scr index 8f5b757a..37f0395b 100644 --- a/src/test/dos/scripts/prlj300.scr +++ b/src/test/dos/scripts/prlj300.scr @@ -1,3 +1,25 @@ +# 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. + # prlj300.scr -- the Printer object on the 300 dpi LaserJet: the page as # packed PCL rows, black only, its text in DVX's own fonts. # diff --git a/src/test/dos/scripts/progcach.scr b/src/test/dos/scripts/progcach.scr new file mode 100644 index 00000000..c6f40f6b --- /dev/null +++ b/src/test/dos/scripts/progcach.scr @@ -0,0 +1,37 @@ +# 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. + +# progcach.scr -- the program cache on DOS: Program Manager's start +# reads every program and icon and keeps them, the idle time saves +# USER\PROGRAMS.CAC a few seconds later, and a Refresh (F5) reads the +# programs again through it (each checked by its directory entry). Nothing +# fails to save or read. +# FILE: USER/PROGRAMS.CAC +# NOLOG: the program cache could not be saved +# NOLOG: is no program cache +TIMEOUT 180 +WAITWIN "Program Manager" +MOVE 639 479 +WAITMS 8000 +KEY F5 +WAITMS 2000 +QUIT diff --git a/src/test/dos/scripts/progman.scr b/src/test/dos/scripts/progman.scr index 012118f0..388bb8f7 100644 --- a/src/test/dos/scripts/progman.scr +++ b/src/test/dos/scripts/progman.scr @@ -1,3 +1,25 @@ +# 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. + # progman.scr -- Program Manager smoke test. # # The desktop comes up with its installed-program scan logged, File > Run... diff --git a/src/test/dos/scripts/prps.scr b/src/test/dos/scripts/prps.scr index ef70965f..ad060538 100644 --- a/src/test/dos/scripts/prps.scr +++ b/src/test/dos/scripts/prps.scr @@ -1,3 +1,25 @@ +# 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. + # prps.scr -- the Printer object on a color PostScript printer: the # page drawn by the printer itself (vector PostScript), its text in the # printer's Times and Helvetica. diff --git a/src/test/dos/scripts/record.scr b/src/test/dos/scripts/record.scr index 6d481f22..558d4aa5 100644 --- a/src/test/dos/scripts/record.scr +++ b/src/test/dos/scripts/record.scr @@ -1,3 +1,25 @@ +# 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. + # record.scr -- the Sound Recorder records from the sound card, plays # the recording, and saves it. # diff --git a/src/test/dos/scripts/safevid.scr b/src/test/dos/scripts/safevid.scr index 96e225d5..28d8be80 100644 --- a/src/test/dos/scripts/safevid.scr +++ b/src/test/dos/scripts/safevid.scr @@ -1,3 +1,25 @@ +# 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. + # safevid.scr -- safe video mode after a saved mode the card cannot set. # VIDEO: 4000 3000 16 TIMEOUT 120 diff --git a/src/test/dos/scripts/saver.scr b/src/test/dos/scripts/saver.scr index a5d843eb..36c9877a 100644 --- a/src/test/dos/scripts/saver.scr +++ b/src/test/dos/scripts/saver.scr @@ -1,3 +1,25 @@ +# 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. + # saver.scr -- the screen saver: after the wait with no input it takes # the whole screen, and the next input gives the screen back. # diff --git a/src/test/dos/scripts/sb1.scr b/src/test/dos/scripts/sb1.scr index 9b227fa5..503a1981 100644 --- a/src/test/dos/scripts/sb1.scr +++ b/src/test/dos/scripts/sb1.scr @@ -1,3 +1,25 @@ +# 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. + # sb1.scr -- sound through a first Sound Blaster, recorded. # # The emulators get a Sound Blaster 1.x instead of the SB16 diff --git a/src/test/dos/scripts/sb2.scr b/src/test/dos/scripts/sb2.scr index bb963a0c..7750f497 100644 --- a/src/test/dos/scripts/sb2.scr +++ b/src/test/dos/scripts/sb2.scr @@ -1,3 +1,25 @@ +# 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. + # sb2.scr -- sound through a Sound Blaster 2.0, recorded. # # The emulators get an SB 2.0 (DSP 2.01) instead of the SB16 diff --git a/src/test/dos/scripts/sbpro.scr b/src/test/dos/scripts/sbpro.scr index f3e4ce8f..5a7946b7 100644 --- a/src/test/dos/scripts/sbpro.scr +++ b/src/test/dos/scripts/sbpro.scr @@ -1,3 +1,25 @@ +# 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. + # sbpro.scr -- sound through a Sound Blaster Pro 2, recorded. # # The emulators get an SB Pro 2 (DSP 3.02) instead of the SB16 diff --git a/src/test/dos/scripts/scrpage.scr b/src/test/dos/scripts/scrpage.scr index d9e93565..abf6e737 100644 --- a/src/test/dos/scripts/scrpage.scr +++ b/src/test/dos/scripts/scrpage.scr @@ -1,3 +1,25 @@ +# 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. + # scrpage.scr -- more program screen modes and pages (fixtures/scrpage). # # TEST\SCRPAGE.APP draws on SCREEN 7's hidden page 1 while page 0 shows diff --git a/src/test/dos/scripts/scrtest.scr b/src/test/dos/scripts/scrtest.scr index 0ab4f72f..baa7db94 100644 --- a/src/test/dos/scripts/scrtest.scr +++ b/src/test/dos/scripts/scrtest.scr @@ -1,3 +1,25 @@ +# 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. + # scrtest.scr -- the BASIC program screen (fixtures/scrtest). # # TEST\SCRTEST.APP gets its screen window with CLS: console text in diff --git a/src/test/dos/scripts/sdk.scr b/src/test/dos/scripts/sdk.scr index b308e135..b096fdf6 100644 --- a/src/test/dos/scripts/sdk.scr +++ b/src/test/dos/scripts/sdk.scr @@ -1,3 +1,25 @@ +# 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. + # sdk.scr -- the SDK template projects work on DOS. # # Needs the packages sdkTest.sh builds (SDK_PKG_OUT= diff --git a/src/test/dos/scripts/sdkgame.scr b/src/test/dos/scripts/sdkgame.scr index 1175b741..f150f559 100644 --- a/src/test/dos/scripts/sdkgame.scr +++ b/src/test/dos/scripts/sdkgame.scr @@ -1,3 +1,25 @@ +# 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. + # sdkgame.scr -- the SDK's C game template plays on DOS. # # Needs the game packages sdkTest.sh builds (staged under vendor sdktest diff --git a/src/test/dos/scripts/seclink.scr b/src/test/dos/scripts/seclink.scr index fa00cb56..61c340b7 100644 --- a/src/test/dos/scripts/seclink.scr +++ b/src/test/dos/scripts/seclink.scr @@ -1,3 +1,25 @@ +# 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. + # seclink.scr -- Terminal's secure link to the host proxy, no passphrase. # # COM2 is joined to bin/host/secproxy (runDos.sh, REQUIRES: SECLINK), which diff --git a/src/test/dos/scripts/secpass.scr b/src/test/dos/scripts/secpass.scr index 7742133d..e12485e3 100644 --- a/src/test/dos/scripts/secpass.scr +++ b/src/test/dos/scripts/secpass.scr @@ -1,3 +1,25 @@ +# 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. + # secpass.scr -- Terminal's secure link to the host proxy with a passphrase. # # As seclink.scr, but the proxy has a passphrase (PASSPHRASE:) and the diff --git a/src/test/dos/scripts/shellcmd.scr b/src/test/dos/scripts/shellcmd.scr new file mode 100644 index 00000000..ab928f23 --- /dev/null +++ b/src/test/dos/scripts/shellcmd.scr @@ -0,0 +1,48 @@ +# 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. + +# shellcmd.scr -- BASIC's SHELL runs a DOS command with DVX stepped aside. +# +# TEST\SHELLRUN.APP (fixtures/shellrun) runs TEST\DOSPROG.BAT with SHELL. +# Only Program Manager (the desktop) runs besides it, so nothing is asked +# to close: DVX lets the screen and its hardware go, runs the batch file +# (it writes TEST\DOSRAN.TXT), and comes back (the log says so). The +# program waited for it and goes on: its window is still there and shows +# the exit code it got, 0 (the log too). VIDEOCHECK: the screen is what +# DVX drew. +# STAGE: dosrun/dosprog.bat test/DOSPROG.BAT +# FILE: TEST/DOSRAN.TXT +# LOG: DOS: back from +# LOG: ShellRun: SHELL returned 0 +TIMEOUT 60 +WAITWIN "Program Manager" +MOVE 639 479 +LAUNCH TEST/SHELLRUN.APP +WAITLOG "ShellRun: ShellRunDosWait returned 0" +WAITLOG "ShellRun: SHELL returned" +WAITWIN "Shell Run" +WAIT 20 +VIDEOCHECK +SHOT sc-back +KEY Alt+F4 +WAITNOWIN "Shell Run" +QUIT diff --git a/src/test/dos/scripts/sndbas.scr b/src/test/dos/scripts/sndbas.scr index 169624ad..f98e6235 100644 --- a/src/test/dos/scripts/sndbas.scr +++ b/src/test/dos/scripts/sndbas.scr @@ -1,3 +1,25 @@ +# 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. + # sndbas.scr -- sound from BASIC (sound.bas). # # TEST\SNDBAS.APP (fixtures/sndbas) plays a tone with PlayTone, waits for diff --git a/src/test/dos/scripts/sndload.scr b/src/test/dos/scripts/sndload.scr index 3a035952..f9a77374 100644 --- a/src/test/dos/scripts/sndload.scr +++ b/src/test/dos/scripts/sndload.scr @@ -1,3 +1,25 @@ +# 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. + # sndload.scr -- how much of a 486 the sound takes (a measurement). # # A Gravis UltraSound alone (# AUDIO-CARD, recorded but not compared). diff --git a/src/test/dos/scripts/sndmore.scr b/src/test/dos/scripts/sndmore.scr index 3f2b38c9..ade6a718 100644 --- a/src/test/dos/scripts/sndmore.scr +++ b/src/test/dos/scripts/sndmore.scr @@ -1,3 +1,25 @@ +# 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. + # sndmore.scr -- the round-2 sound.bas calls on DOS. # # TEST\SNDMORE.APP (fixtures/sndmore) makes a sound from binary data with diff --git a/src/test/dos/scripts/software.scr b/src/test/dos/scripts/software.scr index 87f39ea1..b3d08742 100644 --- a/src/test/dos/scripts/software.scr +++ b/src/test/dos/scripts/software.scr @@ -1,3 +1,25 @@ +# 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. + # software.scr -- Software installs a package opened with it, with the # package it needs from the same folder (a theme that needs another), then # removes the needed one, which takes the other with it. diff --git a/src/test/dos/scripts/softwave.scr b/src/test/dos/scripts/softwave.scr index 4cdf515b..74a34465 100644 --- a/src/test/dos/scripts/softwave.scr +++ b/src/test/dos/scripts/softwave.scr @@ -1,3 +1,25 @@ +# 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. + # softwave.scr -- a MIDI file on the SoundFont software synthesizer, # recorded. # diff --git a/src/test/dos/scripts/sound.scr b/src/test/dos/scripts/sound.scr index 9c921f7d..fe27f9b9 100644 --- a/src/test/dos/scripts/sound.scr +++ b/src/test/dos/scripts/sound.scr @@ -1,3 +1,25 @@ +# 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. + # sound.scr -- sound through the Sound Blaster driver, recorded. # # TEST\SNDTEST.APP (src/test/dos/sndTest) plays its "full" timeline: a diff --git a/src/test/dos/scripts/sound2.scr b/src/test/dos/scripts/sound2.scr index 03ccc010..879680c8 100644 --- a/src/test/dos/scripts/sound2.scr +++ b/src/test/dos/scripts/sound2.scr @@ -1,3 +1,25 @@ +# 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. + # sound2.scr -- two programs playing at once through one mixer, recorded. # # Two copies of TEST\SNDTEST.APP: "a" sets the start time and plays the diff --git a/src/test/dos/scripts/speaker.scr b/src/test/dos/scripts/speaker.scr index d606b223..3eec3203 100644 --- a/src/test/dos/scripts/speaker.scr +++ b/src/test/dos/scripts/speaker.scr @@ -1,3 +1,25 @@ +# 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. + # speaker.scr -- tones on the PC speaker, recorded. # # DVX.INI names the PC speaker driver (# INI), which plays tones on the diff --git a/src/test/dos/scripts/spkpcm.scr b/src/test/dos/scripts/spkpcm.scr index a4de4beb..0fd31fe6 100644 --- a/src/test/dos/scripts/spkpcm.scr +++ b/src/test/dos/scripts/spkpcm.scr @@ -1,3 +1,25 @@ +# 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. + # spkpcm.scr -- sampled sound on the PC speaker (DVX.INI speakerPcm), # recorded. # diff --git a/src/test/dos/scripts/sql.scr b/src/test/dos/scripts/sql.scr index 13171388..752ac92e 100644 --- a/src/test/dos/scripts/sql.scr +++ b/src/test/dos/scripts/sql.scr @@ -1,9 +1,35 @@ +# 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. + # sql.scr -- SQLite from a BASIC program, through a DOUBLE extern. # # TEST\SQLSMK.APP (fixtures/sqlsmk) creates TEST\SMOKE.DB, inserts three # rows in one transaction, reads them back and sums a REAL column with -# SQLFieldDbl (a DECLARE LIBRARY function returning DOUBLE); its window -# title reports "SQL PASS 3 rows 3.25" only when every step worked. +# SQLFieldDbl (a DECLARE LIBRARY function returning DOUBLE). Then a long +# statement, during which the program's Timer events run (and are refused +# the busy database), and a Data control that reads 1000 records a part +# at a time (64 at Refresh) and walks them with Do Until EOF. Its window +# title reports "SQL PASS 3 rows 3.25 long data 64/1000 loop 1000" only +# when every step worked. TIMEOUT 120 WAITWIN "Program Manager" MOVE 639 479 @@ -11,7 +37,7 @@ LAUNCH TEST/SQLSMK.APP WAITWIN "SQL *" WAIT 10 SHOT sqlsmk -ASSERTWIN "SQL PASS 3 rows 3.25" +ASSERTWIN "SQL PASS 3 rows 3.25 long data 64/1000 loop 1000" KEY Alt+F4 WAITNOWIN "SQL *" QUIT diff --git a/src/test/dos/scripts/svdbg.scr b/src/test/dos/scripts/svdbg.scr index f29a6c8b..7542e991 100644 --- a/src/test/dos/scripts/svdbg.scr +++ b/src/test/dos/scripts/svdbg.scr @@ -1,3 +1,25 @@ +# 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. + # svdbg.scr -- TEMPORARY: how long a full-screen saver takes to close. TIMEOUT 120 WAITWIN "Program Manager" diff --git a/src/test/dos/scripts/telnet.scr b/src/test/dos/scripts/telnet.scr index 9260ef6f..e3fee62e 100644 --- a/src/test/dos/scripts/telnet.scr +++ b/src/test/dos/scripts/telnet.scr @@ -1,3 +1,25 @@ +# 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. + # telnet.scr -- a Telnet session in Terminal over the network. # # REQUIRES: NIC diff --git a/src/test/dos/scripts/terminal.scr b/src/test/dos/scripts/terminal.scr index 141c0b0c..54ffbf36 100644 --- a/src/test/dos/scripts/terminal.scr +++ b/src/test/dos/scripts/terminal.scr @@ -1,3 +1,25 @@ +# 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. + # terminal.scr -- Terminal smoke test. # # Terminal starts with its default session (a fresh TERMINAL.INI), the diff --git a/src/test/dos/scripts/ucdemo.scr b/src/test/dos/scripts/ucdemo.scr index 3f8579b8..33a62e24 100644 --- a/src/test/dos/scripts/ucdemo.scr +++ b/src/test/dos/scripts/ucdemo.scr @@ -1,3 +1,25 @@ +# 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. + # ucdemo.scr -- a program using a control written in BASIC that is # installed in WIDGETS. # diff --git a/src/test/dos/scripts/ucide.scr b/src/test/dos/scripts/ucide.scr index 6ba77ca6..7913374a 100644 --- a/src/test/dos/scripts/ucide.scr +++ b/src/test/dos/scripts/ucide.scr @@ -1,3 +1,25 @@ +# 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. + # ucide.scr -- UserControls in the DVX BASIC IDE. # # TEST\UCIDE\UCIDE.DBP (fixtures/ucide) is a program with a control of @@ -9,7 +31,9 @@ # TreeView and VBox; clicking it and then the form places a second one # ("UCIDE INIT 2 DESIGN"). F5 runs the program: its own UcMeter # (UserMode True, "UCIDE INIT 1 RUN"), Value 9 from the .frm in the -# caption ("UCIDE PASS 9"). +# caption ("UCIDE PASS 9"). The program runs on a task of its own, so +# the shell's main loop goes on meanwhile: the Clock, started and closed +# while it runs, is reaped at once. # # The click positions are frame-relative (the IDE's Project window and # Toolbox at 800 x 600). The Toolbox position was read off the ucide-tbx @@ -34,9 +58,9 @@ WAITMS 5000 MOVE 799 599 WAIT 20 SHOT ucide-tbx -# The user control's tool: row 9, column 3 (the tools are in name order, -# so a new widget moves it). -CLICK "Toolbox" 92 301 +# The user control's tool: row 10, column 1 (the tools are in name order, +# so a new widget, or one newly placeable, moves it). +CLICK "Toolbox" 28 333 WAIT 5 CLICK "UCIDE [Design]" C C WAITLOG "UCIDE INIT 2 DESIGN" @@ -48,6 +72,13 @@ WAITWIN "UCIDE PASS 9" WAITLOG "UCIDE INIT 1 RUN" WAIT 10 SHOT ucide-run +LAUNCH apps/kpunch/clock/clock.app +WAITWIN "Clock" +WAIT 10 +KEY Alt+F4 +WAITLOG "Shell: reaped app 'Clock'" +CLICK "UCIDE PASS 9" 60 8 +WAIT 5 KEY Alt+F4 WAITNOWIN "UCIDE PASS 9" QUIT diff --git a/src/test/dos/scripts/upgdrop.scr b/src/test/dos/scripts/upgdrop.scr index 050ae7a0..128e5b51 100644 --- a/src/test/dos/scripts/upgdrop.scr +++ b/src/test/dos/scripts/upgdrop.scr @@ -1,21 +1,45 @@ +# 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. + # upgdrop.scr -- SETUP.EXE upgrades DVX with parts left out: they go. # # Setup first installs all of DVX from the CD image (# SETUPFIRST: # /PARTS=ALL), then upgrades that folder with /PARTS=COMM: Communications -# and the Networking part it needs stay, the other parts are passed over -# while unpacking and removed (their files, folders, and records, which -# the folder's SETUP.LST names). The settings are kept, so the first-run -# Welcome window still opens, without its DVX BASIC and DVX Demo tiles; -# Software lists DVX, its fonts, Communications, and Networking only. -# On the disk: the parts' folders, their DOS programs in SYSTEM, and their -# records gone; DVX's own DVXPKG.EXE and the kept parts' files there. +# stays, the other parts (Networking too: Communications works without +# it, Telnet aside) are passed over while unpacking and removed (their +# files, folders, and records, which the folder's SETUP.LST names). The +# settings are kept, so the first-run Welcome window still opens, without +# its DVX BASIC and DVX Demo tiles; Software lists DVX, its fonts, and +# Communications only. On the disk: the parts' folders, their DOS +# programs in SYSTEM, and their records gone; DVX's own DVXPKG.EXE and the +# kept part's files there. The Terminal starts without the networking +# library. # Setup's pages are install.scr's and upgrade.scr's (# SETUPSHOTS: no). # INSTALL: cd # SETUPSHOTS: no # SETUPFIRST: /PARTS=ALL # SETUPARGS: /PARTS=COMM # SETUPLOG: installing in C:\DVX (upgrade, settings kept) -# SETUPLOG: parts kpunch.net kpunch.comm +# SETUPLOG: parts kpunch.comm # NOFILE: APPS/KPUNCH/DVXBASIC # NOFILE: LIBS/KPUNCH/DVXSQL # NOFILE: SYSTEM/BASCOMP.EXE @@ -26,7 +50,7 @@ # NOFILE: APPS/KPUNCH/HELPEDIT # FILE: SYSTEM/DVXPKG.EXE # FILE: APPS/KPUNCH/TERMINAL/TERMINAL.APP -# FILE: LIBS/KPUNCH/DVXNET +# NOFILE: LIBS/KPUNCH/DVXNET # FILE: SYSTEM/PACKAGES/KPUNCH/COMM.INI # LOG: DVX Shell ready. TIMEOUT 400 @@ -46,7 +70,8 @@ SHOT ud-sw KEY Alt+F4 WAITNOWIN "Software" -# Communications kept: Terminal starts (no port is opened) +# Communications kept: Terminal starts without the networking library +# (no port is opened) LAUNCH apps/kpunch/terminal/terminal.app WAITLOG "Terminal: started with session Direct COM1" WAITWIN "Direct COM1" diff --git a/src/test/dos/scripts/upgrade.scr b/src/test/dos/scripts/upgrade.scr index 2e23fa00..64118790 100644 --- a/src/test/dos/scripts/upgrade.scr +++ b/src/test/dos/scripts/upgrade.scr @@ -1,3 +1,25 @@ +# 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. + # upgrade.scr -- SETUP.EXE run over an installed DVX: Upgrade or repair, # settings kept. # diff --git a/src/test/dos/scripts/video.scr b/src/test/dos/scripts/video.scr index 6fe98209..45094ff5 100644 --- a/src/test/dos/scripts/video.scr +++ b/src/test/dos/scripts/video.scr @@ -1,3 +1,25 @@ +# 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. + # video.scr -- switch the video mode live from the Control Panel. # # Video tab, pick the first 800x600 mode (type-ahead '8' in the mode diff --git a/src/test/dos/scripts/welcome.scr b/src/test/dos/scripts/welcome.scr index 57f03f3c..11cbb120 100644 --- a/src/test/dos/scripts/welcome.scr +++ b/src/test/dos/scripts/welcome.scr @@ -1,3 +1,25 @@ +# 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. + # welcome.scr -- the Welcome window. # # Started with LAUNCH (the harness removes [shell] welcome from the staged diff --git a/src/test/dos/scripts/winops.scr b/src/test/dos/scripts/winops.scr index ca805c7f..3424e8ad 100644 --- a/src/test/dos/scripts/winops.scr +++ b/src/test/dos/scripts/winops.scr @@ -1,3 +1,25 @@ +# 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. + # winops.scr -- move, resize, maximize, restore and minimize a window. # # Notepad opens at 80,80 (480x320). Its title bar is dragged, its diff --git a/src/test/dos/scripts/wss.scr b/src/test/dos/scripts/wss.scr index 2c80a230..67520b27 100644 --- a/src/test/dos/scripts/wss.scr +++ b/src/test/dos/scripts/wss.scr @@ -1,3 +1,25 @@ +# 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. + # wss.scr -- sound through a Windows Sound System board, recorded. # # 86Box's Windows Sound System (# AUDIO-CARD): an AD1848 codec behind diff --git a/src/test/dos/scripts/xferperf.scr b/src/test/dos/scripts/xferperf.scr index c09a47f0..9305a5c1 100644 --- a/src/test/dos/scripts/xferperf.scr +++ b/src/test/dos/scripts/xferperf.scr @@ -1,3 +1,25 @@ +# 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. + # xferperf.scr -- ZMODEM transfers in Terminal over the secure link, timed. # # COM2 is joined to bin/host/secproxy (REQUIRES: SECLINK), which talks to diff --git a/src/test/dos/sndTest/sndTest.c b/src/test/dos/sndTest/sndTest.c index ae8979bd..cc5ac7b2 100644 --- a/src/test/dos/sndTest/sndTest.c +++ b/src/test/dos/sndTest/sndTest.c @@ -46,6 +46,7 @@ // 500-2000; b: 784 Hz 900-1700. #include "dvxApp.h" +#include "dvxEndian.h" #include "audio/audio.h" #include "shellApp.h" #include "taskSwch.h" @@ -154,8 +155,6 @@ static uint32_t sStreamStep = 0; static void addStep(const char *name, StepKindE kind, AudioWaveE wave, uint32_t hz, uint32_t startMs, uint32_t lengthMs); static int64_t nowMs(void); static void onEnd(AudioCtxT *ctx, int32_t voice, AudioEndE reason, void *userData); -static void put16(uint8_t *p, uint32_t v); -static void put32(uint8_t *p, uint32_t v); static bool readEpoch(void); static int32_t runTimeline(AudioCtxT *ctx); static int32_t squareSample(uint32_t *phase, uint32_t step); @@ -209,18 +208,6 @@ static void onEnd(AudioCtxT *ctx, int32_t voice, AudioEndE reason, void *userDat } -static void put16(uint8_t *p, uint32_t v) { - p[0] = (uint8_t)(v & SND_BYTE_MASK); - p[1] = (uint8_t)((v >> SND_BYTE_BITS) & SND_BYTE_MASK); -} - - -static void put32(uint8_t *p, uint32_t v) { - put16(p, v & 0xFFFF); - put16(p + 2, v >> 16); -} - - // "b": E as program "a" wrote it. static bool readEpoch(void) { uint64_t until = platformMicros() + (uint64_t)SND_SYNC_WAIT_MS * SND_US_PER_MS; @@ -247,8 +234,9 @@ static bool readEpoch(void) { } -// Start every step when its time comes, close the stream once it has -// played out, and check each end against its plan. +// Start every step when its time comes, close the stream once its voice +// has ended (it ends by itself: audioStreamEnd), and check each end +// against its plan. static int32_t runTimeline(AudioCtxT *ctx) { uint32_t lastEnd = 0; int32_t problems = 0; @@ -266,15 +254,11 @@ static int32_t runTimeline(AudioCtxT *ctx) { } } - if (sStream && sStreamLeft == 0 && audioStreamQueued(sStream) == 0) { - for (int32_t i = 0; i < sStepCount; i++) { - if (sSteps[i].kind == STEP_STREAM) { - sSteps[i].endedMs = nowMs(); - } + for (int32_t i = 0; sStream && i < sStepCount; i++) { + if (sSteps[i].kind == STEP_STREAM && sSteps[i].endedMs >= 0) { + audioStreamClose(sStream); + sStream = NULL; } - - audioStreamClose(sStream); - sStream = NULL; } audioPump(); @@ -326,6 +310,7 @@ static void startStep(AudioCtxT *ctx, StepT *s) { sStreamPhase = 0; sStreamStep = (uint32_t)(((uint64_t)s->hz << SND_PHASE_SHIFT) / SND_STREAM_RATE); s->voice = audioStreamOpen(ctx, &fmt, SND_STREAM_MS, streamFill, NULL, &sStream); + s->voice = s->voice == AUDIO_OK ? audioStreamVoice(sStream) : s->voice; break; } @@ -335,12 +320,12 @@ static void startStep(AudioCtxT *ctx, StepT *s) { } -// Pull stream: the square wave until its length is used up, then nothing. +// Pull stream: the square wave until its length is used up; the last of +// it ends the stream (audioStreamEnd), whose voice then ends as a sound's. static uint32_t streamFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData) { int16_t *out = (int16_t *)buf; uint32_t n = frames < sStreamLeft ? frames : sStreamLeft; - (void)stream; (void)userData; for (uint32_t i = 0; i < n; i++) { @@ -351,6 +336,11 @@ static uint32_t streamFill(AudioStreamT *stream, void *buf, uint32_t frames, voi } sStreamLeft -= n; + + if (sStreamLeft == 0) { + audioStreamEnd(stream); + } + return n; } @@ -385,36 +375,36 @@ static bool writeFiles(void) { if (ok) { memcpy(wav, "RIFF", 4); - put32(wav + WAV_RIFF_SIZE, wavBytes - WAV_RIFF_SIZE_BASE); + dvxPutLe32(wav + WAV_RIFF_SIZE, wavBytes - WAV_RIFF_SIZE_BASE); memcpy(wav + WAV_FORM, "WAVEfmt ", 8); - put32(wav + WAV_FMT_SIZE, WAV_FMT_BYTES); - put16(wav + WAV_FMT_TAG, WAV_TAG_PCM); - put16(wav + WAV_FMT_CHANNELS, 1); - put32(wav + WAV_FMT_RATE, SND_WAV_RATE); - put32(wav + WAV_FMT_BYTE_RATE, SND_WAV_RATE); - put16(wav + WAV_FMT_ALIGN, 1); - put16(wav + WAV_FMT_BITS, 8); + dvxPutLe32(wav + WAV_FMT_SIZE, WAV_FMT_BYTES); + dvxPutLe16(wav + WAV_FMT_TAG, WAV_TAG_PCM); + dvxPutLe16(wav + WAV_FMT_CHANNELS, 1); + dvxPutLe32(wav + WAV_FMT_RATE, SND_WAV_RATE); + dvxPutLe32(wav + WAV_FMT_BYTE_RATE, SND_WAV_RATE); + dvxPutLe16(wav + WAV_FMT_ALIGN, 1); + dvxPutLe16(wav + WAV_FMT_BITS, 8); memcpy(wav + WAV_DATA_ID, "data", 4); - put32(wav + WAV_DATA_SIZE, wavFrames); + dvxPutLe32(wav + WAV_DATA_SIZE, wavFrames); for (uint32_t i = 0; i < wavFrames; i++) { wav[WAV_HEADER_BYTES + i] = (uint8_t)((squareSample(&phase, step) >> SND_U8_SHIFT) + SND_U8_BIAS); } memcpy(voc, "Creative Voice File\x1A", VOC_SIGNATURE_BYTES); - put16(voc + VOC_HEADER_SIZE, SND_VOC_HEADER); - put16(voc + VOC_VERSION, SND_VOC_VERSION); - put16(voc + VOC_CHECKSUM, (~SND_VOC_VERSION + SND_VOC_MAGIC) & 0xFFFF); + dvxPutLe16(voc + VOC_HEADER_SIZE, SND_VOC_HEADER); + dvxPutLe16(voc + VOC_VERSION, SND_VOC_VERSION); + dvxPutLe16(voc + VOC_CHECKSUM, (~SND_VOC_VERSION + SND_VOC_MAGIC) & 0xFFFF); uint8_t *b = voc + SND_VOC_HEADER; b[0] = SND_VOC_NEW_SOUND; - put16(b + 1, blockLen & 0xFFFF); + dvxPutLe16(b + 1, blockLen & 0xFFFF); b[3] = (uint8_t)((blockLen >> 16) & SND_BYTE_MASK); - put32(b + VOC_BLOCK_HEADER, SND_VOC_RATE); + dvxPutLe32(b + VOC_BLOCK_HEADER, SND_VOC_RATE); b[VOC_BLOCK_HEADER + VOC_NEW_BITS] = 16; b[VOC_BLOCK_HEADER + VOC_NEW_CHANNELS] = 2; - put16(b + VOC_BLOCK_HEADER + VOC_NEW_CODEC, SND_VOC_CODEC_PCM16); + dvxPutLe16(b + VOC_BLOCK_HEADER + VOC_NEW_CODEC, SND_VOC_CODEC_PCM16); phase = 0; step = (uint32_t)(((uint64_t)SND_VOC_HZ << SND_PHASE_SHIFT) / SND_VOC_RATE); @@ -422,8 +412,8 @@ static bool writeFiles(void) { uint32_t v = (uint32_t)squareSample(&phase, step) & 0xFFFF; uint8_t *at = b + VOC_BLOCK_HEADER + SND_VOC_NEW_HEADER + i * SND_VOC_FRAME_BYTES; - put16(at, v); - put16(at + 2, v); + dvxPutLe16(at, v); + dvxPutLe16(at + 2, v); } FILE *wf = fopen(SND_WAV_FILE, "wb"); diff --git a/src/test/dvxtest/checkStatics.sh b/src/test/dvxtest/checkStatics.sh index 3c5c9a05..78bb35ff 100755 --- a/src/test/dvxtest/checkStatics.sh +++ b/src/test/dvxtest/checkStatics.sh @@ -1,4 +1,26 @@ #!/bin/sh +# 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. + # checkStatics.sh -- every file-scope variable in the libdvx widget/app/ # dialog sources must be handled by a test reset hook or be listed in # staticsAllowlist.txt (process-lifetime tables). Guards the host test diff --git a/src/test/dvxtest/dvxtest.c b/src/test/dvxtest/dvxtest.c index c0d2933b..fb78a38b 100644 --- a/src/test/dvxtest/dvxtest.c +++ b/src/test/dvxtest/dvxtest.c @@ -25,6 +25,7 @@ #include "dvxtest.h" #include "dvxWgtP.h" #include "dvxVideo.h" +#include "dvxHash.h" #include "checkbox/checkbox.h" #include "listBox/listBox.h" #include "slider/slider.h" @@ -70,8 +71,6 @@ #define FKEY_F12 12 // FNV-1a parameters for the golden hash printed on mismatch. -#define FNV_OFFSET 2166136261u -#define FNV_PRIME 16777619u // Spy target kinds. #define SPY_KIND_WIDGET 0 @@ -311,26 +310,27 @@ static void caseTimeout(int sig) { } -// The dialog script's idle hook: answer the modal dialog; with none up, -// run the idle callback it replaced (a shell's yields to the app tasks, -// which a program's own event loop relies on while a script is set). -// Not both: another task's idle would see the answered dialog before its -// own loop closed it and answer it again. +// The dialog script's idle hook: answer the modal dialog, then run the +// idle callback it replaced (a shell's yields to the app tasks: a program +// on its own task runs the click on its dialog, and closes it, only +// then). Another task's frame may run this before that program's loop +// has closed the dialog it answered, so a script answers a window once +// until it closes (shellFixture.c's, testTermApp.c's). static void dialogIdle(void *userCtx) { AppContextT *ctx = (AppContextT *)userCtx; - if (ctx->modalWindow) { - if (sDialogFn) { - sDialogFn(ctx, ctx->modalWindow, sDialogCtx); - } - } else if (sDialogChain) { + if (ctx->modalWindow && sDialogFn) { + sDialogFn(ctx, ctx->modalWindow, sDialogCtx); + } + + if (sDialogChain) { sDialogChain(sDialogChainCtx); } } static int32_t fnvHashRegion(const DisplayT *d, const uint8_t *buf, int32_t pitch, int32_t w, int32_t h, uint32_t *outHash) { - uint32_t hash = FNV_OFFSET; + uint32_t hash = DVX_FNV_BASIS; int32_t bpp = d->format.bytesPerPixel; for (int32_t y = 0; y < h; y++) { @@ -342,9 +342,9 @@ static int32_t fnvHashRegion(const DisplayT *d, const uint8_t *buf, int32_t pitc uint8_t b; unpackColor(d, readPixel(row + (size_t)x * (size_t)bpp, bpp), &r, &g, &b); - hash = (hash ^ r) * FNV_PRIME; - hash = (hash ^ g) * FNV_PRIME; - hash = (hash ^ b) * FNV_PRIME; + hash = dvxFnvByte(hash, r); + hash = dvxFnvByte(hash, g); + hash = dvxFnvByte(hash, b); } } diff --git a/src/test/dvxtest/dvxtest.mk b/src/test/dvxtest/dvxtest.mk index 5aa7b027..ab85cd6c 100644 --- a/src/test/dvxtest/dvxtest.mk +++ b/src/test/dvxtest/dvxtest.mk @@ -95,7 +95,7 @@ DVXTEST_HLPC := $(DVXTEST_ROOT)/bin/host/dvxhlpc DVXTEST_HELP_SRC := $(DVXTEST_ROOT)/src/test/tools/fixtures/help DVXTEST_HELP_HLP := $(DVXTEST_BINDIR)/help/fixture.hlp -DVXTEST_CFLAGS := $(HOST_CFLAGS) $(DVXTEST_INCLUDES) -DDVXTEST_WIDGET_DIR='"$(DVXTEST_WIDGET_DIR)"' -DDVXTEST_GOLDEN_DIR='"$(DVXTEST_GOLDEN_DIR)"' -DDVXTEST_HELP_HLP='"$(DVXTEST_HELP_HLP)"' +DVXTEST_CFLAGS := $(HOST_CFLAGS) $(DVXTEST_INCLUDES) -DDVXTEST_WIDGET_DIR='"$(DVXTEST_WIDGET_DIR)"' -DDVXTEST_GOLDEN_DIR='"$(DVXTEST_GOLDEN_DIR)"' -DDVXTEST_HELP_HLP='"$(DVXTEST_HELP_HLP)"' -DDVXTEST_FONT_FILES='"$(DVXTEST_FONTS)"' DVXTEST_LDFLAGS := -rdynamic $(HOST_LDFLAGS) DVXTEST_LINK := $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_LIBDVX_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm diff --git a/src/test/dvxtest/hostLookup.c b/src/test/dvxtest/hostLookup.c new file mode 100644 index 00000000..ea638fd4 --- /dev/null +++ b/src/test/dvxtest/hostLookup.c @@ -0,0 +1,91 @@ +// 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. + +// hostLookup.c -- dvxnet's lookups that do not wait (dvxNet.h) for the +// host builds of code that uses them (http, conn): the host's sockets are +// its own, so a lookup is the host's getaddrinfo, made at the first look +// (inside a poll, as DVX's own lookup would be under way; code a suite +// set with tDuringLookup runs there). + +#include "dvxnet/dvxNet.h" + +#include +#include +#include +#include +#include + +// ============================================================ +// Prototypes +// ============================================================ + +void netLookupCancel(NetLookupT *lk); +int32_t netLookupPoll(NetLookupT *lk, uint32_t *addr); +void netLookupStart(NetLookupT *lk, const char *name); + + +void netLookupCancel(NetLookupT *lk) { + if (lk->result == NET_LOOKUP_BUSY) { + lk->result = EAI_AGAIN; + } +} + + +int32_t netLookupPoll(NetLookupT *lk, uint32_t *addr) { + if (lk->result == NET_LOOKUP_BUSY) { + struct addrinfo hints; + struct addrinfo *found = NULL; + int rc; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + rc = getaddrinfo(lk->name, NULL, &hints, &found); + + if (rc == 0 && found) { + lk->addr = ((const struct sockaddr_in *)found->ai_addr)->sin_addr.s_addr; + lk->result = 0; + } else { + lk->result = rc ? rc : EAI_NONAME; + } + + if (found) { + freeaddrinfo(found); + } + } + + if (lk->result == 0) { + *addr = lk->addr; + } + + return lk->result; +} + + +void netLookupStart(NetLookupT *lk, const char *name) { + memset(lk, 0, sizeof(*lk)); + lk->result = NET_LOOKUP_BUSY; + + if (!name || !name[0] || snprintf(lk->name, sizeof(lk->name), "%s", name) >= (int)sizeof(lk->name)) { + lk->result = EAI_NONAME; + } +} diff --git a/src/test/dvxtest/lookupHook.c b/src/test/dvxtest/lookupHook.c new file mode 100644 index 00000000..18addbc7 --- /dev/null +++ b/src/test/dvxtest/lookupHook.c @@ -0,0 +1,70 @@ +// 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. + +// lookupHook.c -- run test code inside a name lookup (see lookupHook.h) + +#include "lookupHook.h" + +#include +#include + +// ============================================================ +// Prototypes +// ============================================================ + +int __real_getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); +int __wrap_getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); +void tDuringLookup(void (*fn)(void *ctx), void *ctx); +bool tLookupPending(void); + +// ============================================================ +// Module state +// ============================================================ + +static void (*sFn)(void *ctx) = NULL; +static void *sCtx = NULL; + + +// The linker's name for the wrapped getaddrinfo (--wrap): the code set +// runs once, then the real lookup. +int __wrap_getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res) { + void (*fn)(void *ctx) = sFn; + + sFn = NULL; + + if (fn) { + fn(sCtx); + } + + return __real_getaddrinfo(node, service, hints, res); +} + + +void tDuringLookup(void (*fn)(void *ctx), void *ctx) { + sFn = fn; + sCtx = ctx; +} + + +bool tLookupPending(void) { + return sFn != NULL; +} diff --git a/src/test/dvxtest/lookupHook.h b/src/test/dvxtest/lookupHook.h new file mode 100644 index 00000000..e74ddd0e --- /dev/null +++ b/src/test/dvxtest/lookupHook.h @@ -0,0 +1,42 @@ +// 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. + +// lookupHook.h -- run test code inside a name lookup +// +// A suite linked with lookupHook.c and -Wl,--wrap=getaddrinfo gets a +// getaddrinfo that first runs the code set here (once), then the real +// one: what other programs' code may do while DVX's own lookup waits for +// the network (it yields), so a suite can free or cancel a request whose +// poll is under way. + +#ifndef LOOKUP_HOOK_H +#define LOOKUP_HOOK_H + +#include + +// fn runs, given ctx, inside the next name lookup (NULL: nothing does). +void tDuringLookup(void (*fn)(void *ctx), void *ctx); + +// True while code set with tDuringLookup has not run yet. +bool tLookupPending(void); + +#endif diff --git a/src/test/fileops/fileMgrTest.c b/src/test/fileops/fileMgrTest.c index 955e8db8..f78de89a 100644 --- a/src/test/fileops/fileMgrTest.c +++ b/src/test/fileops/fileMgrTest.c @@ -80,9 +80,10 @@ typedef struct { const char *text; const char *check; const char *button; - const char *expectLabel; // a label the dialog must show + const char *expectLabel; // a label the dialog must show; it is answered once it does int32_t answered; int32_t sawLabel; + int32_t answeredId; // the dialog answered last: up until its program's task runs } ScriptT; // ============================================================ @@ -142,12 +143,12 @@ static void caseBrowse(void) { wgtSetFocused(list); wgtListViewSetSelected(list, 0); tWidgetKey(win, KEY_ENTER, 0); - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); TEST_EQ(wgtListViewGetRowCount(list), 1); TEST_STR_EQ(wgtListViewGetCell(list, 0, 0), "inner.txt"); TEST_TRUE(strstr(win->title, "sub") != NULL); tWidgetKey(win, KEY_BACKSPACE, 0); - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); TEST_EQ(wgtListViewGetRowCount(list), 3); // Sort by size, then descending (folders stay first). @@ -176,7 +177,7 @@ static void caseBrowse(void) { // F5 re-reads the folder. TEST_TRUE(tWriteFile("data/gamma.txt", "g", 1)); tFKey(5); - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); TEST_EQ(wgtListViewGetRowCount(list), 3); closeManagers(); @@ -211,6 +212,7 @@ static void caseCopyPaste(void) { selectOnly(listA, "alpha.txt"); wgtListViewSetItemSelected(listA, rowOf(listA, "sub"), true); tWidgetKey(winA, KEY_CTRL_C, KEY_MOD_CTRL); + fixPumpSettled(); int32_t len = 0; const char *clip = dvxClipboardGet(&len); @@ -241,6 +243,7 @@ static void caseCopyPaste(void) { wgtSetFocused(listA); selectOnly(listA, "beta.bat"); tWidgetKey(winA, KEY_CTRL_X, KEY_MOD_CTRL); + fixPumpSettled(); menu(winB, MENU_PASTE); pumpUntilNoWindow("Moving"); TEST_TRUE(exists("dest/beta.bat")); @@ -276,7 +279,7 @@ static void caseCopyPaste(void) { int32_t before = shellRunningAppCount(); menu(winA, MENU_NEW_WINDOW); - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); TEST_EQ(shellRunningAppCount(), before + 1); closeManagers(); @@ -305,6 +308,7 @@ static void caseErrors(void) { wgtSetText(path, "no/such/folder"); wgtSetFocused(path); tWidgetKey(win, KEY_ENTER, 0); + fixPumpSettled(); TEST_EQ(s.answered, 1); TEST_TRUE(strstr(win->title, "data") != NULL); @@ -375,6 +379,7 @@ static void caseFolderOps(void) { selectOnly(list, "alpha.txt"); scriptOn(&s, NULL, NULL, NULL, "No"); tWidgetKey(win, KEY_DELETE, 0); + fixPumpSettled(); fixDialogsOff(); TEST_TRUE(exists("data/alpha.txt")); @@ -384,7 +389,7 @@ static void caseFolderOps(void) { static void caseKeysAndProperties(void) { - tCase("properties: size shown, read-only set"); + tCase("properties: size and contents counted, read-only set"); fixShellUp(SCREEN_BPP); makeData(); @@ -417,12 +422,21 @@ static void caseKeysAndProperties(void) { // Alt+Enter opens the same dialog; Cancel changes nothing. scriptOn(&s, NULL, NULL, "Read-only", "Cancel"); tKey(KEY_SCAN_ENTER, 0, KEY_MOD_ALT); - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); fixDialogsOff(); TEST_EQ(s.answered, 1); TEST_EQ(stat("data/alpha.txt", &st), 0); TEST_EQ(st.st_mode & S_IWUSR, 0); + // A folder: what is in it, the folder itself among the folders + selectOnly(list, "sub"); + scriptOn(&s, NULL, NULL, NULL, "Cancel"); + s.expectLabel = "Contains: 1 file, 1 folder"; + menu(win, MENU_PROPERTIES); + fixDialogsOff(); + TEST_EQ(s.answered, 1); + TEST_EQ(s.sawLabel, 1); + closeManagers(); fixShellDown(); } @@ -451,6 +465,7 @@ static void caseOpenAndTree(void) { selectOnly(list, "alpha.txt"); scriptOn(&s, NULL, NULL, NULL, "OK"); tWidgetKey(win, KEY_ENTER, 0); + fixPumpSettled(); fixDialogsOff(); TEST_EQ(s.answered, 1); @@ -463,6 +478,7 @@ static void caseOpenAndTree(void) { wgtSetFocused(list); tWidgetKey(win, KEY_ENTER, 0); fixPumpShell(TASK_FRAMES); + fixPumpSettled(); fixDialogsOff(); TEST_EQ(s.answered, 0); TEST_EQ(shellRunningAppCount(), before + 1); @@ -476,7 +492,7 @@ static void caseOpenAndTree(void) { wgtSetText(tFind(win, "fmPath"), dir); wgtSetFocused(tFind(win, "fmPath")); tWidgetKey(win, KEY_ENTER, 0); - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); TEST_TRUE(rowOf(list, "clock.app") >= 0); TEST_STR_EQ(wgtListViewGetCell(list, rowOf(list, "clock.app"), 2), "Program"); wgtSetFocused(list); @@ -484,6 +500,7 @@ static void caseOpenAndTree(void) { before = shellRunningAppCount(); tWidgetKey(win, KEY_ENTER, 0); fixPumpShell(TASK_FRAMES); + fixPumpSettled(); TEST_EQ(shellRunningAppCount(), before + 1); // The tree shows the path; choosing a folder there lists it. @@ -494,7 +511,7 @@ static void caseOpenAndTree(void) { if (kpunch) { wgtSetFocused(tree); wgtTreeViewSetSelected(tree, kpunch); - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); TEST_TRUE(rowOf(list, "clock") >= 0); TEST_TRUE(rowOf(list, "fileMgr") >= 0); TEST_TRUE(strstr(win->title, "kpunch") != NULL); @@ -502,7 +519,7 @@ static void caseOpenAndTree(void) { // Backspace in the tree goes up too. tWidgetKey(win, KEY_BACKSPACE, 0); - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); TEST_TRUE(rowOf(list, "kpunch") >= 0); } @@ -519,9 +536,16 @@ static void closeManagers(void) { for (int32_t i = ctx->stack.count - 1; i >= 0; i--) { WindowT *win = ctx->stack.windows[i]; + // A window closed changes the stack: walk it again (one that did + // not close is left, or the walk would never end) if (strncmp(win->title, FM_TITLE_PREFIX, strlen(FM_TITLE_PREFIX)) == 0 && !win->destroyPending) { - TEST_TRUE(fixCloseApp(win)); - i = ctx->stack.count; + bool closed = fixCloseApp(win); + + TEST_TRUE(closed); + + if (closed) { + i = ctx->stack.count; + } } } } @@ -549,7 +573,7 @@ static int32_t launch(const char *dir) { fixPumpShell(1); } - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); return id; } @@ -590,6 +614,7 @@ static void menu(WindowT *win, const char *caption) { ctx->currentAppId = win->appId; win->onMenu(win, id); ctx->currentAppId = saved; + fixPumpSettled(); } @@ -601,7 +626,7 @@ static void pumpUntilNoWindow(const char *title) { fixPumpShell(1); if (!tFindWindow(title)) { - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); return; } } @@ -637,7 +662,17 @@ static void scriptRun(AppContextT *ctx, WindowT *modal, void *userCtx) { (void)ctx; - if (s->expectLabel && tFindByText(modal->widgetRoot, s->expectLabel)) { + if (modal->id == s->answeredId) { + return; + } + + if (s->expectLabel) { + if (!tFindByText(modal->widgetRoot, s->expectLabel)) { + // Not yet: the clock moves on, so the timer that fills it in runs + hostClockAdvance(JOB_FRAME_MS); + return; + } + s->sawLabel++; } @@ -659,6 +694,7 @@ static void scriptRun(AppContextT *ctx, WindowT *modal, void *userCtx) { if (tDialogClickButton(modal, s->button) || tDialogClickButton(modal, "OK")) { s->answered++; + s->answeredId = modal->id; } } diff --git a/src/test/fileops/fileOpsTest.c b/src/test/fileops/fileOpsTest.c index de4babe2..aea4f277 100644 --- a/src/test/fileops/fileOpsTest.c +++ b/src/test/fileops/fileOpsTest.c @@ -28,11 +28,15 @@ // BASIC entry points. #include "dvxtest.h" +#include "dvxApp.h" #include "dvxPlat.h" +#include "dvxPlatHost.h" #include "fileops/fileOps.h" +#include "fileops/fileOpsSys.h" #include "stb_ds_wrap.h" #include "dvxBas.h" +#include #include #include #include @@ -64,6 +68,21 @@ #define SHARED_WPAPER DVX_SHARED_DIR "/" DVX_WPAPER_DIR #define USER_FILE_BYTES 5 // the user's b.bmp, which hides SHARED's #define SHARED_FILE_BYTES 3 +#define MANY_FILES 100 // a folder read in several scan steps +#define PART_FILES 40 // short names in the folder that fails part way +#define PART_DEST_LEN 230 // a destination that fits short names, not PART_LONG_LEN +#define PART_LONG_LEN 40 +#define PUMP_READ_MS 30 // each clock read moves the fake clock this far +#define PUMP_FILES 20 +#define JOB_MEASURE 3 // fileops.bas FILEOPS_MEASURE +#define JOB_BAD_OP 4 + +// The test's wait pump (dvxSetWaitPump): calls, and the answer from which +// call on is stop (0: never) +typedef struct { + int32_t calls; + int32_t stopFrom; +} PumpT; // Scripted answers for runScripted typedef struct { @@ -79,9 +98,11 @@ typedef struct { // Prototypes // ============================================================ +static void caseAnswers(void); static void caseAttributes(void); static void caseBasic(void); static void caseBasicFolders(void); +static void caseBasicJobs(void); static void caseBasicPlatform(void); static void caseCancel(void); static void caseClearAttr(void); @@ -93,21 +114,81 @@ static void caseDelete(void); static void caseFormat(void); static void caseListSort(void); static void caseMakeRename(void); +static void caseMeasure(void); static void caseMove(void); +static void casePartialFolder(void); static void casePaths(void); static void caseRunCallbacks(void); +static void caseScanPieces(void); static void caseSymlinks(void); +static void caseSysDir(void); +static void caseTreePump(void); static void caseTreeSize(void); static bool exists(const char *path); static int64_t fileSize(const char *path); static void makeDir(const char *path); static void makeFile(const char *path, int32_t bytes, char fill); +static void makeFiles(const char *dir, int32_t count); +static bool pumpHook(void); static char readFirst(const char *path); static bool runCancelProgress(FileOpsJobT *job, void *ctx); static FileOpsStepE runScripted(FileOpsJobT *job, ScriptT *s); +static FileOpsStepE stepUntilAsked(FileOpsJobT *job); static const char *tp(const char *rel); static void treeMake(const char *base); +static PumpT sPump; + + +// A Cancel answer stops the job at once, even when the item would go +// through when tried again; an answer the retried item did not need is +// not kept for the next question. +static void caseAnswers(void) { + tCase("answers: Cancel stops at once, an unused answer is dropped"); + + if (geteuid() == 0) { + TEST_TRUE(true); + return; + } + + makeDir(tp("ans")); + makeDir(tp("ans/dst")); + makeDir(tp("ans/one")); + makeFile(tp("ans/one/f.txt"), 1, 'f'); + makeDir(tp("ans/two")); + TEST_EQ(chmod(tp("ans/one"), MODE_NONE), 0); + TEST_EQ(chmod(tp("ans/two"), MODE_NONE), 0); + + FileOpsJobT *job = fileOpsJobCreate(FileOpsCopyE, tp("ans/dst")); + + TEST_TRUE(fileOpsJobAddSource(job, tp("ans/one"))); + TEST_EQ(stepUntilAsked(job), FileOpsStepErrorE); + fileOpsJobAnswer(job, FileOpsAnswerCancelE); + TEST_EQ(chmod(tp("ans/one"), MODE_DIR), 0); + TEST_EQ(fileOpsJobStep(job), FileOpsStepCancelledE); + TEST_TRUE(!exists(tp("ans/dst/one"))); + fileOpsJobFree(job); + + // Skip answered, then one reads after all: two must still ask + TEST_EQ(chmod(tp("ans/one"), MODE_NONE), 0); + job = fileOpsJobCreate(FileOpsCopyE, tp("ans/dst")); + TEST_TRUE(fileOpsJobAddSource(job, tp("ans/one"))); + TEST_TRUE(fileOpsJobAddSource(job, tp("ans/two"))); + TEST_EQ(stepUntilAsked(job), FileOpsStepErrorE); + TEST_TRUE(strstr(fileOpsJobMessage(job), "one") != NULL); + fileOpsJobAnswer(job, FileOpsAnswerSkipE); + TEST_EQ(chmod(tp("ans/one"), MODE_DIR), 0); + TEST_EQ(stepUntilAsked(job), FileOpsStepErrorE); + TEST_TRUE(strstr(fileOpsJobMessage(job), "two") != NULL); + fileOpsJobAnswer(job, FileOpsAnswerSkipE); + TEST_EQ(stepUntilAsked(job), FileOpsStepDoneE); + TEST_TRUE(exists(tp("ans/dst/one/f.txt"))); + TEST_TRUE(!exists(tp("ans/dst/two"))); + fileOpsJobFree(job); + + TEST_EQ(chmod(tp("ans/two"), MODE_DIR), 0); +} + static void caseAttributes(void) { tCase("attributes and dates"); @@ -176,6 +257,26 @@ static void caseBasic(void) { TEST_EQ(FileOpsCopyTree(tp("bas/nothing"), tp("bas/dst"), 0), 0); TEST_EQ(FileOpsDeleteTree(tp("bas/nothing")), 0); + // A listing's folders alone, by name, from memory + treeMake(tp("bas/tree")); + + int32_t list = FileOpsList(tp("bas/tree"), ""); + + TEST_TRUE(list > 0); + FileOpsListSort(list, FileOpsSortSizeE, BAS_TRUE); + + int32_t folders = FileOpsListFolders(list); + + TEST_TRUE(folders > 0 && folders != list); + TEST_EQ(FileOpsListCount(folders), 2); + TEST_STR_EQ(FileOpsListName(folders, 0), "empty"); + TEST_STR_EQ(FileOpsListName(folders, 1), "sub"); + TEST_EQ(FileOpsListIsFolder(folders, 1), BAS_TRUE); + TEST_EQ(FileOpsListCount(list), 3); + FileOpsListFree(folders); + FileOpsListFree(list); + TEST_EQ(FileOpsListFolders(0), 0); + TEST_STR_EQ(FileOpsDriveList(), "/"); TEST_TRUE(FileOpsFreeKb(tTempDir()) >= 0); @@ -246,6 +347,45 @@ static void caseBasicFolders(void) { } +// A measure job and the tree size from BASIC. +static void caseBasicJobs(void) { + tCase("BASIC entry points: measure job and tree size"); + + treeMake(tp("bj")); + + int32_t job = FileOpsJobCreate(JOB_MEASURE, ""); + int32_t step = FileOpsStepBusyE; + + TEST_TRUE(job > 0); + TEST_EQ(FileOpsJobAddSource(job, tp("bj")), BAS_TRUE); + + for (int32_t i = 0; i < STEP_LIMIT && step == FileOpsStepBusyE; i++) { + step = FileOpsJobRunFor(job, JOB_SLICE_MS); + } + + TEST_EQ(step, FileOpsStepDoneE); + TEST_EQ(FileOpsJobFilesTotal(job), 3); + TEST_EQ(FileOpsJobFoldersTotal(job), 3); + TEST_EQ(FileOpsJobUnread(job), 0); + TEST_TRUE(FileOpsJobBytesTotal(job) == 10 + 20 + MID_FILE_BYTES); + TEST_EQ(FileOpsJobPercent(job), FILEOPS_PERCENT_FULL); + FileOpsJobFree(job); + TEST_EQ(FileOpsJobCreate(JOB_BAD_OP, ""), 0); + TEST_EQ(FileOpsJobFoldersTotal(0), 0); + TEST_EQ(FileOpsJobUnread(0), 0); + + TEST_EQ(FileOpsTreeSize(tp("bj")), BAS_TRUE); + TEST_TRUE(FileOpsTreeBytes() == 10 + 20 + MID_FILE_BYTES); + TEST_EQ(FileOpsTreeFiles(), 3); + TEST_EQ(FileOpsTreeFolders(), 3); + TEST_EQ(FileOpsTreeSize(tp("bj/a.txt")), BAS_TRUE); + TEST_EQ(FileOpsTreeFiles(), 1); + TEST_EQ(FileOpsTreeFolders(), 0); + TEST_EQ(FileOpsTreeSize(tp("bj/none")), BAS_FALSE); + TEST_EQ(FileOpsTreeFiles(), 0); +} + + // The fileops.bas entry points added on the platform layer (drives, // disks, paths, filters, the temp folder), a job's byte progress and its // attribute clearing, and setting a file's time from FileOpsModified$'s @@ -960,6 +1100,74 @@ static void caseMakeRename(void) { } +// A measure counts and changes nothing; it asks nothing, counting the +// folders it cannot read instead. +static void caseMeasure(void) { + tCase("measure: totals, unread folders, no questions"); + + FileOpsProgressT p; + ScriptT s; + + treeMake(tp("ms")); + + FileOpsJobT *job = fileOpsJobCreate(FileOpsMeasureE, NULL); + + TEST_TRUE(fileOpsJobAddSource(job, tp("ms"))); + TEST_TRUE(fileOpsJobAddSource(job, tp("ms/a.txt"))); + TEST_TRUE(!fileOpsJobAddSource(job, tp("ms/none"))); + memset(&s, 0, sizeof(s)); + TEST_EQ(runScripted(job, &s), FileOpsStepDoneE); + fileOpsJobProgress(job, &p); + TEST_EQ(p.filesTotal, 4); + TEST_EQ(p.foldersTotal, 3); + TEST_EQ(p.unread, 0); + TEST_EQ(p.bytesTotal, 10 + 10 + 20 + MID_FILE_BYTES); + TEST_EQ(p.percent, FILEOPS_PERCENT_FULL); + TEST_EQ(fileOpsJobOp(job), FileOpsMeasureE); + fileOpsJobFree(job); + + // Nothing changed: the read-only file is still read-only + TEST_TRUE(access(tp("ms/sub/b.txt"), W_OK) != 0); + + if (geteuid() == 0) { + return; + } + + // An unreadable folder inside is counted as a folder and as unread + TEST_EQ(chmod(tp("ms/sub/deep"), MODE_NONE), 0); + job = fileOpsJobCreate(FileOpsMeasureE, NULL); + TEST_TRUE(fileOpsJobAddSource(job, tp("ms"))); + memset(&s, 0, sizeof(s)); + TEST_EQ(runScripted(job, &s), FileOpsStepDoneE); + TEST_EQ(s.asks + s.errors, 0); + fileOpsJobProgress(job, &p); + TEST_EQ(p.filesTotal, 2); + TEST_EQ(p.foldersTotal, 3); + TEST_EQ(p.unread, 1); + TEST_EQ(p.bytesTotal, 10 + 20); + fileOpsJobFree(job); + + // An unreadable source: unread, no folders inside + job = fileOpsJobCreate(FileOpsMeasureE, NULL); + TEST_TRUE(fileOpsJobAddSource(job, tp("ms/sub/deep"))); + memset(&s, 0, sizeof(s)); + TEST_EQ(runScripted(job, &s), FileOpsStepDoneE); + fileOpsJobProgress(job, &p); + TEST_EQ(p.unread, 1); + TEST_EQ(p.foldersTotal, 0); + fileOpsJobFree(job); + + uint64_t bytes; + int32_t files; + int32_t folders; + + TEST_TRUE(!fileOpsTreeSize(tp("ms"), &bytes, &files, &folders)); + TEST_EQ(files, 2); + TEST_EQ(folders, 3); + TEST_EQ(chmod(tp("ms/sub/deep"), MODE_DIR), 0); +} + + static void caseMove(void) { tCase("move: whole-item rename and merge into an existing folder"); @@ -1013,6 +1221,45 @@ static void caseMove(void) { } +// A folder whose read fails part way (an entry's destination path is +// too long) keeps nothing of what was read: not after Retry, not after +// Skip. +static void casePartialFolder(void) { + tCase("a folder failing part way keeps nothing of it"); + + char dest[PART_DEST_LEN + 1]; + char longName[PART_LONG_LEN + 1]; + char path[PATH_MAX_T]; + FileOpsProgressT p; + ScriptT s; + + makeDir(tp("part")); + makeDir(tp("part/src")); + makeFiles(tp("part/src"), PART_FILES); + memset(longName, 'l', PART_LONG_LEN); + longName[PART_LONG_LEN] = '\0'; + snprintf(path, sizeof(path), "%s/%s", tp("part/src"), longName); + makeFile(path, 1, 'l'); + + memset(dest, 'd', PART_DEST_LEN); + dest[0] = '/'; + dest[PART_DEST_LEN] = '\0'; + + FileOpsJobT *job = fileOpsJobCreate(FileOpsCopyE, dest); + + TEST_TRUE(fileOpsJobAddSource(job, tp("part/src"))); + memset(&s, 0, sizeof(s)); + s.answers[s.count++] = FileOpsAnswerRetryE; + s.answers[s.count++] = FileOpsAnswerSkipE; + TEST_EQ(runScripted(job, &s), FileOpsStepDoneE); + TEST_EQ(s.errors, 2); + fileOpsJobProgress(job, &p); + TEST_EQ(p.filesTotal, 0); + TEST_EQ(p.filesDone, 0); + fileOpsJobFree(job); +} + + static void casePaths(void) { tCase("paths"); @@ -1119,6 +1366,68 @@ static void caseRunCallbacks(void) { // Links are items of their own: a link to a parent folder does not make // the walk recurse forever, and deleting a link leaves its target alone. +// A big folder is read a piece per step, and a job stopped or freed in +// the middle of a folder read lets it go. +static void caseScanPieces(void) { + tCase("folders are read in pieces; cancel and free mid-read"); + + FileOpsProgressT p; + int32_t steps = 0; + int32_t maxPiece = 0; + int32_t last = 0; + FileOpsStepE step = FileOpsStepBusyE; + + makeDir(tp("pcs")); + makeDir(tp("pcs/many")); + makeDir(tp("pcs/dst")); + makeFiles(tp("pcs/many"), MANY_FILES); + + FileOpsJobT *job = fileOpsJobCreate(FileOpsMeasureE, NULL); + + TEST_TRUE(fileOpsJobAddSource(job, tp("pcs/many"))); + + for (int32_t i = 0; i < STEP_LIMIT && step == FileOpsStepBusyE; i++) { + step = fileOpsJobStep(job); + fileOpsJobProgress(job, &p); + maxPiece = p.filesTotal - last > maxPiece ? p.filesTotal - last : maxPiece; + last = p.filesTotal; + steps++; + } + + TEST_EQ(step, FileOpsStepDoneE); + TEST_EQ(p.filesTotal, MANY_FILES); + TEST_TRUE(maxPiece > 0 && maxPiece < MANY_FILES); + TEST_TRUE(steps > MANY_FILES / maxPiece); + + // A measure changes nothing installed; a job that ran may have + uint32_t changes = platformInstalledChangeCount(); + + fileOpsJobFree(job); + TEST_EQ(platformInstalledChangeCount(), changes); + job = fileOpsJobCreate(FileOpsCopyE, tp("pcs/dst")); + fileOpsJobFree(job); + TEST_EQ(platformInstalledChangeCount(), changes); + + // Cancelled, then freed, in the middle of the folder read + job = fileOpsJobCreate(FileOpsCopyE, tp("pcs/dst")); + TEST_TRUE(fileOpsJobAddSource(job, tp("pcs/many"))); + TEST_EQ(fileOpsJobStep(job), FileOpsStepBusyE); + TEST_EQ(fileOpsJobStep(job), FileOpsStepBusyE); + fileOpsJobCancel(job); + TEST_EQ(fileOpsJobStep(job), FileOpsStepCancelledE); + TEST_TRUE(!exists(tp("pcs/dst/many"))); + fileOpsJobFree(job); + TEST_EQ(platformInstalledChangeCount(), changes + 1); + + job = fileOpsJobCreate(FileOpsDeleteE, NULL); + TEST_TRUE(fileOpsJobAddSource(job, tp("pcs/many"))); + TEST_EQ(fileOpsJobStep(job), FileOpsStepBusyE); + TEST_EQ(fileOpsJobStep(job), FileOpsStepBusyE); + fileOpsJobFree(job); + TEST_TRUE(exists(tp("pcs/many"))); +} + + static void caseSymlinks(void) { tCase("symbolic links are not followed"); @@ -1163,6 +1472,92 @@ static void caseSymlinks(void) { } +// The folder reader the scans use: entries one at a time, "." and ".." +// left out, a close before the end, and what cannot be opened. +static void caseSysDir(void) { + tCase("folder reader: next, early close, errors"); + + FileOpsSysEntryT e; + int32_t count = 0; + int32_t dirs = 0; + + treeMake(tp("sys")); + + FileOpsSysDirT *d = fileOpsSysOpen(tp("sys"), false); + + TEST_TRUE(d != NULL); + + while (d && fileOpsSysNext(d, &e)) { + TEST_TRUE(strcmp(e.name, ".") != 0 && strcmp(e.name, "..") != 0); + count++; + dirs += e.isDir ? 1 : 0; + + if (strcmp(e.name, "a.txt") == 0) { + TEST_EQ(e.size, 10); + TEST_EQ(e.mtime, OLD_TIME); + } + } + + TEST_EQ(count, 3); + TEST_EQ(dirs, 2); + TEST_TRUE(!fileOpsSysNext(d, &e)); + fileOpsSysClose(d); + + d = fileOpsSysOpen(tp("sys"), false); + TEST_TRUE(d && fileOpsSysNext(d, &e)); + fileOpsSysClose(d); + fileOpsSysClose(NULL); + + errno = 0; + TEST_TRUE(fileOpsSysOpen(tp("sys/none"), false) == NULL); + TEST_EQ(errno, ENOENT); + errno = 0; + TEST_TRUE(fileOpsSysOpen(tp("sys/a.txt"), false) == NULL); + TEST_EQ(errno, ENOTDIR); +} + + +// The whole-tree calls let DVX go on (dvxWaitPump) at the first step and +// then every so often; a first "stop" (code that may not wait) only ends +// the pumping, a later one (the program ending) cancels the call. +static void caseTreePump(void) { + tCase("whole-tree calls pump; stop ends pumping or cancels"); + + makeDir(tp("pump")); + makeDir(tp("pump/src")); + makeDir(tp("pump/dst")); + makeFiles(tp("pump/src"), PUMP_FILES); + dvxSetWaitPump(pumpHook); + hostClockSetReadStep(PUMP_READ_MS); + + memset(&sPump, 0, sizeof(sPump)); + TEST_EQ(FileOpsCopyTree(tp("pump/src"), tp("pump/dst"), 0), BAS_TRUE); + TEST_TRUE(sPump.calls > 1); + TEST_TRUE(exists(tp("pump/dst/src/f00.txt"))); + + memset(&sPump, 0, sizeof(sPump)); + TEST_EQ(FileOpsTreeSize(tp("pump/src")), BAS_TRUE); + TEST_EQ(FileOpsTreeFiles(), PUMP_FILES); + TEST_TRUE(sPump.calls >= 1); + + // Code that may not wait: one pump, and the call still finishes + memset(&sPump, 0, sizeof(sPump)); + sPump.stopFrom = 1; + TEST_EQ(FileOpsDeleteTree(tp("pump/dst/src")), BAS_TRUE); + TEST_EQ(sPump.calls, 1); + TEST_TRUE(!exists(tp("pump/dst/src"))); + + // The program ending part way: the call is cancelled + memset(&sPump, 0, sizeof(sPump)); + sPump.stopFrom = 2; + TEST_EQ(FileOpsCopyTree(tp("pump/src"), tp("pump/dst"), 0), BAS_FALSE); + TEST_EQ(sPump.calls, 2); + + hostClockSetReadStep(0); + dvxSetWaitPump(NULL); +} + + static void caseTreeSize(void) { tCase("tree size"); @@ -1212,6 +1607,24 @@ static void makeFile(const char *path, int32_t bytes, char fill) { } +// count files f00.txt, f01.txt, ... in dir. +static void makeFiles(const char *dir, int32_t count) { + char path[PATH_MAX_T]; + + for (int32_t i = 0; i < count; i++) { + snprintf(path, sizeof(path), "%s/f%02d.txt", dir, (int)i); + makeFile(path, 1, 'f'); + } +} + + +// The wait pump: stop from call sPump.stopFrom on. +static bool pumpHook(void) { + sPump.calls++; + return sPump.stopFrom == 0 || sPump.calls < sPump.stopFrom; +} + + static char readFirst(const char *path) { FILE *f = fopen(path, "rb"); int32_t c = f ? fgetc(f) : EOF; @@ -1262,6 +1675,18 @@ static FileOpsStepE runScripted(FileOpsJobT *job, ScriptT *s) { } +// Step the job until it asks, ends, or the step limit runs out. +static FileOpsStepE stepUntilAsked(FileOpsJobT *job) { + FileOpsStepE step = FileOpsStepBusyE; + + for (int32_t i = 0; i < STEP_LIMIT && step == FileOpsStepBusyE; i++) { + step = fileOpsJobStep(job); + } + + return step; +} + + // Path inside the temporary directory (one of a few rotating buffers). static const char *tp(const char *rel) { static char bufs[4][PATH_MAX_T]; @@ -1309,7 +1734,10 @@ int main(void) { caseClearAttr(); caseListSort(); caseMakeRename(); + caseSysDir(); caseTreeSize(); + caseMeasure(); + caseScanPieces(); caseCopyTree(); caseConflicts(); caseCopyIntoItself(); @@ -1317,11 +1745,15 @@ int main(void) { caseDelete(); caseCancel(); caseCopyErrors(); + caseAnswers(); + casePartialFolder(); caseRunCallbacks(); caseSymlinks(); caseBasic(); caseBasicFolders(); + caseBasicJobs(); caseBasicPlatform(); + caseTreePump(); return tSuiteEnd(); } diff --git a/src/test/formrt/Makefile b/src/test/formrt/Makefile index 70e1260f..34d9086f 100644 --- a/src/test/formrt/Makefile +++ b/src/test/formrt/Makefile @@ -40,8 +40,9 @@ TARGET = $(BINDIR)/testFormRt STUBS_SO = $(BINDIR)/frtStubs.so # Every .c here except the stub library is part of the test binary. SRCS = $(filter-out frtStubs.c,$(wildcard *.c)) -SEC_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/serial/security -SEC_OBJS = $(patsubst $(SEC_DIR)/%.c,$(BINDIR)/security/%.o,$(wildcard $(SEC_DIR)/*.c)) +SEC_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/crypto +MONO_DIR = $(SEC_DIR)/thirdparty/monocypher/src +SEC_OBJS = $(patsubst $(SEC_DIR)/%.c,$(BINDIR)/security/%.o,$(wildcard $(SEC_DIR)/*.c)) $(BINDIR)/security/monocypher.o $(BINDIR)/security/monocypher-ed25519.o OBJS = $(patsubst %.c,$(BINDIR)/%.o,$(SRCS)) $(BINDIR)/basBuild.o $(BINDIR)/game.o $(SEC_OBJS) GAME_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/game CFLAGS = $(DVXTEST_CFLAGS) -I$(DVXBASIC) -I$(DVXTEST_ROOT)/src/libs/kpunch/dvxshell -DFRT_FIXTURE_DIR='"$(abspath .)/fixtures"' -DFRT_INCLUDE_DIR='"$(DVXTEST_ROOT)/src/include/basic"' -DFRT_STUBS_SO='"$(abspath $(STUBS_SO))"' @@ -64,10 +65,18 @@ $(BINDIR)/game.o: $(GAME_DIR)/game.c $(DVX_MAKEFILES) @mkdir -p $(BINDIR) $(HOSTCC) $(CFLAGS) -I$(GAME_DIR) -c -o $@ $< -# security.bas calls the security library for real +# security.bas calls the security library for real, Monocypher too $(BINDIR)/security/%.o: $(SEC_DIR)/%.c $(DVX_MAKEFILES) @mkdir -p $(dir $@) - $(HOSTCC) $(CFLAGS) -c -o $@ $< + $(HOSTCC) $(CFLAGS) -I$(MONO_DIR) -c -o $@ $< + +$(BINDIR)/security/%.o: $(MONO_DIR)/%.c $(DVX_MAKEFILES) + @mkdir -p $(dir $@) + $(HOSTCC) $(CFLAGS) -I$(MONO_DIR) -c -o $@ $< + +$(BINDIR)/security/%.o: $(MONO_DIR)/optional/%.c $(DVX_MAKEFILES) + @mkdir -p $(dir $@) + $(HOSTCC) $(CFLAGS) -I$(MONO_DIR) -c -o $@ $< $(STUBS_SO): frtStubs.c $(DVX_MAKEFILES) @mkdir -p $(BINDIR) diff --git a/src/test/formrt/frtHarness.c b/src/test/formrt/frtHarness.c index 4bf7494b..723966c9 100644 --- a/src/test/formrt/frtHarness.c +++ b/src/test/formrt/frtHarness.c @@ -182,7 +182,7 @@ static int32_t emitFormHeader(const BasFormT *form, char *buf, int32_t bufSize, const BasPropDescT *pd = &props[i]; // Run-time state, never in a .frm file - if (strcasecmp(pd->name, "Name") == 0 || strcasecmp(pd->name, "Visible") == 0 || strcasecmp(pd->name, BAS_PROP_DESIGNMODE) == 0 || strcasecmp(pd->name, BAS_PROP_DESIGNSELECTION) == 0 || pd->type == BAS_PROP_OBJECT) { + if (strcasecmp(pd->name, BAS_PROP_NAME) == 0 || basFormRtFormPropRuntimeOnly(pd->name) || pd->type == BAS_PROP_OBJECT) { continue; } @@ -217,12 +217,14 @@ static int32_t emitFormHeader(const BasFormT *form, char *buf, int32_t bufSize, } // A Padding / Spacing nothing set (the content box keeps its - // own), WindowState 0 (a normal window) and MousePointer 0 - // (vbDefault) are not written, as the IDE designer does + // own), and a WindowState, MousePointer, MaxWidth, or MaxHeight + // of 0 or a GameInput of False (the defaults) are not written, + // as the IDE designer does const WidgetT *box = form->contentBox; bool gapUnset = box && ((strcasecmp(pd->name, BAS_PROP_PADDING) == 0 && box->padding == WGT_GAP_DEFAULT) || (strcasecmp(pd->name, BAS_PROP_SPACING) == 0 && box->spacing == WGT_GAP_DEFAULT)); + bool zeroSkip = strcasecmp(pd->name, BAS_PROP_WINDOWSTATE) == 0 || strcasecmp(pd->name, BAS_PROP_MOUSEPOINTER) == 0 || strcasecmp(pd->name, BAS_PROP_MAXWIDTH) == 0 || strcasecmp(pd->name, BAS_PROP_MAXHEIGHT) == 0 || strcasecmp(pd->name, BAS_PROP_GAMEINPUT) == 0; - if (gapUnset || ((strcasecmp(pd->name, BAS_PROP_WINDOWSTATE) == 0 || strcasecmp(pd->name, BAS_PROP_MOUSEPOINTER) == 0) && basValToNumber(v) == 0)) { + if (gapUnset || (zeroSkip && basValToNumber(v) == 0)) { basValRelease(&v); continue; } diff --git a/src/test/formrt/frtStubs.c b/src/test/formrt/frtStubs.c index f88470d1..272e2691 100644 --- a/src/test/formrt/frtStubs.c +++ b/src/test/formrt/frtStubs.c @@ -30,6 +30,7 @@ #include "frtStubs.h" #include "../../tools/hlpcCompile.h" +#include "../../libs/kpunch/seclink/packet.h" #include "dvxTypes.h" #include @@ -69,6 +70,7 @@ typedef struct { char pass[FRT_STUB_PASS_MAX]; bool plain[FRT_STUB_CHANNELS]; bool stepping; // secLinkHandshakeStep answered PENDING once + PktStreamT stream; // secLinkOpenStream's (read NULL for a COM port link) } StubLinkT; typedef struct { @@ -81,6 +83,9 @@ typedef struct { const FrtStubApiT *frtStubApi(void); int32_t hlpcCompile(const char **inputs, int32_t inputCount, const char *outputFile, const char *a, const char *b, uint32_t options, HlpcProgressFnT x, void *y); void hlpcSetMessageFn(HlpcMessageFnT fn, void *ctx); +void hlpcSetTickFn(HlpcTickFnT fn, void *ctx); +int32_t netBasStreamRead(int32_t handle, uint8_t *buf, int32_t len); +int32_t netBasStreamWrite(int32_t handle, const uint8_t *data, int32_t len); int32_t rs232ClearRxBuffer(int32_t com); int32_t rs232ClearTxBuffer(int32_t com); int32_t rs232Close(int32_t com); @@ -114,13 +119,13 @@ int32_t rs232WriteBuf(int32_t com, const char *data, int32_t len); void secLinkClose(void *link); int32_t secLinkGetDropped(void *link); int32_t secLinkGetPending(void *link); -int32_t secLinkHandshake(void *link); +int32_t secLinkHandshakeStart(void *link); int32_t secLinkHandshakeStep(void *link); bool secLinkIsReady(void *link); void *secLinkOpen(int32_t com, int32_t bps, int32_t dataBits, char parity, int32_t stopBits, int32_t handshake, void (*recvCb)(void *, const uint8_t *, int32_t, uint8_t, bool), void *recvCtx); +void *secLinkOpenStream(const PktStreamT *stream, void (*recvCb)(void *, const uint8_t *, int32_t, uint8_t, bool), void *recvCtx); int32_t secLinkPoll(void *link); int32_t secLinkSend(void *link, const uint8_t *data, int32_t len, uint8_t channel, bool encrypt, bool flush); -int32_t secLinkSendBuf(void *link, const uint8_t *data, int32_t len, uint8_t channel, bool encrypt); int32_t secLinkSetChannelPlain(void *link, uint8_t channel, bool plain); int32_t secLinkSetPassphrase(void *link, const char *pass); void shellOpenHelp(void *ctx, const char *args); @@ -134,10 +139,15 @@ static int32_t stubCommLinks(void); static void stubCommPeerPass(const char *pass); static bool stubCommPlain(uint8_t channel); static const uint8_t *stubCommSent(int32_t *len, uint8_t *channel, bool *encrypted); +static int32_t stubCommStreamRead(uint8_t *buf, int32_t len, int32_t *tcp); +static int32_t stubCommStreamWrite(const void *data, int32_t len, int32_t *tcp); static const char *stubHlpcLastOutput(void); static void stubHlpcRc(int32_t rc); +static int32_t stubHlpcTicked(void); +static void stubHlpcTicks(int32_t ticks, void (*during)(void)); static int32_t stubIdleCount(void); static void stubIdleRun(void); +static StubLinkT *stubLastStream(void); static int32_t stubLine(int32_t com, bool up); static void stubReset(void); static bool stubSerBreak(int32_t com); @@ -162,15 +172,21 @@ static int32_t sStubSentLen = 0; static uint8_t sStubSentChannel = 0; static bool sStubSentEncrypted = false; static char sStubPeerPass[FRT_STUB_PASS_MAX]; +static int32_t sStubTcpHandle = 0; // the handle the last netBasStream call got static StubIdleT sStubIdle[FRT_STUB_MAX_IDLE]; static int32_t sStubIdleCount = 0; -static int32_t sStubHlpcRc = 0; -static char sStubHlpcOutput[FRT_STUB_PATH_MAX]; -static HlpcMessageFnT sStubHlpcMsgFn = NULL; -static void *sStubHlpcMsgCtx = NULL; -static char sStubShellArgs[FRT_STUB_PATH_MAX]; +static int32_t sStubHlpcRc = 0; +static char sStubHlpcOutput[FRT_STUB_PATH_MAX]; +static HlpcMessageFnT sStubHlpcMsgFn = NULL; +static void *sStubHlpcMsgCtx = NULL; +static HlpcTickFnT sStubHlpcTickFn = NULL; +static void *sStubHlpcTickCtx = NULL; +static int32_t sStubHlpcTicks = 0; // ticks a compile makes +static void (*sStubHlpcDuring)(void) = NULL; +static int32_t sStubHlpcTicked = 0; // ticks the last compile made +static char sStubShellArgs[FRT_STUB_PATH_MAX]; static const FrtStubApiT sStubApi = { .reset = stubReset, @@ -186,10 +202,14 @@ static const FrtStubApiT sStubApi = { .commHandshakeRc = stubCommHandshakeRc, .commPeerPass = stubCommPeerPass, .commPlain = stubCommPlain, + .commStreamWrite = stubCommStreamWrite, + .commStreamRead = stubCommStreamRead, .idleRun = stubIdleRun, .idleCount = stubIdleCount, .hlpcRc = stubHlpcRc, .hlpcLastOutput = stubHlpcLastOutput, + .hlpcTicks = stubHlpcTicks, + .hlpcTicked = stubHlpcTicked, .shellLastArgs = stubShellLastArgs, }; @@ -217,6 +237,25 @@ int32_t hlpcCompile(const char **inputs, int32_t inputCount, const char *outputF sStubHlpcMsgFn(sStubHlpcMsgCtx, FRT_STUB_HLPC_ERROR); } + // The ticks the real compiler makes as it goes + sStubHlpcTicked = 0; + + for (int32_t i = 0; i < sStubHlpcTicks && sStubHlpcTickFn; i++) { + if (sStubHlpcDuring) { + sStubHlpcDuring(); + } + + sStubHlpcTicked++; + + if (!sStubHlpcTickFn(sStubHlpcTickCtx)) { + if (sStubHlpcMsgFn) { + sStubHlpcMsgFn(sStubHlpcMsgCtx, FRT_STUB_HLPC_STOPPED); + } + + return 1; + } + } + return sStubHlpcRc; } @@ -227,6 +266,33 @@ void hlpcSetMessageFn(HlpcMessageFnT fn, void *ctx) { } +void hlpcSetTickFn(HlpcTickFnT fn, void *ctx) { + sStubHlpcTickFn = fn; + sStubHlpcTickCtx = ctx; +} + + +// The net.bas stream: records the handle; a read gives one 'r', a write +// takes everything +int32_t netBasStreamRead(int32_t handle, uint8_t *buf, int32_t len) { + sStubTcpHandle = handle; + + if (len < 1) { + return 0; + } + + buf[0] = 'r'; + return 1; +} + + +int32_t netBasStreamWrite(int32_t handle, const uint8_t *data, int32_t len) { + (void)data; + sStubTcpHandle = handle; + return len; +} + + int32_t rs232ClearRxBuffer(int32_t com) { if (com < 0 || com >= FRT_STUB_PORTS) { return -1; @@ -498,19 +564,21 @@ int32_t secLinkGetPending(void *link) { } -int32_t secLinkHandshake(void *link) { +// An exchange starts: PENDING (the step answers PENDING once more, then +// how it ends). +int32_t secLinkHandshakeStart(void *link) { StubLinkT *l = (StubLinkT *)link; - if (sStubHandshakeRc != 0) { - return sStubHandshakeRc; + if (l) { + l->stepping = false; } - return (l && strcmp(l->pass, sStubPeerPass) == 0) ? 0 : FRT_STUB_ERR_AUTH; + return FRT_STUB_PENDING; } -// PENDING on the first step of an exchange, then what secLinkHandshake -// would return. +// PENDING on the first step of an exchange, then how it ends: the result +// set (commHandshakeRc), else success when the passphrases match. int32_t secLinkHandshakeStep(void *link) { StubLinkT *l = (StubLinkT *)link; @@ -523,7 +591,11 @@ int32_t secLinkHandshakeStep(void *link) { l->stepping = false; } - return secLinkHandshake(link); + if (sStubHandshakeRc != 0) { + return sStubHandshakeRc; + } + + return (l && strcmp(l->pass, sStubPeerPass) == 0) ? 0 : FRT_STUB_ERR_AUTH; } @@ -560,6 +632,18 @@ void *secLinkOpen(int32_t com, int32_t bps, int32_t dataBits, char parity, int32 } +// A link over a byte stream: a COM port link that keeps the stream +void *secLinkOpenStream(const PktStreamT *stream, void (*recvCb)(void *, const uint8_t *, int32_t, uint8_t, bool), void *recvCtx) { + StubLinkT *l = (StubLinkT *)secLinkOpen(0, 0, 0, 'N', 0, 0, recvCb, recvCtx); + + if (l) { + l->stream = *stream; + } + + return l; +} + + int32_t secLinkPoll(void *link) { (void)link; return 0; @@ -567,14 +651,10 @@ int32_t secLinkPoll(void *link) { int32_t secLinkSend(void *link, const uint8_t *data, int32_t len, uint8_t channel, bool encrypt, bool flush) { - (void)flush; - return secLinkSendBuf(link, data, len, channel, encrypt); -} - - -int32_t secLinkSendBuf(void *link, const uint8_t *data, int32_t len, uint8_t channel, bool encrypt) { StubLinkT *l = (StubLinkT *)link; + (void)flush; + if (!l || !l->open || sStubCommSendFails) { return -1; } @@ -720,6 +800,24 @@ static const uint8_t *stubCommSent(int32_t *len, uint8_t *channel, bool *encrypt } +static int32_t stubCommStreamRead(uint8_t *buf, int32_t len, int32_t *tcp) { + StubLinkT *l = stubLastStream(); + int32_t rc = l ? l->stream.read(l->stream.ctx, buf, len) : 0; + + *tcp = l ? sStubTcpHandle : 0; + return rc; +} + + +static int32_t stubCommStreamWrite(const void *data, int32_t len, int32_t *tcp) { + StubLinkT *l = stubLastStream(); + int32_t rc = l ? l->stream.write(l->stream.ctx, (const uint8_t *)data, len) : 0; + + *tcp = l ? sStubTcpHandle : 0; + return rc; +} + + static const char *stubHlpcLastOutput(void) { return sStubHlpcOutput; } @@ -730,6 +828,17 @@ static void stubHlpcRc(int32_t rc) { } +static int32_t stubHlpcTicked(void) { + return sStubHlpcTicked; +} + + +static void stubHlpcTicks(int32_t ticks, void (*during)(void)) { + sStubHlpcTicks = ticks; + sStubHlpcDuring = during; +} + + static int32_t stubIdleCount(void) { return sStubIdleCount; } @@ -748,6 +857,18 @@ static void stubIdleRun(void) { } +// The last open link with a stream (secLinkOpenStream), or NULL. +static StubLinkT *stubLastStream(void) { + for (int32_t i = FRT_STUB_MAX_LINKS - 1; i >= 0; i--) { + if (sStubLinks[i].open && sStubLinks[i].stream.read) { + return &sStubLinks[i]; + } + } + + return NULL; +} + + // A modem line of an open stub port (1 up, 0 down), -1 when not open. static int32_t stubLine(int32_t com, bool up) { if (com < 0 || com >= FRT_STUB_PORTS || !sStubPorts[com].open) { @@ -761,6 +882,7 @@ static int32_t stubLine(int32_t com, bool up) { static void stubReset(void) { memset(sStubPorts, 0, sizeof(sStubPorts)); memset(sStubLinks, 0, sizeof(sStubLinks)); + sStubTcpHandle = 0; memset(sStubIdle, 0, sizeof(sStubIdle)); sStubSerOpenFails = false; sStubSerWriteFails = false; @@ -774,6 +896,9 @@ static void stubReset(void) { sStubIdleCount = 0; sStubHlpcRc = 0; sStubHlpcOutput[0] = '\0'; + sStubHlpcTicks = 0; + sStubHlpcDuring = NULL; + sStubHlpcTicked = 0; sStubShellArgs[0] = '\0'; } diff --git a/src/test/formrt/frtStubs.h b/src/test/formrt/frtStubs.h index 7e770671..11e66717 100644 --- a/src/test/formrt/frtStubs.h +++ b/src/test/formrt/frtStubs.h @@ -23,7 +23,7 @@ // frtStubs.h -- control interface of the frtStubs.so test library // -// The shared object exports the rs232*, secLink*, hlpcCompile and +// The shared object exports the rs232*, secLink*, hlpc* and // shell* symbols that formrt.c resolves lazily with dlsym(NULL, ...). // A test case dlopen()s it with RTLD_GLOBAL when it wants those APIs // resolvable, drives the fakes through the control struct returned by @@ -50,7 +50,8 @@ #define FRT_STUB_LSR 0x62 // overrun (0x02) plus the transmitter-empty bits #define FRT_STUB_MSR 0x50 // ring indicator (0x40) and CTS #define FRT_STUB_DROPPED 3 // secLinkGetDropped of every open link -#define FRT_STUB_HLPC_ERROR "error: guide.dhs:3: stub compile failure" // what a failing fake compile reports +#define FRT_STUB_HLPC_ERROR "error: guide.dhs:3: stub compile failure" // what a failing fake compile reports +#define FRT_STUB_HLPC_STOPPED "Stopped." // what a fake compile its tick stopped reports typedef struct { // Reset every fake to its load-time state. @@ -68,21 +69,27 @@ typedef struct { bool (*serBreak)(int32_t com); // ---- secLink (Comm*) ---- - // Force secLinkOpen to return NULL / secLinkSendBuf to fail. + // Force secLinkOpen to return NULL / secLinkSend to fail. void (*commFail)(bool openFails, bool sendFails); // Deliver bytes to every open link's receive callback on a channel. void (*commInject)(const void *data, int32_t len, uint8_t channel); // Number of currently open links. int32_t (*commLinks)(void); - // Last payload handed to secLinkSend/secLinkSendBuf. + // Last payload handed to secLinkSend. const uint8_t *(*commSent)(int32_t *len, uint8_t *channel, bool *encrypted); - // Result secLinkHandshake returns (0 = success). + // Result a key exchange ends with (0 = success). void (*commHandshakeRc)(int32_t rc); // Passphrase the simulated peer uses ("" = none). A handshake whose // link passphrase differs fails with FRT_STUB_ERR_AUTH. void (*commPeerPass)(const char *pass); // Plaintext flag of a channel on the first open link. bool (*commPlain)(uint8_t channel); + // Write data through the byte stream of the last link + // secLinkOpenStream opened (as the link would), and read back from + // it: what reached netBasStreamWrite, and the handle it was given. + // The write's result; *tcp the handle (0: no stream link). + int32_t (*commStreamWrite)(const void *data, int32_t len, int32_t *tcp); + int32_t (*commStreamRead)(uint8_t *buf, int32_t len, int32_t *tcp); // ---- shell idle registry ---- // Invoke every registered idle handler once. @@ -92,6 +99,11 @@ typedef struct { // ---- help hooks ---- void (*hlpcRc)(int32_t rc); const char *(*hlpcLastOutput)(void); + // The ticks (hlpcSetTickFn) the next compiles make, each after a call + // of during (NULL: none); a tick answering false stops the compile. + void (*hlpcTicks)(int32_t ticks, void (*during)(void)); + // The ticks the last compile made. + int32_t (*hlpcTicked)(void); const char *(*shellLastArgs)(void); } FrtStubApiT; diff --git a/src/test/formrt/testFontProps.c b/src/test/formrt/testFontProps.c index 873f72c2..bc152016 100644 --- a/src/test/formrt/testFontProps.c +++ b/src/test/formrt/testFontProps.c @@ -30,7 +30,7 @@ // bin/shared/fonts in as SHARED/FONTS. #include "frtHarness.h" -#include "../../apps/kpunch/dvxbasic/formrt/frmModel.h" +#include "../../apps/kpunch/dvxbasic/basdev/frmModel.h" #include "dvxDraw.h" #include "dvxPlat.h" #include "dvxText.h" @@ -713,8 +713,8 @@ static void caseFontProps(void) { // A RichEdit from BASIC: its text, a selection given a style, a color, a -// family and a size, read back; alignment and Undo; Find; the RTF -// holding the bold run +// family and a size, read back; alignment and Undo; Find (any case, and +// matching it); the RTF holding the bold run static void caseRichEdit(void) { tCase("RichEdit from BASIC: selection styles, alignment, undo, Find, TextRTF"); tInit(FP_SCREEN_W, FP_SCREEN_H, FP_SCREEN_BPP); @@ -742,6 +742,7 @@ static void caseRichEdit(void) { " Edit1.Undo\n" " IF Edit1.SelAlignment = 0 AND Edit1.CanRedo THEN PRINT \"undone\"\n" " IF Edit1.Find(\"WORLD\") = 6 THEN PRINT \"found\"\n" + " IF Edit1.Find(\"WORLD\", True) = -1 AND Edit1.Find(\"world\", True) = 6 AND Edit1.Find(\"WORLD\", False) = 6 THEN PRINT \"case\"\n" " IF INSTR(Edit1.TextRTF, \"\\b\") > 0 THEN PRINT \"rtf\"\n" "End Sub\n"; FrtAppT app; @@ -755,7 +756,7 @@ static void caseRichEdit(void) { frtStart(&app, NULL); TEST_TRUE(frtFire(&app, "Form1", "Fill")); - TEST_STR_EQ(frtOut(&app), "styled\nfont\ncentered\nundone\nfound\nrtf\n"); + TEST_STR_EQ(frtOut(&app), "styled\nfont\ncentered\nundone\nfound\ncase\nrtf\n"); frtDestroy(&app); tShutdown(); } diff --git a/src/test/formrt/testFormRt.c b/src/test/formrt/testFormRt.c index d363d3c1..2d753569 100644 --- a/src/test/formrt/testFormRt.c +++ b/src/test/formrt/testFormRt.c @@ -44,12 +44,14 @@ #include "dvxCur.h" #include "dvxDlg.h" #include "platform/dvxPlatHost.h" +#include "platform/dvxPlatIsr.h" #include "dvxWgtP.h" #include "dvxVideo.h" #include "dvxWm.h" #include "dvxText.h" #include "richText.h" -#include "../../libs/kpunch/serial/seclink/secLink.h" +#include "taskSwch.h" +#include "../../libs/kpunch/seclink/secLink.h" #include "../../libs/kpunch/hlpfile/hlpFormat.h" #include @@ -59,6 +61,7 @@ #include #include #include +#include #define FRT_OWN_FONTS_DIR "kpunch" // DVX's own fonts' folder in FONTS (src/tools/Makefile) #define SCREEN_W 800 @@ -74,6 +77,10 @@ #define FRM_TEXT_MAX 16384 #define POPUP_OFF_X 5 #define POPUP_OFF_Y 7 +#define LINE_THICKNESS 2 // a Line's etched pair (SEPARATOR_THICKNESS) +#define FORM_MAX_W 320 // caseFormLimits: the .frm's MaxWidth +#define FORM_MAX_H 150 // and the MaxHeight its code sets +#define TERM_NUL_PASTE_LEN 3 // "x", CHR$(0), "y" #define MENU_ID_BASE 10000 // New-case constants: the stub UART identity (mirrors frtStubs.c), the @@ -82,11 +89,13 @@ #define SER_STUB_BASE 0x3F8 #define SER_STUB_IRQ 4 #define SER_STUB_UART 4 +#define SER_TCP_HANDLE 7 // a net.bas connection handle for CommOpenTcp #define SQL_FAKE_DB 7 #define SQL_FAKE_RS 42 #define SQL_FAKE_ROWS 2 -#define RES_HANDLE_PROBE 9 +#define RES_HANDLE_PROBE 9 // ResOpen handles held at once (more than the old fixed eight) #define FRT_EXT_WIDE_RESULT 0x12345 +#define FRT_SETTLED_TIME 946684800 // 2000-01-01: a file time long past #define FRT_EXT_BIN_MAX 64 // frtExtXor result buffer #define FRT_FIT_BIG_W 600 // caseFormWindowMembers: the size FitWidth / FitHeight shrink from #define FRT_FIT_BIG_H 400 @@ -104,6 +113,7 @@ #define FRT_SARROW_X 40 // caseDrawThemed Form1_HBar: DrawScrollArrow's button (16 square, pointing right) #define FRT_SARROW_Y 30 #define FRT_PAINT_PUMPS 3 // event pumps that paint a newly shown window +#define FRT_HELP_TICKS 3 // caseHelpHooks: ticks the stub help compiler makes #define FRT_EXT_BIN_NULL (-1) // frtExtXor key: return NULL #define FRT_EXT_BIN_NOLEN 99 // frtExtXor key: leave *outLen alone #define FRT_EXT_LEN_WEIGHT 100 // frtExtBinSum: weight of the first length @@ -146,6 +156,7 @@ #define FRT_PARDSGN_FRM FRT_INCLUDE_DIR "/../../test/dos/fixtures/pardsgn/pardsgn.frm" #define FRT_PARDSGN_TICKS 50 // clock steps the fixture gets to run its checks #define FRT_PARDSGN_TICK_MS 100 +#define FRT_IDLE_STEP_MS 1000 // frtSecondIdle: the clock a frame #define FRT_DYN_TYPE "frtdyn" // caseDynamicControl's widget type #define FRT_DYN_BAS "FrtDyn" #define FRT_DYN_LEVEL 9 @@ -189,7 +200,9 @@ void CommClose(int32_t handle); void CommDetach(int32_t handle); int32_t CommHandshake(int32_t handle, const char *passphrase); int32_t CommIsReady(int32_t handle); +int32_t CommLastError(int32_t handle); int32_t CommOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake); +int32_t CommOpenTcp(int32_t tcp); int32_t CommPending(int32_t handle); int32_t CommPoll(int32_t handle); const uint8_t *CommRecv(int32_t handle, int32_t channel, int32_t *outLen); @@ -249,6 +262,7 @@ int32_t SQLAffected(int32_t db); void SQLClose(int32_t db); int32_t SQLEof(int32_t rs); const char *SQLError(int32_t db); +const char *SQLEscape(const char *text); int32_t SQLExec(int32_t db, const char *sql); const char *SQLField(int32_t rs, const char *colName); int32_t SQLFieldCount(int32_t rs); @@ -269,6 +283,19 @@ typedef struct { char button[16]; } DialogCtxT; +// caseLoadFormsYield's other task: its turns, until stop +typedef struct { + int32_t turns; + bool stop; +} TaskTurnsT; + +// caseCurrentPerTask: a program's runtime, and the one current when its +// code resumed from a yield +typedef struct { + BasFormRtT *rt; + BasFormRtT *seen; +} TaskRtCheckT; + // caseNewArgs' message box script typedef struct { FrtAppT *app; @@ -276,6 +303,16 @@ typedef struct { bool firedDuring; // NewArgs ran while the box was open } NewArgsDlgT; +// caseHelpHooks: the HelpCompile calls made during a compile's ticks +// (helpNestCompile), what the last saw, and the runtime they end (NULL: +// none) +typedef struct { + int32_t calls; + int32_t rc; + char messages[FRT_OUT_MAX]; + BasFormRtT *stopRt; +} HelpNestT; + // Modal-dialog driver: type text and accept with OK, or cancel with // Esc when no text is given. @@ -290,6 +327,10 @@ typedef struct { static FrtAppT *sModalCheckApp = NULL; static int32_t sModalChecks = 0; +// The pump SQLOpen gave the fake database (sqlFakeSetPump) +static bool (*sSqlPump)(int32_t db) = NULL; +static int32_t sSqlPumpDb = 0; + // frmParse unit-test recorder. typedef struct { @@ -325,6 +366,7 @@ static void caseAppCommandLine(void); static void caseAppIconEd(void); static void caseAppNotepad(void); static void caseAppNotepadCloseRequest(void); +static void caseBarLayoutProps(void); static void caseBoxLayoutProps(void); static void caseBuildProject(void); static void caseClipboard(void); @@ -332,6 +374,7 @@ static void caseCommonMethods(void); static void caseControlArrays(void); static void caseControlMembers2(void); static void caseCreateFormDynamic(void); +static void caseCurrentPerTask(void); static void caseCustomEvents(void); static void caseDataBinding(void); static void caseDefaultCancelButtons(void); @@ -341,6 +384,7 @@ static void caseDialogWrappers(void); static void caseDragDrop(void); static void caseDrawMethods(void); static void caseDrawPaint(void); +static void caseDrawPaintPicture(void); static void caseDrawScale(void); static void caseDrawStyle(void); static void caseDrawThemed(void); @@ -351,7 +395,9 @@ static void caseEvents(void); static void caseExternCalls(void); static void caseExternLive(void); static void caseExternMarshal(void); +static void caseFormFullScreen(void); static void caseFormInstances(void); +static void caseFormLimits(void); static void caseFormShowHide(void); static void caseFormVars(void); static void caseFormWindowMembers(void); @@ -363,8 +409,12 @@ static void caseHandlerError(void); static void caseHelpHooks(void); static void caseHelpPane(void); static void caseHelpTopic(void); +static void caseListSetItems(void); static void caseListTreeMethods(void); static void caseLoadForm2(void); +static void caseLoadFormsYield(void); +static void caseLoadPicture(void); +static void caseLoadPictureData(void); static void caseMenuBarRuntime(void); static void caseMenuCaption(void); static void caseMenuRuntime(void); @@ -375,6 +425,7 @@ static void caseMethodSignatures(void); static void caseModalShow(void); static void caseMsgBox(void); static void caseNewArgs(void); +static void caseOptionGroup(void); static void caseParDsgnFixture(void); static void casePendingUnloadDrain(void); static void casePictureLoadSave(void); @@ -394,11 +445,13 @@ static void caseRunSimple(void); static void caseRuntimeError(void); static void caseScreenMembers(void); static void caseScreenObject(void); +static void caseScreenPaint(void); static void caseSerialLoopback(void); static void caseSerialUnresolved(void); static void caseSetEvent(void); static void caseSqlWrappers(void); static void caseTermBind(void); +static void caseTermSend(void); static void caseTextBoxProps(void); static void caseTimers(void); static void caseToolbarClearFromHandler(void); @@ -423,6 +476,8 @@ static WidgetT *frtDynCreate(WidgetT *parent, const WgtIfaceT *iface); static bool frtDynGet(WidgetT *w, int32_t propIdx, WgtValueT *out); static bool frtDynSet(WidgetT *w, int32_t propIdx, const WgtValueT *value); static int32_t frtProbeRetIntStr(WidgetT *w, const char *text); +static void frtSecondIdle(void *ctx); +static void helpNestCompile(void); static void newArgsDuringDialog(AppContextT *ctx, WindowT *modal, void *userCtx); static void outContains(const FrtAppT *app, const char *needle); static void parseOnCtrlBegin(void *userData, const char *typeName, const char *name); @@ -435,6 +490,7 @@ static void parseOnMenuBegin(void *userData, const char *name, int32_t le static void parseOnMenuEnd(void *userData); static void parseOnMenuProp(void *userData, const char *key, const char *value); static const char *roundTripValue(const char *basName, const WgtPropDescT *p, char *buf, int32_t bufSize); +static int32_t screenPixel(const BasScreenT *scr, int32_t x, int32_t y); static const char *serReadText(int32_t com); static int32_t serWriteText(int32_t com, const char *text); static void setStrProp(FrtAppT *app, BasControlT *ctrl, const char *propName, const char *text); @@ -442,6 +498,7 @@ static int32_t sqlFakeAffected(int32_t db); static void sqlFakeClose(int32_t db); static bool sqlFakeEof(int32_t rs); static const char *sqlFakeError(int32_t db); +static int32_t sqlFakeEscape(const char *src, char *dst, int32_t dstSize); static bool sqlFakeExec(int32_t db, const char *sql); static const char *sqlFakeFieldByName(int32_t rs, const char *name); static int32_t sqlFakeFieldCount(int32_t rs); @@ -453,6 +510,9 @@ static void sqlFakeFree(int32_t rs); static bool sqlFakeNext(int32_t rs); static int32_t sqlFakeOpen(const char *path); static int32_t sqlFakeQuery(int32_t db, const char *sql); +static void sqlFakeSetPump(int32_t db, bool (*pump)(int32_t db)); +static void taskCodeYield(void *arg); +static void taskTurns(void *arg); static void termBindUnbound(void *ctx); static int32_t termBindWrite(void *ctx, const uint8_t *data, int32_t len); static void unloadModalCheck(AppContextT *ctx, WindowT *modal, void *userCtx); @@ -481,6 +541,8 @@ static char sBindText[FRT_BIND_MAX]; static int32_t sBindLen = 0; static int32_t sBindUnbound = 0; +static HelpNestT sHelpNest; + // errorDialogCheck: how often a modal box came up, and the last one's // title typedef struct { @@ -1109,6 +1171,108 @@ static void caseAppNotepadCloseRequest(void) { } +// Alignment / Padding / Spacing of a Toolbar, a StatusBar, and a WrapBox, +// from the .frm and from code: each class's own default until set, and 0 +// is no gap. A Line in the form's VBox turned Vertical. +static void caseBarLayoutProps(void) { + tCase("bar layout: Toolbar, StatusBar, WrapBox Alignment/Padding/Spacing, Line Vertical"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Bars\"\n" + " Begin Toolbar Tb1\n" + " Alignment = Right\n" + " Padding = 5\n" + " Spacing = 0\n" + " Begin CommandButton TbA\n" + " Caption = \"A\"\n" + " End\n" + " Begin CommandButton TbB\n" + " Caption = \"B\"\n" + " End\n" + " End\n" + " Begin WrapBox Wb1\n" + " Spacing = 9\n" + " Begin CommandButton WbA\n" + " Caption = \"A\"\n" + " End\n" + " Begin CommandButton WbB\n" + " Caption = \"B\"\n" + " End\n" + " End\n" + " Begin Line Ln1\n" + " Vertical = True\n" + " End\n" + " Begin Line Ln2\n" + " End\n" + " Begin StatusBar Sb1\n" + " Padding = 3\n" + " Begin Label Pane1\n" + " Caption = \"Ready\"\n" + " End\n" + " End\n" + "End\n" + "\n" + "Sub Form1_Test\n" + " Print Tb1.Alignment; Tb1.Padding; Tb1.Spacing; Sb1.Padding; Sb1.Spacing; Wb1.Padding; Wb1.Spacing\n" + " Tb1.Padding = 0\n" + " Sb1.Alignment = \"Center\"\n" + " Wb1.Padding = 6\n" + " Print Tb1.Padding; Sb1.Alignment; Wb1.Padding\n" + " Print Ln1.Vertical; Ln2.Vertical\n" + " Ln1.Vertical = False\n" + " Print Ln1.Vertical\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *tb = form ? frtCtrl(form, "Tb1") : NULL; + BasControlT *tbA = form ? frtCtrl(form, "TbA") : NULL; + BasControlT *tbB = form ? frtCtrl(form, "TbB") : NULL; + BasControlT *wbA = form ? frtCtrl(form, "WbA") : NULL; + BasControlT *wbB = form ? frtCtrl(form, "WbB") : NULL; + BasControlT *sb = form ? frtCtrl(form, "Sb1") : NULL; + BasControlT *pane1 = form ? frtCtrl(form, "Pane1") : NULL; + BasControlT *ln1 = form ? frtCtrl(form, "Ln1") : NULL; + + TEST_TRUE(tb != NULL && tbA != NULL && tbB != NULL && wbA != NULL && wbB != NULL && sb != NULL && pane1 != NULL && ln1 != NULL); + + if (tb && tbA && tbB && wbA && wbB && sb && pane1 && ln1) { + // A vertical Line in a VBox: 2 wide, at least, not stretched across + TEST_EQ(ln1->widget->calcMinW, LINE_THICKNESS); + TEST_EQ(ln1->widget->calcMinH, 0); + + // Toolbar: padded by 5, no gap, pushed to the right + TEST_EQ(tbA->widget->y, tb->widget->y + 5); + TEST_EQ(tbB->widget->x, tbA->widget->x + tbA->widget->w); + TEST_EQ(tbB->widget->x + tbB->widget->w, tb->widget->x + tb->widget->w - 5); + // WrapBox: 9 between the items; StatusBar: padded by 3 + TEST_EQ(wbB->widget->x, wbA->widget->x + wbA->widget->w + 9); + TEST_EQ(pane1->widget->x, sb->widget->x + 3); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Test")); + TEST_STR_EQ(frtOut(&app), "2 5 0 3 2 2 9 \n0 1 6 \nTrue False \nFalse \n"); + + // Padding = 0 is no padding, not the toolbar's default + tPump(1); + TEST_EQ(tbA->widget->y, tb->widget->y); + TEST_EQ(sb->widget->align, AlignCenterE); + TEST_EQ(ln1->widget->calcMinH, LINE_THICKNESS); + } + + frtDestroy(&app); + tShutdown(); +} + + // Padding / Spacing of a form (its content box) and Alignment / Padding / // Spacing of a box control, from the .frm and from code (0 = no gap). static void caseBoxLayoutProps(void) { @@ -1748,6 +1912,52 @@ static void caseCreateFormDynamic(void) { // written back (ByRef), Strings included. // Universal names go to the universal callbacks. Also the PictureBox's // BorderStyle (the canvas border-less mode) from the .frm and from code. +// Two programs' code on two tasks, each yielding inside it (as DoEvents, +// a wait, or a modal dialog does): each finds its own runtime current when +// it resumes, and the main task finds none. +static void caseCurrentPerTask(void) { + tCase("the current runtime is the task's own after its code yields"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + FrtAppT a; + FrtAppT b; + + if (!expectBuild(&a, "", NULL, 0)) { + tShutdown(); + return; + } + + if (!expectBuild(&b, "", NULL, 0)) { + frtDestroy(&a); + tShutdown(); + return; + } + + TaskRtCheckT checks[] = { { a.rt, NULL }, { b.rt, NULL } }; + + basFormRtSetCurrent(NULL); + TEST_EQ(tsInit(), TS_OK); + TEST_TRUE(tsCreate("rtA", taskCodeYield, &checks[0], 0, TS_PRIORITY_NORMAL) > 0); + TEST_TRUE(tsCreate("rtB", taskCodeYield, &checks[1], 0, TS_PRIORITY_NORMAL) > 0); + + while (tsActiveCount() > 1) { + tsYield(); + TEST_TRUE(basFormRtCurrent() == NULL); + } + + tsShutdown(); + TEST_TRUE(checks[0].seen == a.rt); + TEST_TRUE(checks[1].seen == b.rt); + + // A runtime that ends is current nowhere + basFormRtSetCurrent(b.rt); + frtDestroy(&b); + TEST_TRUE(basFormRtCurrent() == NULL); + frtDestroy(&a); + tShutdown(); +} + + static void caseCustomEvents(void) { tCase("custom events through onEvent, ByRef write-back, PictureBox BorderStyle"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -2245,7 +2455,7 @@ static void caseDesignerRegistry(void) { TEST_TRUE(frtFire(&app, "Form1", "Types")); TEST_STR_EQ(frtOut(&app), "1 True True 0 \n" - "3 2 0 -1 \n" + "3 2 2 -1 \n" "Command Change Load True \n" "True True \n" "True True \n" @@ -2886,6 +3096,66 @@ static void caseDrawPaint(void) { } +// PaintPicture's paths: a copy at the source's size (and with the source +// rectangle partly outside the source), a stretched copy, a raster +// operation, and a picture file decoded once for the runtime. +static void caseDrawPaintPicture(void) { + tCase("PaintPicture copies, stretches, combines, and keeps picture files"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Paint\"\n" + " Begin PictureBox Pic\n" + " End\n" + " Begin PictureBox Pic2\n" + " End\n" + "End\n" + "Sub Form1_T ()\n" + " Pic.Resize 8, 8\n" + " Pic.Line (0, 0)-(7, 7), RGB(255, 0, 0), BF\n" + " Pic.PSet (0, 0), RGB(0, 255, 0)\n" + " Pic2.Resize 16, 16\n" + " Pic2.Line (0, 0)-(15, 15), RGB(0, 0, 255), BF\n" + " Pic2.PaintPicture \"Pic\", 4, 4\n" + " Print Pic2.Point(4, 4); Pic2.Point(5, 5); Pic2.Point(3, 3)\n" + " Pic2.PaintPicture \"Pic\", 0, 0, 16, 16\n" + " Print Pic2.Point(1, 1); Pic2.Point(2, 2)\n" + " Pic2.PaintPicture \"Pic\", 0, 0, 8, 8, -4, -4, 8, 8\n" + " Print Pic2.Point(4, 4); Pic2.Point(3, 3)\n" + " Pic2.Line (0, 0)-(15, 15), RGB(0, 0, 255), BF\n" + " Pic2.PaintPicture \"Pic\", 0, 0, 8, 8, 0, 0, 8, 8, vbSrcInvert\n" + " Print Pic2.Point(1, 1); Pic2.Point(0, 0); Pic2.Point(9, 9)\n" + " Pic2.PaintPicture \"" FRT_FIXTURE_DIR "/icon.bmp\", 0, 0\n" + " Pic2.PaintPicture \"" FRT_FIXTURE_DIR "/icon.bmp\", 0, 0\n" + "End Sub\n"; + FrtAppT app; + struct utimbuf settled; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + // A file changed in the last two seconds is decoded every time (FAT + // times): the fixture is made older than that + settled.actime = FRT_SETTLED_TIME; + settled.modtime = FRT_SETTLED_TIME; + TEST_EQ(utime(FRT_FIXTURE_DIR "/icon.bmp", &settled), 0); + frtStart(&app, NULL); + TEST_TRUE(frtFire(&app, "Form1", "T")); + TEST_STR_EQ(frtOut(&app), + "65280 255 16711680 \n" + "65280 255 \n" + "65280 255 \n" + "16711935 16776960 16711680 \n"); + TEST_EQ((int32_t)arrlen(app.rt->drawPics), 1); + TEST_TRUE(app.rt->drawPicsBytes > 0); + frtDestroy(&app); + tShutdown(); +} + + // ScaleMode, ScaleLeft / Top / Width / Height, Scale and hatched // FillStyles on a PictureBox (round 2). static void caseDrawScale(void) { @@ -3922,6 +4192,61 @@ static void caseExternMarshal(void) { } +// A form's FullScreen: its window's own content on the whole screen +// (dvxExclusiveMirror), the FullScreen event, reading it back, giving it +// back; FULLSCREEN (the program screen's) reads False and makes no +// screen. +static void caseFormFullScreen(void) { + tCase("Form FullScreen, its event, and FULLSCREEN's state"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Full\"\n" + " Begin CommandButton Cmd1\n" + " Caption = \"Go\"\n" + " End\n" + "End\n" + "\n" + "Sub Form1_FullScreen(State As Integer)\n" + " Print \"state\"; State; State = dvxFullScreenActive\n" + "End Sub\n" + "Sub Form1_Go\n" + " Print FULLSCREEN; Me.FullScreen\n" + " Me.FullScreen = True\n" + " Print Me.FullScreen\n" + " Me.FullScreen = False\n" + " Print Me.FullScreen; dvxFullScreenEnded; dvxFullScreenSuspended\n" + "End Sub\n" + "Sub Form1_Stay\n" + " Me.FullScreen = True\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Go")); + TEST_STR_EQ(frtOut(&app), "False False \nstate1 True \nTrue \nFalse 0 2 \n"); + TEST_TRUE(app.vm->screen == NULL); + TEST_TRUE(tCtx()->exclusive.owner == NULL); + + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Stay")); + TEST_TRUE(tCtx()->exclusive.owner != NULL && tCtx()->exclusive.mirror); + tPump(1); + TEST_TRUE(tCtx()->exclusive.active); + + frtDestroy(&app); + TEST_TRUE(tCtx()->exclusive.owner == NULL); + tShutdown(); +} + + // New FormName: each copy has its own window, controls, menus, // form-level variables and Me; its events run against it; the form's // name stays the first copy; unloading a copy keeps the others. A @@ -4058,6 +4383,63 @@ static void caseFormInstances(void) { } +// Form MaxWidth / MaxHeight (from the .frm and from code; 0 is the +// screen's size, and a window larger than a new limit shrinks to it) and +// GameInput (every key to the form, none to F1, F10, or accelerators). +static void caseFormLimits(void) { + tCase("Form MaxWidth / MaxHeight / GameInput"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Limits\"\n" + " Width = 300\n" + " Height = 200\n" + " Resizable = True\n" + " MaxWidth = 320\n" + " GameInput = True\n" + "End\n" + "\n" + "Sub Form1_Test\n" + " Print Me.MaxWidth; Me.MaxHeight; Me.GameInput\n" + " Me.MaxWidth = 250\n" + " Me.MaxHeight = 150\n" + " Me.GameInput = False\n" + " Print Me.Width; Me.Height; Me.GameInput\n" + " Me.MaxWidth = 0\n" + " Print Me.MaxWidth\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + + BasFormT *form = frtForm(&app, "Form1"); + WindowT *win = form ? form->window : NULL; + + TEST_TRUE(win != NULL); + + if (win) { + TEST_EQ(win->maxW, FORM_MAX_W); + TEST_EQ(win->maxH, WM_MAX_FROM_SCREEN); + TEST_TRUE(win->gameInput); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Test")); + TEST_STR_EQ(frtOut(&app), "320 0 True \n250 150 False \n0 \n"); + TEST_EQ(win->maxW, WM_MAX_FROM_SCREEN); + TEST_EQ(win->maxH, FORM_MAX_H); + TEST_TRUE(!win->gameInput); + } + + frtDestroy(&app); + tShutdown(); +} + + static void caseFormShowHide(void) { tCase("Show modal / non-modal and Hide"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -4788,7 +5170,8 @@ static void caseFrmParserUnit(void) { // If Text1 = Text2 compares the texts), the control itself as an argument // (col.Add Text1) and with Is; a picture property reads as a Picture // object whose value is its file name; LoadPicture's size and colour -// arguments; a class object's default member read and assigned. +// arguments; a class object's default member read and assigned; a +// PictureBox's Picture and a ListBox's Text. static void caseGapClosure(void) { tCase("gap closure: control default properties, Picture objects, LoadPicture arguments, PaintPicture raster operations"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -4809,8 +5192,28 @@ static void caseGapClosure(void) { " End\n" " Begin PictureBox Pic2\n" " End\n" + " Begin ListBox List1\n" + " End\n" "End\n" "\n" + "Sub Form1_PicList\n" + " Print \"[\"; Pic; \"]\"; InStr(Img, \"icon.bmp\") > 0\n" + " Pic = \"" FRT_FIXTURE_DIR "/icon.bmp\"\n" + " Print InStr(Pic, \"icon.bmp\") > 0; Pic.ImageWidth; InStr(Pic.Picture, \"icon.bmp\") > 0\n" + " Pic.Picture = LoadPicture(\"\")\n" + " Print Len(Pic.Picture); Pic.ImageWidth\n" + " Pic.Picture = LoadPicture(\"" FRT_FIXTURE_DIR "/icon.bmp\")\n" + " Print InStr(Pic.Picture, \"icon.bmp\") > 0\n" + " Pic.Resize 4, 4\n" + " Print Len(Pic.Picture)\n" + " List1.AddItem \"Alpha\"\n" + " List1.AddItem \"Beta\"\n" + " Print \"[\"; List1; \"]\"\n" + " List1 = \"beta\"\n" + " Print List1.ListIndex; List1; \" \"; List1.Text\n" + " List1.Text = \"none\"\n" + " Print List1.ListIndex\n" + "End Sub\n" "Sub Form1_Rop\n" " Pic.Resize 4, 4\n" " Pic2.Resize 4, 4\n" @@ -4864,6 +5267,13 @@ static void caseGapClosure(void) { TEST_TRUE(frtFire(&app, "Form1", "Bad")); TEST_STR_EQ(frtOut(&app), "err5 \n"); + // A PictureBox's Picture and an Image's (their default property, a + // Picture object whose own default is its file name) and a ListBox's + // Text (its default property): the selected item, set by text + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "PicList")); + TEST_STR_EQ(frtOut(&app), "[]True \nTrue 32 True \n0 32 \nTrue \n0 \n[]\n1 Beta Beta\n1 \n"); + // Source red over destination blue: OR, AND, XOR, NOT destination, // copy, NOT source (one pixel untouched) frtOutClear(&app); @@ -5067,6 +5477,30 @@ static void caseHelpHooks(void) { TEST_TRUE(HelpCompileMessages()[0] != '\0'); TEST_EQ(HelpCompile("x.dhs", NULL), 0); + // The compiler's ticks: a HelpCompile meanwhile is refused at once, + // the running compile's messages left alone + memset(&sHelpNest, 0, sizeof(sHelpNest)); + api->hlpcTicks(FRT_HELP_TICKS, helpNestCompile); + TEST_EQ(HelpCompile("guide.dhs", "ticks.hlp"), 0); + TEST_EQ(api->hlpcTicked(), FRT_HELP_TICKS); + TEST_EQ(sHelpNest.calls, FRT_HELP_TICKS); + TEST_EQ(sHelpNest.rc, 0); + TEST_STR_EQ(sHelpNest.messages, FRT_STUB_HLPC_ERROR "\n"); + TEST_STR_EQ(api->hlpcLastOutput(), "ticks.hlp"); + TEST_STR_EQ(HelpCompileMessages(), FRT_STUB_HLPC_ERROR "\n"); + api->hlpcRc(0); + TEST_EQ(HelpCompile("guide.dhs", "ticks.hlp"), -1); + TEST_STR_EQ(HelpCompileMessages(), ""); + + // The program ending stops the compile at its next tick + sHelpNest.calls = 0; + sHelpNest.stopRt = app.rt; + TEST_EQ(HelpCompile("guide.dhs", "stop.hlp"), 0); + TEST_EQ(api->hlpcTicked(), 1); + TEST_STR_EQ(HelpCompileMessages(), FRT_STUB_HLPC_STOPPED "\n"); + app.rt->terminated = false; + api->hlpcTicks(0, NULL); + // A bare help file name resolves against the app directory. char expected[DVX_MAX_PATH * 2]; @@ -5327,9 +5761,62 @@ static void caseHelpTopic(void) { } -// ListView SetColumns alignment, SetRows, SelectedRows$ and ColumnClick -// (a string method named with or without BASIC's $); TreeView ListIndex, -// item tags and HasChildren. +// SetItems on a ListBox, a ComboBox, and a DropDown: the "|" list +// replaces the items (every "|" separates, "" empties), nothing is +// selected after it, and a ComboBox keeps its text. +static void caseListSetItems(void) { + tCase("ListBox / ComboBox / DropDown SetItems"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Items\"\n" + " Begin ListBox Lb1\n" + " End\n" + " Begin ComboBox Cb1\n" + " End\n" + " Begin DropDown Dd1\n" + " End\n" + "End\n" + "\n" + "Sub Form1_Fill\n" + " Lb1.SetItems \"Red|Green|Blue\"\n" + " Cb1.SetItems \"Red|Green|Blue\"\n" + " Dd1.SetItems \"Red|Green|Blue\"\n" + " Print Lb1.ListCount(); Lb1.List(1); Cb1.ListCount(); Cb1.List(2); Dd1.ListCount(); Dd1.List(0)\n" + " Lb1.ListIndex = 1\n" + " Cb1.ListIndex = 1\n" + " Dd1.ListIndex = 1\n" + " Cb1.Text = \"typed\"\n" + " Lb1.SetItems \"a|\"\n" + " Cb1.SetItems \"a|\"\n" + " Dd1.SetItems \"a|\"\n" + " Print Lb1.ListCount(); \"[\"; Lb1.List(1); \"]\"; Lb1.ListIndex; Cb1.ListCount(); Cb1.ListIndex; Cb1.Text; Dd1.ListCount(); Dd1.ListIndex\n" + " Lb1.SetItems \"\"\n" + " Cb1.SetItems \"\"\n" + " Dd1.SetItems \"\"\n" + " Print Lb1.ListCount(); Cb1.ListCount(); Dd1.ListCount()\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Fill")); + TEST_STR_EQ(frtOut(&app), "3 Green3 Blue3 Red\n2 []-1 2 -1 typed2 -1 \n0 0 0 \n"); + frtDestroy(&app); + tShutdown(); +} + + +// ListView SetColumns alignment, SetRows, SelectedRows$, ColumnClick (a +// string method named with or without BASIC's $), SortColumn, and +// SortOrder; TreeView ListIndex, item tags, HasChildren, ItemDepth, +// ItemParent, SetItemText, and RemoveItem. static void caseListTreeMethods(void) { tCase("ListView SetRows / SelectedRows / ColumnClick, TreeView ListIndex / tags"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -5363,9 +5850,22 @@ static void caseListTreeMethods(void) { " LV1.SetRows \"x\" & Chr$(10) & \"y\"\n" "End Sub\n" "Sub LV1_ColumnClick(Column As Integer)\n" - " Print \"col\"; Column\n" + " Print \"col\"; Column; LV1.SortColumn; LV1.SortOrder\n" " LV1.SetSort -1, 0\n" "End Sub\n" + "Sub Form1_Sort\n" + " Print LV1.SortColumn; LV1.SortOrder;\n" + " LV1.SortColumn = 2\n" + " Print LV1.SortColumn; LV1.SortOrder;\n" + " LV1.SortOrder = 2\n" + " Print LV1.SortColumn; LV1.SortOrder;\n" + " LV1.SortColumn = 1\n" + " Print LV1.SortColumn; LV1.SortOrder;\n" + " LV1.SortColumn = 7\n" + " Print LV1.SortColumn; LV1.SortOrder;\n" + " LV1.SortOrder = 1\n" + " Print LV1.SortColumn; LV1.SortOrder\n" + "End Sub\n" "Sub Form1_Tree\n" " TV1.AddItem \"root\"\n" " TV1.AddChildItem 0, \"b\"\n" @@ -5380,6 +5880,17 @@ static void caseListTreeMethods(void) { " TV1.ListIndex = -1\n" " Print \"[\"; TV1.GetItemTag(1); \"]\"; TV1.ListIndex\n" "End Sub\n" + "Sub Form1_TreeEdit\n" + " Print TV1.ItemDepth(0); TV1.ItemDepth(2); TV1.ItemParent(0); TV1.ItemParent(2); TV1.ItemParent(3); TV1.ItemDepth(9)\n" + " TV1.SetItemText 1, \"B\"\n" + " Print TV1.GetItemText(1)\n" + " TV1.ListIndex = 3\n" + " TV1.RemoveItem 1\n" + " Print TV1.ItemCount(); TV1.GetItemText(1); TV1.ListIndex; TV1.ItemParent(1)\n" + " TV1.RemoveItem 1\n" + " TV1.RemoveItem 5\n" + " Print TV1.ItemCount(); TV1.ListIndex\n" + "End Sub\n" "Sub TV1_Change\n" " changes = changes + 1\n" "End Sub\n"; @@ -5408,12 +5919,25 @@ static void caseListTreeMethods(void) { tWidgetCenter(lv->widget, &cx, &cy); frtOutClear(&app); tClick(cx - lv->widget->w / 2 + LV_SECOND_COL_X, cy - lv->widget->h / 2 + LV_HEADER_Y); - TEST_STR_EQ(frtOut(&app), "col1 \n"); + TEST_STR_EQ(frtOut(&app), "col1 1 1 \n"); } + // SortColumn / SortOrder: read what SetSort (or the click) chose; a + // column keeps the order, an order keeps the column, a bad column + // unsorts, and an unsorted list stays unsorted + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Sort")); + TEST_STR_EQ(frtOut(&app), "-1 0 2 1 2 2 1 2 -1 0 -1 0 \n"); + frtOutClear(&app); TEST_TRUE(frtFire(&app, "Form1", "Tree")); TEST_STR_EQ(frtOut(&app), "4 b1cpath-b[]True False \n3 1 \n[]-1 \n"); + + // TreeView ItemDepth / ItemParent / SetItemText / RemoveItem: root, + // b (b1 under it), c; removing b takes b1 and moves c (current) up + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "TreeEdit")); + TEST_STR_EQ(frtOut(&app), "0 2 -1 1 0 -1 \nB\n2 c1 0 \n1 -1 \n"); frtDestroy(&app); tShutdown(); } @@ -5493,6 +6017,42 @@ static void caseLoadForm2(void) { // HiMetric and Type, ScaleX / ScaleY between the scales (defaults, a // ScaleMode, a user scale), and the errors for a missing file and a file // that is not a picture. +// Starting a program lets the other tasks run between its forms. +static void caseLoadFormsYield(void) { + tCase("loading a program's forms lets the other tasks run"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frms[] = { + "Begin Form Form1\nEnd\n", + "Begin Form Form2\nEnd\n", + "Begin Form Form3\nEnd\n" + }; + FrtAppT app; + TaskTurnsT other; + + if (!expectBuild(&app, "", frms, DVX_ARRAY_LEN(frms))) { + tShutdown(); + return; + } + + memset(&other, 0, sizeof(other)); + TEST_EQ(tsInit(), TS_OK); + TEST_TRUE(tsCreate("turns", taskTurns, &other, 0, TS_PRIORITY_NORMAL) > 0); + basFormRtLoadAllForms(app.rt, "Form1"); + TEST_EQ((int32_t)arrlen(app.rt->forms), DVX_ARRAY_LEN(frms)); + TEST_TRUE(other.turns >= DVX_ARRAY_LEN(frms)); + other.stop = true; + + while (tsActiveCount() > 1) { + tsYield(); + } + + tsShutdown(); + frtDestroy(&app); + tShutdown(); +} + + static void caseLoadPicture(void) { tCase("LoadPicture and Picture objects"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -5569,6 +6129,93 @@ static void caseLoadPicture(void) { } +// LoadPictureData: a Picture object for a picture file's bytes (the same +// bytes, the same object) that every picture property takes and reads +// back (until the control drops it: a PictureBox's Resize), and +// PaintPicture draws; SetPictureData shows bytes without one, Picture +// reading the name given; bytes that are no picture are error 481; a +// control without a picture to show stops the program. +static void caseLoadPictureData(void) { + tCase("LoadPictureData and SetPictureData"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Bytes\"\n" + " Begin Label Src\n" + " Caption = \"Source\"\n" + " End\n" + " Begin Image Img\n" + " End\n" + " Begin PictureBox Pic\n" + " End\n" + "End\n" + "\n" + "Function IconBytes() As String\n" + " Open \"" FRT_FIXTURE_DIR "/icon.bmp\" For Binary As #1\n" + " IconBytes = Input$(LOF(1), #1)\n" + " Close #1\n" + "End Function\n" + "Sub Form1_Use\n" + " Dim d As String\n" + " Dim p As Object\n" + " Dim e As Object\n" + " d = IconBytes()\n" + " Set p = LoadPictureData(d)\n" + " Print TypeName(p); \" \"; p Is LoadPictureData(d); p.Width; p.Type; \"[\"; p; \"]\"\n" + " Img.Picture = p\n" + " Print Img.ImageWidth; Img.Picture Is p; TypeName(Img.Picture)\n" + " Pic.Picture = p\n" + " Print Pic.ImageWidth; Pic.Picture Is p\n" + " Pic.Resize 4, 4\n" + " Print Pic.Picture Is p\n" + " Set Src.DragIcon = p\n" + " Print Src.DragIcon Is p\n" + " Form1.Icon = p\n" + " Print Form1.Icon Is p\n" + " Pic.PaintPicture p, 0, 0\n" + " Print Img.SetPictureData(d); Img.SetPictureData(\"junk\"); Pic.SetPictureData(d)\n" + " Print Img.SetPictureData(d, \"net.png\"); Img.Picture\n" + " Img.Picture = \"" FRT_FIXTURE_DIR "/icon.bmp\"\n" + " Print Img.Picture Is p; InStr(Img.Picture, \"icon.bmp\") > 0\n" + " Set e = LoadPictureData(\"\")\n" + " Print e.Type\n" + "End Sub\n" + "Sub Form1_Bad\n" + " On Error GoTo failed\n" + " Dim p As Object\n" + " Set p = LoadPictureData(\"not a picture\")\n" + " Exit Sub\n" + "failed:\n" + " Print \"err\"; Err\n" + "End Sub\n" + "Sub Form1_NoPicture\n" + " Src.Picture = LoadPictureData(IconBytes())\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Use")); + TEST_STR_EQ(frtOut(&app), "Picture True 847 1 []\n32 True Picture\n32 True \nFalse \nTrue \nTrue \nTrue False True \nTrue net.png\nFalse True \n0 \n"); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Bad")); + TEST_STR_EQ(frtOut(&app), "err481 \n"); + TEST_TRUE(!app.rt->terminated); + + frtFire(&app, "Form1", "NoPicture"); + TEST_TRUE(app.rt->terminated); + + frtDestroy(&app); + tShutdown(); +} + + // Menus made at run time: AddMenu (a menu bar on a form without one), // AddMenuItem with a shortcut key, AddMenuRadioItem, a menu item's // Shortcut read and changed, RemoveMenu (the bar goes with its last @@ -6681,6 +7328,53 @@ static void caseObjectRefs(void) { } +// OptionButtons in a Frame from BASIC: OptionIndex, GetSelected (-1 with +// none on), SetSelected, and Value, each read back through the group. +static void caseOptionGroup(void) { + tCase("OptionButton OptionIndex / GetSelected / SetSelected / Value"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Options\"\n" + " Begin Frame Fr1\n" + " Begin OptionButton Opt1\n" + " Caption = \"One\"\n" + " End\n" + " Begin OptionButton Opt2\n" + " Caption = \"Two\"\n" + " End\n" + " Begin OptionButton Opt3\n" + " Caption = \"Three\"\n" + " End\n" + " End\n" + "End\n" + "\n" + "Sub Form1_Test\n" + " Print Opt1.OptionIndex; Opt3.OptionIndex; Opt2.GetSelected()\n" + " Opt1.SetSelected 2\n" + " Print Opt1.GetSelected(); Opt3.Value; Opt1.Value\n" + " Opt2.Value = True\n" + " Print Opt3.GetSelected(); Opt3.Value\n" + " Opt2.Value = False\n" + " Print Opt1.GetSelected()\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Test")); + TEST_STR_EQ(frtOut(&app), "0 2 -1 \n2 True False \n1 False \n-1 \n"); + frtDestroy(&app); + tShutdown(); +} + + // The DOS feasibility fixture pardsgn on the host: its checks run from a // Timer and put the form in design mode; a click on the Surface frame and // a key, as the DOS script gives them, finish it with "PARDSGN PASS". @@ -8211,12 +8905,14 @@ static void caseResources(void) { TEST_EQ(ResRemove(resPath, "blob"), -1); TEST_EQ(ResExtract(resPath, "greet", "/no/dir/x.out"), 0); - // basFormRtTestReset closes every open resource handle. - TEST_EQ(ResOpen(resPath), 2); - basFormRtTestReset(); - TEST_EQ(ResCount(2), 0); + // A closed handle reads as nothing. + int32_t one = ResOpen(resPath); - // Fill every handle slot from a clean table; the ninth open fails. + TEST_TRUE(one > 0); + ResClose(one); + TEST_EQ(ResCount(one), 0); + + // No fixed number of handles: more than the old eight open at once. int32_t handles[RES_HANDLE_PROBE]; int32_t got = 0; @@ -8230,8 +8926,7 @@ static void caseResources(void) { got++; } - TEST_EQ(got, RES_HANDLE_PROBE - 1); - TEST_EQ(ResOpen(resPath), 0); + TEST_EQ(got, RES_HANDLE_PROBE); for (int32_t i = 0; i < got; i++) { ResClose(handles[i]); @@ -8697,6 +9392,69 @@ static void caseScreenObject(void) { } +// PAINT on the program screen (whole rows read at a time): a region that +// runs round under a wall is filled to the border, over another colour, +// and stops at the edge of the view; DRAW's P paints the same way. +static void caseScreenPaint(void) { + tCase("PAINT and DRAW P on the program screen"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *bas = + "SCREEN 13\n" + "LINE (10, 10)-(60, 40), 14, B\n" + "LINE (30, 10)-(30, 30), 14\n" + "PSET (20, 20), 9\n" + "PAINT (15, 15), 4, 14\n" + "CIRCLE (150, 100), 20, 2\n" + "PAINT (150, 100), 2\n" + "LINE (200, 0)-(200, 199), 5\n" + "PAINT (319, 0), 6, 5\n" + "LINE (100, 150)-(120, 170), 3, B\n" + "DRAW \"BM110,160 P 1,3\"\n"; + FrtAppT app; + + if (!expectBuild(&app, bas, NULL, 0)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + tPump(1); + + const BasScreenT *scr = app.vm->screen; + + TEST_TRUE(scr != NULL); + + if (!scr) { + frtDestroy(&app); + tShutdown(); + return; + } + + TEST_EQ(screenPixel(scr, 15, 15), 4); + TEST_EQ(screenPixel(scr, 45, 15), 4); + TEST_EQ(screenPixel(scr, 20, 20), 4); + TEST_EQ(screenPixel(scr, 30, 35), 4); + TEST_EQ(screenPixel(scr, 59, 39), 4); + TEST_EQ(screenPixel(scr, 30, 20), 14); + TEST_EQ(screenPixel(scr, 10, 10), 14); + TEST_EQ(screenPixel(scr, 5, 5), 0); + TEST_EQ(screenPixel(scr, 61, 41), 0); + TEST_EQ(screenPixel(scr, 150, 100), 2); + TEST_EQ(screenPixel(scr, 150, 85), 2); + TEST_EQ(screenPixel(scr, 150, 125), 0); + TEST_EQ(screenPixel(scr, 201, 0), 6); + TEST_EQ(screenPixel(scr, 319, 199), 6); + TEST_EQ(screenPixel(scr, 199, 0), 0); + TEST_EQ(screenPixel(scr, 110, 160), 1); + TEST_EQ(screenPixel(scr, 101, 151), 1); + TEST_EQ(screenPixel(scr, 100, 150), 3); + TEST_EQ(screenPixel(scr, 99, 150), 0); + frtDestroy(&app); + tShutdown(); +} + + // A ScrollPane fires Change when the user moves it (here: the Down key), // not when the program sets ScrollTop; the window-scrollbar route of the // C demo (a large PictureBox in a NoBorder ScrollPane). @@ -8782,6 +9540,8 @@ static void caseSerialLoopback(void) { " End\n" " Begin Terminal Term2\n" " End\n" + " Begin CommandButton Cmd1\n" + " End\n" "End\n"; FrtAppT app; @@ -8795,9 +9555,15 @@ static void caseSerialLoopback(void) { frtStart(&app, NULL); // Raw serial: open, attach, write, loop bytes back, poll via the - // stub idle registry, close. + // stub idle registry, close. A control that is no Terminal is not + // attached (no poller is registered for it). TEST_EQ(SerOpen(1, 9600, 8, "N", 1, 0), -1); TEST_TRUE(api->serIsOpen(0)); + + int32_t idleBefore = api->idleCount(); + + SerAttach(1, "Cmd1"); + TEST_EQ(api->idleCount(), idleBefore); SerAttach(1, "Term1"); TEST_TRUE(api->idleCount() >= 1); TEST_EQ(serWriteText(1, "hi"), -1); @@ -8871,6 +9637,7 @@ static void caseSerialLoopback(void) { int32_t h = CommOpen(1, 9600, 8, "N", 1, 0); TEST_EQ(h, 1); + CommAttach(h, "Cmd1", 2, 1); CommAttach(h, "Term1", 2, 1); CommAttach(h, "Term2", 2, 1); CommAttach(h, "Term1", 2, 1); @@ -8913,9 +9680,20 @@ static void caseSerialLoopback(void) { TEST_EQ(CommHandshake(h, "secret"), -1); TEST_EQ(CommHandshake(h, "other"), 0); TEST_EQ(CommHandshake(h, ""), 0); + // A peer that never finishes: CommHandshake gives up after its limit, + // the program's events running meanwhile (the clock moves a second a + // frame here) api->commPeerPass(""); api->commHandshakeRc(1); + void (*savedIdle)(void *) = tCtx()->idleCallback; + void (*savedBusy)(void *) = tCtx()->busyCallback; + + tCtx()->idleCallback = frtSecondIdle; + tCtx()->busyCallback = frtSecondIdle; TEST_EQ(CommHandshake(h, NULL), 0); + TEST_EQ(CommLastError(h), SECLINK_ERR_TIMEOUT); + tCtx()->idleCallback = savedIdle; + tCtx()->busyCallback = savedBusy; api->commHandshakeRc(0); TEST_EQ(CommHandshake(9, NULL), 0); @@ -8958,6 +9736,27 @@ static void caseSerialLoopback(void) { api->commFail(false, false); CommClose(h); + // CommOpenTcp: the same link over a net.bas connection, whose stream + // reads and writes go to that connection's handle; channels work as + // on a COM port. No handle, no link. + { + int32_t tcp = 0; + uint8_t got = 0; + int32_t ht = CommOpenTcp(SER_TCP_HANDLE); + + TEST_EQ(CommOpenTcp(0), 0); + TEST_TRUE(ht > 0); + TEST_EQ(api->commStreamWrite("abc", 3, &tcp), 3); + TEST_EQ(tcp, SER_TCP_HANDLE); + tcp = 0; + TEST_EQ(api->commStreamRead(&got, 1, &tcp), 1); + TEST_EQ(tcp, SER_TCP_HANDLE); + TEST_EQ(got, 'r'); + TEST_EQ(commSendText(ht, "tcp", 3, 1), -1); + TEST_STR_EQ(commRecvText(ht, 3), "tcp"); + CommClose(ht); + } + // Shutdown with a live connection and an attached port closes both, // and also closes a port that was opened but never attached. TEST_EQ(CommOpen(1, 9600, 8, "N", 1, 0), 1); @@ -8965,9 +9764,19 @@ static void caseSerialLoopback(void) { SerAttach(1, "Term1"); TEST_EQ(SerOpen(3, 9600, 8, "N", 1, 0), -1); TEST_TRUE(api->serIsOpen(2)); - basFormRtSerialShutdown(); + + // Another program's end leaves this program's link and ports open + // (any other runtime pointer stands for it; it is only compared) + basFormRtSerialShutdown((const BasFormRtT *)&app); + TEST_TRUE(api->serIsOpen(0)); + TEST_TRUE(api->serIsOpen(2)); + TEST_EQ(CommLastError(1), 0); + + // ... and its own end closes them + basFormRtSerialShutdown(app.rt); TEST_TRUE(!api->serIsOpen(0)); TEST_TRUE(!api->serIsOpen(2)); + basFormRtSerialShutdown(NULL); frtDestroy(&app); basFormRtTestReset(); frtStubsClose(); @@ -9016,7 +9825,7 @@ static void caseSerialUnresolved(void) { CommDetach(1); CommClose(1); CommClose(0); - basFormRtSerialShutdown(); + basFormRtSerialShutdown(NULL); TEST_TRUE(!app.rt->terminated); tPump(1); @@ -9201,6 +10010,7 @@ static void caseSqlWrappers(void) { TEST_EQ(SQLFieldInt(1, 0), 0); TEST_TRUE(SQLFieldDbl(1, 0) == 0.0); TEST_EQ(SQLAffected(1), 0); + TEST_STR_EQ(SQLEscape("it's"), ""); SQLFreeResult(1); SQLClose(1); @@ -9222,9 +10032,54 @@ static void caseSqlWrappers(void) { tbl.sqlFieldDbl = sqlFakeFieldDbl; tbl.sqlFreeResult = sqlFakeFree; tbl.sqlAffectedRows = sqlFakeAffected; + tbl.sqlSetPump = sqlFakeSetPump; + tbl.sqlEscape = sqlFakeEscape; basVmSetSqlCallbacks(app.vm, &tbl); + // SQLEscape$ hands the library room for every character doubled + TEST_STR_EQ(SQLEscape("it's"), "iitt''ss"); + TEST_STR_EQ(SQLEscape(""), ""); + TEST_EQ(SQLOpen("db"), SQL_FAKE_DB); + + // A program's database pumps: once a BIOS tick (not again in the same + // one, nor in a control file's code), and stops the statement once + // the program ends + TEST_TRUE(sSqlPump != NULL && sSqlPumpDb == SQL_FAKE_DB); + + if (sSqlPump) { + uint32_t frames; + + platformTimer.biosTicks++; + frames = tCtx()->frameCount; + TEST_TRUE(sSqlPump(SQL_FAKE_DB)); + TEST_TRUE(tCtx()->frameCount > frames); + frames = tCtx()->frameCount; + TEST_TRUE(sSqlPump(SQL_FAKE_DB)); + TEST_EQ(tCtx()->frameCount, frames); + platformTimer.biosTicks++; + app.vm->runLimit = 1; + TEST_TRUE(sSqlPump(SQL_FAKE_DB)); + TEST_EQ(tCtx()->frameCount, frames); + app.vm->runLimit = 0; + app.rt->terminated = true; + TEST_TRUE(!sSqlPump(SQL_FAKE_DB)); + app.rt->terminated = false; + } + + // A library's wait (dvxWaitPump) in the program pumps as DoEvents; + // not in a control file's code, nor once the program ends + uint32_t frames = tCtx()->frameCount; + + TEST_TRUE(dvxWaitPump()); + TEST_TRUE(tCtx()->frameCount > frames); + app.vm->runLimit = 1; + TEST_TRUE(!dvxWaitPump()); + app.vm->runLimit = 0; + app.rt->terminated = true; + TEST_TRUE(!dvxWaitPump()); + app.rt->terminated = false; + TEST_EQ(SQLOpen(NULL), 0); TEST_EQ(SQLExec(SQL_FAKE_DB, "create"), -1); TEST_EQ(SQLExec(SQL_FAKE_DB, NULL), 0); @@ -9346,6 +10201,53 @@ static void caseTermBind(void) { } +// A Terminal with no link, from BASIC: Write goes past a CHR$(0) (the +// status request after it is answered), and the Send event hands the +// program the reply and a paste whole, CHR$(0) included. +static void caseTermSend(void) { + tCase("Terminal: Write past CHR$(0), Send event with the bytes for the host"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Send\"\n" + " Begin Terminal Term1\n" + " End\n" + "End\n" + "\n" + "Sub Form1_Ask\n" + " Term1.Write Chr$(12) & \"ab\" & Chr$(0) & \"cd\" & Chr$(27) & \"[6n\"\n" + "End Sub\n" + "Sub Form1_Paste\n" + " Term1.Paste\n" + "End Sub\n" + "Sub Term1_Send(Bytes As String)\n" + " Print Len(Bytes); Asc(Bytes); Asc(Mid$(Bytes, 2)); Mid$(Bytes, 3)\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Ask")); + tPump(1); + TEST_STR_EQ(frtOut(&app), "6 27 91 1;5R\n"); + + // The paste "x" + CHR$(0) + "y" reaches the program as three bytes + frtOutClear(&app); + dvxClipboardCopy("x\0y", TERM_NUL_PASTE_LEN); + TEST_TRUE(frtFire(&app, "Form1", "Paste")); + tPump(1); + TEST_STR_EQ(frtOut(&app), "3 120 0 y\n"); + frtDestroy(&app); + tShutdown(); +} + + // TextBox MaxLength (the text is cut; 0 = the default) and PasswordChar. static void caseTextBoxProps(void) { tCase("TextBox MaxLength and PasswordChar"); @@ -10294,6 +11196,27 @@ static int32_t frtProbeRetIntStr(WidgetT *w, const char *text) { } +// A frame's idle moment: the clock moves a second (a wait with a limit +// reaches it). +static void frtSecondIdle(void *ctx) { + (void)ctx; + hostClockAdvance(FRT_IDLE_STEP_MS); +} + + +// The stub help compiler's tick hook in caseHelpHooks: a HelpCompile +// while one runs, then the program ended when the case asks. +static void helpNestCompile(void) { + sHelpNest.calls++; + sHelpNest.rc = HelpCompile("other.dhs", "other.hlp"); + snprintf(sHelpNest.messages, sizeof(sHelpNest.messages), "%s", HelpCompileMessages()); + + if (sHelpNest.stopRt) { + sHelpNest.stopRt->terminated = true; + } +} + + // caseNewArgs' message box: two sets of new arguments arrive while it is // open (nothing may run yet), then OK. static void newArgsDuringDialog(AppContextT *ctx, WindowT *modal, void *userCtx) { @@ -10459,6 +11382,15 @@ static const char *roundTripValue(const char *basName, const WgtPropDescT *p, ch return "-1"; } + // A ListView without columns has nothing to sort by + if (strcasecmp(p->name, "SortColumn") == 0) { + return "-1"; + } + + if (strcasecmp(p->name, "SortOrder") == 0) { + return "0"; + } + // A single page: index 0 is the only active one. if (strcasecmp(p->name, "TabIndex") == 0 || strcasecmp(p->name, "Decimals") == 0) { return "0"; @@ -10536,7 +11468,12 @@ static const char *roundTripValue(const char *basName, const WgtPropDescT *p, ch } -// Set a string property from a C literal, releasing the temporary. +// The colour number at (x, y) of the program screen's active page. +static int32_t screenPixel(const BasScreenT *scr, int32_t x, int32_t y) { + return scr->fb[y * scr->width + x]; +} + + // SerRead's bytes as a C string (these tests send text). static const char *serReadText(int32_t com) { static char text[FRT_OUT_MAX]; @@ -10555,6 +11492,7 @@ static int32_t serWriteText(int32_t com, const char *text) { } +// Set a string property from a C literal, releasing the temporary. static void setStrProp(FrtAppT *app, BasControlT *ctrl, const char *propName, const char *text) { BasValueT v = basValStringFromC(text); @@ -10589,6 +11527,25 @@ static const char *sqlFakeError(int32_t db) { } +// The most an escape can grow: every character doubled ("ab" -> "aabb"); +// -1 without room for that and the NUL, as dvxSqlEscape. +static int32_t sqlFakeEscape(const char *src, char *dst, int32_t dstSize) { + int32_t len = (int32_t)strlen(src); + + if (dstSize < len * 2 + 1) { + return -1; + } + + for (int32_t i = 0; i < len; i++) { + dst[i * 2] = src[i]; + dst[i * 2 + 1] = src[i]; + } + + dst[len * 2] = '\0'; + return len * 2; +} + + static bool sqlFakeExec(int32_t db, const char *sql) { (void)db; return sql != NULL; @@ -10665,8 +11622,39 @@ static int32_t sqlFakeQuery(int32_t db, const char *sql) { } -// The dialog script of caseCreateFormDynamic: the form shown modal holds -// the input gate; unloading it ends the Show. +// The pump SQLOpen gives a program's database. +static void sqlFakeSetPump(int32_t db, bool (*pump)(int32_t db)) { + sSqlPumpDb = db; + sSqlPump = pump; +} + + +// caseCurrentPerTask's task: enters its program's code, yields twice (the +// other task's program enters its code meanwhile), and notes the runtime +// current then. +static void taskCodeYield(void *arg) { + TaskRtCheckT *check = (TaskRtCheckT *)arg; + BasFormRtT *prev = basFormRtCodeEnter(check->rt); + + tsYield(); + tsYield(); + check->seen = basFormRtCurrent(); + basFormRtCodeLeave(check->rt, prev); +} + + +// caseLoadFormsYield's other task: counts its turns until told to stop. +static void taskTurns(void *arg) { + TaskTurnsT *t = (TaskTurnsT *)arg; + + while (!t->stop) { + t->turns++; + tsYield(); + } +} + + +// The bound stream's unbind callback: counts the unbinds. static void termBindUnbound(void *ctx) { (void)ctx; sBindUnbound++; @@ -10685,6 +11673,8 @@ static int32_t termBindWrite(void *ctx, const uint8_t *data, int32_t len) { } +// The dialog script of caseCreateFormDynamic: the form shown modal holds +// the input gate; unloading it ends the Show. static void unloadModalCheck(AppContextT *ctx, WindowT *modal, void *userCtx) { BasFormT *form = (BasFormT *)userCtx; @@ -10743,6 +11733,7 @@ int main(void) { caseRemoveContainerFromHandler(); caseVisibleKeepsModal(); caseLoadForm2(); + caseLoadFormsYield(); caseSetEvent(); caseQueryUnloadCancel(); caseValidateCancel(); @@ -10765,17 +11756,21 @@ int main(void) { caseMethodRetIntStr(); casePropEdges(); caseCreateFormDynamic(); + caseCurrentPerTask(); caseDataBinding(); caseDialogWrappers(); caseClipboard(); caseGapClosure(); caseDragDrop(); caseLoadPicture(); + caseLoadPictureData(); + caseFormFullScreen(); caseDrawMethods(); caseDrawFocusRect(); caseSizeUnits(); caseScrollPaneChange(); caseDrawPaint(); + caseDrawPaintPicture(); caseDrawScale(); caseDrawStyle(); caseEventLoopExits(); @@ -10791,6 +11786,7 @@ int main(void) { caseHelpHooks(); caseHelpPane(); caseHelpTopic(); + caseOptionGroup(); casePendingUnloadDrain(); casePictureLoadSave(); caseResources(); @@ -10800,9 +11796,12 @@ int main(void) { frtFrmModelCases(); caseSqlWrappers(); caseBoxLayoutProps(); + caseBarLayoutProps(); caseDefaultCancelButtons(); + caseFormLimits(); caseHandlerContext(); caseHandlerError(); + caseListSetItems(); caseListTreeMethods(); caseDesignerLive(); caseDesignerRegistry(); @@ -10812,6 +11811,7 @@ int main(void) { caseMenuCaption(); caseModalShow(); caseScreenObject(); + caseScreenPaint(); caseScreenMembers(); caseFormWindowMembers(); caseMenuBarRuntime(); @@ -10821,6 +11821,7 @@ int main(void) { caseMenuRuntime(); caseProgramIcon(); caseTermBind(); + caseTermSend(); caseTextBoxProps(); caseWidgetParity(); caseTwoRuntimes(); diff --git a/src/test/formrt/testFrmModel.c b/src/test/formrt/testFrmModel.c index 7581f53e..870b8901 100644 --- a/src/test/formrt/testFrmModel.c +++ b/src/test/formrt/testFrmModel.c @@ -23,7 +23,7 @@ // testFrmModel.c -- the .frm document model (suite T5, round 2) // -// formrt/frmModel.c from C: every .frm in the source tree loads and +// basdev/frmModel.c from C: every .frm in the source tree loads and // writes back unchanged by a second round (the canonical text the IDE // saves); the edit operations (add, remove, rename, move, reparent, // properties, control arrays, the OptionButton rule), copy and paste of @@ -33,7 +33,7 @@ // FormText$ of a live form. #include "frtHarness.h" -#include "../../apps/kpunch/dvxbasic/formrt/frmModel.h" +#include "../../apps/kpunch/dvxbasic/basdev/frmModel.h" #include "dvxPlat.h" #include "stb_ds_wrap.h" @@ -64,7 +64,6 @@ static void caseFrmModelMenus(void); static void caseFrmModelPaste(void); static void caseFrmModelRepo(void); static void collectFrms(const char *dir, char ***out); -static bool fmIdent(const char *name); static char *fmText(const FrmModelT *m); void frtFrmModelCases(void); @@ -664,18 +663,17 @@ static void caseFrmModelMenus(void) { arrput(work, mi); } - TEST_EQ(frmModelMenuValidate(m, work, fmIdent, reason, sizeof(reason)), FRM_NO_INDEX); + TEST_EQ(frmModelMenuValidate(m, work, reason, sizeof(reason)), FRM_NO_INDEX); snprintf(work[1].shortcut, sizeof(work[1].shortcut), "%s", "Ctrl+Nothing"); - TEST_EQ(frmModelMenuValidate(m, work, fmIdent, reason, sizeof(reason)), 1); + TEST_EQ(frmModelMenuValidate(m, work, reason, sizeof(reason)), 1); TEST_TRUE(strstr(reason, "Unknown shortcut key") != NULL); work[1].shortcut[0] = '\0'; snprintf(work[2].name, sizeof(work[2].name), "%s", "mnuFile"); - TEST_EQ(frmModelMenuValidate(m, work, fmIdent, reason, sizeof(reason)), 2); + TEST_EQ(frmModelMenuValidate(m, work, reason, sizeof(reason)), 2); snprintf(work[2].name, sizeof(work[2].name), "%s", "bad name"); - TEST_EQ(frmModelMenuValidate(m, work, fmIdent, reason, sizeof(reason)), 2); - TEST_EQ(frmModelMenuValidate(m, work, NULL, reason, sizeof(reason)), FRM_NO_INDEX); + TEST_EQ(frmModelMenuValidate(m, work, reason, sizeof(reason)), 2); snprintf(work[2].name, sizeof(work[2].name), "%s", "lst"); - TEST_EQ(frmModelMenuValidate(m, work, fmIdent, reason, sizeof(reason)), 2); + TEST_EQ(frmModelMenuValidate(m, work, reason, sizeof(reason)), 2); // Replace: the renamed popup's bindings follow by id; a blank childless // item is stripped @@ -843,22 +841,6 @@ static void collectFrms(const char *dir, char ***out) { } -// Identifier check standing in for the compiler's: letters, digits and -// underscores, not starting with a digit. -static bool fmIdent(const char *name) { - for (const char *p = name; *p; p++) { - bool alpha = (*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z') || *p == '_'; - bool digit = *p >= '0' && *p <= '9'; - - if (!alpha && !(digit && p != name)) { - return false; - } - } - - return name[0] != '\0'; -} - - // The model's .frm text (malloc'd), NULL on failure. static char *fmText(const FrmModelT *m) { int32_t len = 0; diff --git a/src/test/formrt/testLibBas.c b/src/test/formrt/testLibBas.c index 2d6d41b0..db47f809 100644 --- a/src/test/formrt/testLibBas.c +++ b/src/test/formrt/testLibBas.c @@ -35,9 +35,10 @@ #include "../../libs/kpunch/conn/conn.h" #include "../../libs/kpunch/print/print.h" #include "../../libs/kpunch/serial/rs232/rs232.h" -#include "../../libs/kpunch/serial/seclink/secLink.h" -#include "../../libs/kpunch/serial/security/secHash.h" -#include "../../libs/kpunch/serial/security/security.h" +#include "../../libs/kpunch/seclink/secLink.h" +#include "../../libs/kpunch/crypto/secCurve.h" +#include "../../libs/kpunch/crypto/secHash.h" +#include "../../libs/kpunch/crypto/security.h" #include "../../libs/kpunch/xfer/xfer.h" #include "dvxPlat.h" @@ -87,6 +88,7 @@ static const LibConstT sCommConsts[] = { LIB_CONST("SER_ERR_NULL_PTR", RS232_ERR_NULL_PTR), LIB_CONST("SER_ERR_IRQ_NOT_FOUND", RS232_ERR_IRQ_NOT_FOUND), LIB_CONST("SER_ERR_LOCK_MEM", RS232_ERR_LOCK_MEM), + LIB_CONST("SER_ERR_NO_MEMORY", RS232_ERR_NO_MEMORY), LIB_CONST("SER_LINE_OVERRUN", RS232_LSR_OVERRUN), LIB_CONST("SER_LINE_PARITY", RS232_LSR_PARITY_ERR), LIB_CONST("SER_LINE_FRAMING", RS232_LSR_FRAME_ERR), @@ -94,7 +96,6 @@ static const LibConstT sCommConsts[] = { LIB_CONST("COMM_OK", SECLINK_SUCCESS), LIB_CONST("COMM_ERR_PARAM", SECLINK_ERR_PARAM), LIB_CONST("COMM_ERR_SERIAL", SECLINK_ERR_SERIAL), - LIB_CONST("COMM_ERR_ALLOC", SECLINK_ERR_ALLOC), LIB_CONST("COMM_ERR_HANDSHAKE", SECLINK_ERR_HANDSHAKE), LIB_CONST("COMM_ERR_NOT_READY", SECLINK_ERR_NOT_READY), LIB_CONST("COMM_ERR_SEND", SECLINK_ERR_SEND), @@ -115,7 +116,6 @@ static const LibConstT sConnConsts[] = { LIB_CONST("CONN_OK", CONN_OK), LIB_CONST("CONN_ERR_PARAM", CONN_ERR_PARAM), LIB_CONST("CONN_ERR_PORT", CONN_ERR_PORT), - LIB_CONST("CONN_ERR_HOST", CONN_ERR_HOST), LIB_CONST("CONN_FLOW_NONE", RS232_HANDSHAKE_NONE), LIB_CONST("CONN_FLOW_XONXOFF", RS232_HANDSHAKE_XONXOFF), LIB_CONST("CONN_FLOW_RTSCTS", RS232_HANDSHAKE_RTSCTS) @@ -197,14 +197,23 @@ static const LibConstT sListConsts[] = { }; static const LibConstT sSecConsts[] = { - LIB_CONST("SEC_DH_KEY_SIZE", SEC_DH_KEY_SIZE), - LIB_CONST("SEC_XTEA_KEY_SIZE", SEC_XTEA_KEY_SIZE), - LIB_CONST("SEC_SHA256_SIZE", SEC_SHA256_SIZE), - LIB_CONST("SEC_HKDF_MAX_OUT", SEC_HKDF_MAX_OUT), - LIB_CONST("SEC_OK", SEC_SUCCESS), - LIB_CONST("SEC_ERR_PARAM", SEC_ERR_PARAM), - LIB_CONST("SEC_ERR_NOT_READY", SEC_ERR_NOT_READY), - LIB_CONST("SEC_ERR_IO", SEC_ERR_IO) + LIB_CONST("SEC_DH_KEY_SIZE", SEC_DH_KEY_SIZE), + LIB_CONST("SEC_XTEA_KEY_SIZE", SEC_XTEA_KEY_SIZE), + LIB_CONST("SEC_SHA256_SIZE", SEC_SHA256_SIZE), + LIB_CONST("SEC_HKDF_MAX_OUT", SEC_HKDF_MAX_OUT), + LIB_CONST("SEC_X25519_KEY_SIZE", SEC_X25519_KEY_SIZE), + LIB_CONST("SEC_AEAD_KEY_SIZE", SEC_AEAD_KEY_SIZE), + LIB_CONST("SEC_AEAD_NONCE_SIZE", SEC_AEAD_NONCE_SIZE), + LIB_CONST("SEC_AEAD_TAG_SIZE", SEC_AEAD_TAG_SIZE), + LIB_CONST("SEC_ED25519_SEED_SIZE", SEC_ED25519_SEED_SIZE), + LIB_CONST("SEC_ED25519_SECRET_SIZE", SEC_ED25519_SECRET_SIZE), + LIB_CONST("SEC_ED25519_PUBLIC_SIZE", SEC_ED25519_PUBLIC_SIZE), + LIB_CONST("SEC_ED25519_SIG_SIZE", SEC_ED25519_SIG_SIZE), + LIB_CONST("SEC_X25519_PENDING", SEC_X25519_PENDING), + LIB_CONST("SEC_OK", SEC_SUCCESS), + LIB_CONST("SEC_ERR_PARAM", SEC_ERR_PARAM), + LIB_CONST("SEC_ERR_NOT_READY", SEC_ERR_NOT_READY), + LIB_CONST("SEC_ERR_IO", SEC_ERR_IO) }; static const LibConstT sCommDlgConsts[] = { @@ -272,6 +281,7 @@ static void caseCommAdditions(void); static void caseLibConsts(void); static void caseListHelp(void); static void caseSecurity(void); +static void caseSecurityCurve(void); void frtLibBasCases(void); static bool runModule(const char *const *files, int32_t fileCount, const char *code, char *out, size_t outSize); @@ -533,7 +543,7 @@ static void caseListHelp(void) { static void caseSecurity(void) { static const char *const files[] = { "security.bas" }; static const char code[] = - "Dim a As Long, b As Long, c As Long, d As Long, h As Long\n" + "Dim a As Long, b As Long, c As Long, d As Long, e As Long, h As Long\n" "Dim k As String, x As String, y As String, z As String\n" "Print Len(SecRandom$(40)); Len(SecRandom$(0)); SecRandom$(16) <> SecRandom$(16)\n" "SecAddEntropy \"some\" & Chr$(0) & \"noise\"\n" @@ -556,6 +566,15 @@ static void caseSecurity(void) { "Print a > 0; b > 0; Len(SecDhPublicKey$(a)); SecDhSecret$(a) = \"\"\n" "Print SecDhComputeSecret(a, SecDhPublicKey$(b)) = SEC_OK; SecDhComputeSecret(b, SecDhPublicKey$(a)) = SEC_OK; SecDhComputeSecret(a, \"short\") = SEC_ERR_PARAM\n" "Print SecDhSecret$(a) = SecDhSecret$(b); Len(SecDhSecret$(a))\n" + "e = SecDhCreateStart()\n" + "Print SecDhPublicKey$(e) = \"\"; SecDhStep(e) = SEC_DH_PENDING\n" + "Do While SecDhStep(e) = SEC_DH_PENDING\n" + "Loop\n" + "Print Len(SecDhPublicKey$(e)); SecDhComputeStart(e, SecDhPublicKey$(b)) = SEC_DH_PENDING; SecDhComputeStart(e, \"short\") = SEC_ERR_PARAM\n" + "Do While SecDhStep(e) = SEC_DH_PENDING\n" + "Loop\n" + "Print SecDhComputeSecret(b, SecDhPublicKey$(e)) = SEC_OK; SecDhSecret$(e) = SecDhSecret$(b); SecDhStep(e) = SEC_ERR_NOT_READY\n" + "SecDhFree e\n" "k = SecHkdf$(\"\", SecDhSecret$(a), \"cipher\", SEC_XTEA_KEY_SIZE)\n" "c = SecCipherCreate(k)\n" "d = SecCipherCreate(k)\n" @@ -588,6 +607,9 @@ static void caseSecurity(void) { "True True 128 True \n" "True True True \n" "True 128 \n" + "True True \n" + "128 True True \n" + "True True True \n" "12 True True \n" "True 0 True \n" "True True \n", @@ -597,11 +619,75 @@ static void caseSecurity(void) { } +// security.bas's X25519, sealing, and Ed25519 called for real: both sides +// of an exchange agree and a zero key is refused; a sealed message opens +// only with the same key, nonce, and extra bytes and unchanged; a +// signature checks only for its key and bytes; wrong sizes give "". +static void caseSecurityCurve(void) { + static const char *const files[] = { "security.bas" }; + static const char code[] = + "Dim a As String, b As String, k As String, n As String, x As String, y As String, s As String, p As String, g As String\n" + "Dim h As Long\n" + "a = SecRandom$(SEC_X25519_KEY_SIZE)\n" + "b = SecRandom$(SEC_X25519_KEY_SIZE)\n" + "Print Len(SecX25519PublicKey$(a)); SecX25519$(a, SecX25519PublicKey$(b)) = SecX25519$(b, SecX25519PublicKey$(a)); Len(SecX25519$(a, SecX25519PublicKey$(b)))\n" + "Print SecX25519$(a, String$(SEC_X25519_KEY_SIZE, 0)) = \"\"; SecX25519PublicKey$(\"short\") = \"\"; SecX25519$(a, \"short\") = \"\"\n" + "h = SecX25519Start(a, SecX25519PublicKey$(b))\n" + "n = \"\"\n" + "Do While SecX25519Step(h) = SEC_X25519_PENDING\n" + " n = n & \".\"\n" + "Loop\n" + "Print h > 0; Len(n); SecX25519End$(h) = SecX25519$(a, SecX25519PublicKey$(b)); SecX25519Step(h) = SEC_ERR_PARAM\n" + "h = SecX25519Start(a, \"\")\n" + "Do While SecX25519Step(h) = SEC_X25519_PENDING\n" + "Loop\n" + "Print SecX25519End$(h) = SecX25519PublicKey$(a); SecX25519Start(\"short\", \"\"); SecX25519Start(a, \"short\")\n" + "h = SecX25519Start(a, String$(SEC_X25519_KEY_SIZE, 0))\n" + "Do While SecX25519Step(h) = SEC_X25519_PENDING\n" + "Loop\n" + "Print SecX25519End$(h) = \"\"\n" + "h = SecX25519Start(a, \"\")\n" + "SecX25519Free h\n" + "Print SecX25519Step(h) = SEC_ERR_PARAM; SecX25519End$(h) = \"\"\n" + "k = SecHkdf$(\"\", SecX25519$(a, SecX25519PublicKey$(b)), \"seal\", SEC_AEAD_KEY_SIZE)\n" + "n = SecRandom$(SEC_AEAD_NONCE_SIZE)\n" + "x = \"zero\" & Chr$(0) & \"inside\"\n" + "y = SecAeadLock$(k, n, \"hdr\", x)\n" + "Print Len(y) = Len(x) + SEC_AEAD_TAG_SIZE; Left$(y, Len(x)) <> x; SecAeadUnlock$(k, n, \"hdr\", y) = x\n" + "Print SecAeadUnlock$(k, n, \"HDR\", y) = \"\"; SecAeadUnlock$(k, SecRandom$(SEC_AEAD_NONCE_SIZE), \"hdr\", y) = \"\"; SecAeadUnlock$(k, n, \"hdr\", Chr$(Asc(y) Xor 1) & Mid$(y, 2)) = \"\"\n" + "Print SecAeadLock$(\"short\", n, \"\", x) = \"\"; SecAeadLock$(k, \"short\", \"\", x) = \"\"; SecAeadUnlock$(k, n, \"\", \"short\") = \"\"\n" + "s = SecEd25519KeyPair$(SecRandom$(SEC_ED25519_SEED_SIZE))\n" + "p = SecEd25519PublicKey$(s)\n" + "g = SecEd25519Sign$(s, x)\n" + "Print Len(s); Len(p); Len(g); Right$(s, SEC_ED25519_PUBLIC_SIZE) = p\n" + "Print SecEd25519Check(g, p, x); SecEd25519Check(g, p, x & \"!\"); SecEd25519Check(g, SecEd25519PublicKey$(SecEd25519KeyPair$(SecRandom$(SEC_ED25519_SEED_SIZE))), x); SecEd25519Check(\"short\", p, x)\n" + "Print SecEd25519KeyPair$(\"short\") = \"\"; SecEd25519PublicKey$(\"short\") = \"\"; SecEd25519Sign$(\"short\", x) = \"\"\n"; + char out[FRT_OUT_MAX]; + + tCase("security.bas X25519, sealing, and Ed25519 from BASIC"); + TEST_TRUE(runModule(files, DVX_ARRAY_LEN(files), code, out, sizeof(out))); + TEST_STR_EQ(out, + "32 True 32 \n" + "True True True \n" + "True 8 True True \n" + "True 0 0 \n" + "True \n" + "True True \n" + "True True True \n" + "True True True \n" + "True True True \n" + "64 32 64 True \n" + "-1 0 0 0 \n" + "True True True \n"); +} + + void frtLibBasCases(void) { caseCommAdditions(); caseLibConsts(); caseListHelp(); caseSecurity(); + caseSecurityCurve(); } diff --git a/src/test/formrt/testUcDesign.c b/src/test/formrt/testUcDesign.c index 3d06038d..48df6498 100644 --- a/src/test/formrt/testUcDesign.c +++ b/src/test/formrt/testUcDesign.c @@ -330,7 +330,7 @@ static bool designerBuild(FrtAppT *app, const char *frm, const char *iconOut) { // The project's code as the designer compiles it: the Meter class. static BasModuleT *designModule(void) { BasCompileErrorT err; - BasModuleT *mod = basBuildCompile(sMeterClass, (int32_t)strlen(sMeterClass), false, NULL, NULL, &err); + BasModuleT *mod = basBuildCompile(sMeterClass, (int32_t)strlen(sMeterClass), false, NULL, NULL, &err, NULL, NULL); if (!mod) { printf(" design module: %s\n", err.message); diff --git a/src/test/formrt/testUserCtl.c b/src/test/formrt/testUserCtl.c index 5db707b1..d7cf8ab2 100644 --- a/src/test/formrt/testUserCtl.c +++ b/src/test/formrt/testUserCtl.c @@ -58,6 +58,10 @@ #define UC_LISTHELP_BAS FRT_INCLUDE_DIR "/listhelp.bas" #define UC_ICON_ITEMS 6 // the IconView form's first items #define UC_ICON_MORE 40 // and the ones Form1_More adds +#define UC_SLICE_MS 20 // time a sliced program's pump lets pass +#define UC_SLICE_SCREEN_W 320 // caseUcSlicedLoop's small screen (a pump composites it) +#define UC_SLICE_SCREEN_H 240 +#define UC_SLICE_SCREEN_BPP 8 #define UC_GAMMA 2 // Gamma's index (Alpha is dragged onto its cell) #define UC_DELTA 3 // Delta's index (type-ahead "d" selects it) #define UC_PLACED_ITEMS 3 // Form1_Manual's items, in cells of their own @@ -240,14 +244,20 @@ static const char sUcFakeColors[] = " End Select\n" "End Function\n"; +// caseUcSlicedLoop's slice clock: it never moves, so with a span of 0 +// every slice is over at its first check +static volatile uint32_t sUcSliceClock = 0; + // Prototypes static void caseUcCHost(void); static void caseUcDesignMode(void); static void caseUcEnumProps(void); static void caseUcIconView(void); static void caseUcInstalled(void); +static void caseUcOtherTask(void); static void caseUcProgram(void); static void caseUcRegisterErrors(void); +static void caseUcSlicedLoop(void); static void caseUcTwoPrograms(void); void frtUserCtlCases(void); static bool ucBuild(FrtAppT *app, const char *frm); @@ -258,6 +268,7 @@ static void ucIndexPoint(const WidgetT *w, const ListIconGridT *g, int32_t index static void ucItemPoint(const WidgetT *w, const ListIconGridT *g, int32_t col, int32_t row, int32_t *sx, int32_t *sy); static void ucOnEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc); static bool ucPainted(WidgetT *w); +static bool ucSlicedDoEvents(void *ctx); static void ucViewPoint(const WidgetT *w, int32_t x, int32_t y, int32_t *sx, int32_t *sy); @@ -494,7 +505,7 @@ static void caseUcEnumProps(void) { "ENDCLASS\n"; static const char *fileHeader = "Begin UserControl Compass\nEnd\n"; BasCompileErrorT err; - BasModuleT *mod = basBuildCompile(fileSource, (int32_t)strlen(fileSource), false, NULL, NULL, &err); + BasModuleT *mod = basBuildCompile(fileSource, (int32_t)strlen(fileSource), false, NULL, NULL, &err, NULL, NULL); char path[DVX_MAX_PATH]; BasBuildSpecT spec; const char *headers[] = { fileHeader }; @@ -865,6 +876,13 @@ static void caseUcInstalled(void) { "Public Function Made() AS LONG\n" " Made = madeAt\n" "End Function\n" + "Public Sub Spin()\n" + " DO\n" + " LOOP\n" + "End Sub\n" + "Public Sub Nap()\n" + " SLEEP 1\n" + "End Sub\n" "ENDCLASS\n"; static const char *ctlHeader = "Begin UserControl Dial\n" @@ -874,7 +892,7 @@ static void caseUcInstalled(void) { "End\n"; BasCompileErrorT err; - BasModuleT *mod = basBuildCompile(ctlSource, (int32_t)strlen(ctlSource), false, NULL, NULL, &err); + BasModuleT *mod = basBuildCompile(ctlSource, (int32_t)strlen(ctlSource), false, NULL, NULL, &err, NULL, NULL); char path[DVX_MAX_PATH]; snprintf(path, sizeof(path), "%s/%s", tTempDir(), UC_FILE_NAME); @@ -936,6 +954,11 @@ static void caseUcInstalled(void) { " Dial1.Turn 3\n" " Print Dial1.Value; Dial1.Made()\n" "End Sub\n" + "Sub Form1_Stuck ()\n" + " Dial1.Spin\n" + " Dial1.Nap\n" + " Print \"after\"; Dial1.Value\n" + "End Sub\n" "Sub Dial1_Turned (ByVal Steps As Long)\n" " Print \"turned\"; Steps\n" "End Sub\n"; @@ -950,6 +973,16 @@ static void caseUcInstalled(void) { tPump(UC_SETTLE); TEST_TRUE(frtFire(&app, "Form1", "T")); TEST_STR_EQ(frtOut(&app), "turned2 \nturned5 \n5 7 \n"); + + // The file's code has no event pump: an endless loop is stopped (error + // 18) and SLEEP refused (error 5), each shown, and the control and the + // program carry on + int32_t shown = app.dialogsDismissed; + + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Stuck")); + TEST_STR_EQ(frtOut(&app), "after5 \n"); + TEST_EQ(app.dialogsDismissed, shown + 2); frtDestroy(&app); // From C: the same VM @@ -981,6 +1014,56 @@ static void caseUcInstalled(void) { } +// A program whose task is another (its runtime's ownTask made another +// here): its control's code never runs on this one -- the click waits for +// the program's own turn (basFormRtUpdate on its task), then runs. +static void caseUcOtherTask(void) { + tCase("UserControl events from another task wait for the program's turn"); + tInit(UC_SCREEN_W, UC_SCREEN_H, UC_SCREEN_BPP); + + FrtAppT app; + + if (!ucBuild(&app, sForm)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + tPump(UC_SETTLE); + + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *ctrl = form ? frtCtrl(form, "Gauge1") : NULL; + WidgetT *w = ctrl ? ctrl->widget : NULL; + uint32_t own = app.rt->ownTask; + int32_t sx = 0; + int32_t sy = 0; + + TEST_TRUE(w != NULL); + + if (!w) { + frtDestroy(&app); + tShutdown(); + return; + } + + app.rt->ownTask = own + 1; + tWidgetCenter(w, &sx, &sy); + tClick(sx, sy); + tPump(1); + app.rt->ownTask = own; + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "C")); + TEST_STR_EQ(frtOut(&app), "fromfrm\n"); + + TEST_TRUE(basFormRtUpdate(app.rt)); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "C")); + TEST_STR_EQ(frtOut(&app), "down1 click\n"); + frtDestroy(&app); + tShutdown(); +} + + // The program's own control from its .frm. static void caseUcProgram(void) { tCase("UserControl of a program: .frm, events, methods, mouse and keys"); @@ -1071,6 +1154,114 @@ static void caseUcRegisterErrors(void) { } +// Program Manager's fill (parpm.scr): a FOR loop calling the IconView's +// methods, with every slice of the program ending at its first call (the +// time-sliced VM on a slow machine) and each pump running the program's +// waiting events and advancing time, so its Timer fires in the middle of +// the loop. The loop must come through whole, and the form's end (the +// control's Terminate gives way too) must not reach its freed controls. +static void caseUcSlicedLoop(void) { + tCase("IconView: a FOR loop sliced at every call, a Timer between"); + tInit(UC_SLICE_SCREEN_W, UC_SLICE_SCREEN_H, UC_SLICE_SCREEN_BPP); + + int32_t ctlLen = 0; + int32_t helpLen = 0; + char *ctl = platformReadFile(UC_ICONVIEW_CTL, &ctlLen); + char *help = platformReadFile(UC_LISTHELP_BAS, &helpLen); + + TEST_TRUE(ctl != NULL && help != NULL); + + if (!ctl || !help) { + free(ctl); + free(help); + tShutdown(); + return; + } + + int32_t codeOff = basClassCodePos(ctl, ctlLen); + size_t cap = (size_t)ctlLen + (size_t)helpLen + sizeof(sUcFakeColors) + UC_TEXT_MAX; + char *source = (char *)malloc(cap); + + snprintf(source, cap, "%s\n%s\nBEGINCLASS \"IconView\" USERCONTROL\n%s\nENDCLASS\n", help, sUcFakeColors, ctl + codeOff); + ctl[codeOff] = '\0'; + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Fill\"\n" + " Begin IconView Icons\n" + " End\n" + " Begin Label Shown\n" + " End\n" + " Begin Timer Tick\n" + " Interval = 10\n" + " Enabled = True\n" + " End\n" + "End\n" + "Dim ticks As Long\n" + "Sub Form1_Load ()\n" + " Dim i As Long\n" + " Icons.Clear\n" + " Icons.AutoArrange = False\n" + " For i = 0 To 5\n" + " Icons.AddItem \"Item\" & i, \"\", \"\"\n" + " Icons.SetItemDetail i, \"detail \" & i\n" + " Next i\n" + " Print \"filled\"; Icons.ItemCount; \" \";\n" + "End Sub\n" + "Sub PlaceAll ()\n" + " Dim j As Long\n" + " For j = 0 To Icons.ItemCount - 1\n" + " Icons.SetItemCell j, j Mod 4, j \\ 4\n" + " Next j\n" + "End Sub\n" + "Sub Icons_ColumnsChange (ByVal Columns As Integer)\n" + " PlaceAll\n" + "End Sub\n" + "Sub ShowState ()\n" + " Form1.Caption = \"tick \" & ticks\n" + " Shown.Caption = Form1.Caption\n" + "End Sub\n" + "Sub Tick_Timer ()\n" + " ticks = ticks + 1\n" + " PlaceAll\n" + " ShowState\n" + "End Sub\n"; + const char *frms[] = { frm }; + FrtAppT app; + bool built = frtBuild(&app, source, frms, 1); + + free(source); + free(help); + + if (!built) { + printf(" compile error: %s\n", app.error); + } + + TEST_TRUE(built); + + if (!built) { + free(ctl); + tShutdown(); + return; + } + + TEST_TRUE(basFormRtRegisterCtl(app.rt, ctl, codeOff)); + free(ctl); + + basVmSetSliceClock(app.vm, &sUcSliceClock, 0); + basVmSetDoEventsCallback(app.vm, ucSlicedDoEvents, &app); + basNativeHostSetLive(true); + frtStart(&app, NULL); + tPump(UC_SETTLE); + + TEST_TRUE(strstr(frtOut(&app), "filled6") != NULL); + TEST_STR_EQ(basVmGetError(app.vm), ""); + frtDestroy(&app); + basNativeHostSetLive(false); + tShutdown(); +} + + // Two programs (or two copies of one) with a control of the same name: // each has its own, registered under a key of its own; the name refers // to the program's own control in each. @@ -1116,6 +1307,8 @@ void frtUserCtlCases(void) { caseUcRegisterErrors(); caseUcTwoPrograms(); caseUcIconView(); + caseUcSlicedLoop(); + caseUcOtherTask(); caseUcInstalled(); } @@ -1211,6 +1404,16 @@ static bool ucPainted(WidgetT *w) { } +// caseUcSlicedLoop's DoEvents: time passes (the Timer comes due), then +// the program's turn as the stub gives it (basFormRtUpdate). +static bool ucSlicedDoEvents(void *ctx) { + FrtAppT *app = (FrtAppT *)ctx; + + hostClockAdvance(UC_SLICE_MS); + return basFormRtUpdate(app->rt); +} + + // The screen point x, y pixels into IconView w. static void ucViewPoint(const WidgetT *w, int32_t x, int32_t y, int32_t *sx, int32_t *sy) { int32_t cx = 0; diff --git a/src/test/fuzz/Makefile b/src/test/fuzz/Makefile index f676436f..4f36a6e8 100644 --- a/src/test/fuzz/Makefile +++ b/src/test/fuzz/Makefile @@ -31,7 +31,7 @@ # fuzzBasCompile BASIC compiler front end (source -> module -> run) # fuzzBasModule BASIC module deserializer + VM (runtime/serialize.c) # fuzzDfn .DFN font reader (dvxFontFmt.c) and the font engine -# fuzzFrmModel .frm form model parser / writer (formrt/frmModel.c) +# fuzzFrmModel .frm form model parser / writer (basdev/frmModel.c) # fuzzHlpc help compiler (.dhs -> .hlp, src/tools/dvxhlpc.c) # fuzzHlpFile compiled help file reader (libs/kpunch/hlpfile) # fuzzImage libdvx image loaders (stb_image + pixel conversion) @@ -79,7 +79,7 @@ FZ_BAS := $(CH_BAS_COMP) $(CH_BAS_VMLINK) $(CH_BAS_SUPPORT) FZ_PREFS := $(filter %/dvxPrefs.c,$(CH_BAS_VMLINK)) $(CH_BAS_SUPPORT) FZ_RES := $(filter-out %/dvxres.c,$(CH_TOOL_RES)) FZ_HLPFILE := $(CH_ROOT)/src/libs/kpunch/hlpfile/hlpFile.c -FZ_PACKET := $(CH_ROOT)/src/libs/kpunch/serial/packet/packet.c +FZ_PACKET := $(CH_ROOT)/src/libs/kpunch/seclink/packet.c FZ_LOOPSHIM := $(CH_ROOT)/src/test/serial/loopShim.c $(foreach v,FZ_PREFS FZ_RES,$(call chRequire,$v)) diff --git a/src/test/fuzz/fuzzFrmModel.c b/src/test/fuzz/fuzzFrmModel.c index 6a15adc9..c6cd892d 100644 --- a/src/test/fuzz/fuzzFrmModel.c +++ b/src/test/fuzz/fuzzFrmModel.c @@ -35,7 +35,7 @@ #include "fuzzUtil.h" -#include "formrt/frmModel.h" +#include "basdev/frmModel.h" #include #include diff --git a/src/test/fuzz/fuzzPacket.c b/src/test/fuzz/fuzzPacket.c index d078b71a..0de556bd 100644 --- a/src/test/fuzz/fuzzPacket.c +++ b/src/test/fuzz/fuzzPacket.c @@ -22,7 +22,7 @@ // fuzzPacket.c -- libFuzzer harness: the serial packet layer -// (serial/packet/packet.c: HDLC deframer, CRC check, Go-Back-N window) +// (seclink/packet.c: HDLC deframer, CRC check, Go-Back-N window) // // The connection runs over a PktStreamT whose read side returns bytes // the input supplies and whose write side swallows everything. packet.c @@ -132,7 +132,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { return 0; } - PktStreamT stream = { streamRead, streamWrite, st }; + PktStreamT stream = { streamRead, streamWrite, st, NULL }; PktConnT *conn = pktOpenStream(&stream, windowByte % (PKT_MAX_WINDOW + 2), onReceive, NULL); if (!conn) { diff --git a/src/test/fuzz/lsan.supp b/src/test/fuzz/lsan.supp index e85df728..ee0c6f69 100644 --- a/src/test/fuzz/lsan.supp +++ b/src/test/fuzz/lsan.supp @@ -1,3 +1,25 @@ +# 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. + # lsan.supp -- LeakSanitizer suppressions for the fuzzers (runFuzz.sh # passes this file; FUZZ_LSAN_SUPP= uses another). # diff --git a/src/test/fuzz/ubsan.supp b/src/test/fuzz/ubsan.supp index dd2ec140..8c9852c1 100644 --- a/src/test/fuzz/ubsan.supp +++ b/src/test/fuzz/ubsan.supp @@ -1,3 +1,25 @@ +# 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. + # ubsan.supp -- UndefinedBehaviorSanitizer suppressions for the fuzzers # (runFuzz.sh passes this file; FUZZ_UBSAN_SUPP= uses another). # diff --git a/src/test/game/gameTest.c b/src/test/game/gameTest.c index f23af261..27d3adee 100644 --- a/src/test/game/gameTest.c +++ b/src/test/game/gameTest.c @@ -34,7 +34,7 @@ // of held keys when focus moves, gameKeyIsDown, GameKeyDown, // GameKeyClear, GameWaitRetrace // exit exit() / abort() of a module reach the hook -// pacing gameFrameWait on time, missed frames, no drift, clamping, +// pacing gameFrameWait on time, missed frames (still one yield), no drift, clamping, // gameFrameNext (the next frame without the wait), // from a task (tsYield) // palette gamePalNearest, the nearest-colour cache and its resync @@ -416,7 +416,10 @@ static void frameTests(void) { TEST_TRUE(missed >= 2); TEST_EQ(p.frame, current); TEST_EQ(p.missed, missed); - TEST_EQ(platformMicros(), now); + + // The frame running now was already due: one yield (the host clock + // moves one yield step), no waiting beyond it + TEST_EQ(platformMicros(), now + YIELD_STEP_US); TEST_EQ(gameFrameWait(&p), 0); tCase("pacing: gameFrameNext moves on without waiting"); diff --git a/src/test/hlpfile/hlpFileTest.c b/src/test/hlpfile/hlpFileTest.c index e8663b81..89d6a0ff 100644 --- a/src/test/hlpfile/hlpFileTest.c +++ b/src/test/hlpfile/hlpFileTest.c @@ -53,6 +53,8 @@ #define BADVER_FILE "badver.hlp" #define IMAGE_OUT "image.png" #define MISSING_FILE "/nonexistent/none.hlp" +#define DAMAGED_FILE "damaged.hlp" +#define TRIGRAMS_BAD 0xFFFFFFFFu // a search header claiming more trigrams than fit // The records of one topic as hlpFileForEachRecord gave them. typedef struct { @@ -70,6 +72,10 @@ typedef void (*CleanupFnT)(int32_t appId); static CleanupFnT sCleanup = NULL; +// caseCache: the caches cacheFreed freed, in order +static void *sCacheFreed[2]; +static int32_t sCacheFrees = 0; + // ============================================================ // Prototypes // ============================================================ @@ -100,12 +106,15 @@ const char *HlpTopicId(int32_t handle, int32_t topic); const char *HlpTopicTitle(int32_t handle, int32_t topic); const char *HlpWrap(const char *text, int32_t cols); +static void cacheFreed(void *cache); static void caseBasic(void); static void caseBasicCleanup(void); +static void caseCache(void); static void caseIndex(void); static void caseOpenBad(void); static void caseRecords(void); static void caseSearch(void); +static void caseSearchDamaged(void); static void caseToc(void); static void caseTopics(void); static void caseTrigramRule(void); @@ -119,6 +128,16 @@ static bool walkRecord(const HlpRecordHdrT *hdr, const char *payload, void *user int main(void); +// caseCache's free function: records what it was given. +static void cacheFreed(void *cache) { + if (sCacheFrees < (int32_t)(sizeof(sCacheFreed) / sizeof(sCacheFreed[0]))) { + sCacheFreed[sCacheFrees] = cache; + } + + sCacheFrees++; +} + + // The BASIC face: every entry point on the fixture, results kept in the // handle, bad handles and bad numbers. static void caseBasic(void) { @@ -267,6 +286,36 @@ static void caseBasicCleanup(void) { } +// A caller's cache rides with the file: freed when another is set and +// when the file closes. +static void caseCache(void) { + tCase("cache slot"); + + HlpFileT *f = hlpFileOpen(DVXTEST_HELP_HLP); + int32_t first = 1; + int32_t other = 2; + + TEST_TRUE(f != NULL); + + if (!f) { + return; + } + + sCacheFrees = 0; + TEST_TRUE(hlpFileCache(f) == NULL); + hlpFileSetCache(f, &first, cacheFreed); + TEST_TRUE(hlpFileCache(f) == &first); + hlpFileSetCache(f, &other, cacheFreed); + TEST_EQ(sCacheFrees, 1); + TEST_TRUE(sCacheFreed[0] == &first); + TEST_TRUE(hlpFileCache(f) == &other); + hlpFileClose(f); + TEST_EQ(sCacheFrees, 2); + TEST_TRUE(sCacheFreed[1] == &other); + TEST_TRUE(hlpFileCache(NULL) == NULL); +} + + // Keyword index entries are in order and name their topics. static void caseIndex(void) { tCase("keyword index"); @@ -462,6 +511,61 @@ static void caseSearch(void) { } +// The search section is read on the first search; a damaged one is +// dropped and every topic is scanned instead, with the same answers. +static void caseSearchDamaged(void) { + tCase("search with a damaged search index"); + + FILE *in = fopen(DVXTEST_HELP_HLP, "rb"); + FILE *out = fopen(DAMAGED_FILE, "wb"); + HlpHeaderT hdr; + int c; + + TEST_TRUE(in != NULL && out != NULL); + + while (in && out && (c = fgetc(in)) != EOF) { + fputc(c, out); + } + + if (in) { + fclose(in); + } + + if (out) { + fclose(out); + } + + out = fopen(DAMAGED_FILE, "r+b"); + + if (out && fread(&hdr, sizeof(hdr), 1, out) == 1 && hdr.searchSize >= sizeof(HlpSearchHeaderT)) { + HlpSearchHeaderT bad; + + bad.trigramCount = TRIGRAMS_BAD; + fseek(out, (long)hdr.searchOffset, SEEK_SET); + TEST_EQ((int32_t)fwrite(&bad, sizeof(bad), 1, out), 1); + } else { + TEST_TRUE(false); + } + + if (out) { + fclose(out); + } + + HlpFileT *f = hlpFileOpen(DAMAGED_FILE); + + TEST_TRUE(f != NULL); + + if (f) { + TEST_EQ(searchCount(f, "continuation", hlpFileFindTopic(f, "fx.blocks")), 1); + TEST_EQ(searchCount(f, "continuation qqqzzz", -1), 0); + TEST_EQ(searchCount(f, "zebra", hlpFileFindTopic(f, "fx.deep")), 1); + hlpFileClose(f); + } + + remove(DAMAGED_FILE); +} + + // The table of contents: section headings with no topic, depths, titles. static void caseToc(void) { tCase("table of contents"); @@ -664,6 +768,8 @@ int main(void) { caseIndex(); caseRecords(); caseSearch(); + caseSearchDamaged(); + caseCache(); caseTrigramRule(); caseWrap(); caseBasic(); diff --git a/src/test/ide/Makefile b/src/test/ide/Makefile index 33876db1..21494803 100644 --- a/src/test/ide/Makefile +++ b/src/test/ide/Makefile @@ -61,8 +61,8 @@ STUBTARGET := $(BINDIR)/testStub IDE_SRCS := ideDesigner.c ideProperties.c ideProject.c ideMenuEditor.c ideToolbox.c idePackage.c COMP_SRCS := lexer.c ast.c scope.c parse.c parseExpr.c parseStmt.c parseDecl.c resolve.c resolveExpr.c typecheck.c fold.c gen.c genExpr.c genStmt.c compiler.c codegen.c strip.c obfuscate.c compact.c RT_SRCS := vm.c values.c dates.c serialize.c gfx.c screen.c -FRM_SRCS := formrt.c formGfx.c formPrinter.c formDesign.c formCtl.c frmParser.c frmModel.c frmModelBas.c basNativeHost.c basRunHost.c -DEV_SRCS := basValidate.c basDebug.c basProcs.c basSource.c basHelpMap.c basProject.c +FRM_SRCS := formrt.c formGfx.c formPrinter.c formDesign.c formCtl.c frmParser.c basNativeHost.c basRunHost.c +DEV_SRCS := basValidate.c basDebug.c basProcs.c basSource.c basHelpMap.c basProject.c frmModel.c frmModelBas.c OBJS := $(BINDIR)/testIde.o $(BINDIR)/ideStubs.o OBJS += $(patsubst %.c,$(BINDIR)/ide/%.o,$(IDE_SRCS)) @@ -75,9 +75,9 @@ OBJS += $(BINDIR)/basBuild.o # Make Package's package library, as pkg.lib has it PKG_DIR := $(DVXTEST_ROOT)/src/libs/kpunch/pkg MINIZ_DIR := $(PKG_DIR)/thirdparty/miniz -MONO_DIR := $(PKG_DIR)/thirdparty/monocypher/src -SEC_DIR := $(DVXTEST_ROOT)/src/libs/kpunch/serial/security -PKG_SRCS := $(addprefix $(PKG_DIR)/,pkgDpk.c pkgFormat.c pkgIni.c pkgManifest.c pkgSign.c pkgZip.c pkgZipWrite.c) $(MINIZ_DIR)/miniz.c $(MONO_DIR)/monocypher.c $(MONO_DIR)/optional/monocypher-ed25519.c $(SEC_DIR)/secHash.c $(SEC_DIR)/security.c +MONO_DIR := $(DVXTEST_ROOT)/src/libs/kpunch/crypto/thirdparty/monocypher/src +SEC_DIR := $(DVXTEST_ROOT)/src/libs/kpunch/crypto +PKG_SRCS := $(addprefix $(PKG_DIR)/,pkgDpk.c pkgFormat.c pkgIni.c pkgManifest.c pkgSign.c pkgSignMake.c pkgZip.c pkgZipWrite.c) $(MINIZ_DIR)/miniz.c $(MONO_DIR)/monocypher.c $(MONO_DIR)/optional/monocypher-ed25519.c $(SEC_DIR)/secHash.c $(SEC_DIR)/secSign.c $(SEC_DIR)/security.c OBJS += $(patsubst %.c,$(BINDIR)/pkg/%.o,$(notdir $(PKG_SRCS))) STUBOBJS := $(BINDIR)/testStub.o $(BINDIR)/stub/basModuleRes.o diff --git a/src/test/ide/fixtures/form1.frm b/src/test/ide/fixtures/form1.frm index bda558ee..b5bdb029 100644 --- a/src/test/ide/fixtures/form1.frm +++ b/src/test/ide/fixtures/form1.frm @@ -10,6 +10,30 @@ Begin Form Form1 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. + + Sub btnGo_Click() Call Helper PRINT "clicked"; counter diff --git a/src/test/ide/fixtures/main.bas b/src/test/ide/fixtures/main.bas index ba4a5eda..1d940ab0 100644 --- a/src/test/ide/fixtures/main.bas +++ b/src/test/ide/fixtures/main.bas @@ -1,3 +1,26 @@ +' 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. + + ' Fixture module Dim Shared counter As Integer counter = 10 diff --git a/src/test/ide/testIde.c b/src/test/ide/testIde.c index 74cd79d6..a762e0a1 100644 --- a/src/test/ide/testIde.c +++ b/src/test/ide/testIde.c @@ -75,6 +75,7 @@ #define FIXTURE_RENAMED "btnRun" #define FIXTURE_HANDLER "btnGo_Click" #define FIXTURE_RENAMED_HANDLER "btnRun_Click" +#define FIXTURE_DIM_LINE "Dim Shared counter As Integer" #define FIXTURE_GLOBAL_LINE "counter = 10" #define FIXTURE_HELPER_LINE "counter = counter + 1" #define FIXTURE_CALL_LINE "Call Helper" @@ -132,6 +133,7 @@ #define MODULE_FRAME_NAME BAS_DBG_MODULE_FRAME #define CMD_BUF (DVX_MAX_PATH * 3 + 64) #define LINE_BUF 256 +#define FIXTURE_TEXT_MAX 4096 // a fixture module's text (its licence header included) with lines added // Scripted answers for every modal the IDE can raise during a command. typedef struct { diff --git a/src/test/ide/testIdeDesign.c b/src/test/ide/testIdeDesign.c index 30f5b65b..79b707f0 100644 --- a/src/test/ide/testIdeDesign.c +++ b/src/test/ide/testIdeDesign.c @@ -1625,7 +1625,7 @@ static void caseGapMisc(void) { treeOpenFile(FILE_MAIN); showProc(-1); - char withZeta[LINE_BUF * 4]; + char withZeta[FIXTURE_TEXT_MAX]; snprintf(withZeta, sizeof(withZeta), "%s\nSub Zeta\n counter = 9\nEnd Sub\n", wgtGetText(sEditor)); wgtSetText(sEditor, withZeta); @@ -2446,7 +2446,7 @@ static void caseProcSplit(void) { treeOpenFile(FILE_MAIN); // Type a new Sub into (General): it moves into its own buffer. - char newText[LINE_BUF * 4]; + char newText[FIXTURE_TEXT_MAX]; snprintf(newText, sizeof(newText), "%s\nSub Extra\n counter = 2\nEnd Sub\n", wgtGetText(sEditor)); wgtSetText(sEditor, newText); @@ -2469,7 +2469,7 @@ static void caseProcSplit(void) { // new one and the duplicate are extracted. showProc(0); - char multi[LINE_BUF * 6]; + char multi[FIXTURE_TEXT_MAX]; snprintf(multi, sizeof(multi), "%s\nSub Another\n counter = 3\nEnd Sub\n\nSub Extra\n counter = 4\nEnd Sub\n", wgtGetText(sEditor)); wgtSetText(sEditor, multi); @@ -3366,25 +3366,25 @@ static char *dfBuildDeepFrm(int32_t ctrlDepth, int32_t menuLevels, const char *d return NULL; } - pos = frmEmitClamped(buf, size, pos, "VERSION DVX 1.00\nBegin Form Form1\n AutoSize = %s\n", autoSize ? "True" : "False"); + pos = dvxStrAppendfCut(buf, size, pos, "VERSION DVX 1.00\nBegin Form Form1\n AutoSize = %s\n", autoSize ? "True" : "False"); for (int32_t lv = 0; lv < menuLevels; lv++) { - pos = frmEmitClamped(buf, size, pos, "Begin Menu mnuD%d\n Caption = \"%s\"\n", (int)lv, lv == menuLevels - 1 ? deepCaption : "M"); + pos = dvxStrAppendfCut(buf, size, pos, "Begin Menu mnuD%d\n Caption = \"%s\"\n", (int)lv, lv == menuLevels - 1 ? deepCaption : "M"); } for (int32_t lv = 0; lv < menuLevels; lv++) { - pos = frmEmitClamped(buf, size, pos, "End\n"); + pos = dvxStrAppendfCut(buf, size, pos, "End\n"); } for (int32_t d = 1; d <= ctrlDepth; d++) { - pos = frmEmitClamped(buf, size, pos, "Begin Frame Frame%d\n", (int)d); + pos = dvxStrAppendfCut(buf, size, pos, "Begin Frame Frame%d\n", (int)d); } for (int32_t d = 1; d <= ctrlDepth; d++) { - pos = frmEmitClamped(buf, size, pos, "End\n"); + pos = dvxStrAppendfCut(buf, size, pos, "End\n"); } - frmEmitClamped(buf, size, pos, "End\n"); + dvxStrAppendfCut(buf, size, pos, "End\n"); return buf; } diff --git a/src/test/ide/testIdeRegress.c b/src/test/ide/testIdeRegress.c index 702b495d..cd71c8f5 100644 --- a/src/test/ide/testIdeRegress.c +++ b/src/test/ide/testIdeRegress.c @@ -65,8 +65,6 @@ #define RG_MENU_STEP_SELECT 0 #define RG_MENU_STEP_DELETE 1 #define RG_MENU_STEP_OK 2 -#define RG_GENERAL_BP_LINE 2 -#define RG_GENERAL_JOIN_LINE 3 #define RG_FORM_VAR_COUNT 2 #define RG_SCOPE_PROC "P" #define RG_SCOPE_CALL_DEPTH 2 @@ -346,28 +344,37 @@ static void caseRgBreakpointShift(void) { treeOpenFile(FILE_MAIN); showProc(-1); - // The code editor shows the breakpoint as a line marker (gutter dot) + // The breakpoint goes on the Dim line; the line after it is joined + // into it + int32_t bpLine = lineOf(wgtGetText(sEditor), FIXTURE_DIM_LINE); + int32_t joinLine = lineOf(wgtGetText(sEditor), FIXTURE_GLOBAL_LINE); + + TEST_EQ(joinLine, bpLine + 1); + + // The code editor shows the breakpoint as a line marker (gutter dot), + // once the line is in view + wgtTextAreaGoToLine(sEditor, bpLine); TEST_TRUE(!rgCodeWinHasRgb(IDE_BP_GUTTER_RGB)); - toggleBreakpointLine(RG_GENERAL_BP_LINE); + toggleBreakpointLine(bpLine); TEST_EQ((int32_t)arrlen(sBreakpoints), 1); TEST_TRUE(rgCodeWinHasRgb(IDE_BP_GUTTER_RGB)); // Go To Line selects the line; Home leaves just the caret at column 0 editorFocus(); - wgtTextAreaGoToLine(sEditor, RG_GENERAL_JOIN_LINE); + wgtTextAreaGoToLine(sEditor, joinLine); tKey(KEY_SCAN_HOME, 0, 0); tKeyBackspace(); TEST_EQ((int32_t)arrlen(sBreakpoints), 1); - TEST_EQ(arrlen(sBreakpoints) > 0 ? sBreakpoints[0].codeLine : -1, RG_GENERAL_BP_LINE); + TEST_EQ(arrlen(sBreakpoints) > 0 ? sBreakpoints[0].codeLine : -1, bpLine); - wgtTextAreaGoToLine(sEditor, RG_GENERAL_BP_LINE); + wgtTextAreaGoToLine(sEditor, bpLine); tKey(KEY_SCAN_HOME, 0, 0); tKeyEnter(); TEST_EQ((int32_t)arrlen(sBreakpoints), 1); - TEST_EQ(arrlen(sBreakpoints) > 0 ? sBreakpoints[0].codeLine : -1, RG_GENERAL_BP_LINE + 1); + TEST_EQ(arrlen(sBreakpoints) > 0 ? sBreakpoints[0].codeLine : -1, bpLine + 1); TEST_TRUE(rgCodeWinHasRgb(IDE_BP_GUTTER_RGB)); - toggleBreakpointLine(RG_GENERAL_BP_LINE + 1); + toggleBreakpointLine(bpLine + 1); TEST_EQ((int32_t)arrlen(sBreakpoints), 0); TEST_TRUE(!rgCodeWinHasRgb(IDE_BP_GUTTER_RGB)); @@ -493,6 +500,21 @@ static void caseRgClassDebugger(void) { TEST_STR_EQ(buf, "Nothing"); TEST_STR_EQ(basDbgTypeName(BAS_TYPE_VARIANT), "Variant"); + // Numbers read as PRINT shows them (CStr), whatever their type + BasValueT nums[] = { basValSingle(0.1f), basValDouble(1.0 / 3.0), basValDouble(1e20), basValLong(-7) }; + + for (int32_t i = 0; i < DVX_ARRAY_LEN(nums); i++) { + BasStringT *want = basValFormatString(nums[i]); + + basDbgFormatValue(sVm, &nums[i], buf, sizeof(buf)); + TEST_STR_EQ(buf, want->data); + basStringUnref(want); + } + + v = basValDouble(1e20); + basDbgFormatValue(sVm, &v, buf, sizeof(buf)); + TEST_STR_EQ(buf, "1E+20"); + // A Collection shows its item count BasObjectT *coll = basObjectNew(BAS_OBJ_COLLECTION, 0, NULL); @@ -559,7 +581,7 @@ static void caseRgClassDebugger(void) { char watched[BAS_DBG_VALUE_LEN]; - TEST_TRUE(basDbgEval(sVm, RG_DBG_OBJ_VAR "." RG_DBG_MEMBER, watched, sizeof(watched))); + TEST_TRUE(basDbgEval(sVm, RG_DBG_OBJ_VAR "." RG_DBG_MEMBER, watched, sizeof(watched), NULL)); TEST_EQ(atoi(watched), RG_DBG_MEMBER_VALUE); TEST_PTR_EQ(basDbgResolvePath(sVm, RG_DBG_OBJ_VAR ".nosuch", NULL), NULL); @@ -1007,7 +1029,7 @@ static void caseRgFindScopes(void) { wgtSetText(sFindInput, RG_FIND_GENERAL); onFindNext(NULL); TEST_EQ(sCurProcIdx, -1); - TEST_EQ(wgtTextAreaGetCursorLine(sEditor), 1); + TEST_EQ(wgtTextAreaGetCursorLine(sEditor), lineOf(wgtGetText(sEditor), RG_FIND_GENERAL)); // F3 with the dialog closed repeats the search wgtSetText(sFindInput, FIXTURE_HELPER_DECL); diff --git a/src/test/ide/testStub.c b/src/test/ide/testStub.c index 61f86adc..f2f17453 100644 --- a/src/test/ide/testStub.c +++ b/src/test/ide/testStub.c @@ -189,7 +189,6 @@ static void caseStubRun(void) { TEST_TRUE(dxe.onHelpQuery == NULL); TEST_STR_EQ(sShellApp.name, APP_NAME_RES); TEST_TRUE(strstr(dxe.helpFile, "stub.hlp") != NULL); - appShutdown(); tPump(SETTLE_FRAMES); tShutdown(); } diff --git a/src/test/joystick/joystickTest.c b/src/test/joystick/joystickTest.c index 5060e40a..bc0b9320 100644 --- a/src/test/joystick/joystickTest.c +++ b/src/test/joystick/joystickTest.c @@ -242,6 +242,42 @@ static void basicTests(void) { TEST_EQ(JoyHat(pad + 1, 2), JOY_HAT_CENTERED); TEST_EQ(JoyHat(pad + 1, 0), JOY_HAT_CENTERED); sPad.raw.hats[0] = JOY_HAT_CENTERED; + pollNext(); + + tCase("basic: JoyWatch and JoyEvent, each change once, the program's own"); + TEST_EQ(JoyWatch(0, -1), 0); + TEST_EQ(JoyWatch(pad + 1, -1), -1); + + // The hat the JoyHat case moved back is the first change told + pollNext(); + joyPump(); + TEST_EQ(JoyEvent(), JoyEventHatE + JOY_BAS_EVENT_FIRST); + TEST_EQ(JoyEventValue(), JOY_HAT_CENTERED); + TEST_EQ(JoyEvent(), JOY_BAS_EVENT_NONE); + sPad.raw.axes[1] = PAD_MOVE; + pollNext(); + joyPump(); + sPad.raw.pressed = 1U << 2; + pollNext(); + joyPump(); + ctx->currentAppId = OWNER_B; + TEST_EQ(JoyEvent(), JOY_BAS_EVENT_NONE); + ctx->currentAppId = OWNER_A; + TEST_EQ(JoyEvent(), JoyEventAxisE + JOY_BAS_EVENT_FIRST); + TEST_EQ(JoyEventJoy(), pad + 1); + TEST_EQ(JoyEventIndex(), 2); + TEST_EQ(JoyEventValue(), joyDeadZoneApply(PAD_MOVE, JOY_DEADZONE_DEFAULT)); + TEST_EQ(JoyEvent(), JoyEventButtonDownE + JOY_BAS_EVENT_FIRST); + TEST_EQ(JoyEventIndex(), 3); + TEST_EQ(JoyEvent(), JoyEventButtonUpE + JOY_BAS_EVENT_FIRST); + TEST_EQ(JoyEvent(), JOY_BAS_EVENT_NONE); + + // Not watched, nothing is kept + TEST_EQ(JoyWatch(pad + 1, 0), -1); + sPad.raw.axes[1] = 0; + pollNext(); + joyPump(); + TEST_EQ(JoyEvent(), JOY_BAS_EVENT_NONE); tCase("basic: one shared handle per program and device, reopened after cleanup"); int32_t shared = joyOpenShared(ctx, dev); diff --git a/src/test/lint/Makefile b/src/test/lint/Makefile index 1280d310..dc665334 100644 --- a/src/test/lint/Makefile +++ b/src/test/lint/Makefile @@ -1,13 +1,37 @@ +# 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. + # src/test/lint/Makefile -- T11 static + style gate, and the report-only # quality tools. # -# make run styleLint, asciiCheck, the staticGate self-test, +# make run styleLint, asciiCheck, licenseCheck, the staticGate self-test, # shellcheck, codespell, clang-tidy and staticGate (fails # on new findings) # make baseline regenerate the baselines of everything `make` runs # (staticGate's merge rules for each) # make style styleLint only # make ascii asciiCheck only +# make license licenseCheck only (every source file and script has +# the MIT licence header) # make static staticGate (cppcheck + -fanalyzer; slow) # make cppcheck staticGate cppcheck pass only # make selftest test staticGate's baseline merge logic (fast) @@ -54,9 +78,9 @@ # make STATIC_SKIP_CPPCHECK=1 # to skip only the cppcheck pass deliberately (reported on stderr). -.PHONY: all style ascii static cppcheck baseline selftest shellcheck codespell tidy tidy-baseline clang complexity msan valgrind qa +.PHONY: all style ascii license static cppcheck baseline selftest shellcheck codespell tidy tidy-baseline clang complexity msan valgrind qa -all: style ascii selftest shellcheck codespell tidy static +all: style ascii license selftest shellcheck codespell tidy static style: python3 ./styleLint.py @@ -64,6 +88,9 @@ style: ascii: ./asciiCheck.sh +license: + ./licenseCheck.sh + static: ./staticGate.sh diff --git a/src/test/lint/analyzerBaseline.txt b/src/test/lint/analyzerBaseline.txt index 678e2e9a..33d23c28 100644 --- a/src/test/lint/analyzerBaseline.txt +++ b/src/test/lint/analyzerBaseline.txt @@ -50,30 +50,36 @@ 3 src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c|kindList|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: same arrput store as ideProperties.c|addPropRow, reported at the stb_ds macro expansion. 1 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|addPropRow|leak of [CWE-401] [-Wanalyzer-malloc-leak] +# FP: fillRows' row (its fields) is stored via arrput in d->rows, freed by freeCache; reported at the stb_ds macro expansion. +1 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|fillRows|leak of [CWE-401] [-Wanalyzer-malloc-leak] +# FP: each match's strdup is stored via arrput into *walk->files (collectGlobFiles' list, freed by the caller); reported at the stb_ds macro expansion. +2 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|globVisit.part.0|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: li->count > idx guarantees widgetAdoptOwnedStrings copies count > 0 entries; the path needs its loop to run 0 times. Reached directly and via listPopupRemoveItem; the analyzer reports the path once or twice from run to run. 2 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|listItemsRemove|dereference of NULL [CWE-476] [-Wanalyzer-null-dereference] # FP: strdup("") is stored via arrput in remapped, which becomes lv->ownedCells (freed with the widget). 1 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|listViewRemapOwnedCells|leak of [CWE-401] [-Wanalyzer-malloc-leak] -# FP: scanDir grows the caller's module list with arrput (inside stb_ds's arrgrow); the list is the loader's for the whole run, and the analyzer loses the store into *mods. -1 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|loadAllModules|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: row stored into d->rows via arrput; freeCache releases it (the real partial-load leak was fixed: freeCache walks arrlen). 1 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|loadCache|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: copy/m.path are stored in sMemFiles via arrput and held on purpose until DVX exits (read-only media keeps settings in memory); stb_ds OOM hook never returns. 2 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|memFileStore|leak of [CWE-401] [-Wanalyzer-malloc-leak] +# FP: each module record is appended to *walk->mods via arrput (the loader's list for the whole run, freeMods); the analyzer loses the store into the caller's array. +1 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|moduleVisit.part.0|leak of [CWE-401] [-Wanalyzer-malloc-leak] +# FP: each folder entry's strdup'd name is stored via arrput in names and freed by platformWalkDir's last loop; reported at the stb_ds macro expansion. +3 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|platformWalkDir|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: inlined memFileStore - same arrput store into sMemFiles, held until DVX exits; stb_ds OOM hook never returns. 2 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|prefsSaveAs.part.0|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: each procBuf is stored via arrput into *outProcs (freed by the caller, and by splitProcs itself on its failure path); reported at the stb_ds macro expansion. 1 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|splitProcs|leak of [CWE-401] [-Wanalyzer-malloc-leak] -# THIRDPARTY: upstream stbi_write_hdr does not check its scratch malloc. DVX only calls stbi_write_png/stbi_write_bmp (dvxApp.c, dvxPlatformHost.c), never the HDR writer. Upstream code not modified. -1 src/libs/kpunch/libdvx/thirdparty/stb_image_write.h|stbiw__write_hdr_scanline|dereference of possibly-NULL [CWE-690] [-Wanalyzer-possible-null-dereference] +# THIRDPARTY: upstream stb_image_write's stretchy buffers (stbiw__sbpush) keep each block as (int *)p + 2, which the analyzer loses; seen now that it is compiled static in dvxImage.c (B1). Upstream code not modified. +5 src/libs/kpunch/libdvx/thirdparty/stb_image_write.h|stbi_zlib_compress|leak of [CWE-401] [-Wanalyzer-malloc-leak] +# THIRDPARTY: upstream stbiw__zlib_flushf, the same stretchy-buffer pointer (stbiw__sbpush). Upstream code not modified. +2 src/libs/kpunch/libdvx/thirdparty/stb_image_write.h|stbiw__zlib_flushf|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: the stack block is stored in task->stack (tasks[] entry, freed by tsKill/tsExit/tsShutdown); the analyzer loses it at the dummy-return-address store into the block. 1 src/libs/kpunch/libtasks/taskswitch.c|tsCreate|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: really listItemsAdd (inlined listItemsSync context): the strdup is stored via arrput in li->owned, freed by listItemsFree/Remove/Clear. 1 src/libs/kpunch/listhelp/listHelp.c|listItemsRemove|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: each strdup is stored via arrput in copy, which is returned to the caller as its owned array. 1 src/libs/kpunch/listhelp/listHelp.c|widgetAdoptOwnedStrings|leak of [CWE-401] [-Wanalyzer-malloc-leak] -# FP: copy is stored via arrput into *outFiles (freed by the caller); was the stb_ds.h|collectGlobFiles key. -1 src/loader/loaderMain.c|collectGlobFiles|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: data stored in topic->records[] and freed by freeAll; count 2 because the loader also compiles dvxhlpc.c. 2 src/tools/dvxhlpc.c|addRecord|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: postings buffer is stored in the trigrams[] entry (tri->postings) and freed by freeAll; the analyzer loses the store through the pointer into the realloc'd array. Count 2: host tool and loader builds both compile dvxhlpc.c. @@ -84,12 +90,20 @@ 1 src/widgets/kpunch/dataCtrl/widgetDataCtrl.c|dataCtrlSetField|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: on a failed strdup freeFields(fields, i) frees entries 0..i-1 and the array; nothing leaks. 1 src/widgets/kpunch/dataCtrl/widgetDataCtrl.c|dupFields|leak of [CWE-401] [-Wanalyzer-malloc-leak] +# FP: each field copy goes into row.fields, which arrput stores in d->rows (freed by freeCache); a failed copy frees the row's fields first. Reported at the loop and at the stb_ds macro. +1 src/widgets/kpunch/dataCtrl/widgetDataCtrl.c|fillRows|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: strings stored in row.fields / d->colNames via arrput, released by freeCache (which now walks arrlen, fixing the real partial-load leak). 2 src/widgets/kpunch/dataCtrl/widgetDataCtrl.c|loadCache|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: d->sortIndex is freed on the !keys || !d->sortIndex path and otherwise kept in the widget (freed on rebuild/destroy); the analyzer assumes free(keys) can clobber d->sortIndex. 2 src/widgets/kpunch/dbGrid/widgetDbGrid.c|dbGridBuildSortIndex|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: each successful realloc result is stored in d->columns / d->resolvedW before return. 2 src/widgets/kpunch/dbGrid/widgetDbGrid.c|dbGridSetDataWidget|leak of [CWE-401] [-Wanalyzer-malloc-leak] +# FP: dbGridSortMerge's merged index becomes d->sortIndex (the old one freed first; the new one freed with the grid or at the next sort); the analyzer loses the store into the caller's struct. +1 src/widgets/kpunch/dbGrid/widgetDbGrid.c|dbGridSortMerge|leak of [CWE-401] [-Wanalyzer-malloc-leak] +# FP: dbGridSortMerge inlined into dbGridSyncData: the same store of the merged index into d->sortIndex. +1 src/widgets/kpunch/dbGrid/widgetDbGrid.c|dbGridSyncData|leak of [CWE-401] [-Wanalyzer-malloc-leak] +# FP: buildImagePic keeps a copy of the picture (kept.pixels) via arrput in the help file's picture cache (pics->pics, freed by picsFree with the file); stb_ds OOM hook never returns. +1 src/widgets/kpunch/helpView/widgetHelpView.c|buildImagePic|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: the strdup'd target is stored in hv->linkTarget (the view's heap data, freed on the next link and when the widget is destroyed); the analyzer loses the store into the heap struct. 1 src/widgets/kpunch/helpView/widgetHelpView.c|helpLinkFollow|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: title strdup stored via arrput in titles, which becomes lv->ownedColTitles (NULL titles are tolerated). diff --git a/src/test/lint/asciiCheck.sh b/src/test/lint/asciiCheck.sh index 1701a1f4..09a25703 100755 --- a/src/test/lint/asciiCheck.sh +++ b/src/test/lint/asciiCheck.sh @@ -1,4 +1,26 @@ #!/bin/bash +# 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. + # # asciiCheck.sh -- fail if any text file under src/ contains a non-ASCII # byte. Thirdparty code is excluded. diff --git a/src/test/lint/baseline.txt b/src/test/lint/baseline.txt index 211d8a2a..bb57d576 100644 --- a/src/test/lint/baseline.txt +++ b/src/test/lint/baseline.txt @@ -129,7 +129,7 @@ 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|static void serLineSet(int (*const *set)(int, bool), int32_t com, int32_t on) { 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|static void serLineSet(int (*const *set)(int, bool), int32_t com, int32_t on); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|void *(*open)(int, int32_t, int, char, int, int, void (*)(void *, const uint8_t *, int, uint8_t, bool), void *); -1 src/apps/kpunch/dvxbasic/formrt/frmModelBas.c|plainInt|snprintf(mi.caption, sizeof(mi.caption), "", (int)len, label); +1 src/apps/kpunch/dvxbasic/basdev/frmModelBas.c|plainInt|snprintf(mi.caption, sizeof(mi.caption), "", (int)len, label); 1 src/apps/kpunch/dvxbasic/formrt/frmParser.c|plainInt|dvxLog("", (int)lineNum, FRM_MAX_LINE_LEN - 1); 1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "", (int)IDE_MAX_SOURCE, sProject.files[failIdx].path); 1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "", errFile, (int)errLocalLine, msg); @@ -582,64 +582,64 @@ 1 src/libs/kpunch/print/printText.c|plainInt|snprintf(stamp, sizeof(stamp), "", (int)page); 1 src/libs/kpunch/print/printText.c|plainInt|snprintf(stamp, sizeof(stamp), "", (int)pages); 1 src/libs/kpunch/print/printUi.c|plainInt|snprintf(allText, sizeof(allText), "", (int)pageCount); -1 src/libs/kpunch/serial/packet/packet.c|plainInt|PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx); -1 src/libs/kpunch/serial/packet/packet.c|plainInt|PktConnT *pktOpenStream(const PktStreamT *stream, int windowSize, PktRecvCallbackT callback, void *callbackCtx) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|PktConnT *pktOpenStream(const PktStreamT *stream, int windowSize, PktRecvCallbackT callback, void *callbackCtx); -1 src/libs/kpunch/serial/packet/packet.c|plainInt|for (int i = 0; i < conn->txCount; i++) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|for (int i = 0; i < len; i++) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|for (int i = 0; i < rawLen; i++) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|for (int i = idx; i < conn->txCount; i++) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int com; -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int delivered = 0; -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int frameLen = conn->rxFrameLen; -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int idx = txSlotIndex(conn, seq); -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int lastCount; -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int len; -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int n = conn->stream.write(conn->stream.ctx, stuffed + sent, out - sent); -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int nRead = conn->stream.read(conn->stream.ctx, conn->rxChunk, RX_READ_CHUNK); -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int out; -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int payloadLen; -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pktGetPending(PktConnT *conn) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pktGetPending(PktConnT *conn); -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pktPoll(PktConnT *conn) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pktPoll(PktConnT *conn); -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pktReset(PktConnT *conn) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pktReset(PktConnT *conn); -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block); -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pktWaitSpace(PktConnT *conn) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pktWaitSpace(PktConnT *conn); -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pollDepth; -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int rawLen; -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int rc = pktWaitSpace(conn); -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int rxChunkLen; -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int rxChunkPos; -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int rxFrameLen; -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int sent; -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int txCount; -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int txHead; -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int windowSize; -1 src/libs/kpunch/serial/packet/packet.c|plainInt|static PktConnT *connCreate(const PktStreamT *stream, int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|static PktConnT *connCreate(const PktStreamT *stream, int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx); -1 src/libs/kpunch/serial/packet/packet.c|plainInt|static bool processFrame(PktConnT *conn, const uint8_t *frame, int len) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|static bool processFrame(PktConnT *conn, const uint8_t *frame, int len); -1 src/libs/kpunch/serial/packet/packet.c|plainInt|static int seqInWindow(uint8_t seq, uint8_t base, int size) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|static int seqInWindow(uint8_t seq, uint8_t base, int size); -1 src/libs/kpunch/serial/packet/packet.c|plainInt|static int txSlotIndex(PktConnT *conn, uint8_t seq) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|static int txSlotIndex(PktConnT *conn, uint8_t seq); -1 src/libs/kpunch/serial/packet/packet.c|plainInt|static uint16_t crcCalc(const uint8_t *data, int len) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|static uint16_t crcCalc(const uint8_t *data, int len); -1 src/libs/kpunch/serial/packet/packet.c|plainInt|static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len); -1 src/libs/kpunch/serial/packet/packet.h|plainInt|PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx); -1 src/libs/kpunch/serial/packet/packet.h|plainInt|PktConnT *pktOpenStream(const PktStreamT *stream, int windowSize, PktRecvCallbackT callback, void *callbackCtx); -1 src/libs/kpunch/serial/packet/packet.h|plainInt|int pktGetPending(PktConnT *conn); -1 src/libs/kpunch/serial/packet/packet.h|plainInt|int pktPoll(PktConnT *conn); -1 src/libs/kpunch/serial/packet/packet.h|plainInt|int pktReset(PktConnT *conn); -1 src/libs/kpunch/serial/packet/packet.h|plainInt|int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block); -1 src/libs/kpunch/serial/packet/packet.h|plainInt|int pktWaitSpace(PktConnT *conn); -1 src/libs/kpunch/serial/packet/packet.h|plainInt|typedef void (*PktRecvCallbackT)(void *ctx, const uint8_t *data, int len); +1 src/libs/kpunch/seclink/packet.c|plainInt|PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx) { +1 src/libs/kpunch/seclink/packet.c|plainInt|PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx); +1 src/libs/kpunch/seclink/packet.c|plainInt|PktConnT *pktOpenStream(const PktStreamT *stream, int windowSize, PktRecvCallbackT callback, void *callbackCtx) { +1 src/libs/kpunch/seclink/packet.c|plainInt|PktConnT *pktOpenStream(const PktStreamT *stream, int windowSize, PktRecvCallbackT callback, void *callbackCtx); +1 src/libs/kpunch/seclink/packet.c|plainInt|for (int i = 0; i < conn->txCount; i++) { +1 src/libs/kpunch/seclink/packet.c|plainInt|for (int i = 0; i < len; i++) { +1 src/libs/kpunch/seclink/packet.c|plainInt|for (int i = 0; i < rawLen; i++) { +1 src/libs/kpunch/seclink/packet.c|plainInt|for (int i = idx; i < conn->txCount; i++) { +1 src/libs/kpunch/seclink/packet.c|plainInt|int com; +1 src/libs/kpunch/seclink/packet.c|plainInt|int delivered = 0; +1 src/libs/kpunch/seclink/packet.c|plainInt|int frameLen = conn->rxFrameLen; +1 src/libs/kpunch/seclink/packet.c|plainInt|int idx = txSlotIndex(conn, seq); +1 src/libs/kpunch/seclink/packet.c|plainInt|int lastCount; +1 src/libs/kpunch/seclink/packet.c|plainInt|int len; +1 src/libs/kpunch/seclink/packet.c|plainInt|int n = conn->stream.write(conn->stream.ctx, stuffed + sent, out - sent); +1 src/libs/kpunch/seclink/packet.c|plainInt|int nRead = conn->stream.read(conn->stream.ctx, conn->rxChunk, RX_READ_CHUNK); +1 src/libs/kpunch/seclink/packet.c|plainInt|int out; +1 src/libs/kpunch/seclink/packet.c|plainInt|int payloadLen; +1 src/libs/kpunch/seclink/packet.c|plainInt|int pktGetPending(PktConnT *conn) { +1 src/libs/kpunch/seclink/packet.c|plainInt|int pktGetPending(PktConnT *conn); +1 src/libs/kpunch/seclink/packet.c|plainInt|int pktPoll(PktConnT *conn) { +1 src/libs/kpunch/seclink/packet.c|plainInt|int pktPoll(PktConnT *conn); +1 src/libs/kpunch/seclink/packet.c|plainInt|int pktReset(PktConnT *conn) { +1 src/libs/kpunch/seclink/packet.c|plainInt|int pktReset(PktConnT *conn); +1 src/libs/kpunch/seclink/packet.c|plainInt|int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block) { +1 src/libs/kpunch/seclink/packet.c|plainInt|int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block); +1 src/libs/kpunch/seclink/packet.c|plainInt|int pktWaitSpace(PktConnT *conn) { +1 src/libs/kpunch/seclink/packet.c|plainInt|int pktWaitSpace(PktConnT *conn); +1 src/libs/kpunch/seclink/packet.c|plainInt|int pollDepth; +1 src/libs/kpunch/seclink/packet.c|plainInt|int rawLen; +1 src/libs/kpunch/seclink/packet.c|plainInt|int rc = pktWaitSpace(conn); +1 src/libs/kpunch/seclink/packet.c|plainInt|int rxChunkLen; +1 src/libs/kpunch/seclink/packet.c|plainInt|int rxChunkPos; +1 src/libs/kpunch/seclink/packet.c|plainInt|int rxFrameLen; +1 src/libs/kpunch/seclink/packet.c|plainInt|int sent; +1 src/libs/kpunch/seclink/packet.c|plainInt|int txCount; +1 src/libs/kpunch/seclink/packet.c|plainInt|int txHead; +1 src/libs/kpunch/seclink/packet.c|plainInt|int windowSize; +1 src/libs/kpunch/seclink/packet.c|plainInt|static PktConnT *connCreate(const PktStreamT *stream, int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx) { +1 src/libs/kpunch/seclink/packet.c|plainInt|static PktConnT *connCreate(const PktStreamT *stream, int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx); +1 src/libs/kpunch/seclink/packet.c|plainInt|static bool processFrame(PktConnT *conn, const uint8_t *frame, int len) { +1 src/libs/kpunch/seclink/packet.c|plainInt|static bool processFrame(PktConnT *conn, const uint8_t *frame, int len); +1 src/libs/kpunch/seclink/packet.c|plainInt|static int seqInWindow(uint8_t seq, uint8_t base, int size) { +1 src/libs/kpunch/seclink/packet.c|plainInt|static int seqInWindow(uint8_t seq, uint8_t base, int size); +1 src/libs/kpunch/seclink/packet.c|plainInt|static int txSlotIndex(PktConnT *conn, uint8_t seq) { +1 src/libs/kpunch/seclink/packet.c|plainInt|static int txSlotIndex(PktConnT *conn, uint8_t seq); +1 src/libs/kpunch/seclink/packet.c|plainInt|static uint16_t crcCalc(const uint8_t *data, int len) { +1 src/libs/kpunch/seclink/packet.c|plainInt|static uint16_t crcCalc(const uint8_t *data, int len); +1 src/libs/kpunch/seclink/packet.c|plainInt|static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len) { +1 src/libs/kpunch/seclink/packet.c|plainInt|static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len); +1 src/libs/kpunch/seclink/packet.h|plainInt|PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx); +1 src/libs/kpunch/seclink/packet.h|plainInt|PktConnT *pktOpenStream(const PktStreamT *stream, int windowSize, PktRecvCallbackT callback, void *callbackCtx); +1 src/libs/kpunch/seclink/packet.h|plainInt|int pktGetPending(PktConnT *conn); +1 src/libs/kpunch/seclink/packet.h|plainInt|int pktPoll(PktConnT *conn); +1 src/libs/kpunch/seclink/packet.h|plainInt|int pktReset(PktConnT *conn); +1 src/libs/kpunch/seclink/packet.h|plainInt|int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block); +1 src/libs/kpunch/seclink/packet.h|plainInt|int pktWaitSpace(PktConnT *conn); +1 src/libs/kpunch/seclink/packet.h|plainInt|typedef void (*PktRecvCallbackT)(void *ctx, const uint8_t *data, int len); 1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|sIsrAddr.pm_offset = (unsigned long)comGeneralIsr; 1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static int dpmiLockMemory(void) { 1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static int dpmiLockMemory(void); @@ -647,101 +647,101 @@ 1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static void dpmiGetPvect(int vector, _go32_dpmi_seginfo *info); 1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static void dpmiSetPvect(int vector, _go32_dpmi_seginfo *info) { 1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static void dpmiSetPvect(int vector, _go32_dpmi_seginfo *info); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int pubLen = SEC_DH_KEY_SIZE; -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkGetPending(SecLinkT *link) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkGetPending(SecLinkT *link); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkHandshake(SecLinkT *link) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkHandshake(SecLinkT *link); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkHandshakeStep(SecLinkT *link) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkHandshakeStep(SecLinkT *link); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkPoll(SecLinkT *link) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkPoll(SecLinkT *link); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkSetChannelPlain(SecLinkT *link, uint8_t channel, bool plain) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkSetChannelPlain(SecLinkT *link, uint8_t channel, bool plain); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkSetPassphrase(SecLinkT *link, const char *pass) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkSetPassphrase(SecLinkT *link, const char *pass); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secretLen = SEC_DH_KEY_SIZE; -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static int handshakeStatus(const SecLinkT *link) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static int handshakeStatus(const SecLinkT *link); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static int leaveCall(SecLinkT *link, int32_t rc) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static int leaveCall(SecLinkT *link, int32_t rc); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static int pollOnce(SecLinkT *link) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static int pollOnce(SecLinkT *link); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void internalRecv(void *ctx, const uint8_t *data, int len) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void internalRecv(void *ctx, const uint8_t *data, int len); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void onCipher(SecLinkT *link, const uint8_t *data, int len) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void onCipher(SecLinkT *link, const uint8_t *data, int len); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void onConfirm(SecLinkT *link, const uint8_t *data, int len) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void onConfirm(SecLinkT *link, const uint8_t *data, int len); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void onHello(SecLinkT *link, const uint8_t *data, int len) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void onHello(SecLinkT *link, const uint8_t *data, int len); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void onPlain(SecLinkT *link, const uint8_t *data, int len) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void onPlain(SecLinkT *link, const uint8_t *data, int len); -1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx); -1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|int secLinkGetPending(SecLinkT *link); -1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|int secLinkHandshake(SecLinkT *link); -1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|int secLinkHandshakeStep(SecLinkT *link); -1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|int secLinkPoll(SecLinkT *link); -1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block); -1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt); -1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|int secLinkSetChannelPlain(SecLinkT *link, uint8_t channel, bool plain); -1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|int secLinkSetPassphrase(SecLinkT *link, const char *pass); -1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted); -2 src/libs/kpunch/serial/security/secBas.c|plainInt|int len = SEC_DH_KEY_SIZE; -1 src/libs/kpunch/serial/security/secHash.c|plainInt|int secHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, uint8_t *okm, int32_t okmLen) { -1 src/libs/kpunch/serial/security/secHash.c|plainInt|int secHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, uint8_t *okm, int32_t okmLen); -1 src/libs/kpunch/serial/security/secHash.h|plainInt|int secHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, uint8_t *okm, int32_t okmLen); -7 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = 0; i < BN_WORDS; i++) { -1 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = 0; i < M0INV_NEWTON_ITERS; i++) { -1 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = 0; i < R2_DOUBLE_ITERATIONS; i++) { -1 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = 0; i < XTEA_ROUNDS; i++) { -1 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = 0; i < len; i++) { -1 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = BN_WORDS - 1; i >= 0; i--) { -1 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = DH_PRIVATE_BITS - 1; i >= 0; i--) { -1 src/libs/kpunch/serial/security/security.c|plainInt|for (int j = 0; j < BN_WORDS; j++) { -1 src/libs/kpunch/serial/security/security.c|plainInt|for (int j = 1; j < BN_WORDS; j++) { -1 src/libs/kpunch/serial/security/security.c|plainInt|int carry = bnShiftLeft1(r2); -2 src/libs/kpunch/serial/security/security.c|plainInt|int j = (BN_WORDS - 1 - i) * 4; -1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len) { -1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len); -1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhGenerateKeys(SecDhT *dh) { -1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhGenerateKeys(SecDhT *dh); -1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len) { -1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len); -1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len) { -1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len); -1 src/libs/kpunch/serial/security/security.c|plainInt|int secRngLoadSeed(const char *path) { -1 src/libs/kpunch/serial/security/security.c|plainInt|int secRngLoadSeed(const char *path); -1 src/libs/kpunch/serial/security/security.c|plainInt|int secRngSaveSeed(const char *path) { -1 src/libs/kpunch/serial/security/security.c|plainInt|int secRngSaveSeed(const char *path); -1 src/libs/kpunch/serial/security/security.c|plainInt|return (int)borrow; -1 src/libs/kpunch/serial/security/security.c|plainInt|secRngAddEntropy(seed, (int)got); -1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnBit(const BigNumT *a, int n) { -1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnBit(const BigNumT *a, int n); -1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnCmp(const BigNumT *a, const BigNumT *b) { -1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnCmp(const BigNumT *a, const BigNumT *b); -1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnShiftLeft1(BigNumT *a) { -1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnShiftLeft1(BigNumT *a); -1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnSub(BigNumT *result, const BigNumT *a, const BigNumT *b) { -1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnSub(BigNumT *result, const BigNumT *a, const BigNumT *b); -1 src/libs/kpunch/serial/security/security.c|plainInt|void secCipherCrypt(SecCipherT *c, uint8_t *data, int len) { -1 src/libs/kpunch/serial/security/security.c|plainInt|void secCipherCrypt(SecCipherT *c, uint8_t *data, int len); -1 src/libs/kpunch/serial/security/security.c|plainInt|void secSecureZero(void *ptr, int len) { -1 src/libs/kpunch/serial/security/security.c|plainInt|void secSecureZero(void *ptr, int len); -1 src/libs/kpunch/serial/security/security.h|plainInt|int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len); -1 src/libs/kpunch/serial/security/security.h|plainInt|int secDhGenerateKeys(SecDhT *dh); -1 src/libs/kpunch/serial/security/security.h|plainInt|int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len); -1 src/libs/kpunch/serial/security/security.h|plainInt|int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len); -1 src/libs/kpunch/serial/security/security.h|plainInt|int secRngLoadSeed(const char *path); -1 src/libs/kpunch/serial/security/security.h|plainInt|int secRngSaveSeed(const char *path); -1 src/libs/kpunch/serial/security/security.h|plainInt|void secCipherCrypt(SecCipherT *c, uint8_t *data, int len); -1 src/libs/kpunch/serial/security/security.h|plainInt|void secSecureZero(void *ptr, int len); +1 src/libs/kpunch/seclink/secLink.c|plainInt|SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx) { +1 src/libs/kpunch/seclink/secLink.c|plainInt|SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx); +1 src/libs/kpunch/seclink/secLink.c|plainInt|int pubLen = SEC_DH_KEY_SIZE; +1 src/libs/kpunch/seclink/secLink.c|plainInt|int secLinkGetPending(SecLinkT *link) { +1 src/libs/kpunch/seclink/secLink.c|plainInt|int secLinkGetPending(SecLinkT *link); +1 src/libs/kpunch/seclink/secLink.c|plainInt|int secLinkHandshake(SecLinkT *link) { +1 src/libs/kpunch/seclink/secLink.c|plainInt|int secLinkHandshake(SecLinkT *link); +1 src/libs/kpunch/seclink/secLink.c|plainInt|int secLinkHandshakeStep(SecLinkT *link) { +1 src/libs/kpunch/seclink/secLink.c|plainInt|int secLinkHandshakeStep(SecLinkT *link); +1 src/libs/kpunch/seclink/secLink.c|plainInt|int secLinkPoll(SecLinkT *link) { +1 src/libs/kpunch/seclink/secLink.c|plainInt|int secLinkPoll(SecLinkT *link); +1 src/libs/kpunch/seclink/secLink.c|plainInt|int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block) { +1 src/libs/kpunch/seclink/secLink.c|plainInt|int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block); +1 src/libs/kpunch/seclink/secLink.c|plainInt|int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt) { +1 src/libs/kpunch/seclink/secLink.c|plainInt|int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt); +1 src/libs/kpunch/seclink/secLink.c|plainInt|int secLinkSetChannelPlain(SecLinkT *link, uint8_t channel, bool plain) { +1 src/libs/kpunch/seclink/secLink.c|plainInt|int secLinkSetChannelPlain(SecLinkT *link, uint8_t channel, bool plain); +1 src/libs/kpunch/seclink/secLink.c|plainInt|int secLinkSetPassphrase(SecLinkT *link, const char *pass) { +1 src/libs/kpunch/seclink/secLink.c|plainInt|int secLinkSetPassphrase(SecLinkT *link, const char *pass); +1 src/libs/kpunch/seclink/secLink.c|plainInt|int secretLen = SEC_DH_KEY_SIZE; +1 src/libs/kpunch/seclink/secLink.c|plainInt|static int handshakeStatus(const SecLinkT *link) { +1 src/libs/kpunch/seclink/secLink.c|plainInt|static int handshakeStatus(const SecLinkT *link); +1 src/libs/kpunch/seclink/secLink.c|plainInt|static int leaveCall(SecLinkT *link, int32_t rc) { +1 src/libs/kpunch/seclink/secLink.c|plainInt|static int leaveCall(SecLinkT *link, int32_t rc); +1 src/libs/kpunch/seclink/secLink.c|plainInt|static int pollOnce(SecLinkT *link) { +1 src/libs/kpunch/seclink/secLink.c|plainInt|static int pollOnce(SecLinkT *link); +1 src/libs/kpunch/seclink/secLink.c|plainInt|static void internalRecv(void *ctx, const uint8_t *data, int len) { +1 src/libs/kpunch/seclink/secLink.c|plainInt|static void internalRecv(void *ctx, const uint8_t *data, int len); +1 src/libs/kpunch/seclink/secLink.c|plainInt|static void onCipher(SecLinkT *link, const uint8_t *data, int len) { +1 src/libs/kpunch/seclink/secLink.c|plainInt|static void onCipher(SecLinkT *link, const uint8_t *data, int len); +1 src/libs/kpunch/seclink/secLink.c|plainInt|static void onConfirm(SecLinkT *link, const uint8_t *data, int len) { +1 src/libs/kpunch/seclink/secLink.c|plainInt|static void onConfirm(SecLinkT *link, const uint8_t *data, int len); +1 src/libs/kpunch/seclink/secLink.c|plainInt|static void onHello(SecLinkT *link, const uint8_t *data, int len) { +1 src/libs/kpunch/seclink/secLink.c|plainInt|static void onHello(SecLinkT *link, const uint8_t *data, int len); +1 src/libs/kpunch/seclink/secLink.c|plainInt|static void onPlain(SecLinkT *link, const uint8_t *data, int len) { +1 src/libs/kpunch/seclink/secLink.c|plainInt|static void onPlain(SecLinkT *link, const uint8_t *data, int len); +1 src/libs/kpunch/seclink/secLink.h|plainInt|SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx); +1 src/libs/kpunch/seclink/secLink.h|plainInt|int secLinkGetPending(SecLinkT *link); +1 src/libs/kpunch/seclink/secLink.h|plainInt|int secLinkHandshake(SecLinkT *link); +1 src/libs/kpunch/seclink/secLink.h|plainInt|int secLinkHandshakeStep(SecLinkT *link); +1 src/libs/kpunch/seclink/secLink.h|plainInt|int secLinkPoll(SecLinkT *link); +1 src/libs/kpunch/seclink/secLink.h|plainInt|int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block); +1 src/libs/kpunch/seclink/secLink.h|plainInt|int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt); +1 src/libs/kpunch/seclink/secLink.h|plainInt|int secLinkSetChannelPlain(SecLinkT *link, uint8_t channel, bool plain); +1 src/libs/kpunch/seclink/secLink.h|plainInt|int secLinkSetPassphrase(SecLinkT *link, const char *pass); +1 src/libs/kpunch/seclink/secLink.h|plainInt|typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted); +2 src/libs/kpunch/crypto/secBas.c|plainInt|int len = SEC_DH_KEY_SIZE; +1 src/libs/kpunch/crypto/secHash.c|plainInt|int secHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, uint8_t *okm, int32_t okmLen) { +1 src/libs/kpunch/crypto/secHash.c|plainInt|int secHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, uint8_t *okm, int32_t okmLen); +1 src/libs/kpunch/crypto/secHash.h|plainInt|int secHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, uint8_t *okm, int32_t okmLen); +7 src/libs/kpunch/crypto/security.c|plainInt|for (int i = 0; i < BN_WORDS; i++) { +1 src/libs/kpunch/crypto/security.c|plainInt|for (int i = 0; i < M0INV_NEWTON_ITERS; i++) { +1 src/libs/kpunch/crypto/security.c|plainInt|for (int i = 0; i < R2_DOUBLE_ITERATIONS; i++) { +1 src/libs/kpunch/crypto/security.c|plainInt|for (int i = 0; i < XTEA_ROUNDS; i++) { +1 src/libs/kpunch/crypto/security.c|plainInt|for (int i = 0; i < len; i++) { +1 src/libs/kpunch/crypto/security.c|plainInt|for (int i = BN_WORDS - 1; i >= 0; i--) { +1 src/libs/kpunch/crypto/security.c|plainInt|for (int i = DH_PRIVATE_BITS - 1; i >= 0; i--) { +1 src/libs/kpunch/crypto/security.c|plainInt|for (int j = 0; j < BN_WORDS; j++) { +1 src/libs/kpunch/crypto/security.c|plainInt|for (int j = 1; j < BN_WORDS; j++) { +1 src/libs/kpunch/crypto/security.c|plainInt|int carry = bnShiftLeft1(r2); +2 src/libs/kpunch/crypto/security.c|plainInt|int j = (BN_WORDS - 1 - i) * 4; +1 src/libs/kpunch/crypto/security.c|plainInt|int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len) { +1 src/libs/kpunch/crypto/security.c|plainInt|int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len); +1 src/libs/kpunch/crypto/security.c|plainInt|int secDhGenerateKeys(SecDhT *dh) { +1 src/libs/kpunch/crypto/security.c|plainInt|int secDhGenerateKeys(SecDhT *dh); +1 src/libs/kpunch/crypto/security.c|plainInt|int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len) { +1 src/libs/kpunch/crypto/security.c|plainInt|int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len); +1 src/libs/kpunch/crypto/security.c|plainInt|int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len) { +1 src/libs/kpunch/crypto/security.c|plainInt|int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len); +1 src/libs/kpunch/crypto/security.c|plainInt|int secRngLoadSeed(const char *path) { +1 src/libs/kpunch/crypto/security.c|plainInt|int secRngLoadSeed(const char *path); +1 src/libs/kpunch/crypto/security.c|plainInt|int secRngSaveSeed(const char *path) { +1 src/libs/kpunch/crypto/security.c|plainInt|int secRngSaveSeed(const char *path); +1 src/libs/kpunch/crypto/security.c|plainInt|return (int)borrow; +1 src/libs/kpunch/crypto/security.c|plainInt|secRngAddEntropy(seed, (int)got); +1 src/libs/kpunch/crypto/security.c|plainInt|static int bnBit(const BigNumT *a, int n) { +1 src/libs/kpunch/crypto/security.c|plainInt|static int bnBit(const BigNumT *a, int n); +1 src/libs/kpunch/crypto/security.c|plainInt|static int bnCmp(const BigNumT *a, const BigNumT *b) { +1 src/libs/kpunch/crypto/security.c|plainInt|static int bnCmp(const BigNumT *a, const BigNumT *b); +1 src/libs/kpunch/crypto/security.c|plainInt|static int bnShiftLeft1(BigNumT *a) { +1 src/libs/kpunch/crypto/security.c|plainInt|static int bnShiftLeft1(BigNumT *a); +1 src/libs/kpunch/crypto/security.c|plainInt|static int bnSub(BigNumT *result, const BigNumT *a, const BigNumT *b) { +1 src/libs/kpunch/crypto/security.c|plainInt|static int bnSub(BigNumT *result, const BigNumT *a, const BigNumT *b); +1 src/libs/kpunch/crypto/security.c|plainInt|void secCipherCrypt(SecCipherT *c, uint8_t *data, int len) { +1 src/libs/kpunch/crypto/security.c|plainInt|void secCipherCrypt(SecCipherT *c, uint8_t *data, int len); +1 src/libs/kpunch/crypto/security.c|plainInt|void secSecureZero(void *ptr, int len) { +1 src/libs/kpunch/crypto/security.c|plainInt|void secSecureZero(void *ptr, int len); +1 src/libs/kpunch/crypto/security.h|plainInt|int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len); +1 src/libs/kpunch/crypto/security.h|plainInt|int secDhGenerateKeys(SecDhT *dh); +1 src/libs/kpunch/crypto/security.h|plainInt|int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len); +1 src/libs/kpunch/crypto/security.h|plainInt|int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len); +1 src/libs/kpunch/crypto/security.h|plainInt|int secRngLoadSeed(const char *path); +1 src/libs/kpunch/crypto/security.h|plainInt|int secRngSaveSeed(const char *path); +1 src/libs/kpunch/crypto/security.h|plainInt|void secCipherCrypt(SecCipherT *c, uint8_t *data, int len); +1 src/libs/kpunch/crypto/security.h|plainInt|void secSecureZero(void *ptr, int len); 1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|int sqlite3_os_end(void) { 1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|int sqlite3_os_end(void); 1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|int sqlite3_os_init(void) { diff --git a/src/test/lint/clangTidy.yaml b/src/test/lint/clangTidy.yaml index 66009557..1319f866 100644 --- a/src/test/lint/clangTidy.yaml +++ b/src/test/lint/clangTidy.yaml @@ -1,3 +1,25 @@ +# 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. + # clangTidy.yaml -- clang-tidy configuration for the DVX gate # (qaGate.sh tidy; passed with --config-file, so it applies only there). # diff --git a/src/test/lint/clangTidyBaseline.txt b/src/test/lint/clangTidyBaseline.txt index c39cfa25..6e65ea0f 100644 --- a/src/test/lint/clangTidyBaseline.txt +++ b/src/test/lint/clangTidyBaseline.txt @@ -19,9 +19,9 @@ # FP: rawResult is BasNativeWordT (uintptr_t), the native call's pointer-sized return register; a STRING-returning DECLARE LIBRARY function returns a char * in it 2 src/apps/kpunch/dvxbasic/formrt/formrt.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities # FP: frmMenuPropText: the path assumes text = mi->name is NULL, but the address of an array member is never NULL, so mi is not NULL -1 src/apps/kpunch/dvxbasic/formrt/frmModel.c|clang-analyzer-core.NullDereference|Access to field 'level' results in a dereference of a null pointer (loaded from variable 'mi') +1 src/apps/kpunch/dvxbasic/basdev/frmModel.c|clang-analyzer-core.NullDereference|Access to field 'level' results in a dereference of a null pointer (loaded from variable 'mi') # FP: frmModelReparent refuses a new parent inside ctrl: frmModelIsDescendant(m, parent, ctrl) asks whether parent descends from ancestor ctrl, the intended order -1 src/apps/kpunch/dvxbasic/formrt/frmModel.c|readability-suspicious-call-argument|2nd argument 'parent' (passed to 'ctrl') looks like it might be swapped with the 3rd, 'ctrl' (passed to 'ancestor') +1 src/apps/kpunch/dvxbasic/basdev/frmModel.c|readability-suspicious-call-argument|2nd argument 'parent' (passed to 'ctrl') looks like it might be swapped with the 3rd, 'ctrl' (passed to 'ancestor') # FP: pkgDpkFolderFiles(folder, prefix, out) reads the staged files from scratch and names them under the package's install folder (the prefix), its parameter order; only the names differ 1 src/apps/kpunch/dvxbasic/ide/idePackage.c|readability-suspicious-call-argument|1st argument 'scratch' (passed to 'folder') looks like it might be swapped with the 2nd, 'folder' (passed to 'prefix') # FP: the project tree keeps a file index (or -1 for group rows) in the widget's void *userData, the only per-item slot the tree widget offers; it is cast back, never dereferenced @@ -74,10 +74,14 @@ 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 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 app id is passed as the task argument (the app task and the app's work task) on purpose: the slot is looked up again when the task starts +2 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 1 src/libs/kpunch/dvxshell/shellBas.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities +# FP: the program cache file is length-delimited: the magic's 8 bytes and each name and description after its length byte; no NUL is wanted +2 src/libs/kpunch/dvxshell/shellProgCache.c|bugprone-not-null-terminated-result|the result from calling 'memcpy' is not null-terminated +# FP: saveStep runs only while sSave.f is open, and saveBegin opens it only once formatCache gave the data (saveEnd frees both together) +1 src/libs/kpunch/dvxshell/shellProgCache.c|clang-analyzer-unix.StdCLibraryFunctions|The 1st argument to 'fwrite' is NULL but should not be NULL # FP: bodyWrite gets at most the bytes the socket read into the 4096-byte buffer (a body takes part of them), never more 1 src/libs/kpunch/http/http.c|clang-analyzer-unix.StdCLibraryFunctions|The 1st argument to 'fwrite' is a buffer with size 4096 but should be a buffer with size equal to or greater than the value of the 2nd argument (which is 1) times the 3rd argument # FP: platformAltScanToChar returns 0 or an ASCII letter or digit from its table, never a negative char @@ -108,6 +112,8 @@ 1 src/libs/kpunch/libdvx/widgetIface.c|bugprone-switch-missing-default-case|switching on non-enum value without default case may not cover all cases # FP: the dummy return address is written at the aligned stack top, which is computed as an integer on purpose 1 src/libs/kpunch/libtasks/taskswitch.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities +# FP: vocOpen: the voc format is set only by vocSetFormat after checkFormat accepted 1 to CHANNELS_MAX channels, and haveFmt is checked first, so channels is never 0 +1 src/libs/kpunch/music/musicSound.c|clang-analyzer-core.DivideZero|Division by zero # FP: sVibrato is a signed int8_t table of pitch steps (-2..2); the sign is meant 1 src/libs/kpunch/music/musicSynth.c|bugprone-signed-char-misuse,cert-str34-c|'signed char' to 'int32_t' (aka 'int') conversion; consider casting to 'unsigned char' first. # FP: platformPathJoin(out, size, dir, name) builds the SYSTEM folder (sys) and then the database folder in it, written to the variable named dir; only the names differ @@ -121,13 +127,15 @@ # FP: outLine builds a length-delimited line (margin, text, CR LF) that outBytes writes with its length; no NUL is wanted 1 src/libs/kpunch/print/printText.c|bugprone-not-null-terminated-result|the result from calling 'memcpy' is not null-terminated # FP: the frame-type switch drops unknown types from the wire on purpose, and rxState only ever holds RX_STATE_HUNT, ACTIVE or ESCAPE, each with a case -2 src/libs/kpunch/serial/packet/packet.c|bugprone-switch-missing-default-case|switching on non-enum value without default case may not cover all cases +2 src/libs/kpunch/seclink/packet.c|bugprone-switch-missing-default-case|switching on non-enum value without default case may not cover all cases # FP: the COM port number is the packet stream's context pointer on purpose -1 src/libs/kpunch/serial/packet/packet.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities -# FP: destroyLink runs only when depth returns to 0; secLinkHandshake holds depth >= 1 around its secLinkHandshakeStep calls (enterCall / leaveCall always pair, depth starts at 0), so the inner leaveCall never frees -1 src/libs/kpunch/serial/seclink/secLink.c|clang-analyzer-unix.Malloc|Use of memory after it is freed +1 src/libs/kpunch/seclink/packet.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities +# FP: destroyLink runs only when depth returns to 0; secLinkHandshake holds depth >= 1 around its secLinkHandshakeStart and secLinkHandshakeStep calls (enterCall / leaveCall always pair, depth starts at 0), so their inner leaveCall never frees +3 src/libs/kpunch/seclink/secLink.c|clang-analyzer-unix.Malloc|Use of memory after it is freed # FP: the result buffer is allocated as the last test of the guard, only after the argument checks pass; parenthesized and negated on purpose -4 src/libs/kpunch/serial/security/secBas.c|bugprone-assignment-in-if-condition|an assignment within an 'if' condition is bug-prone +4 src/libs/kpunch/crypto/secBas.c|bugprone-assignment-in-if-condition|an assignment within an 'if' condition is bug-prone +# FP: the 1-based database handle is SQLite's progress-callback argument on purpose: sDbs grows by realloc, so an entry pointer could go stale +1 src/libs/kpunch/sql/dvxSql.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities # FP: a link record is 'target NUL display' and addRecord gets its length; the target copy includes its NUL, the display text needs none 1 src/tools/dvxhlpc.c|bugprone-not-null-terminated-result|the result from calling 'memcpy' is not null-terminated # FP: _POSIX_C_SOURCE is the POSIX feature-test macro; the program must define it before any include diff --git a/src/test/lint/codespellWords.txt b/src/test/lint/codespellWords.txt index 4d955109..1dfe8be1 100644 --- a/src/test/lint/codespellWords.txt +++ b/src/test/lint/codespellWords.txt @@ -59,3 +59,5 @@ tyes # The AD1848 codec's Auto-Calibrate In-progress bit, as its data sheet # names it (wssDrv.c) aci +# the PNG filter that predicts from the left, upper, and upper-left pixels (Alan Paeth) +paeth diff --git a/src/test/lint/cppcheckBaseline.txt b/src/test/lint/cppcheckBaseline.txt index dd254b6c..0fae0668 100644 --- a/src/test/lint/cppcheckBaseline.txt +++ b/src/test/lint/cppcheckBaseline.txt @@ -75,6 +75,10 @@ # FP: signature fixed by the BasDeviceCallbacksT frameBusy pointer type (void *ctx shared with callbacks that write through it). 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'deviceFrameBusy' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s). # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'deviceShellResult' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s). +# FP: formFullChanged is an ExclusiveFnT; the callback type fixes the parameter +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|constParameterCallback|Parameter 'win' can be declared as pointer to const. However it seems that 'formFullChanged' is a callback function, if 'win' is declared with const you might also need to cast function pointer(s). +# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|constParameterCallback|Parameter 'win' can be declared as pointer to const. However it seems that 'onFormClose' is a callback function, if 'win' is declared with const you might also need to cast function pointer(s). # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|constParameterCallback|Parameter 'win' can be declared as pointer to const. However it seems that 'onFormMenu' is a callback function, if 'win' is declared with const you might also need to cast function pointer(s). @@ -163,6 +167,8 @@ 1 src/apps/kpunch/progman/progman.c|unsignedLessThanZero|Checking if unsigned expression '(sHotkeys)?((struct stbds_array_header*)(sHotkeys)-1)->capacity:0' is less than zero. # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. 1 src/installer/setup.c|constParameterCallback|Parameter 'w' can be declared as pointer to const. However it seems that 'boardPaint' is a callback function, if 'w' is declared with const you might also need to cast function pointer(s). +# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. +1 src/libs/kpunch/audio/audioLoad.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'vocMemRead' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s). # FP: signature fixed by the widget onClick callback type (WidgetT *). # FP: public audio.h API taking the program's AudioCtxT handle (non-const by convention); cppcheck calls it a callback only because its address is in the audioSynthLock pin table. 1 src/libs/kpunch/audio/audioSynth.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'audioFmFree' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s). @@ -184,6 +190,8 @@ 1 src/libs/kpunch/dvxnet/pktDrvDos.c|unusedStructMember|struct member 'FarStackT::esp' is never used. # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. 1 src/libs/kpunch/dvxshell/shellApp.c|constParameterCallback|Parameter 'arg' can be declared as pointer to const. However it seems that 'appTaskWrapper' is a callback function, if 'arg' is declared with const you might also need to cast function pointer(s). +# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. +1 src/libs/kpunch/dvxshell/shellApp.c|constParameterCallback|Parameter 'arg' can be declared as pointer to const. However it seems that 'appWorkTaskWrapper' is a callback function, if 'arg' is declared with const you might also need to cast function pointer(s). # FP: style suggestion only (variable scope), no defect. 1 src/libs/kpunch/dvxshell/shellAssoc.c|variableScope|The scope of the variable 'tokenLen' can be reduced. # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. @@ -196,6 +204,8 @@ 1 src/libs/kpunch/fileops/fileOps.c|memleak|Memory leak: entry.name # FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. 1 src/libs/kpunch/hlpfile/hlpFileBas.c|unsignedLessThanZero|Checking if unsigned expression 'hd->found?((struct stbds_array_header*)(hd->found)-1)->capacity:0' is less than zero. +# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. +1 src/libs/kpunch/libdvx/dvxApp.c|constParameterCallback|Parameter 'user' can be declared as pointer to const. However it seems that 'imageSrcEof' is a callback function, if 'user' is declared with const you might also need to cast function pointer(s). # FP: style suggestion only (could be const), no defect. 1 src/libs/kpunch/libdvx/dvxApp.c|constVariable|Variable 'keyRec' can be declared as const array # FP: style suggestion only (could be const), no defect. @@ -208,6 +218,8 @@ 1 src/libs/kpunch/libdvx/dvxFlow.c|unsignedLessThanZero|Checking if unsigned expression 'flow->lines?((struct stbds_array_header*)(flow->lines)-1)->capacity:0' is less than zero. # FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. 1 src/libs/kpunch/libdvx/dvxFlow.c|unsignedLessThanZero|Checking if unsigned expression 'flow->segs?((struct stbds_array_header*)(flow->segs)-1)->capacity:0' is less than zero. +# FP: dfnTextToUnicode's index is a uint8_t (0..255); cppcheck forgets the type past ch<0x80 +1 src/libs/kpunch/libdvx/dvxFontFmt.c|arrayIndexOutOfBoundsCond|Either the condition 'ch<0x80' is redundant or the array 'dfnCp437[256]' is accessed at index 256, which is out of bounds. # FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. 1 src/libs/kpunch/libdvx/dvxText.c|unsignedLessThanZero|Checking if unsigned expression '(sView)?((struct stbds_array_header*)(sView)-1)->capacity:0' is less than zero. # FP: the gate defines DXE_EXPORT as empty, which leaves DXE_EXPORT(strerror) as a bare (strerror) cppcheck cannot parse; the real macro builds the export table entry. @@ -230,6 +242,8 @@ 1 src/libs/kpunch/listhelp/listHelp.c|unsignedLessThanZero|Checking if unsigned expression '(owned)?((struct stbds_array_header*)(owned)-1)->capacity:0' is less than zero. # FP: inside the stb_ds arrdel macro expansion (unsigned capacity compared by the library macro), not DVX logic. 1 src/libs/kpunch/music/musicMmlPlay.c|unsignedLessThanZero|Checking if unsigned expression 'mml->parts?((struct stbds_array_header*)(mml->parts)-1)->capacity:0' is less than zero. +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. +1 src/libs/kpunch/pkg/pkgBas.c|unsignedLessThanZero|Checking if unsigned expression 'ss->zipEntries?((struct stbds_array_header*)(ss->zipEntries)-1)->capacity:0' is less than zero. # FP: parseOpts gets main's argv (char *const there); C does not convert char ** to const char *const * without a cast. 1 src/libs/kpunch/pkg/pkgCli.c|constParameter|Parameter 'argv' can be declared as const array # FP: strtol's long is 64 bits on 64-bit hosts (the host tools and tests), where n > INT32_MAX is a real check; cppcheck runs as unix32. @@ -273,17 +287,21 @@ # FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. 1 src/libs/kpunch/richtext/richText.c|unsignedLessThanZero|Checking if unsigned expression 'rt->text?((struct stbds_array_header*)(rt->text)-1)->capacity:0' is less than zero. # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. -1 src/libs/kpunch/serial/packet/packet.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'portRead' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s). +1 src/libs/kpunch/seclink/packet.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'portPending' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s). # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. -1 src/libs/kpunch/serial/packet/packet.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'portWrite' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s). +1 src/libs/kpunch/seclink/packet.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'portRead' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s). +# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. +1 src/libs/kpunch/seclink/packet.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'portWrite' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s). # FP: style suggestion only (variable scope), no defect. 1 src/libs/kpunch/serial/rs232/rs232.c|variableScope|The scope of the variable 'i' can be reduced. # FP: t is read only when done > 0, after the first block has written it (HKDF T(n-1)). -1 src/libs/kpunch/serial/security/secHash.c|uninitvar|Uninitialized variable: t +1 src/libs/kpunch/crypto/secHash.c|uninitvar|Uninitialized variable: t # FP: the sample struct is hashed as bytes into the entropy pool; the members are written and read only as part of those bytes. -1 src/libs/kpunch/serial/security/security.c|unusedStructMember|struct member 'EntropySampleT::tscHi' is never used. +1 src/libs/kpunch/crypto/security.c|unusedStructMember|struct member 'EntropySampleT::tscHi' is never used. # FP: the sample struct is hashed as bytes into the entropy pool; the members are written and read only as part of those bytes. -1 src/libs/kpunch/serial/security/security.c|unusedStructMember|struct member 'EntropySampleT::tscLo' is never used. +1 src/libs/kpunch/crypto/security.c|unusedStructMember|struct member 'EntropySampleT::tscLo' is never used. +# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. +1 src/libs/kpunch/sql/dvxSql.c|constParameterCallback|Parameter 'arg' can be declared as pointer to const. However it seems that 'progressCb' is a callback function, if 'arg' is declared with const you might also need to cast function pointer(s). # FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. 1 src/libs/kpunch/taskmgr/shellTaskMgr.c|unsignedLessThanZero|Checking if unsigned expression '(sCells)?((struct stbds_array_header*)(sCells)-1)->capacity:0' is less than zero. # FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. @@ -292,6 +310,8 @@ 1 src/libs/kpunch/texthelp/textHelp.c|constParameterCallback|Parameter 'w' can be declared as pointer to const. However it seems that 'textEditClearWidgetRef' is a callback function, if 'w' is declared with const you might also need to cast function pointer(s). # FP: style suggestion only (variable scope), no defect. 1 src/libs/kpunch/texthelp/textHelp.c|variableScope|The scope of the variable 'vc' can be reduced. +# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. +1 src/loader/loaderMain.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'exportAdd' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s). # FP: style suggestion only (could be const), no defect. 1 src/tools/bmpDraw.c|constVariable|Variable 'pad' can be declared as const array # FP: style suggestion only (could be const), no defect. diff --git a/src/test/lint/hostDb.py b/src/test/lint/hostDb.py index 7acb95b3..bff79a3c 100755 --- a/src/test/lint/hostDb.py +++ b/src/test/lint/hostDb.py @@ -1,4 +1,26 @@ #!/usr/bin/env python3 +# 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. + # hostDb.py -- compile database and clang build of the DVX host code. # # hostDb.py db write obj/qa/compile_commands.json diff --git a/src/test/lint/licenseCheck.sh b/src/test/lint/licenseCheck.sh new file mode 100755 index 00000000..0fa8282f --- /dev/null +++ b/src/test/lint/licenseCheck.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# 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. + +# licenseCheck.sh -- fail if a source file or script of the project lacks +# the MIT licence header. +# +# Usage: +# licenseCheck.sh +# +# Looked at: C (.c .h), shell, Python, Lua, make (Makefile, .mk), BASIC +# (.bas .cls .ctl .frm: a form's header follows its form block), DOS test +# scripts (.scr), batch files, resource scripts (.res), help sources (.dhs +# .bhs), and the YAML, DOSBox .conf, and sanitizer .supp files, of the +# files git has or would add (so not build output or what .gitignore +# leaves out). Not looked at: third-party code, the toolchains, +# just-stuff, the fuzzers' seed corpora (input data, not source), and the +# SDK project templates (copied into the developer's own project, so they +# carry no licence of ours). +# +# Exit status: 0 = every file has the header, 1 = some do not. + +set -u + +HERE="$(cd "$(dirname "$0")" && pwd)" +cd "$HERE/../../.." || exit 1 + +MISSING=0 +CHECKED=0 + +while IFS= read -r -d '' f; do + case "$f" in + */thirdparty/*|toolchains/*|just-stuff/*|*/seeds/*|src/sdk/templates/*) + continue + ;; + *.c|*.h|*.sh|*.py|*.lua|Makefile|*/Makefile|*.mk|*.bas|*.cls|*.ctl|*.frm|*.scr|*.bat|*.BAT|*.res|*.dhs|*.bhs|*.yaml|*.conf|*.supp) + ;; + *) + continue + ;; + esac + + [ -f "$f" ] || continue + CHECKED=$((CHECKED + 1)) + + if ! grep -q "The MIT License (MIT)" "$f"; then + echo "$f: no MIT licence header" + MISSING=$((MISSING + 1)) + fi +done < <(git ls-files -z --cached --others --exclude-standard) + +echo "licenseCheck: ${CHECKED} file(s), ${MISSING} without the header" + +if [ "${MISSING}" -ne 0 ]; then + echo "licenseCheck: FAIL" + exit 1 +fi + +echo "licenseCheck: PASS" +exit 0 diff --git a/src/test/lint/qaGate.sh b/src/test/lint/qaGate.sh index 323e4345..8da6b199 100755 --- a/src/test/lint/qaGate.sh +++ b/src/test/lint/qaGate.sh @@ -1,4 +1,26 @@ #!/bin/bash +# 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. + # # qaGate.sh -- clang-tidy, shellcheck and codespell gates for the DVX # tree, and the lizard complexity report. diff --git a/src/test/lint/staticGate.sh b/src/test/lint/staticGate.sh index b1334f28..4bbf3114 100755 --- a/src/test/lint/staticGate.sh +++ b/src/test/lint/staticGate.sh @@ -1,4 +1,26 @@ #!/bin/bash +# 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. + # # staticGate.sh -- cppcheck + gcc -fanalyzer gate for the DVX tree. # @@ -529,6 +551,8 @@ if [ "$RUN_CPPCHECK" -eq 1 ]; then -Isrc/libs/kpunch/dvxshell -Isrc/libs/kpunch/taskmgr -Isrc/libs/kpunch/serial + -Isrc/libs/kpunch/crypto + -Isrc/libs/kpunch/seclink -Isrc/libs/kpunch/audio -Isrc/libs/kpunch/music -Isrc/libs/kpunch/music/thirdparty/libxmp-lite/include @@ -635,7 +659,7 @@ WRAPEOF # -Orecurse groups each sub-make's output so the "Entering directory" # lines below stay associated with their compiles under -j. # LC_ALL=C keeps gcc quoting ASCII ('x' not UTF-8 curly quotes). - (cd "$COPY" && LC_ALL=C make -k -j"$(nproc)" -Orecurse "${TOOLCHAIN_ARGS[@]}" CC="$WRAP" HOSTCC="$HOSTWRAP" libdvx libtasks loader texthelp listhelp richtext tools widgets dvxshell taskmgr serial sql apps > "$WORK/analyzer.raw" 2>&1) + (cd "$COPY" && LC_ALL=C make -k -j"$(nproc)" -Orecurse "${TOOLCHAIN_ARGS[@]}" CC="$WRAP" HOSTCC="$HOSTWRAP" libdvx libtasks loader texthelp listhelp richtext tools widgets dvxshell taskmgr serial crypto seclink sql apps > "$WORK/analyzer.raw" 2>&1) compiles="$(grep -c "^$WRAP .* -c \|^$HOSTWRAP " "$WORK/analyzer.raw" || true)" makeErrors="$(grep -c '^make.*\*\*\*' "$WORK/analyzer.raw" || true)" echo "staticGate: analyzer compiled $compiles file(s)" diff --git a/src/test/lint/styleLint.py b/src/test/lint/styleLint.py index c5dca665..7e45b53d 100755 --- a/src/test/lint/styleLint.py +++ b/src/test/lint/styleLint.py @@ -1,4 +1,26 @@ #!/usr/bin/env python3 +# 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. + # styleLint.py -- mechanical CLAUDE.md style gate for the DVX source tree. # # Usage: diff --git a/src/test/music/Makefile b/src/test/music/Makefile index 7a2ee422..485b03e0 100644 --- a/src/test/music/Makefile +++ b/src/test/music/Makefile @@ -135,9 +135,9 @@ $(BANKFILE): $(MUSIC_DIR)/thirdparty/genmidi/GENMIDI.OP2 @mkdir -p $(dir $@) cp $< $@ -$(MUSIC_TEST): musicTest.c $(FILES_OBJ) $(LIB_OBJS) $(XMP_OBJS) $(EMU_OBJS) $(DVX_MAKEFILES) $(DVXTEST_ARCHIVES) | dvxtest-libs +$(MUSIC_TEST): musicTest.c $(FILES_OBJ) $(LIB_OBJS) $(XMP_OBJS) $(EMU_OBJS) $(DVX_MAKEFILES) $(DVXTEST_ARCHIVES) $(DVXTEST_TASKS_A) | dvxtest-libs @mkdir -p $(BINDIR) - $(HOSTCC) $(CFLAGS) -o $@ $< $(FILES_OBJ) $(LIB_OBJS) $(XMP_OBJS) $(EMU_OBJS) $(DVXTEST_LINK) + $(HOSTCC) $(CFLAGS) -o $@ $< $(FILES_OBJ) $(LIB_OBJS) $(XMP_OBJS) $(EMU_OBJS) $(DVXTEST_TASKS_A) $(DVXTEST_LINK) $(PLAYER_TEST) $(RECORDER_TEST): $(BINDIR)/%: %.c $(FILES_OBJ) $(LIB_OBJS) $(XMP_OBJS) $(SHELL_OBJS) $(FOPS_OBJS) $(DVX_MAKEFILES) $(DVXTEST_ARCHIVES) $(DVXTEST_TASKS_A) $(DVXTEST_BASRT_A) $(DVXTEST_BASSTUB_SO) | dvxtest-libs dvxtest-apps dvxtest-bascomp @mkdir -p $(BINDIR) diff --git a/src/test/music/lsan.supp b/src/test/music/lsan.supp index 1a4a3b0f..646911cf 100644 --- a/src/test/music/lsan.supp +++ b/src/test/music/lsan.supp @@ -1,3 +1,25 @@ +# 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. + # lsan.supp -- LeakSanitizer suppressions for the music suite # # emu8950 1.2.1 (the host-only OPL2 reference in toolchains/emu8950) diff --git a/src/test/music/musicFiles.c b/src/test/music/musicFiles.c index f15236ea..8dbe7704 100644 --- a/src/test/music/musicFiles.c +++ b/src/test/music/musicFiles.c @@ -158,6 +158,29 @@ uint32_t vocBuild(uint8_t *out, uint32_t frames) { } +// A VOC file of three parts: vocBuild's sound block of frames frames +// (its terminator dropped), silence frames of silence, then the same +// sound again in a continuation block. +uint32_t vocPartsBuild(uint8_t *out, uint32_t frames, uint32_t silence) { + uint32_t n = vocBuild(out, frames) - 1; + uint8_t *p = out + n; + + *p++ = VOC_BLOCK_SILENCE; + put16le(p, VOC_SILENCE_BYTES); + p[2] = 0; + put16le(p + 3, silence - 1); + p[5] = (uint8_t)(VOC_TC_BASE - VOC_TC_CLOCK / SOUND_VOC_RATE); + p += 6; + *p++ = VOC_BLOCK_CONTINUE; + put16le(p, frames); + p[2] = (uint8_t)(frames >> 16); + memcpy(p + 3, out + VOC_HEADER_BYTES + VOC_BLOCK_BYTES + VOC_SOUND_BYTES, frames); + p += 3 + frames; + *p++ = 0; + return (uint32_t)(p - out); +} + + // A WAV file of soundSine at SOUND_RATE: bits 8 or 16, the sine on the // left (stereo: the right silent), format tag tag. uint32_t wavBuild(uint8_t *out, uint32_t bits, uint32_t channels, uint32_t frames, uint32_t tag) { diff --git a/src/test/music/musicFiles.h b/src/test/music/musicFiles.h index 7160dc2c..23621176 100644 --- a/src/test/music/musicFiles.h +++ b/src/test/music/musicFiles.h @@ -61,6 +61,9 @@ #define VOC_TC_BASE 256 #define VOC_TC_CLOCK 1000000 #define VOC_CODEC_PCM8 0 +#define VOC_BLOCK_CONTINUE 2 +#define VOC_BLOCK_SILENCE 3 +#define VOC_SILENCE_BYTES 3 // its frames - 1 and time constant uint32_t buildSmf(uint8_t *buf, uint16_t type, uint16_t division, const uint8_t *const *tracks, const uint32_t *lens, int32_t count); uint32_t fluteSmf(uint8_t *smf); @@ -70,6 +73,7 @@ void put32be(uint8_t *p, uint32_t v); void put32le(uint8_t *p, uint32_t v); int32_t soundSine(uint32_t i, uint32_t frames, int32_t amp); uint32_t vocBuild(uint8_t *out, uint32_t frames); +uint32_t vocPartsBuild(uint8_t *out, uint32_t frames, uint32_t silence); uint32_t wavBuild(uint8_t *out, uint32_t bits, uint32_t channels, uint32_t frames, uint32_t tag); void writeFile(const char *path, const void *data, uint32_t size); diff --git a/src/test/music/musicTest.c b/src/test/music/musicTest.c index 9bfc69c6..23e93e7b 100644 --- a/src/test/music/musicTest.c +++ b/src/test/music/musicTest.c @@ -92,6 +92,7 @@ #include "audioInt.h" #include "emu8950.h" #include "stb_ds.h" +#include "xmp.h" #include #include @@ -159,6 +160,9 @@ #define OUT_FRAMES_MAX (RATE_DEV * 2) #define RENDER_CHUNK 512 #define MML_QUARTER_US 500000 // T120 +#define MML_C4_NOTE 72 // O4 C as a MIDI note +#define MML_D4_NOTE 74 +#define MML_V50_VELOCITY 50 // V50 of the full velocity (100) #define MML_NOTES_MAX 16 #define MML_BG_NOTES 40 #define MML_C4_MHZ 523248 // octave 4's C (default) @@ -302,6 +306,10 @@ #define SOUND_SHORT (RATE_DEV / 10) #define SOUND_SHORT_MS 100 #define SOUND_HELD 4 // device frames: the mixer's current and next source frames +#define VOC_PART_FRAMES 2000 // vocPartsBuild: each sound part +#define VOC_PART_SILENCE 1500 // ... the silence between them +#define VOC_AGREE_CHUNK 333 // soundsAgree reads this many frames at a time +#define JUNK_BYTES 8192 // a file of no music kind #define SOUND_TONE_RATIO 4.0 // a tone heard stands this far over the other #define SOUND_POS_TOL_MS 10 #define SOUND_SEEK_MS 800 // into the second half @@ -458,6 +466,7 @@ static void sfTests(void); static void smfTests(void); static void softwaveTests(void); static bool soundNear(uint32_t ms, uint32_t want); +static bool soundsAgree(SoundSrcT *a, SoundSrcT *b, uint32_t frames); static void soundTests(void); static void waveLoadTests(void); static void wavePlayTests(void); @@ -468,6 +477,7 @@ static void wrecFree(void *owner, int32_t voice); static void wrecRelease(void *owner, int32_t voice); static void wrecStart(void *owner, int32_t voice, const AudioWtVoiceT *v); static void wrecUpdate(void *owner, int32_t voice, const AudioWtVoiceT *v, uint32_t what); +static void xmpCountHook(void *user); static void xmpTests(void); @@ -690,6 +700,28 @@ static void basicTests(void) { TEST_EQ(MusicLoad("BASTEST.WAV"), 0); TEST_STR_EQ(MusicError(), audioErrorText(AUDIO_ERR_UNSUPPORTED)); + tCase("basic: MusicLoadData; MusicEnded once after the end, not after a stop"); + int32_t mem = MusicLoadData(wav, (int32_t)wavBuild(wav, 16, 1, SOUND_FRAMES, WAV_TAG_PCM)); + + TEST_TRUE(mem > 0); + TEST_STR_EQ(MusicInfo(mem, "kind"), "sound"); + TEST_EQ(MusicLoadData((const uint8_t *)"junk", 4), 0); + MusicPlay(mem, 0); + TEST_EQ(MusicEnded(mem), 0); + + for (int32_t ms = 0; ms <= SOUND_MS; ms += SOUND_STEP_MS) { + pump(SOUND_STEP); + } + + TEST_EQ(MusicEnded(mem), -1); + TEST_EQ(MusicEnded(mem), 0); + MusicPlay(mem, 0); + MusicStop(mem); + pump(SOUND_STEP); + TEST_EQ(MusicEnded(mem), 0); + TEST_EQ(MusicEnded(mem + 1), 0); + MusicFree(mem); + tCase("basic: live MIDI with channels 1..16"); int32_t live = MusicMidiOpen(); @@ -1795,6 +1827,19 @@ static void mmlPlayTests(void) { TEST_EQ(musicBasPlay("CQ"), MUSIC_ERR_MACRO); TEST_EQ(musicBasPlayCount(), 3); TEST_TRUE(!musicBasBusy()); + + // V0 is silent; V and @ last to the next string + pump(RATE_DEV * 2); + TEST_EQ(musicBasPlay("V0 @3 L8 C"), MUSIC_OK); + pump(RATE_DEV / 10); + TEST_EQ(outPeak(RATE_DEV / 10), 0); + pump(RATE_DEV / 4); + TEST_EQ(musicBasPlay("V100 C"), MUSIC_OK); + pump(RATE_DEV / 10); + TEST_TRUE(outPeak(RATE_DEV / 10) > 0); + pump(RATE_DEV / 4); + TEST_EQ(musicBasPlay("@2"), MUSIC_ERR_MACRO); + TEST_EQ(musicBasPlay("@1"), MUSIC_OK); sAppId = APP_B; TEST_EQ(musicBasPlayCount(), 0); TEST_TRUE(!musicBasBusy()); @@ -1862,8 +1907,22 @@ static void mmlTests(void) { TEST_EQ(st.octave, 0); arrsetlen(n, 0); + tCase("mml: V (volume) and @ (waveform), QB64's"); + mmlStateInit(&st); + TEST_EQ(mmlParse(&st, "C V50 @3 D @5 V0 E @1 F", &n), MUSIC_OK); + TEST_EQ(arrlen(n), 4); + TEST_EQ(n[0].volume, MML_VOLUME_MAX); + TEST_EQ(n[0].wave, AUDIO_WAVE_SQUARE); + TEST_EQ(n[1].volume, MML_VOLUME_MAX / 2); + TEST_EQ(n[1].wave, AUDIO_WAVE_TRIANGLE); + TEST_EQ(n[2].volume, 0); + TEST_EQ(n[2].wave, AUDIO_WAVE_NOISE); + TEST_EQ(n[3].wave, AUDIO_WAVE_SQUARE); + TEST_EQ(st.volume, 0); + arrsetlen(n, 0); + tCase("mml: mistakes change nothing"); - const char *bad[] = { "X", "Z", "L0", "L65", "T31", "T256", "O7", "M", "MX", "C65", "N85", "P", "P0", "O0 C-", "O6 B#", "I129", "L", "C D &" }; + const char *bad[] = { "X", "Z", "L0", "L65", "T31", "T256", "O7", "M", "MX", "C65", "N85", "P", "P0", "O0 C-", "O6 B#", "I129", "L", "C D &", "V101", "V", "@2", "@4", "@" }; for (int32_t i = 0; i < DVX_ARRAY_LEN(bad); i++) { mmlStateInit(&st); @@ -1883,6 +1942,30 @@ static void mmlTests(void) { uint8_t *smf = NULL; uint32_t size = 0; + // V scales an instrument's velocity: half at V50, no note at V0 + mmlStateInit(&st); + TEST_EQ(mmlParse(&st, "I1 V50 C V0 D", &n), MUSIC_OK); + TEST_EQ(mmlSmfBuild(n, (int32_t)arrlen(n), &smf, &size), MUSIC_OK); + + int32_t cOn = 0; + int32_t dOn = 0; + + for (uint32_t i = 0; i + 2 < size; i++) { + if (smf[i] == MIDI_NOTE_ON && smf[i + 1] == MML_C4_NOTE && smf[i + 2] == MML_V50_VELOCITY) { + cOn++; + } + + if (smf[i] == MIDI_NOTE_ON && smf[i + 1] == MML_D4_NOTE) { + dOn++; + } + } + + TEST_EQ(cOn, 1); + TEST_EQ(dOn, 0); + free(smf); + smf = NULL; + arrsetlen(n, 0); + mmlStateInit(&st); TEST_EQ(mmlParse(&st, "I41 L4 C P4 I1 D", &n), MUSIC_OK); TEST_EQ(mmlSmfBuild(n, (int32_t)arrlen(n), &smf, &size), MUSIC_OK); @@ -2140,13 +2223,18 @@ static void modHwTests(void) { TEST_TRUE(musicGetPosition(m) + MODHW_SEEK_TOL_MS >= MODHW_ORDER_MS); musicFree(m); musicShutdown(); + // Too big for the synthesizer's memory: nothing is uploaded for + // nothing (each way of storing it is passed over), it plays mixed setIni("fakeWtVoices = 8\nfakeWtMemory = 64\n"); + sWtUploads = 0; TEST_EQ(musicLoadMem(mod, modSize, &m), MUSIC_OK); TEST_EQ(musicPlay(m, false), MUSIC_OK); musicGetInfo(m, &info); TEST_EQ(info.where, MUSIC_OUT_STREAM); + TEST_EQ(sWtUploads, 0); audioWtGetInfo(&wt); TEST_EQ(wt.freeMemory, wt.memory); + TEST_EQ(wt.freeTotal, wt.memory); musicFree(m); musicShutdown(); } @@ -3011,6 +3099,7 @@ static void seqTests(void) { } +// DVX.INI as DVX starts with it: no driver modules parked from before. static void setIni(const char *text) { FILE *f = fopen(platformDvxIniPath(), "w"); @@ -3018,6 +3107,8 @@ static void setIni(const char *text) { fprintf(f, "[audio]\ndriver = pcm\n%s", text); fclose(f); } + + audioDevForget(); } @@ -3785,6 +3876,30 @@ static bool soundNear(uint32_t ms, uint32_t want) { } +// a and b give the same frames frames from where they are (all of them, +// then the end). +static bool soundsAgree(SoundSrcT *a, SoundSrcT *b, uint32_t frames) { + static int16_t fromA[VOC_AGREE_CHUNK * 2]; + static int16_t fromB[VOC_AGREE_CHUNK * 2]; + uint32_t chans = soundChannels(a); + uint32_t done = 0; + + while (done < frames) { + uint32_t want = DVX_MIN(frames - done, VOC_AGREE_CHUNK); + uint32_t gotA = soundRead(a, fromA, want); + uint32_t gotB = soundRead(b, fromB, want); + + if (gotA != want || gotB != want || memcmp(fromA, fromB, want * chans * sizeof(int16_t)) != 0) { + return false; + } + + done += want; + } + + return soundRead(a, fromA, 1) == 0 && soundRead(b, fromB, 1) == 0; +} + + // WAV and VOC files as music: a WAV played from the disk (its tone, the // position heard, pause, resume, seek, the end, again, stop, loop), 8-bit // stereo from memory, a VOC, damaged, cut-short, and compressed WAVs; a @@ -3892,6 +4007,42 @@ static void soundTests(void) { TEST_TRUE(outTone(SOUND_HELD, SOUND_STEP - SOUND_HELD, SOUND_VOC_HZ_FIRST) > SOUND_TONE_RATIO * outTone(SOUND_HELD, SOUND_STEP - SOUND_HELD, SOUND_VOC_HZ_SECOND)); musicFree(m); + tCase("sound: a VOC file of several blocks read as it plays, as decoded"); + { + SoundSrcT *fromFile = NULL; + SoundSrcT *fromMem = NULL; + uint32_t frames = 2 * VOC_PART_FRAMES + VOC_PART_SILENCE; + + size = vocPartsBuild(wav, VOC_PART_FRAMES, VOC_PART_SILENCE); + writeFile("PARTS.VOC", wav, size); + TEST_EQ(soundOpenFile("PARTS.VOC", &fromFile), MUSIC_OK); + TEST_EQ(soundOpenMem(wav, size, &fromMem), MUSIC_OK); + + if (fromFile && fromMem) { + TEST_EQ(soundFrames(fromFile), frames); + TEST_EQ(soundFrames(fromMem), frames); + TEST_TRUE(soundsAgree(fromFile, fromMem, frames)); + + // Into the silence, and into the continuation + soundSeek(fromFile, VOC_PART_FRAMES + VOC_PART_SILENCE / 2); + soundSeek(fromMem, VOC_PART_FRAMES + VOC_PART_SILENCE / 2); + TEST_TRUE(soundsAgree(fromFile, fromMem, frames - VOC_PART_FRAMES - VOC_PART_SILENCE / 2)); + soundSeek(fromFile, VOC_PART_FRAMES + VOC_PART_SILENCE + 1); + soundSeek(fromMem, VOC_PART_FRAMES + VOC_PART_SILENCE + 1); + TEST_EQ(soundTell(fromFile), VOC_PART_FRAMES + VOC_PART_SILENCE + 1); + TEST_TRUE(soundsAgree(fromFile, fromMem, VOC_PART_FRAMES - 1)); + } + + soundClose(fromFile); + soundClose(fromMem); + } + + tCase("sound: a file of no music kind is refused by its head"); + memset(wav, 'J', JUNK_BYTES); + writeFile("JUNK.DAT", wav, JUNK_BYTES); + TEST_EQ(musicLoad("JUNK.DAT", &m), MUSIC_ERR_FORMAT); + TEST_TRUE(m == NULL); + tCase("sound: damaged, cut-short, and compressed WAV files"); size = wavBuild(wav, 16, 1, SOUND_FRAMES, WAV_TAG_PCM); writeFile("CUT.WAV", wav, SOUND_CUT_BYTES); @@ -4035,11 +4186,15 @@ static void waveLoadTests(void) { TEST_EQ(wt.freeMemory, 1024); audioClose(ctx); + // A quality that cannot fit is passed over: each sample is uploaded + // once, at the quality that fits tCase("waveload: 8-bit when 16-bit does not fit"); setIni("fakeWtVoices = 4\nfakeWtMemory = 300\nfakeWtFlags = 19\n"); - ctx = audioOpen(); + ctx = audioOpen(); + sWtUploads = 0; TEST_EQ(waveCacheInit(&cache, &bank), MUSIC_OK); TEST_EQ(waveLoadNeeded(ctx, &bank, &cache, need, WT_FLAGS_HW | AUDIO_WT_8BIT, &missing), MUSIC_OK); + TEST_EQ(sWtUploads, 2); TEST_EQ(cache.loaded[0].bits, 8); TEST_EQ(cache.loaded[1].bits, 8); TEST_EQ(cache.loaded[0].shift, 0); @@ -4051,9 +4206,11 @@ static void waveLoadTests(void) { tCase("waveload: half the rate, then a quarter"); setIni("fakeWtVoices = 4\nfakeWtMemory = 300\nfakeWtFlags = 3\n"); - ctx = audioOpen(); + ctx = audioOpen(); + sWtUploads = 0; TEST_EQ(waveCacheInit(&cache, &bank), MUSIC_OK); TEST_EQ(waveLoadNeeded(ctx, &bank, &cache, need, WT_FLAGS_HW, &missing), MUSIC_OK); + TEST_EQ(sWtUploads, 2); TEST_EQ(cache.loaded[0].bits, 16); TEST_EQ(cache.loaded[0].shift, 1); TEST_EQ(cache.loaded[0].frames, WT_RAMP_FRAMES / 2); @@ -4197,6 +4354,16 @@ static void wavePlayTests(void) { audioWtGetInfo(&wt); TEST_TRUE(!wt.present); + // The SoundFont itself stayed: the next song plays on its kept bank, + // its samples loaded into the synthesizer again + uploads = sWtUploads; + TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK); + TEST_EQ(musicPlay(m, false), MUSIC_OK); + musicGetInfo(m, &info); + TEST_EQ(info.where, MUSIC_OUT_WAVE); + TEST_TRUE(sWtUploads > uploads); + musicFree(m); + // musicSuspend, the library's platform suspend hook tCase("waveplay: a DOS program run stops the music and lets the samples go at once"); TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK); @@ -4292,9 +4459,9 @@ static void wavePlayTests(void) { TEST_EQ(sWtVoice[sWtLast].pitch, WT_DRUM_PITCH); musicFree(m); - // A program's samples loaded as it is chosen (the notes sounding - // stopped first): LIVE.SF2's first program needs only Hit, Narrow - // Ramp as well + // A program's samples loaded once it is chosen, a sample a musicPump + // (the notes sounding go on): LIVE.SF2's first program needs only + // Hit, Narrow Ramp as well spec.instrument = 1; writeFile(SF_LIVE_FILE, data, sfBuild(&spec, data)); setIni("fakeWtVoices = 8\nsoundFont = " SF_LIVE_NAME "\n"); @@ -4305,8 +4472,10 @@ static void wavePlayTests(void) { TEST_TRUE(sWtActive & (1u << sWtLast)); uploads = sWtUploads; TEST_EQ(musicMidiProgram(m, 0, 1), MUSIC_OK); + TEST_EQ(sWtUploads, uploads); + musicPump(); TEST_EQ(sWtUploads, uploads + 1); - TEST_EQ(sWtActive, 0); + TEST_TRUE(sWtActive & (1u << sWtLast)); TEST_EQ(musicMidiNoteOn(m, 0, 55, 127), MUSIC_OK); TEST_EQ(sWtVoice[sWtLast].loop, AUDIO_WT_LOOP_ON); TEST_EQ(musicMidiProgram(m, 0, 0), MUSIC_OK); @@ -4558,6 +4727,12 @@ static void wrecUpdate(void *owner, int32_t voice, const AudioWtVoiceT *v, uint3 } +// The load hook of xmpTests: counts its calls. +static void xmpCountHook(void *user) { + (*(int32_t *)user)++; +} + + // Each format rendered for a second at 22,050 Hz, hashed; damaged // modules refused. static void xmpTests(void) { @@ -4566,6 +4741,34 @@ static void xmpTests(void) { const char *names[] = { "mod", "s3m", "xm", "it" }; XmpPlayerT *p; + tCase("xmp: the load hook runs before each sample (DVX patch 0002)"); + { + char path[DVX_MAX_PATH]; + int32_t len = 0; + int32_t calls = 0; + xmp_context ctx = xmp_create_context(); + + snprintf(path, sizeof(path), "%s/test.it", XMP_TEST_DIR); + + char *file = platformReadFile(path, &len); + + TEST_TRUE(file != NULL && ctx != NULL); + + if (file && ctx) { + struct xmp_module_info mi; + + xmp_set_load_hook(ctx, xmpCountHook, &calls); + TEST_EQ(xmp_load_module_from_memory(ctx, file, len), 0); + xmp_get_module_info(ctx, &mi); + TEST_TRUE(mi.mod && mi.mod->smp > 0); + TEST_TRUE(mi.mod && calls >= mi.mod->smp); + xmp_release_module(ctx); + } + + xmp_free_context(ctx); + free(file); + } + for (int32_t i = 0; i < 4; i++) { char title[GOLDEN_NAME_MAX + 16]; uint32_t size = 0; @@ -4628,6 +4831,20 @@ static void xmpTests(void) { TEST_TRUE(rc == MUSIC_OK || rc == MUSIC_ERR_FORMAT || rc == MUSIC_ERR_CORRUPT); xmpClose(p); } + + // xmpIdle lets the player module go only with no module open (a + // player's calls go through it), and the next open finds it again + tCase("xmp: the player module kept while a module is open"); + TEST_EQ(xmpOpen(data, modSize, RATE_DEV, 2, &p), MUSIC_OK); + hostClockAdvanceUs(MUSIC_XMP_KEEP_SEC * US_PER_SEC + 1); + xmpIdle(); + TEST_TRUE(p && xmpLength(p) > 0); + xmpClose(p); + hostClockAdvanceUs(MUSIC_XMP_KEEP_SEC * US_PER_SEC + 1); + xmpIdle(); + TEST_EQ(xmpOpen(data, modSize, RATE_DEV, 2, &p), MUSIC_OK); + TEST_TRUE(p && xmpLength(p) > 0); + xmpClose(p); } diff --git a/src/test/music/playerTest.c b/src/test/music/playerTest.c index 5373eb1c..60880034 100644 --- a/src/test/music/playerTest.c +++ b/src/test/music/playerTest.c @@ -300,6 +300,7 @@ static void caseOpenAndFolder(void) { static void casePauseSeekVolume(void) { tCase("player: pause, resume, seek, volume"); tWidgetClick(tFind(sWin, "btnPlay")); + fixPumpSettled(); TEST_STR_EQ(text("btnPlay"), PLAY_CAPTION); tick(1); TEST_EQ(heard(), 0); @@ -481,7 +482,7 @@ static int32_t launch(const char *args) { fixPumpShell(1); } - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); sWin = fixWindowForApp(sId); return sId; } @@ -536,7 +537,7 @@ static void menu(const char *caption) { ctx->currentAppId = sWin->appId; sWin->onMenu(sWin, id); ctx->currentAppId = saved; - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); } @@ -549,7 +550,7 @@ static bool menuChecked(const char *caption) { static void pressKey(const char *widget, int32_t key) { wgtSetFocused(tFind(sWin, widget)); tWidgetKey(sWin, key, 0); - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); } diff --git a/src/test/music/recorderTest.c b/src/test/music/recorderTest.c index ae9eae4f..8992e87a 100644 --- a/src/test/music/recorderTest.c +++ b/src/test/music/recorderTest.c @@ -305,10 +305,12 @@ static void caseRecordPlaySave(void) { TEST_STR_EQ(text("lblFormat"), RADIO_FORMAT); TEST_TRUE(!tFind(sWin, "btnRecord")->enabled); TEST_TRUE(tFind(sWin, "btnStop")->enabled); + // The meter shows the level recorded since the tick before pushBlocks(REC_BLOCKS); - tick(2); - TEST_STR_EQ(text("lblTime"), "0:02.0"); + tick(1); TEST_EQ(wgtProgressBarGetValue(tFind(sWin, "prgLevel")), (REC_LEVEL - U8_MIDDLE) * U8_SCALE * PERCENT / INT16_MAX); + tick(1); + TEST_STR_EQ(text("lblTime"), "0:02.0"); click("btnStop"); TEST_STR_EQ(sLastEvent, "pcm recordstop"); @@ -372,7 +374,7 @@ static void caseStart(void) { static void click(const char *widget) { tWidgetClick(tFind(sWin, widget)); - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); } @@ -454,7 +456,7 @@ static int32_t launch(void) { fixPumpShell(1); } - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); sWin = fixWindowForApp(sId); return sId; } @@ -484,7 +486,7 @@ static void menu(const char *caption) { ctx->currentAppId = sWin->appId; sWin->onMenu(sWin, id); ctx->currentAppId = saved; - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); } @@ -543,13 +545,13 @@ static const char *text(const char *widget) { // n ticks of TICK_MS: the clock (the program's timer), the mixer (into -// sOut, the last tick kept), the shell (the library's idle time, the -// program). +// sOut, the last tick kept), the shell (the library's idle time) and the +// program, on its own task, until it has handled what the tick brought. static void tick(int32_t n) { for (int32_t i = 0; i < n; i++) { tAdvanceClock(TICK_MS); audioMixRender(sOut, TICK_FRAMES); - fixPumpShell(1); + fixPumpSteady(); } } diff --git a/src/test/net/Makefile b/src/test/net/Makefile index e709fa01..ec868dd6 100644 --- a/src/test/net/Makefile +++ b/src/test/net/Makefile @@ -43,6 +43,7 @@ include ../dvxtest/dvxtest.mk .DEFAULT_GOAL := all NETDIR = $(DVXTEST_ROOT)/src/libs/kpunch/dvxnet +SEC_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/crypto LWIP_DIR = $(NETDIR)/thirdparty/lwip/src BINDIR = $(DVXTEST_BINDIR)/net NET_TEST = $(BINDIR)/netTest @@ -52,14 +53,17 @@ NET_SRCS = $(filter-out $(NETDIR)/netDos.c $(NETDIR)/netOrder.c $(NETDIR)/pktD LWIP_SRCS = $(wildcard $(LWIP_DIR)/core/*.c) $(wildcard $(LWIP_DIR)/core/ipv4/*.c) $(LWIP_DIR)/netif/ethernet.c NET_OBJS = $(patsubst $(NETDIR)/%.c,$(BINDIR)/net/%.o,$(NET_SRCS)) LWIP_OBJS = $(patsubst $(LWIP_DIR)/%.c,$(BINDIR)/lwip/%.o,$(LWIP_SRCS)) +# The security library's generator (netStackRand) +SEC_OBJS = $(BINDIR)/sec/security.o $(BINDIR)/sec/secHash.o HOST_OBJS = $(BINDIR)/hostFd.o $(BINDIR)/fakeDrv.o LIBS_A = $(DVXTEST_ARCHIVES) $(DVXTEST_TASKS_A) $(DVXTEST_BASRT_A) # Standard names renamed; plain (non-GNU, unfortified) prototypes so the -# library's definitions match the C library's declarations. +# library's definitions match the C library's declarations. lwIP counts +# its pools' use (NET_POOL_STATS, port/lwipopts.h) for the pools case. NET_INCS = -I. -I$(NETDIR) -I$(NETDIR)/port -I$(LWIP_DIR)/include -I$(DVXTEST_ROOT)/src/libs/kpunch/dvxshell -NET_CFLAGS = $(DVXTEST_CFLAGS) $(NET_INCS) -I$(DVXTEST_BASICDIR) -DBASIC_INCLUDE_DIR='"$(DVXTEST_ROOT)/src/include/basic"' -U_GNU_SOURCE -D_DEFAULT_SOURCE -U_FORTIFY_SOURCE -include netHostNames.h -DRV_CFLAGS = $(DVXTEST_CFLAGS) $(NET_INCS) +NET_CFLAGS = $(DVXTEST_CFLAGS) -DNET_POOL_STATS $(NET_INCS) -I$(DVXTEST_BASICDIR) -DBASIC_INCLUDE_DIR='"$(DVXTEST_ROOT)/src/include/basic"' -U_GNU_SOURCE -D_DEFAULT_SOURCE -U_FORTIFY_SOURCE -include netHostNames.h +DRV_CFLAGS = $(DVXTEST_CFLAGS) -DNET_POOL_STATS $(NET_INCS) .PHONY: all run clean @@ -73,6 +77,10 @@ $(BINDIR)/lwip/%.o: $(LWIP_DIR)/%.c $(DVX_MAKEFILES) @mkdir -p $(dir $@) $(HOSTCC) $(DRV_CFLAGS) -c -o $@ $< +$(BINDIR)/sec/%.o: $(SEC_DIR)/%.c $(DVX_MAKEFILES) + @mkdir -p $(dir $@) + $(HOSTCC) $(DRV_CFLAGS) -c -o $@ $< + $(BINDIR)/hostFd.o: hostFd.c $(DVX_MAKEFILES) netHostNames.h @mkdir -p $(BINDIR) $(HOSTCC) $(NET_CFLAGS) -c -o $@ $< @@ -81,11 +89,11 @@ $(BINDIR)/fakeDrv.o: fakeDrv.c $(DVX_MAKEFILES) @mkdir -p $(BINDIR) $(HOSTCC) $(DRV_CFLAGS) -c -o $@ $< -$(NET_TEST): netTest.c $(NET_OBJS) $(LWIP_OBJS) $(HOST_OBJS) $(DVX_MAKEFILES) $(LIBS_A) | dvxtest-libs +$(NET_TEST): netTest.c $(NET_OBJS) $(LWIP_OBJS) $(SEC_OBJS) $(HOST_OBJS) $(DVX_MAKEFILES) $(LIBS_A) | dvxtest-libs @mkdir -p $(BINDIR) - $(HOSTCC) $(NET_CFLAGS) -o $@ $< $(NET_OBJS) $(LWIP_OBJS) $(HOST_OBJS) $(DVXTEST_BASRT_A) $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm + $(HOSTCC) $(NET_CFLAGS) -o $@ $< $(NET_OBJS) $(LWIP_OBJS) $(SEC_OBJS) $(HOST_OBJS) $(DVXTEST_BASRT_A) $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm --include $(NET_OBJS:.o=.d) $(LWIP_OBJS:.o=.d) $(HOST_OBJS:.o=.d) $(NET_TEST).d +-include $(NET_OBJS:.o=.d) $(LWIP_OBJS:.o=.d) $(SEC_OBJS:.o=.d) $(HOST_OBJS:.o=.d) $(NET_TEST).d run: $(NET_TEST) cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1 ./netTest diff --git a/src/test/net/netTest.c b/src/test/net/netTest.c index ce8593b6..8e299c31 100644 --- a/src/test/net/netTest.c +++ b/src/test/net/netTest.c @@ -35,6 +35,8 @@ #include "compiler/compiler.h" #include "runtime/vm.h" #include "formrt/formrt.h" +#include "lwip/memp.h" +#include "lwip/stats.h" #include #include @@ -63,7 +65,8 @@ #define PORT_CLOSED 7099 #define BAS_BAD_SOCKET 999 #define BAS_ENDPOINT_TEXT 32 -#define BAS_PROGRAM_MAX 256 +#define BAS_PROGRAM_MAX 512 +#define BAS_STREAM_PROBE 16 // netBasStreamRead buffer // The simulated network: this process is the DHCP client, the child is // the "other computer" at PEER_ADDR (DHCP, DNS and echo server). @@ -127,6 +130,11 @@ #define USEC_PER_MS 1000 #define TEST_APP_ID 7 #define BURST_DATAGRAMS 40 +// Sockets whose full datagram queues are more than the receive pool +#define STARVE_SOCKETS (PBUF_POOL_SIZE / PROTO_DGRAM_QUEUE + 1) +#define STARVE_PORT PORT_A +#define STARVE_TEXT "starved" +#define STARVE_LEN ((ssize_t)sizeof(STARVE_TEXT) - 1) #define BURST_BYTES 600 #define PEER_BURST_PORT 9000 #define BYTE_MASK 0xFF @@ -146,6 +154,12 @@ typedef struct { static const uint8_t sClientMac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x15 }; static const uint8_t sPeerMac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x02 }; +// lwIP's pool names, in memp_t order +static const char *sPoolNames[MEMP_MAX] = { +#define LWIP_MEMPOOL(name, num, size, desc) desc, +#include "lwip/priv/memp_std.h" +}; + // ============================================================ // Prototypes @@ -153,6 +167,7 @@ static const uint8_t sPeerMac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x02 }; static void basOut(void *ctx, const char *text, bool newline); static void caseBasic(void); +static void caseBasicStart(void); static void caseBulk(void); static void caseConfig(void); static void caseConfigBasic(void); @@ -160,6 +175,7 @@ static void caseErrors(void); static void caseInet(void); static void caseNonBlocking(void); static void caseOptions(void); +static void casePools(void); static void caseReclaim(void); static void caseRefused(void); static void caseRing(void); @@ -186,6 +202,10 @@ int main(void); // The BASIC functions (netBas.c), over the same sockets. const char *HostLookup(const char *name); +void HostLookupClose(int32_t lookup); +int32_t HostLookupPoll(int32_t lookup); +const char *HostLookupResult(int32_t lookup); +int32_t HostLookupStart(const char *name); const char *HostName(const char *address); int32_t NetConfigChanged(int32_t config); void NetConfigFree(int32_t config); @@ -196,6 +216,7 @@ int32_t NetConfigSet(int32_t config, const char *field, const char *value); int32_t NetConfigStore(int32_t config, int32_t prefs); const char *NetError(void); int32_t NetRestart(void); +int32_t NetState(void); const char *NetStatus(const char *field); void NetStop(void); int32_t NetVector(int32_t index); @@ -204,12 +225,16 @@ int32_t TcpAccept(int32_t handle); void TcpClose(int32_t handle); int32_t TcpConnect(const char *host, int32_t port); int32_t TcpConnected(int32_t handle); +int32_t TcpConnectPoll(int32_t handle); +int32_t TcpConnectStart(const char *host, int32_t port); int32_t TcpListen(int32_t port); int32_t TcpLocalPort(int32_t handle); +const uint8_t *TcpPeek(int32_t handle, int32_t *outLen); const char *TcpPeer(int32_t handle); int32_t TcpPending(int32_t handle); const uint8_t *TcpRecv(int32_t handle, int32_t *outLen); int32_t TcpSend(int32_t handle, const uint8_t *data, int32_t len); +int32_t TcpSendSome(int32_t handle, const uint8_t *data, int32_t len); int32_t TcpSetKeepAlive(int32_t handle, int32_t on); int32_t TcpSetNoDelay(int32_t handle, int32_t on); int32_t TcpShutdown(int32_t handle); @@ -291,9 +316,29 @@ static void caseBasic(void) { netPoll(); } - snprintf(program, sizeof(program), "DIM x AS STRING\nx = TcpRecv$(%ld)\nPRINT LEN(x); ASC(MID$(x, 2, 1)); RIGHT$(x, 1)\n", (long)conn); + // TcpPeek$ shows what waits and leaves it for TcpRecv$ + snprintf(program, sizeof(program), "DIM x AS STRING\nx = TcpPeek$(%ld)\nPRINT LEN(x); TcpPending(%ld)\nx = TcpRecv$(%ld)\nPRINT LEN(x); ASC(MID$(x, 2, 1)); RIGHT$(x, 1)\n", (long)conn, (long)conn, (long)conn); TEST_TRUE(runBasic(program, out, sizeof(out))); - TEST_STR_EQ(out, "3 0 b\n"); + TEST_STR_EQ(out, "3 3 \n3 0 b\n"); + bytes = TcpPeek(conn, &len); + TEST_EQ(len, 0); + + // The stream another library runs over a connection (CommOpenTcp): + // nothing waiting is 0, what was sent arrives + { + uint8_t got[BAS_STREAM_PROBE]; + + TEST_EQ(netBasStreamRead(conn, got, (int32_t)sizeof(got)), 0); + TEST_EQ(netBasStreamWrite(client, (const uint8_t *)"xy", 2), 2); + start = nowMs(); + + while (TcpPending(conn) < 2 && nowMs() - start < WAIT_MS) { + netPoll(); + } + + TEST_EQ(netBasStreamRead(conn, got, (int32_t)sizeof(got)), 2); + TEST_TRUE(memcmp(got, "xy", 2) == 0); + } // Endpoints, options, half-close (round 2 additions) TEST_EQ(TcpLocalPort(server), PORT_C); @@ -318,6 +363,14 @@ static void caseBasic(void) { } TEST_EQ(TcpConnected(conn), 0); + + // A stream whose other side has closed reads as gone + { + uint8_t got[BAS_STREAM_PROBE]; + + TEST_EQ(netBasStreamRead(conn, got, (int32_t)sizeof(got)), -1); + } + TEST_EQ(TcpSend(conn, (const uint8_t *)"late", 4), 4); start = nowMs(); @@ -361,6 +414,101 @@ static void caseBasic(void) { } +// The BASIC lookups and connections that do not wait (HostLookupStart, +// TcpConnectStart, TcpSendSome), from C and from a program. +static void caseBasicStart(void) { + int32_t server = TcpListen(PORT_D); + int32_t conn = 0; + int32_t lookup; + int32_t client; + int32_t rc; + uint32_t start; + char program[BAS_PROGRAM_MAX]; + char out[BAS_OUT_MAX]; + + tCase("basic, not waiting"); + TEST_TRUE(server > 0); + lookup = HostLookupStart("localhost"); + TEST_TRUE(lookup > 0); + start = nowMs(); + + while ((rc = HostLookupPoll(lookup)) == BasNetBusyE && nowMs() - start < WAIT_MS) { + netPoll(); + } + + TEST_EQ(rc, BasNetDoneE); + TEST_STR_EQ(HostLookupResult(lookup), "127.0.0.1"); + HostLookupClose(lookup); + TEST_EQ(HostLookupPoll(lookup), BasNetFailedE); + TEST_STR_EQ(HostLookupResult(lookup), ""); + + // Connected while polled; the handle then works as TcpConnect's + client = TcpConnectStart("localhost", PORT_D); + TEST_TRUE(client > 0); + start = nowMs(); + + while ((rc = TcpConnectPoll(client)) == BasNetBusyE && nowMs() - start < WAIT_MS) { + netPoll(); + } + + TEST_EQ(rc, BasNetDoneE); + TEST_EQ(TcpConnectPoll(client), BasNetDoneE); + start = nowMs(); + + while (conn == 0 && nowMs() - start < WAIT_MS) { + netPoll(); + conn = TcpAccept(server); + } + + TEST_TRUE(conn > 0); + TEST_EQ(TcpSendSome(client, (const uint8_t *)"some", 4), 4); + start = nowMs(); + + while (TcpPending(conn) < 4 && nowMs() - start < WAIT_MS) { + netPoll(); + } + + TEST_STR_EQ(recvText(conn), "some"); + TcpClose(conn); + TcpClose(client); + + // Refused: failed, saying why; closed while still connecting + client = TcpConnectStart("127.0.0.1", PORT_CLOSED); + TEST_TRUE(client > 0); + start = nowMs(); + + while ((rc = TcpConnectPoll(client)) == BasNetBusyE && nowMs() - start < WAIT_MS) { + netPoll(); + } + + TEST_EQ(rc, BasNetFailedE); + TEST_STR_EQ(NetError(), netErrorText(ECONNREFUSED)); + TcpClose(client); + TEST_EQ(TcpConnectStart("localhost", 0), 0); + client = TcpConnectStart("localhost", PORT_D); + TEST_TRUE(client > 0); + TcpClose(client); + + // A program waits for both from loops of its own + snprintf(program, sizeof(program), + "DIM h AS LONG\n" + "h = HostLookupStart(\"localhost\")\n" + "DO WHILE HostLookupPoll(h) = NET_BUSY\n" + "LOOP\n" + "PRINT HostLookupResult$(h)\n" + "HostLookupClose h\n" + "h = TcpConnectStart(\"127.0.0.1\", %d)\n" + "DO WHILE TcpConnectPoll(h) = NET_BUSY\n" + "LOOP\n" + "PRINT TcpConnectPoll(h) = NET_DONE; TcpSendSome(h, \"xy\")\n" + "TcpClose h\n", PORT_D); + TEST_TRUE(runBasic(program, out, sizeof(out))); + TEST_STR_EQ(out, "127.0.0.1\nTrue 2 \n"); + TcpClose(server); + TEST_EQ(netSockCount(), 0); +} + + // A quarter megabyte each way through one loopback connection, both // ends non-blocking, driven by poll(). static void caseBulk(void) { @@ -584,6 +732,7 @@ static void caseConfigBasic(void) { // Status of a stopped network netStop(); + TEST_EQ(NetState(), NetStoppedE); TEST_STR_EQ(NetStatus("message"), netStateText(NetStoppedE)); TEST_STR_EQ(NetStatus("up"), "0"); TEST_STR_EQ(NetStatus("rxdropped"), "0"); @@ -606,9 +755,11 @@ static void caseConfigBasic(void) { "PRINT PrefsGet$(p, \"network\", \"hostname\", \"?\")\n" "PRINT NetVectorCount(); NetVector(1)\n" "PRINT NetStatus$(\"up\")\n" + "PRINT NetState() = NET_STATE_STOPPED; NET_STATE_NO_DRIVER; NET_STATE_CONFIGURING; NET_STATE_UP\n" "NetConfigFree c\n" "PrefsClose p\n", out, sizeof(out))); - TEST_STR_EQ(out, "DVX0 \n-1 -1 \n-1 True \n-1 -1 \nbasic-pc\n34 96 \n0\n"); + TEST_STR_EQ(out, "DVX0 \n-1 -1 \n-1 True \n-1 -1 \nbasic-pc\n34 96 \n0\nTrue 1 2 3 \n"); + TEST_TRUE(NetNoDriverE == 1 && NetConfiguringE == 2 && NetUpE == 3); } // An app's handles are freed when it ends @@ -934,6 +1085,23 @@ static void caseOptions(void) { // Sockets are charged to the app that made them and closed when it ends. +// lwIP's heap and pools over the whole suite (built with NET_POOL_STATS): +// no allocation failed, and each peak is printed beside its size, the +// measure lwipopts.h's sizes are tuned against. +static void casePools(void) { + tCase("pools"); + printf(" heap: peak %lu of %lu bytes\n", (unsigned long)lwip_stats.mem.max, (unsigned long)lwip_stats.mem.avail); + tCheckEq(__FILE__, __LINE__, "heap failures", (int64_t)lwip_stats.mem.err, 0); + + for (int32_t i = 0; i < MEMP_MAX; i++) { + const struct stats_mem *s = lwip_stats.memp[i]; + + printf(" %s: peak %lu of %lu\n", sPoolNames[i], (unsigned long)s->max, (unsigned long)s->avail); + tCheckEq(__FILE__, __LINE__, sPoolNames[i], (int64_t)s->err, 0); + } +} + + static void caseReclaim(void) { int32_t app = TEST_APP_ID; int32_t *saved = dvxMemAppIdPtr; @@ -1195,6 +1363,9 @@ static void caseWire(void) { char buf[BURST_BYTES]; uint32_t start; int32_t burst = 0; + NetLookupT lk; + uint32_t addr = 0; + int32_t rc; tCase("wire"); TEST_EQ(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair), 0); @@ -1214,7 +1385,23 @@ static void caseWire(void) { TEST_EQ(netStart(), NET_OK); // Right after the start DHCP is still running: a lookup (which needs - // the DNS server DHCP hands out) waits for the address by itself. + // the DNS server DHCP hands out) waits for the address too, a look at + // a time, never inside one + buf[0] = '\0'; + netLookupStart(&lk, ECHO_NAME); + TEST_EQ(netLookupPoll(&lk, &addr), NET_LOOKUP_BUSY); + start = nowMs(); + + while ((rc = netLookupPoll(&lk, &addr)) == NET_LOOKUP_BUSY && nowMs() - start < WAIT_MS) { + usleep(USEC_PER_MS); + } + + TEST_EQ(rc, 0); + inet_ntop(AF_INET, &addr, buf, sizeof(buf)); + TEST_STR_EQ(buf, PEER_ADDR); + TEST_TRUE(waitUp()); + + // getaddrinfo waits the same way; a lookup given up says so buf[0] = '\0'; if (TEST_EQ(getaddrinfo(ECHO_NAME, "echo", NULL, &res), 0)) { @@ -1223,7 +1410,11 @@ static void caseWire(void) { } TEST_STR_EQ(buf, PEER_ADDR); - TEST_TRUE(waitUp()); + netLookupStart(&lk, "other.test"); + netLookupCancel(&lk); + TEST_EQ(netLookupPoll(&lk, &addr), EAI_AGAIN); + netLookupStart(&lk, ""); + TEST_EQ(netLookupPoll(&lk, &addr), EAI_NONAME); netGetStatus(&st); TEST_EQ(st.state, NetUpE); @@ -1240,6 +1431,14 @@ static void caseWire(void) { // A name the peer's DNS server does not know TEST_EQ(getaddrinfo("nosuch.test", NULL, NULL, &res), EAI_NONAME); + netLookupStart(&lk, "nosuch.test"); + start = nowMs(); + + while ((rc = netLookupPoll(&lk, &addr)) == NET_LOOKUP_BUSY && nowMs() - start < WAIT_MS) { + usleep(USEC_PER_MS); + } + + TEST_EQ(rc, EAI_NONAME); he = gethostbyname("nosuch.test"); TEST_PTR_EQ(he, NULL); TEST_EQ(h_errno, HOST_NOT_FOUND); @@ -1299,6 +1498,70 @@ static void caseWire(void) { TEST_TRUE(st.rxDropped > 0); close(fd); + // Unread datagrams that fill the receive pool: frames beyond it are + // dropped, a TCP reply among them too, and the reply still arrives + // once the datagrams are read (the peer sends it again). The pool's + // peak and failure count go back to what they were for casePools. + { + struct stats_mem *pool = lwip_stats.memp[MEMP_PBUF_POOL]; + struct stats_mem saved = *pool; + int starve[STARVE_SOCKETS]; + int32_t held = 0; + ssize_t echoed = 0; + + fd = connectTo(PEER_ADDR, ECHO_PORT); + TEST_TRUE(fd >= 0); + makeAddr(&sa, PEER_ADDR, PEER_BURST_PORT); + + for (int32_t i = 0; i < STARVE_SOCKETS; i++) { + starve[i] = udpOn((uint16_t)(STARVE_PORT + i)); + TEST_EQ(sendto(starve[i], "go", 2, 0, (struct sockaddr *)&sa, sizeof(sa)), 2); + start = nowMs(); + + while (nowMs() - start < SHORT_TIMEOUT_MS * 2) { + netPoll(); + } + } + + TEST_TRUE(pool->err > saved.err); + TEST_EQ(pool->used, pool->avail); + TEST_EQ(send(fd, STARVE_TEXT, STARVE_LEN, 0), STARVE_LEN); + start = nowMs(); + + while (nowMs() - start < SHORT_TIMEOUT_MS) { + netPoll(); + } + + TEST_EQ(recv(fd, buf, sizeof(buf), MSG_DONTWAIT), -1); + + for (int32_t i = 0; i < STARVE_SOCKETS; i++) { + while (recv(starve[i], buf, sizeof(buf), MSG_DONTWAIT) > 0) { + held++; + } + + close(starve[i]); + } + + TEST_TRUE(held > 0 && held <= PBUF_POOL_SIZE); + start = nowMs(); + + while (echoed < STARVE_LEN && nowMs() - start < WAIT_MS) { + ssize_t n = recv(fd, buf + echoed, (size_t)(STARVE_LEN - echoed), MSG_DONTWAIT); + + if (n > 0) { + echoed += n; + } else { + netPoll(); + } + } + + TEST_EQ(echoed, STARVE_LEN); + TEST_TRUE(memcmp(buf, STARVE_TEXT, (size_t)STARVE_LEN) == 0); + close(fd); + pool->err = saved.err; + pool->max = saved.max; + } + // Restart with a static address (no DHCP). { PrefsHandleT *h = prefsLoad(platformDvxIniPath()); @@ -1343,6 +1606,7 @@ static void caseWire(void) { TEST_EQ(NetRestart(), -1); TEST_STR_EQ(NetStatus("up"), "-1"); + TEST_EQ(NetState(), NetUpE); TEST_STR_EQ(NetStatus("link"), "-1"); TEST_STR_EQ(NetStatus("dhcp"), "0"); TEST_STR_EQ(NetStatus("address"), "10.0.2.98"); @@ -1804,7 +2068,9 @@ int main(void) { caseErrors(); caseReclaim(); caseBasic(); + caseBasicStart(); caseWire(); + casePools(); return tSuiteEnd(); } diff --git a/src/test/pkg/Makefile b/src/test/pkg/Makefile index 67879085..548b156f 100644 --- a/src/test/pkg/Makefile +++ b/src/test/pkg/Makefile @@ -22,7 +22,7 @@ # Package library suite: pkgTest against the package library's sources # (src/libs/kpunch/pkg), built for the host with miniz, Monocypher and the -# serial library's SHA-256 as pkg.lib has them, and the package server's +# crypto library's SHA-256 as pkg.lib has them, and the package server's # client (pkgnet, over http and json; pkgTestNet.c runs a server of its # own on the host's sockets); softwareTest and # pkgbuildTest run the Software and Package Builder programs (compiled @@ -35,8 +35,8 @@ include ../dvxtest/dvxtest.mk PKG_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/pkg MINIZ_DIR = $(PKG_DIR)/thirdparty/miniz -MONO_DIR = $(PKG_DIR)/thirdparty/monocypher/src -SEC_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/serial/security +MONO_DIR = $(SEC_DIR)/thirdparty/monocypher/src +SEC_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/crypto NET_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/pkgnet HTTP_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/http JSON_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/json @@ -51,11 +51,11 @@ FILEOPS_OBJS = $(patsubst $(FILEOPS_DIR)/%.c,$(BINDIR)/fileops/%.o,$(wildcard $( SHELL_OBJS = $(patsubst $(SHELLDIR)/%.c,$(BINDIR)/shell/%.o,$(wildcard $(SHELLDIR)/*.c)) SHFIX_OBJ = $(BINDIR)/shellFixture.o SW_CFLAGS = $(CFLAGS) -fno-omit-frame-pointer -I$(SHELLDIR) -I$(SHELLTESTDIR) -DDVXTEST_APPS_DIR='"$(APPS_SO_DIR)"' -DDVXTEST_FIXTURES_DIR='"$(BINDIR)"' -DSHELLTEST_BASSTUB_SO='"$(DVXTEST_BASSTUB_SO)"' -DSHELLTEST_BASCOMP='"$(DVXTEST_BASCOMP)"' -PKG_SRCS = $(addprefix $(PKG_DIR)/,pkgBas.c pkgCli.c pkgDb.c pkgDpk.c pkgFormat.c pkgIni.c pkgManifest.c pkgOp.c pkgPlan.c pkgSign.c pkgTrust.c pkgZip.c pkgZipWrite.c) $(MINIZ_DIR)/miniz.c $(MONO_DIR)/monocypher.c $(MONO_DIR)/optional/monocypher-ed25519.c $(SEC_DIR)/secHash.c $(SEC_DIR)/security.c $(NET_DIR)/pkgNet.c $(NET_DIR)/pkgNetBas.c $(HTTP_DIR)/http.c $(HTTP_DIR)/httpBas.c $(JSON_DIR)/json.c $(JSON_DIR)/jsonBas.c +PKG_SRCS = $(addprefix $(PKG_DIR)/,pkgBas.c pkgCli.c pkgDb.c pkgDpk.c pkgFormat.c pkgIni.c pkgManifest.c pkgOp.c pkgPlan.c pkgSign.c pkgSignMake.c pkgTrust.c pkgZip.c pkgZipJob.c pkgZipWrite.c) $(MINIZ_DIR)/miniz.c $(MONO_DIR)/monocypher.c $(MONO_DIR)/optional/monocypher-ed25519.c $(SEC_DIR)/secHash.c $(SEC_DIR)/secSign.c $(SEC_DIR)/security.c $(NET_DIR)/pkgNet.c $(NET_DIR)/pkgNetBas.c $(HTTP_DIR)/http.c $(HTTP_DIR)/httpBas.c $(JSON_DIR)/json.c $(JSON_DIR)/jsonBas.c $(DVXTEST_DIR)/hostLookup.c PKG_OBJS = $(patsubst %.c,$(BINDIR)/obj/%.o,$(notdir $(PKG_SRCS))) CFLAGS = $(DVXTEST_CFLAGS) -I$(PKG_DIR) -I$(NET_DIR) -I$(HTTP_DIR) -I$(JSON_DIR) -I$(MINIZ_DIR) -I$(MONO_DIR) -I$(DVXTEST_ROOT)/src/libs/kpunch -I$(DVXTEST_BASICDIR) -DBASIC_INCLUDE_DIR='"$(DVXTEST_ROOT)/src/include/basic"' $(PKG_MINIZ_FLAGS) -vpath %.c $(PKG_DIR) $(MINIZ_DIR) $(MONO_DIR) $(MONO_DIR)/optional $(SEC_DIR) $(NET_DIR) $(HTTP_DIR) $(JSON_DIR) +vpath %.c $(PKG_DIR) $(MINIZ_DIR) $(MONO_DIR) $(MONO_DIR)/optional $(SEC_DIR) $(NET_DIR) $(HTTP_DIR) $(JSON_DIR) $(DVXTEST_DIR) .PHONY: all run clean host-tools @@ -65,9 +65,9 @@ $(BINDIR)/obj/%.o: %.c $(DVX_MAKEFILES) @mkdir -p $(dir $@) $(HOSTCC) $(CFLAGS) -c -o $@ $< -$(TEST_BIN): pkgTest.c pkgTestBas.c pkgTestEngine.c pkgTestNet.c pkgServe.c pkgServe.h $(PKG_OBJS) $(DVX_MAKEFILES) $(DVXTEST_ARCHIVES) $(DVXTEST_TASKS_A) $(DVXTEST_BASRT_A) | dvxtest-libs +$(TEST_BIN): pkgTest.c pkgTestBas.c pkgTestEngine.c pkgTestNet.c pkgServe.c pkgServe.h $(DVXTEST_DIR)/lookupHook.c $(PKG_OBJS) $(DVX_MAKEFILES) $(DVXTEST_ARCHIVES) $(DVXTEST_TASKS_A) $(DVXTEST_BASRT_A) | dvxtest-libs @mkdir -p $(BINDIR) - $(HOSTCC) $(CFLAGS) -o $@ pkgTest.c pkgTestBas.c pkgTestEngine.c pkgTestNet.c pkgServe.c $(PKG_OBJS) $(DVXTEST_BASRT_A) $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm + $(HOSTCC) $(CFLAGS) -o $@ pkgTest.c pkgTestBas.c pkgTestEngine.c pkgTestNet.c pkgServe.c $(DVXTEST_DIR)/lookupHook.c $(PKG_OBJS) $(DVXTEST_BASRT_A) $(DVXTEST_LDFLAGS) -Wl,--wrap=getaddrinfo -Wl,--whole-archive $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm $(BINDIR)/fileops/%.o: $(FILEOPS_DIR)/%.c $(DVX_MAKEFILES) @mkdir -p $(dir $@) diff --git a/src/test/pkg/appDrive.c b/src/test/pkg/appDrive.c index 6a3e1ff3..856f9de0 100644 --- a/src/test/pkg/appDrive.c +++ b/src/test/pkg/appDrive.c @@ -68,7 +68,8 @@ int32_t driveLaunch(const char *appPath, const char *arg, const char *title) { fixPumpShell(1); } - fixPumpShell(SETTLE_FRAMES); + // What its start does (a file it was given installed, say) finished + fixPumpSettled(); return id; } @@ -82,7 +83,7 @@ void driveMenu(WindowT *win, const char *caption) { ctx->currentAppId = win->appId; win->onMenu(win, id); ctx->currentAppId = saved; - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); } @@ -115,7 +116,8 @@ void driveScriptOn(DriveScriptT *s, const char *answer, const char *typeName) { // Each idle moment of a modal: the clock moves on (timers run), the // modal's title is noted once, a file name is typed when asked, and the -// answer (else OK, else Yes) is pressed when the modal has it. +// answer (else OK, else Yes) is pressed when the modal has it, once a +// showing. static void scriptRun(AppContextT *ctx, WindowT *modal, void *userCtx) { DriveScriptT *s = (DriveScriptT *)userCtx; char mark[DRIVE_TITLES_MAX]; @@ -123,7 +125,11 @@ static void scriptRun(AppContextT *ctx, WindowT *modal, void *userCtx) { (void)ctx; hostClockAdvance(DRIVE_FRAME_MS); - if (!modal) { + if (!modal || modal->id != s->answeredId) { + s->answeredId = 0; + } + + if (!modal || modal->id == s->answeredId) { return; } @@ -144,5 +150,6 @@ static void scriptRun(AppContextT *ctx, WindowT *modal, void *userCtx) { if (tDialogClickButton(modal, s->answer) || tDialogClickButton(modal, "OK") || tDialogClickButton(modal, "Yes")) { s->answered++; + s->answeredId = modal->id; } } diff --git a/src/test/pkg/appDrive.h b/src/test/pkg/appDrive.h index d96e2e75..263c2452 100644 --- a/src/test/pkg/appDrive.h +++ b/src/test/pkg/appDrive.h @@ -41,6 +41,7 @@ typedef struct { const char *answer; const char *typeName; int32_t answered; + int32_t answeredId; // the window answered last, until it closes (its program closes it on its own task) char titles[DRIVE_TITLES_MAX]; } DriveScriptT; @@ -51,7 +52,8 @@ bool driveExists(const char *path); // id. int32_t driveLaunch(const char *appPath, const char *arg, const char *title); -// Choose a menu command of win by its caption, as the window's program. +// Choose a menu command of win by its caption, as the window's program, +// and wait until the program is done with it (fixPumpSettled). void driveMenu(WindowT *win, const char *caption); // DVX itself recorded as installed in the current folder. diff --git a/src/test/pkg/pkgServe.c b/src/test/pkg/pkgServe.c index 4cb40c00..0d32f6c8 100644 --- a/src/test/pkg/pkgServe.c +++ b/src/test/pkg/pkgServe.c @@ -28,7 +28,7 @@ #include "json.h" #include "dvxPlat.h" #include "dvxTypes.h" -#include "serial/security/secHash.h" +#include "crypto/secHash.h" #include "stb_ds_wrap.h" #include diff --git a/src/test/pkg/pkgTest.c b/src/test/pkg/pkgTest.c index 9b680bf4..fec7a887 100644 --- a/src/test/pkg/pkgTest.c +++ b/src/test/pkg/pkgTest.c @@ -46,7 +46,6 @@ #define PATH_BUF 512 #define BIG_SIZE 200000 // a file big enough for many reader steps #define SMALL_STEP 100 // pkgZipData work per call in the stepping test -#define INFLATE_WINDOW 32768 // a deflated entry comes out at most a window a step #define SEED_BYTE 0x5A // the fake random source's byte #define ARGV_MAX 8 @@ -815,12 +814,20 @@ static void caseZip(void) { uint32_t len; } sink = { back, 0 }; - while ((step = pkgZipData(z, memWrite, &sink, SMALL_STEP)) == PkgZipMoreE) { - steps++; - } + uint32_t most = 0; + do { + uint32_t before = sink.len; + + step = pkgZipData(z, memWrite, &sink, SMALL_STEP); + most = sink.len - before > most ? sink.len - before : most; + steps++; + } while (step == PkgZipMoreE); + + // No step hands out more than its work, though an inflate makes more TEST_EQ(step, PkgZipFileDoneE); - TEST_TRUE(steps >= BIG_SIZE / INFLATE_WINDOW - 1); + TEST_TRUE(most <= SMALL_STEP); + TEST_TRUE(steps >= BIG_SIZE / SMALL_STEP); TEST_EQ(sink.len, BIG_SIZE); TEST_TRUE(memcmp(back, big, BIG_SIZE) == 0); TEST_EQ(pkgZipNext(z, &e), PkgZipEntryE); diff --git a/src/test/pkg/pkgTestBas.c b/src/test/pkg/pkgTestBas.c index 6a5af735..7a911e55 100644 --- a/src/test/pkg/pkgTestBas.c +++ b/src/test/pkg/pkgTestBas.c @@ -49,7 +49,7 @@ #define BT_RUN_MAX 100 // RunFor calls before giving up #define BT_SEED 0x44 // the test key's seed bytes #define BT_BAD 99 // a handle or number never given -#define BT_SRC_MAX 16384 +#define BT_SRC_MAX 32768 #define BT_CAPTURE_MAX 256 #define BT_ABI "abi = " DVX_ABI_STR(DVX_ABI_VERSION) "\n" #define BT_TEMPLATE "[package]\nid = acme.chess\nname = Chess\nversion = 2.0\nkind = app\n[builder]\nfolder = here\n" // no format or abi: the build gives them @@ -63,6 +63,7 @@ static bool btExists(const char *path); static void btLive(void); static void btMake(const char *dir, const char *text, const char *file, const char *body, const char *out, const PkgSecretT *secret); static void btMaking(void); +static int32_t btOffer(int32_t s, const char *path); static int32_t btRun(int32_t s); void testBasBinding(void); @@ -102,10 +103,10 @@ static void btLive(void) { "s = PkgOpen()\n" "PRINT PkgField$(s, \"kpunch.dvx\", PKG_FIELD_NAME); \"/\"; PkgField$(s, \"kpunch.dvx\", PKG_FIELD_KIND)\n" "IF PkgPlan(s, \"5\") = 0 THEN PRINT \"no plan\"\n" - "IF PkgOfferFile(s, \"nowhere.dpk\") = -1 THEN PRINT \"no file\"\n" + "IF PkgOfferFileStart(s, \"nowhere.dpk\") AND PkgRunFor(s, 0) = PKG_RUN_FAILED THEN PRINT \"no file\"\n" "PRINT PkgKindName$(0); \" \"; PkgKindFolder$(\"font\", \"acme.serif\"); \" \"; PkgDvxVersion$()\n" "IF PkgDvxAbi() > 0 AND PkgKeyInfo$(s, \"nowhere.key\") = \"\" AND PkgFolderRows$(s, \"nowhere\", \"\") = \"\" AND PkgIdValid(\"a.b\") AND NOT PkgVersionValid(\"x\") AND PkgIdPart$(\"A B\") = \"ab\" THEN PRINT \"checked\"\n" - "IF PkgBuild(s, \"x.ini\", \"\", \"x.dpk\", \"\") = 0 AND PkgSign(s, \"x.dpk\", \"x.sec\", 0) = 0 AND PkgKeygen(s, \"\", \"x\") = 0 THEN PRINT \"refused\"\n" + "IF PkgBuildStart(s, \"x.ini\", \"\", \"x.dpk\", \"\") = 0 AND PkgSignStart(s, \"x.dpk\", \"x.sec\", 0) = 0 AND PkgKeygen(s, \"\", \"x\") = 0 THEN PRINT \"refused\"\n" "PkgClose s\n", bas); free(bas); @@ -199,6 +200,12 @@ static void btMaking(void) { TEST_EQ(PkgVersionValid("2"), BAS_TRUE); TEST_EQ(PkgVersionValid("1.x"), BAS_FALSE); TEST_EQ(PkgVersionValid(""), BAS_FALSE); + TEST_EQ(PkgVersionCompare("1.10.0", "1.9.2"), 1); + TEST_EQ(PkgVersionCompare("1.2", "1.2.0"), 0); + TEST_EQ(PkgVersionCompare("1.2", "1.3"), -1); + TEST_EQ(PkgVersionCompare("x", "1"), -1); + TEST_EQ(PkgVersionCompare("1", "x"), 1); + TEST_EQ(PkgVersionCompare("x", NULL), 0); TEST_STR_EQ(PkgDvxVersion(), DVX_VERSION_TEXT); TEST_EQ(PkgDvxAbi(), DVX_ABI_VERSION); @@ -235,44 +242,203 @@ static void btMaking(void) { TEST_STR_EQ(PkgKeyInfo(s, tmpl), ""); TEST_TRUE(PkgError(s)[0] != '\0'); - // Built signed: an offer with both files, signed by the new key + // Any bytes (a NUL among them) signed with the secret key and checked + // with the public one; other bytes, another key, or a key DVX does not + // trust fail + { + static const uint8_t data[] = { 'm', 's', 0, 'g' }; + char sigText[BT_PATH]; + char keyPath[BT_PATH]; + + snprintf(sigText, sizeof(sigText), "%s", PkgSignData(s, data, (int32_t)sizeof(data), path)); + TEST_TRUE(sigText[0] != '\0'); + snprintf(keyPath, sizeof(keyPath), "%s%s", base, PKG_KEY_EXT); + TEST_EQ(PkgCheckData(s, data, (int32_t)sizeof(data), sigText, keyPath), BAS_TRUE); + TEST_EQ(PkgCheckData(s, data, (int32_t)sizeof(data) - 1, sigText, keyPath), BAS_FALSE); + TEST_TRUE(strstr(PkgError(s), "was not made by") != NULL); + TEST_EQ(PkgCheckData(s, data, (int32_t)sizeof(data), sigText, ""), BAS_FALSE); + TEST_EQ(PkgCheckData(s, data, (int32_t)sizeof(data), "not a signature", keyPath), BAS_FALSE); + TEST_STR_EQ(PkgSignData(s, data, (int32_t)sizeof(data), "nowhere.sec"), ""); + TEST_TRUE(PkgError(s)[0] != '\0'); + } + + // Built signed, as a job (one at a time): an offer with both files, + // signed by the new key snprintf(out, sizeof(out), "%s/CHESS2.DPK", tTempDir()); - TEST_EQ(PkgBuild(s, tmpl, rows, out, path), BAS_TRUE); + TEST_EQ(PkgBuildStart(s, tmpl, rows, out, path), BAS_TRUE); + TEST_EQ(PkgBuildStart(s, tmpl, rows, out, path), BAS_FALSE); + TEST_EQ(PkgSignStart(s, out, path, BAS_FALSE), BAS_FALSE); + TEST_EQ(btRun(s), PkgRunDoneE); + TEST_EQ(PkgRunPercent(s), 100); TEST_STR_EQ(PkgError(s), ""); - TEST_EQ(PkgOfferFile(s, out), 0); + TEST_EQ(btOffer(s, out), 0); TEST_STR_EQ(PkgOfferFilesRows(s, 0), "APPS/ACME/CHESS/CHESS.APP\t5\nAPPS/ACME/CHESS/DATA/BOARD.DAT\t5\n"); TEST_STR_EQ(PkgOfferField(s, 0, PkgFieldVersionE), "2.0.0"); TEST_STR_EQ(PkgOfferField(s, 0, PkgFieldAbiE), DVX_ABI_STR(DVX_ABI_VERSION)); TEST_STR_EQ(PkgOfferField(s, 0, PkgFieldSignerE), id); - // Built unsigned, then signed into it and beside it + // Built unsigned, then signed into it (a job) and beside it (at once) snprintf(out, sizeof(out), "%s/CHESS3.DPK", tTempDir()); - TEST_EQ(PkgBuild(s, tmpl, rows, out, ""), BAS_TRUE); - TEST_EQ(PkgOfferFile(s, out), 0); + TEST_EQ(PkgBuildStart(s, tmpl, rows, out, ""), BAS_TRUE); + TEST_EQ(btRun(s), PkgRunDoneE); + TEST_EQ(btOffer(s, out), 0); TEST_STR_EQ(PkgOfferField(s, 0, PkgFieldSignerE), ""); - TEST_EQ(PkgSign(s, out, path, BAS_FALSE), BAS_TRUE); - TEST_EQ(PkgOfferFile(s, out), 0); + TEST_EQ(PkgSignStart(s, out, path, BAS_FALSE), BAS_TRUE); + TEST_EQ(btRun(s), PkgRunDoneE); + TEST_EQ(btOffer(s, out), 0); TEST_STR_EQ(PkgOfferField(s, 0, PkgFieldSignerE), id); - TEST_EQ(PkgSign(s, out, path, BAS_TRUE), BAS_TRUE); + TEST_EQ(PkgSignStart(s, out, path, BAS_TRUE), BAS_TRUE); + TEST_EQ(PkgRunFor(s, 0), PkgRunDoneE); snprintf(out, sizeof(out), "%s/CHESS3%s", tTempDir(), PKG_SIG_EXT); TEST_TRUE(btExists(out)); + // A plain ZIP made from the program's files, listed, and unpacked; a + // damaged one and one whose entry would leave the folder fail + { + char zipPath[BT_PATH]; + char zipRows[BT_PATH]; + char into[BT_PATH]; + char check[BT_PATH]; + int32_t len = 0; + char *data = NULL; + char *at = NULL; + + snprintf(zipPath, sizeof(zipPath), "%s/CHESS.ZIP", tTempDir()); + snprintf(zipRows, sizeof(zipRows), "CHESS.APP\t%s/CHESS.APP\nXX/BOARD.DAT\t%s/DATA/BOARD.DAT\n", stage, stage); + TEST_EQ(PkgZipCreateStart(s, zipPath, zipRows), BAS_TRUE); + TEST_EQ(PkgZipCreateStart(s, zipPath, zipRows), BAS_FALSE); + TEST_EQ(btRun(s), PkgRunDoneE); + TEST_EQ(PkgRunPercent(s), 100); + TEST_TRUE(strncmp(PkgZipRows(s), "CHESS.APP\t5\t", strlen("CHESS.APP\t5\t")) == 0); + + TEST_EQ(PkgZipListStart(s, zipPath), BAS_TRUE); + TEST_EQ(btRun(s), PkgRunDoneE); + TEST_TRUE(strstr(PkgZipRows(s), "XX/BOARD.DAT\t5\t") != NULL); + + snprintf(into, sizeof(into), "%s/unzipped", tTempDir()); + TEST_EQ(PkgZipExtractStart(s, zipPath, into), BAS_TRUE); + TEST_EQ(btRun(s), PkgRunDoneE); + snprintf(check, sizeof(check), "%s/XX/BOARD.DAT", into); + data = platformReadFile(check, &len); + TEST_TRUE(data && len == 5 && memcmp(data, "board", 5) == 0); + free(data); + + // XX -> .. in the names (both copies): unpacking it stops, nothing outside + data = platformReadFile(zipPath, &len); + TEST_TRUE(data != NULL); + + for (at = data; data && (at = memmem(at, (size_t)(len - (at - data)), "XX/", 3)) != NULL; at += 3) { + at[0] = '.'; + at[1] = '.'; + } + + snprintf(check, sizeof(check), "%s/EVIL.ZIP", tTempDir()); + TEST_TRUE(data && tWriteFile(check, data, len)); + TEST_EQ(PkgZipExtractStart(s, check, into), BAS_TRUE); + TEST_EQ(btRun(s), PkgRunFailedE); + TEST_TRUE(strstr(PkgRunMessage(s), "outside") != NULL); + snprintf(check, sizeof(check), "%s/BOARD.DAT", tTempDir()); + TEST_TRUE(!btExists(check)); + free(data); + + // A byte of a file changed: its CRC fails the list + data = platformReadFile(zipPath, &len); + at = data ? memmem(data, (size_t)len, "chess", 5) : NULL; + TEST_TRUE(at != NULL); + + if (at) { + at[0] = 'C'; + snprintf(check, sizeof(check), "%s/BAD.ZIP", tTempDir()); + TEST_TRUE(tWriteFile(check, data, len)); + TEST_EQ(PkgZipListStart(s, check), BAS_TRUE); + TEST_EQ(btRun(s), PkgRunFailedE); + TEST_TRUE(strstr(PkgRunMessage(s), "CHESS.APP") != NULL); + } + + free(data); + TEST_EQ(PkgZipListStart(s, "nowhere.zip"), BAS_FALSE); + TEST_TRUE(PkgError(s)[0] != '\0'); + } + + // Checked without installing (a job): intact, and signed by the key + // named; a key that did not sign it starts nothing; a file damaged + // inside the package fails the check + { + char dpk[BT_PATH]; + char pub[BT_PATH]; + char otherBase[BT_PATH]; + char otherKey[BT_PATH]; + char damaged[BT_PATH]; + int32_t len = 0; + char *data = NULL; + char *at = NULL; + + snprintf(dpk, sizeof(dpk), "%s/CHESS3.DPK", tTempDir()); + snprintf(pub, sizeof(pub), "%s%s", base, PKG_KEY_EXT); + TEST_EQ(PkgVerifyStart(s, dpk, ""), BAS_TRUE); + TEST_EQ(PkgVerifyStart(s, dpk, ""), BAS_FALSE); + TEST_EQ(btRun(s), PkgRunDoneE); + TEST_EQ(PkgRunPercent(s), 100); + TEST_EQ(PkgVerifyStart(s, dpk, pub), BAS_TRUE); + TEST_EQ(btRun(s), PkgRunDoneE); + snprintf(otherBase, sizeof(otherBase), "%s/OTHERPUB", tTempDir()); + TEST_EQ(PkgKeygen(s, "Other Publisher", otherBase), BAS_TRUE); + snprintf(otherKey, sizeof(otherKey), "%s%s", otherBase, PKG_KEY_EXT); + TEST_EQ(PkgVerifyStart(s, dpk, otherKey), BAS_FALSE); + TEST_TRUE(strstr(PkgError(s), "is not signed by") != NULL); + + data = platformReadFile(dpk, &len); + at = data ? memmem(data, (size_t)len, "board", 5) : NULL; + TEST_TRUE(at != NULL); + + if (at) { + at[4] = 'D'; + snprintf(damaged, sizeof(damaged), "%s/DAMAGED.DPK", tTempDir()); + TEST_TRUE(tWriteFile(damaged, data, len)); + TEST_EQ(PkgVerifyStart(s, damaged, ""), BAS_TRUE); + TEST_EQ(btRun(s), PkgRunFailedE); + TEST_TRUE(strstr(PkgRunMessage(s), "BOARD.DAT") != NULL); + } + + free(data); + } + + // A build stopped part way through its package: nothing left behind + snprintf(out, sizeof(out), "%s/STOPPED.DPK", tTempDir()); + TEST_EQ(PkgBuildStart(s, tmpl, rows, out, ""), BAS_TRUE); + + for (int32_t i = 0; !btExists(out) && i < BT_RUN_MAX; i++) { + TEST_EQ(PkgRunFor(s, 0), PkgRunBusyE); + } + + TEST_TRUE(btExists(out)); + TEST_TRUE(PkgRunPercent(s) > 0 && PkgRunPercent(s) < 100); + TEST_TRUE(PkgRunCurrent(s)[0] != '\0'); + PkgRunCancel(s); + TEST_EQ(PkgRunFor(s, 0), PkgRunFailedE); + TEST_STR_EQ(PkgRunMessage(s), "Stopped before it was done."); + TEST_TRUE(!btExists(out)); + // The errors: a row with no TAB, a file outside the program's folder, a - // missing template, file, or key + // missing template, file, key, or package snprintf(out, sizeof(out), "%s/NOTBUILT.DPK", tTempDir()); - TEST_EQ(PkgBuild(s, tmpl, "APPS/ACME/CHESS/CHESS.APP\n", out, ""), BAS_FALSE); + TEST_EQ(PkgBuildStart(s, tmpl, "APPS/ACME/CHESS/CHESS.APP\n", out, ""), BAS_FALSE); TEST_TRUE(strstr(PkgError(s), "no TAB") != NULL); snprintf(rows, sizeof(rows), "\nAPPS/ACME/OTHER/CHESS.APP\t%s/CHESS.APP\n\n", stage); - TEST_EQ(PkgBuild(s, tmpl, rows, out, ""), BAS_FALSE); - TEST_STR_EQ(PkgError(s), "This app package may not put a file at APPS/ACME/OTHER/CHESS.APP."); + TEST_EQ(PkgBuildStart(s, tmpl, rows, out, ""), BAS_TRUE); + TEST_EQ(btRun(s), PkgRunFailedE); + TEST_STR_EQ(PkgRunMessage(s), "This app package may not put a file at APPS/ACME/OTHER/CHESS.APP."); TEST_TRUE(!btExists(out)); - TEST_EQ(PkgBuild(s, "nowhere.ini", rows, out, ""), BAS_FALSE); - TEST_EQ(PkgBuild(s, tmpl, "APPS/ACME/CHESS/X.APP\tnowhere.app\n", out, ""), BAS_FALSE); - TEST_TRUE(strstr(PkgError(s), "nowhere.app") != NULL); - TEST_EQ(PkgBuild(s, tmpl, rows, out, "nowhere.sec"), BAS_FALSE); - TEST_EQ(PkgSign(s, out, "nowhere.sec", BAS_FALSE), BAS_FALSE); - TEST_EQ(PkgBuild(BT_BAD, tmpl, rows, out, ""), BAS_FALSE); - TEST_EQ(PkgSign(BT_BAD, out, path, BAS_FALSE), BAS_FALSE); + TEST_EQ(PkgBuildStart(s, "nowhere.ini", rows, out, ""), BAS_FALSE); + TEST_EQ(PkgBuildStart(s, tmpl, "APPS/ACME/CHESS/X.APP\tnowhere.app\n", out, ""), BAS_TRUE); + TEST_EQ(btRun(s), PkgRunFailedE); + TEST_TRUE(strstr(PkgRunMessage(s), "nowhere.app") != NULL); + TEST_EQ(PkgBuildStart(s, tmpl, rows, out, "nowhere.sec"), BAS_FALSE); + TEST_EQ(PkgSignStart(s, out, "nowhere.sec", BAS_FALSE), BAS_FALSE); + TEST_EQ(PkgSignStart(s, "nowhere.dpk", path, BAS_FALSE), BAS_FALSE); + TEST_TRUE(strstr(PkgError(s), "nowhere.dpk") != NULL); + TEST_EQ(PkgBuildStart(BT_BAD, tmpl, rows, out, ""), BAS_FALSE); + TEST_EQ(PkgSignStart(BT_BAD, out, path, BAS_FALSE), BAS_FALSE); TEST_EQ(PkgKeygen(BT_BAD, "x", base), BAS_FALSE); TEST_STR_EQ(PkgFolderRows(BT_BAD, stage, ""), ""); TEST_STR_EQ(PkgKeyInfo(BT_BAD, path), ""); @@ -280,6 +446,17 @@ static void btMaking(void) { } +// Read the package at path and those beside it as s's offers (a job run +// to its end): path's offer (0), or -1 when it cannot be read. +static int32_t btOffer(int32_t s, const char *path) { + if (!PkgOfferFileStart(s, path)) { + return -1; + } + + return btRun(s) == PkgRunDoneE ? 0 : -1; +} + + // Run s's job to its end: how it ended. static int32_t btRun(int32_t s) { int32_t rc = PkgRunFor(s, BT_RUN_MS); @@ -338,9 +515,22 @@ void testBasBinding(void) { TEST_EQ(PkgRunFor(s, 0), PkgRunDoneE); TEST_STR_EQ(PkgInstalledRows(s), "kpunch.dvx\tDVX\t1.0.0\tsystem\t\n"); + // Another DVX folder beside this one: nothing installed there + { + char other[BT_PATH]; + int32_t o; + + snprintf(other, sizeof(other), "%s/otherdvx", tTempDir()); + TEST_EQ(platformMkdirRecursive(other), 0); + o = PkgOpenIn(other); + TEST_TRUE(o != BAS_NO_HANDLE); + TEST_STR_EQ(PkgInstalledRows(o), ""); + PkgClose(o); + } + // The file and the other package beside it snprintf(path, sizeof(path), "%s/CHESS.DPK", dl); - TEST_EQ(PkgOfferFile(s, path), 0); + TEST_EQ(btOffer(s, path), 0); TEST_EQ(PkgOfferCount(s), 2); TEST_STR_EQ(PkgOfferField(s, 0, PkgFieldNameE), "Chess"); TEST_STR_EQ(PkgOfferField(s, 0, PkgFieldTrustE), "none"); @@ -362,8 +552,28 @@ void testBasBinding(void) { TEST_TRUE(tWriteFile(path, text, len)); free(text); TEST_EQ(PkgTrustKey(s, path), BAS_TRUE); + + // Listed as the user's; untrusted (its KEYS file goes), trusted again + { + char idText[PKG_KEY_ID_TEXT]; + char row[BT_PATH]; + char kept[BT_PATH]; + + pkgKeyIdText(key.id, idText); + snprintf(row, sizeof(row), "Acme Games\t%s\tuser\n", idText); + TEST_TRUE(strstr(PkgKeysRows(s), row) != NULL); + TEST_EQ(PkgUntrustKey(s, "0000000000000000"), BAS_FALSE); + TEST_TRUE(PkgError(s)[0] != '\0'); + TEST_EQ(PkgUntrustKey(s, idText), BAS_TRUE); + TEST_TRUE(strstr(PkgKeysRows(s), row) == NULL); + snprintf(kept, sizeof(kept), "%s/" PKG_KEYS_DIR "/ACME.KEY", platformUserDir()); + TEST_TRUE(!btExists(kept)); + TEST_EQ(PkgTrustKey(s, path), BAS_TRUE); + TEST_TRUE(btExists(kept)); + } + snprintf(path, sizeof(path), "%s/CHESS.DPK", dl); - TEST_EQ(PkgOfferFile(s, path), 0); + TEST_EQ(btOffer(s, path), 0); TEST_STR_EQ(PkgOfferField(s, 0, PkgFieldTrustE), "user"); TEST_STR_EQ(PkgOfferField(s, 0, PkgFieldSignerNameE), "Acme Games"); @@ -426,8 +636,25 @@ void testBasBinding(void) { TEST_TRUE(strstr(PkgRunMessage(s), "Stopped") != NULL); TEST_EQ(PkgRunPercent(s), 0); + // A removal stopped: the package it is on is finished first, a step at + // a time (PkgRunFor says BUSY until then), and the next is not started + TEST_EQ(PkgPlan(s, "0"), BAS_TRUE); + TEST_EQ(PkgInstallStart(s), BAS_TRUE); + TEST_EQ(btRun(s), PkgRunDoneE); + TEST_EQ(PkgRemoveStart(s, "acme.chess\nacme.cards", BAS_FALSE), BAS_TRUE); + TEST_EQ(PkgRunFor(s, 0), PkgRunBusyE); + PkgRunCancel(s); + TEST_EQ(PkgRunFor(s, 0), PkgRunBusyE); + TEST_EQ(btRun(s), PkgRunFailedE); + TEST_STR_EQ(PkgRunMessage(s), "Stopped before it was done."); + TEST_TRUE(!btExists("APPS/ACME/CHESS/CHESS.APP")); + TEST_STR_EQ(PkgField(s, "acme.chess", PkgFieldNameE), ""); + TEST_STR_EQ(PkgField(s, "acme.cards", PkgFieldNameE), "Cards"); + TEST_EQ(PkgRemoveStart(s, "acme.cards", BAS_FALSE), BAS_TRUE); + TEST_EQ(btRun(s), PkgRunDoneE); + // Errors and bad handles - TEST_EQ(PkgOfferFile(s, "nowhere.dpk"), -1); + TEST_EQ(btOffer(s, "nowhere.dpk"), -1); TEST_TRUE(PkgError(s)[0] != '\0'); TEST_EQ(PkgOfferCount(s), 0); TEST_EQ(PkgTrustKey(s, "nowhere.key"), BAS_FALSE); diff --git a/src/test/pkg/pkgTestEngine.c b/src/test/pkg/pkgTestEngine.c index 0989f552..b9f29c26 100644 --- a/src/test/pkg/pkgTestEngine.c +++ b/src/test/pkg/pkgTestEngine.c @@ -44,6 +44,9 @@ #define ENG_SEED_ACME 0x22 #define ENG_SEED_OTHER 0x33 #define ENG_BIG 100000 // a file that takes several steps +#define ENG_MANY 20 // files: a swap of several steps +#define ENG_SWAP_STEP 8 // the most files one step swaps in (pkgOp.c's OP_SWAP_STEP) +#define ENG_MANY_BAD 15 // the one whose place is taken by a folder #define ENG_TEXT_MAX 2048 #define ENG_SHA_ZERO "0000000000000000000000000000000000000000000000000000000000000000" #define ENG_ABI "abi = " DVX_ABI_STR(DVX_ABI_VERSION) "\n" // code packages state it @@ -54,6 +57,7 @@ // ============================================================ static void caseDb(void); +static void caseManySwap(void); static void caseOps(void); static void casePlan(void); static void casePlanRemove(void); @@ -64,6 +68,7 @@ static bool exists(const char *path); static bool fileIs(const char *path, const char *text); static void keyMake(uint8_t seedByte, const char *name, PkgKeyT *key, PkgSecretT *secret); static void makePkg(const char *stage, const char *text, const char *out, const PkgSecretT *secret); +static int32_t manyAre(const char *root, const char *text); static int32_t offerAdd(PkgOfferT **offers, const char *text, PkgTrustE trust, const uint8_t *signer); static bool planHas(const PkgPlanT *plan, const PkgOfferT *offers, const char *id, PkgActE act, bool explicitly); static int32_t runOp(PkgOpT *op); @@ -163,6 +168,97 @@ static void caseDb(void) { } +// A package of many files: no step swaps in more than a few of them, and +// a swap that fails late puts every one back, a few a step. +static void caseManySwap(void) { + char root[ENG_PATH]; + char stageDir[ENG_PATH]; + char dpk[ENG_PATH]; + char name[ENG_PATH]; + char path[ENG_PATH]; + PkgDbT db; + PkgKeyringT ring; + PkgOpT *op; + PkgOpStateE st; + int32_t before; + int32_t most = 0; + const char *v1 = "[package]\nformat = 1\nid = acme.many\nname = Many\nversion = 1.0\nkind = app\n" ENG_ABI; + const char *v2 = "[package]\nformat = 1\nid = acme.many\nname = Many\nversion = 2.0\nkind = app\n" ENG_ABI; + + tCase("a swap of many files, a few a step; undone the same way"); + engPath(root, tTempDir(), "manyroot"); + TEST_EQ(platformMkdirRecursive(root), 0); + pkgKeyringLoad(&ring, root, NULL); + TEST_TRUE(pkgDbLoad(&db, root)); + + engPath(stageDir, tTempDir(), "manystage1"); + + for (int32_t i = 0; i < ENG_MANY; i++) { + snprintf(name, sizeof(name), "APPS/ACME/MANY/FILE%02d.TXT", (int)i); + stage(stageDir, name, "one"); + } + + engPath(dpk, tTempDir(), "MANY1.DPK"); + makePkg(stageDir, v1, dpk, NULL); + op = pkgOpInstallFile(&db, dpk, &ring, true); + TEST_TRUE(runOp(op) > 0); + pkgOpFree(op); + TEST_EQ(manyAre(root, "one"), ENG_MANY); + + // Version 2, stepped: never more than a few files changed a step + engPath(stageDir, tTempDir(), "manystage2"); + + for (int32_t i = 0; i < ENG_MANY; i++) { + snprintf(name, sizeof(name), "APPS/ACME/MANY/FILE%02d.TXT", (int)i); + stage(stageDir, name, "two"); + } + + engPath(dpk, tTempDir(), "MANY2.DPK"); + makePkg(stageDir, v2, dpk, NULL); + + // Each finished operation says what is installed changed; a failed one + // does not + uint32_t changes = platformInstalledChangeCount(); + + op = pkgOpInstallFile(&db, dpk, &ring, true); + before = 0; + + do { + int32_t now; + + st = pkgOpStep(op); + now = manyAre(root, "two"); + most = now - before > most ? now - before : most; + before = now; + } while (st == PkgOpRunningE); + + TEST_EQ(st, PkgOpDoneE); + TEST_EQ(platformInstalledChangeCount(), changes + 1); + TEST_TRUE(most > 0 && most <= ENG_SWAP_STEP); + TEST_EQ(manyAre(root, "two"), ENG_MANY); + pkgOpFree(op); + + // A folder where a late file goes: the swap fails and every file is + // version 2 again, put back over several steps + snprintf(name, sizeof(name), "APPS/ACME/MANY/FILE%02d.TXT", (int)ENG_MANY_BAD); + engPath(path, root, name); + TEST_EQ(remove(path), 0); + TEST_EQ(platformMkdirRecursive(path), 0); + engPath(dpk, tTempDir(), "MANY1.DPK"); + op = pkgOpInstallFile(&db, dpk, &ring, true); + TEST_EQ(runOp(op), -1); + TEST_EQ(platformInstalledChangeCount(), changes + 1); + TEST_TRUE(strstr(pkgOpMessage(op), "is a folder") != NULL); + pkgOpFree(op); + TEST_EQ(manyAre(root, "two"), ENG_MANY - 1); + engPath(path, root, "APPS/ACME/MANY"); + TEST_EQ(scratchLeft(path), 0); + + pkgKeyringFree(&ring); + pkgDbFree(&db); +} + + // Installing and removing real packages: fresh, upgraded (the old // version's leftovers removed unless changed), damaged (nothing changed), // a swap that fails (put back), stopped (nothing changed), DVX's own rules, @@ -206,7 +302,7 @@ static void caseOps(void) { engPath(dpk1, tTempDir(), "HELLO1.DPK"); makePkg(stageDir, v1, dpk1, NULL); - op = pkgOpInstall(&db, dpk1, &ring, true); + op = pkgOpInstallFile(&db, dpk1, &ring, true); TEST_TRUE(op != NULL); TEST_TRUE(runOp(op) > 2); TEST_STR_EQ(pkgOpMessage(op), ""); @@ -244,7 +340,7 @@ static void caseOps(void) { pkgZipAddMem(w, "APPS/ACME/HELLO/README.TXT", "TWO", 3, 0); TEST_TRUE(pkgZipFinish(w)); pkgDpkInfoFree(&info); - op = pkgOpInstall(&db, bad, &ring, true); + op = pkgOpInstallFile(&db, bad, &ring, true); TEST_EQ(runOp(op), -1); TEST_TRUE(strstr(pkgOpMessage(op), "README.TXT is not the file") != NULL); pkgOpFree(op); @@ -258,7 +354,7 @@ static void caseOps(void) { // A swap that cannot be done (a folder where a file goes): all put back engPath(path, root, "APPS/ACME/HELLO/NEW.TXT"); TEST_EQ(platformMkdirRecursive(path), 0); - op = pkgOpInstall(&db, dpk2, &ring, false); + op = pkgOpInstallFile(&db, dpk2, &ring, false); TEST_EQ(runOp(op), -1); TEST_TRUE(strstr(pkgOpMessage(op), "is a folder") != NULL); pkgOpFree(op); @@ -271,7 +367,7 @@ static void caseOps(void) { TEST_EQ(scratchLeft(path), 0); // Stopped while unpacking: nothing changes - op = pkgOpInstall(&db, dpk2, &ring, false); + op = pkgOpInstallFile(&db, dpk2, &ring, false); TEST_EQ(pkgOpStep(op), PkgOpRunningE); TEST_EQ(pkgOpStep(op), PkgOpRunningE); pkgOpFree(op); @@ -281,7 +377,7 @@ static void caseOps(void) { TEST_EQ(scratchLeft(path), 0); // The upgrade: the dropped file and its emptied folder go; still chosen - op = pkgOpInstall(&db, dpk2, &ring, false); + op = pkgOpInstallFile(&db, dpk2, &ring, false); TEST_TRUE(runOp(op) > 0); TEST_EQ(arrlen(pkgOpKept(op)), 0); pkgOpFree(op); @@ -303,7 +399,7 @@ static void caseOps(void) { makePkg(stageDir, v3, dpk3, NULL); engPath(path, root, "APPS/ACME/HELLO/NEW.TXT"); TEST_TRUE(tWriteFile(path, "mine", 4)); - op = pkgOpInstall(&db, dpk3, &ring, true); + op = pkgOpInstallFile(&db, dpk3, &ring, true); TEST_TRUE(runOp(op) > 0); TEST_EQ(arrlen(pkgOpKept(op)), 1); @@ -330,7 +426,7 @@ static void caseOps(void) { // A changed file is kept unless asked; asked, everything goes, and // the vendor's folder with it - op = pkgOpInstall(&db, dpk1, &ring, true); + op = pkgOpInstallFile(&db, dpk1, &ring, true); TEST_TRUE(runOp(op) > 0); pkgOpFree(op); engPath(path, root, "APPS/ACME/HELLO/README.TXT"); @@ -363,7 +459,7 @@ static void caseOps(void) { stage(stageDir, "APPS/KPUNCH/CHESS/CHESS.APP", "chess"); engPath(kp, tTempDir(), "CHESS.DPK"); makePkg(stageDir, kpText, kp, NULL); - op = pkgOpInstall(&db, kp, &ring, true); + op = pkgOpInstallFile(&db, kp, &ring, true); TEST_EQ(runOp(op), -1); TEST_TRUE(strstr(pkgOpMessage(op), "not signed by DVX's key") != NULL); pkgOpFree(op); @@ -372,7 +468,7 @@ static void caseOps(void) { // A file another package has is left when this one goes dbAdd(&db, "[package]\nformat = 1\nid = acme.other\nname = Other\nversion = 1.0\nkind = app\n[files]\nAPPS/ACME/HELLO/README.TXT = 3 " ENG_SHA_ZERO "\n", true, NULL); - op = pkgOpInstall(&db, dpk1, &ring, true); + op = pkgOpInstallFile(&db, dpk1, &ring, true); TEST_TRUE(runOp(op) > 0); pkgOpFree(op); op = pkgOpRemove(&db, "acme.hello", false); @@ -769,6 +865,23 @@ static void makePkg(const char *stageDir, const char *text, const char *out, con } +// How many of caseManySwap's files under root hold text. +static int32_t manyAre(const char *root, const char *text) { + int32_t count = 0; + + for (int32_t i = 0; i < ENG_MANY; i++) { + char name[ENG_PATH]; + char path[ENG_PATH]; + + snprintf(name, sizeof(name), "APPS/ACME/MANY/FILE%02d.TXT", (int)i); + engPath(path, root, name); + count += fileIs(path, text) ? 1 : 0; + } + + return count; +} + + // An offer from a manifest's text; its index. static int32_t offerAdd(PkgOfferT **offers, const char *text, PkgTrustE trust, const uint8_t *signer) { char err[PKG_ERR_MAX]; @@ -849,4 +962,5 @@ void testEngine(void) { casePlan(); casePlanRemove(); caseOps(); + caseManySwap(); } diff --git a/src/test/pkg/pkgTestNet.c b/src/test/pkg/pkgTestNet.c index ce01daa0..79104a78 100644 --- a/src/test/pkg/pkgTestNet.c +++ b/src/test/pkg/pkgTestNet.c @@ -43,6 +43,7 @@ #include "dvxtest.h" #include "pkgServe.h" +#include "lookupHook.h" #include "pkgBas.h" #include "pkgNet.h" #include "pkgNetBas.h" @@ -99,6 +100,7 @@ static void caseAccount(void); static void caseBasBinding(void); static void caseBasProgram(void); static void caseCancelClear(void); +static void caseCancelInPoll(void); static void caseCatalogue(void); static void caseConfig(void); static void caseDamaged(void); @@ -112,8 +114,11 @@ static void dirEmpty(void); static bool logHas(const char *text); static void makeFixtures(void); static uint32_t msNow(void); +static void ntCancel(void *ctx); static void ntCapture(void *ctx, const char *text, bool newline); static PkgNetT *ntCreate(const char *prefix); +static void ntFree(void *ctx); +static int32_t ntOffers(int32_t s, const char *dir); static void ntPath(char *out, const char *dir, const char *name); static void recordAdd(const char *text); static void recordDrop(const char *id); @@ -217,7 +222,7 @@ static void caseAccount(void) { // pkgnet.bas's functions: the catalogue as rows and fields, a fetch, and -// pkg.bas planning what it fetched (PkgOfferFolder, PkgOfferIndex). +// pkg.bas planning what it fetched (PkgOfferFolderStart, PkgOfferIndex). static void caseBasBinding(void) { char home[NT_PATH]; char url[NT_PATH]; @@ -254,7 +259,17 @@ static void caseBasBinding(void) { snprintf(number, sizeof(number), "%lu", (unsigned long)sPkgs[NtChessE].size); TEST_STR_EQ(PkgNetField(h, "acme.chess", PkgFieldSizeE), number); TEST_STR_EQ(PkgNetField(h, "acme.chess", PkgFieldSignerE), ""); + TEST_STR_EQ(PkgNetField(h, "acme.chess", PkgFieldSha256E), sPkgs[NtChessE].sha); + TEST_STR_EQ(PkgNetField(h, "acme.chess", PkgFieldRepoSignedE), PKG_BAS_YES); TEST_STR_EQ(PkgNetField(h, "acme.none", PkgFieldNameE), ""); + + // A handle for another DVX folder fetching elsewhere opens the same way + { + int32_t other = PkgNetOpenIn(url, sRoot, sRoot); + + TEST_TRUE(other != BAS_NO_HANDLE); + PkgNetClose(other); + } TEST_STR_EQ(PkgNetDependsRows(h, "acme.chess"), "acme.cards\t1.0.0\t\n"); TEST_STR_EQ(PkgNetDependsRows(h, "acme.none"), ""); @@ -277,7 +292,7 @@ static void caseBasBinding(void) { TEST_TRUE(strstr(dir, PKGNET_DIR) != NULL); s = PkgOpen(); - TEST_EQ(PkgOfferFolder(s, dir), 2); + TEST_EQ(ntOffers(s, dir), 2); chess = PkgOfferIndex(s, "acme.chess"); TEST_TRUE(chess >= 0); TEST_EQ(PkgOfferIndex(s, "acme.none"), -1); @@ -285,8 +300,8 @@ static void caseBasBinding(void) { snprintf(number, sizeof(number), "%d", (int)chess); TEST_EQ(PkgPlan(s, number), BAS_TRUE); TEST_TRUE(strncmp(PkgPlanRows(s), "acme.cards\t", strlen("acme.cards\t")) == 0); - TEST_EQ(PkgOfferFolder(s, "nowhere"), 0); - TEST_EQ(PkgOfferFolder(NT_BAD, dir), -1); + TEST_EQ(ntOffers(s, "nowhere"), 0); + TEST_EQ(PkgOfferFolderStart(NT_BAD, dir), BAS_FALSE); TEST_EQ(PkgOfferIndex(NT_BAD, "acme.chess"), -1); PkgClose(s); @@ -432,6 +447,27 @@ static void caseCancelClear(void) { } +// A job stopped, or the client freed, while a poll waits in the name +// lookup (other code ran meanwhile): carried out when the poll returns, +// the stop said as any stop is; nothing touches freed memory (ASan). +static void caseCancelInPoll(void) { + PkgNetT *n; + + tCase("pkgnet: stopped or freed while a poll waits for the name lookup"); + dirEmpty(); + n = ntCreate("ok"); + TEST_TRUE(pkgNetCatalogueStart(n, msNow())); + tDuringLookup(ntCancel, n); + TEST_EQ(pkgNetPoll(n, msNow()), PkgNetFailedE); + TEST_TRUE(!tLookupPending()); + TEST_STR_EQ(pkgNetMessage(n), "Stopped before it finished."); + TEST_TRUE(pkgNetCatalogueStart(n, msNow())); + tDuringLookup(ntFree, n); + TEST_EQ(pkgNetPoll(n, msNow()), PkgNetFailedE); + TEST_TRUE(!tLookupPending()); +} + + // The catalogue: the packages, their fields and needs; an entry of a kind // this DVX does not know, and one with a bad id, left out. static void caseCatalogue(void) { @@ -867,6 +903,12 @@ static uint32_t msNow(void) { } +// caseCancelInPoll's code in the lookup: stop the job. +static void ntCancel(void *ctx) { + pkgNetCancel((PkgNetT *)ctx); +} + + static void ntCapture(void *ctx, const char *text, bool newline) { size_t used = strlen(sCapture); @@ -888,6 +930,25 @@ static PkgNetT *ntCreate(const char *prefix) { } +// caseCancelInPoll's code in the lookup: free the client. +static void ntFree(void *ctx) { + pkgNetFree((PkgNetT *)ctx); +} + + +// Every package in dir as s's offers (a job run to its end): how many, +// or -1 when the job would not start or end. +static int32_t ntOffers(int32_t s, const char *dir) { + int32_t rc = PkgOfferFolderStart(s, dir) ? PkgRunFor(s, NT_RUN_MS) : PkgRunFailedE; + + for (int32_t i = 0; rc == PkgRunBusyE && i < NT_RUN_MAX; i++) { + rc = PkgRunFor(s, NT_RUN_MS); + } + + return rc == PkgRunDoneE ? PkgOfferCount(s) : -1; +} + + static void ntPath(char *out, const char *dir, const char *name) { snprintf(out, NT_PATH, "%s/%s", dir, name); } @@ -949,6 +1010,7 @@ void testNet(void) { caseUpdates(); caseRefusals(); caseCancelClear(); + caseCancelInPoll(); caseConfig(); caseAccount(); caseBasBinding(); diff --git a/src/test/pkg/pkgbuildTest.c b/src/test/pkg/pkgbuildTest.c index 1bb931da..516fd353 100644 --- a/src/test/pkg/pkgbuildTest.c +++ b/src/test/pkg/pkgbuildTest.c @@ -200,9 +200,9 @@ static void caseBuild(void) { // Show page of win's tabs and click its control name there. static void click(WindowT *win, int32_t page, const char *name) { wgtTabControlSetActive(tFind(win, "tabs"), page); - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); tWidgetClick(tFind(win, name)); - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); } diff --git a/src/test/print/Makefile b/src/test/print/Makefile index 35d662f3..9e0924e4 100644 --- a/src/test/print/Makefile +++ b/src/test/print/Makefile @@ -32,7 +32,7 @@ include ../dvxtest/dvxtest.mk PRINT_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/print BINDIR = $(DVXTEST_BINDIR)/print PRINT_TEST = $(BINDIR)/printTest -PRINT_SRCS = $(wildcard $(PRINT_DIR)/*.c) +PRINT_SRCS = $(wildcard $(PRINT_DIR)/*.c $(PRINT_DIR)/drivers/*/*.c) # Fonts as the build makes them (src/tools/Makefile), for the text at # printer sizes: DejaVu Sans, with its print sizes, and DejaVu Sans diff --git a/src/test/print/printTest.c b/src/test/print/printTest.c index 9b4eed70..ad242e35 100644 --- a/src/test/print/printTest.c +++ b/src/test/print/printTest.c @@ -92,6 +92,7 @@ #define PAGES_TEXT_MAX 1024 #define PAGES_LINES 20 #define PAGES_FILE "pages.prn" +#define TICK_FILE "tick.ps" // tickBand's white page #define PAGES_SRC_MAX 65536 #define FORM_FEED 0x0C #define FORM_FEED_TEXT "\f" @@ -414,6 +415,7 @@ static bool ppmRead(const char *path, PpmT *img); static void printDlgScript(AppContextT *ctx, WindowT *modal, void *userCtx); static void printerErrorScript(AppContextT *ctx, WindowT *modal, void *userCtx); static void printerRun(PrinterRunT *run, const char *src); +static void printerRunAs(PrinterRunT *run, const char *src, bool killed); static void progressScript(AppContextT *ctx, WindowT *modal, void *userCtx); static void resetFake(void); static void retryScript(AppContextT *ctx, WindowT *modal, void *userCtx); @@ -428,6 +430,7 @@ static int32_t squareErrors(const PageDotsT *pg, const SquarePageT *sq, c static void squareInit(SquarePageT *sq, int32_t left, int32_t top, uint8_t r, uint8_t g, uint8_t b, int32_t width); static bool sysInk(uint8_t ch, int32_t row, int32_t col); static bool sysUniform(uint8_t ch, int32_t row, int32_t col); +static bool tickBand(void *ctx, uint8_t *rgb, int32_t width, int32_t y0, int32_t rows); static bool unpackBits(const uint8_t *in, int32_t n, uint8_t *out, int32_t outMax); static bool vmClose(void *ctx, void *job); static void *vmOpen(void *ctx, int32_t port); @@ -1210,6 +1213,37 @@ static void caseDocuments(void) { } printSetPortOps(NULL); + + // A white page sends nothing (PostScript to a file), yet the idle hook + // runs as it is worked out (printJobPoll between sub-bands: here each + // band call takes PRINT_POLL_MS), and a Cancel there stops the page + int32_t calls = 0; + + hookInit(&hook); + hooks = hooksOf(&hook); + doc = printDocOpen(printJobOpen(PrintDestFileE, TICK_FILE, &hooks, err, sizeof(err)), PrintModelPostScriptE); + TEST_TRUE(doc != NULL); + + if (doc) { + TEST_TRUE(printDocPage(doc, tickBand, &calls)); + TEST_TRUE(calls > 1 && hook.idles >= calls); + TEST_TRUE(printDocClose(doc)); + } + + calls = 0; + hookInit(&hook); + hook.cancelAfterIdles = 1; + hooks = hooksOf(&hook); + doc = printDocOpen(printJobOpen(PrintDestFileE, TICK_FILE, &hooks, err, sizeof(err)), PrintModelPostScriptE); + TEST_TRUE(doc != NULL); + + if (doc) { + TEST_TRUE(!printDocPage(doc, tickBand, &calls)); + TEST_EQ(calls, 1); + TEST_TRUE(!printDocClose(doc)); + } + + unlink(TICK_FILE); } @@ -2057,6 +2091,12 @@ static void casePrinter(void) { printerRun(&run, "Printer.Circle (1440, 1440), 720\n"); TEST_EQ(pclPageCount(PRINTER_FILE), 1); + // A program ended by force prints nothing in the kill: its page is + // thrown away + unlink(PRINTER_FILE); + printerRunAs(&run, "Printer.Circle (1440, 1440), 720\n", true); + TEST_EQ(pclPageCount(PRINTER_FILE), -1); + // CurrentX / CurrentY set before anything is drawn hold for the first // Print: text 2 inches in and down, nothing above it unlink(PRINTER_FILE); @@ -3448,6 +3488,13 @@ static void printerErrorScript(AppContextT *ctx, WindowT *modal, void *userCtx) // A program run with the form runtime on the test GUI (its PRINT output // in sCapture), the runtime then destroyed (a page left is printed). static void printerRun(PrinterRunT *run, const char *src) { + printerRunAs(run, src, false); +} + + +// printerRun, the program's end a normal one or (killed) a force kill's, +// as the BASIC runner's kill hook does it. +static void printerRunAs(PrinterRunT *run, const char *src, bool killed) { BasModuleT *mod = compile(src); BasVmT *vm = basVmCreate(); @@ -3477,6 +3524,10 @@ static void printerRun(PrinterRunT *run, const char *src) { } } while (r == BAS_VM_STEP_LIMIT); + if (killed) { + basFormRtPrinterKill(rt); + } + basFormRtDestroy(rt); tDialogScript(NULL, NULL); basVmDestroy(vm); @@ -3739,6 +3790,19 @@ static bool sysUniform(uint8_t ch, int32_t row, int32_t col) { } +// A white band that takes PRINT_POLL_MS of the host clock to work out; +// ctx counts the calls. +static bool tickBand(void *ctx, uint8_t *rgb, int32_t width, int32_t y0, int32_t rows) { + (void)rgb; + (void)width; + (void)y0; + (void)rows; + (*(int32_t *)ctx)++; + hostClockAdvance(PRINT_POLL_MS); + return true; +} + + // TIFF PackBits in[0..n) into out (at most outMax bytes); false when it // runs over or is cut short static bool unpackBits(const uint8_t *in, int32_t n, uint8_t *out, int32_t outMax) { diff --git a/src/test/samples/golden/basdemo-debug.app b/src/test/samples/golden/basdemo-debug.app index f3bce4453e3f1d3b62018a1ae66c812d0fa6473d..f8514e63c6aa064af69cdb0eaf1ddc0088160150 100644 GIT binary patch delta 10133 zcmZ`<3!GKc{$FdK_1ot(Q<87r zzjRQq&J{w4TKu)fvk)JNPtyz`BFXl7c=BIaGdvhZ6glW!=NGXP&C`E~$AIXs- zTZk+nuF(r4ctFd=MeJk!SVWR|SXVL{a4(MJ2tiMZf}-Zkuq2|!-hJ|sUt82GfR>j0 zfvUm=Ok2u|lZa8i$}_@?L>%avI4u_O>xO|-$Tjig0IqAvZBbI#?+j=-9*Hi%qZSV- zv``q>0!L`6q6;EIJIZ0+1xn}$5E7acEfIIdv}>NzgpRQYJtbz+QW2dhy_l|Nk!8Z2 zDq}iB(Q;8d8hq#?Q_%|BsIO?HZ8T7DWRk6L6>>;tTTQFjcQQld$Dy!9=ccjhCW@YB za-5zKac^i+Ly2x~rB@4RPUuq%um(e2E2v5&N_0!s1Hb6jiq;8wR@4aL2uHEenL-TJ zc?ycbQ&1EGq|Z==^ZD6UMYG|hc52RGZnjs|nNpPM4l2t-l%V3(@mji$ zY=YIC5td_!qW^eFmg||tsHM$tONkz)=w(qnEl9$fOh3r#F-q2YtbMFDB3Axg;prA5Ed05eIOW6EGiZD;nheG~b`(z55uV@b^+Ts+nK+#7G zElEL-DEgRF7x~xTqMlxel&W2+A6JDv?B`x>C_6brawTMSI0MSo~?q;zjAf z+sDCMu4umv-pa5%Rw+7QqWFxW&yqPKOwM?zq*<-#AUn2;9h_dH=yQf-M0B=cS*z%a zBzU$3s}y}{KoZE4b1c42(N~OjwaBxI4hi~NbTf#_|MiN#VJ12(&P#RvoT6`ovC?$(g>fi-MM+JQ^E=p$G*m7Sx;1$Wm><%P1FVu1aTlIJ9wUk(I}lJ2^WLg$n8LobrJ$<2l|g zh4}yprEbr8y3hXK&MdgGiPXCd7!fKzW7m`8|I9bwhl+mV$;0Q4eiw1PK8V*)`yNGq zFd4ddC?Wibu@VUjTOCp1m|1ME!fe9`PhCK3`)mn?#LG0PRX z&KfKZaJVbvL3a3|bcYW@YCX;9w@Dvxq@zNAr?R5Pj0^!^YtzDja=3NGv@n=cxpg$; zRAM&##KPZu27{m5Fvkz31!tbktz(L6MW8%P2X3X3gIhs-I90e6v5CViRVQ{o@wh3u zcM%c^{D&e>%5X()_P>um<{C@~rg4>^pN0i45`^t{>F)W?&zEDiAF`KckE z5j;;Iu3JaOk;cfa2wr&AGB3PXk&LJB;KJ^d>_?wP@PdqE&wdK4@61i8hyR5>n;UIV zOpYkr#T}hnr=#=JJL*QVzux#$`VbO?cQcpyn_K0oeneURo>pz@UqO^DQCt{N zBfnwmR$l)U(AerKY=UkwbYY5?V`(uxAibj!q7$u;uc2%CjHm|^>8a2}CiJyb zh|mf>gs7=!*{-Jn*v%0&vq>_7;Bn3XOll&^yScw}{E->W@IKPK=ZI#A)hMEqIsI^~ zP3AP-O`@mLT94*&z$+=GGMeo{dJLf`m?lJXeCoDAA8Y7f$q4AS&HSK`Gjt?Ul3u3` z`glV}z?1rI8~lw}!OcX?tsf@(FX!cYWkjb)3=27!D^q5Lz+Y?|gwEgK9KAK&*QavX zP2n`AN}P1+vCr=yI!$s7cm|J~2_a4`WSplDxbPZ%7g0-uWg3QyvE5B{x`cf3{WvT* zc9j1nY9(64|y!WyV!?`+SyL$ z5w+(q!!OuUhI7$;q7Ig20Ts4jRSSvElsH3$Opm3Hpd*i9F;OQwf~Sbi!pzKn{rm#z z?8$9bbSdGwL7ij%s(R2lX{2RD=Su8UsY@EToaj7hj~aA-8fj(vD7x|}))IBIqgcm< z#d*HoqrF4v0$I~YbV)z9K1Xz6ooV)Y3Qnjma4~Qen*-`bPB=c9Qg@j=k-li5{1AG{ zLKn&L*q|jjH!EBi*+5)HRMrG+tJ(UPP>?$wv*@*>$F`&m&#A#;r)Q# zP1H;7O|?HH>McJ_wf7M9k%v<4kBBal&xXU%9~1SJ>pk24B#g;kqJDA%N9R)>Wf!Ln zlI4;jJ`d}Cj2NIZT`uFybb#oJI-xq4ZpM{t#@9sstr>^Ie!k_EUZy^!N>JWb> zy4KJF#dT=E5M39_4VE+{UFmN;ngjZebT3@bUXU)`V7;JRx=~^t#c_v9W!$+K-ld`P z9!yT9nq2lW-+UAl@pYbdUHZQ;O@fQFm`lS#L}$8OPMAdMyEI(V2uXjJxCnAZ#8f)= zNker5mqzkDYvY~e(kMylCQ1MB3p?aoknJK_F&XtL;x$n>a%r>#Vv8c^H^-$hw%?{M zjg>UcU)Uk;HFIfvumcEZqLW={qvVe7X7)hbEnWr;cXztAJ4=Y*PAbnW{GNMk=h6fz zr${+c9_e$umUF8;JAyB}CSo=1;JSR%*U4aIl5UYRmk0QI|{5DDd48&2vT@7@*q$zChqpTD=oNi|LQ>3|Uy951S;9@UB z|B~3X9@Y7wOnD&VK?s|Wi;PftAe4Ym4;On77Mcp7o-Bkb$4lJeQr_Rd50|q5Om!l7 zntQua-URXmuD*iV+tdOE?Q}$Oguc?~zXkg5gf)CxGbTIAzp!KT*wwB%xtI7ibZk2C z8W$WZ47p{c*%}WF5Y`4nA<+g|b$4;>_#joA{I_aqB*v@lI-a!#TfRH}?Hy0g9Aewk z(9Z2-wiHmiYAVI_NEhcJ^u4rYW>gHfiX2*oS$;C!6yPnYh@OyS@nlswvb5B&X zbmB0!ZMKX3h(D%N6K|~R6aq1`49-Q1Tb9N%Gcnk4w$r<1R!n5o6ImG|E7Od(oHN(- zKHde&-QDbhhVJ;}bO#%8ij7uj7mEk!d!onD!?958ubkH$CLzj=(qBvfV4E z3S7|LVSxn4Ciu6C?~ngwDM{98?(q_Rugf>rC3==SOD0R9!Yw>tE+8LtX@-;qQob(2 zT9{)p4z(~7X*CzMfW)6?u=^z4Z);(`Np`T!0^6e@y}~B@LN0SIso9`c8aj&Mfq;&* zU*s7K)!=hVm})>`FE)7$u?HcxBuwf#Na|%F|6I$DOU30JUGC-~@UJip6v4ybU&Z{> zVE0;gmLu|ThU5GCD&wLGB(?{kc@m^GaZDK-*b#T)F8C#Q3l?u;J_wZPYIjy7>9#Fy zVL3viUw3J~l(?CgX}$J_868wEz&N(SYcR3KVDQ=_)@$2CuRSWggnq{`ER^(^b=$jU ze2-y#k4t$By52K>1WzSw+8Nrk$DO6vCjPC9DtP<{gDsKtl(p)Z z8NpK+0j9>0kWc6zEwoh9GRfKblZBQ`oE*_Vqd!{|j4KS|FBSqLkDK{^wIKLb8ou8w z1U_DYnD2KBf^U`K`@=%ZxnP*Q}^B+z=BA2GwV zJ_z=6h)QP2vmxL?ue%qi18#*Nf;YC(6Z!`g#eu*d&zMWMwe5HZ?Mv0vWE!W>L(oEb|}8G*9+To8=6T z*FRhiofhhOSz?wpePdRQ8$B-vdR~E^7E#Q_tfv}!T7@!OtPJM+)<$MaAoD6@@}ii$ zS!OF_+J-W(1v0#nW?(m9gkB4TUWZWoC?<9mdILfojnFnpZw9`~#J<1?ya|ETg5H8a zXXfFF<~&QO;XH%POsbLP{ZNJ%TGIBAg9_qU|2hQy;tlCH9(9fri+BuRyq^o}91u}RX^h7{BUx`6jQV=mv<1?`yxi!pQV zM3I!}fl*9|ydDgW^3S;?dT5kBu)FGE(S^0OeQM3#ad($^Nyq0QH!<0%KVKzimSRe|nrB@0!ziJZBo?yF;l7 zexI%#PMjFUT6?|mxIML%`S*0q^KOmOhwO^$YinU8tlJ~`4%}|G_2ejh#0>PY%$4+s zEKi+UZi~`h-pTJiPvW;1$lvH#O}IJpf!z}xVtQNH&xnOGqe z@3ZfWrU1!1>bvRy@Y{XSnn_|xGDk4vf+BN4cYl;VGg-<%-RfD?x^9&EREMG9s{A9eG z{`fx(XfI$7!*q!svLG6htOek{@8Q1nyhvH?f#wZ<{q9ZW^M32@@#|iQ@~H*+Pe}ea zpV~IE1pGi8mmee@!-8$(_!zK)j{%#a^rJip@juBGj$1t746aZ8%3tOD$GbPnskQ_9 zDcJEO^s9bN_fzCn|C{d3GGDV~wkXfrX4_zgm*M6&Ep#*pFIGM$3KE_iF8qs$snM8z z%QF5P%GN|3{+|w}P}^q4;MKReEb*Itu!`VU5g5gFrguc~@Ym&^)Z!hh=NCSi8qXy3 zyB0m_>-=usd$#=zuew;maVgj~9y;zPw1T?+2&5D z0XqLDLA@A<up7Ma*@qVTjZap3k7*}ou-|zG+aD!dcpz+_ai!89avL`e zS~;v+5h(iH0yf9LFp$UD0XrNpeHk*ruP_-^#gkI7AN(q$5fre=X%14@9l|e%LY9;` z=83>&qYdxLqWn5!C0Sjyq(`-r(@69lHG23}<0oD*YGPUMaifO~pD?j%)WriL?Q%m( zzi~s8gaPBqN=Ij<;U!}y3@;t#ztpRLMobKwGr4!wNtDj;1pHte)YQOCr cotn>>AjFw`osPdtuNhRd;dcC55;Vns0kd`C^#A|> delta 10136 zcmZu%3!IJB7T^0jdwpkSP%||s&ojf7x14%n-P_e(RX_w zMIwcY5k0s>FC=heZP#xUy+@SXF7MTc9GTn9OrzQ zZpCh@?#MO%;i#4){atB0YWKWt)bN7Qg@Z;7EglvQPJAHz#p(TOo&hbF;$4jQ zV|XvadsRi-jQkqj33#tMn(gq8zl{%lQ-tP(Pt`++!ozLeNtpzn~r%I_bM2YQ%kY zkDka$&xe{^enVHh21}1)%?ZS4FEzsRL=52a7|r)vpB3{yh(zar`9pk3p+AbC_c@Nx zAEO-+`jdzQRbq}3`cohz^v(!9E$&UzpPBE;LhoV`S|Fy;Ga@=srs*#%uu!-Rd>Nrd zqHq}4(8KNsJ!>0#A|5Tat-b8K1Ydj^5=VbyRXxWJk{Kc|28~7fPztO5E<(>UIYvuF zY+yn`k=9mtses}#ebfNUVCf5jUKC|T`WN&+m%Xo#N9ZL%%f(h99O0PIRTE;MKEb{R zPkuof`}Frnd^Ugo8Hr}WOMgWkxt5zJBO9wpQLHPOJ5vf-D(-N!P%bk`1!QM$nHKtK zsVFTOxwE%O(~j~$8>1tNX$i|#n6Z>;rQ&T^c~oUOB1nidRy9UvD5?ksyi&wwA(StR z7%P}79)NF7R~(^9MLJs*Rz#pd=$fi~%*rV4Ymm5UUu6@`-_jU>!7-5{y+JtZqW*4{-qC_G*ks9lwvRMi}D zgYKr((0T`jh8EWQ)E&Kb6n9|LUpg*rSBh@{7Jn9seXQ&{oG&C zJ9gyPDw$;xet<%4$&#MX*DHEAoOb#KMVojqus)VlTC8Yu0?kRH0~Kvapt(u(Mn&%> z&_+njt-=oCCPnWjD2+q@VW^afwk6fi&xE2v4^{M`ke^o0 zOG39O`benRv-njbe}0m1tD=wnw)Jaz!xViYrIgNhCXrqXxfVw=t=IGAEn z6&>b|WOGqr^-NQAM93~FX+K@j_kw;9osw!FQly2f6f3OQ3`Iu;{U}~aGR;i&>rd?0 z#}xf+{Mv%MpUrfcF3Qc)tDoQ|W>A$fz zWcCGIr{M-nomvu!{ELWzyFavjh03BAsydEPVKy)3d_i2wrNcGlIh7gVzSX$w7kYj5 zd!zgtYD@h*gB~(2fwnT}62lr1DlcQlf|9@T47gO$uc#)%=Z=39F+BbUk6~}i6dh+W zbg^k6{E~44G8)csM7d+C+H!^J!wCMQsO>r0BCyyIwNn6yMi^LOWI%?DssaF`DgZ=W z1}cn5(08;70FksLFbpjCOJ`)Iq7x!Cezl1LQV3K84G#qzg;x~)Zld(6p}Qd+54>g4 zuW`Sqq?xX$Cr4E1H&kZS3@k$+CL2r(CT4SMqiMl_ z2HbixY%^)$ue=})-hvN$M8Vfw9=E=%__{0uc=o35$ z4!;!rfgNEZ|4?->1HI6)lc_dWDspWv5*9)U*8+z)-Ox{j-~}1O*1bOKSKJKrYvSjo zm|c#D@8FK2#2tnGWKhZ~SDkvxD+HH>8pIR!YSE*>6rwGDa~LkCMn zK(}q?2feGIBdZehDs9la89Jh$&~Mw|_gF!9qO+_Yda{{F!NhdO!$s_@UPNb0*ohRa zC6nU}fl~(CA7Yz0h5MvBzYb5ze{q&0lk*GGq1V%KqPmi&NP!{LlUy%iRA0upy1=Pd z>3&2FY*hYD)KKzFcnF6L$C`aLQA|>fJO#dnC|6RWQ{e%jSstJ98nazD5)S@hu^2>; z^x;ttrJyS4TZ~uWt6K@T1mTn%MsyCx9TsCt8luCAnp&0-6whHzBZ-?j?bGv!@@z*> zr~0fD`)n~$XX~@)iSp%__JTn#A?hM`C)-Pjy2?F?HqZZMMBU_}B*P0t-Q~-nIr>GS z9&(j!z7&RJIZ;pf3I}He8+n1#6uHu^fDhEVf)N9>rCu_|OfM5%S|wDgc^F=}5$Mh1 zSWDE$j$>We(Rz;kWq*tP8>hy8BX>%dOZun8T>MJE8Md#G^e?pEGVQpeDf z-NvJc_#UESNdqO_=t*2`xFWaQW6-b-{77g`(euZadp8Z7B%iDC`*baM$SgxPI< z!&MW3>`}mLrru99L;|sMBrxLu(NJr~VWL|k-71~txG$a2vaWZ8Xjrgk2&>Z%sZJTr zPWhP%pF=kSSCPkQh7iF$*9nUM#NGZyG(yVpQVx+vd!4N0d^>5PjjD1RmfT8m`S#Fp zIlv{7M#`zo23JJfOw0iqC7r%dkJ~I4W2f7t!@IdMO{O`TM_#7WjQAar#suP+)zu7i zr=+nw>ba~Gd!p(tgaMfwyt~kU4Hvr~x?9pX`J>JZWyS|G?IDD%OO_ED9|%o=&>1c^ zDlBvlgtA!(*O;~3!eZVP!4D1my{+mtXvl=7m8gp7vk^i#SjUW}kU zL}t3k%n+H?jH#uZsLLi&n(pQTprO0|I1Nl-`!BWf_e!TD_wz13Xp&4Bu1eFr z|Hk$OvrWc+CU6UP1is5G-+gkTz|CRY0uwNLHptk7>*p&iCBfR?Jz1m&xP0$jqzAe) zWFqNrbmRNbnZDVj`=!j6@?8-o{SeRO7P9^UB+yXT<;(J62Ad-3L7Vl%O&82w`0F=ZnkSuSFl1*uYMC9? zhHzB#VaY~}3LbykU{6b0U`O?iv0wo#KoJ}Q`7-^kg`SbLQ1Xu>n=G_Qno}i%n=J^w zXPM8_TPy}fwwoE>vmh838--gf1U}wqGT-|a1mAOpZ<~b{@q}Tn?G^&p^M>n#1msPl zB~q?WomU^GaxXR9A6W>@m6fdXV+%p&GQ;(WgKVo=Q{I%)b-i@(I{0!!}*A&(%w z+=zc}T#W=;VH0GRVOtRdy8=Y%Sz(AR-*e!YrZ{4NxFjZed70;9AZHZrdTGCU`b zxTvta1icO!9rnE@=o|M&KZR=6N)&0R@hZ8h{%rKD4fOmMdXBlM#jIx?^!yeItxpJ5 za=kxpWYz~VZ$RdRi{i~P8zA#%D6`SZWMJ7hG8+S#Hz9Kp^)JH+y#*l|<>mfuN$&&( zSHlh<3U8u4jqqdJyO2=K!-Xc@QZ{j(8JX95uZMyz}JquV#}`b6my_RMval^8Wf z`>70WGQ(Z-Wl`F}475|$lJuD@O`c#bkJ9J7tKWH{jL|O0;o=0bw(eu)dY2eqO>b|a#M(-S{YqdnS)V4M8BF=nh0xzqS{KPKq6(!pr3 z){Zrh$JjPrX0UOxOxuG+r%c~%p>@(N)8lOOK;W`6J>EhG6W2E4VCOPDA%wHA3r1x; zB1iNA(ZUk@c-)o`X`30bkHxk zq-KMAZTpBcr`R$*$wJ@ze{`&WrJihoBf%N$0SmxeKfqg4qFAC5lWTkDST$GNpr>*s z;y3T0jt0NF6!A=)=4W+`T{At(Cm75CLi)$}!1fR;fh&l^pd}r}3T{;KxnK#O3uZ>? zM_C6MP{$RHTiEv;o}v1YQ|0_=9na2w)Y|w{umvj9kNdB7tpAT$Q9h3#M@n#5Zf3@u zn`11VZAEzCO&ZL#ZK#0daQ>4P`rhxH*P!!Lw*7O+GB4`zKTVinH>;KgPdxp9Ji)bE zFYuqpYmoDd)p3l^qQ)U*dZ9(X_jl!W$XjIF2YB_xQjQdB77P9L3)dVYI#I%sW;td0 zSu1hSzp_)dw>WIuED1%RIR+5D=74zfSW^lKr2Y{C#Rf4KS>oAv7l(Nsr-dfPBUtdq$a5;nVB;WG@DGF zaBekY+7xNp=7OkYwrHrCim6$drKwqIl1qzbIx^?nxBhK0m%kr=oeS zPxt}melur(xjwfjYUHU((pWV&pIQ%5Yr(p77Vf&a4hB!0{9rjO*dVhoB~21>UQyG+ zaZgbrc=85<&fXfB+c2`OYHmZ<%_HmXt&-F6eyu!Rp2a+jm0h(Sm?-bnGBHUm)VgDq z+@_^twrtQA;S2Jyh(*{>27Kc&Uq0bG;y+4S$2FG|MLIT@Pm7N9-XywV^V&0_sfN4d z<;b%*N1l%QOnx1GYd3a$bV>2lf=N^EFDx&bJf^t36doaBD|!aeQ%p}8JuB#Who0@V z`Fd`%>;6lEO{v?Rp5Q8a>7p81(~pnZ>aR6IZ~?V+@uFNCpMy)~>G*7Y83bYxY69XV znVFDpEQi@qx}f(Q&`G0M0rSPn5IGB@obD^&=dO4ahUE|hN#Sd5^L4jbZ8w9nQ)yUR z-AafdDINL+NKFx&>F8NCoOuDU8UiI2vB;5I0|c#9-5RoLEr?pERPrs^sdXUMLp^{7 znsT`bK#5M;M711{*e97CSpyZ5A2>J|qVyPqX78;>5#E74K&q5A?~+#b-Q~XLa+M~r z*&J~T3>#|2w;EddD1eQ!zD=g_zUuf}3Ua!REv^YT0o)4YnWw)EVl;1mJH$kI`yT+N zk})wpvWgpyq8$(;yuc5^H!wbL_~n#nIjwCAI}STl9CkT|VoP9$Q9w&Bi`{N!>)BAU zE#NV3k3~!I&_1$gsUF&1i1{~YjQ<5Cad5U2Yk58(?YEcXz;l-t^M zG!BQQ=LoDLv%hl9CMjXn+))+lV@|9E6$RJ*ar5VEE0YkO$rBuqY|qf&FaaZ=Yrt^Q z{jEjo=2pOWiX?!N+1;g{WNMDvKLz4@=uo2d}0zZTJ1=hF#Ps}c;m|c`Zlk%e|TpESzSGgqVTD&B; zB^4+A#jm&qx84GjZoN{a|;@LX06p2Wh3wzVUC zfE#Gr3Rz4A+NQt73zWW5t%$;plp$Nty9P!o1EO35qDesu@`|S#MUQ{3%2O%51D?60 zJA-J1G(&t<4oJdX-# z|7Z=jOITz%>|lFk?{!FIoOv|N2Xtf|3`g~&yDBt0P+)vV>5B0M~#P(_WJGn#00 zFrt}kbZT$pIZ^faF+ln82iK2*c-T3Q=^`JGczOl+m8W+zp7Qht;d(dPgK>k~vGy(; zBHLXXF#d=EwUylx5)+R_wrzL6h;w> zJE%*6v+GIR$);F>Bk-ulsXy5qQWDC!OL0gc=?mrj*_QbWijiJzG{|w?@OU~(c{8sCm`3#mo(_a58$AhD@G#_l_dcPWZ{QH(`8B>N%4 zJR~F4LcAEqtBISP@hI*?F+uLlNbOLDYS*y>W9h!%)jbi#BpH{PVoWwydOLz1pB`YJ zhD{<0K#~3=Kq1}IKo9O$c1}T@i8v0=-F&r?tAOK9XZxwNfch6({c;o!;yeb>CPb!+ zX{Ze+eU+;75NbX?5}Li~NLw#8NzKD3W|(3;3yAOtika-sbF3=DypqgBb(i=njYqqZ z_#4#&p108z4w}#3q@lt@S2EhC(6dam%KlZM=AKH$)j*vDUe(#Dg_kO7~fbW@RS?Tr``BJ<3Q=QLpKsu{VzsRlQQ*h6FG;= zN`rKS;>DC{cE5_;BM{V;~%!HRwyn=SFneZyAgPE5^nAaSPVS)JS83uaY z0a+McGQuz`!!Tq_7-CfjVm0FCkdmZ%t2`m2)`TM)8^d(fgdof$%KXBl>3ID)UzJt-aLVswX zccR#3+SbH`?jqkxV4J*`$Xvo9!jjJ8fZ=hD)yF9bcI?h*agdjO~iS)`cfX zg4sIja(kSQ02E*q|0?v!G1+O|rol`rZiLJ&_*$ibKf39`28y*VPM9lL{WN$+ zZp==?Z{)%3Ec{lAo>};v>_+dCa&*rO;}nJNd(;G}F~iM-k9m!7y9dnm!X?z9-|d_> zM{)@b*3|({_IHRab>Q0n0mT_Mm6jf}%cnRZ*h>))UWbv67!``f2Q!xtKANleY>49D z>utaaRfPb1`&0BY0!{pIcUv`$c-V8O4u4YFy^S9<|FEbuE`L3SZ?7P1896pVMuWuYmg~7CU8YKLA9aYlsS=QcXj)U%&ISjSHf?6vQiPYD*CcIsM9@!141X# zE4WjAqqPD<^FPAyL^?d#qJjN&45lwjyf^&79+?(8*(6D8(NYm3r+u`&xxTuh^byFc zN2LkryI^=d)(+}Zr^U8{g7IagLnlwYW&G6gf=Oj#N~cV%ZF1A_h|D;fGh|YU#TY)R zytpjJLob>*rL=gA9G!QY9tC5jO`kfhw5+UlWnN8$oRQy4j>>OWTa&-KBfc(+Cv?F# zM@d*9?G4<&` zBERD*Aw(@c8Tb$3NBDIxgz#JNK>YLPzF70Wku_eov`r@7OU7l?D_n4DL4MrG(+eSZ@l4UNqEZZ+JY~jov3PS9i&?y*rD{WI+4o?)u&_?S zxLFv4YI4CocTaG^nUR%C7M$r=F|u;(lGE9y3^j!&8C&(_U3jYFlt*<`ny(AhSHpd+ zDM`)tb*2op#g|2G)luJaTB2sgETVNd?n~-aKu5(L~!h@vczM#b4j>Qb@T9zasn|!*4x) zTgy8dxpmwAg6P!&*crdj20RQ|DjMRCqp3d)NrV<-WXP3jWpa0VQXNXpF;0rH=Y?TR3(6Nou-HNtI`h^Eu+G-tdRVnV$d|-yJ-=__WIjLKGX2CQDVjlwQr*k#(YE%GASyM4XXGVy0KpCN)2$X|r-u`(_bbj(}_t zK{mNm%x5cCtJhQR3R{Y|YQ-0jv=4%mcqJ5`A8Hw@8ff z!j-B+lSbY4h(zhdz9IbmlkXyICcw`aA7*;7gH3R)u8dM3Rm=%dn`9 z)HG?x2g3iva{kn_|KaL9c5wRIoe&QPG z3FA>6#!n;C%$0mh=&Cs`C>@2u3qlW^B!9;K=Xg|~Ud zj22v5E)quEBNuBJC|=)(}fhB|@) z9k!%p87*2TbHs5Dv2us8PN;Cya~7}?Ws?yxw0+ocjm};b-qq&RLoa1W7 zB0yx170a$vb*bp$Ju}E|5zDGFL4Adt{p=GmhABWN7MPKbjKi7*~`qVin88VNKd7vxk zxCgqDPJ5vKw8f2eK5caYGcJ+_sJP36;eiy?ThUb!5%E|wNLw`6waAI@+QJ@G9Y5i(<|Zhnk>g~>u|GkGs&?eZ&5p1rH>v*dJnOdnI(I?EBjWG z6Uf&ZDs?;ejll>C3?bo_5D76bRZcW}`u|v~lT_c<&BBw3W)Y2}a|8dZAsIta{_)5tdrI%aF&cJlchU^-!Y1s+mnkeq4G;?@Sl9VG8$mr7Yu zjM6`gF8O+M^!DN*?;{*h%JcED|fM3VECs)n%hr67bl>pHxlSXN6ah zZ!HA*X5kSP?WeR=>?IVfCb`D3iZ!o+W>ot)jLnNFBi539n(VwX;u+E_(2D}kvkp(# zV&dCh6w`AKlf}c$A&TeuC>|IS#qmM}$BRT25#<-QJb_W`qB(18qhzg*;MhR&CGzo^ zZjR4-LjZ?2@-mN_SElD#b-5`bf8kA-gQS#O2}bfan+#SP*sSSQHwpAGc!z_J+(JRK z%2CN%O=hlIc!ECiGM^rGB_na1rvS|v+6 zNF~L#h-y{Sse>+=gS=s0V!ki5)*mxhGrFEtDdw^??L-i?rB^{zo=e!xX8>(%6ya@6 zU~{7gRWyXBu)!XRS*5m_FqyA+Hdsw1XgBYoYS9N*(cJqAjo1|uDUt7~lbr`Nd7rMS zt%Xcur4ZNv2jc!^?WRwvE4!rA2Wo7W3E>afqEWTAkP2xZ;W7~MHs_E&%^apAk{?rv z=V)|=O30hb%obbkd5-a!2T9N=TS9&n%4u=kG;yaD zTX5zU@GD6}9@nYjS7k=%3?gR8&&;M*HBBrtqs}?cEeJ4sXdkn%5jZWJP1F7TCUf{UoJt{bAJpM$J# z2wq30Nq)^fppBW}1OPzTZH<-hqF84ds?5j7ITsPF%@zDjgy8>{TcbvGOG#o4XGotM zU7=^y;%@25CJ9np0s`@^db3;cD5iZ!@*LW0Fd-mDkHEWxGX4JGJT=B;ym3JI0z>0k z@G>zH`6?@}*IgRnxt$w)1=Qou_NgS(VTu zT{HfmI`s&p@Z=x)c!qYH+l4bv z*pq?ie9Id4*D)BcXL+kR%^sN+I;LPIN8PpzPW!Q2y1u$o@h-?LNiB(RXN@k&@-Dq9 zedP&#hsCG`dDoPm$SaFc`|_KrW%=F8PvyUqNfqjm3GL~CdIQfR>H?nURp!L@bWs)J s*+9+1vya*`vAw)KS%^E->517*zb+P{fV&$${fCbz=siJ*coP(V0j+D%<^TWy diff --git a/src/test/samples/golden/clock-debug.app b/src/test/samples/golden/clock-debug.app index a38987c7cafc1a8aec45f840a8d451035de10f0e..88a88a725b2a08d89f1f555eb96703cf9a6469ed 100644 GIT binary patch delta 1041 zcmaJpcX``5NIH#Ray)o)*^mmP59J^3Ms8mNsvgPzlWu*^r09Rv{8%| zN!ko9Sow*G8yChBH^u~0G|{LFH)4#R1WhCo-^Lvayz@#CT{y|PbMKimXYQMMpN~Cg zx3zZJtw{h_#$m$Qfgp%VEkNE%y&GpfW0i^a~sBqomUJ5YLQ){t22 z$s`kt)|sA+FUdy8AeGHbAx)%=S!pBDGas!Zn^}tPB=zhWHIrRzkxr3Y>`LAgu`xrw zMC|NZ{-=U3=o%_u^*auY<^vP1OM>TeeUR? z@X(-B79$?F?1hU+ZbTeLxDi3bJ;W2l^R!*txPJ63rhEtp#|w0EWH2au>1%*f+53cA z0<;v@w-f>RlzkKhLN8nw+z{hwUp6(;ZKyG#&{#xJx1Xv6)&?B~pg zpuU()162`HUwXk_Ecag{i;q9Zw- zxQ6D9iAwjMMtZ3bgaBT19uUfV(};>U*PNdUgFv)kGpK<#7T|9ngdoeT(h?QI@P=Qh zGQbt;O9>IUsu9Iy4S3FtL<8Pnkag>A?2dkqZUeY@?HmrZhNCCllAM08r_@rmN{6J7 z-!UYGha~wz#b2z;E9Vy2hpL9!^6Y9`C@Q;yL9eGJ;12n{o=yMIX-##vhTLbK8Vh;I zCNVx`vl!>tFN_~qjmbinv`jQ@CspjS$wE5Ww8>K7ssQNFvISEexy{PVmTJQmfQWii Q0IeswoXHvh{3F%z8yzzLb^rhX delta 952 zcmaJ=-%Ha`6hHUsopUqX0#j+hzOv2oM_;G*kTA^hXB3Q$uldllnbTQQmrjcc+$f=8 z!t6eTZ#_j&L9p~mNLT~~W<5wkp${P>377r?JNG+hK6T)nd(P*6?&qF+@Av!W^429! zYfqc|8362|IdJr%6ZAS0K>mK+fuppC1=+D|KYumvaaq1#(HD$H{o%>jz}W54`TpAZ zRM=6hCAX7p#=y=j3eQb}dnE&1qtVG2%)NClZ9>G)^L-&5ftMq0th*G-B zpeK8`P)UGVaK6F}Feu6>64+IE%mNUXX>T@F=t<-#NL0)OdF50Gu(n$%h~v~kAkbh;u~1ZqwFU9i13xv^#H6-aB}SqPZRu*+B3%^2ff9pS zk)+Mj11CHfy?C<53o*e|jWNo>i(E7&YNCFhacrz_fAhq+Kk%ytvnI@cTKRE2p>R=GBfCd8YR3Hh|2x>G!%N zBb)nwOp#+Y<0p27*z6ORX!a}?AfSzm=`i~g+gPI$Nt}@@Lh(C9g5M8O>9rS*%u0VD zn_2NJ_7`GV9B0E^ci}4QVm0XD9n6j~et_-49B*fB_>#NWVO-&J>>ge>%`qQ3%2ejZ zTVNdYh4%VH1H$ z2I3}$eH1D=Ood-lHGnL|po$U{87x~t8iF@;Ubaqx6er3I6O~LZK$&)APt?S!y6hsLztWa_k{>iL|Vb9c?Atf|^ia8yl>s`}Ns zf7EbPFC`Egye+lGY(PutN+J=B^u!fyC>p8$PtGqkbeFS7XmYyQV{E7R90w^bBU1c` y1*e;@S@5^>AZtKplbfByaFbgK*8x1h8%?dd|2P0TjYk3KJ=GU-I|1mYGuB_eU)(1E delta 818 zcmYjQ-%Ha`6hG&ycU#kP_QN^zVb<40EnPDUdKe^XX-{J|+e6c4n{Kt~nsY@4Dj79Q zn4O34sUCua>S-oD2n`kyfl?2VP}oaBk`Vm`cJ6mudEs;J=bU@a=kvW6zK_nWcDp)! zozBMqu*FN|-OWJ8F(W{EF+a<@T+{l(UR&hP=Ug_^2Tu9}@pw;oEEycRK0G(jG?xjh zRo?WEbf>|;F~h>MDRAcU&^;U|2bCE}_gP9F`L4)ELGcIYvuQ}2;Y{N=wP+GB?dX*zsLsQCeROzK+!zTKw%wvX9 zW%GD~Oyv=D(arL8-5L*ub<`(psG~cgk>fMbg1YRws8n#7e(8VXHTq)sY(6uRh_1)^ri> z3qMS9N%TodPtEpb$@*c^Dyy=Ad3(3#Fe{WQ+eaSMS|;SgB`_gK2TdseCIzN?-brup ziUFwR1C9u56(BCdd(I^bGDA%_#2SMzX#!~T>?(Q{YFyHEhl(AztwXC#Xx83;Zedsf zC<9caxuN790W^Zkss(muRdEFhX{jcz!m9ijK2sPVqHxdP^==6m2#h9@70?xm#@k0? z@klhZ=YK+VmPSJ`bz?Lfiblz0IhDO@Q51T$za?8!@6uzA&L4E(Tbkzhi559-(sz!3 xXs_x}sw${cZAJ$rRR@OXiRutTW_|@)Q|o2v9!dd_T diff --git a/src/test/samples/golden/fileMgr-debug.app b/src/test/samples/golden/fileMgr-debug.app index ce8067516acf5575d3fc60e2b5b369ad2112b2ed..15f16974c8b8126ca6a907f553ed5f0ff99f2917 100644 GIT binary patch literal 35771 zcmeHw3wT`Bb?!dWoO#J_V_t=1Fj%~dk<253K#(krZ5hiR&v+z505g(~tihuhWoBgK zhBOw~4T^Da3Z?Wy0=eZjuRf9{DdC2+ZXlTULXrYWNtzInKqCt6frOF-65Rh^d!I9B zq!G|M-~I0Q-OR_a&RKh{wbx#I?X}ll`|LyamSp<@rPN{kmf>#-mxb%txKhpL`F#B4 zuNu*Rk2UxAG%59BSgB`lk)FQ;xZj!^8|!u|gXR2KwNzG5+W-yZD!rwl@q)9ylp9Jr z6_r9QMC{Gwi<+(c`@r*a4+MYNkCXf4vy;i_mCyYE6-+OmeD;PH=Il#G?PH@#J*3pg zZtzJ7Rch)lKybr@3npK<@`3$dXk`T-p8~tR2dk%M_bt(!_dZsQPiDb=g5pa(J8@_5 z+YdgVdWvJ?Rki=GNuOnXQQ@*LyI}9ZhniINYG}vK41k)aZDjn$H(2)m(AVQ6o-aY#5x>vdR@wyn180cUfUbb!}eHKl`h# zl()YxGx5TW6fGIur~ZTrz}@~W(ah9`U)TCc?%8{A{+fygO-E|#Y$;BlsBh|Ije00a z-Kf<5UNLqon*Ud)Bfb0seR*n`lhx*0W=q|oWJLC zShsHK{`Wt;uOpP2x*Bx%!OGO+SKAK)?`NMu`Jv?9fGDYU^2+-slV73`M}gDc$0m~p ztIaojHMBo@_hk0LL&-;oc_{hB-SX#qSL4q^$!9Gn<4Y+qslL?IYroQ6a)>g#2EkGF zz6E<9o4@A-2-N<{{wqH@`IYNnS2TU$@lMm{!}wwQmY{kkazdP{!KNfX`&lm@2H+_*!7Jh zO-(I+O{R)hU-K0({yADza?if^?@mqq2ym!&@?s9#L%VNBN3=n($=!G2?()m7ftmg- zP}r_}HH$q5$G3n%|I{Zm=^>k>{AqH1M{<34w|{q!xg)b{G_(EA%k2l>bJbRiW$K+h zzncULKs&jc1kc~He}}q8dzg`@0Unu$Hm0Ud_ZfT%&r0*S1|06xoo>*YfVO2{c29ch z7q5y2o_`cby?1ZgcOr+ZU}dJhhqB3YPnmiGf3Ly-?VtJwgMAm8nwq*FPuIZz`6Q{S zPYJ?N{Zn`2W((Arnz~KXJU2oQ^}?J@Q+o~IC75{7{T}Gu*qvRUOq88UX}mn>B!+Y1 zx~iqbkW+EW`CK9YyUq$V9j#cZYQN?csqW;e>>71I`=kBIr*C*6aVSYGseR+o)W~C) zB>JXa|E07XBYIo^$1$+_XVMw;zveJ?mo7wCPX0o=Vfet8?gbfqcKhMUrW(Aa0!)KyRcEHB zE73VN#>)O1KlVCjRn#OuK664`T`G^JtNpoRwX(t4*;(FJQOS@}w7spvWyew90p?r` z;vrn4@F-8<+6gWv!XB=~RnKTGri0C;vHYOg01i?%wBuTtyjGR2RdhZcfu4#nM|(Y} z7K0sX5xLUi#kHkM6~)zZX_xu~$aDp2^@z3DX=hu$QgzB*)q;8ia(ShO*ehOL9#syC z89Ho*IGFm=PHsru2K-lWoq1e#@7b(Ni5)lpWUl5S^Ud|OcbtVf0_ zvSUNU--{ZhMHS4Gg*8r<9rzch@=icKX-!+ki<$iB7-rHe#HcUxdges1L!mUH*Qr!; z+Z<~3all^1m4NISPe~BCP$&(miy*SlRElHee6c#L9tHFFp#Be{R387#muHgWk(I(?Bwz{G&2BKG& z)`QX9`_RlqxYpvz;VR(z1g@{(dI;A5s1D%z8m<>{h0&l>a9xV)DO}IudI8s~xITbJ zK8NdMRCX4wt8je|*8{j7#`P#J7uUfmpG{|{)u>y6*!-(a)A_t+ah`y2cZ45#t$ zc=*NN;D5j6|HjJvt$u#1pEuUP8<)yB>e5;{|_2ySKX_&^d2pS!j=B!GnCqf zMHGJBeZ*?ImEq{;FzQ(1goUmZduE z2bBGwdNS7BwonlJ+)SvsX^m0~o0OXkEwa?YW-j*@hSUQ5*Q}m=x3wzWL$1w)Jb8(S z+!q4dt0eso-W};|rW35D5& zqj*2gdVpmgQf;O#uwWxwRO+L;k#Xhj3Zd!i(#MnJcgYw`bIO85$lti{3#C2}(cRw* zEovg|VhFrD^fBcr5bBr+EeKQdTS=kZ>*Y}q*?w5rf3NI+P*27~C?%g8SZe2C;Gzh)>kbyNJWn*`@2B7RUZCX+22zyxqm45-&giic=#ilFm>j&dc)zv zXqtOFo0>TXsRB#6|3fH#pzNo8q3#flKhzyQ;od2a!1xh(epDWQtiluSU8LNrvhF8o zj~>5Ii8UZ=A-yz=PR=R|nF{1UV0KL%xOtQbI{_)!ZO;8P=?y;2+I~hoX*HpvkG4Kb z`aad9jU?-S)(j5x&R>N-WIwBR4`etVEeo14E zh?9U}V8ARR>feOyf8k(+DCq4r`zNaH(E;_h9z`laq=VCtrs|R02Sn+@1gxQP3`zGv zBLv#9$NgHpM71$+D1;PJn(0&b;ZP4vHN&PK8);hp% zv-|B3Y!mJv>wdo`5iIy7nz`3-@sfUrKxdbk4s4rnp9+vY9w0-l?5fLY`}hsm&%w(E zdA!Gz4BP*zG8D4O0=pjs*gXbz&l@QWyQ>V*MAm&i1aG|%O?^Rb;2qqM{eoI#(~WGE zOs7%~?l*bD+W)57Nbq9F{&$1dNC?|iZJNs!rnW_4l^C{P^u^?C@KXx9F8w7x& zeo1}ZLQCD3&25uPwW4TI#8h;pVFhJ$^`w!`Qtm4u`=^HLm+YSN?4PL{?3YD{yU*)_ z-D}8yc#s~9jhEe5LjgRHO}MXVh{glig!^lH(=^S+eVzMupt#9?g+tV`A$IyKmNePI z=;tW(EbuM_zb(bX$kakrU06O`NT;m zzlo1`;AX$v0P!PoW2@BXS$-FvFw?mox5JnO-T!IZzfgAo?N4-_cYx(RcH|CV{HcvG z_ct;El{yt3gLBMMsBqZ*gx$oHA)1t6$9~dodYktM`cK|4XVsIR09R_CMCbN zwq;#!Mcps>pe8F!z!wG3Y~5sstfo(cYQp^zaMkyyod0d3AEBs@G@~5%%l30HfdTh# zd~l55zqM`KBKiY_M>6(24yJWCV*-9ab*|O1$AhdVa|nW2)_{i-br1RSgstl_>pl#f zhv5yrfo`SyMB~3HU4^DUW@Gla>hNJO!hc4l$L-W@6yn>q9kHyqy131bT5Z(SlQz`R zv;LTO0VYS{(Id zFOJH(&)5-~*Ryu!w7KA=+b zaD&9h2)DXM92fQ)Araj1eDtkE!c_MJK><%6o^_XoZ${gHfsW-kbWfJ1aJV59nP=S$ z6j zpa<|DqI?GZUBJOTAPkMcv70Jyg&Cu7l3^I_%_y~G5^Pgq{VIXc>wtn-aD;rZ2GWF- z+e2DZ(Ush4{)fb9JZHO zX1ax5`obth#hqzx&AI)21k06PgyU}EzLZjTgy0hfXaKrHPltmcyrl_Vc*scKp=TT? z96>L*!{OA$z;N?nZ|Ge^JaBTivznWm)Xi9k+!g_ zj<&RT?Pf-&|E&6@$A|+P4`&w0j#AdWPC6Za@dFeb6`f<~#EDL@kEFX#r>(2;1yMn^NxZOXeYAFm&qs~NEt<{UpzhGx;{jV@?K!)4rINFWE*Xg!?-Z+4B zy?%o2yI-U(q@N_#`fEOV9!O7%_l8qxivDHa((d==JlX;&?c^(-UAPB1urq7%4@T|Z z`3?W7-wzLX{g8DZ5#hxvE%!{G&HlRA?DW~J`5S(HC(;lm+;94T1OZgE`;VRkS@*kP zot~iI_-9YK-=k}SwLT>POhZqHZw{;UnXKT4Ml^!V?v2eo@P(0h!z~LG(BAyKbJ;%z~iF&wty~rGZ=h$12 z5^$dnbInf&I-k|NK;Zz9HKql2t#EsIi|QlV?P2}?k}s-f+dm^sEyOT}zLu6e-MEsh z<7H1YDVh5NU$AhnZwq6oCxTRwoP)3PJ17 zHzu)G=P(u%0pPPtCvmjuseegC+b{}!&&i>DY(%%PL&rtOMX;o&?>s(|I+uNUBFjPK zxqYa4*iA&lTP{Qw*^Gx7o}e42sE?CKfX)NW*+#f^Y4Kl6BbTMVMwDfg#m8!>vM4(9 z)Cf}-?9306>a+;tvFV0RqCwA~r-7dy@jKZ)!^dAL_z{%=KVmJ97{80Qf+ySr7fS9~ ztPdrS+*50sQ-Ql8!umjuKG=eG9*gd9&ozpvO;qO@-CCR3_Cbggu=Tg@7WJ7VA z$flQ))9Arx14+=fAfMD)7<}~mwO~?c*1l>B^-E`TIqBI=+KS!-E89?BlWcmVDcQqfOF&K_N>Es&lAMjytn9XpQP9%bU$-3L6qoAs2q+AenuT7taKkgs02oaG<6yL_jVT4EPGHqCRBbhLtShou}VqjVKu81G&{I5u6 z)d|2s<)4NsDgEb&9oh7!0LEj%R(#%X1*Zd)=|)HuU-TN0;&KONklkzjy^+ikrU6uH zlP+TmI?|{`x%WjPnMF_y^6eZ6Amp0I6n;4p_6nH?M6?i3-C+hN1P-2m%fG!k@`l!yx~UNaijeE6JH^oS#z(wTW1(@E4KP1kl{q$XW-< zzlzwWGxwl_Ar4eL1F{v*;_}7_)9Xs#^nLTB4k)(X6@A`c|N9eiVXEZ^eRWYA0Ks1!(9G z9|Q*SV{SqOh5$>W_Bob)uEix4UI0|hl7W!m_>?Fh5fJdFmA@>SYQuwj8pQ^`4&4E# zM|I*%cfgq}WEyQn)K3?m7p3V*KBF#}I8}!ZXsd*n=Zl)@vk=V?#64i1cY ziEq*AUl8^E&bz(FT;g>@XVgC5Vt;f+wYNRdghP4;>5kg(km5DGvERw)V0!C&X10DE zUTX*CIn&&QaUQ7DcP@r#^^H_GsFX zXzB{mZjYiISZIV7mdONGkT%yPQn2nGaEnol8?gitp%hIaA&Wh_8XGSL)G5FU@x7%J zR?gAXvlM+N?H6^v+go)p#rVD`hS~-8g;odSmI1ZHbYhQA6kKPjM#(nQ4N>>~(bTPE zxSO;nxz;cg5jV1Af+cwD)V6y)AK@jm?e2-%@3I(>d@#x-^+jBvw%He3W=Z|-+LHRl zXi7|WAEiJ=MI+q0v?Q-?B0bt~;{4T$=Nu3L)nxY>-AHol6f>j{kZ0CMlFbOu8 z;#B@5YIj+7x8)h@J<-&C#Qvlx_)Lr0C&>6IYU)-?;{VUE2>xj$9A}5#eEqYiUJAf* zx_@bgyEs{(^<&Y($iV$95>57b=qgVZpT^fgcAhrE&Vu-rh}M`k+huTxReo%T1PsRNh;-N&Lb z6~cyKvEE+y94?C<*V5n}eD{fHWHEyLZ<7%gaTpX>J7Bqi6^5*Eb7&`58!<0;_fJu5 zeLw@jZS8ehwNFOF;FU^{L?f*A{F&@~e>aNVA(#g01wT$+F_k)?CoilYpP~#hnaGg+ zfmrM!)nj-d6E*BUZGisTAcO(9HP^pOp`FCkihS*APQ%dZ#imB&5>KK*yf_`p>m5viYnZ_P znMPQv+4RV`op4`{MlwG}z=HmNrM`05Dt*9OJ>&jdQ?Jw1ms%)A&Cr^1e_1P&^S9A{s3n?kBQ@BM6>y^Ryw`H$u|){#wcBrrrFa;?ofo5-HhBAD z9Hq6lFQCWyF=K+h`9xqo3u9OvL!?#OU>C*0@MI~DWn!bO=}DO5+C?GN!))t1EhMC1 zeBmA?=(X1~uG6NA*E5cdVPxy*{{77tR4Bg7{ezM$P$~;AG zpu%Nx1A9CzHX}$ogY7rt-NMu$ll7T(z%DJu@|c~m?9ILyXU9?)aLQdtWEo@}z@Gie z+<8u2kw3yX#hn*}3CWO=>GS+p>N`}(1yV_taY-9DXz(`}pK#)RR}7H)=c?gg+Y^q zkR225l>)xZ2eSbHUnStneK64h-YVet_+VMZOt@DI_zDeXx^y6>BOlaVzacOfGaCX~ z_Znkpu&?c&eHA?W8ujd}C^wi`#-$jWTn^ogYsOLfwQc2EN{^O?YUXUx9R;*2g1 zbyv*3icju!0VZ^$5{SG%h8~r-BqXLj+6vlXo4pka1uT(d7XCm0Rm%c5dbJ?9*RyYF zHt;WAs+@hh$Mmh)a@`wCeV7JwBf&5je_Ml7#D`+qy&B2(8JsoVq$MlHn;KCbFqEA- zZ~qZ0gtlsXQGi|s-69Rh>F8`A5MlkXs2+0N5!0sck2WlA-H(zT-M&t_e2kBNEQ&XV zVt)SO&tsW9*J4;I{Z<F7x;@&Ldg(UY(EdpYM-lt~ptw{$0$ZX0q->vCRA4uAz^_ z5FWzI%F1ehVhyPK%=|Whdp~&1y8jr%uKo?!82)z5%T{FFe~x)+jja20%p@wZ?z1tI z3CX(u8q3Tv(z>;w_Hrz9C*Ny>%)gBJX`$D3bCB2ZQ#j3WvAG~98kbdL5HvR~QBM%G zD6TiqybMv+Jtl4sT49@`+vE0--mn)Cmfrv-;nVA+cGDZ;m}{h2J#(1l-Xth` zW;Q5kK_L^3k`)v^K^v4lL1D*fl*C|AbAoUgBnb4Er_4th8F`1Ds> zt}!jZt1WJcrPEVE(!kD_O$d|C9>(CxRm#;C)m_-eLv%c9=s#p~JDj<}lAfaf$g-Yd zOiJa{n^1X2N*59?4_*8oK>^=1h!eDGyjm?4D?-*aQpyiHhO8>c^EH`%$W)!NC;?ri zLa7|#-Re@&h>DvHIESZ2#X|;XjpukMnd+{B7CcqP7Zs|M6)^>xR~oC#m&1Y^$0|Jg zw;4w$eLyb`_z|$s!|0ateFQD=LFN1oXFZO$&h;QXg?Z_KkHE)OyQ)sb=d!s}%@qXX zBXyI@=3QeozN<>5sz80Ro=Sf?U(F2^YDnu##cgbL#b=ZmDV3|KTyw5|400lL zH7=hu0hGE9Koz!ljQHR;i4R@)?nTwdCnSS*)&W?>q9B9S`s`&L602PtMC!rGY97(7 z!=mlf0n#}3SqEUJ`pu@AHsF}7oNxE_MQAFA)1fsn30ham7kyR)WFS%3WF;?X&4mIPe zJb7)EXJt!8pL1_+yMs>kjXW) zh-cD%`vodyU1u{Ch;{gtdOhij%wycuYR$qk`q?MInd$s+-l-W45ImTV#Ia6u@Wm#e ze(Vy$mR0=b3Pf9<-xWMHofEI*vszag=)&>uK&JxIIW&}!&nE=Ya2^}wOdSN#wEqqA>KZT8OoNotp6*hyX5?TeKyhA2d;LzRZm7V6^<;}^ z+fcBcl!LEA6svxJgIF{vq`}$+%G&+DWIa%GRN?%)oY8Ml{DeW5{yfg&;DgajB`Uza zyt8wze19*gZ=+EV=^J)Nm+@iE? zfqw4I7xSaA8GJ-wp@wi0?Jfq!MWwf033ygd{Y9MO7d6#7fS{fs!wFr>XYJC0L;*z_9L^d? zX>kmsa}fR#gVMC!K;?T0_3SUr7Yg;HsdB!ADSDUb2SbV$dId&a7)DQJ?Rarp?YS52 zF7GlGbdBH(1&*|EiIxq19x(%gCS5dS6jTFqel*mZbCvDp%LhjDdg|{f4wvR=cFAk= z)$SeF#?;zUb-S|*K4*@()%uT{rwz`vRquW??hU^+<kHb5;L#$2yxIFkm43O*H^xE* z2&P7}@~C6LAVklF#$Gf@xy&9kb|9b>C%|k`=0Qw3H63)p7xhSFCNSQL+@`DeEdAq` zLO+2rXvZ{}m|8d^y=FDGEAYsO)`-s1v{Nc0_%SLWM8phWNT9&j)vx|iXID@~Bcn-094Vgm1LMdbd&A181IRk+(6X9O zpNTR}J?l%`jIzKC{&9P?G*$!jJB7j0sADNCwJcT9!HxG3k<--%e5c4Mmd3Y@Bt~Vl zCh%n=49jvAUt@C;r7@?hKgD*bO2E$fS)&0bAzy4;o+y5lDa@DZvclcEk98BX(hi}rX%9U+3HURn9NExe1#(AQ4 z*3zO?t2swi=XkY5aFmS{jW??^R-B=iJ!l4LZPlM2s*d2C+914VF^t%S9T!sTo#AS; zLIeZRM!5pQ5OJ_DDvXa7E3i)JV|n|z*O5Q|E>Dx{IxZ=fx3^vA#kI@Z&&Oa#oXd{` zW?)By_)ueItiUfTMpdd*$unHA)Cvq3r@9&;OJ%7jxn|z@uua*4-DCDkl=Vcr0@ENy z5-ogGK6^9BuM1sbsk5WB4q3)p>cWWX%UAN~K}%hs18i?_CC*BS&NyMTm@Y{S;{hXE zgmi|u1dF2OnQE>&j{ZtiOhMu_^pSMp=^@3=iBNQ+&x08v0M;DPtT`AcGAuDvi-~j9 zW{g_h%ZZ^9KG?(FbgqSF{n5%*IR_(4*vY$AQ2BH+A;tNSPYm{BiXGBZ9CEHjV4ACR zjpPeMh^b*~`2NzhV~8Xj&P+~~?$T@090kWNsDw&)@@8`mBU>bo5J8v0>6UlUgGZ^>v{Nnbx~???PvSw5t?Aox*6N=1$xNavyLn#l zabxN-3-91??2ZkTa^)eZiOoc+g)YZQSn3jR(D&%LI%Qo7YWOt!4bHx3+Jhx3LE9=BdRte`&VyV-;Ec|@F}f?URfL?rp;kY zFIxm$_t}NV)u3L-Y|b4v-FtsEP(l}LCz{B~kYhxg)F9k}Zf&ACY7=@=?2PSZAC z#b`0+4J`S}rjwv!D3=O}RIcb0RxE*Y2@ZYo5igAnqN?gNn%x{n0;{HQXgH2{esV~} z^osQjda6#0U=>9>GXf=$Z9&aryJ(7C<04Eh!3CHfJ@V}1KltEpi{2qoytmSWezLQ`~Z>{753t6PIOR$-XV-I9Z#KNhCWOO!;~$D zD$H<22V!)gxhv+1rSKMIWlRIwm@p7p)M#;>&(}U(yXz zsy$#472?e6gka!fyP8#Ea|x3T?Y4s3mY^3oGdS^&=7#W9FvygzRuV%qJxPmVqu}*d zj1)|O~u-;7x(%H-C?Bauf?gQfU%fcS^OdU ztwI)hnG21L!>1r!ZJ>NvkEOh#RsJY4sOrPbPuAupxfn@$RiTXMkIcABf*d}f+Q{<3^ zTt2OxNz6LzC3sJ)Q5Sd-!d4R`(o&6}`os{p*8N(P{`lbLe6@g)8o<}*igY#H&(xg~ zH~2cb2%zf|RJ1s}Ns3Dw#~g$1MH&GDeYmnt7(74(!~l!{Fss$cBj>Du5gZ1#MGu@C zYJ{&(h0WwoduoI)M__?YJK^iFY=DhXx*@kCzfIg&K=|Z*l;THWxZkCfgBTjiK*U-V zC_8|?7ARhC(O+n6FCJU9OHVcd0(rF-h%^L#5t6b*tvL!4qaT6Q)gY&}fJDvng*Ntr zWzAO%sJy^PVyHkR`t+8@<;EjN6+3`5X?*AtVOL!p_i-N>KMKRQ4|FT$ny_uFPCwL9 zyqj;a&G^mtDAts8R9r{18Z@kdSM4{?qrr-~yR;L*V4xu%011WB0(nG>K*E0Bs>Ax3 z1L9vAO2_tSGMPi`Dtf6Q1=}MO-B|i@tyV|#t2d@;sDlt*d2g%(si%<+?wyr{5%$(b zBtPVf_4u(S?tN?Z`AfvQR_OQiwXKjw|F8(~g09qfy@S$c9;SCA0%9y-B<8JSuQ$Fu z5j^&SjebVtTZgST&oy=e$Xam0?kVm-v{Z+!UksQo2}B|Z`t7jxk?W02yn${ufHw^S7zRX8P4gko0m{&)k-&D|QKg}0K zTB-xPI&6K?0%&zVX=xym3k)26P-MDlq@@h@>X_^IDW+S12)`K#y2`LqW&{`Du1~gj z-Q&rG<=E&@cB~HbbEkTx!M}4EUQ*)?CbRn+QY<2c->D7A4mBj3PcqrCE;XR}r`c&3 z%5H8%^OMO^=GP5r7PCDyi)ug@r7_L;dFtzz)ldVna~hIKKQRm5 zfa0Qt6mvx3;5(cB}#ZZ4L1kQZR3!*MRC%Z%oC7U<0cA-Mxl4uC>g@CgB) z^lOQhFyW)%$>$)1r%b`R3YO|B*gJe%O~(pRsg|X}*{T-+i!Kk@uUw-Dh0ldInrWaJ zLczA!Y>SYQyZlIC_8BNZKQQp-^kLp3HI2Mx!66=DAmwfF7gBvrd4OPzAk7XUU|&KU zHx;8%*e7+rxIy89gObj|$*0lfc)SxAl7L(!LbO`d5rr3~_)UI+dNSJzdA%Z%5>Po| zyqH47l1}ivu8S+3c%wKz0~R)`Ynb)G>WfeMDvbf4YoO@Za6w>O5hOwhizx01;nf;p zrWsMUh~!1LASW8HsLB<&SL+M`*A^Qh*ix)zKDcYNnjq{`O$?cV6BTnNB%}tb<${2y zaE*kI+Y5xYq#<650Fde;3TZsPuLnGwsqvwRrSR&BhO!A%c1YoWFM%Wuuyl?58)T^E z4MHeyTw(qj!$uZ%C_t<`g9X@sl3g9)h-cFlkDkKNeCz9A<3ef(DMXO5LJo9-D)9OM z=|&KQoeHx9Bw)5eV{X%Gv85FRg;i;IxZ-H(@GJ}15Y2)o2erUl(+cP^0g)R)HX0|H zFH}sW5H_P-NRtFw!FFlLXr*R}gjg!if8;k8|ArNE^jLQQsGCKgtuVfvYXyY`A%dcvMUl)FG0Zz?kC3jJ%=)u55)IxB_H~@8Xm?MZQYyfaUsfVTJzyeqshL(@Z1uS-p@UkZ}0|4XA zSAYl(sUo)71d~;15YyxUSt#{D+Z~7FO8X5iJH6KE9wsD=N|B+UCE!h900@jJK+2LI z@yU4xuAZEb>F(e-zyew%LxMcQ`MA@Nuo@gMSMY!P@+8)jz6NOM1jUVfOjZjn`;cjq zI^D7!0^`b&ohmNAMRHch0_+fu4^ZiTeS`)bCMzUNnm@tfR*`@UnnYoxr zesTkH!e(Ng(g2gNdf&GUkcQrr1Yfu$>1f}+1(<#aA zR9wx~I5jbDDml?Ky$QrB?#w3xf|$9q0zYg zCN*#<5ENDT9}dYoD1u2@Gu%np7Z0YX>mc1|ks_QpnH^fT6Y=jaCaRDXB(dC?{wXQ-l1;hQbk4 z=dkdMuh0dD3nA-pVIYk1tqdlU@W_uU>ev@b;#)s35Tw@$Wxz{p0L`8g%!w*x0TaDU zt5iA%2jM$8c)*JivlM%frc}BTbQGK=WGHmt3Xmm^f(6WKOM%WpQVdHLLUo0``HK9n zmRK3BgN5FD!7L~Ap+G6o%LEUu$F`=>s{^k@ht@0!E(*L-?T}cCJ?e41OvCs3>(+Zs zAzzCcWoDihD4tF+?IEEE*Imz}rdtFvK#%Os(JVYO;ORx2Vdl;vmeM0Z%=nS{L*s)0v))8xnXYA;JIMReTlx z&um4!Xo1%@0#(P&3uTE!;^M?o7I<$RERClDWmNM}$2UnWS@O2AShyobbr;v*T> zOIrzD8pr-wxeAxfH#FF!B;PGt8c!C7rZMpou|6kY1DI&l=CoqPibiCVC*zE@2dU0# zgs3yNJ{o0dB@Ay82y0JJ7OZ|}BMv%$ThD>8h8*6}h=b1N)^pIXnIf!g#6YKZYYcR^ zEp65Cnc4=t5t~}Jx5f(1PPO{`3N=>LM`xjW=ByoOdSIo`3Iq*%0G+9wN)S_eSc4l$ zYM7rBBx%T;8RZ~SvK0W$ai*;$hw71A37Brxc=yL==oxj_cq{NcQ}JxN3k&))w=@!; z1~!9Iey#-d83$?&HbmHo%&kGPKWYBp=KSZpkO7d>S;1*U66gxSnaW zX2%j6@xIi0E^2CZdN#5uH@Lkrh8NKrRnI29wT^)^W$YHL9l|mNj~6i$tt?v_Pv`J| zRg|}`&|*Ce=vT0`yAa4sMUx3L^0V<~StH(!IWbE=8z+{%bxzFO&&G*mZ;g{t@tb=o zs)2!pUnTV0-Rg(1vzaOL{y~yf0;Zd3aC5*%_6>-yJ7WD*nQ1($+Z$O8xxs3DMi8{2 zYm|+JAXe+l_tJbjul45UQmHVI!^`sn@={U7%-40VpeL{iA*FFiVWBA(C)(oXU1Qwq zsHDd8O^Mr&gm~qV5TA1-#OEFf@p(r=eEyLTzvD=V-+3g&7aR$3`;nR05!qP#5gA$g z5m{OL5t&*05!qS$5gA(h5m{RM5t&;15!u?xBeJ!XM`UX&kI2?m9+9oBJR)0L+1S=< z69Er9C2-b>*(Ecv$g!yeUY8e`w+?0~M!*XaH8c4XjIz?uqEF0MhOIy6)KPVk@tcyKoLt>1C)Li9Fxq0U)W1b^R^xCwPB@fv%(?2b z6hWRfs2A)>;7#kq5e@GYN00)CgK|S>@l;MwUqLE;+@p?0gJvTzheCp5MuTP}Fh@t~ zsxUB3QX50wNHuSv$Nf`@(3PBYn=LNfxZP(j*4Vo7(xvfkXE=xLWeF6;q;O*2WTY-m zyt6HiN0X4cH~|+_6Jti?sJEo2RiS}P0p%&gz;fr!tMSGeQ`<=X&H7_{_s_6!fdUyz_RY)KOfE@SDILz}?ZJRM5Py#xJjZ zVf}lGdH!URQcs7JdJH$|c^$&@mQ1PCXIDnbxl*-QR^J6WL8-BcYPPtepdPgV7|v7% zirI<0y{?$aChdw!AX}mhWO464G&@YJjW z@rZRwM5+6f8ruUd386|%{XGb7`1;(*r>?p8;Ah*(|BflJ+kd#)mNq;O0OjCz)p__{ z)n6!0RFy$Ic+WY+2igr!MTF`=dhY(i_cf{NSsOPV7&u{La_rB6a3DP^Ikp=QiK#Bh zEymmqO+M9m_?ml14?S_d=D)re8!3*La(O!&8y7vqMvD`LY%EuZRmXCb7&MX3jby61 zVquwzm&;IOAv12PvzMJ$QR5T&YA#o(R5OJUTaDTI(kM8ovny9i@~REVfwS{j)jPP3 zKL>I2gY?EKjra~T{1wg*fdI&NG5>w9aMZ6>NTMW%DLbFI{ zzONG}DbqzmLr7t2#G=G}7+Z=5>jc>FSWpP=5P}mnP2J+My^U-)Zaf&jEpcFC-=?W8 zPyo#ZEZg>732WCyJ@~f!4|D|+Q*Qy?7hq*-@@t)kf%ltFApJ=Ec0iP5J9*7LlkvML z#0lWE|NE2i!`0>+z7{+fzkM?O^&|0b6Z1&?L8|yj{3mb0&wcU715n1@2{ExviK&U}_*eAA+-n>4|yy)V-TR4>>xb}@dho12=N zkd#>m<98m3-w83P(S51J)HRx7@~is~PwYD&a&$gF`L;bzo@1I#Vin{8@}8l@RENoC zu_n6V<*w=lhTW#Il_sZFFDH}5i!XT!nD{iUD!%W)+x8@;egQaCJGqh$;mDp_P!Syv zY;wtLo|0Se1?mu9i=@Wf^?7@GR1COu-2l$R#gPbAk*w|h@_nkO>5PBS}b zKi7Hqs%y8PZBy^8`8_0<3);y&BzW>Y@FUl04>R^Kz+)klF)?+n$KY{t$|q zn7RjV*TMgJB#EiN6@(Lprf$c>W~ehU^}6^?TA^6g{>@ENees_3YIR8aql58BZ+I$p zB>pJ$p$<$$5@X*-AF*lb<=;uk(W18wP2LOBel(GqdIG@7(-%!1nfjSdzWWf6_aCjU zITC-23=YO0+n5-8L^#}|Q6}#NkCS2bk0n#6f6Za)Lpl*vIr)NA!|0*A?*bWoc<0f{ zrW(Aa0(6V2r#sc(8|yBWnAv~hVy&~6MNV?@o&DjeVtG7S9m*7{mG$C2 zC${>o9AnO|*JS%H6#EN=tUl~z{)GD_vfOXyM45TicLt(h~x z4vA8T0lQMkY_+M?9{~0u?igfGxk`e-`FwFiy$K=5)*}yu~@1)kzjwF38I+%aP-9DGj;S2_-`hEK>o0o-~Zql&dpzB z7&BEb(HJ-Yhn(FFmO_y{nZdBM2>)x`NO%s1T_NjoP8o10cEHLpoI zrv~Q-)VyYn|KNWR55GCklk6vYGa(OO<|3aKoZFUk|v zvQ{-0|5|j(1}R<(At)JTSwV(#Ej5gsk|w7lM`q3&f*}^+1U_HFe1K)$qdH7pV8KGR zs>BC$AzPHQGKiwDO`b-QHQhKy;@kIKPRs3m|Y`@Po=#Ak+~L z&J9uXTS%dtRr0EcY<*c-|ER35sE1pENF|@uOtlUxa2xIb3^n6iPMEHxrZFHjT@OA_ zqlT#Db;2&p{D4k94FuVe>+9+OS)hJ42IoeU^$isW z05vJ;_bKa$m!5J-?)N~00{W)1{z-#SyXoNEsItDLtZ#$MCP{xlS>I8Be!yfjBpH2G zS^o@_&GPbHWqnUQ<6I&5-&fWT@NyMRm^yPyz5eJ?6wSGsMNQ2@3kWFZTA_GQSr2(a zZ4r(?)D^zhd5gRP<9~x^MqVCPp}o!sDfg?ilcznp{Kmx^FdZSaG>S?dQ~^vuU~+?v ztqTtia5hdr3U-@u3ZysqFl*}(^>CmG6@60RQ>0I;CT%2Xr)nApYG-@!FRdS`J;SMm z;1~!%Gp3#GB)8_KRcn){BE{L|V-99RuEnLDz0&$&7-%qO6ZQI_^Qk@ihP`j>hn=N+PSVFK3BIGUt$qY(n-*yl{vOH^wE2ZERvN^)-0d1o;7 z;n{eAc5gOiMqRzPVVRL|3yUIUrZ$}S^R-q1UYVW03c@zw4${sYHHl!s@1&W#1-E{x zI%*XHoozN1*s<67u#fD19~p9GRb4^b$2DyIY#NU%-INaNQ3XbS?PGKVjDBwNH;k?_ z=zG%%shY2Hv1#-~llf-ol=*(>i=gR{SFOUaI zCBDh@m3)KKbuP0)@W)PiR&$NqV4vS0MsY z0{As>9M$x3P{G?+sJ@?2Ih!rCKPajzNrui9))Q!{!_L(nI7;v}mi4sgGDCPcHH^NY zDX{h?^aa0A-D`9=ILv%f=Yt>+7Z+^CmdfmD>K*2_bl-K^FI&8pU=f649A)6Vr)nC5kZmD&%B!PAYF^_;q4 zU)nh&#R9)~TWB(+&o!HZ#V5OxSf9w+(#v;c&?p}Pb4LK?>{l*jrH zC`3Hn`$ePA6X{)S&#;k%1Om6tNU(m(Huyn5`~|r5&B*LMC_>u#h^zXKO11$nTd4PM zsb0x;4h8!-UmT+g}BQrgai|J^EGTNDIyV*^F=`c zPY<4U4qG>&?7u|CvK=~Km7=h@p;vrS-2@c|7+_o^OCWIOdlPUT%DHz2UdP_F^DU7N z$=|jRgh8|46EwD;mtdTzzhUPG7W7K9PUgwzL3#Ztdn!aqtTpLp5e7++*b5vyqn~9c z1&$97y?)WVtM@Gm|N(^T7$}V zeqq7TAx2#HmcJAxDj5zbT-_>o8iRNBveKp-hi>2yNScO}HTzTRcF z(9m@yJ4km{h;e-|fE+sDe>=4AvjT4L?JNq3I?fIyK1%$>B5w#46%L?U_d4f>Qr~6{ z=qz2)oh8&%a+JWOf*%2XG_VIXkAX$Vt@$n@ba)6Y0VG@>SgRXhC()1tikNy2T1zvO zwcNvPC2r{Yrbt2}oE4$e$y^4*_@SowE-5l#J@SCI(kJw+3JQY;j*+UUFg;m+LN=Mx{!K$9dS1B)pb3(KTL!Y zYnU|XSz6kYvnc>l+R5e+bdb6gHH-Ftg;(4wJ?&rRYCr8{MPw0WiEHRA?5JDVU1M@>yD#O*N?YiFzVb8;%JkuxQ*HDqi}#|;lQpP4iuYn{)cv;7&%UQ zqPiA+fHbv;!kG6O+V6CeiZhRQx}r(yF(rFfy<|7lN#@+lR}NH;9(6sgh@MHJg}dTfOsH?6Qs$1edZ@(xmcQS?m@ zGMiR8gPvCUrI1%s&OILf9KjE(82AzEA6=J;vZ9P=;qA=eE6fjO0?98nk|#M_at@2e zK@UZ4MmbM`PM!OVVrqTN5u-aa&`r*Fn7hmc90~-f(C@k}<)KjMR!j~2&=aps9R4G0 z*r0*6o*Zy~6q1gKP3*^wyve_~jRKmGTu_LMDCJW3Lj&F_^G zpJ!2i>4`Eops7)(Ck5zk9`lmsAV6dk2!=WpihSJI0h-yrF()K2ZhX!}pDbll=v=#k3tcb&h&ES7KrGLhx$CH;Z zyJz_dCBksL{^B%ku%n@y!b-bl6;6zid2<+oSZ*)I zF`E@ekpPS*F;js4-b0wjvR)rR?=tK}!cYz^89xxsXJBjBXm=AOM#zm&)6U#*NCqw^ zl6_MO8JMW#q;Tp5P6R56!kI{Abm;;9DdE(wIkH757KtG2;^qfX8(=`oZx?pNh?A6q zn7%d1i}B-}!z2VVW}a~|Uq=zytWACVETF1{xW)4X95%4D(;4={lsAM^S8^T>nZGHl zhh4qI4sZM$09&H|QpCMd(5E9!7ecaF?G_@zAqvtUyIcDHaOx&VgVJx*X)Hlk61iX^ zE*wss2i0I6mMsB<9H*GXfpEx8r1p@=L9tQW1`wW(SB1&iO%%C_&E8CS4v8XlptGko z)J!{eSdS@@^;q~A(?-{k8>UdS6>SSAo(E&6K&Tn=HN6QC7(~k7L6>L^;f>!!l2?A> zIb~8>3-tWvFy^#6L^}x0`ADa19;uh%j?`xSHmk(jH6x~XB|GugsS2YL0^G}mV4u_$ zpERR->TV=J46SNnB^cXAg|sUvBcSbM2boiC;0Ie_;MwBrB-BjdLA5tfVd#y}wDyG& zEg%rvA4aQSBY#IY^(={G%1z@;QVF%*RIBhW!->5>a}JWVj&I);woVLyhmLO#g{_nL z=DdgGa4xVO_#_USFt^SK;at$O^T41A5dws`Ic%LAfN$*hP5>L%0B;vJmbf3@1!BTK zy-#f56m&hO(pSJSrk&eNX&}r8q!cLDho+SRX?IZ2>81E^II)AJ_=qo&GyD=6{!Q3A zjlKhJ^rS$C^}0X@G^WSg9}8QjGfDG9Wt}03&L>#brXH}gULVkN6Q*K585XMm4HTXF z05CA8<$Oj2h5(-pTYnO;VgZhZurW+E%dCOKwVw|I5(WV;t^B*fi4MFtU!>UJ*QG1q zOJNL|G1;Xn;LA+p)Ysv#H`{n5Ow*G&h`M0n9B(#4;>7zspM|(nys8ADXKGKn77mPa z2@lcff2-zqzUvm|du}!SAZ#rR(99kRYj1mo2|RTV^24xomLxyI2Wt_dgXyLJ@yycy z#Pk6_BU6ZnM*C(jz&Vq8d6fJB?s8{e+(nX~hZ9SQ@L1S7n}w;H0d^jD73KUZb!US6 zwDW7XJdQlTQlFLw822;s0L%PMm{B&pr}JDmaSM#y`K_l##-;3|en+-Q>cLNaBpUD6 zv@eDeSCjVlVMfYQHa9Gj4(w$qbKT?zhRnl`ilE(yC5Q-tNCK0n7#K%9Hf{{4r|m4_ zyQ7|n6N)4rr|8iLt6;ub=?<7UEjT-Zrgn+d9_V7+GNQ_+68rTSe~!r-DOpnu5vMJZ zxS0&+lNKpe!%#%rz?20{!E3j+-4poAMJI_+Pl{OQ1lZtDjc_!)n1jm>>)e1D4WCyV z4WAZCh{>KoDUi{1M!2;}nQ(eN=}{)@e2*mUoE<^F3zK9j6T>&m-EZi1cc} zMN2bI%jx&@BJr}GdAnTaREL>%QW4$JfYl&13?-#qm0u+f7#m$J4;UG}B~st@fNGeE zn(nkmB8g|w2RfrrJL+kSIP+#n|1w+6#lKm)e|bNOfM-C=EM1Nz<9KtOF%pQorZuFV zbFLFJov#MymT}2sowF?hvjh5iX|#lLpq8hC0fiOAKnUeXtl;uqZFuY2C=ywBV~~Rn zvsGfV?$2gn)K1x@f00&P6(gRH&BK70C0H$4+vu8%J-(+%h zXB+oV$8swSy`s!E-l!2S)NC#aKoZPB{v|C?TiF4PaIr>M!HymIxYLat)*CqA$Y|O* z_$ruh1g3LHBVD4|T}syhQT{a0fttX>n~3x7`toA0V==aNj`g2ZoNOM_(D(K$pykDnvd1-8mChf0uTDO z=HlNR-C8D)if2GfYx$UK(VjK7!Wusw!Ro)*7-LW86KI1iDnW0-wuQ8&hhg1o<_A?j zr(*lPj+qgR^LK)dJlGQ2RARSo33HV5=?L~F=<&m6B33+Lt@8Zf=OSMG^2M4%`I5K| zc$9lwkMd=CfO`H>9?*Myb*4wTx31>9lM)Vmy$;x`#rQ_VS{<;~cw*ciNkD4le2d7^ zX6Z`Y<{-WEx9gI;2Fp(LJEprc%Uk~(NjyS@d|xu@XXMktWgGnZBOrF|4@Ll~kN+Nu zBruPxoQG@q!9hbBce(^F={e^|%n}TByS)q(wxEGMa4pmMF*625*R&8R@kXSgrRd4% zp9%|U_ud>Gnv?S=J)?Bq(p~?YuaL-jyjE4e5~Jj}cdbmTJQ?u@sLxVtuEhR^CdRec z{}ANMVe;!3mPxPGwbyxGzyltb1pxTB0$%Tdi4O1!0^Z<(Wt_Cv`JI3h8q8VMmrPKE zyz5sB{xf1$3(`(=)EgoNql^|HttAhTHY5+QudqCzqm4%EyDpezYZUyacU`lhiEi3Z zTa=}g;^^>aZq(WsV9X%;oFBE4d~;6pF`-|UQ0Js5YE+i-C2Bt@;J3pLD}|v524T|A zpW;LH#wvq?;Jhvh^Pt%vLUHHOS+{3Ib=?|EeSI|XF6h@elVBK(x8}jQ+OwiC6{yEZ zw#eYDa{E9Tlb+p(@?1mNtta)*qe5t_mK$g2fz}c!Kw8Itn|v|ca#20xx+tnm-)n6c zJUc7MPVBRdvcG|^UbLHbx})9<$*O2-@2SASVC?d!*j;L15qtKulh89B-g`QldhAS% zdS%p1{-m8P(bRjm)&&APiU9O;ZiAv&BkI$pyBx-|7apgb>!R2_z~$m%)SVB({+Fmb zagug+Ma@)5+WCv9nQOtwJ(^l%q`fifdBua#)Mpt%LWFlmy@{vy>EdAK$eSd(O_#_I zx+B`eQu{%Fqo)}Bpie~g>Y6(Tm3BTAwT1#Pos7G(mv%nqR>@sa1l_W3C4U%c-yO9! z2WlV;@B#cn)Vd;|AHOIK0=1;9D+Ac8mri^h&xudrkMm1ikOgRPk4Hgg>F#!qcGLeF zMPDOLsecy3vnK>a_qqnl$!UF%51l1`&0T0-Ye7k3sFgx!%{-!o<^KU zxV-f8NP8Z~;#&ynnW$Eag^G~%jumqwwjrwua+@a8FR7|siV)CS%oodHKCLPijHq~6 zkK9m7#L3niez{{I6Qs1p^)E|+J)1b<8xf_pgmsd z%ay(NG>(Si-77oa$;Zo{fc^r>JetdMg)C@JaC!1wpRDt9B6JmQk2L|5x(+}Uwz`b? z;uVQ6z4&xO)x#$wg>u#b>arjPvsmEg*pCCaJjGW>MmwnkBylpZ4!{cZib$2L$6-l1 z2JPvF&_o6Y2x}q{w6>Tlc&rFWL8LmO{z?+{?9~-<`YYYTm12IPS`%J_`*G+oYkSHU zes-yj-FkdZV!J)xEi~FM4(twOsv~2b7(JPaP0e`fjqj}TIAyWmaURHQvr&PbK_}&4 z2zIRXnxctG|r0 z!2e=OS&0a6Q_kKo+k28z?h%D^({i>uqEbBK4NbLqPjKv8%bD>S0C|k#R~(rbM`;a`$sje7DHtRJ5#LmgL}RUouvbo2RFZFaq(pN4J^^IIS&es#O3%{aM2aHOY7Fm;6LN zMH(Es`4hA_2GTtOr-u&!n6evaFOR<0v%fr-&l>?y)BTk-6NRm{_W=~7yvtDZj^Svz z?XTIvOl6xnhHliOJJkNdXmOU7C%!XR?c2UHs@4>%+w5Iv-LuT2R#J<3TW{~Iy3d1n zHvCqXixZ^^Pvh4C7^~C)Xi9Yenk3f#7W<>pq~HdV!UsUI&D`thYfJEq9Q)WR9*HLKDt-z(!pBRW%)cCm~w z!>EK15z~huT{0s=FZ;{wU49jjsDGqbFlsdI*0gBD$N>2n5x0Gobt^Oe5h=BVjT6PH z-J&z3^}E_fNfYH<5s{@^BKOs(1IW66*Rq;#Qi(E6KI@8Gjk3TDPHbDXSgHYr?EFY^ z9I-gN6!%LG=c@JiV2oWTPHY{EjZ4dm;S)3X-c`AZ&tuuKV#zM+Z)07qVz6(1UuM{j z$tSdy#ENA>%Ho41xq_g@N^rTc9mR=!mUvZs6e5q{cv-B!8pFr7D%*0UlAT=^mhVEq z)r1r(4Zj#qFs8?orB;ojlLAJj#E)`C6+WPm`!mMdmRgq?w)0_?AI=YrWvU@HWb@M! zI7Gx=2OkIN$&@Ws8U}roNTQueP_4liLom5r+^IX_sOrw=wiei_gj6s7+6b~tHR^#- zRE>;hH8M7f=?6>o;zMEE<|uI25P5G^8z=1Yu5=*}k0FV^cS{ZC#_e)QRmaO9i{Mjd z$ZUHCWx(qo*o9TRz_MinYIjhv2}h-d2DX+jjYSnceNo6&#yCo&`t^t{pkhH4D~`rO zOh*)CDd`@#@NnL;^K|$Q28@3!HI&O%$8efw1pcak#R^ebP_45^tIY~MdPt3qGfsz5 z+6eIT6XS&n>;jT3>AYw+12OzANm9|fIW$_**|FP=*_L#!K=VQj$M2-2P=h1Yu&R{u z{I*;~C5n|Ct1zIJ!9?xqD)i))h1vi;!@_qx$~G(xokFav`=4d#SYW%ft#SG4#|S%- z%c892l6<`5c%z-r^($kz###yjeX&oq7A$E}( zMB~vPD~V-`_;?8$j=d9V_8JX`nrstHK}ee2wTv~vHXc;$j`+r?HmX=V{e`T(6H!~H z(mR&RXAwLiqQd89c9sxa+3XnXD!r9k)L9CCRZt0)y5+-Q1`SLk4--MB!RMMZDISM( zr>piW?8@%;^~K%zrB7(@&gXZVrf`C4PukV;uHEe^G-wx!DTqy*m#*qx7f;1{(}Q#T zuNx9q1hDUfO{g?n%#^c|6N`!Y1GaoS7EqUggMLQMSL_(-B*&__49YGRMswxym`)GT zhR~2sg&!^Mh!rucHs6RrygxDqFLLP|)n8$uTzy2;`uNbQ4eR=_xeA{ais>Qf$ztbh zN_2Jwz;&HndP=PkqTL{>aX&p1!*R4y$xgyhl8$)Az1i@ZMLNv_g_pLuY_T@@ zDw>Y5bzsTY2kjV&g-<=@V~I?`&M!L=ZCpI3@v`J2&JqWwSQA6+G(Dv4xM_W!TMj zJEFQLszr(&F+BraFa3^PuIB8@a#3vtjM4bm_4%1#=y!zwZ_d09krLz z2m7tPV005?uWJXP)zYqJ6&oy~qhVvI;IY;3y3Y4^`s0}_KFb7|a@9&KJJWq(X%B@$ zzq4#CU%Ev7cfakl>-Rrueyheuv)*tVtQB>d}Q(+&-#My;2!iBxzs|yxLCqH zZ@l;%353x&3t9BVO6mPn^4%y&)Vw0>sL@}PtJtNA)gUei$-XM`kSj!t?#ec$z9#Gb znzfs%)bxNssWvbmcb$Wa86_V`gYF)m`T`0itVfw1UO-qBhj_eeSDOfpvb%8+iXGC) zl$~N~QS~H+2^F?*%u^RUd<08|Y%BP zOdn62Uhp<{R?^v9NB(sbo4A*3#?|IhtS;*CxQ^yp&@czywQG(`gTZPazVlp1@;;DI7%h-X zG#@0a=Pf$EpK0MQH0qYg?%)00RIw;7Ux?dmO#Te|j|{7{&lb z;@vv-dKuOg!OcLhVX%mL>#+4wuEyr33@x}|_ZPM!XsN^2%hb~)f#~B|1jRZgt|QQg z0JJ0c6OTQDkMZmVM5lGQdfUDBdNJT?uZK8lJS$;o9kO2U)-C!hAnVO37={Dci?-@0 z^t!oLJNF9tN~+_lI_#UsbjBqm^#xQu51gJ8}v>q8dA(Bg;%K!$hsSnwUJC#V2cfC zHXfUX5#?Ybn#{3jbiFmmCx2`j-C`Qh>}W`{faR%KR0F!}8`8Pz8(Rq|kuq#fC(Zk#>g-XsmN)G#~(E2$U{0z@OU?e;x&M$6F1k&U$4kj>j5M zt$1ZB4%`}0^}jL|M|cgWu6`9N3Nsw{~J;8bgoeB8@U*l-OcWI1Zrn$Kd3#)#-SB zA8yRcaWDqaYFURBHs@qb8SaFec4B<#@maaxmw0a0}XtE(y z938FLnj3*Eagf!x*~`EKT_GTrfglUQ$P5lj%R&$~Lk_B|UJeZzuWSV~><>h`G)0y3 zWM=~kDNJ7@EEgn-PoSk>=k`uPB#eXTIz)0K(87|Ci-=b>8+RQdsR;}!)T^!8w9U5J z4hT?+qf)n&+Jp!RAyW+CX$iVo2#hK~$})Z6k#kU2Pfp16=F8($KJ8Lqf?UEjQ5hj2 zH8N4I;6DhWZYiLq^fW-bC#c29$0=38WgVW3yH2;vhrkwP%OVcPrXo4j!vw6PO$<}% zUVemH3Tp&XcAQ8vm;o_KoH;k|mdY6Sf|JJv=Bp{1zh))txWb;6ZP<@t%3TBjNIs@- z!MBw}I9&&vAem7Ide+3NP!@uq2rOr(EH8PbAlO2q*>2Nw=~7Ii28QAuWuMU0Awgk< z@q2GEOY6cbE*`YSmhZ9LC^eEb_T0A#u~66O`VO`4AeRiQ^vY7aJ@5=3ed?q=n>D=LSOFk2|xQ>{wR z2Gq3}JgRowEvD6b-60ds!5xJWZrzHmO?oTs5eRo%2eZ-t0mJkL$dtvny~60WN)Wzc z1SzY!9yEe04? z5P&-tg)uuXGWMG#UhzFF$_h-+LRbYFioU^vvK9!fb9K#rRL{>*1w@^LIxHXz%bHD* zA{|{D6H(2qpOn(_DEnz{0NwHzk3PsguwV;ub zYBYv(f<`el$m_r;96@zX5bx>&+_ff1$ZFhZT;tpk!TB4!T9ifNS}2KoU0@NI(I%9^ zEU^KUy(<6(0+f>poLl8gMx}dX1pb%}61*reQ?dGHQl%93b?;3hFUx%D@tIgH@BJnoedZS-@N;ksH>ReN%Oig!QXSTjiPoH#XKU@HAO6 zJ|j-6s_ZI^z!n+t)HAYV{6?fr@nouh!}=K3>tp}J2?1Lgd|9`cJx{S% zY-MaA6Wnbq3tQG@c5&5Hk~U#KQ;WM$P25$Pe8p}-w5>Nv0Ww&guv_%rChT4CP-HU@ znE@+6cM5V_G*Z{`bwK*+YwyY5uVw(?g>y?Sx^$TUfh- zGGWyl8*$L{oAn$BYsldZjX3Dp&Uy|SHdBPu&l&?=Z428qe5SSmZ^WiH8(L$9 zVkg?YdG{JC>Z3bfJ^z>$XKG-%#|i`uYXFt0or)h*YFL9CNots1=qG8&oHMR|q}Pc$M8Dol85V>Mtiyx+kkC7>o}^Z<>}f; zPiACWrG!1wjk0HvUR%YOy+8ikAuOk;v9~JL&LC`IOEQE1eN8q8p~Siy&^v{+yAa4s zMUx3hj!J4gUzND?c!-xD5AlV^ zLwwQk5MO*e#4C=6_zlNH{Kn%UzT|j_JCD!Aj?2b6kITq9kITwBkIT$DkIT+FkIT?H zkIT|JkIU3LkIUAUAD68yKQ3Eaeq6S;{J3mw`El9W^2WAS>j-!rCx+uVoC-1>i=6C< zVN1Ed+;K2NF#<02_kawl$Uq;KXwheY(A{*(xU!Z@s+7-h<}+pvr1&xQ_?ts^h6Z58 zN8h-V)9xmPkJCBJ`4Jp8idDEb7-vc9O5q+b@%Oq5-P}DvwFq9hxz)3tfF*J7SDHYO zIU!CZc;xJe7uJS(uQGWc5utzXK=<4J$pb-|CNQH-FGu9(uOhEh?y-PZMXZZGkHyVB z1@Nle+yel|LR^I-%Gh6D&T!sS@8tKR)}U?-C-F3m4o3yt(*%ABoQKI|m+~NrUk!dL zee9r)MuUz;U=Anvn}P-%i@+RVsLR5@G)b*#xDC+krf%`h!@+Rmq}Q>+y$#zu_F`o1 z8!lhi(r1rmu*@s*pI8bGa+`^!m9aN=wBXguGp&rlv(&_x5fJL7;b~cD;6gxo{>?Xv zdG&1E=ELb_%;_ET;uPy)Oo-1m7@3JGKHzaobXk>o0Qg)mkcBC@;>CmveG{QUj*_CH_gR zN?l^$xshVw!sXMDe6O6E-2i;|KiwYZc|RP?sj^CZcFTWL^)SIi5iQm}}Qv->A V+4Z>jfD7}s$-mgISM2%ze*lU@vjqSE diff --git a/src/test/samples/golden/fileMgr-release.app b/src/test/samples/golden/fileMgr-release.app index 592060dca3b5fd2dc21e286c93329e5d60b5036d..f822400ea6692a9bf3b38914e19d4dc28f6fd5ce 100644 GIT binary patch delta 9129 zcmai43v`snb-wejW_R@f^g<8NV+BY8fzaxWFpm`=A&G~U{k1}Xfk;NeN`wTGa2>bK zB5Zsh#1?qco?}xt>C-u_k577(lM|mb@dI$&+PIWFOd4a8+Hp*=W0SZEsol`;&iwys zWz*CDX#btLbLY;T`<_|dctax@0%T9l7u&z9Ma_-w+q7%P3c4<82MAX0g+5{+`I?;mGAEEW|x5{{O^>b&b zefZ$vaeDaMYMCfmr~o_}O^e+9RW!oy27&s(xxk6{cAq_U;=StUcAp(MrS266tno7S zfoK-ls@kd(`D(zb5Jl>=RV$XM^H!OtSMOPmiLa}NJdcUhDv&fNTGW@5-tb-p)w^e_ zVOb_-t3Q>ccz;*ch}l!Wl4+KBMSbl1Kpa!o{jaIFlDE|G#`pI1j&|2FN8{ zkQD?po#?nfie8_5fr9B4K7xdxkUUHB99>TK1T!>Nw|YMJbYX-nO)}L(1L+=COen&< z$nzv$R3mw7gsq;=%kiD3bV>3D>gBwpqF4PcZ$-{aB>$P@ztH8Bq`g|B>0XjAtCswx z*eitg9u1sG_mR9nKK=bF$$zDj8~<=C!%yjck{6jVJ@RiPFU3bN1DHQd?Ei@5k4-dcZ_0%pOJi>ZZHp5NWMWgG6l0{xP$R+y>*MoLf7t1 zVs;tvO_EpROM}GEjf6BgjQt150s0nsnek&3g}|NJkQq9AC>ipjfeV=wRmbL4E_s_S z3k$N%5#MBBH(3@V_@f?cBN3GGW%&y_(r1TMNkz8$U|v>d4+#f_waQKsiY&lezGD_( zH3j7_DR`leo^^4y-n0nn=jf)k{94#2nId1gyS{{yf)ZCY89qTzfqeE{2m5a$Z=SaTssG(8w?gf`GVW8PZMXC#G- z4LHL?-Yg8=q%*Kl5TY{{`ObIzjZBtX>X1BJ1m ziOv*&W%UCK;s$#7TTlbm0h%`V^^vuX7%~mN7LqgK;A9f-ufmBGeLzS}0*ZI|cLw`ZSk?jsn0dNY%k<=uV zGFlETk;a6fBGC=-QPH8z2Tf4Jmh7H6unObYOrcm{$a~cChjo_xG21t}B4ZKgS@0-V ze1)oX;qusW$DY}oK-dvNM`RmjqYtIlxgvx5!M3(Hc5bv?$I)Rb%r<757~!swBY+OQ zIH~|o_BUp+WNgEqog9~n`q9FQ&1~F-ELj~TxEJ6aZ_CxDDIeqc6ZWb@CG2<7OWJV_ z+l{JY(SpLbw9o6{?idwf_(9Kp9SNdhheHrDn`w!x zNf;_+#*T_qoJGJM3mIQ#i#uHvcC_gt1_NHs!JJ{EUe2nRYS~~Vq{r<`>?vdvixDme zLZ*x@Ea8zTbInFtrmin;T)3*3BS@I*BM=(!*#;1Gjo*GbSG6oDneULj4|plb4pp9H z7nZ1rCAkgdpo3-G%($t<;dgM@#b5|WujOWh53HdGk9aYnUR{#AY9m)ysPcIuJP^`V zBeQ8@*^%j*;&vy2py*POiqe5{a}?;4Z&Og43GxItx#V)1%vN8}z6Owo!uOzuz3_M_ z!t8+oxmf6IYp8H7^r@-pN=4Q3lOP14K?{_ENzSQ8pHtw5s19|V4P!=(|8SOC+YnGY zm(EIC!bOw`@fB)p=`yiYeSK+3(K47|C7TdTk~atvU<9)fv@rY4rHywjX9Gc@mLr0) z3iSu%$!eBChVRi94nC{(2rR_Xg;b3=?i`8uoT~T9JA^?$g+2;bUITzyA?w7X>Zn|> zi1VbuG4+orS~bf6qkhF%NIko(@=hmEnn1vvdSk*Ja0{-lF2n6y6LOZ(8vQMuG6sOr$HJJ}2+#bT6BDnRo$!%PG>6kCA<-WO^n#__-<S0TF(xg|*k!?EU){L0GMARc4RLf=*)?9LVp=?M>!!4?VTEa~LGpNErUDp9z z9Gv1n2TxzRtQT^FaLoXP6LT$>3a&-NAA^=0l?v+p)mf|QvSzhvG`IuqkWGsY?j0+be zJY5mPFSrfJ<+T>p$waXX%K{-gZtPsDc|k(oQ8=S*uAjts6en#+5-WiYXV_#4KL%$5 z=OFC_iBw~opxo}vMfJ(j03@CrlaOpQWRq)$T!ZMFt#$ewuD8VE)M5xlyf8?XGn)~P z&777^=z)wwk%bu4r|A|57blojVW_WmsG~YB`Yeh|0?}(D3V2PR%@AnUW;)@s{qRt2 zEwSn4bgjgvPtGTLR)?9jNni+TaUzMs(}d5E-V7vMl6Z`~+n}OmGq5E-z`ITu*cu;5 z!r3cvq%%Geh)?llY58GcbIriU#_ceUothiIYWTTXeHy@5WITFIWP0FlIw1WmXtEup zXgf-!bo|9qj3Ev3SPFxTAvtyu*R>D2)+N-sh_tqPj4p(83MODv zI(yhUd#Jkl(uR1!&E~HSyIJQSv=K(!9kJdxt93i4IyG58H@Y1P@8M*l9V0ZsNIT*% zUeCaG#$k+TV7uZl4j%?~C&Se5>kBH(Y17I5i2=wQG!nzm+J@S+-Gbw#&YFAFmm21+ zc)`89&JkLauFJ{2s0ciyudg5&eFZ6|>xIv$iw#96aGeD9MQ{c&Ko{{dt9vQ|N@;v_ zqZ^Mn68~QlnspHeVX;mSK~12IpEr6<4(h{&iW9}1gz~Av9`zS!=zNyOt8qc;R=+4s z0ivi~V7|3l>~knY1x0lQ=yvRLeB}#FQ^iK@L-TYWDfN4BYExA3%6;^VBfGxDbQ{Gz z+yf6rxnkAC-J&*Xo7y#MoANhmzr@;L>JBNX*g0cbrTw=2Lm9e2fc`o z9O-bIr{73{FgJ7Knt0&~xU~wCFxch9C(TgvM@}}FI@$D`m7PhggG2qD2gf!Kj*YjD z4DIV59h=&;s>@T6qia`kE?SVuc)WmD%G6!9QD(-<=!M39Pd9cRob57 z@p~!2oiSCld7k>G&6}n&w!AF1v3A&0`Qb>2%Vlb(AV0) z=H2mO+U(9lt<}L)ZA@GCkMfU{-O1{yaJBV#vZk|cTMn2v*qnn64?L@8#w>GTh`b9N zzB{zk!`pKC5)#a;V)VKh==#DCXv~Z&Hi=(xTC3}4;614cl=DEti~$`xF`4yYE)Q7_ z)sd_w6R`|wjtJ8nub4r>+@=|A@yS4_`y;u%A9B5ghb5#>o7`r`YGz7FU$S6~_sTyU z@V-i=A&JCD%;Mt z-pN!|cD=PGORM5OyTEEi6KGGPxz%m@)HQpqWoJFwRc+nCJ5O!u%AYltxx&Ls6stpB z#Y;@gH>~I({o<|F&S3eHs*aX7nSk$#&&6l#nHn;Q!r%pfCe-O@ZoSE5W_5QitE9E2 zamHFE^BFaks4LO=O(qc`J!0Ade2g>l%%&MLP3AM!!ztaPs=IS5Onx+bTnuPj`L#3E zQeLb@`cQYKwW=5{xep7}&J=^mzqb)r?MzcP8PQz|YwCyT%@owu620Wp-33;+XO!mbJWnT z`PMfK^od=0*7Akg!Y}NaXVszU%Pvlz4*h$h!$Tvz`x27DW{s0RJ0A1*b`2irA62jK z3dHuqDfuVO1a#KTU&E}pJYWmsN3x5+b0Pj20pW%H-y*OIs9Yg6DX;&YQR#@jh cr^?5c7kCfh-&BnE(1z}w$nZGvFJJokUxWQ1ivR!s delta 8369 zcmai33v`snb-pvJ*&&_{_v_6kmaFB#S7+nJ>pLe;)AJ z-y&!HH;=W?%DsTe?OXTmjc-3Va_8{fdmbC8f9&M;nNt(TetGOxf8Vv^VEo7-YC3z4 zR_xh(aD*QD{w!wm$TPF`Tv1u~rx+hkWXHDOhe7n+4xr!vLipJGedkUdd%x*y-??ok zuhm%|dau}>b=nX=ho^o^bc$?UDO*KQ_sUuk(xb9P%+ycGS)yLQAs-hf^&!vWqD|*! z>=5nx-i%A$iy++Y)%|Lg@am(g8qe>jX5k&Xq_U-WM!)B~F81ly{XZ-08s5JjralZkM=K4te1NqGFIZ`js z@#PYqvdi={kI>SYFfb%HJS zS3kxocDO+56}rJP{Djo2bR$o&XjVE8(dzMWB8h3d9qPdv5dW3bzooVYi=SEz8FpCv z8Q1}Ok-RMUQA$9W4mEU!Nk3&mf3)p<9<9?~DygseIb9SIx($iHV_<@$WCVZ0BjMjc zqA#k~=ZZy1>2fA#88(gVkh9mN83Px zBbOqh6ErVPqH)kRvf1$&P;Y&L!6vMfnx%+(JB`hYU~`#tbb7&-(L}TuIAF1@076`$ z0zn>GAkEN2iD-1X0iUu@!SGXxqBmJ^&_mv7Sh`B5pkUtYlqBCN4?&@)bz^zU++Sd? zU((?)AMsaqyfjzxhKiTonY8qdezd$S$x<*}`YXpMPc(ut}4E%SXyhrMNIwJh; z{MY1lX8(cJ>m;9qFr(1T8;}tW1Gzh5CaMvg11`V|B=rGV6kmL27InfydrlxcuTW=q zEFbIGA^tt=EyRGq077cl8+3&HzEj=_>#HkzSH<+A|0Iz`?Gg1MMS_y$mrHo7Gx}>4 zYs7key`nPbjB#!C8@m621gOfxi6fkiD*jK4DR*glLlTC|s@;`Kf2y1ZiR+5gBM-fh2KZO+;H3uQ` zur#i%{})=v2?nq!n<;h&Z;tULP7XoEctMkxkY+F5B9_G383C&B>OfUlveBjj3d0iy zk*}a0phv$CwP72e8TVWoTkMGadm}cWCdN)=u;8_B*hSPI5O;9+JQQ6gV%5hmOtd_W ze1a?qI9QDX2YLWq5eZIB6v%)$87%#+CQI>gNti5Q_H_9xA;6yPHh}>>j3%%LAf2&7 zEZO~<++<~AGXn`o=u1^K@=T`AoKcxvi2i`c>)4#xacoIpsUyyTCWVL|11l3?!%~ehHo~gmpR(A(cc;++CsMq}ZGdWG<8$0t_Pv%7~31B_eS? zEAk4v>z`rBa(bZ{pMU~K&z%YuDV1@U7%BvptG-PAg_#Y>ddIcdQNh?zLQjl4 z3Cv;l7FT8PEQoDfnd2N|H-=2cip~QtZqtRkhZI1EUEucd)@)Wu+zRGuL*C=pt<|-w z*q)iJQj;lomd!Ihk84KT*C4+5Fp@*fq)SA?2``!8werr>PgYlzF$)ublcfH;OBjBbDj6wb(5RWF)VYn&`o@0_*x|H1vyteJU`$xjloIUYp2!fMh@vp0(d z{mAT!l#6`UCkLNHbt+i?n?8>U3WLoioJ=mD(3n=%PPrxYJ$^L{otIk;HM0&pPExATJ+^P`DH9w zu7DXK+-!lq#tAlt=jgne?mX*`K@;2NAOSs8v#94Y9Tnvm0=p-M-90qwgFGfwk(}!#4&$y2EN>bEtLD%7nTR_6~#&}gozgAl*Yx4CUY70v( zkLXq~%E~k{bQlTZua)V>x}vru+7EQ(4R)R1X*yzvUx??duymm0vg(NSM4`T~uBggp zki!#2{nI_<>AHxB>v!s^lhbS?IiP+{5hKMw#0)vFIz!Ik1CjU-0rF7vklmKoJ7O#y zbffZxDR-6&HzS`(y2z5+c9pTB+4}h0P{Un-KyzHXv%t-1luhf4;6UHT-p4VFlzU&c z{_))U>;i#_&9))!ub(Rlbz6O9c@d0V%+3L4<5Pp&CNZQDP>$5kpI5@ZfUY4VBC1qG zz}{F`O=o!j_&AIT;mgblyg@F3RH|HGt`8N$GateX-6IPFaD`A|aYDbiZJEy%KIPE^bPU3Z9iv@Aj zj~unPvA{`lUu9_Zu~!z2u61I}%E=YtBWJ z;r4e0q7BKpN!5!Ea{@063B@ReOKOG6qYHQ|fI<#x15l9_QZNV2yoomog^;%WL=>Hf zsVIel`$qGcF!e74O4T+<4&QH9h5CV}_-)pkaR$ZGP30Dd+f@W3Gw`yS6Lf7!NOeURB^Gvwl8$>)XFz!O$EE{z?#177+K zTP=@n<4D8DcC)5YTx}wULEQN$##)mC@#&J5;c_=d_)P?OTm{koPBDWi6y0e%Vn_mN zP6Yw$so>Y`kOq84S%9R99G74CULN(hUYQAqsfkNsTpc8F3ygYet8|A6QIMd{^V<;j zG&aFt&yLs(;4)Y>LHExSZh0*Cn;q^>r~L^Omj!YZH`->k#WH9Wxk*1@P-DzAoy@$= zuFd152+`eEJEG=uuU+NJ@JG>Z0-D)mfn~IiBLkZ+5)mjCgPVbnI#`lV##b-qC6kMm zp>4Ocw+Jj@S1w~I_(!<*F_XnWy3Cg=rVMq%c{=SvM{0rna?(O)YJuOicp>SKEJ>|| zQzX7==H?1}J*<`r?#-VvnTXV*8p}O_owiOq+4rbaRCjbWYz8TX> zI6s>6KMi3N2v%^z7{M#F&YU+N7nPm!3JVtF9RkZxm8mhyO^wmJ=T#?DJuy54USd)# zS1F8Q4rU7|8t?IEJ8KRs9B&@$Oe_H=q!CBhW;TR9W zb5eZI;giD@eE((cDMEsiovZzbilfKykh!Lsz)N)?>vZN#<;j#qK4#Dml(ft&b5^v^MlK8%N9IL*E|*|d4sBnV^`quLeOsE^Farp}2)CQL2b z^f|G}nocd+4647ruqX?;5f^#<9Q%0TSvmHT_H`Z|?Od(D)!FX9^Uys9whs;s>8qU! z#_E?ysr#2Tk3F+&xo0fU{embF6waXV@V2m*!rOKX4IWU`yLIT`ApSox-1BB2lcM{F z_a5MXX6RKb>-CA14duYxF*3OOfRBa-hPw6-4hASZ@VSw}1L19$jGbGV<*5qr+N$9@ zwvOx=-V-2qv@jetRPi16>dPxa{%VI(m-bXN7@DQSdq%eI*mEF2?g%t~N4B|UG(2?Y z;Qj;p`s$Uw&VwVv;59aX&C4RjPRrSmKRov1)v2o)|0ThgJ%M9@;l?_6cC`@MmnO%H zv1UJ5YsKxOM_=v>>0@hi%k3MGOF9Xyjp(-?of8k~^J{bQKcY)(i*j*mQsl_NCiCd3 z*c`djt5?Mq%6y-0^5yHlj8)2NpMEY@IDdYCx}f*l?%K~vHut?ugfAy9mN$dXaZ?)F z2R`iCBwPK4+pDo)jlCHo@_~TA%z&R=O_4OpGrA*QhXQ6X_!f@Up>Pq{)zqAZxiCw2 z#*6f~<2gxOPg^vgvKA*uTGL4EKO`(`ZgqJZD@0owp1nA-e5R)1%}c|(k2fRQMBKe` zr18dnBWt?a%vA-}_&l6gjp__uy58(nm0jm7mZ49sE1YtQS>wapr|Y?iYUvLe#om=D zms5iJ+1?`kwM4OO59%|CHuU{jhBUd)FZ=HCnhHK!ZMrc0}3=gt@G8LuUc z!GQwis{7XGByG~SVNwC)l!2Pl4P-O3^`x+2iicsHZXjC#B%Un+5p(itxdHZMq%^?I59?uJjQA&7sEXyaRuVY#_F`c@pShh?|mtwlePWNxBl1uII$xW4Vm)+;; zrV_c&LZ@vmmG@d`*X9y=zl9#!Tqcj(;kP$eN^OU4Z!VXo>@a^zh5UvKReJT7V)-3A z{rr{^ITqIY`pWd_Ers%&9lnp@t9Dq`S17OYuv~BK3&{_8*eaxceq(`tx^Je;tAtvb z#AXAS(_bVmV^pX&^q1(L_806B$V~jNwMmJcP1ajU#O2FWbNnjyRP)~f#UN3K2Y>vW zSImDKOorofXm9$BCuO9lJQ;fT?MF*JhL}->X~Wn*P)NZ)pnrFJSYFK-EB%v&d1GJL zpD)E(tw$Qgd3_GgH}r>ieyC?0Y!n~qIG!PWKb~v!7@kM<)q{=R2hl$Cbm^i)wepod cz3foK^t?Spi*4qkWxX3>Yxkkw$8P-jKhv?cfdBvi diff --git a/src/test/samples/golden/helpedit-debug.app b/src/test/samples/golden/helpedit-debug.app index bf9da0efd5ff46e60a608755bcd8a475f099d2d9..c05b8fb6736b6ef2c076feb2dde4127541d67a62 100644 GIT binary patch delta 3896 zcmZ7(3vg7`_1t^+Z8iyn00|#c;ZYZ}ODFj02isbwGF2HZtp%lCZ98M9w58|Vw{NrL&D{6S zJ^$Z5clYH`U$l1Ns)p))0B{SR8Td7DF5ogkfjW*(iG-4AluGE}Oqc($Y zfMWzKDbH~q5D4-`Mc#r&z607Lkd)^+WAGkEH{Y9w3<>#)Lih~q7zV}y4+;%s=O^kQ}Fy%JpwzdYfT>wk)# zD%Uph&?U6(z;d8g*LHwz13Mky(G*~#cipz@b(kJY0Zu}*#cj^!2%|AU-i{WjMCS=@ zH)xMj8L@0GqyaWSvm7HZX~L58z+yUT9HIQH4snobYPEmjca;gH{Aa#*7 z785&7xW{7tzU(kg7YVYAoHN9M<(NlVEQVC$l#oU29o7R~OqQY?iq8~?*|@~pLdMHz zvnkh&?v;yttokUzR-hpPhJq^vW#>&`sDGBocY_U9i+l~{a_Wr+Z=!&tY6yi?^nG^9 zcVj;eHF*yi>6Y_`>Gz3z7fEA+baYsq>^;y^Z@2gTVi-5fmy};sQo}Ul$JBv*AfYn* zU0YWSc6nr-FuI9UpH{UQP*qYcB-UmcO%3E@KaQqG;k3_#ira1WCNg6w_ zAvjKw2sw|TP(yGTwHv8Z(5ayYvXfm0(7I9sHk~9NrpPZZW*($rc{w0;cIV~A;8}$% z?^Ai>B9Y?3gmykmG-;Apx9ma-sH8iU#*M1QR2nzg3|oX|QHSc3#t~V>3apSx6)Wo4 zDzqn3r;2Bus%==%M8>h&E(|qi9;HH4op*`Uu=LA4!{C0WB7XU#pxb7B1=Ux^EFEv%)7KZBEcQXAQK&LJfBf3*aH$OfezkFY4M@FFjM6Q)M9v=n0Svy876M#EX ze|5@qP2gRvu8unOrrcjwxj&AhfUcZ0`eUJ;pwWmNpAZMf1=>jr+AlxD`^4Ha^`AK& zXzAV%Zx4pUR#LCE-q7cb9Rohvd%GeFqw$r2kU4lu=e(*QjgEvOZAL52>LnP3QLOFm) zBNprq1=r`G4M!E(Q?T9|Am!8YMj))g(xZ%7Ju^#Lk@XC|k0D%OeKd0tjuLBrR)JMe zR>+HO@PV?)ywL_DWs@ogRNd*OvLE`ou&_nz{R)5(YmGM3 z4;4jxZnpK0iX|=d`<@G?fmzCS70#H;@9Cnk$6TU~PL>WSzp1X~J;`M-+k_ zwdFbkX1hX>QT&sK##`y7ibH!hM6oi*j~0}q1##z z{Icao=|O4BfLLqp)XOC%ibg6`5)*5x1f!rPDkP%#<1})o&dJ?ra(7;RZ@&9o+Pi;l zvfuCb&1c@cdGp@q&FsMDQ_VFi)~yUZ0RZFJit%gV!|+*@3E)Zfe~n*idorBwtd#uh zKuwANUF_W75RXTi+7lZ$KiE33zH*?ysd##{=gXdzzWU1t&~R@Dght0;b!)sm0lhDl zkeS|B7ODzXRQ?dk2cp^GrX46)U)Q4g?$-l7-_(!w_k0r?tsmRef4SVTq2jD1bHq%3 z72oO-Th6jo5nsY`)LLG~@>P;w&x+MaUcxHXMLx*-Rfia4)ynT_W;JTN=M(P*oY>@5 z_sJ6GReR+Ed_N->Gw<*xGMlqk)L$|*+oC@5oiDg$^VXJdBGS0Exgq9m4&DRx^pv~u zW}uZ-_%z^i44>opj0}e|t*Q3kFi4MKms%b^*0%`gPA*$u2!(A%V4!!n$W9k|m~)ym z#6jB}b1)=8C%J5e-9FuAzI#FMr4hLq4$3&lEzre$dS4pa3SOI#?ljT{UIO-~frJC} zq=9x|+?Y9-hB`n#2rLt706_#aec(}BbNkG&>Ha~^f(142P5*k3Eu6>;;>%--U(L@#_|bH zqmU!OZ*!wgvCKaGz=aTbG3>k#@7`KA9rnmGV| zfVKK*9{DAiWPKc#XJMiZdYng27_=^oilCpPWl_@4b87&UAn0EmwAK1@8tipZy5Gu?4(rxkdG?FS+FY)Ft<4W^v-}|FX*t$p434N&R`6 zOJ5jsDK70rF8%4MPnReA^k;6}LOy-#M)Er@_krxDG$A!G4>slrkWrdrSTMx3&Sw6~ zEknyI{2Dw9Q?!ige;GR%hJPEu1NtYPK@|OuI{@P7%d{dBVjbcVTt&KTZTDC+X)@fK+mv6D&Kmhu!6K`zQAG$({Zm0oHYuC+;5m9 zdmu3QLO}i&E8IkDzb~q<6%ILT9dgbUk(k8Epaa6Xo4)f&0clMk;JR3#cwsg3Ieyzj zie2$77gmE$FQBmwy)f->=>&h}0YE!}q`uB|IjXB@i^T7skOxzFuB7h}9e zSpUEQ>qZ*AV^T1}!k!e3iNT0C!MHOWjCJm2?w9P$)yxAohZm6Xd#R7z#W%=9 z$YfLjT~8RPE%ZQ$CN&Ts`yfEn*kDiewjaISD7y6~TA+^sR>=%7rvZeq4nRLJQG0+? z0!hn^G*28kjCOvLVqa9pMdZ9;L{xXUqV5zp853it3T~UQ=FZi36O(8ygw=;XVhDPzpFQkYJ3|zI@_8WV=?8K_lw~*^Eg*_ z@#5i+i%$xrOGnh<(t7pB>z1i0Wee5vvia(8*&Ovw*=+UCvZpG{U4v$g>-fbR5Ua%p zcQ$q=RyDLW`CwIJEWUDMbK(S3?^VoHFO=u9GwO7CDYMkO6?w{2QNWLL70Sp}p^7|y z&JpjeDCFNc;(>};JSlK)WsY;d;vU?E)bz^P{Ckg?U0a#UV-odp)v?Na{<1XaSY;s} zbHppCd(IK(R~7Q}j&8Urk59`my3bch)m2r%3*GJb4kPFVlOjQ)h zFQt1!Kug;LX`Ed3!p#k>8yaH>Vh>xcx<6FspPXRMhEIjEJ#18+t`4%d)erdoNR=!N zvMXvWzJFBv@SUed@O_8+YH85hT>$V^o|?U^Otw@4463?i<#W72fU(q`SFBwZeseJZ I{-rnVXPY;mR{#J2 diff --git a/src/test/samples/golden/helpedit-release.app b/src/test/samples/golden/helpedit-release.app index ad3a07f70b744b884500cb73292ad3af6c98486a..ba4a938ad9c02ee8e1cb956411f6a708dfda432e 100644 GIT binary patch delta 3036 zcmaJ@du&rx82`>~Pg^c9I%L4a!JWv;CU$f~AnMdPM!<-vt%Dguw4)tsEbVOX=5%VA z2s#nAc|^sjC_eBHqrn+NVoWsnOvFDlQ4$RrABjekM5F)g5Bz=iv}=ctlbmzE_xWD; zyZ6Mclkt{yo$aAfB07t;49_sOz+Pw)xeEAdJbBq({ajdpUl?nt8?#r89ZIeWbnLDA z!9z6s{h?{zEt1O*wn;<-_jM!tBIADF%-MH!=lK0Y(DFl5D-&-TI944^nDQE}^FXpg{ zjt$bz5rbN163vMDi8h%8XQEsF0NlP z!?~MnhY#a@W}E%y6vyvo6$E4zQ@)ROwngUW`LYtJBsOF+T;i~><0`VSUt7u2gFZ7H zFSjsA(1I%RRrVJCv*jT$WQOaB@(9bIh;&S|$8yAJL^(>=htJh}jP)j2 zLS|J>%~J8WYOPt6e}b%WjO?-t4nmO8Cozr2$*Ym4iU`~q5p-fMore_jits10T>ohn zu!h2|=g^;lT@;^hgZezQ4g~UgS~^W$qLN{OCl7I}mt66byW&ABML%+yaSAYTF>WXp zVaYiTJPQZ3?^XPz!18tEP5B&0Q=+hQ0qq>g!(?^I=UHGr83mQ8y65$1bmkwo5b7;o zD2%%)qI{93gytHMAo&uag3Edis=F7TF?|&8AISA2QYm%Yk{0#)k~NicD9U)GFTF04 z+Y(FK6F)C8jK+CWR_NMxxZp|e#Xj5$9x5VFTai@bU%TkO8qjNTruASk}a_{(SOCE?)+%Z)WZ>p<2E3OrWb6J?Wt8>*apq zcLxmmsIB>ywp4Q4ZOnImE?}oDy6uQ6Vetm20&t4ppNbBOAr`U>M+4W6#AsMYUB z$w64da?$$GO19VJWlJj9Fvqyu0V`)PwPdwl3k*Qu0S!J4@MW)hwPC68t5<#Au(pB! z|9G*>yxbh1bR1_?L;RkKiHk4Z;}Wl^*Mp7XnEDRqhiYzPqd29m!TG();uPvI&KC7q jW1}axfatF(^@_E1Re`faG=Y(eZxB;pF0qNcF8@5fJPsz9?;MvtH&l`5zXoPYeIycY z?&@sp*xf$Vuy|;&IXAVY@Atl{#Lah(!*PF<0+%k+`u0dyC-tAtV>kU5^Hs6PF4_nB zcujJsc|VBY?bR?paK-7nxB2p5-@U-4&6l?g-YGIZR7AAd7xd;Yv8rFi8j-BBtz{xj zZL$i*Y}I2e6}jqjD^Dy|H?0vdsG^<`QLg;n7Ez)0dT;s0v9Z~wUXyvkrw+*{v3?}W zgm3JYOt!>DbvNM;u|wTRyq;Cvv9m4I8E)FyQr{X+j(#BZ;f&hMba+{ZS&w-d^JB~l zV*xujGxm#*=smDJGBJbKA+Ti|jk+K4C}*+hs6{sFKI(zCoem`G^^lgy4mu$tBzI7+ zNMs`|*h#*r>@Js$x$4g8>Mj!2wCz!nyGhto;mAGY7~;)1vDYPxc;7T`f86LTm*}y> zuj&+ZXtB2uY>Z?#_1HqtO(ec1l#B#57vAb&cm=OWQ;al|wN5BX-ANWT*xFYbs$?Nm zO7oE4qH?&Z^^hoFM}9%p0uT$K@IxqQ>2r?t&%^}fCW#nrtAlJDh+)GtB0te4JDkPN z#DGO>l5lNC)~jV3LNnPbXc*hUT4rPqU1gpPh>`@=>rFJhpYH?#J2^rL33P| z`gzX6Sete=fUDr_aTwXm^pb-kg8YhXdBE6sD1?B(ALKw;uglo3s|ZO?4f6~OHw6Yt zI9V5|#vbs{hEOW+7ZVfOh&8}KjU685XhjIo{i;^0CHMoEM@sgduXmLfS&X77g*NK&| zPZWM{k{8ec8g#|cyHF8;`#TcY@Ar}>f?vC+i0T^ivx+i;dTbQBVaofQ3TTrU-$cCZ zc5snC)uK=gIM(M!ZFy8*pzI@O4%-q^Z@R6t)h3VvrNu$Q)Dvu;`{P zi(kn2dXYGJG2M|JJdDg#3|{34l$QWui!6N_FoyEDVVH8K z>d=8Lj>(`IsCh`3DB%&^=-^}urBhQFc*XU|jZW9p4Xhv43jPFPu#7eTJxST7zL}pR z-ck4G&x;L^Gm3t|b#eUu53U5;al;OcD3GryoDPm$9ghuI(#9U{KNGzr;b zy0C7w{4)5GPP?tu2%msdo?!?ZwFMKBBgDwU3sb!d{!%fgBC#>LXIFDmYpeRgSvls- zMlUMQDH~Hc=R9gS|AJbTU#ngySgt-T$XB-ua+Z6kWbL{z*5=koGF5i8MOs>0+P9aK zl-MrU)cCZM8mFCY^)0PUjoC^q`XHveDWcHW!WUf~Ft(^VU{Pg2_f?i)J{+tqW3BE> z5SuG=^{idWTGKin*$dn1Wvn$nG^BB5VcewAtN0T2UcvUY_4zL-o)ZTEmn65 zQ>Vx6;DYmcjs-eOx6)BRA!TnGpw42= wRM)V+bh4~8?MN2U`C06csQSgw#wCb$p2O~*DZ=Yw0%Pr_x==qF?+nAzzZ^lg$^ZZW diff --git a/src/test/samples/golden/iconed-debug.app b/src/test/samples/golden/iconed-debug.app index 30c39dfdfa0482e84d878a96a3b8f7715612fa70..66128e90402ca22bfb6c71e9085c898f72d76955 100644 GIT binary patch delta 4178 zcmai1dvui55uf{g_a>Vxgg_xLLRgcMkPwIwD5nQv7YoRnWtT;)Mchb$*pL8;=&9(2 zAPA*)1DB@@5*4)NQK10~@-S^cTOPJj3#X?o=0G`>%Hg8e_NYCjGqby20{+oG`M&S| zX6DY!otZoL=Gc01bG@h-Giz*lGXUJi-vIpj@DccAr2^P-{4M;_uOT`1&5DnAuB#|q z*HF4{NA<9x;MzWaO9EJPb>;bn)HOlZ>}$(WWAmD^IC=_4FCHwU$>nYBFrjwQlMV3V zTe?)-Q=pN!HrUO-U;_?(AK#0jD^7aWT%X9$78M>L1BVNgb$lL7J{rg(6LFR0HsR zRNW`iT5qUsmN=`wN;YDp{={`rUrCvAA1!=NWvG5!-I9g=hTx*Pp@u4W28q4!8IMm0 zpBjALz~?wV@3xLj9o#*97KijJz{htj9l(sVQJ2*papK!-i6}BgIgDl@q5{kqTdjv^ z3i$TZyzzEK@Dx&)Da-`M0xXOR;fahbu-e+%2u~s$7B*Oojg3T@%mNZ8#1u?ntdJkW zQyC{A9|t*@J7-tdo56xe^0S{p< z^#!bvMfzrT{+xO{IqHI-`Js(<6N*}3o{5cYFw1OuK>KkHE^Y*IwmOQUdSKsS`P(`rNdbxt zC$a60*f#xK-;(?tEXJndc37;Xi`ePDC6ByHD~?FWcUurLuQ}Z96fTOGMSq<6Cgn0a z9f6mj2{2-NI?Z&hXc9TJS5M2$cJJc}6zCngKDV%Fzk}ln4l|+U&Nv;>ZMj*QZ&Ug# z7b^Rvh0Paz6zJnoZF2j#k2%w#x<|i$?&A*Lu1ECqgx_&++|EfRyux-ONt}&S4k<2k z+71M%#e+Fx2M$RdowWnk6pe!B18PiIf9AZcUZqSO80k^3fd{jRBVzo)j-&tzjrRHx zg87hEpmw523b4QhJK_Srn|K#(wF};-7@I$_Bd?=xZ%`>N51Nm3Mqc624%@eu_?PVn z_A4A=SyFMDD>j!@PYZl0gs#Dc(6{>w6Z z5>sx+r-UhrYA?=M?x9iB>SRUO&~HNtM1G^z9}o zZ*=UN?oz!87U{%9ujZ0WlNf1>>c#8t6(*r{8Mr_qV0ud5qx9`9bMO}bcu z2NHgK*NN1agdf4MnMiExvSw1Jp~+IU-eqXY-G-)0ABDzDm+By>LvW7VWlsRpTxtB2 zW>g)ojezv=>Vh&yfOJ+D4mt6Ei%OQ`KiZ#1nk%LHCDAyky9v3(=8>nz2-?8Z|Gu2K(JR#LlTu9v552SAw zxw4SFcG6$##I{b?7G#GPky30Zs4(8v@%${Nd5b0W7CaSf)=6b{ERLJ3d70KM6ugLc zvsuVLgkRG@VXnmcoO#lz@TE@Po^tYLmT}t9#IrK2j)OWu3Ba?5`!JSHrFo82btWJz zxh~bP zi2Ra_A1kr#6eWli6YKrkrE2GD-$913NY2fNCt{5fT&9JHmxE@PRIkH|=YwW9Eif=0 zcNT5MH)&@&=%5r9quIT|$OMJ?3{Ze$blVcX832m%dRn>qD7DE$ELj2dh5^ zqaN6Voe__`3!Jfd!cJ0M9`LAB;6cSWc$d55U+?hm68h)vbnqkHTv!qdO@PwrfNw*N~{E9zVN1tkja6hUJtj5XhfV$rr0UC`)$RN$s~OQkmz6^E6=0Py80 zA4B4CD`X`pyqBjd$Pg6pWR(cGOLeBGJDE>WU5g}Lso%i4N~i)X5B%Xt-bZ1D(v2rW z;mSlRRCxGBq)de+SFC=E9qtZ#mQttV7oKbq;d{0^(>i}(yVbiFjH|AxnzpEZa&>*f zxVoCTRZHqyO9subOEMCiiFK86#*Dg#P)&Llda!m$RcNk$ZphSB7tCGybp3*=nwr+L zL;h^(_VN+A_`l?m>RMF0WMPla;h5@BP2K#~FUyzq5qtIG2g}4^{R+O{(;wjbk{093 z#1&nRZ=pl@&d}?|m#N8_0DJVQ@q@&_^{3;@a$ol1{^BzlV9d-}{>BFZC>iwoAG6P@ Ad;kCd delta 4141 zcmai1eRNdC6~FV|%x1#|NkT|M62d}AA_Pc-d}txMp&$@Y)?Jqmd-w>-mlX&UX{+?S zBw%d>+`tGird0${L{Kphzap%S){g^iX{*J8m{V(y9BGG&r?z@d>AiP%Ul#u9o_+7l z?|#hu=H5Fq?_Jy>x^{@_Df6aQ?FN9$_#1}LAYKBm@ALtX2|Nv-^xK>kf95CJ2e((3 zW&RGqqPqI}@QQoa-?ipPtG6#1wY_D7!Pe7^TlzZK6eQu-n8qohes}YJHzP=Q)jz zD?ECkDia?4fGWlNb83w6bevNemgv%7q#3bRpYi@F@8&h@R)yAwm#$k`cTaM1a0>MP z^tl7Fv6tz1)#25O*CD(*JF5DO>f8TaY|^g;ui*Ji08^^h=H)wOHip$2jM5e%+69== zwt5)a(;@gG8I7@{f&y=mZ=NvMF&1Fq)CrGeY=ISv#R!jMY=hO%&_H-RV~O^0{|Ss0 z>f_kCo&_E>KnXhBz&H&J0*t@Q*h>R4p2#?z24FmiaUUcY*H<(4A&%>BWSl{ICVUg) zzNBx!@f;Ii!8K$+0#n1dAL2lO@MOlBh;anO-(#FbnC+)9&ZZf$ek$Ysgjs(x;~a_r z>!&duKp6FoFwVTvxZem3JlhTyzMspcoxu>~I_7!DtX80ueKhY9tGjPGuql%*B$J zgiSM_6^Z&G1*!{4afd!TaOk21c3Qg^BIYg|OR28i0<$#UvcW8~EvhS2l+?9LFw3bc z6@{&;g@pKTvb+(q%8p`jA%Z|Chk9PkcH7Zzhh9*SS6a_l69!LXupim1W7#7KS;YKU zw-uD-55I5K|F zHG5RwT{vQBGn)yB+Q|U1*+VI2orNQ6w$gN>5{CLLs59GKZ5zdc!eP;$Gu@=T=3&?1 zF=zq|$4;a>Q0zjcIeK=H^0l%7MXg;gDk>{~%*6?ZJuLXem6P794;JN5c#3jid2u$w zEZkL*lR!@UTxm%{N=%=2yJ^+m77cQqad8`D3g%2*um?^9xzEG-G~y;b>T$}=;fa3r1o$!h z*doOnb~GK(s3wF6%rTml+Km?J!1|aS^+L!;x;Jh0B>aZLZ@QVt9^|%{O0Y3v-Xlj; zO~wa~Gj#?|SLod0Z0CJDxP^R9*iqciu!SXxMqp0bTwYlg1k(xs*2ZZfGNgw3-A%fO$)PcF5BT7<9d+s{K(rUlV$B>uqnm=5Ur&q+-4wvh4~fEY>vs2;c6lVCR+->OQ^q0 z63QWW%FzJH4BI_ma%Eb38koE!*L)eS;H6^*x(o{vtQ|0eB#wEO7t1v?QZeIj6_OCR z+DauYm64<=lj`}DU}ANca!GlP2a|JftqhivZG}s5WRk}yH&UaMJR*)6OKLn`%(&ix z#!L0WRRc}9dZ6oNkV0c7O7$YB!*G)5G84cwFENuOh7-F^)JC-o^6YMs1p=fyJ7=aF z|Cgv_IsP@FV(NL8R2?Mqt0Um2gklAG%`GyBY{ATS9gji-7go@fbV36TUV|e+cognr zLe$O(aXp4yn-(hEsAJPfXTokl+%DD2IFZC)5gFV^ROS+EH~sV6*v_Z!vTQnd%5i7H zGZ;^xndET?Yj6ZBslg*`IBC8wX&=I)22CL?e(JuWpDY>VETkAO!M8uN$Sv>1Zqn-9 zq?u)$Gwf@*RIh@1je?H{7w^Zv#QDUUY~zj->7gtqObvDe>93dXGx114a^2cBiDXjP{X02w&#FysS@@mN~C*!Pt-~ z@!+Bj=2e+Ei_L2(6it^5@_XSi>7o7SxGccZFTkP*@4{^nPrz7e#$r@}hyChJ@MFi= z_!V!B?_5FfT_ou1W%!=nSXSn||9`tNCuB4YLi9moPF_tTV*bDzWW=1N9j7*ac6=m* zi^=?~RNYk0S(IG-@yh;~NF`3Q5;_^oIT_xVL3;BEZ(DrS_vne`g(E+uMB!^k#9UxE zdNqoN`|lKPeoHFxQNaHpbyN9p$4CqSzlsgTkobKHRTm|Gd1Wfd6%_D)x&i+(B}IK{ z_)94nh?sArdW+cC0hR~;@EE_GTo9meW}**N`}sz%NkU=C4OegD_I8EbQtH=T!7gS}(=>0KF6MBlO$TE7jsWfIIbnrdNn=T{@$(=0kpWxNuE;-RDWMxRr4U+mi5UWB zM%YN9hpC7I)H$4+=Q~xS9AtoH6kaqlupT+HRO7zT}+uyXXGy z`MT$|6Mf5l*4oy(vQYr|hn_ro+esKnMlwJ`Z2cg;#S=`5zB6L@_?R_+ELb!)?X4_u z9o~2=5n$+tLth4yhg|Bmp9aX|=ujP@Ckc(5%@>Eu<`Tp=KlK35Y?bM>5=QAxH<$0tp@^Yq){K``#b_4%Py3G#Z zFY2%Hrlz@E6u!6J9jNc_?Q#TNJss|#2c9Bh8p$IhZW15KEXgI3#YkOp@qOecps!-# z$Vt6H;s{6hI9|;(@uP&E$DcC|=`RQbi39c&Iz&!6MYcj`bzHTat zp}+5-;_UL6t%H!Lc^W6P3;b^o673ZpPYRE~--H3XE1H5tX4v5{lNN+i_mWQ&d2@^` zDP%*8{MNnXIV?4sG;f0>m^_Rv<_Z?ZVY6vVG_>eRydyGy2jl^lQ>7xV5?v&H2EQ;{ z>5Nv+CS5FNY2Ll#ZCc*A%}8A5S0ZcC%1I448@X6GtCW|EjHwJl)J}__MAjTOJbf1U zd$25mNlMUl4BF}b>C6s?IBs7@%D_P7^l1c385oh(IdNTsjKbO+;tS{R3xmwhgDlz> zhUe7wTVxjt!M_0g{VQgoEqj1fEaWO75CxenY))0PC@fJp3 zj!W_RAui@TWR6bPCEy>4LTGTKg#Z;I51JO;{KftoDe zyZ8yzS*~TqGPn#n`W$3_1!SE{fJOY>^6>xcHg76nOL%b8jmE36+zuXKVz}sQ7FSK7 zmw+XMRaO+OMH{ZswXC)A&!S%y?MWoSuSdr77NiYnQ1A75cJ~CDyn$eSx39y~8;Cr* zc~3%NdVFVRcYADOPj}Gm)2*?){JkD`2cFuxD_IR4PaFtzdVIdfpIg6?@V5u5vov7u z_4>(eZ`b;}u-5DLb?=W9l(cBsY3#F>u=6-adI?uZub{T3gx$m%(hU8i({ZY%B(+Ho nkRpq+0qS=eQZ-z6hHT^w|m{@D1$lMb?xg`hVCPEjD;!bpvxwbC8X)-He)6Xm28Q?Tm~97 zWSttFleXiCW5W-eq9~xU!u*;r?Nm&dQ4^!d%#1`6h$JNbV`5D7ocme^A@SwibARug zbI<*qbI*Nqd&l+$nmU?m4g!o!igaQ71gLKoMQ`;)WA(B;llV(4p<1AE1eoGG>VEJ^%D1VtWt zwbf!NwrLgOW{hgJ!iT4|a#4*xYhxmTd$Ptv9oj8DB7l!s7OX$f!fq?xC(DHupOz~~ z9+RttHMt=3G%<~TWM2^v4ONnY z&0O3?GiJlwVx0&=|H9J92o%I1GXw z!Ju;)MvFa?2_C{D#kVbEWEz>)ODg!U#TykGMZe2XXbd;I%F5Wf==(DJLfi_2Lg%&o zJ&xlp*QOXyn(`IS>*QKPz6wS>ixFw#Hjpt0hVv;es)#&HHXA654q=_N@-_H|OT-~3 zDCFyCDS57na~S0naSN2%g_;P`Yu8{ue;jJ5;kKz@#WBJ=R8s0&dr^I#) zG>wytVWX#ZLktetpf$o8Xl;>ds#DYA_FTkF2@QE1jA*3Q!dFx=VB#lm+7l2l{M+Lu z=`dWjcdzc!V0cNl)mUCcheBneBA3xp`QBBJVZ-ToALL1xON^nG_FdjbG9^Ar998k%$U diff --git a/src/test/samples/golden/imgview-debug.app b/src/test/samples/golden/imgview-debug.app index 0ea9543d6af1c546067ba863be16a3e0f4a7bf7c..b9c7c921201e78e1a3e1f80a644c4db4b81e2d39 100644 GIT binary patch delta 889 zcmYjQ?MstU6hG&2SJ$$Umak|moU<2U&FtkvgIvzHvex7&zlbrT5;rRdufq6oUxb;5Lz0V(JUd= z-0(ffj^U7(vy+^Sj5%bnYa#~KUERku7GW%A zcXN~00;iZvXH+qo7#)n8jB&>6sMoT44f^SHR%ZV#M1)Vm17&_I@9DRVbzL{-2&#S; zc`J?idnopSD2HjuD<VTosOpo6{zk)@T+pw-K~c+Biu=4pUy<~gN>m3{njZ*EXF)C~%mA2G{XL3G7(Aou zAFyt-;vRZocjook0InE41C$@B_pd7e;4fv2Kkp0K+yDRo delta 907 zcmY*YO-xfk5T1FmrB+lhT7=M~glAC^YyF9MXhIcA!M|slhg{Ims3}pP62L@Fpn{1F zG%4AG5)X1x40Ak^J;rQ=rnaIkVX z9#XgK(eKe(bMx{b3CDWCl}tf>d$_v`ViOK26PqdL2e7o_4&{S-o+}3j$#3CRQ98lN0cL|@=3*km3&bd z!8q^PG=j%?eoia8`R$w~(*kV_nfN8Ol**?M9H$XQPGZ z=r;esUyz-L_p&sRkqm_#n8ptli9~sEM&T!HC0wx`Pw2L943oU7%!z3pqPWN(QT)Z{ yD6aBVidMc?b<+3Nq&nI4LVye(QY+BQr&VXkJv+c-qfmgF#wNe541j(z!~OtCncM&X diff --git a/src/test/samples/golden/imgview-release.app b/src/test/samples/golden/imgview-release.app index a4888cbe47e0d2ba680a049c5856bd08dcdab480..f8c82b3be2b733dd04d7a236b34f96adf5f37dbb 100644 GIT binary patch delta 793 zcmY*XUr3Wt6hHU6>*li2B%9i*Z<-52n6vfgWzg2PT(w-^_(}*7hsuO$l3yhR=`f>Z z(#EL=pYkE-rME>8VF_0Bpas1I6(wlUpV3}=E3k97LF>Zr+;h(FoO6Hoa=(XGL*77l z(47H*4O*?VM+uD3Uvd-Jw(Q*@&mL{2e!UtX{bl;CZcb~a zo^L6dm`Ml8yF}jXL#J$ZFD^lNWb}d#vyZTiIa-jCGg_^hxJ^#nMLGz)_OvNjLyAu1|MT*r30GqgB3s=0TF-| znc)T^fXd=qDDTH*n|&e(H${j7NRa?hiioXlQU~?GxD1Ic-8u}+3|^&Z=oO@tE)rUq6H$0=k>?Z1i*iokuZDuz1pODs z#}Wg{WNxkT1IPFFgZR{L&sldY7}x`R<8ZOZxIy+6x|&_=Gxm~Q$6I7MJ|pYELbJ#1 delta 778 zcmY*XUr3Wt6hHTCcbjX~jM`i*+H@{x$yzD(GVm)mXJxH#d?v(-jfz>ROQt~uI#g)b zVBcY0H0J2zV^-iCl@NQqkmS`s(nVp+cgqkWadI zsoB+R&=^~rp~AT_@R!QaGn5)lLvGF^%H$Rg;$i0U-XniTH7m*c-wGrJKh z=d&CBQmlMEzvivgz!V!aEog^d6XF*pGc&rRcGie}(jjKWjC72-abEJUHmpjsERSQ` zW?2VXbOS7acXiA9PjoP;$IINq^f=9137_$!OkZ5)W{EB0x?z*u#wFuNTWEM>P)SFx zjSR$Z{T&WI06uBBxOX>I36sUi9+ABuTPXTXCw8z4c*C}@T?Sd%PC>??8oeYOP`M0^ z0Q@BIQ&15RNjxx+EMcLzj4y4D@h*7CgG3-hUxC*cW$V}kQCSghC=?;mcLNVYhW7wv zocuI6MM8!L#)$PlmAK^$DZ=2)sQ=1OkhW8yB=EE2T2bw+_#5g(iF&_c1v^L-0Whl6 z!{n42Z4r#7>=nc90VWAb+1k`$U?vDSHK11zt(>Dq>tp;pyxS?Rq@<*^6Ty|H&EjYK zl7#CH2hKT;7Y+Mg=~xNhx_qpHKMAYY+U#RLP$m3@(}Y%>C+xu0W*@(41Ne^i7Ox|A V0H9av381U5UwP^RXb>&Z`U7I~uHygz diff --git a/src/test/samples/golden/inc-comm-debug.app b/src/test/samples/golden/inc-comm-debug.app index de7e85229d298184525c236affee516ac11ca776..dd404b3346f684c5a4d492c85daa26def8b84c6e 100644 GIT binary patch delta 444 zcmca;a>c~fCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&!oYtvhT_zs%*33{isChl z3hY2$a!zJmHV1=qer~RRL24d1oE4H>FnPJeKgKnamr3d~Zk&8gQlIs?7z4x1$y`#} TG7XXp3@lJ%UBV)QT9Ehvdp&?3 delta 433 zcmca&a?!-rCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFH)V#^7B>piTn7mR_pYiDAo09shzr`3BnkMr~Y0K0~Lc$$ngiBaN IP$Lo_0B==%DgXcg diff --git a/src/test/samples/golden/inc-comm-release.app b/src/test/samples/golden/inc-comm-release.app index 7969e2f491dabf4d8349bfea980408a916a8784b..5c7b911f3f63b6e34f5a21f7c69c99e11583b6d2 100644 GIT binary patch delta 434 zcmbPhvcSaFCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&!oYtvhT_zs%*33{isChl z3hY2$a!zJmHV1=qer~RRL24d1oE4H>FnPJeFUB>K7fI?fZk&8cQeWn|7z4vhCWvV+ OVG%(Ml8}f1i2wk-1b!p{ delta 423 zcmZ2rGS|e`CBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFH)V#^7Bz`d-n7mX{pYiDAtCIRMzr`3BnwTKkT*4xPY9$#Mn4zWs E0Nng~F8}}l diff --git a/src/test/samples/golden/inc-commdlg-debug.app b/src/test/samples/golden/inc-commdlg-debug.app index 5f530207b64d9e19482ab845befeef8c56bdf6f4..b673f37ab1502ebf739a28b586644b7ac7cf0f8f 100644 GIT binary patch delta 376 zcmexn_{h-KCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&LCg6@Y+CSVWKz5+49Y6>Hf5 delta 388 zcmaE4_|4GPCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFy~E}_qOVsbi={5N^J VguaXtFuXuP2?8!*5kYE5d;nP6Z?XUY diff --git a/src/test/samples/golden/inc-commdlg-release.app b/src/test/samples/golden/inc-commdlg-release.app index 6c108576fa44fbd55ce6d17984e6e34d207f0137..34269ef4faed8bd88bd0396cdfe716b3553db71c 100644 GIT binary patch delta 370 zcmX?PxX#elCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&x7+Ro!1OWgF+iB|n delta 382 zcmZ2yc*xM!CBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFy~ETPYMVsf&CzRW){ W1_l*SXaa#tSVWK#FyxpaTm}FFb8W-` diff --git a/src/test/samples/golden/inc-compiler-debug.app b/src/test/samples/golden/inc-compiler-debug.app index 1f2fd7802cfc456101d15dc9e3dce5fa325dafdd..62a6a8b8a62ba2dc4fcf7eb74bbaec9fdcb8563e 100644 GIT binary patch delta 376 zcmX?Pw$9AfCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFQ;R1Y!v;1_ov*-wa5DKnXj9mVmOCOte*-SfVp= zex`|fXt(GSHU@^yr>&PtxH>~QVqZ1_O`Oo_3S^Z7S-o4-K$;B?9A@y$D<~~t=-oD1 zhfz^^GElHPlqV=-3Xl=-!T_kW`G`X2o7lq>I^P%`h&`M(VR8jyE>nrx!(RFec%RROSKvOgL zCg-uLG4f5G%Bl(^ce5HX@@;;{%EQEXX7X?L_l&KRUvpfa{Fu|%2;}kjq{L#k{G!~T zlF)*b#FA8oYM`JH5PJY|A`s^Q@p2&E3&h7ZyK?yoFs_+ASxTRA>*T{y`ZBM@7#Q+F RAqNC5VG%)5QjmZI@d0OMYm)!~ delta 382 zcmexm+Gl3#65(pNfPsPGKM<$@=>Q;R1Y!d&1_l-=-yKMUKnpvBmVmO?Ote)KxB%qD zCl(Y0XOxzt6_l1R^lniD8PmH* zeX$X+4Y_;;7!ORYm(piEIeER5zRZ6y W28MiQpd1jmghd49Nii@$xeNedh;M8F diff --git a/src/test/samples/golden/inc-conn-debug.app b/src/test/samples/golden/inc-conn-debug.app index 7fb5974e0efc2861f6235b0e6d75eee99214bc2f..87872e908afb510cf583b707a8bd7b72bce6935d 100644 GIT binary patch delta 410 zcmbPXJl)vVCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFu)gzhLe*6B(-IZ07DNHupr5PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFQ;R1Y!v;1_ov*-wa5DKnXj9mVmOCOte*-SfVp= zex`|fXt(GSHU@^yr>&PtxH>~QVqZ1_O`Oo_3S^Z7S-o4-K$;B?9A@y$D<~~t=-oD1 zhfz^^GElHPlqV=-3Xl=-!T_kW`G`X2o7lq>I^P%`h&`M(VR8jyE>nrx!(RFec%RROSKvOgL zCg-uLG4f5G%Bl(^ce5HX@@;;{%EQEXX7X?L_l&KRUvpfa{Fu|%2;}kjq{L#k{G!~T zlF)*b#FA8oYM`JH5PJY|A`s^Q@p2&E3&h7ZyK))wF|L_hE}_r3Y4UO)`C{^Q34IwW SU>LCg6@Y+CSVWK=5+49{Dr`3Z delta 388 zcmeA(T4ZGF65(pNfPsPGKM<$@=>Q;R1Y!d&1_l-=-yKMUKnpvBmVmO?Ote)KxB%qD zCl(Y0XOxzt6_l1R^lniD8PmH* zeX$X+4Y>^Y7!OQNme6NBHhD6T{4@Em VguaX)FuXuP2?8!*5kWynd;owba1a0h diff --git a/src/test/samples/golden/inc-debug-release.app b/src/test/samples/golden/inc-debug-release.app index 177c975350fb013d488609741c8d219bc36d0257..d7fdced33dd1062298286bd4b4db9497346a7f7a 100644 GIT binary patch delta 370 zcmaE7c+1e%CBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFy4FQLzPY;wPZzRVvn W28JL|Xaa#tSVWK?FyxpaTm}H1yKeRX diff --git a/src/test/samples/golden/inc-designer-debug.app b/src/test/samples/golden/inc-designer-debug.app index b6c0bebdc967f2e70a511927914ffc63ff76eadc..79c11e849574633c3bda99013a1e4dd8e2a6ee6b 100644 GIT binary patch delta 401 zcmca>dePL@CBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFv_L%!E@2TtyhwZi$*puj diff --git a/src/test/samples/golden/inc-designer-release.app b/src/test/samples/golden/inc-designer-release.app index e7f07536030c7e26941653bd730beddac3e00e16..0594fdde24d5d67c86dcbcf564c07ec9d8e2a3d7 100644 GIT binary patch delta 391 zcmZ2!I@i?JCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&ka delta 394 zcmbPhy3*9vCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFQ;R1Y!v;1_ov*-wa5DKnXj9mVmOCOte*-SfVp= zex`|fXt(GSHU@^yr>&PtxH>~QVqZ1_O`Oo_3S^Z7S-o4-K$;B?9A@y$D<~~t=-oD1 zhfz^^GElHPlqV=-3Xl=-!T_kW`G`X2o7lq>I^P%`h&`M(VR8jyE>nrx!(RFec%RROSKvOgL zCg-uLG4f5G%Bl(^ce5HX@@;;{%EQEXX7X?L_l&KRUvpfa{Fu|%2;}kjq{L#k{G!~T zlF)*b#FA8oYM`JH5PJY|A`s^Q@p2&E3&h7ZyKz52za%k707*PFuP8M!WwNWxB*rz9Z^-B~Zkfy> htIzsMjDg|(WE)v+nVT{U3@kuPLBJ&}BIqs>9{`UUifRA= delta 408 zcmZ2$-E3{^65(pNfPsPGKM<$@=>Q;R1Y!d&1_l-=-yKMUKnpvBmVmO?Ote)KxB%qD zCl(Y0XOxzt6_l1R^lniD8PmH* zeX$X+4Y^M9F#2x(EB1hK^GAsg=E?ao slNb+7W|GxsJTciwR-g5s7z4w`$!W6MGV5g^!3)&K;1U)Qv>AyH0C{wG_W%F@ diff --git a/src/test/samples/golden/inc-fileops-release.app b/src/test/samples/golden/inc-fileops-release.app index 76bb7659560600c336a77893eb027e22f8a25c05..43d1f398d46ef74e645980796a72d118db09d4c8 100644 GIT binary patch delta 450 zcmX?a_0!7MCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&bAB&bA4eo11E0FroUUQudd%4AoWKE^eZugK^#Zkfy? bt1t6PjDg`jE6^?=a0!bDx+w#R7!V%-8SaSR delta 398 zcmexqb>7O>CBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFtU$Fu;1U)Qv|fgRff>SO006<=cWVFu diff --git a/src/test/samples/golden/inc-font-debug.app b/src/test/samples/golden/inc-font-debug.app index 68c9dc568fccc29de4d77f71f0c715033d121412..e7e4ee80c2fbd87dda8640af40db18437cfb48a2 100644 GIT binary patch delta 376 zcmaE0c*fAyCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&uHb^7-Vc68bVf T#2FY^fC@msB`hN74-y{$OZ#ph delta 388 zcmX?O_`uNCCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOF5PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&t5i`ZCYO7#IXW R1_6OfSVYhdaY#Ud_yF|lYfJzD delta 382 zcmbPXxW>@dCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOF5PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&oabgd`?_n@VOWRLnF{Q g5O8sI3Uvpnl2!l(e}*^%0}EIjC=(VDl!L?v0HDx*&Hw-a delta 437 zcmdmEaMHlmCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFQ;R1Y!v;1_ov*-wa5DKnXj9mVmOCOte*-SfVp= zex`|fXt(GSHU@^yr>&PtxH>~QVqZ1_O`Oo_3S^Z7S-o4-K$;B?9A@y$D<~~t=-oD1 zhfz^^GElHPlqV=-3Xl=-!T_kW`G`X2o7lq>I^P%`h&`M(VR8jyE>nrx!(RFec%RROSKvOgL zCg-uLG4f5G%Bl(^ce5HX@@;;{%EQEXX7X?L_l&KRUvpfa{Fu|%2;}kjq{L#k{G!~T zlF)*b#FA8oYM`JH5PJY|A`s^Q@p2&E3&h7ZyK&SrU|5 zQk0mS%E|yVtEhyV!L0}=;hR}poSHJ(kzHnTskjQ`n#r@o^%*x#J|(U%^IVL9p%G{t Q2)Kkr1Z9Xr0w2T&03q*tSpWb4 delta 427 zcmeA$m}+3_65(pNfPsPGKM<$@=>Q;R1Y!d&1_l-=-yKMUKnpvBmVmO?Ote)KxB%qD zCl(Y0XOxzt6_l1R^lniD8PmH* zeX$X+4Y^M6FtSemC8jIK&EQs)n49XG zSzMf&!q4CiWQQkamIS4i6eT97PBvthnVcc6!gyeEued(r(aC$o^<{pGF)&mC%>eh<$z6 diff --git a/src/test/samples/golden/inc-help-debug.app b/src/test/samples/golden/inc-help-debug.app index 46a9412a0de5bfd260696986cc907be4c9ba1b63..40e7f40fbdff3b54609a8f2d7a2af846e47c1c79 100644 GIT binary patch delta 395 zcmX?Ou-m}aCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFkIxCV_>KSDgyx*SEo?-$%gFG3d}%Wg*YUr!7^bHK{ZHx0NbE;XaE2J diff --git a/src/test/samples/golden/inc-help-release.app b/src/test/samples/golden/inc-help-release.app index eddeaa08af99b8b353c7e0b563b3e72059bd8ce8..67950aaa88a0e64ab7d6b7d5f677abddc74e4936 100644 GIT binary patch delta 385 zcmbPX&~9Ms65(n%fq{YHKM<$_=>Q;R1Y!v;1_ov*-wa5DKnXj9mVmOCOte*-SfVp= zex`|fXt(GSHU@^yr>&PtxH>~QVqZ1_O`Oo_3S^Z7S-o4-K$;B?9A@y$D<~~t=-oD1 zhfz^^GElHPlqV=-3Xl=-!T_kW`G`X2o7lq>I^P%`h&`M(VR8jyE>nrx!(RFec%RROSKvOgL zCg-uLG4f5G%Bl(^ce5HX@@;;{%EQEXX7X?L_l&KRUvpfa{Fu|%2;}kjq{L#k{G!~T zlF)*b#FA8oYM`JH5PJY|A`s^Q@p2&E3&h7ZyK-IS;o@LODXS>XNX^NatSDa1xMuQP gaec;(lh2Cl%RCojU}ylU1Ob<@h@fk delta 394 zcmZoSm|Q;R1Y!d&1_l-=-yKMUKnpvBmVmO?Ote)KxB%qD zCl(Y0XOxzt6_l1R^lniD8PmH* zeX$X+4Y{uJu(L8GB^DQzOcoHYW;`&t jM_ixr=;S@(`ZB-87#M1S%0R#+EF!2voPmKE!esyeMQU>Q diff --git a/src/test/samples/golden/inc-hlpfile-debug.app b/src/test/samples/golden/inc-hlpfile-debug.app index 8038da4cd831ad357bbfeab9b7093be2e7fc2254..34cb9627ef4e13d88e7e143e2119077ce7399ce0 100644 GIT binary patch delta 376 zcmaE6bjirpCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFz_Ttc7m#N_Kh^50~3 WNqw0~!0-YEB?!2LMFdSl;sXF=ZgDjL diff --git a/src/test/samples/golden/inc-hlpfile-release.app b/src/test/samples/golden/inc-hlpfile-release.app index 080d3a1867838c136999e19c59b37771f123f7cc..8821440e809a5c06037112d28f2d88904b1c40e5 100644 GIT binary patch delta 370 zcmdmFG|$M^CBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFz_SVEui#N^8o`ZE8- X7#OC2LK6sF!Xknu0Yi=%!esye7W;43 diff --git a/src/test/samples/golden/inc-http-debug.app b/src/test/samples/golden/inc-http-debug.app index 1ad8ef004af5627447e8212168b03ff04755744a..a48793edf0a716233dec248dd2f9877ea14ce94b 100644 GIT binary patch delta 409 zcmX?Vc*xM!CBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&Na delta 403 zcmX?Pc+}9=CBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFghd2>MdAYhJSTP_ diff --git a/src/test/samples/golden/inc-http-release.app b/src/test/samples/golden/inc-http-release.app index bc789e0e5545075ec4b25ee820321c5d6d281497..8922288b7d6c1675c8e17208cd5eeb630b21cac5 100644 GIT binary patch delta 398 zcmbPe*k@?#65(n%fq{YHKM<$_=>Q;R1Y!v;1_ov*-wa5DKnXj9mVmOCOte*-SfVp= zex`|fXt(GSHU@^yr>&PtxH>~QVqZ1_O`Oo_3S^Z7S-o4-K$;B?9A@y$D<~~t=-oD1 zhfz^^GElHPlqV=-3Xl=-!T_kW`G`X2o7lq>I^P%`h&`M(VR8jyE>nrx!(RFec%RROSKvOgL zCg-uLG4f5G%Bl(^ce5HX@@;;{%EQEXX7X?L_l&KRUvpfa{Fu|%2;}kjq{L#k{G!~T zlF)*b#FA8oYM`JH5PJY|A`s^Q@p2&E3&h7ZyK6V$3Iyq4M2;-W`ViNj{8z;L-=*v78V_;whSpozuVG%*^#36wU;sXF|sB?b+ delta 392 zcmeA(oM>q465(pNfPsPGKM<$@=>Q;R1Y!d&1_l-=-yKMUKnpvBmVmO?Ote)KxB%qD zCl(Y0XOxzt6_l1R^lniD8PmH* zeX$X+4Y^M6FbYimCFVTYPy7hufyu%W h`iw^>J4@)x{1#(i_y$x20xn?@K_A5#7?>ek1^{$Ub3_0D diff --git a/src/test/samples/golden/inc-joystick-debug.app b/src/test/samples/golden/inc-joystick-debug.app index 274187441063f3280d43ff0f51888c76d677d62b..7b4bf53032a62c4dc982310bd94d0232abe3fe1b 100644 GIT binary patch delta 455 zcmeA**Q;R1Y!v;1_ov*-wa5DKnXj9mVmOCOte*-SfVp= zex`|fXt(GSHU@^yr>&PtxH>~QVqZ1_O`Oo_3S^Z7S-o4-K$;B?9A@y$D<~~t=-oD1 zhfz^^GElHPlqV=-3Xl=-!T_kW`G`X2o7lq>I^P%`h&`M(VR8jyE>nrx!(RFec%RROSKvOgL zCg-uLG4f5G%Bl(^ce5HX@@;;{%EQEXX7X?L_l&KRUvpfa{Fu|%2;}kjq{L#k{G!~T zlF)*b#FA8oYM`JH5PJY|A`s^Q@p2&E3&h7ZyK1I%(Q zOU*0c28n@LK$;iM@yts}tw3Q;R1Y!d&1_l-=-yKMUKnpvBmVmO?Ote)KxB%qD zCl(Y0XOxzt6_l1R^lniD8PmH* zeX$X+4Y|(nFse`fC)T_9l=vY=#siak mCG{CkPTni2&-!1Cfx&9>M@elNOG!w`0<|!>ghd3|BJlwgQg$c+ diff --git a/src/test/samples/golden/inc-joystick-release.app b/src/test/samples/golden/inc-joystick-release.app index 0eb71940b2491c763a056634731c4695e9c4072c..a2530ee28822decaec03532ba56b070e34ba6564 100644 GIT binary patch delta 445 zcmca<++t$u65(n%fq{YHKM<$_=>Q;R1Y!v;1_ov*-wa5DKnXj9mVmOCOte*-SfVp= zex`|fXt(GSHU@^yr>&PtxH>~QVqZ1_O`Oo_3S^Z7S-o4-K$;B?9A@y$D<~~t=-oD1 zhfz^^GElHPlqV=-3Xl=-!T_kW`G`X2o7lq>I^P%`h&`M(VR8jyE>nrx!(RFec%RROSKvOgL zCg-uLG4f5G%Bl(^ce5HX@@;;{%EQEXX7X?L_l&KRUvpfa{Fu|%2;}kjq{L#k{G!~T zlF)*b#FA8oYM`JH5PJY|A`s^Q@p2&E3&h7ZyK1I%(Q zOU*0c28n@LK$;iM@yts}tw3LiZPC$GB0K$Z~ delta 393 zcmZoMxoK?c65(pNfPsPGKM<$@=>Q;R1Y!d&1_l-=-yKMUKnpvBmVmO?Ote)KxB%qD zCl(Y0XOxzt6_l1R^lniD8PmH* zeX$X+4Y|(nFse`fC)T_9l=wbI#siZ( jCG{CkPTnc0FY{lFfx(Iis0;{P!Xkn!B^el)AzTIkd*5^o diff --git a/src/test/samples/golden/inc-json-debug.app b/src/test/samples/golden/inc-json-debug.app index 3de70d29621b6826f30e080c592b7d88dcd50b3b..a9fe494dc51d8b0f29580356b61fdb3507a45345 100644 GIT binary patch delta 376 zcmexq@YKN8CBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&xMH)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOF5PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&v0L^<|!mF)*wG RN`Zh&SVYi7aY#Ud_yAkpY<2(u delta 382 zcmdmJaMZxoCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFJiF diff --git a/src/test/samples/golden/inc-layout-debug.app b/src/test/samples/golden/inc-layout-debug.app index ae6a90976e38e870972e124811f1406a0175800c..44105e3cde37201b4faef6f1f39fcfac1c31e75a 100644 GIT binary patch delta 376 zcmZ2&Fx|k`CBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&HE5kXUSY delta 388 zcmbPku-d@ZCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOF5PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOF5PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFQ;R1Y!v;1_ov*-wa5DKnXj9mVmOCOte*-SfVp= zex`|fXt(GSHU@^yr>&PtxH>~QVqZ1_O`Oo_3S^Z7S-o4-K$;B?9A@y$D<~~t=-oD1 zhfz^^GElHPlqV=-3Xl=-!T_kW`G`X2o7lq>I^P%`h&`M(VR8jyE>nrx!(RFec%RROSKvOgL zCg-uLG4f5G%Bl(^ce5HX@@;;{%EQEXX7X?L_l&KRUvpfa{Fu|%2;}kjq{L#k{G!~T zlF)*b#FA8oYM`JH5PJY|A`s^Q@p2&E3&h7ZyK?#QGp?CDMN*$}>*OPn`ZBM@7#Iqe SfO0_K5*85@EeQ!o5FY?!jcbtr delta 382 zcmexu+;3v*65(pNfPsPGKM<$@=>Q;R1Y!d&1_l-=-yKMUKnpvBmVmO?Ote)KxB%qD zCl(Y0XOxzt6_l1R^lniD8PmH* zeX$X+4Y~aI84pZukkn^9IeCMmzRZ6y X28IGApd1jmghd49OENGpL%0k8UYBob diff --git a/src/test/samples/golden/inc-music-debug.app b/src/test/samples/golden/inc-music-debug.app index f5ebb4bc0cd506581564f6f72cea808ada39ab19..a1c5434476b1ea9d2981db505622f6be25bb65a8 100644 GIT binary patch delta 424 zcmX?Ra@NGwCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&Bu%UtEr#m%+ERI5XKNKQYB6 zu_SS_u6P9x7evf8FC{f)^HTAzjErk0&z96@+%);Lq(18lF$RW-lmAO<%T!7-Ft7j( N1Ob<@h@e^|J^&2!e3bwI delta 407 zcmX?Wa?HfmCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFZebzr>3=CzH*`%~(iX5PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&Bu%UtEr#m%+ERI5XKNKQYB6 zu_SS_u6P9x7evf8FC{f)^HTATjErk0PnXnZ+%);Pq`u4xF$RW-Oh9vhz$Gjqs8SLV H=pa4-WqEn6 delta 397 zcmbPfGRef&CBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOF5PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&oeol@65neuj|b0_Xg^ywv0ps6hw;kU^6p z#pSiRL8^jN^HPHIb5prMoB$vvGcTPT#12YLE(0?HQd6@x_lg@ca&a*DrGm^WNuBJ- yt}yw5gbL%D$*hw4j2kAKO6s#c6JuaVXPTTPsU_np$-uw@^d1Pfghd1eBJly-1gU)h delta 438 zcmZ2zvccHaCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOF>zefYI51;FXG0GlMN+Z7!OP?lhkKCGI^P#KHD!b28KtBlW$9E U$=m^kFevarz$Gjq=m8QR0H5PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&oeol@65neuj|b0_Xg^ywv0ps6hw;kU^6p z#pSiRL8^jN^HPHIb5prMoB$vvGcTPT#12YLE(0?HQd6@x_lg@ca&a*DrGm^WNuBJ- sE;IR#gbL%D$&8Zvj2k8!O6toz6JuaVX99W(2wcJ)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOF>zefYI51;FXG0GlMN*u7!OP?lGJBBGI^1tzRWK%28KtVpa%k% Ou!x{Lz+h&Ea2WveaDP|; diff --git a/src/test/samples/golden/inc-pkg-debug.app b/src/test/samples/golden/inc-pkg-debug.app index 5dd455d2691a67064e5f9db07716b9298caae800..8ebdbe9d3c5e32e387d7057894541654ee916b78 100644 GIT binary patch delta 652 zcmaEEGQrH&CBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&Z39Xd@_qmz#4?00)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFrJ3~Nr zx_?3HWLfbnIUX=4EiJXkEi)&T55ddNNl7i5yk0zMv#x{)TXCQp^pXZt0_z%Y|(@+m1T TnaPrn*aSJuB`hLnIuaiM(bSdN diff --git a/src/test/samples/golden/inc-pkg-release.app b/src/test/samples/golden/inc-pkg-release.app index de09fca9ffff27faa4a889f3c70980f8ede571b2..b5182ffbc9bd677cb70a4740c648ae64ae4e67df 100644 GIT binary patch delta 641 zcmdmNa>vxxCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&Z39Xd@_qmz#4?00&$wao3rT&MXJQNtVW5Nr1TJ9_K{is5bOquA E0FtJ~{r~^~ delta 508 zcmca(y4l3mCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFrJ3~Nr zx_?3HWLfbnIUX=4EiJXkEi)&T55ddNNl7i5yk0zMv#x{)TXCQp>om-!{ez%Y{u;ue>% Oh@i=m3=GUrrvU(TzLSLj diff --git a/src/test/samples/golden/inc-pkgnet-debug.app b/src/test/samples/golden/inc-pkgnet-debug.app index 992d9ab0b0d05d900501f40de333463b9032f7ec..b840e5243757bd35fb5288dbda04cd4569e319f7 100644 GIT binary patch delta 405 zcmext^w!AMCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&Bhan(4-7mGozaTZw xbF;qq3`WK^lTS$KGj5*zLqebRr5FRlhRHgT+A=eN0muT>4FWD<5kYg2_yFWpb-@4t delta 403 zcmaEB^x4SPCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOF5PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&Bhan(4-7mGozaTZw sbF;qq6h_81laEN~Gj5*zMM7WZr5FRl29OOv;1U)QG!qz%pb!QD0Ga%Ai~s-t delta 393 zcmdmKwBN|qCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFxC{V-Q*;af diff --git a/src/test/samples/golden/inc-prefs-debug.app b/src/test/samples/golden/inc-prefs-debug.app index 0d2a7322cb100d46e725415911684eb740faec16..6aae6cd7f453c5574bf23754472d5362471d7970 100644 GIT binary patch delta 376 zcmca$yvNwqCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOF~cfP*R`q*knf_`Db#W Wq`u4_V0eLo5(HerB7zts5qtm|!*GTG diff --git a/src/test/samples/golden/inc-prefs-release.app b/src/test/samples/golden/inc-prefs-release.app index 26ba981678a84420fd73b058f76dda932aa64f48..a5f5c2674ed399b7987c28748570e39c6c7002d3 100644 GIT binary patch delta 370 zcmbPZ++l3%65(n%fq{YHKM<$_=>Q;R1Y!v;1_ov*-wa5DKnXj9mVmOCOte*-SfVp= zex`|fXt(GSHU@^yr>&PtxH>~QVqZ1_O`Oo_3S^Z7S-o4-K$;B?9A@y$D<~~t=-oD1 zhfz^^GElHPlqV=-3Xl=-!T_kW`G`X2o7lq>I^P%`h&`M(VR8jyE>nrx!(RFec%RROSKvOgL zCg-uLG4f5G%Bl(^ce5HX@@;;{%EQEXX7X?L_l&KRUvpfa{Fu|%2;}kjq{L#k{G!~T zlF)*b#FA8oYM`JH5PJY|A`s^Q@p2&E3&h7ZyK>#;V_Y*?QBt3A)8s%&eVG?xKyfA@ S1_GC`h@cn1&;kV{2mk=X@oMG( delta 382 zcmeA$o?~q565(pNfPsPGKM<$@=>Q;R1Y!d&1_l-=-yKMUKnpvBmVmO?Ote)KxB%qD zCl(Y0XOxzt6_l1R^lniD8PmH* zeX$X+4Y_XfF&>!AE2+3;?((ZodEk diff --git a/src/test/samples/golden/inc-print-debug.app b/src/test/samples/golden/inc-print-debug.app index dd67753f4861ad88287d7b9d089fe0bf5c7aa1e6..bf9540cf685ff713b65e2a438c745450cb7294db 100644 GIT binary patch delta 410 zcmexo`q9+ZCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&F^FUwjSIVG%)!NPGZY CV|C*I delta 436 zcmexp`p?wXCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFsfXFN8!OiG{ij~D}k-sDwM+A>;Fke~;e$>0(e J5u}I22LP!efBpaf diff --git a/src/test/samples/golden/inc-print-release.app b/src/test/samples/golden/inc-print-release.app index 2987d89e9cc94e79cfce52be0041e44afe78c41f..1c883e6afd004758b92f342cc79ce41dd2b07e16 100644 GIT binary patch delta 400 zcmX?Qy4Td!CBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&F^FUwjSI)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFpeXFN8!NJ?Mkj~D}k9y8D!AaDtb2-1>bV1RNN E0OWIhxc~qF diff --git a/src/test/samples/golden/inc-resource-debug.app b/src/test/samples/golden/inc-resource-debug.app index aa4d299ecba01c1367804ca78c5764a341ac8d35..8a0cff65fc55c3af303c697f78256508f953258b 100644 GIT binary patch delta 393 zcmZ2r*lTF(65(n%fq{YHKM<$_=>Q;R1Y!v;1_ov*-wa5DKnXj9mVmOCOte*-SfVp= zex`|fXt(GSHU@^yr>&PtxH>~QVqZ1_O`Oo_3S^Z7S-o4-K$;B?9A@y$D<~~t=-oD1 zhfz^^GElHPlqV=-3Xl=-!T_kW`G`X2o7lq>I^P%`h&`M(VR8jyE>nrx!(RFec%RROSKvOgL zCg-uLG4f5G%Bl(^ce5HX@@;;{%EQEXX7X?L_l&KRUvpfa{Fu|%2;}kjq{L#k{G!~T zlF)*b#FA8oYM`JH5PJY|A`s^Q@p2&E3&h7ZyK>#+;b3FP$xKQqtC*}Rex7m7Q;R1Y!d&1_l-=-yKMUKnpvBmVmO?Ote)KxB%qD zCl(Y0XOxzt6_l1R^lniD8PmH* zeX$X+4Y_Xeu(L8GB^DQzOcoM9&v;<+ nBXNDklaqxc^jZIlF)*B+>?WZtb4DBzv_L%!E@2Tt7m)Y>9gTIz diff --git a/src/test/samples/golden/inc-resource-release.app b/src/test/samples/golden/inc-resource-release.app index 3542ad1ea6e032ed953176de818bbbca48034412..27b1204f8dbb78b88548ee2bd06e4a97a4f04a9d 100644 GIT binary patch delta 383 zcmaE3aMQrnCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFUV_-N7R0aYrVG%)R#2FZvAzTIkRg`o@ diff --git a/src/test/samples/golden/inc-richtext-debug.app b/src/test/samples/golden/inc-richtext-debug.app index 04af92c05d66bad1a05781a1eb13cdca8fcde031..c9576ce8dfaebbecb671817c5027ca32d9d5be0c 100644 GIT binary patch delta 376 zcmZ2yFw4N!CBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&*UQq^7Z7$;`%af T;tUKdKm{P+5*88Ug~SH{oEL19 delta 388 zcmbPbu+G5NCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFaBKho diff --git a/src/test/samples/golden/inc-richtext-release.app b/src/test/samples/golden/inc-richtext-release.app index 397b622816b77e05eb1c171f9be6030640207e98..7a0532b485fee2c7b4b8c371f9d569c333cdfd70 100644 GIT binary patch delta 370 zcmeyR|4iT3CBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&*V#~`ZBM@7#N~~ RQXt?G77^qo4hcvQ9{|R=Yij@i delta 382 zcmaE+|4ZN2CBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOF5PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&3Z_a7kiOi6B%YKNlDZ5Ft*8P;g0V!DL@?n{XizKf=h=)X*}ZG$|)D z**mootOg>+hmZ!F$cxNRE#N_LUGq|qBqz6ud&+QwWF1ozQ+)E1v%!)ePH0{ZkUROD zc&;bdeXc2B+ky55BxV)~gY-a!;0^-20xB4snV!dwq{ulV6=<5ZgdgLY$z2lqj9Vw~ hlF(;+EylnQ#5DQ6gqDo0Bm)BrG{juOB7z)|_yCq|zbOC! delta 434 zcmbPeven4eCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFQ;R1Y!v;1_ov*-wa5DKnXj9mVmOCOte*-SfVp= zex`|fXt(GSHU@^yr>&PtxH>~QVqZ1_O`Oo_3S^Z7S-o4-K$;B?9A@y$D<~~t=-oD1 zhfz^^GElHPlqV=-3Xl=-!T_kW`G`X2o7lq>I^P%`h&`M(VR8jyE>nrx!(RFec%RROSKvOgL zCg-uLG4f5G%Bl(^ce5HX@@;;{%EQEXX7X?L_l&KRUvpfa{Fu|%2;}kjq{L#k{G!~T zlF)*b#FA8oYM`JH5PJY|A`s^Q@p2&E3&h7ZyK5Hh!7`4D7YlGV6v~cO}G$+-PL$65(pNfPsPGKM<$@=>Q;R1Y!d&1_l-=-yKMUKnpvBmVmO?Ote)KxB%qD zCl(Y0XOxzt6_l1R^lniD8PmH* zeX$X+4Y|(nh>8K3Fz}y^p%|=xvXHp? z5PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&cCNVid(nwttBI}t~T#}fRlbYh3 zk(igBnj(xQyt!9$0yE>9$#Sy#jGHF=$?6Nf5My9S1;zsixVSonx=(gwmsS8pq^~Rk Q0}EIjC=(VD6o|wJ0Gz^^&Hw-a delta 413 zcmZ2zbJE(@CBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOF5PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&cCNVid(nwttBI}t~T#}fRlbYh3 zk(igBnj(xQyt!AhhnaEBWGPvF#!ZvGWc6iUh%qpv0^)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFSO0012dcme!H}mJc{dMfa@l<+N zN!AG=e=vqHXHXO+2MKXG^#tZ_2%nv0haWal@rFn=?ia@n|2h{GnGq&pxfjnUmOFAdXvXh=tB0$xdRIu0Fal|F!5u2q_XL3fcVaNE7^ z{e-TC2^xHD!V8B4AISHkj7)57SIASdS+9RlJ)sa-t|KXJo3tbe)$h zaLH@i9mlLNIVcoSuQRZIuF^^`6{n+E$AFo*c^9w z#wc#_REaaw>BC34uNU8ob}FAUOwD#^{*;eWzmHrL*#fIH5B3d6^f64wM(9&mjE#6V bb`dh*g(tD$m~RguZv3y^NYBiwq5tZ?dvVgY delta 486 zcmca@{n^siCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFuxAU9XnUDA2y*<5hCY_m4 zQZ+)zUyLElSrkRdVM2UfJ&w5(BH(7(G&)IcM5sRpc#= z2~O@}vk!G1#ZWqG1+|L$&``o3BUFbSF*P10IkRGDilK2Wy3HzQj%fgk9q;XNn;E+_ z&uw@V8yojla)JrRTgmF|jxx^;5pt(5noR5cj~ONyti_T8DxS?7Ia?BrH!@jdhQW0Q zeDdl}&oL)#&dPW!Xv&ALhscyExO3U!M$p9xO^djc6`9pI+P_btFCY^irY}LnhvS=j U2swanrxTg!88xuqeR`exA4Y`GiU0rr delta 460 zcmZ2(z2DN-CBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOF@HB_& diff --git a/src/test/samples/golden/inc-sql-debug.app b/src/test/samples/golden/inc-sql-debug.app index 2ae361c52f92bdeadc96d011e97e71686b25851d..64d425f7c8f038b46b24d488ef6e5b79b99b89ba 100644 GIT binary patch delta 426 zcmdmExW&-cCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFJK^0O%=s AP5=M^ diff --git a/src/test/samples/golden/inc-sql-release.app b/src/test/samples/golden/inc-sql-release.app index fa964a78aea6171a04c1c008f4aaa02601a60f3b..01d4251355dfb2c81785eb4a02bd0b8dbb507bd6 100644 GIT binary patch delta 416 zcmeA$Y%sKSiEuTXz`(%p9|%-|bN~=D06_l1R^lqE1 z!>Fh{87SBt$`h0^1;_|^VE|Oxd_X3z3nsTQ&t$ZntiY1V=sLNJ1oC)%Qev@Neo<~v zNoYYzVo54PHBe9ph&_Nf5r}hucsUU71>$3yUAd0)hzbIkFz}y^A*HOMxG-n3g7_M4 zPKMw>AJ^jK#Ddhxj_fj%Ux=$Pu9?g$q0hKsvaN)^%rh|thF?&vE@2Tt55ytC4iW+Y DC^dGr delta 415 zcmZoL>@c)-iEuSsz`(%p9|%-{bN~=D0>mcJysqj#yyi?v%hC7oP3+(y0}|@QEpI4 zaB5LzVh+r63>O(Duiy+96#|;#0mO+wJROMF0`a!ZhFr&aL5PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&eb^5x`eX?>Y{ TQVa|%Km{P+5*88k2#F5>|BY>O delta 388 zcmdmFe$3p~CBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOF<_+)b+`FC=< Ww7$$IDM(NP)iAh(MFf3A;sXE5PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOFgeVM;v X3=ChHfpS3L5*88kNs561%4GlmcQkMc diff --git a/src/test/samples/golden/inc-xfer-debug.app b/src/test/samples/golden/inc-xfer-debug.app index 77f83b22e66f733e45f0c918492996f252bb32ab..ef85a39a6e602ef58031ab060de5fbfdd1826692 100644 GIT binary patch delta 438 zcmca+^w`MOCBoHk0s{lXe;`l=(g8rs2*eUx3=GUrz8R1Pff9BIEdgaOnP{svu|#L$ z{7e(|&~DKuYzz#YPg^gQaCL@q#J+35PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&FU2i0 zC$*Rx%neFSPR%S!6+q^Ir8irP=QA>{nY>y;pK;^l+Y)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOF5PLXn!sH6ZT&5DW$?q8*8AT== zGMO++PEKReVw9Uak;#}*XYx@dHAd6PZE|}cLJd@FOvI0vcqwC}@mV0aufTm{f zP0nLgW8|AWl~ol;?q)S&FU2i0 zC$*Rx%neFSPR%S!6+q^Ir8irPXEQRcnY>&=pK;^l>k|4h&&3!RR)CxU1TJ9_LH)pR J21Nk~006{4e!~C& delta 407 zcmZ2)w8+TTCBoHk0RscWe;`l+(g8rs2*d_l3=Aw#zB`ZxffjZMEdgb(nP{sfZ~@4P zPb?@1&L}NO$uG~Fn4~*#LWT-|Xy?;TSB}`1-9UqT%NPs~9A@y$D<~~t=-r|QGNyNr z`eYqOMeS-JyE~L8D5D0*2zcQDRNj0)gfZD7s& zf#l@-jE;;vlNFgv7zHPXF=;W1O>Sf|W>lHHl}U|Jck&&knT!)Amod*|G@UHKlF4X0 zxr*f;+YO+`47SN}tZIyGlUrF;f#hmdBSyB(_gHzD823zm&HkRTaPn=A>*8+tMY%yG z!Kp==i8(OOF?Cvf*t2aEnHOR+FZnnUfCSNbJYZ$|LHw*PI0X;_l+$?r7oe=juikM0WK4 z-UZNlWy>eg^wx-H!L=6B*w$J`)Ec689u4y0;T>@(uWwu(g`IE8LXnp*3EUCsW-WV) z2=nSN65V{<*V?%t-qzYVykkMUwoNvR6&MXlB}zn=9FC(zA6bh-!~nS)M~Xsu0)t|- z?8JTIpxo-%CyJ%dStrWmcIVfwOQdyKrmQ!DB2(@)0`z;#7$Gv-uN&P^oRK%&N^Fro zc`nGyX){J};ESrOR!weLxx$J@8kbZ>YhXQry=YFQSw(X>%|kTbqj|EuEIq$_?0JfO z5p8+&p=ll@7-1`NX8<_i+AZx}%c*2+ju1&(D-cA8!Ae_L^F+tdu zq@~2lB(>^Yy${cK@*PFg2duXzKupEOE>gyF?5R4gx0As~Of!vzX%PQE$ zceb2|ji zgAX9BC6?xOGA~9<@_CN>O((UT}JLH!}z?9rdJI6?g-Ts;j?H#8SP{hbuMDfS*_ ztN)02hJzft#g2I$AU48kp#WWcS|A~7E373fiM&~t;eu-Ah@sa2s~f{kbdv>B(=HV0 zQ2a3n^238(7uaMPdfhM@+2b-Riw~tULQL`~1NlnbVD6!ZpfcG=lj_mM_mV4gq9>Xm zKADAHCs;9lm#VidoQ=kG&XUiV0ls~&`;3|3<4P-q$2~epK5c^H8AQI)xqMn2p!|Ff zx*li2#tbLL6~Mh)S#c2K8AwSfKu!wkq@Ws1p3t$o5Cz{qMNO_ySQ7wh=NcgNeGWmA zzgR9j#G@Q(?#RZA<>09#k*_$$!lVd!*!U>3)ZGj4*)pB&rnGTVNuTmH6FupTDjoMbgBrOjK*v* zEG}RKq~}Kk43wfNNGUa9rBuJV+NYu!B%Z3kzPG8pgQ00G%H3dkmwpSwzpyT0pAMtx zXVJ)QS3y~6QFGW2K0CMT>_Y7uJ;6m7V`F^<#Sx`DL{VX@Ta4yTR#(IKMmIw*Pger> zoG@&hXmKA`wJBp^&SgTbMb+t3$(qpeWCqm3I^+ksib_2aLz6!psy?M^7OQGe4J=#D zjyHBWzB-xIrvFC22F+sDvKEafRyK=ow8&0KqJ({{FZos}&f6i1>+=Pi15>eZd z*Bwzi6NQjyq)+meir;RIZD`EloqirqK@Tr3hh zh(29BnH2W{Y;WLn6hhDrSJu@>`9s*Fi=0$fwY*`eY|1+mNP`(w&C_e@mqwT6fUjt8G~6o7oVpX{=gO)LuI1Wsyc-JN;L}A%D%!!xo1uac9Y){%q`U$n*XzS?TYC z1DzV(=IIomy8Ddc*GWXEs!TV z<6o#^q-E{am)w?>F*tQ3!9Q%dAk5@XCpykZUr1{dV<~&7mR2r-K1b$wQ%` Xxz7#(Se!6EX-=hevj~8j7>|Dem%1*5 delta 2204 zcmaJ?du)_N5TBXeUat@I3Z*U3!WDXV<<$p*Mq^`(6xs@S+;MFI5!wJLkwPJD31~b@ zF`>bR!UhR}C;|!wjDZ5tKw`B*40oA%RwJpr@FzsU5K zaB{HjB_;ybidnt&6HnXCh20%(Hw(KKb}#F=KHm1B8?Zj{q-Fde&-w=}!DO9HlQBhC z(l|`hE%XTH=o93_iTX0_!VcXWw+p9ew_`aL>Gh5)&K@?bbL!`n51slI<>mRXnuN~I zD=L}rw7wBEs;9xZ5nbj`A7dJz z1B7)LnRl7Jlms7Qx8{ho54re~5W?n|_)P(bm`>cv&O}d8n&Z~C6g(^pBnamus_CL? z-p8#7!9l|N0M!g|gQaCoS*MpVoO0o{giV(P)_jDaWvmT+-lt+)5o9-DK0_ymV$O(k zku5$iF64}_Z9SLheS?9;vVMoE0?vtCDh(hBp~hT5Hv$QxI5>u9a}h&%aUwQ77ETNM z_xfnYh{$D$oxL--O7n{qxf9G)i--qv4ddnL>%2I?{EE&L1?Gk&B``N*YPWF5d*Z$q zyOZJ=fe{4mIKU0igyuIkW9#3MIqNEdOGNVz*{n~BIHA_uMz?}svatW;gA@cT`ftQU zDEbnRar}dBC)ng2bjQOK62zs!R57|M92PR7f1`*3+quroJvpS=-0znIIVB#XK)zTy zBII^JP@2&wvXtXVJp+v1qh^9<*HMp}1s*A>64>mrlH?{UB`Lhbff-CYW{dR@>P{k# zJe4GJvLYull#gSjG-6IqIl9D=)aGp1(6G9v99$8TF0y8mVFMc$1(qpbY+PBk!YESD z{x7Z^vWvCYE>g_xCQ6Nho=F%R1h(bGw%N&FJD1jLpp-?vyQil0tm%S+<6#z9y z%8G#G<$@&OYbqp;pLL3ZRtZj`fV@6tG7TT#c8IX1h^DmYqy6rmq|j|~g|>AO1#);7 zF;7XMMw7zMTTHRw5aW`5<1z~yH%7U2*l^@H8rhDp!gg0d0k51qbAUI#h!!(Tpwx=H z+5PT9u~6r7`~y*iDNrPCsz?Q+Dd0}(PeJvADVRqA>j7C{ry%x#ET(9?h0W5~EriVr z(ff%$IxA$G5{bPeD@ld=`mmACrNIdM?*Xo`SvMf64Yi>!1Q4=Hy4~fX@-nG1Xx$Vx zt$n(4VWUVk_c6jYqrHz&2@y+WdmpFDR@oU#3pY#H>>*Vx)sQMJ#veOAV@s?=F08$IKk2IZkMqVPFG&)m%oBf#fk3QR(G3Fgi zkT9i$Nq?S`O?h#;Fg{hIHy!KsFmE26wHY42(zV_+bm?{82~_3K2fYP!!eJ@j@@7(( zO^))79AqVq9Wn+`y26)3f7!xKzHCy;GCAeTqFkHo^<`3_O?vzzXsS)l@n=%VCR_bk zw8SPy`JDQ+KZBZ-zTkg?T5YZ`H-}zRdS-4#g8aMWbLuy8Gj&()P)yYqa(g?kjeT5U zmmZy;k7sm{=ZktH&%OE>&$snY`T3NRq=yvb@!xoALB4Z-I>27HURy9u-Sq<;&~F!v Y&wPC>z+$UR0j5<}2k%V;kOr6UKYtzb_5c6? diff --git a/src/test/samples/golden/notepad-release.app b/src/test/samples/golden/notepad-release.app index 9dcb44664a61d4d5ad95c461cf29dfa8d95366c0..b6dc037e214caee697ee5f1c15b21a7a2a097583 100644 GIT binary patch delta 1700 zcmaJ>Urd`-6hHT)w=G|xmSv0<%J^z&b(O#t!qntS`Fz(x-xQ9CQ%cf{tD;uy#&YF=aA2R}6MJ!5sSqXjwc-_( zxT4I8n|Lu}R#c;A7!Uz`&#-L#g;4vgI3&HoiZjwf-!G(JSm*wdS&H}$|1qtK3;3t` zM|_mo;^)G<IHtOQLV)0;)TE{9{ReB=T_( zg>I5Um343H%Zky|th0a1p` zA>B1XU;`T)TmwJIAZYXu;sL4h`IM1>utk>}feiUwkJUt>kD$XI5J$1w?hneI4CAZ$xm4L`ez1^7)5f(- zY)&x3;|9I`_*1@f{A~uI@+oDSie5Q8Mc{0Gcn`mAPB_9jZCFj{9_g1&Tk4m5G2K2j zRJ0wTjSMC9gg*s&NaIlg`GRgtz(juVUUJgU%sYbY~LPpu!;rRvB$1F<3Z-n z;OlNXu0LrJx6tA)RE`+vZ8tvWwu9m{p)3>Uz{hTjSZ6NVp9V8ZoG!NG;bM!Dn_|0(E!NrWZ9ywo<5ub< z-YtGwX=N$T>ffIR>Xv%a4o^`g|Gn`sVXen0SrBtw9;ZRv#y9tsi97fyeHZWneIKCe yD-#d#u&+${SH+XQQengEzA_oi1Na;7`JQn;>jikEFCsugduMpB4Z){Ul6hHU&-L|~dt)0NStqU!E?Q{iZ2gCSbW_5qqhK}0WZlW96P_o4h<{UEt z6BuCPWMf-S2wC_dF-{X8aVac(AhF3z)DQS4eqaVNKNu5<1Y-PPVuE_kdu``p;!V!I z_jlg8zw__DyV`X=-O$|K68!)G{-$Bj8YdAXPw4S@6aaxOCy<|{@g!#zh`-N@_sbivwUdwUc}oP31<6Su~W^vp$FY3{)4T=Ulrmfmn=-_}IdsSH2@m&?7`Le1L z_ROksNpTZ@bvzVr;vMJhveqM`!-=uv>!X8xL;2+3JHSsibZsgnmNt?;k_#kPNtQBE zJ^0v(M^v>}3Gyf>iT^R0z0BXJZK$ zQR#wEve(8PN-?E}bTh=j25vT(fjUqv;HM99ZkDcEEh7PmA`|ZcGUV^3uQqbsj>_92 zI&iNyAQo`gTOT4~yBV^_0lI^29UcvIyGL1C$qRnr9Tzctw!E%pFSrHx)$`z=U-YXN zz|UF5;5{d~O6Lh|pr3EV#q!Ehw}h2;;moJr2`90%BD`bY|FT$VHYSQWi{u@NhbqDW zs{$$x>P5H(AVKn#1`L>z11lBs13a(3 zFG53kqWO~?wZqy=duqy>X< zZdfq3dBwos_?+AYoO2w1c5GixzRP##IQ9iQ@x>v@<@@|N+p!(L5yItm)D+TjD3!$8s82+9G|hpj1~Fs3-C=( z#?s$o{qcRLmP|jDnSJVPYW|G)vC(gvj2(IB@GmmON8Fwn` zMrOJvc>^Ei)f)48h1X%s;uF17#ymdTd&Zc~ul7DP?&0UW+lubtOT4O3iZAz$hF&w@ z0N1YJURq4&!@aviHU2&>=JMf#AB$lI|B3e}OY+0s2lk8JTjAEZwD4_hiGdZp{XNl4 zystfxNwWO_mf_QkPXeEAe7=Rx6ZrgaaCz~Km$_t@1(l4QAqIGA@R1d!7|Y8!ljGIE zU~-Zg6frqP4MNO$WefC}H4!omr}Q)oUHAL`>j0q0OfACL!0D&*W`<-khu5Ze9W@!;yrB~#b zVj@=L7A`o)eZA=%Chi-|SO(T*+#p^J(Fg(KW|;{t<7vE6b0H>o_{+#K*lpJ1|$@>Yr~BLb*z zz2kw6zO4Dx`nr7-d14jUjh~b)k$c3HGOG;1LR>r^)rMXylQfa?~sNOg@I5 z%QI}^2e40?q7zBbPt39+h8f_lqMRgxU<@apsOWn>aN$d85MuJxJg;T^Ah!4~^MIv% zsaVcQnb%CPEr%Abn_>lrx>E|X;u|VTR}y?CQ@Mzh7_39S>Ge&R?!HAcLT z?jn8Prg=H!oLm%^v8w~KhG5or&C(EyeS~=ClzS*#ngPKOEEo7Jo3#mko!7n@%Rwr4 zO9WK;4XkIf%|(p&pbpEANGHsm8{{e}{W8!{itO1Ook{xrmSDbM9cQ0h!d`LzeIDGz zO9jiu<)1XDg}cfJaME(UPyZYsj*C@Xxv53F>0sVX!YV*uYF1j7%BhIj9L8TC^5iDY z_##r+&d*y#0S({L7Ft{%Rofw)M?+1{vYSEjEbI}N&o$(Ol>F?{~&9l&4aj1bkX3224Ta3&mrzaCR2vJaHeEtBMN@4q zq}oDMZlN*cR86MqkH5<6FJU%~>`m6Z;UZL9j5<}MuIc2ja^#{Jiu6WZfFSCjAo0`A znQ4`VT%BJt6d$v!GHk^Va_V(Ff{?&aZC50+9)F%fk@OomutR+rp=?A#ue7kAr*k$E z8DX}s0PXOh;++Gu)!Z7rP^+hfcg@GWh zM*}gMmNZW@(xuSMI@qyHUaf&`5IN7~W>zi=7;V2uMqBJr^kgvTVxqtT9?4~dIxF@r zb)igNW97vfHb++x!*#Tz zJQlti2}K?99sY@GG-2?sNb;A$MO1~5NyhMqUy_uxh-mP7rdGIdngiPUC>@aGMoV@_I{ja?!;>j8`<;VYE>6l!TCiZ$vN>yir%jt(NmAV)jMl8dbD_N39YF z+=L}kTv#Lmyv6Z>TU)Ae#mgv(* zv0XfZ>fS&tJf7T{=U&ShO-%2zL_Ze;L<^llI$Rc0)6ypISMf-(uFj<2vYfZbWDgP; zJeLNIPZ4Mz()+@Db@~MNVcO)_&kAjL@uwX^Fux>e`Vc{_*HFT{rbCHsnTN-e*&!~rg{LoP{ZQ+zsfQ~b_yHef;WGXm@D5Eu6$Dr^2hkrv#A{Lyl*AmTq0 zf^LoeGQ{Hl8(3U^o-ebu?K`C`gr-yFtguZ*SQgv*P<5Ft4sxRjjOiR*!d)12giQva znq40yC(2UWiZ#G)oGzgp>KBNBazcGn9n8xVQ|#J}vciUXbTiSQ?@q?qRGn8-jvxeD zgKx5svNq0$lWo<8>q`@uQ;c;{Ob>AkQB2YP&(H+^g$K1;YJ%Qh2UKxT0RI+40sKRC z9;tU)0kxk<^AviU5rykZ(A%tFj^1Sj0RQeoF;Oizzo*Y9<+XSz+c?zkGF zbVV(fsaM?_qPi|uY*dF1^N!(d$oV({U{DyQ=w7HtH<@!9$pKzMYG_e2b+b)61h-9v z^W-YqB@gMoo2dLiK-Y~n905K=j9eY?+|KN{D;cZ*o>38QR)%Ey$!J^{wV;!S$`Kc{VarAh}yg~n9`{s}0_bHdj zKL$||m!AhK<>8!*RC8*CotsD*5f-QYvOUBCRv1dE#VK+->@*X334w#4swT)Vf>2Cd z7FLDVWt@U)csK|ucpBwYu`7(JG2x)Vq9|5|O~2aWr7E2BYIS}%D@>6>SLPmW=loKQ z_2Nn)M-+D=Yz`t)9bBSeKT_jzbC}{+)%7g_R9+vZ_#IqX`vYTC8C#L+POj@4Sd$va ztmy4-PxcQChUaf6nl&cCtmKpDx^S#Ypwq(UI&Yaj{IIKU1CuDX|LZ?O)V3x`Z&Rllumuy*CY9 zMlbPnX7GhYmAqW>*$5p|z3Hajp1wqX()(ob%&QbtnY4tXo-Y_ZE zu}=%Rs_yb_N(hlGOJov?zNss@eJ8z*Ao}Z5ePj|fq09#?-<#=AY}YWQ35c3tfs9^J zu}?;74=$_w5%)%4yKv+P79*=fcc+*24s1%KGJ|WbHH?~3IdD3a?sWQ7z5S`ozRQO| zW64Z_@YXJ!J7cuQwxkE(hwfy1Q&%G0nQSk=2t2rd>F>jpniA>lN&Na*ZFv8>W|Fa>$a`wtDDV9>CC17k@3S>i`MX|a>ooqnSKm63f8=dz zoy>Q8+19E2QSY0rQ;kKI_v6+{JmbCJTFu|_O4m*@tP-zw?PPw;i>@75WY-oe(#f zB#yD_XHP(>bEeaRmT5ueCiojl4X1f{XzDocE9ZWs$o)){w=Rbon=$;Ja?y?M@D z<1ZSr-au?e!lTog|};c#CTF8zPo;c@pCc68mlyZ zriX_xd|3}4i&YrKl|(zaoNe!Er%=-Yv#qH+wS8wk_(oS!+-w+?i^gs1cPIPz#naur ziS|4h*{wuJBXS6WmTVS=?Wp(3lMz&)WJd69c^|~a<7NyH{8t2kJ$pmK;M_}WSjg+W zdp6)c?sR-KFY?xJ8r4n*9FukSCI?#5@CLnN4n85)kNvDbB$G; zhnn8q>2|;b2sEM1cBazpYZEmWdgwf3<+_Fl}0EJ(b2iUG_(o=6|v-A z@)@qae28MunDAXp%OE;~^>0^l$)?l;gjiyrfmSb+jVO6t#GXJVOQZ*GO7^3V@m*97 zXy6YFRZc4+os|;AowA%@dlKEdwWmo{f)%hQGM$?8j-~g!6=^jD34A$+oJot()3pEb zs`8h~g1k?R@l<#RHjlcNy3_)pgs8d{Gy|YZ7|ih=-@I#fF$zvld_W~&EB(KaN`TNJ zQScPT9IW25YlJs@%lL^w1uJe!^kvWzR8>_ishSe-@%pyZu3C}k9!M59CDWN?f3ltM z8~S%Ai#Mg(GhHAjz`O;K;^t(kvn!+DYv)CZ8bk&+fTe7>#uZ~_{*Tg0bC%OH} zfq}+Ef0Zhf#mm#}{+#B<-n~_Ay?eFN#SPu)F?t{#g0vRiF# zo|T)s&;S4X-~ayKd*2t|lV7U9wcWj)iT-4| zx1(uixH<8f@So4yIJN*>YVc{o=Sh5Cz~={p<-@jI=P3de3-M*rBA$8p=#xq*IYtWo z91kp^|B?q0p~xD)}<;ly!+1@%*83(|FgXRr0b$ z@Zpwd*4~d)z9qn<3NoH=5p6<}83|HdlOPN0GfLTVC^nva11zxGS>=u9P& zLRi9L4#Nf5|5)WSuPP(M-z)chV)+vSpDz&A!ZXVammvPB%9kR!dJH&2=t)F)Ua33@ zXj~1GnMHr5+;0%_H3g}F#0J1FSwO0k&Dg{tp;(*4BjPjtI&pnpUr_lGQD=5^QK{j< zj=XTg8SZZ?w~Dy`T&WTAes!HZq((|LN@9ulfc{qnb~HTyU13Q{iWD*Zwo;=RY5^rw zfqW>I((i{RxokL%0jEN_zmk-l7)eWfpp-XOLV@lxcun?_-gpCFfd(*!yiE5irN%MY zZopWoqJS4R`_NF@D@u)L+S-t|IJ<5&PYwYz*$ZdaB;bTX|5~Z*B@hQqI79IhWE>Le z-;f%~D-!IX+U#i}cuz7~I3Q#C6SiRB8=oqd5`g|p+3-*(ra}LX>~6(LGNU8u+2Be1 zBR0!6IS`gW$~H>|&STqdJMqgilv6-CRqjtrCD<_AxB{3#_(wz}$%{5# zWphT?UW<4za9Hx3+?g!#7?uwl3PVB1+1D`1@?fVQKUBaX5}???ra@?U({wrDb^@F) zJfqVoL^ELs%?x%ZRU+d9?kb=&BDaXFdob@wL*~r{Zzx0PDMP@sZSVZoFxHM2?sJ5+?Gi}U`GR~7}Tgti_HL!C0oVmnAACe)YM?mCiUnc+7V z{N_obm@h9$3*J3Wss%Fc!Nf=e%Y(ENR|{pFJgI_hUnb)}lLA?<($4r$RhD5~?gyjQ ziOIB3wOtSqs*Jd^C(()^7$?alY|FaPgtzk`D)a_>bB4BD9zY}~vfSrMBzlr1X@l(; zNKtRA8ziKi9!SesE5(v7CU_}Q0z?tWtZjOue`eAwuaUHZ%meJg09%*XMe=T~GCqk=72;g?`Hv!(EcD~Pfc+OtJt zM95B5M%_#@VRpF)^H-}Ku+FzdT}`5t~){22#4mfR;*+ zZ2+aLF>$&pL>y8#N>=?+W2OUHGwsXL#3>C9=zDF>v+&x?lwg0rZ@Ga`Y}Ug(t1F3lT^!xFw?%FTEVdy zP_4X%*W-rjW}@;q{NwCVP!RE_#MZYmFOYsb|8(PmyyRkrSlXe)5kK zb^$^$ijp>8#;IIHP4pmy*weG6T4(R8YH&C}rbw{ovs9VuMp!*a0dBez* zLVLhP#G+L+!1Y*isweFM{X)pdy=sl-fGL?XOn0}dY9K9U>3 zUl3SrLWK@{{daAbdP@Bs$C1j$5#+$ma^)~_WV3}hX5Rk;VUW@v2L5z~odst9I7^1o z&DmEkAQiFZSpA>aYICmoKegSjlT@FPRI1GPW|o%ySH`#I66&{XbW2nxM$zl%jOEq> zM%Vvi`)|#hnJUSkB?e+DB14uMh$((hQh34|&^H>4DF;lu%s@DWQwFaf&kCzw zCB2ZR-ejmyTfCg&vfK$X_X;M2tFCmC&9IM?Bvwn@H)k?6F`~;}oEvVNgfT z75G4{NqDz8YMWH;L(cZ(D|cjl1>P(4ddID&?*zd!$9PUANhZxX!q+?Q9HQMw2)gKK z93tYTP(;1i$u#~gAxS|Ly%>jbD`R!K+%boZc1LwcYc)92`4a7Uo8C?vvZ$0*C2+GK z^bW^KexC2YJ$HiFnPuKjg3!^tIx&%`c01G&FQXtK1Zv|GBFbGT&Pewf#ha@EC=S!A z)OLvxiV51^0|p-)_^I7sUkuJvoE!<@so)sTkl<9A!7Af~`?G)`lVkzL1#>8j1LgpX z|K$LJvp@6jxQ2$(1t;$;chgnuZ)QkvyAltNg9jb70LXa{(>GGyAqOgYjG75NoVdSA z8pUgnN*;CGN@|-QV^^r8T25XglXN)-Mn1#QK+~m5ptxFq<^;OT%z2WK9(Bcp4HTQfKE+ef3=^`No7(*uN~^ zt%XGca#R057fkd{GKpy`X|?cLCD)Jjw?ZN`m_!s99PK6gO?Wc13%y?dc}VTbz!`=x zdyddlQjUn@LC+#^^8QRePY6IWy(I#x!F>x6>Ar@4`V9xkur=_U5dtB5k%|d>4&MR3 z>8KqtEMjkQXg#1WIqpc1=yxbi!Qg?gyQB!N<$Uyut5N7A%{lp92U8WDJaHv4WI7%9 z-=lfd-$TM@sfoYLmx6%7Y{GL2OryH4|>u&uqgD5lpCQN%{V0s-UMsvmUsbSQcxf@OC|8|mp zHu`3;z!hp9arzz6U@s~C1?d&KN{3y3N=IXmg4!B8rusl9jHmcb@iAU320gHl6Npb< zEQ2Pc4d`L9pwOgrek@rE8d_v*EO`>Y1<>&NSaQyFW;i9597$K1fR=O;_k zAVX&^?DQP7t`{~Tr7<^wnRAIM@hXiiQs&IV_3I}VMplxe67Fao@ z8^WD7h9tpN{HB<@muR=d)R%(FIz$6^k&qs5j@z)iV{Q%6H?#18o6ICg3qidVq~jx4rvtu@sb3-tPfJ zH2C_Wm#x9)m()l6SrvEq_f^#TuUB+T9wFQv+t#`af$zIE?xP51gOmCb|i zRTfG=w!CyqjIgVAckFC+Xq4>O*5%)|ylC*k@_&%A);;(1wWoLP^pCBq82tB@mgN^# zFBh!^ zer-o@A8sm4K{QN&4<6b^GTmR>hBIUVFZfbol*?@$C}^v6BS7C#2~9R9`o3f(wSKyF$_$^WpKP5e@=w<=CPU*vZ;TrY3OXsmytVY1xJqc<8R$TW|l8z;yf z9xd5eJef8$T(CpI{qXA??eYwEOSMP&A8(u}i~YjJCf{o;lgs@V8{6g^MRlh8`kJ;u zHRiV|R+%l@*ku_=WD&o5Q=vy$D=K=s_x5EF2GrBk-<|={Zw^t>(bd+?ATj}gY|+x* z-QAa6h=To^g$NiGJ3GiVkfCWZfu!g_jsSps&Cq1lBoPcREYO$k;)yB%;zGE4cULQ5 zilrdJnAp+L)p|!$XI2&pZe9aR(0=JXDl$tL<)Y3?T%Lt;q26cakXZC?i4IyTipd5L zA$0-5rBI;Y3nf|**`&cyycEGAm}YcJ8#YvM*G>Z?<^;O|5_M2yLrCIJu*`zAVFEZ} zPrA3Sqnp$a6}InfF2=keHlxxI72CSIX#YmX1Pb?0%U$PhxO41HRKu>Z5CL;kLjn^h zBGUJe&2Yjdi2uzy$9jgesjr&$p?J4Q!&lEHkS&_J`u3)KSs>Cj*O)-rc;);O5j)bj z#K96oK^aGhJxx1z8#9t11k1{XU;FTG5=GW*^q#c}2}_V%tB`KZEI1t$L=PnsTdZ#C zg7E-kn4sSYhUkaDbObaqpr3=R^w3W>S-BWA)Bk+SuED3bZXe}8+EpUY`lq_e)(R^zxVs`zt}&C|4sgd?lSvHtcsuO^Lpm0r*Hy&++Wr+Z{qw-LTooa QCT?kHOgw@8k`L4K|NJA?QUCw| diff --git a/src/test/samples/golden/pkgbuild-release.app b/src/test/samples/golden/pkgbuild-release.app index 55cc013527d87579621699b5f09ee26a17331b48..68551f0b26cfeff6b713eee58bc3f30718008780 100644 GIT binary patch delta 7669 zcmaJ`4Qy1$5#D`wYa2rV{{RNtaM)l2f%t5|{3HhdV8=EXjyn!WfS9v=whzu{_k1MK zrhPU)N%#wprEMZb4XM&ZYT87t+e%fNltSC6N=d6GQd6~xnxsuyC23S8RV!6h`pxdX zch3&#$@gw|W_EUVcJ`au=SQ#e^RMuREqhyQFEYkH!DlIcL-=xhOY#}>nSMQf>66LT zza^&q)TM^1OPQ6Io=B{zj-H+K=N!gP|K-$gGx?{Zc?bSB4viO2x1x0)TIau4MT2Xv zUS}Ptks}#){s(d;UtB3cJR6-69=wc3=&wPDp7>ed^!o>{Up)PO?bQR<`!C9)e8|dF z$q#uGFP62|8a`k4TPyfNdC6MMSIVDTReY^{-+GK+kq`JD;|((4Pw-ZG#{WjnpP+Sc zrc8+{K2v^KEXDIjqK?m;_`8^4@n6aRg+-xu)x#YAiV{#$JFOT#?IE|UU$YVZ{$yojr(rJg?Z^s~^_ zBE>AwPbE@du|mNjKcV5gvm-3<8;YYBdBGymbc^-OR%#ky$&CSam~lTCq_U|!Y%iqd zd_I{Mj69FQoQ2AvNG#x@l;2>?VwM^?ok68i(5R>T7j%GdqmVTl<BTjuX7!-7x5D!z|V=rTvTuv6C4+n>i0$uo5MM| zljVseie!P$5EBpOMRSUBXp>>`8O*DMqHt~jwudxUL1Q%^+Xzx)7_MVt8Sz3Cy^#{z zUGI4~iUxOTq2)?wnB+;z3Zn(HT$n6)s}gK;1XpIKYf>W}f_G$lcrMAW;y3CC*d@l) zwg$y*JP6C+Y>JI=g`=_tDua4u$9W#uk>@2}P-?5ydsbR&rgn9h@aT3F)j?o29}Aa* z0FUX3V3%Y*H?TsrE;CXMR6sC9N?a$MGa*O;Vta>SJ9d*PSe0`9{~1f+t32bFQ|#Ec z2KHg|*YfdDA863298orfU#}uS*z0(3Jah+`9s{$xI92O}VIP=5RQ6+naaq9*w&RHf7J&q0>2tPdP9LwxU*T=HOnXLgDHgQ6M&HQzBH0E;A z$b&S#kTp1`K{WB8-eA$pgRgLac#bnC#UQC^;bVx>mT)Pskre`%nn|Ow)3E1qgAZyy zG)DRLs!3uCKaNA>sN2LvE61ErQv_vg9QIU9#t?C*94l+!HS)!>`Sxw@{ui>f&bs}; z8b6DyLX>dY8rrmWP|$?BdZw^|wTCJ9xHGrIj%v=P3)Y`gki+)+7{4D01Wv-3I_Dr8 zZLlhJ6z|rs`ytzMC{~l6V5%J^&a_1kAla? z6|mS&m=N8}2-Mia8Lx8|r*$HN;8z&Cp)u@6oz{k3AKwf1D_E`qDtHQk@C3VJfCVX| zAAZ2B<^X>UlPe=_4Q3QcTe;ZA;c1NfI`gaJJFaAOxEhhJn~G>KGa}o$*zsvCAuO<3 zg)Ao8>1eSh%@p>^L>!q(BD=gI(VL3w_KNJesmNYO#HMB6byFD)vt~_qPZcrreN*T? zkpqA@bw-Mi|KQgB8fW3vXB|+~;jNI!K8#&1grpyNbTbbUi28MW0|1m34xagj;sBuF-$VJS% zvEptdKv=9pNrP|Bc<2^~(t~+c^BiY*?-aE?k=OjY$XQSiTc2}|Xyi_+HC?3gF+dg` z8hBtK(t)a<3WRDqxjJc3Ml{RH#Vc(}co(3C4!i!{S=X8ggVMtnt_zdp@JAk29YR0jF3F(}I_Zr!9H7&{H*6-aFfE)G$J1ng1BHz_g>>!1OP0EJlc zR7Dk*-)Kt5Lu7dlak6y=s!pPbWVUjn+(LmUvVnNB=@6qB=mg!=a6*Pt+a=a#%O#37 z;9wSmn)UjJyz4)uXUE?4RIcPD@N1uTX2;#aG`3=rK<1+xakqK(D zsIb4lPvqqk36_f*U!WuIheaq6zlhaQ_e%=1utBPOimFaxQEkWhX&)N;4JHh3G51A@Ei4I|pWnF)qd@ zUEQYEh?^mWA?`&@9d)SCmo(HnxT#S63z?hx$SDww_X_=}8Kyy==%Ge5#Bpw^LKL?J zas{o20J||vKF*DN6V(Z)x;=!Y6JV-4W@DHtiX2%}Jwrh=UEc_rx6&blEgO*mzQO#; zGp0<|H#hi!mM>0lv;eQB`ZAX=%9$z*OiisITM^9tN)gQ6!7y!Xvkv01thf$i4?zdA zhoD2*L(oNphhQ#dN~uaEj&bH5WoBE2`E{}!>ro_gM-H0(2ZH)2;rS^mJ}LW`1?(;- zO-0VZX*$QFSR-sSj_C1lP=FD~c)-iq$BipI(x}Bq+IYk(jS7r7VQ#Anua||&i&(}< zUJeQ_;uvpvIViJ;W8CKDpt>TC@t?zi3z3X*87Yt_msbZVp$92yfTcR2qs)cKenLMS z*twi84XC!zpkR`JSiZ>C)eI?j?y}xEqv4Q1T|*LPPf*pM%t53nElx-p(OzygYl<2K zF(}`55Q5KBpxvl1ASb!WF=W-r(3|?C zV8mIc!XZ~QewvJ`hOTj>O5HOy@m$PG1LY?x%52J_r?_~4u4g1dH&gTiPz#)!8MoU?x-&eEkvUuqjMnNS(+vDs;KJ`cOX$CPlcqX}nd}V>R+D587r`(L%vg z1rT`_Rw!iz7KBrg8rX}MUSai`KmlS@3rV?VVJ1>>|ERx zy8;}L#lxyVyD?z9FY4U4Wq7ShBF58E$2M`nltvfnZIolFiiW7T#8C;vU3VClh3T!M z*(zhZ>r7RbbkkK=JYurPXD(-hvB+$$N)*hKC_H;hVL|J|);c@ufInWLnKS_Nbu}N2 zY%>ko5PAV6abGuyLm6+_(a;ho4pD7n0z?H2T`D{QGx5Z#YgTa~YfB{KJ4QygB}OxC z!^ysQdUPTd+~r#_*Wq*y_ZY^m;Y=(!+e440((zcI{Qm0g`FX7G-lL;~@nll^)^y}g zW1+(XJ4WKE4!L#Bp=AZC+ut8g({zzcES)LJlGL<{C|_OEGiN$+w#JW*hKKJPy@fi_ zRA%C~wWYjB>Ddh%6T_*N;h~XOIxaivm)lCJ`A8z!r_{`#L6O8jO7&(^4}%i@$5db0 z2K@u^RKA(2Bh^2wrlmn!VwsrI&@veBJxsk>BzAXVgq);Q6dFS7(M&qln^~fW$~Z{1 zz$qEMNTnYnTFJ!Y^)GQbw0>Q|EEXY0g^#4R43F-KB{CCFt+%Y2Vwapsqy|Fi#Be&1 zIW}(+H4@J_4EglNy6QQa+mRZ@)+FP7ErYStK)kPL3VGtyjsKihx`2gZy@z81@qltw zJiRe+IDRZJ6zhx2m$rPTCXa>E!vplTK9?nXl6wYYnLMUMVUJ)VJqU$qE+H^+wDn=1 z^(>c7xy73;)>g-udgYEdqauW$iM|2}rEN81`t`Y{fowkDt-;HGxw)fc?^?!m2x|q7 z2=&_VPZRaZ6GT!<&7g5$<~HuoCq@3J}Rt^olU6603Wc5)D2y zVu4IngM6>E_igAs^`~v6edBa%+`tegvV_b7jHy^;V0fbZ;-U z_JD%MXYzIkB&u4 zEY%xNcDE!Gy@#_0Whq=oexPsmQ|un3?wv>C>0{AUayZtP9YyY(t`8Cr5$BDDz6G~Z zb(0kXF`t!S2Ey}P`O_U`c$tB{rPvjv@{Juai}Q(fp(kg_RH$sBb9nPxVj~#u-xCN98-AMfU3YV19c%F))~c6vS#*2lJcb1Bp~%TdXIZ44C8Zs)C}5 z{5@s>#PeHIeP#enn~sl;HpkKd1rZ)zd-L$oK+o_|?Y#V^B;a)@p2|$alr8v;r)8kK zq&DkccS2=*l^$0CDmh=9x+{GoE}QPDlw;j~`C9if{w?|6?i&7Mx$0mIe_0;D^DTKE z&v)eucnbLeo?Ws$R#R{SzI>X>FMQf|gu8;+;?g;EsFv?y>=3;a#)mky#0d~b_A++H QZ10xcd&A{%jSnRGKiFCEv;Y7A delta 6470 zcmaJ_U2s*^72fOIyDxzR@=G8PA_+141jtP!7%0&jgpdG|!#Q5SDqv!ANiZgW{D^ku zv);guFPcXI*N=K?=$9B}(vF$iM)Cc>mz4tlyB*bQN z@7-tZwbx#2?X|wO_BnT%zJHNAx9r>6`kWBrYy36h*TskMNzV}?X`i>@m;WA_Wqudh z@y{-FHkG}NhlBk`j`+hTjt`EX8oMyC=EC{m#?tKBzn$G$wEvUGAo$!#(fZ~^(K~kJ z#Bp)%l_nN*?)BwrHPtsii1B0D5^wlB7`Pv2!2a;>mY)4=|HboXKWlw+|Ha(-PnwMk zb%gGn^O|1#8+@w|sEbNey|a$W)gEURRja&n12w8wohDkNt~$@qd37@J47IDWDwuPhc2sRd-rc5i?Sgi95o%L*?hu#oV73^GBh1uSPa zfs~eWC=!1L{ED?msetW6-{geDdWLfA?Nd) zt1$H6O5j^-_+6NWG1SsL)tYA<1TAEN6#`QpoyxdSGj@lJHG_riy7bbT8J6~V)x2O4 zLEc%DXYl1>N<%TYhfmXM2u0RHz!GEXLsW!m|A4w_(c)_it+N|NGVFmK3A%C_M*}e@ov}f~M;gSJ!S#FC=P*ip{{O7yfYXi%)u{^g ztA~C#=myHWgP=jCc0}#8`DOaFWzb0Jyt@gLF$D{!I9=-XkXkxTQWNEEVuiu794f4! zMI@4Nbylmn(zdApjfkb^L*Cb@>vH~?hX zCYxnxxt7xAfMpA%FOx`!RJbPxGK)#m1eV|8Ee42*3cyGUOVoPW>1whWtrY6pj!*9#@TM=XP~)No~+7Yzo_DWht?}$6E$Q5UfNmI!V^Q9r`aP zQ7lC+l9i@1De~sD)<;g-s^eTUG4V!sx#?Odolvbxhaxt*iKPVB>~wRSCo|(tU2&GpGqT(g zXW23%%T{IyLT$Qdu@aRCZtXR323vm^f}IIl88Ft~ zgHx>lXA&s9n$ir8NfWn7@L{_`PZ?pDoq9%TPFdb7A(}{(C+wzpoVKNV7U7@|bUUnH zL5l-NV~r*m1m`VwklZ=VsJx}$N^)2HVC6Q{HE3*d%YbSs@6HV=eb{FyrNZUzG_m^z z{D%={x@JSyXTyy2ztou!B2YpAituFs1sVe8br(}E^*F^91LE3q@fij4MdeF zS$jzCcF#*{Te~mJMOMCx-;4&7Sy<2MO}!4w82o4Auanb~VI@590)@+j z4ab!JZ!b%zs_Pd9cZ(-P0xEGo$X&JU|E4yzm*v8|0UV-9BzZS6VNfevsP02JGs6cR z3gjH63lV5)SS1CF((q&)`lCk!^FVhqYal(EHRv5Z8l;XM4Jt>E8i5NN8L&l+24Pcb zwlKI9tqKtZgegFKZ5&z<0VA$I{_nX$QVD?9e}RWWmqqkaKumaFLl9VzG6C*SLLMSH zJmsn~{Y-BL7tSrZnCOH;*u^d-+&qG1#_dCFvx-$DMaG*A{#5x&T)Q$D$q{lmB_-ei zx1bz!bP{;Zct`{>%YSyBDqgW%qg)LLoE;q;h}dx>!1Q|}sX^3slI9`VQ%Y@I5(OZp1IlN*OV>L#PC!Xw$dm|8Wo*eafRVHfE95uioQybACf~A+?1K3kd+*hMSd;7;BCW;;FLuW3=T98%)zG>;3|mT0nG%0aOwtz*Cl(7)8sJFo)IOA2R=bDqjJ!FBL)q2GJ&?FI- zJQDU1T!k{%lF>WxC5UB7?zeHj(p!b=_#!2=pP~Hv)Tb22b99 z-|*1rsCsVWhRIJiI*wZ0**1B)^Ix;onQbpk4)#2epfOdtbD?Y#;(+s%UzKr0_YFem zH;`}zS2X^>K9K}Eb*~e`*ktG++ds^;3-MM$P3&B7`+6bt8v@L61?3=P36dq?RiA!U z7}D&omfj(R$wJgy&^cjrfI^3EoZ>rD{d?!aWLk(4HTTw^m9XoDQr=`2p4woW?wr8D9<#X)-}5Bw)vskS z!S3PLiqR%*u=gr*mj8I=LiNozoN+T;;MO^(s??3{I_FbE$-A}Af6Q>wUE+xOTJ+yBEHcBI-L=j< zGu*Me(W#rSj_z)8+UBe0cc+~f&BVvxdBqGDd3DYk%u}sgug3X<8Rjv(Y#3hhYU;M1& diff --git a/src/test/samples/golden/player-debug.app b/src/test/samples/golden/player-debug.app index 3db2b4e963ed9ca473e98b18bff2d82cde34d307..bafac192368b1dbe435790e84049b231b4dd253c 100644 GIT binary patch delta 6517 zcmaJ_33L=i8m^jjbx*>TaGx>~0XZ~)fPk_lKrkR^+rt2Yh{P}w7l9=~aCKiZAU*@K z0UAXN9;>b=>O+v_5El%hAcyCwtIM&1fFgyWuITP&zrVV=XR^!Nyv$5>)nEVd{YU-P z8#eJhoA}^ia~R9g&;9XBf0dU1+ggvGT0XeT^2(meA1m+M zHL;|{QIoN%V~gIe%&AI5C!I`zuR!@>|TASr%~f0b8S!@;s!@Lc*Ovo!~><^2K z_|>E{W=HvA@*+%*HX|z51IASt)tpts{*`>F)1H`B>fM;X7|Ick z*mBMllex5p1?bp{%XtdnjR5^mG_$WM8_c-UXaSGPJBc$P^;;Ui5{L$~v2ua035>*i zR}P)ogmDqZFDK~>Ndl~C$$gIfnL?a7;KbxTWGL}mWZ2CxF&^7?6F61cN7zdumQmmk ziYpASNskt8A#L^uT3|oaX+V!F4Y7&sXBo3$B;(=G+|8sR z*;o@!Z6U#_gnW|RmIp{-8kFN~9JZSmkie(B-?#11`x!z5R+A&1BjRd<6*?CY!ShCZ zM-&4lUkE)U*Lt6~ZR@^dfQ)t-Os)%sZY80l$9lzxvms7P+P)#A@YS-L)9l8KSzm{g z+#JMELhewQWieTsK{sDT@=lkagRB@~x=Z6Y1Rwy;AtMMp&WcAkq`Z8~u;dfSXhX?DU$^538$8LcjB2I34+Bz!*TAq--h}q;x_?7!uoeHU_;~RC zikyE&f$|z+eH)waQ1z1EdXKel<9=tr$uEEy5#H$!3a+!711tyE@AVULB{X2`4{8dC zMzn~HB;qGRK-G&Prw<7@M-%a91*NrO{IlNYE3a6&zt9|P0x3AxWo${-WFf&Vq4GX6 z+bG#CApucAO6g=P^iwQbV1iOCM}#T%0jbwmGtw#6S}0@(0b0sL*+wYUX=htupQfn} zqB@IgR#DSY1lAQgK#R$kccw!-x08VG;&mU_kv^;ZG^>k*y_AT%hE~XKYKQg&>A>00 z5{JS>b{9cK>>>O@9bq<|y)X%4hB)1c>e4i?UT&s%hfW09n+!v$k?;Bu7=Tgs50Qd_ zP=Bl}61v0;6+zYx6FM-9X`mfPyN4?&R49>;9B0$jLRTA?@4J;vBOCCStPxC(6xD*1 zEpig1%{BT_oo%nO`AN`MvWW6&RE`Q(|Lg-ut24sY(X@)0*_uCb{l3P;StY7XHqyD9 z;oy_k3y|E&m@3><@T3!36c@oUIYybV9b$G6nkktO;*ImxOi{~XJckYTzd##{#9)co z$?l8FvBIJV87FM~;E-z(3YaL&0NOIIeXO-RRRCvwsQKBO6!wL*tDL+-MOPR@-a zbAhO~n3GS#h5rw9TM1mG1y3{@+?5l|N^BRhoArvq8fUorde=z`@$(I8D z@?{avzP<^z!wDO_ft}j9TQv6~rqc4<0p~juR{bneX`JLoWeQSMJMzRQY(ni1$H@4?YKk@AdZ;37o*< z0i#bSg2?kFj_LtE)nKxTm<=~8?v8o8g zPK?Np6Blf#Xa0#a&R+}rN}@X*Dp$WwKv`v>6!B)%DlvJ+6*k!_&juhSB;Zprd*-z2N)+UNKnAK|)aXy#IHi6W3p|Yw|Z%>cn9dgKBETRhSAX2WQ3Ckf$ zH#RX7hXc*$XQ&2>l)T8z(2dTP%aMVcr)1}}igYZAy(|VrV<~yDTJYTfJ5#b-II)3X zOvxUhj-AM=o*C=)-9S!sS$;|um_g<0W$Ii?$o|S^RN?`epY9_D23QO-?Ik2&umWLU zCJ5H`gRzp6E%pNfRa34q#R018HdR_IysH;nS!9a$)C$F6jvQ)=_vtYzhnqMJ#0M+} zT+qA!N?$<+#Keaxpu|U+B4Y^XQXn-}jyA*8R88}>3V^@(@$fO64Nu^x^k-dv`V-(5 z4}toW>5xpxF>3O!ph{0BwG$FD+%fd)sFmiSBTFLYoB{Zy0&$HXC5pSeF^FwzBhp2J~cqszSxY9f_{we zTge{KnXUl1pVUTZ>f*Q5v>(BfT&C2btGzN(9Ax5Dy1l35a+OS2J2gKE-m zKA|lsbBwwEVY8C0CT)~5s{&=dr2NiMrWCI;Ou0tU9%teNHHYi2h}GYr zTCzt~kW-~lcdW-uyCdYtCulnECLt9!fB7W&s|#ufXZF+7vp*fEane6M0>rSlK^9`} zSreBpY%azR`iUVWek44ntknH#7O-7Qe6ab+@DP8|)U9?xZgc}5Z!$sipmHJvP*887 zjS!&q-fG&D@FZV1;UbEt?Mjkv|NRoM!-R9F1kf1`;^3(CJE2+=KBJwx0@DsLm#F*z zL4UlG)iD187?p1YM2D$q#nQv!(%Zq(F(pLbCusYckY;y4^Chc}*5AG-SUSl#6cs*L z|0RB}*SJT!+xIEbz`t-0T&&xF%7IWNdeLxzy!I|_NGHh?OhpSy-FvivEuk9{@-u}! zAU$Y`ubBAS>(#Tldss;fqveQc-$c=JRBdm{`sxy?V)2*~=D)T=+{oi9=b;APNVTP1 zClo;v7^A}m!O6h$@)VJ#UpsvpsA8Qp#i>A-AL{G!qiL(F=ehIg@^gl$sQe`ml#tv) zPSD9{TH+h!3qYPxYGl*`GU_B%`H^ke?J3S0S~lh>mC~3vQ}5oAXW4rRjOLct?2^uI3))2y)sz)zZX+GlRkS2I;o_xQ$I^__SQ_`S zjLKFP(hl)}_apQoOJ!_3;tc9OVWxE;sPEWFzlpZ7cpq)kr5~!irbv;RGu_5ha9Z zC#Q#HYDfh?APNSypo z!XZBswh|w=`|EQEa9d%Cvus}}n;LjDAy){6KFj zFf08QGUy)nN!M!O{tvX zJ==d?!yJFEct+XOvh}gcZ_MNCydzh)@K%)M^PS$tk`cVgDh!1r*kYGQ%78xAPgB3>X_WcDyrT L0y;hl1^oR7x;EX? delta 6521 zcmaJ_378bc6|SD`>K=AMb~)vAkV`;>MQ%j)09i=DwkNYJmyuNmFx-L=G3gz^C?f1G zt?^jz&w~hpuz;ALqKgp)1W!P@)EE)b(x_k5sGI*)Rrky$eD1e1Q(g7y9sfJ(Rll~w z*t)|QKC*07{zHthdi-_4uY-?)Pv30DO#M6>zx21lR=?-!@rLT*-Et4&;kVP~&5O@i zwD6j_i|16A_pYv*(WPl(<;N>WMW=qc3XE6Z!18z1u`zSzEn3J{Z|z2GR`2ZYUubmd zwG`u3iN@}Xn=o)r6@YyC?%2vRQ|qc$p2^=ewJurpX)ndYpJ&X@-Yz%4fN%eVQD`*w zJ4N~%d46f6r_st!MJ_hF_}e1gjNbmy$UR1te?!(i#sELpoM{aAZ#0ivhp};nyFm8fbX|PJze}_@*wkKv~ zdN<}TfO5nmR&5xP$z)o?0(5L`h$#}`rvdtAn%UosUBV2h(J~_{rV?jD>Pi~G5{L$~ zQjt`ez(~w@H=r|{I3dD}K_q=TNq{x248?H(lZZ1HoS2wFh7!-2rqdh~W!UyAfiqkC z2zyDyLJAy0@dp1yn|8^?1S(K3c5{f!5|eLXuQLPdONk*#G$1j>j z(9l}ajjZ#+sjVb3l@Pc1SG8?p-0I)hwu85dOonNM_G)rurO681E+l-78GkW~0TY#> z8$?wQjt+TN(HFTCpkl3wy~N67;?7X$HXboUH8Tmx%<+Pybd&7-e zHRvgEe-J(i@tA}wi-{*P=qh9ouOsLnD@JHO>Cb7`(OYjiONi-4=_&#)4_i0sr@(!P zkEgXSbF(N~wvZuUR^XmPyK4z~80ECY6RkwBB2!=+i6nD($gm!YxCM;Q2I_WAh&?)% zA@(&w8m=vH2~@Ecae*iCy5F~b%gYYxNRu(EB5eYgNu=H|D>t z9q>KFD%>ZCThCUS@N1o_xt#U(BUi#Ue| zg|Df9dB=8Mv-GYOnQ~MDwd5)niD?^7L5Hwu%gIj!3B*f7yK-GJy7M4GFW@>5d(c38 zh<5jsQgo6T9uAvcJS-wtvuj9M{3UBR6BqGHPL3;d6Qp||{V30>O{^lh|7tdeMWoqL z(KlFqRv|^JGsD$rSf+ycRJC^IC2!7esJ1&i9WL^Kfu!2gfLBrM2M>= z57$x+&H}3I_=}2(DlEtelek@DF)ukU#S4EQh-;7fQz+(0O9la6-}`nPPm)PLq4v2tj4oY z&m&lSK+7z2N0Ai|YEg4_4n%h3ApVNo9<=xnElTer1?Go&^Kc+ZJi?teMEE#IE1A^t z$}IKO5!7h8|F?N2E zdh-u-9zx>}CCM&Xb)aTEmr}ssFS3}@W1jYEQm@3&SAC+z6)GH0>Bn>d^)f1GLVP3Z zIMn%84$wLLJL5IG6YJI3Jb#j7PFpR%U4q3z^vNs+|L6v2}a){C`k7?n6p?TXv5OJj%C{iLS zCv=_0Av6eiX&5QdNX|UB%qT7;gw7f>??+2CA$&d z0|;cP>1^>=soXnMfU)peu+Y`wd*uSfVYcXQ@qP3d6+JDS5qv-MVgL`l`*3~v7#!oT z%YfnsGDXG^(B(j?RODM>YRXoyp9J8b^7b3>HavkdHFNe&z&0qv=`E%KOo_o3{LlE? z*qokCh=Kr|q7dtz#I%?m5ez~3k|)7PJsCv5>@G)#(vVG1Tlu2UZB1-SG8S_oKXBBq zD6BHFirN;HWAZ{H#>ssu|0;l9p=t)lXG~)jNjHH4QgNjtxQw8VQ0eHrc^H2Oq4q9y z4RWT7f2ez|H_bX;&JU3r`s+9k;l3`wVwN1Bv7ckbe+@;Mv40I(4%IA4B<7L+h)i7t z=UdMGcoGYxessIvBxC(B<44q;9t_qp^z2jvQ(}?KDy+W8`1=fN)y$MRHzW^I7fUWj z!Q~m2YchU}71GafR#=h}OXQ+zP}O2VO8j0G;qjq~h`~ZXw5YLoqR!^EmbMw*_i^q|T85G3?FBLK||s|3Hsc-fC%-?r5_B zWfg4$?fv1AUTx{FJ0b23JbIr6k}Byku%oW#0os@L;DeSkl|1-}1&2^1Jt_t1i-5}f z$1Lt0lNnBDGpK_jQZ+_TSa1<-eKN4*h$K?sfChREyOXI)OH`~6Xx^jv#L^w%(uQE^ zgxs!g7PS4(Y)r8KsetGttBf|>zcE<)NbXly{g3#kE&pVjmVRz8?roOjflJ{Lcs0Bu zWavdN84i%6w$g@lUfic&d_=`IT7a6ND-z-b=>XvRlEweQ_{UON#7fsuZqXUQtZ$mH7{~@LsAd?b;^^k}}GOV1Ez|;vkWx zd&GB?YG{fMS^S@8)va6{X{g$JmLo5!$IhnO@eIjPaUzhL5FZC1oVNHU8UeI_3W3=u z_To%KF`rvbdkV%cX_}JoE4hA(s#XsTx2(NilS!Z~P+iqG7XOs$PK=*!aQXSxa`q86 z-&ubBgk@rEt$jh z#2M82VKN#vsL~onZ2p;S17dVrlke=*{VdB??VDwX$(v)#hWAPIX9oJ~TwC?q!E;L+ zY`KnZ3`eOKORX}elkwfCceJ~`9bA?>cy{n!wqr4Oo*;F-_4e< zNQjT<^Fq^Gf7TIPBrw`p~pCF-22}sBN02f3!2n?>-h5$8Yw2 z-@WIYd!E00&z?CfJ~=FEw(P8}JU~RB;wi#6gds4pJVZ%$eG9(oi7qzZEIa?%!5aU; zXxYIZcT^WQJig+>BBJ4okG>uC3^%xU%?v@~f#F)r9>i?_QNP+;d2)ij*tNSaO8bv- znJ6e^5Fc+?8g3uOBy{O>5dGGqZ+Lds#DU@2%9Fb$S`To)c)*GJ`I-od~nR9TK=x<-9_i2wLOKqq+g`)6S4^Jmt>Vl ziCvXT9q|VLZ}BzpC|`D;;pybn!z1{8L8GU@Q0b3 z#1USXb=rFj>t9f+ps8h4!^zoO#4UVxb{V*Pvr9MslHMfYP^v>Cq=Yhr5+mVK7JW+` zYT7ACypH54-AdPsO861sj<1jNrR*Wid5ODnGH+Ef$0-%wgwmj6PK-tb6$=`QJf*5n z4TvHwX`apFIalR4_MV9SeC0+Vf2F2;WmSP8a(eFVmKMqN!}*7ER}WZIP&(&)8?}>w zO`_H@Nh4Aj2!`viHgl668f=|vqbNC!l5|_vua!x|>Plcivd8LF%03=jL?er6Te$7( z*bC-EL^@k4S&=f0i@%_G)_&&(PzW#-R z-hw*K*ile`Zx26QaO;Lw;QtFW=v+e8R=18NlTO)~M_2Au9o$GVMJl}rSIFOz&p5P) zX9|i2G#0N*wl2H|ahjkJ7dgPzr-5*V&DU#MrF`9h*<=q;(a=6+c$0wEunHYN*FwZD z5!9rBWy(FEYQMrHN&ZgdX)igtR<}U{5QS(UjLStK9~6aEV_(5nt!(uKMSiqI1(@K5 z+WrRlJ+l9px9?3GK74 zPbIp(_{-HGdy?gw@Lda^R@+0lhWh(B^({?RaP-Kx)!Yqi$v=?KTm^U7Grqwmy?@kI zlYi1?_-tXpz@JT~&mhw&(4bObNoVKdd&l}5)Diy$TjnXc%JHCr;WbcVfYE1<5E9nwJJl%Poshy60A{I7z{7BKA0TOF@`WpKpq0KD}8Y(T?U|oh&&jM2cl{G&YC>= zSCsD@|9{P@^1rFualHixTx){n9jXdQ8Qw06v;p8Grbg)r68x4wgTGefw)lpue3#^V z<`Ra;56A`MD81#JC;2{s{v^3H`ZhwL^6u|Q*$GN@k({QW`dpykR-H_J`&mvvQ_P7r zLWVL<<$AlZe$gCESUGf^^$%1C$~vd{GgtqEc@1@X+7 zqp9NIA%Edod)p@Iut5E(Y8-gni8s2VEPbC(`*R20SJ_ixnqUSESf@2CGq6vw>^-H= zAfm^HxnwH#-2E|1W8^Z{=5h*WVe9a9^ov?A8p$Semygm9uao$IM%*{rIiq))DA6Ba zlAkLs%84h;uX8rnXkeLVi&y$b=_lmCSL6YnC8i~Q(S1nrGOxQibGf=~Bm_gM9Vy{u zRLwhYzSE;myPUOhxDKqGIyF|#p4KKbeo!J=uqecW%p+Y_bYz$;1TyMu6@FwC8YS|o z@}R_ekDIdr?f8Rw zbNo_>S1fW0$wg8cOc(*!bRv)pY{kp;M$$XCF0_mO6bS%Z0{=<=O;b(O5|1Wc!AY_W zx(sdFc8-q|yEbU@KkA>9u9u1Sli15^C!FQWsIH^4pOa*b={rKEj_)lZTuMqEXJ>D01AKnf>i+3HLtFMszGKs#!soU4CO4>veOGfGN PY}v6heC#eFyQI$mC61!$ delta 3123 zcma)8dvH|M8UN1Bo@BEmn4m0#=5a%k4Ix0-02ahdcL^v0I_q76=!gypEtIUN|C}*^xDj6%gTc2E#RpOltgk`^wA0#Yr_-jt@0@!#3&S71v-j*h z-*>+E?|kR{<56+sQBk*W<)ZN8L^Oh@9RD%A0`JTKkH@Cxr)x=$Z|!ZChw-e~8s= z-CwDuiPGx3@%>Okj?;V(K4PaLSpKVncGsCzLp@z*!h@@Z5*ja>?Y?4w>k#j@}zMn?MLKWMgvuts{b zIp1;@JuG*VwFZn%Z@x!;GGT$}Qw0+bf)+TJLPcs7^EzYs=aI0gQc!37@eH=sz^-MCN~k9%os;{Z_qk;6kLC&aJb$XA zg{6k5MhfSbl~f7*Mwr zZ^~$*&FX0JT>D}cW0ycLJc8==DqK>cv)!*6O4jD2FIR7t%!8iWmCufqHql1cL+kl5 zMD>nsbm;J)Xp?HQ?>b-nU-ng%V0dwQkc!P@Nko`MOBNe1T2!L+$8%pXd9zNTNRtx) zpVr+Q(3u<5(RXK4Je-Rx33-5QBiyP!F0E+4SkiT!HNx`+b^Azr5OHc?pV9KFRw;)J zs!e)<42|)FhBp8tb+Nnn`Ob&z7(tDUBb`a1em*&;{WZ>L8(A9CSAYW~VoAiwgbF|f zlu^5kQl7bG6%^lzyb6(Kp2c4$TMm$|sU|@XO3ik&!cGIOXwWHfBjbg;-3qs=NoB?D zdM}!F5eIhIFSrxq&IYo4Bi;3^>t}-;$d+%ycfI&6-pa8eLw(z$zN4w!c?0rY{`Lc% za+qw>3+mj>J=d1G@9ApEL)wfwSXSEpzRB|;P(}%(QHTmEVs_%0CNb(qgzMXs;fAgS<9X^hv zJx6&ylAlqO|4vYJu}-IMb1o;MDQ5SZMTWAEW@vF^{iNBFu%a%o{y96(*42M1CQSYl z$uAi2d*|CO*>#HBJY%F!T^_0$i+H{?o08*EEupe%x2QjdBm zRMdWq^TuJCU^)z3KhwYrLXKH(Pt&^)(Q{=?I7c-*)j77&Z^>t@rE&^qVe2;+&`)YX zoEQsCH{V9TyI{rV)a}2Rh_ z#sD9L>B;gM-&Z8R){YQmP=xQtPFdA$ZSsLcspu3MeXbg}D zP@Sy@_oO;UX6n)8cYI|tzUZudpZsO@kd<6MV|j);9KKFXt-V$~PEzGL#kU{RZ*mnGxT2LobI9a~x>hSiv5HR8CMkMVD6 vJ;r~kUW`HY4#u0*zn9g>o(V+DRn4{4g}W#0OU9pxi;BuaNY z+&Fnu_^wO7&WqVekutPJ3A_2^+Q8z!zwZuqA(h54jnV?OkgPaC! z3@vnaXp?BE^OiP}PI1m?yXhm&)~wyM+$qj(q?OM0>?8V7kh-dm)67QFKF%&S0=^Hj ziL_7mIhLo<51fB-Nw+ws#b?g(oCOon@tf--ZS&%-tIb5XB^pV@$a(_#Oq~s(U7W~k)S?%WA+^XO#u3C<>DI}7WRcan z)zVw7YIN&~UbqTNL$|JlXaf3r-TF+xf&wNImbJRI6pbNriEecw0;uhn4hFD7dN(Vg z8*5lG?dc$I5UQ-=p(+y(Og89jDAkH#$%fHwLax+}I8ex2eGMjxleg(c9fF(FCbR^l zn=_E;cHJt{U_on;$tJQ@H%gFejm}DF7r5K5n1+K%Bd7)*lccd1IoIlgfo(8_MV0U?y`EyvCr4*j zpikfL(oWsD1GLEd^`4bvlkMBy#faRkrx>~zGRu+TLDZ2VL|G+dmlrZ${7SO!J0iA6 zH-;ng!zg}3T6#8-k9h9sL_VtP-~^matu3F?t+UuIh-rxCNPO z3H1#6xhD{0zkrqla;ulQ4H(3I2!!Mqb5U{t)ty9B2WgIi{3Ej4W8UI118&ZFJeU1? zN)R|11N)CWptG^T@P4=dFMGnarOkZBGZO-QFnz3$*-;Rhw8Qc(J*B&uJG2X8^EPh5 zgxMw*w0tKMwf8YTonA-**&us#h@l33??Q$76jC2f$Ltc^{uZ}D0zThCG%su)DGNKu z4wN(&$QoRV%_PgC_*jWWbcuEW2&#HfQ1*$lx}ebhREN@1whz-Fc%%ev{`}jP8S}xW zusn*Hj_G;~BdH3W$m6;J@`yZ`M!$ME+GzL0iGhSN!MEAHN?U zzx;>N4*e?R5muQhDZbHJX&@;elEL=zPu&=XivOcS&SEq8Z#n@~U0=x5HRBiqXmCX8 z96SRDT0INFfb)hrPr(O)da&(400la0WnbU9tt;W8pfp)4OBKa*ih#J}C5RKi}09J3L3(cOkP zb>P)OlIEXnGmP@(1PnPjij$fe5xnUv6EgWw9 z9s(w-KoY3hh^*&W{|E@Vv%v~(Rf`}LdcIvH zymGUZ!(7!5kZqhzqWv`DR`D9nCM$3|XHyU?*KszLg8oz<0lTtEmhxK_=z8=Al!Z(i z`-j}XjqUhrBOHc7vX;vZwSA3?+Wfa*$?&ii36tM>-va2XGll9(k)JubI_A`V7usz)a;ktat z17_;>5sVOE28w-*3NhNU>4dER0l!^id_-QvD z{mDfUe#UJ@&$@wr4ugl2)$@MD!}5TiRlnrceOO~S>e}b!E2>7XRy_nF=(mlEs-!%q zP}e9F!hwlYB;~IZIOKvwdg=)Q$Eyle;i7U#Qog1Dvwhm6d_7&~*uQQ02JQo=2B%wZ z6afk@LAMng;%pA4wt_Ynl~ga149nx3EegJFz}Yp( zFBO>qWTP?p;oek7G`0$)z&V{z=<9=MKoB4Ol|q|AG_sGiJ@%6dk8eApDYH=dcCWHt zaOHoUmJhE(0AjAHjWsBMpHv5(BE0t}e>oanrK+mUSs0?zDA8u3eJmEr$Akab_{AgH<*uhTKp>^N~b53B!8{ zbd?6#PXL{UPM5qmlidKhx0HI2JTe>vz+p~=4(7}nRbUSm7_1xJfV#8PbAjKVF`ecYgv%1NN986^$@v=g$9NxuAxGWyT~;ZK(a(=!)jfs;V2vC zNEALIEj%p8DvR*E;fPHXBuhOs+}UG2-Chc~_;H>Vz(c5s94}O|`T?4%Bzn2wMthoI z%W)#M4ZLvB8DF6q5oQQ>6QYe#-%_WrGX=X@VO7^C7wi_?sk69Nz@!ifzUY8K?xe6l z9X&W}2&)I(qCGo_y(=?8Hb_$d9E=IM@9~x}ro&FwBAC@vwt})Ka@|j|)g?LuW4FY| z#@rV&z&}8;fsYWC4s|}hfkN&Dxbgb5<i} zX=-ror9ut#>E(*d4JW1@S_0fRDX@EN2*wi%C2e`Lg1d)>+xtyWav9gkAx zoxR%&ykkYp5%d%seB2*w!*Zn`^jifWeyXAialsDPgw!_3_M*L77+-@;vPD3UF|fA_ z?|%y%dD@gcex0Z^NC*fP5URQl1i@5k(JmlrC}v&2!Hw?A=p7SJ}QRw3#hI%%JxwW6o;@N*G5ETGVC$;1c;DHL;z_ZJlk#I{Ws-i++D&@ z)Azl?|MMz^0mRv$Q*eAC*#STBAIW`gh2iVHqbOh?*eTMl2)k9-A(&kk%ikl6*%;%8 z1-q59+i1#G6J%C!f_zlDhSRzl;~q@LWL{%aY*95QZC~5AD%RBG z%$&5KZ*Q_-Ofm_B8fJS#b}DGt89-oCvseB)Y&_;RO%;AFD*3T zk6Djoc4MN=Bbl=*(%c+tYIBxMe>LBeXeGd{)nJx0V8(Dehb)XIVr`3BV$H%`Rz#zD z?#hZK7RQ@oz1-dPYhr6+(OeJJ5?K-R*0u4*^mjHjI9ZagRz+I6H(Zr7gzj~=E-9l2odfXwo^t}e|LhE`E2Afz zdGMX>w8MAYd8)2Ve-cKBQP&1AOMz)|q?j2!TvjpZ@`S CIIkHj;sT!_+WKL@|`*=C2oy`uHrU3J5%p26nArVSf1 zt2b}DX#HjDb~TUQ)v|g>m+bbpx6gK#zqbQ|cU~%LAAMZRUAJNLCb9GWVI*efgTsSS za^T1-vEGrb@K*m23-?$G;cqA7JK3@EyD+ z)3PEMXib#WL8CQ7_6Ty;1UV$wZw-^9gCo`+*%Dk@vPVt|s!G?$y5Ne^qxxkquGYZ? zYM9i)HEJln?^ffbZabun zH3^eGQSNm_eL{iY*)XZaDy*MNte=n9nO7=txNIhM|E1uqRmOeE~;f|_HYrVjz+;c*(mRg}aR0hr60sXDJi-3GS8HPdMesu>=YuBj_J zEk*vhK5{9LtIP}y7h33RC)3_tL_M?Mv+lH=MO%=x&sr&dD6GhDb6CZ#Vn-YuqF5<@ zBuXNJdKOFA!PZI0sqZ9lSVOS1Z-0NDu9Ctc)dyK*)nL+j;WssN=R>Eg$!at$O4r5Y zNiJ<+mS`99ggH;gEor&Vmk95C()a?MnMhqjQkX6(MAH&xh0deCab()-OO#EtXN5tz zCN|EcUS?KO^2ON+Pc^&K2)wE&e5=@75H5vTt@CIgl=;lRhU|&^0r8u)d^)a+&HcLg z>$K{c$OO1A*)!H_cMzSnQ6o{Q16)!FIv4XMUFgbJu;J)m!dyuQK?!)|7vC^AP3r!#&_Hfu#%{PGy3>WVGqy7`8lI6d*Jy5M ziXNcoDN$xQ_Xiz(bT-Mmqob69w_+*g&V&#g^Gxv2fT8|fMMHiaCwA7{r}MTz4WvEX z48pY07WKgCNIB1$ykCb+s@S7AWqT6G?+6#(V^RMi97q4*B+3taTjog=4}AH^e~E04 zP+9YY-c2KzIY3qZM%Ek(Zv)auN$qO1cJCdWC@&F&yZIgM4I{*Pv;T-U? z&dl}82yb2?A!HzBE;S0u#I(ofy;|(~Z^V7!OfFp(Z zR99KzFJzwiOz)mepwHtj0iL(a7sN_)*O!ru36YHE)(TwNns4r}q7?Oo`C7X?97$XC zlVGh;{biy%WQ}7}PlVRxHqgm7$6dCuA+m7``O%#`rGX20pvUyuWIZ< z6CKG8MVgGZ{mcziXEfTLFk@}FQ4K+@SCR+gPMEIGnQ@F7$|!<^N2GJ6mche9u)`dL zNr7WLqlSm5GLbVA7;suln=_N_yzPgz&JFzx>?!Bss3E$|?f{6xO&E5A$+kM364|BN zF;i_dg44Q=C69#U?h+BonX?!$Dh8<0vIb>YlQSthHY()}A{l;~Nyjj$8Y?p^VYmSK zIFP&NVoeNWZp!O{Z!zi4$_(1>+FuNZ_p)8SVXzeZ06d*0DYNVRT5_gUsadeYO1U*jgs>U zTTP>#xr&>~bopmhCu`6V?wwYn&cdox)F3yev(RgZ#yWubBuio7t?{&* zHAiBnzh(o`!rp~n$k~4*Y9F#AZ@*!???YAdwhbqfW8bke)gTc(JRDP z2z;KS&XwWtdp=QERn%QoRw}3Mu~AQ0ZF|O^TN3n~urOFZVL|Zpgv;9&P267+)Jz^7 zESo$jXr0_AI52rs&|}I2!I3FLf)!J{x8X{8fY-byVIA@W!U|dkUY(U?G3x(KC Rs^K%Maf#Q2jt{T%=RXjOWA^|6 diff --git a/src/test/samples/golden/recorder-release.app b/src/test/samples/golden/recorder-release.app index 0dfd39352d593b1b64cf3ab88e16249d16b2d029..0bb08857ef779dced0245cfae451b1602cd13471 100644 GIT binary patch delta 4093 zcma)9eQZrEp+}gWDd-iic-~v_7et*b6>;7OrQ&to zfbT1O3zRPYgO|AA_u@a^2XIE*^Svu>7Y%Kpz@LvsQhkZ!aVtGK6N{v6_5u>u;M0#! z1fOYqzK_pUe15&yYn)wKJ41^2+lWtS5rqxzhn!b^is>UhSU1#C4X^SVI6c5kxRqtU zWFXbNfqrW(DjIwPxJ(@IT5+tRg-dC%P~l%y2RQ^mVP2024dQA=eWnp^tzq5FH_#u5KE;}UbH=2uscpi`J|lFJB$0Vf6#*lh z`vv0W)h-|A$LS>Cyyy z(J`Il%$Va;qyg&*?e838E;7~OdnC>Y_Qg+%vs#0GNen*Tl*uwj-)GEu2svo2CkdR~ znqa3GNN}GaKRJ)IkBqZ=1;=xSB)u2q8<#B$%6x^n@Wtj%vTmYqNN+ zy4Bc>+pwMXun-H?<-7x@o#Kbp{w2m;Vi=M^a_@@|t9OE5wAIvD%SuG1tem{Z;#sz* ziG#@)`xqk{eixIA$;_9V_-4&ks1@(j3RYy1$fu$MF456}zjHBT{+{Q~RZ4YLEf#P3YA;R&Gi;@hEM&8TD~ z8nU9eAd{kW!{eD9BpF4V9*rZl3OdqK85Lj|p&R4}_BQ?`Al3`I;T5}Bl&DBLnXfNg zUqxElu-Fm#P6(E{GK}wnV82C2nZf^;GrXrTAR`ozP9WAU2vD=5P^i7@kh*k8(Od~L zR^)ul@ZxT!G*I7`T`)s!P%lfEtwcP)1%4C=o)ivxA# z89f<<+0d#-G>2dbH)zonLgK0o_?E#Sd=8TdC`IPfe#4QP4omBE#cURHzrXOz#oUie zjS!0Ll*z{)BRk}#e$>~wK7iaj6UnTQX%SPH^%WU92b+P1$-*nT7Jj@O1&@6sa0PgJ zfNXFA&~%A~H-r9t#L(xeGi;U&tIG+Ve6FGMRU>9_=2bHVsV54?i@`fcXw8z%#vvdy zm0@TdVK2Xyph%E-SeIk6a`D(JbF1DT(3LU*t351KL%rVMqco9QbqsFugJ9z3`^ip| z9}=St&Fen{CVv)?B zswq2L8CLlS?$&wsy6`mBWum~30gn;k>&zn?<__w)h{lm;tD6LJ-(Vhef-&r5;|_Nm z5Vk;5_7iedrib<(pMZc`X_+TUnsi80@G6zUlEUc$8Y@Yxt$e|t5G@4R*J!G4h)8=t z6IJ~X5ibHCX`(7V!qvHzHkAJ9fmT`Ax_-$b#*G1e$GNEFdms`ohy%^$a7;EBeQl-F zs=$4{q!Y3qQHd6WW2vQ@%=ag0RQ#g3+?H!VC&7944d&6;p8Nt}JjlpB${2^al8}YAk1N~^r`ezE-ywl}zKZI|ShoBGACvj}>&jJ6uv_t=`+_E~T zqU8Gf#kH0W=o25d^oFP5R+QPxlScZr1-BMD=P9V;psw`tC?z6~!yhP9#C74rcmjCs zVs_&V*V;9#Z))1!Ka(1qN~QY}(=j`lS{!N{ac}&T!x>0KHDe@^j!c&o&`0A*I}#Ib zcMKao7CU(=HEB;ziw7M;t5&k16RD|VgNaCNZzLTNyE;#m7>d>#kJ+)sOPxP+R~amv zI1!IYN@;#<*^yKtE~dKMRu(Jb-sIFtJ1H)7cNc4wa5|C}H@mCQkc7w^y3^fSZQ;MP zB1M0CDxK0IdnP0CxILW`#oMlyX=~v$25=nhi{Eam&lIuYMA}a6pRwaUH4MgLC2AD5 z(<6zvU94sYPS_{xn4wuSkz=+Vk0qw^<2}=MB&q3prV}Ytw(MhxWDK28rVTy8Xwe2S ze94%?UHtd9?cnpWgX!>OWM;8r`^B~JBk`xfcDVAjp?0__I`I8haTMQo#5}$|;u^ja x;-5q9tG|c)BDon?8Gn8It)UJP^j4Z~oUQ9)?9Wt#_}e>n$okh1V?}Dp{tIFz*{A>j delta 3650 zcma)9Yiv~45uUm0<6YLqHrU2%uOEwdZR1z{0)t~5b^{K^V0(KvcG60;Wlb$m3o$Ox zM@Sdfq>4kRO{Y;5nWpWpHiATnT%%TvS|s?D#tm&F(JBr3QB_KmNJ-W7NAoA0Ip^HX z8d9Yz-FwfSIrE))d}q$xt3SbaFX6zRhX%WT3IHGTFUHR#U&vQy5FpRK@8zfb&iTzV zWXD%74m6hBCxH<>F0@P;y_>f!1hC$^=C&B|2ps`T1(zEH&P)+eVTw)NOtb&YCK+lC@ znL=FM2wTCCdO^Abpah}Bfz*RV{EkzM|0*brKkSNILA4$+2m=v)WA%!uHAL*1_;?_g zJS{U>`6ojN5S%~fj9>T04XMO>03W8cX9!DdCl1;WKV0(Zs83W@XT;|s%%2UWT;>{<(Y zns!t*MdsiW`-MC~PPmmagI`uBVTLkQoBAi=tgYYFQc!zvJFQCA6ou!`f}?BTMzy^X z`wZQzsu|qy*=})EJpFrsHWvMma3#@(3Ekm#-yA$a>2OE=B%A@qP8HKRIPc|%>A^_f zrRDHw1y8r)0)VOH7kQhF3qeEG9ll98OvBan<&$vKgG#b3Q-Tvmv(;6+*cEzKKUe5C z)%9H)bHd=6YsgeBmh8O84NU|ps;LT3!ZDjX_Tj^1sZI`x%P-RInq8%H@S5kkB$wr@ z^i)l>t~6&g7T!r=x>ZxhG0f0^YMLA7;DtLfhdOg>z1dsa7As?dmdlR3;4PXu&*Vm& z)tTPJ>I!svz!jy9bvmuQ(RYj&_`IDZqLDCk68 zX}+9*a!oFx|c8&&P6)0hgKqyh8mQjs#?q`tguC+u`12PI7}Z!sxoylGtD?V zEsnOz={Wym>)jAPxOb@tGS(^E&M<^zOG1(MZA0pz4>nhPfjc^P;cOGLHAn$d1yFdb*%H&;jtv*M z2g%+~3>g|VXoW~w2ni!>6d`@3zAV#Z)a$?~wwTudzb^QJ*Gjl@0?SKIjth|a6lVEC z0Ng^+t7fTpQV#8)YSFcqZqdqc<;dAH=xyGsYR8BQU}Tp5RbN*6IW1NlnEIJMMIx$` zs-odOEAPVDWE%r>aKq-r5@@UJW@WLgnj`C>O= zYg_L5$f=#usJ%R;NNC1AMJKU!h|O&#w~LbAkA~C^Jt~ZaGIAIdq3RQ~bC#Abt~KG~ zSRB^Yd@>852C#CCCGvfuu!oyBEYb>hs~5HSr{tish@<@GH*bQIeK9b84%9>9)m+^c zJOM)sqmmW6j^=ZL{uwLp=be|wy!4s&9JfWkN2Ips{bmO5sGazV8o&tek(c@->Rw7T zY`J$AMpPVGM$K zvL>$UPNT`r7Re^ImN<0GbYEkWu7k`o4Bag)EV;|h@-U2UkJiaynMKIj4QicX_Wd%} zzHuYnn|tbJ8>zQHu{a=bt3QAR`k*nI8A7!W)vyRILY{7UebP)BO|fpjIc-_{L&#$% z0Js-TdD;WWusN3Lr9#7aogUd>Y6Rn^<~mPWH0p`Q@W~8kOky(zXNP5MW-`mN)Zs}| zBU#^~83u_^sBeXaNh0J^{9`LLG!$H$TfK&{KReu}DQ=l8D{|Z%(2u>AYPvh;gN2JU zuqBci)?LJYSju^mxKy6S9lC>v&#Jdn{-+@B1WEyylM}&wj&r ziv5xb);x8m<^V1f-er^_Z?csQj z_^FbpKCc(z&t33oz6i&(-#)t1+=*@UX7gam#Z~!lVCWMtX%zcF&NmB_R@?j4=De5UW&GQ39N(GGl#_VfKVJ;L{Y(`CMYL~rmtLXo+B^jJ&vt9t<6l0@)t&%{ISzsp-}omeucJBN`=DmULPM7v8B`&%10=Fb$LaU z3WU}L6M`a%8t_L#AVi}WwD<=MCI%Cc7(j(UxSHTECgRN9-Pe5(H@UkzbLY;Tb7t=B z{@w6rL-CY(<#}xYaGS2d^fYK7jr2qSpCgZ^CtuB7>}R_3{#tu+c6)P9`zwuwLn7PL zt|S1o{pF2UzW*0CTKQ zL^c*#H^g2%U_I;Gi^W#Z--zYb^ZttoKa$smBx||M#w2Tx98CB3Wj-byy(zm3JZ=5c zMd4QKdf>cuxocHE17BEMvwG@^Rm(!nk(G5d&5L0ZDN|`oqftYniN--1Cun?nv^=q> zhgnCjeD$JX>?h__0m{L!k6w!0m(eKLWFLYWgEAYo1k_ml+zM(u(#~=S?2to2-VZGp zP!qIj7^#MX%z>zygrvDZUQ0tsuWOu_2G?VjM(A}s3mupi%#?OnE6H5d>#>v z8>m9nGQG~u&Mr#bXb2L>BSHB91fyyNhO?MVh^RF&X#Gi6YaM94!A2iY>pII~gIg*a zV^P^uZPIc!>@c|)#!xXlsWoG%I>telsCr6^<6ZG-ElzMnQ;Wr}xLJ!6T`{V~Nv_zU zMW^mcAR1BIVh~bI#?X2fhV4ikP$GIc39IKK-+|#YoxX^&v`;SYQrJwoa?0o}wc8U1 zkr!2aJdtR{i&|j^&Webs_Ay6XGcTbm134MaaJ#bePhM-)%Se`FQxXbru=~-_r{w_N z1K`)E1t~4BV>qafqdLg;wR}D`EBBAIk9~?Lg8v zU0DZBz$xWI*O~~YY^L#oVAO5~=e#qOTu|rZ0e^=EH(vF<3-|)c2OYrPKq*b8`oSA1 zXQ>nLMKrE+P?u0n2RQ>|755bK&=oXc&<&}NT(;X6l-V$^pmCfRU3JtYA#w`p8tN{Z zBETS?L0w1P9ZgcGn>tP(D1~w+$XRhE=p#0N#blo+p}(O~%|33qObVcRDw6)-_i*`p zjLSc~j2T`o&A+@;n#_OW$-Iq5Ig{z*`1KcsjvPU#SAIdrNa+BV!pLO9fFKG}s64s~ z?v(l*yA72Ht5w}Q^?gmJCw3F$u!l$%_6Majl)(U#^qx))%%%g~QyA~@M5^9{80#gt zm~_+5a1SjrmZlB-p!x_I0vTqTpsc2*Np7T2S zjN`dL8&G`}3ON_-vLb%%vn~jCkw`X$r>;f|V-|ZJ>w*>x`d;^d4&=FUUO@9~TIUn3 zB6d6{cr6}c$Ym~Mo7>64IL)pq$k2>0scG?0D~0hYTh4HyR{alDL&%5iSRR3({v{!X zw^32msM)%x7_`&CL&BKJP9q$ZY1hsGrwr{TG$)qWxn3*_yosIfoq3c|E)=nEbe#)W z=Qx()#ll$1J2&dJ`s-ynXK6`1@TI~iW6Kqtc-@~3-E*E06u0y1)sqcZiST;*S*HIb z`fCH#EW+RFf{v(-LN0_eLCA7{5F*OdbduR-F-blKmarYkOh<=xuew;-;pf`?GrX#J8=(`MLOkb&>90 zSxE)Cc*&Yb_g}2#beGnCx(~3<7UZT>_XT*NH-i9}QeGCRuu8l1ll?e0vb+nlV6(`p@_r@57Kk=b zDUM1AgrFED5q~rwM#WgIQDeYBB4PqUBpN}`8xtcMpXc0}+0K$U$(@;V?z!il?>pz* znR~H6Vil9;*AyQIfPd-Ap{GFuX^iO!;CJMy^yF(#s{QQeyq`K)ksG{7!qSH3=16SQ z#ue+ftUcIRaIig=lM!wGzO^P$e|sM}ZrcpSon277wt3S=Xgib3ZraX`u<~(8-VS=- z7flbxcG8P+tCGxje;jJPTi?~*dbhZ>PWLxt z38tLBF4F~`v+krSeA>DixRO0(-TF1*jgcqUuWVTDf`dz7o$N6$t2cR>Mx%koAsTPe zID5LdXZZtWBfav~hlX*CSXB8ahrtngDRf_oP_W4n1XYYO2X+TkiGJ<{Rfa^r91ioRU=6?3gl>rt8qx0Yvi>m+>89E@mh|Y@d1oSFh=sOlQ4)NIF*&QQ6Oo=nQKfKl=3V|3A4U7InWf@PR58WzJOXj;68iN zZJ%w;>Oa72(gt=a4p5%TAVgtxgk(8{;wpxbVUz=as9KxAHi%@kjztQt57=-5s=2!c z*1L7FA+f#9)kZDn*ol9H5^rwOnz5uA2dQzjS&QRcaf=oob;Ye(oZyPvv^dcfw`;M& z6?bUSDYC~P9#!!K1TJ^gLZX?BVGj}qgsQibuqLkPy%@>T>5HoU+9%iYB-ll|8p`Oe zs?8Gzkr!9bc_Pt@16pAR&Wfmdfho!a6V!_+D?wKAwp7D9)8w^Ly@X^*MwQ_JM|%hj z-5Q7SAv(x(YapfN7)C<6_tbH=Z{)UoS?^}E%s#3)m9J=}O>|0JozUWVQ?o_#t6H|1 zCh;}x?0zO*?`GnZyKhu==uY1gRcD#K?ug$;;|fRqt_>B`dng|VIhjq!a|-zUgkN_> zqdJGAa*kte40cm%BYz)_cJ|Y${ZK+a)P8Ebe%K8XX&-sr*vq@nn9a*S;poV--Bbg> z$2m12%pVE$DMoJEG=8RuaLQ&1F9^l$)8KRO%v4U|#bm&j&?q6N>Pr{!Wt7wI1I&SP zcE7R%W&Cs{1HOXB9a5^RC~HAZ2RVZe72>Cd{(@roZ%&j3@IGwIa8ENbY0Fy7&b zG`$BgmM*xM^alIZE@BZKk+D_2<^SoyFe5 zF4io^f6&kTI>qYn+gG@?n(d7ANEoyDS>l2g z40^ELp%5O*bERHDb8TAZ5v?N8p5Ga2{_VN|natQ&8lRggO6DM9t@ z{AXpb;VKc?nnEvN&@UROH6n6Z7j#r@5VD@rIK=%yh^nocPBPmj6h3ozXIU0Rq_ixsKbJHtNZHcmr?y&ZK^9A~y6&=w3Q|@|z|h*-RY^2i z7sN=B6f*ii`cNNC^uZUzm?mn1K~0QOjW3OfF$OhiBoa+uj7I&=y?d8U#Oh7v?3tM} zXTI~zHv`xDf9-GW=nIGDiRdqU)%cB|2{gBh$d>8v$1i`$JoDF`@n2bJ4J;(<7cPx8 z*TiRce(NBb`EL62q-!Sb9QZMfjPo;Lcu&Hcdo#d`Lo2J)b^6TXNt%00)r+d#3WKxp z!szIWa3VkKL+I4|`pm|_>io<`Xk}n^WL`~(Q(7{hHbt8#RUxfel&cY~PWaS<)+FlH z2U8O z+Z5C4C+Ai5P5$vlE_^UCbguK!$4*6)@iW6i$&>UH220S6q79*qqrHmuF4~97VOPry zJ;QMM+kqCj>?D%abkVXJ&s-EF12pAiGtC6c8hSz2lDwHFg_HYr=-C#_Ec9Q$pEhg$kpdfpD6h1Fnk|EMLNs-yF zBm0tUG=@0ZLeiwpx;?fM+NtK)`GhGmzV6B99k!Htg3n|aG@DzY%W6DTFE~1gx zAgmCRHlwPoSZyoTt=#3lQnTO-z`mb!U|G`TE$Uc#WvYZqNf@j-v)mbcX2H@V8QB9S zX9vI;(i}wZR?0K{+j9IvIeu%7e>lg#ealZ-M{$JGany&@ZP6Q16nv18#RlZ)?qC+U zZk%vzojrD=!iJ*Rv}PWh&9mhVU-Ht{cjW1ku|#95us#`#F9AvB3vBFiutKw*-d>;scS z{Zd)sK~R?&HIa#e$`N%ZgG^L5Ku;dE;w@{-X!|G&R8JdHWI{5-UCeXto_}oZdCc5X zZB+VqZEg1+2XvFXi{x=t<>_>RF&oLdN#3K*dP-AS^n7QL8rv3YRBYn|^OD^tHtNG% z>=8p@Ibb6H6JsIP@G=%S&KZX@f?Y7u9+g34j%+YS;4<%sp%v>M6$nc3#W4oURLJW+ zl%e8YN=NW;7z=F%azk_FmIQ2+;GA}nQIavfgSzZd=epCFC&4>6dUAYx`Er$BAYNBr)HjHC zRDQ5Qe5?+_uB$QF@6{r#R3E|m)NjEC*;7XJvpx5FX@_1bJ9C7h_1AnTby-`T~gvfy6{235g;4pi#eb|Nn1`Na9WI{m;GU z^qlW}=iJ+0d++zQwD)vG&J)q!7(slam;!TS9#M|H-+?cWleyN{XUiYYwuB0=VADS^ zIvO87I`-Q{&A8u zloW!l%S!c`HzaD+Ew8Vtj@H0$1>6)0GFsgLFCQ)6T^@L9QG&0ykCQ#0qvYE_e{3A_U0x@CM7W3lAkO8oW zgjRXdylSq~$BL1yDN^qN=OmI~y(Q6W_ z=sXr1(6c1lS%wq0`mt=CA4Xl4)jPHo+8tIM402Cd1DdT&9WV2?+3a-^Fqlc*r;OM> z-od11$NV!BbEg$kT`Q|7$%OlY4Z27^M{<{1>F?YI##|(KlYIUG##~?w=jmkl9wQP^ z6dD8#GV!Yq{od_1l0E+uU;)r`0=mZPmfsfy0PCjc4sdQ$zMw(AKKigkm6w-0kzSx= z6iHRL9?M3 zP0ZLZ9)X`>7$g%mw}ykz0Jk_#bK7Zii*yYpc0IWehbYDm7)P`LcU#Hq!?icK8N&Q( zW$Ak|$gSSGVCx;Siz4sigke1j@k>^S{bU;aTA&&8Dok{9ITji-H zJEQ}(K1gBrzX7eSGQ9&ZFhr=>sJ4sKwL^?l>Al_LH@B&g3if4f6q= zfk|>e)tL8GATT|@7%=n2lDb;gAg-v~aD%w1wqX5Dy^i${bp~sx`U-2K`aRqrhw*y8 cp=#@E{ohv;-R3_KjP~xH*g_rA3TsRM0HD}WH2?qr diff --git a/src/test/samples/golden/software-debug.app b/src/test/samples/golden/software-debug.app index fc4892c9d9cc5ce991a879744669ff8eba4b70bd..4f25d68574029c9246193e98e96218800bb0b4e2 100644 GIT binary patch delta 13763 zcmb7L349bq*01WMGSf2?j&K+bA)FFH2#C6(xQU1YA~Z7ub-(pUV1P)Ngk%Vy>^2cl z5tX9_MHi2CydaVcY{okwZo|(A6@0cHxsd`tvdiCC`SJlnC zH(G0MwI&{M+~lzvg%Er28HQg6Ux{ydOo$@Ce;|J8lZ}|)a=-njO%sQ2%8uG}X~Xz^ zYS;JrphSpuAFh2N8(UW!J>ipfNZhz?GFr>fI``J$#60%?hs4xO^U|z1cc(W>4jSSC zxV|=#TzEbj&aNtuu70>;-RCDfv~k_%WA8uVq4^uV<#Mr=9qwsaBYS#dt?{ytH{aS< z4)8Wv`^!<@Bi3+vfcLqzOW#S@c!!efyYkH1U(_>spU&YFV7j);7ZV+ z8}Ypw-+S?Wv}1DYuu`{aKOy>C=;Lz~zRt0Ih0q%!-K2=jng~G5Rwu~nWEV8`rieOG zb_dZAgE%QbcoB6nBVc+F(_12{j=IrqLd?;ZM2bZBBB3v(-(sOJi>P|pT?u`8M3tje zBJ>rJZkCvEFg35Fq19ZkZ+YcmS4FxN2@uyLZ=>O_=I%WCnzTsHwUKTmgstvYETj~@ zrJ5(Js9lV9m73?Zm-kCw7wM)%KmeejundLi6y6ar8Y%BC@{v1}5 zsqw*|>f}h`d7aiyErh7g0F*+%@e3huMx1+y^lhvy7IdyRs&D1wzlV!EnQ-s0!bPm* z8h?6mCFbk*$gYq)Ik`8{y&u7{(5mCB8oZ184t94Dh?F=*rtZR2;cK6C)}dDagB*+1 zt{^%81R)Gz38!iQ+!MOqX!vsiC6y$*?#iRa0wu$y&)uBP& z+9fM#j6_r|k{~6Yat$Howbs|0l=f{?tIMOX9R*+?ZXoFTr4>Bz|g2eWZ!VIdB z;{ozc^?fN^?7gGTd6xq(4eSVELXcxJvf^B(1rk^hcIZZ)Wf1>CBk>p;Zf%} zV%wJnhTQXfw~7~uul2~N%E;8srSK*hF9u+gPjyb<$*DHvb0C$d*Zchf`Id|HFQu9* zRg-*86iuMKGR7y{?2j}p*@v{N;`ue_ymxHWxsMRzqiTsv?SXnya>&#qhZ6jNsA?fQ zL+J}T8{@#Jvzln!gQKcdV!^9a7CELwR6Vmq41ydWMN}UWg})kaJ~XP9O87vPYLitK z7Re+4hcYjmzz`i45-lqrqRmWKhsfn2av0~3DN*C1Q|T8DI*nHwxnw%ed9HWRfJ*n6 zDETFMY8^?N!CjML!7`IHg&v${*{GgD#SiE~X1$m_@0Rb9cxpVRtVP z&H{G#N^-*#jS~3#@xIo-VQrzqZ+&f}11tAGA=GvpX?r3C3M-Ge6#As7S|Oj6P)XMl zh9l0KH*nCf`S}sG*dHMijL__laNWSl$|d0l6(q8SM>rL;3Zs{H8p`=U4eaB#<;!g+ zR^Pu*k2=H1zt=|9X>xT`pAqFfq(?;PGikIQQK8R5$Y76H7X=}x&QU8Rly28&GmrXR z4zk`reh-LC>9<{c+&;BJzGfGZ<;{>(z1+rpBvQ2pd^_1|*`;}VTj6aPJj}hD6R0{J ztG`;RHS%;B)%WCJYh_PC7Hi}%y6FP_#dM?L^q0gg4>hBEUyq?dIgtA}E+h3BXe^K}3s32$q z&*5bs=E9r-t8)mb3wJEDO}<(nU?U57Ee8wu^_%{*&LtPaCms=L{Z+|3m=aPaCRc4nc!@Gcy-xBFcX!atBaV6kcDiWt-t zTk{G~E?<9)I?odS&&0rv!$}2vK_x>eEW{B-9*zT2*kwfm|GG(+a?L${KKxo;(5Y0G zwlkN;Dz?>yLFHt|%oq7Tj#sd)E1NcnZQ(8~-Ls;)#HKvR39gF`3;3C^E7M+-D?7T` z&QwyShpjFyC_DUo+LPrtbk5~`%5`OLl49Jkb1gz6x_Y3EV4-AM!40IVLEOL&IhY$r zL_@fNZFs1S#EVL4xJ}73gQV1pG}^~je~>|KKf-p}V01mwrUj*w14v-5;A9k+sK`e7 zNEe8W!s5e6+yY`W&zkbdG?F~VR@)_PQl&1T9o2j>qf|M3f$}baE`>c02Q?jxEQ*;L zbI_@>oi8vr`?tExh`GFw)(wgGx19$-tPdguQg1pwj49tTCSiHU$b7L*WT_ zkU(dV!iU)Ek7lGR@)RyERQOM!60Watb+RXMA8J!g0%|ipRsXu$ADCSo=JIfo18v|Y zHRLkt%AA`VZad$TtjV^zs=!VBlg5#@x;m^*Pd1q|tktK|U>M{=KW%VIJ=zXZ>Ut79 z-B#C79yP|lHk?a1l{8Tu!%TC%ag~+bXV~T#UX$!WsF}9cSUJ?KwbgZO;`|V))&FIe zaRqR5c)eo4y7IAQ`Jd2L+Z`8 zx`FPAoI%vvLfV2C4)JKddRuMvXTD9^hNz9uHA8e%rF#)Y9hVH#iTPsgnobc#&YptA8QCcG&}2s&EX&U9BZ-OUAblm$D}UBh&@6wuMixt$5GWx|dELaK8+ z(_P1Ow-(Uhmh=~N*E8L11$0yb`0`wnV=?0!m~dwSA?^8{2mZ6IZkJeoX2fX@tUGLV zN0^ih=%&zR$3up`nT6kJgd5j_@2wJ+%kEnrmW9|-V1DOdV(MU~y9zXdUj-W7lrE^J z?gpLV);n!=Pq=>=eeST;y>!sKlk>^Fs2OpZ`{nw#>${jx{l!%C_i?3ocMg`T=I=4U z`wig(On7e&wwAH*%TgEI)OcqT@|lJtgz*L%7`eo2{PqH@D9BP__C4a)|KC`k6rQ zlRmQ5v-vZ6F2L-v)$@5X`qWl`&CTd5N}BMzul*$Y4bKn><6CYZKm41l(~{;rAv>TeQG91Ta|kDeAwmC<#lsMRxLsd0XDb}Y4%EDq@L zvDEaTrg>tHU~Vk+vrn)vmb#EGW(i_*jipx8GaJdi zcX_NEZbX6do$rq^=U@oZSBBzV!D%@%s~>*6i+l&bXSp!J$+*=>duvHY~cFoz+{6*$YxNb#+dn0>ZTSVQ{$xa zSS;mKU^J}fvw@)J3s#D)Ut`YQe+@Bk3Cx@C8D5A*d_UAL#+(^|YWI}_mv}8yU@T!@ zk6{7LGU6}GL2(h>`PZj@f@D-uZ{$>aFHr4+7%1^{inkf|F}{`kFsA;Vm%p_8QOub| zLwyoc@Ax(;Fv(|uMr4XZkff!YlBA%~pXX1Jwtn*ZFLGh|RiMf@AtCuQmx(n=KK_3j zgZ;zEfe4oIT`cv`AnNFU#~j{8e=IQ4?m#88Nc4yH0 z`JgfeQJ92OJs9)>Kr4x{UH4?bhdDs6cxwB;^h5;y)GwY|POF3Fpg71WRSabY$|-C# zvRcgs9Mh+od^#-d=SMv}?mR#fsKiMY9~`PlXHxJZxoRK2**g}0F&3U`#IcG2`Ze09 zU5jcg3SBB8guufO`3lwZ`w?l*Z$`(}$N7FUChlSP*edK^QTQSS7QI$x_V zav++_T~(M(-Q86H{Y#s_#$6!tFN#ft|yW#$tEzB67-BXIP*c`O$S$wUK+>o64R_PhqpHF zhK0?tI31I6PkFq^gi(>-2MSH7blp&)t7x=Ug$kWc?3hL{KF7y_P=(9dKq(840t?QP zt7Og8Bo(!KbsR?p;x>9K9>m zcVM_M?qJ)*c{OfsGr@Lm7%Tto6f1wou}ej&~B-IHWu0=VU>FWtC-n# zw#vgS)oenCQz=>j*@WvljG3r^y&?E{No(sW-feE}-hxK${U<~<10mF+SxS+J;d!*CpzNbE_U-gqh zbrO3TUM|5+q?M~U0h^=WVKLG=M_E>+UBAoSqM-X;9Jl*I6$kzI8Lfiu2iz?Qx*u|v zGB(Tlh`W&h`!RQ;L3bB-?V$S!cVj{KQ|`vmby?PD+)o7P&$(ObcWd=mJW(9#r6{Jd z<{R!CPn(paw5Gph(Ccs;fh_WqDKF@LZ+9HYcp2~OxbhUZQ2aTrb_f*P$4XGo26IB; z?Ff;;CY)q1kqIPfq>AwNlP*fF&Oo}GEb0uzJ*h59c3>1$4D~NP^xwS)+$0=!CLH4@=O6?;1TkK~?D* zJ%Vj?AgNlJ=zP7rZ^EPq{|b74oODJeOns8Ol^&fiTkwk9LFYi$ox|3f)76O$^i~1eyC&fu#*s;< zB*MyVsZOSidWoma@7FqAiH5IWFE)m)t#!zVP8I&> z2NNj|%TD$5IUlg&jFqYkorklG!#DIJsafC8vN1Un1!j;yIJ`jh!_-@Hz7UD z*L(6o6KM5eLdZBAKh~Z-LQI-U-sba60JOel>Xf9*WvnPwJuRlHuTs6NDysrI@mqCT zmIiFSEqXj|l{zN@sTITbGAfWbA}ja1)b3;2^@yz71=Y0lfQC>1C8erw5Gt;Yl(NNi zth9uVMDbE~zFJ+z1CW${mi0ZjC*q`1nitC~kRO2dDs|H6>GC|CFl>ikfwt>D;Uqq# zNWaz35B2T3vM-~I;y?wva=Y#qj?X+hJ5D{~ajR#UII^KJJ+rxWdP8gW$fm}5>6X@x z!;hL>v~TYKGp(uKN6c=@)-`s^gV$zS(slDXwoV%=V^J}$ZF%d$bYr7;*U?jBxDj8p zU}keVGu5-EpE9C5cjwPfx6pX0Y+XyXM-If}GSEAFdVQ}rVUA9(XiYXPYb~WtZ6@3C z;q-p82g{iQB^#PDlbRMc*R`ZK&!}{n=CVlNrdmfoc+r zk~MQ=`2f*GN<{Ob1(|e~VKdTMG7b;px2JZ@pD{;zkIp=(dsp0qz>vwMnIoE7kF9IS zcI=*MSz~&J;7mhifz#5^)Y6b$u~z{!mCgnTZ_li8`}8u{shL)+U1NIQq=j{v1?hP` zI)OVzB_~I``;Ra8mK;A5j~}bh?%na_@x6)%?MI&rD_WLjX#CL; zk;Lkw)lqa}Ra~Cujc*tv-}2@(473&&dzUTgIiOUemiZ&ovI}9}%oKti4k+*1hW;*B zpXd>Uz7$x+xU)+wJEkF%CcEM0k>B(Kf}TB4gye{3)Gbcas(^?e82dz&t_)EWl8A`J z@Jq75&@r5HDN4u`V;Eu2l%W zssV~FW++?I$Ue!^`o@OVg?OzRaLSBCg5=_p;Bc{wY$(K?-d|1`B-eTGoHA$#zi;T% zB4sk4WG$g0qEU!l-q1zENAUZ0KE8)9vsfk|6YiKz&rUBVvn((7W;ONn&REpN`mx-b zzNp;Wu_!Tu>tFDYH7SW1W6^q<*T#Lr&-ZFFy}a)h4UkK?HL|fnuH;r7T4$oQm$$8P zu-weBzcvn$+qo56JVai@tx1b3WCyoapmiU&Zd^Q2KFO^Q77vy$qt(Z&$W+L8&^lOt z#+Z$nf%02!J(3x0$$nl`r#M*9}t6Te8qee2cqjiAQ zW||+j4zyg;?4PZ$E;7wzcCfX>G|$cswC)^9QRv;5t&pF1?__^f`md1~A0JYqk2h@T z229X zr46~=XZpbVYUxDT*Bjf`-%GX~MDZq6$9Zip%8sSWGxR)e*}}Rk^e`_KlHiCyf@;a8 z5es<7jufOYiHiAk4UOc<9fm9>H648piE8W8K zC{e=USN3v^4A>U`6(mrY0=`%@*JY8Id?V6vSBY%P($;K9Mzv?2j-)mf;6 zCpZouDH`yHuY3gci$q=PB0xwNA;vQ#m>WS3z&C$xWC4N|iFxT}@EHpb1jSFfc~ZDg zWCQ}+!fgmPUeFiQ3Nj)h7N-~22cAje6M3GoX#oj@;hUCbXie~7(}!82u?+x9-9k*j zH;<20Bv6T%RF{G0;Heye@R@`!7QLy(2qVzsj>W&<)mz@+&Dk%Dc1^>fTSYU^k@(reo~rlbx&aaNso{nmZeC(DH>_1@Sz()zaDi(D|;>T|Al n5PnYB;4Q$<;7#5I7mV(|W3v!f(X8<~V$N~N*%z2(N&Wu@IJonr delta 13141 zcma)C34B$>)t`CEL=6%+&5e!F4ej(Z55Z=DlWCP)mG~=pGp_&!uLOC?!E6NwBN7qmzO(p=FFM1&zZTe z{h`Q*_eCb3c-oAyyM+)3@fnJzgRjK5#1U(#Y@a;5`|IeORbF48 zT-DsPysou&d&TJO+g1(jmfH03rWw|mpKbx+tu12g)6a?7we`(SV(UZ0iOkj~j`T*! zK_kyYdrK-evFdy@oKH%DzTx?zO`o6n+_p`hkA3>g=T>a{bflr-)yo?DVV?XH{(HZd zWirk$kBO^t zyrpWm%<(p=VfcTyI!5Mne4uh8@(J&=sFrKI_pG=3%&cpuNi-!Z8&;QB2f~Rrgm*{m z)cl^HG7H~we6PXx9(*6}7;8_-bH5%Xgo>b#&rE!scMFBk`O)rDL>Ep5z>cZevNUlR znz~O^&5=ETG{g+d4F(FM>SP{(8HBC-Mb$j&THS?Mq=!bcM2{??htV@z=;2W{U-nQ! z9~o5zXypi99PJ(vlO|GgBrUBLcsT{dQ$|I*X9*yeCHB(tqq*zI{W3yy#zed4&}eD* zY$2uSEtMlnsGW^=iE_M}f_{}H(e6qF2ml(4h`}(Otjvx-SC&9$G)-WzB0>t<*C1L& z7CD&+>Ck8inEj%l@Yo5o5`<=%Pl)F9iU~a>+A|70o`^YiEJBW2n&^Z7x-<&()$vFu;gkBVNfFktiv?lO@{0J!tp9-*WftY4y0_ViUNSbj5nL{OIN%AuQrO=CM zDa?B`U{EiKI!_SlS-!cxDk<>l`}UY|c4)F0G+fSWX7NI<&tY7N6?z$I3&dw6`V-#r zD3ndM#8;i-z1VlKyP816M@lAlgIjpeIbGRE={2-I=#(akh)x5q7ZLEA412{Psnd_f zn+P35X?&O1>)VsYA>bl?9_ctD^!ZVh2hYTk7y9zuKd`{NtX~h;Wz~>N9zpchleokr zdnNc_e~=}Z3i*ah0Y7*4ARidlZzuG?N45s`Xb|TIs*zC56~0K*5=YSz+em1&h&^(j z+aQ{K+l9v29(5S;Dw((R@!}5=9txQX8BJw zt2YSukkxOAI+sIkeS0+83zY7Ns?+2KOWzqyz8nMlLf^%ZjZ*K4_RJB!Pu>%CPNP{5 z(8A#J5{h7a;#uN=elV&|m&t4L;CeEi4M2yFwNbdf-zJwJxnop`3jI(x8Vp{SMT2UA zRA}YPP_V}QHQd%emts{$mAxlCnd*6Sz@RVuFy|J z)tS<8y{)S$ejar;(B!A1YN>=tW3_9uTi4JAYisaSEYLb$!!3eV`p$AFw^#O&$-X(%T z0Q|vc@Iz({%l*k`jDl_Wz#ij13Yj~GnEN=)EeKel{~T4z<*O1L=ug8i^v)SLXxJB- zVRpb@SUW@&#Q&`i?N9f5=?RvM|wi@tEZdBNxxL z)C!7-Jd3xHUQwYBvk;=aETP?QR+RRM?iMhCc9~i!p|UpJgD0s~X-H24Sq;cJ6f14I z7eh8;x3+X|q8K>=G9azp8)TDgWRpH!C?Ot{ptgon}!wRW-1B ztyFdLB5CRAX;{7N8xh$AJkbEx0I@+{gd{VQ*kCo35L+ju#~Y<;k{B#eNEkGCR(hh(^w#MF3YgQhq+K^ zFx5F8*v9LE-O6l{OFI!bj|rTWh6VF=g)i2{iH5wnCb+(kh%b&ozod~A*usDM zk*yZ#9jqhdr=8q@@_!m=?OJXiYh1?-B){v)J}7B+acVmq86=!P8CF&BB{3roJ9myZL z3gdjJE`@B#VmB0bPapNadu~pw{~TsG;ui1-pi>N$Yyw?ykn%T-r&sU!L1@RX&t{iibWSy~LfwB3(v&VSE` z`ELsme!)`PDd`#Mt~^wfO2T>(dO_A);9XT*+~XxmD;&4IXzUl3_iFJ__f<=Il5`w6 zEXHu${EgL<Hbny~c87CS5z7=O+zjwA}7zjP{T2=Ym0^@4@l^rs9#arBv`KPPpX;;2Dv_2V6W zzCwRtDHp|AP-h|SnA`!~UlC;0HvMHLbz_;!efiq7Ms4ILhSG&sQf>5gx;FZTXSVAD zybGmzuc|s-o#c86c5s%t{O+`c_O`#W@d(W3)6@3HWj- z;rPT7bj;JZCc2t9b_P)|kGO?T!4n?NWbd$;`X837M+|!xvdDhvHH{jc>KQYYPg&w* zn%;}Mre=d$@?*iy&vk$An5p}rr$QP*SVJ*%KB3aqzY8i+*LZqVq1!(O&8r>$(S(^| z$4bseipoltaswD|XD2+0^{#jW8Skf^@F>}J#VcaGYu!$Wlwi9e4r0XXI^j|6*%fav z<6YkgFX!Lj4Pm?+d_31kOuKPcfE=!x!HEKn?yu}r?A!u8zYae- zrf%gFMgYT4Nle{_Q-M`?u+17*XABD(pMBdK;I9r7P_526(4Iyo(Vh zq+#{6-%x;t!mfp|wE0uY#SsAYa!MahYKksg1SlfrZfxCru{t@X_VE2Fo!~HVPcSew zrtZzWalJ1XD2u84Gj3d`$J7Jq8`n9UHK5Ux-I#h%st&mi=awK4aoMHk$Dmm7RciL= z1u=&=`b0XdZ1D4>sV$N*^^nZCpWrPl6;pftEv##imeNaDQa89memG!!X-qwmDbb_B zz}YeNScXIuG4->wL{%|Ul2+3b8MB5ZL~f|%22w#it;ZQ9f;0p}^oXK#zH)3|cMZE0 zon$;{S2u7syy9r2T*F7R;PN7ebBTYGkvvj3*^k7;n09dcpW>T%yGp5QC$Jey+e=ZKIeCD-0`Mm)?;em&Gs&$nq?gEIQq z$sBsEBB6*1U1)c5#=f>Q5p;BaP#a;$J%uA}bYVY0ag9#2{A=`qG?M|zD(|py1@<5U zpm0IAWL$weguAFDt5ESog!3S#+Il#%`83#khN=`rt!L$WG%t$K9umMhk4lX4ecrzx zj?i$IAv!)1;VdFd*8$65ydW@}}mmAts zdc3V(pt!CN3bd4-80hzSTfHdNOYR|YKiOAxnRyK{g}TY3urJ~0G1X4KIgpf_(lY}> zvqFoK33%r$GpF9!;RtLEx%reo$Bz0ArsvwueDWSWzmu(QOUA^ zBT~g6q+=;(B}fl?d8UZ8$&))*+F`s^1+3JBh^!@G;_!nbH*Sk-!y%}N28c$Tot!y{ zI=bF=c)x7yq@?D6C1a%)w$k)&Y^Bz;DqK6-3+ZOPt$sxpjBWhHMf(Q3C+?TqdHr@f z*^Baz&>QXOwRl{N2aJ6Q9@xn*W%gc%pVmuwC^5H*L9b+jHZzFAB*fanpnU+Xq={|% zG6uYw25hyHZqW!@I;A~3`5nFYLUU)JwCi{RB@0#>DW~iTDdx;J`SN<(Pkj0Y+j$0q z`bL`-h$})kg(3!Dlk1N}Pl{9VpO(@$k*b2>XK=-VU5~0M3SBA|gurVD`4*MwxB5yh zO|akGW~Lx8_QyL{-fcf(FDhm+ou&VU`E z<@efG2$2{^Oh3$;fDz1ztHVEeRL~l~hL~^4^%cnkFe5TK7_WLFP~($!PZbaFy>2Vc>?kV)=skbBW~=ry+f zrJX|_^vZwDt{d>H_hrx|vB=|~Nfyy>lUCJ!sovrHQuVGZL44og$9GAael0MquM$eA zvlFy$>DO)b9!aB&4%P=p*_9+9s;+lzR1Ls;m!5F`_vi`lf1jR6FTZ1-L@0g04OA1q z=LWXnKiY^M9Hd%;qd46*d~Cy+=neK?Y;58F-SguUd(8X9_o=ePy~Nk2+zs!ZKeOSE ze7&sRN7b{1P(CO61g3II%m8<$Y8;1bGOuq|4 zCtV-;qM7;#o%A~(bo_Tfh(PG{d)sS0zP~HuWU}AM%RtxnYaL;jF(ay?3VhiwK9K77 z>=$h*9gC-3-)4M2>yYcmvrP!P$<4LGXFFdF(2nyU=kqy$Q>K{)tr z7Wpx;&QQ`2=E~32q@p)h?VU*@+ZfoaDCXe24gUz+GmR7x_* zzb@MxzTQ7314*5)zs(x@vs8aE8u~;+D-!~(7-_p|gwHiK|aDwm85R>C>3b zUqSve8Ew<2bNBP0dj@yE2)c{8`(@Bw!d)G7&*Uy;XC}3jyYBx6{j<3LRnR?~yI%+0 za_)W;bSt=fAm}dR?%#s$a_;`!@1}GW1Qn=F`bv>aB}@(X+09%rJ&{Mk*D~x`IE_FK zX~Oj%K6Jg<97on|)bdEpfF=;5Kx+By0c`euJ=<0?NFGSr>;+ktcThA;XuP z=XM3s{o;A?{4S`t+9q6Zo6II5T_EfUGHWk9ZRgx-mGO=$k0n zq#;!}*4!L7#Yh_Iwz%1En}TU6eJ}HFf|ibf>p(s{2BIj&+k${YXNegjv-wW{U>rvh z@9b#>?n7}ZFB7cNy>b6g>QGr~>qp~P(2IK>do1o(_(-Eq$HOY^nLyG1eU+ASr~q_$ z=lKNYCf%j<7x5hWrOcOpKjn}N4&S2o%4NN#QV!|RDZxKIDL3h{8>Vz=E`CnNj$4+% zfmZ0UU`$WTg$YsPVCrUIJMH+VYjW8vDcuYgMJD(j_D3;!Khnbr-3nL;nncD&5P`@v zJR%1V3Nd|lCIX-pWwU3Qigj(S`av>MMTzFo-?*}5=SWkrHFTW&5ZOkb3nL#Lk&fsC7(J`u@| zM83u3$vBkcrXv~q9TB7}PQ5kPsifz&44$y;&3?7nrniSuyh)LMi=Q4U+Vqt?V)({L zup_r=FI=Cepx(l^w+x+4Y7L878UR}K$cSThzldD(O#;7s3 za^1?xTH9~Vu3b^*J+t8Ojz29(No#q_+QwCt)z#jy3nzB0Ul@sa4di$=CwESGF5Z>l_{axDq>T_5l2n!)4v z*CjqO`F$Gsl!oTUCLfurDvXjodwYMa=^Od9koo9aJ0NnvH0ReAMGl%~du@@ly@hr8 z-qaT5JyknU=5ud!o#lOB+fNqxy&-ZWS`*+C3+wuNH`eu&(|F|9bpz!*Zbj<{%hR|u zwSK5v#;tW|)pDz&exO{(txtg0&aI*~1LW0yOL?cSDU9sx>ou(zAL)MtbNu9*{*mFP z`PG_Xa-!F#VS;S)<~968_VX@om=L+OzxVS7`i=Bg4ddl2_@D0;tx;ZSV_}}Tu{74{ z~M8F_Oc6TQ8$uROwgwsAt9xnmK6uGkB_?55(#YeN`idQ-p1*F(A4)HEQEuOfYY z7kayzhDMei$%F4S70SWhH%+5u(i_x#Y-CR{gKL_HKyBNb$H@EeKi_++d4T+uTXxGp z8S$P0Y-&r9?C-afcWz66IhK3(0OJI2U&{np&HaMbA$iRsvADl}UvF0HQSwx8P3x?J zkFasXk<$_NAa`ILiC6wU1os^&<2x`z5y<+TPH1YWkNs;oEb z5ch;=G03DF*^N^MfwM(RWkW@{6iNti)UXTy-2b3s0ASJePh^6&&n!_}N#yx_%8+c) zfSkm1XqrI+2#{_pD?yZ4hIC3;-B_6hR$E!=3q`yVEJrjVKeIZsnNt8=h`PM8zOvTG zrNn6FBab$vl|{KCThx~~A=yAr^r4KPL=$BKhLKL`0F}WrfvBFzA~Bdhl7b)s&ye2; zVt|#kO${igumY_?ux!zUoWvlKgFu~ZdlchzVvM}E6V9kZaYp+%4pT-)9F1?<5ePCC zUny4OpD_savqX7g6(Gcm5aSpU$VQOy-k|e)jTp~x=oOgV*x&@QNYm3pQ&DOj?`gM> zOM_8SQ(03{+0ck7xWb_^d^2=OyAgq)xw_`sCgzH)MX-pdZUKNYun-gR&6r0vAW)8& zUS11F0UmnzOv4u@t!s!IVgx&}W83;qddsK1b=QoMzx3|J|NY)O_^-X#jxq9pH*v?9 zNYA);&W@wyRBz{wF*(h!$fC2nS9gq1FM|im`(Xz%pX-g-d35COo4qsen6%Z~jK{&t dy`Swo`iL#tgt*OkkT`MCX^F8{n}kTq{~xM#3)cVu diff --git a/src/test/samples/golden/software-release.app b/src/test/samples/golden/software-release.app index 30992cb1461d7142406fe96a5e729aebfb290e2a..1015912f0b86f8a3c55e2dbcb76d9f9ed1cec8be 100644 GIT binary patch delta 10658 zcmb7K3wV^(nLg*AoFs%>Zjc)!LqfR7ZNg2>sw4uD;Hu+H2d%qUGcXAQA%QR=D0T=C z6h#aiFIBs?U0q$f7BBJAR#6nStSfHsin?fPZM{&`F1uUXuI~Gu^Z#=Rai6F2(_(2E)ScP&QiDzt8TY!ZnZ4jvM%UF)t) zh?}0!)8&LZ4d9KD5usJLpb>mO0MzX-_;(#&eyD%f@rHxT54HE}O>(W1n4~|J&9YQC zICEu%Zg)V^S^4B;F37PUJMPQuxX3bq?UGnvFzRq$ zM>dKr!k2@Vz-*S3qXBUAT6vRiqqsv?R#YeVi)V#2g2>O06bj)4K%rvt`uvDb6oFmr z2)tr?BcCJt#Z$te)Wu2FF2j8gfln?CDFA|#04F0@eZTk-w}MX;oNV(*AQ{ym1L2mD zSP5jM5nx#S!yX&KcL2jQpxaK-;DfKGP%RpyUj_O{0gunfWiZx9v;3=93$<@&wK(s5*=f(u{(7)JY8)oNl))zsuh(VG$vM4uK< zi2kS6NHA{{WW)}s^tnrOU6;oQSN6=$Rl!DaqdV_XD?6~&X)FbET~Xa*+y~uv$?zN4yOhIo#7R58+tYaXTErEMC=nez z(uN2nrCK3Xw|rgr8sQ~CNrIz&?(9oLC6pGJU6)dHkeXHNW%z1rSxO^_=1?_(Jrobf zZ%=K=+D0(J(G3zZ52ys}KUV~ftQQkNjRBDleH*>2D`laI0%xNPIGEkqe(-d(ps_Rx zFs{nN7(-kVV`Nk22>r%^-K3qz*u&EC(=Vqj{2gcZ41v}H@A7f)#_ho*|gLB z#AVzAj=CC*c}%;!uF!8)H^^)BsG5r8HFCGwB7xYeu94f-wb|XTqno5Bo6$Zh*P&M; zN<8DaV~%LgCWV0nfC!xDJk>VZ+3j?8?4Yq4eP2x#IC;KivTwh5ULUWio(ws7{z%_2 z-VG1QwnxOm2LJRVH~M{U6mqfA8L`D7n;B|BC#@EAtXdfNJhcF*jIO>1+aPd-41ftv zLv^XGNFIJJ1C6Jn&1R|J@>i7gdV@wMV7Up8+N?)RsHb9y2e^FNp7HK0HB&FiRGD8H^SMH0Avzirrjwv zI5m)fG6;v$-6R80lI^JZOTOvl#Ul z2+jkV<-!YUCPZcmt3O5Q=N>&$OLY_VkI(Qt5mTG=tF=~1uAKME!aQ-LSX}HGuG(h; zP!>oV*#svJV)&L*Xm2%$HmVFZUsFn$2%vlFYDZFiRhZDa^mpoJ!n9$5prW^ACK`mK z+cNU7N#9R{5~xGw8hT@Dj2M{B%R@BsVjuskPd@FXGj)4%P=#|9i7@ZV)%!ObARxHFkfAx&KRuB;T&6dDE~DTB3W6b}QbN!3PVLnIY0 z06Q&$^u8p%Anc>=!%AK7=g|TMg!<;b`U)g@uNoUJ1+yY>ut2|XOp!F)fe)B?)f`$x zM*l%#BLQ`{jUtRB7!K#5+HwPn`WAM7E>TgdIW)#*TZBCcZ?;zTZ9B(zfS7AB_1*Pj zlWisiV|^Q-pg3^jBR6|JZgRkliBQd<^SQ)7+L`Xj;3g-Vn|lXy(-eYOO?m;QT{3`d zS1ghQ(t^06M)AmII_dvYT4TpBpzh1km@TE${a=#wu@g-CjUfUHW0?A9Mw42zZS}yZ z4F5fF7>HQ1m+Rw`N@Y~b$^a(0z!>1fLNS znJydRoC5KQ_6XBqK1v<1UXmyBN`^7f186d>%+9o4cW|bq%bHBPQQYEsB}^?V)54am zZ$omoc>yak(dJpH-l}Iz@gvc1-vLW>oy~fIDRuDW-!QV{A%$*P?)L9Ggje4qY{{=R8t$sbtqlud1QRT@nd3wDwquRGco5A?bq$BecJ&Ng6s6j;rhxDO=~9i5gBInv z6bb=-oDuUkr8ae~yD1p^>%)8~r6Rz{yLc&1OuVCb9wtt|P>0NI#2hpMeZhmin0a&Y zk_Y`!#?8fHw(}tilhrWMze@G8Jc1}8S1PBH$@eXLHfR%?6DYNuuw3fkItBv z>?i{RA`4fDL{8nzAjGh#i;G3sbFz=1M z&O~4`Fdc%)NQg^;b`(Vf(9&xBU8Wk73o#2S(Ts_(2v%rH&x;oR;4JSmp$sw@FFLqd zLCF>hU`kR$h_>WCz$ zA!A+@C5%&Y?5kDT}46lOi1cK;lA)PlYjwZ>f!CWS59M^ zU(cOh%PAOPvN9jmag!AcN(bi6k}LET^QPAm`Od-X@eJhFA}nlI=tt&NEK3*XYO8E+ z4FySM-f=(zM6(rbMjmz9PK~7VsLMR3_Rp_Ro|L;WlWRSmI8k5Wcz}l%to}=?|2CTG z&9*MrKWSZWgQ3B}&NCqw?SFvAi#~f>0ptUmogzJ{{&=e4xl}Z~?ZC-(BMKr0CIIyd zo->4>M!76AZOxK$FN`mqt+{QpqtB%JleM+vpN+A3Q997;8kL>H+LU)~oxOWER&n<~ zw7K109Mc_#1$=03kcbJbA z9rtWOY`Fx+(>!A8^M9rK-0uDhnN022(hb@%r|}YK-o=dJs6nt0SYQm{Fbqi9`eJd& zp8>N4fIDh#TDI|!7!5uLq*NuN51x@<$1pH>3c~CZW>kN4MqM2@F&K|9i)Ti4!Q`rSK zVnUOe%!zb;Ef=FOX&*SRjdt7$fyzd|(CbsF=ofi?suBHS_T5|w&fijurL3;lVJVoz z?CyAkH_JO%XYSy&&JLC?JGjXiLm+d;wEx%|rk++T6M% zWL|KTItC*eP-C2cap`3!hx_hwtN`v&!^7;@Lf(nnhMivhQ;=q1=n0gFCn z(6BWFwwwX%E(`t70qGus53cm&MmnCWfHc1uWi_=xJMFVTKbRErbXXc1Hy+8rjYj>(+F1#S#W~ zVJtyWm{r`iY#k_HvP|ktt)~}_7GcUQbY<6~?hTi;#S;S$wK~qM(iFHW-qjK8iFfzJ z6PwGjpy61;8_KCkU_jsvq6zr; zZb7CtH4E4_ltK>+5T7hEC+VDiFu@GY|;0DF0283o+^a1a78FaH4| zg7ee$v>ug%VSlJDC3s&p4dbu-!>1&>YPH`l_=9}%c&Y8AZ(lOC#w#^IBY_-knI%=GYZS(1lTIZ{_IeN9ypLJ9?9qI0jRTWNe z83nN~u5{a~D(7&yVRYZB@$yyu@Tx|6Oizf9*%$ZAGTk2^qvyx#Q7gfI~Ud%vYIs|&Qd>{Q=zyyfoxK9G3V;1Jf$bS9z^SF4gH;KN~YP< zwrA5oGapVjCwsd2b%y59nh8#DqG|qVO`Y?AZO-Yebn5C2bVX;i(_x#pch)#-ZS$2* zzq8FYowc=&w#~-1HO^i2bbZ~u)-Rve{cFz{dAQ#4K%=YBzgRoP**(e3T+%hC{9Tj? zSHsQnMXd$w~l8@_|eY4~X`f_}~srTUf9sNw-Ea&qA{c+z+IZapZ zoK@5YtxVz@X?$8P3SAHYt5v!5_v_Z{lX5HaNJs T6%b<4vP(nn?G%FE&(Hq>8txjL delta 10062 zcmbVS4SbZvnV*^6*@Ofngs&uoWF_H45@3_?C4wdbDIb-ryDp*yEelBq2}y_ny-2$R zf`EVmb9Ytr44?&3v`9d0^-e6kJK9RC)n1`;YU$Cdhac^gyDQfZ`hRBT-F=pTf-~Xv@dVDzj;@5+^(G~iu*^lUf4Q2eaXk$z<9@6R(9efo7>#F zrk(9LHj3Enczv`g<=2-yi1xNf-_VM6Xas*<3G|KM^KboR$;q8tKPfx0k4p>> z)yh|8ysU}ez*Bw5t9V~^y_m=csD)xIAEG)%1us^|#V9^bofD7qoocP;aXv|9c^mi) z)#3fXcLszjeCi%Kiu=?iITFw3WjXitd?5P@{<^xD_Bmgz-cSG8(CU`9RiXB9ZCk_A zCTDT%9j2b`bLZe}aG8VeQhdLG?+f_8+*6iubHCU#Se+E;<5P`quyF`uS)8TaJ`uf) zK5{Ov3}vDz=kWm;9&VuX9q5-elr)_oZ=-g47Gn!NY=DR1;bn3GA0YM9$Fc;QcH;p2 zawco|87>B-sn3RvDqp~>LWjX2%fn<)!@VraM?{rbUdA~a!T}FZ+lzKU232*==!F&} z49JXo+#QR|16V&1GYm!rE%tO9yPtW;U>LX=QW_1&8g(+K($mIvD_?Fwtebs{ajl*7 z^avIv0-%t$^DXHSy+m3aWCeSzwA0<}c_zpn*(Dj`;YR4i!{>!00Kw1kAVDyDH+zMa zf=?8Ze9e-8ZdPanfY6f+^Fa7$0YKbRyy^%NJV+>{LiPLH(#fQ}>3|SxkLDM%{4yo0 z%aXM0*`Yk5(YsLzndKeo?z}NI5gzUbDLkn&M8F+Xl_A<};wy-?OJ}f$Xpr0rh<31> z9arz<4bFf2I@sAjbf_*~_%?$ggtPQa~={0RisD34&A5!qz5X zM3XQE6RxFQ<{lvj(sl)Vv2LWTCOEwG9cKG0l2|Sr7!O3ei5x<@`q+tU<1YQHu)#skAQ^ z4Du5`c?Qv$AIo<4N z+%}CxVW%mUQ;ZmZ-GOssVcNy%=|uNr<3(JW;xFw-8hMp8g5?EOtAEfEinH+V#HOKR zHjUz=vuOZIy_2`VbO>0(17s+G(`L#>WF9~9g*_w!+H&;^f8Nk0XH0JxMAX4dR#shAW zHMBhd(EbzE5;xUaxMBlI9BOk*pD{sQvhYal)XL}F@^E<`m! z2g(N=`iIaDJMwuUpAI`!q4_lDHp6y7%&B);!<@a_j=~XG zC@%(5=z#q4kwl{Z03i8jN}{JaOYXczqQ_FOj9tBi3t}&KQH;>bmb+8s&8!S9CJ_)` zYSh@#rF?<9b96~eanh9Aej?dA-|TZL)oelOC(%U>>FUC!(A|re1m>(Ss=pKn{3`|G zVRaG8a_O)oh>bJ~S|@q(_%n!RV5bCv!?i@bYnK=R( z96@I=m-m2I)f^LV|602}mX(4MfL8`V?VP1T@EIomh7(u^IIbt}W-nXg?vF?um48bJ zh`reC-8YoZq-<76Ob{I6hclaL5*3mp^MGSuWmKP0?O{)D0r?HZ$L7Z#1K3mmSh0=; z;R`T!55P9V!|fkWLLy)21!+$b5;=S?NKYgodC6RQ!Fn#VEqa?j$TyUkWR^ms15!DnZ=|ihXg%)P2MOZ9J6YiQ#_3Fm6W=+O3Sn)4fq{ zFgDpzJe?#0_QDb2NNrNKeFk`jR(?}^dP1fH@IZ3B14!wpTD2LuceE2D8|6WaqS$&? zmjDN17Lus82@x{2P7SnkHbMFIqw>kKQ2G`vLnJPS_CWDs}A(rF2TMN7Yq@_h7}Ugsl1^E zxapR0rKnM=LS2}ijKa7niUt%l@yipmecCt43DHTE;pil;P0;D>%!C@;oWIDw<9QxF zPH6)dJgQ>+q}XL#9WfRO4;*TKTj`1b{g3Df>VhA#@G+zv7V(9jL^$F=*kb_UF1oI8 z5jYG}e&uUc)vwcq3`e~n{~iYQ4_qIzU?$;ng_@Ds)$m(>^f*i-HfM)ldc_hCW9 zy$^YaFSfQ4#|?QhK4mg{LJGhwTy5jxipYN?gKl+lv%VI=68?$H zf9~DVt4x`K$QgcyZX;NDF=Y*ADM@0jF&vh(^H+sP*$x<+fF>Ph((LvIsYbhM52`{X z0Hbh#lFd`op)xpb?{Z?%@CmT;q|6VDZ<;$_^Bh|K5c zJ1UpxlvK!m$a0Z6dRsCuP-v26yXXi=bo=wE6&!;{FDusNA4aD4bE`FNAiVkq2(9$M z)eWDsem`ZAv_5^(q_MXsZ4*5e^VJnAaV?L7Lt&~1lseAl1`t^k0N!UM%_rf<7 z6Z=T`9o(Gswp6vDJUidGcJL4|bcKr3fHUcj)sga{F`E8O3fzLSC>cB@55^V&@htAc zo#?5z>23Em%>+IwMIgjE!lgK?bh_Hy(Sa|#L-9Yw9<-A$q7@jeqpk`Cz>~9L(LRZ>ALUBv{f`niK=C z#r9J2kmr!T;3+yOYTLkN95P;qleufu^hu>7hy_K7q%4Iwkae8PxrZlB;5F(ylS)St z@}AWB=n!J-19OR-IXSm)qHdOrDlmEQq-$L@IVGqo$5~-vuc~A6$k@N~?I^&kNhCCf zdN_)&-h>ah{7`Q~XR^sypZY1x;6sR=8lW6&v1lIwjqb;AxW^xmUm)(J!b|cOSKFAS zA{*NX9G{H>@Nz?a%rWrremEd^jG9(pNU23J!Lcjb)+;*C&rjaHc)&a zFF;vHK8kyDtjF?u8~i4MUjjreOQD^}p(Y>t6_@{R7W-=+U!UP**5_P(M~I7);?i!X zD4WqoN59HY$&GeC*o!nJ!0eFU1v5EXkE$zEi;5^*r{WRJ;OS8{`^F+2M^o`gdV@zT zj0zKDsglvRaPhJvm6A5uP*RI1y^wJOofM<=qK@K32w8~9k4;F9>6cp=SxAfj4-j4C zX`R&m#MS=P)&9)Y{@m53NaWD|#?}6Ld-3%8OGr6(Vz7p?dBn#n)R7xs= z8Sk}b{Ds#NYQRksWB=n^b?GuIxEAy4M52#?9%L3{mt+Yz4gKCp*R!M4NE~+ECXkFt-rsT^P<8S+% zYB2TUMqf3wJEI9lP-WAH=G)@tHgJVMK+SMTvLPz;)bi>6n5nvhROVznDv(sQF3c$) zJ@Ws~&ygUVFM`-@eIef!@!5BBU6Du-yUK$4g5!Yl%(x{+Yu9rB5sE~zKLV4lBRjVJ zX{1T8f*xM*%mcztBG6l42mcDaF$e|$CI}PT zV>IV9;*UNTT7+rT7N#AAjU8lOKafAuMn3pFrzcscbmN=Dq7u8&=>zFP+wD5SKC(I&{TU}qSw;2ytYmKt~%PY`b&ejdTh=Oc*_OcTh}x+ zEl;CXxS_sTeKuz(_O7+DzPYwNgP;p)+iO~Ddp6Bo$kUgvec-+owM|Xx?7SO$islQU z?zpYI=jd&fX(}U9lQV?*mo05>ZfW;NYyAz)_q8u=YN}m6&EK;)QsRC3&|R70f>6&d z&x`4+RZ=Cs6!u8jUM^TQ-ddUy4V8URjvb?3^;E{pJ8{@E;6$rq2#q1;?*}sR(dR2m z=S2lnT^3+WzhSXNeJGWi8ELePF@0riM=44r2Fn@K|0w_<$pT9?a%sL5jF~sia3&0p zqZxsSEwY||y$m2xk{uCSNtBgkctNAjH-OOkVT~@n2zKWD!5G z`duHO#2A@Bp__6nU)KTv(sda{fVkqyvHXr9>bDI;6tB-08-}Q|v_Yz(-Y*_D%?31g zn`U=?vDjmpXX}f^e$&ieQ6!!-&G{?*;;3nMqWPL>zOtfFoHWf(R*VooG|l3MV)3qN z-h<}*rukSyq4=d~o^BW+E|O&B;xhf@s+(8lih)DbpH}9oww1&9-RjMigVob3hx4zZ zHI28bKdzi44&|td#wlVZ~1|Z52-)b{%X^&2qe>ZMi5P2bwyb++#ItYu3EFoFHVinOL}kB z_2PVyZe}(YiJOXbb5?VnI9{xGc5QRMxNM-$HW!L3rrBLD)%oUOe307LGFVYBr)4-F zseAKV3dJ}yn+B=%Eq;CnfK&V3J(6TM(&wpPwTux3qx4k8traW^ zp43D{9Nt9^OE|(<_RJ@Q~hD{cz%l-yJdoJ z4X)RGi`2ba#)=19)V3|-#Zw*XHT;}kr!L~>;Mdi#t>g16)-!fM3&7Z{g^NOGyG-;T GLH+_j{BZ05 diff --git a/src/test/samples/golden/terminal-debug.app b/src/test/samples/golden/terminal-debug.app index b008e9a82422993e934187fcf874c11e01260152..9fca04facc5024c5d765fccbea5936c76a0e81b0 100644 GIT binary patch delta 15954 zcma)j2b@$z@_+Z+eLLaJ47;5 zQGtiqGoA4c;~Kz->5O=0@C-lG1<%k1PS4Zh{@<#8{bm-u&*#4%X6AKOb#--hRdsds zI5S#z9;kB0V=5%oOr&(~^x_J_wmgDK7`)a9k{hp`9)aE7Uw1|rya0kjB zH7Smh$UZge+($O!Tl__&;2-b+)$eM>5UD`CNExkRv0P^difuvg7J?fA&tuj-WTl)UWF^L5YX#LR2KJ<7SSy2SH8*P{M#J!% z+AVsC6zYONr4)hGcAGp>ZE;Vptj=6SeIR7H6cqq&rDi$UOEEz|*}bIA%fCa`N-=S2 zvI9+eo9SkW3QT-Q0G>ef1^&!fK}>t8p)C*)bR1^wz z4a;Mb_Qu(L+yPY;hyT`4*rLJ~>iU4)m)RBl!q7Ju+9FW_jo#$8R#g|@Y=8k#5fJJ& z_ig}hH^5R+Q7Y6<_k*hL@{a9Yy30G=YJA`2j>Gre?i_sI`Qy2LtRN)NTVT*Kwe} zqFktlGXzEWB>f~K;Hn-Wh}{nXLZsDGxrTb0o;jw_LO;VT=$+T^kKAfSyrva;mRmu> zVF{k2mfc^HvY*fOf;wLa6lV-eDDp){QjD)aIC_a&-Kd3_w3l0*sfBp-GPUdhl63Va zejX@EMX&I4l_br)YM!Bx*UU5Y@H#zP3F1^NROStCVzQ#5M5s5p^>@fn5f|z$ZUMzh zx{q3c3IxCX`l*vpZ|kQrq5e!yMHQWedWW8%Pv|$TK5FkoP|n8`IT8QV-{>Jzf~kHM z2*6F%7aT7J$r1IV?Ll&Uk`$=E3?xEOy81d$S&;L+Zwx71YB(|bmSTlHSdMgTF<*U0 zApmPLQOD5lso4j?Dj*W0iRlkaJp|PCMIist3m_i(Pt^3tIYJ#I@)1q|q*lSGNnO_ERjgdg2f>Lk`WWN!a4^N&(Rz!30@Lr6m2tMAx99r zibq9DiecDzCy)$YYnViMVy$-xCf0#?CJ-;uW>Za_?q>0*L*{PmJy5Sl-6NQoL9BZP z5jlx)J{f5rYFTW~@Rz=fgLVRNT$o`$ZWoAh_$Y15^)NxzKZwXoo6P>B5uFDFi%j$$ z5sXV=RfBF)61!kVsF6Xl{i#vG#1S<7uY$-5K=)5c*1;^sA^rL{62vn`1=XL)E3&YDEmmOlQngTMN@!Gv}V{xPNA8dLc?QI zER)z~z~B9*M}1~i5D@a4Q-eA)0QD`jSaW>Uz!7JtGlQv{X&9l-3dTb^((xiU2NMsG z)t*gY?5!JVO#ETi^VPB-I-@yG8$?SmaWT;@C$K$2+9Sh~=Zt=v-JLzV%9ZX5J*xue zF@BW$dC#5$&ktf5&}m|l_vJOzByD94kHhj<{Hmr~W-TK|OZ!*S9wXxgB1HtYBxSBM zxWlDAR>li`9JIUM;Es@SxIaX?Kos&+BDOf7HW>7gG7dM)M;8IT(V$NlC*$z(e0%`- z-x&OO>8%$iOdettV4QkNA2$&hFgt{vRWPl#x_x_f3vCM~=8^VyxYK)8$V=UYy?SP@ z@C=XCab+-W>3u~nd}=UQ7DyOVTHO=`$KUWmU|l5EWuJI9cM^?(KeBThrx_ zjN%lLP)AFoi-bXZ3q}>Pq+*y69%bsHtr-X!>Dv=)Aecf$+ zYBJ9g%Ih*s+Q%@@fRT)H!58u((B8}=n#n{?d$U9a`Bomy$xIVzPlL>IA87pfpTb1Z_C!cI_vbO1MG@i>QJ_z2 z6-Ywg&cmL{96Ge;fUMxpc{H<`raVW3T)B~w_IEf-@b>ex`j7%InJV1Q^4u;Lnm#hv ziR^D6fGhn%bo$uPX|DsF_PRXMj0vTm7~07^BF=1>Yi`KD81&JM#xboO`mY8(h0!)m zFxUBS20N9pIF$Kl%s;69Zt&UxAtxL#Pe>!>60MYmp1*U(xylnyki1>T5$h zOX8?N{_o3}po5Y6Hb+4HVx-E8{XOM7gN3yL%hu*IP~RIYdg9eQ-1GEk^IrM)G8g2$OyP2Wr` zoA=6ygKQ)diaR<>ECa1-4cVv3y{+O$lLM)2=%893dcV;=o$~6Gus9w>LZVS^2&J~5 zripGFL*V#sD%=&UscwawD^wL00ZQj7>E%>@Q%KuiDFV%A>R`{2TkHlI50lSl)fR>| zGR&T?&E=PMa~ zMJUxqeu5k`lv+=xETOIn*=Kn=t82GX*Mty8le8e}x1rSNDxyXL8j8n7vYwzf*M#h5 zDZ^rHoxNDrSrY)Xm&i@5k?}gmzr}raNNwi!kbSmCxb%Mz{yv1W1`aQ2bsx`l8Rps| z16lPzC|*JqH9pyk27Qp4dk?}0ysTR7qH+MvTG+UEhXSS8n;!|KW^zSbJsL_q*%hZV zbbO3d1HDxFruewm5j);c@KZb9{xB!!^2BL#YKUL!y?(*JBUx4phi?r|5M3^^mro{Wau8>a9>}Wlx$- zTK!*$47oD~XT4WDfY&{&;q@#{)$O=KnE4N53bUrz9w+In9Gg$ z=}{=$U)OiYd_&+;WI*4BUQH_zCWl!BC7;KOABV%pFpY@d@^Uo`Q@}LjK&zOhR3A#l zWmO?#$a{PY*)lLTW5{)U46V+%Y(^C^iX6vBkr|^?fHA9mOc{g;GVFXG))}yH7-}RL zDYV5!U%@pGY(S1dAZo=$KOY|D8IcYnYy+{dMK>sr^|iDr;}J*&Yj}Fa7@@Nd%P_12 zuyVt^n8$|o#Svf%*yw_)BAj}T76$bd?n^PKRTgDcmCq_m#qWM;RUM{67btu11D3m2 zn4F2%w|6-42gs%Rhuy@m>I_c(Y5+^$fsgcq!U)RKv`-B-eCi3v_5+c6dA2oeN|J7W z)X=cKR$5qd_Q{|c7ETPI0Y*|MfFa8`k91VnUYAdLSooL(9hcP@mLMjm);*S=QElM$ zq*dq$Bkn>&9T7$uyfD=nORdp$Coc!F>f!|pr7hFeF4-IC#htw~{b{P2p*WKtF~VU#z2zR;Cz*GeLzP$EJpT=SYOx9loHfRG81b=`h$*%_#LM?z$`^&5UqjHA!=F zm~fd?0AWo^I<(CS;~a#!PSBOXQ<)%DivoqlpYG$yFV&o|yx4U?xsg0*39(?r|bG}oE#Y6EFX2ceB&Z-Yp1 zwBO{L^o71j=WVNDZx2bU%dlbrw&*L^YTd;ni!xVwR*Twe9a%{j2W=RAVjNBH>M%~Q z1kKN-?cZohq|J5a896fdwgvwn>m?@YTAG@@N!Hm5xE%BWm|Bui*N5XVk#yM4ZwT9) z<;zsOQaAAwE`V1y(BT%>ICpvq>*_fSSGVpWPn;O=AGMFy8u_cP#P1Aq^L>H!8`(jNFA1GlyZ?qcA!d?2ZjJ*38) z2pH}TXE14dJLnK(V>I2zxhG{LfS~@$(r48k22(Dg!RSVz_z4D67NWuI>{<0BgDL;e zU>&&A>bWrP|Dd$z=@;61A>2XVa=u6a)D>QKTMsJ_yb`uuTza|J9ab55Eo|?g=RJp2 z7QYd;FZC|N$QR#o|8ZDl_kH9*sF{~Z`!c>I1)KdmKE>+G-I~z@PkSdEOxy#B+2z9O zJs*7qUo^fC^md?w>cg;or5OQoejMicm)QCmiAt)t7dQi?>Jy*GC9AKquZ7E8C+l#jnO2VD z?L$$=zMcEIMn9_pTFqJBhu- zqp#y$Gp1r-Kga$ZiA|MBAqu^c0w&$41~||lM^}26K@D`=566^d2BBn(rkI}^3NS_- z;v~3?KGY!|b)v~z#c4Vy4|D9Bhc*%O7zArR>X#-A-e$g&oJ3~`tfsg-4=>M5b?jUCe(V^BwtMIwo7J5p%CQdJ zap53Llk(P&v~awq1#kJLI|+`8N$L;26O&kF?3j*ys~03^PzN0^Pa-1J6v>-sI`(b2 zkb}ghIPl47W_a8>an3T!N%SF3r#bfR(wcxr`}cnO&lIOS3EYi~#8Gq&G1niTd(9(7 zA|hczq(!c@$Hl-y)+(J*XTcmv?s3|}njQNOo`vls3tQ|YE+Z+H5*bbgg>-w5Kd#`8 zPx2ktG7r~L>U$h*pxnL1L*)(>=QxQ;#BhZ}Cl3nfI^SPO?RH0x)zp^`9_KqnbnsZ? zr1sHxI2t>?J8pJT_x1-&Ch4|TUEsvaL@J777);_q$2(Z5i*p@ZAGia+Om||KR#%d` z+U@gKIbJ@buFiG7o_sM$@NE(C1deN!{k?eLjP+ z?{ILorsL1OerUOq18tijPpe%%>Mlm@&Y>RjQTjcQ$8)Hsy;=44JTR!9$su2Ku*YSf zVtRq#CC4P%d!3ZhiPg(F+SeV9?5VzeNwI0Q-{JN5+wVENr~B=X9DVRI^q)Fa9Z_KL zvaHV>`)-*UIS9OhGI-)XCVFz9e6fCFpGGYo}c{^{p@WcdUl? zMT+k|E#nH>-7~H#^KZw#$E=Myn44Zf1ftQ7UnZte*|;!bv|&e#Hi{y?%{hh~XCJzP zkND(KpFEZ$FO8&D48tU4CdnCK$B0Q?JLibHL{j_c2rg9D9I$7EZsokwYfTQ`FG8My zJ-vU#)FuW*Oz~l$&wfy@$Iu8C5YCn=f$`D`j>79-7|m}V9?>gk@Z%$T1x^ARUb00!QI-Q`BQDP;&)HxA4@(0zq5xzGrg;JMCPE3Ud@Dx`>Ft&X^ z_U3L9C8w-f=>zyUr_ST<8>Ctlfj@(I61F+ad4^i&Q)+A9;O?AImD%V+t0j_pP~z@Z z&|5|`G2QnNAJb|J^Mspi_lfmeT9cA|v~csm5gdZm4%6eBd=$d_e043Y0OXp0dq0UN zc9a*iuZtv3qD8zhVn2{CNuB*5a)e!`JhQufApcz?(Sv&3LR?b|aAZOBy47FD+ajLg z((10CNpcVR&;>cRE#4IFjUE^+R{gCH(GReEdu87ta9f3Tr+wBkg zP;S^KBbcXE4Zf4~Ht|%%e%RY4FqceFJsn9j(y-6+u;!($=OSQhKSFQ!s23vkqyFer zyiqS|TJJarz)Swvd(GJD<-8d~JIJ};jHKqW9l_$p;vAY7mBF_XQK~Klm5f3ec=QuL zD;g-p-Ap0F9|Qbx$?rg*p2DC#8gwK?%Bms;J)uEVYeE5qK~HKBy$Xf;34@+89pPA6 z74l(EOX~-ajYUm`Vb2&kB6Aq@M-8IgC#zx%BFUbW&==QNJ231yz@BFqQrD%J7ks{$ zF)sjv!yc${*c+R}mN523VCjaE334yF85VISdI>~ySLq&{)HzeipuGUSOt+K7*cRSf zf2LbUruh?S=#G+Ua&NI2qE1Zo3W#213fz}?f|oOfw5_M413LecyeW+3qQZ^zB{=x(>+R};tHb-s#MjCp>=tda@cI5BSCxSL5UhCH`SYA zZ-BCf#q0=JEyLdQVVwZ$$1qw!ElUT$`ZMe;xy6$1IY*ae261~I+Sne2wH{wIOtDBE zk64HVZ!i<=2LWvW7T%U+f)bx#2ot;wf_X$x4Cf>cai5@$3H}T*=CZh`BYIk`kMg0E z3`$E_4he@c`8)nhsOC3}Vek4ds`-sz7+Ic6cJYB3$GvL=)U9EhRevARW zZh*KtKQ&rfzt-4jQ~I-FDDtGc1)}(D)uuCkOJYe5}Kp88<^fRP|N?~Y>5xd9(C1JcV>k1{nB;Tc;g-0LxhX)Uz!i(QX1 zM601SLhowrVMsoj-w2utIwC z{lW+3w$Lwqm=5Vm`2ZcWzRECP=>8khy*^ET8;y77+SvhGf>PM|_xTI+6L*20WQ?4j zguuc?IzvxZjBTtS#&=UlF;1owBSjg;2qs<$;=gcA`756OCR;PsO<>ZaKP9pweUiWX zB%jJwYoJdtiYfkqt~ErFRfjR)vm9VFB!IX7D(2mBsWCCSAKQSeeOxR+sPPQgm{wC` zm|YE36Q~j+x8RIk9-Dxq)kl zCB8%5Q_VY6m^zZQiFNcPbkrbiC8<8m#eyPFS^A76Q z-iWv&)vktoNuB8d9d7`q5J~?c4;VH;dil9IhB6Tra$<5Cjl4L9;wm7M^y}?Q2urWL zVmrz#W2wH@v-n2pH0pH?Ea~`UEwMf4e{}VG@O{Vf|3URn zS(jC-=6m7?jrjDE^!YLKlJu!G&Ki#<;47BOu2Qm$MIxnJb@q3-7J$`dctCB;dPM@# zy*g2m$^I`sUdQzwKIVA`Y_sYT?yCJ%%VzpDan{xoitKkoHZc(6 zWYo6*zk#3*>_ag)rC#Stx!X@#zW;dgb@1eCgk|uR;N^K#S`_1=S#@;`TR}!&u_XB< zcnwn@)S7}({nzNW_-FGL7R322(@TpkWiyXTT}=?}#E} z7lH)7YFk-dp}v_rde9;Z$GDxeUf=<*ES1a%!Ql=Q^iU0p=?cY!F?1ZpmKM_$3DbTg zrYjbv{X~qn$`rMc5~tO(6z-BeXxX3hY@?I^Uc>Wdpaj=sUW|D!HKf%W+zn+Cyw3Nt zZf}`xY9B0CoXPjSX7(`Pd6`Z zYMAHFnst0QDCV8JV%dVGMT^>QpJiF@{j*1vbU+yafaE#NM=f4qY3(svT&TlZfZPVNtMaA>QiA{@|n_8+Hn&(xw zENF78Ztps#y1A)oUiI-!Ez1`#JrlAHtX{rg;oJq)3ztKAyy}H5gWKL;I5HZdOEg*<>2_V-J<2aTp+ynCi#v6BjrC+#cYL6uyLNd;OXaty-;T{rjDHPh-5t21yLEOo_itEH8P#ue;Q!;GQ%`r( ziXKt@nu%sp<36yWhc&dued3&s?iVY{WzsELIY{nzhp!wI)ipoL^MvT-Hm~ev4XtIu z8&_6XU)8#DeVP0E$}U!qKJM2m>#UJ|c+gSjb+={{q|#k_UYT`|hF*JKh4qkr+>6II z_2WR4Si@;jWt(4p1#^~53cHDEz32BukK{6 zGtKI#OW_)SaCcGbZdu)1Ho70K?&My3UXiTa%37A+wB4)Mlv(RE=-D+r ztjqM{!8Mgpz8CNW?Bn)EsX&(>wa9(l6EfA-DFb-q-(fzqPk~+pXW-$^8mX zAMw+GY>y~cg*~Q&+>^8YEoZp9Av;p8a=!-gtJxa4o}a>NyGQx`50B+wcjVgM@&-I5F--gJx*EB5`}$GV_Axw*>Fc{$H)_4pnz(xB}w+qg-tZ~JNEvce7lvH0Akr3egIMx2h?7F-w_D0jL`9~>g@ zcW=b^)9!2de#f-R%Hf;6Ax)NYNJD7QL~={eD+}d(I{y z=D4-H2UvTyyQ$rSty`{k*Wu@aYuz^d47xfUpJG6Iql&G%S%_FZX%_3Zft&xUtF(CzlXhKt&I( zZ=U16Z-_bqDyZWQIy#Fy#|32^H?(yI^|_4xe^q_@+#8?s=lha$S5;S6S65Y6S04_1 zYrS*C8Zl;CWBpA+h@bJV2mU7UBk@xg7NXGWkHug5w=tmqR(S3Et{hQY{6}=on6+q8 zdj5sW<}6%s!IiW7U%7RDk9L_$KD?wcc={)s!T7S}qJHnQV*CY*E?g!qd!&}wT=qmS zx1a3VcMaN`Gtt!iwP++iZUA}9A8IZ+eEPFnFF9Pl_w;AyZT+OL=HV`q=Z7ET!F%xA zeNT>*QMao#SeCd=Rv%gJX01W8hx@2iEBm`|Ti462?(%}```vka*!_O@IT#KLk@$=Y@`tX97^V4u4ejp9v-)#J--xr~0bwSf^C()FZVyg_4 z0<)*cE|#T1q4shkB-C?7IT{ft4GZ;VTS%EuZ!vKPkzCOpJPs8VOJSGDmxq@KQ7nYh zq&_Js5G4gd9cENQ9*`DwzbLXurjkNYTp+edh%&WM$blrHDgvmuP?XDI#g-V>kOD8E zzAOR=E%jAV65@(!Ab%Y@p}sD%@aJbeVUzkHH&3zUfJp=^hbT=-21vM4*^Fh6N&zI4 z70^;(ujAA&G$a@_j|W8v0x?FVm>?QJ8&f5_gPbr0VzkD@185T_M2w1QSc&LLN@!9Q z0j-0o0J?UW+#rIgnx{&*$5!?ZbYggWcYbB{h#H!KC4>w@b@e2)gzCXmr4smTM)eOs zA6hm{jIG<>ELz;BD@&b$f#M>~fkmkc6x*VtNT@*sH)?%q01!8u35eO_Wz*aErDTcN{RKlR9 z2JDVZTL@bB)2d=;njt6@m4!l0XBli#RfAp4q_)W8>WMJJjIu?gEz}tSyA#tE{fY=@ z8rmXJ2?d|UGXw(wU z-Dq_ed4byxzZbfb@OzPaUUir9B|HS0@SL~4653ko-dtVQcbP#&g++>~uZ#+Hp#jE( zMNU;;853%`d$799Sz%D^gmnU=+6lGN04sEGsIROLYBd2dBWWxZ1*uv?5W6#ihRCQ5 zxsGa~Hm{qudn5O-RlJL>+-vTM9`?!E#N8kvu`3r*&+Z~g*B9r8L7SIw4`i&lOSxA- zJ%p&u-0Ms|#HlUZ>p(rksLQBl*T~9sBPT)7&25Qgu0r0K=B@4L%l#H0^+s0)n2IUbgNvb>#0>#*+Hlq z0uGu`DD<0AH@EdEXy=vya0rU(4sKwn_XGm)S#=+Wi*9mA{V=KF;JlKwc!~ zsa;+)@u+uG*Q4eD^(0Xbi3C9Ubf7puj2rNWO$%<#ay}DC&IVk)fT& z__si^1;o>Uc!kcS_h`^VEFKMb2qzTu&DwB;Iv7Yc5$i*AWDt5D+3i25XR(FDg+5{& zB76Y83&Z@F`vsx`UQ4Gp2Y^ok2;GD@Fc#ta(?F4l?neR%MB*CIHKypWR9^?oA?c`Z z0?FeE_FVvp1L(fygOOQ$p97^Gs0@PqK&-GqrD*?1ZQ6W3+?$jYw0q0_1H}nKQg$Yw zY<<)QgJ6x#0c&$Ugo3P&(sAGu4sxg{MTn0CdC8@){ip|p#m0g~r3eV|V4xI{pdJ1o zcoqlKTj)fiBqgIN+&w+Zi%z43RJyNqZE&iB`e;@igE~RLadQW4Mwg&jJIts_^$DhL zCpJR$3noH3xUrS?4<>h!Rn`$0r)ie<9RIMYdFtpO2BX`ct!7X#c{$MzC9vH`+I_>4 z*MxB!-Rrt_>VB+VO^HRvAaq7H1&Mmo#KU-0KllBfW$xi_@qWhzK?8Q9QzV!gL0!^! zgW=vkk9lqP&dx|i43PFfY1hd_fk+d9ElG`|46a@xl}r@+IOuh>!3~lL!~zK0Dhhd# zQCl2QV+?w*Ou+Z^(M3Qv8niP+CgAY-_yF)H8vIb{?Gz|lL?PRTRe*WwDZd;`WWZb@ z^s0gxbuxi~r{n-8);J!&ps+Ds{743qb4W31B17uoj_Q$+r?{CO-JGdGZIYy}X~Bf0 z4p*H!$&NnE3&kkZu7UgX@q6-X>zj?nxw9pU@k`ugb zyx5;+kwN-3YSAp2urSTM6Dz^r1tj@o6@A# z2ixR_FvR9y{;8BD_XH)W^%%-2kywC#}z3m{Zc>bQ~UboNLy4wq=#RqkJEyO*~zijze`9Vd}E+8y0^ukPiW7{$pUp+-m~ zigvX-wpUI0MU3KHkWj}<OY$g2GMbHJqvw3{ zD5tqJk7g9pa1_eX;Pl#@M>Cpf!eYOJkmEDLL9!)}Xbck-lcsYt$ZRglqiJNClD0I+ zcP`JPIgx1+ZE27NT>%=$tbMGs$1z>0zjmazTl3hAXLNa+&~c;^8Pb(`L=%{(!!L-C zJYAJXG?9tQh{*AS*bz#=+&rpDOx6AutdN^+%Ojf1M3J`Eh~)9=Jerf3Cfb$;N#!-5 z@z;Mc6UEvRA=$h(k5Q8R{ZdiDF>yAfpV#GKr!a?hZ8_ldzdny9#WWQ;8V9E?rRz5Y zDI4(4?TosKf+d+Myv_2wEf;8RHrQj?%|HMT`juF7i=orb20HC*d8CdBjJF!v;XETQ zZCGn=%G(V3I7Z{1)&_mML62axO$*EozQbUTXDn`GJ{s!}syhu{J0RqQ1Lg^7WW3AJ zVb?*#yPlinZiB^s1IzoJ!`@@C*l1vRqk*MFiA1>9pyA;_vxnzSjr$B1z6~tL z!NRVAWxE!+;QS!%KcJ}^%%mPN#7T+U0{Oo$W0LMf>fsy#jf;~iFY!-^M-3L%1}s~f z&pe8(=xu z527J4Ta5{&x1ijKVT~bhd^`PiHCR&_3mI0ZV_0yGZlSWpDf`%v zw!cyYnsGG1&d4oxlT3uk=d)@e!=^IKo~AP{&oQ!U5+hIbk+cFIIhm2C`A9NzA9)fZ zrxVhV#Fs8EKK5kB&d@_o|HY(9M$Yt+Z6=+<$TNIon@LlQJkv+&IpP0EPqHc6fY)JvEeWB|kyN7)m$MZAz$9L-uS>XLW6MYI+D^G({Vt zW`xqiIubR~$xtF8QuPF#(>-LLC1qHQsI%wDI%^bw_FQ=}Yhm*1|Y8|Ntdgci3uqxJgY4p$7-MB6|5XC(rpc;f5S2)YiWKxcOUO!h1_+V zPQI@SY5TdSJ9&}1I+UK+jU>;g8$)Es9WWW+(S8?7QaMK55?Y~d4HcE*Y`l#&=uhf) zEV>;AJP>*Wt}<6WMBqfRzETv6u7$#Fs&D6PCvYh;poc@RXVe~6@+>HMA#Z*H4zrhM zL~yyOhJ|^CX~=;VF-@tyVl-scvy36{@iAn}!2E$RwM>oAJv0HOAWEEMC& zn%>B$zwivCf~CB?V$ASYAGVBPC4jvda&J7k+BpzPzeqcPf(a8*u(?ncWz}2$nB`R8 z&Zzf7RLTP3L2h7MKVZAyeLNIOuEYH5V-~F)nY{Xx+o)Txs%U}j#e{L%fj;w?6{s&l zZe&o+VC*8;H;hXm$l=K#WUr7GVC;E8^<^kY*IM;2W*MhDqxw4JJ~gPa^EV+>tqaqi z45py!S=G0pp3qRxwJ~>B=;| zHe~P}d_4JzstDUFCFHELSIM>^tTLRWz^AHd02N(2g()5osB<{u7ma#^aoK}XN2Um; zdWO?SsOSqN#i(B!wpYt^A$w>>4G2^5$f61lc8X7;fre{F9pmmC(#JWLbOW_j`H^8* zn7j`X{3h%jWaxVb*>E#ljR<2IIxrj`_JVejBXC35Sl{R{H1mt2ZcNyV>lt-Y*e)(& z>r1-fL0wPOOPUfUt7=EJ&^l^`#q2s*Ub2DKmk#4LNznWR*IuJ3k;~TEYuwj{b{f+} zYp~bKI{S34yd1<}OA;S7Eu4sp6v7YAld!!`zDfloHG@T64@YUDmjt|}%ya9GDRa&a z;|!TVra7NjYO_bZNS(t291K`M8%j8^6b<9D>Rd)O{}MG4sPh<=HK?pQp8*>T0O<^< zE?{5_fPFP{hAw32#+)#*BKFmjY3r3)|cHE<>iSb76SVZ1n;$r8uuo z{};o_$+WV4vKQq5p)Eu1ZR2MIYCFR&%*X~ zG+CW}y{t1Eb2yBw3R5U=3gG|q6dCo6(e}4yz29>KX8a?CA|0$d3HP4kN}Yd)lN~5{ zi3mA1`wlhOImeS1%7}dfzf=`O5Lx)_9hG{3#3_u>a}zG6v@&lqNijtctr%}J1Cbi+d4x;Ud3bj3o1jP~iw>)(s0*{oWbMIRzVrv9C`p=?GYQ~b8n*TW&{=lh?wRvPZ8ejYBD z;rEXu8;N6G#9Re9|E{NQo2$TJ8cG*|A(0}w2n>y+@22^LIws=#=Ez8TV;{g|iXKkY zs7Rt*q~F8I4{ID9alD(3Ix#n5V#EL^<$zO2U2V?zRK&}CR60jCEkY+F&8JSK5o8_- z(Z+XAi`X}t{37Y_aX%xH?ty#*gU&Fl)R__gke?et0zlW1bLce@_q{FlEgWXgHPjik z$Vc7EsKq(dav!C?$XJm>t#!O*_3(BrsMh7MEfJ&%P%%B;Z;Y6Xq&1R$S|?STa-^Fh z96!?E!?q-DCtD-D|9=1K2>X`bzagS8LcPs^nmz{(3SSClz+>?-)Hwg2>T$C9$mwu4*Bc-yU+f^ z+=x$U7ZI$|hkEO6RNgk4{t<5*P5(P@8%_VGh&E3#yuAMx2t5!s$LwHANv5#2bal5%0}w-_&SwDhY5})V_yjrYi!jd3E-^ z4w8raOzGtQwn2S*G})Dg%_Oer`MA0ulARGX)^cXl(_coN{eL7`U?kBe$T`vEBw}@b zl%0>2$F~$Gs1`=;2YeUg6Jt@7)WeV63C{x;`_nGTO}jdZby_uG+^CO?HBtLP@5sPf zf@*CvIh&?+cv|yt-W&y2`yu+~MmZaz_I7`EDy*m$P3&C@0odrz-D>7moAOo+Ns+o= z7EKT7i}3=6H=+oJcMN{1$D>sE396k;e{L=z+!YO!;@QM`f)Nh`u|x8A2wC+cgC5bK z6TmU6o?_6W8bo!VtlGn%$25q(NJ627LBG!*__PmtTw33OYD88&!?2x9=2&`yXBoCj z5ADL-o@WqAwp&7BT>E^1VNU?|B*T!?E`hzl^%8OC4qt> zeG(u@1OLnfFGyz#pHfyoD2pgUKE<14jPwvuj=|~x)6)*_rervq|KVvZ;8N17_KyY(i}b+WJNCZ` z10y6xd)$NL%AKDW(vcysK9Afhz2j3N?BmZ&Qq3f=y|yIurJaZo*G^0T!)r?b9{A`+ z5(&);EA;U6l5IH#4m-kTHuKwcfdRf?fOxDfG{Bb)5RcQg0ls2@sKpi;;64LH@)|I} z{RW6sHW+g}v9P^HETmy!L#$1#nO}k<2B>WeK!mWU0qPwG5T|R*0QHswh{U9w0cvl< z2BJh>?5|(@8=!awE-|Fq;Q+)%IAMU=ngK+$y3_!*5dw&p+x7$`ui^`8IrZR9cpOgB zTa&6VW60B--4cWFZrm|Ohm-kCt2$Oh0lHI+EB6TMH(>9Q+we<`jZqapsK&)OhEIsm zuD%Xd2;uZm4&I1-5ZodNz$6B|0h&MY7et=>Oa|By_Mdq}P`%HplbG-?8V~^BWCr|I z1Bw8c!n4CoQn5TYIW>j_Vzp{I(~~q<7vK3$kKu#@wn3L2XU4p5iHLeu%$9GuUrj7? z&hn^6YMsr=W1|nab9i3(;leyrb2NyH=;9a_nVVsWnSnk|x{#@{a?iL*;ZDmLrdQp} zA4;uo7^K(T9Hoz+Rx(5{yg5prORdsdvf2|QliZEXOg>6GdP|HRRD$YOy(PEt*aJ`k zwnU$BxBH;nNqC13+l@Cj#9MWz579B{F6Ikm-$TmQ?=$zt5*@gZb|2}y6o&m^{!To? zL!hUfJ}>M9ilqJ(qZg=F;bheRLU+J@$B<1K zCF58gl?NmWB<(|z3e#K?_`#!^=ba99*(BwBs!Klo z-~WH~s+%|CPo`doOh|Q)J9+}}XE7p$NauTaq(6JJ($~hSHjZKtXhx?d(9FH!D4+t; zsJ|yaim>!SDbA$6EYyd35&w{&!x4z8`$w`4L}+}BMuET@PNvHkn%M7=saj&&|38fN zhwzKW!2dw?iLA@2L7w#gHqz6_&qLz+zfz1&&7f(9dNe-@u1`_@jw-EEvXDi>>($wx z;ynPCcnnXd4O}biN00klYYql>1NYuHXOlI=e3;K%>(uA%F+0w~7m&`$LRqUv(a;fO zSN3Oks^D#B3cwAAI{UDer-9}9oK=hwSvA_@iT_uj_mLLe=r4LcF+Y(9eWCY7d6tFs zx0O=wQ(F~e)mR>@9a_s~#D8_H`hX@GW}(7U2Zb{RdKG*rcK|W7?|ox`fBz}BkX@+ zkTvt*Ev^X^EphMrf{fb2@E;r-U6>%61)|RBNC=lKB%fXd~kGg8dzVw@N z$77lG){b%0PAPDopMKJg&ND14IA{6FrSs>Wcb?mM=CB>}XIhqf{h3Ga`1Z`{1+j>j zHgE2d)ZDY?E?7QyNAsMfq8-PdJ2a-hYQQ|gUAMAJj6b45hw7{y-Md$Ix3+b1>jP!( z2P@00r*$*Bsv@Qc+8EdPoXLNH5n(yf56KH$COM)}h`!eJ7{NYTt*a-?Pqk_d2#a)hV}nYEX;Q%^H%A?PxXTcWw6v5N9-~S94cu zzHZKJu8MJa+Y_+XZEc1TZ~;w=+{@kGTx0d?%QJt}+{GHAo7LHx*x6he(R4?-KIoxJtqC!og4aC?+kK3+%VLdJebMrTe@3Kx_M4Zml%Jv;IZxRUenT3E_J_Z zY45(?(nYRCYlys?`^Rmpw)zZZ4i{|f6w_be;H?xx2e{X5>?OKGm ztTDrx__)^Y_^kfS)?=(W!+9+awsy9bY22aKUe>+CdCO`x9bZbHzXippg$vK6|CtKL z!f_XGs+d@S+BDUZqU@C?FIu+L;2TlJrAtXrBxf!4p?@DN1m(q1kzBIy>?Lz^nA_Yx zZ(1e?x#KUo)ID&~#j<6`tc#Zx?)Y>IGRVE|=Z_DRFS(s|4wMJn$@s0@b@+Y6y?^IG zD-m(u-8n$ccVoK-${XDgy9Sorgh!6)3rS2N#x#z~jKz98hRB(J*d)XR_vT%Ft;Q?e zS9cAt`rhFFgg@>6+a0ibfc5zO?o9mIyWPEf_kiksJCLT*DEu4KG%Ypx_gwko{{I8~ CwyI+Q diff --git a/src/test/samples/golden/terminal-release.app b/src/test/samples/golden/terminal-release.app index a402c61cdda616cec40cc3c3686cf3592e3a131c..a44b3383ae3f96a56bf1abfb0994dc6e4e3b523a 100644 GIT binary patch delta 10626 zcmai433yz^k?wxd8;wUJSsF{c0SF# z?yBnQs_Lrl>Uuixs`kTd*%dN+_!#X zi0%8Sn8FK-1%P|n?EdaUX!!o@1?i6GJiE_cdE&tCvvtp0d7|rp*vzlhLS^C;-oi6Q zoz}=*qD!mf1>&IAz^91kwKCo$&T4n^d&Nx&ck!eTkPVu4jx8nB`me!NvS9i1zF6bY))*otH-`O4tvTXo5 z@Lhy&JHEa6K7j9&_`WdOYU@b}pKD?)Nkbo>4t#B3q)kFYcXKqkiyh!e8G!4#Jj1FU z6Yz*&SyF~=Og~eRWi#KcG%}4dJ&*tTf^^2x7+Y>jVCe}8n$Jab&gD7z{0*+9Y0Sx! zA%vu%F$LnDoC28YshrD_C-EDYRdUJ~ZdWz;h()fcxdj}fM%rv?i3|hKPD@~r^VCkZ zOWf(2I=PU$Ipc1P@f$RZhEhcwTHU}^rl}@_G-y@I(a%W`N8D~}0&_tPy-eKguEwOzdbv2|E>EhEBo-nW&B{H@T|oKak&=}@ z83tt#7@|xTFXhePmBNzWI=PCwk`=)e?y{>^E_bCwTB$}$gz{Id0VW4bV%LBy3L(-H z)dLHa1anJ+Rr9908G1Eu@x#fP&G#!TtIeIjk^)wp0>&hQ(v378#?)(sx1gNYij@UD z`29segWb!U)2s}}tJCWQpR{mdJ)a9envGx$dV^Ro>9nPhpYpP1G0XEOCuyzbyNl*? zOkh}*o5YO5;?yRtPvz3(0NO3CDZGGB6GsbY5@LD`;^V@i!Wmqj$@L4kBY_1@f|1TM z$al~HW)|0Hi@KtWFxqptLuXxJcneDe9bS6IW;`TL6zxga$!->x6<36%b`DO6y)03xsT@~o=g`I@AbksW zSP8KwZbYo|&(Xvr#W)hO0(T^`fFuc=mnLGJePbtxm}VR%VwZj5qLOkf(O}8s(w$5| z46*`3750^S9%5?Mtgy#&il%2vO6+qfxUk(qK`uT4ar3>jO)oSjf>vtnDu?&n`ECi@07+e>6;If|q6GxO6p8 zMj;Jc%=HTVi7!j56PIxPB2ihE7rs~ykrv-=xJ#Fd))PC3ns2G3kWL$=z`hryxI|Le zNWUlrcETvdrII2oo+1qt;m9;TNs=BXp?xH4k#%Ouhn zlsTIy8SCF6I$`WKmcDQJNj1YfI5>Je0H!+stN=cEU*!5L- zu7I~{hPTE9nDPa3T#4pJjExB~MMYjMsWes|M<~u!I-3(%2jm^cdZ8SVcz)y}iPTBR zJ)*hF8Md6Cj@(+3CjB${unf>pF2gu6V1MAG{m0>8dEm(ME5^%@Kmt+*Qp{!-2_fKQ z2#v$Rvw%~cwNF%4=1sVQtAl%w@Kt8DULo91)fs--0S8S+shGp=LBU51qN`&3{HfJR)f)6Ar_+oZgg+vGf zsX}lZ5-tiPTr@YvZ`y2uzagbUy15*AozEwJTvgO~6+bo1zBcz@+d11i9s1RD5(k(j z!^1v-b=U$cC(uC3@7@Z`7l~6UnpX3@dOLU6$ni%Ap?64#zDDimhF%KQX`n>=l#Eru z2pt`8adce~6y@M?sI>~!eID*T28y_cuZhr&P&IC+T++@ zpax=5Zha7l3O(p;t*wT|Suxkg7d6f`nL`99qwUO_*(WVc{-%av& zv;5s6+G{4)KOt#-B^Uv{pi~v*TGZ3oB9)zL(h93RGLH*Xi6?5V4);Qitx`7!R+Uy4 z9{N4l2dSR9#@i%5gGuR{qYE5?(>w;8);a>G0S25FIszv-6ued69zi&nVtri%k_`xy zA*m=R9IS$AQV6RFhQ~YDWO2H-G@CTJUP_u~Fd|4;JZ*4Pq1-3-lP2svp|h)`k}d&weyrlhz5cN8@A z^?aAr=F5PEj3WngHJd~`;)p(`fiztzlhWA4SKd5+x5%zL!gIxAb$PZbx@yT>UYxF5 zJM%`4oW-l}17I)XX_!)7$eieQv zHdMIcqudAsGL-N&)Hl^|F9~R49bWMB%^}V>$OsmDkm)xoI-Fx({buoKL!rKld-ZPd zc7r>8H-DKNQ+n4eSc-OGZEVo@pcN9$jhS70sYRN>esZx}7O8dHXChR0zuCny7!
    I!GY#^G46Ie?bdQnM&6>a(3Es&`(hFDi76Zvr)hpH>XWU0b_iewwrCDrfGJf zz~xZ%H{~WCqQ-tSZllISO_h$@>H11HCz_C4@ec7p(`@Oz-yfF3h!n-i4oh&p$DRtmwcm=#yzgxNk?N6d*T0yM z`eCfaJ=}|nZ8QSz#fd5oPRo)PP<0iXN2`nQ!x(sYrFeQ;cIgqWc>y-udZJwa?@6wP z7}8a~o;EN4`*cLpJ%zDEBk@Y+I9VVuX1Zc}?u=^O&%l6n1%`^G20lOWqcL69(imRUf5H=U-mp-}>+X83h)@90Op08N6)i}eVPWodR0uc?&7TormE z25^OIu&xlGX>1-aSt*wi^{lICWz zB=CP@A&N%&F&-Ee<+G+~6LQ7sS>2*)6iKPN{gz$7tqPVwc`@^Iw4P|eBV=-B2~Lh93Ci`glOQvM@( z@?p4AtKS(n5g)cA-55tJ9-UL1|I=93Uj0#Qr9U$>yT`=mb3887bV~e~xrFz^DTzNP z2U&e#{v~6{Pz2IE5Tn)DNgg+LlE>suvWx^IPczGKj!dRMLFGZ4{v@${@xt72JWClQ zN=cXkCtTnIX;9ewA(Zlb58y|plU7MN1iKlBN>T={

    600roVP1zu(MXB6NBVjq_7YaffoFX|fb_@ZY_DF)H&;&e*^|BX1; zQW+)_oumVF#77l*sh5J0z{po57d3wfP2wSZgXT&st$igWsz=5IH)qQV<}fY z$%i{AVhj*GDDWmk<=jQdzoLWp1l|<)&r3@xr2|`kQ9Lovom3{MBY8pr#t8*HiMH0=?Zp2S=KEJ3yC7{r8kLBL&sX z6f{cRqD&BFrS@;q&^Wu^4nr2$+it}a&rZ@GyAR;P zN;795{GTKE4}hN%!M72E{{}%Z5vbMwFxs_bQ0AyAEH+HEV<%F?Bmz?BCUy0oH-7K| zWK&4gWyUYoEt-LBT{lvHKV_UY{VZ2_Gw=7J%HWd;i2l)eP*gUEz2a#s{b&4E10B8MQIj1mhx; zjBCFEO6D14P`v+1L_($XwsCQMaYOi1CB|QQn$F5_G4$$x<6d0M(D)3E1O_QC!br4W z))qe!b0z=yff0IXU|P!YG=Jw_)RN@FSV>(WZ0}^lcg~lmk2DPK-Wfm^1O+_!d$m#$dJ?HNZr@qOn2on>@=$G3mHH!{a)!8OE1~N zZ(jNqM*(17e_!9SE$f3VL+w4i?H#?r(KVM|uVp&ef}Y;srRxVT?ima%=Gf1oF{B|8op2!>3A_~okl@+^g2+&2gj zdV`(wy4(BK20M`=(hL#s=-*eZPN+#?E4zXNfnZ-JDL0rbyZ+#sV9!u6Ew&@kqa9Z+ zx1@EkWx?LQV93+n*Xap$2gSMD^Dpr91%sWQOM;lDwX?w&Q>o}NKZUw_Ec zV@gxwfxH{nd)Bn~dD=TWJw2hC(Vwl^VxLdzPFoIq2cHDCpuW+Hi3YBvf2)}f7bgTc z0KG#Xx6nBeHARWexW#+@Ibz@XWGWb-*)%PRRMi?avH+u_(@c*N9VY?Ay&E#c*X!-& z>RPY}<4ayl7Er>(C^;m&10~!oE*s1e*9;W!d9rnQ zAdfGTtyc#o@r?$|B9aH4{7LlOV%A_Le-5o0{-(s55DX$kB zHfD(x8(n-ATD5#w9NE~!4@>A-Kp&Q^+@U<}L@7*^Eqp_E{-NNTCW-AsPX47t|6r&{ zODdD1y*uR5vdpGUcs4n;V%5EPQ=!&aCPoHbV%MgudRfskM8O41iX^?N5vw`3zkeOM zo~k@3WQBNTQ;v2O8!vbcJ4MaupdNToQmxh&mDK55TS~PzRP&B4g_W{aWlU6UJeA~) zb{Vyot`EuoNBfC5vt^o=Q7f6$ZY|c_s@c7DQrX=oE0~kX+7}opCI~8D`=DenQM7E! z5~SrnSQKeEl{BIlhQRRYMR=R5A^my{d zCb^tH+?J;KnxurUZ7b5QQcapNb$fx=?mfs@oKU!LcB@+{KCru5t@}Us-tU`9!v1!CQf5-ZBopVN98^XlC7E#@M&`mE#@7AID$BVk}+vFTk6AyWHxXr`wMn zno{X|1Dz|Eu3HyhxxQ=J+6`+CwU0S;aAo<>w!MGcJJYk|qy1ofb2qCyeuB+evu=GC zyZOmVVsrDcDlwXuj@p9u{x)xHs;2720X8F2Km$!KP_nD#p1JqnIlSgC_@pH@{m`x zd_0utw6b(dTvd2kBQSYjl4?=14@3`3S1*{|4dcoA59f__P3Ca%m%<_++LfF&5E)Eh zmWhI*Dmc)LqE&c4R#fk;;LSb{!<6gHN^z#>f}vHs6#$<_uzIsvOfP=VHG-e4Wz$4; z$)AEWITuYXdMDQ2@(*FKWv{3xEz26o%~9L{jzGJ_!qQ26w79i&A|b}4AbwjqJW<2V zT5d+TKaIs-0wa?LXu;6{rjEm5{pk*dlvkK~?l;*Au;0znF^Vy-$8 zM(%efkqtD|oy3mg@oo=b(lJ;%?F-CEgM>PAXb6YM1LE=F`_p>Z4l$*yIw2?P=Y+VC zrOR0>?~{}D8-o#;*f{R@5aNb@BT{YgJP9T(29Y>MktFf+(m3YXGf;qIn?V@IFnh%K z@+z#@mE|KUde|*cgRFpfgMsBd4=}Ze9@t|cg~ZYF^2B(G5bT#wd8am9J3QW^P(v^y zNGEW!NfL2;K|?$hKaraui8ol4LjtD_8IsBjbIoWHN1M73n8w=U?NCqLSP{r2&oP*p zZxc@DXq)+Bb48V}zy?m?W&!?*A6JwI3vJ+3ZWhu%V(Ah+72zgVs%q1?*(`BxM|F29 zzC|MClI{{yD}&xv36L?dOI%eMOiY&mi_Ei2yX=Y(Hp8TOW=INOe+pkJ#Y{;ttUtxD zREk-W!r!054~om2Rk(ngvn5r)nMxp)#T$;vxyj8NendmCZa!l0nKQJ%%&u5 z;Up;v9*zmvvJ&zav34NFE++~^9%I9_RxmrHg zhE=j1lg*B0qG1`*3=>Nz@v`EkI8t+EVkz`mE9d6Nn$z0D zf49OwAi;3y0qZ0_z@&D~$>n$8BuB$(?HxD`&~RFK2Tpn@c%FH^g9tLkx`QC%p4wn; z6*Htv53LpH2KV=~YVm4qMJ_4cDHZeSaRliS?nvDda^&^grPvX;o01Y-+;LHd9q5JW z`Ta~^p)bb-Wp3v2BjV`T5HiY_#$JYJX?=Bm|3KEhtfo)eiyP}R8_Rfv z=4P?>2;@YkN^T)((A0y`+@k1k3`Wc?;^q2Mb1RRSOT^jw@UU(Cb@DOkDBEG?g(BQg zpP)EiMe~G7l*`OkE73C$(V`K(HbT+uu)EmtG{w%rpk0GOy9p#-YbaT816pvmEJ_;5 zjlZM%X<8vAhjtYZ0hr>zw4W$oiuck^vQ9KNPD#I!QqSJT{PaE4IE=<#YCPLG(!Y@wdh)(ot`cNwFt-6Rhm9%$sS*{BaeUYSXwYluL?|h8*$fxR%5%7`>28 z*h_c*_z^e}9M(J_y{F&q4jcP3v7HR1^+P9s+gDs|T59H`$U$x%qN{eqycKIFcm#QM zG8+Ct9?!H^>UhXtB{&~IP9Cl!WIC}&5CVz_93kEupIh;d+=u{dF6VnZR#Ylr-X_Ip zg-L|SoG`ucc9K9T7nOR{O)!I#3|tO;V)calrqRee!HcbkM~BO~Mq^SU#?-Q_^-eZd zMS%G|;uSIP;1T%#a}$ay298e7O+`9cH(mH9l#0rx!g+VfY#qKOW41MR8%d(99Q6p2 zt*ziF=on$OBt(sZvFR~7JuUMtaj+?$|C4yMsbt9A+-L>Ryhr@HX<}m3fEmf!14cIM zE#qEoy~EOaJ0xou_Y3x=o~?AsmRM~PGlX+_ek-%ffwUf}2S~Ju$Z@QQTP9|u0VD36 z7)<2SI;HqUyqYRwJVsXs%O7O%Z?NOgned=K{cHys1W-py*x9(^YJW?*96~Ne%;o6{ zgTh%@LZz_YETpjJeb}(1J4Y$ex8g6#4bo;{AG}z>HXLqA7yiDCph2E}|M|hH2xU_z zNEv8St}TmW-ku<~K$tLu6fC|d7y97+f!ci?650k>{OdBZP@5&ElI#&O*^}q1O_`s~ z++cs|0>h1ve}Kmqi>ArrhiLXbCPvhk_y+FO;^E1q-Un?HK~M4aPJp(@H|N>6H2@vPA2&(TT_4 z4%JYH$7ppdrRNvi?awBMfrAbljuMwq0@9g!`*>V$A4lc(v4EJ6!^J%&EsX^}=8vf4 zX_-HEM3I{)Dsg67MxwunlzGEsI5y)aq~WnPN5M1t5FYaO>5-Jz!oK>1atBaXAgetA zSRXeipw;?3sQ}X)z*7o9XGL#9j(J)^XqQuCellkHSaGuVBwrMDJkkUT~A|!ZA(DN)>RM-;5E%}M#=uq)W4ndY5Z-;z? ztD$P-b0GNzRkKKr$<9M@0u-{YMHI;R&qMM&NM4W}knu~sB=ZR-*-lL%Lr;N>ryyPp zA>%1XriQ#IK5Z%DF9~1k$R%XBXXxbZjl#c_&I(5A;A$l2|F59RlL};O$jc5SM?=Wq zO3qLXc}3jbTEt%!Pq!8&UgI|zRA)CP<3X&4vA7qJtZzamfnG*vUzI}p`=MV4??qBR z5|5+-Oj=VVGf&I>H%Bi!Q+R_Q2{|}d6Vb}+EdiO*Zf^N==uQupMyZr?r#6ZW+gqRE zJ+exwL2#1zX{gFMl}0rX4x)j&7z#N2gjm31;-|%1(}V7O**`AMP7k{aWcxYcn~~=( zl z2r*3s-%VgS0Ym*z!<6>|ge$8CDMtecML-QwegIIO1fkxf?z%w3wNX!us#}bjoAPf^ zqs}Pz!|`xLtfecElTh2&E^KJauz1lc@UToI(jH{ndIx(G=E4p}UmR3)5CC52(cQPu z{iXaQlH#eqk~T(~t%m$sb=|uAw(7dr%w8#NfJ>^3l$=S4>Y)#7&gEO?c>0qFWv1qD z#Lctv5|j+e`*w1i+Ougo`driOydJQF{;*ZEN*Ogf`k+F?F(vH{0UfYvK0Wf24pFEH z^~mou)PI{2JouTHKI_1+%8F#yyAeB4`b=q=C{3i;^%O?HuE~Rd0R1<0f~eg}szqHL z$6%$57Rw^m{@etk^c^z_kk6&WH`PPbNvpW^cylp(rJ}^OMPHz^>{&X?Xew(U<=WD9H9P4f=+tDnVZ|v2Au!;tTebzN*Hk3}uICmcxUOj>zN)Z=%L@b9>Gzn|LT-M*%_Umg-oFBvE$Xi1PGX*1Gs&r&n|mWh|<)F=L^1pX)YnGD-5Y7c+n z5#(rSe27LGi}Xe>1b?((z?K-5z2u4x9_Y|ZQ_^e((|nXv^BbBx-KR3}5!3Ry=&YC? z!`0C?&)3>IE8>)*axOJyEbXB2dVnHJ(;O zH2;huj4Tz}Xj^#1{5UD@j5HePZIc_eeVwU@RTp|^{Y4q~tX@hG$yG@5BwP2NQL1|@ zO}KdG!lGsWFDpz=)-aMQ^of%Gsg|CurMF{6Df5zsQ#4A@5HaPVg2ZQPZdmTmZCjXw z^gc(zXobH3^(rmEt;rPTufY5_6)9w8v03RRt8X;!f0S-t;>zUguM(Ev=2sk-VyJ|% z@MaIUOOrar?bI*}|1Co%iZcIkdN_ty-04vo@ARk+Nfl`w0hUt@)|@F(uEbuVs=?3! z^wQszeNySgwt4w{U*8?`-r;?F=5IIp!m$Nu;(Kiu_kGf47@lR_8#`AnzwSEm$)!zw z6BikVxOVZ_zLytYo|c)x7Ohy`5nF!s@>Si-`(jrtboKd`Z}QF{y_3H*HfwByhb>s% zv6}wZN|7uZsQP5Jaa^KS23TDR=&8pE68t`umfEoevfXM;zUTrQ=2e|?!TMm0~bFEJ+B%?we{ony>Nc4OT+nU|8M;74|?5MA9BhPOg; zdAvJh)T`#Z-C<*~YG!RHF*d5^fi6?b*^q7QRo%`F1^jMta6`HAfBXo+8K2pYeX z-CH*fH{MSoS~mubvv#+{_(nA!*jQ?0RZ8)vH-;~iRVb|+ZFQuhbf1Pg4{Ke!_FD2W zl|L&a5{RUOG+<$EqGO_YQ-g6rv7W!x6bCltXP%Y$s~S-)ezK`Lb0sm*JVuC*H&yc2 z#qTy@oi-O3v#O|C{W4XO6XC^_WG%@xKIs(E&E zX{M~k=ux9Z;g$lvUrgLG-Z)buS~i8mOyi1=yh+v=@$r^CTeg|%@p<9^7sS^S-P!^&q~1+@&&T@*0vJfE?b`MRs4{MY>$to z?~6%$Lm(@Id8umC&m@Fs6OV5X<+^cspnJB5MVBfy2)9nWw;dlD#FyK z;>k*WTs-lETK=;5J)Wn8|DjrbR-JnD z#vd7JR42sMM{10$Jpv!JjL-Io6L{Tqm-rm7`|lNH57!htejhGqv~A!wW8tD$m=!?lhL#yGoA5|+7=8iMJN&h+Y0q9h&(JHugFD2-dwt^kM0aFc`!oHY;a0M z!59bN)ArCnJ^YpZQ(T^xwmOB(7TA$kad!z zUdGyjwX?S-yXLBS8N3m{aajKSLy7cJj}T;WzB+~Mu}B{q1b9e)F$j>Pj~f6vG+Pwu z6VB%&he!K8!Fw%$q`&k8pW`DTeZ~WWY7PBHt)<<5`XiBKJf^i_ylNP#mx3B{Ld5i0 zgRhI@>r?nw2Hz0JH>U7&2Hylcx|Y;yDaO}^@p^p2<`jP3;BNrmLToq-F0O!?78uW)v!vR|`S}wGlfg`#E!8eY{sQM%f2Mx|3l5d)?ZJXq$wXc&SgbyM zi>-$-Gn7brt{@-0tIs8xtMetuMAml`l{;%j4~rHWFdlgtJ9Tb*vph)+H%fS>{+52M z_3G~wd;lkpA}NswMg4=+JFXeEn|x}IOZg1>&vY{CQ}3qe_mZk6NZ}#%zB%3r=D5@c zN#;Z;B&iQcC5u9Rlw?klLXrBIRI-xPCrRc_Qg})2HC15LMupm!Rueb?^(i4_U%eYt zsT3Kc_LE8$l={roXX`TAVG**6)QuFnTy}U2WifS&?2v}?fU`*~i{P39$o^e-i>0;~!g z;9Jma$Wv4{L{Xg%OZWrZm4T7WY+)wvUSf7z-7 zJjMWw4UzEq@u=)joPOBEIWBXJjl@?)Kbi2PDyd<5T*bW%%jCJA z)R#Dc-x}f!pIb~jhR~E76%@1e3#JvJxdl(*)}n9qxPS#kWZ~0F>MV(|gd1oLyjw}Z z$I!rWBENG5)mNm>(Q<*lP^W+Iq9TVf+`@X%1r>_x?Y#qVW0iz`QruOKlQ9+3FD1oJ z#0|p(pGlxErdPYnJk&4_<0PrC6ZIA+p$^vxL#pn!DMV)X6s!vJ+=)l7{A>k-~%Z(bj(?N{r6^XblJwRq5_wl zNE%A1u)xYhmf=#oGB&we+#`9-1=M-xp>JeAlIE18c;||XWH?3-(ATa|5^_$5ZI6Iy(VtTs^CEG!i z%Uh612u>y5Ba(+SAG+3asqZiwzd^z-kouOZckQh{brC-BM1uF2-f2WGLF6*?;nK92 z-etfmE@-5uqM+Y1cOD~2^aE4ryQvIDD+4}u{6mJ|6e_@~1Q6^&edAO0k@Rl_srQ;K z=rYKu#dFHk>46uSvCq|I?(TlRYDTN+H=i*x4<-*7+nB3-*i)zv8EZ1$aKJvCpiRGl z(j(4^g7U(n62`bIpc?%%krG>WNn^5&{r>byN^t_?JL%dr!33p*$G01;h1d|Et z4-~S$7y00VbC5z@=}XB}nNh?KDbiOAm_DB<;5`tx6xZkg^j;u*qkxVOu_&e%7Dx;X z=#rQ#Y_cHAHjrS`39}R$F~oe}CPx`tU#&VYVsa}Si%9DdGZLl~x4uYZ> zz-&E7bzpMl2;!-=^;||wOfGVt%1Fj^zfwZ1c+%D(r7VG*r0IWv^`l?wW~DGVW>baF z{9$M@r&>u7M;QKrkMXdA!abaC-NGWKaVoFqZ5_K$47-MEo0>zjm$c1nYKzpiG&h7= z+j?gd-Qy`9;d1V3sZTKOX^GS|=ceIyxHVMQ;H(-yD?5`K9(uT~G1T1boF8A|+*(|o zcQpl>4m=pHZfToW*A(e(Ew-#l!;)~gDZI$v+SJn86nSLiAT$_?xC|#!GI`waIQPzQ z8$4(ZHB4))3oi;aT>C%piq$0>SC@!avcxOhv&zIIv99|Ge)M(!nIE5aALPfO?woQl z$@(t4`}T4%QB3R(gUoBdyGu6JssmjP#_`uQ0?maBuUfVqYd8>9#_oQ3I b#P2_Gndry28F{LnJ3p`qO#}ZMncsf__dPx_ delta 3575 zcmZu!4RBP|6+ZX9dw27Luq$n#jg}3jK>|TT1*{sI#H6BByWQ;r#uhtCfyPdN2#8Kg z=dl4=Q^bV6cD4*tr!!TwAXw|4}UB#o|jd1GwUmVOGE>>O7V>17Pzl3Ad+eNc0Bo75llTtrr)1lQ&ko^ z2;zGUEiLh;2Nus>@ZkJ4jTLJ;no5T`ZSS|$=gm633WirNp~{|Kx?_IJ1B+?(^JQ#i z_0}=ocu_L$$9P}m42d-@#Y^;qYKSl28EHE^tGA==Y-P`^-nkv8$E9p|En;3lmr>r1 zU+=i65ktHZ>jn|_rd#Er$V*sX5vAT1t4vgQN3FG@!&_pn6_dP>yiZhlKa%}{BT#G# zc=xI@5%5;3FXMNknkWLD{c4CMwt8oRTHNpTO5x9B@m_;Jo5KH)#djKfa|(Yhi@(OWt2$GR=d+AmMqvx^E@H>| zh?yO~Zt(6D4nsb^+u$z%e~|(Sy~mI*`Q%3v+3_%(&xvHA@o^|A}Gt<*|+ zj(*FIuS7&dj(*25Ujc6$EsKq1^%kJ_q-k*5$GtK~&2Sg_w#ujUFK zw+Gd=aj=G?-BZkLOcy;QD1z`BLqyZ{M>fcG)%O!kPP>7xddOTbG8c?Yst|*uv&(oS z-_97k%=xPwgIBrHRlOAdAzA@l^qGy+)J{^b`EJ!Nim2CpDq_gpx#XTKxi@2Jz!o;C zeWb8T6=wC+8yO}^AxZ5g#XaI89U;_#3|B~@Nd1virnA(+4AYV#K*|=J$IWiXHb_#zT?`vFp-=$(enQqvLSL6P;U@I;|6kwHH%Zh!93XwOc?puP zlJE+!CU%T>#jS>Xk0uu?YS7gZ@xXU=Yy>lFn916;M%$ujgyA}dAvi`*CAhAxPXlFw zZV2OGoGLky9+(-*@^?tYkL;p90ENn_znVnfqX_Uwt3bYWu_Bh7W)#y5)|kZTT@uB$ z_^han6*F)*18`O-5%V`)*{- zThJOfSWWSz_%LHd=KF%`LsDm`Rp4M~&!N&|JHZ3?PjAC2v)BtllN# zDxnu<#7?9fBZ7!2U?iaz`OGkS7>jX|)Y){urAU~=c0&6(x|MTWhl($kqMCSVR~T(R z#IZL}hmvy3r9vA4uk<-$WOgU~)CV)Uahv_jM+fKjun(qocM)PB#g6)EnbnNyl~~+O zC3E9N3cE~jU$(K%NF&AJC@S%lxRhSUsG_TW?wj@3Wt9)8kGvhD#)(I~bECc(T%AI_ zVUaTTQD0(kcthiMTAimDbmHJ5;?FOkg z7!{l<*vl5ro3&G8k1=DTZ_Dh}O}w>?Rdc>P!^|*TK5Kkq(2783wBBsI$Gd=~-ebH~AB#wSXKB%|(1y!GV6{APGyA1sgPvQe8Sr6Ab!Yj{soREe3O6{iG+)( zg$2?B15QQE6>+jq1@?ggo0G3ks*9`<0XH+sIQoQa7pbGK6qz=pha^Xzl7aXREP=pK zbY{@|dC7y>Z8$-g^C>7dOJ0)v@%EVmIh0fipM9T~pF<5*JTLgX;W^Y76lZi7Mg8&1 z(O=4eTnu22zDBiUl;#XlrFHbRjF_B!q(TKVIZagR5-nfh=qXD5`+w};!uD}K>syuj z4~YV*ikKgUzNSz^b(vBK!&gTbuU6_aI>7bTH7sHdN;N>8_m;jbI;WL=+xGU1y*sF= zv0>4|#a>ByRo?6+4=rk%)7&q?6J3M~k@<|ollPbiM`JyLzY_hmc>`q=W z1&_nYDt??x&f~`?$=~2%g$t7hriclmHhCFj_#yQCNuF78)z#NJw|kYrVr7H6Cpqgz n$X}{YK6;~=V0A~6+wiCO-l+jh0^kfZ34E?l7dBE19k?1@ERYTgg|GQ>ZH@qW+tSaCTNu( zp&-swMA})M48sro;fy~J5sM&H6f96wKpraPjY6S71$=-H#QN>MxydbNX0zvf=dowM zJ-hebIrG{J=CU~z?$Q<_>cg)9b2$dX7-l6hCG;tnH*6$E5<{T{muJaA_ z)V>9wyytPauG#Ns?yc--Y3?oETiLO!MKrSIO0-biW>2wfQL0R0!^JYCnB|F9zg zG_fvsMeHMbgKwiF<+Pc|3?nsJk&u6^!n}>V?NAxh(Cr%fNq=;QhVJZ-eyX9n`lGuw zv|UGITH?xvbvG7Iz7vGRkGfi1)o>5*>bol@V5eXDwTk?M4)zmtKpDxg~cc~|pNO;!9 zLH1d}BEKwLBMSyyk(b}bNFv?h`H{7;tE8S}lj?L$wQ+K2Y7f~IvgvA88rjl_UniSM zW?8MIt1(x}X2DgE8P#w@s4D(H%qP1h%YtU=q#T&7SOwx!o(|bOQn?|P+U=}UG}?>v zJ87n8plYxXoXaaAV+Z`ETFniy&z{3>h%UPu{&|m1i}e!!IZ@9oS-)1#Z5h*~bu_Mp zw*}pkXnd#&swV|^f%qL_j}bGG(syhodF{C9C=lD_@-qEG{8w7d(3yT6W0{oZZO4r= zX>LoTA4oma)rY0^z-y!~kpg?>eX_C+e6jDZG%~MLyo+_Tt;<^t$BHFDan;MIdwxK9Zq|c zQ%Oh7Y7OR8awDlS{&z7re?HqMmgW~1L4TMj>C%rV3G|2$^NV6q0%h_k)_4Js?4u1_ z5~LQ49@JVAHDUu$D;Tj|_{gYi-bI>w#Q%^R+&06({{-3c8wXiS`xhMkmR1DM)}Z@< zMyQzTItx7`yOpCcLC_IV?{G~f#k=UX83keTd65WdcdR;cZ65rbOfR!SNp9WdN4L zP$=faM+hb3Y4wqUXxZqfCR--s#NgzBZHwr0PLAO-GDH0YV3$T2?^zNz%6I~kxKYNd znZ%86lSz&tjmf`j9ID7q5fqhm^L#_PlBAYqjH;) z0`;bG3u~J;`ZUWPOml;wz;pGHr-PB`+`3S8pdr$hRj|lZoTFP7)>S18i|V5Okj;Sm z!VLj`wFr!T)|yV$FE&PM1EG-EJ+@4+qN!PrkXP!S72!E`ktP0Mv~60EqD;t6!r@@J zro16o*AR@poZAoe2BNw_OdmIK%uvm`ARIvjp+NQQT7S4EP(9=!cumc?oi*dw5XL@; zuXM2qYRhx=Vf%)NLuc7Ib#*7-MIa9;zw#ZDG!wwoYsHFI3s+kqk z4`c?;{jj4>lOM{l)XY#!(WEdlOT(~g7uukNR=#wz=Z&{o*CPm~;s|qsQZBV9{RaN;b z7F3tdT{LG^Fn3i$McO@qx^s0!@zXA@K;V@NDZizao}5#)pqf^0%iu68cR!?ZL~8ac zkXHoy`YK+9P<&w=%$M#n>Ml=fZK%7P-!iRrX2Zp7D~75Pv-Kv^c`x4Tq!=&ys#NU} z;ZY@8mKdmNw2>lBZPzkHuIkWMiw3pOv098$Zs#mfpk8)%y4vAb;Zo1b4B=ACWjfv) zg5ifT%X1N^}iV(5rM?iOy%W~YsirSKt9BPKNn`$XOp!=Ui z5Tby&lRdycrX{{~AiCVkTU}2=V}p2Iwy?ed8#MLeOw;I--bOLx1Y@lOI<#Pfwq&;; zBY`OBCMwaPM2I^?G+LDp%5;EX)i5YA^l2>FLD@?Fp$?9rBPY3!o^r+p*%|MeH=zGx|2AP-;#_Tvi!~t70}f?gL5W*Pl-aWckwaEW44U6+ilrN^0+M{ zuODm~iJX8O(803oP)W(@{eI*w0KOUNR8x{GLMO>Q-5S&AlsPBKu+-D!*2rzEo;Y$N z*PqDk;3&0Pyy?GWFQBlRv@!N-vHAXnVwPmQi@=xKk#Vi9Lb*} zS)S)zSy^7-{nl8ekDKCeaB!P6Zvs>edm;@TljKDbX(Sw^b+?Tpe+pVC(ne~M@7FIR zFHx;PJ(t;-=udI}W=Z6*%%-v(s6No%jiqhJQ)DjD1Dv*&!@7p%!n#Iu%7Zf#OuoF# zlz@CwRY_j&48GPKv;@0K&C)2r5lI8ENFrBk8feC0#aQlqzE zc$Lr^fDIH#6l0{5B4zYjKWXshn#LtE?}hw}jSbALSC>*ph42v>iwYO$|NKFT%IIu& zmC+2Nc&v;D8pUH}r12CL+n*4V%2+_XH8i*MZ(6zxGhKI@diX2!4+)(L4D-bR@|XXT zrAI+|n-23fdDgsCvYQ&y9y~4@J?YOlRQ<50<0J*k=Fh8E=Z6)5TK^2CI2 zR-PE{TAWC9D?L0tPh^Rz@N0myvlGM3c_J?*kL$N3nD3QJ0TB{_o7E6m@23Iz4y diff --git a/src/test/samples/golden/widshow-debug.app b/src/test/samples/golden/widshow-debug.app index 4c8960dda5b8d3a5609651347d318c3d22d32be7..4c82121ac5b8bfd8ea38ab06b1065ea52048c3bf 100644 GIT binary patch delta 1622 zcmZ9NeN0RDG21SjV(?4dSiQiG@rV{VefBcBna}V94;U?$abARXS zz57mYUyDvfo0?mKHRAw~qESg}h(wUMasjf^`;D~f5zCohU1@!4qRBrItDczbu3K## zcK(nJki0Zi9bQTguv1fW6W$3hi%Y-P0E9fJX@yliW_yMiL=;f#gk+_osuo?fHW{K}r}l z)h=E~K(*;Jl-cmK%mHaaLYS&O6D?u1I}_zH>R_~VJ}{3_XC}&L^k60`fI*$Vo7pmo zEYzCV4GF_kdu>_-LPA_c7!=PJSkBpcGEoVm-c00V)Thxi0O}#Ot)L7mLAtbUhuUW| zdf!s^@3)a#YeIl5gG838Vv7t0NDmwK+XhPFo z9<~wr8eF!VeILQrQdiYuY_6Dp{Tn#qaT`^#=I4WZ0w+tIk-iwDy6^Fljw6h*B)Zqjr4r$K?WcmdY*oSYBYlu0>mwK1b4d2 z>W?z7WpBzP>qzs!P+T1YS?DwbUUdD}w8i&*4YAwI!1 z9>4e;@Ah2p`hvrB$KDBad*6W8j9an#Vv{?1Y?clRN z(4X+1rTztZFMOo0U}|C-rlkxaTcC)ZA*)2l`#|JElaD9O90F&&&ec-L`{836p!q{QPxE*9H_ex^sv#gG?x495 pM;ijNZzVt@zSXc!yof(H1U%(*7Rz;N8qM2V!gcfyLH`##{s-vnV^shE delta 1589 zcmZXVe@vBC7{{OUat~ZDko*w1_q~@w#One9QG+!itwhkRja_;bQk08gF!^ys!2D5g zLAUOdggtEyTdkIJwRFo7Y)+a?T>q5Lw#r>gS7QcDDYgwoH1?bLHjE&hB~6b3V^` zzURk#-uIpjjfW~W)m4_f4*>I2@~Ew$DyXh;0XQD^tEkoGctW&wKlIO!RQOYFQfGHC z6sm3M>E7FUsAHt5aAde8Z>bgeB~s~Zm_13tL%mQkH4R%jLOtCu^qF7F3{9@W)na+U zQSwh(Nr9GQqH)o8-7uUw~bLTjJRb6 zE8@rQ$RtBd;=KeV4&aZ@TUnbsyV?WYwas0v!M26X!PkHvE!m!tMk>`*gH+$9dV%W1 zSc$7CxvvhSgh8jO=k^4s9r`?FJe-mVAe}HEoN8w*TEeIy7P%PhW|SCBOk&g+i;@`y zW6@*Kr}g(RTS}QJ+7o+Vz;LR)HcbT~A*}W>NQ+L8&T6m3qGgO)Vo?U8R*l{PQ2RNS zNeWpY-8xpJ+H6Mq_3-$18=2Y@0;CrP;+(1juejayopzWqvNNNJ5rg#g_kR8Qw(=Xn`+orkyLry5CVX4p` z&^KD01^Eorer(Xfpe9+9b08}gh^5Yhe0H&S5#)x&-UP^vi@nQm8o)0gsBg(62EAF1 z&p|$qH@v24sjED-zIabVKP|R0ut85Yjh}i=aSgxpuJC^WvT9N8tN$hFZ-}~%%Y1(E z4VL?I#kaW2mnpu(9$$IdEvRu)ypHzqE`2&IHG@}ux$e8Hq96EHpracQ0ap15@B6Z= zeqPME%sIdD=s@&*66sx6_n6c&(MPZ57`#NFNDE#JtKYFPJG1W(cwni&KyHWY0CD=t zQE^)8KFB&qrG@_svL1vBDstFp^gt`fmmx}QJP&dQ+|ctV5H!M|3UVjB;k2xI2~EL+ zUEPUL-P{&>p|d^I+SZ&7)nk8WH@d|X)|VEGn;0QGgO|wuiL+$?K~Gt+@Zc7*FXG{{ mVmX)vP>L7I3dAM+p{&@qmX0M|&qHO?wz@!NDFFQ)aQP26EN1oq diff --git a/src/test/samples/golden/widshow-release.app b/src/test/samples/golden/widshow-release.app index fb05d515f2f2092f20b7c67510522ae311795840..a87ab02ef36f8fd0e764cefa07a5c88564bb89e6 100644 GIT binary patch delta 426 zcmXYs-z$T06voead1scUM5acxyo%NAhn0(6s7+j`wVGU5nrv;gAL7Nzm1Ol|^{zwO z6&K2lyU7h|HQaFJ#+_t@Ec*i}&f1>(Jm)#5^F8%Fh^C`1_l(D?0>B@upR1Qa#-Iae zsP|5;Rmn}YYN+SqtV_zu=ImkAG2q|nc+~(XZ(9$tPVs94pBsEpRXm*5IAzYH>SJr6 z2$PA$6&W(;WX3)d@o>lA;*0EY@O}*Q>gIK?QVtYVrEDz(ieZ&f7#C!TDmaGiWEC9P zMPXqOdnhZ|(M&gjgdhs@X zi-7m^(^SDN`qDm9sd>s){rKE`aCtnr6!*#g#f6|8f^{CYF%wLXi7_Y46?2>S=cCU_wOR0z%ytXj(JcE5?XthqCA&yv_E9oAEMl?~ppU$cA){&n TaJKSImV0`}cWVP^uC?$Fb{T!Z delta 435 zcmXv}O(?^07=GWs{kNIbP`>gJnJJ^$jN3wOK9bUw*=01Lt+1J)aM4g}t+w97+KrpS zX*emXIXcW~ism3?S-H75@XmPZ`KsUh)bG#}_n6EMi~a}zz8PxvDJC*|6#$aIKFVIi zzO3Z+fB7tC(o|-+%(;9%n|mX$;N4n{&G*EjZnbJU^cu3ro%t{iM>av9O2hc7ZzBMa z3r%q*a@|SY*rwg#9G+HM-Mbv7-iG*j@U}HnaHgZ7fOEZfNjz5aV`IU4(@?Y%4ce-d1Z|0V#b;0%&Vkc zaaS!9Vcxaw4= 0); + TEST_TRUE("quiet waiting", outWait(&p, "Waiting for secLink handshake", WAIT_TIMEOUT_MS)); + usleep(QUIET_WAIT_US); + TEST_TRUE("quiet nothing sent", recv(fd, &byte, 1, MSG_DONTWAIT) < 0); + close(fd); + TEST_EQ("quiet exit", proxyWait(&p), EXIT_CODE_FAIL); +} + + // The full data path: telnet negotiation answered and stripped, clean data // (including an escaped 0xFF) relayed to the terminal, terminal keystrokes // relayed to the BBS, back-pressure on the encrypted side survived, and a @@ -945,7 +970,7 @@ static void testProxyUsage(void) { } -// rs232Write's EINTR and EAGAIN handling: a blocking send interrupted by a +// rs232WriteBuf's EINTR and EAGAIN handling: a blocking send interrupted by a // signal resumes, and a non-blocking fd waits on poll(POLLOUT) instead of // truncating the frame. The writer runs in a fork so the parent can drain // the other end of the pair. @@ -982,12 +1007,12 @@ static void testSockShimBackpressure(void) { it.it_interval.tv_usec = ALARM_INTERVAL_US; it.it_value.tv_usec = ALARM_INTERVAL_US; setitimer(ITIMER_REAL, &it, 0); - rc1 = rs232Write(CLIENT_COM, (const char *)big, BIG_WRITE); + rc1 = rs232WriteBuf(CLIENT_COM, (const char *)big, BIG_WRITE); // Non-blocking fd: send returns EAGAIN and the shim waits on // poll(POLLOUT) instead of returning a partial write. flags = fcntl(sp[0], F_GETFL, 0); fcntl(sp[0], F_SETFL, flags | O_NONBLOCK); - rc2 = rs232Write(CLIENT_COM, (const char *)big, BIG_WRITE); + rc2 = rs232WriteBuf(CLIENT_COM, (const char *)big, BIG_WRITE); free(big); exit(rc1 == BIG_WRITE && rc2 == BIG_WRITE ? 0 : 1); } @@ -1026,8 +1051,8 @@ static void testSockShimEdges(void) { TEST_EQ("shim close ok", rs232Close(RS232_COM4), RS232_SUCCESS); TEST_EQ("shim read invalid", rs232Read(-1, (char *)buf, 1), RS232_ERR_NOT_OPEN); TEST_EQ("shim read unset", rs232Read(RS232_COM4, (char *)buf, 1), RS232_ERR_NOT_OPEN); - TEST_EQ("shim write invalid", rs232Write(-1, (const char *)buf, 1), RS232_ERR_NOT_OPEN); - TEST_EQ("shim write unset", rs232Write(RS232_COM4, (const char *)buf, 1), RS232_ERR_NOT_OPEN); + TEST_EQ("shim write invalid", rs232WriteBuf(-1, (const char *)buf, 1), RS232_ERR_NOT_OPEN); + TEST_EQ("shim write unset", rs232WriteBuf(RS232_COM4, (const char *)buf, 1), RS232_ERR_NOT_OPEN); // A live socket pair behind the shim. TEST_EQ("shim socketpair", socketpair(AF_UNIX, SOCK_STREAM, 0, sp), 0); sockShimSetFd(RS232_COM3, sp[0]); @@ -1036,19 +1061,20 @@ static void testSockShimEdges(void) { TEST_EQ("shim peer write", (int)send(sp[1], "AB", 2, 0), 2); TEST_EQ("shim read", rs232Read(RS232_COM3, (char *)buf, (int)sizeof(buf)), 2); TEST_MEM_EQ("shim read bytes", buf, "AB", 2); - TEST_EQ("shim write returns count", rs232Write(RS232_COM3, "xy", 2), 2); + TEST_EQ("shim write returns count", rs232WriteBuf(RS232_COM3, "xy", 2), 2); + TEST_EQ("shim nothing pending", rs232GetTxBuffered(RS232_COM3), 0); TEST_EQ("shim peer read", (int)recv(sp[1], buf, sizeof(buf), 0), 2); TEST_MEM_EQ("shim peer bytes", buf, "xy", 2); // Peer closes: FIN reads as a disconnect, writes report the dead line. close(sp[1]); TEST_EQ("shim fin", rs232Read(RS232_COM3, (char *)buf, (int)sizeof(buf)), RS232_ERR_NOT_OPEN); - TEST_EQ("shim epipe", rs232Write(RS232_COM3, "zz", 2), RS232_ERR_NOT_OPEN); + TEST_EQ("shim epipe", rs232WriteBuf(RS232_COM3, "zz", 2), RS232_ERR_NOT_OPEN); close(sp[0]); // An fd that is not a socket: recv/send fail and are reported. TEST_EQ("shim pipe", pipe(pp), 0); sockShimSetFd(RS232_COM3, pp[0]); TEST_EQ("shim notsock read", rs232Read(RS232_COM3, (char *)buf, 1), RS232_ERR_UNKNOWN); - TEST_EQ("shim notsock write", rs232Write(RS232_COM3, "q", 1), RS232_ERR_NOT_OPEN); + TEST_EQ("shim notsock write", rs232WriteBuf(RS232_COM3, "q", 1), RS232_ERR_NOT_OPEN); close(pp[0]); close(pp[1]); // Clearing the fd makes the port unusable again. @@ -1141,5 +1167,6 @@ int main(void) { testProxySigint(); testProxyPassphrase(); testProxyPassphraseMismatch(); + testProxyQuietUntilSpoken(); return testSummary(); } diff --git a/src/test/serial/testSerial.c b/src/test/serial/testSerial.c index 5dd2a385..18414065 100644 --- a/src/test/serial/testSerial.c +++ b/src/test/serial/testSerial.c @@ -21,7 +21,8 @@ // IN THE SOFTWARE. // Serial stack test suite (T8): packet (CRC-16, HDLC framing, Go-Back-N), -// security (DH group 2, XTEA-CTR, SHA-256/HMAC/HKDF, DRBG) and secLink +// security (DH group 2, XTEA-CTR, SHA-256/HMAC/HKDF, DRBG, X25519, +// XChaCha20-Poly1305, Ed25519) and secLink // (handshake, passphrases, MACs, channels, bulk transfer) exercised over // the in-process loopback shim. // @@ -39,9 +40,10 @@ #include "loopShim.h" #include "serialTest.h" -#include "packet/packet.h" -#include "security/secHash.h" -#include "security/security.h" +#include "seclink/packet.h" +#include "crypto/secCurve.h" +#include "crypto/secHash.h" +#include "crypto/security.h" #include "seclink/secLink.h" // ======================================================================== @@ -129,7 +131,12 @@ #define REENTRANT_PAYLOAD 60 // 4 frames span several 128-byte read chunks #define SHORT_WRITE_PAYLOAD 40 #define SHORT_WRITE_CAP 5 +#define STALL_CAP 0 // a port flow control holds: it takes nothing +#define RETRANSMIT_STEP_MS 600 // past the packet layer's 500 ms retransmit timeout +#define DH_STEPS_MIN 4 // secDhStep calls an exponentiation takes at least (it is sliced) +#define MS_TO_TICKS(ms) ((clock_t)(ms) * CLOCKS_PER_SEC / 1000) #define CTR_WRAP_START 0x1FFFFFFFFull +#define SILENT_STEP_CAP (2 * MS_TO_TICKS(PKT_LINK_TIMEOUT_MS) / PUMP_TICKS) // Hash vector parameters (FIPS 180, RFC 4231, RFC 5869) #define SHA_CHUNK 1000 @@ -158,24 +165,40 @@ #define SL_HELLO_VER_OFS 1 #define SL_HELLO_FLAG_OFS 2 #define SL_HELLO_PUB_OFS 3 -#define SL_HELLO_SIZE (SL_HELLO_PUB_OFS + SEC_DH_KEY_SIZE) +#define SL_HELLO_SIZE (SL_HELLO_PUB_OFS + SEC_X25519_KEY_SIZE) #define SL_CONFIRM_SIZE (1 + SEC_SHA256_SIZE) #define SL_ROLE_LOW 0x01 #define RST_MARKER_REPLY_TEST 0x01 // packet.c RST_MARKER_REPLY #define SL_BAD_VERSION 0x63 // any protocol version but ours #define SL_FIRST_DATA_SEQ 2 // packet seq after HELLO and CONFIRM #define SL_ROLE_HIGH 0x02 -#define SL_KDF_LABEL "DVX secLink v2" +#define SL_KDF_LABEL "DVX secLink v3" #define SL_KDF_LABEL_LEN ((int32_t)sizeof(SL_KDF_LABEL) - 1) -#define SL_KDF_INFO_SIZE (SL_KDF_LABEL_LEN + 2 * SEC_DH_KEY_SIZE) -#define SL_KEY_ENC_LOW 0 -#define SL_KEY_ENC_HIGH (SL_KEY_ENC_LOW + SEC_XTEA_KEY_SIZE) -#define SL_KEY_MAC_LOW (SL_KEY_ENC_HIGH + SEC_XTEA_KEY_SIZE) -#define SL_KEY_MAC_HIGH (SL_KEY_MAC_LOW + SEC_SHA256_SIZE) -#define SL_KEY_CONFIRM (SL_KEY_MAC_HIGH + SEC_SHA256_SIZE) +#define SL_KDF_INFO_SIZE (SL_KDF_LABEL_LEN + 2 * SEC_X25519_KEY_SIZE) +#define SL_KEY_LOW 0 +#define SL_KEY_HIGH (SL_KEY_LOW + SEC_AEAD_KEY_SIZE) +#define SL_KEY_CONFIRM (SL_KEY_HIGH + SEC_AEAD_KEY_SIZE) #define SL_KEY_BLOCK_SIZE (SL_KEY_CONFIRM + SEC_SHA256_SIZE) -#define SL_CTR_FRAME_SHIFT 5 -#define SL_SEQ_BYTES 8 +#define SL_NONCE_SEQ_BYTES 8 + +// Curve25519 and XChaCha20-Poly1305 vectors (RFC 7748 6.1; +// draft-irtf-cfrg-xchacha-03 A.3.1; both checked against libsodium) +#define X25519_ALICE_SECRET "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a" +#define X25519_ALICE_PUBLIC "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a" +#define X25519_BOB_SECRET "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb" +#define X25519_BOB_PUBLIC "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f" +#define X25519_SHARED "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742" +#define AEAD_PLAIN "Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it." +#define AEAD_PLAIN_LEN ((int32_t)sizeof(AEAD_PLAIN) - 1) +#define AEAD_AD "50515253c0c1c2c3c4c5c6c7" +#define AEAD_KEY "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f" +#define AEAD_NONCE "404142434445464748494a4b4c4d4e4f5051525354555657" +#define AEAD_CIPHER "bd6d179d3e83d43b9576579493c0e939572a1700252bfaccbed2902c21396cbb731c7f1b0b4aa6440bf3a82f4eda7e39ae64c6708c54c216cb96b72e1213b4522f8c9ba40db5d945b11b69b982c1bb9e3f3fac2bc369488f76b2383565d3fff921f9664c97637da9768812f615c68b13b52e" +#define AEAD_TAG "c0875924c1c7987947deafd8780acf49" +#define AEAD_MAX 128 +#define ED25519_MSG "signed by DVX" +#define ED25519_MSG_LEN ((int32_t)sizeof(ED25519_MSG) - 1) +#define X25519_JOB_STEPS 9 // eight 32-bit slices of the ladder, then the inversion // ======================================================================== @@ -222,7 +245,7 @@ typedef struct { typedef struct { PktConnT *pkt; SinkT *sink; // raw packets received from the link under test - SecDhT *dh; + uint8_t secret[SEC_X25519_KEY_SIZE]; uint8_t hello[SL_HELLO_SIZE]; uint8_t keys[SL_KEY_BLOCK_SIZE]; bool forged; // advertises a forged public key @@ -285,13 +308,15 @@ static void sinkLinkRecv(void *ctx, const uint8_t *data, int len, uint8_t ch static void sinkPktRecv(void *ctx, const uint8_t *data, int len); static void sinkPktRecvCtx(void *ctx, const uint8_t *data, int len); static void sinkReset(SinkT *sink); -static void slFrameMac(const uint8_t *macKey, uint64_t seq, const uint8_t *frame, int32_t bodyLen, uint8_t *tag); -static int32_t slKeyOfs(bool senderIsLow, bool mac); +static int32_t slKeyOfs(bool senderIsLow); +static void slNonce(uint64_t seq, uint8_t *nonce); static void testCrcLinearity(void); static void testCrcVectors(void); +static void testCurveVectors(void); static void testDhHandshake(void); static void testDhModArithmetic(void); static void testDhRejectsBadPublic(void); +static void testDhStepped(void); static void testDrbgPool(void); static void testDrbgStream(void); static void testGbnFaultKinds(void); @@ -306,7 +331,6 @@ static void testGbnSequenceWrap(void); static void testGbnWindowFull(void); static void testHashVectors(void); static void testHdlcRoundTrip(void); -static void testLinkAllocFailures(void); static void testLinkBadRemoteKey(void); static void testLinkBulkUnderFaults(void); static void testLinkChannels(void); @@ -321,28 +345,19 @@ static void testLinkPassphrases(void); static void testLinkReentrantSend(void); static void testLinkReset(void); static void testLinkSendBufPartial(void); +static void testLinkSilentStep(void); static void testPktCloseInCallback(void); +static void testPktLinkLost(void); static void testPktOpenRejects(void); static void testPktReentrantPoll(void); static void testPktResetCallback(void); static void testPktSendTimeout(void); static void testPktShortWrite(void); +static void testPktStalledPort(void); static void testXteaCounterProgression(void); static void testXteaSetCounter(void); static void testXteaVectors(void); -// Linker-interposed allocation hooks (Makefile: -Wl,--wrap). A positive -// fail count makes that many upcoming create calls return NULL, driving -// the allocation failure paths in secLink. -SecCipherT *__real_secCipherCreate(const uint8_t *key); -SecDhT *__real_secDhCreate(void); -SecCipherT *__wrap_secCipherCreate(const uint8_t *key); -SecDhT *__wrap_secDhCreate(void); - -// Allocation-failure injection counters for the __wrap_ hooks below. -static int32_t sFailCipherCreate = 0; -static int32_t sFailDhCreate = 0; - // RFC 2409 Group 2 prime, big-endian bytes. static const uint8_t sDhPrimeBytes[SEC_DH_KEY_SIZE] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC9, 0x0F, 0xDA, 0xA2, 0x21, 0x68, 0xC2, 0x34, @@ -356,28 +371,6 @@ static const uint8_t sDhPrimeBytes[SEC_DH_KEY_SIZE] = { }; -// ======================================================================== -// Allocation-failure hooks (linked via -Wl,--wrap) -// ======================================================================== - -SecCipherT *__wrap_secCipherCreate(const uint8_t *key) { - if (sFailCipherCreate > 0) { - sFailCipherCreate--; - return 0; - } - return __real_secCipherCreate(key); -} - - -SecDhT *__wrap_secDhCreate(void) { - if (sFailDhCreate > 0) { - sFailDhCreate--; - return 0; - } - return __real_secDhCreate(); -} - - // ======================================================================== // Functions (alphabetical) // ======================================================================== @@ -639,26 +632,26 @@ static bool linkPairOpen(LinkPairT *pair, int portA, int portB, SinkT *sinkA, Si } -// The manual peer's CONFIRM for the link's HELLO: DH secret, HKDF, the -// transcript hash and the role byte, exactly as the protocol specifies. +// The manual peer's CONFIRM for the link's HELLO: X25519 secret, HKDF, +// the transcript hash and the role byte, exactly as the protocol +// specifies. static bool manualPeerConfirm(ManualPeerT *mp, const uint8_t *linkHello) { - uint8_t secret[SEC_DH_KEY_SIZE]; + uint8_t secret[SEC_X25519_KEY_SIZE]; uint8_t info[SL_KDF_INFO_SIZE]; uint8_t confirm[SL_CONFIRM_SIZE]; const uint8_t *low; const uint8_t *high; - int len = SEC_DH_KEY_SIZE; - if (secDhComputeSecret(mp->dh, linkHello + SL_HELLO_PUB_OFS, SEC_DH_KEY_SIZE) != SEC_SUCCESS || secDhGetSecret(mp->dh, secret, &len) != SEC_SUCCESS) { + if (!secX25519(secret, mp->secret, linkHello + SL_HELLO_PUB_OFS)) { return false; } - mp->isLow = memcmp(mp->hello + SL_HELLO_PUB_OFS, linkHello + SL_HELLO_PUB_OFS, SEC_DH_KEY_SIZE) < 0; + mp->isLow = memcmp(mp->hello + SL_HELLO_PUB_OFS, linkHello + SL_HELLO_PUB_OFS, SEC_X25519_KEY_SIZE) < 0; low = mp->isLow ? mp->hello : linkHello; high = mp->isLow ? linkHello : mp->hello; memcpy(info, SL_KDF_LABEL, SL_KDF_LABEL_LEN); - memcpy(info + SL_KDF_LABEL_LEN, low + SL_HELLO_PUB_OFS, SEC_DH_KEY_SIZE); - memcpy(info + SL_KDF_LABEL_LEN + SEC_DH_KEY_SIZE, high + SL_HELLO_PUB_OFS, SEC_DH_KEY_SIZE); - secHkdf(0, 0, secret, len, info, SL_KDF_INFO_SIZE, mp->keys, SL_KEY_BLOCK_SIZE); + memcpy(info + SL_KDF_LABEL_LEN, low + SL_HELLO_PUB_OFS, SEC_X25519_KEY_SIZE); + memcpy(info + SL_KDF_LABEL_LEN + SEC_X25519_KEY_SIZE, high + SL_HELLO_PUB_OFS, SEC_X25519_KEY_SIZE); + secHkdf(0, 0, secret, SEC_X25519_KEY_SIZE, info, SL_KDF_INFO_SIZE, mp->keys, SL_KEY_BLOCK_SIZE); manualPeerConfirmMac(mp, low, high, mp->isLow, confirm + 1); confirm[0] = SL_TYPE_CONFIRM; return pktSend(mp->pkt, confirm, SL_CONFIRM_SIZE, false) == PKT_SUCCESS; @@ -681,19 +674,16 @@ static void manualPeerConfirmMac(const ManualPeerT *mp, const uint8_t *low, cons // An encrypted data frame from the manual peer to the link, sequence seq: -// XTEA-CTR from counter seq << SL_CTR_FRAME_SHIFT, then the truncated -// HMAC over type, channel, big-endian seq and ciphertext. +// XChaCha20-Poly1305 with the sequence nonce and the type and channel +// bytes as associated data, the tag after the ciphertext. static int32_t manualPeerFrame(const ManualPeerT *mp, uint64_t seq, uint8_t channel, const uint8_t *msg, int32_t len, uint8_t *out) { - SecCipherT *c; + uint8_t nonce[SEC_AEAD_NONCE_SIZE]; out[0] = SL_TYPE_CIPHER; out[1] = channel; memcpy(out + SECLINK_FRAME_HDR_SIZE, msg, len); - c = secCipherCreate(mp->keys + slKeyOfs(mp->isLow, false)); - secCipherSetCounter(c, seq << SL_CTR_FRAME_SHIFT); - secCipherCrypt(c, out + SECLINK_FRAME_HDR_SIZE, len); - secCipherDestroy(c); - slFrameMac(mp->keys + slKeyOfs(mp->isLow, true), seq, out, len, out + SECLINK_FRAME_HDR_SIZE + len); + slNonce(seq, nonce); + secAeadLock(mp->keys + slKeyOfs(mp->isLow), nonce, out, SECLINK_FRAME_HDR_SIZE, out + SECLINK_FRAME_HDR_SIZE, len, out + SECLINK_FRAME_HDR_SIZE + len); return SECLINK_FRAME_HDR_SIZE + len + SECLINK_MAC_SIZE; } @@ -719,22 +709,18 @@ static void manualPeerHook(void *ctx) { // A hand-built protocol peer on a raw packet connection. A non-NULL pub // replaces the advertised public key (forged-key tests). static void manualPeerInit(ManualPeerT *mp, PktConnT *pkt, SinkT *sink, const uint8_t *pub) { - int len = SEC_DH_KEY_SIZE; - - secDhDestroy(mp->dh); memset(mp, 0, sizeof(*mp)); mp->pkt = pkt; mp->sink = sink; mp->forged = pub != 0; - mp->dh = secDhCreate(); - secDhGenerateKeys(mp->dh); + secRngBytes(mp->secret, SEC_X25519_KEY_SIZE); mp->hello[0] = SL_TYPE_HELLO; mp->hello[SL_HELLO_VER_OFS] = SECLINK_PROTOCOL_VERSION; mp->hello[SL_HELLO_FLAG_OFS] = 0; if (pub) { - memcpy(mp->hello + SL_HELLO_PUB_OFS, pub, SEC_DH_KEY_SIZE); + memcpy(mp->hello + SL_HELLO_PUB_OFS, pub, SEC_X25519_KEY_SIZE); } else { - secDhGetPublicKey(mp->dh, mp->hello + SL_HELLO_PUB_OFS, &len); + secX25519PublicKey(mp->hello + SL_HELLO_PUB_OFS, mp->secret); } } @@ -958,30 +944,19 @@ static void sinkReset(SinkT *sink) { } -// Truncated frame MAC: HMAC(macKey, type || channel || seq BE || body). -static void slFrameMac(const uint8_t *macKey, uint64_t seq, const uint8_t *frame, int32_t bodyLen, uint8_t *tag) { - SecHmacT h; - uint8_t seqBytes[SL_SEQ_BYTES]; - uint8_t full[SEC_SHA256_SIZE]; - - for (int32_t i = 0; i < SL_SEQ_BYTES; i++) { - seqBytes[i] = (uint8_t)(seq >> ((SL_SEQ_BYTES - 1 - i) * 8)); - } - secHmacInit(&h, macKey, SEC_SHA256_SIZE); - secHmacUpdate(&h, frame, SECLINK_FRAME_HDR_SIZE); - secHmacUpdate(&h, seqBytes, SL_SEQ_BYTES); - secHmacUpdate(&h, frame + SECLINK_FRAME_HDR_SIZE, bodyLen); - secHmacFinal(&h, full); - memcpy(tag, full, SECLINK_MAC_SIZE); +// Offset in the HKDF key block of the sending side's key. +static int32_t slKeyOfs(bool senderIsLow) { + return senderIsLow ? SL_KEY_LOW : SL_KEY_HIGH; } -// Offset in the HKDF key block of the sending side's cipher or MAC key. -static int32_t slKeyOfs(bool senderIsLow, bool mac) { - if (mac) { - return senderIsLow ? SL_KEY_MAC_LOW : SL_KEY_MAC_HIGH; +// A frame's nonce: its sequence number, little-endian, then zeros. +static void slNonce(uint64_t seq, uint8_t *nonce) { + memset(nonce, 0, SEC_AEAD_NONCE_SIZE); + + for (int32_t i = 0; i < SL_NONCE_SEQ_BYTES; i++) { + nonce[i] = (uint8_t)(seq >> (i * 8)); } - return senderIsLow ? SL_KEY_ENC_LOW : SL_KEY_ENC_HIGH; } @@ -1063,6 +1038,109 @@ static void testCrcVectors(void) { } +// X25519 (RFC 7748's exchange; a zero public key is refused), +// XChaCha20-Poly1305 (the draft's vector; in place; a flipped bit in the +// ciphertext, the associated data, or the tag fails and leaves the data +// alone), and Ed25519 (a signature checks, a changed message or +// signature does not; the key pair wipes the seed). +static void testCurveVectors(void) { + uint8_t secretA[SEC_X25519_KEY_SIZE]; + uint8_t secretB[SEC_X25519_KEY_SIZE]; + uint8_t expect[AEAD_MAX]; + uint8_t got[SEC_X25519_KEY_SIZE]; + uint8_t zero[SEC_X25519_KEY_SIZE]; + uint8_t key[SEC_AEAD_KEY_SIZE]; + uint8_t nonce[SEC_AEAD_NONCE_SIZE]; + uint8_t ad[AEAD_MAX]; + uint8_t data[AEAD_MAX]; + uint8_t tag[SEC_AEAD_TAG_SIZE]; + uint8_t seed[SEC_ED25519_SEED_SIZE]; + uint8_t edSecret[SEC_ED25519_SECRET_SIZE]; + uint8_t edPublic[SEC_ED25519_PUBLIC_SIZE]; + uint8_t sig[SEC_ED25519_SIG_SIZE]; + uint8_t msg[] = ED25519_MSG; + int32_t adLen; + int32_t steps; + SecX25519JobT job; + + hexDecode(X25519_ALICE_SECRET, secretA); + hexDecode(X25519_BOB_SECRET, secretB); + secX25519PublicKey(got, secretA); + hexDecode(X25519_ALICE_PUBLIC, expect); + TEST_MEM_EQ("x25519 alice public", got, expect, SEC_X25519_KEY_SIZE); + secX25519PublicKey(got, secretB); + hexDecode(X25519_BOB_PUBLIC, expect); + TEST_MEM_EQ("x25519 bob public", got, expect, SEC_X25519_KEY_SIZE); + hexDecode(X25519_ALICE_PUBLIC, data); + TEST_TRUE("x25519 bob shared ok", secX25519(got, secretB, data)); + hexDecode(X25519_SHARED, expect); + TEST_MEM_EQ("x25519 bob shared", got, expect, SEC_X25519_KEY_SIZE); + hexDecode(X25519_BOB_PUBLIC, data); + TEST_TRUE("x25519 alice shared ok", secX25519(got, secretA, data)); + TEST_MEM_EQ("x25519 alice shared", got, expect, SEC_X25519_KEY_SIZE); + memset(zero, 0, sizeof(zero)); + TEST_TRUE("x25519 zero key refused", !secX25519(got, secretA, zero)); + + // The same a slice at a time: the public key, then the shared secret + secX25519JobStart(&job, secretA, NULL); + for (steps = 1; !secX25519JobStep(&job); steps++) { + } + TEST_EQ("x25519 job steps", steps, X25519_JOB_STEPS); + TEST_TRUE("x25519 job public ok", secX25519JobResult(&job, got)); + hexDecode(X25519_ALICE_PUBLIC, expect); + TEST_MEM_EQ("x25519 job public", got, expect, SEC_X25519_KEY_SIZE); + hexDecode(X25519_BOB_PUBLIC, data); + secX25519JobStart(&job, secretA, data); + while (!secX25519JobStep(&job)) { + } + TEST_TRUE("x25519 job shared ok", secX25519JobResult(&job, got)); + hexDecode(X25519_SHARED, expect); + TEST_MEM_EQ("x25519 job shared", got, expect, SEC_X25519_KEY_SIZE); + secX25519JobStart(&job, secretA, zero); + while (!secX25519JobStep(&job)) { + } + TEST_TRUE("x25519 job zero key refused", !secX25519JobResult(&job, got)); + secX25519JobStart(&job, secretA, data); + TEST_TRUE("x25519 job unfinished refused", !secX25519JobResult(&job, got)); + + hexDecode(AEAD_KEY, key); + hexDecode(AEAD_NONCE, nonce); + adLen = hexDecode(AEAD_AD, ad); + memcpy(data, AEAD_PLAIN, AEAD_PLAIN_LEN); + secAeadLock(key, nonce, ad, adLen, data, AEAD_PLAIN_LEN, tag); + hexDecode(AEAD_CIPHER, expect); + TEST_MEM_EQ("aead cipher", data, expect, AEAD_PLAIN_LEN); + hexDecode(AEAD_TAG, expect); + TEST_MEM_EQ("aead tag", tag, expect, SEC_AEAD_TAG_SIZE); + data[0] ^= 1; + TEST_TRUE("aead flipped cipher fails", !secAeadUnlock(key, nonce, ad, adLen, data, AEAD_PLAIN_LEN, tag)); + hexDecode(AEAD_CIPHER, expect); + TEST_TRUE("aead failure leaves data", data[0] == (uint8_t)(expect[0] ^ 1) && memcmp(data + 1, expect + 1, AEAD_PLAIN_LEN - 1) == 0); + data[0] ^= 1; + ad[0] ^= 1; + TEST_TRUE("aead flipped ad fails", !secAeadUnlock(key, nonce, ad, adLen, data, AEAD_PLAIN_LEN, tag)); + ad[0] ^= 1; + tag[0] ^= 1; + TEST_TRUE("aead flipped tag fails", !secAeadUnlock(key, nonce, ad, adLen, data, AEAD_PLAIN_LEN, tag)); + tag[0] ^= 1; + TEST_TRUE("aead unlock", secAeadUnlock(key, nonce, ad, adLen, data, AEAD_PLAIN_LEN, tag)); + TEST_MEM_EQ("aead plain", data, AEAD_PLAIN, AEAD_PLAIN_LEN); + + secRngBytes(seed, sizeof(seed)); + secEd25519KeyPair(edSecret, edPublic, seed); + memset(zero, 0, sizeof(zero)); + TEST_MEM_EQ("ed25519 seed wiped", seed, zero, SEC_ED25519_SEED_SIZE); + TEST_MEM_EQ("ed25519 public in secret", edSecret + SEC_ED25519_SEED_SIZE, edPublic, SEC_ED25519_PUBLIC_SIZE); + secEd25519Sign(sig, edSecret, msg, ED25519_MSG_LEN); + TEST_TRUE("ed25519 checks", secEd25519Check(sig, edPublic, msg, ED25519_MSG_LEN)); + msg[0] ^= 1; + TEST_TRUE("ed25519 changed message fails", !secEd25519Check(sig, edPublic, msg, ED25519_MSG_LEN)); + msg[0] ^= 1; + sig[0] ^= 1; + TEST_TRUE("ed25519 changed signature fails", !secEd25519Check(sig, edPublic, msg, ED25519_MSG_LEN)); +} + + // Two independent DH contexts exchanging public keys derive identical // shared secrets; a third party derives something else. static void testDhHandshake(void) { @@ -1174,6 +1252,63 @@ static void testDhRejectsBadPublic(void) { } +// The key pair and the secret computed a slice at a time agree with the +// ones made at once; nothing is ready until the last slice, and a step +// with nothing started is refused. +static void testDhStepped(void) { + SecDhT *x = secDhCreate(); + SecDhT *y = secDhCreate(); + uint8_t pubX[SEC_DH_KEY_SIZE]; + uint8_t pubY[SEC_DH_KEY_SIZE]; + uint8_t secX[SEC_DH_KEY_SIZE]; + uint8_t secY[SEC_DH_KEY_SIZE]; + int len = SEC_DH_KEY_SIZE; + int32_t steps = 0; + int rc; + + TEST_TRUE("dhstep create", x && y); + TEST_EQ("dhstep idle", secDhStep(x), SEC_ERR_NOT_READY); + TEST_EQ("dhstep start", secDhGenerateStart(x), SEC_DH_PENDING); + TEST_EQ("dhstep no key yet", secDhGetPublicKey(x, pubX, &len), SEC_ERR_NOT_READY); + + while ((rc = secDhStep(x)) == SEC_DH_PENDING) { + steps++; + } + + TEST_EQ("dhstep keys", rc, SEC_SUCCESS); + TEST_TRUE("dhstep sliced", steps >= DH_STEPS_MIN); + TEST_EQ("dhstep idle again", secDhStep(x), SEC_ERR_NOT_READY); + TEST_EQ("dhstep y at once", secDhGenerateKeys(y), SEC_SUCCESS); + len = SEC_DH_KEY_SIZE; + TEST_EQ("dhstep pub x", secDhGetPublicKey(x, pubX, &len), SEC_SUCCESS); + len = SEC_DH_KEY_SIZE; + TEST_EQ("dhstep pub y", secDhGetPublicKey(y, pubY, &len), SEC_SUCCESS); + + // x's secret a slice at a time, y's at once: the same + TEST_EQ("dhstep secret start", secDhComputeStart(x, pubY, SEC_DH_KEY_SIZE), SEC_DH_PENDING); + len = SEC_DH_KEY_SIZE; + TEST_EQ("dhstep no secret yet", secDhGetSecret(x, secX, &len), SEC_ERR_NOT_READY); + steps = 0; + + while ((rc = secDhStep(x)) == SEC_DH_PENDING) { + steps++; + } + + TEST_EQ("dhstep secret", rc, SEC_SUCCESS); + TEST_TRUE("dhstep secret sliced", steps >= DH_STEPS_MIN); + TEST_EQ("dhstep y secret", secDhComputeSecret(y, pubX, SEC_DH_KEY_SIZE), SEC_SUCCESS); + len = SEC_DH_KEY_SIZE; + TEST_EQ("dhstep get x", secDhGetSecret(x, secX, &len), SEC_SUCCESS); + len = SEC_DH_KEY_SIZE; + TEST_EQ("dhstep get y", secDhGetSecret(y, secY, &len), SEC_SUCCESS); + TEST_MEM_EQ("dhstep same secret", secX, secY, SEC_DH_KEY_SIZE); + TEST_EQ("dhstep bad key refused at once", secDhComputeStart(x, pubY, SEC_DH_KEY_SIZE - 1), SEC_ERR_PARAM); + TEST_EQ("dhstep null", secDhStep(0), SEC_ERR_PARAM); + secDhDestroy(x); + secDhDestroy(y); +} + + // Must run before anything in this process draws DRBG output (children // inherit a pool that has not auto-gathered yet). With the OS random // device unavailable the auto-gather adds nothing, which makes the @@ -1827,55 +1962,13 @@ static void testHdlcRoundTrip(void) { } -// Allocation failures inside the handshake: a failed DH context surfaces -// as SECLINK_ERR_ALLOC, a failed session cipher aborts that side's -// handshake instead of entering READY with a NULL cipher (its peer, left -// without a CONFIRM, times out), and a retry with working allocation -// recovers the link. -static void testLinkAllocFailures(void) { - SinkT sinkA; - SinkT sinkB; - LinkPairT pair; - uint8_t payload[4] = { 0xDE, 0xAD, 0xBE, 0xEF }; - - sinkReset(&sinkA); - sinkReset(&sinkB); - loopReset(); - TEST_TRUE("alloc open", linkPairOpen(&pair, PORT_A, PORT_B, &sinkA, &sinkB)); - sFailDhCreate = 1; - TEST_EQ("alloc dh fail", secLinkHandshake(pair.a), SECLINK_ERR_ALLOC); - TEST_TRUE("alloc dh not ready", !secLinkIsReady(pair.a)); - // B derives first (inside A's idle hook) and consumes the failure. - sFailCipherCreate = 1; - TEST_EQ("alloc cipher a waits out", secLinkHandshake(pair.a), SECLINK_ERR_TIMEOUT); - TEST_EQ("alloc cipher b fails", pair.bHandshakeRc, SECLINK_ERR_ALLOC); - TEST_TRUE("alloc cipher a not ready", !secLinkIsReady(pair.a)); - TEST_TRUE("alloc cipher b not ready", !secLinkIsReady(pair.b)); - TEST_EQ("alloc cipher spent", sFailCipherCreate, 0); - TEST_EQ("alloc b encrypted refused", secLinkSend(pair.b, payload, sizeof(payload), 0, true, false), SECLINK_ERR_NOT_READY); - // A retry with working allocation recovers both sides: A resumes its - // pending exchange and B's fresh HELLO restarts it. - pair.bStarted = false; - TEST_EQ("alloc retry a", secLinkHandshake(pair.a), SECLINK_SUCCESS); - TEST_EQ("alloc retry b", pair.bHandshakeRc, SECLINK_SUCCESS); - TEST_TRUE("alloc retry both ready", secLinkIsReady(pair.a) && secLinkIsReady(pair.b)); - TEST_EQ("alloc retry send", secLinkSend(pair.a, payload, sizeof(payload), TEST_CHANNEL, true, true), SECLINK_SUCCESS); - for (int32_t i = 0; i < PUMP_CAP_SHORT && sinkB.packets < 1; i++) { - secLinkPoll(pair.b); - secLinkPoll(pair.a); - } - TEST_EQ("alloc retry delivered", sinkB.packets, 1); - TEST_MEM_EQ("alloc retry bytes", sinkB.data, payload, sizeof(payload)); - linkPairClose(&pair); -} - - // A hand-built peer checks the wire protocol from outside: a forged -// (out-of-range) public key fails the handshake; a genuine exchange with -// keys derived per the specification succeeds; the link's CONFIRM and -// encrypted frames verify and decrypt with those keys; frames the peer -// builds are accepted; and replayed, tampered, injected-plaintext and -// stray handshake frames are dropped without breaking the stream. +// public key (zero, which makes every secret zero) fails the handshake; a +// genuine exchange with keys derived per the specification succeeds; the +// link's CONFIRM and encrypted frames verify and decrypt with those keys; +// frames the peer builds are accepted; and replayed, tampered, +// injected-plaintext and stray handshake frames are dropped without +// breaking the stream. static void testLinkBadRemoteKey(void) { static const uint8_t msg[] = "INTEROP-CHECK"; static const uint8_t plain[] = { SL_TYPE_PLAIN, PEER_CHANNEL, 'x' }; @@ -1884,8 +1977,8 @@ static void testLinkBadRemoteKey(void) { SecLinkT *a; PktConnT *peer; ManualPeerT mp; - SecCipherT *c; - uint8_t zeroKey[SEC_DH_KEY_SIZE]; + uint8_t zeroKey[SEC_X25519_KEY_SIZE]; + uint8_t nonce[SEC_AEAD_NONCE_SIZE]; uint8_t mac[SEC_SHA256_SIZE]; uint8_t frame[PKT_MAX_PAYLOAD]; uint8_t body[sizeof(msg)]; @@ -1908,6 +2001,10 @@ static void testLinkBadRemoteKey(void) { loopSetIdleCap(IDLE_CAP); TEST_EQ("badkey handshake fails", secLinkHandshake(a), SECLINK_ERR_HANDSHAKE); TEST_TRUE("badkey not ready", !secLinkIsReady(a)); + + // Our HELLO went out as our key was ready, just before the forged key + // failed the exchange: the peer takes it now + pktPoll(peer); TEST_EQ("badkey peer got hello", sinkPeer.packets, 1); TEST_EQ("badkey hello len", sinkPeer.packetLen[0], SL_HELLO_SIZE); TEST_EQ("badkey hello type", sinkPeer.data[0], SL_TYPE_HELLO); @@ -1922,13 +2019,17 @@ static void testLinkBadRemoteKey(void) { TEST_TRUE("interop ready", secLinkIsReady(a)); TEST_TRUE("interop peer confirmed", mp.confirmSent); loopSetIdleHook(0, 0); + + // Our CONFIRM went out as the link became ready: the peer takes it + pktPoll(peer); TEST_EQ("interop peer packets", sinkPeer.packets, 2); TEST_EQ("interop confirm len", sinkPeer.packetLen[1], SL_CONFIRM_SIZE); linkHello = sinkPeer.data; manualPeerConfirmMac(&mp, mp.isLow ? mp.hello : linkHello, mp.isLow ? linkHello : mp.hello, !mp.isLow, mac); TEST_MEM_EQ("interop link confirm", sinkPeer.data + SL_HELLO_SIZE + 1, mac, SEC_SHA256_SIZE); - // Link -> peer: verify the MAC, then decrypt, with the spec keys. + // Link -> peer: the tag checks and the frame decrypts with the spec + // keys and the nonce of sequence 0. TEST_EQ("interop link send", secLinkSend(a, msg, sizeof(msg), TEST_CHANNEL, true, true), SECLINK_SUCCESS); for (int32_t i = 0; i < PUMP_CAP_SHORT && sinkPeer.packets < 3; i++) { pktPoll(peer); @@ -1939,13 +2040,9 @@ static void testLinkBadRemoteKey(void) { wire = sinkPeer.data + SL_HELLO_SIZE + SL_CONFIRM_SIZE; TEST_EQ("interop frame type", wire[0], SL_TYPE_CIPHER); TEST_EQ("interop frame channel", wire[1], TEST_CHANNEL); - slFrameMac(mp.keys + slKeyOfs(!mp.isLow, true), 0, wire, sizeof(msg), mac); - TEST_MEM_EQ("interop frame mac", wire + SECLINK_FRAME_HDR_SIZE + sizeof(msg), mac, SECLINK_MAC_SIZE); memcpy(body, wire + SECLINK_FRAME_HDR_SIZE, sizeof(msg)); - c = secCipherCreate(mp.keys + slKeyOfs(!mp.isLow, false)); - secCipherSetCounter(c, 0); - secCipherCrypt(c, body, sizeof(msg)); - secCipherDestroy(c); + slNonce(0, nonce); + TEST_TRUE("interop frame tag", secAeadUnlock(mp.keys + slKeyOfs(!mp.isLow), nonce, wire, SECLINK_FRAME_HDR_SIZE, body, sizeof(msg), wire + SECLINK_FRAME_HDR_SIZE + sizeof(msg))); TEST_MEM_EQ("interop frame decrypts", body, msg, sizeof(msg)); // Peer -> link: a frame built by hand is accepted as encrypted. @@ -1995,7 +2092,7 @@ static void testLinkBadRemoteKey(void) { TEST_EQ("plain channel bad", secLinkSetChannelPlain(a, OUT_OF_RANGE_CHANNEL, true), SECLINK_ERR_PARAM); TEST_EQ("plain channel null", secLinkSetChannelPlain(0, 0, true), SECLINK_ERR_PARAM); TEST_EQ("dropped null", secLinkGetDropped(0), SECLINK_ERR_PARAM); - secDhDestroy(mp.dh); + secSecureZero(mp.secret, SEC_X25519_KEY_SIZE); secLinkClose(a); pktClose(peer); rs232Close(PORT_B); @@ -2045,7 +2142,9 @@ static void testLinkBulkUnderFaults(void) { TEST_EQ("bulk plain channel", secLinkSetChannelPlain(pair.a, SECLINK_MAX_CHANNEL, true), SECLINK_SUCCESS); fillPattern(bulk, BULK_BYTES, SEED_D); TEST_EQ("bulk b->a send", secLinkSendBuf(pair.b, bulk, BULK_BYTES, SECLINK_MAX_CHANNEL, false), SECLINK_SUCCESS); - for (int32_t i = 0; i < PUMP_CAP_LONG && sinkA.len < BULK_BYTES; i++) { + // Until B's window is acknowledged too (the last ACK may be one the + // faulty wire drops, so it comes with B's retransmit) + for (int32_t i = 0; i < PUMP_CAP_LONG && (sinkA.len < BULK_BYTES || secLinkGetPending(pair.b) > 0); i++) { secLinkPoll(pair.a); secLinkPoll(pair.b); loopClockAdvance(PUMP_TICKS); @@ -2164,15 +2263,16 @@ static void testLinkCloseDuringHandshake(void) { pair.b = secLinkOpen(PORT_B, BPS, DATA_BITS, PARITY, STOP_BITS, RS232_HANDSHAKE_NONE, sinkLinkRecv, &sinkB); TEST_TRUE("close-hs open", pair.a && pair.b); loopSetIdleCap(PUMP_CAP_SHORT); - // Port closed under the link: the queued key never leaves and the - // first poll sees the dead port. + // Port closed under the link: the first poll sees the dead port before + // our key is even made (it takes a slice of work a poll), so nothing + // was queued. rs232Close(PORT_A); TEST_EQ("close-hs dead port", secLinkHandshake(pair.a), SECLINK_ERR_HANDSHAKE); TEST_TRUE("close-hs not ready", !secLinkIsReady(pair.a)); pending = secLinkGetPending(pair.a); - TEST_EQ("close-hs key still queued", pending, 1); - // Reopen, fill the remaining window, and let the blocking key send - // hit the dead-line detection inside pktSend itself. + TEST_EQ("close-hs nothing queued", pending, 0); + // Reopen, fill the window, and let the exchange's polls hit the + // dead-line detection. rs232Open(PORT_A, BPS, DATA_BITS, PARITY, STOP_BITS, RS232_HANDSHAKE_NONE); for (int32_t i = pending; i < WINDOW; i++) { TEST_EQ("close-hs fill", secLinkSend(pair.a, payload, sizeof(payload), 0, false, false), SECLINK_SUCCESS); @@ -2637,6 +2737,37 @@ static void testLinkSendBufPartial(void) { } +// secLinkHandshakeStep on a line nobody answers (the far port is never +// opened, so nothing reads or acknowledges our HELLO): once the packet +// layer gives up on the silent line the step says SECLINK_ERR_TIMEOUT, not +// a failed exchange (a dead port fails at once with SECLINK_ERR_HANDSHAKE: +// testLinkCloseDuringHandshake). +static void testLinkSilentStep(void) { + SinkT sink; + SecLinkT *a; + clock_t start; + int rc = SECLINK_PENDING; + int32_t steps = 0; + + sinkReset(&sink); + loopReset(); + a = secLinkOpen(PORT_A, BPS, DATA_BITS, PARITY, STOP_BITS, RS232_HANDSHAKE_NONE, sinkLinkRecv, &sink); + TEST_TRUE("silent open", a != 0); + start = clock(); + + while (rc == SECLINK_PENDING && steps < SILENT_STEP_CAP) { + rc = secLinkHandshakeStep(a); + loopClockAdvance(PUMP_TICKS); + steps++; + } + + TEST_EQ("silent step times out", rc, SECLINK_ERR_TIMEOUT); + TEST_TRUE("silent step waited out the line", clock() - start >= MS_TO_TICKS(PKT_LINK_TIMEOUT_MS)); + TEST_TRUE("silent not ready", !secLinkIsReady(a)); + secLinkClose(a); +} + + // pktClose from inside the receive callback: the connection is freed // when the outermost pktPoll unwinds (no use after free), pktPoll reports // the disconnect, and the COM slot is free for a new connection. @@ -2668,6 +2799,40 @@ static void testPktCloseInCallback(void) { } +// A peer that says nothing: retransmits go on for a while, then the link +// is reported lost; one that answers never is. +static void testPktLinkLost(void) { + SinkT sinkA; + SinkT sinkB; + PktPairT pair; + uint8_t payload[3] = { 1, 2, 3 }; + int rc = 0; + + sinkReset(&sinkA); + sinkReset(&sinkB); + pktPairOpen(&pair, WINDOW, &sinkA, &sinkB); + TEST_EQ("lost send", pktSend(pair.a, payload, sizeof(payload), false), PKT_SUCCESS); + + for (int32_t ms = 0; ms < PKT_LINK_TIMEOUT_MS - RETRANSMIT_STEP_MS && rc >= 0; ms += RETRANSMIT_STEP_MS) { + loopClockAdvance(MS_TO_TICKS(RETRANSMIT_STEP_MS)); + rc = pktPoll(pair.a); + } + + TEST_TRUE("lost not yet", rc >= 0); + loopClockAdvance(MS_TO_TICKS(RETRANSMIT_STEP_MS * 2)); + TEST_EQ("lost reported", pktPoll(pair.a), PKT_ERR_DISCONNECTED); + + // The peer answers (late, but well within the limit): it was alive + pktPoll(pair.b); + TEST_EQ("lost delivered once", sinkB.packets, 1); + pktPoll(pair.a); + TEST_EQ("lost acked", pktGetPending(pair.a), 0); + loopClockAdvance(MS_TO_TICKS(PKT_LINK_TIMEOUT_MS * 2)); + TEST_TRUE("idle link kept", pktPoll(pair.a) >= 0); + pktPairClose(&pair); +} + + // pktOpen registry: bad port, duplicate open, window clamping. static void testPktOpenRejects(void) { SinkT sink; @@ -2788,7 +2953,7 @@ static void testPktSendTimeout(void) { // A port that accepts only a few bytes per write (flow control) still -// gets whole frames: sendFrame keeps writing the remainder. +// gets whole frames: the queue keeps writing the remainder. static void testPktShortWrite(void) { SinkT sinkA; SinkT sinkB; @@ -2808,6 +2973,41 @@ static void testPktShortWrite(void) { } +// A port flow control holds (it takes nothing): sending and polling never +// wait for it (the test clock does not move by itself), the window fills, +// and once the line is free every frame goes, once. +static void testPktStalledPort(void) { + SinkT sinkA; + SinkT sinkB; + PktPairT pair; + uint8_t payload[SHORT_WRITE_PAYLOAD]; + + fillPattern(payload, sizeof(payload), SEED_D); + sinkReset(&sinkA); + sinkReset(&sinkB); + pktPairOpen(&pair, WINDOW, &sinkA, &sinkB); + loopSetWriteCap(PORT_A, STALL_CAP); + + for (int32_t i = 0; i < WINDOW; i++) { + TEST_EQ("stall send", pktSend(pair.a, payload, sizeof(payload), false), PKT_SUCCESS); + } + + TEST_TRUE("stall window full", !pktCanSend(pair.a)); + loopClockAdvance(MS_TO_TICKS(RETRANSMIT_STEP_MS * 4)); + TEST_TRUE("stall poll returns", pktPoll(pair.a) >= 0); + pktPoll(pair.b); + TEST_EQ("stall nothing yet", sinkB.packets, 0); + + loopSetWriteCap(PORT_A, LOOP_CAP_NONE); + pktPoll(pair.a); + pktPoll(pair.b); + TEST_EQ("stall all delivered", sinkB.packets, WINDOW); + pktPoll(pair.a); + TEST_EQ("stall acked", pktGetPending(pair.a), 0); + pktPairClose(&pair); +} + + // Counter progression: each secCipherCrypt call starts on a block boundary // and the counter advances one per block (or partial block) consumed. static void testXteaCounterProgression(void) { @@ -2977,17 +3177,20 @@ int main(void) { testPktSendTimeout(); testPktResetCallback(); testPktShortWrite(); + testPktStalledPort(); + testPktLinkLost(); testXteaVectors(); testXteaCounterProgression(); testXteaSetCounter(); testDhHandshake(); + testDhStepped(); testDhRejectsBadPublic(); testDhModArithmetic(); + testCurveVectors(); testLinkNotReady(); testLinkChannels(); testLinkEncryptFlag(); testLinkBulkUnderFaults(); - testLinkAllocFailures(); testLinkBadRemoteKey(); testLinkPassphrases(); testLinkEarlyHello(); @@ -2998,5 +3201,6 @@ int main(void) { testLinkOpenConflict(); testLinkReentrantSend(); testLinkSendBufPartial(); + testLinkSilentStep(); return testSummary(); } diff --git a/src/test/setup/Makefile b/src/test/setup/Makefile index d18c5e91..06a9b900 100644 --- a/src/test/setup/Makefile +++ b/src/test/setup/Makefile @@ -32,8 +32,8 @@ include ../dvxtest/dvxtest.mk SETUP_DIR = $(DVXTEST_ROOT)/src/installer PKG_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/pkg MINIZ_DIR = $(PKG_DIR)/thirdparty/miniz -MONO_DIR = $(PKG_DIR)/thirdparty/monocypher/src -SEC_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/serial/security +MONO_DIR = $(SEC_DIR)/thirdparty/monocypher/src +SEC_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/crypto FILEOPS_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/fileops SHELLDIR = $(DVXTEST_ROOT)/src/libs/kpunch/dvxshell SHELLTESTDIR = $(DVXTEST_ROOT)/src/test/shell @@ -41,7 +41,7 @@ APPS_SO_DIR = $(DVXTEST_OBJDIR)/apps BINDIR = $(DVXTEST_BINDIR)/setup FILEOPS_OBJS = $(patsubst $(FILEOPS_DIR)/%.c,$(BINDIR)/fileops/%.o,$(wildcard $(FILEOPS_DIR)/*.c)) SHELL_OBJS = $(patsubst $(SHELLDIR)/%.c,$(BINDIR)/shell/%.o,$(wildcard $(SHELLDIR)/*.c)) -CORE_OBJ = $(patsubst %.c,$(BINDIR)/installer/%.o,setupCore.c pkgRead.c pkgBuild.c miniz.c pkgDb.c pkgDpk.c pkgFormat.c pkgIni.c pkgManifest.c pkgOp.c pkgPlan.c pkgSign.c pkgTrust.c pkgZip.c pkgZipWrite.c monocypher.c monocypher-ed25519.c secHash.c security.c) +CORE_OBJ = $(patsubst %.c,$(BINDIR)/installer/%.o,setupCore.c pkgRead.c pkgBuild.c miniz.c pkgDb.c pkgDpk.c pkgFormat.c pkgIni.c pkgManifest.c pkgOp.c pkgPlan.c pkgSign.c pkgSignMake.c pkgTrust.c pkgZip.c pkgZipWrite.c monocypher.c monocypher-ed25519.c secHash.c secSign.c security.c) SHFIX_OBJ = $(BINDIR)/shellFixture.o SETUP_TEST = $(BINDIR)/setupTest OBJS = $(CORE_OBJ) $(FILEOPS_OBJS) $(SHELL_OBJS) $(SHFIX_OBJ) diff --git a/src/test/setup/setupTest.c b/src/test/setup/setupTest.c index 985fd831..7570dfe9 100644 --- a/src/test/setup/setupTest.c +++ b/src/test/setup/setupTest.c @@ -72,6 +72,7 @@ #define HELP_SYS_DIR "APPS/KPUNCH/PROGMAN" #define FAKE_EXE "fake program bytes" #define BIG_FILE_BYTES 9000 +#define ZIP_SCAN_MAX 65536 // zipHas: the test packages' size at most #define ADDON_FONT "font" // an add-on's one file's bytes #define ADDON_SEED 7 // its key's seed bytes #define TREE_FILES 6 @@ -201,6 +202,7 @@ static void caseSplitSwap(void); static void caseTiers(void); static void caseUpgradeUninstall(void); static void caseWelcomeApp(void); +static void caseZipCase(void); static void clearDrive(const char *drive); static bool exists(const char *path); static bool fileIs(const char *path, const char *text); @@ -217,6 +219,7 @@ static bool treeMatches(const char *dest); static void treeText(int32_t i, char *out, int32_t outSize); static bool welcomeKey(char *out, int32_t outSize); static void writeText(const char *path, const char *text); +static bool zipHas(const char *zip, const char *name); // Write the package tree under root. @@ -545,8 +548,8 @@ static void caseCrc(void) { char *hit = NULL; for (int32_t i = 0; data && i + PKG_ZIP_LOCAL_SIZE < len && !hit; i++) { - if (pkgGet32((uint8_t *)data + i) == PKG_ZIP_LOCAL_SIG && memcmp(data + i + PKG_ZIP_LOCAL_SIZE, name, strlen(name)) == 0) { - hit = data + i + PKG_ZIP_LOCAL_SIZE + strlen(name) + pkgGet16((uint8_t *)data + i + ZIP_LH_EXTRA_LEN) + pkgGet32((uint8_t *)data + i + ZIP_LH_PACKED) / 2; + if (dvxGetLe32((uint8_t *)data + i) == PKG_ZIP_LOCAL_SIG && memcmp(data + i + PKG_ZIP_LOCAL_SIZE, name, strlen(name)) == 0) { + hit = data + i + PKG_ZIP_LOCAL_SIZE + strlen(name) + dvxGetLe16((uint8_t *)data + i + ZIP_LH_EXTRA_LEN) + dvxGetLe32((uint8_t *)data + i + ZIP_LH_PACKED) / 2; } } @@ -1647,12 +1650,13 @@ static void caseWelcomeApp(void) { int32_t before = ctx->stack.count; tWidgetClick(tFindByText(win->widgetRoot, "Read the User Guide")); - fixPumpShell(SETTLE_FRAMES); + fixPumpSettled(); TEST_TRUE(ctx->stack.count > before); fixDialogsOn("OK"); fixDialogs = 0; ctx->currentAppId = id; win->onMenu(win, fixMenuId(win, MENU_ABOUT)); + fixPumpSettled(); TEST_EQ(fixDialogs, 1); fixDialogsOff(); ctx->currentAppId = id; @@ -1684,6 +1688,39 @@ static void caseWelcomeApp(void) { } +// A tree with a folder spelled two ways (system, and SYSTEM where the +// package tool wrote DVX's spelling into a tree copied from Linux) packs +// as one folder, the lowercase spelling: DOS sees one, and so does an +// unzip on Linux. Two files only case apart are refused. +static void caseZipCase(void) { + tCase("package of a tree whose folders differ only in case"); + + char tree[PATH_MAX_T]; + char zip[PATH_MAX_T]; + char path[PATH_MAX_T]; + char err[PKG_BUILD_ERR_MAX]; + + tp(tree, "caseTree"); + snprintf(path, sizeof(path), "%s/system", tree); + TEST_EQ(platformMkdirRecursive(path), 0); + snprintf(path, sizeof(path), "%s/SYSTEM/PACKAGES", tree); + TEST_EQ(platformMkdirRecursive(path), 0); + snprintf(path, sizeof(path), "%s/system/A.INI", tree); + writeText(path, "a"); + snprintf(path, sizeof(path), "%s/SYSTEM/PACKAGES/B.INI", tree); + writeText(path, "b"); + tp(zip, "case.zip"); + TEST_TRUE(pkgBuildZip(tree, zip, NULL, 0, err, sizeof(err))); + TEST_TRUE(zipHas(zip, "system/A.INI")); + TEST_TRUE(zipHas(zip, "system/PACKAGES/B.INI")); + TEST_TRUE(!zipHas(zip, "SYSTEM/")); + + snprintf(path, sizeof(path), "%s/SYSTEM/a.ini", tree); + writeText(path, "c"); + TEST_TRUE(!pkgBuildZip(tree, zip, NULL, 0, err, sizeof(err))); + TEST_TRUE(strstr(err, "differ only in case") != NULL); +} + // Take every part out of the drive. static void clearDrive(const char *drive) { @@ -1929,6 +1966,21 @@ static void writeText(const char *path, const char *text) { } +// True when the package file zip names name (a ZIP holds its entries' +// names as they are). +static bool zipHas(const char *zip, const char *name) { + static char buf[ZIP_SCAN_MAX]; + FILE *f = fopen(zip, "rb"); + size_t len = f ? fread(buf, 1, sizeof(buf), f) : 0; + + if (f) { + fclose(f); + } + + return memmem(buf, len, name, strlen(name)) != NULL; +} + + int main(void) { tSuiteBegin("setup"); @@ -1946,6 +1998,7 @@ int main(void) { caseSkip(); caseCrc(); caseEmptyLists(); + caseZipCase(); caseCancel(); caseUpgradeUninstall(); caseReadOnly(); diff --git a/src/test/shell/Makefile b/src/test/shell/Makefile index 4b409d43..06ae2b50 100644 --- a/src/test/shell/Makefile +++ b/src/test/shell/Makefile @@ -47,7 +47,7 @@ FIX_SOS = $(patsubst fixtures/%.c,$(FIXDIR)/%.so,$(FIX_SRCS)) FIX_HLP = $(FIXDIR)/fixture.hlp HLPC = $(DVXTEST_ROOT)/bin/host/dvxhlpc HELP_FIX = $(DVXTEST_ROOT)/src/test/tools/fixtures/help -SHELL_SRCS = $(SHELLDIR)/shellApp.c $(SHELLDIR)/shellBas.c $(SHELLDIR)/shellSysBas.c $(SHELLDIR)/shellMain.c $(SHELLDIR)/shellInfo.c $(SHELLDIR)/shellAssoc.c $(SHELLDIR)/shellProg.c $(SHELLDIR)/shellRun.c $(SHELLDIR)/shellSaver.c $(SHELLDIR)/pmModel.c $(SHELLDIR)/pmModelBas.c +SHELL_SRCS = $(SHELLDIR)/shellApp.c $(SHELLDIR)/shellBas.c $(SHELLDIR)/shellSysBas.c $(SHELLDIR)/shellMain.c $(SHELLDIR)/shellInfo.c $(SHELLDIR)/shellAssoc.c $(SHELLDIR)/shellProg.c $(SHELLDIR)/shellProgCache.c $(SHELLDIR)/shellRun.c $(SHELLDIR)/shellSaver.c $(SHELLDIR)/pmModel.c $(SHELLDIR)/pmModelBas.c SHELL_OBJS = $(patsubst $(SHELLDIR)/%.c,$(BINDIR)/%.o,$(SHELL_SRCS)) SHELL_BINS = $(SHELL_TEST) $(APPS_TEST) $(SHMAIN_TEST) $(PM_TEST) SHFIX_OBJ = $(BINDIR)/shellFixture.o @@ -113,7 +113,9 @@ $(SHFIX_BINS): EXTRA_OBJS = $(SHFIX_OBJ) $(SHFIX_BINS): $(SHFIX_OBJ) -$(APPS_TEST): EXTRA_OBJS = $(SHFIX_OBJ) $(FILEOPS_OBJS) $(JOY_OBJS) $(AUDIO_OBJS) $(GAME_OBJ) $(HLPC_OBJ) -Wl,--whole-archive $(DVXTEST_BASRT_A) -Wl,--no-whole-archive +# appsTest counts the pictures the shell decodes (and can make each slow): +# Program Manager's icon pacing case wraps dvxLoadImageFromMemory. +$(APPS_TEST): EXTRA_OBJS = $(SHFIX_OBJ) $(FILEOPS_OBJS) $(JOY_OBJS) $(AUDIO_OBJS) $(GAME_OBJ) $(HLPC_OBJ) -Wl,--whole-archive $(DVXTEST_BASRT_A) -Wl,--no-whole-archive -Wl,--wrap=dvxLoadImageFromMemory $(APPS_TEST): $(FILEOPS_OBJS) $(JOY_OBJS) $(AUDIO_OBJS) $(GAME_OBJ) $(HLPC_OBJ) $(DVXTEST_BASRT_A) $(DVXTEST_BASSTUB_SO) $(JOYKEYS_DRV) | dvxtest-bascomp $(JOYKEYS_DRV): $(JOY_DIR)/drivers/joykeys/joyKeys.c $(DVX_MAKEFILES) diff --git a/src/test/shell/appsTest.c b/src/test/shell/appsTest.c index f26f72d8..6cf2aaa8 100644 --- a/src/test/shell/appsTest.c +++ b/src/test/shell/appsTest.c @@ -191,6 +191,11 @@ #define PM_CMD_AUTO_ARRANGE 410 #define PM_CMD_ARRANGE 411 #define PM_SCAN_K 0x25 // BIOS scancode of K (Alt+K / Ctrl+Alt+K arrive with ascii 0) +#define PM_PACE_ITEMS 4 // the items of caseProgmanIconPacing's group (its INI says so too) +#define PM_PACE_ICON_DIR "apps/kpunch/dvxdemo" // their pictures, one each (logo, new, open, save) +#define PM_PACE_LOAD_MS 50 // a picture load there: more than a paint's icon budget +#define PM_PACE_FRAME_MS 10 // what a frame takes there besides +#define PM_PACE_FRAMES 200 // frames it waits for every icon (or the other program's end) at most // Progman widgets (wgtFind names), texts and files (progman.c, pmView.c) #define PM_NAME_GROUPS "groups" @@ -219,6 +224,10 @@ #define NOTEPAD_TITLE NOTEPAD_DOC " - Notepad" #define NOTEPAD_EDITOR "Editor" #define NOTEPAD_WAIT_FRAMES 200 +#define NOTEPAD_MENU_SAVE "&Save" +#define NOTEPAD_BIG_BYTES 40000 // more than two of Notepad's save pieces (SAVE_CHUNK_BYTES) +#define NOTEPAD_BIG_LINE 64 // its lines' length, the line end included +#define NOTEPAD_LETTERS 26 #define FM_APP "apps/kpunch/fileMgr/fileMgr.app" #define FM_DBP "src/apps/kpunch/fileMgr/fileMgr.dbp" #define FM_LIST "fmList" @@ -250,6 +259,16 @@ #define PARDEMO_ASCII_CTRL_N 0x0E // what the keyboard gives for Ctrl+N #define PARDEMO_WAIT_SECS 40 // real seconds before giving up #define PARDEMO_FRAME_MS 50 // test clock step per frame (the fixture's Timer is 200 ms) +#define SHELLRUN_DIR "src/test/dos/fixtures/shellrun" +#define SHELLRUN_DBP SHELLRUN_DIR "/shellrun.dbp" +#define SHELLRUN_APP "apps/test/shellrun.app" +#define SHELLRUN_TITLE "Shell Run" +#define SHELLRUN_LABEL "lblResult" +#define SHELLRUN_RESULT "SHELL returned " // the label once SHELL has come back +#define SHELLRUN_WAIT_LABEL "lblWait" +#define SHELLRUN_WAIT_RESULT "ShellRunDosWait returned " +#define SHELLRUN_FRAMES 400 +#define SHELLRUN_FRAME_MS 50 // test clock step per frame (the fixture's Timer is 100 ms) #define PARHLP_DIR "src/test/dos/fixtures/parhlp" #define PARHLP_DBP PARHLP_DIR "/parhlp.dbp" #define PARHLP_DHS PARHLP_DIR "/parhlp.dhs" @@ -376,6 +395,12 @@ const char *PrefsGet(int32_t handle, const char *section, const char *key, c int32_t PrefsGetInt(int32_t handle, const char *section, const char *key, int32_t defaultValue); void PrefsSet(int32_t handle, const char *section, const char *key, const char *value); +// The picture decoder, wrapped (-Wl,--wrap=dvxLoadImageFromMemory): the +// shell's decodes (every picture it shows, from the program cache's bytes) +// are counted, and each can be made to take a while +uint8_t *__real_dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH, int32_t *outPitch); +uint8_t *__wrap_dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH, int32_t *outPitch); + static void assocShipped(const char *store); static void caseClockTick(void); static void caseCpanelCancelKeepsDblClick(void); @@ -414,6 +439,7 @@ static void caseParhlp(void); static void caseParpm(void); static void caseProgmanEmpty(void); static void caseProgmanHotkeysGlobal(void); +static void caseProgmanIconPacing(void); static void caseProgmanInstalledChange(void); static void caseProgmanItems(void); static void caseProgmanMenus(void); @@ -424,6 +450,7 @@ static void caseScreenSaver(void); static void caseShellBasBindings(void); static void caseShellInfo(void); static void caseShellRunningBas(void); +static void caseShellWaitsOwnTask(void); static void caseSysInputBas(void); static void caseTempCopyEdges(void); static WidgetT *clickButton(WindowT *win, const char *caption); @@ -444,9 +471,11 @@ static bool iniHas(const char *section, const char *key, const char *value); static int32_t loadApp(const char *path); static int32_t logCount(const char *text); static bool menuEnabled(WindowT *win, int32_t id); +static void pacerTask(void *arg); static int32_t parpmItemCell(WidgetT *icons, const char *method, int32_t index); static void pmBrowse(const char *label); static void pmField(const char *label, const char *text); +static int32_t pmPaceRun(bool *adjacent); static void pmScriptRun(AppContextT *ctx, WindowT *modal, void *userCtx); static void pmScriptStart(void); static void pmStep(const char *button); @@ -487,6 +516,16 @@ static int32_t sF1Step = 0; // f1DuringIndex progress static int32_t sSysKeys[SYS_KEYS_MAX]; // keys caseSysInputBas's window got static int32_t sSysMods[SYS_KEYS_MAX]; static int32_t sSysKeyCount = 0; +static int32_t sImageLoads = 0; // pictures the shell decoded (__wrap_dvxLoadImageFromMemory) +static int32_t sImageLoadMs = 0; // the fake clock moves on this much for each +static bool sPacerRuns = false; // pacerTask goes on taking its turns + + +uint8_t *__wrap_dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH, int32_t *outPitch) { + sImageLoads++; + hostClockAdvance(sImageLoadMs); + return __real_dvxLoadImageFromMemory(ctx, data, dataLen, outW, outH, outPitch); +} // The shipped file types (SYSTEM\ASSOC.INI) are store, the user has @@ -1901,6 +1940,41 @@ static void caseDocumentOpensNotepad(void) { TEST_TRUE(np != NULL && editor != NULL); TEST_STR_EQ(editor ? wgtGetText(editor) : "", NOTEPAD_DOC_TEXT); + // Saving writes the text in pieces: a document of several comes + // back whole, with the one line end PRINT adds after it + if (pass == 0 && np && editor) { + char *big = (char *)malloc(NOTEPAD_BIG_BYTES + 1); + + for (int32_t i = 0; i < NOTEPAD_BIG_BYTES; i++) { + big[i] = (i % NOTEPAD_BIG_LINE == NOTEPAD_BIG_LINE - 1) ? '\n' : (char)('a' + i % NOTEPAD_LETTERS); + } + + big[NOTEPAD_BIG_BYTES] = '\0'; + wgtSetText(editor, big); + np->onMenu(np, fixMenuId(np, NOTEPAD_MENU_SAVE)); + fixPumpSettled(); + + const char *shown = wgtGetText(editor); + int32_t want = (int32_t)strlen(shown); + int32_t len = 0; + char *back = platformReadFile(NOTEPAD_DOC, &len); + + while (want > 0 && (shown[want - 1] == '\n' || shown[want - 1] == '\r')) { + want--; + } + + while (back && len > 0 && (back[len - 1] == '\n' || back[len - 1] == '\r')) { + len--; + } + + TEST_TRUE(want > NOTEPAD_BIG_BYTES - NOTEPAD_BIG_LINE); + TEST_EQ(len, want); + TEST_TRUE(back != NULL && strncmp(back, shown, (size_t)want) == 0); + free(back); + free(big); + TEST_TRUE(writeBytes(NOTEPAD_DOC, NOTEPAD_DOC_TEXT, (int32_t)strlen(NOTEPAD_DOC_TEXT))); + } + // Closed like a user would, so the programs end and free their // runtimes (killing their tasks would not). TEST_TRUE(np && fixCloseApp(np)); @@ -1982,6 +2056,39 @@ static void caseDosPrograms(void) { TEST_PTR_EQ(shellGetApp(clk), NULL); TEST_TRUE(shellGetApp(pm) != NULL); + tCase("a program waiting for its DOS run (SHELL) stays, and hears how it ended"); + int32_t waiter = loadApp(APP_CLOCK); + int32_t rc = 0; + + fixPumpShell(TASK_FRAMES); + unlink(DOS_RAN); + fixDialogs = 0; + shellRunDosFor(waiter, DOS_PROG, "z", NULL, false); + TEST_TRUE(!shellRunDosDone(waiter, &rc)); + shellRunDosRequest(ctx, pm); + TEST_EQ(fixDialogs, 0); + TEST_TRUE(dosRan(DOS_RAN, "ran z\n")); + TEST_TRUE(shellRunDosDone(waiter, &rc)); + TEST_EQ(rc, 0); + TEST_TRUE(shellGetApp(waiter) != NULL); + + // Replaced by another request, a program that is not there, or none: + // over, not run + shellRunDosFor(waiter, DOS_PROG, "q", NULL, false); + shellRunDos(DOS_PROG, "r", NULL, false); + TEST_TRUE(shellRunDosDone(waiter, &rc)); + TEST_EQ(rc, -1); + shellRunDosRequest(ctx, pm); + TEST_TRUE(dosRan(DOS_RAN, "ran r\n")); + fixDialogsOn("OK"); + shellRunDosFor(waiter, DOS_MISSING, "", NULL, false); + shellRunDosRequest(ctx, pm); + TEST_TRUE(shellRunDosDone(waiter, &rc)); + TEST_EQ(rc, -1); + shellRunDosFor(waiter, "", "", NULL, false); + TEST_TRUE(shellRunDosDone(waiter, &rc)); + TEST_EQ(rc, -1); + tCase("a missing DOS program is said; shellRunProgram and associations"); sDosHooks[0] = '\0'; fixDialogsOn("OK"); @@ -2961,15 +3068,17 @@ static void caseParpm(void) { int32_t x1; int32_t y1; + // The control's code runs on the program's task: each drop + // is in once the program has had its turn pmViewCellPoint(icons, 0, 0, &x0, &y0); pmViewCellPoint(icons, freeCol, freeRow, &x1, &y1); tDrag(x0, y0, x1, y1); - fixPumpShell(1); + fixPumpSettled(); TEST_EQ(parpmItemCell(icons, "ItemCol", 0), freeCol); TEST_EQ(parpmItemCell(icons, "ItemRow", 0), freeRow); pmViewCellPoint(icons, 1, 0, &x0, &y0); tDrag(x0, y0, x1, y1); - fixPumpShell(1); + fixPumpSettled(); TEST_EQ(parpmItemCell(icons, "ItemCol", 0), 1); TEST_EQ(parpmItemCell(icons, "ItemRow", 0), 0); TEST_EQ(parpmItemCell(icons, "ItemCol", 1), freeCol); @@ -3065,8 +3174,56 @@ static void caseProgmanHotkeysGlobal(void) { } +// Program Manager decodes its icons a paint at a time (one a paint when a +// decode is slow; its Main group shows first, each item with a picture of +// its own, kept by the program cache after the first decode and decoded +// again by a fresh Program Manager). While another program runs, a frame that decoded is +// followed by as long a rest, so the shell's run of turns stays short and +// no two frames in a row decode; alone it decodes every frame. +static void caseProgmanIconPacing(void) { + static const char ini[] = "[options]\ngroup = Main\n[groups]\ncount = 1\n[group1]\nname = Main\ncount = 4\nitem1.name = A\nitem1.program = " APP_CLOCK "\nitem1.icon = " PM_PACE_ICON_DIR "/logo.bmp\nitem2.name = B\nitem2.program = " APP_CLOCK "\nitem2.icon = " PM_PACE_ICON_DIR "/new.bmp\nitem3.name = C\nitem3.program = " APP_CLOCK "\nitem3.icon = " PM_PACE_ICON_DIR "/open.bmp\nitem4.name = D\nitem4.program = " APP_CLOCK "\nitem4.icon = " PM_PACE_ICON_DIR "/save.bmp\n"; + + bool adjacent = false; + int32_t id = 0; + + tCase("progman rests between icon decodes while programs run"); + fixShellUp(SCREEN_BPP); + TEST_EQ(platformMkdirRecursive(PM_INI_DIR), 0); + TEST_TRUE(writeBytes(PM_INI, ini, (int32_t)strlen(ini))); + sImageLoadMs = PM_PACE_LOAD_MS; + sPacerRuns = true; + TEST_TRUE(tsCreate("pacer", pacerTask, NULL, 0, TS_PRIORITY_NORMAL) > 0); + sImageLoads = 0; + progmanUp(&id); + pmPaceRun(&adjacent); + TEST_EQ(sImageLoads, PM_PACE_ITEMS); + TEST_TRUE(!adjacent); + + // The other program ends: a fresh Program Manager decodes every frame + // (the frames that opened it decoded the first). + sPacerRuns = false; + fixKillAllApps(); + + for (int32_t i = 0; i < PM_PACE_FRAMES && tsActiveCount() > 1; i++) { + fixPumpShell(1); + } + + TEST_EQ((int64_t)tsActiveCount(), 1); + sImageLoads = 0; + progmanUp(&id); + TEST_TRUE(pmPaceRun(&adjacent) < PM_PACE_ITEMS); + TEST_EQ(sImageLoads, PM_PACE_ITEMS); + TEST_TRUE(adjacent); + + sImageLoadMs = 0; + fixKillAllApps(); + fixShellDown(); +} + + // Programs installed or removed while DVX runs: Program Manager notices -// when its window comes back to the front. +// when its window comes back to the front after the change was announced +// (platformInstalledChanged). static void caseProgmanInstalledChange(void) { tCase("progman notices installed and removed programs"); fixShellUp(SCREEN_BPP); @@ -3079,21 +3236,30 @@ static void caseProgmanInstalledChange(void) { TEST_TRUE(items != NULL && search != NULL && win->onFocus != NULL); if (items && search && win->onFocus) { + // Copied in without a word: not looked for when it comes back TEST_EQ(platformMkdirRecursive("apps/zzz"), 0); TEST_TRUE(fixCopyFileTo(FIXTURE("fixNoMain.so"), PM_NEWBIE_APP)); win->onFocus(win); wgtSetText(search, "newbie"); search->onChange(search); + TEST_STR_EQ(wgtGetText(items), PM_EMPTY_SEARCH); + + // Said to have changed (a package or a file job says so itself) + ShellInstalledChanged(); + TEST_EQ(ShellInstalledCount(), (int32_t)platformInstalledChangeCount()); + win->onFocus(win); + search->onChange(search); wgtSetFocused(items); tWidgetKey(win, KEY_HOME, 0); TEST_STR_EQ(wgtGetText(items), "Newbie"); TEST_EQ(remove(PM_NEWBIE_APP), 0); + platformInstalledChanged(); win->onFocus(win); search->onChange(search); TEST_STR_EQ(wgtGetText(items), PM_EMPTY_SEARCH); - // Nothing changed: the check alone, no rescan. + // Nothing changed: no rescan. win->onFocus(win); TEST_STR_EQ(wgtGetText(items), PM_EMPTY_SEARCH); } @@ -3943,6 +4109,17 @@ static void caseShellRunningBas(void) { TEST_EQ(ShellAppId(), 0); TEST_EQ(ShellPriority(), -1); TEST_EQ(ShellSetPriority(TS_PRIORITY_NORMAL), 0); + + // Another program's priority, from the shell: the Clock's main loop + // task; a callback program (the save fixture) has none of its own + TEST_EQ(ShellRunningPriority(clk), TS_PRIORITY_HIGH); + TEST_EQ(ShellRunningSetPriority(clk, TS_PRIORITY_LOW), -1); + TEST_EQ(ShellRunningPriority(clk), TS_PRIORITY_LOW); + TEST_EQ(ShellRunningSetPriority(clk, TS_PRIORITY_HIGH + 1), 0); + TEST_EQ(ShellRunningPriority(dirty), SHELL_NO_PRIORITY); + TEST_EQ(ShellRunningSetPriority(dirty, TS_PRIORITY_LOW), 0); + TEST_EQ(ShellRunningPriority(RUN_NO_APP), SHELL_NO_PRIORITY); + TEST_EQ(ShellRunningSetPriority(RUN_NO_APP, TS_PRIORITY_LOW), 0); TEST_EQ(ShellRunningKill(extra), -1); fixPumpShell(SETTLE_FRAMES); TEST_PTR_EQ(shellGetApp(extra), NULL); @@ -3993,6 +4170,55 @@ static void caseShellRunningBas(void) { } +// A BASIC program's SHELL from its Timer (the DOS harness fixture +// shellrun): the shell task's frame fires the Timer, the program's own +// task runs the handler, and its SHELL waits there while the shell's main +// loop carries out the DOS run (here fixPumpShell and shellRunDosRequest, +// as that loop does; on the host the run fails and the box saying so is +// answered). Run on the shell task, the handler would hold that loop +// below it and never see its run. +static void caseShellWaitsOwnTask(void) { + tCase("BASIC SHELL and ShellRunDosWait from a Timer wait on the program's own task"); + fixShellUp(SCREEN_BPP); + + if (!fixInstallBasicApp(SHELLTEST_BASCOMP, SHELLTEST_BASSTUB_SO, SHELLRUN_DBP, SHELLRUN_APP)) { + fixShellDown(); + return; + } + + basNativeHostSetLive(true); + fixDialogsOn("OK"); + + int32_t id = shellLoadApp(tCtx(), SHELLRUN_APP); + WindowT *win = waitForWindow(SHELLRUN_TITLE); + WidgetT *lbl = win ? wgtFind(win->widgetRoot, SHELLRUN_LABEL) : NULL; + + TEST_TRUE(id > 0 && lbl != NULL); + + for (int32_t i = 0; lbl && i < SHELLRUN_FRAMES && strncmp(wgtGetText(lbl), SHELLRUN_RESULT, strlen(SHELLRUN_RESULT)) != 0; i++) { + tAdvanceClock(SHELLRUN_FRAME_MS); + fixPumpShell(1); + shellRunDosRequest(tCtx(), 0); + } + + TEST_TRUE(lbl && strncmp(wgtGetText(lbl), SHELLRUN_RESULT, strlen(SHELLRUN_RESULT)) == 0); + + // ShellRunDosWait ran before it, waiting the same way + WidgetT *waited = win ? wgtFind(win->widgetRoot, SHELLRUN_WAIT_LABEL) : NULL; + + TEST_TRUE(waited && strncmp(wgtGetText(waited), SHELLRUN_WAIT_RESULT, strlen(SHELLRUN_WAIT_RESULT)) == 0); + + if (win) { + TEST_TRUE(fixCloseApp(win)); + } + + fixDialogsOff(); + basNativeHostSetLive(false); + fixKillAllApps(); + fixShellDown(); +} + + // system.bas pointer, keyboard, screen and memory entry points: the input // state the desktop sees, the pointer moved, keys typed into the focused // window one per frame (modifiers released after the last), a screenshot. @@ -4445,6 +4671,16 @@ int32_t musicSetBank(MusicT *music, const char *bank) { } +// Another program: it only takes its turns, until sPacerRuns is cleared. +static void pacerTask(void *arg) { + (void)arg; + + while (sPacerRuns) { + tsYield(); + } +} + + // parpm's IconView's ItemCol or ItemRow (method) of item index; -1 when // the call fails. static int32_t parpmItemCell(WidgetT *icons, const char *method, int32_t index) { @@ -4499,6 +4735,30 @@ static void pmField(const char *label, const char *text) { } +// Frames (each PM_PACE_FRAME_MS besides its decoding) until Program +// Manager has loaded every icon of caseProgmanIconPacing's group, or +// PM_PACE_FRAMES pass; how many it took. *adjacent: two frames in a row +// decoded (the frame before counts: it decoded when a load was counted). +static int32_t pmPaceRun(bool *adjacent) { + int32_t frames = 0; + bool decoded = sImageLoads > 0; + + *adjacent = false; + + while (sImageLoads < PM_PACE_ITEMS && frames < PM_PACE_FRAMES) { + int32_t before = sImageLoads; + + hostClockAdvance(PM_PACE_FRAME_MS); + fixPumpShell(1); + *adjacent = *adjacent || (decoded && sImageLoads > before); + decoded = sImageLoads > before; + frames++; + } + + return frames; +} + + // Each modal gets the next step (counted before the click: the click may // open a nested dialog that takes the step after it); with none left the // dialog is cancelled so a surprise never hangs the suite. @@ -4555,6 +4815,7 @@ static void pmStep(const char *button) { } + // Screen point in the middle of a large-icon cell of an icon view -- // Program Manager's, or parpm's IconView written in BASIC: both follow // the list helper library's icon view rules. @@ -4711,6 +4972,7 @@ int main(void) { RUN_CASE(caseShellRunningBas); RUN_CASE(caseNewArgsRelaunch); RUN_CASE(casePardemo); + RUN_CASE(caseShellWaitsOwnTask); RUN_CASE(caseParpm); RUN_CASE(caseLoadErrors); RUN_CASE(caseTempCopyEdges); @@ -4721,6 +4983,7 @@ int main(void) { RUN_CASE(caseProgmanViews); RUN_CASE(caseProgmanStartup); RUN_CASE(caseProgmanHotkeysGlobal); + RUN_CASE(caseProgmanIconPacing); RUN_CASE(caseDocumentOpensNotepad); RUN_CASE(caseDosPrograms); RUN_CASE(caseScreenSaver); diff --git a/src/test/shell/loaderTest.c b/src/test/shell/loaderTest.c index 5083c13e..b3e30687 100644 --- a/src/test/shell/loaderTest.c +++ b/src/test/shell/loaderTest.c @@ -34,6 +34,7 @@ #undef main #include "dvxPlatHost.h" +#include "dvxEndian.h" #include #include @@ -82,6 +83,16 @@ #define LOAD_NEED_DIR DVX_WIDGETS_DIR "/acme/needx" #define LOAD_NEED_WGT LOAD_NEED_DIR "/needx.wgt" #define LOAD_NEED_DEP LOAD_NEED_DIR "/needx.dep" +#define LAZY_DXE "lazy.lib" +#define LAZY_DXE_HEADER 80 // the DXE3 header, and the fields platformDxeExports reads +#define LAZY_DXE_MAGIC 0x31455844u +#define LAZY_DXE_COUNT_OFS 28 +#define LAZY_DXE_TABLE_OFS 32 +#define LAZY_DXE_SIZE_OFS 36 +#define LAZY_DXE_INIT_OFS 72 +#define LAZY_DXE_INIT_AT 0x40 // any offset: the library has constructors +#define LAZY_DXE_SYM_OFS 4 +#define LAZY_DXE_MAX 128 typedef struct { @@ -106,6 +117,7 @@ static void caseHcfLongKey(void); static void caseHcfLongPattern(void); static void caseHelpFailureRetries(void); static void caseHelpLenientLinks(void); +static void caseLazyLibraries(void); static void caseLoadAll(void); static void caseMarkAddOns(void); static void caseMkdirTooLong(void); @@ -113,9 +125,11 @@ static void caseOwnFatal(void); static void caseResourceOnlyWidget(void); static void caseSafeStart(void); static bool fileContains(const char *path, const char *needle); +static void loaderStateFree(void); static void modAdd(ModuleT **mods, const char *path, const char *base, const char *dep, bool addOn); static bool notesHave(const char *needle); static void writeBytes(const char *path, const void *data, size_t len); +static void writeDxe(const char *path, const char *name); static void writeFile(const char *path, const char *text); @@ -462,6 +476,70 @@ static void caseHelpLenientLinks(void) { } +// The libraries left for later: the index (read from the files' DXE +// headers) tells which library has a name and which run code as they +// load; during an open the resolver only marks the libraries with a +// missing name; a library's .dep libraries load first and one not there +// fails it; after the start a library that cannot load is logged and not +// tried again (not fatal, even one of DVX's own); and the index then +// keeps only the libraries still to load. +static void caseLazyLibraries(void) { + tCase("libraries load when first needed"); + + // lazy.lib is a file the index reads; the others (no files) go in by + // hand, as exportsBuild would stop the start over a missing own one + loaderStateFree(); + writeDxe(LAZY_DXE, "_lazyFn"); + modAdd(&sLibs, LAZY_DXE, "lazy", NULL, false); + exportsBuild(); + TEST_TRUE(sLibs[0].runsAtLoad); + modAdd(&sLibs, "own.lib", "own", NULL, false); + modAdd(&sLibs, "needown.lib", "needown", "own", true); + modAdd(&sLibs, "needgone.lib", "needgone", "gone", true); + exportAdd((void *)(uintptr_t)1, "_ownFn"); + exportAdd((void *)(uintptr_t)2, "_sharedFn"); + exportAdd((void *)(uintptr_t)3, "_sharedFn"); + qsort(sExports, (size_t)arrlen(sExports), sizeof(ExportT), exportCompare); + + TEST_PTR_EQ(libExporting("_lazyFn"), &sLibs[0]); + TEST_PTR_EQ(libExporting("_ownFn"), &sLibs[1]); + TEST_PTR_EQ(libExporting("_nowhere"), NULL); + TEST_PTR_EQ(libFind("NEEDOWN"), &sLibs[2]); + TEST_PTR_EQ(libFind("gone"), NULL); + + // During an open: marked, nothing given, nothing loaded + sOpenDepth = 1; + TEST_PTR_EQ(resolveLater("_sharedFn"), NULL); + TEST_TRUE(sLibs[2].wanted && sLibs[3].wanted && !sLibs[0].wanted && !sLibs[1].wanted); + TEST_TRUE(!libsWant("_nowhere")); + sOpenDepth = 0; + sLibs[2].wanted = false; + sLibs[3].wanted = false; + + // A .dep library that is not installed fails an add-on with a note + TEST_TRUE(!libLoad(&sLibs[3])); + TEST_TRUE(sLibs[3].skipped); + TEST_TRUE(notesHave("needgone.lib was not loaded: it needs gone, which is not installed.")); + + // After the start: own.lib (no file) cannot load; it and what needs it + // are logged and left, DVX goes on, and neither is tried again + sStarted = true; + TEST_TRUE(!libLoad(&sLibs[2])); + TEST_TRUE(sLibs[1].skipped && sLibs[2].skipped); + TEST_TRUE(fileContains("loaderTest.log", "FAILED: own.lib")); + TEST_PTR_EQ(resolveLater("_ownFn"), NULL); + TEST_TRUE(!libLoad(&sLibs[1])); + + // Only lazy.lib is left to load: the index keeps its one name + exportsPrune(); + TEST_EQ((int32_t)arrlen(sExports), 1); + TEST_PTR_EQ(libExporting("_lazyFn"), &sLibs[0]); + sStarted = false; + loaderStateFree(); + remove(LAZY_DXE); +} + + // The whole start: DVX's own control loads, a broken add-on library and // the add-on widget that needs it are skipped with notes; a safe start // leaves the add-ons out (one note says so), and the counts the help @@ -469,7 +547,6 @@ static void caseHelpLenientLinks(void) { static void caseLoadAll(void) { tCase("loadAllModules skips broken add-ons, and all of them in a safe start"); - void **handles; int32_t loads; TEST_EQ(platformMkdirRecursive(LOAD_X_DIR), 0); @@ -482,8 +559,8 @@ static void caseLoadAll(void) { sCtlLoads = 0; sCtlAccept = true; - handles = loadAllModules(false); - TEST_EQ((int32_t)arrlen(handles), 0); + TEST_TRUE(!loadAllModules(false)); + TEST_EQ((int32_t)arrlen(sLoaded), 0); TEST_EQ(sCtlLoads, 1); TEST_STR_EQ(sCtlLoaded, LOAD_OWN_WGT); TEST_EQ(sLibCount, 1); @@ -491,16 +568,16 @@ static void caseLoadAll(void) { TEST_TRUE(notesHave(LOAD_X_LIB " was not loaded: ")); TEST_TRUE(notesHave(LOAD_NEED_WGT " was not loaded: it needs " LOAD_X_LIB ", which was not loaded.")); TEST_TRUE(!notesHave(LOADER_SAFE_NOTE)); - arrfree(handles); + loaderStateFree(); - loads = sCtlLoads; - handles = loadAllModules(true); + loads = sCtlLoads; + TEST_TRUE(!loadAllModules(true)); TEST_EQ(sCtlLoads, loads + 1); TEST_EQ(sLibCount, 1); TEST_EQ(sWidgetCount, 2); TEST_TRUE(notesHave(LOADER_SAFE_NOTE)); TEST_TRUE(fileContains("loaderTest.log", "Safe start: not loading " LOAD_X_LIB)); - arrfree(handles); + loaderStateFree(); } @@ -601,10 +678,10 @@ static void caseResourceOnlyWidget(void) { TEST_EQ((int32_t)arrlen(sResOnlyWidgets), 1); arrfree(mods); - // Handed over once every module is there, then the list is gone + // Handed over once the BASIC library is there, then the list is gone sCtlLoads = 0; sCtlAccept = true; - loadBasicControls(); + loadBasicControls(basCtlLoadFile); TEST_EQ(sCtlLoads, 1); TEST_STR_EQ(sCtlLoaded, RES_ONLY_CTL); TEST_PTR_EQ(sResOnlyWidgets, NULL); @@ -612,11 +689,18 @@ static void caseResourceOnlyWidget(void) { // A file the library refuses is skipped and logged arrput(sResOnlyWidgets, strdup(RES_ONLY_CTL)); sCtlAccept = false; - loadBasicControls(); + loadBasicControls(basCtlLoadFile); TEST_EQ(sCtlLoads, 2); TEST_PTR_EQ(sResOnlyWidgets, NULL); TEST_TRUE(fileContains("loaderTest.log", "no usable BASIC control")); sCtlAccept = true; + + // Without the BASIC library the files are skipped + arrput(sResOnlyWidgets, strdup(RES_ONLY_CTL)); + loadBasicControls(NULL); + TEST_EQ(sCtlLoads, 2); + TEST_PTR_EQ(sResOnlyWidgets, NULL); + TEST_TRUE(fileContains("loaderTest.log", "the BASIC run-time library is not installed")); } @@ -653,6 +737,18 @@ static bool fileContains(const char *path, const char *needle) { } +// The loader's tables back to empty: loadAllModules and the lazy cases +// fill them. +static void loaderStateFree(void) { + freeMods(sLibs); + sLibs = NULL; + arrfree(sExports); + sExports = NULL; + arrfree(sLoaded); + sLoaded = NULL; +} + + // A module record as scanDir and readDeps would make it. static void modAdd(ModuleT **mods, const char *path, const char *base, const char *dep, bool addOn) { ModuleT mod; @@ -687,6 +783,22 @@ static void writeBytes(const char *path, const void *data, size_t len) { } +// A DXE3 file that exports name and has constructors. +static void writeDxe(const char *path, const char *name) { + uint8_t file[LAZY_DXE_MAX]; + int32_t len = (int32_t)strlen(name) + 1; + + memset(file, 0, sizeof(file)); + dvxPutLe32(file, LAZY_DXE_MAGIC); + dvxPutLe32(file + LAZY_DXE_COUNT_OFS, 1); + dvxPutLe32(file + LAZY_DXE_TABLE_OFS, LAZY_DXE_HEADER); + dvxPutLe32(file + LAZY_DXE_SIZE_OFS, (uint32_t)(LAZY_DXE_SYM_OFS + len)); + dvxPutLe32(file + LAZY_DXE_INIT_OFS, LAZY_DXE_INIT_AT); + memcpy(file + LAZY_DXE_HEADER + LAZY_DXE_SYM_OFS, name, (size_t)len); + writeBytes(path, file, (size_t)(LAZY_DXE_HEADER + LAZY_DXE_SYM_OFS + len)); +} + + static void writeFile(const char *path, const char *text) { writeBytes(path, text, strlen(text)); } @@ -718,6 +830,7 @@ int main(void) { caseSafeStart(); caseHelpFailureRetries(); caseHelpLenientLinks(); + caseLazyLibraries(); TEST_EQ(chdir(startDir), 0); return tSuiteEnd(); diff --git a/src/test/shell/progmanTest.c b/src/test/shell/progmanTest.c index 3155f3f4..f458c5c1 100644 --- a/src/test/shell/progmanTest.c +++ b/src/test/shell/progmanTest.c @@ -27,19 +27,24 @@ // launcher; installed-program discovery and icons (shellProg.c); the // command-line helpers, shellRunProgram and Run Minimized (shellRun.c); // the desktop change counter and Task Manager hook; and the icon view -// rules Program Manager's view shares with BASIC icon views (listHelp.c). -// Program Manager's window itself is driven by appsTest.c. +// rules Program Manager's view shares with BASIC icon views (listHelp.c); +// and the program cache behind the program and icon calls +// (shellProgCache.c). Program Manager's window itself is driven by +// appsTest.c. #include "shellFixture.h" #include "shellAssoc.h" #include "shellProg.h" +#include "shellProgCache.h" #include "shellRun.h" #include "pmModel.h" #include "listHelp.h" #include "dvxPrefs.h" +#include "dvxRes.h" #include "stb_ds_wrap.h" #include "dvxBas.h" +#include #include #include #include @@ -70,6 +75,11 @@ #define ICON_LIST_W 300 // a list's inner width #define ICON_LABEL_MAX 10 // a large label's characters (an 80-pixel cell) #define ICON_ROWS_OWN 5 // rows items in cells of their own take +#define CACHE_DIR "apps/cache" // caseProgCache's files +#define CACHE_PROG CACHE_DIR "/cached.app" +#define CACHE_PICTURE CACHE_DIR "/cached.bmp" +#define CACHE_STEP_SEC 10 // seconds between the times it gives its files +#define CACHE_IDLE_CALLS 64 // idle calls a save may take at most // ============================================================ @@ -87,12 +97,15 @@ static void caseModelEdits(void); static void caseModelFields(void); static void caseModelFirstRun(void); static void caseModelRoundTrip(void); +static void caseProgCache(void); static void caseProgScan(void); static void caseRunHelpers(void); static void caseRunMinimized(void); static void caseRunProgram(void); static void countTaskManager(AppContextT *ctx); static int32_t fakeLaunch(AppContextT *ctx, const char *path, const char *args); +static void fakeProgram(const char *path, const char *name, const char *desc); +static void fileTime(const char *path, const struct timespec *mtime); static PmItemT makeItem(const char *name, const char *program); static void writeText(const char *path, const char *text); @@ -1081,6 +1094,145 @@ static void caseModelRoundTrip(void) { } +// The program cache: what was read from a file is answered while the +// file's size and time are unchanged (a file rewritten with both kept +// still answers the old), also from the saved cache after a fresh read; a +// new time is noticed by the scan's check, and by a lookup after what is +// installed changed. Pictures likewise, and bytes that are no image are +// noted as none; a file that is gone goes. The idle time saves a change +// once it is old enough and frees a cache unused long enough; a damaged +// cache file is ignored, and nothing is saved when DVX cannot save. +static void caseProgCache(void) { + static const char junk[] = "this is no picture"; + char cachePath[DVX_MAX_PATH]; + ShellProgramT info; + struct stat st; + struct timespec t; + int32_t pitch = 0; + int32_t len = 0; + uint8_t *icon; + char *bmp; + + tCase("program cache"); + fixShellUp(SCREEN_BPP); + TEST_EQ(platformMkdirRecursive(platformUserDir()), 0); + TEST_TRUE(platformUserPath(cachePath, sizeof(cachePath), SHELL_PROG_CACHE_NAME)); + TEST_EQ(platformMkdirRecursive(CACHE_DIR), 0); + fakeProgram(CACHE_PROG, "Alpha", "First"); + shellProgramInfo(CACHE_PROG, &info); + TEST_STR_EQ(info.name, "Alpha"); + TEST_EQ(stat(CACHE_PROG, &st), 0); + t = st.st_mtim; + fakeProgram(CACHE_PROG, "Bravo", "Other"); + fileTime(CACHE_PROG, &t); + shellProgramInfo(CACHE_PROG, &info); + TEST_STR_EQ(info.name, "Alpha"); + TEST_STR_EQ(info.description, "First"); + + // Saved, then read again + shellProgCacheShutdown(); + TEST_EQ(access(cachePath, F_OK), 0); + shellProgramInfo(CACHE_PROG, &info); + TEST_STR_EQ(info.name, "Alpha"); + + // A new time: the scan's check reads the file again + t.tv_sec -= CACHE_STEP_SEC; + fileTime(CACHE_PROG, &t); + shellProgCacheCheck(CACHE_PROG); + shellProgramInfo(CACHE_PROG, &info); + TEST_STR_EQ(info.name, "Bravo"); + + // A lookup checks again only once what is installed changed + fakeProgram(CACHE_PROG, "Delta", "Third"); + t.tv_sec -= CACHE_STEP_SEC; + fileTime(CACHE_PROG, &t); + shellProgramInfo(CACHE_PROG, &info); + TEST_STR_EQ(info.name, "Bravo"); + platformInstalledChanged(); + shellProgramInfo(CACHE_PROG, &info); + TEST_STR_EQ(info.name, "Delta"); + + // A picture's bytes are kept: rewritten with its time kept, it still shows + bmp = platformReadFile(DEMO_BMP, &len); + TEST_TRUE(bmp != NULL && len > (int32_t)sizeof(junk)); + TEST_TRUE(tWriteFile(CACHE_PICTURE, bmp, len)); + icon = shellItemIcon(tCtx(), CACHE_PICTURE, "", SHELL_PROG_ICON, &pitch); + TEST_TRUE(icon != NULL); + dvxFreeImage(icon); + TEST_EQ(stat(CACHE_PICTURE, &st), 0); + t = st.st_mtim; + memset(bmp, 0, (size_t)len); + memcpy(bmp, junk, sizeof(junk)); + TEST_TRUE(tWriteFile(CACHE_PICTURE, bmp, len)); + fileTime(CACHE_PICTURE, &t); + icon = shellItemIcon(tCtx(), CACHE_PICTURE, "", SHELL_PROG_ICON, &pitch); + TEST_TRUE(icon != NULL); + dvxFreeImage(icon); + + // Bytes that are no image are noted as none (a picture written back + // with the same stamp is not looked at again) + t.tv_sec -= CACHE_STEP_SEC; + fileTime(CACHE_PICTURE, &t); + shellProgCacheCheck(CACHE_PICTURE); + TEST_PTR_EQ(shellItemIcon(tCtx(), CACHE_PICTURE, "", SHELL_PROG_ICON, &pitch), NULL); + free(bmp); + bmp = platformReadFile(DEMO_BMP, &len); + TEST_TRUE(bmp != NULL && tWriteFile(CACHE_PICTURE, bmp, len)); + fileTime(CACHE_PICTURE, &t); + TEST_PTR_EQ(shellItemIcon(tCtx(), CACHE_PICTURE, "", SHELL_PROG_ICON, &pitch), NULL); + free(bmp); + + // A file that is gone goes from the cache + TEST_EQ(remove(CACHE_PICTURE), 0); + shellProgCacheCheck(CACHE_PICTURE); + TEST_PTR_EQ(shellItemIcon(tCtx(), CACHE_PICTURE, "", SHELL_PROG_ICON, &pitch), NULL); + + // The idle time saves a change once it is SHELL_PROG_CACHE_SAVE_MS old + TEST_EQ(remove(cachePath), 0); + fakeProgram(CACHE_PROG, "Echo", "Fifth"); + t.tv_sec -= CACHE_STEP_SEC; + fileTime(CACHE_PROG, &t); + shellProgCacheCheck(CACHE_PROG); + shellProgramInfo(CACHE_PROG, &info); + TEST_STR_EQ(info.name, "Echo"); + shellProgCacheIdle(NULL); + TEST_TRUE(access(cachePath, F_OK) != 0); + hostClockAdvance(SHELL_PROG_CACHE_SAVE_MS); + + for (int32_t i = 0; i < CACHE_IDLE_CALLS && access(cachePath, F_OK) != 0; i++) { + shellProgCacheIdle(NULL); + } + + TEST_EQ(access(cachePath, F_OK), 0); + + // Unused for SHELL_PROG_CACHE_DROP_MS, the memory goes: with the saved + // cache gone too, a file rewritten with its stamp kept is read anew + hostClockAdvance(SHELL_PROG_CACHE_DROP_MS); + shellProgCacheIdle(NULL); + TEST_EQ(remove(cachePath), 0); + fakeProgram(CACHE_PROG, "Golf", "Sixth"); + fileTime(CACHE_PROG, &t); + shellProgramInfo(CACHE_PROG, &info); + TEST_STR_EQ(info.name, "Golf"); + + // A damaged cache file is ignored + shellProgCacheDrop(); + writeText(cachePath, "DVXPCACH but nothing after it"); + shellProgramInfo(CACHE_PROG, &info); + TEST_STR_EQ(info.name, "Golf"); + + // Nothing is saved when DVX cannot save + shellProgCacheDrop(); + TEST_EQ(remove(cachePath), 0); + platformSetReadOnly(true); + shellProgramInfo(CACHE_PROG, &info); + shellProgCacheShutdown(); + TEST_TRUE(access(cachePath, F_OK) != 0); + platformSetReadOnly(false); + fixShellDown(); +} + + // Discovery in the fixture's apps tree: names from resources, skipping a // path, paths-only mode, icons from programs and pictures at any size. static void caseProgScan(void) { @@ -1305,6 +1457,27 @@ static int32_t fakeLaunch(AppContextT *ctx, const char *path, const char *args) } +// A program file of its own: a few module bytes, then its name and +// description resources (names of one length give one size). +static void fakeProgram(const char *path, const char *name, const char *desc) { + static const char module[] = "MODULEBYTES"; + + TEST_TRUE(tWriteFile(path, module, (int32_t)sizeof(module))); + TEST_EQ(dvxResAppend(path, "name", DVX_RES_TEXT, name, (uint32_t)strlen(name) + 1), 0); + TEST_EQ(dvxResAppend(path, "description", DVX_RES_TEXT, desc, (uint32_t)strlen(desc) + 1), 0); +} + + +// Give path the modification time mtime (to the nanosecond). +static void fileTime(const char *path, const struct timespec *mtime) { + struct timespec times[2]; + + times[0] = *mtime; + times[1] = *mtime; + TEST_EQ(utimensat(AT_FDCWD, path, times, 0), 0); +} + + static PmItemT makeItem(const char *name, const char *program) { PmItemT item; @@ -1338,6 +1511,7 @@ int main(void) { caseAssoc(); caseRunHelpers(); caseProgScan(); + caseProgCache(); caseRunProgram(); caseRunMinimized(); caseItemIcon(); diff --git a/src/test/shell/shellFixture.c b/src/test/shell/shellFixture.c index 4f6544f3..b5742714 100644 --- a/src/test/shell/shellFixture.c +++ b/src/test/shell/shellFixture.c @@ -28,6 +28,7 @@ #include "dvxRes.h" #include "stb_ds_wrap.h" +#include #include #include #include @@ -37,10 +38,12 @@ #define BUILT_SUFFIX ".built" // bascomp output beside the app #define CMD_MAX (DVX_MAX_PATH * 4) #define CLOSE_FRAMES_MAX 400 // fixCloseApp: frames to wait for the end +#define SETTLED_FRAMES_MAX 400 // fixPumpSettled: frames a program may take #define REPO_ROOT_DEPTH 3 // DVXTEST_APPS_DIR is /obj/host/apps static void dismissDialog(AppContextT *ctx, WindowT *modal, void *userCtx); static int32_t menuFind(const MenuT *menu, const char *caption); +static void pumpWhileBusy(int32_t minFrames); char fixRepoRoot[DVX_MAX_PATH]; char fixStartDir[DVX_MAX_PATH]; @@ -52,12 +55,27 @@ static clock_t sDialogNotUntil = 0; static void (*sSavedIdle)(void *ctx) = NULL; static void *sSavedIdleCtx = NULL; +// fixPumpSettled: the BASIC runtime's basFormRtBusy, found at run time (a +// suite without the runtime has no BASIC program to wait for) +static bool (*sBasicBusy)(void) = NULL; +static bool sBasicBusyResolved = false; + +// The dialog window last answered, until it closes: a dialog of a program +// on its own task closes only when that task next runs, so the shell's +// frames meanwhile see it still up and must not answer it again (a form +// shown again later, the same window, is answered again). +static int32_t sDialogAnswered = 0; + static void dismissDialog(AppContextT *ctx, WindowT *modal, void *userCtx) { (void)ctx; (void)userCtx; - if (!modal) { + if (!modal || modal->id != sDialogAnswered) { + sDialogAnswered = 0; + } + + if (!modal || modal->id == sDialogAnswered) { return; } @@ -96,6 +114,7 @@ static void dismissDialog(AppContextT *ctx, WindowT *modal, void *userCtx) { // "Yes" answers the nested Confirm Save / Confirm Create boxes the // file dialogs raise after the scripted Open/Save click. if (tDialogClickButton(modal, sDialogAnswer) || tDialogClickButton(modal, "OK") || tDialogClickButton(modal, "Yes")) { + sDialogAnswered = modal->id; fixDialogs++; } } @@ -329,14 +348,15 @@ bool fixProcessInit(const char *logName) { } +void fixPumpSettled(void) { + pumpWhileBusy(SETTLE_FRAMES); +} + + void fixPumpShell(int32_t frames) { for (int32_t i = 0; i < frames; i++) { tPump(1); - if (tsActiveCount() > 1) { - tsYield(); - } - if (shellReapApps(tCtx())) { shellDesktopUpdate(); } @@ -344,6 +364,11 @@ void fixPumpShell(int32_t frames) { } +void fixPumpSteady(void) { + pumpWhileBusy(1); +} + + void fixShellDown(void) { fixKillAllApps(); tsShutdown(); @@ -398,3 +423,17 @@ static int32_t menuFind(const MenuT *menu, const char *caption) { return -1; } + + +// At least minFrames frames, then more while a BASIC program has code +// running or events waiting (up to SETTLED_FRAMES_MAX). +static void pumpWhileBusy(int32_t minFrames) { + if (!sBasicBusyResolved) { + sBasicBusyResolved = true; + sBasicBusy = (bool (*)(void))dlsym(RTLD_DEFAULT, DVX_SYM("basFormRtBusy")); + } + + for (int32_t i = 0; i < SETTLED_FRAMES_MAX && (i < minFrames || (sBasicBusy && sBasicBusy())); i++) { + fixPumpShell(1); + } +} diff --git a/src/test/shell/shellFixture.h b/src/test/shell/shellFixture.h index d4cdebd5..27ffc4f4 100644 --- a/src/test/shell/shellFixture.h +++ b/src/test/shell/shellFixture.h @@ -113,8 +113,18 @@ int32_t fixMenuId(const WindowT *win, const char *caption); // /, derive fixRepoRoot and load the widget modules. bool fixProcessInit(const char *logName); -// One shell main-loop iteration per frame: dvxUpdate (whose idle path -// yields to app tasks), an explicit yield, then reaping. +// Frames (SETTLE_FRAMES at least) until no BASIC program has code running +// or events waiting: a program runs a handler on its own task, at its +// turn, so a test that dispatched an event waits for it this way. +void fixPumpSettled(void); + +// One frame, then more while a BASIC program has code running or events +// waiting: a clock step's events handled, with no extra frames for a +// program that counts them (a level read "since the last time"). +void fixPumpSteady(void); + +// One shell main-loop iteration per frame: dvxUpdate (whose idle or busy +// callback yields to app tasks), then reaping. void fixPumpShell(int32_t frames); // Tear down everything fixShellUp built and return to fixStartDir. diff --git a/src/test/shell/shellMainTest.c b/src/test/shell/shellMainTest.c index 0d5765ca..863d0710 100644 --- a/src/test/shell/shellMainTest.c +++ b/src/test/shell/shellMainTest.c @@ -39,7 +39,7 @@ #include "dvxPlat.h" #include "dvxPlatHost.h" #include "taskSwch.h" -#include "../../libs/kpunch/serial/security/security.h" +#include "../../libs/kpunch/crypto/security.h" #include #include @@ -80,9 +80,9 @@ static char sStartDir[DVX_MAX_PATH]; static char sWorkDir[DVX_MAX_PATH]; static char sLogPath[DVX_MAX_PATH]; -// Stand-ins for the serial library's RNG entry points (security.h): exported from the +// Stand-ins for the cryptography library's RNG entry points (security.h): exported from the // test binary (-rdynamic) so the shell's dlsym finds them, as it finds the -// real ones in the serial library on DOS. +// real ones in the cryptography library on DOS. static int32_t sEntropyBytes = 0; static int32_t sLoadCalls = 0; static int32_t sSaveCalls = 0; diff --git a/src/test/shell/shellTest.c b/src/test/shell/shellTest.c index 195b0045..492e1380 100644 --- a/src/test/shell/shellTest.c +++ b/src/test/shell/shellTest.c @@ -29,6 +29,7 @@ // (apps/, USER/) resolve exactly as they do under the DOS shell. #include "shellFixture.h" +#include "dvxComp.h" #include "dvxMem.h" #include "dvxPlat.h" #include "dvxRes.h" @@ -66,6 +67,9 @@ #define APP_FONTS "apps/kpunch/clock/fonts.app" // a copy of the Clock carrying a font #define FONT_FIXTURE "src/test/tools/golden/fonts/TINY.DFN" // its font: family Tiny #define FONT_SIZES 2 // the sizes it has +#define BUSY_IDLE_MS 20 // shellMain.c SHELL_BUSY_IDLE_US +#define BUSY_FRAMES 4 +#define BUSY_DIRTY_PX 8 typedef struct { @@ -93,6 +97,7 @@ static void caseForceKill(void); static void caseForceKillDeferred(void); static void caseForceKillReleasesClasses(void); static void caseForceKillRunaway(void); +static void caseIdleBusyFrames(void); static void caseIdleFanOut(void); static void caseMemoryAttribution(void); static void caseMultiInstance(void); @@ -102,6 +107,7 @@ static void caseSymbolPrefix(void); static void caseTaskManager(void); static void caseTaskManagerHung(void); static void caseTerminateAll(void); +static void caseWorkTask(void); static void cleanupLog(int32_t appId); static void clickAround(WindowT *win, bool escExits); static void closeAppWindows(int32_t appId, const char *answer); @@ -112,6 +118,8 @@ static void hangKey(WindowT *win, int32_t key, int32_t mod); static void idleAlloc(void *ctx); static void idleKill(void *ctx); static void idleLog(void *ctx); +static void taskCount(void *arg); +static void workTask(void *arg); // ============================================================ @@ -126,6 +134,8 @@ static const AppFixtureT sApps[] = { }; static char sIdleLog[IDLE_LOG_MAX]; +static int32_t sCountTurns = 0; // taskCount: turns it has had +static bool sCountStop = false; // taskCount: end now static void *sAppAlloc = NULL; static int32_t sDesktopUpdates = 0; static int32_t sKillTarget = 0; @@ -726,6 +736,43 @@ static void caseForceKillRunaway(void) { } +// Frames that keep painting (no idle frame comes) still give the other +// tasks a turn each frame and run the idle handlers every +// BUSY_IDLE_MS of the fake clock. +static void caseIdleBusyFrames(void) { + tCase("busy frames yield to other tasks and still run the idle handlers"); + fixShellUp(SCREEN_BPP); + shellRegisterIdle(idleLog, (void *)"a"); + fixPumpShell(SETTLE_FRAMES); + sCountTurns = 0; + sCountStop = false; + + int32_t taskId = tsCreate("count", taskCount, NULL, 0, TS_PRIORITY_NORMAL); + + TEST_TRUE(taskId > 0); + hostClockAdvance(BUSY_IDLE_MS); + sIdleLog[0] = '\0'; + + for (int32_t i = 0; i < BUSY_FRAMES; i++) { + dirtyListAdd(&tCtx()->dirty, 0, 0, BUSY_DIRTY_PX, BUSY_DIRTY_PX); + tPump(1); + } + + TEST_STR_EQ(sIdleLog, "a"); + TEST_TRUE(sCountTurns >= BUSY_FRAMES); + hostClockAdvance(BUSY_IDLE_MS); + dirtyListAdd(&tCtx()->dirty, 0, 0, BUSY_DIRTY_PX, BUSY_DIRTY_PX); + tPump(1); + TEST_STR_EQ(sIdleLog, "aa"); + + sCountStop = true; + fixPumpShell(SETTLE_FRAMES); + TEST_TRUE(tsGetState((uint32_t)taskId) == TaskStateTerminated); + shellUnregisterIdle(idleLog, (void *)"a"); + fixShellDown(); +} + + static void caseIdleFanOut(void) { tCase("idle handlers fan out in registration order"); fixShellUp(SCREEN_BPP); @@ -1168,6 +1215,65 @@ static void caseTerminateAll(void) { } +// shellAppTaskStart: the work runs on a task of its own as its program, +// one at a time; the program is not reaped while it runs (its windows all +// closed), is once it has ended, and ending it by force ends the task. +static void caseWorkTask(void) { + tCase("a program's work task: its program's, holds the reap, ends with a kill"); + fixShellUp(SCREEN_BPP); + + int32_t id = shellLoadApp(tCtx(), APP_CPANEL); + ShellAppT *app = shellGetApp(id); + int32_t seen = 0; + + TEST_TRUE(app != NULL); + + if (!app) { + fixShellDown(); + return; + } + + // Only a running program has one + TEST_EQ(shellAppTaskStart(tCtx(), "work", 0, workTask, &seen), -1); + tCtx()->currentAppId = id; + sCountTurns = 0; + sCountStop = false; + TEST_TRUE(shellAppTaskStart(tCtx(), "work", 0, workTask, &seen) > 0); + TEST_EQ(shellAppTaskStart(tCtx(), "work", 0, workTask, &seen), -1); + tCtx()->currentAppId = 0; + fixPumpShell(SETTLE_FRAMES); + TEST_EQ(seen, id); + TEST_TRUE(sCountTurns > 0); + TEST_EQ(tsActiveCount(), 2); + + // Its windows closed, the program stays while the work runs + closeAppWindows(id, "Yes"); + TEST_EQ(fixAppWindowCount(id), 0); + TEST_PTR_EQ(shellGetApp(id), app); + + // The work done, it is reaped + sCountStop = true; + fixPumpShell(SETTLE_FRAMES); + TEST_PTR_EQ(shellGetApp(id), NULL); + TEST_EQ(tsActiveCount(), 1); + + // Ended by force (a main-loop program this time), its work task goes + // with it + id = shellLoadApp(tCtx(), APP_CLOCK); + tCtx()->currentAppId = id; + sCountStop = false; + TEST_TRUE(shellAppTaskStart(tCtx(), "work", 0, workTask, &seen) > 0); + tCtx()->currentAppId = 0; + fixPumpShell(SETTLE_FRAMES); + TEST_EQ(tsActiveCount(), 3); + shellForceKillApp(tCtx(), shellGetApp(id)); + fixPumpShell(1); + TEST_PTR_EQ(shellGetApp(id), NULL); + TEST_EQ(tsActiveCount(), 1); + fixShellDown(); +} + + static void cleanupLog(int32_t appId) { sCleanedApp = appId; sCleanupCalls++; @@ -1324,6 +1430,24 @@ static void idleLog(void *ctx) { } +static void taskCount(void *arg) { + (void)arg; + + while (!sCountStop) { + sCountTurns++; + tsYield(); + } +} + + +// caseWorkTask's work: the program it runs as (into *arg), then turns as +// taskCount takes them. +static void workTask(void *arg) { + *(int32_t *)arg = tCtx()->currentAppId; + taskCount(NULL); +} + + int main(void) { tSuiteBegin("T10 shell"); @@ -1343,6 +1467,7 @@ int main(void) { caseForceKill(); caseForceKillDeferred(); caseTerminateAll(); + caseIdleBusyFrames(); caseIdleFanOut(); caseConfigPaths(); caseDefaultHelpFile(); @@ -1352,6 +1477,7 @@ int main(void) { caseTaskManagerHung(); caseForceKillReleasesClasses(); caseForceKillRunaway(); + caseWorkTask(); return tSuiteEnd(); } diff --git a/src/test/sql/sqlMain.c b/src/test/sql/sqlMain.c index e1b7719d..d84c2104 100644 --- a/src/test/sql/sqlMain.c +++ b/src/test/sql/sqlMain.c @@ -83,6 +83,12 @@ #define SQL_TEMP_PREFIX "SQL" #define SQL_TEMP_EXT ".TMP" #define SQL_FILL_SQL "WITH RECURSIVE c(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM c WHERE x < %d) INSERT INTO %s SELECT hex(zeroblob(100)) FROM c" +#define SQL_LONG_ROWS 100000 // a statement long enough for many pump calls +#define SQL_LONG_FILL "CREATE TABLE big AS WITH RECURSIVE c(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM c WHERE x < 100000) SELECT x FROM c" +#define SQL_LONG_SORT "SELECT x FROM big ORDER BY x DESC" +#define SQL_LONG_UPDATE "UPDATE big SET x = x + 1" +#define SQL_ERR_RUNNING "A statement is still running on this database" +#define SQL_ERR_HANDLE "Invalid database handle" #define SQL_DIR_MODE 0755 #define SQL_READONLY_MODE 0444 #define SQL_WRITABLE_MODE 0644 @@ -109,6 +115,7 @@ static void caseLegacyFixture(void); static void caseNextErrors(void); static void caseOpenClose(void); static void caseOpenFailure(void); +static void casePump(void); static void caseQueryErrors(void); static void caseSampleProgram(void); static void caseTransactions(void); @@ -123,14 +130,33 @@ static int32_t countTempFiles(const char *dir); static bool fileExists(const char *path); static bool filesEqual(const char *pathA, const char *pathB); static int32_t openWithTable(void); +static bool pumpTest(int32_t db); static int32_t queryInt(int32_t db, const char *sql); static const char *queryText(int32_t db, const char *sql); void secRngBytes(uint8_t *buf, int32_t len); static void tempPath(char *out, const char *name); +// What casePump's pump does +typedef enum { + PumpCountE, // counts its calls + PumpStopE, // stops the statement + PumpNestedE, // calls in again (refused), and grows the tables with other databases + PumpCloseE // frees the cursor and closes the database +} PumpModeE; + // Calls to the fake secRngBytes below (the VFS finds it with dlsym). static int32_t sRngCalls = 0; +// casePump's pump: what it does, how often it ran, the cursor stepped, +// the databases and cursors it opened, and whether the nested calls on +// the running database were all refused. +static PumpModeE sPumpMode = PumpCountE; +static int32_t sPumpCalls = 0; +static int32_t sPumpRs = 0; +static int32_t sPumpDbs[SQL_MANY_DBS]; +static int32_t sPumpCursors[SQL_MANY_CURSORS]; +static bool sPumpRefused = false; + static void caseAffectedRows(void) { tCase("affected rows follows INSERT/UPDATE/DELETE, not SELECT"); @@ -162,7 +188,8 @@ static void caseBuildOptions(void) { static const char *const options[] = { "THREADSAFE=0", "ENABLE_8_3_NAMES=2", "TEMP_STORE=2", "DEFAULT_PAGE_SIZE=1024", "DEFAULT_CACHE_SIZE=-512", "DEFAULT_MEMSTATUS=0", "MAX_MMAP_SIZE=0", "OMIT_WAL", "OMIT_LOAD_EXTENSION", "OMIT_SHARED_CACHE", - "OMIT_DEPRECATED", "OMIT_PROGRESS_CALLBACK", "OMIT_JSON", "OMIT_UTF16", "DQS=0", "LIKE_DOESNT_MATCH_BLOBS" + "OMIT_DEPRECATED", "OMIT_JSON", "OMIT_UTF16", "DQS=0", "LIKE_DOESNT_MATCH_BLOBS", + "OMIT_AUTHORIZATION", "OMIT_COMPLETE", "OMIT_INTROSPECTION_PRAGMAS" }; char path[SQL_PATH_MAX]; int32_t db = dvxSqlOpen(SQL_MEMORY_DB); @@ -177,6 +204,9 @@ static void caseBuildOptions(void) { } } + // The progress callback a database's pump runs from is built in + TEST_TRUE(!sqlite3_compileoption_used("OMIT_PROGRESS_CALLBACK")); + // DQS=0: a double-quoted word is an identifier, never a string TEST_EQ(dvxSqlQuery(db, "SELECT \"nosuchcolumn\""), 0); TEST_TRUE(strstr(dvxSqlError(db), "no such column") != NULL); @@ -693,6 +723,95 @@ static void caseOpenFailure(void) { } +// A database's pump runs while a long statement does (dvxSqlExec, and a +// query's first step, which sorts every row) and can stop it; the calls it +// makes on that database are refused, a free and a close it asks for wait +// for the statement's end, and other databases work, also when opening +// them grows the tables under the running call. +static void casePump(void) { + tCase("a database's pump: runs in long statements, stops them, guarded"); + + int32_t db = dvxSqlOpen(SQL_MEMORY_DB); + + // No pump: nothing runs + sPumpMode = PumpCountE; + sPumpCalls = 0; + TEST_TRUE(dvxSqlExec(db, SQL_LONG_FILL)); + TEST_EQ(sPumpCalls, 0); + + dvxSqlSetPump(db, pumpTest); + TEST_TRUE(dvxSqlExec(db, "DROP TABLE big")); + TEST_TRUE(dvxSqlExec(db, SQL_LONG_FILL)); + TEST_TRUE(sPumpCalls > 0); + TEST_EQ(dvxSqlAffectedRows(db), 0); + + sPumpCalls = 0; + + int32_t rs = dvxSqlQuery(db, SQL_LONG_SORT); + + TEST_TRUE(dvxSqlNext(rs)); + TEST_TRUE(sPumpCalls > 0); + TEST_EQ(dvxSqlFieldInt(rs, 0), SQL_LONG_ROWS); + dvxSqlFreeResult(rs); + + // Stopped: the call fails and the statement is undone + sPumpMode = PumpStopE; + TEST_TRUE(!dvxSqlExec(db, SQL_LONG_UPDATE)); + TEST_TRUE(strstr(dvxSqlError(db), "interrupt") != NULL); + sPumpMode = PumpCountE; + TEST_EQ(queryInt(db, "SELECT max(x) FROM big"), SQL_LONG_ROWS); + + // Calls in again: refused on this database, others work + int32_t other = dvxSqlOpen(SQL_MEMORY_DB); + + sPumpMode = PumpNestedE; + sPumpCalls = 0; + sPumpRefused = false; + sPumpRs = dvxSqlQuery(db, SQL_LONG_SORT); + TEST_TRUE(dvxSqlNext(sPumpRs)); + TEST_TRUE(sPumpRefused); + TEST_EQ(dvxSqlFieldInt(sPumpRs, 0), SQL_LONG_ROWS); + TEST_TRUE(dvxSqlNext(sPumpRs)); + TEST_EQ(dvxSqlFieldInt(sPumpRs, 0), SQL_LONG_ROWS - 1); + + for (int32_t i = 0; i < SQL_MANY_DBS; i++) { + TEST_TRUE(sPumpDbs[i] > 0); + TEST_EQ(queryInt(sPumpDbs[i], "SELECT 7"), 7); + } + + for (int32_t i = 0; i < SQL_MANY_CURSORS; i++) { + TEST_TRUE(sPumpCursors[i] > 0); + dvxSqlFreeResult(sPumpCursors[i]); + } + + for (int32_t i = 0; i < SQL_MANY_DBS; i++) { + dvxSqlClose(sPumpDbs[i]); + } + + dvxSqlFreeResult(sPumpRs); + dvxSqlClose(other); + + // A free and a close from the pump: done when the step ends + sPumpMode = PumpCloseE; + sPumpCalls = 0; + sPumpRs = dvxSqlQuery(db, SQL_LONG_SORT); + TEST_TRUE(!dvxSqlNext(sPumpRs)); + TEST_TRUE(sPumpCalls > 0); + TEST_EQ(dvxSqlFieldCount(sPumpRs), 0); + TEST_STR_EQ(dvxSqlError(db), SQL_ERR_HANDLE); + + // ... and a close during dvxSqlExec + db = dvxSqlOpen(SQL_MEMORY_DB); + sPumpRs = 0; + sPumpCalls = 0; + dvxSqlSetPump(db, pumpTest); + TEST_TRUE(dvxSqlExec(db, SQL_LONG_FILL)); + TEST_TRUE(sPumpCalls > 0); + TEST_STR_EQ(dvxSqlError(db), SQL_ERR_HANDLE); + sPumpMode = PumpCountE; +} + + static void caseQueryErrors(void) { tCase("query rejects NULL/bad SQL and leaves the error for the handle"); @@ -924,7 +1043,7 @@ static void caseVfsMisc(void) { TEST_TRUE(vfs != NULL); TEST_STR_EQ(vfs->zName, "dvx"); - // The serial library's DRBG supplies randomness when it is loaded + // The cryptography library's DRBG supplies randomness when it is loaded memset(buf, 0, sizeof(buf)); TEST_EQ(vfs->xRandomness(vfs, sizeof(buf), (char *)buf), SQL_RNG_BYTES); TEST_EQ(sRngCalls, calls + 1); @@ -1110,6 +1229,49 @@ static int32_t openWithTable(void) { } +// casePump's pump (see PumpModeE). The nested calls are made on its +// first call only. +static bool pumpTest(int32_t db) { + sPumpCalls++; + + switch (sPumpMode) { + case PumpStopE: + return false; + + case PumpNestedE: + if (sPumpCalls == 1) { + sPumpRefused = !dvxSqlExec(db, "SELECT 1") && strcmp(dvxSqlError(db), SQL_ERR_RUNNING) == 0; + sPumpRefused = sPumpRefused && dvxSqlQuery(db, "SELECT 1") == 0 && !dvxSqlNext(sPumpRs); + sPumpRefused = sPumpRefused && dvxSqlFieldCount(sPumpRs) == 0 && dvxSqlFieldText(sPumpRs, 0)[0] == '\0'; + + for (int32_t i = 0; i < SQL_MANY_DBS; i++) { + sPumpDbs[i] = dvxSqlOpen(SQL_MEMORY_DB); + } + + for (int32_t i = 0; i < SQL_MANY_CURSORS; i++) { + sPumpCursors[i] = dvxSqlQuery(sPumpDbs[0], "SELECT 1"); + } + } + + return true; + + case PumpCloseE: + if (sPumpCalls == 1) { + dvxSqlFreeResult(sPumpRs); + dvxSqlClose(db); + sPumpRefused = dvxSqlFieldCount(sPumpRs) == 0; + } + + return true; + + case PumpCountE: + break; + } + + return true; +} + + // First column of the first row as an integer; -1 when the query fails. static int32_t queryInt(int32_t db, const char *sql) { int32_t rs = dvxSqlQuery(db, sql); @@ -1141,7 +1303,7 @@ static const char *queryText(int32_t db, const char *sql) { } -// Stand-in for the serial library's DRBG. The VFS looks secRngBytes up +// Stand-in for the cryptography library's DRBG. The VFS looks secRngBytes up // with dlsym and the suite links -rdynamic, so it finds this one. void secRngBytes(uint8_t *buf, int32_t len) { memset(buf, SQL_RNG_FILL, len); @@ -1164,6 +1326,7 @@ int main(void) { caseQueryErrors(); caseFieldAccess(); caseNextErrors(); + casePump(); caseAffectedRows(); caseTransactions(); caseEscape(); diff --git a/src/test/terminal/Makefile b/src/test/terminal/Makefile index 6fa968c4..6799047a 100644 --- a/src/test/terminal/Makefile +++ b/src/test/terminal/Makefile @@ -36,6 +36,9 @@ # proxy's serial socket shim and directly over TCP # testTelnet the Telnet filter byte for byte, and Telnet sessions # against a host played by the test on a loopback socket +# testConnNoNet the connection library without the networking library +# (no lookups, nothing exported): no Telnet, network +# connections end at once, serial ports open as ever # testTermApp the Terminal program (compiled from its project, run by # the BASIC stub) in the host shell, end to end, and its # phone book module on its own (bookTest/bookTest.dbp) @@ -48,9 +51,9 @@ include ../dvxtest/dvxtest.mk ROOT = $(DVXTEST_ROOT) LIBS = $(ROOT)/src/libs/kpunch +MONO = $(LIBS)/crypto/thirdparty/monocypher/src XFERDIR = $(LIBS)/xfer CONNDIR = $(LIBS)/conn -SERIAL = $(LIBS)/serial SHELLDIR = $(LIBS)/dvxshell PROXYDIR = $(ROOT)/src/tools/proxy SERTEST = $(ROOT)/src/test/serial @@ -69,18 +72,18 @@ SHIM = -include $(SERTEST)/loopShim.h XFER_OBJS = $(OBJDIR)/xfer.o $(OBJDIR)/xferZmodem.o # The connection library (Telnet and TCP use the host's own sockets), and # the BASIC faces of both libraries (these need libdvx) -CONN_OBJS = $(OBJDIR)/conn.o $(OBJDIR)/connTelnet.o +CONN_OBJS = $(OBJDIR)/conn.o $(OBJDIR)/connTelnet.o $(OBJDIR)/hostLookup.o BAS_OBJS = $(OBJDIR)/connBas.o $(OBJDIR)/xferBas.o # The file operations library (the Terminal makes its download folder) FILEOPS_OBJS = $(patsubst $(FILEOPS)/%.c,$(OBJDIR)/fileops/%.o,$(wildcard $(FILEOPS)/*.c)) # The serial stack over the loopback shim (clock() redirected), and over # the proxy's socket shim. -LOOP_OBJS = $(OBJDIR)/loopShim.o $(OBJDIR)/loopPacket.o $(OBJDIR)/loopSecLink.o $(OBJDIR)/secHash.o $(OBJDIR)/security.o -SOCK_OBJS = $(OBJDIR)/sockShim.o $(OBJDIR)/sockPacket.o $(OBJDIR)/sockSecLink.o $(OBJDIR)/secHash.o $(OBJDIR)/security.o $(OBJDIR)/proxyMain.o +LOOP_OBJS = $(OBJDIR)/loopShim.o $(OBJDIR)/loopPacket.o $(OBJDIR)/loopSecLink.o $(OBJDIR)/secCurve.o $(OBJDIR)/secHash.o $(OBJDIR)/security.o $(OBJDIR)/monocypher.o +SOCK_OBJS = $(OBJDIR)/sockShim.o $(OBJDIR)/sockPacket.o $(OBJDIR)/sockSecLink.o $(OBJDIR)/secCurve.o $(OBJDIR)/secHash.o $(OBJDIR)/security.o $(OBJDIR)/monocypher.o $(OBJDIR)/proxyMain.o # Every shell source (the app test runs the real shell). SHELL_OBJS = $(patsubst $(SHELLDIR)/%.c,$(OBJDIR)/shell/%.o,$(wildcard $(SHELLDIR)/*.c)) $(OBJDIR)/shell/shellFixture.o -BINS = $(OBJDIR)/testXfer $(OBJDIR)/testLrzsz $(OBJDIR)/testConn $(OBJDIR)/testConnProxy $(OBJDIR)/testTelnet $(OBJDIR)/testTermApp +BINS = $(OBJDIR)/testXfer $(OBJDIR)/testLrzsz $(OBJDIR)/testConn $(OBJDIR)/testConnProxy $(OBJDIR)/testTelnet $(OBJDIR)/testConnNoNet $(OBJDIR)/testTermApp .PHONY: all build run clean @@ -97,6 +100,7 @@ run: build cd $(OBJDIR) && ASAN_OPTIONS=detect_leaks=1 ./testConn cd $(OBJDIR) && ASAN_OPTIONS=detect_leaks=1 ./testConnProxy cd $(OBJDIR) && ASAN_OPTIONS=detect_leaks=1 ./testTelnet + cd $(OBJDIR) && ASAN_OPTIONS=detect_leaks=1 ./testConnNoNet cd $(OBJDIR) && ASAN_OPTIONS=detect_leaks=1:detect_odr_violation=0 ./testTermApp $(OBJDIR)/testXfer: testXfer.c $(XFER_OBJS) $(DVXTEST_LIBDVX_A) $(DVX_MAKEFILES) | dvxtest-libs @@ -109,15 +113,20 @@ $(OBJDIR)/testLrzsz: testLrzsz.c $(XFER_OBJS) $(DVXTEST_LIBDVX_A) $(DVX_MAKEFILE $(OBJDIR)/testConn: testConn.c $(CONN_OBJS) $(BAS_OBJS) $(LOOP_OBJS) $(XFER_OBJS) $(DVXTEST_LIBDVX_A) $(DVX_MAKEFILES) | dvxtest-libs @mkdir -p $(OBJDIR) - $(HOSTCC) $(CFLAGS) -I$(SERIAL) -o $@ testConn.c $(CONN_OBJS) $(BAS_OBJS) $(LOOP_OBJS) $(XFER_OBJS) $(HOST_LDFLAGS) -rdynamic $(DVXTEST_LIBDVX_A) -ldl -lm + $(HOSTCC) $(CFLAGS) -o $@ testConn.c $(CONN_OBJS) $(BAS_OBJS) $(LOOP_OBJS) $(XFER_OBJS) $(HOST_LDFLAGS) -rdynamic $(DVXTEST_LIBDVX_A) -ldl -lm $(OBJDIR)/testConnProxy: testConnProxy.c $(CONN_OBJS) $(SOCK_OBJS) $(XFER_OBJS) $(DVXTEST_LIBDVX_A) $(DVX_MAKEFILES) | dvxtest-libs @mkdir -p $(OBJDIR) - $(HOSTCC) $(CFLAGS) -I$(SERIAL) -I$(PROXYDIR) -o $@ testConnProxy.c $(CONN_OBJS) $(SOCK_OBJS) $(XFER_OBJS) $(HOST_LDFLAGS) $(DVXTEST_LIBDVX_A) -ldl -lm + $(HOSTCC) $(CFLAGS) -I$(PROXYDIR) -o $@ testConnProxy.c $(CONN_OBJS) $(SOCK_OBJS) $(XFER_OBJS) $(HOST_LDFLAGS) -rdynamic $(DVXTEST_LIBDVX_A) -ldl -lm $(OBJDIR)/testTelnet: testTelnet.c $(CONN_OBJS) $(LOOP_OBJS) $(DVX_MAKEFILES) @mkdir -p $(OBJDIR) - $(HOSTCC) $(CFLAGS) -o $@ testTelnet.c $(CONN_OBJS) $(LOOP_OBJS) $(HOST_LDFLAGS) + $(HOSTCC) $(CFLAGS) -o $@ testTelnet.c $(CONN_OBJS) $(LOOP_OBJS) $(HOST_LDFLAGS) -rdynamic -ldl + +# No hostLookup.o and no -rdynamic: the networking calls are not there. +$(OBJDIR)/testConnNoNet: testConnNoNet.c $(OBJDIR)/conn.o $(OBJDIR)/connTelnet.o $(LOOP_OBJS) $(DVX_MAKEFILES) + @mkdir -p $(OBJDIR) + $(HOSTCC) $(CFLAGS) -o $@ testConnNoNet.c $(OBJDIR)/conn.o $(OBJDIR)/connTelnet.o $(LOOP_OBJS) $(HOST_LDFLAGS) -ldl # The BASIC stub resolves the runtime and the libraries from the # executable (-rdynamic). @@ -149,6 +158,10 @@ $(OBJDIR)/connTelnet.o: $(CONNDIR)/connTelnet.c $(DVX_MAKEFILES) @mkdir -p $(OBJDIR) $(HOSTCC) $(CFLAGS) -c -o $@ $< +$(OBJDIR)/hostLookup.o: $(ROOT)/src/test/dvxtest/hostLookup.c $(DVX_MAKEFILES) + @mkdir -p $(OBJDIR) + $(HOSTCC) $(CFLAGS) -c -o $@ $< + $(OBJDIR)/fileops/%.o: $(FILEOPS)/%.c $(DVX_MAKEFILES) @mkdir -p $(OBJDIR)/fileops $(HOSTCC) $(CFLAGS) -c -o $@ $< @@ -157,19 +170,27 @@ $(OBJDIR)/loopShim.o: $(SERTEST)/loopShim.c $(DVX_MAKEFILES) @mkdir -p $(OBJDIR) $(HOSTCC) $(CFLAGS) -c -o $@ $< -$(OBJDIR)/loopPacket.o: $(SERIAL)/packet/packet.c $(DVX_MAKEFILES) +$(OBJDIR)/loopPacket.o: $(LIBS)/seclink/packet.c $(DVX_MAKEFILES) @mkdir -p $(OBJDIR) $(HOSTCC) $(CFLAGS) $(SHIM) -c -o $@ $< -$(OBJDIR)/loopSecLink.o: $(SERIAL)/seclink/secLink.c $(DVX_MAKEFILES) +$(OBJDIR)/loopSecLink.o: $(LIBS)/seclink/secLink.c $(DVX_MAKEFILES) @mkdir -p $(OBJDIR) $(HOSTCC) $(CFLAGS) $(SHIM) -c -o $@ $< -$(OBJDIR)/secHash.o: $(SERIAL)/security/secHash.c $(DVX_MAKEFILES) +$(OBJDIR)/secCurve.o: $(LIBS)/crypto/secCurve.c $(DVX_MAKEFILES) + @mkdir -p $(OBJDIR) + $(HOSTCC) $(CFLAGS) -I$(MONO) -c -o $@ $< + +$(OBJDIR)/secHash.o: $(LIBS)/crypto/secHash.c $(DVX_MAKEFILES) @mkdir -p $(OBJDIR) $(HOSTCC) $(CFLAGS) -c -o $@ $< -$(OBJDIR)/security.o: $(SERIAL)/security/security.c $(DVX_MAKEFILES) +$(OBJDIR)/monocypher.o: $(MONO)/monocypher.c $(DVX_MAKEFILES) + @mkdir -p $(OBJDIR) + $(HOSTCC) $(CFLAGS) -I$(MONO) -c -o $@ $< + +$(OBJDIR)/security.o: $(LIBS)/crypto/security.c $(DVX_MAKEFILES) @mkdir -p $(OBJDIR) $(HOSTCC) $(CFLAGS) -c -o $@ $< @@ -177,11 +198,11 @@ $(OBJDIR)/sockShim.o: $(PROXYDIR)/sockShim.c $(DVX_MAKEFILES) @mkdir -p $(OBJDIR) $(HOSTCC) $(CFLAGS) -c -o $@ $< -$(OBJDIR)/sockPacket.o: $(SERIAL)/packet/packet.c $(DVX_MAKEFILES) +$(OBJDIR)/sockPacket.o: $(LIBS)/seclink/packet.c $(DVX_MAKEFILES) @mkdir -p $(OBJDIR) $(HOSTCC) $(CFLAGS) -include $(PROXYDIR)/sockShim.h -c -o $@ $< -$(OBJDIR)/sockSecLink.o: $(SERIAL)/seclink/secLink.c $(DVX_MAKEFILES) +$(OBJDIR)/sockSecLink.o: $(LIBS)/seclink/secLink.c $(DVX_MAKEFILES) @mkdir -p $(OBJDIR) $(HOSTCC) $(CFLAGS) -include $(PROXYDIR)/sockShim.h -c -o $@ $< diff --git a/src/test/terminal/testConn.c b/src/test/terminal/testConn.c index 7ba9d62e..9616fdfa 100644 --- a/src/test/terminal/testConn.c +++ b/src/test/terminal/testConn.c @@ -34,7 +34,7 @@ #include "../serial/loopShim.h" #include "conn.h" #include "xfer.h" -#include "../../libs/kpunch/serial/seclink/secLink.h" +#include "../../libs/kpunch/seclink/secLink.h" #include "dvxPlatHost.h" #include "dvxBas.h" @@ -102,6 +102,7 @@ void ConnSetXferWindow(int32_t handle, int32_t bytes); int32_t ConnState(int32_t handle); const char *ConnStatus(int32_t handle); int32_t ConnStatusSerial(int32_t handle); +int32_t ConnTypeAvailable(int32_t type); int32_t ConnWrite(int32_t handle, const uint8_t *data, int32_t len); int32_t ConnZmodemRequest(int32_t handle); void XferAnswer(int32_t handle, int32_t action); @@ -123,9 +124,11 @@ const char *XferMessage(int32_t handle); int32_t XferPoll(int32_t handle); int32_t XferProtocol(int32_t handle); int32_t XferReceive(int32_t com, int32_t proto, const char *target); +int32_t XferReceiveFiles(int32_t com, int32_t proto, const char *target, int32_t exists, int32_t escapeCtl); int32_t XferResumedAt(int32_t handle); const char *XferSafeName(const char *fileName, const char *folder); int32_t XferSendFile(int32_t com, int32_t proto, const char *path); +int32_t XferSendFiles(int32_t com, int32_t proto, const char *files, int32_t escapeCtl, int32_t resumeFiles); int32_t XferSending(int32_t handle); int32_t XferSize(int32_t handle); int32_t XferStatus(int32_t handle); @@ -169,8 +172,10 @@ static int32_t sPeerLen; static PeerRecvT sPeerRecv; -// The BASIC functions: a ZMODEM send and a YMODEM receive on COM1 (port -// 0) with the test's engine on the other end, and the error returns. +// The BASIC functions: a ZMODEM send, a two-file batch, a YMODEM receive +// (a file there renamed, then overwritten as asked) on COM1 (port 0) with +// the test's engine on the other end, the list split, and the error +// returns. static void caseBasic(void) { XferT *peer = xferCreate(); XferIoT io; @@ -237,8 +242,60 @@ static void caseBasic(void) { TEST_MEM_EQ("bas recv bytes", back, data, XFER_SIZE); } + // A batch of two (a line with CR LF, an empty line), sent escaping + // control characters: the receiver gets both + unlink(TMP_DIR "/bas2/BAS.DAT"); + unlink(TMP_DIR "/bas2/BAS2.DAT"); + mkdir(TMP_DIR "/bas2", 0755); + fp = fopen(TMP_DIR "/BAS2.DAT", "wb"); + fwrite(data, 1, XFER_SIZE / 2, fp); + fclose(fp); + h = XferSendFiles(LOCAL_COM + 1, XferZmodemE, TMP_DIR "/BAS.DAT\r\n\n" TMP_DIR "/BAS2.DAT\n", -1, 0); + TEST_TRUE("bas batch handle", h > 0); + TEST_EQ("bas batch count", XferFileCount(h), 2); + xferStartReceive(peer, XferZmodemE, &io, NULL, TMP_DIR "/bas2", 0); + + for (int32_t i = 0; i < LIMIT_MS / TICK_MS && (XferPoll(h) == XferRunningE || xferProgress(peer)->status == XferRunningE); i++) { + xferPoll(peer, (uint32_t)(i * TICK_MS)); + hostClockAdvance(TICK_MS); + } + + TEST_EQ("bas batch done", XferPoll(h), XferDoneE); + TEST_EQ("bas batch received", xferProgress(peer)->status, XferDoneE); + XferClose(h); + fp = fopen(TMP_DIR "/bas2/BAS2.DAT", "rb"); + TEST_TRUE("bas batch second file", fp != NULL); + + if (fp) { + fclose(fp); + } + + // XFER_EXIST_OVERWRITE: BAS.DAT is there, and is replaced + h = XferReceiveFiles(LOCAL_COM + 1, XferYmodemE, TMP_DIR "/bas", XferExistOverwriteE, 0); + TEST_TRUE("bas recv overwrite handle", h > 0); + xferStartSend(peer, XferYmodemE, &io, NULL, peerFiles, 1, 0); + + for (int32_t i = 0; i < LIMIT_MS / TICK_MS && (XferPoll(h) == XferRunningE || xferProgress(peer)->status == XferRunningE); i++) { + xferPoll(peer, (uint32_t)(i * TICK_MS)); + hostClockAdvance(TICK_MS); + } + + TEST_EQ("bas recv overwrite done", XferPoll(h), XferDoneE); + TEST_TRUE("bas recv overwrite name", strstr(XferFile(h), "BAS.DAT") != NULL && strstr(XferFile(h), "~") == NULL); + XferClose(h); + + // The list split: CR LF, empty lines, and no line end at the end + { + char list[] = "a\r\n\nb\nc"; + const char *paths[XFER_MAX_FILES] = { NULL }; + + TEST_EQ("split count", xferSplitList(list, paths), 3); + TEST_TRUE("split items", strcmp(paths[0], "a") == 0 && strcmp(paths[1], "b") == 0 && strcmp(paths[2], "c") == 0); + } + // Bad ports, files and handles TEST_EQ("bas bad com low", XferSendFile(0, XferZmodemE, path), 0); + TEST_EQ("bas bad batch", XferSendFiles(LOCAL_COM + 1, XferZmodemE, "", 0, 0), 0); TEST_EQ("bas bad com high", XferReceive(RS232_NUM_PORTS + 1, XferZmodemE, TMP_DIR), 0); TEST_EQ("bas missing file", XferSendFile(LOCAL_COM + 1, XferZmodemE, TMP_DIR "/NOPE.DAT"), 0); TEST_EQ("bas bad handle poll", XferPoll(BAS_BAD_HANDLE), XferIdleE); @@ -443,8 +500,12 @@ static void caseConnBasic(void) { TEST_EQ("cb bad read", len, 0); ConnSetXferWindow(BAS_BAD_HANDLE, 0); TEST_EQ("cb bad ask remote", XferAskRemoteSize(BAS_BAD_HANDLE), -1); - TEST_TRUE("cb error text", strcmp(ConnErrorText(CONN_ERR_HOST), connErrorText(CONN_ERR_HOST)) == 0); + TEST_TRUE("cb error text", strcmp(ConnErrorText(CONN_ERR_PORT), connErrorText(CONN_ERR_PORT)) == 0); TEST_TRUE("cb error text ok", strcmp(ConnErrorText(CONN_OK), connErrorText(CONN_OK)) == 0); + TEST_TRUE("cb error text no net", strcmp(ConnErrorText(CONN_ERR_NO_NET), connErrorText(CONN_ERR_NO_NET)) == 0); + TEST_EQ("cb telnet available", ConnTypeAvailable(ConnTelnetE), BAS_TRUE); + TEST_EQ("cb serial available", ConnTypeAvailable(ConnSerialE), BAS_TRUE); + TEST_EQ("cb bad type unavailable", ConnTypeAvailable(ConnTypeCountE), BAS_FALSE); TEST_EQ("cb crc16", XferCrc16((const uint8_t *)CRC_CHECK_TEXT, CRC_CHECK_LEN), CRC16_CHECK); TEST_EQ("cb crc32", (uint32_t)XferCrc32((const uint8_t *)CRC_CHECK_TEXT, CRC_CHECK_LEN), CRC32_CHECK); TEST_EQ("cb crc32 zero byte", (uint32_t)XferCrc32((const uint8_t *)"a\0b", 3), xferCrc32((const uint8_t *)"a\0b", 3)); diff --git a/src/test/terminal/testConnNoNet.c b/src/test/terminal/testConnNoNet.c new file mode 100644 index 00000000..51a60fb4 --- /dev/null +++ b/src/test/terminal/testConnNoNet.c @@ -0,0 +1,91 @@ +// 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. + +// testConnNoNet.c -- the connection library without the networking +// library (an optional part of DVX): built without the lookups and +// without exporting anything, so conn.c finds no network calls. Telnet +// is not available, a network connection ends at once saying why, and a +// serial port opens as ever. + +#include "../serial/serialTest.h" +#include "../serial/loopShim.h" +#include "conn.h" + +#include + +#define NO_NET_TEXT "Networking is not installed" +#define REMOTE_HOST "bbs.example.org" + +// ============================================================ +// Prototypes +// ============================================================ + +static void caseNetworkConnections(void); +static void caseSerialStillOpens(void); +int main(void); + + +// Telnet, and a secure link over the network, end at once. +static void caseNetworkConnections(void) { + static const int32_t netTypes[] = { ConnTelnetE, ConnSecureE }; + ConnT *c = connCreate(); + ConnConfigT cfg; + + TEST_TRUE("telnet unavailable", !connTypeAvailable(ConnTelnetE)); + TEST_TRUE("serial available", connTypeAvailable(ConnSerialE)); + TEST_TRUE("modem available", connTypeAvailable(ConnModemE)); + TEST_TRUE("secure available", connTypeAvailable(ConnSecureE)); + TEST_TRUE("error text", strcmp(connErrorText(CONN_ERR_NO_NET), NO_NET_TEXT) == 0); + + for (int32_t i = 0; i < (int32_t)(sizeof(netTypes) / sizeof(netTypes[0])); i++) { + connConfigDefaults(&cfg); + cfg.type = netTypes[i]; + snprintf(cfg.host, sizeof(cfg.host), "%s", REMOTE_HOST); + TEST_EQ("network open", connOpen(c, &cfg, 0), CONN_ERR_NO_NET); + TEST_EQ("network closed", connState(c), ConnClosedE); + TEST_TRUE("network status", strcmp(connStatus(c), NO_NET_TEXT) == 0); + connPoll(c, 0); + TEST_EQ("network still closed", connState(c), ConnClosedE); + } + + connDestroy(c); +} + + +// A serial connection does not need the networking library. +static void caseSerialStillOpens(void) { + ConnT *c = connCreate(); + ConnConfigT cfg; + + loopReset(); + connConfigDefaults(&cfg); + TEST_EQ("serial open", connOpen(c, &cfg, 0), CONN_OK); + TEST_TRUE("serial not closed", connState(c) != ConnClosedE); + connDestroy(c); +} + + +int main(void) { + caseNetworkConnections(); + caseSerialStillOpens(); + return testSummary(); +} diff --git a/src/test/terminal/testConnProxy.c b/src/test/terminal/testConnProxy.c index 60dbf597..43ca1f51 100644 --- a/src/test/terminal/testConnProxy.c +++ b/src/test/terminal/testConnProxy.c @@ -98,14 +98,13 @@ static void relay(bool overTcp); static bool runXfer(ConnT *c, XferT *a, XferT *b); int main(void); -// secLink over the socket shim needs only open/close/read/write; the -// connection library also links the serial and modem calls, which the -// secure-link path never makes. +// secLink over the socket shim needs only what the shim gives (open, +// close, read, buffered write and its pending count); the connection +// library also links the serial and modem calls, which the secure-link +// path never makes. int32_t rs232GetDcd(int32_t com); -int32_t rs232GetTxBuffered(int32_t com); int32_t rs232SetBreak(int32_t com, bool on); int32_t rs232SetDtr(int32_t com, bool dtr); -int32_t rs232WriteBuf(int32_t com, const char *data, int32_t len); static int32_t bbsRead(void *ctx, uint8_t *buf, int32_t maxLen) { @@ -508,12 +507,6 @@ int32_t rs232GetDcd(int32_t com) { } -int32_t rs232GetTxBuffered(int32_t com) { - (void)com; - return 0; -} - - int32_t rs232SetBreak(int32_t com, bool on) { (void)com; (void)on; @@ -529,11 +522,6 @@ int32_t rs232SetDtr(int32_t com, bool dtr) { } -int32_t rs232WriteBuf(int32_t com, const char *data, int32_t len) { - return rs232Write(com, data, len); -} - - // Run two transfer engines (and the connection) until both end. static bool runXfer(ConnT *c, XferT *a, XferT *b) { uint32_t start = nowMs(); diff --git a/src/test/terminal/testTelnet.c b/src/test/terminal/testTelnet.c index 16925e32..d97c984f 100644 --- a/src/test/terminal/testTelnet.c +++ b/src/test/terminal/testTelnet.c @@ -209,10 +209,13 @@ static void caseFailures(void) { TEST_EQ("no host", connOpen(c, &cfg, nowMs()), CONN_ERR_PARAM); TEST_TRUE("no host status", strcmp(connStatus(c), "No host name is set") == 0); + // The host is looked up while the connection is polled snprintf(cfg.host, sizeof(cfg.host), "%s", "no-such-host.invalid"); - TEST_EQ("unknown host", connOpen(c, &cfg, nowMs()), CONN_ERR_HOST); + TEST_EQ("unknown host", connOpen(c, &cfg, nowMs()), CONN_OK); + TEST_EQ("unknown host opening", connState(c), ConnOpeningE); + connPoll(c, nowMs()); + TEST_EQ("unknown host closed", connState(c), ConnClosedE); TEST_TRUE("unknown host status", strcmp(connStatus(c), "The host no-such-host.invalid was not found") == 0); - TEST_TRUE("host error text", strcmp(connErrorText(CONN_ERR_HOST), "The host could not be reached") == 0); TEST_EQ("telnet cannot break", connCanBreak(ConnTelnetE), false); connDestroy(c); } diff --git a/src/test/terminal/testTermApp.c b/src/test/terminal/testTermApp.c index 68c3882a..74754723 100644 --- a/src/test/terminal/testTermApp.c +++ b/src/test/terminal/testTermApp.c @@ -127,6 +127,7 @@ typedef struct { ScriptStepT steps[SCRIPT_STEPS_MAX]; int32_t count; int32_t answered; + int32_t answeredId; // the window answered last, until it closes (the program closes it on its own task) } ScriptT; // The settings the program starts with: a serial BBS and a secure link @@ -765,6 +766,10 @@ static void menu(WindowT *win, const char *caption) { ctx->currentAppId = win->appId; win->onMenu(win, id); ctx->currentAppId = saved; + + // The program runs the command on its own task: until it is done (a + // dialog it opens answered meanwhile) + fixPumpSettled(); } @@ -920,7 +925,11 @@ static void scriptRun(AppContextT *ctx, WindowT *modal, void *userCtx) { (void)ctx; - if (!modal || s->answered >= s->count) { + if (!modal || modal->id != s->answeredId) { + s->answeredId = 0; + } + + if (!modal || modal->id == s->answeredId || s->answered >= s->count) { return; } @@ -931,6 +940,7 @@ static void scriptRun(AppContextT *ctx, WindowT *modal, void *userCtx) { } s->answered++; + s->answeredId = modal->id; if (step->prepare) { step->prepare(modal); @@ -942,7 +952,8 @@ static void scriptRun(AppContextT *ctx, WindowT *modal, void *userCtx) { // Answer modal windows with s's steps from now on. static void scriptStart(ScriptT *s) { - s->answered = 0; + s->answered = 0; + s->answeredId = 0; tDialogScript(scriptRun, s); } diff --git a/src/test/terminal/testXfer.c b/src/test/terminal/testXfer.c index 9e854cf8..7622f370 100644 --- a/src/test/terminal/testXfer.c +++ b/src/test/terminal/testXfer.c @@ -56,6 +56,15 @@ #define CRC_PKT (PKT_HDR + SMALL_BLOCK + 2) #define SEED_BASE 0x5EED0001u #define BIG_FILE 200000 +#define ZM_TYPE_RINIT 1 // ZMODEM header types the CRC test plays a receiver with +#define ZM_TYPE_CRC 13 +#define ZM_HDR_BYTES 4 +#define BYTE_BITS 8 +#define BYTE_MASK 0xFF +#define ZM_HEX_LEAD "**\x18" "B" +#define ZM_HEX_HEADER_MAX 24 // lead, type, 4 bytes and CRC as hex, CR LF, XON +#define ZM_REPLY_POLLS_MAX 1000 // polls the sender may take to answer +#define ZM_REPLY_POLLS_MIN 3 // ... and at least (the reply is worked out a piece a poll) #define MID_FILE 70000 #define NOISE_RUNS 25 #define EARLY_MS 20000 // receiver runs alone this long (several start requests) @@ -108,6 +117,7 @@ static void caseXmodemSizes(void); static void caseYmodemBatch(void); static void caseYmodemHeader(void); static void caseZmodemBatch(void); +static void caseZmodemCrc(void); static void caseZmodemEscape(void); static void caseZmodemResume(void); static void caseZmodemWindow(void); @@ -127,6 +137,7 @@ static bool running(const XferT *x); static void runPair(XferT *a, XferT *b, uint32_t *now, uint32_t limitMs); static void setupIo(XferIoT *io, PipeT *in, PipeT *out); static bool transferOne(XferProtoE proto, int32_t size, uint32_t seed); +static int32_t zmHexHeader(char *out, uint8_t type, const uint8_t hdr[ZM_HDR_BYTES]); int main(void); @@ -849,6 +860,70 @@ static void caseZmodemBatch(void) { } +// A receiver that asks for the file's CRC (ZCRC: crash recovery) gets it, +// worked out a piece a poll (no one poll reads the whole file). +static void caseZmodemCrc(void) { + static const uint8_t zero[ZM_HDR_BYTES] = { 0, 0, 0, 0 }; + XferT *s = xferCreate(); + XferIoT ioS; + uint32_t now = 0; + const char *files[1] = { SEND_DIR "/CRC.DAT" }; + uint8_t *data = (uint8_t *)malloc(BIG_FILE); + uint8_t *wire = (uint8_t *)malloc(PIPE_SIZE); + char hdr[ZM_HEX_HEADER_MAX]; + char want[ZM_HEX_HEADER_MAX]; + uint8_t crcBytes[ZM_HDR_BYTES]; + int32_t got = 0; + int32_t polls = 0; + FILE *f; + uint32_t crc; + + cleanDir(SEND_DIR); + makeFile(files[0], BIG_FILE, SEED_BASE + 70); + f = fopen(files[0], "rb"); + TEST_TRUE("crc read", f && data && wire && fread(data, 1, BIG_FILE, f) == BIG_FILE); + + if (f) { + fclose(f); + } + + crc = xferCrc32(data, BIG_FILE); + + for (int32_t i = 0; i < ZM_HDR_BYTES; i++) { + crcBytes[i] = (uint8_t)(crc >> (i * BYTE_BITS)); + } + + zmHexHeader(want, ZM_TYPE_CRC, crcBytes); + pipeInit(&sAtoB); + pipeInit(&sBtoA); + setupIo(&ioS, &sBtoA, &sAtoB); + xferStartSend(s, XferZmodemE, &ioS, NULL, files, 1, now); + xferPoll(s, now); + + // ZRINIT: the sender offers the file; then ZCRC for all of it + got = zmHexHeader(hdr, ZM_TYPE_RINIT, zero); + pipePut(&sBtoA, (const uint8_t *)hdr, got); + xferPoll(s, ++now); + pipeTakeAll(&sAtoB, wire, PIPE_SIZE); + got = zmHexHeader(hdr, ZM_TYPE_CRC, zero); + pipePut(&sBtoA, (const uint8_t *)hdr, got); + got = 0; + + while (got == 0 && polls < ZM_REPLY_POLLS_MAX && xferProgress(s)->status == XferRunningE) { + xferPoll(s, ++now); + polls++; + got = pipeTakeAll(&sAtoB, wire, PIPE_SIZE); + } + + TEST_TRUE("crc reply sliced", polls >= ZM_REPLY_POLLS_MIN); + TEST_TRUE("crc reply", got >= (int32_t)strlen(want) && memcmp(wire, want, strlen(want)) == 0); + xferCancel(s); + xferDestroy(s); + free(data); + free(wire); +} + + // Escaping every control character on both sides still carries all 256 // byte values. static void caseZmodemEscape(void) { @@ -1254,6 +1329,21 @@ static bool transferOne(XferProtoE proto, int32_t size, uint32_t seed) { } +// A ZMODEM hex header as a receiver sends it (lowercase hex, CRC-16 over +// the type and the four bytes); its length. +static int32_t zmHexHeader(char *out, uint8_t type, const uint8_t hdr[ZM_HDR_BYTES]) { + uint8_t body[1 + ZM_HDR_BYTES]; + uint16_t crc; + int32_t len; + + body[0] = type; + memcpy(body + 1, hdr, ZM_HDR_BYTES); + crc = xferCrc16(body, (int32_t)sizeof(body)); + len = snprintf(out, ZM_HEX_HEADER_MAX, ZM_HEX_LEAD "%02x%02x%02x%02x%02x%02x%02x", body[0], body[1], body[2], body[3], body[4], (unsigned)(crc >> BYTE_BITS), (unsigned)(crc & BYTE_MASK)); + return len; +} + + int main(void) { caseCrc(); caseSafeName(); @@ -1265,6 +1355,7 @@ int main(void) { caseYmodemBatch(); caseYmodemHeader(); caseZmodemBatch(); + caseZmodemCrc(); caseZmodemEscape(); caseZmodemWindow(); caseShortWrites(); diff --git a/src/test/tools/dfnReadTest.c b/src/test/tools/dfnReadTest.c index f28561a5..80de1a88 100644 --- a/src/test/tools/dfnReadTest.c +++ b/src/test/tools/dfnReadTest.c @@ -39,6 +39,7 @@ // number of failures (2 for usage). #include "dvxFontFmt.h" +#include "dvxHash.h" #include #include @@ -50,8 +51,6 @@ #define WRAP_BEFORE 1000 // bytes put before a font inside another file #define WRAP_AFTER 300 // and after it #define WRAP_FILL 0xA5 -#define FNV_OFFSET 2166136261u // FNV-1a, 32 bits -#define FNV_PRIME 16777619u // A damage done to a copy of the file and the reason the reader must give typedef struct { @@ -74,7 +73,6 @@ static void damage(uint8_t *data, int32_t *len, const char *name); static int32_t dump(const char *path); static void expectFail(const char *what, bool ok, const char *err, const char *reason); static bool fingerprint(const DfnSourceT *src, uint32_t *hash, char *err, int32_t errSize); -static uint32_t fnv(uint32_t hash, const void *data, size_t len); static uint8_t *readAll(const char *path, int32_t *len); static int32_t sources(const char *path, const char *dir); static bool writeAll(const char *path, const uint8_t *data, int32_t len); @@ -402,8 +400,8 @@ static bool fingerprint(const DfnSourceT *src, uint32_t *hash, char *err, int32_ return false; } - *hash = fnv(FNV_OFFSET, &f.header, sizeof(f.header)); - *hash = fnv(*hash, f.strikes, sizeof(f.strikes[0]) * (size_t)f.header.strikeCount); + *hash = dvxFnvBytes(DVX_FNV_BASIS, &f.header, (int32_t)sizeof(f.header)); + *hash = dvxFnvBytes(*hash, f.strikes, (int32_t)sizeof(f.strikes[0]) * f.header.strikeCount); for (int32_t i = 0; i < f.header.strikeCount; i++) { DfnStrikeDataT s; @@ -412,8 +410,8 @@ static bool fingerprint(const DfnSourceT *src, uint32_t *hash, char *err, int32_ return false; } - *hash = fnv(*hash, s.glyphs, sizeof(s.glyphs)); - *hash = fnv(*hash, s.bitmaps, s.info.bitmapBytes); + *hash = dvxFnvBytes(*hash, s.glyphs, (int32_t)sizeof(s.glyphs)); + *hash = dvxFnvBytes(*hash, s.bitmaps, (int32_t)s.info.bitmapBytes); dfnFreeStrike(&s); } @@ -424,7 +422,7 @@ static bool fingerprint(const DfnSourceT *src, uint32_t *hash, char *err, int32_ return false; } - *hash = fnv(*hash, text, strlen(text)); + *hash = dvxFnvBytes(*hash, text, (int32_t)strlen(text)); free(text); } @@ -432,17 +430,6 @@ static bool fingerprint(const DfnSourceT *src, uint32_t *hash, char *err, int32_ } -static uint32_t fnv(uint32_t hash, const void *data, size_t len) { - const uint8_t *p = (const uint8_t *)data; - - for (size_t i = 0; i < len; i++) { - hash = (hash ^ p[i]) * FNV_PRIME; - } - - return hash; -} - - // The whole file (malloc'd), or NULL static uint8_t *readAll(const char *path, int32_t *len) { FILE *fp = fopen(path, "rb"); diff --git a/src/test/tools/fixtures/genFixtures.py b/src/test/tools/fixtures/genFixtures.py index 48c32734..bf2f5622 100644 --- a/src/test/tools/fixtures/genFixtures.py +++ b/src/test/tools/fixtures/genFixtures.py @@ -1,4 +1,26 @@ #!/usr/bin/env python3 +# 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. + # genFixtures.py -- regenerates the binary fixtures used by runTools.sh. # Everything here is deterministic so the checked-in files never change. diff --git a/src/test/tools/fixtures/help/badDirective.dhs b/src/test/tools/fixtures/help/badDirective.dhs index 579fa282..79baa7e5 100644 --- a/src/test/tools/fixtures/help/badDirective.dhs +++ b/src/test/tools/fixtures/help/badDirective.dhs @@ -1,3 +1,25 @@ +# 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. + .topic neg.bad .title Bad .bogus this directive does not exist diff --git a/src/test/tools/fixtures/help/brokenLink.dhs b/src/test/tools/fixtures/help/brokenLink.dhs index e4a006c3..dc265066 100644 --- a/src/test/tools/fixtures/help/brokenLink.dhs +++ b/src/test/tools/fixtures/help/brokenLink.dhs @@ -1,3 +1,25 @@ +# 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. + # brokenLink.dhs -- .link targets that name no topic in the compile. .topic neg.link .title Broken Link diff --git a/src/test/tools/fixtures/help/brokenLinkInc.dhs b/src/test/tools/fixtures/help/brokenLinkInc.dhs index e435c28d..39c8993f 100644 --- a/src/test/tools/fixtures/help/brokenLinkInc.dhs +++ b/src/test/tools/fixtures/help/brokenLinkInc.dhs @@ -1,2 +1,24 @@ +# 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. + # brokenLinkInc.dhs -- included by brokenLink.dhs; its miss names this file. .link neg.alsoMissing Missing target from an included file diff --git a/src/test/tools/fixtures/help/codeLink.dhs b/src/test/tools/fixtures/help/codeLink.dhs index e8adf25e..22617fcd 100644 --- a/src/test/tools/fixtures/help/codeLink.dhs +++ b/src/test/tools/fixtures/help/codeLink.dhs @@ -1,3 +1,25 @@ +# 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. + # codeLink.dhs -- a .link line inside .code is example text. .topic cl.one .title Code Link diff --git a/src/test/tools/fixtures/help/extra.dhs b/src/test/tools/fixtures/help/extra.dhs index d3a7dae3..42b52c33 100644 --- a/src/test/tools/fixtures/help/extra.dhs +++ b/src/test/tools/fixtures/help/extra.dhs @@ -1,3 +1,25 @@ +# 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. + # extra.dhs -- second input file (listed via the @inputs.txt response file) .topic fx.extra .title Extra File diff --git a/src/test/tools/fixtures/help/imageNoName.dhs b/src/test/tools/fixtures/help/imageNoName.dhs index 0c7db9de..dc7de79c 100644 --- a/src/test/tools/fixtures/help/imageNoName.dhs +++ b/src/test/tools/fixtures/help/imageNoName.dhs @@ -1,3 +1,25 @@ +# 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. + .topic t1 Some text. .image diff --git a/src/test/tools/fixtures/help/imgKinds.dhs b/src/test/tools/fixtures/help/imgKinds.dhs index bde5c235..6972486a 100644 --- a/src/test/tools/fixtures/help/imgKinds.dhs +++ b/src/test/tools/fixtures/help/imgKinds.dhs @@ -1,3 +1,25 @@ +# 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. + .topic t1 .title Image Kinds .image anim.gif diff --git a/src/test/tools/fixtures/help/inc.dhs b/src/test/tools/fixtures/help/inc.dhs index b12f7c26..85d33a36 100644 --- a/src/test/tools/fixtures/help/inc.dhs +++ b/src/test/tools/fixtures/help/inc.dhs @@ -1,3 +1,25 @@ +# 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. + # inc.dhs -- pulled in by .include from main.dhs (same topic continues) .h2 Included Heading diff --git a/src/test/tools/fixtures/help/includeNoFile.dhs b/src/test/tools/fixtures/help/includeNoFile.dhs index 89e734dd..8c544673 100644 --- a/src/test/tools/fixtures/help/includeNoFile.dhs +++ b/src/test/tools/fixtures/help/includeNoFile.dhs @@ -1,3 +1,25 @@ +# 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. + .topic t1 Some text. .include diff --git a/src/test/tools/fixtures/help/indexNoKey.dhs b/src/test/tools/fixtures/help/indexNoKey.dhs index c38dacb6..7ef7bdbd 100644 --- a/src/test/tools/fixtures/help/indexNoKey.dhs +++ b/src/test/tools/fixtures/help/indexNoKey.dhs @@ -1,3 +1,25 @@ +# 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. + .topic t1 Some text. .index diff --git a/src/test/tools/fixtures/help/itemOutside.dhs b/src/test/tools/fixtures/help/itemOutside.dhs index 71978baf..5ff42d3a 100644 --- a/src/test/tools/fixtures/help/itemOutside.dhs +++ b/src/test/tools/fixtures/help/itemOutside.dhs @@ -1,3 +1,25 @@ +# 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. + .topic t1 .title Item Outside .item stray item diff --git a/src/test/tools/fixtures/help/linkA.dhs b/src/test/tools/fixtures/help/linkA.dhs index 63556cc6..0b5826d8 100644 --- a/src/test/tools/fixtures/help/linkA.dhs +++ b/src/test/tools/fixtures/help/linkA.dhs @@ -1,3 +1,25 @@ +# 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. + # linkA.dhs -- links resolved across input files and includes in one compile. .topic lk.a .title Link A diff --git a/src/test/tools/fixtures/help/linkB.dhs b/src/test/tools/fixtures/help/linkB.dhs index 155a5740..eb858d0f 100644 --- a/src/test/tools/fixtures/help/linkB.dhs +++ b/src/test/tools/fixtures/help/linkB.dhs @@ -1,3 +1,25 @@ +# 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. + # linkB.dhs -- second input file for linkA.dhs. .topic lk.b .title Link B diff --git a/src/test/tools/fixtures/help/linkInc.dhs b/src/test/tools/fixtures/help/linkInc.dhs index a4fc7211..8c9aa420 100644 --- a/src/test/tools/fixtures/help/linkInc.dhs +++ b/src/test/tools/fixtures/help/linkInc.dhs @@ -1,3 +1,25 @@ +# 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. + # linkInc.dhs -- included by linkA.dhs; defines a link target. .topic lk.inc .title Included Topic diff --git a/src/test/tools/fixtures/help/linkNoId.dhs b/src/test/tools/fixtures/help/linkNoId.dhs index b62e5278..60aecd57 100644 --- a/src/test/tools/fixtures/help/linkNoId.dhs +++ b/src/test/tools/fixtures/help/linkNoId.dhs @@ -1,3 +1,25 @@ +# 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. + .topic ln.one .title Link Without ID .link diff --git a/src/test/tools/fixtures/help/listEnd.dhs b/src/test/tools/fixtures/help/listEnd.dhs index dd1119fa..9ce58460 100644 --- a/src/test/tools/fixtures/help/listEnd.dhs +++ b/src/test/tools/fixtures/help/listEnd.dhs @@ -1,3 +1,25 @@ +# 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. + .topic t1 .title List End .list diff --git a/src/test/tools/fixtures/help/main.dhs b/src/test/tools/fixtures/help/main.dhs index bd1b59e2..c2d12993 100644 --- a/src/test/tools/fixtures/help/main.dhs +++ b/src/test/tools/fixtures/help/main.dhs @@ -1,3 +1,25 @@ +# 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. + # main.dhs -- positive fixture exercising every dvxhlpc directive. # Comment lines like this one produce no output. diff --git a/src/test/tools/fixtures/help/missingImage.dhs b/src/test/tools/fixtures/help/missingImage.dhs index ef25e923..e9a3ae60 100644 --- a/src/test/tools/fixtures/help/missingImage.dhs +++ b/src/test/tools/fixtures/help/missingImage.dhs @@ -1,3 +1,25 @@ +# 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. + .topic neg.img .title Missing Image .image nothere.png diff --git a/src/test/tools/fixtures/help/nestedSection.dhs b/src/test/tools/fixtures/help/nestedSection.dhs index 6d74966f..52f5ff25 100644 --- a/src/test/tools/fixtures/help/nestedSection.dhs +++ b/src/test/tools/fixtures/help/nestedSection.dhs @@ -1,3 +1,25 @@ +# 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. + .section Dev|Libs .topic n1 .title Lib One diff --git a/src/test/tools/fixtures/help/noteEnd.dhs b/src/test/tools/fixtures/help/noteEnd.dhs index c11a5b20..2ff2ff94 100644 --- a/src/test/tools/fixtures/help/noteEnd.dhs +++ b/src/test/tools/fixtures/help/noteEnd.dhs @@ -1,3 +1,25 @@ +# 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. + .topic t1 .title Note End .note diff --git a/src/test/tools/fixtures/help/outsideTopic.dhs b/src/test/tools/fixtures/help/outsideTopic.dhs index 37aaf516..5a05d390 100644 --- a/src/test/tools/fixtures/help/outsideTopic.dhs +++ b/src/test/tools/fixtures/help/outsideTopic.dhs @@ -1,3 +1,25 @@ +# 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. + .h1 Heading before any topic .topic neg.late .title Late diff --git a/src/test/tools/fixtures/help/sectionReset.dhs b/src/test/tools/fixtures/help/sectionReset.dhs index 01988069..977cb264 100644 --- a/src/test/tools/fixtures/help/sectionReset.dhs +++ b/src/test/tools/fixtures/help/sectionReset.dhs @@ -1,3 +1,25 @@ +# 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. + .section Alpha .topic t1 .title First diff --git a/src/test/tools/fixtures/help/selfInc.dhs b/src/test/tools/fixtures/help/selfInc.dhs index aef47c5e..4c542aee 100644 --- a/src/test/tools/fixtures/help/selfInc.dhs +++ b/src/test/tools/fixtures/help/selfInc.dhs @@ -1,3 +1,25 @@ +# 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. + .topic t1 .title Self Include .include selfInc.dhs diff --git a/src/test/tools/fixtures/help/topicNoId.dhs b/src/test/tools/fixtures/help/topicNoId.dhs index 76912ddb..f528a84b 100644 --- a/src/test/tools/fixtures/help/topicNoId.dhs +++ b/src/test/tools/fixtures/help/topicNoId.dhs @@ -1 +1,23 @@ +# 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. + .topic diff --git a/src/test/tools/fixtures/res/manifest.res b/src/test/tools/fixtures/res/manifest.res index 65cd4f5d..c7dd5f22 100644 --- a/src/test/tools/fixtures/res/manifest.res +++ b/src/test/tools/fixtures/res/manifest.res @@ -1,3 +1,25 @@ +# 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. + # manifest.res -- every resource type from src/tools/README.md icon32 icon icon32.bmp photo image icon32.bmp diff --git a/src/test/tools/hlpcLibTest.c b/src/test/tools/hlpcLibTest.c index f9d5328e..6178ca60 100644 --- a/src/test/tools/hlpcLibTest.c +++ b/src/test/tools/hlpcLibTest.c @@ -1,3 +1,25 @@ +// 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. + // hlpcLibTest.c -- exercises hlpcCompile() in library mode (HLPC_NO_MAIN). // // Built by runTools.sh against src/tools/dvxhlpc.c. Compiles the same @@ -22,6 +44,8 @@ #define LIB_TEST_MAX_CALLS 64 #define LIB_TEST_ARGC 6 +#define LIB_TEST_MSG_LEN 256 +#define LIB_TEST_STOP_TICK 2 // checkTicks: the tick that answers stop typedef struct { int32_t calls; @@ -31,11 +55,22 @@ typedef struct { bool totalStable; } ProgressStateT; +// checkTicks: the ticks a compile made, the one to answer stop at (0: +// none), and the messages it sent +typedef struct { + int32_t ticks; + int32_t stopAt; + char messages[LIB_TEST_MSG_LEN]; +} TickStateT; + static bool checkLenient(const char *outHlp, const char *outHtml); static bool checkNegative(const char *outHlp, const char *input); static bool checkPositive(const char *outHlp, const char *outHtml); +static bool checkTicks(const char *refHlp, const char *outHlp); static bool fileHas(const char *path, const char *needle); +static void onMessage(void *ctx, const char *line); static void onProgress(void *ctx, int32_t current, int32_t total); +static bool onTick(void *ctx); static char *readAll(const char *path, long *outLen); @@ -139,6 +174,54 @@ static bool checkPositive(const char *outHlp, const char *outHtml) { } +// The tick (hlpcSetTickFn): a compile makes them as it goes and writes +// the same file; one answering stop ends the compile with "Stopped.", +// leaving the earlier output in place. +static bool checkTicks(const char *refHlp, const char *outHlp) { + const char *inputs[] = { "fixtures/help/main.dhs", "fixtures/help/extra.dhs" }; + TickStateT st; + long refLen = 0; + long outLen = 0; + bool ok = true; + + memset(&st, 0, sizeof(st)); + hlpcSetTickFn(onTick, &st); + hlpcSetMessageFn(onMessage, &st); + + int32_t rc = hlpcCompile(inputs, 2, outHlp, "fixtures/help", NULL, HLPC_QUIET, NULL, NULL); + char *ref = readAll(refHlp, &refLen); + char *out = readAll(outHlp, &outLen); + int32_t ticked = st.ticks; + + if (rc != 0 || ticked <= LIB_TEST_STOP_TICK || !ref || !out || refLen != outLen || memcmp(ref, out, (size_t)refLen) != 0) { + fprintf(stderr, "FAIL: a compile with a tick returned %d after %d ticks, output the same: %d\n", (int)rc, (int)ticked, (int)(ref && out && refLen == outLen && memcmp(ref, out, (size_t)refLen) == 0)); + ok = false; + } + + free(out); + memset(&st, 0, sizeof(st)); + st.stopAt = LIB_TEST_STOP_TICK; + rc = hlpcCompile(inputs, 2, outHlp, "fixtures/help", NULL, HLPC_QUIET, NULL, NULL); + out = readAll(outHlp, &outLen); + + if (ok && (rc == 0 || st.ticks != LIB_TEST_STOP_TICK || strcmp(st.messages, "Stopped.\n") != 0 || !out || refLen != outLen || memcmp(ref, out, (size_t)refLen) != 0)) { + fprintf(stderr, "FAIL: a stopped compile returned %d after %d ticks, said [%s]\n", (int)rc, (int)st.ticks, st.messages); + ok = false; + } + + hlpcSetTickFn(NULL, NULL); + hlpcSetMessageFn(NULL, NULL); + free(ref); + free(out); + + if (ok) { + printf("ok: a compile ticked %d times; one stopped at tick %d said Stopped. and kept the output\n", (int)ticked, (int)LIB_TEST_STOP_TICK); + } + + return ok; +} + + // True when needle occurs anywhere in the file's bytes. static bool fileHas(const char *path, const char *needle) { long len = 0; @@ -160,6 +243,15 @@ static bool fileHas(const char *path, const char *needle) { } +// checkTicks' message hook: the lines, each ended with a newline. +static void onMessage(void *ctx, const char *line) { + TickStateT *st = (TickStateT *)ctx; + size_t len = strlen(st->messages); + + snprintf(st->messages + len, sizeof(st->messages) - len, "%s\n", line); +} + + static void onProgress(void *ctx, int32_t current, int32_t total) { ProgressStateT *st = (ProgressStateT *)ctx; @@ -181,6 +273,15 @@ static void onProgress(void *ctx, int32_t current, int32_t total) { } +// checkTicks' tick: counts, and answers stop at st->stopAt. +static bool onTick(void *ctx) { + TickStateT *st = (TickStateT *)ctx; + + st->ticks++; + return st->ticks != st->stopAt; +} + + // Whole file into a malloc'd buffer (NULL on any failure). static char *readAll(const char *path, long *outLen) { FILE *f = fopen(path, "rb"); @@ -228,6 +329,10 @@ int main(int argc, char **argv) { return 1; } + if (!checkTicks(argv[1], argv[3])) { + return 1; + } + if (!checkPositive(argv[1], argv[2])) { return 1; } diff --git a/src/test/tools/hlpcOomTest.c b/src/test/tools/hlpcOomTest.c index be71ac17..bb324620 100644 --- a/src/test/tools/hlpcOomTest.c +++ b/src/test/tools/hlpcOomTest.c @@ -1,3 +1,25 @@ +// 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. + // hlpcOomTest.c -- hlpcCompile() survives out of memory (library mode). // // dvx.exe runs the help compiler in-process when it rebuilds help at diff --git a/src/test/tools/runTools.sh b/src/test/tools/runTools.sh index 5f311d9a..bbace371 100755 --- a/src/test/tools/runTools.sh +++ b/src/test/tools/runTools.sh @@ -1,4 +1,26 @@ #!/usr/bin/env bash +# 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. + # runTools.sh -- suite T9: host tool regression tests. # # Covers dvxhlpc (CLI + library mode), dvxres, mkicon, mktbicon, @@ -265,7 +287,7 @@ expectNoGrep "html drops source comments" 'positive fixture exercising' "$WORK/m # ------------------------------------------------------------------ "$HOST/dvxhlpc" -o "$WORK/bad.hlp" --quiet badDirective.dhs > /dev/null 2> "$WORK/bad.err" rc=$? -expectGrep "unknown directive is reported" "warning: badDirective.dhs:3: unknown directive .bogus" "$WORK/bad.err" +expectGrep "unknown directive is reported" "warning: badDirective.dhs:25: unknown directive .bogus" "$WORK/bad.err" # The compiler treats an unknown directive as a warning, not an error (documented behaviour). expectRc "unknown directive is a warning (compile still succeeds)" 0 $rc @@ -278,7 +300,7 @@ check "missing image writes no .hlp" test ! -e "$WORK/img.hlp" "$HOST/dvxhlpc" -o "$WORK/out.hlp" --quiet outsideTopic.dhs > /dev/null 2> "$WORK/out.err" rc=$? expectRc "directive outside topic fails" 1 $rc -expectGrep "directive outside topic message" "error: outsideTopic.dhs:1: directive .h1 outside of a topic" "$WORK/out.err" +expectGrep "directive outside topic message" "error: outsideTopic.dhs:23: directive .h1 outside of a topic" "$WORK/out.err" expectGrep "directive outside topic abort summary" "Aborting due to 1 error(s)." "$WORK/out.err" check "directive outside topic writes no .hlp" test ! -e "$WORK/out.hlp" @@ -286,15 +308,15 @@ check "directive outside topic writes no .hlp" test ! -e "$WORK/out.hlp" # at the .link's own file and line (an included file names itself). "$HOST/dvxhlpc" -o "$WORK/link.hlp" --quiet brokenLink.dhs > /dev/null 2> "$WORK/link.err" expectRc "broken link target fails" 1 $? -expectGrep "broken link names file, line and topic" "error: brokenLink.dhs:4: link to missing topic 'neg.nowhere'" "$WORK/link.err" -expectGrep "broken link in an included file names that file" "error: brokenLinkInc.dhs:2: link to missing topic 'neg.alsoMissing'" "$WORK/link.err" +expectGrep "broken link names file, line and topic" "error: brokenLink.dhs:26: link to missing topic 'neg.nowhere'" "$WORK/link.err" +expectGrep "broken link in an included file names that file" "error: brokenLinkInc.dhs:24: link to missing topic 'neg.alsoMissing'" "$WORK/link.err" expectNoGrep "link to an existing topic is not reported" "'neg.link'" "$WORK/link.err" expectGrep "broken links abort summary" "Aborting due to 2 error(s)." "$WORK/link.err" check "broken link writes no .hlp" test ! -e "$WORK/link.hlp" "$HOST/dvxhlpc" -o "$WORK/linkNoId.hlp" --quiet linkNoId.dhs > /dev/null 2> "$WORK/linkNoId.err" expectRc "hlpc .link without topic ID fails" 1 $? -expectGrep "hlpc .link without topic ID message" "error: linkNoId.dhs:3: .link requires a topic ID" "$WORK/linkNoId.err" +expectGrep "hlpc .link without topic ID message" "error: linkNoId.dhs:25: .link requires a topic ID" "$WORK/linkNoId.err" "$HOST/dvxhlpc" -o "$WORK/codeLink.hlp" --html "$WORK/codeLink.html" --quiet codeLink.dhs > /dev/null 2> "$WORK/codeLink.err" expectRc "hlpc .link inside .code is not checked" 0 $? @@ -307,8 +329,8 @@ check "hlpc cross-file links produce no diagnostics" test ! -s "$WORK/linkAB.err "$HOST/dvxhlpc" -o "$WORK/linkA.hlp" --quiet linkA.dhs > /dev/null 2> "$WORK/linkA.err" expectRc "hlpc link to a file outside the compile fails" 1 $? -expectGrep "hlpc link from the first file to a missing file" "error: linkA.dhs:6: link to missing topic 'lk.b'" "$WORK/linkA.err" -expectGrep "hlpc link from an included file to a missing file" "error: linkInc.dhs:6: link to missing topic 'lk.b'" "$WORK/linkA.err" +expectGrep "hlpc link from the first file to a missing file" "error: linkA.dhs:28: link to missing topic 'lk.b'" "$WORK/linkA.err" +expectGrep "hlpc link from an included file to a missing file" "error: linkInc.dhs:28: link to missing topic 'lk.b'" "$WORK/linkA.err" expectNoGrep "hlpc code block target never checked" "lk.nowhere" "$WORK/linkA.err" "$HOST/dvxhlpc" -o "$WORK/nofile.hlp" --quiet doesNotExist.dhs > /dev/null 2> "$WORK/nofile.err" @@ -1429,7 +1451,7 @@ if $HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -DHLPC_NO_MAIN -o "$WORK/hlpcLibTe sed 's/^/ | /' "$WORK/libRun.out" # The negative compiles legitimately print their diagnostics; anything # beyond that is a sanitizer report. - printf 'error: fixtures/help/outsideTopic.dhs:1: directive .h1 outside of a topic\nAborting due to 1 error(s).\nerror: fixtures/help/brokenLink.dhs:4: link to missing topic '"'"'neg.nowhere'"'"'\nerror: fixtures/help/brokenLinkInc.dhs:2: link to missing topic '"'"'neg.alsoMissing'"'"'\nAborting due to 2 error(s).\nwarning: fixtures/help/brokenLink.dhs:4: link to missing topic '"'"'neg.nowhere'"'"' shown as plain text\nwarning: fixtures/help/brokenLinkInc.dhs:2: link to missing topic '"'"'neg.alsoMissing'"'"' shown as plain text\n' > "$WORK/libRun.expected" + printf 'error: fixtures/help/outsideTopic.dhs:23: directive .h1 outside of a topic\nAborting due to 1 error(s).\nerror: fixtures/help/brokenLink.dhs:26: link to missing topic '"'"'neg.nowhere'"'"'\nerror: fixtures/help/brokenLinkInc.dhs:24: link to missing topic '"'"'neg.alsoMissing'"'"'\nAborting due to 2 error(s).\nwarning: fixtures/help/brokenLink.dhs:26: link to missing topic '"'"'neg.nowhere'"'"' shown as plain text\nwarning: fixtures/help/brokenLinkInc.dhs:24: link to missing topic '"'"'neg.alsoMissing'"'"' shown as plain text\n' > "$WORK/libRun.expected" if cmp -s "$WORK/libRun.err" "$WORK/libRun.expected"; then pass "hlpcLibTest is sanitizer clean (stderr holds only the expected diagnostics)" else @@ -1551,14 +1573,14 @@ expectGrep "mkPackages lists a member with its folder" "kpunch.terminal apps/kpu expectGrep "mkPackages lists a member's paths after its folder" "kpunch.basdev libs/kpunch/basdev system/BASCOMP.EXE" "$PKG_OUT/members.lst" expectGrep "mkPackages lists the bundles" "kpunch.comm" "$PKG_OUT/bundles.lst" expectGrep "mkPackages: a bundle needs its members" "kpunch.terminal = $PKG_VERSION" "$PKG_OUT/kpunch.comm.ini" -expectGrep "mkPackages: a bundle needs the bundles it needs" "kpunch.net = $PKG_VERSION" "$PKG_OUT/kpunch.comm.ini" -expectGrep "mkPackages: a widget needs the library it imports" "kpunch.dvxsql = $PKG_VERSION" "$PKG_OUT/kpunch.datactrl.ini" +expectGrep "mkPackages: a library needs the library it imports" "kpunch.http = $PKG_VERSION" "$PKG_OUT/kpunch.pkgnet.ini" +expectGrep "mkPackages: a widget needs the library it looks up by name" "kpunch.dvxsql = $PKG_VERSION" "$PKG_OUT/kpunch.datactrl.ini" expectGrep "mkPackages: a member needs what it uses by name" "kpunch.datactrl = $PKG_VERSION" "$PKG_OUT/kpunch.dbgrid.ini" check "mkPackages: code carries the ABI" grep -qE "^abi += *$PKG_ABI\$" "$PKG_OUT/kpunch.dvxsql.ini" grep -qE "^abi +=" "$PKG_OUT/kpunch.comm.ini" expectRc "mkPackages: a bundle carries no ABI" 1 $? expectGrep "mkPackages: SETUP.INI names the components" "kpunch.comm = Communications" "$PKG_OUT/SETUP.INI" -expectGrep "mkPackages: SETUP.INI gives a component's folders" "folders = apps/kpunch/terminal/ libs/kpunch/conn/ libs/kpunch/xfer/" "$PKG_OUT/SETUP.INI" +expectGrep "mkPackages: SETUP.INI gives a component's folders" "folders = apps/kpunch/terminal/ widgets/kpunch/ansiterm/ libs/kpunch/conn/ libs/kpunch/seclink/ libs/kpunch/xfer/" "$PKG_OUT/SETUP.INI" expectGrep "mkPackages: SETUP.INI gives a component's paths" "paths = system/BASCOMP.EXE" "$PKG_OUT/SETUP.INI" check "mkPackages: SETUP.INI has CR LF line ends" grep -q $'\r$' "$PKG_OUT/SETUP.INI" @@ -1578,7 +1600,14 @@ mkPkgRefuses "DVX itself needing an optional library" "DVX itself needs" 't = t. mkPkgRefuses "a path that is not built" "its file system/NOTHERE.EXE is not in the release" 't = t.replace("paths = system/BASCOMP.EXE", "paths = system/NOTHERE.EXE")' mkPkgRefuses "a path in two packages" "is in two packages" 't = t.replace("paths = system/DVXRES.EXE", "paths = system/DVXRES.EXE system/BASCOMP.EXE")' mkPkgRefuses "a path in a member's folder" "is in the folder of" 't = t.replace("paths = system/DVXRES.EXE", "paths = system/DVXRES.EXE libs/kpunch/basdev/basdev.lib")' -mkPkgRefuses "a member needing a bundle its own does not" "does not need" 't = t.replace("needs = kpunch.net\n", "")' +mkPkgRefuses "a member needing a bundle its own does not" "does not need" 't = t.replace("members = kpunch.dvxsql kpunch.datactrl kpunch.dbgrid", "members = kpunch.dvxsql kpunch.dbgrid").replace("members = kpunch.dvxdemo kpunch.basdemo", "members = kpunch.dvxdemo kpunch.basdemo kpunch.datactrl")' + +# A bundle that needs another carries it in its template. +python3 -I -c 't = open(__import__("sys").argv[1]).read(); t = t.replace("members = kpunch.terminal kpunch.ansiterm kpunch.conn kpunch.seclink kpunch.xfer\n", "members = kpunch.terminal kpunch.ansiterm kpunch.conn kpunch.seclink kpunch.xfer\nneeds = kpunch.net\n"); open(__import__("sys").argv[2], "w").write(t)' "$PKG_MANIFEST" "$WORK/mkpkgNeeds.ini" +python3 -I "$MKPKG" "$WORK/mkpkgNeeds.ini" "$ROOT/bin" "$ROOT/src" "$PKG_VERSION" "$PKG_ABI" "$WORK/mkpkgNeeds" > "$WORK/mkpkgNeeds.log" 2>&1 +expectRc "mkPackages works out a bundle that needs another" 0 $? +expectGrep "mkPackages: a bundle needs the bundles it needs" "kpunch.net = $PKG_VERSION" "$WORK/mkpkgNeeds/kpunch.comm.ini" +expectGrep "mkPackages: SETUP.INI gives a component's needs" "needs = kpunch.net" "$WORK/mkpkgNeeds/SETUP.INI" # ------------------------------------------------------------------ echo "== T9 summary: $passCount passed, $failCount failed ==" diff --git a/src/test/unit/testDraw.c b/src/test/unit/testDraw.c index 271b6287..6bd3ecf0 100644 --- a/src/test/unit/testDraw.c +++ b/src/test/unit/testDraw.c @@ -87,6 +87,7 @@ #define PROP_BLANK_ADV 5 #define PROP_UNDERLINE_Y 9 #define PROP_STRIKE_Y 5 +#define PROP_LOW_LINE_Y (PROP_HEIGHT + 1) // testTextClipRows: an underline below the glyph cell #define PROP_BITS_MAX 512 #define PROP_PATTERN 4 // a glyph pixel is set unless (3x + 5y + ch) is a multiple of this #define PROP_X_STEP 3 @@ -155,6 +156,7 @@ static void testScrollbarArrowButton(void); static void testSpans(void); static void testTermRow(int32_t bpp); static void testText(int32_t bpp); +static void testTextClipRows(int32_t bpp); static void testTextProp(int32_t bpp); static void testTextScaled(int32_t bpp); static void testTextStyled(int32_t bpp); @@ -530,6 +532,7 @@ void testDraw(void) { testRectCopy(sBpps[i]); testBevel(sBpps[i]); testText(sBpps[i]); + testTextClipRows(sBpps[i]); testTextProp(sBpps[i]); testTextScaled(sBpps[i]); testTextStyled(sBpps[i]); @@ -1297,6 +1300,61 @@ static void testText(int32_t bpp) { } +// A text row is passed over only when it lies wholly outside the clip, its +// lines included: a clip holding just an underline below the glyph cell +// draws the underline; one through the glyphs draws exactly the rows +// inside it; clips above and below draw nothing. +static void testTextClipRows(int32_t bpp) { + DisplayT d; + BlitOpsT ops; + BitmapFontT lined = sPropFont; + uint32_t *ref = (uint32_t *)calloc((size_t)UNIT_SCREEN_W * UNIT_SCREEN_H, sizeof(uint32_t)); + uint32_t *full = (uint32_t *)calloc((size_t)UNIT_SCREEN_W * UNIT_SCREEN_H, sizeof(uint32_t)); + uint32_t fg = colorForBpp(bpp, DRAW_FG); + uint32_t bg = colorForBpp(bpp, DRAW_BG); + uint32_t pre = colorForBpp(bpp, DRAW_FILL); + int32_t x = 3; + int32_t y = 4; + + // Clips as first row and row count + const int32_t clips[][2] = { { y + PROP_LOW_LINE_Y, 1 }, { y + 3, 3 }, { 0, y }, { y + PROP_LOW_LINE_Y + 1, 2 } }; + + lined.style = FONT_STYLE_UNDERLINE; + lined.underlineY = PROP_LOW_LINE_Y; + unitDisplayCreate(&d, UNIT_SCREEN_W, UNIT_SCREEN_H, bpp); + drawInit(&ops, &d); + resetClipRect(&d); + primeSurface(&d, ref, pre); + drawText(&d, &ops, &lined, x, y, "AWg", fg, bg, true); + + for (int32_t i = 0; i < d.width * d.height; i++) { + full[i] = unitPixelGet(&d, i % d.width, i / d.width); + } + + TEST_EQ(full[(y + PROP_LOW_LINE_Y) * d.width + x], fg); + + for (int32_t ci = 0; ci < DVX_ARRAY_LEN(clips); ci++) { + int32_t top = clips[ci][0]; + int32_t rows = clips[ci][1]; + + resetClipRect(&d); + primeSurface(&d, ref, pre); + + for (int32_t row = top; row < top + rows; row++) { + memcpy(&ref[row * d.width], &full[row * d.width], (size_t)d.width * sizeof(uint32_t)); + } + + setClipRect(&d, 0, top, d.width, rows); + drawText(&d, &ops, &lined, x, y, "AWg", fg, bg, true); + TEST_EQ(firstMismatch(&d, ref), -1); + } + + free(full); + free(ref); + unitDisplayDestroy(&d); +} + + // Text in a loaded (proportional) font against the reference: glyphs of // one and two bytes per row, left of the pen, past the advance and below // the baseline; opaque and transparent; clipped on every side, a clip diff --git a/src/test/unit/testFonts.c b/src/test/unit/testFonts.c index d5e48211..aea79e3a 100644 --- a/src/test/unit/testFonts.c +++ b/src/test/unit/testFonts.c @@ -852,6 +852,29 @@ static void testSmooth(void) { TEST_TRUE(under->glyphs == f->glyphs); TEST_TRUE(rowSet(f, 'A', 2, 0, w)); + // A font at a time: Begin builds nothing, each step one font (the + // fonts before the fonts with lines), none left after the last + serial = f->serial; + dvxFontSmoothBegin(true); + TEST_TRUE(dvxFontSmooth()); + TEST_FALSE(f->flags & FONT_FLAG_GRAY); + TEST_EQ(f->serial, serial); + + int32_t steps = 0; + + while (dvxFontRebuildStep()) { + steps++; + TEST_TRUE(!(under->flags & FONT_FLAG_GRAY) || (f->flags & FONT_FLAG_GRAY)); + } + + TEST_TRUE(steps > 1); + TEST_TRUE(f->flags & FONT_FLAG_GRAY); + TEST_TRUE(under->flags & FONT_FLAG_GRAY); + TEST_NE(f->serial, serial); + TEST_FALSE(dvxFontRebuildStep()); + dvxFontSetSmooth(false); + TEST_FALSE(f->flags & FONT_FLAG_GRAY); + dvxFontClose(f); dvxFontClose(under); dvxFontClose(ital); diff --git a/src/test/unit/testPlatform.c b/src/test/unit/testPlatform.c index 28f7a985..8e003093 100644 --- a/src/test/unit/testPlatform.c +++ b/src/test/unit/testPlatform.c @@ -1,3 +1,25 @@ +// 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. + // testPlatform.c -- host platform backend: mouse clamping, crash handler, // stb_ds out-of-memory hook, PNG capture at every depth, system/memory // info, palette range checks, video init failure and the DOS-only stubs @@ -6,9 +28,18 @@ #include "dvxPlatHost.h" #include "dvxVideo.h" #include "dvxPal.h" +#include "dvxEndian.h" +// The PNGs written are read back through a copy of stb_image of this +// file's own (libdvx keeps its copy static). +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#define STB_IMAGE_STATIC +#define STB_IMAGE_IMPLEMENTATION #include "stb_image_wrap.h" +#pragma GCC diagnostic pop #include "stb_ds_wrap.h" +#include #include #include #include @@ -61,6 +92,21 @@ #define PLAT_NOTE_FLOOD 40 // more of them than there is room for #define PLAT_NOTE_MORE "...\n" +// A DXE3 file made by testDxeExports (the header fields it reads, as +// platformDxeExports has them) +#define PLAT_DXE_MAGIC 0x31455844u +#define PLAT_DXE_HEADER 80 +#define PLAT_DXE_COUNT_OFS 28 +#define PLAT_DXE_TABLE_OFS 32 +#define PLAT_DXE_SIZE_OFS 36 +#define PLAT_DXE_INIT_OFS 72 +#define PLAT_DXE_NO_INIT 0xFFFFFFFFu +#define PLAT_DXE_INIT_AT 0x40 // any offset: the module has constructors +#define PLAT_DXE_SYM_OFS 4 +#define PLAT_DXE_FILE_MAX 256 +#define PLAT_DXE_NAMES_MAX 128 +#define PLAT_DXE_CUT 3 // bytes cut off the end: a name loses its NUL + static const int32_t sBpps[] = { 8, 16, 32 }; static const int32_t sSignals[] = { SIGSEGV, SIGFPE, SIGILL, SIGBUS }; @@ -72,9 +118,14 @@ static const int32_t sSignals[] = { SIGSEGV, SIGFPE, SIGILL, SIGBUS }; static void testBankChunk(void); static void testBootNotes(void); static void testCrashHandler(void); +static void testDirEntries(void); static void testDrives(void); +static void testDxeExports(void); +static void testDxeName(void *ctx, const char *name); +static void testDxeWrite(const char *path, uint32_t init, int32_t cut); static void testFolders(void); static void testInfo(void); +static void testInstalledCount(void); static void testMouseClamp(void); static void testOutOfMemory(void); static void testPalette(void); @@ -309,6 +360,49 @@ static void testCrashHandler(void) { // One "/" drive; the free space of a folder not created yet is that of the // nearest existing parent; no boot drive off DOS. +// platformDirEntryIsDir: the type readdir gave decides; unknown and links +// are looked up. +static void testDirEntries(void) { + char dir[UNIT_PATH_MAX]; + char sub[UNIT_PATH_MAX]; + char link[UNIT_PATH_MAX]; + + unitScratchPath(dir, sizeof(dir), "dirents"); + TEST_EQ(platformMkdirRecursive(dir), 0); + snprintf(sub, sizeof(sub), "%s/sub", dir); + TEST_EQ(platformMkdirRecursive(sub), 0); + snprintf(link, sizeof(link), "%s/toSub", dir); + unlink(link); + TEST_EQ(symlink(sub, link), 0); + + TEST_TRUE(platformDirEntryIsDir(dir, "nothing", DT_DIR)); + TEST_FALSE(platformDirEntryIsDir(dir, "sub", DT_REG)); + TEST_TRUE(platformDirEntryIsDir(dir, "sub", DT_UNKNOWN)); + TEST_TRUE(platformDirEntryIsDir(dir, "toSub", DT_LNK)); + TEST_FALSE(platformDirEntryIsDir(dir, "nothing", DT_UNKNOWN)); + + // What readdir really says agrees + DIR *d = opendir(dir); + int32_t found = 0; + + for (struct dirent *ent = d ? readdir(d) : NULL; ent; ent = readdir(d)) { + if (strcmp(ent->d_name, "sub") == 0 || strcmp(ent->d_name, "toSub") == 0) { + TEST_TRUE(platformDirEntryIsDir(dir, ent->d_name, ent->d_type)); + found++; + } + } + + if (d) { + closedir(d); + } + + TEST_EQ(found, 2); + unlink(link); + rmdir(sub); + rmdir(dir); +} + + static void testDrives(void) { PlatformDriveT drives[PLATFORM_DRIVE_MAX]; char path[UNIT_PATH_MAX]; @@ -337,6 +431,87 @@ static void testDrives(void) { } +// platformDxeExports on files made here: the names in order and whether +// the module runs code as it loads; a file that is not a DXE, one cut +// short, and one that is not there are refused. +static void testDxeExports(void) { + char path[UNIT_PATH_MAX]; + char names[PLAT_DXE_NAMES_MAX]; + bool runs = false; + FILE *plain; + + unitScratchPath(path, sizeof(path), "module.dxe"); + names[0] = '\0'; + testDxeWrite(path, PLAT_DXE_INIT_AT, 0); + TEST_TRUE(platformDxeExports(path, testDxeName, names, &runs)); + TEST_STR_EQ(names, "_alpha _beta "); + TEST_TRUE(runs); + + names[0] = '\0'; + testDxeWrite(path, PLAT_DXE_NO_INIT, 0); + TEST_TRUE(platformDxeExports(path, testDxeName, names, &runs)); + TEST_TRUE(!runs); + + // Cut short: the second name has no NUL in the table + names[0] = '\0'; + testDxeWrite(path, PLAT_DXE_NO_INIT, PLAT_DXE_CUT); + TEST_TRUE(!platformDxeExports(path, testDxeName, names, &runs)); + TEST_STR_EQ(names, "_alpha "); + + // Not a DXE: the header's first word is not the magic + testDxeWrite(path, PLAT_DXE_NO_INIT, 0); + plain = fopen(path, "r+b"); + + if (plain) { + fputc(0, plain); + fclose(plain); + } + + TEST_TRUE(!platformDxeExports(path, testDxeName, names, &runs)); + remove(path); + TEST_TRUE(!platformDxeExports(path, testDxeName, names, &runs)); +} + + +// testDxeExports' callback: each name and a space, onto ctx. +static void testDxeName(void *ctx, const char *name) { + char *names = (char *)ctx; + + strncat(names, name, PLAT_DXE_NAMES_MAX - strlen(names) - 1); + strncat(names, " ", PLAT_DXE_NAMES_MAX - strlen(names) - 1); +} + + +// A DXE3 file exporting _alpha and _beta, its _init init, the last cut +// bytes left off. +static void testDxeWrite(const char *path, uint32_t init, int32_t cut) { + static const char *const exports[] = { "_alpha", "_beta" }; + uint8_t file[PLAT_DXE_FILE_MAX]; + int32_t pos = PLAT_DXE_HEADER; + FILE *f; + + memset(file, 0, sizeof(file)); + + for (int32_t i = 0; i < DVX_ARRAY_LEN(exports); i++) { + pos += PLAT_DXE_SYM_OFS; + memcpy(file + pos, exports[i], strlen(exports[i]) + 1); + pos += (int32_t)strlen(exports[i]) + 1; + } + + dvxPutLe32(file, PLAT_DXE_MAGIC); + dvxPutLe32(file + PLAT_DXE_COUNT_OFS, DVX_ARRAY_LEN(exports)); + dvxPutLe32(file + PLAT_DXE_TABLE_OFS, PLAT_DXE_HEADER); + dvxPutLe32(file + PLAT_DXE_SIZE_OFS, (uint32_t)(pos - PLAT_DXE_HEADER - cut)); + dvxPutLe32(file + PLAT_DXE_INIT_OFS, init); + f = fopen(path, "wb"); + + if (f) { + fwrite(file, 1, (size_t)(pos - cut), f); + fclose(f); + } +} + + // The DVX folders: the user folder (DVXUSER, USER, TEMP, nowhere) and // its first start, a program's user folder, and the kinds' folders with // the user's found first. Runs in a scratch folder of its own, as DVX @@ -548,6 +723,18 @@ static void testInfo(void) { } +// The installed-change count moves on with each change. +static void testInstalledCount(void) { + uint32_t before = platformInstalledChangeCount(); + + platformInstalledChanged(); + TEST_EQ(platformInstalledChangeCount(), before + 1); + platformInstalledChanged(); + platformInstalledChanged(); + TEST_EQ(platformInstalledChangeCount(), before + 3); +} + + static void testMouseClamp(void) { int32_t x = 0; int32_t y = 0; @@ -678,7 +865,10 @@ void testPlatform(void) { testVideoInitFailure(); testVideoFailNext(); testDrives(); + testDxeExports(); testFolders(); + testDirEntries(); + testInstalledCount(); for (int32_t i = 0; i < DVX_ARRAY_LEN(sBpps); i++) { testPng(sBpps[i]); diff --git a/src/test/unit/testPrefs.c b/src/test/unit/testPrefs.c index 45930cbb..4b04bed2 100644 --- a/src/test/unit/testPrefs.c +++ b/src/test/unit/testPrefs.c @@ -55,6 +55,10 @@ #define PREFS_LONG_LINE 4000 #define PREFS_NO_ACCESS 0 #define PREFS_RW_ACCESS 0600 +#define PREFS_INDEX_KEYS 2000 // testIndex: keys, enough to grow the lookup index often +#define PREFS_INDEX_PROBE 1234 // one of them +#define PREFS_DUP_FILE "dups.ini" +#define PREFS_KEY_TEXT 16 // testSecrets #define PREFS_SECRET_FILE "secret.ini" @@ -131,6 +135,8 @@ static void testBasicChoices(void); static void testChoices(void); static void testCreateAndSave(void); static void testCrlfAndTopLevel(void); +static void testDvxIniShared(void); +static void testIndex(void); static void testKeyNames(void); static void testLoadSeed(void); static void testMissingAndNull(void); @@ -527,6 +533,109 @@ static void testCrlfAndTopLevel(void) { // prefsKeyName walks one section's keys in file order, counts a repeated // key once, stops with NULL, and ignores comments and other sections. +// The shared DVX.INI is read once and again only after a save in DVX or +// a change on disk. +static void testDvxIniShared(void) { + const char *path = platformDvxIniPath(); + int32_t oldLen = 0; + char *old = platformReadFile(path, &oldLen); + char dir[UNIT_PATH_MAX]; + + snprintf(dir, sizeof(dir), "%s", path); + + char *slash = strrchr(dir, '/'); + + if (slash) { + *slash = '\0'; + platformMkdirRecursive(dir); + } + + writeText(path, "[shared]\r\nk = 1\r\n"); + + PrefsHandleT *first = prefsDvxIniShared(); + + TEST_EQ(prefsGetInt(first, "shared", "k", 0), 1); + TEST_TRUE(prefsDvxIniShared() == first); + + // A save in DVX + PrefsHandleT *own = prefsLoad(path); + + prefsSetInt(own, "shared", "k", 2); + TEST_TRUE(prefsSave(own)); + prefsClose(own); + TEST_EQ(prefsGetInt(prefsDvxIniShared(), "shared", "k", 0), 2); + + // A change on disk (another size) + writeText(path, "[shared]\r\nk = 333\r\n"); + TEST_EQ(prefsGetInt(prefsDvxIniShared(), "shared", "k", 0), 333); + + if (old) { + TEST_TRUE(platformWriteFile(path, old, oldLen)); + free(old); + } else { + remove(path); + } + + unitPass(); +} + + +// Lookups go through an index of the keys and headers: the first of a +// repeated key wins, names match in any case, and the answers stay right +// as keys are added at the end, inserted into an earlier section, and +// removed, in a file big enough to grow the index many times. +static void testIndex(void) { + PrefsHandleT *h = prefsCreate(); + char key[PREFS_KEY_TEXT]; + int32_t wrong = 0; + + TEST_PTR_NOT_NULL(h); + + if (!h) { + return; + } + + // Two sections taking turns: every second key goes before the end + for (int32_t i = 0; i < PREFS_INDEX_KEYS; i++) { + snprintf(key, sizeof(key), "k%d", (int)i); + prefsSetInt(h, (i % 2) ? "Odd" : "Even", key, i); + } + + for (int32_t i = 0; i < PREFS_INDEX_KEYS; i++) { + snprintf(key, sizeof(key), "K%d", (int)i); + wrong += prefsGetInt(h, (i % 2) ? "ODD" : "even", key, -1) != i; + wrong += prefsGetInt(h, (i % 2) ? "Even" : "Odd", key, -1) != -1; + } + + TEST_EQ(wrong, 0); + snprintf(key, sizeof(key), "k%d", PREFS_INDEX_PROBE); + prefsRemove(h, "Even", key); + TEST_EQ(prefsGetInt(h, "Even", key, -1), -1); + TEST_EQ(prefsGetInt(h, "Even", "k0", -1), 0); + prefsSetInt(h, "Even", key, 7); + TEST_EQ(prefsGetInt(h, "Even", key, -1), 7); + prefsSetInt(h, "Third", "k0", 3); + TEST_EQ(prefsGetInt(h, "third", "K0", -1), 3); + TEST_EQ(prefsGetInt(h, "Even", "k0", -1), 0); + prefsClose(h); + + // A repeated key: the first wins, and the next once it is removed + char path[UNIT_PATH_MAX]; + + unitScratchPath(path, sizeof(path), PREFS_DUP_FILE); + writeText(path, "[A]\nk = 1\nk = 2\n[a]\nk = 3\nj = 4\n"); + h = prefsLoad(path); + TEST_STR_EQ(prefsGetString(h, "A", "k", "?"), "1"); + TEST_STR_EQ(prefsGetString(h, "a", "j", "?"), "4"); + prefsRemove(h, "A", "k"); + TEST_STR_EQ(prefsGetString(h, "A", "k", "?"), "2"); + prefsRemove(h, "A", "k"); + TEST_STR_EQ(prefsGetString(h, "A", "k", "?"), "3"); + prefsClose(h); + remove(path); +} + + static void testKeyNames(void) { char path[UNIT_PATH_MAX]; @@ -637,6 +746,8 @@ void testPrefs(void) { testCreateAndSave(); testRewritePreserves(); testCrlfAndTopLevel(); + testDvxIniShared(); + testIndex(); testChoices(); testKeyNames(); testUnreadableKept(); diff --git a/src/test/unit/testResource.c b/src/test/unit/testResource.c index 7ce6a560..baf3b6b5 100644 --- a/src/test/unit/testResource.c +++ b/src/test/unit/testResource.c @@ -28,10 +28,14 @@ // "module". A byte pattern of known size stands in for the DXE. #include "unitTest.h" +#include "dvxApp.h" #include "dvxRes.h" +#include "dvxResInt.h" +#include "../../tools/dvxResWrite.h" #include #include +#include #include #include @@ -45,9 +49,11 @@ #define RES_BEYOND_EOF 0x7FFFFFF0u // entry offset far past the file end #define RES_HUGE_SIZE 0xFFFFFFF0u // entry size no file could satisfy #define RES_DIR_MODE 0755 +#define RES_TEXT_SMALL 5 // a text buffer too small for sAuthorData -static const char sIconData[] = "ICONDATA-not-really-a-bmp"; -static const char sAuthorData[] = "Scott Duensing"; +static const char sIconData[] = "ICONDATA-not-really-a-bmp"; +static const char sAuthorData[] = "Scott Duensing"; +static const char sToolboxIcon[] = "TOOLBOX-ICON"; // ============================================================ @@ -59,7 +65,9 @@ static bool checkNoTempFiles(const char *path); static int64_t fileSize(const char *path); static void testAppendAndRead(const char *path, const uint8_t *module); static void testBadInputs(const char *path); +static void testBatch(const char *path, const uint8_t *module); static void testCorruptFiles(void); +static void testIconRead(const char *path, const uint8_t *module); static void testRemoveAndStrip(const char *path, const uint8_t *module); static void testTempNameIs83(const char *path, const uint8_t *module); static void writeModule(const char *path, uint8_t *module); @@ -188,6 +196,23 @@ static void testAppendAndRead(const char *path, const uint8_t *module) { TEST_PTR_NULL(dvxResRead(h, "missing", &size)); + // Text straight into a buffer: whole, cut to fit, or left alone + // when there is no such resource + char whole[sizeof(sAuthorData)]; + char small[RES_TEXT_SMALL]; + + TEST_TRUE(dvxResReadText(h, "author", whole, sizeof(whole))); + TEST_STR_EQ(whole, sAuthorData); + TEST_TRUE(dvxResReadText(h, "author", small, sizeof(small))); + TEST_EQ((int32_t)strlen(small), RES_TEXT_SMALL - 1); + TEST_TRUE(strncmp(small, sAuthorData, RES_TEXT_SMALL - 1) == 0); + snprintf(small, sizeof(small), "kept"); + TEST_FALSE(dvxResReadText(h, "missing", small, sizeof(small))); + TEST_STR_EQ(small, "kept"); + TEST_FALSE(dvxResReadText(NULL, "author", small, sizeof(small))); + TEST_FALSE(dvxResReadText(h, "author", NULL, RES_TEXT_SMALL)); + TEST_FALSE(dvxResReadText(h, "author", small, 0)); + // Entry data never overlaps and sits after the module. bool laidOut = true; @@ -263,6 +288,54 @@ static void testBadInputs(const char *path) { } +// A batch writes its entries at once: what the file held, then each add +// (a same name replacing the one before); one freed unwritten leaves the +// file as it was. +static void testBatch(const char *path, const uint8_t *module) { + DvxResBatchT b; + char text[RES_TEXT_SMALL]; + + TEST_EQ(dvxResAppend(path, "keep", DVX_RES_TEXT, "k", 2), 0); + + int64_t before = fileSize(path); + + TEST_EQ(dvxResBatchBegin(&b, path), 0); + TEST_EQ(dvxResBatchAdd(&b, "one", DVX_RES_TEXT, "1", 2), 0); + dvxResBatchFree(&b); + TEST_EQ(fileSize(path), before); + + TEST_EQ(dvxResBatchBegin(&b, path), 0); + TEST_EQ(dvxResBatchAdd(&b, "one", DVX_RES_TEXT, "1", 2), 0); + TEST_EQ(dvxResBatchAdd(&b, "two", DVX_RES_BINARY, sIconData, sizeof(sIconData)), 0); + TEST_EQ(dvxResBatchAdd(&b, "one", DVX_RES_TEXT, "uno", 4), 0); + TEST_EQ(dvxResBatchCommit(&b), 0); + TEST_TRUE(checkNoTempFiles(path)); + checkModuleBytesIntact(path, module); + + DvxResHandleT *h = dvxResOpen(path); + + TEST_PTR_NOT_NULL(h); + + if (h) { + const DvxResDirEntryT *two = dvxResFind(h, "two"); + + TEST_EQ(h->entryCount, 3); + TEST_TRUE(dvxResReadText(h, "one", text, sizeof(text))); + TEST_STR_EQ(text, "uno"); + TEST_TRUE(dvxResReadText(h, "keep", text, sizeof(text))); + TEST_STR_EQ(text, "k"); + TEST_PTR_NOT_NULL(two); + TEST_EQ(two ? two->size : 0, sizeof(sIconData)); + dvxResClose(h); + } + + TEST_EQ(dvxResRemove(path, "one"), 0); + TEST_EQ(dvxResRemove(path, "two"), 0); + TEST_EQ(dvxResRemove(path, "keep"), 0); + TEST_EQ(fileSize(path), RES_MODULE_SIZE); +} + + // Structurally corrupt resource blocks must be rejected without reading // past the file: a directory as the "file", a footer whose directory is // truncated, and an entry whose data offset lies beyond the end. @@ -324,6 +397,64 @@ static void testCorruptFiles(void) { } +// A program's icon read from the file already open (dvxResIconRead): +// DVX_RES_NAME_ICON before DVX_RES_NAME_WIDGET_ICON, nothing from a file +// without either, and the same through dvxResLoadIconData. +static void testIconRead(const char *path, const uint8_t *module) { + uint32_t size = 0; + + TEST_PTR_NULL(dvxResIconRead(NULL, &size)); + TEST_PTR_NULL(dvxResLoadIconData(NULL, &size)); + TEST_PTR_NULL(dvxResLoadIconData(path, &size)); + + FILE *fp = fopen(path, "rb"); + + TEST_PTR_NOT_NULL(fp); + TEST_PTR_NULL(fp ? dvxResIconRead(fp, &size) : NULL); + + if (fp) { + fclose(fp); + } + + TEST_EQ(dvxResAppend(path, "author", DVX_RES_TEXT, sAuthorData, sizeof(sAuthorData)), 0); + TEST_PTR_NULL(dvxResLoadIconData(path, &size)); + + TEST_EQ(dvxResAppend(path, DVX_RES_NAME_WIDGET_ICON, DVX_RES_ICON, sToolboxIcon, sizeof(sToolboxIcon)), 0); + + char *icon = (char *)dvxResLoadIconData(path, &size); + + TEST_PTR_NOT_NULL(icon); + TEST_EQ(size, sizeof(sToolboxIcon)); + + if (icon) { + TEST_STR_EQ(icon, sToolboxIcon); + free(icon); + } + + TEST_EQ(dvxResAppend(path, DVX_RES_NAME_ICON, DVX_RES_ICON, sIconData, sizeof(sIconData)), 0); + fp = fopen(path, "rb"); + icon = fp ? (char *)dvxResIconRead(fp, &size) : NULL; + + if (fp) { + fclose(fp); + } + + TEST_PTR_NOT_NULL(icon); + TEST_EQ(size, sizeof(sIconData)); + + if (icon) { + TEST_STR_EQ(icon, sIconData); + free(icon); + } + + TEST_EQ(dvxResRemove(path, DVX_RES_NAME_ICON), 0); + TEST_EQ(dvxResRemove(path, DVX_RES_NAME_WIDGET_ICON), 0); + TEST_EQ(dvxResRemove(path, "author"), 0); + TEST_EQ(fileSize(path), RES_MODULE_SIZE); + checkModuleBytesIntact(path, module); +} + + static void testRemoveAndStrip(const char *path, const uint8_t *module) { TEST_EQ(dvxResRemove(path, "missing"), -1); TEST_EQ(dvxResRemove(path, "blob"), 0); @@ -384,8 +515,10 @@ void testResource(void) { testBadInputs(path); testCorruptFiles(); + testIconRead(path, module); testAppendAndRead(path, module); testRemoveAndStrip(path, module); + testBatch(path, module); testTempNameIs83(path, module); remove(path); diff --git a/src/test/unit/testUtil.c b/src/test/unit/testUtil.c index 0cd1cc43..0c27e698 100644 --- a/src/test/unit/testUtil.c +++ b/src/test/unit/testUtil.c @@ -28,7 +28,9 @@ #include #include #include +#include #include +#include #define UTIL_DIR "utildir" #define UTIL_DEEP "utildir/a/b/c" @@ -39,12 +41,16 @@ #define UTIL_SMALL_BUF 16 #define UTIL_TRIM_BUF 4 // dvxCopyTrimmed truncation case #define UTIL_DIR_FILES 3 +#define UTIL_WALK_DIR "walkdir" +#define UTIL_WALK_FILES 3 // a.txt, sub/b.txt, sub/deep/c.txt (the dot names skipped) // Reports a 4K size but reads only a few bytes (short-read path). #define UTIL_SYSFS_FILE "/sys/kernel/osrelease" #define UTIL_SWAP_APP "swap.app" #define UTIL_SWAP_HLP "swap.hlp" #define UTIL_BIG_FILE 3000000000.0 // bytes: a FAT32 file past 2 GB +#define UTIL_STAMP_FILE "utildir/stamp.txt" +#define UTIL_STAMP_BACK 60 // seconds back: the rewritten stamp file's time // ============================================================ @@ -55,6 +61,7 @@ static int compareNames(const void *a, const void *b); static void testCopyFile(void); static void testDirs(void); static void testFileNames(void); +static void testFileStamp(void); static void testFilter(void); static void testGlob(void); static void testLog(void); @@ -62,6 +69,9 @@ static void testPaths(void); static void testReadFile(void); static void testReplaceFile(void); static void testStrings(void); +static void testWalkDir(void); +static bool walkCount(const char *path, const char *name, void *ctx); +static bool walkStop(const char *path, const char *name, void *ctx); static int compareNames(const void *a, const void *b) { @@ -328,6 +338,42 @@ static void testFileNames(void) { } +// platformFileStamp: the same file gives the same stamp, a rewrite of the +// same size at another time a new one (a file system's clock is coarse, +// so the time is set); a missing file, a folder, and a pattern give none. +static void testFileStamp(void) { + static const char first[] = "first"; + static const char second[] = "other"; + char path[UNIT_PATH_MAX]; + char pattern[UNIT_PATH_MAX]; + char dir[UNIT_PATH_MAX]; + PlatformStampT a; + PlatformStampT b; + struct utimbuf earlier; + + earlier.actime = time(NULL) - UTIL_STAMP_BACK; + earlier.modtime = earlier.actime; + unitScratchPath(path, sizeof(path), UTIL_STAMP_FILE); + unitScratchPath(dir, sizeof(dir), UTIL_DIR); + TEST_TRUE(platformWriteFile(path, first, (int32_t)sizeof(first))); + TEST_TRUE(platformFileStamp(path, &a)); + TEST_EQ((int64_t)a.size, (int64_t)sizeof(first)); + TEST_TRUE(platformFileStamp(path, &b)); + TEST_TRUE(a.size == b.size && a.time == b.time); + TEST_TRUE(platformWriteFile(path, second, (int32_t)sizeof(second))); + TEST_EQ(utime(path, &earlier), 0); + TEST_TRUE(platformFileStamp(path, &b)); + TEST_EQ((int64_t)b.size, (int64_t)a.size); + TEST_TRUE(b.time != a.time); + remove(path); + TEST_FALSE(platformFileStamp(path, &b)); + TEST_FALSE(platformFileStamp(dir, &b)); + TEST_FALSE(platformFileStamp(NULL, &b)); + snprintf(pattern, sizeof(pattern), "%s/*.txt", dir); + TEST_FALSE(platformFileStamp(pattern, &b)); +} + + // Several patterns, DOS "*.*", blank filters. static void testFilter(void) { char longFilter[UNIT_PATH_MAX * 2]; @@ -747,6 +793,19 @@ static void testStrings(void) { TEST_EQ(dvxStrAppendf(small, sizeof(small), 0, "%s", "exactly15chars!"), 15); TEST_EQ(dvxStrAppendf(small, sizeof(small), 0, "%s", "sixteen chars!!!"), -1); + // dvxStrAppendfCut: what does not fit is cut, and a position out of + // range (a caller's negative code too) comes back unchanged. + pos = dvxStrAppendfCut(small, sizeof(small), 0, "%s", "abc"); + TEST_EQ(pos, 3); + pos = dvxStrAppendfCut(small, sizeof(small), pos, "%s", "0123456789xyz"); + TEST_EQ(pos, (int32_t)sizeof(small) - 1); + TEST_STR_EQ(small, "abc0123456789xy"); + TEST_EQ(dvxStrAppendfCut(small, sizeof(small), pos, "more"), (int32_t)sizeof(small) - 1); + TEST_EQ(dvxStrAppendfCut(small, sizeof(small), (int32_t)sizeof(small), "x"), (int32_t)sizeof(small)); + TEST_EQ(dvxStrAppendfCut(small, sizeof(small), -2, "x"), -2); + TEST_EQ(dvxStrAppendfCut(NULL, sizeof(small), 0, "x"), 0); + TEST_EQ(dvxStrAppendfCut(small, sizeof(small), 0, "%s", "exactly15chars!"), 15); + // Alt+key scancode table. TEST_EQ(platformAltScanToChar(0x10), 'q'); TEST_EQ(platformAltScanToChar(0x32), 'm'); @@ -785,4 +844,76 @@ void testUtil(void) { testReadFile(); testReplaceFile(); testCopyFile(); + testWalkDir(); + testFileStamp(); +} + + +// platformWalkDir: every file below the folder, depth first, dot names +// skipped; a visitor's false stops the walk. +static void testWalkDir(void) { + static const char *const files[] = { "a.txt", "sub/b.txt", "sub/deep/c.txt", ".hid/d.txt", ".e.txt" }; + char dir[UNIT_PATH_MAX]; + char path[UNIT_PATH_MAX]; + int32_t count = 0; + + unitScratchPath(dir, sizeof(dir), UTIL_WALK_DIR); + + for (int32_t i = 0; i < DVX_ARRAY_LEN(files); i++) { + snprintf(path, sizeof(path), "%s/%s", dir, files[i]); + + char folder[UNIT_PATH_MAX]; + + if (platformPathDirName(path, folder, sizeof(folder))) { + platformMkdirRecursive(folder); + } + + FILE *fp = fopen(path, "wb"); + + if (fp) { + fclose(fp); + } + } + + TEST_TRUE(platformWalkDir(dir, walkCount, &count)); + TEST_EQ(count, UTIL_WALK_FILES); + + count = 0; + TEST_FALSE(platformWalkDir(dir, walkStop, &count)); + TEST_EQ(count, 1); + + count = 0; + TEST_TRUE(platformWalkDir("/no/such/dir/anywhere", walkCount, &count)); + TEST_EQ(count, 0); + + for (int32_t i = 0; i < DVX_ARRAY_LEN(files); i++) { + snprintf(path, sizeof(path), "%s/%s", dir, files[i]); + remove(path); + } + + snprintf(path, sizeof(path), "%s/sub/deep", dir); + rmdir(path); + snprintf(path, sizeof(path), "%s/sub", dir); + rmdir(path); + snprintf(path, sizeof(path), "%s/.hid", dir); + rmdir(path); + rmdir(dir); +} + + +// Count the files visited; each must be a file the walk was meant to see. +static bool walkCount(const char *path, const char *name, void *ctx) { + struct stat st; + + TEST_TRUE(name[0] != '.' && stat(path, &st) == 0 && S_ISREG(st.st_mode)); + (*(int32_t *)ctx)++; + return true; +} + + +static bool walkStop(const char *path, const char *name, void *ctx) { + (void)path; + (void)name; + (*(int32_t *)ctx)++; + return false; } diff --git a/src/test/web/Makefile b/src/test/web/Makefile index 5205b9f4..0a9e99a0 100644 --- a/src/test/web/Makefile +++ b/src/test/web/Makefile @@ -33,9 +33,9 @@ JSON_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/json HTTP_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/http BINDIR = $(DVXTEST_BINDIR)/web TEST_BIN = $(BINDIR)/webTest -TEST_SRCS = webTest.c jsonTest.c httpTest.c -LIB_SRCS = $(JSON_DIR)/json.c $(JSON_DIR)/jsonBas.c $(HTTP_DIR)/http.c $(HTTP_DIR)/httpBas.c -CFLAGS = $(DVXTEST_CFLAGS) -I$(JSON_DIR) -I$(HTTP_DIR) -I$(DVXTEST_BASICDIR) -DBASIC_INCLUDE_DIR='"$(DVXTEST_ROOT)/src/include/basic"' +TEST_SRCS = webTest.c jsonTest.c httpTest.c $(DVXTEST_DIR)/lookupHook.c +LIB_SRCS = $(JSON_DIR)/json.c $(JSON_DIR)/jsonBas.c $(HTTP_DIR)/http.c $(HTTP_DIR)/httpBas.c $(DVXTEST_DIR)/hostLookup.c +CFLAGS = $(DVXTEST_CFLAGS) -I$(JSON_DIR) -I$(HTTP_DIR) -I$(DVXTEST_ROOT)/src/libs/kpunch -I$(DVXTEST_BASICDIR) -DBASIC_INCLUDE_DIR='"$(DVXTEST_ROOT)/src/include/basic"' .PHONY: all run clean @@ -43,7 +43,7 @@ all: run $(TEST_BIN): $(TEST_SRCS) $(LIB_SRCS) $(DVX_MAKEFILES) $(DVXTEST_ARCHIVES) $(DVXTEST_TASKS_A) $(DVXTEST_BASRT_A) | dvxtest-libs @mkdir -p $(BINDIR) - $(HOSTCC) $(CFLAGS) -o $@ $(TEST_SRCS) $(LIB_SRCS) $(DVXTEST_BASRT_A) $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm + $(HOSTCC) $(CFLAGS) -o $@ $(TEST_SRCS) $(LIB_SRCS) $(DVXTEST_BASRT_A) $(DVXTEST_LDFLAGS) -Wl,--wrap=getaddrinfo -Wl,--whole-archive $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm run: $(TEST_BIN) cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1 ./webTest diff --git a/src/test/web/httpTest.c b/src/test/web/httpTest.c index ddcc915a..3a85a958 100644 --- a/src/test/web/httpTest.c +++ b/src/test/web/httpTest.c @@ -39,6 +39,7 @@ #include "compiler/compiler.h" #include "runtime/vm.h" #include "formrt/formrt.h" +#include "lookupHook.h" #include #include @@ -71,6 +72,7 @@ #define HT_CAPTURE_MAX 256 #define HT_BAD 99 // a handle never given #define HT_SLICE_MS 20 +#define HT_FILE_BODY 20000 // a body sent from a file: several pieces // ============================================================ // Prototypes @@ -83,6 +85,8 @@ static void htBodies(void); static void htCapture(void *ctx, const char *text, bool newline); static void htErrors(void); static void htFiles(void); +static void htFreeInPoll(void); +static void htFreeLater(void *ctx); static void htLive(void); static void htPost(void); static void htRedirects(void); @@ -173,6 +177,7 @@ static void htBasic(void) { TEST_STR_EQ(HttpError(HT_BAD), ""); TEST_EQ(HttpAddHeader(HT_BAD, "A", "b"), BAS_FALSE); TEST_EQ(HttpSetBody(HT_BAD, (const uint8_t *)"x", 1, ""), BAS_FALSE); + TEST_EQ(HttpSetBodyFile(HT_BAD, "x", ""), BAS_FALSE); HttpSaveTo(HT_BAD, "x", BAS_TRUE); HttpSetTimeout(HT_BAD, 1); } @@ -423,6 +428,26 @@ static void htFiles(void) { } +// A request freed while its poll waits in the name lookup (other code ran +// meanwhile, as it may while DVX's lookup waits) is freed when the poll +// returns, which then fails; nothing touches freed memory (ASan). +static void htFreeInPoll(void) { + tCase("http: freed while its poll waits for the name lookup"); + + HttpT *h = start("GET", "/hello"); + + tDuringLookup(htFreeLater, h); + TEST_EQ(httpPoll(h, msNow()), HttpFailedE); + TEST_TRUE(!tLookupPending()); +} + + +// htFreeInPoll's code in the lookup. +static void htFreeLater(void *ctx) { + httpFree((HttpT *)ctx); +} + + // A BASIC program fetching a page through http.bas. static void htLive(void) { static char src[HT_SRC_MAX]; @@ -489,10 +514,15 @@ static void htLive(void) { } -// A POST, and a 303 after it (a GET without the body). +// A POST, and a 303 after it (a GET without the body); a body sent from +// a file. static void htPost(void) { - char url[HT_URL_MAX]; - HttpT *h; + char url[HT_URL_MAX]; + char path[HT_PATH_MAX]; + char want[HT_URL_MAX]; + uint32_t sum = 0; + FILE *f; + HttpT *h; tCase("http: a POST, and a 303 after one"); urlOf(url, sizeof(url), "/post"); @@ -517,6 +547,40 @@ static void htPost(void) { TEST_EQ(runRequest(h), HttpDoneE); TEST_STR_EQ(httpBody(h, NULL), "GET "); httpFree(h); + + // From a file, a piece at a time: every byte, in order + snprintf(path, sizeof(path), "%s/BODY.DAT", tTempDir()); + f = fopen(path, "wb"); + TEST_TRUE(f != NULL); + + for (int32_t i = 0; f && i < HT_FILE_BODY; i++) { + fputc(bigByte(i), f); + sum += bigByte(i); + } + + if (f) { + fclose(f); + } + + urlOf(url, sizeof(url), "/count"); + h = httpCreate("POST", url); + TEST_TRUE(httpBodyFile(h, path, "application/octet-stream")); + TEST_TRUE(httpStart(h, msNow())); + TEST_EQ(runRequest(h), HttpDoneE); + snprintf(want, sizeof(want), "%d %lu", HT_FILE_BODY, (unsigned long)sum); + TEST_STR_EQ(httpBody(h, NULL), want); + httpFree(h); + + // ... dropped by a 303 as one in memory is; not a file: refused + urlOf(url, sizeof(url), "/seeother"); + h = httpCreate("POST", url); + TEST_TRUE(httpBodyFile(h, path, "")); + TEST_TRUE(httpStart(h, msNow())); + TEST_EQ(runRequest(h), HttpDoneE); + TEST_STR_EQ(httpBody(h, NULL), "GET "); + TEST_TRUE(!httpBodyFile(h, "nowhere.dat", "")); + TEST_TRUE(!httpBodyFile(h, tTempDir(), "")); + httpFree(h); } @@ -689,6 +753,38 @@ static void serveOne(int conn) { bodyLen = cl ? strtol(cl + strlen("Content-Length: "), NULL, 10) : 0; range = rg ? strtol(rg + strlen("Range: bytes="), NULL, 10) : -1; + // /count takes a body of any length: its bytes counted and summed + if (strcmp(path, "/count") == 0) { + long count = 0; + uint32_t total = 0; + uint8_t piece[HT_REPLY_MAX]; + char text[HT_REPLY_MAX]; + + for (const char *p = end + 4; p < req + got; p++) { + total += (uint8_t)*p; + count++; + } + + while (count < bodyLen) { + ssize_t n = recv(conn, piece, sizeof(piece), 0); + + if (n <= 0) { + break; + } + + for (ssize_t i = 0; i < n; i++) { + total += piece[i]; + } + + count += (long)n; + } + + snprintf(text, sizeof(text), "%ld %lu", count, (unsigned long)total); + snprintf(reply, sizeof(reply), "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s", (int)strlen(text), text); + sendAll(conn, reply, strlen(reply)); + return; + } + // The body, after the head char *body = end + 4; int32_t haveLen = got - (int32_t)(body - req); @@ -707,6 +803,19 @@ static void serveOne(int conn) { body[bodyLen < haveLen ? bodyLen : haveLen] = '\0'; + // A body longer than the request kept is read to its end anyway, as a + // server would before answering + for (long left = bodyLen - haveLen; left > 0; ) { + char drop[HT_REPLY_MAX]; + ssize_t n = recv(conn, drop, (size_t)(left < (long)sizeof(drop) ? left : (long)sizeof(drop)), 0); + + if (n <= 0) { + break; + } + + left -= (long)n; + } + if (strcmp(path, "/hello") == 0 || strcmp(path, "/sub/hello") == 0) { snprintf(reply, sizeof(reply), "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\n%s", strcmp(method, "HEAD") == 0 ? "" : "hello"); sendAll(conn, reply, strlen(reply)); @@ -863,6 +972,7 @@ void testHttp(void) { htPost(); htFiles(); htErrors(); + htFreeInPoll(); htBasic(); htLive(); kill(sServer, SIGTERM); diff --git a/src/test/web/jsonTest.c b/src/test/web/jsonTest.c index b65c4936..e9cf420e 100644 --- a/src/test/web/jsonTest.c +++ b/src/test/web/jsonTest.c @@ -70,6 +70,7 @@ static void jtLive(void); static void jtLookups(void); static void jtQuote(void); static void jtStrings(void); +static void jtUnicode(void); void testJson(void); static char sCapture[JT_CAPTURE_MAX]; @@ -306,6 +307,10 @@ static void jtQuote(void) { TEST_STR_EQ(out, "\"ab"); TEST_EQ(jsonQuote("abc", NULL, 0), 5); + // Characters from 128 on go out as their Unicode code points + TEST_EQ(jsonQuote("caf\x82 \xc4", out, sizeof(out)), 18); + TEST_STR_EQ(out, "\"caf\\u00e9 \\u2500\""); + // What jsonQuote writes reads back as the text char err[JT_ERR_MAX]; JsonDocT *doc; @@ -315,6 +320,12 @@ static void jtQuote(void) { TEST_TRUE(doc != NULL); TEST_STR_EQ(jsonText(doc, 0), "a\"b\\c\nd\re\tf\001"); jsonFree(doc); + + jsonQuote("caf\x82 \xc4\x01", out, sizeof(out)); + doc = jsonParse(out, (int32_t)strlen(out), err, sizeof(err)); + TEST_TRUE(doc != NULL); + TEST_STR_EQ(jsonText(doc, 0), "caf\x82 \xc4\x01"); + jsonFree(doc); } @@ -332,13 +343,13 @@ static void jtStrings(void) { return; } - TEST_STR_EQ(jsonText(doc, jsonFind(doc, 0, "e")), "a\nb\t\"c\\d/eA?\b\f\r"); + TEST_STR_EQ(jsonText(doc, jsonFind(doc, 0, "e")), "a\nb\t\"c\\d/eA\x82\b\f\r"); TEST_TRUE(jsonNumber(doc, jsonFind(doc, 0, "weird")) == 1.0); TEST_TRUE(jsonNumber(doc, jsonFind(doc, 0, "n")) == -50.0); TEST_TRUE(jsonNumber(doc, jsonFind(doc, 0, "z")) == 0.0); TEST_TRUE(jsonNumber(doc, jsonFind(doc, 0, "big")) == 12345678901.0); TEST_TRUE(jsonNumber(doc, jsonFind(doc, 0, "dup")) == 1.0); - TEST_STR_EQ(jsonText(doc, jsonFind(doc, 0, "raw")), "caf\xc3\xa9"); + TEST_STR_EQ(jsonText(doc, jsonFind(doc, 0, "raw")), "caf\x82"); TEST_STR_EQ(jsonKey(doc, jsonChild(doc, 0, 2)), ""); jsonFree(doc); @@ -349,11 +360,43 @@ static void jtStrings(void) { } +// Unicode in strings, as \u escapes and as UTF-8, becomes DVX text +// (code page 437), '?' for a character DVX does not have; bytes that are +// not UTF-8 pass through. +static void jtUnicode(void) { + static const struct { + const char *json; + const char *text; + } cases[] = { + { "\"\\u2500\\u2591\"", "\xc4\xb0" }, + { "\"\\ud83d\\ude00!\"", "?!" }, + { "\"\\ud83dA\"", "?A" }, + { "\"\\ud83d\\u0041\"", "?A" }, + { "\"\\u4e2d\\u263a\"", "?\x01" }, + { "\"\xe2\x94\x80\xf0\x9f\x98\x80\"", "\xc4?" }, + { "\"\xc9x\xe2\x94\"", "\xc9x\xe2\x94" }, + { "\"\xc0\x81\xed\xa0\x80\"", "\xc0\x81\xed\xa0\x80" } + }; + char err[JT_ERR_MAX]; + + tCase("json: Unicode becomes DVX text"); + + for (int32_t i = 0; i < DVX_ARRAY_LEN(cases); i++) { + JsonDocT *doc = jsonParse(cases[i].json, (int32_t)strlen(cases[i].json), err, sizeof(err)); + + TEST_TRUE(doc != NULL); + TEST_STR_EQ(doc ? jsonText(doc, 0) : err, cases[i].text); + jsonFree(doc); + } +} + + void testJson(void) { jtLookups(); jtStrings(); jtErrors(); jtQuote(); + jtUnicode(); jtBasic(); jtLive(); } diff --git a/src/test/widgets/testCovListView.c b/src/test/widgets/testCovListView.c index f6a1860e..cbd1a4db 100644 --- a/src/test/widgets/testCovListView.c +++ b/src/test/widgets/testCovListView.c @@ -28,6 +28,8 @@ // the BASIC SetColumns spec parser. #include "wtCommon.h" +#include "box/box.h" +#include "button/button.h" #include "listView/listView.h" #include @@ -52,6 +54,8 @@ #define LV_LETTER_TO_UPPER 32 #define LV_BAD_SORT_DIR 7 // not a ListViewSortE value #define LV_DESTROY_CHANGE 2 // onChange count at which onDestroyOnSecond destroys +#define LV_INC_ROWS 7 // caseIncremental's rows +#define LV_INC_WIDE_ROW 4 // the one with the widest value static const char *sNames[LV_ROW_COUNT] = { "Delta", "alpha", "Charlie", "Bravo", "Echo", "Foxtrot" }; @@ -62,6 +66,7 @@ static const char *sNames[LV_ROW_COUNT] = { "Delta", "alpha", "Charlie", "Bravo" static void caseColumnResize(void); static void caseCursorAndWheel(void); +static void caseIncremental(void); static void caseMultiSelect(void); static void caseOwnedCells(void); static void caseRegressReorder(void); @@ -75,6 +80,7 @@ static int32_t charH(void); static int32_t charW(void); static int32_t colBorderX(const WidgetT *w, int32_t col); static int32_t cursorShapeAt(const WidgetT *w, int32_t vx, int32_t vy); +static void fillRows(WidgetT *lv, const WidgetT *from); static int32_t headerY(const WidgetT *w); static void onDbl(WidgetT *w); static void onDestroyOnSecond(WidgetT *w); @@ -210,6 +216,79 @@ static void caseCursorAndWheel(void) { } +// A sorted list changed row by row, painted after every change (so its +// sort and column widths are built, then kept up), looks the same as one +// given the same rows with no paint between them (sorted and measured +// whole): rows added (one far wider), a sort cell set, the widest row +// removed, the sort turned round and a row added again. +static void caseIncremental(void) { + tCase("listView: a built sort and column widths kept up row by row"); + + static const char *const keys[LV_INC_ROWS] = { "m", "c", "x", "c", "a", "m", "b" }; + + WtFixtureT f = wtFixture("LvIncremental"); + WidgetT *row = wgtHBox(f.root); + WidgetT *kept = wgtListView(row); + WidgetT *whole = wgtListView(row); + WidgetT *other = wgtButton(f.root, "Focus"); + ListViewColT cols[2]; + + memset(cols, 0, sizeof(cols)); + cols[0].title = "Key"; + cols[1].title = "Value"; + row->weight = WGT_WEIGHT_FILL; + kept->weight = WGT_WEIGHT_FILL; + whole->weight = WGT_WEIGHT_FILL; + wgtListViewSetColumns(kept, cols, 2); + wgtListViewSetColumns(whole, cols, 2); + wgtListViewSetSort(kept, 0, ListViewSortAscE); + wtSettle(); + wgtSetFocused(other); + + for (int32_t i = 0; i < LV_INC_ROWS; i++) { + wgtListViewAddItem(kept, keys[i]); + wgtListViewSetCell(kept, i, 1, i == LV_INC_WIDE_ROW ? "a value far wider than the rest" : "v"); + wtSettle(); + } + + wgtListViewSetCell(kept, 2, 0, "c"); + wtSettle(); + wgtListViewRemoveRow(kept, LV_INC_WIDE_ROW); + wtSettle(); + wgtListViewSetSort(kept, 0, ListViewSortDescE); + wtSettle(); + wgtListViewAddItem(kept, "c"); + wgtListViewSetCell(kept, LV_INC_ROWS - 1, 1, "w"); + wtSettle(); + + // The same rows, sorted and measured whole + wgtListViewSetSort(whole, 0, ListViewSortDescE); + fillRows(whole, kept); + wtSettle(); + + TEST_EQ(kept->w, whole->w); + TEST_EQ(kept->h, whole->h); + + int32_t differ = 0; + + for (int32_t y = 0; y < kept->h && kept->w == whole->w && kept->h == whole->h; y++) { + for (int32_t x = 0; x < kept->w; x++) { + differ += wtContentPixel(f.win, kept->x + x, kept->y + y) != wtContentPixel(f.win, whole->x + x, whole->y + y); + } + } + + TEST_EQ(differ, 0); + + // Its order: the keys descending, equal keys in data order + wgtSetFocused(kept); + tWidgetKey(f.win, KEY_HOME, 0); + TEST_STR_EQ(wgtListViewGetCell(kept, wgtListViewGetSelected(kept), 0), "m"); + tWidgetKey(f.win, KEY_END, 0); + TEST_STR_EQ(wgtListViewGetCell(kept, wgtListViewGetSelected(kept), 0), "b"); + tShutdown(); +} + + static void caseMultiSelect(void) { tCase("listView: multi-select ranges by mouse and keyboard, Enter, Space, Ctrl+A"); @@ -1000,6 +1079,16 @@ static int32_t cursorShapeAt(const WidgetT *w, int32_t vx, int32_t vy) { } +// caseIncremental: lv given from's rows (two columns), with no paint +// between them. +static void fillRows(WidgetT *lv, const WidgetT *from) { + for (int32_t r = 0; r < wgtListViewGetRowCount(from); r++) { + wgtListViewAddItem(lv, wgtListViewGetCell(from, r, 0)); + wgtListViewSetCell(lv, r, 1, wgtListViewGetCell(from, r, 1)); + } +} + + static int32_t headerY(const WidgetT *w) { return w->y + LV_BORDER + (charH() + LV_HEADER_VPAD) / 2; } @@ -1076,6 +1165,7 @@ void wtSectionCovListView(void) { caseOwnedCells(); caseSetColumnsSpec(); caseCursorAndWheel(); + caseIncremental(); caseRegressSort(); caseRegressRows(); caseRegressReorder(); diff --git a/src/test/widgets/testCovMisc.c b/src/test/widgets/testCovMisc.c index bc2c1fc2..aa57a4f1 100644 --- a/src/test/widgets/testCovMisc.c +++ b/src/test/widgets/testCovMisc.c @@ -483,8 +483,10 @@ static void caseScrollPaneHorizontal(void) { int32_t sbY = sp->y + sp->h - CM_SP_BORDER - CM_SP_SB_W / 2; int32_t sbX = sp->x + sp->w - CM_SP_BORDER - CM_SP_SB_W / 2; - // Left/Right keys scroll horizontally; PgUp/PgDn page vertically. + // Left/Right keys scroll horizontally; PgUp/PgDn page vertically. A + // scroll lays the pane out at once (the window is not measured again). wclsOnKey(sp, KEY_RIGHT, 0); + TEST_EQ(wide->x, x0 - cmCharW()); wtSettle(); TEST_EQ(wide->x, x0 - cmCharW()); wclsOnKey(sp, KEY_LEFT, 0); diff --git a/src/test/widgets/testCovPopups.c b/src/test/widgets/testCovPopups.c index 890b496b..e9f41677 100644 --- a/src/test/widgets/testCovPopups.c +++ b/src/test/widgets/testCovPopups.c @@ -29,6 +29,7 @@ // in testLists.c. #include "wtCommon.h" +#include "dvxHash.h" #include "comboBox/comboBox.h" #include "dropdown/dropdown.h" #include "label/label.h" @@ -56,8 +57,6 @@ #define PP_HS_CAP 80 // a list box's MaxWidth, narrower than PP_HS_LONG #define PP_HS_LONG "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn" // every character a different glyph #define PP_HS_PRESSES 60 // more Rights than PP_HS_LONG has characters -#define PP_FNV_BASIS 2166136261u -#define PP_FNV_PRIME 16777619u // ============================================================ @@ -607,13 +606,13 @@ static const WgtMethodDescT *findMethod(const char *widget, const char *method) // of which of the cell's pixels are not the background. static uint32_t lbCellShape(const WindowT *win, const WidgetT *lb, int32_t col) { uint32_t content = tCtx()->colors.contentBg; - uint32_t hash = PP_FNV_BASIS; + uint32_t hash = DVX_FNV_BASIS; int32_t cellX = lb->x + PP_BORDER + PP_LB_PAD + col * charWd(); int32_t cellY = lb->y + PP_BORDER; for (int32_t y = 0; y < charHt(); y++) { for (int32_t x = 0; x < charWd(); x++) { - hash = (hash ^ (wtContentPixel(win, cellX + x, cellY + y) != content ? 1u : 0u)) * PP_FNV_PRIME; + hash = dvxFnvByte(hash, wtContentPixel(win, cellX + x, cellY + y) != content); } } diff --git a/src/test/widgets/testCovTerm.c b/src/test/widgets/testCovTerm.c index 7ddd06ea..086eafd3 100644 --- a/src/test/widgets/testCovTerm.c +++ b/src/test/widgets/testCovTerm.c @@ -57,12 +57,17 @@ #define TERM_WHEEL_NOTCH 1 #define TERM_HELLO_LEN 5 #define TERM_SCHEME_BASE 10 +#define TERM_LONG_TEXT 9000 // caseTermLongWrite: more than two parse budgets (4096) +#define TERM_LONG_TAIL "\x1b[1;1HDONE" // its end: the cursor home, then DONE +#define TERM_LONG_POLLS 3 // polls that parse the rest #define TERM_SCROLL_ONE 1 #define TERM_SCROLL_SOME 3 #define TERM_LINE_MAX 32 #define TERM_ANSI_COLORS 7 // SGR 31..37 #define TERM_MACRO_MAX 64 // the longest music string a case expects #define TERM_MUSIC_LONG 4200 // more than the widget's ANSI_MUSIC_MAX (4096) +#define TERM_NUL_TEXT_LEN 3 // "A", CHR$(0), "B" +#define TERM_KEYS_SENT_LEN 4 // "h" then ESC [ A typedef struct { uint8_t feed[TERM_FEED_MAX]; @@ -70,6 +75,8 @@ typedef struct { int32_t feedPos; uint8_t sent[TERM_SENT_MAX]; int32_t sentLen; + int32_t room; // when limited: bytes it takes before it is full + bool limited; } FakeCommT; // What the Music event's handler does besides counting (recordEvent) @@ -77,14 +84,18 @@ typedef enum { TermActNoneE, TermActWriteE, // writes "Z" to the terminal TermActPollE, // gives the connection a "Q" and polls - TermActDestroyE // destroys the terminal + TermActDestroyE, // destroys the terminal + TermActKeyE // types "k" (the Bell handler) } TermActE; -// The Music and Bell events the terminal fired (recordEvent) +// The Music, Bell, and Send events the terminal fired (recordEvent) typedef struct { int32_t bells; int32_t musics; + int32_t sends; char macro[TERM_MACRO_MAX]; + uint8_t sent[TERM_SENT_MAX]; // the Send events' bytes, one after another + int32_t sentLen; TermActE action; FakeCommT *comm; int32_t pollRead; // what the Poll in the handler read @@ -99,6 +110,7 @@ static TermEventsT sEvents; static void caseTermCursorOps(void); static void caseTermExtensions(void); +static void caseTermLongWrite(void); static void caseTermMusic(void); static void caseTermRegressions(void); static void caseTermResizeHistory(void); @@ -106,6 +118,7 @@ static void caseTermScrollbackKeys(void); static void caseTermScrollPaint(void); static void caseTermScrollRegion(void); static void caseTermSelection(void); +static void caseTermSend(void); static void caseTermSgrBlink(void); static int32_t cellX(const WidgetT *term, int32_t col); static int32_t cellY(const WidgetT *term, int32_t row); @@ -116,6 +129,7 @@ static int32_t fakeWrite(void *ctx, const uint8_t *data, int32_t len); static void feedTerm(FakeCommT *c, WidgetT *term, const char *text); static void recordEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc); static bool rowIs(WindowT *win, WidgetT *term, int32_t row, const char *expect); +static bool sendIs(const char *expect, int32_t len); static bool sentIs(const FakeCommT *c, const char *expect); static bool termMatchesFullPaint(WindowT *win, WidgetT *term); @@ -246,6 +260,53 @@ static void caseTermExtensions(void) { } +// A long text written is parsed a budget at a time, the rest at the next +// polls (a frame each); a Clear right after one clears what it wrote too, +// which does not come back after the clear. +static void caseTermLongWrite(void) { + tCase("ansiTerm: a long Write parsed over polls, a Clear after it"); + + WtFixtureT f = wtFixture("TermLong"); + WidgetT *term = wgtAnsiTerm(f.root, TERM_COLS, TERM_ROWS); + FakeCommT comm; + char *text = (char *)malloc(TERM_LONG_TEXT + 1); + int32_t tail = (int32_t)strlen(TERM_LONG_TAIL); + + memset(&comm, 0, sizeof(comm)); + wgtAnsiTermSetComm(term, &comm, fakeRead, fakeWrite); + wtSettle(); + TEST_TRUE(text != NULL); + + if (!text) { + tShutdown(); + return; + } + + memset(text, 'x', TERM_LONG_TEXT); + memcpy(text + TERM_LONG_TEXT - tail, TERM_LONG_TAIL, (size_t)tail); + text[TERM_LONG_TEXT] = '\0'; + wgtAnsiTermWrite(term, (const uint8_t *)text, TERM_LONG_TEXT); + TEST_TRUE(rowIs(f.win, term, 0, "xxxx")); + + for (int32_t i = 0; i < TERM_LONG_POLLS; i++) { + wgtAnsiTermPoll(term); + } + + TEST_TRUE(rowIs(f.win, term, 0, "DONE")); + + wgtAnsiTermWrite(term, (const uint8_t *)text, TERM_LONG_TEXT); + wgtAnsiTermClear(term); + + for (int32_t i = 0; i < TERM_LONG_POLLS; i++) { + wgtAnsiTermPoll(term); + } + + expectCursor(&comm, term, 1, 1); + free(text); + tShutdown(); +} + + // ANSI music: ESC[| and ESC[N always, ESC[M with a mode letter after it // (bare ESC[M otherwise still deletes a line), to Ctrl+N, kept off the // screen and handed to the Music event; BEL to the Bell event. While a @@ -692,6 +753,19 @@ static void caseTermSelection(void) { tWidgetKey(f.win, KEY_CTRL_V, KEY_MOD_CTRL); TEST_TRUE(sentIs(&comm, "AB CDE FG\rnex")); + // A link with room for only part of it: the rest goes from the polls + // once the link takes more + comm.sentLen = 0; + comm.limited = true; + comm.room = 4; + tWidgetKey(f.win, KEY_CTRL_V, KEY_MOD_CTRL); + TEST_TRUE(sentIs(&comm, "AB C")); + tPump(2); + TEST_TRUE(sentIs(&comm, "AB C")); + comm.limited = false; + tPump(2); + TEST_TRUE(sentIs(&comm, "AB CDE FG\rnex")); + // A selection ending at column 0 leaves that row's painted span // empty; drag clamping pins coordinates to the cell grid. wtSettle(); @@ -722,6 +796,59 @@ static void caseTermSelection(void) { } +// No link attached (a program with its own transport): typed keys, a +// paste, and the replies to the host's status requests go to the Send +// event, bytes and all; a key typed while another event runs is sent +// when that one returns. Write takes NUL bytes from BASIC (its dynamic +// method) without stopping at them. +static void caseTermSend(void) { + tCase("ansiTerm: Send event without a link, Write with NUL bytes"); + + WtFixtureT f = wtFixture("TermSend"); + WidgetT *term = wgtAnsiTerm(f.root, TERM_COLS, TERM_ROWS); + WgtValueT arg = wgtValueBytes("A\0B", TERM_NUL_TEXT_LEN); + WgtValueT ret; + + memset(&sEvents, 0, sizeof(sEvents)); + term->onEvent = recordEvent; + wtSettle(); + wtClickAt(f.win, cellX(term, 0), cellY(term, 0)); + TEST_PTR_EQ(wgtGetFocused(), term); + + // Keys: one Send each + tWidgetKey(f.win, 'h', 0); + tWidgetKey(f.win, KEY_UP, 0); + TEST_EQ(sEvents.sends, 2); + TEST_TRUE(sendIs("h\x1b[A", TERM_KEYS_SENT_LEN)); + + // The host asks where the cursor is: the reply goes once the byte is parsed + wgtAnsiTermWrite(term, (const uint8_t *)"\fab\x1b[6n", (int32_t)strlen("\fab\x1b[6n")); + TEST_TRUE(sendIs("\x1b[1;3R", (int32_t)strlen("\x1b[1;3R"))); + + // A paste, newlines as CR, in one Send + clipboardCopy("hi\nyo", (int32_t)strlen("hi\nyo")); + sEvents.sends = 0; + tWidgetKey(f.win, KEY_CTRL_V, KEY_MOD_CTRL); + TEST_EQ(sEvents.sends, 1); + TEST_TRUE(sendIs("hi\ryo", (int32_t)strlen("hi\ryo"))); + + // A key typed in the Bell handler waits for it, then goes + sEvents.action = TermActKeyE; + sEvents.sends = 0; + wgtAnsiTermWrite(term, (const uint8_t *)"\a", 1); + TEST_EQ(sEvents.bells, 1); + TEST_EQ(sEvents.sends, 1); + TEST_TRUE(sendIs("k", 1)); + sEvents.action = TermActNoneE; + + // Write "A" + CHR$(0) + "B": the NUL does not end the text + wgtAnsiTermWrite(term, (const uint8_t *)"\f", 1); + TEST_TRUE(wgtCallMethod(term, "Write", &arg, 1, &ret)); + TEST_TRUE(rowIs(f.win, term, 0, "AB")); + tShutdown(); +} + + static void caseTermSgrBlink(void) { tCase("ansiTerm: SGR attribute set, blink cell polling"); @@ -804,9 +931,15 @@ static int32_t fakeRead(void *ctx, uint8_t *buf, int32_t max) { } +// Takes everything, or (limited) only what room is left. static int32_t fakeWrite(void *ctx, const uint8_t *data, int32_t len) { FakeCommT *c = (FakeCommT *)ctx; + if (c->limited) { + len = DVX_MIN(len, c->room); + c->room -= len; + } + for (int32_t i = 0; i < len && c->sentLen < TERM_SENT_MAX; i++) { c->sent[c->sentLen++] = data[i]; } @@ -826,11 +959,29 @@ static void feedTerm(FakeCommT *c, WidgetT *term, const char *text) { } -// The Music and Bell events: counted, the string kept, and the action of -// the case done +// The Music, Bell, and Send events: counted, the string or bytes kept, +// and the action of the case done static void recordEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc) { + if (strcmp(name, ANSITERM_EVT_SEND) == 0) { + int32_t len = (argc == 1) ? wgtValueLen(&args[0]) : 0; + + sEvents.sends++; + + if (sEvents.sentLen + len <= TERM_SENT_MAX) { + memcpy(sEvents.sent + sEvents.sentLen, args[0].s, (size_t)len); + sEvents.sentLen += len; + } + + return; + } + if (strcmp(name, ANSITERM_EVT_BELL) == 0) { sEvents.bells++; + + if (sEvents.action == TermActKeyE) { + tWidgetKey(w->window, 'k', 0); + } + return; } @@ -856,6 +1007,7 @@ static void recordEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t a break; case TermActNoneE: + case TermActKeyE: break; } } @@ -870,6 +1022,15 @@ static bool rowIs(WindowT *win, WidgetT *term, int32_t row, const char *expect) } +// The Send events handed over exactly len bytes of expect, in all +static bool sendIs(const char *expect, int32_t len) { + bool same = sEvents.sentLen == len && memcmp(sEvents.sent, expect, (size_t)len) == 0; + + sEvents.sentLen = 0; + return same; +} + + static bool sentIs(const FakeCommT *c, const char *expect) { int32_t len = (int32_t)strlen(expect); @@ -915,6 +1076,7 @@ static bool termMatchesFullPaint(WindowT *win, WidgetT *term) { void wtSectionCovTerm(void) { caseTermCursorOps(); caseTermExtensions(); + caseTermLongWrite(); caseTermMusic(); caseTermRegressions(); caseTermResizeHistory(); @@ -922,5 +1084,6 @@ void wtSectionCovTerm(void) { caseTermScrollRegion(); caseTermScrollbackKeys(); caseTermSelection(); + caseTermSend(); caseTermSgrBlink(); } diff --git a/src/test/widgets/testCovText.c b/src/test/widgets/testCovText.c index c4b56bbc..92af10d9 100644 --- a/src/test/widgets/testCovText.c +++ b/src/test/widgets/testCovText.c @@ -113,6 +113,7 @@ static void caseMaskedInput(void); static void caseSpinnerReal(void); static void caseSpinnerText(void); static void covAreaClick(WindowT *win, WidgetT *ta, int32_t gutterW, int32_t row, int32_t col); +static bool covCacheIs(TextEditLineCacheT *lc, const char *buf); static bool covClipIs(const char *expect); static void covColorizeEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc); static void covDestroyOnChange(WidgetT *w); @@ -331,9 +332,25 @@ static void caseAreaCacheLibrary(void) { TEST_EQ(lc.cachedLines, -1); TEST_EQ(textEditLineCount(&lc, "a\nb\ncd", 6), 3); - // An inserted newline dirties the cache. + // Inserted newlines (Enter with its indent, a paste of lines) and a + // deleted one are patched into the table: the same table a rebuild + // makes, no scan of the text + textEditLineCacheDirty(&lc); + TEST_EQ(textEditLineCount(&lc, "ab\ncd", 5), 2); textEditLineCacheNotifyInsert(&lc, "a\nb\ncd", 6, 1, 1); - TEST_EQ(lc.cachedLines, -1); + TEST_EQ(lc.cachedLines, 3); + TEST_TRUE(covCacheIs(&lc, "a\nb\ncd")); + textEditLineCacheNotifyInsert(&lc, "a\nb\nX\n\tY\nZcd", 12, 4, 6); + TEST_EQ(lc.cachedLines, 5); + TEST_TRUE(covCacheIs(&lc, "a\nb\nX\n\tY\nZcd")); + textEditLineCacheNotifyInsert(&lc, "a\nb\nX\n\tY\nZcd\n ", 15, 12, 3); + TEST_TRUE(covCacheIs(&lc, "a\nb\nX\n\tY\nZcd\n ")); + textEditLineCacheNotifyDelete(&lc, "a\nbX\n\tY\nZcd\n ", 14, 3, 1, true); + TEST_EQ(lc.cachedLines, 5); + TEST_TRUE(covCacheIs(&lc, "a\nbX\n\tY\nZcd\n ")); + textEditLineCacheNotifyDelete(&lc, "a\nbX\n\tY\nZcd ", 13, 11, 1, true); + TEST_TRUE(covCacheIs(&lc, "a\nbX\n\tY\nZcd ")); + textEditLineCacheDirty(&lc); // Deleting the only byte of the first line collapses its span. TEST_EQ(textEditLineCount(&lc, "\na", 2), 2); @@ -527,6 +544,22 @@ static void caseAreaColorizeEvent(void) { TEST_TRUE(sCovColorizeLines >= 2); TEST_TRUE(covPaintHasRgb(ta, textEditDefaultSyntaxRgb(TEXT_SYNTAX_KEYWORD))); + // Drawn again unchanged, the lines keep their colours without the + // event; only a changed line asks again; the mode set again asks for + // every line afresh + sCovColorizeLines = 0; + wgtInvalidatePaint(ta); + wtSettle(); + TEST_EQ(sCovColorizeLines, 0); + TEST_TRUE(covPaintHasRgb(ta, textEditDefaultSyntaxRgb(TEXT_SYNTAX_KEYWORD))); + wgtSetText(ta, "kkk plain\nkk"); + wtSettle(); + TEST_EQ(sCovColorizeLines, 1); + sCovColorizeLines = 0; + wgtTextAreaSetSyntaxMode(ta, "custom"); + wtSettle(); + TEST_TRUE(sCovColorizeLines >= 2); + // Another mode stops the events wgtTextAreaSetSyntaxMode(ta, ""); sCovColorizeLines = 0; @@ -1468,13 +1501,14 @@ static void caseEditRegressions(void) { wgtSetText(mkTmp, "42"); TEST_STR_EQ(wgtGetText(mkTmp), "42"); - // Deleting a newline must rebuild the line table, never patch it. + // Deleting a newline joins its line and the next in the table. TextEditLineCacheT lc; textEditLineCacheInit(&lc, 0); TEST_EQ(textEditLineCount(&lc, "ab\ncd", 5), 2); textEditLineCacheNotifyDelete(&lc, "abcd", 4, 2, 1, true); - TEST_EQ(textEditLineCount(&lc, "abcd", 4), 1); + TEST_EQ(lc.cachedLines, 1); + TEST_TRUE(covCacheIs(&lc, "abcd")); textEditLineCacheFree(&lc); tShutdown(); } @@ -2115,6 +2149,26 @@ static void covAreaClick(WindowT *win, WidgetT *ta, int32_t gutterW, int32_t row } +// lc (valid) is the table a rebuild for buf makes: its line starts, the +// tab-expanded lengths, and the line count. +static bool covCacheIs(TextEditLineCacheT *lc, const char *buf) { + TextEditLineCacheT fresh; + int32_t len = (int32_t)strlen(buf); + bool ok = lc->cachedLines >= 0; + + textEditLineCacheInit(&fresh, lc->tabWidth); + ok = ok && textEditLineCount(&fresh, buf, len) == lc->cachedLines; + + for (int32_t i = 0; ok && i < lc->cachedLines; i++) { + ok = lc->lineOffsets[i] == fresh.lineOffsets[i] && lc->lineVisLens[i] == fresh.lineVisLens[i]; + } + + ok = ok && lc->lineOffsets[lc->cachedLines] == fresh.lineOffsets[fresh.cachedLines]; + textEditLineCacheFree(&fresh); + return ok; +} + + static bool covClipIs(const char *expect) { int32_t len = 0; const char *clip = clipboardGet(&len); diff --git a/src/test/widgets/testCovTreeView.c b/src/test/widgets/testCovTreeView.c index a849b588..a85ab9ba 100644 --- a/src/test/widgets/testCovTreeView.c +++ b/src/test/widgets/testCovTreeView.c @@ -64,6 +64,7 @@ static void caseCallbackDestroys(void); static void caseDragIndicator(void); static void caseExpandSelection(void); static void caseIconAndClamp(void); +static void caseIndexKept(void); static void caseItemText(void); static void caseKeyboardEdge(void); static void caseMultiSelectRange(void); @@ -422,6 +423,56 @@ static void caseIconAndClamp(void) { } +// Items added one by one keep the depth-first index right without a +// rebuild between adds (each add follows a query, so the index is valid): +// under the tree, under a root that has a subtree, under a deep item, and +// under an earlier item, with a child of another kind last in the tree. +static void caseIndexKept(void) { + tCase("treeView: the index keeps up with items added one by one"); + + WtFixtureT f = wtFixture("TreeIdx"); + WidgetT *tv = wgtTreeView(f.root); + + MethodStrFnT addItem = (MethodStrFnT)findMethod("AddItem"); + MethodIntStrFnT addChildItem = (MethodIntStrFnT)findMethod("AddChildItem"); + MethodRetStrIntFnT getItemText = (MethodRetStrIntFnT)findMethod("GetItemText"); + MethodRetIntFnT itemCount = (MethodRetIntFnT)findMethod("ItemCount"); + + TEST_TRUE(addItem && addChildItem && getItemText && itemCount); + + if (!addItem || !addChildItem || !getItemText || !itemCount) { + tShutdown(); + return; + } + + static const char *const want[] = { "A", "A1", "A1a", "A1a-x", "A2", "B", "C" }; + + addItem(tv, "A"); + TEST_EQ(itemCount(tv), 1); + addChildItem(tv, 0, "A1"); + TEST_STR_EQ(getItemText(tv, 1), "A1"); + addChildItem(tv, 1, "A1a"); + TEST_STR_EQ(getItemText(tv, 2), "A1a"); + addItem(tv, "B"); + TEST_STR_EQ(getItemText(tv, 3), "B"); + wgtLabel(tv, "not an item"); + addChildItem(tv, 0, "A2"); + TEST_STR_EQ(getItemText(tv, 3), "A2"); + TEST_STR_EQ(getItemText(tv, 4), "B"); + addItem(tv, "C"); + TEST_STR_EQ(getItemText(tv, 5), "C"); + addChildItem(tv, 2, "A1a-x"); + TEST_EQ(itemCount(tv), DVX_ARRAY_LEN(want)); + + for (int32_t i = 0; i < DVX_ARRAY_LEN(want); i++) { + TEST_STR_EQ(getItemText(tv, i), want[i]); + } + + wtSettle(); + tShutdown(); +} + + // Item text get/set through the generic widget text API, NULL text, // and next-visible traversal past a NULL-text item. static void caseItemText(void) { @@ -1117,6 +1168,7 @@ void wtSectionCovTreeView(void) { caseDragIndicator(); caseExpandSelection(); caseIconAndClamp(); + caseIndexKept(); caseItemText(); caseKeyboardEdge(); caseMultiSelectRange(); diff --git a/src/test/widgets/testData.c b/src/test/widgets/testData.c index 9e55ae2c..7ade031a 100644 --- a/src/test/widgets/testData.c +++ b/src/test/widgets/testData.c @@ -55,6 +55,13 @@ #define TD_WHEEL_ROWS 40 // more rows than the grid shows #define TD_VALIDATE_MAX 8 // nested Validate calls before the handler stops recursing #define TD_OOM_TEXT "td-oom" // cell text whose strdup fails while sFailStrdup is set +#define TD_FIRST_ROWS 64 // the rows a Refresh reads (DATA_FIRST_ROWS); the polls read the rest +#define TD_PAGED_ROWS 500 // a record set the polls finish +#define TD_PAGED_OOM_ID 100 // a row the polls read (past the first ones) +#define TD_WIDTH_ROWS 200 // the rows auto column widths measure (DBGRID_WIDTH_ROWS) +#define TD_WIDTH_IN_ID 100 // a row among them, read by the polls +#define TD_WIDTH_OUT_ID 250 // a row past them +#define TD_WIDE_NAME "pWWWWWWWWWWWWWWW" typedef struct { WtFixtureT fx; @@ -64,6 +71,7 @@ typedef struct { } TdFixtureT; static int32_t sDbSeq = 0; +static int32_t sChanges = 0; // tdCountChange calls static int32_t sValidateCalls = 0; static int32_t sReentrant = 0; static WidgetT *sVictim = NULL; @@ -84,6 +92,8 @@ static void tdCaseDataLongKeys(void); static void tdCaseDataLooseKeys(void); static void tdCaseDataMouse(void); static void tdCaseDataNavigation(void); +static void tdCaseDataPaged(void); +static void tdCaseDataPagedOom(void); static void tdCaseDataReadOnlyDirty(void); static void tdCaseDataRefresh(void); static void tdCaseDataRefreshOom(void); @@ -106,13 +116,16 @@ static void tdCaseGridScrollH(void); static void tdCaseGridScrollV(void); static void tdCaseGridSelect(void); static void tdCaseGridSort(void); +static void tdCaseGridSortPaged(void); static void tdCaseGridWheel(void); +static void tdCaseGridWidthSample(void); static void tdCaseMasterDetail(void); static void tdCaseNullArgs(void); static void tdCaseReentrantRefresh(void); static void tdCaseSetCurrentRow(void); static void tdCaseTruncation(void); static uint32_t tdContentPixel(const WindowT *win, int32_t x, int32_t y); +static void tdCountChange(WidgetT *w); static int32_t tdCurrentId(WidgetT *data); static bool tdDeleteValidate(WidgetT *w); static void tdDestroyChange(WidgetT *w); @@ -122,6 +135,7 @@ static void tdExec(const char *path, const char *sql); static TdFixtureT tdFixture(int32_t rows); static void tdGetProp(const char *ifaceName, WidgetT *w, const char *propName, char *out, int32_t outSize); static void tdGridLayout(const WidgetT *g, int32_t rowCount, int32_t totalColW, int32_t *innerW, int32_t *innerH, int32_t *visRows, bool *needV, bool *needH); +static int32_t tdHeaderBorderX(const TdFixtureT *t, int32_t nth); static bool tdKillVictimValidate(WidgetT *w); static bool tdNavigateValidate(WidgetT *w); static int32_t tdQueryInt(const char *path, const char *sql); @@ -255,8 +269,9 @@ static void tdCaseDataDelete(void) { char buf[TD_VAL_MAX]; + // On a record: neither BOF nor EOF (VB) tdGetProp("data", t.data, "BOF", buf, sizeof(buf)); - TEST_STR_EQ(buf, "True"); + TEST_STR_EQ(buf, "False"); // Deleting the last row clamps the cursor wgtDataCtrlSetCurrentRow(t.data, 1); @@ -536,16 +551,29 @@ static void tdCaseDataNavigation(void) { wgtDataCtrlMoveNext(t.data); TEST_EQ(tdCurrentId(t.data), 1); + tdGetProp("data", t.data, "BOF", buf, sizeof(buf)); + TEST_STR_EQ(buf, "False"); + wgtDataCtrlMoveNext(t.data); + TEST_EQ(tdCurrentId(t.data), 2); + tdGetProp("data", t.data, "EOF", buf, sizeof(buf)); + TEST_STR_EQ(buf, "False"); + + // Next past the last row only latches EOF (VB: so a Do Until EOF + // loop that ends in MoveNext reads the last row too) wgtDataCtrlMoveNext(t.data); TEST_EQ(tdCurrentId(t.data), 2); tdGetProp("data", t.data, "EOF", buf, sizeof(buf)); TEST_STR_EQ(buf, "True"); - - // Next past the last row only latches EOF - wgtDataCtrlMoveNext(t.data); - TEST_EQ(tdCurrentId(t.data), 2); TEST_EQ(tSpyCount(&spy, "onChange"), 2); + // MovePrevious onto the first row leaves BOF clear + wgtDataCtrlMovePrev(t.data); + wgtDataCtrlMovePrev(t.data); + TEST_EQ(tdCurrentId(t.data), 0); + tdGetProp("data", t.data, "BOF", buf, sizeof(buf)); + TEST_STR_EQ(buf, "False"); + TEST_EQ(tSpyCount(&spy, "onChange"), 4); + // MoveLast / MoveFirst clear both flags (VB semantics) wgtDataCtrlMoveLast(t.data); tdGetProp("data", t.data, "EOF", buf, sizeof(buf)); @@ -554,7 +582,7 @@ static void tdCaseDataNavigation(void) { TEST_EQ(tdCurrentId(t.data), 0); tdGetProp("data", t.data, "BOF", buf, sizeof(buf)); TEST_STR_EQ(buf, "False"); - TEST_EQ(tSpyCount(&spy, "onChange"), 4); + TEST_EQ(tSpyCount(&spy, "onChange"), 6); // Field access edges TEST_STR_EQ(wgtDataCtrlGetField(t.data, "NAME"), "p00"); @@ -574,6 +602,116 @@ static void tdCaseDataNavigation(void) { // Edits on a read-only (SELECT) source are dropped when the cursor moves, // so the old row's snapshot can never be written against a later row. +// A record set bigger than the first rows a Refresh reads: the polls +// read the rest; MoveNext past the rows read, AbsolutePosition beyond +// them, MoveLast, AddNew, and the writes (which the open query's lock +// would refuse) read what they need first. +static void tdCaseDataPaged(void) { + tCase("dataCtrl: a big record set arrives a part at a time"); + + TdFixtureT t = tdFixture(TD_PAGED_ROWS); + char buf[TD_VAL_MAX]; + + TEST_EQ(wgtDataCtrlGetRowCount(t.data), TD_PAGED_ROWS); + + // Refresh reads the first rows; EOF is not true at the last of them + wgtDataCtrlRefresh(t.data); + TEST_EQ(wgtDataCtrlGetRowCount(t.data), TD_FIRST_ROWS); + wgtDataCtrlSetCurrentRow(t.data, TD_FIRST_ROWS - 1); + tdGetProp("data", t.data, "EOF", buf, sizeof(buf)); + TEST_STR_EQ(buf, "False"); + wgtDataCtrlMoveNext(t.data); + TEST_EQ(tdCurrentId(t.data), TD_FIRST_ROWS); + TEST_TRUE(wgtDataCtrlGetRowCount(t.data) > TD_FIRST_ROWS); + wtSettle(); + TEST_EQ(wgtDataCtrlGetRowCount(t.data), TD_PAGED_ROWS); + wgtDataCtrlSetCurrentRow(t.data, TD_PAGED_ROWS - 1); + wgtDataCtrlMoveNext(t.data); + tdGetProp("data", t.data, "EOF", buf, sizeof(buf)); + TEST_STR_EQ(buf, "True"); + + // A Do Until EOF loop reads every row once, while they arrive too + int32_t loops = 0; + + wgtDataCtrlRefresh(t.data); + tdGetProp("data", t.data, "EOF", buf, sizeof(buf)); + + while (strcmp(buf, "False") == 0 && loops <= TD_PAGED_ROWS) { + TEST_EQ(tdCurrentId(t.data), loops); + loops++; + wgtDataCtrlMoveNext(t.data); + tdGetProp("data", t.data, "EOF", buf, sizeof(buf)); + } + + TEST_EQ(loops, TD_PAGED_ROWS); + + // AbsolutePosition beyond the rows read + wgtDataCtrlRefresh(t.data); + wgtDataCtrlSetCurrentRow(t.data, TD_PAGED_ROWS - 10); + TEST_EQ(tdCurrentId(t.data), TD_PAGED_ROWS - 10); + + // MoveLast reads them all + wgtDataCtrlRefresh(t.data); + wgtDataCtrlMoveLast(t.data); + TEST_EQ(wgtDataCtrlGetRowCount(t.data), TD_PAGED_ROWS); + TEST_EQ(tdCurrentId(t.data), TD_PAGED_ROWS - 1); + + // A write while rows are unread is not locked out + wgtDataCtrlRefresh(t.data); + wgtDataCtrlSetField(t.data, "name", "edited"); + wgtDataCtrlUpdate(t.data); + tdQueryText(t.dbPath, "SELECT name FROM people WHERE id = 0", buf, sizeof(buf)); + TEST_STR_EQ(buf, "edited"); + TEST_EQ(wgtDataCtrlGetRowCount(t.data), TD_PAGED_ROWS); + + wgtDataCtrlRefresh(t.data); + wgtDataCtrlDelete(t.data); + TEST_EQ(tdQueryInt(t.dbPath, "SELECT COUNT(*) FROM people"), TD_PAGED_ROWS - 1); + + // AddNew's row goes after every row + wgtDataCtrlRefresh(t.data); + wgtDataCtrlAddNew(t.data); + TEST_EQ(wgtDataCtrlGetCurrentRow(t.data), TD_PAGED_ROWS - 1); + wgtDataCtrlSetField(t.data, "id", "9999"); + wgtDataCtrlSetField(t.data, "name", "new"); + wgtDataCtrlUpdate(t.data); + TEST_EQ(tdQueryInt(t.dbPath, "SELECT COUNT(*) FROM people"), TD_PAGED_ROWS); + + tShutdown(); +} + + +// A copy that runs out of memory while the polls read the rest frees the +// cache whole (as a failed Refresh does), and Reposition clears the bound +// controls; the next Refresh reads it all again. +static void tdCaseDataPagedOom(void) { + tCase("dataCtrl: running out of memory while the rest is read frees the rows"); + + TdFixtureT t = tdFixture(TD_PAGED_ROWS); + char sql[TD_VAL_MAX]; + + snprintf(sql, sizeof(sql), "UPDATE people SET name = '%s' WHERE id = %d", TD_OOM_TEXT, TD_PAGED_OOM_ID); + tdExec(t.dbPath, sql); + wgtDataCtrlRefresh(t.data); + TEST_EQ(wgtDataCtrlGetRowCount(t.data), TD_FIRST_ROWS); + + sChanges = 0; + t.data->onChange = tdCountChange; + sFailStrdup = true; + wtSettle(); + sFailStrdup = false; + t.data->onChange = NULL; + TEST_EQ(wgtDataCtrlGetRowCount(t.data), 0); + TEST_EQ(wgtDataCtrlGetColCount(t.data), 0); + TEST_EQ(sChanges, 1); + + wgtDataCtrlRefresh(t.data); + wtSettle(); + TEST_EQ(wgtDataCtrlGetRowCount(t.data), TD_PAGED_ROWS); + tShutdown(); +} + + static void tdCaseDataReadOnlyDirty(void) { tCase("dataCtrl: read-only edits do not follow the cursor"); @@ -1527,6 +1665,60 @@ static void tdCaseGridSort(void) { } +// A grid sorted while the rows arrive sorts each part in as it comes: the +// order (walked with Down from Home) is the one sorting them all gives. +static void tdCaseGridSortPaged(void) { + tCase("dbGrid: rows that arrive later are sorted in"); + + TdFixtureT t = tdFixture(TD_PAGED_ROWS); + + wgtDbGridSetColumnWidth(t.grid, "id", wgtPixels(50)); + wgtDbGridSetColumnWidth(t.grid, "name", wgtPixels(60)); + wtSettle(); + + int32_t headerY = t.grid->y + TD_BORDER + TD_HEADER_H / 2; + int32_t nameX = t.grid->x + TD_BORDER + 50 + 30; + + // Descending by name, sorted over the first rows only + wgtDataCtrlRefresh(t.data); + wgtDbGridRefresh(t.grid); + wtClickAt(t.fx.win, nameX, headerY); + wtClickAt(t.fx.win, nameX, headerY); + TEST_EQ(wgtDataCtrlGetRowCount(t.data), TD_FIRST_ROWS); + wtSettle(); + TEST_EQ(wgtDataCtrlGetRowCount(t.data), TD_PAGED_ROWS); + + char prev[TD_VAL_MAX]; + int32_t walked = 1; + bool ordered = true; + + tWidgetKey(t.fx.win, KEY_HOME, 0); + snprintf(prev, sizeof(prev), "%s", wgtDataCtrlGetField(t.data, "name")); + TEST_STR_EQ(prev, "p99"); + + for (int32_t i = 1; i < TD_PAGED_ROWS; i++) { + int32_t before = wgtDataCtrlGetCurrentRow(t.data); + + tWidgetKey(t.fx.win, KEY_DOWN, 0); + + if (wgtDataCtrlGetCurrentRow(t.data) == before) { + break; + } + + const char *name = wgtDataCtrlGetField(t.data, "name"); + + ordered = ordered && strcasecmp(prev, name) >= 0; + snprintf(prev, sizeof(prev), "%s", name); + walked++; + } + + TEST_TRUE(ordered); + TEST_EQ(walked, TD_PAGED_ROWS); + TEST_STR_EQ(prev, "p00"); + tShutdown(); +} + + // The wheel scrolls the grid's view by wheelStep rows per notch without // moving the record cursor; the first visible row then is row wheelStep. static void tdCaseGridWheel(void) { @@ -1555,6 +1747,33 @@ static void tdCaseGridWheel(void) { } +// Auto column widths fit the first TD_WIDTH_ROWS rows, including those +// the polls read after the first ones; a wider cell past them leaves the +// width as it is. +static void tdCaseGridWidthSample(void) { + tCase("dbGrid: auto widths measure the first rows, also when they arrive later"); + + TdFixtureT t = tdFixture(TD_PAGED_ROWS); + int32_t narrow = tdHeaderBorderX(&t, 2); + char sql[TD_VAL_MAX]; + + TEST_TRUE(narrow > 0); + + snprintf(sql, sizeof(sql), "UPDATE people SET name = '%s' WHERE id = %d", TD_WIDE_NAME, TD_WIDTH_OUT_ID); + tdExec(t.dbPath, sql); + wgtDataCtrlRefresh(t.data); + wtSettle(); + TEST_EQ(tdHeaderBorderX(&t, 2), narrow); + + snprintf(sql, sizeof(sql), "UPDATE people SET name = '%s' WHERE id = %d", TD_WIDE_NAME, TD_WIDTH_IN_ID); + tdExec(t.dbPath, sql); + wgtDataCtrlRefresh(t.data); + wtSettle(); + TEST_TRUE(tdHeaderBorderX(&t, 2) > narrow); + tShutdown(); +} + + static void tdCaseMasterDetail(void) { tCase("dataCtrl: master-detail filter with escaped master value"); @@ -1720,6 +1939,12 @@ static uint32_t tdContentPixel(const WindowT *win, int32_t x, int32_t y) { // Current row's id as an integer (-1 when there is no current row). +static void tdCountChange(WidgetT *w) { + (void)w; + sChanges++; +} + + static int32_t tdCurrentId(WidgetT *data) { const char *id = wgtDataCtrlGetField(data, "id"); @@ -1837,6 +2062,32 @@ static void tdGridLayout(const WidgetT *g, int32_t rowCount, int32_t totalColW, } +// The screen x of the grid's nth column border (1 = the first column's +// right edge), found with the resize cursor over the header; -1 when not +// on screen. +static int32_t tdHeaderBorderX(const TdFixtureT *t, int32_t nth) { + WindowT *win = t->fx.win; + int32_t left = win->x + win->contentX + t->grid->x + TD_BORDER; + int32_t headerY = win->y + win->contentY + t->grid->y + TD_BORDER + TD_HEADER_H / 2; + int32_t found = 0; + bool over = false; + + for (int32_t x = left; x < left + t->grid->w; x++) { + tMouseMove(x, headerY); + + bool now = tCtx()->cursorId == CURSOR_RESIZE_H; + + if (now && !over && ++found == nth) { + return x; + } + + over = now; + } + + return -1; +} + + static bool tdKillVictimValidate(WidgetT *w) { (void)w; @@ -1939,6 +2190,8 @@ void wtSectionData(void) { tdCaseDataRefreshOom(); tdCaseDataRefreshSaves(); tdCaseDataNavigation(); + tdCaseDataPaged(); + tdCaseDataPagedOom(); tdCaseDataMouse(); tdCaseDataSelectSource(); tdCaseDataUpdate(); @@ -1962,6 +2215,8 @@ void wtSectionData(void) { tdCaseGridColumns(); tdCaseGridSelect(); tdCaseGridSort(); + tdCaseGridSortPaged(); + tdCaseGridWidthSample(); tdCaseGridKeys(); tdCaseGridScrollV(); tdCaseGridScrollH(); diff --git a/src/test/widgets/testHelpView.c b/src/test/widgets/testHelpView.c index 67616fe9..0dc2dd66 100644 --- a/src/test/widgets/testHelpView.c +++ b/src/test/widgets/testHelpView.c @@ -311,10 +311,22 @@ static void caseHelpViewShow(void) { // heading, 2 paragraphs, 3 pictures, heading, 2 links, heading, rule TEST_EQ(childCount(contentBox(view)), HV_WELCOME_CHILDREN); + // Its pictures are kept decoded with the file + void *pics = hlpFileCache(file); + + TEST_TRUE(pics != NULL); + // The blocks topic (lists, code, table, notes) builds too wgtHelpViewShowTopic(view, file, hlpFileFindTopic(file, "fx.blocks")); wtSettle(); TEST_TRUE(childCount(contentBox(view)) > 0); + + // Shown again, the welcome topic takes its pictures from the kept + // ones + wgtHelpViewShowTopic(view, file, welcome); + wtSettle(); + TEST_EQ(childCount(contentBox(view)), HV_WELCOME_CHILDREN); + TEST_TRUE(hlpFileCache(file) == pics); } hlpFileClose(file); diff --git a/src/test/widgets/testInput.c b/src/test/widgets/testInput.c index 3074768e..7958bccb 100644 --- a/src/test/widgets/testInput.c +++ b/src/test/widgets/testInput.c @@ -36,6 +36,7 @@ #define TEXT_MAX_LEN 16 #define AREA_MAX_LEN 256 +#define AREA_SMALL_LEN 10 // caseTextArea: a TextArea three of its replacements fill #define RADIO_COUNT 3 @@ -206,6 +207,16 @@ static void caseLabel(void) { wtSettle(); TEST_TRUE(lbl->calcMinW > narrowW); + // The caption it already has asks for no relayout or repaint (a loop + // setting it every pass); a new one does. + f.win->paintNeeded = PAINT_NONE; + wgtSetText(lbl, "&Name of the thing:"); + TEST_EQ(f.win->paintNeeded, PAINT_NONE); + wgtSetText(lbl, "&Name of it:"); + TEST_EQ(f.win->paintNeeded, PAINT_FULL); + wgtSetText(lbl, "&Name of the thing:"); + wtSettle(); + wgtLabelSetAlign(lbl, AlignEndE); TEST_TRUE(wtPropToString(lbl, wgtIfaceFindProp(iface, "Alignment"), out, sizeof(out))); TEST_STR_EQ(out, "Right"); @@ -372,6 +383,16 @@ static void caseTextArea(void) { tWidgetKey(f.win, KEY_CTRL_V, KEY_MOD_CTRL); TEST_TRUE(strstr(wgtGetText(ta), "bX\nc") != NULL); + // Longer and shorter text, any case, matches side by side, one undo + // for the lot + wgtSetText(ta, "abababX"); + TEST_EQ(wgtTextAreaReplaceAll(ta, "AB", "xyz", false), 3); + TEST_STR_EQ(wgtGetText(ta), "xyzxyzxyzX"); + TEST_EQ(wgtTextAreaReplaceAll(ta, "xyz", "", true), 3); + TEST_STR_EQ(wgtGetText(ta), "X"); + tWidgetKey(f.win, KEY_CTRL_Z, KEY_MOD_CTRL); + TEST_STR_EQ(wgtGetText(ta), "xyzxyzxyzX"); + // Search and replace through the API. wgtSetText(ta, "one two\nthree two"); TEST_TRUE(wgtTextAreaFindNext(ta, "two", true, true)); @@ -428,6 +449,29 @@ static void caseTextArea(void) { wgtTextAreaSetCursor(ta, 0, 0); typeKeys(f.win, "<"); TEST_STR_EQ(wgtGetText(ta), "type)); + + // The value it already shows asks for no repaint; another does. + wtSettle(); + f.win->paintNeeded = PAINT_NONE; + wgtProgressBarSetValue(pv, PROGRESS_MAX + 1); + TEST_EQ(f.win->paintNeeded, PAINT_NONE); + wgtProgressBarSetValue(pv, 1); + TEST_EQ(f.win->paintNeeded, PAINT_PARTIAL); wtSettle(); tShutdown(); } diff --git a/src/test/wm/wmExcl.c b/src/test/wm/wmExcl.c index 61d1019c..5aa091ed 100644 --- a/src/test/wm/wmExcl.c +++ b/src/test/wm/wmExcl.c @@ -25,9 +25,10 @@ // Taking and giving back the screen, suspending when the owner loses the // focus and resuming when it gets it back, other windows painting on // while the desktop is not drawn, input going to the owner, the 8-bit -// picture scaled through the palette, screenshots of the full screen, -// and the failure paths (busy, bad arguments, no mode, no mode again on -// resuming, the owner closing). +// picture scaled through the palette, a window's own content mirrored +// (dvxExclusiveMirror), screenshots of the full screen, and the failure +// paths (busy, bad arguments, no mode, no mode again on resuming, the +// owner closing). #include "dvxtest.h" #include "dvxPlat.h" @@ -68,6 +69,9 @@ #define SHOT_FILE "wmExcl.png" #define SCAN_X 0x2D #define ASCII_X 'x' +#define MIRROR_FULL 255 // a full colour channel +#define MIRROR_MOUSE_X 20 // where in the mirrored content the mouse goes +#define MIRROR_MOUSE_Y 10 static int32_t sActiveCount = 0; static int32_t sSuspendedCount = 0; @@ -82,6 +86,7 @@ static int32_t sPaintCount = 0; static void caseBeginEnd(void); static void caseErrors(void); static void caseInput(void); +static void caseMirror(void); static void caseOthersRunOn(void); static void casePresentScaled(void); static void caseResumeFails(void); @@ -228,6 +233,82 @@ static void caseInput(void) { } +// A mirror: the owner's content in the desktop's format, centered on +// black, shown again as the window invalidates, the mouse in content +// coordinates with its cursor drawn. +static void caseMirror(void) { + tCase("dvxExclusiveMirror shows the window's own content"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + countsReset(); + + AppContextT *ctx = tCtx(); + WindowT *a = ownerCreate(); + + tPump(SETTLE_FRAMES); + + uint32_t red = packColor(&ctx->display, MIRROR_FULL, 0, 0); + uint32_t green = packColor(&ctx->display, 0, MIRROR_FULL, 0); + uint32_t blue = packColor(&ctx->display, 0, 0, MIRROR_FULL); + + for (int32_t y = 0; y < a->contentH; y++) { + uint32_t *row = (uint32_t *)(a->contentBuf + (size_t)y * (size_t)a->contentPitch); + + for (int32_t x = 0; x < a->contentW; x++) { + row[x] = red; + } + } + + ((uint32_t *)a->contentBuf)[0] = green; + + TEST_EQ(dvxExclusiveMirror(ctx, NULL, onChange, NULL), DVX_EXCL_ERR_ARGS); + TEST_EQ(dvxExclusiveMirror(ctx, a, onChange, NULL), DVX_EXCL_OK); + TEST_EQ(sActiveCount, 1); + + DisplayT *d = dvxExclusiveDisplay(ctx); + + TEST_TRUE(d != NULL); + TEST_EQ(d->format.bitsPerPixel, SCREEN_BPP); + TEST_TRUE(d->width >= a->contentW && d->height >= a->contentH); + + RectT place = ctx->exclusive.presentRect; + const uint32_t *fb = (const uint32_t *)hostFramebuffer(d); + int32_t words = d->pitch / (int32_t)sizeof(uint32_t); + + TEST_EQ(place.x, (d->width - place.w) / 2); + TEST_EQ(fb[place.y * words + place.x], green); + TEST_EQ(fb[(place.y + place.h - 1) * words + place.x + place.w - 1], red); + + if (place.x > 0) { + TEST_EQ(fb[place.y * words + place.x - 1], 0); + } + + // The mouse in content coordinates, its cursor drawn over the content + tMouseMove(place.x + MIRROR_MOUSE_X, place.y + MIRROR_MOUSE_Y); + tPump(1); + TEST_EQ(sMouseX, MIRROR_MOUSE_X / (place.w / a->contentW)); + TEST_EQ(sMouseY, MIRROR_MOUSE_Y / (place.h / a->contentH)); + TEST_TRUE(fb[(place.y + MIRROR_MOUSE_Y) * words + place.x + MIRROR_MOUSE_X] != red); + + // What the window paints is shown again + for (int32_t y = 0; y < a->contentH; y++) { + uint32_t *row = (uint32_t *)(a->contentBuf + (size_t)y * (size_t)a->contentPitch); + + for (int32_t x = 0; x < a->contentW; x++) { + row[x] = blue; + } + } + + dvxInvalidateRect(ctx, a, 0, 0, a->contentW, a->contentH); + tPump(1); + TEST_EQ(fb[(place.y + place.h - 1) * words + place.x + place.w - 1], blue); + TEST_EQ(fb[place.y * words + place.x], blue); + + dvxExclusiveEnd(ctx, a); + TEST_TRUE(dvxExclusiveDisplay(ctx) == NULL); + tShutdown(); +} + + // Other windows paint on into their own buffers; the desktop's screen is // not drawn until the owner lets go, then all of it is. static void caseOthersRunOn(void) { @@ -472,6 +553,7 @@ int main(void) { caseBeginEnd(); caseErrors(); caseInput(); + caseMirror(); caseOthersRunOn(); casePresentScaled(); caseResumeFails(); diff --git a/src/test/wm/wmFixes.c b/src/test/wm/wmFixes.c index ead64e45..5739132d 100644 --- a/src/test/wm/wmFixes.c +++ b/src/test/wm/wmFixes.c @@ -32,6 +32,7 @@ #include "dvxPlat.h" #include "dvxPrefs.h" #include "dvxRes.h" +#include "dvxText.h" #include "dvxWgtP.h" #include "box/box.h" #include "button/button.h" @@ -58,11 +59,28 @@ #define WIN_H 150 #define SETTLE_FRAMES 3 +#define BUDGET_STEP_US 15000 // caseCompositeBudget: each clock read 15 ms (three bands fit 40 ms) +#define BUDGET_FRAMES_MAX 50 // frames a banded full repaint may take at most #define IDLE_FRAMES 5 #define TEXT_MAX_LEN 32 #define TALL_BUTTON_COUNT 15 #define TIP_MAX 16 +// caseFontSmoothFrames: a family (added from DVX's own fonts) whose +// smoothed sizes are looked for in this range, and the frames the fonts +// open are given to be built again +#define SMOOTH_FAMILY "DejaVu Sans" +#define SMOOTH_FILE DVXTEST_FONT_FILES "/kpunch/DJVSANS.DFN" +#define SMOOTH_SIZE_MIN 8 +#define SMOOTH_SIZE_MAX 32 +#define SMOOTH_FRAMES_MAX 64 + +// caseImageDecodePumps: a picture big enough for a few of the decode's +// pumps (a frame each 32 KB read) +#define IMG_BIG_W 160 +#define IMG_BIG_H 160 +#define IMG_BIG_PUMPS 2 + // Private to dvxApp.c; mirrored so the test can cross the thresholds. #define TOOLTIP_DELAY_MS 500 #define POPUP_BEVEL 2 @@ -156,11 +174,15 @@ static void caseAccelModifiedKeys(void); static void caseAccelText(void); static void caseAltTabMinimizedTop(void); static void caseCallbackAppId(void); +static void caseCompositeBudget(void); static void caseContentDisplayNull(void); static void caseDeferredDestroyIconDirty(void); static void caseDisabledChainNoFocus(void); static void caseFocusSkipsMinimized(void); +static void caseFontSmoothFrames(void); +static void caseImageDecodePumps(void); static void caseInputEntropy(void); +static void caseMaximizeFill(void); static void caseMenuKeyboardModal(void); static void caseMouseDownDestroyNoFocusCallbacks(void); static void casePrefsInsertAndSave(void); @@ -171,11 +193,14 @@ static void caseSubmenuHoverIdle(void); static void caseSysMenuClamped(void); static void caseTooltipScrolled(void); static void caseWallpaperFailureKeepsOld(void); +static void caseWallpaperKept(void); static void caseWheelHook(void); +static uint32_t contentPixel(const WindowT *win, int32_t x, int32_t y); static void countPaint(WindowT *win, RectT *dirty); static void entropySink(const uint8_t *data, int32_t len); static void idleCounter(void *userCtx); static void killWindowOnDown(WidgetT *w, int32_t button, int32_t x, int32_t y); +static bool noInputHere(void *ctx); static bool readFile(const char *path, uint8_t *buf, int32_t size, int32_t *outLen); static void recBlur(WindowT *win); static void recFocus(WindowT *win); @@ -491,6 +516,51 @@ static void caseCallbackAppId(void) { } +// A frame composites for 40 ms at most (here each clock read takes 15 ms: +// a few bands), the rest of a full-screen repaint waiting for the next +// frames; the screen it ends with is the one a composite at once draws. +static void caseCompositeBudget(void) { + tCase("composite: a frame's time budget, the rest at the next frames"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + AppContextT *ctx = tCtx(); + WindowT *win = dvxCreateWindow(ctx, "Budget", WIN_A_X, WIN_A_Y, WIN_W, WIN_H, true); + int32_t bytes = ctx->display.pitch * ctx->display.height; + uint8_t *whole = (uint8_t *)malloc((size_t)bytes); + + wgtInitWindow(ctx, win); + tPump(SETTLE_FRAMES); + TEST_TRUE(whole != NULL); + + if (!whole) { + tShutdown(); + return; + } + + // The screen composited at once + dvxSetColor(ctx, ColorDesktopE, 1, 2, 3); + tPump(SETTLE_FRAMES); + memcpy(whole, ctx->display.backBuf, (size_t)bytes); + + // The same screen again, a few bands a frame + dvxSetColor(ctx, ColorDesktopE, 1, 2, 4); + dvxSetColor(ctx, ColorDesktopE, 1, 2, 3); + hostClockSetMicrosStep(BUDGET_STEP_US); + tPump(1); + TEST_TRUE(ctx->dirty.count > 0); + + for (int32_t i = 0; i < BUDGET_FRAMES_MAX && ctx->dirty.count > 0; i++) { + tPump(1); + } + + hostClockSetMicrosStep(0); + TEST_EQ(ctx->dirty.count, 0); + TEST_TRUE(memcmp(whole, ctx->display.backBuf, (size_t)bytes) == 0); + free(whole); + tShutdown(); +} + + // dvxContentDisplay honours the NULL-window contract: an empty display // that drawing clips away. static void caseContentDisplayNull(void) { @@ -609,6 +679,111 @@ static void caseFocusSkipsMinimized(void) { } +// Font smoothing switched (the Control Panel): the fonts open are built +// again in the frames after, a font a frame, and every window is painted +// again once they all are. +static void caseFontSmoothFrames(void) { + tCase("font smoothing: the open fonts built again in the frames after, then a repaint"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + AppContextT *ctx = tCtx(); + BitmapFontT *f = NULL; + + TEST_TRUE(dvxFontAddFile(0, SMOOTH_FILE)); + dvxSetFontSmoothing(ctx, true); + tPump(SMOOTH_FRAMES_MAX); + TEST_TRUE(dvxFontSmooth()); + + for (int32_t size = SMOOTH_SIZE_MIN; !f && size <= SMOOTH_SIZE_MAX; size++) { + BitmapFontT *t = dvxFontOpen(SMOOTH_FAMILY, size, 0); + + if (t && (t->flags & FONT_FLAG_GRAY)) { + f = t; + } else { + dvxFontClose(t); + } + } + + TEST_TRUE(f != NULL); + + if (!f) { + dvxFontRemoveOwner(0); + tShutdown(); + return; + } + + WindowT *win = dvxCreateWindow(ctx, "Smooth", WIN_A_X, WIN_A_Y, WIN_W, WIN_H, false); + + win->onPaint = countPaint; + tPump(SETTLE_FRAMES); + + // Switched: nothing is built at once + dvxSetFontSmoothing(ctx, false); + TEST_TRUE(!dvxFontSmooth()); + TEST_TRUE(f->flags & FONT_FLAG_GRAY); + + int32_t frames = 0; + + while ((f->flags & FONT_FLAG_GRAY) && frames < SMOOTH_FRAMES_MAX) { + tPump(1); + frames++; + } + + TEST_TRUE(frames >= 1); + TEST_TRUE(!(f->flags & FONT_FLAG_GRAY)); + + // Painted again once every font is done + sPaintCount = 0; + tPump(SMOOTH_FRAMES_MAX); + TEST_TRUE(sPaintCount > 0); + TEST_TRUE(!dvxFontRebuildStep()); + dvxFontClose(f); + dvxFontRemoveOwner(0); + tShutdown(); +} + + +// An image decode pumps a frame now and then for a caller on a task that +// takes no input (a program on its own task), never on the input task +// (where a frame could run the caller's own handlers). +static void caseImageDecodePumps(void) { + tCase("image decodes pump on a task without the input, not on the input task"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + AppContextT *ctx = tCtx(); + static uint8_t pixels[IMG_BIG_W * IMG_BIG_H * sizeof(uint32_t)]; + char path[DVX_MAX_PATH]; + int32_t w = 0; + int32_t h = 0; + int32_t pitch = 0; + + memset(pixels, WHITE, sizeof(pixels)); + snprintf(path, sizeof(path), "%s/big.bmp", tTempDir()); + TEST_EQ(dvxSaveImage(ctx, pixels, IMG_BIG_W, IMG_BIG_H, IMG_BIG_W * (int32_t)sizeof(uint32_t), path), 0); + + bool (*saved)(void *) = ctx->inputHere; + uint32_t frames = ctx->frameCount; + + ctx->inputHere = noInputHere; + + uint8_t *img = dvxLoadImage(ctx, path, &w, &h, &pitch); + + TEST_TRUE(img != NULL); + TEST_EQ(w, IMG_BIG_W); + TEST_TRUE(ctx->frameCount - frames >= IMG_BIG_PUMPS); + dvxFreeImage(img); + + ctx->inputHere = NULL; + frames = ctx->frameCount; + img = dvxLoadImage(ctx, path, &w, &h, &pitch); + TEST_TRUE(img != NULL); + TEST_EQ(ctx->frameCount, frames); + dvxFreeImage(img); + ctx->inputHere = saved; + tShutdown(); +} + + static void caseInputEntropy(void) { tCase("input entropy hook: keys and mouse changes are fed in full batches, idle frames are not"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -657,6 +832,37 @@ static void caseInputEntropy(void) { } +// A maximized window's new content buffer: a widget window's full paint +// fills every pixel (the buffer is not cleared before it); a window whose +// own paint draws nothing shows the clear colour (all ones: white). +static void caseMaximizeFill(void) { + tCase("maximize: a widget window painted whole, a raw-paint one cleared"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + AppContextT *ctx = tCtx(); + WindowT *wwin = dvxCreateWindow(ctx, "Widgets", WIN_A_X, WIN_A_Y, WIN_W, WIN_H, true); + WindowT *rwin = dvxCreateWindow(ctx, "Raw", WIN_A_X, WIN_A_Y, WIN_W, WIN_H, true); + + wgtInitWindow(ctx, wwin); + rwin->onPaint = countPaint; + tPump(SETTLE_FRAMES); + dvxMaximizeWindow(ctx, wwin); + dvxMaximizeWindow(ctx, rwin); + tPump(SETTLE_FRAMES); + + int32_t lastX = wwin->contentW - 1; + int32_t lastY = wwin->contentH - 1; + + TEST_EQ(contentPixel(wwin, 0, 0), ctx->colors.contentBg); + TEST_EQ(contentPixel(wwin, lastX, 0), ctx->colors.contentBg); + TEST_EQ(contentPixel(wwin, 0, lastY), ctx->colors.contentBg); + TEST_EQ(contentPixel(wwin, lastX, lastY), ctx->colors.contentBg); + TEST_EQ(contentPixel(rwin, 0, 0), UINT32_MAX); + TEST_EQ(contentPixel(rwin, rwin->contentW - 1, rwin->contentH - 1), UINT32_MAX); + tShutdown(); +} + + static void caseMenuKeyboardModal(void) { tCase("keys that match nothing in an open menu do not reach the window"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -1072,6 +1278,33 @@ static void caseWallpaperFailureKeepsOld(void) { } +// The wallpaper's picture is kept while it is the wallpaper: a wallpaper +// mode change builds from it (its file gone meanwhile); cleared, it goes. +static void caseWallpaperKept(void) { + tCase("wallpaper: its picture kept for a mode change, gone with it"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + AppContextT *ctx = tCtx(); + uint8_t pixels[SHOT_W * SHOT_H * sizeof(uint32_t)]; + char path[DVX_MAX_PATH]; + + memset(pixels, WHITE, sizeof(pixels)); + snprintf(path, sizeof(path), "%s/kept.png", tTempDir()); + TEST_EQ(dvxSaveImage(ctx, pixels, SHOT_W, SHOT_H, SHOT_W * (int32_t)sizeof(uint32_t), path), 0); + TEST_TRUE(dvxSetWallpaper(ctx, path)); + TEST_EQ(remove(path), 0); + + uint8_t *before = ctx->wallpaperBuf; + + dvxSetWallpaperMode(ctx, WallpaperTileE); + TEST_TRUE(ctx->wallpaperBuf != NULL && ctx->wallpaperBuf != before); + TEST_STR_EQ(ctx->wallpaperPath, path); + TEST_TRUE(dvxSetWallpaper(ctx, NULL)); + TEST_TRUE(!dvxSetWallpaper(ctx, path)); + tShutdown(); +} + + static void caseWheelHook(void) { tCase("wheel: an ON_WHEEL handler sees notches first and can consume them"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -1125,6 +1358,15 @@ static void caseWheelHook(void) { } +// The pixel at x, y of win's content buffer (SCREEN_BPP: four bytes). +static uint32_t contentPixel(const WindowT *win, int32_t x, int32_t y) { + uint32_t pixel = 0; + + memcpy(&pixel, win->contentBuf + y * win->contentPitch + x * (int32_t)sizeof(pixel), sizeof(pixel)); + return pixel; +} + + static void countPaint(WindowT *win, RectT *dirty) { (void)win; (void)dirty; @@ -1163,6 +1405,13 @@ static void killWindowOnDown(WidgetT *w, int32_t button, int32_t x, int32_t y) { } +// caseImageDecodePumps' input hook: this task takes no input. +static bool noInputHere(void *ctx) { + (void)ctx; + return false; +} + + static bool readFile(const char *path, uint8_t *buf, int32_t size, int32_t *outLen) { FILE *fp = fopen(path, "rb"); @@ -1228,11 +1477,15 @@ int main(void) { caseAccelText(); caseAltTabMinimizedTop(); caseCallbackAppId(); + caseCompositeBudget(); caseContentDisplayNull(); caseDeferredDestroyIconDirty(); caseDisabledChainNoFocus(); caseFocusSkipsMinimized(); + caseFontSmoothFrames(); + caseImageDecodePumps(); caseInputEntropy(); + caseMaximizeFill(); caseMenuKeyboardModal(); caseMouseDownDestroyNoFocusCallbacks(); casePrefsInsertAndSave(); @@ -1243,6 +1496,7 @@ int main(void) { caseSysMenuClamped(); caseTooltipScrolled(); caseWallpaperFailureKeepsOld(); + caseWallpaperKept(); caseWheelHook(); return tSuiteEnd(); diff --git a/src/test/wm/wmSuite.c b/src/test/wm/wmSuite.c index 2342d5cb..ccb817ba 100644 --- a/src/test/wm/wmSuite.c +++ b/src/test/wm/wmSuite.c @@ -1452,9 +1452,13 @@ static void sev80InvalidateSkipsPending(void) { TEST_EQ(tSpyCount(&spy, "Gone.onPaint"), 0); TEST_EQ(tSpyCount(&spy, "Stay.onPaint"), 1); - // A colour change invalidates every window -- still not the dying one. + // A colour change paints every window at the next frame -- once, + // however many come first, and still not the dying one. tSpyClear(&spy); dvxSetColor(ctx, ColorWindowFaceE, 1, 2, 3); + dvxSetColor(ctx, ColorWindowFaceE, 4, 5, 6); + TEST_EQ(tSpyCount(&spy, "Stay.onPaint"), 0); + tPump(1); TEST_EQ(tSpyCount(&spy, "Gone.onPaint"), 0); TEST_EQ(tSpyCount(&spy, "Stay.onPaint"), 1); diff --git a/src/test/wm/wmSuite2.c b/src/test/wm/wmSuite2.c index 79d86f0b..3a7e6c1c 100644 --- a/src/test/wm/wmSuite2.c +++ b/src/test/wm/wmSuite2.c @@ -69,6 +69,9 @@ #define TOP_DOWN_BMP_W 2 // the hand-made top-down BMP in caseImageApis #define TOP_DOWN_BMP_H 3 #define SETTLE_FRAMES 3 +#define SCRIBBLE_BYTE 0xA5 // casePartialRepaintArea's mark in the content buffer +#define PAINTED_GAP_MIN 8 // the mark lies this far below the repainted button at least +#define SPACER_WEIGHT 100 // a spacer taking the room between two buttons #define ICON_PUMP_FRAMES 60 #define TINY_DISPLAY_W 40 #define TINY_DISPLAY_H 100 @@ -142,8 +145,14 @@ // Case filter for debugging: WM2_CASE= runs only that case. #define RUN_CASE(fn) do { if (caseEnabled(#fn)) { fn(); } } while (0) -// stb_image_write lives inside libdvx.a (whole-archive link). +// A copy of stb_image_write of this file's own (libdvx keeps its copy +// static). +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#define STB_IMAGE_WRITE_STATIC +#define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image_write_wrap.h" +#pragma GCC diagnostic pop typedef struct { @@ -181,6 +190,7 @@ static void caseIconsAndThumbnails(void); static void caseImageApis(void); static void caseKeyboardMoveResize(void); static void caseMenuSubAndShortcuts(void); +static void casePartialRepaintArea(void); static void caseSaveLoadTheme(void); static void caseScreenshots(void); static void caseScrollIntoView(void); @@ -776,6 +786,22 @@ static void caseImageApis(void) { TEST_TRUE(!dvxSetWallpaper(tCtx(), empty)); } + // A truncated JPEG from a file fails at once: its APP0 length skips + // past the end, and the file's skip used to leave end-of-file unseen, + // so stb's marker scan looped for ever (the fuzzImage reproducer). + { + static const uint8_t truncJpeg[] = { 0xFF, 0xD8, 0xFF, 0xE0, 0x10 }; + char trunc[DVX_MAX_PATH]; + int32_t outW = 0; + int32_t outH = 0; + int32_t outPitch = 0; + + snprintf(trunc, sizeof(trunc), "%s/trunc.jpg", tTempDir()); + TEST_TRUE(tWriteFile(trunc, truncJpeg, (int32_t)sizeof(truncJpeg))); + TEST_PTR_EQ(dvxLoadImage(tCtx(), trunc, &outW, &outH, &outPitch), NULL); + TEST_PTR_EQ(dvxLoadImageFromMemory(tCtx(), truncJpeg, (int32_t)sizeof(truncJpeg), &outW, &outH, &outPitch), NULL); + } + // A top-down BMP (height stored negative, here 2 x -3 at 24 bits, rows // padded to 8 bytes) has the size it loads at: stb_image's info call // passes the negative height through. @@ -804,6 +830,24 @@ static void caseImageApis(void) { TEST_EQ(outW, TOP_DOWN_BMP_W); TEST_EQ(outH, TOP_DOWN_BMP_H); dvxFreeImage(pixels); + + // A program file is no picture: its icon is read from the file + // already open, the toolbox icon when it has no program icon, and + // the program icon first (here no picture, so nothing loads). + static const char moduleBytes[] = "MODULEBYTES"; + static const char notPicture[] = "x"; + char module[DVX_MAX_PATH]; + + snprintf(module, sizeof(module), "%s/iconmod.app", tTempDir()); + TEST_TRUE(tWriteFile(module, moduleBytes, (int32_t)sizeof(moduleBytes))); + TEST_EQ(dvxResAppend(module, DVX_RES_NAME_WIDGET_ICON, DVX_RES_ICON, topDownBmp, sizeof(topDownBmp)), 0); + pixels = dvxLoadImage(tCtx(), module, &outW, &outH, &outPitch); + TEST_TRUE(pixels != NULL); + TEST_EQ(outW, TOP_DOWN_BMP_W); + TEST_EQ(outH, TOP_DOWN_BMP_H); + dvxFreeImage(pixels); + TEST_EQ(dvxResAppend(module, DVX_RES_NAME_ICON, DVX_RES_ICON, notPicture, sizeof(notPicture)), 0); + TEST_PTR_EQ(dvxLoadImage(tCtx(), module, &outW, &outH, &outPitch), NULL); } // dvxLoadImageAlpha keys out transparent pixels at every depth. @@ -1201,6 +1245,55 @@ static void caseMenuSubAndShortcuts(void) { } +// A partial repaint composites only the widgets it repainted: a mark left +// in the content buffer over another widget stays off the screen, while +// the repainted widget's change reaches it. +static void casePartialRepaintArea(void) { + tCase("a partial repaint composites only what it repainted"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + AppContextT *ctx = tCtx(); + WindowT *win = dvxCreateWindow(ctx, "Narrow", WIN_A_X, WIN_A_Y, WIN_W, WIN_H, false); + WidgetT *root = wgtInitWindow(ctx, win); + WidgetT *box = wgtVBox(root); + WidgetT *top = wgtButton(box, "Top"); + WidgetT *bottom = NULL; + + wgtSpacer(box)->weight = SPACER_WEIGHT; + bottom = wgtButton(box, "Bottom"); + wgtInvalidate(root); + tPump(SETTLE_FRAMES); + + int32_t bpp = ctx->display.format.bytesPerPixel; + int32_t pitch = ctx->display.pitch; + const uint8_t *fb = hostFramebuffer(&ctx->display); + int32_t bx = bottom->x + bottom->w / 2; + int32_t by = bottom->y + bottom->h / 2; + size_t bAt = (size_t)(win->y + win->contentY + by) * (size_t)pitch + (size_t)(win->x + win->contentX + bx) * (size_t)bpp; + size_t tAt = (size_t)(win->y + win->contentY + top->y) * (size_t)pitch + (size_t)(win->x + win->contentX + top->x) * (size_t)bpp; + uint8_t bBefore[sizeof(uint32_t)]; + uint8_t tBefore[sizeof(uint32_t)]; + + TEST_TRUE(by > top->y + top->h + PAINTED_GAP_MIN); + memcpy(bBefore, fb + bAt, (size_t)bpp); + memcpy(tBefore, fb + tAt, (size_t)bpp); + memset(win->contentBuf + (size_t)by * (size_t)win->contentPitch + (size_t)bx * (size_t)bpp, SCRIBBLE_BYTE, (size_t)bpp); + top->pressed = true; + wgtInvalidatePaint(top); + tPump(1); + TEST_TRUE(memcmp(fb + bAt, bBefore, (size_t)bpp) == 0); + TEST_TRUE(memcmp(fb + tAt, tBefore, (size_t)bpp) != 0); + + // A full repaint composites the whole window again (the mark painted over) + top->pressed = false; + dvxInvalidateWindow(ctx, win); + tPump(1); + TEST_TRUE(memcmp(fb + bAt, bBefore, (size_t)bpp) == 0); + TEST_TRUE(memcmp(fb + tAt, tBefore, (size_t)bpp) == 0); + tShutdown(); +} + + static void caseSaveLoadTheme(void) { tCase("theme save/load round-trip, malformed lines, reset"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -2187,6 +2280,7 @@ int main(void) { RUN_CASE(caseDvxInitFail); RUN_CASE(caseDvxRunQuit); RUN_CASE(caseHotkeys); + RUN_CASE(casePartialRepaintArea); RUN_CASE(caseFitAndResizeClamps); RUN_CASE(caseFlushPendingPaths); RUN_CASE(caseIconsAndThumbnails); diff --git a/src/tools/README.md b/src/tools/README.md index 51758d95..8414d5f4 100644 --- a/src/tools/README.md +++ b/src/tools/README.md @@ -240,9 +240,11 @@ dvxhlpc -o out.hlp @inputs.txt * Target: `bin/system/DVXHLPC.EXE` (DOS executable; released with the Help Editor, part of Developer Tools: `src/packages.ini`) -The help compiler is also linked into the loader as an object file so -help files can be rebuilt on first boot if the module set has -changed (in lenient-link mode, see above). +The help compiler is also built as a module of its own, +`bin/system/HLPC.DXE` (`-DHLPC_NO_MAIN`, `hlpcModule.h`), which the +loader opens to rebuild help files when the module set has changed (in +lenient-link mode, see above) and BASIC's HelpCompile opens for a +compile; neither keeps it loaded. ## Icon Generators @@ -414,7 +416,7 @@ the program is built). The SDK ships it in `bin/`. ## afmWidths.py -- PostScript Font Widths -Makes `src/libs/kpunch/print/printPsAfm.c`, the widths of the twelve +Makes `src/libs/kpunch/print/drivers/ps/printPsAfm.c`, the widths of the twelve text faces every PostScript printer has (Helvetica, Times, and Courier, each upright, bold, slanted, and both), from Adobe's Core 14 AFM files. The printing library measures text on vector pages with them, so it @@ -422,7 +424,7 @@ lines up as the printer prints it. Not part of the build: run it again only to change what the table keeps. ``` -python3 src/tools/afmWidths.py AFMDIR src/libs/kpunch/print/printPsAfm.c +python3 src/tools/afmWidths.py AFMDIR src/libs/kpunch/print/drivers/ps/printPsAfm.c ``` AFMDIR holds the AFM files and Adobe's `MustRead.html` (on Ubuntu, diff --git a/src/tools/afmWidths.py b/src/tools/afmWidths.py index e2e61368..afbbd0e5 100644 --- a/src/tools/afmWidths.py +++ b/src/tools/afmWidths.py @@ -20,7 +20,7 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -# afmWidths.py -- make src/libs/kpunch/print/printPsAfm.c from Adobe's +# afmWidths.py -- make src/libs/kpunch/print/drivers/ps/printPsAfm.c from Adobe's # Core 14 AFM files: the character widths, in ISOLatin1Encoding order, of # the twelve text faces every PostScript printer has (Helvetica, Times, # Courier, each upright, bold, oblique or italic, and bold oblique or @@ -103,6 +103,21 @@ def wrap(text, lead): return lines +# The MIT notice this program opens with (its leading comment, to the +# blank line), as C comments: the code it writes is under it, the widths +# under Adobe's notices below it +def ownNotice(): + notice = [] + + for line in open(__file__).read().split('\n'): + if not line.startswith('#'): + break + + notice.append('//' + line[1:]) + + return notice + + def main(): afmDir, outPath = sys.argv[1:3] @@ -115,7 +130,8 @@ def main(): if notice not in notices: notices.append(notice) - out = [] + out = ownNotice() + out.append('') out.append('// printPsAfm.c -- Printing library: the widths of the PostScript') out.append('// printers\' own text faces (printInt.h), made by src/tools/afmWidths.py') out.append('// -- do not edit.') diff --git a/src/tools/bmp2raw.c b/src/tools/bmp2raw.c index 08c6df6e..ef0c9b4f 100644 --- a/src/tools/bmp2raw.c +++ b/src/tools/bmp2raw.c @@ -28,6 +28,7 @@ #include "bmpDraw.h" #include "dvxPlat.h" +#include "dvxEndian.h" #include #include @@ -35,19 +36,8 @@ #include #include -static uint16_t readLe16(const uint8_t *p); -static uint32_t readLe32(const uint8_t *p); -static uint16_t readLe16(const uint8_t *p) { - return (uint16_t)(p[0] | (p[1] << 8)); -} - - -// Assembled in uint32_t: shifting a byte into bit 31 of an int is undefined. -static uint32_t readLe32(const uint8_t *p) { - return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); -} int main(int argc, char **argv) { @@ -78,11 +68,11 @@ int main(int argc, char **argv) { return 1; } - uint32_t pixelOffset = readLe32(&hdr[BMP_OFF_PIXEL_OFFSET]); - int32_t width = (int32_t)readLe32(&hdr[BMP_OFF_WIDTH]); - int32_t height = (int32_t)readLe32(&hdr[BMP_OFF_HEIGHT]); - uint16_t bpp = readLe16(&hdr[BMP_OFF_BPP]); - uint32_t compression = readLe32(&hdr[BMP_OFF_COMPRESSION]); + uint32_t pixelOffset = dvxGetLe32(&hdr[BMP_OFF_PIXEL_OFFSET]); + int32_t width = (int32_t)dvxGetLe32(&hdr[BMP_OFF_WIDTH]); + int32_t height = (int32_t)dvxGetLe32(&hdr[BMP_OFF_HEIGHT]); + uint16_t bpp = dvxGetLe16(&hdr[BMP_OFF_BPP]); + uint32_t compression = dvxGetLe32(&hdr[BMP_OFF_COMPRESSION]); if (width != SPLASH_W || abs(height) != SPLASH_H || bpp != SPLASH_BPP) { fprintf(stderr, "BMP must be %dx%d %dbpp (got %dx%d %dbpp)\n", SPLASH_W, SPLASH_H, SPLASH_BPP, width, abs(height), bpp); @@ -101,13 +91,13 @@ int main(int argc, char **argv) { // The palette follows the file header and the DIB header, whose size // varies by BMP version. - uint32_t dibSize = readLe32(&hdr[BMP_OFF_DIB_SIZE]); + uint32_t dibSize = dvxGetLe32(&hdr[BMP_OFF_DIB_SIZE]); uint32_t palOffset = BMP_FILE_HEADER_SIZE + dibSize; // biClrUsed: an optimized 8bpp BMP can have fewer than 256 entries, so // a fixed 256-entry read would pull pixel bytes into the palette tail. // 0 means "all" (256). - uint32_t clrUsed = readLe32(&hdr[BMP_OFF_CLR_USED]); + uint32_t clrUsed = dvxGetLe32(&hdr[BMP_OFF_CLR_USED]); if (clrUsed == 0 || clrUsed > SPLASH_PAL_ENTRIES) { clrUsed = SPLASH_PAL_ENTRIES; diff --git a/src/tools/bmpDraw.c b/src/tools/bmpDraw.c index 2b410f9e..99dfaa95 100644 --- a/src/tools/bmpDraw.c +++ b/src/tools/bmpDraw.c @@ -23,6 +23,7 @@ // bmpDraw.c -- Shared 24-bit BMP canvas + drawing primitives for host tools #include "bmpDraw.h" +#include "dvxEndian.h" #include #include @@ -34,8 +35,6 @@ #define BMP_BITS_PER_PIXEL 24 static void bmpIconTypes(const BmpIconT *icons); -static void bmpPut16(uint8_t *dst, int32_t value); -static void bmpPut32(uint8_t *dst, int32_t value); void bmpClear(BmpCanvasT *c, uint8_t r, uint8_t g, uint8_t b) { @@ -177,20 +176,6 @@ void bmpPixel(BmpCanvasT *c, int32_t x, int32_t y, uint8_t r, uint8_t g, uint8_t } -static void bmpPut16(uint8_t *dst, int32_t value) { - dst[0] = (uint8_t)(value & 0xFF); - dst[1] = (uint8_t)((value >> 8) & 0xFF); -} - - -static void bmpPut32(uint8_t *dst, int32_t value) { - dst[0] = (uint8_t)(value & 0xFF); - dst[1] = (uint8_t)((value >> 8) & 0xFF); - dst[2] = (uint8_t)((value >> 16) & 0xFF); - dst[3] = (uint8_t)((value >> 24) & 0xFF); -} - - void bmpRect(BmpCanvasT *c, int32_t x0, int32_t y0, int32_t w, int32_t h, uint8_t r, uint8_t g, uint8_t b) { bmpHLine(c, x0, y0, w, r, g, b); bmpHLine(c, x0, y0 + h - 1, w, r, g, b); @@ -218,14 +203,14 @@ bool bmpWrite(const BmpCanvasT *c, const char *path) { header[0] = 'B'; header[1] = 'M'; - bmpPut32(&header[BMP_OFF_FILE_SIZE], fileSize); - bmpPut32(&header[BMP_OFF_PIXEL_OFFSET], BMP_HEADER_SIZE); - bmpPut32(&header[BMP_OFF_DIB_SIZE], BMP_DIB_SIZE); - bmpPut32(&header[BMP_OFF_WIDTH], c->w); - bmpPut32(&header[BMP_OFF_HEIGHT], c->h); - bmpPut16(&header[BMP_OFF_PLANES], BMP_PLANES); - bmpPut16(&header[BMP_OFF_BPP], BMP_BITS_PER_PIXEL); - bmpPut32(&header[BMP_OFF_IMAGE_SIZE], dataSize); + dvxPutLe32(&header[BMP_OFF_FILE_SIZE], (uint32_t)fileSize); + dvxPutLe32(&header[BMP_OFF_PIXEL_OFFSET], BMP_HEADER_SIZE); + dvxPutLe32(&header[BMP_OFF_DIB_SIZE], BMP_DIB_SIZE); + dvxPutLe32(&header[BMP_OFF_WIDTH], (uint32_t)c->w); + dvxPutLe32(&header[BMP_OFF_HEIGHT], (uint32_t)c->h); + dvxPutLe16(&header[BMP_OFF_PLANES], BMP_PLANES); + dvxPutLe16(&header[BMP_OFF_BPP], BMP_BITS_PER_PIXEL); + dvxPutLe32(&header[BMP_OFF_IMAGE_SIZE], (uint32_t)dataSize); FILE *f = fopen(path, "wb"); diff --git a/src/tools/dvxResWrite.c b/src/tools/dvxResWrite.c index f16d0b41..351b8956 100644 --- a/src/tools/dvxResWrite.c +++ b/src/tools/dvxResWrite.c @@ -47,79 +47,102 @@ static int32_t dvxResReadLayout(FILE *f, DvxResLayoutT *layout); // Returns 0 on success. int32_t dvxResAppendEntry(const char *path, const char *name, uint32_t type, const void *resData, uint32_t resSize) { - if (!path || !name || !resData || resSize == 0) { + DvxResBatchT b; + + if (!path || !name || !resData || resSize == 0 || dvxResBatchBegin(&b, path) < 0) { return -1; } - DvxResLayoutT layout; - uint8_t **data = NULL; - - // A read error has already freed any partial allocations; bail rather - // than truncate the file to only the new entry. - if (dvxResLoad(path, &layout, &data) < 0) { + if (dvxResBatchAdd(&b, name, type, resData, resSize) < 0) { + dvxResBatchFree(&b); return -1; } - DvxResDirEntryT *entries = layout.entries; - uint32_t count = layout.count; + return dvxResBatchCommit(&b); +} - // Remove existing entry with same name - for (uint32_t i = 0; i < count; i++) { - if (strcmp(entries[i].name, name) == 0) { - free(data[i]); - for (uint32_t j = i; j < count - 1; j++) { - entries[j] = entries[j + 1]; - data[j] = data[j + 1]; +// A same-named entry is replaced (its old data freed, the rest moved up); +// tmp pointers keep a failed realloc from stranding the blocks. +int32_t dvxResBatchAdd(DvxResBatchT *b, const char *name, uint32_t type, const void *resData, uint32_t resSize) { + for (uint32_t i = 0; i < b->count; i++) { + if (strcmp(b->entries[i].name, name) == 0) { + free(b->data[i]); + + for (uint32_t j = i; j < b->count - 1; j++) { + b->entries[j] = b->entries[j + 1]; + b->data[j] = b->data[j + 1]; } - count--; + b->count--; break; } } - // Add new entry. Use tmp pointers so a realloc failure doesn't - // strand the existing blocks as unreachable leaks. Abort the - // write on OOM -- nothing useful to recover to here. - int32_t result = -1; - DvxResDirEntryT *newEntries = (DvxResDirEntryT *)realloc(entries, (count + 1) * sizeof(DvxResDirEntryT)); + DvxResDirEntryT *newEntries = (DvxResDirEntryT *)realloc(b->entries, (b->count + 1) * sizeof(DvxResDirEntryT)); - if (!newEntries) { - goto done; + if (newEntries) { + b->entries = newEntries; } - entries = newEntries; + uint8_t **newData = newEntries ? (uint8_t **)realloc(b->data, (b->count + 1) * sizeof(uint8_t *)) : NULL; - uint8_t **newData = (uint8_t **)realloc(data, (count + 1) * sizeof(uint8_t *)); - - if (!newData) { - goto done; + if (newData) { + b->data = newData; } - data = newData; + uint8_t *copy = newData ? (uint8_t *)malloc(resSize) : NULL; - memset(&entries[count], 0, sizeof(DvxResDirEntryT)); - strncpy(entries[count].name, name, DVX_RES_NAME_MAX - 1); - entries[count].type = type; - entries[count].size = resSize; - data[count] = (uint8_t *)malloc(resSize); - - if (!data[count]) { - goto done; + if (!copy) { + b->failed = true; + return -1; } - memcpy(data[count], resData, resSize); - count++; + memcpy(copy, resData, resSize); + memset(&b->entries[b->count], 0, sizeof(DvxResDirEntryT)); + strncpy(b->entries[b->count].name, name, DVX_RES_NAME_MAX - 1); + b->entries[b->count].type = type; + b->entries[b->count].size = resSize; + b->data[b->count] = copy; + b->count++; + return 0; +} - result = dvxResWriteBlock(path, layout.dxeSize, entries, count, data); -done: - dvxResFreeBlobs(data, count); - free(entries); +// A read error has already freed any partial allocations: the batch is +// not started (a commit would cut the file to the new entries alone). +int32_t dvxResBatchBegin(DvxResBatchT *b, const char *path) { + DvxResLayoutT layout; + + memset(b, 0, sizeof(*b)); + + if (dvxResLoad(path, &layout, &b->data) < 0) { + return -1; + } + + b->path = path; + b->dxeSize = layout.dxeSize; + b->count = layout.count; + b->entries = layout.entries; + return 0; +} + + +int32_t dvxResBatchCommit(DvxResBatchT *b) { + int32_t result = b->failed ? -1 : dvxResWriteBlock(b->path, b->dxeSize, b->entries, b->count, b->data); + + dvxResBatchFree(b); return result; } +void dvxResBatchFree(DvxResBatchT *b) { + dvxResFreeBlobs(b->data, b->count); + free(b->entries); + memset(b, 0, sizeof(*b)); +} + + // Free the first count data blobs and the array holding them. void dvxResFreeBlobs(uint8_t **data, uint32_t count) { if (!data) { diff --git a/src/tools/dvxResWrite.h b/src/tools/dvxResWrite.h index 1937deff..db839473 100644 --- a/src/tools/dvxResWrite.h +++ b/src/tools/dvxResWrite.h @@ -48,10 +48,38 @@ typedef struct { DvxResDirEntryT *entries; } DvxResLayoutT; +// Resources gathered in memory and written to the file once, instead of a +// rewrite of the whole block per resource (a build adding dozens): what +// the file had (dvxResBatchBegin), then each dvxResBatchAdd, a same-named +// one replacing the one before. +typedef struct { + const char *path; + long dxeSize; + uint32_t count; + DvxResDirEntryT *entries; + uint8_t **data; + bool failed; // an add ran out of memory: the commit fails +} DvxResBatchT; + // Append one resource to a DXE file (replacing a same-named one) and -// rewrite the resource block. Returns 0 on success. +// rewrite the resource block (a batch of one). Returns 0 on success. int32_t dvxResAppendEntry(const char *path, const char *name, uint32_t type, const void *resData, uint32_t resSize); +// Add a copy of a resource to the batch. Returns 0, or -1 (out of memory: +// the batch then fails to commit). +int32_t dvxResBatchAdd(DvxResBatchT *b, const char *name, uint32_t type, const void *resData, uint32_t resSize); + +// Start a batch with what path already holds. Returns 0, or -1 when the +// file cannot be read (nothing to free then). +int32_t dvxResBatchBegin(DvxResBatchT *b, const char *path); + +// Write the batch to its file (dvxResWriteBlock) and free it. Returns 0, +// or -1 when an add failed or the write did (the file is then unchanged). +int32_t dvxResBatchCommit(DvxResBatchT *b); + +// Free a batch without writing it. +void dvxResBatchFree(DvxResBatchT *b); + // Free the per-entry data blobs returned by dvxResLoad. void dvxResFreeBlobs(uint8_t **data, uint32_t count); diff --git a/src/tools/dvxhlpc.c b/src/tools/dvxhlpc.c index c6718113..2bdd30d9 100644 --- a/src/tools/dvxhlpc.c +++ b/src/tools/dvxhlpc.c @@ -44,6 +44,7 @@ #include "hlpcCompile.h" #include "../libs/kpunch/hlpfile/hlpFormat.h" #include "../libs/kpunch/libdvx/platform/dvxPlat.h" +#include "../libs/kpunch/libdvx/dvxHash.h" #include #include @@ -89,10 +90,6 @@ // slot in a direct index of HLP_TRIGRAM_ALPHABET^3 entries. #define TRIGRAM_SLOTS (HLP_TRIGRAM_ALPHABET * HLP_TRIGRAM_ALPHABET * HLP_TRIGRAM_ALPHABET) -// 32-bit FNV-1a for the string table hash -#define FNV_OFFSET_BASIS 2166136261u -#define FNV_PRIME 16777619u - // --------------------------------------------------------------------------- // Compiler data structures // --------------------------------------------------------------------------- @@ -261,13 +258,21 @@ static bool quietMode = false; static bool lenientLinks = false; // HLPC_LENIENT_LINKS for this compile static ScratchT sScratch; -static jmp_buf sOomJmp; // fatalOom's way back into hlpcCompile -static bool sOomArmed = false; // sOomJmp is valid (a compile is running) +static jmp_buf sAbortJmp; // fatalOom's and tick's way back into hlpcCompile +static bool sAbortArmed = false; // sAbortJmp is valid (a compile is running) + +// sAbortJmp's values: why the compile stopped +#define ABORT_OOM 1 +#define ABORT_STOPPED 2 // Where messages go (hlpcSetMessageFn; NULL: stderr) static HlpcMessageFnT sMessageFn = NULL; static void *sMessageCtx = NULL; +// The tick (hlpcSetTickFn; NULL: none) +static HlpcTickFnT sTickFn = NULL; +static void *sTickCtx = NULL; + // Progress callback (set by hlpcCompile, NULL for standalone) static HlpcProgressFnT sProgressFn = NULL; static void *sProgressCtx = NULL; @@ -322,6 +327,7 @@ static void flushPending(ParseStateT *ps); static void freeAll(void); static void hlpcInfo(const char *fmt, ...); void hlpcSetMessageFn(HlpcMessageFnT fn, void *ctx); +void hlpcSetTickFn(HlpcTickFnT fn, void *ctx); static void htmlEscapeWrite(FILE *f, const char *text); static void htmlWriteImage(FILE *f, const RecordT *rec); static void htmlWritePreformatted(FILE *f, const char *text); @@ -344,6 +350,7 @@ static void resetAll(void); static void strHashGrow(void); static uint32_t strHashString(const char *str); static int32_t strTableAdd(const char *str); +static void tick(void); #ifndef HLPC_NO_MAIN static void usage(void); #endif @@ -573,6 +580,8 @@ static void buildSearchIndex(void) { for (int32_t t = 0; t < topicCount; t++) { TopicT *topic = &topics[t]; + tick(); + // Collect all searchable text for this topic textBuf->size = 0; bufAppend(textBuf, topic->title, (int32_t)strlen(topic->title)); @@ -1050,8 +1059,8 @@ static void emitWarning(const char *fmt, ...) { static void fatalOom(void) { diag("error: out of memory\n"); - if (sOomArmed) { - longjmp(sOomJmp, 1); + if (sAbortArmed) { + longjmp(sAbortJmp, ABORT_OOM); } exit(1); @@ -1197,15 +1206,17 @@ int32_t hlpcCompile(const char **inputFiles, int32_t inputCount, const char *out hlpcInfo("dvxhlpc: DVX Help Compiler\n"); - if (setjmp(sOomJmp) == 0) { - sOomArmed = true; - result = compilePasses(inputFiles, inputCount, outputPath); + int jumped = setjmp(sAbortJmp); + + if (jumped == 0) { + sAbortArmed = true; + result = compilePasses(inputFiles, inputCount, outputPath); } else { releaseScratch(); - diag("Aborting: out of memory.\n"); + diag(jumped == ABORT_STOPPED ? "Stopped.\n" : "Aborting: out of memory.\n"); } - sOomArmed = false; + sAbortArmed = false; // Every compile reports exactly hlpcProgressTotal() steps, even when it // stops early, so callers that budget by that total stay in step. @@ -1237,6 +1248,12 @@ void hlpcSetMessageFn(HlpcMessageFnT fn, void *ctx) { } +void hlpcSetTickFn(HlpcTickFnT fn, void *ctx) { + sTickFn = fn; + sTickCtx = ctx; +} + + // Writes text with HTML special characters escaped. NULL (an empty // record such as a bare .h1) writes nothing. static void htmlEscapeWrite(FILE *f, const char *text) { @@ -1740,6 +1757,10 @@ static void parseFile(const char *path, ParseStateT *ps, int32_t includeDepth) { while (readLine(f, line, (int32_t)sizeof(line), &len, &hasNul)) { currentLine++; + if (currentLine % HLPC_TICK_LINES == 0) { + tick(); + } + // A line longer than the buffer comes back in pieces; parsing the // tail as its own line could turn text into a directive. if (len > 0 && line[len - 1] != '\n' && !feof(f)) { @@ -1937,6 +1958,8 @@ static int32_t pass4Serialize(const char *outputPath) { for (int32_t t = 0; t < topicCount; t++) { TopicT *topic = &topics[t]; + + tick(); topicContentOffsets[t] = w.offset; for (int32_t r = 0; r < topic->recordCount; r++) { @@ -2351,12 +2374,12 @@ static void strHashGrow(void) { } +// The string table's hash: FNV-1a of the string. static uint32_t strHashString(const char *str) { - uint32_t h = FNV_OFFSET_BASIS; + uint32_t h = DVX_FNV_BASIS; for (const uint8_t *p = (const uint8_t *)str; *p; p++) { - h ^= *p; - h *= FNV_PRIME; + h = dvxFnvByte(h, *p); } return h; @@ -2399,6 +2422,15 @@ static int32_t strTableAdd(const char *str) { } +// The caller's tick (hlpcSetTickFn); a false answer abandons the compile +// as running out of memory does (hlpcCompile cleans up). +static void tick(void) { + if (sTickFn && !sTickFn(sTickCtx) && sAbortArmed) { + longjmp(sAbortJmp, ABORT_STOPPED); + } +} + + #ifndef HLPC_NO_MAIN static void usage(void) { fprintf(stderr, "Usage: dvxhlpc -o output.hlp [-i imagedir] [--html out.html] [--quiet] input.dhs [@filelist] [...]\n"); diff --git a/src/tools/dxeGc.sh b/src/tools/dxeGc.sh new file mode 100755 index 00000000..ca5e68d7 --- /dev/null +++ b/src/tools/dxeGc.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# 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. + +# dxeGc.sh -- pre-link a module's objects, keeping only what its own code +# reaches. +# +# Usage: dxeGc.sh TOOLPREFIX OUT OWN.o... -- ALL.o... +# +# A DXE exports every global, so third-party code linked into a module +# keeps every function, called or not. With the objects compiled with +# -ffunction-sections, this links ALL into one relocatable +# OUT (TOOLPREFIX-ld -r --gc-sections) rooted at every global the +# module's OWN objects define: those stay exported, and the third-party +# functions they do not reach are dropped. The third-party globals that +# are kept are made local (the references to them are already resolved +# inside OUT), so the module exports its own functions only. The +# symbols left behind by the dropped code are stripped (objcopy +# --strip-unneeded), so dxe3gen reads OUT cleanly. + +set -e + +prefix=$1 +out=$2 +shift 2 + +own=() + +while [ $# -gt 0 ] && [ "$1" != "--" ]; do + own+=("$1") + shift +done + +shift + +ownSyms="$out.own" +foreign="$out.foreign" +roots=() + +"$prefix-nm" -g --defined-only "${own[@]}" | awk 'NF == 3 { print $3 }' | sort -u > "$ownSyms" + +while read -r sym; do + roots+=(-u "$sym") +done < "$ownSyms" + +"$prefix-ld" -r -x --gc-sections "${roots[@]}" -o "$out" "$@" +"$prefix-nm" -g --defined-only "$out" | awk 'NF == 3 { print $3 }' | sort -u | comm -23 - "$ownSyms" > "$foreign" +"$prefix-objcopy" --localize-symbols="$foreign" --strip-unneeded "$out" +rm -f "$ownSyms" "$foreign" diff --git a/src/tools/hlpcCompile.h b/src/tools/hlpcCompile.h index d090736c..3b425990 100644 --- a/src/tools/hlpcCompile.h +++ b/src/tools/hlpcCompile.h @@ -73,6 +73,18 @@ typedef void (*HlpcMessageFnT)(void *ctx, const char *line); void hlpcSetMessageFn(HlpcMessageFnT fn, void *ctx); +// Called often during a compile (every HLPC_TICK_LINES source lines and +// every topic of the later passes), for a caller that lets other work go +// on; false stops the compile (it says "Stopped." and returns non-zero). +// The callback must not start another compile: the compiler does one at +// a time. NULL (the default) calls nothing; the setting stays until +// changed. +typedef bool (*HlpcTickFnT)(void *ctx); + +#define HLPC_TICK_LINES 64 + +void hlpcSetTickFn(HlpcTickFnT fn, void *ctx); + // Return the number of progress steps for a given input count: // inputCount (one per file in pass 1) + 3 (passes 2-4). // Useful for pre-computing the total across multiple compilations. diff --git a/src/tools/hlpcModule.h b/src/tools/hlpcModule.h new file mode 100644 index 00000000..311d6526 --- /dev/null +++ b/src/tools/hlpcModule.h @@ -0,0 +1,86 @@ +// 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. + +// hlpcModule.h -- the help compiler as a module loaded while help compiles +// +// dvx.exe does not carry the help compiler: SYSTEM\HLPC.DXE (built from +// dvxhlpc.c) does, opened for a compile and closed after it -- the boot +// rebuild of the programs' help and BASIC's HelpCompile. A host build +// links the compiler in, so its own symbols are found first. + +#ifndef HLPC_MODULE_H +#define HLPC_MODULE_H + +#include "hlpcCompile.h" +#include "../libs/kpunch/libdvx/platform/dvxPlat.h" + +#include +#include +#include + +#define HLPC_MODULE DVX_SYSTEM_DIR DVX_PATH_SEP "HLPC.DXE" + +// The compiler's entry points, from the module (handle) or the program +typedef struct { + void *handle; // NULL: the program's own + __typeof__(hlpcCompile) *compile; + __typeof__(hlpcSetMessageFn) *setMessage; + __typeof__(hlpcSetTickFn) *setTick; +} HlpcModuleT; + + +// Close what hlpcModuleOpen opened; the entry points are gone after. +static inline void hlpcModuleClose(HlpcModuleT *m) { + if (m->handle) { + dlclose(m->handle); + } + + memset(m, 0, sizeof(*m)); +} + + +// Find the compiler: in the program, else in HLPC_MODULE. False (m +// cleared) when neither has all of it. +static inline bool hlpcModuleOpen(HlpcModuleT *m) { + memset(m, 0, sizeof(*m)); + + if (!dlsym(NULL, DVX_SYM("hlpcCompile"))) { + m->handle = platformModuleOpen(HLPC_MODULE, RTLD_NOW); + + if (!m->handle) { + return false; + } + } + + m->compile = (__typeof__(hlpcCompile) *)dlsym(m->handle, DVX_SYM("hlpcCompile")); + m->setMessage = (__typeof__(hlpcSetMessageFn) *)dlsym(m->handle, DVX_SYM("hlpcSetMessageFn")); + m->setTick = (__typeof__(hlpcSetTickFn) *)dlsym(m->handle, DVX_SYM("hlpcSetTickFn")); + + if (!m->compile || !m->setMessage || !m->setTick) { + hlpcModuleClose(m); + return false; + } + + return true; +} + +#endif // HLPC_MODULE_H diff --git a/src/tools/mkfont.c b/src/tools/mkfont.c index f9b0eb06..c677d54a 100644 --- a/src/tools/mkfont.c +++ b/src/tools/mkfont.c @@ -78,6 +78,7 @@ // sizes, and its print sizes together. // Any error stops the run with a message naming the font or file. +#include "dvxEndian.h" #include "dvxFontFmt.h" #include "dvxPlat.h" #include "dvxPrefs.h" @@ -569,7 +570,7 @@ static char *faceNotice(const char *path, FT_Face face) { // UTF-16, big-endian for (FT_UInt k = 0; k + 1 < sn.string_len; k += 2) { - appendAscii(&text, ((uint32_t)sn.string[k] << 8) | sn.string[k + 1], path, 1); + appendAscii(&text, dvxGetBe16(sn.string + k), path, 1); } } diff --git a/src/tools/proxy/Makefile b/src/tools/proxy/Makefile index 7423d5fc..e9a20aa0 100644 --- a/src/tools/proxy/Makefile +++ b/src/tools/proxy/Makefile @@ -21,21 +21,22 @@ # IN THE SOFTWARE. # SecLink Proxy -- Linux build -# Compiles the packet, security, and secLink layers against a socket -# shim (sockShim.c implements the rs232 entry points they call) instead of +# Compiles the packet, security (with Monocypher), and secLink layers +# against a socket shim (sockShim.c implements the rs232 entry points they call) instead of # the DJGPP rs232 driver. include ../../common.mk -CFLAGS = $(HOST_TOOL_CFLAGS) +KPUNCH = ../../libs/kpunch +MONO = $(KPUNCH)/crypto/thirdparty/monocypher/src +CFLAGS = $(HOST_TOOL_CFLAGS) -I$(MONO) -SERIAL = ../../libs/kpunch/serial OBJDIR = ../../../obj/proxy BINDIR = ../../../bin/host TARGET = $(BINDIR)/secproxy -SRCS = sockShim.c proxy.c $(SERIAL)/packet/packet.c $(SERIAL)/security/secHash.c $(SERIAL)/security/security.c $(SERIAL)/seclink/secLink.c +SRCS = sockShim.c proxy.c $(KPUNCH)/seclink/packet.c $(KPUNCH)/crypto/secCurve.c $(KPUNCH)/crypto/secHash.c $(KPUNCH)/crypto/security.c $(MONO)/monocypher.c $(KPUNCH)/seclink/secLink.c OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(SRCS))) .PHONY: all clean @@ -47,7 +48,7 @@ $(TARGET): $(OBJS) | $(BINDIR) $(HOSTCC) -o $@ $(OBJS) # One object rule per source: the local shim and proxy plus the shared -# serial library sources, compiled unmodified except that sockShim.h is +# seclink and crypto library sources, compiled unmodified except that sockShim.h is # force-included (its wall-clock clock(), see there). define PROXY_OBJ_RULE $(OBJDIR)/$(basename $(notdir $1)).o: $1 $$(DVX_MAKEFILES) | $$(OBJDIR) diff --git a/src/tools/proxy/README.md b/src/tools/proxy/README.md index 3caa3c39..317f61a7 100644 --- a/src/tools/proxy/README.md +++ b/src/tools/proxy/README.md @@ -4,9 +4,9 @@ Linux-hosted proxy that bridges an emulated DOS serial port to a remote telnet BBS. Part of the DVX GUI project. The DOS side communicates using the SecLink protocol (HDLC packet -framing, CRC-16, authenticated DH key exchange, XTEA-CTR encryption -with per-frame HMAC-SHA256). The BBS side is plain telnet over TCP. All crypto is transparent to the BBS -- it -sees a normal telnet client. +framing, CRC-16, authenticated X25519 key exchange, XChaCha20-Poly1305 +encryption of every frame). The BBS side is plain telnet over TCP. All +crypto is transparent to the BBS -- it sees a normal telnet client. ## Architecture @@ -32,11 +32,11 @@ port on the Network tab. The proxy cannot tell the two apart and needs no option for it. The proxy accepts a single TCP connection from the DOS side, performs -the SecLink handshake (Diffie-Hellman key exchange, optionally -authenticated by a shared passphrase), then connects to the BBS. All -traffic between the DOS side and the proxy is encrypted and MACed on -channel 0; clear frames from the DOS side are not relayed. Traffic between the proxy and the BBS is -unencrypted telnet with IAC negotiation handling. +the SecLink handshake (X25519 key exchange, optionally authenticated +by a shared passphrase), then connects to the BBS. All traffic between +the DOS side and the proxy is encrypted and authenticated on channel 0; +clear frames from the DOS side are not relayed. Traffic between the +proxy and the BBS is unencrypted telnet with IAC negotiation handling. ## Usage diff --git a/src/tools/proxy/proxy.c b/src/tools/proxy/proxy.c index 2d038ede..98130b45 100644 --- a/src/tools/proxy/proxy.c +++ b/src/tools/proxy/proxy.c @@ -64,8 +64,8 @@ #include #include "sockShim.h" -#include "../../libs/kpunch/serial/seclink/secLink.h" -#include "../../libs/kpunch/serial/security/security.h" +#include "../../libs/kpunch/seclink/secLink.h" +#include "../../libs/kpunch/crypto/security.h" // ======================================================================== @@ -564,10 +564,17 @@ static void telnetSendSb(int bbsFd, const uint8_t *data, int len) { // Initial key exchange. The DOS user may start the terminal long after // the emulator connected, so this waits as long as it takes, sleeping in -// poll() between steps (no busy loop) and giving up on Ctrl+C. +// poll() (no busy loop) and giving up on Ctrl+C. Nothing of ours goes out +// until the terminal speaks (its HELLO): a frame of ours left unanswered +// for PKT_LINK_TIMEOUT_MS would end the link before the terminal starts. static int waitForHandshake(SecLinkT *link, int fd) { - struct pollfd pfd = {fd, POLLIN, 0}; - int rc = SECLINK_PENDING; + struct pollfd pfd = {fd, POLLIN, 0}; + int rc = SECLINK_PENDING; + int ready = 0; + + while (sRunning && ready == 0) { + ready = poll(&pfd, 1, POLL_TIMEOUT_MS); + } while (sRunning) { rc = secLinkHandshakeStep(link); diff --git a/src/tools/proxy/sockShim.c b/src/tools/proxy/sockShim.c index 08e4da8d..6d0d7b0e 100644 --- a/src/tools/proxy/sockShim.c +++ b/src/tools/proxy/sockShim.c @@ -25,7 +25,7 @@ // Maps up to 4 "COM ports" to TCP socket file descriptors. Reads use // MSG_DONTWAIT (non-blocking) to match rs232Read's non-blocking semantics. // Writes are blocking (loop until all bytes sent): a frame is never cut -// short by a full TCP buffer. (The real rs232Write may return a partial +// short by a full TCP buffer. (The real rs232WriteBuf may return a partial // count under flow control; packet.c then carries on with the rest.) // // The shim does NOT close sockets in rs232Close -- socket lifecycle is @@ -64,6 +64,13 @@ int32_t rs232Close(int32_t com) { } +// Nothing waits in a buffer of the shim's: rs232WriteBuf sends it all. +int32_t rs232GetTxBuffered(int32_t com) { + (void)com; + return 0; +} + + // Serial parameters are ignored -- TCP handles framing, flow control, and // error correction. We just validate that a socket has been assigned. int32_t rs232Open(int32_t com, int32_t bps, int32_t dataBits, char parity, int32_t stopBits, int32_t handshake) { @@ -111,8 +118,8 @@ int32_t rs232Read(int32_t com, char *data, int32_t len) { // Blocking write: loops until all bytes are sent. MSG_NOSIGNAL prevents // SIGPIPE on a closed connection (returns EPIPE instead). Returns the -// byte count written, like the real rs232Write. -int32_t rs232Write(int32_t com, const char *data, int32_t len) { +// byte count written, like the real rs232WriteBuf. +int32_t rs232WriteBuf(int32_t com, const char *data, int32_t len) { if (com < 0 || com >= RS232_NUM_PORTS || sFds[com] < 0) { return RS232_ERR_NOT_OPEN; } @@ -162,7 +169,7 @@ void sockShimSetFd(int32_t com, int32_t fd) { // Keep the socket BLOCKING. rs232Read already does a per-call // non-blocking recv (MSG_DONTWAIT), so it needs no socket-wide flag; - // a non-blocking socket would instead make rs232Write's send() return + // a non-blocking socket would instead make rs232WriteBuf's send() return // EAGAIN when the TCP buffer fills. Blocking writes keep every HDLC // frame whole on the wire. int flags = fcntl(fd, F_GETFL, 0); diff --git a/src/tools/proxy/sockShim.h b/src/tools/proxy/sockShim.h index 195437e2..0e2f3742 100644 --- a/src/tools/proxy/sockShim.h +++ b/src/tools/proxy/sockShim.h @@ -23,12 +23,13 @@ // Socket shim -- provides rs232-compatible API backed by TCP sockets // Used by the Linux proxy to reuse the packet and secLink layers. // -// Design: the packet and secLink layers call rs232Read/rs232Write assuming -// a real UART underneath. sockShim.c implements those entry points (as -// declared by the real rs232.h, so constants and signatures cannot drift) -// over TCP sockets, and the protocol stack links against it unmodified. -// Only the subset packet and secLink use is provided (open, close, read, -// write); hardware functions (IRQ, FIFO, flow control) are not needed. +// Design: the packet and secLink layers call rs232Read/rs232WriteBuf +// assuming a real UART underneath. sockShim.c implements those entry +// points (as declared by the real rs232.h, so constants and signatures +// cannot drift) over TCP sockets, and the protocol stack links against it +// unmodified. Only the subset packet and secLink use is provided (open, +// close, read, buffered write and its pending count); hardware functions +// (IRQ, FIFO, flow control) are not needed. #ifndef SOCKSHIM_H #define SOCKSHIM_H diff --git a/src/widgets/kpunch/Makefile b/src/widgets/kpunch/Makefile index c3a4d11f..ddf27837 100644 --- a/src/widgets/kpunch/Makefile +++ b/src/widgets/kpunch/Makefile @@ -60,7 +60,7 @@ WIDGETS = \ splitter:splitter:widgetSplitter:splitter \ statbar:statusBar:widgetStatusBar:statbar \ tabctrl:tabControl:widgetTabControl:tabctrl \ - terminal:ansiTerm:widgetAnsiTerm:terminal \ + ansiterm:ansiTerm:widgetAnsiTerm:terminal \ textinpt:textInput:widgetTextInput:textinpt \ timer:timer:widgetTimer:timer \ datactrl:dataCtrl:widgetDataCtrl:datactr \ diff --git a/src/widgets/kpunch/ansiTerm/ansiTerm.dhs b/src/widgets/kpunch/ansiTerm/ansiTerm.dhs index 48c83b47..e492145d 100644 --- a/src/widgets/kpunch/ansiTerm/ansiTerm.dhs +++ b/src/widgets/kpunch/ansiTerm/ansiTerm.dhs @@ -39,6 +39,8 @@ The widget renders through two paint paths: a full paint used by the normal widg Header: widgets/ansiTerm.h +Loaded as: bin/widgets/kpunch/ansiterm/ansiterm.wgt, part of Communications. Without it there is no Terminal control and dvxAnsiTermApi() is NULL, so a program that may run without Communications checks that before it makes a terminal. + .h3 Creation .code @@ -58,7 +60,7 @@ WidgetT *term = wgtAnsiTerm(parent, 80, 25); Function Description -------- ----------- WidgetT *wgtAnsiTerm(parent, cols, rows) Create an ANSI terminal widget with the given column/row dimensions. - void wgtAnsiTermWrite(w, data, len) Write raw bytes into the terminal's ANSI parser. data is a const uint8_t * buffer. + void wgtAnsiTermWrite(w, data, len) Write raw bytes into the terminal's ANSI parser. data is a const uint8_t * buffer. Up to 4 KB is parsed at once; the rest of a longer text waits in order and is parsed 4 KB per poll (a frame each), so a long text does not hold DVX up. Clear, a resize, and a new scrollback length parse what waits first. void wgtAnsiTermClear(w) Clear the terminal screen and reset the cursor to the home position. void wgtAnsiTermSetComm(w, ctx, readFn, writeFn) Attach a communication channel. readFn and writeFn are I/O callbacks; ctx is passed as their first argument. void wgtAnsiTermSetScrollback(w, maxLines) Set the maximum number of scrollback lines. @@ -119,11 +121,11 @@ Features added after the original table live in a second table so AnsiTermApiT k ------ ----------- Clear Clear the terminal screen. Copy Copy the selection (copySelection); returns False when nothing is selected. - Paste Send the clipboard to the comm write callback as Ctrl+V does (new lines as CR, 64-byte chunks). + Paste Send the clipboard to the comm write callback as Ctrl+V does (new lines as CR, 64-byte chunks); with no write callback, to the Send event in one piece. Poll Process any pending bytes on the attached communication channel. ResetColors The standard CGA colors again (setPalette NULL). SetColor index, color: one attribute color (0-15) from a BASIC color (&HBBGGRR, converted with wgtBasicColor). - Write Write a string into the terminal (with ANSI escape processing). + Write Write a string into the terminal (with ANSI escape processing). Dynamic (WGT_SIG_DYNAMIC) so every byte of the BASIC string arrives, NUL bytes included (wgtValueLen); the parser ignores a NUL as padding. .endtable .h3 Events @@ -131,14 +133,16 @@ Features added after the original table live in a second table so AnsiTermApiT k .index ANSI Music .index ANSITERM_EVT_MUSIC .index ANSITERM_EVT_BELL +.index ANSITERM_EVT_SEND -Besides the common events, two of its own, fired through wgtFireEvent (a C host gets them in onEvent with the names ANSITERM_EVT_MUSIC and ANSITERM_EVT_BELL): +Besides the common events, three of its own, fired through wgtFireEvent (a C host gets them in onEvent with the names ANSITERM_EVT_MUSIC, ANSITERM_EVT_BELL, and ANSITERM_EVT_SEND): .table Event Parameters Description ----- ---------- ----------- Music Macro As String An ANSI music string arrived: the BASIC PLAY text between ESC[| or ESC[N, or ESC[MF (MB, MN, ML, MS), and Ctrl+N (0x0E). For the ESC[M form the macro begins with that M command (e.g. "MFT120O4L8CDE"). Line ends are left out; a string longer than 4096 characters, an empty one, or one cut short by ESC fires nothing. Bell (none) A BEL character (Ctrl+G) arrived. + Send Bytes As String Bytes for the host while no comm write callback is set (wgtAnsiTermSetComm): typed keys, a paste, and the replies to ESC[c and ESC[6n / ESC[255n. The argument is wgtValueBytes (read its length with wgtValueLen: it may hold NUL bytes). A reply waits until the byte that asked for it is parsed; keys typed while a Music, Bell, or Send handler runs go when it returns. .endtable The widget plays nothing itself: a handler plays the macro (BASIC: PLAY Macro; C: the music library's musicMmlPlay). While a Music or Bell handler runs, the terminal takes no input: Poll leaves the bytes in the connection, and Write keeps them, in order, until the handler returns. A tune played in the foreground (MF) therefore holds back the text after it, as on a DOS terminal, while the window stays responsive. Bare ESC[M (no parameter) followed by anything but F, B, N, L, or S is still Delete Line. A handler may close the terminal's window; the input after the event is then dropped. diff --git a/src/widgets/kpunch/ansiTerm/ansiTerm.h b/src/widgets/kpunch/ansiTerm/ansiTerm.h index 901fd0c7..cb37b151 100644 --- a/src/widgets/kpunch/ansiTerm/ansiTerm.h +++ b/src/widgets/kpunch/ansiTerm/ansiTerm.h @@ -51,14 +51,19 @@ typedef struct { void (*setMusic)(WidgetT *w, bool on); // recognise ANSI music (on at first) } AnsiTermExApiT; -// Events (the widget's onEvent; BASIC Terminal_Bell and Terminal_Music). -// Music hands over an ANSI music string -- the BASIC PLAY text between -// ESC[| or ESC[N or ESC[MF (MB, MN, ML, MS) and Ctrl+N -- as its one -// string argument; Bell is a BEL (Ctrl+G). The terminal takes no input -// until the handler returns, so a tune played in the foreground holds -// back the text that follows it, as on a DOS terminal. +// Events (the widget's onEvent; BASIC Terminal_Bell, Terminal_Music, and +// Terminal_Send). Music hands over an ANSI music string -- the BASIC PLAY +// text between ESC[| or ESC[N or ESC[MF (MB, MN, ML, MS) and Ctrl+N -- as +// its one string argument; Bell is a BEL (Ctrl+G). The terminal takes no +// input until the handler returns, so a tune played in the foreground +// holds back the text that follows it, as on a DOS terminal. Send hands +// over the bytes the terminal has for the host -- typed keys, a paste, +// its replies to the host's status requests -- when no link is attached +// (wgtAnsiTermSetComm), as one string argument (wgtValueLen: it may hold +// NUL bytes), for a program with its own transport to send on. #define ANSITERM_EVT_BELL "Bell" #define ANSITERM_EVT_MUSIC "Music" +#define ANSITERM_EVT_SEND "Send" // Consumer side: the accessor and convenience macros. The widget's own // implementation defines ANSITERM_IMPL so its functions keep their names. @@ -66,7 +71,9 @@ typedef struct { static inline const AnsiTermApiT *dvxAnsiTermApi(void) { static const AnsiTermApiT *sApi; - if (!sApi) { sApi = (const AnsiTermApiT *)wgtGetApi("ansiterm", sizeof(AnsiTermApiT)); } + if (!sApi) { + sApi = (const AnsiTermApiT *)wgtGetApi("ansiterm", sizeof(AnsiTermApiT)); + } return sApi; } @@ -82,7 +89,9 @@ static inline const AnsiTermApiT *dvxAnsiTermApi(void) { static inline const AnsiTermExApiT *dvxAnsiTermExApi(void) { static const AnsiTermExApiT *sApiEx; - if (!sApiEx) { sApiEx = (const AnsiTermExApiT *)wgtGetApi("ansitermex", sizeof(AnsiTermExApiT)); } + if (!sApiEx) { + sApiEx = (const AnsiTermExApiT *)wgtGetApi("ansitermex", sizeof(AnsiTermExApiT)); + } return sApiEx; } diff --git a/src/widgets/kpunch/ansiTerm/ansiterm.bhs b/src/widgets/kpunch/ansiTerm/ansiterm.bhs index 6bd4223b..38c5ded9 100644 --- a/src/widgets/kpunch/ansiTerm/ansiterm.bhs +++ b/src/widgets/kpunch/ansiTerm/ansiterm.bhs @@ -27,6 +27,7 @@ .index Terminal .index ANSI Terminal .index VT100 +.index Send Event .h1 Terminal @@ -42,6 +43,7 @@ Its font must be fixed-pitch, every character the same width; given a proportion Property Type Description ---------- ------- ------------------------------------------- Cols Integer Number of character columns (1-512). Setting Cols or Rows gives the terminal a new size, starting as a new terminal: empty screen and scrollback, modes the host set reset (its colors and connection stay); set the form's AutoSize to True afterwards to fit the window to it. + Music Boolean Whether ANSI music sequences (ESC[M, ESC[N, ESC[| followed by PLAY text, as BBSes send) are recognised (True at first): each raises the Music event instead of being shown. False: ESC[M is always Delete Line, and ESC[N and ESC[| do nothing. Rows Integer Number of character rows (1-512). Scrollback Integer Maximum number of scrollback lines. Older lines are dropped (default 500). Setting it discards the existing history. .endtable @@ -53,14 +55,24 @@ Its font must be fixed-pitch, every character the same width; given a proportion ------ ----------- Clear Clear the terminal screen and reset the cursor to the home position. Copy() Copy the selected text to the clipboard and clear the selection. Returns False when nothing is selected. - Paste Send the clipboard's text to the attached connection as if it were typed (new lines as Enter), as Ctrl+V does. + Paste Send the clipboard's text to the attached connection as if it were typed (new lines as Enter), as Ctrl+V does. With no connection attached it goes to the Send event. Poll Poll the attached communication channel for incoming bytes and feed them into the ANSI parser. ResetColors Back to the standard 16 colors. SetColor index%, color& The color (RGB(), as ForeColor takes it) of text attribute index (0-15, in CGA order: black, blue, green, cyan, red, magenta, brown, light gray, then their bright forms). An index outside 0-15 is ignored. - Write text$ Write a string to the terminal with full ANSI escape processing. + Write text$ Write a string to the terminal with full ANSI escape processing. Every byte of the string is written, CHR$(0) included (a CHR$(0) itself is padding and shows nothing, as on other terminals), so text read from a connection of the program's own can be written as it came. A very long text appears over the next few moments, a part at a time, so the program and the rest of DVX keep going; Clear after it still clears all of it. .endtable -No default event. Uses common events only. +.h2 Type-Specific Events + +.table + Event Description + ----- ----------- + Bell A bell character (BEL, CHR$(7)) arrived. The terminal makes no sound itself: the program decides (Beep, a sound of its own, or nothing). + Music (Macro As String) An ANSI music sequence arrived (Music True): Macro is its PLAY text, to play with PLAY or MusicLoadMacro. + Send (Bytes As String) The terminal has bytes for the host and no connection is attached: the keys the user typed, a paste, or the terminal's answer to the host asking where the cursor is. A program with a connection of its own (a TCP socket, a modem it drives itself) sends Bytes on. Bytes may hold any character, CHR$(0) included. +.endtable + +No default event. The common events are raised too. .h2 Serial Communication @@ -80,6 +92,20 @@ End If The passphrase is optional. Both ends must give the same passphrase, or both give none; otherwise CommHandshake returns False. See comm.bas for the full communications API. +.h2 A Connection of Your Own + +Without an attached connection, Write shows what the program received and the Send event hands over what the terminal would send back: + +.code +Sub Sock_Data () + Terminal1.Write ReceivedText$ +End Sub + +Sub Terminal1_Send (Bytes As String) + SendToHost Bytes +End Sub +.endcode + .h2 Example .code diff --git a/src/widgets/kpunch/ansiTerm/terminal.dep b/src/widgets/kpunch/ansiTerm/ansiterm.dep similarity index 100% rename from src/widgets/kpunch/ansiTerm/terminal.dep rename to src/widgets/kpunch/ansiTerm/ansiterm.dep diff --git a/src/widgets/kpunch/ansiTerm/terminal.res b/src/widgets/kpunch/ansiTerm/terminal.res index c0c71e9a..5b0b9d6c 100644 --- a/src/widgets/kpunch/ansiTerm/terminal.res +++ b/src/widgets/kpunch/ansiTerm/terminal.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon terminal.bmp name text "Terminal" author text "Scott Duensing" diff --git a/src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c b/src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c index ad3952e1..cbced42f 100644 --- a/src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c +++ b/src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c @@ -81,7 +81,7 @@ #include "../../../libs/kpunch/texthelp/textHelp.h" // ABI stamp checked by the loader (see DVX_ABI_VERSION). -DVX_ABI_STAMP(terminal); +DVX_ABI_STAMP(ansiterm); static int32_t sTypeId = -1; @@ -105,11 +105,14 @@ static int32_t sTypeId = -1; #define ANSI_READY_NONE 0 // what the last byte parsed finished for an event #define ANSI_READY_MUSIC 1 #define ANSI_READY_BELL 2 +#define ANSI_READY_SEND 3 // a reply waits in outbox for the Send event #define ANSI_DEFAULT_ATTR 0x07 #define BLINK_MS 500 #define ANSI_TAB_WIDTH 8 #define ANSI_PALETTE_SIZE ANSITERM_PALETTE_COLORS #define ANSI_POLL_BUF 256 +#define ANSI_PARSE_BUDGET 4096 // bytes a Write or a poll parses at once (a longer text waits in pending for the next polls) +#define ANSI_PARSE_ALL INT32_MAX #define ANSI_C0_FIRST 0x01 // dirtyRows is a multi-word bitmask sized at ANSI_DIRTY_WORDS(rows) @@ -152,8 +155,11 @@ typedef struct { uint8_t *scrollback; char *music; // stb_ds: the music string being collected uint8_t *pending; // stb_ds: input waiting while an event runs (or left after one) + uint8_t *paste; // stb_ds: pasted text the host's link has not taken yet + uint8_t *outbox; // stb_ds: bytes for the host with no link attached, for the Send event bool *alive; // while an event runs: cleared when the terminal is destroyed int32_t pendingAt; // the next byte of pending to parse + int32_t pasteAt; // the next byte of paste to send int32_t scrollbackMax; int32_t scrollbackCount; int32_t scrollbackHead; @@ -241,12 +247,13 @@ static void ansiTermDirtyRow(WidgetT *w, int32_t row); static void ansiTermDirtyShiftUp(WidgetT *w); static bool ansiTermDirtyTest(const WidgetT *w, int32_t row); static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd); -static bool ansiTermDrain(WidgetT *w); +static bool ansiTermDrain(WidgetT *w, int32_t budget); static void ansiTermEraseDisplay(WidgetT *w, int32_t mode); static void ansiTermEraseLine(WidgetT *w, int32_t mode); static bool ansiTermFeed(WidgetT *w, const uint8_t *data, int32_t len); static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count); static bool ansiTermFireReady(WidgetT *w); +static bool ansiTermFlushOut(WidgetT *w); static const uint8_t *ansiTermGetLine(WidgetT *w, int32_t lineIndex); static bool ansiTermHasSelection(const WidgetT *w); static void ansiTermInsertLines(WidgetT *w, int32_t count); @@ -254,12 +261,15 @@ static void ansiTermMoveRows(WidgetT *w, DisplayT *d, const BlitOpsT static void ansiTermMusicByte(WidgetT *w, uint8_t ch); static void ansiTermMusicStart(WidgetT *w, const char *lead); static void ansiTermNewline(WidgetT *w); +static void ansiTermOut(WidgetT *w, const uint8_t *data, int32_t len); static void ansiTermPaintSelRow(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t screenRow, int32_t baseX, int32_t baseY); +static bool ansiTermPasteSend(WidgetT *w); static void ansiTermPasteToComm(WidgetT *w); static void ansiTermProcessByte(WidgetT *w, uint8_t ch); static void ansiTermProcessSgr(WidgetT *w); static void ansiTermPushScreen(WidgetT *w); static void ansiTermPutChar(WidgetT *w, uint8_t ch); +static void ansiTermReply(WidgetT *w, const uint8_t *data, int32_t len); static void ansiTermResetScreen(WidgetT *w); static void ansiTermResize(WidgetT *w, int32_t cols, int32_t rows); static void ansiTermScrollDown(WidgetT *w); @@ -272,6 +282,7 @@ static void wgtAnsiTermBasResetColors(WidgetT *w); static void wgtAnsiTermBasSetColor(WidgetT *w, int32_t index, int32_t color); static void wgtAnsiTermBasSetCols(WidgetT *w, int32_t cols); static void wgtAnsiTermBasSetRows(WidgetT *w, int32_t rows); +static bool wgtAnsiTermBasWrite(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret); void wgtAnsiTermClear(WidgetT *w); static bool wgtAnsiTermCopy(WidgetT *w); static int32_t wgtAnsiTermGetCols(const WidgetT *w); @@ -285,7 +296,6 @@ static void wgtAnsiTermSetMusic(WidgetT *w, bool on); static void wgtAnsiTermSetPalette(WidgetT *w, const uint8_t *rgb); void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines); void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len); -static void wgtAnsiTermWriteString(WidgetT *w, const char *text); void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font); static bool widgetAnsiTermClearSelection(WidgetT *w); void widgetAnsiTermDestroy(WidgetT *w); @@ -567,10 +577,9 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { break; } case 'c': { - if (at->commWrite) { - const uint8_t reply[] = "\033[?1;2c"; - at->commWrite(at->commCtx, reply, sizeof(reply) - 1); - } + const uint8_t reply[] = "\033[?1;2c"; + + ansiTermReply(w, reply, sizeof(reply) - 1); break; } case 'E': { @@ -691,16 +700,14 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { break; case 'n': { int32_t mode = (n >= 1) ? p[0] : 0; - if (at->commWrite) { - if (mode == 6) { - char reply[16]; - int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)at->cursorRow + 1, (long)at->cursorCol + 1); - at->commWrite(at->commCtx, (const uint8_t *)reply, len); - } else if (mode == 255) { - char reply[16]; - int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)at->rows, (long)at->cols); - at->commWrite(at->commCtx, (const uint8_t *)reply, len); - } + if (mode == 6) { + char reply[16]; + int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)at->cursorRow + 1, (long)at->cursorCol + 1); + ansiTermReply(w, (const uint8_t *)reply, len); + } else if (mode == 255) { + char reply[16]; + int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)at->rows, (long)at->cols); + ansiTermReply(w, (const uint8_t *)reply, len); } break; } @@ -739,13 +746,13 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { } -// Parse the input waiting in pending, unless an event runs; what arrives -// while one does goes on the end. False when an event destroyed the -// terminal. -static bool ansiTermDrain(WidgetT *w) { +// Parse up to budget bytes of the input waiting in pending, unless an +// event runs; what arrives while one does goes on the end. False when an +// event destroyed the terminal. +static bool ansiTermDrain(WidgetT *w, int32_t budget) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; - while (!at->inEvent && at->pendingAt < arrlen(at->pending)) { + for (int32_t n = 0; n < budget && !at->inEvent && at->pendingAt < arrlen(at->pending); n++) { ansiTermProcessByte(w, at->pending[at->pendingAt++]); if (at->ready != ANSI_READY_NONE && !ansiTermFireReady(w)) { @@ -753,7 +760,7 @@ static bool ansiTermDrain(WidgetT *w) { } } - if (!at->inEvent) { + if (!at->inEvent && at->pendingAt >= arrlen(at->pending)) { arrfree(at->pending); at->pendingAt = 0; } @@ -838,9 +845,11 @@ static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count) { } -// Fire the event the parse finished: Music with its string, or Bell. The -// terminal takes no input until the handler returns. False when the -// handler destroyed the terminal (its window closed). +// Fire the event the parse finished (Music with its string, or Bell), +// then Send with the bytes waiting in outbox, again while the handlers +// add more (keys typed while one runs). The terminal takes no input +// until the handlers return. False when a handler destroyed the +// terminal (its window closed). static bool ansiTermFireReady(WidgetT *w) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; uint8_t ready = at->ready; @@ -854,20 +863,51 @@ static bool ansiTermFireReady(WidgetT *w) { WgtValueT arg = wgtValueStr(at->music); wgtFireEvent(w, ANSITERM_EVT_MUSIC, &arg, 1); - } else { + } else if (ready == ANSI_READY_BELL) { wgtFireEvent(w, ANSITERM_EVT_BELL, NULL, 0); } + // The bytes are taken first: a handler may add more, or destroy the + // terminal and its outbox + while (alive && arrlen(at->outbox) > 0) { + uint8_t *out = at->outbox; + int32_t len = (int32_t)arrlen(out); + + at->outbox = NULL; + arrput(out, '\0'); + + WgtValueT arg = wgtValueBytes((const char *)out, len); + + wgtFireEvent(w, ANSITERM_EVT_SEND, &arg, 1); + arrfree(out); + } + if (!alive) { return false; } + at->ready = ANSI_READY_NONE; at->alive = NULL; at->inEvent = false; return true; } +// Send what waits in outbox now (the Send event), unless an event runs: +// then it goes when that one returns. False when the handler destroyed +// the terminal. +static bool ansiTermFlushOut(WidgetT *w) { + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + + if (at->inEvent || arrlen(at->outbox) == 0) { + return true; + } + + at->ready = ANSI_READY_SEND; + return ansiTermFireReady(w); +} + + static const uint8_t *ansiTermGetLine(WidgetT *w, int32_t lineIndex) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t cols = at->cols; @@ -996,6 +1036,19 @@ static void ansiTermNewline(WidgetT *w) { } +// Bytes for the host: to the attached link, else (a program's own +// transport) into outbox for the Send event. +static void ansiTermOut(WidgetT *w, const uint8_t *data, int32_t len) { + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + + if (at->commWrite) { + at->commWrite(at->commCtx, data, len); + } else { + memcpy(arraddnptr(at->outbox, len), data, (size_t)len); + } +} + + static void ansiTermPaintSelRow(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t screenRow, int32_t baseX, int32_t baseY) { if (!ansiTermHasSelection(w)) { return; @@ -1030,32 +1083,54 @@ static void ansiTermPaintSelRow(WidgetT *w, DisplayT *d, const BlitOpsT *ops, co } -// Send the clipboard to the host (newlines as CR) in chunks and, like -// typing, return the view to the live screen. +// Send what is left of a paste, in chunks, as far as the host's link takes +// it now; the rest goes from the next polls. With no link the Send event +// takes it all at once. False when its handler destroyed the terminal. +static bool ansiTermPasteSend(WidgetT *w) { + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + int32_t left = (int32_t)arrlen(at->paste) - at->pasteAt; + + if (!at->commWrite && left > 0) { + ansiTermOut(w, at->paste + at->pasteAt, left); + } + + while (at->commWrite && at->pasteAt < (int32_t)arrlen(at->paste)) { + int32_t n = at->commWrite(at->commCtx, at->paste + at->pasteAt, DVX_MIN((int32_t)arrlen(at->paste) - at->pasteAt, ANSI_PASTE_CHUNK)); + + if (n <= 0) { + return true; + } + + at->pasteAt += n; + } + + arrfree(at->paste); + at->paste = NULL; + at->pasteAt = 0; + return ansiTermFlushOut(w); +} + + +// Send the clipboard to the host (newlines as CR; after a paste still +// going out) and, like typing, return the view to the live screen. static void ansiTermPasteToComm(WidgetT *w) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t clipLen = 0; const char *clip = clipboardGet(&clipLen); - if (!at->commWrite || !clip || clipLen <= 0) { + if (!clip || clipLen <= 0) { return; } ansiTermScrollView(w, at->scrollbackCount - at->scrollPos); - uint8_t chunk[ANSI_PASTE_CHUNK]; - int32_t n = 0; + uint8_t *to = arraddnptr(at->paste, clipLen); for (int32_t i = 0; i < clipLen; i++) { - uint8_t ch = (uint8_t)clip[i]; - - chunk[n++] = (ch == '\n') ? '\r' : ch; - - if (n == ANSI_PASTE_CHUNK || i == clipLen - 1) { - at->commWrite(at->commCtx, chunk, n); - n = 0; - } + to[i] = (clip[i] == '\n') ? '\r' : (uint8_t)clip[i]; } + + ansiTermPasteSend(w); } @@ -1063,7 +1138,9 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; switch (at->parseState) { case PARSE_NORMAL: - if (ch == KEY_ESCAPE) { + if (ch == '\0') { + // NUL is line padding: ignored, as terminals do + } else if (ch == KEY_ESCAPE) { at->parseState = PARSE_ESC; } else if (ch == '\r') { at->cursorCol = 0; @@ -1281,6 +1358,20 @@ static void ansiTermPutChar(WidgetT *w, uint8_t ch) { } +// A reply to the host's request, from the parse: to the link, else into +// outbox for the Send event, which fires once this byte is parsed +// (ansiTermFireReady). +static void ansiTermReply(WidgetT *w, const uint8_t *data, int32_t len) { + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + + ansiTermOut(w, data, len); + + if (!at->commWrite) { + at->ready = ANSI_READY_SEND; + } +} + + // The screen of a new terminal: every cell blank in the default // attribute, the cursor home and the parser idle. Nothing goes to the // scrollback (callers that clear a screen the user saw push it first). @@ -1307,6 +1398,11 @@ static void ansiTermResetScreen(WidgetT *w) { static void ansiTermResize(WidgetT *w, int32_t cols, int32_t rows) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; + // Text written before is laid out in the old size, as it was sent + if (!ansiTermDrain(w, ANSI_PARSE_ALL)) { + return; + } + cols = DVX_MIN(DVX_MAX(cols, 1), ANSI_MAX_COLS); rows = DVX_MIN(DVX_MAX(rows, 1), ANSI_MAX_ROWS); @@ -1557,8 +1653,34 @@ static void wgtAnsiTermBasSetRows(WidgetT *w, int32_t rows) { } +// Write text$ in BASIC (dynamic, for the string's length: a stream from a +// program's own transport may hold NUL bytes). +static bool wgtAnsiTermBasWrite(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret) { + char buf[WGT_VALUE_TEXT_MAX]; + + (void)ret; + + if (argc != 1) { + return false; + } + + const char *text = wgtValueToText(&args[0], buf, (int32_t)sizeof(buf)); + int32_t len = (args[0].type == WGT_IFACE_STRING) ? wgtValueLen(&args[0]) : (int32_t)strlen(text); + + wgtAnsiTermWrite(w, (const uint8_t *)text, len); + return true; +} + + +// Text written before (still waiting to be parsed) goes on the screen +// first, so it does not come back after the clear. void wgtAnsiTermClear(WidgetT *w) { VALIDATE_WIDGET_VOID(w, sTypeId); + + if (!ansiTermDrain(w, ANSI_PARSE_ALL)) { + return; + } + ansiTermPushScreen(w); ansiTermResetScreen(w); } @@ -1641,6 +1763,15 @@ int32_t wgtAnsiTermPoll(WidgetT *w) { at->cursorOn = !at->cursorOn; ansiTermDirtyRow(w, at->cursorRow); } + if (at->paste && !ansiTermPasteSend(w)) { + return 0; + } + + // The rest of a long text written: another budget of it + if (!ansiTermDrain(w, ANSI_PARSE_BUDGET)) { + return 0; + } + // While an event runs the input stays in the connection if (!at->commRead || at->inEvent) { return 0; @@ -1793,7 +1924,8 @@ static void wgtAnsiTermSetPalette(WidgetT *w, const uint8_t *rgb) { void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines) { - if (!w || w->type != sTypeId || maxLines <= 0) { + // Text written before scrolls into the old scrollback, which then goes + if (!w || w->type != sTypeId || maxLines <= 0 || !ansiTermDrain(w, ANSI_PARSE_ALL)) { return; } // Cap the line count so maxLines * cols * 2 cannot overflow int32_t @@ -1827,23 +1959,15 @@ void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; ansiTermClearSelection(w); - // Behind any input waiting (while an event runs, it only waits) - if (at->inEvent || arrlen(at->pending) > 0) { + // Behind any input waiting (while an event runs, it only waits); a long + // text is parsed a budget at a time, the rest at the next polls + if (at->inEvent || arrlen(at->pending) > 0 || len > ANSI_PARSE_BUDGET) { memcpy(arraddnptr(at->pending, len), data, (size_t)len); } else if (!ansiTermFeed(w, data, len)) { return; } - ansiTermDrain(w); -} - - -static void wgtAnsiTermWriteString(WidgetT *w, const char *text) { - if (!text) { - return; - } - - wgtAnsiTermWrite(w, (const uint8_t *)text, (int32_t)strlen(text)); + ansiTermDrain(w, ANSI_PARSE_BUDGET); } @@ -1882,6 +2006,8 @@ void widgetAnsiTermDestroy(WidgetT *w) { free(at->dirtyRows); arrfree(at->music); arrfree(at->pending); + arrfree(at->paste); + arrfree(at->outbox); free(at); w->data = NULL; } @@ -1936,10 +2062,6 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) { return; } - if (!at->commWrite) { - return; - } - ansiTermClearSelection(w); uint8_t ch = 0; @@ -1968,10 +2090,12 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) { ansiTermScrollView(w, at->scrollbackCount - at->scrollPos); if (seq) { - at->commWrite(at->commCtx, (const uint8_t *)seq, (int32_t)strlen(seq)); + ansiTermOut(w, (const uint8_t *)seq, (int32_t)strlen(seq)); } else { - at->commWrite(at->commCtx, &ch, 1); + ansiTermOut(w, &ch, 1); } + + ansiTermFlushOut(w); } @@ -2166,13 +2290,14 @@ static const WgtMethodDescT sMethods[] = { { "Poll", WGT_SIG_VOID, (void *)wgtAnsiTermPoll }, { "ResetColors", WGT_SIG_VOID, (void *)wgtAnsiTermBasResetColors }, { "SetColor", WGT_SIG_INT_INT, (void *)wgtAnsiTermBasSetColor }, - { "Write", WGT_SIG_STR, (void *)wgtAnsiTermWriteString } + { "Write", WGT_SIG_DYNAMIC, (void *)wgtAnsiTermBasWrite } }; // Bell: a BEL arrived. Music: an ANSI music string (BASIC PLAY text). static const WgtEventDescT sEvents[] = { { ANSITERM_EVT_BELL, "" }, - { ANSITERM_EVT_MUSIC, "Macro As String" } + { ANSITERM_EVT_MUSIC, "Macro As String" }, + { ANSITERM_EVT_SEND, "Bytes As String" } }; static const WgtIfaceT sIface = { diff --git a/src/widgets/kpunch/box/box.h b/src/widgets/kpunch/box/box.h index 801ba671..10b23c8d 100644 --- a/src/widgets/kpunch/box/box.h +++ b/src/widgets/kpunch/box/box.h @@ -39,7 +39,9 @@ typedef struct { static inline const BoxApiT *dvxBoxApi(void) { static const BoxApiT *sApi; - if (!sApi) { sApi = (const BoxApiT *)wgtGetApi("box", sizeof(BoxApiT)); } + if (!sApi) { + sApi = (const BoxApiT *)wgtGetApi("box", sizeof(BoxApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/box/box.res b/src/widgets/kpunch/box/box.res index 53cef95c..fd8b8200 100644 --- a/src/widgets/kpunch/box/box.res +++ b/src/widgets/kpunch/box/box.res @@ -1,3 +1,25 @@ +# 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. + # box.res -- Resources for VBox, HBox, and Frame widgets # # VBox (first registered, uses unsuffixed names) diff --git a/src/widgets/kpunch/box/boxMetrics.h b/src/widgets/kpunch/box/boxMetrics.h index af59c017..e3bd762d 100644 --- a/src/widgets/kpunch/box/boxMetrics.h +++ b/src/widgets/kpunch/box/boxMetrics.h @@ -20,18 +20,26 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -// boxMetrics.h -- class default padding/spacing for box-style containers +// boxMetrics.h -- class default padding/spacing for box-style containers, +// and the BASIC properties they share // // widgetBoxMetrics resolves w->padding / w->spacing (falling back to // DEFAULT_PADDING / DEFAULT_SPACING) before it calls a class's // GET_LAYOUT_METRICS hook. Containers with their own defaults (Frame, // Toolbar, StatusBar, WrapBox) call this from the hook so the class // default applies only when the property was left unset (WGT_GAP_DEFAULT) -// and a user Padding/Spacing, 0 included, still wins. Private to the widget modules; include -// after dvxWgtP.h. +// and a user Padding/Spacing, 0 included, still wins. Private to the +// widget modules; include after dvxWgtP.h (and ../label/alignNames.h for +// BOX_ALIGN_PROP). #ifndef BOXMETRICS_H #define BOXMETRICS_H +// A box-style container's BASIC Alignment, Padding, and Spacing (one +// WgtPropDescT entry each) +#define BOX_ALIGN_PROP { "Alignment", WGT_IFACE_ENUM, (void *)boxGetAlign, (void *)boxSetAlign, sAlignNames } +#define BOX_PADDING_PROP { "Padding", WGT_IFACE_INT, (void *)wgtGetPaddingPx, (void *)wgtSetPaddingPx, NULL } +#define BOX_SPACING_PROP { "Spacing", WGT_IFACE_INT, (void *)wgtGetSpacingPx, (void *)wgtSetSpacingPx, NULL } + // Padding default only (the spacing keeps the generic default) static inline void boxClassDefaultPad(const WidgetT *w, int32_t *pad, int32_t defPad) { if (w->padding == WGT_GAP_DEFAULT) { @@ -48,4 +56,16 @@ static inline void boxClassDefaults(const WidgetT *w, int32_t *pad, int32_t *gap } } + +// BASIC Alignment of a box-style container: how its children sit across it +static inline int32_t boxGetAlign(const WidgetT *w) { + return (int32_t)w->align; +} + + +static inline void boxSetAlign(WidgetT *w, int32_t align) { + w->align = (WidgetAlignE)align; + wgtInvalidate(w); +} + #endif // BOXMETRICS_H diff --git a/src/widgets/kpunch/box/widgetBox.c b/src/widgets/kpunch/box/widgetBox.c index 396b3ad6..a7881b33 100644 --- a/src/widgets/kpunch/box/widgetBox.c +++ b/src/widgets/kpunch/box/widgetBox.c @@ -85,8 +85,6 @@ typedef struct { // Prototypes // ============================================================ -static int32_t boxGetAlign(const WidgetT *w); -static void boxSetAlign(WidgetT *w, int32_t align); static int32_t frameBoxTop(const BitmapFontT *font); WidgetT *wgtFrame(WidgetT *parent, const char *title); WidgetT *wgtHBox(WidgetT *parent); @@ -96,19 +94,6 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, co void wgtRegister(void); -// BASIC Alignment of a VBox, HBox or Frame (Padding and Spacing are the -// shared wgtGetPaddingPx / wgtSetPaddingPx family). -static int32_t boxGetAlign(const WidgetT *w) { - return (int32_t)w->align; -} - - -static void boxSetAlign(WidgetT *w, int32_t align) { - w->align = (WidgetAlignE)align; - wgtInvalidate(w); -} - - // Offset of the groove below the widget top: half a glyph, so the title // sits centered on the top border line. static int32_t frameBoxTop(const BitmapFontT *font) { @@ -241,9 +226,9 @@ static const FrameCreateApiT sFrameApi = { // The same properties on every box type. static const WgtPropDescT sBoxProps[] = { - { "Alignment", WGT_IFACE_ENUM, (void *)boxGetAlign, (void *)boxSetAlign, sAlignNames }, - { "Padding", WGT_IFACE_INT, (void *)wgtGetPaddingPx, (void *)wgtSetPaddingPx, NULL }, - { "Spacing", WGT_IFACE_INT, (void *)wgtGetSpacingPx, (void *)wgtSetSpacingPx, NULL } + BOX_ALIGN_PROP, + BOX_PADDING_PROP, + BOX_SPACING_PROP }; static const WgtIfaceT sIfaceVBox = { diff --git a/src/widgets/kpunch/button/button.h b/src/widgets/kpunch/button/button.h index e5ad6ff3..aef5901f 100644 --- a/src/widgets/kpunch/button/button.h +++ b/src/widgets/kpunch/button/button.h @@ -37,7 +37,9 @@ typedef struct { static inline const ButtonApiT *dvxButtonApi(void) { static const ButtonApiT *sApi; - if (!sApi) { sApi = (const ButtonApiT *)wgtGetApi("button", sizeof(ButtonApiT)); } + if (!sApi) { + sApi = (const ButtonApiT *)wgtGetApi("button", sizeof(ButtonApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/button/button.res b/src/widgets/kpunch/button/button.res index effc92e5..94ae87a7 100644 --- a/src/widgets/kpunch/button/button.res +++ b/src/widgets/kpunch/button/button.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon button.bmp name text "Button" author text "Scott Duensing" diff --git a/src/widgets/kpunch/canvas/canvas.bhs b/src/widgets/kpunch/canvas/canvas.bhs index c783eac2..6ce8c086 100644 --- a/src/widgets/kpunch/canvas/canvas.bhs +++ b/src/widgets/kpunch/canvas/canvas.bhs @@ -29,6 +29,7 @@ .index SetPenColor .index SetPenSize .index BorderStyle +.index SetPictureData .h1 PictureBox @@ -46,6 +47,7 @@ Colors are Visual Basic RGB values, &HBBGGRR, as RGB() makes them (RGB(255, 0, 0 BorderStyle Enum 0 - None: no frame, the control is exactly the size of the drawing. 1 - FixedSingle (the default): a sunken frame 2 pixels wide around the drawing. Mouse coordinates and drawing positions start inside the frame either way. ImageHeight Integer Height of the drawing in pixels (read-only). After Load it is the loaded image's height. ImageWidth Integer Width of the drawing in pixels (read-only). After Load it is the loaded image's width. + Picture String The picture file the canvas shows ("" when it shows none). Setting it to a file name, or to LoadPicture(path$), loads that file as Load does; LoadPictureData(bytes$) shows a picture from bytes; setting it to "" (or LoadPicture("")) clears the canvas to white. Clear and Resize wipe the picture, so Picture becomes "". The default property: Picture1 = "logo.bmp" loads it. .endtable Load and Save return 0 on success and -1 on failure (missing or unreadable file, unsupported format, path that cannot be written). Call them as functions to check the result, or as statements to ignore it. A failed Load leaves the drawing unchanged. @@ -67,6 +69,7 @@ Load and Save return 0 on success and -1 on failure (missing or unreadable file, Save(path$) Save the canvas: BMP when path$ ends in .bmp, PNG otherwise. Returns 0 on success, -1 on failure. SetPenColor color% Set the drawing color for subsequent operations. SetPenSize size% Set the pen/brush size in pixels (1 to 255; larger values use 255). + SetPictureData(bytes$ [, name$]) Show the picture held in bytes$ (the bytes of a BMP, PNG, JPEG, or GIF file, such as HttpBody$ or ResGetData$ gives) as Load shows a file, without keeping a Picture object as Picture = LoadPictureData(bytes$) does. Picture then reads name$ ("" without it). Returns True, or False (the drawing unchanged) when the bytes are not a picture. SetPixel x%, y%, color% Set the color of a single pixel. .endtable diff --git a/src/widgets/kpunch/canvas/canvas.dhs b/src/widgets/kpunch/canvas/canvas.dhs index 15fb42db..795b4b27 100644 --- a/src/widgets/kpunch/canvas/canvas.dhs +++ b/src/widgets/kpunch/canvas/canvas.dhs @@ -37,6 +37,7 @@ .index wgtCanvasDrawTextScaled .index wgtCanvasSave .index wgtCanvasLoad +.index wgtCanvasLoadData .index wgtCanvasResize .index wgtCanvasSetBorderStyle .index wgtCanvasGetBorderStyle @@ -83,6 +84,7 @@ WidgetT *cv = wgtCanvas(parent, 320, 200); void wgtCanvasSetMouseCallback(w, cb) Set a mouse callback. Signature: void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag). cx/cy are canvas-relative, drag=true on mouse-move during press. int32_t wgtCanvasSave(w, path) Save the canvas buffer to an image file: BMP when path ends in .bmp, PNG otherwise. Returns 0 on success, -1 on error. int32_t wgtCanvasLoad(w, path) Load an image file into the canvas (resizes buffer to match). Returns 0 on success, -1 on error. + int32_t wgtCanvasLoadData(w, data, len, name) Load the picture in len bytes of an image file (as dvxLoadImageFromMemory reads them) as wgtCanvasLoad loads a file; Picture then reads name (NULL: ""). Returns 0 on success, -1 when they are not a picture. void wgtCanvasResize(w, newW, newH) Resize the canvas buffer and relayout. The old content is discarded; the new buffer is white. void wgtCanvasSetBorderStyle(w, style) CANVAS_BORDER_NONE (0) drops the sunken frame, any other value (CANVAS_BORDER_FIXED, 1, the default) shows it. The buffer keeps its size; the widget relays out. int32_t wgtCanvasGetBorderStyle(w) CANVAS_BORDER_FIXED or CANVAS_BORDER_NONE. @@ -114,6 +116,7 @@ WidgetT *cv = wgtCanvas(parent, 320, 200); getBorderStyle wgtCanvasGetBorderStyle getDisplay wgtCanvasGetDisplay drawTextScaled wgtCanvasDrawTextScaled + loadData wgtCanvasLoadData .endtable .h3 Properties @@ -126,6 +129,7 @@ Drawing state (pen color, pen size) is managed by the API functions above. The B BorderStyle Enum Read/Write None (0) or FixedSingle (1, the default): wgtCanvasSetBorderStyle / wgtCanvasGetBorderStyle. ImageHeight Integer Read-only Height of the canvas buffer in pixels (0 when there is no buffer). ImageWidth Integer Read-only Width of the canvas buffer in pixels (0 when there is no buffer). + Picture String Read/Write The file wgtCanvasLoad loaded, or the name wgtCanvasLoadData was given ("" when none; Clear and Resize wipe it). Setting a path loads it; setting "" clears the canvas to white. In BASIC it reads as a Picture object (see LOADPICTURE). .endtable .h3 Methods (BASIC Interface) @@ -145,6 +149,7 @@ Drawing state (pen color, pen size) is managed by the API functions above. The B Save(path$) Save the canvas: BMP when path$ ends in .bmp, PNG otherwise. Returns 0 on success, -1 on error (WGT_SIG_RET_INT_STR). SetPenColor color% Set the drawing color (&HBBGGRR). SetPenSize size% Set the pen size in pixels (1 to 255). + SetPictureData(bytes$[, name$]) WGT_METHOD_SET_PICTURE_DATA (WGT_SIG_DYNAMIC): wgtCanvasLoadData(bytes, length, name). Returns True when the bytes are a picture. SetPixel x%, y%, color% Set a single pixel. .endtable diff --git a/src/widgets/kpunch/canvas/canvas.h b/src/widgets/kpunch/canvas/canvas.h index feca83af..3403de6e 100644 --- a/src/widgets/kpunch/canvas/canvas.h +++ b/src/widgets/kpunch/canvas/canvas.h @@ -54,6 +54,7 @@ typedef struct { int32_t (*getBorderStyle)(const WidgetT *w); bool (*getDisplay)(const WidgetT *w, DisplayT *out); void (*drawTextScaled)(WidgetT *w, int32_t x, int32_t y, const char *text, int32_t scale); + int32_t (*loadData)(WidgetT *w, const uint8_t *data, int32_t len, const char *name); } CanvasApiT; // Consumer side: the accessor and convenience macros. The widget's own @@ -62,7 +63,9 @@ typedef struct { static inline const CanvasApiT *dvxCanvasApi(void) { static const CanvasApiT *sApi; - if (!sApi) { sApi = (const CanvasApiT *)wgtGetApi("canvas", sizeof(CanvasApiT)); } + if (!sApi) { + sApi = (const CanvasApiT *)wgtGetApi("canvas", sizeof(CanvasApiT)); + } return sApi; } @@ -85,6 +88,7 @@ static inline const CanvasApiT *dvxCanvasApi(void) { #define wgtCanvasGetBorderStyle(w) dvxCanvasApi()->getBorderStyle(w) #define wgtCanvasGetDisplay(w, out) dvxCanvasApi()->getDisplay(w, out) #define wgtCanvasDrawTextScaled(w, x, y, text, scale) dvxCanvasApi()->drawTextScaled(w, x, y, text, scale) +#define wgtCanvasLoadData(w, data, len, name) dvxCanvasApi()->loadData(w, data, len, name) #endif // CANVAS_IMPL diff --git a/src/widgets/kpunch/canvas/canvas.res b/src/widgets/kpunch/canvas/canvas.res index efab0ca4..2cc591aa 100644 --- a/src/widgets/kpunch/canvas/canvas.res +++ b/src/widgets/kpunch/canvas/canvas.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon canvas.bmp name text "Canvas" author text "Scott Duensing" diff --git a/src/widgets/kpunch/canvas/widgetCanvas.c b/src/widgets/kpunch/canvas/widgetCanvas.c index 3c75e482..8b821cde 100644 --- a/src/widgets/kpunch/canvas/widgetCanvas.c +++ b/src/widgets/kpunch/canvas/widgetCanvas.c @@ -80,6 +80,7 @@ typedef struct { uint32_t penColor; int32_t penSize; void (*onMouse)(struct WidgetT *w, int32_t cx, int32_t cy, bool drag); + char picturePath[DVX_MAX_PATH]; // the file it shows (Load, BASIC Picture); "" when none } CanvasDataT; @@ -94,9 +95,12 @@ static void basFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t static void basFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height, int32_t color); static int32_t basGetImageHeight(const WidgetT *w); static int32_t basGetImageWidth(const WidgetT *w); +static const char *basGetPicture(const WidgetT *w); static uint32_t basGetPixel(const WidgetT *w, int32_t x, int32_t y); static uint32_t basicToPacked(WidgetT *w, uint32_t color); static void basSetPenColor(WidgetT *w, int32_t color); +static void basSetPicture(WidgetT *w, const char *path); +static bool basSetPictureData(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret); static void basSetPixel(WidgetT *w, int32_t x, int32_t y, int32_t color); static uint8_t *canvasAllocWhite(const DisplayT *d, const BlitOpsT *ops, int32_t w, int32_t h, int32_t *pitchOut); static int32_t canvasBufferBytes(int32_t w, int32_t h, int32_t bpp); @@ -105,12 +109,11 @@ static CanvasDataT *canvasData(const WidgetT *w); static void canvasDrawDot(CanvasDataT *cd, int32_t cx, int32_t cy); static void canvasDrawLine(CanvasDataT *cd, int32_t x0, int32_t y0, int32_t x1, int32_t y1); static void canvasFillCircleSpans(CanvasDataT *cd, int32_t cx, int32_t cy, int32_t radius); -static inline uint32_t canvasGetPixel(const uint8_t *src, int32_t bpp); static void canvasHSpan(CanvasDataT *cd, int64_t py, int64_t x0, int64_t x1); static int64_t canvasIsqrt(uint64_t n); +static int32_t canvasLoadImage(WidgetT *w, const char *path, const uint8_t *data, int32_t len); static int32_t canvasOutCode(int64_t x, int64_t y, int64_t minX, int64_t minY, int64_t maxX, int64_t maxY); static void canvasPlot(CanvasDataT *cd, int64_t x, int64_t y); -static inline void canvasPutPixel(uint8_t *dst, uint32_t color, int32_t bpp); WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h); void wgtCanvasClear(WidgetT *w, uint32_t color); void wgtCanvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1); @@ -123,6 +126,7 @@ int32_t wgtCanvasGetBorderStyle(const WidgetT *w); bool wgtCanvasGetDisplay(const WidgetT *w, DisplayT *out); uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y); int32_t wgtCanvasLoad(WidgetT *w, const char *path); +int32_t wgtCanvasLoadData(WidgetT *w, const uint8_t *data, int32_t len, const char *name); void wgtCanvasResize(WidgetT *w, int32_t newW, int32_t newH); int32_t wgtCanvasSave(WidgetT *w, const char *path); void wgtCanvasSetBorderStyle(WidgetT *w, int32_t style); @@ -186,6 +190,14 @@ static int32_t basGetImageWidth(const WidgetT *w) { } +// Picture: the file the box shows ("" when it shows none). +static const char *basGetPicture(const WidgetT *w) { + const CanvasDataT *cd = canvasData(w); + + return cd ? cd->picturePath : ""; +} + + static uint32_t basGetPixel(const WidgetT *w, int32_t x, int32_t y) { uint32_t packed = wgtCanvasGetPixel(w, x, y); AppContextT *ctx = wgtGetContext(w); @@ -224,6 +236,35 @@ static void basSetPenColor(WidgetT *w, int32_t color) { } +// Picture = path: show that file (Load, the box taking its size); "" (an +// empty LoadPicture) clears the box to white. +static void basSetPicture(WidgetT *w, const char *path) { + AppContextT *ctx = wgtGetContext(w); + + if (path && path[0]) { + wgtCanvasLoad(w, path); + } else if (ctx) { + wgtCanvasClear(w, packColor(&ctx->display, CANVAS_WHITE, CANVAS_WHITE, CANVAS_WHITE)); + } +} + + +// SetPictureData bytes$[, name$] in BASIC (dynamic: the bytes may hold +// NULs): True when they are a picture DVX can show +static bool basSetPictureData(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret) { + char nameBuf[WGT_VALUE_TEXT_MAX]; + + if (argc < WGT_PICTURE_DATA_ARGS_MIN || argc > WGT_PICTURE_DATA_ARGS_MAX || args[0].type != WGT_IFACE_STRING) { + return false; + } + + const char *name = (argc == WGT_PICTURE_DATA_ARGS_MAX) ? wgtValueToText(&args[1], nameBuf, (int32_t)sizeof(nameBuf)) : NULL; + + *ret = wgtValueBool(wgtCanvasLoadData(w, (const uint8_t *)args[0].s, wgtValueLen(&args[0]), name) == 0); + return true; +} + + static void basSetPixel(WidgetT *w, int32_t x, int32_t y, int32_t color) { wgtCanvasSetPixel(w, x, y, basicToPacked(w, (uint32_t)color)); } @@ -446,19 +487,6 @@ static void canvasFillCircleSpans(CanvasDataT *cd, int32_t cx, int32_t cy, int32 } -// Read/write a single pixel at the given address, respecting the display's -// bytes-per-pixel depth (1=8-bit palette, 2=16-bit hicolor, 4=32-bit truecolor). -static inline uint32_t canvasGetPixel(const uint8_t *src, int32_t bpp) { - if (bpp == 1) { - return *src; - } else if (bpp == 2) { - return *(const uint16_t *)src; - } else { - return *(const uint32_t *)src; - } -} - - // Fill the inclusive run [x0, x1] of row py with the pen color, clipped. static void canvasHSpan(CanvasDataT *cd, int64_t py, int64_t x0, int64_t x1) { if (py < 0 || py >= cd->canvasH) { @@ -506,6 +534,43 @@ static int64_t canvasIsqrt(uint64_t n) { } +// Load (the file at path) or LoadData (data, len bytes; path the name to +// keep): the picture becomes the drawing buffer, the box taking its size, +// and Picture reads path. 0, else -1 (the buffer unchanged) when it +// cannot be read. +static int32_t canvasLoadImage(WidgetT *w, const char *path, const uint8_t *data, int32_t len) { + AppContextT *ctx = (w && w->type == sTypeId) ? wgtGetContext(w) : NULL; + + if (!ctx) { + return -1; + } + + int32_t imgW; + int32_t imgH; + int32_t pitch; + uint8_t *pixels = data ? dvxLoadImageFromMemory(ctx, data, len, &imgW, &imgH, &pitch) : dvxLoadImage(ctx, path, &imgW, &imgH, &pitch); + + if (!pixels) { + return -1; + } + + CanvasDataT *cd = (CanvasDataT *)w->data; + + free(cd->pixelData); + cd->pixelData = pixels; + cd->canvasW = imgW; + cd->canvasH = imgH; + cd->canvasPitch = pitch; + cd->canvasBpp = ctx->display.format.bytesPerPixel; + snprintf(cd->picturePath, sizeof(cd->picturePath), "%s", path); + + // The buffer's size drives the min-size calc, so a full relayout is + // needed, not just a repaint + wgtInvalidate(w); + return 0; +} + + static int32_t canvasOutCode(int64_t x, int64_t y, int64_t minX, int64_t minY, int64_t maxX, int64_t maxY) { int32_t code = 0; @@ -531,18 +596,7 @@ static void canvasPlot(CanvasDataT *cd, int64_t x, int64_t y) { return; } - canvasPutPixel(cd->pixelData + (int32_t)y * cd->canvasPitch + (int32_t)x * cd->canvasBpp, cd->penColor, cd->canvasBpp); -} - - -static inline void canvasPutPixel(uint8_t *dst, uint32_t color, int32_t bpp) { - if (bpp == 1) { - *dst = (uint8_t)color; - } else if (bpp == 2) { - *(uint16_t *)dst = (uint16_t)color; - } else { - *(uint32_t *)dst = color; - } + pixelPut(cd->pixelData + (int32_t)y * cd->canvasPitch + (int32_t)x * cd->canvasBpp, cd->penColor, cd->canvasBpp); } @@ -605,6 +659,7 @@ void wgtCanvasClear(WidgetT *w, uint32_t color) { cd->ops->spanFill(cd->pixelData + y * cd->canvasPitch, color, cd->canvasW); } + cd->picturePath[0] = '\0'; wgtInvalidatePaint(w); } @@ -769,43 +824,17 @@ uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y) { return 0; } - return canvasGetPixel(cd->pixelData + y * cd->canvasPitch + x * cd->canvasBpp, cd->canvasBpp); + return pixelGet(cd->pixelData + y * cd->canvasPitch + x * cd->canvasBpp, cd->canvasBpp); } int32_t wgtCanvasLoad(WidgetT *w, const char *path) { - if (!w || w->type != sTypeId || !path) { - return -1; - } + return path ? canvasLoadImage(w, path, NULL, 0) : -1; +} - AppContextT *ctx = wgtGetContext(w); - if (!ctx) { - return -1; - } - - int32_t imgW; - int32_t imgH; - int32_t pitch; - uint8_t *data = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch); - - if (!data) { - return -1; - } - - CanvasDataT *cd = (CanvasDataT *)w->data; - - free(cd->pixelData); - cd->pixelData = data; - cd->canvasW = imgW; - cd->canvasH = imgH; - cd->canvasPitch = pitch; - cd->canvasBpp = ctx->display.format.bytesPerPixel; - - // Load changes canvas dimensions (canvasW/canvasH drive the min-size - // calc), so a full relayout is needed, not just a repaint. - wgtInvalidate(w); - return 0; +int32_t wgtCanvasLoadData(WidgetT *w, const uint8_t *data, int32_t len, const char *name) { + return data ? canvasLoadImage(w, name ? name : "", data, len) : -1; } @@ -836,6 +865,8 @@ void wgtCanvasResize(WidgetT *w, int32_t newW, int32_t newH) { cd->canvasH = newH; cd->canvasPitch = pitch; cd->canvasBpp = ctx->display.format.bytesPerPixel; + + cd->picturePath[0] = '\0'; wgtInvalidate(w); } @@ -929,7 +960,7 @@ void wgtCanvasSetPixel(WidgetT *w, int32_t x, int32_t y, uint32_t color) { return; } - canvasPutPixel(cd->pixelData + y * cd->canvasPitch + x * cd->canvasBpp, color, cd->canvasBpp); + pixelPut(cd->pixelData + y * cd->canvasPitch + x * cd->canvasBpp, color, cd->canvasBpp); // Guard the invalidate so a per-pixel plotting burst (BASIC SetPixel loop) // walks the ancestor chain only once per frame instead of once per pixel. @@ -1081,7 +1112,8 @@ static const CanvasApiT sApi = { .setBorderStyle = wgtCanvasSetBorderStyle, .getBorderStyle = wgtCanvasGetBorderStyle, .getDisplay = wgtCanvasGetDisplay, - .drawTextScaled = wgtCanvasDrawTextScaled + .drawTextScaled = wgtCanvasDrawTextScaled, + .loadData = wgtCanvasLoadData }; // BorderStyle value names, indexed by CANVAS_BORDER_NONE / _FIXED (VB's @@ -1101,13 +1133,15 @@ static const WgtMethodDescT sMethods[] = { { "Save", WGT_SIG_RET_INT_STR, (void *)wgtCanvasSave }, { "SetPenColor", WGT_SIG_INT, (void *)basSetPenColor }, { "SetPenSize", WGT_SIG_INT, (void *)wgtCanvasSetPenSize }, + { WGT_METHOD_SET_PICTURE_DATA, WGT_SIG_DYNAMIC, (void *)basSetPictureData }, { "SetPixel", WGT_SIG_INT3, (void *)basSetPixel }, }; static const WgtPropDescT sProps[] = { - { "BorderStyle", WGT_IFACE_ENUM, (void *)wgtCanvasGetBorderStyle, (void *)wgtCanvasSetBorderStyle, sBorderStyleNames }, - { "ImageHeight", WGT_IFACE_INT, (void *)basGetImageHeight, NULL, NULL }, - { "ImageWidth", WGT_IFACE_INT, (void *)basGetImageWidth, NULL, NULL }, + { "BorderStyle", WGT_IFACE_ENUM, (void *)wgtCanvasGetBorderStyle, (void *)wgtCanvasSetBorderStyle, sBorderStyleNames }, + { "ImageHeight", WGT_IFACE_INT, (void *)basGetImageHeight, NULL, NULL }, + { "ImageWidth", WGT_IFACE_INT, (void *)basGetImageWidth, NULL, NULL }, + { "Picture", WGT_IFACE_STRING, (void *)basGetPicture, (void *)basSetPicture, NULL }, }; static const WgtIfaceT sIface = { diff --git a/src/widgets/kpunch/checkbox/checkbox.h b/src/widgets/kpunch/checkbox/checkbox.h index e8bb80b4..6672e49d 100644 --- a/src/widgets/kpunch/checkbox/checkbox.h +++ b/src/widgets/kpunch/checkbox/checkbox.h @@ -39,7 +39,9 @@ typedef struct { static inline const CheckboxApiT *dvxCheckboxApi(void) { static const CheckboxApiT *sApi; - if (!sApi) { sApi = (const CheckboxApiT *)wgtGetApi("checkbox", sizeof(CheckboxApiT)); } + if (!sApi) { + sApi = (const CheckboxApiT *)wgtGetApi("checkbox", sizeof(CheckboxApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/checkbox/checkbox.res b/src/widgets/kpunch/checkbox/checkbox.res index 5f84f9dc..d4bd18dd 100644 --- a/src/widgets/kpunch/checkbox/checkbox.res +++ b/src/widgets/kpunch/checkbox/checkbox.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon checkbox.bmp name text "CheckBox" author text "Scott Duensing" diff --git a/src/widgets/kpunch/comboBox/comboBox.dhs b/src/widgets/kpunch/comboBox/comboBox.dhs index 98053e36..0c0130d2 100644 --- a/src/widgets/kpunch/comboBox/comboBox.dhs +++ b/src/widgets/kpunch/comboBox/comboBox.dhs @@ -114,6 +114,7 @@ Editable text is accessible via the generic "Text" property. "ListCount" is avai List(index%) Return the text of the item at the given index. ListCount() Return the total number of items. RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection (the text is kept). + SetItems items$ Replace all the items at once from a "|"-separated list ("Red|Green|Blue"; every "|" separates two items, "" empties the list). Nothing is selected afterwards. The text is kept. .endtable .h3 Events diff --git a/src/widgets/kpunch/comboBox/comboBox.h b/src/widgets/kpunch/comboBox/comboBox.h index 823d439f..96f29d9b 100644 --- a/src/widgets/kpunch/comboBox/comboBox.h +++ b/src/widgets/kpunch/comboBox/comboBox.h @@ -47,7 +47,9 @@ typedef struct { static inline const ComboBoxApiT *dvxComboBoxApi(void) { static const ComboBoxApiT *sApi; - if (!sApi) { sApi = (const ComboBoxApiT *)wgtGetApi("combobox", sizeof(ComboBoxApiT)); } + if (!sApi) { + sApi = (const ComboBoxApiT *)wgtGetApi("combobox", sizeof(ComboBoxApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/comboBox/combobox.bhs b/src/widgets/kpunch/comboBox/combobox.bhs index 1671b877..07c111d3 100644 --- a/src/widgets/kpunch/comboBox/combobox.bhs +++ b/src/widgets/kpunch/comboBox/combobox.bhs @@ -29,7 +29,7 @@ VB Equivalent: ComboBox -- DVX Widget: combobox (editable text field + drop-down list, default maxLen 256) -A combination of a single-line text input and a drop-down list. The user can either type a value or pick one from the list. Selecting a list item copies its text into the edit field. Supports the same AddItem / RemoveItem / Clear / List methods as ListBox. +A combination of a single-line text input and a drop-down list. The user can either type a value or pick one from the list. Selecting a list item copies its text into the edit field. Supports the same AddItem / RemoveItem / Clear / SetItems / List methods as ListBox. Its font must be fixed-pitch, every character the same width; the designer's Font dialog offers only those families for it. Given a proportional font it draws in the system font instead. @@ -53,6 +53,7 @@ Its font must be fixed-pitch, every character the same width; the designer's Fon List(index%) Return the text of the item at the given index. ListCount() Return the number of items in the list. RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection (the text is kept). + SetItems items$ Replace all the items at once from a "|"-separated list ("Red|Green|Blue"; every "|" separates two items, "" empties the list). Nothing is selected afterwards. The text is kept. .endtable Default Event: Click diff --git a/src/widgets/kpunch/comboBox/combobox.res b/src/widgets/kpunch/comboBox/combobox.res index 1609e546..211017ba 100644 --- a/src/widgets/kpunch/comboBox/combobox.res +++ b/src/widgets/kpunch/comboBox/combobox.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon combobox.bmp name text "ComboBox" author text "Scott Duensing" diff --git a/src/widgets/kpunch/comboBox/widgetComboBox.c b/src/widgets/kpunch/comboBox/widgetComboBox.c index cddb9c48..12034b8b 100644 --- a/src/widgets/kpunch/comboBox/widgetComboBox.c +++ b/src/widgets/kpunch/comboBox/widgetComboBox.c @@ -68,6 +68,7 @@ typedef struct { // Prototypes // ============================================================ +static void basSetItems(WidgetT *w, const char *spec); static int32_t comboBoxFieldW(const WidgetT *w); static int32_t comboBoxVisibleChars(const WidgetT *w, const BitmapFontT *font); WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen); @@ -81,7 +82,6 @@ void wgtComboBoxRemoveItem(WidgetT *w, int32_t idx); void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count); void wgtComboBoxSetMaxLength(WidgetT *w, int32_t maxLen); void wgtComboBoxSetSelected(WidgetT *w, int32_t idx); -void wgtRegister(void); void widgetComboBoxAccelActivate(WidgetT *w, WidgetT *root); void widgetComboBoxCalcMinSize(WidgetT *w, const BitmapFontT *font); static bool widgetComboBoxClearSelection(WidgetT *w); @@ -95,6 +95,16 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int void widgetComboBoxPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetComboBoxPaintPopup(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetComboBoxSetText(WidgetT *w, const char *text); +void wgtRegister(void); + + +// BASIC wrapper: SetItems "Red|Green|Blue" replaces the items +// (listPopupSetItemsSpec); nothing is selected and the text stays. +static void basSetItems(WidgetT *w, const char *spec) { + VALIDATE_WIDGET_VOID(w, sTypeId); + + listPopupSetItemsSpec(w, &((ComboBoxDataT *)w->data)->popup, spec); +} // Width of the edit field handed to the text engine: the text area inside @@ -454,6 +464,7 @@ static const WgtMethodDescT sMethods[] = { { "List", WGT_SIG_RET_STR_INT, (void *)wgtComboBoxGetItem }, { "ListCount", WGT_SIG_RET_INT, (void *)wgtComboBoxGetItemCount }, { "RemoveItem", WGT_SIG_INT, (void *)wgtComboBoxRemoveItem }, + { "SetItems", WGT_SIG_STR, (void *)basSetItems }, }; static const WgtIfaceT sIface = { diff --git a/src/widgets/kpunch/dataCtrl/dataCtrl.dhs b/src/widgets/kpunch/dataCtrl/dataCtrl.dhs index 0a53b297..7b2a780c 100644 --- a/src/widgets/kpunch/dataCtrl/dataCtrl.dhs +++ b/src/widgets/kpunch/dataCtrl/dataCtrl.dhs @@ -32,9 +32,9 @@ .h2 DataCtrl -A VB3-style Data control for database binding. Displays a visible navigation bar (|< < > >|) that connects to a SQLite database via dvxSql* functions. Reads all rows from the RecordSource query into an in-memory cache for bidirectional navigation. Fires a Reposition event each time the current row changes so bound controls can update. Supports master-detail linking between Data controls (via MasterSource / MasterField / DetailField), INSERT via AddNew, and DELETE via Delete. UPDATE and DELETE identify the row by the KeyColumn value it had when it was loaded, so editing the key column changes the key instead of writing to another row; an UPDATE writes only the changed columns and fails (the row stays unsaved) when no database row matches. A key the quoted text match cannot find (an integer in a column without a declared type, a REAL, or NULL) is matched by its text form instead, as long as that names exactly one row. Setting a field to the value it already has is not an edit. With a read-only source, pending edits are dropped when the cursor moves. Refresh saves a pending edit first; if that save fails or is cancelled the edit is discarded. With a SELECT RecordSource the control is read-only: Update and Delete do nothing, and a master-detail filter is applied to the query as a subquery. +A VB3-style Data control for database binding. Displays a visible navigation bar (|< < > >|) that connects to a SQLite database via dvxSql* functions. Reads the RecordSource query's rows into an in-memory cache for bidirectional navigation: a Refresh reads the first 64, and the widget's polls read the rest, about 20 ms a frame, so a big record set does not hold DVX up. Fires a Reposition event each time the current row changes so bound controls can update. Supports master-detail linking between Data controls (via MasterSource / MasterField / DetailField), INSERT via AddNew, and DELETE via Delete. UPDATE and DELETE identify the row by the KeyColumn value it had when it was loaded, so editing the key column changes the key instead of writing to another row; an UPDATE writes only the changed columns and fails (the row stays unsaved) when no database row matches. A key the quoted text match cannot find (an integer in a column without a declared type, a REAL, or NULL) is matched by its text form instead, as long as that names exactly one row. Setting a field to the value it already has is not an edit. With a read-only source, pending edits are dropped when the cursor moves. Refresh saves a pending edit first; if that save fails or is cancelled the edit is discarded. With a SELECT RecordSource the control is read-only: Update and Delete do nothing, and a master-detail filter is applied to the query as a subquery. -The widget depends on the "dvxsql" library DXE; the navigation logic caches the full result set so there is no round-trip for MoveNext/MovePrev. +The widget depends on the "dvxsql" library DXE; the navigation logic caches the result set so there is no round-trip for MoveNext/MovePrev. Until every row is read, the query stays open on a connection of its own, which holds the database's read lock: a write to that database from elsewhere (another program, or SQLExec) fails with "database is locked" until then. What needs the rows does not wait for the polls: MoveNext past the rows read and dataCtrlSetCurrentRow beyond them read on as far as they need, and MoveLast, AddNew, Update, and Delete read the rest first (the control's own writes would be locked out by its query too). The row count grows as rows arrive; a copy that runs out of memory part way frees the cache whole, as a failed Refresh does, and Reposition then fires so bound controls clear. Header: widgets/dataCtrl.h @@ -65,20 +65,20 @@ Set properties (DatabaseName, RecordSource, KeyColumn, etc.) and then call dataC void dataCtrlMoveFirst(w) Move the cursor to the first row. void dataCtrlMovePrev(w) Move the cursor to the previous row. void dataCtrlMoveNext(w) Move the cursor to the next row. - void dataCtrlMoveLast(w) Move the cursor to the last row. + void dataCtrlMoveLast(w) Move the cursor to the last row (the record set is read to its end first). const char *dataCtrlGetField(w, colName) Get the value of a column in the current row (case-insensitive). void dataCtrlSetField(w, colName, value) Set the value of a column in the current row. Marks the row dirty unless the value is unchanged. void dataCtrlUpdate(w) Flush pending changes (INSERT or UPDATE) to the database. void dataCtrlAddNew(w) Append a blank row and move the cursor to it. The row is dirty and new until Update is called. void dataCtrlDelete(w) Delete the current row from the cache and the database. void dataCtrlSetMasterValue(w, val) Set the master-detail filter value. Normally managed by the form runtime. - int32_t dataCtrlGetRowCount(w) Get the total number of cached rows. + int32_t dataCtrlGetRowCount(w) Get the number of rows read so far (all of them once the polls have read the rest, or after MoveLast). int32_t dataCtrlGetColCount(w) Get the number of columns in the result set. const char *dataCtrlGetColName(w, col) Get the name of a column by index. const char *dataCtrlGetCellText(w, row, col) Get the text of a specific cell. void dataCtrlSetCurrentRow(w, row) Set the current row by index. Auto-saves the previous row if dirty. int32_t dataCtrlGetCurrentRow(w) Get the current row index (-1 if there are no rows). - uint32_t dataCtrlGetGeneration(w) A counter that changes whenever the cached rows or cell values change (Refresh, AddNew, Delete, SetField) but not on cursor moves. Bound views use it to rebuild derived state only when needed. + uint32_t dataCtrlGetGeneration(w) A counter that changes whenever the cached rows or cell values change (Refresh, AddNew, Delete, SetField) but not on cursor moves, nor when more rows of the record set arrive (only the row count grows then). Bound views use it to rebuild derived state only when needed. .endtable .h3 API Struct (wgtRegisterApi "data") @@ -121,8 +121,8 @@ Set properties (DatabaseName, RecordSource, KeyColumn, etc.) and then call dataC MasterField String Read/Write Column in the master control to read for the filter value. DetailField String Read/Write Column in this table to filter by the master value. Caption String Read/Write Text displayed on the navigator bar. - BOF Boolean Read-only True when the cursor is at the first row (or no rows). - EOF Boolean Read-only True when the cursor is past the last row (or no rows). + BOF Boolean Read-only True after a MovePrevious at the first row (the cursor stays there), or when there are no rows. A move onto a row clears it. + EOF Boolean Read-only True after a MoveNext at the last row (the cursor stays there), or when there are no rows. A move onto a row clears it. .endtable .h3 Methods (BASIC Interface) @@ -133,7 +133,7 @@ Set properties (DatabaseName, RecordSource, KeyColumn, etc.) and then call dataC AddNew Append a blank row and move to it. Delete Delete the current row. MoveFirst Move the cursor to the first row. - MoveLast Move the cursor to the last row. + MoveLast Move the cursor to the last row (all rows are read first). MoveNext Move the cursor to the next row. MovePrevious Move the cursor to the previous row. Refresh Save a pending edit, then re-execute the query and rebuild the cache. diff --git a/src/widgets/kpunch/dataCtrl/datactr.res b/src/widgets/kpunch/dataCtrl/datactr.res index f89efe80..1ad22b1a 100644 --- a/src/widgets/kpunch/dataCtrl/datactr.res +++ b/src/widgets/kpunch/dataCtrl/datactr.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon datactr.bmp name text "Data" author text "Scott Duensing" diff --git a/src/widgets/kpunch/dataCtrl/datactrl.bhs b/src/widgets/kpunch/dataCtrl/datactrl.bhs index 42b0acf5..94e8b48e 100644 --- a/src/widgets/kpunch/dataCtrl/datactrl.bhs +++ b/src/widgets/kpunch/dataCtrl/datactrl.bhs @@ -41,6 +41,18 @@ VB Equivalent: Data -- DVX Widget: data | Name Prefix: Data A data access control that connects to a SQLite database and provides record navigation. Other controls bind to a Data control via their DataSource and DataField properties. The Data control cache is populated by calling Refresh. Move operations and Refresh auto-save the current row if dirty; a move is refused while the save fails, and Refresh discards an edit that cannot be saved. Updates find the record by the key value it was loaded with, so the key column itself can be edited; a key stored as a number in a column without a declared type, or a missing (NULL) key, is still found as long as it identifies exactly one record. Writing a field back with the value it already has is not an edit and does not fire Validate. While a Validate handler runs, MoveFirst, MoveLast, MoveNext, MovePrevious, AddNew, Refresh, and Update on the same Data control are ignored (they would save, and validate, the same record again); Delete is allowed. A RecordSource that is a SELECT query is read-only, and edits made to its records are dropped when the current record changes. +A big record set is read a part at a time: Refresh reads the first 64 records, and the rest arrive while your program and DVX go on, so RecordCount grows until every record is in. MoveLast, AddNew, Update, and Delete read the rest first, and MoveNext and AbsolutePosition read as far as they need. Until every record is in, other programs (and SQLExec) cannot change that database: their changes fail with "database is locked". + +EOF and BOF work as in Visual Basic, so a loop like this reads every record once: + +.code +Data1.Refresh +Do Until Data1.EOF + total = total + Val(Data1.GetField("amount")) + Data1.MoveNext +Loop +.endcode + .link ctrl.databinding See Data Binding for details .h2 Type-Specific Properties @@ -52,12 +64,12 @@ A data access control that connects to a SQLite database and provides record nav RecordSource String Table name or SQL SELECT query for the recordset. KeyColumn String Primary key column name (used for UPDATE/DELETE operations). Caption String Text displayed on the navigator bar. - BOF Boolean True if the current position is the first record or there are no records (read-only). - EOF Boolean True if the current position is past the last record or there are no records (read-only). + BOF Boolean True after MovePrevious at the first record (which stays the current record), or when there are no records; moving onto a record clears it (read-only). + EOF Boolean True after MoveNext at the last record (which stays the current record), or when there are no records; moving onto a record clears it (read-only). MasterSource String Name of a master Data control (for master-detail binding). MasterField String Column in the master recordset to read for the filter value. DetailField String Column in this recordset that matches the master field. - RecordCount Long The number of records (read-only). + RecordCount Long The number of records read so far: all of them once the rest has arrived, or after MoveLast (read-only). FieldCount Integer The number of columns (read-only). AbsolutePosition Long The current record, 0 for the first (-1 when there are none). Setting it moves there (saving pending changes first, as the Move methods do) and fires Reposition; a value out of range does nothing. .endtable @@ -68,7 +80,7 @@ A data access control that connects to a SQLite database and provides record nav Method Parameters Description ------------ ---------- ------------------------------------------- MoveFirst (none) Navigate to the first record. - MoveLast (none) Navigate to the last record. + MoveLast (none) Navigate to the last record (every record is read first). MoveNext (none) Navigate to the next record. MovePrevious (none) Navigate to the previous record. AddNew (none) Append a blank record and move to it. diff --git a/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c b/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c index e6217a48..468e134c 100644 --- a/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c +++ b/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c @@ -24,10 +24,11 @@ // widgetDataCtrl.c -- VB3-style Data control for database binding // // A visible navigation bar that connects to a SQLite database via -// dvxSql* functions. Reads all rows from the RecordSource query -// into an in-memory cache for bidirectional navigation. Fires -// Reposition events when the cursor moves so bound controls can -// update. +// dvxSql* functions. Reads the RecordSource query's rows into an +// in-memory cache for bidirectional navigation: DATA_FIRST_ROWS at a +// Refresh, the rest a part at a time from the widget's polls (at once +// when a move, AddNew, or a write needs them). Fires Reposition events +// when the cursor moves so bound controls can update. // // Depends on the dvxsql library DXE (via datactrl.dep) so all // dvxSql* functions are resolved at load time. @@ -35,8 +36,10 @@ #include "dvxWgtP.h" #include "dataCtrl.h" #include "../../../libs/kpunch/sql/dvxSql.h" +#include "dvxPlat.h" #include "thirdparty/stb_ds_wrap.h" +#include #include #include #include @@ -64,6 +67,13 @@ DVX_ABI_STAMP(datactrl); #define DATA_SQL_MAX 2048 #define DATA_COND_MAX (DATA_MAX_FIELD * 4) // key name + escaped key text +// Reading the record set: the rows a Refresh reads, the rows read between +// clock checks after that, and the time a poll reads for +#define DATA_FIRST_ROWS 64 +#define DATA_FILL_ROWS 16 +#define DATA_FILL_MS 20 +#define DATA_ROWS_ALL INT32_MAX // fillTo: every row + // RecordSource values starting with this prefix are queries, not tables #define DATA_SELECT_PREFIX "SELECT " #define DATA_SELECT_LEN ((int32_t)sizeof(DATA_SELECT_PREFIX) - 1) @@ -113,6 +123,11 @@ typedef struct { int32_t colCount; int32_t currentRow; // 0-based, -1 = no rows + // The record set's rest, still being read (fillTo): its query and + // database, 0 once every row is in + int32_t fillDb; + int32_t fillRs; + // Copy of the current row as loaded, taken when a loaded row first // becomes dirty. UPDATE/DELETE locate the database row by the // ORIGINAL key value, so editing the key column cannot redirect the @@ -122,15 +137,36 @@ typedef struct { DataWatchT *watches; // live watches, innermost first uint32_t generation; // bumped whenever the cached rows or cells change - bool bof; - bool eof; + bool bof; // a MovePrevious tried to pass the first row, or there are none (VB's BOF) + bool eof; // a MoveNext tried to pass the last row, or there are none (VB's EOF) bool dirty; // current row has unsaved changes bool isNewRow; // current row was created by AddNew bool refreshing; // guards against re-entrant Refresh recursion bool validating; // Validate handler running: saving operations are refused + bool lostRows; // a read of more rows failed (cache freed): the next poll fires Reposition } DataCtrlDataT; -static int32_t sTypeId = -1; +// The database library's calls, found by name the first time a Data +// control needs them (asking loads the library): the widget imports none +// of them, so DVX does not load the library at start for its sake +typedef struct { + __typeof__(dvxSqlAffectedRows) *affectedRows; + __typeof__(dvxSqlClose) *close; + __typeof__(dvxSqlEscape) *escape; + __typeof__(dvxSqlExec) *exec; + __typeof__(dvxSqlFieldCount) *fieldCount; + __typeof__(dvxSqlFieldInt) *fieldInt; + __typeof__(dvxSqlFieldName) *fieldName; + __typeof__(dvxSqlFieldText) *fieldText; + __typeof__(dvxSqlFreeResult) *freeResult; + __typeof__(dvxSqlNext) *next; + __typeof__(dvxSqlOpen) *open; + __typeof__(dvxSqlQuery) *query; +} DataSqlT; + +static int32_t sTypeId = -1; +static DataSqlT sSql; +static bool sSqlFound = false; // ============================================================ @@ -168,6 +204,7 @@ static void dataCtrlMoveNext(WidgetT *w); static void dataCtrlMovePrev(WidgetT *w); static void dataCtrlOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); static void dataCtrlPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +static void dataCtrlPoll(WidgetT *w, WindowT *win); void dataCtrlRefresh(WidgetT *w); static void dataCtrlSetCaption(WidgetT *w, const char *val); void dataCtrlSetCurrentRow(WidgetT *w, int32_t row); @@ -183,6 +220,9 @@ void dataCtrlUpdate(WidgetT *w); static void drawNavGlyph(DisplayT *disp, const BlitOpsT *ops, int32_t tipX, int32_t cy, int32_t dir, bool bar, uint32_t color); static char **dupFields(char *const *src, int32_t count); static bool execKeyed(const DataCtrlDataT *d, int32_t db, const char *stmt); +static void fillEnd(DataCtrlDataT *d); +static bool fillRows(DataCtrlDataT *d, int32_t maxRows); +static void fillTo(DataCtrlDataT *d, int32_t row, bool timed); static int32_t findKeyCol(const DataCtrlDataT *d); static bool fireReposition(WidgetT *w); static void freeCache(DataCtrlDataT *d); @@ -192,6 +232,7 @@ static bool isSelectSource(const char *recordSource); static const char *keyValue(const DataCtrlDataT *d, int32_t keyCol); static bool loadCache(DataCtrlDataT *d); static int32_t rowsMatching(const DataCtrlDataT *d, int32_t db, const char *cond); +static bool sqlFind(void); static void watchBegin(DataCtrlDataT *d, DataWatchT *watch); static bool watchEnd(DataCtrlDataT *d, DataWatchT *watch); void wgtRegister(void); @@ -262,6 +303,9 @@ void dataCtrlAddNew(WidgetT *w) { return; } + // The new row goes after every row of the record set + fillTo(d, DATA_ROWS_ALL, false); + if (d->colCount == 0) { return; } @@ -351,6 +395,13 @@ void dataCtrlDelete(WidgetT *w) { return; } + // The record set's open query would lock the write out + fillTo(d, DATA_ROWS_ALL, false); + + if (d->currentRow < 0) { + return; + } + // Delete from database (unless it's an unsaved new row). The cached // row may only be removed below when the database row is actually // gone; dropping it while the DELETE was skipped or failed would @@ -363,11 +414,11 @@ void dataCtrlDelete(WidgetT *w) { char sql[DATA_SQL_MAX]; - if (dvxStrAppendf(sql, sizeof(sql), 0, "DELETE FROM %s", d->recordSource) < 0) { + if (!sqlFind() || dvxStrAppendf(sql, sizeof(sql), 0, "DELETE FROM %s", d->recordSource) < 0) { return; } - int32_t db = dvxSqlOpen(d->databaseName); + int32_t db = sSql.open(d->databaseName); if (db <= 0) { return; @@ -375,7 +426,7 @@ void dataCtrlDelete(WidgetT *w) { bool ok = execKeyed(d, db, sql); - dvxSqlClose(db); + sSql.close(db); if (!ok) { return; @@ -400,8 +451,8 @@ void dataCtrlDelete(WidgetT *w) { d->currentRow = d->rowCount - 1; } - d->bof = (d->currentRow == 0); - d->eof = (d->currentRow >= d->rowCount - 1); + d->bof = false; + d->eof = false; } fireReposition(w); @@ -518,7 +569,8 @@ const char *dataCtrlGetField(WidgetT *w, const char *colName) { // Changes whenever the cached rows or cell values change (Refresh, AddNew, -// Delete, SetField), but not on cursor moves: a bound view rebuilds its +// Delete, SetField), but not on cursor moves, nor when more of the record +// set arrives (only the row count grows then): a bound view rebuilds its // sort order and column widths only when this moved. uint32_t dataCtrlGetGeneration(const WidgetT *w) { if (!w || !w->data) { @@ -576,9 +628,12 @@ static void dataCtrlMoveFirst(WidgetT *w) { } +// The last row: the record set is read to its end first. static void dataCtrlMoveLast(WidgetT *w) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; + fillTo(d, DATA_ROWS_ALL, false); + if (d->rowCount <= 0) { return; } @@ -594,9 +649,12 @@ static void dataCtrlMoveLast(WidgetT *w) { } +// At the last row (the record set read to its end) only EOF is set. static void dataCtrlMoveNext(WidgetT *w) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; + fillTo(d, d->currentRow + 1, false); + if (d->rowCount <= 0 || d->currentRow >= d->rowCount - 1) { d->eof = true; return; @@ -608,7 +666,7 @@ static void dataCtrlMoveNext(WidgetT *w) { d->currentRow++; d->bof = false; - d->eof = (d->currentRow >= d->rowCount - 1); + d->eof = false; fireReposition(w); } @@ -626,7 +684,7 @@ static void dataCtrlMovePrev(WidgetT *w) { } d->currentRow--; - d->bof = (d->currentRow == 0); + d->bof = false; d->eof = false; fireReposition(w); } @@ -715,6 +773,21 @@ static void dataCtrlPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const // dataCtrlRefresh -- execute query and cache all rows +// WGT_METHOD_POLL: more of the record set, DATA_FILL_MS a poll; after a +// failed read (the cache freed), Reposition so bound controls clear. +static void dataCtrlPoll(WidgetT *w, WindowT *win) { + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + + (void)win; + fillTo(d, DATA_ROWS_ALL, true); + + if (d->lostRows) { + d->lostRows = false; + fireReposition(w); + } +} + + void dataCtrlRefresh(WidgetT *w) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; @@ -776,7 +849,13 @@ void dataCtrlSetCurrentRow(WidgetT *w, int32_t row) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; - if (row < 0 || row >= d->rowCount) { + if (row < 0) { + return; + } + + fillTo(d, row, false); + + if (row >= d->rowCount) { return; } @@ -786,8 +865,8 @@ void dataCtrlSetCurrentRow(WidgetT *w, int32_t row) { } d->currentRow = row; - d->bof = (row == 0); - d->eof = (row >= d->rowCount - 1); + d->bof = false; + d->eof = false; fireReposition(w); } @@ -904,7 +983,14 @@ void dataCtrlUpdate(WidgetT *w) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; // An Update from the Validate handler is refused like every move - if (d->validating || !hasPendingWrite(d)) { + if (d->validating || !hasPendingWrite(d) || !sqlFind()) { + return; + } + + // The record set's open query would lock the write out + fillTo(d, DATA_ROWS_ALL, false); + + if (!hasPendingWrite(d)) { return; } @@ -951,7 +1037,7 @@ void dataCtrlUpdate(WidgetT *w) { pos = dvxStrAppendf(sql, sizeof(sql), pos, ") VALUES ("); for (int32_t i = 0; i < d->colCount; i++) { - if (dvxSqlEscape(fields[i], escaped, sizeof(escaped)) < 0) { + if (sSql.escape(fields[i], escaped, sizeof(escaped)) < 0) { return; } @@ -970,7 +1056,7 @@ void dataCtrlUpdate(WidgetT *w) { continue; } - if (dvxSqlEscape(fields[i], escaped, sizeof(escaped)) < 0) { + if (sSql.escape(fields[i], escaped, sizeof(escaped)) < 0) { return; } @@ -989,7 +1075,7 @@ void dataCtrlUpdate(WidgetT *w) { return; } - int32_t db = dvxSqlOpen(d->databaseName); + int32_t db = sSql.open(d->databaseName); if (db <= 0) { return; @@ -998,12 +1084,12 @@ void dataCtrlUpdate(WidgetT *w) { bool ok; if (d->isNewRow) { - ok = dvxSqlExec(db, sql) && dvxSqlAffectedRows(db) > 0; + ok = sSql.exec(db, sql) && sSql.affectedRows(db) > 0; } else { ok = execKeyed(d, db, sql); } - dvxSqlClose(db); + sSql.close(db); if (ok) { clearDirty(d); @@ -1064,15 +1150,15 @@ static bool execKeyed(const DataCtrlDataT *d, int32_t db, const char *stmt) { // Escape and quote the key value: an unquoted/unescaped key produces // malformed SQL for text keys and is an injection vector. - if (dvxSqlEscape(keyText, keyEsc, sizeof(keyEsc)) < 0) { + if (sSql.escape(keyText, keyEsc, sizeof(keyEsc)) < 0) { return false; } - if (dvxStrAppendf(sql, sizeof(sql), 0, "%s WHERE %s='%s'", stmt, keyName, keyEsc) < 0 || !dvxSqlExec(db, sql)) { + if (dvxStrAppendf(sql, sizeof(sql), 0, "%s WHERE %s='%s'", stmt, keyName, keyEsc) < 0 || !sSql.exec(db, sql)) { return false; } - if (dvxSqlAffectedRows(db) > 0) { + if (sSql.affectedRows(db) > 0) { return true; } @@ -1092,7 +1178,75 @@ static bool execKeyed(const DataCtrlDataT *d, int32_t db, const char *stmt) { return false; } - return dvxSqlExec(db, sql) && dvxSqlAffectedRows(db) > 0; + return sSql.exec(db, sql) && sSql.affectedRows(db) > 0; +} + + +// The record set is all in, or dropped: its query and database close +// (closing the database ends the query). +static void fillEnd(DataCtrlDataT *d) { + if (d->fillDb > 0) { + sSql.close(d->fillDb); + } + + d->fillDb = 0; + d->fillRs = 0; +} + + +// Read up to maxRows more rows of the record set into the cache (closing +// it at its end). False when a copy failed, the rows read so far kept. +static bool fillRows(DataCtrlDataT *d, int32_t maxRows) { + for (int32_t n = 0; n < maxRows; n++) { + if (!sSql.next(d->fillRs)) { + fillEnd(d); + return true; + } + + DataRowT row; + bool ok; + + row.fieldCount = d->colCount; + row.fields = (char **)calloc((size_t)d->colCount, sizeof(char *)); + ok = (row.fields != NULL); + + for (int32_t i = 0; i < d->colCount && ok; i++) { + const char *text = sSql.fieldText(d->fillRs, i); + + row.fields[i] = strdup(text ? text : ""); + ok = (row.fields[i] != NULL); + } + + if (!ok) { + freeFields(row.fields, d->colCount); + return false; + } + + arrput(d->rows, row); + d->rowCount = (int32_t)arrlen(d->rows); + // cppcheck-suppress memleak ; row.fields is stored into d->rows via arrput + } + + return true; +} + + +// Read the record set's rest until row is in (DATA_ROWS_ALL: all of it) +// or, timed, DATA_FILL_MS has passed. A copy that fails frees the cache +// whole, as a failed Refresh does (a partial cache would show a truncated +// record set as if complete), and the next poll fires Reposition: callers +// check the rows again afterwards. +static void fillTo(DataCtrlDataT *d, int32_t row, bool timed) { + PlatformTicksT since = platformClock(); + PlatformTicksT budget = PLATFORM_MS_TO_TICKS(DATA_FILL_MS); + + while (d->fillRs > 0 && d->rowCount <= row && (!timed || platformClock() - since < budget)) { + if (!fillRows(d, DATA_FILL_ROWS)) { + freeCache(d); + d->generation++; + d->lostRows = true; + } + } } @@ -1129,6 +1283,7 @@ static void freeCache(DataCtrlDataT *d) { // snapshot is sized by it): a Refresh after AddNew left isNewRow set, // so the next autoSave would re-INSERT row 0's values as a duplicate. clearDirty(d); + fillEnd(d); // Walk the arrays themselves, not rowCount/colCount: a loadCache that // fails part way has stored rows before rowCount is set. @@ -1202,11 +1357,12 @@ static const char *keyValue(const DataCtrlDataT *d, int32_t keyCol) { // Run the RecordSource query (plus the master-detail filter) and cache -// the result. Returns false, with the cache left empty, when the control +// its first DATA_FIRST_ROWS rows; the query stays open for the rest +// (fillTo). Returns false, with the cache left empty, when the control // is unconfigured or the open, escape, statement build, query or an // allocation fails. static bool loadCache(DataCtrlDataT *d) { - if (!d->databaseName[0] || !d->recordSource[0]) { + if (!d->databaseName[0] || !d->recordSource[0] || !sqlFind()) { return false; } @@ -1219,7 +1375,7 @@ static bool loadCache(DataCtrlDataT *d) { if (d->detailField[0] && d->masterValue[0]) { char escaped[DATA_MAX_FIELD * 2]; - if (dvxSqlEscape(d->masterValue, escaped, sizeof(escaped)) < 0) { + if (sSql.escape(d->masterValue, escaped, sizeof(escaped)) < 0) { return false; } @@ -1238,26 +1394,26 @@ static bool loadCache(DataCtrlDataT *d) { return false; } - int32_t db = dvxSqlOpen(d->databaseName); + int32_t db = sSql.open(d->databaseName); if (db <= 0) { return false; } - int32_t rs = dvxSqlQuery(db, query); + int32_t rs = sSql.query(db, query); if (rs <= 0) { - dvxSqlClose(db); + sSql.close(db); return false; } bool ok = true; - int32_t colCount = dvxSqlFieldCount(rs); + int32_t colCount = sSql.fieldCount(rs); // Cache column names; colCount tracks what was actually stored so a // failure part way leaves freeCache a consistent array to release. for (int32_t i = 0; i < colCount && ok; i++) { - const char *name = dvxSqlFieldName(rs, i); + const char *name = sSql.fieldName(rs, i); char *copy = strdup(name ? name : ""); ok = (copy != NULL); @@ -1265,41 +1421,15 @@ static bool loadCache(DataCtrlDataT *d) { d->colCount = (int32_t)arrlen(d->colNames); } - // Cache all rows - while (ok && dvxSqlNext(rs)) { - DataRowT row; - - row.fieldCount = d->colCount; - row.fields = (char **)calloc((size_t)d->colCount, sizeof(char *)); - ok = (row.fields != NULL); - - for (int32_t i = 0; i < d->colCount && ok; i++) { - const char *text = dvxSqlFieldText(rs, i); - - row.fields[i] = strdup(text ? text : ""); - ok = (row.fields[i] != NULL); - } - - if (!ok) { - freeFields(row.fields, d->colCount); - break; - } - - arrput(d->rows, row); - // cppcheck-suppress memleak ; row.fields is stored into d->rows via arrput - } - - dvxSqlFreeResult(rs); - dvxSqlClose(db); + d->fillDb = db; + d->fillRs = rs; // A partial cache would show a truncated record set as if complete - if (!ok) { + if (!ok || !fillRows(d, DATA_FIRST_ROWS)) { freeCache(d); return false; } - d->rowCount = (int32_t)arrlen(d->rows); - if (d->rowCount > 0) { d->currentRow = 0; d->bof = false; @@ -1318,22 +1448,47 @@ static int32_t rowsMatching(const DataCtrlDataT *d, int32_t db, const char *cond return -1; } - int32_t rs = dvxSqlQuery(db, sql); + int32_t rs = sSql.query(db, sql); int32_t count = -1; if (rs <= 0) { return -1; } - if (dvxSqlNext(rs)) { - count = dvxSqlFieldInt(rs, 0); + if (sSql.next(rs)) { + count = sSql.fieldInt(rs, 0); } - dvxSqlFreeResult(rs); + sSql.freeResult(rs); return count; } +// The database library's calls into sSql, looked up until they are all +// there; false when the library is not installed. +static bool sqlFind(void) { + if (sSqlFound) { + return true; + } + + sSql.affectedRows = (__typeof__(sSql.affectedRows))dlsym(NULL, DVX_SYM("dvxSqlAffectedRows")); + sSql.close = (__typeof__(sSql.close))dlsym(NULL, DVX_SYM("dvxSqlClose")); + sSql.escape = (__typeof__(sSql.escape))dlsym(NULL, DVX_SYM("dvxSqlEscape")); + sSql.exec = (__typeof__(sSql.exec))dlsym(NULL, DVX_SYM("dvxSqlExec")); + sSql.fieldCount = (__typeof__(sSql.fieldCount))dlsym(NULL, DVX_SYM("dvxSqlFieldCount")); + sSql.fieldInt = (__typeof__(sSql.fieldInt))dlsym(NULL, DVX_SYM("dvxSqlFieldInt")); + sSql.fieldName = (__typeof__(sSql.fieldName))dlsym(NULL, DVX_SYM("dvxSqlFieldName")); + sSql.fieldText = (__typeof__(sSql.fieldText))dlsym(NULL, DVX_SYM("dvxSqlFieldText")); + sSql.freeResult = (__typeof__(sSql.freeResult))dlsym(NULL, DVX_SYM("dvxSqlFreeResult")); + sSql.next = (__typeof__(sSql.next))dlsym(NULL, DVX_SYM("dvxSqlNext")); + sSql.open = (__typeof__(sSql.open))dlsym(NULL, DVX_SYM("dvxSqlOpen")); + sSql.query = (__typeof__(sSql.query))dlsym(NULL, DVX_SYM("dvxSqlQuery")); + + sSqlFound = sSql.affectedRows && sSql.close && sSql.escape && sSql.exec && sSql.fieldCount && sSql.fieldInt && sSql.fieldName && sSql.fieldText && sSql.freeResult && sSql.next && sSql.open && sSql.query; + return sSqlFound; +} + + // Link a watch before running a user handler (see DataWatchT) static void watchBegin(DataCtrlDataT *d, DataWatchT *watch) { watch->alive = true; @@ -1360,12 +1515,13 @@ static bool watchEnd(DataCtrlDataT *d, DataWatchT *watch) { static const WidgetClassT sClass = { .version = WGT_CLASS_VERSION, - .flags = WCLASS_FOCUSABLE, + .flags = WCLASS_FOCUSABLE | WCLASS_NEEDS_POLL, .handlers = { [WGT_METHOD_PAINT] = (void *)dataCtrlPaint, [WGT_METHOD_CALC_MIN_SIZE] = (void *)dataCtrlCalcMinSize, [WGT_METHOD_ON_MOUSE] = (void *)dataCtrlOnMouse, [WGT_METHOD_DESTROY] = (void *)dataCtrlDestroy, + [WGT_METHOD_POLL] = (void *)dataCtrlPoll, } }; diff --git a/src/widgets/kpunch/dbGrid/dbGrid.dhs b/src/widgets/kpunch/dbGrid/dbGrid.dhs index 3aa67b18..1f3ccc22 100644 --- a/src/widgets/kpunch/dbGrid/dbGrid.dhs +++ b/src/widgets/kpunch/dbGrid/dbGrid.dhs @@ -30,7 +30,7 @@ .h2 DbGrid -A database grid widget that displays all records from a Data control in a scrollable, sortable table. Columns auto-populate from the Data control's column names and can be hidden, resized (by dragging a column border), and renamed by the application. Clicking a column header sorts the display ascending/descending. The highlighted row is always the Data control's current row: selecting a row moves the Data control's cursor, and the highlight is read from the cursor whenever the grid paints. The Data control does not repaint the grid itself, so a C application that moves the cursor elsewhere (the Data control's own buttons or API) calls dbGridRefresh (or wgtInvalidatePaint on the grid) from the Data control's onChange; the form runtime does this for BASIC programs. The grid reads directly from the Data control's cached rows, so there is no separate copy of the data. Re-binding the same Data control (as the form runtime does on every Reposition) keeps the sort order and column settings while the column set is unchanged (the sort order and automatic widths are rebuilt when the Data control's rows or values changed), and a width set by dragging a column border is kept like one set with dbGridSetColumnWidth. Cell and header text is clipped to its column. +A database grid widget that displays all records from a Data control in a scrollable, sortable table. Columns auto-populate from the Data control's column names and can be hidden, resized (by dragging a column border), and renamed by the application. Clicking a column header sorts the display ascending/descending. The highlighted row is always the Data control's current row: selecting a row moves the Data control's cursor, and the highlight is read from the cursor whenever the grid paints. The grid's polls follow the Data control's rows: a change to them, and the rows of a record set that arrive a part at a time (sorted in as they come, in the order a full sort gives), are shown without a call. A cursor move is not: a C application that moves the cursor elsewhere (the Data control's own buttons or API) calls dbGridRefresh (or wgtInvalidatePaint on the grid) from the Data control's onChange; the form runtime does this for BASIC programs. The grid reads directly from the Data control's cached rows, so there is no separate copy of the data. Re-binding the same Data control (as the form runtime does on every Reposition) keeps the sort order and column settings while the column set is unchanged (the sort order and automatic widths are rebuilt when the Data control's rows or values changed), and a width set by dragging a column border is kept like one set with dbGridSetColumnWidth. An automatic column width fits the header and the first 200 rows. Cell and header text is clipped to its column. The grid supports alternating row shading, vertical and horizontal scrollbars, keyboard navigation (arrows, Page Up/Down, Home, End), and double-click activation (fires onDblClick). The mouse wheel scrolls the view without moving the record cursor. diff --git a/src/widgets/kpunch/dbGrid/dbgrid.bhs b/src/widgets/kpunch/dbGrid/dbgrid.bhs index 0d6d74fc..a5b06a4b 100644 --- a/src/widgets/kpunch/dbGrid/dbgrid.bhs +++ b/src/widgets/kpunch/dbGrid/dbgrid.bhs @@ -30,7 +30,7 @@ VB Equivalent: DBGrid -- DVX Widget: dbgrid | Name Prefix: DBGrid -A data-bound grid that displays records from a Data control in a tabular format. Columns auto-populate from the query results. Bind the grid using the DataSource property. Click a column header to sort; drag a column border to resize. The highlighted row is always the bound Data control's current record: selecting a row moves the Data control's cursor, and navigating the Data control moves the highlight. Re-binding keeps the sort order and column settings, and a column width set by dragging a border is kept. Text wider than its column is clipped. The mouse wheel scrolls the rows without changing the current record. +A data-bound grid that displays records from a Data control in a tabular format. Columns auto-populate from the query results. Bind the grid using the DataSource property. Click a column header to sort; drag a column border to resize. The highlighted row is always the bound Data control's current record: selecting a row moves the Data control's cursor, and navigating the Data control moves the highlight. Re-binding keeps the sort order and column settings, and a column width set by dragging a border is kept. A column's automatic width fits its header and its first 200 records; text wider than its column is clipped. Records that arrive later (a Data control reads a big record set a part at a time) appear as they come, in the grid's sort order. The mouse wheel scrolls the rows without changing the current record. .h2 Type-Specific Properties @@ -40,6 +40,7 @@ A data-bound grid that displays records from a Data control in a tabular format. DataSource String Name of the Data control that supplies records. GridLines Boolean Show or hide grid lines between cells (default: True). SelectedRow Integer Index of the current record's row (-1 = no records). Setting it moves the Data control's cursor; negative values are ignored. + Text String The current record's first visible column ("" with no record), read-only. It is the grid's default property, so Print DBGrid1 prints it. For another field, read the Data control's recordset or the field's bound control. .endtable .h2 Type-Specific Methods diff --git a/src/widgets/kpunch/dbGrid/dbgrid.res b/src/widgets/kpunch/dbGrid/dbgrid.res index ca1f5540..bc262168 100644 --- a/src/widgets/kpunch/dbGrid/dbgrid.res +++ b/src/widgets/kpunch/dbGrid/dbgrid.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon dbgrid.bmp name text "DBGrid" author text "Scott Duensing" diff --git a/src/widgets/kpunch/dbGrid/widgetDbGrid.c b/src/widgets/kpunch/dbGrid/widgetDbGrid.c index b45cd390..2d3b8888 100644 --- a/src/widgets/kpunch/dbGrid/widgetDbGrid.c +++ b/src/widgets/kpunch/dbGrid/widgetDbGrid.c @@ -33,7 +33,9 @@ // The grid reads directly from the Data control's cached rows via // the row-level accessor API (getRowCount, getCellText, etc.) so // there is no separate copy of the data. The grid is refreshed by -// calling dbGridRefresh() which re-reads the Data control's state. +// calling dbGridRefresh() which re-reads the Data control's state, and +// its polls follow the Data control on their own: the rows of a record +// set that arrives a part at a time are sorted in as they come. // // Depends on the datactrl widget DXE for the DataCtrlApiT. @@ -64,6 +66,7 @@ DVX_ABI_STAMP(dbgrid); #define DBGRID_MAX_NAME 64 #define DBGRID_RESIZE_ZONE 3 #define DBGRID_HSTEP 20 // horizontal scroll step (px) for arrow clicks +#define DBGRID_WIDTH_ROWS 200 // auto column widths: the rows measured (the first ones) // Vertical padding added to font->charHeight for header and rows. #define DBGRID_HEADER_VPAD 4 @@ -125,6 +128,7 @@ typedef struct { int32_t *sortIndex; // display-row -> data-row mapping (NULL = natural) int32_t sortCount; // length of sortIndex uint32_t dataGen; // Data generation the widths and sort were built for + int32_t rowsSeen; // Data rows they were brought up to (more arrive as it reads) // Column resize drag state int32_t resizeCol; @@ -173,6 +177,7 @@ static void dbGridDestroy(WidgetT *w); static int32_t dbGridGetCursorShape(const WidgetT *w, int32_t mx, int32_t my); static bool dbGridGetGridLines(const WidgetT *w); int32_t dbGridGetSelectedRow(const WidgetT *w); +static const char *dbGridGetText(const WidgetT *w); static void dbGridOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y); static void dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y); static void dbGridOnKey(WidgetT *w, int32_t key, int32_t mod); @@ -180,6 +185,7 @@ static void dbGridOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t static bool dbGridOnWheel(WidgetT *w, int32_t delta); static void dbGridOnWidgetDestroyed(WidgetT *dead); static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +static void dbGridPoll(WidgetT *w, WindowT *win); void dbGridRefresh(WidgetT *w); void dbGridSetColumnHeader(WidgetT *w, const char *fieldName, const char *header); void dbGridSetColumnVisible(WidgetT *w, const char *fieldName, bool visible); @@ -188,6 +194,7 @@ void dbGridSetDataWidget(WidgetT *w, WidgetT *dataWidget); static void dbGridSetGridLines(WidgetT *w, bool val); void dbGridSetSelectedRow(WidgetT *w, int32_t row); static int dbGridSortCmp(const void *a, const void *b); +static void dbGridSortMerge(WidgetT *w, int32_t from, int32_t rowCount); static bool dbGridSyncData(WidgetT *w); static int32_t displayToDataRow(const DbGridDataT *d, int32_t dispRow); static const char *getCellText(const DbGridDataT *d, int32_t dispRow, int32_t visCol); @@ -454,6 +461,23 @@ int32_t dbGridGetSelectedRow(const WidgetT *w) { } +// Text (BASIC's default property): the current record's first visible +// column ("" with no record or no visible column). The grid has no +// current column, so this stands for VB's current cell; read-only. +static const char *dbGridGetText(const WidgetT *w) { + const DbGridDataT *d = (const DbGridDataT *)w->data; + int32_t row = currentDataRow(d); + + for (int32_t c = 0; d->dataWidget && row >= 0 && c < d->colCount; c++) { + if (d->columns[c].visible) { + return wgtDataCtrlGetCellText(d->dataWidget, row, c); + } + } + + return ""; +} + + // A finished column-resize drag stores the width as the column's explicit // width, so later layout recalculations (refresh, rebind, column config) // keep it instead of reverting to the auto width. @@ -1002,6 +1026,19 @@ static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const B } +// WGT_METHOD_POLL: the Data control's changes, and the rows it has read +// since, are shown. +static void dbGridPoll(WidgetT *w, WindowT *win) { + DbGridDataT *d = (DbGridDataT *)w->data; + + (void)win; + + if (d->dataWidget && (getDataRowCount(d) != d->rowsSeen || wgtDataCtrlGetGeneration(d->dataWidget) != d->dataGen)) { + dbGridRefresh(w); + } +} + + // Refresh the grid display (e.g. after Data control refresh). The sort // order and auto column widths are rebuilt only when the Data control's // cells changed; a mere cursor move (the form runtime refreshes on every @@ -1152,6 +1189,7 @@ void dbGridSetDataWidget(WidgetT *w, WidgetT *dataWidget) { d->sortCol = -1; d->sortDir = ListViewSortNoneE; d->dataGen = wgtDataCtrlGetGeneration(dataWidget); + d->rowsSeen = getDataRowCount(d); wgtInvalidate(w); } @@ -1207,25 +1245,109 @@ static int dbGridSortCmp(const void *a, const void *b) { } -// Drop what is derived from the Data control's cells (auto column widths, -// sort order) when its generation moved since they were built; cursor -// moves leave them. Returns true when something was rebuilt. -static bool dbGridSyncData(WidgetT *w) { - DbGridDataT *d = (DbGridDataT *)w->data; - uint32_t gen = wgtDataCtrlGetGeneration(d->dataWidget); +// Sort rows from..rowCount-1 (read by the Data control since the index was +// built over rows 0..from-1) into the sort index: the new rows sorted on +// their own, then merged with the old order in one pass, which gives the +// order sorting them all would. +static void dbGridSortMerge(WidgetT *w, int32_t from, int32_t rowCount) { + DbGridDataT *d = (DbGridDataT *)w->data; - if (gen == d->dataGen) { + if (!d->sortIndex || d->sortCount != from) { + dbGridBuildSortIndex(w); + return; + } + + int32_t added = rowCount - from; + int32_t sign = (d->sortDir == ListViewSortDescE) ? -1 : 1; + DbGridSortKeyT *keys = (DbGridSortKeyT *)malloc((size_t)added * sizeof(DbGridSortKeyT)); + int32_t *index = (int32_t *)malloc((size_t)rowCount * sizeof(int32_t)); + + if (!keys || !index) { + free(keys); + free(index); + dbGridBuildSortIndex(w); + return; + } + + for (int32_t i = 0; i < added; i++) { + keys[i].text = wgtDataCtrlGetCellText(d->dataWidget, from + i, d->sortCol); + keys[i].row = from + i; + keys[i].sign = sign; + } + + qsort(keys, (size_t)added, sizeof(DbGridSortKeyT), dbGridSortCmp); + + int32_t oldAt = 0; + int32_t newAt = 0; + int32_t out = 0; + + while (oldAt < from && newAt < added) { + DbGridSortKeyT old; + + old.text = wgtDataCtrlGetCellText(d->dataWidget, d->sortIndex[oldAt], d->sortCol); + old.row = d->sortIndex[oldAt]; + old.sign = sign; + + if (dbGridSortCmp(&old, &keys[newAt]) <= 0) { + index[out++] = d->sortIndex[oldAt++]; + } else { + index[out++] = keys[newAt++].row; + } + } + + while (oldAt < from) { + index[out++] = d->sortIndex[oldAt++]; + } + + while (newAt < added) { + index[out++] = keys[newAt++].row; + } + + free(d->sortIndex); + free(keys); + d->sortIndex = index; + d->sortCount = rowCount; +} + + +// Bring what is derived from the Data control's cells (auto column +// widths, sort order) up to date: rebuilt when its generation moved since +// they were built; rows that arrived since (it reads its record set a +// part at a time) are sorted in, and measured while they are among the +// first DBGRID_WIDTH_ROWS. Cursor moves leave them. Returns true when +// they were rebuilt. +static bool dbGridSyncData(WidgetT *w) { + DbGridDataT *d = (DbGridDataT *)w->data; + uint32_t gen = wgtDataCtrlGetGeneration(d->dataWidget); + int32_t rows = getDataRowCount(d); + int32_t seen = d->rowsSeen; + + if (gen == d->dataGen && rows == seen) { return false; } - d->dataGen = gen; - d->totalColW = 0; + d->rowsSeen = rows; - if (d->sortDir != ListViewSortNoneE) { - dbGridBuildSortIndex(w); + if (gen != d->dataGen || rows < seen) { + d->dataGen = gen; + d->totalColW = 0; + + if (d->sortDir != ListViewSortNoneE) { + dbGridBuildSortIndex(w); + } + + return true; } - return true; + if (seen < DBGRID_WIDTH_ROWS) { + d->totalColW = 0; + } + + if (d->sortDir != ListViewSortNoneE) { + dbGridSortMerge(w, seen, rows); + } + + return false; } @@ -1259,7 +1381,8 @@ static int32_t getDataRowCount(const DbGridDataT *d) { // resolveColumns -- compute pixel widths for each column (again after a -// data or column change, or in another font) +// data or column change, or in another font). An auto width fits the +// header and the first DBGRID_WIDTH_ROWS rows. static void resolveColumns(const WidgetT *w, const BitmapFontT *font) { DbGridDataT *d = (DbGridDataT *)w->data; @@ -1267,7 +1390,7 @@ static void resolveColumns(const WidgetT *w, const BitmapFontT *font) { return; } - int32_t rowCount = getDataRowCount(d); + int32_t rowCount = DVX_MIN(getDataRowCount(d), DBGRID_WIDTH_ROWS); d->totalColW = 0; @@ -1332,7 +1455,7 @@ static const WidgetClassT sClass = { // Not WCLASS_SCROLLABLE: that turns wheel notches into Up/Down keys, // which here would move the record cursor (autosave + Reposition). // dbGridOnWheel scrolls the view instead. - .flags = WCLASS_FOCUSABLE | WCLASS_NO_HIT_RECURSE, + .flags = WCLASS_FOCUSABLE | WCLASS_NO_HIT_RECURSE | WCLASS_NEEDS_POLL, .handlers = { [WGT_METHOD_PAINT] = (void *)dbGridPaint, [WGT_METHOD_CALC_MIN_SIZE] = (void *)dbGridCalcMinSize, @@ -1340,9 +1463,11 @@ static const WidgetClassT sClass = { [WGT_METHOD_ON_KEY] = (void *)dbGridOnKey, [WGT_METHOD_DESTROY] = (void *)dbGridDestroy, [WGT_METHOD_GET_CURSOR_SHAPE] = (void *)dbGridGetCursorShape, + [WGT_METHOD_GET_TEXT] = (void *)dbGridGetText, [WGT_METHOD_ON_DRAG_UPDATE] = (void *)dbGridOnDragUpdate, [WGT_METHOD_ON_DRAG_END] = (void *)dbGridOnDragEnd, [WGT_METHOD_ON_WHEEL] = (void *)dbGridOnWheel, + [WGT_METHOD_POLL] = (void *)dbGridPoll, } }; diff --git a/src/widgets/kpunch/dropdown/dropdown.bhs b/src/widgets/kpunch/dropdown/dropdown.bhs index c9593bc1..7d80190e 100644 --- a/src/widgets/kpunch/dropdown/dropdown.bhs +++ b/src/widgets/kpunch/dropdown/dropdown.bhs @@ -29,7 +29,7 @@ DVX Extension -- DVX Widget: dropdown (non-editable drop-down list) -A read-only drop-down list. Unlike ComboBox, the user cannot type free text; they can only select from the provided items. Supports AddItem / RemoveItem / Clear / List. Use the Up and Down keys to change the choice. F4, Alt+Down, Space, or Enter opens the list; in the open list use the arrow keys to move and Enter (or F4, Alt+Down) to pick. Esc closes the list without changing the choice. +A read-only drop-down list. Unlike ComboBox, the user cannot type free text; they can only select from the provided items. Supports AddItem / RemoveItem / Clear / SetItems / List. Use the Up and Down keys to change the choice. F4, Alt+Down, Space, or Enter opens the list; in the open list use the arrow keys to move and Enter (or F4, Alt+Down) to pick. Esc closes the list without changing the choice. .h2 Type-Specific Properties @@ -49,6 +49,7 @@ A read-only drop-down list. Unlike ComboBox, the user cannot type free text; the List(index%) Return the text of the item at the given index. ListCount() Return the number of items in the list. RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection. + SetItems items$ Replace all the items at once from a "|"-separated list ("Red|Green|Blue"; every "|" separates two items, "" empties the list). Nothing is selected afterwards. .endtable Default Event: Change (fires when the user picks a new item; Click fires only when the arrow opens the popup) diff --git a/src/widgets/kpunch/dropdown/dropdown.dhs b/src/widgets/kpunch/dropdown/dropdown.dhs index d26697e2..b6b6d26c 100644 --- a/src/widgets/kpunch/dropdown/dropdown.dhs +++ b/src/widgets/kpunch/dropdown/dropdown.dhs @@ -101,6 +101,7 @@ wgtDropdownAddItem(dd, "Blue"); List(index%) Return the text of the item at the given index. ListCount() Return the total number of items. RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection. + SetItems items$ Replace all the items at once from a "|"-separated list ("Red|Green|Blue"; every "|" separates two items, "" empties the list). Nothing is selected afterwards. .endtable .h3 Events diff --git a/src/widgets/kpunch/dropdown/dropdown.h b/src/widgets/kpunch/dropdown/dropdown.h index bd7b838c..6dab470e 100644 --- a/src/widgets/kpunch/dropdown/dropdown.h +++ b/src/widgets/kpunch/dropdown/dropdown.h @@ -45,7 +45,9 @@ typedef struct { static inline const DropdownApiT *dvxDropdownApi(void) { static const DropdownApiT *sApi; - if (!sApi) { sApi = (const DropdownApiT *)wgtGetApi("dropdown", sizeof(DropdownApiT)); } + if (!sApi) { + sApi = (const DropdownApiT *)wgtGetApi("dropdown", sizeof(DropdownApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/dropdown/dropdown.res b/src/widgets/kpunch/dropdown/dropdown.res index 1631897e..2c106eb6 100644 --- a/src/widgets/kpunch/dropdown/dropdown.res +++ b/src/widgets/kpunch/dropdown/dropdown.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon dropdown.bmp name text "DropDown" author text "Scott Duensing" diff --git a/src/widgets/kpunch/dropdown/widgetDropdown.c b/src/widgets/kpunch/dropdown/widgetDropdown.c index 622e72f0..b2f5e9fb 100644 --- a/src/widgets/kpunch/dropdown/widgetDropdown.c +++ b/src/widgets/kpunch/dropdown/widgetDropdown.c @@ -67,6 +67,7 @@ typedef struct { // Prototypes // ============================================================ +static void basSetItems(WidgetT *w, const char *spec); WidgetT *wgtDropdown(WidgetT *parent); void wgtDropdownAddItem(WidgetT *w, const char *text); void wgtDropdownClear(WidgetT *w); @@ -76,7 +77,6 @@ int32_t wgtDropdownGetSelected(const WidgetT *w); void wgtDropdownRemoveItem(WidgetT *w, int32_t idx); void wgtDropdownSetItems(WidgetT *w, const char **items, int32_t count); void wgtDropdownSetSelected(WidgetT *w, int32_t idx); -void wgtRegister(void); void widgetDropdownAccelActivate(WidgetT *w, WidgetT *root); void widgetDropdownCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetDropdownClosePopup(WidgetT *w); @@ -87,6 +87,16 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); void widgetDropdownPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetDropdownPaintPopup(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +void wgtRegister(void); + + +// BASIC wrapper: SetItems "Red|Green|Blue" replaces the items +// (listPopupSetItemsSpec); nothing is selected. +static void basSetItems(WidgetT *w, const char *spec) { + VALIDATE_WIDGET_VOID(w, sTypeId); + + listPopupSetItemsSpec(w, &((DropdownDataT *)w->data)->popup, spec); +} WidgetT *wgtDropdown(WidgetT *parent) { @@ -392,6 +402,7 @@ static const WgtMethodDescT sMethods[] = { { "List", WGT_SIG_RET_STR_INT, (void *)wgtDropdownGetItem }, { "ListCount", WGT_SIG_RET_INT, (void *)wgtDropdownGetItemCount }, { "RemoveItem", WGT_SIG_INT, (void *)wgtDropdownRemoveItem }, + { "SetItems", WGT_SIG_STR, (void *)basSetItems }, }; static const WgtIfaceT sIface = { diff --git a/src/widgets/kpunch/helpView/helpView.h b/src/widgets/kpunch/helpView/helpView.h index f2acffe1..75d58a59 100644 --- a/src/widgets/kpunch/helpView/helpView.h +++ b/src/widgets/kpunch/helpView/helpView.h @@ -56,7 +56,9 @@ typedef struct { static inline const HelpViewApiT *dvxHelpViewApi(void) { static const HelpViewApiT *sApi; - if (!sApi) { sApi = (const HelpViewApiT *)wgtGetApi("helpview", sizeof(HelpViewApiT)); } + if (!sApi) { + sApi = (const HelpViewApiT *)wgtGetApi("helpview", sizeof(HelpViewApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/helpView/helpview.res b/src/widgets/kpunch/helpView/helpview.res index f4bf0ccc..89e7529b 100644 --- a/src/widgets/kpunch/helpView/helpview.res +++ b/src/widgets/kpunch/helpView/helpview.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon helpview.bmp name text "HelpPane" author text "Scott Duensing" diff --git a/src/widgets/kpunch/helpView/widgetHelpView.c b/src/widgets/kpunch/helpView/widgetHelpView.c index 6957e130..3268d233 100644 --- a/src/widgets/kpunch/helpView/widgetHelpView.c +++ b/src/widgets/kpunch/helpView/widgetHelpView.c @@ -46,6 +46,7 @@ #include "../box/box.h" #include "../image/image.h" #include "../scrollPane/scrlPane.h" +#include "stb_ds_wrap.h" #include #include @@ -73,6 +74,7 @@ DVX_ABI_STAMP(helpview); #define HELP_RULE_LINE_Y 2 // rule line offset within the widget #define HELP_BULLET "\x07 " // list item prefix (bullet glyph + space) #define HELP_BULLET_LEN 2 +#define HELP_PICS_KEEP_BYTES (512 * 1024) // decoded pictures kept with a help file at most // Horizontal space inside the pane that text cannot use: the ScrollPane // border on both sides, its vertical scrollbar and the content VBox @@ -113,6 +115,7 @@ typedef struct { typedef struct { char *text; int32_t lineCount; + int32_t widestChars; // its longest line's characters (measured once) } HelpCodeDataT; // One content record being added: the HelpView and the box records go in. @@ -122,6 +125,25 @@ typedef struct { WidgetT *box; } HelpBuildT; +// A help file's pictures decoded for the display, kept with the file +// (hlpFileSetCache) so a topic shown again does not decode them again: +// up to HELP_PICS_KEEP_BYTES, for the pixel size they were made in. +typedef struct { + uint8_t *pixels; + uint32_t offset; // the picture's place in the file (HlpImageRefT.imageOffset) + uint32_t keyColor; + int32_t w; + int32_t h; + int32_t pitch; + bool hasAlpha; +} HelpPicT; + +typedef struct { + HelpPicT *pics; // stb_ds + int32_t bpp; // the display's bytes per pixel when they were made + int32_t bytes; +} HelpPicsT; + static int32_t sTypeId = -1; static int32_t sHelpTextTypeId = -1; static int32_t sHelpHeadingTypeId = -1; @@ -137,6 +159,7 @@ static int32_t sHelpRuleTypeId = -1; static void attachContentData(WidgetT *w, void *data, bool complete, void (*destroy)(WidgetT *w)); static void basShowTopic(WidgetT *w, int32_t handle, int32_t topic); static void buildImage(const HelpBuildT *b, const HlpRecordHdrT *hdr, const char *payload); +static HelpPicT buildImagePic(const HelpBuildT *b, const HlpImageRefT *ref); static bool buildRecord(const HlpRecordHdrT *hdr, const char *payload, void *userData); static int32_t countLines(const char *text); static void doWrap(HelpWrapDataT *wd, int32_t pixelW, int32_t charW, int32_t padPixels); @@ -165,10 +188,11 @@ static int32_t helpViewWrapWidth(const WidgetT *w, const BitmapFontT *font); static void helpWrapCalcMinSize(WidgetT *w, const BitmapFontT *font, int32_t padPixels, int32_t extraH); static void helpWrapDestroy(WidgetT *w); static bool isFirstChild(const WidgetT *w); -static int32_t maxLineWidth(const BitmapFontT *font, const char *text); +static int32_t maxLineChars(const char *text); static bool nextSiblingHasOwnBorder(const WidgetT *w); static WidgetT *ownerView(const WidgetT *w); static void paintLines(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t lineCount, uint32_t fg); +static void picsFree(void *cache); WidgetT *wgtHelpView(WidgetT *parent); void wgtHelpViewClear(WidgetT *w); const char *wgtHelpViewGetLinkTarget(const WidgetT *w); @@ -275,32 +299,18 @@ static void basShowTopic(WidgetT *w, int32_t handle, int32_t topic) { } -// An image record: its picture decoded for the display, in an HBox when -// it is centered or right-aligned. +// An image record: its picture decoded for the display (buildImagePic), +// in an HBox when it is centered or right-aligned. static void buildImage(const HelpBuildT *b, const HlpRecordHdrT *hdr, const char *payload) { HlpImageRefT ref; - uint32_t size = 0; if (!hlpRecordImage(hdr, payload, &ref)) { return; } - uint8_t *bmpData = hlpFileReadImage(b->file, &ref, &size); + HelpPicT pic = buildImagePic(b, &ref); - if (!bmpData) { - return; - } - - int32_t imgW = 0; - int32_t imgH = 0; - int32_t imgP = 0; - bool hasAlpha = false; - uint32_t keyColor = 0; - uint8_t *pixels = dvxLoadImageAlpha(wgtGetContext(b->view), bmpData, (int32_t)size, &imgW, &imgH, &imgP, &hasAlpha, &keyColor); - - free(bmpData); - - if (!pixels) { + if (!pic.pixels) { return; } @@ -316,14 +326,87 @@ static void buildImage(const HelpBuildT *b, const HlpRecordHdrT *hdr, const char } // wgtImage takes ownership of pixels even on failure - WidgetT *imgWgt = wgtImage(parent, pixels, imgW, imgH, imgP); + WidgetT *imgWgt = wgtImage(parent, pic.pixels, pic.w, pic.h, pic.pitch); - if (imgWgt && hasAlpha) { - wgtImageSetTransparent(imgWgt, true, keyColor); + if (imgWgt && pic.hasAlpha) { + wgtImageSetTransparent(imgWgt, true, pic.keyColor); } } +// The picture at ref for an Image widget (its pixels are the caller's): +// a copy of the one kept with the help file, or decoded now and, while +// there is room, a copy kept. No pixels when it cannot be read. +static HelpPicT buildImagePic(const HelpBuildT *b, const HlpImageRefT *ref) { + AppContextT *ctx = wgtGetContext(b->view); + HelpPicsT *pics = (HelpPicsT *)hlpFileCache(b->file); + HelpPicT pic; + uint32_t size = 0; + + memset(&pic, 0, sizeof(pic)); + + // Pictures made for another pixel size (the video mode changed) go + if (pics && pics->bpp != ctx->display.format.bytesPerPixel) { + hlpFileSetCache(b->file, NULL, NULL); + pics = NULL; + } + + for (int32_t i = 0; pics && i < (int32_t)arrlen(pics->pics); i++) { + if (pics->pics[i].offset == ref->imageOffset) { + size_t bytes = (size_t)pics->pics[i].pitch * (size_t)pics->pics[i].h; + + pic = pics->pics[i]; + pic.pixels = (uint8_t *)malloc(bytes); + + if (pic.pixels) { + memcpy(pic.pixels, pics->pics[i].pixels, bytes); + } + + return pic; + } + } + + uint8_t *data = hlpFileReadImage(b->file, ref, &size); + + if (!data) { + return pic; + } + + pic.offset = ref->imageOffset; + pic.pixels = dvxLoadImageAlpha(ctx, data, (int32_t)size, &pic.w, &pic.h, &pic.pitch, &pic.hasAlpha, &pic.keyColor); + free(data); + + int32_t bytes = pic.pitch * pic.h; + + if (!pic.pixels || (pics ? pics->bytes : 0) + bytes > HELP_PICS_KEEP_BYTES) { + return pic; + } + + if (!pics) { + pics = (HelpPicsT *)calloc(1, sizeof(HelpPicsT)); + + if (!pics) { + return pic; + } + + pics->bpp = ctx->display.format.bytesPerPixel; + hlpFileSetCache(b->file, pics, picsFree); + } + + HelpPicT kept = pic; + + kept.pixels = (uint8_t *)malloc((size_t)bytes); + + if (kept.pixels) { + memcpy(kept.pixels, pic.pixels, (size_t)bytes); + arrput(pics->pics, kept); + pics->bytes += bytes; + } + + return pic; +} + + // hlpFileForEachRecord callback that adds one record's widget(s). static bool buildRecord(const HlpRecordHdrT *hdr, const char *payload, void *userData) { const HelpBuildT *b = (const HelpBuildT *)userData; @@ -427,8 +510,9 @@ static bool buildRecord(const HlpRecordHdrT *hdr, const char *payload, void *use HelpCodeDataT *cd = (HelpCodeDataT *)calloc(1, sizeof(HelpCodeDataT)); if (cd) { - cd->text = strdup(payload); - cd->lineCount = countLines(cd->text); + cd->text = strdup(payload); + cd->lineCount = countLines(cd->text); + cd->widestChars = maxLineChars(cd->text); } attachContentData(w, cd, cd && cd->text, helpCodeDestroy); @@ -477,7 +561,7 @@ static void doWrap(HelpWrapDataT *wd, int32_t pixelW, int32_t charW, int32_t pad static void helpCodeCalcMinSize(WidgetT *w, const BitmapFontT *font) { HelpCodeDataT *cd = (HelpCodeDataT *)w->data; - w->calcMinW = maxLineWidth(font, cd->text) + HELP_CONTENT_PAD * 2 + HELP_CODE_PAD * 2; + w->calcMinW = cd->widestChars * font->charWidth + HELP_CONTENT_PAD * 2 + HELP_CODE_PAD * 2; w->calcMinH = cd->lineCount * font->charHeight + HELP_CODE_PAD * 2; } @@ -807,8 +891,9 @@ static bool isFirstChild(const WidgetT *w) { } -static int32_t maxLineWidth(const BitmapFontT *font, const char *text) { - int32_t maxW = 0; +// The characters of text's longest line. +static int32_t maxLineChars(const char *text) { + int32_t widest = 0; int32_t lineLen = 0; if (!text) { @@ -817,12 +902,7 @@ static int32_t maxLineWidth(const BitmapFontT *font, const char *text) { for (const char *p = text; ; p++) { if (*p == '\n' || *p == '\0') { - int32_t w = lineLen * font->charWidth; - - if (w > maxW) { - maxW = w; - } - + widest = DVX_MAX(widest, lineLen); lineLen = 0; if (*p == '\0') { @@ -833,7 +913,7 @@ static int32_t maxLineWidth(const BitmapFontT *font, const char *text) { } } - return maxW; + return widest; } @@ -857,18 +937,21 @@ static WidgetT *ownerView(const WidgetT *w) { } +// Lines above the clip rect are only stepped over, and the first below it +// ends the walk: a long topic draws what shows. static void paintLines(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t lineCount, uint32_t fg) { if (!text) { return; } - const char *line = text; + const char *line = text; + int32_t bottom = d->clipY + d->clipH; - for (int32_t i = 0; i < lineCount; i++) { + for (int32_t i = 0; i < lineCount && y < bottom; i++) { const char *end = strchr(line, '\n'); int32_t len = end ? (int32_t)(end - line) : (int32_t)strlen(line); - if (len > 0) { + if (len > 0 && y + font->charHeight > d->clipY) { drawTextN(d, ops, font, x, y, line, len, fg, 0, false); } @@ -878,6 +961,23 @@ static void paintLines(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font } +// hlpFileSetCache's free for HelpPicsT. +static void picsFree(void *cache) { + HelpPicsT *pics = (HelpPicsT *)cache; + + if (!pics) { + return; + } + + for (int32_t i = 0; i < (int32_t)arrlen(pics->pics); i++) { + free(pics->pics[i].pixels); + } + + arrfree(pics->pics); + free(pics); +} + + WidgetT *wgtHelpView(WidgetT *parent) { WidgetT *w = parent ? widgetAlloc(parent, sTypeId) : NULL; diff --git a/src/widgets/kpunch/image/image.bhs b/src/widgets/kpunch/image/image.bhs index b8d5ee23..4a904453 100644 --- a/src/widgets/kpunch/image/image.bhs +++ b/src/widgets/kpunch/image/image.bhs @@ -28,6 +28,7 @@ .index BMP .index MaskColor .index UseMaskColor +.index SetPictureData .h1 Image @@ -40,7 +41,7 @@ A static image display control. Loads bitmap images from file. Set the Picture p .table Property Type Description ----------- ------- ------------------------------------------- - Picture Picture Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget, or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. A disabled Image is drawn grayed (its transparent color stays transparent). + Picture Picture Path to an image file to load (or a Picture object from LOADPICTURE or LOADPICTUREDATA); a program, widget, or library file gives its icon. Reads back as a Picture object for the picture shown, which stands for the file name ("" for a picture from bytes) where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. A disabled Image is drawn grayed (its transparent color stays transparent). Stretch Boolean When True, an image larger than the control is scaled down to fit, keeping its proportions; a smaller image stays at natural size. When False, the image is shown at natural size. ImageWidth Integer Width of the loaded image in pixels (read-only). ImageHeight Integer Height of the loaded image in pixels (read-only). @@ -48,7 +49,15 @@ A static image display control. Loads bitmap images from file. Set the Picture p UseMaskColor Boolean True leaves out the pixels of MaskColor (default False). .endtable -No type-specific methods or events. +.h2 Type-Specific Methods + +.table + Method Description + ------ ----------- + SetPictureData(bytes$ [, name$]) Show the picture held in bytes$ (the bytes of a BMP, PNG, JPEG, or GIF file, such as HttpBody$ or ResGetData$ gives), as Picture = LoadPictureData(bytes$) does but without keeping a Picture object, so it suits showing many pictures in turn. Picture then reads name$ ("" without it). Returns True, or False (the picture unchanged) when the bytes are not a picture. +.endtable + +No type-specific events. Default Event: Click diff --git a/src/widgets/kpunch/image/image.dhs b/src/widgets/kpunch/image/image.dhs index 3f7beb81..e8c0e0b7 100644 --- a/src/widgets/kpunch/image/image.dhs +++ b/src/widgets/kpunch/image/image.dhs @@ -30,6 +30,7 @@ .index wgtImageFromFile .index wgtImageSetData .index wgtImageLoadFile +.index wgtImageLoadData .index wgtImageSetTransparent .h2 Image @@ -55,6 +56,7 @@ WidgetT *img = wgtImageFromFile(parent, "logo.bmp"); WidgetT *wgtImageFromFile(parent, path) Create by loading an image file. void wgtImageSetData(w, data, imgW, imgH, pitch) Replace the displayed image with new raw pixel data. void wgtImageLoadFile(w, path) Replace by loading a new file. Static but exposed via the API struct. + bool wgtImageLoadData(w, data, len, name) Replace with the picture in len bytes of an image file (as dvxLoadImageFromMemory reads them); Picture then reads name (NULL: ""). False (the image unchanged) when they are not a picture. void wgtImageSetTransparent(w, hasTransparency, keyColor) Set chroma-key color. Pixels matching keyColor are not drawn. .endtable @@ -69,6 +71,7 @@ WidgetT *img = wgtImageFromFile(parent, "logo.bmp"); setData wgtImageSetData loadFile wgtImageLoadFile setTransparent wgtImageSetTransparent + loadData wgtImageLoadData .endtable .h3 Properties (BASIC Interface) @@ -76,15 +79,23 @@ WidgetT *img = wgtImageFromFile(parent, "logo.bmp"); .table Property Type Access Description -------- ---- ------ ----------- - Picture String Read/Write Setting writes a file path (loads image). Reading returns the path of the loaded file (also set by wgtImageFromFile); in BASIC the path comes as a Picture object, which stands for the path where a value is needed (see LOADPICTURE); wgtImageSetData clears it. Setting "" clears the image and the path; a file that cannot be loaded leaves both unchanged. + Picture String Read/Write Setting writes a file path (loads image). Reading returns the path of the loaded file (also set by wgtImageFromFile), or the name wgtImageLoadData was given; in BASIC the path comes as a Picture object, which stands for the path where a value is needed (see LOADPICTURE); wgtImageSetData clears it. Setting "" clears the image and the path; a file that cannot be loaded leaves both unchanged. Stretch Boolean Read/Write When true, an image larger than the widget is scaled down to fit (aspect preserved, nearest neighbour); a smaller image stays at natural size. When false, shown at natural size. Changing it relayouts the widget. ImageWidth Integer Read-only Width of the currently loaded image in pixels. ImageHeight Integer Read-only Height of the currently loaded image in pixels. + MaskColor Integer Read/Write The transparent colour as a BASIC colour (&HBBGGRR), converted with wgtImageSetTransparent. + UseMaskColor Boolean Read/Write Whether pixels of MaskColor are left out (wgtImageSetTransparent's hasTransparency). .endtable .h3 Methods -No widget-specific BASIC methods. Setting Picture loads a new image. +.table + Method Description + ------ ----------- + SetPictureData(bytes$[, name$]) WGT_METHOD_SET_PICTURE_DATA (WGT_SIG_DYNAMIC): wgtImageLoadData(bytes, length, name). Returns True when the bytes are a picture. +.endtable + +Setting Picture loads a new image; the BASIC form runtime shows a LOADPICTUREDATA picture through SetPictureData. .h3 Events diff --git a/src/widgets/kpunch/image/image.h b/src/widgets/kpunch/image/image.h index 6b74cc13..2ec11bb8 100644 --- a/src/widgets/kpunch/image/image.h +++ b/src/widgets/kpunch/image/image.h @@ -33,6 +33,7 @@ typedef struct { void (*setData)(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch); void (*loadFile)(WidgetT *w, const char *path); void (*setTransparent)(WidgetT *w, bool hasTransparency, uint32_t keyColor); + bool (*loadData)(WidgetT *w, const uint8_t *data, int32_t len, const char *name); } ImageApiT; // Consumer side: the accessor and convenience macros. The widget's own @@ -41,7 +42,9 @@ typedef struct { static inline const ImageApiT *dvxImageApi(void) { static const ImageApiT *sApi; - if (!sApi) { sApi = (const ImageApiT *)wgtGetApi("image", sizeof(ImageApiT)); } + if (!sApi) { + sApi = (const ImageApiT *)wgtGetApi("image", sizeof(ImageApiT)); + } return sApi; } @@ -50,6 +53,7 @@ static inline const ImageApiT *dvxImageApi(void) { #define wgtImageSetData(w, data, imgW, imgH, pitch) dvxImageApi()->setData(w, data, imgW, imgH, pitch) #define wgtImageLoadFile(w, path) dvxImageApi()->loadFile(w, path) #define wgtImageSetTransparent(w, has, key) dvxImageApi()->setTransparent(w, has, key) +#define wgtImageLoadData(w, data, len, name) dvxImageApi()->loadData(w, data, len, name) #endif // IMAGE_IMPL diff --git a/src/widgets/kpunch/image/image.res b/src/widgets/kpunch/image/image.res index 3d3944da..a519ec9c 100644 --- a/src/widgets/kpunch/image/image.res +++ b/src/widgets/kpunch/image/image.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon image.bmp name text "Image" author text "Scott Duensing" diff --git a/src/widgets/kpunch/image/imageBuf.h b/src/widgets/kpunch/image/imageBuf.h index c581a4b1..860612f1 100644 --- a/src/widgets/kpunch/image/imageBuf.h +++ b/src/widgets/kpunch/image/imageBuf.h @@ -53,7 +53,8 @@ static inline void imageBufDropGray(ImageBufT *b); static inline void imageBufFree(ImageBufT *b); static inline const uint8_t *imageBufGray(ImageBufT *b, const DisplayT *disp, const BlitOpsT *ops, bool keyed, uint32_t keyColor); static inline void imageBufInit(ImageBufT *b, uint8_t *pixelData, int32_t w, int32_t h, int32_t pitch); -static inline uint8_t *imageBufLoad(WidgetT *w, const char *path, int32_t *imgW, int32_t *imgH, int32_t *pitch); +static inline uint8_t *imageBufLoad(WidgetT *w, const char *path, const uint8_t *data, int32_t len, int32_t *imgW, int32_t *imgH, int32_t *pitch); +static inline bool imageBufLoadData(ImageBufT *b, WidgetT *w, const uint8_t *data, int32_t len, const char *name); static inline bool imageBufLoadFile(ImageBufT *b, WidgetT *w, const char *path); static inline void imageBufSetData(ImageBufT *b, uint8_t *pixelData, int32_t imgW, int32_t imgH, int32_t pitch); static inline void imageBufSetPath(ImageBufT *b, const char *path); @@ -145,22 +146,42 @@ static inline void imageBufInit(ImageBufT *b, uint8_t *pixelData, int32_t w, int } -// Load a picture file in the display format with the busy cursor shown. -// w is any widget of the target window (it supplies the context). -static inline uint8_t *imageBufLoad(WidgetT *w, const char *path, int32_t *imgW, int32_t *imgH, int32_t *pitch) { - AppContextT *ctx = path ? wgtGetContext(w) : NULL; +// Load a picture file (path), or a picture file's bytes (data, len bytes, +// path NULL), in the display format with the busy cursor shown. w is any +// widget of the target window (it supplies the context). +static inline uint8_t *imageBufLoad(WidgetT *w, const char *path, const uint8_t *data, int32_t len, int32_t *imgW, int32_t *imgH, int32_t *pitch) { + AppContextT *ctx = (path || data) ? wgtGetContext(w) : NULL; if (!ctx) { return NULL; } dvxSetBusy(ctx, true); - uint8_t *buf = dvxLoadImage(ctx, path, imgW, imgH, pitch); + uint8_t *buf = path ? dvxLoadImage(ctx, path, imgW, imgH, pitch) : dvxLoadImageFromMemory(ctx, data, len, imgW, imgH, pitch); dvxSetBusy(ctx, false); return buf; } +// Replace the pixels with those of a picture file's bytes, name (NULL: +// "") kept as the path. Returns false (image unchanged) when they are +// not a picture DVX can show. +static inline bool imageBufLoadData(ImageBufT *b, WidgetT *w, const uint8_t *data, int32_t len, const char *name) { + int32_t imgW; + int32_t imgH; + int32_t pitch; + uint8_t *buf = imageBufLoad(w, NULL, data, len, &imgW, &imgH, &pitch); + + if (!buf) { + return false; + } + + imageBufSetData(b, buf, imgW, imgH, pitch); + imageBufSetPath(b, name ? name : ""); + return true; +} + + // Replace the pixels with a picture file and remember its path. An empty // (or NULL) path clears the image and its path. Returns false (image // unchanged) when the file cannot be loaded. @@ -173,7 +194,7 @@ static inline bool imageBufLoadFile(ImageBufT *b, WidgetT *w, const char *path) int32_t imgW; int32_t imgH; int32_t pitch; - uint8_t *buf = imageBufLoad(w, path, &imgW, &imgH, &pitch); + uint8_t *buf = imageBufLoad(w, path, NULL, 0, &imgW, &imgH, &pitch); if (!buf) { return false; diff --git a/src/widgets/kpunch/image/widgetImage.c b/src/widgets/kpunch/image/widgetImage.c index 91cc1a10..05105c13 100644 --- a/src/widgets/kpunch/image/widgetImage.c +++ b/src/widgets/kpunch/image/widgetImage.c @@ -47,9 +47,6 @@ // ABI stamp checked by the loader (see DVX_ABI_VERSION). DVX_ABI_STAMP(image); -#define IMAGE_FIX_SHIFT 16 // 16.16 fixed point for the scaled blit -#define IMAGE_BPP8 1 -#define IMAGE_BPP16 2 #define IMAGE_RED_SHIFT 16 // &HRRGGBB channels #define IMAGE_GREEN_SHIFT 8 @@ -67,7 +64,7 @@ typedef struct { // Prototypes // ============================================================ -static void imagePaintScaled(DisplayT *disp, const uint8_t *src, int32_t srcPitch, int32_t imgW, int32_t imgH, int32_t dx, int32_t dy, int32_t fitW, int32_t fitH, bool keyed, uint32_t keyColor); +static bool basSetPictureData(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret); WidgetT *wgtImage(WidgetT *parent, uint8_t *pixelData, int32_t w, int32_t h, int32_t pitch); WidgetT *wgtImageFromFile(WidgetT *parent, const char *path); static int32_t wgtImageGetHeight(const WidgetT *w); @@ -76,6 +73,7 @@ static const char *wgtImageGetPicture(const WidgetT *w); static bool wgtImageGetStretch(const WidgetT *w); static bool wgtImageGetUseMaskColor(const WidgetT *w); static int32_t wgtImageGetWidth(const WidgetT *w); +bool wgtImageLoadData(WidgetT *w, const uint8_t *data, int32_t len, const char *name); static void wgtImageLoadFile(WidgetT *w, const char *path); void wgtImageSetData(WidgetT *w, uint8_t *pixelData, int32_t imgW, int32_t imgH, int32_t pitch); static void wgtImageSetMaskColor(WidgetT *w, int32_t color); @@ -88,74 +86,19 @@ void widgetImagePaint(WidgetT *w, DisplayT *disp, const BlitOpsT * void wgtRegister(void); -// Nearest-neighbour scale of src (imgW x imgH) into the fitW x fitH box at -// (dx, dy) of the backbuffer. The clip is applied once (row and column -// ranges), source coordinates step in 16.16 fixed point (no per-pixel -// divide) and the depth switch runs once per row, not per pixel. With -// keyed set, source pixels equal to keyColor are skipped. -static void imagePaintScaled(DisplayT *disp, const uint8_t *src, int32_t srcPitch, int32_t imgW, int32_t imgH, int32_t dx, int32_t dy, int32_t fitW, int32_t fitH, bool keyed, uint32_t keyColor) { - int32_t bpp = disp->format.bytesPerPixel; - uint32_t stepX = (uint32_t)(((uint64_t)imgW << IMAGE_FIX_SHIFT) / (uint32_t)fitW); - uint32_t stepY = (uint32_t)(((uint64_t)imgH << IMAGE_FIX_SHIFT) / (uint32_t)fitH); - int32_t x0 = disp->clipX > dx ? disp->clipX - dx : 0; - int32_t x1 = disp->clipX + disp->clipW < dx + fitW ? disp->clipX + disp->clipW - dx : fitW; - int32_t y0 = disp->clipY > dy ? disp->clipY - dy : 0; - int32_t y1 = disp->clipY + disp->clipH < dy + fitH ? disp->clipY + disp->clipH - dy : fitH; +// SetPictureData bytes$[, name$] in BASIC (dynamic: the bytes may hold +// NULs): True when they are a picture DVX can show +static bool basSetPictureData(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret) { + char nameBuf[WGT_VALUE_TEXT_MAX]; - for (int32_t y = y0; y < y1; y++) { - const uint8_t *srcRow = src + (int32_t)(((uint64_t)y * stepY) >> IMAGE_FIX_SHIFT) * srcPitch; - uint8_t *dstRow = disp->backBuf + (dy + y) * disp->pitch + (dx + x0) * bpp; - uint32_t sx = (uint32_t)x0 * stepX; - int32_t count = x1 - x0; - - switch (bpp) { - case IMAGE_BPP8: { - for (int32_t i = 0; i < count; i++) { - uint8_t px = srcRow[sx >> IMAGE_FIX_SHIFT]; - - if (!keyed || px != (uint8_t)keyColor) { - dstRow[i] = px; - } - - sx += stepX; - } - - break; - } - case IMAGE_BPP16: { - const uint16_t *s16 = (const uint16_t *)srcRow; - uint16_t *d16 = (uint16_t *)dstRow; - - for (int32_t i = 0; i < count; i++) { - uint16_t px = s16[sx >> IMAGE_FIX_SHIFT]; - - if (!keyed || px != (uint16_t)keyColor) { - d16[i] = px; - } - - sx += stepX; - } - - break; - } - default: { - const uint32_t *s32 = (const uint32_t *)srcRow; - uint32_t *d32 = (uint32_t *)dstRow; - - for (int32_t i = 0; i < count; i++) { - uint32_t px = s32[sx >> IMAGE_FIX_SHIFT]; - - if (!keyed || px != keyColor) { - d32[i] = px; - } - - sx += stepX; - } - - break; - } - } + if (argc < WGT_PICTURE_DATA_ARGS_MIN || argc > WGT_PICTURE_DATA_ARGS_MAX || args[0].type != WGT_IFACE_STRING) { + return false; } + + const char *name = (argc == WGT_PICTURE_DATA_ARGS_MAX) ? wgtValueToText(&args[1], nameBuf, (int32_t)sizeof(nameBuf)) : NULL; + + *ret = wgtValueBool(wgtImageLoadData(w, (const uint8_t *)args[0].s, wgtValueLen(&args[0]), name)); + return true; } @@ -194,7 +137,7 @@ WidgetT *wgtImageFromFile(WidgetT *parent, const char *path) { int32_t imgW; int32_t imgH; int32_t pitch; - uint8_t *buf = imageBufLoad(parent, path, &imgW, &imgH, &pitch); + uint8_t *buf = imageBufLoad(parent, path, NULL, 0, &imgW, &imgH, &pitch); if (!buf) { return NULL; @@ -263,6 +206,22 @@ static int32_t wgtImageGetWidth(const WidgetT *w) { } +// Show the bytes of a picture file (as dvxLoadImageFromMemory reads +// them); Picture then reads name (NULL: ""). False (the image unchanged) +// when they are not a picture DVX can show. +bool wgtImageLoadData(WidgetT *w, const uint8_t *data, int32_t len, const char *name) { + VALIDATE_WIDGET(w, sTypeId, false); + ImageDataT *d = (ImageDataT *)w->data; + + if (!imageBufLoadData(&d->buf, w, data, len, name)) { + return false; + } + + wgtInvalidate(w); + return true; +} + + static void wgtImageLoadFile(WidgetT *w, const char *path) { VALIDATE_WIDGET_VOID(w, sTypeId); ImageDataT *d = (ImageDataT *)w->data; @@ -399,7 +358,7 @@ void widgetImagePaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const Bit } if (fitW != imgW || fitH != imgH) { - imagePaintScaled(disp, src, b->imgPitch, imgW, imgH, dx, dy, fitW, fitH, keyed, d->keyColor); + drawScaled(disp, dx, dy, fitW, fitH, src, imgW, imgH, b->imgPitch, keyed, d->keyColor); } else if (keyed) { rectCopyTransparent(disp, ops, dx, dy, src, b->imgPitch, 0, 0, imgW, imgH, d->keyColor); } else { @@ -429,7 +388,12 @@ static const ImageApiT sApi = { .fromFile = wgtImageFromFile, .setData = wgtImageSetData, .loadFile = wgtImageLoadFile, - .setTransparent = wgtImageSetTransparent + .setTransparent = wgtImageSetTransparent, + .loadData = wgtImageLoadData +}; + +static const WgtMethodDescT sMethods[] = { + { WGT_METHOD_SET_PICTURE_DATA, WGT_SIG_DYNAMIC, (void *)basSetPictureData } }; static const WgtPropDescT sProps[] = { @@ -445,8 +409,8 @@ static const WgtIfaceT sIface = { .basName = "Image", .props = sProps, .propCount = DVX_ARRAY_LEN(sProps), - .methods = NULL, - .methodCount = 0, + .methods = sMethods, + .methodCount = DVX_ARRAY_LEN(sMethods), .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT_DATA, diff --git a/src/widgets/kpunch/imageButton/imgBtn.h b/src/widgets/kpunch/imageButton/imgBtn.h index 6935a3a0..633648e5 100644 --- a/src/widgets/kpunch/imageButton/imgBtn.h +++ b/src/widgets/kpunch/imageButton/imgBtn.h @@ -32,6 +32,7 @@ typedef struct { WidgetT *(*fromFile)(WidgetT *parent, const char *path); void (*setData)(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch); void (*loadFile)(WidgetT *w, const char *path); + bool (*loadData)(WidgetT *w, const uint8_t *data, int32_t len, const char *name); } ImageButtonApiT; // Consumer side: the accessor and convenience macros. The widget's own @@ -40,7 +41,9 @@ typedef struct { static inline const ImageButtonApiT *dvxImageButtonApi(void) { static const ImageButtonApiT *sApi; - if (!sApi) { sApi = (const ImageButtonApiT *)wgtGetApi("imagebutton", sizeof(ImageButtonApiT)); } + if (!sApi) { + sApi = (const ImageButtonApiT *)wgtGetApi("imagebutton", sizeof(ImageButtonApiT)); + } return sApi; } @@ -48,6 +51,7 @@ static inline const ImageButtonApiT *dvxImageButtonApi(void) { #define wgtImageButtonFromFile(parent, path) dvxImageButtonApi()->fromFile(parent, path) #define wgtImageButtonSetData(w, data, imgW, imgH, pitch) dvxImageButtonApi()->setData(w, data, imgW, imgH, pitch) #define wgtImageButtonLoadFile(w, path) dvxImageButtonApi()->loadFile(w, path) +#define wgtImageButtonLoadData(w, data, len, name) dvxImageButtonApi()->loadData(w, data, len, name) #endif // IMGBTN_IMPL diff --git a/src/widgets/kpunch/imageButton/imgbtn.bhs b/src/widgets/kpunch/imageButton/imgbtn.bhs index 78ee611f..d3bd3296 100644 --- a/src/widgets/kpunch/imageButton/imgbtn.bhs +++ b/src/widgets/kpunch/imageButton/imgbtn.bhs @@ -24,6 +24,7 @@ .title ImageButton .toc 1 ImageButton .index ImageButton +.index SetPictureData .h1 ImageButton @@ -36,12 +37,20 @@ A button that displays an image instead of text. Has the same press/release beha .table Property Type Description ----------- ------- ------------------------------------------- - Picture Picture Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget, or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. + Picture Picture Path to an image file to load (or a Picture object from LOADPICTURE or LOADPICTUREDATA); a program, widget, or library file gives its icon. Reads back as a Picture object for the picture shown, which stands for the file name ("" for a picture from bytes) where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. ImageWidth Integer Width of the loaded image in pixels (read-only). ImageHeight Integer Height of the loaded image in pixels (read-only). .endtable -No type-specific methods or events. +.h2 Type-Specific Methods + +.table + Method Description + ------ ----------- + SetPictureData(bytes$ [, name$]) Show the picture held in bytes$ (the bytes of a BMP, PNG, JPEG, or GIF file, such as HttpBody$ or ResGetData$ gives), as Picture = LoadPictureData(bytes$) does but without keeping a Picture object, so it suits showing many pictures in turn. Picture then reads name$ ("" without it). Returns True, or False (the picture unchanged) when the bytes are not a picture. +.endtable + +No type-specific events. Default Event: Click diff --git a/src/widgets/kpunch/imageButton/imgbtn.dhs b/src/widgets/kpunch/imageButton/imgbtn.dhs index ae9a7857..1c059258 100644 --- a/src/widgets/kpunch/imageButton/imgbtn.dhs +++ b/src/widgets/kpunch/imageButton/imgbtn.dhs @@ -29,6 +29,7 @@ .index wgtImageButtonFromFile .index wgtImageButtonSetData .index wgtImageButtonLoadFile +.index wgtImageButtonLoadData .h2 ImageButton @@ -52,6 +53,7 @@ btn->onClick = onSaveClicked; WidgetT *wgtImageButtonFromFile(parent, path) Create by loading an image file. void wgtImageButtonSetData(w, data, imgW, imgH, pitch) Replace the image with new raw pixel data. void wgtImageButtonLoadFile(w, path) Replace the image by loading a new file. + bool wgtImageButtonLoadData(w, data, len, name) Replace the image with the picture in len bytes of an image file (as dvxLoadImageFromMemory reads them); Picture then reads name (NULL: ""). False (the image unchanged) when they are not a picture. .endtable .h3 API Struct (wgtRegisterApi "imagebutton") @@ -64,6 +66,7 @@ btn->onClick = onSaveClicked; fromFile wgtImageButtonFromFile setData wgtImageButtonSetData loadFile wgtImageButtonLoadFile + loadData wgtImageButtonLoadData .endtable .h3 Properties (BASIC Interface) @@ -71,14 +74,20 @@ btn->onClick = onSaveClicked; .table Property Type Access Description -------- ---- ------ ----------- - Picture String Read/Write Setting writes a file path (loads image). Reading returns the path of the loaded file (also set by wgtImageButtonFromFile); in BASIC the path comes as a Picture object, which stands for the path where a value is needed (see LOADPICTURE); wgtImageButtonSetData clears it. Setting "" clears the image and the path; a file that cannot be loaded leaves both unchanged. + Picture String Read/Write Setting writes a file path (loads image). Reading returns the path of the loaded file (also set by wgtImageButtonFromFile), or the name wgtImageButtonLoadData was given; in BASIC the path comes as a Picture object, which stands for the path where a value is needed (see LOADPICTURE); wgtImageButtonSetData clears it. Setting "" clears the image and the path; a file that cannot be loaded leaves both unchanged. ImageWidth Integer Read-only Width of the currently loaded image in pixels. ImageHeight Integer Read-only Height of the currently loaded image in pixels. .endtable .h3 Methods -No widget-specific BASIC methods. Setting Picture loads a new image. +.table + Method Description + ------ ----------- + SetPictureData(bytes$[, name$]) WGT_METHOD_SET_PICTURE_DATA (WGT_SIG_DYNAMIC): wgtImageButtonLoadData(bytes, length, name). Returns True when the bytes are a picture. +.endtable + +Setting Picture loads a new image; the BASIC form runtime shows a LOADPICTUREDATA picture through SetPictureData. .h3 Events diff --git a/src/widgets/kpunch/imageButton/imgbtn.res b/src/widgets/kpunch/imageButton/imgbtn.res index 9e86981f..f4bb3b8d 100644 --- a/src/widgets/kpunch/imageButton/imgbtn.res +++ b/src/widgets/kpunch/imageButton/imgbtn.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon imgbtn.bmp name text "ImageButton" author text "Scott Duensing" diff --git a/src/widgets/kpunch/imageButton/widgetImageButton.c b/src/widgets/kpunch/imageButton/widgetImageButton.c index bf525e57..646e4ce5 100644 --- a/src/widgets/kpunch/imageButton/widgetImageButton.c +++ b/src/widgets/kpunch/imageButton/widgetImageButton.c @@ -65,11 +65,13 @@ typedef struct { // Prototypes // ============================================================ +static bool basSetPictureData(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret); WidgetT *wgtImageButton(WidgetT *parent, uint8_t *pixelData, int32_t w, int32_t h, int32_t pitch); WidgetT *wgtImageButtonFromFile(WidgetT *parent, const char *path); static int32_t wgtImageButtonGetHeight(const WidgetT *w); static const char *wgtImageButtonGetPicture(const WidgetT *w); static int32_t wgtImageButtonGetWidth(const WidgetT *w); +bool wgtImageButtonLoadData(WidgetT *w, const uint8_t *data, int32_t len, const char *name); static void wgtImageButtonLoadFile(WidgetT *w, const char *path); void wgtImageButtonSetData(WidgetT *w, uint8_t *pixelData, int32_t imgW, int32_t imgH, int32_t pitch); void widgetImageButtonCalcMinSize(WidgetT *w, const BitmapFontT *font); @@ -78,6 +80,22 @@ void widgetImageButtonPaint(WidgetT *w, DisplayT *disp, const Blit void wgtRegister(void); +// SetPictureData bytes$[, name$] in BASIC (dynamic: the bytes may hold +// NULs): True when they are a picture DVX can show +static bool basSetPictureData(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret) { + char nameBuf[WGT_VALUE_TEXT_MAX]; + + if (argc < WGT_PICTURE_DATA_ARGS_MIN || argc > WGT_PICTURE_DATA_ARGS_MAX || args[0].type != WGT_IFACE_STRING) { + return false; + } + + const char *name = (argc == WGT_PICTURE_DATA_ARGS_MAX) ? wgtValueToText(&args[1], nameBuf, (int32_t)sizeof(nameBuf)) : NULL; + + *ret = wgtValueBool(wgtImageButtonLoadData(w, (const uint8_t *)args[0].s, wgtValueLen(&args[0]), name)); + return true; +} + + // Ownership contract matches wgtImage: the widget takes pixelData // unconditionally and frees it on failure. A NULL buffer creates an // empty button (the form runtime's WGT_CREATE_PARENT_DATA path) whose @@ -112,7 +130,7 @@ WidgetT *wgtImageButtonFromFile(WidgetT *parent, const char *path) { int32_t imgW; int32_t imgH; int32_t pitch; - uint8_t *buf = imageBufLoad(parent, path, &imgW, &imgH, &pitch); + uint8_t *buf = imageBufLoad(parent, path, NULL, 0, &imgW, &imgH, &pitch); if (!buf) { return NULL; @@ -149,6 +167,22 @@ static int32_t wgtImageButtonGetWidth(const WidgetT *w) { } +// Show the bytes of a picture file (as dvxLoadImageFromMemory reads +// them); Picture then reads name (NULL: ""). False (the image unchanged) +// when they are not a picture DVX can show. +bool wgtImageButtonLoadData(WidgetT *w, const uint8_t *data, int32_t len, const char *name) { + VALIDATE_WIDGET(w, sTypeId, false); + ImageButtonDataT *d = (ImageButtonDataT *)w->data; + + if (!imageBufLoadData(&d->buf, w, data, len, name)) { + return false; + } + + wgtInvalidate(w); + return true; +} + + static void wgtImageButtonLoadFile(WidgetT *w, const char *path) { VALIDATE_WIDGET_VOID(w, sTypeId); ImageButtonDataT *d = (ImageButtonDataT *)w->data; @@ -237,7 +271,12 @@ static const ImageButtonApiT sApi = { .create = wgtImageButton, .fromFile = wgtImageButtonFromFile, .setData = wgtImageButtonSetData, - .loadFile = wgtImageButtonLoadFile + .loadFile = wgtImageButtonLoadFile, + .loadData = wgtImageButtonLoadData +}; + +static const WgtMethodDescT sImgBtnMethods[] = { + { WGT_METHOD_SET_PICTURE_DATA, WGT_SIG_DYNAMIC, (void *)basSetPictureData } }; static const WgtPropDescT sImgBtnProps[] = { @@ -250,8 +289,8 @@ static const WgtIfaceT sIface = { .basName = "ImageButton", .props = sImgBtnProps, .propCount = DVX_ARRAY_LEN(sImgBtnProps), - .methods = NULL, - .methodCount = 0, + .methods = sImgBtnMethods, + .methodCount = DVX_ARRAY_LEN(sImgBtnMethods), .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT_DATA, diff --git a/src/widgets/kpunch/label/label.h b/src/widgets/kpunch/label/label.h index 7b46f04a..575fb309 100644 --- a/src/widgets/kpunch/label/label.h +++ b/src/widgets/kpunch/label/label.h @@ -38,7 +38,9 @@ typedef struct { static inline const LabelApiT *dvxLabelApi(void) { static const LabelApiT *sApi; - if (!sApi) { sApi = (const LabelApiT *)wgtGetApi("label", sizeof(LabelApiT)); } + if (!sApi) { + sApi = (const LabelApiT *)wgtGetApi("label", sizeof(LabelApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/label/label.res b/src/widgets/kpunch/label/label.res index f3819c35..2b4ce481 100644 --- a/src/widgets/kpunch/label/label.res +++ b/src/widgets/kpunch/label/label.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon label.bmp name text "Label" author text "Scott Duensing" diff --git a/src/widgets/kpunch/listBox/listBox.h b/src/widgets/kpunch/listBox/listBox.h index 723152b5..d69fb719 100644 --- a/src/widgets/kpunch/listBox/listBox.h +++ b/src/widgets/kpunch/listBox/listBox.h @@ -51,7 +51,9 @@ typedef struct { static inline const ListBoxApiT *dvxListBoxApi(void) { static const ListBoxApiT *sApi; - if (!sApi) { sApi = (const ListBoxApiT *)wgtGetApi("listbox", sizeof(ListBoxApiT)); } + if (!sApi) { + sApi = (const ListBoxApiT *)wgtGetApi("listbox", sizeof(ListBoxApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/listBox/listbox.bhs b/src/widgets/kpunch/listBox/listbox.bhs index e9b7188a..ad2e0b8f 100644 --- a/src/widgets/kpunch/listBox/listbox.bhs +++ b/src/widgets/kpunch/listBox/listbox.bhs @@ -41,6 +41,7 @@ A scrollable list of selectable items. Items are managed via methods (AddItem, R Property Type Description --------- ------- ------------------------------------------- ListIndex Integer Index of the currently selected item (-1 = no selection). + Text String The selected item ("" with none); the default property, so Print List1 prints it. Setting it selects the first item that is that text (case ignored); with none, the selection stays as it is. .endtable .h2 Type-Specific Methods diff --git a/src/widgets/kpunch/listBox/listbox.res b/src/widgets/kpunch/listBox/listbox.res index 2c5a8ff8..64143080 100644 --- a/src/widgets/kpunch/listBox/listbox.res +++ b/src/widgets/kpunch/listBox/listbox.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon listbox.bmp name text "ListBox" author text "Scott Duensing" diff --git a/src/widgets/kpunch/listBox/widgetListBox.c b/src/widgets/kpunch/listBox/widgetListBox.c index 5fcddff1..41875e69 100644 --- a/src/widgets/kpunch/listBox/widgetListBox.c +++ b/src/widgets/kpunch/listBox/widgetListBox.c @@ -63,6 +63,7 @@ #include #include +#include // ABI stamp checked by the loader (see DVX_ABI_VERSION). DVX_ABI_STAMP(listbox); @@ -129,9 +130,9 @@ void wgtListBoxSetItemSelected(WidgetT *w, int32_t idx, bool selec void wgtListBoxSetMultiSelect(WidgetT *w, bool multi); void wgtListBoxSetReorderable(WidgetT *w, bool reorderable); void wgtListBoxSetSelected(WidgetT *w, int32_t idx); -void wgtRegister(void); void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetListBoxDestroy(WidgetT *w); +const char *widgetListBoxGetText(const WidgetT *w); static void widgetListBoxOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y); static void widgetListBoxOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y); void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod); @@ -140,34 +141,17 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *o static void widgetListBoxReorderDrop(WidgetT *w); static void widgetListBoxReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y); static void widgetListBoxScrollDragUpdate(WidgetT *w, ScrollbarOrientE orient, int32_t dragOff, int32_t mouseX, int32_t mouseY); +void widgetListBoxSetText(WidgetT *w, const char *text); +void wgtRegister(void); -// BASIC wrapper: SetItems "Item1|Item2|Item3" replaces the items with the -// pipe-separated list (every '|' separates, so "a|" is "a" plus an empty -// item; "" leaves the list empty). The items are appended to the owned -// array directly and the widget is refreshed once. +// BASIC wrapper: SetItems "Item1|Item2|Item3" replaces the items +// (listItemsSetSpec) and refreshes the widget once. static void basSetItems(WidgetT *w, const char *spec) { VALIDATE_WIDGET_VOID(w, sTypeId); - ListBoxDataT *d = (ListBoxDataT *)w->data; - const BitmapFontT *font = wgtFont(w); - bool widthChanged = listItemsClear(&d->items); - char *buf = (spec && *spec) ? strdup(spec) : NULL; + ListBoxDataT *d = (ListBoxDataT *)w->data; + bool widthChanged = listItemsSetSpec(&d->items, wgtFont(w), spec); - for (char *tok = buf; tok; ) { - char *sep = strchr(tok, '|'); - - if (sep) { - *sep = '\0'; - } - - if (listItemsAdd(&d->items, font, tok)) { - widthChanged = true; - } - - tok = sep ? sep + 1 : NULL; - } - - free(buf); listBoxResetState(w, d); listBoxInvalidate(w, widthChanged); } @@ -454,6 +438,16 @@ void widgetListBoxDestroy(WidgetT *w) { } +// Text (BASIC's default property, as VB's): the selected item, "" when +// none is. +const char *widgetListBoxGetText(const WidgetT *w) { + int32_t sel = wgtListBoxGetSelected(w); + const char *item = sel >= 0 ? wgtListBoxGetItem(w, sel) : NULL; + + return item ? item : ""; +} + + // The drop fires onChange last (the handler may destroy the widget), and // the dispatcher repaints after drag end, so nothing follows it here. static void widgetListBoxOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { @@ -764,6 +758,18 @@ static void widgetListBoxScrollDragUpdate(WidgetT *w, ScrollbarOrientE orient, i } +// Text = item: the first item that is it (case ignored, as VB) is +// selected; none leaves the selection as it is. +void widgetListBoxSetText(WidgetT *w, const char *text) { + for (int32_t i = 0; text && i < wgtListBoxGetItemCount(w); i++) { + if (strcasecmp(wgtListBoxGetItem(w, i), text) == 0) { + wgtListBoxSetSelected(w, i); + return; + } + } +} + + // ============================================================ // DXE registration // ============================================================ @@ -777,6 +783,8 @@ static const WidgetClassT sClassListBox = { [WGT_METHOD_ON_MOUSE] = (void *)widgetListBoxOnMouse, [WGT_METHOD_ON_KEY] = (void *)widgetListBoxOnKey, [WGT_METHOD_DESTROY] = (void *)widgetListBoxDestroy, + [WGT_METHOD_GET_TEXT] = (void *)widgetListBoxGetText, + [WGT_METHOD_SET_TEXT] = (void *)widgetListBoxSetText, [WGT_METHOD_ON_DRAG_UPDATE] = (void *)widgetListBoxOnDragUpdate, [WGT_METHOD_ON_DRAG_END] = (void *)widgetListBoxOnDragEnd, } diff --git a/src/widgets/kpunch/listView/listView.dhs b/src/widgets/kpunch/listView/listView.dhs index 61561e34..becb3401 100644 --- a/src/widgets/kpunch/listView/listView.dhs +++ b/src/widgets/kpunch/listView/listView.dhs @@ -77,6 +77,8 @@ typedef enum { .index wgtListViewGetSelected .index wgtListViewSetSelected .index wgtListViewSetSort +.index wgtListViewGetSortCol +.index wgtListViewGetSortDir .index wgtListViewSetHeaderClickCallback .index wgtListViewSetMultiSelect .index wgtListViewSelectAll @@ -92,6 +94,8 @@ typedef enum { wgtListViewGetSelected(w) Get the index of the selected row (-1 if none). wgtListViewSetSelected(w, idx) Set the selected row by index (outside the list = none) and scroll it into view. wgtListViewSetSort(w, col, dir) Set the sort column and direction. A column outside the defined columns, or ListViewSortNoneE, clears the sort. Ignored while reorder is enabled. + wgtListViewGetSortCol(w) The column the list is sorted by (-1 when unsorted): the last wgtListViewSetSort or header click. + wgtListViewGetSortDir(w) The sort direction (ListViewSortNoneE when unsorted). wgtListViewSetHeaderClickCallback(w, cb) Set a callback for header clicks. Signature: void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir). After it, the ListView fires its own event ColumnClick (Column As Integer, 0-based) with wgtFireEvent, which reaches the widget's onEvent (BASIC: CtrlName_ColumnClick). wgtListViewSetMultiSelect(w, multi) Enable or disable multi-selection. wgtListViewIsItemSelected(w, idx) Check if a specific row is selected. @@ -99,6 +103,12 @@ typedef enum { wgtListViewSelectAll(w) Select all rows. wgtListViewClearSelection(w) Deselect all rows. wgtListViewSetReorderable(w, reorderable) Enable drag-to-reorder of rows. + wgtListViewAddItem(w, text) Add a row with text in its first column (the list keeps its own copy of the cells from then on). + wgtListViewRemoveRow(w, row) Remove a data row. + wgtListViewClear(w) Remove all rows. + wgtListViewGetCell(w, row, col) The text of a cell ("" outside the list). + wgtListViewSetCell(w, row, col, text) Set the text of a cell. + wgtListViewGetRowCount(w) The number of rows. .endtable .h3 Events @@ -117,6 +127,8 @@ typedef enum { Property Type Access Description -------- ---- ------ ----------- ListIndex Integer Read/Write Index of the currently selected row. + SortColumn Integer Read/Write The sort column (-1 when unsorted). Setting it sorts by that column in the current order (ascending when unsorted); -1 unsorts. + SortOrder Integer Read/Write The sort order (0 none, 1 ascending, 2 descending). Setting it re-sorts the current column; an unsorted list stays unsorted. .endtable .h3 Methods (BASIC Interface) @@ -129,14 +141,17 @@ typedef enum { ClearSelection Deselect all rows. GetCell(row%, col%) Returns the text of a cell. IsItemSelected(index%) Check if a specific row is selected. + ListCount() Returns the number of rows (the same as RowCount). RemoveItem index% Remove a row by index. Rows after it keep their selection; removing the selected row leaves no selection. RowCount() Returns the number of rows. SelectAll Select all rows. + SelectedRows() Returns the selected rows' numbers separated by spaces ("" when none). SetCell row%, col%, text$ Set the text of a cell. SetColumns spec$ Define columns from a pipe-delimited spec string ("Name,Width|Name,Width"). SetItemSelected idx%, sel Select or deselect a specific row by index. SetMultiSelect multi Enable or disable multi-selection. SetReorderable reorderable Enable or disable drag-to-reorder. + SetRows text$ Replace all rows at once: rows separated by CHR$(10), cells by CHR$(9). SetSort col%, dir% Set the sort column and direction (0=none, 1=ascending, 2=descending). An invalid column or direction clears the sort; ignored while reorder is enabled. .endtable diff --git a/src/widgets/kpunch/listView/listView.h b/src/widgets/kpunch/listView/listView.h index c7dcd158..0191e826 100644 --- a/src/widgets/kpunch/listView/listView.h +++ b/src/widgets/kpunch/listView/listView.h @@ -51,6 +51,8 @@ typedef struct { const char *(*getCell)(const WidgetT *w, int32_t row, int32_t col); void (*setCell)(WidgetT *w, int32_t row, int32_t col, const char *text); int32_t (*getRowCount)(const WidgetT *w); + int32_t (*getSortCol)(const WidgetT *w); + ListViewSortE (*getSortDir)(const WidgetT *w); } ListViewApiT; // Consumer side: the accessor and convenience macros. The widget's own @@ -59,7 +61,9 @@ typedef struct { static inline const ListViewApiT *dvxListViewApi(void) { static const ListViewApiT *sApi; - if (!sApi) { sApi = (const ListViewApiT *)wgtGetApi("listview", sizeof(ListViewApiT)); } + if (!sApi) { + sApi = (const ListViewApiT *)wgtGetApi("listview", sizeof(ListViewApiT)); + } return sApi; } @@ -82,6 +86,8 @@ static inline const ListViewApiT *dvxListViewApi(void) { #define wgtListViewGetCell(w, row, col) dvxListViewApi()->getCell(w, row, col) #define wgtListViewSetCell(w, row, col, text) dvxListViewApi()->setCell(w, row, col, text) #define wgtListViewGetRowCount(w) dvxListViewApi()->getRowCount(w) +#define wgtListViewGetSortCol(w) dvxListViewApi()->getSortCol(w) +#define wgtListViewGetSortDir(w) dvxListViewApi()->getSortDir(w) #endif // LISTVIEW_IMPL diff --git a/src/widgets/kpunch/listView/listview.bhs b/src/widgets/kpunch/listView/listview.bhs index b08445c8..1c151ca2 100644 --- a/src/widgets/kpunch/listView/listview.bhs +++ b/src/widgets/kpunch/listView/listview.bhs @@ -28,6 +28,8 @@ .index Multi-Column List .index SetColumns .index SetSort +.index SortColumn +.index SortOrder .index SetRows .index SelectedRows .index ColumnClick @@ -41,9 +43,11 @@ A multi-column list with column headers. Supports sorting, multi-select, and dra .h2 Type-Specific Properties .table - Property Type R/W Description - --------- ------- --- ------------------------------------------- - ListIndex Integer R/W Index of the currently selected row (-1 = none). + Property Type R/W Description + ---------- ------- --- ------------------------------------------- + ListIndex Integer R/W Index of the currently selected row (-1 = none). + SortColumn Integer R/W The column the list is sorted by, -1 when it is unsorted: what SetSort or the user's last click on a column header chose. Setting it sorts by that column in the current SortOrder (ascending when the list was unsorted); -1 unsorts the list. + SortOrder Integer R/W The order of the sort: 0 = none, 1 = ascending, 2 = descending (a click on the same header again reverses it). Setting it re-sorts the SortColumn in that order; 0 unsorts the list, and an unsorted list stays unsorted. .endtable .h2 Type-Specific Methods @@ -75,7 +79,7 @@ A multi-column list with column headers. Supports sorting, multi-select, and dra .table Event Parameters Description ----------- ---------------------- ------------------------------------------- - ColumnClick Column As Integer A column header was clicked (Column 0 is the first). Without a handler the list sorts itself by that column's text (the same column again reverses); a program that orders the rows itself calls SetSort -1, 0 in the handler and fills the list in its own order. + ColumnClick Column As Integer A column header was clicked (Column 0 is the first). The list has already sorted itself by that column's text (the same column again reverses), with or without a handler; a program that orders the rows itself calls SetSort -1, 0 in the handler (or SetSort for another order) and fills the list in its own order. .endtable Default Event: Click diff --git a/src/widgets/kpunch/listView/listview.res b/src/widgets/kpunch/listView/listview.res index 97afa6ef..bb13ab89 100644 --- a/src/widgets/kpunch/listView/listview.res +++ b/src/widgets/kpunch/listView/listview.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon listview.bmp name text "ListView" author text "Scott Duensing" diff --git a/src/widgets/kpunch/listView/widgetListView.c b/src/widgets/kpunch/listView/widgetListView.c index 091a703f..ab4a9499 100644 --- a/src/widgets/kpunch/listView/widgetListView.c +++ b/src/widgets/kpunch/listView/widgetListView.c @@ -43,13 +43,17 @@ // 2. Selection bits are indexed by data row, not display row -- // rearranging data would invalidate all selection state // The index is rebuilt lazily (sortDirty) the next time it is needed, with -// a stable merge sort, so a burst of AddItem/SetCell calls costs one sort. +// a stable merge sort, when the rows are replaced; a row added (AddItem), +// a sort cell set (SetCell), or a row removed (RemoveRow) is put in or +// taken out of a built index (binary search), so filling a sorted list +// row by row is not a sort per frame. // // Column widths are tagged sizes (wgtPixels/wgtChars/wgtPercent, resolved // through wgtResolveSize) or 0 for auto (widest cell or title). They are -// resolved lazily in resolveColumnWidths whenever totalColW is 0 (data -// changes reset it) or the font is not the one they were resolved in -// (colWFont). A width the user set by dragging a header border or +// resolved lazily in resolveColumnWidths whenever totalColW is 0 (the rows +// replaced, a column's widest cell made narrower or removed) or the font +// is not the one they were resolved in (colWFont); a row added or a cell +// set wider widens them in place (listViewWidthsGrow). A width the user set by dragging a header border or // double-clicking it (autosize) is kept in userColW and survives data // changes until new columns are set. Titles and cells are cut to the // characters that fit whole in their column, in any font. @@ -162,56 +166,67 @@ typedef struct { // Prototypes // ============================================================ -static const char *basSelectedRows(const WidgetT *w); -static void basSetColumns(WidgetT *w, const char *spec); -static void basSetRows(WidgetT *w, const char *text); -static char *cellCopy(const char *text, int32_t len); -static void listViewAdoptCells(WidgetT *w); -static int32_t listViewAutoColWidth(const ListViewDataT *lv, int32_t col, const BitmapFontT *font); -static void listViewBuildSortIndex(ListViewDataT *lv); -static void listViewCalcGeom(const WidgetT *w, const ListViewDataT *lv, const BitmapFontT *font, ListViewGeomT *g); -static void listViewClampScroll(ListViewDataT *lv, const ListViewGeomT *g); -static int32_t listViewCompareRows(const ListViewDataT *lv, int32_t a, int32_t b); -static void listViewEnsureColWidths(const WidgetT *w, const BitmapFontT *font); -static void listViewKeepVisible(ListViewDataT *lv, int32_t displayRow, int32_t visibleRows); -static const int32_t *listViewOrder(ListViewDataT *lv); -static void listViewRemapOwnedCells(WidgetT *w, int32_t newColCount); -static void listViewSetUserColW(ListViewDataT *lv, int32_t col, int32_t width); -static void listViewSyncOwned(WidgetT *w); -static int32_t listViewTypeAhead(const ListViewDataT *lv, const int32_t *order, char ch, int32_t displaySel); -static void resolveColumnWidths(const WidgetT *w, const BitmapFontT *font); -WidgetT *wgtListView(WidgetT *parent); -void wgtListViewAddItem(WidgetT *w, const char *text); -void wgtListViewClear(WidgetT *w); -void wgtListViewClearSelection(WidgetT *w); -const char *wgtListViewGetCell(const WidgetT *w, int32_t row, int32_t col); -int32_t wgtListViewGetRowCount(const WidgetT *w); -int32_t wgtListViewGetSelected(const WidgetT *w); -bool wgtListViewIsItemSelected(const WidgetT *w, int32_t idx); -void wgtListViewRemoveRow(WidgetT *w, int32_t row); -void wgtListViewSelectAll(WidgetT *w); -void wgtListViewSetCell(WidgetT *w, int32_t row, int32_t col, const char *text); -void wgtListViewSetColumns(WidgetT *w, const ListViewColT *cols, int32_t count); -void wgtListViewSetData(WidgetT *w, const char **cellData, int32_t rowCount); -void wgtListViewSetHeaderClickCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir)); -void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected); -void wgtListViewSetMultiSelect(WidgetT *w, bool multi); -void wgtListViewSetReorderable(WidgetT *w, bool reorderable); -void wgtListViewSetSelected(WidgetT *w, int32_t idx); -void wgtListViewSetSort(WidgetT *w, int32_t col, ListViewSortE dir); -void wgtRegister(void); -void widgetListViewCalcMinSize(WidgetT *w, const BitmapFontT *font); -bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy); -void widgetListViewDestroy(WidgetT *w); -int32_t widgetListViewGetCursorShape(const WidgetT *w, int32_t vx, int32_t vy); -static void widgetListViewOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y); -static void widgetListViewOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y); -void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod); -void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy); -void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -static void widgetListViewReorderDrop(WidgetT *w); -static void widgetListViewReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y); -static void widgetListViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY); +static const char *basSelectedRows(const WidgetT *w); +static void basSetColumns(WidgetT *w, const char *spec); +static void basSetRows(WidgetT *w, const char *text); +static void basSetSortColumn(WidgetT *w, int32_t col); +static void basSetSortOrder(WidgetT *w, int32_t dir); +static char *cellCopy(const char *text, int32_t len); +static void listViewAdoptCells(WidgetT *w); +static bool listViewAutoCol(const ListViewDataT *lv, int32_t col); +static int32_t listViewAutoColWidth(const ListViewDataT *lv, int32_t col, const BitmapFontT *font); +static void listViewBuildSortIndex(ListViewDataT *lv); +static void listViewCalcGeom(const WidgetT *w, const ListViewDataT *lv, const BitmapFontT *font, ListViewGeomT *g); +static int32_t listViewCellWidth(const ListViewDataT *lv, const BitmapFontT *font, int32_t row, int32_t col); +static void listViewClampScroll(ListViewDataT *lv, const ListViewGeomT *g); +static int32_t listViewCompareRows(const ListViewDataT *lv, int32_t a, int32_t b); +static void listViewEnsureColWidths(const WidgetT *w, const BitmapFontT *font); +static void listViewKeepVisible(ListViewDataT *lv, int32_t displayRow, int32_t visibleRows); +static const int32_t *listViewOrder(ListViewDataT *lv); +static void listViewRemapOwnedCells(WidgetT *w, int32_t newColCount); +static void listViewSetUserColW(ListViewDataT *lv, int32_t col, int32_t width); +static void listViewSortPlace(ListViewDataT *lv, int32_t row); +static void listViewSortTake(ListViewDataT *lv, int32_t row, int32_t count); +static void listViewSyncCount(WidgetT *w); +static void listViewSyncOwned(WidgetT *w); +static int32_t listViewTypeAhead(const ListViewDataT *lv, const int32_t *order, char ch, int32_t displaySel); +static const BitmapFontT *listViewWidthFont(const WidgetT *w); +static void listViewWidthsGrow(WidgetT *w, int32_t row); +static void resolveColumnWidths(const WidgetT *w, const BitmapFontT *font); +WidgetT *wgtListView(WidgetT *parent); +void wgtListViewAddItem(WidgetT *w, const char *text); +void wgtListViewClear(WidgetT *w); +void wgtListViewClearSelection(WidgetT *w); +const char *wgtListViewGetCell(const WidgetT *w, int32_t row, int32_t col); +int32_t wgtListViewGetRowCount(const WidgetT *w); +int32_t wgtListViewGetSelected(const WidgetT *w); +int32_t wgtListViewGetSortCol(const WidgetT *w); +ListViewSortE wgtListViewGetSortDir(const WidgetT *w); +bool wgtListViewIsItemSelected(const WidgetT *w, int32_t idx); +void wgtListViewRemoveRow(WidgetT *w, int32_t row); +void wgtListViewSelectAll(WidgetT *w); +void wgtListViewSetCell(WidgetT *w, int32_t row, int32_t col, const char *text); +void wgtListViewSetColumns(WidgetT *w, const ListViewColT *cols, int32_t count); +void wgtListViewSetData(WidgetT *w, const char **cellData, int32_t rowCount); +void wgtListViewSetHeaderClickCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir)); +void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected); +void wgtListViewSetMultiSelect(WidgetT *w, bool multi); +void wgtListViewSetReorderable(WidgetT *w, bool reorderable); +void wgtListViewSetSelected(WidgetT *w, int32_t idx); +void wgtListViewSetSort(WidgetT *w, int32_t col, ListViewSortE dir); +void widgetListViewCalcMinSize(WidgetT *w, const BitmapFontT *font); +bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy); +void widgetListViewDestroy(WidgetT *w); +int32_t widgetListViewGetCursorShape(const WidgetT *w, int32_t vx, int32_t vy); +static void widgetListViewOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y); +static void widgetListViewOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y); +void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod); +void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy); +void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +static void widgetListViewReorderDrop(WidgetT *w); +static void widgetListViewReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y); +static void widgetListViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY); +void wgtRegister(void); // BASIC wrapper: SelectedRows$ -- the selected rows' numbers (data rows, @@ -366,6 +381,26 @@ static void basSetRows(WidgetT *w, const char *text) { } +// BASIC wrapper: SortColumn = col% -- sort by that column, in the order +// already chosen (ascending when the list was unsorted); -1 unsorts it +static void basSetSortColumn(WidgetT *w, int32_t col) { + VALIDATE_WIDGET_VOID(w, sTypeId); + ListViewSortE dir = ((const ListViewDataT *)w->data)->sortDir; + + wgtListViewSetSort(w, col, dir == ListViewSortNoneE ? ListViewSortAscE : dir); +} + + +// BASIC wrapper: SortOrder = dir% -- the order (0 none, 1 ascending, 2 +// descending) for the column the list is sorted by; an unsorted list stays +// unsorted +static void basSetSortOrder(WidgetT *w, int32_t dir) { + VALIDATE_WIDGET_VOID(w, sTypeId); + + wgtListViewSetSort(w, ((const ListViewDataT *)w->data)->sortCol, (ListViewSortE)dir); +} + + // A heap copy of len characters of text, NUL-terminated (NULL when out of // memory). static char *cellCopy(const char *text, int32_t len) { @@ -412,6 +447,12 @@ static void listViewAdoptCells(WidgetT *w) { } +// A column whose width is its widest cell: auto (0), not one the user set. +static bool listViewAutoCol(const ListViewDataT *lv, int32_t col) { + return lv->userColW[col] <= 0 && lv->cols[col].width == 0; +} + + // Width that fits the column's title and its widest cell. static int32_t listViewAutoColWidth(const ListViewDataT *lv, int32_t col, const BitmapFontT *font) { const char *title = lv->cols[col].title; @@ -540,6 +581,15 @@ static void listViewCalcGeom(const WidgetT *w, const ListViewDataT *lv, const Bi } +// The width an auto column takes for the cell at row, col in font (with +// its pad). +static int32_t listViewCellWidth(const ListViewDataT *lv, const BitmapFontT *font, int32_t row, int32_t col) { + const char *cell = lv->cellData[row * lv->colCount + col]; + + return (cell ? textWidth(font, cell) : 0) + LISTVIEW_COL_PAD; +} + + static void listViewClampScroll(ListViewDataT *lv, const ListViewGeomT *g) { lv->scrollPos = clampInt(lv->scrollPos, 0, g->maxScrollV); lv->scrollPosH = clampInt(lv->scrollPosH, 0, g->maxScrollH); @@ -648,21 +698,81 @@ static void listViewSetUserColW(ListViewDataT *lv, int32_t col, int32_t width) { } -// Sync cellData/rowCount from ownedCells after a mutation. The sort index -// and auto column widths are rebuilt lazily; the minimum size does not -// depend on the data, so a repaint is enough. -static void listViewSyncOwned(WidgetT *w) { +// Data row row into a built sort (one of rowCount - 1 rows), after the +// rows that sort before it and equal keys earlier in data order, as the +// stable sort has them. Out of memory: the sort is rebuilt lazily. +static void listViewSortPlace(ListViewDataT *lv, int32_t row) { + if (lv->sortDirty || lv->sortCol < 0 || lv->sortDir == ListViewSortNoneE) { + return; + } + + int32_t *idx = (int32_t *)realloc(lv->sortIndex, (size_t)lv->rowCount * sizeof(int32_t)); + + if (!idx) { + lv->sortDirty = true; + return; + } + + lv->sortIndex = idx; + + int32_t lo = 0; + int32_t hi = lv->rowCount - 1; + + while (lo < hi) { + int32_t mid = (lo + hi) / 2; + int32_t cmp = listViewCompareRows(lv, idx[mid], row); + + if (cmp < 0 || (cmp == 0 && idx[mid] < row)) { + lo = mid + 1; + } else { + hi = mid; + } + } + + memmove(&idx[lo + 1], &idx[lo], (size_t)(lv->rowCount - 1 - lo) * sizeof(int32_t)); + idx[lo] = row; +} + + +// Data row row out of a built sort of count rows (its key changes, or the +// row goes). +static void listViewSortTake(ListViewDataT *lv, int32_t row, int32_t count) { + if (lv->sortDirty || !lv->sortIndex) { + return; + } + + for (int32_t i = 0; i < count; i++) { + if (lv->sortIndex[i] == row) { + memmove(&lv->sortIndex[i], &lv->sortIndex[i + 1], (size_t)(count - 1 - i) * sizeof(int32_t)); + return; + } + } +} + + +// Sync cellData/rowCount from ownedCells after a mutation; the minimum +// size does not depend on the data, so a repaint is enough. +static void listViewSyncCount(WidgetT *w) { ListViewDataT *lv = (ListViewDataT *)w->data; lv->cellData = (const char **)lv->ownedCells; lv->rowCount = lv->colCount > 0 ? (int32_t)arrlen(lv->ownedCells) / lv->colCount : 0; - lv->totalColW = 0; lv->ownsCells = true; - lv->sortDirty = true; wgtInvalidatePaint(w); } +// listViewSyncCount after the rows were replaced: the sort index and the +// auto column widths are rebuilt lazily. +static void listViewSyncOwned(WidgetT *w) { + ListViewDataT *lv = (ListViewDataT *)w->data; + + listViewSyncCount(w); + lv->totalColW = 0; + lv->sortDirty = true; +} + + // Type-ahead: next display row after displaySel whose first-column text // starts with ch, or -1. static int32_t listViewTypeAhead(const ListViewDataT *lv, const int32_t *order, char ch, int32_t displaySel) { @@ -679,6 +789,33 @@ static int32_t listViewTypeAhead(const ListViewDataT *lv, const int32_t *order, } +// The widget's font while the column widths hold, resolved in it (NULL: +// they are resolved again whole at the next paint). +static const BitmapFontT *listViewWidthFont(const WidgetT *w) { + const ListViewDataT *lv = (const ListViewDataT *)w->data; + AppContextT *ctx = wgtGetContext(w); + const BitmapFontT *font = ctx ? wgtFontFor(w, &ctx->font) : NULL; + + return (lv->totalColW != 0 && font && fontStampIs(&lv->colWFont, font)) ? font : NULL; +} + + +// Auto columns row widens take its width, while the widths hold. +static void listViewWidthsGrow(WidgetT *w, int32_t row) { + ListViewDataT *lv = (ListViewDataT *)w->data; + const BitmapFontT *font = listViewWidthFont(w); + + for (int32_t c = 0; font && c < lv->colCount; c++) { + int32_t width = listViewAutoCol(lv, c) ? listViewCellWidth(lv, font, row, c) : 0; + + if (width > lv->resolvedColW[c]) { + lv->totalColW += width - lv->resolvedColW[c]; + lv->resolvedColW[c] = width; + } + } +} + + // Resolve every column width: a user-set width wins, then 0 = auto (widest // cell or title), otherwise the tagged size via wgtResolveSize. static void resolveColumnWidths(const WidgetT *w, const BitmapFontT *font) { @@ -767,7 +904,11 @@ void wgtListViewAddItem(WidgetT *w, const char *text) { lv->ownedCells[idx] = strdup(text ? text : ""); lv->nextCell++; - listViewSyncOwned(w); + // The new row widens the columns and takes its place in the sort, not + // a measure and sort of every row (a list filled row by row) + listViewSyncCount(w); + listViewWidthsGrow(w, lv->rowCount - 1); + listViewSortPlace(lv, lv->rowCount - 1); listSelInsert(&lv->sel, lv->rowCount, lv->rowCount - 1); } @@ -828,6 +969,22 @@ int32_t wgtListViewGetSelected(const WidgetT *w) { } +// The column the list is sorted by (-1 when unsorted): the program's +// SetSort or the last header click. +int32_t wgtListViewGetSortCol(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, -1); + + return ((const ListViewDataT *)w->data)->sortCol; +} + + +ListViewSortE wgtListViewGetSortDir(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, ListViewSortNoneE); + + return ((const ListViewDataT *)w->data)->sortDir; +} + + bool wgtListViewIsItemSelected(const WidgetT *w, int32_t idx) { VALIDATE_WIDGET(w, sTypeId, false); const ListViewDataT *lv = (const ListViewDataT *)w->data; @@ -854,12 +1011,28 @@ void wgtListViewRemoveRow(WidgetT *w, int32_t row) { int32_t base = row * lv->colCount; + // A column's widest cell going: its width is resolved again + const BitmapFontT *font = listViewWidthFont(w); + + for (int32_t c = 0; font && c < lv->colCount; c++) { + if (listViewAutoCol(lv, c) && listViewCellWidth(lv, font, row, c) >= lv->resolvedColW[c]) { + lv->totalColW = 0; + } + } + for (int32_t c = 0; c < lv->colCount; c++) { free(lv->ownedCells[base + c]); } arrdeln(lv->ownedCells, base, lv->colCount); + // Out of the sort, the rows after it one data row earlier + listViewSortTake(lv, row, oldRowCount); + + for (int32_t i = 0; !lv->sortDirty && lv->sortIndex && i < oldRowCount - 1; i++) { + lv->sortIndex[i] -= lv->sortIndex[i] > row ? 1 : 0; + } + // Adjust nextCell if it was past the removed row if (lv->nextCell > base + lv->colCount) { lv->nextCell -= lv->colCount; @@ -868,7 +1041,7 @@ void wgtListViewRemoveRow(WidgetT *w, int32_t row) { } listReorderCancel(w, &lv->dragIdx, &lv->dropIdx); - listViewSyncOwned(w); + listViewSyncCount(w); listSelRemove(&lv->sel, oldRowCount, row); } @@ -894,14 +1067,27 @@ void wgtListViewSetCell(WidgetT *w, int32_t row, int32_t col, const char *text) listViewAdoptCells(w); - int32_t idx = row * lv->colCount + col; + int32_t idx = row * lv->colCount + col; + const BitmapFontT *font = listViewAutoCol(lv, col) ? listViewWidthFont(w) : NULL; + int32_t was = font ? listViewCellWidth(lv, font, row, col) : 0; + + if (col == lv->sortCol) { + listViewSortTake(lv, row, lv->rowCount); + } free(lv->ownedCells[idx]); lv->ownedCells[idx] = strdup(text ? text : ""); - lv->totalColW = 0; + + // A column's widest cell made narrower is resolved again; a wider one + // widens it + if (font && was >= lv->resolvedColW[col] && listViewCellWidth(lv, font, row, col) < was) { + lv->totalColW = 0; + } else { + listViewWidthsGrow(w, row); + } if (col == lv->sortCol) { - lv->sortDirty = true; + listViewSortPlace(lv, row); } wgtInvalidatePaint(w); @@ -1759,12 +1945,16 @@ static const ListViewApiT sApi = { .clear = wgtListViewClear, .getCell = wgtListViewGetCell, .setCell = wgtListViewSetCell, - .getRowCount = wgtListViewGetRowCount + .getRowCount = wgtListViewGetRowCount, + .getSortCol = wgtListViewGetSortCol, + .getSortDir = wgtListViewGetSortDir }; static const WgtPropDescT sProps[] = { - { "ListIndex", WGT_IFACE_INT, (void *)wgtListViewGetSelected, (void *)wgtListViewSetSelected, NULL } + { "ListIndex", WGT_IFACE_INT, (void *)wgtListViewGetSelected, (void *)wgtListViewSetSelected, NULL }, + { "SortColumn", WGT_IFACE_INT, (void *)wgtListViewGetSortCol, (void *)basSetSortColumn, NULL }, + { "SortOrder", WGT_IFACE_INT, (void *)wgtListViewGetSortDir, (void *)basSetSortOrder, NULL } }; static const WgtMethodDescT sMethods[] = { diff --git a/src/widgets/kpunch/progressBar/progress.h b/src/widgets/kpunch/progressBar/progress.h index e3c23cea..a258ebda 100644 --- a/src/widgets/kpunch/progressBar/progress.h +++ b/src/widgets/kpunch/progressBar/progress.h @@ -42,7 +42,9 @@ typedef struct { static inline const ProgressBarApiT *dvxProgressBarApi(void) { static const ProgressBarApiT *sApi; - if (!sApi) { sApi = (const ProgressBarApiT *)wgtGetApi("progressbar", sizeof(ProgressBarApiT)); } + if (!sApi) { + sApi = (const ProgressBarApiT *)wgtGetApi("progressbar", sizeof(ProgressBarApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/progressBar/progress.res b/src/widgets/kpunch/progressBar/progress.res index 0e0a0eec..fa338b12 100644 --- a/src/widgets/kpunch/progressBar/progress.res +++ b/src/widgets/kpunch/progressBar/progress.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon progress.bmp name text "ProgressBar" author text "Scott Duensing" diff --git a/src/widgets/kpunch/progressBar/widgetProgressBar.c b/src/widgets/kpunch/progressBar/widgetProgressBar.c index ebda18e1..5f1686b6 100644 --- a/src/widgets/kpunch/progressBar/widgetProgressBar.c +++ b/src/widgets/kpunch/progressBar/widgetProgressBar.c @@ -119,12 +119,16 @@ bool wgtProgressBarGetVertical(const WidgetT *w) { } +// The value the bar already shows changes nothing (no repaint). void wgtProgressBarSetValue(WidgetT *w, int32_t value) { VALIDATE_WIDGET_VOID(w, sTypeId); - ProgressBarDataT *d = (ProgressBarDataT *)w->data; + ProgressBarDataT *d = (ProgressBarDataT *)w->data; + int32_t clamped = clampInt(value, 0, PROGRESSBAR_MAX_VALUE); - d->value = clampInt(value, 0, PROGRESSBAR_MAX_VALUE); - wgtInvalidatePaint(w); + if (clamped != d->value) { + d->value = clamped; + wgtInvalidatePaint(w); + } } diff --git a/src/widgets/kpunch/radio/radio.bhs b/src/widgets/kpunch/radio/radio.bhs index e616073f..96005395 100644 --- a/src/widgets/kpunch/radio/radio.bhs +++ b/src/widgets/kpunch/radio/radio.bhs @@ -25,6 +25,7 @@ .toc 1 OptionButton .index OptionButton .index Radio Button +.index GetSelected .index SetSelected .index OptionIndex @@ -49,6 +50,7 @@ A radio button for mutually exclusive choices. OptionButtons placed in the same .table Method Parameters Description ----------- ----------------- ------------------------------------------- + GetSelected (none) Returns the index (as OptionIndex counts) of the selected OptionButton of this button's container, or -1 when none is: one call instead of reading every button's Value. SetSelected Index As Integer Select the OptionButton at the given index within this button's container. .endtable diff --git a/src/widgets/kpunch/radio/radio.dhs b/src/widgets/kpunch/radio/radio.dhs index 56ae3636..20932f77 100644 --- a/src/widgets/kpunch/radio/radio.dhs +++ b/src/widgets/kpunch/radio/radio.dhs @@ -95,6 +95,7 @@ Caption is managed via the standard wgtSetText() / wgtGetText() interface (WCLAS .table Method Description ------ ----------- + GetSelected() The index of the selected radio button of this button's group, -1 when none (called on a group, wgtRadioGroupGetSelected). SetSelected idx% Select the radio button at index idx% within this button's group (called on a group, within that group). .endtable diff --git a/src/widgets/kpunch/radio/radio.h b/src/widgets/kpunch/radio/radio.h index 877bc737..3c119bdf 100644 --- a/src/widgets/kpunch/radio/radio.h +++ b/src/widgets/kpunch/radio/radio.h @@ -48,7 +48,9 @@ typedef struct { static inline const RadioApiT *dvxRadioApi(void) { static const RadioApiT *sApi; - if (!sApi) { sApi = (const RadioApiT *)wgtGetApi("radio", sizeof(RadioApiT)); } + if (!sApi) { + sApi = (const RadioApiT *)wgtGetApi("radio", sizeof(RadioApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/radio/radio.res b/src/widgets/kpunch/radio/radio.res index 75ed5c94..d0629b3b 100644 --- a/src/widgets/kpunch/radio/radio.res +++ b/src/widgets/kpunch/radio/radio.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon radio.bmp name text "RadioButton" author text "Scott Duensing" diff --git a/src/widgets/kpunch/radio/widgetRadio.c b/src/widgets/kpunch/radio/widgetRadio.c index f7e0d7cb..c1ea7993 100644 --- a/src/widgets/kpunch/radio/widgetRadio.c +++ b/src/widgets/kpunch/radio/widgetRadio.c @@ -78,6 +78,7 @@ typedef struct { // Prototypes // ============================================================ +static int32_t basGetSelected(const WidgetT *w); static void basSetSelected(WidgetT *w, int32_t idx); static WidgetT *radioAtIndex(WidgetT *parent, int32_t idx); static int32_t radioIndexOf(const WidgetT *w); @@ -100,6 +101,14 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, c void wgtRegister(void); +// BASIC GetSelected(): called on an OptionButton, the index of the +// selected button of its container (RADIO_NONE when none is); called on +// a RadioGroup, wgtRadioGroupGetSelected. +static int32_t basGetSelected(const WidgetT *w) { + return wgtRadioGroupGetSelected(w->type == sRadioTypeId ? w->parent : w); +} + + // BASIC SetSelected: called on an OptionButton it selects the sibling // at idx within the button's container; called on a RadioGroup it acts // like wgtRadioGroupSetSelected. @@ -358,7 +367,8 @@ static const WgtPropDescT sProps[] = { }; static const WgtMethodDescT sMethods[] = { - { "SetSelected", WGT_SIG_INT, (void *)basSetSelected } + { "GetSelected", WGT_SIG_RET_INT, (void *)basGetSelected }, + { "SetSelected", WGT_SIG_INT, (void *)basSetSelected } }; static const WgtIfaceT sIface = { diff --git a/src/widgets/kpunch/richEdit/richEdit.h b/src/widgets/kpunch/richEdit/richEdit.h index 4c27a624..d25bf47c 100644 --- a/src/widgets/kpunch/richEdit/richEdit.h +++ b/src/widgets/kpunch/richEdit/richEdit.h @@ -81,7 +81,9 @@ typedef struct { static inline const RichEditApiT *dvxRichEditApi(void) { static const RichEditApiT *sApi; - if (!sApi) { sApi = (const RichEditApiT *)wgtGetApi("richedit", sizeof(RichEditApiT)); } + if (!sApi) { + sApi = (const RichEditApiT *)wgtGetApi("richedit", sizeof(RichEditApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/richEdit/richedit.bhs b/src/widgets/kpunch/richEdit/richedit.bhs index a5ed2386..18077b1b 100644 --- a/src/widgets/kpunch/richEdit/richedit.bhs +++ b/src/widgets/kpunch/richEdit/richedit.bhs @@ -73,15 +73,15 @@ Text (a common property) is the plain text, a line per paragraph; setting it rep .h2 Type-Specific Methods .table - Method Description - ------ ----------- - Undo Undo the last edit (typing a run of characters is one edit). Up to 100 edits are kept. - Redo Do an undone edit again. - Cut, Copy, Paste The clipboard. Text copied from a RichEdit pastes back into a RichEdit with its looks; text from elsewhere takes the look of the text typed next. - SelectAll Select all the text. - Find(text) Select the next place text is (any case), from the selection on and around past the end; returns where it starts, or -1. - LoadFile(path) Load a file: RTF when it is RTF, else plain text. Returns 0, or -1 when it cannot be read. - SaveFile(path) Save the text: as RTF when path ends in .rtf, else as plain text. Returns 0, or -1 when it cannot be written. + Method Description + ------ ----------- + Undo Undo the last edit (typing a run of characters is one edit). Up to 100 edits are kept. + Redo Do an undone edit again. + Cut, Copy, Paste The clipboard. Text copied from a RichEdit pastes back into a RichEdit with its looks; text from elsewhere takes the look of the text typed next. + SelectAll Select all the text. + Find(text[, matchCase]) Select the next place text is, from the selection on and around past the end; returns where it starts, or -1. Any case unless matchCase is True. + LoadFile(path) Load a file: RTF when it is RTF, else plain text. Returns 0, or -1 when it cannot be read. + SaveFile(path) Save the text: as RTF when path ends in .rtf, else as plain text. Returns 0, or -1 when it cannot be written. .endtable .h2 Type-Specific Events diff --git a/src/widgets/kpunch/richEdit/richedit.dhs b/src/widgets/kpunch/richEdit/richedit.dhs index bda53b44..6f9e21ef 100644 --- a/src/widgets/kpunch/richEdit/richedit.dhs +++ b/src/widgets/kpunch/richEdit/richedit.dhs @@ -88,7 +88,7 @@ The slots are the functions above in order: create, getRtf, setRtf, getSelRtf, s .h3 BASIC Interface -Properties: TextRTF and SelRTF (wgtRichEditGetRtf / SetRtf, GetSelRtf / SetSelRtf), SelStart, SelLength, SelText, SelBold, SelItalic, SelUnderline, SelStrikethru (one FONT_STYLE_* bit of SetSelStyle each), SelColor (a BASIC color; -1 for ForeColor), SelFontName and SelFontSize (wgtRichEditSetSelFont with the other part left alone), SelAlignment, and the read-only CanUndo and CanRedo. Methods: Undo, Redo, Cut, Copy, Paste, SelectAll, Find (any case), LoadFile, and SaveFile. Event: SelChange; default event Change. +Properties: TextRTF and SelRTF (wgtRichEditGetRtf / SetRtf, GetSelRtf / SetSelRtf), SelStart, SelLength, SelText, SelBold, SelItalic, SelUnderline, SelStrikethru (one FONT_STYLE_* bit of SetSelStyle each), SelColor (a BASIC color; -1 for ForeColor), SelFontName and SelFontSize (wgtRichEditSetSelFont with the other part left alone), SelAlignment, and the read-only CanUndo and CanRedo. Methods: Undo, Redo, Cut, Copy, Paste, SelectAll, Find (text$ and an optional matchCase, any case without it), LoadFile, and SaveFile. Event: SelChange; default event Change. .h3 Methods Used diff --git a/src/widgets/kpunch/richEdit/richedit.res b/src/widgets/kpunch/richEdit/richedit.res index ea6d7515..522371f1 100644 --- a/src/widgets/kpunch/richEdit/richedit.res +++ b/src/widgets/kpunch/richEdit/richedit.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon richedit.bmp name text "RichEdit" author text "Scott Duensing" diff --git a/src/widgets/kpunch/richEdit/widgetRichEdit.c b/src/widgets/kpunch/richEdit/widgetRichEdit.c index dda19cec..3d26b880 100644 --- a/src/widgets/kpunch/richEdit/widgetRichEdit.c +++ b/src/widgets/kpunch/richEdit/widgetRichEdit.c @@ -56,17 +56,19 @@ // ABI stamp checked by the loader (see DVX_ABI_VERSION). DVX_ABI_STAMP(richedit); -#define RICH_BORDER 2 // sunken bevel -#define RICH_PAD 2 // between the bevel and the text -#define RICH_CARET_W 1 -#define RICH_MIN_COLS 20 // the smallest size, in the control's font -#define RICH_MIN_ROWS 4 -#define RICH_UNDO_MAX 100 // undo steps kept -#define RICH_WHEEL_LINES 3 // lines a wheel notch scrolls -#define RICH_NONE (-1) -#define RICH_EXT_RTF ".rtf" -#define RICH_CHAR_NEWLINE '\n' -#define BAS_COLOR_DEFAULT (-1) // SetColor in BASIC: the control's ForeColor +#define RICH_BORDER 2 // sunken bevel +#define RICH_PAD 2 // between the bevel and the text +#define RICH_CARET_W 1 +#define RICH_MIN_COLS 20 // the smallest size, in the control's font +#define RICH_MIN_ROWS 4 +#define RICH_UNDO_MAX 100 // undo steps kept +#define RICH_WHEEL_LINES 3 // lines a wheel notch scrolls +#define RICH_NONE (-1) +#define RICH_EXT_RTF ".rtf" +#define RICH_CHAR_NEWLINE '\n' +#define RICH_FIND_ARGS_MIN 1 // Find text$ +#define RICH_FIND_ARGS_MAX 2 // Find text$, matchCase +#define BAS_COLOR_DEFAULT (-1) // SetColor in BASIC: the control's ForeColor // Ctrl+letter keys the box takes (KEY_CTRL_A ... are in dvxTypes.h) #define RICH_KEY_BOLD 0x02 // Ctrl+B @@ -150,7 +152,7 @@ static int32_t sLiveCount = 0; // RichEdit controls alive: the last one go // ============================================================ static void applyChange(WidgetT *w, const RichChangeT *change); -static int32_t basFind(WidgetT *w, const char *text); +static bool basFind(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret); static bool basGetSelBold(const WidgetT *w); static int32_t basGetSelColor(const WidgetT *w); static bool basGetSelItalic(const WidgetT *w); @@ -283,9 +285,19 @@ static void applyChange(WidgetT *w, const RichChangeT *change) { } -// Find in BASIC: any case -static int32_t basFind(WidgetT *w, const char *text) { - return wgtRichEditFind(w, text, false); +// Find(text$[, matchCase]) in BASIC (dynamic for the optional argument): +// any case unless matchCase is True +static bool basFind(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret) { + char buf[WGT_VALUE_TEXT_MAX]; + + if (argc < RICH_FIND_ARGS_MIN || argc > RICH_FIND_ARGS_MAX) { + return false; + } + + bool matchCase = argc == RICH_FIND_ARGS_MAX && wgtValueToBool(&args[1]); + + *ret = wgtValueInt(wgtRichEditFind(w, wgtValueToText(&args[0], buf, (int32_t)sizeof(buf)), matchCase)); + return true; } @@ -1961,7 +1973,7 @@ static const WgtPropDescT sProps[] = { static const WgtMethodDescT sMethods[] = { { "Copy", WGT_SIG_VOID, (void *)wgtRichEditCopy }, { "Cut", WGT_SIG_VOID, (void *)wgtRichEditCut }, - { "Find", WGT_SIG_RET_INT_STR, (void *)basFind }, + { "Find", WGT_SIG_DYNAMIC, (void *)basFind }, { "LoadFile", WGT_SIG_RET_INT_STR, (void *)wgtRichEditLoadFile }, { "Paste", WGT_SIG_VOID, (void *)wgtRichEditPaste }, { "Redo", WGT_SIG_VOID, (void *)wgtRichEditRedo }, diff --git a/src/widgets/kpunch/richLabel/richLbl.h b/src/widgets/kpunch/richLabel/richLbl.h index 8474cddd..38a826e6 100644 --- a/src/widgets/kpunch/richLabel/richLbl.h +++ b/src/widgets/kpunch/richLabel/richLbl.h @@ -52,7 +52,9 @@ typedef struct { static inline const RichLabelApiT *dvxRichLabelApi(void) { static const RichLabelApiT *sApi; - if (!sApi) { sApi = (const RichLabelApiT *)wgtGetApi("richlabel", sizeof(RichLabelApiT)); } + if (!sApi) { + sApi = (const RichLabelApiT *)wgtGetApi("richlabel", sizeof(RichLabelApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/richLabel/richlbl.res b/src/widgets/kpunch/richLabel/richlbl.res index ea71c49c..4c4b311a 100644 --- a/src/widgets/kpunch/richLabel/richlbl.res +++ b/src/widgets/kpunch/richLabel/richlbl.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon richlbl.bmp name text "RichLabel" author text "Scott Duensing" diff --git a/src/widgets/kpunch/scrollPane/scrlPane.h b/src/widgets/kpunch/scrollPane/scrlPane.h index 948dd761..fb85544e 100644 --- a/src/widgets/kpunch/scrollPane/scrlPane.h +++ b/src/widgets/kpunch/scrollPane/scrlPane.h @@ -44,7 +44,9 @@ typedef struct { static inline const ScrollPaneApiT *dvxScrollPaneApi(void) { static const ScrollPaneApiT *sApi; - if (!sApi) { sApi = (const ScrollPaneApiT *)wgtGetApi("scrollpane", sizeof(ScrollPaneApiT)); } + if (!sApi) { + sApi = (const ScrollPaneApiT *)wgtGetApi("scrollpane", sizeof(ScrollPaneApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/scrollPane/scrlpane.res b/src/widgets/kpunch/scrollPane/scrlpane.res index 6f481f28..dc3a4065 100644 --- a/src/widgets/kpunch/scrollPane/scrlpane.res +++ b/src/widgets/kpunch/scrollPane/scrlpane.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon scrlpane.bmp name text "ScrollPane" author text "Scott Duensing" diff --git a/src/widgets/kpunch/scrollPane/widgetScrollPane.c b/src/widgets/kpunch/scrollPane/widgetScrollPane.c index 18312f30..84cd0b0f 100644 --- a/src/widgets/kpunch/scrollPane/widgetScrollPane.c +++ b/src/widgets/kpunch/scrollPane/widgetScrollPane.c @@ -197,8 +197,11 @@ static bool spGetNoBorder(const WidgetT *w) { // The user moved the scroll position (it was oldV / oldH): clamp it to -// the content and, when it really moved, relayout and fire onChange last -// (the handler may destroy the pane). +// the content and, when it really moved, place the children again and +// fire onChange last (the handler may destroy the pane). Nothing changed +// size, so the window is not measured again: only this pane is laid out +// and repainted (a long help topic scrolls without measuring every +// paragraph). static void spMoved(WidgetT *w, const SpGeomT *g, int32_t oldV, int32_t oldH) { ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data; @@ -208,8 +211,8 @@ static void spMoved(WidgetT *w, const SpGeomT *g, int32_t oldV, int32_t oldH) { return; } - // The relayout repositions the children. - wgtInvalidate(w); + widgetScrollPaneLayout(w, wgtFont(w)); + wgtInvalidatePaint(w); widgetFireChange(w); } @@ -342,10 +345,10 @@ static const ScrollPaneApiT sApi = { static const WgtPropDescT sProps[] = { { "NoBorder", WGT_IFACE_BOOL, (void *)spGetNoBorder, (void *)spSetNoBorder, NULL }, - { "Padding", WGT_IFACE_INT, (void *)wgtGetPaddingPx, (void *)wgtSetPaddingPx, NULL }, + BOX_PADDING_PROP, { "ScrollLeft", WGT_IFACE_INT, (void *)wgtScrollPaneGetScrollLeft, (void *)wgtScrollPaneSetScrollLeft, NULL }, { "ScrollTop", WGT_IFACE_INT, (void *)wgtScrollPaneGetScrollTop, (void *)wgtScrollPaneSetScrollTop, NULL }, - { "Spacing", WGT_IFACE_INT, (void *)wgtGetSpacingPx, (void *)wgtSetSpacingPx, NULL } + BOX_SPACING_PROP }; static const WgtMethodDescT sMethods[] = { @@ -653,8 +656,11 @@ void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B // layout with the scroll offset) setClipRect(d, w->x + border, w->y + border, g.innerW, g.innerH); + // Children scrolled wholly out of view are passed over for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { - widgetPaintOne(c, d, ops, font, colors); + if (c->y < d->clipY + d->clipH && c->y + c->h > d->clipY && c->x < d->clipX + d->clipW && c->x + c->w > d->clipX) { + widgetPaintOne(c, d, ops, font, colors); + } } setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); diff --git a/src/widgets/kpunch/separator/separatr.bhs b/src/widgets/kpunch/separator/separatr.bhs index 1e3515dd..ac8a8490 100644 --- a/src/widgets/kpunch/separator/separatr.bhs +++ b/src/widgets/kpunch/separator/separatr.bhs @@ -26,13 +26,22 @@ .toc 1 Line .index Line .index Separator +.index Vertical .h1 Line VB Equivalent: Line -- DVX Widget: separator -A visual separator line. The underlying widget supports both horizontal and vertical orientations. A Line placed in a horizontal container (HBox, Toolbar) is vertical; anywhere else it is horizontal. +A visual separator line, horizontal or vertical. A Line placed in a horizontal container (HBox, Toolbar) starts vertical; anywhere else it starts horizontal. Set Vertical to turn it the other way, for example a vertical Line between two side-by-side groups inside a VBox. -No type-specific properties, events, or methods. +.h2 Type-Specific Properties + +.table + Property Type Description + -------- ------- ------------------------------------------- + Vertical Boolean True for a vertical line, False for a horizontal one. +.endtable + +No type-specific events or methods. .link ctrl.common.props Common Properties, Events, and Methods diff --git a/src/widgets/kpunch/separator/separatr.dhs b/src/widgets/kpunch/separator/separatr.dhs index 72bc72fc..52da833f 100644 --- a/src/widgets/kpunch/separator/separatr.dhs +++ b/src/widgets/kpunch/separator/separatr.dhs @@ -28,6 +28,8 @@ .index wgtSeparator .index wgtHSeparator .index wgtVSeparator +.index wgtSeparatorIsVertical +.index wgtSeparatorSetVertical .h2 Separator @@ -43,6 +45,8 @@ Header: widgets/separatr.h WidgetT *wgtSeparator(parent) Create a separator oriented from the parent: vertical in a horizontal container, horizontal otherwise. WidgetT *wgtHSeparator(parent) Create a horizontal separator line. WidgetT *wgtVSeparator(parent) Create a vertical separator line. + bool wgtSeparatorIsVertical(w) True for a vertical line. + void wgtSeparatorSetVertical(w, v) Turn the line vertical (true) or horizontal (false); the window lays out again. .endtable .h3 API Struct (wgtRegisterApi "separator") @@ -54,13 +58,19 @@ Header: widgets/separatr.h hSeparator wgtHSeparator vSeparator wgtVSeparator create wgtSeparator + isVertical wgtSeparatorIsVertical + setVertical wgtSeparatorSetVertical .endtable The BASIC Line control is registered as interface "line" with its own one-slot API (create = wgtSeparator). -.h3 Properties +.h3 Properties (BASIC Interface) -No widget-specific properties. +.table + Property Type Access Description + -------- ---- ------ ----------- + Vertical Boolean Read/Write The orientation (wgtSeparatorIsVertical / wgtSeparatorSetVertical). +.endtable .h3 Methods diff --git a/src/widgets/kpunch/separator/separatr.h b/src/widgets/kpunch/separator/separatr.h index 22baaba1..d788cf77 100644 --- a/src/widgets/kpunch/separator/separatr.h +++ b/src/widgets/kpunch/separator/separatr.h @@ -33,6 +33,8 @@ typedef struct { WidgetT *(*hSeparator)(WidgetT *parent); WidgetT *(*vSeparator)(WidgetT *parent); WidgetT *(*create)(WidgetT *parent); // orientation from the parent + bool (*isVertical)(const WidgetT *w); + void (*setVertical)(WidgetT *w, bool vertical); } SeparatorApiT; // Consumer side: the accessor and convenience macros. The widget's own @@ -41,13 +43,17 @@ typedef struct { static inline const SeparatorApiT *dvxSeparatorApi(void) { static const SeparatorApiT *sApi; - if (!sApi) { sApi = (const SeparatorApiT *)wgtGetApi("separator", sizeof(SeparatorApiT)); } + if (!sApi) { + sApi = (const SeparatorApiT *)wgtGetApi("separator", sizeof(SeparatorApiT)); + } return sApi; } -#define wgtSeparator(parent) dvxSeparatorApi()->create(parent) -#define wgtHSeparator(parent) dvxSeparatorApi()->hSeparator(parent) -#define wgtVSeparator(parent) dvxSeparatorApi()->vSeparator(parent) +#define wgtSeparator(parent) dvxSeparatorApi()->create(parent) +#define wgtHSeparator(parent) dvxSeparatorApi()->hSeparator(parent) +#define wgtVSeparator(parent) dvxSeparatorApi()->vSeparator(parent) +#define wgtSeparatorIsVertical(w) dvxSeparatorApi()->isVertical(w) +#define wgtSeparatorSetVertical(w, vert) dvxSeparatorApi()->setVertical(w, vert) #endif // SEPARATR_IMPL diff --git a/src/widgets/kpunch/separator/separatr.res b/src/widgets/kpunch/separator/separatr.res index d3ecc6b1..e014c50b 100644 --- a/src/widgets/kpunch/separator/separatr.res +++ b/src/widgets/kpunch/separator/separatr.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon separatr.bmp name text "Separator" author text "Scott Duensing" diff --git a/src/widgets/kpunch/separator/widgetSeparator.c b/src/widgets/kpunch/separator/widgetSeparator.c index b4a58d05..a348373f 100644 --- a/src/widgets/kpunch/separator/widgetSeparator.c +++ b/src/widgets/kpunch/separator/widgetSeparator.c @@ -71,6 +71,8 @@ typedef struct { static WidgetT *separatorCreate(WidgetT *parent, bool vertical); WidgetT *wgtHSeparator(WidgetT *parent); WidgetT *wgtSeparator(WidgetT *parent); +bool wgtSeparatorIsVertical(const WidgetT *w); +void wgtSeparatorSetVertical(WidgetT *w, bool vertical); WidgetT *wgtVSeparator(WidgetT *parent); void widgetSeparatorCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetSeparatorPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); @@ -109,6 +111,24 @@ WidgetT *wgtSeparator(WidgetT *parent) { } +bool wgtSeparatorIsVertical(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, false); + + return ((const SeparatorDataT *)w->data)->vertical; +} + + +// Turn the line across (BASIC Vertical): a vertical one in a VBox, a +// horizontal one in an HBox. Its minimum size changes, so the window +// lays out again. +void wgtSeparatorSetVertical(WidgetT *w, bool vertical) { + VALIDATE_WIDGET_VOID(w, sTypeId); + + ((SeparatorDataT *)w->data)->vertical = vertical; + wgtInvalidate(w); +} + + WidgetT *wgtVSeparator(WidgetT *parent) { return separatorCreate(parent, true); } @@ -159,10 +179,12 @@ static const WidgetClassT sClassSeparator = { }; static const SeparatorApiT sApi = { - .header = WGT_API_HEADER(SeparatorApiT), - .hSeparator = wgtHSeparator, - .vSeparator = wgtVSeparator, - .create = wgtSeparator + .header = WGT_API_HEADER(SeparatorApiT), + .hSeparator = wgtHSeparator, + .vSeparator = wgtVSeparator, + .create = wgtSeparator, + .isVertical = wgtSeparatorIsVertical, + .setVertical = wgtSeparatorSetVertical }; static const LineApiT sLineApi = { @@ -170,10 +192,14 @@ static const LineApiT sLineApi = { .create = wgtSeparator }; +static const WgtPropDescT sProps[] = { + { "Vertical", WGT_IFACE_BOOL, (void *)wgtSeparatorIsVertical, (void *)wgtSeparatorSetVertical, NULL } +}; + static const WgtIfaceT sIface = { .basName = "Line", - .props = NULL, - .propCount = 0, + .props = sProps, + .propCount = DVX_ARRAY_LEN(sProps), .methods = NULL, .methodCount = 0, .events = NULL, diff --git a/src/widgets/kpunch/slider/slider.h b/src/widgets/kpunch/slider/slider.h index 10639ab6..a948d8c4 100644 --- a/src/widgets/kpunch/slider/slider.h +++ b/src/widgets/kpunch/slider/slider.h @@ -39,7 +39,9 @@ typedef struct { static inline const SliderApiT *dvxSliderApi(void) { static const SliderApiT *sApi; - if (!sApi) { sApi = (const SliderApiT *)wgtGetApi("slider", sizeof(SliderApiT)); } + if (!sApi) { + sApi = (const SliderApiT *)wgtGetApi("slider", sizeof(SliderApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/slider/slider.res b/src/widgets/kpunch/slider/slider.res index fdc6f203..a02e932c 100644 --- a/src/widgets/kpunch/slider/slider.res +++ b/src/widgets/kpunch/slider/slider.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon slider.bmp name text "Slider" author text "Scott Duensing" diff --git a/src/widgets/kpunch/spacer/spacer.h b/src/widgets/kpunch/spacer/spacer.h index ef8b1f08..1fef631a 100644 --- a/src/widgets/kpunch/spacer/spacer.h +++ b/src/widgets/kpunch/spacer/spacer.h @@ -37,7 +37,9 @@ typedef struct { static inline const SpacerApiT *dvxSpacerApi(void) { static const SpacerApiT *sApi; - if (!sApi) { sApi = (const SpacerApiT *)wgtGetApi("spacer", sizeof(SpacerApiT)); } + if (!sApi) { + sApi = (const SpacerApiT *)wgtGetApi("spacer", sizeof(SpacerApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/spacer/spacer.res b/src/widgets/kpunch/spacer/spacer.res index c51c2427..a48b8d7f 100644 --- a/src/widgets/kpunch/spacer/spacer.res +++ b/src/widgets/kpunch/spacer/spacer.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon spacer.bmp name text "Spacer" author text "Scott Duensing" diff --git a/src/widgets/kpunch/spinner/spinner.h b/src/widgets/kpunch/spinner/spinner.h index 75deff63..ad0c5c29 100644 --- a/src/widgets/kpunch/spinner/spinner.h +++ b/src/widgets/kpunch/spinner/spinner.h @@ -47,7 +47,9 @@ typedef struct { static inline const SpinnerApiT *dvxSpinnerApi(void) { static const SpinnerApiT *sApi; - if (!sApi) { sApi = (const SpinnerApiT *)wgtGetApi("spinner", sizeof(SpinnerApiT)); } + if (!sApi) { + sApi = (const SpinnerApiT *)wgtGetApi("spinner", sizeof(SpinnerApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/spinner/spinner.res b/src/widgets/kpunch/spinner/spinner.res index 2e768d07..56dafd9d 100644 --- a/src/widgets/kpunch/spinner/spinner.res +++ b/src/widgets/kpunch/spinner/spinner.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon spinner.bmp name text "Spinner" author text "Scott Duensing" diff --git a/src/widgets/kpunch/splitter/splitter.bhs b/src/widgets/kpunch/splitter/splitter.bhs index 35c91dd3..8e268d68 100644 --- a/src/widgets/kpunch/splitter/splitter.bhs +++ b/src/widgets/kpunch/splitter/splitter.bhs @@ -41,6 +41,14 @@ A resizable split pane. Holds exactly two child widgets separated by a draggable Vertical Boolean True (the default): panes side by side. False: one pane above the other. Can be set in the designer or at run time. .endtable +.h2 Type-Specific Events + +.table + Event Description + ----- ----------- + Change The user moved the divider: raised as it is dragged, Position giving where it is. Not raised for a Position set in code. +.endtable + Container: Yes No default event. diff --git a/src/widgets/kpunch/splitter/splitter.h b/src/widgets/kpunch/splitter/splitter.h index 69286cbf..2c0d32fe 100644 --- a/src/widgets/kpunch/splitter/splitter.h +++ b/src/widgets/kpunch/splitter/splitter.h @@ -41,7 +41,9 @@ typedef struct { static inline const SplitterApiT *dvxSplitterApi(void) { static const SplitterApiT *sApi; - if (!sApi) { sApi = (const SplitterApiT *)wgtGetApi("splitter", sizeof(SplitterApiT)); } + if (!sApi) { + sApi = (const SplitterApiT *)wgtGetApi("splitter", sizeof(SplitterApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/splitter/splitter.res b/src/widgets/kpunch/splitter/splitter.res index faa7ec67..bbea822d 100644 --- a/src/widgets/kpunch/splitter/splitter.res +++ b/src/widgets/kpunch/splitter/splitter.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon splitter.bmp name text "Splitter" author text "Scott Duensing" diff --git a/src/widgets/kpunch/statusBar/statBar.h b/src/widgets/kpunch/statusBar/statBar.h index 4b75e4f9..bfa6297b 100644 --- a/src/widgets/kpunch/statusBar/statBar.h +++ b/src/widgets/kpunch/statusBar/statBar.h @@ -37,7 +37,9 @@ typedef struct { static inline const StatusBarApiT *dvxStatusBarApi(void) { static const StatusBarApiT *sApi; - if (!sApi) { sApi = (const StatusBarApiT *)wgtGetApi("statusbar", sizeof(StatusBarApiT)); } + if (!sApi) { + sApi = (const StatusBarApiT *)wgtGetApi("statusbar", sizeof(StatusBarApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/statusBar/statbar.bhs b/src/widgets/kpunch/statusBar/statbar.bhs index da284c74..23393b93 100644 --- a/src/widgets/kpunch/statusBar/statbar.bhs +++ b/src/widgets/kpunch/statusBar/statbar.bhs @@ -24,6 +24,9 @@ .title StatusBar .toc 1 StatusBar .index StatusBar +.index Alignment +.index Padding +.index Spacing .h1 StatusBar @@ -31,10 +34,20 @@ VB Equivalent: StatusBar -- DVX Widget: statusbar A horizontal container styled as a status bar, typically placed at the bottom of a form. Put Labels (or other controls) inside it as its panes: they sit side by side, each drawn with a sunken-panel border for the classic segmented status-bar look. Give the pane that should take the spare room Weight = 100. Panes can be nested in the designer or the .frm, or added at run time with CreateControl(form, "Label", name$, StatusBar1). -No type-specific properties, events, or methods. - Container: Yes +.h2 Type-Specific Properties + +.table + Property Type Description + --------- ------- ------------------------------------------- + Alignment Enum Where the panes sit along the bar when they do not fill it: Left (the default), Center, or Right. + Padding Integer Space in pixels between the edge of the bar and the panes: 1 unless set; 0 is none. + Spacing Integer Space in pixels between one pane and the next: 2 unless set; 0 is none. +.endtable + +No type-specific events or methods. + .h2 Example .code diff --git a/src/widgets/kpunch/statusBar/statbar.dhs b/src/widgets/kpunch/statusBar/statbar.dhs index 77c6ee76..495558a3 100644 --- a/src/widgets/kpunch/statusBar/statbar.dhs +++ b/src/widgets/kpunch/statusBar/statbar.dhs @@ -57,9 +57,19 @@ wgtLabel(sb, "Ready"); create wgtStatusBar .endtable -.h3 Properties, Methods, Events +.h3 Properties (BASIC Interface) -StatusBar has no widget-specific properties, methods, or events. Add child widgets (labels, progress bars, etc.) to populate. +.table + Property Type Access Description + -------- ---- ------ ----------- + Alignment Enum (Left, Center, Right) Read/Write Where the children sit along the bar when they do not fill it (the align field). + Padding Integer Read/Write The padding field in pixels (1 unless set; wgtGetPaddingPx / wgtSetPaddingPx). + Spacing Integer Read/Write The spacing field in pixels (2 unless set; wgtGetSpacingPx / wgtSetSpacingPx). +.endtable + +.h3 Methods, Events + +StatusBar has no widget-specific methods or events. Add child widgets (labels, progress bars, etc.) to populate. .h3 Default Event diff --git a/src/widgets/kpunch/statusBar/statbar.res b/src/widgets/kpunch/statusBar/statbar.res index c79ecd49..47346558 100644 --- a/src/widgets/kpunch/statusBar/statbar.res +++ b/src/widgets/kpunch/statusBar/statbar.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon statbar.bmp name text "StatusBar" author text "Scott Duensing" diff --git a/src/widgets/kpunch/statusBar/widgetStatusBar.c b/src/widgets/kpunch/statusBar/widgetStatusBar.c index 023ce725..f58f0f2a 100644 --- a/src/widgets/kpunch/statusBar/widgetStatusBar.c +++ b/src/widgets/kpunch/statusBar/widgetStatusBar.c @@ -43,6 +43,7 @@ #include "dvxWgtP.h" #include "statBar.h" #include "../box/boxMetrics.h" +#include "../label/alignNames.h" // ABI stamp checked by the loader (see DVX_ABI_VERSION). DVX_ABI_STAMP(statbar); @@ -89,10 +90,16 @@ static const StatusBarApiT sApi = { .create = wgtStatusBar }; +static const WgtPropDescT sProps[] = { + BOX_ALIGN_PROP, + BOX_PADDING_PROP, + BOX_SPACING_PROP +}; + static const WgtIfaceT sIface = { .basName = "StatusBar", - .props = NULL, - .propCount = 0, + .props = sProps, + .propCount = DVX_ARRAY_LEN(sProps), .methods = NULL, .methodCount = 0, .events = NULL, diff --git a/src/widgets/kpunch/tabControl/tabCtrl.h b/src/widgets/kpunch/tabControl/tabCtrl.h index faafb6ce..b82d7a18 100644 --- a/src/widgets/kpunch/tabControl/tabCtrl.h +++ b/src/widgets/kpunch/tabControl/tabCtrl.h @@ -40,7 +40,9 @@ typedef struct { static inline const TabControlApiT *dvxTabControlApi(void) { static const TabControlApiT *sApi; - if (!sApi) { sApi = (const TabControlApiT *)wgtGetApi("tabcontrol", sizeof(TabControlApiT)); } + if (!sApi) { + sApi = (const TabControlApiT *)wgtGetApi("tabcontrol", sizeof(TabControlApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/tabControl/tabctrl.bhs b/src/widgets/kpunch/tabControl/tabctrl.bhs index 3cb4ff33..4381b5c5 100644 --- a/src/widgets/kpunch/tabControl/tabctrl.bhs +++ b/src/widgets/kpunch/tabControl/tabctrl.bhs @@ -39,7 +39,7 @@ A tabbed container. Each tab page is a separate container that holds child contr .table Property Type Description -------- ------- ------------------------------------------- - TabIndex Integer Index of the active tab (0-based). Note: this property name collides with the common VB-compatibility TabIndex property, which shadows it at runtime. Use the SetActive method instead to switch tabs. + TabIndex Integer Index of the active tab (0-based). Setting it switches tabs as SetActive does. .endtable .h2 Type-Specific Methods @@ -49,15 +49,19 @@ A tabbed container. Each tab page is a separate container that holds child contr ------ ----------- AddPage title$ Add a new tab page with the given title. GetActive() Returns the index of the active tab. - SetActive index% Switch to the tab at the given index. This is the recommended way to change tabs at runtime (the TabIndex property is shadowed by the common property handler). A negative index selects the first tab and an index past the last tab selects the last tab. Switching closes a drop-down list opened from the page being hidden and moves the focus to the tab strip if it was on a control of the page being hidden. The Click event is not raised for a switch made in code. + SetActive index% Switch to the tab at the given index (the same as setting TabIndex). A negative index selects the first tab and an index past the last tab selects the last tab. Switching closes a drop-down list opened from the page being hidden and moves the focus to the tab strip if it was on a control of the page being hidden. No event is raised for a switch made in code. +.endtable + +.h2 Type-Specific Events + +.table + Event Description + ----- ----------- + Change The user switched tabs, with the mouse or the keys (not raised for a switch made in code). GetActive (or TabIndex) says which tab is active now. .endtable Container: Yes Default Event: Click -.note warning -The TabIndex property is shadowed by the common property handler at runtime. Use the SetActive method to change tabs programmatically. -.endnote - .link ctrl.common.props Common Properties, Events, and Methods diff --git a/src/widgets/kpunch/tabControl/tabctrl.res b/src/widgets/kpunch/tabControl/tabctrl.res index 52f4b9b4..e58ce621 100644 --- a/src/widgets/kpunch/tabControl/tabctrl.res +++ b/src/widgets/kpunch/tabControl/tabctrl.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon tabctrl.bmp name text "TabStrip" icon24-2 icon tabctrl.bmp diff --git a/src/widgets/kpunch/textInput/textInpt.h b/src/widgets/kpunch/textInput/textInpt.h index 4b3d4992..8b4dab8a 100644 --- a/src/widgets/kpunch/textInput/textInpt.h +++ b/src/widgets/kpunch/textInput/textInpt.h @@ -83,7 +83,9 @@ typedef struct { static inline const TextInputApiT *dvxTextInputApi(void) { static const TextInputApiT *sApi; - if (!sApi) { sApi = (const TextInputApiT *)wgtGetApi("textinput", sizeof(TextInputApiT)); } + if (!sApi) { + sApi = (const TextInputApiT *)wgtGetApi("textinput", sizeof(TextInputApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/textInput/textinpt.bhs b/src/widgets/kpunch/textInput/textinpt.bhs index a918b527..6cc3cf5a 100644 --- a/src/widgets/kpunch/textInput/textinpt.bhs +++ b/src/widgets/kpunch/textInput/textinpt.bhs @@ -161,7 +161,7 @@ Its font must be fixed-pitch, every character the same width; the designer's Fon SetSyntaxMode mode$ Activate syntax highlighting. Modes: "dhs" (help source), "bas" (BASIC, coloured as the DVX BASIC IDE colours it), "custom" (the program colours each line in the Colorize event). Pass "" to disable. SetTabWidth width% Set the tab stop width in characters. SetUseTabChar useChar When True, Tab inserts a tab character; when False, inserts spaces. - Undo Take back the last change (Ctrl+Z); a second Undo puts it back. + Undo Take back the last change (Ctrl+Z; characters typed one after another are one change); a second Undo puts it back. .endtable Cut, Paste, and Undo do nothing in a read-only area. @@ -172,7 +172,7 @@ Cut, Paste, and Undo do nothing in a read-only area. Event Parameters Description ----------- ----------------------- ------------------------------------------- GutterClick LineNumber As Integer The line-number gutter (shown by SetShowLineNumbers) was clicked beside a line: toggle a breakpoint there. A click in the gutter does not move the cursor. - Colorize LineText As String, Colors As String With SetSyntaxMode "custom", for every line as it is drawn: Colors comes with one "0" for each character of LineText; set each to the digit of a syntax colour (vbSyntaxText 0 to vbSyntaxType 6, colours as SetSyntaxColor sets them). Characters it leaves out stay plain. It runs whenever lines are drawn, so it must be quick and must not change the area. + Colorize LineText As String, Colors As String With SetSyntaxMode "custom", for every line as it is drawn: Colors comes with one "0" for each character of LineText; set each to the digit of a syntax colour (vbSyntaxText 0 to vbSyntaxType 6, colours as SetSyntaxColor sets them). Characters it leaves out stay plain. It runs when a line is drawn with text it has not coloured lately (the colours it gave are kept with the text, so a line drawn again unchanged does not ask again), so it must be quick and must not change the area; its colours should depend only on LineText. SetSyntaxMode "custom" again colours every line afresh (after the program changes how it colours). .endtable .code diff --git a/src/widgets/kpunch/textInput/textinpt.dhs b/src/widgets/kpunch/textInput/textinpt.dhs index 2980bcd5..a53f21da 100644 --- a/src/widgets/kpunch/textInput/textinpt.dhs +++ b/src/widgets/kpunch/textInput/textinpt.dhs @@ -87,7 +87,7 @@ Header: widgets/textInpt.h bool wgtTextAreaReplaceSelection(w, needle, repl, caseSens, fwd) If the current selection matches needle, replace it, then find the next occurrence in direction fwd. Returns true if anything was replaced or found. void wgtTextAreaSetLineMarker(w, line, gutterRgb, backRgb) Mark 1-based line: a dot of gutterRgb (0xRRGGBB) in the line-number gutter and / or the line's background in backRgb; TEXTAREA_MARKER_NONE leaves a part out, both NONE removes the marker. Markers keep their line numbers when the text changes (the owner moves them). The DVX BASIC IDE marks breakpoints and the paused line this way. void wgtTextAreaClearLineMarkers(w) Remove every line marker. - void wgtTextAreaSetSyntaxMode(w, mode) Use a built-in colorizer: "bas" / "basic" (BASIC: the compiler's keyword list, date literals by the lexer's shape rule, &H / &O and exponent numbers, REM and ' comments; the IDE's code editor uses it), "dhs" / "help" (help source), or "custom" (each painted line goes to the "Colorize" event through wgtFireEvent: args LineText and Colors, a string of one digit per character, "0" = TEXT_SYNTAX_DEFAULT; the host writes the TEXT_SYNTAX_* digits back into Colors; the BASIC Colorize event); NULL or "" turns colouring off. + void wgtTextAreaSetSyntaxMode(w, mode) Use a built-in colorizer: "bas" / "basic" (BASIC: the compiler's keyword list, date literals by the lexer's shape rule, &H / &O and exponent numbers, REM and ' comments; the IDE's code editor uses it), "dhs" / "help" (help source), or "custom" (each painted line goes to the "Colorize" event through wgtFireEvent: args LineText and Colors, a string of one digit per character, "0" = TEXT_SYNTAX_DEFAULT; the host writes the TEXT_SYNTAX_* digits back into Colors; the BASIC Colorize event; the answers are kept with the line text, 64 lines, so a line drawn again unchanged does not fire it again, and setting the mode again drops them); NULL or "" turns colouring off. int32_t wgtTextAreaGetCursorLine(w) Get the current cursor line number. void wgtTextAreaSetGutterClickCallback(w, fn) Set a callback for gutter clicks (e.g. for breakpoint toggling). int32_t wgtTextAreaGetWordAtCursor(w, buf, bufSize) Copy the word under the cursor into buf. Returns its length. diff --git a/src/widgets/kpunch/textInput/textinpt.res b/src/widgets/kpunch/textInput/textinpt.res index c01b7efb..4f7faf3f 100644 --- a/src/widgets/kpunch/textInput/textinpt.res +++ b/src/widgets/kpunch/textInput/textinpt.res @@ -1,3 +1,25 @@ +# 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. + # textinpt.res -- Resources for TextBox and TextArea widgets # # TextBox (first registered, uses unsuffixed names) diff --git a/src/widgets/kpunch/textInput/widgetTextInput.c b/src/widgets/kpunch/textInput/widgetTextInput.c index 0a14ad86..e826ddeb 100644 --- a/src/widgets/kpunch/textInput/widgetTextInput.c +++ b/src/widgets/kpunch/textInput/widgetTextInput.c @@ -96,6 +96,14 @@ typedef struct { char maskChar; // password mode: shown for each character } TextInputDataT; +// A Colorize answer kept: a line's text and the colours the program gave +// it (one allocation, the text then the colours; NULL: an empty slot). +typedef struct { + uint32_t hash; + int32_t len; + char *text; +} TextAreaColorSlotT; + // One line marker of a TextArea: a gutter dot and / or a line background // (0xRRGGBB, TEXTAREA_MARKER_NONE for neither). typedef struct { @@ -125,6 +133,11 @@ typedef struct { void (*colorize)(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); void *colorizeCtx; + // The Colorize event's answers kept by line text (TEXTAREA_COLOR_SLOTS; + // NULL: none yet), so a line drawn again unchanged does not run the + // program's handler again + TextAreaColorSlotT *colorSlots; + // Line markers (breakpoint dots, the current line): stb_ds array, // painted by textAreaMarkerDecorator. TextAreaMarkerT *markers; @@ -175,6 +188,7 @@ typedef struct { } TextCreateApiT; #include "dvxDlg.h" +#include "dvxHash.h" #include "stb_ds_wrap.h" #include @@ -186,6 +200,7 @@ typedef struct { #define TEXTAREA_MODE_CUSTOM "custom" #define TEXTAREA_COLORIZE_ARGS 2 // Colorize LineText, Colors #define TEXTAREA_COLOR_DIGIT '0' // Colors: one digit (a TEXT_SYNTAX_* index) per character +#define TEXTAREA_COLOR_SLOTS 64 // Colorize answers kept (a power of two; slot = text hash) #define TEXTAREA_PAD 2 #define TEXTAREA_MIN_ROWS 4 #define TEXTAREA_MIN_COLS 20 @@ -247,6 +262,7 @@ static const char *basGetWordAtCursor(const WidgetT *w); static bool basIsTypeSuffix(char ch); static int basKeywordCmp(const void *key, const void *entry); static void basSetLineMarker(WidgetT *w, int32_t line, int32_t gutterColor, int32_t backColor); +static void colorSlotsFree(TextAreaDataT *ta); static void dhsColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); static void eventColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); static bool maskCharValid(char slot, char ch); @@ -544,6 +560,17 @@ static void basSetLineMarker(WidgetT *w, int32_t line, int32_t gutterColor, int3 } +// The Colorize answers kept go (the area's colouring changed, or it ends). +static void colorSlotsFree(TextAreaDataT *ta) { + for (int32_t i = 0; ta->colorSlots && i < TEXTAREA_COLOR_SLOTS; i++) { + free(ta->colorSlots[i].text); + } + + free(ta->colorSlots); + ta->colorSlots = NULL; +} + + // .dhs help source colorizer static void dhsColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx) { (void)ctx; @@ -630,11 +657,22 @@ static void dhsColorize(const char *line, int32_t lineLen, uint8_t *colors, void // (ctx is the TextArea) with Colors, one digit per character (all // TEXT_SYNTAX_DEFAULT), which the handler changes (ByRef) to the // TEXT_SYNTAX_* index of each character; anything else stays default. -// It runs for every painted line, so the handler must be quick. +// The answer is kept with the line's text (colorSlots), so a line drawn +// again unchanged -- scrolling, the caret blinking, typing on another +// line -- does not run the handler again. static void eventColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx) { static char *sLine = NULL; static char *sColors = NULL; static int32_t sCap = 0; + TextAreaDataT *ta = (TextAreaDataT *)((WidgetT *)ctx)->data; + uint32_t hash = dvxFnvBytes(DVX_FNV_BASIS, line, lineLen); + + TextAreaColorSlotT *slot = ta->colorSlots ? &ta->colorSlots[hash & (TEXTAREA_COLOR_SLOTS - 1)] : NULL; + + if (slot && slot->text && slot->hash == hash && slot->len == lineLen && memcmp(slot->text, line, (size_t)lineLen) == 0) { + memcpy(colors, slot->text + lineLen, (size_t)lineLen); + return; + } if (lineLen + 1 > sCap) { char *grownLine = (char *)realloc(sLine, (size_t)lineLen + 1); @@ -669,6 +707,23 @@ static void eventColorize(const char *line, int32_t lineLen, uint8_t *colors, vo colors[i] = (idx > TEXT_SYNTAX_DEFAULT && idx < TEXT_SYNTAX_MAX) ? (uint8_t)idx : TEXT_SYNTAX_DEFAULT; } + + // Kept for the next time this text is drawn (out of memory: not kept) + if (!ta->colorSlots) { + ta->colorSlots = (TextAreaColorSlotT *)calloc(TEXTAREA_COLOR_SLOTS, sizeof(TextAreaColorSlotT)); + } + + char *kept = ta->colorSlots ? (char *)malloc((size_t)lineLen * 2 + 1) : NULL; + + if (kept) { + slot = &ta->colorSlots[hash & (TEXTAREA_COLOR_SLOTS - 1)]; + memcpy(kept, line, (size_t)lineLen); + memcpy(kept + lineLen, colors, (size_t)lineLen); + free(slot->text); + slot->hash = hash; + slot->len = lineLen; + slot->text = kept; + } } @@ -1509,6 +1564,7 @@ void wgtTextAreaSetColorize(WidgetT *w, void (*fn)(const char *, int32_t, uint8_ TextAreaDataT *ta = (TextAreaDataT *)w->data; ta->colorize = fn; ta->colorizeCtx = ctx; + colorSlotsFree(ta); } @@ -1990,6 +2046,7 @@ void widgetTextAreaDestroy(WidgetT *w) { if (ta) { textEditMultiFree(&ta->te); arrfree(ta->markers); + colorSlotsFree(ta); free(ta->rawSyntax); free(ta->expandBuf); free(ta->syntaxBuf); diff --git a/src/widgets/kpunch/timer/timer.bhs b/src/widgets/kpunch/timer/timer.bhs index 94fc48c3..665ff176 100644 --- a/src/widgets/kpunch/timer/timer.bhs +++ b/src/widgets/kpunch/timer/timer.bhs @@ -25,6 +25,7 @@ .toc 1 Timer .index Timer .index Interval +.index Repeat .index Start .index Stop @@ -41,6 +42,7 @@ A non-visual control that fires its event at a regular interval. The Timer widge -------- ------- ------------------------------------------- Enabled Boolean True to start the timer, False to stop it. Interval Integer Timer interval in milliseconds. Setting it restarts the countdown. An Interval of 0 (or less) disables the Timer event. + Repeat Boolean True (the default) to fire every Interval; False for a one-shot timer that fires once and stops itself (Enabled turns False), until Start or Enabled = True runs it again. .endtable .h2 Type-Specific Methods diff --git a/src/widgets/kpunch/timer/timer.dhs b/src/widgets/kpunch/timer/timer.dhs index 3723f495..c91ac616 100644 --- a/src/widgets/kpunch/timer/timer.dhs +++ b/src/widgets/kpunch/timer/timer.dhs @@ -31,6 +31,8 @@ .index wgtTimerStop .index wgtTimerSetInterval .index wgtTimerIsRunning +.index wgtTimerGetRepeat +.index wgtTimerSetRepeat .index wgtUpdateTimers .h2 Timer @@ -57,6 +59,8 @@ tmr->onChange = onTimerTick; void wgtTimerSetInterval(w, intervalMs) Change the timer interval and restart the countdown. An interval of 0 or less never fires. int32_t wgtTimerGetInterval(w) Returns the current interval in milliseconds. bool wgtTimerIsRunning(w) Returns true if the timer is currently running. + bool wgtTimerGetRepeat(w) Returns true for a repeating timer, false for a one-shot. + void wgtTimerSetRepeat(w, repeat) Make the timer repeating or one-shot from its next fire on (a running countdown is kept). void wgtTimerSetEnabled(w, enabled) Start or stop the timer based on enabled flag. void wgtUpdateTimers(void) Global tick function. Called by the event loop to advance all active timers. .endtable @@ -73,6 +77,9 @@ tmr->onChange = onTimerTick; setInterval wgtTimerSetInterval isRunning wgtTimerIsRunning updateTimers wgtUpdateTimers + getInterval wgtTimerGetInterval + getRepeat wgtTimerGetRepeat + setRepeat wgtTimerSetRepeat testReset wgtTimerTestReset (present only in host test builds, DVX_HOST_TEST) .endtable @@ -91,6 +98,7 @@ tmr->onChange = onTimerTick; -------- ---- ------ ----------- Enabled Boolean Read/Write Whether the timer is running. Reading returns the running state; writing starts or stops it. Interval Integer Read/Write Timer interval in milliseconds. Setting it restarts the countdown; 0 or less disables firing. + Repeat Boolean Read/Write True (the default) for a repeating timer, False for a one-shot (wgtTimerGetRepeat / wgtTimerSetRepeat). .endtable .h3 Methods (BASIC Interface) diff --git a/src/widgets/kpunch/timer/timer.h b/src/widgets/kpunch/timer/timer.h index 87c5a6af..84e872f8 100644 --- a/src/widgets/kpunch/timer/timer.h +++ b/src/widgets/kpunch/timer/timer.h @@ -34,6 +34,9 @@ typedef struct { void (*setInterval)(WidgetT *w, int32_t intervalMs); bool (*isRunning)(const WidgetT *w); void (*updateTimers)(void); + int32_t (*getInterval)(const WidgetT *w); + bool (*getRepeat)(const WidgetT *w); + void (*setRepeat)(WidgetT *w, bool repeat); #ifdef DVX_HOST_TEST void (*testReset)(void); // host test harness only: clear the active list #endif @@ -45,7 +48,9 @@ typedef struct { static inline const TimerApiT *dvxTimerApi(void) { static const TimerApiT *sApi; - if (!sApi) { sApi = (const TimerApiT *)wgtGetApi("timer", sizeof(TimerApiT)); } + if (!sApi) { + sApi = (const TimerApiT *)wgtGetApi("timer", sizeof(TimerApiT)); + } return sApi; } @@ -54,6 +59,9 @@ static inline const TimerApiT *dvxTimerApi(void) { #define wgtTimerStop(w) dvxTimerApi()->stop(w) #define wgtTimerSetInterval(w, intervalMs) dvxTimerApi()->setInterval(w, intervalMs) #define wgtTimerIsRunning(w) dvxTimerApi()->isRunning(w) +#define wgtTimerGetInterval(w) dvxTimerApi()->getInterval(w) +#define wgtTimerGetRepeat(w) dvxTimerApi()->getRepeat(w) +#define wgtTimerSetRepeat(w, repeat) dvxTimerApi()->setRepeat(w, repeat) #define wgtUpdateTimers() do { const TimerApiT *_ta = dvxTimerApi(); if (_ta) { _ta->updateTimers(); } } while(0) #endif // TIMER_IMPL diff --git a/src/widgets/kpunch/timer/timer.res b/src/widgets/kpunch/timer/timer.res index 7a40ab13..6ca93555 100644 --- a/src/widgets/kpunch/timer/timer.res +++ b/src/widgets/kpunch/timer/timer.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon timer.bmp name text "Timer" author text "Scott Duensing" diff --git a/src/widgets/kpunch/timer/widgetTimer.c b/src/widgets/kpunch/timer/widgetTimer.c index 753384d0..8f39d74a 100644 --- a/src/widgets/kpunch/timer/widgetTimer.c +++ b/src/widgets/kpunch/timer/widgetTimer.c @@ -72,9 +72,11 @@ static void timerRemoveFromActiveList(WidgetT *w); static void timerSetInterval(TimerDataT *d, int32_t intervalMs); WidgetT *wgtTimer(WidgetT *parent, int32_t intervalMs, bool repeat); int32_t wgtTimerGetInterval(const WidgetT *w); +bool wgtTimerGetRepeat(const WidgetT *w); bool wgtTimerIsRunning(const WidgetT *w); void wgtTimerSetEnabled(WidgetT *w, bool enabled); void wgtTimerSetInterval(WidgetT *w, int32_t intervalMs); +void wgtTimerSetRepeat(WidgetT *w, bool repeat); void wgtTimerStart(WidgetT *w); void wgtTimerStop(WidgetT *w); #ifdef DVX_HOST_TEST @@ -183,6 +185,15 @@ int32_t wgtTimerGetInterval(const WidgetT *w) { } +// True for a timer that fires every interval, false for a one-shot (it +// stops itself before its event runs). +bool wgtTimerGetRepeat(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, false); + + return ((const TimerDataT *)w->data)->repeat; +} + + bool wgtTimerIsRunning(const WidgetT *w) { if (!w || w->type != sTypeId) { return false; @@ -211,6 +222,15 @@ void wgtTimerSetInterval(WidgetT *w, int32_t intervalMs) { } +// Repeating or one-shot from the next fire on; a running timer keeps its +// countdown. +void wgtTimerSetRepeat(WidgetT *w, bool repeat) { + VALIDATE_WIDGET_VOID(w, sTypeId); + + ((TimerDataT *)w->data)->repeat = repeat; +} + + void wgtTimerStart(WidgetT *w) { if (!w || w->type != sTypeId) { return; @@ -355,14 +375,18 @@ static const TimerApiT sApi = { .setInterval = wgtTimerSetInterval, .isRunning = wgtTimerIsRunning, .updateTimers = wgtUpdateTimers, + .getInterval = wgtTimerGetInterval, + .getRepeat = wgtTimerGetRepeat, + .setRepeat = wgtTimerSetRepeat, #ifdef DVX_HOST_TEST .testReset = wgtTimerTestReset #endif }; static const WgtPropDescT sProps[] = { - { "Enabled", WGT_IFACE_BOOL, (void *)wgtTimerIsRunning, (void *)wgtTimerSetEnabled, NULL }, - { "Interval", WGT_IFACE_INT, (void *)wgtTimerGetInterval, (void *)wgtTimerSetInterval, NULL } + { "Enabled", WGT_IFACE_BOOL, (void *)wgtTimerIsRunning, (void *)wgtTimerSetEnabled, NULL }, + { "Interval", WGT_IFACE_INT, (void *)wgtTimerGetInterval, (void *)wgtTimerSetInterval, NULL }, + { "Repeat", WGT_IFACE_BOOL, (void *)wgtTimerGetRepeat, (void *)wgtTimerSetRepeat, NULL } }; static const WgtMethodDescT sMethods[] = { diff --git a/src/widgets/kpunch/toolbar/toolbar.bhs b/src/widgets/kpunch/toolbar/toolbar.bhs index 663fe7fa..2dddc56a 100644 --- a/src/widgets/kpunch/toolbar/toolbar.bhs +++ b/src/widgets/kpunch/toolbar/toolbar.bhs @@ -25,6 +25,9 @@ .toc 1 Bar Controls .toc 1 Toolbar .index Toolbar +.index Alignment +.index Padding +.index Spacing .h1 Toolbar @@ -34,6 +37,16 @@ A horizontal container styled as a toolbar, with compact padding and spacing. Pl Container: Yes -No type-specific properties, events, or methods. +.h2 Type-Specific Properties + +.table + Property Type Description + --------- ------- ------------------------------------------- + Alignment Enum Where the controls sit along the toolbar when they do not fill it: Left (the default), Center, or Right. + Padding Integer Space in pixels between the edge of the toolbar and the controls: 2 unless set; 0 is none. + Spacing Integer Space in pixels between one control and the next: 2 unless set; 0 is none. +.endtable + +No type-specific events or methods. .link ctrl.common.props Common Properties, Events, and Methods diff --git a/src/widgets/kpunch/toolbar/toolbar.dhs b/src/widgets/kpunch/toolbar/toolbar.dhs index 18b9f580..251b4782 100644 --- a/src/widgets/kpunch/toolbar/toolbar.dhs +++ b/src/widgets/kpunch/toolbar/toolbar.dhs @@ -61,9 +61,19 @@ wgtImageButtonFromFile(tb, "icons/open.bmp"); getFace wgtToolbarGetFace .endtable -.h3 Properties, Methods, Events +.h3 Properties (BASIC Interface) -Toolbar has no widget-specific properties, methods, or events. Add children (buttons, separators, etc.) to populate. +.table + Property Type Access Description + -------- ---- ------ ----------- + Alignment Enum (Left, Center, Right) Read/Write Where the children sit along the bar when they do not fill it (the align field). + Padding Integer Read/Write The padding field in pixels (2 unless set; wgtGetPaddingPx / wgtSetPaddingPx). + Spacing Integer Read/Write The spacing field in pixels (2 unless set; wgtGetSpacingPx / wgtSetSpacingPx). +.endtable + +.h3 Methods, Events + +Toolbar has no widget-specific methods or events. Add children (buttons, separators, etc.) to populate. .h3 Default Event diff --git a/src/widgets/kpunch/toolbar/toolbar.h b/src/widgets/kpunch/toolbar/toolbar.h index e4c59c0f..4368f0cc 100644 --- a/src/widgets/kpunch/toolbar/toolbar.h +++ b/src/widgets/kpunch/toolbar/toolbar.h @@ -40,7 +40,9 @@ typedef struct { static inline const ToolbarApiT *dvxToolbarApi(void) { static const ToolbarApiT *sApi; - if (!sApi) { sApi = (const ToolbarApiT *)wgtGetApi("toolbar", sizeof(ToolbarApiT)); } + if (!sApi) { + sApi = (const ToolbarApiT *)wgtGetApi("toolbar", sizeof(ToolbarApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/toolbar/toolbar.res b/src/widgets/kpunch/toolbar/toolbar.res index 7b8370ed..bb6efdcd 100644 --- a/src/widgets/kpunch/toolbar/toolbar.res +++ b/src/widgets/kpunch/toolbar/toolbar.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon toolbar.bmp name text "Toolbar" author text "Scott Duensing" diff --git a/src/widgets/kpunch/toolbar/widgetToolbar.c b/src/widgets/kpunch/toolbar/widgetToolbar.c index b9ecdcc2..559eee1c 100644 --- a/src/widgets/kpunch/toolbar/widgetToolbar.c +++ b/src/widgets/kpunch/toolbar/widgetToolbar.c @@ -41,6 +41,7 @@ #include "dvxWgtP.h" #include "toolbar.h" #include "../box/boxMetrics.h" +#include "../label/alignNames.h" // ABI stamp checked by the loader (see DVX_ABI_VERSION). DVX_ABI_STAMP(toolbar); @@ -121,10 +122,16 @@ static const ToolbarApiT sApi = { .getFace = wgtToolbarGetFace }; +static const WgtPropDescT sProps[] = { + BOX_ALIGN_PROP, + BOX_PADDING_PROP, + BOX_SPACING_PROP +}; + static const WgtIfaceT sIface = { .basName = "Toolbar", - .props = NULL, - .propCount = 0, + .props = sProps, + .propCount = DVX_ARRAY_LEN(sProps), .methods = NULL, .methodCount = 0, .events = NULL, diff --git a/src/widgets/kpunch/treeView/treeView.dhs b/src/widgets/kpunch/treeView/treeView.dhs index e12b776a..f4093d9f 100644 --- a/src/widgets/kpunch/treeView/treeView.dhs +++ b/src/widgets/kpunch/treeView/treeView.dhs @@ -87,9 +87,13 @@ WidgetT *child = wgtTreeItem(root, "Child"); itemSetSelected wgtTreeItemSetSelected .endtable -.h3 Properties +.h3 Properties (BASIC Interface) -No widget-specific properties. +.table + Property Type Access Description + -------- ---- ------ ----------- + ListIndex Integer R/W Depth-first index of the current node, -1 when none. Setting it selects the node as a click would (Change fires). +.endtable .h3 Methods (BASIC Interface) @@ -99,12 +103,19 @@ No widget-specific properties. AddChildItem parentIdx%, text$ Add a child node under the node at the given index. AddItem text$ Add a root-level node. Clear Remove all nodes. + GetItemTag(index%) Returns the text SetItemTag stored on the node ("" when none). GetItemText(index%) Returns the text of the node at the given depth-first index. + HasChildren(index%) Returns True if the node has nodes under it. IsExpanded(index%) Returns True if the node is expanded. IsItemSelected(index%) Returns True if the node is selected. ItemCount() Returns the total number of nodes. + ItemDepth(index%) Returns how many nodes the node is under (0 at the root level, -1 for no such node). + ItemParent(index%) Returns the index of the node's parent (-1 at the root level or for no such node). + RemoveItem index% Remove the node and every node under it (destroys the item widgets). SetExpanded index%, expanded Expand or collapse the node (collapsing moves a hidden selection to the node). SetItemSelected index%, selected Select or deselect the node (single-select: sets or clears the selection). + SetItemTag index%, tag$ Keep a string with the node ("" frees it). + SetItemText index%, text$ Change the node's text. SetMultiSelect multi Enable or disable multi-selection. SetReorderable reorderable Enable or disable drag-to-reorder. .endtable diff --git a/src/widgets/kpunch/treeView/treeView.h b/src/widgets/kpunch/treeView/treeView.h index 0f751f20..483b2609 100644 --- a/src/widgets/kpunch/treeView/treeView.h +++ b/src/widgets/kpunch/treeView/treeView.h @@ -46,7 +46,9 @@ typedef struct { static inline const TreeViewApiT *dvxTreeViewApi(void) { static const TreeViewApiT *sApi; - if (!sApi) { sApi = (const TreeViewApiT *)wgtGetApi("treeview", sizeof(TreeViewApiT)); } + if (!sApi) { + sApi = (const TreeViewApiT *)wgtGetApi("treeview", sizeof(TreeViewApiT)); + } return sApi; } diff --git a/src/widgets/kpunch/treeView/treeview.bhs b/src/widgets/kpunch/treeView/treeview.bhs index fbc41e8a..3e77c2ae 100644 --- a/src/widgets/kpunch/treeView/treeview.bhs +++ b/src/widgets/kpunch/treeView/treeview.bhs @@ -29,6 +29,10 @@ .index SetItemTag .index GetItemTag .index HasChildren +.index ItemDepth +.index ItemParent +.index RemoveItem +.index SetItemText .h1 TreeView @@ -58,9 +62,13 @@ A hierarchical tree of expandable/collapsible nodes. Supports multi-select and d IsExpanded(index%) Returns True if the node at the given index is expanded. IsItemSelected(index%) Returns True if the node at the given index is selected. ItemCount() Returns the total number of nodes (all levels). + ItemDepth(index%) Returns how many nodes the node is under: 0 for a root-level node, 1 for its children, and so on (-1 when there is no such node). + ItemParent(index%) Returns the index of the node's parent, or -1 for a root-level node (or when there is no such node). + RemoveItem index% Remove the node and every node under it. The nodes after them move up. SetExpanded index%, expanded Expand or collapse the node at the given index. Collapsing a node that hides the current item moves the current item to that node. SetItemSelected index%, selected Select or deselect the node at the given index. Without multi-select this sets (or clears) the single selected node. SetItemTag index%, tag$ Keep any text with the node (like VB's Node.Tag): a file path, a key. "" removes it. + SetItemText index%, text$ Change the text of the node at the given index. SetMultiSelect multi Enable or disable multi-select mode. Turning it off clears the multi-selection. SetReorderable reorderable Enable or disable node reordering. .endtable diff --git a/src/widgets/kpunch/treeView/treeview.res b/src/widgets/kpunch/treeView/treeview.res index 7f579790..dad02306 100644 --- a/src/widgets/kpunch/treeView/treeview.res +++ b/src/widgets/kpunch/treeView/treeview.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon treeview.bmp name text "TreeView" author text "Scott Duensing" diff --git a/src/widgets/kpunch/treeView/widgetTreeView.c b/src/widgets/kpunch/treeView/widgetTreeView.c index cf65b651..e531a32c 100644 --- a/src/widgets/kpunch/treeView/widgetTreeView.c +++ b/src/widgets/kpunch/treeView/widgetTreeView.c @@ -87,6 +87,7 @@ DVX_ABI_STAMP(treeview); #define TREE_BORDER 2 #define TREE_MIN_ROWS 4 #define TREE_MIN_CHARS 6 +#define TREE_INDEX_MIN 32 // indexItems slots at least, once it grows by an added item // sbDragOrient values: which scrollbar thumb is being dragged. #define TREE_SB_VERT 0 @@ -122,6 +123,7 @@ typedef struct { const char *text; char *tag; // BASIC SetItemTag text (NULL = none) WidgetT *treeView; // owning treeView, cached at creation for destroy cleanup + int32_t flatIndex; // its place in the tree's indexItems while that is valid bool expanded; bool selected; } TreeItemDataT; @@ -140,9 +142,13 @@ static bool basHasChildren(const WidgetT *w, int32_t idx); static bool basIsExpanded(const WidgetT *w, int32_t idx); static bool basIsItemSelected(const WidgetT *w, int32_t idx); static int32_t basItemCount(const WidgetT *w); +static int32_t basItemDepth(const WidgetT *w, int32_t idx); +static int32_t basItemParent(const WidgetT *w, int32_t idx); +static void basRemoveItem(WidgetT *w, int32_t idx); static void basSetExpanded(WidgetT *w, int32_t idx, bool expanded); static void basSetItemSelected(WidgetT *w, int32_t idx, bool selected); static void basSetItemTag(WidgetT *w, int32_t idx, const char *tag); +static void basSetItemText(WidgetT *w, int32_t idx, const char *text); static void basSetListIndex(WidgetT *w, int32_t idx); static void basTreeCollect(WidgetT *parent, WidgetT **items, int32_t *count); static int32_t basTreeCountItems(WidgetT *parent); @@ -165,11 +171,13 @@ static void treeApplyScrollHit(WidgetT *w, int32_t orient, int32_t sbLen, static void treeCalcScrollbarNeeds(WidgetT *w, const BitmapFontT *font, int32_t *outTotalH, int32_t *outTotalW, int32_t *outInnerH, int32_t *outInnerW, bool *outNeedVSb, bool *outNeedHSb); static void treeClampScroll(WidgetT *w, int32_t totalH, int32_t totalW, int32_t innerH, int32_t innerW); static bool treeFireItemClick(WidgetT *item, bool preferDbl); +static void treeIndexInsert(WidgetT *item, WidgetT *after); static WidgetT *treeItemAtY(WidgetT *parent, int32_t targetY, int32_t *curY, const BitmapFontT *font); static bool treeItemHasChildren(const WidgetT *item); static bool treeItemInSubtree(WidgetT *item, WidgetT *candidate); static int32_t treeItemYPos(WidgetT *treeView, WidgetT *target, const BitmapFontT *font); static int32_t treeItemYPosHelper(WidgetT *parent, WidgetT *target, int32_t *curY, const BitmapFontT *font); +static WidgetT *treeLastItem(WidgetT *node); static void treeScrollItemIntoView(WidgetT *w, const BitmapFontT *font, WidgetT *item); static bool treeSetExpanded(WidgetT *item, bool expanded); static WidgetT *treeViewOf(WidgetT *w); @@ -272,13 +280,7 @@ static int32_t basGetListIndex(const WidgetT *w) { return -1; } - for (int32_t i = 0; i < tv->indexCount; i++) { - if (tv->indexItems[i] == tv->selectedItem) { - return i; - } - } - - return -1; + return ((const TreeItemDataT *)tv->selectedItem->data)->flatIndex; } @@ -312,6 +314,50 @@ static int32_t basItemCount(const WidgetT *w) { } +// ItemDepth(index%) -- how many items the item at index is under (0 for a +// root-level item, -1 for no such item) +static int32_t basItemDepth(const WidgetT *w, int32_t idx) { + WidgetT *item = basTreeItemAt((WidgetT *)w, idx); + + if (!item) { + return -1; + } + + int32_t depth = 0; + + for (WidgetT *p = item->parent; p && p->type == sTreeItemTypeId; p = p->parent) { + depth++; + } + + return depth; +} + + +// ItemParent(index%) -- the index of the item the item at index is under, +// -1 for a root-level item (or no such item) +static int32_t basItemParent(const WidgetT *w, int32_t idx) { + WidgetT *item = basTreeItemAt((WidgetT *)w, idx); + + if (!item || item->parent->type != sTreeItemTypeId) { + return -1; + } + + return ((const TreeItemDataT *)item->parent->data)->flatIndex; +} + + +// RemoveItem index% -- remove the item at index and every item under it +// (the items after them move up). A repaint is enough, as for AddItem. +static void basRemoveItem(WidgetT *w, int32_t idx) { + WidgetT *item = basTreeItemAt(w, idx); + + if (item) { + wgtDestroy(item); + wgtInvalidatePaint(w); + } +} + + // SetExpanded index%, expanded -- expand/collapse item at index static void basSetExpanded(WidgetT *w, int32_t idx, bool expanded) { WidgetT *item = basTreeItemAt(w, idx); @@ -348,6 +394,17 @@ static void basSetItemTag(WidgetT *w, int32_t idx, const char *tag) { } +// SetItemText index%, text$ -- change the text of the item at index +static void basSetItemText(WidgetT *w, int32_t idx, const char *text) { + WidgetT *item = basTreeItemAt(w, idx); + + if (item) { + widgetTreeItemSetText(item, text); + wgtInvalidatePaint(w); + } +} + + // ListIndex = index% -- make the item at index the current one, as a click // would (the tree fires Change); -1 or an index past the end clears it static void basSetListIndex(WidgetT *w, int32_t idx) { @@ -359,6 +416,7 @@ static void basSetListIndex(WidgetT *w, int32_t idx) { static void basTreeCollect(WidgetT *parent, WidgetT **items, int32_t *count) { for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) { if (c->type == sTreeItemTypeId) { + ((TreeItemDataT *)c->data)->flatIndex = *count; items[(*count)++] = c; basTreeCollect(c, items, count); } @@ -506,15 +564,14 @@ static WidgetT *firstVisibleChild(WidgetT *parent) { // Find the TreeView owning w (w itself when it is the TreeView) and mark -// its cached dimensions and BASIC index list invalid. This ensures the -// next treeCalcScrollbarNeeds call will recompute height and width. +// its cached dimensions invalid, so the next treeCalcScrollbarNeeds call +// recomputes height and width. The BASIC index list stays: an item's +// text is not its place. static void invalidateTreeDims(WidgetT *w) { WidgetT *tree = treeViewOf(w); if (tree) { - TreeViewDataT *tv = (TreeViewDataT *)tree->data; - tv->dimsFont.font = NULL; - tv->indexValid = false; + ((TreeViewDataT *)tree->data)->dimsFont.font = NULL; } } @@ -887,6 +944,50 @@ static bool treeFireItemClick(WidgetT *item, bool preferDbl) { } +// item was just added as its parent's last child, after the item after +// (its depth-first predecessor; NULL: first in the tree): into a valid +// index there at once, so a tree filled item by item -- each add followed +// by a SetItemTag, say -- does not walk the whole tree again per item. +static void treeIndexInsert(WidgetT *item, WidgetT *after) { + WidgetT *tree = treeViewOf(item); + + if (!tree) { + return; + } + + TreeViewDataT *tv = (TreeViewDataT *)tree->data; + + tv->dimsFont.font = NULL; + + if (!tv->indexValid) { + return; + } + + if (tv->indexCount == tv->indexCap) { + int32_t cap = tv->indexCap ? tv->indexCap * 2 : TREE_INDEX_MIN; + WidgetT **items = (WidgetT **)realloc(tv->indexItems, (size_t)cap * sizeof(WidgetT *)); + + if (!items) { + tv->indexValid = false; + return; + } + + tv->indexItems = items; + tv->indexCap = cap; + } + + int32_t pos = after ? ((TreeItemDataT *)after->data)->flatIndex + 1 : 0; + + memmove(&tv->indexItems[pos + 1], &tv->indexItems[pos], (size_t)(tv->indexCount - pos) * sizeof(WidgetT *)); + tv->indexItems[pos] = item; + tv->indexCount++; + + for (int32_t i = pos; i < tv->indexCount; i++) { + ((TreeItemDataT *)tv->indexItems[i]->data)->flatIndex = i; + } +} + + // Find the tree item at a given Y coordinate. On success *curY is the // found item's top. static WidgetT *treeItemAtY(WidgetT *parent, int32_t targetY, int32_t *curY, const BitmapFontT *font) { @@ -979,6 +1080,28 @@ static int32_t treeItemYPosHelper(WidgetT *parent, WidgetT *target, int32_t *cur } +// The last item of node's subtree in depth-first order (node itself when +// it holds none): down through each last item child. +static WidgetT *treeLastItem(WidgetT *node) { + for (;;) { + WidgetT *last = (node->lastChild && node->lastChild->type == sTreeItemTypeId) ? node->lastChild : NULL; + + // A child of another kind last: look through them all + for (WidgetT *c = last ? NULL : node->firstChild; c; c = c->nextSibling) { + if (c->type == sTreeItemTypeId) { + last = c; + } + } + + if (!last) { + return node; + } + + node = last; + } +} + + // Scroll vertically so item's row is inside the visible band. static void treeScrollItemIntoView(WidgetT *w, const BitmapFontT *font, WidgetT *item) { TreeViewDataT *tv = (TreeViewDataT *)w->data; @@ -1052,7 +1175,10 @@ static WidgetT *treeViewOf(WidgetT *w) { WidgetT *wgtTreeItem(WidgetT *parent, const char *text) { - WidgetT *w = widgetAlloc(parent, sTreeItemTypeId); + // The item the new one follows depth-first: the last of its parent's + // subtree (none when that is the tree itself, empty) + WidgetT *after = parent ? treeLastItem(parent) : NULL; + WidgetT *w = widgetAlloc(parent, sTreeItemTypeId); if (!w) { return NULL; @@ -1072,7 +1198,7 @@ WidgetT *wgtTreeItem(WidgetT *parent, const char *text) { ti->text = text ? strdup(text) : NULL; ti->treeView = treeViewOf(parent); w->data = ti; - invalidateTreeDims(w); + treeIndexInsert(w, (after && after->type == sTreeItemTypeId) ? after : NULL); return w; } @@ -1944,9 +2070,13 @@ static const WgtMethodDescT sMethods[] = { { "IsExpanded", WGT_SIG_RET_BOOL_INT, (void *)basIsExpanded }, { "IsItemSelected", WGT_SIG_RET_BOOL_INT, (void *)basIsItemSelected }, { "ItemCount", WGT_SIG_RET_INT, (void *)basItemCount }, + { "ItemDepth", WGT_SIG_RET_INT_INT, (void *)basItemDepth }, + { "ItemParent", WGT_SIG_RET_INT_INT, (void *)basItemParent }, + { "RemoveItem", WGT_SIG_INT, (void *)basRemoveItem }, { "SetExpanded", WGT_SIG_INT_BOOL, (void *)basSetExpanded }, { "SetItemSelected", WGT_SIG_INT_BOOL, (void *)basSetItemSelected }, { "SetItemTag", WGT_SIG_INT_STR, (void *)basSetItemTag }, + { "SetItemText", WGT_SIG_INT_STR, (void *)basSetItemText }, { "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtTreeViewSetMultiSelect }, { "SetReorderable", WGT_SIG_BOOL, (void *)wgtTreeViewSetReorderable }, }; diff --git a/src/widgets/kpunch/wrapBox/widgetWrapBox.c b/src/widgets/kpunch/wrapBox/widgetWrapBox.c index 9d5daff8..c5d7e897 100644 --- a/src/widgets/kpunch/wrapBox/widgetWrapBox.c +++ b/src/widgets/kpunch/wrapBox/widgetWrapBox.c @@ -69,8 +69,6 @@ void widgetWrapBoxGetLayoutMetrics(const WidgetT *w, const BitmapFont void widgetWrapBoxLayout(WidgetT *w, const BitmapFontT *font); void widgetWrapBoxPoll(WidgetT *w, WindowT *win); static WidgetT *wrapBoxCreate(WidgetT *parent); -static int32_t wrapBoxGetAlign(const WidgetT *w); -static void wrapBoxSetAlign(WidgetT *w, int32_t align); void wgtRegister(void); @@ -274,19 +272,10 @@ static WidgetT *wrapBoxCreate(WidgetT *parent) { } -static int32_t wrapBoxGetAlign(const WidgetT *w) { - return (int32_t)w->align; -} - - -static void wrapBoxSetAlign(WidgetT *w, int32_t align) { - w->align = (WidgetAlignE)align; - wgtInvalidate(w); -} - - static const WgtPropDescT sProps[] = { - { "Alignment", WGT_IFACE_ENUM, (void *)wrapBoxGetAlign, (void *)wrapBoxSetAlign, sAlignNames } + BOX_ALIGN_PROP, + BOX_PADDING_PROP, + BOX_SPACING_PROP }; diff --git a/src/widgets/kpunch/wrapBox/wrapBox.dhs b/src/widgets/kpunch/wrapBox/wrapBox.dhs index aa35b102..b4ade010 100644 --- a/src/widgets/kpunch/wrapBox/wrapBox.dhs +++ b/src/widgets/kpunch/wrapBox/wrapBox.dhs @@ -76,6 +76,8 @@ WrapBox uses the common WidgetT container fields for layout control: Property Type Access Description -------- ---- ------ ----------- Alignment Enum (Left, Center, Right) Read/Write Row alignment for rows that do not fill the full width. + Padding Integer Read/Write The padding field in pixels (wgtGetPaddingPx / wgtSetPaddingPx). + Spacing Integer Read/Write The spacing field in pixels (wgtGetSpacingPx / wgtSetSpacingPx). .endtable .h3 Methods diff --git a/src/widgets/kpunch/wrapBox/wrapbox.bhs b/src/widgets/kpunch/wrapBox/wrapbox.bhs index 3b25f5bc..46407d05 100644 --- a/src/widgets/kpunch/wrapBox/wrapbox.bhs +++ b/src/widgets/kpunch/wrapBox/wrapbox.bhs @@ -25,6 +25,8 @@ .toc 1 WrapBox .index WrapBox .index Flow Layout +.index Padding +.index Spacing .h1 WrapBox @@ -35,9 +37,11 @@ A container that arranges children in a flowing layout, wrapping to the next row .h2 Type-Specific Properties .table - Property Type Description - --------- ---- ------------------------------------------- - Alignment Enum Horizontal alignment of items: Left, Center, or Right. + Property Type Description + --------- ------- ------------------------------------------- + Alignment Enum Horizontal alignment of items: Left, Center, or Right. + Padding Integer Space in pixels between the edge of the box and the items: 2 unless set; 0 is none. + Spacing Integer Space in pixels between one item and the next, and between rows: 4 unless set; 0 is none. .endtable Container: Yes diff --git a/src/widgets/kpunch/wrapBox/wrapbox.res b/src/widgets/kpunch/wrapBox/wrapbox.res index 20303c56..cfced710 100644 --- a/src/widgets/kpunch/wrapBox/wrapbox.res +++ b/src/widgets/kpunch/wrapBox/wrapbox.res @@ -1,3 +1,25 @@ +# 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. + icon24 icon wrapbox.bmp name text "WrapBox" author text "Scott Duensing"