1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-11-16 23:42:39 +01:00
uBlock/src/js/diff-updater.js

244 lines
8.1 KiB
JavaScript
Raw Normal View History

/*******************************************************************************
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();
const reFileName = /[^\/]+$/;
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);
return match && match[0] || '';
};
const resolveURL = (path, url) => {
try {
const urlAfter = new URL(path, url);
return urlAfter.href;
}
catch(_) {
}
};
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 ) {
const diffParsed = /^([ad])(\d+) (\d+)$/.exec(diffLines[iDiff++]);
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');
}
/******************************************************************************/
// 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 ) {
assetDetails.error = 'badchecksum';
return false;
}
assetDetails.text = textAfter;
const head = textAfter.slice(0, 1024);
let match = /^! Diff-Path: (\S+)/m.exec(head);
assetDetails.patchPath = match ? match[1] : undefined;
match = /^! Diff-Name: (\S+)/m.exec(head);
assetDetails.diffName = match ? match[1] : undefined;
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) ) {
const diffURL = resolveURL(patchPath, cdnURL);
if ( diffURL === undefined ) { continue; }
const response = await fetch(diffURL).catch(reason => {
console.error(reason);
});
if ( response === undefined ) { continue; }
if ( response.ok !== true ) { continue; }
const patchText = await response.text();
const patchDetails = parsePatch(patchText);
if ( patchDetails === undefined ) { continue; }
return { diffURL, patchDetails };
}
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);
}
if ( assetDetails.fetch === false ) { return null; }
const patchDetailsPromise = fetchPatchDetailsFromCDNs(assetDetails);
patches.set(patchFile, patchDetailsPromise);
return patchDetailsPromise;
}
async function fetchAndApplyAllPatches(assetDetails) {
const { diffURL, patchDetails } = await fetchPatchDetails(assetDetails);
if ( patchDetails === null ) {
assetDetails.error = 'nopatch';
return assetDetails;
}
const diffDetails = patchDetails.get(assetDetails.diffName);
if ( diffDetails === undefined ) {
assetDetails.error = 'nodiff';
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';
assetDetails.diffURL = diffURL;
return assetDetails;
}
/******************************************************************************/
const bc = new globalThis.BroadcastChannel('diffUpdater');
bc.onmessage = ev => {
const message = ev.data;
switch ( message.what ) {
case 'update':
fetchAndApplyAllPatches(message).then(response => {
bc.postMessage(response);
});
break;
}
};
bc.postMessage({ what: 'ready' });
/******************************************************************************/