# frozen_string_literal: true require 'base64' module Puppet::Pops module Types # A Puppet Language Type that represents binary data content (a sequence of 8-bit bytes). # Instances of this data type can be created from `String` and `Array[Integer[0,255]]` # values. Also see the `binary_file` function for reading binary content from a file. # # A `Binary` can be converted to `String` and `Array` form - see function `new` for # the respective target data type for more information. # # Instances of this data type serialize as base 64 encoded strings when the serialization # format is textual, and as binary content when a serialization format supports this. # # @api public class PBinaryType < PAnyType # Represents a binary buffer # @api public class Binary attr_reader :binary_buffer # Constructs an instance of Binary from a base64 urlsafe encoded string (RFC 2045). # @param [String] A string with RFC 2045 compliant encoded binary # def self.from_base64(str) new(Base64.decode64(str)) end # Constructs an instance of Binary from a base64 encoded string (RFC4648 with "URL and Filename # Safe Alphabet" (That is with '-' instead of '+', and '_' instead of '/'). # def self.from_base64_urlsafe(str) new(Base64.urlsafe_decode64(str)) end # Constructs an instance of Binary from a base64 strict encoded string (RFC 4648) # Where correct padding must be used and line breaks causes an error to be raised. # # @param [String] A string with RFC 4648 compliant encoded binary # def self.from_base64_strict(str) new(Base64.strict_decode64(str)) end # Creates a new Binary from a String containing binary data. If the string's encoding # is not already ASCII-8BIT, a copy of the string is force encoded as ASCII-8BIT (that is Ruby's "binary" format). # This means that this binary will have the exact same content, but the string will considered # to hold a sequence of bytes in the range 0 to 255. # # The given string will be frozen as a side effect if it is in ASCII-8BIT encoding. If this is not # wanted, a copy should be given to this method. # # @param [String] A string with binary data # @api public # def self.from_binary_string(bin) new(bin) end # Creates a new Binary from a String containing text/binary in its given encoding. If the string's encoding # is not already UTF-8, the string is first transcoded to UTF-8. # This means that this binary will have the UTF-8 byte representation of the original string. # For this to be valid, the encoding used in the given string must be valid. # The validity of the given string is therefore asserted. # # The given string will be frozen as a side effect if it is in ASCII-8BIT encoding. If this is not # wanted, a copy should be given to this method. # # @param [String] A string with valid content in its given encoding # @return [Puppet::Pops::Types::PBinaryType::Binary] with the UTF-8 representation of the UTF-8 transcoded string # @api public # def self.from_string(encoded_string) enc = encoded_string.encoding.name unless encoded_string.valid_encoding? raise ArgumentError, _("The given string in encoding '%{enc}' is invalid. Cannot create a Binary UTF-8 representation") % { enc: enc } end # Convert to UTF-8 (if not already UTF-8), and then to binary encoded_string = (enc == "UTF-8") ? encoded_string.dup : encoded_string.encode('UTF-8') encoded_string.force_encoding("ASCII-8BIT") new(encoded_string) end # Creates a new Binary from a String containing raw binary data of unknown encoding. If the string's encoding # is not already ASCII-8BIT, a copy of the string is forced to ASCII-8BIT (that is Ruby's "binary" format). # This means that this binary will have the exact same content, but the string will considered # to hold a sequence of bytes in the range 0 to 255. # # @param [String] A string with binary data # @api private # def initialize(bin) @binary_buffer = (bin.encoding.name == "ASCII-8BIT" ? bin : bin.b).freeze end # Presents the binary content as a string base64 encoded string (without line breaks). # def to_s Base64.strict_encode64(@binary_buffer) end # Returns the binary content as a "relaxed" base64 (standard) encoding where # the string is broken up with new lines. def relaxed_to_s Base64.encode64(@binary_buffer) end # Returns the binary content as a url safe base64 string (where + and / are replaced by - and _) # def urlsafe_to_s Base64.urlsafe_encode64(@binary_buffer) end def hash @binary_buffer.hash end def eql?(o) self.class == o.class && @binary_buffer == o.binary_buffer end def ==(o) self.eql?(o) end def length() @binary_buffer.length end end def self.register_ptype(loader, ir) create_ptype(loader, ir, 'AnyType') end # Only instances of Binary are instances of the PBinaryType # def instance?(o, guard = nil) o.is_a?(Binary) end def eql?(o) self.class == o.class end # Binary uses the strict base64 format as its string representation # @return [TrueClass] true def roundtrip_with_string? true end # @api private def self.new_function(type) @new_function ||= Puppet::Functions.create_loaded_function(:new_Binary, type.loader) do local_types do type 'ByteInteger = Integer[0,255]' type 'Base64Format = Enum["%b", "%u", "%B", "%s", "%r"]' type 'StringHash = Struct[{value => String, "format" => Optional[Base64Format]}]' type 'ArrayHash = Struct[{value => Array[ByteInteger]}]' type 'BinaryArgsHash = Variant[StringHash, ArrayHash]' end # Creates a binary from a base64 encoded string in one of the formats %b, %u, %B, %s, or %r dispatch :from_string do param 'String', :str optional_param 'Base64Format', :format end dispatch :from_array do param 'Array[ByteInteger]', :byte_array end # Same as from_string, or from_array, but value and (for string) optional format are given in the form # of a hash. # dispatch :from_hash do param 'BinaryArgsHash', :hash_args end def from_string(str, format = nil) format ||= '%B' case format when "%b" # padding must be added for older rubies to avoid truncation padding = '=' * (str.length % 3) Binary.new(Base64.decode64(str + padding)) when "%u" Binary.new(Base64.urlsafe_decode64(str)) when "%B" Binary.new(Base64.strict_decode64(str)) when "%s" Binary.from_string(str) when "%r" Binary.from_binary_string(str) else raise ArgumentError, "Unsupported Base64 format '#{format}'" end end def from_array(array) # The array is already known to have bytes in the range 0-255, or it is in error # Without this pack C would produce weird results Binary.from_binary_string(array.pack("C*")) end def from_hash(hash) case hash['value'] when Array from_array(hash['value']) when String from_string(hash['value'], hash['format']) end end end end DEFAULT = PBinaryType.new protected def _assignable?(o, guard) o.class == self.class end end end end