Spicy: Rework code for converting Spicy values to Zeek values.

The logic was template-based so far, which wasn't great because: (1)
conceptually, it models the Spicy types at the wrong layer (C++ rather
than HILTI types), and (2) stopped working with some recent Spicy
updates (which we have temporarily reverted in the meantime to keep
Zeek working).

The new code is based on HILTI's runtime type information and the
corresponding introspection API, pretty much like `spicy-dump` works
as well. This is the recommended approach for working with HILTI
values, and generally much cleaner.

This is on top of https://github.com/zeek/zeek/pull/4300.
This commit is contained in:
Robin Sommer 2025-03-19 12:09:54 +01:00
parent af46322152
commit 000ed528dc
No known key found for this signature in database
GPG key ID: D8187293B3FFE5D0
3 changed files with 424 additions and 481 deletions

View file

@ -491,491 +491,15 @@ void forward_packet(const hilti::rt::integer::safe<uint32_t>& identifier);
/** Gets the network time from Zeek. */
hilti::rt::Time network_time();
// Forward-declare to_val() functions.
template<typename... Ts>
ValPtr to_val(const hilti::rt::Tuple<Ts...>& t, const hilti::rt::TypeInfo* ti, const TypePtr& target);
template<typename... Ts>
inline ValPtr to_val(const hilti::rt::Bitfield<Ts...>& v, const hilti::rt::TypeInfo* ti, const TypePtr& target);
template<typename T, typename std::enable_if_t<std::is_base_of<::hilti::rt::trait::isStruct, T>::value>* = nullptr>
ValPtr to_val(const T& t, const hilti::rt::TypeInfo* ti, const TypePtr& target);
template<typename T, typename std::enable_if_t<std::is_enum<typename T::Value>::value>* = nullptr>
ValPtr to_val(const T& t, const hilti::rt::TypeInfo* ti, const TypePtr& target);
template<typename T, typename std::enable_if_t<std::is_enum<T>::value>* = nullptr>
ValPtr to_val(const T& t, const hilti::rt::TypeInfo* ti, const TypePtr& target);
template<typename K, typename V>
ValPtr to_val(const hilti::rt::Map<K, V>& s, const hilti::rt::TypeInfo* ti, const TypePtr& target);
template<typename T>
ValPtr to_val(const hilti::rt::Set<T>& s, const hilti::rt::TypeInfo* ti, const TypePtr& target);
template<typename T, typename Allocator>
ValPtr to_val(const hilti::rt::Vector<T, Allocator>& v, const hilti::rt::TypeInfo* ti, const TypePtr& target);
template<typename T>
ValPtr to_val(const std::optional<T>& t, const hilti::rt::TypeInfo* ti, const TypePtr& target);
template<typename T>
ValPtr to_val(hilti::rt::integer::safe<T> i, const hilti::rt::TypeInfo* ti, const TypePtr& target);
template<typename T>
ValPtr to_val(const hilti::rt::ValueReference<T>& t, const hilti::rt::TypeInfo* ti, const TypePtr& target);
inline ValPtr to_val(const hilti::rt::Bool& b, const hilti::rt::TypeInfo* ti, const TypePtr& target);
inline ValPtr to_val(const hilti::rt::Address& d, const hilti::rt::TypeInfo* ti, const TypePtr& target);
inline ValPtr to_val(const hilti::rt::Bytes& b, const hilti::rt::TypeInfo* ti, const TypePtr& target);
inline ValPtr to_val(const hilti::rt::Interval& t, const hilti::rt::TypeInfo* ti, const TypePtr& target);
inline ValPtr to_val(const hilti::rt::Port& d, const hilti::rt::TypeInfo* ti, const TypePtr& target);
inline ValPtr to_val(const hilti::rt::Time& t, const hilti::rt::TypeInfo* ti, const TypePtr& target);
inline ValPtr to_val(const std::string& s, const hilti::rt::TypeInfo* ti, const TypePtr& target);
inline ValPtr to_val(double r, const hilti::rt::TypeInfo* ti, const TypePtr& target);
/**
* Converts a Spicy-side optional value to a Zeek value. This assumes the
* optional is set, and will throw an exception if not. The result is
* returned with ref count +1.
*/
template<typename T>
inline ValPtr to_val(const std::optional<T>& t, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
if ( t.has_value() )
return to_val(hilti::rt::optional::value(t), nullptr, target);
return nullptr;
}
/**
* Converts a Spicy-side string to a Zeek value. The result is returned with
* ref count +1.
*/
inline ValPtr to_val(const std::string& s, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
if ( target->Tag() != TYPE_STRING )
throw ParameterMismatch("string", target);
return make_intrusive<StringVal>(s);
}
/**
* Converts a Spicy-side bytes instance to a Zeek value. The result is returned with
* ref count +1.
*/
inline ValPtr to_val(const hilti::rt::Bytes& b, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
if ( target->Tag() != TYPE_STRING )
throw ParameterMismatch("string", target);
return make_intrusive<StringVal>(b.str());
}
/**
* Converts a Spicy-side integer to a Zeek value. The result is
* returned with ref count +1.
*/
template<typename T>
inline ValPtr to_val(hilti::rt::integer::safe<T> i, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
ValPtr v = nullptr;
if constexpr ( std::is_unsigned<T>::value ) {
if ( target->Tag() == TYPE_COUNT )
return val_mgr->Count(i);
if ( target->Tag() == TYPE_INT )
return val_mgr->Int(i);
throw ParameterMismatch("uint64", target);
}
else {
if ( target->Tag() == TYPE_INT )
return val_mgr->Int(i);
if ( target->Tag() == TYPE_COUNT ) {
if ( i >= 0 )
return val_mgr->Count(i);
else
throw ParameterMismatch("negative int64", target);
}
throw ParameterMismatch("int64", target);
}
namespace detail {
extern ValPtr to_val(const hilti::rt::type_info::Value& value, const TypePtr& target);
}
template<typename T>
ValPtr to_val(const hilti::rt::ValueReference<T>& t, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
if ( auto* x = t.get() )
return to_val(*x, nullptr, target);
return nullptr;
ValPtr to_val(const T& value, const hilti::rt::TypeInfo* type, const TypePtr& target) {
return detail::to_val(hilti::rt::type_info::Value(&value, type), target);
}
/**
* Converts a Spicy-side signed bool to a Zeek value. The result is
* returned with ref count +1.
*/
inline ValPtr to_val(const hilti::rt::Bool& b, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
if ( target->Tag() != TYPE_BOOL )
throw ParameterMismatch("bool", target);
return val_mgr->Bool(b);
}
/**
* Converts a Spicy-side real to a Zeek value. The result is returned with
* ref count +1.
*/
inline ValPtr to_val(double r, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
if ( target->Tag() != TYPE_DOUBLE )
throw ParameterMismatch("double", target);
return make_intrusive<DoubleVal>(r);
}
/**
* Converts a Spicy-side address to a Zeek value. The result is returned with
* ref count +1.
*/
inline ValPtr to_val(const hilti::rt::Address& d, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
if ( target->Tag() != TYPE_ADDR )
throw ParameterMismatch("addr", target);
auto in_addr = d.asInAddr();
if ( auto v4 = std::get_if<struct in_addr>(&in_addr) )
return make_intrusive<AddrVal>(IPAddr(*v4));
else {
auto v6 = std::get<struct in6_addr>(in_addr);
return make_intrusive<AddrVal>(IPAddr(v6));
}
}
/**
* Converts a Spicy-side address to a Zeek value. The result is returned with
* ref count +1.
*/
inline ValPtr to_val(const hilti::rt::Port& p, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
if ( target->Tag() != TYPE_PORT )
throw ParameterMismatch("port", target);
switch ( p.protocol().value() ) {
case hilti::rt::Protocol::TCP: return val_mgr->Port(p.port(), ::TransportProto::TRANSPORT_TCP);
case hilti::rt::Protocol::UDP: return val_mgr->Port(p.port(), ::TransportProto::TRANSPORT_UDP);
case hilti::rt::Protocol::ICMP: return val_mgr->Port(p.port(), ::TransportProto::TRANSPORT_ICMP);
default: throw InvalidValue("port value with undefined protocol");
}
}
/**
* Converts a Spicy-side time to a Zeek value. The result is returned with
* ref count +1.
*/
inline ValPtr to_val(const hilti::rt::Interval& i, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
if ( target->Tag() != TYPE_INTERVAL )
throw ParameterMismatch("interval", target);
return make_intrusive<IntervalVal>(i.seconds());
}
/**
* Converts a Spicy-side time to a Zeek value. The result is returned with
* ref count +1.
*/
inline ValPtr to_val(const hilti::rt::Time& t, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
if ( target->Tag() != TYPE_TIME )
throw ParameterMismatch("time", target);
return make_intrusive<TimeVal>(t.seconds());
}
/**
* Converts a Spicy-side vector to a Zeek value. The result is returned with
* ref count +1.
*/
template<typename T, typename Allocator>
inline ValPtr to_val(const hilti::rt::Vector<T, Allocator>& v, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
if ( target->Tag() != TYPE_VECTOR && target->Tag() != TYPE_LIST )
throw ParameterMismatch("expected vector or list", target);
auto vt = cast_intrusive<VectorType>(target);
auto zv = make_intrusive<VectorVal>(vt);
for ( const auto& i : v )
zv->Assign(zv->Size(), to_val(i, nullptr, vt->Yield()));
return std::move(zv);
}
/**
* Converts a Spicy-side map to a Zeek value. The result is returned with
* ref count +1.
*/
template<typename K, typename V>
inline ValPtr to_val(const hilti::rt::Map<K, V>& m, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
if ( target->Tag() != TYPE_TABLE )
throw ParameterMismatch("map", target);
auto tt = cast_intrusive<TableType>(target);
if ( tt->IsSet() )
throw ParameterMismatch("map", target);
if ( tt->GetIndexTypes().size() != 1 )
throw ParameterMismatch("map with non-tuple elements", target);
auto zv = make_intrusive<TableVal>(tt);
for ( const auto& i : m ) {
auto k = to_val(i.first, nullptr, tt->GetIndexTypes()[0]);
auto v = to_val(i.second, nullptr, tt->Yield());
zv->Assign(std::move(k), std::move(v));
}
return zv;
} // namespace spicy::rt
/**
* Converts a Spicy-side set to a Zeek value. The result is returned with
* ref count +1.
*/
template<typename T>
inline ValPtr to_val(const hilti::rt::Set<T>& s, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
if ( target->Tag() != TYPE_TABLE )
throw ParameterMismatch("set", target);
auto tt = cast_intrusive<TableType>(target);
if ( ! tt->IsSet() )
throw ParameterMismatch("set", target);
auto zv = make_intrusive<TableVal>(tt);
for ( const auto& i : s ) {
if ( tt->GetIndexTypes().size() != 1 )
throw ParameterMismatch("set with non-tuple elements", target);
auto idx = to_val(i, nullptr, tt->GetIndexTypes()[0]);
zv->Assign(std::move(idx), nullptr);
}
return zv;
}
namespace {
template<typename, template<typename...> typename>
struct is_instance_impl : std::false_type {};
template<template<typename...> typename U, typename... Ts>
struct is_instance_impl<U<Ts...>, U> : std::true_type {};
} // namespace
template<typename T, template<typename...> typename U>
using is_instance = is_instance_impl<std::remove_cv_t<T>, U>;
template<typename T>
inline void set_record_field(RecordVal* rval, const IntrusivePtr<RecordType>& rtype, int idx, const T& x) {
using NoConversionNeeded = std::integral_constant<
bool, std::is_same_v<T, int8_t> || std::is_same_v<T, int16_t> || std::is_same_v<T, int32_t> ||
std::is_same_v<T, int64_t> || std::is_same_v<T, uint8_t> || std::is_same_v<T, uint16_t> ||
std::is_same_v<T, uint32_t> || std::is_same_v<T, uint64_t> || std::is_same_v<T, double> ||
std::is_same_v<T, std::string> || std::is_same_v<T, bool>>;
using IsSignedInteger = std::integral_constant<bool, std::is_same_v<T, hilti::rt::integer::safe<int8_t>> ||
std::is_same_v<T, hilti::rt::integer::safe<int16_t>> ||
std::is_same_v<T, hilti::rt::integer::safe<int32_t>> ||
std::is_same_v<T, hilti::rt::integer::safe<int64_t>>>;
using IsUnsignedInteger = std::integral_constant<bool, std::is_same_v<T, hilti::rt::integer::safe<uint8_t>> ||
std::is_same_v<T, hilti::rt::integer::safe<uint16_t>> ||
std::is_same_v<T, hilti::rt::integer::safe<uint32_t>> ||
std::is_same_v<T, hilti::rt::integer::safe<uint64_t>>>;
if constexpr ( NoConversionNeeded::value )
rval->Assign(idx, x);
else if constexpr ( IsSignedInteger::value )
rval->Assign(idx, static_cast<int64_t>(x.Ref()));
else if constexpr ( IsUnsignedInteger::value )
rval->Assign(idx, static_cast<uint64_t>(x.Ref()));
else if constexpr ( std::is_same_v<T, hilti::rt::Bytes> )
rval->Assign(idx, x.str());
else if constexpr ( std::is_same_v<T, hilti::rt::Bool> )
rval->Assign(idx, static_cast<bool>(x));
else if constexpr ( std::is_same_v<T, std::string> )
rval->Assign(idx, x);
else if constexpr ( std::is_same_v<T, hilti::rt::Time> )
rval->AssignTime(idx, x.seconds());
else if constexpr ( std::is_same_v<T, hilti::rt::Interval> )
rval->AssignInterval(idx, x.seconds());
else if constexpr ( std::is_same_v<T, hilti::rt::Null> ) {
// "Null" turns into an unset optional record field.
}
else if constexpr ( is_instance<T, std::optional>::value ) {
if ( x.has_value() )
set_record_field(rval, rtype, idx, *x);
}
else {
ValPtr v = nullptr;
// This may return a nullptr in cases where the field is to be left unset.
v = to_val(x, nullptr, rtype->GetFieldType(idx));
if ( v )
rval->Assign(idx, v);
else {
// Field must be &optional or &default.
if ( auto attrs = rtype->FieldDecl(idx)->attrs;
! attrs || ! (attrs->Find(detail::ATTR_DEFAULT) || attrs->Find(detail::ATTR_OPTIONAL)) )
throw ParameterMismatch(hilti::rt::fmt("missing initialization for field '%s'", rtype->FieldName(idx)));
}
}
}
/**
* Converts a Spicy-side tuple to a Zeek record value. The result is returned
* with ref count +1.
*/
template<typename... Ts>
ValPtr to_val(const hilti::rt::Tuple<Ts...>& t, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
if ( target->Tag() != TYPE_RECORD )
throw ParameterMismatch("tuple", target);
auto rtype = cast_intrusive<RecordType>(target);
if ( sizeof...(Ts) != rtype->NumFields() )
throw ParameterMismatch("tuple", target);
auto rval = make_intrusive<RecordVal>(rtype);
size_t idx = 0;
hilti::rt::tuple_for_each(t, [&](const auto& x) { set_record_field(rval.get(), rtype, idx++, x); });
return rval;
}
/**
* Converts a Spicy-side bitfield to a Zeek record value. The result is returned
* with ref count +1.
*/
template<typename... Ts>
inline ValPtr to_val(const hilti::rt::Bitfield<Ts...>& v, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
using Bitfield = hilti::rt::Bitfield<Ts...>;
if ( target->Tag() != TYPE_RECORD )
throw ParameterMismatch("bitfield", target);
auto rtype = cast_intrusive<RecordType>(target);
if ( sizeof...(Ts) - 1 != rtype->NumFields() )
throw ParameterMismatch("bitfield", target);
auto rval = make_intrusive<RecordVal>(rtype);
size_t idx = 0;
hilti::rt::tuple_for_each(v.value, [&](const auto& x) {
if ( idx < sizeof...(Ts) - 1 ) // last element is original integer value, with no record equivalent
set_record_field(rval.get(), rtype, idx++, x);
});
return rval;
}
template<typename>
constexpr bool is_optional_impl = false;
template<typename T>
constexpr bool is_optional_impl<std::optional<T>> = true;
template<typename T>
constexpr bool is_optional = is_optional_impl<std::remove_cv_t<std::remove_reference_t<T>>>;
/**
* Converts Spicy-side struct to a Zeek record value. The result is returned
* with a ref count +1.
*/
template<typename T, typename std::enable_if_t<std::is_base_of<::hilti::rt::trait::isStruct, T>::value>*>
inline ValPtr to_val(const T& t, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
if ( target->Tag() != TYPE_RECORD )
throw ParameterMismatch("struct", target);
auto rtype = cast_intrusive<RecordType>(target);
auto rval = make_intrusive<RecordVal>(rtype);
int idx = 0;
auto num_fields = rtype->NumFields();
t.__visit([&](std::string_view name, const auto& val) {
if ( idx >= num_fields )
throw ParameterMismatch(hilti::rt::fmt("no matching record field for field '%s'", name));
// Special-case: Lift up anonymous bitfields (which always come as std::optionals).
if ( name == "<anon>" ) {
using X = typename std::decay<decltype(val)>::type;
if constexpr ( is_optional<X> ) {
if constexpr ( std::is_base_of<::hilti::rt::trait::isBitfield, typename X::value_type>::value ) {
size_t j = 0;
hilti::rt::tuple_for_each(val->value, [&](const auto& x) {
if ( j++ < std::tuple_size<decltype(val->value)>() -
1 ) // last element is original integer value, with no record equivalent
set_record_field(rval.get(), rtype, idx++, x);
});
return;
}
}
// There can't be any other anonymous fields.
auto msg = hilti::rt::fmt("unexpected anonymous field: %s", name);
reporter->InternalError("%s", msg.c_str());
}
else {
auto field = rtype->GetFieldType(idx);
std::string field_name = rtype->FieldName(idx);
if ( field_name != name )
throw ParameterMismatch(
hilti::rt::fmt("mismatch in field name: expected '%s', found '%s'", name, field_name));
set_record_field(rval.get(), rtype, idx++, val);
}
});
// We already check above that all Spicy-side fields are mapped so we
// can only hit this if there are uninitialized Zeek-side fields left.
if ( idx != num_fields )
throw ParameterMismatch(hilti::rt::fmt("missing initialization for field '%s'", rtype->FieldName(idx + 1)));
return rval;
}
/** Maps HILTI's `Protocol` enum to Zeek's `transport_proto` enum. */
inline ValPtr to_val_for_transport_proto(int64_t val, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
switch ( val ) {
case hilti::rt::Protocol::TCP: return id::transport_proto->GetEnumVal(::TransportProto::TRANSPORT_TCP);
case hilti::rt::Protocol::UDP: return id::transport_proto->GetEnumVal(::TransportProto::TRANSPORT_UDP);
case hilti::rt::Protocol::ICMP: return id::transport_proto->GetEnumVal(::TransportProto::TRANSPORT_ICMP);
case hilti::rt::Protocol::Undef: [[fallthrough]]; // just for readability, make Undef explicit
default: return id::transport_proto->GetEnumVal(::TransportProto::TRANSPORT_UNKNOWN);
}
hilti::rt::cannot_be_reached();
}
/**
* Converts a Spicy-side enum to a Zeek enum value. The result is returned
* with ref count +1.
*/
template<typename T, typename std::enable_if_t<std::is_enum<typename T::Value>::value>*>
inline ValPtr to_val(const T& t, const hilti::rt::TypeInfo* ti, const TypePtr& target) {
if ( target->Tag() != TYPE_ENUM )
throw ParameterMismatch("enum", target);
// We'll usually be getting an int64_t for T, but allow other signed ints
// as well.
static_assert(std::is_signed<std::underlying_type_t<typename T::Value>>{});
auto it = static_cast<int64_t>(t.value());
// Special case: map enum values to Zeek's semantics.
if ( target->GetName() == "transport_proto" ) {
if ( ! std::is_same_v<T, hilti::rt::Protocol> )
throw ParameterMismatch(hilti::rt::demangle(typeid(t).name()), target);
return to_val_for_transport_proto(it, nullptr, target);
}
// Zeek's enum can't be negative, so we swap in max_int for our Undef (-1).
if ( it == std::numeric_limits<int64_t>::max() )
// can't allow this ...
throw InvalidValue("enum values with value max_int not supported by Zeek integration");
zeek_int_t bt = (it >= 0 ? it : std::numeric_limits<::zeek_int_t>::max());
return target->AsEnumType()->GetEnumVal(bt);
}
/**
* Returns the Zeek value associated with a global Zeek-side ID. Throws if the
* ID does not exist.