From 42baadf65d76a203f0a58d153a251f2b057722dc Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 5 Jan 2023 10:28:07 -0600 Subject: [PATCH 01/10] add fuzzing test to dioxus-core --- packages/core/Cargo.toml | 1 + packages/core/tests/fuzzing.rs | 280 +++++++++++++++++++++++++++++++++ 2 files changed, 281 insertions(+) create mode 100644 packages/core/tests/fuzzing.rs diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 8f9b71035..270e957d1 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -41,6 +41,7 @@ serde = { version = "1", features = ["derive"], optional = true } tokio = { version = "1", features = ["full"] } dioxus = { path = "../dioxus" } pretty_assertions = "1.3.0" +rand = "0.8.5" [features] default = [] diff --git a/packages/core/tests/fuzzing.rs b/packages/core/tests/fuzzing.rs new file mode 100644 index 000000000..aefd947ef --- /dev/null +++ b/packages/core/tests/fuzzing.rs @@ -0,0 +1,280 @@ +use dioxus::prelude::Props; +use dioxus_core::*; +use std::cell::Cell; + +fn random_ns() -> Option<&'static str> { + let namespace = rand::random::() % 2; + match namespace { + 0 => None, + 1 => Some(Box::leak( + format!("ns{}", rand::random::()).into_boxed_str(), + )), + _ => unreachable!(), + } +} + +fn create_random_attribute(attr_idx: &mut usize) -> TemplateAttribute<'static> { + match rand::random::() % 2 { + 0 => TemplateAttribute::Static { + name: Box::leak(format!("attr{}", rand::random::()).into_boxed_str()), + value: Box::leak(format!("value{}", rand::random::()).into_boxed_str()), + namespace: random_ns(), + }, + 1 => TemplateAttribute::Dynamic { + id: { + let old_idx = *attr_idx; + *attr_idx += 1; + old_idx + }, + }, + _ => unreachable!(), + } +} + +fn create_random_template_node( + template_idx: &mut usize, + attr_idx: &mut usize, + depth: usize, +) -> TemplateNode<'static> { + match rand::random::() % 3 { + 0 => { + let attrs = { + let attrs: Vec<_> = (0..(rand::random::() % 10)) + .map(|_| create_random_attribute(attr_idx)) + .collect(); + Box::leak(attrs.into_boxed_slice()) + }; + TemplateNode::Element { + tag: Box::leak(format!("tag{}", rand::random::()).into_boxed_str()), + namespace: random_ns(), + attrs, + children: { + if depth > 10 { + &[] + } else { + let children: Vec<_> = (0..(rand::random::() % 3)) + .map(|_| create_random_template_node(template_idx, attr_idx, depth + 1)) + .collect(); + Box::leak(children.into_boxed_slice()) + } + }, + } + } + 1 => TemplateNode::Text { + text: Box::leak(format!("{}", rand::random::()).into_boxed_str()), + }, + 2 => TemplateNode::DynamicText { + id: { + let old_idx = *template_idx; + *template_idx += 1; + old_idx + }, + }, + 2 => TemplateNode::Dynamic { + id: { + let old_idx = *template_idx; + *template_idx += 1; + old_idx + }, + }, + _ => unreachable!(), + } +} + +fn generate_paths( + node: &TemplateNode<'static>, + current_path: &Vec, + node_paths: &mut Vec>, + attr_paths: &mut Vec>, +) { + match node { + TemplateNode::Element { children, attrs, .. } => { + for attr in *attrs { + match attr { + TemplateAttribute::Static { .. } => {} + TemplateAttribute::Dynamic { .. } => { + attr_paths.push(current_path.clone()); + } + } + } + for (i, child) in children.iter().enumerate() { + let mut current_path = current_path.clone(); + current_path.push(i as u8); + generate_paths(child, ¤t_path, node_paths, attr_paths); + } + } + TemplateNode::Text { .. } => {} + TemplateNode::DynamicText { .. } => { + node_paths.push(current_path.clone()); + } + TemplateNode::Dynamic { .. } => { + node_paths.push(current_path.clone()); + } + } +} + +fn create_random_template(name: &'static str) -> Template<'static> { + let mut template_idx = 0; + let mut attr_idx = 0; + let roots = (0..(1 + rand::random::() % 5)) + .map(|_| create_random_template_node(&mut template_idx, &mut attr_idx, 0)) + .collect::>(); + assert!(roots.len() > 0); + let roots = Box::leak(roots.into_boxed_slice()); + let mut node_paths = Vec::new(); + let mut attr_paths = Vec::new(); + for (i, root) in roots.iter().enumerate() { + generate_paths(root, &vec![i as u8], &mut node_paths, &mut attr_paths); + } + let node_paths = Box::leak( + node_paths + .into_iter() + .map(|v| &*Box::leak(v.into_boxed_slice())) + .collect::>() + .into_boxed_slice(), + ); + let attr_paths = Box::leak( + attr_paths + .into_iter() + .map(|v| &*Box::leak(v.into_boxed_slice())) + .collect::>() + .into_boxed_slice(), + ); + Template { name, roots, node_paths, attr_paths } +} + +fn create_random_dynamic_node<'a>(cx: &'a ScopeState, depth: usize) -> DynamicNode<'a> { + let range = if depth > 10 { 2 } else { 4 }; + match rand::random::() % range { + 0 => DynamicNode::Text(VText { + value: Box::leak(format!("{}", rand::random::()).into_boxed_str()), + id: Default::default(), + }), + 1 => DynamicNode::Placeholder(Default::default()), + 2 => cx.make_node((0..(rand::random::() % 5)).map(|_| VNode { + key: None, + parent: Default::default(), + template: Cell::new(Template { + name: concat!(file!(), ":", line!(), ":", column!(), ":0"), + roots: &[TemplateNode::Dynamic { id: 0 }], + node_paths: &[&[0]], + attr_paths: &[], + }), + root_ids: Default::default(), + dynamic_nodes: cx.bump().alloc([cx.component( + create_random_element, + DepthProps { depth: depth }, + "create_random_element", + )]), + dynamic_attrs: &[], + })), + 3 => cx.component( + create_random_element, + DepthProps { depth: depth }, + "create_random_element", + ), + _ => unreachable!(), + } +} + +fn create_random_dynamic_attr<'a>(cx: &'a ScopeState) -> Attribute<'a> { + let value = match rand::random::() % 6 { + 0 => AttributeValue::Text(Box::leak( + format!("{}", rand::random::()).into_boxed_str(), + )), + 1 => AttributeValue::Float(rand::random()), + 2 => AttributeValue::Int(rand::random()), + 3 => AttributeValue::Bool(rand::random()), + 4 => cx.any_value(rand::random::()), + 5 => AttributeValue::None, + // Listener(RefCell>>), + _ => unreachable!(), + }; + Attribute { + name: Box::leak(format!("attr{}", rand::random::()).into_boxed_str()), + value, + namespace: random_ns(), + mounted_element: Default::default(), + volatile: rand::random(), + } +} + +#[test] +fn debugging() { + let template = create_random_template("test"); + println!("{:#?}", template) +} + +static mut TEMPLATE_COUNT: usize = 0; + +#[derive(PartialEq, Props)] +struct DepthProps { + depth: usize, +} + +fn create_random_element<'a>(cx: Scope<'a, DepthProps>) -> Element<'a> { + cx.needs_update(); + match rand::random::() % 3 { + 0 | 1 => { + // This is dynamicly created every render to trigger the pointer equality check on the template path + // In the future this check should be changed to something more obvious + let template = create_random_template(Box::leak( + format!( + "{}{}", + concat!(file!(), ":", line!(), ":", column!(), ":"), + { + unsafe { + let old = TEMPLATE_COUNT; + TEMPLATE_COUNT += 1; + old + } + } + ) + .into_boxed_str(), + )); + println!("{:#?}", template); + let node = VNode { + key: None, + parent: None, + template: Cell::new(template), + root_ids: Default::default(), + dynamic_nodes: { + let dynamic_nodes: Vec<_> = (0..template.node_paths.len()) + .map(|_| create_random_dynamic_node(cx, cx.props.depth + 1)) + .collect(); + cx.bump().alloc(dynamic_nodes) + }, + dynamic_attrs: cx.bump().alloc( + (0..template.attr_paths.len()) + .map(|_| create_random_dynamic_attr(cx)) + .collect::>(), + ), + }; + println!("{:#?}", node); + Some(node) + } + _ => None, + } +} + +// test for panics when creating random nodes and templates +#[test] +fn create() { + for _ in 0..1000 { + let mut vdom = VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0 }); + vdom.rebuild(); + } +} + +// test for panics when diffing random nodes +// This test will change the template every render which is not very realistic, but it helps stress the system +#[test] +fn diff() { + for _ in 0..100 { + let mut vdom = VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0 }); + vdom.rebuild(); + for _ in 0..10 { + vdom.render_immediate(); + } + } +} From 990b0503e1257983697ea1dadfe2beae1053f6fb Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 5 Jan 2023 10:36:30 -0600 Subject: [PATCH 02/10] remove outdated comment --- packages/core/tests/fuzzing.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/tests/fuzzing.rs b/packages/core/tests/fuzzing.rs index aefd947ef..5a027ea8a 100644 --- a/packages/core/tests/fuzzing.rs +++ b/packages/core/tests/fuzzing.rs @@ -216,8 +216,6 @@ fn create_random_element<'a>(cx: Scope<'a, DepthProps>) -> Element<'a> { cx.needs_update(); match rand::random::() % 3 { 0 | 1 => { - // This is dynamicly created every render to trigger the pointer equality check on the template path - // In the future this check should be changed to something more obvious let template = create_random_template(Box::leak( format!( "{}{}", From 112738c23d92a0348ac268508b93fc11ca6ab56d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 7 Jan 2023 18:52:37 -0600 Subject: [PATCH 03/10] improve fuzzing test --- packages/core/tests/fuzzing.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/core/tests/fuzzing.rs b/packages/core/tests/fuzzing.rs index 5a027ea8a..e7e67403f 100644 --- a/packages/core/tests/fuzzing.rs +++ b/packages/core/tests/fuzzing.rs @@ -163,14 +163,14 @@ fn create_random_dynamic_node<'a>(cx: &'a ScopeState, depth: usize) -> DynamicNo root_ids: Default::default(), dynamic_nodes: cx.bump().alloc([cx.component( create_random_element, - DepthProps { depth: depth }, + DepthProps { depth, root: false }, "create_random_element", )]), dynamic_attrs: &[], })), 3 => cx.component( create_random_element, - DepthProps { depth: depth }, + DepthProps { depth, root: false }, "create_random_element", ), _ => unreachable!(), @@ -199,22 +199,18 @@ fn create_random_dynamic_attr<'a>(cx: &'a ScopeState) -> Attribute<'a> { } } -#[test] -fn debugging() { - let template = create_random_template("test"); - println!("{:#?}", template) -} - static mut TEMPLATE_COUNT: usize = 0; #[derive(PartialEq, Props)] struct DepthProps { depth: usize, + root: bool, } fn create_random_element<'a>(cx: Scope<'a, DepthProps>) -> Element<'a> { cx.needs_update(); - match rand::random::() % 3 { + let range = if cx.props.root { 2 } else { 3 }; + let node = match rand::random::() % range { 0 | 1 => { let template = create_random_template(Box::leak( format!( @@ -248,18 +244,20 @@ fn create_random_element<'a>(cx: Scope<'a, DepthProps>) -> Element<'a> { .collect::>(), ), }; - println!("{:#?}", node); Some(node) } _ => None, - } + }; + println!("{:#?}", node); + node } // test for panics when creating random nodes and templates #[test] fn create() { for _ in 0..1000 { - let mut vdom = VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0 }); + let mut vdom = + VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true }); vdom.rebuild(); } } @@ -269,7 +267,8 @@ fn create() { #[test] fn diff() { for _ in 0..100 { - let mut vdom = VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0 }); + let mut vdom = + VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true }); vdom.rebuild(); for _ in 0..10 { vdom.render_immediate(); From 927db9723ab9fdbecb7757208ee1607331022cb0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 7 Jan 2023 19:01:29 -0600 Subject: [PATCH 04/10] handle abort diffing --- packages/core/src/arena.rs | 8 ++++++++ packages/core/src/diff.rs | 29 ++++++++++++++++++++++------- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index a11fc69ba..4ffa5fe07 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -44,6 +44,14 @@ impl VirtualDom { self.next_reference(template, ElementPath::Root(path)) } + pub(crate) fn next_null(&mut self) -> ElementId { + let entry = self.elements.vacant_entry(); + let id = entry.key(); + + entry.insert(ElementRef::null()); + ElementId(id) + } + fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId { let entry = self.elements.vacant_entry(); let id = entry.key(); diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 7227ee6e7..4617919cf 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -42,7 +42,7 @@ impl<'b> VirtualDom { // Just move over the placeholder (Aborted(l), Aborted(r)) => r.id.set(l.id.get()), - // Becomes async, do nothing while we ait + // Becomes async, do nothing while we wait (Ready(_nodes), Pending(_fut)) => self.diff_ok_to_async(_nodes, scope), // Placeholder becomes something @@ -65,8 +65,26 @@ impl<'b> VirtualDom { // } - fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _p: &'b VPlaceholder) { - todo!() + fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) { + let id = self.next_null(); + p.id.set(Some(id)); + self.mutations.push(Mutation::CreatePlaceholder { id }); + + let pre_edits = self.mutations.edits.len(); + + self.remove_node(l, true); + + // We should always have a remove mutation + // Eventually we don't want to generate placeholders, so this might not be true. But it's true today + assert!(self.mutations.edits.len() > pre_edits); + + // We want to optimize the replace case to use one less mutation if possible + // Since mutations are done in reverse, the last node removed will be the first in the stack + // Instead of *just* removing it, we can use the replace mutation + match self.mutations.edits.pop().unwrap() { + Mutation::Remove { id } => self.mutations.push(Mutation::ReplaceWith { id, m: 1 }), + _ => panic!("Expected remove mutation from remove_node"), + }; } fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { @@ -905,10 +923,7 @@ impl<'b> VirtualDom { match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { RenderReturn::Ready(t) => self.remove_node(t, gen_muts), - RenderReturn::Aborted(t) => { - if let Some(id) = t.id.get() { - self.try_reclaim(id); - } + RenderReturn::Aborted(_) => { return; } _ => todo!(), From 232b49a3be79cb2ce6cddbcd52e24298031d0626 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 12 Jan 2023 11:21:34 -0600 Subject: [PATCH 05/10] change some upper camel case to snake case and fix some incorrect attributes --- packages/html/src/global_attributes.rs | 124 ++++++++++++------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index b85cb8fa1..3792397ad 100644 --- a/packages/html/src/global_attributes.rs +++ b/packages/html/src/global_attributes.rs @@ -1622,22 +1622,22 @@ trait_methods! { ascent: "ascent"; /// - attributeName: "attributeName"; + attribute_name: "attributeName"; /// - attributeType: "attributeType"; + attribute_type: "attributeType"; /// azimuth: "azimuth"; /// - baseFrequency: "baseFrequency"; + base_frequency: "baseFrequency"; /// baseline_shift: "baseline-shift"; /// - baseProfile: "baseProfile"; + base_profile: "baseProfile"; /// bbox: "bbox"; @@ -1652,7 +1652,7 @@ trait_methods! { by: "by"; /// - calcMode: "calcMode"; + calc_mode: "calcMode"; /// cap_height: "cap-height"; @@ -1664,7 +1664,7 @@ trait_methods! { clip: "clip"; /// - clipPathUnits: "clipPathUnits"; + clip_path_units: "clipPathUnits"; /// clip_path: "clip-path"; @@ -1688,10 +1688,10 @@ trait_methods! { color_rendering: "color-rendering"; /// - contentScriptType: "contentScriptType"; + content_script_type: "contentScriptType"; /// - contentStyleType: "contentStyleType"; + content_style_type: "contentStyleType"; /// crossorigin: "crossorigin"; @@ -1715,7 +1715,7 @@ trait_methods! { descent: "descent"; /// - diffuseConstant: "diffuseConstant"; + diffuse_constant: "diffuseConstant"; /// direction: "direction"; @@ -1739,7 +1739,7 @@ trait_methods! { dy: "dy"; /// - edgeMode: "edgeMode"; + edge_mode: "edgeMode"; /// elevation: "elevation"; @@ -1829,13 +1829,13 @@ trait_methods! { glyph_orientation_vertical: "glyph-orientation-vertical"; /// - glyphRef: "glyphRef"; + glyph_ref: "glyphRef"; /// - gradientTransform: "gradientTransform"; + gradient_transform: "gradientTransform"; /// - gradientUnits: "gradientUnits"; + gradient_units: "gradientUnits"; /// hanging: "hanging"; @@ -1889,28 +1889,28 @@ trait_methods! { k4: "k4"; /// - kernelMatrix: "kernelMatrix"; + kernel_matrix: "kernelMatrix"; /// - kernelUnitLength: "kernelUnitLength"; + kernel_unit_length: "kernelUnitLength"; /// kerning: "kerning"; /// - keyPoints: "keyPoints"; + key_points: "keyPoints"; /// - keySplines: "keySplines"; + key_splines: "keySplines"; /// - keyTimes: "keyTimes"; + key_times: "keyTimes"; /// lang: "lang"; /// - lengthAdjust: "lengthAdjust"; + length_adjust: "lengthAdjust"; /// letter_spacing: "letter-spacing"; @@ -1919,7 +1919,7 @@ trait_methods! { lighting_color: "lighting-color"; /// - limitingConeAngle: "limitingConeAngle"; + limiting_cone_angle: "limitingConeAngle"; /// local: "local"; @@ -1930,26 +1930,26 @@ trait_methods! { /// marker_mid: "marker-mid"; - /// - marker_start: "marker_start"; + /// + marker_start: "marker-start"; /// - markerHeight: "markerHeight"; + marker_height: "markerHeight"; /// - markerUnits: "markerUnits"; + marker_units: "markerUnits"; /// - markerWidth: "markerWidth"; + marker_width: "markerWidth"; /// mask: "mask"; /// - maskContentUnits: "maskContentUnits"; + mask_content_units: "maskContentUnits"; /// - maskUnits: "maskUnits"; + mask_units: "maskUnits"; /// mathematical: "mathematical"; @@ -1973,7 +1973,7 @@ trait_methods! { name: "name"; /// - numOctaves: "numOctaves"; + num_octaves: "numOctaves"; /// offset: "offset"; @@ -2015,16 +2015,16 @@ trait_methods! { path: "path"; /// - pathLength: "pathLength"; + path_length: "pathLength"; /// - patternContentUnits: "patternContentUnits"; + pattern_content_units: "patternContentUnits"; /// - patternTransform: "patternTransform"; + pattern_transform: "patternTransform"; /// - patternUnits: "patternUnits"; + pattern_units: "patternUnits"; /// ping: "ping"; @@ -2036,22 +2036,22 @@ trait_methods! { points: "points"; /// - pointsAtX: "pointsAtX"; + points_at_x: "pointsAtX"; /// - pointsAtY: "pointsAtY"; + points_at_y: "pointsAtY"; /// - pointsAtZ: "pointsAtZ"; + points_at_z: "pointsAtZ"; /// - preserveAlpha: "preserveAlpha"; + preserve_alpha: "preserveAlpha"; /// - preserveAspectRatio: "preserveAspectRatio"; + preserve_aspect_ratio: "preserveAspectRatio"; /// - primitiveUnits: "primitiveUnits"; + primitive_units: "primitiveUnits"; /// r: "r"; @@ -2060,13 +2060,13 @@ trait_methods! { radius: "radius"; /// - referrerPolicy: "referrerPolicy"; + referrer_policy: "referrerPolicy"; /// - refX: "refX"; + ref_x: "refX"; /// - refY: "refY"; + ref_y: "refY"; /// rel: "rel"; @@ -2075,16 +2075,16 @@ trait_methods! { rendering_intent: "rendering-intent"; /// - repeatCount: "repeatCount"; + repeat_count: "repeatCount"; /// - repeatDur: "repeatDur"; + repeat_dur: "repeatDur"; /// - requiredExtensions: "requiredExtensions"; + required_extensions: "requiredExtensions"; /// - requiredFeatures: "requiredFeatures"; + required_features: "requiredFeatures"; /// restart: "restart"; @@ -2120,22 +2120,22 @@ trait_methods! { spacing: "spacing"; /// - specularConstant: "specularConstant"; + specular_constant: "specularConstant"; /// - specularExponent: "specularExponent"; + specular_exponent: "specularExponent"; /// speed: "speed"; /// - spreadMethod: "spreadMethod"; + spread_method: "spreadMethod"; /// - startOffset: "startOffset"; + start_offset: "startOffset"; /// - stdDeviation: "stdDeviation"; + std_deviation: "stdDeviation"; /// stemh: "stemh"; @@ -2144,13 +2144,13 @@ trait_methods! { stemv: "stemv"; /// - stitchTiles: "stitchTiles"; + stitch_tiles: "stitchTiles"; - /// - stop_color: "stop_color"; + /// + stop_color: "stop-color"; - /// - stop_opacity: "stop_opacity"; + /// + stop_opacity: "stop-opacity"; /// strikethrough_position: "strikethrough-position"; @@ -2189,25 +2189,25 @@ trait_methods! { style: "style"; /// - surfaceScale: "surfaceScale"; + surface_scale: "surfaceScale"; /// - systemLanguage: "systemLanguage"; + system_language: "systemLanguage"; /// tabindex: "tabindex"; /// - tableValues: "tableValues"; + table_values: "tableValues"; /// target: "target"; /// - targetX: "targetX"; + target_x: "targetX"; /// - targetY: "targetY"; + target_y: "targetY"; /// text_anchor: "text-anchor"; @@ -2219,7 +2219,7 @@ trait_methods! { text_rendering: "text-rendering"; /// - textLength: "textLength"; + text_length: "textLength"; /// to: "to"; @@ -2342,6 +2342,6 @@ trait_methods! { z: "z"; /// - zoomAndPan: "zoomAndPan"; + zoom_and_pan: "zoomAndPan"; } From d8d483a8b4baf60128beab32ee169659d0108802 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 12 Jan 2023 11:31:21 -0600 Subject: [PATCH 06/10] apply the same fixes to element specific attributes --- packages/html/src/elements.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 6cf60c6ea..3fc400ea9 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -294,7 +294,7 @@ builder_constructors! { rel: LinkType DEFAULT, sizes: String DEFAULT, // FIXME title: String DEFAULT, // FIXME - r#type: Mime DEFAULT, + r#type: Mime "type", integrity: String DEFAULT, }; @@ -312,7 +312,7 @@ builder_constructors! { /// [`