gnome-shell/js/misc/extensionUtils.js
Florian Müllner 74dcf99ea5 extensionSystem: Split out loadExtensionMetadata()
There is no need for sharing the functionality, but moving
the code into extensionUtils allows adding unit tests for it.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3379>
2024-10-16 16:16:08 +00:00

173 lines
4.4 KiB
JavaScript

// Common utils for the extension system, the extensions D-Bus service
// and the Extensions app
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
export const ExtensionType = {
SYSTEM: 1,
PER_USER: 2,
};
/**
* @enum {number}
*/
export const ExtensionState = {
ACTIVE: 1,
INACTIVE: 2,
ERROR: 3,
OUT_OF_DATE: 4,
DOWNLOADING: 5,
INITIALIZED: 6,
DEACTIVATING: 7,
ACTIVATING: 8,
// Used as an error state for operations on unknown extensions,
// should never be in a real extensionMeta object.
UNINSTALLED: 99,
};
const SERIALIZED_PROPERTIES = [
'type',
'state',
'enabled',
'path',
'error',
'hasPrefs',
'hasUpdate',
'canChange',
'sessionModes',
];
/**
* Serialize extension into an object that can be used
* in a vardict {GLib.Variant}
*
* @param {object} extension - an extension object
* @returns {object}
*/
export function serializeExtension(extension) {
let obj = {...extension.metadata};
SERIALIZED_PROPERTIES.forEach(prop => {
obj[prop] = extension[prop];
});
function packValue(val) {
let type;
switch (typeof val) {
case 'string':
type = 's';
break;
case 'number':
type = 'd';
break;
case 'boolean':
type = 'b';
break;
case 'object':
if (Array.isArray(val)) {
type = 'av';
val = val.map(v => packValue(v));
} else {
type = 'a{sv}';
let res = {};
for (let key in val) {
let packed = packValue(val[key]);
if (packed)
res[key] = packed;
}
val = res;
}
break;
default:
return null;
}
return GLib.Variant.new(type, val);
}
return packValue(obj).deepUnpack();
}
/**
* Deserialize an unpacked variant into an extension object
*
* @param {object} variant - an unpacked {GLib.Variant}
* @returns {object}
*/
export function deserializeExtension(variant) {
let res = {metadata: {}};
for (let prop in variant) {
let val = variant[prop].recursiveUnpack();
if (SERIALIZED_PROPERTIES.includes(prop))
res[prop] = val;
else
res.metadata[prop] = val;
}
// add the 2 additional properties to create a valid extension object, as createExtensionObject()
res.uuid = res.metadata.uuid;
res.dir = Gio.File.new_for_path(res.path);
return res;
}
/**
* Load extension metadata from directory
*
* @param {string} uuid of the extension
* @param {GioFile} dir to load metadata from
* @returns {object}
*/
export function loadExtensionMetadata(uuid, dir) {
const dirName = dir.get_basename();
if (dirName !== uuid)
throw new Error(`Directory name "${dirName}" does not match UUID "${uuid}"`);
const metadataFile = dir.get_child('metadata.json');
if (!metadataFile.query_exists(null))
throw new Error('Missing metadata.json');
let metadataContents, success_;
try {
[success_, metadataContents] = metadataFile.load_contents(null);
metadataContents = new TextDecoder().decode(metadataContents);
} catch (e) {
throw new Error(`Failed to load metadata.json: ${e}`);
}
let meta;
try {
meta = JSON.parse(metadataContents);
} catch (e) {
throw new Error(`Failed to parse metadata.json: ${e}`);
}
const requiredProperties = [{
prop: 'uuid',
typeName: 'string',
}, {
prop: 'name',
typeName: 'string',
}, {
prop: 'description',
typeName: 'string',
}, {
prop: 'shell-version',
typeName: 'string array',
typeCheck: v => Array.isArray(v) && v.length > 0 && v.every(e => typeof e === 'string'),
}];
for (let i = 0; i < requiredProperties.length; i++) {
const {
prop, typeName, typeCheck = v => typeof v === typeName,
} = requiredProperties[i];
if (!meta[prop])
throw new Error(`missing "${prop}" property in metadata.json`);
if (!typeCheck(meta[prop]))
throw new Error(`property "${prop}" is not of type ${typeName}`);
}
if (uuid !== meta.uuid)
throw new Error(`UUID "${meta.uuid}" from metadata.json does not match directory name "${uuid}"`);
return meta;
}