fix(sub): use a fullwidth percent in USAGE_PERCENTAGE (#6174)

* fix(sub): use a fullwidth percent in USAGE_PERCENTAGE

A remark is placed in the share link fragment, so an ASCII percent is
percent-encoded to %25. Happ treats such a fragment as malformed, discards the
whole remark and falls back to showing the server hostname, which defeats the
point of a remark template and leaks the host into the client's server list.

Emit U+FF05 FULLWIDTH PERCENT SIGN instead. It renders the same to a reader,
never produces %25, and round-trips through url.Parse unchanged.

* test(sub): exercise production fragment encoding

---------

Co-authored-by: n0ctal <293235942+n0ctal@users.noreply.github.com>
This commit is contained in:
n0ctal
2026-08-14 23:13:15 +05:00
committed by GitHub
parent 34c248bb79
commit ad32144c42
3 changed files with 26 additions and 10 deletions
+1 -1
View File
@@ -39,7 +39,7 @@ export const REMARK_VARIABLES: RemarkVar[] = [
{ token: 'STATUS_EMOJI', group: 'time', sample: '✅' },
{ token: 'DAYS_LEFT', group: 'time', sample: '12' },
{ token: 'TIME_LEFT', group: 'time', sample: '12d 4h 30m' },
{ token: 'USAGE_PERCENTAGE', group: 'time', sample: '52.3%' },
{ token: 'USAGE_PERCENTAGE', group: 'time', sample: '52.3' },
{ token: 'EXPIRE_DATE', group: 'time', sample: '2026-09-01' },
{ token: 'JALALI_EXPIRE_DATE', group: 'time', sample: '1405/06/10' },
{ token: 'EXPIRE_UNIX', group: 'time', sample: '1788300000' },
+3 -3
View File
@@ -350,8 +350,8 @@ func statusEmoji(st xray.ClientTraffic) string {
}
}
// usagePercentage computes the traffic usage as a percentage string (e.g. "52.3%").
// Returns "" when the client has no traffic limit.
// usagePercentage computes the traffic usage as a percentage string (e.g. "52.3").
// Uses U+FF05: an ASCII percent encodes to %25, which Happ rejects, dropping the remark.
func usagePercentage(st xray.ClientTraffic) string {
if st.Total <= 0 {
return ""
@@ -361,7 +361,7 @@ func usagePercentage(st xray.ClientTraffic) string {
if pct > 100 {
pct = 100 // clamp over-quota usage, consistent with TRAFFIC_LEFT
}
return fmt.Sprintf("%.1f%%", pct)
return fmt.Sprintf("%.1f", pct)
}
// timeLeftLabel renders remaining time as "Xd Xh Xm" (or shorter when days/hours
+22 -6
View File
@@ -1,6 +1,7 @@
package sub
import (
"net/url"
"strings"
"testing"
@@ -480,18 +481,33 @@ func TestStatusEmoji(t *testing.T) {
}
func TestUsagePercentage(t *testing.T) {
if got := usagePercentage(xray.ClientTraffic{Total: 100 * gb, Up: 25 * gb, Down: 25 * gb}); got != "50.0%" {
if got := usagePercentage(xray.ClientTraffic{Total: 100 * gb, Up: 25 * gb, Down: 25 * gb}); got != "50.0" {
t.Errorf("usagePercentage 50%% = %q", got)
}
if got := usagePercentage(xray.ClientTraffic{Total: 0}); got != "" {
t.Errorf("usagePercentage unlimited = %q, want empty", got)
}
if got := usagePercentage(xray.ClientTraffic{Total: 10 * gb, Up: 10 * gb}); got != "100.0%" {
if got := usagePercentage(xray.ClientTraffic{Total: 10 * gb, Up: 10 * gb}); got != "100.0" {
t.Errorf("usagePercentage 100%% = %q", got)
}
// Over-quota usage clamps to 100%, consistent with TRAFFIC_LEFT.
if got := usagePercentage(xray.ClientTraffic{Total: 10 * gb, Up: 25 * gb}); got != "100.0%" {
t.Errorf("usagePercentage over-quota = %q, want 100.0%%", got)
if got := usagePercentage(xray.ClientTraffic{Total: 10 * gb, Up: 25 * gb}); got != "100.0" {
t.Errorf("usagePercentage over-quota = %q, want 100.0", got)
}
}
func TestUsagePercentageSurvivesFragmentEncoding(t *testing.T) {
remark := "node " + usagePercentage(xray.ClientTraffic{Total: 100 * gb, Up: 50 * gb})
link := buildLinkWithParams("vless://id@example.test:443", nil, remark)
if strings.Contains(link, "%25") {
t.Fatalf("encoded remark contains %%25, Happ drops such remarks: %s", link)
}
u, err := url.Parse(link)
if err != nil {
t.Fatalf("parse: %v", err)
}
if u.Fragment != remark {
t.Fatalf("fragment = %q, want %q", u.Fragment, remark)
}
}
@@ -546,7 +562,7 @@ func TestExpandNewTokensInTemplate(t *testing.T) {
cases := []struct{ tmpl, want string }{
{"{{STATUS_EMOJI}}", "✅"},
{"{{USAGE_PERCENTAGE}}", "50.0%"},
{"{{USAGE_PERCENTAGE}}", "50.0"},
{"{{PROTOCOL}}", "VLESS"},
{"{{TRANSPORT}}", "ws"},
{"{{SECURITY}}", "REALITY"},
@@ -619,7 +635,7 @@ func TestExpandRemarkVars_SingleBracketUI(t *testing.T) {
{"{DATA_USAGE}", "50.00GB"},
{"{DATA_LIMIT}", "100.00GB"},
{"{STATUS_EMOJI}", "✅"},
{"{USAGE_PERCENTAGE}", "50.0%"},
{"{USAGE_PERCENTAGE}", "50.0"},
{"{PROTOCOL}", "VLESS"},
{"{TRANSPORT}", "ws"},
{"{SECURITY}", "TLS"},