use rupg_common::{Error, Result, SqlState};
use rupg_types::Value;
use crate::{Call, Kernel, bad_value};
pub(crate) fn by_src(src: &str) -> Option<Kernel> {
Some(match src {
"binary_encode" => binary_encode,
"binary_decode" => binary_decode,
_ => return None,
})
}
#[derive(Clone, Copy)]
enum Form {
Hex,
Base64 { url: bool },
Base32Hex,
Escape,
}
fn form(name: &str) -> Result<Form> {
Ok(match name.to_ascii_lowercase().as_str() {
"hex" => Form::Hex,
"base64" => Form::Base64 { url: false },
"base64url" => Form::Base64 { url: true },
"base32hex" => Form::Base32Hex,
"escape" => Form::Escape,
_ => {
return Err(Error::new(
SqlState::INVALID_PARAMETER_VALUE,
format!("unrecognized encoding: \"{name}\""),
)
.with_hint(
"Valid encodings are \"base32hex\", \"base64\", \"base64url\", \"escape\", and \"hex\".",
));
}
})
}
fn binary_encode(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let (Some(Value::Bytea(data)), Some(Value::Text(name))) = (args.first(), args.get(1)) else {
return Err(bad_value());
};
let text = match form(name)? {
Form::Hex => hex_encode(data),
Form::Base64 { url } => base64_encode(data, url),
Form::Base32Hex => base32hex_encode(data),
Form::Escape => escape_encode(data),
};
Ok(Value::Text(text))
}
fn binary_decode(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let (Some(Value::Text(data)), Some(Value::Text(name))) = (args.first(), args.get(1)) else {
return Err(bad_value());
};
let bytes = match form(name)? {
Form::Hex => hex_decode(data)?,
Form::Base64 { url } => base64_decode(data, url)?,
Form::Base32Hex => base32hex_decode(data)?,
Form::Escape => escape_decode(data)?,
};
Ok(Value::Bytea(bytes))
}
fn invalid(message: String) -> Error {
Error::new(SqlState::INVALID_PARAMETER_VALUE, message)
}
fn symbol(text: &str, at: usize) -> &str {
let c = text[at..].chars().next().map_or(0, char::len_utf8);
&text[at..at + c]
}
fn space(c: u8) -> bool {
matches!(c, b' ' | b'\t' | b'\n' | b'\r')
}
fn hex_encode(data: &[u8]) -> String {
const DIGITS: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(data.len() * 2);
for &b in data {
out.push(char::from(DIGITS[usize::from(b >> 4)]));
out.push(char::from(DIGITS[usize::from(b & 15)]));
}
out
}
fn hex_decode(text: &str) -> Result<Vec<u8>> {
let src = text.as_bytes();
let digit = |at: usize| {
char::from(src[at])
.to_digit(16)
.ok_or_else(|| invalid(format!("invalid hexadecimal digit: \"{}\"", symbol(text, at))))
};
let mut out = Vec::with_capacity(src.len() / 2);
let mut s = 0;
while s < src.len() {
if space(src[s]) {
s += 1;
continue;
}
let high = digit(s)?;
s += 1;
if s >= src.len() {
return Err(invalid("invalid hexadecimal data: odd number of digits".into()));
}
let low = digit(s)?;
s += 1;
out.push(((high << 4) | low) as u8);
}
Ok(out)
}
fn base64_encode(data: &[u8], url: bool) -> String {
let alphabet: &[u8; 64] = if url {
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
} else {
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
};
let sym = |buf: u32, shift: u32| char::from(alphabet[((buf >> shift) & 0x3f) as usize]);
let mut out = String::with_capacity(data.len().div_ceil(3) * 4 + data.len() / 57);
let mut line = 76;
let (groups, rest) = data.as_chunks::<3>();
for c in groups {
let buf = (u32::from(c[0]) << 16) | (u32::from(c[1]) << 8) | u32::from(c[2]);
for shift in [18, 12, 6, 0] {
out.push(sym(buf, shift));
}
if !url && out.len() >= line {
out.push('\n');
line = out.len() + 76;
}
}
match *rest {
[a] => {
let buf = u32::from(a) << 16;
out.push(sym(buf, 18));
out.push(sym(buf, 12));
if !url {
out.push_str("==");
}
}
[a, b] => {
let buf = (u32::from(a) << 16) | (u32::from(b) << 8);
out.push(sym(buf, 18));
out.push(sym(buf, 12));
out.push(sym(buf, 6));
if !url {
out.push('=');
}
}
_ => {}
}
out
}
fn base64_decode(text: &str, url: bool) -> Result<Vec<u8>> {
let name = if url { "base64url" } else { "base64" };
let mut out = Vec::with_capacity(text.len() * 3 / 4);
let (mut buf, mut pos, mut end) = (0u32, 0, 0);
for (at, mut c) in text.bytes().enumerate() {
if space(c) {
continue;
}
if url {
c = match c {
b'-' => b'+',
b'_' => b'/',
c => c,
};
}
let b = if c == b'=' {
if end == 0 {
end = match pos {
2 => 1,
3 => 2,
_ => {
return Err(invalid(format!(
"unexpected \"=\" while decoding {name} sequence"
)));
}
};
}
0
} else {
match c {
b'A'..=b'Z' => c - b'A',
b'a'..=b'z' => c - b'a' + 26,
b'0'..=b'9' => c - b'0' + 52,
b'+' => 62,
b'/' => 63,
_ => {
return Err(invalid(format!(
"invalid symbol \"{}\" found while decoding {name} sequence",
symbol(text, at)
)));
}
}
};
buf = (buf << 6) | u32::from(b);
pos += 1;
if pos == 4 {
out.push((buf >> 16) as u8);
if end == 0 || end > 1 {
out.push((buf >> 8) as u8);
}
if end == 0 || end > 2 {
out.push(buf as u8);
}
buf = 0;
pos = 0;
}
}
match pos {
0 => {}
2 if url => out.push((buf >> 4) as u8),
3 if url => {
out.push((buf >> 10) as u8);
out.push((buf >> 2) as u8);
}
_ => {
return Err(invalid(format!("invalid {name} end sequence")).with_hint(
"Input data is missing padding, is truncated, or is otherwise corrupted.",
));
}
}
Ok(out)
}
fn base32hex_encode(data: &[u8]) -> String {
const ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHIJKLMNOPQRSTUV";
let mut out = String::with_capacity(data.len().div_ceil(5) * 8);
let (mut buf, mut bits) = (0u32, 0);
for &b in data {
buf = (buf << 8) | u32::from(b);
bits += 8;
while bits >= 5 {
bits -= 5;
out.push(char::from(ALPHABET[((buf >> bits) & 0x1f) as usize]));
buf &= (1 << bits) - 1;
}
}
if bits > 0 {
out.push(char::from(ALPHABET[((buf << (5 - bits)) & 0x1f) as usize]));
}
while !out.len().is_multiple_of(8) {
out.push('=');
}
out
}
fn base32hex_decode(text: &str) -> Result<Vec<u8>> {
let bad = |at| {
invalid(format!(
"invalid symbol \"{}\" found while decoding base32hex sequence",
symbol(text, at)
))
};
let mut out = Vec::with_capacity(text.len() * 5 / 8);
let (mut buf, mut bits, mut pos, mut end) = (0u32, 0, 0, false);
for (at, c) in text.bytes().enumerate() {
if space(c) {
continue;
}
if c == b'=' {
if !end {
if !matches!(pos, 2 | 4 | 5 | 7) {
return Err(invalid(
"unexpected \"=\" while decoding base32hex sequence".into(),
));
}
end = true;
}
pos += 1;
continue;
}
if end {
return Err(bad(at));
}
let val = match c {
b'0'..=b'9' => c - b'0',
b'A'..=b'V' => c - b'A' + 10,
b'a'..=b'v' => c - b'a' + 10,
_ => return Err(bad(at)),
};
buf = (buf << 5) | u32::from(val);
bits += 5;
pos += 1;
while bits >= 8 {
bits -= 8;
out.push((buf >> bits) as u8);
buf &= (1 << bits) - 1;
}
if pos == 8 {
pos = 0;
}
}
Ok(out)
}
fn escape_encode(data: &[u8]) -> String {
let mut out = String::with_capacity(data.len());
for &c in data {
if c == 0 || c >= 0x80 {
out.push('\\');
for shift in [6, 3, 0] {
out.push(char::from(b'0' + ((c >> shift) & 7)));
}
} else if c == b'\\' {
out.push_str("\\\\");
} else {
out.push(char::from(c));
}
}
out
}
fn escape_decode(text: &str) -> Result<Vec<u8>> {
let src = text.as_bytes();
let mut out = Vec::with_capacity(src.len());
let mut s = 0;
while s < src.len() {
if src[s] != b'\\' {
out.push(src[s]);
s += 1;
} else if s + 3 < src.len()
&& (b'0'..=b'3').contains(&src[s + 1])
&& (b'0'..=b'7').contains(&src[s + 2])
&& (b'0'..=b'7').contains(&src[s + 3])
{
out.push(((src[s + 1] - b'0') << 6) | ((src[s + 2] - b'0') << 3) | (src[s + 3] - b'0'));
s += 4;
} else if s + 1 < src.len() && src[s + 1] == b'\\' {
out.push(b'\\');
s += 2;
} else {
return Err(Error::new(
SqlState::INVALID_TEXT_REPRESENTATION,
"invalid input syntax for type bytea",
));
}
}
Ok(out)
}