Skip to content

Commit 6eeb8de

Browse files
committed
Initial commit
1 parent 4dbdf90 commit 6eeb8de

File tree

5 files changed

+208
-1
lines changed

5 files changed

+208
-1
lines changed

README.md

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,67 @@
1-
# pathcodec
1+
# pathcodec
2+
3+
![Go Version](https://img.shields.io/badge/Go-1.24%2B-blue)
4+
![Go Reference](https://pkg.go.dev/badge/github.com/itpey/pathcodec.svg)
5+
![ReportCard](https://goreportcard.com/badge/github.com/itpey/pathcodec)
6+
![Coverage](https://coveralls.io/repos/github/itpey/pathcodec/badge.svg?branch=main)
7+
![License](https://img.shields.io/github/license/itpey/pathcodec)
8+
9+
A lightweight Go package for compressing and decompressing
10+
[Telegram vector thumbnails](https://core.telegram.org/api/files#vector-thumbnails).
11+
12+
## Installation
13+
14+
```sh
15+
go get -u github.com/itpey/pathcodec
16+
```
17+
18+
## Usage
19+
20+
```go
21+
package main
22+
23+
import (
24+
"fmt"
25+
"github.com/itpey/pathcodec"
26+
)
27+
28+
func main() {
29+
compressed, err := pathcodec.Compress("M257,455c-56,0-109-25-146-65-143-156,31-397,224-318,201,83,136,386-78,383z")
30+
if err != nil {
31+
fmt.Println("Compression error:", err)
32+
return
33+
}
34+
fmt.Println("Compressed:", compressed)
35+
36+
decompressed, err := pathcodec.Decompress(compressed)
37+
if err != nil {
38+
fmt.Println("Decompression error:", err)
39+
return
40+
}
41+
fmt.Println("Decompressed:", decompressed)
42+
}
43+
```
44+
45+
## API
46+
47+
### `func Compress(path string) ([]byte, error)`
48+
49+
Compresses a path string into a compact byte format. The input must start with 'M' and end with 'z'.
50+
51+
### `func Decompress(encoded []byte) (string, error)`
52+
53+
Decompresses a byte slice back into a path string.
54+
55+
## Feedback and Contributions
56+
57+
If you encounter any issues or have suggestions for improvement, please [open an issue](https://github.com/itpey/pathcodec/issues) on GitHub.
58+
59+
We welcome contributions! Fork the repository, make your changes, and submit a pull request.
60+
61+
## License
62+
63+
pathcodec is open-source software released under the MIT License. You can find a copy of the license in the [LICENSE](https://github.com/itpey/pathcodec/blob/main/LICENSE) file.
64+
65+
## Author
66+
67+
pathcodec was created by [itpey](https://github.com/itpey)

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/itpey/pathcodec
2+
3+
go 1.24.1

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
2+
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
3+
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
4+
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

pathcodec.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package pathcodec
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
)
7+
8+
var charToIndex = [256]byte{}
9+
var indexToChar = "AACAAAAHAAALMAAAQASTAVAAAZaacaaaahaaalmaaaqastava.az0123456789-,"
10+
11+
func init() {
12+
for i := range charToIndex {
13+
charToIndex[i] = 255
14+
}
15+
for i, c := range indexToChar {
16+
charToIndex[c] = byte(i)
17+
}
18+
}
19+
20+
func Compress(path string) ([]byte, error) {
21+
if len(path) < 2 || path[0] != 'M' || path[len(path)-1] != 'z' {
22+
return nil, errors.New("invalid input: must start with 'M' and end with 'z'")
23+
}
24+
25+
length := len(path) - 2 // Exclude 'M' and 'z'
26+
encoded := make([]byte, length)
27+
28+
for i := 1; i < len(path)-1; i++ {
29+
char := path[i]
30+
if char == ',' {
31+
encoded[i-1] = 128
32+
} else if char == '-' {
33+
encoded[i-1] = 64
34+
} else if char >= '0' && char <= '9' {
35+
encoded[i-1] = char - '0'
36+
} else if charToIndex[char] == 255 {
37+
return nil, fmt.Errorf("invalid character '%c' in input", char)
38+
} else {
39+
encoded[i-1] = charToIndex[char] + 192
40+
}
41+
}
42+
return encoded, nil
43+
}
44+
45+
func Decompress(encoded []byte) (string, error) {
46+
if len(encoded) == 0 {
47+
return "", errors.New("invalid input: encoded data cannot be empty")
48+
}
49+
length := len(encoded) + 2
50+
path := make([]byte, length)
51+
path[0] = 'M'
52+
path[length-1] = 'z'
53+
54+
for i, num := range encoded {
55+
num = num & 0xff
56+
if num >= 192 {
57+
if int(num-192) >= len(indexToChar) {
58+
return "", fmt.Errorf("invalid encoded byte: %d", num)
59+
}
60+
path[i+1] = indexToChar[num-192]
61+
} else if num >= 128 {
62+
path[i+1] = ','
63+
} else if num >= 64 {
64+
path[i+1] = '-'
65+
} else if num <= 9 {
66+
path[i+1] = '0' + num
67+
} else {
68+
return "", fmt.Errorf("invalid encoded byte: %d", num)
69+
}
70+
}
71+
return string(path), nil
72+
}

pathcodec_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package pathcodec
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestComprkessDecompress(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
input string
11+
expected string
12+
wantErr bool
13+
}{
14+
{"Valid path", "M257,455c-56,0-109-25-146-65-143-156,31-397,224-318,201,83,136,386-78,383z", "M257,455c-56,0-109-25-146-65-143-156,31-397,224-318,201,83,136,386-78,383z", false},
15+
{"Valid path", "M255,491h-182l-1-2-2-2,0-231,0-231,2-1,2-2h182,182l1,2,2,2v230,231l-2,2-2,2h-182z", "M255,491h-182l-1-2-2-2,0-231,0-231,2-1,2-2h182,182l1,2,2,2v230,231l-2,2-2,2h-182z", false},
16+
{"Valid path", "M256,330h-28l-2-2-2-2,1-2,0-2-13-119-14-119,0-4,0-4,3-3,2-2h56,55l2,2,2,2v2l1,2-14,123-14,123-2,3-3,2h-27z", "M256,330h-28l-2-2-2-2,1-2,0-2-13-119-14-119,0-4,0-4,3-3,2-2h56,55l2,2,2,2v2l1,2-14,123-14,123-2,3-3,2h-27z", false},
17+
{"Valid path", "M258,458h-211l-1-1-2-2,1-9,1-9,0-62,0-63,1-119,1-118,1-2,2-2,0-6,0-6,2-1,2-1h206,206l2,1,2,1v6,7h2l2,1,1,3v3l1,126,1,125v52,52,10l1,10-2,2-2,2h-210z", "M258,458h-211l-1-1-2-2,1-9,1-9,0-62,0-63,1-119,1-118,1-2,2-2,0-6,0-6,2-1,2-1h206,206l2,1,2,1v6,7h2l2,1,1,3v3l1,126,1,125v52,52,10l1,10-2,2-2,2h-210z", false},
18+
{"Valid path", "M134,465h-3l1-1,0-2,1-2,0-2,59-90,58-91v1h-69l-69,1,1-2,1-1,0-2,0-2,107-105,108-105,8-8,8-8,2,1h2l1,2v2l-39,81-40,82h71l71,1v2,2l-2,2-1,2-134,123-133,122h-3z", "M134,465h-3l1-1,0-2,1-2,0-2,59-90,58-91v1h-69l-69,1,1-2,1-1,0-2,0-2,107-105,108-105,8-8,8-8,2,1h2l1,2v2l-39,81-40,82h71l71,1v2,2l-2,2-1,2-134,123-133,122h-3z", false},
19+
{"Valid path", "M256,478c-108,0-111,14-112-98,0-26-6-173,0-184,2-3-10-32-2-40,5-5-3-84,9-102,6-10,18-17,30-18,30-3,136-7,161,2,8,3,15,8,20,15,16,20,7,89,7,115,0,79,0,163,0,245,0,15,4,34-6,47-21,30-75,18-106,18z", "M256,478c-108,0-111,14-112-98,0-26-6-173,0-184,2-3-10-32-2-40,5-5-3-84,9-102,6-10,18-17,30-18,30-3,136-7,161,2,8,3,15,8,20,15,16,20,7,89,7,115,0,79,0,163,0,245,0,15,4,34-6,47-21,30-75,18-106,18z", false},
20+
{"Valid path with letters", "MASTAVAz", "MASTAVAz", false},
21+
{"Invalid start character", "X1-3,5-7,9z", "", true},
22+
{"Invalid end character", "M1-3,5-7,9y", "", true},
23+
{"Invalid character in path", "M1-3,5-7,9$z", "", true},
24+
{"Empty path", "", "", true},
25+
}
26+
27+
for _, tt := range tests {
28+
t.Run(tt.name, func(t *testing.T) {
29+
encoded, err := Compress(tt.input)
30+
if (err != nil) != tt.wantErr {
31+
t.Errorf("compress() error = %v, wantErr %v", err, tt.wantErr)
32+
return
33+
}
34+
if err != nil {
35+
return
36+
}
37+
38+
decoded, err := Decompress(encoded)
39+
if (err != nil) != tt.wantErr {
40+
t.Errorf("decompress() error = %v, wantErr %v", err, tt.wantErr)
41+
return
42+
}
43+
if decoded != tt.expected {
44+
t.Errorf("decompress() = %v, expected %v", decoded, tt.expected)
45+
}
46+
})
47+
}
48+
}
49+
50+
func BenchmarkCompress(b *testing.B) {
51+
path := "M257,455c-56,0-109-25-146-65-143-156,31-397,224-318,201,83,136,386-78,383z"
52+
for i := 0; i < b.N; i++ {
53+
_, _ = Compress(path)
54+
}
55+
}
56+
57+
func BenchmarkDecompress(b *testing.B) {
58+
encoded := []byte{2, 5, 7, 128, 4, 5, 5, 220, 64, 5, 6, 128, 0, 64, 1, 0, 9, 64, 2, 5, 64, 1, 4, 6, 64, 6, 5, 64, 1, 4, 3, 64, 1, 5, 6, 128, 3, 1, 64, 3, 9, 7, 128, 2, 2, 4, 64, 3, 1, 8, 128, 2, 0, 1, 128, 8, 3, 128, 1, 3, 6, 128, 3, 8, 6, 64, 7, 8, 128, 3, 8, 3}
59+
for i := 0; i < b.N; i++ {
60+
_, _ = Decompress(encoded)
61+
}
62+
}

0 commit comments

Comments
 (0)