forked from FOSS/BangleApps
Data files: remove settings magic, some more sanitychecks
parent
9e0fd91339
commit
9f0adf1900
13
README.md
13
README.md
|
@ -263,9 +263,6 @@ and which gives information about the app for the Launcher.
|
|||
* tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher` or empty.
|
||||
* storage is used to identify the app files and how to handle them
|
||||
* data is used to clean up files when the app is uninstalled
|
||||
(If the app has settings but no data section, it is assumed settings are
|
||||
stored in `appid.settings.json`, so there is no need to add a data section
|
||||
containing only that file)
|
||||
|
||||
### `apps.json`: `custom` element
|
||||
|
||||
|
@ -351,10 +348,10 @@ Example `settings.js`
|
|||
```js
|
||||
// make sure to enclose the function in parentheses
|
||||
(function(back) {
|
||||
let settings = require('Storage').readJSON('app.settings.json',1)||{};
|
||||
let settings = require('Storage').readJSON('app.json',1)||{};
|
||||
function save(key, value) {
|
||||
settings[key] = value;
|
||||
require('Storage').write('app.settings.json',settings);
|
||||
require('Storage').write('app.json',settings);
|
||||
}
|
||||
const appMenu = {
|
||||
'': {'title': 'App Settings'},
|
||||
|
@ -367,13 +364,17 @@ Example `settings.js`
|
|||
E.showMenu(appMenu)
|
||||
})
|
||||
```
|
||||
In this example the app needs to add `app.settings.js` to `apps.json`:
|
||||
In this example the app needs to add `app.settings.js` to `storage` in `apps.json`.
|
||||
It should also add `app.json` to `data`, to make sure it is cleaned up when the app is uninstalled.
|
||||
```json
|
||||
{ "id": "app",
|
||||
...
|
||||
"storage": [
|
||||
...
|
||||
{"name":"app.settings.js","url":"settings.js"},
|
||||
],
|
||||
"data": [
|
||||
{"name":"app.json"}
|
||||
]
|
||||
},
|
||||
```
|
||||
|
|
|
@ -43,7 +43,22 @@ const APP_KEYS = [
|
|||
];
|
||||
const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate'];
|
||||
const DATA_KEYS = ['name', 'wildcard', 'storageFile'];
|
||||
const FORBIDDEN_FILE_NAME_CHARS = /[,;]/; // used as separators in appid.info
|
||||
|
||||
function globToRegex(pattern) {
|
||||
const ESCAPE = '.*+-?^${}()|[]\\';
|
||||
const regex = pattern.replace(/./g, c => {
|
||||
switch (c) {
|
||||
case '?': return '.';
|
||||
case '*': return '.*';
|
||||
default: return ESCAPE.includes(c) ? ('\\' + c) : c;
|
||||
}
|
||||
});
|
||||
return new RegExp('^'+regex+'$');
|
||||
}
|
||||
const isGlob = f => /[?*]/.test(f)
|
||||
// All storage+data files in all apps: {app:<appid>,[file:<storage.name> | data:<data.name|data.wildcard>]}
|
||||
let allFiles = [];
|
||||
apps.forEach((app,appIdx) => {
|
||||
if (!app.id) ERROR(`App ${appIdx} has no id`);
|
||||
//console.log(`Checking ${app.id}...`);
|
||||
|
@ -75,11 +90,13 @@ apps.forEach((app,appIdx) => {
|
|||
var fileNames = [];
|
||||
app.storage.forEach((file) => {
|
||||
if (!file.name) ERROR(`App ${app.id} has a file with no name`);
|
||||
if (file.name.includes('?') || file.name.includes('*'))
|
||||
ERROR(`App ${app.id} storage file ${file.name} contains wildcards`);
|
||||
if (isGlob(file.name)) ERROR(`App ${app.id} storage file ${file.name} contains wildcards`);
|
||||
let char = file.name.match(FORBIDDEN_FILE_NAME_CHARS)
|
||||
if (char) ERROR(`App ${app.id} storage file ${file.name} contains invalid character "${char[0]}"`)
|
||||
if (fileNames.includes(file.name))
|
||||
ERROR(`App ${app.id} file ${file.name} is a duplicate`);
|
||||
fileNames.push(file.name);
|
||||
allFiles.push({app: app.id, file: file.name});
|
||||
if (file.url) if (!fs.existsSync(appDir+file.url)) ERROR(`App ${app.id} file ${file.url} doesn't exist`);
|
||||
if (!file.url && !file.content && !app.custom) ERROR(`App ${app.id} file ${file.name} has no contents`);
|
||||
var fileContents = "";
|
||||
|
@ -124,14 +141,13 @@ apps.forEach((app,appIdx) => {
|
|||
if (dataNames.includes(data.name||data.wildcard))
|
||||
ERROR(`App ${app.id} data file ${data.name||data.wildcard} is a duplicate`);
|
||||
dataNames.push(data.name||data.wildcard)
|
||||
allFiles.push({app: app.id, data: (data.name||data.wildcard)});
|
||||
if ('name' in data && 'wildcard' in data)
|
||||
ERROR(`App ${app.id} data file ${data.name} has both name and wildcard`);
|
||||
if (data.name) {
|
||||
if (data.name.includes('?') || data.name.includes('*'))
|
||||
ERROR(`App ${app.id} data file name ${data.name} contains wildcards`);
|
||||
}
|
||||
if (isGlob(data.name))
|
||||
ERROR(`App ${app.id} data file name ${data.name} contains wildcards`);
|
||||
if (data.wildcard) {
|
||||
if (!data.wildcard.includes('?') && !data.wildcard.includes('*'))
|
||||
if (!isGlob(data.wildcard))
|
||||
ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not actually contains wildcard`);
|
||||
if (data.wildcard.replace(/\?|\*/g,'') === '')
|
||||
ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not contain regular characters`);
|
||||
|
@ -140,6 +156,8 @@ apps.forEach((app,appIdx) => {
|
|||
else if (!data.wildcard.includes(app.id))
|
||||
WARN(`App ${app.id} data file wildcard ${data.wildcard} does not include app ID`);
|
||||
}
|
||||
let char = (data.name||data.wildcard).match(FORBIDDEN_FILE_NAME_CHARS)
|
||||
if (char) ERROR(`App ${app.id} data file ${data.name||data.wildcard} contains invalid character "${char[0]}"`)
|
||||
if ('storageFile' in data && typeof data.storageFile !== 'boolean')
|
||||
ERROR(`App ${app.id} data file ${data.name||data.wildcard} has non-boolean value for "storageFile"`);
|
||||
for (const key in data) {
|
||||
|
@ -147,8 +165,24 @@ apps.forEach((app,appIdx) => {
|
|||
ERROR(`App ${app.id} data file ${data.name||data.wildcard} has unknown property "${key}"`);
|
||||
}
|
||||
});
|
||||
if (fileNames.includes(app.id+".settings.js") && dataNames.length===1 && dataNames[0] === app.id+'.settings.json')
|
||||
WARN(`App ${app.id} has settings, so does not need to declare data file ${app.id+'.settings.json'}`)
|
||||
// prefer "appid.json" over "appid.settings.json" (TODO: change to ERROR once all apps comply?)
|
||||
if (dataNames.includes(app.id+".settings.json") && !dataNames.includes(app.id+".json"))
|
||||
WARN(`App ${app.id} uses data file ${app.id+'.settings.json'} instead of ${app.id+'.json'}`)
|
||||
// settings files should be listed under data, not storage (TODO: change to ERROR once all apps comply?)
|
||||
if (fileNames.includes(app.id+".settings.json"))
|
||||
WARN(`App ${app.id} uses storage file ${app.id+'.settings.json'} instead of data file`)
|
||||
if (fileNames.includes(app.id+".json"))
|
||||
WARN(`App ${app.id} uses storage file ${app.id+'.json'} instead of data file`)
|
||||
// warn if storage file matches data file of same app
|
||||
dataNames.forEach(dataName=>{
|
||||
const glob = globToRegex(dataName)
|
||||
fileNames.forEach(fileName=>{
|
||||
if (glob.test(fileName)) {
|
||||
if (isGlob(dataName)) WARN(`App ${app.id} storage file ${fileName} matches data wildcard ${dataName}`)
|
||||
else WARN(`App ${app.id} storage file ${fileName} is also listed in data`)
|
||||
}
|
||||
})
|
||||
})
|
||||
//console.log(fileNames);
|
||||
if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`);
|
||||
if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`);
|
||||
|
@ -157,3 +191,20 @@ apps.forEach((app,appIdx) => {
|
|||
if (!APP_KEYS.includes(key)) ERROR(`App ${app.id} has unknown key ${key}`);
|
||||
}
|
||||
});
|
||||
// Do not allow files from different apps to collide
|
||||
let fileA
|
||||
while(fileA=allFiles.pop()) {
|
||||
const nameA = (fileA.file||fileA.data),
|
||||
globA = globToRegex(nameA),
|
||||
typeA = fileA.file?'storage':'data'
|
||||
allFiles.forEach(fileB => {
|
||||
const nameB = (fileB.file||fileB.data),
|
||||
globB = globToRegex(nameB),
|
||||
typeB = fileB.file?'storage':'data'
|
||||
if (globA.test(nameB)||globB.test(nameA)) {
|
||||
if (isGlob(nameA)||isGlob(nameB))
|
||||
ERROR(`App ${fileB.app} ${typeB} file ${nameB} matches app ${fileA.app} ${typeB} file ${nameA}`)
|
||||
else ERROR(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -69,19 +69,16 @@ var AppInfo = {
|
|||
var fileList = fileContents.map(storageFile=>storageFile.name);
|
||||
fileList.unshift(appJSONName); // do we want this? makes life easier!
|
||||
json.files = fileList.join(",");
|
||||
let data = {dataFiles: [], storageFiles: []};
|
||||
if ('data' in app) {
|
||||
let data = {dataFiles: [], storageFiles: []};
|
||||
// add "data" files to appropriate list
|
||||
app.data.forEach(d=>{
|
||||
if (d.storageFile) data.storageFiles.push(d.name||d.wildcard)
|
||||
else data.dataFiles.push(d.name||d.wildcard)
|
||||
})
|
||||
} else if (json.settings) {
|
||||
// settings but no data files: assume app uses <appid>.settings.json file
|
||||
data.dataFiles.push(app.id + '.settings.json')
|
||||
const dataString = AppInfo.makeDataString(data)
|
||||
if (dataString) json.data = dataString
|
||||
}
|
||||
const dataString = AppInfo.makeDataString(data)
|
||||
if (dataString) json.data = dataString
|
||||
fileContents.push({
|
||||
name : appJSONName,
|
||||
content : JSON.stringify(json)
|
||||
|
|
|
@ -101,17 +101,16 @@ removeApp : app => { // expects an appid.info structure (i.e. with `files`)
|
|||
cmds += app.files.split(',').map(file => `\x10s.erase(${toJS(file)});\n`).join("");
|
||||
// remove app Data: (dataFiles and storageFiles)
|
||||
const data = AppInfo.parseDataString(app.data)
|
||||
const isGlob = f => /[?*]/.test(f)
|
||||
// regular files, can use wildcards
|
||||
cmds += data.dataFiles.map(file => {
|
||||
const isGlob = (file.includes('*') || file.includes('?'))
|
||||
if (!isGlob) return `\x10s.erase(${toJS(file)});\n`;
|
||||
if (!isGlob(file)) return `\x10s.erase(${toJS(file)});\n`;
|
||||
const regex = new RegExp(globToRegex(file))
|
||||
return `\x10s.list(${regex}).forEach(f=>s.erase(f));\n`;
|
||||
}).join("");
|
||||
// storageFiles, can use wildcards
|
||||
cmds += data.storageFiles.map(file => {
|
||||
const isGlob = (file.includes('*') || file.includes('?'))
|
||||
if (!isGlob) return `\x10s.open(${toJS(file)},'r').erase();\n`;
|
||||
if (!isGlob(file)) return `\x10s.open(${toJS(file)},'r').erase();\n`;
|
||||
// storageFiles have a chunk number appended to their real name
|
||||
const regex = globToRegex(file+'\u0001')
|
||||
// open() doesn't want the chunk number though
|
||||
|
|
|
@ -355,9 +355,6 @@ function updateApp(app) {
|
|||
const removeData = (f) => !app.data.some(d => (d.name || d.wildcard)===f)
|
||||
data.dataFiles = data.dataFiles.filter(removeData)
|
||||
data.storageFiles = data.storageFiles.filter(removeData)
|
||||
} else if (remove.settings || app.settings) {
|
||||
// app with settings but no data files declared: keep <appid>.settings.json
|
||||
data.dataFiles = data.dataFiles.filter(f => f!==(app.id+'.settings.json'))
|
||||
}
|
||||
remove.data = AppInfo.makeDataString(data)
|
||||
return Comms.removeApp(remove);
|
||||
|
|
Loading…
Reference in New Issue