use core::ops::Range;
use core::ptr::NonNull;
use std::io::Write as _;
use bstr::BStr;
use bun_alloc::AllocError;
use bun_core::StringBuilder;
use bun_core::{OwnedString, strings};
use bun_url::PercentEncoding;
use bun_url::whatwg::URL as JscUrl;
use enum_map::{Enum, EnumMap};
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq, strum::IntoStaticStr)]
pub enum HostedGitInfoError {
#[error("OutOfMemory")]
OutOfMemory,
#[error("InvalidURL")]
InvalidURL,
}
bun_core::oom_from_alloc!(HostedGitInfoError);
bun_core::named_error_set!(HostedGitInfoError);
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq, strum::IntoStaticStr)]
pub enum ParseUrlError {
#[error("InvalidGitUrl")]
InvalidGitUrl,
#[error("OutOfMemory")]
OutOfMemory,
}
bun_core::oom_from_alloc!(ParseUrlError);
bun_core::named_error_set!(ParseUrlError);
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::IntoStaticStr)]
#[strum(serialize_all = "lowercase")]
pub enum Representation {
Shortcut,
Sshurl,
Ssh,
Https,
Git,
Http,
}
pub struct HostedGitInfo {
committish: Option<Range<usize>>,
project: Range<usize>,
user: Option<Range<usize>>,
pub host_provider: HostProvider,
pub default_representation: Representation,
_memory_buffer: Box<[u8]>,
}
impl HostedGitInfo {
#[inline]
pub fn committish(&self) -> Option<&[u8]> {
self.committish.clone().map(|r| &self._memory_buffer[r])
}
#[inline]
pub fn project(&self) -> &[u8] {
&self._memory_buffer[self.project.clone()]
}
#[inline]
pub fn user(&self) -> Option<&[u8]> {
self.user.clone().map(|r| &self._memory_buffer[r])
}
fn decode_and_append(
sb: &mut StringBuilder,
input: &[u8],
) -> Result<Range<usize>, HostedGitInfoError> {
let start = sb.len;
let writable = sb.writable();
let decoded_len = PercentEncoding::decode_into(writable, input)
.map_err(|_| HostedGitInfoError::InvalidURL)? as usize;
sb.len += decoded_len;
Ok(start..start + decoded_len)
}
fn copy_from(
committish: Option<&[u8]>,
project: &[u8],
user: Option<&[u8]>,
host_provider: HostProvider,
default_representation: Representation,
) -> Result<Self, HostedGitInfoError> {
let mut sb = StringBuilder::default();
if let Some(u) = user {
sb.count(u);
}
sb.count(project);
if let Some(c) = committish {
sb.count(c);
}
sb.allocate().map_err(|_| HostedGitInfoError::OutOfMemory)?;
let user_part = match user {
Some(u) => Some(Self::decode_and_append(&mut sb, u)?),
None => None,
};
let project_part = Self::decode_and_append(&mut sb, project)?;
let committish_part = match committish {
Some(c) => Some(Self::decode_and_append(&mut sb, c)?),
None => None,
};
let owned_buffer = sb.move_to_slice();
Ok(Self {
committish: committish_part,
project: project_part,
user: user_part,
host_provider,
default_representation,
_memory_buffer: owned_buffer,
})
}
fn move_from_extracted(
extracted: &mut ExtractResult,
host_provider: HostProvider,
default_representation: Representation,
) -> Self {
let moved = extracted.move_out();
Self {
committish: extracted.committish.clone(),
project: extracted.project.clone(),
user: extracted.user.clone(),
host_provider,
default_representation,
_memory_buffer: moved,
}
}
pub fn from_url(git_url: &[u8]) -> Result<Option<Self>, HostedGitInfoError> {
let git_url_owned: Option<Box<[u8]>>;
let mut git_url_mut: &[u8] = git_url;
if is_github_shorthand(git_url) {
let concatenated = strings::concat(&[b"github:", git_url]);
git_url_owned = Some(concatenated);
git_url_mut = git_url_owned.as_deref().unwrap();
} else {
git_url_owned = None;
}
let _ = &git_url_owned;
let Ok(parsed) = parse_url(git_url_mut) else {
return Ok(None);
};
let host_provider = match parsed.proto {
UrlProtocol::WellFormed(p) => p
.host_provider()
.or_else(|| HostProvider::from_url_domain(&parsed.url)),
UrlProtocol::Unknown => HostProvider::from_url_domain(&parsed.url),
UrlProtocol::Custom(_) => HostProvider::from_url(&parsed.url),
};
let Some(host_provider) = host_provider else {
return Ok(None);
};
let is_shortcut = matches!(parsed.proto, UrlProtocol::WellFormed(p) if p.is_shortcut());
if !is_shortcut {
let Some(mut extracted) = host_provider.extract(&parsed.url)? else {
return Ok(None);
};
return Ok(Some(HostedGitInfo::move_from_extracted(
&mut extracted,
host_provider,
parsed.proto.default_representation(),
)));
}
let pathname_owned = parsed.url.pathname().to_owned_slice();
let mut pathname: &[u8] = strings::trim_prefix(&pathname_owned, b"/");
if let Some(first_at) = strings::index_of_char(pathname, b'@') {
pathname = &pathname[first_at as usize + 1..];
}
let mut user_part: Option<&[u8]> = None;
let project_part: &[u8] = 'blk: {
if let Some(last_slash) = strings::last_index_of_char(pathname, b'/') {
let user_str = &pathname[0..last_slash];
if !user_str.is_empty() {
user_part = Some(user_str);
}
break 'blk &pathname[last_slash + 1..];
} else {
break 'blk pathname;
}
};
let project_trimmed = strings::trim_suffix(project_part, b".git");
let fragment = parsed.url.fragment_identifier().to_owned_slice();
let committish: Option<&[u8]> = if !fragment.is_empty() {
Some(&fragment)
} else {
None
};
Ok(Some(HostedGitInfo::copy_from(
committish,
project_trimmed,
user_part,
host_provider,
Representation::Shortcut, )?))
}
}
pub struct StringPair {
pub save_spec: Box<[u8]>,
pub fetch_spec: Option<Box<[u8]>>,
}
pub struct OwnedJscUrl(NonNull<JscUrl>);
impl core::ops::Deref for OwnedJscUrl {
type Target = JscUrl;
fn deref(&self) -> &JscUrl {
unsafe { self.0.as_ref() }
}
}
impl Drop for OwnedJscUrl {
fn drop(&mut self) {
unsafe { self.0.as_mut() }.deinit();
}
}
pub struct ParsedUrl<'a> {
pub url: OwnedJscUrl,
pub proto: UrlProtocol<'a>,
}
pub fn parse_url(npa_str: &[u8]) -> Result<ParsedUrl<'_>, ParseUrlError> {
let proto_pair = normalize_protocol(npa_str);
let maybe_url = proto_pair.to_url();
if let Some(url) = maybe_url {
return Ok(ParsedUrl {
url,
proto: proto_pair.protocol,
});
}
let corrected = correct_url(&proto_pair)?;
let corrected_url = corrected.to_url();
if let Some(url) = corrected_url {
return Ok(ParsedUrl {
url,
proto: corrected.protocol,
});
}
Err(ParseUrlError::InvalidGitUrl)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::IntoStaticStr)]
pub enum WellDefinedProtocol {
Git,
GitPlusFile,
GitPlusFtp,
GitPlusHttp,
GitPlusHttps,
GitPlusRsync,
GitPlusSsh,
Http,
Https,
Ssh,
Github,
Bitbucket,
Gitlab,
Gist,
Sourcehut,
}
pub(crate) type StringWithColonBuffer = [u8; WellDefinedProtocol::MAX_PROTOCOL_LENGTH + 1];
impl WellDefinedProtocol {
pub(crate) const STRINGS: phf::Map<&'static [u8], WellDefinedProtocol> = phf::phf_map! {
b"bitbucket" => WellDefinedProtocol::Bitbucket,
b"gist" => WellDefinedProtocol::Gist,
b"git+file" => WellDefinedProtocol::GitPlusFile,
b"git+ftp" => WellDefinedProtocol::GitPlusFtp,
b"git+http" => WellDefinedProtocol::GitPlusHttp,
b"git+https" => WellDefinedProtocol::GitPlusHttps,
b"git+rsync" => WellDefinedProtocol::GitPlusRsync,
b"git+ssh" => WellDefinedProtocol::GitPlusSsh,
b"git" => WellDefinedProtocol::Git,
b"github" => WellDefinedProtocol::Github,
b"gitlab" => WellDefinedProtocol::Gitlab,
b"http" => WellDefinedProtocol::Http,
b"https" => WellDefinedProtocol::Https,
b"sourcehut" => WellDefinedProtocol::Sourcehut,
b"ssh" => WellDefinedProtocol::Ssh,
};
fn protocol_str(self) -> &'static [u8] {
match self {
Self::Bitbucket => b"bitbucket",
Self::Gist => b"gist",
Self::GitPlusFile => b"git+file",
Self::GitPlusFtp => b"git+ftp",
Self::GitPlusHttp => b"git+http",
Self::GitPlusHttps => b"git+https",
Self::GitPlusRsync => b"git+rsync",
Self::GitPlusSsh => b"git+ssh",
Self::Git => b"git",
Self::Github => b"github",
Self::Gitlab => b"gitlab",
Self::Http => b"http",
Self::Https => b"https",
Self::Sourcehut => b"sourcehut",
Self::Ssh => b"ssh",
}
}
pub(crate) fn from_string_with_colon(protocol_with_colon: &[u8]) -> Option<Self> {
if protocol_with_colon.is_empty() {
None
} else {
Self::STRINGS
.get(strings::trim_suffix(protocol_with_colon, b":"))
.copied()
}
}
pub(crate) const MAX_PROTOCOL_LENGTH: usize = 9;
pub(crate) fn to_string_with_colon(self, buf: &mut StringWithColonBuffer) -> &[u8] {
let protocol_str = self.protocol_str();
buf[0..protocol_str.len()].copy_from_slice(protocol_str);
buf[protocol_str.len()] = b':';
&buf[0..protocol_str.len() + 1]
}
fn protocol_resource_identifier_concatenation_token(self) -> &'static [u8] {
match self {
Self::Git
| Self::GitPlusFile
| Self::GitPlusFtp
| Self::GitPlusHttp
| Self::GitPlusHttps
| Self::GitPlusRsync
| Self::GitPlusSsh
| Self::Http
| Self::Https
| Self::Ssh => b"//",
Self::Github | Self::Bitbucket | Self::Gitlab | Self::Gist | Self::Sourcehut => b"",
}
}
fn default_representation(self) -> Representation {
match self {
Self::GitPlusSsh | Self::Ssh | Self::GitPlusHttp => Representation::Sshurl,
Self::GitPlusHttps => Representation::Https,
Self::GitPlusFile | Self::GitPlusFtp | Self::GitPlusRsync | Self::Git => {
Representation::Git
}
Self::Http => Representation::Http,
Self::Https => Representation::Https,
Self::Github | Self::Bitbucket | Self::Gitlab | Self::Gist | Self::Sourcehut => {
Representation::Shortcut
}
}
}
fn host_provider(self) -> Option<HostProvider> {
match self {
Self::Github => Some(HostProvider::Github),
Self::Bitbucket => Some(HostProvider::Bitbucket),
Self::Gitlab => Some(HostProvider::Gitlab),
Self::Gist => Some(HostProvider::Gist),
Self::Sourcehut => Some(HostProvider::Sourcehut),
_ => None,
}
}
fn is_shortcut(self) -> bool {
matches!(
self,
Self::Github | Self::Bitbucket | Self::Gitlab | Self::Gist | Self::Sourcehut
)
}
}
pub(crate) fn is_github_shorthand(npa_str: &[u8]) -> bool {
if npa_str.is_empty() {
return false;
}
if npa_str[0] == b'.' || npa_str[0] == b'/' {
return false;
}
let mut pound_idx: Option<usize> = None;
let mut seen_slash = false;
for (i, &c) in npa_str.iter().enumerate() {
match c {
b':' | b'@' => {
if pound_idx.is_none() {
return false;
}
}
b'#' => {
pound_idx = Some(i);
}
b'/' => {
if seen_slash && pound_idx.is_none() {
return false;
}
seen_slash = true;
}
_ => {
if matches!(c, b' ' | b'\t' | b'\n' | b'\r' | 0x0B | 0x0C) && pound_idx.is_none() {
return false;
}
}
}
}
let does_not_end_with_slash = if let Some(pi) = pound_idx {
pi == 0 || npa_str[pi - 1] != b'/'
} else {
!npa_str.is_empty() && npa_str[npa_str.len() - 1] != b'/'
};
seen_slash && does_not_end_with_slash
}
#[derive(Debug, Clone, Copy)]
pub enum UrlProtocol<'a> {
WellFormed(WellDefinedProtocol),
Custom(&'a [u8]),
Unknown,
}
impl<'a> UrlProtocol<'a> {
pub(crate) fn default_representation(self) -> Representation {
match self {
UrlProtocol::WellFormed(p) => p.default_representation(),
_ => Representation::Sshurl, }
}
}
pub(crate) enum UrlProtocolPairUrl<'a> {
Managed(Box<[u8]>),
Unmanaged(&'a [u8]),
}
pub(crate) struct UrlProtocolPair<'a> {
pub url: UrlProtocolPairUrl<'a>,
pub protocol: UrlProtocol<'a>,
}
impl<'a> UrlProtocolPair<'a> {
pub(crate) fn url_slice(&self) -> &[u8] {
match &self.url {
UrlProtocolPairUrl::Managed(s) => s,
UrlProtocolPairUrl::Unmanaged(s) => s,
}
}
fn to_url(&self) -> Option<OwnedJscUrl> {
const _LONG_URL_THRESH: usize = 2048;
let mut protocol_buf: StringWithColonBuffer =
[0u8; WellDefinedProtocol::MAX_PROTOCOL_LENGTH + 1];
match self.protocol {
UrlProtocol::Unknown => Self::concat_parts_to_url(&[b"git+ssh://", self.url_slice()]),
UrlProtocol::Custom(proto_str) => {
Self::concat_parts_to_url(&[proto_str, b"//", self.url_slice()])
}
UrlProtocol::WellFormed(proto_tag) => Self::concat_parts_to_url(&[
proto_tag.to_string_with_colon(&mut protocol_buf),
proto_tag.protocol_resource_identifier_concatenation_token(),
self.url_slice(),
]),
}
}
fn concat_parts_to_url(parts: &[&[u8]]) -> Option<OwnedJscUrl> {
let new_str = strings::concat(parts);
JscUrl::from_utf8(&new_str).map(OwnedJscUrl)
}
}
fn normalize_protocol(npa_str: &[u8]) -> UrlProtocolPair<'_> {
let mut first_colon_idx: i32 = -1;
if let Some(idx) = strings::index_of_char(npa_str, b':') {
first_colon_idx = i32::try_from(idx).expect("int cast");
}
let proto_slice = &npa_str[0..usize::try_from(first_colon_idx + 1).expect("int cast")];
if let Some(url_protocol) = WellDefinedProtocol::from_string_with_colon(proto_slice) {
let post_colon = strings::substring(
npa_str,
Some(usize::try_from(first_colon_idx + 1).expect("int cast")),
None,
);
return UrlProtocolPair {
url: UrlProtocolPairUrl::Unmanaged(if post_colon.starts_with(b"//") {
&post_colon[2..post_colon.len()]
} else {
post_colon
}),
protocol: UrlProtocol::WellFormed(url_protocol),
};
}
let first_at_idx = strings::index_of_char(npa_str, b'@');
if let Some(at_idx) = first_at_idx {
if first_colon_idx != -1 {
if i32::try_from(at_idx).expect("int cast") > first_colon_idx {
return UrlProtocolPair {
url: UrlProtocolPairUrl::Unmanaged(npa_str),
protocol: UrlProtocol::WellFormed(WellDefinedProtocol::GitPlusSsh),
};
} else {
return UrlProtocolPair {
url: UrlProtocolPairUrl::Unmanaged(npa_str),
protocol: UrlProtocol::Unknown,
};
}
} else {
return UrlProtocolPair {
url: UrlProtocolPairUrl::Unmanaged(npa_str),
protocol: UrlProtocol::WellFormed(WellDefinedProtocol::GitPlusSsh),
};
}
}
let maybe_dup_slash_idx = strings::index_of(npa_str, b"//");
if let Some(dup_slash_idx) = maybe_dup_slash_idx {
if i32::try_from(dup_slash_idx).expect("int cast") == first_colon_idx + 1 {
return UrlProtocolPair {
url: UrlProtocolPairUrl::Unmanaged(strings::substring(
npa_str,
Some(dup_slash_idx + 2),
None,
)),
protocol: UrlProtocol::Custom(&npa_str[0..dup_slash_idx]),
};
}
}
if first_colon_idx != -1 {
return UrlProtocolPair {
url: UrlProtocolPairUrl::Unmanaged(strings::substring(
npa_str,
Some(usize::try_from(first_colon_idx + 1).expect("int cast")),
None,
)),
protocol: UrlProtocol::Custom(
&npa_str[0..usize::try_from(first_colon_idx + 1).expect("int cast")],
),
};
}
UrlProtocolPair {
url: UrlProtocolPairUrl::Unmanaged(npa_str),
protocol: UrlProtocol::Unknown,
}
}
pub(crate) fn correct_url<'a>(
url_proto_pair: &UrlProtocolPair<'a>,
) -> Result<UrlProtocolPair<'a>, AllocError> {
let at_idx: isize = if let Some(idx) =
strings::last_index_before_char(url_proto_pair.url_slice(), b'@', b'#')
{
isize::try_from(idx).expect("int cast")
} else {
-1
};
let col_idx: isize = if let Some(idx) =
strings::last_index_before_char(url_proto_pair.url_slice(), b':', b'#')
{
isize::try_from(idx).expect("int cast")
} else {
-1
};
if col_idx > at_idx {
let mut duped: Box<[u8]> = Box::from(url_proto_pair.url_slice());
duped[usize::try_from(col_idx).expect("int cast")] = b'/';
return Ok(UrlProtocolPair {
url: UrlProtocolPairUrl::Managed(duped),
protocol: UrlProtocol::WellFormed(WellDefinedProtocol::GitPlusSsh),
});
}
if col_idx == -1 && matches!(url_proto_pair.protocol, UrlProtocol::Unknown) {
return Ok(UrlProtocolPair {
url: match &url_proto_pair.url {
UrlProtocolPairUrl::Unmanaged(s) => UrlProtocolPairUrl::Unmanaged(s),
UrlProtocolPairUrl::Managed(s) => UrlProtocolPairUrl::Managed(s.clone()),
},
protocol: UrlProtocol::WellFormed(WellDefinedProtocol::GitPlusSsh),
});
}
Ok(UrlProtocolPair {
url: match &url_proto_pair.url {
UrlProtocolPairUrl::Unmanaged(s) => UrlProtocolPairUrl::Unmanaged(s),
UrlProtocolPairUrl::Managed(s) => UrlProtocolPairUrl::Managed(s.clone()),
},
protocol: url_proto_pair.protocol,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Enum, strum::IntoStaticStr)]
#[strum(serialize_all = "lowercase")]
pub enum HostProvider {
Bitbucket,
Gist,
Github,
Gitlab,
Sourcehut,
}
impl HostProvider {
const ALL: [HostProvider; 5] = [
HostProvider::Bitbucket,
HostProvider::Gist,
HostProvider::Github,
HostProvider::Gitlab,
HostProvider::Sourcehut,
];
fn extract(self, url: &JscUrl) -> Result<Option<ExtractResult>, HostedGitInfoError> {
(configs()[self].format_extract)(url)
}
pub fn type_str(self) -> &'static str {
<&'static str>::from(self)
}
fn shortcut(self) -> &'static [u8] {
configs()[self].shortcut
}
pub fn domain(self) -> &'static [u8] {
configs()[self].domain
}
fn shortcut_without_colon(self) -> &'static [u8] {
let shct = self.shortcut();
&shct[0..shct.len() - 1]
}
fn from_shortcut(shortcut_str: &[u8], with_colon: bool) -> Option<HostProvider> {
for provider in Self::ALL {
let shortcut_matches = if with_colon {
provider.shortcut() == shortcut_str
} else {
provider.shortcut_without_colon() == shortcut_str
};
if shortcut_matches {
return Some(provider);
}
}
None
}
fn from_domain(domain_str: &[u8]) -> Option<HostProvider> {
Self::ALL
.into_iter()
.find(|&provider| provider.domain() == domain_str)
}
fn from_url(url: &JscUrl) -> Option<HostProvider> {
let proto_str = OwnedString::new(url.protocol());
if let Some(provider) = HostProvider::from_shortcut(proto_str.byte_slice(), false) {
return Some(provider);
}
HostProvider::from_url_domain(url)
}
fn from_url_domain(url: &JscUrl) -> Option<HostProvider> {
const _MAX_HOSTNAME_LEN: usize = 253;
let hostname_str = OwnedString::new(url.hostname());
let hostname_utf8 = hostname_str.to_utf8();
let hostname = strings::without_prefix(hostname_utf8.slice(), b"www.");
HostProvider::from_domain(hostname)
}
}
pub struct Config {
pub protocols: &'static [WellDefinedProtocol],
pub domain: &'static [u8],
pub shortcut: &'static [u8],
pub tree_path: Option<&'static [u8]>,
pub blob_path: Option<&'static [u8]>,
pub edit_path: Option<&'static [u8]>,
pub format_ssh: formatters::ssh::Type,
pub format_sshurl: formatters::ssh_url::Type,
pub format_https: formatters::https::Type,
pub format_shortcut: formatters::shortcut::Type,
pub format_git: formatters::git::Type,
pub format_extract: formatters::extract::Type,
}
pub struct ExtractResult {
pub user: Option<Range<usize>>,
pub project: Range<usize>,
pub committish: Option<Range<usize>>,
_owned_buffer: Option<Box<[u8]>>,
}
impl ExtractResult {
fn move_out(&mut self) -> Box<[u8]> {
let Some(buffer) = self._owned_buffer.take() else {
panic!(
"Cannot move an empty Result. This is a bug in Bun. Please \
report this issue on GitHub."
);
};
buffer
}
}
pub mod formatters {
use super::*;
pub(super) fn requires_user(user: Option<&[u8]>) {
if user.is_none() {
panic!(
"Attempted to format a default SSH URL without a user. This is an \
irrecoverable programming bug in Bun. Please report this issue \
on GitHub."
);
}
}
pub mod ssh {
use super::*;
pub type Type = fn(
self_: HostProvider,
user: Option<&[u8]>,
project: &[u8],
committish: Option<&[u8]>,
) -> Result<Vec<u8>, AllocError>;
pub(crate) fn default(
self_: HostProvider,
user: Option<&[u8]>,
project: &[u8],
committish: Option<&[u8]>,
) -> Result<Vec<u8>, AllocError> {
requires_user(user);
let cmsh: &[u8] = committish.unwrap_or(b"");
let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" };
let mut v = Vec::new();
write!(
&mut v,
"git@{}:{}/{}.git{}{}",
BStr::new(self_.domain()),
BStr::new(user.unwrap()),
BStr::new(project),
BStr::new(cmsh_sep),
BStr::new(cmsh),
)
.map_err(|_| AllocError)?;
Ok(v)
}
pub(crate) fn gist(
self_: HostProvider,
_user: Option<&[u8]>,
project: &[u8],
committish: Option<&[u8]>,
) -> Result<Vec<u8>, AllocError> {
let cmsh: &[u8] = committish.unwrap_or(b"");
let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" };
let mut v = Vec::new();
write!(
&mut v,
"git@{}:{}.git{}{}",
BStr::new(self_.domain()),
BStr::new(project),
BStr::new(cmsh_sep),
BStr::new(cmsh),
)
.map_err(|_| AllocError)?;
Ok(v)
}
}
pub mod ssh_url {
use super::*;
pub type Type = fn(
self_: HostProvider,
user: Option<&[u8]>,
project: &[u8],
committish: Option<&[u8]>,
) -> Result<Vec<u8>, AllocError>;
pub(crate) fn default(
self_: HostProvider,
user: Option<&[u8]>,
project: &[u8],
committish: Option<&[u8]>,
) -> Result<Vec<u8>, AllocError> {
requires_user(user);
let cmsh: &[u8] = committish.unwrap_or(b"");
let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" };
let mut v = Vec::new();
write!(
&mut v,
"git+ssh://git@{}/{}/{}.git{}{}",
BStr::new(self_.domain()),
BStr::new(user.unwrap()),
BStr::new(project),
BStr::new(cmsh_sep),
BStr::new(cmsh),
)
.map_err(|_| AllocError)?;
Ok(v)
}
pub(crate) fn gist(
self_: HostProvider,
_user: Option<&[u8]>,
project: &[u8],
committish: Option<&[u8]>,
) -> Result<Vec<u8>, AllocError> {
let cmsh: &[u8] = committish.unwrap_or(b"");
let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" };
let mut v = Vec::new();
write!(
&mut v,
"git+ssh://git@{}/{}.git{}{}",
BStr::new(self_.domain()),
BStr::new(project),
BStr::new(cmsh_sep),
BStr::new(cmsh),
)
.map_err(|_| AllocError)?;
Ok(v)
}
}
pub mod https {
use super::*;
pub type Type = fn(
self_: HostProvider,
auth: Option<&[u8]>,
user: Option<&[u8]>,
project: &[u8],
committish: Option<&[u8]>,
) -> Result<Vec<u8>, AllocError>;
pub(crate) fn default(
self_: HostProvider,
auth: Option<&[u8]>,
user: Option<&[u8]>,
project: &[u8],
committish: Option<&[u8]>,
) -> Result<Vec<u8>, AllocError> {
requires_user(user);
let auth_str: &[u8] = auth.unwrap_or(b"");
let auth_sep: &[u8] = if !auth_str.is_empty() { b"@" } else { b"" };
let cmsh: &[u8] = committish.unwrap_or(b"");
let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" };
let mut v = Vec::new();
write!(
&mut v,
"git+https://{}{}{}/{}/{}.git{}{}",
BStr::new(auth_str),
BStr::new(auth_sep),
BStr::new(self_.domain()),
BStr::new(user.unwrap()),
BStr::new(project),
BStr::new(cmsh_sep),
BStr::new(cmsh),
)
.map_err(|_| AllocError)?;
Ok(v)
}
pub(crate) fn gist(
self_: HostProvider,
_auth: Option<&[u8]>,
_user: Option<&[u8]>,
project: &[u8],
committish: Option<&[u8]>,
) -> Result<Vec<u8>, AllocError> {
let cmsh: &[u8] = committish.unwrap_or(b"");
let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" };
let mut v = Vec::new();
write!(
&mut v,
"git+https://{}/{}.git{}{}",
BStr::new(self_.domain()),
BStr::new(project),
BStr::new(cmsh_sep),
BStr::new(cmsh),
)
.map_err(|_| AllocError)?;
Ok(v)
}
pub(crate) fn sourcehut(
self_: HostProvider,
_auth: Option<&[u8]>,
user: Option<&[u8]>,
project: &[u8],
committish: Option<&[u8]>,
) -> Result<Vec<u8>, AllocError> {
requires_user(user);
let cmsh: &[u8] = committish.unwrap_or(b"");
let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" };
let mut v = Vec::new();
write!(
&mut v,
"https://{}/{}/{}.git{}{}",
BStr::new(self_.domain()),
BStr::new(user.unwrap()),
BStr::new(project),
BStr::new(cmsh_sep),
BStr::new(cmsh),
)
.map_err(|_| AllocError)?;
Ok(v)
}
}
pub mod shortcut {
use super::*;
pub type Type = fn(
self_: HostProvider,
user: Option<&[u8]>,
project: &[u8],
committish: Option<&[u8]>,
) -> Result<Vec<u8>, AllocError>;
pub(crate) fn default(
self_: HostProvider,
user: Option<&[u8]>,
project: &[u8],
committish: Option<&[u8]>,
) -> Result<Vec<u8>, AllocError> {
requires_user(user);
let cmsh: &[u8] = committish.unwrap_or(b"");
let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" };
let mut v = Vec::new();
write!(
&mut v,
"{}{}/{}{}{}",
BStr::new(self_.shortcut()),
BStr::new(user.unwrap()),
BStr::new(project),
BStr::new(cmsh_sep),
BStr::new(cmsh),
)
.map_err(|_| AllocError)?;
Ok(v)
}
pub(crate) fn gist(
self_: HostProvider,
_user: Option<&[u8]>,
project: &[u8],
committish: Option<&[u8]>,
) -> Result<Vec<u8>, AllocError> {
let cmsh: &[u8] = committish.unwrap_or(b"");
let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" };
let mut v = Vec::new();
write!(
&mut v,
"{}{}{}{}",
BStr::new(self_.shortcut()),
BStr::new(project),
BStr::new(cmsh_sep),
BStr::new(cmsh),
)
.map_err(|_| AllocError)?;
Ok(v)
}
}
pub mod extract {
use super::*;
pub type Type = fn(url: &JscUrl) -> Result<Option<ExtractResult>, HostedGitInfoError>;
pub(crate) fn github(url: &JscUrl) -> Result<Option<ExtractResult>, HostedGitInfoError> {
let pathname_owned = url.pathname().to_owned_slice();
let pathname = strings::trim_prefix(&pathname_owned, b"/");
let mut iter = pathname.split(|&b| b == b'/');
let Some(user_part) = iter.next() else {
return Ok(None);
};
let Some(project_part) = iter.next() else {
return Ok(None);
};
let type_part = iter.next();
let committish_part = iter.next();
let project = strings::trim_suffix(project_part, b".git");
if user_part.is_empty() || project.is_empty() {
return Ok(None);
}
if let Some(tp) = type_part {
if tp != b"tree" {
return Ok(None);
}
}
let fragment_utf8;
let committish: Option<&[u8]> = if type_part.is_none() {
let fragment_str = OwnedString::new(url.fragment_identifier());
fragment_utf8 = fragment_str.to_utf8();
let fragment = fragment_utf8.slice();
if !fragment.is_empty() {
Some(fragment)
} else {
None
}
} else {
committish_part
};
let mut sb = StringBuilder::default();
sb.count(user_part);
sb.count(project);
if let Some(c) = committish {
sb.count(c);
}
sb.allocate()?;
let user_slice = HostedGitInfo::decode_and_append(&mut sb, user_part)?;
let project_slice = HostedGitInfo::decode_and_append(&mut sb, project)?;
let committish_slice = match committish {
Some(c) => Some(HostedGitInfo::decode_and_append(&mut sb, c)?),
None => None,
};
Ok(Some(ExtractResult {
user: Some(user_slice),
project: project_slice,
committish: committish_slice,
_owned_buffer: Some(sb.move_to_slice()),
}))
}
pub(crate) fn bitbucket(url: &JscUrl) -> Result<Option<ExtractResult>, HostedGitInfoError> {
let pathname_owned = url.pathname().to_owned_slice();
let pathname = strings::trim_prefix(&pathname_owned, b"/");
let mut iter = pathname.split(|&b| b == b'/');
let Some(user_part) = iter.next() else {
return Ok(None);
};
let Some(project_part) = iter.next() else {
return Ok(None);
};
let aux = iter.next();
if let Some(a) = aux {
if a == b"get" {
return Ok(None);
}
}
let project = strings::trim_suffix(project_part, b".git");
if user_part.is_empty() || project.is_empty() {
return Ok(None);
}
let fragment_str = OwnedString::new(url.fragment_identifier());
let fragment_utf8 = fragment_str.to_utf8();
let fragment = fragment_utf8.slice();
let committish: Option<&[u8]> = if !fragment.is_empty() {
Some(fragment)
} else {
None
};
let mut sb = StringBuilder::default();
sb.count(user_part);
sb.count(project);
if let Some(c) = committish {
sb.count(c);
}
sb.allocate()?;
let user_slice = HostedGitInfo::decode_and_append(&mut sb, user_part)?;
let project_slice = HostedGitInfo::decode_and_append(&mut sb, project)?;
let committish_slice = match committish {
Some(c) => Some(HostedGitInfo::decode_and_append(&mut sb, c)?),
None => None,
};
Ok(Some(ExtractResult {
user: Some(user_slice),
project: project_slice,
committish: committish_slice,
_owned_buffer: Some(sb.move_to_slice()),
}))
}
pub(crate) fn gitlab(url: &JscUrl) -> Result<Option<ExtractResult>, HostedGitInfoError> {
let pathname_owned = url.pathname().to_owned_slice();
let pathname = strings::trim_prefix(&pathname_owned, b"/");
if strings::index_of(pathname, b"/-/").is_some()
|| strings::index_of(pathname, b"/archive.tar.gz").is_some()
{
return Ok(None);
}
let Some(end_slash) = strings::last_index_of_char(pathname, b'/') else {
return Ok(None);
};
let project_part = &pathname[end_slash + 1..];
let user_part = &pathname[0..end_slash];
let project = strings::trim_suffix(project_part, b".git");
if user_part.is_empty() || project.is_empty() {
return Ok(None);
}
let fragment_str = OwnedString::new(url.fragment_identifier());
let fragment_utf8 = fragment_str.to_utf8();
let committish = fragment_utf8.slice();
let mut sb = StringBuilder::default();
sb.count(user_part);
sb.count(project);
if !committish.is_empty() {
sb.count(committish);
}
sb.allocate()?;
let user_slice = HostedGitInfo::decode_and_append(&mut sb, user_part)?;
let project_slice = HostedGitInfo::decode_and_append(&mut sb, project)?;
let committish_slice = if !committish.is_empty() {
let Ok(r) = HostedGitInfo::decode_and_append(&mut sb, committish) else {
return Ok(None);
};
Some(r)
} else {
None
};
Ok(Some(ExtractResult {
user: Some(user_slice),
project: project_slice,
committish: committish_slice,
_owned_buffer: Some(sb.move_to_slice()),
}))
}
pub(crate) fn gist(url: &JscUrl) -> Result<Option<ExtractResult>, HostedGitInfoError> {
let pathname_owned = url.pathname().to_owned_slice();
let pathname = strings::trim_prefix(&pathname_owned, b"/");
let mut iter = pathname.split(|&b| b == b'/');
let Some(mut user_part) = iter.next() else {
return Ok(None);
};
let mut project_part = iter.next();
let aux = iter.next();
if let Some(a) = aux {
if a == b"raw" {
return Ok(None);
}
}
if project_part.is_none() || project_part.unwrap().is_empty() {
project_part = Some(user_part);
user_part = b"";
}
let project = strings::trim_suffix(project_part.unwrap(), b".git");
let user: Option<&[u8]> = if !user_part.is_empty() {
Some(user_part)
} else {
None
};
if project.is_empty() {
return Ok(None);
}
let fragment_str = OwnedString::new(url.fragment_identifier());
let fragment_utf8 = fragment_str.to_utf8();
let fragment = fragment_utf8.slice();
let committish: Option<&[u8]> = if !fragment.is_empty() {
Some(fragment)
} else {
None
};
let mut sb = StringBuilder::default();
if let Some(u) = user {
sb.count(u);
}
sb.count(project);
if let Some(c) = committish {
sb.count(c);
}
let Ok(()) = sb.allocate() else {
return Ok(None);
};
let user_slice = match user {
Some(u) => {
let Ok(r) = HostedGitInfo::decode_and_append(&mut sb, u) else {
return Ok(None);
};
Some(r)
}
None => None,
};
let Ok(project_slice) = HostedGitInfo::decode_and_append(&mut sb, project) else {
return Ok(None);
};
let committish_slice = match committish {
Some(c) => {
let Ok(r) = HostedGitInfo::decode_and_append(&mut sb, c) else {
return Ok(None);
};
Some(r)
}
None => None,
};
Ok(Some(ExtractResult {
user: user_slice,
project: project_slice,
committish: committish_slice,
_owned_buffer: Some(sb.move_to_slice()),
}))
}
pub(crate) fn sourcehut(url: &JscUrl) -> Result<Option<ExtractResult>, HostedGitInfoError> {
let pathname_owned = url.pathname().to_owned_slice();
let pathname = strings::trim_prefix(&pathname_owned, b"/");
let mut iter = pathname.split(|&b| b == b'/');
let Some(user_part) = iter.next() else {
return Ok(None);
};
let Some(project_part) = iter.next() else {
return Ok(None);
};
let aux = iter.next();
if let Some(a) = aux {
if a == b"archive" {
return Ok(None);
}
}
let project = strings::trim_suffix(project_part, b".git");
if user_part.is_empty() || project.is_empty() {
return Ok(None);
}
let fragment_str = OwnedString::new(url.fragment_identifier());
let fragment_utf8 = fragment_str.to_utf8();
let fragment = fragment_utf8.slice();
let committish: Option<&[u8]> = if !fragment.is_empty() {
Some(fragment)
} else {
None
};
let mut sb = StringBuilder::default();
sb.count(user_part);
sb.count(project);
if let Some(c) = committish {
sb.count(c);
}
let Ok(()) = sb.allocate() else {
return Ok(None);
};
let user_slice = 'blk: {
let start = sb.len;
let writable = sb.writable();
let Ok(decoded_len) = PercentEncoding::decode_into(writable, user_part) else {
return Ok(None);
};
let decoded_len = decoded_len as usize;
sb.len += decoded_len;
break 'blk start..start + decoded_len;
};
let project_slice = 'blk: {
let start = sb.len;
let writable = sb.writable();
let Ok(decoded_len) = PercentEncoding::decode_into(writable, project) else {
return Ok(None);
};
let decoded_len = decoded_len as usize;
sb.len += decoded_len;
break 'blk start..start + decoded_len;
};
let committish_slice = if let Some(c) = committish {
let start = sb.len;
let writable = sb.writable();
let Ok(decoded_len) = PercentEncoding::decode_into(writable, c) else {
return Ok(None);
};
let decoded_len = decoded_len as usize;
sb.len += decoded_len;
Some(start..start + decoded_len)
} else {
None
};
Ok(Some(ExtractResult {
user: Some(user_slice),
project: project_slice,
committish: committish_slice,
_owned_buffer: Some(sb.move_to_slice()),
}))
}
}
pub mod git {
use super::*;
pub type Type = Option<
fn(
self_: HostProvider,
auth: Option<&[u8]>,
user: Option<&[u8]>,
project: &[u8],
committish: Option<&[u8]>,
) -> Result<Vec<u8>, AllocError>,
>;
pub(crate) const DEFAULT: Type = None;
pub(crate) fn github(
self_: HostProvider,
auth: Option<&[u8]>,
user: Option<&[u8]>,
project: &[u8],
committish: Option<&[u8]>,
) -> Result<Vec<u8>, AllocError> {
requires_user(user);
let auth_str: &[u8] = auth.unwrap_or(b"");
let auth_sep: &[u8] = if !auth_str.is_empty() { b"@" } else { b"" };
let cmsh: &[u8] = committish.unwrap_or(b"");
let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" };
let mut v = Vec::new();
write!(
&mut v,
"git://{}{}{}/{}/{}.git{}{}",
BStr::new(auth_str),
BStr::new(auth_sep),
BStr::new(self_.domain()),
BStr::new(user.unwrap()),
BStr::new(project),
BStr::new(cmsh_sep),
BStr::new(cmsh),
)
.map_err(|_| AllocError)?;
Ok(v)
}
pub(crate) fn gist(
self_: HostProvider,
_auth: Option<&[u8]>,
_user: Option<&[u8]>,
project: &[u8],
committish: Option<&[u8]>,
) -> Result<Vec<u8>, AllocError> {
let cmsh: &[u8] = committish.unwrap_or(b"");
let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" };
let mut v = Vec::new();
write!(
&mut v,
"git://{}/{}.git{}{}",
BStr::new(self_.domain()),
BStr::new(project),
BStr::new(cmsh_sep),
BStr::new(cmsh),
)
.map_err(|_| AllocError)?;
Ok(v)
}
}
}
fn configs() -> &'static EnumMap<HostProvider, Config> {
use std::sync::OnceLock;
static CONFIGS: OnceLock<EnumMap<HostProvider, Config>> = OnceLock::new();
CONFIGS.get_or_init(|| {
EnumMap::from_fn(|k| match k {
HostProvider::Bitbucket => Config {
protocols: &[
WellDefinedProtocol::GitPlusHttp,
WellDefinedProtocol::GitPlusHttps,
WellDefinedProtocol::Ssh,
WellDefinedProtocol::Https,
],
domain: b"bitbucket.org",
shortcut: b"bitbucket:",
tree_path: Some(b"src"),
blob_path: Some(b"src"),
edit_path: Some(b"?mode=edit"),
format_ssh: formatters::ssh::default,
format_sshurl: formatters::ssh_url::default,
format_https: formatters::https::default,
format_shortcut: formatters::shortcut::default,
format_git: formatters::git::DEFAULT,
format_extract: formatters::extract::bitbucket,
},
HostProvider::Gist => Config {
protocols: &[
WellDefinedProtocol::Git,
WellDefinedProtocol::GitPlusSsh,
WellDefinedProtocol::GitPlusHttps,
WellDefinedProtocol::Ssh,
WellDefinedProtocol::Https,
],
domain: b"gist.github.com",
shortcut: b"gist:",
tree_path: None,
blob_path: None,
edit_path: Some(b"edit"),
format_ssh: formatters::ssh::gist,
format_sshurl: formatters::ssh_url::gist,
format_https: formatters::https::gist,
format_shortcut: formatters::shortcut::gist,
format_git: Some(formatters::git::gist),
format_extract: formatters::extract::gist,
},
HostProvider::Github => Config {
protocols: &[
WellDefinedProtocol::Git,
WellDefinedProtocol::Http,
WellDefinedProtocol::GitPlusSsh,
WellDefinedProtocol::GitPlusHttps,
WellDefinedProtocol::Ssh,
WellDefinedProtocol::Https,
],
domain: b"github.com",
shortcut: b"github:",
tree_path: Some(b"tree"),
blob_path: Some(b"blob"),
edit_path: Some(b"edit"),
format_ssh: formatters::ssh::default,
format_sshurl: formatters::ssh_url::default,
format_https: formatters::https::default,
format_shortcut: formatters::shortcut::default,
format_git: Some(formatters::git::github),
format_extract: formatters::extract::github,
},
HostProvider::Gitlab => Config {
protocols: &[
WellDefinedProtocol::GitPlusSsh,
WellDefinedProtocol::GitPlusHttps,
WellDefinedProtocol::Ssh,
WellDefinedProtocol::Https,
],
domain: b"gitlab.com",
shortcut: b"gitlab:",
tree_path: Some(b"tree"),
blob_path: Some(b"tree"),
edit_path: Some(b"-/edit"),
format_ssh: formatters::ssh::default,
format_sshurl: formatters::ssh_url::default,
format_https: formatters::https::default,
format_shortcut: formatters::shortcut::default,
format_git: formatters::git::DEFAULT,
format_extract: formatters::extract::gitlab,
},
HostProvider::Sourcehut => Config {
protocols: &[WellDefinedProtocol::GitPlusSsh, WellDefinedProtocol::Https],
domain: b"git.sr.ht",
shortcut: b"sourcehut:",
tree_path: Some(b"tree"),
blob_path: Some(b"tree"),
edit_path: None,
format_ssh: formatters::ssh::default,
format_sshurl: formatters::ssh_url::default,
format_https: formatters::https::sourcehut,
format_shortcut: formatters::shortcut::default,
format_git: formatters::git::DEFAULT,
format_extract: formatters::extract::sourcehut,
},
})
})
}
pub mod testing_apis {}