1//========================================================================
2//
3// DistinguishedNameParser.h
4//
5// This file is licensed under the GPLv2 or later
6//
7// Copyright 2002 g10 Code GmbH
8// Copyright 2004 Klarälvdalens Datakonsult AB
9// Copyright 2021 g10 Code GmbH
10// Copyright 2023 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk>
11//
12// Derived from libkleopatra (KDE key management library) dn.cpp
13//
14//========================================================================
15
16#ifndef DISTINGUISHEDNAMEPARSER_H
17#define DISTINGUISHEDNAMEPARSER_H
18
19#include <vector>
20#include <string>
21#include <utility>
22#include <optional>
23#include <algorithm>
24
25namespace DN {
26namespace detail {
27
28inline std::string_view removeLeadingSpaces(std::string_view view)
29{
30 auto pos = view.find_first_not_of(c: ' ');
31 if (pos > view.size()) {
32 return {};
33 }
34 return view.substr(pos: pos);
35}
36
37inline std::string_view removeTrailingSpaces(std::string_view view)
38{
39 auto pos = view.find_last_not_of(c: ' ');
40 if (pos > view.size()) {
41 return {};
42 }
43 return view.substr(pos: 0, n: pos + 1);
44}
45
46inline unsigned char xtoi(unsigned char c)
47{
48 if (c <= '9') {
49 return c - '0';
50 }
51 if (c <= 'F') {
52 return c - 'A' + 10;
53 }
54 return c < 'a' + 10;
55}
56
57inline unsigned char xtoi(unsigned char first, unsigned char second)
58{
59 return 16 * xtoi(c: first) + xtoi(c: second);
60}
61// Parses a hex string into actual content
62inline std::optional<std::string> parseHexString(std::string_view view)
63{
64 auto size = view.size();
65 if (size == 0 || (size % 2 == 1)) {
66 return std::nullopt;
67 }
68 // It is only supposed to be called with actual hex strings
69 // but this is just to be extra sure
70 auto endHex = view.find_first_not_of(str: "1234567890abcdefABCDEF");
71 if (endHex != std::string_view::npos) {
72 return {};
73 }
74 std::string result;
75 result.reserve(res: size / 2);
76 for (size_t i = 0; i < (view.size() - 1); i += 2) {
77 result.push_back(c: xtoi(first: view[i], second: view[i + 1]));
78 }
79 return result;
80}
81
82static const std::vector<std::pair<std::string_view, std::string_view>> oidmap = {
83 // clang-format off
84 // keep them ordered by oid:
85 {"NameDistinguisher", "0.2.262.1.10.7.20" },
86 {"EMAIL", "1.2.840.113549.1.9.1"},
87 {"CN", "2.5.4.3" },
88 {"SN", "2.5.4.4" },
89 {"SerialNumber", "2.5.4.5" },
90 {"T", "2.5.4.12" },
91 {"D", "2.5.4.13" },
92 {"BC", "2.5.4.15" },
93 {"ADDR", "2.5.4.16" },
94 {"PC", "2.5.4.17" },
95 {"GN", "2.5.4.42" },
96 {"Pseudo", "2.5.4.65" },
97 // clang-format on
98};
99
100static std::string_view attributeNameForOID(std::string_view oid)
101{
102 if (oid.substr(pos: 0, n: 4) == std::string_view { "OID." } || oid.substr(pos: 0, n: 4) == std::string_view { "oid." }) { // c++20 has starts_with. we don't have that yet.
103 oid.remove_prefix(n: 4);
104 }
105 for (const auto &m : oidmap) {
106 if (oid == m.second) {
107 return m.first;
108 }
109 }
110 return {};
111}
112
113/* Parse a DN and return an array-ized one. This is not a validating
114 parser and it does not support any old-stylish syntax; gpgme is
115 expected to return only rfc2253 compatible strings. */
116static std::pair<std::optional<std::string_view>, std::pair<std::string, std::string>> parse_dn_part(std::string_view stringv)
117{
118 std::pair<std::string, std::string> dnPair;
119 auto separatorPos = stringv.find_first_of(c: '=');
120 if (separatorPos == 0 || separatorPos == std::string_view::npos) {
121 return {}; /* empty key */
122 }
123
124 std::string_view key = stringv.substr(pos: 0, n: separatorPos);
125 key = removeTrailingSpaces(view: key);
126 // map OIDs to their names:
127 if (auto name = attributeNameForOID(oid: key); !name.empty()) {
128 key = name;
129 }
130
131 dnPair.first = std::string { key };
132 stringv = removeLeadingSpaces(view: stringv.substr(pos: separatorPos + 1));
133 if (stringv.empty()) {
134 return {};
135 }
136
137 if (stringv.front() == '#') {
138 /* hexstring */
139 stringv.remove_prefix(n: 1);
140 auto endHex = stringv.find_first_not_of(str: "1234567890abcdefABCDEF");
141 if (!endHex || (endHex % 2 == 1)) {
142 return {}; /* empty or odd number of digits */
143 }
144 auto value = parseHexString(view: stringv.substr(pos: 0, n: endHex));
145 if (!value.has_value()) {
146 return {};
147 }
148 stringv = stringv.substr(pos: endHex);
149 dnPair.second = value.value();
150 } else if (stringv.front() == '"') {
151 stringv.remove_prefix(n: 1);
152 std::string value;
153 bool stop = false;
154 while (!stringv.empty() && !stop) {
155 switch (stringv.front()) {
156 case '\\': {
157 if (stringv.size() < 2) {
158 return {};
159 }
160 if (stringv[1] == '"') {
161 value.push_back(c: '"');
162 stringv.remove_prefix(n: 2);
163 } else {
164 // it is a bit unclear in rfc2253 if escaped hex chars should
165 // be decoded inside quotes. Let's just forward the verbatim
166 // for now
167 value.push_back(c: stringv.front());
168 value.push_back(c: stringv[1]);
169 stringv.remove_prefix(n: 2);
170 }
171 break;
172 }
173 case '"': {
174 stop = true;
175 stringv.remove_prefix(n: 1);
176 break;
177 }
178 default: {
179 value.push_back(c: stringv.front());
180 stringv.remove_prefix(n: 1);
181 }
182 }
183 }
184 if (!stop) {
185 // we have reached end of string, but never an actual ", so error out
186 return {};
187 }
188 dnPair.second = value;
189 } else {
190 std::string value;
191 bool stop = false;
192 bool lastAddedEscapedSpace = false;
193 while (!stringv.empty() && !stop) {
194 switch (stringv.front()) {
195 case '\\': //_escaping
196 {
197 stringv.remove_prefix(n: 1);
198 if (stringv.empty()) {
199 return {};
200 }
201 switch (stringv.front()) {
202 case ',':
203 case '=':
204 case '+':
205 case '<':
206 case '>':
207 case '#':
208 case ';':
209 case '\\':
210 case '"':
211 case ' ': {
212 if (stringv.front() == ' ') {
213 lastAddedEscapedSpace = true;
214 } else {
215 lastAddedEscapedSpace = false;
216 }
217 value.push_back(c: stringv.front());
218 stringv.remove_prefix(n: 1);
219 break;
220 }
221 default: {
222 if (stringv.size() < 2) {
223 // this should be double hex-ish, but isn't.
224 return {};
225 }
226 if (std::isxdigit(stringv.front()) && std::isxdigit(stringv[1])) {
227 lastAddedEscapedSpace = false;
228 value.push_back(c: xtoi(first: stringv.front(), second: stringv[1]));
229 stringv.remove_prefix(n: 2);
230 break;
231 } else {
232 // invalid escape
233 return {};
234 }
235 }
236 }
237 break;
238 }
239 case '"':
240 // unescaped " in the middle; not allowed
241 return {};
242 case ',':
243 case '=':
244 case '+':
245 case '<':
246 case '>':
247 case '#':
248 case ';': {
249 stop = true;
250 break; //
251 }
252 default:
253 lastAddedEscapedSpace = false;
254 value.push_back(c: stringv.front());
255 stringv.remove_prefix(n: 1);
256 }
257 }
258 if (lastAddedEscapedSpace) {
259 dnPair.second = value;
260 } else {
261 dnPair.second = std::string { removeTrailingSpaces(view: value) };
262 }
263 }
264 return { stringv, dnPair };
265}
266}
267
268using Result = std::vector<std::pair<std::string, std::string>>;
269
270/* Parse a DN and return an array-ized one. This is not a validating
271 parser and it does not support any old-stylish syntax; gpgme is
272 expected to return only rfc2253 compatible strings. */
273static Result parseString(std::string_view string)
274{
275 Result result;
276 while (!string.empty()) {
277 string = detail::removeLeadingSpaces(view: string);
278 if (string.empty()) {
279 break;
280 }
281
282 auto [partResult, dnPair] = detail::parse_dn_part(stringv: string);
283 if (!partResult.has_value()) {
284 return {};
285 }
286
287 string = partResult.value();
288 if (dnPair.first.size() && dnPair.second.size()) {
289 result.emplace_back(args: std::move(dnPair));
290 }
291
292 string = detail::removeLeadingSpaces(view: string);
293 if (string.empty()) {
294 break;
295 }
296 switch (string.front()) {
297 case ',':
298 case ';':
299 case '+':
300 string.remove_prefix(n: 1);
301 break;
302 default:
303 // some unexpected characters here
304 return {};
305 }
306 }
307 return result;
308}
309
310/// returns the first value of a given key (note. there can be multiple)
311/// or nullopt if key is not available
312inline std::optional<std::string> FindFirstValue(const Result &dn, std::string_view key)
313{
314 auto first = std::find_if(first: dn.begin(), last: dn.end(), pred: [&key](const auto &it) { return it.first == key; });
315 if (first == dn.end()) {
316 return {};
317 }
318 return first->second;
319}
320} // namespace DN
321#endif // DISTINGUISHEDNAMEPARSER_H
322

source code of poppler/poppler/DistinguishedNameParser.h