Files
Goober5000andClaude Opus 4.7 ba7aa70794 Fix 32-bit truncation in the flagset u64 round-trip
Mission flags are saved to the mission file as the full 64-bit flagset value, but three issues prevented anything larger than 32 bits from being saved as intended:

 * `stuff_flagset` parsed the value with `stuff_long`, where `atol` clamps anything above `LONG_MAX`
 * `flagset::from_u64` cast the incoming value to `unsigned long`, truncating bits 32-63
 * `flagset::to_u64` used `bitset::to_ulong` where it should use `bitset::to_ullong`

Fix these issues and add `static_assert` in the flagset functions to defend against future issues.  Also use the proper UINT64_T_ARG where it should be used.

This was first triggered by `Limited_support_rearm_pool`, the first mission flag to occupy bit 31.  Only mission flags are affected; the other flagset serialization paths were audited and are safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-08-30 16:46:11 -04:00

229 lines
5.2 KiB
C++

#ifndef _FLAGSET_
#define _FLAGSET_
#include <bitset>
#include <cstdint>
#include "osapi/dialogs.h"
constexpr size_t UBYTE_SIZE = 8;
template<class T, size_t SIZE = static_cast < size_t >(T::NUM_VALUES)>
class flagset {
protected:
std::bitset<SIZE> values;
public:
flagset() {}
flagset(const std::initializer_list<T>& init_list) {
for (auto& val : init_list) {
set(val);
}
}
// Returns true if any of the given flags are set.
template <typename... Ts>
bool any_of(T first, Ts... rest) const
{
static_assert((std::is_same<Ts, T>::value && ...),
"All arguments to any_of() must be flags of the same enum type");
return (*this)[first] || ((*this)[rest] || ...);
}
// Returns true if none of the given flags are set.
template <typename... Ts>
bool none_of(T first, Ts... rest) const
{
return !any_of(first, rest...);
}
bool operator[](const T idx) const { return values[(static_cast < size_t >(idx))]; };
flagset<T> operator&(const flagset<T>& other) const {
flagset<T> result;
result.values = this->values & other.values;
return result;
}
flagset<T> operator+(const T flag) const {
flagset<T> result = *this;
result.set(flag);
return result;
}
flagset<T>& operator+=(const T flag) {
this->set(flag);
return *this;
}
flagset<T> operator-(const T flag) const {
flagset<T> result = *this;
result.remove(flag);
return result;
}
flagset<T>& operator-=(const T flag) {
this->remove(flag);
return *this;
}
flagset<T> operator|(const flagset<T>& other) const {
flagset<T> result;
result.values = this->values | other.values;
return result;
}
flagset<T>& operator|=(const flagset<T>& other) {
this->values |= other.values;
return *this;
}
void operator|=(const T flag) {
set(flag);
}
bool operator==(const flagset<T>& other) const { return this->values == other.values; }
bool operator!=(const flagset<T>& other) const { return this->values != other.values; }
void reset() { values.reset(); }
flagset<T>& set(T idx, bool value = true) {
Assertion(static_cast<size_t>(idx) < values.size(),
"Invalid value passed to flagset::set(), get a stacktrace, a coder, an old priest and a young priest.");
values.set(static_cast < size_t >(idx), value);
return *this;
}
template<typename TIter>
flagset<T>& set_multiple(TIter begin, TIter end) {
auto current = begin;
while (current != end) {
set(*current, true);
current = std::next(current);
}
return *this;
}
template<typename TIter>
flagset<T>& set_multiple_from_source(const flagset<T>& source, TIter begin, TIter end) {
auto current = begin;
while (current != end) {
if (source[*current]) {
set(*current, true);
}
current = std::next(current);
}
return *this;
}
flagset<T>& remove(T idx) {
return set(idx, false);
}
template<typename TIter>
flagset<T>& remove_multiple(TIter begin, TIter end) {
auto current = begin;
while (current != end) {
set(*current, false);
current = std::next(current);
}
return *this;
}
size_t to_ubyte_vector(SCP_vector<ubyte>& vector_out)
{
int counter = 0; // a counter to determine how far to bitshift
ubyte ubyte_out= 0; // the holder for our bitshifted flags
for (size_t i = 0; i < SIZE; i++) {
// this needs to be at the beginning for push_back after the loop
// to work properly.
if (counter == UBYTE_SIZE) {
vector_out.push_back(ubyte_out);
counter = 0;
ubyte_out = 0;
}
// check whether that flag was set
// there is probably a more efficient way to do this,
// but not unless we can copy the underlying data safely.
if (values[i]) {
ubyte_out |= 1 << counter;
}
// iterate
++counter;
}
vector_out.push_back(ubyte_out);
return vector_out.size();
}
void set_from_vector(const SCP_vector<ubyte>& vector_in)
{
size_t flag = 0;
// just iterate through the entries, and set flags as appropriate.
for (auto& entry : vector_in) {
for (uint bitshift = 0; bitshift < UBYTE_SIZE; bitshift++){
if (entry & (1 << bitshift)) {
values.set(flag, true);
}
++flag;
// this function is used in multi where the size of flagsets can be different
// on server and client. Small differences in flagset sizes should not affect
// gameplay, but we have to make sure not to exceed the size of the flagset.
if (flag >= SIZE){
return;
}
}
}
}
flagset<T>& toggle(T idx) {
values[static_cast < size_t >(idx)] = !values[static_cast < size_t >(idx)];
return *this;
}
bool any_set() const { return values.any(); }
bool none_set() const { return values.none(); }
void from_u64(std::uint64_t num)
{
static_assert(SIZE <= 64, "from_u64 can only be used with flagsets of up to 64 flags");
values = std::bitset<SIZE>(num);
}
std::uint64_t to_u64() const
{
static_assert(SIZE <= 64, "to_u64 can only be used with flagsets of up to 64 flags");
return static_cast<std::uint64_t>(values.to_ullong());
}
size_t hash() const { return std::hash<std::bitset<SIZE>>()(values); }
};
namespace std {
template <typename T, size_t N>
struct hash<::flagset<T, N>> {
size_t operator()(const ::flagset<T, N>& val) const { return val.hash(); }
};
} // namespace std
#define FLAG_LIST(Type) enum class Type : size_t
#endif