diff --git a/.codespellrc b/.codespellrc index 615f7addeaa6..e9724dc839e6 100644 --- a/.codespellrc +++ b/.codespellrc @@ -2,4 +2,4 @@ check-filenames= check-hidden= ignore-words=.codespellignore -skip=*.pem,.git,man,vcr_cassettes,vendor,net-http +skip=*.pem,.git,man,vcr_cassettes,vendor,net-http,rqrcode_code diff --git a/.rubocop.yml b/.rubocop.yml index 571fc32f8872..819a04acfe7e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -14,6 +14,7 @@ AllCops: - lib/rubygems/optparse/**/* - lib/rubygems/net-protocol/**/* - lib/rubygems/net-http/**/* + - lib/rubygems/rqrcode_core/**/* - bundler/lib/bundler/vendor/**/* CacheRootDirectory: tmp/rubocop MaxFilesInCache: 5000 @@ -241,8 +242,8 @@ Lint/SafeNavigationConsistency: Lint/ScriptPermission: Enabled: true Exclude: - - 'bundler/lib/bundler/templates/Executable' - - 'bundler/lib/bundler/templates/Executable.*' + - "bundler/lib/bundler/templates/Executable" + - "bundler/lib/bundler/templates/Executable.*" Lint/ShadowedArgument: Enabled: true @@ -406,8 +407,8 @@ Layout/SpaceInsideBlockBraces: Enabled: true SpaceBeforeBlockParameters: false Exclude: - - bundler/lib/bundler/templates/Gemfile - - bundler/lib/bundler/templates/gems.rb + - bundler/lib/bundler/templates/Gemfile + - bundler/lib/bundler/templates/gems.rb Layout/SpaceInsideHashLiteralBraces: Enabled: true @@ -494,7 +495,7 @@ Naming/ClassAndModuleCamelCase: Naming/FileName: Enabled: true Exclude: - - 'bundler/spec/realworld/fixtures/warbler/bin/warbler-example.rb' + - "bundler/spec/realworld/fixtures/warbler/bin/warbler-example.rb" Naming/MemoizedInstanceVariableName: Enabled: true diff --git a/Rakefile b/Rakefile index 8c1a8e0f287d..56637658a052 100644 --- a/Rakefile +++ b/Rakefile @@ -187,6 +187,27 @@ if File.exist?("tool/automatiek.rake") lib.license_path = "LICENSE.txt" end + desc "Vendor a specific version of rqrcode_core to rubygems" + Automatiek::RakeTask.new("rqrcode_core") do |lib| + lib.version = "v1.2.0" + lib.download = { github: "https://github.com/whomwah/rqrcode_core" } + lib.namespace = "RQRCodeCore" + lib.prefix = "Gem" + lib.vendor_lib = "lib/rubygems/rqrcode_core" + lib.license_path = "LICENSE.txt" + end + + timeout_dep = ->(lib) { + lib.dependency("timeout") do |timeout| + timeout.version = "v0.4.1" + timeout.download = { github: "https://github.com/ruby/timeout" } + timeout.namespace = "Timeout" + timeout.prefix = "Gem" + timeout.vendor_lib = "lib/rubygems/timeout" + timeout.license_path = "LICENSE.txt" + end + } + # We currently include the following changes over the official version: # * Avoid requiring the optional `net-http-pipeline` dependency, so that its version can be selected by end users. # * Require vendored net/http version RubyGems if available, otherwise the stdlib version. @@ -207,14 +228,7 @@ if File.exist?("tool/automatiek.rake") sublib.vendor_lib = "bundler/lib/bundler/vendor/connection_pool" sublib.license_path = "LICENSE" - sublib.dependency("timeout") do |subsublib| - subsublib.version = "v0.4.1" - subsublib.download = { github: "https://github.com/ruby/timeout" } - subsublib.namespace = "Timeout" - subsublib.prefix = "Gem" - subsublib.vendor_lib = "lib/rubygems/timeout" - subsublib.license_path = "LICENSE.txt" - end + timeout_dep.call(sublib) end lib.dependency("uri") do |sublib| @@ -242,24 +256,10 @@ if File.exist?("tool/automatiek.rake") subsublib.vendor_lib = "lib/rubygems/net-protocol" subsublib.license_path = "LICENSE.txt" - subsublib.dependency("timeout") do |ssslib| - ssslib.version = "v0.4.1" - ssslib.download = { github: "https://github.com/ruby/timeout" } - ssslib.namespace = "Timeout" - ssslib.prefix = "Gem" - ssslib.vendor_lib = "lib/rubygems/timeout" - ssslib.license_path = "LICENSE.txt" - end + timeout_dep.call(subsublib) end - sublib.dependency("timeout") do |subsublib| - subsublib.version = "v0.4.1" - subsublib.download = { github: "https://github.com/ruby/timeout" } - subsublib.namespace = "Timeout" - subsublib.prefix = "Gem" - subsublib.vendor_lib = "lib/rubygems/timeout" - subsublib.license_path = "LICENSE.txt" - end + timeout_dep.call(sublib) sublib.dependency("resolv") do |subsublib| subsublib.version = "v0.2.2" @@ -269,14 +269,7 @@ if File.exist?("tool/automatiek.rake") subsublib.vendor_lib = "lib/rubygems/resolv" subsublib.license_path = "LICENSE.txt" - subsublib.dependency("timeout") do |ssslib| - ssslib.version = "v0.4.1" - ssslib.download = { github: "https://github.com/ruby/timeout" } - ssslib.namespace = "Timeout" - ssslib.prefix = "Gem" - ssslib.vendor_lib = "lib/rubygems/timeout" - ssslib.license_path = "LICENSE.txt" - end + timeout_dep.call(subsublib) end end end diff --git a/lib/rubygems/gemcutter_utilities.rb b/lib/rubygems/gemcutter_utilities.rb index 7fcc0e037d22..eaa19e4034d8 100644 --- a/lib/rubygems/gemcutter_utilities.rb +++ b/lib/rubygems/gemcutter_utilities.rb @@ -2,6 +2,7 @@ require_relative "remote_fetcher" require_relative "text" +require_relative "rqrcode_core/lib/rqrcode_core" require_relative "gemcutter_utilities/webauthn_listener" require_relative "gemcutter_utilities/webauthn_poller" @@ -259,6 +260,7 @@ def fetch_otp(credentials) url_with_port = "#{webauthn_url}?port=#{port}" say "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option." + qrcode Gem::RQRCodeCore::QRCode.new(url_with_port) threads = [WebauthnListener.listener_thread(host, server), WebauthnPoller.poll_thread(options, host, webauthn_url, credentials)] otp_thread = wait_for_otp_thread(*threads) diff --git a/lib/rubygems/rqrcode_core/LICENSE.txt b/lib/rubygems/rqrcode_core/LICENSE.txt new file mode 100644 index 000000000000..0ffd88cf130b --- /dev/null +++ b/lib/rubygems/rqrcode_core/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2008 Duncan Robertson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/rubygems/rqrcode_core/lib/rqrcode_core.rb b/lib/rubygems/rqrcode_core/lib/rqrcode_core.rb new file mode 100644 index 000000000000..56079edc9b85 --- /dev/null +++ b/lib/rubygems/rqrcode_core/lib/rqrcode_core.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Gem::RQRCodeCore + require "rubygems/rqrcode_core/lib/rqrcode_core/qrcode" + require "rubygems/rqrcode_core/lib/rqrcode_core/version" +end diff --git a/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode.rb b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode.rb new file mode 100644 index 000000000000..175b62ad301b --- /dev/null +++ b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "./qrcode/qr_8bit_byte" +require_relative "./qrcode/qr_alphanumeric" +require_relative "./qrcode/qr_bit_buffer" +require_relative "./qrcode/qr_code" +require_relative "./qrcode/qr_math" +require_relative "./qrcode/qr_multi" +require_relative "./qrcode/qr_numeric" +require_relative "./qrcode/qr_polynomial" +require_relative "./qrcode/qr_rs_block" +require_relative "./qrcode/qr_segment" +require_relative "./qrcode/qr_util" diff --git a/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_8bit_byte.rb b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_8bit_byte.rb new file mode 100644 index 000000000000..6f26506ad152 --- /dev/null +++ b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_8bit_byte.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gem::RQRCodeCore + class QR8bitByte + def initialize(data) + @data = data + end + + def write(buffer) + buffer.byte_encoding_start(@data.bytesize) + + @data.each_byte do |b| + buffer.put(b, 8) + end + end + end +end diff --git a/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_alphanumeric.rb b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_alphanumeric.rb new file mode 100644 index 000000000000..646977b76ef8 --- /dev/null +++ b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_alphanumeric.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Gem::RQRCodeCore + ALPHANUMERIC = [ + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", + "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", " ", "$", + "%", "*", "+", "-", ".", "/", ":" + ].freeze + + class QRAlphanumeric + def initialize(data) + unless QRAlphanumeric.valid_data?(data) + raise QRCodeArgumentError, "Not a alpha numeric uppercase string `#{data}`" + end + + @data = data + end + + def self.valid_data?(data) + (data.chars - ALPHANUMERIC).empty? + end + + def write(buffer) + buffer.alphanumeric_encoding_start(@data.size) + + @data.size.times do |i| + if i % 2 == 0 + if i == (@data.size - 1) + value = ALPHANUMERIC.index(@data[i]) + buffer.put(value, 6) + else + value = (ALPHANUMERIC.index(@data[i]) * 45) + ALPHANUMERIC.index(@data[i + 1]) + buffer.put(value, 11) + end + end + end + end + end +end diff --git a/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_bit_buffer.rb b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_bit_buffer.rb new file mode 100644 index 000000000000..e4c7e943da9f --- /dev/null +++ b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_bit_buffer.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module Gem::RQRCodeCore + class QRBitBuffer + attr_reader :buffer + + PAD0 = 0xEC + PAD1 = 0x11 + + def initialize(version) + @version = version + @buffer = [] + @length = 0 + end + + def get(index) + buf_index = (index / 8).floor + ((QRUtil.rszf(@buffer[buf_index], 7 - index % 8)) & 1) == 1 + end + + def put(num, length) + (0...length).each do |i| + put_bit(((QRUtil.rszf(num, length - i - 1)) & 1) == 1) + end + end + + def get_length_in_bits + @length + end + + def put_bit(bit) + buf_index = (@length / 8).floor + if @buffer.size <= buf_index + @buffer << 0 + end + + if bit + @buffer[buf_index] |= QRUtil.rszf(0x80, @length % 8) + end + + @length += 1 + end + + def byte_encoding_start(length) + put(QRMODE[:mode_8bit_byte], 4) + put(length, QRUtil.get_length_in_bits(QRMODE[:mode_8bit_byte], @version)) + end + + def alphanumeric_encoding_start(length) + put(QRMODE[:mode_alpha_numk], 4) + put(length, QRUtil.get_length_in_bits(QRMODE[:mode_alpha_numk], @version)) + end + + def numeric_encoding_start(length) + put(QRMODE[:mode_number], 4) + put(length, QRUtil.get_length_in_bits(QRMODE[:mode_number], @version)) + end + + def pad_until(prefered_size) + # Align on byte + while get_length_in_bits % 8 != 0 + put_bit(false) + end + + # Pad with padding code words + while get_length_in_bits < prefered_size + put(PAD0, 8) + put(PAD1, 8) if get_length_in_bits < prefered_size + end + end + + def end_of_message(max_data_bits) + put(0, 4) unless get_length_in_bits + 4 > max_data_bits + end + end +end diff --git a/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_code.rb b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_code.rb new file mode 100644 index 000000000000..a3fa2b571b57 --- /dev/null +++ b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_code.rb @@ -0,0 +1,510 @@ +# frozen_string_literal: true + +module Gem::RQRCodeCore + QRMODE = { + mode_number: 1 << 0, + mode_alpha_numk: 1 << 1, + mode_8bit_byte: 1 << 2 + }.freeze + + QRMODE_NAME = { + number: :mode_number, + alphanumeric: :mode_alpha_numk, + byte_8bit: :mode_8bit_byte, + multi: :mode_multi + }.freeze + + QRERRORCORRECTLEVEL = { + l: 1, + m: 0, + q: 3, + h: 2 + }.freeze + + QRMASKPATTERN = { + pattern000: 0, + pattern001: 1, + pattern010: 2, + pattern011: 3, + pattern100: 4, + pattern101: 5, + pattern110: 6, + pattern111: 7 + }.freeze + + QRMASKCOMPUTATIONS = [ + proc { |i, j| (i + j) % 2 == 0 }, + proc { |i, j| i % 2 == 0 }, + proc { |i, j| j % 3 == 0 }, + proc { |i, j| (i + j) % 3 == 0 }, + proc { |i, j| ((i / 2).floor + (j / 3).floor) % 2 == 0 }, + proc { |i, j| (i * j) % 2 + (i * j) % 3 == 0 }, + proc { |i, j| ((i * j) % 2 + (i * j) % 3) % 2 == 0 }, + proc { |i, j| ((i * j) % 3 + (i + j) % 2) % 2 == 0 } + ].freeze + + QRPOSITIONPATTERNLENGTH = (7 + 1) * 2 + 1 + QRFORMATINFOLENGTH = 15 + + # http://web.archive.org/web/20110710094955/http://www.denso-wave.com/qrcode/vertable1-e.html + # http://web.archive.org/web/20110710094955/http://www.denso-wave.com/qrcode/vertable2-e.html + # http://web.archive.org/web/20110710094955/http://www.denso-wave.com/qrcode/vertable3-e.html + # http://web.archive.org/web/20110710094955/http://www.denso-wave.com/qrcode/vertable4-e.html + QRMAXBITS = { + l: [152, 272, 440, 640, 864, 1088, 1248, 1552, 1856, 2192, 2592, 2960, 3424, 3688, 4184, + 4712, 5176, 5768, 6360, 6888, 7456, 8048, 8752, 9392, 10_208, 10_960, 11_744, 12_248, + 13_048, 13_880, 14_744, 15_640, 16_568, 17_528, 18_448, 19_472, 20_528, 21_616, 22_496, 23_648], + m: [128, 224, 352, 512, 688, 864, 992, 1232, 1456, 1728, 2032, 2320, 2672, 2920, 3320, 3624, + 4056, 4504, 5016, 5352, 5712, 6256, 6880, 7312, 8000, 8496, 9024, 9544, 10_136, 10_984, + 11_640, 12_328, 13_048, 13_800, 14_496, 15_312, 15_936, 16_816, 17_728, 18_672], + q: [104, 176, 272, 384, 496, 608, 704, 880, 1056, 1232, 1440, 1648, 1952, 2088, 2360, 2600, 2936, + 3176, 3560, 3880, 4096, 4544, 4912, 5312, 5744, 6032, 6464, 6968, 7288, 7880, 8264, 8920, 9368, + 9848, 10288, 10832, 11408, 12016, 12656, 13328], + h: [72, 128, 208, 288, 368, 480, 528, 688, 800, 976, 1120, 1264, 1440, 1576, 1784, + 2024, 2264, 2504, 2728, 3080, 3248, 3536, 3712, 4112, 4304, 4768, 5024, 5288, 5608, 5960, + 6344, 6760, 7208, 7688, 7888, 8432, 8768, 9136, 9776, 10_208] + }.freeze + + # StandardErrors + + class QRCodeArgumentError < ArgumentError; end + + class QRCodeRunTimeError < RuntimeError; end + + # == Creation + # + # QRCode objects expect only one required constructor parameter + # and an optional hash of any other. Here's a few examples: + # + # qr = Gem::RQRCodeCore::QRCode.new('hello world') + # qr = Gem::RQRCodeCore::QRCode.new('hello world', size: 1, level: :m, mode: :alphanumeric) + # + + class QRCode + attr_reader :modules, :module_count, :version + + # Expects a string or array (for multi-segment encoding) to be parsed in, other args are optional + # + # # data - the string, QRSegment or array of Hashes (with data:, mode: keys) you wish to encode + # # size - the size (Integer) of the QR Code (defaults to smallest size needed to encode the data) + # # max_size - the max_size (Integer) of the QR Code (default Gem::RQRCodeCore::QRUtil.max_size) + # # level - the error correction level, can be: + # * Level :l 7% of code can be restored + # * Level :m 15% of code can be restored + # * Level :q 25% of code can be restored + # * Level :h 30% of code can be restored (default :h) + # # mode - the mode of the QR Code (defaults to alphanumeric or byte_8bit, depending on the input data, only used when data is a string): + # * :number + # * :alphanumeric + # * :byte_8bit + # * :kanji + # + # qr = Gem::RQRCodeCore::QRCode.new('hello world', size: 1, level: :m, mode: :alphanumeric) + # segment_qr = QRCodeCore::QRCode.new({ data: 'foo', mode: :byte_8bit }) + # multi_qr = Gem::RQRCodeCore::QRCode.new([{ data: 'foo', mode: :byte_8bit }, { data: 'bar1', mode: :alphanumeric }]) + + def initialize(data, *args) + options = extract_options!(args) + + level = (options[:level] || :h).to_sym + max_size = options[:max_size] || QRUtil.max_size + + @data = case data + when String + QRSegment.new(data: data, mode: options[:mode]) + when Array + raise QRCodeArgumentError, "Array must contain Hashes with :data and :mode keys" unless data.all? { |seg| seg.is_a?(Hash) && %i[data mode].all? { |s| seg.key? s } } + data.map { |seg| QRSegment.new(**seg) } + when QRSegment + data + else + raise QRCodeArgumentError, "data must be a String, QRSegment, or an Array" + end + @error_correct_level = QRERRORCORRECTLEVEL[level] + + unless @error_correct_level + raise QRCodeArgumentError, "Unknown error correction level `#{level.inspect}`" + end + + size = options[:size] || minimum_version(limit: max_size) + + if size > max_size + raise QRCodeArgumentError, "Given size greater than maximum possible size of #{QRUtil.max_size}" + end + + @version = size + @module_count = @version * 4 + QRPOSITIONPATTERNLENGTH + @modules = Array.new(@module_count) + @data_list = multi_segment? ? QRMulti.new(@data) : @data.writer + @data_cache = nil + make + end + + # checked? is called with a +col+ and +row+ parameter. This will + # return true or false based on whether that coordinate exists in the + # matrix returned. It would normally be called while iterating through + # modules. A simple example would be: + # + # instance.checked?( 10, 10 ) => true + # + + def checked?(row, col) + if !row.between?(0, @module_count - 1) || !col.between?(0, @module_count - 1) + raise QRCodeRunTimeError, "Invalid row/column pair: #{row}, #{col}" + end + @modules[row][col] + end + alias_method :dark?, :checked? + extend Gem::Deprecate + deprecate :dark?, :checked?, 2020, 1 + + # This is a public method that returns the QR Code you have + # generated as a string. It will not be able to be read + # in this format by a QR Code reader, but will give you an + # idea if the final outout. It takes two optional args + # +:dark+ and +:light+ which are there for you to choose + # how the output looks. Here's an example of it's use: + # + # instance.to_s => + # xxxxxxx x x x x x xx xxxxxxx + # x x xxx xxxxxx xxx x x + # x xxx x xxxxx x xx x xxx x + # + # instance.to_s( dark: 'E', light: 'Q' ) => + # EEEEEEEQEQQEQEQQQEQEQQEEQQEEEEEEE + # EQQQQQEQQEEEQQEEEEEEQEEEQQEQQQQQE + # EQEEEQEQQEEEEEQEQQQQQQQEEQEQEEEQE + # + + def to_s(*args) + options = extract_options!(args) + dark = options[:dark] || "x" + light = options[:light] || " " + quiet_zone_size = options[:quiet_zone_size] || 0 + + rows = [] + + @modules.each do |row| + cols = light * quiet_zone_size + row.each do |col| + cols += (col ? dark : light) + end + rows << cols + end + + quiet_zone_size.times do + rows.unshift(light * (rows.first.length / light.size)) + rows << light * (rows.first.length / light.size) + end + rows.join("\n") + end + + # Public overide as default inspect is very verbose + # + # Gem::RQRCodeCore::QRCode.new('my string to generate', size: 4, level: :h) + # => QRCodeCore: @data='my string to generate', @error_correct_level=2, @version=4, @module_count=33 + # + + def inspect + "QRCodeCore: @data='#{@data}', @error_correct_level=#{@error_correct_level}, @version=#{@version}, @module_count=#{@module_count}" + end + + # Return a symbol for current error connection level + def error_correction_level + QRERRORCORRECTLEVEL.invert[@error_correct_level] + end + + # Return true if this QR Code includes multiple encoded segments + def multi_segment? + @data.is_a?(Array) + end + + # Return a symbol in QRMODE.keys for current mode used + def mode + case @data_list + when QRNumeric + :mode_number + when QRAlphanumeric + :mode_alpha_numk + else + :mode_8bit_byte + end + end + + protected + + def make #:nodoc: + prepare_common_patterns + make_impl(false, get_best_mask_pattern) + end + + private + + def prepare_common_patterns #:nodoc: + @modules.map! { |row| Array.new(@module_count) } + + place_position_probe_pattern(0, 0) + place_position_probe_pattern(@module_count - 7, 0) + place_position_probe_pattern(0, @module_count - 7) + place_position_adjust_pattern + place_timing_pattern + + @common_patterns = @modules.map(&:clone) + end + + def make_impl(test, mask_pattern) #:nodoc: + @modules = @common_patterns.map(&:clone) + + place_format_info(test, mask_pattern) + place_version_info(test) if @version >= 7 + + if @data_cache.nil? + @data_cache = QRCode.create_data( + @version, @error_correct_level, @data_list + ) + end + + map_data(@data_cache, mask_pattern) + end + + def place_position_probe_pattern(row, col) #:nodoc: + (-1..7).each do |r| + next unless (row + r).between?(0, @module_count - 1) + + (-1..7).each do |c| + next unless (col + c).between?(0, @module_count - 1) + + is_vert_line = (r.between?(0, 6) && (c == 0 || c == 6)) + is_horiz_line = (c.between?(0, 6) && (r == 0 || r == 6)) + is_square = r.between?(2, 4) && c.between?(2, 4) + + is_part_of_probe = is_vert_line || is_horiz_line || is_square + @modules[row + r][col + c] = is_part_of_probe + end + end + end + + def get_best_mask_pattern #:nodoc: + min_lost_point = 0 + pattern = 0 + + (0...8).each do |i| + make_impl(true, i) + lost_point = QRUtil.get_lost_points(modules) + + if i == 0 || min_lost_point > lost_point + min_lost_point = lost_point + pattern = i + end + end + pattern + end + + def place_timing_pattern #:nodoc: + (8...@module_count - 8).each do |i| + @modules[i][6] = @modules[6][i] = i % 2 == 0 + end + end + + def place_position_adjust_pattern #:nodoc: + positions = QRUtil.get_pattern_positions(@version) + + positions.each do |row| + positions.each do |col| + next unless @modules[row][col].nil? + + (-2..2).each do |r| + (-2..2).each do |c| + is_part_of_pattern = (r.abs == 2 || c.abs == 2 || (r == 0 && c == 0)) + @modules[row + r][col + c] = is_part_of_pattern + end + end + end + end + end + + def place_version_info(test) #:nodoc: + bits = QRUtil.get_bch_version(@version) + + (0...18).each do |i| + mod = (!test && ((bits >> i) & 1) == 1) + @modules[(i / 3).floor][ i % 3 + @module_count - 8 - 3 ] = mod + @modules[i % 3 + @module_count - 8 - 3][ (i / 3).floor ] = mod + end + end + + def place_format_info(test, mask_pattern) #:nodoc: + data = (@error_correct_level << 3 | mask_pattern) + bits = QRUtil.get_bch_format_info(data) + + QRFORMATINFOLENGTH.times do |i| + mod = (!test && ((bits >> i) & 1) == 1) + + # vertical + row = if i < 6 + i + elsif i < 8 + i + 1 + else + @module_count - 15 + i + end + @modules[row][8] = mod + + # horizontal + col = if i < 8 + @module_count - i - 1 + elsif i < 9 + 15 - i - 1 + 1 + else + 15 - i - 1 + end + @modules[8][col] = mod + end + + # fixed module + @modules[@module_count - 8][8] = !test + end + + def map_data(data, mask_pattern) #:nodoc: + inc = -1 + row = @module_count - 1 + bit_index = 7 + byte_index = 0 + + (@module_count - 1).step(1, -2) do |col| + col -= 1 if col <= 6 + + loop do + (0...2).each do |c| + if @modules[row][col - c].nil? + dark = false + if byte_index < data.size && !data[byte_index].nil? + dark = ((QRUtil.rszf(data[byte_index], bit_index) & 1) == 1) + end + mask = QRUtil.get_mask(mask_pattern, row, col - c) + dark = !dark if mask + @modules[row][ col - c ] = dark + bit_index -= 1 + + if bit_index == -1 + byte_index += 1 + bit_index = 7 + end + end + end + + row += inc + + if row < 0 || @module_count <= row + row -= inc + inc = -inc + break + end + end + end + end + + def minimum_version(limit: QRUtil.max_size, version: 1) + raise QRCodeRunTimeError, "Data length exceed maximum capacity of version #{limit}" if version > limit + + max_size_bits = QRMAXBITS[error_correction_level][version - 1] + + size_bits = multi_segment? ? @data.sum { |seg| seg.size(version) } : @data.size(version) + + return version if size_bits < max_size_bits + + minimum_version(limit: limit, version: version + 1) + end + + def extract_options!(arr) #:nodoc: + arr.last.is_a?(::Hash) ? arr.pop : {} + end + + class << self + def count_max_data_bits(rs_blocks) #:nodoc: + max_data_bytes = rs_blocks.reduce(0) do |sum, rs_block| + sum + rs_block.data_count + end + + max_data_bytes * 8 + end + + def create_data(version, error_correct_level, data_list) #:nodoc: + rs_blocks = QRRSBlock.get_rs_blocks(version, error_correct_level) + max_data_bits = QRCode.count_max_data_bits(rs_blocks) + buffer = QRBitBuffer.new(version) + + data_list.write(buffer) + buffer.end_of_message(max_data_bits) + + if buffer.get_length_in_bits > max_data_bits + raise QRCodeRunTimeError, "code length overflow. (#{buffer.get_length_in_bits}>#{max_data_bits}). (Try a larger size!)" + end + + buffer.pad_until(max_data_bits) + + QRCode.create_bytes(buffer, rs_blocks) + end + + def create_bytes(buffer, rs_blocks) #:nodoc: + offset = 0 + max_dc_count = 0 + max_ec_count = 0 + dcdata = Array.new(rs_blocks.size) + ecdata = Array.new(rs_blocks.size) + + rs_blocks.each_with_index do |rs_block, r| + dc_count = rs_block.data_count + ec_count = rs_block.total_count - dc_count + max_dc_count = [max_dc_count, dc_count].max + max_ec_count = [max_ec_count, ec_count].max + + dcdata_block = Array.new(dc_count) + dcdata_block.size.times do |i| + dcdata_block[i] = 0xff & buffer.buffer[i + offset] + end + dcdata[r] = dcdata_block + + offset += dc_count + rs_poly = QRUtil.get_error_correct_polynomial(ec_count) + raw_poly = QRPolynomial.new(dcdata[r], rs_poly.get_length - 1) + mod_poly = raw_poly.mod(rs_poly) + + ecdata_block = Array.new(rs_poly.get_length - 1) + ecdata_block.size.times do |i| + mod_index = i + mod_poly.get_length - ecdata_block.size + ecdata_block[i] = mod_index >= 0 ? mod_poly.get(mod_index) : 0 + end + ecdata[r] = ecdata_block + end + + total_code_count = rs_blocks.reduce(0) do |sum, rs_block| + sum + rs_block.total_count + end + + data = Array.new(total_code_count) + index = 0 + + max_dc_count.times do |i| + rs_blocks.size.times do |r| + if i < dcdata[r].size + data[index] = dcdata[r][i] + index += 1 + end + end + end + + max_ec_count.times do |i| + rs_blocks.size.times do |r| + if i < ecdata[r].size + data[index] = ecdata[r][i] + index += 1 + end + end + end + + data + end + end + end +end diff --git a/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_math.rb b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_math.rb new file mode 100644 index 000000000000..9805d24b7e9c --- /dev/null +++ b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_math.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Gem::RQRCodeCore + class QRMath + module_eval { + exp_table = Array.new(256) + log_table = Array.new(256) + + (0...8).each do |i| + exp_table[i] = 1 << i + end + + (8...256).each do |i| + exp_table[i] = exp_table[i - 4] \ + ^ exp_table[i - 5] \ + ^ exp_table[i - 6] \ + ^ exp_table[i - 8] + end + + (0...255).each do |i| + log_table[exp_table[i]] = i + end + + const_set(:EXP_TABLE, exp_table).freeze + const_set(:LOG_TABLE, log_table).freeze + } + + class << self + def glog(n) + raise QRCodeRunTimeError, "glog(#{n})" if n < 1 + LOG_TABLE[n] + end + + def gexp(n) + while n < 0 + n += 255 + end + + while n >= 256 + n -= 255 + end + + EXP_TABLE[n] + end + end + end +end diff --git a/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_multi.rb b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_multi.rb new file mode 100644 index 000000000000..4a2a36fcdf80 --- /dev/null +++ b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_multi.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Gem::RQRCodeCore + class QRMulti + def initialize(data) + @data = data + end + + def write(buffer) + @data.each { |seg| seg.writer.write(buffer) } + end + end +end diff --git a/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_numeric.rb b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_numeric.rb new file mode 100644 index 000000000000..20fc44f7096e --- /dev/null +++ b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_numeric.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Gem::RQRCodeCore + NUMERIC = %w[0 1 2 3 4 5 6 7 8 9].freeze + + class QRNumeric + def initialize(data) + raise QRCodeArgumentError, "Not a numeric string `#{data}`" unless QRNumeric.valid_data?(data) + + @data = data + end + + def self.valid_data?(data) + (data.chars - NUMERIC).empty? + end + + def write(buffer) + buffer.numeric_encoding_start(@data.size) + + @data.size.times do |i| + if i % 3 == 0 + chars = @data[i, 3] + bit_length = get_bit_length(chars.length) + buffer.put(get_code(chars), bit_length) + end + end + end + + private + + NUMBER_LENGTH = { + 3 => 10, + 2 => 7, + 1 => 4 + }.freeze + + def get_bit_length(length) + NUMBER_LENGTH[length] + end + + def get_code(chars) + chars.to_i + end + end +end diff --git a/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_polynomial.rb b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_polynomial.rb new file mode 100644 index 000000000000..ca04a4fb12a9 --- /dev/null +++ b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_polynomial.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Gem::RQRCodeCore + class QRPolynomial + def initialize(num, shift) + raise QRCodeRunTimeError, "#{num.size}/#{shift}" if num.empty? + offset = 0 + + while offset < num.size && num[offset] == 0 + offset += 1 + end + + @num = Array.new(num.size - offset + shift) + + (0...num.size - offset).each do |i| + @num[i] = num[i + offset] + end + end + + def get(index) + @num[index] + end + + def get_length + @num.size + end + + def multiply(e) + num = Array.new(get_length + e.get_length - 1) + + (0...get_length).each do |i| + (0...e.get_length).each do |j| + tmp = num[i + j].nil? ? 0 : num[i + j] + num[i + j] = tmp ^ QRMath.gexp(QRMath.glog(get(i)) + QRMath.glog(e.get(j))) + end + end + + QRPolynomial.new(num, 0) + end + + def mod(e) + if get_length - e.get_length < 0 + return self + end + + ratio = QRMath.glog(get(0)) - QRMath.glog(e.get(0)) + num = Array.new(get_length) + + (0...get_length).each do |i| + num[i] = get(i) + end + + (0...e.get_length).each do |i| + tmp = num[i].nil? ? 0 : num[i] + num[i] = tmp ^ QRMath.gexp(QRMath.glog(e.get(i)) + ratio) + end + + QRPolynomial.new(num, 0).mod(e) + end + end +end diff --git a/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_rs_block.rb b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_rs_block.rb new file mode 100644 index 000000000000..e2d1e382f364 --- /dev/null +++ b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_rs_block.rb @@ -0,0 +1,298 @@ +# frozen_string_literal: true + +module Gem::RQRCodeCore + class QRRSBlock + attr_reader :data_count, :total_count + + def initialize(total_count, data_count) + @total_count = total_count + @data_count = data_count + end + + # http://www.thonky.com/qr-code-tutorial/error-correction-table/ + RS_BLOCK_TABLE = [ + # L + # M + # Q + # H + + # 1 + [1, 26, 19], + [1, 26, 16], + [1, 26, 13], + [1, 26, 9], + + # 2 + [1, 44, 34], + [1, 44, 28], + [1, 44, 22], + [1, 44, 16], + + # 3 + [1, 70, 55], + [1, 70, 44], + [2, 35, 17], + [2, 35, 13], + + # 4 + [1, 100, 80], + [2, 50, 32], + [2, 50, 24], + [4, 25, 9], + + # 5 + [1, 134, 108], + [2, 67, 43], + [2, 33, 15, 2, 34, 16], + [2, 33, 11, 2, 34, 12], + + # 6 + [2, 86, 68], + [4, 43, 27], + [4, 43, 19], + [4, 43, 15], + + # 7 + [2, 98, 78], + [4, 49, 31], + [2, 32, 14, 4, 33, 15], + [4, 39, 13, 1, 40, 14], + + # 8 + [2, 121, 97], + [2, 60, 38, 2, 61, 39], + [4, 40, 18, 2, 41, 19], + [4, 40, 14, 2, 41, 15], + + # 9 + [2, 146, 116], + [3, 58, 36, 2, 59, 37], + [4, 36, 16, 4, 37, 17], + [4, 36, 12, 4, 37, 13], + + # 10 + [2, 86, 68, 2, 87, 69], + [4, 69, 43, 1, 70, 44], + [6, 43, 19, 2, 44, 20], + [6, 43, 15, 2, 44, 16], + + # 11 + [4, 101, 81], + [1, 80, 50, 4, 81, 51], + [4, 50, 22, 4, 51, 23], + [3, 36, 12, 8, 37, 13], + + # 12 + [2, 116, 92, 2, 117, 93], + [6, 58, 36, 2, 59, 37], + [4, 46, 20, 6, 47, 21], + [7, 42, 14, 4, 43, 15], + + # 13 + [4, 133, 107], + [8, 59, 37, 1, 60, 38], + [8, 44, 20, 4, 45, 21], + [12, 33, 11, 4, 34, 12], + + # 14 + [3, 145, 115, 1, 146, 116], + [4, 64, 40, 5, 65, 41], + [11, 36, 16, 5, 37, 17], + [11, 36, 12, 5, 37, 13], + + # 15 + [5, 109, 87, 1, 110, 88], + [5, 65, 41, 5, 66, 42], + [5, 54, 24, 7, 55, 25], + [11, 36, 12, 7, 37, 13], + + # 16 + [5, 122, 98, 1, 123, 99], + [7, 73, 45, 3, 74, 46], + [15, 43, 19, 2, 44, 20], + [3, 45, 15, 13, 46, 16], + + # 17 + [1, 135, 107, 5, 136, 108], + [10, 74, 46, 1, 75, 47], + [1, 50, 22, 15, 51, 23], + [2, 42, 14, 17, 43, 15], + + # 18 + [5, 150, 120, 1, 151, 121], + [9, 69, 43, 4, 70, 44], + [17, 50, 22, 1, 51, 23], + [2, 42, 14, 19, 43, 15], + + # 19 + [3, 141, 113, 4, 142, 114], + [3, 70, 44, 11, 71, 45], + [17, 47, 21, 4, 48, 22], + [9, 39, 13, 16, 40, 14], + + # 20 + [3, 135, 107, 5, 136, 108], + [3, 67, 41, 13, 68, 42], + [15, 54, 24, 5, 55, 25], + [15, 43, 15, 10, 44, 16], + + # 21 + [4, 144, 116, 4, 145, 117], + [17, 68, 42], + [17, 50, 22, 6, 51, 23], + [19, 46, 16, 6, 47, 17], + + # 22 + [2, 139, 111, 7, 140, 112], + [17, 74, 46], + [7, 54, 24, 16, 55, 25], + [34, 37, 13], + + # 23 + [4, 151, 121, 5, 152, 122], + [4, 75, 47, 14, 76, 48], + [11, 54, 24, 14, 55, 25], + [16, 45, 15, 14, 46, 16], + + # 24 + [6, 147, 117, 4, 148, 118], + [6, 73, 45, 14, 74, 46], + [11, 54, 24, 16, 55, 25], + [30, 46, 16, 2, 47, 17], + + # 25 + [8, 132, 106, 4, 133, 107], + [8, 75, 47, 13, 76, 48], + [7, 54, 24, 22, 55, 25], + [22, 45, 15, 13, 46, 16], + + # 26 + [10, 142, 114, 2, 143, 115], + [19, 74, 46, 4, 75, 47], + [28, 50, 22, 6, 51, 23], + [33, 46, 16, 4, 47, 17], + + # 27 + [8, 152, 122, 4, 153, 123], + [22, 73, 45, 3, 74, 46], + [8, 53, 23, 26, 54, 24], + [12, 45, 15, 28, 46, 16], + + # 28 + [3, 147, 117, 10, 148, 118], + [3, 73, 45, 23, 74, 46], + [4, 54, 24, 31, 55, 25], + [11, 45, 15, 31, 46, 16], + + # 29 + [7, 146, 116, 7, 147, 117], + [21, 73, 45, 7, 74, 46], + [1, 53, 23, 37, 54, 24], + [19, 45, 15, 26, 46, 16], + + # 30 + [5, 145, 115, 10, 146, 116], + [19, 75, 47, 10, 76, 48], + [15, 54, 24, 25, 55, 25], + [23, 45, 15, 25, 46, 16], + + # 31 + [13, 145, 115, 3, 146, 116], + [2, 74, 46, 29, 75, 47], + [42, 54, 24, 1, 55, 25], + [23, 45, 15, 28, 46, 16], + + # 32 + [17, 145, 115], + [10, 74, 46, 23, 75, 47], + [10, 54, 24, 35, 55, 25], + [19, 45, 15, 35, 46, 16], + + # 33 + [17, 145, 115, 1, 146, 116], + [14, 74, 46, 21, 75, 47], + [29, 54, 24, 19, 55, 25], + [11, 45, 15, 46, 46, 16], + + # 34 + [13, 145, 115, 6, 146, 116], + [14, 74, 46, 23, 75, 47], + [44, 54, 24, 7, 55, 25], + [59, 46, 16, 1, 47, 17], + + # 35 + [12, 151, 121, 7, 152, 122], + [12, 75, 47, 26, 76, 48], + [39, 54, 24, 14, 55, 25], + [22, 45, 15, 41, 46, 16], + + # 36 + [6, 151, 121, 14, 152, 122], + [6, 75, 47, 34, 76, 48], + [46, 54, 24, 10, 55, 25], + [2, 45, 15, 64, 46, 16], + + # 37 + [17, 152, 122, 4, 153, 123], + [29, 74, 46, 14, 75, 47], + [49, 54, 24, 10, 55, 25], + [24, 45, 15, 46, 46, 16], + + # 38 + [4, 152, 122, 18, 153, 123], + [13, 74, 46, 32, 75, 47], + [48, 54, 24, 14, 55, 25], + [42, 45, 15, 32, 46, 16], + + # 39 + [20, 147, 117, 4, 148, 118], + [40, 75, 47, 7, 76, 48], + [43, 54, 24, 22, 55, 25], + [10, 45, 15, 67, 46, 16], + + # 40 + [19, 148, 118, 6, 149, 119], + [18, 75, 47, 31, 76, 48], + [34, 54, 24, 34, 55, 25], + [20, 45, 15, 61, 46, 16] + + ].freeze + + def self.get_rs_blocks(version, error_correct_level) + rs_block = QRRSBlock.get_rs_block_table(version, error_correct_level) + + if rs_block.nil? + raise QRCodeRunTimeError, + "bad rsblock @ version: #{version}/error_correct_level:#{error_correct_level}" + end + + length = rs_block.size / 3 + list = [] + + (0...length).each do |i| + count = rs_block[i * 3 + 0] + total_count = rs_block[i * 3 + 1] + data_count = rs_block[i * 3 + 2] + + (0...count).each do |j| + list << QRRSBlock.new(total_count, data_count) + end + end + + list + end + + def self.get_rs_block_table(version, error_correct_level) + case error_correct_level + when QRERRORCORRECTLEVEL[:l] + QRRSBlock::RS_BLOCK_TABLE[(version - 1) * 4 + 0] + when QRERRORCORRECTLEVEL[:m] + QRRSBlock::RS_BLOCK_TABLE[(version - 1) * 4 + 1] + when QRERRORCORRECTLEVEL[:q] + QRRSBlock::RS_BLOCK_TABLE[(version - 1) * 4 + 2] + when QRERRORCORRECTLEVEL[:h] + QRRSBlock::RS_BLOCK_TABLE[(version - 1) * 4 + 3] + end + end + end +end diff --git a/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_segment.rb b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_segment.rb new file mode 100644 index 000000000000..dae2c11eee48 --- /dev/null +++ b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_segment.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Gem::RQRCodeCore + class QRSegment + attr_reader :data, :mode + + def initialize(data:, mode: nil) + @data = data + @mode = QRMODE_NAME.dig(mode&.to_sym) + + # If mode is not explicitely found choose mode according to data type + @mode ||= if Gem::RQRCodeCore::QRNumeric.valid_data?(@data) + QRMODE_NAME[:number] + elsif QRAlphanumeric.valid_data?(@data) + QRMODE_NAME[:alphanumeric] + else + QRMODE_NAME[:byte_8bit] + end + end + + def size(version) + 4 + header_size(version) + content_size + end + + def header_size(version) + QRUtil.get_length_in_bits(QRMODE[mode], version) + end + + def content_size + chunk_size, bit_length, extra = case mode + when :mode_number + [3, QRNumeric::NUMBER_LENGTH[3], QRNumeric::NUMBER_LENGTH[data_length % 3] || 0] + when :mode_alpha_numk + [2, 11, 6] + when :mode_8bit_byte + [1, 8, 0] + end + + (data_length / chunk_size) * bit_length + ((data_length % chunk_size) == 0 ? 0 : extra) + end + + def writer + case mode + when :mode_number + QRNumeric.new(data) + when :mode_alpha_numk + QRAlphanumeric.new(data) + when :mode_multi + QRMulti.new(data) + else + QR8bitByte.new(data) + end + end + + private + + def data_length + data.bytesize + end + end +end diff --git a/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_util.rb b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_util.rb new file mode 100644 index 000000000000..8741043aac64 --- /dev/null +++ b/lib/rubygems/rqrcode_core/lib/rqrcode_core/qrcode/qr_util.rb @@ -0,0 +1,256 @@ +# frozen_string_literal: true + +module Gem::RQRCodeCore + class QRUtil + PATTERN_POSITION_TABLE = [ + [], + [6, 18], + [6, 22], + [6, 26], + [6, 30], + [6, 34], + [6, 22, 38], + [6, 24, 42], + [6, 26, 46], + [6, 28, 50], + [6, 30, 54], + [6, 32, 58], + [6, 34, 62], + [6, 26, 46, 66], + [6, 26, 48, 70], + [6, 26, 50, 74], + [6, 30, 54, 78], + [6, 30, 56, 82], + [6, 30, 58, 86], + [6, 34, 62, 90], + [6, 28, 50, 72, 94], + [6, 26, 50, 74, 98], + [6, 30, 54, 78, 102], + [6, 28, 54, 80, 106], + [6, 32, 58, 84, 110], + [6, 30, 58, 86, 114], + [6, 34, 62, 90, 118], + [6, 26, 50, 74, 98, 122], + [6, 30, 54, 78, 102, 126], + [6, 26, 52, 78, 104, 130], + [6, 30, 56, 82, 108, 134], + [6, 34, 60, 86, 112, 138], + [6, 30, 58, 86, 114, 142], + [6, 34, 62, 90, 118, 146], + [6, 30, 54, 78, 102, 126, 150], + [6, 24, 50, 76, 102, 128, 154], + [6, 28, 54, 80, 106, 132, 158], + [6, 32, 58, 84, 110, 136, 162], + [6, 26, 54, 82, 110, 138, 166], + [6, 30, 58, 86, 114, 142, 170] + ].freeze + + G15 = 1 << 10 | 1 << 8 | 1 << 5 | 1 << 4 | 1 << 2 | 1 << 1 | 1 << 0 + G18 = 1 << 12 | 1 << 11 | 1 << 10 | 1 << 9 | 1 << 8 | 1 << 5 | 1 << 2 | 1 << 0 + G15_MASK = 1 << 14 | 1 << 12 | 1 << 10 | 1 << 4 | 1 << 1 + + DEMERIT_POINTS_1 = 3 + DEMERIT_POINTS_2 = 3 + DEMERIT_POINTS_3 = 40 + DEMERIT_POINTS_4 = 10 + + BITS_FOR_MODE = { + QRMODE[:mode_number] => [10, 12, 14], + QRMODE[:mode_alpha_numk] => [9, 11, 13], + QRMODE[:mode_8bit_byte] => [8, 16, 16], + QRMODE[:mode_kanji] => [8, 10, 12] + }.freeze + + def self.max_size + PATTERN_POSITION_TABLE.count + end + + def self.get_bch_format_info(data) + d = data << 10 + while QRUtil.get_bch_digit(d) - QRUtil.get_bch_digit(G15) >= 0 + d ^= (G15 << (QRUtil.get_bch_digit(d) - QRUtil.get_bch_digit(G15))) + end + ((data << 10) | d) ^ G15_MASK + end + + def self.rszf(num, count) + # zero fill right shift + (num >> count) & ((2**((num.size * 8) - count)) - 1) + end + + def self.get_bch_version(data) + d = data << 12 + while QRUtil.get_bch_digit(d) - QRUtil.get_bch_digit(G18) >= 0 + d ^= (G18 << (QRUtil.get_bch_digit(d) - QRUtil.get_bch_digit(G18))) + end + (data << 12) | d + end + + def self.get_bch_digit(data) + digit = 0 + + while data != 0 + digit += 1 + data = QRUtil.rszf(data, 1) + end + + digit + end + + def self.get_pattern_positions(version) + PATTERN_POSITION_TABLE[version - 1] + end + + def self.get_mask(mask_pattern, i, j) + if mask_pattern > QRMASKCOMPUTATIONS.size + raise QRCodeRunTimeError, "bad mask_pattern: #{mask_pattern}" + end + + QRMASKCOMPUTATIONS[mask_pattern].call(i, j) + end + + def self.get_error_correct_polynomial(error_correct_length) + a = QRPolynomial.new([1], 0) + + (0...error_correct_length).each do |i| + a = a.multiply(QRPolynomial.new([1, QRMath.gexp(i)], 0)) + end + + a + end + + def self.get_length_in_bits(mode, version) + if !QRMODE.value?(mode) + raise QRCodeRunTimeError, "Unknown mode: #{mode}" + end + + if version > 40 + raise QRCodeRunTimeError, "Unknown version: #{version}" + end + + if version.between?(1, 9) + # 1 - 9 + macro_version = 0 + elsif version <= 26 + # 10 - 26 + macro_version = 1 + elsif version <= 40 + # 27 - 40 + macro_version = 2 + end + + BITS_FOR_MODE[mode][macro_version] + end + + def self.get_lost_points(modules) + demerit_points = 0 + + demerit_points += QRUtil.demerit_points_1_same_color(modules) + demerit_points += QRUtil.demerit_points_2_full_blocks(modules) + demerit_points += QRUtil.demerit_points_3_dangerous_patterns(modules) + demerit_points += QRUtil.demerit_points_4_dark_ratio(modules) + + demerit_points + end + + def self.demerit_points_1_same_color(modules) + demerit_points = 0 + module_count = modules.size + + # level1 + (0...module_count).each do |row| + (0...module_count).each do |col| + same_count = 0 + dark = modules[row][col] + + (-1..1).each do |r| + next if row + r < 0 || module_count <= row + r + + (-1..1).each do |c| + next if col + c < 0 || module_count <= col + c + next if r == 0 && c == 0 + if dark == modules[row + r][col + c] + same_count += 1 + end + end + end + + if same_count > 5 + demerit_points += (DEMERIT_POINTS_1 + same_count - 5) + end + end + end + + demerit_points + end + + def self.demerit_points_2_full_blocks(modules) + demerit_points = 0 + module_count = modules.size + + # level 2 + (0...(module_count - 1)).each do |row| + (0...(module_count - 1)).each do |col| + count = 0 + count += 1 if modules[row][col] + count += 1 if modules[row + 1][col] + count += 1 if modules[row][col + 1] + count += 1 if modules[row + 1][col + 1] + if count == 0 || count == 4 + demerit_points += DEMERIT_POINTS_2 + end + end + end + + demerit_points + end + + def self.demerit_points_3_dangerous_patterns(modules) + demerit_points = 0 + module_count = modules.size + + # level 3 + modules.each do |row| + (module_count - 6).times do |col_idx| + if row[col_idx] && + !row[col_idx + 1] && + row[col_idx + 2] && + row[col_idx + 3] && + row[col_idx + 4] && + !row[col_idx + 5] && + row[col_idx + 6] + demerit_points += DEMERIT_POINTS_3 + end + end + end + + (0...module_count).each do |col| + (0...(module_count - 6)).each do |row| + if modules[row][col] && + !modules[row + 1][col] && + modules[row + 2][col] && + modules[row + 3][col] && + modules[row + 4][col] && + !modules[row + 5][col] && + modules[row + 6][col] + demerit_points += DEMERIT_POINTS_3 + end + end + end + + demerit_points + end + + def self.demerit_points_4_dark_ratio(modules) + # level 4 + dark_count = modules.reduce(0) do |sum, col| + sum + col.count(true) + end + + ratio = dark_count / (modules.size * modules.size) + ratio_delta = (100 * ratio - 50).abs / 5 + + ratio_delta * DEMERIT_POINTS_4 + end + end +end diff --git a/lib/rubygems/rqrcode_core/lib/rqrcode_core/version.rb b/lib/rubygems/rqrcode_core/lib/rqrcode_core/version.rb new file mode 100644 index 000000000000..b24ea59780fc --- /dev/null +++ b/lib/rubygems/rqrcode_core/lib/rqrcode_core/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Gem::RQRCodeCore + VERSION = "1.2.0" +end diff --git a/lib/rubygems/user_interaction.rb b/lib/rubygems/user_interaction.rb index 0172c4ee897a..641be06618d5 100644 --- a/lib/rubygems/user_interaction.rb +++ b/lib/rubygems/user_interaction.rb @@ -164,6 +164,10 @@ def terminate_interaction(exit_code = 0) def verbose(msg = nil) say(clean_text(msg || yield)) if Gem.configuration.really_verbose end + + def qrcode(qrcode) + ui.qrcode qrcode + end end ## @@ -380,6 +384,42 @@ def progress_reporter(*args) end end + def qrcode(qrcode) + return unless tty? + + quiet_zone_size = 2 + output = [] + fill = "\u2588" + quiet_row = fill * (quiet_zone_size * 2 + qrcode.module_count) + output << quiet_row + chars = { + [true, true] => " ", + [true, false] => "\u2584", + [false, false] => "\u2588", + [false, true] => "\u2580", + } + (qrcode.module_count + 1)./(2).times do |y| + row = fill * quiet_zone_size + previous_dark = false + qrcode.module_count.times do |x| + top = qrcode.checked?(2 * y, x) + if y * 2 == qrcode.module_count - 1 + bottom = false + else + bottom = qrcode.checked?(2 * y + 1, x) + end + + row << chars.fetch([top, bottom]) + end + row << fill * quiet_zone_size + + output << row + end + output << quiet_row + + say(output.join("\n")) + end + ## # An absolutely silent progress reporter.