🎉 Sync 2026-03-25 23:51:47

This commit is contained in:
github-actions[bot] 2026-03-25 23:51:47 +08:00
parent f9454edb7a
commit a0b1837a1a
4 changed files with 151 additions and 92 deletions

View File

@ -33,8 +33,8 @@ echo '</style>'
tail -n +$(( $p +1 )) $DOCNAME.html
} > buildin.html
popd
minify "$PKG_BUILD_DIR"/buildin.html | base64 | tr -d '\n' > "$PKG_BUILD_DIR"/base64
sed -i "s|'cmxzdHBsYWNlaG9sZGVy'|'$(cat "$PKG_BUILD_DIR"/base64)'|" "$PKG_BUILD_DIR"/htdocs/luci-static/resources/fchomo.js
minify "$PKG_BUILD_DIR"/buildin.html | gzip -9 -c | base64 | tr -d '\n' > "$PKG_BUILD_DIR"/base64
sed -i "s|'H4sIAAAAAAAAAyvKKS4pyElMTs3Iz0lJLQIA8fIyYQ8AAAA='|'$(cat "$PKG_BUILD_DIR"/base64)'|" "$PKG_BUILD_DIR"/htdocs/luci-static/resources/fchomo.js
# shaka audio
sed -i "s|audio/x-wav|audio/mpeg|;
s|'UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA='|'$(base64 "$CURDIR/docs/$SHARKAUDIO" | tr -d '\n')'|" \

View File

@ -59,9 +59,11 @@ Base64edStr format file content.
Generation steps:
1. Base64 encode payload.
2. Replace all `+` with `-` and all `/` with `_` in base64 string.
3. Remove all `=` from the EOF the base64 string.
0. Payload only supports `string` format.
1. Compress payload using `gzip`. (Optional)
2. Base64 encode payload/compressed payload.
3. Replace all `+` with `-` and all `/` with `_` in base64 string.
4. Remove all `=` from the EOF the base64 string.
### URIFragment

View File

@ -8,7 +8,10 @@
'require validation';
/* Member */
const rulesetdoc = 'data:text/html;base64,' + 'cmxzdHBsYWNlaG9sZGVy';
const rulesetdoc = [
'data:text/html;base64,',
'H4sIAAAAAAAAAyvKKS4pyElMTs3Iz0lJLQIA8fIyYQ8AAAA='
];
const sharkaudio = function() {
return 'data:audio/x-wav;base64,' +
@ -839,48 +842,95 @@ function calcStringMD5(e) {
return (p(a) + p(b) + p(c) + p(d)).toLowerCase();
}
/* Thanks to luci-app-ssr-plus */
function base64Prefmt(str) {
str = str.replace(/-/g, '+').replace(/_/g, '/');
const padding = (4 - (str.length % 4)) % 4;
if (padding)
str += '='.repeat(padding);
return str;
}
/* thanks to homeproxy */
function decodeBase64Str(str) {
/**
* General Base64 Decoding
* @param {string} str - Base64 string
* @param {boolean} asText - true: decoded string (UTF-8), false: Uint8Array
* @returns {string|Uint8Array}
*/
function decodeBase64(str, asText = false) {
if (!str)
return null;
/* Thanks to luci-app-ssr-plus */
str = str.replace(/-/g, '+').replace(/_/g, '/');
let padding = (4 - (str.length % 4)) % 4;
if (padding)
str = str + Array(padding + 1).join('=');
str = base64Prefmt(str);
return decodeURIComponent(Array.prototype.map.call(atob(str), (c) =>
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
).join(''));
if (asText)
return decodeURIComponent(Array.prototype.map.call(atob(str), c =>
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
).join(''));
return Uint8Array.fromBase64(str); // OR Uint8Array.from(atob(str), c => c.charCodeAt(0));
}
function encodeBase64Str(str) {
if (!str)
/**
* General Base64 Encoding
* @param {string|Array|ArrayBuffer|Uint8Array} input - String or binary data to be encoded
* @returns {string}
*/
function encodeBase64(input) {
if (isEmpty(input))
return null;
let buf = encodeURIComponent(str).split('%').slice(1).map(h => parseInt(h, 16));
return btoa(String.fromCharCode(...buf));
if (typeof input === 'string') {
const rawStr = encodeURIComponent(input).replace(/%([0-9A-F]{2})/g, (match, p1) =>
String.fromCharCode(parseInt(p1, 16))
);
return btoa(rawStr);
}
const buf = (input instanceof ArrayBuffer) ? new Uint8Array(input) : input;
return new Uint8Array(buf).toBase64(); // OR btoa(String.fromCharCode.apply(null, buf));
}
function decodeBase64Bin(str) {
if (!str)
// thanks to https://github.com/scottschiller/ArmorAlley/blob/master/src/floppy/index-floppy.html
/**
* Unzip Gzip data
* @param {string|Array|ArrayBuffer|Uint8Array} input - Input data (Base64 string or binary object)
* @param {boolean} asText - Return a string? (true: text, false: arrayBuffer)
* @returns {Promise<string|Uint8Array>}
*/
async function decompressGzip(input, asText = false) {
if (isEmpty(input))
return null;
/* Thanks to luci-app-ssr-plus */
str = str.replace(/-/g, '+').replace(/_/g, '/');
let padding = (4 - (str.length % 4)) % 4;
if (padding)
str = str + Array(padding + 1).join('=');
const ds = new DecompressionStream('gzip');
return Array.prototype.map.call(atob(str), c => c.charCodeAt(0)); // OR Uint8Array.fromBase64(str);
}
let blob_in;
if (typeof input === 'string') {
if (input.match(/^H4sI/)) { // Gzip magic + Deflate
const response = await window.fetch('data:application/octet-stream;base64,' + base64Prefmt(input));
blob_in = await response.blob();
} else
throw new Error('Not a valid base64 encoded gzip');
} else {
// The `input` should be `Array` or `ArrayBuffer` or `Uint8Array`.
input = new Uint8Array(input);
if (input.subarray(0, 3).join(',') === [0x1f, 0x8b, 0x08].join(',')) // Gzip magic + Deflate
blob_in = new Blob([input]);
else
throw new Error('Not a valid gzip');
}
function encodeBase64Bin(buf) {
if (isEmpty(buf))
return null;
const decodedStream = blob_in.stream().pipeThrough(ds);
const blob_out = await new window.Response(decodedStream).blob();
return btoa(String.fromCharCode(...buf)); // OR new Uint8Array(buf).toBase64();
if (asText)
return await blob_out.text();
else {
const buf = await blob_out.arrayBuffer();
return new Uint8Array(buf);
}
}
function generateRand(type, length) {
@ -916,11 +966,11 @@ function shuffle(StrORArr) {
else
throw new Error(`String or Array only`);
for (let i = arr.length - 1; i > 0; i--) { // Traverse the array from back to front
const j = Math.floor(Math.random() * (i + 1)); // Generate a random index between 0 and i
for (let i = arr.length - 1; i > 0; i--) { // Traverse the array from back to front
const j = Math.floor(Math.random() * (i + 1)); // Generate a random index between 0 and i
[arr[i], arr[j]] = [arr[j], arr[i]]; // Swap positions
}
[arr[i], arr[j]] = [arr[j], arr[i]]; // Swap positions
}
if (typeof StrORArr === 'string')
return arr.join('');
@ -951,7 +1001,8 @@ function yaml2json(content, command) {
function isEmpty(res) {
if (res == null) return true; // null, undefined
if (typeof res === 'string' || Array.isArray(res)) return res.length === 0; // empty String/Array
if (typeof res.length === 'number') return res.length === 0; // empty String/Array/TypedArray
if (res instanceof ArrayBuffer) return res.byteLength === 0; // empty ArrayBuffer
if (typeof res === 'object') {
if (res instanceof Map || res instanceof Set) return res.size === 0; // empty Map/Set
return Object.keys(res).length === 0; // empty Object
@ -1510,9 +1561,9 @@ function validateSudokuCustomTable(section_id, value) {
return _('Expecting: %s').format(_('valid format: 2x, 2p, 4v'));
const counts = {};
for (const c of value)
counts[c] = (counts[c] || 0) + 1;
if (!(counts.x === 2 && counts.p === 2 && counts.v === 4))
for (const c of value)
counts[c] = (counts[c] || 0) + 1;
if (!(counts.x === 2 && counts.p === 2 && counts.v === 4))
return _('Expecting: %s').format(_('valid format: 2x, 2p, 4v'));
return true;
@ -1706,10 +1757,9 @@ return baseclass.extend({
/* Method */
calcStringMD5,
decodeBase64Str,
encodeBase64Str,
decodeBase64Bin,
encodeBase64Bin,
decodeBase64,
encodeBase64,
decompressGzip,
generateRand,
shuffle,
json2yaml,

View File

@ -33,15 +33,24 @@ const parseRulesetYaml = hm.parseYaml.extend({
}
});
function parseRulesetLink(section_type, uri) {
const filefmt = new RegExp(/^(text|yaml|mrs)$/);
const filebehav = new RegExp(/^(domain|ipcidr|classical)$/);
async function parseRulesetLink(section_type, uri) {
const filefmt = /^(text|yaml|mrs)$/;
const filebehav = /^(domain|ipcidr|classical)$/;
let config;
uri = uri.split('://');
if (uri[0] && uri[1]) {
switch (uri[0]) {
const done = (conf) => {
if (conf && conf.type && conf.id) {
if (!conf.label) conf.label = conf.id;
return conf;
}
return null;
};
if (!(uri[0] && uri[1])) return done();
switch (uri[0]) {
case 'http':
case 'https':
var url = new URL('http://' + uri[1]);
@ -54,7 +63,7 @@ function parseRulesetLink(section_type, uri) {
if (filefmt.test(format) && filebehav.test(behavior)) {
let fullpath = (url.username ? url.username + '@' : '') + url.host + url.pathname + (rawquery ? '?' + decodeURIComponent(rawquery) : '');
config = {
label: url.hash ? decodeURIComponent(url.hash.slice(1)) : name ? name : null,
label: url.hash ? decodeURIComponent(url.hash.slice(1)) : (name || null),
type: 'http',
format: format,
behavior: behavior,
@ -64,7 +73,7 @@ function parseRulesetLink(section_type, uri) {
};
}
break;
return done(config);
case 'file':
var url = new URL('file://' + uri[1]);
var format = url.searchParams.get('fmt');
@ -75,53 +84,54 @@ function parseRulesetLink(section_type, uri) {
if (filefmt.test(format) && filebehav.test(behavior)) {
config = {
label: url.hash ? decodeURIComponent(url.hash.slice(1)) : name ? name : null,
label: url.hash ? decodeURIComponent(url.hash.slice(1)) : (name || null),
type: 'file',
format: format,
behavior: behavior,
id: hm.calcStringMD5(String.format('file://%s%s', url.host, url.pathname))
};
hm.writeFile(section_type, config.id, hm.decodeBase64Str(filler));
if (filler?.match(/^H4sI/)) // Gzip magic + Deflate
await hm.writeFile(section_type, config.id, await hm.decompressGzip(filler, true));
else
hm.writeFile(section_type, config.id, hm.decodeBase64(filler, true));
}
break;
return done(config);
case 'inline':
var url = new URL('inline:' + uri[1]);
var behavior = url.searchParams.get('behav');
var payload = hm.decodeBase64Str(url.pathname).trim();
var payload = url.pathname.match(/^H4sI/) // Gzip magic + Deflate
? await hm.decompressGzip(url.pathname, true)
: hm.decodeBase64(url.pathname, true);
if (filebehav.test(behavior) && payload && payload.length) {
payload = (payload || '').trim();
if (filebehav.test(behavior) && payload) {
config = {
label: url.hash ? decodeURIComponent(url.hash.slice(1)) : null,
type: 'inline',
behavior: behavior,
payload: payload,
id: hm.calcStringMD5(String.format('inline:%s', btoa(payload)))
id: hm.calcStringMD5(String.format('inline:%s', hm.encodeBase64(payload)))
};
}
break;
}
return done(config);
default:
return done();
}
if (config) {
if (!config.type || !config.id)
return null;
else if (!config.label)
config.label = config.id;
}
return config;
}
return view.extend({
load() {
return Promise.all([
uci.load('fchomo')
uci.load('fchomo'),
hm.decompressGzip(hm.rulesetdoc[1], true).then((res) => { return hm.rulesetdoc[0] + hm.encodeBase64(res); })
]);
},
render(data) {
const rulesetdoc = data[1];
let m, s, o;
m = new form.Map('fchomo', _('Edit ruleset'));
@ -188,34 +198,31 @@ return view.extend({
_('Supports rule-set links of type: <code>%s</code> and format: <code>%s</code>.</br>')
.format('file, http, inline', 'text, yaml, mrs') +
_('Please refer to <a href="%s" target="_blank">%s</a> for link format standards.')
.format(hm.rulesetdoc, _('Ruleset-URI-Scheme')));
.format(rulesetdoc, _('Ruleset-URI-Scheme')));
o.placeholder = 'http(s)://github.com/ACL4SSR/ACL4SSR/raw/refs/heads/master/Clash/Providers/BanAD.yaml?fmt=yaml&behav=classical&rawq=good%3Djob#BanAD\n' +
'file:///example.txt?fmt=text&behav=domain&fill=LmNuCg#CN%20TLD\n' +
'inline://LSAnLmhrJwoK?behav=domain#HK%20TLD\n';
o.handleFn = function(textarea) {
'inline://LSAnLmhrJwoK?behav=domain#HK%20TLD\n' +
'inline://H4sIAAAAAAACA9NVUNcrKVcHANszKpEHAAAA?behav=domain#TW%20TLD\n';
o.handleFn = async function(textarea) {
let input_links = textarea.getValue().trim().split('\n');
if (!input_links[0]) return ui.hideModal();
let imported_count = 0;
if (input_links && input_links[0]) {
/* Remove duplicate lines */
input_links = input_links.reduce((pre, cur) =>
(!pre.includes(cur) && pre.push(cur), pre), []);
input_links.forEach((l) => {
let config = parseRulesetLink(section_type, l);
if (config) {
this.write(config);
imported_count++;
}
});
if (imported_count === 0)
ui.addNotification(null, E('p', _('No valid rule-set link found.')));
else
ui.addNotification(null, E('p', _('Successfully imported %s %s of total %s.')
.format(imported_count, _('rule-set'), input_links.length)), 'info');
input_links = [...new Set(input_links)]; // Remove duplicate lines
for (const link of input_links) {
let config = await parseRulesetLink(section_type, link);
if (config) {
this.write(config);
imported_count++;
}
}
if (imported_count === 0)
ui.addNotification(null, E('p', _('No valid rule-set link found.')));
else
ui.addNotification(null, E('p', _('Successfully imported %s %s of total %s.')
.format(imported_count, _('rule-set'), input_links.length)), 'info');
if (imported_count)
return this.save();
else