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