2023-10-30 17:30:46 +01:00
|
|
|
/*******************************************************************************
|
|
|
|
|
|
|
|
uBlock Origin - a browser extension to block requests.
|
|
|
|
Copyright (C) 2014-present Raymond Hill
|
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
|
|
|
|
|
|
|
Home: https://github.com/gorhill/uBlock
|
|
|
|
*/
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
// This module can be dynamically loaded or spun off as a worker.
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
const patches = new Map();
|
|
|
|
const encoder = new TextEncoder();
|
2023-11-05 14:03:43 +01:00
|
|
|
const reFileName = /([^\/]+?)(?:#.+)?$/;
|
2023-10-30 17:30:46 +01:00
|
|
|
const EMPTYLINE = '';
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
const suffleArray = arr => {
|
|
|
|
const out = arr.slice();
|
|
|
|
for ( let i = 0, n = out.length; i < n; i++ ) {
|
|
|
|
const j = Math.floor(Math.random() * n);
|
|
|
|
if ( j === i ) { continue; }
|
|
|
|
[ out[j], out[i] ] = [ out[i], out[j] ];
|
|
|
|
}
|
|
|
|
return out;
|
|
|
|
};
|
|
|
|
|
|
|
|
const basename = url => {
|
|
|
|
const match = reFileName.exec(url);
|
2023-11-05 14:03:43 +01:00
|
|
|
return match && match[1] || '';
|
2023-10-30 17:30:46 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
const resolveURL = (path, url) => {
|
|
|
|
try {
|
2023-11-03 23:39:14 +01:00
|
|
|
return new URL(path, url);
|
2023-10-30 17:30:46 +01:00
|
|
|
}
|
|
|
|
catch(_) {
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-10-31 15:52:03 +01:00
|
|
|
const expectedTimeFromPatch = assetDetails => {
|
|
|
|
const match = /(\d+)\.(\d+)\.(\d+)\.(\d+)/.exec(assetDetails.patchPath);
|
|
|
|
if ( match === null ) { return 0; }
|
|
|
|
const date = new Date();
|
|
|
|
date.setUTCFullYear(
|
|
|
|
parseInt(match[1], 10),
|
|
|
|
parseInt(match[2], 10) - 1,
|
|
|
|
parseInt(match[3], 10)
|
|
|
|
);
|
|
|
|
date.setUTCHours(0, parseInt(match[4], 10), 0, 0);
|
|
|
|
return date.getTime() + assetDetails.diffExpires;
|
|
|
|
};
|
|
|
|
|
2023-10-30 17:30:46 +01:00
|
|
|
function parsePatch(patch) {
|
|
|
|
const patchDetails = new Map();
|
|
|
|
const diffLines = patch.split('\n');
|
|
|
|
let i = 0, n = diffLines.length;
|
|
|
|
while ( i < n ) {
|
|
|
|
const line = diffLines[i++];
|
|
|
|
if ( line.startsWith('diff ') === false ) { continue; }
|
|
|
|
const fields = line.split(/\s+/);
|
|
|
|
const diffBlock = {};
|
|
|
|
for ( let j = 0; j < fields.length; j++ ) {
|
|
|
|
const field = fields[j];
|
|
|
|
const pos = field.indexOf(':');
|
|
|
|
if ( pos === -1 ) { continue; }
|
|
|
|
const name = field.slice(0, pos);
|
|
|
|
if ( name === '' ) { continue; }
|
|
|
|
const value = field.slice(pos+1);
|
|
|
|
switch ( name ) {
|
|
|
|
case 'name':
|
|
|
|
case 'checksum':
|
|
|
|
diffBlock[name] = value;
|
|
|
|
break;
|
|
|
|
case 'lines':
|
|
|
|
diffBlock.lines = parseInt(value, 10);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ( diffBlock.name === undefined ) { return; }
|
|
|
|
if ( isNaN(diffBlock.lines) || diffBlock.lines <= 0 ) { return; }
|
|
|
|
if ( diffBlock.checksum === undefined ) { return; }
|
|
|
|
patchDetails.set(diffBlock.name, diffBlock);
|
|
|
|
diffBlock.diff = diffLines.slice(i, i + diffBlock.lines).join('\n');
|
|
|
|
i += diffBlock.lines;
|
|
|
|
}
|
|
|
|
if ( patchDetails.size === 0 ) { return; }
|
|
|
|
return patchDetails;
|
|
|
|
}
|
|
|
|
|
|
|
|
function applyPatch(text, diff) {
|
|
|
|
// Inspired from (Perl) "sub _patch" at:
|
|
|
|
// https://twiki.org/p/pub/Codev/RcsLite/RcsLite.pm
|
|
|
|
// Apparently authored by John Talintyre in Jan. 2002
|
|
|
|
// https://twiki.org/cgi-bin/view/Codev/RcsLite
|
|
|
|
const lines = text.split('\n');
|
|
|
|
const diffLines = diff.split('\n');
|
|
|
|
let iAdjust = 0;
|
|
|
|
let iDiff = 0, nDiff = diffLines.length;
|
|
|
|
while ( iDiff < nDiff ) {
|
2023-11-13 15:42:31 +01:00
|
|
|
const diffLine = diffLines[iDiff++];
|
|
|
|
if ( diffLine === '' ) { break; }
|
|
|
|
const diffParsed = /^([ad])(\d+) (\d+)$/.exec(diffLine);
|
2023-10-30 17:30:46 +01:00
|
|
|
if ( diffParsed === null ) { return; }
|
|
|
|
const op = diffParsed[1];
|
|
|
|
const iOp = parseInt(diffParsed[2], 10);
|
|
|
|
const nOp = parseInt(diffParsed[3], 10);
|
|
|
|
const iOpAdj = iOp + iAdjust;
|
|
|
|
if ( iOpAdj > lines.length ) { return; }
|
|
|
|
// Delete lines
|
|
|
|
if ( op === 'd' ) {
|
|
|
|
lines.splice(iOpAdj-1, nOp);
|
|
|
|
iAdjust -= nOp;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Add lines: Don't use splice() to avoid stack limit issues
|
|
|
|
for ( let i = 0; i < nOp; i++ ) {
|
|
|
|
lines.push(EMPTYLINE);
|
|
|
|
}
|
|
|
|
lines.copyWithin(iOpAdj+nOp, iOpAdj);
|
|
|
|
for ( let i = 0; i < nOp; i++ ) {
|
|
|
|
lines[iOpAdj+i] = diffLines[iDiff+i];
|
|
|
|
}
|
|
|
|
iAdjust += nOp;
|
|
|
|
iDiff += nOp;
|
|
|
|
}
|
|
|
|
return lines.join('\n');
|
|
|
|
}
|
|
|
|
|
2023-10-31 15:52:03 +01:00
|
|
|
function hasPatchDetails(assetDetails) {
|
|
|
|
const { patchPath } = assetDetails;
|
|
|
|
const patchFile = basename(patchPath);
|
|
|
|
return patchFile !== '' && patches.has(patchFile);
|
|
|
|
}
|
|
|
|
|
2023-10-30 17:30:46 +01:00
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
// Async
|
|
|
|
|
|
|
|
async function applyPatchAndValidate(assetDetails, diffDetails) {
|
|
|
|
const { text } = assetDetails;
|
|
|
|
const { diff, checksum } = diffDetails;
|
|
|
|
const textAfter = applyPatch(text, diff);
|
|
|
|
if ( typeof textAfter !== 'string' ) {
|
|
|
|
assetDetails.error = 'baddiff';
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const crypto = globalThis.crypto;
|
|
|
|
if ( typeof crypto !== 'object' ) {
|
|
|
|
assetDetails.error = 'nocrypto';
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const arrayin = encoder.encode(textAfter);
|
|
|
|
const arraybuffer = await crypto.subtle.digest('SHA-1', arrayin);
|
|
|
|
const arrayout = new Uint8Array(arraybuffer);
|
|
|
|
const sha1Full = Array.from(arrayout).map(i =>
|
|
|
|
i.toString(16).padStart(2, '0')
|
|
|
|
).join('');
|
|
|
|
if ( sha1Full.startsWith(checksum) === false ) {
|
2023-11-11 19:25:58 +01:00
|
|
|
assetDetails.error = `badchecksum: expected ${checksum}, computed ${sha1Full.slice(0, checksum.length)}`;
|
2023-10-30 17:30:46 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
assetDetails.text = textAfter;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function fetchPatchDetailsFromCDNs(assetDetails) {
|
|
|
|
const { patchPath, cdnURLs } = assetDetails;
|
|
|
|
if ( Array.isArray(cdnURLs) === false ) { return null; }
|
|
|
|
if ( cdnURLs.length === 0 ) { return null; }
|
|
|
|
for ( const cdnURL of suffleArray(cdnURLs) ) {
|
2023-10-30 18:47:24 +01:00
|
|
|
const patchURL = resolveURL(patchPath, cdnURL);
|
|
|
|
if ( patchURL === undefined ) { continue; }
|
|
|
|
const response = await fetch(patchURL).catch(reason => {
|
2023-10-30 17:30:46 +01:00
|
|
|
console.error(reason);
|
|
|
|
});
|
|
|
|
if ( response === undefined ) { continue; }
|
2023-11-14 23:39:48 +01:00
|
|
|
if ( response.status === 404 ) { break; }
|
2023-10-30 17:30:46 +01:00
|
|
|
if ( response.ok !== true ) { continue; }
|
|
|
|
const patchText = await response.text();
|
|
|
|
const patchDetails = parsePatch(patchText);
|
2023-11-03 23:39:14 +01:00
|
|
|
if ( patchURL.hash.length > 1 ) {
|
|
|
|
assetDetails.diffName = patchURL.hash.slice(1);
|
2023-11-08 13:16:23 +01:00
|
|
|
patchURL.hash = '';
|
2023-11-03 23:39:14 +01:00
|
|
|
}
|
2023-10-30 18:47:24 +01:00
|
|
|
return {
|
2023-11-03 23:39:14 +01:00
|
|
|
patchURL: patchURL.href,
|
2023-10-30 18:47:24 +01:00
|
|
|
patchSize: `${(patchText.length / 1000).toFixed(1)} KB`,
|
|
|
|
patchDetails,
|
|
|
|
};
|
2023-10-30 17:30:46 +01:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function fetchPatchDetails(assetDetails) {
|
|
|
|
const { patchPath } = assetDetails;
|
|
|
|
const patchFile = basename(patchPath);
|
|
|
|
if ( patchFile === '' ) { return null; }
|
|
|
|
if ( patches.has(patchFile) ) {
|
|
|
|
return patches.get(patchFile);
|
|
|
|
}
|
|
|
|
const patchDetailsPromise = fetchPatchDetailsFromCDNs(assetDetails);
|
|
|
|
patches.set(patchFile, patchDetailsPromise);
|
|
|
|
return patchDetailsPromise;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function fetchAndApplyAllPatches(assetDetails) {
|
2023-10-31 15:52:03 +01:00
|
|
|
if ( assetDetails.fetch === false ) {
|
|
|
|
if ( hasPatchDetails(assetDetails) === false ) {
|
|
|
|
assetDetails.status = 'nodiff';
|
|
|
|
return assetDetails;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// uBO-specific, to avoid pointless fetches which are likely to fail
|
|
|
|
// because the patch has not yet been created
|
|
|
|
const patchTime = expectedTimeFromPatch(assetDetails);
|
|
|
|
if ( patchTime > Date.now() ) {
|
|
|
|
assetDetails.status = 'nopatch-yet';
|
|
|
|
return assetDetails;
|
|
|
|
}
|
2023-10-30 18:47:24 +01:00
|
|
|
const patchData = await fetchPatchDetails(assetDetails);
|
|
|
|
if ( patchData === null ) {
|
2023-10-31 15:52:03 +01:00
|
|
|
assetDetails.status = (Date.now() - patchTime) < (4 * assetDetails.diffExpires)
|
|
|
|
? 'nopatch-yet'
|
|
|
|
: 'nopatch';
|
2023-10-30 17:30:46 +01:00
|
|
|
return assetDetails;
|
|
|
|
}
|
2023-10-30 18:47:24 +01:00
|
|
|
const { patchDetails } = patchData;
|
2023-10-31 15:52:03 +01:00
|
|
|
if ( patchDetails instanceof Map === false ) {
|
|
|
|
assetDetails.status = 'nodiff';
|
|
|
|
return assetDetails;
|
|
|
|
}
|
2023-10-30 17:30:46 +01:00
|
|
|
const diffDetails = patchDetails.get(assetDetails.diffName);
|
|
|
|
if ( diffDetails === undefined ) {
|
2023-10-31 15:52:03 +01:00
|
|
|
assetDetails.status = 'nodiff';
|
2023-10-30 17:30:46 +01:00
|
|
|
return assetDetails;
|
|
|
|
}
|
|
|
|
if ( assetDetails.text === undefined ) {
|
|
|
|
assetDetails.status = 'needtext';
|
|
|
|
return assetDetails;
|
|
|
|
}
|
|
|
|
const outcome = await applyPatchAndValidate(assetDetails, diffDetails);
|
|
|
|
if ( outcome !== true ) { return assetDetails; }
|
|
|
|
assetDetails.status = 'updated';
|
2023-10-30 18:47:24 +01:00
|
|
|
assetDetails.patchURL = patchData.patchURL;
|
|
|
|
assetDetails.patchSize = patchData.patchSize;
|
2023-10-30 17:30:46 +01:00
|
|
|
return assetDetails;
|
|
|
|
}
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
const bc = new globalThis.BroadcastChannel('diffUpdater');
|
|
|
|
|
|
|
|
bc.onmessage = ev => {
|
2023-12-01 20:35:28 +01:00
|
|
|
const message = ev.data || {};
|
2023-10-30 17:30:46 +01:00
|
|
|
switch ( message.what ) {
|
|
|
|
case 'update':
|
|
|
|
fetchAndApplyAllPatches(message).then(response => {
|
|
|
|
bc.postMessage(response);
|
2023-10-30 18:47:24 +01:00
|
|
|
}).catch(error => {
|
|
|
|
bc.postMessage({ what: 'broken', error });
|
2023-10-30 17:30:46 +01:00
|
|
|
});
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
bc.postMessage({ what: 'ready' });
|
|
|
|
|
|
|
|
/******************************************************************************/
|