124 lines
3.7 KiB
Crystal
124 lines
3.7 KiB
Crystal
require "yaml"
|
|
require "semantic_version"
|
|
|
|
module App::Lib
|
|
struct UserAgent
|
|
REGEXES_PATH = "data/uap_core_regexes.yaml"
|
|
|
|
@@regexes_cache : YAML::Any? = nil
|
|
@@compiled_regexes = {} of String => Array(Tuple(Regex, YAML::Any))
|
|
@@mutex = Mutex.new
|
|
|
|
private def self.load_regexes
|
|
@@mutex.synchronize do
|
|
if @@regexes_cache.nil?
|
|
begin
|
|
regexes_yaml = File.read(REGEXES_PATH)
|
|
@@regexes_cache = YAML.parse(regexes_yaml)
|
|
|
|
# Pre-compile all regexes for better performance
|
|
["user_agent_parsers", "os_parsers", "device_parsers"].each do |parser_type|
|
|
@@compiled_regexes[parser_type] = [] of Tuple(Regex, YAML::Any)
|
|
|
|
@@regexes_cache.not_nil![parser_type].as_a.each do |parser|
|
|
regex_str = parser["regex"].as_s
|
|
options = parser["regex_flag"]?.try(&.as_s) == "i" ?
|
|
Regex::Options::IGNORE_CASE : Regex::Options::None
|
|
|
|
begin
|
|
compiled_regex = Regex.new(regex_str, options)
|
|
@@compiled_regexes[parser_type] << {compiled_regex, parser}
|
|
rescue
|
|
# Skip invalid regexes
|
|
end
|
|
end
|
|
end
|
|
rescue ex
|
|
# If loading fails, set an empty cache to prevent repeated failures
|
|
@@regexes_cache = YAML.parse("{}")
|
|
@@compiled_regexes = {} of String => Array(Tuple(Regex, YAML::Any))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.parse(user_agent_string : String)
|
|
return {nil, nil, nil, nil} if user_agent_string.empty?
|
|
|
|
# Load regexes only once and cache them
|
|
load_regexes
|
|
|
|
family = nil
|
|
version = nil
|
|
device = nil
|
|
os = nil
|
|
|
|
@@compiled_regexes["user_agent_parsers"]?.try &.each do |regex_tuple|
|
|
regex, parser = regex_tuple
|
|
match = regex.match(user_agent_string)
|
|
next unless match
|
|
|
|
family = match[1]? || nil
|
|
v1 = (match[2]? || "0").to_i
|
|
v2 = (match[3]? || "0").to_i
|
|
v3 = (match[4]? || "0").to_i
|
|
|
|
# Apply replacements if defined
|
|
if replacement = parser["family_replacement"]?
|
|
family = replacement.as_s.gsub("$1", family.to_s)
|
|
end
|
|
|
|
version = SemanticVersion.new(v1, v2, v3)
|
|
break
|
|
end
|
|
|
|
@@compiled_regexes["os_parsers"]?.try &.each do |regex_tuple|
|
|
regex, parser = regex_tuple
|
|
match = regex.match(user_agent_string)
|
|
next unless match
|
|
|
|
os_family = match[1]? || nil
|
|
os_v1 = (match[2]? || "0").to_i
|
|
os_v2 = (match[3]? || "0").to_i
|
|
os_v3 = (match[4]? || "0").to_i
|
|
|
|
# Apply replacements if defined
|
|
if replacement = parser["os_replacement"]?
|
|
os_family = replacement.as_s.gsub("$1", os_family.to_s)
|
|
end
|
|
|
|
os = {os_family, SemanticVersion.new(os_v1, os_v2, os_v3)}
|
|
break
|
|
end
|
|
|
|
@@compiled_regexes["device_parsers"]?.try &.each do |regex_tuple|
|
|
regex, parser = regex_tuple
|
|
match = regex.match(user_agent_string)
|
|
next unless match
|
|
|
|
model = match[1]? || nil
|
|
device_name = model
|
|
brand = nil
|
|
|
|
# Apply replacements if defined
|
|
if device_replacement = parser["device_replacement"]?
|
|
device_name = device_replacement.as_s.gsub("$1", device_name.to_s)
|
|
end
|
|
|
|
if model_replacement = parser["model_replacement"]?
|
|
model = model_replacement.as_s.gsub("$1", model.to_s)
|
|
end
|
|
|
|
if brand_replacement = parser["brand_replacement"]?
|
|
brand = brand_replacement.as_s
|
|
end
|
|
|
|
device = {model, brand, device_name}
|
|
break
|
|
end
|
|
|
|
{family, version, device, os}
|
|
end
|
|
end
|
|
end
|