Documentation
¶
Overview ¶
Package csv reads and writes graphs as edge lists in CSV format.
The format is a simple table of columns: source, destination, and (optionally) a weight. Lines beginning with the comment character (default '#') are skipped. A header row may declare the column types; without it the reader assumes a fixed (src, dst[, weight]) layout.
Index ¶
- Constants
- Variables
- func ReadInto(r io.Reader, opts Options) (*adjlist.AdjList[string, int64], int, error)
- func ReadIntoCtx(ctx context.Context, r io.Reader, opts Options) (*adjlist.AdjList[string, int64], int, error)
- func Write(w io.Writer, a *adjlist.AdjList[string, int64], opts Options) (int, error)
- func WriteCtx(ctx context.Context, w io.Writer, a *adjlist.AdjList[string, int64], ...) (int, error)
- type Options
Examples ¶
Constants ¶
const DefaultMaxBytes int64 = 128 << 20 // 128 MiB
DefaultMaxBytes is the default ceiling, in bytes, on the amount of input a reader will consume before failing with ErrInputTooLarge. It guards against memory exhaustion from untrusted files (a crafted multi-gigabyte field, for example). A value of zero or less disables the cap; see Options.MaxBytes.
Peak memory ¶
Two independent bounds shape the reader's peak transient RAM:
- The byte cap (this value) bounds the total bytes drawn from the reader. encoding/csv does not bound the size of a single field, so a hostile input such as an unterminated quoted field is buffered up to MaxBytes and the working set (raw buffer plus the parsed field) amplifies that to roughly 4–5× the cap.
- A per-record field-count guard (see the internal fieldGuardReader) caps the delimiter-separated fields in any one record. encoding/csv allocates ~40 bytes of metadata per field, so without this guard a single delimiter-only record would amplify its bytes ~40× — several GiB at this cap. The guard bounds that per-record metadata term to a few MiB regardless of MaxBytes.
DefaultMaxBytes is set to 128 MiB; with both bounds in force the worst-case transient stays a small multiple of MaxBytes (dominated by field content, not by per-field metadata), so raising MaxBytes for a trusted large input scales peak RAM proportionally rather than pathologically. Callers parsing untrusted input should keep the default or lower it further.
Variables ¶
var ErrInputTooLarge = errors.New("csv: input exceeds maximum size")
ErrInputTooLarge is returned by ReadInto and ReadIntoCtx when the input stream exceeds the configured Options.MaxBytes ceiling. The reader stops drawing bytes from the input as soon as the limit is crossed; note, however, that a single oversized field may already have been buffered by encoding/csv up to the cap before the limit trips, so the decoder's peak working set is a multiple of MaxBytes (see DefaultMaxBytes).
var ErrTooManyFields = errors.New("csv: record exceeds maximum field count")
ErrTooManyFields is returned by ReadInto / ReadIntoCtx (wrapped with the offending row) when a single CSV record exceeds [maxFieldsPerRecord] delimiter-separated fields.
Functions ¶
func ReadInto ¶
ReadInto streams a CSV from r into an adjacency list, returning the loaded list and the number of rows ingested. Each row must have at least two fields (src, dst); a third field is parsed as a int64 weight.
Example ¶
ExampleReadInto parses a CSV edge list (src,dst[,weight] per row, '#' comment lines skipped) into a mutable adjacency list and reports the resulting order, size and one edge.
package main
import (
"fmt"
"strings"
"github.com/FlavioCFOliveira/GoGraph/graph/io/csv"
)
func main() {
const data = "# a tiny directed triangle\n" +
"a,b,1\n" +
"b,c,2\n" +
"c,a,3\n"
opts := csv.DefaultOptions()
opts.Directed = true
g, rows, err := csv.ReadInto(strings.NewReader(data), opts)
if err != nil {
panic(err)
}
fmt.Println("rows:", rows)
fmt.Println("order:", g.Order())
fmt.Println("size:", g.Size())
fmt.Println("a->b:", g.HasEdge("a", "b"))
}
Output: rows: 3 order: 3 size: 3 a->b: true
func ReadIntoCtx ¶
func ReadIntoCtx(ctx context.Context, r io.Reader, opts Options) (*adjlist.AdjList[string, int64], int, error)
ReadIntoCtx is the context-aware variant of ReadInto. ctx.Err() is checked every 4096 rows.
On any error — a parse error, context cancellation, or the ErrInputTooLarge cap — the returned graph is nil; the import is all-or-nothing at the in-memory level, so a caller cannot accidentally commit a half-built graph. The typed error (parse error, ctx.Err(), or ErrInputTooLarge) is returned unchanged; only the graph value is discarded.
func Write ¶
Write streams every edge of a in src,dst,weight order to w. Returns the number of rows written.
Example ¶
ExampleWrite shows a CSV round-trip: build a graph, Write it to a buffer, then ReadInto a fresh graph and confirm the edges survived. The serialised row order follows internal NodeID assignment, so the example asserts on edge presence rather than on exact bytes.
package main
import (
"bytes"
"fmt"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/io/csv"
)
func main() {
src := adjlist.New[string, int64](adjlist.Config{Directed: true})
_ = src.AddEdge("a", "b", 1)
_ = src.AddEdge("a", "c", 2)
_ = src.AddEdge("b", "c", 3)
var buf bytes.Buffer
rows, err := csv.Write(&buf, src, csv.DefaultOptions())
if err != nil {
panic(err)
}
readOpts := csv.DefaultOptions()
readOpts.Directed = true
dst, _, err := csv.ReadInto(&buf, readOpts)
if err != nil {
panic(err)
}
fmt.Println("rows written:", rows)
fmt.Println("edges survive:", dst.HasEdge("a", "b") && dst.HasEdge("a", "c") && dst.HasEdge("b", "c"))
}
Output: rows written: 3 edges survive: true
Types ¶
type Options ¶
type Options struct {
// Delimiter is the column separator; defaults to ','.
Delimiter rune
// Comment is the comment character; defaults to '#'.
Comment rune
// HasHeader skips the first line when true.
HasHeader bool
// Directed selects the underlying adjacency-list config.
Directed bool
// Multigraph allows parallel edges.
Multigraph bool
// MaxBytes caps the number of bytes read from the input before the
// reader fails with [ErrInputTooLarge]. [DefaultOptions] sets it to
// [DefaultMaxBytes].
//
// SECURITY: a value of zero or less DISABLES the cap entirely, so the
// reader will consume unbounded input. Because the zero value of an
// Options literal (a bare Options{}) leaves MaxBytes at 0, constructing
// Options by hand for UNTRUSTED input silently opts out of the memory
// bound. Prefer starting from [DefaultOptions] (which sets MaxBytes to
// [DefaultMaxBytes]) and overriding the fields you need, or set an
// explicit positive MaxBytes. Leave the cap disabled only for input you
// fully trust.
MaxBytes int64
// SanitizeFormulae, when true, neutralises spreadsheet formula
// injection (OWASP CSV injection, CWE-1236) on the write path. A cell
// whose first character is one of '=', '+', '-', '@', TAB (0x09), or
// CR (0x0D) is treated as a live formula by Excel, LibreOffice Calc,
// and Google Sheets when the exported file is opened, enabling DDE
// command execution or data exfiltration in the context of the human
// who opens it. With this option set, [Write] and [WriteCtx] prefix
// each such cell with a single apostrophe ('), the de-facto neutraliser
// those spreadsheets honour, so the value is rendered as text.
//
// It is OFF by default to preserve the lossless round-trip: an
// apostrophe-prefixed cell no longer re-imports byte-identically
// through [ReadInto], so a graph written with the default options round
// -trips exactly while one written with sanitisation enabled does not.
// Enable it only when the destination is a spreadsheet and faithful
// re-import is not required. This flag affects the writer only; the
// reader ignores it.
SanitizeFormulae bool
}
Options controls Reader / Writer behaviour.
func DefaultOptions ¶
func DefaultOptions() Options
DefaultOptions returns the minimal config: comma delimiter, '#' comments, directed simple graph, no header, and the DefaultMaxBytes input-size ceiling.