From 25837b6ae9f911873168f92f16aeb37cbfcb465c Mon Sep 17 00:00:00 2001 From: minjaesong Date: Thu, 11 May 2023 21:05:31 +0900 Subject: [PATCH] lfs -c impl; tvdos app spec change --- assets/disk0/home/appexec.js | 22 +++++------ assets/disk0/tvdos/bin/lfs.js | 69 ++++++++++++++++++++++++++++++++++- tvdos_app_package.html | 22 +++++------ 3 files changed, 90 insertions(+), 23 deletions(-) diff --git a/assets/disk0/home/appexec.js b/assets/disk0/home/appexec.js index 64c5784..9a905e1 100644 --- a/assets/disk0/home/appexec.js +++ b/assets/disk0/home/appexec.js @@ -40,9 +40,9 @@ if (targetOS != 1 && targetOS != 0) { } -function strToInt48(str) { +function strToInt32(str) { let s = [...str].map(it=>it.charCodeAt(0)) - return ((4294967296 + (s[0] << 40)) + (s[1] << 32) + (s[2] << 24) + (s[3] << 16) + (s[4] << 8) + s[5]) - 4294967296 + return (s[0] << 24) + (s[1] << 16) + (s[2] << 8) + s[3] } function makeHash(length) { @@ -63,8 +63,8 @@ let sectionTable = [] let rodata = {} for (let i = 0; i < sectionCount; i++) { - let sectName = filebytes.substring(16 * (i+1), 16 * (i+1) + 10).trimNull() - let sectOffset = strToInt48(filebytes.substring(16 * (i+1) + 10, 16 * (i+1) + 16)) + let sectName = filebytes.substring(16 * (i+1), 16 * (i+1) + 12).trimNull() + let sectOffset = strToInt32(filebytes.substring(16 * (i+1) + 12, 16 * (i+1) + 16)) sectionTable.push([sectName, sectOffset]) } @@ -72,17 +72,17 @@ for (let i = 0; i < sectionTable.length - 1; i++) { let [sectName, sectOffset] = sectionTable[i] let nextSectOffset = sectionTable[i+1][1] - let uncompLen = strToInt48(filebytes.substring(sectOffset, sectOffset + 6)) - let compPayload = filebytes.substring(sectOffset + 6, nextSectOffset) + let uncompLen = strToInt32(filebytes.substring(sectOffset, sectOffset + 4)) + let compPayload = filebytes.substring(sectOffset + 4, nextSectOffset) if ("RODATA" == sectName) { let rodataPtr = 0 while (rodataPtr < nextSectOffset - sectOffset) { let labelLen = filebytes.charCodeAt(sectOffset + rodataPtr) let label = filebytes.substring(sectOffset + rodataPtr + 1, sectOffset + rodataPtr + 1 + labelLen) - let payloadLen = strToInt48(filebytes.substring(sectOffset + rodataPtr + 1 + labelLen, sectOffset + rodataPtr + 1 + labelLen + 6)) - let uncompLen = strToInt48(filebytes.substring(sectOffset + rodataPtr + 1 + labelLen + 6, sectOffset + rodataPtr + 1 + labelLen + 12)) - let sectPayload = filebytes.substring(sectOffset + rodataPtr + 1 + labelLen + 12, sectOffset + rodataPtr + 1 + labelLen + 12 + payloadLen) + let payloadLen = strToInt32(filebytes.substring(sectOffset + rodataPtr + 1 + labelLen, sectOffset + rodataPtr + 1 + labelLen + 4)) + let uncompLen = strToInt32(filebytes.substring(sectOffset + rodataPtr + 1 + labelLen + 4, sectOffset + rodataPtr + 1 + labelLen + 8)) + let sectPayload = filebytes.substring(sectOffset + rodataPtr + 1 + labelLen + 8, sectOffset + rodataPtr + 1 + labelLen + 8 + payloadLen) try { @@ -96,11 +96,11 @@ for (let i = 0; i < sectionTable.length - 1; i++) { decompFun(payload) - rodataPtr += 13 + labelLen + payloadLen + rodataPtr += 9 + labelLen + payloadLen } } else if ("TEXT" == sectName) { - let program = String.fromCharCode.apply(null, decompFun(compPayload)) + let program = btostr(decompFun(compPayload)) // inject RODATA map let rodataSnippet = `const __RODATA=Object.freeze(${JSON.stringify(rodata)});` diff --git a/assets/disk0/tvdos/bin/lfs.js b/assets/disk0/tvdos/bin/lfs.js index 86694ec..29015ae 100644 --- a/assets/disk0/tvdos/bin/lfs.js +++ b/assets/disk0/tvdos/bin/lfs.js @@ -9,7 +9,7 @@ To list the collected files: lfs -t`) } -const option = exec_args[1] +let option = exec_args[1] const lfsPath = exec_args[2] const dirPath = exec_args[3] @@ -19,3 +19,70 @@ if (option === undefined || lfsPath === undefined || option.toUpperCase() != "-T return 0 } +option = option.toUpperCase() + + +function recurseDir(file, action) { + if (!file.isDirectory) { + action(file) + } + else { + file.list().forEach(fd => { + recurseDir(fd, action) + }) + } + +} + +const lfsFile = files.open(_G.shell.resolvePathInput(lfsPath).full) +const rootDir = files.open(_G.shell.resolvePathInput(dirPath).full) + +const rootDirPathLen = rootDir.fullPath.length + +if ("-C" == option) { + if (!rootDir.exists) { + printerrln(`No such directory: ${rootDir.fullPath}`) + return 1 + } + + let out = "TVDOSLFS\x01\x00\x00\x00\x00\x00\x00\x00" + + recurseDir(rootDir, file=>{ + let f = files.open(file.fullPath) + let flen = f.size + let fname = file.fullPath.substring(rootDirPathLen + 1) + let plen = fname.length + + out += "\x01" + String.fromCharCode( + (plen >>> 8) & 255, + plen & 255 + ) + + out += fname + + out += String.fromCharCode( + (flen >>> 24) & 255, + (flen >>> 16) & 255, + (flen >>> 8) & 255, + flen & 255 + ) + + out += f.sread() + }) + + lfsFile.swrite(out) +} +else if ("T" == option || "-X" == option) { + if (!lfsFile.exists) { + printerrln(`No such file: ${lfsFile.fullPath}`) + return 1 + } + + + TODO() + +} +else { + printerrln("Unknown option: " + option) + return 2 +} \ No newline at end of file diff --git a/tvdos_app_package.html b/tvdos_app_package.html index 2c496a0..653d90e 100644 --- a/tvdos_app_package.html +++ b/tvdos_app_package.html @@ -666,19 +666,19 @@ blockquote { background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Crect%20x%3D%220.75%22%20y%3D%220.75%22%20width%3D%2214.5%22%20height%3D%2214.5%22%20fill%3D%22white%22%20stroke%3D%22%2336352F%22%20stroke-width%3D%221.5%22%2F%3E%0A%3C%2Fsvg%3E"); } -

TVDOS App Package

TVDOS App Package brings the convenience of the self-packaged executable to the TVDOS. Applications containing multiple resources, library dependencies, specific environment variables, etc. are compressed, self-contained, and upon execution, unpacked and presented to your application — all in a single .app file.

TVDOS App Package is built with following goals in mind:

  • Smaller file size than “everything spilled out”
  • All required libraries are self-contained
  • Has its own bootloader so that minimal extra code is required on DOS-side
  • Bootloader must be versatile enough that DOS code needs no change when more advanced features were added

How It Runs

TVDOS will recognise that the file is an App Package, by considering the .app extension and the file header. Once recognised, TVDOS will go through the following steps to present the packed app to the end user:

  1. Internally defines the path named MOUNT (typically under $:\TMP\)
  1. An empty filesystem is then created and mounted to the MOUNT
  1. Optional VDISK is unpacked to Program Memory and then mounted under the MOUNT\
  1. Resources defined in RODATA are loaded into the Scratchpad Memory
  1. Pointers to the loaded resources are injected as an JavaScript Object __RODATA, into the main executable code (format: __RODATA.mylabel = 123456)
  1. Injects patched files.open() which redirects any file references that are defined in the VDISK to the mounted VDISK
  1. Main executable code (“bootloader” for the App Package) is copied as MOUNT\run.com
  1. MOUNT\run.com is then read and called
  1. If any exceptions were caught or quit successfully, undoes any patching, unmounts itself, then returns the Errorlevel of the app to TVDOS

ᅟTVDOS App Package Format

Structure

Overview

Header Area
Section Table
VDISK
RODATA
TEXT

Header Area

OffsetTypeDescription
0\x7F A p PMagic (App Package)
4Uint8Endianness. 1 for Big
5Uint8Version. Always 1
6Uint8Section Compression. 0—None, 1—Gzip
7Uint8Number of Sections. Always greater than zero because of the ENDSECTION
8Uint8Target OS. 1—TVDOS, 0—Unspecified
9Byte[7]Padding bytes

Section Table

Repetition of:

Section Name -
(10 bytes, \x00 padded)
Offset -
(Uint48; 6 bytes)

Recognised section names:

  • VDISK — Virtual Disk. Extra libraries must be contained here
  • RODATA — Read-only Data. Typically graphical resources
  • TEXT — The Bootloader (run.com)
  • APPINFO — Desktop Entry text that contains information about the App
  • COPYING — Licence text
  • ENDSECTION — Marker for the end of the penultimate section. The offset is identical to the size of the entire App Package if no extra bytes (aka footers) are followed

The Section Structure (not RODATA)

Uncompressed Size -
(Uint48; 6 bytes)
Compressed Payload +

TVDOS App Package

TVDOS App Package brings the convenience of the self-packaged executable to TVDOS. Applications containing multiple resources, library dependencies, specific environment variables, etc. are compressed, self-contained, and upon execution, unpacked and presented to your application — all in a single .app file.

TVDOS App Package is built with following goals in mind:

  • Smaller file size than “everything spilled out”
  • All required libraries are self-contained
  • Has its own bootloader so that minimal extra code is required on DOS-side
  • Bootloader must be versatile enough that DOS code needs no change when more advanced features were added

How It Runs

TVDOS will recognise that the file is an App Package, by considering the .app extension and the file header. Once recognised, TVDOS will go through the following steps to present the packed app to the end user:

  1. Internally defines the path named MOUNT (typically under $:\TMP\)
  1. An empty filesystem is then created and mounted to the MOUNT
  1. Optional VDISK is unpacked to Program Memory and then mounted under the MOUNT\
  1. Resources defined in RODATA are loaded into the Scratchpad Memory
  1. Pointers to the loaded resources are injected as an JavaScript Object __RODATA, into the main executable code (format: __RODATA.mylabel = 123456)
  1. Injects patched files.open() which redirects any file references that are defined in the VDISK to the mounted VDISK
  1. Main executable code (“bootloader” for the App Package) is copied as MOUNT\run.com
  1. MOUNT\run.com is then read and called
  1. If any exceptions were caught or quit successfully, undoes any patching, unmounts itself, then returns the Errorlevel of the app to TVDOS

TVDOS App Package Format

Structure

Overview

Header Area
Section Table
VDISK
RODATA
TEXT

Header Area

OffsetTypeDescription
0\x7F A p PMagic (App Package)
4Uint8Endianness. 1 for Big
5Uint8Version. Always 1
6Uint8Section Compression. 0—None, 1—Gzip
7Uint8Number of Sections. Always greater than zero because of the ENDOFSECTION
8Uint8Target OS. 1—TVDOS, 0—Unspecified
9Byte[7]Padding bytes

Section Table

Repetition of:

Section Name +
(12 bytes, \x00 padded)
Offset +
(Uint32; 4 bytes)

Recognised section names:

  • VDISK — Virtual Disk (TVDOS LFS format). Extra libraries must be contained here
  • RODATA — Read-only Data. Typically graphical resources
  • TEXT — The Bootloader (run.com)
  • APPINFO — Desktop Entry text that contains information about the App
  • COPYING — Licence text
  • ENDOFSECTION — Marker for the end of the penultimate section. The offset is identical to the size of the entire App Package if no extra bytes (aka footers) are followed

The Section Structure (not RODATA)

Uncompressed Size +
(Uint48; 4 bytes)
Compressed Payload
(arbitrary size)

RODATA Section Structure

Repetition of:

Label Length
(1—255)
Label
(arbitrary size)
Compressed Size -
(Uint48; 6 bytes)
Uncompressed Size -
(Uint48; 6 bytes)
Payload -
(arbitrary size)
  • Compressed Size is equal to the size of the Payload block

ApP Devkit

The ApP Devkit allows the programmers to test their to-be-packaged apps before the actual packaging. All the required files are placed into a directory, and the Package Simulator will run the contents in the directory as if they were packed into the App Package.

Package Simulator Components

apprun.js

apprun is an executable provided by the Simulator that simulates the runtime for the App Package.

Synopsis: apprun dir\to\app\files

rodata.json

rodata.json simulates the RODATA map of the App Package, and is required to actually create the App Package. The map contains the simple list of paths to the assets. The load order is the same as the order in the text file.

This file must reside in the root directory of your app.

Example:

{
-	"splash": "A:\myapp\assets\splash.ipf",
-	"icons": "A:\myapp\assets\icons.bin",
-	"frame": "A:\myapp\assets\guiframe.bin",
-	"jingle": "A:\myapp\assets\jingle.mp2"
+    
(Uint32; 4 bytes)
Uncompressed Size +
(Uint32; 4 bytes)
Payload +
(arbitrary size)
  • Compressed Size is equal to the size of the Payload block

ApP Devkit

The ApP Devkit allows the programmers to test their to-be-packaged apps before the actual packaging. All the required files are placed into a directory, and the Package Simulator will run the contents in the directory as if they were packed into the App Package.

Directory Structure for Building App

  • (base)/rodata.json — RODATA specification. The actual assets for the RODATA can be placed anywhere, even on a separate disk; they don't need to exist on the app’s base directory
  • (base)/run.com — App entry point (“bootloader”)
  • (base)/vdisk/ — root directory for the VDISK. Any files under this directory will be compiled into a VDISK archive

Package Simulator Components

apprun.js

apprun is an executable provided by the Simulator that simulates the runtime for the App Package.

Synopsis: apprun dir\to\app\files

rodata.json

rodata.json simulates the RODATA map of the App Package, and is required to actually create the App Package. The map contains the simple list of paths to the assets. The load order is the same as the order in the text file.

This file must reside in the root directory of your app.

Example:

{
+	"splash": "B:\Pictures\birds.ipf",
+	"icons": "A:\home\myapp\assets\icons.bin",
+	"frame": "A:\home\myapp\assets\guiframe.bin",
+	"jingle": "C:\Music\jingle3.mp2"
 }

The key of the JSON object will be the label for finding the pointer to the loaded assets. In this example, the pointer where guiframe.bin is stored is found on __RODATA.frame in your app’s code.

run.com

run.com is an entry point and the bootloader for your app.

This file must reside in the root directory of your app.

\ No newline at end of file