From 90d9454d1e431277bf92e57cecae28f056662a3f Mon Sep 17 00:00:00 2001
From: Rylan Polster <rslpolster@gmail.com>
Date: Tue, 18 Aug 2020 10:56:54 -0400
Subject: [PATCH] utils/spdx: add support for complex expressions

Co-authored-by: Seeker <meaningseeking@protonmail.com>
---
 .github/workflows/spdx.yml                    |   2 +-
 Library/Homebrew/config.rb                    |   3 +
 .../Homebrew/data/spdx/spdx_exceptions.json   | 466 ++++++++++++++++++
 .../{spdx.json => spdx/spdx_licenses.json}    |   0
 .../Homebrew/dev-cmd/update-license-data.rb   |   6 +-
 Library/Homebrew/test/support/lib/config.rb   |   3 +
 Library/Homebrew/test/utils/spdx_spec.rb      | 283 ++++++++++-
 Library/Homebrew/utils/spdx.rb                | 154 +++++-
 8 files changed, 900 insertions(+), 17 deletions(-)
 create mode 100644 Library/Homebrew/data/spdx/spdx_exceptions.json
 rename Library/Homebrew/data/{spdx.json => spdx/spdx_licenses.json} (100%)

diff --git a/.github/workflows/spdx.yml b/.github/workflows/spdx.yml
index 6cd9a5739f..1126ced451 100644
--- a/.github/workflows/spdx.yml
+++ b/.github/workflows/spdx.yml
@@ -26,7 +26,7 @@ jobs:
         run: |
           cd "$GITHUB_WORKSPACE/Library/Homebrew"
           if brew update-license-data --commit --fail-if-not-changed; then
-            SPDX_VERSION=$(jq -er .licenseListVersion data/spdx.json)
+            SPDX_VERSION=$(jq -er .licenseListVersion data/spdx/spdx_licenses.json)
             if ! git ls-remote --exit-code --heads origin "spdx-$SPDX_VERSION"; then
               git checkout -b "spdx-$SPDX_VERSION"
               git push origin "spdx-$SPDX_VERSION"
diff --git a/Library/Homebrew/config.rb b/Library/Homebrew/config.rb
index 30d39094de..e4514dfabd 100644
--- a/Library/Homebrew/config.rb
+++ b/Library/Homebrew/config.rb
@@ -29,6 +29,9 @@ HOMEBREW_LIBRARY = Pathname.new(get_env_or_raise("HOMEBREW_LIBRARY")).freeze
 # Where shim scripts for various build and SCM tools are stored
 HOMEBREW_SHIMS_PATH = (HOMEBREW_LIBRARY/"Homebrew/shims").freeze
 
+# Where external data that has been incorporated into Homebrew is stored
+HOMEBREW_DATA_PATH = (HOMEBREW_LIBRARY/"Homebrew/data").freeze
+
 # Where we store symlinks to currently linked kegs
 HOMEBREW_LINKED_KEGS = (HOMEBREW_PREFIX/"var/homebrew/linked").freeze
 
diff --git a/Library/Homebrew/data/spdx/spdx_exceptions.json b/Library/Homebrew/data/spdx/spdx_exceptions.json
new file mode 100644
index 0000000000..9efd4ea6d7
--- /dev/null
+++ b/Library/Homebrew/data/spdx/spdx_exceptions.json
@@ -0,0 +1,466 @@
+{
+  "licenseListVersion": "3.10",
+  "releaseDate": "2020-08-03",
+  "exceptions": [
+    {
+      "reference": "./GCC-exception-2.0.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/GCC-exception-2.0.json",
+      "referenceNumber": "1",
+      "name": "GCC Runtime Library exception 2.0",
+      "seeAlso": [
+        "https://gcc.gnu.org/git/?p\u003dgcc.git;a\u003dblob;f\u003dgcc/libgcc1.c;h\u003d762f5143fc6eed57b6797c82710f3538aa52b40b;hb\u003dcb143a3ce4fb417c68f5fa2691a1b1b1053dfba9#l10"
+      ],
+      "licenseExceptionId": "GCC-exception-2.0"
+    },
+    {
+      "reference": "./openvpn-openssl-exception.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/openvpn-openssl-exception.json",
+      "referenceNumber": "2",
+      "name": "OpenVPN OpenSSL Exception",
+      "seeAlso": [
+        "http://openvpn.net/index.php/license.html"
+      ],
+      "licenseExceptionId": "openvpn-openssl-exception"
+    },
+    {
+      "reference": "./Nokia-Qt-exception-1.1.html",
+      "isDeprecatedLicenseId": true,
+      "detailsUrl": "http://spdx.org/licenses/Nokia-Qt-exception-1.1.json",
+      "referenceNumber": "3",
+      "name": "Nokia Qt LGPL exception 1.1",
+      "seeAlso": [
+        "https://www.keepassx.org/dev/projects/keepassx/repository/revisions/b8dfb9cc4d5133e0f09cd7533d15a4f1c19a40f2/entry/LICENSE.NOKIA-LGPL-EXCEPTION"
+      ],
+      "licenseExceptionId": "Nokia-Qt-exception-1.1"
+    },
+    {
+      "reference": "./GPL-3.0-linking-exception.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/GPL-3.0-linking-exception.json",
+      "referenceNumber": "4",
+      "name": "GPL-3.0 Linking Exception",
+      "seeAlso": [
+        "https://www.gnu.org/licenses/gpl-faq.en.html#GPLIncompatibleLibs"
+      ],
+      "licenseExceptionId": "GPL-3.0-linking-exception"
+    },
+    {
+      "reference": "./Fawkes-Runtime-exception.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/Fawkes-Runtime-exception.json",
+      "referenceNumber": "5",
+      "name": "Fawkes Runtime Exception",
+      "seeAlso": [
+        "http://www.fawkesrobotics.org/about/license/"
+      ],
+      "licenseExceptionId": "Fawkes-Runtime-exception"
+    },
+    {
+      "reference": "./u-boot-exception-2.0.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/u-boot-exception-2.0.json",
+      "referenceNumber": "6",
+      "name": "U-Boot exception 2.0",
+      "seeAlso": [
+        "http://git.denx.de/?p\u003du-boot.git;a\u003dblob;f\u003dLicenses/Exceptions"
+      ],
+      "licenseExceptionId": "u-boot-exception-2.0"
+    },
+    {
+      "reference": "./PS-or-PDF-font-exception-20170817.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/PS-or-PDF-font-exception-20170817.json",
+      "referenceNumber": "7",
+      "name": "PS/PDF font exception (2017-08-17)",
+      "seeAlso": [
+        "https://github.com/ArtifexSoftware/urw-base35-fonts/blob/65962e27febc3883a17e651cdb23e783668c996f/LICENSE"
+      ],
+      "licenseExceptionId": "PS-or-PDF-font-exception-20170817"
+    },
+    {
+      "reference": "./gnu-javamail-exception.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/gnu-javamail-exception.json",
+      "referenceNumber": "8",
+      "name": "GNU JavaMail exception",
+      "seeAlso": [
+        "http://www.gnu.org/software/classpathx/javamail/javamail.html"
+      ],
+      "licenseExceptionId": "gnu-javamail-exception"
+    },
+    {
+      "reference": "./LGPL-3.0-linking-exception.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/LGPL-3.0-linking-exception.json",
+      "referenceNumber": "9",
+      "name": "LGPL-3.0 Linking Exception",
+      "seeAlso": [
+        "https://raw.githubusercontent.com/go-xmlpath/xmlpath/v2/LICENSE",
+        "https://github.com/goamz/goamz/blob/master/LICENSE",
+        "https://github.com/juju/errors/blob/master/LICENSE"
+      ],
+      "licenseExceptionId": "LGPL-3.0-linking-exception"
+    },
+    {
+      "reference": "./DigiRule-FOSS-exception.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/DigiRule-FOSS-exception.json",
+      "referenceNumber": "10",
+      "name": "DigiRule FOSS License Exception",
+      "seeAlso": [
+        "http://www.digirulesolutions.com/drupal/foss"
+      ],
+      "licenseExceptionId": "DigiRule-FOSS-exception"
+    },
+    {
+      "reference": "./LLVM-exception.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/LLVM-exception.json",
+      "referenceNumber": "11",
+      "name": "LLVM Exception",
+      "seeAlso": [
+        "http://llvm.org/foundation/relicensing/LICENSE.txt"
+      ],
+      "licenseExceptionId": "LLVM-exception"
+    },
+    {
+      "reference": "./Linux-syscall-note.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/Linux-syscall-note.json",
+      "referenceNumber": "12",
+      "name": "Linux Syscall Note",
+      "seeAlso": [
+        "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/COPYING"
+      ],
+      "licenseExceptionId": "Linux-syscall-note"
+    },
+    {
+      "reference": "./GPL-3.0-linking-source-exception.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/GPL-3.0-linking-source-exception.json",
+      "referenceNumber": "13",
+      "name": "GPL-3.0 Linking Exception (with Corresponding Source)",
+      "seeAlso": [
+        "https://www.gnu.org/licenses/gpl-faq.en.html#GPLIncompatibleLibs",
+        "https://github.com/mirror/wget/blob/master/src/http.c#L20"
+      ],
+      "licenseExceptionId": "GPL-3.0-linking-source-exception"
+    },
+    {
+      "reference": "./Qwt-exception-1.0.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/Qwt-exception-1.0.json",
+      "referenceNumber": "14",
+      "name": "Qwt exception 1.0",
+      "seeAlso": [
+        "http://qwt.sourceforge.net/qwtlicense.html"
+      ],
+      "licenseExceptionId": "Qwt-exception-1.0"
+    },
+    {
+      "reference": "./389-exception.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/389-exception.json",
+      "referenceNumber": "15",
+      "name": "389 Directory Server Exception",
+      "seeAlso": [
+        "http://directory.fedoraproject.org/wiki/GPL_Exception_License_Text"
+      ],
+      "licenseExceptionId": "389-exception"
+    },
+    {
+      "reference": "./mif-exception.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/mif-exception.json",
+      "referenceNumber": "16",
+      "name": "Macros and Inline Functions Exception",
+      "seeAlso": [
+        "http://www.scs.stanford.edu/histar/src/lib/cppsup/exception",
+        "http://dev.bertos.org/doxygen/",
+        "https://www.threadingbuildingblocks.org/licensing"
+      ],
+      "licenseExceptionId": "mif-exception"
+    },
+    {
+      "reference": "./eCos-exception-2.0.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/eCos-exception-2.0.json",
+      "referenceNumber": "17",
+      "name": "eCos exception 2.0",
+      "seeAlso": [
+        "http://ecos.sourceware.org/license-overview.html"
+      ],
+      "licenseExceptionId": "eCos-exception-2.0"
+    },
+    {
+      "reference": "./CLISP-exception-2.0.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/CLISP-exception-2.0.json",
+      "referenceNumber": "18",
+      "name": "CLISP exception 2.0",
+      "seeAlso": [
+        "http://sourceforge.net/p/clisp/clisp/ci/default/tree/COPYRIGHT"
+      ],
+      "licenseExceptionId": "CLISP-exception-2.0"
+    },
+    {
+      "reference": "./Bison-exception-2.2.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/Bison-exception-2.2.json",
+      "referenceNumber": "19",
+      "name": "Bison exception 2.2",
+      "seeAlso": [
+        "http://git.savannah.gnu.org/cgit/bison.git/tree/data/yacc.c?id\u003d193d7c7054ba7197b0789e14965b739162319b5e#n141"
+      ],
+      "licenseExceptionId": "Bison-exception-2.2"
+    },
+    {
+      "reference": "./Libtool-exception.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/Libtool-exception.json",
+      "referenceNumber": "20",
+      "name": "Libtool Exception",
+      "seeAlso": [
+        "http://git.savannah.gnu.org/cgit/libtool.git/tree/m4/libtool.m4"
+      ],
+      "licenseExceptionId": "Libtool-exception"
+    },
+    {
+      "reference": "./LZMA-exception.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/LZMA-exception.json",
+      "referenceNumber": "21",
+      "name": "LZMA exception",
+      "seeAlso": [
+        "http://nsis.sourceforge.net/Docs/AppendixI.html#I.6"
+      ],
+      "licenseExceptionId": "LZMA-exception"
+    },
+    {
+      "reference": "./OpenJDK-assembly-exception-1.0.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/OpenJDK-assembly-exception-1.0.json",
+      "referenceNumber": "22",
+      "name": "OpenJDK Assembly exception 1.0",
+      "seeAlso": [
+        "http://openjdk.java.net/legal/assembly-exception.html"
+      ],
+      "licenseExceptionId": "OpenJDK-assembly-exception-1.0"
+    },
+    {
+      "reference": "./Font-exception-2.0.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/Font-exception-2.0.json",
+      "referenceNumber": "23",
+      "name": "Font exception 2.0",
+      "seeAlso": [
+        "http://www.gnu.org/licenses/gpl-faq.html#FontException"
+      ],
+      "licenseExceptionId": "Font-exception-2.0"
+    },
+    {
+      "reference": "./OCaml-LGPL-linking-exception.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/OCaml-LGPL-linking-exception.json",
+      "referenceNumber": "24",
+      "name": "OCaml LGPL Linking Exception",
+      "seeAlso": [
+        "https://caml.inria.fr/ocaml/license.en.html"
+      ],
+      "licenseExceptionId": "OCaml-LGPL-linking-exception"
+    },
+    {
+      "reference": "./GCC-exception-3.1.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/GCC-exception-3.1.json",
+      "referenceNumber": "25",
+      "name": "GCC Runtime Library exception 3.1",
+      "seeAlso": [
+        "http://www.gnu.org/licenses/gcc-exception-3.1.html"
+      ],
+      "licenseExceptionId": "GCC-exception-3.1"
+    },
+    {
+      "reference": "./Bootloader-exception.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/Bootloader-exception.json",
+      "referenceNumber": "26",
+      "name": "Bootloader Distribution Exception",
+      "seeAlso": [
+        "https://github.com/pyinstaller/pyinstaller/blob/develop/COPYING.txt"
+      ],
+      "licenseExceptionId": "Bootloader-exception"
+    },
+    {
+      "reference": "./SHL-2.0.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/SHL-2.0.json",
+      "referenceNumber": "27",
+      "name": "Solderpad Hardware License v2.0",
+      "seeAlso": [
+        "https://solderpad.org/licenses/SHL-2.0/"
+      ],
+      "licenseExceptionId": "SHL-2.0"
+    },
+    {
+      "reference": "./Classpath-exception-2.0.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/Classpath-exception-2.0.json",
+      "referenceNumber": "28",
+      "name": "Classpath exception 2.0",
+      "seeAlso": [
+        "http://www.gnu.org/software/classpath/license.html",
+        "https://fedoraproject.org/wiki/Licensing/GPL_Classpath_Exception"
+      ],
+      "licenseExceptionId": "Classpath-exception-2.0"
+    },
+    {
+      "reference": "./Swift-exception.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/Swift-exception.json",
+      "referenceNumber": "29",
+      "name": "Swift Exception",
+      "seeAlso": [
+        "https://swift.org/LICENSE.txt",
+        "https://github.com/apple/swift-package-manager/blob/7ab2275f447a5eb37497ed63a9340f8a6d1e488b/LICENSE.txt#L205"
+      ],
+      "licenseExceptionId": "Swift-exception"
+    },
+    {
+      "reference": "./Autoconf-exception-2.0.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/Autoconf-exception-2.0.json",
+      "referenceNumber": "30",
+      "name": "Autoconf exception 2.0",
+      "seeAlso": [
+        "http://ac-archive.sourceforge.net/doc/copyright.html",
+        "http://ftp.gnu.org/gnu/autoconf/autoconf-2.59.tar.gz"
+      ],
+      "licenseExceptionId": "Autoconf-exception-2.0"
+    },
+    {
+      "reference": "./FLTK-exception.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/FLTK-exception.json",
+      "referenceNumber": "31",
+      "name": "FLTK exception",
+      "seeAlso": [
+        "http://www.fltk.org/COPYING.php"
+      ],
+      "licenseExceptionId": "FLTK-exception"
+    },
+    {
+      "reference": "./freertos-exception-2.0.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/freertos-exception-2.0.json",
+      "referenceNumber": "32",
+      "name": "FreeRTOS Exception 2.0",
+      "seeAlso": [
+        "https://web.archive.org/web/20060809182744/http://www.freertos.org/a00114.html"
+      ],
+      "licenseExceptionId": "freertos-exception-2.0"
+    },
+    {
+      "reference": "./Universal-FOSS-exception-1.0.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/Universal-FOSS-exception-1.0.json",
+      "referenceNumber": "33",
+      "name": "Universal FOSS Exception, Version 1.0",
+      "seeAlso": [
+        "https://oss.oracle.com/licenses/universal-foss-exception/"
+      ],
+      "licenseExceptionId": "Universal-FOSS-exception-1.0"
+    },
+    {
+      "reference": "./WxWindows-exception-3.1.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/WxWindows-exception-3.1.json",
+      "referenceNumber": "34",
+      "name": "WxWindows Library Exception 3.1",
+      "seeAlso": [
+        "http://www.opensource.org/licenses/WXwindows"
+      ],
+      "licenseExceptionId": "WxWindows-exception-3.1"
+    },
+    {
+      "reference": "./OCCT-exception-1.0.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/OCCT-exception-1.0.json",
+      "referenceNumber": "35",
+      "name": "Open CASCADE Exception 1.0",
+      "seeAlso": [
+        "http://www.opencascade.com/content/licensing"
+      ],
+      "licenseExceptionId": "OCCT-exception-1.0"
+    },
+    {
+      "reference": "./Autoconf-exception-3.0.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/Autoconf-exception-3.0.json",
+      "referenceNumber": "36",
+      "name": "Autoconf exception 3.0",
+      "seeAlso": [
+        "http://www.gnu.org/licenses/autoconf-exception-3.0.html"
+      ],
+      "licenseExceptionId": "Autoconf-exception-3.0"
+    },
+    {
+      "reference": "./i2p-gpl-java-exception.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/i2p-gpl-java-exception.json",
+      "referenceNumber": "37",
+      "name": "i2p GPL+Java Exception",
+      "seeAlso": [
+        "http://geti2p.net/en/get-involved/develop/licenses#java_exception"
+      ],
+      "licenseExceptionId": "i2p-gpl-java-exception"
+    },
+    {
+      "reference": "./GPL-CC-1.0.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/GPL-CC-1.0.json",
+      "referenceNumber": "38",
+      "name": "GPL Cooperation Commitment 1.0",
+      "seeAlso": [
+        "https://github.com/gplcc/gplcc/blob/master/Project/COMMITMENT",
+        "https://gplcc.github.io/gplcc/Project/README-PROJECT.html"
+      ],
+      "licenseExceptionId": "GPL-CC-1.0"
+    },
+    {
+      "reference": "./Qt-LGPL-exception-1.1.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/Qt-LGPL-exception-1.1.json",
+      "referenceNumber": "39",
+      "name": "Qt LGPL exception 1.1",
+      "seeAlso": [
+        "http://code.qt.io/cgit/qt/qtbase.git/tree/LGPL_EXCEPTION.txt"
+      ],
+      "licenseExceptionId": "Qt-LGPL-exception-1.1"
+    },
+    {
+      "reference": "./SHL-2.1.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/SHL-2.1.json",
+      "referenceNumber": "40",
+      "name": "Solderpad Hardware License v2.1",
+      "seeAlso": [
+        "https://solderpad.org/licenses/SHL-2.1/"
+      ],
+      "licenseExceptionId": "SHL-2.1"
+    },
+    {
+      "reference": "./Qt-GPL-exception-1.0.html",
+      "isDeprecatedLicenseId": false,
+      "detailsUrl": "http://spdx.org/licenses/Qt-GPL-exception-1.0.json",
+      "referenceNumber": "41",
+      "name": "Qt GPL exception 1.0",
+      "seeAlso": [
+        "http://code.qt.io/cgit/qt/qtbase.git/tree/LICENSE.GPL3-EXCEPT"
+      ],
+      "licenseExceptionId": "Qt-GPL-exception-1.0"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Library/Homebrew/data/spdx.json b/Library/Homebrew/data/spdx/spdx_licenses.json
similarity index 100%
rename from Library/Homebrew/data/spdx.json
rename to Library/Homebrew/data/spdx/spdx_licenses.json
diff --git a/Library/Homebrew/dev-cmd/update-license-data.rb b/Library/Homebrew/dev-cmd/update-license-data.rb
index 78b9f3e333..dae194f907 100644
--- a/Library/Homebrew/dev-cmd/update-license-data.rb
+++ b/Library/Homebrew/dev-cmd/update-license-data.rb
@@ -28,13 +28,13 @@ module Homebrew
 
     SPDX.download_latest_license_data!
 
-    Homebrew.failed = system("git", "diff", "--stat", "--exit-code", SPDX::JSON_PATH) if args.fail_if_not_changed?
+    Homebrew.failed = system("git", "diff", "--stat", "--exit-code", SPDX::DATA_PATH) if args.fail_if_not_changed?
 
     return unless args.commit?
 
     ohai "git add"
-    safe_system "git", "add", SPDX::JSON_PATH
+    safe_system "git", "add", SPDX::DATA_PATH
     ohai "git commit"
-    system "git", "commit", "--message", "data/spdx.json: update to #{SPDX.latest_tag}"
+    system "git", "commit", "--message", "spdx license data: update to #{SPDX.latest_tag}"
   end
 end
diff --git a/Library/Homebrew/test/support/lib/config.rb b/Library/Homebrew/test/support/lib/config.rb
index a34a660740..a9c6769f70 100644
--- a/Library/Homebrew/test/support/lib/config.rb
+++ b/Library/Homebrew/test/support/lib/config.rb
@@ -19,6 +19,9 @@ end.freeze
 # Paths pointing into the Homebrew code base that persist across test runs
 HOMEBREW_SHIMS_PATH = (HOMEBREW_LIBRARY_PATH/"shims").freeze
 
+# Where external data that has been incorporated into Homebrew is stored
+HOMEBREW_DATA_PATH = (HOMEBREW_LIBRARY_PATH/"data").freeze
+
 require "extend/git_repository"
 
 # Paths redirected to a temporary directory and wiped at the end of the test run
diff --git a/Library/Homebrew/test/utils/spdx_spec.rb b/Library/Homebrew/test/utils/spdx_spec.rb
index e99cc5e610..01fb65107e 100644
--- a/Library/Homebrew/test/utils/spdx_spec.rb
+++ b/Library/Homebrew/test/utils/spdx_spec.rb
@@ -3,30 +3,299 @@
 require "utils/spdx"
 
 describe SPDX do
-  describe ".spdx_data" do
+  describe ".license_data" do
     it "has the license list version" do
-      expect(described_class.spdx_data["licenseListVersion"]).not_to eq(nil)
+      expect(described_class.license_data["licenseListVersion"]).not_to eq(nil)
     end
 
     it "has the release date" do
-      expect(described_class.spdx_data["releaseDate"]).not_to eq(nil)
+      expect(described_class.license_data["releaseDate"]).not_to eq(nil)
     end
 
     it "has licenses" do
-      expect(described_class.spdx_data["licenses"].length).not_to eq(0)
+      expect(described_class.license_data["licenses"].length).not_to eq(0)
+    end
+  end
+
+  describe ".exception_data" do
+    it "has the license list version" do
+      expect(described_class.exception_data["licenseListVersion"]).not_to eq(nil)
+    end
+
+    it "has the release date" do
+      expect(described_class.exception_data["releaseDate"]).not_to eq(nil)
+    end
+
+    it "has exceptions" do
+      expect(described_class.exception_data["exceptions"].length).not_to eq(0)
     end
   end
 
   describe ".download_latest_license_data!", :needs_network do
-    let(:tmp_json_path) { Pathname.new("#{TEST_TMPDIR}/spdx.json") }
+    let(:tmp_json_path) { Pathname.new(TEST_TMPDIR) }
 
     after do
-      FileUtils.rm tmp_json_path
+      FileUtils.rm tmp_json_path/"spdx_licenses.json"
+      FileUtils.rm tmp_json_path/"spdx_exceptions.json"
     end
 
     it "downloads latest license data" do
       described_class.download_latest_license_data! to: tmp_json_path
-      expect(tmp_json_path).to exist
+      expect(tmp_json_path/"spdx_licenses.json").to exist
+      expect(tmp_json_path/"spdx_exceptions.json").to exist
+    end
+  end
+
+  describe ".parse_license_expression" do
+    it "returns a single license" do
+      expect(described_class.parse_license_expression("MIT").first).to eq ["MIT"]
+    end
+
+    it "returns a single license with plus" do
+      expect(described_class.parse_license_expression("Apache-2.0+").first).to eq ["Apache-2.0+"]
+    end
+
+    it "returns multiple licenses with :any" do
+      expect(described_class.parse_license_expression(any_of: ["MIT", "0BSD"]).first).to eq ["MIT", "0BSD"]
+    end
+
+    it "returns multiple licenses with :all" do
+      expect(described_class.parse_license_expression(all_of: ["MIT", "0BSD"]).first).to eq ["MIT", "0BSD"]
+    end
+
+    it "returns multiple licenses with plus" do
+      expect(described_class.parse_license_expression(any_of: ["MIT", "EPL-1.0+"]).first).to eq ["MIT", "EPL-1.0+"]
+    end
+
+    it "returns license and exception" do
+      license_expression = { "MIT" => { with: "LLVM-exception" } }
+      expect(described_class.parse_license_expression(license_expression)).to eq [["MIT"], ["LLVM-exception"]]
+    end
+
+    it "returns licenses and exceptions for compex license expressions" do
+      license_expression = { any_of: [
+        "MIT",
+        :public_domain,
+        all_of: ["0BSD", "Zlib"],
+        "curl" => { with: "LLVM-exception" },
+      ] }
+      result = [["MIT", :public_domain, "0BSD", "Zlib", "curl"], ["LLVM-exception"]]
+      expect(described_class.parse_license_expression(license_expression)).to eq result
+    end
+
+    it "returns :public_domain" do
+      expect(described_class.parse_license_expression(:public_domain).first).to eq [:public_domain]
+    end
+  end
+
+  describe ".valid_license?" do
+    it "returns true for valid license identifier" do
+      expect(described_class.valid_license?("MIT")).to eq true
+    end
+
+    it "returns false for invalid license identifier" do
+      expect(described_class.valid_license?("foo")).to eq false
+    end
+
+    it "returns true for deprecated license identifier" do
+      expect(described_class.valid_license?("GPL-1.0")).to eq true
+    end
+
+    it "returns true for license identifier with plus" do
+      expect(described_class.valid_license?("Apache-2.0+")).to eq true
+    end
+
+    it "returns true for :public_domain" do
+      expect(described_class.valid_license?(:public_domain)).to eq true
+    end
+  end
+
+  describe ".deprecated_license?" do
+    it "returns true for deprecated license identifier" do
+      expect(described_class.deprecated_license?("GPL-1.0")).to eq true
+    end
+
+    it "returns false for non-deprecated license identifier" do
+      expect(described_class.deprecated_license?("MIT")).to eq false
+    end
+
+    it "returns false for invalid license identifier" do
+      expect(described_class.deprecated_license?("foo")).to eq false
+    end
+
+    it "returns false for :public_domain" do
+      expect(described_class.deprecated_license?(:public_domain)).to eq false
+    end
+  end
+
+  describe ".valid_license_exception?" do
+    it "returns true for valid license exception identifier" do
+      expect(described_class.valid_license_exception?("LLVM-exception")).to eq true
+    end
+
+    it "returns false for invalid license exception identifier" do
+      expect(described_class.valid_license_exception?("foo")).to eq false
+    end
+
+    it "returns false for deprecated license exception identifier" do
+      expect(described_class.valid_license_exception?("Nokia-Qt-exception-1.1")).to eq false
+    end
+  end
+
+  describe ".license_expression_to_string" do
+    it "returns a single license" do
+      expect(described_class.license_expression_to_string("MIT")).to eq "MIT"
+    end
+
+    it "returns a single license with plus" do
+      expect(described_class.license_expression_to_string("Apache-2.0+")).to eq "Apache-2.0+"
+    end
+
+    it "returns multiple licenses with :any" do
+      expect(described_class.license_expression_to_string(any_of: ["MIT", "0BSD"])).to eq "MIT or 0BSD"
+    end
+
+    it "returns multiple licenses with :all" do
+      expect(described_class.license_expression_to_string(all_of: ["MIT", "0BSD"])).to eq "MIT and 0BSD"
+    end
+
+    it "returns multiple licenses with plus" do
+      expect(described_class.license_expression_to_string(any_of: ["MIT", "EPL-1.0+"])).to eq "MIT or EPL-1.0+"
+    end
+
+    it "returns license and exception" do
+      license_expression = { "MIT" => { with: "LLVM-exception" } }
+      expect(described_class.license_expression_to_string(license_expression)).to eq "MIT with LLVM-exception"
+    end
+
+    it "returns licenses and exceptions for compex license expressions" do
+      license_expression = { any_of: [
+        "MIT",
+        :public_domain,
+        all_of: ["0BSD", "Zlib"],
+        "curl" => { with: "LLVM-exception" },
+      ] }
+      result = "MIT or Public Domain or (0BSD and Zlib) or (curl with LLVM-exception)"
+      expect(described_class.license_expression_to_string(license_expression)).to eq result
+    end
+
+    it "returns :public_domain" do
+      expect(described_class.license_expression_to_string(:public_domain)).to eq "Public Domain"
+    end
+  end
+
+  describe ".license_version_info_info" do
+    it "returns license without version" do
+      expect(described_class.license_version_info("MIT")).to eq ["MIT"]
+    end
+
+    it "returns :public_domain without version" do
+      expect(described_class.license_version_info(:public_domain)).to eq [:public_domain]
+    end
+
+    it "returns license with version" do
+      expect(described_class.license_version_info("Apache-2.0")).to eq ["Apache", "2.0", false]
+    end
+
+    it "returns license with version and plus" do
+      expect(described_class.license_version_info("Apache-2.0+")).to eq ["Apache", "2.0", true]
+    end
+
+    it "returns more complicated license with version" do
+      expect(described_class.license_version_info("CC-BY-3.0-AT")).to eq ["CC-BY", "3.0", false]
+    end
+
+    it "returns more complicated license with version and plus" do
+      expect(described_class.license_version_info("CC-BY-3.0-AT+")).to eq ["CC-BY", "3.0", true]
+    end
+
+    it "returns license with -only" do
+      expect(described_class.license_version_info("GPL-3.0-only")).to eq ["GPL", "3.0", false]
+    end
+
+    it "returns license with -or-later" do
+      expect(described_class.license_version_info("GPL-3.0-or-later")).to eq ["GPL", "3.0", true]
+    end
+  end
+
+  describe ".licenses_forbid_installation?" do
+    let(:mit_forbidden) { { "MIT" => described_class.license_version_info("MIT") } }
+    let(:epl_1_forbidden) { { "EPL-1.0" => described_class.license_version_info("EPL-1.0") } }
+    let(:epl_1_plus_forbidden) { { "EPL-1.0+" => described_class.license_version_info("EPL-1.0+") } }
+    let(:multiple_forbidden) {
+      {
+        "MIT"  => described_class.license_version_info("MIT"),
+        "0BSD" => described_class.license_version_info("0BSD"),
+      }
+    }
+    let(:any_of_license) { { any_of: ["MIT", "0BSD"] } }
+    let(:all_of_license) { { all_of: ["MIT", "0BSD"] } }
+    let(:license_exception) { { "MIT" => { with: "LLVM-exception" } } }
+
+    it "allows installation with no forbidden licenses" do
+      expect(described_class.licenses_forbid_installation?("MIT", {})).to eq false
+    end
+
+    it "allows installation with non-forbidden license" do
+      expect(described_class.licenses_forbid_installation?("0BSD", mit_forbidden)).to eq false
+    end
+
+    it "forbids installation with forbidden license" do
+      expect(described_class.licenses_forbid_installation?("MIT", mit_forbidden)).to eq true
+    end
+
+    it "allows installation of later license version" do
+      expect(described_class.licenses_forbid_installation?("EPL-2.0", epl_1_forbidden)).to eq false
+    end
+
+    it "forbids installation of later license version with plus in forbidden license list" do
+      expect(described_class.licenses_forbid_installation?("EPL-2.0", epl_1_plus_forbidden)).to eq true
+    end
+
+    it "allows installation when one of the any_of licenses is allowed" do
+      expect(described_class.licenses_forbid_installation?(any_of_license, mit_forbidden)).to eq false
+    end
+
+    it "forbids installation when none of the any_of licenses are allowed" do
+      expect(described_class.licenses_forbid_installation?(any_of_license, multiple_forbidden)).to eq true
+    end
+
+    it "forbids installation when one of the all_of licenses is allowed" do
+      expect(described_class.licenses_forbid_installation?(all_of_license, mit_forbidden)).to eq true
+    end
+
+    it "allows installation with license + exception that aren't forbidden" do
+      expect(described_class.licenses_forbid_installation?(license_exception, epl_1_forbidden)).to eq false
+    end
+
+    it "forbids installation with license + exception that are't forbidden" do
+      expect(described_class.licenses_forbid_installation?(license_exception, mit_forbidden)).to eq true
+    end
+  end
+
+  describe ".forbidden_licenses_include?" do
+    let(:mit_forbidden) { { "MIT" => described_class.license_version_info("MIT") } }
+    let(:epl_1_forbidden) { { "EPL-1.0" => described_class.license_version_info("EPL-1.0") } }
+    let(:epl_1_plus_forbidden) { { "EPL-1.0+" => described_class.license_version_info("EPL-1.0+") } }
+
+    it "returns false with no forbidden licenses" do
+      expect(described_class.forbidden_licenses_include?("MIT", {})).to eq false
+    end
+
+    it "returns false with no matching forbidden licenses" do
+      expect(described_class.forbidden_licenses_include?("MIT", epl_1_forbidden)).to eq false
+    end
+
+    it "returns true with matching license" do
+      expect(described_class.forbidden_licenses_include?("MIT", mit_forbidden)).to eq true
+    end
+
+    it "returns false with later version of forbidden license" do
+      expect(described_class.forbidden_licenses_include?("EPL-2.0", epl_1_forbidden)).to eq false
+    end
+
+    it "returns true with later version of forbidden license with later versions forbidden" do
+      expect(described_class.forbidden_licenses_include?("EPL-2.0", epl_1_plus_forbidden)).to eq true
     end
   end
 end
diff --git a/Library/Homebrew/utils/spdx.rb b/Library/Homebrew/utils/spdx.rb
index ffa2de8054..a52cf6090c 100644
--- a/Library/Homebrew/utils/spdx.rb
+++ b/Library/Homebrew/utils/spdx.rb
@@ -5,19 +5,161 @@ require "utils/github"
 module SPDX
   module_function
 
-  JSON_PATH = (HOMEBREW_LIBRARY_PATH/"data/spdx.json").freeze
+  DATA_PATH = (HOMEBREW_DATA_PATH/"spdx").freeze
   API_URL = "https://api.github.com/repos/spdx/license-list-data/releases/latest"
 
-  def spdx_data
-    @spdx_data ||= JSON.parse(JSON_PATH.read)
+  def license_data
+    @license_data ||= JSON.parse (DATA_PATH/"spdx_licenses.json").read
+  end
+
+  def exception_data
+    @exception_data ||= JSON.parse (DATA_PATH/"spdx_exceptions.json").read
   end
 
   def latest_tag
     @latest_tag ||= GitHub.open_api(API_URL)["tag_name"]
   end
 
-  def download_latest_license_data!(to: JSON_PATH)
-    data_url = "https://raw.githubusercontent.com/spdx/license-list-data/#{latest_tag}/json/licenses.json"
-    curl_download(data_url, to: to, partial: false)
+  def download_latest_license_data!(to: DATA_PATH)
+    data_url = "https://raw.githubusercontent.com/spdx/license-list-data/#{latest_tag}/json/"
+    curl_download("#{data_url}licenses.json", to: to/"spdx_licenses.json", partial: false)
+    curl_download("#{data_url}exceptions.json", to: to/"spdx_exceptions.json", partial: false)
+  end
+
+  def parse_license_expression(license_expression)
+    licenses = []
+    exceptions = []
+
+    case license_expression
+    when String, Symbol
+      licenses.push license_expression
+    when Hash
+      license_expression.each do |key, value|
+        if [:any_of, :all_of].include? key
+          sub_license, sub_exception = parse_license_expression value
+          licenses += sub_license
+          exceptions += sub_exception
+        else
+          licenses.push key
+          exceptions.push value[:with]
+        end
+      end
+    when Array
+      license_expression.each do |license|
+        sub_license, sub_exception = parse_license_expression license
+        licenses += sub_license
+        exceptions += sub_exception
+      end
+    end
+
+    [licenses, exceptions]
+  end
+
+  def valid_license?(license)
+    return true if license == :public_domain
+
+    license = license.delete_suffix "+"
+    license_data["licenses"].any? { |spdx_license| spdx_license["licenseId"] == license }
+  end
+
+  def deprecated_license?(license)
+    return false if license == :public_domain
+    return false unless valid_license?(license)
+
+    license_data["licenses"].none? do |spdx_license|
+      spdx_license["licenseId"] == license && !spdx_license["isDeprecatedLicenseId"]
+    end
+  end
+
+  def valid_license_exception?(exception)
+    exception_data["exceptions"].any? do |spdx_exception|
+      spdx_exception["licenseExceptionId"] == exception && !spdx_exception["isDeprecatedLicenseId"]
+    end
+  end
+
+  def license_expression_to_string(license_expression, bracket: false, hash_type: nil)
+    case license_expression
+    when String
+      license_expression
+    when :public_domain
+      "Public Domain"
+    when Hash
+      expressions = []
+
+      if license_expression.keys.length == 1
+        hash_type = license_expression.keys.first
+        if hash_type.is_a? String
+          expressions.push "#{hash_type} with #{license_expression[hash_type][:with]}"
+        else
+          expressions += license_expression[hash_type].map do |license|
+            license_expression_to_string license, bracket: true, hash_type: hash_type
+          end
+        end
+      else
+        bracket = false
+        license_expression.each do |expression|
+          expressions.push license_expression_to_string(Hash[*expression], bracket: true)
+        end
+      end
+
+      operator = if hash_type == :any_of
+        " or "
+      else
+        " and "
+      end
+
+      if bracket
+        "(#{expressions.join operator})"
+      else
+        expressions.join operator
+      end
+    end
+  end
+
+  def license_version_info(license)
+    return [license] if license == :public_domain
+
+    match = license.match(/-(?<version>[0-9.]+)(?:-.*?)??(?<or_later>\+|-only|-or-later)?$/)
+    return [license] if match.blank?
+
+    license_name = license.split(match[0]).first
+    or_later = match["or_later"].present? && %w[+ -or-later].include?(match["or_later"])
+
+    # [name, version, later versions allowed?]
+    # e.g. GPL-2.0-or-later --> ["GPL", "2.0", true]
+    [license_name, match["version"], or_later]
+  end
+
+  def licenses_forbid_installation?(license_expression, forbidden_licenses)
+    case license_expression
+    when String, Symbol
+      forbidden_licenses_include? license_expression.to_s, forbidden_licenses
+    when Hash
+      key = license_expression.keys.first
+      case key
+      when :any_of
+        license_expression[key].all? { |license| licenses_forbid_installation? license, forbidden_licenses }
+      when :all_of
+        license_expression[key].any? { |license| licenses_forbid_installation? license, forbidden_licenses }
+      else
+        forbidden_licenses_include? key, forbidden_licenses
+      end
+    end
+  end
+
+  def forbidden_licenses_include?(license, forbidden_licenses)
+    return true if forbidden_licenses.key? license
+
+    name, version, = license_version_info license
+
+    forbidden_licenses.each do |_, license_info|
+      forbidden_name, forbidden_version, forbidden_or_later = *license_info
+      next unless forbidden_name == name
+
+      return true if forbidden_or_later && forbidden_version <= version
+
+      return true if forbidden_version == version
+    end
+    false
   end
 end
-- 
GitLab