more fuzzed fixes

This commit is contained in:
David 2024-03-30 20:10:41 -04:00
parent 6aa1219f2e
commit 377c3b02b7
12 changed files with 152 additions and 33 deletions

80
main.go
View File

@ -14,6 +14,8 @@ import (
"golang.org/x/term" "golang.org/x/term"
) )
const DEBUG = false
var ( var (
// upper-left, upper-right, left side, right side, bottom-left, bottom-right // upper-left, upper-right, left side, right side, bottom-left, bottom-right
SAY_BORDER = []rune{'/', '\\', '|', '|', '\\', '/'} SAY_BORDER = []rune{'/', '\\', '|', '|', '\\', '/'}
@ -103,7 +105,7 @@ func main() {
if err != nil { if err != nil {
log.Fatalf("error reading from stdin: %v", err) log.Fatalf("error reading from stdin: %v", err)
} }
input = strings.TrimSpace(string(pipedInput)) input = string(bytes.TrimSpace(pipedInput))
} }
if input == "" { if input == "" {
@ -115,7 +117,7 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
generateCow(string(b), input, opts) fmt.Print(generateCow(string(b), input, opts))
} }
func generateCow(cow, say string, opt options) string { func generateCow(cow, say string, opt options) string {
@ -157,21 +159,73 @@ func generateCow(cow, say string, opt options) string {
return out.String() return out.String()
} }
// this is an attempt to emulate the rather poorly-documented behavior of Text::Wrap::fill
// from CPAN: https://github.com/ap/Text-Tabs/blob/master/lib.modern/Text/Wrap.pm
// The basic idea is: take an input. Format each "paragraph" of text independently,
// then merge with empty lines between.
//
// What is under-defined in the documentation is what fill() considers a "paragraph".
// From testing and reviewing the rather obtuse code, a paragraph is any number of lines
// that start with a line that has optional whitespace at its start, then all subsequent lines
// that have no whitespace at the start, until either reaching another line that has leading
// whitespace, or a blank line. fill() also "destroy[s] any whitespace in the original text",
// by which it appearently means all tabs and multiple-spaces are replaced with single-spaces.
//
// So, for example, `text\ntext` is one paragraph, `text\n text` is two paragraphs. Since for
// our purposes we don't want any indentation of paragraphs, the output of these two examples
// should be `text text` and `text\n\ntext`, respectively.
//
// This is a pretty gnarly! Might be easier in a multi-pass model: collect together each paragraph,
// collapse them into single strings and format in a sub-function, then merge them all together.
// That's closer to how the perl module does it.
func wordWrap(text []string, column int) ([]string, int) { func wordWrap(text []string, column int) ([]string, int) {
out := []string{} out := []string{}
length := 0 length := 0
var b strings.Builder var b strings.Builder
for _, line := range text { var inParagraph bool
b = strings.Builder{} for beginning, line := range text {
// remove control character whitespace // remove control character whitespace
line = strings.ReplaceAll(line, "\t", " ") line = strings.ReplaceAll(line, "\t", " ")
line = strings.ReplaceAll(line, "\v", " ") line = strings.ReplaceAll(line, "\v", " ")
line = strings.ReplaceAll(line, "\f", " ") line = strings.ReplaceAll(line, "\f", " ")
line = strings.ReplaceAll(line, "\r", " ")
words := strings.Split(line, " ") words := strings.Split(line, " ")
// skip empty newlines if not in a paragraph, but start a new paragraph if we are.
if strings.TrimSpace(line) == "" {
if inParagraph {
length = max(length, b.Len()-1)
out = append(out, b.String()[0:b.Len()-1])
out = append(out, "")
b = strings.Builder{}
inParagraph = false
}
continue
}
// we -1 all over the place to clean up the loose space on the end of the builder // we -1 all over the place to clean up the loose space on the end of the builder
for _, word := range words { // split into words for i, word := range words { // split into words
if b.Len() > 0 && word == "" { if DEBUG {
// skip multiple spaces in a row log.Printf("%d, `%s`", i, b.String())
}
if inParagraph && i == 0 && word == "" && len(words) != 0 {
// we've found a new paragraph while we were still processing the old one.
// (that is, the new line we're parsing had leading whitespace)
length = max(length, b.Len()-1)
out = append(out, b.String()[0:b.Len()-1])
out = append(out, "")
b = strings.Builder{}
log.Println("here")
}
// bizarrely, cowsay allows for indentation to survive in the /first/ paragraph,
// but only up to two spaces worth.
if beginning == 0 && (b.Len() == 0 || b.String() == " ") && word == "" {
b.WriteRune(' ')
}
if b.Len() == 0 && word != "" {
inParagraph = true
}
if b.Len() >= 0 && word == "" && i+1 != len(words) {
// skip multiple spaces in a row...
// ...but a single trailing space on a line is OK??
continue continue
} }
if b.Len() > 0 && b.Len()+len(word)+1 > column { if b.Len() > 0 && b.Len()+len(word)+1 > column {
@ -182,8 +236,9 @@ func wordWrap(text []string, column int) ([]string, int) {
out = append(out, b.String()[0:b.Len()-1]) out = append(out, b.String()[0:b.Len()-1])
b = strings.Builder{} b = strings.Builder{}
} }
if b.Len() == 0 && len(word) >= column { for b.Len() == 0 && len(word) >= column {
// our word is longer than our maximum column size. let's break it up. // our word is longer than our maximum column size. let's break it up.
// we loop until we've consumed the full overly-long word.
length = max(length, column-1) length = max(length, column-1)
out = append(out, word[0:column-1]) out = append(out, word[0:column-1])
word = word[column-1:] word = word[column-1:]
@ -192,11 +247,14 @@ func wordWrap(text []string, column int) ([]string, int) {
// actually append the word and a space // actually append the word and a space
b.WriteString(word) b.WriteString(word)
b.WriteRune(' ') b.WriteRune(' ')
if DEBUG {
log.Printf("%d, `%s`", i, b.String())
}
} }
// out of words in the line, so save it off and start on the next one
length = max(length, b.Len()-1)
out = append(out, b.String()[0:b.Len()-1])
} }
// out of words! save off our last line.
length = max(length, b.Len()-1)
out = append(out, b.String()[0:b.Len()-1])
return out, length return out, length
} }

View File

@ -43,28 +43,37 @@ func TestGenerateCow(t *testing.T) {
expected: string(must(os.ReadFile("./testdata/basic.cow"))), expected: string(must(os.ReadFile("./testdata/basic.cow"))),
}, },
{ {
name: "multiline test", name: "single-paragraph test",
args: args{ args: args{
cow: string(must(os.ReadFile("./cows/default.cow"))), cow: string(must(os.ReadFile("./cows/default.cow"))),
say: "test\ntext", say: "test\ntext",
opt: defaultOptions, opt: defaultOptions,
}, },
expected: string(must(os.ReadFile("./testdata/multiline.cow"))), expected: string(must(os.ReadFile("./testdata/one_paragraph.cow"))),
}, },
{ {
name: "multiline with wordwrap test", name: "multiline with wordwrap test",
args: args{ args: args{
cow: string(must(os.ReadFile("./cows/default.cow"))), cow: string(must(os.ReadFile("./cows/default.cow"))),
say: "this is a long block of text.\n\n\nIt goes over many lines! It'll even word-wrap for us.", say: "this is a long block of text.\nIt goes over many lines! It'll even word-wrap for us.",
opt: defaultOptions, opt: defaultOptions,
}, },
expected: string(must(os.ReadFile("./testdata/longer_multiline.cow"))), expected: string(must(os.ReadFile("./testdata/longer_multiline.cow"))),
}, },
{
name: "two-paragraph multiline with wordwrap test",
args: args{
cow: string(must(os.ReadFile("./cows/default.cow"))),
say: "this is a long block of text.\n\nIt goes over many lines! It'll even word-wrap for us.",
opt: defaultOptions,
},
expected: string(must(os.ReadFile("./testdata/two_paragraph_multiline.cow"))),
},
{ {
name: "offset bubble", name: "offset bubble",
args: args{ args: args{
cow: string(must(os.ReadFile("./cows/dragon-and-cow.cow"))), cow: string(must(os.ReadFile("./cows/dragon-and-cow.cow"))),
say: "this is a long block of text.\n\n\nIt goes over many lines! It'll even word-wrap for us.", say: "this is a long block of text.\nIt goes over many lines! It'll even word-wrap for us.",
opt: defaultOptions, opt: defaultOptions,
}, },
expected: string(must(os.ReadFile("./testdata/dragon.cow"))), expected: string(must(os.ReadFile("./testdata/dragon.cow"))),
@ -105,6 +114,25 @@ func TestGenerateCow(t *testing.T) {
}, },
expected: string(must(os.ReadFile("./testdata/too_long.cow"))), expected: string(must(os.ReadFile("./testdata/too_long.cow"))),
}, },
{
name: "fortune cow",
args: args{
cow: string(must(os.ReadFile("./cows/default.cow"))),
say: `Most people eat as though they were fattening themselves for market.
-- E.W. Howe`,
opt: defaultOptions,
},
expected: string(must(os.ReadFile("./testdata/fortune.cow"))),
},
{
name: "trailing space cow",
args: args{
cow: string(must(os.ReadFile("./cows/default.cow"))),
say: `test `,
opt: defaultOptions,
},
expected: string(must(os.ReadFile("./testdata/trailing_space.cow"))),
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

12
testdata/dragon.cow vendored
View File

@ -1,10 +1,8 @@
_____________________________________ _______________________________________
/ this is a long block of text. \ / this is a long block of text. It goes \
| | | over many lines! It'll even word-wrap |
| | \ for us. /
| It goes over many lines! It'll even | ---------------------------------------
\ word-wrap for us. /
-------------------------------------
\ ^ /^ \ ^ /^
\ / \ // \ \ / \ // \
\ |\___/| / \// .\ \ |\___/| / \// .\

11
testdata/fortune.cow vendored Normal file
View File

@ -0,0 +1,11 @@
_____________________________________
/ Most people eat as though they were \
| fattening themselves for market. |
| |
\ -- E.W. Howe /
-------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||

View File

@ -0,0 +1,2 @@
go test fuzz v1
string("0 \n0")

View File

@ -0,0 +1,2 @@
go test fuzz v1
string("0000000000000000000000000000000000000000000000000000000000000000000000000000000")

View File

@ -0,0 +1,2 @@
go test fuzz v1
string("0 \n 0")

View File

@ -0,0 +1,2 @@
go test fuzz v1
string("0\r0")

View File

@ -1,10 +1,8 @@
_____________________________________ _______________________________________
/ this is a long block of text. \ / this is a long block of text. It goes \
| | | over many lines! It'll even word-wrap |
| | \ for us. /
| It goes over many lines! It'll even | ---------------------------------------
\ word-wrap for us. /
-------------------------------------
\ ^__^ \ ^__^
\ (oo)\_______ \ (oo)\_______
(__)\ )\/\ (__)\ )\/\

8
testdata/one_paragraph.cow vendored Normal file
View File

@ -0,0 +1,8 @@
___________
< test text >
-----------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||

View File

@ -1,7 +1,6 @@
______ _______
/ test \ < test >
\ text / -------
------
\ ^__^ \ ^__^
\ (oo)\_______ \ (oo)\_______
(__)\ )\/\ (__)\ )\/\

11
testdata/two_paragraph_multiline.cow vendored Normal file
View File

@ -0,0 +1,11 @@
_____________________________________
/ this is a long block of text. \
| |
| It goes over many lines! It'll even |
\ word-wrap for us. /
-------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||