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