Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
122 changes: 87 additions & 35 deletions utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,46 @@ function toBase64(hexString) {
return Buffer.from(hexString, "hex").toString("base64");
}

const spdxLicenseExpressionOp = [
" with ",
" and ",
" or ", // Apache-2.0 OR MIT
"-or-later", // GPL-2.0-or-later
"-only" // GPL-2.0-only
];

/**
* Method to determine if a license is a valid SPDX license expression
*
* @param {string} license License string
* @returns {boolean} true if the license is a valid SPDX license expression
* @see https://spdx.dev/learn/handling-license-info/
**/
function isSpdxLicenseExpression(license) {
const licenseLoweCase = (license || "").toLowerCase();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we rename this variable to licenseLowerCase or reuse the same license variable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will change it.

if (!licenseLoweCase) {
Copy link
Collaborator

@prabhu prabhu Apr 13, 2024

Choose a reason for hiding this comment

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

This condition will always be false since we substitute empty value above.

Copy link
Contributor Author

@validide validide Apr 13, 2024

Choose a reason for hiding this comment

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

If the function receives an undefined/null value we will have an empty string here. The empty string is "falsy". I was thinking it's not worth it to do all the logic on an empty string.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh yeah. For some reason, I forgot this falsy thing and assumed only python behaves this way. Another learning for today!

return false;
}

if (
spdxLicenseExpressionOp.some((op) => {
Copy link
Collaborator

@prabhu prabhu Apr 13, 2024

Choose a reason for hiding this comment

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

Can this be simplified to check for the characters space and open bracket ( using regex?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should it check for the existence of both characters or at least one? I have see license expressions without the ( character.

Copy link
Collaborator

Choose a reason for hiding this comment

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

You are correct. I meant either.

return licenseLoweCase.includes(op);
})
) {
return true;
}

if (licenseLoweCase.endsWith("+")) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Interesting!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is for the "GPL-2.0+" (or latter) scenario. If this would be part of a more complex case it would be covered by the check for (space) or (.

return true; // GPL-2.0+ means GPL-2.0 or any later version, at the licensee’s option.
}

if (licenseLoweCase.includes("(") && licenseLoweCase.includes(")")) {
return true; // (MIT) means MIT license. It is unlikely to be used in SPDX license expression but just to be safe.
}

return false;
}

/**
* Performs a lookup + validation of the license specified in the
* package. If the license is a valid SPDX license ID, set the 'id'
Expand All @@ -286,43 +326,55 @@ export function getLicenses(pkg) {
if (!Array.isArray(license)) {
license = [license];
}
return license
.map((l) => {
let licenseContent = {};
if (typeof l === "string" || l instanceof String) {
if (
spdxLicenses.some((v) => {
return l === v;
})
) {
licenseContent.id = l;
licenseContent.url = "https://opensource.org/licenses/" + l;
} else if (l.startsWith("http")) {
const knownLicense = getKnownLicense(l, pkg);
if (knownLicense) {
licenseContent.id = knownLicense.id;
licenseContent.name = knownLicense.name;
}
// We always need a name to avoid validation errors
// Issue: #469
if (!licenseContent.name && !licenseContent.id) {
licenseContent.name = "CUSTOM";
}
licenseContent.url = l;
} else {
licenseContent.name = l;
}
} else if (Object.keys(l).length) {
licenseContent = l;
const licensesAndExpressions = license.map((l) => {
let licenseContent = {};
if (typeof l === "string" || l instanceof String) {
if (
spdxLicenses.some((v) => {
return l === v;
})
) {
licenseContent.id = l;
licenseContent.url = "https://opensource.org/licenses/" + l;
} else if (l.startsWith("http")) {
const knownLicense = getKnownLicense(l, pkg);
if (knownLicense) {
licenseContent.id = knownLicense.id;
licenseContent.name = knownLicense.name;
}
// We always need a name to avoid validation errors
// Issue: #469
if (!licenseContent.name && !licenseContent.id) {
licenseContent.name = "CUSTOM";
}
licenseContent.url = l;
} else if (isSpdxLicenseExpression(l)) {
licenseContent.expression = l;
} else {
return undefined;
}
if (!licenseContent.id) {
addLicenseText(pkg, l, licenseContent);
licenseContent.name = l;
}
return licenseContent;
})
.map((l) => ({ license: l }));
} else if (Object.keys(l).length) {
licenseContent = l;
} else {
return undefined;
}
if (!licenseContent.id) {
addLicenseText(pkg, l, licenseContent);
}
return licenseContent;
});

const expressions = licensesAndExpressions.filter((f) => {
return f.expression;
});
if (expressions.length > 1) {
console.warn("multiple license expressions found", expressions);
return [{ expression: expressions[0].expression }];
} else if (expressions.length === 1) {
return [{ expression: expressions[0].expression }];
} else {
return licensesAndExpressions.map((l) => ({ license: l }));
}
} else {
const knownLicense = getKnownLicense(undefined, pkg);
if (knownLicense) {
Expand Down
40 changes: 40 additions & 0 deletions utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2058,6 +2058,46 @@ test("get licenses", () => {
}
}
]);

licenses = getLicenses({
license: "GPL-2.0+"
});
expect(licenses).toEqual([
{
license: {
id: "GPL-2.0+",
url: "https://opensource.org/licenses/GPL-2.0+"
}
}
]);

licenses = getLicenses({
license: "(MIT or Apache-2.0)"
});
expect(licenses).toEqual([
{
expression: "(MIT or Apache-2.0)"
}
]);

// In case this is not a known license in the current build but it is a valid SPDX license expression
licenses = getLicenses({
license: "NOT-GPL-2.1+"
});
expect(licenses).toEqual([
{
expression: "NOT-GPL-2.1+"
}
]);

licenses = getLicenses({
license: "GPL-3.0-only WITH Classpath-exception-2.0"
});
expect(licenses).toEqual([
{
expression: "GPL-3.0-only WITH Classpath-exception-2.0"
}
]);
});

test("parsePkgJson", async () => {
Expand Down