Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Map;
import java.util.HashMap;
Expand All @@ -20,18 +21,32 @@ public class CargoTomlParser {
private static final String NAME_KEY = "name";
private static final String VERSION_KEY = "version";
private static final String PACKAGE_KEY = "package";
private static final String WORKSPACE_KEY = "workspace";
private static final String NORMAL_DEPENDENCIES_KEY = "dependencies";
private static final String BUILD_DEPENDENCIES_KEY = "build-dependencies";
private static final String DEV_DEPENDENCIES_KEY = "dev-dependencies";

public Optional<NameVersion> parseNameVersionFromCargoToml(String tomlFileContents) {
TomlParseResult cargoTomlObject = Toml.parse(tomlFileContents);
if (cargoTomlObject.contains(PACKAGE_KEY)) {
return Optional.ofNullable(cargoTomlObject.getTable(PACKAGE_KEY))
.filter(info -> info.contains(NAME_KEY))
.map(info -> new NameVersion(info.getString(NAME_KEY), info.getString(VERSION_KEY)));
TomlTable packageTable = cargoTomlObject.getTable(PACKAGE_KEY);
if (packageTable == null || !packageTable.contains(NAME_KEY)) {
return Optional.empty();
}
return Optional.empty();

String name = packageTable.getString(NAME_KEY);
String version = null;
Object versionObj = packageTable.get(VERSION_KEY);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we want to do for conditions when versionObj is null, I have seen examples in python where version is not specified, do we have similar concept in Cargo?

Copy link
Collaborator Author

@zahidblackduck zahidblackduck Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's an interesting question which I didn't think of earlier. I'll explore further whether there is any possibility of an version section being null and if so, then how cargo declares / handles this.

Copy link
Collaborator Author

@zahidblackduck zahidblackduck Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have investigated further how cargo treats version field, and current behavior of the cargo detectors when version field is missing i.e. versionObj is null. I'm sharing my findings here,

Yes, versionObj can be null. The the version field has been optional in Cargo.toml since Cargo 1.75 (defaulting to 0.0.0 if omitted). [Reference]
However, the behavior differs based on project structure:

1. Projects Using Workspace Inheritance Syntax

Cargo CLI Detector:

  • Depends on cargo tree command (introduced in Cargo 1.44)
  • When version.workspace = true but workspace doesn't define version, cargo tree command fails
  • The detector never reaches the CargoTomlParser logic
  • Falls back to Cargo Lockfile Detector

Cargo Lockfile Detector:

  • Successfully parses Cargo.toml even with null/missing version
  • Defaults to "Default Detect Version" when version is null

Reference Doc

2. Standalone Projects (No Workspace)

Both Cargo CLI Detector and Cargo Lockfile Detector:

  • Successfully parse Cargo.toml with null/missing version
  • Both default to "Default Detect Version" when version is null
  • cargo tree command succeeds even without an explicit version

So, the version field is required, when:

  • Publishing to crates.io: Always required
  • Workspace inheritance: Workspace must define the version (enforced by cargo tree)
  • Local development: Optional since Cargo 1.75 (defaults to 0.0.0)

Conclusion
Our implementation of the current merge request safely handles null by returning Optional.of(new NameVersion(name, null)). When version is null, both detectors already default to "Default Detect Version"


if (versionObj instanceof String) {
version = (String) versionObj;
} else if (versionObj instanceof TomlTable && Boolean.TRUE.equals(((TomlTable) versionObj).getBoolean(WORKSPACE_KEY))) {
TomlTable workspacePackage = cargoTomlObject.getTable(WORKSPACE_KEY) != null ? Objects.requireNonNull(cargoTomlObject.getTable(WORKSPACE_KEY)).getTable(PACKAGE_KEY) : null;
if (workspacePackage != null) {
version = workspacePackage.getString(VERSION_KEY);
}
}

return Optional.of(new NameVersion(name, version));
}

public boolean hasDependencySections(String tomlFileContents) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.blackduck.integration.detectable.detectables.cargo.parse;

import com.blackduck.integration.detectable.detectables.cargo.parse.CargoTomlParser;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
Expand All @@ -15,6 +14,25 @@

class CargoTomlParserTest {

private static final String WORKSPACE_TOML = String.join("\n",
"[workspace]",
"members = [\"extra-lib\"]",
"resolver = \"2\"",
"",
"[workspace.package]",
"version = \"0.5.0\"",
"edition = \"2021\"",
"repository = \"https://github.com/example/repo\"",
"license = \"CLOSED\"",
"",
"[package]",
"name = \"root-app\"",
"version.workspace = true",
"edition.workspace = true",
"repository.workspace = true",
"license.workspace = true"
);

@Test
void extractNameVersion() {
Optional<NameVersion> nameVersion = parseCargoTomlLines(
Expand Down Expand Up @@ -60,6 +78,17 @@ void extractNoPackage() {
assertFalse(nameVersion.isPresent());
}

@Test
void extractNameVersionFromWorkspacePackage() {
CargoTomlParser parser = new CargoTomlParser();

Optional<NameVersion> nameVersion = parser.parseNameVersionFromCargoToml(WORKSPACE_TOML);

assertTrue(nameVersion.isPresent());
assertEquals("root-app", nameVersion.get().getName());
assertEquals("0.5.0", nameVersion.get().getVersion());
}

private Optional<NameVersion> parseCargoTomlLines(String... lines) {
CargoTomlParser parser = new CargoTomlParser();
String cargoTomlContents = StringUtils.joinWith(System.lineSeparator(), (Object[]) lines);
Expand Down