diff --git a/Gemfile b/Gemfile index 0e3b255..8a418fa 100644 --- a/Gemfile +++ b/Gemfile @@ -10,3 +10,5 @@ gem "rake", "~> 13.0" gem "minitest", "~> 5.16" gem "rubocop", "~> 1.21" + +gem "standard", "~> 1.44" diff --git a/Gemfile.lock b/Gemfile.lock index 8be10b7..e5a35b6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - picopackage (0.2.0) + picopackage (0.2.1) digest open-uri (~> 0.5) yaml (~> 0.4) @@ -14,22 +14,27 @@ GEM debug (1.10.0) irb (~> 1.10) reline (>= 0.3.8) - digest (3.1.1) + digest (3.2.0) io-console (0.8.0) - irb (1.14.3) + irb (1.15.1) + pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.9.1) language_server-protocol (3.17.0.3) + lint_roller (1.1.0) minitest (5.25.4) open-uri (0.5.0) stringio time uri parallel (1.26.3) - parser (3.3.6.0) + parser (3.3.7.0) ast (~> 2.4.1) racc + pp (0.6.2) + prettyprint + prettyprint (0.2.0) psych (5.2.3) date stringio @@ -53,7 +58,22 @@ GEM unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.37.0) parser (>= 3.3.1.0) + rubocop-performance (1.23.1) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (1.13.0) + standard (1.44.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.70.0) + standard-custom (~> 1.0.0) + standard-performance (~> 1.6) + standard-custom (1.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.50) + standard-performance (1.6.0) + lint_roller (~> 1.1) + rubocop-performance (~> 1.23.0) stringio (3.1.2) time (0.4.1) date @@ -73,6 +93,7 @@ DEPENDENCIES picopackage! rake (~> 13.0) rubocop (~> 1.21) + standard (~> 1.44) BUNDLED WITH 2.6.2 diff --git a/Rakefile b/Rakefile index 2bf771f..ab58bab 100644 --- a/Rakefile +++ b/Rakefile @@ -5,8 +5,9 @@ require "minitest/test_task" Minitest::TestTask.create -require "rubocop/rake_task" +# require "rubocop/rake_task" +# RuboCop::RakeTask.new -RuboCop::RakeTask.new +require "standard/rake" -task default: %i[test rubocop] +task default: %i[test standard] diff --git a/bin/console b/bin/console index 677e348..6dca5fb 100755 --- a/bin/console +++ b/bin/console @@ -2,7 +2,7 @@ # frozen_string_literal: true require "bundler/setup" -require "picop" +require "picopackage" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. diff --git a/exe/ppkg b/exe/ppkg index a7d4f33..b0940ef 100755 --- a/exe/ppkg +++ b/exe/ppkg @@ -1,15 +1,15 @@ #!/usr/bin/env ruby # Add lib directory to load path -lib_path = File.expand_path('../lib', __dir__) +lib_path = File.expand_path("../lib", __dir__) $LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path) -require 'picopackage' +require "picopackage" begin Picopackage::CLI.run(ARGV) rescue => e warn "Error: #{e.message}" - warn e.backtrace if ENV['DEBUG'] + warn e.backtrace if ENV["DEBUG"] exit 1 end diff --git a/lib/picopackage.rb b/lib/picopackage.rb index e0cf1e0..83fde8e 100644 --- a/lib/picopackage.rb +++ b/lib/picopackage.rb @@ -10,7 +10,10 @@ require_relative "picopackage/cli" module Picopackage class Error < StandardError; end + class FileTooLargeError < StandardError; end + class FetchError < StandardError; end + class LocalModificationError < StandardError; end end diff --git a/lib/picopackage/cli.rb b/lib/picopackage/cli.rb index 5f49679..71452dd 100644 --- a/lib/picopackage/cli.rb +++ b/lib/picopackage/cli.rb @@ -5,17 +5,17 @@ module Picopackage def self.run(argv = ARGV) command = argv.shift case command - when 'scan' + when "scan" options = {} OptionParser.new do |opts| opts.banner = "Usage: ppkg scan [options] DIRECTORY" # opts.on('-v', '--verbose', 'Run verbosely') { |v| options[:verbose] = v } end.parse!(argv) - dir = argv.first || '.' - Picopackage::Scanner.scan(dir).each {|f| puts f.file_path } + dir = argv.first || "." + Picopackage::Scanner.scan(dir).each { |f| puts f.file_path } - when 'digest' + when "digest" OptionParser.new do |opts| opts.banner = "Usage: ppkg digest FILE" end.parse!(argv) @@ -23,14 +23,14 @@ module Picopackage file = argv.first Picopackage::SourceFile.from_file(file).digest! - when 'checksum' + when "checksum" OptionParser.new do |opts| opts.banner = "Usage: ppkg checksum FILE" end.parse!(argv) file = argv.first puts Picopackage::SourceFile.from_file(file).checksum - when 'verify' + when "verify" OptionParser.new do |opts| opts.banner = "Usage: ppkg sign FILE" end.parse!(argv) @@ -38,7 +38,7 @@ module Picopackage path = argv.first source = SourceFile.from_file(path) - if source.metadata['content_checksum'].nil? + if source.metadata["content_checksum"].nil? puts "⚠️ No checksum found in #{path}" puts "Run 'ppkg sign #{path}' to add one" exit 1 @@ -46,14 +46,14 @@ module Picopackage unless source.verify puts "❌ Checksum verification failed for #{path}" - puts "Expected: #{source.metadata['content_checksum']}" + puts "Expected: #{source.metadata["content_checksum"]}" puts "Got: #{source.checksum}" exit 1 end - + puts "✅ #{path} verified successfully" - when 'inspect' + when "inspect" OptionParser.new do |opts| opts.banner = "Usage: ppkg inspect FILE|DIRECTORY" end.parse!(argv) @@ -61,21 +61,21 @@ module Picopackage path = argv.first Picopackage::SourceFile.from_file(path).inspect - when 'fetch' - options = { force: false } + when "fetch" + options = {force: false} OptionParser.new do |opts| opts.banner = "Usage: ppkg fetch [options] URI [PATH]" - opts.on('-f', '--force', 'Force fetch') { |f| options[:force] = f } + opts.on("-f", "--force", "Force fetch") { |f| options[:force] = f } end.parse!(argv) - + url = argv.shift - path = argv.shift || '.' # use '.' if no path provided - + path = argv.shift || "." # use '.' if no path provided + if url.nil? puts "Error: URI is required" exit 1 end - + begin Fetch.fetch(url, path, force: options[:force]) rescue LocalModificationError => e @@ -85,11 +85,11 @@ module Picopackage exit 1 end - when 'update' - options = { force: false } + when "update" + options = {force: false} OptionParser.new do |opts| opts.banner = "Usage: ppkg update [options] FILE" - opts.on('-f', '--force', 'Force update') { |f| options[:force] = f } + opts.on("-f", "--force", "Force update") { |f| options[:force] = f } end.parse!(argv) file = argv.first @@ -113,8 +113,8 @@ module Picopackage exit 1 rescue => e puts "Error: #{e.message}" - puts e.backtrace if ENV['DEBUG'] + puts e.backtrace if ENV["DEBUG"] exit 1 end end -end \ No newline at end of file +end diff --git a/lib/picopackage/fetch.rb b/lib/picopackage/fetch.rb index a32a044..5d2614f 100644 --- a/lib/picopackage/fetch.rb +++ b/lib/picopackage/fetch.rb @@ -1,8 +1,8 @@ -require 'net/http' -require 'fileutils' -require 'tempfile' -require 'json' -require 'debug' +require "net/http" +require "fileutils" +require "tempfile" +require "json" +require "debug" module Picopackage class Fetch @@ -45,26 +45,25 @@ module Picopackage end class Status - attr_reader :state, :local_version, :remote_version + attr_reader :state, :local_comparison, :remote_comparison def self.compare(local_source_file, remote_source_file) return new(:outdated) if local_source_file.metadata.nil? || remote_source_file.metadata.nil? - local_version = local_source_file.metadata["version"] - remote_version = remote_source_file.metadata["version"] + local_comparison = local_source_file.metadata["version"] || local_source_file.metadata["updated_at"]&.to_s + remote_comparison = remote_source_file.metadata["version"] || remote_source_file.metadata["updated_at"]&.to_s - if local_version == remote_version + if local_comparison == remote_comparison if local_source_file.modified? - new(:modified, local_version:) + new(:modified, local_version: local_comparison) else - new(:up_to_date, local_version:) + new(:up_to_date, local_version: local_comparison) end else - new(:outdated, - local_version:, - remote_version:, - modified: local_source_file.modified? - ) + new(:outdated, + local_version: local_comparison, + remote_version: remote_comparison, + modified: local_source_file.modified?) end end @@ -93,9 +92,9 @@ module Picopackage "Picopackage is up to date" when :outdated if modified? - "Local Picopackage (v#{local_version}) has modifications but remote version (v#{remote_version}) is available" + "Local Picopackage (v#{local_comparison}) has modifications but remote version (v#{remote_comparison}) is available" else - "Local Picopackage (v#{local_version}) is outdated. Remote version: v#{remote_version}" + "Local Picopackage (v#{local_comparison}) is outdated. Remote version: v#{remote_comparison}" end when :modified "Local Picopackage has been modified from original version (v#{local_version})" diff --git a/lib/picopackage/http_fetcher.rb b/lib/picopackage/http_fetcher.rb deleted file mode 100644 index 6b16f28..0000000 --- a/lib/picopackage/http_fetcher.rb +++ /dev/null @@ -1,36 +0,0 @@ -# Currently unused. If we get to the point where a provider needs to make a http request, we'll -# swappout DefaultProvider#fetch and include this module. - -module Picopackage - module HttpFetcher - MAX_SIZE = 1024 * 1024 - TIMEOUT = 10 - - # This seemed to cause loops - constanting making requests to the same URL - - def fetch_url(url, max_size: MAX_SIZE, timeout: TIMEOUT) - raise ArgumentError, "This method shouldn't be called" - uri = URI(url) - - Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', read_timeout: timeout, open_timeout: timeout) do |http| - request = Net::HTTP::Get.new(uri.path) - - http.request_get(uri.path) do |response| - unless response.is_a?(Net::HTTPSuccess) - raise FetchError, "HTTP #{response.code} #{response.message}" - end - - data = String.new(capacity: max_size) - response.read_body do |chunk| - # Stream chunks with size checking - if data.bytesize + chunk.bytesize > max_size - raise FileTooLargeError - end - data << chunk - end - end - data - end - end - end -end diff --git a/lib/picopackage/provider.rb b/lib/picopackage/provider.rb index 30cae51..9a09f5f 100644 --- a/lib/picopackage/provider.rb +++ b/lib/picopackage/provider.rb @@ -1,3 +1,5 @@ +require "time" + module Picopackage class Provider def self.for(url) @@ -17,18 +19,18 @@ module Picopackage end # Base class for fetching content from a URL - # The variable `body` will contain the content retrieved from the URL - # The variable `content` will contain both and code + metadata - this would be writen to a file. - # The variable `code` will contain the code extracted from `content` - # The variable `metadata` will contain the metadata extracted from `content` + # The variable `body` will contain the package_data retrieved from the URL + # The variable `package_data` will contain both and payload + metadata - this would be writen to a file. + # The variable `payload` will contain the payload extracted from `package_data` + # The variable `metadata` will contain the metadata extracted from `package_data` - # Job of the Provider class is to fetch the body from the URL, and then extract the content and the filename from the body - # The SourceFile class will then take the body and split it into code and metadata + # Job of the Provider class is to fetch the body from the URL, and then extract the package_data + # and the filename from the body. The SourceFile class will then take the body and split it into payload and metadata class DefaultProvider MAX_SIZE = 1024 * 1024 TIMEOUT = 10 - attr_reader :url, :source_file + attr_reader :url def self.handles_url?(url) = :maybe @@ -40,65 +42,79 @@ module Picopackage end def body = @body ||= fetch + def json_body = @json_body ||= JSON.parse(body) + def transform_url(url) = url def fetch - begin - Net::HTTP.start(@uri.host, @uri.port, use_ssl: @uri.scheme == 'https', read_timeout: TIMEOUT, open_timeout: TIMEOUT) do |http| - http.request_get(@uri.path) do |response| - raise "Unexpected response: #{response.code}" unless response.is_a?(Net::HTTPSuccess) + Net::HTTP.start(@uri.host, @uri.port, use_ssl: @uri.scheme == "https", read_timeout: TIMEOUT, open_timeout: TIMEOUT) do |http| + http.request_get(@uri.path) do |response| + raise "Unexpected response: #{response.code}" unless response.is_a?(Net::HTTPSuccess) - @body = String.new(capacity: MAX_SIZE) - response.read_body do |chunk| - if @body.bytesize + chunk.bytesize > MAX_SIZE - raise FileTooLargeError, "Response would exceed #{MAX_SIZE} bytes" - end - @body << chunk + @body = String.new(capacity: MAX_SIZE) + response.read_body do |chunk| + if @body.bytesize + chunk.bytesize > MAX_SIZE + raise FileTooLargeError, "Response would exceed #{MAX_SIZE} bytes" end - @body + @body << chunk end + @body end end + @body end def handles_body? true - rescue FileTooLargeError, Net::HTTPError, RuntimeError => e + rescue FileTooLargeError, Net::HTTPError, RuntimeError false end - # Implement in subclass - this come from the `body`. - # Spliting content into code and metadata is the job of the SourceFile class + # Implement in subclass - this come from the `body`. + # Spliting content into payload and metadata is the job of the SourceFile class def content = body # Implement in subclass - this should return the filename extracted from the body - if it exists, but not from the metadata def filename = File.basename @url def source_file - @source_file ||= SourceFile.from_content(content, metadata: {'filename' => filename, 'url' => url, 'version' => '0.0.1'}) + @source_file ||= SourceFile.from_content(content, metadata: {"filename" => filename, "url" => url, "packaged_at" => packaged_at}.compact) end end class GithubGistProvider < DefaultProvider def self.handles_url?(url) = url.match?(%r{gist\.github\.com}) + def content = json_body["files"].values.first["content"] + def filename = json_body["files"].values.first["filename"] + def transform_url(url) gist_id = url[/gist\.github\.com\/[^\/]+\/([a-f0-9]+)/, 1] "https://api.github.com/gists/#{gist_id}" end + + def packaged_at + Time.parse(json_body["created_at"]) + rescue ArgumentError + nil + end end class OpenGistProvider < DefaultProvider def handles_url?(url) = :maybe + def transform_url(url) = "#{url}.json" - def content = json_body.dig("files",0, "content") - def filename = json_body.dig("files",0, "filename") + + def content = json_body.dig("files", 0, "content") + + def filename = json_body.dig("files", 0, "filename") + def handles_body? content && filename - rescue FileTooLargeError, Net::HTTPError, RuntimeError => e + rescue FileTooLargeError, Net::HTTPError, RuntimeError false end # If we successfully fetch the body, and the body contains content and a filename, then we can handle the body @@ -109,4 +125,4 @@ module Picopackage OpenGistProvider, DefaultProvider ].freeze -end \ No newline at end of file +end diff --git a/lib/picopackage/scanner.rb b/lib/picopackage/scanner.rb index 72a8219..e38f8cf 100644 --- a/lib/picopackage/scanner.rb +++ b/lib/picopackage/scanner.rb @@ -8,4 +8,4 @@ module Picopackage end.map { |file| SourceFile.new(file) } end end -end \ No newline at end of file +end diff --git a/lib/picopackage/source_file.rb b/lib/picopackage/source_file.rb index ba0508f..62af8ff 100644 --- a/lib/picopackage/source_file.rb +++ b/lib/picopackage/source_file.rb @@ -8,7 +8,7 @@ module Picopackage METADATA_PATTERN = /^\n*#\s*@PICOPACKAGE_START\n(.*?)^\s*#\s*@PICOPACKAGE_END\s*$/m def self.from_file(file_path) = new(content: File.read(file_path), original_path: file_path) - + def self.from_content(content, metadata: {}) instance = new(content: content) instance.imported! if instance.metadata.empty? @@ -25,21 +25,21 @@ module Picopackage @original_path = original_path @content = content - @metadata = extract_metadata @code = extract_code + @metadata = extract_metadata end def imported! = @imported = true def imported? = @imported ||= false - def content = @content - - def url = @metadata['url'] + def url = @metadata["url"] - def filename = @metadata['filename'] + def filename = @metadata["filename"] - def version = @metadata['version'] || '0.0.1' + def version = @metadata["version"] + + def packaged_at = @metadata["packaged_at"] def checksum = "sha256:#{Digest::SHA256.hexdigest(code)}" @@ -51,15 +51,15 @@ module Picopackage path end - def extract_code = content.sub(METADATA_PATTERN, '') + def extract_code = content.sub(METADATA_PATTERN, "") def extract_metadata return {} unless content =~ METADATA_PATTERN - + yaml_content = $1.lines.map do |line| - line.sub(/^\s*#\s?/, '').rstrip + line.sub(/^\s*#\s?/, "").rstrip end.join("\n") - + YAML.safe_load(yaml_content) end @@ -67,19 +67,19 @@ module Picopackage @metadata = metadata_hash @content = generate_content end - + def digest! hash = checksum - return puts "File already has a checksum" if metadata['content_checksum'] == hash - - new_metadata = metadata.merge('content_checksum' => hash) + return puts "File already has a checksum" if metadata["content_checksum"] == hash + + new_metadata = metadata.merge("content_checksum" => hash) update_metadata(new_metadata) save end def verify - return false unless metadata.key? 'content_checksum' - checksum == metadata['content_checksum'] + return false unless metadata.key? "content_checksum" + checksum == metadata["content_checksum"] end def modified? = !verify @@ -88,13 +88,13 @@ module Picopackage def generate_content metadata_block = generate_metadata - if content =~ METADATA_PATTERN + if METADATA_PATTERN.match?(content) content.sub(METADATA_PATTERN, "\n#{metadata_block}") else [content.rstrip, "\n#{metadata_block}"].join("\n") end end - + # This will need a comment style one day, to work with other languages def generate_metadata yaml_content = @metadata.to_yaml.strip @@ -115,6 +115,5 @@ module Picopackage destination end end - end -end \ No newline at end of file +end diff --git a/picopackage.gemspec b/picopackage.gemspec index edc3c33..1fa37a4 100644 --- a/picopackage.gemspec +++ b/picopackage.gemspec @@ -14,11 +14,11 @@ Gem::Specification.new do |spec| spec.license = "MIT" spec.required_ruby_version = ">= 3.1.0" - #spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'" + # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'" - #spec.metadata["homepage_uri"] = spec.homepage - #spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." - #spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." + # spec.metadata["homepage_uri"] = spec.homepage + # spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." + # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. @@ -37,7 +37,8 @@ Gem::Specification.new do |spec| spec.add_dependency "yaml", "~> 0.4" spec.add_dependency "digest" spec.add_development_dependency "debug" - + spec.add_development_dependency "standard" + # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html end diff --git a/test/files/broken_front_matter_1.rb b/test/files/broken_front_matter_1.rb index f12d713..e6a871b 100644 --- a/test/files/broken_front_matter_1.rb +++ b/test/files/broken_front_matter_1.rb @@ -6,4 +6,4 @@ # content_hash: sha256:d747c448ce60a6ef1a8372f7c7eee10160075acbed6412a12c2f6d71ca8b2f76 # @META_END require "cgi" -puts "hi" \ No newline at end of file +puts "hi" diff --git a/test/test_picopackage.rb b/test/test_picopackage.rb index c309b22..467b91c 100644 --- a/test/test_picopackage.rb +++ b/test/test_picopackage.rb @@ -7,7 +7,8 @@ class TestPicopackage < Minitest::Test refute_nil ::Picopackage::VERSION end - def test_it_does_something_useful - assert false + def test_it_can_load_a_picopackage_file + sf = Picopackage::SourceFile.from_content(File.read("test/files/uniquify_array.rb")) + assert_equal sf.metadata["filename"], "uniquify_array.rb" end end