Unified · Split
| @@ -62,12 +62,25 @@ mod access { · 61 unmodified lines | ||
| 62 | 62 | index += 1; |
| 63 | 63 | } |
| 64 | 64 | } else { |
| 65 | − index += usize::try_from(rlw_running_len_bits(word)).ok()?; | |
| 65 | + let len = usize::try_from(rlw_running_len_bits(word)).ok()?; | |
| 66 | + let end = index.checked_add(len)?; | |
| 67 | + if end > num_bits { | |
| 68 | + return None; | |
| 69 | + } | |
| 70 | + index = end; | |
| 66 | 71 | } |
| 67 | 72 | |
| 68 | 73 | for _ in 0..rlw_literal_words(word) { |
| 69 | 74 | let word = iter.next()?; |
| 70 | − for bit_index in 0..64 { | |
| 75 | + let remaining = num_bits.checked_sub(index)?; | |
| 76 | + if remaining == 0 { | |
| 77 | + return None; | |
| 78 | + } | |
| 79 | + let bits_in_word = remaining.min(64); | |
| 80 | + if bits_in_word < 64 && (word >> bits_in_word) != 0 { | |
| 81 | + return None; | |
| 82 | + } | |
| 83 | + for bit_index in 0..bits_in_word { | |
| 71 | 84 | if word & (1 << bit_index) != 0 { |
| 72 | 85 | f(index)?; |
| 73 | 86 | } |
| @@ -22,6 +22,27 @@ mod fuzzed { · 21 unmodified lines | ||
| 22 | 22 | ); |
| 23 | 23 | } |
| 24 | 24 | |
| 25 | + #[test] | |
| 26 | + fn non_zero_padding_bits_in_last_literal_word_are_rejected() { | |
| 27 | + let mut data = Vec::new(); | |
| 28 | + // One logical bit, two compressed words, then RLW position 0. | |
| 29 | + data.extend_from_slice(&1u32.to_be_bytes()); | |
| 30 | + data.extend_from_slice(&2u32.to_be_bytes()); | |
| 31 | + // The only in-range bit is bit 0 and it is unset; bit 33 is set only in padding. | |
| 32 | + data.extend_from_slice(&(1u64 << 33).to_be_bytes()); | |
| 33 | + data.extend_from_slice(&2u64.to_be_bytes()); | |
| 34 | + data.extend_from_slice(&0u32.to_be_bytes()); | |
| 35 | + | |
| 36 | + let (bitmap, rest) = gix_bitmap::ewah::decode(&data).expect("fixture must decode"); | |
| 37 | + | |
| 38 | + assert!(rest.is_empty(), "fixture should be fully consumed"); | |
| 39 | + assert_eq!( | |
| 40 | + bitmap.for_each_set_bit(|_| Some(())), | |
| 41 | + None, | |
| 42 | + "set bits outside the declared bit length must be rejected" | |
| 43 | + ); | |
| 44 | + } | |
| 45 | + | |
| 25 | 46 | fn artifact_paths(target: &str) -> Vec<std::path::PathBuf> { |
| 26 | 47 | std::fs::read_dir( |
| 27 | 48 | std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) |
| @@ -16,7 +16,7 @@ pub fn leb64_from_read(mut r: impl Read) -> Result<(u64, usize), std::io::Error> · 15 unmodified lines | ||
| 16 | 16 | .checked_add(1) |
| 17 | 17 | .and_then(|value| value.checked_shl(7)) |
| 18 | 18 | .and_then(|value| value.checked_add(u64::from(c) & 0x7f)) |
| 19 | − .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "ofs-delta distance overflowed"))?; | |
| 19 | + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "LEB64 value overflowed"))?; | |
| 20 | 20 | } |
| 21 | 21 | Ok((value, i)) |
| 22 | 22 | } |
| @@ -29,7 +29,7 @@ pub enum Error { · 28 unmodified lines | ||
| 29 | 29 | |
| 30 | 30 | /// Object lookup |
| 31 | 31 | impl Store { |
| 32 | − const MAP_ACTION: &'static str = "open"; | |
| 32 | + const OPEN_OR_MAP_ACTION: &'static str = "open or map"; | |
| 33 | 33 | |
| 34 | 34 | /// Returns true if the given id is contained in our repository. |
| 35 | 35 | pub fn contains(&self, id: &gix_hash::oid) -> bool { |
| @@ -200,7 +200,7 @@ impl Store { · 164 unmodified lines | ||
| 200 | 200 | zlib::stream::inflate::read(&mut input, &mut inflate.state, &mut out[decompressed_body_prefix_len..]) |
| 201 | 201 | .map_err(|e| Error::Io { |
| 202 | 202 | source: e, |
| 203 | − action: "deflate", | |
| 203 | + action: "inflate", | |
| 204 | 204 | path: path.to_owned(), |
| 205 | 205 | })?; |
| 206 | 206 | |
| @@ -232,7 +232,7 @@ impl Store { · 25 unmodified lines | ||
| 232 | 232 | Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), |
| 233 | 233 | Err(err) => { |
| 234 | 234 | return Err(Error::Io { |
| 235 | − action: Self::MAP_ACTION, | |
| 235 | + action: Self::OPEN_OR_MAP_ACTION, | |
| 236 | 236 | source: err, |
| 237 | 237 | path: path.to_owned(), |
| 238 | 238 | }) |
| @@ -242,7 +242,7 @@ impl Store { · 3 unmodified lines | ||
| 242 | 242 | if map.is_empty() { |
| 243 | 243 | return Err(Error::Io { |
| 244 | 244 | source: io::Error::other("empty loose object file"), |
| 245 | − action: Self::MAP_ACTION, | |
| 245 | + action: Self::OPEN_OR_MAP_ACTION, | |
| 246 | 246 | path: path.to_owned(), |
| 247 | 247 | }); |
| 248 | 248 | } |
| @@ -3,7 +3,7 @@ · 2 unmodified lines | ||
| 3 | 3 | use anyhow::Result; |
| 4 | 4 | use gix_features::{progress, zlib}; |
| 5 | 5 | use gix_pack::{cache, data}; |
| 6 | −use gix_pack_fuzz::{virtual_path, interrupt_flag}; | |
| 6 | +use gix_pack_fuzz::{interrupt_flag, virtual_path}; | |
| 7 | 7 | use libfuzzer_sys::fuzz_target; |
| 8 | 8 | use std::hint::black_box; |
| 9 | 9 | |
| @@ -4,7 +4,7 @@ use anyhow::Result; · 3 unmodified lines | ||
| 4 | 4 | use gix_features::progress; |
| 5 | 5 | use gix_hash::Prefix; |
| 6 | 6 | use gix_pack::multi_index; |
| 7 | −use gix_pack_fuzz::{interrupt_flag, virtual_path, empty_candidates}; | |
| 7 | +use gix_pack_fuzz::{empty_candidates, interrupt_flag, virtual_path}; | |
| 8 | 8 | use libfuzzer_sys::fuzz_target; |
| 9 | 9 | use std::hint::black_box; |
| 10 | 10 | |
| @@ -110,11 +110,11 @@ pub(crate) fn apply(base: &[u8], mut target: &mut [u8], data: &[u8]) -> Result<( · 109 unmodified lines | ||
| 110 | 110 | } |
| 111 | 111 | } |
| 112 | 112 | } |
| 113 | − if i != data.len() { | |
| 114 | − return Err(apply::Error::Corrupt { | |
| 115 | − message: "delta instructions were not consumed completely", | |
| 116 | − }); | |
| 117 | − } | |
| 113 | + debug_assert_eq!( | |
| 114 | + i, | |
| 115 | + data.len(), | |
| 116 | + "delta instructions were not consumed completely, should be impossible" | |
| 117 | + ); | |
| 118 | 118 | if !target.is_empty() { |
| 119 | 119 | return Err(apply::Error::Corrupt { |
| 120 | 120 | message: "delta instructions produced fewer bytes than promised", |
| @@ -11,7 +11,7 @@ use crate::data; · 10 unmodified lines | ||
| 11 | 11 | pub enum Error { |
| 12 | 12 | #[error("Object type {type_id} is unsupported")] |
| 13 | 13 | UnsupportedType { type_id: u8 }, |
| 14 | − #[error("Pack entry is truncated while reading {message}")] | |
| 14 | + #[error("Pack entry is truncated: {message}")] | |
| 15 | 15 | Corrupt { message: &'static str }, |
| 16 | 16 | #[error("Pack entry header value overflowed while decoding")] |
| 17 | 17 | Overflow, |
| @@ -142,7 +142,7 @@ fn parse_header_info(data: &[u8]) -> Result<(u8, u64, usize), Error> { · 124 unmodified lines | ||
| 142 | 142 | let mut shift = 4u32; |
| 143 | 143 | while c & 0b1000_0000 != 0 { |
| 144 | 144 | c = *data.get(i).ok_or(Error::Corrupt { |
| 145 | − message: "Pack entry header continuation byte is missing", | |
| 145 | + message: "pack entry header continuation byte", | |
| 146 | 146 | })?; |
| 147 | 147 | i += 1; |
| 148 | 148 | let component = u64::from(c & 0b0111_1111).checked_shl(shift).ok_or(Error::Overflow)?; |
| @@ -153,14 +153,15 @@ fn validate_size(data: &[u8], kind: Version, num_objects: u32, hash_len: usize) · 152 unmodified lines | ||
| 153 | 153 | ), |
| 154 | 154 | }); |
| 155 | 155 | } |
| 156 | − let large_offset_indices: Vec<_> = data[offset32_start..offset32_end] | |
| 156 | + let (large_offsets, max_large_offset_index) = data[offset32_start..offset32_end] | |
| 157 | 157 | .chunks_exact(N32_SIZE) |
| 158 | 158 | .filter_map(|offset| { |
| 159 | 159 | let offset = crate::read_u32(offset); |
| 160 | 160 | (offset & (1 << 31) != 0).then_some((offset ^ (1 << 31)) as usize) |
| 161 | 161 | }) |
| 162 | − .collect(); | |
| 163 | − let large_offsets = large_offset_indices.len(); | |
| 162 | + .fold((0usize, 0usize), |(count, max_index), index| { | |
| 163 | + (count + 1, max_index.max(index)) | |
| 164 | + }); | |
| 164 | 165 | v2_header_size |
| 165 | 166 | .checked_add(oid_bytes) |
| 166 | 167 | .and_then(|size| size.checked_add(table_bytes)) |
| @@ -171,7 +172,6 @@ fn validate_size(data: &[u8], kind: Version, num_objects: u32, hash_len: usize) · 4 unmodified lines | ||
| 171 | 172 | message: "Pack index size overflowed while validating version 2 layout".into(), |
| 172 | 173 | }) |
| 173 | 174 | .and_then(|expected_size| { |
| 174 | − let max_large_offset_index = large_offset_indices.into_iter().max().unwrap_or(0); | |
| 175 | 175 | if large_offsets > 0 && max_large_offset_index >= large_offsets { |
| 176 | 176 | return Err(Error::Corrupt { |
| 177 | 177 | message: format!( |
| @@ -22,7 +22,7 @@ pub struct Outcome { · 21 unmodified lines | ||
| 22 | 22 | pub num_objects: u32, |
| 23 | 23 | } |
| 24 | 24 | |
| 25 | −/// The progress ids used in [`write_data_iter_from_stream()`][crate::index::write_data_iter_to_stream()]. | |
| 25 | +/// The progress ids used in [`write_data_iter_to_stream()`][crate::index::write_data_iter_to_stream()]. | |
| 26 | 26 | /// |
| 27 | 27 | /// Use this information to selectively extract the progress of interest in case the parent application has custom visualization. |
| 28 | 28 | #[derive(Debug, Copy, Clone)] |
| @@ -203,7 +203,7 @@ pub mod lookup { · 202 unmodified lines | ||
| 203 | 203 | |
| 204 | 204 | /// Return true if the size of the `offset` range seems to match for a `hash` of the given kind and the amount of objects. |
| 205 | 205 | pub fn is_valid(offset: &Range<usize>, hash: gix_hash::Kind, num_objects: u32) -> bool { |
| 206 | − (offset.end - offset.start) == num_objects as usize * hash.len_in_bytes() | |
| 206 | + (offset.end - offset.start) == (num_objects as usize).saturating_mul(hash.len_in_bytes()) | |
| 207 | 207 | } |
| 208 | 208 | } |
| 209 | 209 | |
| @@ -251,10 +251,10 @@ pub mod offsets { · 41 unmodified lines | ||
| 251 | 251 | Ok(()) |
| 252 | 252 | } |
| 253 | 253 | |
| 254 | − /// Returns true if the `offset` range seems to match the size required for `num_objects`. | |
| 254 | + /// Returns true if the `offset` range seems to match the size required for the untrusted `num_objects`. | |
| 255 | 255 | pub fn is_valid(offset: &Range<usize>, num_objects: u32) -> bool { |
| 256 | 256 | let entry_size = 4 /* pack-id */ + 4 /* pack-offset */; |
| 257 | − (offset.end - offset.start) == num_objects as usize * entry_size | |
| 257 | + (offset.end - offset.start) == (num_objects as usize).saturating_mul(entry_size) | |
| 258 | 258 | } |
| 259 | 259 | } |
| 260 | 260 | |
| @@ -34,6 +34,8 @@ pub mod integrity { · 33 unmodified lines | ||
| 34 | 34 | Fan { index: usize }, |
| 35 | 35 | #[error("The multi-index claims to have no objects")] |
| 36 | 36 | Empty, |
| 37 | + #[error("The multi-index path '{path}' has no parent directory")] | |
| 38 | + InvalidPath { path: std::path::PathBuf }, | |
| 37 | 39 | #[error("Interrupted")] |
| 38 | 40 | Interrupted, |
| 39 | 41 | } |
| @@ -142,7 +144,11 @@ where · 102 unmodified lines | ||
| 142 | 144 | C: crate::cache::DecodeEntry, |
| 143 | 145 | F: Fn() -> C + Send + Clone, |
| 144 | 146 | { |
| 145 | − let parent = self.path.parent().unwrap_or_else(|| std::path::Path::new("")); | |
| 147 | + let parent = self.path.parent().ok_or_else(|| { | |
| 148 | + index::traverse::Error::Processor(integrity::Error::InvalidPath { | |
| 149 | + path: self.path.clone(), | |
| 150 | + }) | |
| 151 | + })?; | |
| 146 | 152 | |
| 147 | 153 | let actual_index_checksum = self |
| 148 | 154 | .verify_checksum( |
| @@ -6,7 +6,7 @@ fn multi_index() -> (File, PathBuf) { · 5 unmodified lines | ||
| 6 | 6 | let path = crate::scripted_fixture_read_only("make_pack_gen_repo_multi_index.sh") |
| 7 | 7 | .expect("test fixture exists") |
| 8 | 8 | .join(".git/objects/pack/multi-pack-index"); |
| 9 | − let file = gix_pack::multi_index::File::at(&path, None).unwrap(); | |
| 9 | + let file = gix_pack::multi_index::File::at(&path, None).expect("multi-index fixture opens"); | |
| 10 | 10 | (file, path) |
| 11 | 11 | } |
| 12 | 12 | |
| @@ -6,21 +6,36 @@ pub struct Error { · 5 unmodified lines | ||
| 6 | 6 | expected_url: String, |
| 7 | 7 | } |
| 8 | 8 | |
| 9 | −fn shares_authority(lhs: &str, rhs: &str) -> bool { | |
| 10 | − let Ok(lhs) = gix_url::parse(lhs.into()) else { | |
| 9 | +pub(crate) fn shares_authority_or_upgrades_scheme(redirect_url: &str, original_url: &str) -> bool { | |
| 10 | + let Ok(redirect_url) = gix_url::parse(redirect_url.into()) else { | |
| 11 | 11 | return false; |
| 12 | 12 | }; |
| 13 | − let Ok(rhs) = gix_url::parse(rhs.into()) else { | |
| 13 | + let Ok(original_url) = gix_url::parse(original_url.into()) else { | |
| 14 | 14 | return false; |
| 15 | 15 | }; |
| 16 | − lhs.scheme == rhs.scheme && lhs.host == rhs.host && lhs.port_or_default() == rhs.port_or_default() | |
| 16 | + | |
| 17 | + if redirect_url.host != original_url.host { | |
| 18 | + return false; | |
| 19 | + } | |
| 20 | + | |
| 21 | + if redirect_url.scheme == original_url.scheme { | |
| 22 | + return redirect_url.port_or_default() == original_url.port_or_default(); | |
| 23 | + } | |
| 24 | + | |
| 25 | + if original_url.scheme == gix_url::Scheme::Http && redirect_url.scheme == gix_url::Scheme::Https { | |
| 26 | + let original_port = original_url.port_or_default(); | |
| 27 | + let redirect_port = redirect_url.port_or_default(); | |
| 28 | + return original_port == redirect_port || matches!((original_port, redirect_port), (Some(80), Some(443))); | |
| 29 | + } | |
| 30 | + | |
| 31 | + false | |
| 17 | 32 | } |
| 18 | 33 | |
| 19 | 34 | pub(crate) fn base_url(redirect_url: &str, base_url: &str, url: String) -> Result<String, Error> { |
| 20 | 35 | let tail = url |
| 21 | 36 | .strip_prefix(base_url) |
| 22 | 37 | .expect("BUG: caller assures `base_url` is subset of `url`"); |
| 23 | − if !shares_authority(redirect_url, base_url) { | |
| 38 | + if !shares_authority_or_upgrades_scheme(redirect_url, base_url) { | |
| 24 | 39 | return Err(Error { |
| 25 | 40 | redirect_url: redirect_url.into(), |
| 26 | 41 | expected_url: url, |
| @@ -62,46 +77,89 @@ mod tests { · 35 unmodified lines | ||
| 62 | 77 | ); |
| 63 | 78 | } |
| 64 | 79 | |
| 80 | + #[test] | |
| 81 | + fn base_url_allows_same_host_scheme_upgrade() { | |
| 82 | + assert_eq!( | |
| 83 | + base_url( | |
| 84 | + "https://original/b/info/refs?hi", | |
| 85 | + "http://original/a", | |
| 86 | + "http://original/a/info/refs?hi".into() | |
| 87 | + ) | |
| 88 | + .unwrap(), | |
| 89 | + "https://original/b" | |
| 90 | + ); | |
| 91 | + } | |
| 92 | + | |
| 65 | 93 | #[test] |
| 66 | 94 | fn base_url_rejects_authority_changes() { |
| 67 | − assert!(base_url( | |
| 68 | − "http://original/b/info/refs?hi", | |
| 69 | − "https://original/a", | |
| 70 | − "https://original/a/info/refs?hi".into() | |
| 71 | − ) | |
| 72 | − .is_err()); | |
| 73 | − assert!(base_url( | |
| 74 | − "https://redirected.org/b/info/refs?hi", | |
| 75 | − "https://original/a", | |
| 76 | − "https://original/a/info/refs?hi".into() | |
| 77 | − ) | |
| 78 | − .is_err()); | |
| 79 | − assert!(base_url( | |
| 80 | − "https://original:444/b/info/refs?hi", | |
| 81 | − "https://original/a", | |
| 82 | − "https://original/a/info/refs?hi".into() | |
| 83 | − ) | |
| 84 | − .is_err()); | |
| 95 | + assert!( | |
| 96 | + base_url( | |
| 97 | + "http://original/b/info/refs?hi", | |
| 98 | + "https://original/a", | |
| 99 | + "https://original/a/info/refs?hi".into() | |
| 100 | + ) | |
| 101 | + .is_err(), | |
| 102 | + "downgrading from https to http must be rejected" | |
| 103 | + ); | |
| 104 | + assert!( | |
| 105 | + base_url( | |
| 106 | + "https://redirected.org/b/info/refs?hi", | |
| 107 | + "https://original/a", | |
| 108 | + "https://original/a/info/refs?hi".into() | |
| 109 | + ) | |
| 110 | + .is_err(), | |
| 111 | + "changing the host must be rejected" | |
| 112 | + ); | |
| 113 | + assert!( | |
| 114 | + base_url( | |
| 115 | + "https://original:444/b/info/refs?hi", | |
| 116 | + "https://original/a", | |
| 117 | + "https://original/a/info/refs?hi".into() | |
| 118 | + ) | |
| 119 | + .is_err(), | |
| 120 | + "changing the port on https must be rejected" | |
| 121 | + ); | |
| 122 | + assert!( | |
| 123 | + base_url( | |
| 124 | + "https://original:444/b/info/refs?hi", | |
| 125 | + "http://original/a", | |
| 126 | + "http://original/a/info/refs?hi".into() | |
| 127 | + ) | |
| 128 | + .is_err(), | |
| 129 | + "upgrading the scheme does not allow changing the port" | |
| 130 | + ); | |
| 85 | 131 | } |
| 86 | 132 | |
| 87 | 133 | #[test] |
| 88 | − fn shares_authority_complete() { | |
| 89 | − assert!(shares_authority( | |
| 90 | − "https://original/b/info/refs?hi", | |
| 91 | − "https://original/a" | |
| 92 | − )); | |
| 93 | − assert!(!shares_authority( | |
| 94 | − "http://original/b/info/refs?hi", | |
| 95 | − "https://original/a" | |
| 96 | − )); | |
| 97 | − assert!(!shares_authority( | |
| 98 | − "https://redirected.org/b/info/refs?hi", | |
| 99 | − "https://original/a" | |
| 100 | − )); | |
| 101 | − assert!(!shares_authority( | |
| 102 | − "https://original:444/b/info/refs?hi", | |
| 103 | − "https://original/a" | |
| 104 | − )); | |
| 134 | + fn shares_authority_or_upgrades_scheme_complete() { | |
| 135 | + assert!( | |
| 136 | + shares_authority_or_upgrades_scheme("https://original/b/info/refs?hi", "https://original/a"), | |
| 137 | + "keeping the same https authority must be allowed" | |
| 138 | + ); | |
| 139 | + assert!( | |
| 140 | + shares_authority_or_upgrades_scheme("https://original/b/info/refs?hi", "http://original/a"), | |
| 141 | + "upgrading from http to https on the same host must be allowed" | |
| 142 | + ); | |
| 143 | + assert!( | |
| 144 | + shares_authority_or_upgrades_scheme("https://original:8080/b/info/refs?hi", "http://original:8080/a"), | |
| 145 | + "upgrading from http to https on the same host and port must be allowed" | |
| 146 | + ); | |
| 147 | + assert!( | |
| 148 | + !shares_authority_or_upgrades_scheme("http://original/b/info/refs?hi", "https://original/a"), | |
| 149 | + "downgrading from https to http must be rejected" | |
| 150 | + ); | |
| 151 | + assert!( | |
| 152 | + !shares_authority_or_upgrades_scheme("https://redirected.org/b/info/refs?hi", "https://original/a"), | |
| 153 | + "changing the host must be rejected" | |
| 154 | + ); | |
| 155 | + assert!( | |
| 156 | + !shares_authority_or_upgrades_scheme("https://original:444/b/info/refs?hi", "https://original/a"), | |
| 157 | + "changing the port on https must be rejected" | |
| 158 | + ); | |
| 159 | + assert!( | |
| 160 | + !shares_authority_or_upgrades_scheme("https://original:444/b/info/refs?hi", "http://original/a"), | |
| 161 | + "upgrading the scheme does not allow changing the port" | |
| 162 | + ); | |
| 105 | 163 | } |
| 106 | 164 | |
| 107 | 165 | #[test] |
| @@ -41,12 +41,6 @@ impl Default for Remote { · 40 unmodified lines | ||
| 41 | 41 | let (req_send, req_recv) = std::sync::mpsc::sync_channel(0); |
| 42 | 42 | let (res_send, res_recv) = std::sync::mpsc::sync_channel(0); |
| 43 | 43 | let handle = std::thread::spawn(move || -> Result<(), Error> { |
| 44 | − fn shares_authority(lhs: &reqwest::Url, rhs: &reqwest::Url) -> bool { | |
| 45 | − lhs.scheme() == rhs.scheme() | |
| 46 | − && lhs.host_str() == rhs.host_str() | |
| 47 | − && lhs.port_or_known_default() == rhs.port_or_known_default() | |
| 48 | − } | |
| 49 | − | |
| 50 | 44 | let mut follow = None; |
| 51 | 45 | let mut redirected_base_url = None::<String>; |
| 52 | 46 | let allow_redirects = Arc::new(atomic::AtomicBool::new(false)); |
| @@ -64,7 +58,12 @@ impl Default for Remote { · 11 unmodified lines | ||
| 64 | 58 | let prev_urls = attempt.previous(); |
| 65 | 59 | |
| 66 | 60 | match prev_urls.first() { |
| 67 | − Some(prev_url) if !shares_authority(prev_url, curr_url) => { | |
| 61 | + Some(prev_url) | |
| 62 | + if !redirect::shares_authority_or_upgrades_scheme( | |
| 63 | + curr_url.as_str(), | |
| 64 | + prev_url.as_str(), | |
| 65 | + ) => | |
| 66 | + { | |
| 68 | 67 | // git does not want to be redirected to a different host. |
| 69 | 68 | attempt.stop() |
| 70 | 69 | } |