Skip to content

Commit a92dc17

Browse files
committed
minor syntax cleanups; allow the server to specify a more specific mime type compared to the client, which is then picked up as the negotiation result.
1 parent e561648 commit a92dc17

File tree

5 files changed

+131
-81
lines changed

5 files changed

+131
-81
lines changed

include/cxxhttp/mime-type.h

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -91,55 +91,67 @@ class mimeType {
9191
} state = inType;
9292

9393
std::string key, value;
94+
std::string *collectTo = &type;
9495
bool space = false;
9596

9697
for (const auto &c : pType) {
97-
if (!isValid) {
98-
// do nothing if we're in a bad state.
99-
} else if (state == inValueEscaped) {
98+
bool token = isToken(c);
99+
bool tokenShiftSpace = token && !space;
100+
bool tokenTargetEmpty = token && collectTo->empty();
101+
bool doCollect = tokenShiftSpace || tokenTargetEmpty;
102+
bool pushLC =
103+
doCollect && (state == inType || state == inSub || state == inKey);
104+
bool pushCS = state == inValueQuoted || (doCollect && state == inValue);
105+
space = isSpace(c);
106+
107+
if (state == inValueEscaped) {
100108
state = inValueQuoted;
101-
value.push_back(c);
109+
collectTo->push_back(c);
102110
} else if (state == inValueQuoted && c == '"') {
103111
state = inValue;
104-
} else if (state == inValue && c == '"' && value.empty()) {
105-
state = inValueQuoted;
106112
} else if (state == inValueQuoted && c == '\\') {
107113
state = inValueEscaped;
108-
} else if (state == inValueQuoted) {
109-
value.push_back(c);
110-
} else if (state == inType && isToken(c) && (!space || type.empty())) {
111-
type.push_back(std::tolower(c));
112-
} else if (state == inSub && isToken(c) && (!space || subtype.empty())) {
113-
subtype.push_back(std::tolower(c));
114-
} else if (state == inKey && isToken(c) && (!space || key.empty())) {
115-
key.push_back(std::tolower(c));
116-
} else if (state == inValue && isToken(c) && (!space || value.empty())) {
117-
value.push_back(c);
114+
} else if (state == inValue && c == '"' && value.empty()) {
115+
state = inValueQuoted;
118116
} else if (state == inType && c == '/' && !type.empty()) {
119117
state = inSub;
118+
collectTo = &subtype;
120119
} else if (state == inSub && c == ';' && !subtype.empty()) {
121120
state = inKey;
121+
collectTo = &key;
122122
} else if (state == inValue && c == ';') {
123-
isValid = !key.empty() && !value.empty();
124-
state = inKey;
125123
attributes[key] = value;
124+
state = inKey;
126125
key.clear();
127126
value.clear();
127+
collectTo = &key;
128128
} else if (state == inKey && c == '=') {
129129
state = inValue;
130-
} else if (!isSpace(c)) {
130+
collectTo = &value;
131+
isValid = isValid && !key.empty();
132+
} else if (pushLC) {
133+
collectTo->push_back(std::tolower(c));
134+
} else if (pushCS) {
135+
collectTo->push_back(c);
136+
} else if (!space) {
131137
// ignore free spaces; this is somewhat more lenient than the original
132138
// grammar, but then that also insists on some level of leniency there.
133139
isValid = false;
134140
}
135-
space = isSpace(c);
136-
}
137141

138-
isValid = isValid && (state == inSub || state == inValue) &&
139-
(type != "*" || subtype == "*");
142+
if (state == inSub && type == "*" && !subtype.empty()) {
143+
isValid = subtype == "*";
144+
}
145+
146+
if (!isValid) {
147+
break;
148+
}
149+
}
140150

141-
if (isValid && state == inValue) {
151+
if (state == inValue) {
142152
attributes[key] = value;
153+
} else if (state != inSub) {
154+
isValid = false;
143155
}
144156
}
145157

@@ -200,10 +212,35 @@ class mimeType {
200212
* @return Whether or not the two mime types match.
201213
*/
202214
bool operator==(const mimeType &b) const {
203-
return valid() && b.valid() && (!wildcard() || !b.wildcard()) &&
204-
(type == "*" || b.type == "*" || type == b.type) &&
205-
(subtype == "*" || b.subtype == "*" || subtype == b.subtype) &&
206-
(attributes == b.attributes);
215+
return typeMatch(b) && attributes == b.attributes;
216+
}
217+
218+
/* Subset operator.
219+
* @b The instance to compare to.
220+
*
221+
* Similar to the quality operator, except that the attribute set is allowed
222+
* to be a subset of the right-hand side.
223+
* This is for cases where two mime types are basically the same, except one
224+
* side is a more specific version of the other side, e.g. the charset is
225+
* specified explicitly.
226+
*
227+
* @return Whether or not the two mime types match.
228+
*/
229+
bool operator<=(const mimeType &b) const {
230+
if (!typeMatch(b)) {
231+
return false;
232+
}
233+
234+
// we already have a type match at this point, so just check that all
235+
// attributes in this instance are the same in the b instance.
236+
for (const auto &v : attributes) {
237+
const auto &bv = b.attributes.find(v.first);
238+
if (bv == b.attributes.end() || bv->second != v.second) {
239+
return false;
240+
}
241+
}
242+
243+
return true;
207244
}
208245

209246
/* Does this media type contain wildcards?
@@ -225,6 +262,21 @@ class mimeType {
225262
*/
226263
bool isValid;
227264

265+
/* Type equality match.
266+
* @b The instance to compare to.
267+
*
268+
* Full wildcards for either the type or subtype are allowed as well.
269+
* Two types do not match if both sides have wildcards.
270+
* Does not look at attribute values.
271+
*
272+
* @return Whether or not the two mime types match.
273+
*/
274+
bool typeMatch(const mimeType &b) const {
275+
return valid() && b.valid() && (!wildcard() || !b.wildcard()) &&
276+
(type == "*" || b.type == "*" || type == b.type) &&
277+
(subtype == "*" || b.subtype == "*" || subtype == b.subtype);
278+
}
279+
228280
/* Check character against the set of control characters.
229281
* @c The character to check.
230282
*

include/cxxhttp/negotiate.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,11 @@ class qvalue {
292292
}
293293

294294
if (mime.valid() && b.mime.valid()) {
295-
return mime == b.mime;
295+
// for MIME types, allow a subset match; the server-side value is later
296+
// expected to be the one actually supported and fully qualified.
297+
// We expect that the right-hand side is the server-side for negotiations,
298+
// so the client specification has to be a subset of ours.
299+
return mime == b.mime || mime <= b.mime;
296300
}
297301

298302
// since the two items aren't MIME types, if either of the two is a wildcard

include/cxxhttp/uri.h

Lines changed: 24 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -153,32 +153,24 @@ class uri {
153153
static std::string decode(const std::string &s, bool &isValid) {
154154
std::string rv;
155155

156-
bool isEncoded = false;
157-
bool haveFirst = false;
156+
int expectEncoded = 0;
158157
int first = 0;
159158

160159
for (const auto &c : s) {
161-
if (isEncoded) {
162-
if (haveFirst) {
163-
isEncoded = false;
164-
haveFirst = false;
165-
rv.push_back((decode(first, isValid) << 4) | decode(c, isValid));
166-
} else {
167-
haveFirst = true;
168-
first = c;
169-
}
160+
if (expectEncoded == 2) {
161+
expectEncoded = 0;
162+
rv.push_back(first | decode(c, isValid));
163+
} else if (expectEncoded == 1) {
164+
expectEncoded = 2;
165+
first = decode(c, isValid) << 4;
166+
} else if (c == '%') {
167+
expectEncoded = 1;
170168
} else {
171-
if (c == '%') {
172-
isEncoded = true;
173-
} else {
174-
rv.push_back(c);
175-
}
169+
rv.push_back(c);
176170
}
177171
}
178172

179-
if (haveFirst || isEncoded) {
180-
isValid = false;
181-
}
173+
isValid = isValid && (expectEncoded == 0);
182174

183175
return rv;
184176
}
@@ -199,38 +191,27 @@ class uri {
199191
bool isKey = true;
200192

201193
std::string key = "";
202-
std::string value;
194+
std::string value = "";
203195

204196
isValid = true;
205197

206198
for (const auto &c : s) {
207-
if (isKey) {
208-
switch (c) {
209-
case '=':
210-
isKey = false;
211-
value.clear();
212-
break;
213-
default:
214-
key.push_back(c);
215-
}
199+
if (isKey && c == '=') {
200+
isKey = false;
201+
value.clear();
202+
} else if (isKey) {
203+
key.push_back(c);
204+
} else if (c == '&') {
205+
isKey = true;
206+
rv[key] = decode(value, isValid);
207+
key.clear();
216208
} else {
217-
switch (c) {
218-
case '&':
219-
isKey = true;
220-
rv[key] = decode(value, isValid);
221-
key.clear();
222-
break;
223-
default:
224-
value.push_back(c);
225-
}
209+
value.push_back(c);
226210
}
227211
}
228212

229-
if (isKey) {
230-
isValid = false;
231-
} else {
232-
rv[key] = decode(value, isValid);
233-
}
213+
rv[key] = decode(value, isValid);
214+
isValid = isValid && !isKey;
234215

235216
return rv;
236217
}

src/test-case/mime-type.cpp

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,16 +140,19 @@ bool testNormalise(std::ostream &log) {
140140
bool testCompare(std::ostream &log) {
141141
struct sampleData {
142142
std::string a, b;
143-
bool less, rless, equal, awildcard, bwildcard;
143+
bool less, rless, equal, subset, awildcard, bwildcard;
144144
};
145145

146146
std::vector<sampleData> tests{
147-
{"a/b", "a/b", false, false, true, false, false},
148-
{"a/b", "c/d", true, false, false, false, false},
149-
{"a/*", "c/d", true, false, false, true, false},
150-
{"foo/bar", "foo/*", false, true, true, false, true},
151-
{"foo/bar; a=b", "foo/* ; a =b", false, true, true, false, true},
152-
{"foo/bar ;a= b", "foo/bar; a =c", true, false, false, false, false},
147+
{"a/b", "a/b", false, false, true, true, false, false},
148+
{"a/b", "c/d", true, false, false, false, false, false},
149+
{"a/*", "c/d", true, false, false, false, true, false},
150+
{"foo/bar", "foo/*", false, true, true, true, false, true},
151+
{"foo/bar; a=b", "foo/* ; a =b", false, true, true, true, false, true},
152+
{"foo/bar ;a= b", "foo/bar; a =c", true, false, false, false, false,
153+
false},
154+
{"foo/bar", "foo/bar; a=c", true, false, false, true, false, false},
155+
{"foo/bar; a =b", "foo/bar", false, true, false, false, false, false},
153156
};
154157

155158
for (const auto &tt : tests) {
@@ -168,6 +171,10 @@ bool testCompare(std::ostream &log) {
168171
log << "('" << tt.a << "' == '" << tt.b << "') = '" << (a == b) << "'\n";
169172
return false;
170173
}
174+
if ((a <= b) != tt.subset) {
175+
log << "('" << tt.a << "' <= '" << tt.b << "') = '" << (a <= b) << "'\n";
176+
return false;
177+
}
171178
if (a.wildcard() != tt.awildcard) {
172179
log << "w('" << tt.a << "').wildcard() = '" << a.wildcard() << "'\n";
173180
return false;

src/test-case/negotiate.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,9 @@ bool testQvalueMatch(std::ostream &log) {
276276
{"*", "foo;bar", false, true, false},
277277
{"*;baz", "foo", false, true, false},
278278
{"a/b", "*/*", true, false, true},
279-
{"a/b", "a/b;c=d", false, false, false},
279+
{"a/b", "a/b;c=d", true, false, false},
280+
{"a/b;c=d", "a/b", false, false, false},
281+
{"a/b;d=e", "a/b;c=d", false, false, false},
280282
{"a/*;c=d", "a/b;c=d", true, true, false},
281283
{"*/*;c=d", "a/b;c=d", true, true, false},
282284
};
@@ -339,7 +341,11 @@ bool testFullNegotiation(std::ostream &log) {
339341
"text/*, text/*;level=1", "text/html;level=1", "text/html;level=1"},
340342
{"text/*;q=0.3, text/html;q=0.7, text/html;level=1,"
341343
"text/html;level=2;q=0.4, */*;q=0.5",
342-
"text/*;q=0.1, text/html", "text/html", "text/html"},
344+
"text/*;q=0.1, text/html", "text/html", "text/html;level=1"},
345+
// upcasting example
346+
{"text/markdown, text/plain;q=0.9",
347+
"text/markdown; charset=utf-8, text/plain",
348+
"text/markdown;charset=utf-8", "text/plain"},
343349
};
344350

345351
for (const auto &tt : tests) {

0 commit comments

Comments
 (0)