Unified · Split
| @@ -62,12 +62,25 @@ mod access { · 61 unmodified lines | |||
| 62 | index += 1; | 62 | index += 1; |
| 63 | } | 63 | } |
| 64 | } else { | 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 | for _ in 0..rlw_literal_words(word) { | 73 | for _ in 0..rlw_literal_words(word) { |
| 69 | let word = iter.next()?; | 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 | if word & (1 << bit_index) != 0 { | 84 | if word & (1 << bit_index) != 0 { |
| 72 | f(index)?; | 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 | fn artifact_paths(target: &str) -> Vec<std::path::PathBuf> { | 46 | fn artifact_paths(target: &str) -> Vec<std::path::PathBuf> { |
| 26 | std::fs::read_dir( | 47 | std::fs::read_dir( |
| 27 | std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) | 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 | .checked_add(1) | 16 | .checked_add(1) |
| 17 | .and_then(|value| value.checked_shl(7)) | 17 | .and_then(|value| value.checked_shl(7)) |
| 18 | .and_then(|value| value.checked_add(u64::from(c) & 0x7f)) | 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 | Ok((value, i)) | 21 | Ok((value, i)) |
| 22 | } | 22 | } |
| @@ -29,7 +29,7 @@ pub enum Error { · 28 unmodified lines | |||
| 29 | 29 | ||
| 30 | /// Object lookup | 30 | /// Object lookup |
| 31 | impl Store { | 31 | impl Store { |
| 32 | const MAP_ACTION: &'static str = "open"; | 32 | const OPEN_OR_MAP_ACTION: &'static str = "open or map"; |
| 33 | 33 | ||
| 34 | /// Returns true if the given id is contained in our repository. | 34 | /// Returns true if the given id is contained in our repository. |
| 35 | pub fn contains(&self, id: &gix_hash::oid) -> bool { | 35 | pub fn contains(&self, id: &gix_hash::oid) -> bool { |
| @@ -200,7 +200,7 @@ impl Store { · 164 unmodified lines | |||
| 200 | zlib::stream::inflate::read(&mut input, &mut inflate.state, &mut out[decompressed_body_prefix_len..]) | 200 | zlib::stream::inflate::read(&mut input, &mut inflate.state, &mut out[decompressed_body_prefix_len..]) |
| 201 | .map_err(|e| Error::Io { | 201 | .map_err(|e| Error::Io { |
| 202 | source: e, | 202 | source: e, |
| 203 | action: "deflate", | 203 | action: "inflate", |
| 204 | path: path.to_owned(), | 204 | path: path.to_owned(), |
| 205 | })?; | 205 | })?; |
| 206 | 206 | ||
| @@ -232,7 +232,7 @@ impl Store { · 25 unmodified lines | |||
| 232 | Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), | 232 | Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), |
| 233 | Err(err) => { | 233 | Err(err) => { |
| 234 | return Err(Error::Io { | 234 | return Err(Error::Io { |
| 235 | action: Self::MAP_ACTION, | 235 | action: Self::OPEN_OR_MAP_ACTION, |
| 236 | source: err, | 236 | source: err, |
| 237 | path: path.to_owned(), | 237 | path: path.to_owned(), |
| 238 | }) | 238 | }) |
| @@ -242,7 +242,7 @@ impl Store { · 3 unmodified lines | |||
| 242 | if map.is_empty() { | 242 | if map.is_empty() { |
| 243 | return Err(Error::Io { | 243 | return Err(Error::Io { |
| 244 | source: io::Error::other("empty loose object file"), | 244 | source: io::Error::other("empty loose object file"), |
| 245 | action: Self::MAP_ACTION, | 245 | action: Self::OPEN_OR_MAP_ACTION, |
| 246 | path: path.to_owned(), | 246 | path: path.to_owned(), |
| 247 | }); | 247 | }); |
| 248 | } | 248 | } |
| @@ -3,7 +3,7 @@ · 2 unmodified lines | |||
| 3 | use anyhow::Result; | 3 | use anyhow::Result; |
| 4 | use gix_features::{progress, zlib}; | 4 | use gix_features::{progress, zlib}; |
| 5 | use gix_pack::{cache, data}; | 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 | use libfuzzer_sys::fuzz_target; | 7 | use libfuzzer_sys::fuzz_target; |
| 8 | use std::hint::black_box; | 8 | use std::hint::black_box; |
| 9 | 9 | ||
| @@ -4,7 +4,7 @@ use anyhow::Result; · 3 unmodified lines | |||
| 4 | use gix_features::progress; | 4 | use gix_features::progress; |
| 5 | use gix_hash::Prefix; | 5 | use gix_hash::Prefix; |
| 6 | use gix_pack::multi_index; | 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 | use libfuzzer_sys::fuzz_target; | 8 | use libfuzzer_sys::fuzz_target; |
| 9 | use std::hint::black_box; | 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() { | 113 | debug_assert_eq!( |
| 114 | return Err(apply::Error::Corrupt { | 114 | i, |
| 115 | message: "delta instructions were not consumed completely", | 115 | data.len(), |
| 116 | }); | 116 | "delta instructions were not consumed completely, should be impossible" |
| 117 | } | 117 | ); |
| 118 | if !target.is_empty() { | 118 | if !target.is_empty() { |
| 119 | return Err(apply::Error::Corrupt { | 119 | return Err(apply::Error::Corrupt { |
| 120 | message: "delta instructions produced fewer bytes than promised", | 120 | message: "delta instructions produced fewer bytes than promised", |
| @@ -11,7 +11,7 @@ use crate::data; · 10 unmodified lines | |||
| 11 | pub enum Error { | 11 | pub enum Error { |
| 12 | #[error("Object type {type_id} is unsupported")] | 12 | #[error("Object type {type_id} is unsupported")] |
| 13 | UnsupportedType { type_id: u8 }, | 13 | UnsupportedType { type_id: u8 }, |
| 14 | #[error("Pack entry is truncated while reading {message}")] | 14 | #[error("Pack entry is truncated: {message}")] |
| 15 | Corrupt { message: &'static str }, | 15 | Corrupt { message: &'static str }, |
| 16 | #[error("Pack entry header value overflowed while decoding")] | 16 | #[error("Pack entry header value overflowed while decoding")] |
| 17 | Overflow, | 17 | Overflow, |
| @@ -142,7 +142,7 @@ fn parse_header_info(data: &[u8]) -> Result<(u8, u64, usize), Error> { · 124 unmodified lines | |||
| 142 | let mut shift = 4u32; | 142 | let mut shift = 4u32; |
| 143 | while c & 0b1000_0000 != 0 { | 143 | while c & 0b1000_0000 != 0 { |
| 144 | c = *data.get(i).ok_or(Error::Corrupt { | 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 | i += 1; | 147 | i += 1; |
| 148 | let component = u64::from(c & 0b0111_1111).checked_shl(shift).ok_or(Error::Overflow)?; | 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 | .chunks_exact(N32_SIZE) | 157 | .chunks_exact(N32_SIZE) |
| 158 | .filter_map(|offset| { | 158 | .filter_map(|offset| { |
| 159 | let offset = crate::read_u32(offset); | 159 | let offset = crate::read_u32(offset); |
| 160 | (offset & (1 << 31) != 0).then_some((offset ^ (1 << 31)) as usize) | 160 | (offset & (1 << 31) != 0).then_some((offset ^ (1 << 31)) as usize) |
| 161 | }) | 161 | }) |
| 162 | .collect(); | 162 | .fold((0usize, 0usize), |(count, max_index), index| { |
| 163 | let large_offsets = large_offset_indices.len(); | 163 | (count + 1, max_index.max(index)) |
| 164 | }); | ||
| 164 | v2_header_size | 165 | v2_header_size |
| 165 | .checked_add(oid_bytes) | 166 | .checked_add(oid_bytes) |
| 166 | .and_then(|size| size.checked_add(table_bytes)) | 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 | message: "Pack index size overflowed while validating version 2 layout".into(), | 172 | message: "Pack index size overflowed while validating version 2 layout".into(), |
| 172 | }) | 173 | }) |
| 173 | .and_then(|expected_size| { | 174 | .and_then(|expected_size| { |
| 174 | let max_large_offset_index = large_offset_indices.into_iter().max().unwrap_or(0); | ||
| 175 | if large_offsets > 0 && max_large_offset_index >= large_offsets { | 175 | if large_offsets > 0 && max_large_offset_index >= large_offsets { |
| 176 | return Err(Error::Corrupt { | 176 | return Err(Error::Corrupt { |
| 177 | message: format!( | 177 | message: format!( |
| @@ -22,7 +22,7 @@ pub struct Outcome { · 21 unmodified lines | |||
| 22 | pub num_objects: u32, | 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 | /// Use this information to selectively extract the progress of interest in case the parent application has custom visualization. | 27 | /// Use this information to selectively extract the progress of interest in case the parent application has custom visualization. |
| 28 | #[derive(Debug, Copy, Clone)] | 28 | #[derive(Debug, Copy, Clone)] |
| @@ -203,7 +203,7 @@ pub mod lookup { · 202 unmodified lines | |||
| 203 | 203 | ||
| 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. | 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 | pub fn is_valid(offset: &Range<usize>, hash: gix_hash::Kind, num_objects: u32) -> bool { | 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 | Ok(()) | 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 | pub fn is_valid(offset: &Range<usize>, num_objects: u32) -> bool { | 255 | pub fn is_valid(offset: &Range<usize>, num_objects: u32) -> bool { |
| 256 | let entry_size = 4 /* pack-id */ + 4 /* pack-offset */; | 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 | Fan { index: usize }, | 34 | Fan { index: usize }, |
| 35 | #[error("The multi-index claims to have no objects")] | 35 | #[error("The multi-index claims to have no objects")] |
| 36 | Empty, | 36 | Empty, |
| 37 | #[error("The multi-index path '{path}' has no parent directory")] | ||
| 38 | InvalidPath { path: std::path::PathBuf }, | ||
| 37 | #[error("Interrupted")] | 39 | #[error("Interrupted")] |
| 38 | Interrupted, | 40 | Interrupted, |
| 39 | } | 41 | } |
| @@ -142,7 +144,11 @@ where · 102 unmodified lines | |||
| 142 | C: crate::cache::DecodeEntry, | 144 | C: crate::cache::DecodeEntry, |
| 143 | F: Fn() -> C + Send + Clone, | 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 | let actual_index_checksum = self | 153 | let actual_index_checksum = self |
| 148 | .verify_checksum( | 154 | .verify_checksum( |
| @@ -6,7 +6,7 @@ fn multi_index() -> (File, PathBuf) { · 5 unmodified lines | |||
| 6 | let path = crate::scripted_fixture_read_only("make_pack_gen_repo_multi_index.sh") | 6 | let path = crate::scripted_fixture_read_only("make_pack_gen_repo_multi_index.sh") |
| 7 | .expect("test fixture exists") | 7 | .expect("test fixture exists") |
| 8 | .join(".git/objects/pack/multi-pack-index"); | 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 | (file, path) | 10 | (file, path) |
| 11 | } | 11 | } |
| 12 | 12 | ||
| @@ -6,21 +6,36 @@ pub struct Error { · 5 unmodified lines | |||
| 6 | expected_url: String, | 6 | expected_url: String, |
| 7 | } | 7 | } |
| 8 | 8 | ||
| 9 | fn shares_authority(lhs: &str, rhs: &str) -> bool { | 9 | pub(crate) fn shares_authority_or_upgrades_scheme(redirect_url: &str, original_url: &str) -> bool { |
| 10 | let Ok(lhs) = gix_url::parse(lhs.into()) else { | 10 | let Ok(redirect_url) = gix_url::parse(redirect_url.into()) else { |
| 11 | return false; | 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 | return false; | 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 | pub(crate) fn base_url(redirect_url: &str, base_url: &str, url: String) -> Result<String, Error> { | 34 | pub(crate) fn base_url(redirect_url: &str, base_url: &str, url: String) -> Result<String, Error> { |
| 20 | let tail = url | 35 | let tail = url |
| 21 | .strip_prefix(base_url) | 36 | .strip_prefix(base_url) |
| 22 | .expect("BUG: caller assures `base_url` is subset of `url`"); | 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 | return Err(Error { | 39 | return Err(Error { |
| 25 | redirect_url: redirect_url.into(), | 40 | redirect_url: redirect_url.into(), |
| 26 | expected_url: url, | 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 | #[test] | 93 | #[test] |
| 66 | fn base_url_rejects_authority_changes() { | 94 | fn base_url_rejects_authority_changes() { |
| 67 | assert!(base_url( | 95 | assert!( |
| 68 | "http://original/b/info/refs?hi", | 96 | base_url( |
| 69 | "https://original/a", | 97 | "http://original/b/info/refs?hi", |
| 70 | "https://original/a/info/refs?hi".into() | 98 | "https://original/a", |
| 71 | ) | 99 | "https://original/a/info/refs?hi".into() |
| 72 | .is_err()); | 100 | ) |
| 73 | assert!(base_url( | 101 | .is_err(), |
| 74 | "https://redirected.org/b/info/refs?hi", | 102 | "downgrading from https to http must be rejected" |
| 75 | "https://original/a", | 103 | ); |
| 76 | "https://original/a/info/refs?hi".into() | 104 | assert!( |
| 77 | ) | 105 | base_url( |
| 78 | .is_err()); | 106 | "https://redirected.org/b/info/refs?hi", |
| 79 | assert!(base_url( | 107 | "https://original/a", |
| 80 | "https://original:444/b/info/refs?hi", | 108 | "https://original/a/info/refs?hi".into() |
| 81 | "https://original/a", | 109 | ) |
| 82 | "https://original/a/info/refs?hi".into() | 110 | .is_err(), |
| 83 | ) | 111 | "changing the host must be rejected" |
| 84 | .is_err()); | 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 | #[test] | 133 | #[test] |
| 88 | fn shares_authority_complete() { | 134 | fn shares_authority_or_upgrades_scheme_complete() { |
| 89 | assert!(shares_authority( | 135 | assert!( |
| 90 | "https://original/b/info/refs?hi", | 136 | shares_authority_or_upgrades_scheme("https://original/b/info/refs?hi", "https://original/a"), |
| 91 | "https://original/a" | 137 | "keeping the same https authority must be allowed" |
| 92 | )); | 138 | ); |
| 93 | assert!(!shares_authority( | 139 | assert!( |
| 94 | "http://original/b/info/refs?hi", | 140 | shares_authority_or_upgrades_scheme("https://original/b/info/refs?hi", "http://original/a"), |
| 95 | "https://original/a" | 141 | "upgrading from http to https on the same host must be allowed" |
| 96 | )); | 142 | ); |
| 97 | assert!(!shares_authority( | 143 | assert!( |
| 98 | "https://redirected.org/b/info/refs?hi", | 144 | shares_authority_or_upgrades_scheme("https://original:8080/b/info/refs?hi", "http://original:8080/a"), |
| 99 | "https://original/a" | 145 | "upgrading from http to https on the same host and port must be allowed" |
| 100 | )); | 146 | ); |
| 101 | assert!(!shares_authority( | 147 | assert!( |
| 102 | "https://original:444/b/info/refs?hi", | 148 | !shares_authority_or_upgrades_scheme("http://original/b/info/refs?hi", "https://original/a"), |
| 103 | "https://original/a" | 149 | "downgrading from https to http must be rejected" |
| 104 | )); | 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 | #[test] | 165 | #[test] |
| @@ -41,12 +41,6 @@ impl Default for Remote { · 40 unmodified lines | |||
| 41 | let (req_send, req_recv) = std::sync::mpsc::sync_channel(0); | 41 | let (req_send, req_recv) = std::sync::mpsc::sync_channel(0); |
| 42 | let (res_send, res_recv) = std::sync::mpsc::sync_channel(0); | 42 | let (res_send, res_recv) = std::sync::mpsc::sync_channel(0); |
| 43 | let handle = std::thread::spawn(move || -> Result<(), Error> { | 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 | let mut follow = None; | 44 | let mut follow = None; |
| 51 | let mut redirected_base_url = None::<String>; | 45 | let mut redirected_base_url = None::<String>; |
| 52 | let allow_redirects = Arc::new(atomic::AtomicBool::new(false)); | 46 | let allow_redirects = Arc::new(atomic::AtomicBool::new(false)); |
| @@ -64,7 +58,12 @@ impl Default for Remote { · 11 unmodified lines | |||
| 64 | let prev_urls = attempt.previous(); | 58 | let prev_urls = attempt.previous(); |
| 65 | 59 | ||
| 66 | match prev_urls.first() { | 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 | // git does not want to be redirected to a different host. | 67 | // git does not want to be redirected to a different host. |
| 69 | attempt.stop() | 68 | attempt.stop() |
| 70 | } | 69 | } |