|
| 1 | +use base64::Engine as _; |
1 | 2 | use reqwest::StatusCode; |
2 | 3 | use reqwest::blocking::Body; |
3 | 4 | use reqwest::header::{AUTHORIZATION, CONTENT_TYPE}; |
4 | 5 | use serde::Deserialize; |
| 6 | +use serde::Deserializer; |
| 7 | +use serde::de::Error as _; |
5 | 8 | use std::collections::BTreeMap; |
6 | 9 | use std::error; |
7 | 10 | use std::fmt; |
@@ -309,16 +312,74 @@ pub struct ApiTokenMetadata { |
309 | 312 | pub inserted_at: String, |
310 | 313 | } |
311 | 314 |
|
312 | | -#[derive(Debug, Clone, Deserialize)] |
| 315 | +#[derive(Debug, Clone)] |
313 | 316 | pub struct Paste { |
314 | | - pub data: String, |
| 317 | + pub data: Vec<u8>, |
| 318 | + pub content_type: String, |
315 | 319 | pub syntax_highlight: String, |
316 | 320 | pub visibility: String, |
| 321 | + pub is_text: bool, |
| 322 | +} |
| 323 | + |
| 324 | +impl Paste { |
| 325 | + pub fn text(&self) -> Option<&str> { |
| 326 | + if self.is_text && !self.data.contains(&0) { |
| 327 | + std::str::from_utf8(&self.data).ok() |
| 328 | + } else { |
| 329 | + None |
| 330 | + } |
| 331 | + } |
| 332 | +} |
| 333 | + |
| 334 | +impl<'de> Deserialize<'de> for Paste { |
| 335 | + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> |
| 336 | + where |
| 337 | + D: Deserializer<'de>, |
| 338 | + { |
| 339 | + let paste = PasteResponse::deserialize(deserializer)?; |
| 340 | + let (data, is_text) = match ( |
| 341 | + paste.data, |
| 342 | + paste.data_base64, |
| 343 | + paste.data_encoding.as_deref(), |
| 344 | + ) { |
| 345 | + (Some(data), None, None) => (data.into_bytes(), true), |
| 346 | + (None, Some(data), Some("base64")) => base64::engine::general_purpose::STANDARD |
| 347 | + .decode(data) |
| 348 | + .map(|data| (data, false)) |
| 349 | + .map_err(D::Error::custom)?, |
| 350 | + _ => return Err(D::Error::custom("invalid paste data representation")), |
| 351 | + }; |
| 352 | + |
| 353 | + Ok(Self { |
| 354 | + data, |
| 355 | + content_type: paste.content_type, |
| 356 | + syntax_highlight: paste.syntax_highlight, |
| 357 | + visibility: paste.visibility, |
| 358 | + is_text, |
| 359 | + }) |
| 360 | + } |
| 361 | +} |
| 362 | + |
| 363 | +#[derive(Deserialize)] |
| 364 | +struct PasteResponse { |
| 365 | + data: Option<String>, |
| 366 | + data_base64: Option<String>, |
| 367 | + data_encoding: Option<String>, |
| 368 | + #[serde(default = "default_content_type")] |
| 369 | + content_type: String, |
| 370 | + syntax_highlight: String, |
| 371 | + visibility: String, |
| 372 | +} |
| 373 | + |
| 374 | +fn default_content_type() -> String { |
| 375 | + "text/plain".to_string() |
317 | 376 | } |
318 | 377 |
|
319 | 378 | #[derive(Debug, Clone, Deserialize)] |
320 | 379 | pub struct CreatedPaste { |
321 | 380 | pub id: String, |
| 381 | + #[serde(default = "default_content_type")] |
| 382 | + pub content_type: String, |
322 | 383 | pub syntax_highlight: String, |
323 | 384 | pub visibility: String, |
324 | 385 | pub expires_at: Option<String>, |
@@ -559,6 +620,48 @@ mod tests { |
559 | 620 | assert_eq!(created.token.name, "Laptop"); |
560 | 621 | } |
561 | 622 |
|
| 623 | + #[test] |
| 624 | + fn decodes_text_and_binary_paste_representations() { |
| 625 | + let text = decode_json::<ShowResponse>( |
| 626 | + r#"{"data":{"data":"hello","content_type":"text/plain","syntax_highlight":"plain","visibility":"public"}}"#, |
| 627 | + ) |
| 628 | + .unwrap() |
| 629 | + .data; |
| 630 | + let binary = decode_json::<ShowResponse>( |
| 631 | + r#"{"data":{"data":null,"data_base64":"/wAB","data_encoding":"base64","content_type":"application/octet-stream","syntax_highlight":"plain","visibility":"public"}}"#, |
| 632 | + ) |
| 633 | + .unwrap() |
| 634 | + .data; |
| 635 | + |
| 636 | + assert_eq!(text.data, b"hello"); |
| 637 | + assert_eq!(text.text(), Some("hello")); |
| 638 | + assert_eq!(binary.data, [255, 0, 1]); |
| 639 | + assert_eq!(binary.text(), None); |
| 640 | + } |
| 641 | + |
| 642 | + #[test] |
| 643 | + fn base64_representation_remains_binary_for_utf8_shaped_bytes() { |
| 644 | + for encoded in ["aGVsbG8=", "AA=="] { |
| 645 | + let body = format!( |
| 646 | + r#"{{"data":{{"data":null,"data_base64":"{encoded}","data_encoding":"base64","content_type":"application/octet-stream","syntax_highlight":"plain","visibility":"public"}}}}"# |
| 647 | + ); |
| 648 | + let paste = decode_json::<ShowResponse>(&body).unwrap().data; |
| 649 | + |
| 650 | + assert_eq!(paste.text(), None); |
| 651 | + } |
| 652 | + } |
| 653 | + |
| 654 | + #[test] |
| 655 | + fn defaults_content_type_for_older_server_responses() { |
| 656 | + let paste = decode_json::<ShowResponse>( |
| 657 | + r#"{"data":{"data":"hello","syntax_highlight":"plain","visibility":"public"}}"#, |
| 658 | + ) |
| 659 | + .unwrap() |
| 660 | + .data; |
| 661 | + |
| 662 | + assert_eq!(paste.content_type, "text/plain"); |
| 663 | + } |
| 664 | + |
562 | 665 | #[test] |
563 | 666 | fn builds_canonical_paste_url() { |
564 | 667 | let client = Client::new("https://demo.textbin.com/"); |
|
0 commit comments