Files
luma-home/tv/read-app-icon.cjs
T
Kaloyan DanchevandClaude Opus 4.6 cbea5a322f
CI / Identify a merged version PR (push) Canceled after 0s
CI / Rust checks, rendering, and native package (push) Canceled after 0s
CI / Publish release and Homebrew feed (push) Canceled after 0s
Release PR / Maintain Version Packages PR (push) Canceled after 0s
Merge Luma launcher fork into monorepo
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>
2026-10-24 11:52:55 +03:00

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 }));