git.delta.rocks / jrsonnet / refs/commits / eee6c6261500

difftreelog

fix json string formatting

novypzkoYaroslav Bolyukin2026-04-25parent: #2b1a57e.patch.diff
in: master

1 file changed

modifiedcrates/jrsonnet-evaluator/src/manifest.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/manifest.rs
1use std::{borrow::Cow, fmt::Write, hint::black_box, ptr};23use crate::{Result, ResultExt, Val, bail, in_description_frame};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	/// When outputing to file, is it safe to append a trailing newline (I.e newline won't change13	/// the meaning).14	///15	/// Default implementation returns `true`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}4647pub struct BlackBoxFormat;48impl ManifestFormat for BlackBoxFormat {49	#[allow(clippy::only_used_in_recursion)]50	fn manifest_buf(&self, val: Val, buf: &mut String) -> Result<()> {51		match val {52			Val::Bool(v) => {53				black_box(v);54			}55			val @ Val::Null => {56				black_box(val);57			}58			Val::Str(str_value) => {59				black_box(format!("{str_value}"));60			}61			Val::Num(num_value) => {62				black_box(num_value);63			}64			Val::Arr(arr_value) => {65				for ele in arr_value.iter() {66					let ele = ele?;67					self.manifest_buf(ele, buf)?;68				}69			}70			Val::Obj(obj_value) => {71				for (name, value) in obj_value.iter(72					#[cfg(feature = "exp-preserve-order")]73					true,74				) {75					black_box(name);76					let value = value?;77					self.manifest_buf(value, buf)?;78				}79			}80			Val::Func(func_val) => {81				black_box(func_val);82				bail!("tried to manifest function")83			}84			#[cfg(feature = "exp-bigint")]85			Val::BigInt(n) => {86				black_box(n);87			}88		}89		Ok(())90	}91}9293#[derive(PartialEq, Eq, Clone, Copy)]94enum JsonFormatting {95	// Applied in manifestification96	Manifest,97	/// Used for std.manifestJson98	/// Empty array/objects extends to "[\n\n]" instead of "[ ]" as in manifest99	Std,100	/// No line breaks, used in `obj+''`101	ToString,102	/// Minified json103	Minify,104}105106pub struct JsonFormat<'s> {107	padding: Cow<'s, str>,108	mtype: JsonFormatting,109	newline: &'s str,110	key_val_sep: &'s str,111	#[cfg(feature = "exp-preserve-order")]112	preserve_order: bool,113	#[cfg(feature = "exp-bigint")]114	preserve_bigints: bool,115}116117impl<'s> JsonFormat<'s> {118	// Minifying format119	pub fn minify(#[cfg(feature = "exp-preserve-order")] preserve_order: bool) -> Self {120		Self {121			padding: Cow::Borrowed(""),122			mtype: JsonFormatting::Minify,123			newline: "\n",124			key_val_sep: ":",125			#[cfg(feature = "exp-preserve-order")]126			preserve_order,127			#[cfg(feature = "exp-bigint")]128			preserve_bigints: false,129		}130	}131	/// Same format as std.toString, except does not keeps top-level string as-is132	/// To avoid confusion, the format is private in jrsonnet, use [`ToStringFormat`] instead133	const fn std_to_string_helper() -> Self {134		Self {135			padding: Cow::Borrowed(""),136			mtype: JsonFormatting::ToString,137			newline: "\n",138			key_val_sep: ": ",139			#[cfg(feature = "exp-preserve-order")]140			preserve_order: false,141			#[cfg(feature = "exp-bigint")]142			preserve_bigints: false,143		}144	}145	pub fn std_to_json(146		padding: String,147		newline: &'s str,148		key_val_sep: &'s str,149		#[cfg(feature = "exp-preserve-order")] preserve_order: bool,150	) -> Self {151		Self {152			padding: Cow::Owned(padding),153			mtype: JsonFormatting::Std,154			newline,155			key_val_sep,156			#[cfg(feature = "exp-preserve-order")]157			preserve_order,158			#[cfg(feature = "exp-bigint")]159			preserve_bigints: false,160		}161	}162	// Same format as CLI manifestification163	pub fn cli(164		padding: usize,165		#[cfg(feature = "exp-preserve-order")] preserve_order: bool,166	) -> Self {167		if padding == 0 {168			return Self::minify(169				#[cfg(feature = "exp-preserve-order")]170				preserve_order,171			);172		}173		Self {174			padding: Cow::Owned(" ".repeat(padding)),175			mtype: JsonFormatting::Manifest,176			newline: "\n",177			key_val_sep: ": ",178			#[cfg(feature = "exp-preserve-order")]179			preserve_order,180			#[cfg(feature = "exp-bigint")]181			preserve_bigints: false,182		}183	}184	// Same format as CLI manifestification185	pub fn debug() -> Self {186		Self {187			padding: Cow::Borrowed("   "),188			mtype: JsonFormatting::Manifest,189			newline: "\n",190			key_val_sep: ": ",191			#[cfg(feature = "exp-preserve-order")]192			preserve_order: true,193			#[cfg(feature = "exp-bigint")]194			preserve_bigints: true,195		}196	}197}198impl Default for JsonFormat<'static> {199	fn default() -> Self {200		Self {201			padding: Cow::Borrowed("    "),202			mtype: JsonFormatting::Manifest,203			newline: "\n",204			key_val_sep: ": ",205			#[cfg(feature = "exp-preserve-order")]206			preserve_order: false,207			#[cfg(feature = "exp-bigint")]208			preserve_bigints: false,209		}210	}211}212213pub fn manifest_json_ex(val: &Val, options: &JsonFormat<'_>) -> Result<String> {214	let mut out = String::new();215	manifest_json_ex_buf(val, &mut out, &mut String::new(), options)?;216	Ok(out)217}218219#[allow(clippy::too_many_lines)]220fn manifest_json_ex_buf(221	val: &Val,222	buf: &mut String,223	cur_padding: &mut String,224	options: &JsonFormat<'_>,225) -> Result<()> {226	use JsonFormatting::*;227228	let mtype = options.mtype;229	match val {230		Val::Bool(v) => {231			if *v {232				buf.push_str("true");233			} else {234				buf.push_str("false");235			}236		}237		Val::Null => buf.push_str("null"),238		Val::Str(s) => {239			buf.reserve(2 + s.len());240			buf.push('"');241			s.chunks(&mut |c| {242				escape_string_json_buf_raw(c, buf);243			});244			buf.push('"');245		}246		Val::Num(n) => write!(buf, "{n}").unwrap(),247		#[cfg(feature = "exp-bigint")]248		Val::BigInt(n) => {249			if options.preserve_bigints {250				write!(buf, "{n}").unwrap();251			} else {252				write!(buf, "{:?}", n.to_string()).unwrap();253			}254		}255		Val::Arr(items) => {256			buf.push('[');257258			let old_len = cur_padding.len();259			cur_padding.push_str(&options.padding);260261			let mut had_items = false;262			for (i, item) in items.iter().enumerate() {263				had_items = true;264				let item = item.with_description(|| format!("elem <{i}> evaluation"))?;265266				if i != 0 {267					buf.push(',');268				}269				match mtype {270					Manifest | Std => {271						buf.push_str(options.newline);272						buf.push_str(cur_padding);273					}274					ToString if i != 0 => buf.push(' '),275					Minify | ToString => {}276				}277278				in_description_frame(279					|| format!("elem <{i}> manifestification"),280					|| manifest_json_ex_buf(&item, buf, cur_padding, options),281				)?;282			}283284			cur_padding.truncate(old_len);285286			match mtype {287				Manifest | ToString if !had_items => {288					// Empty array as "[ ]"289					buf.push(' ');290				}291				Manifest => {292					buf.push_str(options.newline);293					buf.push_str(cur_padding);294				}295				Std => {296					if !had_items {297						// Stdlib formats empty array as "[\n\n]"298						buf.push_str(options.newline);299					}300					buf.push_str(options.newline);301					buf.push_str(cur_padding);302				}303				Minify | ToString => {}304			}305306			buf.push(']');307		}308		Val::Obj(obj) => {309			obj.run_assertions()?;310			buf.push('{');311312			let old_len = cur_padding.len();313			cur_padding.push_str(&options.padding);314315			let mut had_fields = false;316			for (i, (key, value)) in obj317				.iter(318					#[cfg(feature = "exp-preserve-order")]319					options.preserve_order,320				)321				.enumerate()322			{323				had_fields = true;324				let value = value.with_description(|| format!("field <{key}> evaluation"))?;325326				if i != 0 {327					buf.push(',');328				}329				match mtype {330					Manifest | Std => {331						buf.push_str(options.newline);332						buf.push_str(cur_padding);333					}334					ToString if i != 0 => buf.push(' '),335					Minify | ToString => {}336				}337338				escape_string_json_buf(&key, buf);339				buf.push_str(options.key_val_sep);340				in_description_frame(341					|| format!("field <{key}> manifestification"),342					|| manifest_json_ex_buf(&value, buf, cur_padding, options),343				)?;344			}345346			cur_padding.truncate(old_len);347348			match mtype {349				Manifest | ToString if !had_fields => {350					// Empty object as "{ }"351					buf.push(' ');352				}353				Manifest => {354					buf.push_str(options.newline);355					buf.push_str(cur_padding);356				}357				Std => {358					if !had_fields {359						// Stdlib formats empty object as "{\n\n}"360						buf.push_str(options.newline);361					}362					buf.push_str(options.newline);363					buf.push_str(cur_padding);364				}365				Minify | ToString => {}366			}367368			buf.push('}');369		}370		Val::Func(_) => bail!("tried to manifest function"),371	}372	Ok(())373}374375impl ManifestFormat for JsonFormat<'_> {376	fn manifest_buf(&self, val: Val, buf: &mut String) -> Result<()> {377		manifest_json_ex_buf(&val, buf, &mut String::new(), self)378	}379}380381/// Same as [`JsonFormat`] with pre-set options, but top-level string is serialized as-is,382/// without quoting.383pub struct ToStringFormat;384impl ManifestFormat for ToStringFormat {385	fn manifest_buf(&self, val: Val, out: &mut String) -> Result<()> {386		const JSON_TO_STRING: JsonFormat = JsonFormat::std_to_string_helper();387		if let Some(str) = val.as_str() {388			out.push_str(&str);389			return Ok(());390		}391		#[cfg(feature = "exp-bigint")]392		if let Some(int) = val.as_bigint() {393			out.push_str(&int.to_str_radix(10));394			return Ok(());395		}396		JSON_TO_STRING.manifest_buf(val, out)397	}398	fn file_trailing_newline(&self) -> bool {399		false400	}401}402pub struct StringFormat;403impl ManifestFormat for StringFormat {404	fn manifest_buf(&self, val: Val, out: &mut String) -> Result<()> {405		let Val::Str(s) = val else {406			bail!(407				"output should be string for string manifest format, got {}",408				val.value_type()409			)410		};411		write!(out, "{s}").unwrap();412		Ok(())413	}414	fn file_trailing_newline(&self) -> bool {415		false416	}417}418419pub struct YamlStreamFormat<I> {420	inner: I,421	c_document_end: bool,422	end_newline: bool,423}424impl<I> YamlStreamFormat<I> {425	pub fn std_yaml_stream(inner: I, c_document_end: bool) -> Self {426		Self {427			inner,428			c_document_end,429			// Stdlib format always inserts useless newline at the end430			end_newline: true,431		}432	}433	pub fn cli(inner: I) -> Self {434		Self {435			inner,436			c_document_end: true,437			end_newline: false,438		}439	}440}441impl<I: ManifestFormat> ManifestFormat for YamlStreamFormat<I> {442	fn manifest_buf(&self, val: Val, out: &mut String) -> Result<()> {443		let Val::Arr(arr) = val else {444			bail!(445				"output should be array for yaml stream format, got {}",446				val.value_type()447			)448		};449		for (i, v) in arr.iter().enumerate() {450			if i != 0 {451				out.push('\n');452			}453			let v = v.with_description(|| format!("elem <{i}> evaluation"))?;454			out.push_str("---\n");455			in_description_frame(456				|| format!("elem <{i}> manifestification"),457				|| self.inner.manifest_buf(v, out),458			)?;459		}460		if self.c_document_end {461			out.push('\n');462			out.push_str("...");463		}464		if self.end_newline {465			out.push('\n');466		}467		Ok(())468	}469}470471pub fn escape_string_json(s: &str) -> String {472	let mut buf = String::new();473	escape_string_json_buf(s, &mut buf);474	buf475}476477// Json string encoding was borrowed from https://github.com/serde-rs/json478479const BB: u8 = b'b'; // \x08480const TT: u8 = b't'; // \x09481const NN: u8 = b'n'; // \x0A482const FF: u8 = b'f'; // \x0C483const RR: u8 = b'r'; // \x0D484const QU: u8 = b'"'; // \x22485const BS: u8 = b'\\'; // \x5C486const UU: u8 = b'u'; // \x00...\x1F except the ones above487const __: u8 = 0;488489// Lookup table of escape sequences. A value of b'x' at index i means that byte490// i is escaped as "\x" in JSON. A value of 0 means that byte i is not escaped.491static ESCAPE: [u8; 256] = [492	//   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F493	UU, UU, UU, UU, UU, UU, UU, UU, BB, TT, NN, UU, FF, RR, UU, UU, // 0494	UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, // 1495	__, __, QU, __, __, __, __, __, __, __, __, __, __, __, __, __, // 2496	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 3497	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 4498	__, __, __, __, __, __, __, __, __, __, __, __, BS, __, __, __, // 5499	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 6500	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 7501	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 8502	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 9503	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // A504	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // B505	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // C506	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // D507	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // E508	__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // F509];510511pub fn escape_string_json_buf(value: &str, buf: &mut String) {512	buf.reserve_exact(value.len() + 2);513	escape_string_json_buf_raw(value, buf);514}515516fn escape_string_json_buf_raw(value: &str, buf: &mut String) {517	// Safety: we only write correct utf-8 in this function518	let buf: &mut Vec<u8> = unsafe { &mut *ptr::from_mut(buf).cast::<Vec<u8>>() };519	let bytes = value.as_bytes();520521	let mut start = 0;522523	for (i, &byte) in bytes.iter().enumerate() {524		let escape = ESCAPE[byte as usize];525		if escape == __ {526			continue;527		}528529		if start < i {530			buf.extend_from_slice(&bytes[start..i]);531		}532		start = i + 1;533534		match escape {535			self::BB | self::TT | self::NN | self::FF | self::RR | self::QU | self::BS => {536				buf.extend_from_slice(&[b'\\', escape]);537			}538			self::UU => {539				static HEX_DIGITS: [u8; 16] = *b"0123456789abcdef";540				let bytes = &[541					b'\\',542					b'u',543					b'0',544					b'0',545					HEX_DIGITS[(byte >> 4) as usize],546					HEX_DIGITS[(byte & 0xF) as usize],547				];548				buf.extend_from_slice(bytes);549			}550			_ => unreachable!(),551		}552	}553554	if start == bytes.len() {555		return;556	}557558	buf.extend_from_slice(&bytes[start..]);559}