# typed: true # frozen_string_literal: true require "bundle_version" module Homebrew module Livecheck module Strategy # The {Sparkle} strategy fetches content at a URL and parses it as a # Sparkle appcast in XML format. # # This strategy is not applied automatically and it's necessary to use # `strategy :sparkle` in a `livecheck` block to apply it. # # @api private class Sparkle extend T::Sig # A priority of zero causes livecheck to skip the strategy. We do this # for {Sparkle} so we can selectively apply it when appropriate. PRIORITY = 0 # The `Regexp` used to determine if the strategy applies to the URL. URL_MATCH_REGEX = %r{^https?://}i.freeze # Whether the strategy can be applied to the provided URL. # # @param url [String] the URL to match against # @return [Boolean] sig { params(url: String).returns(T::Boolean) } def self.match?(url) URL_MATCH_REGEX.match?(url) end # @api private Item = Struct.new( # @api public :title, # @api public :channel, # @api private :pub_date, # @api public :url, # @api private :bundle_version, keyword_init: true, ) do extend T::Sig extend Forwardable # @api public delegate version: :bundle_version # @api public delegate short_version: :bundle_version # @api public delegate nice_version: :bundle_version end # Identifies version information from a Sparkle appcast. # # @param content [String] the text of the Sparkle appcast # @return [Item, nil] sig { params(content: String).returns(T::Array[Item]) } def self.items_from_content(content) require "rexml/document" parsing_tries = 0 xml = begin REXML::Document.new(content) rescue REXML::UndefinedNamespaceException => e undefined_prefix = e.to_s[/Undefined prefix ([^ ]+) found/i, 1] raise if undefined_prefix.blank? # Only retry parsing once after removing prefix from content parsing_tries += 1 raise if parsing_tries > 1 # When an XML document contains a prefix without a corresponding # namespace, it's necessary to remove the prefix from the content # to be able to successfully parse it using REXML content = content.gsub(%r{(