Skip to content

Commit c4ad4d4

Browse files
rhaferJosh Hawn
and
Josh Hawn
authored
Add String() methods to DN and its subtypes (#386)
* Add String() methods to DN and its subtypes This patch adds `String() string` methods to each of the following types: - DN - RelativeDN - AttributeTypeAndValue So that a `*DN` implements the `fmt.Stringer` interface. These methods also produce normalized strings: Attribute Type and Value are lowercased and joined with a "=" character while multiple attributes of a Relative DN are sorted lexicographically before being joined witha "+" character. This allows one to use the string representation of a DN as a map key and ensure that two DNs which `Equal()` eachother would have the same `String()` value. Docker-DCO-1.1-Signed-off-by: Josh Hawn <josh.hawn@docker.com> (github: jlhawn) * Mirror DN String() methods to v3 folder Co-authored-by: Josh Hawn <josh.hawn@docker.com>
1 parent f78b5ac commit c4ad4d4

File tree

4 files changed

+179
-9
lines changed

4 files changed

+179
-9
lines changed

dn.go

+80
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
enchex "encoding/hex"
66
"errors"
77
"fmt"
8+
"sort"
89
"strings"
910

1011
ber "github.com/go-asn1-ber/asn1-ber"
@@ -18,16 +19,95 @@ type AttributeTypeAndValue struct {
1819
Value string
1920
}
2021

22+
// String returns a normalized string representation of this attribute type and
23+
// value pair which is the a lowercased join of the Type and Value with a "=".
24+
func (a *AttributeTypeAndValue) String() string {
25+
return strings.ToLower(a.Type) + "=" + a.encodeValue()
26+
}
27+
28+
func (a *AttributeTypeAndValue) encodeValue() string {
29+
// Normalize the value first.
30+
// value := strings.ToLower(a.Value)
31+
value := a.Value
32+
33+
encodedBuf := bytes.Buffer{}
34+
35+
escapeChar := func(c byte) {
36+
encodedBuf.WriteByte('\\')
37+
encodedBuf.WriteByte(c)
38+
}
39+
40+
escapeHex := func(c byte) {
41+
encodedBuf.WriteByte('\\')
42+
encodedBuf.WriteString(enchex.EncodeToString([]byte{c}))
43+
}
44+
45+
for i := 0; i < len(value); i++ {
46+
char := value[i]
47+
if i == 0 && char == ' ' || char == '#' {
48+
// Special case leading space or number sign.
49+
escapeChar(char)
50+
continue
51+
}
52+
if i == len(value)-1 && char == ' ' {
53+
// Special case trailing space.
54+
escapeChar(char)
55+
continue
56+
}
57+
58+
switch char {
59+
case '"', '+', ',', ';', '<', '>', '\\':
60+
// Each of these special characters must be escaped.
61+
escapeChar(char)
62+
continue
63+
}
64+
65+
if char < ' ' || char > '~' {
66+
// All special character escapes are handled first
67+
// above. All bytes less than ASCII SPACE and all bytes
68+
// greater than ASCII TILDE must be hex-escaped.
69+
escapeHex(char)
70+
continue
71+
}
72+
73+
// Any other character does not require escaping.
74+
encodedBuf.WriteByte(char)
75+
}
76+
77+
return encodedBuf.String()
78+
}
79+
2180
// RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514
2281
type RelativeDN struct {
2382
Attributes []*AttributeTypeAndValue
2483
}
2584

85+
// String returns a normalized string representation of this relative DN which
86+
// is the a join of all attributes (sorted in increasing order) with a "+".
87+
func (r *RelativeDN) String() string {
88+
attrs := make([]string, len(r.Attributes))
89+
for i := range r.Attributes {
90+
attrs[i] = r.Attributes[i].String()
91+
}
92+
sort.Strings(attrs)
93+
return strings.Join(attrs, "+")
94+
}
95+
2696
// DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514
2797
type DN struct {
2898
RDNs []*RelativeDN
2999
}
30100

101+
// String returns a normalized string representation of this DN which is the
102+
// join of all relative DNs with a ",".
103+
func (d *DN) String() string {
104+
rdns := make([]string, len(d.RDNs))
105+
for i := range d.RDNs {
106+
rdns[i] = d.RDNs[i].String()
107+
}
108+
return strings.Join(rdns, ",")
109+
}
110+
31111
// ParseDN returns a distinguishedName or an error.
32112
// The function respects https://tools.ietf.org/html/rfc4514
33113
func ParseDN(str string) (*DN, error) {

dn_test.go

+9-4
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ func TestDNEqual(t *testing.T) {
148148
},
149149
// Difference in leading/trailing chars is ignored
150150
{
151-
"cn=John Doe, ou=People, dc=sun.com",
152-
"cn=John Doe,ou=People,dc=sun.com",
151+
"cn=\\ John\\20Doe, ou=People, dc=sun.com",
152+
"cn= \\ John Doe,ou=People,dc=sun.com",
153153
true,
154154
},
155155
// Difference in values is significant
@@ -174,11 +174,16 @@ func TestDNEqual(t *testing.T) {
174174
continue
175175
}
176176
if expected, actual := tc.Equal, a.Equal(b); expected != actual {
177-
t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual)
177+
t.Errorf("%d: when comparing %q and %q expected %v, got %v", i, a, b, expected, actual)
178178
continue
179179
}
180180
if expected, actual := tc.Equal, b.Equal(a); expected != actual {
181-
t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual)
181+
t.Errorf("%d: when comparing %q and %q expected %v, got %v", i, a, b, expected, actual)
182+
continue
183+
}
184+
185+
if expected, actual := a.Equal(b), a.String() == b.String(); expected != actual {
186+
t.Errorf("%d: when asserting string comparison of %q and %q expected equal %v, got %v", i, a, b, expected, actual)
182187
continue
183188
}
184189
}

v3/dn.go

+81-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
enchex "encoding/hex"
66
"errors"
77
"fmt"
8+
"sort"
89
"strings"
910

1011
ber "github.com/go-asn1-ber/asn1-ber"
@@ -18,16 +19,95 @@ type AttributeTypeAndValue struct {
1819
Value string
1920
}
2021

22+
// String returns a normalized string representation of this attribute type and
23+
// value pair which is the a lowercased join of the Type and Value with a "=".
24+
func (a *AttributeTypeAndValue) String() string {
25+
return strings.ToLower(a.Type) + "=" + a.encodeValue()
26+
}
27+
28+
func (a *AttributeTypeAndValue) encodeValue() string {
29+
// Normalize the value first.
30+
// value := strings.ToLower(a.Value)
31+
value := a.Value
32+
33+
encodedBuf := bytes.Buffer{}
34+
35+
escapeChar := func(c byte) {
36+
encodedBuf.WriteByte('\\')
37+
encodedBuf.WriteByte(c)
38+
}
39+
40+
escapeHex := func(c byte) {
41+
encodedBuf.WriteByte('\\')
42+
encodedBuf.WriteString(enchex.EncodeToString([]byte{c}))
43+
}
44+
45+
for i := 0; i < len(value); i++ {
46+
char := value[i]
47+
if i == 0 && char == ' ' || char == '#' {
48+
// Special case leading space or number sign.
49+
escapeChar(char)
50+
continue
51+
}
52+
if i == len(value)-1 && char == ' ' {
53+
// Special case trailing space.
54+
escapeChar(char)
55+
continue
56+
}
57+
58+
switch char {
59+
case '"', '+', ',', ';', '<', '>', '\\':
60+
// Each of these special characters must be escaped.
61+
escapeChar(char)
62+
continue
63+
}
64+
65+
if char < ' ' || char > '~' {
66+
// All special character escapes are handled first
67+
// above. All bytes less than ASCII SPACE and all bytes
68+
// greater than ASCII TILDE must be hex-escaped.
69+
escapeHex(char)
70+
continue
71+
}
72+
73+
// Any other character does not require escaping.
74+
encodedBuf.WriteByte(char)
75+
}
76+
77+
return encodedBuf.String()
78+
}
79+
2180
// RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514
2281
type RelativeDN struct {
2382
Attributes []*AttributeTypeAndValue
2483
}
2584

85+
// String returns a normalized string representation of this relative DN which
86+
// is the a join of all attributes (sorted in increasing order) with a "+".
87+
func (r *RelativeDN) String() string {
88+
attrs := make([]string, len(r.Attributes))
89+
for i := range r.Attributes {
90+
attrs[i] = r.Attributes[i].String()
91+
}
92+
sort.Strings(attrs)
93+
return strings.Join(attrs, "+")
94+
}
95+
2696
// DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514
2797
type DN struct {
2898
RDNs []*RelativeDN
2999
}
30100

101+
// String returns a normalized string representation of this DN which is the
102+
// join of all relative DNs with a ",".
103+
func (d *DN) String() string {
104+
rdns := make([]string, len(d.RDNs))
105+
for i := range d.RDNs {
106+
rdns[i] = d.RDNs[i].String()
107+
}
108+
return strings.Join(rdns, ",")
109+
}
110+
31111
// ParseDN returns a distinguishedName or an error.
32112
// The function respects https://tools.ietf.org/html/rfc4514
33113
func ParseDN(str string) (*DN, error) {
@@ -84,7 +164,7 @@ func ParseDN(str string) (*DN, error) {
84164
if len(str) > i+1 && str[i+1] == '#' {
85165
i += 2
86166
index := strings.IndexAny(str[i:], ",+")
87-
data := str
167+
var data string
88168
if index > 0 {
89169
data = str[i : i+index]
90170
} else {

v3/dn_test.go

+9-4
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ func TestDNEqual(t *testing.T) {
148148
},
149149
// Difference in leading/trailing chars is ignored
150150
{
151-
"cn=John Doe, ou=People, dc=sun.com",
152-
"cn=John Doe,ou=People,dc=sun.com",
151+
"cn=\\ John\\20Doe, ou=People, dc=sun.com",
152+
"cn= \\ John Doe,ou=People,dc=sun.com",
153153
true,
154154
},
155155
// Difference in values is significant
@@ -174,11 +174,16 @@ func TestDNEqual(t *testing.T) {
174174
continue
175175
}
176176
if expected, actual := tc.Equal, a.Equal(b); expected != actual {
177-
t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual)
177+
t.Errorf("%d: when comparing %q and %q expected %v, got %v", i, a, b, expected, actual)
178178
continue
179179
}
180180
if expected, actual := tc.Equal, b.Equal(a); expected != actual {
181-
t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual)
181+
t.Errorf("%d: when comparing %q and %q expected %v, got %v", i, a, b, expected, actual)
182+
continue
183+
}
184+
185+
if expected, actual := a.Equal(b), a.String() == b.String(); expected != actual {
186+
t.Errorf("%d: when asserting string comparison of %q and %q expected equal %v, got %v", i, a, b, expected, actual)
182187
continue
183188
}
184189
}

0 commit comments

Comments
 (0)