Skip to content

Commit f3cce8e

Browse files
committed
Add byteforamts
1 parent 537613e commit f3cce8e

File tree

3 files changed

+347
-0
lines changed

3 files changed

+347
-0
lines changed

common/byteformats/formats.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package byteformats
2+
3+
import (
4+
"fmt"
5+
"math"
6+
)
7+
8+
var (
9+
unitNames = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
10+
iUnitNames = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
11+
)
12+
13+
func formatBytes(s uint64, base float64, sizes []string) string {
14+
if s < 10 {
15+
return fmt.Sprintf("%d B", s)
16+
}
17+
e := math.Floor(logn(float64(s), base))
18+
suffix := sizes[int(e)]
19+
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
20+
f := "%.0f %s"
21+
if val < 10 {
22+
f = "%.1f %s"
23+
}
24+
25+
return fmt.Sprintf(f, val, suffix)
26+
}
27+
28+
func logn(n, b float64) float64 {
29+
return math.Log(n) / math.Log(b)
30+
}
31+
32+
func FormatBytes(s uint64) string {
33+
return formatBytes(s, 1000, unitNames)
34+
}
35+
36+
func FormatMemoryBytes(s uint64) string {
37+
return formatBytes(s, 1024, unitNames)
38+
}
39+
40+
func FormatIBytes(s uint64) string {
41+
return formatBytes(s, 1024, iUnitNames)
42+
}

common/byteformats/json.go

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package byteformats
2+
3+
import (
4+
"strconv"
5+
"strings"
6+
7+
E "github.com/sagernet/sing/common/exceptions"
8+
"github.com/sagernet/sing/common/json"
9+
)
10+
11+
const (
12+
Byte = 1 << (iota * 10)
13+
KiByte
14+
MiByte
15+
GiByte
16+
TiByte
17+
PiByte
18+
EiByte
19+
)
20+
21+
const (
22+
KByte = Byte * 1000
23+
MByte = KByte * 1000
24+
GByte = MByte * 1000
25+
TByte = GByte * 1000
26+
PByte = TByte * 1000
27+
EByte = PByte * 1000
28+
)
29+
30+
var unitValueTable = map[string]uint64{
31+
"b": Byte,
32+
"k": KByte,
33+
"kb": KByte,
34+
"ki": KiByte,
35+
"kib": KiByte,
36+
"m": MByte,
37+
"mb": MByte,
38+
"mi": MiByte,
39+
"mib": MiByte,
40+
"g": GByte,
41+
"gb": GByte,
42+
"gi": GiByte,
43+
"gib": GiByte,
44+
"t": TByte,
45+
"tb": TByte,
46+
"ti": TiByte,
47+
"tib": TiByte,
48+
"p": PByte,
49+
"pb": PByte,
50+
"pi": PiByte,
51+
"pib": PiByte,
52+
"e": EByte,
53+
"eb": EByte,
54+
"ei": EiByte,
55+
"eib": EiByte,
56+
}
57+
58+
var memoryUnitValueTable = map[string]uint64{
59+
"b": Byte,
60+
"k": KiByte,
61+
"kb": KiByte,
62+
"m": MiByte,
63+
"mb": MiByte,
64+
"g": GiByte,
65+
"gb": GiByte,
66+
"t": TiByte,
67+
"tb": TiByte,
68+
"p": PiByte,
69+
"pb": PiByte,
70+
"e": EiByte,
71+
"eb": EiByte,
72+
}
73+
74+
var networkUnitValueTable = map[string]uint64{
75+
"Bps": Byte,
76+
"Kbps": KByte / 8,
77+
"KBps": KByte,
78+
"Mbps": MByte / 8,
79+
"MBps": MByte,
80+
"Gbps": GByte / 8,
81+
"GBps": GByte,
82+
"Tbps": TByte / 8,
83+
"TBps": TByte,
84+
"Pbps": PByte / 8,
85+
"PBps": PByte,
86+
"Ebps": EByte / 8,
87+
"EBps": EByte,
88+
}
89+
90+
type rawBytes struct {
91+
Value uint64
92+
Unit string
93+
UnitValue uint64
94+
}
95+
96+
func (b rawBytes) MarshalJSON() ([]byte, error) {
97+
if b.Unit == "" {
98+
return json.Marshal(b.Value)
99+
}
100+
return json.Marshal(strconv.FormatUint(b.Value/b.UnitValue, 10) + b.Unit)
101+
}
102+
103+
func parseUnit(b *rawBytes, unitTable map[string]uint64, caseSensitive bool, bytes []byte) error {
104+
var intValue int64
105+
err := json.Unmarshal(bytes, &intValue)
106+
if err == nil {
107+
b.Value = uint64(intValue)
108+
b.Unit = ""
109+
b.UnitValue = 1
110+
return nil
111+
}
112+
var stringValue string
113+
err = json.Unmarshal(bytes, &stringValue)
114+
if err != nil {
115+
return err
116+
}
117+
unitIndex := 0
118+
for i, c := range stringValue {
119+
if c < '0' || c > '9' {
120+
unitIndex = i
121+
break
122+
}
123+
}
124+
if unitIndex == 0 {
125+
return E.New("invalid format: ", stringValue)
126+
}
127+
value, err := strconv.ParseUint(stringValue[:unitIndex], 10, 64)
128+
if err != nil {
129+
return E.Cause(err, "parse ", stringValue[:unitIndex])
130+
}
131+
rawUnit := stringValue[unitIndex:]
132+
var unit string
133+
if caseSensitive {
134+
unit = strings.TrimSpace(rawUnit)
135+
} else {
136+
unit = strings.TrimSpace(strings.ToLower(rawUnit))
137+
}
138+
unitValue, loaded := unitTable[unit]
139+
if !loaded {
140+
return E.New("unsupported unit: ", rawUnit)
141+
}
142+
b.Value = value * unitValue
143+
b.Unit = rawUnit
144+
b.UnitValue = unitValue
145+
return nil
146+
}
147+
148+
type Bytes struct {
149+
rawBytes
150+
}
151+
152+
func (b *Bytes) UnmarshalJSON(bytes []byte) error {
153+
return parseUnit(&b.rawBytes, unitValueTable, false, bytes)
154+
}
155+
156+
type MemoryBytes struct {
157+
rawBytes
158+
}
159+
160+
func (m *MemoryBytes) UnmarshalJSON(bytes []byte) error {
161+
return parseUnit(&m.rawBytes, memoryUnitValueTable, false, bytes)
162+
}
163+
164+
type NetworkBytes struct {
165+
rawBytes
166+
}
167+
168+
func (n *NetworkBytes) UnmarshalJSON(bytes []byte) error {
169+
return parseUnit(&n.rawBytes, networkUnitValueTable, true, bytes)
170+
}
171+
172+
type NetworkBytesCompat struct {
173+
rawBytes
174+
}
175+
176+
func (n *NetworkBytesCompat) UnmarshalJSON(bytes []byte) error {
177+
err := parseUnit(&n.rawBytes, networkUnitValueTable, true, bytes)
178+
if err != nil {
179+
newErr := parseUnit(&n.rawBytes, unitValueTable, false, bytes)
180+
if newErr == nil {
181+
return nil
182+
}
183+
}
184+
return err
185+
}

common/byteformats/json_test.go

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package byteformats_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/sagernet/sing/common/byteformats"
7+
"github.com/sagernet/sing/common/json"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestNetworkBytes(t *testing.T) {
13+
t.Parallel()
14+
testMap := map[string]uint64{
15+
"1 Bps": byteformats.Byte,
16+
"1 Kbps": byteformats.KByte / 8,
17+
"1 KBps": byteformats.KByte,
18+
"1 Mbps": byteformats.MByte / 8,
19+
"1 MBps": byteformats.MByte,
20+
"1 Gbps": byteformats.GByte / 8,
21+
"1 GBps": byteformats.GByte,
22+
"1 Tbps": byteformats.TByte / 8,
23+
"1 TBps": byteformats.TByte,
24+
"1 Pbps": byteformats.PByte / 8,
25+
"1 PBps": byteformats.PByte,
26+
"1k": byteformats.KByte,
27+
"1m": byteformats.MByte,
28+
}
29+
for k, v := range testMap {
30+
t.Run(k, func(t *testing.T) {
31+
var nb byteformats.NetworkBytesCompat
32+
require.NoError(t, json.Unmarshal([]byte("\""+k+"\""), &nb))
33+
require.Equal(t, v, nb.Value)
34+
b, err := json.Marshal(nb)
35+
require.NoError(t, err)
36+
require.Equal(t, "\""+k+"\"", string(b))
37+
})
38+
}
39+
}
40+
41+
func TestMemoryBytes(t *testing.T) {
42+
t.Parallel()
43+
testMap := map[string]uint64{
44+
"1 B": byteformats.Byte,
45+
"1 KB": byteformats.KiByte,
46+
"1 MB": byteformats.MiByte,
47+
"1 GB": byteformats.GiByte,
48+
"1 TB": byteformats.TiByte,
49+
"1 PB": byteformats.PiByte,
50+
}
51+
for k, v := range testMap {
52+
t.Run(k, func(t *testing.T) {
53+
var mb byteformats.MemoryBytes
54+
require.NoError(t, json.Unmarshal([]byte("\""+k+"\""), &mb))
55+
require.Equal(t, v, mb.Value)
56+
b, err := json.Marshal(mb)
57+
require.NoError(t, err)
58+
require.Equal(t, "\""+k+"\"", string(b))
59+
})
60+
}
61+
}
62+
63+
func TestDefaultBytes(t *testing.T) {
64+
t.Parallel()
65+
testMap := map[string]uint64{
66+
"1 B": byteformats.Byte,
67+
"1 KB": byteformats.KByte,
68+
"1 KiB": byteformats.KiByte,
69+
"1 MB": byteformats.MByte,
70+
"1 MiB": byteformats.MiByte,
71+
"1 GB": byteformats.GByte,
72+
"1 GiB": byteformats.GiByte,
73+
"1 TB": byteformats.TByte,
74+
"1 TiB": byteformats.TiByte,
75+
"1 PB": byteformats.PByte,
76+
"1 PiB": byteformats.PiByte,
77+
"1 EB": byteformats.EByte,
78+
"1 EiB": byteformats.EiByte,
79+
"1k": byteformats.KByte,
80+
"1m": byteformats.MByte,
81+
"1g": byteformats.GByte,
82+
"1t": byteformats.TByte,
83+
"1p": byteformats.PByte,
84+
"1e": byteformats.EByte,
85+
"1K": byteformats.KByte,
86+
"1M": byteformats.MByte,
87+
"1G": byteformats.GByte,
88+
"1T": byteformats.TByte,
89+
"1P": byteformats.PByte,
90+
"1E": byteformats.EByte,
91+
"1Ki": byteformats.KiByte,
92+
"1Mi": byteformats.MiByte,
93+
"1Gi": byteformats.GiByte,
94+
"1Ti": byteformats.TiByte,
95+
"1Pi": byteformats.PiByte,
96+
"1Ei": byteformats.EiByte,
97+
"1KiB": byteformats.KiByte,
98+
"1MiB": byteformats.MiByte,
99+
"1GiB": byteformats.GiByte,
100+
"1TiB": byteformats.TiByte,
101+
"1PiB": byteformats.PiByte,
102+
"1EiB": byteformats.EiByte,
103+
"1kB": byteformats.KByte,
104+
"1mB": byteformats.MByte,
105+
"1gB": byteformats.GByte,
106+
"1tB": byteformats.TByte,
107+
"1pB": byteformats.PByte,
108+
"1eB": byteformats.EByte,
109+
}
110+
for k, v := range testMap {
111+
t.Run(k, func(t *testing.T) {
112+
var mb byteformats.Bytes
113+
require.NoError(t, json.Unmarshal([]byte("\""+k+"\""), &mb))
114+
require.Equal(t, v, mb.Value)
115+
b, err := json.Marshal(mb)
116+
require.NoError(t, err)
117+
require.Equal(t, "\""+k+"\"", string(b))
118+
})
119+
}
120+
}

0 commit comments

Comments
 (0)