github.com/inspektor-gadget/inspektor-gadget@v0.28.1/pkg/columns/formatter/json/json.go (about)

     1  // Copyright 2023 The Inspektor Gadget authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package json
    16  
    17  import (
    18  	"encoding/json"
    19  	"fmt"
    20  	"math"
    21  	"reflect"
    22  	"strconv"
    23  	"strings"
    24  	"unicode/utf8"
    25  	_ "unsafe"
    26  
    27  	"github.com/inspektor-gadget/inspektor-gadget/pkg/columns"
    28  )
    29  
    30  type column[T any] struct {
    31  	column    *columns.Column[T]
    32  	formatter func(*encodeState, *T)
    33  }
    34  
    35  type Formatter[T any] struct {
    36  	options *Options
    37  	columns []*column[T]
    38  	printer func(buf *encodeState, entry *T, indent string)
    39  }
    40  
    41  // NewFormatter returns a Formatter that will turn entries of type T into JSON representation
    42  func NewFormatter[T any](cols columns.ColumnMap[T], options ...Option) *Formatter[T] {
    43  	opts := DefaultOptions()
    44  	for _, o := range options {
    45  		o(opts)
    46  	}
    47  
    48  	ncols := make([]*column[T], 0)
    49  	for _, col := range cols.GetOrderedColumns() {
    50  		colName := col.Name
    51  		if strings.Contains(colName, ".") {
    52  			hierarchy := strings.Split(colName, ".")
    53  			colName = hierarchy[len(hierarchy)-1]
    54  		}
    55  		name, _ := json.Marshal(colName)
    56  		key := append(name, []byte(": ")...)
    57  
    58  		var formatter func(*encodeState, *T)
    59  		switch col.Kind() {
    60  		default:
    61  			continue
    62  		case reflect.Int,
    63  			reflect.Int8,
    64  			reflect.Int16,
    65  			reflect.Int32,
    66  			reflect.Int64:
    67  			ff := columns.GetFieldAsNumberFunc[int64, T](col)
    68  			formatter = func(e *encodeState, t *T) {
    69  				e.Write(key)
    70  				b := strconv.AppendInt(e.scratch[:0], ff(t), 10)
    71  				e.Write(b)
    72  			}
    73  		case reflect.Uint,
    74  			reflect.Uint8,
    75  			reflect.Uint16,
    76  			reflect.Uint32,
    77  			reflect.Uint64:
    78  			ff := columns.GetFieldAsNumberFunc[uint64, T](col)
    79  			formatter = func(e *encodeState, t *T) {
    80  				e.Write(key)
    81  				b := strconv.AppendUint(e.scratch[:0], ff(t), 10)
    82  				e.Write(b)
    83  			}
    84  		case reflect.Bool:
    85  			ff := columns.GetFieldFunc[bool, T](col)
    86  			formatter = func(e *encodeState, t *T) {
    87  				e.Write(key)
    88  				if ff(t) {
    89  					e.WriteString("true")
    90  					return
    91  				}
    92  				e.WriteString("false")
    93  			}
    94  		case reflect.Float32:
    95  			ff := columns.GetFieldAsNumberFunc[float64, T](col)
    96  			formatter = func(e *encodeState, t *T) {
    97  				e.Write(key)
    98  				floatEncoder(32).writeFloat(e, ff(t))
    99  			}
   100  		case reflect.Float64:
   101  			ff := columns.GetFieldAsNumberFunc[float64, T](col)
   102  			formatter = func(e *encodeState, t *T) {
   103  				e.Write(key)
   104  				floatEncoder(64).writeFloat(e, ff(t))
   105  			}
   106  		case reflect.Array:
   107  			ff := columns.GetFieldAsString[T](col)
   108  			formatter = func(e *encodeState, t *T) {
   109  				e.Write(key)
   110  				writeString(e, ff(t))
   111  			}
   112  		case reflect.String:
   113  			ff := columns.GetFieldFunc[string, T](col)
   114  			formatter = func(e *encodeState, t *T) {
   115  				e.Write(key)
   116  				writeString(e, ff(t))
   117  			}
   118  		}
   119  
   120  		ncols = append(ncols, &column[T]{
   121  			column:    col,
   122  			formatter: formatter,
   123  		})
   124  	}
   125  
   126  	tf := &Formatter[T]{
   127  		options: opts,
   128  		columns: ncols,
   129  	}
   130  	tf.printer = tf.getPrinter(0, ncols)
   131  	return tf
   132  }
   133  
   134  func (f *Formatter[T]) getPrinter(level int, cols []*column[T]) func(buf *encodeState, entry *T, indent string) {
   135  	levelIndent := []byte("  ")
   136  	colsWithParent := map[string][]*column[T]{}
   137  	colsWithoutParent := []*column[T]{}
   138  
   139  	funcs := make([]func(buf *encodeState, entry *T, indent string), 0)
   140  
   141  	for _, col := range cols {
   142  		// Save columns which have a parent in its name
   143  		if strings.Count(col.column.Name, ".") > level {
   144  			hierarchy := strings.Split(col.column.Name, ".")
   145  			parentName := hierarchy[level]
   146  			colsWithParent[parentName] = append(colsWithParent[parentName], col)
   147  			continue
   148  		}
   149  		// Don't print columns without a parent directly
   150  		// It is possible that their name is the same as a name of an object in the same level
   151  		// For example: {"a": {"bbb": 1}, "a": 2}
   152  		// We will filter them out after we gathered all object names
   153  		colsWithoutParent = append(colsWithoutParent, col)
   154  	}
   155  
   156  	first := true
   157  	// Better name?
   158  	handlePrefix := func() {
   159  		if !first {
   160  			funcs = append(funcs, func(buf *encodeState, entry *T, indent string) {
   161  				buf.WriteByte(',')
   162  				if f.options.prettyPrint {
   163  					buf.WriteByte('\n')
   164  					buf.WriteString(indent)
   165  					buf.Write(levelIndent)
   166  				} else {
   167  					buf.WriteByte(' ')
   168  				}
   169  			})
   170  		} else {
   171  			if f.options.prettyPrint {
   172  				funcs = append(funcs, func(buf *encodeState, entry *T, indent string) {
   173  					buf.WriteByte('\n')
   174  					buf.WriteString(indent)
   175  					buf.Write(levelIndent)
   176  				})
   177  			}
   178  			first = false
   179  		}
   180  	}
   181  
   182  	// Now print columns with their parents
   183  	for key, val := range colsWithParent {
   184  		handlePrefix()
   185  
   186  		parentName, _ := json.Marshal(strings.Split(key, ".")[0])
   187  
   188  		childFuncs := f.getPrinter(level+1, val)
   189  		funcs = append(funcs, func(buf *encodeState, entry *T, indent string) {
   190  			buf.Write(parentName)
   191  			buf.WriteString(": {")
   192  
   193  			childFuncs(buf, entry, indent+"  ")
   194  
   195  			// End parent
   196  			if f.options.prettyPrint {
   197  				buf.WriteString("\n" + indent + "  }")
   198  			} else {
   199  				buf.WriteByte('}')
   200  			}
   201  		})
   202  	}
   203  
   204  	// Now all cols without a parent
   205  	for i := range colsWithoutParent {
   206  		col := colsWithoutParent[i]
   207  		childName := strings.Split(col.column.Name, ".")[level]
   208  		_, found := colsWithParent[childName]
   209  		if found {
   210  			// We already have an object with this key, skip this column
   211  			continue
   212  		}
   213  		handlePrefix()
   214  
   215  		funcs = append(funcs, func(buf *encodeState, entry *T, indent string) {
   216  			col.formatter(buf, entry)
   217  		})
   218  	}
   219  
   220  	return func(buf *encodeState, entry *T, indent string) {
   221  		for _, f := range funcs {
   222  			f(buf, entry, indent)
   223  		}
   224  	}
   225  }
   226  
   227  func (f *Formatter[T]) formatEntry(buf *encodeState, entry *T, indent string) {
   228  	if entry == nil {
   229  		buf.WriteString(indent + "null")
   230  		return
   231  	}
   232  	buf.WriteString(indent + "{")
   233  	f.printer(buf, entry, indent)
   234  	if f.options.prettyPrint {
   235  		buf.WriteString("\n" + indent)
   236  	}
   237  	buf.WriteByte('}')
   238  }
   239  
   240  // FormatEntry returns an entry as a formatted string, respecting the given formatting settings
   241  func (f *Formatter[T]) FormatEntry(entry *T) string {
   242  	buf := bufpool.Get().(*encodeState)
   243  	buf.Reset()
   244  	defer bufpool.Put(buf)
   245  
   246  	f.formatEntry(buf, entry, "")
   247  
   248  	return buf.String()
   249  }
   250  
   251  // FormatEntries returns a slice of entries as a formatted string, respecting the given formatting settings
   252  func (f *Formatter[T]) FormatEntries(entries []*T) string {
   253  	if entries == nil {
   254  		return "null"
   255  	}
   256  
   257  	// TODO[Mauricio]: I can't remember why but the behavior of the default golang formatter was
   258  	// causing issues when the slice was empty. So let's marshall to [] instead of null on this case.
   259  	if len(entries) == 0 {
   260  		return "[]"
   261  	}
   262  
   263  	buf := bufpool.Get().(*encodeState)
   264  	buf.Reset()
   265  	defer bufpool.Put(buf)
   266  
   267  	if !f.options.prettyPrint {
   268  		buf.WriteByte('[')
   269  		for l, entry := range entries {
   270  			f.formatEntry(buf, entry, "")
   271  			if l < len(entries)-1 {
   272  				buf.WriteString(", ")
   273  			}
   274  		}
   275  		buf.WriteByte(']')
   276  	} else {
   277  		buf.WriteString("[\n")
   278  		for l, entry := range entries {
   279  			f.formatEntry(buf, entry, "  ")
   280  			if l < len(entries)-1 {
   281  				buf.WriteString(",\n")
   282  			}
   283  		}
   284  		buf.WriteString("\n]")
   285  	}
   286  
   287  	return buf.String()
   288  }
   289  
   290  type floatEncoder int // number of bits
   291  
   292  // from encoding/json/encode.go
   293  func (bits floatEncoder) writeFloat(e *encodeState, f float64) {
   294  	if math.IsInf(f, 0) || math.IsNaN(f) {
   295  		e.err = fmt.Errorf("invalid float value")
   296  		return
   297  	}
   298  
   299  	// Convert as if by ES6 number to string conversion.
   300  	// This matches most other JSON generators.
   301  	// See golang.org/issue/6384 and golang.org/issue/14135.
   302  	// Like fmt %g, but the exponent cutoffs are different
   303  	// and exponents themselves are not padded to two digits.
   304  	b := e.scratch[:0]
   305  	abs := math.Abs(f)
   306  	fmt := byte('f')
   307  	// Note: Must use float32 comparisons for underlying float32 value to get precise cutoffs right.
   308  	if abs != 0 {
   309  		if bits == 64 && (abs < 1e-6 || abs >= 1e21) || bits == 32 && (float32(abs) < 1e-6 || float32(abs) >= 1e21) {
   310  			fmt = 'e'
   311  		}
   312  	}
   313  	b = strconv.AppendFloat(b, f, fmt, -1, int(bits))
   314  	if fmt == 'e' {
   315  		// clean up e-09 to e-9
   316  		n := len(b)
   317  		if n >= 4 && b[n-4] == 'e' && b[n-3] == '-' && b[n-2] == '0' {
   318  			b[n-2] = b[n-1]
   319  			b = b[:n-1]
   320  		}
   321  	}
   322  
   323  	e.Write(b)
   324  }
   325  
   326  // from encoding/json/encode.go
   327  func writeString(e *encodeState, s string) {
   328  	e.WriteByte('"')
   329  	start := 0
   330  	for i := 0; i < len(s); {
   331  		if b := s[i]; b < utf8.RuneSelf {
   332  			if safeSet[b] {
   333  				i++
   334  				continue
   335  			}
   336  			if start < i {
   337  				e.WriteString(s[start:i])
   338  			}
   339  			e.WriteByte('\\')
   340  			switch b {
   341  			case '\\', '"':
   342  				e.WriteByte(b)
   343  			case '\n':
   344  				e.WriteByte('n')
   345  			case '\r':
   346  				e.WriteByte('r')
   347  			case '\t':
   348  				e.WriteByte('t')
   349  			default:
   350  				// This encodes bytes < 0x20 except for \t, \n and \r.
   351  				// If escapeHTML is set, it also escapes <, >, and &
   352  				// because they can lead to security holes when
   353  				// user-controlled strings are rendered into JSON
   354  				// and served to some browsers.
   355  				e.WriteString(`u00`)
   356  				e.WriteByte(hex[b>>4])
   357  				e.WriteByte(hex[b&0xF])
   358  			}
   359  			i++
   360  			start = i
   361  			continue
   362  		}
   363  		c, size := utf8.DecodeRuneInString(s[i:])
   364  		if c == utf8.RuneError && size == 1 {
   365  			if start < i {
   366  				e.WriteString(s[start:i])
   367  			}
   368  			e.WriteString(`\ufffd`)
   369  			i += size
   370  			start = i
   371  			continue
   372  		}
   373  		// U+2028 is LINE SEPARATOR.
   374  		// U+2029 is PARAGRAPH SEPARATOR.
   375  		// They are both technically valid characters in JSON strings,
   376  		// but don't work in JSONP, which has to be evaluated as JavaScript,
   377  		// and can lead to security holes there. It is valid JSON to
   378  		// escape them, so we do so unconditionally.
   379  		// See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion.
   380  		if c == '\u2028' || c == '\u2029' {
   381  			if start < i {
   382  				e.WriteString(s[start:i])
   383  			}
   384  			e.WriteString(`\u202`)
   385  			e.WriteByte(hex[c&0xF])
   386  			i += size
   387  			start = i
   388  			continue
   389  		}
   390  		i += size
   391  	}
   392  	if start < len(s) {
   393  		e.WriteString(s[start:])
   394  	}
   395  	e.WriteByte('"')
   396  }
   397  
   398  var hex = "0123456789abcdef"
   399  
   400  // use safeSet from encoding/json directly
   401  //
   402  //go:linkname safeSet encoding/json.safeSet
   403  var safeSet = [utf8.RuneSelf]bool{}