diff --git a/src/ssn/getPatchmanifest.ts b/src/ssn/getPatchmanifest.ts index 1786da8..50ca5f2 100644 --- a/src/ssn/getPatchmanifest.ts +++ b/src/ssn/getPatchmanifest.ts @@ -2,6 +2,7 @@ import { TextDecoder } from 'util'; import * as xmlJs from 'xml-js'; import getUrlContents from '../cdn/getUrlContents'; import { Product } from '../interfaces/ISettings'; +import verifyPatchmanifest from '../ssn/verifyPatchmanifest'; import verifyProductName from '../ssn/verifyProductName'; import extractFile from './extractFile'; import readSsnFile from './readSsnFile'; @@ -33,7 +34,8 @@ export default async function getPatchmanifest(product: Product): Promise { const patchmanifestFile = await extractFile(firstFile, [new DataView(ssnFile)]); const patchmanifestXml = Decoder.decode(patchmanifestFile); - const patchManifestJson = xmlJs.xml2js(patchmanifestXml); + const patchManifestJson = xmlJs.xml2js(patchmanifestXml) as xmlJs.Element; + verifyPatchmanifest(patchManifestJson, product); return patchManifestJson; } diff --git a/src/ssn/verifyPatchmanifest.ts b/src/ssn/verifyPatchmanifest.ts new file mode 100644 index 0000000..73478c6 --- /dev/null +++ b/src/ssn/verifyPatchmanifest.ts @@ -0,0 +1,160 @@ +import * as xmlJs from 'xml-js'; +import { Product } from '../interfaces/ISettings'; + +/** Receives a JSON-converted version of the manifest.xml file */ +export default function verifyPatchmanifest(obj: xmlJs.Element, product: Product): any { + // + if (obj.declaration === undefined || obj.declaration.attributes === undefined || Object.keys(obj.declaration.attributes).length !== 2 || obj.declaration.attributes.version !== '1.0' || obj.declaration.attributes.encoding !== 'utf-8') { + throw new Error('Expected declration with version 1.0 and utf-8 encoding.'); + } + + // + if (obj.elements === undefined || obj.elements.length !== 1) { + throw new Error('Expected one root element.'); + } + const root = obj.elements[0]; + if (root.type !== 'element' || root.name !== 'PatchManifest') { + throw new Error(`Expected root element to be called PatchManifest but it was "${root.name}".`); + } + if (root.attributes === undefined || Object.keys(root.attributes).length !== 2 || root.attributes['xmlns:xsi'] !== 'http://www.w3.org/2001/XMLSchema-instance' || root.attributes['xmlns:xsd'] !== 'http://www.w3.org/2001/XMLSchema') { + throw new Error(`Expected root element to have attributes xmlns:xsi and xmlns:xsd.`); + } + if (root.elements === undefined) { + throw new Error(`Expected child elements under PatchManifest but there were none.`); + } + if (root.elements.length !== 9) { + throw new Error(`Expected 9 child elements under PatchManifest but there were ${root.elements.length}.`); + } + + // + const Dependencies = root.elements[0]; + if (Dependencies.type !== 'element' || Dependencies.name !== 'Dependencies' || Dependencies.attributes !== undefined || Dependencies.elements !== undefined) { + throw new Error('Expected Dependencies element with no child elements and no attributes.'); + } + + //assets_swtor_de_de + const Name = root.elements[1]; + if (Name.type !== 'element' || Name.name !== 'Name' || Name.attributes !== undefined || Name.elements === undefined || Name.elements.length !== 1 || Name.elements[0].type !== 'text' || Name.elements[0].text !== product) { + throw new Error('Expected Name element with one child element equal to product and no attributes.'); + } + + //289 + const RequiredRelease = root.elements[2]; + if (RequiredRelease.type !== 'element' || RequiredRelease.name !== 'RequiredRelease' || RequiredRelease.attributes !== undefined || RequiredRelease.elements === undefined) { + throw new Error('Expected RequiredRelease element.'); + } + if (RequiredRelease.elements.length !== 1 || RequiredRelease.elements[0].type !== 'text' || typeof RequiredRelease.elements[0].text !== 'string' || String(RequiredRelease.elements[0].text).match(/^(0|[1-9][0-9]*)$/) === null) { + throw new Error('Expected integer in RequiredRelease element.'); + } + + //-1 + const UpcomingRelease = root.elements[3]; + if (UpcomingRelease.type !== 'element' || UpcomingRelease.name !== 'UpcomingRelease' || UpcomingRelease.attributes !== undefined || UpcomingRelease.elements === undefined) { + throw new Error('Expected UpcomingRelease element.'); + } + if (UpcomingRelease.elements.length !== 1 || UpcomingRelease.elements[0].type !== 'text' || UpcomingRelease.elements[0].text !== '-1') { + throw new Error('Expected -1 in UpcomingRelease element.'); + } + + //{ModulePath} + const TargetDirectory = root.elements[4]; + if (TargetDirectory.type !== 'element' || TargetDirectory.name !== 'TargetDirectory' || TargetDirectory.attributes !== undefined || TargetDirectory.elements === undefined) { + throw new Error('Expected TargetDirectory element.'); + } + if (TargetDirectory.elements.length !== 1 || TargetDirectory.elements[0].type !== 'text' || TargetDirectory.elements[0].text !== '{ModulePath}') { + throw new Error('Expected {ModulePath} in TargetDirectory element.'); + } + + //false + const RequiresElevation = root.elements[5]; + if (RequiresElevation.type !== 'element' || RequiresElevation.name !== 'RequiresElevation' || RequiresElevation.attributes !== undefined || RequiresElevation.elements === undefined) { + throw new Error('Expected RequiresElevation element.'); + } + if (RequiresElevation.elements.length !== 1 || RequiresElevation.elements[0].type !== 'text' || RequiresElevation.elements[0].text !== 'false') { + throw new Error('Expected false in RequiresElevation element.'); + } + + //false + const Maintenance = root.elements[6]; + if (Maintenance.type !== 'element' || Maintenance.name !== 'Maintenance' || Maintenance.attributes !== undefined || Maintenance.elements === undefined) { + throw new Error('Expected Maintenance element.'); + } + if (Maintenance.elements.length !== 1 || Maintenance.elements[0].type !== 'text' || Maintenance.elements[0].text !== 'false') { + throw new Error('Expected false in Maintenance element.'); + } + + // + const Releases = root.elements[7]; + if (Releases.type !== 'element' || Releases.name !== 'Releases' || Releases.attributes !== undefined || Releases.elements === undefined) { + throw new Error('Expected Releases element.'); + } + for (let i = 0, il = Releases.elements.length; i < il; i += 1) { + const Release = Releases.elements[i]; + // + if (Release.type !== 'element' || Release.name !== 'Release' || Release.attributes !== undefined || Release.elements === undefined || Release.elements.length !== 3) { + throw new Error('Expected Release element.'); + } + //0 + const Id = Release.elements[0]; + if (Id.type !== 'element' || Id.name !== 'Id' || Id.attributes !== undefined || Id.elements === undefined || Id.elements.length !== 1 || Id.elements[0].type !== 'text' || typeof Id.elements[0].text !== 'string' || String(Id.elements[0].text).match(/^(0|[1-9][0-9]*)$/) === null) { + throw new Error('Expected Id element.'); + } + //53678f8057e52896a8145dca5c188ab3f24fa55f + const Sha1 = Release.elements[1]; + if (Sha1.type !== 'element' || Sha1.name !== 'SHA1' || Sha1.attributes !== undefined || Sha1.elements === undefined || Sha1.elements.length !== 1 || Sha1.elements[0].type !== 'text' || typeof Sha1.elements[0].text !== 'string' || String(Sha1.elements[0].text).match(/^[0-9a-z]{40}$/) === null) { + throw new Error('Expected SHA1 element.'); + } + //assets_swtor_de_de_0 + const ReleaseName = Release.elements[2]; + if (ReleaseName.type !== 'element' || ReleaseName.name !== 'Name' || ReleaseName.attributes !== undefined || ReleaseName.elements === undefined || ReleaseName.elements.length !== 1 || ReleaseName.elements[0].type !== 'text' || ReleaseName.elements[0].text !== `${product}_${Id.elements[0].text}`) { + throw new Error('Expected Release->Name element.'); + } + } + + // + const ReleaseUpdatePaths = root.elements[8]; + if (ReleaseUpdatePaths.type !== 'element' || ReleaseUpdatePaths.name !== 'ReleaseUpdatePaths' || ReleaseUpdatePaths.attributes !== undefined || ReleaseUpdatePaths.elements === undefined) { + throw new Error('Expected ReleaseUpdatePaths element.'); + } + for (let i = 0, il = ReleaseUpdatePaths.elements.length; i < il; i += 1) { + const ReleaseUpdatePath = ReleaseUpdatePaths.elements[i]; + // + if (ReleaseUpdatePath.type !== 'element' || ReleaseUpdatePath.name !== 'ReleaseUpdatePath' || ReleaseUpdatePath.attributes !== undefined || ReleaseUpdatePath.elements === undefined || ReleaseUpdatePath.elements.length !== 3) { + throw new Error('Expected ReleaseUpdatePath element.'); + } + //289 + const From = ReleaseUpdatePath.elements[0]; + if (From.type !== 'element' || From.name !== 'From' || From.attributes !== undefined || From.elements === undefined || From.elements.length !== 1 || From.elements[0].type !== 'text' || typeof From.elements[0].text !== 'string' || String(From.elements[0].text).match(/^(-1|0|[1-9][0-9]*)$/) === null) { + throw new Error('Expected From element.'); + } + //285 + const To = ReleaseUpdatePath.elements[1]; + if (To.type !== 'element' || To.name !== 'To' || To.attributes !== undefined || To.elements === undefined || To.elements.length !== 1 || To.elements[0].type !== 'text' || typeof To.elements[0].text !== 'string' || String(To.elements[0].text).match(/^(0|[1-9][0-9]*)$/) === null) { + throw new Error('Expected To element.'); + } + //TODO: check if From and To are valid relations + // + const ExtraData = ReleaseUpdatePath.elements[2]; + if (ExtraData.type !== 'element' || ExtraData.name !== 'ExtraData' || ExtraData.attributes !== undefined || ExtraData.elements === undefined) { + throw new Error('Expected ExtraData element.'); + } + for (let j = 0, jl = ExtraData.elements.length; j < jl; j += 1) { + // + const ExtraDataItem = ExtraData.elements[j]; + if (ExtraDataItem.type !== 'element' || ExtraDataItem.name !== 'ExtraDataItem' || ExtraDataItem.attributes !== undefined || ExtraDataItem.elements === undefined || ExtraDataItem.elements.length !== 2) { + throw new Error('Expected ExtraDataItem element.'); + } + //MetafileUrl + const Key = ExtraDataItem.elements[0]; + if (Key.type !== 'element' || Key.name !== 'Key' || Key.attributes !== undefined || Key.elements === undefined || Key.elements.length !== 1 || Key.elements[0].type !== 'text') { + throw new Error('Expected Key element.'); + } + //http://cdn-patch.swtor.com/patch/assets_swtor_de_de/assets_swtor_de_de_289to285.solidpkg + const Value = ExtraDataItem.elements[1]; + if (Value.type !== 'element' || Value.name !== 'Value' || Value.attributes !== undefined || Value.elements === undefined || Value.elements.length !== 1 || Value.elements[0].type !== 'text') { + throw new Error('Expected Value element.'); + } + //TODO: parse Key and Value + } + } +}