diff --git a/.rubocop.yml b/.rubocop.yml index eaf2df8a5..ff46e5ce6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -126,10 +126,12 @@ Style/MapIntoArray: Style/CaseEquality: Exclude: - 'lib/ronin/support/network/ip_range.rb' + - 'lib/ronin/support/network/wildcard.rb' - 'spec/network/ip_range_spec.rb' - 'spec/network/ip_range/cidr_spec.rb' - 'spec/network/ip_range/glob_spec.rb' - 'spec/network/ip_range/range_spec.rb' + - 'spec/network/wildcard_spec.rb' Style/ConditionalAssignment: Exclude: diff --git a/README.md b/README.md index 69517c65d..010deafbf 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,14 @@ research and development. * [Hex][docs-encoding-hex] * [HTML][docs-encoding-html] * [HTTP][docs-encoding-http] + * [Java][docs-encoding-java] * [JavaScript][docs-encoding-js] + * [Node.js][docs-encoding-node-js] * [PowerShell][docs-encoding-powershell] * [Punycode][docs-encoding-punycode] * [Quoted-printable][docs-encoding-quoted-printable] + * [Perl strings][docs-encoding-perl] + * [PHP strings][docs-encoding-php] * [Ruby strings][docs-encoding-ruby] * [Shell][docs-encoding-shell] * [SQL][docs-encoding-sql] @@ -90,10 +94,14 @@ research and development. * [Public Suffix List][docs-network-public_suffix] * [Host names][docs-network-host] * [Domain names][docs-network-domain] + * [Defangin / Refanging][docs-network-defang] * Working with text: * [Generating typos][docs-text-typo]. * [Generating homoglyphs][docs-text-homoglyp]. * [Regexs for matching/extracting common types of data][docs-text-patterns]. + * Working with software versions: + * [Parsing versions][docs-software-version]. + * [Version ranges][docs-software-version_range]. * Adds additional methods to many of [Ruby's core classes][docs-core-exts]. * Small memory footprint (~46Kb). * Has 96% documentation coverage. @@ -205,10 +213,14 @@ along with ronin-support. If not, see . [docs-encoding-hex]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Encoding/Hex.html [docs-encoding-html]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Encoding/HTML.html [docs-encoding-http]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Encoding/HTTP.html +[docs-encoding-java]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Encoding/Java.html [docs-encoding-js]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Encoding/JS.html +[docs-encoding-node-js]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Encoding/NodeJS.html [docs-encoding-powershell]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Encoding/PowerShell.html [docs-encoding-punycode]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Encoding/Punycode.html [docs-encoding-quoted-printable]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Encoding/QuotedPrintable.html +[docs-encoding-perl]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Encoding/Perl.html +[docs-encoding-php]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Encoding/PHP.html [docs-encoding-ruby]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Encoding/Ruby.html [docs-encoding-shell]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Encoding/Shell.html [docs-encoding-sql]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Encoding/SQL.html @@ -250,7 +262,10 @@ along with ronin-support. If not, see . [docs-network-public_suffix]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/PublicSuffix.html [docs-network-host]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/Host.html [docs-network-domain]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/Domain.html +[docs-network-defang]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/Defang.html [docs-text-typo]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Text/Typo.html [docs-text-homoglyp]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Text/Homoglyph.html [docs-text-patterns]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Text/Patterns.html +[docs-software-version]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Software/Version.html +[docs-software-version_range]: https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Software/VersionRange.html [docs-core-exts]: https://ronin-rb.dev/docs/ronin-support/top-level-namespace.html diff --git a/lib/ronin/support.rb b/lib/ronin/support.rb index e755b03e1..e616ce7f0 100644 --- a/lib/ronin/support.rb +++ b/lib/ronin/support.rb @@ -25,6 +25,7 @@ require 'ronin/support/network' require 'ronin/support/path' require 'ronin/support/text' +require 'ronin/support/software' require 'ronin/support/version' module Ronin diff --git a/lib/ronin/support/binary/ctypes.rb b/lib/ronin/support/binary/ctypes.rb index f23d9f87e..0af8e58a1 100644 --- a/lib/ronin/support/binary/ctypes.rb +++ b/lib/ronin/support/binary/ctypes.rb @@ -24,7 +24,7 @@ require 'ronin/support/binary/ctypes/os' require 'ronin/support/binary/ctypes/array_type' -require 'ronin/support/binary/ctypes/unbounded_array_type' +require 'ronin/support/binary/ctypes/flexible_array_type' require 'ronin/support/binary/ctypes/struct_type' require 'ronin/support/binary/ctypes/array_object_type' @@ -65,7 +65,7 @@ module Binary # `1.79769313486231570E+308`) # * Aggregate Types: # * {CTypes::ArrayType Array} (ex: `{1,2,3}`) - # * {CTypes::UnboundedArrayType unbounded Array} (ex: `char c[] = {...}`) + # * {CTypes::FlexibleArrayType unbounded Array} (ex: `char c[] = {...}`) # * {CTypes::StructType struct} (ex: `struct s = {1, 'c', ...}`) # * {CTypes::UnionType union} (ex: `union u = {1234}`) # * Object Types: diff --git a/lib/ronin/support/binary/ctypes/array_type.rb b/lib/ronin/support/binary/ctypes/array_type.rb index 5791ab8c1..f8368de97 100644 --- a/lib/ronin/support/binary/ctypes/array_type.rb +++ b/lib/ronin/support/binary/ctypes/array_type.rb @@ -17,7 +17,7 @@ # require 'ronin/support/binary/ctypes/aggregate_type' -require 'ronin/support/binary/ctypes/unbounded_array_type' +require 'ronin/support/binary/ctypes/flexible_array_type' module Ronin module Support @@ -60,8 +60,8 @@ class ArrayType < AggregateType # Custom type alignment to override the type's alignment. # def initialize(type,length, alignment: nil) - if type.kind_of?(UnboundedArrayType) - raise(ArgumentError,"cannot initialize an #{self.class} of #{UnboundedArrayType}") + if type.kind_of?(FlexibleArrayType) + raise(ArgumentError,"cannot initialize an #{self.class} of #{FlexibleArrayType}") end @type = type diff --git a/lib/ronin/support/binary/ctypes/unbounded_array_type.rb b/lib/ronin/support/binary/ctypes/flexible_array_type.rb similarity index 97% rename from lib/ronin/support/binary/ctypes/unbounded_array_type.rb rename to lib/ronin/support/binary/ctypes/flexible_array_type.rb index 8b5c0b935..4ad7c9895 100644 --- a/lib/ronin/support/binary/ctypes/unbounded_array_type.rb +++ b/lib/ronin/support/binary/ctypes/flexible_array_type.rb @@ -28,9 +28,9 @@ module CTypes # # @api private # - # @since 1.0.0 + # @since 1.2.0 # - class UnboundedArrayType < AggregateType + class FlexibleArrayType < AggregateType # The type of each element in the unbounded array type. # @@ -44,11 +44,11 @@ class UnboundedArrayType < AggregateType # The type of each element in the unbounded array type. # # @raise [ArgumentError] - # Cannot initialize a nested {UnboundedArrayType}. + # Cannot initialize a nested {FlexibleArrayType}. # def initialize(type, alignment: nil) - if type.kind_of?(UnboundedArrayType) - raise(ArgumentError,"cannot initialize a nested #{UnboundedArrayType}") + if type.kind_of?(FlexibleArrayType) + raise(ArgumentError,"cannot initialize a nested #{FlexibleArrayType}") end @type = type diff --git a/lib/ronin/support/binary/ctypes/type.rb b/lib/ronin/support/binary/ctypes/type.rb index b808f9533..f1a984eb7 100644 --- a/lib/ronin/support/binary/ctypes/type.rb +++ b/lib/ronin/support/binary/ctypes/type.rb @@ -53,13 +53,13 @@ def initialize(pack_string: ) # @param [Integer, nil] length # The length of the Array. # - # @return [ArrayType, UnboundedArrayType] + # @return [ArrayType, FlexibleArrayType] # The new Array type or an unbounded Array type if `length` was not # given. # def [](length=nil) if length then ArrayType.new(self,length) - else UnboundedArrayType.new(self) + else FlexibleArrayType.new(self) end end diff --git a/lib/ronin/support/binary/ctypes/type_resolver.rb b/lib/ronin/support/binary/ctypes/type_resolver.rb index 01418dae6..f9c2a76f2 100644 --- a/lib/ronin/support/binary/ctypes/type_resolver.rb +++ b/lib/ronin/support/binary/ctypes/type_resolver.rb @@ -142,7 +142,7 @@ def resolve_array(type_signature) # # @param [Range] type_signature # - # @return [UnboundedArrayType] + # @return [FlexibleArrayType] # def resolve_range(type_signature) range = type_signature diff --git a/lib/ronin/support/binary/struct.rb b/lib/ronin/support/binary/struct.rb index 2c9111b62..31da61d18 100644 --- a/lib/ronin/support/binary/struct.rb +++ b/lib/ronin/support/binary/struct.rb @@ -126,7 +126,7 @@ module Binary # struct.pack # # => "\x00\x00\x00\x00\x01\x02\x03\x04\x00\x00\x00\x00\x00\x00" # - # ### Unbounded Array Fields + # ### Flexible Array Fields # # class MyStruct < Ronin::Support::Binary::Struct # @@ -517,7 +517,7 @@ def self.read_from(io) def [](name) if (member = @type.members[name]) case member.type - when CTypes::UnboundedArrayType + when CTypes::FlexibleArrayType # XXX: but how do we handle an unbounded array of structs? @cache[name] ||= begin offset = member.offset diff --git a/lib/ronin/support/binary/union.rb b/lib/ronin/support/binary/union.rb index a933a565d..7c64874c6 100644 --- a/lib/ronin/support/binary/union.rb +++ b/lib/ronin/support/binary/union.rb @@ -121,7 +121,7 @@ module Binary # union.pack # # => "\x01\x02\x03\x04\x00\x00\x00\x00\x00\x00" # - # ### Unbounded Array Fields + # ### Flexible Array Fields # # class MyUnion < Ronin::Support::Binary::Union # diff --git a/lib/ronin/support/crypto.rb b/lib/ronin/support/crypto.rb index 85ed5d7c9..5eff5fefd 100644 --- a/lib/ronin/support/crypto.rb +++ b/lib/ronin/support/crypto.rb @@ -19,6 +19,7 @@ require 'ronin/support/crypto/openssl' require 'ronin/support/crypto/hmac' require 'ronin/support/crypto/cipher' +require 'ronin/support/crypto/cipher/des3' require 'ronin/support/crypto/cipher/aes' require 'ronin/support/crypto/cipher/aes128' require 'ronin/support/crypto/cipher/aes256' @@ -289,6 +290,105 @@ def self.decrypt(data, cipher: ,**kwargs) self.cipher(cipher, direction: :decrypt, **kwargs).decrypt(data) end + # + # Creates a new DES3 cipher. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments for {Cipher::DES3#initialize}. + # + # @option kwargs [:wrap, Symbol, nil] :mode + # The desired DES3 cipher mode. + # + # @option kwargs [String] :key + # The secret key to use. + # + # @option kwargs [String] :iv + # The optional Initial Vector (IV). + # + # @option kwargs [Integer] :padding + # Sets the padding for the cipher. + # + # @return [Cipher::DES3] + # The new DES3 cipher. + # + # @example + # Crypto.des3_cipher(direction: :encrypt, key: 'A' * 24) + # # => # + # + # @see Cipher::DES3 + # + # @since 1.2.0 + # + def self.des3_cipher(**kwargs) + Cipher::DES3.new(**kwargs) + end + + # + # Encrypts data using DES3. + # + # @param [#to_s] data + # The data to encrypt. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments for {Cipher::DES3#initialize}. + # + # @option kwargs [:wrap, Symbol, nil] :mode + # The desired DES3 cipher mode. + # + # @option kwargs [String] :key + # The secret key to use. + # + # @option kwargs [String] :iv + # The optional Initial Vector (IV). + # + # @option kwargs [Integer] :padding + # Sets the padding for the cipher. + # + # @return [String] + # The encrypted data. + # + # @raise [ArgumentError] + # The `key:` keyword argument must be given. + # + # @since 1.2.0 + # + def self.des3_encrypt(data,**kwargs) + self.des3_cipher(direction: :encrypt, **kwargs).encrypt(data) + end + + # + # Decrypts data using DES3. + # + # @param [#to_s] data + # The data to decrypt. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments for {Cipher::DES3#initialize}. + # + # @option kwargs [:wrap, Symbol, nil] :mode + # The desired DES3 cipher mode. + # + # @option kwargs [String] :key + # The secret key to use. + # + # @option kwargs [String] :iv + # The optional Initial Vector (IV). + # + # @option kwargs [Integer] :padding + # Sets the padding for the cipher. + # + # @return [String] + # The encrypted data. + # + # @raise [ArgumentError] + # The `key:` keyword argument must be given. + # + # @since 1.2.0 + # + def self.des3_decrypt(data,**kwargs) + self.des3_cipher(direction: :decrypt, **kwargs).decrypt(data) + end + # # Creates a new AES cipher. # diff --git a/lib/ronin/support/crypto/cipher/des3.rb b/lib/ronin/support/crypto/cipher/des3.rb new file mode 100644 index 000000000..67dea8e38 --- /dev/null +++ b/lib/ronin/support/crypto/cipher/des3.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# Ronin Support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ronin Support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Ronin Support. If not, see . +# + +require 'ronin/support/crypto/cipher' + +module Ronin + module Support + module Crypto + class Cipher < OpenSSL::Cipher + # + # The DES3 cipher. + # + # @since 1.2.0 + # + class DES3 < Cipher + + # The DES3 cipher mode. + # + # @return [:wrap, Symbol, nil] + attr_reader :mode + + # + # Initializes the DES3 cipher. + # + # @param [:wrap, Symbol, nil] mode + # The desired DES3 cipher mode. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments for {Cipher#initialize}. + # + def initialize(mode: nil, **kwargs) + name = if mode then "des3-#{mode}" + else "des3" + end + + super(name, **kwargs) + + @mode = mode + end + + # + # The list of supported DES3 ciphers. + # + # @return [Array] + # The list of supported DES3 cipher names. + # + def self.supported + super().grep(/^des3/) + end + + end + end + end + end +end diff --git a/lib/ronin/support/crypto/core_ext/file.rb b/lib/ronin/support/crypto/core_ext/file.rb index 1b24fb643..0dba583bd 100644 --- a/lib/ronin/support/crypto/core_ext/file.rb +++ b/lib/ronin/support/crypto/core_ext/file.rb @@ -303,6 +303,100 @@ def self.decrypt(path,cipher, block_size: 16384, output: nil, **kwargs,&block) return cipher.stream(file, block_size: block_size, output: output,&block) end + # + # Encrypts the file using DES3. + # + # @param [String] path + # The path to the file. + # + # @param [Integer] block_size + # Reads data from the file in chunks of the given block size. + # + # @param [String, #<<, nil] output + # The optional output buffer to append the AES encrypted data to. + # Defaults to an empty ASCII 8bit encoded String. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments for {Ronin::Support::Crypto.des3_cipher}. + # + # @option kwargs [:wrap, Symbol, nil] :mode + # The desired DES3 cipher mode. + # + # @option kwargs [String] :key + # The secret key to use. + # + # @option kwargs [String] :iv + # The optional Initial Vector (IV). + # + # @option kwargs [Integer] :padding + # Sets the padding for the cipher. + # + # @return [String] + # The encrypted data. + # + # @raise [ArgumentError] + # The `key:` keyword argument must be given. + # + # @example + # File.des3_encrypt('file.txt', key: 'A' * 24) + # # => "..." + # + # @since 1.2.0 + # + def self.des3_encrypt(path, block_size: 16384, output: nil, **kwargs,&block) + cipher = Ronin::Support::Crypto.des3_cipher(direction: :encrypt, **kwargs) + file = File.open(path,'rb') + + return cipher.stream(file, block_size: block_size, output: output,&block) + end + + # + # Decrypts the file using DES3. + # + # @param [String] path + # The path to the file. + # + # @param [Integer] block_size + # Reads data from the file in chunks of the given block size. + # + # @param [String, #<<, nil] output + # The optional output buffer to append the AES encrypted data to. + # Defaults to an empty ASCII 8bit encoded String. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments for {Ronin::Support::Crypto.des3_cipher}. + # + # @option kwargs [:wrap, Symbol, nil] :mode + # The desired DES3 cipher mode. + # + # @option kwargs [String] :key + # The secret key to use. + # + # @option kwargs [String] :iv + # The optional Initial Vector (IV). + # + # @option kwargs [Integer] :padding + # Sets the padding for the cipher. + # + # @return [String] + # The decrypted data. + # + # @raise [ArgumentError] + # The `key:` keyword argument must be given. + # + # @example + # File.des3_decrypt('encrypted.bin', key: 'A' * 24) + # # => "..." + # + # @since 1.2.0 + # + def self.des3_decrypt(path, block_size: 16384, output: nil, **kwargs,&block) + cipher = Ronin::Support::Crypto.des3_cipher(direction: :decrypt, **kwargs) + file = File.open(path,'rb') + + return cipher.stream(file, block_size: block_size, output: output,&block) + end + # # Encrypts the file using AES. # diff --git a/lib/ronin/support/crypto/core_ext/string.rb b/lib/ronin/support/crypto/core_ext/string.rb index 6703abd2f..8f7d73c58 100644 --- a/lib/ronin/support/crypto/core_ext/string.rb +++ b/lib/ronin/support/crypto/core_ext/string.rb @@ -219,6 +219,66 @@ def decrypt(cipher,**kwargs) **kwargs) end + # + # Encrypts data using DES3. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments for {Ronin::Support::Crypto.des3_encrypt}. + # + # @option kwargs [:wrap, Symbol, nil] :mode + # The desired DES3 cipher mode. + # + # @option kwargs [String] :key + # The secret key to use. + # + # @option kwargs [String] :iv + # The optional Initial Vector (IV). + # + # @option kwargs [Integer] :padding + # Sets the padding for the cipher. + # + # @return [String] + # The encrypted data. + # + # @raise [ArgumentError] + # The `key:` keyword argument must be given. + # + # @since 1.2.0 + # + def des3_encrypt(**kwargs) + Ronin::Support::Crypto.des3_encrypt(self,**kwargs) + end + + # + # Decrypts data using DES3. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments for {Ronin::Support::Crypto.des3_decrypt}. + # + # @option kwargs [:wrap, Symbol, nil] :mode + # The desired DES3 cipher mode. + # + # @option kwargs [String] :key + # The secret key to use. + # + # @option kwargs [String] :iv + # The optional Initial Vector (IV). + # + # @option kwargs [Integer] :padding + # Sets the padding for the cipher. + # + # @return [String] + # The encrypted data. + # + # @raise [ArgumentError] + # The `key:` keyword argument must be given. + # + # @since 1.2.0 + # + def des3_decrypt(**kwargs) + Ronin::Support::Crypto.des3_decrypt(self,**kwargs) + end + # # Encrypts the String using AES. # diff --git a/lib/ronin/support/crypto/mixin.rb b/lib/ronin/support/crypto/mixin.rb index 46577b560..fdcb7c883 100644 --- a/lib/ronin/support/crypto/mixin.rb +++ b/lib/ronin/support/crypto/mixin.rb @@ -205,6 +205,115 @@ def crypto_decrypt(data, cipher: ,**kwargs) alias decrypt crypto_decrypt + # + # Creates a new DES3 cipher. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments for {Cipher::DES3#initialize}. + # + # @option kwargs [:wrap, Symbol, nil] :mode + # The desired AES cipher mode. + # + # @option kwargs [String] :key + # The secret key to use. + # + # @option kwargs [String] :iv + # The optional Initial Vector (IV). + # + # @option kwargs [Integer] :padding + # Sets the padding for the cipher. + # + # @return [Cipher::DES3] + # The new DES3 cipher. + # + # @example + # crypto_des3_cipher(direction: :encrypt, key: 'A' * 24) + # # => # + # + # @see Crypto.des3_cipher + # + # @since 1.2.0 + # + def crypto_des3_cipher(**kwargs) + Crypto.des3_cipher(**kwargs) + end + + alias des3_cipher crypto_des3_cipher + + # + # Encrypts data using DES3. + # + # @param [#to_s] data + # The data to encrypt. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments for {Cipher::DES3#initialize}. + # + # @option kwargs [:wrap, Symbol, nil] :mode + # The desired DES3 cipher mode. + # + # @option kwargs [String] :key + # The secret key to use. + # + # @option kwargs [String] :iv + # The optional Initial Vector (IV). + # + # @option kwargs [Integer] :padding + # Sets the padding for the cipher. + # + # @return [String] + # The encrypted data. + # + # @raise [ArgumentError] + # The `key:` keyword argument must be given. + # + # @see Crypto.des3_encrypt + # + # @since 1.2.0 + # + def crypto_des3_encrypt(data,**kwargs) + Crypto.des3_encrypt(data,**kwargs) + end + + alias des3_encrypt crypto_des3_encrypt + + # + # Decrypts data using DES3. + # + # @param [#to_s] data + # The data to decrypt. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments for {Cipher::DES3#initialize}. + # + # @option kwargs [:wrap, Symbol, nil] :mode + # The desired DES3 cipher mode. + # + # @option kwargs [String] :key + # The secret key to use. + # + # @option kwargs [String] :iv + # The optional Initial Vector (IV). + # + # @option kwargs [Integer] :padding + # Sets the padding for the cipher. + # + # @return [String] + # The encrypted data. + # + # @raise [ArgumentError] + # The `key:` keyword argument must be given. + # + # @see Crypto.des3_decrypt + # + # @since 1.2.0 + # + def crypto_des3_decrypt(data,**kwargs) + Crypto.des3_decrypt(data,**kwargs) + end + + alias des3_decrypt crypto_des3_decrypt + # # Creates a new AES cipher. # diff --git a/lib/ronin/support/encoding.rb b/lib/ronin/support/encoding.rb index b0dec99b4..c756b1819 100644 --- a/lib/ronin/support/encoding.rb +++ b/lib/ronin/support/encoding.rb @@ -28,9 +28,15 @@ require 'ronin/support/encoding/http' require 'ronin/support/encoding/xml' require 'ronin/support/encoding/html' +require 'ronin/support/encoding/java' require 'ronin/support/encoding/js' +require 'ronin/support/encoding/node_js' require 'ronin/support/encoding/sql' require 'ronin/support/encoding/quoted_printable' +require 'ronin/support/encoding/smtp' +require 'ronin/support/encoding/perl' +require 'ronin/support/encoding/php' +require 'ronin/support/encoding/python' require 'ronin/support/encoding/ruby' require 'ronin/support/encoding/uri' require 'ronin/support/encoding/punycode' @@ -92,12 +98,33 @@ module Support # * {String#http_encode} # * {String#http_escape} # * {String#http_unescape} + # * {Integer#java_escape} + # * {Integer#java_encode} + # * {String#java_escape} + # * {String#java_unescape} + # * {String#java_encode} + # * {String#java_string} + # * {String#java_unquote} # * {String#js_decode} # * {String#js_encode} # * {String#js_escape} # * {String#js_string} # * {String#js_unescape} # * {String#js_unquote} + # * {Integer#node_js_escape} + # * {Integer#node_js_encode} + # * {String#node_js_escape} + # * {String#node_js_unescape} + # * {String#node_js_encode} + # * {String#node_js_decode} + # * {String#node_js_string} + # * {String#node_js_unquote} + # * {String#php_escape} + # * {String#php_unescape} + # * {String#php_encode} + # * {String#php_decode} + # * {String#php_string} + # * {String#php_unquote} # * {String#powershell_decode} # * {String#powershell_encode} # * {String#powershell_escape} @@ -106,8 +133,17 @@ module Support # * {String#powershell_unquote} # * {String#punycode_decode} # * {String#punycode_encode} + # * {Integer#python_escape} + # * {Integer#python_encode} + # * {String#python_escape} + # * {String#python_unescape} + # * {String#python_encode} + # * {String#python_string} + # * {String#python_unquote} # * {String#quoted_printable_escape} # * {String#quoted_printable_unescape} + # * {String#smtp_escape} + # * {String#smtp_unescape} # * {String#ruby_decode} # * {String#ruby_encode} # * {String#ruby_escape} diff --git a/lib/ronin/support/encoding/base64.rb b/lib/ronin/support/encoding/base64.rb index 0f10a32cd..49d799d2a 100644 --- a/lib/ronin/support/encoding/base64.rb +++ b/lib/ronin/support/encoding/base64.rb @@ -16,8 +16,6 @@ # along with ronin-support. If not, see . # -require 'base64' - module Ronin module Support class Encoding < ::Encoding @@ -48,9 +46,9 @@ module Base64 # def self.encode(data, mode: nil) case mode - when :strict then ::Base64.strict_encode64(data) - when :url_safe then ::Base64.urlsafe_encode64(data) - when nil then ::Base64.encode64(data) + when :strict then strict_encode64(data) + when :url_safe then urlsafe_encode64(data) + when nil then encode64(data) else raise(ArgumentError,"Base64 mode must be either :string, :url_safe, or nil: #{mode.inspect}") end @@ -70,13 +68,100 @@ def self.encode(data, mode: nil) # def self.decode(data, mode: nil) case mode - when :strict then ::Base64.strict_decode64(data) - when :url_safe then ::Base64.urlsafe_decode64(data) - when nil then ::Base64.decode64(data) + when :strict then strict_decode64(data) + when :url_safe then urlsafe_decode64(data) + when nil then decode64(data) else raise(ArgumentError,"Base64 mode must be either :string, :url_safe, or nil: #{mode.inspect}") end end + + # + # Base64 encodes the given data. + # + # @param [String] data + # The data to Base64 encode. + # + # @return [String] + # The Base64 encoded data. + # + def self.encode64(data) + [data].pack("m") + end + + # + # Base64 decodes the given data. + # + # @param [String] data + # The Base64 data to decode. + # + # @return [String] + # The decoded data. + # + def self.decode64(data) + data.unpack1("m") + end + + # + # Base64 strict encodes the given data. + # + # @param [String] data + # The data to Base64 encode. + # + # @return [String] + # The Base64 strict encoded data. + # + def self.strict_encode64(data) + [data].pack("m0") + end + + # + # Base64 strict decodes the given data. + # + # @param [String] data + # The Base64 data to decode. + # + # @return [String] + # The strict decoded data. + # + def self.strict_decode64(data) + data.unpack1("m0") + end + + # + # Base64 url-safe encodes the given data. + # + # @param [String] data + # The data to Base64 encode. + # + # @return [String] + # The Base64 url-safe encoded data. + # + def self.urlsafe_encode64(data, padding: true) + str = strict_encode64(data) + str.chomp!("==") or str.chomp!("=") unless padding + str.tr!("+/", "-_") + str + end + + # + # Base64 url-safe decodes the given data. + # + # @param [String] data + # The Base64 data to decode. + # + # @return [String] + # The url-safe decoded data. + # + def self.urlsafe_decode64(data) + if !data.end_with?("=") && data.length % 4 != 0 + data = data.ljust((str.length + 3) & ~3, "=") + data.tr!("-_", "+/") + else + data = data.tr("-_", "+/") + end + strict_decode64(data) + end end end end diff --git a/lib/ronin/support/encoding/core_ext.rb b/lib/ronin/support/encoding/core_ext.rb index 4aadf5e88..c7a4f8b0e 100644 --- a/lib/ronin/support/encoding/core_ext.rb +++ b/lib/ronin/support/encoding/core_ext.rb @@ -25,9 +25,15 @@ require 'ronin/support/encoding/powershell/core_ext' require 'ronin/support/encoding/html/core_ext' require 'ronin/support/encoding/http/core_ext' +require 'ronin/support/encoding/java/core_ext' require 'ronin/support/encoding/js/core_ext' +require 'ronin/support/encoding/node_js/core_ext' require 'ronin/support/encoding/sql/core_ext' require 'ronin/support/encoding/xml/core_ext' require 'ronin/support/encoding/quoted_printable/core_ext' +require 'ronin/support/encoding/smtp/core_ext' +require 'ronin/support/encoding/perl/core_ext' +require 'ronin/support/encoding/php/core_ext' +require 'ronin/support/encoding/python/core_ext' require 'ronin/support/encoding/ruby/core_ext' require 'ronin/support/encoding/uri/core_ext' diff --git a/lib/ronin/support/encoding/java.rb b/lib/ronin/support/encoding/java.rb new file mode 100644 index 000000000..d3c2faf5d --- /dev/null +++ b/lib/ronin/support/encoding/java.rb @@ -0,0 +1,300 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'strscan' + +module Ronin + module Support + class Encoding < ::Encoding + # + # Contains methods for encoding/decoding escaping/unescaping Java data. + # + # ## Core-Ext Methods + # + # * {Integer#java_escape} + # * {Integer#java_encode} + # * {String#java_escape} + # * {String#java_unescape} + # * {String#java_encode} + # * {String#java_string} + # * {String#java_unquote} + # + # @see https://docs.oracle.com/javase/tutorial/java/data/characters.html + # + # @api public + # + # @since 1.2.0 + # + module Java + # + # Encodes a byte as a Java escaped character. + # + # @param [Integer] byte + # The byte value to encode. + # + # @return [String] + # The escaped Java character. + # + # @example + # Encoding::Java.encode_byte(0x41) + # # => "\\u0041" + # Encoding::Java.encode_byte(0x221e) + # # => "\\u221E" + # + # @see https://docs.oracle.com/javase/tutorial/java/data/characters.html + # + def self.encode_byte(byte) + if byte >= 0x00 && byte <= 0xffff + "\\u%.4X" % byte + else + raise(RangeError,"#{byte.inspect} out of char range") + end + end + + # Special Java bytes and their escaped characters. + # + # @see https://docs.oracle.com/javase/tutorial/java/data/characters.html + ESCAPE_BYTES = { + 0x00 => '\0', # prefer \0 over \u0000 + 0x08 => '\b', + 0x09 => '\t', + 0x0a => '\n', + 0x0b => '\v', + 0x0c => '\f', + 0x0d => '\r', + 0x22 => '\"', + 0x27 => "\\'", + 0x5c => '\\\\' + } + + # + # Escapes a byte as a Java character. + # + # @param [Integer] byte + # The byte value to escape. + # + # @return [String] + # The escaped Java character. + # + # @raise [RangeError] + # The integer value is negative. + # + # @example + # Encoding::Java.escape_byte(0x41) + # # => "A" + # Encoding::Java.escape_byte(0x22) + # # => "\\\"" + # Encoding::Java.escape_byte(0x7f) + # # => "\\u007F" + # + # @example Escaping unicode characters: + # Encoding::Java.escape_byte(0xffff) + # # => "\\uFFFF" + # + # @see https://docs.oracle.com/javase/tutorial/java/data/characters.html + # + def self.escape_byte(byte) + if byte >= 0x00 && byte <= 0xff + ESCAPE_BYTES.fetch(byte) do + if byte >= 0x20 && byte <= 0x7e + byte.chr + else + encode_byte(byte) + end + end + else + encode_byte(byte) + end + end + + # + # Encodes each character of the given data as Java escaped characters. + # + # @param [String] data + # The given data to encode. + # + # @return [String] + # The Java encoded String. + # + # @example + # Encoding::Java.encode("hello") + # # => "\\u0068\\u0065\\u006C\\u006C\\u006F" + # + # @see https://docs.oracle.com/javase/tutorial/java/data/characters.html + # + def self.encode(data) + encoded = String.new + + if data.valid_encoding? + data.each_codepoint do |codepoint| + encoded << encode_byte(codepoint) + end + else + data.each_byte do |byte| + encoded << encode_byte(byte) + end + end + + return encoded + end + + # + # Decodes the Java encoded data. + # + # @param [String] data + # The given Java data to decode. + # + # @return [String] + # The decoded data. + # + # @see unescape + # @see https://docs.oracle.com/javase/tutorial/java/data/characters.html + # + def self.decode(data) + unescape(data) + end + + # + # Escapes the Java encoded data. + # + # @param [String] data + # The data to Java escape. + # + # @return [String] + # The Java escaped String. + # + # @see https://docs.oracle.com/javase/tutorial/java/data/characters.html + # + def self.escape(data) + escaped = String.new + + if data.valid_encoding? + data.each_codepoint do |codepoint| + escaped << escape_byte(codepoint) + end + else + data.each_byte do |byte| + escaped << escape_byte(byte) + end + end + + return escaped + end + + # Java characters that must be back-slashed. + # + # @see https://docs.oracle.com/javase/tutorial/java/data/characters.html + BACKSLASHED_CHARS = { + '\\b' => "\b", + '\\t' => "\t", + '\\n' => "\n", + '\\f' => "\f", + '\\r' => "\r", + "\\\"" => '"', + "\\'" => "'", + "\\\\" => "\\" + } + + # + # Unescapes the given Java escaped data. + # + # @param [String] data + # The given Java escaped data. + # + # @return [String] + # The unescaped Java String. + # + # @raise [ArgumentError] + # An invalid Java backslach escape sequence was encountered while + # parsing the String. + # + # @example + # Encoding::Java.unescape("\\u0068\\u0065\\u006C\\u006C\\u006F\\u0020\\u0077\\u006F\\u0072\\u006C\\u0064") + # # => "hello world" + # + # @see https://docs.oracle.com/javase/tutorial/java/data/characters.html + # + def self.unescape(data) + unescaped = String.new(encoding: Encoding::UTF_8) + scanner = StringScanner.new(data) + + until scanner.eos? + unescaped << if (unicode_escape = scanner.scan(/\\u[0-9a-fA-F]{4}/)) # \uXXXX + unicode_escape[2..].to_i(16).chr(Encoding::UTF_8) + elsif (octal_escape = scanner.scan(/\\[0-7]{1,3}/)) # \N, \NN, or \NNN + octal_escape[1..].to_i(8).chr + elsif (backslash_escape = scanner.scan(/\\./)) # \[A-Za-z] + BACKSLASHED_CHARS.fetch(backslash_escape) do + raise(ArgumentError,"invalid Java backslash escape sequence: #{backslash_escape.inspect}") + end + else + scanner.getch + end + end + + return unescaped + end + + # + # Escapes and quotes the given data as a Java String. + # + # @param [String] data + # The given data to escape and quote. + # + # @return [String] + # The quoted Java String. + # + # @example + # Encoding::Java.quote("hello\nworld\n") + # # => "\"hello\\nworld\\n\"" + # + # @see https://docs.oracle.com/javase/tutorial/java/data/characters.html + # + def self.quote(data) + "\"#{escape(data)}\"" + end + + # + # Unquotes and unescapes the given Java String. + # + # @param [String] data + # The given Java String. + # + # @return [String] + # The un-quoted String if the String begins and ends with quotes, or + # the same String if it is not quoted. + # + # @example + # Encoding::Java.unquote("\"hello\\nworld\"") + # # => "hello\nworld" + # + # @see https://docs.oracle.com/javase/tutorial/java/data/characters.html + # + def self.unquote(data) + if (data.start_with?('"') && data.end_with?('"')) + unescape(data[1..-2]) + else + data + end + end + end + end + end +end + +require 'ronin/support/encoding/java/core_ext' diff --git a/lib/ronin/support/encoding/java/core_ext.rb b/lib/ronin/support/encoding/java/core_ext.rb new file mode 100644 index 000000000..9bade50c7 --- /dev/null +++ b/lib/ronin/support/encoding/java/core_ext.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/encoding/java/core_ext/integer' +require 'ronin/support/encoding/java/core_ext/string' diff --git a/lib/ronin/support/encoding/java/core_ext/integer.rb b/lib/ronin/support/encoding/java/core_ext/integer.rb new file mode 100644 index 000000000..632c49384 --- /dev/null +++ b/lib/ronin/support/encoding/java/core_ext/integer.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/encoding/java' + +class Integer + + # + # Escapes the Integer as a Java character. + # + # @return [String] + # The escaped Java character. + # + # @example + # 0x41.java_escape + # # => "A" + # 0x22.java_escape + # # => "\\\"" + # 0x7f.java_escape + # # => "\\u007F" + # + # @see Ronin::Support::Encoding::Java.escape_byte + # + # @since 1.2.0 + # + # @api public + # + def java_escape + Ronin::Support::Encoding::Java.escape_byte(self) + end + + # + # Encodes the Integer as a Java character. + # + # @return [String] + # The encoded Java character. + # + # @example + # 0x41.java_encode + # # => "\\u0041" + # + # @see Ronin::Support::Encoding::Java.encode_byte + # + # @since 1.2.0 + # + # @api public + # + def java_encode + Ronin::Support::Encoding::Java.encode_byte(self) + end + +end diff --git a/lib/ronin/support/encoding/java/core_ext/string.rb b/lib/ronin/support/encoding/java/core_ext/string.rb new file mode 100644 index 000000000..d6345b4c2 --- /dev/null +++ b/lib/ronin/support/encoding/java/core_ext/string.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# Ronin Support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ronin Support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Ronin Support. If not, see . +# + +require 'ronin/support/encoding/java' + +class String + + # + # Escapes a String for Java. + # + # @return [String] + # The Java escaped String. + # + # @example + # "hello\nworld\n".java_escape + # # => "hello\\nworld\\n" + # + # @see Ronin::Support::Encoding::Java.escape + # + # @since 1.2.0 + # + # @api public + # + def java_escape + Ronin::Support::Encoding::Java.escape(self) + end + + # + # Unescapes a Java escaped String. + # + # @return [String] + # The unescaped Java String. + # + # @example + # "\\u0068\\u0065\\u006C\\u006C\\u006F world".java_unescape + # # => "hello world" + # + # @see Ronin::Support::Encoding::Java.unescape + # + # @since 1.2.0 + # + # @api public + # + def java_unescape + Ronin::Support::Encoding::Java.unescape(self) + end + + # + # Java escapes every character of the String. + # + # @return [String] + # The Java escaped String. + # + # @example + # "hello".java_encode + # # => "\\u0068\\u0065\\u006C\\u006C\\u006F" + # + # @see Ronin::Support::Encoding::Java.encode + # + # @api public + # + # @since 1.2.0 + # + def java_encode + Ronin::Support::Encoding::Java.encode(self) + end + + alias java_decode java_unescape + + # + # Converts the String into a Java String. + # + # @return [String] + # + # @example + # "hello\nworld\n".java_string + # # => "\"hello\\nworld\\n\"" + # + # @see Ronin::Support::Encoding::Java.quote + # + # @since 1.2.0 + # + # @api public + # + def java_string + Ronin::Support::Encoding::Java.quote(self) + end + + # + # Removes the quotes an unescapes a Java String. + # + # @return [String] + # The un-quoted String if the String begins and ends with quotes, or the + # same String if it is not quoted. + # + # @example + # "\"hello\\nworld\"".java_unquote + # # => "hello\nworld" + # + # @see Ronin::Support::Encoding::Java.unquote + # + # @since 1.2.0 + # + # @api public + # + def java_unquote + Ronin::Support::Encoding::Java.unquote(self) + end + +end diff --git a/lib/ronin/support/encoding/node_js.rb b/lib/ronin/support/encoding/node_js.rb new file mode 100644 index 000000000..a33074cc6 --- /dev/null +++ b/lib/ronin/support/encoding/node_js.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/encoding/js' + +module Ronin + module Support + class Encoding < ::Encoding + # + # Contains methods for encoding/decoding escaping/unescaping Node.js + # data. + # + # ## Core-Ext Methods + # + # * {Integer#node_js_escape} + # * {Integer#node_js_encode} + # * {String#node_js_escape} + # * {String#node_js_unescape} + # * {String#node_js_encode} + # * {String#node_js_decode} + # * {String#node_js_string} + # * {String#node_js_unquote} + # + # @api public + # + # @since 1.2.0 + # + NodeJS = JS + end + end +end + +require 'ronin/support/encoding/node_js/core_ext' diff --git a/lib/ronin/support/encoding/node_js/core_ext.rb b/lib/ronin/support/encoding/node_js/core_ext.rb new file mode 100644 index 000000000..fe9fb6dba --- /dev/null +++ b/lib/ronin/support/encoding/node_js/core_ext.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/encoding/node_js/core_ext/integer' +require 'ronin/support/encoding/node_js/core_ext/string' diff --git a/lib/ronin/support/encoding/node_js/core_ext/integer.rb b/lib/ronin/support/encoding/node_js/core_ext/integer.rb new file mode 100644 index 000000000..cf1be4f5f --- /dev/null +++ b/lib/ronin/support/encoding/node_js/core_ext/integer.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/encoding/node_js' + +class Integer + + # + # Escapes the Integer as a Node.js character. + # + # @return [String] + # The escaped Node.js character. + # + # @example + # 0x41.node_js_escape + # # => "A" + # 0x22.node_js_escape + # # => "\\\"" + # 0x7f.node_js_escape + # # => "\\x7F" + # + # @see Ronin::Support::Encoding::NodeJS.escape_byte + # + # @since 1.2.0 + # + # @api public + # + def node_js_escape + Ronin::Support::Encoding::NodeJS.escape_byte(self) + end + + # + # Encodes the Integer as a Node.js character. + # + # @return [String] + # The encoded Node.js character. + # + # @example + # 0x41.node_js_encode + # # => "\\x41" + # + # @see Ronin::Support::Encoding::NodeJS.encode_byte + # + # @since 1.2.0 + # + # @api public + # + def node_js_encode + Ronin::Support::Encoding::NodeJS.encode_byte(self) + end + +end diff --git a/lib/ronin/support/encoding/node_js/core_ext/string.rb b/lib/ronin/support/encoding/node_js/core_ext/string.rb new file mode 100644 index 000000000..073319302 --- /dev/null +++ b/lib/ronin/support/encoding/node_js/core_ext/string.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# Ronin Support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ronin Support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Ronin Support. If not, see . +# + +require 'ronin/support/encoding/node_js' + +class String + + # + # Escapes a String for Node.js. + # + # @return [String] + # The Node.js escaped String. + # + # @example + # "hello\nworld\n".node_js_escape + # # => "hello\\nworld\\n" + # + # @see Ronin::Support::Encoding::NodeJS.escape + # + # @since 1.2.0 + # + # @api public + # + def node_js_escape + Ronin::Support::Encoding::NodeJS.escape(self) + end + + # + # Unescapes a Node.js escaped String. + # + # @return [String] + # The unescaped Node.js String. + # + # @example + # "\\u0068\\u0065\\u006C\\u006C\\u006F world".node_js_unescape + # # => "hello world" + # + # @see Ronin::Support::Encoding::NodeJS.unescape + # + # @since 1.2.0 + # + # @api public + # + def node_js_unescape + Ronin::Support::Encoding::NodeJS.unescape(self) + end + + # + # Node.js escapes every character of the String. + # + # @return [String] + # The Node.js escaped String. + # + # @example + # "hello".node_js_encode + # # => "\\u0068\\u0065\\u006C\\u006C\\u006F" + # + # @see Ronin::Support::Encoding::NodeJS.encode + # + # @api public + # + # @since 1.2.0 + # + def node_js_encode + Ronin::Support::Encoding::NodeJS.encode(self) + end + + alias node_js_decode node_js_unescape + + # + # Converts the String into a Node.js string. + # + # @return [String] + # + # @example + # "hello\nworld\n".node_js_string + # # => "\"hello\\nworld\\n\"" + # + # @see Ronin::Support::Encoding::NodeJS.quote + # + # @since 1.2.0 + # + # @api public + # + def node_js_string + Ronin::Support::Encoding::NodeJS.quote(self) + end + + # + # Removes the quotes an unescapes a Node.js string. + # + # @return [String] + # The un-quoted String if the String begins and ends with quotes, or the + # same String if it is not quoted. + # + # @example + # "\"hello\\nworld\"".node_js_unquote + # # => "hello\nworld" + # + # @see Ronin::Support::Encoding::NodeJS.unquote + # + # @since 1.2.0 + # + # @api public + # + def node_js_unquote + Ronin::Support::Encoding::NodeJS.unquote(self) + end + +end diff --git a/lib/ronin/support/encoding/perl.rb b/lib/ronin/support/encoding/perl.rb new file mode 100644 index 000000000..e9906ba62 --- /dev/null +++ b/lib/ronin/support/encoding/perl.rb @@ -0,0 +1,385 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'strscan' + +module Ronin + module Support + class Encoding < ::Encoding + # + # Contains methods for encoding/decoding escaping/unescaping Perl strings. + # + # ## Core-Ext Methods + # + # * {String#perl_escape} + # * {String#perl_unescape} + # * {String#perl_encode} + # * {String#perl_decode} + # * {String#perl_string} + # * {String#perl_unquote} + # + # @api public + # + # @since 1.2.0 + # + module Perl + # + # Encodes a byte as a Perl escaped character. + # + # @param [Integer] byte + # The byte value to encode. + # + # @return [String] + # The escaped Perl character. + # + # @example + # Encoding::Perl.encode_byte(0x41) + # # => "\\x41" + # Encoding::Perl.encode_byte(0x100) + # # => "\\x{100}" + # Encoding::Perl.encode_byte(0x10000) + # # => "\\x{10000}" + # + # @see https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + # + def self.encode_byte(byte) + if byte >= 0x00 && byte <= 0xff + "\\x%2X" % byte + elsif byte >= 0x100 && byte <= 0x10ffff + "\\x{%.X}" % byte + else + raise(RangeError,"#{byte.inspect} out of char range") + end + end + + # Special Perl bytes and their escaped Strings. + # + # @see https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + ESCAPE_BYTES = { + 0x07 => '\a', + 0x08 => '\b', + 0x09 => '\t', + 0x0a => '\n', + 0x0c => '\f', + 0x0d => '\r', + 0x1b => '\e', + 0x22 => '\"', + 0x24 => '\$', + 0x5c => '\\\\' + } + + # + # Escapes a byte as a Perl character. + # + # @param [Integer] byte + # The byte value to escape. + # + # @return [String] + # The escaped Perl character. + # + # @raise [RangeError] + # The byte value isn't a valid ASCII or UTF byte. + # + # @see https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + # + def self.escape_byte(byte) + if byte >= 0x00 && byte <= 0xff + ESCAPE_BYTES.fetch(byte) do + if byte >= 0x20 && byte <= 0x7e + byte.chr + else + encode_byte(byte) + end + end + else + encode_byte(byte) + end + end + + # + # Encodes each byte of the given data as a Perl escaped character. + # + # @param [String] data + # The given data to encode. + # + # @return [String] + # The encoded Perl String. + # + # @example + # Encoding::Perl.encode("hello") + # # => "\\x68\\x65\\x6c\\x6c\\x6f" + # + # @see https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + # + def self.encode(data) + encoded = String.new + + if data.valid_encoding? + data.each_codepoint do |codepoint| + encoded << encode_byte(codepoint) + end + else + data.each_byte do |byte| + encoded << encode_byte(byte) + end + end + + return encoded + end + + # + # Decodes the Perl encoded data. + # + # @param [String] data + # The given Perl data to decode. + # + # @return [String] + # The decoded data. + # + # @see unescape + # @see https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + # + def self.decode(data) + unescape(data) + end + + # + # Escapes the Perl string. + # + # @param [String] data + # The data to escape. + # + # @return [String] + # The Perl escaped String. + # + # @see https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + # + def self.escape(data) + escaped = String.new + + if data.valid_encoding? + data.each_codepoint do |codepoint| + escaped << escape_byte(codepoint) + end + else + data.each_byte do |byte| + escaped << escape_byte(byte) + end + end + + return escaped + end + + # Common escaped characters. + # + # @see https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + BACKSLASH_CHARS = { + '\a' => "\a", + '\b' => "\b", + '\t' => "\t", + '\n' => "\n", + '\f' => "\f", + '\r' => "\r", + '\e' => "\e", + '\"' => '"', + '\$' => '$', + '\\\\' => '\\' + } + + # `\c` control characters. + # + # @see https://perldoc.perl.org/perlop#%5B5%5D + CONTROL_CHARS = { + '\c@' => "\x00", + '\cA' => "\x01", + '\cB' => "\x02", + '\cC' => "\x03", + '\cD' => "\x04", + '\cE' => "\x05", + '\cF' => "\x06", + '\cG' => "\x07", + '\cH' => "\x08", + '\cI' => "\x09", + '\cJ' => "\x0a", + '\cK' => "\x0b", + '\cL' => "\x0c", + '\cM' => "\x0d", + '\cN' => "\x0e", + '\cO' => "\x0f", + '\cP' => "\x10", + '\cQ' => "\x11", + '\cR' => "\x12", + '\cS' => "\x13", + '\cT' => "\x14", + '\cU' => "\x15", + '\cV' => "\x16", + '\cW' => "\x17", + '\cX' => "\x18", + '\cY' => "\x19", + '\cZ' => "\x1a", + '\ca' => "\x01", + '\cb' => "\x02", + '\cc' => "\x03", + '\cd' => "\x04", + '\ce' => "\x05", + '\cf' => "\x06", + '\cg' => "\x07", + '\ch' => "\x08", + '\ci' => "\x09", + '\cj' => "\x0a", + '\ck' => "\x0b", + '\cl' => "\x0c", + '\cm' => "\x0d", + '\cn' => "\x0e", + '\co' => "\x0f", + '\cp' => "\x10", + '\cq' => "\x11", + '\cr' => "\x12", + '\cs' => "\x13", + '\ct' => "\x14", + '\cu' => "\x15", + '\cv' => "\x16", + '\cw' => "\x17", + '\cx' => "\x18", + '\cy' => "\x19", + '\cz' => "\x1a", + '\c[' => "\x1b", + '\c]' => "\x1d", + '\c^' => "\x1e", + '\c_' => "\x1f", + '\c?' => "\x7f" + } + + # + # Unescapes the escaped [Perl String][1]. + # + # @param [String] data + # The given Perl escaped data. + # + # @return [String] + # The unescaped version of the hex escaped String. + # + # @raise [NotImplementedError] + # Decoding [Perl Unicode Named Characters][2] is currently not + # supported (ex: `\N{GREEK CAPITAL LETTER SIGMA}`). + # + # [2]: https://www.perl.com/pub/2012/04/perlunicook-unicode-named-characters.html/ + # + # @example + # Encoding::Perl.unescape("\\x68\\x65\\x6c\\x6c\\x6f") + # # => "hello" + # + # @see https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + # + def self.unescape(data) + unescaped = String.new(encoding: Encoding::UTF_8) + scanner = StringScanner.new(data) + + until scanner.eos? + unescaped << if (unicode_escape = scanner.scan(/\\x\{\s*[0-9a-fA-F]{1,8}\s*\}/)) # \x{X...} + unicode_escape[3..-2].strip.to_i(16).chr(Encoding::UTF_8) + elsif (unicode_escape = scanner.scan(/\\N\{\s*U\+[0-9a-fA-F]{1,8}\s*\}/)) # \N{U+X...} + unicode_escape[3..-2].strip[2..].to_i(16).chr(Encoding::UTF_8) + elsif (unicode_escape = scanner.scan(/\\N\{[^\}]+\}/)) # \N{NAMED UNICODE CHAR} + raise(NotImplementedError,"decoding Perl Unicode Named Characters (#{unicode_escape.inspect}) is currently not supported: #{data.inspect}") + elsif (hex_escape = scanner.scan(/\\x[0-9a-fA-F]{0,2}/)) # \xXX, \xX, or \x + hex = hex_escape[2..] + + unless hex.empty? then hex.to_i(16).chr + else '' # no-op + end + elsif (octal_escape = scanner.scan(/\\o\{\s*[0-7]{1,3}\s*\}/)) # \o{NNN}, \o{NN}, \o{N} + octal_escape[3..-2].strip.to_i(8).chr + elsif (octal_escape = scanner.scan(/\\[0-7]{1,3}/)) # \NNN, \NN, \N + octal_escape[1..].to_i(8).chr + elsif (control_char = scanner.scan(/\\c[@a-zA-Z\[\]\^_\?]/)) # \c control char + CONTROL_CHARS.fetch(control_char) + elsif (backslash_escape = scanner.scan(/\\./)) # \C + BACKSLASH_CHARS.fetch(backslash_escape) do + backslash_escape[1] + end + else + scanner.getch + end + end + + return unescaped + end + + # + # Escapes and quotes the given data as a Perl string. + # + # @param [String] data + # The given data to escape and quote. + # + # @return [String] + # The quoted Perl string. + # + # @example + # Encoding::Perl.quote("hello\nworld\n") + # # => "\"hello\\nworld\\n\"" + # + # @see https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + # + def self.quote(data) + "\"#{escape(data)}\"" + end + + # + # Removes the quotes an unescapes a [quoted Perl string][1]. + # + # [1]: https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + # + # @param [String] data + # The given Perl string. + # + # @return [String] + # The un-quoted String if the String begins and ends with quotes, or + # the same String if it is not quoted. + # + # @example + # Encoding::Perl.unquote("\"hello\\nworld\"") + # # => "hello\nworld" + # Encoding::Perl.unquote("qq{hello\\'world}") + # # => "hello'world" + # Encoding::Perl.unquote("'hello\\'world'") + # # => "hello'world" + # Encoding::Perl.unquote("q{hello\\'world}") + # # => "hello\\'world" + # + # @see https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + # + def self.unquote(data) + if (data.start_with?('"') && data.end_with?('"')) + unescape(data[1..-2]) + elsif (data.start_with?('qq{') && data.end_with?('}')) + unescape(data[3..-2]) + elsif (data.start_with?("'") && data.end_with?("'")) + data[1..-2].gsub(/\\(['\\])/,'\1') + elsif (data.start_with?('q{') && data.end_with?('}')) + data[2..-2] + else + data + end + end + end + end + end +end + +require 'ronin/support/encoding/perl/core_ext' diff --git a/lib/ronin/support/encoding/perl/core_ext.rb b/lib/ronin/support/encoding/perl/core_ext.rb new file mode 100644 index 000000000..c3f0ad539 --- /dev/null +++ b/lib/ronin/support/encoding/perl/core_ext.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/encoding/perl/core_ext/integer' +require 'ronin/support/encoding/perl/core_ext/string' diff --git a/lib/ronin/support/encoding/perl/core_ext/integer.rb b/lib/ronin/support/encoding/perl/core_ext/integer.rb new file mode 100644 index 000000000..bc15ab7e2 --- /dev/null +++ b/lib/ronin/support/encoding/perl/core_ext/integer.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/encoding/perl' + +class Integer + + # + # Escapes the Integer as a Perl character. + # + # @return [String] + # The escaped Perl character. + # + # @example + # 0x41.perl_escape + # # => "A" + # 0x22.perl_escape + # # => "\\\"" + # 0x7f.perl_escape + # # => "\\x7F" + # + # @see Ronin::Support::Encoding::Perl.escape_byte + # @see https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + # + # @since 1.2.0 + # + # @api public + # + def perl_escape + Ronin::Support::Encoding::Perl.escape_byte(self) + end + + # + # Encodes the Integer as a Perl character. + # + # @return [String] + # The encoded Perl character. + # + # @example + # 0x41.perl_encode + # # => "\\x41" + # + # @see Ronin::Support::Encoding::Perl.encode_byte + # @see https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + # + # @since 1.2.0 + # + # @api public + # + def perl_encode + Ronin::Support::Encoding::Perl.encode_byte(self) + end + +end diff --git a/lib/ronin/support/encoding/perl/core_ext/string.rb b/lib/ronin/support/encoding/perl/core_ext/string.rb new file mode 100644 index 000000000..8aa334ad8 --- /dev/null +++ b/lib/ronin/support/encoding/perl/core_ext/string.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# Ronin Support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ronin Support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Ronin Support. If not, see . +# + +require 'ronin/support/encoding/perl' + +class String + + # + # Escapes a String for Perl. + # + # @return [String] + # The Perl escaped String. + # + # @example + # "hello\nworld\n".perl_escape + # # => "hello\\nworld\\n" + # + # @see Ronin::Support::Encoding::Perl.escape + # @see https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + # + # @since 1.2.0 + # + # @api public + # + def perl_escape + Ronin::Support::Encoding::Perl.escape(self) + end + + # + # Unescapes a Perl escaped String. + # + # @return [String] + # The unescaped Perl String. + # + # @raise [NotImplementedError] + # Decoding Perl Unicode Named Characters is currently not supported + # (ex: `\N{GREEK CAPITAL LETTER SIGMA}`). + # + # @example + # "\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64".perl_unescape + # # => "hello world" + # + # @see Ronin::Support::Encoding::Perl.unescape + # @see https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + # + # @since 1.2.0 + # + # @api public + # + def perl_unescape + Ronin::Support::Encoding::Perl.unescape(self) + end + + # + # Perl escapes every character in the String. + # + # @return [String] + # The Perl escaped String. + # + # @example + # "hello".perl_encode + # # => "\\x68\\x65\\x6c\\x6c\\x6f" + # + # @see Ronin::Support::Encoding::Perl.encode + # @see https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + # + # @api public + # + # @since 1.2.0 + # + def perl_encode + Ronin::Support::Encoding::Perl.encode(self) + end + + alias perl_decode perl_unescape + + # + # Converts the String into a Perl string. + # + # @return [String] + # + # @example + # "hello\nworld\n".perl_string + # # => "\"hello\\nworld\\n\"" + # + # @see Ronin::Support::Encoding::Perl.quote + # @see https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + # + # @since 1.2.0 + # + # @api public + # + def perl_string + Ronin::Support::Encoding::Perl.quote(self) + end + + # + # Removes the quotes an unescapes a Perl string. + # + # @return [String] + # The un-quoted String if the String begins and ends with quotes, or the + # same String if it is not quoted. + # + # @example + # "\"hello\\nworld\"".perl_unquote + # # => "hello\nworld" + # "qq{hello\\'world}".perl_unquote + # # => "hello'world" + # "'hello\\'world'".perl_unquote + # # => "hello'world" + # "q{hello\\'world}".perl_unquote + # # => "hello\\'world" + # + # @see Ronin::Support::Encoding::Perl.unquote + # @see https://perldoc.perl.org/perlop#Quote-and-Quote-like-Operators + # + # @since 1.2.0 + # + # @api public + # + def perl_unquote + Ronin::Support::Encoding::Perl.unquote(self) + end + +end diff --git a/lib/ronin/support/encoding/php.rb b/lib/ronin/support/encoding/php.rb new file mode 100644 index 000000000..a2d0ed238 --- /dev/null +++ b/lib/ronin/support/encoding/php.rb @@ -0,0 +1,291 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'strscan' + +module Ronin + module Support + class Encoding < ::Encoding + # + # Contains methods for encoding/decoding escaping/unescaping PHP strings. + # + # ## Core-Ext Methods + # + # * {String#php_escape} + # * {String#php_unescape} + # * {String#php_encode} + # * {String#php_decode} + # * {String#php_string} + # * {String#php_unquote} + # + # @api public + # + # @since 1.2.0 + # + # @see https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.double + # + module PHP + # Special PHP bytes and their escaped Strings. + ESCAPE_BYTES = { + 0x00 => '\0', + 0x09 => '\t', + 0x0a => '\n', + 0x0c => '\f', + 0x0d => '\r', + 0x1b => '\e', + 0x22 => '\"', + 0x24 => '\$', + 0x5c => '\\\\' + } + + # + # Encodes a byte as a PHP escaped character. + # + # @param [Integer] byte + # The byte value to encode. + # + # @return [String] + # The escaped PHP character. + # + # @example + # Encoding::PHP.encode_byte(0x41) + # # => "\\x41" + # Encoding::PHP.encode_byte(0x100) + # # => "\\u{100}" + # Encoding::PHP.encode_byte(0x10000) + # # => "\\u{10000}" + # + # @see https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.double + # + def self.encode_byte(byte) + if byte >= 0x00 && byte <= 0xff + "\\x%.2x" % byte + elsif byte >= 0x100 && byte <= 0x10ffff + "\\u{%.x}" % byte + else + raise(RangeError,"#{byte.inspect} out of char range") + end + end + + # + # Escapes a byte as a PHP character. + # + # @param [Integer] byte + # The byte value to escape. + # + # @return [String] + # The escaped PHP character. + # + # @raise [RangeError] + # The byte value isn't a valid ASCII or UTF byte. + # + # @see https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.double + # + def self.escape_byte(byte) + if byte >= 0x00 && byte <= 0xff + ESCAPE_BYTES.fetch(byte) do + if byte >= 0x20 && byte <= 0x7e + byte.chr + else + encode_byte(byte) + end + end + else + encode_byte(byte) + end + end + + # + # Encodes each byte of the given data as a PHP escaped character. + # + # @param [String] data + # The given data to encode. + # + # @return [String] + # The encoded PHP String. + # + # @example + # Encoding::PHP.encode("hello") + # # => "\\x68\\x65\\x6c\\x6c\\x6f" + # + # @see https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.double + # + def self.encode(data) + encoded = String.new + + if data.valid_encoding? + data.each_codepoint do |codepoint| + encoded << encode_byte(codepoint) + end + else + data.each_byte do |byte| + encoded << encode_byte(byte) + end + end + + return encoded + end + + # + # Decodes the PHP encoded data. + # + # @param [String] data + # The given PHP data to decode. + # + # @return [String] + # The decoded data. + # + # @see unescape + # @see https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.double). + # + def self.decode(data) + unescape(data) + end + + # + # Escapes the PHP string. + # + # @param [String] data + # The data to escape. + # + # @return [String] + # The PHP escaped String. + # + # @see https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.double + # + def self.escape(data) + escaped = String.new + + if data.valid_encoding? + data.each_codepoint do |codepoint| + escaped << escape_byte(codepoint) + end + else + data.each_byte do |byte| + escaped << escape_byte(byte) + end + end + + return escaped + end + + # PHP characters that must be backslash escaped. + # + # @see https://www.php.net/manual/en/language.types.string.php + BACKSLASH_CHARS = { + '\0' => "\0", + '\t' => "\t", + '\n' => "\n", + '\v' => "\v", + '\f' => "\f", + '\r' => "\r", + '\"' => '"', + '\$' => "$", + '\\\\' => "\\" + } + + # + # Unescapes the PHP escaped String. + # + # @param [String] data + # The given PHP escaped data. + # + # @return [String] + # The unescaped version of the hex escaped String. + # + # @example + # Encoding::PHP.unescape("\\x68\\x65\\x6c\\x6c\\x6f") + # # => "hello" + # + # @see https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.double + # + def self.unescape(data) + unescaped = String.new(encoding: Encoding::UTF_8) + scanner = StringScanner.new(data) + + until scanner.eos? + # see https://www.php.net/manual/en/language.types.string.php + unescaped << if (hex_escape = scanner.scan(/\\x[0-9a-fA-F]{1,2}/)) + hex_escape[2..].to_i(16).chr + elsif (unicode_escape = scanner.scan(/\\u\{[0-9a-fA-F]+\}/)) + unicode_escape[3..-2].to_i(16).chr(Encoding::UTF_8) + elsif (octal_escape = scanner.scan(/\\[0-7]{1,3}/)) + octal_escape[1..].to_i(8).chr + + elsif (backslash_char = scanner.scan(/\\./)) + BACKSLASH_CHARS.fetch(backslash_char,backslash_char) + else + scanner.getch + end + end + + return unescaped + end + + # + # Escapes and quotes the given data as a PHP string. + # + # @param [String] data + # The given data to escape and quote. + # + # @return [String] + # The quoted PHP string. + # + # @example + # Encoding::PHP.quote("hello\nworld\n") + # # => "\"hello\\nworld\\n\"" + # + # @see https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.double + # + def self.quote(data) + "\"#{escape(data)}\"" + end + + # + # Removes the quotes and unescapes the PHP quoted string. + # + # @param [String] data + # The given PHP string. + # + # @return [String] + # The un-quoted String if the String begins and ends with quotes, or + # the same String if it is not quoted. + # + # @example + # Encoding::PHP.unquote("\"hello\\nworld\"") + # # => "hello\nworld" + # Encoding::PHP.unquote("'hello\\'world'") + # # => "hello'world" + # + # @see https://www.php.net/manual/en/language.types.string.php + # + def self.unquote(data) + if (data.start_with?('"') && data.end_with?('"')) + unescape(data[1..-2]) + elsif (data.start_with?("'") && data.end_with?("'")) + data[1..-2].gsub(/\\(['\\])/,'\1') + else + data + end + end + end + end + end +end + +require 'ronin/support/encoding/php/core_ext' diff --git a/lib/ronin/support/encoding/php/core_ext.rb b/lib/ronin/support/encoding/php/core_ext.rb new file mode 100644 index 000000000..cf3d8a79c --- /dev/null +++ b/lib/ronin/support/encoding/php/core_ext.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/encoding/php/core_ext/integer' +require 'ronin/support/encoding/php/core_ext/string' diff --git a/lib/ronin/support/encoding/php/core_ext/integer.rb b/lib/ronin/support/encoding/php/core_ext/integer.rb new file mode 100644 index 000000000..15d0f48c8 --- /dev/null +++ b/lib/ronin/support/encoding/php/core_ext/integer.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/encoding/php' + +class Integer + + # + # Escapes the Integer as a PHP character. + # + # @return [String] + # The escaped PHP character. + # + # @example + # 0x41.php_escape + # # => "A" + # 0x22.php_escape + # # => "\\\"" + # 0x7f.php_escape + # # => "\\x7F" + # + # @see Ronin::Support::Encoding::PHP.escape_byte + # @see https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.double + # + # @since 1.2.0 + # + # @api public + # + def php_escape + Ronin::Support::Encoding::PHP.escape_byte(self) + end + + # + # Encodes the Integer as a PHP character. + # + # @return [String] + # The encoded PHP character. + # + # @example + # 0x41.php_encode + # # => "\\x41" + # + # @see Ronin::Support::Encoding::PHP.encode_byte + # @see https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.double + # + # @since 1.2.0 + # + # @api public + # + def php_encode + Ronin::Support::Encoding::PHP.encode_byte(self) + end + +end diff --git a/lib/ronin/support/encoding/php/core_ext/string.rb b/lib/ronin/support/encoding/php/core_ext/string.rb new file mode 100644 index 000000000..d07db2b32 --- /dev/null +++ b/lib/ronin/support/encoding/php/core_ext/string.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# Ronin Support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ronin Support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Ronin Support. If not, see . +# + +require 'ronin/support/encoding/php' + +class String + + # + # Escapes a String for PHP. + # + # @return [String] + # The PHP escaped String. + # + # @example + # "hello\nworld\n".php_escape + # # => "hello\\nworld\\n" + # + # @see Ronin::Support::Encoding::PHP.escape + # @see https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.double + # + # @since 1.2.0 + # + # @api public + # + def php_escape + Ronin::Support::Encoding::PHP.escape(self) + end + + # + # Unescapes a PHP escaped String. + # + # @return [String] + # The unescaped PHP String. + # + # @example + # "\\x68\\x65\\x6c\\x6c\\x6f\\x20\\x77\\x6f\\x72\\x6c\\x64".php_unescape + # # => "hello world" + # + # @see Ronin::Support::Encoding::PHP.unescape + # @see https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.double). + # + # @since 1.2.0 + # + # @api public + # + def php_unescape + Ronin::Support::Encoding::PHP.unescape(self) + end + + # + # PHP escapes every character of the String. + # + # @return [String] + # The PHP escaped String. + # + # @example + # "hello".php_encode + # # => "\\u{0068}\\u{0065}\\u{006C}\\u{006C}\\u{006F}" + # + # @see Ronin::Support::Encoding::PHP.encode + # @see https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.double + # + # @api public + # + # @since 1.2.0 + # + def php_encode + Ronin::Support::Encoding::PHP.encode(self) + end + + alias php_decode php_unescape + + # + # Converts the String into a PHP string. + # + # @return [String] + # + # @example + # "hello\nworld\n".php_string + # # => "\"hello\\nworld\\n\"" + # + # @see Ronin::Support::Encoding::PHP.quote + # @see https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.double + # + # @since 1.2.0 + # + # @api public + # + def php_string + Ronin::Support::Encoding::PHP.quote(self) + end + + # + # Removes the quotes an unescapes a PHP string. + # + # @return [String] + # The un-quoted String if the String begins and ends with quotes, or the + # same String if it is not quoted. + # + # @example + # "\"hello\\nworld\"".php_unquote + # # => "hello\nworld" + # + # @see Ronin::Support::Encoding::PHP.unquote + # @see https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.double + # + # @since 1.2.0 + # + # @api public + # + def php_unquote + Ronin::Support::Encoding::PHP.unquote(self) + end + +end diff --git a/lib/ronin/support/encoding/python.rb b/lib/ronin/support/encoding/python.rb new file mode 100644 index 000000000..77e6b1581 --- /dev/null +++ b/lib/ronin/support/encoding/python.rb @@ -0,0 +1,322 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'strscan' + +module Ronin + module Support + class Encoding < ::Encoding + # + # Contains methods for encoding/decoding escaping/unescaping Python data. + # + # ## Core-Ext Methods + # + # * {Integer#python_escape} + # * {Integer#python_encode} + # * {String#python_escape} + # * {String#python_unescape} + # * {String#python_encode} + # * {String#python_string} + # * {String#python_unquote} + # + # @see https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals + # + # @api public + # + # @since 1.2.0 + # + module Python + # + # Encodes a byte as a Python escaped String. + # + # @param [Integer] byte + # The byte value to encode. + # + # @return [String] + # The escaped Python character. + # + # @example + # Encoding::Python.encode_byte(0x41) + # # => "\\x41" + # Encoding::Python.encode_byte(0x100) + # # => "\\u1000" + # Encoding::Python.encode_byte(0x10000) + # # => "\\u100000" + # + # @see https://docs.python.org/3/reference/lexical_analysis.html#escape-sequences + # + def self.encode_byte(byte) + if byte >= 0x00 && byte <= 0xff + "\\x%.2x" % byte + elsif byte >= 0x100 && byte <= 0xffff + "\\u%.4x" % byte + elsif byte >= 0x10000 && byte <= 0x10ffff + "\\U%.8x" % byte + else + raise(RangeError,"#{byte.inspect} out of char range") + end + end + + # Special Python bytes and their escaped Strings. + # + # @see https://docs.python.org/3/reference/lexical_analysis.html#escape-sequences + ESCAPE_BYTES = { + 0x00 => '\x00', + 0x07 => '\a', + 0x08 => '\b', + 0x09 => '\t', + 0x0a => '\n', + 0x0b => '\v', + 0x0c => '\f', + 0x0d => '\r', + 0x22 => '\"', + 0x5c => '\\\\' + } + + # + # Escapes a byte as a Python character. + # + # @param [Integer] byte + # The byte value to escape. + # + # @return [String] + # The escaped Python character. + # + # @raise [RangeError] + # The integer value is negative. + # + # @example + # Encoding::Python.escape_byte(0x41) + # # => "A" + # Encoding::Python.escape_byte(0x22) + # # => "\\\"" + # Encoding::Python.escape_byte(0x7f) + # # => "\\x7f" + # + # @example Escaping unicode characters: + # Encoding::Python.escape_byte(0xffff) + # # => "\\uffff" + # Encoding::Python.escape_byte(0x10000) + # # => "\\U00100000" + # + # @see https://docs.python.org/3/reference/lexical_analysis.html#escape-sequences + # + def self.escape_byte(byte) + if byte >= 0x00 && byte <= 0xff + ESCAPE_BYTES.fetch(byte) do + if byte >= 0x20 && byte <= 0x7e + byte.chr + else + encode_byte(byte) + end + end + else + encode_byte(byte) + end + end + + # + # Encodes each character of the given data as Python escaped characters. + # + # @param [String] data + # The given data to encode. + # + # @return [String] + # The Python encoded String. + # + # @example + # Encoding::Python.encode("hello") + # # => "\\x68\\x65\\x6c\\x6c\\x6f" + # + # @see https://docs.python.org/3/reference/lexical_analysis.html#escape-sequences + # + def self.encode(data) + encoded = String.new + + if data.valid_encoding? + data.each_codepoint do |codepoint| + encoded << encode_byte(codepoint) + end + else + data.each_byte do |byte| + encoded << encode_byte(byte) + end + end + + return encoded + end + + # + # Decodes the Python encoded data. + # + # @param [String] data + # The given Python data to decode. + # + # @return [String] + # The decoded data. + # + # @see unescape + # @see https://docs.python.org/3/reference/lexical_analysis.html#escape-sequences + # + def self.decode(data) + unescape(data) + end + + # + # Escapes the Python encoded data. + # + # @param [String] data + # The data to Python escape. + # + # @return [String] + # The Python escaped String. + # + # @see https://docs.python.org/3/reference/lexical_analysis.html#escape-sequences + # + def self.escape(data) + escaped = String.new + + if data.valid_encoding? + data.each_codepoint do |codepoint| + escaped << escape_byte(codepoint) + end + else + data.each_byte do |byte| + escaped << escape_byte(byte) + end + end + + return escaped + end + + # Python characters that must be back-slashed. + # + # @see https://docs.python.org/3/reference/lexical_analysis.html#escape-sequences + BACKSLASHED_CHARS = { + '\\0' => "\0", + '\\a' => "\a", + '\\b' => "\b", + '\\t' => "\t", + '\\n' => "\n", + '\\v' => "\v", + '\\f' => "\f", + '\\r' => "\r", + "\\\"" => '"', + "\\'" => "'", + "\\\\" => "\\" + } + + # + # Unescapes the given Python escaped data. + # + # @param [String] data + # The given Python escaped data. + # + # @return [String] + # The unescaped Python String. + # + # @example + # Encoding::Python.unescape("\\x68\\x65\\x6c\\x6c\\x6f\\x20\\x77\\x6f\\x72\\x6c\\x64") + # # => "hello world" + # + # @see https://docs.python.org/3/reference/lexical_analysis.html#escape-sequences + # + def self.unescape(data) + unescaped = String.new(encoding: Encoding::UTF_8) + scanner = StringScanner.new(data) + + until scanner.eos? + unescaped << if (hex_escape = scanner.scan(/\\x[0-9a-fA-F]{1,2}/)) # \xXX + hex_escape[2..].to_i(16).chr + elsif (unicode_escape = scanner.scan(/\\u[0-9a-fA-F]{4}|\\U[0-9a-fA-F]{8}/)) # \uXXXX or \UXXXXXXXX + unicode_escape[2..].to_i(16).chr(Encoding::UTF_8) + elsif (octal_escape = scanner.scan(/\\[0-7]{1,3}/)) # \N, \NN, or \NNN + octal_escape[1..].to_i(8).chr + elsif (backslash_escape = scanner.scan(/\\./)) # \[A-Za-z] + BACKSLASHED_CHARS.fetch(backslash_escape,backslash_escape) + else + scanner.getch + end + end + + return unescaped + end + + # + # Escapes and quotes the given data as a Python string. + # + # @param [String] data + # The given data to escape and quote. + # + # @return [String] + # The quoted Python string. + # + # @example + # Encoding::Python.quote("hello\nworld\n") + # # => "\"hello\\nworld\\n\"" + # + # @see https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals + # + def self.quote(data) + "\"#{escape(data)}\"" + end + + # + # Unquotes and unescapes the given Python string. + # + # @param [String] data + # The given Python string. + # + # @return [String] + # The un-quoted String if the String begins and ends with quotes, or + # the same String if it is not quoted. + # + # @example + # Encoding::Python.unquote("\"hello\\nworld\"") + # # => "hello\nworld" + # Encoding::Python.unquote("'hello\\nworld'") + # # => "hello\nworld" + # Encoding::Python.unquote("'''hello\\nworld'''") + # # => "hello\nworld" + # Encoding::Python.unquote("u'hello\\nworld'") + # # => "hello\nworld" + # Encoding::Python.unquote("r'hello\\nworld'") + # # => "hello\\nworld" + # + # @see https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals + # + def self.unquote(data) + if (data.start_with?("'''") && data.end_with?("'''")) + unescape(data[3..-4]) + elsif ((data.start_with?('"') && data.end_with?('"')) || + (data.start_with?("'") && data.end_with?("'"))) + unescape(data[1..-2]) + elsif (data.start_with?("u'") && data.end_with?("'")) # unicode string + unescape(data[2..-2]) + elsif (data.start_with?("r'") && data.end_with?("'")) # raw string + data[2..-2] + else + data + end + end + end + end + end +end + +require 'ronin/support/encoding/python/core_ext' diff --git a/lib/ronin/support/encoding/python/core_ext.rb b/lib/ronin/support/encoding/python/core_ext.rb new file mode 100644 index 000000000..28a29163a --- /dev/null +++ b/lib/ronin/support/encoding/python/core_ext.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/encoding/python/core_ext/integer' +require 'ronin/support/encoding/python/core_ext/string' diff --git a/lib/ronin/support/encoding/python/core_ext/integer.rb b/lib/ronin/support/encoding/python/core_ext/integer.rb new file mode 100644 index 000000000..778de90e2 --- /dev/null +++ b/lib/ronin/support/encoding/python/core_ext/integer.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/encoding/python' + +class Integer + + # + # Escapes the Integer as a Python character. + # + # @return [String] + # The escaped Python character. + # + # @example + # 0x41.python_escape + # # => "A" + # 0x22.python_escape + # # => "\\\"" + # 0x7f.python_escape + # # => "\\x7f" + # + # @see Ronin::Support::Encoding::Python.escape_byte + # @see https://docs.python.org/3/reference/lexical_analysis.html#escape-sequences + # + # @since 1.2.0 + # + # @api public + # + def python_escape + Ronin::Support::Encoding::Python.escape_byte(self) + end + + # + # Encodes the Integer as a Python character. + # + # @return [String] + # The encoded Python character. + # + # @example + # 0x41.python_encode + # # => "\\x41" + # + # @see Ronin::Support::Encoding::Python.encode_byte + # @see https://docs.python.org/3/reference/lexical_analysis.html#escape-sequences + # + # @since 1.2.0 + # + # @api public + # + def python_encode + Ronin::Support::Encoding::Python.encode_byte(self) + end + +end diff --git a/lib/ronin/support/encoding/python/core_ext/string.rb b/lib/ronin/support/encoding/python/core_ext/string.rb new file mode 100644 index 000000000..ae9422c14 --- /dev/null +++ b/lib/ronin/support/encoding/python/core_ext/string.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# Ronin Support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ronin Support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Ronin Support. If not, see . +# + +require 'ronin/support/encoding/python' + +class String + + # + # Escapes a String for Python. + # + # @return [String] + # The Python escaped String. + # + # @example + # "hello\nworld\n".python_escape + # # => "hello\\nworld\\n" + # + # @see Ronin::Support::Encoding::Python.escape + # @see https://docs.python.org/3/reference/lexical_analysis.html#escape-sequences + # + # @since 1.2.0 + # + # @api public + # + def python_escape + Ronin::Support::Encoding::Python.escape(self) + end + + # + # Unescapes a Python escaped String. + # + # @return [String] + # The unescaped Python String. + # + # @example + # "\\x68\\x65\\x6c\\x6c\\x6f\\x20\\x77\\x6f\\x72\\x6c\\x64".python_unescape + # # => "hello world" + # + # @see Ronin::Support::Encoding::Python.unescape + # @see https://docs.python.org/3/reference/lexical_analysis.html#escape-sequences + # + # @since 1.2.0 + # + # @api public + # + def python_unescape + Ronin::Support::Encoding::Python.unescape(self) + end + + # + # Python escapes every character of the String. + # + # @return [String] + # The Python escaped String. + # + # @example + # "hello".python_encode + # # => "\\u0068\\u0065\\u006c\\u006c\\u006f" + # + # @see Ronin::Support::Encoding::Python.encode + # @see https://docs.python.org/3/reference/lexical_analysis.html#escape-sequences + # + # @api public + # + # @since 1.2.0 + # + def python_encode + Ronin::Support::Encoding::Python.encode(self) + end + + alias python_decode python_unescape + + # + # Converts the String into a Python string. + # + # @return [String] + # + # @example + # "hello\nworld\n".python_string + # # => "\"hello\\nworld\\n\"" + # + # @see Ronin::Support::Encoding::Python.quote + # @see https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals + # + # @since 1.2.0 + # + # @api public + # + def python_string + Ronin::Support::Encoding::Python.quote(self) + end + + # + # Removes the quotes an unescapes a Python string. + # + # @return [String] + # The un-quoted String if the String begins and ends with quotes, or the + # same String if it is not quoted. + # + # @example + # "\"hello\\nworld\"".python_unquote + # # => "hello\nworld" + # "\"hello\\nworld\"".python_unquote + # # => "hello\nworld" + # "'hello\\nworld'".python_unquote + # # => "hello\nworld" + # "'''hello\\nworld'''".python_unquote + # # => "hello\nworld" + # "u'hello\\nworld'".python_unquote + # # => "hello\nworld" + # "r'hello\\nworld'".python_unquote + # # => "hello\\nworld" + # + # @see Ronin::Support::Encoding::Python.unquote + # @see https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals + # + # @since 1.2.0 + # + # @api public + # + def python_unquote + Ronin::Support::Encoding::Python.unquote(self) + end + +end diff --git a/lib/ronin/support/encoding/ruby.rb b/lib/ronin/support/encoding/ruby.rb index 40189ec7b..4b1f82bbb 100644 --- a/lib/ronin/support/encoding/ruby.rb +++ b/lib/ronin/support/encoding/ruby.rb @@ -145,20 +145,18 @@ def self.escape(data) end # Common escaped characters. - UNESCAPE_CHARS = Hash.new do |hash,char| - if char[0] == '\\' then char[1] - else char - end - end - - UNESCAPE_CHARS['\0'] = "\0" - UNESCAPE_CHARS['\a'] = "\a" - UNESCAPE_CHARS['\b'] = "\b" - UNESCAPE_CHARS['\t'] = "\t" - UNESCAPE_CHARS['\n'] = "\n" - UNESCAPE_CHARS['\v'] = "\v" - UNESCAPE_CHARS['\f'] = "\f" - UNESCAPE_CHARS['\r'] = "\r" + # + # @since 1.2.0 + BACKSLASH_CHARS = { + '\0' => "\0", + '\a' => "\a", + '\b' => "\b", + '\t' => "\t", + '\n' => "\n", + '\v' => "\v", + '\f' => "\f", + '\r' => "\r" + } # # Unescapes the escaped String. @@ -188,7 +186,7 @@ def self.unescape(data) octal_escape[1,3].to_i(8).chr elsif (backslash_escape = scanner.scan(/\\./)) - UNESCAPE_CHARS[backslash_escape] + BACKSLASH_CHARS.fetch(backslash_escape,backslash_escape) else scanner.getch end diff --git a/lib/ronin/support/encoding/smtp.rb b/lib/ronin/support/encoding/smtp.rb index 7bade354a..5035cfa76 100644 --- a/lib/ronin/support/encoding/smtp.rb +++ b/lib/ronin/support/encoding/smtp.rb @@ -18,3 +18,25 @@ require 'ronin/support/encoding/base64' require 'ronin/support/encoding/quoted_printable' + +module Ronin + module Support + class Encoding < ::Encoding + # + # Alias for {QuotedPrintable}. + # + # ## Core-Ext Methods + # + # * {String#smtp_escape} + # * {String#smtp_unescape} + # + # @api public + # + # @since 1.2.0 + # + SMTP = QuotedPrintable + end + end +end + +require 'ronin/support/encoding/smtp/core_ext' diff --git a/lib/ronin/support/encoding/smtp/core_ext.rb b/lib/ronin/support/encoding/smtp/core_ext.rb new file mode 100644 index 000000000..8500f8a83 --- /dev/null +++ b/lib/ronin/support/encoding/smtp/core_ext.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/encoding/smtp/core_ext/string' diff --git a/lib/ronin/support/encoding/smtp/core_ext/string.rb b/lib/ronin/support/encoding/smtp/core_ext/string.rb new file mode 100644 index 000000000..4451e90a7 --- /dev/null +++ b/lib/ronin/support/encoding/smtp/core_ext/string.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/encoding/smtp' + +class String + + # + # Escapes the String as [Quoted-Printable]. + # + # [Quoted-Printable]: https://en.wikipedia.org/wiki/Quoted-printable + # + # @return [String] + # The quoted-printable escaped String. + # + # @example + # 'link'.smtp_escape + # # => "link=\n" + # + # @api public + # + # @since 1.2.0 + # + # @see #quoted_printable_escape + # + def smtp_escape + Ronin::Support::Encoding::SMTP.escape(self) + end + + alias smtp_encode smtp_escape + + # + # Unescapes a [Quoted-Printable] encoded String. + # + # [Quoted-Printable]: https://en.wikipedia.org/wiki/Quoted-printable + # + # @return [String] + # The unescaped String. + # + # @example + # "link=\n".smtp_unescape + # # => "link" + # + # @api public + # + # @since 1.2.0 + # + # @see #quoted_printable_unescape + # + def smtp_unescape + Ronin::Support::Encoding::SMTP.unescape(self) + end + + alias smtp_decode smtp_unescape + +end diff --git a/lib/ronin/support/network.rb b/lib/ronin/support/network.rb index 2721454fa..892eae82b 100644 --- a/lib/ronin/support/network.rb +++ b/lib/ronin/support/network.rb @@ -17,6 +17,7 @@ # require 'ronin/support/network/asn' +require 'ronin/support/network/defang' require 'ronin/support/network/dns' require 'ronin/support/network/domain' require 'ronin/support/network/email_address' @@ -33,5 +34,6 @@ require 'ronin/support/network/tld' require 'ronin/support/network/tls' require 'ronin/support/network/udp' +require 'ronin/support/network/url' require 'ronin/support/network/mixin' require 'ronin/support/network/core_ext' diff --git a/lib/ronin/support/network/defang.rb b/lib/ronin/support/network/defang.rb new file mode 100644 index 000000000..0c0cbfafa --- /dev/null +++ b/lib/ronin/support/network/defang.rb @@ -0,0 +1,223 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require_relative 'url' +require_relative 'ip' + +module Ronin + module Support + module Network + # + # Handles defanging and refanging IP addresses, host names, or URLs. + # + # ## Core-Ext Methods + # + # * {String#defang} + # * {String#refang} + # * {IPAddr#defang} + # * {URI::HTTP#defang} + # + # @api public + # + # @since 1.2.0 + # + module Defang + # + # Defangs an IP address. + # + # @param [#to_s] ip + # The IP address to defang. + # + # @return [String] + # The defanged IP address. + # + # @example + # Defang.defang_ip("192.168.1.1") + # # => "192[.]168[.]1[.]1" + # + def self.defang_ip(ip) + ip.to_s.gsub(/(?:\.|:{1,2})/) do |separator| + "[#{separator}]" + end + end + + # + # Refangs a de-fanged IP address. + # + # @param [String] ip + # The de-fanged IP address. + # + # @return [String] + # The refanged IP address. + # + # @example + # Defang.refang_ip("192[.]168[.]1[.]1") + # # => "192.168.1.1" + # + def self.refang_ip(ip) + ip.gsub(/\[(?:\.|:{1,2})\]/) do |separator| + separator[1..-2] + end + end + + # + # Defangs the host name. + # + # @param [#to_s] host + # The host name to defang. + # + # @return [String] + # The defanged host name. + # + # @example + # Defang.defang_host("www.example.com") + # # => "www[.]example[.]com" + # + def self.defang_host(host) + host.to_s.gsub('.','[.]') + end + + # + # Refangs a de-fanged host name. + # + # @param [String] host + # The de-fanged host name to refang. + # + # @return [String] + # The refanged host name. + # + # @example + # Defang.refang_host("www[.]example[.]com") + # # => "www.example.com" + # + def self.refang_host(host) + host.gsub('[.]') do |separator| + separator[1..-2] + end + end + + # + # Defangs a URL. + # + # @param [#to_s] url + # The URL to defang. + # + # @return [String] + # The defanged URL. + # + # @example + # Defang.defang_url("https://www.example.com:8080/foo?q=1") + # # => "hxxps[://]www[.]example[.]com[:]8080/foo?q=1" + # + def self.defang_url(url) + url.to_s.sub(%r{^[^:]+://[^/]+}) do |scheme_and_authority| + scheme, authority = scheme_and_authority.split('://',2) + + scheme.sub!(/^htt/,'hxx') + authority.gsub!(/(?:\.|:{1,2})/) do |separator| + "[#{separator}]" + end + + "#{scheme}[://]#{authority}" + end + end + + # + # Refangs a defanged URL. + # + # @param [String] url + # The defanged URL. + # + # @return [String] + # The refanged URL. + # + # @example + # Defang.refang_url("hxxps[://]www[.]example[.]com[:]8080/foo?q=1") + # # => "https://www.example.com:8080/foo?q=1" + # + def self.refang_url(url) + url.sub(%r{^[^:]+(?:\[://\]|://)[^/]+}) do |scheme_and_authority| + scheme, authority = scheme_and_authority.split(%r{\[://\]|://},2) + + scheme.sub!(/^hxx/,'htt') + authority.gsub!(/\[(?:\.|:{1,2})\]/) do |separator| + separator[1..-2] + end + + "#{scheme}://#{authority}" + end + end + + # + # Defangs a URL, IP address, or host name. + # + # @param [String] string + # The URL, IP address, or host name. + # + # @return [String] + # The defanged URL, IP address, or host name. + # + # @example + # Defang.defang("https://www.example.com:8080/foo?q=1") + # # => "hxxps[://]www[.]example[.]com[:]8080/foo?q=1" + # Defang.defang("192.168.1.1") + # # => "192[.]168[.]1[.]1" + # Defang.defang("www.example.com") + # # => "www[.]example[.]com" + # + def self.defang(string) + case string + when IP::REGEX then defang_ip(string) + when URL::REGEX then defang_url(string) + else defang_host(string) + end + end + + # + # Refangs a defanged URL, IP address, or host name. + # + # @param [String] string + # The defanged URL, IP address, or host name. + # + # @return [String] + # The refanged URL, IP address, or host name. + # + # @example + # Defang.refang("hxxps[://]www[.]example[.]com[:]8080/foo?q=1") + # # => "https://www.example.com:8080/foo?q=1" + # Defang.refang("192[.]168[.]1[.]1") + # # => "192.168.1.1" + # Defang.refang("www[.]example[.]com") + # # => "www.example.com" + # + def self.refang(string) + case string + when %r{^hxxp(s)?(?:://|\[://\])} + refang_url(string) + when /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\[\.\]|[0-9a-f]{1,4}\[:{1,2}\])/ + refang_ip(string) + else + refang_host(string) + end + end + end + end + end +end + +require 'ronin/support/network/defang/core_ext' diff --git a/lib/ronin/support/network/defang/core_ext.rb b/lib/ronin/support/network/defang/core_ext.rb new file mode 100644 index 000000000..aca298d42 --- /dev/null +++ b/lib/ronin/support/network/defang/core_ext.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/network/defang/core_ext/string' +require 'ronin/support/network/defang/core_ext/ipaddr' +require 'ronin/support/network/defang/core_ext/uri/http' diff --git a/lib/ronin/support/network/defang/core_ext/ipaddr.rb b/lib/ronin/support/network/defang/core_ext/ipaddr.rb new file mode 100644 index 000000000..469cfcc02 --- /dev/null +++ b/lib/ronin/support/network/defang/core_ext/ipaddr.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/network/defang' + +class IPAddr + + # + # Defangs an IP address. + # + # @return [String] + # The defanged IP address. + # + # @example + # ip = IPAddr.new("192.168.1.1") + # ip.defang + # # => "192[.]168[.]1[.]1" + # + # @api public + # + # @since 1.2.0 + # + def defang + Ronin::Support::Network::Defang.defang_ip(self) + end + +end diff --git a/lib/ronin/support/network/defang/core_ext/string.rb b/lib/ronin/support/network/defang/core_ext/string.rb new file mode 100644 index 000000000..3511ca3e5 --- /dev/null +++ b/lib/ronin/support/network/defang/core_ext/string.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/network/defang' + +class String + + # + # Defangs a URL, IP address, or host name. + # + # @return [String] + # The defanged URL, IP address, or host name. + # + # @example + # "https://www.example.com:8080/foo?q=1".defang + # # => "hxxps[://]www[.]example[.]com[:]8080/foo?q=1" + # "192.168.1.1".defang + # # => "192[.]168[.]1[.]1" + # "www.example.com".defang + # # => "www[.]example[.]com" + # + def defang + Ronin::Support::Network::Defang.defang(self) + end + + # + # Refangs a defanged URL, IP address, or host name. + # + # @return [String] + # The refanged URL, IP address, or host name. + # + # @example + # "hxxps[://]www[.]example[.]com[:]8080/foo?q=1".refang + # # => "https://www.example.com:8080/foo?q=1" + # "192[.]168[.]1[.]1".refang + # # => "192.168.1.1" + # "www[.]example[.]com".refang + # # => "www.example.com" + # + # @api public + # + # @since 1.2.0 + # + def refang + Ronin::Support::Network::Defang.refang(self) + end + +end diff --git a/lib/ronin/support/network/defang/core_ext/uri/http.rb b/lib/ronin/support/network/defang/core_ext/uri/http.rb new file mode 100644 index 000000000..ffe4be1f7 --- /dev/null +++ b/lib/ronin/support/network/defang/core_ext/uri/http.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/network/defang' + +require 'uri/http' + +module URI + class HTTP < Generic + + # + # Defangs a URL. + # + # @return [String] + # The defanged URL. + # + # @example + # uri = URI("https://www.example.com:8080/foo?q=1") + # uri.defang + # # => "hxxps[://]www[.]example[.]com[:]8080/foo?q=1" + # + # @api public + # + # @since 1.2.0 + # + def defang + Ronin::Support::Network::Defang.defang_url(self) + end + + end +end diff --git a/lib/ronin/support/network/defang/mixin.rb b/lib/ronin/support/network/defang/mixin.rb new file mode 100644 index 000000000..c28026e5c --- /dev/null +++ b/lib/ronin/support/network/defang/mixin.rb @@ -0,0 +1,181 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# Ronin Support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ronin Support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Ronin Support. If not, see . +# + +require 'ronin/support/network/defang' + +module Ronin + module Support + module Network + module Defang + # + # Provides helper methods for defanging or refanging URLs, IP addresses, + # or host names. + # + # @api public + # + # @since 1.2.0 + # + module Mixin + # + # Defangs an IP address. + # + # @param [#to_s] ip + # The IP address to defang. + # + # @return [String] + # The defanged IP address. + # + # @example + # defang_ip("192.168.1.1") + # # => "192[.]168[.]1[.]1" + # + def defang_ip(ip) + Defang.defang_ip(ip) + end + + # + # Refangs a de-fanged IP address. + # + # @param [String] ip + # The de-fanged IP address. + # + # @return [String] + # The refanged IP address. + # + # @example + # refang_ip("192[.]168[.]1[.]1") + # # => "192.168.1.1" + # + def refang_ip(ip) + Defang.refang_ip(ip) + end + + # + # Defangs the host name. + # + # @param [#to_s] host + # The host name to defang. + # + # @return [String] + # The defanged host name. + # + # @example + # defang_host("www.example.com") + # # => "www[.]example[.]com" + # + def defang_host(host) + Defang.defang_host(host) + end + + # + # Refangs a de-fanged host name. + # + # @param [String] host + # The de-fanged host name to refang. + # + # @return [String] + # The refanged host name. + # + # @example + # refang_host("www[.]example[.]com") + # # => "www.example.com" + # + def refang_host(host) + Defang.refang_host(host) + end + + # + # Defangs a URL. + # + # @param [#to_s] url + # The URL to defang. + # + # @return [String] + # The defanged URL. + # + # @example + # defang_url("https://www.example.com:8080/foo?q=1") + # # => "hxxps[://]www[.]example[.]com[:]8080/foo?q=1" + # + def defang_url(url) + Defang.defang_url(url) + end + + # + # Refangs a defanged URL. + # + # @param [String] url + # The defanged URL. + # + # @return [String] + # The refanged URL. + # + # @example + # refang_url("hxxps[://]www[.]example[.]com[:]8080/foo?q=1") + # # => "https://www.example.com:8080/foo?q=1" + # + def refang_url(url) + Defang.refang_url(url) + end + + # + # Defangs a URL, IP address, or host name. + # + # @param [String] string + # The URL, IP address, or host name. + # + # @return [String] + # The defanged URL, IP address, or host name. + # + # @example + # defang("https://www.example.com:8080/foo?q=1") + # # => "hxxps[://]www[.]example[.]com[:]8080/foo?q=1" + # defang("192.168.1.1") + # # => "192[.]168[.]1[.]1" + # defang("www.example.com") + # # => "www[.]example[.]com" + # + def defang(string) + Defang.defang(string) + end + + # + # Refangs a defanged URL, IP address, or host name. + # + # @param [String] string + # The defanged URL, IP address, or host name. + # + # @return [String] + # The refanged URL, IP address, or host name. + # + # @example + # refang("hxxps[://]www[.]example[.]com[:]8080/foo?q=1") + # # => "https://www.example.com:8080/foo?q=1" + # refang("192[.]168[.]1[.]1") + # # => "192.168.1.1" + # refang("www[.]example[.]com") + # # => "www.example.com" + # + def refang(string) + Defang.refang(string) + end + end + end + end + end +end diff --git a/lib/ronin/support/network/host.rb b/lib/ronin/support/network/host.rb index af1ef86b1..f2e6102cf 100644 --- a/lib/ronin/support/network/host.rb +++ b/lib/ronin/support/network/host.rb @@ -21,6 +21,7 @@ require 'ronin/support/network/ip' require 'ronin/support/network/tld' require 'ronin/support/network/public_suffix' +require 'ronin/support/network/defang' module Ronin module Support @@ -140,6 +141,11 @@ module Network # class Host + # A regular expression for matching host names. + # + # @since 1.2.0 + REGEX = /\A(?:(?:[a-zA-Z\d](?:[-a-zA-Z\d]*[a-zA-Z\d])?)\.)*(?:[a-zA-Z](?:[-a-zA-Z\d]*[a-zA-Z\d])?)\.?\z/ + # The host name. # # @return [String] @@ -158,6 +164,23 @@ def initialize(name) @name = name end + # + # Defangs the host name. + # + # @return [String] + # The defanged host name. + # + # @example + # host = Host.new("www.example.com") + # host.defang + # # => "www[.]example[.]com" + # + # @since 1.2.0 + # + def defang + Defang.defang_host(self) + end + # # Determines if the hostname is an [IDN] hostname. # diff --git a/lib/ronin/support/network/ip.rb b/lib/ronin/support/network/ip.rb index 8fd300c47..34e881208 100644 --- a/lib/ronin/support/network/ip.rb +++ b/lib/ronin/support/network/ip.rb @@ -20,6 +20,7 @@ require 'ronin/support/network/asn' require 'ronin/support/network/dns' require 'ronin/support/network/host' +require 'ronin/support/network/defang' require 'ronin/support/text/patterns' require 'ipaddr' @@ -322,6 +323,23 @@ def address @address ||= to_s end + # + # Defangs the IP address. + # + # @return [String] + # The defanged IP address. + # + # @example + # ip = IP.new("192.168.1.1") + # ip.defang + # # => "192[.]168[.]1[.]1" + # + # @since 1.2.0 + # + def defang + Defang.defang_ip(self) + end + # # The Autonomous System Number (ASN) information for the IP address. # diff --git a/lib/ronin/support/network/mixin.rb b/lib/ronin/support/network/mixin.rb index df3a31382..9db50a593 100644 --- a/lib/ronin/support/network/mixin.rb +++ b/lib/ronin/support/network/mixin.rb @@ -23,6 +23,7 @@ require 'ronin/support/network/ssl/mixin' require 'ronin/support/network/unix/mixin' require 'ronin/support/network/http/mixin' +require 'ronin/support/network/defang/mixin' module Ronin module Support @@ -42,6 +43,7 @@ module Mixin include SSL::Mixin include UNIX::Mixin include HTTP::Mixin + include Defang::Mixin end end end diff --git a/lib/ronin/support/network/url.rb b/lib/ronin/support/network/url.rb new file mode 100644 index 000000000..0444bc316 --- /dev/null +++ b/lib/ronin/support/network/url.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require 'ronin/support/network/http' +require 'ronin/support/network/defang' + +require 'addressable/uri' +require 'uri/query_params/core_ext/addressable/uri' + +module Ronin + module Support + module Network + # + # Represents a URL. + # + # ## Features + # + # * Supports parsing URLs with IDN domains. + # + # @api public + # + # @since 1.2.0 + # + class URL < Addressable::URI + + # Regular expression to match all URLs. + REGEX = URI::DEFAULT_PARSER.make_regexp + + # + # Defangs the URL. + # + # @return [String] + # The defanged URL. + # + # @example + # url = URL.new("https://www.example.com:8080/foo?q=1") + # url.defang + # # => "hxxps[://]www[.]example[.]com[:]8080/foo?q=1" + # + def defang + Defang.defang_url(self) + end + + # + # Returns the Status Code of the HTTP Response for the URL. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments for {HTTP.response_status}. + # + # @return [Integer] + # The HTTP Response Status. + # + # @example + # url = Network::URL.parse('http://github.com/') + # url.status + # # => 301 + # + # @see HTTP.response_status + # + def status(**kwargs) + HTTP.response_status(self,**kwargs) + end + + # + # Checks if the HTTP response for the URL has an HTTP `OK` status code. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments for {HTTP.ok?}. + # + # @return [Boolean] + # Specifies whether the response had an HTTP OK status code or not. + # + # @example + # url = Network::URL.parse('https://example.com/') + # url.ok? + # # => true + # + # @see HTTP.ok? + # + def ok?(**kwargs) + HTTP.ok?(self,**kwargs) + end + + end + end + end +end diff --git a/lib/ronin/support/network/wildcard.rb b/lib/ronin/support/network/wildcard.rb index fe2675977..b68bcfe23 100644 --- a/lib/ronin/support/network/wildcard.rb +++ b/lib/ronin/support/network/wildcard.rb @@ -41,6 +41,13 @@ class Wildcard # @return [String] attr_reader :template + # The regular expression that represents the hostname wildcard. + # + # @return [Regexp] + # + # @since 1.2.0 + attr_reader :regex + # # Initializes the wildcard hostname. # @@ -52,6 +59,14 @@ class Wildcard # def initialize(template) @template = template + + if @template.include?('*') + prefix, suffix = @template.split('*',2) + + @regex = /\A#{Regexp.escape(prefix)}(.*?)#{Regexp.escape(suffix)}\z/ + else + @regex = /\A#{Regexp.escape(@template)}\z/ + end end # @@ -72,6 +87,27 @@ def subdomain(name) Host.new(@template.sub('*',name)) end + # + # Tests whether the hostname belongs to the wildcard hostname. + # + # @param [String] host + # The hostname to compare against the wildcard hostname. + # + # @return [Boolean] + # + # @example + # wildcard = Network::Wildcard.new('*.example.com') + # wildcard === 'www.example.com' + # # => true + # + # @since 1.2.0 + # + def ===(host) + @regex === host + end + + alias include? === + # # Converts the wildcard hostname to a String. # diff --git a/lib/ronin/support/software.rb b/lib/ronin/support/software.rb new file mode 100644 index 000000000..ba2fb0488 --- /dev/null +++ b/lib/ronin/support/software.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require_relative 'software/version' +require_relative 'software/version_range' diff --git a/lib/ronin/support/software/version.rb b/lib/ronin/support/software/version.rb new file mode 100644 index 000000000..e49ebfa34 --- /dev/null +++ b/lib/ronin/support/software/version.rb @@ -0,0 +1,305 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +module Ronin + module Support + module Software + # + # Represents a software version number. + # + # ## Examples + # + # Supports parsing a variety of version formats: + # + # 42 + # 1.2 + # 1.2.3 + # 1.2.3a + # 1.2.3.4 + # 1.2.3-4 + # 1.2.3_4 + # 1.2.3.pre + # 1.2.3-pre + # 1.2.3_pre + # 1.2.3.pre1 + # 1.2.3-pre1 + # 1.2.3_pre1 + # 1.2.3.alpha + # 1.2.3-alpha + # 1.2.3_alpha + # 1.2.3.alpha1 + # 1.2.3-alpha1 + # 1.2.3_alpha1 + # 1.2.3.beta + # 1.2.3-beta + # 1.2.3_beta + # 1.2.3.beta1 + # 1.2.3-beta1 + # 1.2.3_beta1 + # 1.2.3.rc + # 1.2.3-rc + # 1.2.3_rc + # 1.2.3.rc1 + # 1.2.3-rc1 + # 1.2.3_rc1 + # + # Supports comparing version numbers: + # + # version1 = Software::Version.new('1.2.0') + # version2 = Software::Version.new('1.2.3.1') + # version2 >= version1 + # # => true + # + # @api public + # + # @since 1.2.0 + # + class Version + + include Comparable + + # The version string. + # + # @return [String] + attr_reader :string + + # The individual parsed version numbers. + # + # @return [Array] + attr_reader :parts + + # + # Initializes the version number. + # + # @param [String] string + # The version string to parse. + # + # @example + # Software::Version.new('42') + # Software::Version.new('1.2') + # Software::Version.new('1.2.3') + # Software::Version.new('1.2.3.rc1') + # Software::Version.new('1.2.3-rc1') + # Software::Version.new('1.2.a') + # Software::Version.new('1.2.abc') + # + def initialize(string) + @string = string + @parts = [] + + parse! + end + + private + + # + # Internal method which parses the {#string} instance variable and + # populates {#parts}. + # + # @note + # This method mainly exists in case you want to sub-class {Version} + # and define your own custom version string parsing logic. + # + # @api private + # + def parse! + # ignore everything after the '+' symbol, then split by '.', '-', '_'. + @string.sub(/\+.+\z/,'').split(/[._-]/).each do |part| + if part =~ /\A\d+\z/ + # append the version number + @parts << part.to_i + elsif (match = part.match(/\A(pre|alpha|beta|rc)(\d+)?\z/)) + # append the pre|alpha|beta|rc as a separate Symbol element + @parts << match[1].to_sym + + if (number = match[2]) + # append the number as a separate Integer element + @parts << number.to_i + end + elsif (match = part.match(/\Ap(\d+)\z/)) # -pN / .pN + # omit the 'p' prefix and append the number + @parts << match[1].to_i + else + # append everything else as a String + @parts << part + end + end + end + + public + + # + # Parses the version string. + # + # @param [String] string + # The version string to parse. + # + # @return [Version] + # The parsed version string. + # + # @see #initialize + # + def self.parse(string) + new(string) + end + + # Explicit order of pre-release version tags. + # + # @api private + PRERELEASE_ORDER = [ + :pre, + :alpha, + :beta, + :rc + ] + + # + # Compares the version to another version. + # + # @param [Version] other + # The other version to compare with. + # + # @return [-1, 0, 1] + # Returns `-1`, `0`, `1`, if the version is less than, equal to, or + # greater than the other version, respectively. + # + def <=>(other) + # quickly return if the version strings are identical + return 0 if @string == other.string + + max_length = [@parts.length, other.parts.length].max + index = 0 + + while index < max_length + # missing version parts will be filled in with 0s + # + # 1.2 <=> 1.2.3 ---> 1.2.0 <=> 1.2.3 + # + part = @parts.fetch(index,0) + other_part = other.parts.fetch(index,0) + + # must increment index before calling next + index += 1 + + case part + when Integer + case other_part + when Integer + # Comparison between two version numbers. + # + # Examples: + # 1.2.3 == 1.2.3 + # 1.2.0 < 1.2.3 + # 1.2.3 > 1.2.0 + if part == other_part + next # keep going + else + return part <=> other_part # tie breaker + end + when Symbol + # Comparison between a version number and a version modifier. + # + # Examples: + # 1.2.0.1 > 1.2.0.alpha + # 1.2.0.1 > 1.2.0-a1b2c3 + return 1 + when String + # Comparison between a version number and an unrecognized + # version tag / build-info string. + # + # Examples: + # 1.2.3 < 1.2.3a + # 1.2.30 > 1.2.3a + return part.to_s <=> other_part + end + when Symbol + case other_part + when Integer + # Comparison between a recognized version modifier and a + # version number. + # + # Examples: + # 1.2.0.alpha < 1.2.0.1 + return -1 + when Symbol + # Comparison between two recognized version modifiers. + # + # Examples: + # 1.2.0.pre < 1.2.0.alpha + # 1.2.0.alpha < 1.2.0.beta + # 1.2.3.beta < 1.2.0.rc + if part == other_part + next # keep going + else + # tie breaker + return PRERELEASE_ORDER.index(part) <=> + PRERELEASE_ORDER.index(other_part) + end + when String + # Comparison between a recognized version modifier (ex: alpha) + # and an unrecognized version tag or build-info. + # + # Examples: + # 1.2.0.alpha > 1.2.0.1a + return 1 + end + when String + case other_part + when Integer + # Comparison between an unrecognized version tag or build-info + # string and an Integer. + # + # Examples: + # 1.2.3a > 1.2.3 + # 1.2.3a < 1.2.30 + return part <=> other_part.to_s + when Symbol + # Comparison between an unrecognized version tag or build-info + # string and a recognized version modifier. + # + # Examples: + # 1.2.0.1a > 1.2.0.pre1 + return 1 + when String + # Comparison between two unrecognized version tags or build-info + # strings. + # + # Examples: + # 1.2.3a < 1.2.3b + # 1.2.3b < 1.2.3c + return part <=> other_part + end + end + end + + # All version elements are equal, even though the version strings may + # be slightly different. + # + # Examples: + # 1.2.3.alpha1 == 1.2.3.alpha.1 + # 1.2.3.alpha1 == 1.2.3-alpha1 + # 1.2.3.alpha1 == 1.2.3-alpha-1 + return 0 + end + + alias to_s string + + end + end + end +end diff --git a/lib/ronin/support/software/version_constraint.rb b/lib/ronin/support/software/version_constraint.rb new file mode 100644 index 000000000..e463ebb31 --- /dev/null +++ b/lib/ronin/support/software/version_constraint.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require_relative 'version' + +module Ronin + module Support + module Software + # + # Represents a version constraint (ex: `>= 1.2.3`). + # + # ## Examples + # + # constraint = Software::VersionConstraint.new('>= 1.2.3') + # version = Software::Version.new('1.3.0') + # constraint.include?(version) + # # => true + # + # @api public + # + # @since 1.2.0 + # + class VersionConstraint + + # The version constraint string. + # + # @return [String] + attr_reader :string + + # The version constraint operator. + # + # @return [">", ">=", "<", "<=", "="] + attr_reader :operator + + # The parsed version number. + # + # @return [Version] + attr_reader :version + + # + # Initializes the version constraint. + # + # @param [String] string + # The version constraint to parse. + # + # @raise [ArgumentError] + # Could not parse the version constraint. + # + # @example + # Software::VersionConstraint.new('>= 1.2.3') + # Software::VersionConstraint.new('> 1.2.3') + # Software::VersionConstraint.new('<= 1.2.3') + # Software::VersionConstraint.new('< 1.2.3') + # Software::VersionConstraint.new('= 1.2.3') + # Software::VersionConstraint.new('1.2.3') + # + def initialize(string) + @string = string + + if (match = string.match(/\A(?:(?>=|>|<=|<|=)?\s*)(?\S+)\z/)) + @operator = match[:operator] || '=' + @version = Version.new(match[:version]) + else + raise(ArgumentError,"invalid version constraint: #{string.inspect}") + end + end + + # + # Parses the version constraint. + # + # @param [String] string + # The version constraint to parse. + # + # @return [VersionConstraint] + # The parsed version constraint. + # + def self.parse(string) + new(string) + end + + # + # Compares the version to the version constraint. + # + # @param [Version] version + # The version number to compare. + # + # @return [Boolean] + # + # @api public + # + def include?(version) + case @operator + when '>' then version > @version + when '>=' then version >= @version + when '<' then version < @version + when '<=' then version <= @version + when '=' then version == @version + else + raise(NotImplementedError,"version operator not supported: #{@operator.inspect}") + end + end + + alias === include? + + # + # Compares the version constraint to another version constraint. + # + # @param [VersionConstraint] other + # The other version constraint to compare against. + # + # @return [Boolean] + # + # @api public + # + def ==(other) + self.class == other.class && + @operator == other.operator && + @version == other.version + end + + end + end + end +end diff --git a/lib/ronin/support/software/version_range.rb b/lib/ronin/support/software/version_range.rb new file mode 100644 index 000000000..7a7fc87b9 --- /dev/null +++ b/lib/ronin/support/software/version_range.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +require_relative 'version_constraint' + +module Ronin + module Support + module Software + # + # Represents a version range (ex: `>= 1.2.3, < 2.0.0`). + # + # ## Examples + # + # version_range = Software::VersionRange.new('>= 1.2.3, < 2.0.0') + # version = Software::Version.new('1.2.8') + # version_range.include?(version) + # # => true + # + # @api public + # + # @since 1.2.0 + # + class VersionRange + + # The version range string. + # + # @return [String] + attr_reader :string + + # The individual version constraints. + # + # @return [Array] + attr_reader :constraints + + # + # Initializes the version range. + # + # @param [String] string + # The version range string to parse. + # + # @example + # version_range = Software::VersionRange.new('>= 1.2.3, < 2.0.0') + # version = Software::Version.new('1.2.8') + # version_range.include?(version) + # # => true + # + def initialize(string) + @string = string + + @constraints = string.split(/,\s*/).map do |constraint| + VersionConstraint.new(constraint) + end + end + + # + # Parses a version range. + # + # @param [String] string + # The version range string to parse. + # + # @return [VersionRange] + # The parsed version range. + # + def self.parse(string) + new(string) + end + + # + # Compares the version number against the version range. + # + # @param [Version] version + # The version number to compare. + # + # @return [Boolean] + # Indicates whether the version number satisfies all of the version + # constraints in the version range. + # + def include?(version) + @constraints.all? { |constraint| constraint.include?(version) } + end + + alias === include? + + # + # Compares the version range to another version range. + # + # @param [VersionRange] other + # The other version range. + # + # @return [Boolean] + # + def ==(other) + self.class == other.class && @constraints == other.constraints + end + + alias to_s string + + end + end + end +end diff --git a/lib/ronin/support/text/patterns.rb b/lib/ronin/support/text/patterns.rb index bd2f36da0..402fb94ab 100644 --- a/lib/ronin/support/text/patterns.rb +++ b/lib/ronin/support/text/patterns.rb @@ -23,4 +23,5 @@ require 'ronin/support/text/patterns/file_system' require 'ronin/support/text/patterns/network' require 'ronin/support/text/patterns/pii' +require 'ronin/support/text/patterns/software' require 'ronin/support/text/patterns/source_code' diff --git a/lib/ronin/support/text/patterns/numeric.rb b/lib/ronin/support/text/patterns/numeric.rb index 5246b1133..5f1638aab 100644 --- a/lib/ronin/support/text/patterns/numeric.rb +++ b/lib/ronin/support/text/patterns/numeric.rb @@ -30,22 +30,55 @@ module Patterns # Regular expression for finding all numbers in text. # # @since 1.0.0 - NUMBER = /[0-9]+/ + NUMBER = /(?:-)?[0-9]+(?:e[+-]?\d+)?/ + + # Regular expression for finding all floating point numbers in text. + # + # @since 1.2.0 + FLOAT = /(?:-)?\d+\.\d+(?:e[+-]?\d+)?/ + + # Regular expression for finding a octal bytes (0 - 377) + # + # @since 1.2.0 + OCTAL_BYTE = /(?<=[^\d]|^)(?:3[0-7]{2}|[0-2][0-7]{2}|[0-7]{1,2})(?=[^\d]|$)/ + + # Regular expression for finding a decimal bytes (0 - 255) + # + # @since 1.2.0 + DECIMAL_BYTE = /(?<=[^\d]|^)(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])(?=[^\d]|$)/ # Regular expression for finding a decimal octet (0 - 255) # # @since 0.4.0 - DECIMAL_OCTET = /(?<=[^\d]|^)(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])(?=[^\d]|$)/ + # + # @deprecated + # Deprecated as of 1.2.0. Please use {DECIMAL_BYTE} instead. + DECIMAL_OCTET = DECIMAL_BYTE - # Regular expression for finding all hexadecimal numbers in text. + # Regular expression for finding hexadecimal bytes (00 - ff). # - # @since 1.0.0 - HEX_NUMBER = /(?:0x)?[0-9a-fA-F]+/ + # @since 1.2.0 + HEX_BYTE = /(?:0x)?[0-9a-fA-F]{2}/ + + # Regular expression for finding hexadecimal words (0000 - ffff). + # + # @since 1.2.0 + HEX_WORD = /(?:0x)?[0-9a-fA-F]{4}/ + + # Regular expression for finding hexadecimal double words (00000000 - ffffffff). + # + # @since 1.2.0 + HEX_DWORD = /(?:0x)?[0-9a-fA-F]{8}/ - # Regular expression for finding version numbers in text. + # Regular expression for finding hexadecimal double words (0000000000000000 - ffffffffffffffff). + # + # @since 1.2.0 + HEX_QWORD = /(?:0x)?[0-9a-fA-F]{16}/ + + # Regular expression for finding all hexadecimal numbers in text. # # @since 1.0.0 - VERSION_NUMBER = /\d+\.\d+(?:(?!\.(?:tar|tgz|tbz|zip|rar|txt|htm|xml))[._-][A-Za-z0-9]+)*/ + HEX_NUMBER = /(?:0x)?[0-9a-fA-F]+/ end end end diff --git a/lib/ronin/support/text/patterns/software.rb b/lib/ronin/support/text/patterns/software.rb new file mode 100644 index 000000000..ff4b3ca21 --- /dev/null +++ b/lib/ronin/support/text/patterns/software.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +module Ronin + module Support + module Text + # + # @since 0.3.0 + # + module Patterns + # + # @group Software Version Patterns + # + + # Regular expression for finding version numbers in text. + # + # @since 1.0.0 + VERSION_NUMBER = /\d+\.\d+(?:(?!\.(?:tar|tgz|tbz|zip|rar|txt|htm|xml))[._-][A-Za-z0-9]+)*/ + + # Regular expression for finding version constraints in text. + # + # @since 1.2.0 + VERSION_CONSTRAINT = /(?:>=|>|<=|<|=)\s*#{VERSION_NUMBER}/ + + # Regular expression for finding version ranges in text. + # + # @since 1.2.0 + VERSION_RANGE = /#{VERSION_CONSTRAINT}(?:(?:,\s*|\s+)#{VERSION_CONSTRAINT})?/ + end + end + end +end diff --git a/lib/ronin/support/version.rb b/lib/ronin/support/version.rb index 1e1c66a3d..5936d00bf 100644 --- a/lib/ronin/support/version.rb +++ b/lib/ronin/support/version.rb @@ -19,6 +19,6 @@ module Ronin module Support # ronin-support version - VERSION = '1.1.1' + VERSION = '1.2.0' end end diff --git a/spec/binary/ctypes/array_type_spec.rb b/spec/binary/ctypes/array_type_spec.rb index 0393ee9bb..2a4fdbd8c 100644 --- a/spec/binary/ctypes/array_type_spec.rb +++ b/spec/binary/ctypes/array_type_spec.rb @@ -39,9 +39,9 @@ end end - context "when the given type is an UnboundedArrayType" do + context "when the given type is an FlexibleArrayType" do let(:type) do - Ronin::Support::Binary::CTypes::UnboundedArrayType.new(super()) + Ronin::Support::Binary::CTypes::FlexibleArrayType.new(super()) end it do diff --git a/spec/binary/ctypes/unbounded_array_type_spec.rb b/spec/binary/ctypes/flexible_array_type_spec.rb similarity index 98% rename from spec/binary/ctypes/unbounded_array_type_spec.rb rename to spec/binary/ctypes/flexible_array_type_spec.rb index 2e87ea2b6..9f5a2f176 100644 --- a/spec/binary/ctypes/unbounded_array_type_spec.rb +++ b/spec/binary/ctypes/flexible_array_type_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' -require 'ronin/support/binary/ctypes/unbounded_array_type' +require 'ronin/support/binary/ctypes/flexible_array_type' require 'ronin/support/binary/ctypes/int32_type' require 'ronin/support/binary/ctypes/array_type' require 'ronin/support/binary/ctypes/struct_type' require 'ronin/support/binary/ctypes' -describe Ronin::Support::Binary::CTypes::UnboundedArrayType do +describe Ronin::Support::Binary::CTypes::FlexibleArrayType do let(:endian) { :little } let(:pack_string) { 'L<' } @@ -48,9 +48,9 @@ end end - context "when the given type is an UnboundedArrayType" do + context "when the given type is an FlexibleArrayType" do let(:type) do - Ronin::Support::Binary::CTypes::UnboundedArrayType.new(super()) + Ronin::Support::Binary::CTypes::FlexibleArrayType.new(super()) end it do diff --git a/spec/binary/ctypes/struct_type_spec.rb b/spec/binary/ctypes/struct_type_spec.rb index c74490167..ba799bd6d 100644 --- a/spec/binary/ctypes/struct_type_spec.rb +++ b/spec/binary/ctypes/struct_type_spec.rb @@ -191,7 +191,7 @@ let(:members) do { a: Ronin::Support::Binary::CTypes::INT32, - b: Ronin::Support::Binary::CTypes::UnboundedArrayType.new( + b: Ronin::Support::Binary::CTypes::FlexibleArrayType.new( Ronin::Support::Binary::CTypes::INT32[3] ) } @@ -202,7 +202,7 @@ end end - context "when one of the members is a Ronin::Support::Binary::CTypes::UnboundedArrayType" do + context "when one of the members is a Ronin::Support::Binary::CTypes::FlexibleArrayType" do let(:members) do { a: Ronin::Support::Binary::CTypes::INT16, @@ -211,7 +211,7 @@ } end - it "must omit the UnboundedArrayType member size from #size" do + it "must omit the FlexibleArrayType member size from #size" do expect(subject.size).to eq( members[:a].size + members[:b].size ) @@ -366,7 +366,7 @@ end end - context "when the last value in #members is an UnboundedArrayType" do + context "when the last value in #members is an FlexibleArrayType" do let(:members) do { a: Ronin::Support::Binary::CTypes::CHAR, @@ -539,7 +539,7 @@ end end - context "when the last value in #members is an UnboundedArrayType" do + context "when the last value in #members is an FlexibleArrayType" do let(:members) do { a: Ronin::Support::Binary::CTypes::CHAR, @@ -734,7 +734,7 @@ end end - context "when the last value in #members is an UnboundedArrayType" do + context "when the last value in #members is an FlexibleArrayType" do let(:members) do { a: Ronin::Support::Binary::CTypes::CHAR, @@ -861,7 +861,7 @@ end end - context "when the last value in #members is an UnboundedArrayType" do + context "when the last value in #members is an FlexibleArrayType" do let(:members) do { a: Ronin::Support::Binary::CTypes::CHAR, diff --git a/spec/binary/ctypes/type_examples.rb b/spec/binary/ctypes/type_examples.rb index 529ceadda..ff34c4b3a 100644 --- a/spec/binary/ctypes/type_examples.rb +++ b/spec/binary/ctypes/type_examples.rb @@ -1,6 +1,6 @@ require 'rspec' require 'ronin/support/binary/ctypes/array_type' -require 'ronin/support/binary/ctypes/unbounded_array_type' +require 'ronin/support/binary/ctypes/flexible_array_type' shared_examples_for "Type examples" do describe "#[]" do @@ -21,8 +21,8 @@ end context "when no argument is given" do - it "must return an UnboundedArrayType" do - expect(subject[]).to be_kind_of(Ronin::Support::Binary::CTypes::UnboundedArrayType) + it "must return an FlexibleArrayType" do + expect(subject[]).to be_kind_of(Ronin::Support::Binary::CTypes::FlexibleArrayType) end it "must have a #type of self" do diff --git a/spec/binary/ctypes/type_resolver_spec.rb b/spec/binary/ctypes/type_resolver_spec.rb index 161000fa1..bae9a679f 100644 --- a/spec/binary/ctypes/type_resolver_spec.rb +++ b/spec/binary/ctypes/type_resolver_spec.rb @@ -228,10 +228,10 @@ class TestUnionWithAlignment < Ronin::Support::Binary::Union context "when given a Range" do context "and it starts with a Symbol" do - it "must return an UnboundedArrayType containing the resolved Type" do + it "must return an FlexibleArrayType containing the resolved Type" do type = subject.resolve(type_name..) - expect(type).to be_kind_of(Ronin::Support::Binary::CTypes::UnboundedArrayType) + expect(type).to be_kind_of(Ronin::Support::Binary::CTypes::FlexibleArrayType) expect(type.type).to eq(types[type_name]) end end @@ -243,7 +243,7 @@ class TestUnionWithAlignment < Ronin::Support::Binary::Union it "must return an ArrayObjectType containing the resolved Type and length" do type = subject.resolve([type_name, length]..) - expect(type).to be_kind_of(Ronin::Support::Binary::CTypes::UnboundedArrayType) + expect(type).to be_kind_of(Ronin::Support::Binary::CTypes::FlexibleArrayType) expect(type.type).to be_kind_of(Ronin::Support::Binary::CTypes::ArrayObjectType) expect(type.type.type).to eq(types[type_name]) expect(type.type.length).to eq(length) @@ -257,7 +257,7 @@ class TestUnionWithAlignment < Ronin::Support::Binary::Union it "must return an ArrayObjectType containing an ArrayObjectType and the length" do type = subject.resolve([[type_name, length2], length1]..) - expect(type).to be_kind_of(Ronin::Support::Binary::CTypes::UnboundedArrayType) + expect(type).to be_kind_of(Ronin::Support::Binary::CTypes::FlexibleArrayType) expect(type.type).to be_kind_of(Ronin::Support::Binary::CTypes::ArrayObjectType) expect(type.type.type).to be_kind_of(Ronin::Support::Binary::CTypes::ArrayObjectType) expect(type.type.length).to eq(length1) @@ -273,7 +273,7 @@ class TestUnionWithAlignment < Ronin::Support::Binary::Union it "must return an ArrayObjectType containing a StructObjectType and the length" do type = subject.resolve([struct_class, length]..) - expect(type).to be_kind_of(Ronin::Support::Binary::CTypes::UnboundedArrayType) + expect(type).to be_kind_of(Ronin::Support::Binary::CTypes::FlexibleArrayType) expect(type.type).to be_kind_of(Ronin::Support::Binary::CTypes::ArrayObjectType) expect(type.type.type).to be_kind_of(Ronin::Support::Binary::CTypes::StructObjectType) expect(type.type.length).to eq(length) @@ -300,7 +300,7 @@ class TestUnionWithAlignment < Ronin::Support::Binary::Union it "must return an ArrayObjectType containing a UnionObjectType and the length" do type = subject.resolve([union_class, length]..) - expect(type).to be_kind_of(Ronin::Support::Binary::CTypes::UnboundedArrayType) + expect(type).to be_kind_of(Ronin::Support::Binary::CTypes::FlexibleArrayType) expect(type.type).to be_kind_of(Ronin::Support::Binary::CTypes::ArrayObjectType) expect(type.type.type).to be_kind_of(Ronin::Support::Binary::CTypes::UnionObjectType) expect(type.type.length).to eq(length) @@ -327,7 +327,7 @@ class TestUnionWithAlignment < Ronin::Support::Binary::Union it "must return an ArrayObjectType containing the Type and length" do type = subject.resolve([type_object, length]..) - expect(type).to be_kind_of(Ronin::Support::Binary::CTypes::UnboundedArrayType) + expect(type).to be_kind_of(Ronin::Support::Binary::CTypes::FlexibleArrayType) expect(type.type).to be_kind_of(Ronin::Support::Binary::CTypes::ArrayObjectType) expect(type.type.type).to eq(type_object) expect(type.type.length).to eq(length) diff --git a/spec/binary/ctypes/union_type_spec.rb b/spec/binary/ctypes/union_type_spec.rb index 303d5a692..a6e2b24ad 100644 --- a/spec/binary/ctypes/union_type_spec.rb +++ b/spec/binary/ctypes/union_type_spec.rb @@ -126,7 +126,7 @@ expect(subject.pack_string).to be(nil) end - context "when one of the fields is a Ronin::Support::Binary::CTypes::UnboundedArrayType" do + context "when one of the fields is a Ronin::Support::Binary::CTypes::FlexibleArrayType" do let(:members) do { a: Ronin::Support::Binary::CTypes::CHAR, @@ -135,7 +135,7 @@ } end - it "must omit the UnboundedArrayType member size from #size" do + it "must omit the FlexibleArrayType member size from #size" do expect(subject.size).to eq( [members[:a].size, members[:b].size].max ) @@ -257,7 +257,7 @@ end end - context "and when the member type is an UnboundedArrayType" do + context "and when the member type is an FlexibleArrayType" do let(:members) do { a: Ronin::Support::Binary::CTypes::CHAR, @@ -270,7 +270,7 @@ let(:value) { [*0x00..0x10] } let(:hash) { {key => value} } - it "must pack the value using the member's UnboundedArrayType" do + it "must pack the value using the member's FlexibleArrayType" do expect(subject.pack(hash)).to eq(type.pack(value)) end end @@ -455,7 +455,7 @@ end end - context "when the last value in #members is an UnboundedArrayType" do + context "when the last value in #members is an FlexibleArrayType" do let(:members) do { a: Ronin::Support::Binary::CTypes::CHAR, diff --git a/spec/binary/struct_spec.rb b/spec/binary/struct_spec.rb index 41eb457ea..ebbd063a0 100644 --- a/spec/binary/struct_spec.rb +++ b/spec/binary/struct_spec.rb @@ -24,7 +24,7 @@ class StructWithAnArray < Ronin::Support::Binary::Struct member :baz, :uint64 end - class StructWithUnboundedArray < Ronin::Support::Binary::Struct + class StructWithFlexibleArray < Ronin::Support::Binary::Struct member :foo, :uint16 member :bar, :int32 member :baz, (:uint64..) diff --git a/spec/binary/template_spec.rb b/spec/binary/template_spec.rb index 45f091707..8625b5c55 100644 --- a/spec/binary/template_spec.rb +++ b/spec/binary/template_spec.rb @@ -132,21 +132,21 @@ context "when given an infinite range" do let(:fields) { [type_name..] } - let(:unbounded_array_type) do - Ronin::Support::Binary::CTypes::UnboundedArrayType.new(type) + let(:flexible_array_type) do + Ronin::Support::Binary::CTypes::FlexibleArrayType.new(type) end it "must set #type_system to Ronin::Support::Binary::CTypes" do expect(subject.type_system).to be(Ronin::Support::Binary::CTypes) end - it "must add an Ronin::Support::Binary::CTypes::UnboundedArrayType to #types" do - expect(subject.types.first).to be_kind_of(Ronin::Support::Binary::CTypes::UnboundedArrayType) + it "must add an Ronin::Support::Binary::CTypes::FlexibleArrayType to #types" do + expect(subject.types.first).to be_kind_of(Ronin::Support::Binary::CTypes::FlexibleArrayType) expect(subject.types.first.type).to eq(type) end - it "must set #pack_string to the UnboundedArrayType's #pack_string" do - expect(subject.pack_string).to eq(unbounded_array_type.pack_string) + it "must set #pack_string to the FlexibleArrayType's #pack_string" do + expect(subject.pack_string).to eq(flexible_array_type.pack_string) end end @@ -362,8 +362,8 @@ let(:type_name2) { :uint16 } let(:type2) { Ronin::Support::Binary::CTypes::TYPES[type_name2] } - let(:unbounded_array_type) do - Ronin::Support::Binary::CTypes::UnboundedArrayType.new(type2) + let(:flexible_array_type) do + Ronin::Support::Binary::CTypes::FlexibleArrayType.new(type2) end let(:fields) { [type_name1, type_name2..] } @@ -374,7 +374,7 @@ it "must pack the remainder of the values using the last field's pack string" do expect(subject.pack(*values)).to eq( - "#{type1.pack(value1)}#{unbounded_array_type.pack(value2)}" + "#{type1.pack(value1)}#{flexible_array_type.pack(value2)}" ) end end @@ -387,8 +387,8 @@ let(:array_type) do Ronin::Support::Binary::CTypes::ArrayType.new(type2,array_length) end - let(:unbounded_array_type) do - Ronin::Support::Binary::CTypes::UnboundedArrayType.new(array_type) + let(:flexible_array_type) do + Ronin::Support::Binary::CTypes::FlexibleArrayType.new(array_type) end let(:fields) { [type_name1, [type_name2, array_length]..] } @@ -399,7 +399,7 @@ it "must pack the last value using the last field's type's #pack method" do expect(subject.pack(*values)).to eq( - "#{type1.pack(value1)}#{unbounded_array_type.pack(value2)}" + "#{type1.pack(value1)}#{flexible_array_type.pack(value2)}" ) end end @@ -478,8 +478,8 @@ let(:type_name2) { :uint16 } let(:type2) { Ronin::Support::Binary::CTypes::TYPES[type_name2] } - let(:unbounded_array_type) do - Ronin::Support::Binary::CTypes::UnboundedArrayType.new(type2) + let(:flexible_array_type) do + Ronin::Support::Binary::CTypes::FlexibleArrayType.new(type2) end let(:fields) { [type_name1, type_name2..] } @@ -488,7 +488,7 @@ let(:value2) { [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } let(:data) do - "#{type1.pack(value1)}#{unbounded_array_type.pack(value2)}" + "#{type1.pack(value1)}#{flexible_array_type.pack(value2)}" end it "must unpack the remainder of the values using the last field's pack string" do @@ -508,8 +508,8 @@ let(:array_type) do Ronin::Support::Binary::CTypes::ArrayType.new(type2,array_length) end - let(:unbounded_array_type) do - Ronin::Support::Binary::CTypes::UnboundedArrayType.new(array_type) + let(:flexible_array_type) do + Ronin::Support::Binary::CTypes::FlexibleArrayType.new(array_type) end let(:fields) { [type_name1, [type_name2, array_length]..] } @@ -519,7 +519,7 @@ let(:values) { [value1, value2] } let(:data) do - "#{type1.pack(value1)}#{unbounded_array_type.pack(value2)}" + "#{type1.pack(value1)}#{flexible_array_type.pack(value2)}" end it "must unpack the remainder of the values using the last field's type's #unpack method" do diff --git a/spec/crypto/cipher/des3_spec.rb b/spec/crypto/cipher/des3_spec.rb new file mode 100644 index 000000000..135e79210 --- /dev/null +++ b/spec/crypto/cipher/des3_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' +require 'ronin/support/crypto/cipher/des3' + +describe Ronin::Support::Crypto::Cipher::DES3 do + let(:key) { 'A' * 24 } + + describe "#initialize" do + subject do + described_class.new( + direction: :encrypt, + key: key + ) + end + + it "must default #mode to nil" do + expect(subject.mode).to be(nil) + end + + it "must initialize an DES3 cipher" do + expect(subject.name).to eq("DES-EDE3-CBC") + end + + # NOTE: Ruby 3.0's openssl does not support des3-wrap + if RUBY_VERSION >= '3.1.0' + context "when given the mode: keyword argument" do + let(:mode) { :wrap } + let(:key) { "A" * 24 } + + subject do + described_class.new( + direction: :encrypt, + mode: mode, + key: key + ) + end + + it "must set #mode" do + expect(subject.mode).to eq(mode) + end + + it "must initialize an DES3 cipher with the mode and direction" do + expect(subject.name).to eq("DES3-#{mode.upcase}") + end + end + end + end + + describe ".supported" do + subject { described_class } + + it "must return all ciphers beginning with 'des3'" do + expect(subject.supported).to_not be_empty + expect(subject.supported).to all(be =~ /^des3/) + end + end +end diff --git a/spec/crypto/core_ext/file_spec.rb b/spec/crypto/core_ext/file_spec.rb index fb2bce156..be73478a6 100644 --- a/spec/crypto/core_ext/file_spec.rb +++ b/spec/crypto/core_ext/file_spec.rb @@ -159,6 +159,57 @@ end end + let(:des3_key) { 'A' * 24 } + let(:des3_cipher_text) do + cipher = OpenSSL::Cipher.new('des3') + + cipher.encrypt + cipher.key = des3_key + + cipher.update(clear_text) + cipher.final + end + + describe ".des3_encrypt" do + it "must encrypt a given String using DES3" do + expect(subject.des3_encrypt(path, key: des3_key)).to eq(des3_cipher_text) + end + + context "when given a block" do + it "must yield each AES encrypted block" do + output = String.new + + subject.des3_encrypt(path, key: des3_key) do |block| + output << block + end + + expect(output).to eq(des3_cipher_text) + end + end + end + + describe ".des3_decrypt" do + let(:tempfile) { Tempfile.new('ronin-support') } + let(:path) { tempfile.path } + + before { File.write(path,des3_cipher_text) } + + it "must decrypt the given String" do + expect(subject.des3_decrypt(path, key: des3_key)).to eq(clear_text) + end + + context "when given a block" do + it "must yield each AES decrypted block" do + output = String.new + + subject.des3_decrypt(path, key: des3_key) do |block| + output << block + end + + expect(output).to eq(clear_text) + end + end + end + let(:aes_cipher_text) do cipher = OpenSSL::Cipher.new('aes-256-cbc') diff --git a/spec/crypto/core_ext/string_spec.rb b/spec/crypto/core_ext/string_spec.rb index 9d279d401..e5a7d98b3 100644 --- a/spec/crypto/core_ext/string_spec.rb +++ b/spec/crypto/core_ext/string_spec.rb @@ -122,6 +122,28 @@ end end + let(:des3_key) { 'A' * 24 } + let(:des3_cipher_text) do + cipher = OpenSSL::Cipher.new('des3') + + cipher.encrypt + cipher.key = des3_key + + cipher.update(subject) + cipher.final + end + + describe "#des3_encrypt" do + it "must encrypt a given String using DES3" do + expect(subject.des3_encrypt(key: des3_key)).to eq(des3_cipher_text) + end + end + + describe "#des3_decrypt" do + it "must decrypt the given String" do + expect(des3_cipher_text.des3_decrypt(key: des3_key)).to eq(subject) + end + end + let(:aes_cipher_text) do cipher = OpenSSL::Cipher.new('aes-256-cbc') diff --git a/spec/crypto/mixin_spec.rb b/spec/crypto/mixin_spec.rb index 9717d1d74..b71763bcd 100644 --- a/spec/crypto/mixin_spec.rb +++ b/spec/crypto/mixin_spec.rb @@ -128,6 +128,60 @@ end end + describe "#crypto_des3_cipher" do + let(:key) { 'A' * 24 } + let(:direction) { :decrypt } + + it "must return a Ronin::Support::Crypto::Cipher::DES3 object" do + new_cipher = subject.crypto_des3_cipher(direction: direction, key: key) + + expect(new_cipher).to be_kind_of(Ronin::Support::Crypto::Cipher::DES3) + end + + it "must default to cipher 'DES-EDE3-CBC'" do + new_cipher = subject.crypto_des3_cipher(direction: direction, key: key) + + expect(new_cipher.name).to eq("DES-EDE3-CBC") + end + + # NOTE: Ruby 3.0's openssl does not support des3-wrap + if RUBY_VERSION >= '3.1.0' + context "when the mode: keyword argument is given" do + let(:mode) { :wrap } + + it "must use the given mode" do + new_cipher = subject.crypto_des3_cipher(mode: mode, + direction: direction, + key: key) + + expect(new_cipher.name).to eq("DES3-#{mode.upcase}") + end + end + end + end + + let(:des3_key) { 'A' * 24 } + let(:des3_cipher_text) do + cipher = OpenSSL::Cipher.new('des3') + + cipher.encrypt + cipher.key = des3_key + + cipher.update(clear_text) + cipher.final + end + + describe "#crypto_des3_encrypt" do + it "must encrypt a given String using DES3" do + expect(subject.crypto_des3_encrypt(clear_text, key: des3_key)).to eq(des3_cipher_text) + end + end + + describe "#crypto_des3_decrypt" do + it "must decrypt the given String" do + expect(subject.crypto_des3_decrypt(des3_cipher_text, key: des3_key)).to eq(clear_text) + end + end + describe "#crypto_aes_cipher" do let(:key_size) { 256 } let(:hash) { :sha256 } diff --git a/spec/crypto_spec.rb b/spec/crypto_spec.rb index d9236dec0..2e63f1d4b 100644 --- a/spec/crypto_spec.rb +++ b/spec/crypto_spec.rb @@ -137,6 +137,60 @@ cipher.update(clear_text) + cipher.final end + describe ".des3_cipher" do + let(:key) { 'A' * 24 } + let(:direction) { :decrypt } + + it "must return a Ronin::Support::Crypto::Cipher::DES3 object" do + new_cipher = subject.des3_cipher(direction: direction, key: key) + + expect(new_cipher).to be_kind_of(Ronin::Support::Crypto::Cipher::DES3) + end + + it "must default to cipher 'DES-EDE3-CBC'" do + new_cipher = subject.des3_cipher(direction: direction, key: key) + + expect(new_cipher.name).to eq("DES-EDE3-CBC") + end + + # NOTE: Ruby 3.0's openssl does not support des3-wrap + if RUBY_VERSION >= '3.1.0' + context "when the mode: keyword argument is given" do + let(:mode) { :wrap } + + it "must use the given mode" do + new_cipher = subject.des3_cipher(mode: mode, + direction: direction, + key: key) + + expect(new_cipher.name).to eq("DES3-#{mode.upcase}") + end + end + end + end + + let(:des3_key) { 'A' * 24 } + let(:des3_cipher_text) do + cipher = OpenSSL::Cipher.new('des3') + + cipher.encrypt + cipher.key = des3_key + + cipher.update(clear_text) + cipher.final + end + + describe ".des3_encrypt" do + it "must encrypt a given String using DES3" do + expect(subject.des3_encrypt(clear_text, key: des3_key)).to eq(des3_cipher_text) + end + end + + describe ".des3_decrypt" do + it "must decrypt the given String" do + expect(subject.des3_decrypt(des3_cipher_text, key: des3_key)).to eq(clear_text) + end + end + describe ".aes_cipher" do let(:key_size) { 256 } let(:hash) { :sha256 } diff --git a/spec/encoding/base64/core_ext/string_spec.rb b/spec/encoding/base64/core_ext/string_spec.rb index 839098b3c..95a7d997e 100644 --- a/spec/encoding/base64/core_ext/string_spec.rb +++ b/spec/encoding/base64/core_ext/string_spec.rb @@ -13,7 +13,7 @@ subject { "hello" } it "must Base64 encode the String" do - expect(subject.base64_encode).to eq(Base64.encode64(subject)) + expect(subject.base64_encode).to eq(Ronin::Support::Encoding::Base64.encode64(subject)) end context "when given the mode: keyword of :strict" do @@ -21,7 +21,7 @@ it "must strict encode the String" do expect(subject.base64_encode(mode: :strict)).to eq( - Base64.strict_encode64(subject) + Ronin::Support::Encoding::Base64.strict_encode64(subject) ) end end @@ -31,7 +31,7 @@ it "must URL-safe encode the String" do expect(subject.base64_encode(mode: :url_safe)).to eq( - Base64.strict_encode64(subject) + Ronin::Support::Encoding::Base64.strict_encode64(subject) ) end end @@ -49,7 +49,7 @@ describe "#base64_decode" do let(:data) { "hello" } - let(:subject) { Base64.encode64(data) } + let(:subject) { Ronin::Support::Encoding::Base64.encode64(data) } it "must Base64 decode the given data" do expect(subject.base64_decode).to eq(data) @@ -57,7 +57,7 @@ context "when given the mode: keyword of :strict" do let(:data) { 'A' * 256 } - let(:subject) { Base64.strict_encode64(data) } + let(:subject) { Ronin::Support::Encoding::Base64.strict_encode64(data) } it "must strict decode the given data" do expect(subject.base64_decode(mode: :strict)).to eq(data) @@ -66,7 +66,7 @@ context "when given the mode: keyword of :url_safe" do let(:data) { 'A' * 256 } - let(:subject) { Base64.urlsafe_encode64(data) } + let(:subject) { Ronin::Support::Encoding::Base64.urlsafe_encode64(data) } it "must URL-safe decode the given data" do expect(subject.base64_decode(mode: :url_safe)).to eq(data) diff --git a/spec/encoding/base64_spec.rb b/spec/encoding/base64_spec.rb index b933cab5a..92918f01e 100644 --- a/spec/encoding/base64_spec.rb +++ b/spec/encoding/base64_spec.rb @@ -6,7 +6,7 @@ let(:data) { "hello" } it "must Base64 encode the given data" do - expect(subject.encode(data)).to eq(Base64.encode64(data)) + expect(subject.encode(data)).to eq(described_class.encode64(data)) end context "when given the mode: keyword of :strict" do @@ -14,7 +14,7 @@ it "must strict encode the given data" do expect(subject.encode(data, mode: :strict)).to eq( - Base64.strict_encode64(data) + described_class.strict_encode64(data) ) end end @@ -24,7 +24,7 @@ it "must URL-safe encode the given data" do expect(subject.encode(data, mode: :url_safe)).to eq( - Base64.strict_encode64(data) + described_class.strict_encode64(data) ) end end @@ -42,7 +42,7 @@ describe ".decode" do let(:data) { "hello" } - let(:encoded_data) { Base64.encode64(data) } + let(:encoded_data) { described_class.encode64(data) } it "must Base64 decode the given data" do expect(subject.decode(encoded_data)).to eq(data) @@ -50,7 +50,7 @@ context "when given the mode: keyword of :strict" do let(:data) { 'A' * 256 } - let(:encoded_data) { Base64.strict_encode64(data) } + let(:encoded_data) { described_class.strict_encode64(data) } it "must strict decode the given data" do expect(subject.decode(encoded_data, mode: :strict)).to eq(data) @@ -59,7 +59,7 @@ context "when given the mode: keyword of :url_safe" do let(:data) { 'A' * 256 } - let(:encoded_data) { Base64.urlsafe_encode64(data) } + let(:encoded_data) { described_class.urlsafe_encode64(data) } it "must URL-safe decode the given data" do expect(subject.decode(encoded_data, mode: :url_safe)).to eq(data) @@ -76,4 +76,58 @@ end end end + + describe "#encode64" do + let(:data) { "AAAA" } + let(:encoded_data) { "QUFBQQ==\n" } + + it "must encode the given data" do + expect(subject.encode64(data)).to eq(encoded_data) + end + end + + describe "#strict_encode64" do + let(:data) { "AAAA" } + let(:encoded_data) { "QUFBQQ==" } + + it "must strict encode the given data" do + expect(subject.strict_encode64(data)).to eq(encoded_data) + end + end + + describe "#urlsafe_encode64" do + let(:data) { "AAAA" } + let(:encoded_data) { "QUFBQQ==" } + + it "must URL-safe encode the given data" do + expect(subject.urlsafe_encode64(data)).to eq(encoded_data) + end + end + + describe "#decode64" do + let(:data) { "QUFBQQ==\n" } + let(:decoded_data) { "AAAA" } + + it "must decode the given data" do + expect(subject.decode64(data)).to eq(decoded_data) + end + end + + describe "#strict_decode64" do + let(:data) { "QUFBQQ==" } + let(:decoded_data) { "AAAA" } + + it "must strict decode the given data" do + expect(subject.strict_decode64(data)).to eq(decoded_data) + end + end + + describe "#urlsafe_decode64" do + let(:data) { "QUFBQQ==" } + let(:decoded_data) { "AAAA" } + + it "must URL-safe decode the given data" do + expect(subject.urlsafe_decode64(data)).to eq(decoded_data) + end + end end diff --git a/spec/encoding/java/core_ext/integer_spec.rb b/spec/encoding/java/core_ext/integer_spec.rb new file mode 100644 index 000000000..c1ce46020 --- /dev/null +++ b/spec/encoding/java/core_ext/integer_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper' +require 'ronin/support/encoding/java/core_ext/integer' + +describe Integer do + subject { 0x26 } + + it { expect(subject).to respond_to(:java_escape) } + it { expect(subject).to respond_to(:java_encode) } + + describe "#java_escape" do + { + 0x00 => '\0', + 0x08 => '\b', + 0x09 => '\t', + 0x0a => '\n', + 0x0b => '\v', + 0x0c => '\f', + 0x0d => '\r', + 0x22 => '\"', + 0x27 => '\\\'', + 0x5c => '\\\\' + }.each do |byte,escaped_char| + context "when called on #{byte}" do + subject { byte } + + it "must return #{escaped_char.inspect}" do + expect(subject.java_escape).to eq(escaped_char) + end + end + end + + context "when called on an Integer between 0x20 and 0x7e" do + subject { 0x41 } + + it "must return the ASCII character for the byte" do + expect(subject.java_escape).to eq(subject.chr) + end + end + + context "when called on an Integer that does not map to an ASCII char" do + subject { 0xFF } + + it "must return the uppercase '\\u00XX' hex escaped String" do + expect(subject.java_escape).to eq('\u00FF') + end + end + + context "when called on an Integer between 0x100 and 0xffff" do + subject { 0xFFFF } + + it "must return the uppercase '\\uXXXX' hex escaped String" do + expect(subject.java_escape).to eq('\uFFFF') + end + end + + context "when called on an Integer between 0x10000 and 0x10ffff" do + subject { 0x10000 } + + it do + expect { + subject.java_escape + }.to raise_error(RangeError,"#{subject.inspect} out of char range") + end + end + + context "when called on a negative Integer" do + subject { -1 } + + it do + expect { + subject.java_escape + }.to raise_error(RangeError,"#{subject.inspect} out of char range") + end + end + end + + describe "#java_encode" do + context "when called on an Integer that does not map to an ASCII char" do + subject { 0xFF } + + it "must return the uppercase '\\u00XX' hex escaped String" do + expect(subject.java_encode).to eq('\u00FF') + end + end + + context "when called on an Integer between 0x100 and 0x10ffff" do + subject { 0xFFFF } + + it "must return the uppercase '\\uXXXX' hex escaped String" do + expect(subject.java_encode).to eq('\uFFFF') + end + end + + context "when called on an Integer between 0x10000 and 0x10ffff" do + subject { 0x10000 } + + it do + expect { + subject.java_encode + }.to raise_error(RangeError,"#{subject.inspect} out of char range") + end + end + + context "when called on a negative Integer" do + subject { -1 } + + it do + expect { + subject.java_encode + }.to raise_error(RangeError,"#{subject.inspect} out of char range") + end + end + end +end diff --git a/spec/encoding/java/core_ext/string_spec.rb b/spec/encoding/java/core_ext/string_spec.rb new file mode 100644 index 000000000..632fc6641 --- /dev/null +++ b/spec/encoding/java/core_ext/string_spec.rb @@ -0,0 +1,207 @@ +require 'spec_helper' +require 'ronin/support/encoding/java/core_ext/string' + +describe String do + subject { "hello world" } + + it { expect(subject).to respond_to(:java_escape) } + it { expect(subject).to respond_to(:java_unescape) } + it { expect(subject).to respond_to(:java_encode) } + it { expect(subject).to respond_to(:java_decode) } + it { expect(subject).to respond_to(:java_string) } + it { expect(subject).to respond_to(:java_unquote) } + + describe ".java_escape" do + context "when the given String does not contain special characters" do + subject { "abc" } + + it "must return the given String" do + expect(subject.java_escape).to eq(subject) + end + end + + context "when the given String contains back-slashed escaped characters" do + subject { "\0\b\t\n\v\f\r\\\"" } + + let(:escaped_string) { "\\0\\b\\t\\n\\v\\f\\r\\\\\\\"" } + + it "must escape the special characters with an extra back-slash" do + expect(subject.java_escape).to eq(escaped_string) + end + end + + context "when the given String contains non-printable characters" do + subject { "hello\xffworld".force_encoding(Encoding::ASCII_8BIT) } + + let(:escaped_string) { "hello\\u00FFworld" } + + it "must escape non-printable characters" do + expect(subject.java_escape).to eq(escaped_string) + end + end + + context "when the given String contains unicode characters" do + subject { "hello\u1001world" } + + let(:escaped_string) { "hello\\u1001world" } + + it "must escape the unicode characters as '\\uXXXX'" do + expect(subject.java_escape).to eq(escaped_string) + end + end + + context "when the String contains invalid byte sequences" do + subject { "hello\xfe\xff" } + + let(:escaped_string) { "hello\\u00FE\\u00FF" } + + it "must escape each byte in the String" do + expect(subject.java_escape).to eq(escaped_string) + end + end + end + + describe ".java_unescape" do + context "when the given String contains escaped hexadecimal characters" do + subject { "\\u0068\\u0065\\u006C\\u006C\\u006F\\u0020\\u0077\\u006F\\u0072\\u006C\\u0064" } + + let(:unescaped) { "hello world" } + + it "must unescape the hexadecimal characters" do + expect(subject.java_unescape).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.java_unescape.encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains escaped unicode characters" do + subject { "\\u00D8" } + + let(:unescaped) { "Ø" } + + it "must unescape the '\\uXXXX' unicode characters" do + expect(subject.java_unescape).to eq(unescaped) + end + end + + context "when the given String contains single character escaped octal characters" do + subject { "\\0\\1\\2\\3\\4\\5\\6\\7" } + + let(:unescaped) { "\0\1\2\3\4\5\6\7" } + + it "must unescape the octal characters" do + expect(subject.java_unescape).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.java_unescape.encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains two character escaped octal characters" do + subject { "\\10\\11\\12\\13\\14\\15\\16\\17\\20" } + + let(:unescaped) { "\10\11\12\13\14\15\16\17\20" } + + it "must unescape the octal characters" do + expect(subject.java_unescape).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.java_unescape.encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains three character escaped octal characters" do + subject { "\\150\\145\\154\\154\\157\\040\\167\\157\\162\\154\\144" } + + let(:unescaped) { "hello world" } + + it "must unescape the octal characters" do + expect(subject.java_unescape).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.java_unescape.encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains escaped special characters" do + subject { "hello\\0world\\n" } + + let(:unescaped) { "hello\0world\n" } + + it "must unescape Python special characters" do + expect(subject.java_unescape).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.java_unescape.encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String does not contain escaped characters" do + subject { "hello world" } + + it "must return the given String" do + expect(subject.java_unescape).to eq(subject) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.java_unescape.encoding).to be(Encoding::UTF_8) + end + end + end + + describe ".java_encode" do + subject { "ABC" } + + let(:encoded) { '\u0041\u0042\u0043' } + + it "must Python encode each character in the String" do + expect(subject.java_encode).to eq(encoded) + end + + context "when the String contains invalid byte sequences" do + subject { "ABC\xfe\xff" } + + let(:encoded) { '\u0041\u0042\u0043\u00FE\u00FF' } + + it "must encode each byte in the String" do + expect(subject.java_encode).to eq(encoded) + end + end + end + + describe ".java_string" do + subject { "hello\nworld" } + + let(:quoted) { '"hello\nworld"' } + + it "must return a double quoted Python String" do + expect(subject.java_string).to eq(quoted) + end + end + + describe ".java_unquote" do + context "when the given String is double-quoted" do + subject { "\"hello\\nworld\"" } + + let(:unescaped) { "hello\nworld" } + + it "must remove double-quotes and unescape the Python String" do + expect(subject.java_unquote).to eq(unescaped) + end + end + + context "when the given String is not quoted" do + subject { "hello world" } + + it "must return the same String" do + expect(subject.java_unquote).to be(subject) + end + end + end +end diff --git a/spec/encoding/java_spec.rb b/spec/encoding/java_spec.rb new file mode 100644 index 000000000..0c9cffc61 --- /dev/null +++ b/spec/encoding/java_spec.rb @@ -0,0 +1,301 @@ +require 'spec_helper' +require 'ronin/support/encoding/java' + +describe Ronin::Support::Encoding::Java do + let(:data) { "hello world" } + + describe ".escape_byte" do + { + 0x00 => '\0', + 0x08 => '\b', + 0x09 => '\t', + 0x0a => '\n', + 0x0b => '\v', + 0x0c => '\f', + 0x0d => '\r', + 0x22 => '\"', + 0x27 => '\\\'', + 0x5c => '\\\\' + }.each do |byte,escaped_char| + context "when called on #{byte}" do + let(:byte) { byte } + + it "must return #{escaped_char.inspect}" do + expect(subject.escape_byte(byte)).to eq(escaped_char) + end + end + end + + context "when called on an Integer between 0x20 and 0x7e" do + let(:byte) { 0x41 } + + it "must return the ASCII character for the byte" do + expect(subject.escape_byte(byte)).to eq(byte.chr) + end + end + + context "when called on an Integer that does not map to an ASCII char" do + let(:byte) { 0xFF } + + it "must return the uppercase '\\u00XX' hex escaped String" do + expect(subject.escape_byte(byte)).to eq('\u00FF') + end + end + + context "when called on an Integer between 0x100 and 0xffff" do + let(:byte) { 0xFFFF } + + it "must return the uppercase '\\uXXXX' hex escaped String" do + expect(subject.escape_byte(byte)).to eq('\uFFFF') + end + end + + context "when called on an Integer greater than 0xffff" do + let(:byte) { 0x10000 } + + it do + expect { + subject.escape_byte(byte) + }.to raise_error(RangeError,"#{byte.inspect} out of char range") + end + end + + context "when called on a negative Integer" do + let(:byte) { -1 } + + it do + expect { + subject.escape_byte(byte) + }.to raise_error(RangeError,"#{byte.inspect} out of char range") + end + end + end + + describe ".encode_byte" do + context "when called on an Integer that does not map to an ASCII char" do + let(:byte) { 0xFF } + + it "must return the uppercase '\\u00XX' hex escaped String" do + expect(subject.encode_byte(byte)).to eq('\u00FF') + end + end + + context "when called on an Integer between 0x100 and 0xffff" do + let(:byte) { 0xFFFF } + + it "must return the uppercase '\\uXXXX' hex escaped String" do + expect(subject.encode_byte(byte)).to eq('\uFFFF') + end + end + + context "when called on an Integer greater than 0xffff" do + let(:byte) { 0x10000 } + + it do + expect { + subject.encode_byte(byte) + }.to raise_error(RangeError,"#{byte.inspect} out of char range") + end + end + + context "when called on a negative Integer" do + let(:byte) { -1 } + + it do + expect { + subject.encode_byte(byte) + }.to raise_error(RangeError,"#{byte.inspect} out of char range") + end + end + end + + describe ".escape" do + context "when the given String does not contain special characters" do + let(:data) { "abc" } + + it "must return the given String" do + expect(subject.escape(data)).to eq(data) + end + end + + context "when the given String contains back-slashed escaped characters" do + let(:data) { "\0\b\t\n\v\f\r\\\"'" } + let(:escaped_string) { "\\0\\b\\t\\n\\v\\f\\r\\\\\\\"\\'" } + + it "must escape the special characters with an extra back-slash" do + expect(subject.escape(data)).to eq(escaped_string) + end + end + + context "when the given String contains non-printable characters" do + let(:data) do + "hello\xffworld".force_encoding(Encoding::ASCII_8BIT) + end + let(:escaped_string) { "hello\\u00FFworld" } + + it "must escape non-printable characters with an extra back-slash" do + expect(subject.escape(data)).to eq(escaped_string) + end + end + + context "when the given String contains unicode characters" do + let(:data) { "hello\u1001world" } + let(:escaped_string) { "hello\\u1001world" } + + it "must escape the unicode characters as \\uXXXX" do + expect(subject.escape(data)).to eq(escaped_string) + end + end + + context "when the String contains invalid byte sequences" do + let(:data) { "hello\xfe\xff" } + let(:escaped_string) { "hello\\u00FE\\u00FF" } + + it "must escape each byte in the String" do + expect(subject.escape(data)).to eq(escaped_string) + end + end + end + + describe ".unescape" do + context "when the given String contains escaped hexadecimal characters" do + let(:data) do + "\\u0068\\u0065\\u006c\\u006c\\u006f\\u0020\\u0077\\u006f\\u0072\\u006c\\u0064" + end + let(:unescaped) { "hello world" } + + it "must unescape the hexadecimal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains escaped unicode characters" do + let(:data) { "\\u00D8" } + let(:unescaped) { "Ø" } + + it "must unescape the '\\uXXXX' unicode characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains single character escaped octal characters" do + let(:data) { "\\0\\1\\2\\3\\4\\5\\6\\7" } + let(:unescaped) { "\0\1\2\3\4\5\6\7" } + + it "must unescape the octal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains two character escaped octal characters" do + let(:data) { "\\10\\11\\12\\13\\14\\15\\16\\17\\20" } + let(:unescaped) { "\10\11\12\13\14\15\16\17\20" } + + it "must unescape the octal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains three character escaped octal characters" do + let(:data) do + "\\150\\145\\154\\154\\157\\040\\167\\157\\162\\154\\144" + end + let(:unescaped) { "hello world" } + + it "must unescape the octal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains escaped special characters" do + let(:data) { "hello\\0world\\n" } + let(:unescaped) { "hello\0world\n" } + + it "must unescape Ruby special characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String does not contain escaped characters" do + let(:data) { "hello world" } + + it "must return the given String" do + expect(subject.unescape(data)).to eq(data) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + end + + describe ".encode" do + let(:data) { "ABC" } + let(:encoded) { '\u0041\u0042\u0043' } + + it "must Ruby encode each character in the String" do + expect(subject.encode(data)).to eq(encoded) + end + + context "when the String contains invalid byte sequences" do + let(:data) { "ABC\xfe\xff" } + let(:encoded) { '\u0041\u0042\u0043\u00FE\u00FF' } + + it "must encode each byte in the String" do + expect(subject.encode(data)).to eq(encoded) + end + end + end + + describe ".quote" do + let(:data) { "hello\nworld" } + let(:quoted) { '"hello\nworld"' } + + it "must return a double quoted Ruby String" do + expect(subject.quote(data)).to eq(quoted) + end + end + + describe ".unquote" do + context "when the given String is double-quoted" do + let(:data) { "\"hello\\nworld\"" } + let(:unescaped) { "hello\nworld" } + + it "must remove double-quotes and unescape the Ruby String" do + expect(subject.unquote(data)).to eq(unescaped) + end + end + + context "when the given String is not quoted" do + let(:data) { "hello world" } + + it "must return the same String" do + expect(subject.unquote(data)).to be(data) + end + end + end +end diff --git a/spec/encoding/node_js/core_ext/integer_spec.rb b/spec/encoding/node_js/core_ext/integer_spec.rb new file mode 100644 index 000000000..9d62a4429 --- /dev/null +++ b/spec/encoding/node_js/core_ext/integer_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' +require 'ronin/support/encoding/node_js/core_ext/integer' + +describe Integer do + subject { 0x26 } + + it { expect(subject).to respond_to(:node_js_escape) } + it { expect(subject).to respond_to(:node_js_encode) } + + describe "#node_js_escape" do + context "when given a byte that maps to a special character" do + subject { 0x0a } + + let(:escaped_special_byte) { '\n' } + + it "must escape special Node.js characters" do + expect(subject.node_js_escape).to eq(escaped_special_byte) + end + end + + context "when given a byte that maps to a printable ASCII character" do + subject { 0x41 } + + let(:normal_char) { 'A' } + + it "must ignore normal characters" do + expect(subject.node_js_escape).to eq(normal_char) + end + end + + context "when called on an Integer that does not map to an ASCII char" do + subject { 0xFF } + + let(:escaped_byte) { '\xFF' } + + it "must escape special Node.js characters" do + expect(subject.node_js_escape).to eq(escaped_byte) + end + end + + context "when called on an Integer between 0x100 and 0xffff" do + subject { 0xFFFF } + + let(:escaped_byte) { '\uFFFF' } + + it "must return the lowercase '\\uXXXX' escaped Node.js character" do + expect(subject.node_js_escape).to eq(escaped_byte) + end + end + end + + describe "#node_js_encode" do + context "when given a ASCII byte" do + let(:node_js_escaped) { '\x26' } + + it "must Node.js format ascii bytes" do + expect(subject.node_js_encode).to eq(node_js_escaped) + end + end + + context "when given a unicode byte" do + subject { 0xd556 } + + let(:escaped_unicode_byte) { '\uD556' } + + it "must Node.js format unicode bytes" do + expect(subject.node_js_encode).to eq('\uD556') + end + end + end +end diff --git a/spec/encoding/node_js/core_ext/string_spec.rb b/spec/encoding/node_js/core_ext/string_spec.rb new file mode 100644 index 000000000..ad48ade3c --- /dev/null +++ b/spec/encoding/node_js/core_ext/string_spec.rb @@ -0,0 +1,117 @@ +require 'spec_helper' +require 'ronin/support/encoding/node_js/core_ext/string' + +describe String do + subject { "one & two" } + + it { expect(subject).to respond_to(:node_js_escape) } + it { expect(subject).to respond_to(:node_js_unescape) } + it { expect(subject).to respond_to(:node_js_encode) } + it { expect(subject).to respond_to(:node_js_string) } + it { expect(subject).to respond_to(:node_js_unquote) } + + describe "#node_js_escape" do + let(:special_chars) { "\t\n\r" } + let(:escaped_special_chars) { '\t\n\r' } + + let(:normal_chars) { "abc" } + + it "must escape special Node.js characters" do + expect(special_chars.node_js_escape).to eq(escaped_special_chars) + end + + it "must ignore normal characters" do + expect(normal_chars.node_js_escape).to eq(normal_chars) + end + + context "when the String contains invalid byte sequences" do + let(:invalid_string) { "hello\xfe\xff" } + let(:escaped_string) { "hello\\xFE\\xFF" } + + it "must Node.js escape each byte in the String" do + expect(invalid_string.node_js_escape).to eq(escaped_string) + end + end + end + + describe "#node_js_unescape" do + let(:node_js_unicode) do + "%u006F%u006E%u0065%u0020%u0026%u0020%u0074%u0077%u006F" + end + let(:node_js_hex) { "%6F%6E%65%20%26%20%74%77%6F" } + let(:node_js_mixed) { "%u6F%u6E%u65 %26 two" } + + it "must unescape Node.js unicode characters" do + expect(node_js_unicode.node_js_unescape).to eq(subject) + end + + it "must unescape Node.js hex characters" do + expect(node_js_hex.node_js_unescape).to eq(subject) + end + + it "must unescape backslash-escaped characters" do + expect("\\b\\t\\n\\f\\r\\\"\\'\\\\".node_js_unescape).to eq("\b\t\n\f\r\"'\\") + end + + it "must ignore non-escaped characters" do + expect(node_js_mixed.node_js_unescape).to eq(subject) + end + end + + describe "#node_js_encode" do + let(:node_js_encoded) { '\x6F\x6E\x65\x20\x26\x20\x74\x77\x6F' } + + it "must Node.js escape all characters" do + expect(subject.node_js_encode).to eq(node_js_encoded) + end + + context "when the String contains invalid byte sequences" do + let(:invalid_string) { "hello\xfe\xff" } + let(:encoded_string) { '\x68\x65\x6C\x6C\x6F\xFE\xFF' } + + it "must Node.js encode each byte in the String" do + expect(invalid_string.node_js_encode).to eq(encoded_string) + end + end + end + + describe "#node_js_string" do + subject { "hello\nworld" } + + let(:node_js_string) { "\"hello\\nworld\"" } + + it "must return a double quoted Node.js string" do + expect(subject.node_js_string).to eq(node_js_string) + end + end + + describe "#node_js_unquote" do + context "when the String is double-quoted" do + subject { "\"hello\\nworld\"" } + + let(:unescaped) { "hello\nworld" } + + it "must remove double-quotes and unescape the Node.js string" do + expect(subject.node_js_unquote).to eq(unescaped) + end + end + + context "when the String is single-quoted" do + subject { "'hello\\'world'" } + + let(:unescaped) { "hello'world" } + + it "must remove the single-quotes and unescape the Node.js string" do + expect(subject.node_js_unquote).to eq(unescaped) + end + end + + context "when the String is not quoted" do + subject { "hello world" } + + it "must return the same String" do + expect(subject.node_js_unquote).to be(subject) + end + end + end +end diff --git a/spec/encoding/node_js_spec.rb b/spec/encoding/node_js_spec.rb new file mode 100644 index 000000000..f14dcf2de --- /dev/null +++ b/spec/encoding/node_js_spec.rb @@ -0,0 +1,178 @@ +require 'spec_helper' +require 'ronin/support/encoding/node_js' + +require 'json' + +describe Ronin::Support::Encoding::NodeJS do + describe ".escape_byte" do + context "when given a byte that maps to a special character" do + let(:special_byte) { 0x0a } + let(:escaped_special_byte) { '\n' } + + it "must escape special Node.js characters" do + expect(subject.escape_byte(special_byte)).to eq(escaped_special_byte) + end + end + + context "when called on an Integer between 0x20 and 0x7e" do + let(:normal_byte) { 0x41 } + let(:normal_char) { 'A' } + + it "must ignore normal characters" do + expect(subject.escape_byte(normal_byte)).to eq(normal_char) + end + end + + context "when called on an Integer that does not map to an ASCII char" do + let(:byte) { 0xFF } + let(:escaped_byte) { '\xFF' } + + it "must escape special Node.js characters" do + expect(subject.escape_byte(byte)).to eq(escaped_byte) + end + end + + context "when called on an Integer between 0x100 and 0xffff" do + let(:byte) { 0xFFFF } + let(:escaped_byte) { '\uFFFF' } + + it "must return the lowercase '\\uXXXX' escaped Node.js character" do + expect(subject.escape_byte(byte)).to eq(escaped_byte) + end + end + end + + describe ".encode_byte" do + context "when given a ASCII byte" do + let(:byte) { 0x26 } + let(:js_escaped) { '\x26' } + + it "must Node.js format ascii bytes" do + expect(subject.encode_byte(byte)).to eq(js_escaped) + end + end + + context "when given a unicode byte" do + let(:byte) { 0xd556 } + let(:escaped_unicode_byte) { '\uD556' } + + it "must Node.js format unicode bytes" do + expect(byte.js_encode).to eq(escaped_unicode_byte) + end + end + end + + describe ".escape" do + let(:special_chars) { "\t\n\r" } + let(:escaped_special_chars) { '\t\n\r' } + + let(:normal_chars) { "abc" } + + it "must escape special Node.js characters" do + expect(subject.escape(special_chars)).to eq(escaped_special_chars) + end + + it "must ignore normal characters" do + expect(subject.escape(normal_chars)).to eq(normal_chars) + end + + context "when the String contains invalid byte sequences" do + let(:invalid_string) { "hello\xfe\xff" } + let(:escaped_string) { "hello\\xFE\\xFF" } + + it "must Node.js escape each byte in the String" do + expect(subject.escape(invalid_string)).to eq(escaped_string) + end + end + end + + let(:data) { "one & two" } + + describe ".unescape" do + let(:js_unicode) do + "%u006F%u006E%u0065%u0020%u0026%u0020%u0074%u0077%u006F" + end + let(:js_hex) { "%6F%6E%65%20%26%20%74%77%6F" } + let(:js_mixed) { "%u6F%u6E%u65 %26 two" } + + it "must unescape Node.js unicode characters" do + expect(subject.unescape(js_unicode)).to eq(data) + end + + it "must unescape Unicode surrogate pair characters" do + expect(subject.unescape("\\uD83D\\uDE80")).to eq( + JSON.parse("\"\\uD83D\\uDE80\"") + ) + end + + it "must unescape Node.js hex characters" do + expect(subject.unescape(js_hex)).to eq(data) + end + + it "must unescape backslash-escaped characters" do + expect(subject.unescape("\\b\\t\\n\\f\\r\\\"\\'\\\\")).to eq("\b\t\n\f\r\"'\\") + end + + it "must ignore non-escaped characters" do + expect(subject.unescape(js_mixed)).to eq(data) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + + describe ".encode" do + let(:js_encoded) { '\x6F\x6E\x65\x20\x26\x20\x74\x77\x6F' } + + it "must Node.js escape all characters" do + expect(subject.encode(data)).to eq(js_encoded) + end + + context "when the String contains invalid byte sequences" do + let(:invalid_string) { "hello\xfe\xff" } + let(:encoded_string) { '\x68\x65\x6C\x6C\x6F\xFE\xFF' } + + it "must Node.js encode each byte in the String" do + expect(subject.encode(invalid_string)).to eq(encoded_string) + end + end + end + + describe ".quote" do + let(:data) { "hello\nworld" } + let(:js_string) { "\"hello\\nworld\"" } + + it "must return a double quoted Node.js string" do + expect(subject.quote(data)).to eq(js_string) + end + end + + describe ".unquote" do + context "when the String is double-quoted" do + let(:data) { "\"hello\\nworld\"" } + let(:unescaped) { "hello\nworld" } + + it "must remove double-quotes and unescape the Node.js string" do + expect(subject.unquote(data)).to eq(unescaped) + end + end + + context "when the String is single-quoted" do + let(:data) { "'hello\\'world'" } + let(:unescaped) { "hello'world" } + + it "must remove the single-quotes and unescape the Node.js string" do + expect(subject.unquote(data)).to eq(unescaped) + end + end + + context "when the String is not quoted" do + let(:data) { "hello world" } + + it "must return the same String" do + expect(subject.unquote(data)).to be(data) + end + end + end +end diff --git a/spec/encoding/perl/core_ext/integer_spec.rb b/spec/encoding/perl/core_ext/integer_spec.rb new file mode 100644 index 000000000..b0ee56a28 --- /dev/null +++ b/spec/encoding/perl/core_ext/integer_spec.rb @@ -0,0 +1,102 @@ +require 'spec_helper' +require 'ronin/support/encoding/perl/core_ext/integer' + +describe Integer do + subject { 0x26 } + + it { expect(subject).to respond_to(:perl_escape) } + it { expect(subject).to respond_to(:perl_encode) } + + describe "#perl_escape" do + { + 0x07 => '\a', + 0x08 => '\b', + 0x09 => '\t', + 0x0a => '\n', + 0x0c => '\f', + 0x0d => '\r', + 0x1B => '\e', + 0x22 => '\"', + 0x24 => '\$', + 0x5c => '\\\\' + }.each do |byte,escaped_char| + context "when called on #{byte}" do + subject { byte } + + it "must return #{escaped_char.inspect}" do + expect(subject.perl_escape).to eq(escaped_char) + end + end + end + + context "when called on an Integer between 0x20 and 0x7e" do + subject { 0x41 } + + it "must return the ASCII character for the byte" do + expect(subject.perl_escape).to eq(subject.chr) + end + end + + context "when called on an Integer that does not map to an ASCII char" do + subject { 0xFF } + + it "must return the lowercase '\\xXX' hex escaped String" do + expect(subject.perl_escape).to eq('\xFF') + end + end + + context "when called on an Integer between 0x100 and 0x10ffff" do + subject { 0xFFFF } + + it "must return the lowercase '\\x{XXXX}' hex escaped String" do + expect(subject.perl_escape).to eq('\x{FFFF}') + end + end + + context "when called on a negative Integer" do + subject { -1 } + + it do + expect { + subject.perl_escape + }.to raise_error(RangeError,"#{subject.inspect} out of char range") + end + end + end + + describe "#perl_encode" do + subject { 0x26 } + + let(:encoded_byte) { '\x26' } + + it "must return the '\\xXX' form of the byte" do + expect(subject.perl_encode).to eq(encoded_byte) + end + + context "when called on an Integer that does not map to an ASCII char" do + subject { 0xFF } + + it "must return the lowercase '\\xXX' hex escaped String" do + expect(subject.perl_encode).to eq('\xFF') + end + end + + context "when called on an Integer between 0x100 and 0xffff" do + subject { 0xFFFF } + + it "must return the lowercase '\\x{XXXX}' hex escaped String" do + expect(subject.perl_encode).to eq('\x{FFFF}') + end + end + + context "when called on a negative Integer" do + subject { -1 } + + it do + expect { + subject.perl_encode + }.to raise_error(RangeError,"#{subject.inspect} out of char range") + end + end + end +end diff --git a/spec/encoding/perl/core_ext/string_spec.rb b/spec/encoding/perl/core_ext/string_spec.rb new file mode 100644 index 000000000..849b8ee14 --- /dev/null +++ b/spec/encoding/perl/core_ext/string_spec.rb @@ -0,0 +1,326 @@ +require 'spec_helper' +require 'ronin/support/encoding/perl/core_ext/string' + +describe String do + subject { "hello world" } + + describe "#perl_escape" do + context "when the given String does not contain special characters" do + subject { "abc" } + + it "must return the given String" do + expect(subject.perl_escape).to eq(subject) + end + end + + context "when the given String contains back-slashed escaped characters" do + subject { "\a\b\e\t\n\f\r\\\"$" } + + let(:escaped_string) { "\\a\\b\\e\\t\\n\\f\\r\\\\\\\"\\$" } + + it "must escape the special characters with an extra back-slash" do + expect(subject.perl_escape).to eq(escaped_string) + end + end + + context "when the given String contains non-printable characters" do + subject do + "hello\xffworld".force_encoding(Encoding::ASCII_8BIT) + end + + let(:escaped_string) { "hello\\xFFworld" } + + it "must escape non-printable characters with an extra back-slash" do + expect(subject.perl_escape).to eq(escaped_string) + end + end + + context "when the given String contains unicode characters" do + subject { "hello\u1001world" } + + let(:escaped_string) { "hello\\x{1001}world" } + + it "must escape the unicode characters with a \\u" do + expect(subject.perl_escape).to eq(escaped_string) + end + end + + context "when the String contains invalid byte sequences" do + subject { "hello\xfe\xff" } + + let(:escaped_string) { "hello\\xFE\\xFF" } + + it "must escape each byte in the String" do + expect(subject.perl_escape).to eq(escaped_string) + end + end + end + + describe ".perl_unescape" do + context "when the given String contains escaped hexadecimal characters" do + subject do + "\\x68\\x65\\x6c\\x6c\\x6f\\x20\\x77\\x6f\\x72\\x6c\\x64" + end + + let(:unescaped) { "hello world" } + + it "must unescape the hexadecimal characters" do + expect(subject.perl_unescape).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.perl_unescape.encoding).to be(Encoding::UTF_8) + end + + context "when the given String contains empty '\\x' hexadecimal escapes" do + subject { "hello\\xworld" } + + let(:unescaped) { "helloworld" } + + it "must ignore empty '\\x' hexadecimal escapes" do + expect(subject.perl_unescape).to eq(unescaped) + end + end + end + + context "when the given String contains escaped unicode characters" do + subject { "\\x{00D8}\\N{U+2070E}" } + + let(:unescaped) { "Ø𠜎" } + + it "must unescape the hexadecimal characters" do + expect(subject.perl_unescape).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.perl_unescape.encoding).to be(Encoding::UTF_8) + end + + context "and when there are spaces within the '\\x{ ... }'" do + subject { "\\x{ 00D8 }\\N{U+2070E}" } + + it "must skip the spaces within the '\\x{...}'" do + expect(subject.perl_unescape).to eq(unescaped) + end + end + end + + context "when the given String contains escaped Unicode Named Characters" do + let(:named_char) { "\\N{GREEK CAPITAL LETTER SIGMA}" } + + subject { "hello #{named_char} world" } + + it do + expect { + subject.perl_unescape + }.to raise_error(NotImplementedError,"decoding Perl Unicode Named Characters (#{named_char.inspect}) is currently not supported: #{subject.inspect}") + end + end + + context "when the given String contains single character escaped octal characters" do + subject { "\\0\\1\\2\\3\\4\\5\\6\\7" } + + let(:unescaped) { "\0\1\2\3\4\5\6\7" } + + it "must unescape the octal characters" do + expect(subject.perl_unescape).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.perl_unescape.encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains two character escaped octal characters" do + subject { "\\10\\11\\12\\13\\14\\15\\16\\17\\20" } + + let(:unescaped) { "\10\11\12\13\14\15\16\17\20" } + + it "must unescape the octal characters" do + expect(subject.perl_unescape).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.perl_unescape.encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains three character escaped octal characters" do + subject do + "\\150\\145\\154\\154\\157\\040\\167\\157\\162\\154\\144" + end + + let(:unescaped) { "hello world" } + + it "must unescape the octal characters" do + expect(subject.perl_unescape).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.perl_unescape.encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains '\\o{...}' three character escaped octal characters" do + subject do + "\\o{150}\\o{145}\\o{154}\\o{154}\\o{157}\\o{040}\\o{167}\\o{157}\\o{162}\\o{154}\\o{144}" + end + + let(:unescaped) { "hello world" } + + it "must unescape the octal characters" do + expect(subject.perl_unescape).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.perl_unescape.encoding).to be(Encoding::UTF_8) + end + + context "and when there are spaces within the '\\o{ ... }'" do + subject do + "\\o{ 150 }\\o{ 145 }\\o{ 154 }\\o{ 154 }\\o{ 157 }\\o{ 040 }\\o{ 167 }\\o{ 157 }\\o{ 162 }\\o{ 154 }\\o{ 144 }" + end + + it "must skip the spaces within the '\\o{...}'" do + expect(subject.perl_unescape).to eq(unescaped) + end + end + end + + context "when the given String contains escaped special characters" do + subject { "hello\\0world\\n" } + + let(:unescaped) { "hello\0world\n" } + + it "must unescape Perl special characters" do + expect(subject.perl_unescape).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.perl_unescape.encoding).to be(Encoding::UTF_8) + end + + context "but the escaped character is not a known escaped character" do + subject { "hello\\world" } + + let(:unescaped) { "helloworld" } + + it "must return the character following the backslash escape" do + expect(subject.perl_unescape).to eq(unescaped) + end + end + end + + context "when the given String does not contain escaped characters" do + subject { "hello world" } + + it "must return the given String" do + expect(subject.perl_unescape).to eq(subject) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.perl_unescape.encoding).to be(Encoding::UTF_8) + end + end + end + + describe "#perl_encode" do + subject { "ABC" } + + let(:encoded) { '\x41\x42\x43' } + + it "must Perl encode each character in the string" do + expect(subject.perl_encode).to eq(encoded) + end + + context "when the String contains invalid byte sequences" do + subject { "ABC\xfe\xff" } + + let(:encoded) { '\x41\x42\x43\xFE\xFF' } + + it "must encode each byte in the String" do + expect(subject.perl_encode).to eq(encoded) + end + end + end + + describe "#perl_string" do + subject { "hello\nworld" } + + let(:quoted) { '"hello\nworld"' } + + it "must return a double quoted Perl string" do + expect(subject.perl_string).to eq(quoted) + end + end + + describe "#perl_unquote" do + context "when the given String is double-quoted" do + subject { "\"hello\\nworld\"" } + + let(:unescaped) { "hello\nworld" } + + it "must remove double-quotes and unescape the Perl string" do + expect(subject.perl_unquote).to eq(unescaped) + end + end + + context "when the given String is 'qq{ ... }' quoted" do + subject { "qq{hello\\nworld}" } + + let(:unescaped) { "hello\nworld" } + + it "must remove 'qq{ ... }' quotes and unescape the Perl string" do + expect(subject.perl_unquote).to eq(unescaped) + end + end + + context "when the given String is a single-quoted character" do + subject { "'A'" } + + let(:unescaped) { "A" } + + it "must remove single-quotes and return the character" do + expect(subject.perl_unquote).to eq(unescaped) + end + + context "but the character is a backslash escaped \\ character" do + subject { "'\\\\'" } + + let(:unescaped) { "\\" } + + it "must remove single-quotes and return the unescaped character" do + expect(subject.perl_unquote).to eq(unescaped) + end + end + + context "but the character is a backslash escaped ' character" do + subject { "'\\''" } + + let(:unescaped) { "'" } + + it "must remove single-quotes and return the unescaped character" do + expect(subject.perl_unquote).to eq(unescaped) + end + end + end + + context "when the given String is a 'q{ ... }' quoted" do + subject { "q{hello\\'world}" } + + let(:unescaped) { "hello\\'world" } + + it "must return the String without the 'q{ ... }' quoting" do + expect(subject.perl_unquote).to eq(unescaped) + end + end + + context "when the given String is not quoted" do + subject { "hello world" } + + it "must return the same String" do + expect(subject.perl_unquote).to be(subject) + end + end + end +end diff --git a/spec/encoding/perl_spec.rb b/spec/encoding/perl_spec.rb new file mode 100644 index 000000000..8e335d4f0 --- /dev/null +++ b/spec/encoding/perl_spec.rb @@ -0,0 +1,395 @@ +require 'spec_helper' +require 'ronin/support/encoding/perl' + +describe Ronin::Support::Encoding::Perl do + let(:data) { "hello world" } + + describe ".escape_byte" do + { + 0x07 => '\a', + 0x08 => '\b', + 0x09 => '\t', + 0x0a => '\n', + 0x0c => '\f', + 0x0d => '\r', + 0x1B => '\e', + 0x22 => '\"', + 0x24 => '\$', + 0x5c => '\\\\' + }.each do |byte,escaped_char| + context "when called on #{byte}" do + let(:byte) { byte } + + it "must return #{escaped_char.inspect}" do + expect(subject.escape_byte(byte)).to eq(escaped_char) + end + end + end + + context "when called on an Integer between 0x20 and 0x7e" do + let(:byte) { 0x41 } + + it "must return the ASCII character for the byte" do + expect(subject.escape_byte(byte)).to eq(byte.chr) + end + end + + context "when called on an Integer that does not map to an ASCII char" do + let(:byte) { 0xFF } + + it "must return the lowercase '\\xXX' hex escaped String" do + expect(subject.escape_byte(byte)).to eq('\xFF') + end + end + + context "when called on an Integer between 0x100 and 0x10ffff" do + let(:byte) { 0xFFFF } + + it "must return the lowercase '\\x{XXXX}' hex escaped String" do + expect(subject.escape_byte(byte)).to eq('\x{FFFF}') + end + end + + context "when called on a negative Integer" do + let(:byte) { -1 } + + it do + expect { + subject.escape_byte(byte) + }.to raise_error(RangeError,"#{byte.inspect} out of char range") + end + end + end + + describe ".encode_byte" do + let(:byte) { 0x26 } + let(:encoded_byte) { '\x26' } + + it "must return the '\\xXX' form of the byte" do + expect(subject.encode_byte(byte)).to eq(encoded_byte) + end + + context "when called on an Integer that does not map to an ASCII char" do + let(:byte) { 0xFF } + + it "must return the lowercase '\\xXX' hex escaped String" do + expect(subject.encode_byte(byte)).to eq('\xFF') + end + end + + context "when called on an Integer between 0x100 and 0xffff" do + let(:byte) { 0xFFFF } + + it "must return the lowercase '\\x{XXXX}' hex escaped String" do + expect(subject.encode_byte(byte)).to eq('\x{FFFF}') + end + end + + context "when called on a negative Integer" do + let(:byte) { -1 } + + it do + expect { + subject.encode_byte(byte) + }.to raise_error(RangeError,"#{byte.inspect} out of char range") + end + end + end + + describe ".escape" do + context "when the given String does not contain special characters" do + let(:data) { "abc" } + + it "must return the given String" do + expect(subject.escape(data)).to eq(data) + end + end + + context "when the given String contains back-slashed escaped characters" do + let(:data) { "\a\b\e\t\n\f\r\\\"$" } + let(:escaped_string) { "\\a\\b\\e\\t\\n\\f\\r\\\\\\\"\\$" } + + it "must escape the special characters with an extra back-slash" do + expect(subject.escape(data)).to eq(escaped_string) + end + end + + context "when the given String contains non-printable characters" do + let(:data) do + "hello\xffworld".force_encoding(Encoding::ASCII_8BIT) + end + let(:escaped_string) { "hello\\xFFworld" } + + it "must escape non-printable characters with an extra back-slash" do + expect(subject.escape(data)).to eq(escaped_string) + end + end + + context "when the given String contains unicode characters" do + let(:data) { "hello\u1001world" } + let(:escaped_string) { "hello\\x{1001}world" } + + it "must escape the unicode characters with a \\u" do + expect(subject.escape(data)).to eq(escaped_string) + end + end + + context "when the String contains invalid byte sequences" do + let(:data) { "hello\xfe\xff" } + let(:escaped_string) { "hello\\xFE\\xFF" } + + it "must escape each byte in the String" do + expect(subject.escape(data)).to eq(escaped_string) + end + end + end + + describe ".unescape" do + context "when the given String contains escaped hexadecimal characters" do + let(:data) do + "\\x68\\x65\\x6c\\x6c\\x6f\\x20\\x77\\x6f\\x72\\x6c\\x64" + end + let(:unescaped) { "hello world" } + + it "must unescape the hexadecimal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + + context "when the given String contains empty '\\x' hexadecimal escapes" do + let(:data) { "hello\\xworld" } + let(:unescaped) { "helloworld" } + + it "must ignore empty '\\x' hexadecimal escapes" do + expect(subject.unescape(data)).to eq(unescaped) + end + end + end + + context "when the given String contains escaped unicode characters" do + let(:data) { "\\x{00D8}\\N{U+2070E}" } + let(:unescaped) { "Ø𠜎" } + + it "must unescape the hexadecimal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + + context "and when there are spaces within the '\\x{ ... }'" do + let(:data) { "\\x{ 00D8 }\\N{U+2070E}" } + + it "must skip the spaces within the '\\x{...}'" do + expect(subject.unescape(data)).to eq(unescaped) + end + end + end + + context "when the given String contains escaped Unicode Named Characters" do + let(:named_char) { "\\N{GREEK CAPITAL LETTER SIGMA}" } + let(:data) { "hello #{named_char} world" } + + it do + expect { + subject.unescape(data) + }.to raise_error(NotImplementedError,"decoding Perl Unicode Named Characters (#{named_char.inspect}) is currently not supported: #{data.inspect}") + end + end + + context "when the given String contains single character escaped octal characters" do + let(:data) { "\\0\\1\\2\\3\\4\\5\\6\\7" } + let(:unescaped) { "\0\1\2\3\4\5\6\7" } + + it "must unescape the octal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains two character escaped octal characters" do + let(:data) { "\\10\\11\\12\\13\\14\\15\\16\\17\\20" } + let(:unescaped) { "\10\11\12\13\14\15\16\17\20" } + + it "must unescape the octal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains three character escaped octal characters" do + let(:data) do + "\\150\\145\\154\\154\\157\\040\\167\\157\\162\\154\\144" + end + let(:unescaped) { "hello world" } + + it "must unescape the octal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains '\\o{...}' three character escaped octal characters" do + let(:data) do + "\\o{150}\\o{145}\\o{154}\\o{154}\\o{157}\\o{040}\\o{167}\\o{157}\\o{162}\\o{154}\\o{144}" + end + let(:unescaped) { "hello world" } + + it "must unescape the octal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + + context "and when there are spaces within the '\\o{ ... }'" do + let(:data) do + "\\o{ 150 }\\o{ 145 }\\o{ 154 }\\o{ 154 }\\o{ 157 }\\o{ 040 }\\o{ 167 }\\o{ 157 }\\o{ 162 }\\o{ 154 }\\o{ 144 }" + end + + it "must skip the spaces within the '\\o{...}'" do + expect(subject.unescape(data)).to eq(unescaped) + end + end + end + + context "when the given String contains escaped special characters" do + let(:data) { "hello\\0world\\n" } + let(:unescaped) { "hello\0world\n" } + + it "must unescape Perl special characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + + context "but the escaped character is not a known escaped character" do + let(:data) { "hello\\world" } + let(:unescaped) { "helloworld" } + + it "must return the character following the backslash escape" do + expect(subject.unescape(data)).to eq(unescaped) + end + end + end + + context "when the given String does not contain escaped characters" do + let(:data) { "hello world" } + + it "must return the given String" do + expect(subject.unescape(data)).to eq(data) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + end + + describe ".encode" do + let(:data) { "ABC" } + let(:encoded) { '\x41\x42\x43' } + + it "must Perl encode each character in the string" do + expect(subject.encode(data)).to eq(encoded) + end + + context "when the String contains invalid byte sequences" do + let(:data) { "ABC\xfe\xff" } + let(:encoded) { '\x41\x42\x43\xFE\xFF' } + + it "must encode each byte in the String" do + expect(subject.encode(data)).to eq(encoded) + end + end + end + + describe ".quote" do + let(:data) { "hello\nworld" } + let(:quoted) { '"hello\nworld"' } + + it "must return a double quoted Perl string" do + expect(subject.quote(data)).to eq(quoted) + end + end + + describe ".unquote" do + context "when the given String is double-quoted" do + let(:data) { "\"hello\\nworld\"" } + let(:unescaped) { "hello\nworld" } + + it "must remove double-quotes and unescape the Perl string" do + expect(subject.unquote(data)).to eq(unescaped) + end + end + + context "when the given String is 'qq{ ... }' quoted" do + let(:data) { "qq{hello\\nworld}" } + let(:unescaped) { "hello\nworld" } + + it "must remove 'qq{ ... }' quotes and unescape the Perl string" do + expect(subject.unquote(data)).to eq(unescaped) + end + end + + context "when the given String is a single-quoted character" do + let(:data) { "'A'" } + let(:unescaped) { "A" } + + it "must remove single-quotes and return the character" do + expect(subject.unquote(data)).to eq(unescaped) + end + + context "but the character is a backslash escaped \\ character" do + let(:data) { "'\\\\'" } + let(:unescaped) { "\\" } + + it "must remove single-quotes and return the unescaped character" do + expect(subject.unquote(data)).to eq(unescaped) + end + end + + context "but the character is a backslash escaped ' character" do + let(:data) { "'\\''" } + let(:unescaped) { "'" } + + it "must remove single-quotes and return the unescaped character" do + expect(subject.unquote(data)).to eq(unescaped) + end + end + end + + context "when the given String is a 'q{ ... }' quoted" do + let(:data) { "q{hello\\'world}" } + let(:unescaped) { "hello\\'world" } + + it "must return the String without the 'q{ ... }' quoting" do + expect(subject.unquote(data)).to eq(unescaped) + end + end + + context "when the given String is not quoted" do + let(:data) { "hello world" } + + it "must return the same String" do + expect(subject.unquote(data)).to be(data) + end + end + end +end diff --git a/spec/encoding/php/core_ext/integer_spec.rb b/spec/encoding/php/core_ext/integer_spec.rb new file mode 100644 index 000000000..2cac460f0 --- /dev/null +++ b/spec/encoding/php/core_ext/integer_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' +require 'ronin/support/encoding/php/core_ext/integer' + +describe Integer do + subject { 0x26 } + + it { expect(subject).to respond_to(:php_escape) } + it { expect(subject).to respond_to(:php_encode) } + + describe "#php_escape" do + Ronin::Support::Encoding::PHP::ESCAPE_BYTES.each do |byte,escaped_char| + context "when called on #{byte}" do + subject { byte } + + it "must return #{escaped_char.inspect}" do + expect(subject.php_escape).to eq(escaped_char) + end + end + end + + context "when called on an Integer between 0x20 and 0x7e" do + subject { 0x41 } + + it "must return the ASCII character for the byte" do + expect(subject.php_escape).to eq(subject.chr) + end + end + + context "when called on an Integer that does not map to an ASCII char" do + subject { 0xFF } + + it "must return the lowercase '\\xXX' hex escaped String" do + expect(subject.php_escape).to eq('\xff') + end + end + + context "when called on an Integer between 0x100 and 0x10ffff" do + subject { 0xFFFF } + + it "must return the lowercase '\\u{XXXX}' hex escaped String" do + expect(subject.php_escape).to eq('\u{ffff}') + end + end + + context "when called on a negative Integer" do + subject { -1 } + + it do + expect { + subject.php_escape + }.to raise_error(RangeError,"#{subject} out of char range") + end + end + end + + describe "#php_encode" do + let(:php_formatted) { '\x26' } + + it "must return the '\\xXX' form of the byte" do + expect(subject.php_encode).to eq(php_formatted) + end + + context "when called on an Integer that does not map to an ASCII char" do + subject { 0xFF } + + it "must return the lowercase '\\xXX' hex escaped String" do + expect(subject.php_encode).to eq('\xff') + end + end + + context "when called on an Integer between 0x100 and 0x10ffff" do + subject { 0xFFFF } + + it "must return the lowercase '\\u{XXXX}' hex escaped String" do + expect(subject.php_encode).to eq('\u{ffff}') + end + end + + context "when called on a negative Integer" do + subject { -1 } + + it do + expect { + subject.php_encode + }.to raise_error(RangeError,"#{subject} out of char range") + end + end + end +end diff --git a/spec/encoding/php/core_ext/string_spec.rb b/spec/encoding/php/core_ext/string_spec.rb new file mode 100644 index 000000000..519c1edbd --- /dev/null +++ b/spec/encoding/php/core_ext/string_spec.rb @@ -0,0 +1,193 @@ +require 'spec_helper' +require 'ronin/support/encoding/php/core_ext/string' + +describe String do + subject { "hello world" } + + it { expect(subject).to respond_to(:php_escape) } + it { expect(subject).to respond_to(:php_unescape) } + it { expect(subject).to respond_to(:php_encode) } + it { expect(subject).to respond_to(:php_decode) } + it { expect(subject).to respond_to(:php_string) } + it { expect(subject).to respond_to(:php_unquote) } + + describe "#php_escape" do + context "when the String does not contain special characters" do + subject { "abc" } + + it "must return the String" do + expect(subject.php_escape).to eq(subject) + end + end + + context "when the String contains back-slashed escaped characters" do + subject { "\0\t\n\f\r\e\\\"$" } + + let(:escaped_php_string) { "\\0\\t\\n\\f\\r\\e\\\\\\\"\\$" } + + it "must escape the special characters with an extra back-slash" do + expect(subject.php_escape).to eq(escaped_php_string) + end + end + + context "when the String contains non-printable characters" do + subject { "hello\xffworld".force_encoding(Encoding::ASCII_8BIT) } + + let(:escaped_php_string) { "hello\\xffworld" } + + it "must escape non-printable characters with an extra back-slash" do + expect(subject.php_escape).to eq(escaped_php_string) + end + end + + context "when the String contains unicode characters" do + subject { "hello\u1001world" } + + let(:escaped_php_string) { "hello\\u{1001}world" } + + it "must escape the unicode characters with a \\u" do + expect(subject.php_escape).to eq(escaped_php_string) + end + end + + context "when the String contains invalid byte sequences" do + subject { "hello\xfe\xff" } + + let(:escaped_string) { "hello\\xfe\\xff" } + + it "must C escape each byte in the String" do + expect(subject.php_escape).to eq(escaped_string) + end + end + end + + describe "#php_unescape" do + context "when the String contains escaped hexadecimal characters" do + subject { "\\x68\\x65\\x6c\\x6c\\x6f\\x20\\x77\\x6f\\x72\\x6c\\x64" } + + let(:unescaped) { "hello world" } + + it "must unescape the hexadecimal characters" do + expect(subject.php_unescape).to eq(unescaped) + end + end + + context "when the String contains escaped unicode characters" do + subject { "\\u{00D8}\\u{2070E}" } + + let(:unescaped) { "Ø𠜎" } + + it "must unescape the hexadecimal characters" do + expect(subject.php_unescape).to eq(unescaped) + end + end + + context "when the String contains single character escaped octal characters" do + subject { "\\0\\1\\2\\3\\4\\5\\6\\7" } + + let(:unescaped) { "\0\1\2\3\4\5\6\7" } + + it "must unescape the octal characters" do + expect(subject.php_unescape).to eq(unescaped) + end + end + + context "when the String contains two character escaped octal characters" do + subject { "\\10\\11\\12\\13\\14\\15\\16\\17\\20" } + + let(:unescaped) { "\10\11\12\13\14\15\16\17\20" } + + it "must unescape the octal characters" do + expect(subject.php_unescape).to eq(unescaped) + end + end + + context "when the String contains three character escaped octal characters" do + subject { "\\150\\145\\154\\154\\157\\040\\167\\157\\162\\154\\144" } + + let(:unescaped) { "hello world" } + + it "must unescape the octal characters" do + expect(subject.php_unescape).to eq(unescaped) + end + end + + context "when the String contains escaped special characters" do + subject { "hello\\0world\\n" } + + let(:unescaped) { "hello\0world\n" } + + it "must unescape C special characters" do + expect(subject.php_unescape).to eq(unescaped) + end + end + + context "when the String does not contain escaped characters" do + subject { "hello world" } + + it "must return the String" do + expect(subject.php_unescape).to eq(subject) + end + end + end + + describe "#php_encode" do + subject { "ABC" } + + let(:php_encoded) { '\x41\x42\x43' } + + it "must C encode each character in the string" do + expect(subject.php_encode).to eq(php_encoded) + end + + context "when the String contains invalid byte sequences" do + subject { "ABC\xfe\xff" } + + let(:php_encoded) { '\x41\x42\x43\xfe\xff' } + + it "must C encode each byte in the String" do + expect(subject.php_encode).to eq(php_encoded) + end + end + end + + describe "#php_string" do + subject { "hello\nworld" } + + let(:php_string) { '"hello\nworld"' } + + it "must return a double quoted C string" do + expect(subject.php_string).to eq(php_string) + end + end + + describe "#php_unquote" do + context "when the String is double-quoted" do + subject { "\"hello\\nworld\"" } + + let(:unescaped) { "hello\nworld" } + + it "must remove double-quotes and unescape the C string" do + expect(subject.php_unquote).to eq(unescaped) + end + end + + context "when the String is a single-quoted character" do + subject { "'hello world\\''" } + + let(:unescaped) { "hello world'" } + + it "must remove single-quotes and unescape the C character" do + expect(subject.php_unquote).to eq(unescaped) + end + end + + context "when the String is not quoted" do + subject { "hello world" } + + it "must return the same String" do + expect(subject.php_unquote).to be(subject) + end + end + end +end diff --git a/spec/encoding/php_spec.rb b/spec/encoding/php_spec.rb new file mode 100644 index 000000000..a713d03b8 --- /dev/null +++ b/spec/encoding/php_spec.rb @@ -0,0 +1,260 @@ +require 'spec_helper' +require 'ronin/support/encoding/php' + +describe Ronin::Support::Encoding::PHP do + let(:data) { "hello world" } + + describe ".escape_byte" do + described_class::ESCAPE_BYTES.each do |byte,escaped_char| + context "when called on #{byte}" do + let(:byte) { byte } + + it "must return #{escaped_char.inspect}" do + expect(subject.escape_byte(byte)).to eq(escaped_char) + end + end + end + + context "when called on an Integer between 0x20 and 0x7e" do + let(:byte) { 0x41 } + + it "must return the ASCII character for the byte" do + expect(subject.escape_byte(byte)).to eq(byte.chr) + end + end + + context "when called on an Integer that does not map to an ASCII char" do + let(:byte) { 0xFF } + + it "must return the lowercase '\\xXX' hex escaped String" do + expect(subject.escape_byte(byte)).to eq('\xff') + end + end + + context "when called on an Integer between 0x100 and 0x10ffff" do + let(:byte) { 0xFFFF } + + it "must return the lowercase '\\u{XXXX..}' hex escaped String" do + expect(subject.escape_byte(byte)).to eq('\u{ffff}') + end + end + + context "when called on a negative Integer" do + let(:byte) { -1 } + + it do + expect { + subject.escape_byte(byte) + }.to raise_error(RangeError,"#{byte.inspect} out of char range") + end + end + end + + describe ".encode_byte" do + let(:byte) { 0x26 } + let(:encoded_byte) { '\x26' } + + it "must return the '\\xXX' form of the byte" do + expect(subject.encode_byte(byte)).to eq(encoded_byte) + end + + context "when called on an Integer that does not map to an ASCII char" do + let(:byte) { 0xFF } + + it "must return the lowercase '\\xXX' hex escaped String" do + expect(subject.encode_byte(byte)).to eq('\xff') + end + end + + context "when called on an Integer between 0x100 and 0x10ffff" do + let(:byte) { 0xFFFF } + + it "must return the lowercase '\\u{XXXX}' hex escaped String" do + expect(subject.encode_byte(byte)).to eq('\u{ffff}') + end + end + + context "when called on a negative Integer" do + let(:byte) { -1 } + + it do + expect { + subject.encode_byte(byte) + }.to raise_error(RangeError,"#{byte.inspect} out of char range") + end + end + end + + describe ".escape" do + context "when the given String does not contain special characters" do + let(:data) { "abc" } + + it "must return the given String" do + expect(subject.escape(data)).to eq(data) + end + end + + context "when the given String contains back-slashed escaped characters" do + let(:data) { "\0\t\n\f\r\e\\\"$" } + let(:escaped_string) { "\\0\\t\\n\\f\\r\\e\\\\\\\"\\$" } + + it "must escape the special characters with an extra back-slash" do + expect(subject.escape(data)).to eq(escaped_string) + end + end + + context "when the given String contains non-printable characters" do + let(:data) { "hello\x01world" } + let(:escaped_string) { "hello\\x01world" } + + it "must escape non-printable characters with an extra back-slash" do + expect(subject.escape(data)).to eq(escaped_string) + end + end + + context "when the given String contains unicode characters" do + let(:data) { "hello\u1001world" } + let(:escaped_string) { "hello\\u{1001}world" } + + it "must escape the unicode characters with a \\u" do + expect(subject.escape(data)).to eq(escaped_string) + end + end + + context "when the String contains invalid byte sequences" do + let(:data) { "hello\xfe\xff" } + let(:escaped_string) { "hello\\xfe\\xff" } + + it "must C escape each byte in the String" do + expect(subject.escape(data)).to eq(escaped_string) + end + end + end + + describe ".unescape" do + context "when the given String contains escaped hexadecimal characters" do + let(:data) do + "\\x68\\x65\\x6c\\x6c\\x6f\\x20\\x77\\x6f\\x72\\x6c\\x64" + end + let(:unescaped) { "hello world" } + + it "must unescape the hexadecimal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + end + + context "when the given String contains escaped unicode characters" do + let(:data) { "\\u{00D8}\\u{2070E}" } + let(:unescaped) { "Ø𠜎" } + + it "must unescape the hexadecimal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + end + + context "when the given String contains single character escaped octal characters" do + let(:data) { "\\0\\1\\2\\3\\4\\5\\6\\7" } + let(:unescaped) { "\0\1\2\3\4\5\6\7" } + + it "must unescape the octal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + end + + context "when the given String contains two character escaped octal characters" do + let(:data) { "\\10\\11\\12\\13\\14\\15\\16\\17\\20" } + let(:unescaped) { "\10\11\12\13\14\15\16\17\20" } + + it "must unescape the octal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + end + + context "when the given String contains three character escaped octal characters" do + let(:data) do + "\\150\\145\\154\\154\\157\\040\\167\\157\\162\\154\\144" + end + let(:unescaped) { "hello world" } + + it "must unescape the octal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + end + + context "when the given String contains escaped special characters" do + let(:data) { "hello\\0world\\n" } + let(:unescaped) { "hello\0world\n" } + + it "must unescape C special characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + end + + context "when the given String does not contain escaped characters" do + let(:data) { "hello world" } + + it "must return the given String" do + expect(subject.unescape(data)).to eq(data) + end + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + + describe ".encode" do + let(:data) { "ABC" } + let(:encoded) { '\x41\x42\x43' } + + it "must C encode each character in the string" do + expect(subject.encode(data)).to eq(encoded) + end + + context "when the String contains invalid byte sequences" do + let(:data) { "ABC\xfe\xff" } + let(:encoded) { '\x41\x42\x43\xfe\xff' } + + it "must C escape each byte in the String" do + expect(subject.encode(data)).to eq(encoded) + end + end + end + + describe ".quote" do + let(:data) { "hello\nworld" } + let(:quoted) { '"hello\nworld"' } + + it "must return a double quoted C string" do + expect(subject.quote(data)).to eq(quoted) + end + end + + describe ".unquote" do + context "when the given String is double-quoted" do + let(:data) { "\"hello\\nworld\"" } + let(:unescaped) { "hello\nworld" } + + it "must remove double-quotes and unescape the C string" do + expect(subject.unquote(data)).to eq(unescaped) + end + end + + context "when the given String is a single-quoted character" do + let(:data) { "'hello world\\''" } + let(:unescaped) { "hello world'" } + + it "must remove single-quotes and unescape the C character" do + expect(subject.unquote(data)).to eq(unescaped) + end + end + + context "when the given String is not quoted" do + let(:data) { "hello world" } + + it "must return the same String" do + expect(subject.unquote(data)).to be(data) + end + end + end +end diff --git a/spec/encoding/python/core_ext/integer_spec.rb b/spec/encoding/python/core_ext/integer_spec.rb new file mode 100644 index 000000000..78c4ade2e --- /dev/null +++ b/spec/encoding/python/core_ext/integer_spec.rb @@ -0,0 +1,118 @@ +require 'spec_helper' +require 'ronin/support/encoding/python/core_ext/integer' + +describe Integer do + subject { 0x26 } + + it { expect(subject).to respond_to(:python_escape) } + it { expect(subject).to respond_to(:python_encode) } + + describe "#python_escape" do + { + 0x00 => '\x00', + 0x07 => '\a', + 0x08 => '\b', + 0x09 => '\t', + 0x0a => '\n', + 0x0b => '\v', + 0x0c => '\f', + 0x0d => '\r', + 0x22 => '\"', + 0x5c => '\\\\' + }.each do |byte,escaped_char| + context "when called on #{byte}" do + subject { byte } + + it "must return #{escaped_char.inspect}" do + expect(subject.python_escape).to eq(escaped_char) + end + end + end + + context "when called on an Integer between 0x20 and 0x7e" do + subject { 0x41 } + + it "must return the ASCII character for the byte" do + expect(subject.python_escape).to eq(subject.chr) + end + end + + context "when called on an Integer that does not map to an ASCII char" do + subject { 0xFF } + + it "must return the lowercase '\\xXX' hex escaped String" do + expect(subject.python_escape).to eq('\xff') + end + end + + context "when called on an Integer between 0x100 and 0xffff" do + subject { 0xFFFF } + + it "must return the lowercase '\\uXXXX' hex escaped String" do + expect(subject.python_escape).to eq('\uffff') + end + end + + context "when called on an Integer between 0x10000 and 0x10ffff" do + subject { 0x10000 } + + it "must return the lowercase '\\UXXXXXXXX' hex escaped String" do + expect(subject.python_escape).to eq('\U00010000') + end + end + + context "when called on a negative Integer" do + subject { -1 } + + it do + expect { + subject.python_escape + }.to raise_error(RangeError,"#{subject.inspect} out of char range") + end + end + end + + describe "#python_encode" do + subject { 0x26 } + + let(:encoded_byte) { '\x26' } + + it "must return the '\\xXX' form of the byte" do + expect(subject.python_encode).to eq(encoded_byte) + end + + context "when called on an Integer that does not map to an ASCII char" do + subject { 0xFF } + + it "must return the lowercase '\\xXX' hex escaped String" do + expect(subject.python_encode).to eq('\xff') + end + end + + context "when called on an Integer between 0x100 and 0x10ffff" do + subject { 0xFFFF } + + it "must return the lowercase '\\uXXXX' hex escaped String" do + expect(subject.python_encode).to eq('\uffff') + end + end + + context "when called on an Integer between 0x10000 and 0x10ffff" do + subject { 0x10000 } + + it "must return the lowercase '\\UXXXXXXXX' hex escaped String" do + expect(subject.python_escape).to eq('\U00010000') + end + end + + context "when called on a negative Integer" do + subject { -1 } + + it do + expect { + subject.python_encode + }.to raise_error(RangeError,"#{subject.inspect} out of char range") + end + end + end +end diff --git a/spec/encoding/python/core_ext/string_spec.rb b/spec/encoding/python/core_ext/string_spec.rb new file mode 100644 index 000000000..cf71ed234 --- /dev/null +++ b/spec/encoding/python/core_ext/string_spec.rb @@ -0,0 +1,267 @@ +require 'spec_helper' +require 'ronin/support/encoding/python/core_ext/string' + +describe String do + subject { "hello world" } + + it { expect(subject).to respond_to(:python_escape) } + it { expect(subject).to respond_to(:python_unescape) } + it { expect(subject).to respond_to(:python_encode) } + it { expect(subject).to respond_to(:python_decode) } + it { expect(subject).to respond_to(:python_string) } + it { expect(subject).to respond_to(:python_unquote) } + + describe ".python_escape" do + context "when the given String does not contain special characters" do + subject { "abc" } + + it "must return the given String" do + expect(subject.python_escape).to eq(subject) + end + end + + context "when the given String contains back-slashed escaped characters" do + subject { "\0\a\b\t\n\v\f\r\\\"" } + + let(:escaped_string) { "\\x00\\a\\b\\t\\n\\v\\f\\r\\\\\\\"" } + + it "must escape the special characters with an extra back-slash" do + expect(subject.python_escape).to eq(escaped_string) + end + end + + context "when the given String contains non-printable characters" do + subject { "hello\xffworld".force_encoding(Encoding::ASCII_8BIT) } + + let(:escaped_string) { "hello\\xffworld" } + + it "must escape non-printable characters with an extra back-slash" do + expect(subject.python_escape).to eq(escaped_string) + end + end + + context "when the given String contains unicode characters" do + subject { "hello\u1001world" } + + let(:escaped_string) { "hello\\u1001world" } + + it "must escape the unicode characters with a \\u" do + expect(subject.python_escape).to eq(escaped_string) + end + end + + context "when the String contains invalid byte sequences" do + subject { "hello\xfe\xff" } + + let(:escaped_string) { "hello\\xfe\\xff" } + + it "must escape each byte in the String" do + expect(subject.python_escape).to eq(escaped_string) + end + end + end + + describe ".python_unescape" do + context "when the given String contains escaped hexadecimal characters" do + subject { "\\x68\\x65\\x6c\\x6c\\x6f\\x20\\x77\\x6f\\x72\\x6c\\x64" } + + let(:unescaped) { "hello world" } + + it "must unescape the hexadecimal characters" do + expect(subject.python_unescape).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.python_unescape.encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains escaped unicode characters" do + subject { "\\u00D8\\U0002070E" } + + let(:unescaped) { "Ø𠜎" } + + it "must unescape the hexadecimal characters" do + expect(subject.python_unescape).to eq(unescaped) + end + end + + context "when the given String contains single character escaped octal characters" do + subject { "\\0\\1\\2\\3\\4\\5\\6\\7" } + + let(:unescaped) { "\0\1\2\3\4\5\6\7" } + + it "must unescape the octal characters" do + expect(subject.python_unescape).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.python_unescape.encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains two character escaped octal characters" do + subject { "\\10\\11\\12\\13\\14\\15\\16\\17\\20" } + + let(:unescaped) { "\10\11\12\13\14\15\16\17\20" } + + it "must unescape the octal characters" do + expect(subject.python_unescape).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.python_unescape.encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains three character escaped octal characters" do + subject { "\\150\\145\\154\\154\\157\\040\\167\\157\\162\\154\\144" } + + let(:unescaped) { "hello world" } + + it "must unescape the octal characters" do + expect(subject.python_unescape).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.python_unescape.encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains escaped special characters" do + subject { "hello\\0world\\n" } + + let(:unescaped) { "hello\0world\n" } + + it "must unescape Python special characters" do + expect(subject.python_unescape).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.python_unescape.encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String does not contain escaped characters" do + subject { "hello world" } + + it "must return the given String" do + expect(subject.python_unescape).to eq(subject) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.python_unescape.encoding).to be(Encoding::UTF_8) + end + end + end + + describe ".python_encode" do + subject { "ABC" } + + let(:encoded) { '\x41\x42\x43' } + + it "must Python encode each character in the string" do + expect(subject.python_encode).to eq(encoded) + end + + context "when the String contains invalid byte sequences" do + subject { "ABC\xfe\xff" } + + let(:encoded) { '\x41\x42\x43\xfe\xff' } + + it "must encode each byte in the String" do + expect(subject.python_encode).to eq(encoded) + end + end + end + + describe ".python_string" do + subject { "hello\nworld" } + + let(:quoted) { '"hello\nworld"' } + + it "must return a double quoted Python string" do + expect(subject.python_string).to eq(quoted) + end + end + + describe ".python_unquote" do + context "when the given String is double-quoted" do + subject { "\"hello\\nworld\"" } + + let(:unescaped) { "hello\nworld" } + + it "must remove double-quotes and unescape the Python string" do + expect(subject.python_unquote).to eq(unescaped) + end + end + + context "when the given String is a single-quoted character" do + subject { "'A'" } + + let(:unescaped) { "A" } + + it "must remove single-quotes and return the character" do + expect(subject.python_unquote).to eq(unescaped) + end + + context "but the character is a backslash escaped \\ character" do + subject { "'\\\\'" } + + let(:unescaped) { "\\" } + + it "must remove single-quotes and return the unescaped character" do + expect(subject.python_unquote).to eq(unescaped) + end + end + + context "but the character is a backslash escaped ' character" do + subject { "'\\''" } + + let(:unescaped) { "'" } + + it "must remove single-quotes and return the unescaped character" do + expect(subject.python_unquote).to eq(unescaped) + end + end + end + + context "when the given String is triple-quoted" do + subject { "'''hello\\nworld'''" } + + let(:unescaped) { "hello\nworld" } + + it "must remove triple-quotes and unescape the Python string" do + expect(subject.python_unquote).to eq(unescaped) + end + end + + context "when the given String starts with 'u'" do + subject { "u'hello\\nworld'" } + + let(:unescaped) { "hello\nworld" } + + it "must remove 'u' and quotes, and unescape the Python string" do + expect(subject.python_unquote).to eq(unescaped) + end + end + + context "when the given String starts with 'r'" do + let(:raw_string) { "hello\\nworld" } + + subject { "r'#{raw_string}'" } + + it "must remove 'r' and single-quotes, but not unescape the Python string" do + expect(subject.python_unquote).to eq(raw_string) + end + end + + context "when the given String is not quoted" do + subject { "hello world" } + + it "must return the same String" do + expect(subject.python_unquote).to be(subject) + end + end + end +end diff --git a/spec/encoding/python_spec.rb b/spec/encoding/python_spec.rb new file mode 100644 index 000000000..7343e2aa5 --- /dev/null +++ b/spec/encoding/python_spec.rb @@ -0,0 +1,354 @@ +require 'spec_helper' +require 'ronin/support/encoding/python' + +describe Ronin::Support::Encoding::Python do + let(:data) { "hello world" } + + describe ".escape_byte" do + { + 0x00 => '\x00', + 0x07 => '\a', + 0x08 => '\b', + 0x09 => '\t', + 0x0a => '\n', + 0x0b => '\v', + 0x0c => '\f', + 0x0d => '\r', + 0x22 => '\"', + 0x5c => '\\\\' + }.each do |byte,escaped_char| + context "when called on #{byte}" do + let(:byte) { byte } + + it "must return #{escaped_char.inspect}" do + expect(subject.escape_byte(byte)).to eq(escaped_char) + end + end + end + + context "when called on an Integer between 0x20 and 0x7e" do + let(:byte) { 0x41 } + + it "must return the ASCII character for the byte" do + expect(subject.escape_byte(byte)).to eq(byte.chr) + end + end + + context "when called on an Integer that does not map to an ASCII char" do + let(:byte) { 0xFF } + + it "must return the lowercase '\\xXX' hex escaped String" do + expect(subject.escape_byte(byte)).to eq('\xff') + end + end + + context "when called on an Integer between 0x100 and 0xffff" do + let(:byte) { 0xFFFF } + + it "must return the lowercase '\\uXXXX' hex escaped String" do + expect(subject.escape_byte(byte)).to eq('\uffff') + end + end + + context "when called on an Integer between 0x10000 and 0x10ffff" do + let(:byte) { 0x10000 } + + it "must return the lowercase '\\UXXXXXXXX' hex escaped String" do + expect(subject.escape_byte(byte)).to eq('\U00010000') + end + end + + context "when called on a negative Integer" do + let(:byte) { -1 } + + it do + expect { + subject.escape_byte(byte) + }.to raise_error(RangeError,"#{byte.inspect} out of char range") + end + end + end + + describe ".encode_byte" do + let(:byte) { 0x26 } + let(:encoded_byte) { '\x26' } + + it "must return the '\\xXX' form of the byte" do + expect(subject.encode_byte(byte)).to eq(encoded_byte) + end + + context "when called on an Integer that does not map to an ASCII char" do + let(:byte) { 0xFF } + + it "must return the lowercase '\\xXX' hex escaped String" do + expect(subject.encode_byte(byte)).to eq('\xff') + end + end + + context "when called on an Integer between 0x100 and 0x10ffff" do + let(:byte) { 0xFFFF } + + it "must return the lowercase '\\uXXXX' hex escaped String" do + expect(subject.encode_byte(byte)).to eq('\uffff') + end + end + + context "when called on an Integer between 0x10000 and 0x10ffff" do + let(:byte) { 0x10000 } + + it "must return the lowercase '\\UXXXXXXXX' hex escaped String" do + expect(subject.escape_byte(byte)).to eq('\U00010000') + end + end + + context "when called on a negative Integer" do + let(:byte) { -1 } + + it do + expect { + subject.encode_byte(byte) + }.to raise_error(RangeError,"#{byte.inspect} out of char range") + end + end + end + + describe ".escape" do + context "when the given String does not contain special characters" do + let(:data) { "abc" } + + it "must return the given String" do + expect(subject.escape(data)).to eq(data) + end + end + + context "when the given String contains back-slashed escaped characters" do + let(:data) { "\0\a\b\t\n\v\f\r\\\"" } + let(:escaped_string) { "\\x00\\a\\b\\t\\n\\v\\f\\r\\\\\\\"" } + + it "must escape the special characters with an extra back-slash" do + expect(subject.escape(data)).to eq(escaped_string) + end + end + + context "when the given String contains non-printable characters" do + let(:data) do + "hello\xffworld".force_encoding(Encoding::ASCII_8BIT) + end + let(:escaped_string) { "hello\\xffworld" } + + it "must escape non-printable characters with an extra back-slash" do + expect(subject.escape(data)).to eq(escaped_string) + end + end + + context "when the given String contains unicode characters" do + let(:data) { "hello\u1001world" } + let(:escaped_string) { "hello\\u1001world" } + + it "must escape the unicode characters with a \\u" do + expect(subject.escape(data)).to eq(escaped_string) + end + end + + context "when the String contains invalid byte sequences" do + let(:data) { "hello\xfe\xff" } + let(:escaped_string) { "hello\\xfe\\xff" } + + it "must escape each byte in the String" do + expect(subject.escape(data)).to eq(escaped_string) + end + end + end + + describe ".unescape" do + context "when the given String contains escaped hexadecimal characters" do + let(:data) do + "\\x68\\x65\\x6c\\x6c\\x6f\\x20\\x77\\x6f\\x72\\x6c\\x64" + end + let(:unescaped) { "hello world" } + + it "must unescape the hexadecimal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains escaped unicode characters" do + let(:data) { "\\u00D8\\U0002070E" } + let(:unescaped) { "Ø𠜎" } + + it "must unescape the hexadecimal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + end + + context "when the given String contains single character escaped octal characters" do + let(:data) { "\\0\\1\\2\\3\\4\\5\\6\\7" } + let(:unescaped) { "\0\1\2\3\4\5\6\7" } + + it "must unescape the octal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains two character escaped octal characters" do + let(:data) { "\\10\\11\\12\\13\\14\\15\\16\\17\\20" } + let(:unescaped) { "\10\11\12\13\14\15\16\17\20" } + + it "must unescape the octal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains three character escaped octal characters" do + let(:data) do + "\\150\\145\\154\\154\\157\\040\\167\\157\\162\\154\\144" + end + let(:unescaped) { "hello world" } + + it "must unescape the octal characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String contains escaped special characters" do + let(:data) { "hello\\0world\\n" } + let(:unescaped) { "hello\0world\n" } + + it "must unescape Python special characters" do + expect(subject.unescape(data)).to eq(unescaped) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + + context "when the given String does not contain escaped characters" do + let(:data) { "hello world" } + + it "must return the given String" do + expect(subject.unescape(data)).to eq(data) + end + + it "must set the String encoding to Encoding::UTF_8" do + expect(subject.unescape(data).encoding).to be(Encoding::UTF_8) + end + end + end + + describe ".encode" do + let(:data) { "ABC" } + let(:encoded) { '\x41\x42\x43' } + + it "must Python encode each character in the string" do + expect(subject.encode(data)).to eq(encoded) + end + + context "when the String contains invalid byte sequences" do + let(:data) { "ABC\xfe\xff" } + let(:encoded) { '\x41\x42\x43\xfe\xff' } + + it "must encode each byte in the String" do + expect(subject.encode(data)).to eq(encoded) + end + end + end + + describe ".quote" do + let(:data) { "hello\nworld" } + let(:quoted) { '"hello\nworld"' } + + it "must return a double quoted Python string" do + expect(subject.quote(data)).to eq(quoted) + end + end + + describe ".unquote" do + context "when the given String is double-quoted" do + let(:data) { "\"hello\\nworld\"" } + let(:unescaped) { "hello\nworld" } + + it "must remove double-quotes and unescape the Python string" do + expect(subject.unquote(data)).to eq(unescaped) + end + end + + context "when the given String is a single-quoted character" do + let(:data) { "'A'" } + let(:unescaped) { "A" } + + it "must remove single-quotes and return the character" do + expect(subject.unquote(data)).to eq(unescaped) + end + + context "but the character is a backslash escaped \\ character" do + let(:data) { "'\\\\'" } + let(:unescaped) { "\\" } + + it "must remove single-quotes and return the unescaped character" do + expect(subject.unquote(data)).to eq(unescaped) + end + end + + context "but the character is a backslash escaped ' character" do + let(:data) { "'\\''" } + let(:unescaped) { "'" } + + it "must remove single-quotes and return the unescaped character" do + expect(subject.unquote(data)).to eq(unescaped) + end + end + end + + context "when the given String is triple-quoted" do + let(:data) { "'''hello\\nworld'''" } + let(:unescaped) { "hello\nworld" } + + it "must remove triple-quotes and unescape the Python string" do + expect(subject.unquote(data)).to eq(unescaped) + end + end + + context "when the given String starts with 'u'" do + let(:data) { "u'hello\\nworld'" } + let(:unescaped) { "hello\nworld" } + + it "must remove 'u' and quotes, and unescape the Python string" do + expect(subject.unquote(data)).to eq(unescaped) + end + end + + context "when the given String starts with 'r'" do + let(:raw_string) { "hello\\nworld" } + let(:data) { "r'#{raw_string}'" } + + it "must remove 'r' and single-quotes, but not unescape the Python string" do + expect(subject.unquote(data)).to eq(raw_string) + end + end + + context "when the given String is not quoted" do + let(:data) { "hello world" } + + it "must return the same String" do + expect(subject.unquote(data)).to be(data) + end + end + end +end diff --git a/spec/encoding/smtp/core_ext/string_spec.rb b/spec/encoding/smtp/core_ext/string_spec.rb new file mode 100644 index 000000000..a161b3032 --- /dev/null +++ b/spec/encoding/smtp/core_ext/string_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' +require 'ronin/support/encoding/smtp/core_ext/string' + +describe String do + subject { "hello=world" } + + it "must provide String#smtp_escape" do + expect(subject).to respond_to(:smtp_escape) + end + + it "must provide String#smtp_unescape" do + expect(subject).to respond_to(:smtp_unescape) + end + + it "must provide String#smtp_encode" do + expect(subject).to respond_to(:smtp_encode) + end + + it "must provide String#smtp_decode" do + expect(subject).to respond_to(:smtp_decode) + end + + describe "#smtp_escape" do + it "must escape '=' characters as '=3D' and append '=\\n' to the end of Strings" do + expect(subject.smtp_escape).to eq("hello=3Dworld=\n") + end + end + + describe "#smtp_unescape" do + subject { "hello=3Dworld=\n" } + + it "must unescape '=3D' as a '=' character and remove '=\\n' from the String" do + expect(subject.smtp_unescape).to eq("hello=world") + end + end +end diff --git a/spec/encoding/smtp_spec.rb b/spec/encoding/smtp_spec.rb new file mode 100644 index 000000000..1d53a7176 --- /dev/null +++ b/spec/encoding/smtp_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' +require 'ronin/support/encoding/smtp' + +describe "Ronin::Support::Encoding::SMTP" do + subject { Ronin::Support::Encoding::SMTP } + + it "must be an alias to Ronin::Support::Encoding::QuotedPrintable" do + expect(subject).to be(Ronin::Support::Encoding::QuotedPrintable) + end +end diff --git a/spec/network/defang/core_ext/ipaddr_spec.rb b/spec/network/defang/core_ext/ipaddr_spec.rb new file mode 100644 index 000000000..eeededc45 --- /dev/null +++ b/spec/network/defang/core_ext/ipaddr_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' +require 'ronin/support/network/defang/core_ext/ipaddr' + +describe IPAddr do + describe "#defang" do + subject { described_class.new('192.168.1.1') } + + let(:defanged) { '192[.]168[.]1[.]1' } + + it "must return the defanged IP address" do + expect(subject.defang).to eq(defanged) + end + end +end diff --git a/spec/network/defang/core_ext/string_spec.rb b/spec/network/defang/core_ext/string_spec.rb new file mode 100644 index 000000000..12888b0d9 --- /dev/null +++ b/spec/network/defang/core_ext/string_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' +require 'ronin/support/network/defang/core_ext/string' + +describe String do + describe "#defang" do + context "when given a defanged URL" do + subject { 'http://www.example.com/foo?q=1' } + + let(:defanged) { 'hxxp[://]www[.]example[.]com/foo?q=1' } + + it "must return the defanged URL" do + expect(subject.defang).to eq(defanged) + end + end + + context "when given a defanged IPv4 address" do + subject { '192.168.1.1' } + + let(:defanged) { '192[.]168[.]1[.]1' } + + it "must return the defanged IPv4 address" do + expect(subject.defang).to eq(defanged) + end + end + + context "when given a defanged IPv6 address" do + subject { '2606:2800:21f:cb07:6820:80da:af6b:8b2c' } + + let(:defanged) { '2606[:]2800[:]21f[:]cb07[:]6820[:]80da[:]af6b[:]8b2c' } + + it "must return the defanged IPv6 address" do + expect(subject.defang).to eq(defanged) + end + end + + context "when given a defanged host name" do + subject { 'www.example.com' } + + let(:defanged) { 'www[.]example[.]com' } + + it "must return the defanged host name" do + expect(subject.defang).to eq(defanged) + end + end + end + + describe "#refang" do + context "when the String is a defanged URL" do + subject { 'hxxp[://]www[.]example[.]com/foo?q=1' } + + let(:url) { 'http://www.example.com/foo?q=1' } + + it "must return the refanged URL" do + expect(subject.refang).to eq(url) + end + end + + context "when the String is a defanged IPv4 address" do + subject { '192[.]168[.]1[.]1' } + + let(:ip) { '192.168.1.1' } + + it "must return the refanged IPv4 address" do + expect(subject.refang).to eq(ip) + end + end + + context "when the String is a defanged IPv6 address" do + subject { '2606[:]2800[:]21f[:]cb07[:]6820[:]80da[:]af6b[:]8b2c' } + + let(:ip) { '2606:2800:21f:cb07:6820:80da:af6b:8b2c' } + + it "must return the refanged IPv6 address" do + expect(subject.refang).to eq(ip) + end + end + + context "when the String is a defanged host name" do + subject { 'www[.]example[.]com' } + + let(:host) { 'www.example.com' } + + it "must return the refanged host name" do + expect(subject.refang).to eq(host) + end + end + end +end diff --git a/spec/network/defang/core_ext/uri/http_spec.rb b/spec/network/defang/core_ext/uri/http_spec.rb new file mode 100644 index 000000000..badc23cff --- /dev/null +++ b/spec/network/defang/core_ext/uri/http_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' +require 'ronin/support/network/defang/core_ext/uri/http' + +describe URI::HTTP do + describe "#defang" do + subject { URI('http://www.example.com/foo?q=1') } + + let(:defanged) { 'hxxp[://]www[.]example[.]com/foo?q=1' } + + it "must return the defanged URL" do + expect(subject.defang).to eq(defanged) + end + end +end diff --git a/spec/network/defang/mixin_spec.rb b/spec/network/defang/mixin_spec.rb new file mode 100644 index 000000000..e6bfe73b1 --- /dev/null +++ b/spec/network/defang/mixin_spec.rb @@ -0,0 +1,274 @@ +require 'spec_helper' +require 'ronin/support/network/defang/mixin' + +describe Ronin::Support::Network::Defang::Mixin do + subject do + obj = Object.new + obj.extend described_class + obj + end + + describe "#defang_ip" do + context "when given an IPv4 address" do + let(:ip) { '192.168.1.1' } + let(:defanged) { '192[.]168[.]1[.]1' } + + it "must escape the '.' separators" do + expect(subject.defang_ip(ip)).to eq(defanged) + end + end + + context "when given an IPv6 address" do + let(:ip) { '2606:2800:21f:cb07:6820:80da:af6b:8b2c' } + let(:defanged) { '2606[:]2800[:]21f[:]cb07[:]6820[:]80da[:]af6b[:]8b2c' } + + it "must escape the ':' separators" do + expect(subject.defang_ip(ip)).to eq(defanged) + end + + context "and when the IPv6 address contains a '::' separator" do + let(:ip) { 'ffff:abcd::12' } + let(:defanged) { 'ffff[:]abcd[::]12' } + + it "must also escape the '::' separator as '[::]'" do + expect(subject.defang_ip(ip)).to eq(defanged) + end + end + end + end + + describe "#refang_ip" do + context "when given a defanged IPv4 address" do + let(:defanged) { '192[.]168[.]1[.]1' } + let(:ip) { '192.168.1.1' } + + it "must unescape the '[.]' separators" do + expect(subject.refang_ip(defanged)).to eq(ip) + end + end + + context "when given a defanged IPv6 address" do + let(:defanged) { '2606[:]2800[:]21f[:]cb07[:]6820[:]80da[:]af6b[:]8b2c' } + let(:ip) { '2606:2800:21f:cb07:6820:80da:af6b:8b2c' } + + it "must unescape the '[:]' separators" do + expect(subject.refang_ip(defanged)).to eq(ip) + end + + context "and when the IPv6 address contains an escaped '[::]' separator" do + let(:defanged) { 'ffff[:]abcd[::]12' } + let(:ip) { 'ffff:abcd::12' } + + it "must also unescape the '[::]' separator" do + expect(subject.refang_ip(defanged)).to eq(ip) + end + end + end + end + + describe "#defang_host" do + let(:host) { 'www.example.com' } + let(:defanged) { 'www[.]example[.]com' } + + it "must escape the '.' separators" do + expect(subject.defang_host(host)).to eq(defanged) + end + end + + describe "#refang_host" do + let(:defanged) { 'www[.]example[.]com' } + let(:host) { 'www.example.com' } + + it "must unescape the '[.]' separators" do + expect(subject.refang_host(defanged)).to eq(host) + end + end + + describe "#defang_url" do + context "when the URL starts with 'http://'" do + let(:url) { 'http://www.example.com/foo?q=1' } + let(:defanged) { 'hxxp[://]www[.]example[.]com/foo?q=1' } + + it "must replace `http://` with 'hxxp[://]'" do + expect(subject.defang_url(url)).to eq(defanged) + end + end + + context "when the URL starts with 'https://'" do + let(:url) { 'https://www.example.com/foo?q=1' } + let(:defanged) { 'hxxps[://]www[.]example[.]com/foo?q=1' } + + it "must replace `https://` with 'hxxps[://]'" do + expect(subject.defang_url(url)).to eq(defanged) + end + end + + context "when the URL contains a host name" do + let(:url) { 'https://www.example.com/foo?q=1' } + let(:defanged) { 'hxxps[://]www[.]example[.]com/foo?q=1' } + + it "must escape the '.' separators in the host name" do + expect(subject.defang_url(url)).to eq(defanged) + end + end + + context "when the URL contains an IPv4 address" do + let(:url) { 'https://192.168.1.1/foo?q=1' } + let(:defanged) { 'hxxps[://]192[.]168[.]1[.]1/foo?q=1' } + + it "must escape the '.' separators in the IPv4 address" do + expect(subject.defang_url(url)).to eq(defanged) + end + end + + context "when the URL contains an IPv6 address" do + let(:url) { "https://2606:2800:21f:cb07:6820:80da:af6b:8b2c/foo?q=1" } + let(:defanged) { 'hxxps[://]2606[:]2800[:]21f[:]cb07[:]6820[:]80da[:]af6b[:]8b2c/foo?q=1' } + + it "must escape the ':' separators in the IPv6 address" do + expect(subject.defang_url(url)).to eq(defanged) + end + + context "and the IPv6 address also contains a '::' separator" do + let(:url) { "https://ffff:abcd::12/foo?q=1" } + let(:defanged) { 'hxxps[://]ffff[:]abcd[::]12/foo?q=1' } + + it "must also escape the '::' separator as '[::]'" do + expect(subject.defang_url(url)).to eq(defanged) + end + end + end + end + + describe "#refang_url" do + context "when the URL starts with 'hxxp[://]'" do + let(:defanged) { 'hxxp[://]www[.]example[.]com/foo?q=1' } + let(:url) { 'http://www.example.com/foo?q=1' } + + it "must replace 'hxxp[://]' with 'httw[://]'" do + expect(subject.refang_url(defanged)).to eq(url) + end + end + + context "when the URL starts with 'hxxps[://]'" do + let(:defanged) { 'hxxps[://]www[.]example[.]com/foo?q=1' } + let(:url) { 'https://www.example.com/foo?q=1' } + + it "must replace `hxxps[://]` with 'https://'" do + expect(subject.refang_url(defanged)).to eq(url) + end + end + + context "when the URL contains a host name" do + let(:defanged) { 'hxxps[://]www[.]example[.]com/foo?q=1' } + let(:url) { 'https://www.example.com/foo?q=1' } + + it "must unescape the '[.]' separators in the host name" do + expect(subject.refang_url(defanged)).to eq(url) + end + end + + context "when the URL contains an IPv4 address" do + let(:defanged) { 'hxxps[://]192[.]168[.]1[.]1/foo?q=1' } + let(:url) { 'https://192.168.1.1/foo?q=1' } + + it "must unescape the '[.]' separators in the IPv4 address" do + expect(subject.refang_url(defanged)).to eq(url) + end + end + + context "when the URL contains an IPv6 address" do + let(:defanged) { 'hxxps[://]2606[:]2800[:]21f[:]cb07[:]6820[:]80da[:]af6b[:]8b2c/foo?q=1' } + let(:url) { "https://2606:2800:21f:cb07:6820:80da:af6b:8b2c/foo?q=1" } + + it "must unescape the '[:]' separators in the IPv6 address" do + expect(subject.refang_url(defanged)).to eq(url) + end + + context "and the IPv6 address also contains a '[::]' separator" do + let(:defanged) { 'hxxps[://]ffff[:]abcd[::]12/foo?q=1' } + let(:url) { "https://ffff:abcd::12/foo?q=1" } + + it "must also unescape the '[::]' separator as '::'" do + expect(subject.refang_url(defanged)).to eq(url) + end + end + end + end + + describe "#defang" do + context "when given a defanged URL" do + let(:url) { 'http://www.example.com/foo?q=1' } + let(:defanged) { 'hxxp[://]www[.]example[.]com/foo?q=1' } + + it "must return the defanged URL" do + expect(subject.defang(url)).to eq(defanged) + end + end + + context "when given a defanged IPv4 address" do + let(:ip) { '192.168.1.1' } + let(:defanged) { '192[.]168[.]1[.]1' } + + it "must return the defanged IPv4 address" do + expect(subject.defang(ip)).to eq(defanged) + end + end + + context "when given a defanged IPv6 address" do + let(:ip) { '2606:2800:21f:cb07:6820:80da:af6b:8b2c' } + let(:defanged) { '2606[:]2800[:]21f[:]cb07[:]6820[:]80da[:]af6b[:]8b2c' } + + it "must return the defanged IPv6 address" do + expect(subject.defang(ip)).to eq(defanged) + end + end + + context "when given a defanged host name" do + let(:host) { 'www.example.com' } + let(:defanged) { 'www[.]example[.]com' } + + it "must return the defanged host name" do + expect(subject.defang(host)).to eq(defanged) + end + end + end + + describe "#refang" do + context "when given a defanged URL" do + let(:defanged) { 'hxxp[://]www[.]example[.]com/foo?q=1' } + let(:url) { 'http://www.example.com/foo?q=1' } + + it "must return the refanged URL" do + expect(subject.refang(defanged)).to eq(url) + end + end + + context "when given a defanged IPv4 address" do + let(:defanged) { '192[.]168[.]1[.]1' } + let(:ip) { '192.168.1.1' } + + it "must return the refanged IPv4 address" do + expect(subject.refang(defanged)).to eq(ip) + end + end + + context "when given a defanged IPv6 address" do + let(:defanged) { '2606[:]2800[:]21f[:]cb07[:]6820[:]80da[:]af6b[:]8b2c' } + let(:ip) { '2606:2800:21f:cb07:6820:80da:af6b:8b2c' } + + it "must return the refanged IPv6 address" do + expect(subject.refang(defanged)).to eq(ip) + end + end + + context "when given a defanged host name" do + let(:defanged) { 'www[.]example[.]com' } + let(:host) { 'www.example.com' } + + it "must return the refanged host name" do + expect(subject.refang(defanged)).to eq(host) + end + end + end +end diff --git a/spec/network/defang_spec.rb b/spec/network/defang_spec.rb new file mode 100644 index 000000000..9b5c3fe3f --- /dev/null +++ b/spec/network/defang_spec.rb @@ -0,0 +1,268 @@ +require 'spec_helper' +require 'ronin/support/network/defang' + +describe Ronin::Support::Network::Defang do + describe ".defang_ip" do + context "when given an IPv4 address" do + let(:ip) { '192.168.1.1' } + let(:defanged) { '192[.]168[.]1[.]1' } + + it "must escape the '.' separators" do + expect(subject.defang_ip(ip)).to eq(defanged) + end + end + + context "when given an IPv6 address" do + let(:ip) { '2606:2800:21f:cb07:6820:80da:af6b:8b2c' } + let(:defanged) { '2606[:]2800[:]21f[:]cb07[:]6820[:]80da[:]af6b[:]8b2c' } + + it "must escape the ':' separators" do + expect(subject.defang_ip(ip)).to eq(defanged) + end + + context "and when the IPv6 address contains a '::' separator" do + let(:ip) { 'ffff:abcd::12' } + let(:defanged) { 'ffff[:]abcd[::]12' } + + it "must also escape the '::' separator as '[::]'" do + expect(subject.defang_ip(ip)).to eq(defanged) + end + end + end + end + + describe ".refang_ip" do + context "when given a defanged IPv4 address" do + let(:defanged) { '192[.]168[.]1[.]1' } + let(:ip) { '192.168.1.1' } + + it "must unescape the '[.]' separators" do + expect(subject.refang_ip(defanged)).to eq(ip) + end + end + + context "when given a defanged IPv6 address" do + let(:defanged) { '2606[:]2800[:]21f[:]cb07[:]6820[:]80da[:]af6b[:]8b2c' } + let(:ip) { '2606:2800:21f:cb07:6820:80da:af6b:8b2c' } + + it "must unescape the '[:]' separators" do + expect(subject.refang_ip(defanged)).to eq(ip) + end + + context "and when the IPv6 address contains an escaped '[::]' separator" do + let(:defanged) { 'ffff[:]abcd[::]12' } + let(:ip) { 'ffff:abcd::12' } + + it "must also unescape the '[::]' separator" do + expect(subject.refang_ip(defanged)).to eq(ip) + end + end + end + end + + describe ".defang_host" do + let(:host) { 'www.example.com' } + let(:defanged) { 'www[.]example[.]com' } + + it "must escape the '.' separators" do + expect(subject.defang_host(host)).to eq(defanged) + end + end + + describe ".refang_host" do + let(:defanged) { 'www[.]example[.]com' } + let(:host) { 'www.example.com' } + + it "must unescape the '[.]' separators" do + expect(subject.refang_host(defanged)).to eq(host) + end + end + + describe ".defang_url" do + context "when the URL starts with 'http://'" do + let(:url) { 'http://www.example.com/foo?q=1' } + let(:defanged) { 'hxxp[://]www[.]example[.]com/foo?q=1' } + + it "must replace `http://` with 'hxxp[://]'" do + expect(subject.defang_url(url)).to eq(defanged) + end + end + + context "when the URL starts with 'https://'" do + let(:url) { 'https://www.example.com/foo?q=1' } + let(:defanged) { 'hxxps[://]www[.]example[.]com/foo?q=1' } + + it "must replace `https://` with 'hxxps[://]'" do + expect(subject.defang_url(url)).to eq(defanged) + end + end + + context "when the URL contains a host name" do + let(:url) { 'https://www.example.com/foo?q=1' } + let(:defanged) { 'hxxps[://]www[.]example[.]com/foo?q=1' } + + it "must escape the '.' separators in the host name" do + expect(subject.defang_url(url)).to eq(defanged) + end + end + + context "when the URL contains an IPv4 address" do + let(:url) { 'https://192.168.1.1/foo?q=1' } + let(:defanged) { 'hxxps[://]192[.]168[.]1[.]1/foo?q=1' } + + it "must escape the '.' separators in the IPv4 address" do + expect(subject.defang_url(url)).to eq(defanged) + end + end + + context "when the URL contains an IPv6 address" do + let(:url) { "https://2606:2800:21f:cb07:6820:80da:af6b:8b2c/foo?q=1" } + let(:defanged) { 'hxxps[://]2606[:]2800[:]21f[:]cb07[:]6820[:]80da[:]af6b[:]8b2c/foo?q=1' } + + it "must escape the ':' separators in the IPv6 address" do + expect(subject.defang_url(url)).to eq(defanged) + end + + context "and the IPv6 address also contains a '::' separator" do + let(:url) { "https://ffff:abcd::12/foo?q=1" } + let(:defanged) { 'hxxps[://]ffff[:]abcd[::]12/foo?q=1' } + + it "must also escape the '::' separator as '[::]'" do + expect(subject.defang_url(url)).to eq(defanged) + end + end + end + end + + describe ".refang_url" do + context "when the URL starts with 'hxxp[://]'" do + let(:defanged) { 'hxxp[://]www[.]example[.]com/foo?q=1' } + let(:url) { 'http://www.example.com/foo?q=1' } + + it "must replace 'hxxp[://]' with 'httw[://]'" do + expect(subject.refang_url(defanged)).to eq(url) + end + end + + context "when the URL starts with 'hxxps[://]'" do + let(:defanged) { 'hxxps[://]www[.]example[.]com/foo?q=1' } + let(:url) { 'https://www.example.com/foo?q=1' } + + it "must replace `hxxps[://]` with 'https://'" do + expect(subject.refang_url(defanged)).to eq(url) + end + end + + context "when the URL contains a host name" do + let(:defanged) { 'hxxps[://]www[.]example[.]com/foo?q=1' } + let(:url) { 'https://www.example.com/foo?q=1' } + + it "must unescape the '[.]' separators in the host name" do + expect(subject.refang_url(defanged)).to eq(url) + end + end + + context "when the URL contains an IPv4 address" do + let(:defanged) { 'hxxps[://]192[.]168[.]1[.]1/foo?q=1' } + let(:url) { 'https://192.168.1.1/foo?q=1' } + + it "must unescape the '[.]' separators in the IPv4 address" do + expect(subject.refang_url(defanged)).to eq(url) + end + end + + context "when the URL contains an IPv6 address" do + let(:defanged) { 'hxxps[://]2606[:]2800[:]21f[:]cb07[:]6820[:]80da[:]af6b[:]8b2c/foo?q=1' } + let(:url) { "https://2606:2800:21f:cb07:6820:80da:af6b:8b2c/foo?q=1" } + + it "must unescape the '[:]' separators in the IPv6 address" do + expect(subject.refang_url(defanged)).to eq(url) + end + + context "and the IPv6 address also contains a '[::]' separator" do + let(:defanged) { 'hxxps[://]ffff[:]abcd[::]12/foo?q=1' } + let(:url) { "https://ffff:abcd::12/foo?q=1" } + + it "must also unescape the '[::]' separator as '::'" do + expect(subject.refang_url(defanged)).to eq(url) + end + end + end + end + + describe ".defang" do + context "when given a defanged URL" do + let(:url) { 'http://www.example.com/foo?q=1' } + let(:defanged) { 'hxxp[://]www[.]example[.]com/foo?q=1' } + + it "must return the defanged URL" do + expect(subject.defang(url)).to eq(defanged) + end + end + + context "when given a defanged IPv4 address" do + let(:ip) { '192.168.1.1' } + let(:defanged) { '192[.]168[.]1[.]1' } + + it "must return the defanged IPv4 address" do + expect(subject.defang(ip)).to eq(defanged) + end + end + + context "when given a defanged IPv6 address" do + let(:ip) { '2606:2800:21f:cb07:6820:80da:af6b:8b2c' } + let(:defanged) { '2606[:]2800[:]21f[:]cb07[:]6820[:]80da[:]af6b[:]8b2c' } + + it "must return the defanged IPv6 address" do + expect(subject.defang(ip)).to eq(defanged) + end + end + + context "when given a defanged host name" do + let(:host) { 'www.example.com' } + let(:defanged) { 'www[.]example[.]com' } + + it "must return the defanged host name" do + expect(subject.defang(host)).to eq(defanged) + end + end + end + + describe ".refang" do + context "when given a defanged URL" do + let(:defanged) { 'hxxp[://]www[.]example[.]com/foo?q=1' } + let(:url) { 'http://www.example.com/foo?q=1' } + + it "must return the refanged URL" do + expect(subject.refang(defanged)).to eq(url) + end + end + + context "when given a defanged IPv4 address" do + let(:defanged) { '192[.]168[.]1[.]1' } + let(:ip) { '192.168.1.1' } + + it "must return the refanged IPv4 address" do + expect(subject.refang(defanged)).to eq(ip) + end + end + + context "when given a defanged IPv6 address" do + let(:defanged) { '2606[:]2800[:]21f[:]cb07[:]6820[:]80da[:]af6b[:]8b2c' } + let(:ip) { '2606:2800:21f:cb07:6820:80da:af6b:8b2c' } + + it "must return the refanged IPv6 address" do + expect(subject.refang(defanged)).to eq(ip) + end + end + + context "when given a defanged host name" do + let(:defanged) { 'www[.]example[.]com' } + let(:host) { 'www.example.com' } + + it "must return the refanged host name" do + expect(subject.refang(defanged)).to eq(host) + end + end + end +end diff --git a/spec/network/host_spec.rb b/spec/network/host_spec.rb index 792843919..2a0812d93 100644 --- a/spec/network/host_spec.rb +++ b/spec/network/host_spec.rb @@ -18,12 +18,37 @@ subject { described_class.new(hostname) } + describe "REGEX" do + subject { described_class::REGEX } + + it "must match a local hostname" do + expect(subject =~ 'localhost').to be_truthy + end + + it "must match a domain name" do + expect(subject =~ 'example.com').to be_truthy + end + + it "must match a sub-domain name" do + expect(subject =~ 'www.example.com').to be_truthy + end + end + describe "#initialize" do it "must set #name" do expect(subject.name).to eq(hostname) end end + describe "#defang" do + let(:hostname) { 'www.example.com' } + let(:defanged) { 'www[.]example[.]com' } + + it "must return the defanged host name" do + expect(subject.defang).to eq(defanged) + end + end + let(:unicode_hostname) { "www.詹姆斯.com" } let(:punycode_hostname) { 'www.xn--8ws00zhy3a.com' } diff --git a/spec/network/ip_spec.rb b/spec/network/ip_spec.rb index ecbcc2487..8210cd56e 100644 --- a/spec/network/ip_spec.rb +++ b/spec/network/ip_spec.rb @@ -536,6 +536,16 @@ end end + describe "#defang" do + subject { described_class.new('192.168.1.1') } + + let(:defanged) { '192[.]168[.]1[.]1' } + + it "must return the defanged IP address" do + expect(subject.defang).to eq(defanged) + end + end + describe "#broadcast?" do context "when the IP is an IPv4 adress" do context "and the IP address is 255.255.255.255" do diff --git a/spec/network/mixin_spec.rb b/spec/network/mixin_spec.rb index d095d9f7c..8c1ed5533 100644 --- a/spec/network/mixin_spec.rb +++ b/spec/network/mixin_spec.rb @@ -29,4 +29,8 @@ it "must include `Ronin::Support::Network::HTTP::Mixin`" do expect(subject).to include(Ronin::Support::Network::HTTP::Mixin) end + + it "must include `Ronin::Support::Network::Defang::Mixin`" do + expect(subject).to include(Ronin::Support::Network::Defang::Mixin) + end end diff --git a/spec/network/url_spec.rb b/spec/network/url_spec.rb new file mode 100644 index 000000000..e2a82f30d --- /dev/null +++ b/spec/network/url_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' +require 'ronin/support/network/url' + +require 'webmock/rspec' + +describe Ronin::Support::Network::URL do + it "must inherit from Addressable::URI" do + expect(described_class).to be < Addressable::URI + end + + it "must include URI::QueryParams::Mixin" do + expect(described_class).to include(URI::QueryParams::Mixin) + end + + let(:url) { 'https://example.com/' } + + subject { described_class.parse(url) } + + describe "REGEX" do + subject { described_class::REGEX } + + it "must match a http:// URL" do + expect(subject =~ 'http://example.com/').to be_truthy + end + + it "must match a https:// URL" do + expect(subject =~ 'https://example.com/').to be_truthy + end + + it "must match a http(s):// URL with an IP address for a host name" do + expect(subject =~ 'http://[192.168.1.1]/').to be_truthy + expect(subject =~ 'https://[192.168.1.1]/').to be_truthy + end + + it "must match a http(s):// URL with a path" do + expect(subject =~ 'http://example.com/foo/index.html').to be_truthy + expect(subject =~ 'https://example.com/foo/index.html').to be_truthy + end + + it "must match a http(s):// URL with a query string" do + expect(subject =~ 'http://example.com/?q=1').to be_truthy + expect(subject =~ 'https://example.com/?q=1').to be_truthy + end + + it "must match a http(s):// URL with a fragment" do + expect(subject =~ 'http://example.com/#foo').to be_truthy + expect(subject =~ 'https://example.com/#foo').to be_truthy + end + + it "must match a URI with an arbitrary scheme" do + expect(subject =~ 'foo:').to be_truthy + end + end + + describe "#defang" do + let(:url) { 'http://www.example.com/foo?q=1' } + let(:defanged) { 'hxxp[://]www[.]example[.]com/foo?q=1' } + + it "must return the defanged URL" do + expect(subject.defang).to eq(defanged) + end + end + + describe "#status" do + context "integration", :network do + before(:all) { WebMock.allow_net_connect! } + + it "must request the HTTP status for the URI" do + expect(subject.status).to eq(200) + end + end + end + + describe "#ok?" do + context "integration", :network do + before(:all) { WebMock.allow_net_connect! } + + context "when the URI returns has a HTTP 200 response" do + it "must return true" do + expect(subject.ok?).to be(true) + end + end + + context "when the URI does not return a HTTP 200 response" do + subject { described_class.parse('https://example.com/foo') } + + it "must return false" do + expect(subject.ok?).to be(false) + end + end + end + end +end diff --git a/spec/network/wildcard_spec.rb b/spec/network/wildcard_spec.rb index 2299bc39a..63e4eeacd 100644 --- a/spec/network/wildcard_spec.rb +++ b/spec/network/wildcard_spec.rb @@ -10,6 +10,38 @@ it "must set #template" do expect(subject.template).to eq(wildcard) end + + context "when the wildcard template starts with a '*' character" do + let(:wildcard) { '*.example.com' } + + it "must initialize #regex to a Regexp that matches the suffix of the wildcard template" do + expect(subject.regex).to eq(/\A(.*?)\.example\.com\z/) + end + end + + context "when the wildcard template includes a '*' character" do + let(:wildcard) { 'www.*.example.com' } + + it "must initialize #regex to a Regexp that matches both the prefix and suffix of the wildcard template" do + expect(subject.regex).to eq(/\Awww\.(.*?)\.example\.com\z/) + end + end + + context "when the wildcard template ends with a '*' character" do + let(:wildcard) { 'www.example.*' } + + it "must initialize #regex to a Regexp that matches the prefix of the wildcard template" do + expect(subject.regex).to eq(/\Awww\.example\.(.*?)\z/) + end + end + + context "when the wildcard template does not include a '*' character" do + let(:wildcard) { 'www.example.com' } + + it "must initialize #regex to a Regexp that matches the whole wildcard template string" do + expect(subject.regex).to eq(/\Awww\.example\.com\z/) + end + end end describe "#subdomain" do @@ -23,6 +55,96 @@ end end + describe "#===" do + context "when the wildcard template starts with a '*' character" do + let(:wildcard) { '*.example.com' } + + context "and when the given hostname ends with the wildcard template string" do + let(:host) { 'www.example.com' } + + it "must return true" do + expect(subject === host).to be(true) + end + end + + context "but the given hostname does not end with the wildcard template string" do + let(:host) { 'www.example.co.uk' } + + it "must return false" do + expect(subject === host).to be(false) + end + end + end + + context "when the wildcard template includes a '*' character" do + let(:wildcard) { 'www.*.example.com' } + + context "and when the given hostname starts with and ends with the wildcard template prefix and suffix" do + let(:host) { 'www.foo.example.com' } + + it "must return true" do + expect(subject === host).to be(true) + end + end + + context "and when the given hostname does not start with the wildcard template prefix" do + let(:host) { 'foo.bar.example.com' } + + it "must return false" do + expect(subject === host).to be(false) + end + end + + context "and when the given hostname does not end with the wildcard template suffix" do + let(:host) { 'www.foo.example.co.uk' } + + it "must return false" do + expect(subject === host).to be(false) + end + end + end + + context "when the wildcard template ends with a '*' character" do + let(:wildcard) { 'www.example.*' } + + context "and when the given hostname does start with the wildcard template prefix" do + let(:host) { 'www.example.co.uk' } + + it "must return true" do + expect(subject === host).to be(true) + end + end + + context "but when the given hostname does not start with the wildcard template prefix" do + let(:host) { 'foo.example.com' } + + it "must return false" do + expect(subject === host).to be(false) + end + end + end + + context "when the wildcard template does not include a '*' character" do + let(:wildcard) { 'www.example.com' } + + context "and when the given hostname matches the whole wildcard template string" do + let(:host) { wildcard } + + it "must return true" do + expect(subject === host).to be(true) + end + end + + context "but when the given hostname does not match the whole wildcard template string" do + let(:host) { "foo.example.com" } + + it "must return false" do + expect(subject === host).to be(false) + end + end + end + end + describe "#to_s" do it "must return the wildcard String" do expect(subject.to_s).to eq(wildcard) diff --git a/spec/software/version_constraint_spec.rb b/spec/software/version_constraint_spec.rb new file mode 100644 index 000000000..5bf5e2150 --- /dev/null +++ b/spec/software/version_constraint_spec.rb @@ -0,0 +1,315 @@ +require 'spec_helper' +require 'ronin/support/software/version_constraint' + +describe Ronin::Support::Software::VersionConstraint do + let(:operator) { '>=' } + let(:version) { '1.2.3' } + let(:string) { "#{operator} #{version}" } + + subject { described_class.new(string) } + + describe "#initialize" do + it "must set #string to the given version constraint string" do + expect(subject.string).to eq(string) + end + + it "must parse and set #version to a new Ronin::Support::Software::Version object using the version string within the version constraint string" do + expect(subject.version).to be_kind_of(Ronin::Support::Software::Version) + expect(subject.version.string).to eq(version) + end + + context "when the version constraint string starts with '>='" do + let(:operator) { '>=' } + + it "must parse and set #operator to '>='" do + expect(subject.operator).to eq(operator) + end + end + + context "when the version constraint string starts with '>'" do + let(:operator) { '>' } + + it "must parse and set #operator to '>'" do + expect(subject.operator).to eq(operator) + end + end + + context "when the version constraint string starts with '<='" do + let(:operator) { '<=' } + + it "must parse and set #operator to '<='" do + expect(subject.operator).to eq(operator) + end + end + + context "when the version constraint string starts with '<'" do + let(:operator) { '<' } + + it "must parse and set #operator to '<'" do + expect(subject.operator).to eq(operator) + end + end + + context "when the version constraint string starts with '='" do + let(:operator) { '=' } + + it "must parse and set #operator to '='" do + expect(subject.operator).to eq(operator) + end + end + + context "but the version constraint string does not start with any operator" do + let(:string) { version } + + it "must default #operator to '='" do + expect(subject.operator).to eq('=') + end + end + + context "but there are no spaces between the operator and the version" do + let(:string) { "#{operator}#{version}" } + + it "must still parse and set #operator" do + expect(subject.operator).to eq(operator) + end + + it "must still parse and set #version" do + expect(subject.version).to be_kind_of(Ronin::Support::Software::Version) + expect(subject.version.string).to eq(version) + end + end + + context "but the version constraint string is malformed" do + let(:string) { '' } + + it "must raise an ArgumentError exception" do + expect { + described_class.new(string) + }.to raise_error(ArgumentError,"invalid version constraint: #{string.inspect}") + end + end + end + + describe ".parse" do + subject { described_class.parse(string) } + + it "must return a new #{described_class} with the given version constraint string" do + expect(subject).to be_kind_of(described_class) + expect(subject.string).to eq(string) + end + end + + describe "#include?" do + let(:lesser_version) { '1.2.2' } + let(:greater_version) { '1.2.4' } + + context "when the #operator is '>'" do + let(:operator) { '>' } + + context "and the given version is greater than #version" do + let(:other_version) do + Ronin::Support::Software::Version.new(greater_version) + end + + it "must return true" do + expect(subject.include?(other_version)).to be(true) + end + end + + context "but the given version is equal to #version" do + let(:other_version) do + Ronin::Support::Software::Version.new(version) + end + + it "must return false" do + expect(subject.include?(other_version)).to be(false) + end + end + + context "but the given version is less than #version" do + let(:other_version) do + Ronin::Support::Software::Version.new(lesser_version) + end + + it "must return false" do + expect(subject.include?(other_version)).to be(false) + end + end + end + + context "when the #operator is '>='" do + let(:operator) { '>=' } + + context "and the given version is greater than #version" do + let(:other_version) do + Ronin::Support::Software::Version.new(greater_version) + end + + it "must return true" do + expect(subject.include?(other_version)).to be(true) + end + end + + context "and the given version is equal to #version" do + let(:other_version) do + Ronin::Support::Software::Version.new(version) + end + + it "must return true" do + expect(subject.include?(other_version)).to be(true) + end + end + + context "but the given version is less than #version" do + let(:other_version) do + Ronin::Support::Software::Version.new(lesser_version) + end + + it "must return false" do + expect(subject.include?(other_version)).to be(false) + end + end + end + + context "when the #operator is '<'" do + let(:operator) { '<' } + + context "but the given version is greater than #version" do + let(:other_version) do + Ronin::Support::Software::Version.new(greater_version) + end + + it "must return false" do + expect(subject.include?(other_version)).to be(false) + end + end + + context "but the given version is equal to #version" do + let(:other_version) do + Ronin::Support::Software::Version.new(version) + end + + it "must return false" do + expect(subject.include?(other_version)).to be(false) + end + end + + context "and the given version is less than #version" do + let(:other_version) do + Ronin::Support::Software::Version.new(lesser_version) + end + + it "must return true" do + expect(subject.include?(other_version)).to be(true) + end + end + end + + context "when the #operator is '<='" do + let(:operator) { '<=' } + + context "but the given version is greater than #version" do + let(:other_version) do + Ronin::Support::Software::Version.new(greater_version) + end + + it "must return false" do + expect(subject.include?(other_version)).to be(false) + end + end + + context "and the given version is equal to #version" do + let(:other_version) do + Ronin::Support::Software::Version.new(version) + end + + it "must return true" do + expect(subject.include?(other_version)).to be(true) + end + end + + context "and the given version is less than #version" do + let(:other_version) do + Ronin::Support::Software::Version.new(lesser_version) + end + + it "must return true" do + expect(subject.include?(other_version)).to be(true) + end + end + end + + context "when the #operator is '='" do + let(:operator) { '=' } + + context "and the given version is equal to #version" do + let(:other_version) do + Ronin::Support::Software::Version.new(version) + end + + it "must return true" do + expect(subject.include?(other_version)).to be(true) + end + end + + context "but the given version is less than #version" do + let(:other_version) do + Ronin::Support::Software::Version.new(lesser_version) + end + + it "must return false" do + expect(subject.include?(other_version)).to be(false) + end + end + + context "but the given version is greater than #version" do + let(:other_version) do + Ronin::Support::Software::Version.new(greater_version) + end + + it "must return false" do + expect(subject.include?(other_version)).to be(false) + end + end + end + end + + describe "#==" do + context "when given another #{described_class}" do + let(:other_operator) { operator } + let(:other_version) { version } + let(:other_string) { "#{other_operator} #{other_version}" } + let(:other) { described_class.new(other_string) } + + context "and the #operator and #version are the same" do + it "must return true" do + expect(subject == other).to be(true) + end + end + + context "but the #operator is different" do + let(:other_operator) { '>' } + + it "must return false" do + expect(subject == other).to be(false) + end + end + + context "but the #version is different" do + let(:other_version) { '0.0.0' } + + it "must return false" do + expect(subject == other).to be(false) + end + end + end + + context "when given another kind of object" do + let(:other) { Object.new } + + it "must return false" do + expect(subject == other).to be(false) + end + end + end +end diff --git a/spec/software/version_range_spec.rb b/spec/software/version_range_spec.rb new file mode 100644 index 000000000..b920f24d5 --- /dev/null +++ b/spec/software/version_range_spec.rb @@ -0,0 +1,96 @@ +require 'spec_helper' +require 'ronin/support/software/version_range' + +describe Ronin::Support::Software::VersionRange do + let(:constraint1) { '>= 1.2.3' } + let(:constraint2) { '< 2.0.0' } + let(:string) { "#{constraint1}, #{constraint2}" } + + subject { described_class.new(string) } + + describe "#initialize" do + it "must set #string to the given version range string" do + expect(subject.string).to eq(string) + end + + it "must parse and populate #constraints" do + expect(subject.constraints).to be_kind_of(Array) + expect(subject.constraints.length).to eq(2) + expect(subject.constraints[0]).to be_kind_of(Ronin::Support::Software::VersionConstraint) + expect(subject.constraints[0].string).to eq(constraint1) + expect(subject.constraints[1]).to be_kind_of(Ronin::Support::Software::VersionConstraint) + expect(subject.constraints[1].string).to eq(constraint2) + end + end + + describe ".parse" do + subject { described_class.parse(string) } + + it "must return a new #{described_class} with the given version range string" do + expect(subject).to be_kind_of(described_class) + expect(subject.string).to eq(string) + end + end + + describe "#include?" do + context "when the given version satisfies all of the version constraints" do + let(:other_version) do + Ronin::Support::Software::Version.new('1.10.0') + end + + it "must return true" do + expect(subject.include?(other_version)).to be(true) + end + end + + context "when the given version does not satisfy all of the version constraints" do + let(:other_version) do + Ronin::Support::Software::Version.new('2.0.1') + end + + it "must return false" do + expect(subject.include?(other_version)).to be(false) + end + end + end + + describe "#==" do + context "when given another #{described_class}" do + context "and all of the other #constraints are equal" do + let(:other) { described_class.new(string) } + + it "must return true" do + expect(subject == other).to be(true) + end + end + + context "but one of the constraints is different" do + let(:other_constraint1) { constraint1 } + let(:other_constraint2) { '< 2.0.1' } + let(:other_string) { "#{other_constraint1}, #{other_constraint2}" } + let(:other) { described_class.new(other_string) } + + it "must return false" do + expect(subject == other).to be(false) + end + end + + context "but the other #{described_class} has fewer version constraints" do + let(:other_string) { ">= 1.2.3" } + let(:other) { described_class.new(other_string) } + + it "must return false" do + expect(subject == other).to be(false) + end + end + end + + context "when given another kind of object" do + let(:other) { Object.new } + + it "must return false" do + expect(subject == other).to be(false) + end + end + end +end diff --git a/spec/software/version_spec.rb b/spec/software/version_spec.rb new file mode 100644 index 000000000..695184c7c --- /dev/null +++ b/spec/software/version_spec.rb @@ -0,0 +1,632 @@ +require 'spec_helper' +require 'ronin/support/software/version' + +describe Ronin::Support::Software::Version do + let(:version) { '1.2.3' } + + subject { described_class.new(version) } + + describe "#initialize" do + it "must initialize #string" do + expect(subject.string).to eq(version) + end + + it "must parse the version string and populate #parts" do + expect(subject.parts).to eq([1, 2, 3]) + end + end + + describe ".parse" do + subject { described_class.parse(version) } + + it "must return a new #{described_class} with the given version" do + expect(subject).to be_kind_of(described_class) + expect(subject.string).to eq(version) + end + end + + describe "#parts" do + context "when the version string is of the form 'XYZ'" do + let(:version) { '1234' } + + it "must contain a single Integer" do + expect(subject.parts).to eq([version.to_i]) + end + end + + context "when the version string is of the form 'X.Y'" do + let(:version) { '1.2' } + + it "must contain two Integers" do + expect(subject.parts).to eq([1, 2]) + end + end + + context "when the version string is of the form 'X-Y'" do + let(:version) { '1-2' } + + it "must contain two Integers" do + expect(subject.parts).to eq([1, 2]) + end + end + + context "when the version string is of the form 'X_Y'" do + let(:version) { '1_2' } + + it "must contain two Integers" do + expect(subject.parts).to eq([1, 2]) + end + end + + context "when the version string is of the form 'X.Y.Z'" do + let(:version) { '1.2.3' } + + it "must contain three Integers" do + expect(subject.parts).to eq([1, 2, 3]) + end + end + + context "when the version string is of the form 'X.Y-Z'" do + let(:version) { '1.2-3' } + + it "must contain three Integers" do + expect(subject.parts).to eq([1, 2, 3]) + end + end + + context "when the version string is of the form 'X-Y.Z'" do + let(:version) { '1-2.3' } + + it "must contain three Integers" do + expect(subject.parts).to eq([1, 2, 3]) + end + end + + context "when the version string is of the form 'X-Y-Z'" do + let(:version) { '1-2-3' } + + it "must contain three Integers" do + expect(subject.parts).to eq([1, 2, 3]) + end + end + + context "when the version string is of the form 'X.Y_Z'" do + let(:version) { '1.2_3' } + + it "must contain three Integers" do + expect(subject.parts).to eq([1, 2, 3]) + end + end + + context "when the version string is of the form 'X_Y.Z'" do + let(:version) { '1_2.3' } + + it "must contain three Integers" do + expect(subject.parts).to eq([1, 2, 3]) + end + end + + context "when the version string is of the form 'X_Y_Z'" do + let(:version) { '1_2_3' } + + it "must contain three Integers" do + expect(subject.parts).to eq([1, 2, 3]) + end + end + + context "when the version string is of the form 'X.Y.Z.W'" do + let(:version) { '1.2.3.4' } + + it "must contain four Integers" do + expect(subject.parts).to eq([1, 2, 3, 4]) + end + end + + context "when the version string is of the form 'X.Y.Z-W'" do + let(:version) { '1.2.3-4' } + + it "must contain four Integers" do + expect(subject.parts).to eq([1, 2, 3, 4]) + end + end + + context "when the version string is of the form 'X.Y-Z.W'" do + let(:version) { '1.2-3.4' } + + it "must contain four Integers" do + expect(subject.parts).to eq([1, 2, 3, 4]) + end + end + + context "when the version string is of the form 'X-Y.Z.W'" do + let(:version) { '1-2.3.4' } + + it "must contain four Integers" do + expect(subject.parts).to eq([1, 2, 3, 4]) + end + end + + context "when the version string is of the form 'X-Y.Z-W'" do + let(:version) { '1-2.3-4' } + + it "must contain four Integers" do + expect(subject.parts).to eq([1, 2, 3, 4]) + end + end + + context "when the version string is of the form 'X-Y-Z.W'" do + let(:version) { '1-2-3.4' } + + it "must contain four Integers" do + expect(subject.parts).to eq([1, 2, 3, 4]) + end + end + + context "when the version string is of the form 'X-Y-Z-W'" do + let(:version) { '1-2-3-4' } + + it "must contain four Integers" do + expect(subject.parts).to eq([1, 2, 3, 4]) + end + end + + context "when the version string is of the form 'X.Y.Z_W'" do + let(:version) { '1.2.3_4' } + + it "must contain four Integers" do + expect(subject.parts).to eq([1, 2, 3, 4]) + end + end + + context "when the version string is of the form 'X.Y_Z.W'" do + let(:version) { '1.2_3.4' } + + it "must contain four Integers" do + expect(subject.parts).to eq([1, 2, 3, 4]) + end + end + + context "when the version string is of the form 'X_Y.Z.W'" do + let(:version) { '1_2.3.4' } + + it "must contain four Integers" do + expect(subject.parts).to eq([1, 2, 3, 4]) + end + end + + context "when the version string is of the form 'X_Y.Z_W'" do + let(:version) { '1_2.3_4' } + + it "must contain four Integers" do + expect(subject.parts).to eq([1, 2, 3, 4]) + end + end + + context "when the version string is of the form 'X_Y_Z.W'" do + let(:version) { '1_2_3.4' } + + it "must contain four Integers" do + expect(subject.parts).to eq([1, 2, 3, 4]) + end + end + + context "when the version string is of the form 'X_Y_Z_W'" do + let(:version) { '1_2_3_4' } + + it "must contain four Integers" do + expect(subject.parts).to eq([1, 2, 3, 4]) + end + end + + context "when one of the version numbers contains a letter" do + let(:version) { '1.2.3a' } + + it "must parse the version 'number' containing a letter as a String" do + expect(subject.parts).to eq([1, 2, '3a']) + end + end + + context "when one of the version 'numbers' only contains letters" do + let(:version) { '1.2.3.abc' } + + it "must parse the version 'number' only containing letters as a String" do + expect(subject.parts).to eq([1, 2, 3, 'abc']) + end + end + + context "when one of the version numbers starts with a '.p' prefix" do + let(:version) { '1.2.3.p4' } + + it "must omit the '.p' prefix and parse the number" do + expect(subject.parts).to eq([1, 2, 3, 4]) + end + end + + context "when one of the version numbers starts with a '-p' prefix" do + let(:version) { '1.2.3-p4' } + + it "must omit the '-p' prefix and parse the number" do + expect(subject.parts).to eq([1, 2, 3, 4]) + end + end + + [:pre, :alpha, :beta, :rc].each do |modifier| + context "when the version string ends with '-#{modifier}'" do + let(:modifier) { modifier } + let(:version) { "1.2.3-#{modifier}" } + + it "must parse the '-#{modifier}' as the #{modifier.inspect} Symbol" do + expect(subject.parts).to eq([1, 2, 3, modifier]) + end + end + + context "when the version string ends with '.#{modifier}'" do + let(:modifier) { modifier } + let(:version) { "1.2.3.#{modifier}" } + + it "must parse the '.#{modifier}' as the #{modifier.inspect} Symbol" do + expect(subject.parts).to eq([1, 2, 3, modifier]) + end + end + + context "when the version string ends with '-#{modifier}N'" do + let(:modifier) { modifier } + let(:version) { "1.2.3-#{modifier}4" } + + it "must parse the '-#{modifier}N' as the #{modifier.inspect} Symbol followed by the Integer N" do + expect(subject.parts).to eq([1, 2, 3, modifier, 4]) + end + end + + context "when the version string ends with '.#{modifier}N'" do + let(:modifier) { modifier } + let(:version) { "1.2.3.#{modifier}4" } + + it "must parse the '.#{modifier}N' as the #{modifier.inspect} Symbol followed by the Integer N" do + expect(subject.parts).to eq([1, 2, 3, modifier, 4]) + end + end + end + + context "when the version string ends with '+XXX'" do + let(:version) { '1.2.3+1a2b3c' } + + it "must ignore everything after the '+' character" do + expect(subject.parts).to eq([1, 2, 3]) + end + end + end + + describe "#<=>" do + let(:other) { described_class.new(other_version) } + + context "when the version string equals the other version string " do + let(:other_version) { version } + + it "must return 0 (indicating they are equal)" do + expect(subject <=> other).to eq(0) + end + end + + context "when the version string is different from the other version string" do + context "but only the deliminators are different" do + let(:version) { '1.2.3' } + let(:other_version) { '1.2-3' } + + it "must return 0 (indicating they are equal)" do + expect(subject <=> other).to eq(0) + end + end + end + + context "when a version number in the other version string is greater" do + let(:version) { '1.2.3' } + let(:other_version) { '1.2.4' } + + it "must return -1 (indicating the other version is greater)" do + expect(subject <=> other).to eq(-1) + end + end + + context "when a version number in the other version string is less than the version number" do + let(:version) { '1.2.3' } + let(:other_version) { '1.2.2' } + + it "must return 1 (indicating the other version is lesser)" do + expect(subject <=> other).to eq(1) + end + end + + context "when the version contains a version modifier (pre, alpha, beta, rc)" do + let(:version) { '1.2.3.alpha' } + let(:other_version) { '1.2.3' } + + it "must return -1 (indicating the version is less)" do + expect(subject <=> other).to eq(-1) + end + + context "but the other version numbers contains letters instead of a version modifier" do + let(:other_version) { '1.2.3a' } + + it "must return -1 (indicating the version is less)" do + expect(subject <=> other).to eq(-1) + end + end + end + + context "when the other version contains a version modifier (pre, alpha, beta, rc)" do + let(:version) { '1.2.3' } + let(:other_version) { '1.2.3.alpha' } + + it "must return 1 (indicating the version is greater)" do + expect(subject <=> other).to eq(1) + end + + context "but the version numbers contains letters instead of a version modifier" do + let(:version) { '1.2.3a' } + + it "must return 1 (indicating the version is greater)" do + expect(subject <=> other).to eq(1) + end + end + end + + context "when both of the versions contain a version modifier (pre, alpha, beta, rc)" do + context "and the version contains 'pre'" do + let(:version) { '1.2.3.pre' } + + context "and the other version contains 'pre'" do + let(:other_version) { '1.2.3.pre' } + + it "must return 0 (indicating the versions are equal)" do + expect(subject <=> other).to eq(0) + end + end + + context "but the other version contains 'alpha'" do + let(:other_version) { '1.2.3.alpha' } + + it "must return -1 (indicating the version is less)" do + expect(subject <=> other).to eq(-1) + end + end + + context "but the other version contains 'beta'" do + let(:other_version) { '1.2.3.beta' } + + it "must return -1 (indicating the version is less)" do + expect(subject <=> other).to eq(-1) + end + end + + context "but the other version contains 'rc'" do + let(:other_version) { '1.2.3.rc' } + + it "must return -1 (indicating the version is less)" do + expect(subject <=> other).to eq(-1) + end + end + end + + context "and the version contains 'alpha'" do + let(:version) { '1.2.3.alpha' } + + context "and the other version contains 'pre'" do + let(:other_version) { '1.2.3.pre' } + + it "must return 1 (indicating the version is greater)" do + expect(subject <=> other).to eq(1) + end + end + + context "but the other version contains 'alpha'" do + let(:other_version) { '1.2.3.alpha' } + + it "must return 0 (indicating the versions are equal)" do + expect(subject <=> other).to eq(0) + end + end + + context "but the other version contains 'beta'" do + let(:other_version) { '1.2.3.beta' } + + it "must return -1 (indicating the version is less)" do + expect(subject <=> other).to eq(-1) + end + end + + context "but the other version contains 'rc'" do + let(:other_version) { '1.2.3.rc' } + + it "must return -1 (indicating the version is less)" do + expect(subject <=> other).to eq(-1) + end + end + end + + context "and the version contains 'beta'" do + let(:version) { '1.2.3.beta' } + + context "and the other version contains 'pre'" do + let(:other_version) { '1.2.3.pre' } + + it "must return 1 (indicating the version is greater)" do + expect(subject <=> other).to eq(1) + end + end + + context "but the other version contains 'alpha'" do + let(:other_version) { '1.2.3.alpha' } + + it "must return 1 (indicating the version is greater)" do + expect(subject <=> other).to eq(1) + end + end + + context "but the other version contains 'beta'" do + let(:other_version) { '1.2.3.beta' } + + it "must return 0 (indicating the versions are equal)" do + expect(subject <=> other).to eq(0) + end + end + + context "but the other version contains 'rc'" do + let(:other_version) { '1.2.3.rc' } + + it "must return -1 (indicating the version is less)" do + expect(subject <=> other).to eq(-1) + end + end + end + + context "and the version contains 'rc'" do + let(:version) { '1.2.3.rc' } + + context "and the other version contains 'pre'" do + let(:other_version) { '1.2.3.pre' } + + it "must return 1 (indicating the version is greater)" do + expect(subject <=> other).to eq(1) + end + end + + context "but the other version contains 'alpha'" do + let(:other_version) { '1.2.3.alpha' } + + it "must return 1 (indicating the version is greater)" do + expect(subject <=> other).to eq(1) + end + end + + context "but the other version contains 'beta'" do + let(:other_version) { '1.2.3.beta' } + + it "must return 1 (indicating the version is greater)" do + expect(subject <=> other).to eq(1) + end + end + + context "but the other version contains 'rc'" do + let(:other_version) { '1.2.3.rc' } + + it "must return 0 (indicating the versions are equal)" do + expect(subject <=> other).to eq(0) + end + end + end + end + + context "when the version contains numbers with a letter" do + let(:version) { '1.2.3a' } + let(:other_version) { '1.2.3' } + + it "must return 1 (indicating the version is greater)" do + expect(subject <=> other).to eq(1) + end + end + + context "when the other version contains numbers with a letter" do + let(:version) { '1.2.3' } + let(:other_version) { '1.2.3a' } + + it "must return -1 (indicating the version is less)" do + expect(subject <=> other).to eq(-1) + end + end + + context "when both of the versions have numbers that contains letters" do + context "and they are the same" do + let(:version) { '1.2.3a' } + let(:other_version) { '1.2.3a' } + + it "must return 0 (indicating the versions are equal)" do + expect(subject <=> other).to eq(0) + end + end + + context "but the version number with letters is lexically less than the other version's number that also contains letters" do + let(:version) { '1.2.3a' } + let(:other_version) { '1.2.3b' } + + it "must return -1 (indicating the version is less)" do + expect(subject <=> other).to eq(-1) + end + end + + context "but the version number with letters is lexically greater than the other version's number that also contains letters" do + let(:version) { '1.2.3b' } + let(:other_version) { '1.2.3a' } + + it "must return 1 (indicating the version is greater)" do + expect(subject <=> other).to eq(1) + end + end + end + + context "when the other version has fewer parts than the version" do + let(:other_version) { '1.2' } + + it "must return 1 (indicating the other version is less)" do + expect(subject <=> other).to eq(1) + end + + context "but the additional part is a version modifier (pre, alpha, beta, rc)" do + let(:version) { '1.2.3.alpha' } + let(:other_version) { '1.2.3' } + + it "must return -1 (indicating the version is less)" do + expect(subject <=> other).to eq(-1) + end + end + + context "when one of the numbers in the version contains a letter" do + let(:version) { '1.2.3a' } + + it "must return 1 (indicating the version is greater)" do + expect(subject <=> other).to eq(1) + end + end + end + + context "when the other version has more parts than the version" do + context "and the additional part is a number" do + let(:other_version) { '1.2.3.4' } + + it "must return -1 (indicating the other version is greater)" do + expect(subject <=> other).to eq(-1) + end + + context "but the additional numbers are 0" do + let(:other_version) { '1.2.3.0' } + + it "must implicitly consider the two versions equal and return 0" do + expect(subject <=> other).to eq(0) + end + end + end + + context "but the additional part is a version modifier (pre, alpha, beta, rc)" do + let(:other_version) { '1.2.3.alpha' } + + it "must return 1 (indicating the version is greater)" do + expect(subject <=> other).to eq(1) + end + end + + context "when one of the numbers in the other version contains a letter" do + let(:other_version) { '1.2.3.4a' } + + it "must return -1 (indicating the version is less)" do + expect(subject <=> other).to eq(-1) + end + end + end + end + + describe "#to_s" do + it "must return the version string" do + expect(subject.to_s).to eq(version) + end + end +end diff --git a/spec/text/patterns/numeric_spec.rb b/spec/text/patterns/numeric_spec.rb index 29b20058a..4690e77c6 100644 --- a/spec/text/patterns/numeric_spec.rb +++ b/spec/text/patterns/numeric_spec.rb @@ -11,989 +11,258 @@ it "must match one or more digits" do expect(number).to fully_match(subject) end - end - - describe "DECIMAL_OCTET" do - subject { described_class::DECIMAL_OCTET } - - it "must match 0 - 255" do - numbers = (0..255).map(&:to_s) - - expect(numbers).to all(match(subject)) - end - - it "must not match numbers greater than 255" do - expect('256').to_not match(subject) - end - end - - describe "HEX_NUMBER" do - subject { described_class::HEX_NUMBER } - - it "must match one or more decimal digits" do - number = "0123456789" - expect(number).to fully_match(subject) + it "must match negative numbers" do + expect("-#{number}").to fully_match(subject) end - it "must match one or more lowercase hexadecimal digits" do - hex = "0123456789abcdef" - - expect(hex).to fully_match(subject) + it "must match numbers with an 'e' exponent suffix" do + expect("1e10").to fully_match(subject) end - it "must match one or more uppercase hexadecimal digits" do - hex = "0123456789ABCDEF" - - expect(hex).to fully_match(subject) + it "must match numbers with an 'e+' exponent suffix" do + expect("1e+10").to fully_match(subject) end - context "when the number begins with '0x'" do - it "must match one or more decimal digits" do - number = "0x0123456789" - - expect(number).to fully_match(subject) - end - - it "must match one or more lowercase hexadecimal digits" do - hex = "0x0123456789abcdef" - - expect(hex).to fully_match(subject) - end - - it "must match one or more uppercase hexadecimal digits" do - hex = "0x0123456789ABCDEF" - - expect(hex).to fully_match(subject) - end + it "must match numbers with an 'e-' exponent suffix" do + expect("1e-10").to fully_match(subject) end end - describe "VERSION_NUMBER" do - subject { described_class::VERSION_NUMBER } - - it "must match 'X.Y' versions" do - version = '1.0' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y' versions" do - version = '1.2.3' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y-Z' versions" do - version = '1.2-3' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y_Z' versions" do - version = '1.2_3' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.Z' versions" do - version = '1.2.3.4' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-Z' versions" do - version = '1.2.3-4' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y_Z' versions" do - version = '1.2.3_4' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Ypre' versions" do - version = '1.2.3pre' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Yrc' versions" do - version = '1.2.3rc' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Yalpha' versions" do - version = '1.2.3alpha' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Ybeta' versions" do - version = '1.2.3beta' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Yword' versions" do - version = '1.2.3hotfix' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-pre' versions" do - version = '1.2.3-pre' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-rc' versions" do - version = '1.2.3-rc' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-alpha' versions" do - version = '1.2.3-alpha' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-beta' versions" do - version = '1.2.3-beta' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-word' versions" do - version = '1.2.3-hotfix' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.pre' versions" do - version = '1.2.3.pre' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.rc' versions" do - version = '1.2.3.rc' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.alpha' versions" do - version = '1.2.3.alpha' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.beta' versions" do - version = '1.2.3.beta' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.word' versions" do - version = '1.2.3.word' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.YpreNNN' versions" do - version = '1.2.3pre123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.YrcNNN' versions" do - version = '1.2.3rc123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.YalphaNNN' versions" do - version = '1.2.3alpha123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.YbetaNNN' versions" do - version = '1.2.3beta123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.YwordNNN' versions" do - version = '1.2.3hotfix123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Ypre-NNN' versions" do - version = '1.2.3pre-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Yrc-NNN' versions" do - version = '1.2.3rc-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Yalpha-NNN' versions" do - version = '1.2.3alpha-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Ybeta-NNN' versions" do - version = '1.2.3beta-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Yword-NNN' versions" do - version = '1.2.3hotfix-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Ypre.NNN' versions" do - version = '1.2.3pre.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Yrc.NNN' versions" do - version = '1.2.3rc.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Yalpha.NNN' versions" do - version = '1.2.3alpha.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Ybeta.NNN' versions" do - version = '1.2.3beta.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Yword.NNN' versions" do - version = '1.2.3hotfix-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-preNNN' versions" do - version = '1.2.3-pre123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-rcNNN' versions" do - version = '1.2.3-rc123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-alphaNNN' versions" do - version = '1.2.3-alpha123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-betaNNN' versions" do - version = '1.2.3-beta123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-xyzNNN' versions" do - version = '1.2.3-hotfix123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.preNNN' versions" do - version = '1.2.3.pre123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.rcNNN' versions" do - version = '1.2.3.rc123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.alphaNNN' versions" do - version = '1.2.3.alpha123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.betaNNN' versions" do - version = '1.2.3.beta123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.xyzNNN' versions" do - version = '1.2.3.hotfix123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Ypre-NNN' versions" do - version = '1.2.3pre-1234' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Yrc-NNN' versions" do - version = '1.2.3rc-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Yalpha-NNN' versions" do - version = '1.2.3alpha-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Ybeta-NNN' versions" do - version = '1.2.3beta-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Yxyz-NNN' versions" do - version = '1.2.3hotfix-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-pre-NNN' versions" do - version = '1.2.3-pre-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-rc-NNN' versions" do - version = '1.2.3-rc-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-alpha-NNN' versions" do - version = '1.2.3-alpha-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-beta-NNN' versions" do - version = '1.2.3-beta-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-xyz-NNN' versions" do - version = '1.2.3-hotfix-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.pre-NNN' versions" do - version = '1.2.3.pre-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.rc-NNN' versions" do - version = '1.2.3.rc-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.alpha-NNN' versions" do - version = '1.2.3.alpha-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.beta-NNN' versions" do - version = '1.2.3.beta-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.xyz-NNN' versions" do - version = '1.2.3.hotfix-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Ypre.NNN' versions" do - version = '1.2.3pre.1234' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Yrc.NNN' versions" do - version = '1.2.3rc.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Yalpha.NNN' versions" do - version = '1.2.3alpha.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Ybeta.NNN' versions" do - version = '1.2.3beta.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Yxyz.NNN' versions" do - version = '1.2.3hotfix.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-pre.NNN' versions" do - version = '1.2.3-pre.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-rc.NNN' versions" do - version = '1.2.3-rc.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-alpha.NNN' versions" do - version = '1.2.3-alpha.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-beta.NNN' versions" do - version = '1.2.3-beta.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y-xyz.NNN' versions" do - version = '1.2.3-hotfix.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.pre.NNN' versions" do - version = '1.2.3.pre.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.rc.NNN' versions" do - version = '1.2.3.rc.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.alpha.NNN' versions" do - version = '1.2.3.alpha.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.beta.NNN' versions" do - version = '1.2.3.beta.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Y.xyz.NNN' versions" do - version = '1.2.3.hotfix.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Zpre' versions" do - version = '1.2.3.4pre' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Zrc' versions" do - version = '1.2.3.4rc' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Zalpha' versions" do - version = '1.2.3.4alpha' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Zbeta' versions" do - version = '1.2.3.4beta' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Zword' versions" do - version = '1.2.3.4hotfix' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Z-pre' versions" do - version = '1.2.3.4-pre' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Z-rc' versions" do - version = '1.2.3.4-rc' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Z-alpha' versions" do - version = '1.2.3.4-alpha' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Z-beta' versions" do - version = '1.2.3.4-beta' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Z-xyz' versions" do - version = '1.2.3.4-hotfix' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Z.pre' versions" do - version = '1.2.3.4.pre' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Z.rc' versions" do - version = '1.2.3.4.rc' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Z.alpha' versions" do - version = '1.2.3.4.alpha' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Z.beta' versions" do - version = '1.2.3.4.beta' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Z.word' versions" do - version = '1.2.3.4.hotfix' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.ZpreNNN' versions" do - version = '1.2.3.4pre123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.ZrcNNN' versions" do - version = '1.2.3.4rc123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.ZalphaNNN' versions" do - version = '1.2.3.4alpha123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.ZbetaNNN' versions" do - version = '1.2.3.4beta123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.ZwordNNN' versions" do - version = '1.2.3.4hotfix123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Zpre-NNN' versions" do - version = '1.2.3.4pre-1234' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Zrc-NNN' versions" do - version = '1.2.3.4rc-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Zalpha-NNN' versions" do - version = '1.2.3.4alpha-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Zbeta-NNN' versions" do - version = '1.2.3.4beta-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Zword-NNN' versions" do - version = '1.2.3.4hotfix-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Zpre.NNN' versions" do - version = '1.2.3.4pre.1234' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Zrc.NNN' versions" do - version = '1.2.3.4rc.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Zalpha.NNN' versions" do - version = '1.2.3.4alpha.123' + describe "FLOAT" do + subject { described_class::FLOAT } - expect(version).to fully_match(subject) + it "must match 0.5" do + expect('0.5').to fully_match(subject) end - it "must match 'X.Y.Z.Y.Zbeta.NNN' versions" do - version = '1.2.3.4beta.123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Zword-NNN' versions" do - version = '1.2.3.4hotfix-123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Z-preNNN' versions" do - version = '1.2.3.4-pre123' - - expect(version).to fully_match(subject) + it "must match 0.1234" do + expect('0.1234').to fully_match(subject) end - it "must match 'X.Y.Z.Y.Z-rcNNN' versions" do - version = '1.2.3.4-rc123' - - expect(version).to fully_match(subject) + it "must match 1234.0" do + expect('1234.0').to fully_match(subject) end - it "must match 'X.Y.Z.Y.Z-alphaNNN' versions" do - version = '1.2.3.4-alpha123' - - expect(version).to fully_match(subject) + it "must match 1234.5678" do + expect('1234.5678').to fully_match(subject) end - it "must match 'X.Y.Z.Y.Z-betaNNN' versions" do - version = '1.2.3.4-beta123' - - expect(version).to fully_match(subject) + it "must match 1.0e10" do + expect('1.0e10').to fully_match(subject) end - it "must match 'X.Y.Z.Y.Z-wordNNN' versions" do - version = '1.2.3.4-hotfix123' - - expect(version).to fully_match(subject) - end - - it "must match 'X.Y.Z.Y.Z.preNNN' versions" do - version = '1.2.3.4.pre123' - - expect(version).to fully_match(subject) + it "must match 1.0e+10" do + expect('1.0e+10').to fully_match(subject) end - it "must match 'X.Y.Z.Y.Z.rcNNN' versions" do - version = '1.2.3.4.rc123' - - expect(version).to fully_match(subject) + it "must match 1.0e-10" do + expect('1.0e-10').to fully_match(subject) end - it "must match 'X.Y.Z.Y.Z.alphaNNN' versions" do - version = '1.2.3.4.alpha123' - - expect(version).to fully_match(subject) + it "must match negative numbers" do + expect("-1.234").to fully_match(subject) end + end - it "must match 'X.Y.Z.Y.Z.betaNNN' versions" do - version = '1.2.3.4.beta123' + describe "OCTAL_BYTE" do + subject { described_class::OCTAL_BYTE } - expect(version).to fully_match(subject) - end + it "must match 0 - 377" do + numbers = (0..255).map { |byte| byte.to_s(8) } - it "must match 'X.Y.Z.Y.Z.wordNNN' versions" do - version = '1.2.3.4.hotfix123' - - expect(version).to fully_match(subject) + expect(numbers).to all(match(subject)) end - it "must match 'X.Y.Z.Y.Zpre-NNN' versions" do - version = '1.2.3.4pre-1234' - - expect(version).to fully_match(subject) + it "must not match numbers greater than 377" do + expect('378').to_not match(subject) end + end - it "must match 'X.Y.Z.Y.Zrc-NNN' versions" do - version = '1.2.3.4rc-123' - - expect(version).to fully_match(subject) - end + describe "DECIMAL_BYTE" do + subject { described_class::DECIMAL_BYTE } - it "must match 'X.Y.Z.Y.Zalpha-NNN' versions" do - version = '1.2.3.4alpha-123' + it "must match 0 - 255" do + numbers = (0..255).map(&:to_s) - expect(version).to fully_match(subject) + expect(numbers).to all(match(subject)) end - it "must match 'X.Y.Z.Y.Zbeta-NNN' versions" do - version = '1.2.3.4beta-123' - - expect(version).to fully_match(subject) + it "must not match numbers greater than 255" do + expect('256').to_not match(subject) end + end - it "must match 'X.Y.Z.Y.Zword-NNN' versions" do - version = '1.2.3.4hotfix-123' + describe "DECIMAL_OCTET" do + subject { described_class::DECIMAL_OCTET } - expect(version).to fully_match(subject) + it "must be an alias for DECIMAL_BYTE" do + expect(subject).to be(described_class::DECIMAL_BYTE) end + end - it "must match 'X.Y.Z.Y.Z-pre-NNN' versions" do - version = '1.2.3.4-pre-123' - - expect(version).to fully_match(subject) - end + describe "HEX_BYTE" do + subject { described_class::HEX_BYTE } - it "must match 'X.Y.Z.Y.Z-rc-NNN' versions" do - version = '1.2.3.4-rc-123' + it "must match 00 - ff" do + hex_bytes = (0..0xff).map { |byte| "%.2x" % byte } - expect(version).to fully_match(subject) + expect(hex_bytes).to all(match(subject)) end - it "must match 'X.Y.Z.Y.Z-alpha-NNN' versions" do - version = '1.2.3.4-alpha-123' + it "must match 00 - FF" do + hex_bytes = (0..0xff).map { |byte| "%.2X" % byte } - expect(version).to fully_match(subject) + expect(hex_bytes).to all(match(subject)) end - it "must match 'X.Y.Z.Y.Z-beta-NNN' versions" do - version = '1.2.3.4-beta-123' + it "must match 0x00 - 0xff" do + hex_bytes = (0..0xff).map { |byte| "0x%.2x" % byte } - expect(version).to fully_match(subject) + expect(hex_bytes).to all(match(subject)) end - it "must match 'X.Y.Z.Y.Z-word-NNN' versions" do - version = '1.2.3.4-hotfix-123' + it "must match 0x00 - 0xFF" do + hex_bytes = (0..0xff).map { |byte| "0x%.2X" % byte } - expect(version).to fully_match(subject) + expect(hex_bytes).to all(match(subject)) end - it "must match 'X.Y.Z.Y.Z.pre-NNN' versions" do - version = '1.2.3.4.pre-123' + it "must only match two hexadecimal digits" do + string = "a1b2" - expect(version).to fully_match(subject) + expect(string[subject]).to eq("a1") end + end - it "must match 'X.Y.Z.Y.Z.rc-NNN' versions" do - version = '1.2.3.4.rc-123' + describe "HEX_WORD" do + subject { described_class::HEX_WORD } - expect(version).to fully_match(subject) + it "must match 0000 - ffff" do + expect("0000").to match(subject) + expect("ffff").to match(subject) end - it "must match 'X.Y.Z.Y.Z.alpha-NNN' versions" do - version = '1.2.3.4.alpha-123' - - expect(version).to fully_match(subject) + it "must match 0000 - FFFF" do + expect("0000").to match(subject) + expect("FFFF").to match(subject) end - it "must match 'X.Y.Z.Y.Z.beta-NNN' versions" do - version = '1.2.3.4.beta-123' - - expect(version).to fully_match(subject) + it "must match 0x0000 - 0xffff" do + expect("0x0000").to match(subject) + expect("0xffff").to match(subject) end - it "must match 'X.Y.Z.Y.Z.word-NNN' versions" do - version = '1.2.3.4.hotfix-123' - - expect(version).to fully_match(subject) + it "must match 0x0000 - 0xFFFF" do + expect("0x0000").to match(subject) + expect("0xFFFF").to match(subject) end - it "must match 'X.Y.Z.Y.Zpre.NNN' versions" do - version = '1.2.3.4pre.1234' + it "must only match four hexadecimal digits" do + string = "a1b2c3" - expect(version).to fully_match(subject) + expect(string[subject]).to eq("a1b2") end + end - it "must match 'X.Y.Z.Y.Zrc.NNN' versions" do - version = '1.2.3.4rc.123' + describe "HEX_DWORD" do + subject { described_class::HEX_DWORD } - expect(version).to fully_match(subject) + it "must match 00000000 - ffffffff" do + expect("00000000").to match(subject) + expect("ffffffff").to match(subject) end - it "must match 'X.Y.Z.Y.Zalpha.NNN' versions" do - version = '1.2.3.4alpha.123' - - expect(version).to fully_match(subject) + it "must match 00000000 - FFFFFFFF" do + expect("00000000").to match(subject) + expect("FFFFFFFF").to match(subject) end - it "must match 'X.Y.Z.Y.Zbeta.NNN' versions" do - version = '1.2.3.4beta.123' - - expect(version).to fully_match(subject) + it "must match 0x00000000 - 0xffffffff" do + expect("0x00000000").to match(subject) + expect("0xffffffff").to match(subject) end - it "must match 'X.Y.Z.Y.Zword.NNN' versions" do - version = '1.2.3.4hotfix.123' - - expect(version).to fully_match(subject) + it "must match 0x00000000 - 0xFFFFFFFF" do + expect("0x00000000").to match(subject) + expect("0xFFFFFFFF").to match(subject) end - it "must match 'X.Y.Z.Y.Z-pre.NNN' versions" do - version = '1.2.3.4-pre.123' + it "must only match eight hexadecimal digits" do + string = "1234abcdefg" - expect(version).to fully_match(subject) + expect(string[subject]).to eq("1234abcd") end + end - it "must match 'X.Y.Z.Y.Z-rc.NNN' versions" do - version = '1.2.3.4-rc.123' + describe "HEX_QWORD" do + subject { described_class::HEX_QWORD } - expect(version).to fully_match(subject) + it "must match 0000000000000000 - ffffffffffffffff" do + expect("0000000000000000").to match(subject) + expect("ffffffffffffffff").to match(subject) end - it "must match 'X.Y.Z.Y.Z-alpha.NNN' versions" do - version = '1.2.3.4-alpha.123' - - expect(version).to fully_match(subject) + it "must match 0000000000000000 - FFFFFFFFFFFFFFFF" do + expect("0000000000000000").to match(subject) + expect("FFFFFFFFFFFFFFFF").to match(subject) end - it "must match 'X.Y.Z.Y.Z-beta.NNN' versions" do - version = '1.2.3.4-beta.123' - - expect(version).to fully_match(subject) + it "must match 0x0000000000000000 - 0xffffffffffffffff" do + expect("0x0000000000000000").to match(subject) + expect("0xffffffffffffffff").to match(subject) end - it "must match 'X.Y.Z.Y.Z-word.NNN' versions" do - version = '1.2.3.4-hotfix.123' - - expect(version).to fully_match(subject) + it "must match 0x0000000000000000 - 0xFFFFFFFFFFFFFFFF" do + expect("0x0000000000000000").to match(subject) + expect("0xFFFFFFFFFFFFFFFF").to match(subject) end - it "must match 'X.Y.Z.Y.Z.pre.NNN' versions" do - version = '1.2.3.4.pre.123' + it "must only match eight hexadecimal digits" do + string = "1234567890abcdef11111" - expect(version).to fully_match(subject) + expect(string[subject]).to eq("1234567890abcdef") end + end - it "must match 'X.Y.Z.Y.Z.rc.NNN' versions" do - version = '1.2.3.4.rc.123' - - expect(version).to fully_match(subject) - end + describe "HEX_NUMBER" do + subject { described_class::HEX_NUMBER } - it "must match 'X.Y.Z.Y.Z.alpha.NNN' versions" do - version = '1.2.3.4.alpha.123' + it "must match one or more decimal digits" do + number = "0123456789" - expect(version).to fully_match(subject) + expect(number).to fully_match(subject) end - it "must match 'X.Y.Z.Y.Z.beta.NNN' versions" do - version = '1.2.3.4.beta.123' + it "must match one or more lowercase hexadecimal digits" do + hex = "0123456789abcdef" - expect(version).to fully_match(subject) + expect(hex).to fully_match(subject) end - it "must match 'X.Y.Z.Y.Z.word.NNN' versions" do - version = '1.2.3.4.hotfix.123' + it "must match one or more uppercase hexadecimal digits" do + hex = "0123456789ABCDEF" - expect(version).to fully_match(subject) + expect(hex).to fully_match(subject) end - context "when the version ends with a '+XXX' suffix" do - it "must not match the '+XXX' suffix" do - version = '1.2.3+a1b2c3' + context "when the number begins with '0x'" do + it "must match one or more decimal digits" do + number = "0x0123456789" - expect(version[subject]).to eq('1.2.3') + expect(number).to fully_match(subject) end - end - it "must not accidentally match a phone number" do - expect('1-800-111-2222').to_not match(subject) - end - - it "must not accidentally match 'MM-DD-YY'" do - expect('01-02-24').to_not match(subject) - end - - it "must not accidentally match 'MM-DD-YYYY'" do - expect('01-02-2024').to_not match(subject) - end - - it "must not accidentally match 'YYYY-MM-DD'" do - expect('2024-01-02').to_not match(subject) - end - - it "must not accidentally match 'CVE-YYYY-XXXX'" do - expect('CVE-2024-1234').to_not match(subject) - end + it "must match one or more lowercase hexadecimal digits" do + hex = "0x0123456789abcdef" - context "when the version is within a filename" do - let(:version) { '1.2.3' } + expect(hex).to fully_match(subject) + end - %w[.tar.gz .tar.bz2 .tar.xz .tgz .tbz2 .zip .rar .htm .html .xml .txt].each do |extname| - context "and when the filename ends with '#{extname}'" do - let(:extname) { extname } - let(:filename) { "foo-#{version}#{extname}" } + it "must match one or more uppercase hexadecimal digits" do + hex = "0x0123456789ABCDEF" - it "must not accidentally match '#{extname}' as part of the version" do - expect(filename[subject]).to eq(version) - end - end + expect(hex).to fully_match(subject) end end end diff --git a/spec/text/patterns/software_spec.rb b/spec/text/patterns/software_spec.rb new file mode 100644 index 000000000..77b49f2f2 --- /dev/null +++ b/spec/text/patterns/software_spec.rb @@ -0,0 +1,1006 @@ +require 'spec_helper' +require 'matchers/fully_match' +require 'ronin/support/text/patterns/software' + +describe Ronin::Support::Text::Patterns do + describe "VERSION_NUMBER" do + subject { described_class::VERSION_NUMBER } + + it "must match 'X.Y' versions" do + version = '1.0' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y' versions" do + version = '1.2.3' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y-Z' versions" do + version = '1.2-3' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y_Z' versions" do + version = '1.2_3' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.Z' versions" do + version = '1.2.3.4' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-Z' versions" do + version = '1.2.3-4' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y_Z' versions" do + version = '1.2.3_4' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Ypre' versions" do + version = '1.2.3pre' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Yrc' versions" do + version = '1.2.3rc' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Yalpha' versions" do + version = '1.2.3alpha' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Ybeta' versions" do + version = '1.2.3beta' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Yword' versions" do + version = '1.2.3hotfix' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-pre' versions" do + version = '1.2.3-pre' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-rc' versions" do + version = '1.2.3-rc' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-alpha' versions" do + version = '1.2.3-alpha' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-beta' versions" do + version = '1.2.3-beta' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-word' versions" do + version = '1.2.3-hotfix' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.pre' versions" do + version = '1.2.3.pre' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.rc' versions" do + version = '1.2.3.rc' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.alpha' versions" do + version = '1.2.3.alpha' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.beta' versions" do + version = '1.2.3.beta' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.word' versions" do + version = '1.2.3.word' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.YpreNNN' versions" do + version = '1.2.3pre123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.YrcNNN' versions" do + version = '1.2.3rc123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.YalphaNNN' versions" do + version = '1.2.3alpha123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.YbetaNNN' versions" do + version = '1.2.3beta123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.YwordNNN' versions" do + version = '1.2.3hotfix123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Ypre-NNN' versions" do + version = '1.2.3pre-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Yrc-NNN' versions" do + version = '1.2.3rc-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Yalpha-NNN' versions" do + version = '1.2.3alpha-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Ybeta-NNN' versions" do + version = '1.2.3beta-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Yword-NNN' versions" do + version = '1.2.3hotfix-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Ypre.NNN' versions" do + version = '1.2.3pre.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Yrc.NNN' versions" do + version = '1.2.3rc.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Yalpha.NNN' versions" do + version = '1.2.3alpha.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Ybeta.NNN' versions" do + version = '1.2.3beta.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Yword.NNN' versions" do + version = '1.2.3hotfix-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-preNNN' versions" do + version = '1.2.3-pre123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-rcNNN' versions" do + version = '1.2.3-rc123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-alphaNNN' versions" do + version = '1.2.3-alpha123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-betaNNN' versions" do + version = '1.2.3-beta123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-xyzNNN' versions" do + version = '1.2.3-hotfix123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.preNNN' versions" do + version = '1.2.3.pre123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.rcNNN' versions" do + version = '1.2.3.rc123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.alphaNNN' versions" do + version = '1.2.3.alpha123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.betaNNN' versions" do + version = '1.2.3.beta123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.xyzNNN' versions" do + version = '1.2.3.hotfix123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Ypre-NNN' versions" do + version = '1.2.3pre-1234' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Yrc-NNN' versions" do + version = '1.2.3rc-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Yalpha-NNN' versions" do + version = '1.2.3alpha-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Ybeta-NNN' versions" do + version = '1.2.3beta-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Yxyz-NNN' versions" do + version = '1.2.3hotfix-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-pre-NNN' versions" do + version = '1.2.3-pre-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-rc-NNN' versions" do + version = '1.2.3-rc-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-alpha-NNN' versions" do + version = '1.2.3-alpha-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-beta-NNN' versions" do + version = '1.2.3-beta-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-xyz-NNN' versions" do + version = '1.2.3-hotfix-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.pre-NNN' versions" do + version = '1.2.3.pre-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.rc-NNN' versions" do + version = '1.2.3.rc-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.alpha-NNN' versions" do + version = '1.2.3.alpha-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.beta-NNN' versions" do + version = '1.2.3.beta-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.xyz-NNN' versions" do + version = '1.2.3.hotfix-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Ypre.NNN' versions" do + version = '1.2.3pre.1234' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Yrc.NNN' versions" do + version = '1.2.3rc.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Yalpha.NNN' versions" do + version = '1.2.3alpha.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Ybeta.NNN' versions" do + version = '1.2.3beta.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Yxyz.NNN' versions" do + version = '1.2.3hotfix.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-pre.NNN' versions" do + version = '1.2.3-pre.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-rc.NNN' versions" do + version = '1.2.3-rc.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-alpha.NNN' versions" do + version = '1.2.3-alpha.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-beta.NNN' versions" do + version = '1.2.3-beta.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y-xyz.NNN' versions" do + version = '1.2.3-hotfix.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.pre.NNN' versions" do + version = '1.2.3.pre.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.rc.NNN' versions" do + version = '1.2.3.rc.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.alpha.NNN' versions" do + version = '1.2.3.alpha.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.beta.NNN' versions" do + version = '1.2.3.beta.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Y.xyz.NNN' versions" do + version = '1.2.3.hotfix.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zpre' versions" do + version = '1.2.3.4pre' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zrc' versions" do + version = '1.2.3.4rc' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zalpha' versions" do + version = '1.2.3.4alpha' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zbeta' versions" do + version = '1.2.3.4beta' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zword' versions" do + version = '1.2.3.4hotfix' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-pre' versions" do + version = '1.2.3.4-pre' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-rc' versions" do + version = '1.2.3.4-rc' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-alpha' versions" do + version = '1.2.3.4-alpha' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-beta' versions" do + version = '1.2.3.4-beta' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-xyz' versions" do + version = '1.2.3.4-hotfix' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.pre' versions" do + version = '1.2.3.4.pre' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.rc' versions" do + version = '1.2.3.4.rc' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.alpha' versions" do + version = '1.2.3.4.alpha' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.beta' versions" do + version = '1.2.3.4.beta' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.word' versions" do + version = '1.2.3.4.hotfix' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.ZpreNNN' versions" do + version = '1.2.3.4pre123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.ZrcNNN' versions" do + version = '1.2.3.4rc123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.ZalphaNNN' versions" do + version = '1.2.3.4alpha123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.ZbetaNNN' versions" do + version = '1.2.3.4beta123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.ZwordNNN' versions" do + version = '1.2.3.4hotfix123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zpre-NNN' versions" do + version = '1.2.3.4pre-1234' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zrc-NNN' versions" do + version = '1.2.3.4rc-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zalpha-NNN' versions" do + version = '1.2.3.4alpha-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zbeta-NNN' versions" do + version = '1.2.3.4beta-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zword-NNN' versions" do + version = '1.2.3.4hotfix-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zpre.NNN' versions" do + version = '1.2.3.4pre.1234' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zrc.NNN' versions" do + version = '1.2.3.4rc.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zalpha.NNN' versions" do + version = '1.2.3.4alpha.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zbeta.NNN' versions" do + version = '1.2.3.4beta.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zword-NNN' versions" do + version = '1.2.3.4hotfix-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-preNNN' versions" do + version = '1.2.3.4-pre123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-rcNNN' versions" do + version = '1.2.3.4-rc123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-alphaNNN' versions" do + version = '1.2.3.4-alpha123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-betaNNN' versions" do + version = '1.2.3.4-beta123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-wordNNN' versions" do + version = '1.2.3.4-hotfix123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.preNNN' versions" do + version = '1.2.3.4.pre123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.rcNNN' versions" do + version = '1.2.3.4.rc123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.alphaNNN' versions" do + version = '1.2.3.4.alpha123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.betaNNN' versions" do + version = '1.2.3.4.beta123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.wordNNN' versions" do + version = '1.2.3.4.hotfix123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zpre-NNN' versions" do + version = '1.2.3.4pre-1234' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zrc-NNN' versions" do + version = '1.2.3.4rc-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zalpha-NNN' versions" do + version = '1.2.3.4alpha-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zbeta-NNN' versions" do + version = '1.2.3.4beta-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zword-NNN' versions" do + version = '1.2.3.4hotfix-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-pre-NNN' versions" do + version = '1.2.3.4-pre-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-rc-NNN' versions" do + version = '1.2.3.4-rc-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-alpha-NNN' versions" do + version = '1.2.3.4-alpha-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-beta-NNN' versions" do + version = '1.2.3.4-beta-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-word-NNN' versions" do + version = '1.2.3.4-hotfix-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.pre-NNN' versions" do + version = '1.2.3.4.pre-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.rc-NNN' versions" do + version = '1.2.3.4.rc-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.alpha-NNN' versions" do + version = '1.2.3.4.alpha-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.beta-NNN' versions" do + version = '1.2.3.4.beta-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.word-NNN' versions" do + version = '1.2.3.4.hotfix-123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zpre.NNN' versions" do + version = '1.2.3.4pre.1234' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zrc.NNN' versions" do + version = '1.2.3.4rc.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zalpha.NNN' versions" do + version = '1.2.3.4alpha.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zbeta.NNN' versions" do + version = '1.2.3.4beta.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Zword.NNN' versions" do + version = '1.2.3.4hotfix.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-pre.NNN' versions" do + version = '1.2.3.4-pre.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-rc.NNN' versions" do + version = '1.2.3.4-rc.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-alpha.NNN' versions" do + version = '1.2.3.4-alpha.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-beta.NNN' versions" do + version = '1.2.3.4-beta.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z-word.NNN' versions" do + version = '1.2.3.4-hotfix.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.pre.NNN' versions" do + version = '1.2.3.4.pre.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.rc.NNN' versions" do + version = '1.2.3.4.rc.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.alpha.NNN' versions" do + version = '1.2.3.4.alpha.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.beta.NNN' versions" do + version = '1.2.3.4.beta.123' + + expect(version).to fully_match(subject) + end + + it "must match 'X.Y.Z.Y.Z.word.NNN' versions" do + version = '1.2.3.4.hotfix.123' + + expect(version).to fully_match(subject) + end + + context "when the version ends with a '+XXX' suffix" do + it "must not match the '+XXX' suffix" do + version = '1.2.3+a1b2c3' + + expect(version[subject]).to eq('1.2.3') + end + end + + it "must not accidentally match a phone number" do + expect('1-800-111-2222').to_not match(subject) + end + + it "must not accidentally match 'MM-DD-YY'" do + expect('01-02-24').to_not match(subject) + end + + it "must not accidentally match 'MM-DD-YYYY'" do + expect('01-02-2024').to_not match(subject) + end + + it "must not accidentally match 'YYYY-MM-DD'" do + expect('2024-01-02').to_not match(subject) + end + + it "must not accidentally match 'CVE-YYYY-XXXX'" do + expect('CVE-2024-1234').to_not match(subject) + end + + context "when the version is within a filename" do + let(:version) { '1.2.3' } + + %w[.tar.gz .tar.bz2 .tar.xz .tgz .tbz2 .zip .rar .htm .html .xml .txt].each do |extname| + context "and when the filename ends with '#{extname}'" do + let(:extname) { extname } + let(:filename) { "foo-#{version}#{extname}" } + + it "must not accidentally match '#{extname}' as part of the version" do + expect(filename[subject]).to eq(version) + end + end + end + end + end + + describe "VERSION_CONSTRAINT" do + subject { described_class::VERSION_CONSTRAINT } + + it "must match '>= X.Y.Z'" do + expect('>= 1.2.3').to fully_match(subject) + end + + it "must match '> X.Y.Z'" do + expect('> 1.2.3').to fully_match(subject) + end + + it "must match '<= X.Y.Z'" do + expect('<= 1.2.3').to fully_match(subject) + end + + it "must match '< X.Y.Z'" do + expect('< 1.2.3').to fully_match(subject) + end + + it "must match '= X.Y.Z'" do + expect('= 1.2.3').to fully_match(subject) + end + + it "must match '>=X.Y.Z'" do + expect('>=1.2.3').to fully_match(subject) + end + + it "must match '>X.Y.Z'" do + expect('>1.2.3').to fully_match(subject) + end + + it "must match '<=X.Y.Z'" do + expect('<=1.2.3').to fully_match(subject) + end + + it "must match '=|>|<=|<|=) X.Y.Z'" do + expect('>= 1.2.3').to fully_match(subject) + end + + it "must match '(>=|>|<=|<|=)X.Y.Z'" do + expect('>=1.2.3').to fully_match(subject) + end + + it "must match '(>=|>|<=|<|=) X.Y.Z, (>=|>|<=|<|=) A.B.C'" do + expect('>= 1.2.3, < 2.0.0').to fully_match(subject) + end + + it "must match '(>=|>|<=|<|=)X.Y.Z,(>=|>|<=|<|=)A.B.C'" do + expect('>=1.2.3,<2.0.0').to fully_match(subject) + end + + it "must match '(>=|>|<=|<|=) X.Y.Z (>=|>|<=|<|=) A.B.C'" do + expect('>= 1.2.3 < 2.0.0').to fully_match(subject) + end + + it "must match '(>=|>|<=|<|=)X.Y.Z (>=|>|<=|<|=)A.B.C'" do + expect('>=1.2.3 <2.0.0').to fully_match(subject) + end + end +end