Repository: servo/rust-url
Pull request #761 (2022-04-01 20:54:34)

perf(idna): fast-path simple/ascii domains
Complements #760, together they reduce url-parsing benches by ~25-30% (or improve throughput by +33-40%)

Before (main 48fcbe1):
```
test long  ... bench:         484 ns/iter (+/- 6) = 88 MB/s
test short ... bench:         347 ns/iter (+/- 5) = 72 MB/s
```

Inter (#760):
```
test long  ... bench:         413 ns/iter (+/- 4) = 104 MB/s
test short ... bench:         298 ns/iter (+/- 9) = 83 MB/s
```

After (#760 + this #761)
```
test long  ... bench:         363 ns/iter (+/- 10) = 118 MB/s
test short ... bench:         247 ns/iter (+/- 12) = 101 MB/s
```


Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 74b4c16..a114a27 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -5,25 +5,26 @@ on:
     branches: ["master"]
   pull_request:
 
+env:
+  CARGO_NET_GIT_FETCH_WITH_CLI: true
+
 jobs:
   Test:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        rust: [1.45.0, stable, beta, nightly]
+        rust: [1.51.0, stable, beta, nightly]
         exclude:
           - os: macos-latest
-            rust: 1.45.0
+            rust: 1.51.0
           - os: windows-latest
-            rust: 1.45.0
+            rust: 1.51.0
           - os: macos-latest
             rust: beta
           - os: windows-latest
             rust: beta
           - os: macos-latest
             rust: nightly
-          - os: windows-latest
-            rust: nightly
 
     runs-on: ${{ matrix.os }}
 
@@ -38,10 +39,27 @@ jobs:
         with:
           command: build
           args: --all-targets
-      - uses: actions-rs/cargo@v1
+        # Run tests
+      - name: Run tests
+        uses: actions-rs/cargo@v1
+        with:
+          command: test
+        # Run tests enabling the serde feature
+      - name: Run tests with the serde feature
+        uses: actions-rs/cargo@v1
+        with:
+          command: test
+          args: --features "url/serde"
+      # The #[debugger_visualizer] attribute is currently gated behind an unstable feature flag.
+      # In order to test the visualizers for the url crate, they have to be tested on a nightly build.
+      - name: Run debugger_visualizer tests
+        if: |
+          matrix.os == 'windows-latest' &&
+          matrix.rust == 'nightly'
+        uses: actions-rs/cargo@v1
         with:
           command: test
-          args: --all-features
+          args: --test debugger_visualizer --features "url/serde,url/debugger_visualizer" -- --test-threads=1
 
   WASM:
     runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index b40d13e..ab31bff 100644
--- a/README.md
+++ b/README.md
@@ -11,4 +11,4 @@ URL library for Rust, based on the [URL Standard](https://url.spec.whatwg.org/).
 
 [Documentation](https://docs.rs/url/)
 
-Please see [UPGRADING.md](https://github.com/servo/rust-url/blob/master/UPGRADING.md) if you are upgrading from 0.x to 1.x.
+Please see [UPGRADING.md](https://github.com/servo/rust-url/blob/master/UPGRADING.md) if you are upgrading from a previous version.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..06436bb
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,13 @@
+# Security Policy
+
+## Supported Versions
+
+| Version | Supported          |
+| ------- | ------------------ |
+| 2.2.x   | :white_check_mark: |
+
+## Reporting a Vulnerability
+
+The most secure way to report a vulnerability while this crate is maintained by a Mozilla contributor is by reporting a bug on the Bugzilla issue tracker using this [link](https://bugzilla.mozilla.org/enter_bug.cgi?product=Core&component=Networking&groups=network-core-security&short_desc=[rust-url]%20DESCRIPTION&bug_type=defect).
+
+Alternatively, you may send an email to valentin AT mozilla DOT com.
diff --git a/data-url/Cargo.toml b/data-url/Cargo.toml
index 6765f45..43c5586 100644
--- a/data-url/Cargo.toml
+++ b/data-url/Cargo.toml
@@ -7,7 +7,7 @@ repository = "https://github.com/servo/rust-url"
 license = "MIT OR Apache-2.0"
 edition = "2018"
 autotests = false
-rust-version = "1.45"
+rust-version = "1.51"
 
 [dev-dependencies]
 tester = "0.9"
diff --git a/debug_metadata/README.md b/debug_metadata/README.md
new file mode 100644
index 0000000..785a454
--- /dev/null
+++ b/debug_metadata/README.md
@@ -0,0 +1,111 @@
+## Debugger Visualizers
+
+Many languages and debuggers enable developers to control how a type is
+displayed in a debugger. These are called "debugger visualizations" or "debugger
+views".
+
+The Windows debuggers (WinDbg\CDB) support defining custom debugger visualizations using
+the `Natvis` framework. To use Natvis, developers write XML documents using the natvis
+schema that describe how debugger types should be displayed with the `.natvis` extension.
+(See: https://docs.microsoft.com/en-us/visualstudio/debugger/create-custom-views-of-native-objects?view=vs-2019)
+The Natvis files provide patterns which match type names a description of how to display
+those types.
+
+The Natvis schema can be found either online (See: https://code.visualstudio.com/docs/cpp/natvis#_schema)
+or locally at `<VS Installation Folder>\Xml\Schemas\1033\natvis.xsd`.
+
+The GNU debugger (GDB) supports defining custom debugger views using Pretty Printers.
+Pretty printers are written as python scripts that describe how a type should be displayed
+when loaded up in GDB/LLDB. (See: https://sourceware.org/gdb/onlinedocs/gdb/Pretty-Printing.html#Pretty-Printing)
+The pretty printers provide patterns, which match type names, and for matching
+types, descibe how to display those types. (For writing a pretty printer, see: https://sourceware.org/gdb/onlinedocs/gdb/Writing-a-Pretty_002dPrinter.html#Writing-a-Pretty_002dPrinter).
+
+### Embedding Visualizers
+
+Through the use of the currently unstable `#[debugger_visualizer]` attribute, the `url`
+crate can embed debugger visualizers into the crate metadata.
+
+Currently the two types of visualizers supported are Natvis and Pretty printers.
+
+For Natvis files, when linking an executable with a crate that includes Natvis files,
+the MSVC linker will embed the contents of all Natvis files into the generated `PDB`.
+
+For pretty printers, the compiler will encode the contents of the pretty printer
+in the `.debug_gdb_scripts` section of the `ELF` generated.
+
+### Testing Visualizers
+
+The `url` crate supports testing debugger visualizers defined for this crate. The entry point for
+these tests are `tests/debugger_visualizer.rs`. These tests are defined using the `debugger_test` and
+`debugger_test_parser` crates. The `debugger_test` crate is a proc macro crate which defines a
+single proc macro attribute, `#[debugger_test]`. For more detailed information about this crate,
+see https://crates.io/crates/debugger_test. The CI pipeline for the `url` crate has been updated
+to run the debugger visualizer tests to ensure debugger visualizers do not become broken/stale.
+
+The `#[debugger_test]` proc macro attribute may only be used on test functions and will run the
+function under the debugger specified by the `debugger` meta item.
+
+This proc macro attribute has 3 required values:
+
+1. The first required meta item, `debugger`, takes a string value which specifies the debugger to launch.
+2. The second required meta item, `commands`, takes a string of new line (`\n`) separated list of debugger
+commands to run.
+3. The third required meta item, `expected_statements`, takes a string of new line (`\n`) separated list of
+statements that must exist in the debugger output. Pattern matching through regular expressions is also
+supported by using the `pattern:` prefix for each expected statement.
+
+#### Example:
+
+```rust
+#[debugger_test(
+    debugger = "cdb",
+    commands = "command1\ncommand2\ncommand3",
+    expected_statements = "statement1\nstatement2\nstatement3")]
+fn test() {
+
+}
+```
+
+Using a multiline string is also supported, with a single debugger command/expected statement per line:
+
+```rust
+#[debugger_test(
+    debugger = "cdb",
+    commands = "
+command1
+command2
+command3",
+    expected_statements = "
+statement1
+pattern:statement[0-9]+
+statement3")]
+fn test() {
+    
+}
+```
+
+In the example above, the second expected statement uses pattern matching through a regular expression
+by using the `pattern:` prefix.
+
+#### Testing Locally
+
+Currently, only Natvis visualizations have been defined for the `url` crate via `debug_metadata/url.natvis`,
+which means the `tests/debugger_visualizer.rs` tests need to be run on Windows using the `*-pc-windows-msvc` targets.
+To run these tests locally, first ensure the debugging tools for Windows are installed or install them following
+the steps listed here, [Debugging Tools for Windows](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/).
+Once the debugging tools have been installed, the tests can be run in the same manner as they are in the CI
+pipeline.
+
+#### Note
+
+When running the debugger visualizer tests, `tests/debugger_visualizer.rs`, they need to be run consecutively
+and not in parallel. This can be achieved by passing the flag `--test-threads=1` to rustc. This is due to
+how the debugger tests are run. Each test marked with the `#[debugger_test]` attribute launches a debugger
+and attaches it to the current test process. If tests are running in parallel, the test will try to attach
+a debugger to the current process which may already have a debugger attached causing the test to fail.
+
+For example:
+
+```
+cargo test --test debugger_visualizer --features debugger_visualizer -- --test-threads=1
+```
diff --git a/debug_metadata/url.natvis b/debug_metadata/url.natvis
new file mode 100644
index 0000000..3712470
--- /dev/null
+++ b/debug_metadata/url.natvis
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
+  <Type Name="url::Url">
+    <Intrinsic Name="ptr" Expression="serialization.vec.buf.ptr.pointer.pointer" />
+    <DisplayString>{serialization}</DisplayString>
+    <Expand>
+      <Synthetic Name="[scheme]">
+        <DisplayString>{(char*)(ptr()),[scheme_end]s8}</DisplayString>
+      </Synthetic>
+      <Synthetic Name="[username]" Condition="username_end > (scheme_end + 3)">
+        <!-- Add 3 to the scheme end to account for the scheme separator which is '://' -->
+        <DisplayString>{(char*)(ptr()+(scheme_end + 3)),[((username_end)-(scheme_end + 3))]s8}</DisplayString>
+      </Synthetic>
+      <Synthetic Name="[host]" Condition="host.tag != 0">
+        <DisplayString>{(char*)(ptr()+host_start),[host_end-host_start]s8}</DisplayString>
+      </Synthetic>
+      <Synthetic Name="[port]" Condition="port.tag == 1">
+        <DisplayString>{port.variant1.value.__0,d}</DisplayString>
+      </Synthetic>
+      <Synthetic Name="[path]">
+        <DisplayString Condition="query_start.tag == 0 &amp;&amp; fragment_start.tag == 0">{(char*)(ptr()+path_start),[(serialization.vec.len-path_start)]s8}</DisplayString>
+        <DisplayString Condition="query_start.tag == 1">{(char*)(ptr()+path_start),[(query_start.variant1.value.__0-path_start)]s8}</DisplayString>
+        <DisplayString Condition="fragment_start.tag == 1">{(char*)(ptr()+path_start),[(fragment_start.variant1.value.__0-path_start)]s8}</DisplayString>
+      </Synthetic>
+      <Synthetic Name="[query]" Condition="query_start.tag == 1">
+        <DisplayString Condition="fragment_start.tag == 0">{(char*)(ptr()+query_start.variant1.value.__0+1),[((serialization.vec.len)-(query_start.variant1.value.__0+1))]s8}</DisplayString>
+        <DisplayString Condition="fragment_start.tag == 1">{(char*)(ptr()+query_start.variant1.value.__0+1),[((fragment_start.variant1.value.__0)-(query_start.variant1.value.__0+1))]s8}</DisplayString>
+      </Synthetic>
+      <Synthetic Name="[fragment]" Condition="fragment_start.tag == 1">
+        <DisplayString>{(char*)(ptr()+fragment_start.variant1.value.__0+1),[(serialization.vec.len-fragment_start.variant1.value.__0-1)]s8}</DisplayString>
+      </Synthetic>
+    </Expand>
+  </Type>
+</AutoVisualizer>
diff --git a/form_urlencoded/Cargo.toml b/form_urlencoded/Cargo.toml
index c778d2d..6191cb8 100644
--- a/form_urlencoded/Cargo.toml
+++ b/form_urlencoded/Cargo.toml
@@ -4,9 +4,9 @@ version = "1.0.1"
 authors = ["The rust-url developers"]
 description = "Parser and serializer for the application/x-www-form-urlencoded syntax, as used by HTML forms."
 repository = "https://github.com/servo/rust-url"
-license = "MIT/Apache-2.0"
+license = "MIT OR Apache-2.0"
 edition = "2018"
-rust-version = "1.45"
+rust-version = "1.51"
 
 [lib]
 test = false
diff --git a/form_urlencoded/src/lib.rs b/form_urlencoded/src/lib.rs
index b0916e4..477594b 100644
--- a/form_urlencoded/src/lib.rs
+++ b/form_urlencoded/src/lib.rs
@@ -399,8 +399,7 @@ pub(crate) fn decode_utf8_lossy(input: Cow<'_, [u8]>) -> Cow<'_, str> {
                     // replace invalid bytes with a placeholder.
 
                     // First we do a debug_assert to confirm our description above.
-                    let raw_utf8: *const [u8];
-                    raw_utf8 = utf8.as_bytes();
+                    let raw_utf8: *const [u8] = utf8.as_bytes();
                     debug_assert!(raw_utf8 == &*bytes as *const [u8]);
 
                     // Given we know the original input bytes are valid UTF-8,
diff --git a/idna/Cargo.toml b/idna/Cargo.toml
index 9566cc7..2c77469 100644
--- a/idna/Cargo.toml
+++ b/idna/Cargo.toml
@@ -4,10 +4,10 @@ version = "0.2.3"
 authors = ["The rust-url developers"]
 description = "IDNA (Internationalizing Domain Names in Applications) and Punycode."
 repository = "https://github.com/servo/rust-url/"
-license = "MIT/Apache-2.0"
+license = "MIT OR Apache-2.0"
 autotests = false
 edition = "2018"
-rust-version = "1.45"
+rust-version = "1.51"
 
 [lib]
 doctest = false
diff --git a/idna/src/uts46.rs b/idna/src/uts46.rs
index 9ee83bc..ec2fd0b 100644
--- a/idna/src/uts46.rs
+++ b/idna/src/uts46.rs
@@ -318,50 +318,48 @@ fn check_validity(label: &str, config: Config, errors: &mut Errors) {
     // V8: Bidi rules are checked inside `processing()`
 }
 
-/// http://www.unicode.org/reports/tr46/#Processing
-fn processing(
-    domain: &str,
-    config: Config,
-    normalized: &mut String,
-    output: &mut String,
-) -> Errors {
-    // Weed out the simple cases: only allow all lowercase ASCII characters and digits where none
-    // of the labels start with PUNYCODE_PREFIX and labels don't start or end with hyphen.
-    let (mut prev, mut simple, mut puny_prefix) = ('?', !domain.is_empty(), 0);
+// Detect simple cases: all lowercase ASCII characters and digits where none
+// of the labels start with PUNYCODE_PREFIX and labels don't start or end with hyphen.
+fn is_simple(domain: &str) -> bool {
+    if domain.is_empty() {
+        return false;
+    }
+    let (mut prev, mut puny_prefix) = ('?', 0);
     for c in domain.chars() {
         if c == '.' {
             if prev == '-' {
-                simple = false;
-                break;
+                return false;
             }
             puny_prefix = 0;
             continue;
         } else if puny_prefix == 0 && c == '-' {
-            simple = false;
-            break;
+            return false;
         } else if puny_prefix < 5 {
             if c == ['x', 'n', '-', '-'][puny_prefix] {
                 puny_prefix += 1;
                 if puny_prefix == 4 {
-                    simple = false;
-                    break;
+                    return false;
                 }
             } else {
                 puny_prefix = 5;
             }
         }
         if !c.is_ascii_lowercase() && !c.is_ascii_digit() {
-            simple = false;
-            break;
+            return false;
         }
         prev = c;
     }
 
-    if simple {
-        output.push_str(domain);
-        return Errors::default();
-    }
+    true
+}
 
+/// http://www.unicode.org/reports/tr46/#Processing
+fn processing(
+    domain: &str,
+    config: Config,
+    normalized: &mut String,
+    output: &mut String,
+) -> Errors {
     normalized.clear();
     let mut errors = Errors::default();
     let offset = output.len();
@@ -447,11 +445,13 @@ impl Idna {
         }
     }
 
-    /// http://www.unicode.org/reports/tr46/#ToASCII
-    #[allow(clippy::wrong_self_convention)]
-    pub fn to_ascii<'a>(&'a mut self, domain: &str, out: &mut String) -> Result<(), Errors> {
-        let mut errors = processing(domain, self.config, &mut self.normalized, &mut self.output);
-
+    pub fn to_ascii_inner(&mut self, domain: &str, out: &mut String) -> Errors {
+        if is_simple(domain) {
+            out.push_str(domain);
+            return Errors::default();
+        }
+        let mut errors = processing(domain, self.config, &mut self.normalized, out);
+        self.output = std::mem::replace(out, String::with_capacity(out.len()));
         let mut first = true;
         for label in self.output.split('.') {
             if !first {
@@ -470,6 +470,13 @@ impl Idna {
                 }
             }
         }
+        errors
+    }
+
+    /// http://www.unicode.org/reports/tr46/#ToASCII
+    #[allow(clippy::wrong_self_convention)]
+    pub fn to_ascii<'a>(&'a mut self, domain: &str, out: &mut String) -> Result<(), Errors> {
+        let mut errors = self.to_ascii_inner(domain, out);
 
         if self.config.verify_dns_length {
             let domain = if out.ends_with('.') {
@@ -491,6 +498,10 @@ impl Idna {
     /// http://www.unicode.org/reports/tr46/#ToUnicode
     #[allow(clippy::wrong_self_convention)]
     pub fn to_unicode<'a>(&'a mut self, domain: &str, out: &mut String) -> Result<(), Errors> {
+        if is_simple(domain) {
+            out.push_str(domain);
+            return Errors::default().into();
+        }
         processing(domain, self.config, &mut self.normalized, out).into()
     }
 }
@@ -554,7 +565,7 @@ impl Config {
 
     /// http://www.unicode.org/reports/tr46/#ToASCII
     pub fn to_ascii(self, domain: &str) -> Result<String, Errors> {
-        let mut result = String::new();
+        let mut result = String::with_capacity(domain.len());
         let mut codec = Idna::new(self);
         codec.to_ascii(domain, &mut result).map(|()| result)
     }
diff --git a/percent_encoding/Cargo.toml b/percent_encoding/Cargo.toml
index bfc458d..5b271c9 100644
--- a/percent_encoding/Cargo.toml
+++ b/percent_encoding/Cargo.toml
@@ -4,9 +4,9 @@ version = "2.1.0"
 authors = ["The rust-url developers"]
 description = "Percent encoding and decoding"
 repository = "https://github.com/servo/rust-url/"
-license = "MIT/Apache-2.0"
+license = "MIT OR Apache-2.0"
 edition = "2018"
-rust-version = "1.45"
+rust-version = "1.51"
 
 [features]
 default = ["alloc"]
diff --git a/percent_encoding/src/lib.rs b/percent_encoding/src/lib.rs
index 2517ae8..46a5d74 100644
--- a/percent_encoding/src/lib.rs
+++ b/percent_encoding/src/lib.rs
@@ -453,8 +453,7 @@ fn decode_utf8_lossy(input: Cow<'_, [u8]>) -> Cow<'_, str> {
                     // replace invalid bytes with a placeholder.
 
                     // First we do a debug_assert to confirm our description above.
-                    let raw_utf8: *const [u8];
-                    raw_utf8 = utf8.as_bytes();
+                    let raw_utf8: *const [u8] = utf8.as_bytes();
                     debug_assert!(raw_utf8 == &*bytes as *const [u8]);
 
                     // Given we know the original input bytes are valid UTF-8,
diff --git a/url/Cargo.toml b/url/Cargo.toml
index 3955078..1747e4a 100644
--- a/url/Cargo.toml
+++ b/url/Cargo.toml
@@ -11,10 +11,10 @@ repository = "https://github.com/servo/rust-url"
 readme = "../README.md"
 keywords = ["url", "parser"]
 categories = ["parser-implementations", "web-programming", "encoding"]
-license = "MIT/Apache-2.0"
+license = "MIT OR Apache-2.0"
 include = ["src/**/*", "LICENSE-*", "README.md", "tests/**"]
 edition = "2018"
-rust-version = "1.45"
+rust-version = "1.51"
 
 [badges]
 travis-ci = { repository = "servo/rust-url" }
@@ -23,6 +23,9 @@ appveyor = { repository = "Manishearth/rust-url" }
 [dev-dependencies]
 serde_json = "1.0"
 bencher = "0.1"
+# To test debugger visualizers defined for the url crate such as url.natvis
+debugger_test = "0.1"
+debugger_test_parser = "0.1"
 
 [dependencies]
 form_urlencoded = { version = "1.0.0", path = "../form_urlencoded" }
@@ -32,8 +35,17 @@ serde = {version = "1.0", optional = true, features = ["derive"]}
 
 [features]
 default = ["idna"]
+# UNSTABLE FEATURES (requires Rust nightly)
+# Enable to use the #[debugger_visualizer] attribute.
+debugger_visualizer = []
 
 [[bench]]
 name = "parse_url"
 path = "benches/parse_url.rs"
 harness = false
+
+[[test]]
+name = "debugger_visualizer"
+path = "tests/debugger_visualizer.rs"
+required-features = ["debugger_visualizer"]
+test = false
diff --git a/url/benches/parse_url.rs b/url/benches/parse_url.rs
index 6e21fdb..cc87dda 100644
--- a/url/benches/parse_url.rs
+++ b/url/benches/parse_url.rs
@@ -12,5 +12,12 @@ fn short(bench: &mut Bencher) {
     bench.iter(|| black_box(url).parse::<Url>().unwrap());
 }
 
-benchmark_group!(benches, short);
+fn long(bench: &mut Bencher) {
+    let url = "https://example.com/parkbench?tre=es&st=uff";
+
+    bench.bytes = url.len() as u64;
+    bench.iter(|| black_box(url).parse::<Url>().unwrap());
+}
+
+benchmark_group!(benches, short, long);
 benchmark_main!(benches);
diff --git a/url/src/lib.rs b/url/src/lib.rs
index 8bdd378..967282e 100644
--- a/url/src/lib.rs
+++ b/url/src/lib.rs
@@ -131,6 +131,11 @@ url = { version = "2", default-features = false }
 */
 
 #![doc(html_root_url = "https://docs.rs/url/2.2.2")]
+#![cfg_attr(
+    feature = "debugger_visualizer",
+    feature(debugger_visualizer),
+    debugger_visualizer(natvis_file = "../../debug_metadata/url.natvis")
+)]
 
 pub use form_urlencoded;
 
@@ -468,7 +473,7 @@ impl Url {
         }
 
         // Add the filename if they are not the same
-        if base_filename != url_filename {
+        if !relative.is_empty() || base_filename != url_filename {
             // If the URIs filename is empty this means that it was a directory
             // so we'll have to append a '/'.
             //
@@ -1307,7 +1312,7 @@ impl Url {
     /// # Ok(())
     /// # }
     /// # run().unwrap();
-    ///
+    /// ```
 
     #[inline]
     pub fn query_pairs(&self) -> form_urlencoded::Parse<'_> {
diff --git a/url/src/parser.rs b/url/src/parser.rs
index ed4ca3c..b83bdd4 100644
--- a/url/src/parser.rs
+++ b/url/src/parser.rs
@@ -143,7 +143,7 @@ impl fmt::Display for SyntaxViolation {
     }
 }
 
-#[derive(Copy, Clone, PartialEq)]
+#[derive(Copy, Clone, PartialEq, Eq)]
 pub enum SchemeType {
     File,
     SpecialNotFile,
@@ -1216,13 +1216,11 @@ impl<'a> Parser<'a> {
                     }
                 }
             }
-            // Going from &str to String to &str to please the 1.33.0 borrow checker
-            let before_slash_string = if ends_with_slash {
-                self.serialization[segment_start..self.serialization.len() - 1].to_owned()
+            let segment_before_slash = if ends_with_slash {
+                &self.serialization[segment_start..self.serialization.len() - 1]
             } else {
-                self.serialization[segment_start..self.serialization.len()].to_owned()
+                &self.serialization[segment_start..self.serialization.len()]
             };
-            let segment_before_slash: &str = &before_slash_string;
             match segment_before_slash {
                 // If buffer is a double-dot path segment, shorten url’s path,
                 ".." | "%2e%2e" | "%2e%2E" | "%2E%2e" | "%2E%2E" | "%2e." | "%2E." | ".%2e"
@@ -1412,7 +1410,8 @@ impl<'a> Parser<'a> {
         scheme_end: u32,
         mut input: Input<'i>,
     ) -> Option<Input<'i>> {
-        let mut query = String::new(); // FIXME: use a streaming decoder instead
+        let len = input.chars.as_str().len();
+        let mut query = String::with_capacity(len); // FIXME: use a streaming decoder instead
         let mut remaining = None;
         while let Some(c) = input.next() {
             if c == '#' && self.context == Context::UrlParser {
@@ -1562,7 +1561,7 @@ pub fn is_windows_drive_letter(segment: &str) -> bool {
 /// Whether path starts with a root slash
 /// and a windows drive letter eg: "/c:" or "/a:/"
 fn path_starts_with_windows_drive_letter(s: &str) -> bool {
-    if let Some(c) = s.as_bytes().get(0) {
+    if let Some(c) = s.as_bytes().first() {
         matches!(c, b'/' | b'\\' | b'?' | b'#') && starts_with_windows_drive_letter(&s[1..])
     } else {
         false


Test patch:
diff --git a/idna/tests/uts46.rs b/idna/tests/uts46.rs
index 03ba821..bd402ce 100644
--- a/idna/tests/uts46.rs
+++ b/idna/tests/uts46.rs
@@ -8,6 +8,7 @@
 
 use crate::test::TestFn;
 use std::char;
+use std::fmt::Write;
 
 use idna::Errors;
 
@@ -160,8 +161,8 @@ fn unescape(input: &str) -> String {
                             match char::from_u32(((c1 * 16 + c2) * 16 + c3) * 16 + c4) {
                                 Some(c) => output.push(c),
                                 None => {
-                                    output
-                                        .push_str(&format!("\\u{:X}{:X}{:X}{:X}", c1, c2, c3, c4));
+                                    write!(&mut output, "\\u{:X}{:X}{:X}{:X}", c1, c2, c3, c4)
+                                        .expect("Could not write to output");
                                 }
                             };
                         }
diff --git a/url/tests/data.rs b/url/tests/data.rs
index 5ec642d..e054708 100644
--- a/url/tests/data.rs
+++ b/url/tests/data.rs
@@ -149,7 +149,7 @@ fn setters_tests() {
             let mut url = Url::parse(&href).unwrap();
             let comment_ref = comment.as_deref();
             passed &= check_invariants(&url, &name, comment_ref);
-            let _ = set(&mut url, attr, &new_value);
+            set(&mut url, attr, &new_value);
 
             for attr in ATTRIBS {
                 if let Some(value) = expected.take_key(attr) {
diff --git a/url/tests/debugger_visualizer.rs b/url/tests/debugger_visualizer.rs
new file mode 100644
index 0000000..4558e07
--- /dev/null
+++ b/url/tests/debugger_visualizer.rs
@@ -0,0 +1,102 @@
+use debugger_test::debugger_test;
+use url::Url;
+
+#[inline(never)]
+fn __break() {}
+
+#[debugger_test(
+    debugger = "cdb",
+    commands = "
+    .nvlist
+
+    dx base_url
+
+    dx url_with_non_special_scheme
+
+    dx url_with_user_pass_port_query_fragments
+
+    dx url_blob
+
+    dx url_with_base
+
+    dx url_with_base_replaced
+
+    dx url_with_comma",
+    expected_statements = r#"
+    pattern:debugger_visualizer-.*\.exe \(embedded NatVis ".*-[0-9]+\.natvis"\)
+
+    base_url         : "http://example.org/foo/bar" [Type: url::Url]
+    [<Raw View>]     [Type: url::Url]
+    [scheme]         : "http"
+    [host]           : "example.org"
+    [path]           : "/foo/bar"
+
+    url_with_non_special_scheme : "non-special://test/x" [Type: url::Url]
+    [<Raw View>]     [Type: url::Url]
+    [scheme]         : "non-special"
+    [host]           : "test"
+    [path]           : "/x"
+
+    url_with_user_pass_port_query_fragments : "http://user:pass@foo:21/bar;par?b#c" [Type: url::Url]
+    [<Raw View>]     [Type: url::Url]
+    [scheme]         : "http"
+    [username]       : "user"
+    [host]           : "foo"
+    [port]           : 21
+    [path]           : "/bar;par"
+    [query]          : "b"
+    [fragment]       : "c"
+
+    url_blob         : "blob:https://example.com:443/" [Type: url::Url]
+    [<Raw View>]     [Type: url::Url]
+    [scheme]         : "blob"
+    [path]           : "https://example.com:443/"
+
+    url_with_base    : "http://example.org/a%2fc" [Type: url::Url]
+    [<Raw View>]     [Type: url::Url]
+    [scheme]         : "http"
+    [host]           : "example.org"
+    [path]           : "/a%2fc"
+
+    url_with_base_replaced : "http://[::7f00:1]/" [Type: url::Url]
+    [<Raw View>]     [Type: url::Url]
+    [scheme]         : "http"
+    [host]           : "[::7f00:1]"
+    [path]           : "/"
+
+    url_with_comma   : "data:text/html,test#test" [Type: url::Url]
+    [<Raw View>]     [Type: url::Url]
+    [scheme]         : "data"
+    [path]           : "text/html,test"
+    [fragment]       : "test"
+    "#
+)]
+fn test_url_visualizer() {
+    // Copied from https://github.com/web-platform-tests/wpt/blob/master/url/
+    let base_url = Url::parse("http://example.org/foo/bar").unwrap();
+    assert_eq!(base_url.as_str(), "http://example.org/foo/bar");
+
+    let url_with_non_special_scheme = Url::parse("non-special://:@test/x").unwrap();
+    assert_eq!(url_with_non_special_scheme.as_str(), "non-special://test/x");
+
+    let url_with_user_pass_port_query_fragments =
+        Url::parse("http://user:pass@foo:21/bar;par?b#c").unwrap();
+    assert_eq!(
+        url_with_user_pass_port_query_fragments.as_str(),
+        "http://user:pass@foo:21/bar;par?b#c"
+    );
+
+    let url_blob = Url::parse("blob:https://example.com:443/").unwrap();
+    assert_eq!(url_blob.as_str(), "blob:https://example.com:443/");
+
+    let url_with_base = base_url.join("/a%2fc").unwrap();
+    assert_eq!(url_with_base.as_str(), "http://example.org/a%2fc");
+
+    let url_with_base_replaced = base_url.join("http://[::127.0.0.1]").unwrap();
+    assert_eq!(url_with_base_replaced.as_str(), "http://[::7f00:1]/");
+
+    let url_with_comma = base_url.join("data:text/html,test#test").unwrap();
+    assert_eq!(url_with_comma.as_str(), "data:text/html,test#test");
+
+    __break();
+}
diff --git a/url/tests/unit.rs b/url/tests/unit.rs
index 0fa99b6..401eaf9 100644
--- a/url/tests/unit.rs
+++ b/url/tests/unit.rs
@@ -799,7 +799,7 @@ fn test_syntax_violation_callback_types() {
         ("file:/foo.txt", ExpectedFileDoubleSlash, "expected // after file:"),
         ("file://mozilla.org/c:/file.txt", FileWithHostAndWindowsDrive, "file: with host and Windows drive letter"),
         ("http://mozilla.org/^", NonUrlCodePoint, "non-URL code point"),
-        ("http://mozilla.org/#\00", NullInFragment, "NULL characters are ignored in URL fragment identifiers"),
+        ("http://mozilla.org/#\x000", NullInFragment, "NULL characters are ignored in URL fragment identifiers"),
         ("http://mozilla.org/%1", PercentDecode, "expected 2 hex digits after %"),
         ("http://mozilla.org\t/foo", TabOrNewlineIgnored, "tabs or newlines are ignored in URLs"),
         ("http://user@:pass@mozilla.org", UnencodedAtSign, "unencoded @ sign in username or password")
@@ -1084,6 +1084,16 @@ fn test_make_relative() {
             "http://127.0.0.1:8080/test/video?baz=meh#456",
             "video?baz=meh#456",
         ),
+        (
+            "http://127.0.0.1:8080/file.txt",
+            "http://127.0.0.1:8080/test/file.txt",
+            "test/file.txt",
+        ),
+        (
+            "http://127.0.0.1:8080/not_equal.txt",
+            "http://127.0.0.1:8080/test/file.txt",
+            "test/file.txt",
+        ),
     ];
 
     for (base, uri, relative) in &tests {