1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
|
// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package id
import (
"bytes"
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
)
var (
InvalidContentURI = errors.New("invalid Matrix content URI")
InputNotJSONString = errors.New("input doesn't look like a JSON string")
)
// ContentURIString is a string that's expected to be a Matrix content URI.
// It's useful for delaying the parsing of the content URI to move errors from the event content
// JSON parsing step to a later step where more appropriate errors can be produced.
type ContentURIString string
func (uriString ContentURIString) Parse() (ContentURI, error) {
return ParseContentURI(string(uriString))
}
func (uriString ContentURIString) ParseOrIgnore() ContentURI {
parsed, _ := ParseContentURI(string(uriString))
return parsed
}
// ContentURI represents a Matrix content URI.
// https://spec.matrix.org/v1.2/client-server-api/#matrix-content-mxc-uris
type ContentURI struct {
Homeserver string
FileID string
}
func MustParseContentURI(uri string) ContentURI {
parsed, err := ParseContentURI(uri)
if err != nil {
panic(err)
}
return parsed
}
// ParseContentURI parses a Matrix content URI.
func ParseContentURI(uri string) (parsed ContentURI, err error) {
if len(uri) == 0 {
return
} else if !strings.HasPrefix(uri, "mxc://") {
err = InvalidContentURI
} else if index := strings.IndexRune(uri[6:], '/'); index == -1 || index == len(uri)-7 {
err = InvalidContentURI
} else {
parsed.Homeserver = uri[6 : 6+index]
parsed.FileID = uri[6+index+1:]
}
return
}
var mxcBytes = []byte("mxc://")
func ParseContentURIBytes(uri []byte) (parsed ContentURI, err error) {
if len(uri) == 0 {
return
} else if !bytes.HasPrefix(uri, mxcBytes) {
err = InvalidContentURI
} else if index := bytes.IndexRune(uri[6:], '/'); index == -1 || index == len(uri)-7 {
err = InvalidContentURI
} else {
parsed.Homeserver = string(uri[6 : 6+index])
parsed.FileID = string(uri[6+index+1:])
}
return
}
func (uri *ContentURI) UnmarshalJSON(raw []byte) (err error) {
if string(raw) == "null" {
*uri = ContentURI{}
return nil
} else if len(raw) < 2 || raw[0] != '"' || raw[len(raw)-1] != '"' {
return InputNotJSONString
}
parsed, err := ParseContentURIBytes(raw[1 : len(raw)-1])
if err != nil {
return err
}
*uri = parsed
return nil
}
func (uri *ContentURI) MarshalJSON() ([]byte, error) {
if uri == nil || uri.IsEmpty() {
return []byte("null"), nil
}
return json.Marshal(uri.String())
}
func (uri *ContentURI) UnmarshalText(raw []byte) (err error) {
parsed, err := ParseContentURIBytes(raw)
if err != nil {
return err
}
*uri = parsed
return nil
}
func (uri ContentURI) MarshalText() ([]byte, error) {
return []byte(uri.String()), nil
}
func (uri *ContentURI) Scan(i interface{}) error {
var parsed ContentURI
var err error
switch value := i.(type) {
case nil:
// don't do anything, set uri to empty
case []byte:
parsed, err = ParseContentURIBytes(value)
case string:
parsed, err = ParseContentURI(value)
default:
return fmt.Errorf("invalid type %T for ContentURI.Scan", i)
}
if err != nil {
return err
}
*uri = parsed
return nil
}
func (uri *ContentURI) Value() (driver.Value, error) {
if uri == nil {
return nil, nil
}
return uri.String(), nil
}
func (uri ContentURI) String() string {
if uri.IsEmpty() {
return ""
}
return fmt.Sprintf("mxc://%s/%s", uri.Homeserver, uri.FileID)
}
func (uri ContentURI) CUString() ContentURIString {
return ContentURIString(uri.String())
}
func (uri ContentURI) IsEmpty() bool {
return len(uri.Homeserver) == 0 || len(uri.FileID) == 0
}
var simpleHomeserverRegex = regexp.MustCompile(`^[a-zA-Z0-9.:-]+$`)
func (uri ContentURI) IsValid() bool {
return IsValidMediaID(uri.FileID) && uri.Homeserver != "" && simpleHomeserverRegex.MatchString(uri.Homeserver)
}
func IsValidMediaID(mediaID string) bool {
if len(mediaID) == 0 {
return false
}
for _, char := range mediaID {
if (char < 'A' || char > 'Z') && (char < 'a' || char > 'z') && (char < '0' || char > '9') && char != '-' && char != '_' {
return false
}
}
return true
}
|