Skip to content

Commit 8c884b4

Browse files
committed
fix(client): fix escaping and validation
1 parent 283e7b6 commit 8c884b4

File tree

6 files changed

+1383
-214
lines changed

6 files changed

+1383
-214
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Features:
88
* Context-aware API.
99
* Optimized for batch writes.
1010
* Supports TLS encryption and [ILP authentication](https://questdb.io/docs/reference/api/ilp/authenticate).
11-
* Tested against QuestDB 6.4 and newer versions.
11+
* Tested against QuestDB 6.4.1 and newer versions.
1212

1313
Documentation is available [here](https://pkg.go.dev/github.com/questdb/go-questdb-client).
1414

sender.go

Lines changed: 174 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ type LineSender struct {
7272
lastMsgPos int
7373
lastErr error
7474
hasTable bool
75+
hasTags bool
7576
hasFields bool
7677
}
7778

@@ -232,8 +233,8 @@ func (s *LineSender) Close() error {
232233
// called before any Symbol or Column method.
233234
//
234235
// Table name cannot contain any of the following characters:
235-
// '\n', '\r', '.', '?', ',', ':', '\', '/', '\0', ')', '(', '+', '*',
236-
// '~', '%', '-'.
236+
// '\n', '\r', '?', ',', ''', '"', '\', '/', ':', ')', '(', '+', '*',
237+
// '%', '~', starting '.', trailing '.', or a non-printable char.
237238
func (s *LineSender) Table(name string) *LineSender {
238239
if s.lastErr != nil {
239240
return s
@@ -242,7 +243,7 @@ func (s *LineSender) Table(name string) *LineSender {
242243
s.lastErr = fmt.Errorf("table name already provided: %w", ErrInvalidMsg)
243244
return s
244245
}
245-
s.lastErr = s.writeStrName(name)
246+
s.lastErr = s.writeTableName(name)
246247
if s.lastErr != nil {
247248
return s
248249
}
@@ -254,11 +255,8 @@ func (s *LineSender) Table(name string) *LineSender {
254255
// before any Column method.
255256
//
256257
// Symbol name cannot contain any of the following characters:
257-
// '\n', '\r', '.', '?', ',', ':', '\', '/', '\0', ')', '(', '+', '*',
258-
// '~', '%', '-'.
259-
//
260-
// Symbol values cannot contain any of the following characters:
261-
// '\n', '\r'.
258+
// '\n', '\r', '?', '.', ',', ''', '\"', '\\', '/', ':', ')', '(', '+',
259+
// '-', '*' '%%', '~', or a non-printable char.
262260
func (s *LineSender) Symbol(name, val string) *LineSender {
263261
if s.lastErr != nil {
264262
return s
@@ -272,26 +270,30 @@ func (s *LineSender) Symbol(name, val string) *LineSender {
272270
return s
273271
}
274272
s.buf.WriteByte(',')
275-
s.lastErr = s.writeStrName(name)
273+
s.lastErr = s.writeColumnName(name)
276274
if s.lastErr != nil {
277275
return s
278276
}
279277
s.buf.WriteByte('=')
280278
s.lastErr = s.writeStrValue(val, false)
279+
if s.lastErr != nil {
280+
return s
281+
}
282+
s.hasTags = true
281283
return s
282284
}
283285

284286
// Int64Column adds a 64-bit integer (long) column value to the ILP
285287
// message.
286288
//
287289
// Column name cannot contain any of the following characters:
288-
// '\n', '\r', '.', '?', ',', ':', '\', '/', '\0', ')', '(', '+', '*',
289-
// '~', '%', '-'.
290+
// '\n', '\r', '?', '.', ',', ''', '\"', '\\', '/', ':', ')', '(', '+',
291+
// '-', '*' '%%', '~', or a non-printable char.
290292
func (s *LineSender) Int64Column(name string, val int64) *LineSender {
291293
if !s.prepareForField(name) {
292294
return s
293295
}
294-
s.lastErr = s.writeStrName(name)
296+
s.lastErr = s.writeColumnName(name)
295297
if s.lastErr != nil {
296298
return s
297299
}
@@ -306,13 +308,13 @@ func (s *LineSender) Int64Column(name string, val int64) *LineSender {
306308
// message.
307309
//
308310
// Column name cannot contain any of the following characters:
309-
// '\n', '\r', '.', '?', ',', ':', '\', '/', '\0', ')', '(', '+', '*',
310-
// '~', '%', '-'.
311+
// '\n', '\r', '?', '.', ',', ''', '\"', '\\', '/', ':', ')', '(', '+',
312+
// '-', '*' '%%', '~', or a non-printable char.
311313
func (s *LineSender) Float64Column(name string, val float64) *LineSender {
312314
if !s.prepareForField(name) {
313315
return s
314316
}
315-
s.lastErr = s.writeStrName(name)
317+
s.lastErr = s.writeColumnName(name)
316318
if s.lastErr != nil {
317319
return s
318320
}
@@ -325,16 +327,13 @@ func (s *LineSender) Float64Column(name string, val float64) *LineSender {
325327
// StringColumn adds a string column value to the ILP message.
326328
//
327329
// Column name cannot contain any of the following characters:
328-
// '\n', '\r', '.', '?', ',', ':', '\\', '/', '\0', ')', '(', '+', '*',
329-
// '~', '%', '-'.
330-
//
331-
// Column values cannot contain any of the following characters:
332-
// '\n', '\r'.
330+
// '\n', '\r', '?', '.', ',', ''', '\"', '\\', '/', ':', ')', '(', '+',
331+
// '-', '*' '%%', '~', or a non-printable char.
333332
func (s *LineSender) StringColumn(name, val string) *LineSender {
334333
if !s.prepareForField(name) {
335334
return s
336335
}
337-
s.lastErr = s.writeStrName(name)
336+
s.lastErr = s.writeColumnName(name)
338337
if s.lastErr != nil {
339338
return s
340339
}
@@ -352,13 +351,13 @@ func (s *LineSender) StringColumn(name, val string) *LineSender {
352351
// BoolColumn adds a boolean column value to the ILP message.
353352
//
354353
// Column name cannot contain any of the following characters:
355-
// '\n', '\r', '.', '?', ',', ':', '\', '/', '\0', ')', '(', '+', '*',
356-
// '~', '%', '-'.
354+
// '\n', '\r', '?', '.', ',', ''', '\"', '\\', '/', ':', ')', '(', '+',
355+
// '-', '*' '%%', '~', or a non-printable char.
357356
func (s *LineSender) BoolColumn(name string, val bool) *LineSender {
358357
if !s.prepareForField(name) {
359358
return s
360359
}
361-
s.lastErr = s.writeStrName(name)
360+
s.lastErr = s.writeColumnName(name)
362361
if s.lastErr != nil {
363362
return s
364363
}
@@ -372,9 +371,9 @@ func (s *LineSender) BoolColumn(name string, val bool) *LineSender {
372371
return s
373372
}
374373

375-
func (s *LineSender) writeStrName(str string) error {
374+
func (s *LineSender) writeTableName(str string) error {
376375
if str == "" {
377-
return fmt.Errorf("table or column name cannot be empty: %w", ErrInvalidMsg)
376+
return fmt.Errorf("table name cannot be empty: %w", ErrInvalidMsg)
378377
}
379378
// Since we're interested in ASCII chars, it's fine to iterate
380379
// through bytes instead of runes.
@@ -385,16 +384,14 @@ func (s *LineSender) writeStrName(str string) error {
385384
s.buf.WriteByte('\\')
386385
case '=':
387386
s.buf.WriteByte('\\')
388-
case '"':
389-
s.buf.WriteByte('\\')
390-
case '\n':
391-
return fmt.Errorf("table or column name contains a new line char: %s: %w", str, ErrInvalidMsg)
392-
case '\r':
393-
return fmt.Errorf("table or column name contains a carriage return char: %s: %w", str, ErrInvalidMsg)
387+
case '.':
388+
if i == 0 || i == len(str)-1 {
389+
return fmt.Errorf("table name contains '.' char at the start or end: %s: %w", str, ErrInvalidMsg)
390+
}
394391
default:
395-
if illegalNameChar(b) {
396-
return fmt.Errorf("table or column name contains one of illegal chars: "+
397-
"'.', '?', ',', ':', '\\', '/', '\\0', ')', '(', '+', '*', '~', '%%', '-': %s: %w",
392+
if illegalTableNameChar(b) {
393+
return fmt.Errorf("table name contains an illegal char: "+
394+
"'\\n', '\\r', '?', ',', ''', '\"', '\\', '/', ':', ')', '(', '+', '*' '%%', '~', or a non-printable char: %s: %w",
398395
str, ErrInvalidMsg)
399396
}
400397
}
@@ -403,20 +400,26 @@ func (s *LineSender) writeStrName(str string) error {
403400
return nil
404401
}
405402

406-
func illegalNameChar(ch byte) bool {
403+
func illegalTableNameChar(ch byte) bool {
407404
switch ch {
408-
case '.':
405+
case '\n':
406+
return true
407+
case '\r':
409408
return true
410409
case '?':
411410
return true
412411
case ',':
413412
return true
414-
case ':':
413+
case '\'':
414+
return true
415+
case '"':
415416
return true
416417
case '\\':
417418
return true
418419
case '/':
419420
return true
421+
case ':':
422+
return true
420423
case ')':
421424
return true
422425
case '(':
@@ -425,13 +428,134 @@ func illegalNameChar(ch byte) bool {
425428
return true
426429
case '*':
427430
return true
431+
case '%':
432+
return true
428433
case '~':
429434
return true
430-
case '%':
435+
case '\u0000':
436+
return true
437+
case '\u0001':
438+
return true
439+
case '\u0002':
440+
return true
441+
case '\u0003':
442+
return true
443+
case '\u0004':
444+
return true
445+
case '\u0005':
446+
return true
447+
case '\u0006':
448+
return true
449+
case '\u0007':
450+
return true
451+
case '\u0008':
452+
return true
453+
case '\u0009':
454+
return true
455+
case '\u000b':
456+
return true
457+
case '\u000c':
458+
return true
459+
case '\u000e':
460+
return true
461+
case '\u000f':
462+
return true
463+
case '\u007f':
464+
return true
465+
}
466+
return false
467+
}
468+
469+
func (s *LineSender) writeColumnName(str string) error {
470+
if str == "" {
471+
return fmt.Errorf("column name cannot be empty: %w", ErrInvalidMsg)
472+
}
473+
// Since we're interested in ASCII chars, it's fine to iterate
474+
// through bytes instead of runes.
475+
for i := 0; i < len(str); i++ {
476+
b := str[i]
477+
switch b {
478+
case ' ':
479+
s.buf.WriteByte('\\')
480+
case '=':
481+
s.buf.WriteByte('\\')
482+
default:
483+
if illegalColumnNameChar(b) {
484+
return fmt.Errorf("column name contains an illegal char: "+
485+
"'\\n', '\\r', '?', '.', ',', ''', '\"', '\\', '/', ':', ')', '(', '+', '-', '*' '%%', '~', or a non-printable char: %s: %w",
486+
str, ErrInvalidMsg)
487+
}
488+
}
489+
s.buf.WriteByte(b)
490+
}
491+
return nil
492+
}
493+
494+
func illegalColumnNameChar(ch byte) bool {
495+
switch ch {
496+
case '\n':
497+
return true
498+
case '\r':
499+
return true
500+
case '?':
501+
return true
502+
case '.':
503+
return true
504+
case ',':
505+
return true
506+
case '\'':
507+
return true
508+
case '"':
509+
return true
510+
case '\\':
511+
return true
512+
case '/':
513+
return true
514+
case ':':
515+
return true
516+
case ')':
517+
return true
518+
case '(':
519+
return true
520+
case '+':
431521
return true
432522
case '-':
433523
return true
434-
case '\x00':
524+
case '*':
525+
return true
526+
case '%':
527+
return true
528+
case '~':
529+
return true
530+
case '\u0000':
531+
return true
532+
case '\u0001':
533+
return true
534+
case '\u0002':
535+
return true
536+
case '\u0003':
537+
return true
538+
case '\u0004':
539+
return true
540+
case '\u0005':
541+
return true
542+
case '\u0006':
543+
return true
544+
case '\u0007':
545+
return true
546+
case '\u0008':
547+
return true
548+
case '\u0009':
549+
return true
550+
case '\u000b':
551+
return true
552+
case '\u000c':
553+
return true
554+
case '\u000e':
555+
return true
556+
case '\u000f':
557+
return true
558+
case '\u007f':
435559
return true
436560
}
437561
return false
@@ -456,13 +580,15 @@ func (s *LineSender) writeStrValue(str string, quoted bool) error {
456580
s.buf.WriteByte('\\')
457581
}
458582
case '"':
583+
if quoted {
584+
s.buf.WriteByte('\\')
585+
}
586+
case '\n':
587+
s.buf.WriteByte('\\')
588+
case '\r':
459589
s.buf.WriteByte('\\')
460590
case '\\':
461591
s.buf.WriteByte('\\')
462-
case '\n':
463-
return fmt.Errorf("symbol or string column value contains a new line char: %s: %w", str, ErrInvalidMsg)
464-
case '\r':
465-
return fmt.Errorf("symbol or string column value contains a carriage return char: %s: %w", str, ErrInvalidMsg)
466592
}
467593
s.buf.WriteByte(b)
468594
}
@@ -512,6 +638,9 @@ func (s *LineSender) At(ctx context.Context, ts int64) error {
512638
if !s.hasTable {
513639
return fmt.Errorf("table name was not provided: %w", ErrInvalidMsg)
514640
}
641+
if !s.hasTags && !s.hasFields {
642+
return fmt.Errorf("no symbols or columns were provided: %w", ErrInvalidMsg)
643+
}
515644

516645
if ts > -1 {
517646
s.buf.WriteByte(' ')
@@ -521,6 +650,7 @@ func (s *LineSender) At(ctx context.Context, ts int64) error {
521650

522651
s.lastMsgPos = s.buf.Len()
523652
s.hasTable = false
653+
s.hasTags = false
524654
s.hasFields = false
525655

526656
if s.buf.Len() > s.bufCap {

0 commit comments

Comments
 (0)