Skip to content

Commit 93fe399

Browse files
Resolved #224: German umlauts in site names
1 parent 73830aa commit 93fe399

19 files changed

Lines changed: 144 additions & 205 deletions

File tree

.vscode/extensions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"recommendations": [
3-
"ms-dotnettools.csharp",
3+
"ms-dotnettools.csdevkit",
44
"ms-vscode.PowerShell",
55
"ms-vscode.cpptools",
66
"GitHub.vscode-pull-request-github",

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"dotnet.defaultSolution": "Src/DSInternals.slnx"
3+
}

Documentation/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ All notable changes to this project will be documented in this file. The format
1515

1616
- Fixed a missing `throw` in `SafeUnicodeSecureStringPointer` that silently ignored invalid password byte arrays.
1717
- Improved SAMR authentication fallback for localhost and missing SPN scenarios.
18+
- Fixed `Get-ADReplAccount` failure on domains with non-ASCII site names (e.g. German umlauts)
19+
caused by over-escaping of distinguished names
20+
([#224](https://github.com/MichaelGrafnetter/DSInternals/issues/224)).
1821

1922
## [6.4] - 2026-03-28
2023

Src/DSInternals.ADSI/packages.lock.json

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,6 @@
22
"version": 2,
33
"dependencies": {
44
".NETFramework,Version=v4.8": {
5-
"Microsoft.NETFramework.ReferenceAssemblies": {
6-
"type": "Direct",
7-
"requested": "[1.0.3, )",
8-
"resolved": "1.0.3",
9-
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
10-
"dependencies": {
11-
"Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3"
12-
}
13-
},
145
"Microsoft.SourceLink.GitHub": {
156
"type": "Direct",
167
"requested": "[8.0.0, )",
@@ -45,11 +36,6 @@
4536
"resolved": "8.0.0",
4637
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
4738
},
48-
"Microsoft.NETFramework.ReferenceAssemblies.net48": {
49-
"type": "Transitive",
50-
"resolved": "1.0.3",
51-
"contentHash": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ=="
52-
},
5339
"Microsoft.SourceLink.Common": {
5440
"type": "Transitive",
5541
"resolved": "8.0.0",

Src/DSInternals.Common.Test/DistinguishedNameTester.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,25 @@ public void DistinguishedName_SpacesAtBeginningAndEnd()
111111
[TestMethod]
112112
public void DistinguishedName_HexEscapeNonSpecialCharacter()
113113
{
114-
throw new AssertInconclusiveException("Support for this type of DN has not yet been implemented.");
114+
// \20 is the hex escape for a space character.
115115
var dn = new DistinguishedName(@"CN=John\20Doe,OU=Employees,DC=adatum,DC=com");
116-
Assert.AreEqual(dn.ToString(), @"CN=John Doe,OU=Employees,DC=adatum,DC=com");
116+
Assert.AreEqual("John Doe", dn.Components[0].Value);
117+
Assert.AreEqual(@"CN=John Doe,OU=Employees,DC=adatum,DC=com", dn.ToString());
118+
119+
// \C3\B6 is the RFC 4514 byte-by-byte UTF-8 encoding of 'ö'. The parser must accept
120+
// hex-escaped input (some tools produce it), but the writer must emit the literal
121+
// Unicode character — DRSR IDL_DRSCrackNames rejects the hex-escaped form for
122+
// DS_FQDN_1779_NAME, which broke Get-ADReplAccount on domains with German umlauts
123+
// in site names (issue #224).
124+
var dn2 = new DistinguishedName(@"CN=142SRV-DC,CN=Servers,CN=L\C3\B6hne,CN=Sites,CN=Configuration,DC=adatum,DC=local");
125+
Assert.AreEqual("Löhne", dn2.Components[2].Value);
126+
Assert.AreEqual("CN=142SRV-DC,CN=Servers,CN=Löhne,CN=Sites,CN=Configuration,DC=adatum,DC=local", dn2.ToString());
127+
128+
// Round-trip a DN that already uses literal Unicode — non-ASCII chars must be
129+
// preserved verbatim, not converted to hex byte escapes.
130+
var dn3 = new DistinguishedName("CN=Löhne,CN=Sites,CN=Configuration,DC=adatum,DC=local");
131+
Assert.AreEqual("Löhne", dn3.Components[0].Value);
132+
Assert.AreEqual("CN=Löhne,CN=Sites,CN=Configuration,DC=adatum,DC=local", dn3.ToString());
117133
}
118134

119135
[TestMethod]

Src/DSInternals.Common.Test/packages.lock.json

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,6 @@
1111
"Microsoft.CodeCoverage": "17.13.0"
1212
}
1313
},
14-
"Microsoft.NETFramework.ReferenceAssemblies": {
15-
"type": "Direct",
16-
"requested": "[1.0.3, )",
17-
"resolved": "1.0.3",
18-
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
19-
"dependencies": {
20-
"Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3"
21-
}
22-
},
2314
"Microsoft.SourceLink.GitHub": {
2415
"type": "Direct",
2516
"requested": "[8.0.0, )",
@@ -127,11 +118,6 @@
127118
"System.Text.Json": "6.0.11"
128119
}
129120
},
130-
"Microsoft.NETFramework.ReferenceAssemblies.net48": {
131-
"type": "Transitive",
132-
"resolved": "1.0.3",
133-
"contentHash": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ=="
134-
},
135121
"Microsoft.SourceLink.Common": {
136122
"type": "Transitive",
137123
"resolved": "8.0.0",

Src/DSInternals.Common/Data/DistinguishedName.cs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,25 @@ private static string[] SplitDN(string dn, bool isRDN)
191191
switch (currentChar)
192192
{
193193
case escapeChar:
194-
// Skip the next char if not at the end of the string:
195-
if (i < dn.Length - 1)
194+
if (i + 2 < dn.Length && IsHexDigit(dn[i + 1]) && IsHexDigit(dn[i + 2]))
196195
{
196+
// RFC 4514 hex-pair escape (\XX). Consecutive \XX pairs form a UTF-8
197+
// byte sequence and must be decoded together to handle multi-byte
198+
// characters (e.g. \C3\B6 → "ö").
199+
var bytes = new List<byte>(2);
200+
do
201+
{
202+
bytes.Add((byte)((HexValue(dn[i + 1]) << 4) | HexValue(dn[i + 2])));
203+
i += 3;
204+
}
205+
while (i + 2 < dn.Length && dn[i] == escapeChar && IsHexDigit(dn[i + 1]) && IsHexDigit(dn[i + 2]));
206+
// The outer for-loop will increment i.
207+
i--;
208+
currentSegment.Append(Encoding.UTF8.GetString(bytes.ToArray()));
209+
}
210+
else if (i < dn.Length - 1)
211+
{
212+
// Single-character escape (e.g. \, \= \\ \").
197213
i++;
198214
currentSegment.Append(dn[i]);
199215
}
@@ -243,6 +259,18 @@ private static string[] SplitDN(string dn, bool isRDN)
243259
return segments.ToArray();
244260
}
245261

262+
private static bool IsHexDigit(char c)
263+
{
264+
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
265+
}
266+
267+
private static int HexValue(char c)
268+
{
269+
if (c <= '9') return c - '0';
270+
if (c <= 'F') return c - 'A' + 10;
271+
return c - 'a' + 10;
272+
}
273+
246274
public static string GetDnsNameFromDN(string dn)
247275
{
248276
var dnParsed = new DistinguishedName(dn);

Src/DSInternals.Common/Data/DistinguishedNameComponent.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,6 @@ private static string EscapeValue(string input)
5353
// Escape control chars
5454
result.AppendFormat(CultureInfo.InvariantCulture, "\\{0:X2}", (int)currentChar);
5555
}
56-
else if (currentChar >= 128)
57-
{
58-
// Escape multibyte chars
59-
byte[] bytes = Encoding.UTF8.GetBytes(currentChar.ToString());
60-
61-
foreach (byte currentByte in bytes)
62-
{
63-
result.AppendFormat(CultureInfo.InvariantCulture, "\\{0:X2}", currentByte);
64-
}
65-
}
6656
else
6757
{
6858
// Append the char without escaping

Src/DSInternals.Common/packages.lock.json

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,6 @@
22
"version": 2,
33
"dependencies": {
44
".NETFramework,Version=v4.8": {
5-
"Microsoft.NETFramework.ReferenceAssemblies": {
6-
"type": "Direct",
7-
"requested": "[1.0.3, )",
8-
"resolved": "1.0.3",
9-
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
10-
"dependencies": {
11-
"Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3"
12-
}
13-
},
145
"Microsoft.SourceLink.GitHub": {
156
"type": "Direct",
167
"requested": "[8.0.0, )",
@@ -125,11 +116,6 @@
125116
"resolved": "8.0.0",
126117
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
127118
},
128-
"Microsoft.NETFramework.ReferenceAssemblies.net48": {
129-
"type": "Transitive",
130-
"resolved": "1.0.3",
131-
"contentHash": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ=="
132-
},
133119
"Microsoft.SourceLink.Common": {
134120
"type": "Transitive",
135121
"resolved": "8.0.0",

Src/DSInternals.DataStore.Test/packages.lock.json

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,6 @@
1111
"Microsoft.CodeCoverage": "17.13.0"
1212
}
1313
},
14-
"Microsoft.NETFramework.ReferenceAssemblies": {
15-
"type": "Direct",
16-
"requested": "[1.0.3, )",
17-
"resolved": "1.0.3",
18-
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
19-
"dependencies": {
20-
"Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3"
21-
}
22-
},
2314
"Microsoft.SourceLink.GitHub": {
2415
"type": "Direct",
2516
"requested": "[8.0.0, )",
@@ -127,11 +118,6 @@
127118
"System.Text.Json": "6.0.11"
128119
}
129120
},
130-
"Microsoft.NETFramework.ReferenceAssemblies.net48": {
131-
"type": "Transitive",
132-
"resolved": "1.0.3",
133-
"contentHash": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ=="
134-
},
135121
"Microsoft.SourceLink.Common": {
136122
"type": "Transitive",
137123
"resolved": "8.0.0",
@@ -281,7 +267,7 @@
281267
"dsinternals.datastore": {
282268
"type": "Project",
283269
"dependencies": {
284-
"DSInternals.Common": "[6.3.0, )",
270+
"DSInternals.Common": "[6.4.0, )",
285271
"DSInternals.ManagedEsent.Interop": "[2.0.4.1, )",
286272
"DSInternals.ManagedEsent.Isam": "[2.0.4.1, )"
287273
}
@@ -578,7 +564,7 @@
578564
"dsinternals.datastore": {
579565
"type": "Project",
580566
"dependencies": {
581-
"DSInternals.Common": "[6.3.0, )",
567+
"DSInternals.Common": "[6.4.0, )",
582568
"DSInternals.ManagedEsent.Interop": "[2.0.4.1, )",
583569
"DSInternals.ManagedEsent.Isam": "[2.0.4.1, )"
584570
}
@@ -836,7 +822,7 @@
836822
"dsinternals.datastore": {
837823
"type": "Project",
838824
"dependencies": {
839-
"DSInternals.Common": "[6.3.0, )",
825+
"DSInternals.Common": "[6.4.0, )",
840826
"DSInternals.ManagedEsent.Interop": "[2.0.4.1, )",
841827
"DSInternals.ManagedEsent.Isam": "[2.0.4.1, )"
842828
}

0 commit comments

Comments
 (0)