mirror of
https://github.com/gorhill/uBlock.git
synced 2026-03-11 09:04:36 +00:00
Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcc5113e54 | ||
|
|
c5d6aea28c | ||
|
|
9aa216642c | ||
|
|
27bc54b2cf | ||
|
|
9a208bd469 | ||
|
|
461c95c769 | ||
|
|
01a3b3f717 | ||
|
|
805336febd |
7 changed files with 141 additions and 51 deletions
|
|
@ -1538,19 +1538,31 @@
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
// https://github.com/uBlockOrigin/uAssets/issues/5184
|
|
||||||
/// twitch-videoad.js
|
/// twitch-videoad.js
|
||||||
|
// https://github.com/uBlockOrigin/uAssets/issues/5184
|
||||||
|
// https://github.com/pixeltris/TwitchAdSolutions/commit/6be4c5313035
|
||||||
(function() {
|
(function() {
|
||||||
if ( /(^|\.)twitch\.tv$/.test(document.location.hostname) === false ) { return; }
|
if ( /(^|\.)twitch\.tv$/.test(document.location.hostname) === false ) { return; }
|
||||||
var realFetch = window.fetch;
|
window.fetch = new Proxy(window.fetch, {
|
||||||
window.fetch = function(input) {
|
apply: function(target, thisArg, args) {
|
||||||
if ( arguments.length >= 2 && typeof input === 'string' && input.includes('/access_token') ) {
|
const [ url, init ] = args;
|
||||||
var url = new URL(arguments[0]);
|
if (
|
||||||
url.searchParams.delete('platform');
|
typeof url === 'string' &&
|
||||||
arguments[0] = url.href;
|
url.includes('gql') &&
|
||||||
|
init instanceof Object &&
|
||||||
|
init.headers instanceof Object
|
||||||
|
) {
|
||||||
|
const { headers } = init;
|
||||||
|
if ( typeof headers['X-Device-Id'] === 'string' ) {
|
||||||
|
headers['X-Device-Id'] = 'twitch-web-wall-mason';
|
||||||
}
|
}
|
||||||
return realFetch.apply(this, arguments);
|
if ( typeof headers['Device-ID'] === 'string' ) {
|
||||||
};
|
headers['Device-ID'] = 'twitch-web-wall-mason';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Reflect.apply(target, thisArg, args);
|
||||||
|
}
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
2
dist/version
vendored
2
dist/version
vendored
|
|
@ -1 +1 @@
|
||||||
1.38.0
|
1.38.4
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@gorhill/ubo-core",
|
"name": "@gorhill/ubo-core",
|
||||||
"version": "0.1.10",
|
"version": "0.1.11",
|
||||||
"description": "To create a working instance of uBlock Origin's static network filtering engine",
|
"description": "To create a working instance of uBlock Origin's static network filtering engine",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,24 @@ describe('SNFE', () => {
|
||||||
await engine.deserialize(serialized);
|
await engine.deserialize(serialized);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('Filter matching', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
engine = await module.StaticNetFilteringEngine.create();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match block-important pure-hostname filter', async () => {
|
||||||
|
await engine.useLists([
|
||||||
|
{ name: 'test', raw: '@@||example.com^\n||example.com^$important' },
|
||||||
|
]);
|
||||||
|
engine.matchRequest({
|
||||||
|
originURL: 'https://www.example.com/',
|
||||||
|
type: 'main_frame',
|
||||||
|
url: 'https://www.example.com/',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -153,8 +153,8 @@ const µBlock = { // jshint ignore:line
|
||||||
|
|
||||||
// Read-only
|
// Read-only
|
||||||
systemSettings: {
|
systemSettings: {
|
||||||
compiledMagic: 38, // Increase when compiled format changes
|
compiledMagic: 39, // Increase when compiled format changes
|
||||||
selfieMagic: 38, // Increase when selfie format changes
|
selfieMagic: 39, // Increase when selfie format changes
|
||||||
},
|
},
|
||||||
|
|
||||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/759#issuecomment-546654501
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/759#issuecomment-546654501
|
||||||
|
|
|
||||||
|
|
@ -135,8 +135,6 @@ const allTypesBits =
|
||||||
1 << (typeNameToTypeValue['main_frame'] >>> TypeBitsOffset) - 1 |
|
1 << (typeNameToTypeValue['main_frame'] >>> TypeBitsOffset) - 1 |
|
||||||
1 << (typeNameToTypeValue['inline-font'] >>> TypeBitsOffset) - 1 |
|
1 << (typeNameToTypeValue['inline-font'] >>> TypeBitsOffset) - 1 |
|
||||||
1 << (typeNameToTypeValue['inline-script'] >>> TypeBitsOffset) - 1;
|
1 << (typeNameToTypeValue['inline-script'] >>> TypeBitsOffset) - 1;
|
||||||
const documentTypeBit =
|
|
||||||
1 << (typeNameToTypeValue['main_frame'] >>> TypeBitsOffset) - 1;
|
|
||||||
const unsupportedTypeBit =
|
const unsupportedTypeBit =
|
||||||
1 << (typeNameToTypeValue['unsupported'] >>> TypeBitsOffset) - 1;
|
1 << (typeNameToTypeValue['unsupported'] >>> TypeBitsOffset) - 1;
|
||||||
|
|
||||||
|
|
@ -161,6 +159,8 @@ const typeValueToTypeName = [
|
||||||
'inline-font',
|
'inline-font',
|
||||||
'inline-script',
|
'inline-script',
|
||||||
'cname',
|
'cname',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
'webrtc',
|
'webrtc',
|
||||||
'unsupported',
|
'unsupported',
|
||||||
];
|
];
|
||||||
|
|
@ -186,6 +186,7 @@ const EMPTY_TOKEN_HASH = 0xF0000000;
|
||||||
// See the following as short-lived registers, used during evaluation. They are
|
// See the following as short-lived registers, used during evaluation. They are
|
||||||
// valid until the next evaluation.
|
// valid until the next evaluation.
|
||||||
|
|
||||||
|
let $requestTypeValue = 0;
|
||||||
let $requestURL = '';
|
let $requestURL = '';
|
||||||
let $requestURLRaw = '';
|
let $requestURLRaw = '';
|
||||||
let $requestHostname = '';
|
let $requestHostname = '';
|
||||||
|
|
@ -1271,6 +1272,57 @@ registerFilterClass(FilterRegex);
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
|
// stylesheet: 1 => bit 0
|
||||||
|
// image: 2 => bit 1
|
||||||
|
// object: 3 => bit 2
|
||||||
|
// script: 4 => bit 3
|
||||||
|
// ...
|
||||||
|
|
||||||
|
const FilterNotType = class {
|
||||||
|
constructor(notTypeBits) {
|
||||||
|
this.notTypeBits = notTypeBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
match() {
|
||||||
|
return $requestTypeValue !== 0 &&
|
||||||
|
(this.notTypeBits & (1 << ($requestTypeValue - 1))) === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
logData(details) {
|
||||||
|
let bits = this.notTypeBits;
|
||||||
|
for ( let i = 1; bits !== 0 && i < typeValueToTypeName.length; i++ ) {
|
||||||
|
const bit = 1 << (i - 1);
|
||||||
|
if ( (bits & bit) === 0 ) { continue; }
|
||||||
|
bits &= ~bit;
|
||||||
|
details.options.push(`~${typeValueToTypeName[i]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toSelfie() {
|
||||||
|
return [ this.fid, this.notTypeBits ];
|
||||||
|
}
|
||||||
|
|
||||||
|
static compile(details) {
|
||||||
|
return [ FilterNotType.fid, details.notTypeBits ];
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromCompiled(args) {
|
||||||
|
return new FilterNotType(args[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromSelfie(args) {
|
||||||
|
return new FilterNotType(args[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static keyFromArgs(args) {
|
||||||
|
return `${args[1]}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerFilterClass(FilterNotType);
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
// A helper class to parse `domain=` option.
|
// A helper class to parse `domain=` option.
|
||||||
|
|
||||||
const DomainOptIterator = class {
|
const DomainOptIterator = class {
|
||||||
|
|
@ -2908,8 +2960,10 @@ class FilterCompiler {
|
||||||
: allTypesBits;
|
: allTypesBits;
|
||||||
if ( not ) {
|
if ( not ) {
|
||||||
this.notTypeBits |= typeBit;
|
this.notTypeBits |= typeBit;
|
||||||
|
this.typeBits &= ~typeBit;
|
||||||
} else {
|
} else {
|
||||||
this.typeBits |= typeBit;
|
this.typeBits |= typeBit;
|
||||||
|
this.notTypeBits &= ~typeBit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3013,6 +3067,7 @@ class FilterCompiler {
|
||||||
break;
|
break;
|
||||||
case this.parser.OPTTokenImportant:
|
case this.parser.OPTTokenImportant:
|
||||||
if ( this.action === AllowAction ) { return false; }
|
if ( this.action === AllowAction ) { return false; }
|
||||||
|
this.optionUnitBits |= this.IMPORTANT_BIT;
|
||||||
this.action = BlockImportant;
|
this.action = BlockImportant;
|
||||||
break;
|
break;
|
||||||
// Used by Adguard:
|
// Used by Adguard:
|
||||||
|
|
@ -3085,14 +3140,8 @@ class FilterCompiler {
|
||||||
// - no network type is present -- i.e. all network types are
|
// - no network type is present -- i.e. all network types are
|
||||||
// implicitly toggled on
|
// implicitly toggled on
|
||||||
if ( this.notTypeBits !== 0 ) {
|
if ( this.notTypeBits !== 0 ) {
|
||||||
if (
|
|
||||||
(this.notTypeBits & allNetworkTypesBits) !== 0 ||
|
|
||||||
(this.typeBits & allNetworkTypesBits) === 0
|
|
||||||
) {
|
|
||||||
this.typeBits |= allNetworkTypesBits;
|
|
||||||
}
|
|
||||||
this.typeBits &= ~this.notTypeBits;
|
this.typeBits &= ~this.notTypeBits;
|
||||||
if ( this.typeBits === 0 ) { return false; }
|
this.optionUnitBits |= this.NOT_TYPE_BIT;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSP directives implicitly apply only to document/subdocument.
|
// CSP directives implicitly apply only to document/subdocument.
|
||||||
|
|
@ -3434,6 +3483,11 @@ class FilterCompiler {
|
||||||
units.push(FilterAnchorRight.compile());
|
units.push(FilterAnchorRight.compile());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Not types
|
||||||
|
if ( this.notTypeBits !== 0 ) {
|
||||||
|
units.push(FilterNotType.compile(this));
|
||||||
|
}
|
||||||
|
|
||||||
// Strict partiness
|
// Strict partiness
|
||||||
if ( this.strictParty !== 0 ) {
|
if ( this.strictParty !== 0 ) {
|
||||||
units.push(FilterStrictParty.compile(this));
|
units.push(FilterStrictParty.compile(this));
|
||||||
|
|
@ -3492,22 +3546,21 @@ class FilterCompiler {
|
||||||
|
|
||||||
compileToAtomicFilter(fdata, writer) {
|
compileToAtomicFilter(fdata, writer) {
|
||||||
const catBits = this.action | this.party;
|
const catBits = this.action | this.party;
|
||||||
let { typeBits } = this;
|
let { notTypeBits, typeBits } = this;
|
||||||
|
|
||||||
// Typeless
|
// Typeless
|
||||||
if ( typeBits === 0 ) {
|
if ( typeBits === 0 ) {
|
||||||
writer.push([ catBits, this.tokenHash, fdata ]);
|
writer.push([ catBits, this.tokenHash, fdata ]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// If all network types are set, create a typeless filter
|
// If all network types are set, create a typeless filter. Excluded
|
||||||
if (
|
// network types are tested at match time, se we act as if they are
|
||||||
(typeBits & allNetworkTypesBits) === allNetworkTypesBits &&
|
// set.
|
||||||
(this.notTypeBits & documentTypeBit) === 0
|
if ( ((typeBits | notTypeBits) & allNetworkTypesBits) === allNetworkTypesBits ) {
|
||||||
) {
|
|
||||||
writer.push([ catBits, this.tokenHash, fdata ]);
|
writer.push([ catBits, this.tokenHash, fdata ]);
|
||||||
typeBits &= ~allNetworkTypesBits;
|
typeBits &= ~allNetworkTypesBits;
|
||||||
|
if ( typeBits === 0 ) { return; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// One filter per specific types
|
// One filter per specific types
|
||||||
let bitOffset = 1;
|
let bitOffset = 1;
|
||||||
do {
|
do {
|
||||||
|
|
@ -3524,13 +3577,15 @@ class FilterCompiler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterCompiler.prototype.DOMAIN_BIT = 0b00000001;
|
FilterCompiler.prototype.DOMAIN_BIT = 0b000000001;
|
||||||
FilterCompiler.prototype.DENYALLOW_BIT = 0b00000010;
|
FilterCompiler.prototype.DENYALLOW_BIT = 0b000000010;
|
||||||
FilterCompiler.prototype.HEADER_BIT = 0b00000100;
|
FilterCompiler.prototype.HEADER_BIT = 0b000000100;
|
||||||
FilterCompiler.prototype.STRICT_PARTY_BIT = 0b00001000;
|
FilterCompiler.prototype.STRICT_PARTY_BIT = 0b000001000;
|
||||||
FilterCompiler.prototype.CSP_BIT = 0b00010000;
|
FilterCompiler.prototype.CSP_BIT = 0b000010000;
|
||||||
FilterCompiler.prototype.QUERYPRUNE_BIT = 0b00100000;
|
FilterCompiler.prototype.QUERYPRUNE_BIT = 0b000100000;
|
||||||
FilterCompiler.prototype.REDIRECT_BIT = 0b01000000;
|
FilterCompiler.prototype.REDIRECT_BIT = 0b001000000;
|
||||||
|
FilterCompiler.prototype.NOT_TYPE_BIT = 0b010000000;
|
||||||
|
FilterCompiler.prototype.IMPORTANT_BIT = 0b100000000;
|
||||||
|
|
||||||
FilterCompiler.prototype.FILTER_OK = 0;
|
FilterCompiler.prototype.FILTER_OK = 0;
|
||||||
FilterCompiler.prototype.FILTER_INVALID = 1;
|
FilterCompiler.prototype.FILTER_INVALID = 1;
|
||||||
|
|
@ -3896,14 +3951,16 @@ FilterContainer.prototype.matchAndFetchModifiers = function(
|
||||||
fctxt,
|
fctxt,
|
||||||
modifierType
|
modifierType
|
||||||
) {
|
) {
|
||||||
|
const typeBits = typeNameToTypeValue[fctxt.type] || otherTypeBitValue;
|
||||||
|
|
||||||
$requestURL = urlTokenizer.setURL(fctxt.url);
|
$requestURL = urlTokenizer.setURL(fctxt.url);
|
||||||
$requestURLRaw = fctxt.url;
|
$requestURLRaw = fctxt.url;
|
||||||
$docHostname = fctxt.getDocHostname();
|
$docHostname = fctxt.getDocHostname();
|
||||||
$docDomain = fctxt.getDocDomain();
|
$docDomain = fctxt.getDocDomain();
|
||||||
$docEntity.reset();
|
$docEntity.reset();
|
||||||
$requestHostname = fctxt.getHostname();
|
$requestHostname = fctxt.getHostname();
|
||||||
|
$requestTypeValue = (typeBits & TypeBitsMask) >>> TypeBitsOffset;
|
||||||
|
|
||||||
const typeBits = typeNameToTypeValue[fctxt.type] || otherTypeBitValue;
|
|
||||||
const partyBits = fctxt.is3rdPartyToDoc() ? ThirdParty : FirstParty;
|
const partyBits = fctxt.is3rdPartyToDoc() ? ThirdParty : FirstParty;
|
||||||
|
|
||||||
const catBits00 = ModifyAction;
|
const catBits00 = ModifyAction;
|
||||||
|
|
@ -4192,6 +4249,7 @@ FilterContainer.prototype.matchRequestReverse = function(type, url) {
|
||||||
// Prime tokenizer: we get a normalized URL in return.
|
// Prime tokenizer: we get a normalized URL in return.
|
||||||
$requestURL = urlTokenizer.setURL(url);
|
$requestURL = urlTokenizer.setURL(url);
|
||||||
$requestURLRaw = url;
|
$requestURLRaw = url;
|
||||||
|
$requestTypeValue = (typeBits & TypeBitsMask) >>> TypeBitsOffset;
|
||||||
this.$filterUnit = 0;
|
this.$filterUnit = 0;
|
||||||
|
|
||||||
// These registers will be used by various filters
|
// These registers will be used by various filters
|
||||||
|
|
@ -4234,17 +4292,17 @@ FilterContainer.prototype.matchRequestReverse = function(type, url) {
|
||||||
* @returns {integer} 0=no match, 1=block, 2=allow (exeption)
|
* @returns {integer} 0=no match, 1=block, 2=allow (exeption)
|
||||||
*/
|
*/
|
||||||
FilterContainer.prototype.matchRequest = function(fctxt, modifiers = 0) {
|
FilterContainer.prototype.matchRequest = function(fctxt, modifiers = 0) {
|
||||||
let typeValue = typeNameToTypeValue[fctxt.type];
|
let typeBits = typeNameToTypeValue[fctxt.type];
|
||||||
if ( modifiers === 0 ) {
|
if ( modifiers === 0 ) {
|
||||||
if ( typeValue === undefined ) {
|
if ( typeBits === undefined ) {
|
||||||
typeValue = otherTypeBitValue;
|
typeBits = otherTypeBitValue;
|
||||||
} else if ( typeValue === 0 || typeValue > otherTypeBitValue ) {
|
} else if ( typeBits === 0 || typeBits > otherTypeBitValue ) {
|
||||||
modifiers |= 0b0001;
|
modifiers |= 0b0001;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ( (modifiers & 0b0001) !== 0 ) {
|
if ( (modifiers & 0b0001) !== 0 ) {
|
||||||
if ( typeValue === undefined ) { return 0; }
|
if ( typeBits === undefined ) { return 0; }
|
||||||
typeValue |= 0x80000000;
|
typeBits |= 0x80000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
const partyBits = fctxt.is3rdPartyToDoc() ? ThirdParty : FirstParty;
|
const partyBits = fctxt.is3rdPartyToDoc() ? ThirdParty : FirstParty;
|
||||||
|
|
@ -4259,13 +4317,14 @@ FilterContainer.prototype.matchRequest = function(fctxt, modifiers = 0) {
|
||||||
$docDomain = fctxt.getDocDomain();
|
$docDomain = fctxt.getDocDomain();
|
||||||
$docEntity.reset();
|
$docEntity.reset();
|
||||||
$requestHostname = fctxt.getHostname();
|
$requestHostname = fctxt.getHostname();
|
||||||
|
$requestTypeValue = (typeBits & TypeBitsMask) >>> TypeBitsOffset;
|
||||||
|
|
||||||
// Evaluate block realm before allow realm, and allow realm before
|
// Evaluate block realm before allow realm, and allow realm before
|
||||||
// block-important realm, i.e. by order of likelihood of a match.
|
// block-important realm, i.e. by order of likelihood of a match.
|
||||||
const r = this.realmMatchString(BlockAction, typeValue, partyBits);
|
const r = this.realmMatchString(BlockAction, typeBits, partyBits);
|
||||||
if ( r || (modifiers & 0b0010) !== 0 ) {
|
if ( r || (modifiers & 0b0010) !== 0 ) {
|
||||||
if ( this.realmMatchString(AllowAction, typeValue, partyBits) ) {
|
if ( this.realmMatchString(AllowAction, typeBits, partyBits) ) {
|
||||||
if ( this.realmMatchString(BlockImportant, typeValue, partyBits) ) {
|
if ( this.realmMatchString(BlockImportant, typeBits, partyBits) ) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return 2;
|
return 2;
|
||||||
|
|
@ -4278,7 +4337,7 @@ FilterContainer.prototype.matchRequest = function(fctxt, modifiers = 0) {
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
FilterContainer.prototype.matchHeaders = function(fctxt, headers) {
|
FilterContainer.prototype.matchHeaders = function(fctxt, headers) {
|
||||||
const typeValue = typeNameToTypeValue[fctxt.type] || otherTypeBitValue;
|
const typeBits = typeNameToTypeValue[fctxt.type] || otherTypeBitValue;
|
||||||
const partyBits = fctxt.is3rdPartyToDoc() ? ThirdParty : FirstParty;
|
const partyBits = fctxt.is3rdPartyToDoc() ? ThirdParty : FirstParty;
|
||||||
|
|
||||||
// Prime tokenizer: we get a normalized URL in return.
|
// Prime tokenizer: we get a normalized URL in return.
|
||||||
|
|
@ -4291,13 +4350,14 @@ FilterContainer.prototype.matchHeaders = function(fctxt, headers) {
|
||||||
$docDomain = fctxt.getDocDomain();
|
$docDomain = fctxt.getDocDomain();
|
||||||
$docEntity.reset();
|
$docEntity.reset();
|
||||||
$requestHostname = fctxt.getHostname();
|
$requestHostname = fctxt.getHostname();
|
||||||
|
$requestTypeValue = (typeBits & TypeBitsMask) >>> TypeBitsOffset;
|
||||||
$httpHeaders.init(headers);
|
$httpHeaders.init(headers);
|
||||||
|
|
||||||
let r = 0;
|
let r = 0;
|
||||||
if ( this.realmMatchString(HEADERS | BlockImportant, typeValue, partyBits) ) {
|
if ( this.realmMatchString(HEADERS | BlockImportant, typeBits, partyBits) ) {
|
||||||
r = 1;
|
r = 1;
|
||||||
} else if ( this.realmMatchString(HEADERS | BlockAction, typeValue, partyBits) ) {
|
} else if ( this.realmMatchString(HEADERS | BlockAction, typeBits, partyBits) ) {
|
||||||
r = this.realmMatchString(HEADERS | AllowAction, typeValue, partyBits)
|
r = this.realmMatchString(HEADERS | AllowAction, typeBits, partyBits)
|
||||||
? 2
|
? 2
|
||||||
: 1;
|
: 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit cfe4ae71348fa08bfd5018a20946d13df3be9813
|
Subproject commit 5c857ff1834fa44a482e0190e212d933bb41d28a
|
||||||
Loading…
Reference in a new issue