4 Commits

Author SHA1 Message Date
brent saner
6ddfcdb416 v1.13.0
ADDED:
* stringsx functions
2025-11-30 16:53:56 -05:00
brent saner
79f10b7611 v1.12.1
FIXED:
* Aaaannnddd need to make the Windows multilogger AddDefaultLogger
  use the right/matching parameters as well.
2025-11-22 17:19:41 -05:00
brent saner
01adbfc605 v1.12.0
FIXED:
* logging package on Windows had a non-conformant GetLogger().
  While this fix technically breaks API, this was a horribly broken
  thing so I'm including it as a minor bump instead of major and
  thus breaking SemVer. Too bad, so sad, deal with it; Go modules
  have versioning for a reason.
  The previous logging.GetLogger() behavior on Windows has been moved
  to logging.GetLoggerWindows().
2025-11-22 15:53:38 -05:00
brent saner
b1d8ea34a6 v1.11.0
ADDED:
* `stringsx` package
** `stringsx.Indent()`, to indent/prefix multiline strings
** `stringsx.Redact()`, to mask strings
** `stringsx.TrimLines()`, like strings.TrimSpace() but multiline
** `stringsx.TrimSpaceLeft()`, like strings.TrimSpace() but only to the
    left of a string.
** `stringsx.TrimSpaceRight()`, like strings.TrimSpace() but only to the
    right of a string.
2025-11-14 01:02:59 -05:00
12 changed files with 862 additions and 20 deletions

1
go.mod
View File

@@ -5,6 +5,7 @@ go 1.24.5
require (
github.com/coreos/go-systemd/v22 v22.5.0
github.com/google/uuid v1.6.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/sys v0.34.0
r00t2.io/sysutils v1.14.0
)

3
go.sum
View File

@@ -5,9 +5,12 @@ github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYC
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
r00t2.io/sysutils v1.14.0 h1:Lrio3uPi9CuUdg+sg3WkVV1CK/qcOpV9GdFCGFG1KJs=
r00t2.io/sysutils v1.14.0/go.mod h1:ZJ7gZxFVQ7QIokQ5fPZr7wl0XO5Iu+LqtE8j3ciRINw=

View File

@@ -1,3 +1,5 @@
- logging probably needs mutexes
- macOS support beyond the legacy NIX stuff. it apparently uses something called "ULS", "Unified Logging System".
-- https://developer.apple.com/documentation/os/logging
-- https://developer.apple.com/documentation/os/generating-log-messages-from-your-code

View File

@@ -21,7 +21,7 @@ import (
Only the first logPaths entry that "works" will be used, later entries will be ignored.
Currently this will almost always return a WinLogger.
*/
func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID, logFlags int, logPaths ...string) (err error) {
func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths ...string) (err error) {
var l Logger
var exists bool
@@ -36,9 +36,9 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID,
}
if logPaths != nil {
l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logFlags, logPaths...)
l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags, logPaths...)
} else {
l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logFlags)
l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags)
}
if err != nil {
return

View File

@@ -10,32 +10,63 @@ import (
)
/*
GetLogger returns an instance of Logger that best suits your system's capabilities. Note that this is a VERY generalized interface to the Windows Event Log.
GetLogger returns an instance of Logger that best suits your system's capabilities.
Note that this is a VERY generalized interface to the Windows Event Log to conform with multiplatform compat.
You'd have a little more flexibility with [GetLoggerWindows] (this function wraps that one).
If you need more custom behavior than that, I recommend using [golang.org/x/sys/windows/svc/eventlog] directly
(or using another logging module).
If `enableDebug` is true, debug messages (which according to your program may or may not contain sensitive data) are rendered and written (otherwise they are ignored).
The `prefix` correlates to the `source` parameter in [GetLoggerWindows], and this function inherently uses [DefaultEventID],
but otherwise it remains the same as [GetLoggerWindows] - refer to it for documentation on the other parameters.
If you call [GetLogger], you will only get a single ("best") logger your system supports.
If you want to log to multiple [Logger] destinations at once (or want to log to an explicit [Logger] type),
use [GetMultiLogger].
*/
func GetLogger(enableDebug bool, prefix string, logConfigFlags int, logPaths ...string) (logger Logger, err error) {
if logger, err = GetLoggerWindows(enableDebug, prefix, DefaultEventID, logConfigFlags, logPaths...); err != nil {
return
}
return
}
/*
GetLoggerWindows returns an instance of Logger that best suits your system's capabilities.
This is a slightly less (but still quite) generalized interface to the Windows Event Log than [GetLogger].
If you require more robust logging capabilities (e.g. custom event IDs per uniquely identifiable event),
you will want to set up your own logger (golang.org/x/sys/windows/svc/eventlog).
you will want to set up your own logger via [golang.org/x/sys/windows/svc/eventlog].
If enableDebug is true, debug messages (which according to your program may or may not contain sensitive data) are rendered and written (otherwise they are ignored).
If `enableDebug` is true, debug messages (which according to your program may or may not contain sensitive data)
are rendered and written (otherwise they are ignored).
A blank source will return an error as it's used as the source name. Other functions, struct fields, etc. will refer to this as the "prefix".
A blank `source` will return an error as it's used as the source name.
Throughout the rest of this documentation you will see this referred to as the `prefix` to remain platform-agnostic.
A pointer to a WinEventID struct may be specified for eventIDs to map extended logging levels (as Windows only supports three levels natively).
A pointer to a [WinEventID] struct may be specified for `eventIDs` to map extended logging levels
(as Windows only supports three levels natively).
If it is nil, a default one (DefaultEventID) will be used.
logConfigFlags is the corresponding flag(s) OR'd for StdLogger.LogFlags / FileLogger.StdLogger.LogFlags if either is selected. See StdLogger.LogFlags and
https://pkg.go.dev/log#pkg-constants for details.
`logConfigFlags` is the corresponding flag(s) OR'd for [StdLogger.LogFlags] (and/or the [StdLogger.LogFlags] for [FileLogger])
if either is selected. See [StdLogger.LogFlags] and [stdlib log's constants] for details.
logPaths is an (optional) list of strings to use as paths to test for writing. If the file can be created/written to,
it will be used (assuming you have no higher-level loggers available).
`logPaths` is an (optional) list of strings to use as paths to test for writing.
If the file can be created/written to, it will be used (assuming you have no higher-level loggers available).
Only the first logPaths entry that "works" will be used, later entries will be ignored.
Currently this will almost always return a WinLogger.
Only the first `logPaths` entry that "works" will be used, later entries will be ignored.
Currently this will almost always return a [WinLogger].
If you call GetLogger, you will only get a single ("best") logger your system supports.
If you want to log to multiple Logger destinations at once (or want to log to an explicit Logger type),
use GetMultiLogger.
If you call [GetLoggerWindows], you will only get a single ("best") logger your system supports.
If you want to log to multiple [Logger] destinations at once (or want to log to an explicit [Logger] type),
use [GetMultiLogger].
[stdlib log's constants]: https://pkg.go.dev/log#pkg-constants
*/
func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, logConfigFlags int, logPaths ...string) (logger Logger, err error) {
func GetLoggerWindows(enableDebug bool, source string, eventIDs *WinEventID, logConfigFlags int, logPaths ...string) (logger Logger, err error) {
var logPath string
var logFlags bitmask.MaskBit

View File

@@ -124,7 +124,7 @@ func TestDefaultLogger(t *testing.T) {
t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error())
}
if l, err = GetLogger(true, TestLogPrefix, DefaultEventID, logFlags, tempfilePath); err != nil {
if l, err = GetLoggerWindows(true, TestLogPrefix, DefaultEventID, logFlags, tempfilePath); err != nil {
t.Fatalf("error when spawning default Windows logger via GetLogger: %v", err.Error())
}

View File

@@ -35,7 +35,7 @@ func TestMultiLogger(t *testing.T) {
t.Fatalf("error when adding FileLogger to MultiLogger: %v", err.Error())
}
if err = l.AddDefaultLogger("DefaultLogger", DefaultEventID, logFlags, tempfilePath); err != nil {
if err = l.AddDefaultLogger("DefaultLogger", logFlags, tempfilePath); err != nil {
t.Fatalf("error when adding default logger to MultiLogger: %v", err.Error())
}

5
stringsx/TODO Normal file
View File

@@ -0,0 +1,5 @@
- Banner struct, with .Format(s string) method
-- draw border around multiline s
-- i have a version in python somewhere that does this, should dig that up
- create bytesx package that duplicates the functions here?

6
stringsx/consts.go Normal file
View File

@@ -0,0 +1,6 @@
package stringsx
const (
// DefMaskStr is the string used as the default maskStr if left empty in [Redact].
DefMaskStr string = "***"
)

17
stringsx/doc.go Normal file
View File

@@ -0,0 +1,17 @@
/*
Package stringsx aims to extend functionality of the stdlib [strings] module.
Note that if you need a way of mimicking Bash's shell quoting rules, [desertbit/shlex] or [buildkite/shellwords]
would be better options than [google/shlex] but this package does not attempt to reproduce
any of that functionality.
For line splitting, one should use [muesli/reflow/wordwrap].
Likewise for indentation, one should use [muesli/reflow/indent].
[desertbit/shlex]: https://pkg.go.dev/github.com/desertbit/go-shlex
[buildkite/shellwords]: https://pkg.go.dev/github.com/buildkite/shellwords
[google/shlex]: https://pkg.go.dev/github.com/google/shlex
[muesli/reflow/wordwrap]: https://pkg.go.dev/github.com/muesli/reflow/wordwrap
[muesli/reflow/indent]: https://pkg.go.dev/github.com/muesli/reflow/indent
*/
package stringsx

326
stringsx/funcs.go Normal file
View File

@@ -0,0 +1,326 @@
package stringsx
import (
`fmt`
`strings`
`unicode`
)
/*
LenSplit formats string `s` to break at, at most, every `width` characters.
Any existing newlines (e.g. \r\n) will be removed during a string/
substring/line's length calculation. (e.g. `foobarbaz\n` and `foobarbaz\r\n` are
both considered to be lines of length 9, not 10 and 11 respectively).
This also means that any newlines (\n or \r\n) are inherently removed from
`out` (even if included in `wordWrap`; see below).
Note that if `s` is multiline (already contains newlines), they will be respected
as-is - that is, if a line ends with less than `width` chars and then has a newline,
it will be preserved as an empty element. That is to say:
"foo\nbar\n\n" → []string{"foo", "bar", ""}
"foo\n\nbar\n" → []string{"foo", "", "bar"}
This splitter is particularly simple. If you need wordwrapping, it should be done
with e.g. [github.com/muesli/reflow/wordwrap].
*/
func LenSplit(s string, width uint) (out []string) {
var end int
var line string
var lineRunes []rune
if width == 0 {
out = []string{s}
return
}
for line = range strings.Lines(s) {
line = strings.TrimRight(line, "\n")
line = strings.TrimRight(line, "\r")
lineRunes = []rune(line)
if uint(len(lineRunes)) <= width {
out = append(out, line)
continue
}
for i := 0; i < len(lineRunes); i += int(width) {
end = i + int(width)
if end > len(lineRunes) {
end = len(lineRunes)
}
out = append(out, string(lineRunes[i:end]))
}
}
return
}
/*
LenSplitStr wraps [LenSplit] but recombines into a new string with newlines.
It's mostly just a convenience wrapper.
All arguments remain the same as in [LenSplit] with an additional one,
`winNewLine`, which if true will use \r\n as the newline instead of \n.
*/
func LenSplitStr(s string, width uint, winNewline bool) (out string) {
var outSl []string = LenSplit(s, width)
if winNewline {
out = strings.Join(outSl, "\r\n")
} else {
out = strings.Join(outSl, "\n")
}
return
}
/*
Pad pads each element in `s` to length `width` using `pad`.
If `pad` is empty, a single space (0x20) will be assumed.
Note that `width` operates on rune size, not byte size.
(In ASCII, they will be the same size.)
If a line in `s` is greater than or equal to `width`,
no padding will be performed.
If `leftPad` is true, padding will be applied to the "left" (beginning")
of each element instead of the "right" ("end").
*/
func Pad(s []string, width uint, pad string, leftPad bool) (out []string) {
var idx int
var padIdx int
var runeIdx int
var padLen uint
var elem string
var unpadLen uint
var tmpPadLen int
var padRunes []rune
var tmpPad []rune
if width == 0 {
out = s
return
}
out = make([]string, len(s))
// Easy; supported directly in fmt.
if pad == "" {
for idx, elem = range s {
if leftPad {
out[idx] = fmt.Sprintf("%*s", width, elem)
} else {
out[idx] = fmt.Sprintf("%-*s", width, elem)
}
}
return
}
// This gets a little more tricky.
padRunes = []rune(pad)
padLen = uint(len(padRunes))
for idx, elem = range s {
// First we need to know the number of runes in elem.
unpadLen = uint(len([]rune(elem)))
// If it's more than/equal to width, as-is.
if unpadLen >= width {
out[idx] = elem
} else {
// Otherwise, we need to construct/calculate a pad.
if (width-unpadLen)%padLen == 0 {
// Also easy enough.
if leftPad {
out[idx] = fmt.Sprintf("%s%s", strings.Repeat(pad, int((width-unpadLen)/padLen)), elem)
} else {
out[idx] = fmt.Sprintf("%s%s", elem, strings.Repeat(pad, int((width-unpadLen)/padLen)))
}
} else {
// This is where it gets a little hairy.
tmpPad = []rune{}
tmpPadLen = int(width - unpadLen)
idx = 0
padIdx = 0
for runeIdx = range tmpPadLen {
tmpPad[runeIdx] = padRunes[padIdx]
if uint(padIdx) >= padLen {
padIdx = 0
} else {
padIdx++
}
runeIdx++
}
if leftPad {
out[idx] = fmt.Sprintf("%s%s", string(tmpPad), elem)
} else {
out[idx] = fmt.Sprintf("%s%s", elem, string(tmpPad))
}
}
}
}
return
}
/*
Redact provides a "masked" version of string s (e.g. `my_terrible_password` -> `my****************rd`).
maskStr is the character or sequence of characters
to repeat for every masked character of s.
If an empty string, the default [DefMaskStr] will be used.
(maskStr does not need to be a single character.
It is recommended to use a multi-char mask to help obfuscate a string's length.)
leading specifies the number of leading characters of s to leave *unmasked*.
If 0, no leading characters will be unmasked.
trailing specifies the number of trailing characters of s to leave *unmasked*.
if 0, no trailing characters will be unmasked.
newlines, if true, will preserve newline characters - otherwise
they will be treated as regular characters.
As a safety precaution, if:
len(s) <= (leading + trailing)
then the entire string will be *masked* and no unmasking will be performed.
Note that this DOES NOT do a string *replace*, it provides a masked version of `s` itself.
Wrap Redact with [strings.ReplaceAll] if you want to replace a certain value with a masked one.
*/
func Redact(s, maskStr string, leading, trailing uint, newlines bool) (redacted string) {
var nl string
var numMasked int
var sb strings.Builder
var endIdx int = int(leading)
// This condition functionally won't do anything, so just return the input as-is.
if s == "" {
return
}
if maskStr == "" {
maskStr = DefMaskStr
}
if newlines {
for line := range strings.Lines(s) {
nl = getNewLine(line)
sb.WriteString(
Redact(
strings.TrimSuffix(line, nl), maskStr, leading, trailing, false,
),
)
sb.WriteString(nl)
}
} else {
if len(s) <= int(leading+trailing) {
redacted = strings.Repeat(maskStr, len(s))
return
}
if leading == 0 && trailing == 0 {
redacted = strings.Repeat(maskStr, len(s))
return
}
numMasked = len(s) - int(leading+trailing)
endIdx = endIdx + numMasked
if leading > 0 {
sb.WriteString(s[:int(leading)])
}
sb.WriteString(strings.Repeat(maskStr, numMasked))
if trailing > 0 {
sb.WriteString(s[endIdx:])
}
}
redacted = sb.String()
return
}
/*
TrimLines is like [strings.TrimSpace] but operates on *each line* of s.
It is *NIX-newline (`\n`) vs. Windows-newline (`\r\n`) agnostic.
The first encountered linebreak (`\n` vs. `\r\n`) are assumed to be
the canonical linebreak for the rest of s.
left, if true, performs a [TrimSpaceLeft] on each line (retaining the newline).
right, if true, performs a [TrimSpaceRight] on each line (retaining the newline).
*/
func TrimLines(s string, left, right bool) (trimmed string) {
var sl string
var nl string
var sb strings.Builder
// These conditions functionally won't do anything, so just return the input as-is.
if s == "" {
return
}
if !left && !right {
trimmed = s
return
}
for line := range strings.Lines(s) {
nl = getNewLine(line)
sl = strings.TrimSuffix(line, nl)
if left && right {
sl = strings.TrimSpace(sl)
} else if left {
sl = TrimSpaceLeft(sl)
} else if right {
sl = TrimSpaceRight(sl)
}
sb.WriteString(sl + nl)
}
trimmed = sb.String()
return
}
// TrimSpaceLeft is like [strings.TrimSpace] but only removes leading whitespace from string `s`.
func TrimSpaceLeft(s string) (trimmed string) {
trimmed = strings.TrimLeftFunc(s, unicode.IsSpace)
return
}
/*
TrimSpaceRight is like [strings.TrimSpace] but only removes trailing whitespace from string s.
*/
func TrimSpaceRight(s string) (trimmed string) {
trimmed = strings.TrimRightFunc(s, unicode.IsSpace)
return
}
// getNewLine is too unpredictable/nuanced to be used as part of a public API promise so it isn't exported.
func getNewLine(s string) (nl string) {
if strings.HasSuffix(s, "\r\n") {
nl = "\r\n"
} else if strings.HasSuffix(s, "\n") {
nl = "\n"
}
return
}

451
stringsx/funcs_test.go Normal file
View File

@@ -0,0 +1,451 @@
package stringsx
import (
"testing"
)
type (
testIndentSet struct {
name string
orig string
indent string
lvl uint
ws bool
empty bool
tgt string
}
testRedactSet struct {
name string
orig string
leading uint
trailing uint
tgt string
newline bool
mask string // defaults to DefMaskStr.
}
testTrimLinesSet struct {
name string
orig string
left bool
right bool
tgt string
}
testTrimSet struct {
name string
orig string
tgt string
}
)
func TestIndent(t *testing.T) {
var out string
var tests []testIndentSet = []testIndentSet{
testIndentSet{
name: "standard, no trailing newline",
orig: "foo\nbar\nbaz",
indent: "",
lvl: 1,
ws: false,
empty: false,
tgt: "\tfoo\n\tbar\n\tbaz",
},
testIndentSet{
name: "standard, trailing newline",
orig: "foo\nbar\nbaz\n",
indent: "",
lvl: 1,
ws: false,
empty: false,
tgt: "\tfoo\n\tbar\n\tbaz\n",
},
testIndentSet{
name: "standard, trailing newline with empty",
orig: "foo\nbar\nbaz\n",
indent: "",
lvl: 1,
ws: false,
empty: true,
tgt: "\tfoo\n\tbar\n\tbaz\n\t",
},
testIndentSet{
name: "standard, trailing newline with ws",
orig: "foo\nbar\nbaz\n",
indent: "",
lvl: 1,
ws: true,
empty: false,
tgt: "\tfoo\n\tbar\n\tbaz\n",
},
testIndentSet{
name: "standard, trailing newline with ws and empty",
orig: "foo\nbar\nbaz\n",
indent: "",
lvl: 1,
ws: true,
empty: true,
tgt: "\tfoo\n\tbar\n\tbaz\n\t",
},
testIndentSet{
name: "standard, trailing ws newline with empty",
orig: "foo\nbar\nbaz\n ",
indent: "",
lvl: 1,
ws: false,
empty: true,
tgt: "\tfoo\n\tbar\n\tbaz\n ",
},
testIndentSet{
name: "standard, trailing ws newline with ws",
orig: "foo\nbar\nbaz\n ",
indent: "",
lvl: 1,
ws: true,
empty: false,
tgt: "\tfoo\n\tbar\n\tbaz\n\t ",
},
testIndentSet{
name: "standard, trailing ws newline with ws and empty",
orig: "foo\nbar\nbaz\n \n",
indent: "",
lvl: 1,
ws: true,
empty: true,
tgt: "\tfoo\n\tbar\n\tbaz\n\t \n\t",
},
testIndentSet{
name: "comment",
orig: "foo\nbar\nbaz",
indent: "# ",
lvl: 1,
ws: false,
empty: false,
tgt: "# foo\n# bar\n# baz",
},
}
for idx, ts := range tests {
out = Indent(ts.orig, ts.indent, ts.lvl, ts.ws, ts.empty)
if out == ts.tgt {
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
} else {
t.Errorf(
"[%d] FAIL (%s): %#v (len %d):\n"+
"\t\t\texpected (len %d): %#v\n"+
"\t\t\tgot (len %d): %#v\n"+
"\t\t%#v",
idx, ts.name, ts.orig, len(ts.orig),
len(ts.tgt), ts.tgt,
len(out), out,
ts,
)
}
}
}
func TestRedact(t *testing.T) {
var out string
var tests []testRedactSet = []testRedactSet{
testRedactSet{
name: "empty in, empty out",
orig: "",
leading: 0,
trailing: 0,
tgt: "",
},
testRedactSet{
name: "standard",
orig: "password",
leading: 0,
trailing: 0,
tgt: "************************",
},
testRedactSet{
name: "standard with newline",
orig: "pass\nword",
leading: 0,
trailing: 0,
tgt: "************\n************",
newline: true,
},
testRedactSet{
name: "standard with Windows newline",
orig: "pass\r\nword",
leading: 0,
trailing: 0,
tgt: "************\r\n************",
newline: true,
},
testRedactSet{
name: "standard with newline without newlines",
orig: "pass\nword",
leading: 0,
trailing: 0,
tgt: "***************************",
},
testRedactSet{
name: "single leading",
orig: "password",
leading: 1,
trailing: 0,
tgt: "p*********************",
},
testRedactSet{
name: "single trailing",
orig: "password",
leading: 0,
trailing: 1,
tgt: "*********************d",
},
testRedactSet{
name: "three leading",
orig: "password",
leading: 3,
trailing: 0,
tgt: "pas***************",
},
testRedactSet{
name: "three trailing",
orig: "password",
leading: 0,
trailing: 3,
tgt: "***************ord",
},
testRedactSet{
name: "three leading and trailing",
orig: "password",
leading: 3,
trailing: 3,
tgt: "pas******ord",
},
testRedactSet{
name: "unmask overflow leading",
orig: "password",
leading: 5,
trailing: 4,
tgt: "************************",
},
testRedactSet{
name: "unmask overflow trailing",
orig: "password",
leading: 4,
trailing: 5,
tgt: "************************",
},
testRedactSet{
name: "single mask",
orig: "password",
leading: 0,
trailing: 0,
tgt: "********",
mask: "*",
},
testRedactSet{
name: "standard trailing newline with newlines",
orig: "password\n",
leading: 0,
trailing: 0,
tgt: "************************\n",
newline: true,
},
testRedactSet{
name: "standard trailing newline without newlines",
orig: "password\n",
leading: 0,
trailing: 0,
tgt: "***************************",
},
}
for idx, ts := range tests {
out = Redact(ts.orig, ts.mask, ts.leading, ts.trailing, ts.newline)
if out == ts.tgt {
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
} else {
t.Errorf(
"[%d] FAIL (%s): %#v (len %d):\n"+
"\t\t\texpected (len %d): %#v\n"+
"\t\t\tgot (len %d): %#v\n"+
"\t\t%#v",
idx, ts.name, ts.orig, len(ts.orig),
len(ts.tgt), ts.tgt,
len(out), out,
ts,
)
}
}
}
func TestTrimLines(t *testing.T) {
var out string
var tests []testTrimLinesSet = []testTrimLinesSet{
testTrimLinesSet{
name: "none",
orig: " foo \n bar \n baz ",
left: false,
right: false,
tgt: " foo \n bar \n baz ",
},
testTrimLinesSet{
name: "standard",
orig: " foo \n bar \n baz ",
left: true,
right: true,
tgt: "foo\nbar\nbaz",
},
testTrimLinesSet{
name: "left only",
orig: " foo \n bar \n baz ",
left: true,
right: false,
tgt: "foo \nbar \nbaz ",
},
testTrimLinesSet{
name: "right only",
orig: " foo \n bar \n baz ",
left: false,
right: true,
tgt: " foo\n bar\n baz",
},
testTrimLinesSet{
name: "standard, trailing newline",
orig: " foo \n bar \n baz \n",
left: true,
right: true,
tgt: "foo\nbar\nbaz\n",
},
testTrimLinesSet{
name: "left only, trailing newline",
orig: " foo \n bar \n baz \n",
left: true,
right: false,
tgt: "foo \nbar \nbaz \n",
},
testTrimLinesSet{
name: "right only, trailing newline",
orig: " foo \n bar \n baz \n",
left: false,
right: true,
tgt: " foo\n bar\n baz\n",
},
// Since there's no "non-space" boundary, both of these condition tests do the same thing.
testTrimLinesSet{
name: "left only, trailing newline and ws",
orig: " foo \n bar \n baz \n ",
left: true,
right: false,
tgt: "foo \nbar \nbaz \n",
},
testTrimLinesSet{
name: "right only, trailing newline and ws",
orig: " foo \n bar \n baz \n ",
left: false,
right: true,
tgt: " foo\n bar\n baz\n",
},
}
for idx, ts := range tests {
out = TrimLines(ts.orig, ts.left, ts.right)
if out == ts.tgt {
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
} else {
t.Errorf(
"[%d] FAIL (%s): %#v (len %d):\n"+
"\t\t\texpected (len %d): %#v\n"+
"\t\t\tgot (len %d): %#v\n"+
"\t\t%#v",
idx, ts.name, ts.orig, len(ts.orig),
len(ts.tgt), ts.tgt,
len(out), out,
ts,
)
}
}
}
func TestTrimSpaceLeft(t *testing.T) {
var out string
var tests []testTrimSet = []testTrimSet{
testTrimSet{
name: "standard",
orig: " foo ",
tgt: "foo ",
},
testTrimSet{
name: "tabs",
orig: "\t\tfoo\t\t",
tgt: "foo\t\t",
},
testTrimSet{
name: "newlines",
orig: "\n\nfoo\n\n",
tgt: "foo\n\n",
},
}
for idx, ts := range tests {
out = TrimSpaceLeft(ts.orig)
if out == ts.tgt {
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
} else {
t.Errorf(
"[%d] FAIL (%s): %#v (len %d):\n"+
"\t\t\texpected (len %d): %#v\n"+
"\t\t\tgot (len %d): %#v\n"+
"\t\t%#v",
idx, ts.name, ts.orig, len(ts.orig),
len(ts.tgt), ts.tgt,
len(out), out,
ts,
)
}
}
}
func TestTrimSpaceRight(t *testing.T) {
var out string
var tests []testTrimSet = []testTrimSet{
testTrimSet{
name: "standard",
orig: " foo ",
tgt: " foo",
},
testTrimSet{
name: "tabs",
orig: "\t\tfoo\t\t",
tgt: "\t\tfoo",
},
testTrimSet{
name: "newlines",
orig: "\n\nfoo\n\n",
tgt: "\n\nfoo",
},
}
for idx, ts := range tests {
out = TrimSpaceRight(ts.orig)
if out == ts.tgt {
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
} else {
t.Errorf(
"[%d] FAIL (%s): %#v (len %d):\n"+
"\t\t\texpected (len %d): %#v\n"+
"\t\t\tgot (len %d): %#v\n"+
"\t\t%#v",
idx, ts.name, ts.orig, len(ts.orig),
len(ts.tgt), ts.tgt,
len(out), out,
ts,
)
}
}
}