mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-07-03 08:22:01 +02:00
03caf391e7
* web: refactor 1/7 data processing * web: close coverage gaps in data_processing submodules Bring every file under lib/potato_mesh/application/data_processing/ to 100% line coverage so codecov/patch passes on the 1/7 refactor PR. The gap was a relocation of pre-existing untested branches; closing them here keeps the subsequent refactor PRs in the series unblocked. * Add unit tests covering canonical sender/recipient overrides, reply_id/emoji updates on existing rows, and the rare INSERT ConstraintException recovery path inside +insert_message+. * Cover the non-canonical reporter and per-neighbour resolution branches in +insert_neighbors+. * Cover the SQLException rescue in +upsert_ingestor+, the fallback_num branch in +touch_node_last_seen+, the limit fallback in +read_json_body+, the unrecognised-type branch in +store_decrypted_payload+, the +power+ telemetry_type fallback, the default-coercion path in +resolve_numeric_metric+, and the numeric/bare-hex paths in +canonical_node_parts+ and +coerce_trace_node_id+. Drop dead code surfaced while pinning behaviour: * +clear_encrypted+ in +insert_message+ has been initialised to +false+ and never reassigned since #633 dropped the decrypted-text override; remove it and the four dependent branches. * The +rescue ArgumentError; nil+ tails in +identity.resolve_node_num+ and +traces.coerce_trace_node_id+ are unreachable because every +Integer(...)+ call inside is guarded by a regex pre-check. Add a comment to the +data_processing.rb+ shim explaining that the +require_relative+ list is ordered by dependency rather than alphabetically, addressing review nit #5.
200 lines
7.1 KiB
Ruby
200 lines
7.1 KiB
Ruby
# Copyright © 2025-26 l5yth & contributors
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
# frozen_string_literal: true
|
|
|
|
module PotatoMesh
|
|
module App
|
|
module DataProcessing
|
|
# Resolve the numeric representation of a node identifier from a packet payload.
|
|
#
|
|
# The +payload["num"]+ field may arrive as an Integer, a decimal string, or
|
|
# a hexadecimal string (with or without an +0x+ prefix). When the field is
|
|
# absent or ambiguous the method falls back to decoding the hex portion of
|
|
# +node_id+.
|
|
#
|
|
# @param node_id [String, nil] canonical node identifier in +!xxxxxxxx+ form.
|
|
# @param payload [Hash] inbound message payload that may carry a +num+ field.
|
|
# @return [Integer, nil] resolved 32-bit node number or +nil+ when undecidable.
|
|
def resolve_node_num(node_id, payload)
|
|
raw = payload["num"]
|
|
|
|
case raw
|
|
when Integer
|
|
return raw
|
|
when Numeric
|
|
return raw.to_i
|
|
when String
|
|
trimmed = raw.strip
|
|
return nil if trimmed.empty?
|
|
return Integer(trimmed, 10) if trimmed.match?(/\A[0-9]+\z/)
|
|
return Integer(trimmed.delete_prefix("0x").delete_prefix("0X"), 16) if trimmed.match?(/\A0[xX][0-9A-Fa-f]+\z/)
|
|
if trimmed.match?(/\A[0-9A-Fa-f]+\z/)
|
|
canonical = node_id.is_a?(String) ? node_id.strip : ""
|
|
return Integer(trimmed, 16) if canonical.match?(/\A!?[0-9A-Fa-f]+\z/)
|
|
end
|
|
end
|
|
|
|
return nil unless node_id.is_a?(String)
|
|
|
|
hex = node_id.strip
|
|
return nil if hex.empty?
|
|
hex = hex.delete_prefix("!")
|
|
return nil unless hex.match?(/\A[0-9A-Fa-f]+\z/)
|
|
|
|
Integer(hex, 16)
|
|
end
|
|
|
|
# Derive the canonical triplet for a node reference.
|
|
#
|
|
# Accepts an Integer node number, a hex string with or without the +!+
|
|
# sigil, a decimal numeric string, or a +0x+-prefixed hex string. A
|
|
# +fallback_num+ may be provided when +node_ref+ is nil.
|
|
#
|
|
# @param node_ref [Integer, String, nil] raw node identifier from a packet.
|
|
# @param fallback_num [Integer, nil] numeric fallback when +node_ref+ is nil.
|
|
# @return [Array(String, Integer, String), nil] tuple of
|
|
# +[canonical_id, node_num, short_id]+ or +nil+ when the reference cannot
|
|
# be resolved. +canonical_id+ is prefixed with +!+ and zero-padded to
|
|
# eight lowercase hex digits. +short_id+ is the upper-case last four
|
|
# hex digits used for display.
|
|
def canonical_node_parts(node_ref, fallback_num = nil)
|
|
fallback = coerce_integer(fallback_num)
|
|
|
|
hex = nil
|
|
num = nil
|
|
|
|
case node_ref
|
|
when Integer
|
|
num = node_ref
|
|
when Numeric
|
|
num = node_ref.to_i
|
|
when String
|
|
trimmed = node_ref.strip
|
|
return nil if trimmed.empty?
|
|
|
|
if trimmed.start_with?("!")
|
|
hex = trimmed.delete_prefix("!")
|
|
elsif trimmed.match?(/\A0[xX][0-9A-Fa-f]+\z/)
|
|
hex = trimmed[2..].to_s
|
|
elsif trimmed.match?(/\A-?\d+\z/)
|
|
num = trimmed.to_i
|
|
elsif trimmed.match?(/\A[0-9A-Fa-f]+\z/)
|
|
hex = trimmed
|
|
else
|
|
return nil
|
|
end
|
|
when nil
|
|
num = fallback if fallback
|
|
else
|
|
return nil
|
|
end
|
|
|
|
num ||= fallback if fallback
|
|
|
|
if hex
|
|
begin
|
|
num ||= Integer(hex, 16)
|
|
rescue ArgumentError
|
|
return nil
|
|
end
|
|
elsif num
|
|
return nil if num.negative?
|
|
hex = format("%08x", num & 0xFFFFFFFF)
|
|
else
|
|
return nil
|
|
end
|
|
|
|
return nil if hex.nil? || hex.empty?
|
|
|
|
begin
|
|
parsed = Integer(hex, 16)
|
|
rescue ArgumentError
|
|
return nil
|
|
end
|
|
|
|
parsed &= 0xFFFFFFFF
|
|
canonical_hex = format("%08x", parsed)
|
|
short_id = canonical_hex[-4, 4].upcase
|
|
|
|
["!#{canonical_hex}", parsed, short_id]
|
|
end
|
|
|
|
# Detect whether a node reference resolves to the broadcast address.
|
|
#
|
|
# @param node_ref [Integer, String, nil] raw node reference.
|
|
# @param fallback_num [Integer, nil] optional numeric fallback.
|
|
# @return [Boolean] true when the reference matches the broadcast address.
|
|
def broadcast_node_ref?(node_ref, fallback_num = nil)
|
|
return true if fallback_num == 0xFFFFFFFF
|
|
trimmed = string_or_nil(node_ref)
|
|
return false unless trimmed
|
|
normalized = trimmed.delete_prefix("!").strip.downcase
|
|
normalized == "ffffffff"
|
|
end
|
|
|
|
# Converts a protocol identifier such as +meshtastic+ or +mesh-core+ into
|
|
# the display label used in generated node names: capitalised parts joined
|
|
# without a separator (e.g. +Meshtastic+, +MeshCore+).
|
|
#
|
|
# @param protocol [String] protocol identifier.
|
|
# @return [String] formatted display label.
|
|
def protocol_display_label(protocol)
|
|
protocol.split(/[-_]/).map(&:capitalize).join
|
|
end
|
|
|
|
# Returns true if +long_name+ is the synthetic placeholder generated by
|
|
# +ensure_unknown_node+ for the given +node_id+ and +protocol+. Such
|
|
# names carry no real information and must not overwrite a known name
|
|
# already on record.
|
|
#
|
|
# @param long_name [String, nil] candidate long name.
|
|
# @param node_id [String, nil] canonical node identifier.
|
|
# @param protocol [String] protocol identifier the placeholder was generated for.
|
|
# @return [Boolean] true when the long name is a generic placeholder.
|
|
def generic_fallback_name?(long_name, node_id, protocol)
|
|
return false unless long_name && !long_name.empty?
|
|
|
|
parts = canonical_node_parts(node_id)
|
|
return false unless parts
|
|
|
|
short_id = parts[2]
|
|
long_name == "#{protocol_display_label(protocol)} #{short_id}"
|
|
end
|
|
|
|
# Resolve a raw node reference to its canonical row in the +nodes+ table.
|
|
#
|
|
# @param db [SQLite3::Database] open database handle.
|
|
# @param node_ref [Object] raw reference (string, integer, or hex string).
|
|
# @return [String, nil] canonical +node_id+ or nil when no match exists.
|
|
def normalize_node_id(db, node_ref)
|
|
return nil if node_ref.nil?
|
|
ref_str = node_ref.to_s.strip
|
|
return nil if ref_str.empty?
|
|
|
|
node_id = db.get_first_value("SELECT node_id FROM nodes WHERE node_id = ?", [ref_str])
|
|
return node_id if node_id
|
|
|
|
begin
|
|
ref_num = Integer(ref_str, 10)
|
|
rescue ArgumentError
|
|
return nil
|
|
end
|
|
|
|
db.get_first_value("SELECT node_id FROM nodes WHERE num = ?", [ref_num])
|
|
end
|
|
end
|
|
end
|
|
end
|