import { writeFile } from 'node:fs/promises' import { XMLParser } from 'fast-xml-parser' const OUTPUT_PATH = new URL('../config/tr181-catalog.json', import.meta.url) const INDEX_URL = 'https://cwmp-data-models.broadband-forum.org/' const SOURCE_CANDIDATES = [ 'https://raw.githubusercontent.com/BroadbandForum/usp-data-models/master/tr-181-2-usp.xml', 'https://raw.githubusercontent.com/BroadbandForum/usp-data-models/master/tr-181-2-common.xml', 'https://raw.githubusercontent.com/BroadbandForum/cwmp-data-models/master/tr-181-2-18-0.xml', ] const XML_PARSER = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_', }) async function fetchText(url) { const response = await fetch(url) if (!response.ok) { throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`) } return response.text() } function toArray(value) { if (!value) { return [] } return Array.isArray(value) ? value : [value] } function getDocumentRoot(parsedXml) { return parsedXml['dm:document'] ?? parsedXml.document ?? parsedXml } function normalizeObjectPath(objectName) { return objectName.endsWith('.') ? objectName.slice(0, -1) : objectName } function extractImportUrl(baseUrl, importNode) { const fileReference = importNode?.['@_file'] ?? importNode?.['@_href'] ?? null if (!fileReference) { return null } return new URL(fileReference, baseUrl).href } function discoverLatestCwmpFullUrl(indexHtml) { const matches = [...indexHtml.matchAll(/href=["'](tr-181-[^"']+-cwmp-full\.xml)["']/gi)] if (matches.length === 0) { throw new Error('Could not discover a TR-181 CWMP full XML URL from the Broadband Forum index') } return new URL(matches[0][1], INDEX_URL).href } async function resolveRootSourceUrl() { for (const candidate of SOURCE_CANDIDATES) { try { await fetchText(candidate) return candidate } catch { // Fall through to the next candidate. } } const indexHtml = await fetchText(INDEX_URL) return discoverLatestCwmpFullUrl(indexHtml) } async function loadDocuments(rootUrl) { const queuedUrls = [rootUrl] const visitedUrls = new Set() const documents = [] while (queuedUrls.length > 0) { const currentUrl = queuedUrls.shift() if (!currentUrl || visitedUrls.has(currentUrl)) { continue } visitedUrls.add(currentUrl) const xmlText = await fetchText(currentUrl) const parsedXml = XML_PARSER.parse(xmlText) const documentRoot = getDocumentRoot(parsedXml) documents.push({ url: currentUrl, documentRoot }) for (const importNode of toArray(documentRoot.import)) { const importUrl = extractImportUrl(currentUrl, importNode) if (importUrl && !visitedUrls.has(importUrl)) { queuedUrls.push(importUrl) } } } return documents } function extractCatalogEntries(documentRoot) { const catalogEntries = [] for (const model of toArray(documentRoot.model)) { for (const objectNode of toArray(model.object)) { const objectName = objectNode?.['@_name'] if (!objectName) { continue } const objectPath = normalizeObjectPath(objectName) const objectAccess = objectNode['@_access'] ?? 'readOnly' for (const parameterNode of toArray(objectNode.parameter)) { const parameterName = parameterNode?.['@_name'] if (!parameterName) { continue } const parameterAccess = parameterNode['@_access'] ?? objectAccess catalogEntries.push({ path: `${objectPath}.${parameterName}`, writable: parameterAccess === 'readWrite', deviceClass: 'tr181', }) } } } return catalogEntries } async function main() { const rootSourceUrl = await resolveRootSourceUrl() const documents = await loadDocuments(rootSourceUrl) const catalogByPath = new Map() for (const { documentRoot } of documents) { for (const entry of extractCatalogEntries(documentRoot)) { const existingEntry = catalogByPath.get(entry.path) if (!existingEntry) { catalogByPath.set(entry.path, entry) continue } if (existingEntry.writable !== entry.writable) { throw new Error(`Conflicting writability detected for ${entry.path}`) } } } const catalogEntries = [...catalogByPath.values()].sort((left, right) => left.path.localeCompare(right.path) ) await writeFile(OUTPUT_PATH, `${JSON.stringify(catalogEntries, null, 2)}\n`) console.log( `Generated ${catalogEntries.length} TR-181 parameters from ${documents.length} XML document(s); source=${rootSourceUrl}` ) } await main()