276 lines
8.1 KiB
Ruby
Raw Permalink Normal View History

# typed: strict
2020-08-04 10:07:57 -07:00
# frozen_string_literal: true
require "utils/curl"
2020-08-04 10:07:57 -07:00
require "utils/github"
2020-08-26 09:16:05 +02:00
# Helper module for updating SPDX license data.
2020-08-04 10:07:57 -07:00
module SPDX
module_function
DATA_PATH = T.let((HOMEBREW_DATA_PATH/"spdx").freeze, Pathname)
2020-08-04 10:07:57 -07:00
API_URL = "https://api.github.com/repos/spdx/license-list-data/releases/latest"
LICENSEREF_PREFIX = "LicenseRef-Homebrew-"
ALLOWED_LICENSE_SYMBOLS = [
:public_domain,
:cannot_represent,
].freeze
2020-08-04 10:07:57 -07:00
sig { returns(T::Hash[String, T.untyped]) }
def license_data
@license_data ||= T.let(JSON.parse((DATA_PATH/"spdx_licenses.json").read), T.nilable(T::Hash[String, T.untyped]))
end
sig { returns(T::Hash[String, T.untyped]) }
def exception_data
@exception_data ||= T.let(JSON.parse((DATA_PATH/"spdx_exceptions.json").read),
T.nilable(T::Hash[String, T.untyped]))
2020-08-04 10:07:57 -07:00
end
sig { returns(String) }
def latest_tag
@latest_tag ||= T.let(GitHub::API.open_rest(API_URL)["tag_name"], T.nilable(String))
end
sig { params(to: Pathname).void }
def download_latest_license_data!(to: DATA_PATH)
data_url = "https://raw.githubusercontent.com/spdx/license-list-data/#{latest_tag}/json/"
Utils::Curl.curl_download("#{data_url}licenses.json", to: to/"spdx_licenses.json")
Utils::Curl.curl_download("#{data_url}exceptions.json", to: to/"spdx_exceptions.json")
end
sig {
params(
license_expression: T.any(
String,
Symbol,
T::Hash[T.any(Symbol, String), T.untyped],
T::Array[String],
),
).returns(
[
T::Array[T.any(String, Symbol)], T::Array[String]
],
)
}
def parse_license_expression(license_expression)
2020-12-28 13:34:07 +00:00
licenses = T.let([], T::Array[T.any(String, Symbol)])
exceptions = T.let([], T::Array[String])
case license_expression
when String, Symbol
licenses.push license_expression
2020-08-19 10:25:02 -04:00
when Hash, Array
if license_expression.is_a? Hash
license_expression = license_expression.filter_map do |key, value|
2020-08-19 10:25:02 -04:00
if key.is_a? String
licenses.push key
exceptions.push value[:with]
next
end
value
end
end
2020-08-19 10:25:02 -04:00
license_expression.each do |license|
sub_license, sub_exception = parse_license_expression license
licenses += sub_license
exceptions += sub_exception
end
end
[licenses, exceptions]
end
sig { params(license: T.any(String, Symbol)).returns(T::Boolean) }
def valid_license?(license)
return ALLOWED_LICENSE_SYMBOLS.include? license if license.is_a? Symbol
license = license.delete_suffix "+"
license_data["licenses"].any? { |spdx_license| spdx_license["licenseId"].downcase == license.downcase }
end
sig { params(license: T.any(String, Symbol)).returns(T::Boolean) }
def deprecated_license?(license)
return false if ALLOWED_LICENSE_SYMBOLS.include? license
return false unless valid_license?(license)
license = license.to_s.delete_suffix "+"
license_data["licenses"].none? do |spdx_license|
spdx_license["licenseId"].downcase == license.downcase && !spdx_license["isDeprecatedLicenseId"]
end
end
sig { params(exception: String).returns(T::Boolean) }
def valid_license_exception?(exception)
exception_data["exceptions"].any? do |spdx_exception|
spdx_exception["licenseExceptionId"].downcase == exception.downcase && !spdx_exception["isDeprecatedLicenseId"]
end
end
sig {
params(
license_expression: T.any(String, Symbol, T::Hash[T.nilable(T.any(Symbol, String)), T.untyped]),
bracket: T::Boolean,
hash_type: T.nilable(T.any(String, Symbol)),
).returns(T.nilable(String))
}
def license_expression_to_string(license_expression, bracket: false, hash_type: nil)
case license_expression
when String
license_expression
when Symbol
LICENSEREF_PREFIX + license_expression.to_s.tr("_", "-")
2020-08-20 10:26:37 -04:00
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|
2024-03-07 16:20:20 +00:00
license_expression_to_string license, bracket: true, hash_type:
end
end
else
bracket = false
license_expression.each do |expression|
2021-02-16 09:25:34 +00:00
expressions.push license_expression_to_string([expression].to_h, 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
sig {
params(
string: T.nilable(String),
).returns(
T.nilable(
T.any(
String,
Symbol,
T::Hash[T.any(String, Symbol), T.untyped],
),
),
)
}
2023-02-06 09:57:25 +00:00
def string_to_license_expression(string)
return if string.blank?
result = string
result_type = nil
and_parts = string.split(/ and (?![^(]*\))/i)
2023-02-06 09:57:25 +00:00
if and_parts.length > 1
result = and_parts
result_type = :all_of
else
or_parts = string.split(/ or (?![^(]*\))/i)
2023-02-06 09:57:25 +00:00
if or_parts.length > 1
result = or_parts
result_type = :any_of
end
end
if result_type
result.map! do |part|
part = part[1..-2] if part[0] == "(" && part[-1] == ")"
string_to_license_expression(part)
end
{ result_type => result }
else
with_parts = string.split(/ with /i, 2)
2023-02-06 09:57:25 +00:00
if with_parts.length > 1
{ with_parts.first => { with: with_parts.second } }
else
return result unless result.start_with?(LICENSEREF_PREFIX)
license_sym = result.delete_prefix(LICENSEREF_PREFIX).downcase.tr("-", "_").to_sym
ALLOWED_LICENSE_SYMBOLS.include?(license_sym) ? license_sym : result
2023-02-06 09:57:25 +00:00
end
end
end
sig {
params(
license: T.any(String, Symbol),
).returns(
T.any(
[T.any(String, Symbol)],
[String, T.nilable(String), T::Boolean],
),
)
}
def license_version_info(license)
return [license] if ALLOWED_LICENSE_SYMBOLS.include? license
match = license.match(/-(?<version>[0-9.]+)(?:-.*?)??(?<or_later>\+|-only|-or-later)?$/)
return [license] if match.blank?
license_name = license.to_s.split(match[0].to_s).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
sig {
params(license_expression: T.any(String, Symbol, T::Hash[Symbol, T.untyped]),
forbidden_licenses: T::Hash[Symbol, T.untyped]).returns(T::Boolean)
}
def licenses_forbid_installation?(license_expression, forbidden_licenses)
case license_expression
when String, Symbol
forbidden_licenses_include? license_expression.to_s, forbidden_licenses
2020-08-20 10:26:37 -04:00
when Hash
key = license_expression.keys.first
return false if key.nil?
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
sig {
params(
license: T.any(Symbol, String),
forbidden_licenses: T::Hash[T.any(Symbol, String), T.untyped],
).returns(T::Boolean)
}
def forbidden_licenses_include?(license, forbidden_licenses)
return true if forbidden_licenses.key? license
name, version, = license_version_info license
forbidden_licenses.each_value do |license_info|
forbidden_name, forbidden_version, forbidden_or_later = *license_info
2023-04-18 15:06:50 -07:00
next if forbidden_name != name
return true if forbidden_or_later && forbidden_version <= version
return true if forbidden_version == version
end
false
2020-08-04 10:07:57 -07:00
end
end