Move trampoline files (boot-hook, watcher, overlay) into trampoline/ subdirectory. Add upstream Luma v2.3.1 source at repo root with our additions: - Page::Ha (4th nav tab) with placeholder empty state - HA tab config schema and default entity mapping (ha-home/) - Nav keys renamed: "home" → "start", new "ha" tab between Apps and Settings - All state.rs/view.rs/main.rs/tests.rs updated for 4-tab navigation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
101 lines
3.1 KiB
JavaScript
101 lines
3.1 KiB
JavaScript
// Embedded in the native executable and run only through Homebrew's elevated
|
|
// exec service when jailed file reads fail. Compatible with webOS's Node 8.
|
|
var fs = require('fs');
|
|
var path = require('path');
|
|
var source = JSON.parse(process.argv[1]);
|
|
var roots = [
|
|
'/media/developer/apps/usr/palm/applications',
|
|
'/media/cryptofs/apps/usr/palm/applications',
|
|
'/usr/palm/applications'
|
|
];
|
|
var remaining = 512 * 1024;
|
|
var icons = [];
|
|
var seen = Object.create(null);
|
|
|
|
function read(file, limit) {
|
|
var fd;
|
|
try {
|
|
fd = fs.openSync(file, 'r');
|
|
var stat = fs.fstatSync(fd);
|
|
if (!stat.isFile() || stat.size > limit) return null;
|
|
var bytes = Buffer.alloc(stat.size);
|
|
var offset = 0;
|
|
while (offset < bytes.length) {
|
|
var count = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
if (!count) return null;
|
|
offset += count;
|
|
}
|
|
return bytes;
|
|
} catch (error) {
|
|
if (error.code !== 'ENOENT' && error.code !== 'EACCES') console.error(error.message);
|
|
return null;
|
|
} finally {
|
|
if (fd !== undefined) fs.closeSync(fd);
|
|
}
|
|
}
|
|
|
|
if (
|
|
typeof source.id !== 'string' ||
|
|
source.id.length > 128 ||
|
|
!/^[a-zA-Z0-9]+([._-][a-zA-Z0-9]+)*$/.test(source.id)
|
|
) {
|
|
throw new Error('Invalid application ID');
|
|
}
|
|
var directories = roots.map(function (root) {
|
|
return path.join(root, source.id);
|
|
});
|
|
if (
|
|
typeof source.directory === 'string' &&
|
|
roots.indexOf(path.dirname(source.directory)) >= 0 &&
|
|
source.directory.split('/').indexOf('..') < 0
|
|
) {
|
|
if (directories.indexOf(source.directory) >= 0)
|
|
directories.splice(directories.indexOf(source.directory), 1);
|
|
directories.unshift(source.directory);
|
|
}
|
|
directories.forEach(function (directory) {
|
|
var manifest = {};
|
|
var bytes = read(path.join(directory, 'appinfo.json'), 64 * 1024);
|
|
if (bytes) {
|
|
try {
|
|
manifest = JSON.parse(bytes.toString('utf8'));
|
|
if (!manifest || manifest.id !== source.id) manifest = {};
|
|
} catch (error) {
|
|
manifest = {};
|
|
console.error('Invalid app manifest: ' + error.message);
|
|
}
|
|
}
|
|
[source.large_icon, manifest.largeIcon, source.icon, manifest.icon, 'icon.png'].forEach(
|
|
function (icon) {
|
|
if (
|
|
typeof icon !== 'string' ||
|
|
!icon ||
|
|
icon.length > 4096 ||
|
|
icon.split('').some(function (character) {
|
|
var code = character.charCodeAt(0);
|
|
return code < 32 || code === 127;
|
|
})
|
|
)
|
|
return;
|
|
icon = icon.replace(/^file:\/\//, '');
|
|
if (icon.indexOf('://') >= 0) return;
|
|
if (icon.split('/').indexOf('..') >= 0) return;
|
|
var file = path.resolve(directory, icon);
|
|
if (['.png', '.jpg', '.jpeg', '.webp'].indexOf(path.extname(file).toLowerCase()) < 0) return;
|
|
if (
|
|
!roots.some(function (root) {
|
|
return file.indexOf(root + '/') === 0;
|
|
}) ||
|
|
seen[file]
|
|
)
|
|
return;
|
|
seen[file] = true;
|
|
var data = read(file, remaining);
|
|
if (!data || !data.length) return;
|
|
remaining -= data.length;
|
|
icons.push(data.toString('hex'));
|
|
}
|
|
);
|
|
});
|
|
console.log(JSON.stringify({ returnValue: true, icons: icons }));
|