diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
commit | 10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 (patch) | |
tree | bdffd5d80c26cf4a7a518281a204be1ace85b4c1 /vendor/gix-protocol/src/handshake/refs | |
parent | Releasing progress-linux version 1.70.0+dfsg1-9~progress7.99u1. (diff) | |
download | rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.tar.xz rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.zip |
Merging upstream version 1.70.0+dfsg2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/gix-protocol/src/handshake/refs')
-rw-r--r-- | vendor/gix-protocol/src/handshake/refs/async_io.rs | 43 | ||||
-rw-r--r-- | vendor/gix-protocol/src/handshake/refs/blocking_io.rs | 31 | ||||
-rw-r--r-- | vendor/gix-protocol/src/handshake/refs/mod.rs | 72 | ||||
-rw-r--r-- | vendor/gix-protocol/src/handshake/refs/shared.rs | 237 | ||||
-rw-r--r-- | vendor/gix-protocol/src/handshake/refs/tests.rs | 223 |
5 files changed, 606 insertions, 0 deletions
diff --git a/vendor/gix-protocol/src/handshake/refs/async_io.rs b/vendor/gix-protocol/src/handshake/refs/async_io.rs new file mode 100644 index 000000000..19ea543c7 --- /dev/null +++ b/vendor/gix-protocol/src/handshake/refs/async_io.rs @@ -0,0 +1,43 @@ +use crate::handshake::{refs, refs::parse::Error, Ref}; + +/// Parse refs from the given input line by line. Protocol V2 is required for this to succeed. +pub async fn from_v2_refs(in_refs: &mut dyn gix_transport::client::ReadlineBufRead) -> Result<Vec<Ref>, Error> { + let mut out_refs = Vec::new(); + while let Some(line) = in_refs + .readline() + .await + .transpose()? + .transpose()? + .and_then(|l| l.as_bstr()) + { + out_refs.push(refs::shared::parse_v2(line)?); + } + Ok(out_refs) +} + +/// Parse refs from the return stream of the handshake as well as the server capabilities, also received as part of the +/// handshake. +/// Together they form a complete set of refs. +/// +/// # Note +/// +/// Symbolic refs are shoe-horned into server capabilities whereas refs (without symbolic ones) are sent automatically as +/// part of the handshake. Both symbolic and peeled refs need to be combined to fit into the [`Ref`] type provided here. +pub async fn from_v1_refs_received_as_part_of_handshake_and_capabilities<'a>( + in_refs: &mut dyn gix_transport::client::ReadlineBufRead, + capabilities: impl Iterator<Item = gix_transport::client::capabilities::Capability<'a>>, +) -> Result<Vec<Ref>, refs::parse::Error> { + let mut out_refs = refs::shared::from_capabilities(capabilities)?; + let number_of_possible_symbolic_refs_for_lookup = out_refs.len(); + + while let Some(line) = in_refs + .readline() + .await + .transpose()? + .transpose()? + .and_then(|l| l.as_bstr()) + { + refs::shared::parse_v1(number_of_possible_symbolic_refs_for_lookup, &mut out_refs, line)?; + } + Ok(out_refs.into_iter().map(Into::into).collect()) +} diff --git a/vendor/gix-protocol/src/handshake/refs/blocking_io.rs b/vendor/gix-protocol/src/handshake/refs/blocking_io.rs new file mode 100644 index 000000000..7ad695b77 --- /dev/null +++ b/vendor/gix-protocol/src/handshake/refs/blocking_io.rs @@ -0,0 +1,31 @@ +use crate::handshake::{refs, refs::parse::Error, Ref}; + +/// Parse refs from the given input line by line. Protocol V2 is required for this to succeed. +pub fn from_v2_refs(in_refs: &mut dyn gix_transport::client::ReadlineBufRead) -> Result<Vec<Ref>, Error> { + let mut out_refs = Vec::new(); + while let Some(line) = in_refs.readline().transpose()?.transpose()?.and_then(|l| l.as_bstr()) { + out_refs.push(refs::shared::parse_v2(line)?); + } + Ok(out_refs) +} + +/// Parse refs from the return stream of the handshake as well as the server capabilities, also received as part of the +/// handshake. +/// Together they form a complete set of refs. +/// +/// # Note +/// +/// Symbolic refs are shoe-horned into server capabilities whereas refs (without symbolic ones) are sent automatically as +/// part of the handshake. Both symbolic and peeled refs need to be combined to fit into the [`Ref`] type provided here. +pub fn from_v1_refs_received_as_part_of_handshake_and_capabilities<'a>( + in_refs: &mut dyn gix_transport::client::ReadlineBufRead, + capabilities: impl Iterator<Item = gix_transport::client::capabilities::Capability<'a>>, +) -> Result<Vec<Ref>, Error> { + let mut out_refs = refs::shared::from_capabilities(capabilities)?; + let number_of_possible_symbolic_refs_for_lookup = out_refs.len(); + + while let Some(line) = in_refs.readline().transpose()?.transpose()?.and_then(|l| l.as_bstr()) { + refs::shared::parse_v1(number_of_possible_symbolic_refs_for_lookup, &mut out_refs, line)?; + } + Ok(out_refs.into_iter().map(Into::into).collect()) +} diff --git a/vendor/gix-protocol/src/handshake/refs/mod.rs b/vendor/gix-protocol/src/handshake/refs/mod.rs new file mode 100644 index 000000000..889842e4c --- /dev/null +++ b/vendor/gix-protocol/src/handshake/refs/mod.rs @@ -0,0 +1,72 @@ +use bstr::BStr; + +use super::Ref; + +/// +pub mod parse { + use bstr::BString; + + /// The error returned when parsing References/refs from the server response. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + DecodePacketline(#[from] gix_transport::packetline::decode::Error), + #[error(transparent)] + Id(#[from] gix_hash::decode::Error), + #[error("{symref:?} could not be parsed. A symref is expected to look like <NAME>:<target>.")] + MalformedSymref { symref: BString }, + #[error("{0:?} could not be parsed. A V1 ref line should be '<hex-hash> <path>'.")] + MalformedV1RefLine(BString), + #[error( + "{0:?} could not be parsed. A V2 ref line should be '<hex-hash> <path>[ (peeled|symref-target):<value>'." + )] + MalformedV2RefLine(BString), + #[error("The ref attribute {attribute:?} is unknown. Found in line {line:?}")] + UnknownAttribute { attribute: BString, line: BString }, + #[error("{message}")] + InvariantViolation { message: &'static str }, + } +} + +impl Ref { + /// Provide shared fields referring to the ref itself, namely `(name, target, [peeled])`. + /// In case of peeled refs, the tag object itself is returned as it is what the ref directly refers to, and target of the tag is returned + /// as `peeled`. + /// If `unborn`, the first object id will be the null oid. + pub fn unpack(&self) -> (&BStr, Option<&gix_hash::oid>, Option<&gix_hash::oid>) { + match self { + Ref::Direct { full_ref_name, object } + | Ref::Symbolic { + full_ref_name, object, .. + } => (full_ref_name.as_ref(), Some(object), None), + Ref::Peeled { + full_ref_name, + tag: object, + object: peeled, + } => (full_ref_name.as_ref(), Some(object), Some(peeled)), + Ref::Unborn { + full_ref_name, + target: _, + } => (full_ref_name.as_ref(), None, None), + } + } +} + +#[cfg(any(feature = "blocking-client", feature = "async-client"))] +pub(crate) mod shared; + +#[cfg(feature = "async-client")] +mod async_io; +#[cfg(feature = "async-client")] +pub use async_io::{from_v1_refs_received_as_part_of_handshake_and_capabilities, from_v2_refs}; + +#[cfg(feature = "blocking-client")] +mod blocking_io; +#[cfg(feature = "blocking-client")] +pub use blocking_io::{from_v1_refs_received_as_part_of_handshake_and_capabilities, from_v2_refs}; + +#[cfg(test)] +mod tests; diff --git a/vendor/gix-protocol/src/handshake/refs/shared.rs b/vendor/gix-protocol/src/handshake/refs/shared.rs new file mode 100644 index 000000000..1d0dfc256 --- /dev/null +++ b/vendor/gix-protocol/src/handshake/refs/shared.rs @@ -0,0 +1,237 @@ +use bstr::{BStr, BString, ByteSlice}; + +use crate::handshake::{refs::parse::Error, Ref}; + +impl From<InternalRef> for Ref { + fn from(v: InternalRef) -> Self { + match v { + InternalRef::Symbolic { + path, + target: Some(target), + object, + } => Ref::Symbolic { + full_ref_name: path, + target, + object, + }, + InternalRef::Symbolic { + path, + target: None, + object, + } => Ref::Direct { + full_ref_name: path, + object, + }, + InternalRef::Peeled { path, tag, object } => Ref::Peeled { + full_ref_name: path, + tag, + object, + }, + InternalRef::Direct { path, object } => Ref::Direct { + full_ref_name: path, + object, + }, + InternalRef::SymbolicForLookup { .. } => { + unreachable!("this case should have been removed during processing") + } + } + } +} + +#[cfg_attr(test, derive(PartialEq, Eq, Debug, Clone))] +pub(crate) enum InternalRef { + /// A ref pointing to a `tag` object, which in turns points to an `object`, usually a commit + Peeled { + path: BString, + tag: gix_hash::ObjectId, + object: gix_hash::ObjectId, + }, + /// A ref pointing to a commit object + Direct { path: BString, object: gix_hash::ObjectId }, + /// A symbolic ref pointing to `target` ref, which in turn points to an `object` + Symbolic { + path: BString, + /// It is `None` if the target is unreachable as it points to another namespace than the one is currently set + /// on the server (i.e. based on the repository at hand or the user performing the operation). + /// + /// The latter is more of an edge case, please [this issue][#205] for details. + target: Option<BString>, + object: gix_hash::ObjectId, + }, + /// extracted from V1 capabilities, which contain some important symbolic refs along with their targets + /// These don't contain the Id + SymbolicForLookup { path: BString, target: Option<BString> }, +} + +impl InternalRef { + fn unpack_direct(self) -> Option<(BString, gix_hash::ObjectId)> { + match self { + InternalRef::Direct { path, object } => Some((path, object)), + _ => None, + } + } + fn lookup_symbol_has_path(&self, predicate_path: &BStr) -> bool { + matches!(self, InternalRef::SymbolicForLookup { path, .. } if path == predicate_path) + } +} + +pub(crate) fn from_capabilities<'a>( + capabilities: impl Iterator<Item = gix_transport::client::capabilities::Capability<'a>>, +) -> Result<Vec<InternalRef>, Error> { + let mut out_refs = Vec::new(); + let symref_values = capabilities.filter_map(|c| { + if c.name() == b"symref".as_bstr() { + c.value().map(ToOwned::to_owned) + } else { + None + } + }); + for symref in symref_values { + let (left, right) = symref.split_at(symref.find_byte(b':').ok_or_else(|| Error::MalformedSymref { + symref: symref.to_owned(), + })?); + if left.is_empty() || right.is_empty() { + return Err(Error::MalformedSymref { + symref: symref.to_owned(), + }); + } + out_refs.push(InternalRef::SymbolicForLookup { + path: left.into(), + target: match &right[1..] { + b"(null)" => None, + name => Some(name.into()), + }, + }) + } + Ok(out_refs) +} + +pub(in crate::handshake::refs) fn parse_v1( + num_initial_out_refs: usize, + out_refs: &mut Vec<InternalRef>, + line: &BStr, +) -> Result<(), Error> { + let trimmed = line.trim_end(); + let (hex_hash, path) = trimmed.split_at( + trimmed + .find(b" ") + .ok_or_else(|| Error::MalformedV1RefLine(trimmed.to_owned().into()))?, + ); + let path = &path[1..]; + if path.is_empty() { + return Err(Error::MalformedV1RefLine(trimmed.to_owned().into())); + } + match path.strip_suffix(b"^{}") { + Some(stripped) => { + let (previous_path, tag) = + out_refs + .pop() + .and_then(InternalRef::unpack_direct) + .ok_or(Error::InvariantViolation { + message: "Expecting peeled refs to be preceded by direct refs", + })?; + if previous_path != stripped { + return Err(Error::InvariantViolation { + message: "Expecting peeled refs to have the same base path as the previous, unpeeled one", + }); + } + out_refs.push(InternalRef::Peeled { + path: previous_path, + tag, + object: gix_hash::ObjectId::from_hex(hex_hash.as_bytes())?, + }); + } + None => { + let object = gix_hash::ObjectId::from_hex(hex_hash.as_bytes())?; + match out_refs + .iter() + .take(num_initial_out_refs) + .position(|r| r.lookup_symbol_has_path(path.into())) + { + Some(position) => match out_refs.swap_remove(position) { + InternalRef::SymbolicForLookup { path: _, target } => out_refs.push(InternalRef::Symbolic { + path: path.into(), + object, + target, + }), + _ => unreachable!("Bug in lookup_symbol_has_path - must return lookup symbols"), + }, + None => out_refs.push(InternalRef::Direct { + object, + path: path.into(), + }), + }; + } + } + Ok(()) +} + +pub(in crate::handshake::refs) fn parse_v2(line: &BStr) -> Result<Ref, Error> { + let trimmed = line.trim_end(); + let mut tokens = trimmed.splitn(3, |b| *b == b' '); + match (tokens.next(), tokens.next()) { + (Some(hex_hash), Some(path)) => { + let id = if hex_hash == b"unborn" { + None + } else { + Some(gix_hash::ObjectId::from_hex(hex_hash.as_bytes())?) + }; + if path.is_empty() { + return Err(Error::MalformedV2RefLine(trimmed.to_owned().into())); + } + Ok(if let Some(attribute) = tokens.next() { + let mut tokens = attribute.splitn(2, |b| *b == b':'); + match (tokens.next(), tokens.next()) { + (Some(attribute), Some(value)) => { + if value.is_empty() { + return Err(Error::MalformedV2RefLine(trimmed.to_owned().into())); + } + match attribute { + b"peeled" => Ref::Peeled { + full_ref_name: path.into(), + object: gix_hash::ObjectId::from_hex(value.as_bytes())?, + tag: id.ok_or(Error::InvariantViolation { + message: "got 'unborn' as tag target", + })?, + }, + b"symref-target" => match value { + b"(null)" => Ref::Direct { + full_ref_name: path.into(), + object: id.ok_or(Error::InvariantViolation { + message: "got 'unborn' while (null) was a symref target", + })?, + }, + name => match id { + Some(id) => Ref::Symbolic { + full_ref_name: path.into(), + object: id, + target: name.into(), + }, + None => Ref::Unborn { + full_ref_name: path.into(), + target: name.into(), + }, + }, + }, + _ => { + return Err(Error::UnknownAttribute { + attribute: attribute.to_owned().into(), + line: trimmed.to_owned().into(), + }) + } + } + } + _ => return Err(Error::MalformedV2RefLine(trimmed.to_owned().into())), + } + } else { + Ref::Direct { + object: id.ok_or(Error::InvariantViolation { + message: "got 'unborn' as object name of direct reference", + })?, + full_ref_name: path.into(), + } + }) + } + _ => Err(Error::MalformedV2RefLine(trimmed.to_owned().into())), + } +} diff --git a/vendor/gix-protocol/src/handshake/refs/tests.rs b/vendor/gix-protocol/src/handshake/refs/tests.rs new file mode 100644 index 000000000..a7c9171a5 --- /dev/null +++ b/vendor/gix-protocol/src/handshake/refs/tests.rs @@ -0,0 +1,223 @@ +use gix_transport::{client, client::Capabilities}; + +/// Convert a hexadecimal hash into its corresponding `ObjectId` or _panic_. +fn oid(hex: &str) -> gix_hash::ObjectId { + gix_hash::ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex") +} + +use crate::handshake::{refs, refs::shared::InternalRef, Ref}; + +#[maybe_async::test(feature = "blocking-client", async(feature = "async-client", async_std::test))] +async fn extract_references_from_v2_refs() { + let input = &mut Fixture( + "808e50d724f604f69ab93c6da2919c014667bedb HEAD symref-target:refs/heads/main +808e50d724f604f69ab93c6da2919c014667bedb MISSING_NAMESPACE_TARGET symref-target:(null) +unborn HEAD symref-target:refs/heads/main +unborn refs/heads/symbolic symref-target:refs/heads/target +808e50d724f604f69ab93c6da2919c014667bedb refs/heads/main +7fe1b98b39423b71e14217aa299a03b7c937d656 refs/tags/foo peeled:808e50d724f604f69ab93c6da2919c014667bedb +7fe1b98b39423b71e14217aa299a03b7c937d6ff refs/tags/blaz +" + .as_bytes(), + ); + + let out = refs::from_v2_refs(input).await.expect("no failure on valid input"); + + assert_eq!( + out, + vec![ + Ref::Symbolic { + full_ref_name: "HEAD".into(), + target: "refs/heads/main".into(), + object: oid("808e50d724f604f69ab93c6da2919c014667bedb") + }, + Ref::Direct { + full_ref_name: "MISSING_NAMESPACE_TARGET".into(), + object: oid("808e50d724f604f69ab93c6da2919c014667bedb") + }, + Ref::Unborn { + full_ref_name: "HEAD".into(), + target: "refs/heads/main".into(), + }, + Ref::Unborn { + full_ref_name: "refs/heads/symbolic".into(), + target: "refs/heads/target".into(), + }, + Ref::Direct { + full_ref_name: "refs/heads/main".into(), + object: oid("808e50d724f604f69ab93c6da2919c014667bedb") + }, + Ref::Peeled { + full_ref_name: "refs/tags/foo".into(), + tag: oid("7fe1b98b39423b71e14217aa299a03b7c937d656"), + object: oid("808e50d724f604f69ab93c6da2919c014667bedb") + }, + Ref::Direct { + full_ref_name: "refs/tags/blaz".into(), + object: oid("7fe1b98b39423b71e14217aa299a03b7c937d6ff") + }, + ] + ) +} + +#[maybe_async::test(feature = "blocking-client", async(feature = "async-client", async_std::test))] +async fn extract_references_from_v1_refs() { + let input = &mut Fixture( + "73a6868963993a3328e7d8fe94e5a6ac5078a944 HEAD +21c9b7500cb144b3169a6537961ec2b9e865be81 MISSING_NAMESPACE_TARGET +73a6868963993a3328e7d8fe94e5a6ac5078a944 refs/heads/main +8e472f9ccc7d745927426cbb2d9d077de545aa4e refs/pull/13/head +dce0ea858eef7ff61ad345cc5cdac62203fb3c10 refs/tags/gix-commitgraph-v0.0.0 +21c9b7500cb144b3169a6537961ec2b9e865be81 refs/tags/gix-commitgraph-v0.0.0^{}" + .as_bytes(), + ); + let out = refs::from_v1_refs_received_as_part_of_handshake_and_capabilities( + input, + Capabilities::from_bytes(b"\0symref=HEAD:refs/heads/main symref=MISSING_NAMESPACE_TARGET:(null)") + .expect("valid capabilities") + .0 + .iter(), + ) + .await + .expect("no failure from valid input"); + assert_eq!( + out, + vec![ + Ref::Symbolic { + full_ref_name: "HEAD".into(), + target: "refs/heads/main".into(), + object: oid("73a6868963993a3328e7d8fe94e5a6ac5078a944") + }, + Ref::Direct { + full_ref_name: "MISSING_NAMESPACE_TARGET".into(), + object: oid("21c9b7500cb144b3169a6537961ec2b9e865be81") + }, + Ref::Direct { + full_ref_name: "refs/heads/main".into(), + object: oid("73a6868963993a3328e7d8fe94e5a6ac5078a944") + }, + Ref::Direct { + full_ref_name: "refs/pull/13/head".into(), + object: oid("8e472f9ccc7d745927426cbb2d9d077de545aa4e") + }, + Ref::Peeled { + full_ref_name: "refs/tags/gix-commitgraph-v0.0.0".into(), + tag: oid("dce0ea858eef7ff61ad345cc5cdac62203fb3c10"), + object: oid("21c9b7500cb144b3169a6537961ec2b9e865be81") + }, + ] + ) +} + +#[test] +fn extract_symbolic_references_from_capabilities() -> Result<(), client::Error> { + let caps = client::Capabilities::from_bytes( + b"\0unrelated symref=HEAD:refs/heads/main symref=ANOTHER:refs/heads/foo symref=MISSING_NAMESPACE_TARGET:(null) agent=git/2.28.0", + )? + .0; + let out = refs::shared::from_capabilities(caps.iter()).expect("a working example"); + + assert_eq!( + out, + vec![ + InternalRef::SymbolicForLookup { + path: "HEAD".into(), + target: Some("refs/heads/main".into()) + }, + InternalRef::SymbolicForLookup { + path: "ANOTHER".into(), + target: Some("refs/heads/foo".into()) + }, + InternalRef::SymbolicForLookup { + path: "MISSING_NAMESPACE_TARGET".into(), + target: None + } + ] + ); + Ok(()) +} + +#[cfg(any(feature = "async-client", feature = "blocking-client"))] +struct Fixture<'a>(&'a [u8]); + +#[cfg(feature = "blocking-client")] +impl<'a> std::io::Read for Fixture<'a> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { + self.0.read(buf) + } +} + +#[cfg(feature = "blocking-client")] +impl<'a> std::io::BufRead for Fixture<'a> { + fn fill_buf(&mut self) -> std::io::Result<&[u8]> { + self.0.fill_buf() + } + + fn consume(&mut self, amt: usize) { + self.0.consume(amt) + } +} + +#[cfg(feature = "blocking-client")] +impl<'a> gix_transport::client::ReadlineBufRead for Fixture<'a> { + fn readline( + &mut self, + ) -> Option<std::io::Result<Result<gix_packetline::PacketLineRef<'_>, gix_packetline::decode::Error>>> { + use bstr::{BStr, ByteSlice}; + let bytes: &BStr = self.0.into(); + let mut lines = bytes.lines(); + let res = lines.next()?; + self.0 = lines.as_bytes(); + Some(Ok(Ok(gix_packetline::PacketLineRef::Data(res)))) + } +} + +#[cfg(feature = "async-client")] +impl<'a> Fixture<'a> { + fn project_inner(self: std::pin::Pin<&mut Self>) -> std::pin::Pin<&mut &'a [u8]> { + #[allow(unsafe_code)] + unsafe { + std::pin::Pin::new(&mut self.get_unchecked_mut().0) + } + } +} + +#[cfg(feature = "async-client")] +impl<'a> futures_io::AsyncRead for Fixture<'a> { + fn poll_read( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut [u8], + ) -> std::task::Poll<std::io::Result<usize>> { + self.project_inner().poll_read(cx, buf) + } +} + +#[cfg(feature = "async-client")] +impl<'a> futures_io::AsyncBufRead for Fixture<'a> { + fn poll_fill_buf( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll<std::io::Result<&[u8]>> { + self.project_inner().poll_fill_buf(cx) + } + + fn consume(self: std::pin::Pin<&mut Self>, amt: usize) { + self.project_inner().consume(amt) + } +} + +#[cfg(feature = "async-client")] +#[async_trait::async_trait(?Send)] +impl<'a> gix_transport::client::ReadlineBufRead for Fixture<'a> { + async fn readline( + &mut self, + ) -> Option<std::io::Result<Result<gix_packetline::PacketLineRef<'_>, gix_packetline::decode::Error>>> { + use bstr::{BStr, ByteSlice}; + let bytes: &BStr = self.0.into(); + let mut lines = bytes.lines(); + let res = lines.next()?; + self.0 = lines.as_bytes(); + Some(Ok(Ok(gix_packetline::PacketLineRef::Data(res)))) + } +} |