Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,53 @@ PATH
remote: .
specs:
facet_rails_common (0.1.0)
order_query (~> 0.5.3)

GEM
remote: https://rubygems.org/
specs:
activemodel (8.0.2.1)
activesupport (= 8.0.2.1)
activerecord (8.0.2.1)
activemodel (= 8.0.2.1)
activesupport (= 8.0.2.1)
timeout (>= 0.4.0)
activesupport (8.0.2.1)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
base64 (0.3.0)
benchmark (0.4.1)
bigdecimal (3.2.3)
concurrent-ruby (1.3.5)
connection_pool (2.5.4)
drb (2.2.3)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
logger (1.7.0)
minitest (5.25.5)
order_query (0.5.5)
activerecord (>= 5.0, < 8.1)
activesupport (>= 5.0, < 8.1)
rake (13.1.0)
securerandom (0.4.1)
timeout (0.4.3)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uri (1.0.3)

PLATFORMS
arm64-darwin-20
arm64-darwin-24

DEPENDENCIES
facet_rails_common!
Expand Down
88 changes: 55 additions & 33 deletions lib/facet_rails_common/data_uri.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
require "base64"
require "json"

class ::DataUri
REGEXP = %r{
\Adata:
Expand All @@ -10,16 +13,30 @@ class ::DataUri
(?<data>.*)
}x.freeze

attr_reader :uri, :match
attr_reader :mimetype, :parameters, :extension, :data, :uri, :match

def initialize(uri)
match = REGEXP.match(uri)
raise ArgumentError, 'invalid data URI' unless match

@uri = uri
@match = match
@match = REGEXP.match(uri)
raise ArgumentError, 'invalid data URI' unless @match

header_end = @match.begin(:data)
header = header_end.nil? ? '' : uri[0...header_end]
@header_contains_base64 = !!(header.match?(/base64/i))

@mimetype = String(@match[:mimetype]).empty? ? 'text/plain' : @match[:mimetype]
@parameters = String(@match[:parameters]).split(';').reject(&:empty?)
@extension = @match[:extension]
@data = @match[:data]

validate_base64_content

if uri.start_with?('data:,')
@mimetype = 'text/plain'
@parameters = []
@extension = nil
@data = uri.split(',', 2).last || ''
end
end

def self.valid?(uri)
Expand All @@ -33,57 +50,62 @@ def self.valid?(uri)

def self.esip6?(uri)
begin
parameters = DataUri.new(uri).parameters
data_uri = DataUri.new(uri)

parameters.include?("rule=esip6")
# Use legacy behavior to support Ethscriptions
raw_parameters = String(data_uri.match[:parameters]).split(';')
raw_parameters.include?("rule=esip6")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: esip6? method fails for legacy implicit form URIs

The esip6? method accesses match[:parameters] directly to check for the rule=esip6 parameter. However, for implicit form URIs like data:,text/plain;rule=esip6,..., the regex captures everything after data:, as the data group, leaving match[:parameters] empty. This causes esip6? to return false even when rule=esip6 appears in the content, causing the test_esip6_legacy_implicit_form test to fail.

Fix in Cursor Fix in Web

rescue ArgumentError
false
end
end

def validate_base64_content
if base64?
begin
Base64.strict_decode64(data)
rescue ArgumentError
raise ArgumentError, 'malformed base64 content'
end
end
return unless claims_to_be_base64?

raise ArgumentError, 'malformed base64 content' unless data_valid_base64?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Base64 validation misses implicit claims.

The validation in validate_base64_content only triggers when claims_to_be_base64? returns true (checking for the ;base64 extension). For data:, URIs where "base64" appears in the data portion (like data:,text/plain;charset=utf-8;base64,invaliddata), the extension is nil, so claims_to_be_base64? returns false and validation doesn't happen. This causes the test test_implicit_form_with_legacy_base64_extension_raises to fail since it expects malformed base64 content to be detected and rejected even in this legacy format.

Fix in Cursor Fix in Web

end

def mediatype
"#{mimetype}#{parameters}"
end

def base64?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Mediatype Output Incorrectly Includes Parameters Array

The mediatype method concatenates mimetype with the @parameters array directly. Since @parameters is an array (set on line 35), string interpolation will produce incorrect output like "text/plain[\"foo=bar\", \"baz=qux\"]" instead of "text/plain;foo=bar;baz=qux". The parameters array should be joined with semicolons: "#{mimetype}#{parameters.map { |p| ";#{p}" }.join}" or similar.

Fix in Cursor Fix in Web

@header_contains_base64 && data_valid_base64?
end

def decoded_data
return data unless base64?

Base64.decode64(data)
base64? ? base64_decoded_data : data
end

def base64?
def claims_to_be_base64?
!String(extension).empty?
end

def mimetype
if String(match[:mimetype]).empty? || uri.starts_with?("data:,")
return 'text/plain'
end
def data_valid_base64?
!base64_decoded_data.nil?
end

def json?
return @_json if instance_variable_defined?(:@_json)

match[:mimetype]
@_json = mimetype.include?('json') || decoded_data.lstrip.start_with?('{', '[')
end

def data
match[:data]
def parse_json(symbolize_names: false, max_nesting: 100)
raise ArgumentError, 'not JSON' unless json?
JSON.parse(decoded_data, symbolize_names: symbolize_names, max_nesting: max_nesting)
end

def parameters
return [] if String(match[:mimetype]).empty? && String(match[:parameters]).empty?

match[:parameters].split(";").reject(&:empty?)
end

def extension
match[:extension]
private

def base64_decoded_data
return @base64_decoded_data if instance_variable_defined?(:@base64_decoded_data)

@base64_decoded_data = begin
Base64.strict_decode64(data)
rescue ArgumentError
nil
end
end
end
164 changes: 164 additions & 0 deletions test/data_uri_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# frozen_string_literal: true

require "minitest/autorun"
require "facet_rails_common/data_uri"

class DataUriTest < Minitest::Test
def test_decoded_data_with_base64_extension
uri = "data:text/plain;base64,SGVsbG8="

assert_equal "Hello", DataUri.new(uri).decoded_data
end

def test_implicit_data_uri_keeps_commas_after_first_split
du = DataUri.new("data:,hi/bye,yo")
assert_equal "hi/bye,yo", du.data
end

def test_implicit_data_uri_defaults_and_decoding
uri = "data:,Hello"
du = DataUri.new(uri)
assert_equal "text/plain", du.mimetype
assert_equal [], du.parameters
assert_nil du.extension
assert_equal "Hello", du.data
refute du.base64?
assert_equal "Hello", du.decoded_data
end

def test_decoded_data_when_metadata_mentions_base64
uri = "data:text/plain;foo=base64,SGVsbG8="

assert_equal "Hello", DataUri.new(uri).decoded_data
end

def test_decoded_data_when_metadata_mentions_base64_in_mime_type
uri = "data:application/Base64Something,SGVsbG8="

assert_equal "Hello", DataUri.new(uri).decoded_data
end

def test_returns_original_data_when_metadata_mentions_base64_but_data_invalid
uri = "data:text/plain;foo=base64,@@@"
data_uri = DataUri.new(uri)

refute data_uri.data_valid_base64?
assert_equal "@@@", data_uri.decoded_data
end

def test_returns_original_data_when_base64_only_in_payload
uri = "data:text/plain,base64SGVsbG8="

assert_equal "base64SGVsbG8=", DataUri.new(uri).decoded_data
end

def test_invalid_base64_with_extension_raises
uri = "data:text/plain;base64,@@@"

error = assert_raises(ArgumentError) { DataUri.new(uri) }
assert_equal "malformed base64 content", error.message
end

def test_implicit_form_with_legacy_base64_extension_raises
uri = "data:,text/plain;charset=utf-8;base64,8J+Msi50cmVl,"

error = assert_raises(ArgumentError) { DataUri.new(uri) }
assert_equal "malformed base64 content", error.message
end

def test_implicit_form_with_legacy_base64_valid_decodes_and_defaults
uri = "data:,text/plain;charset=utf-8;base64,SGVsbG8="
du = DataUri.new(uri)

assert_equal "text/plain", du.mimetype
assert_equal [], du.parameters
assert_nil du.extension
assert_equal "Hello", du.decoded_data
end

def test_valid_question_mark_false_for_implicit_legacy_invalid_base64
uri = "data:,text/plain;charset=utf-8;base64,@@@"
refute DataUri.valid?(uri)
end

def test_uppercase_base64_param_valid_decodes
uri = "data:text/plain;foo=BASE64,SGVsbG8="
du = DataUri.new(uri)

assert du.base64?
assert_equal "Hello", du.decoded_data
end

def test_uppercase_base64_param_invalid_returns_original
uri = "data:text/plain;foo=BASE64,@@@"
du = DataUri.new(uri)

refute du.data_valid_base64?
refute du.base64?
assert_equal "@@@", du.decoded_data
end

def test_empty_base64_content_decodes_to_empty_string
uri = "data:text/plain;base64,"
du = DataUri.new(uri)

assert_equal "", du.decoded_data
end

def test_no_mimetype_with_base64_defaults_and_decodes
uri = "data:;base64,SGVsbG8="
du = DataUri.new(uri)

assert_equal "text/plain", du.mimetype
assert_equal "Hello", du.decoded_data
end

def test_valid_question_mark_true_for_valid_data_uris
assert DataUri.valid?("data:text/plain,abc")
assert DataUri.valid?("data:,")
assert DataUri.valid?("data:application/json,{\"a\":1}")
end

def test_valid_question_mark_false_for_non_data_uris
refute DataUri.valid?("http://example.com")
refute DataUri.valid?("data;not,a,uri")
end

def test_json_helpers_by_mimetype
uri = "data:application/json,{\"a\":1}"
du = DataUri.new(uri)
assert du.json?
assert_equal({"a"=>1}, du.parse_json)
assert_equal({a: 1}, du.parse_json(symbolize_names: true))
end

def test_json_helpers_by_payload_prefix
json_b64 = "eyJhIjoxfQ==" # {"a":1}
uri = "data:application/octet-stream;base64,#{json_b64}"
du = DataUri.new(uri)
assert du.json?
assert_equal({"a"=>1}, du.parse_json)
end

def test_parse_json_raises_when_not_json
du = DataUri.new("data:text/plain,hello")
error = assert_raises(ArgumentError) { du.parse_json }
assert_equal "not JSON", error.message
end

def test_esip6_param_detection
assert DataUri.esip6?("data:text/plain;rule=esip6,abc")
refute DataUri.esip6?("data:text/plain;rule=other,abc")
refute DataUri.esip6?("not a data uri")
end

def test_esip6_legacy_implicit_form
uri = 'data:,text/plain;rule=esip6,{"p":"erc-20","op":"mint","tick":"eths","amt":"1000"}'
assert DataUri.esip6?(uri)
end

def test_esip6_legacy_implicit_form_other_rule
uri = 'data:,text/plain;rule=other,{"p":"erc-20","op":"mint","tick":"eths","amt":"1000"}'
refute DataUri.esip6?(uri)
end
end