Skip to content

Commit

Permalink
Parse and add tags to list objects output type
Browse files Browse the repository at this point in the history
  • Loading branch information
donatello committed Apr 3, 2024
1 parent 0a95ec6 commit 961464d
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 3 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ lazy_static = "1.4.0"
md5 = "0.7.0"
multimap = "0.10.0"
os_info = "3.7.0"
percent-encoding = "2.3.0"
rand = "0.8.5"
regex = "1.9.4"
serde = { version = "1.0.188", features = ["derive"] }
Expand Down
2 changes: 2 additions & 0 deletions src/s3/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ pub enum Error {
PostPolicyError(String),
InvalidObjectLockConfig(String),
NoClientProvided,
TagDecodingError(String, String),
}

impl std::error::Error for Error {}
Expand Down Expand Up @@ -214,6 +215,7 @@ impl fmt::Display for Error {
Error::PostPolicyError(m) => write!(f, "{}", m),
Error::InvalidObjectLockConfig(m) => write!(f, "{}", m),
Error::NoClientProvided => write!(f, "no client provided"),
Error::TagDecodingError(input, error_message) => write!(f, "tag decoding failed: {} on input '{}'", error_message, input),
}
}
}
Expand Down
9 changes: 8 additions & 1 deletion src/s3/response/list_objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use crate::s3::{
error::Error,
types::{FromS3Response, ListEntry, S3Request},
utils::{
from_iso8601utc, urldecode,
from_iso8601utc, parse_tags, urldecode,
xml::{Element, MergeXmlElements},
},
};
Expand Down Expand Up @@ -125,6 +125,11 @@ fn parse_list_objects_contents(
})
.collect::<HashMap<String, String>>()
});
let user_tags = content
.get_child_text("UserTags")
.as_ref()
.map(|x| parse_tags(x))
.transpose()?;
let is_delete_marker = content.name() == "DeleteMarker";

contents.push(ListEntry {
Expand All @@ -138,6 +143,7 @@ fn parse_list_objects_contents(
is_latest,
version_id,
user_metadata,
user_tags,
is_prefix: false,
is_delete_marker,
encoding_type: etype,
Expand Down Expand Up @@ -168,6 +174,7 @@ fn parse_list_objects_common_prefixes(
is_latest: false,
version_id: None,
user_metadata: None,
user_tags: None,
is_prefix: true,
is_delete_marker: false,
encoding_type: encoding_type.as_ref().cloned(),
Expand Down
1 change: 1 addition & 0 deletions src/s3/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ pub struct ListEntry {
pub is_latest: bool, // except ListObjects V1/V2
pub version_id: Option<String>, // except ListObjects V1/V2
pub user_metadata: Option<HashMap<String, String>>,
pub user_tags: Option<HashMap<String, String>>,
pub is_prefix: bool,
pub is_delete_marker: bool,
pub encoding_type: Option<String>,
Expand Down
83 changes: 81 additions & 2 deletions src/s3/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@

//! Various utility and helper functions
use crate::s3::error::Error;
use std::collections::{BTreeMap, HashMap};

use base64::engine::general_purpose::STANDARD as BASE64;
use base64::engine::Engine as _;
use byteorder::{BigEndian, ReadBytesExt};
Expand All @@ -24,13 +25,15 @@ use crc::{Crc, CRC_32_ISO_HDLC};
use lazy_static::lazy_static;
use md5::compute as md5compute;
use multimap::MultiMap;
use percent_encoding::{percent_decode_str, utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use regex::Regex;
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
pub use urlencoding::decode as urldecode;
pub use urlencoding::encode as urlencode;
use xmltree::Element;

use crate::s3::error::Error;

/// Date and time with UTC timezone
pub type UtcTime = DateTime<Utc>;

Expand Down Expand Up @@ -392,11 +395,86 @@ pub fn copy_slice(dst: &mut [u8], src: &[u8]) -> usize {
c
}

// Characters to escape in query strings. Based on RFC 3986 and the golang
// net/url implementation used in the MinIO server.
//
// https://tools.ietf.org/html/rfc3986
//
// 1. All non-ascii characters are escaped always.
// 2. All reserved characters are escaped.
// 3. Any other characters are not escaped.
//
// Unreserved characters in addition to alphanumeric characters are: '-', '_',
// '.', '~' (§2.3 Unreserved characters (mark))
//
// Reserved characters for query strings: '$', '&', '+', ',', '/', ':', ';',
// '=', '?', '@' (§3.4)
//
// NON_ALPHANUMERIC already escapes everything non-alphanumeric (it includes all
// the reserved characters). So we only remove the unreserved characters from
// this set.
const QUERY_ESCAPE: &AsciiSet = &NON_ALPHANUMERIC
.remove(b'-')
.remove(b'_')
.remove(b'.')
.remove(b'~');

fn unescape(s: &str) -> Result<String, Error> {
percent_decode_str(s)
.decode_utf8()
.map_err(|e| Error::TagDecodingError(s.to_string(), e.to_string()))
.map(|s| s.to_string())
}

fn escape(s: &str) -> String {
utf8_percent_encode(s, QUERY_ESCAPE).collect()
}

// TODO: use this while adding API to set tags.
//
// Handles escaping same as MinIO server - needed for ensuring compatibility.
pub fn encode_tags(h: &HashMap<String, String>) -> String {
let mut tags = Vec::new();
for (k, v) in h {
tags.push(format!("{}={}", escape(k), escape(v)));
}
tags.join("&")
}

pub fn parse_tags(s: &str) -> Result<HashMap<String, String>, Error> {
let mut tags = HashMap::new();
for tag in s.split('&') {
let mut kv = tag.split('=');
let k = match kv.next() {
Some(v) => unescape(v)?,
None => {
return Err(Error::TagDecodingError(
s.to_string(),
"tag key was empty".to_string(),
))
}
};
let v = match kv.next() {
Some(v) => unescape(v)?,
None => "".to_owned(),
};
if kv.next().is_some() {
return Err(Error::TagDecodingError(
s.to_string(),
"tag had too many values for a key".to_string(),
));
}
tags.insert(k, v);
}
Ok(tags)
}

pub mod xml {
use std::collections::HashMap;

use crate::s3::error::Error;

#[derive(Debug, Clone)]
struct XmlElementIndex {
children: HashMap<String, Vec<usize>>,
}
Expand Down Expand Up @@ -432,6 +510,7 @@ pub mod xml {
}
}

#[derive(Debug, Clone)]
pub struct Element<'a> {
inner: &'a xmltree::Element,
child_element_index: XmlElementIndex,
Expand Down

0 comments on commit 961464d

Please sign in to comment.