
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>
173 lines
4.4 KiB
JavaScript
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;
|
|
}
|