2018-05-30 09:01:35 -04:00
|
|
|
require 'delegate'
|
|
|
|
require 'uri'
|
2019-04-26 07:26:21 -04:00
|
|
|
require 'rubygems/user_interaction'
|
2018-05-30 09:01:35 -04:00
|
|
|
|
|
|
|
class Gem::SpecificationPolicy < SimpleDelegator
|
2019-02-14 07:59:03 -05:00
|
|
|
|
2019-04-26 07:26:21 -04:00
|
|
|
include Gem::UserInteraction
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/.freeze # :nodoc:
|
2018-05-30 09:01:35 -04:00
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
SPECIAL_CHARACTERS = /\A[#{Regexp.escape('.-_')}]+/.freeze # :nodoc:
|
2018-10-21 20:27:02 -04:00
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
VALID_URI_PATTERN = %r{\Ahttps?:\/\/([^\s:@]+:[^\s:@]*@)?[A-Za-z\d\-]+(\.[A-Za-z\d\-]+)+\.?(:\d{1,5})?([\/?]\S*)?\z}.freeze # :nodoc:
|
2018-05-30 09:01:35 -04:00
|
|
|
|
|
|
|
METADATA_LINK_KEYS = %w[
|
|
|
|
bug_tracker_uri
|
|
|
|
changelog_uri
|
|
|
|
documentation_uri
|
|
|
|
homepage_uri
|
|
|
|
mailing_list_uri
|
|
|
|
source_code_uri
|
|
|
|
wiki_uri
|
2018-10-21 20:27:02 -04:00
|
|
|
].freeze # :nodoc:
|
2018-05-30 09:01:35 -04:00
|
|
|
|
2018-08-27 06:05:04 -04:00
|
|
|
def initialize(specification)
|
|
|
|
@warnings = 0
|
|
|
|
|
|
|
|
super(specification)
|
|
|
|
end
|
|
|
|
|
2018-05-30 09:01:35 -04:00
|
|
|
##
|
|
|
|
# If set to true, run packaging-specific checks, as well.
|
|
|
|
|
|
|
|
attr_accessor :packaging
|
|
|
|
|
|
|
|
##
|
|
|
|
# Checks that the specification contains all required fields, and does a
|
|
|
|
# very basic sanity check.
|
|
|
|
#
|
|
|
|
# Raises InvalidSpecificationException if the spec does not pass the
|
|
|
|
# checks.
|
|
|
|
|
2018-08-27 06:05:04 -04:00
|
|
|
def validate(strict = false)
|
2018-05-30 09:01:35 -04:00
|
|
|
validate_nil_attributes
|
|
|
|
|
|
|
|
validate_rubygems_version
|
|
|
|
|
|
|
|
validate_required_attributes
|
|
|
|
|
|
|
|
validate_name
|
|
|
|
|
|
|
|
validate_require_paths
|
|
|
|
|
|
|
|
keep_only_files_and_directories
|
|
|
|
|
|
|
|
validate_non_files
|
|
|
|
|
|
|
|
validate_self_inclusion_in_files_list
|
|
|
|
|
|
|
|
validate_specification_version
|
|
|
|
|
|
|
|
validate_platform
|
|
|
|
|
|
|
|
validate_array_attributes
|
|
|
|
|
|
|
|
validate_authors_field
|
|
|
|
|
|
|
|
validate_metadata
|
|
|
|
|
|
|
|
validate_licenses
|
|
|
|
|
|
|
|
validate_permissions
|
|
|
|
|
|
|
|
validate_lazy_metadata
|
|
|
|
|
|
|
|
validate_values
|
|
|
|
|
|
|
|
validate_dependencies
|
2018-08-27 06:05:04 -04:00
|
|
|
|
|
|
|
if @warnings > 0
|
|
|
|
if strict
|
|
|
|
error "specification has warnings"
|
|
|
|
else
|
|
|
|
alert_warning help_text
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-05-30 09:01:35 -04:00
|
|
|
true
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# Implementation for Specification#validate_metadata
|
|
|
|
|
|
|
|
def validate_metadata
|
2018-11-21 05:20:47 -05:00
|
|
|
unless Hash === metadata
|
2018-08-27 06:05:04 -04:00
|
|
|
error 'metadata must be a hash'
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
metadata.each do |key, value|
|
2018-11-21 05:20:47 -05:00
|
|
|
if !key.kind_of?(String)
|
2018-08-27 06:05:04 -04:00
|
|
|
error "metadata keys must be a String"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
if key.size > 128
|
2018-08-27 06:05:04 -04:00
|
|
|
error "metadata key too large (#{key.size} > 128)"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
if !value.kind_of?(String)
|
2018-08-27 06:05:04 -04:00
|
|
|
error "metadata values must be a String"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
if value.size > 1024
|
2018-08-27 06:05:04 -04:00
|
|
|
error "metadata value too large (#{value.size} > 1024)"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
if METADATA_LINK_KEYS.include? key
|
|
|
|
if value !~ VALID_URI_PATTERN
|
2018-08-27 06:05:04 -04:00
|
|
|
error "metadata['#{key}'] has invalid link: #{value.inspect}"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# Implementation for Specification#validate_dependencies
|
|
|
|
|
|
|
|
def validate_dependencies # :nodoc:
|
|
|
|
# NOTE: see REFACTOR note in Gem::Dependency about types - this might be brittle
|
|
|
|
seen = Gem::Dependency::TYPES.inject({}) { |types, type| types.merge({ type => {}}) }
|
|
|
|
|
|
|
|
error_messages = []
|
|
|
|
warning_messages = []
|
|
|
|
dependencies.each do |dep|
|
2018-11-21 05:20:47 -05:00
|
|
|
if prev = seen[dep.type][dep.name]
|
2018-05-30 09:01:35 -04:00
|
|
|
error_messages << <<-MESSAGE
|
|
|
|
duplicate dependency on #{dep}, (#{prev.requirement}) use:
|
|
|
|
add_#{dep.type}_dependency '#{dep.name}', '#{dep.requirement}', '#{prev.requirement}'
|
|
|
|
MESSAGE
|
|
|
|
end
|
|
|
|
|
|
|
|
seen[dep.type][dep.name] = dep
|
|
|
|
|
|
|
|
prerelease_dep = dep.requirements_list.any? do |req|
|
|
|
|
Gem::Requirement.new(req).prerelease?
|
|
|
|
end
|
|
|
|
|
|
|
|
warning_messages << "prerelease dependency on #{dep} is not recommended" if
|
|
|
|
prerelease_dep && !version.prerelease?
|
|
|
|
|
|
|
|
open_ended = dep.requirement.requirements.all? do |op, version|
|
|
|
|
not version.prerelease? and (op == '>' or op == '>=')
|
|
|
|
end
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
if open_ended
|
2018-05-30 09:01:35 -04:00
|
|
|
op, dep_version = dep.requirement.requirements.first
|
|
|
|
|
2018-12-12 00:07:50 -05:00
|
|
|
segments = dep_version.segments
|
|
|
|
|
|
|
|
base = segments.first 2
|
|
|
|
|
|
|
|
recommendation = if (op == '>' || op == '>=') && segments == [0]
|
|
|
|
" use a bounded requirement, such as '~> x.y'"
|
|
|
|
else
|
|
|
|
bugfix = if op == '>'
|
|
|
|
", '> #{dep_version}'"
|
|
|
|
elsif op == '>=' and base != segments
|
|
|
|
", '>= #{dep_version}'"
|
|
|
|
end
|
|
|
|
|
|
|
|
" if #{dep.name} is semantically versioned, use:\n" \
|
|
|
|
" add_#{dep.type}_dependency '#{dep.name}', '~> #{base.join '.'}'#{bugfix}"
|
|
|
|
end
|
|
|
|
|
|
|
|
warning_messages << ["open-ended dependency on #{dep} is not recommended", recommendation].join("\n") + "\n"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
end
|
2018-11-21 05:20:47 -05:00
|
|
|
if error_messages.any?
|
2018-08-27 06:05:04 -04:00
|
|
|
error error_messages.join
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
2018-11-21 05:20:47 -05:00
|
|
|
if warning_messages.any?
|
2018-05-30 09:01:35 -04:00
|
|
|
warning_messages.each { |warning_message| warning warning_message }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# Issues a warning for each file to be packaged which is world-readable.
|
|
|
|
#
|
|
|
|
# Implementation for Specification#validate_permissions
|
|
|
|
|
|
|
|
def validate_permissions
|
|
|
|
return if Gem.win_platform?
|
|
|
|
|
|
|
|
files.each do |file|
|
|
|
|
next unless File.file?(file)
|
|
|
|
next if File.stat(file).mode & 0444 == 0444
|
|
|
|
warning "#{file} is not world-readable"
|
|
|
|
end
|
|
|
|
|
|
|
|
executables.each do |name|
|
|
|
|
exec = File.join bindir, name
|
|
|
|
next unless File.file?(exec)
|
|
|
|
next if File.stat(exec).executable?
|
|
|
|
warning "#{exec} is not executable"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def validate_nil_attributes
|
|
|
|
nil_attributes = Gem::Specification.non_nil_attributes.select do |attrname|
|
|
|
|
__getobj__.instance_variable_get("@#{attrname}").nil?
|
|
|
|
end
|
|
|
|
return if nil_attributes.empty?
|
2018-08-27 06:05:04 -04:00
|
|
|
error "#{nil_attributes.join ', '} must not be nil"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def validate_rubygems_version
|
|
|
|
return unless packaging
|
|
|
|
return if rubygems_version == Gem::VERSION
|
|
|
|
|
2018-08-27 06:05:04 -04:00
|
|
|
error "expected RubyGems version #{Gem::VERSION}, was #{rubygems_version}"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def validate_required_attributes
|
|
|
|
Gem::Specification.required_attributes.each do |symbol|
|
2018-11-21 05:20:47 -05:00
|
|
|
unless send symbol
|
2018-08-27 06:05:04 -04:00
|
|
|
error "missing value for attribute #{symbol}"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def validate_name
|
2018-10-21 20:27:02 -04:00
|
|
|
if !name.is_a?(String)
|
2018-08-27 06:05:04 -04:00
|
|
|
error "invalid value for attribute name: \"#{name.inspect}\" must be a string"
|
2018-10-21 20:27:02 -04:00
|
|
|
elsif name !~ /[a-zA-Z]/
|
2018-08-27 06:05:04 -04:00
|
|
|
error "invalid value for attribute name: #{name.dump} must include at least one letter"
|
2018-10-21 20:27:02 -04:00
|
|
|
elsif name !~ VALID_NAME_PATTERN
|
2018-08-27 06:05:04 -04:00
|
|
|
error "invalid value for attribute name: #{name.dump} can only include letters, numbers, dashes, and underscores"
|
2018-10-21 20:27:02 -04:00
|
|
|
elsif name =~ SPECIAL_CHARACTERS
|
|
|
|
error "invalid value for attribute name: #{name.dump} can not begin with a period, dash, or underscore"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def validate_require_paths
|
|
|
|
return unless raw_require_paths.empty?
|
|
|
|
|
2018-08-27 06:05:04 -04:00
|
|
|
error 'specification must have at least one require_path'
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def validate_non_files
|
|
|
|
return unless packaging
|
|
|
|
non_files = files.reject {|x| File.file?(x) || File.symlink?(x)}
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
unless non_files.empty?
|
2018-08-27 06:05:04 -04:00
|
|
|
error "[\"#{non_files.join "\", \""}\"] are not files"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def validate_self_inclusion_in_files_list
|
|
|
|
return unless files.include?(file_name)
|
2018-08-27 06:05:06 -04:00
|
|
|
|
2018-08-27 06:05:04 -04:00
|
|
|
error "#{full_name} contains itself (#{file_name}), check your files list"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def validate_specification_version
|
|
|
|
return if specification_version.is_a?(Integer)
|
2018-08-27 06:05:06 -04:00
|
|
|
|
2018-08-27 06:05:04 -04:00
|
|
|
error 'specification_version must be an Integer (did you mean version?)'
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def validate_platform
|
|
|
|
case platform
|
2018-11-21 05:20:47 -05:00
|
|
|
when Gem::Platform, Gem::Platform::RUBY # ok
|
2018-10-21 20:27:02 -04:00
|
|
|
else
|
|
|
|
error "invalid platform #{platform.inspect}, see Gem::Platform"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def validate_array_attributes
|
|
|
|
Gem::Specification.array_attributes.each do |field|
|
|
|
|
validate_array_attribute(field)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def validate_array_attribute(field)
|
|
|
|
val = self.send(field)
|
|
|
|
klass = case field
|
2018-10-21 20:27:02 -04:00
|
|
|
when :dependencies then
|
|
|
|
Gem::Dependency
|
|
|
|
else
|
|
|
|
String
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
unless Array === val and val.all? {|x| x.kind_of?(klass)}
|
2018-05-30 09:01:35 -04:00
|
|
|
raise(Gem::InvalidSpecificationException,
|
|
|
|
"#{field} must be an Array of #{klass}")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def validate_authors_field
|
|
|
|
return unless authors.empty?
|
|
|
|
|
2018-08-27 06:05:04 -04:00
|
|
|
error "authors may not be empty"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def validate_licenses
|
2019-02-14 07:59:03 -05:00
|
|
|
licenses.each do |license|
|
2018-11-21 05:20:47 -05:00
|
|
|
if license.length > 64
|
2018-08-27 06:05:04 -04:00
|
|
|
error "each license must be 64 characters or less"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
if !Gem::Licenses.match?(license)
|
2018-05-30 09:01:35 -04:00
|
|
|
suggestions = Gem::Licenses.suggestions(license)
|
|
|
|
message = <<-warning
|
|
|
|
license value '#{license}' is invalid. Use a license identifier from
|
|
|
|
http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard license.
|
|
|
|
warning
|
|
|
|
message += "Did you mean #{suggestions.map { |s| "'#{s}'"}.join(', ')}?\n" unless suggestions.nil?
|
|
|
|
warning(message)
|
|
|
|
end
|
2019-02-14 07:59:03 -05:00
|
|
|
end
|
2018-05-30 09:01:35 -04:00
|
|
|
|
|
|
|
warning <<-warning if licenses.empty?
|
|
|
|
licenses is empty, but is recommended. Use a license identifier from
|
|
|
|
http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard license.
|
|
|
|
warning
|
|
|
|
end
|
|
|
|
|
|
|
|
LAZY = '"FIxxxXME" or "TOxxxDO"'.gsub(/xxx/, '')
|
2018-11-21 05:20:47 -05:00
|
|
|
LAZY_PATTERN = /FI XME|TO DO/x.freeze
|
|
|
|
HOMEPAGE_URI_PATTERN = /\A[a-z][a-z\d+.-]*:/i.freeze
|
2018-05-30 09:01:35 -04:00
|
|
|
|
|
|
|
def validate_lazy_metadata
|
2018-11-21 05:20:47 -05:00
|
|
|
unless authors.grep(LAZY_PATTERN).empty?
|
2018-08-27 06:05:04 -04:00
|
|
|
error "#{LAZY} is not an author"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
unless Array(email).grep(LAZY_PATTERN).empty?
|
2018-08-27 06:05:04 -04:00
|
|
|
error "#{LAZY} is not an email"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
if description =~ LAZY_PATTERN
|
2018-08-27 06:05:04 -04:00
|
|
|
error "#{LAZY} is not a description"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
if summary =~ LAZY_PATTERN
|
2018-08-27 06:05:04 -04:00
|
|
|
error "#{LAZY} is not a summary"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
# Make sure a homepage is valid HTTP/HTTPS URI
|
|
|
|
if homepage and not homepage.empty?
|
|
|
|
begin
|
|
|
|
homepage_uri = URI.parse(homepage)
|
|
|
|
unless [URI::HTTP, URI::HTTPS].member? homepage_uri.class
|
2018-08-27 06:05:04 -04:00
|
|
|
error "\"#{homepage}\" is not a valid HTTP URI"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
rescue URI::InvalidURIError
|
2018-08-27 06:05:04 -04:00
|
|
|
error "\"#{homepage}\" is not a valid HTTP URI"
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def validate_values
|
|
|
|
%w[author homepage summary files].each do |attribute|
|
|
|
|
validate_attribute_present(attribute)
|
|
|
|
end
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
if description == summary
|
2018-05-30 09:01:35 -04:00
|
|
|
warning "description and summary are identical"
|
|
|
|
end
|
|
|
|
|
|
|
|
executables.each do |executable|
|
|
|
|
validate_shebang_line_in(executable)
|
|
|
|
end
|
|
|
|
|
|
|
|
files.select { |f| File.symlink?(f) }.each do |file|
|
|
|
|
warning "#{file} is a symlink, which is not supported on all platforms"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def validate_attribute_present(attribute)
|
|
|
|
value = self.send attribute
|
|
|
|
warning("no #{attribute} specified") if value.nil? || value.empty?
|
|
|
|
end
|
|
|
|
|
|
|
|
def validate_shebang_line_in(executable)
|
|
|
|
executable_path = File.join(bindir, executable)
|
|
|
|
return if File.read(executable_path, 2) == '#!'
|
|
|
|
|
|
|
|
warning "#{executable_path} is missing #! line"
|
|
|
|
end
|
2018-08-27 06:05:04 -04:00
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
def warning(statement) # :nodoc:
|
2018-08-27 06:05:04 -04:00
|
|
|
@warnings += 1
|
|
|
|
|
|
|
|
alert_warning statement
|
|
|
|
end
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
def error(statement) # :nodoc:
|
2018-08-27 06:05:04 -04:00
|
|
|
raise Gem::InvalidSpecificationException, statement
|
|
|
|
ensure
|
|
|
|
alert_warning help_text
|
|
|
|
end
|
|
|
|
|
|
|
|
def help_text # :nodoc:
|
|
|
|
"See http://guides.rubygems.org/specification-reference/ for help"
|
|
|
|
end
|
2019-02-14 07:59:03 -05:00
|
|
|
|
2018-05-30 09:01:35 -04:00
|
|
|
end
|