Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ function parseList(s, dest)

switch (installed) {
case 'installed':
pkg.installed = true;
pkg.status = ['installed'];
break;
}
break;
Expand Down Expand Up @@ -201,6 +201,48 @@ function parseList(s, dest)
}
}

function parseApkQueryJson(s, dest) {
// Parse the raw JSON text string into an array
const rawData = JSON.parse(s);

// Ensure our storage objects exist on the destination
dest.pkgs = dest.pkgs || {};
dest.providers = dest.providers || {};

// Single pass through each item in the new JSON array
for (const item of rawData) {
const { name, ...attributes } = item;

// Define the package object (merging name back in)
const pkg = { name, ...attributes };
// Add/Update the package in the main map
dest.pkgs[name] = pkg;

// Determine all provided names (a package always provides itself)
const provides = [name, ...(Array.isArray(pkg.provides) ? pkg.provides : [])];
for (const p of provides) {
dest.providers[p] = dest.providers[p] || [];
if (!dest.providers[p].includes(pkg))
dest.providers[p].push(pkg);
}
}
}

function parsePackageData(s, dest) {
if (!s) return;

// Check if the input is JSON (starts with [)
if (s.trim().charAt(0) === '[') {
parseApkQueryJson(s, dest);
} else {
parseList(s, dest);
}
}

function isPkgInstalled(pkg) {
return pkg && Array.isArray(pkg.status) && pkg.status.includes('installed');
}

function display(pattern)
{
const src = packages[currentDisplayMode === 'updates' ? 'installed' : currentDisplayMode];
Expand Down Expand Up @@ -249,7 +291,7 @@ function display(pattern)
const avail = packages.available.pkgs[name];
const inst = packages.installed.pkgs[name];

if (!inst || !inst.installed)
if (!isPkgInstalled(inst))
continue;

if (!avail || compareVersion(avail.version, pkg.version) <= 0)
Expand All @@ -267,7 +309,7 @@ function display(pattern)
}, _('Upgrade…'));
}
else if (currentDisplayMode === 'installed') {
if (!pkg.installed)
if (!isPkgInstalled(pkg))
continue;

ver = truncateVersion(pkg.version || '-');
Expand All @@ -282,14 +324,14 @@ function display(pattern)

ver = truncateVersion(pkg.version || '-');

if (!inst || !inst.installed)
if (!isPkgInstalled(inst))
btn = E('div', {
'class': 'btn cbi-button-action',
'data-package': name,
'data-action': 'install',
'click': handleInstall
}, _('Install…'));
else if (inst.installed && inst.version != pkg.version)
else if (isPkgInstalled(inst) && inst.version !== pkg.version)
btn = E('div', {
'class': 'btn cbi-button-positive',
'data-package': name,
Expand Down Expand Up @@ -420,46 +462,24 @@ function orderOf(c)
return c.charCodeAt(0) + 256;
}

function compareVersion(val, ref)
{
let vi = 0, ri = 0;
const isdigit = { 0:1, 1:1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1 };

val = val || '';
ref = ref || '';

if (val === ref)
return 0;

while (vi < val.length || ri < ref.length) {
let first_diff = 0;

while ((vi < val.length && !isdigit[val.charAt(vi)]) ||
(ri < ref.length && !isdigit[ref.charAt(ri)])) {
const vc = orderOf(val.charAt(vi)), rc = orderOf(ref.charAt(ri));
if (vc !== rc)
return vc - rc;
function compareVersion(val, ref) {
// 1. Split into parts by dots or any non-digit sequences
const vParts = (val || '').split(/[^0-9]+/);
const rParts = (ref || '').split(/[^0-9]+/);

vi++; ri++;
}

while (val.charAt(vi) === '0')
vi++;
// 2. Filter out empty strings caused by leading/trailing non-digits
const v = vParts.filter(x => x.length > 0);
const r = rParts.filter(x => x.length > 0);

while (ref.charAt(ri) === '0')
ri++;
const maxLen = Math.max(v.length, r.length);

while (isdigit[val.charAt(vi)] && isdigit[ref.charAt(ri)]) {
first_diff = first_diff || (val.charCodeAt(vi) - ref.charCodeAt(ri));
vi++; ri++;
}
for (let i = 0; i < maxLen; i++) {
// Convert to integer (base 10) to automatically ignore leading zeros
const vNum = parseInt(v[i] || 0, 10);
const rNum = parseInt(r[i] || 0, 10);

if (isdigit[val.charAt(vi)])
return 1;
else if (isdigit[ref.charAt(ri)])
return -1;
else if (first_diff)
return first_diff;
if (vNum > rNum) return 1;
if (vNum < rNum) return -1;
}

return 0;
Expand All @@ -485,7 +505,7 @@ function versionSatisfied(ver, ref, vop)
return r > 0;

case '=':
return r == 0;
return r === 0;
}

return false;
Expand All @@ -496,7 +516,7 @@ function pkgStatus(pkg, vop, ver, info)
info.errors = info.errors || [];
info.install = info.install || [];

if (pkg.installed) {
if (isPkgInstalled(pkg)) {
if (vop && !versionSatisfied(pkg.version, ver, vop)) {
let repl = null;

Expand Down Expand Up @@ -605,12 +625,17 @@ function renderDependencies(depends, info, flat)
if (deps[i] === 'libc')
continue;

if (deps[i].match(/^(.+?)\s+\((<=|>=|<<|>>|<|>|=)(.+?)\)/)) {
dep = RegExp.$1.trim();
vop = RegExp.$2.trim();
ver = RegExp.$3.trim();
}
else {
// This regex handles "name", "name>=ver", and "name (>=ver)"
const match = deps[i].match(/^([^><=~\s]+)\s*\(?([><=~]+)?\s*([^)]+)?\)?$/);

if (match) {
// Destructure the match array: [full_match, name, operator, version]
const [, matchedDep, matchedVop, matchedVer] = match;
dep = (matchedDep || '').trim();
vop = (matchedVop || '').trim() || null;
ver = (matchedVer || '').trim() || null;
} else {
// Fallback if the string is just a plain name with no operators
dep = deps[i].trim();
vop = ver = null;
}
Expand Down Expand Up @@ -701,16 +726,16 @@ function handleInstall(ev)
const i18n_packages = [];
let i18n_tree;

if (luci_basename && (luci_basename[1] != 'i18n' || luci_basename[2].indexOf('base-') === 0)) {
if (luci_basename && (luci_basename[1] !== 'i18n' || luci_basename[2].indexOf('base-') === 0)) {
let i18n_filter;

if (luci_basename[1] == 'i18n') {
if (luci_basename[1] === 'i18n') {
const basenames = [];

for (let pkgname in packages.installed.pkgs) {
const m = pkgname.match(/^luci-([^-]+)-(.+)$/);

if (m && m[1] != 'i18n')
if (m && m[1] !== 'i18n')
basenames.push(m[2]);
}

Expand All @@ -723,7 +748,7 @@ function handleInstall(ev)

if (i18n_filter) {
for (let pkgname in packages.available.pkgs)
if (pkgname != pkg.name && pkgname.match(i18n_filter))
if (pkgname !== pkg.name && pkgname.match(i18n_filter))
i18n_packages.push(pkgname);

const i18ncache = {};
Expand Down Expand Up @@ -876,9 +901,9 @@ function handleConfig(ev)
}

for (let i = 0; i < partials.length; i++) {
if (partials[i].type == 'file') {
if (partials[i].type === 'file') {
if (L.hasSystemFeature('apk')) {
if (partials[i].name == 'repositories')
if (partials[i].name === 'repositories')
files.push(base_dir + '/' + partials[i].name);
} else if (partials[i].name.match(/\.conf$/)) {
files.push(base_dir + '/' + partials[i].name);
Expand Down Expand Up @@ -1010,7 +1035,7 @@ function handlePkg(ev)

const argv = [ cmd ];

if (cmd == 'remove')
if (cmd === 'remove')
argv.push('--force-removal-of-dependent-packages')

if (rem && rem.checked)
Expand Down Expand Up @@ -1118,14 +1143,14 @@ function updateLists(data)

return (data ? Promise.resolve(data) : downloadLists()).then(function(data) {
const pg = document.querySelector('.cbi-progressbar');
const mount = L.toArray(data[0].filter(function(m) { return m.mount == '/' || m.mount == '/overlay' }))
const mount = L.toArray(data[0].filter(function(m) { return m.mount === '/' || m.mount === '/overlay' }))
.sort(function(a, b) { return a.mount > b.mount })[0] || { size: 0, free: 0 };

pg.firstElementChild.style.width = Math.floor(mount.size ? (100 / mount.size) * (mount.size - mount.free) : 100) + '%';
pg.setAttribute('title', _('%s used (%1024mB used of %1024mB, %1024mB free)').format(pg.firstElementChild.style.width, mount.size - mount.free, mount.size, mount.free));

parseList(data[1], packages.available);
parseList(data[2], packages.installed);
parsePackageData(data[1], packages.available);
parsePackageData(data[2], packages.installed);

for (let pkgname in packages.installed.pkgs)
if (pkgname.indexOf('luci-i18n-base-') === 0)
Expand Down Expand Up @@ -1169,28 +1194,28 @@ return view.extend({

E('div', { 'class': 'controls' }, [
E('div', {}, [
E('label', {}, _('Disk space') + ':'),
E('label', {'id': 'disk-space-label'}, _('Disk space') + ':'),
E('div', { 'class': 'cbi-progressbar', 'title': _('unknown') }, E('div', {}, [ '\u00a0' ]))
]),

E('div', {}, [
E('label', {}, _('Filter') + ':'),
E('label', {'id': 'filter-label'}, _('Filter') + ':'),
E('span', { 'class': 'control-group' }, [
E('input', { 'type': 'text', 'name': 'filter', 'placeholder': _('Type to filter…'), 'value': query, 'input': handleInput }),
E('button', { 'class': 'btn cbi-button', 'click': handleReset }, [ _('Clear') ])
])
]),

E('div', {}, [
E('label', {}, _('Download and install package') + ':'),
E('label', {'id': 'download-label'}, _('Download and install package') + ':'),
E('span', { 'class': 'control-group' }, [
E('input', { 'type': 'text', 'name': 'install', 'placeholder': _('Package name or URL…'), 'keydown': function(ev) { if (ev.keyCode === 13) handleManualInstall(ev) }, 'disabled': isReadonlyView }),
E('button', { 'class': 'btn cbi-button cbi-button-action', 'click': handleManualInstall, 'disabled': isReadonlyView }, [ _('OK') ])
])
]),

E('div', {}, [
E('label', {}, _('Actions') + ':'), ' ',
E('label', {'id': 'action-label'}, _('Actions') + ':'), ' ',
E('span', { 'class': 'control-group' }, [
E('button', { 'class': 'btn cbi-button-positive', 'data-command': 'update', 'click': handlePkg, 'disabled': isReadonlyView }, [ _('Update lists…') ]), ' ',
E('button', { 'class': 'btn cbi-button-action', 'click': handleUpload, 'disabled': isReadonlyView }, [ _('Upload Package…') ]), ' ',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ fi
case "$action" in
list-installed)
if [ $ipkg_bin = "apk" ]; then
$ipkg_bin list -I --full 2>/dev/null
$ipkg_bin query --fields all --format json --installed --from system \* 2>/dev/null
else
cat /usr/lib/opkg/status
fi
;;
list-available)
if [ $ipkg_bin = "apk" ]; then
$ipkg_bin list --full 2>/dev/null
$ipkg_bin query --fields all --format json --available \* 2>/dev/null
else
lists_dir=$(sed -rne 's#^lists_dir \S+ (\S+)#\1#p' /etc/opkg.conf /etc/opkg/*.conf 2>/dev/null | tail -n 1)
find "${lists_dir:-/usr/lib/opkg/lists}" -type f '!' -name '*.sig' | xargs -r gzip -cd
Expand All @@ -31,18 +31,12 @@ case "$action" in
(
cmd="$ipkg_bin"

# APK have command renamed
# The apk package manager uses distinct commands for installing and removing packages.
if [ $ipkg_bin = "apk" ]; then
case "$action" in
install)
action="add"
;;
update)
action="update"
;;
upgrade)
action="upgrade"
;;
remove)
action="del"
;;
Expand All @@ -54,6 +48,11 @@ case "$action" in
if [ $ipkg_bin = "apk" ]; then
while [ -n "$1" ]; do
case "$1" in
--autoremove)
# Just shift and do nothing.
# 'apk del' already cleans up orphaned dependencies by default.
shift
;;
--force-removal-of-dependent-packages)
cmd="$cmd -r"
shift
Expand Down
Loading