-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcsvpp.go
More file actions
129 lines (116 loc) · 4.71 KB
/
Copy pathcsvpp.go
File metadata and controls
129 lines (116 loc) · 4.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package csvpp
import (
"errors"
"fmt"
)
// FieldKind represents the type of field as defined in IETF CSV++ Section 2.2.
// See: https://datatracker.ietf.org/doc/draft-mscaldas-csvpp/
type FieldKind int
const (
SimpleField FieldKind = iota // IETF Section 2.2.1: simple-field = name
ArrayField // IETF Section 2.2.2: array-field = name "[" [delimiter] "]"
StructuredField // IETF Section 2.2.3: struct-field = name [component-delim] "(" component-list ")"
ArrayStructuredField // IETF Section 2.2.4: array-struct-field = name "[" [delimiter] "]" [component-delim] "(" component-list ")"
)
// String returns the string representation of FieldKind.
func (k FieldKind) String() string { //nostyle:recvtype
switch k {
case SimpleField:
return "SimpleField"
case ArrayField:
return "ArrayField"
case StructuredField:
return "StructuredField"
case ArrayStructuredField:
return "ArrayStructuredField"
default:
return fmt.Sprintf("FieldKind(%d)", k)
}
}
// Default delimiters as recommended in IETF CSV++ Section 2.3.2.
// The specification suggests delimiter progression: ~ → ^ → ; → : for nested structures.
const (
DefaultArrayDelimiter = '~' // IETF Section 2.3.2: recommended for array fields
DefaultComponentDelimiter = '^' // IETF Section 2.3.2: recommended for structured fields
)
// DefaultMaxNestingDepth is the default maximum nesting depth.
// IETF Section 5 (Security Considerations) recommends limiting nesting depth to prevent
// stack overflow attacks from maliciously crafted input.
const DefaultMaxNestingDepth = 10
// ColumnHeader represents the declaration information for an individual field.
// It corresponds to the ABNF "field" rule in IETF CSV++ Section 2.2:
//
// field = simple-field / array-field / struct-field / array-struct-field
// name = 1*field-char
// field-char = ALPHA / DIGIT / "_" / "-"
type ColumnHeader struct {
Name string // Field name (ABNF: name = 1*field-char)
Kind FieldKind // Field type (IETF Section 2.2)
ArrayDelimiter rune // Array delimiter (ABNF: delimiter)
ComponentDelimiter rune // Component delimiter (ABNF: component-delim)
Components []*ColumnHeader // Component list (ABNF: component-list)
}
// Field represents a parsed field value from a data row.
// The populated fields depend on the corresponding ColumnHeader.Kind:
//
// - SimpleField: Value is set
// - ArrayField: Values is set
// - StructuredField: Components is set (each component is a Field)
// - ArrayStructuredField: Components is set (each is a Field with its own Components)
type Field struct {
Value string // Value for SimpleField
Values []string // Values for ArrayField (IETF Section 2.2.2)
Components []*Field // Components for StructuredField/ArrayStructuredField (IETF Section 2.2.3/2.2.4)
}
// Error definitions.
var (
ErrNoHeader = errors.New("csvpp: header record is required")
ErrInvalidHeader = errors.New("csvpp: invalid column header format")
ErrNestingTooDeep = errors.New("csvpp: nesting level exceeds limit")
)
// ParseError holds detailed information about an error that occurred during parsing.
type ParseError struct {
Line int // Line number where the error occurred (1-based)
Column int // Column number where the error occurred (1-based)
Field string // Field name (if available)
Err error // Original error
}
// Error returns the error message for ParseError.
func (e *ParseError) Error() string {
if e.Field != "" {
return fmt.Sprintf("csvpp: line %d, column %d (field %q): %v", e.Line, e.Column, e.Field, e.Err)
}
if e.Column > 0 {
return fmt.Sprintf("csvpp: line %d, column %d: %v", e.Line, e.Column, e.Err)
}
return fmt.Sprintf("csvpp: line %d: %v", e.Line, e.Err)
}
// Unwrap returns the original error.
func (e *ParseError) Unwrap() error {
return e.Err
}
// HasFormulaPrefix reports whether s starts with a character that spreadsheet
// applications may interpret as a formula. These characters are: '=', '+', '-', '@'.
//
// When CSV files are opened in spreadsheet applications like Microsoft Excel or
// Google Sheets, values beginning with these characters may be executed as formulas,
// potentially leading to security vulnerabilities (CSV injection).
//
// This function helps identify potentially dangerous values so that applications
// can take appropriate action, such as prefixing with a single quote or rejecting the input.
//
// Example:
//
// if csvpp.HasFormulaPrefix(value) {
// value = "'" + value // Escape for spreadsheet safety
// }
func HasFormulaPrefix(s string) bool {
if len(s) == 0 {
return false
}
switch s[0] {
case '=', '+', '-', '@':
return true
}
return false
}