diff --git a/form_urlencoded/src/lib.rs b/form_urlencoded/src/lib.rs index 639545ae6..40b2bce51 100644 --- a/form_urlencoded/src/lib.rs +++ b/form_urlencoded/src/lib.rs @@ -243,6 +243,70 @@ impl<'a, T: Target> Serializer<'a, T> { self } + /// Remove all name/value pairs whose name is `name`. + /// + /// Names are compared after percent-decoding, so this matches the same + /// names that [`parse`] and `Url::query_pairs` would yield. Pairs that are + /// kept keep their original serialization. + /// + /// ```rust + /// use form_urlencoded; + /// let encoded: String = form_urlencoded::Serializer::new(String::from("a=1&b=2&a=3")) + /// .remove_all("a") + /// .finish(); + /// assert_eq!(encoded, "b=2"); + /// ``` + /// + /// Panics if called after `.finish()`. + pub fn remove_all(&mut self, name: &str) -> &mut Self { + self.remove_pairs(name, false); + self + } + + /// Remove the first name/value pair whose name is `name`. + /// + /// Names are compared after percent-decoding, so this matches the same + /// names that [`parse`] and `Url::query_pairs` would yield. Pairs that are + /// kept keep their original serialization. + /// + /// ```rust + /// use form_urlencoded; + /// let encoded: String = form_urlencoded::Serializer::new(String::from("a=1&b=2&a=3")) + /// .remove_first("a") + /// .finish(); + /// assert_eq!(encoded, "b=2&a=3"); + /// ``` + /// + /// Panics if called after `.finish()`. + pub fn remove_first(&mut self, name: &str) -> &mut Self { + self.remove_pairs(name, true); + self + } + + fn remove_pairs(&mut self, name: &str, first_only: bool) { + let start = self.start_position; + let string = string(&mut self.target); + if string.len() <= start { + return; + } + let mut kept = String::with_capacity(string.len() - start); + let mut removed = false; + for segment in string[start..].split('&') { + let matches = !(first_only && removed) + && decode(segment.split('=').next().unwrap_or("").as_bytes()).as_ref() == name; + if matches { + removed = true; + continue; + } + if !kept.is_empty() { + kept.push('&'); + } + kept.push_str(segment); + } + string.truncate(start); + string.push_str(&kept); + } + /// Set the character encoding to be used for names and values before percent-encoding. pub fn encoding_override(&mut self, new: EncodingOverride<'a>) -> &mut Self { self.encoding = new; diff --git a/url/tests/unit.rs b/url/tests/unit.rs index faeb7c5f4..9698e7059 100644 --- a/url/tests/unit.rs +++ b/url/tests/unit.rs @@ -531,6 +531,47 @@ fn extend_query_pairs_then_mutate() { ); } +#[test] +/// https://github.com/servo/rust-url/issues/1087 +fn remove_query_pairs() { + let mut url: Url = "http://example.com/?a=1&b=2&a=3#frag".parse().unwrap(); + url.query_pairs_mut().remove_all("a"); + url.check_invariants().unwrap(); + assert_eq!(url.to_string(), "http://example.com/?b=2#frag"); + + let mut url: Url = "http://example.com/?a=1&b=2&a=3".parse().unwrap(); + url.query_pairs_mut().remove_first("a"); + url.check_invariants().unwrap(); + assert_eq!(url.to_string(), "http://example.com/?b=2&a=3"); + + // Removing a name that isn't present leaves the query untouched. + let mut url: Url = "http://example.com/?a=1&b=2".parse().unwrap(); + url.query_pairs_mut().remove_all("c"); + url.check_invariants().unwrap(); + assert_eq!(url.to_string(), "http://example.com/?a=1&b=2"); + + // Removing every pair leaves an empty query, like `clear`. + let mut url: Url = "http://example.com/?a=1&a=2".parse().unwrap(); + url.query_pairs_mut().remove_all("a"); + url.check_invariants().unwrap(); + assert_eq!(url.to_string(), "http://example.com/?"); +} + +#[test] +fn remove_query_pairs_percent_decoded() { + // The name is matched after percent-decoding, so `%61` matches `a`, + // and retained pairs keep their original serialization. + let mut url: Url = "http://example.com/?%61=1&b=2".parse().unwrap(); + url.query_pairs_mut().remove_all("a"); + url.check_invariants().unwrap(); + assert_eq!(url.to_string(), "http://example.com/?b=2"); + + let encoded: String = form_urlencoded::Serializer::new(String::from("a+b=1&c=2")) + .remove_all("a b") + .finish(); + assert_eq!(encoded, "c=2"); +} + #[test] /// https://github.com/servo/rust-url/issues/222 fn append_empty_segment_then_mutate() {