From a0b1837a1a19b8fbeb8c4936f22bfe6aa74367eb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 25 Mar 2026 23:51:47 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Sync=202026-03-25=2023:51:47?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- luci-app-fchomo/.prepare.sh | 4 +- luci-app-fchomo/docs/Ruleset-URI-Scheme.md | 8 +- .../htdocs/luci-static/resources/fchomo.js | 128 ++++++++++++------ .../resources/view/fchomo/ruleset.js | 103 +++++++------- 4 files changed, 151 insertions(+), 92 deletions(-) diff --git a/luci-app-fchomo/.prepare.sh b/luci-app-fchomo/.prepare.sh index 60dc6754..d8064040 100755 --- a/luci-app-fchomo/.prepare.sh +++ b/luci-app-fchomo/.prepare.sh @@ -33,8 +33,8 @@ echo '' 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')'|" \ diff --git a/luci-app-fchomo/docs/Ruleset-URI-Scheme.md b/luci-app-fchomo/docs/Ruleset-URI-Scheme.md index e0007b8a..498fabc1 100644 --- a/luci-app-fchomo/docs/Ruleset-URI-Scheme.md +++ b/luci-app-fchomo/docs/Ruleset-URI-Scheme.md @@ -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 diff --git a/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js b/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js index 8393bf34..428a9673 100644 --- a/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js +++ b/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js @@ -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} + */ +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, diff --git a/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/ruleset.js b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/ruleset.js index 24cbd0e8..ce760ae4 100644 --- a/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/ruleset.js +++ b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/ruleset.js @@ -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: %s and format: %s.
') .format('file, http, inline', 'text, yaml, mrs') + _('Please refer to %s 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