// Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package csv import ( "errors" "fmt" "io" "reflect" "slices" "strings" "testing" "unicode/utf8" ) type readTest struct { Name string Input string Output [][]string Positions [][][2]int Errors []error // These fields are copied into the Reader Comma rune Comment rune UseFieldsPerRecord bool // false (default) means FieldsPerRecord is -1 FieldsPerRecord int LazyQuotes bool TrimLeadingSpace bool ReuseRecord bool } // In these tests, the §, ¶ and ∑ characters in readTest.Input are used to denote // the start of a field, a record boundary and the position of an error respectively. // They are removed before parsing and are used to verify the position // information reported by FieldPos. var readTests = []readTest{{ Name: "Simple", Input: "§a,§b,§c\n", Output: [][]string{{"a", "b", "c"}}, }, { Name: "CRLF", Input: "§a,§b\r\n¶§c,§d\r\n", Output: [][]string{{"a", "b"}, {"c", "d"}}, }, { Name: "BareCR", Input: "§a,§b\rc,§d\r\n", Output: [][]string{{"a", "b\rc", "d"}}, }, { Name: "RFC4180test", Input: `§#field1,§field2,§field3 ¶§"aaa",§"bb b",§"ccc" ¶§"a,a",§"b""bb",§"ccc" ¶§zzz,§yyy,§xxx `, Output: [][]string{ {"#field1", "field2", "field3"}, {"aaa", "bb\nb", "ccc"}, {"a,a", `b"bb`, "ccc"}, {"zzz", "yyy", "xxx"}, }, UseFieldsPerRecord: true, FieldsPerRecord: 0, }, { Name: "NoEOLTest", Input: "§a,§b,§c", Output: [][]string{{"a", "b", "c"}}, }, { Name: "Semicolon", Input: "§a;§b;§c\n", Output: [][]string{{"a", "b", "c"}}, Comma: ';', }, { Name: "MultiLine", Input: `§"two line",§"one line",§"three line field"`, Output: [][]string{{"two\nline", "one line", "three\nline\nfield"}}, }, { Name: "BlankLine", Input: "§a,§b,§c\n\n¶§d,§e,§f\n\n", Output: [][]string{ {"a", "b", "c"}, {"d", "e", "f"}, }, }, { Name: "BlankLineFieldCount", Input: "§a,§b,§c\n\n¶§d,§e,§f\n\n", Output: [][]string{ {"a", "b", "c"}, {"d", "e", "f"}, }, UseFieldsPerRecord: true, FieldsPerRecord: 0, }, { Name: "TrimSpace", Input: " §a, §b, §c\n", Output: [][]string{{"a", "b", "c"}}, TrimLeadingSpace: true, }, { Name: "LeadingSpace", Input: "§ a,§ b,§ c\n", Output: [][]string{{" a", " b", " c"}}, }, { Name: "Comment", Input: "#1,2,3\n§a,§b,§c\n#comment", Output: [][]string{{"a", "b", "c"}}, Comment: '#', }, { Name: "NoComment", Input: "§#1,§2,§3\n¶§a,§b,§c", Output: [][]string{{"#1", "2", "3"}, {"a", "b", "c"}}, }, { Name: "LazyQuotes", Input: `§a "word",§"1"2",§a",§"b`, Output: [][]string{{`a "word"`, `1"2`, `a"`, `b`}}, LazyQuotes: true, }, { Name: "BareQuotes", Input: `§a "word",§"1"2",§a"`, Output: [][]string{{`a "word"`, `1"2`, `a"`}}, LazyQuotes: true, }, { Name: "BareDoubleQuotes", Input: `§a""b,§c`, Output: [][]string{{`a""b`, `c`}}, LazyQuotes: true, }, { Name: "BadDoubleQuotes", Input: `§a∑""b,c`, Errors: []error{&ParseError{Err: ErrBareQuote}}, }, { Name: "TrimQuote", Input: ` §"a",§" b",§c`, Output: [][]string{{"a", " b", "c"}}, TrimLeadingSpace: true, }, { Name: "BadBareQuote", Input: `§a ∑"word","b"`, Errors: []error{&ParseError{Err: ErrBareQuote}}, }, { Name: "BadTrailingQuote", Input: `§"a word",b∑"`, Errors: []error{&ParseError{Err: ErrBareQuote}}, }, { Name: "ExtraneousQuote", Input: `§"a ∑"word","b"`, Errors: []error{&ParseError{Err: ErrQuote}}, }, { Name: "BadFieldCount", Input: "§a,§b,§c\n¶∑§d,§e", Errors: []error{nil, &ParseError{Err: ErrFieldCount}}, Output: [][]string{{"a", "b", "c"}, {"d", "e"}}, UseFieldsPerRecord: true, FieldsPerRecord: 0, }, { Name: "BadFieldCountMultiple", Input: "§a,§b,§c\n¶∑§d,§e\n¶∑§f", Errors: []error{nil, &ParseError{Err: ErrFieldCount}, &ParseError{Err: ErrFieldCount}}, Output: [][]string{{"a", "b", "c"}, {"d", "e"}, {"f"}}, UseFieldsPerRecord: true, FieldsPerRecord: 0, }, { Name: "BadFieldCount1", Input: `§∑a,§b,§c`, Errors: []error{&ParseError{Err: ErrFieldCount}}, Output: [][]string{{"a", "b", "c"}}, UseFieldsPerRecord: true, FieldsPerRecord: 2, }, { Name: "FieldCount", Input: "§a,§b,§c\n¶§d,§e", Output: [][]string{{"a", "b", "c"}, {"d", "e"}}, }, { Name: "TrailingCommaEOF", Input: "§a,§b,§c,§", Output: [][]string{{"a", "b", "c", ""}}, }, { Name: "TrailingCommaEOL", Input: "§a,§b,§c,§\n", Output: [][]string{{"a", "b", "c", ""}}, }, { Name: "TrailingCommaSpaceEOF", Input: "§a,§b,§c, §", Output: [][]string{{"a", "b", "c", ""}}, TrimLeadingSpace: true, }, { Name: "TrailingCommaSpaceEOL", Input: "§a,§b,§c, §\n", Output: [][]string{{"a", "b", "c", ""}}, TrimLeadingSpace: true, }, { Name: "TrailingCommaLine3", Input: "§a,§b,§c\n¶§d,§e,§f\n¶§g,§hi,§", Output: [][]string{{"a", "b", "c"}, {"d", "e", "f"}, {"g", "hi", ""}}, TrimLeadingSpace: true, }, { Name: "NotTrailingComma3", Input: "§a,§b,§c,§ \n", Output: [][]string{{"a", "b", "c", " "}}, }, { Name: "CommaFieldTest", Input: `§x,§y,§z,§w ¶§x,§y,§z,§ ¶§x,§y,§,§ ¶§x,§,§,§ ¶§,§,§,§ ¶§"x",§"y",§"z",§"w" ¶§"x",§"y",§"z",§"" ¶§"x",§"y",§"",§"" ¶§"x",§"",§"",§"" ¶§"",§"",§"",§"" `, Output: [][]string{ {"x", "y", "z", "w"}, {"x", "y", "z", ""}, {"x", "y", "", ""}, {"x", "", "", ""}, {"", "", "", ""}, {"x", "y", "z", "w"}, {"x", "y", "z", ""}, {"x", "y", "", ""}, {"x", "", "", ""}, {"", "", "", ""}, }, }, { Name: "TrailingCommaIneffective1", Input: "§a,§b,§\n¶§c,§d,§e", Output: [][]string{ {"a", "b", ""}, {"c", "d", "e"}, }, TrimLeadingSpace: true, }, { Name: "ReadAllReuseRecord", Input: "§a,§b\n¶§c,§d", Output: [][]string{ {"a", "b"}, {"c", "d"}, }, ReuseRecord: true, }, { Name: "StartLine1", // Issue 19019 Input: "§a,\"b\nc∑\"d,e", Errors: []error{&ParseError{Err: ErrQuote}}, }, { Name: "StartLine2", Input: "§a,§b\n¶§\"d\n\n,e∑", Errors: []error{nil, &ParseError{Err: ErrQuote}}, Output: [][]string{{"a", "b"}}, }, { Name: "CRLFInQuotedField", // Issue 21201 Input: "§A,§\"Hello\r\nHi\",§B\r\n", Output: [][]string{ {"A", "Hello\nHi", "B"}, }, }, { Name: "BinaryBlobField", // Issue 19410 Input: "§x09\x41\xb4\x1c,§aktau", Output: [][]string{{"x09A\xb4\x1c", "aktau"}}, }, { Name: "TrailingCR", Input: "§field1,§field2\r", Output: [][]string{{"field1", "field2"}}, }, { Name: "QuotedTrailingCR", Input: "§\"field\"\r", Output: [][]string{{"field"}}, }, { Name: "QuotedTrailingCRCR", Input: "§\"field∑\"\r\r", Errors: []error{&ParseError{Err: ErrQuote}}, }, { Name: "FieldCR", Input: "§field\rfield\r", Output: [][]string{{"field\rfield"}}, }, { Name: "FieldCRCR", Input: "§field\r\rfield\r\r", Output: [][]string{{"field\r\rfield\r"}}, }, { Name: "FieldCRCRLF", Input: "§field\r\r\n¶§field\r\r\n", Output: [][]string{{"field\r"}, {"field\r"}}, }, { Name: "FieldCRCRLFCR", Input: "§field\r\r\n¶§\rfield\r\r\n\r", Output: [][]string{{"field\r"}, {"\rfield\r"}}, }, { Name: "FieldCRCRLFCRCR", Input: "§field\r\r\n¶§\r\rfield\r\r\n¶§\r\r", Output: [][]string{{"field\r"}, {"\r\rfield\r"}, {"\r"}}, }, { Name: "MultiFieldCRCRLFCRCR", Input: "§field1,§field2\r\r\n¶§\r\rfield1,§field2\r\r\n¶§\r\r,§", Output: [][]string{ {"field1", "field2\r"}, {"\r\rfield1", "field2\r"}, {"\r\r", ""}, }, }, { Name: "NonASCIICommaAndComment", Input: "§a£§b,c£ \t§d,e\n€ comment\n", Output: [][]string{{"a", "b,c", "d,e"}}, TrimLeadingSpace: true, Comma: '£', Comment: '€', }, { Name: "NonASCIICommaAndCommentWithQuotes", Input: "§a€§\" b,\"€§ c\nλ comment\n", Output: [][]string{{"a", " b,", " c"}}, Comma: '€', Comment: 'λ', }, { // λ and θ start with the same byte. // This tests that the parser doesn't confuse such characters. Name: "NonASCIICommaConfusion", Input: "§\"abθcd\"λ§efθgh", Output: [][]string{{"abθcd", "efθgh"}}, Comma: 'λ', Comment: '€', }, { Name: "NonASCIICommentConfusion", Input: "§λ\n¶§λ\nθ\n¶§λ\n", Output: [][]string{{"λ"}, {"λ"}, {"λ"}}, Comment: 'θ', }, { Name: "QuotedFieldMultipleLF", Input: "§\"\n\n\n\n\"", Output: [][]string{{"\n\n\n\n"}}, }, { Name: "MultipleCRLF", Input: "\r\n\r\n\r\n\r\n", }, { // The implementation may read each line in several chunks if it doesn't fit entirely // in the read buffer, so we should test the code to handle that condition. Name: "HugeLines", Input: strings.Repeat("#ignore\n", 10000) + "§" + strings.Repeat("@", 5000) + ",§" + strings.Repeat("*", 5000), Output: [][]string{{strings.Repeat("@", 5000), strings.Repeat("*", 5000)}}, Comment: '#', }, { Name: "QuoteWithTrailingCRLF", Input: "§\"foo∑\"bar\"\r\n", Errors: []error{&ParseError{Err: ErrQuote}}, }, { Name: "LazyQuoteWithTrailingCRLF", Input: "§\"foo\"bar\"\r\n", Output: [][]string{{`foo"bar`}}, LazyQuotes: true, }, { Name: "DoubleQuoteWithTrailingCRLF", Input: "§\"foo\"\"bar\"\r\n", Output: [][]string{{`foo"bar`}}, }, { Name: "EvenQuotes", Input: `§""""""""`, Output: [][]string{{`"""`}}, }, { Name: "OddQuotes", Input: `§"""""""∑`, Errors: []error{&ParseError{Err: ErrQuote}}, }, { Name: "LazyOddQuotes", Input: `§"""""""`, Output: [][]string{{`"""`}}, LazyQuotes: true, }, { Name: "BadComma1", Comma: '\n', Errors: []error{errInvalidDelim}, }, { Name: "BadComma2", Comma: '\r', Errors: []error{errInvalidDelim}, }, { Name: "BadComma3", Comma: '"', Errors: []error{errInvalidDelim}, }, { Name: "BadComma4", Comma: utf8.RuneError, Errors: []error{errInvalidDelim}, }, { Name: "BadComment1", Comment: '\n', Errors: []error{errInvalidDelim}, }, { Name: "BadComment2", Comment: '\r', Errors: []error{errInvalidDelim}, }, { Name: "BadComment3", Comment: utf8.RuneError, Errors: []error{errInvalidDelim}, }, { Name: "BadCommaComment", Comma: 'X', Comment: 'X', Errors: []error{errInvalidDelim}, }} func TestRead(t *testing.T) { newReader := func(tt readTest) (*Reader, [][][2]int, map[int][2]int, string) { positions, errPositions, input := makePositions(tt.Input) r := NewReader(strings.NewReader(input)) if tt.Comma != 0 { r.Comma = tt.Comma } r.Comment = tt.Comment if tt.UseFieldsPerRecord { r.FieldsPerRecord = tt.FieldsPerRecord } else { r.FieldsPerRecord = -1 } r.LazyQuotes = tt.LazyQuotes r.TrimLeadingSpace = tt.TrimLeadingSpace r.ReuseRecord = tt.ReuseRecord return r, positions, errPositions, input } for _, tt := range readTests { t.Run(tt.Name, func(t *testing.T) { r, positions, errPositions, input := newReader(tt) out, err := r.ReadAll() if wantErr := firstError(tt.Errors, positions, errPositions); wantErr != nil { if !reflect.DeepEqual(err, wantErr) { t.Fatalf("ReadAll() error mismatch:\ngot %v (%#v)\nwant %v (%#v)", err, err, wantErr, wantErr) } if out != nil { t.Fatalf("ReadAll() output:\ngot %q\nwant nil", out) } } else { if err != nil { t.Fatalf("unexpected Readall() error: %v", err) } if !reflect.DeepEqual(out, tt.Output) { t.Fatalf("ReadAll() output:\ngot %q\nwant %q", out, tt.Output) } } // Check input offset after call ReadAll() inputByteSize := len(input) inputOffset := r.InputOffset() if err == nil && int64(inputByteSize) != inputOffset { t.Errorf("wrong input offset after call ReadAll():\ngot: %d\nwant: %d\ninput: %s", inputOffset, inputByteSize, input) } // Check field and error positions. r, _, _, _ = newReader(tt) for recNum := 0; ; recNum++ { rec, err := r.Read() var wantErr error if recNum < len(tt.Errors) && tt.Errors[recNum] != nil { wantErr = errorWithPosition(tt.Errors[recNum], recNum, positions, errPositions) } else if recNum >= len(tt.Output) { wantErr = io.EOF } if !reflect.DeepEqual(err, wantErr) { t.Fatalf("Read() error at record %d:\ngot %v (%#v)\nwant %v (%#v)", recNum, err, err, wantErr, wantErr) } // ErrFieldCount is explicitly non-fatal. if err != nil && !errors.Is(err, ErrFieldCount) { if recNum < len(tt.Output) { t.Fatalf("need more records; got %d want %d", recNum, len(tt.Output)) } break } if got, want := rec, tt.Output[recNum]; !slices.Equal(got, want) { t.Errorf("Read vs ReadAll mismatch;\ngot %q\nwant %q", got, want) } pos := positions[recNum] if len(pos) != len(rec) { t.Fatalf("mismatched position length at record %d", recNum) } for i := range rec { line, col := r.FieldPos(i) if got, want := [2]int{line, col}, pos[i]; got != want { t.Errorf("position mismatch at record %d, field %d;\ngot %v\nwant %v", recNum, i, got, want) } } } }) } } // firstError returns the first non-nil error in errs, // with the position adjusted according to the error's // index inside positions. func firstError(errs []error, positions [][][2]int, errPositions map[int][2]int) error { for i, err := range errs { if err != nil { return errorWithPosition(err, i, positions, errPositions) } } return nil } func errorWithPosition(err error, recNum int, positions [][][2]int, errPositions map[int][2]int) error { parseErr, ok := err.(*ParseError) if !ok { return err } if recNum >= len(positions) { panic(fmt.Errorf("no positions found for error at record %d", recNum)) } errPos, ok := errPositions[recNum] if !ok { panic(fmt.Errorf("no error position found for error at record %d", recNum)) } parseErr1 := *parseErr parseErr1.StartLine = positions[recNum][0][0] parseErr1.Line = errPos[0] parseErr1.Column = errPos[1] return &parseErr1 } // makePositions returns the expected field positions of all // the fields in text, the positions of any errors, and the text with the position markers // removed. // // The start of each field is marked with a § symbol; // CSV lines are separated by ¶ symbols; // Error positions are marked with ∑ symbols. func makePositions(text string) ([][][2]int, map[int][2]int, string) { buf := make([]byte, 0, len(text)) var positions [][][2]int errPositions := make(map[int][2]int) line, col := 1, 1 recNum := 0 for len(text) > 0 { r, size := utf8.DecodeRuneInString(text) switch r { case '\n': line++ col = 1 buf = append(buf, '\n') case '§': if len(positions) == 0 { positions = append(positions, [][2]int{}) } positions[len(positions)-1] = append(positions[len(positions)-1], [2]int{line, col}) case '¶': positions = append(positions, [][2]int{}) recNum++ case '∑': errPositions[recNum] = [2]int{line, col} default: buf = append(buf, text[:size]...) col += size } text = text[size:] } return positions, errPositions, string(buf) } // nTimes is an io.Reader which yields the string s n times. type nTimes struct { s string n int off int } func (r *nTimes) Read(p []byte) (n int, err error) { for { if r.n <= 0 || r.s == "" { return n, io.EOF } n0 := copy(p, r.s[r.off:]) p = p[n0:] n += n0 r.off += n0 if r.off == len(r.s) { r.off = 0 r.n-- } if len(p) == 0 { return } } } // benchmarkRead measures reading the provided CSV rows data. // initReader, if non-nil, modifies the Reader before it's used. func benchmarkRead(b *testing.B, initReader func(*Reader), rows string) { b.ReportAllocs() r := NewReader(&nTimes{s: rows, n: b.N}) if initReader != nil { initReader(r) } for { _, err := r.Read() if err == io.EOF { break } if err != nil { b.Fatal(err) } } } const benchmarkCSVData = `x,y,z,w x,y,z, x,y,, x,,, ,,, "x","y","z","w" "x","y","z","" "x","y","","" "x","","","" "","","","" ` func BenchmarkRead(b *testing.B) { benchmarkRead(b, nil, benchmarkCSVData) } func BenchmarkReadWithFieldsPerRecord(b *testing.B) { benchmarkRead(b, func(r *Reader) { r.FieldsPerRecord = 4 }, benchmarkCSVData) } func BenchmarkReadWithoutFieldsPerRecord(b *testing.B) { benchmarkRead(b, func(r *Reader) { r.FieldsPerRecord = -1 }, benchmarkCSVData) } func BenchmarkReadLargeFields(b *testing.B) { benchmarkRead(b, nil, strings.Repeat(`xxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv xxxxxxxxxxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvv ,,zzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv `, 3)) } func BenchmarkReadReuseRecord(b *testing.B) { benchmarkRead(b, func(r *Reader) { r.ReuseRecord = true }, benchmarkCSVData) } func BenchmarkReadReuseRecordWithFieldsPerRecord(b *testing.B) { benchmarkRead(b, func(r *Reader) { r.ReuseRecord = true; r.FieldsPerRecord = 4 }, benchmarkCSVData) } func BenchmarkReadReuseRecordWithoutFieldsPerRecord(b *testing.B) { benchmarkRead(b, func(r *Reader) { r.ReuseRecord = true; r.FieldsPerRecord = -1 }, benchmarkCSVData) } func BenchmarkReadReuseRecordLargeFields(b *testing.B) { benchmarkRead(b, func(r *Reader) { r.ReuseRecord = true }, strings.Repeat(`xxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv xxxxxxxxxxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvv ,,zzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv `, 3)) }