From 237edd42739a7d262d9d37db55c61d7d055bfa5e Mon Sep 17 00:00:00 2001 From: Vishv Kakadiya Date: Sat, 15 Aug 2026 22:56:24 +0530 Subject: [PATCH] fix(openapi_fdw): respect quoted strings and escapes in Link header parameter parsing --- wasm-wrappers/fdw/openapi_fdw/src/response.rs | 32 ++++++++++- .../fdw/openapi_fdw/src/response_tests.rs | 54 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/wasm-wrappers/fdw/openapi_fdw/src/response.rs b/wasm-wrappers/fdw/openapi_fdw/src/response.rs index aa765e6f..cccafa0b 100644 --- a/wasm-wrappers/fdw/openapi_fdw/src/response.rs +++ b/wasm-wrappers/fdw/openapi_fdw/src/response.rs @@ -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; diff --git a/wasm-wrappers/fdw/openapi_fdw/src/response_tests.rs b/wasm-wrappers/fdw/openapi_fdw/src/response_tests.rs index fc1a1c23..f7d4ad02 100644 --- a/wasm-wrappers/fdw/openapi_fdw/src/response_tests.rs +++ b/wasm-wrappers/fdw/openapi_fdw/src/response_tests.rs @@ -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", + "; 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", + "; 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", + "; 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() + )) + ); +}