From 2222cea7fb5593843e693d6073a4f9e921636f46 Mon Sep 17 00:00:00 2001 From: brent saner Date: Wed, 27 Aug 2025 19:06:17 -0400 Subject: [PATCH] v1.9.6 FIXED: * More clear docs for bitmask * Resolved potential issue for using PriorityAll in logging.logPrio.HasFlag. --- bitmask/bitmask.go | 12 +++- bitmask/doc.go | 119 +++++++++++++++++++++++++++++++++++-- logging/TODO | 2 + logging/consts_nix.go | 4 +- logging/consts_windows.go | 8 +-- logging/funcs_logprio.go | 4 +- logging/funcs_logwriter.go | 2 + 7 files changed, 136 insertions(+), 15 deletions(-) diff --git a/bitmask/bitmask.go b/bitmask/bitmask.go index 95fcf3d..80bfdc8 100644 --- a/bitmask/bitmask.go +++ b/bitmask/bitmask.go @@ -36,7 +36,9 @@ func NewMaskBitExplicit(value uint) (m *MaskBit) { /* HasFlag is true if m has MaskBit flag set/enabled. + THIS WILL RETURN FALSE FOR OR'd FLAGS. + For example: flagA MaskBit = 0x01 @@ -44,12 +46,15 @@ func NewMaskBitExplicit(value uint) (m *MaskBit) { flagComposite = flagA | flagB m *MaskBit = NewMaskBitExplicit(uint(flagA)) - + m.HasFlag(flagComposite) will return false even though flagComposite is an OR that contains flagA. Use [MaskBit.IsOneOf] instead if you do not desire this behavior, and instead want to test composite flag *membership*. (MaskBit.IsOneOf will also return true for non-composite equality.) + + To be more clear, if MaskBit flag is a composite MaskBit (e.g. flagComposite above), + HasFlag will only return true of ALL bits in flag are also set in MaskBit m. */ func (m *MaskBit) HasFlag(flag MaskBit) (r bool) { @@ -70,12 +75,15 @@ func (m *MaskBit) HasFlag(flag MaskBit) (r bool) { If composite is *not* an OR'd MaskBit (i.e. it falls directly on a boundary -- 0, 1, 2, 4, 8, 16, etc.), then IsOneOf will behave exactly like HasFlag. + + If m is a composite MaskBit (it usually is) and composite is ALSO a composite MaskBit, + IsOneOf will return true if ANY of the flags set in m is set in composite. */ func (m *MaskBit) IsOneOf(composite MaskBit) (r bool) { var b MaskBit = *m - if b&flag != 0 { + if b&composite != 0 { r = true } return diff --git a/bitmask/doc.go b/bitmask/doc.go index d550aa0..8f834cf 100644 --- a/bitmask/doc.go +++ b/bitmask/doc.go @@ -1,9 +1,35 @@ /* Package bitmask handles a flag-like opt/bitmask system. -See https://yourbasic.org/golang/bitmask-flag-set-clear/ for more information. +See https://yourbasic.org/golang/bitmask-flag-set-clear/ for basic information on what bitmasks are and why they're useful. -To use this, set constants like thus: +Specifically, in the case of Go, they allow you to essentially manage many, many, many "booleans" as part of a single value. + +A single bool value in Go takes up 8 bits/1 byte, unavoidably. + +However, a [bitmask.MaskBit] is backed by a uint which (depending on your platform) is either 32 bits/4 bytes or 64 bits/8 bytes. + +"But wait, that takes up more memory though!" + +Yep, but bitmasking lets you store a "boolean" AT EACH BIT - it operates on +whether a bit in a byte/set of bytes at a given position is 0 or 1. + +Which means on 32-bit platforms, a [MaskBit] can have up to 4294967295 "booleans" in a single value (0 to (2^32)-1). + +On 64-bit platforms, a [MaskBit] can have up to 18446744073709551615 "booleans" in a single value (0 to (2^64)-1). + +If you tried to do that with Go bool values, that'd take up 4294967295 bytes (4 GiB) +or 18446744073709551615 bytes (16 EiB - yes, that's [exbibytes]) of RAM for 32-bit/64-bit platforms respectively. + +"But that has to be so slow to unpack that!" + +Nope. It's not using compression or anything, the CPU is just comparing bit "A" vs. bit "B" 32/64 times. That's super easy work for a CPU. + +There's a reason Doom used bitmasking for the "dmflags" value in its server configs. + +# Usage + +To use this library, set constants like thus: package main @@ -42,12 +68,95 @@ But this would return false: MyMask.HasFlag(OPT2) +# Technical Caveats + +TARGETING + +When implementing, you should always set MyMask (from Usage section above) as the actual value. +For example, if you are checking a permissions set for a user that has the value, say, 6 + + var userPerms uint = 6 // 0x0000000000000006 + +and your library has the following permission bits defined: + + const PermsNone bitmask.MaskBit = 0 + const ( + PermsList bitmask.MaskBit = 1 << iota // 1 + PermsRead // 2 + PermsWrite // 4 + PermsExec // 8 + PermsAdmin // 16 + ) + +And you want to see if the user has the PermsRead flag set, you would do: + + userPermMask = bitmask.NewMaskBitExplicit(userPerms) + if userPermMask.HasFlag(PermsRead) { + // ... + } + +NOT: + + userPermMask = bitmask.NewMaskBitExplicit(PermsRead) + // Nor: + // userPermMask = PermsRead + if userPermMask.HasFlag(userPerms) { + // ... + } + +This will be terribly, horribly wrong, cause incredibly unexpected results, +and quite possibly cause massive security issues. Don't do it. + +COMPOSITES + +If you want to define a set of flags that are a combination of other flags, +your inclination would be to bitwise-OR them together: + + const ( + flagA bitmask.MaskBit = 1 << iota // 1 + flagB // 2 + ) + + const ( + flagAB bitmask.MaskBit = flagA | flagB // 3 + ) + +Which is fine and dandy. But if you then have: + + var myMask *bitmask.MaskBit = bitmask.NewMaskBit() + + myMask.AddFlag(flagA) + +You may expect this call to [MaskBit.HasFlag]: + + myMask.HasFlag(flagAB) + +to be true, since flagA is "in" flagAB. +It will return false - HasFlag does strict comparisons. +It will only return true if you then ALSO do: + + // This would require setting flagA first. + // The order of setting flagA/flagB doesn't matter, + // but you must have both set for HasFlag(flagAB) to return true. + myMask.AddFlag(flagB) + +or if you do: + + // This can be done with or without additionally setting flagA. + myMask.AddFlag(flagAB) + +Instead, if you want to see if a mask has membership within a composite flag, +you can use [MaskBit.IsOneOf]. + +# Other Options + If you need something with more flexibility (as always, at the cost of complexity), you may be interested in one of the following libraries: -. github.com/alvaroloes/enumer -. github.com/abice/go-enum -. github.com/jeffreyrichter/enum/enum + * [github.com/alvaroloes/enumer] + * [github.com/abice/go-enum] + * [github.com/jeffreyrichter/enum/enum] +[exbibytes]: https://simple.wikipedia.org/wiki/Exbibyte */ package bitmask diff --git a/logging/TODO b/logging/TODO index abc06b1..1af075a 100644 --- a/logging/TODO +++ b/logging/TODO @@ -4,6 +4,8 @@ -- no native Go support (yet)? --- https://developer.apple.com/forums/thread/773369 +- The log destinations for e.g. consts_nix.go et. al. probably should be unexported types. + - add a `log/slog` logging.Logger? - Implement code line/func/etc. (only for debug?): diff --git a/logging/consts_nix.go b/logging/consts_nix.go index 485c445..3f06036 100644 --- a/logging/consts_nix.go +++ b/logging/consts_nix.go @@ -23,8 +23,8 @@ const ( // LogUndefined indicates an undefined Logger type. const LogUndefined bitmask.MaskBit = iota const ( - // LogJournald flags a SystemDLogger Logger type. - LogJournald = 1 << iota + // LogJournald flags a SystemDLogger Logger type. This will, for hopefully obvious reasons, only work on Linux systemd systems. + LogJournald bitmask.MaskBit = 1 << iota // LogSyslog flags a SyslogLogger Logger type. LogSyslog // LogFile flags a FileLogger Logger type. diff --git a/logging/consts_windows.go b/logging/consts_windows.go index d96b72d..6de88eb 100644 --- a/logging/consts_windows.go +++ b/logging/consts_windows.go @@ -3,16 +3,14 @@ package logging import ( `os` `path/filepath` - - `r00t2.io/goutils/bitmask` ) // Flags for logger configuration. These are used internally. +// LogUndefined indicates an undefined Logger type. +LogUndefined bitmask.MaskBit = 0 const ( - // LogUndefined indicates an undefined Logger type. - LogUndefined bitmask.MaskBit = 1 << iota // LogWinLogger indicates a WinLogger Logger type (Event Log). - LogWinLogger + LogWinLogger bitmask.MaskBit= 1 << iota // LogFile flags a FileLogger Logger type. LogFile // LogStdout flags a StdLogger Logger type. diff --git a/logging/funcs_logprio.go b/logging/funcs_logprio.go index b3d32ab..99ef4f0 100644 --- a/logging/funcs_logprio.go +++ b/logging/funcs_logprio.go @@ -17,7 +17,9 @@ func (l *logPrio) HasFlag(prio logPrio) (hasFlag bool) { m = bitmask.NewMaskBitExplicit(uint(*l)) p = bitmask.NewMaskBitExplicit(uint(prio)) - hasFlag = m.HasFlag(*p) + // Use IsOneOf instead in case PriorityAll is passed for prio. + // hasFlag = m.HasFlag(*p) + hasFlag = m.IsOneOf(*p) return } diff --git a/logging/funcs_logwriter.go b/logging/funcs_logwriter.go index 862266f..dafcd77 100644 --- a/logging/funcs_logwriter.go +++ b/logging/funcs_logwriter.go @@ -40,6 +40,8 @@ func (l *logWriter) Write(b []byte) (n int, err error) { s = string(b) + // Since this explicitly checks each priority level, there's no need for IsOneOf in case of PriorityAll. + if l.prio.HasFlag(PriorityEmergency) { if err = l.backend.Emerg(s); err != nil { mErr.AddError(err)