1use std::collections::{HashMap, HashSet, VecDeque};
5use std::path::Path;
6
7use crate::parser_warn as warn;
8use packageurl::PackageUrl;
9use regex::Regex;
10use serde_json::{Map as JsonMap, Value as JsonValue};
11use toml::Value as TomlValue;
12use toml::map::Map as TomlMap;
13
14use crate::models::{
15 DatasourceId, Dependency, Md5Digest, PackageData, PackageType, ResolvedPackage, Sha256Digest,
16 Sha512Digest,
17};
18use crate::parsers::python::read_toml_file;
19use crate::parsers::utils::{MAX_ITERATION_COUNT, RecursionGuard, truncate_field};
20
21use super::PackageParser;
22
23const FIELD_LOCK_VERSION: &str = "lock-version";
24const FIELD_CREATED_BY: &str = "created-by";
25const SUPPORTED_LOCK_VERSION: &str = "1.0";
26const FIELD_REQUIRES_PYTHON: &str = "requires-python";
27const FIELD_ENVIRONMENTS: &str = "environments";
28const FIELD_EXTRAS: &str = "extras";
29const FIELD_DEPENDENCY_GROUPS: &str = "dependency-groups";
30const FIELD_DEFAULT_GROUPS: &str = "default-groups";
31const FIELD_PACKAGES: &str = "packages";
32const FIELD_NAME: &str = "name";
33const FIELD_VERSION: &str = "version";
34const FIELD_MARKER: &str = "marker";
35const FIELD_DEPENDENCIES: &str = "dependencies";
36const FIELD_INDEX: &str = "index";
37const FIELD_VCS: &str = "vcs";
38const FIELD_DIRECTORY: &str = "directory";
39const FIELD_ARCHIVE: &str = "archive";
40const FIELD_SDIST: &str = "sdist";
41const FIELD_WHEELS: &str = "wheels";
42const FIELD_HASHES: &str = "hashes";
43const FIELD_TOOL: &str = "tool";
44const FIELD_ATTESTATION_IDENTITIES: &str = "attestation-identities";
45
46pub struct PylockTomlParser;
47
48#[derive(Clone, Debug, Default)]
49struct MarkerClassification {
50 is_runtime: bool,
51 is_optional: bool,
52 scope: Option<String>,
53}
54
55struct DependencyAnalysisContext<'a> {
56 package_tables: &'a [&'a TomlMap<String, TomlValue>],
57 dependency_indices: &'a [Vec<usize>],
58 incoming_counts: &'a [usize],
59 root_classifications: &'a [MarkerClassification],
60 runtime_reachable: &'a HashSet<usize>,
61 optional_reachable: &'a HashSet<usize>,
62 scope_sets: &'a HashMap<String, HashSet<usize>>,
63}
64
65impl PackageParser for PylockTomlParser {
66 const PACKAGE_TYPE: PackageType = PackageType::Pypi;
67
68 fn is_match(path: &Path) -> bool {
69 let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
70 return false;
71 };
72
73 file_name == "pylock.toml"
74 || file_name
75 .strip_prefix("pylock.")
76 .and_then(|suffix| suffix.strip_suffix(".toml"))
77 .is_some_and(|middle| !middle.is_empty() && !middle.contains('.'))
78 }
79
80 fn extract_packages(path: &Path) -> Vec<PackageData> {
81 let toml_content = match read_toml_file(path) {
82 Ok(content) => content,
83 Err(e) => {
84 warn!("Failed to read pylock.toml at {:?}: {}", path, e);
85 return vec![default_package_data()];
86 }
87 };
88
89 vec![parse_pylock_toml(&toml_content)]
90 }
91}
92
93fn parse_pylock_toml(toml_content: &TomlValue) -> PackageData {
94 let lock_version = toml_content
95 .get(FIELD_LOCK_VERSION)
96 .and_then(TomlValue::as_str);
97 if lock_version != Some(SUPPORTED_LOCK_VERSION) {
98 warn!(
99 "Invalid pylock.toml: missing or unsupported lock-version {:?}",
100 lock_version
101 );
102 return default_package_data();
103 }
104
105 let created_by = toml_content
106 .get(FIELD_CREATED_BY)
107 .and_then(TomlValue::as_str);
108 if created_by.is_none() {
109 warn!("Invalid pylock.toml: missing required created-by field");
110 return default_package_data();
111 }
112
113 let Some(package_values) = toml_content
114 .get(FIELD_PACKAGES)
115 .and_then(TomlValue::as_array)
116 else {
117 warn!("Invalid pylock.toml: missing required packages array");
118 return default_package_data();
119 };
120
121 let package_tables: Vec<&TomlMap<String, TomlValue>> = package_values
122 .iter()
123 .take(MAX_ITERATION_COUNT)
124 .filter_map(TomlValue::as_table)
125 .collect();
126 if package_tables.is_empty() {
127 warn!("Invalid pylock.toml: packages array does not contain package tables");
128 return default_package_data();
129 }
130
131 let dependency_indices = build_dependency_indices(&package_tables);
132 let incoming_counts = build_incoming_counts(package_tables.len(), &dependency_indices);
133 let default_groups = extract_string_set(toml_content, FIELD_DEFAULT_GROUPS);
134
135 let root_classifications: Vec<MarkerClassification> = package_tables
136 .iter()
137 .enumerate()
138 .map(|(index, table)| {
139 if incoming_counts[index] == 0 {
140 classify_marker(
141 table.get(FIELD_MARKER).and_then(TomlValue::as_str),
142 &default_groups,
143 )
144 } else {
145 MarkerClassification::default()
146 }
147 })
148 .collect();
149
150 let runtime_roots: Vec<usize> = root_classifications
151 .iter()
152 .enumerate()
153 .filter_map(|(index, info)| {
154 (incoming_counts[index] == 0 && info.is_runtime).then_some(index)
155 })
156 .collect();
157 let optional_roots: Vec<usize> = root_classifications
158 .iter()
159 .enumerate()
160 .filter_map(|(index, info)| {
161 (incoming_counts[index] == 0 && info.is_optional).then_some(index)
162 })
163 .collect();
164
165 let runtime_reachable = collect_reachable_indices(&dependency_indices, &runtime_roots);
166 let optional_reachable = collect_reachable_indices(&dependency_indices, &optional_roots);
167
168 let mut scope_sets: HashMap<String, HashSet<usize>> = HashMap::new();
169 for (index, info) in root_classifications.iter().enumerate() {
170 if incoming_counts[index] != 0 {
171 continue;
172 }
173
174 if let Some(scope) = info.scope.as_ref() {
175 scope_sets.insert(
176 scope.clone(),
177 collect_reachable_indices(&dependency_indices, &[index]),
178 );
179 }
180 }
181
182 let analysis = DependencyAnalysisContext {
183 package_tables: &package_tables,
184 dependency_indices: &dependency_indices,
185 incoming_counts: &incoming_counts,
186 root_classifications: &root_classifications,
187 runtime_reachable: &runtime_reachable,
188 optional_reachable: &optional_reachable,
189 scope_sets: &scope_sets,
190 };
191
192 let mut package_data = default_package_data();
193 package_data.extra_data = build_lock_extra_data(toml_content);
194 package_data.dependencies = package_tables
195 .iter()
196 .enumerate()
197 .filter_map(|(index, package_table)| {
198 build_top_level_dependency(index, package_table, &analysis)
199 })
200 .collect();
201
202 package_data
203}
204
205fn build_top_level_dependency(
206 index: usize,
207 package_table: &TomlMap<String, TomlValue>,
208 analysis: &DependencyAnalysisContext<'_>,
209) -> Option<Dependency> {
210 let name = normalized_package_name(package_table)?;
211 let version = package_version(package_table);
212 let direct = analysis
213 .incoming_counts
214 .get(index)
215 .copied()
216 .unwrap_or_default()
217 == 0;
218
219 let (is_runtime, is_optional, scope) = if direct {
220 let classification = analysis
221 .root_classifications
222 .get(index)
223 .cloned()
224 .unwrap_or_default();
225 (
226 classification.is_runtime,
227 classification.is_optional,
228 classification.scope,
229 )
230 } else {
231 let is_runtime = analysis.runtime_reachable.contains(&index);
232 let is_optional = !is_runtime && analysis.optional_reachable.contains(&index);
233 let scope = scope_for_index(analysis.scope_sets, index);
234 (is_runtime, is_optional, scope)
235 };
236
237 Some(Dependency {
238 purl: create_pypi_purl(&name, version.as_deref()),
239 extracted_requirement: None,
240 scope: scope.map(truncate_field),
241 is_runtime: Some(is_runtime),
242 is_optional: Some(is_optional),
243 is_pinned: Some(is_package_pinned(package_table)),
244 is_direct: Some(direct),
245 resolved_package: Some(Box::new(build_resolved_package(
246 package_table,
247 analysis.package_tables,
248 analysis
249 .dependency_indices
250 .get(index)
251 .map(Vec::as_slice)
252 .unwrap_or(&[]),
253 ))),
254 extra_data: build_package_extra_data(package_table),
255 })
256}
257
258fn build_resolved_package(
259 package_table: &TomlMap<String, TomlValue>,
260 package_tables: &[&TomlMap<String, TomlValue>],
261 dependency_indices: &[usize],
262) -> ResolvedPackage {
263 let name = normalized_package_name(package_table).unwrap_or_default();
264 let version = package_version(package_table).unwrap_or_default();
265 let (_, repository_download_url, api_data_url, purl) = build_pypi_urls(
266 Some(&name),
267 (!version.is_empty()).then_some(version.as_str()),
268 );
269 let repository_homepage_url = Some(format!("https://pypi.org/project/{}", name));
270 let (download_url, sha256, sha512, md5) = extract_artifact_metadata(package_table);
271
272 ResolvedPackage {
273 primary_language: Some("Python".to_string()),
274 download_url,
275 sha1: None,
276 sha256: sha256.and_then(|h| Sha256Digest::from_hex(&h).ok()),
277 sha512: sha512.and_then(|h| Sha512Digest::from_hex(&h).ok()),
278 md5: md5.and_then(|h| Md5Digest::from_hex(&h).ok()),
279 is_virtual: false,
280 extra_data: build_package_extra_data(package_table),
281 dependencies: dependency_indices
282 .iter()
283 .filter_map(|child_index| package_tables.get(*child_index))
284 .filter_map(|child| build_resolved_dependency(child))
285 .collect(),
286 repository_homepage_url,
287 repository_download_url,
288 api_data_url,
289 datasource_id: Some(DatasourceId::PypiPylockToml),
290 purl,
291 ..ResolvedPackage::new(PylockTomlParser::PACKAGE_TYPE, String::new(), name, version)
292 }
293}
294
295fn build_resolved_dependency(package_table: &TomlMap<String, TomlValue>) -> Option<Dependency> {
296 let name = normalized_package_name(package_table)?;
297 let version = package_version(package_table);
298
299 Some(Dependency {
300 purl: create_pypi_purl(&name, version.as_deref()),
301 extracted_requirement: None,
302 scope: None,
303 is_runtime: None,
304 is_optional: None,
305 is_pinned: Some(is_package_pinned(package_table)),
306 is_direct: Some(true),
307 resolved_package: None,
308 extra_data: build_package_extra_data(package_table),
309 })
310}
311
312fn build_dependency_indices(package_tables: &[&TomlMap<String, TomlValue>]) -> Vec<Vec<usize>> {
313 let mut total_iterations: usize = 0;
314 package_tables
315 .iter()
316 .map(|package_table| {
317 package_table
318 .get(FIELD_DEPENDENCIES)
319 .and_then(TomlValue::as_array)
320 .into_iter()
321 .flatten()
322 .filter_map(TomlValue::as_table)
323 .flat_map(|reference| {
324 if total_iterations >= MAX_ITERATION_COUNT {
325 return Vec::new();
326 }
327 total_iterations += 1;
328 resolve_dependency_reference_indices(package_tables, reference)
329 })
330 .collect()
331 })
332 .collect()
333}
334
335fn resolve_dependency_reference_indices(
336 package_tables: &[&TomlMap<String, TomlValue>],
337 reference: &TomlMap<String, TomlValue>,
338) -> Vec<usize> {
339 let matches: Vec<usize> = package_tables
340 .iter()
341 .enumerate()
342 .take(MAX_ITERATION_COUNT)
343 .filter_map(|(index, package_table)| {
344 package_reference_matches(package_table, reference).then_some(index)
345 })
346 .collect();
347
348 if matches.len() == 1 {
349 matches
350 } else {
351 Vec::new()
352 }
353}
354
355fn package_reference_matches(
356 package_table: &TomlMap<String, TomlValue>,
357 reference: &TomlMap<String, TomlValue>,
358) -> bool {
359 let mut guard = RecursionGuard::depth_only();
360 reference.iter().all(|(key, ref_value)| {
361 package_table
362 .get(key)
363 .is_some_and(|pkg_value| toml_values_match(pkg_value, ref_value, &mut guard))
364 })
365}
366
367fn toml_values_match(left: &TomlValue, right: &TomlValue, guard: &mut RecursionGuard<()>) -> bool {
368 if guard.descend() {
369 warn!("toml_values_match: recursion depth limit exceeded");
370 return false;
371 }
372 let result = match (left, right) {
373 (TomlValue::String(left), TomlValue::String(right)) => left == right,
374 (TomlValue::Integer(left), TomlValue::Integer(right)) => left == right,
375 (TomlValue::Float(left), TomlValue::Float(right)) => left == right,
376 (TomlValue::Boolean(left), TomlValue::Boolean(right)) => left == right,
377 (TomlValue::Datetime(left), TomlValue::Datetime(right)) => left == right,
378 (TomlValue::Array(left), TomlValue::Array(right)) => {
379 left.len() == right.len()
380 && left
381 .iter()
382 .zip(right.iter())
383 .all(|(left, right)| toml_values_match(left, right, guard))
384 }
385 (TomlValue::Table(left), TomlValue::Table(right)) => {
386 right.iter().all(|(key, right_value)| {
387 left.get(key)
388 .is_some_and(|left_value| toml_values_match(left_value, right_value, guard))
389 })
390 }
391 _ => false,
392 };
393 guard.ascend();
394 result
395}
396
397fn build_incoming_counts(package_count: usize, dependency_indices: &[Vec<usize>]) -> Vec<usize> {
398 let mut incoming = vec![0; package_count];
399 for dependency_list in dependency_indices {
400 for &child_index in dependency_list {
401 if let Some(count) = incoming.get_mut(child_index) {
402 *count += 1;
403 }
404 }
405 }
406 incoming
407}
408
409fn collect_reachable_indices(dependency_indices: &[Vec<usize>], roots: &[usize]) -> HashSet<usize> {
410 let mut visited = HashSet::new();
411 let mut queue: VecDeque<usize> = roots.iter().copied().collect();
412
413 while let Some(index) = queue.pop_front() {
414 if visited.len() >= MAX_ITERATION_COUNT {
415 break;
416 }
417 if !visited.insert(index) {
418 continue;
419 }
420
421 for &child_index in dependency_indices.get(index).into_iter().flatten() {
422 queue.push_back(child_index);
423 }
424 }
425
426 visited
427}
428
429fn classify_marker(marker: Option<&str>, default_groups: &HashSet<String>) -> MarkerClassification {
430 let Some(marker) = marker else {
431 return MarkerClassification {
432 is_runtime: true,
433 is_optional: false,
434 scope: None,
435 };
436 };
437
438 let extras = extract_marker_memberships(marker, "extras");
439 if !extras.is_empty() {
440 return MarkerClassification {
441 is_runtime: false,
442 is_optional: true,
443 scope: single_scope(extras),
444 };
445 }
446
447 let groups = extract_marker_memberships(marker, "dependency_groups");
448 let non_default_groups: Vec<String> = groups
449 .into_iter()
450 .filter(|group| !default_groups.contains(group))
451 .collect();
452 if !non_default_groups.is_empty() {
453 return MarkerClassification {
454 is_runtime: false,
455 is_optional: false,
456 scope: single_scope(non_default_groups),
457 };
458 }
459
460 MarkerClassification {
461 is_runtime: true,
462 is_optional: false,
463 scope: None,
464 }
465}
466
467fn extract_marker_memberships(marker: &str, variable_name: &str) -> Vec<String> {
468 let pattern = format!(
469 r#"['\"]([^'\"]+)['\"]\s+in\s+{}\b"#,
470 regex::escape(variable_name)
471 );
472 let Ok(regex) = Regex::new(&pattern) else {
473 return Vec::new();
474 };
475
476 let mut memberships: Vec<String> = regex
477 .captures_iter(marker)
478 .filter_map(|captures| {
479 captures
480 .get(1)
481 .map(|value| truncate_field(value.as_str().trim().to_string()))
482 })
483 .filter(|value| !value.is_empty())
484 .collect();
485 memberships.sort();
486 memberships.dedup();
487 memberships
488}
489
490fn single_scope(values: Vec<String>) -> Option<String> {
491 (values.len() == 1).then(|| values[0].clone())
492}
493
494fn scope_for_index(scope_sets: &HashMap<String, HashSet<usize>>, index: usize) -> Option<String> {
495 let matches: Vec<String> = scope_sets
496 .iter()
497 .filter_map(|(scope, indices)| indices.contains(&index).then_some(scope.clone()))
498 .collect();
499 single_scope(matches)
500}
501
502fn normalized_package_name(package_table: &TomlMap<String, TomlValue>) -> Option<String> {
503 package_table
504 .get(FIELD_NAME)
505 .and_then(TomlValue::as_str)
506 .map(|value| truncate_field(value.trim().to_ascii_lowercase()))
507}
508
509fn package_version(package_table: &TomlMap<String, TomlValue>) -> Option<String> {
510 package_table
511 .get(FIELD_VERSION)
512 .and_then(TomlValue::as_str)
513 .map(|value| truncate_field(value.to_string()))
514}
515
516fn is_package_pinned(package_table: &TomlMap<String, TomlValue>) -> bool {
517 package_table.contains_key(FIELD_VERSION)
518 || package_table
519 .get(FIELD_VCS)
520 .and_then(TomlValue::as_table)
521 .is_some_and(|table| table.contains_key("commit-id"))
522 || has_hashes(package_table.get(FIELD_ARCHIVE))
523 || has_hashes(package_table.get(FIELD_SDIST))
524 || package_table
525 .get(FIELD_WHEELS)
526 .and_then(TomlValue::as_array)
527 .into_iter()
528 .flatten()
529 .filter_map(TomlValue::as_table)
530 .any(|wheel| wheel.contains_key(FIELD_HASHES))
531}
532
533fn has_hashes(value: Option<&TomlValue>) -> bool {
534 value
535 .and_then(TomlValue::as_table)
536 .is_some_and(|table| table.contains_key(FIELD_HASHES))
537}
538
539fn build_lock_extra_data(toml_content: &TomlValue) -> Option<HashMap<String, JsonValue>> {
540 let mut extra_data = HashMap::new();
541
542 for (source_key, target_key) in [
543 (FIELD_LOCK_VERSION, "lock_version"),
544 (FIELD_CREATED_BY, "created_by"),
545 (FIELD_REQUIRES_PYTHON, "requires_python"),
546 (FIELD_ENVIRONMENTS, FIELD_ENVIRONMENTS),
547 (FIELD_EXTRAS, FIELD_EXTRAS),
548 (FIELD_DEPENDENCY_GROUPS, FIELD_DEPENDENCY_GROUPS),
549 (FIELD_DEFAULT_GROUPS, FIELD_DEFAULT_GROUPS),
550 ] {
551 if let Some(value) = toml_content.get(source_key) {
552 extra_data.insert(target_key.to_string(), toml_value_to_json(value));
553 }
554 }
555
556 if let Some(tool) = toml_content.get(FIELD_TOOL) {
557 extra_data.insert(FIELD_TOOL.to_string(), toml_value_to_json(tool));
558 }
559
560 (!extra_data.is_empty()).then_some(extra_data)
561}
562
563fn build_package_extra_data(
564 package_table: &TomlMap<String, TomlValue>,
565) -> Option<HashMap<String, JsonValue>> {
566 let mut extra_data = HashMap::new();
567
568 for key in [
569 FIELD_MARKER,
570 FIELD_REQUIRES_PYTHON,
571 FIELD_INDEX,
572 FIELD_VCS,
573 FIELD_DIRECTORY,
574 FIELD_ARCHIVE,
575 FIELD_SDIST,
576 FIELD_WHEELS,
577 FIELD_TOOL,
578 FIELD_ATTESTATION_IDENTITIES,
579 ] {
580 if let Some(value) = package_table.get(key) {
581 extra_data.insert(key.to_string(), toml_value_to_json(value));
582 }
583 }
584
585 (!extra_data.is_empty()).then_some(extra_data)
586}
587
588fn extract_artifact_metadata(
589 package_table: &TomlMap<String, TomlValue>,
590) -> (
591 Option<String>,
592 Option<String>,
593 Option<String>,
594 Option<String>,
595) {
596 if let Some(archive_table) = package_table
597 .get(FIELD_ARCHIVE)
598 .and_then(TomlValue::as_table)
599 {
600 return (
601 archive_table
602 .get("url")
603 .and_then(TomlValue::as_str)
604 .map(|value| truncate_field(value.to_string()))
605 .or_else(|| {
606 archive_table
607 .get("path")
608 .and_then(TomlValue::as_str)
609 .map(|value| truncate_field(value.to_string()))
610 }),
611 extract_hash_by_name(archive_table, "sha256"),
612 extract_hash_by_name(archive_table, "sha512"),
613 extract_hash_by_name(archive_table, "md5"),
614 );
615 }
616
617 if let Some(sdist_table) = package_table.get(FIELD_SDIST).and_then(TomlValue::as_table) {
618 return (
619 sdist_table
620 .get("url")
621 .and_then(TomlValue::as_str)
622 .map(|value| truncate_field(value.to_string()))
623 .or_else(|| {
624 sdist_table
625 .get("path")
626 .and_then(TomlValue::as_str)
627 .map(|value| truncate_field(value.to_string()))
628 }),
629 extract_hash_by_name(sdist_table, "sha256"),
630 extract_hash_by_name(sdist_table, "sha512"),
631 extract_hash_by_name(sdist_table, "md5"),
632 );
633 }
634
635 let wheel_table = package_table
636 .get(FIELD_WHEELS)
637 .and_then(TomlValue::as_array)
638 .and_then(|wheels| wheels.first())
639 .and_then(TomlValue::as_table);
640
641 (
642 wheel_table
643 .and_then(|table| table.get("url"))
644 .and_then(TomlValue::as_str)
645 .map(|value| truncate_field(value.to_string()))
646 .or_else(|| {
647 wheel_table
648 .and_then(|table| table.get("path"))
649 .and_then(TomlValue::as_str)
650 .map(|value| truncate_field(value.to_string()))
651 }),
652 wheel_table.and_then(|table| extract_hash_by_name(table, "sha256")),
653 wheel_table.and_then(|table| extract_hash_by_name(table, "sha512")),
654 wheel_table.and_then(|table| extract_hash_by_name(table, "md5")),
655 )
656}
657
658fn extract_hash_by_name(table: &TomlMap<String, TomlValue>, name: &str) -> Option<String> {
659 table
660 .get(FIELD_HASHES)
661 .and_then(TomlValue::as_table)
662 .and_then(|hashes| hashes.get(name))
663 .and_then(TomlValue::as_str)
664 .map(|value| truncate_field(value.to_string()))
665}
666
667fn extract_string_set(toml_content: &TomlValue, key: &str) -> HashSet<String> {
668 toml_content
669 .get(key)
670 .and_then(TomlValue::as_array)
671 .into_iter()
672 .flatten()
673 .filter_map(TomlValue::as_str)
674 .map(|value| value.to_string())
675 .collect()
676}
677
678fn build_pypi_urls(
679 name: Option<&str>,
680 version: Option<&str>,
681) -> (
682 Option<String>,
683 Option<String>,
684 Option<String>,
685 Option<String>,
686) {
687 let repository_homepage_url = name.map(|value| format!("https://pypi.org/project/{}", value));
688 let repository_download_url = name.and_then(|value| {
689 version.map(|ver| {
690 format!(
691 "https://pypi.org/packages/source/{}/{}/{}-{}.tar.gz",
692 &value[..1.min(value.len())],
693 value,
694 value,
695 ver
696 )
697 })
698 });
699 let api_data_url = name.map(|value| {
700 if let Some(ver) = version {
701 format!("https://pypi.org/pypi/{}/{}/json", value, ver)
702 } else {
703 format!("https://pypi.org/pypi/{}/json", value)
704 }
705 });
706 let purl = name.and_then(|value| create_pypi_purl(value, version));
707
708 (
709 repository_homepage_url,
710 repository_download_url,
711 api_data_url,
712 purl,
713 )
714}
715
716fn create_pypi_purl(name: &str, version: Option<&str>) -> Option<String> {
717 if let Ok(mut purl) = PackageUrl::new(PylockTomlParser::PACKAGE_TYPE.as_str(), name) {
718 if let Some(version) = version
719 && purl.with_version(version).is_err()
720 {
721 return None;
722 }
723 return Some(truncate_field(purl.to_string()));
724 }
725
726 let mut purl = format!("pkg:pypi/{}", name);
727 if let Some(version) = version
728 && !version.is_empty()
729 {
730 purl.push('@');
731 purl.push_str(version);
732 }
733 Some(truncate_field(purl))
734}
735
736fn toml_value_to_json(value: &TomlValue) -> JsonValue {
737 match value {
738 TomlValue::String(value) => JsonValue::String(value.clone()),
739 TomlValue::Integer(value) => JsonValue::String(value.to_string()),
740 TomlValue::Float(value) => JsonValue::String(value.to_string()),
741 TomlValue::Boolean(value) => JsonValue::Bool(*value),
742 TomlValue::Datetime(value) => JsonValue::String(value.to_string()),
743 TomlValue::Array(values) => {
744 JsonValue::Array(values.iter().map(toml_value_to_json).collect())
745 }
746 TomlValue::Table(values) => JsonValue::Object(
747 values
748 .iter()
749 .map(|(key, value)| (key.clone(), toml_value_to_json(value)))
750 .collect::<JsonMap<String, JsonValue>>(),
751 ),
752 }
753}
754
755fn default_package_data() -> PackageData {
756 PackageData {
757 package_type: Some(PylockTomlParser::PACKAGE_TYPE),
758 primary_language: Some("Python".to_string()),
759 datasource_id: Some(DatasourceId::PypiPylockToml),
760 ..Default::default()
761 }
762}
763
764crate::register_parser!(
765 "pylock.toml lockfile",
766 &["**/pylock.toml", "**/pylock.*.toml"],
767 "pypi",
768 "Python",
769 Some("https://packaging.python.org/en/latest/specifications/pylock-toml/"),
770);