diff --git a/autotests/kaboutdatatest.cpp b/autotests/kaboutdatatest.cpp --- a/autotests/kaboutdatatest.cpp +++ b/autotests/kaboutdatatest.cpp @@ -1,5 +1,6 @@ /* * Copyright 2008 Friedrich W. H. Kossebau + * Copyright 2017 Harald Sitter * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public @@ -38,6 +39,9 @@ void testCopying(); void testKAboutDataOrganizationDomain(); + + void testLicenseSPDXID(); + void testLicenseOrLater(); }; static const char AppName[] = "app"; @@ -284,15 +288,17 @@ { KAboutData aboutData2(AppName, QLatin1String(ProgramName), Version, QLatin1String(ShortDescription), KAboutLicense::GPL_V3); - aboutData2.addLicense(KAboutLicense::GPL_V2); + aboutData2.addLicense(KAboutLicense::GPL_V2, KAboutLicense::OrLaterVersions); aboutData = aboutData2; } QList licenses = aboutData.licenses(); QCOMPARE(licenses.count(), 2); QCOMPARE(licenses.at(0).key(), KAboutLicense::GPL_V3); + QCOMPARE(aboutData.licenses().at(0).spdx(), QStringLiteral("GPL-3.0")); // check it doesn't crash QVERIFY(!licenses.at(0).text().isEmpty()); QCOMPARE(licenses.at(1).key(), KAboutLicense::GPL_V2); + QCOMPARE(aboutData.licenses().at(1).spdx(), QStringLiteral("GPL-2.0+")); // check it doesn't crash QVERIFY(!licenses.at(1).text().isEmpty()); } @@ -308,6 +314,36 @@ QCOMPARE(aboutData.desktopFileName(), QStringLiteral("foo.bar.application")); } +void KAboutDataTest::testLicenseSPDXID() +{ + // Input with + should output with +. + auto license = KAboutLicense::byKeyword(QStringLiteral("GPLv2+")); + QCOMPARE(license.spdx(), QStringLiteral("GPL-2.0+")); + // Input without should output without. + license = KAboutLicense::byKeyword(QStringLiteral("GPLv2")); + QCOMPARE(license.spdx(), QStringLiteral("GPL-2.0")); +} + +void KAboutDataTest::testLicenseOrLater() +{ + // For kaboutdata we can replace the license with an orLater version. Or add a second one. + KAboutData aboutData(AppName, QLatin1String(ProgramName), Version, + QLatin1String(ShortDescription), KAboutLicense::GPL_V2); + QCOMPARE(aboutData.licenses().at(0).spdx(), QStringLiteral("GPL-2.0")); + aboutData.setLicense(KAboutLicense::GPL_V2, KAboutLicense::OrLaterVersions); + QCOMPARE(aboutData.licenses().at(0).spdx(), QStringLiteral("GPL-2.0+")); + aboutData.addLicense(KAboutLicense::LGPL_V3, KAboutLicense::OrLaterVersions); + bool foundLGPL = false; + for (auto license : aboutData.licenses()) { + if (license.key() == KAboutLicense::LGPL_V3) { + QCOMPARE(license.spdx(), QStringLiteral("LGPL-3.0+")); + foundLGPL = true; + break; + } + } + QCOMPARE(foundLGPL, true); +} + QTEST_MAIN(KAboutDataTest) #include "kaboutdatatest.moc" diff --git a/src/lib/kaboutdata.h b/src/lib/kaboutdata.h --- a/src/lib/kaboutdata.h +++ b/src/lib/kaboutdata.h @@ -4,6 +4,7 @@ * Copyright (C) 2008 Friedrich W. H. Kossebau * Copyright (C) 2010 Teo Mrnjavac * Copyright (C) 2013 David Faure + * Copyright (C) 2017 Harald Sitter * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public @@ -206,6 +207,14 @@ }; /** + * Whether later versions of the license are allowed. + */ + enum VersionRestriction { + OnlyThisVersion, + OrLaterVersions + }; + + /** * Copy constructor. Performs a deep copy. * @param other object to copy */ @@ -243,6 +252,23 @@ KAboutLicense::LicenseKey key() const; /** + * Returns the SPDX license expression of this license. + * If the underlying license cannot be expressed as a SPDX expression a null string is returned. + * + * @note SPDX expression are expansive constructs. If you parse the return value, do it in a + * SPDX specification compliant manner by splitting on whitespaces to discard unwanted + * informationor or by using a complete SPDX license expression parser. + * @note SPDX identifiers are case-insensitive. Do not use case-senstivie checks on the return + * value. + * @see https://spdx.org/licenses + * @return SPDX license expression or QString() if the license has no identifier. Compliant + * with SPDX 2.1. + * + * @since 5.37 + */ + QString spdx() const; + + /** * Fetch a known license by a keyword. * * Frequently the license data is provided by a terse keyword-like string, @@ -269,6 +295,12 @@ /** * @internal Used by KAboutData to construct a predefined license. */ + explicit KAboutLicense(enum KAboutLicense::LicenseKey licenseType, + enum KAboutLicense::VersionRestriction versionRestriction, + const KAboutData *aboutData); + /** + * @internal Used by KAboutData to construct a predefined license. + */ explicit KAboutLicense(enum KAboutLicense::LicenseKey licenseType, const KAboutData *aboutData); /** * @internal Used by KAboutData to construct a KAboutLicense @@ -715,6 +747,19 @@ KAboutData &setLicense(KAboutLicense::LicenseKey licenseKey); /** + * Defines the license identifier. + * + * @param licenseKey The license identifier. + * @param versionRestriction Whether later versions of the license are also allowed. + * e.g. licensed under "GPL 2.0 or at your option later versions" would be OrLaterVersions. + * @see addLicenseText, setLicenseText, setLicenseTextFile + * + * @since 5.37 + */ + KAboutData &setLicense(KAboutLicense::LicenseKey licenseKey, + KAboutLicense::VersionRestriction versionRestriction); + + /** * Adds a license identifier. * * If there is only one unknown license set, e.g. by using the default @@ -726,6 +771,22 @@ KAboutData &addLicense(KAboutLicense::LicenseKey licenseKey); /** + * Adds a license identifier. + * + * If there is only one unknown license set, e.g. by using the default + * parameter in the constructor, that one is replaced. + * + * @param licenseKey The license identifier. + * @param versionRestriction Whether later versions of the license are also allowed. + * e.g. licensed under "GPL 2.0 or at your option later versions" would be OrLaterVersions. + * @see setLicenseText, addLicenseText, addLicenseTextFile + * + * @since 5.37 + */ + KAboutData &addLicense(KAboutLicense::LicenseKey licenseKey, + KAboutLicense::VersionRestriction versionRestriction); + + /** * Defines the copyright statement to show when displaying the license. * * @param copyrightStatement A copyright statement, that can look like diff --git a/src/lib/kaboutdata.cpp b/src/lib/kaboutdata.cpp --- a/src/lib/kaboutdata.cpp +++ b/src/lib/kaboutdata.cpp @@ -4,6 +4,7 @@ * Copyright (C) 2006 Nicolas GOUTTE * Copyright (C) 2008 Friedrich W. H. Kossebau * Copyright (C) 2010 Teo Mrnjavac + * Copyright (C) 2017 Harald Sitter * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public @@ -133,27 +134,27 @@ class KAboutLicense::Private : public QSharedData { public: - Private(LicenseKey licenseType, const KAboutData *aboutData); - Private(const KAboutData *aboutData); + Private(LicenseKey licenseType, + VersionRestriction versionRestriction, + const KAboutData *aboutData); Private(const Private &other); -public: - LicenseKey _licenseKey; - QString _licenseText; - QString _pathToLicenseTextFile; + + QString spdxID() const; + + LicenseKey _licenseKey; + QString _licenseText; + QString _pathToLicenseTextFile; + VersionRestriction _versionRestriction; // needed for access to the possibly changing copyrightStatement() const KAboutData *_aboutData; }; -KAboutLicense::Private::Private(LicenseKey licenseType, const KAboutData *aboutData) +KAboutLicense::Private::Private(LicenseKey licenseType, + VersionRestriction versionRestriction, + const KAboutData *aboutData) : QSharedData(), _licenseKey(licenseType), - _aboutData(aboutData) -{ -} - -KAboutLicense::Private::Private(const KAboutData *aboutData) - : QSharedData(), - _licenseKey(Unknown), + _versionRestriction(versionRestriction), _aboutData(aboutData) { } @@ -163,16 +164,52 @@ _licenseKey(other._licenseKey), _licenseText(other._licenseText), _pathToLicenseTextFile(other._pathToLicenseTextFile), + _versionRestriction(other._versionRestriction), _aboutData(other._aboutData) {} +QString KAboutLicense::Private::spdxID() const +{ + switch (_licenseKey) { + case KAboutLicense::GPL_V2: + return QStringLiteral("GPL-2.0"); + case KAboutLicense::LGPL_V2: + return QStringLiteral("LGPL-2.0"); + case KAboutLicense::BSDL: + return QStringLiteral("BSD-2-Clause"); + case KAboutLicense::Artistic: + return QStringLiteral("Artistic-1.0"); + case KAboutLicense::QPL_V1_0: + return QStringLiteral("QPL-1.0"); + case KAboutLicense::GPL_V3: + return QStringLiteral("GPL-3.0"); + case KAboutLicense::LGPL_V3: + return QStringLiteral("LGPL-3.0"); + case KAboutLicense::LGPL_V2_1: + return QStringLiteral("LGPL-2.1"); + case KAboutLicense::Custom: + case KAboutLicense::File: + case KAboutLicense::Unknown: + return QString(); + } + return QString(); +} + +KAboutLicense::KAboutLicense(LicenseKey licenseType, + VersionRestriction versionRestriction, + const KAboutData *aboutData) + : d(new Private(licenseType, versionRestriction, aboutData)) +{ + +} + KAboutLicense::KAboutLicense(LicenseKey licenseType, const KAboutData *aboutData) - : d(new Private(licenseType, aboutData)) + : d(new Private(licenseType, OnlyThisVersion, aboutData)) { } KAboutLicense::KAboutLicense(const KAboutData *aboutData) - : d(new Private(aboutData)) + : d(new Private(Unknown, OnlyThisVersion, aboutData)) { } @@ -280,6 +317,26 @@ return result; } +QString KAboutLicense::spdx() const +{ + // SPDX licenses are comprised of an identifier (e.g. GPL-2.0), an optional + to denote 'or + // later versions' and optional ' WITH $exception' to denote standardized exceptions from the + // core license. As we do not offer exceptions we effectively only return GPL-2.0 or GPL-2.0+, + // this may change in the future. To that end the documentation makes no assertations about the + // actual content of the SPDX license expression we return. + // Expressions can in theory also contain AND, OR and () to build constructs involving more than + // one license. As this is outside the scope of a single license object we'll ignore this here + // for now. + // The expecation is that the return value is only run through spec-compliant parsers, so this + // can potentially be changed. + + auto id = d->spdxID(); + if (id.isNull()) { // Guard against potential future changes which would allow 'Foo+' as input. + return id; + } + return d->_versionRestriction == OrLaterVersions ? id.append(QLatin1Char('+')) : id; +} + QString KAboutLicense::name(KAboutLicense::NameFormat formatName) const { QString licenseShort; @@ -377,7 +434,8 @@ LicenseKey license = licenseDict.value(keyword.toLatin1(), KAboutLicense::Custom); - return KAboutLicense(license, nullptr); + auto restriction = keyword.endsWith(QLatin1Char('+')) ? OrLaterVersions : OnlyThisVersion; + return KAboutLicense(license, restriction, nullptr); } class KAboutData::Private @@ -645,18 +703,30 @@ KAboutData &KAboutData::setLicense(KAboutLicense::LicenseKey licenseKey) { - d->_licenseList[0] = KAboutLicense(licenseKey, this); + return setLicense(licenseKey, KAboutLicense::OnlyThisVersion); +} + +KAboutData &KAboutData::setLicense(KAboutLicense::LicenseKey licenseKey, + KAboutLicense::VersionRestriction versionRestriction) +{ + d->_licenseList[0] = KAboutLicense(licenseKey, versionRestriction, this); return *this; } KAboutData &KAboutData::addLicense(KAboutLicense::LicenseKey licenseKey) { + return addLicense(licenseKey, KAboutLicense::OnlyThisVersion); +} + +KAboutData &KAboutData::addLicense(KAboutLicense::LicenseKey licenseKey, + KAboutLicense::VersionRestriction versionRestriction) +{ // if the default license is unknown, overwrite instead of append KAboutLicense &firstLicense = d->_licenseList[0]; if (d->_licenseList.count() == 1 && firstLicense.d->_licenseKey == KAboutLicense::Unknown) { - firstLicense = KAboutLicense(licenseKey, this); + firstLicense = KAboutLicense(licenseKey, versionRestriction, this); } else { - d->_licenseList.append(KAboutLicense(licenseKey, this)); + d->_licenseList.append(KAboutLicense(licenseKey, versionRestriction, this)); } return *this; }