From 1aa02a08b91357453653459bf7f2c2ddcf2a8308 Mon Sep 17 00:00:00 2001 From: reverse Date: Wed, 19 Aug 2026 02:47:04 +0000 Subject: [PATCH] =?UTF-8?q?channels:=20replace=20Member's=20six=20parallel?= =?UTF-8?q?=20prefix=20bools=20(oprefix/owner/admin/op/halfop/voice)=20wit?= =?UTF-8?q?h=20a=20single=20u8=20bitfield=20(PFX=5F*)=20+=20inline=20bool?= =?UTF-8?q?=20accessors/mutators=20=E2=80=94=20same=20semantics,=20one=20b?= =?UTF-8?q?yte=20instead=20of=20six,=20no=20more=20risk=20of=20the=20flags?= =?UTF-8?q?=20drifting=20out=20of=20sync;=20all=20call=20sites=20go=20thro?= =?UTF-8?q?ugh=20op()/set=5Fop()-style=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/channels.rs | 161 ++++++++++++++++++++++++------------- src/link.rs | 6 +- src/modules/extjwt.rs | 10 +-- src/modules/operprefix.rs | 4 +- src/modules/rpc/channel.rs | 10 +-- src/server.rs | 14 ++-- 6 files changed, 129 insertions(+), 76 deletions(-) diff --git a/src/channels.rs b/src/channels.rs index df0b96d..8037f83 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -12,18 +12,24 @@ use crate::Uid; /// Per-member prefix modes (+q/+a/+o/+h/+v). Flag modes live in [`ChanModes`]. #[derive(Default)] pub struct Member { - pub oprefix: bool, // operprefix/ojoin: server oper prefix (!), highest rank - pub owner: bool, // +q (~) - pub admin: bool, // +a (&) - pub op: bool, // +o (@) - pub halfop: bool, // +h (%) - pub voice: bool, // +v (+) + /// Built-in prefix modes held, as a bitfield of `PFX_*` (was six parallel bools: + /// oprefix `!`, owner `~`, admin `&`, op `@`, halfop `%`, voice `+`). Read/write + /// through the `op()`/`set_op()`-style accessors below. + pub prefixes: u8, pub custom_prefixes: Vec, // config-defined prefix mode letters held (customprefix) pub joined: u64, // unix ts this member joined (for +d delaymsg; 0 = unknown) pub recent_msgs: Vec, // +K repeat: this member's last few lines here 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 /// config-defined custom prefixes (modules::customprefix) can slot in between. 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; 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). - /// 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 { - if self.oprefix { + if self.oprefix() { RANK_OPER - } else if self.owner { + } else if self.owner() { RANK_OWNER - } else if self.admin { + } else if self.admin() { RANK_ADMIN - } else if self.op { + } else if self.op() { RANK_OP - } else if self.halfop { + } else if self.halfop() { RANK_HALFOP - } else if self.voice { + } else if self.voice() { RANK_VOICE } else { 0 @@ -70,12 +127,12 @@ impl Member { use crate::modules::customprefix::{def_for_letter, sigil}; let mut v: Vec<(u8, &'static str)> = Vec::new(); for (on, r, i) in [ - (self.oprefix, RANK_OPER, 0), - (self.owner, RANK_OWNER, 1), - (self.admin, RANK_ADMIN, 2), - (self.op, RANK_OP, 3), - (self.halfop, RANK_HALFOP, 4), - (self.voice, RANK_VOICE, 5), + (self.oprefix(), RANK_OPER, 0), + (self.owner(), RANK_OWNER, 1), + (self.admin(), RANK_ADMIN, 2), + (self.op(), RANK_OP, 3), + (self.halfop(), RANK_HALFOP, 4), + (self.voice(), RANK_VOICE, 5), ] { if on { v.push((r, sigil(i))); @@ -96,17 +153,17 @@ impl Member { use crate::modules::customprefix::sigil; if self.custom_prefixes.is_empty() { // fast path: built-in tiers only - if self.oprefix { + if self.oprefix() { sigil(0) - } else if self.owner { + } else if self.owner() { sigil(1) - } else if self.admin { + } else if self.admin() { sigil(2) - } else if self.op { + } else if self.op() { sigil(3) - } else if self.halfop { + } else if self.halfop() { sigil(4) - } else if self.voice { + } else if self.voice() { sigil(5) } else { "" @@ -121,21 +178,17 @@ impl Member { /// 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. pub fn clear_status(&mut self) { - self.owner = false; - self.admin = false; - self.op = false; - self.halfop = false; - self.voice = false; + self.prefixes &= !(PFX_OWNER | PFX_ADMIN | PFX_OP | PFX_HALFOP | PFX_VOICE); } pub fn set_prefix(&mut self, letter: char, on: bool) { match letter { - 'y' => self.oprefix = on, - 'q' => self.owner = on, - 'a' => self.admin = on, - 'o' => self.op = on, - 'h' => self.halfop = on, - 'v' => self.voice = on, + 'y' => self.set_bit(PFX_OPER, on), + 'q' => self.set_bit(PFX_OWNER, on), + 'a' => self.set_bit(PFX_ADMIN, on), + 'o' => self.set_bit(PFX_OP, on), + 'h' => self.set_bit(PFX_HALFOP, on), + 'v' => self.set_bit(PFX_VOICE, on), _ => { if crate::modules::customprefix::def_for_letter(letter).is_some() { self.custom_prefixes.retain(|&c| c != letter); @@ -153,12 +206,12 @@ impl Member { if self.custom_prefixes.is_empty() { let mut s = String::new(); for (on, i) in [ - (self.oprefix, 0), - (self.owner, 1), - (self.admin, 2), - (self.op, 3), - (self.halfop, 4), - (self.voice, 5), + (self.oprefix(), 0), + (self.owner(), 1), + (self.admin(), 2), + (self.op(), 3), + (self.halfop(), 4), + (self.voice(), 5), ] { if on { s.push_str(sigil(i)); @@ -177,12 +230,12 @@ impl Member { use crate::modules::customprefix::def_for_letter; let mut s = String::new(); for (on, l) in [ - (self.oprefix, 'y'), - (self.owner, 'q'), - (self.admin, 'a'), - (self.op, 'o'), - (self.halfop, 'h'), - (self.voice, 'v'), + (self.oprefix(), 'y'), + (self.owner(), 'q'), + (self.admin(), 'a'), + (self.op(), 'o'), + (self.halfop(), 'h'), + (self.voice(), 'v'), ] { if on { s.push(l); @@ -898,7 +951,7 @@ impl Server { ch.members.insert( uid, Member { - op: is_new, + prefixes: if is_new { PFX_OP } else { 0 }, joined: now(), ..Default::default() }, @@ -1607,15 +1660,15 @@ mod tests { let mut m = Member::default(); assert_eq!(m.rank(), 0); assert_eq!(m.prefix_char(), ""); - m.voice = true; + m.set_voice(true); 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, "%")); - m.op = true; + m.set_op(true); 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, "&")); - m.owner = true; + m.set_owner(true); assert_eq!((m.rank(), m.prefix_char()), (RANK_OWNER, "~")); } } diff --git a/src/link.rs b/src/link.rs index 66570bd..315487f 100644 --- a/src/link.rs +++ b/src/link.rs @@ -2526,8 +2526,8 @@ mod tests { let msg = crate::message::parse(":42SB00000 IJOIN #echoircd 16 1 ao").unwrap(); s.link_ijoin_recv(1, &msg); let m = &s.channels["#echoircd"].rmembers["42SB00000"]; - 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.admin(), "bot should hold +a (&) 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 @@ -2565,7 +2565,7 @@ mod tests { // 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(); 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_eq!(s.channels["#c"].created, 1000, "our older TS is kept"); } diff --git a/src/modules/extjwt.rs b/src/modules/extjwt.rs index 10f2e69..ed85b30 100644 --- a/src/modules/extjwt.rs +++ b/src/modules/extjwt.rs @@ -105,19 +105,19 @@ impl Command for ExtJwt { let cmodes = member .map(|m| { let mut v = Vec::new(); - if m.owner { + if m.owner() { v.push('q'); } - if m.admin { + if m.admin() { v.push('a'); } - if m.op { + if m.op() { v.push('o'); } - if m.halfop { + if m.halfop() { v.push('h'); } - if m.voice { + if m.voice() { v.push('v'); } v diff --git a/src/modules/operprefix.rs b/src/modules/operprefix.rs index 5b9fb4e..6fece8e 100644 --- a/src/modules/operprefix.rs +++ b/src/modules/operprefix.rs @@ -17,8 +17,8 @@ fn set(s: &mut Server, uid: Uid, key: &str, on: bool) { return; }; let changed = match s.channels.get_mut(key).and_then(|c| c.members.get_mut(&uid)) { - Some(m) if m.oprefix != on => { - m.oprefix = on; + Some(m) if m.oprefix() != on => { + m.set_oprefix(on); true } _ => false, diff --git a/src/modules/rpc/channel.rs b/src/modules/rpc/channel.rs index ce1b14c..40282ad 100644 --- a/src/modules/rpc/channel.rs +++ b/src/modules/rpc/channel.rs @@ -10,11 +10,11 @@ use crate::server::{now, Server}; fn prefixes(m: &crate::channels::Member) -> String { let mut p = String::new(); for (has, ch) in [ - (m.owner, '~'), - (m.admin, '&'), - (m.op, '@'), - (m.halfop, '%'), - (m.voice, '+'), + (m.owner(), '~'), + (m.admin(), '&'), + (m.op(), '@'), + (m.halfop(), '%'), + (m.voice(), '+'), ] { if has { p.push(ch); diff --git a/src/server.rs b/src/server.rs index e501895..fb3f39d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1384,11 +1384,11 @@ impl Server { .map(|mem| { let mut s = String::new(); for (on, c) in [ - (mem.owner, 'q'), - (mem.admin, 'a'), - (mem.op, 'o'), - (mem.halfop, 'h'), - (mem.voice, 'v'), + (mem.owner(), 'q'), + (mem.admin(), 'a'), + (mem.op(), 'o'), + (mem.halfop(), 'h'), + (mem.voice(), 'v'), ] { if on { s.push(c); @@ -1579,8 +1579,8 @@ mod tests { s.join(1, "#c", None); // ann creates -> gets @ s.join(2, "#c", None); // bob joins - assert!(s.channels["#c"].members[&1].op); - assert!(!s.channels["#c"].members[&2].op); + assert!(s.channels["#c"].members[&1].op()); + assert!(!s.channels["#c"].members[&2].op()); assert_eq!(s.channels["#c"].members.len(), 2); let ann: Vec = arx.try_iter().collect();