diff --git a/bin/genieacs-ext b/bin/genieacs-ext index 92405e8..a634fcc 100755 --- a/bin/genieacs-ext +++ b/bin/genieacs-ext @@ -40,13 +40,17 @@ function errorToFault(err: Error): Fault { }; if (err.stack) { - fault.detail.stack = err.stack; + fault.detail["stack"] = err.stack; // Trim the stack trace - const stackTrimIndex = fault.detail.stack.match( + const stackTrimIndex = fault.detail["stack"].match( /\s+at\s[^\s]+\s\(.*genieacs-ext:.+\)/ ); - if (stackTrimIndex) - fault.detail.stack = fault.detail.stack.slice(0, stackTrimIndex.index); + if (stackTrimIndex) { + fault.detail["stack"] = fault.detail["stack"].slice( + 0, + stackTrimIndex.index + ); + } } return fault; diff --git a/lib/common.ts b/lib/common.ts index 23e9865..2752ae5 100644 --- a/lib/common.ts +++ b/lib/common.ts @@ -17,8 +17,6 @@ * along with GenieACS. If not, see . */ -import { ParamPath } from "./types"; - export const UNDEFINED_TYPE = "[object Undefined]"; export const NULL_TYPE = "[object Null]"; export const BOOLEAN_TYPE = "[object Boolean]"; @@ -55,138 +53,6 @@ export function generateDeviceId(deviceIdStruct): string { return esc(deviceIdStruct["OUI"]) + "-" + esc(deviceIdStruct["SerialNumber"]); } -export function parseAlias(pattern, start, res): number { - const aliases = []; - let i = start; - while (i < pattern.length && pattern[i] !== "]") { - const alias = []; - /* eslint-disable-next-line @typescript-eslint/no-use-before-define */ - let j = (i = parsePath(pattern, i, alias) + 1); - while (pattern[j] !== "]" && pattern[j] !== ",") { - if (pattern[j] === '"' && i === j) { - ++j; - while (pattern[j] !== '"' || pattern[j - 1] === "\\") { - if (++j >= pattern.length) - throw new Error("Invalid alias expression"); - } - } - if (++j >= pattern.length) throw new Error("Invalid alias expression"); - } - - let value = pattern.slice(i, j).trim(); - i = j; - if (value[0] === '"') { - try { - value = JSON.parse(value); - } catch (error) { - throw new Error("Invalid alias expression"); - } - } - - alias.push(value); - aliases.push(alias); - if (pattern[i] === ",") ++i; - } - - // Sort to ensure identical expressions have idential string representation - function srt(a, b): number { - const jMax = Math.min(a.length, b.length); - for (let j = 0; j < jMax; j += 2) { - const kMax = Math.min(a[j].length, b[j].length); - for (let k = 0; k < kMax; ++k) { - if (Array.isArray(a[j][k])) { - if (Array.isArray(b[j][k])) return srt(a[j][k], b[j][k]); - else if (b[j][k] == null) return -1; - else return 1; - } else if (a[j][k] == null) { - if (b[j][k] == null) return 0; - else return 1; - } else if (b[j][k] == null || Array.isArray(b[j][k])) { - return -1; - } else if (a[j][k] > b[j][k]) { - return 1; - } else if (a[j][k] < b[j][k]) { - return -1; - } - } - - if (a[j].length > b[j].length) return -1; - else if (a[j].length < b[j].length) return 1; - - if (a[j + 1] > b[j + 1]) return -1; - else if (a[j + 1] < b[j + 1]) return 1; - } - - if (a.length > b.length) return -1; - else if (a.length < b.length) return 1; - - return 0; - } - - aliases.sort(srt); - res.push([].concat.apply([], aliases)); - return i; -} - -/* eslint-disable @typescript-eslint/no-use-before-define */ -export function parsePath(pattern, start, res): number; -export function parsePath(pattern, start?): ParamPath; -export function parsePath(pattern, start?, res?): number | ParamPath { - const path = [] as ParamPath; - path.wildcard = 0; - path.alias = 0; - let i = start || 0; - - // Colon separator is needed for parseAlias - if (i < pattern.length && pattern[i] !== ":") { - for (;;) { - if (pattern[i] === "[") { - path.alias |= 1 << path.length; - i = parseAlias(pattern, i + 1, path) + 1; - } else { - const j = i; - while (i < pattern.length && pattern[i] !== ":" && pattern[i] !== ".") - ++i; - - const n = pattern.slice(j, i); - if (n === "*") path.wildcard |= 1 << path.length; - path.push(n); - } - - if (i >= pattern.length || pattern[i] === ":") break; - else if (pattern[i] !== ".") throw new Error("Invalid alias expression"); - - ++i; - } - } - - Object.freeze(path); - - if (res == null) return path; - - res.push(path); - return i; -} - -export function addPathMeta(path): ParamPath { - if (path.alias != null && path.wildcard != null) return path; - - path.alias = 0; - path.wildcard = 0; - - for (const [i, p] of path.entries()) { - if (Array.isArray(p)) { - path.alias |= 1 << i; - for (let j = 0; j < p.length; j += 2) addPathMeta(p[j]); - } else if (p === "*") { - path.wildcard |= 1 << i; - } - } - - Object.freeze(path); - return path; -} - // Source: http://stackoverflow.com/a/6969486 export function escapeRegExp(str): string { return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); diff --git a/lib/path-set.ts b/lib/common/path-set.ts similarity index 73% rename from lib/path-set.ts rename to lib/common/path-set.ts index 6df6b72..4edad4c 100644 --- a/lib/path-set.ts +++ b/lib/common/path-set.ts @@ -17,11 +17,11 @@ * along with GenieACS. If not, see . */ -import { ParamPath } from "./types"; +import Path from "./path"; export default class PathSet { - private lengthIndex: Set[]; - private fragmentIndex: Map>[]; + private lengthIndex: Set[]; + private fragmentIndex: Map>[]; public constructor() { this.lengthIndex = []; @@ -32,7 +32,8 @@ export default class PathSet { return this.lengthIndex.length; } - public add(path: ParamPath): ParamPath { + public add(path: Path): Path { + if (path.alias) throw new Error("PathSet does not support aliased paths"); const p = this.get(path); if (p) return p; @@ -45,43 +46,36 @@ export default class PathSet { const lengthIndex = this.lengthIndex[path.length]; lengthIndex.add(path); - let wildcard = 0; for (let i = 0; i < path.length; ++i) { - const fragment = path[i]; - if (fragment === "*") wildcard = wildcard | (1 << i); - + const fragment = path.segments[i] as string; const fragmentIndex = this.fragmentIndex[i]; let fragmentIndexSet = fragmentIndex.get(fragment); if (!fragmentIndexSet) { - fragmentIndexSet = new Set(); + fragmentIndexSet = new Set(); fragmentIndex.set(fragment, fragmentIndexSet); } fragmentIndexSet.add(path); } - if (path.wildcard == null) { - path.alias = 0; - path.wildcard = wildcard; - Object.freeze(path); - } return path; } - public get(pattern: ParamPath): ParamPath { - const lengthIndex = this.lengthIndex[pattern.length]; + public get(path: Path): Path { + if (path.alias) throw new Error("PathSet does not support aliased paths"); + const lengthIndex = this.lengthIndex[path.length]; if (!lengthIndex || !lengthIndex.size) return null; - if (lengthIndex.has(pattern)) return pattern; + if (lengthIndex.has(path)) return path; const sets = [lengthIndex]; - for (let i = 0; i < pattern.length; ++i) { + for (let i = 0; i < path.length; ++i) { const fragmentIndex = this.fragmentIndex[i]; if (!fragmentIndex || !fragmentIndex.size) return null; - const fragment = pattern[i]; + const fragment = path.segments[i] as string; const fragmentIndexSet = fragmentIndex.get(fragment); if (!fragmentIndexSet) return null; @@ -92,39 +86,40 @@ export default class PathSet { sets.sort((a, b) => a.size - b.size); const smallestSet = sets.shift(); - for (let path of smallestSet) { + for (let p of smallestSet) { for (const s of sets) { - if (!s.has(path)) { - path = null; + if (!s.has(p)) { + p = null; break; } } - if (path) return path; + if (p) return p; } return null; } public find( - pattern: ParamPath, + path: Path, superset: boolean = false, subset: boolean = false, - depth: number = pattern.length - ): ParamPath[] { + depth: number = path.length + ): Path[] { + if (path.alias) throw new Error("PathSet does not support aliased paths"); const res = []; const groups = [[]]; - for (let i = pattern.length; i <= depth; ++i) + for (let i = path.length; i <= depth; ++i) if (this.lengthIndex[i]) groups[0].push(this.lengthIndex[i]); if (!groups[0].length) return res; - for (let i = 0; i < pattern.length; ++i) { + for (let i = 0; i < path.length; ++i) { const fragmentIndex = this.fragmentIndex[i]; if (!fragmentIndex) return res; - const fragment = pattern[i]; + const fragment = path.segments[i] as string; if (fragment === "*" && subset) continue; @@ -149,22 +144,22 @@ export default class PathSet { const smallestGroup = groups.shift(); for (const set of smallestGroup) { - for (let path of set) { + for (let p of set) { for (const group of groups) { let found = false; for (const s of group) { - if (s.has(path)) { + if (s.has(p)) { found = true; break; } } if (!found) { - path = null; + p = null; break; } } - if (path) res.push(path); + if (p) res.push(p); } } return res; diff --git a/lib/common/path.ts b/lib/common/path.ts index dbf2574..60fd16f 100644 --- a/lib/common/path.ts +++ b/lib/common/path.ts @@ -172,6 +172,12 @@ export default class Path { const alias = this.alias | (path.alias << this.segments.length); return new Path(segments, wildcard, alias); } + + public stripAlias(): Path { + if (!this.alias) return this; + const segments = this.segments.map(s => (Array.isArray(s) ? "*" : s)); + return new Path(segments, this.wildcard | this.alias, 0); + } } const interval = setInterval(() => { diff --git a/lib/cwmp.ts b/lib/cwmp.ts index 06d6aa2..e3d4ba2 100644 --- a/lib/cwmp.ts +++ b/lib/cwmp.ts @@ -32,13 +32,14 @@ import * as localCache from "./local-cache"; import * as db from "./db"; import * as logger from "./logger"; import * as scheduling from "./scheduling"; +import Path from "./common/path"; import { SessionContext, - ParamPath, AcsRequest, SetAcsRequest, SessionFault, - Operation + Operation, + Fault } from "./types"; const MAX_CYCLES = 4; @@ -144,9 +145,19 @@ function writeResponse( } } -function recordFault(sessionContext, fault, provisions, channels): void; -function recordFault(sessionContext, fault): void; -function recordFault(sessionContext, fault, provisions?, channels?): void { +function recordFault( + sessionContext: SessionContext, + fault: Fault, + provisions, + channels +): void; +function recordFault(sessionContext: SessionContext, fault: Fault): void; +function recordFault( + sessionContext: SessionContext, + fault: Fault, + provisions?, + channels? +): void { if (!provisions) { provisions = sessionContext.provisions; channels = sessionContext.channels; @@ -157,7 +168,10 @@ function recordFault(sessionContext, fault, provisions?, channels?): void { const provs = sessionContext.faults[channel] ? sessionContext.faults[channel].provisions : []; - faults[channel] = Object.assign({ provisions: provs }, fault); + faults[channel] = Object.assign( + { provisions: provs, timestamp: sessionContext.timestamp }, + fault + ) as SessionFault; if (channel.startsWith("task_")) { const taskId = channel.slice(5); for (const t of sessionContext.tasks) @@ -334,17 +348,13 @@ function applyPresets(sessionContext: SessionContext): void { deviceData.attributes.revision = 1; const deviceEvents = {}; - for (const p of deviceData.paths.find( - ["Events", "*"] as ParamPath, - false, - true - )) { + for (const p of deviceData.paths.find(Path.parse("Events.*"), false, true)) { const attrs = deviceData.attributes.get(p); if (attrs && attrs.value && attrs.value[1][0] >= sessionContext.timestamp) - deviceEvents[p[1]] = true; + deviceEvents[p.segments[1] as string] = true; } - const parameters: { [name: string]: ParamPath } = {}; + const parameters: { [name: string]: Path } = {}; const filteredPresets = []; for (const preset of presets) { @@ -377,7 +387,7 @@ function applyPresets(sessionContext: SessionContext): void { for (const k of Object.keys(preset.precondition)) { sessionContext.channels[preset.channel] = 0; const p = k.split(/([^a-zA-Z0-9\-_.].*)/, 1)[0]; - parameters[p] = common.parsePath(p); + parameters[p] = Path.parse(p); } } @@ -409,7 +419,7 @@ function applyPresets(sessionContext: SessionContext): void { const parameterValues = {}; for (const [k, v] of Object.entries(parameters)) { const unpacked = device.unpack(deviceData, v); - if (!unpacked[0]) continue; + if (!unpacked.length) continue; const attrs = deviceData.attributes.get(unpacked[0]); if (attrs && attrs.value && attrs.value[1]) parameterValues[k] = attrs.value[1][0]; diff --git a/lib/db.ts b/lib/db.ts index bc6f4b8..7d87d6a 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -21,6 +21,8 @@ import { MongoClient, ObjectID } from "mongodb"; import { get } from "./config"; import { escapeRegExp } from "./common"; import { parse } from "./common/expression-parser"; +import { DeviceData, Attributes } from "./types"; +import Path from "./common/path"; export let tasksCollection, devicesCollection, @@ -87,55 +89,66 @@ function optimizeProjection(obj: {}): {} { return obj; } -export function fetchDevice(id, timestamp, patterns, callback): void { +export function fetchDevice( + id: string, + timestamp: number, + patterns: [Path, number][], + callback +): void { const MAX_DEPTH = +get("MAX_DEPTH", id); - if (!patterns || !patterns.length) patterns = [[[], (1 << MAX_DEPTH) - 1]]; + if (!patterns || !patterns.length) + patterns = [[Path.parse(""), (1 << MAX_DEPTH) - 1]]; let projection = {}; const projectionTree = {}; - function func(path, pats, projTree): void { - const children = {}; + function func( + path: string, + pathLength: number, + pats: [Path, number][], + projTree + ): void { + const children: { [fragment: string]: [Path, number][] } = {}; for (const pat of pats) { - const fragment = pat[0][path.length] || "*"; + const fragment = (pat[0].segments[pathLength] as string) || "*"; if (fragment === "*") { - if (pat[1] << path.length) projection[path.join(".")] = 1; + if (pat[1] << pathLength) projection[path.slice(0, -1)] = 1; return; } if (!projTree[fragment]) projTree[fragment] = {}; - if (pat[1] & (1 << path.length)) { - projection[path.concat("_timestamp").join(".")] = 1; - projection[path.concat("_object").join(".")] = 1; - projection[path.concat("_instance").join(".")] = 1; - projection[path.concat([fragment, "_timestamp"]).join(".")] = 1; - projection[path.concat([fragment, "_value"]).join(".")] = 1; - projection[path.concat([fragment, "_type"]).join(".")] = 1; - projection[path.concat([fragment, "_object"]).join(".")] = 1; - projection[path.concat([fragment, "_instance"]).join(".")] = 1; - projection[path.concat([fragment, "_writable"]).join(".")] = 1; - projection[path.concat([fragment, "_orig"]).join(".")] = 1; + if (pat[1] & (1 << pathLength)) { + projection[path + "_timestamp"] = 1; + projection[path + "_object"] = 1; + projection[path + "_instance"] = 1; + projection[path + fragment + "._timestamp"] = 1; + projection[path + fragment + "._value"] = 1; + projection[path + fragment + "._type"] = 1; + projection[path + fragment + "._object"] = 1; + projection[path + fragment + "._instance"] = 1; + projection[path + fragment + "._writable"] = 1; + projection[path + fragment + "._orig"] = 1; } - if (pat[1] >> (path.length + 1)) { + if (pat[1] >> (pathLength + 1)) { if (!children[fragment]) children[fragment] = []; children[fragment].push(pat); } } for (const [k, v] of Object.entries(children)) - func(path.concat(k), v, projTree[k]); + func(path + k + ".", pathLength + 1, v, projTree[k]); } - func([], patterns, projectionTree); + func("", 0, patterns, projectionTree); delete projectionTree["DeviceID"]; delete projectionTree["Events"]; delete projectionTree["Tags"]; - const res = []; - const loaded = []; + const res: [Path, number, Attributes?][] = []; + const loaded: [Path, number][] = []; projection = optimizeProjection(projection); @@ -143,38 +156,47 @@ export function fetchDevice(id, timestamp, patterns, callback): void { if (k === "" || k === "Events" || k.startsWith("Events.")) { if (k === "" || k === "Events" || k === "Events._writable") { res.push([ - ["Events"], + Path.parse("Events"), timestamp, { object: [timestamp, 1], writable: [timestamp, 0] } ]); - if (k === "Events._writable") loaded.push([["Events"], 1]); + if (k === "Events._writable") loaded.push([Path.parse("Events"), 1]); } if (k === "Events") { projection["_lastInform"] = 1; projection["_lastBoot"] = 1; projection["_lastBootstrap"] = 1; projection["_registered"] = 1; - loaded.push([["Events"], (1 << MAX_DEPTH) - 1]); + loaded.push([Path.parse("Events"), (1 << MAX_DEPTH) - 1]); } else if (k === "Events.Inform._writable" || k === "Events.Inform") { projection["_lastInform"] = 1; - loaded.push([["Events", "Inform"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([Path.parse("Events.Inform"), 1 ^ ((1 << MAX_DEPTH) - 1)]); } else if (k === "Events.1_BOOT._writable" || k === "Events.1_BOOT") { projection["_lastBoot"] = 1; - loaded.push([["Events", "1_BOOT"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([Path.parse("Events.1_BOOT"), 1 ^ ((1 << MAX_DEPTH) - 1)]); } else if ( k === "Events.0_BOOTSTRAP._writable" || k === "Events.0_BOOTSTRAP" ) { projection["_lastBootstrap"] = 1; - loaded.push([["Events", "0_BOOTSTRAP"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([ + Path.parse("Events.0_BOOTSTRAP"), + 1 ^ ((1 << MAX_DEPTH) - 1) + ]); } else if ( k === "Events.Registered._writable" || k === "Events.Registered" ) { projection["_registered"] = 1; - loaded.push([["Events", "Registered"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([ + Path.parse("Events.Registered"), + 1 ^ ((1 << MAX_DEPTH) - 1) + ]); } else if (k.endsWith("._writable") && k !== "Events._writable") { - loaded.push([k.split(".").slice(0, 2), 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([ + Path.parse(k.split(".", 2).join(".")), + 1 ^ ((1 << MAX_DEPTH) - 1) + ]); } if (k !== "") delete projection[k]; } @@ -182,42 +204,58 @@ export function fetchDevice(id, timestamp, patterns, callback): void { if (k === "" || k === "DeviceID" || k.startsWith("DeviceID.")) { if (k === "" || k === "DeviceID" || k === "DeviceID._writable") { res.push([ - ["DeviceID"], + Path.parse("DeviceID"), timestamp, { object: [timestamp, 1], writable: [timestamp, 0] } ]); - if (k === "DeviceID._writable") loaded.push([["DeviceID"], 1]); + if (k === "DeviceID._writable") + loaded.push([Path.parse("DeviceID"), 1]); } if (k === "DeviceID") { projection["_id"] = 1; projection["_deviceId"] = 1; - loaded.push([["DeviceID"], (1 << MAX_DEPTH) - 1]); + loaded.push([Path.parse("DeviceID"), (1 << MAX_DEPTH) - 1]); } else if (k === "DeviceID.ID._writable" || k === "DeviceID.ID") { projection["_id"] = 1; - loaded.push([["DeviceID", "ID"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([Path.parse("DeviceID.ID"), 1 ^ ((1 << MAX_DEPTH) - 1)]); } else if ( k === "DeviceID.Manufacturer._writable" || k === "DeviceID.DeManufacturer" ) { projection["_deviceId._Manufacturer"] = 1; - loaded.push([["DeviceID", "Manufacturer"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([ + Path.parse("DeviceID.Manufacturer"), + 1 ^ ((1 << MAX_DEPTH) - 1) + ]); } else if ( k === "DeviceID.ProductClass._writable" || k === "DeviceID.ProductClass" ) { projection["_deviceId._ProductClass"] = 1; - loaded.push([["DeviceID", "ProductClass"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([ + Path.parse("DeviceID.ProductClass"), + 1 ^ ((1 << MAX_DEPTH) - 1) + ]); } else if (k === "DeviceID.OUI._writable" || k === "DeviceID.OUI") { projection["_deviceId._OUI"] = 1; - loaded.push([["DeviceID", "ProductClass"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([ + Path.parse("DeviceID.ProductClass"), + 1 ^ ((1 << MAX_DEPTH) - 1) + ]); } else if ( k === "DeviceID.SerialNumber._writable" || k === "DeviceID.SerialNumber" ) { projection["_deviceId._SerialNumber"] = 1; - loaded.push([["DeviceID", "SerialNumber"], 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([ + Path.parse("DeviceID.SerialNumber"), + 1 ^ ((1 << MAX_DEPTH) - 1) + ]); } else if (k.endsWith("._writable") && k !== "DeviceID._writable") { - loaded.push([k.split(".").slice(0, 2), 1 ^ ((1 << MAX_DEPTH) - 1)]); + loaded.push([ + Path.parse(k.split(".", 2).join(".")), + 1 ^ ((1 << MAX_DEPTH) - 1) + ]); } if (k !== "") delete projection[k]; } @@ -225,7 +263,7 @@ export function fetchDevice(id, timestamp, patterns, callback): void { if (k === "Tags" || k.startsWith("Tags.")) { if (!projection["_tags"]) { projection["_tags"] = 1; - loaded.push([["Tags"], (1 << MAX_DEPTH) - 1]); + loaded.push([Path.parse("Tags"), (1 << MAX_DEPTH) - 1]); } delete projection[k]; } @@ -240,34 +278,39 @@ export function fetchDevice(id, timestamp, patterns, callback): void { devicesCollection.findOne({ _id: id }, options, (err, device) => { if (err || !device) return void callback(err); - function storeParams(obj, path, ts, descendantsFetched, projTree): void { + function storeParams( + obj, + path: string, + pathLength: number, + ts, + descendantsFetched, + projTree + ): void { let thisFetched = false; if (descendantsFetched) { thisFetched = true; - } else if (projection[path.join(".")]) { + } else if (projection[path.slice(0, -1)]) { descendantsFetched = true; if ( - path.length && + pathLength && projection[ - path - .slice(0, -1) - .concat("_timestamp") - .join(".") + path.slice(0, path.lastIndexOf(".", path.length - 2) + 1) + + "_timestamp" ] ) { thisFetched = true; loaded.push([ - path, - ((1 << (path.length - 1)) - 1) ^ ((1 << MAX_DEPTH) - 1) + Path.parse(path.slice(0, -1)), + ((1 << (pathLength - 1)) - 1) ^ ((1 << MAX_DEPTH) - 1) ]); } else { loaded.push([ - path, - ((1 << path.length) - 1) ^ ((1 << MAX_DEPTH) - 1) + Path.parse(path.slice(0, -1)), + ((1 << pathLength) - 1) ^ ((1 << MAX_DEPTH) - 1) ]); } - } else if (projection[path.concat("_writable").join(".")]) { - loaded.push([path, 1 << (path.length - 1)]); + } else if (projection[path + "_writable"]) { + loaded.push([Path.parse(path.slice(0, -1)), 1 << (pathLength - 1)]); thisFetched = true; } @@ -277,11 +320,7 @@ export function fetchDevice(id, timestamp, patterns, callback): void { if (obj["_instance"] && obj["_object"] == null) obj["_object"] = true; if (thisFetched) { - const attrs: { - object?: [number, number]; - writable?: [number, number]; - value?: [number, [string | number | boolean, string]]; - } = {}; + const attrs: Attributes = {}; let t = obj["_timestamp"] || 1; if (ts > t) t = ts; @@ -297,7 +336,7 @@ export function fetchDevice(id, timestamp, patterns, callback): void { if (obj["_object"] != null) attrs.object = [t, obj["_object"] ? 1 : 0]; - res.push([path, t, attrs]); + res.push([Path.parse(path.slice(0, -1)), t, attrs]); } for (const [k, v] of Object.entries(obj)) { @@ -305,7 +344,8 @@ export function fetchDevice(id, timestamp, patterns, callback): void { obj["_object"] = true; storeParams( v, - path.concat(k), + path + k + ".", + pathLength + 1, obj["_timestamp"], descendantsFetched, projTree ? projTree[k] : null @@ -317,14 +357,14 @@ export function fetchDevice(id, timestamp, patterns, callback): void { if (!descendantsFetched) { if (projTree) { for (const k of Object.keys(projTree)) { - const p = path.concat(k); - loaded.push([p, ((1 << path.length) - 1) ^ ((1 << MAX_DEPTH) - 1)]); - if ((obj["_object"] || !path.length) && obj["_timestamp"]) + const p = Path.parse(path + k); + loaded.push([p, ((1 << pathLength) - 1) ^ ((1 << MAX_DEPTH) - 1)]); + if ((obj["_object"] || !pathLength) && obj["_timestamp"]) res.push([p, obj["_timestamp"]]); } } - } else if ((obj["_object"] || !path.length) && obj["_timestamp"]) { - res.push([path.concat("*"), obj["_timestamp"]]); + } else if ((obj["_object"] || !pathLength) && obj["_timestamp"]) { + res.push([Path.parse(path + "*"), obj["_timestamp"]]); } } @@ -332,7 +372,7 @@ export function fetchDevice(id, timestamp, patterns, callback): void { switch (k) { case "_lastInform": res.push([ - ["Events", "Inform"], + Path.parse("Events.Inform"), +v, { object: [+v, 0], @@ -344,7 +384,7 @@ export function fetchDevice(id, timestamp, patterns, callback): void { break; case "_lastBoot": res.push([ - ["Events", "1_BOOT"], + Path.parse("Events.1_BOOT"), +v, { object: [+v, 0], @@ -356,7 +396,7 @@ export function fetchDevice(id, timestamp, patterns, callback): void { break; case "_lastBootstrap": res.push([ - ["Events", "0_BOOTSTRAP"], + Path.parse("Events.0_BOOTSTRAP"), +v, { object: [+v, 0], @@ -369,7 +409,7 @@ export function fetchDevice(id, timestamp, patterns, callback): void { case "_registered": // Use current timestamp for registered event attribute timestamps res.push([ - ["Events", "Registered"], + Path.parse("Events.Registered"), timestamp, { object: [timestamp, 0], @@ -382,12 +422,12 @@ export function fetchDevice(id, timestamp, patterns, callback): void { case "_id": if (projection[""] || projection["_id"]) { res.push([ - ["DeviceID", "ID"], + Path.parse("DeviceID.ID"), timestamp, { object: [timestamp, 0], writable: [timestamp, 0], - value: [timestamp, [v, "xsd:string"]] + value: [timestamp, [v as string, "xsd:string"]] } ]); } @@ -397,7 +437,7 @@ export function fetchDevice(id, timestamp, patterns, callback): void { case "_tags": if ((v as string[]).length) { res.push([ - ["Tags"], + Path.parse("Tags"), timestamp, { object: [timestamp, 1], writable: [timestamp, 0] } ]); @@ -406,7 +446,7 @@ export function fetchDevice(id, timestamp, patterns, callback): void { for (let t of v as string[]) { t = t.replace(/[^a-zA-Z0-9-]+/g, "_"); res.push([ - ["Tags", t], + Path.parse("Tags." + t), timestamp, { object: [timestamp, 0], @@ -420,7 +460,7 @@ export function fetchDevice(id, timestamp, patterns, callback): void { case "_deviceId": if (v["_Manufacturer"] != null) { res.push([ - ["DeviceID", "Manufacturer"], + Path.parse("DeviceID.Manufacturer"), timestamp, { object: [timestamp, 0], @@ -432,7 +472,7 @@ export function fetchDevice(id, timestamp, patterns, callback): void { if (v["_OUI"] != null) { res.push([ - ["DeviceID", "OUI"], + Path.parse("DeviceID.OUI"), timestamp, { object: [timestamp, 0], @@ -444,7 +484,7 @@ export function fetchDevice(id, timestamp, patterns, callback): void { if (v["_ProductClass"] != null) { res.push([ - ["DeviceID", "ProductClass"], + Path.parse("DeviceID.ProductClass"), timestamp, { object: [timestamp, 0], @@ -456,7 +496,7 @@ export function fetchDevice(id, timestamp, patterns, callback): void { if (v["_SerialNumber"] != null) { res.push([ - ["DeviceID", "SerialNumber"], + Path.parse("DeviceID.SerialNumber"), timestamp, { object: [timestamp, 0], @@ -470,16 +510,16 @@ export function fetchDevice(id, timestamp, patterns, callback): void { } } - storeParams(device, [], 0, false, projectionTree); + storeParams(device, "", 0, 0, false, projectionTree); callback(null, res, loaded); }); } export function saveDevice( - deviceId, - deviceData, - isNew, - sessionTimestamp, + deviceId: string, + deviceData: DeviceData, + isNew: boolean, + sessionTimestamp: number, callback ): void { const update = { $set: {}, $unset: {}, $addToSet: {}, $pull: {} }; @@ -488,30 +528,25 @@ export function saveDevice( if (diff[0].wildcard !== 1 << (diff[0].length - 1)) continue; if ( - diff[0][0] === "Events" || - diff[0][0] === "DeviceID" || - diff[0][0] === "Tags" + diff[0].segments[0] === "Events" || + diff[0].segments[0] === "DeviceID" || + diff[0].segments[0] === "Tags" ) continue; + const parent = deviceData.paths.get(diff[0].slice(0, -1)); + // Param timestamps may be greater than session timestamp to track revisions if (diff[2] > sessionTimestamp) diff[2] = sessionTimestamp; if (diff[2] == null && diff[1] != null) { update["$unset"][ - diff[0] - .slice(0, -1) - .concat("_timestamp") - .join(".") + parent.length ? parent.toString() + "._timestamp" : "_timestamp" ] = 1; } else { - const parent = deviceData.paths.get(diff[0].slice(0, -1)); if (parent && (!parent.length || deviceData.attributes.has(parent))) { update["$set"][ - diff[0] - .slice(0, -1) - .concat("_timestamp") - .join(".") + parent.length ? parent.toString() + "._timestamp" : "_timestamp" ] = new Date(diff[2]); } } @@ -530,11 +565,11 @@ export function saveDevice( const writable2 = ((diff[2] || {}).writable || [])[1]; const writable1 = ((diff[1] || {}).writable || [])[1]; - switch (path[0]) { + switch (path.segments[0]) { case "Events": - if (diff[0].length === 2 && value2 !== value1) { + if (path.length === 2 && value2 !== value1) { if (!diff[2]) { - switch (path[1]) { + switch (path.segments[1]) { case "Inform": update["$unset"]["_lastInform"] = 1; break; @@ -548,8 +583,8 @@ export function saveDevice( update["$unset"]["_registered"] = 1; } } else { - const t = new Date(diff[2].value[1][0]); - switch (path[1]) { + const t = new Date(diff[2].value[1][0] as number); + switch (path.segments[1]) { case "Inform": update["$set"]["_lastInform"] = t; break; @@ -569,7 +604,7 @@ export function saveDevice( case "DeviceID": if (value2 !== value1) { const v = diff[2].value[1][0]; - switch (path[1]) { + switch (path.segments[1]) { case "ID": update["$set"]["_id"] = v; break; @@ -592,21 +627,21 @@ export function saveDevice( if (value2 != null) { if (!update["$addToSet"]["_tags"]) update["$addToSet"]["_tags"] = { $each: [] }; - update["$addToSet"]["_tags"]["$each"].push(diff[0][1]); + update["$addToSet"]["_tags"]["$each"].push(path.segments[1]); } else { if (!update["$pull"]["_tags"]) { update["$pull"]["_tags"] = { $in: [] }; } - update["$pull"]["_tags"]["$in"].push(diff[0][1]); + update["$pull"]["_tags"]["$in"].push(path.segments[1]); } } break; default: if (!diff[2]) { - update["$unset"][diff[0].join(".")] = 1; + update["$unset"][path.toString()] = 1; continue; } @@ -621,35 +656,38 @@ export function saveDevice( if (value2 !== value1) { if ( valueType2 === "xsd:dateTime" && - Number.isInteger(value2) + Number.isInteger(value2 as number) ) { - update["$set"][path.concat("_value").join(".")] = new Date( - value2 + update["$set"][path.toString() + "._value"] = new Date( + value2 as number ); } else { - update["$set"][path.concat("_value").join(".")] = value2; + update["$set"][path.toString() + "._value"] = value2; } } if (valueType2 !== valueType1) - update["$set"][path.concat("_type").join(".")] = valueType2; + update["$set"][path.toString() + "._type"] = valueType2; if (valueTimestamp2 !== valueTimestamp1) { - update["$set"][ - path.concat("_timestamp").join(".") - ] = new Date(valueTimestamp2); + update["$set"][path.toString() + "._timestamp"] = new Date( + valueTimestamp2 + ); } break; case "object": - if (!diff[1] || !diff[1].object || object2 !== object1) - update["$set"][path.concat("_object").join(".")] = !!object2; + if (!diff[1] || !diff[1].object || object2 !== object1) { + update["$set"][ + path.length ? path.toString() + "._object" : "_object" + ] = !!object2; + } break; case "writable": if (!diff[1] || !diff[1].writable || writable2 !== writable1) { update["$set"][ - path.concat("_writable").join(".") + path.length ? path.toString() + "._writable" : "_writable" ] = !!writable2; } } @@ -662,10 +700,11 @@ export function saveDevice( diff[1][attrName][1] != null && (!diff[2] || !diff[2][attrName] || diff[2][attrName][1] == null) ) { - update["$unset"][path.concat(`_${attrName}`).join(".")] = 1; + const p = path.length ? path.toString() + "." : ""; + update["$unset"][`${p}_${attrName}`] = 1; if (attrName === "value") { - update["$unset"][path.concat("_type").join(".")] = 1; - update["$unset"][path.concat("_timestamp").join(".")] = 1; + update["$unset"][p + "_type"] = 1; + update["$unset"][p + "_timestamp"] = 1; } } } diff --git a/lib/default-provisions.ts b/lib/default-provisions.ts index edcf359..fe74590 100644 --- a/lib/default-provisions.ts +++ b/lib/default-provisions.ts @@ -17,18 +17,19 @@ * along with GenieACS. If not, see . */ +import Path from "./common/path"; import * as config from "./config"; -import * as common from "./common"; import * as device from "./device"; import * as scheduling from "./scheduling"; const MAX_DEPTH = +config.get("MAX_DEPTH"); export function refresh(sessionContext, provision, declarations): boolean { - const path = common.parsePath(provision[1]).slice(); - const l = path.length; - path.length = MAX_DEPTH; - path.fill("*", l); + const segments = Path.parse(provision[1]).segments.slice(); + const l = segments.length; + segments.length = MAX_DEPTH; + segments.fill("*", l); + const path = Path.parse(segments.join(".")); const every = 1000 * (provision[2] || 1); const offset = scheduling.variance(sessionContext.deviceId, every); const t = scheduling.interval(sessionContext.timestamp, every, offset); @@ -48,9 +49,8 @@ export function refresh(sessionContext, provision, declarations): boolean { } export function value(sessionContext, provision, declarations): boolean { - const path = common.parsePath(provision[1]); declarations.push({ - path: path, + path: Path.parse(provision[1]), pathGet: 1, pathSet: null, attrGet: { value: 1 }, @@ -62,10 +62,8 @@ export function value(sessionContext, provision, declarations): boolean { } export function tag(sessionContext, provision, declarations): boolean { - const path = ["Tags", provision[1]]; - declarations.push({ - path: path, + path: Path.parse(`Tags.${provision[1]}`), pathGet: 1, pathSet: null, attrGet: { value: 1 }, @@ -78,7 +76,7 @@ export function tag(sessionContext, provision, declarations): boolean { export function reboot(sessionContext, provision, declarations): boolean { declarations.push({ - path: ["Reboot"], + path: Path.parse("Reboot"), pathGet: 1, pathSet: null, attrGet: { value: 1 }, @@ -91,7 +89,7 @@ export function reboot(sessionContext, provision, declarations): boolean { export function reset(sessionContext, provision, declarations): boolean { declarations.push({ - path: ["FactoryReset"], + path: Path.parse("FactoryReset"), pathGet: 1, pathSet: null, attrGet: { value: 1 }, @@ -104,16 +102,13 @@ export function reset(sessionContext, provision, declarations): boolean { export function download(sessionContext, provision, declarations): boolean { const alias = [ - ["FileType"], - provision[1] || "", - ["FileName"], - provision[2] || "", - ["TargetFileName"], - provision[3] || "" - ]; + `FileType:${JSON.stringify(provision[1] || "")}`, + `FileName:${JSON.stringify(provision[2] || "")}`, + `TargetFileName:${JSON.stringify(provision[3] || "")}` + ].join(","); declarations.push({ - path: ["Downloads", alias], + path: Path.parse(`Downloads.[${alias}]`), pathGet: 1, pathSet: 1, attrGet: null, @@ -122,7 +117,7 @@ export function download(sessionContext, provision, declarations): boolean { }); declarations.push({ - path: ["Downloads", alias, "Download"], + path: Path.parse(`Downloads.[${alias}].Download`), pathGet: 1, pathSet: null, attrGet: { value: 1 }, @@ -144,7 +139,7 @@ export function instances( if (Number.isNaN(count)) return true; - const path = common.parsePath(provision[1]); + const path = Path.parse(provision[1]); if (provision[2][0] === "+" || provision[2][0] === "-") { declarations.push({ diff --git a/lib/device.ts b/lib/device.ts index 21df586..7f4fc3e 100644 --- a/lib/device.ts +++ b/lib/device.ts @@ -17,7 +17,8 @@ * along with GenieACS. If not, see . */ -import { ParamPath, DeviceData, Declaration, Clear } from "./types"; +import Path from "./common/path"; +import { DeviceData, Declaration, Clear } from "./types"; const CHANGE_FLAGS = { object: 2, @@ -34,7 +35,7 @@ function parseBool(v): boolean { } export function sanitizeParameterValue( - parameterValue + parameterValue: [string | number | boolean, string] ): [string | number | boolean, string?] { if (parameterValue[0] != null) { switch (parameterValue[1]) { @@ -55,7 +56,7 @@ export function sanitizeParameterValue( case "xsd:int": case "xsd:unsignedInt": if (typeof parameterValue[0] !== "number") { - const i = parseInt(parameterValue[0]); + const i = parseInt(parameterValue[0] as string); if (isNaN(i)) parameterValue[0] = "" + parameterValue[0]; else parameterValue[0] = i; } @@ -65,7 +66,7 @@ export function sanitizeParameterValue( // Don't use parseInt because it reads date string as a number let i = +parameterValue[0]; if (isNaN(i)) { - i = Date.parse(parameterValue[0]); + i = Date.parse(parameterValue[0] as string); if (isNaN(i)) parameterValue[0] = "" + parameterValue[0]; else parameterValue[0] = i; } else { @@ -88,87 +89,75 @@ export function sanitizeParameterValue( return parameterValue; } -export function getAliasDeclarations(path, timestamp): Declaration[] { - const decs = [] as Declaration[]; - - function recursive(p, prefix): void { - const pattern = prefix.concat(p); - - for (let i = prefix.length; i < pattern.length; ++i) { - if (Array.isArray(pattern[i])) { - const pat = pattern[i]; - pattern[i] = "*"; - for (let j = 0; j < pat.length; j += 2) - recursive(pat[j], pattern.slice(0, i + 1)); - } - } - - decs.push({ - path: pattern, +export function getAliasDeclarations( + path: Path, + timestamp: number, + attrGet = null +): Declaration[] { + const stripped = path.stripAlias(); + let decs: Declaration[] = [ + { + path: stripped, pathGet: timestamp, pathSet: null, - attrGet: prefix.length ? { value: timestamp } : null, + attrGet: attrGet, attrSet: null, defer: true - }); - } + } + ]; - recursive(path, []); + if (path.alias) { + for (const [i, alias] of path.segments.entries()) { + if (Array.isArray(alias)) { + const parent = stripped.slice(0, i + 1); + for (const [p] of alias as [Path, string][]) { + decs = decs.concat( + getAliasDeclarations(parent.concat(p), timestamp, { + value: timestamp + }) + ); + } + } + } + } return decs; } export function unpack( - deviceData, - path: ParamPath, + deviceData: DeviceData, + path: Path, revision?: number -): ParamPath[] { - let allMatches = [] as ParamPath[]; +): Path[] { + let allMatches = [] as Path[]; if (!path.alias) { for (const p of deviceData.paths.find(path, false, true)) if (deviceData.attributes.has(p, revision)) allMatches.push(p); } else { - const wildcardPath = path.slice() as ParamPath; - wildcardPath.wildcard = path.wildcard; - for (let i = 0; i < wildcardPath.length; ++i) { - if (Array.isArray(wildcardPath[i])) { - wildcardPath[i] = "*"; - wildcardPath.wildcard |= 1 << i; - } - } + const wildcardPath = path.stripAlias(); for (const p of deviceData.paths.find(wildcardPath, false, true)) if (deviceData.attributes.has(p, revision)) allMatches.push(p); for (let i = path.length - 1; i >= 0; --i) { - if (Array.isArray(path[i])) { - for (let j = 0; j < path[i].length; j += 2) { - const p = wildcardPath - .slice(0, i + 1) - .concat(path[i][j]) as ParamPath; - p.alias = - ((wildcardPath.alias & ((1 << (i + 1)) - 1)) + path[i][j].alias) << - (i + 1); - p.wildcard = - ((wildcardPath.wildcard & ((1 << (i + 1)) - 1)) + - path[i][j].wildcard) << - (i + 1); + if (path.alias & (i << i)) { + for (const [param, val] of path.segments[i] as [Path, string][]) { + const p = wildcardPath.slice(0, i + 1).concat(param); const unpacked = unpack(deviceData, p, revision); - const filtered = [] as ParamPath[]; + const filtered: Path[] = []; for (const up of unpacked) { const attributes = deviceData.attributes.get(up, revision); if ( attributes && attributes.value && attributes.value[1] && - sanitizeParameterValue([ - path[i][j + 1], - attributes.value[1][1] - ])[0] === attributes.value[1][0] + sanitizeParameterValue([val, attributes.value[1][1]])[0] === + attributes.value[1][0] ) { for (let m = 0; m < allMatches.length; ++m) { let k; - for (k = i; k >= 0; --k) if (allMatches[m][k] !== up[k]) break; + for (k = i; k >= 0; --k) + if (allMatches[m].segments[k] !== up.segments[k]) break; if (k < 0) { filtered.push(allMatches[m]); @@ -185,8 +174,8 @@ export function unpack( allMatches.sort((p1, p2) => { for (let i = 0; i < p1.length; ++i) { - const a = p1[i]; - const b = p2[i]; + const a = p1.segments[i] as string; + const b = p2.segments[i] as string; if (a !== b) { // Use numeric sorting for numbers const ia = parseInt(a); @@ -285,7 +274,7 @@ function compareEquality(a, b): boolean { export function set( deviceData: DeviceData, - path: ParamPath, + path: Path, timestamp, attributes, toClear?: Clear[] @@ -345,7 +334,7 @@ export function set( if (path.length > 1) { toClear = set( deviceData, - path.slice(0, path.length - 1) as ParamPath, + path.slice(0, path.length - 1), timestamp, { object: [timestamp, 1] }, toClear @@ -389,7 +378,7 @@ export function set( export function track( deviceData: DeviceData, - path: ParamPath, + path: Path, marker: string, attributes? ): void { @@ -408,7 +397,7 @@ export function track( cur[marker] |= f; } -export function clearTrackers(deviceData, tracker): void { +export function clearTrackers(deviceData: DeviceData, tracker): void { if (Array.isArray(tracker)) { for (const v of deviceData.trackers.values()) for (const t of tracker) delete v[t]; diff --git a/lib/sandbox.ts b/lib/sandbox.ts index 66c0798..f1cbb36 100644 --- a/lib/sandbox.ts +++ b/lib/sandbox.ts @@ -19,12 +19,12 @@ import * as vm from "vm"; import seedrandom from "seedrandom"; -import * as common from "./common"; import * as device from "./device"; import * as extensions from "./extensions"; import * as logger from "./logger"; import * as scheduling from "./scheduling"; -import { ParamPath, Fault } from "./types"; +import Path from "./common/path"; +import { Fault, SessionContext } from "./types"; // Used for throwing to exit user script and commit const COMMIT = Symbol(); @@ -112,7 +112,7 @@ random.seed = function(s) { }; class ParameterWrapper { - public constructor(path, attributes, unpacked?, unpackedRevision?) { + public constructor(path: Path, attributes, unpacked?, unpackedRevision?) { for (const attrName of attributes) { Object.defineProperty(this, attrName, { get: function() { @@ -156,7 +156,7 @@ class ParameterWrapper { if (!unpacked.length) return UNDEFINED; - return unpacked[0].join("."); + return unpacked[0].toString(); } }); @@ -198,7 +198,7 @@ class ParameterWrapper { } function declare( - path: ParamPath, + path: string, timestamps: { [attr: string]: number }, values: { [attr: string]: any } ): ParameterWrapper { @@ -207,7 +207,7 @@ function declare( if (!values) values = {}; - const parsedPath = common.parsePath(path); + const parsedPath = Path.parse(path); const declaration = { path: parsedPath, @@ -249,11 +249,11 @@ function declare( return new ParameterWrapper(parsedPath, attrs); } -function clear(path, timestamp, attributes): void { +function clear(path: string, timestamp: number, attributes?): void { state.uncommitted = true; if (state.revision === state.maxRevision) - state.clear.push([common.parsePath(path), timestamp, attributes]); + state.clear.push([Path.parse(path), timestamp, attributes]); } function commit(): void { @@ -282,19 +282,19 @@ function ext(): any { throw EXT; } -function log(msg, meta): void { +function log(msg: string, meta: {}): void { if (state.revision === state.maxRevision && state.extCounter >= 0) { const details = Object.assign({}, meta, { sessionContext: state.sessionContext, message: `Script: ${msg}` }); - delete details.hostname; - delete details.pid; - delete details.name; - delete details.version; - delete details.deviceId; - delete details.remoteAddress; + delete details["hostname"]; + delete details["pid"]; + delete details["name"]; + delete details["version"]; + delete details["deviceId"]; + delete details["remoteAddress"]; logger.accessInfo(details); } @@ -312,7 +312,7 @@ context.random = random; vm.runInContext("Math.random = random;", context); delete context.random; -function errorToFault(err): Fault { +function errorToFault(err: Error): Fault { if (!err) return null; if (!err.name) return { code: "script", message: `${err}` }; @@ -327,24 +327,28 @@ function errorToFault(err): Fault { }; if (err.stack) { - fault.detail.stack = err.stack; + fault.detail["stack"] = err.stack; // Trim the stack trace at the self-executing anonymous wrapper function - const stackTrimIndex = fault.detail.stack.match( + const stackTrimIndex = fault.detail["stack"].match( /\s+at\s[^\s]+\s+at\s[^\s]+\s\(vm\.js.+\)/ ); - if (stackTrimIndex) - fault.detail.stack = fault.detail.stack.slice(0, stackTrimIndex.index); + if (stackTrimIndex) { + fault.detail["stack"] = fault.detail["stack"].slice( + 0, + stackTrimIndex.index + ); + } } return fault; } export function run( - script, + script: vm.Script, globals, - sessionContext, - startRevision, - maxRevision, + sessionContext: SessionContext, + startRevision: number, + maxRevision: number, callback, extCounter = 0 ): void { diff --git a/lib/session.ts b/lib/session.ts index c2f1817..6e9d58e 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -18,27 +18,32 @@ */ import * as config from "./config"; -import * as common from "./common"; import * as db from "./db"; import * as device from "./device"; import * as sandbox from "./sandbox"; import * as localCache from "./local-cache"; import * as defaultProvisions from "./default-provisions"; import { estimateGpnCount } from "./gpn-heuristic"; -import PathSet from "./path-set"; +import Path from "./common/path"; +import PathSet from "./common/path-set"; import VersionedMap from "./versioned-map"; import InstanceSet from "./instance-set"; import { Attributes, SessionContext, - ParamPath, DeviceData, VirtualParameterDeclaration, GetAcsRequest, SetAcsRequest, AttributeTimestamps, AttributeValues, - Fault + Fault, + Declaration, + Clear, + CpeResponse, + CpeFault, + CpeSetResponse, + CpeGetResponse } from "./types"; function initDeviceData(): DeviceData { @@ -95,7 +100,10 @@ function loadParameters(sessionContext: SessionContext, callback): void { // Device not available in database, mark as new sessionContext.new = true; loaded = [ - [[], (1 << +config.get("MAX_DEPTH", sessionContext.deviceId)) - 1] + [ + Path.parse(""), + (1 << +config.get("MAX_DEPTH", sessionContext.deviceId)) - 1 + ] ]; parameters = []; } @@ -130,9 +138,9 @@ function generateRpcId(sessionContext: SessionContext): string { export function inform(sessionContext: SessionContext, rpcReq, callback): void { const timestamp = sessionContext.timestamp + sessionContext.iteration + 1; - const params: [ParamPath, number, Attributes][] = [ + const params: [Path, number, Attributes][] = [ [ - ["DeviceID", "Manufacturer"] as ParamPath, + Path.parse("DeviceID.Manufacturer"), timestamp, { object: [timestamp, 0], @@ -142,7 +150,7 @@ export function inform(sessionContext: SessionContext, rpcReq, callback): void { ], [ - ["DeviceID", "OUI"] as ParamPath, + Path.parse("DeviceID.OUI"), timestamp, { object: [timestamp, 0], @@ -152,7 +160,7 @@ export function inform(sessionContext: SessionContext, rpcReq, callback): void { ], [ - ["DeviceID", "ProductClass"] as ParamPath, + Path.parse("DeviceID.ProductClass"), timestamp, { object: [timestamp, 0], @@ -162,7 +170,7 @@ export function inform(sessionContext: SessionContext, rpcReq, callback): void { ], [ - ["DeviceID", "SerialNumber"] as ParamPath, + Path.parse("DeviceID.SerialNumber"), timestamp, { object: [timestamp, 0], @@ -173,7 +181,7 @@ export function inform(sessionContext: SessionContext, rpcReq, callback): void { ]; for (const p of rpcReq.parameterList) { - const path = common.parsePath(p[0]); + const path = Path.parse(p[0]); params.push([ path, timestamp, @@ -185,7 +193,7 @@ export function inform(sessionContext: SessionContext, rpcReq, callback): void { } params.push([ - ["Events", "Inform"] as ParamPath, + Path.parse("Events.Inform"), timestamp, { object: [timestamp, 0], @@ -196,7 +204,7 @@ export function inform(sessionContext: SessionContext, rpcReq, callback): void { for (const e of rpcReq.event) { params.push([ - ["Events", e.replace(/\s+/g, "_")] as ParamPath, + Path.parse(`Events.${e.replace(/\s+/g, "_")}`), timestamp, { object: [timestamp, 0], @@ -207,7 +215,7 @@ export function inform(sessionContext: SessionContext, rpcReq, callback): void { } // Preload DeviceID params - loadPath(sessionContext, ["DeviceID", "*"] as ParamPath); + loadPath(sessionContext, Path.parse("DeviceID.*")); for (const p of params) loadPath(sessionContext, p[0]); @@ -216,7 +224,7 @@ export function inform(sessionContext: SessionContext, rpcReq, callback): void { if (sessionContext.new) { params.push([ - ["DeviceID", "ID"] as ParamPath, + Path.parse("DeviceID.ID"), timestamp, { object: [timestamp, 0], @@ -225,7 +233,7 @@ export function inform(sessionContext: SessionContext, rpcReq, callback): void { } ]); params.push([ - ["Events", "Registered"] as ParamPath, + Path.parse("Events.Registered"), timestamp, { object: [timestamp, 0], @@ -241,7 +249,7 @@ export function inform(sessionContext: SessionContext, rpcReq, callback): void { let toClear = null; for (const p of params) { // Don't need to clear wildcards for Events - if (p[0][0] === "Events") { + if (p[0].segments[0] === "Events") { device.set(sessionContext.deviceData, p[0], p[1], p[2]); } else { toClear = device.set( @@ -298,7 +306,7 @@ export function transferComplete( ); } - loadPath(sessionContext, ["Downloads", instance, "*"] as ParamPath); + loadPath(sessionContext, Path.parse(`Downloads.${instance}.*`)); loadParameters(sessionContext, err => { if (err) return void callback(err); @@ -308,11 +316,9 @@ export function transferComplete( let p; - p = sessionContext.deviceData.paths.add([ - "Downloads", - instance, - "LastDownload" - ] as ParamPath); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.LastDownload`) + ); toClear = device.set( sessionContext.deviceData, p, @@ -321,11 +327,9 @@ export function transferComplete( toClear ); - p = sessionContext.deviceData.paths.add([ - "Downloads", - instance, - "LastFileType" - ] as ParamPath); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.LastFileType`) + ); toClear = device.set( sessionContext.deviceData, p, @@ -334,11 +338,9 @@ export function transferComplete( toClear ); - p = sessionContext.deviceData.paths.add([ - "Downloads", - instance, - "LastFileName" - ] as ParamPath); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.LastFileName`) + ); toClear = device.set( sessionContext.deviceData, p, @@ -347,11 +349,9 @@ export function transferComplete( toClear ); - p = sessionContext.deviceData.paths.add([ - "Downloads", - instance, - "LastTargetFileName" - ] as ParamPath); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.LastTargetFileName`) + ); toClear = device.set( sessionContext.deviceData, p, @@ -360,11 +360,9 @@ export function transferComplete( toClear ); - p = sessionContext.deviceData.paths.add([ - "Downloads", - instance, - "StartTime" - ] as ParamPath); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.StartTime`) + ); toClear = device.set( sessionContext.deviceData, p, @@ -373,11 +371,9 @@ export function transferComplete( toClear ); - p = sessionContext.deviceData.paths.add([ - "Downloads", - instance, - "CompleteTime" - ] as ParamPath); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.CompleteTime`) + ); toClear = device.set( sessionContext.deviceData, p, @@ -397,7 +393,7 @@ function revertDownloadParameters( instance, callback ): void { - loadPath(sessionContext, ["Downloads", instance, "*"] as ParamPath); + loadPath(sessionContext, Path.parse(`Downloads.${instance}.*`)); loadParameters(sessionContext, err => { if (err) return void callback(err); @@ -406,19 +402,15 @@ function revertDownloadParameters( let p; - p = sessionContext.deviceData.paths.add([ - "Downloads", - instance, - "LastDownload" - ] as ParamPath); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.LastDownload`) + ); const lastDownload = sessionContext.deviceData.attributes.get(p); - p = sessionContext.deviceData.paths.add([ - "Downloads", - instance, - "Download" - ] as ParamPath); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.Download`) + ); const toClear = device.set(sessionContext.deviceData, p, timestamp, { value: [ @@ -577,11 +569,17 @@ export function clearProvisions(sessionContext: SessionContext): void { } function runProvisions( - sessionContext, - provisions, - startRevision, - endRevision, - callback + sessionContext: SessionContext, + provisions: any[][], + startRevision: number, + endRevision: number, + callback: ( + err: Error, + fault?: Fault, + ret?: any, + decs?: Declaration[], + toClear?: Clear[] + ) => void ): void { let done = true; let allDeclarations = []; @@ -642,11 +640,17 @@ function runProvisions( } function runVirtualParameters( - sessionContext, - provisions, - startRevision, - endRevision, - callback + sessionContext: SessionContext, + provisions: any[][], + startRevision: number, + endRevision: number, + callback: ( + err: Error, + fault?: Fault, + ret?: any, + decs?: Declaration[], + toClear?: Clear[] + ) => void ): void { let done = true; const virtualParameterUpdates = []; @@ -795,7 +799,7 @@ function runVirtualParameters( function runDeclarations( sessionContext: SessionContext, - declarations + declarations: Declaration[] ): VirtualParameterDeclaration[] { if (!sessionContext.syncState) { sessionContext.syncState = { @@ -806,7 +810,7 @@ function runDeclarations( value: new Set() }, spv: new Map(), - gpn: new Set(), + gpn: new Set(), gpnPatterns: new Map(), tags: new Map(), virtualParameterDeclarations: [], @@ -815,25 +819,21 @@ function runDeclarations( downloadsToDelete: new Set(), downloadsToCreate: new InstanceSet(), downloadsValues: new Map(), - downloadsDownload: new Map() + downloadsDownload: new Map(), + reboot: 0, + factoryReset: 0 }; } - const allDeclareTimestamps = new Map(); - const allDeclareAttributeTimestamps = new Map< - ParamPath, - AttributeTimestamps - >(); - const allDeclareAttributeValues = new Map(); + const allDeclareTimestamps = new Map(); + const allDeclareAttributeTimestamps = new Map(); + const allDeclareAttributeValues = new Map(); const allVirtualParameters = localCache.getVirtualParameters( sessionContext.cacheSnapshot ); - function mergeAttributeTimestamps( - p: ParamPath, - attrs: AttributeTimestamps - ): void { + function mergeAttributeTimestamps(p: Path, attrs: AttributeTimestamps): void { let cur = allDeclareAttributeTimestamps.get(p); if (!cur) { allDeclareAttributeTimestamps.set(p, attrs); @@ -846,7 +846,7 @@ function runDeclarations( } function mergeAttributeValues( - p: ParamPath, + p: Path, attrs: AttributeValues, defer: boolean ): void { @@ -860,30 +860,29 @@ function runDeclarations( } for (const declaration of declarations) { - let path = common.addPathMeta(declaration.path); - let unpacked; + let path = declaration.path; + let unpacked: Path[]; - if ((path.alias | path.wildcard) & 1 || path[0] === "VirtualParameters") { - sessionContext.deviceData.paths.add(["VirtualParameters"] as ParamPath); + if ( + (path.alias | path.wildcard) & 1 || + path.segments[0] === "VirtualParameters" + ) { + sessionContext.deviceData.paths.add(Path.parse("VirtualParameters")); if ((path.alias | path.wildcard) & 2) { - sessionContext.deviceData.paths.add([ - "VirtualParameters", - "*" - ] as ParamPath); + sessionContext.deviceData.paths.add(Path.parse("VirtualParameters.*")); for (const k of Object.keys(allVirtualParameters)) { - sessionContext.deviceData.paths.add([ - "VirtualParameters", - k - ] as ParamPath); + sessionContext.deviceData.paths.add( + Path.parse(`VirtualParameters.${k}`) + ); } } } - if ((path.alias | path.wildcard) & 1 || path[0] === "Reboot") - sessionContext.deviceData.paths.add(["Reboot"] as ParamPath); + if ((path.alias | path.wildcard) & 1 || path.segments[0] === "Reboot") + sessionContext.deviceData.paths.add(Path.parse("Reboot")); - if ((path.alias | path.wildcard) & 1 || path[0] === "FactoryReset") - sessionContext.deviceData.paths.add(["FactoryReset"] as ParamPath); + if ((path.alias | path.wildcard) & 1 || path.segments[0] === "FactoryReset") + sessionContext.deviceData.paths.add(Path.parse("FactoryReset")); if (path.alias) { const aliasDecs = device.getAliasDeclarations( @@ -951,16 +950,14 @@ function runDeclarations( minInstances = maxInstances = declaration.pathSet; } - let parent = common.addPathMeta(path.slice(0, -1)); + let parent = path.slice(0, -1); let keys; - if (Array.isArray(path[path.length - 1])) { + if (Array.isArray(path.segments[path.length - 1])) { keys = {}; - for (let i = 0; i < path[path.length - 1].length; i += 2) { - keys[path[path.length - 1][i].join(".")] = - path[path.length - 1][i + 1]; - } - } else if (path[path.length - 1] === "*") { + for (const [p, v] of path.segments[path.length - 1]) + keys[p.toString()] = v; + } else if (path.segments[path.length - 1] === "*") { keys = {}; } @@ -986,14 +983,13 @@ function runDeclarations( sessionContext.deviceData, parent ); - for (let par of parentsUnpacked) { - par = sessionContext.deviceData.paths.add(par); + for (const par of parentsUnpacked) { processInstances( sessionContext, par, device.unpack( sessionContext.deviceData, - common.addPathMeta(par.concat([path[par.length]])) + par.concat(path.slice(-1)) ), keys, minInstances, @@ -1015,7 +1011,7 @@ function runDeclarations( export function rpcRequest( sessionContext: SessionContext, - _declarations, + _declarations: Declaration[], callback ): void { if (sessionContext.rpcRequest != null) { @@ -1217,11 +1213,11 @@ export function rpcRequest( return false; } else if (declaration[0].length === 2) { - if (declaration[0][1] === "*") { + if (declaration[0].segments[1] === "*") { for (const k of Object.keys(allVirtualParameters)) { toClear = device.set( sessionContext.deviceData, - ["VirtualParameters", k] as ParamPath, + Path.parse(`VirtualParameters.${k}`), timestamp, { object: [timestamp, 0] @@ -1237,7 +1233,9 @@ export function rpcRequest( toClear ); return false; - } else if (allVirtualParameters[declaration[0][1]]) { + } else if ( + allVirtualParameters[declaration[0].segments[1] as string] + ) { // Avoid setting on every inform as "exist" timestamp // is not saved in DB if (!sessionContext.deviceData.attributes.has(declaration[0])) { @@ -1331,12 +1329,15 @@ export function rpcRequest( if (index == null) { index = 0; for (const p of sessionContext.deviceData.paths.find( - ["Downloads", "*"] as ParamPath, + Path.parse("Downloads.*"), false, true )) { - if (+p[1] > index && sessionContext.deviceData.attributes.has(p)) - index = +p[1]; + if ( + +p.segments[1] > index && + sessionContext.deviceData.attributes.has(p) + ) + index = +p.segments[1]; } } @@ -1344,7 +1345,7 @@ export function rpcRequest( toClear = device.set( sessionContext.deviceData, - ["Downloads"] as ParamPath, + Path.parse("Downloads"), timestamp, { object: [timestamp, 1], writable: [timestamp, 1] }, toClear @@ -1352,7 +1353,7 @@ export function rpcRequest( toClear = device.set( sessionContext.deviceData, - ["Downloads", "" + index] as ParamPath, + Path.parse(`Downloads.${index}`), timestamp, { object: [timestamp, 1], writable: [timestamp, 1] }, toClear @@ -1386,7 +1387,7 @@ export function rpcRequest( for (const [k, v] of Object.entries(params)) { toClear = device.set( sessionContext.deviceData, - ["Downloads", `${index}`, k] as ParamPath, + Path.parse(`Downloads.${index}.${k}`), timestamp, { object: [timestamp, 0], @@ -1399,7 +1400,7 @@ export function rpcRequest( toClear = device.set( sessionContext.deviceData, - ["Downloads", `${index}`, "*"] as ParamPath, + Path.parse(`Downloads.${index}.*`), timestamp, null, toClear @@ -1417,7 +1418,7 @@ export function rpcRequest( toClear ); for (const p of sessionContext.syncState.downloadsValues.keys()) { - if (p[1] === instance[1]) + if (p.segments[1] === instance.segments[1]) sessionContext.syncState.downloadsValues.delete(p); } } @@ -1504,7 +1505,7 @@ export function rpcRequest( toClear = device.set( sessionContext.deviceData, - ["VirtualParameters", vparams[i][0]] as ParamPath, + Path.parse(`VirtualParameters.${vparams[i][0]}`), timestamp, vpu, toClear @@ -1541,10 +1542,7 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest { } if (!found) { - const p = sessionContext.deviceData.paths.add(path.slice( - 0, - -1 - ) as ParamPath); + const p = sessionContext.deviceData.paths.add(path.slice(0, -1)); syncState.gpn.add(p); const f = 1 << p.length; syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p)); @@ -1572,10 +1570,7 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest { } if (!found) { - const p = sessionContext.deviceData.paths.add(path.slice( - 0, - -1 - ) as ParamPath); + const p = sessionContext.deviceData.paths.add(path.slice(0, -1)); syncState.gpn.add(p); const f = 1 << p.length; syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p)); @@ -1584,10 +1579,7 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest { syncState.refreshAttributes.object.clear(); for (const path of syncState.refreshAttributes.writable) { - const p = sessionContext.deviceData.paths.add(path.slice( - 0, - -1 - ) as ParamPath); + const p = sessionContext.deviceData.paths.add(path.slice(0, -1)); syncState.gpn.add(p); const f = 1 << p.length; syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p)); @@ -1647,7 +1639,7 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest { return { name: "GetParameterNames", - parameterPath: path.concat("").join("."), + parameterPath: path.length ? path.toString() + "." : "", nextLevel: nextLevel }; } @@ -1660,13 +1652,13 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest { sessionContext.configContext ); - const parameterNames = []; + const parameterNames: string[] = []; for (const path of syncState.refreshAttributes.value) { syncState.refreshAttributes.value.delete(path); // Need to check in case param is deleted or changed to object const attrs = sessionContext.deviceData.attributes.get(path); if (attrs && attrs.object && attrs.object[1] === 0) { - parameterNames.push(path); + parameterNames.push(path.toString()); if (parameterNames.length >= GPV_BATCH_SIZE) break; } } @@ -1674,14 +1666,14 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest { if (parameterNames.length) { return { name: "GetParameterValues", - parameterNames: parameterNames.map(p => p.join(".")) + parameterNames: parameterNames }; } } return null; } -function generateSetRpcRequest(sessionContext): SetAcsRequest { +function generateSetRpcRequest(sessionContext: SessionContext): SetAcsRequest { const syncState = sessionContext.syncState; if (!syncState) return null; @@ -1693,7 +1685,7 @@ function generateSetRpcRequest(sessionContext): SetAcsRequest { if (instance && sessionContext.deviceData.attributes.has(instance)) { return { name: "DeleteObject", - objectName: instance.concat("").join(".") + objectName: instance.toString() + "." }; } } @@ -1706,7 +1698,7 @@ function generateSetRpcRequest(sessionContext): SetAcsRequest { instances.delete(instance); return { name: "AddObject", - objectName: param.concat("").join("."), + objectName: param.toString() + ".", instanceValues: instance, next: "getInstanceKeys" }; @@ -1733,17 +1725,13 @@ function generateSetRpcRequest(sessionContext): SetAcsRequest { sessionContext.configContext ); - const parameterValues = [] as [ - ParamPath, - string | number | boolean, - string - ][]; + const parameterValues: [Path, string | number | boolean, string][] = []; for (const [k, v] of syncState.spv) { syncState.spv.delete(k); const attrs = sessionContext.deviceData.attributes.get(k); const curVal = attrs.value ? attrs.value[1] : null; if (curVal && attrs.writable && attrs.writable[1]) { - const val = v.slice(); + const val = v.slice() as [string | number | boolean, string]; if (!val[1]) val[1] = curVal[1]; device.sanitizeParameterValue(val); @@ -1767,7 +1755,7 @@ function generateSetRpcRequest(sessionContext): SetAcsRequest { name: "SetParameterValues", parameterList: parameterValues.map( p => - [p[0].join("."), p[1], p[2]] as [ + [p[0].toString(), p[1], p[2]] as [ string, string | number | boolean, string @@ -1783,32 +1771,34 @@ function generateSetRpcRequest(sessionContext): SetAcsRequest { const attrs = deviceData.attributes.get(p); if (!(attrs && attrs.value && t <= attrs.value[1][0])) { const fileTypeAttrs = deviceData.attributes.get( - deviceData.paths.get(p.slice(0, -1).concat("FileType")) + deviceData.paths.get(p.slice(0, -1).concat(Path.parse("FileType"))) ); const fileNameAttrs = deviceData.attributes.get( - deviceData.paths.get(p.slice(0, -1).concat("FileName")) + deviceData.paths.get(p.slice(0, -1).concat(Path.parse("FileName"))) ); const targetFileNameAttrs = deviceData.attributes.get( - deviceData.paths.get(p.slice(0, -1).concat("TargetFileName")) + deviceData.paths.get( + p.slice(0, -1).concat(Path.parse("TargetFileName")) + ) ); return { name: "Download", commandKey: generateRpcId(sessionContext), - instance: p[1], + instance: p.segments[1] as string, fileType: fileTypeAttrs ? fileTypeAttrs.value - ? fileTypeAttrs.value[1][0] + ? (fileTypeAttrs.value[1][0] as string) : null : null, fileName: fileNameAttrs ? fileNameAttrs.value - ? fileNameAttrs.value[1][0] + ? (fileNameAttrs.value[1][0] as string) : null : null, targetFileName: targetFileNameAttrs ? targetFileNameAttrs.value - ? targetFileNameAttrs.value[1][0] + ? (targetFileNameAttrs.value[1][0] as string) : null : null }; @@ -1817,7 +1807,7 @@ function generateSetRpcRequest(sessionContext): SetAcsRequest { // Reboot if (syncState.reboot) { - const p = sessionContext.deviceData.paths.get(["Reboot"]); + const p = sessionContext.deviceData.paths.get(Path.parse("Reboot")); const attrs = p ? sessionContext.deviceData.attributes.get(p) : null; if (!(attrs && attrs.value && attrs.value[1][0] >= syncState.reboot)) { delete syncState.reboot; @@ -1827,7 +1817,7 @@ function generateSetRpcRequest(sessionContext): SetAcsRequest { // Factory reset if (syncState.factoryReset) { - const p = sessionContext.deviceData.paths.get(["FactoryReset"]); + const p = sessionContext.deviceData.paths.get(Path.parse("FactoryReset")); const attrs = p ? sessionContext.deviceData.attributes.get(p) : null; if ( !(attrs && attrs.value && attrs.value[1][0] >= syncState.factoryReset) @@ -1841,10 +1831,10 @@ function generateSetRpcRequest(sessionContext): SetAcsRequest { } function generateGetVirtualParameterProvisions( - sessionContext, - virtualParameterDeclarations + sessionContext: SessionContext, + virtualParameterDeclarations: VirtualParameterDeclaration[] ): [ - ParamPath, + string, AttributeTimestamps, AttributeValues, AttributeTimestamps, @@ -1873,7 +1863,7 @@ function generateGetVirtualParameterProvisions( if (Object.keys(dec).length) { if (!provisions) provisions = []; provisions.push([ - declaration[0][1], + declaration[0].segments[1], dec, {}, currentTimestamps, @@ -1887,10 +1877,10 @@ function generateGetVirtualParameterProvisions( } function generateSetVirtualParameterProvisions( - sessionContext, - virtualParameterDeclarations + sessionContext: SessionContext, + virtualParameterDeclarations: VirtualParameterDeclaration[] ): [ - ParamPath, + string, AttributeTimestamps, AttributeValues, AttributeTimestamps, @@ -1908,7 +1898,10 @@ function generateSetVirtualParameterProvisions( attrs.value && attrs.value[1] != null ) { - const val = declaration[2].value.slice(); + const val = declaration[2].value.slice() as [ + string | number | boolean, + string + ]; if (val[1] == null) val[1] = attrs.value[1][1]; device.sanitizeParameterValue(val); @@ -1923,7 +1916,7 @@ function generateSetVirtualParameterProvisions( } provisions.push([ - declaration[0][1], + declaration[0].segments[1], {}, { value: val }, currentTimestamps, @@ -1939,16 +1932,16 @@ function generateSetVirtualParameterProvisions( } function processDeclarations( - sessionContext, + sessionContext: SessionContext, allDeclareTimestamps, - allDeclareAttributeTimestamps: Map, - allDeclareAttributeValues: Map + allDeclareAttributeTimestamps: Map, + allDeclareAttributeValues: Map ): VirtualParameterDeclaration[] { const deviceData = sessionContext.deviceData; const syncState = sessionContext.syncState; - const root = sessionContext.deviceData.paths.add([]); - const paths = deviceData.paths.find([], false, true, 99); + const root = sessionContext.deviceData.paths.add(Path.parse("")); + const paths = deviceData.paths.find(root, false, true, 99); paths.sort( (a, b): number => a.wildcard === b.wildcard ? a.length - b.length : a.wildcard - b.wildcard @@ -1957,13 +1950,13 @@ function processDeclarations( const virtualParameterDeclarations = [] as VirtualParameterDeclaration[]; function func( - leafParam: ParamPath, + leafParam: Path, leafIsObject: number, leafTimestamp: number, - _paths: ParamPath[] + _paths: Path[] ): void { const currentPath = _paths[0]; - const children = new Map(); + const children = new Map(); let declareTimestamp = 0; let declareAttributeTimestamps; let declareAttributeValues; @@ -1975,12 +1968,12 @@ function processDeclarations( for (const path of _paths) { if (path.length > currentPath.length) { - const fragment = path[currentPath.length]; + const fragment = path.segments[currentPath.length] as string; let child = children.get(fragment); if (!child) { if (path.length > currentPath.length + 1) { // This is to ensure we don't descend more than one step at a time - const p = common.addPathMeta(path.slice(0, currentPath.length + 1)); + const p = path.slice(0, currentPath.length + 1); child = [p]; } else { child = []; @@ -2036,7 +2029,11 @@ function processDeclarations( leafTimestamp = Math.max(leafTimestamp, currentTimestamp); } - switch (currentPath[0] !== "*" ? currentPath[0] : leafParam[0]) { + switch ( + currentPath.segments[0] !== "*" + ? currentPath.segments[0] + : leafParam.segments[0] + ) { case "Reboot": if (currentPath.length === 1) { if (declareAttributeValues && declareAttributeValues.value) @@ -2056,13 +2053,10 @@ function processDeclarations( declareAttributeValues && declareAttributeValues.value ) { - syncState.tags.set( - currentPath, - device.sanitizeParameterValue([ - declareAttributeValues.value[0], - "xsd:boolean" - ])[0] - ); + syncState.tags.set(currentPath, device.sanitizeParameterValue([ + declareAttributeValues.value[0], + "xsd:boolean" + ])[0] as boolean); } break; @@ -2077,7 +2071,7 @@ function processDeclarations( declareAttributeValues && declareAttributeValues.value ) { - if (currentPath[2] === "Download") { + if (currentPath.segments[2] === "Download") { syncState.downloadsDownload.set( currentPath, declareAttributeValues.value[0] @@ -2150,7 +2144,7 @@ function processDeclarations( syncState.refreshAttributes.object.add(leafParam); if (leafIsObject == null) { const f = - ((1 << syncState.gpnPatterns.length) - 1) ^ + ((1 << syncState.gpnPatterns.size) - 1) ^ ((1 << leafParam.length) - 1); syncState.gpnPatterns.set( currentPath, @@ -2221,7 +2215,7 @@ function processDeclarations( function loadPath( sessionContext: SessionContext, - path: ParamPath, + path: Path, depth?: number ): boolean { depth = depth || (1 << path.length) - 1; @@ -2231,15 +2225,15 @@ function loadPath( // Trim trailing wildcards let trimWildcard = path.length; - while (trimWildcard && path[trimWildcard - 1] === "*") --trimWildcard; + while (trimWildcard && path.segments[trimWildcard - 1] === "*") + --trimWildcard; - if (trimWildcard < path.length) - path = path.slice(0, trimWildcard) as ParamPath; + if (trimWildcard < path.length) path = path.slice(0, trimWildcard); for (let i = 0; i <= path.length; ++i) { const d = i === path.length ? 99 : i; for (const sup of sessionContext.deviceData.paths.find( - path.slice(0, i) as ParamPath, + path.slice(0, i), true, false, d @@ -2260,16 +2254,17 @@ function loadPath( } function processInstances( - sessionContext, - parent, - parameters, - keys, - minInstances, - maxInstances, - defer + sessionContext: SessionContext, + parent: Path, + parameters: Path[], + keys: {}, + minInstances: number, + maxInstances: number, + defer: boolean ): void { + parent = sessionContext.deviceData.paths.add(parent); let instancesToCreate, instancesToDelete; - if (parent[0] === "Downloads") { + if (parent.segments[0] === "Downloads") { if (parent.length !== 1) return; instancesToDelete = sessionContext.syncState.downloadsToDelete; instancesToCreate = sessionContext.syncState.downloadsToCreate; @@ -2319,7 +2314,11 @@ function processInstances( } } -function clear(sessionContext, toClear, callback): void { +function clear( + sessionContext: SessionContext, + toClear: Clear[], + callback +): void { if (!toClear || !toClear.length) return void callback(); const MAX_DEPTH = +config.get("MAX_DEPTH", sessionContext.deviceId); @@ -2352,13 +2351,19 @@ function clear(sessionContext, toClear, callback): void { }); } -export function rpcResponse(sessionContext, id, rpcRes, callback): void { +export function rpcResponse( + sessionContext: SessionContext, + id: string, + _rpcRes: CpeResponse, + callback +): void { if (id !== generateRpcId(sessionContext)) return void callback(new Error("Request ID not recognized")); ++sessionContext.rpcCount; - const rpcReq = sessionContext.rpcRequest; + const rpcRes = _rpcRes as CpeGetResponse & CpeSetResponse; + const rpcReq = sessionContext.rpcRequest as GetAcsRequest & SetAcsRequest; if (!rpcReq.next) { sessionContext.rpcRequest = null; @@ -2374,32 +2379,34 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { if (!parameterNames.length) { sessionContext.rpcRequest = null; } else { - sessionContext.rpcRequest = { + const r: GetAcsRequest = { name: "GetParameterValues", parameterNames: parameterNames, next: "setInstanceKeys", instanceValues: instanceValues }; + sessionContext.rpcRequest = r; } } else if (rpcReq.next === "setInstanceKeys") { - const parameterList = []; + const parameterList: [string, string | number | boolean, string][] = []; for (const p of rpcRes.parameterList) { if (p[1] !== rpcReq.instanceValues[p[0]]) { - parameterList.push( - [p[0]].concat( - device.sanitizeParameterValue([rpcReq.instanceValues[p[0]], p[2]]) - ) - ); + const v = device.sanitizeParameterValue([ + rpcReq.instanceValues[p[0]], + p[2] + ]); + parameterList.push([p[0], v[0], v[1]]); } } if (!parameterList.length) { sessionContext.rpcRequest = null; } else { - sessionContext.rpcRequest = { + const r: SetAcsRequest = { name: "SetParameterValues", parameterList: parameterList }; + sessionContext.rpcRequest = r; } } @@ -2410,7 +2417,11 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { sessionContext.deviceData.timestamps.revision = revision; sessionContext.deviceData.attributes.revision = revision; - let toClear, root, missing, params; + let toClear, + root: Path, + missing, + params: [Path, number, Attributes?][], + wildcardPath: Path; switch (rpcRes.name) { case "GetParameterValuesResponse": @@ -2423,7 +2434,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { for (const p of rpcRes.parameterList) { toClear = device.set( sessionContext.deviceData, - common.parsePath(p[0]), + Path.parse(p[0]), timestamp, { object: [timestamp, 0], value: [timestamp, p.slice(1)] }, toClear @@ -2439,10 +2450,11 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { } if (rpcReq.parameterPath.endsWith(".")) - root = common.parsePath(rpcReq.parameterPath.slice(0, -1)); - else root = common.parsePath(rpcReq.parameterPath); + root = Path.parse(rpcReq.parameterPath.slice(0, -1)); + else root = Path.parse(rpcReq.parameterPath); - params = [[root.concat("*"), timestamp]]; + wildcardPath = Path.parse("*"); + params = [[root.concat(wildcardPath), timestamp]]; // Some clients don't report all ancestors explicitly missing = {}; @@ -2454,8 +2466,9 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { if (p[0].endsWith(".")) { missing[p[0].slice(0, -1)] |= 1; - const path = common.parsePath(p[0].slice(0, -1)); - if (!rpcReq.nextLevel) params.push([path.concat("*"), timestamp]); + const path = Path.parse(p[0].slice(0, -1)); + if (!rpcReq.nextLevel) + params.push([path.concat(wildcardPath), timestamp]); params.push([ path, @@ -2465,7 +2478,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { } else { missing[p[0]] |= 1; params.push([ - common.parsePath(p[0]), + Path.parse(p[0]), timestamp, { object: [timestamp, 0], writable: [timestamp, p[1] ? 1 : 0] } ]); @@ -2475,13 +2488,13 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { for (const [k, v] of Object.entries(missing)) { if (v === 0) { // TODO consider showing a warning - const path = common.parsePath(k); + const path = Path.parse(k); params.push([ path, timestamp, { object: [timestamp, 1], writable: [timestamp, 0] } ]); - params.push([path.concat("*"), timestamp]); + params.push([path.concat(wildcardPath), timestamp]); } } @@ -2492,8 +2505,8 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { params.sort((a, b) => { let al = a[0].length; let bl = b[0].length; - if (b[0][bl - 1] === "*") bl *= -1; - if (a[0][al - 1] === "*") al *= -1; + if (b[0].segments[bl - 1] === "*") bl *= -1; + if (a[0].segments[al - 1] === "*") al *= -1; return bl - al; }); @@ -2517,7 +2530,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { "VirtualParameters", "Downloads" ]) { - const p = sessionContext.deviceData.paths.get([n]); + const p = sessionContext.deviceData.paths.get(Path.parse(n)); if (p && sessionContext.deviceData.attributes.has(p)) sessionContext.deviceData.timestamps.set(p, timestamp); } @@ -2546,7 +2559,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { for (const p of rpcReq.parameterList) { toClear = device.set( sessionContext.deviceData, - common.parsePath(p[0]), + Path.parse(p[0]), timestamp + 1, { object: [timestamp + 1, 0], @@ -2561,7 +2574,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { case "AddObjectResponse": toClear = device.set( sessionContext.deviceData, - common.parsePath(rpcReq.objectName + rpcRes.instanceNumber), + Path.parse(rpcReq.objectName + rpcRes.instanceNumber), timestamp + 1, { object: [timestamp + 1, 1] }, toClear @@ -2570,7 +2583,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { case "DeleteObjectResponse": toClear = device.set( sessionContext.deviceData, - common.parsePath(rpcReq.objectName.slice(0, -1)), + Path.parse(rpcReq.objectName.slice(0, -1)), timestamp + 1, null, toClear @@ -2579,7 +2592,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { case "RebootResponse": toClear = device.set( sessionContext.deviceData, - common.parsePath("Reboot"), + Path.parse("Reboot"), timestamp + 1, { value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] }, toClear @@ -2588,7 +2601,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { case "FactoryResetResponse": toClear = device.set( sessionContext.deviceData, - common.parsePath("FactoryReset"), + Path.parse("FactoryReset"), timestamp + 1, { value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] }, toClear @@ -2597,7 +2610,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { case "DownloadResponse": toClear = device.set( sessionContext.deviceData, - ["Downloads", rpcReq.instance, "Download"] as ParamPath, + Path.parse(`Download.${rpcReq.instance}.Download`), timestamp + 1, { value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] }, toClear @@ -2606,7 +2619,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { if (rpcRes.status === 0) { toClear = device.set( sessionContext.deviceData, - ["Downloads", rpcReq.instance, "LastDownload"] as ParamPath, + Path.parse(`Downloads.${rpcReq.instance}.LastDownload`), timestamp + 1, { value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] @@ -2616,7 +2629,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { toClear = device.set( sessionContext.deviceData, - ["Downloads", rpcReq.instance, "LastFileType"] as ParamPath, + Path.parse(`Downloads.${rpcReq.instance}.LastFileType`), timestamp + 1, { value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] }, toClear @@ -2624,7 +2637,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { toClear = device.set( sessionContext.deviceData, - ["Downloads", rpcReq.instance, "LastFileName"] as ParamPath, + Path.parse(`Downloads.${rpcReq.instance}.LastFileName`), timestamp + 1, { value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] }, toClear @@ -2632,7 +2645,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { toClear = device.set( sessionContext.deviceData, - ["Downloads", rpcReq.instance, "LastTargetFileName"] as ParamPath, + Path.parse(`Downloads.${rpcReq.instance}.LastTargetFileName`), timestamp + 1, { value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] }, toClear @@ -2640,7 +2653,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { toClear = device.set( sessionContext.deviceData, - ["Downloads", rpcReq.instance, "StartTime"] as ParamPath, + Path.parse(`Downloads.${rpcReq.instance}.StartTime`), timestamp + 1, { value: [timestamp + 1, [+rpcRes.startTime, "xsd:dateTime"]] }, toClear @@ -2648,7 +2661,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { toClear = device.set( sessionContext.deviceData, - ["Downloads", rpcReq.instance, "CompleteTime"] as ParamPath, + Path.parse(`Downloads.${rpcReq.instance}.CompleteTime`), timestamp + 1, { value: [timestamp + 1, [+rpcRes.completeTime, "xsd:dateTime"]] }, toClear @@ -2686,49 +2699,46 @@ export function rpcResponse(sessionContext, id, rpcRes, callback): void { } export function rpcFault( - sessionContext, - id, - faultResponse, + sessionContext: SessionContext, + id: string, + faultResponse: CpeFault, callback: (err: Error, fault?: Fault) => void ): void { - const rpcReq = sessionContext.rpcRequest; + const rpcReq = sessionContext.rpcRequest as GetAcsRequest & SetAcsRequest; delete sessionContext.syncState; delete sessionContext.rpcRequest; // Recover from invalid parameter name faults if (faultResponse.detail.faultCode === "9005") { const timestamp = sessionContext.timestamp + sessionContext.iteration; - let toClear; + let toClear: Clear[]; if (rpcReq.name === "GetParameterNames") { if (rpcReq.parameterPath) { toClear = [ - [rpcReq.parameterPath.replace(/\.$/, "").split("."), timestamp] + [Path.parse(rpcReq.parameterPath.replace(/\.$/, "")), timestamp] ]; } } else if (rpcReq.name === "GetParameterValues") { - toClear = rpcReq.parameterNames.map(p => [ - p.replace(/\.$/, "").split("."), - timestamp - ]); + toClear = rpcReq.parameterNames.map( + p => [Path.parse(p.replace(/\.$/, "")), timestamp] as Clear + ); } else if (rpcReq.name === "SetParameterValues") { - toClear = rpcReq.parameterList.map(p => [ - p[0].replace(/\.$/, "").split("."), - timestamp - ]); + toClear = rpcReq.parameterList.map( + p => [Path.parse(p[0].replace(/\.$/, "")), timestamp] as Clear + ); } else if (rpcReq.name === "AddObject") { - toClear = [[rpcReq.objectName.replace(/\.$/, "").split("."), timestamp]]; + toClear = [[Path.parse(rpcReq.objectName.replace(/\.$/, "")), timestamp]]; } else if (rpcReq.name === "DeleteObject") { - toClear = [[rpcReq.objectName.replace(/\.$/, "").split("."), timestamp]]; + toClear = [[Path.parse(rpcReq.objectName.replace(/\.$/, "")), timestamp]]; } if (toClear) return void clear(sessionContext, toClear, callback); } - const fault = { + const fault: Fault = { code: `cwmp.${faultResponse.detail.faultCode}`, message: faultResponse.detail.faultString, - detail: faultResponse.detail, - timestamp: sessionContext.timestamp + detail: faultResponse.detail }; callback(null, fault); @@ -2738,11 +2748,11 @@ export function deserialize(sessionContextString, callback): void { const sessionContext = JSON.parse(sessionContextString); for (const decs of sessionContext.declarations) - for (const d of decs) common.addPathMeta(d.path); + for (const d of decs) d.path = Path.parse(d.path); const deviceData = initDeviceData(); for (const r of sessionContext.deviceData) { - const path = deviceData.paths.add(r[0]); + const path = deviceData.paths.add(Path.parse(r[0])); if (r[1]) deviceData.loaded.set(path, r[1]); @@ -2761,31 +2771,34 @@ export function deserialize(sessionContextString, callback): void { }); } -export function serialize(sessionContext, callback): void { +export function serialize(sessionContext: SessionContext, callback): void { const deviceData = []; for (const path of sessionContext.deviceData.paths.find( - [], + Path.parse(""), false, false, 99 )) { - const e = [path]; - e[1] = sessionContext.deviceData.loaded.get(path) || 0; - e[2] = sessionContext.deviceData.trackers.get(path) || null; - e[3] = sessionContext.deviceData.timestamps.getRevisions(path) || null; - e[4] = sessionContext.deviceData.attributes.getRevisions(path) || null; + const e = [ + path.toString(), + sessionContext.deviceData.loaded.get(path) || 0, + sessionContext.deviceData.trackers.get(path) || null, + sessionContext.deviceData.timestamps.getRevisions(path) || null, + sessionContext.deviceData.attributes.getRevisions(path) || null + ]; deviceData.push(e); } - sessionContext = Object.assign({}, sessionContext); - sessionContext.deviceData = deviceData; - delete sessionContext.syncState; - delete sessionContext.toLoad; - delete sessionContext.httpRequest; - delete sessionContext.httpResponse; + const jsonSessionContext = Object.assign({}, sessionContext, { + deviceData: deviceData, + syncState: null, + toLoad: null, + httpRequest: null, + httpResponse: null + }); - const sessionContextString = JSON.stringify(sessionContext); + const sessionContextString = JSON.stringify(jsonSessionContext); callback(null, sessionContextString); } diff --git a/lib/types.ts b/lib/types.ts index 4c726cb..3323ee6 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,27 +1,24 @@ -import PathSet from "./path-set"; +import Path from "./common/path"; +import PathSet from "./common/path-set"; import VersionedMap from "./versioned-map"; import InstanceSet from "./instance-set"; import { IncomingMessage, ServerResponse } from "http"; -export interface ParamPath extends Array { - wildcard: number; - alias: number; -} - export type Expression = string | number | boolean | null | any[]; export interface Fault { code: string; message: string; - detail?: { - name: string; - message: string; - stack?: string; - }; + detail?: + | FaultStruct + | { + name: string; + message: string; + stack?: string; + }; } export interface SessionFault extends Fault { - channel: string; timestamp: number; provisions: string[][]; retryNow?: boolean; @@ -50,15 +47,15 @@ export interface AttributeValues { export interface DeviceData { paths: PathSet; - timestamps: VersionedMap; - attributes: VersionedMap; - loaded: Map; - trackers: Map; + timestamps: VersionedMap; + attributes: VersionedMap; + loaded: Map; + trackers: Map; changes: Set; } export type VirtualParameterDeclaration = [ - ParamPath, + Path, { path?: number; object?: number; writable?: number; value?: number }?, { path?: [number, number]; @@ -70,22 +67,24 @@ export type VirtualParameterDeclaration = [ export interface SyncState { refreshAttributes: { - exist: Set; - object: Set; - writable: Set; - value: Set; + exist: Set; + object: Set; + writable: Set; + value: Set; }; - spv: Map; - gpn: Set; - gpnPatterns: Map; - tags: Map; + spv: Map; + gpn: Set; + gpnPatterns: Map; + tags: Map; virtualParameterDeclarations: VirtualParameterDeclaration[][]; - instancesToDelete: Map>; - instancesToCreate: Map; - downloadsToDelete: Set; + instancesToDelete: Map>; + instancesToCreate: Map; + downloadsToDelete: Set; downloadsToCreate: InstanceSet; - downloadsValues: Map; - downloadsDownload: Map; + downloadsValues: Map; + downloadsDownload: Map; + reboot: number; + factoryReset: number; } export interface SessionContext { @@ -97,13 +96,19 @@ export interface SessionContext { timeout: number; provisions: any[]; channels: { [channel: string]: number }; - virtualParameters: any[]; + virtualParameters: [ + string, + AttributeTimestamps, + AttributeValues, + AttributeTimestamps, + AttributeValues + ][][]; revisions: number[]; rpcCount: number; iteration: number; cycle: number; extensionsCache: any; - declarations: any[]; + declarations: Declaration[][]; faults?: { [channel: string]: SessionFault }; retries?: { [channel: string]: number }; cacheSnapshot?: string; @@ -112,7 +117,7 @@ export interface SessionContext { httpRequest?: IncomingMessage; faultsTouched?: { [channel: string]: boolean }; presetCycles?: number; - toLoad?: Map; + toLoad?: Map; new?: boolean; debug?: boolean; tasks?: Task[]; @@ -154,6 +159,7 @@ export interface Operation { export interface AcsRequest { name: string; + next?: string; } export interface GetAcsRequest extends AcsRequest { @@ -162,6 +168,7 @@ export interface GetAcsRequest extends AcsRequest { parameterNames?: string[]; parameterPath?: string; nextLevel?: boolean; + instanceValues?: { [name: string]: string }; } export interface SetAcsRequest extends AcsRequest { @@ -175,7 +182,6 @@ export interface SetAcsRequest extends AcsRequest { parameterList?: [string, string | number | boolean, string][]; instanceValues?: { [param: string]: string | number | boolean }; objectName?: string; - next?: string; DATETIME_MILLISECONDS?: boolean; BOOLEAN_LITERAL?: boolean; commandKey?: string; @@ -266,7 +272,7 @@ export interface QueryOptions { } export interface Declaration { - path: ParamPath; + path: Path; pathGet: number; pathSet?: [number, number]; attrGet?: { object?: number; writable?: number; value?: number }; @@ -279,7 +285,7 @@ export interface Declaration { } export type Clear = [ - ParamPath, + Path, number, { object?: number; writable?: number; value?: number }?, number? diff --git a/test/common.ts b/test/common.ts index 5af7c57..c08978d 100644 --- a/test/common.ts +++ b/test/common.ts @@ -1,6 +1,5 @@ import ava from "ava"; import * as common from "../lib/common"; -import { ParamPath } from "../lib/types"; ava("typeOf", t => { const cases = [ @@ -63,73 +62,6 @@ ava("generateDeviceId", t => { for (const c of cases) t.is(common.generateDeviceId(c[0]), c[1]); }); -ava("parsePath", t => { - const case1 = ["Seg1.Seg2.Seg3", ["Seg1", "Seg2", "Seg3"]] as ParamPath; - case1[1].alias = 0; - case1[1].wildcard = 0; - - const case2 = ["Seg1.*.*.Seg4", ["Seg1", "*", "*", "Seg4"]] as ParamPath; - case2[1].alias = 0; - case2[1].wildcard = 6; - - const case3 = [ - "Seg1.Seg2.[Seg6:Seg9].Seg3", - ["Seg1", "Seg2", [["Seg6"], "Seg9"], "Seg3"] - ] as ParamPath; - case3[1].alias = 4; - case3[1].wildcard = 0; - case3[1][2][0].alias = 0; - case3[1][2][0].wildcard = 0; - - const case4 = ["Seg1.[*:SegAny]", ["Seg1", [["*"], "SegAny"]]] as ParamPath; - case4[1].alias = 2; - case4[1].wildcard = 0; - case4[1][1][0].alias = 0; - case4[1][1][0].wildcard = 1; - - const cases = [case1, case2, case3, case4]; - for (const c of cases) t.deepEqual(common.parsePath(c[0]), c[1]); -}); - -ava("addPathMeta", t => { - const case1 = [ - ["Seg1", "Seg2", "Seg3"], - ["Seg1", "Seg2", "Seg3"] - ] as ParamPath; - case1[1].alias = 0; - case1[1].wildcard = 0; - - const case2 = [ - ["Seg1", "*", "*", "Seg4"], - ["Seg1", "*", "*", "Seg4"] - ] as ParamPath; - case2[1].alias = 0; - case2[1].wildcard = 6; - - const case3 = [ - ["Seg1", "Seg2", [["Seg6"], "Seg9"], "Seg3"], - ["Seg1", "Seg2", [["Seg6"], "Seg9"], "Seg3"] - ] as ParamPath; - case3[1].alias = 4; - case3[1].wildcard = 0; - case3[1][2][0].alias = 0; - case3[1][2][0].wildcard = 0; - - const case4 = [ - ["Seg1", [["*"], "SegAny"]], - ["Seg1", [["*"], "SegAny"]] - ] as ParamPath; - case4[1].alias = 2; - case4[1].wildcard = 0; - case4[1][1][0].alias = 0; - case4[1][1][0].wildcard = 1; - - const cases = [case1, case2, case3, case4]; - - t.plan(cases.length); - for (const c of cases) t.deepEqual(common.addPathMeta(c[0]), c[1]); -}); - ava("escapeRegExp", t => { t.is( common.escapeRegExp("\\ ^ $ * + ? . ( ) | { } [ ]"), diff --git a/test/device.ts b/test/device.ts new file mode 100644 index 0000000..59e603c --- /dev/null +++ b/test/device.ts @@ -0,0 +1,20 @@ +import ava from "ava"; +import Path from "../lib/common/path"; +import * as device from "../lib/device"; + +ava("getAliasDeclarations", t => { + const path = Path.parse("a.[aa:10,bb.[aaa:100].cc:1].b"); + const decs = device.getAliasDeclarations(path, 99); + + const expected = ["a.*.b", "a.*.aa", "a.*.bb.*.cc", "a.*.bb.*.aaa"]; + + t.plan(24); + for (const [i, d] of decs.entries()) { + t.is(d.path.toString(), expected[i]); + t.is(d.pathGet, 99); + t.is(d.pathSet, null); + t.deepEqual(d.attrGet, i ? { value: 99 } : null); + t.is(d.attrSet, null); + t.is(d.defer, true); + } +});