1use serde::{Deserialize, Serialize, Serializer};
5use serde_json::Map;
6
7use super::author::OutputAuthor;
8use super::copyright::OutputCopyright;
9use super::email::OutputEmail;
10use super::file_type::OutputFileType;
11use super::holder::OutputHolder;
12use super::license_detection::OutputLicenseDetection;
13use super::license_match::OutputMatch;
14use super::license_policy_entry::OutputLicensePolicyEntry;
15use super::package_data::OutputPackageData;
16use super::serde_helpers::insert_json;
17use super::tallies::OutputTallies;
18use super::url::OutputURL;
19
20#[derive(Debug, Clone, Deserialize)]
21pub struct OutputFileInfo {
22 #[serde(default)]
23 pub name: String,
24 #[serde(default)]
25 pub base_name: String,
26 #[serde(default)]
27 pub extension: String,
28 pub path: String,
29 #[serde(rename = "type")]
30 pub file_type: OutputFileType,
31 pub mime_type: Option<String>,
32 #[serde(rename = "file_type")]
33 pub file_type_label: Option<String>,
34 #[serde(default)]
35 pub size: u64,
36 pub date: Option<String>,
37 pub sha1: Option<String>,
38 pub md5: Option<String>,
39 pub sha256: Option<String>,
40 pub sha1_git: Option<String>,
41 pub programming_language: Option<String>,
42 #[serde(default)]
43 pub package_data: Vec<OutputPackageData>,
44 #[serde(rename = "detected_license_expression_spdx")]
45 pub license_expression: Option<String>,
46 #[serde(default)]
47 pub license_detections: Vec<OutputLicenseDetection>,
48 #[serde(default, skip_serializing_if = "Vec::is_empty")]
49 pub license_clues: Vec<OutputMatch>,
50 pub percentage_of_license_text: Option<f64>,
51 #[serde(default)]
52 pub copyrights: Vec<OutputCopyright>,
53 #[serde(default)]
54 pub holders: Vec<OutputHolder>,
55 #[serde(default)]
56 pub authors: Vec<OutputAuthor>,
57 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 pub emails: Vec<OutputEmail>,
59 #[serde(default)]
60 pub urls: Vec<OutputURL>,
61 #[serde(default)]
62 pub for_packages: Vec<String>,
63 #[serde(default)]
64 pub scan_errors: Vec<String>,
65 pub license_policy: Option<Vec<OutputLicensePolicyEntry>>,
66 pub is_generated: Option<bool>,
67 pub is_binary: Option<bool>,
68 pub is_text: Option<bool>,
69 pub is_archive: Option<bool>,
70 pub is_media: Option<bool>,
71 pub is_source: Option<bool>,
72 pub is_script: Option<bool>,
73 pub files_count: Option<usize>,
74 pub dirs_count: Option<usize>,
75 pub size_count: Option<u64>,
76 pub source_count: Option<usize>,
77 #[serde(default, skip_serializing_if = "is_false")]
78 pub is_legal: bool,
79 #[serde(default, skip_serializing_if = "is_false")]
80 pub is_manifest: bool,
81 #[serde(default, skip_serializing_if = "is_false")]
82 pub is_readme: bool,
83 #[serde(default, skip_serializing_if = "is_false")]
84 pub is_top_level: bool,
85 #[serde(default, skip_serializing_if = "is_false")]
86 pub is_key_file: bool,
87 #[serde(default, skip_serializing_if = "is_false")]
88 pub is_referenced: bool,
89 #[serde(default, skip_serializing_if = "is_false")]
90 pub is_community: bool,
91 #[serde(default, skip_serializing_if = "Vec::is_empty")]
92 pub facets: Vec<String>,
93 pub tallies: Option<OutputTallies>,
94}
95
96impl OutputFileInfo {
97 pub(crate) fn should_serialize_info_surface(&self) -> bool {
98 self.date.is_some()
99 || self.sha1.is_some()
100 || self.md5.is_some()
101 || self.sha256.is_some()
102 || self.sha1_git.is_some()
103 || self.mime_type.is_some()
104 || self.file_type_label.is_some()
105 || self.programming_language.is_some()
106 || self.is_binary.is_some()
107 || self.is_text.is_some()
108 || self.is_archive.is_some()
109 || self.is_media.is_some()
110 || self.is_source.is_some()
111 || self.is_script.is_some()
112 || self.files_count.is_some()
113 || self.dirs_count.is_some()
114 || self.size_count.is_some()
115 }
116
117 pub(crate) fn should_serialize_license_surface(&self) -> bool {
118 self.license_expression.is_some()
119 || !self.license_detections.is_empty()
120 || !self.license_clues.is_empty()
121 || self.percentage_of_license_text.is_some()
122 }
123
124 pub(crate) fn detected_license_expression(&self) -> Option<String> {
130 let combine = |expressions: Vec<String>| -> Option<String> {
131 let expressions: Vec<String> = expressions
132 .into_iter()
133 .filter(|expression| !expression.is_empty())
134 .collect();
135 if expressions.is_empty() {
136 return None;
137 }
138 crate::utils::spdx::select_primary_license_expression(expressions.clone()).or_else(
139 || {
140 crate::utils::spdx::combine_license_expressions_preserving_structure(
141 expressions,
142 )
143 },
144 )
145 };
146
147 combine(
148 self.license_detections
149 .iter()
150 .map(|detection| detection.license_expression.clone())
151 .collect(),
152 )
153 .or_else(|| {
154 combine(
155 self.package_data
156 .iter()
157 .flat_map(|package_data| package_data.license_detections.iter())
158 .map(|detection| detection.license_expression.clone())
159 .collect(),
160 )
161 })
162 .or_else(|| {
163 self.license_expression
164 .clone()
165 .filter(|expression| !expression.is_empty())
166 })
167 }
168
169 pub(crate) fn detected_license_expression_spdx(&self) -> Option<String> {
170 {
171 let expressions: Option<Vec<String>> = self
172 .license_detections
173 .iter()
174 .map(|detection| {
175 (!detection.license_expression_spdx.is_empty())
176 .then(|| detection.license_expression_spdx.clone())
177 })
178 .collect();
179 expressions.and_then(|expressions| {
180 crate::utils::spdx::select_primary_license_expression_strict(expressions.clone())
181 .or_else(|| {
182 crate::utils::spdx::combine_license_expressions_preserving_structure_strict(
183 expressions,
184 )
185 })
186 })
187 }
188 .or_else(|| {
189 let expressions: Option<Vec<String>> = self
190 .package_data
191 .iter()
192 .flat_map(|package_data| package_data.license_detections.iter())
193 .map(|detection| {
194 (!detection.license_expression_spdx.is_empty())
195 .then(|| detection.license_expression_spdx.clone())
196 })
197 .collect();
198 expressions.and_then(|expressions| {
199 crate::utils::spdx::select_primary_license_expression_strict(expressions.clone())
200 .or_else(|| {
201 crate::utils::spdx::combine_license_expressions_preserving_structure_strict(
202 expressions,
203 )
204 })
205 })
206 })
207 .or_else(|| {
208 self.license_expression
209 .clone()
210 .filter(|expression| !expression.is_empty())
211 .and_then(|expression| {
212 crate::utils::spdx::combine_license_expressions_preserving_structure_strict([
213 expression,
214 ])
215 })
216 })
217 }
218}
219
220impl Serialize for OutputFileInfo {
221 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
222 where
223 S: Serializer,
224 {
225 let mut map = Map::new();
226 insert_json(&mut map, "path", &self.path)?;
227 insert_json(&mut map, "type", self.file_type)?;
228 insert_json(&mut map, "name", &self.name)?;
229 insert_json(&mut map, "base_name", &self.base_name)?;
230 insert_json(&mut map, "extension", &self.extension)?;
231 insert_json(&mut map, "size", self.size)?;
232
233 if self.should_serialize_info_surface() {
234 insert_json(&mut map, "date", &self.date)?;
235 insert_json(&mut map, "sha1", self.sha1.as_ref())?;
236 insert_json(&mut map, "md5", self.md5.as_ref())?;
237 insert_json(&mut map, "sha256", self.sha256.as_ref())?;
238 insert_json(&mut map, "sha1_git", self.sha1_git.as_ref())?;
239 insert_json(&mut map, "mime_type", &self.mime_type)?;
240 insert_json(&mut map, "file_type", &self.file_type_label)?;
241 insert_json(&mut map, "programming_language", &self.programming_language)?;
242 insert_json(&mut map, "is_binary", self.is_binary)?;
243 insert_json(&mut map, "is_text", self.is_text)?;
244 insert_json(&mut map, "is_archive", self.is_archive)?;
245 insert_json(&mut map, "is_media", self.is_media)?;
246 insert_json(&mut map, "is_source", self.is_source)?;
247 insert_json(&mut map, "is_script", self.is_script)?;
248 insert_json(&mut map, "files_count", self.files_count)?;
249 insert_json(&mut map, "dirs_count", self.dirs_count)?;
250 insert_json(&mut map, "size_count", self.size_count)?;
251 }
252
253 insert_json(&mut map, "package_data", &self.package_data)?;
254 insert_json(
255 &mut map,
256 "detected_license_expression",
257 self.detected_license_expression(),
258 )?;
259 insert_json(
260 &mut map,
261 "detected_license_expression_spdx",
262 self.detected_license_expression_spdx(),
263 )?;
264 insert_json(&mut map, "license_detections", &self.license_detections)?;
265 if self.should_serialize_license_surface() {
266 insert_json(&mut map, "license_clues", &self.license_clues)?;
267 }
268 if self.percentage_of_license_text.is_some() {
269 insert_json(
270 &mut map,
271 "percentage_of_license_text",
272 self.percentage_of_license_text,
273 )?;
274 }
275 insert_json(&mut map, "copyrights", &self.copyrights)?;
276 insert_json(&mut map, "holders", &self.holders)?;
277 insert_json(&mut map, "authors", &self.authors)?;
278 if !self.emails.is_empty() {
279 insert_json(&mut map, "emails", &self.emails)?;
280 }
281 insert_json(&mut map, "urls", &self.urls)?;
282 insert_json(&mut map, "for_packages", &self.for_packages)?;
283 insert_json(&mut map, "scan_errors", &self.scan_errors)?;
284 if self.license_policy.is_some() {
285 insert_json(&mut map, "license_policy", &self.license_policy)?;
286 }
287 if self.is_generated.is_some() {
288 insert_json(&mut map, "is_generated", self.is_generated)?;
289 }
290 if self.source_count.is_some() {
291 insert_json(&mut map, "source_count", self.source_count)?;
292 }
293 if self.is_legal {
294 insert_json(&mut map, "is_legal", self.is_legal)?;
295 }
296 if self.is_manifest {
297 insert_json(&mut map, "is_manifest", self.is_manifest)?;
298 }
299 if self.is_readme {
300 insert_json(&mut map, "is_readme", self.is_readme)?;
301 }
302 if self.is_top_level {
303 insert_json(&mut map, "is_top_level", self.is_top_level)?;
304 }
305 if self.is_key_file {
306 insert_json(&mut map, "is_key_file", self.is_key_file)?;
307 }
308 if self.is_referenced {
309 insert_json(&mut map, "is_referenced", self.is_referenced)?;
310 }
311 if self.is_community {
312 insert_json(&mut map, "is_community", self.is_community)?;
313 }
314 if !self.facets.is_empty() {
315 insert_json(&mut map, "facets", &self.facets)?;
316 }
317 if self.tallies.is_some() {
318 insert_json(&mut map, "tallies", &self.tallies)?;
319 }
320
321 map.serialize(serializer)
322 }
323}
324
325impl From<&crate::models::FileInfo> for OutputFileInfo {
326 fn from(value: &crate::models::FileInfo) -> Self {
327 Self::from_with_compat_mode(value, crate::cli::CompatibilityMode::Native)
328 }
329}
330
331impl OutputFileInfo {
332 pub fn from_with_compat_mode(
333 value: &crate::models::FileInfo,
334 mode: crate::cli::CompatibilityMode,
335 ) -> Self {
336 Self {
337 name: value.name.clone(),
338 base_name: value.base_name.clone(),
339 extension: value.extension.clone(),
340 path: value.path.clone(),
341 file_type: OutputFileType::from(&value.file_type),
342 mime_type: value.mime_type.clone(),
343 file_type_label: value.file_type_label.clone(),
344 size: value.size,
345 date: value.date.clone(),
346 sha1: value.sha1.as_ref().map(|d| d.as_hex()),
347 md5: value.md5.as_ref().map(|d| d.as_hex()),
348 sha256: value.sha256.as_ref().map(|d| d.as_hex()),
349 sha1_git: value.sha1_git.as_ref().map(|d| d.as_hex()),
350 programming_language: value.programming_language.clone(),
351 package_data: value
352 .package_data
353 .iter()
354 .map(OutputPackageData::from)
355 .collect(),
356 license_expression: value.detected_license_expression.clone(),
357 license_detections: value
358 .license_detections
359 .iter()
360 .map(OutputLicenseDetection::from)
361 .collect(),
362 license_clues: value.license_clues.iter().map(OutputMatch::from).collect(),
363 percentage_of_license_text: value.percentage_of_license_text,
364 copyrights: value
365 .copyrights
366 .iter()
367 .map(|copyright| OutputCopyright::from_with_compat_mode(copyright, mode))
368 .collect(),
369 holders: value.holders.iter().map(OutputHolder::from).collect(),
370 authors: value.authors.iter().map(OutputAuthor::from).collect(),
371 emails: value.emails.iter().map(OutputEmail::from).collect(),
372 urls: value.urls.iter().map(OutputURL::from).collect(),
373 for_packages: value
374 .for_packages
375 .iter()
376 .map(|uid| uid.to_string())
377 .collect(),
378 scan_errors: value
379 .scan_diagnostics
380 .iter()
381 .filter(|d| d.severity != crate::models::DiagnosticSeverity::Info)
382 .map(|d| d.message.clone())
383 .collect(),
384 license_policy: value
385 .license_policy
386 .as_ref()
387 .map(|v| v.iter().map(OutputLicensePolicyEntry::from).collect()),
388 is_generated: value.is_generated,
389 is_binary: value.is_binary,
390 is_text: value.is_text,
391 is_archive: value.is_archive,
392 is_media: value.is_media,
393 is_source: value.is_source,
394 is_script: value.is_script,
395 files_count: value.files_count,
396 dirs_count: value.dirs_count,
397 size_count: value.size_count,
398 source_count: value.source_count,
399 is_legal: value.is_legal,
400 is_manifest: value.is_manifest,
401 is_readme: value.is_readme,
402 is_top_level: value.is_top_level,
403 is_key_file: value.is_key_file,
404 is_referenced: value.is_referenced,
405 is_community: value.is_community,
406 facets: value.facets.clone(),
407 tallies: value.tallies.as_ref().map(OutputTallies::from),
408 }
409 }
410}
411
412impl TryFrom<&OutputFileInfo> for crate::models::FileInfo {
413 type Error = String;
414 fn try_from(value: &OutputFileInfo) -> Result<Self, Self::Error> {
415 let mut package_data = Vec::with_capacity(value.package_data.len());
416 for p in &value.package_data {
417 package_data.push(crate::models::PackageData::try_from(p)?);
418 }
419 let mut license_detections = Vec::with_capacity(value.license_detections.len());
420 for d in &value.license_detections {
421 license_detections.push(crate::models::LicenseDetection::try_from(d)?);
422 }
423 let mut license_clues = Vec::with_capacity(value.license_clues.len());
424 for m in &value.license_clues {
425 license_clues.push(crate::models::Match::try_from(m)?);
426 }
427 let mut copyrights = Vec::with_capacity(value.copyrights.len());
428 for c in &value.copyrights {
429 copyrights.push(crate::models::Copyright::try_from(c)?);
430 }
431 let mut holders = Vec::with_capacity(value.holders.len());
432 for h in &value.holders {
433 holders.push(crate::models::Holder::try_from(h)?);
434 }
435 let mut authors = Vec::with_capacity(value.authors.len());
436 for a in &value.authors {
437 authors.push(crate::models::Author::try_from(a)?);
438 }
439 let mut emails = Vec::with_capacity(value.emails.len());
440 for e in &value.emails {
441 emails.push(crate::models::OutputEmail::try_from(e)?);
442 }
443 let mut urls = Vec::with_capacity(value.urls.len());
444 for u in &value.urls {
445 urls.push(crate::models::OutputURL::try_from(u)?);
446 }
447 let license_policy = value
448 .license_policy
449 .as_ref()
450 .map(|v| {
451 v.iter()
452 .map(crate::models::LicensePolicyEntry::try_from)
453 .collect::<Result<Vec<_>, _>>()
454 })
455 .transpose()?;
456 Ok(Self {
457 name: value.name.clone(),
458 base_name: value.base_name.clone(),
459 extension: value.extension.clone(),
460 path: value.path.clone(),
461 file_type: crate::models::FileType::try_from(value.file_type)?,
462 mime_type: value.mime_type.clone(),
463 file_type_label: value.file_type_label.clone(),
464 size: value.size,
465 date: value.date.clone(),
466 sha1: value
467 .sha1
468 .as_ref()
469 .map(|s| crate::models::Sha1Digest::from_hex(s))
470 .transpose()
471 .map_err(|e| format!("invalid sha1: {}", e))?,
472 md5: value
473 .md5
474 .as_ref()
475 .map(|s| crate::models::Md5Digest::from_hex(s))
476 .transpose()
477 .map_err(|e| format!("invalid md5: {}", e))?,
478 sha256: value
479 .sha256
480 .as_ref()
481 .map(|s| crate::models::Sha256Digest::from_hex(s))
482 .transpose()
483 .map_err(|e| format!("invalid sha256: {}", e))?,
484 sha1_git: value
485 .sha1_git
486 .as_ref()
487 .map(|s| crate::models::GitSha1::from_hex(s))
488 .transpose()
489 .map_err(|e| format!("invalid sha1_git: {}", e))?,
490 programming_language: value.programming_language.clone(),
491 package_data,
492 detected_license_expression: value.license_expression.clone(),
493 license_detections,
494 license_clues,
495 percentage_of_license_text: value.percentage_of_license_text,
496 copyrights,
497 holders,
498 authors,
499 emails,
500 urls,
501 for_packages: value
502 .for_packages
503 .iter()
504 .map(|s| crate::models::PackageUid::from_raw(s.clone()))
505 .collect(),
506 scan_diagnostics: crate::models::diagnostics_from_legacy_scan_errors(
507 &value.scan_errors,
508 ),
509 license_policy,
510 is_generated: value.is_generated,
511 is_binary: value.is_binary,
512 is_text: value.is_text,
513 is_archive: value.is_archive,
514 is_media: value.is_media,
515 is_source: value.is_source,
516 is_script: value.is_script,
517 files_count: value.files_count,
518 dirs_count: value.dirs_count,
519 size_count: value.size_count,
520 source_count: value.source_count,
521 is_legal: value.is_legal,
522 is_manifest: value.is_manifest,
523 is_readme: value.is_readme,
524 is_top_level: value.is_top_level,
525 is_key_file: value.is_key_file,
526 is_referenced: value.is_referenced,
527 is_community: value.is_community,
528 facets: value.facets.clone(),
529 tallies: value
530 .tallies
531 .as_ref()
532 .map(crate::models::Tallies::try_from)
533 .transpose()?,
534 })
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use super::OutputFileInfo;
541 use crate::models::{FileInfoBuilder, FileType, ScanDiagnostic};
542 use crate::output_schema::OutputFileType;
543 use crate::output_schema::license_detection::OutputLicenseDetection;
544 use serde_json::json;
545
546 fn file_info_with_diagnostics(diagnostics: Vec<ScanDiagnostic>) -> crate::models::FileInfo {
547 FileInfoBuilder::default()
548 .name("sample.bin".to_string())
549 .base_name("sample".to_string())
550 .extension(".bin".to_string())
551 .path("sample.bin".to_string())
552 .file_type(FileType::File)
553 .size(1)
554 .scan_diagnostics(diagnostics)
555 .build()
556 .expect("builder should produce file info")
557 }
558
559 #[test]
560 fn scan_errors_excludes_info_diagnostics_but_keeps_real_failures() {
561 let file_info = file_info_with_diagnostics(vec![
562 ScanDiagnostic::info("Text skipped from license/copyright detection: likely binary"),
563 ScanDiagnostic::error("PDF text extraction failed after 3 attempts"),
564 ScanDiagnostic::timeout("license detection timed out"),
565 ]);
566
567 let output = OutputFileInfo::from(&file_info);
568
569 assert_eq!(
570 output.scan_errors,
571 vec![
572 "PDF text extraction failed after 3 attempts".to_string(),
573 "license detection timed out".to_string(),
574 ],
575 "Info diagnostics must not surface as scan_errors, but real failures must"
576 );
577 }
578
579 #[test]
580 fn scan_errors_empty_when_only_info_diagnostics_present() {
581 let file_info = file_info_with_diagnostics(vec![ScanDiagnostic::info(
582 "Text skipped from license/copyright detection: likely binary",
583 )]);
584
585 let output = OutputFileInfo::from(&file_info);
586
587 assert!(
588 output.scan_errors.is_empty(),
589 "a benign binary-content skip must not produce any scan_errors"
590 );
591 }
592
593 fn base_output_file_info() -> OutputFileInfo {
594 OutputFileInfo {
595 name: "mod.rs".to_string(),
596 base_name: "mod".to_string(),
597 extension: ".rs".to_string(),
598 path: "mod.rs".to_string(),
599 file_type: OutputFileType::File,
600 mime_type: None,
601 file_type_label: None,
602 size: 0,
603 date: None,
604 sha1: None,
605 md5: None,
606 sha256: None,
607 sha1_git: None,
608 programming_language: None,
609 package_data: Vec::new(),
610 license_expression: None,
611 license_detections: Vec::new(),
612 license_clues: Vec::new(),
613 percentage_of_license_text: None,
614 copyrights: Vec::new(),
615 holders: Vec::new(),
616 authors: Vec::new(),
617 emails: Vec::new(),
618 urls: Vec::new(),
619 for_packages: Vec::new(),
620 scan_errors: Vec::new(),
621 license_policy: None,
622 is_generated: None,
623 is_binary: None,
624 is_text: None,
625 is_archive: None,
626 is_media: None,
627 is_source: None,
628 is_script: None,
629 files_count: None,
630 dirs_count: None,
631 size_count: None,
632 source_count: None,
633 is_legal: false,
634 is_manifest: false,
635 is_readme: false,
636 is_top_level: false,
637 is_key_file: false,
638 is_referenced: false,
639 is_community: false,
640 facets: Vec::new(),
641 tallies: None,
642 }
643 }
644
645 #[test]
646 fn detected_license_expression_spdx_does_not_recombine_partial_detection_spdx() {
647 let mut file_info = base_output_file_info();
648 file_info.license_expression = Some("Apache-2.0 AND MIT".to_string());
649 file_info.license_detections = vec![
650 OutputLicenseDetection {
651 license_expression: "apache-2.0".to_string(),
652 license_expression_spdx: "Apache-2.0".to_string(),
653 matches: Vec::new(),
654 detection_log: Vec::new(),
655 identifier: None,
656 },
657 OutputLicenseDetection {
658 license_expression: "mit".to_string(),
659 license_expression_spdx: String::new(),
660 matches: Vec::new(),
661 detection_log: Vec::new(),
662 identifier: None,
663 },
664 ];
665
666 assert_eq!(
667 file_info.detected_license_expression_spdx().as_deref(),
668 Some("Apache-2.0 AND MIT")
669 );
670 }
671
672 #[test]
673 fn detected_license_expression_spdx_rejects_invalid_fallback_expression() {
674 let mut file_info = base_output_file_info();
675 file_info.license_expression = Some("MIT\" or malformed".to_string());
676
677 assert_eq!(file_info.detected_license_expression_spdx(), None);
678 }
679
680 #[test]
681 fn serialize_includes_is_referenced_only_when_true() {
682 let mut file_info = base_output_file_info();
683 let without_flag = serde_json::to_value(&file_info).expect("file should serialize");
684 assert_eq!(
685 without_flag,
686 json!({
687 "path": "mod.rs",
688 "type": "file",
689 "name": "mod.rs",
690 "base_name": "mod",
691 "extension": ".rs",
692 "size": 0,
693 "package_data": [],
694 "detected_license_expression": null,
695 "detected_license_expression_spdx": null,
696 "license_detections": [],
697 "copyrights": [],
698 "holders": [],
699 "authors": [],
700 "urls": [],
701 "for_packages": [],
702 "scan_errors": []
703 })
704 );
705
706 file_info.is_referenced = true;
707 let with_flag = serde_json::to_value(&file_info).expect("file should serialize");
708 assert_eq!(with_flag["is_referenced"], json!(true));
709 }
710}