Skip to content

Commit 8dc9498

Browse files
committed
fix: base64 encode meta data in order to support unicode
1 parent 886ced1 commit 8dc9498

File tree

5 files changed

+59
-9
lines changed

5 files changed

+59
-9
lines changed

docs/src/content/docs/reference/store-api.mdx

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Append frame to topic
3939

4040
```sh
4141
curl --unix-socket ./store/sock \
42-
-H "xs-meta: {\"key\":\"value\"}" \
42+
-H "xs-meta: $(echo -n '{\"key\":\"value\"}' | base64)" \
4343
-X POST --data "content" \
4444
"http://localhost/topic?ttl=forever"
4545
```
@@ -54,7 +54,7 @@ Query Parameters:
5454

5555
Headers:
5656

57-
- `xs-meta` - Optional JSON metadata
57+
- `xs-meta` - Optional Base64-encoded JSON metadata. Must be encoded using standard Base64 to support Unicode characters.
5858

5959
Response: Frame JSON
6060

src/api.rs

+18-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use std::str::FromStr;
44

55
use scru128::Scru128Id;
66

7+
use base64::Engine;
8+
79
use tokio::io::AsyncWriteExt;
810
use tokio_stream::wrappers::ReceiverStream;
911
use tokio_stream::StreamExt;
@@ -282,7 +284,22 @@ async fn handle_stream_append(
282284
.map(|x| x.to_str())
283285
.transpose()
284286
.unwrap()
285-
.map(|s| serde_json::from_str(s).map_err(|_| format!("xs-meta isn't valid JSON: {}", s)))
287+
.map(|s| {
288+
// First decode the Base64-encoded string
289+
base64::prelude::BASE64_STANDARD
290+
.decode(s)
291+
.map_err(|e| format!("xs-meta isn't valid Base64: {}", e))
292+
.and_then(|decoded| {
293+
// Then parse the decoded bytes as UTF-8 string
294+
String::from_utf8(decoded)
295+
.map_err(|e| format!("xs-meta isn't valid UTF-8: {}", e))
296+
.and_then(|json_str| {
297+
// Finally parse the UTF-8 string as JSON
298+
serde_json::from_str(&json_str)
299+
.map_err(|e| format!("xs-meta isn't valid JSON: {}", e))
300+
})
301+
})
302+
})
286303
.transpose()
287304
{
288305
Ok(meta) => meta,

src/client/commands.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
use futures::StreamExt;
2+
3+
use base64::Engine;
24
use ssri::Integrity;
5+
use url::form_urlencoded;
36

47
use http_body_util::{combinators::BoxBody, BodyExt, Empty, StreamBody};
58
use hyper::body::Bytes;
69
use hyper::Method;
710
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
811
use tokio::sync::mpsc::Receiver;
912
use tokio_util::io::ReaderStream;
10-
use url::form_urlencoded;
1113

1214
use super::request;
1315
use crate::store::{ReadOptions, TTL};
@@ -100,10 +102,9 @@ where
100102
let body = StreamBody::new(mapped_stream);
101103

102104
let headers = meta.map(|meta_value| {
103-
vec![(
104-
"xs-meta".to_string(),
105-
serde_json::to_string(meta_value).unwrap(),
106-
)]
105+
let json_string = serde_json::to_string(meta_value).unwrap();
106+
let encoded = base64::prelude::BASE64_STANDARD.encode(json_string);
107+
vec![("xs-meta".to_string(), encoded)]
107108
});
108109

109110
let res = request::request(addr, Method::POST, topic, query.as_deref(), body, headers).await?;

src/main.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
189189
Command::Version(args) => version(args).await,
190190
};
191191
if let Err(err) = res {
192-
eprintln!("{}", err);
192+
eprintln!("command error: {:?}", err);
193193
std::process::exit(1);
194194
}
195195
Ok(())

tests/integration.rs

+32
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,38 @@ async fn test_integration() {
145145
assert_eq!(frame.topic, "note");
146146
assert_eq!(frame.context_id.to_string(), context_id);
147147

148+
// assert unicode support
149+
let unicode_output = cmd!(
150+
cargo_bin("xs"),
151+
"append",
152+
store_path,
153+
"unicode",
154+
"--meta",
155+
r#"{"name": "Información"}"#
156+
)
157+
.stdin_bytes("contenido en español".as_bytes())
158+
.read()
159+
.unwrap();
160+
161+
let unicode_frame: Frame = serde_json::from_str(&unicode_output).unwrap();
162+
163+
// Verify it can be retrieved correctly
164+
let retrieved = cmd!(
165+
cargo_bin("xs"),
166+
"get",
167+
store_path,
168+
&unicode_frame.id.to_string()
169+
)
170+
.read()
171+
.unwrap();
172+
let retrieved_frame: Frame = serde_json::from_str(&retrieved).unwrap();
173+
174+
assert_eq!(retrieved_frame.topic, "unicode");
175+
assert_eq!(
176+
retrieved_frame.meta.unwrap().get("name").unwrap(),
177+
"Información"
178+
);
179+
148180
// Clean up
149181
child.kill().await.unwrap();
150182
}

0 commit comments

Comments
 (0)