mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-16 23:42:39 +01:00
244 lines
8.1 KiB
JavaScript
244 lines
8.1 KiB
JavaScript
|
/*******************************************************************************
|
||
|
|
||
|
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' });
|
||
|
|
||
|
/******************************************************************************/
|