Created
October 27, 2025 10:59
-
-
Save simi/d5b4800e9eb050dbefb34f218c8e49b4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env ruby | |
| # Fuzzer for JSON float parsing - generates random numbers and verifies parsing | |
| # Tests edge cases, precision limits, and ensures Ryu implementation is robust | |
| require 'json' | |
| require 'set' | |
| class JSONFloatFuzzer | |
| attr_reader :stats | |
| def initialize | |
| @stats = { | |
| total_numbers: 0, | |
| total_batches: 0, | |
| integers: 0, | |
| floats: 0, | |
| scientific: 0, | |
| negative: 0, | |
| zero: 0, | |
| tiny: 0, | |
| huge: 0, | |
| errors: [], | |
| start_time: Time.now | |
| } | |
| end | |
| # Generate random integer | |
| def random_integer | |
| case rand(10) | |
| when 0..3 # Small integers | |
| rand(-1000..1000) | |
| when 4..6 # Medium integers | |
| rand(-1_000_000..1_000_000) | |
| when 7..8 # Large integers | |
| rand(-1_000_000_000_000..1_000_000_000_000) | |
| else # Very large (may hit precision limits) | |
| rand(-10**17..10**17) | |
| end | |
| end | |
| # Generate random decimal number | |
| def random_decimal | |
| # Generate mantissa with varying digit counts | |
| digit_count = case rand(10) | |
| when 0..4 # Short decimals (1-5 digits) | |
| rand(1..5) | |
| when 5..7 # Medium decimals (6-12 digits) | |
| rand(6..12) | |
| else # Long decimals (13-17 digits, tests Ryu limits) | |
| rand(13..17) | |
| end | |
| mantissa = rand(10**(digit_count-1)..10**digit_count - 1) | |
| decimal_places = rand(1..10) | |
| value = mantissa.to_f / (10 ** decimal_places) | |
| rand < 0.3 ? -value : value | |
| end | |
| # Generate random scientific notation number | |
| def random_scientific | |
| mantissa = rand(1.0..10.0).round(rand(1..15)) | |
| exponent = case rand(10) | |
| when 0..3 # Small exponents | |
| rand(-10..10) | |
| when 4..6 # Medium exponents | |
| rand(-100..100) | |
| when 7..8 # Large exponents | |
| rand(-307..307) # Near double precision limits | |
| else # Extreme (may overflow/underflow) | |
| rand(-400..400) | |
| end | |
| value = mantissa * (10 ** exponent) | |
| rand < 0.3 ? -value : value | |
| end | |
| # Generate special edge cases | |
| def random_edge_case | |
| cases = [ | |
| 0.0, | |
| -0.0, | |
| 0.1, | |
| 0.01, | |
| 0.001, | |
| 0.0001, | |
| 1.0, | |
| 10.0, | |
| 100.0, | |
| 1000.0, | |
| 1.7976931348623157e+308, # Max double | |
| 2.2250738585072014e-308, # Min positive normalized | |
| 5e-324, # Min positive subnormal | |
| -1.7976931348623157e+308, # Min double (most negative) | |
| 1e-10, | |
| 1e-20, | |
| 1e-100, | |
| 1e-200, | |
| 1e+10, | |
| 1e+100, | |
| 1e+200, | |
| 3.141592653589793, # π | |
| 2.718281828459045, # e | |
| 1.4142135623730951, # √2 | |
| ] | |
| cases.sample | |
| end | |
| # Generate a random number based on distribution | |
| def generate_number | |
| case rand(100) | |
| when 0..25 # 25% integers | |
| @stats[:integers] += 1 | |
| random_integer | |
| when 26..55 # 30% decimals | |
| @stats[:floats] += 1 | |
| random_decimal | |
| when 56..80 # 25% scientific notation | |
| @stats[:scientific] += 1 | |
| random_scientific | |
| else # 20% edge cases | |
| random_edge_case | |
| end | |
| end | |
| # Generate a batch of random numbers | |
| def generate_batch(size = 1000) | |
| numbers = [] | |
| size.times do | |
| num = generate_number | |
| numbers << num | |
| # Update stats | |
| @stats[:total_numbers] += 1 | |
| @stats[:negative] += 1 if num < 0 | |
| @stats[:zero] += 1 if num == 0 | |
| @stats[:tiny] += 1 if num.abs > 0 && num.abs < 1e-100 | |
| @stats[:huge] += 1 if num.abs > 1e100 | |
| end | |
| numbers | |
| end | |
| # Test a batch of numbers | |
| def test_batch(numbers, batch_num) | |
| # Filter out special values that JSON doesn't support | |
| valid_numbers = numbers.map do |n| | |
| if n.is_a?(Float) && (n.infinite? || n.nan?) | |
| 0.0 # Replace with 0.0 for JSON serialization | |
| else | |
| n | |
| end | |
| end | |
| json_array = valid_numbers.to_json | |
| begin | |
| parsed = JSON.parse(json_array) | |
| # Verify round-trip for integers and simple floats | |
| valid_numbers.each_with_index do |original, i| | |
| parsed_value = parsed[i] | |
| # For integers, should match exactly | |
| if original.is_a?(Integer) | |
| unless original == parsed_value | |
| @stats[:errors] << { | |
| batch: batch_num, | |
| index: i, | |
| original: original, | |
| parsed: parsed_value, | |
| type: :integer_mismatch | |
| } | |
| end | |
| # For floats, check they're close (within floating point precision) | |
| elsif original.is_a?(Float) | |
| # Special handling for zero | |
| if original == 0.0 && parsed_value == 0.0 | |
| next | |
| end | |
| # For normal numbers, check relative error | |
| if original.finite? && parsed_value.finite? | |
| relative_error = ((parsed_value - original).abs / original.abs) | |
| if relative_error > 1e-14 # Allow small precision differences | |
| @stats[:errors] << { | |
| batch: batch_num, | |
| index: i, | |
| original: original, | |
| parsed: parsed_value, | |
| relative_error: relative_error, | |
| type: :precision_error | |
| } | |
| end | |
| # Check special values match | |
| elsif original.infinite? && parsed_value.infinite? | |
| unless (original <=> parsed_value) == 0 | |
| @stats[:errors] << { | |
| batch: batch_num, | |
| index: i, | |
| original: original, | |
| parsed: parsed_value, | |
| type: :infinity_sign_mismatch | |
| } | |
| end | |
| elsif original.nan? != parsed_value.nan? | |
| @stats[:errors] << { | |
| batch: batch_num, | |
| index: i, | |
| original: original, | |
| parsed: parsed_value, | |
| type: :nan_mismatch | |
| } | |
| end | |
| end | |
| end | |
| true | |
| rescue JSON::ParserError => e | |
| @stats[:errors] << { | |
| batch: batch_num, | |
| error: e.message, | |
| json_size: json_array.size, | |
| type: :parse_error | |
| } | |
| false | |
| rescue => e | |
| @stats[:errors] << { | |
| batch: batch_num, | |
| error: "#{e.class}: #{e.message}", | |
| type: :unexpected_error | |
| } | |
| false | |
| end | |
| end | |
| # Print statistics | |
| def print_stats | |
| elapsed = Time.now - @stats[:start_time] | |
| numbers_per_sec = (@stats[:total_numbers] / elapsed).round(1) | |
| puts "\n" + "=" * 80 | |
| puts "Fuzzer Statistics" | |
| puts "=" * 80 | |
| puts "Batches: #{@stats[:total_batches]}" | |
| puts "Total numbers: #{@stats[:total_numbers]} (#{numbers_per_sec}/sec)" | |
| puts "Elapsed time: #{elapsed.round(2)}s" | |
| puts "" | |
| puts "Number types:" | |
| puts " Integers: #{@stats[:integers]} (#{pct(@stats[:integers])}%)" | |
| puts " Floats: #{@stats[:floats]} (#{pct(@stats[:floats])}%)" | |
| puts " Scientific: #{@stats[:scientific]} (#{pct(@stats[:scientific])}%)" | |
| puts "" | |
| puts "Special values:" | |
| puts " Negative: #{@stats[:negative]} (#{pct(@stats[:negative])}%)" | |
| puts " Zero: #{@stats[:zero]}" | |
| puts " Tiny (<1e-100): #{@stats[:tiny]}" | |
| puts " Huge (>1e100): #{@stats[:huge]}" | |
| puts "" | |
| puts "Errors: #{@stats[:errors].size}" | |
| if @stats[:errors].any? | |
| puts "\nError breakdown:" | |
| error_types = @stats[:errors].group_by { |e| e[:type] } | |
| error_types.each do |type, errors| | |
| puts " #{type}: #{errors.size}" | |
| end | |
| puts "\nFirst 5 errors:" | |
| @stats[:errors].first(5).each_with_index do |err, i| | |
| puts "\n Error #{i + 1}:" | |
| err.each do |k, v| | |
| puts " #{k}: #{v}" | |
| end | |
| end | |
| end | |
| puts "=" * 80 | |
| end | |
| # Calculate percentage | |
| def pct(count) | |
| return 0 if @stats[:total_numbers] == 0 | |
| ((count.to_f / @stats[:total_numbers]) * 100).round(1) | |
| end | |
| # Run the fuzzer | |
| def run(batch_size: 1000, batch_count: nil, continuous: false) | |
| puts "JSON Float Fuzzer" | |
| puts "=" * 80 | |
| puts "Batch size: #{batch_size} numbers per batch" | |
| puts "Mode: #{continuous ? 'Continuous (Ctrl-C to stop)' : "#{batch_count} batches"}" | |
| puts "=" * 80 | |
| puts "" | |
| batch_num = 0 | |
| last_print_time = Time.now | |
| last_stats_time = Time.now | |
| loop do | |
| batch_num += 1 | |
| @stats[:total_batches] = batch_num | |
| # Generate and test batch | |
| numbers = generate_batch(batch_size) | |
| success = test_batch(numbers, batch_num) | |
| current_time = Time.now | |
| # Print progress line every 5 seconds | |
| if current_time - last_print_time >= 5.0 | |
| status = success ? "✓" : "✗" | |
| elapsed = (current_time - @stats[:start_time]).round(2) | |
| rate = (@stats[:total_numbers] / (current_time - @stats[:start_time])).round(0) | |
| print "\r[#{elapsed}s] Batch #{batch_num} #{status} | " | |
| print "#{@stats[:total_numbers]} numbers | " | |
| print "#{rate}/sec | " | |
| print "Errors: #{@stats[:errors].size}" | |
| print " " * 20 # Clear any leftover characters | |
| $stdout.flush | |
| last_print_time = current_time | |
| end | |
| # Print detailed stats every 100 batches (but not more often than every 30 seconds) | |
| if batch_num % 100 == 0 && (current_time - last_stats_time >= 30.0 || batch_num == 100) | |
| puts "" # New line before stats | |
| print_stats | |
| last_print_time = current_time # Reset timer after printing stats | |
| last_stats_time = current_time | |
| end | |
| # Stop if not continuous and reached batch count | |
| break if !continuous && batch_count && batch_num >= batch_count | |
| end | |
| puts "\n" | |
| print_stats | |
| if @stats[:errors].empty? | |
| puts "\n✓ SUCCESS! All numbers parsed correctly!" | |
| exit 0 | |
| else | |
| puts "\n✗ ERRORS FOUND! See details above." | |
| exit 1 | |
| end | |
| end | |
| end | |
| # Parse command line arguments | |
| require 'optparse' | |
| options = { | |
| batch_size: 1000, | |
| batch_count: 10, | |
| continuous: false | |
| } | |
| OptionParser.new do |opts| | |
| opts.banner = "Usage: ruby fuzz.rb [options]" | |
| opts.on("-s", "--size SIZE", Integer, "Numbers per batch (default: 1000)") do |s| | |
| options[:batch_size] = s | |
| end | |
| opts.on("-n", "--batches COUNT", Integer, "Number of batches (default: 10)") do |n| | |
| options[:batch_count] = n | |
| end | |
| opts.on("-c", "--continuous", "Run continuously until Ctrl-C") do | |
| options[:continuous] = true | |
| end | |
| opts.on("-h", "--help", "Show this help") do | |
| puts opts | |
| exit | |
| end | |
| end.parse! | |
| # Run fuzzer | |
| fuzzer = JSONFloatFuzzer.new | |
| begin | |
| fuzzer.run( | |
| batch_size: options[:batch_size], | |
| batch_count: options[:batch_count], | |
| continuous: options[:continuous] | |
| ) | |
| rescue Interrupt | |
| puts "\n\nInterrupted by user" | |
| fuzzer.print_stats | |
| exit 0 | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment