Skip to content

Commit

Permalink
Add push method back to server
Browse files Browse the repository at this point in the history
Support CA realisation with new encoding
  • Loading branch information
linyinfeng committed Dec 1, 2023
1 parent 6d9e02c commit 8ac1d95
Show file tree
Hide file tree
Showing 9 changed files with 546 additions and 74 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ warp = "*"
hyper = "0"
http = "0"
reqwest = "*"
# wait for a release containing krustlet/oci-distribution#57 and #71
# wait for a release containing krustlet/oci-distribution#57, #71, and #90
oci-distribution = { git = "https://github.com/krustlet/oci-distribution" }
clap = { version = "*", features = [ "cargo", "derive" ] }
clap_complete = "*"
Expand All @@ -37,3 +37,4 @@ nix-base32 = "*"
sha2 = "*"
tempfile = "*"
ed25519-compact = "*"
urlencoding = "*"
253 changes: 249 additions & 4 deletions src/convert.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,260 @@
use clap::Parser;
use clap::ValueEnum;
use data_encoding::BASE32_DNSSEC;
use once_cell::sync::Lazy;
use std::collections::BTreeMap;

use crate::error::Error;

pub fn key_to_tag(key: &str) -> String {
#[derive(Clone, Debug, Parser)]
pub struct EncodingOptions {
#[arg(long, value_enum, default_value = "custom")]
pub tag_encoding: TagEncoding,
#[arg(long)]
pub fallback_encodings: Vec<TagEncoding>,
}

impl EncodingOptions {
pub fn key_to_tag(&self, key: &str) -> (String, Vec<String>) {
let main = self.tag_encoding.key_to_tag(key);
let fallbacks = self
.fallback_encodings
.iter()
.map(|e| e.key_to_tag(key))
.collect();
(main, fallbacks)
}
pub fn tag_to_key(&self, tag: &str) -> Result<String, Error> {
let mut errors = vec![];
let main = [self.tag_encoding];
let encodings = main.iter().chain(self.fallback_encodings.iter());
for e in encodings {
match e.tag_to_key(tag) {
Ok(r) => return Ok(r),
Err(e) => errors.push(e),
}
}
Err(Error::TagToKey(errors))
}
}

#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum TagEncoding {
// A custom encoding
Custom,
// https://docs.rs/data-encoding/latest/data_encoding/constant.BASE32_DNSSEC.html
// It uses a base32 extended hex alphabet.
// It is case-insensitive when decoding and uses lowercase when encoding.
// It does not use padding.
BASE32_DNSSEC.encode(key.as_bytes())
Base32DNSSEC,
}

pub fn tag_to_key(tag: &str) -> Result<String, Error> {
Ok(String::from_utf8(BASE32_DNSSEC.decode(tag.as_bytes())?)?)
static CUSTOM_ENCODING: Lazy<CustomEncoding> = Lazy::new(CustomEncoding::new);

impl TagEncoding {
pub fn key_to_tag(&self, key: &str) -> String {
match self {
TagEncoding::Custom => CUSTOM_ENCODING.encode(key),
TagEncoding::Base32DNSSEC => BASE32_DNSSEC.encode(key.as_bytes()),
}
}

pub fn tag_to_key(&self, tag: &str) -> Result<String, Error> {
match self {
TagEncoding::Custom => CUSTOM_ENCODING.decode(tag),
TagEncoding::Base32DNSSEC => {
Ok(String::from_utf8(BASE32_DNSSEC.decode(tag.as_bytes())?)?)
}
}
}
}

/// A tag MUST be at most 128 characters in length and MUST match the following regular expression:
/// [a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}
/// https://github.com/opencontainers/distribution-spec/blob/main/spec.md
#[derive(Clone, Debug)]
pub struct CustomEncoding {
symbol_table: Vec<char>,
reverse_table: BTreeMap<char, u32>,
}

impl Default for CustomEncoding {
fn default() -> Self {
Self::new()
}
}

impl CustomEncoding {
pub fn new() -> CustomEncoding {
let mut symbol_table = Vec::new();
symbol_table.extend('0'..='9');
symbol_table.extend('a'..='z');
symbol_table.extend('A'..='Z');
symbol_table.push('-');
symbol_table.push('.');

let mut reverse_table = BTreeMap::new();

for (i, c) in symbol_table.iter().enumerate() {
reverse_table.insert(*c, i as u32);
}

CustomEncoding {
symbol_table,
reverse_table,
}
}

pub fn encode(&self, key: &str) -> String {
let mut result = String::new();
let mut first = true;
for c in key.chars() {
match c {
'a'..='z' | 'A'..='Z' | '0'..='9' => result.push(c),
'-' | '.' => {
if first {
self.encode_char(&mut result, c);
} else {
result.push(c);
}
}
_ => self.encode_char(&mut result, c),
}
first = false;
}
result
}

fn encode_char(&self, result: &mut String, c: char) {
result.push('_');

let mut n: u32 = c.into();

let mut char_code = Vec::new();
let base = self.symbol_table.len() as u32;
while n != 0 {
let quotient = n / base;
let remainder = n % base;

char_code.push(self.symbol_table[remainder as usize]);

n = quotient;
}
result.extend(char_code.iter().rev());

result.push('_');
}

pub fn decode(&self, tag: &str) -> Result<String, Error> {
let mut chars = tag.chars();
let mut result = String::new();
while let Some(c) = chars.next() {
match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '.' => result.push(c),
'_' => {
let mut encoded_char = Vec::new();
loop {
match chars.next() {
Some('_') => break,
Some(n) => encoded_char.push(n),
None => return Err(Error::InvalidTag(tag.to_string())),
}
}
result.push(
self.decode_char(&encoded_char)
.ok_or_else(|| Error::InvalidTag(tag.to_string()))?,
)
}
_ => return Err(Error::InvalidTag(tag.to_string())),
}
}
Ok(result)
}

fn decode_char(&self, encoded: &[char]) -> Option<char> {
let base = self.symbol_table.len() as u32;
let mut n = 0u32;
for c in encoded.iter() {
n = n.checked_mul(base)?;
n = n.checked_add(*self.reverse_table.get(c)?)?;
}
n.try_into().ok()
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn custom_encode_symbol_table_length() {
assert_eq!(CUSTOM_ENCODING.symbol_table.len(), 64);
}

#[test]
fn custom_encode_symbol_table_validate() {
assert_eq!(
CUSTOM_ENCODING.symbol_table.len(),
CUSTOM_ENCODING.reverse_table.len()
);
for (i, c) in CUSTOM_ENCODING.symbol_table.iter().enumerate() {
assert_eq!(CUSTOM_ENCODING.reverse_table[c], i as u32);
}
for (c, i) in CUSTOM_ENCODING.reverse_table.iter() {
assert_eq!(CUSTOM_ENCODING.symbol_table[*i as usize], *c);
}
}

#[test]
fn custom_encode_id() {
assert_eq!(
CUSTOM_ENCODING
.encode("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-."),
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-."
);
}

#[test]
fn custom_encode_first_special() {
assert_eq!(CUSTOM_ENCODING.encode("--"), "_J_-");
assert_eq!(CUSTOM_ENCODING.encode(".."), "_K_.");
assert_eq!(CUSTOM_ENCODING.encode("//"), "_L__L_");
assert_eq!(CUSTOM_ENCODING.encode("__"), "_1v__1v_");
}

#[test]
fn custom_decode_id() {
assert_eq!(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.",
CUSTOM_ENCODING
.decode("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.")
.unwrap()
);
}

#[test]
fn custom_decode_first_special() {
assert_eq!(("--"), CUSTOM_ENCODING.decode("_J_-").unwrap());
assert_eq!((".."), CUSTOM_ENCODING.decode("_K_.").unwrap());
assert_eq!(("//"), CUSTOM_ENCODING.decode("_L__L_").unwrap());
assert_eq!(("__"), CUSTOM_ENCODING.decode("_1v__1v_").unwrap());
}

#[test]
fn custom_encode_decode() {
let test_strings = [
"test",
"测试",
"_test-测试_",
"._test-测试_.",
"._test-测试_.测试",
"realisations/sha256:67890e0958e5d1a2944a3389151472a9acde025c7812f68381a7eef0d82152d1!libgcc.doi"
];
for s in test_strings {
assert_eq!(
CUSTOM_ENCODING.decode(&CUSTOM_ENCODING.encode(s)).unwrap(),
s
);
}
}
}
8 changes: 7 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ pub enum Error {
Http(#[from] http::Error),
#[error("decode error: {0}")]
Decode(#[from] data_encoding::DecodeError),
#[error("tag-to-key error: {0:?}")]
TagToKey(Vec<Error>),
#[error("invalid tag error: {0}")]
InvalidTag(String),
#[error("from utf-8 error: {0}")]
FromUtf8(#[from] FromUtf8Error),
#[error("invalid authorization header: {0}")]
InvalidAuthorization(String),
#[error("oci distribution error: {0}")]
OciDistribution(#[from] OciDistributionError),
#[error("invalid imag`e layer count: {0}")]
#[error("invalid image layer count: {0}")]
InvalidLayerCount(usize),
#[error("invalid image layer media type: {0}")]
InvalidLayerMediaType(String),
Expand Down Expand Up @@ -85,6 +89,8 @@ impl Error {
match self {
Error::Http(_) => StatusCode::INTERNAL_SERVER_ERROR,
Error::Decode(_) => StatusCode::BAD_REQUEST,
Error::TagToKey(_) => StatusCode::BAD_REQUEST,
Error::InvalidTag(_) => StatusCode::BAD_REQUEST,
Error::FromUtf8(_) => StatusCode::BAD_REQUEST,
Error::InvalidAuthorization(_) => StatusCode::BAD_REQUEST,
Error::OciDistribution(_) => StatusCode::BAD_REQUEST,
Expand Down
25 changes: 18 additions & 7 deletions src/key.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
use crate::{
convert::{key_to_tag, tag_to_key},
error::Error,
options::TagCommands,
};
use crate::{error::Error, options::TagCommands};

pub async fn key_main(command: TagCommands) -> Result<(), Error> {
match command {
TagCommands::Encode { key } => print!("{}", key_to_tag(&key)),
TagCommands::Decode { tag } => print!("{}", tag_to_key(&tag)?),
TagCommands::Encode {
key,
fallbacks,
encoding_options,
} => {
let (m, f) = encoding_options.key_to_tag(&key);
println!("{}", m);
if fallbacks {
for tag in f {
println!("{}", tag);
}
}
}
TagCommands::Decode {
tag,
encoding_options,
} => println!("{}", encoding_options.tag_to_key(&tag)?),
}
Ok(())
}
Loading

0 comments on commit 8ac1d95

Please sign in to comment.