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