├── .github └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── urn.dot └── urn.png ├── examples_test.go ├── go.mod ├── go.sum ├── kind.go ├── lexicaleq_test.go ├── machine.go ├── machine.go.rl ├── machine_test.go ├── makefile ├── options.go ├── parsing_mode.go ├── performance_test.go ├── scim.go ├── scim └── schema │ ├── type.go │ └── type_test.go ├── scim_test.go ├── tables_test.go ├── tools ├── removecomments │ ├── go.mod │ ├── go.sum │ ├── removecomments.go │ └── removecomments_test.go └── snake2camel │ ├── go.mod │ ├── go.sum │ ├── snake2camel.go │ └── snake2camel_test.go ├── urn.go ├── urn8141.go ├── urn8141_test.go └── urn_test.go /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: testing 2 | 3 | on: 4 | push: 5 | branches: ['master'] 6 | pull_request: 7 | branches: ['master'] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | strategy: 15 | fail-fast: true 16 | matrix: 17 | os: [ubuntu-latest, macos-latest] 18 | go: [ 19 | '1.18', 20 | '1.19', 21 | '1.20', 22 | '1.21', 23 | ] 24 | include: 25 | # Set the minimum Go patch version for the given Go minor 26 | - go: '1.18' 27 | GO_VERSION: '~1.18.0' 28 | - go: '1.19' 29 | GO_VERSION: '~1.19.0' 30 | - go: '1.20' 31 | GO_VERSION: '~1.20.0' 32 | - go: '1.21' 33 | GO_VERSION: '~1.21.0' 34 | runs-on: ${{ matrix.os }} 35 | 36 | steps: 37 | - name: Set up Go ${{ matrix.go }} 38 | uses: actions/setup-go@v3 39 | with: 40 | go-version: ${{ matrix.GO_VERSION }} 41 | check-latest: true 42 | 43 | - name: Print environment 44 | id: vars 45 | run: | 46 | printf "Using Go at $(which go) (version $(go version))\n" 47 | printf "\n\nGo environment:\n\n" 48 | go env 49 | printf "\n\nSystem environment:\n\n" 50 | env 51 | 52 | - name: Cache the build cache 53 | uses: actions/cache@v3 54 | with: 55 | path: | 56 | ~/go/pkg/mod 57 | ~/.cache/go-build 58 | key: build-go-${{ matrix.go }}-${{ matrix.os }}-${{ hashFiles('**/go.sum') }} 59 | restore-keys: | 60 | build-go-${{ matrix.go }}-${{ matrix.os }} 61 | 62 | - name: Check out the source code 63 | uses: actions/checkout@v3 64 | 65 | - name: Run tests 66 | run: GO_ARGS="-v -race -covermode=atomic -coverprofile=coverage.out" make tests 67 | 68 | - name: Upload coverage 69 | uses: codecov/codecov-action@v3 70 | if: github.ref == 'refs/heads/master' 71 | with: 72 | token: ${{ secrets.CODECOV_TOKEN }} 73 | fail_ci_if_error: true 74 | verbose: true 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.dll 3 | *.so 4 | *.dylib 5 | 6 | *.test 7 | 8 | *.out 9 | *.txt 10 | 11 | vendor/ 12 | /removecomments 13 | /snake2camel -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Leonardo Di Donato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://img.shields.io/circleci/build/github/leodido/go-urn?style=for-the-badge)](https://app.circleci.com/pipelines/github/leodido/go-urn) [![Coverage](https://img.shields.io/codecov/c/github/leodido/go-urn.svg?style=for-the-badge)](https://codecov.io/gh/leodido/go-urn) [![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=for-the-badge)](https://godoc.org/github.com/leodido/go-urn) 2 | 3 | **A parser for URNs**. 4 | 5 | > As seen on [RFC 2141](https://datatracker.ietf.org/doc/html/rfc2141), [RFC 7643](https://datatracker.ietf.org/doc/html/rfc7643#section-10), and on [RFC 8141](https://datatracker.ietf.org/doc/html/rfc8141). 6 | 7 | [API documentation](https://godoc.org/github.com/leodido/go-urn). 8 | 9 | Starting with version 1.3 this library also supports [RFC 7643 SCIM URNs](https://datatracker.ietf.org/doc/html/rfc7643#section-10). 10 | 11 | Starting with version 1.4 this library also supports [RFC 8141 URNs (2017)](https://datatracker.ietf.org/doc/html/rfc8141). 12 | 13 | ## Installation 14 | 15 | ``` 16 | go get github.com/leodido/go-urn 17 | ``` 18 | 19 | ## Features 20 | 21 | 1. RFC 2141 URNs parsing (default) 22 | 2. RFC 8141 URNs parsing (supersedes RFC 2141) 23 | 3. RFC 7643 SCIM URNs parsing 24 | 4. Normalization as per RFCs 25 | 5. Lexical equivalence as per RFCs 26 | 6. Precise, fine-grained errors 27 | 28 | ## Performances 29 | 30 | This implementation results to be really fast. 31 | 32 | Usually below 400 ns on my machine[1](#mymachine). 33 | 34 | Notice it also performs, while parsing: 35 | 36 | 1. fine-grained and informative erroring 37 | 2. specific-string normalization 38 | 39 | ``` 40 | ok/00/urn:a:b______________________________________/-10 51372006 109.0 ns/op 275 B/op 3 allocs/op 41 | ok/01/URN:foo:a123,456_____________________________/-10 36024072 160.8 ns/op 296 B/op 6 allocs/op 42 | ok/02/urn:foo:a123%2C456___________________________/-10 31901007 188.4 ns/op 320 B/op 7 allocs/op 43 | ok/03/urn:ietf:params:scim:schemas:core:2.0:User___/-10 22736756 266.6 ns/op 376 B/op 6 allocs/op 44 | ok/04/urn:ietf:params:scim:schemas:extension:enterp/-10 18291859 335.2 ns/op 408 B/op 6 allocs/op 45 | ok/05/urn:ietf:params:scim:schemas:extension:enterp/-10 15283087 379.4 ns/op 440 B/op 6 allocs/op 46 | ok/06/urn:burnout:nss______________________________/-10 39407593 155.1 ns/op 288 B/op 6 allocs/op 47 | ok/07/urn:abcdefghilmnopqrstuvzabcdefghilm:x_______/-10 27832718 211.4 ns/op 307 B/op 4 allocs/op 48 | ok/08/urn:urnurnurn:urn____________________________/-10 33269596 168.1 ns/op 293 B/op 6 allocs/op 49 | ok/09/urn:ciao:!!*_________________________________/-10 41100675 148.8 ns/op 288 B/op 6 allocs/op 50 | ok/10/urn:ciao:=@__________________________________/-10 37214253 149.7 ns/op 284 B/op 6 allocs/op 51 | ok/11/urn:ciao:@!=%2C(xyz)+a,b.*@g=$_'_____________/-10 26534240 229.8 ns/op 336 B/op 7 allocs/op 52 | ok/12/URN:x:abc%1Dz%2F%3az_________________________/-10 28166396 211.8 ns/op 336 B/op 7 allocs/op 53 | no/13/URN:---xxx:x_________________________________/-10 23635159 255.6 ns/op 419 B/op 5 allocs/op 54 | no/14/urn::colon:nss_______________________________/-10 23594779 258.4 ns/op 419 B/op 5 allocs/op 55 | no/15/URN:@,:x_____________________________________/-10 23742535 261.5 ns/op 419 B/op 5 allocs/op 56 | no/16/URN:URN:NSS__________________________________/-10 27432714 223.3 ns/op 371 B/op 5 allocs/op 57 | no/17/urn:UrN:NSS__________________________________/-10 26922117 224.9 ns/op 371 B/op 5 allocs/op 58 | no/18/urn:a:%______________________________________/-10 24926733 224.6 ns/op 371 B/op 5 allocs/op 59 | no/19/urn:urn:NSS__________________________________/-10 27652641 220.7 ns/op 371 B/op 5 allocs/op 60 | ``` 61 | 62 | * [1]: Apple M1 Pro 63 | 64 | 65 | ## Example 66 | 67 | For more examples take a look at the [examples file](examples_test.go). 68 | 69 | 70 | ```go 71 | package main 72 | 73 | import ( 74 | "fmt" 75 | "github.com/leodido/go-urn" 76 | ) 77 | 78 | func main() { 79 | var uid = "URN:foo:a123,456" 80 | 81 | // Parse the input string as a RFC 2141 URN only 82 | u, e := urn.NewMachine().Parse(uid) 83 | if e != nil { 84 | fmt.Errorf(err) 85 | 86 | return 87 | } 88 | 89 | fmt.Println(u.ID) 90 | fmt.Println(u.SS) 91 | 92 | // Output: 93 | // foo 94 | // a123,456 95 | } 96 | ``` 97 | 98 | ```go 99 | package main 100 | 101 | import ( 102 | "fmt" 103 | "github.com/leodido/go-urn" 104 | ) 105 | 106 | func main() { 107 | var uid = "URN:foo:a123,456" 108 | 109 | // Parse the input string as a RFC 2141 URN only 110 | u, ok := urn.Parse([]byte(uid)) 111 | if !ok { 112 | panic("error parsing urn") 113 | } 114 | 115 | fmt.Println(u.ID) 116 | fmt.Println(u.SS) 117 | 118 | // Output: 119 | // foo 120 | // a123,456 121 | } 122 | ``` 123 | 124 | ```go 125 | package main 126 | 127 | import ( 128 | "fmt" 129 | "github.com/leodido/go-urn" 130 | ) 131 | 132 | func main() { 133 | input := "urn:ietf:params:scim:api:messages:2.0:ListResponse" 134 | 135 | // Parsing the input string as a RFC 7643 SCIM URN 136 | u, ok := urn.Parse([]byte(input), urn.WithParsingMode(urn.RFC7643Only)) 137 | if !ok { 138 | panic("error parsing urn") 139 | } 140 | 141 | fmt.Println(u.IsSCIM()) 142 | scim := u.SCIM() 143 | fmt.Println(scim.Type.String()) 144 | fmt.Println(scim.Name) 145 | fmt.Println(scim.Other) 146 | 147 | // Output: 148 | // true 149 | // api 150 | // messages 151 | // 2.0:ListResponse 152 | } 153 | ``` -------------------------------------------------------------------------------- /docs/urn.dot: -------------------------------------------------------------------------------- 1 | digraph urn { 2 | rankdir=LR; 3 | node [ shape = point ]; 4 | ENTRY; 5 | en_5; 6 | en_44; 7 | en_48; 8 | en_83; 9 | en_95; 10 | en_1; 11 | eof_1; 12 | eof_2; 13 | eof_3; 14 | eof_4; 15 | eof_5; 16 | eof_6; 17 | eof_7; 18 | eof_8; 19 | eof_9; 20 | eof_10; 21 | eof_11; 22 | eof_12; 23 | eof_13; 24 | eof_14; 25 | eof_15; 26 | eof_16; 27 | eof_17; 28 | eof_18; 29 | eof_19; 30 | eof_20; 31 | eof_21; 32 | eof_22; 33 | eof_23; 34 | eof_24; 35 | eof_25; 36 | eof_26; 37 | eof_27; 38 | eof_28; 39 | eof_29; 40 | eof_30; 41 | eof_31; 42 | eof_32; 43 | eof_33; 44 | eof_34; 45 | eof_35; 46 | eof_36; 47 | eof_37; 48 | eof_38; 49 | eof_39; 50 | eof_40; 51 | eof_41; 52 | eof_42; 53 | eof_43; 54 | eof_44; 55 | eof_45; 56 | eof_46; 57 | eof_47; 58 | eof_48; 59 | eof_49; 60 | eof_50; 61 | eof_51; 62 | eof_52; 63 | eof_53; 64 | eof_54; 65 | eof_55; 66 | eof_56; 67 | eof_57; 68 | eof_58; 69 | eof_59; 70 | eof_60; 71 | eof_61; 72 | eof_62; 73 | eof_63; 74 | eof_64; 75 | eof_65; 76 | eof_66; 77 | eof_67; 78 | eof_68; 79 | eof_69; 80 | eof_70; 81 | eof_71; 82 | eof_72; 83 | eof_73; 84 | eof_74; 85 | eof_75; 86 | eof_76; 87 | eof_77; 88 | eof_78; 89 | eof_79; 90 | eof_80; 91 | eof_81; 92 | eof_82; 93 | eof_83; 94 | eof_84; 95 | eof_85; 96 | eof_86; 97 | eof_88; 98 | eof_89; 99 | eof_91; 100 | eof_92; 101 | eof_93; 102 | node [ shape = circle, height = 0.2 ]; 103 | err_1 [ label=""]; 104 | err_2 [ label=""]; 105 | err_3 [ label=""]; 106 | err_4 [ label=""]; 107 | err_5 [ label=""]; 108 | err_6 [ label=""]; 109 | err_7 [ label=""]; 110 | err_8 [ label=""]; 111 | err_9 [ label=""]; 112 | err_10 [ label=""]; 113 | err_11 [ label=""]; 114 | err_12 [ label=""]; 115 | err_13 [ label=""]; 116 | err_14 [ label=""]; 117 | err_15 [ label=""]; 118 | err_16 [ label=""]; 119 | err_17 [ label=""]; 120 | err_18 [ label=""]; 121 | err_19 [ label=""]; 122 | err_20 [ label=""]; 123 | err_21 [ label=""]; 124 | err_22 [ label=""]; 125 | err_23 [ label=""]; 126 | err_24 [ label=""]; 127 | err_25 [ label=""]; 128 | err_26 [ label=""]; 129 | err_27 [ label=""]; 130 | err_28 [ label=""]; 131 | err_29 [ label=""]; 132 | err_30 [ label=""]; 133 | err_31 [ label=""]; 134 | err_32 [ label=""]; 135 | err_33 [ label=""]; 136 | err_34 [ label=""]; 137 | err_35 [ label=""]; 138 | err_36 [ label=""]; 139 | err_37 [ label=""]; 140 | err_38 [ label=""]; 141 | err_39 [ label=""]; 142 | err_40 [ label=""]; 143 | err_41 [ label=""]; 144 | err_42 [ label=""]; 145 | err_43 [ label=""]; 146 | err_44 [ label=""]; 147 | err_45 [ label=""]; 148 | err_46 [ label=""]; 149 | err_47 [ label=""]; 150 | err_48 [ label=""]; 151 | err_49 [ label=""]; 152 | err_50 [ label=""]; 153 | err_51 [ label=""]; 154 | err_52 [ label=""]; 155 | err_53 [ label=""]; 156 | err_54 [ label=""]; 157 | err_55 [ label=""]; 158 | err_56 [ label=""]; 159 | err_57 [ label=""]; 160 | err_58 [ label=""]; 161 | err_59 [ label=""]; 162 | err_60 [ label=""]; 163 | err_61 [ label=""]; 164 | err_62 [ label=""]; 165 | err_63 [ label=""]; 166 | err_64 [ label=""]; 167 | err_65 [ label=""]; 168 | err_66 [ label=""]; 169 | err_67 [ label=""]; 170 | err_68 [ label=""]; 171 | err_69 [ label=""]; 172 | err_70 [ label=""]; 173 | err_71 [ label=""]; 174 | err_72 [ label=""]; 175 | err_73 [ label=""]; 176 | err_74 [ label=""]; 177 | err_75 [ label=""]; 178 | err_76 [ label=""]; 179 | err_77 [ label=""]; 180 | err_78 [ label=""]; 181 | err_79 [ label=""]; 182 | err_80 [ label=""]; 183 | err_81 [ label=""]; 184 | err_82 [ label=""]; 185 | err_83 [ label=""]; 186 | err_84 [ label=""]; 187 | err_85 [ label=""]; 188 | err_86 [ label=""]; 189 | err_87 [ label=""]; 190 | err_88 [ label=""]; 191 | err_89 [ label=""]; 192 | err_90 [ label=""]; 193 | err_91 [ label=""]; 194 | err_92 [ label=""]; 195 | err_93 [ label=""]; 196 | err_94 [ label=""]; 197 | node [ fixedsize = true, height = 0.65, shape = doublecircle ]; 198 | 87; 199 | 88; 200 | 89; 201 | 90; 202 | 91; 203 | 92; 204 | 93; 205 | 94; 206 | 95; 207 | node [ shape = circle ]; 208 | 1 -> 2 [ label = "'U', 'u' / mark, throw_pre_urn_err" ]; 209 | 1 -> err_1 [ label = "DEF / err_pre, err_parse" ]; 210 | 2 -> 3 [ label = "'R', 'r'" ]; 211 | 2 -> err_2 [ label = "DEF / err_pre, err_parse" ]; 212 | 3 -> 4 [ label = "'N', 'n'" ]; 213 | 3 -> err_3 [ label = "DEF / err_pre, err_parse" ]; 214 | 4 -> 87 [ label = "':' / set_pre, 235:33" ]; 215 | 4 -> err_4 [ label = "DEF / err_pre, err_parse" ]; 216 | 5 -> 6 [ label = "'0'..'9', 'A'..'T', 'V'..'Z', 'a'..'t', 'v'..'z' / mark" ]; 217 | 5 -> 41 [ label = "'U', 'u' / mark, throw_pre_urn_err" ]; 218 | 5 -> err_5 [ label = "DEF / err_nid, err_pre, err_parse" ]; 219 | 6 -> 7 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 220 | 6 -> 38 [ label = "':' / set_nid" ]; 221 | 6 -> err_6 [ label = "DEF / err_nid, err_parse" ]; 222 | 7 -> 8 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 223 | 7 -> 38 [ label = "':' / set_nid" ]; 224 | 7 -> err_7 [ label = "DEF / err_nid, err_parse" ]; 225 | 8 -> 9 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 226 | 8 -> 38 [ label = "':' / set_nid" ]; 227 | 8 -> err_8 [ label = "DEF / err_nid, err_parse" ]; 228 | 9 -> 10 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 229 | 9 -> 38 [ label = "':' / set_nid" ]; 230 | 9 -> err_9 [ label = "DEF / err_nid, err_parse" ]; 231 | 10 -> 11 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 232 | 10 -> 38 [ label = "':' / set_nid" ]; 233 | 10 -> err_10 [ label = "DEF / err_nid, err_parse" ]; 234 | 11 -> 12 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 235 | 11 -> 38 [ label = "':' / set_nid" ]; 236 | 11 -> err_11 [ label = "DEF / err_nid, err_parse" ]; 237 | 12 -> 13 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 238 | 12 -> 38 [ label = "':' / set_nid" ]; 239 | 12 -> err_12 [ label = "DEF / err_nid, err_parse" ]; 240 | 13 -> 14 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 241 | 13 -> 38 [ label = "':' / set_nid" ]; 242 | 13 -> err_13 [ label = "DEF / err_nid, err_parse" ]; 243 | 14 -> 15 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 244 | 14 -> 38 [ label = "':' / set_nid" ]; 245 | 14 -> err_14 [ label = "DEF / err_nid, err_parse" ]; 246 | 15 -> 16 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 247 | 15 -> 38 [ label = "':' / set_nid" ]; 248 | 15 -> err_15 [ label = "DEF / err_nid, err_parse" ]; 249 | 16 -> 17 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 250 | 16 -> 38 [ label = "':' / set_nid" ]; 251 | 16 -> err_16 [ label = "DEF / err_nid, err_parse" ]; 252 | 17 -> 18 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 253 | 17 -> 38 [ label = "':' / set_nid" ]; 254 | 17 -> err_17 [ label = "DEF / err_nid, err_parse" ]; 255 | 18 -> 19 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 256 | 18 -> 38 [ label = "':' / set_nid" ]; 257 | 18 -> err_18 [ label = "DEF / err_nid, err_parse" ]; 258 | 19 -> 20 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 259 | 19 -> 38 [ label = "':' / set_nid" ]; 260 | 19 -> err_19 [ label = "DEF / err_nid, err_parse" ]; 261 | 20 -> 21 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 262 | 20 -> 38 [ label = "':' / set_nid" ]; 263 | 20 -> err_20 [ label = "DEF / err_nid, err_parse" ]; 264 | 21 -> 22 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 265 | 21 -> 38 [ label = "':' / set_nid" ]; 266 | 21 -> err_21 [ label = "DEF / err_nid, err_parse" ]; 267 | 22 -> 23 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 268 | 22 -> 38 [ label = "':' / set_nid" ]; 269 | 22 -> err_22 [ label = "DEF / err_nid, err_parse" ]; 270 | 23 -> 24 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 271 | 23 -> 38 [ label = "':' / set_nid" ]; 272 | 23 -> err_23 [ label = "DEF / err_nid, err_parse" ]; 273 | 24 -> 25 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 274 | 24 -> 38 [ label = "':' / set_nid" ]; 275 | 24 -> err_24 [ label = "DEF / err_nid, err_parse" ]; 276 | 25 -> 26 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 277 | 25 -> 38 [ label = "':' / set_nid" ]; 278 | 25 -> err_25 [ label = "DEF / err_nid, err_parse" ]; 279 | 26 -> 27 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 280 | 26 -> 38 [ label = "':' / set_nid" ]; 281 | 26 -> err_26 [ label = "DEF / err_nid, err_parse" ]; 282 | 27 -> 28 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 283 | 27 -> 38 [ label = "':' / set_nid" ]; 284 | 27 -> err_27 [ label = "DEF / err_nid, err_parse" ]; 285 | 28 -> 29 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 286 | 28 -> 38 [ label = "':' / set_nid" ]; 287 | 28 -> err_28 [ label = "DEF / err_nid, err_parse" ]; 288 | 29 -> 30 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 289 | 29 -> 38 [ label = "':' / set_nid" ]; 290 | 29 -> err_29 [ label = "DEF / err_nid, err_parse" ]; 291 | 30 -> 31 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 292 | 30 -> 38 [ label = "':' / set_nid" ]; 293 | 30 -> err_30 [ label = "DEF / err_nid, err_parse" ]; 294 | 31 -> 32 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 295 | 31 -> 38 [ label = "':' / set_nid" ]; 296 | 31 -> err_31 [ label = "DEF / err_nid, err_parse" ]; 297 | 32 -> 33 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 298 | 32 -> 38 [ label = "':' / set_nid" ]; 299 | 32 -> err_32 [ label = "DEF / err_nid, err_parse" ]; 300 | 33 -> 34 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 301 | 33 -> 38 [ label = "':' / set_nid" ]; 302 | 33 -> err_33 [ label = "DEF / err_nid, err_parse" ]; 303 | 34 -> 35 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 304 | 34 -> 38 [ label = "':' / set_nid" ]; 305 | 34 -> err_34 [ label = "DEF / err_nid, err_parse" ]; 306 | 35 -> 36 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 307 | 35 -> 38 [ label = "':' / set_nid" ]; 308 | 35 -> err_35 [ label = "DEF / err_nid, err_parse" ]; 309 | 36 -> 37 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 310 | 36 -> 38 [ label = "':' / set_nid" ]; 311 | 36 -> err_36 [ label = "DEF / err_nid, err_parse" ]; 312 | 37 -> 38 [ label = "':' / set_nid" ]; 313 | 37 -> err_37 [ label = "DEF / err_nid, err_parse" ]; 314 | 38 -> 88 [ label = "'!', '$', '''..'.', '0'..';', '=', '@'..'Z', '_', 'a'..'z' / mark" ]; 315 | 38 -> 39 [ label = "'%' / mark" ]; 316 | 38 -> err_38 [ label = "DEF / err_nss, err_parse" ]; 317 | 39 -> 40 [ label = "'0'..'9', 'a'..'z'" ]; 318 | 39 -> 40 [ label = "'A'..'Z' / tolower" ]; 319 | 39 -> err_39 [ label = "DEF / err_hex, err_nss, err_parse" ]; 320 | 40 -> 89 [ label = "'0'..'9', 'a'..'z'" ]; 321 | 40 -> 89 [ label = "'A'..'Z' / tolower" ]; 322 | 40 -> err_40 [ label = "DEF / err_hex, err_nss, err_parse" ]; 323 | 41 -> 7 [ label = "'-', '0'..'9', 'A'..'Q', 'S'..'Z', 'a'..'q', 's'..'z'" ]; 324 | 41 -> 38 [ label = "':' / set_nid" ]; 325 | 41 -> 42 [ label = "'R', 'r'" ]; 326 | 41 -> err_41 [ label = "DEF / err_nid, err_pre, err_parse" ]; 327 | 42 -> 8 [ label = "'-', '0'..'9', 'A'..'M', 'O'..'Z', 'a'..'m', 'o'..'z'" ]; 328 | 42 -> 38 [ label = "':' / set_nid" ]; 329 | 42 -> 43 [ label = "'N', 'n'" ]; 330 | 42 -> err_42 [ label = "DEF / err_nid, err_pre, err_parse" ]; 331 | 43 -> 9 [ label = "'-', '0'..'9', 'A'..'Z', 'a'..'z'" ]; 332 | 43 -> err_43 [ label = "DEF / err_nid, err_urn, err_parse" ]; 333 | 44 -> 45 [ label = "'U', 'u' / mark, throw_pre_urn_err" ]; 334 | 44 -> err_44 [ label = "DEF / err_pre" ]; 335 | 45 -> 46 [ label = "'R', 'r'" ]; 336 | 45 -> err_45 [ label = "DEF / err_pre" ]; 337 | 46 -> 47 [ label = "'N', 'n'" ]; 338 | 46 -> err_46 [ label = "DEF / err_pre" ]; 339 | 47 -> 90 [ label = "':' / set_pre, 215:36" ]; 340 | 47 -> err_47 [ label = "DEF / err_pre" ]; 341 | 48 -> 49 [ label = "'i' / mark" ]; 342 | 48 -> err_48 [ label = "DEF / err_scim_nid" ]; 343 | 49 -> 50 [ label = "'e'" ]; 344 | 49 -> err_49 [ label = "DEF / err_scim_nid" ]; 345 | 50 -> 51 [ label = "'t'" ]; 346 | 50 -> err_50 [ label = "DEF / err_scim_nid" ]; 347 | 51 -> 52 [ label = "'f'" ]; 348 | 51 -> err_51 [ label = "DEF / err_scim_nid" ]; 349 | 52 -> 53 [ label = "':'" ]; 350 | 52 -> err_52 [ label = "DEF / err_scim_nid" ]; 351 | 53 -> 54 [ label = "'p'" ]; 352 | 53 -> err_53 [ label = "DEF / err_scim_nid" ]; 353 | 54 -> 55 [ label = "'a'" ]; 354 | 54 -> err_54 [ label = "DEF / err_scim_nid" ]; 355 | 55 -> 56 [ label = "'r'" ]; 356 | 55 -> err_55 [ label = "DEF / err_scim_nid" ]; 357 | 56 -> 57 [ label = "'a'" ]; 358 | 56 -> err_56 [ label = "DEF / err_scim_nid" ]; 359 | 57 -> 58 [ label = "'m'" ]; 360 | 57 -> err_57 [ label = "DEF / err_scim_nid" ]; 361 | 58 -> 59 [ label = "'s'" ]; 362 | 58 -> err_58 [ label = "DEF / err_scim_nid" ]; 363 | 59 -> 60 [ label = "':'" ]; 364 | 59 -> err_59 [ label = "DEF / err_scim_nid" ]; 365 | 60 -> 61 [ label = "'s'" ]; 366 | 60 -> err_60 [ label = "DEF / err_scim_nid" ]; 367 | 61 -> 62 [ label = "'c'" ]; 368 | 61 -> err_61 [ label = "DEF / err_scim_nid" ]; 369 | 62 -> 63 [ label = "'i'" ]; 370 | 62 -> err_62 [ label = "DEF / err_scim_nid" ]; 371 | 63 -> 64 [ label = "'m'" ]; 372 | 63 -> err_63 [ label = "DEF / err_scim_nid" ]; 373 | 64 -> 65 [ label = "':' / set_nid, create_scim" ]; 374 | 64 -> err_64 [ label = "DEF / err_scim_nid" ]; 375 | 65 -> 66 [ label = "'a' / mark" ]; 376 | 65 -> 73 [ label = "'p' / mark" ]; 377 | 65 -> 77 [ label = "'s' / mark" ]; 378 | 65 -> err_65 [ label = "DEF / err_scim_type" ]; 379 | 66 -> 67 [ label = "'p'" ]; 380 | 66 -> err_66 [ label = "DEF / err_scim_type" ]; 381 | 67 -> 68 [ label = "'i'" ]; 382 | 67 -> err_67 [ label = "DEF / err_scim_type" ]; 383 | 68 -> 69 [ label = "':' / set_scim_type" ]; 384 | 68 -> err_68 [ label = "DEF / err_scim_type" ]; 385 | 69 -> 91 [ label = "'0'..'9', 'A'..'Z', 'a'..'z' / mark_scim_name" ]; 386 | 69 -> err_69 [ label = "DEF / err_scim_name" ]; 387 | 70 -> 92 [ label = "'!', '$', '''..'.', '0'..';', '=', '@'..'Z', '_', 'a'..'z' / mark_scim_other" ]; 388 | 70 -> 71 [ label = "'%' / mark_scim_other" ]; 389 | 70 -> err_70 [ label = "DEF / err_scim_other" ]; 390 | 71 -> 72 [ label = "'0'..'9', 'a'..'z'" ]; 391 | 71 -> 72 [ label = "'A'..'Z' / tolower" ]; 392 | 71 -> err_71 [ label = "DEF / err_hex, err_scim_other" ]; 393 | 72 -> 93 [ label = "'0'..'9', 'a'..'z'" ]; 394 | 72 -> 93 [ label = "'A'..'Z' / tolower" ]; 395 | 72 -> err_72 [ label = "DEF / err_hex, err_scim_other" ]; 396 | 73 -> 74 [ label = "'a'" ]; 397 | 73 -> err_73 [ label = "DEF / err_scim_type" ]; 398 | 74 -> 75 [ label = "'r'" ]; 399 | 74 -> err_74 [ label = "DEF / err_scim_type" ]; 400 | 75 -> 76 [ label = "'a'" ]; 401 | 75 -> err_75 [ label = "DEF / err_scim_type" ]; 402 | 76 -> 68 [ label = "'m'" ]; 403 | 76 -> err_76 [ label = "DEF / err_scim_type" ]; 404 | 77 -> 78 [ label = "'c'" ]; 405 | 77 -> err_77 [ label = "DEF / err_scim_type" ]; 406 | 78 -> 79 [ label = "'h'" ]; 407 | 78 -> err_78 [ label = "DEF / err_scim_type" ]; 408 | 79 -> 80 [ label = "'e'" ]; 409 | 79 -> err_79 [ label = "DEF / err_scim_type" ]; 410 | 80 -> 81 [ label = "'m'" ]; 411 | 80 -> err_80 [ label = "DEF / err_scim_type" ]; 412 | 81 -> 82 [ label = "'a'" ]; 413 | 81 -> err_81 [ label = "DEF / err_scim_type" ]; 414 | 82 -> 68 [ label = "'s'" ]; 415 | 82 -> err_82 [ label = "DEF / err_scim_type" ]; 416 | 83 -> 84 [ label = "'U', 'u' / mark, throw_pre_urn_err" ]; 417 | 83 -> err_83 [ label = "DEF / err_pre" ]; 418 | 84 -> 85 [ label = "'R', 'r'" ]; 419 | 84 -> err_84 [ label = "DEF / err_pre" ]; 420 | 85 -> 86 [ label = "'N', 'n'" ]; 421 | 85 -> err_85 [ label = "DEF / err_pre" ]; 422 | 86 -> 94 [ label = "':' / set_pre, 229:37" ]; 423 | 86 -> err_86 [ label = "DEF / err_pre" ]; 424 | 87 -> err_87 [ label = "DEF / err_pre, err_parse" ]; 425 | 88 -> 88 [ label = "'!', '$', '''..'.', '0'..';', '=', '@'..'Z', '_', 'a'..'z'" ]; 426 | 88 -> 39 [ label = "'%'" ]; 427 | 88 -> err_88 [ label = "DEF / err_nss, err_parse" ]; 428 | 89 -> 88 [ label = "'!', '$', '''..'.', '0'..';', '=', '@'..'Z', '_', 'a'..'z'" ]; 429 | 89 -> 39 [ label = "'%'" ]; 430 | 89 -> err_89 [ label = "DEF / err_hex, err_nss, err_parse" ]; 431 | 90 -> err_90 [ label = "DEF / err_pre" ]; 432 | 91 -> 91 [ label = "'0'..'9', 'A'..'Z', 'a'..'z'" ]; 433 | 91 -> 70 [ label = "':' / set_scim_name" ]; 434 | 91 -> err_91 [ label = "DEF / err_scim_name" ]; 435 | 92 -> 92 [ label = "'!', '$', '''..'.', '0'..';', '=', '@'..'Z', '_', 'a'..'z'" ]; 436 | 92 -> 71 [ label = "'%'" ]; 437 | 92 -> err_92 [ label = "DEF / err_scim_other" ]; 438 | 93 -> 92 [ label = "'!', '$', '''..'.', '0'..';', '=', '@'..'Z', '_', 'a'..'z'" ]; 439 | 93 -> 71 [ label = "'%'" ]; 440 | 93 -> err_93 [ label = "DEF / err_hex, err_scim_other" ]; 441 | 94 -> err_94 [ label = "DEF / err_pre" ]; 442 | 95 -> 95 [ label = "0..'\\t', '\\v'..'\\f', 14..255" ]; 443 | ENTRY -> 1 [ label = "IN" ]; 444 | en_5 -> 5 [ label = "urn" ]; 445 | en_44 -> 44 [ label = "urn_only" ]; 446 | en_48 -> 48 [ label = "scim" ]; 447 | en_83 -> 83 [ label = "scim_only" ]; 448 | en_95 -> 95 [ label = "fail" ]; 449 | en_1 -> 1 [ label = "main" ]; 450 | 1 -> eof_1 [ label = "EOF / err_pre, err_parse" ]; 451 | 2 -> eof_2 [ label = "EOF / err_pre, err_parse" ]; 452 | 3 -> eof_3 [ label = "EOF / err_pre, err_parse" ]; 453 | 4 -> eof_4 [ label = "EOF / err_pre, err_parse" ]; 454 | 5 -> eof_5 [ label = "EOF / err_nid, err_pre, err_parse" ]; 455 | 6 -> eof_6 [ label = "EOF / err_nid, err_parse" ]; 456 | 7 -> eof_7 [ label = "EOF / err_nid, err_parse" ]; 457 | 8 -> eof_8 [ label = "EOF / err_nid, err_parse" ]; 458 | 9 -> eof_9 [ label = "EOF / err_nid, err_parse" ]; 459 | 10 -> eof_10 [ label = "EOF / err_nid, err_parse" ]; 460 | 11 -> eof_11 [ label = "EOF / err_nid, err_parse" ]; 461 | 12 -> eof_12 [ label = "EOF / err_nid, err_parse" ]; 462 | 13 -> eof_13 [ label = "EOF / err_nid, err_parse" ]; 463 | 14 -> eof_14 [ label = "EOF / err_nid, err_parse" ]; 464 | 15 -> eof_15 [ label = "EOF / err_nid, err_parse" ]; 465 | 16 -> eof_16 [ label = "EOF / err_nid, err_parse" ]; 466 | 17 -> eof_17 [ label = "EOF / err_nid, err_parse" ]; 467 | 18 -> eof_18 [ label = "EOF / err_nid, err_parse" ]; 468 | 19 -> eof_19 [ label = "EOF / err_nid, err_parse" ]; 469 | 20 -> eof_20 [ label = "EOF / err_nid, err_parse" ]; 470 | 21 -> eof_21 [ label = "EOF / err_nid, err_parse" ]; 471 | 22 -> eof_22 [ label = "EOF / err_nid, err_parse" ]; 472 | 23 -> eof_23 [ label = "EOF / err_nid, err_parse" ]; 473 | 24 -> eof_24 [ label = "EOF / err_nid, err_parse" ]; 474 | 25 -> eof_25 [ label = "EOF / err_nid, err_parse" ]; 475 | 26 -> eof_26 [ label = "EOF / err_nid, err_parse" ]; 476 | 27 -> eof_27 [ label = "EOF / err_nid, err_parse" ]; 477 | 28 -> eof_28 [ label = "EOF / err_nid, err_parse" ]; 478 | 29 -> eof_29 [ label = "EOF / err_nid, err_parse" ]; 479 | 30 -> eof_30 [ label = "EOF / err_nid, err_parse" ]; 480 | 31 -> eof_31 [ label = "EOF / err_nid, err_parse" ]; 481 | 32 -> eof_32 [ label = "EOF / err_nid, err_parse" ]; 482 | 33 -> eof_33 [ label = "EOF / err_nid, err_parse" ]; 483 | 34 -> eof_34 [ label = "EOF / err_nid, err_parse" ]; 484 | 35 -> eof_35 [ label = "EOF / err_nid, err_parse" ]; 485 | 36 -> eof_36 [ label = "EOF / err_nid, err_parse" ]; 486 | 37 -> eof_37 [ label = "EOF / err_nid, err_parse" ]; 487 | 38 -> eof_38 [ label = "EOF / err_nss, err_parse" ]; 488 | 39 -> eof_39 [ label = "EOF / err_hex, err_nss, err_parse" ]; 489 | 40 -> eof_40 [ label = "EOF / err_hex, err_nss, err_parse" ]; 490 | 41 -> eof_41 [ label = "EOF / err_nid, err_pre, err_parse" ]; 491 | 42 -> eof_42 [ label = "EOF / err_nid, err_pre, err_parse" ]; 492 | 43 -> eof_43 [ label = "EOF / err_nid, err_urn, err_parse" ]; 493 | 44 -> eof_44 [ label = "EOF / err_pre" ]; 494 | 45 -> eof_45 [ label = "EOF / err_pre" ]; 495 | 46 -> eof_46 [ label = "EOF / err_pre" ]; 496 | 47 -> eof_47 [ label = "EOF / err_pre" ]; 497 | 48 -> eof_48 [ label = "EOF / err_scim_nid" ]; 498 | 49 -> eof_49 [ label = "EOF / err_scim_nid" ]; 499 | 50 -> eof_50 [ label = "EOF / err_scim_nid" ]; 500 | 51 -> eof_51 [ label = "EOF / err_scim_nid" ]; 501 | 52 -> eof_52 [ label = "EOF / err_scim_nid" ]; 502 | 53 -> eof_53 [ label = "EOF / err_scim_nid" ]; 503 | 54 -> eof_54 [ label = "EOF / err_scim_nid" ]; 504 | 55 -> eof_55 [ label = "EOF / err_scim_nid" ]; 505 | 56 -> eof_56 [ label = "EOF / err_scim_nid" ]; 506 | 57 -> eof_57 [ label = "EOF / err_scim_nid" ]; 507 | 58 -> eof_58 [ label = "EOF / err_scim_nid" ]; 508 | 59 -> eof_59 [ label = "EOF / err_scim_nid" ]; 509 | 60 -> eof_60 [ label = "EOF / err_scim_nid" ]; 510 | 61 -> eof_61 [ label = "EOF / err_scim_nid" ]; 511 | 62 -> eof_62 [ label = "EOF / err_scim_nid" ]; 512 | 63 -> eof_63 [ label = "EOF / err_scim_nid" ]; 513 | 64 -> eof_64 [ label = "EOF / err_scim_nid" ]; 514 | 65 -> eof_65 [ label = "EOF / err_scim_type" ]; 515 | 66 -> eof_66 [ label = "EOF / err_scim_type" ]; 516 | 67 -> eof_67 [ label = "EOF / err_scim_type" ]; 517 | 68 -> eof_68 [ label = "EOF / err_scim_type" ]; 518 | 69 -> eof_69 [ label = "EOF / err_scim_name" ]; 519 | 70 -> eof_70 [ label = "EOF / err_scim_other" ]; 520 | 71 -> eof_71 [ label = "EOF / err_hex, err_scim_other" ]; 521 | 72 -> eof_72 [ label = "EOF / err_hex, err_scim_other" ]; 522 | 73 -> eof_73 [ label = "EOF / err_scim_type" ]; 523 | 74 -> eof_74 [ label = "EOF / err_scim_type" ]; 524 | 75 -> eof_75 [ label = "EOF / err_scim_type" ]; 525 | 76 -> eof_76 [ label = "EOF / err_scim_type" ]; 526 | 77 -> eof_77 [ label = "EOF / err_scim_type" ]; 527 | 78 -> eof_78 [ label = "EOF / err_scim_type" ]; 528 | 79 -> eof_79 [ label = "EOF / err_scim_type" ]; 529 | 80 -> eof_80 [ label = "EOF / err_scim_type" ]; 530 | 81 -> eof_81 [ label = "EOF / err_scim_type" ]; 531 | 82 -> eof_82 [ label = "EOF / err_scim_type" ]; 532 | 83 -> eof_83 [ label = "EOF / err_pre" ]; 533 | 84 -> eof_84 [ label = "EOF / err_pre" ]; 534 | 85 -> eof_85 [ label = "EOF / err_pre" ]; 535 | 86 -> eof_86 [ label = "EOF / err_pre" ]; 536 | 88 -> eof_88 [ label = "EOF / set_nss, base_type" ]; 537 | 89 -> eof_89 [ label = "EOF / set_nss, base_type" ]; 538 | 91 -> eof_91 [ label = "EOF / set_scim_name, set_nss, scim_type" ]; 539 | 92 -> eof_92 [ label = "EOF / set_scim_other, set_nss, scim_type" ]; 540 | 93 -> eof_93 [ label = "EOF / set_scim_other, set_nss, scim_type" ]; 541 | } 542 | -------------------------------------------------------------------------------- /docs/urn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leodido/go-urn/d725923fe33ce69c89b9e2033d069099b498224f/docs/urn.png -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | package urn_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/leodido/go-urn" 7 | ) 8 | 9 | func ExampleParse() { 10 | var uid = "URN:foo:a123,456" 11 | 12 | if u, ok := urn.Parse([]byte(uid)); ok { 13 | fmt.Println(u.ID) 14 | fmt.Println(u.SS) 15 | fmt.Println(u.SCIM()) 16 | } 17 | 18 | // Output: foo 19 | // a123,456 20 | // 21 | } 22 | 23 | func ExampleURN_MarshalJSON() { 24 | var uid = "URN:foo:a123,456" 25 | 26 | if u, ok := urn.Parse([]byte(uid)); ok { 27 | json, err := u.MarshalJSON() 28 | if err != nil { 29 | panic("invalid urn") 30 | } 31 | fmt.Println(string(json)) 32 | } 33 | 34 | // Output: "URN:foo:a123,456" 35 | } 36 | 37 | func ExampleURN_Equal() { 38 | var uid1 = "URN:foo:a123,456" 39 | var uid2 = "URN:FOO:a123,456" 40 | 41 | u1, ok := urn.Parse([]byte(uid1)) 42 | if !ok { 43 | panic("invalid urn") 44 | } 45 | 46 | u2, ok := urn.Parse([]byte(uid2)) 47 | if !ok { 48 | panic("invalid urn") 49 | } 50 | 51 | if u1.Equal(u2) { 52 | fmt.Printf("%s equals %s", u1.String(), u2.String()) 53 | } 54 | 55 | // Output: URN:foo:a123,456 equals URN:FOO:a123,456 56 | } 57 | 58 | func ExampleParse_scim() { 59 | input := "urn:ietf:params:scim:api:messages:2.0:ListResponse" 60 | 61 | u, ok := urn.Parse([]byte(input), urn.WithParsingMode(urn.RFC7643Only)) 62 | if !ok { 63 | panic("invalid SCIM urn") 64 | } 65 | data, err := u.MarshalJSON() 66 | if err != nil { 67 | panic("couldn't marshal") 68 | } 69 | fmt.Println(string(data)) 70 | fmt.Println(u.IsSCIM()) 71 | scim := u.SCIM() 72 | fmt.Println(scim.Type.String()) 73 | fmt.Println(scim.Name) 74 | fmt.Println(scim.Other) 75 | 76 | // Output: 77 | // "urn:ietf:params:scim:api:messages:2.0:ListResponse" 78 | // true 79 | // api 80 | // messages 81 | // 2.0:ListResponse 82 | } 83 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/leodido/go-urn 2 | 3 | go 1.18 4 | 5 | require github.com/stretchr/testify v1.8.4 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 6 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /kind.go: -------------------------------------------------------------------------------- 1 | package urn 2 | 3 | type Kind int 4 | 5 | const ( 6 | NONE Kind = iota 7 | RFC2141 8 | RFC7643 9 | RFC8141 10 | ) 11 | -------------------------------------------------------------------------------- /lexicaleq_test.go: -------------------------------------------------------------------------------- 1 | package urn 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | type equivalenceTestCase struct { 11 | eq bool 12 | lx []byte 13 | rx []byte 14 | } 15 | 16 | var equivalenceTests = []equivalenceTestCase{ 17 | { 18 | true, 19 | []byte("urn:foo:a123%2C456"), 20 | []byte("URN:FOO:a123%2c456"), 21 | }, 22 | { 23 | true, 24 | []byte("urn:example:a123%2Cz456"), 25 | []byte("URN:EXAMPLE:a123%2cz456"), 26 | }, 27 | { 28 | true, 29 | []byte("urn:foo:AbC123%2C456"), 30 | []byte("URN:FOO:AbC123%2c456"), 31 | }, 32 | { 33 | true, 34 | []byte("urn:foo:AbC123%2C456%1f"), 35 | []byte("URN:FOO:AbC123%2c456%1f"), 36 | }, 37 | { 38 | true, 39 | []byte("URN:foo:a123,456"), 40 | []byte("urn:foo:a123,456"), 41 | }, 42 | { 43 | true, 44 | []byte("URN:foo:a123,456"), 45 | []byte("urn:FOO:a123,456"), 46 | }, 47 | { 48 | true, 49 | []byte("urn:foo:a123,456"), 50 | []byte("urn:FOO:a123,456"), 51 | }, 52 | { 53 | true, 54 | []byte("urn:ciao:%2E"), 55 | []byte("urn:ciao:%2e"), 56 | }, 57 | { 58 | false, 59 | []byte("urn:foo:A123,456"), 60 | []byte("URN:foo:a123,456"), 61 | }, 62 | { 63 | false, 64 | []byte("urn:foo:A123,456"), 65 | []byte("urn:foo:a123,456"), 66 | }, 67 | { 68 | false, 69 | []byte("urn:foo:A123,456"), 70 | []byte("urn:FOO:a123,456"), 71 | }, 72 | { 73 | false, 74 | []byte("urn:example:a123%2Cz456"), 75 | []byte("urn:example:a123,z456"), 76 | }, 77 | { 78 | false, 79 | []byte("urn:example:A123,z456"), 80 | []byte("urn:example:a123,Z456"), 81 | }, 82 | } 83 | 84 | var equivalenceTests8141 = []equivalenceTestCase{ 85 | { 86 | false, 87 | []byte("urn:example:a123,z456/bar"), 88 | []byte("urn:example:a123,z456/foo"), 89 | }, 90 | { 91 | true, 92 | []byte("urn:example:a123,z456?+abc"), 93 | []byte("urn:example:a123,z456?=xyz"), 94 | }, 95 | { 96 | true, 97 | []byte("urn:example:a123,z456#789"), 98 | []byte("urn:example:a123,z456?=xyz"), 99 | }, 100 | } 101 | 102 | func lexicalEqual(t *testing.T, ii int, tt equivalenceTestCase, os ...Option) { 103 | t.Helper() 104 | urnlx, oklx := Parse(tt.lx, os...) 105 | urnrx, okrx := Parse(tt.rx, os...) 106 | 107 | if oklx && okrx { 108 | assert.True(t, urnlx.Equal(urnlx)) 109 | assert.True(t, urnrx.Equal(urnrx)) 110 | 111 | if tt.eq { 112 | assert.True(t, urnlx.Equal(urnrx), ierror(ii)) 113 | assert.True(t, urnrx.Equal(urnlx), ierror(ii)) 114 | } else { 115 | assert.False(t, urnlx.Equal(urnrx), ierror(ii)) 116 | assert.False(t, urnrx.Equal(urnlx), ierror(ii)) 117 | } 118 | } else { 119 | t.Log("Something wrong in the testing table ...") 120 | } 121 | } 122 | 123 | func TestLexicalEquivalence(t *testing.T) { 124 | for ii, tt := range equivalenceTests { 125 | lexicalEqual(t, ii, tt, WithParsingMode(RFC2141Only)) 126 | } 127 | 128 | // The r-component, q-component, and f-component not taken into account for purposes of URN-equivalence 129 | // See [RFC8141#3.2](https://datatracker.ietf.org/doc/html/rfc8141#section-3.2) 130 | for ii, tt := range append(equivalenceTests, equivalenceTests8141...) { 131 | lexicalEqual(t, ii, tt, WithParsingMode(RFC8141Only)) 132 | } 133 | } 134 | 135 | func TestEqualNil(t *testing.T) { 136 | u, ok := Parse([]byte("urn:hello:world")) 137 | require.NotNil(t, u) 138 | require.True(t, ok) 139 | require.False(t, u.Equal(nil)) 140 | } 141 | -------------------------------------------------------------------------------- /machine.go.rl: -------------------------------------------------------------------------------- 1 | package urn 2 | 3 | import ( 4 | "fmt" 5 | 6 | scimschema "github.com/leodido/go-urn/scim/schema" 7 | ) 8 | 9 | var ( 10 | errPrefix = "expecting the prefix to be the \"urn\" string (whatever case) [col %d]" 11 | errIdentifier = "expecting the identifier to be string (1..31 alnum chars, also containing dashes but not at its beginning) [col %d]" 12 | errSpecificString = "expecting the specific string to be a string containing alnum, hex, or others ([()+,-.:=@;$_!*']) chars [col %d]" 13 | errNoUrnWithinID = "expecting the identifier to not contain the \"urn\" reserved string [col %d]" 14 | errHex = "expecting the percent encoded chars to be well-formed (%%alnum{2}) [col %d]" 15 | errSCIMNamespace = "expecing the SCIM namespace identifier (ietf:params:scim) [col %d]" 16 | errSCIMType = "expecting a correct SCIM type (schemas, api, param) [col %d]" 17 | errSCIMName = "expecting one or more alnum char in the SCIM name part [col %d]" 18 | errSCIMOther = "expecting a well-formed other SCIM part [col %d]" 19 | errSCIMOtherIncomplete = "expecting a not empty SCIM other part after colon [col %d]" 20 | err8141InformalID = "informal URN namespace must be in the form urn-[1-9][0-9] [col %d]" 21 | err8141SpecificString = "expecting the specific string to contain alnum, hex, or others ([~&()+,-.:=@;$_!*'] or [/?] not in first position) chars [col %d]" 22 | err8141Identifier = "expecting the indentifier to be a string with (length 2 to 32 chars) containing alnum (or dashes) not starting or ending with a dash [col %d]" 23 | err8141RComponentStart = "expecting only one r-component (starting with the ?+ sequence) [col %d]" 24 | err8141QComponentStart = "expecting only one q-component (starting with the ?= sequence) [col %d]" 25 | err8141MalformedRComp = "expecting a non-empty r-component containing alnum, hex, or others ([~&()+,-.:=@;$_!*'] or [/?] but not at its beginning) [col %d]" 26 | err8141MalformedQComp = "expecting a non-empty q-component containing alnum, hex, or others ([~&()+,-.:=@;$_!*'] or [/?] but not at its beginning) [col %d]" 27 | ) 28 | 29 | %%{ 30 | machine urn; 31 | 32 | # unsigned alphabet 33 | alphtype uint8; 34 | 35 | action mark { 36 | m.pb = m.p 37 | } 38 | 39 | action tolower { 40 | // List of positions in the buffer to later lowercase 41 | output.tolower = append(output.tolower, m.p - m.pb) 42 | } 43 | 44 | action set_pre { 45 | output.prefix = string(m.text()) 46 | } 47 | 48 | action throw_pre_urn_err { 49 | if m.parsingMode != RFC8141Only { 50 | // Throw an error when: 51 | // - we are entering here matching the the prefix in the namespace identifier part 52 | // - looking ahead (3 chars) we find a colon 53 | if pos := m.p + 3; pos < m.pe && m.data[pos] == 58 && output.prefix != "" { 54 | m.err = fmt.Errorf(errNoUrnWithinID, pos) 55 | fhold; 56 | fgoto fail; 57 | } 58 | } 59 | } 60 | 61 | action set_nid { 62 | output.ID = string(m.text()) 63 | } 64 | 65 | action set_nss { 66 | output.SS = string(m.text()) 67 | // Iterate upper letters lowering them 68 | for _, i := range output.tolower { 69 | m.data[m.pb+i] = m.data[m.pb+i] + 32 70 | } 71 | output.norm = string(m.text()) 72 | // Revert the buffer to the original 73 | for _, i := range output.tolower { 74 | m.data[m.pb+i] = m.data[m.pb+i] - 32 75 | } 76 | } 77 | 78 | action err_pre { 79 | m.err = fmt.Errorf(errPrefix, m.p) 80 | fhold; 81 | fgoto fail; 82 | } 83 | 84 | action err_nid { 85 | m.err = fmt.Errorf(errIdentifier, m.p) 86 | fhold; 87 | fgoto fail; 88 | } 89 | 90 | action err_nss { 91 | m.err = fmt.Errorf(errSpecificString, m.p) 92 | fhold; 93 | fgoto fail; 94 | } 95 | 96 | action err_urn { 97 | m.err = fmt.Errorf(errNoUrnWithinID, m.p) 98 | fhold; 99 | fgoto fail; 100 | } 101 | 102 | action err_hex { 103 | if m.parsingMode == RFC2141Only || m.parsingMode == RFC8141Only { 104 | m.err = fmt.Errorf(errHex, m.p) 105 | fhold; 106 | fgoto fail; 107 | } 108 | } 109 | 110 | action base_type { 111 | output.kind = RFC2141; 112 | } 113 | 114 | pre = ([uU] @err(err_pre) [rR] @err(err_pre) [nN] @err(err_pre)) >mark >throw_pre_urn_err %set_pre; 115 | 116 | nid = (alnum >mark (alnum | '-'){0,31}) $err(err_nid) %set_nid; 117 | 118 | hex = '%' (digit | lower | upper >tolower){2} $err(err_hex); 119 | 120 | sss = (alnum | [()+,\-.:=@;$_!*']); 121 | 122 | nss = (sss | hex)+ $err(err_nss); 123 | 124 | nid_not_urn = (nid - pre %err(err_urn)); 125 | 126 | urn = pre ':' @err(err_pre) (nid_not_urn ':' nss >mark %set_nss) %eof(base_type); 127 | 128 | ### SCIM BEG 129 | 130 | action err_scim_nid { 131 | m.err = fmt.Errorf(errSCIMNamespace, m.p) 132 | fhold; 133 | fgoto fail; 134 | } 135 | 136 | action err_scim_type { 137 | m.err = fmt.Errorf(errSCIMType, m.p) 138 | fhold; 139 | fgoto fail; 140 | } 141 | 142 | action err_scim_name { 143 | m.err = fmt.Errorf(errSCIMName, m.p) 144 | fhold; 145 | fgoto fail; 146 | } 147 | 148 | action err_scim_other { 149 | if m.p == m.pe { 150 | m.err = fmt.Errorf(errSCIMOtherIncomplete, m.p-1) 151 | } else { 152 | m.err = fmt.Errorf(errSCIMOther, m.p) 153 | } 154 | fhold; 155 | fgoto fail; 156 | } 157 | 158 | action scim_type { 159 | output.kind = RFC7643; 160 | } 161 | 162 | action create_scim { 163 | output.scim = &SCIM{}; 164 | } 165 | 166 | action set_scim_type { 167 | output.scim.Type = scimschema.TypeFromString(string(m.text())) 168 | } 169 | 170 | action mark_scim_name { 171 | output.scim.pos = m.p 172 | } 173 | 174 | action set_scim_name { 175 | output.scim.Name = string(m.data[output.scim.pos:m.p]) 176 | } 177 | 178 | action mark_scim_other { 179 | output.scim.pos = m.p 180 | } 181 | 182 | action set_scim_other { 183 | output.scim.Other = string(m.data[output.scim.pos:m.p]) 184 | } 185 | 186 | scim_nid = 'ietf:params:scim' >mark %set_nid %create_scim $err(err_scim_nid); 187 | 188 | scim_other = ':' (sss | hex)+ >mark_scim_other %set_scim_other $err(err_scim_other); 189 | 190 | scim_name = (alnum)+ >mark_scim_name %set_scim_name $err(err_scim_name); 191 | 192 | scim_type = ('schemas' | 'api' | 'param') >mark %set_scim_type $err(err_scim_type); 193 | 194 | scim_only := pre ':' @err(err_pre) (scim_nid ':' scim_type ':' scim_name scim_other? %set_nss) %eof(scim_type); 195 | 196 | ### SCIM END 197 | 198 | ### 8141 BEG 199 | 200 | action err_nss_8141 { 201 | m.err = fmt.Errorf(err8141SpecificString, m.p) 202 | fhold; 203 | fgoto fail; 204 | } 205 | 206 | action err_nid_8141 { 207 | m.err = fmt.Errorf(err8141Identifier, m.p) 208 | fhold; 209 | fgoto fail; 210 | } 211 | 212 | action rfc8141_type { 213 | output.kind = RFC8141; 214 | } 215 | 216 | action set_r_component { 217 | output.rComponent = string(m.text()) 218 | } 219 | 220 | action set_q_component { 221 | output.qComponent = string(m.text()) 222 | } 223 | 224 | action set_f_component { 225 | output.fComponent = string(m.text()) 226 | } 227 | 228 | action informal_nid_match { 229 | fhold; 230 | m.err = fmt.Errorf(err8141InformalID, m.p); 231 | fgoto fail; 232 | } 233 | 234 | action mark_r_start { 235 | if output.rStart { 236 | m.err = fmt.Errorf(err8141RComponentStart, m.p) 237 | fhold; 238 | fgoto fail; 239 | } 240 | output.rStart = true 241 | } 242 | 243 | action mark_q_start { 244 | if output.qStart { 245 | m.err = fmt.Errorf(err8141QComponentStart, m.p) 246 | fhold; 247 | fgoto fail; 248 | } 249 | output.qStart = true 250 | } 251 | 252 | action err_malformed_r_component { 253 | m.err = fmt.Errorf(err8141MalformedRComp, m.p) 254 | fhold; 255 | fgoto fail; 256 | } 257 | 258 | action err_malformed_q_component { 259 | m.err = fmt.Errorf(err8141MalformedQComp, m.p) 260 | fhold; 261 | fgoto fail; 262 | } 263 | 264 | pchar = (sss | '~' | '&' | hex); 265 | 266 | component = pchar (pchar | '/' | '?')*; 267 | 268 | r_start = ('?+') %mark_r_start; 269 | 270 | r_component = r_start <: (r_start | component)+ $err(err_malformed_r_component) >mark %set_r_component; 271 | 272 | q_start = ('?=') %mark_q_start; 273 | 274 | q_component = q_start <: (q_start | component)+ $err(err_malformed_q_component) >mark %set_q_component; 275 | 276 | rq_components = (r_component :>> q_component? | q_component); 277 | 278 | fragment = (pchar | '/' | '?')*; 279 | 280 | f_component = '#' fragment >mark %set_f_component; 281 | 282 | nss_rfc8141 = (pchar >mark (pchar | '/')*) $err(err_nss_8141) %set_nss; 283 | 284 | nid_rfc8141 = (alnum >mark (alnum | '-'){0,30} alnum) $err(err_nid_8141) %set_nid; 285 | 286 | informal_id = pre ('-' [a-zA-z0] %to(informal_nid_match)); 287 | 288 | nid_rfc8141_not_urn = (nid_rfc8141 - informal_id?); 289 | 290 | rfc8141_only := pre ':' @err(err_pre) nid_rfc8141_not_urn ':' nss_rfc8141 rq_components? f_component? %eof(rfc8141_type); 291 | 292 | ### 8141 END 293 | 294 | fail := (any - [\n\r])* @err{ fgoto main; }; 295 | 296 | main := urn; 297 | 298 | }%% 299 | 300 | %% write data noerror noprefix; 301 | 302 | // Machine is the interface representing the FSM 303 | type Machine interface { 304 | Error() error 305 | Parse(input []byte) (*URN, error) 306 | WithParsingMode(ParsingMode) 307 | } 308 | 309 | type machine struct { 310 | data []byte 311 | cs int 312 | p, pe, eof, pb int 313 | err error 314 | startParsingAt int 315 | parsingMode ParsingMode 316 | parsingModeSet bool 317 | } 318 | 319 | // NewMachine creates a new FSM able to parse RFC 2141 strings. 320 | func NewMachine(options ...Option) Machine { 321 | m := &machine{ 322 | parsingModeSet: false, 323 | } 324 | 325 | for _, o := range options { 326 | o(m) 327 | } 328 | // Set default parsing mode 329 | if !m.parsingModeSet { 330 | m.WithParsingMode(DefaultParsingMode) 331 | } 332 | 333 | %% access m.; 334 | %% variable p m.p; 335 | %% variable pe m.pe; 336 | %% variable eof m.eof; 337 | %% variable data m.data; 338 | 339 | return m 340 | } 341 | 342 | // Err returns the error that occurred on the last call to Parse. 343 | // 344 | // If the result is nil, then the line was parsed successfully. 345 | func (m *machine) Error() error { 346 | return m.err 347 | } 348 | 349 | func (m *machine) text() []byte { 350 | return m.data[m.pb:m.p] 351 | } 352 | 353 | // Parse parses the input byte array as a RFC 2141 or RFC7643 string. 354 | func (m *machine) Parse(input []byte) (*URN, error) { 355 | m.data = input 356 | m.p = 0 357 | m.pb = 0 358 | m.pe = len(input) 359 | m.eof = len(input) 360 | m.err = nil 361 | m.cs = m.startParsingAt 362 | output := &URN{ 363 | tolower: []int{}, 364 | } 365 | 366 | %% write exec; 367 | 368 | if m.cs < first_final || m.cs == en_fail { 369 | return nil, m.err 370 | } 371 | 372 | return output, nil 373 | } 374 | 375 | func (m *machine) WithParsingMode(x ParsingMode) { 376 | m.parsingMode = x 377 | switch m.parsingMode { 378 | case RFC2141Only: 379 | m.startParsingAt = en_main 380 | case RFC8141Only: 381 | m.startParsingAt = en_rfc8141_only 382 | case RFC7643Only: 383 | m.startParsingAt = en_scim_only 384 | } 385 | m.parsingModeSet = true 386 | } -------------------------------------------------------------------------------- /machine_test.go: -------------------------------------------------------------------------------- 1 | package urn 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestDefaultParsingMode(t *testing.T) { 11 | m := NewMachine() 12 | require.NotNil(t, m) 13 | require.IsType(t, &machine{}, m) 14 | require.Equal(t, DefaultParsingMode, m.(*machine).parsingMode) 15 | } 16 | 17 | func exec(t *testing.T, testCases []testCase, mode ParsingMode) { 18 | t.Helper() 19 | m := NewMachine(WithParsingMode(mode)) 20 | for ii, tt := range testCases { 21 | urn, err := m.Parse([]byte(tt.in)) 22 | ok := err == nil 23 | 24 | if ok { 25 | assert.True(t, tt.ok, herror(ii, tt)) 26 | assert.Equal(t, tt.obj.prefix, urn.prefix, herror(ii, tt)) 27 | assert.Equal(t, tt.obj.ID, urn.ID, herror(ii, tt)) 28 | assert.Equal(t, tt.obj.SS, urn.SS, herror(ii, tt)) 29 | assert.Equal(t, tt.str, urn.String(), herror(ii, tt)) 30 | assert.Equal(t, tt.norm, urn.Normalize().String(), herror(ii, tt)) 31 | if mode == RFC8141Only { 32 | assert.Equal(t, tt.obj.rComponent, urn.rComponent, herror(ii, tt)) 33 | assert.Equal(t, tt.obj.rComponent, urn.rComponent, herror(ii, tt)) 34 | assert.Equal(t, tt.obj.rComponent, urn.rComponent, herror(ii, tt)) 35 | assert.Equal(t, urn.RFC(), RFC8141, herror(ii, tt)) 36 | } 37 | if mode == RFC7643Only { 38 | assert.Equal(t, tt.isSCIM, urn.IsSCIM(), herror(ii, tt)) 39 | } 40 | } else { 41 | assert.False(t, tt.ok, herror(ii, tt)) 42 | assert.Empty(t, urn, herror(ii, tt)) 43 | assert.Equal(t, tt.estr, err.Error(), herror(ii, tt)) 44 | assert.Equal(t, tt.estr, m.Error().Error(), herror(ii, tt)) 45 | } 46 | } 47 | } 48 | 49 | func TestParseDefaultMode(t *testing.T) { 50 | require.Equal(t, RFC2141Only, DefaultParsingMode) 51 | exec(t, urn2141OnlyTestCases, DefaultParsingMode) 52 | } 53 | 54 | func TestParse2141Only(t *testing.T) { 55 | exec(t, urn2141OnlyTestCases, RFC2141Only) 56 | } 57 | 58 | func TestParseUrnLex2141Only(t *testing.T) { 59 | exec(t, urnlexTestCases, RFC2141Only) 60 | } 61 | 62 | func TestSCIMOnly(t *testing.T) { 63 | exec(t, scimOnlyTestCases, RFC7643Only) 64 | } 65 | 66 | func TestParse8141Only(t *testing.T) { 67 | exec(t, rfc8141TestCases, RFC8141Only) 68 | } 69 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | RAGEL := ragel 3 | GOFMT := go fmt 4 | 5 | export GO_TEST=env GOTRACEBACK=all go test $(GO_ARGS) 6 | 7 | .PHONY: build 8 | build: machine.go 9 | 10 | .PHONY: clean 11 | clean: 12 | @rm -rf docs 13 | @rm -f machine.go 14 | 15 | .PHONY: images 16 | images: docs/urn.png 17 | 18 | .PHONY: snake2camel 19 | snake2camel: 20 | @cd ./tools/snake2camel; go build -o ../../snake2camel . 21 | 22 | .PHONY: removecomments 23 | removecomments: 24 | @cd ./tools/removecomments; go build -o ../../removecomments . 25 | 26 | machine.go: machine.go.rl 27 | 28 | machine.go: snake2camel 29 | 30 | machine.go: removecomments 31 | 32 | machine.go: 33 | $(RAGEL) -Z -G1 -e -o $@ $< 34 | @./removecomments $@ 35 | @./snake2camel $@ 36 | $(GOFMT) $@ 37 | 38 | docs/urn.dot: machine.go.rl 39 | @mkdir -p docs 40 | $(RAGEL) -Z -e -Vp $< -o $@ 41 | 42 | docs/urn.png: docs/urn.dot 43 | dot $< -Tpng -o $@ 44 | 45 | .PHONY: bench 46 | bench: *_test.go machine.go 47 | go test -bench=. -benchmem -benchtime=5s ./... 48 | 49 | .PHONY: tests 50 | tests: *_test.go 51 | $(GO_TEST) ./... 52 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package urn 2 | 3 | type Option func(Machine) 4 | 5 | func WithParsingMode(mode ParsingMode) Option { 6 | return func(m Machine) { 7 | m.WithParsingMode(mode) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /parsing_mode.go: -------------------------------------------------------------------------------- 1 | package urn 2 | 3 | type ParsingMode int 4 | 5 | const ( 6 | Default ParsingMode = iota 7 | RFC2141Only 8 | RFC7643Only 9 | RFC8141Only 10 | ) 11 | 12 | const DefaultParsingMode = RFC2141Only 13 | -------------------------------------------------------------------------------- /performance_test.go: -------------------------------------------------------------------------------- 1 | package urn 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | var benchs = []testCase{ 9 | urn2141OnlyTestCases[14], 10 | urn2141OnlyTestCases[2], 11 | urn2141OnlyTestCases[6], 12 | urn2141OnlyTestCases[10], 13 | urn2141OnlyTestCases[11], 14 | urn2141OnlyTestCases[13], 15 | urn2141OnlyTestCases[20], 16 | urn2141OnlyTestCases[23], 17 | urn2141OnlyTestCases[33], 18 | urn2141OnlyTestCases[45], 19 | urn2141OnlyTestCases[47], 20 | urn2141OnlyTestCases[48], 21 | urn2141OnlyTestCases[50], 22 | urn2141OnlyTestCases[52], 23 | urn2141OnlyTestCases[53], 24 | urn2141OnlyTestCases[57], 25 | urn2141OnlyTestCases[62], 26 | urn2141OnlyTestCases[63], 27 | urn2141OnlyTestCases[67], 28 | urn2141OnlyTestCases[60], 29 | } 30 | 31 | // This is here to avoid compiler optimizations that 32 | // could remove the actual call we are benchmarking 33 | // during benchmarks 34 | var benchParseResult *URN 35 | 36 | func BenchmarkParse(b *testing.B) { 37 | for ii, tt := range benchs { 38 | tt := tt 39 | outcome := (map[bool]string{true: "ok", false: "no"})[tt.ok] 40 | b.Run( 41 | fmt.Sprintf("%s/%02d/%s/", outcome, ii, rxpad(string(tt.in), 45)), 42 | func(b *testing.B) { 43 | for i := 0; i < b.N; i++ { 44 | benchParseResult, _ = Parse(tt.in) 45 | } 46 | }, 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /scim.go: -------------------------------------------------------------------------------- 1 | package urn 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | scimschema "github.com/leodido/go-urn/scim/schema" 8 | ) 9 | 10 | const errInvalidSCIMURN = "invalid SCIM URN: %s" 11 | 12 | type SCIM struct { 13 | Type scimschema.Type 14 | Name string 15 | Other string 16 | pos int 17 | } 18 | 19 | func (s SCIM) MarshalJSON() ([]byte, error) { 20 | return json.Marshal(s.String()) 21 | } 22 | 23 | func (s *SCIM) UnmarshalJSON(bytes []byte) error { 24 | var str string 25 | if err := json.Unmarshal(bytes, &str); err != nil { 26 | return err 27 | } 28 | // Parse as SCIM 29 | value, ok := Parse([]byte(str), WithParsingMode(RFC7643Only)) 30 | if !ok { 31 | return fmt.Errorf(errInvalidSCIMURN, str) 32 | } 33 | if value.RFC() != RFC7643 { 34 | return fmt.Errorf(errInvalidSCIMURN, str) 35 | } 36 | *s = *value.SCIM() 37 | 38 | return nil 39 | } 40 | 41 | func (s *SCIM) String() string { 42 | ret := fmt.Sprintf("urn:ietf:params:scim:%s:%s", s.Type.String(), s.Name) 43 | if s.Other != "" { 44 | ret += fmt.Sprintf(":%s", s.Other) 45 | } 46 | 47 | return ret 48 | } 49 | -------------------------------------------------------------------------------- /scim/schema/type.go: -------------------------------------------------------------------------------- 1 | package scimschema 2 | 3 | type Type int 4 | 5 | const ( 6 | Unsupported Type = iota 7 | Schemas 8 | API 9 | Param 10 | ) 11 | 12 | func (t Type) String() string { 13 | switch t { 14 | case Schemas: 15 | return "schemas" 16 | case API: 17 | return "api" 18 | case Param: 19 | return "param" 20 | } 21 | 22 | return "" 23 | } 24 | 25 | func TypeFromString(input string) Type { 26 | switch input { 27 | case "schemas": 28 | return Schemas 29 | case "api": 30 | return API 31 | case "param": 32 | return Param 33 | } 34 | 35 | return Unsupported 36 | } 37 | -------------------------------------------------------------------------------- /scim/schema/type_test.go: -------------------------------------------------------------------------------- 1 | package scimschema 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestTypeFromString(t *testing.T) { 10 | uns := TypeFromString("wrong") 11 | require.Equal(t, Unsupported, uns) 12 | require.Empty(t, uns.String()) 13 | 14 | schemas := TypeFromString("schemas") 15 | require.Equal(t, Schemas, schemas) 16 | require.Equal(t, "schemas", schemas.String()) 17 | 18 | api := TypeFromString("api") 19 | require.Equal(t, API, api) 20 | require.Equal(t, "api", api.String()) 21 | 22 | param := TypeFromString("param") 23 | require.Equal(t, Param, param) 24 | require.Equal(t, "param", param.String()) 25 | } 26 | -------------------------------------------------------------------------------- /scim_test.go: -------------------------------------------------------------------------------- 1 | package urn 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | scimschema "github.com/leodido/go-urn/scim/schema" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestSCIMJSONMarshaling(t *testing.T) { 14 | t.Run("roundtrip", func(t *testing.T) { 15 | // Marshal 16 | exp := SCIM{Type: scimschema.Schemas, Name: "core", Other: "extension:enterprise:2.0:User"} 17 | mar, err := json.Marshal(exp) 18 | require.NoError(t, err) 19 | require.Equal(t, `"urn:ietf:params:scim:schemas:core:extension:enterprise:2.0:User"`, string(mar)) 20 | 21 | // Unmarshal 22 | var act SCIM 23 | err = json.Unmarshal(mar, &act) 24 | require.NoError(t, err) 25 | exp.pos = 34 26 | require.Equal(t, exp, act) 27 | }) 28 | 29 | t.Run("unmarshal", func(t *testing.T) { 30 | exp := `urn:ietf:params:scim:schemas:extension:enterprise:2.0:User` 31 | var got SCIM 32 | err := json.Unmarshal([]byte(fmt.Sprintf(`"%s"`, exp)), &got) 33 | if !assert.NoError(t, err) { 34 | return 35 | } 36 | assert.Equal(t, exp, got.String()) 37 | assert.Equal(t, SCIM{ 38 | Type: scimschema.Schemas, 39 | Name: "extension", 40 | Other: "enterprise:2.0:User", 41 | pos: 39, 42 | }, got) 43 | }) 44 | 45 | t.Run("unmarshal without the part", func(t *testing.T) { 46 | exp := `urn:ietf:params:scim:schemas:core` 47 | var got SCIM 48 | err := json.Unmarshal([]byte(fmt.Sprintf(`"%s"`, exp)), &got) 49 | if !assert.NoError(t, err) { 50 | return 51 | } 52 | assert.Equal(t, exp, got.String()) 53 | assert.Equal(t, SCIM{ 54 | Type: scimschema.Schemas, 55 | Name: "core", 56 | pos: 29, 57 | }, got) 58 | }) 59 | 60 | t.Run("invalid URN", func(t *testing.T) { 61 | var actual SCIM 62 | err := json.Unmarshal([]byte(`"not a URN"`), &actual) 63 | assert.EqualError(t, err, "invalid SCIM URN: not a URN") 64 | }) 65 | 66 | t.Run("empty", func(t *testing.T) { 67 | var actual SCIM 68 | err := actual.UnmarshalJSON(nil) 69 | assert.EqualError(t, err, "unexpected end of JSON input") 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /tables_test.go: -------------------------------------------------------------------------------- 1 | package urn 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | func ierror(index int) string { 10 | return "Test case num. " + strconv.Itoa(index+1) 11 | } 12 | 13 | func herror(index int, test testCase) string { 14 | return ierror(index) + ", input \"" + string(test.in) + "\"" 15 | } 16 | 17 | func rxpad(str string, lim int) string { 18 | str = str + strings.Repeat(" ", lim) 19 | return str[:lim] 20 | } 21 | 22 | type testCase struct { 23 | in []byte // the input 24 | ok bool // whether it is valid or not 25 | obj *URN // a pointer to the resulting urn.URN instance 26 | str string // string representation 27 | norm string // norm string representation 28 | estr string // error string 29 | isSCIM bool // whether it is a SCIM URN or not 30 | } 31 | 32 | var urnlexTestCases = []testCase{ 33 | // Italian act 34 | { 35 | []byte("urn:lex:it:stato:legge:2003-09-21;456"), 36 | true, 37 | &URN{ 38 | prefix: "urn", 39 | ID: "lex", 40 | SS: "it:stato:legge:2003-09-21;456", 41 | }, 42 | "urn:lex:it:stato:legge:2003-09-21;456", 43 | "urn:lex:it:stato:legge:2003-09-21;456", 44 | "", 45 | false, 46 | }, 47 | // French act 48 | { 49 | []byte("urn:lex:fr:etat:lois:2004-12-06;321"), 50 | true, 51 | &URN{ 52 | prefix: "urn", 53 | ID: "lex", 54 | SS: "fr:etat:lois:2004-12-06;321", 55 | }, 56 | "urn:lex:fr:etat:lois:2004-12-06;321", 57 | "urn:lex:fr:etat:lois:2004-12-06;321", 58 | "", 59 | false, 60 | }, 61 | // Spanish act 62 | { 63 | []byte("urn:lex:es:estado:ley:2002-07-12;123"), 64 | true, 65 | &URN{ 66 | prefix: "urn", 67 | ID: "lex", 68 | SS: "es:estado:ley:2002-07-12;123", 69 | }, 70 | "urn:lex:es:estado:ley:2002-07-12;123", 71 | "urn:lex:es:estado:ley:2002-07-12;123", 72 | "", 73 | false, 74 | }, 75 | // Glarus Swiss Canton decree 76 | { 77 | []byte("urn:lex:ch;glarus:regiere:erlass:2007-10-15;963"), 78 | true, 79 | &URN{ 80 | prefix: "urn", 81 | ID: "lex", 82 | SS: "ch;glarus:regiere:erlass:2007-10-15;963", 83 | }, 84 | "urn:lex:ch;glarus:regiere:erlass:2007-10-15;963", 85 | "urn:lex:ch;glarus:regiere:erlass:2007-10-15;963", 86 | "", 87 | false, 88 | }, 89 | // EU Council Directive 90 | { 91 | []byte("urn:lex:eu:council:directive:2010-03-09;2010-19-UE"), 92 | true, 93 | &URN{ 94 | prefix: "urn", 95 | ID: "lex", 96 | SS: "eu:council:directive:2010-03-09;2010-19-UE", 97 | }, 98 | "urn:lex:eu:council:directive:2010-03-09;2010-19-UE", 99 | "urn:lex:eu:council:directive:2010-03-09;2010-19-UE", 100 | "", 101 | false, 102 | }, 103 | { 104 | []byte("urn:lex:eu:council:directive:2010-03-09;2010-19-UE"), 105 | true, 106 | &URN{ 107 | prefix: "urn", 108 | ID: "lex", 109 | SS: "eu:council:directive:2010-03-09;2010-19-UE", 110 | }, 111 | "urn:lex:eu:council:directive:2010-03-09;2010-19-UE", 112 | "urn:lex:eu:council:directive:2010-03-09;2010-19-UE", 113 | "", 114 | false, 115 | }, 116 | // US FSC decision 117 | { 118 | []byte("urn:lex:us:federal.supreme.court:decision:1963-03-18;372.us.335"), 119 | true, 120 | &URN{ 121 | prefix: "urn", 122 | ID: "lex", 123 | SS: "us:federal.supreme.court:decision:1963-03-18;372.us.335", 124 | }, 125 | "urn:lex:us:federal.supreme.court:decision:1963-03-18;372.us.335", 126 | "urn:lex:us:federal.supreme.court:decision:1963-03-18;372.us.335", 127 | "", 128 | false, 129 | }, 130 | } 131 | 132 | var scimOnlyTestCases = []testCase{ 133 | // ok 134 | { 135 | []byte("urn:ietf:params:scim:schemas:core:2.0:User"), 136 | true, 137 | &URN{ 138 | prefix: "urn", 139 | ID: "ietf:params:scim", 140 | SS: "schemas:core:2.0:User", 141 | }, 142 | "urn:ietf:params:scim:schemas:core:2.0:User", 143 | "urn:ietf:params:scim:schemas:core:2.0:User", 144 | "", 145 | true, 146 | }, 147 | { 148 | []byte("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"), 149 | true, 150 | &URN{ 151 | prefix: "urn", 152 | ID: "ietf:params:scim", 153 | SS: "schemas:extension:enterprise:2.0:User", 154 | }, 155 | "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", 156 | "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", 157 | "", 158 | true, 159 | }, 160 | { 161 | []byte("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:userName"), 162 | true, 163 | &URN{ 164 | prefix: "urn", 165 | ID: "ietf:params:scim", 166 | SS: "schemas:extension:enterprise:2.0:User:userName", 167 | }, 168 | "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:userName", 169 | "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:userName", 170 | "", 171 | true, 172 | }, 173 | { 174 | []byte("urn:ietf:params:scim:api:messages:2.0:ListResponse"), 175 | true, 176 | &URN{ 177 | prefix: "urn", 178 | ID: "ietf:params:scim", 179 | SS: "api:messages:2.0:ListResponse", 180 | }, 181 | "urn:ietf:params:scim:api:messages:2.0:ListResponse", 182 | "urn:ietf:params:scim:api:messages:2.0:ListResponse", 183 | "", 184 | true, 185 | }, 186 | { 187 | []byte("urn:ietf:params:scim:schemas:core"), 188 | true, 189 | &URN{ 190 | prefix: "urn", 191 | ID: "ietf:params:scim", 192 | SS: "schemas:core", 193 | }, 194 | "urn:ietf:params:scim:schemas:core", 195 | "urn:ietf:params:scim:schemas:core", 196 | "", 197 | true, 198 | }, 199 | { 200 | []byte("urn:ietf:params:scim:param:core"), 201 | true, 202 | &URN{ 203 | prefix: "urn", 204 | ID: "ietf:params:scim", 205 | SS: "param:core", 206 | }, 207 | "urn:ietf:params:scim:param:core", 208 | "urn:ietf:params:scim:param:core", 209 | "", 210 | true, 211 | }, 212 | { 213 | []byte("urn:ietf:params:scim:api:messages:%FF"), 214 | true, 215 | &URN{ 216 | prefix: "urn", 217 | ID: "ietf:params:scim", 218 | SS: "api:messages:%FF", 219 | }, 220 | "urn:ietf:params:scim:api:messages:%FF", 221 | "urn:ietf:params:scim:api:messages:%ff", 222 | "", 223 | true, 224 | }, 225 | 226 | // no 227 | { 228 | []byte("arn:ietf:params:scim:schemas:core"), 229 | false, 230 | nil, 231 | "", 232 | "", 233 | fmt.Sprintf(errPrefix, 0), 234 | false, 235 | }, 236 | { 237 | []byte("usn:ietf:params:scim:schemas:core"), 238 | false, 239 | nil, 240 | "", 241 | "", 242 | fmt.Sprintf(errPrefix, 1), 243 | false, 244 | }, 245 | { 246 | []byte("urm:ietf:params:scim:schemas:core"), 247 | false, 248 | nil, 249 | "", 250 | "", 251 | fmt.Sprintf(errPrefix, 2), 252 | false, 253 | }, 254 | { 255 | []byte("urno:ietf:params:scim:schemas:core"), 256 | false, 257 | nil, 258 | "", 259 | "", 260 | fmt.Sprintf(errPrefix, 3), 261 | false, 262 | }, 263 | { 264 | []byte("urno"), 265 | false, 266 | nil, 267 | "", 268 | "", 269 | fmt.Sprintf(errPrefix, 3), 270 | false, 271 | }, 272 | { 273 | []byte("urn:WRONG:schemas:core"), 274 | false, 275 | nil, 276 | "", 277 | "", 278 | fmt.Sprintf(errSCIMNamespace, 4), 279 | false, 280 | }, 281 | { 282 | []byte("urn:ietf:params:scim:WRONG:core"), 283 | false, 284 | nil, 285 | "", 286 | "", 287 | fmt.Sprintf(errSCIMType, 21), 288 | false, 289 | }, 290 | { 291 | []byte("urn:ietf:params:scim:schemas:$"), 292 | false, 293 | nil, 294 | "", 295 | "", 296 | fmt.Sprintf(errSCIMName, 29), 297 | false, 298 | }, 299 | { 300 | []byte("urn:ietf:params:scim:schemas:core-"), 301 | false, 302 | nil, 303 | "", 304 | "", 305 | fmt.Sprintf(errSCIMName, 33), 306 | false, 307 | }, 308 | { 309 | []byte("urn:ietf:params:scim:schemas:core:"), 310 | false, 311 | nil, 312 | "", 313 | "", 314 | fmt.Sprintf(errSCIMOtherIncomplete, 33), 315 | false, 316 | }, 317 | { 318 | []byte("urn:ietf:params:scim:schemas:core:2.&"), 319 | false, 320 | nil, 321 | "", 322 | "", 323 | fmt.Sprintf(errSCIMOther, 36), 324 | false, 325 | }, 326 | { 327 | []byte("urn:ietf:params:scim:api:messages:%"), 328 | false, 329 | nil, 330 | "", 331 | "", 332 | fmt.Sprintf(errSCIMOtherIncomplete, 34), 333 | false, 334 | }, 335 | { 336 | []byte("urn:ietf:params:scim:api:messages:%F"), 337 | false, 338 | nil, 339 | "", 340 | "", 341 | fmt.Sprintf(errSCIMOtherIncomplete, 35), 342 | false, 343 | }, 344 | // TODO: verify 345 | // { 346 | // []byte("urn:ietf:params:scim:api:core:"), 347 | // true, 348 | // nil, 349 | // "", 350 | // "", 351 | // fmt.Sprintf(errSCIMOtherIncomplete, 29), 352 | // false, 353 | // }, 354 | { 355 | []byte("urn:"), 356 | false, 357 | nil, 358 | "", 359 | "", 360 | fmt.Sprintf(errSCIMNamespace, 4), 361 | false, 362 | }, 363 | { 364 | []byte("urn::"), 365 | false, 366 | nil, 367 | "", 368 | "", 369 | fmt.Sprintf(errSCIMNamespace, 4), 370 | false, 371 | }, 372 | { 373 | []byte("urn:a:"), 374 | false, 375 | nil, 376 | "", 377 | "", 378 | fmt.Sprintf(errSCIMNamespace, 4), 379 | false, 380 | }, 381 | { 382 | []byte("urn:a"), 383 | false, 384 | nil, 385 | "", 386 | "", 387 | fmt.Sprintf(errSCIMNamespace, 4), 388 | false, 389 | }, 390 | { 391 | []byte(`u`), 392 | false, 393 | nil, 394 | "", 395 | "", 396 | fmt.Sprintf(errPrefix, 1), 397 | false, 398 | }, 399 | { 400 | []byte(`ur`), 401 | false, 402 | nil, 403 | "", 404 | "", 405 | fmt.Sprintf(errPrefix, 2), 406 | false, 407 | }, 408 | { 409 | []byte(`urn`), 410 | false, 411 | nil, 412 | "", 413 | "", 414 | fmt.Sprintf(errPrefix, 3), 415 | false, 416 | }, 417 | } 418 | 419 | var urn2141OnlyTestCases = []testCase{ 420 | // ok 421 | { 422 | []byte("urn:simple:simple"), 423 | true, 424 | &URN{ 425 | prefix: "urn", 426 | ID: "simple", 427 | SS: "simple", 428 | }, 429 | "urn:simple:simple", 430 | "urn:simple:simple", 431 | "", 432 | false, 433 | }, 434 | { 435 | []byte("urn:ciao:%5D"), 436 | true, 437 | &URN{ 438 | prefix: "urn", 439 | ID: "ciao", 440 | SS: "%5D", 441 | }, 442 | "urn:ciao:%5D", 443 | "urn:ciao:%5d", 444 | "", 445 | false, 446 | }, 447 | 448 | // ok - RFC examples 449 | { 450 | []byte("URN:foo:a123,456"), 451 | true, 452 | &URN{ 453 | prefix: "URN", 454 | ID: "foo", 455 | SS: "a123,456", 456 | }, 457 | "URN:foo:a123,456", 458 | "urn:foo:a123,456", 459 | "", 460 | false, 461 | }, 462 | { 463 | []byte("urn:foo:a123,456"), 464 | true, 465 | &URN{ 466 | prefix: "urn", 467 | ID: "foo", 468 | SS: "a123,456", 469 | }, 470 | "urn:foo:a123,456", 471 | "urn:foo:a123,456", 472 | "", 473 | false, 474 | }, 475 | { 476 | []byte("urn:FOO:a123,456"), 477 | true, 478 | &URN{ 479 | prefix: "urn", 480 | ID: "FOO", 481 | SS: "a123,456", 482 | }, 483 | "urn:FOO:a123,456", 484 | "urn:foo:a123,456", 485 | "", 486 | false, 487 | }, 488 | { 489 | []byte("urn:foo:A123,456"), 490 | true, 491 | &URN{ 492 | prefix: "urn", 493 | ID: "foo", 494 | SS: "A123,456", 495 | }, 496 | "urn:foo:A123,456", 497 | "urn:foo:A123,456", 498 | "", 499 | false, 500 | }, 501 | { 502 | []byte("urn:foo:a123%2C456"), 503 | true, 504 | &URN{ 505 | prefix: "urn", 506 | ID: "foo", 507 | SS: "a123%2C456", 508 | }, 509 | "urn:foo:a123%2C456", 510 | "urn:foo:a123%2c456", 511 | "", 512 | false, 513 | }, 514 | { 515 | []byte("URN:FOO:a123%2c456"), 516 | true, 517 | &URN{ 518 | prefix: "URN", 519 | ID: "FOO", 520 | SS: "a123%2c456", 521 | }, 522 | "URN:FOO:a123%2c456", 523 | "urn:foo:a123%2c456", 524 | "", 525 | false, 526 | }, 527 | { 528 | []byte("URN:FOO:ABC%FFabc123%2c456"), 529 | true, 530 | &URN{ 531 | prefix: "URN", 532 | ID: "FOO", 533 | SS: "ABC%FFabc123%2c456", 534 | }, 535 | "URN:FOO:ABC%FFabc123%2c456", 536 | "urn:foo:ABC%ffabc123%2c456", 537 | "", 538 | false, 539 | }, 540 | { 541 | []byte("URN:FOO:ABC%FFabc123%2C456%9A"), 542 | true, 543 | &URN{ 544 | prefix: "URN", 545 | ID: "FOO", 546 | SS: "ABC%FFabc123%2C456%9A", 547 | }, 548 | "URN:FOO:ABC%FFabc123%2C456%9A", 549 | "urn:foo:ABC%ffabc123%2c456%9a", 550 | "", 551 | false, 552 | }, 553 | 554 | // ok - SCIM v2 555 | { 556 | []byte("urn:ietf:params:scim:schemas:core:2.0:User"), 557 | true, 558 | &URN{ 559 | prefix: "urn", 560 | ID: "ietf", 561 | SS: "params:scim:schemas:core:2.0:User", 562 | }, 563 | "urn:ietf:params:scim:schemas:core:2.0:User", 564 | "urn:ietf:params:scim:schemas:core:2.0:User", 565 | "", 566 | true, 567 | }, 568 | { 569 | []byte("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"), 570 | true, 571 | &URN{ 572 | prefix: "urn", 573 | ID: "ietf", 574 | SS: "params:scim:schemas:extension:enterprise:2.0:User", 575 | }, 576 | "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", 577 | "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", 578 | "", 579 | true, 580 | }, 581 | { 582 | []byte("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:userName"), 583 | true, 584 | &URN{ 585 | prefix: "urn", 586 | ID: "ietf", 587 | SS: "params:scim:schemas:extension:enterprise:2.0:User:userName", 588 | }, 589 | "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:userName", 590 | "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:userName", 591 | "", 592 | true, 593 | }, 594 | { 595 | []byte("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:meta.lastModified"), 596 | true, 597 | &URN{ 598 | prefix: "urn", 599 | ID: "ietf", 600 | SS: "params:scim:schemas:extension:enterprise:2.0:User:meta.lastModified", 601 | }, 602 | "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:meta.lastModified", 603 | "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:meta.lastModified", 604 | "", 605 | true, 606 | }, 607 | 608 | // ok - minimum urn 609 | { 610 | []byte("urn:a:b"), 611 | true, 612 | &URN{ 613 | prefix: "urn", 614 | ID: "a", 615 | SS: "b", 616 | }, 617 | "urn:a:b", 618 | "urn:a:b", 619 | "", 620 | false, 621 | }, 622 | { 623 | []byte("urn:a::"), 624 | true, 625 | &URN{ 626 | prefix: "urn", 627 | ID: "a", 628 | SS: ":", 629 | }, 630 | "urn:a::", 631 | "urn:a::", 632 | "", 633 | false, 634 | }, 635 | { 636 | []byte("urn:a:-"), 637 | true, 638 | &URN{ 639 | prefix: "urn", 640 | ID: "a", 641 | SS: "-", 642 | }, 643 | "urn:a:-", 644 | "urn:a:-", 645 | "", 646 | false, 647 | }, 648 | 649 | // ok - URN prefix is case-insensitive 650 | { 651 | []byte("URN:simple:simple"), 652 | true, 653 | &URN{ 654 | prefix: "URN", 655 | ID: "simple", 656 | SS: "simple", 657 | }, 658 | "URN:simple:simple", 659 | "urn:simple:simple", 660 | "", 661 | false, 662 | }, 663 | { 664 | []byte("Urn:simple:simple"), 665 | true, 666 | &URN{ 667 | prefix: "Urn", 668 | ID: "simple", 669 | SS: "simple", 670 | }, 671 | "Urn:simple:simple", 672 | "urn:simple:simple", 673 | "", 674 | false, 675 | }, 676 | 677 | // ok - ID can contain the "urn" string but it can not be exactly equal to it 678 | { 679 | []byte("urn:urna:simple"), 680 | true, 681 | &URN{ 682 | prefix: "urn", 683 | ID: "urna", 684 | SS: "simple", 685 | }, 686 | "urn:urna:simple", 687 | "urn:urna:simple", 688 | "", 689 | false, 690 | }, 691 | { 692 | []byte("urn:burnout:nss"), 693 | true, 694 | &URN{ 695 | prefix: "urn", 696 | ID: "burnout", 697 | SS: "nss", 698 | }, 699 | "urn:burnout:nss", 700 | "urn:burnout:nss", 701 | "", 702 | false, 703 | }, 704 | { 705 | []byte("urn:burn:nss"), 706 | true, 707 | &URN{ 708 | prefix: "urn", 709 | ID: "burn", 710 | SS: "nss", 711 | }, 712 | "urn:burn:nss", 713 | "urn:burn:nss", 714 | "", 715 | false, 716 | }, 717 | { 718 | []byte("urn:urnurnurn:x"), 719 | true, 720 | &URN{ 721 | prefix: "urn", 722 | ID: "urnurnurn", 723 | SS: "x", 724 | }, 725 | "urn:urnurnurn:x", 726 | "urn:urnurnurn:x", 727 | "", 728 | false, 729 | }, 730 | 731 | // ok - ID can contains maximum 32 characters 732 | { 733 | []byte("urn:abcdefghilmnopqrstuvzabcdefghilm:x"), 734 | true, 735 | &URN{ 736 | prefix: "urn", 737 | ID: "abcdefghilmnopqrstuvzabcdefghilm", 738 | SS: "x", 739 | }, 740 | "urn:abcdefghilmnopqrstuvzabcdefghilm:x", 741 | "urn:abcdefghilmnopqrstuvzabcdefghilm:x", 742 | "", 743 | false, 744 | }, 745 | 746 | // ok - ID can be alpha numeric 747 | { 748 | []byte("URN:123:x"), 749 | true, 750 | &URN{ 751 | prefix: "URN", 752 | ID: "123", 753 | SS: "x", 754 | }, 755 | "URN:123:x", 756 | "urn:123:x", 757 | "", 758 | false, 759 | }, 760 | { 761 | []byte("URN:1ab:x"), 762 | true, 763 | &URN{ 764 | prefix: "URN", 765 | ID: "1ab", 766 | SS: "x", 767 | }, 768 | "URN:1ab:x", 769 | "urn:1ab:x", 770 | "", 771 | false, 772 | }, 773 | { 774 | []byte("URN:a1b:x"), 775 | true, 776 | &URN{ 777 | prefix: "URN", 778 | ID: "a1b", 779 | SS: "x", 780 | }, 781 | "URN:a1b:x", 782 | "urn:a1b:x", 783 | "", 784 | false, 785 | }, 786 | { 787 | []byte("URN:a12:x"), 788 | true, 789 | &URN{ 790 | prefix: "URN", 791 | ID: "a12", 792 | SS: "x", 793 | }, 794 | "URN:a12:x", 795 | "urn:a12:x", 796 | "", 797 | false, 798 | }, 799 | { 800 | []byte("URN:cd2:x"), 801 | true, 802 | &URN{ 803 | prefix: "URN", 804 | ID: "cd2", 805 | SS: "x", 806 | }, 807 | "URN:cd2:x", 808 | "urn:cd2:x", 809 | "", 810 | false, 811 | }, 812 | 813 | // ok - ID can contain an hyphen (not in its first position, see below) 814 | { 815 | []byte("URN:abcd-:x"), 816 | true, 817 | &URN{ 818 | prefix: "URN", 819 | ID: "abcd-", 820 | SS: "x", 821 | }, 822 | "URN:abcd-:x", 823 | "urn:abcd-:x", 824 | "", 825 | false, 826 | }, 827 | { 828 | []byte("URN:abcd-abcd:x"), 829 | true, 830 | &URN{ 831 | prefix: "URN", 832 | ID: "abcd-abcd", 833 | SS: "x", 834 | }, 835 | "URN:abcd-abcd:x", 836 | "urn:abcd-abcd:x", 837 | "", 838 | false, 839 | }, 840 | { 841 | []byte("URN:a123-456z:x"), 842 | true, 843 | &URN{ 844 | prefix: "URN", 845 | ID: "a123-456z", 846 | SS: "x", 847 | }, 848 | "URN:a123-456z:x", 849 | "urn:a123-456z:x", 850 | "", 851 | false, 852 | }, 853 | 854 | // ok - SS can contain the "urn" string, also be exactly equal to it 855 | { 856 | []byte("urn:urnx:urn"), 857 | true, 858 | &URN{ 859 | prefix: "urn", 860 | ID: "urnx", 861 | SS: "urn", 862 | }, 863 | "urn:urnx:urn", 864 | "urn:urnx:urn", 865 | "", 866 | false, 867 | }, 868 | { 869 | []byte("urn:urnurnurn:urn"), 870 | true, 871 | &URN{ 872 | prefix: "urn", 873 | ID: "urnurnurn", 874 | SS: "urn", 875 | }, 876 | "urn:urnurnurn:urn", 877 | "urn:urnurnurn:urn", 878 | "", 879 | false, 880 | }, 881 | { 882 | []byte("urn:hey:urnurnurn"), 883 | true, 884 | &URN{ 885 | prefix: "urn", 886 | ID: "hey", 887 | SS: "urnurnurn", 888 | }, 889 | "urn:hey:urnurnurn", 890 | "urn:hey:urnurnurn", 891 | "", 892 | false, 893 | }, 894 | 895 | // ok - SS can contains and discerns multiple colons, also at the end 896 | { 897 | []byte("urn:ciao:a:b:c"), 898 | true, 899 | &URN{ 900 | prefix: "urn", 901 | ID: "ciao", 902 | SS: "a:b:c", 903 | }, 904 | "urn:ciao:a:b:c", 905 | "urn:ciao:a:b:c", 906 | "", 907 | false, 908 | }, 909 | { 910 | []byte("urn:aaa:x:y:"), 911 | true, 912 | &URN{ 913 | prefix: "urn", 914 | ID: "aaa", 915 | SS: "x:y:", 916 | }, 917 | "urn:aaa:x:y:", 918 | "urn:aaa:x:y:", 919 | "", 920 | false, 921 | }, 922 | { 923 | []byte("urn:aaa:x:y:"), 924 | true, 925 | &URN{ 926 | prefix: "urn", 927 | ID: "aaa", 928 | SS: "x:y:", 929 | }, 930 | "urn:aaa:x:y:", 931 | "urn:aaa:x:y:", 932 | "", 933 | false, 934 | }, 935 | 936 | // ok - SS can contain (and also start with) some non-alphabetical (ie., OTHER) characters 937 | { 938 | []byte("urn:ciao:-"), 939 | true, 940 | &URN{ 941 | prefix: "urn", 942 | ID: "ciao", 943 | SS: "-", 944 | }, 945 | "urn:ciao:-", 946 | "urn:ciao:-", 947 | "", 948 | false, 949 | }, 950 | { 951 | []byte("urn:ciao:("), 952 | true, 953 | &URN{ 954 | prefix: "urn", 955 | ID: "ciao", 956 | SS: "(", 957 | }, 958 | "urn:ciao:(", 959 | "urn:ciao:(", 960 | "", 961 | false, 962 | }, 963 | { 964 | []byte("urn:ciao:)"), 965 | true, 966 | &URN{ 967 | prefix: "urn", 968 | ID: "ciao", 969 | SS: ")", 970 | }, 971 | "urn:ciao:)", 972 | "urn:ciao:)", 973 | "", 974 | false, 975 | }, 976 | { 977 | []byte("urn:ciao:+"), 978 | true, 979 | &URN{ 980 | prefix: "urn", 981 | ID: "ciao", 982 | SS: "+", 983 | }, 984 | "urn:ciao:+", 985 | "urn:ciao:+", 986 | "", 987 | false, 988 | }, 989 | { 990 | []byte("urn:ciao::"), 991 | true, 992 | &URN{ 993 | prefix: "urn", 994 | ID: "ciao", 995 | SS: ":", 996 | }, 997 | "urn:ciao::", 998 | "urn:ciao::", 999 | "", 1000 | false, 1001 | }, 1002 | { 1003 | []byte("urn:colon:::::nss"), 1004 | true, 1005 | &URN{ 1006 | prefix: "urn", 1007 | ID: "colon", 1008 | SS: "::::nss", 1009 | }, 1010 | "urn:colon:::::nss", 1011 | "urn:colon:::::nss", 1012 | "", 1013 | false, 1014 | }, 1015 | { 1016 | []byte("urn:ciao:!"), 1017 | true, 1018 | &URN{ 1019 | prefix: "urn", 1020 | ID: "ciao", 1021 | SS: "!", 1022 | }, 1023 | "urn:ciao:!", 1024 | "urn:ciao:!", 1025 | "", 1026 | false, 1027 | }, 1028 | { 1029 | []byte("urn:ciao:!!*"), 1030 | true, 1031 | &URN{ 1032 | prefix: "urn", 1033 | ID: "ciao", 1034 | SS: "!!*", 1035 | }, 1036 | "urn:ciao:!!*", 1037 | "urn:ciao:!!*", 1038 | "", 1039 | false, 1040 | }, 1041 | { 1042 | []byte("urn:ciao:-!:-,:x"), 1043 | true, 1044 | &URN{ 1045 | prefix: "urn", 1046 | ID: "ciao", 1047 | SS: "-!:-,:x", 1048 | }, 1049 | "urn:ciao:-!:-,:x", 1050 | "urn:ciao:-!:-,:x", 1051 | "", 1052 | false, 1053 | }, 1054 | { 1055 | []byte("urn:ciao:=@"), 1056 | true, 1057 | &URN{ 1058 | prefix: "urn", 1059 | ID: "ciao", 1060 | SS: "=@", 1061 | }, 1062 | "urn:ciao:=@", 1063 | "urn:ciao:=@", 1064 | "", 1065 | false, 1066 | }, 1067 | { 1068 | []byte("urn:ciao:@!=%2C(xyz)+a,b.*@g=$_'"), 1069 | true, 1070 | &URN{ 1071 | prefix: "urn", 1072 | ID: "ciao", 1073 | SS: "@!=%2C(xyz)+a,b.*@g=$_'", 1074 | }, 1075 | "urn:ciao:@!=%2C(xyz)+a,b.*@g=$_'", 1076 | "urn:ciao:@!=%2c(xyz)+a,b.*@g=$_'", 1077 | "", 1078 | false, 1079 | }, 1080 | 1081 | // ok - SS can contain (and also start with) hexadecimal representation of octets 1082 | { 1083 | []byte("URN:hexes:%25"), 1084 | true, 1085 | &URN{ 1086 | prefix: "URN", 1087 | ID: "hexes", 1088 | SS: "%25", 1089 | }, 1090 | "URN:hexes:%25", 1091 | "urn:hexes:%25", 1092 | "", 1093 | false, 1094 | }, // Literal use of the "%" character in a namespace must be encoded using "%25" 1095 | { 1096 | []byte("URN:x:abc%1Dz%2F%3az"), 1097 | true, 1098 | &URN{ 1099 | prefix: "URN", 1100 | ID: "x", 1101 | SS: "abc%1Dz%2F%3az", 1102 | }, 1103 | "URN:x:abc%1Dz%2F%3az", 1104 | "urn:x:abc%1dz%2f%3az", 1105 | "", 1106 | false, 1107 | }, // Literal use of the "%" character in a namespace must be encoded using "%25" 1108 | 1109 | // no - ID can not start with an hyphen 1110 | { 1111 | []byte("URN:-xxx:x"), 1112 | false, 1113 | nil, 1114 | "", 1115 | "", 1116 | `expecting the identifier to be string (1..31 alnum chars, also containing dashes but not at its beginning) [col 4]`, 1117 | false, 1118 | }, 1119 | { 1120 | []byte("URN:---xxx:x"), 1121 | false, 1122 | nil, 1123 | "", 1124 | "", 1125 | `expecting the identifier to be string (1..31 alnum chars, also containing dashes but not at its beginning) [col 4]`, 1126 | false, 1127 | }, 1128 | 1129 | // no - ID can not start with a colon 1130 | { 1131 | []byte("urn::colon:nss"), 1132 | false, 1133 | nil, 1134 | "", 1135 | "", 1136 | `expecting the identifier to be string (1..31 alnum chars, also containing dashes but not at its beginning) [col 4]`, 1137 | false, 1138 | }, 1139 | { 1140 | []byte("urn::::nss"), 1141 | false, 1142 | nil, 1143 | "", 1144 | "", 1145 | `expecting the identifier to be string (1..31 alnum chars, also containing dashes but not at its beginning) [col 4]`, 1146 | false, 1147 | }, 1148 | 1149 | // no - ID can not contains more than 32 characters 1150 | { 1151 | []byte("urn:abcdefghilmnopqrstuvzabcdefghilmn:specificstring"), 1152 | false, 1153 | nil, 1154 | "", 1155 | "", 1156 | `expecting the identifier to be string (1..31 alnum chars, also containing dashes but not at its beginning) [col 36]`, 1157 | false, 1158 | }, 1159 | 1160 | // no - ID can not contain special characters 1161 | { 1162 | []byte("URN:a!?:x"), 1163 | false, 1164 | nil, 1165 | "", 1166 | "", 1167 | `expecting the identifier to be string (1..31 alnum chars, also containing dashes but not at its beginning) [col 5]`, 1168 | false, 1169 | }, 1170 | { 1171 | []byte("URN:@,:x"), 1172 | false, 1173 | nil, 1174 | "", 1175 | "", 1176 | `expecting the identifier to be string (1..31 alnum chars, also containing dashes but not at its beginning) [col 4]`, 1177 | false, 1178 | }, 1179 | { 1180 | []byte("URN:#,:x"), 1181 | false, 1182 | nil, 1183 | "", 1184 | "", 1185 | `expecting the identifier to be string (1..31 alnum chars, also containing dashes but not at its beginning) [col 4]`, 1186 | false, 1187 | }, 1188 | { 1189 | []byte("URN:bc'.@:x"), 1190 | false, 1191 | nil, 1192 | "", 1193 | "", 1194 | `expecting the identifier to be string (1..31 alnum chars, also containing dashes but not at its beginning) [col 6]`, 1195 | false, 1196 | }, 1197 | 1198 | // no - ID can not be equal to "urn" 1199 | { 1200 | []byte("urn:urn:NSS"), 1201 | false, 1202 | nil, 1203 | "", 1204 | "", 1205 | `expecting the identifier to not contain the "urn" reserved string [col 7]`, 1206 | false, 1207 | }, 1208 | { 1209 | []byte("urn:URN:NSS"), 1210 | false, 1211 | nil, 1212 | "", 1213 | "", 1214 | `expecting the identifier to not contain the "urn" reserved string [col 7]`, 1215 | false, 1216 | }, 1217 | { 1218 | []byte("URN:URN:NSS"), 1219 | false, 1220 | nil, 1221 | "", 1222 | "", 1223 | `expecting the identifier to not contain the "urn" reserved string [col 7]`, 1224 | false, 1225 | }, 1226 | { 1227 | []byte("urn:UrN:NSS"), 1228 | false, 1229 | nil, 1230 | "", 1231 | "", 1232 | `expecting the identifier to not contain the "urn" reserved string [col 7]`, 1233 | false, 1234 | }, 1235 | { 1236 | []byte("urn:Urn:NSS"), 1237 | false, 1238 | nil, 1239 | "", 1240 | "", 1241 | `expecting the identifier to not contain the "urn" reserved string [col 7]`, 1242 | false, 1243 | }, 1244 | 1245 | // no - ID can not contain spaces 1246 | { 1247 | []byte("urn:white space:NSS"), 1248 | false, 1249 | nil, 1250 | "", 1251 | "", 1252 | `expecting the identifier to be string (1..31 alnum chars, also containing dashes but not at its beginning) [col 9]`, 1253 | false, 1254 | }, 1255 | 1256 | // no - SS can not contain spaces 1257 | { 1258 | []byte("urn:concat:no spaces"), 1259 | false, 1260 | nil, 1261 | "", 1262 | "", 1263 | `expecting the specific string to be a string containing alnum, hex, or others ([()+,-.:=@;$_!*']) chars [col 13]`, 1264 | false, 1265 | }, 1266 | 1267 | // no - SS can not contain reserved characters (can accept them only if %-escaped) 1268 | { 1269 | []byte("urn:a:%"), // the presence of an "%" character in an URN MUST be followed by two characters from the character set 1270 | false, 1271 | nil, 1272 | "", 1273 | "", 1274 | fmt.Sprintf(errHex, 7), 1275 | false, 1276 | }, 1277 | { 1278 | []byte("urn:a:%A"), // the presence of an "%" character in an URN MUST be followed by two characters from the character set 1279 | false, 1280 | nil, 1281 | "", 1282 | "", 1283 | fmt.Sprintf(errHex, 8), 1284 | false, 1285 | }, 1286 | { 1287 | []byte("urn:a:?"), 1288 | false, 1289 | nil, 1290 | "", 1291 | "", 1292 | `expecting the specific string to be a string containing alnum, hex, or others ([()+,-.:=@;$_!*']) chars [col 6]`, 1293 | false, 1294 | }, 1295 | { 1296 | []byte("urn:a:#"), 1297 | false, 1298 | nil, 1299 | "", 1300 | "", 1301 | `expecting the specific string to be a string containing alnum, hex, or others ([()+,-.:=@;$_!*']) chars [col 6]`, 1302 | false, 1303 | }, 1304 | { 1305 | []byte("urn:a:/"), 1306 | false, 1307 | nil, 1308 | "", 1309 | "", 1310 | `expecting the specific string to be a string containing alnum, hex, or others ([()+,-.:=@;$_!*']) chars [col 6]`, 1311 | false, 1312 | }, 1313 | { 1314 | []byte("urn:ietf:params:scim:api:messages:%"), 1315 | false, 1316 | nil, 1317 | "", 1318 | "", 1319 | fmt.Sprintf(errHex, 35), 1320 | false, 1321 | }, 1322 | { 1323 | []byte("urn:ietf:params:scim:api:messages:%F"), 1324 | false, 1325 | nil, 1326 | "", 1327 | "", 1328 | fmt.Sprintf(errHex, 36), 1329 | false, 1330 | }, 1331 | { 1332 | []byte("arn:ietf:params:scim:schemas:core"), 1333 | false, 1334 | nil, 1335 | "", 1336 | "", 1337 | fmt.Sprintf(errPrefix, 0), 1338 | false, 1339 | }, 1340 | { 1341 | []byte("usn:ietf:params:scim:schemas:core"), 1342 | false, 1343 | nil, 1344 | "", 1345 | "", 1346 | fmt.Sprintf(errPrefix, 1), 1347 | false, 1348 | }, 1349 | { 1350 | []byte("urm:ietf:params:scim:schemas:core"), 1351 | false, 1352 | nil, 1353 | "", 1354 | "", 1355 | fmt.Sprintf(errPrefix, 2), 1356 | false, 1357 | }, 1358 | { 1359 | []byte("urno:ietf:params:scim:schemas:core"), 1360 | false, 1361 | nil, 1362 | "", 1363 | "", 1364 | fmt.Sprintf(errPrefix, 3), 1365 | false, 1366 | }, 1367 | { 1368 | []byte("urno"), 1369 | false, 1370 | nil, 1371 | "", 1372 | "", 1373 | fmt.Sprintf(errPrefix, 3), 1374 | false, 1375 | }, 1376 | { 1377 | []byte("URN:a!?:x"), 1378 | false, 1379 | nil, 1380 | "", 1381 | "", 1382 | `expecting the identifier to be string (1..31 alnum chars, also containing dashes but not at its beginning) [col 5]`, 1383 | false, 1384 | }, 1385 | { 1386 | []byte("urn:Urn:NSS"), 1387 | false, 1388 | nil, 1389 | "", 1390 | "", 1391 | `expecting the identifier to not contain the "urn" reserved string [col 7]`, 1392 | false, 1393 | }, 1394 | { 1395 | []byte("urn:spazio bianco:NSS"), 1396 | false, 1397 | nil, 1398 | "", 1399 | "", 1400 | `expecting the identifier to be string (1..31 alnum chars, also containing dashes but not at its beginning) [col 10]`, 1401 | false, 1402 | }, 1403 | { 1404 | []byte("urn:conca:z ws"), 1405 | false, 1406 | nil, 1407 | "", 1408 | "", 1409 | fmt.Sprintf(errSpecificString, 11), 1410 | false, 1411 | }, 1412 | { 1413 | []byte("urn:ietf:params:scim:schemas:core:2.&"), 1414 | false, 1415 | nil, 1416 | "", 1417 | "", 1418 | fmt.Sprintf(errSpecificString, 36), 1419 | false, 1420 | }, 1421 | 1422 | // no - Incomplete URNs 1423 | { 1424 | []byte("urn:"), 1425 | false, 1426 | nil, 1427 | "", 1428 | "", 1429 | `expecting the identifier to be string (1..31 alnum chars, also containing dashes but not at its beginning) [col 4]`, 1430 | false, 1431 | }, 1432 | { 1433 | []byte("urn::"), 1434 | false, 1435 | nil, 1436 | "", 1437 | "", 1438 | `expecting the identifier to be string (1..31 alnum chars, also containing dashes but not at its beginning) [col 4]`, 1439 | false, 1440 | }, 1441 | { 1442 | []byte("urn:a:"), 1443 | false, 1444 | nil, 1445 | "", 1446 | "", 1447 | `expecting the specific string to be a string containing alnum, hex, or others ([()+,-.:=@;$_!*']) chars [col 6]`, 1448 | false, 1449 | }, 1450 | { 1451 | []byte("urn:a"), 1452 | false, 1453 | nil, 1454 | "", 1455 | "", 1456 | fmt.Sprintf(errIdentifier, 5), 1457 | false, 1458 | }, 1459 | // no - Incomplete prefix 1460 | { 1461 | []byte(`u`), 1462 | false, 1463 | nil, 1464 | "", 1465 | "", 1466 | fmt.Sprintf(errPrefix, 1), 1467 | false, 1468 | }, 1469 | { 1470 | []byte(`ur`), 1471 | false, 1472 | nil, 1473 | "", 1474 | "", 1475 | fmt.Sprintf(errPrefix, 2), 1476 | false, 1477 | }, 1478 | { 1479 | []byte(`urn`), 1480 | false, 1481 | nil, 1482 | "", 1483 | "", 1484 | fmt.Sprintf(errPrefix, 3), 1485 | false, 1486 | }, 1487 | 1488 | // no, empty 1489 | { 1490 | []byte(""), 1491 | false, 1492 | nil, 1493 | "", 1494 | "", 1495 | fmt.Sprintf(errPrefix, 0), 1496 | false, 1497 | }, 1498 | } 1499 | 1500 | var rfc8141TestCases = []testCase{ 1501 | // ok 1502 | { 1503 | []byte("urn:lex:it:ministero.giustizia:decreto:1992-07-24;358~art5"), // Italian decree 1504 | true, 1505 | &URN{ 1506 | prefix: "urn", 1507 | ID: "lex", 1508 | SS: "it:ministero.giustizia:decreto:1992-07-24;358~art5", 1509 | }, 1510 | "urn:lex:it:ministero.giustizia:decreto:1992-07-24;358~art5", 1511 | "urn:lex:it:ministero.giustizia:decreto:1992-07-24;358~art5", 1512 | "", 1513 | false, 1514 | }, // ~ allowed in the NSS 1515 | { 1516 | []byte("urn:nid:nss/"), 1517 | true, 1518 | &URN{ 1519 | prefix: "urn", 1520 | ID: "nid", 1521 | SS: "nss/", 1522 | }, 1523 | "urn:nid:nss/", 1524 | "urn:nid:nss/", 1525 | "", 1526 | false, 1527 | }, // / allowed in the NSS 1528 | { 1529 | []byte("urn:nid:nss&"), 1530 | true, 1531 | &URN{ 1532 | prefix: "urn", 1533 | ID: "nid", 1534 | SS: "nss&", 1535 | }, 1536 | "urn:nid:nss&", 1537 | "urn:nid:nss&", 1538 | "", 1539 | false, 1540 | }, // & allowed in the NSS 1541 | { 1542 | []byte("urn:example:1/406/47452/2"), 1543 | true, 1544 | &URN{ 1545 | prefix: "urn", 1546 | ID: "example", 1547 | SS: "1/406/47452/2", 1548 | }, 1549 | "urn:example:1/406/47452/2", 1550 | "urn:example:1/406/47452/2", 1551 | "", 1552 | false, 1553 | }, // & allowed in the NSS 1554 | { 1555 | []byte("urn:example:foo-bar-baz-qux?+CCResolve:cc=uk"), 1556 | true, 1557 | &URN{ 1558 | prefix: "urn", 1559 | ID: "example", 1560 | SS: "foo-bar-baz-qux", 1561 | rComponent: "CCResolve:cc=uk", 1562 | }, 1563 | "urn:example:foo-bar-baz-qux?+CCResolve:cc=uk", 1564 | "urn:example:foo-bar-baz-qux", 1565 | "", 1566 | false, 1567 | }, 1568 | { 1569 | []byte("urn:example:foo-bar-baz-qux?+&"), 1570 | true, 1571 | &URN{ 1572 | prefix: "urn", 1573 | ID: "example", 1574 | SS: "foo-bar-baz-qux", 1575 | rComponent: "&", 1576 | }, 1577 | "urn:example:foo-bar-baz-qux?+&", 1578 | "urn:example:foo-bar-baz-qux", 1579 | "", 1580 | false, 1581 | }, 1582 | { 1583 | []byte("urn:example:foo-bar-baz-qux?+~"), 1584 | true, 1585 | &URN{ 1586 | prefix: "urn", 1587 | ID: "example", 1588 | SS: "foo-bar-baz-qux", 1589 | rComponent: "~", 1590 | }, 1591 | "urn:example:foo-bar-baz-qux?+~", 1592 | "urn:example:foo-bar-baz-qux", 1593 | "", 1594 | false, 1595 | }, 1596 | { 1597 | []byte("urn:example:foo-bar-baz-qux?+%16CCResolve:cc=uk"), 1598 | true, 1599 | &URN{ 1600 | prefix: "urn", 1601 | ID: "example", 1602 | SS: "foo-bar-baz-qux", 1603 | rComponent: "%16CCResolve:cc=uk", 1604 | }, 1605 | "urn:example:foo-bar-baz-qux?+%16CCResolve:cc=uk", 1606 | "urn:example:foo-bar-baz-qux", 1607 | "", 1608 | false, 1609 | }, 1610 | { 1611 | []byte("urn:example:foo-bar-baz-qut?+~&%FF()+,-.:=@;$_!/?Alnum123456"), 1612 | true, 1613 | &URN{ 1614 | prefix: "urn", 1615 | ID: "example", 1616 | SS: "foo-bar-baz-qut", 1617 | rComponent: "~&%FF()+,-.:=@;$_!/?Alnum123456", 1618 | }, 1619 | "urn:example:foo-bar-baz-qut?+~&%FF()+,-.:=@;$_!/?Alnum123456", 1620 | "urn:example:foo-bar-baz-qut", 1621 | "", 1622 | false, 1623 | }, 1624 | { 1625 | []byte("urn:example:weather?=op=map&lat=39.56&lon=-104.85&datetime=1969-07-21T02:56:15Z"), 1626 | true, 1627 | &URN{ 1628 | prefix: "urn", 1629 | ID: "example", 1630 | SS: "weather", 1631 | qComponent: "op=map&lat=39.56&lon=-104.85&datetime=1969-07-21T02:56:15Z", 1632 | }, 1633 | "urn:example:weather?=op=map&lat=39.56&lon=-104.85&datetime=1969-07-21T02:56:15Z", 1634 | "urn:example:weather", 1635 | "", 1636 | false, 1637 | }, 1638 | { 1639 | []byte("urn:esempio:climate?=alnum~&%FF()+,-.:=@;$_!/?123456"), 1640 | true, 1641 | &URN{ 1642 | prefix: "urn", 1643 | ID: "esempio", 1644 | SS: "climate", 1645 | qComponent: "alnum~&%FF()+,-.:=@;$_!/?123456", 1646 | }, 1647 | "urn:esempio:climate?=alnum~&%FF()+,-.:=@;$_!/?123456", 1648 | "urn:esempio:climate", 1649 | "", 1650 | false, 1651 | }, 1652 | { 1653 | []byte("urn:esempio:climate?=&&"), 1654 | true, 1655 | &URN{ 1656 | prefix: "urn", 1657 | ID: "esempio", 1658 | SS: "climate", 1659 | qComponent: "&&", 1660 | }, 1661 | "urn:esempio:climate?=&&", 1662 | "urn:esempio:climate", 1663 | "", 1664 | false, 1665 | }, 1666 | { 1667 | []byte("urn:esempio:climate?=%A1alnum~&%FF()+,-.:=@;$_!/?123456"), 1668 | true, 1669 | &URN{ 1670 | prefix: "urn", 1671 | ID: "esempio", 1672 | SS: "climate", 1673 | qComponent: "%A1alnum~&%FF()+,-.:=@;$_!/?123456", 1674 | }, 1675 | "urn:esempio:climate?=%A1alnum~&%FF()+,-.:=@;$_!/?123456", 1676 | "urn:esempio:climate", 1677 | "", 1678 | false, 1679 | }, 1680 | { 1681 | []byte("urn:example:foo-bar-baz-qux#somepart"), 1682 | true, 1683 | &URN{ 1684 | prefix: "urn", 1685 | ID: "example", 1686 | SS: "foo-bar-baz-qux", 1687 | fComponent: "somepart", 1688 | }, 1689 | "urn:example:foo-bar-baz-qux#somepart", 1690 | "urn:example:foo-bar-baz-qux", 1691 | "", 1692 | false, 1693 | }, 1694 | { 1695 | []byte("urn:example:foo-bar-baz-qux#~&"), 1696 | true, 1697 | &URN{ 1698 | prefix: "urn", 1699 | ID: "example", 1700 | SS: "foo-bar-baz-qux", 1701 | fComponent: "~&", 1702 | }, 1703 | "urn:example:foo-bar-baz-qux#~&", 1704 | "urn:example:foo-bar-baz-qux", 1705 | "", 1706 | false, 1707 | }, 1708 | { 1709 | []byte("urn:example:foo-bar-baz-qux#alnum~&%FF()+,-.:=@;$_!/?123456"), 1710 | true, 1711 | &URN{ 1712 | prefix: "urn", 1713 | ID: "example", 1714 | SS: "foo-bar-baz-qux", 1715 | fComponent: "alnum~&%FF()+,-.:=@;$_!/?123456", 1716 | }, 1717 | "urn:example:foo-bar-baz-qux#alnum~&%FF()+,-.:=@;$_!/?123456", 1718 | "urn:example:foo-bar-baz-qux", 1719 | "", 1720 | false, 1721 | }, 1722 | { 1723 | []byte("urn:example:foo-bar-baz-qux#%D0alnum~&%FF()+,-.:=@;$_!/?123456"), 1724 | true, 1725 | &URN{ 1726 | prefix: "urn", 1727 | ID: "example", 1728 | SS: "foo-bar-baz-qux", 1729 | fComponent: "%D0alnum~&%FF()+,-.:=@;$_!/?123456", 1730 | }, 1731 | "urn:example:foo-bar-baz-qux#%D0alnum~&%FF()+,-.:=@;$_!/?123456", 1732 | "urn:example:foo-bar-baz-qux", 1733 | "", 1734 | false, 1735 | }, 1736 | { 1737 | []byte("urn:example:CamelCase1/406/47452/2?+Cc=it&prefix=39?=lat=41.22255&long=16.06596#frag"), 1738 | true, 1739 | &URN{ 1740 | prefix: "urn", 1741 | ID: "example", 1742 | SS: "CamelCase1/406/47452/2", 1743 | rComponent: "Cc=it&prefix=39", 1744 | qComponent: "lat=41.22255&long=16.06596", 1745 | fComponent: "frag", 1746 | }, 1747 | "urn:example:CamelCase1/406/47452/2?+Cc=it&prefix=39?=lat=41.22255&long=16.06596#frag", 1748 | "urn:example:CamelCase1/406/47452/2", 1749 | "", 1750 | false, 1751 | }, // r_component, q_component, f_component 1752 | { 1753 | []byte("urn:example:CamelCase1/406/47452/2?=lat=41.22255&long=16.06596#frag"), 1754 | true, 1755 | &URN{ 1756 | prefix: "urn", 1757 | ID: "example", 1758 | SS: "CamelCase1/406/47452/2", 1759 | qComponent: "lat=41.22255&long=16.06596", 1760 | fComponent: "frag", 1761 | }, 1762 | "urn:example:CamelCase1/406/47452/2?=lat=41.22255&long=16.06596#frag", 1763 | "urn:example:CamelCase1/406/47452/2", 1764 | "", 1765 | false, 1766 | }, // q_component, f_component 1767 | { 1768 | []byte("urn:example:CamelCase1/406/47452/2?=lat=41.22255&long=16.06596#frag?some/slash/~%D0"), 1769 | true, 1770 | &URN{ 1771 | prefix: "urn", 1772 | ID: "example", 1773 | SS: "CamelCase1/406/47452/2", 1774 | qComponent: "lat=41.22255&long=16.06596", 1775 | fComponent: "frag?some/slash/~%D0", 1776 | }, 1777 | "urn:example:CamelCase1/406/47452/2?=lat=41.22255&long=16.06596#frag?some/slash/~%D0", 1778 | "urn:example:CamelCase1/406/47452/2", 1779 | "", 1780 | false, 1781 | }, // q_component, f_component 1782 | { 1783 | []byte("urn:example:CamelCase1/406/47452/2?+Cc=it&prefix=39?=lat=41.22255&long=16.06596"), 1784 | true, 1785 | &URN{ 1786 | prefix: "urn", 1787 | ID: "example", 1788 | SS: "CamelCase1/406/47452/2", 1789 | rComponent: "Cc=it&prefix=39", 1790 | qComponent: "lat=41.22255&long=16.06596", 1791 | }, 1792 | "urn:example:CamelCase1/406/47452/2?+Cc=it&prefix=39?=lat=41.22255&long=16.06596", 1793 | "urn:example:CamelCase1/406/47452/2", 1794 | "", 1795 | false, 1796 | }, // r_component, q_component 1797 | { 1798 | []byte("urn:TESTt3st:&/987/QWERTYUIOP/0#"), 1799 | true, 1800 | &URN{ 1801 | prefix: "urn", 1802 | ID: "TESTt3st", 1803 | SS: "&/987/QWERTYUIOP/0", 1804 | fComponent: "", 1805 | }, 1806 | "urn:TESTt3st:&/987/QWERTYUIOP/0", 1807 | "urn:testt3st:&/987/QWERTYUIOP/0", 1808 | "", 1809 | false, 1810 | }, // empty fragment 1811 | { 1812 | []byte("urn:example:%D0%B0123,z456"), 1813 | true, 1814 | &URN{ 1815 | prefix: "urn", 1816 | ID: "example", 1817 | SS: "%D0%B0123,z456", 1818 | }, 1819 | "urn:example:%D0%B0123,z456", 1820 | "urn:example:%d0%b0123,z456", 1821 | "", 1822 | false, 1823 | }, 1824 | { 1825 | []byte("urn:example:apple:pear:plum:cherry"), 1826 | true, 1827 | &URN{ 1828 | prefix: "urn", 1829 | ID: "example", 1830 | SS: "apple:pear:plum:cherry", 1831 | }, 1832 | "urn:example:apple:pear:plum:cherry", 1833 | "urn:example:apple:pear:plum:cherry", 1834 | "", 1835 | false, 1836 | }, 1837 | { 1838 | []byte("urn:z------------------------------a:q"), 1839 | true, 1840 | &URN{ 1841 | prefix: "urn", 1842 | ID: "z------------------------------a", 1843 | SS: "q", 1844 | }, 1845 | "urn:z------------------------------a:q", 1846 | "urn:z------------------------------a:q", 1847 | "", 1848 | false, 1849 | }, 1850 | { 1851 | []byte("urn:10:2"), 1852 | true, 1853 | &URN{ 1854 | prefix: "urn", 1855 | ID: "10", 1856 | SS: "2", 1857 | }, 1858 | "urn:10:2", 1859 | "urn:10:2", 1860 | "", 1861 | false, 1862 | }, 1863 | { 1864 | []byte("urn:a1l2n3m4-56789aeiou:2"), 1865 | true, 1866 | &URN{ 1867 | prefix: "urn", 1868 | ID: "a1l2n3m4-56789aeiou", 1869 | SS: "2", 1870 | }, 1871 | "urn:a1l2n3m4-56789aeiou:2", 1872 | "urn:a1l2n3m4-56789aeiou:2", 1873 | "", 1874 | false, 1875 | }, 1876 | { 1877 | []byte("urn:a1l2n3m4-56789aeiou:2%D0%B0"), 1878 | true, 1879 | &URN{ 1880 | prefix: "urn", 1881 | ID: "a1l2n3m4-56789aeiou", 1882 | SS: "2%D0%B0", 1883 | }, 1884 | "urn:a1l2n3m4-56789aeiou:2%D0%B0", 1885 | "urn:a1l2n3m4-56789aeiou:2%d0%b0", 1886 | "", 1887 | false, 1888 | }, 1889 | { 1890 | []byte("urn:amp:&"), 1891 | true, 1892 | &URN{ 1893 | prefix: "urn", 1894 | ID: "amp", 1895 | SS: "&", 1896 | }, 1897 | "urn:amp:&", 1898 | "urn:amp:&", 1899 | "", 1900 | false, 1901 | }, 1902 | { 1903 | []byte("urn:tilde:~~~"), 1904 | true, 1905 | &URN{ 1906 | prefix: "urn", 1907 | ID: "tilde", 1908 | SS: "~~~", 1909 | }, 1910 | "urn:tilde:~~~", 1911 | "urn:tilde:~~~", 1912 | "", 1913 | false, 1914 | }, 1915 | { 1916 | []byte("urn:signs:()+,-.:=@;$_!*alnum123456789"), 1917 | true, 1918 | &URN{ 1919 | prefix: "urn", 1920 | ID: "signs", 1921 | SS: "()+,-.:=@;$_!*alnum123456789", 1922 | }, 1923 | "urn:signs:()+,-.:=@;$_!*alnum123456789", 1924 | "urn:signs:()+,-.:=@;$_!*alnum123456789", 1925 | "", 1926 | false, 1927 | }, 1928 | { 1929 | []byte("URN:signs:()+,-.:=@;$_!*alnum123456789"), 1930 | true, 1931 | &URN{ 1932 | prefix: "URN", 1933 | ID: "signs", 1934 | SS: "()+,-.:=@;$_!*alnum123456789", 1935 | }, 1936 | "URN:signs:()+,-.:=@;$_!*alnum123456789", 1937 | "urn:signs:()+,-.:=@;$_!*alnum123456789", 1938 | "", 1939 | false, 1940 | }, 1941 | { 1942 | []byte("urn:urn-7:informal"), 1943 | true, 1944 | &URN{ 1945 | prefix: "urn", 1946 | ID: "urn-7", 1947 | SS: "informal", 1948 | }, 1949 | "urn:urn-7:informal", 1950 | "urn:urn-7:informal", 1951 | "", 1952 | false, 1953 | }, // NID containing informal URN namespace 1954 | { 1955 | []byte("urn:ex:ex?+a?"), 1956 | true, 1957 | &URN{ 1958 | prefix: "urn", 1959 | ID: "ex", 1960 | SS: "ex", 1961 | rComponent: "a?", 1962 | }, 1963 | "urn:ex:ex?+a?", 1964 | "urn:ex:ex", 1965 | "", 1966 | false, 1967 | }, // r_component containing ? not immediately followed by + or = 1968 | 1969 | // no 1970 | { 1971 | []byte("urn:urn-0:nss"), 1972 | false, 1973 | nil, 1974 | "", 1975 | "", 1976 | fmt.Sprintf(err8141InformalID, 7), 1977 | false, 1978 | }, // NID must not start the a wrong informal URN namespace 1979 | { 1980 | []byte("urn:urn-s:nss"), 1981 | false, 1982 | nil, 1983 | "", 1984 | "", 1985 | fmt.Sprintf(err8141InformalID, 7), 1986 | false, 1987 | }, // NID must not start the "urn-" prefix followed by a string 1988 | { 1989 | []byte("urn:example:а123,z456"), 1990 | false, 1991 | nil, 1992 | "", 1993 | "", 1994 | fmt.Sprintf(err8141SpecificString, 12), 1995 | false, 1996 | }, // CYRILLIC а (U+0430) 1997 | { 1998 | []byte("URN:-leading:w"), 1999 | false, 2000 | nil, 2001 | "", 2002 | "", 2003 | fmt.Sprintf(err8141Identifier, 4), 2004 | false, 2005 | }, // leading - not allowed in the NID 2006 | { 2007 | []byte("URN:trailing-:w"), 2008 | false, 2009 | nil, 2010 | "", 2011 | "", 2012 | fmt.Sprintf(err8141Identifier, 13), 2013 | false, 2014 | }, // trailing - not allowed in the NID 2015 | { 2016 | []byte("urn:a:nss"), 2017 | false, 2018 | nil, 2019 | "", 2020 | "", 2021 | fmt.Sprintf(err8141Identifier, 5), 2022 | false, 2023 | }, // NID at least 2 characters 2024 | { 2025 | []byte("urn:1:nss"), 2026 | false, 2027 | nil, 2028 | "", 2029 | "", 2030 | fmt.Sprintf(err8141Identifier, 5), 2031 | false, 2032 | }, // NID at least 2 characters 2033 | { 2034 | []byte("urn:yz-:nss"), 2035 | false, 2036 | nil, 2037 | "", 2038 | "", 2039 | fmt.Sprintf(err8141Identifier, 7), 2040 | false, 2041 | }, // NID must not start with 2 characters followerd by a dash 2042 | { 2043 | []byte("urn:9x-:nss"), 2044 | false, 2045 | nil, 2046 | "", 2047 | "", 2048 | fmt.Sprintf(err8141Identifier, 7), 2049 | false, 2050 | }, // NID must not start with 2 characters followerd by a dash 2051 | { 2052 | []byte("urn:X-:nss"), 2053 | false, 2054 | nil, 2055 | "", 2056 | "", 2057 | fmt.Sprintf(err8141Identifier, 6), 2058 | false, 2059 | }, // NID must not start with experimental URN namespace of RFC3406 2060 | { 2061 | []byte("urn:xn--:nss"), 2062 | false, 2063 | nil, 2064 | "", 2065 | "", 2066 | fmt.Sprintf(err8141Identifier, 8), 2067 | false, 2068 | }, // NID must not start with "xn" followed by 2 dashes 2069 | { 2070 | []byte("urn:ss--:nss"), 2071 | false, 2072 | nil, 2073 | "", 2074 | "", 2075 | fmt.Sprintf(err8141Identifier, 8), 2076 | false, 2077 | }, // NID must not start with 2 chars followed by 2 dashes 2078 | { 2079 | []byte("urn:1E--:nss"), 2080 | false, 2081 | nil, 2082 | "", 2083 | "", 2084 | fmt.Sprintf(err8141Identifier, 8), 2085 | false, 2086 | }, // NID must not start with 2 chars followed by 2 dashes 2087 | { 2088 | []byte("urn:ex:ex?+a?+"), 2089 | false, 2090 | nil, 2091 | "", 2092 | "", 2093 | fmt.Sprintf(err8141RComponentStart, 14), 2094 | false, 2095 | }, // r_component containing ?+ 2096 | { 2097 | []byte("urn:ex:ex?+"), 2098 | false, 2099 | nil, 2100 | "", 2101 | "", 2102 | fmt.Sprintf(err8141MalformedRComp, 11), 2103 | false, 2104 | }, // empty r_component 2105 | { 2106 | []byte("urn:ex:ex?=a?="), 2107 | false, 2108 | nil, 2109 | "", 2110 | "", 2111 | fmt.Sprintf(err8141QComponentStart, 14), 2112 | false, 2113 | }, // q_component containing ?= 2114 | { 2115 | []byte("urn:example:CamelCase1/406/47452/2?="), 2116 | false, 2117 | nil, 2118 | "", 2119 | "", 2120 | fmt.Sprintf(err8141MalformedQComp, 36), 2121 | false, 2122 | }, // empty q_component 2123 | { 2124 | []byte("urn:ex:ex?+rcomponent?+rcomponent?=qcomponent"), 2125 | false, 2126 | nil, 2127 | "", 2128 | "", 2129 | fmt.Sprintf(err8141RComponentStart, 23), 2130 | false, 2131 | }, // r_component containing ?+ followed by q_component 2132 | { 2133 | []byte("urn:ex:ex?+rcomponent?+rcomponent?="), 2134 | false, 2135 | nil, 2136 | "", 2137 | "", 2138 | fmt.Sprintf(err8141RComponentStart, 23), 2139 | false, 2140 | }, // r_component containing ?+ followed by empty q_component 2141 | { 2142 | []byte("urn:ex:ex?+rcomponent?=qcomponent?=q"), 2143 | false, 2144 | nil, 2145 | "", 2146 | "", 2147 | fmt.Sprintf(err8141QComponentStart, 35), 2148 | false, 2149 | }, // r_component followed by q_component containing ?+ 2150 | { 2151 | []byte("urn:ex:ex?+?=q"), 2152 | false, 2153 | nil, 2154 | "", 2155 | "", 2156 | fmt.Sprintf(err8141MalformedRComp, 12), 2157 | false, 2158 | }, // empty r_component followed by q_component 2159 | { 2160 | []byte("urn:ex:ex?+/"), 2161 | false, 2162 | nil, 2163 | "", 2164 | "", 2165 | fmt.Sprintf(err8141MalformedRComp, 11), 2166 | false, 2167 | }, // r_component starting with / 2168 | { 2169 | []byte("urn:ex:ex?+?"), 2170 | false, 2171 | nil, 2172 | "", 2173 | "", 2174 | fmt.Sprintf(err8141MalformedRComp, 12), 2175 | false, 2176 | }, // r_component starting with / 2177 | { 2178 | []byte("urn:ex:ex?=/"), 2179 | false, 2180 | nil, 2181 | "", 2182 | "", 2183 | fmt.Sprintf(err8141MalformedQComp, 11), 2184 | false, 2185 | }, // q_component starting with / 2186 | { 2187 | []byte("urn:ex:ex?=?"), 2188 | false, 2189 | nil, 2190 | "", 2191 | "", 2192 | fmt.Sprintf(err8141MalformedQComp, 12), 2193 | false, 2194 | }, // q_component starting with / 2195 | { 2196 | []byte("urn:mm:/"), 2197 | false, 2198 | nil, 2199 | "", 2200 | "", 2201 | fmt.Sprintf(err8141SpecificString, 7), 2202 | false, 2203 | }, // / not in first position in the NSS part 2204 | { 2205 | []byte("urn:mm:?"), 2206 | false, 2207 | nil, 2208 | "", 2209 | "", 2210 | fmt.Sprintf(err8141SpecificString, 7), 2211 | false, 2212 | }, // ? not in first position in the NSS part 2213 | { 2214 | []byte("urn:123456789-1234567890-abcdefghilmn:o"), 2215 | false, 2216 | nil, 2217 | "", 2218 | "", 2219 | fmt.Sprintf(err8141Identifier, 36), 2220 | false, 2221 | }, // too long NID 2222 | { 2223 | []byte("urn:"), 2224 | false, 2225 | nil, 2226 | "", 2227 | "", 2228 | fmt.Sprintf(err8141Identifier, 4), 2229 | false, 2230 | }, 2231 | { 2232 | []byte("urn::"), 2233 | false, 2234 | nil, 2235 | "", 2236 | "", 2237 | fmt.Sprintf(err8141Identifier, 4), 2238 | false, 2239 | }, 2240 | { 2241 | []byte("urn:aa:"), 2242 | false, 2243 | nil, 2244 | "", 2245 | "", 2246 | fmt.Sprintf(err8141SpecificString, 7), 2247 | false, 2248 | }, 2249 | { 2250 | []byte("urn:a"), 2251 | false, 2252 | nil, 2253 | "", 2254 | "", 2255 | fmt.Sprintf(err8141Identifier, 5), 2256 | false, 2257 | }, 2258 | { 2259 | []byte("urn:ex:ex?=%"), 2260 | false, 2261 | nil, 2262 | "", 2263 | "", 2264 | fmt.Sprintf(errHex, 12), 2265 | false, 2266 | }, // q_component containing incomplete percent encoded chars 2267 | { 2268 | []byte("urn:ex:ex?+%"), 2269 | false, 2270 | nil, 2271 | "", 2272 | "", 2273 | fmt.Sprintf(errHex, 12), 2274 | false, 2275 | }, // r_component containing incomplete percent encoded chars 2276 | { 2277 | []byte("urn:ex:ex?=something#%"), 2278 | false, 2279 | nil, 2280 | "", 2281 | "", 2282 | fmt.Sprintf(errHex, 22), 2283 | false, 2284 | }, // fragment containing incomplete percent encoded chars 2285 | { 2286 | []byte("urn:example%:test"), 2287 | false, 2288 | nil, 2289 | "", 2290 | "", 2291 | fmt.Sprintf(err8141Identifier, 11), 2292 | false, 2293 | }, 2294 | { 2295 | []byte(`urn:"`), 2296 | false, 2297 | nil, 2298 | "", 2299 | "", 2300 | fmt.Sprintf(err8141Identifier, 4), 2301 | false, 2302 | }, 2303 | { 2304 | []byte(`urn:a1{}`), 2305 | false, 2306 | nil, 2307 | "", 2308 | "", 2309 | fmt.Sprintf(err8141Identifier, 6), 2310 | false, 2311 | }, 2312 | { 2313 | []byte(`u`), 2314 | false, 2315 | nil, 2316 | "", 2317 | "", 2318 | fmt.Sprintf(errPrefix, 1), 2319 | false, 2320 | }, 2321 | { 2322 | []byte(`ur`), 2323 | false, 2324 | nil, 2325 | "", 2326 | "", 2327 | fmt.Sprintf(errPrefix, 2), 2328 | false, 2329 | }, 2330 | { 2331 | []byte(`urn`), 2332 | false, 2333 | nil, 2334 | "", 2335 | "", 2336 | fmt.Sprintf(errPrefix, 3), 2337 | false, 2338 | }, 2339 | } 2340 | -------------------------------------------------------------------------------- /tools/removecomments/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/leodido/go-urn/tools/removecomments 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /tools/removecomments/go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leodido/go-urn/d725923fe33ce69c89b9e2033d069099b498224f/tools/removecomments/go.sum -------------------------------------------------------------------------------- /tools/removecomments/removecomments.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "regexp" 7 | ) 8 | 9 | // commentLineRegex matches a comment line and its newline characters. A 10 | // comment line, for the purposes of this function, begins with optional 11 | // whitespace followed by "//line" then any other text. 12 | // 13 | // optional newline character ---------------------------------+++ 14 | // optional carriage return --------------------------------+++||| 15 | // end of line --------------------------------------------+|||||| 16 | // wildcard ---------------------------------------------++||||||| 17 | // the literal word "line" --------------------------++++||||||||| 18 | // comment delimiters -----------------------------++||||||||||||| 19 | // one more more whitespace characters ---------+++||||||||||||||| 20 | // beginning of line --------------------------+|||||||||||||||||| 21 | // allow multiline matching ---------------++++||||||||||||||||||| 22 | // ||||||||||||||||||||||| 23 | var commentLineRegex = regexp.MustCompile(`(?m)^\s*//line.*$\r?\n?`) 24 | 25 | func removeComments(src []byte) []byte { 26 | return commentLineRegex.ReplaceAllLiteral(src, []byte("")) 27 | } 28 | 29 | func removeCommentsFromFile(path string) error { 30 | content, err := os.ReadFile(path) 31 | if err != nil { 32 | return err 33 | } 34 | updated := removeComments(content) 35 | return os.WriteFile(path, updated, 0) 36 | } 37 | 38 | func exitWithError(msg string) { 39 | err := fmt.Sprintf("error: %s\n", msg) 40 | fmt.Fprintln(os.Stderr, err) 41 | os.Exit(1) 42 | } 43 | 44 | func main() { 45 | if len(os.Args) != 2 { 46 | exitWithError("must be called with the file path as the only argument") 47 | } 48 | path := os.Args[1] 49 | if err := removeCommentsFromFile(path); err != nil { 50 | exitWithError(err.Error()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tools/removecomments/removecomments_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestRemoveComments(t *testing.T) { 9 | txt := `//line this should be deleted 10 | 11 | This is a test file 12 | 13 | it would contain go code 14 | 15 | // and comments 16 | // and comments after spaces 17 | // and comments after tabs 18 | //line but not this comment 19 | //line or this one after spaces 20 | //line or this one after tabs 21 | 22 | and that is all 23 | //line this should be deleted` 24 | 25 | expected := ` 26 | This is a test file 27 | 28 | it would contain go code 29 | 30 | // and comments 31 | // and comments after spaces 32 | // and comments after tabs 33 | 34 | and that is all 35 | ` 36 | 37 | s := removeComments([]byte(txt)) 38 | if string(s) != expected { 39 | fmt.Printf("expected length: %d, got %d\n", len(expected), len(s)) 40 | t.Fatalf("expected:\n%s\ngot:\n%s", expected, s) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tools/snake2camel/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/leodido/go-urn/tools/snake2camel 2 | 3 | go 1.21.6 4 | -------------------------------------------------------------------------------- /tools/snake2camel/go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leodido/go-urn/d725923fe33ce69c89b9e2033d069099b498224f/tools/snake2camel/go.sum -------------------------------------------------------------------------------- /tools/snake2camel/snake2camel.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | func exitWithError(msg string) { 11 | err := fmt.Sprintf("error: %s\n", msg) 12 | fmt.Fprintln(os.Stderr, err) 13 | os.Exit(1) 14 | } 15 | 16 | func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) (string, int) { 17 | result := "" 18 | lastIndex := 0 19 | 20 | for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) { 21 | groups := []string{} 22 | for i := 0; i < len(v); i += 2 { 23 | if v[i] == -1 || v[i+1] == -1 { 24 | groups = append(groups, "") 25 | } else { 26 | groups = append(groups, str[v[i]:v[i+1]]) 27 | } 28 | } 29 | 30 | result += str[lastIndex:v[0]] + repl(groups) 31 | lastIndex = v[1] 32 | } 33 | 34 | return result + str[lastIndex:], lastIndex 35 | } 36 | 37 | func snake2camel(content []byte) []byte { 38 | re := regexp.MustCompile(`(.*)([a-z]+[0-9]*)_([a-zA-Z0-9])(.*)`) 39 | res := string(content) 40 | last := -1 41 | 42 | for last != 0 { 43 | res, last = replaceAllStringSubmatchFunc(re, res, func(groups []string) string { 44 | return groups[1] + groups[2] + strings.ToUpper(groups[3]) + groups[4] 45 | }) 46 | } 47 | 48 | return []byte(res) 49 | } 50 | 51 | func snake2camelFile(path string) error { 52 | content, err := os.ReadFile(path) 53 | if err != nil { 54 | return err 55 | } 56 | updated := snake2camel(content) 57 | 58 | return os.WriteFile(path, updated, 0) 59 | } 60 | 61 | func main() { 62 | if len(os.Args) != 2 { 63 | exitWithError("must be called with the file path as the only argument") 64 | } 65 | path := os.Args[1] 66 | if err := snake2camelFile(path); err != nil { 67 | exitWithError(err.Error()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tools/snake2camel/snake2camel_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestSnake2CamelNoReplace(t *testing.T) { 9 | txt := `const start int = 1` 10 | 11 | exp := `const start int = 1` 12 | 13 | got := snake2camel([]byte(txt)) 14 | if string(got) != exp { 15 | fmt.Printf("expected length: %d, got %d\n", len(exp), len(got)) 16 | t.Fatalf("expected:\n%s\ngot:\n%s", exp, got) 17 | } 18 | } 19 | 20 | func TestSnake2CamelOneUnderscore(t *testing.T) { 21 | txt := `const en_urn int = 5 22 | const en_scim int = 44 23 | const en_fail int = 81 24 | const en_main int = 1` 25 | 26 | exp := `const enUrn int = 5 27 | const enScim int = 44 28 | const enFail int = 81 29 | const enMain int = 1` 30 | 31 | got := snake2camel([]byte(txt)) 32 | if string(got) != exp { 33 | fmt.Printf("expected length: %d, got %d\n", len(exp), len(got)) 34 | t.Fatalf("expected:\n%s\ngot:\n%s", exp, got) 35 | } 36 | } 37 | 38 | func TestSnake2CamelMoreUnderscores(t *testing.T) { 39 | txt := `if (m.p) == (m.pe) { 40 | goto _test_eof 41 | } 42 | switch m.cs { 43 | case 1: 44 | goto st_case_1 45 | }` 46 | 47 | exp := `if (m.p) == (m.pe) { 48 | goto _testEof 49 | } 50 | switch m.cs { 51 | case 1: 52 | goto stCase1 53 | }` 54 | 55 | got := snake2camel([]byte(txt)) 56 | if string(got) != exp { 57 | fmt.Printf("expected length: %d, got %d\n", len(exp), len(got)) 58 | t.Fatalf("expected:\n%s\ngot:\n%s", exp, got) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /urn.go: -------------------------------------------------------------------------------- 1 | package urn 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | const errInvalidURN = "invalid URN: %s" 10 | 11 | // URN represents an Uniform Resource Name. 12 | // 13 | // The general form represented is: 14 | // 15 | // urn:: 16 | // 17 | // Details at https://tools.ietf.org/html/rfc2141. 18 | type URN struct { 19 | prefix string // Static prefix. Equal to "urn" when empty. 20 | ID string // Namespace identifier (NID) 21 | SS string // Namespace specific string (NSS) 22 | norm string // Normalized namespace specific string 23 | kind Kind 24 | scim *SCIM 25 | rComponent string // RFC8141 26 | qComponent string // RFC8141 27 | fComponent string // RFC8141 28 | rStart bool // RFC8141 29 | qStart bool // RFC8141 30 | tolower []int 31 | } 32 | 33 | // Normalize turns the receiving URN into its norm version. 34 | // 35 | // Which means: lowercase prefix, lowercase namespace identifier, and immutate namespace specific string chars (except tokens which are lowercased). 36 | func (u *URN) Normalize() *URN { 37 | return &URN{ 38 | prefix: "urn", 39 | ID: strings.ToLower(u.ID), 40 | SS: u.norm, 41 | // rComponent: u.rComponent, 42 | // qComponent: u.qComponent, 43 | // fComponent: u.fComponent, 44 | } 45 | } 46 | 47 | // Equal checks the lexical equivalence of the current URN with another one. 48 | func (u *URN) Equal(x *URN) bool { 49 | if x == nil { 50 | return false 51 | } 52 | nu := u.Normalize() 53 | nx := x.Normalize() 54 | 55 | return nu.prefix == nx.prefix && nu.ID == nx.ID && nu.SS == nx.SS 56 | } 57 | 58 | // String reassembles the URN into a valid URN string. 59 | // 60 | // This requires both ID and SS fields to be non-empty. 61 | // Otherwise it returns an empty string. 62 | // 63 | // Default URN prefix is "urn". 64 | func (u *URN) String() string { 65 | var res string 66 | if u.ID != "" && u.SS != "" { 67 | if u.prefix == "" { 68 | res += "urn" 69 | } 70 | res += u.prefix + ":" + u.ID + ":" + u.SS 71 | if u.rComponent != "" { 72 | res += "?+" + u.rComponent 73 | } 74 | if u.qComponent != "" { 75 | res += "?=" + u.qComponent 76 | } 77 | if u.fComponent != "" { 78 | res += "#" + u.fComponent 79 | } 80 | } 81 | 82 | return res 83 | } 84 | 85 | // Parse is responsible to create an URN instance from a byte array matching the correct URN syntax (RFC 2141). 86 | func Parse(u []byte, options ...Option) (*URN, bool) { 87 | urn, err := NewMachine(options...).Parse(u) 88 | if err != nil { 89 | return nil, false 90 | } 91 | 92 | return urn, true 93 | } 94 | 95 | // MarshalJSON marshals the URN to JSON string form (e.g. `"urn:oid:1.2.3.4"`). 96 | func (u URN) MarshalJSON() ([]byte, error) { 97 | return json.Marshal(u.String()) 98 | } 99 | 100 | // UnmarshalJSON unmarshals a URN from JSON string form (e.g. `"urn:oid:1.2.3.4"`). 101 | func (u *URN) UnmarshalJSON(bytes []byte) error { 102 | var str string 103 | if err := json.Unmarshal(bytes, &str); err != nil { 104 | return err 105 | } 106 | if value, ok := Parse([]byte(str)); !ok { 107 | return fmt.Errorf(errInvalidURN, str) 108 | } else { 109 | *u = *value 110 | } 111 | 112 | return nil 113 | } 114 | 115 | func (u *URN) IsSCIM() bool { 116 | return u.kind == RFC7643 117 | } 118 | 119 | func (u *URN) SCIM() *SCIM { 120 | if u.kind != RFC7643 { 121 | return nil 122 | } 123 | 124 | return u.scim 125 | } 126 | 127 | func (u *URN) RFC() Kind { 128 | return u.kind 129 | } 130 | 131 | func (u *URN) FComponent() string { 132 | return u.fComponent 133 | } 134 | 135 | func (u *URN) QComponent() string { 136 | return u.qComponent 137 | } 138 | 139 | func (u *URN) RComponent() string { 140 | return u.rComponent 141 | } 142 | -------------------------------------------------------------------------------- /urn8141.go: -------------------------------------------------------------------------------- 1 | package urn 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | const errInvalidURN8141 = "invalid URN per RFC 8141: %s" 9 | 10 | type URN8141 struct { 11 | *URN 12 | } 13 | 14 | func (u URN8141) MarshalJSON() ([]byte, error) { 15 | return json.Marshal(u.String()) 16 | } 17 | 18 | func (u *URN8141) UnmarshalJSON(bytes []byte) error { 19 | var str string 20 | if err := json.Unmarshal(bytes, &str); err != nil { 21 | return err 22 | } 23 | if value, ok := Parse([]byte(str), WithParsingMode(RFC8141Only)); !ok { 24 | return fmt.Errorf(errInvalidURN8141, str) 25 | } else { 26 | *u = URN8141{value} 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /urn8141_test.go: -------------------------------------------------------------------------------- 1 | package urn 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestURN8141JSONMarshaling(t *testing.T) { 12 | t.Run("roundtrip", func(t *testing.T) { 13 | // Marshal 14 | expected := URN8141{ 15 | URN: &URN{ 16 | ID: "lex", 17 | SS: "it:ministero.giustizia:decreto:1992-07-24;358~art5", 18 | rComponent: "r", 19 | qComponent: "q%D0", 20 | fComponent: "frag", 21 | }, 22 | } 23 | bytes, err := json.Marshal(expected) 24 | if !assert.NoError(t, err) { 25 | return 26 | } 27 | require.Equal(t, `"urn:lex:it:ministero.giustizia:decreto:1992-07-24;358~art5?+r?=q%D0#frag"`, string(bytes)) 28 | // Unmarshal 29 | var got URN8141 30 | err = json.Unmarshal(bytes, &got) 31 | if !assert.NoError(t, err) { 32 | return 33 | } 34 | assert.Equal(t, expected.String(), got.String()) 35 | assert.Equal(t, expected.fComponent, got.FComponent()) 36 | assert.Equal(t, expected.qComponent, got.QComponent()) 37 | assert.Equal(t, expected.rComponent, got.RComponent()) 38 | }) 39 | 40 | t.Run("invalid URN", func(t *testing.T) { 41 | var actual URN8141 42 | err := json.Unmarshal([]byte(`"not a URN"`), &actual) 43 | assert.EqualError(t, err, "invalid URN per RFC 8141: not a URN") 44 | }) 45 | 46 | t.Run("empty", func(t *testing.T) { 47 | var actual URN8141 48 | err := actual.UnmarshalJSON(nil) 49 | assert.EqualError(t, err, "unexpected end of JSON input") 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /urn_test.go: -------------------------------------------------------------------------------- 1 | package urn 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDefaultPrefixWhenString(t *testing.T) { 11 | u := &URN{ 12 | ID: "pippo", 13 | SS: "pluto", 14 | } 15 | 16 | assert.Equal(t, "urn:pippo:pluto", u.String()) 17 | } 18 | 19 | func TestParseSignature(t *testing.T) { 20 | urn, ok := Parse([]byte(``)) 21 | assert.Nil(t, urn) 22 | assert.False(t, ok) 23 | } 24 | 25 | func TestJSONMarshaling(t *testing.T) { 26 | t.Run("roundtrip", func(t *testing.T) { 27 | // Marshal 28 | expected := URN{ID: "oid", SS: "1.2.3.4"} 29 | bytes, err := json.Marshal(expected) 30 | if !assert.NoError(t, err) { 31 | return 32 | } 33 | // Unmarshal 34 | var actual URN 35 | err = json.Unmarshal(bytes, &actual) 36 | if !assert.NoError(t, err) { 37 | return 38 | } 39 | assert.Equal(t, expected.String(), actual.String()) 40 | }) 41 | 42 | t.Run("invalid URN", func(t *testing.T) { 43 | var actual URN 44 | err := json.Unmarshal([]byte(`"not a URN"`), &actual) 45 | assert.EqualError(t, err, "invalid URN: not a URN") 46 | }) 47 | 48 | t.Run("empty", func(t *testing.T) { 49 | var actual URN 50 | err := actual.UnmarshalJSON(nil) 51 | assert.EqualError(t, err, "unexpected end of JSON input") 52 | }) 53 | } 54 | --------------------------------------------------------------------------------