Skip to content

Commit 7f67422

Browse files
authored
Merge branch 'master' into chore/dependencies
2 parents bd69184 + 570560b commit 7f67422

2 files changed

Lines changed: 352 additions & 0 deletions

File tree

v3/postaladdress.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package ldap
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
)
8+
9+
var ErrEmptyPostalAddress = errors.New("ldap: postal address cannot be empty")
10+
11+
// PostalAddress represents an RFC 4517 Postal Address
12+
// A postal address is a sequence of strings of one or more arbitrary UCS
13+
// characters, which form the lines of the address.
14+
type PostalAddress struct {
15+
lines []string
16+
}
17+
18+
// NewPostalAddress creates a new PostalAddress by copying non-empty lines from the provided slice of strings.
19+
func NewPostalAddress(lines []string) (*PostalAddress, error) {
20+
copiedLines := make([]string, 0, len(lines))
21+
for _, line := range lines {
22+
if line == "" {
23+
continue
24+
}
25+
copiedLines = append(copiedLines, line)
26+
}
27+
28+
if len(copiedLines) == 0 {
29+
return nil, ErrEmptyPostalAddress
30+
}
31+
32+
return &PostalAddress{lines: copiedLines}, nil
33+
}
34+
35+
// Lines returns a copy of the address lines as a slice of strings.
36+
func (p *PostalAddress) Lines() []string {
37+
copiedLines := make([]string, len(p.lines))
38+
copy(copiedLines, p.lines)
39+
return copiedLines
40+
}
41+
42+
// String returns the postal address as a single string, with lines joined by newline characters.
43+
func (p *PostalAddress) String() string {
44+
return strings.Join(p.lines, "\n")
45+
}
46+
47+
// Escape encodes special characters in the PostalAddress lines as per RFC 4517 and appends a `$` at the end of each line.
48+
func (p *PostalAddress) Escape() string {
49+
builder := &strings.Builder{}
50+
51+
for _, line := range p.lines {
52+
for _, char := range line {
53+
switch char {
54+
case '\\':
55+
builder.WriteString("\\5C")
56+
case '$':
57+
builder.WriteString("\\24")
58+
default:
59+
builder.WriteRune(char)
60+
}
61+
}
62+
63+
builder.WriteRune('$')
64+
}
65+
66+
return builder.String()
67+
}
68+
69+
// ParsePostalAddress parses an RFC 4517 escaped postal address string into a PostalAddress object or returns an error.
70+
func ParsePostalAddress(escaped string) (*PostalAddress, error) {
71+
lines := strings.Split(escaped, "$")
72+
parsedLines := make([]string, 0, len(lines))
73+
const totalEscapeLen = 3
74+
75+
for _, line := range lines {
76+
if line == "" {
77+
// Skip empty lines
78+
continue
79+
}
80+
81+
builder := &strings.Builder{}
82+
for i := 0; i < len(line); i++ {
83+
char := line[i]
84+
if char == '\\' && i+totalEscapeLen <= len(line) {
85+
escapeSeq := line[i+1 : i+totalEscapeLen]
86+
switch escapeSeq {
87+
case "5C", "5c":
88+
builder.WriteRune('\\')
89+
i += 2
90+
case "24":
91+
builder.WriteRune('$')
92+
i += 2
93+
default:
94+
return nil, fmt.Errorf("invalid escape sequence: \\%s at position %d", escapeSeq, i)
95+
}
96+
} else if char == '\\' {
97+
return nil, fmt.Errorf("incomplete escape sequence at position %d", i)
98+
} else {
99+
builder.WriteByte(char)
100+
}
101+
}
102+
parsedLines = append(parsedLines, builder.String())
103+
}
104+
105+
if len(parsedLines) == 0 {
106+
return nil, ErrEmptyPostalAddress
107+
}
108+
109+
return &PostalAddress{lines: parsedLines}, nil
110+
}
111+
112+
// Equal compares the current PostalAddress with another PostalAddress and returns true if they are identical.
113+
func (p *PostalAddress) Equal(other *PostalAddress) bool {
114+
if p == other {
115+
return true
116+
}
117+
if p == nil || other == nil {
118+
return false
119+
}
120+
121+
if len(p.lines) != len(other.lines) {
122+
return false
123+
}
124+
for i := range p.lines {
125+
if p.lines[i] != other.lines[i] {
126+
return false
127+
}
128+
}
129+
return true
130+
}

v3/postaladdress_test.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package ldap
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestPostalAddressRoundTrip(t *testing.T) {
10+
testStrings := []struct {
11+
Escaped string
12+
Expected string
13+
}{
14+
{
15+
Escaped: "AAAAA\\5cBBBBB$",
16+
Expected: "AAAAA\\BBBBB",
17+
},
18+
{
19+
Escaped: `line\5C`,
20+
Expected: "line\\",
21+
},
22+
{
23+
Escaped: "1234 Main St.$Anytown, CA 12345$USA",
24+
Expected: "1234 Main St.\nAnytown, CA 12345\nUSA",
25+
},
26+
{
27+
Escaped: `\241,000,000 Sweepstakes$PO Box 1000000$Anytown, CA 12345$USA`,
28+
Expected: "$1,000,000 Sweepstakes\nPO Box 1000000\nAnytown, CA 12345\nUSA",
29+
},
30+
}
31+
for _, str := range testStrings {
32+
t.Run(str.Escaped, func(t *testing.T) {
33+
escaped, err := ParsePostalAddress(str.Escaped)
34+
assert.NoError(t, err)
35+
assert.Equal(t, str.Expected, escaped.String())
36+
37+
addr, err := NewPostalAddress([]string{str.Expected})
38+
assert.NoError(t, err)
39+
assert.Equal(t, str.Expected, addr.String(), "PostalAddress.String() should round-trip")
40+
})
41+
}
42+
}
43+
44+
func TestPostalAddressEmptyLines(t *testing.T) {
45+
_, err := NewPostalAddress([]string{""})
46+
assert.Equal(t, err, ErrEmptyPostalAddress)
47+
}
48+
49+
func TestPostalAddressUTF8Handling(t *testing.T) {
50+
testCases := []struct {
51+
name string
52+
lines []string
53+
expected string
54+
}{
55+
{
56+
name: "emoji characters",
57+
lines: []string{"123 Main St 🏠", "Tokyo 🗾", "Japan 🇯🇵"},
58+
expected: "123 Main St 🏠$Tokyo 🗾$Japan 🇯🇵$",
59+
},
60+
{
61+
name: "cyrillic characters",
62+
lines: []string{"Красная площадь", "Москва 101000", "Россия"},
63+
expected: "Красная площадь$Москва 101000$Россия$",
64+
},
65+
{
66+
name: "chinese characters",
67+
lines: []string{"北京市东城区", "天安门广场", "中国"},
68+
expected: "北京市东城区$天安门广场$中国$",
69+
},
70+
{
71+
name: "arabic characters",
72+
lines: []string{"شارع الملك فهد", "الرياض", "المملكة العربية السعودية"},
73+
expected: "شارع الملك فهد$الرياض$المملكة العربية السعودية$",
74+
},
75+
{
76+
name: "mixed scripts with special chars",
77+
lines: []string{"Café René ☕", "Zürich $1000\\month", "Schweiz 🇨🇭"},
78+
expected: "Café René ☕$Zürich \\241000\\5Cmonth$Schweiz 🇨🇭$",
79+
},
80+
{
81+
name: "mathematical symbols",
82+
lines: []string{"∑ ∫ ∂", "π ≈ 3.14159", "∞ ≠ 0"},
83+
expected: "∑ ∫ ∂$π ≈ 3.14159$∞ ≠ 0$",
84+
},
85+
}
86+
87+
for _, tc := range testCases {
88+
t.Run(tc.name, func(t *testing.T) {
89+
addr, err := NewPostalAddress(tc.lines)
90+
assert.NoError(t, err)
91+
escaped := addr.Escape()
92+
assert.Equal(t, tc.expected, escaped, "UTF-8 characters should be preserved in escaped output")
93+
94+
// Round-trip test
95+
parsed, err := ParsePostalAddress(escaped)
96+
assert.NoError(t, err)
97+
assert.Equal(t, tc.lines, parsed.Lines(), "UTF-8 characters should survive round-trip")
98+
})
99+
}
100+
}
101+
102+
func TestPostalAddressEqual(t *testing.T) {
103+
testCases := []struct {
104+
name string
105+
addr1 *PostalAddress
106+
addr2 *PostalAddress
107+
expected bool
108+
}{
109+
{
110+
name: "both nil",
111+
addr1: nil,
112+
addr2: nil,
113+
expected: true,
114+
},
115+
{
116+
name: "first nil",
117+
addr1: nil,
118+
addr2: mustNewPostalAddress(t, []string{"line1"}),
119+
expected: false,
120+
},
121+
{
122+
name: "second nil",
123+
addr1: mustNewPostalAddress(t, []string{"line1"}),
124+
addr2: nil,
125+
expected: false,
126+
},
127+
{
128+
name: "same single line",
129+
addr1: mustNewPostalAddress(t, []string{"123 Main St"}),
130+
addr2: mustNewPostalAddress(t, []string{"123 Main St"}),
131+
expected: true,
132+
},
133+
{
134+
name: "different single line",
135+
addr1: mustNewPostalAddress(t, []string{"123 Main St"}),
136+
addr2: mustNewPostalAddress(t, []string{"456 Oak Ave"}),
137+
expected: false,
138+
},
139+
{
140+
name: "same multi-line",
141+
addr1: mustNewPostalAddress(t, []string{"123 Main St", "Anytown, CA", "USA"}),
142+
addr2: mustNewPostalAddress(t, []string{"123 Main St", "Anytown, CA", "USA"}),
143+
expected: true,
144+
},
145+
{
146+
name: "different multi-line content",
147+
addr1: mustNewPostalAddress(t, []string{"123 Main St", "Anytown, CA", "USA"}),
148+
addr2: mustNewPostalAddress(t, []string{"123 Main St", "Othertown, CA", "USA"}),
149+
expected: false,
150+
},
151+
{
152+
name: "different line count",
153+
addr1: mustNewPostalAddress(t, []string{"123 Main St", "Anytown, CA"}),
154+
addr2: mustNewPostalAddress(t, []string{"123 Main St", "Anytown, CA", "USA"}),
155+
expected: false,
156+
},
157+
{
158+
name: "same order matters",
159+
addr1: mustNewPostalAddress(t, []string{"line1", "line2"}),
160+
addr2: mustNewPostalAddress(t, []string{"line2", "line1"}),
161+
expected: false,
162+
},
163+
{
164+
name: "whitespace differences",
165+
addr1: mustNewPostalAddress(t, []string{"123 Main St"}),
166+
addr2: mustNewPostalAddress(t, []string{"123 Main St"}),
167+
expected: false,
168+
},
169+
{
170+
name: "case sensitive",
171+
addr1: mustNewPostalAddress(t, []string{"Main Street"}),
172+
addr2: mustNewPostalAddress(t, []string{"main street"}),
173+
expected: false,
174+
},
175+
{
176+
name: "with special characters",
177+
addr1: mustNewPostalAddress(t, []string{"Café René", "$1000\\month"}),
178+
addr2: mustNewPostalAddress(t, []string{"Café René", "$1000\\month"}),
179+
expected: true,
180+
},
181+
{
182+
name: "with UTF-8 characters",
183+
addr1: mustNewPostalAddress(t, []string{"北京市东城区", "中国 🇨🇳"}),
184+
addr2: mustNewPostalAddress(t, []string{"北京市东城区", "中国 🇨🇳"}),
185+
expected: true,
186+
},
187+
}
188+
189+
for _, tc := range testCases {
190+
t.Run(tc.name, func(t *testing.T) {
191+
result := tc.addr1.Equal(tc.addr2)
192+
assert.Equal(t, tc.expected, result)
193+
194+
// Test symmetry (except for nil cases where calling on nil would panic)
195+
if tc.addr1 != nil && tc.addr2 != nil {
196+
reverseResult := tc.addr2.Equal(tc.addr1)
197+
assert.Equal(t, tc.expected, reverseResult, "Equals should be symmetric")
198+
}
199+
})
200+
}
201+
}
202+
203+
func TestParsePostalAddress_Escape(t *testing.T) {
204+
t.Run("incomplete escape", func(t *testing.T) {
205+
_, err := ParsePostalAddress("AAAAAAAAAA\\")
206+
assert.Error(t, err)
207+
})
208+
209+
t.Run("invalid escape", func(t *testing.T) {
210+
_, err := ParsePostalAddress("AAAAAAAAAA\\5XAAAAA")
211+
assert.Error(t, err)
212+
})
213+
}
214+
215+
func mustNewPostalAddress(t *testing.T, lines []string) *PostalAddress {
216+
t.Helper()
217+
addr, err := NewPostalAddress(lines)
218+
if err != nil {
219+
t.Fatalf("NewPostalAddress failed: %v", err)
220+
}
221+
return addr
222+
}

0 commit comments

Comments
 (0)