diff --git a/IANA_languages.js b/IANA_languages.js index 9c35d08..dc80957 100644 --- a/IANA_languages.js +++ b/IANA_languages.js @@ -1,6 +1,6 @@ /** * IANA_languages.js - * + * * Load and check language identifiers */ import { readFile, readFileSync } from "fs"; @@ -46,6 +46,9 @@ export default class IANAlanguages { stats(res) { res.numLanguages = this.#languagesList.length; res.numRedundantLanguages = this.#redundantLanguagesList.length; + let t = []; + this.#redundantLanguagesList.forEach((e) => t.push(`${e.tag}${e.preferred ? `~${e.preferred}` : ""}`)); + res.RedundantLanguages = t.join(", "); res.numLanguageRanges = this.#languageRanges.length; res.numSignLanguages = this.#signLanguagesList.length; if (this.#languageFileDate) res.languageFileDate = this.#languageFileDate; @@ -66,9 +69,7 @@ export default class IANAlanguages { * @return {boolean} true if the language subtag is a sign language */ function isSignLanguage(items) { - for (let i = 0; i < items.length; i++) - if (items[i].startsWith("Description") && items[i].toLowerCase().includes("sign")) - return true; + for (let i = 0; i < items.length; i++) if (items[i].startsWith("Description") && items[i].toLowerCase().includes("sign")) return true; return false; } @@ -141,9 +142,8 @@ export default class IANAlanguages { } else console.log(chalk.red(`error loading languages ${err}`)); }.bind(this) ); - } - else { - let langs = readFileSync(languagesFile, { encoding: "utf-8" } ).toString(); + } else { + let langs = readFileSync(languagesFile, { encoding: "utf-8" }).toString(); this.#processLanguageData(langs); } } @@ -161,7 +161,7 @@ export default class IANAlanguages { if (purge) this.empty(); - if (async) + if (async) fetch(languagesURL) .then(handleErrors) .then((response) => response.text()) @@ -175,14 +175,13 @@ export default class IANAlanguages { console.log(chalk.red(error.message)); } if (resp) { - if (resp.ok) - this.#processLanguageData(response.text); - else console.log(chalk.red(`error (${error}) retrieving ${languagesURL}`)); + if (resp.ok) this.#processLanguageData(resp.text); + else console.log(chalk.red(`error (${resp.error}) retrieving ${languagesURL}`)); } } } - loadLanguages(options, async=true) { + loadLanguages(options, async = true) { if (!options) options = {}; if (!Object.prototype.hasOwnProperty.call(options, "purge")) options.purge = false; @@ -202,6 +201,13 @@ export default class IANAlanguages { if (datatypeIs(value, "string")) { if (this.#languageRanges.find((range) => range.start <= value && value <= range.end)) return { resp: this.languageKnown }; + let found = this.#redundantLanguagesList.find((e) => e.tag.toLowerCase() == value.toLowerCase()); + if (found) { + let res = { resp: this.languageRedundant }; + if (found?.preferred) res.pref = found.preferred; + return res; + } + if (value.indexOf("-") != -1) { let matches = true; let parts = value.split("-"); @@ -213,15 +219,6 @@ export default class IANAlanguages { if (isIni(this.#languagesList, value)) return { resp: this.languageKnown }; - let lc = value.toLowerCase(); - let found = this.#redundantLanguagesList.find((e) => e.tag.toLowerCase() == lc); - if (found) { - let res = { resp: this.languageRedundant }; - if (found?.preferred) res.pref = found.preferred; - []; - return res; - } - return { resp: this.languageUnknown }; } return { resp: this.languageInvalidType }; diff --git a/accessibility_attributes_checks.js b/accessibility_attributes_checks.js index d820b9b..25e5990 100644 --- a/accessibility_attributes_checks.js +++ b/accessibility_attributes_checks.js @@ -1,7 +1,7 @@ /** * accessibility_attribites_checks.js - * - * Checks the value space of the element against the rules and + * + * Checks the value space of the element against the rules and * values provided in DVB A177. */ import { datatypeIs } from "./phlib/phlib.js"; @@ -13,6 +13,7 @@ import { APPLICATION, WARNING } from "./error_list.js"; import { checkTopElementsAndCardinality } from "./schema_checks.js"; import { CS_URI_DELIMITER } from "./classification_scheme.js"; +import { DumpString } from "./utils.js"; export function CheckAccessibilityAttributes(AccessibilityAttributes, cs, errs, errCode) { const ACCESSIBILITY_CHECK_KEY = "accessibility attributes"; @@ -126,23 +127,27 @@ export function CheckAccessibilityAttributes(AccessibilityAttributes, cs, errs, }; let checkCS = (elem, childName, cs, errNum, storage = null) => { - let children = elem.childNodes(); + let rc = true, + children = elem.childNodes(); if (children) children.forEachSubElement((e) => { if (e.name() == childName) { let href = e.attr(tva.a_href) ? e.attr(tva.a_href).value() : null; - if (href && !cs.isIn(href)) + if (href && !cs.isIn(href)) { errs.addError({ code: `${errCode}-${errNum}`, fragment: e, message: `"${href}" is not valid for ${e.name().elementize()} in ${elem.name().elementize()}`, key: ACCESSIBILITY_CHECK_KEY, }); + rc = false; + } if (storage && datatypeIs(storage, "array") && href) { storage.push(href); } } }); + return rc; }; let checkSignLanguage = (elem, childName, errNum) => { @@ -150,13 +155,31 @@ export function CheckAccessibilityAttributes(AccessibilityAttributes, cs, errs, if (children) children.forEachSubElement((e) => { if (e.name() == childName) { - if (cs.KnownLanguages.checkSignLanguage(e.text()) != cs.KnownLanguages.languageKnown) + let languageCode = e.text(); + let lState = cs.KnownLanguages.isKnown(languageCode); + if (lState.resp == cs.KnownLanguages.languageRedundant) { errs.addError({ - code: `${errCode}-${errNum}`, + code: `${errCode}-${errNum}a`, + fragment: e, + message: `sign language ${languageCode.quote()} is redundant${lState.pref ? `, use ${lState.pref.quote()} instead` : ""}`, + key: "deprecated language", + type: WARNING, + }); + if (lState.pref) languageCode = lState.pref; + } + + if (cs.KnownLanguages.checkSignLanguage(languageCode) != cs.KnownLanguages.languageKnown) { + errs.addError({ + code: `${errCode}-${errNum}b`, fragment: e, - message: `"${e.text()}" is not a valid sign language for ${e.name().elementize()} in ${elem.name().elementize()}`, + message: `${languageCode.quote()} is not a valid sign language for ${e.name().elementize()} in ${elem.name().elementize()}`, key: ACCESSIBILITY_CHECK_KEY, }); + errs.errorDescription({ + code: `${errCode}-${errNum}b`, + description: `language used for ${e.name().elementize()}} must be a sign language in the IANA language-subtag-regostry`, + }); + } } }); }; @@ -323,7 +346,8 @@ export function CheckAccessibilityAttributes(AccessibilityAttributes, cs, errs, `${errCode}-71` ); checkAppInformation(elem, 72); - checkCS(elem, tva.e_Coding, cs.VideoCodecCS, 73); + if (!checkCS(elem, tva.e_Coding, cs.VideoCodecCS, 73)) + errs.errorDescription({ code: `${errCode}-73`, description: `value for ${tva.e_Coding.elementize()} is not taken from the VideoCodecCS` }); checkSignLanguage(elem, tva.e_SignLanguage, 74); break; case tva.e_DialogueEnhancementAttributes: diff --git a/cg_check.js b/cg_check.js index 405a853..db07503 100644 --- a/cg_check.js +++ b/cg_check.js @@ -1274,12 +1274,14 @@ export default class ContentGuideCheck { const titleLang = GetNodeLanguage(Title, false, errs, `${errCode}-2`, this.#knownLanguages); const titleStr = unEntity(Title.text()); - if (titleStr.length > dvbi.MAX_TITLE_LENGTH) + if (titleStr.length > dvbi.MAX_TITLE_LENGTH) { errs.addError({ code: `${errCode}-11`, message: `${tva.e_Title.elementize()} length exceeds ${dvbi.MAX_TITLE_LENGTH} characters`, fragment: Title, }); + errs.errorDescription({code:`${errCode}-11`, description: "refer clause 6.10.5 in A177"}) + } switch (titleType) { case mpeg7.TITLE_TYPE_MAIN: if (mainTitles.find((e) => e.lang == titleLang)) @@ -1310,6 +1312,8 @@ export default class ContentGuideCheck { message: `${tva.a_type.attribute()} must be ${mpeg7.TITLE_TYPE_MAIN.quote()} or ${mpeg7.TITLE_TYPE_SECONDARY.quote()} for ${tva.e_Title.elementize()}`, fragment: Title, }); + errs.errorDescription({code:`${errCode}-15`, description: "refer to the relevant subsection of clause 6.10.5 in A177"}); + break; } } secondaryTitles.forEach((item) => { @@ -1522,6 +1526,12 @@ export default class ContentGuideCheck { } } + /*private*/ #NotCRIDFormat(errs, error) + { + errs.addError(error); + errs.errorDescription({code: error?.code, description: "format if a CRID is defined in clause 8 of ETSI TS 102 822"}) + } + /** * validate the element against the profile for the given request/response type * @@ -1573,7 +1583,7 @@ export default class ContentGuideCheck { if (ProgramInformation.attr(tva.a_programId)) { programCRID = ProgramInformation.attr(tva.a_programId).value(); if (!isCRIDURI(programCRID)) - errs.addError({ + this.#NotCRIDFormat(errs, { code: "PI011", message: `${tva.a_programId.attribute(ProgramInformation.name())} is not a valid CRID (${programCRID})`, line: ProgramInformation.line(), @@ -1617,7 +1627,7 @@ export default class ContentGuideCheck { fragment: child, }); else if (!isCRIDURI(foundCRID)) - errs.addError({ + this.#NotCRIDFormat(errs, { code: "PI033", message: `${tva.a_crid.attribute(`${ProgramInformation.name()}.${tva.e_EpisodeOf}`)}=${foundCRID.quote()} is not a valid CRID`, fragment: child, @@ -1652,7 +1662,7 @@ export default class ContentGuideCheck { fragment: child, }); else if (!isCRIDURI(foundCRID)) - errs.addError({ + this.#NotCRIDFormat(errs,{ code: "PI045", message: `${tva.a_crid.attribute(`${ProgramInformation.name()}.${tva.e_MemberOf}`)}=${foundCRID.quote()} is not a valid CRID`, fragment: child, @@ -1951,7 +1961,7 @@ export default class ContentGuideCheck { if (GroupInformation.attr(tva.a_groupId)) { const groupId = GroupInformation.attr(tva.a_groupId).value(); if (!isCRIDURI(groupId)) - errs.addError({ + this.#NotCRIDFormat(errs, { code: "GIM003", message: `${tva.a_groupId.attribute(GroupInformation.name())} value ${groupId.quote()} is not a valid CRID`, line: GroupInformation.line(), @@ -2974,12 +2984,13 @@ export default class ContentGuideCheck { let ProgramCRID = Program.attr(tva.a_crid); if (ProgramCRID) { - if (!isCRIDURI(ProgramCRID.value())) - errs.addError({ + if (!isCRIDURI(ProgramCRID.value())) { + this.#NotCRIDFormat(errs, { code: "SE011", message: `${tva.a_crid.attribute(tva.e_Program)} is not a valid CRID (${ProgramCRID.value()})`, fragment: Program, }); + } if (!isIni(programCRIDs, ProgramCRID.value())) errs.addError({ code: "SE012", diff --git a/sl_check.js b/sl_check.js index 987c296..5a598d6 100644 --- a/sl_check.js +++ b/sl_check.js @@ -1,13 +1,11 @@ /** * sl_check.js - * + * * Check a service list */ import { readFileSync } from "fs"; import process from "process"; - - import chalk from "chalk"; import { parseXmlString } from "libxmljs2"; @@ -384,7 +382,7 @@ export default class ServiceListCheck { #allowedVideoConformancePoints; #RecordingInfoCSvalues; - constructor(useURLs, opts, async=true) { + constructor(useURLs, opts, async = true) { this.#numRequests = 0; this.#knownLanguages = opts?.languages ? opts.languages : LoadLanguages(useURLs, async); @@ -419,7 +417,6 @@ export default class ServiceListCheck { }); } - stats() { let res = {}; res.numRequests = this.#numRequests; @@ -1347,9 +1344,7 @@ export default class ServiceListCheck { if (nestedCAsystemid) { CASystemID_value = nestedCAsystemid.text(); } - } - else - CASystemID_value = CASystemID.text(); + } else CASystemID_value = CASystemID.text(); if (CASystemID_value) { let CASid_value = parseInt(CASystemID_value, 10); if (isNaN(CASid_value)) { @@ -1385,8 +1380,7 @@ export default class ServiceListCheck { if (nestedDRMsystemid) { DRMSystemID_value = nestedDRMsystemid.text().toLowerCase(); } - } - else DRMSystemID_value = DRMSystemID.text().toLowerCase(); + } else DRMSystemID_value = DRMSystemID.text().toLowerCase(); if (DRMSystemID_value && ContentProtectionIDs.find((el) => el.id == DRMSystemID_value || el.id.substring(el.id.lastIndexOf(":") + 1) == DRMSystemID_value) == undefined) { errs.addError({ code: "SI033", @@ -1781,7 +1775,7 @@ export default class ServiceListCheck { fragment: element, }); }; - let DisallowedElement = (element, childElementName, modulation, suffix="-0") => { + let DisallowedElement = (element, childElementName, modulation, suffix = "-0") => { if (hasChild(element, childElementName)) errs.addError({ code: `SI204${suffix}`, @@ -1805,7 +1799,7 @@ export default class ServiceListCheck { checkElement(ModulationType, dvbi.e_ModulationType, sats.S2_Modulation, sats.MODULATION_S2, "SI202b"); checkElement(FEC, dvbi.e_FEC, sats.S2_FEC, sats.MODULATION_S2, "SI203b"); DisallowedElement(DVBSDeliveryParameters, dvbi.e_ModcodMode, sats.MODULATION_S2, "k"); - DisallowedElement(DVBSDeliveryParameters, dvbi.e_InputStreamIdentifier, sats.MODULATION_S2, 'l'); + DisallowedElement(DVBSDeliveryParameters, dvbi.e_InputStreamIdentifier, sats.MODULATION_S2, "l"); DisallowedElement(DVBSDeliveryParameters, dvbi.e_ChannelBonding, sats.MODULATION_S2, "m"); break; case sats.MODULATION_S2X: @@ -1959,13 +1953,15 @@ export default class ServiceListCheck { let uID = service.get(xPath(props.prefix, dvbi.e_UniqueIdentifier), props.schema); if (uID) { thisServiceId = uID.text(); - if (!validServiceIdentifier(thisServiceId)) + if (!validServiceIdentifier(thisServiceId)) { errs.addError({ code: "SL110", message: `${thisServiceId.quote()} is not a valid service identifier`, fragment: uID, key: "invalid tag", }); + errs.errorDescription({ code: "SL110", description: "service identifier should be a tag: URI according to IETF RFC 4151" }); + } if (!uniqueServiceIdentifier(thisServiceId, knownServices)) errs.addError({ code: "SL111", @@ -1995,7 +1991,7 @@ export default class ServiceListCheck { type: WARNING, code: "SL131", key: "duplicate value", - message: `duplicate value (${TargetRegion.value}) specified for ${dvbi.e_TargetRegion.elementize()}`, + message: `duplicate value (${TargetRegion.text()}) specified for ${dvbi.e_TargetRegion.elementize()}`, fragment: TargetRegion, }); } @@ -2309,7 +2305,7 @@ export default class ServiceListCheck { * @param {Class} errs Errors found in validaton * @param {String} log_prefix the first part of the logging location (or null if no logging) */ - /*public*/ doValidateServiceList(SLtext, errs, log_prefix=null) { + /*public*/ doValidateServiceList(SLtext, errs, log_prefix = null) { this.#numRequests++; if (!SLtext) { errs.addError({ @@ -2682,13 +2678,20 @@ export default class ServiceListCheck { key: "undefined region", }); else { - if (foundRegion.selectable == false) + if (foundRegion.selectable == false) { errs.addError({ code: "SL242", message: `${dvbi.e_TargetRegion.elementize()} ${TargetRegion.text().quote()} in ${dvbi.e_LCNTable.elementize()} is not selectable`, fragment: TargetRegion, key: "unselectable region", }); + errs.errorDescription({ + code: "SL242", + description: `the region ID specified in the ${dvbi.e_TargetRegion.elementize()} is defined with ${ + dvbi.a_selectable + }=false in the ${dvbi.e_RegionList.elementize()} `, + }); + } foundRegion.used = true; } @@ -2821,7 +2824,7 @@ export default class ServiceListCheck { errs.errorDescription({ code: "SL282", clause: "see A177 table 14", - description: `lanugages used in ${tva.e_AudioAttributes.elementize()}${tva.e_AudioLanguage.elementize()} should be announced in ${dvbi.e_LanguageList.elementize()}`, + description: `only lanugages used in ${tva.e_AudioAttributes.elementize()}${tva.e_AudioLanguage.elementize()} should be announced in ${dvbi.e_LanguageList.elementize()}`, }); } }); diff --git a/utils.js b/utils.js index 31e203f..142408a 100644 --- a/utils.js +++ b/utils.js @@ -1,6 +1,6 @@ /** * utils.js - * + * * some usefule utility functions that may be used by more than one class */ import { statSync, readFileSync } from "fs"; @@ -34,7 +34,7 @@ export function getElementByTagName(element, childElementName, index = null) { if (!index) return getFirstElementByTagName(element, childElementName); let cnt = 0; - const ch1 = element.childNodes(); + const ch1 = element.childNodes(); for (let i = 0; i < ch1.length; i++) { if (ch1[i].type() == "element" && ch1[i].name() == childElementName) cnt++; if (cnt >= index) return ch1[i]; @@ -200,3 +200,9 @@ export function DuplicatedValue(found, val) { if (!f) found.push(val); return f; } + +export function DumpString(str) { + let t = []; + for (let i = 0; i < str.length; i++) t.push(str.charCodeAt(i).toString(16)); + return `"${str}" --> ${t.join(" ")}`; +}