Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion wasm-wrappers/fdw/openapi_fdw/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,40 @@ fn split_link_entries(s: &str) -> Vec<&str> {
entries
}

/// Split the parameter portion of a Link entry on top-level semicolons,
/// ignoring semicolons inside quoted strings. Honors RFC 7230 `quoted-pair`
/// escapes so an escaped quote (e.g. `title="a \"semi;colon\""`) does not
/// flip the quote state.
fn split_link_params(s: &str) -> Vec<&str> {
let mut params = Vec::new();
let mut in_quotes = false;
let mut escape = false;
let mut start = 0;
for (i, c) in s.char_indices() {
if escape {
escape = false;
continue;
}
match c {
'\\' if in_quotes => escape = true,
'"' => in_quotes = !in_quotes,
';' if !in_quotes => {
params.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
}
if start <= s.len() {
params.push(&s[start..]);
}
params
}

/// Returns true if the parameter list (after the URI part of a Link entry)
/// contains a `rel` parameter whose value is or includes `next`.
fn has_rel_next(params: &str) -> bool {
for raw in params.split(';') {
for raw in split_link_params(params) {
let part = raw.trim();
let Some((name, value)) = part.split_once('=') else {
continue;
Expand Down
54 changes: 54 additions & 0 deletions wasm-wrappers/fdw/openapi_fdw/src/response_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -984,3 +984,57 @@ fn test_link_header_empty_or_malformed() {
fdw.handle_pagination(&resp, &headers);
assert!(fdw.pagination.next.is_none());
}

#[test]
fn test_link_header_quoted_semicolon_in_param() {
// A quoted parameter value containing a semicolon (e.g. title="a;rel=next")
// must not split the parameter list and wrongly identify rel=next when rel is actually "last".
let mut fdw = make_fdw_for_pagination("");
let resp = serde_json::json!({});
let headers = vec![h(
"Link",
"<https://api.example.com/x>; title=\"a;rel=next\"; rel=\"last\"",
)];
fdw.handle_pagination(&resp, &headers);
assert!(
fdw.pagination.next.is_none(),
"Expected no next URL because rel is 'last', but got: {:?}",
fdw.pagination.next
);
}

#[test]
fn test_link_header_quoted_semicolon_with_valid_next() {
// Semicolons inside other parameters must not disrupt finding rel="next".
let mut fdw = make_fdw_for_pagination("");
let resp = serde_json::json!({});
let headers = vec![h(
"Link",
"<https://api.example.com/x>; title=\"item 1; item 2; item 3\"; rel=\"next\"",
)];
fdw.handle_pagination(&resp, &headers);
assert_eq!(
fdw.pagination.next,
Some(PaginationToken::Url(
"https://api.example.com/x".to_string()
))
);
}

#[test]
fn test_link_header_escaped_quote_in_param() {
// Backslash-escaped quotes inside parameters must not disrupt quote state.
let mut fdw = make_fdw_for_pagination("");
let resp = serde_json::json!({});
let headers = vec![h(
"Link",
"<https://api.example.com/x>; title=\"item \\\"foo;bar\\\"\"; rel=\"next\"",
)];
fdw.handle_pagination(&resp, &headers);
assert_eq!(
fdw.pagination.next,
Some(PaginationToken::Url(
"https://api.example.com/x".to_string()
))
);
}
Loading