diff --git a/assets/disk0/home/appexec.js b/assets/disk0/home/appexec.js new file mode 100644 index 0000000..de81eb5 --- /dev/null +++ b/assets/disk0/home/appexec.js @@ -0,0 +1,114 @@ +const filepath = exec_args[1] + +if (!filepath) { + println(`Usage: appexec path/to/application.app`) + return 0 +} + +const file = files.open(_G.shell.resolvePathInput(filepath).full) + +if (!file.exists) { + println("File not found.") + return 1 +} + +if (file.isDirectory) { + println("Not an app file.") + return 2 +} + +const filebytes = file.sread() + + +// check magic +if (filebytes.substring(0,4) != "\x7FApP") { + println("Not an app file.") + return 2 +} + +const endianness = filebytes.charCodeAt(4) +const sectionComp = filebytes.charCodeAt(6) +const sectionCount = filebytes.charCodeAt(7) +const targetOS = filebytes.charCodeAt(8) + +const decompFun = (1 == sectionComp) ? (b) => gzip.decomp(b) : (b) => b +const decompToPtrFun = (1 == sectionComp) ? (b, target) => gzip.decompTo(b, target) : TODO() + +if (targetOS != 1 && targetOS != 0) { + println("App is not an TVDOS executable.") + return 3 +} + + +function strToInt48(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 + +} +function makeHash(length) { + let e = "YBNDRFG8EJKMCPQXOTLVWIS2A345H769" + let m = e.length + let s = "" + for (let i = 0; i < length; i++) { + s += e[Math.floor(Math.random()*m)] + } + return s +} + +const PATH_MOUNT = `$:/TMP/${makeHash(32)}/` + +// READ SECTIONS + +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)) + sectionTable.push([sectName, sectOffset]) +} + +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) + + 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) + + + try { + let ptr = sys.malloc(uncompLen) + decompToPtrFun(sectPayload, ptr) + rodata[label] = ptr + } + catch (e) { + rodata[label] = null + } + + decompFun(payload) + + rodataPtr += 13 + labelLen + payloadLen + } + } + else if ("TEXT" == sectName) { + let program = String.fromCharCode.apply(null, decompFun(compPayload)) + + // inject RODATA map + let rodataSnippet = `const __RODATA=Object.freeze(${JSON.stringify(rodata)});` + + files.open(PATH_MOUNT + "run.com").swrite(rodataSnippet+program) + } +} + +_G.shell.execute(PATH_MOUNT + "run.com") + +// TODO delete PATH_MOUNT \ No newline at end of file diff --git a/assets/disk0/tvdos/TVDOS.SYS b/assets/disk0/tvdos/TVDOS.SYS index 2876719..2c8043c 100644 --- a/assets/disk0/tvdos/TVDOS.SYS +++ b/assets/disk0/tvdos/TVDOS.SYS @@ -51,7 +51,7 @@ class PmemFSfile { // Javascript array OR JVM byte[] else if (Array.isArray(bytes) || bytes.toString().startsWith("[B")) { this.data = "" - for (let i = 0; i < array.length; i++) { + for (let i = 0; i < bytes.length; i++) { this.data += String.fromCharCode(bytes[i]) } } diff --git a/tvdos_app_package.html b/tvdos_app_package.html new file mode 100644 index 0000000..2c496a0 --- /dev/null +++ b/tvdos_app_package.html @@ -0,0 +1,684 @@ +TVDOS App Package

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:

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:

The Section Structure (not RODATA)

Uncompressed Size +
(Uint48; 6 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)

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"
+}

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