1use std::{borrow::Cow, fmt::Write, ptr};23use crate::{bail, in_description_frame, Result, ResultExt, Val};45pub trait ManifestFormat {6 fn manifest_buf(&self, val: Val, buf: &mut String) -> Result<()>;7 fn manifest(&self, val: Val) -> Result<String> {8 let mut out = String::new();9 self.manifest_buf(val, &mut out)?;10 Ok(out)11 }12 13 14 15 16 fn file_trailing_newline(&self) -> bool {17 true18 }19}20impl<T> ManifestFormat for Box<T>21where22 T: ManifestFormat + ?Sized,23{24 fn manifest_buf(&self, val: Val, buf: &mut String) -> Result<()> {25 let inner = &**self;26 inner.manifest_buf(val, buf)27 }28 fn file_trailing_newline(&self) -> bool {29 let inner = &**self;30 inner.file_trailing_newline()31 }32}33impl<T> ManifestFormat for &'_ T34where35 T: ManifestFormat + ?Sized,36{37 fn manifest_buf(&self, val: Val, buf: &mut String) -> Result<()> {38 let inner = &**self;39 inner.manifest_buf(val, buf)40 }41 fn file_trailing_newline(&self) -> bool {42 let inner = &**self;43 inner.file_trailing_newline()44 }45}4647#[derive(PartialEq, Eq, Clone, Copy)]48enum JsonFormatting {49 50 Manifest,51 52 53 Std,54 55 ToString,56 57 Minify,58}5960pub struct JsonFormat<'s> {61 padding: Cow<'s, str>,62 mtype: JsonFormatting,63 newline: &'s str,64 key_val_sep: &'s str,65 #[cfg(feature = "exp-preserve-order")]66 preserve_order: bool,67 #[cfg(feature = "exp-bigint")]68 preserve_bigints: bool,69 debug_truncate_strings: Option<usize>,70}7172impl<'s> JsonFormat<'s> {73 74 pub fn minify(#[cfg(feature = "exp-preserve-order")] preserve_order: bool) -> Self {75 Self {76 padding: Cow::Borrowed(""),77 mtype: JsonFormatting::Minify,78 newline: "\n",79 key_val_sep: ":",80 #[cfg(feature = "exp-preserve-order")]81 preserve_order,82 #[cfg(feature = "exp-bigint")]83 preserve_bigints: false,84 debug_truncate_strings: None,85 }86 }87 88 89 const fn std_to_string_helper() -> Self {90 Self {91 padding: Cow::Borrowed(""),92 mtype: JsonFormatting::ToString,93 newline: "\n",94 key_val_sep: ": ",95 #[cfg(feature = "exp-preserve-order")]96 preserve_order: false,97 #[cfg(feature = "exp-bigint")]98 preserve_bigints: false,99 debug_truncate_strings: None,100 }101 }102 pub fn std_to_json(103 padding: String,104 newline: &'s str,105 key_val_sep: &'s str,106 #[cfg(feature = "exp-preserve-order")] preserve_order: bool,107 ) -> Self {108 Self {109 padding: Cow::Owned(padding),110 mtype: JsonFormatting::Std,111 newline,112 key_val_sep,113 #[cfg(feature = "exp-preserve-order")]114 preserve_order,115 #[cfg(feature = "exp-bigint")]116 preserve_bigints: false,117 debug_truncate_strings: None,118 }119 }120 121 pub fn cli(122 padding: usize,123 #[cfg(feature = "exp-preserve-order")] preserve_order: bool,124 ) -> Self {125 if padding == 0 {126 return Self::minify(127 #[cfg(feature = "exp-preserve-order")]128 preserve_order,129 );130 }131 Self {132 padding: Cow::Owned(" ".repeat(padding)),133 mtype: JsonFormatting::Manifest,134 newline: "\n",135 key_val_sep: ": ",136 #[cfg(feature = "exp-preserve-order")]137 preserve_order,138 #[cfg(feature = "exp-bigint")]139 preserve_bigints: false,140 debug_truncate_strings: None,141 }142 }143 144 pub fn debug() -> Self {145 Self {146 padding: Cow::Borrowed(" "),147 mtype: JsonFormatting::Manifest,148 newline: "\n",149 key_val_sep: ": ",150 #[cfg(feature = "exp-preserve-order")]151 preserve_order: true,152 #[cfg(feature = "exp-bigint")]153 preserve_bigints: true,154 debug_truncate_strings: Some(256),155 }156 }157}158impl Default for JsonFormat<'static> {159 fn default() -> Self {160 Self {161 padding: Cow::Borrowed(" "),162 mtype: JsonFormatting::Manifest,163 newline: "\n",164 key_val_sep: ": ",165 #[cfg(feature = "exp-preserve-order")]166 preserve_order: false,167 #[cfg(feature = "exp-bigint")]168 preserve_bigints: false,169 debug_truncate_strings: None,170 }171 }172}173174pub fn manifest_json_ex(val: &Val, options: &JsonFormat<'_>) -> Result<String> {175 let mut out = String::new();176 manifest_json_ex_buf(val, &mut out, &mut String::new(), options)?;177 Ok(out)178}179180#[allow(clippy::too_many_lines)]181fn manifest_json_ex_buf(182 val: &Val,183 buf: &mut String,184 cur_padding: &mut String,185 options: &JsonFormat<'_>,186) -> Result<()> {187 use JsonFormatting::*;188189 let mtype = options.mtype;190 match val {191 Val::Bool(v) => {192 if *v {193 buf.push_str("true");194 } else {195 buf.push_str("false");196 }197 }198 Val::Null => buf.push_str("null"),199 Val::Str(s) => {200 let flat = s.clone().into_flat();201 if let Some(truncate) = options.debug_truncate_strings {202 if flat.len() > truncate {203 let (start, end) = flat.split_at(truncate / 2);204 let (_, end) = end.split_at(end.len() - truncate / 2);205 escape_string_json_buf(&format!("{start}..{end}"), buf);206 } else {207 escape_string_json_buf(&flat, buf);208 }209 } else {210 escape_string_json_buf(&flat, buf);211 }212 }213 Val::Num(n) => write!(buf, "{n}").unwrap(),214 #[cfg(feature = "exp-bigint")]215 Val::BigInt(n) => {216 if options.preserve_bigints {217 write!(buf, "{n}").unwrap();218 } else {219 write!(buf, "{:?}", n.to_string()).unwrap();220 }221 }222 Val::Arr(items) => {223 buf.push('[');224225 let old_len = cur_padding.len();226 cur_padding.push_str(&options.padding);227228 let mut had_items = false;229 for (i, item) in items.iter().enumerate() {230 had_items = true;231 let item = item.with_description(|| format!("elem <{i}> evaluation"))?;232233 if i != 0 {234 buf.push(',');235 }236 match mtype {237 Manifest | Std => {238 buf.push_str(options.newline);239 buf.push_str(cur_padding);240 }241 ToString if i != 0 => buf.push(' '),242 Minify | ToString => {}243 };244245 in_description_frame(246 || format!("elem <{i}> manifestification"),247 || manifest_json_ex_buf(&item, buf, cur_padding, options),248 )?;249 }250251 cur_padding.truncate(old_len);252253 match mtype {254 Manifest | ToString if !had_items => {255 256 buf.push(' ');257 }258 Manifest => {259 buf.push_str(options.newline);260 buf.push_str(cur_padding);261 }262 Std => {263 if !had_items {264 265 buf.push_str(options.newline);266 }267 buf.push_str(options.newline);268 buf.push_str(cur_padding);269 }270 Minify | ToString => {}271 }272273 buf.push(']');274 }275 Val::Obj(obj) => {276 obj.run_assertions()?;277 buf.push('{');278279 let old_len = cur_padding.len();280 cur_padding.push_str(&options.padding);281282 let mut had_fields = false;283 for (i, (key, value)) in obj284 .iter(285 #[cfg(feature = "exp-preserve-order")]286 options.preserve_order,287 )288 .enumerate()289 {290 had_fields = true;291 let value = value.with_description(|| format!("field <{key}> evaluation"))?;292293 if i != 0 {294 buf.push(',');295 }296 match mtype {297 Manifest | Std => {298 buf.push_str(options.newline);299 buf.push_str(cur_padding);300 }301 ToString if i != 0 => buf.push(' '),302 Minify | ToString => {}303 }304305 escape_string_json_buf(&key, buf);306 buf.push_str(options.key_val_sep);307 in_description_frame(308 || format!("field <{key}> manifestification"),309 || manifest_json_ex_buf(&value, buf, cur_padding, options),310 )?;311 }312313 cur_padding.truncate(old_len);314315 match mtype {316 Manifest | ToString if !had_fields => {317 318 buf.push(' ');319 }320 Manifest => {321 buf.push_str(options.newline);322 buf.push_str(cur_padding);323 }324 Std => {325 if !had_fields {326 327 buf.push_str(options.newline);328 }329 buf.push_str(options.newline);330 buf.push_str(cur_padding);331 }332 Minify | ToString => {}333 }334335 buf.push('}');336 }337 Val::Func(_) => bail!("tried to manifest function"),338 };339 Ok(())340}341342impl ManifestFormat for JsonFormat<'_> {343 fn manifest_buf(&self, val: Val, buf: &mut String) -> Result<()> {344 manifest_json_ex_buf(&val, buf, &mut String::new(), self)345 }346}347348349350pub struct ToStringFormat;351impl ManifestFormat for ToStringFormat {352 fn manifest_buf(&self, val: Val, out: &mut String) -> Result<()> {353 const JSON_TO_STRING: JsonFormat = JsonFormat::std_to_string_helper();354 if let Some(str) = val.as_str() {355 out.push_str(&str);356 return Ok(());357 }358 #[cfg(feature = "exp-bigint")]359 if let Some(int) = val.as_bigint() {360 out.push_str(&int.to_str_radix(10));361 return Ok(());362 }363 JSON_TO_STRING.manifest_buf(val, out)364 }365 fn file_trailing_newline(&self) -> bool {366 false367 }368}369pub struct StringFormat;370impl ManifestFormat for StringFormat {371 fn manifest_buf(&self, val: Val, out: &mut String) -> Result<()> {372 let Val::Str(s) = val else {373 bail!(374 "output should be string for string manifest format, got {}",375 val.value_type()376 )377 };378 write!(out, "{s}").unwrap();379 Ok(())380 }381 fn file_trailing_newline(&self) -> bool {382 false383 }384}385386pub struct YamlStreamFormat<I> {387 inner: I,388 c_document_end: bool,389 end_newline: bool,390}391impl<I> YamlStreamFormat<I> {392 pub fn std_yaml_stream(inner: I, c_document_end: bool) -> Self {393 Self {394 inner,395 c_document_end,396 397 end_newline: true,398 }399 }400 pub fn cli(inner: I) -> Self {401 Self {402 inner,403 c_document_end: true,404 end_newline: false,405 }406 }407}408impl<I: ManifestFormat> ManifestFormat for YamlStreamFormat<I> {409 fn manifest_buf(&self, val: Val, out: &mut String) -> Result<()> {410 let Val::Arr(arr) = val else {411 bail!(412 "output should be array for yaml stream format, got {}",413 val.value_type()414 )415 };416 if !arr.is_empty() {417 for (i, v) in arr.iter().enumerate() {418 let v = v.with_description(|| format!("elem <{i}> evaluation"))?;419 out.push_str("---\n");420 in_description_frame(421 || format!("elem <{i}> manifestification"),422 || self.inner.manifest_buf(v, out),423 )?;424 out.push('\n');425 }426 }427 if self.c_document_end {428 out.push_str("...");429 }430 if self.end_newline {431 out.push('\n');432 }433 Ok(())434 }435}436437pub fn escape_string_json(s: &str) -> String {438 let mut buf = String::new();439 escape_string_json_buf(s, &mut buf);440 buf441}442443444445const BB: u8 = b'b'; 446const TT: u8 = b't'; 447const NN: u8 = b'n'; 448const FF: u8 = b'f'; 449const RR: u8 = b'r'; 450const QU: u8 = b'"'; 451const BS: u8 = b'\\'; 452const UU: u8 = b'u'; 453const __: u8 = 0;454455456457static ESCAPE: [u8; 256] = [458 459 UU, UU, UU, UU, UU, UU, UU, UU, BB, TT, NN, UU, FF, RR, UU, UU, 460 UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, 461 __, __, QU, __, __, __, __, __, __, __, __, __, __, __, __, __, 462 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 463 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 464 __, __, __, __, __, __, __, __, __, __, __, __, BS, __, __, __, 465 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 466 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 467 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 468 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 469 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 470 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 471 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 472 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 473 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 474 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 475];476477pub fn escape_string_json_buf(value: &str, buf: &mut String) {478 479 let buf: &mut Vec<u8> = unsafe { &mut *ptr::from_mut(buf).cast::<Vec<u8>>() };480 let bytes = value.as_bytes();481482 483 buf.reserve(value.len() + 2);484485 buf.push(b'"');486487 let mut start = 0;488489 for (i, &byte) in bytes.iter().enumerate() {490 let escape = ESCAPE[byte as usize];491 if escape == __ {492 continue;493 }494495 if start < i {496 buf.extend_from_slice(&bytes[start..i]);497 }498 start = i + 1;499500 match escape {501 self::BB | self::TT | self::NN | self::FF | self::RR | self::QU | self::BS => {502 buf.extend_from_slice(&[b'\\', escape]);503 }504 self::UU => {505 static HEX_DIGITS: [u8; 16] = *b"0123456789abcdef";506 let bytes = &[507 b'\\',508 b'u',509 b'0',510 b'0',511 HEX_DIGITS[(byte >> 4) as usize],512 HEX_DIGITS[(byte & 0xF) as usize],513 ];514 buf.extend_from_slice(bytes);515 }516 _ => unreachable!(),517 }518 }519520 if start == bytes.len() {521 buf.push(b'"');522 return;523 }524525 buf.extend_from_slice(&bytes[start..]);526 buf.push(b'"');527}