blob: b1471f12ed314b80408074a4a38decb95820f2f8 [file] [log] [blame]
'use strict';
var IDB_NULL = Number.MIN_SAFE_INTEGER;
var IDB_FALSE = Number.MIN_SAFE_INTEGER + 1;
var IDB_TRUE = Number.MIN_SAFE_INTEGER + 2;
//
// IndexedDB only allows valid JS names in its index paths, whereas JSON allows
// for any string at all. This converts invalid JS names to valid ones, to allow
// for them to be indexed.
//
// For example, "foo-bar" is a valid JSON key, but cannot be a valid JS name
// (because that would be read as foo minus bar).
//
// Very high level rules for valid JS names are:
// - First character cannot start with a number
// - Otherwise all characters must be be a-z, A-Z, 0-9, $ or _.
// - We allow . unless the name represents a single field, as that represents
// a deep index path.
//
// This is more aggressive than it needs to be, but also simpler.
//
var KEY_INVALID = /[^a-zA-Z0-9_$]+|(^[^a-zA-Z_$])/g;
var PATH_INVALID = /(\\.)|[^a-zA-Z0-9_$.]+|(^[^a-zA-Z_$])/g;
var SLASH = '\\'.charCodeAt(0);
// These are the same as above but without the global flag
// we want to use RegExp.test because it's really fast, but the global flag
// makes the regex const stateful (seriously) as it walked through all instances
var TEST_KEY_INVALID = /[^a-zA-Z0-9_$]+|(^[^a-zA-Z_$])/;
var TEST_PATH_INVALID = /(\\.)|[^a-zA-Z0-9_$.]+|(^[^a-zA-Z_$])/;
function needsSanitise(name, isPath) {
if (isPath) {
return TEST_PATH_INVALID.test(name);
} else {
return TEST_KEY_INVALID.test(name);
}
}
function sanitise(name, isPath) {
var correctCharacters = function (match) {
var good = '';
for (var i = 0; i < match.length; i++) {
var code = match.charCodeAt(i);
// If you're sanitising a path, a slash character is there to be interpreted
// by whatever parses the path later as "escape the next thing".
//
// e.g., if you want to index THIS string:
// {"foo": {"bar.baz": "THIS"}}
// Your index path would be "foo.bar\.baz".
if (code === SLASH && isPath) {
continue;
}
good += '_c' + code + '_';
}
return good;
};
if (isPath) {
return name.replace(PATH_INVALID, correctCharacters);
} else {
return name.replace(KEY_INVALID, correctCharacters);
}
}
function needsRewrite(data) {
for (var key of Object.keys(data)) {
if (needsSanitise(key)) {
return true;
} else if (data[key] === null || typeof data[key] === 'boolean') {
return true;
} else if (typeof data[key] === 'object') {
return needsRewrite(data[key]);
}
}
}
function rewrite(data) {
if (!needsRewrite(data)) {
return false;
}
var isArray = Array.isArray(data);
var clone = isArray
? []
: {};
Object.keys(data).forEach(function (key) {
var safeKey = isArray ? key : sanitise(key);
if (data[key] === null) {
clone[safeKey] = IDB_NULL;
} else if (typeof data[key] === 'boolean') {
clone[safeKey] = data[key] ? IDB_TRUE : IDB_FALSE;
} else if (typeof data[key] === 'object') {
clone[safeKey] = rewrite(data[key]);
} else {
clone[safeKey] = data[key];
}
});
return clone;
}
export {
IDB_NULL,
IDB_TRUE,
IDB_FALSE,
rewrite,
sanitise
};