channels: replace Member's six parallel prefix bools (oprefix/owner/admin/op/halfop/voice) with a single u8 bitfield (PFX_*) + inline bool accessors/mutators — same semantics, one byte instead of six, no more risk of the flags drifting out of sync; all call sites go through op()/set_op()-style methods

This commit is contained in:
Jean Chevronnet 2026-08-19 02:47:04 +00:00
parent 16fae5b23c
commit 1aa02a08b9
6 changed files with 129 additions and 76 deletions

View file

@ -12,18 +12,24 @@ use crate::Uid;
/// Per-member prefix modes (+q/+a/+o/+h/+v). Flag modes live in [`ChanModes`]. /// Per-member prefix modes (+q/+a/+o/+h/+v). Flag modes live in [`ChanModes`].
#[derive(Default)] #[derive(Default)]
pub struct Member { pub struct Member {
pub oprefix: bool, // operprefix/ojoin: server oper prefix (!), highest rank /// Built-in prefix modes held, as a bitfield of `PFX_*` (was six parallel bools:
pub owner: bool, // +q (~) /// oprefix `!`, owner `~`, admin `&`, op `@`, halfop `%`, voice `+`). Read/write
pub admin: bool, // +a (&) /// through the `op()`/`set_op()`-style accessors below.
pub op: bool, // +o (@) pub prefixes: u8,
pub halfop: bool, // +h (%)
pub voice: bool, // +v (+)
pub custom_prefixes: Vec<char>, // config-defined prefix mode letters held (customprefix) pub custom_prefixes: Vec<char>, // config-defined prefix mode letters held (customprefix)
pub joined: u64, // unix ts this member joined (for +d delaymsg; 0 = unknown) pub joined: u64, // unix ts this member joined (for +d delaymsg; 0 = unknown)
pub recent_msgs: Vec<String>, // +K repeat: this member's last few lines here pub recent_msgs: Vec<String>, // +K repeat: this member's last few lines here
pub hidden: bool, // +D delayjoin: JOIN withheld until they reveal themselves pub hidden: bool, // +D delayjoin: JOIN withheld until they reveal themselves
} }
/// Built-in prefix bits held in [`Member::prefixes`], high→low.
pub const PFX_OPER: u8 = 0b10_0000; // ! network staff (operprefix/ojoin)
pub const PFX_OWNER: u8 = 0b01_0000; // ~ +q
pub const PFX_ADMIN: u8 = 0b00_1000; // & +a
pub const PFX_OP: u8 = 0b00_0100; // @ +o
pub const PFX_HALFOP: u8 = 0b00_0010; // % +h
pub const PFX_VOICE: u8 = 0b00_0001; // + +v
/// Prefix ranks, high→low — gate who may grant a prefix / kick whom. Spaced ×10 so /// Prefix ranks, high→low — gate who may grant a prefix / kick whom. Spaced ×10 so
/// config-defined custom prefixes (modules::customprefix) can slot in between. /// config-defined custom prefixes (modules::customprefix) can slot in between.
pub const RANK_OPER: u8 = 60; // operprefix/ojoin — above channel owner (network staff) pub const RANK_OPER: u8 = 60; // operprefix/ojoin — above channel owner (network staff)
@ -34,20 +40,71 @@ pub const RANK_HALFOP: u8 = 20;
pub const RANK_VOICE: u8 = 10; pub const RANK_VOICE: u8 = 10;
impl Member { impl Member {
#[inline]
pub fn oprefix(&self) -> bool {
self.prefixes & PFX_OPER != 0
}
#[inline]
pub fn owner(&self) -> bool {
self.prefixes & PFX_OWNER != 0
}
#[inline]
pub fn admin(&self) -> bool {
self.prefixes & PFX_ADMIN != 0
}
#[inline]
pub fn op(&self) -> bool {
self.prefixes & PFX_OP != 0
}
#[inline]
pub fn halfop(&self) -> bool {
self.prefixes & PFX_HALFOP != 0
}
#[inline]
pub fn voice(&self) -> bool {
self.prefixes & PFX_VOICE != 0
}
#[inline]
fn set_bit(&mut self, bit: u8, on: bool) {
if on {
self.prefixes |= bit;
} else {
self.prefixes &= !bit;
}
}
pub fn set_oprefix(&mut self, on: bool) {
self.set_bit(PFX_OPER, on);
}
pub fn set_owner(&mut self, on: bool) {
self.set_bit(PFX_OWNER, on);
}
pub fn set_admin(&mut self, on: bool) {
self.set_bit(PFX_ADMIN, on);
}
pub fn set_op(&mut self, on: bool) {
self.set_bit(PFX_OP, on);
}
pub fn set_halfop(&mut self, on: bool) {
self.set_bit(PFX_HALFOP, on);
}
pub fn set_voice(&mut self, on: bool) {
self.set_bit(PFX_VOICE, on);
}
/// This member's numeric rank (0 = plain member). /// This member's numeric rank (0 = plain member).
/// Built-in tier rank from the fixed booleans (0 = none), ignoring custom prefixes. /// Built-in tier rank from the fixed prefix bits (0 = none), ignoring custom prefixes.
fn builtin_rank(&self) -> u8 { fn builtin_rank(&self) -> u8 {
if self.oprefix { if self.oprefix() {
RANK_OPER RANK_OPER
} else if self.owner { } else if self.owner() {
RANK_OWNER RANK_OWNER
} else if self.admin { } else if self.admin() {
RANK_ADMIN RANK_ADMIN
} else if self.op { } else if self.op() {
RANK_OP RANK_OP
} else if self.halfop { } else if self.halfop() {
RANK_HALFOP RANK_HALFOP
} else if self.voice { } else if self.voice() {
RANK_VOICE RANK_VOICE
} else { } else {
0 0
@ -70,12 +127,12 @@ impl Member {
use crate::modules::customprefix::{def_for_letter, sigil}; use crate::modules::customprefix::{def_for_letter, sigil};
let mut v: Vec<(u8, &'static str)> = Vec::new(); let mut v: Vec<(u8, &'static str)> = Vec::new();
for (on, r, i) in [ for (on, r, i) in [
(self.oprefix, RANK_OPER, 0), (self.oprefix(), RANK_OPER, 0),
(self.owner, RANK_OWNER, 1), (self.owner(), RANK_OWNER, 1),
(self.admin, RANK_ADMIN, 2), (self.admin(), RANK_ADMIN, 2),
(self.op, RANK_OP, 3), (self.op(), RANK_OP, 3),
(self.halfop, RANK_HALFOP, 4), (self.halfop(), RANK_HALFOP, 4),
(self.voice, RANK_VOICE, 5), (self.voice(), RANK_VOICE, 5),
] { ] {
if on { if on {
v.push((r, sigil(i))); v.push((r, sigil(i)));
@ -96,17 +153,17 @@ impl Member {
use crate::modules::customprefix::sigil; use crate::modules::customprefix::sigil;
if self.custom_prefixes.is_empty() { if self.custom_prefixes.is_empty() {
// fast path: built-in tiers only // fast path: built-in tiers only
if self.oprefix { if self.oprefix() {
sigil(0) sigil(0)
} else if self.owner { } else if self.owner() {
sigil(1) sigil(1)
} else if self.admin { } else if self.admin() {
sigil(2) sigil(2)
} else if self.op { } else if self.op() {
sigil(3) sigil(3)
} else if self.halfop { } else if self.halfop() {
sigil(4) sigil(4)
} else if self.voice { } else if self.voice() {
sigil(5) sigil(5)
} else { } else {
"" ""
@ -121,21 +178,17 @@ impl Member {
/// Drop the standard status modes (q/a/o/h/v) — used when this side loses a /// Drop the standard status modes (q/a/o/h/v) — used when this side loses a
/// channel-timestamp war and every member must be de-statused. /// channel-timestamp war and every member must be de-statused.
pub fn clear_status(&mut self) { pub fn clear_status(&mut self) {
self.owner = false; self.prefixes &= !(PFX_OWNER | PFX_ADMIN | PFX_OP | PFX_HALFOP | PFX_VOICE);
self.admin = false;
self.op = false;
self.halfop = false;
self.voice = false;
} }
pub fn set_prefix(&mut self, letter: char, on: bool) { pub fn set_prefix(&mut self, letter: char, on: bool) {
match letter { match letter {
'y' => self.oprefix = on, 'y' => self.set_bit(PFX_OPER, on),
'q' => self.owner = on, 'q' => self.set_bit(PFX_OWNER, on),
'a' => self.admin = on, 'a' => self.set_bit(PFX_ADMIN, on),
'o' => self.op = on, 'o' => self.set_bit(PFX_OP, on),
'h' => self.halfop = on, 'h' => self.set_bit(PFX_HALFOP, on),
'v' => self.voice = on, 'v' => self.set_bit(PFX_VOICE, on),
_ => { _ => {
if crate::modules::customprefix::def_for_letter(letter).is_some() { if crate::modules::customprefix::def_for_letter(letter).is_some() {
self.custom_prefixes.retain(|&c| c != letter); self.custom_prefixes.retain(|&c| c != letter);
@ -153,12 +206,12 @@ impl Member {
if self.custom_prefixes.is_empty() { if self.custom_prefixes.is_empty() {
let mut s = String::new(); let mut s = String::new();
for (on, i) in [ for (on, i) in [
(self.oprefix, 0), (self.oprefix(), 0),
(self.owner, 1), (self.owner(), 1),
(self.admin, 2), (self.admin(), 2),
(self.op, 3), (self.op(), 3),
(self.halfop, 4), (self.halfop(), 4),
(self.voice, 5), (self.voice(), 5),
] { ] {
if on { if on {
s.push_str(sigil(i)); s.push_str(sigil(i));
@ -177,12 +230,12 @@ impl Member {
use crate::modules::customprefix::def_for_letter; use crate::modules::customprefix::def_for_letter;
let mut s = String::new(); let mut s = String::new();
for (on, l) in [ for (on, l) in [
(self.oprefix, 'y'), (self.oprefix(), 'y'),
(self.owner, 'q'), (self.owner(), 'q'),
(self.admin, 'a'), (self.admin(), 'a'),
(self.op, 'o'), (self.op(), 'o'),
(self.halfop, 'h'), (self.halfop(), 'h'),
(self.voice, 'v'), (self.voice(), 'v'),
] { ] {
if on { if on {
s.push(l); s.push(l);
@ -898,7 +951,7 @@ impl Server {
ch.members.insert( ch.members.insert(
uid, uid,
Member { Member {
op: is_new, prefixes: if is_new { PFX_OP } else { 0 },
joined: now(), joined: now(),
..Default::default() ..Default::default()
}, },
@ -1607,15 +1660,15 @@ mod tests {
let mut m = Member::default(); let mut m = Member::default();
assert_eq!(m.rank(), 0); assert_eq!(m.rank(), 0);
assert_eq!(m.prefix_char(), ""); assert_eq!(m.prefix_char(), "");
m.voice = true; m.set_voice(true);
assert_eq!((m.rank(), m.prefix_char()), (RANK_VOICE, "+")); assert_eq!((m.rank(), m.prefix_char()), (RANK_VOICE, "+"));
m.halfop = true; m.set_halfop(true);
assert_eq!((m.rank(), m.prefix_char()), (RANK_HALFOP, "%")); assert_eq!((m.rank(), m.prefix_char()), (RANK_HALFOP, "%"));
m.op = true; m.set_op(true);
assert_eq!((m.rank(), m.prefix_char()), (RANK_OP, "@")); assert_eq!((m.rank(), m.prefix_char()), (RANK_OP, "@"));
m.admin = true; m.set_admin(true);
assert_eq!((m.rank(), m.prefix_char()), (RANK_ADMIN, "&")); assert_eq!((m.rank(), m.prefix_char()), (RANK_ADMIN, "&"));
m.owner = true; m.set_owner(true);
assert_eq!((m.rank(), m.prefix_char()), (RANK_OWNER, "~")); assert_eq!((m.rank(), m.prefix_char()), (RANK_OWNER, "~"));
} }
} }

View file

@ -2526,8 +2526,8 @@ mod tests {
let msg = crate::message::parse(":42SB00000 IJOIN #echoircd 16 1 ao").unwrap(); let msg = crate::message::parse(":42SB00000 IJOIN #echoircd 16 1 ao").unwrap();
s.link_ijoin_recv(1, &msg); s.link_ijoin_recv(1, &msg);
let m = &s.channels["#echoircd"].rmembers["42SB00000"]; let m = &s.channels["#echoircd"].rmembers["42SB00000"];
assert!(m.admin, "bot should hold +a (&) from the IJOIN status token"); assert!(m.admin(), "bot should hold +a (&) from the IJOIN status token");
assert!(m.op, "bot should hold +o (@) from the IJOIN status token"); assert!(m.op(), "bot should hold +o (@) from the IJOIN status token");
} }
// A server is a service iff its NAME matches the sasl_server or a `uline` config // A server is a service iff its NAME matches the sasl_server or a `uline` config
@ -2565,7 +2565,7 @@ mod tests {
// a peer bursts #c with a NEWER TS, opping bob — bob must join WITHOUT +o // a peer bursts #c with a NEWER TS, opping bob — bob must join WITHOUT +o
let m = crate::message::parse(":42S FJOIN #c 2000 +nt :o,42SAAAAAA").unwrap(); let m = crate::message::parse(":42S FJOIN #c 2000 +nt :o,42SAAAAAA").unwrap();
s.link_fjoin_recv(1, &m); s.link_fjoin_recv(1, &m);
let opped = s.channels["#c"].rmembers["42SAAAAAA"].op; let opped = s.channels["#c"].rmembers["42SAAAAAA"].op();
assert!(!opped, "a member bursted with a newer (losing) TS must be de-statused"); assert!(!opped, "a member bursted with a newer (losing) TS must be de-statused");
assert_eq!(s.channels["#c"].created, 1000, "our older TS is kept"); assert_eq!(s.channels["#c"].created, 1000, "our older TS is kept");
} }

View file

@ -105,19 +105,19 @@ impl Command for ExtJwt {
let cmodes = member let cmodes = member
.map(|m| { .map(|m| {
let mut v = Vec::new(); let mut v = Vec::new();
if m.owner { if m.owner() {
v.push('q'); v.push('q');
} }
if m.admin { if m.admin() {
v.push('a'); v.push('a');
} }
if m.op { if m.op() {
v.push('o'); v.push('o');
} }
if m.halfop { if m.halfop() {
v.push('h'); v.push('h');
} }
if m.voice { if m.voice() {
v.push('v'); v.push('v');
} }
v v

View file

@ -17,8 +17,8 @@ fn set(s: &mut Server, uid: Uid, key: &str, on: bool) {
return; return;
}; };
let changed = match s.channels.get_mut(key).and_then(|c| c.members.get_mut(&uid)) { let changed = match s.channels.get_mut(key).and_then(|c| c.members.get_mut(&uid)) {
Some(m) if m.oprefix != on => { Some(m) if m.oprefix() != on => {
m.oprefix = on; m.set_oprefix(on);
true true
} }
_ => false, _ => false,

View file

@ -10,11 +10,11 @@ use crate::server::{now, Server};
fn prefixes(m: &crate::channels::Member) -> String { fn prefixes(m: &crate::channels::Member) -> String {
let mut p = String::new(); let mut p = String::new();
for (has, ch) in [ for (has, ch) in [
(m.owner, '~'), (m.owner(), '~'),
(m.admin, '&'), (m.admin(), '&'),
(m.op, '@'), (m.op(), '@'),
(m.halfop, '%'), (m.halfop(), '%'),
(m.voice, '+'), (m.voice(), '+'),
] { ] {
if has { if has {
p.push(ch); p.push(ch);

View file

@ -1384,11 +1384,11 @@ impl Server {
.map(|mem| { .map(|mem| {
let mut s = String::new(); let mut s = String::new();
for (on, c) in [ for (on, c) in [
(mem.owner, 'q'), (mem.owner(), 'q'),
(mem.admin, 'a'), (mem.admin(), 'a'),
(mem.op, 'o'), (mem.op(), 'o'),
(mem.halfop, 'h'), (mem.halfop(), 'h'),
(mem.voice, 'v'), (mem.voice(), 'v'),
] { ] {
if on { if on {
s.push(c); s.push(c);
@ -1579,8 +1579,8 @@ mod tests {
s.join(1, "#c", None); // ann creates -> gets @ s.join(1, "#c", None); // ann creates -> gets @
s.join(2, "#c", None); // bob joins s.join(2, "#c", None); // bob joins
assert!(s.channels["#c"].members[&1].op); assert!(s.channels["#c"].members[&1].op());
assert!(!s.channels["#c"].members[&2].op); assert!(!s.channels["#c"].members[&2].op());
assert_eq!(s.channels["#c"].members.len(), 2); assert_eq!(s.channels["#c"].members.len(), 2);
let ann: Vec<String> = arx.try_iter().collect(); let ann: Vec<String> = arx.try_iter().collect();