library/types/src/modules/Punycode.rb
# ***************************************************************************
#
# Copyright (c) 2002 - 2012 Novell, Inc.
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, contact Novell, Inc.
#
# To contact Novell about this file by physical or electronic mail,
# you may find current contact information at www.novell.com
#
# ***************************************************************************
# File: modules/Punycode.ycp
# Package: Main yast package
# Summary: DNS Punycode Handling
# Authors: Lukas Ocilka <lukas.ocilka@suse.cz>
# Tags: Unstable
#
# $Id$
#
require "yast"
require "simpleidn"
module Yast
class PunycodeClass < Module
# string, matching this regexp, is not cached
NOT_CACHED_REGEXP = /^[0123456789.]*$/.freeze
def main
textdomain "base"
@tmp_dir = nil
#
# Encoded string in cache has the same index
# as its Decoded format in the second list.
#
# list of encoded strings to be cached (Punycode or Unicode)
@cache_encoded = []
# list of decoded strings to be cached (Unicode or Punycode)
@cache_decoded = []
@current_cache_index = 0
# cached amount of data should be controled
@current_cache_size = 0
@maximum_cache_size = 32_768
end
# Returns the maximum cache size (sum of already converted
# strings).
#
# @return [Fixnum] maximum_cache_size
# @see #SetMaximumCacheSize()
def GetMaximumCacheSize
@maximum_cache_size
end
# Offers to set the maximum cache size (sum of already
# converted strings).
#
# @param [Fixnum] new_max_size
# @see #GetMaximumCacheSize()
def SetMaximumCacheSize(new_max_size)
if new_max_size.nil?
Builtins.y2error("Cannot set MaximumCacheSize to nil!")
else
@maximum_cache_size = new_max_size
end
nil
end
# Adds new cache records for encoded and decoded strings.
#
# @param [String] decoded
# @param [String] encoded
def CreateNewCacheRecord(decoded, encoded)
# Erroneous cache record
return if decoded.nil? || encoded.nil?
# Already cached
return if Builtins.contains(@cache_decoded, decoded)
decoded_size = Builtins.size(decoded)
encoded_size = Builtins.size(encoded)
# Do not store this record if the cache would exceed maximum
if Ops.greater_than(
Ops.add(Ops.add(@current_cache_size, decoded_size), encoded_size),
@maximum_cache_size
)
return
end
@current_cache_size = Ops.add(
Ops.add(@current_cache_size, decoded_size),
encoded_size
)
Ops.set(@cache_decoded, @current_cache_index, decoded)
Ops.set(@cache_encoded, @current_cache_index, encoded)
@current_cache_index = Ops.add(@current_cache_index, 1)
nil
end
# Returns string encoded in Punycode if it has been
# already cached. Returns nil if not found.
#
# @param [String] decoded_string (Unicode)
# @return [String] encoded_string (Punycode)
def GetEncodedCachedString(decoded_string)
ret = nil
# numbers and empty strings are not converted
return decoded_string if NOT_CACHED_REGEXP.match?(decoded_string)
counter = -1
# searching through decoded strings to find the index
Builtins.foreach(@cache_decoded) do |cached_string|
counter = Ops.add(counter, 1)
if cached_string == decoded_string
# returning encoded representation
ret = Ops.get(@cache_encoded, counter)
raise Break
end
end
ret
end
# Returns string encoded in Punycode if it has been
# already cached. Returns nil if not found.
#
# @param [String] encoded_string (Punycode)
# @return [String] decoded_string (Unicode)
def GetDecodedCachedString(encoded_string)
ret = nil
# numbers and empty strings are not converted
return encoded_string if NOT_CACHED_REGEXP.match?(encoded_string)
counter = -1
# searching through encoded strings to find the index
Builtins.foreach(@cache_encoded) do |cached_string|
counter = Ops.add(counter, 1)
if cached_string == encoded_string
# returning decoded representation
ret = Ops.get(@cache_decoded, counter)
raise Break
end
end
ret
end
# Returns the current temporary directory.
# Lazy loading for the initialization is used.
def GetTmpDirectory
@tmp_dir = Convert.to_string(SCR.Read(path(".target.tmpdir"))) if @tmp_dir.nil?
@tmp_dir
end
# Function takes the list of strings and returns them in the converted
# format. Unicode to Punycode or Punycode to Unicode (param to_punycode).
# It uses a cache of already-converted strings.
def ConvertBackAndForth(strings_in, to_punycode)
# Some (or maybe all) strings needn't be cached
not_cached = []
# Check the cache for already entered strings
test_cached = strings_in.each_with_object({}) do |string_in, all|
# Numbers, IPs and empty strings are not converted
string_out =
if NOT_CACHED_REGEXP.match?(string_in)
string_in
elsif to_punycode
GetEncodedCachedString(string_in)
else
GetDecodedCachedString(string_in)
end
not_cached << string_in if string_out.nil?
all[string_in] = string_out
end
converted_not_cached = []
# There is something not cached, converting them at once
if !not_cached.empty?
meth = to_punycode ? :to_ascii : :to_unicode
converted_not_cached = not_cached.map { |v| SimpleIDN.public_send(meth, v) }
end
# Listing through the given list and adjusting the return list
found_index = -1
strings_in.each_with_object([]) do |string_in, all|
# Already cached string
if test_cached[string_in]
all << test_cached[string_in]
else # Recently converted strings
found_index += 1
(all << converted_not_cached[found_index]) || ""
# Adding converted strings to cache
if to_punycode
CreateNewCacheRecord(string_in, converted_not_cached[found_index] || "")
else
CreateNewCacheRecord(converted_not_cached[found_index] || "", string_in)
end
end
end
end
# Converts list of UTF-8 strings into their Punycode
# ASCII repserentation.
#
# @param [Array<String>] punycode_strings
# @return [Array<String>] encoded_strings
def EncodePunycodes(punycode_strings)
punycode_strings = deep_copy(punycode_strings)
ConvertBackAndForth(punycode_strings, true)
end
# Converts list of Punycode strings into their UTF-8
# representation.
#
# @param [Array<String>] punycode_strings
# @return [Array<String>] decoded_strings
def DecodePunycodes(punycode_strings)
punycode_strings = deep_copy(punycode_strings)
ConvertBackAndForth(punycode_strings, false)
end
# Encodes the domain name (relative or FQDN) to the Punycode.
#
# @param string decoded domain_name
# @return [String] encoded domain_name
#
# @example
# EncodeDomainName("žížala.jůlinka.go.home")
# -> "xn--ala-qma83eb.xn--jlinka-3mb.go.home"
def EncodeDomainName(decoded_domain_name)
Builtins.mergestring(
EncodePunycodes(Builtins.splitstring(decoded_domain_name, ".")),
"."
)
end
# Decodes the domain name (relative or FQDN) from the Punycode.
#
# @param [String] encoded_domain_name
# @return [String] decoded domain_name
#
# @example
# DecodeDomainName("xn--ala-qma83eb.xn--jlinka-3mb.go.home")
# -> "žížala.jůlinka.go.home"
def DecodeDomainName(encoded_domain_name)
Builtins.mergestring(
DecodePunycodes(Builtins.splitstring(encoded_domain_name, ".")),
"."
)
end
# Decodes the list of domain names to their Unicode representation.
# This function is similar to DecodePunycodes but it works with every
# string as a domain name (that means every domain name is parsed
# by dots and separately evaluated).
#
# @param [Array<String>] encoded_domain_names
# @return [Array<String>] decoded_domain_names
#
# @example
# DocodeDomainNames(["mx1.example.org", "xp3.example.org.", "xn--ala-qma83eb.org.example."])
# -> ["mx1.example.org", "xp3.example.org.", "žížala.org.example."]
def DocodeDomainNames(encoded_domain_names)
encoded_domain_names = deep_copy(encoded_domain_names)
decoded_domain_names = []
strings_to_decode = []
# $[0 : [0, 2], 1 : [3, 5]]
backward_map_of_conversion = {}
current_domain_index = -1
current_domain_item = 0
# parsing all domain names one by one
Builtins.foreach(encoded_domain_names) do |one_domain_name|
current_domain_index = Ops.add(current_domain_index, 1)
start = current_domain_item
# parsing the domain name by dots
Builtins.foreach(Builtins.splitstring(one_domain_name, ".")) do |domain_item|
Ops.set(strings_to_decode, current_domain_item, domain_item)
current_domain_item = Ops.add(current_domain_item, 1)
end
# creating backward index
Ops.set(
backward_map_of_conversion,
current_domain_index,
[start, Ops.subtract(current_domain_item, 1)]
)
end
# Transformating strings to the decoded format
strings_to_decode = DecodePunycodes(strings_to_decode)
current_domain_index = -1
Builtins.foreach(encoded_domain_names) do |one_encoded|
current_domain_index = Ops.add(current_domain_index, 1)
# Where the current string starts and ends
current = Ops.get(backward_map_of_conversion, [current_domain_index, 0])
end_ = Ops.get(backward_map_of_conversion, [current_domain_index, 1])
# error?
if current.nil? || end_.nil?
Builtins.y2error(
"Cannot find start/end for %1 in %2",
one_encoded,
Ops.get(backward_map_of_conversion, current_domain_index)
)
Ops.set(decoded_domain_names, current_domain_index, one_encoded)
else
# create a list of items of the current domain (translated)
decoded_domain = []
while Ops.less_or_equal(current, end_)
decoded_domain = Builtins.add(
decoded_domain,
Ops.get(strings_to_decode, current, "")
)
current = Ops.add(current, 1)
end
# create a domain name from these strings
Ops.set(
decoded_domain_names,
current_domain_index,
Builtins.mergestring(decoded_domain, ".")
)
end
end
deep_copy(decoded_domain_names)
end
publish function: :GetMaximumCacheSize, type: "integer ()"
publish function: :SetMaximumCacheSize, type: "void (integer)"
publish function: :EncodePunycodes, type: "list <string> (list <string>)"
publish function: :DecodePunycodes, type: "list <string> (list <string>)"
publish function: :EncodeDomainName, type: "string (string)"
publish function: :DecodeDomainName, type: "string (string)"
publish function: :DocodeDomainNames, type: "list <string> (list <string>)"
end
Punycode = PunycodeClass.new
Punycode.main
end