wfn/uri.go (314 lines of code) (raw):

// Copyright (c) Facebook, Inc. and its affiliates. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package wfn import ( "fmt" "strconv" "strings" ) // BindToURI binds WFN to URI func (a Attributes) BindToURI() string { var parts []string for i, v := range []string{ a.Part, a.Vendor, a.Product, a.Version, a.Update, "", a.Language, } { if i != 5 { // other than edition parts = append(parts, bindValueURI(v)) continue } edParts := make([]string, 5) allNAs := true for i, v2 := range []string{a.Edition, a.SWEdition, a.TargetSW, a.TargetHW, a.Other} { edParts[i] = bindValueURI(v2) if edParts[i] != "-" { allNAs = false } } if allNAs { parts = append(parts, "-") } else { parts = append(parts, pack(edParts)) } } // empty elements at the end of the URI should be omitted for i := len(parts) - 1; i >= 0; i-- { if parts[i] != "" { break } parts = parts[:i] } return uriPrefix + strings.Join(parts, ":") } // UnbindURI loads WFN from URI func UnbindURI(s string) (*Attributes, error) { if !strings.HasPrefix(s, uriPrefix) { return nil, fmt.Errorf("unbind uri: bad prefix in URI %q", s) } s = strings.ToLower(s[len(uriPrefix):]) // reject schema prefix + normalize attr := Attributes{} var err error for i, partN := 0, 0; i < len(s); i, partN = i+1, partN+1 { switch partN { case 0: attr.Part, i, err = unbindValueURIAtTill(s, i, ':') case 1: attr.Vendor, i, err = unbindValueURIAtTill(s, i, ':') case 2: attr.Product, i, err = unbindValueURIAtTill(s, i, ':') case 3: attr.Version, i, err = unbindValueURIAtTill(s, i, ':') case 4: attr.Update, i, err = unbindValueURIAtTill(s, i, ':') case 5: if s[i] != '~' { attr.Edition, i, err = unbindValueURIAtTill(s, i, ':') break } i++ edition23: for subpartN := 0; i < len(s); i, subpartN = i+1, subpartN+1 { switch subpartN { case 0: attr.Edition, i, err = unbindValueURIAtTill(s, i, '~') case 1: attr.SWEdition, i, err = unbindValueURIAtTill(s, i, '~') case 2: attr.TargetSW, i, err = unbindValueURIAtTill(s, i, '~') case 3: attr.TargetHW, i, err = unbindValueURIAtTill(s, i, '~') case 4: attr.Other, i, err = unbindValueURIAtTill(s, i, ':') default: break edition23 } } case 6: attr.Language, i, err = unbindValueURIAtTill(s, i, ':') } if err != nil { return nil, fmt.Errorf("unbind uri: %v", err) } } return &attr, nil } func pack(ss []string) string { compat := true for _, s := range ss[1:] { if s != "" { compat = false break } } if compat { return ss[0] } return "~" + strings.Join(ss, "~") } // Scans an input string s and applies the following transformations: // - pass alphanumeric characters thru untouched // - percent-encode quoted non-alphanumerics as needed // - unquoted special characters are mapped to their special forms. func bindValueURI(s string) string { var out []byte switch s { case NA: return "-" case Any: return "" } for i := 0; i < len(s); i++ { b := byte(s[i]) if b >= '0' && b <= '9' || b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b == '_' { // alnum + '_' pass untouched out = append(out, b) } else if b == '\\' { // percent-encode escaped characters // sanity check should be done during unbinding, so here we silently skip all // illegal characters i++ if i == len(s) { break } out = append(out, pctEncode(s[i])...) } else if b == '?' { // unquoted '?' -> "%01" out = append(out, '%', '0', '1') } else if b == '*' { // unquoted '*' -> "%02" out = append(out, '%', '0', '2') } } return string(out) } func unbindValueURIAtTill(s string, at int, till byte) (string, int, error) { if at >= len(s) || s[at] == till { return Any, at, nil } if s[at] == '-' { return NA, at + 1, nil } out := make([]byte, 0, len(s)*2) // assume the worst embedded := false i := at loop: for ; i < len(s); i++ { switch s[i] { case till: break loop case '%': if i+3 > len(s) { return "", i, fmt.Errorf("unbind URI attribute: illegal percent-encoded value at %d: %q", i, s[i:]) } codeStr := string(s[i : i+3]) code, err := strconv.ParseInt(string(s[i+1:i+3]), 16, 8) if err != nil { return "", i, fmt.Errorf("unbind URI attribute: illegal percent-encoded value at %d: %q", i, s[i+1:i+3]) } if code == 0x1 || code == 0x2 { if !(i == at || i == len(s)-3 || s[i+3] == till || // at the beginning or at the end of the string (!embedded && i > 2 && s[i-3:i] == codeStr || // not embedded and preceded by the same symbol (embedded && i+6 < len(s) && s[i+3:i+6] == codeStr))) { // embedded and followed by the same symbol return "", i, fmt.Errorf("unbind URI attribute: %%%02d is embedded into string %q", code, s) } switch code { case 0x1: out = append(out, '?') case 0x2: out = append(out, '*') } i += 2 break } switch code { case 0x21: out = append(out, '\\', '!') case 0x22: out = append(out, '\\', '"') case 0x23: out = append(out, '\\', '#') case 0x24: out = append(out, '\\', '$') case 0x25: out = append(out, '\\', '%') case 0x26: out = append(out, '\\', '&') case 0x27: out = append(out, '\\', '\'') case 0x28: out = append(out, '\\', '(') case 0x29: out = append(out, '\\', ')') case 0x2a: out = append(out, '\\', '*') case 0x2b: out = append(out, '\\', '+') case 0x2c: out = append(out, '\\', ',') case 0x2f: out = append(out, '\\', '/') case 0x3a: out = append(out, '\\', ':') case 0x3b: out = append(out, '\\', ';') case 0x3c: out = append(out, '\\', '<') case 0x3d: out = append(out, '\\', '=') case 0x3e: out = append(out, '\\', '>') case 0x3f: out = append(out, '\\', '?') case 0x40: out = append(out, '\\', '@') case 0x5b: out = append(out, '\\', '[') case 0x5c: out = append(out, '\\', '\\') case 0x5d: out = append(out, '\\', ']') case 0x5e: out = append(out, '\\', '^') case 0x60: out = append(out, '\\', '`') case 0x7b: out = append(out, '\\', '{') case 0x7c: out = append(out, '\\', '|') case 0x7d: out = append(out, '\\', '}') case 0x7e: out = append(out, '\\', '~') default: return "", i, fmt.Errorf("unbind URI attribute: illegal percent-encoded value %q", s[i+1:i+3]) } i += 2 embedded = true case '.', '-', '~': out = append(out, '\\', s[i]) embedded = true default: out = append(out, s[i]) embedded = true } } return string(out), i, nil } func pctEncode(b byte) []byte { switch b { case '!': return []byte("%21") case '"': return []byte("%22") case '#': return []byte("%23") case '$': return []byte("%24") case '%': return []byte("%25") case '&': return []byte("%26") case '\'': return []byte("%27") case '(': return []byte("%28") case ')': return []byte("%29") case '*': return []byte("%2a") case '+': return []byte("%2b") case ',': return []byte("%2c") case '-': return []byte("-") // bound without encoding case '.': return []byte(".") // bound without encoding case '/': return []byte("%2f") case ':': return []byte("%3a") case ';': return []byte("%3b") case '<': return []byte("%3c") case '=': return []byte("%3d") case '>': return []byte("%3e") case '?': return []byte("%3f") case '@': return []byte("%40") case '[': return []byte("%5b") case '\\': return []byte("%5c") case ']': return []byte("%5d") case '^': return []byte("%5e") case '`': return []byte("%60") case '{': return []byte("%7b") case '|': return []byte("%7c") case '}': return []byte("%7d") case '~': return []byte("%7e") default: return []byte{b} } }