fix(sub): convert far expiry dates to Jalali in constant time

gregorianToJalali walked the calendar one month at a time from a 2024
reference, so an expiry near the int64 limit (a node reports expiries
unchecked) cost seconds of CPU on every subscription fetch that renders
{{JALALI_EXPIRE_DATE}}. Jalali leap years repeat every 33 years, so
whole 33-year blocks are skipped before the walk.
This commit is contained in:
MHSanaei
2026-10-31 14:28:18 +01:00
parent fe5d704d67
commit 47e9ef1a07
2 changed files with 23 additions and 0 deletions
+7
View File
@@ -423,6 +423,13 @@ func gregorianToJalali(gy, gm, gd int) (jy, jm, jd int) {
refJDN := 2460311
days := int64(jdn - refJDN)
jy, jm, jd = 1402, 10, 11
// Leap years follow y % 33, so whole 33-year blocks are skipped: a node-reported expiry
// can lie hundreds of millions of years away, and the walk below goes month by month.
const jalaliCycleDays = 33*365 + 8
if cycles := days / jalaliCycleDays; cycles != 0 {
jy += int(cycles) * 33
days -= cycles * jalaliCycleDays
}
// Walk forward
for days > 0 {
+16
View File
@@ -4,6 +4,7 @@ import (
"net/url"
"strings"
"testing"
"time"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
@@ -529,6 +530,8 @@ func TestGregorianToJalali(t *testing.T) {
{2024, 1, 1, 1402, 10, 11},
{2000, 3, 20, 1379, 1, 1},
{1979, 2, 11, 1357, 11, 22},
// 2124-01-01 lies three 33-year blocks and a year past the reference.
{2124, 1, 1, 1502, 10, 11},
}
for _, c := range cases {
jy, jm, jd := gregorianToJalali(c.gy, c.gm, c.gd)
@@ -539,6 +542,19 @@ func TestGregorianToJalali(t *testing.T) {
}
}
// A node reports the expiry the remark renders, so a date hundreds of millions of years
// out must not walk the calendar month by month on every subscription fetch.
func TestGregorianToJalaliFarDatesTakeConstantTime(t *testing.T) {
start := time.Now()
if jy, _, _ := gregorianToJalali(292000000, 1, 1); jy < 290000000 {
t.Fatalf("year 292000000 converted to Jalali year %d", jy)
}
_, _, _ = gregorianToJalali(-292000000, 1, 1)
if took := time.Since(start); took > 100*time.Millisecond {
t.Fatalf("converting far dates took %v", took)
}
}
func TestJalaliExpireDateLabel(t *testing.T) {
if got := jalaliExpireDateLabel(0); got != "" {
t.Errorf("jalaliExpireDateLabel(0) = %q, want empty", got)