github.com/arnodel/golua@v0.0.0-20230215163904-e0b5347eaaa1/lib/stringlib/format.go (about)

     1  package stringlib
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"math"
     7  	"strconv"
     8  	"unsafe"
     9  
    10  	"github.com/arnodel/golua/lib/base"
    11  	rt "github.com/arnodel/golua/runtime"
    12  )
    13  
    14  func format(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
    15  	if err := c.Check1Arg(); err != nil {
    16  		return nil, err
    17  	}
    18  	f, err := c.StringArg(0)
    19  	if err != nil {
    20  		return nil, err
    21  	}
    22  	s, err := Format(t, f, c.Etc())
    23  	if err != nil {
    24  		return nil, err
    25  	}
    26  	t.RequireBytes(len(s))
    27  	return c.PushingNext1(t.Runtime, rt.StringValue(s)), nil
    28  }
    29  
    30  var errNotEnoughValues = errors.New("not enough values for format string")
    31  
    32  // Format is the base for the implementation of lua string.format()
    33  //
    34  // It works by scanning the verbs in the format string and converting the
    35  // argument corresponding to this verb to the correct type, then calling Go's
    36  // fmt.Sprintf().
    37  //
    38  // It temporarily requires all the memory needed to store the formatted string,
    39  // but releases it before returning so the caller should require memory first
    40  // thing after the call.
    41  func Format(t *rt.Thread, format string, values []rt.Value) (string, error) {
    42  	var tmpMem uint64
    43  	defer t.ReleaseMem(tmpMem)
    44  	// Temporarily require memory for building the argument list
    45  	tmpMem += t.RequireArrSize(unsafe.Sizeof(interface{}(nil)), len(values))
    46  	args := make([]interface{}, len(values))
    47  	j := 0
    48  	// Temporarily require memory for building the format string
    49  	tmpMem += t.RequireBytes(len(format))
    50  	outFormat := []byte(format)
    51  
    52  	// We require an amount of CPU proportional to the format string size
    53  	t.RequireCPU(uint64(len(format)))
    54  OuterLoop:
    55  	for i := 0; i < len(format); i++ {
    56  		if format[i] == '%' {
    57  			var (
    58  				start        = i + 1
    59  				arg          interface{}
    60  				length, prec int
    61  				foundDot     bool
    62  			)
    63  		ArgLoop:
    64  			for i++; i < len(format); i++ {
    65  				switch format[i] {
    66  				case '%':
    67  					continue OuterLoop
    68  				case 'c':
    69  					if len(args) <= j {
    70  						return "", errNotEnoughValues
    71  					}
    72  					n, ok := rt.ToInt(values[j])
    73  					if !ok {
    74  						return "", errors.New("invalid value for integer format")
    75  					}
    76  					arg = []byte{byte(n)}
    77  					tmpMem += t.RequireBytes(1)
    78  					outFormat[i] = 's'
    79  					break ArgLoop
    80  				case 'b', 'd', 'o', 'x', 'X', 'U', 'i', 'u':
    81  					// integer verbs
    82  					if len(args) <= j {
    83  						return "", errNotEnoughValues
    84  					}
    85  					n, ok := rt.ToInt(values[j])
    86  					if !ok {
    87  						return "", errors.New("invalid value for integer format")
    88  					}
    89  					tmpMem += t.RequireBytes(10)
    90  					switch format[i] {
    91  					case 'u':
    92  						// Unsigned int
    93  						arg = uint64(n)
    94  						outFormat[i] = 'd' // No 'u' verb in Go
    95  					case 'i':
    96  						// Signed int
    97  						arg = int64(n)
    98  						outFormat[i] = 'd' // No 'i' verb in Go
    99  					case 'x', 'X':
   100  						arg = uint64(n) // Need to convert to unsigned
   101  					default:
   102  						arg = int64(n)
   103  					}
   104  					break ArgLoop
   105  				case 'a', 'A':
   106  					// Hexadecimal float verbs
   107  					outFormat[i] += 'x' - 'a'
   108  					fallthrough
   109  				case 'e', 'E', 'f', 'F', 'g', 'G':
   110  					// float verbs
   111  					if len(args) <= j {
   112  						return "", errNotEnoughValues
   113  					}
   114  					f, ok := rt.ToFloat(values[j])
   115  					if !ok {
   116  						return "", errors.New("invalid value for float format")
   117  					}
   118  					tmpMem += t.RequireBytes(10)
   119  					arg = float64(f)
   120  					break ArgLoop
   121  				case 's':
   122  					if len(args) <= j {
   123  						return "", errNotEnoughValues
   124  					}
   125  					s, err := base.ToString(t, values[j])
   126  					if err != nil {
   127  						return "", err
   128  					}
   129  					tmpMem += t.RequireBytes(len(s))
   130  					arg = string(s)
   131  					break ArgLoop
   132  				case 'q':
   133  					// quote, only for literals I think
   134  					if start < i {
   135  						return "", errors.New("specifier '%q' cannot have modifiers")
   136  					}
   137  					if len(args) <= j {
   138  						return "", errNotEnoughValues
   139  					}
   140  					v := values[j]
   141  					if s, ok := quote(v); ok {
   142  						tmpMem += t.RequireBytes(len(s))
   143  						arg = s
   144  						outFormat[i] = 's'
   145  					} else {
   146  						return "", errors.New("no literal")
   147  					}
   148  					break ArgLoop
   149  				case 'p':
   150  					// Pointer address, new in Lua 5.4
   151  					switch v := values[j]; v.Type() {
   152  					case rt.BoolType, rt.FloatType, rt.IntType, rt.NilType:
   153  						outFormat[i] = 's'
   154  						arg = "(null)"
   155  					case rt.StringType:
   156  						// Here we have a problem.  C Lua has one single start
   157  						// address per string, whereas in Go a string is a
   158  						// slice, so we can't make the same guarantee.  Best
   159  						// effort: find the address of the start of the string
   160  						// and format it as a pointer.
   161  						outFormat[i] = 's'
   162  						s := v.AsString()
   163  						ptr := *(*uintptr)(unsafe.Pointer(&s))
   164  						arg = fmt.Sprintf("0x%x", ptr)
   165  					default:
   166  						arg = v.Interface()
   167  					}
   168  					break ArgLoop
   169  				case 't':
   170  					// boolean verb
   171  					if len(args) <= j {
   172  						return "", errNotEnoughValues
   173  					}
   174  					b, ok := values[j].TryBool()
   175  					if !ok {
   176  						return "", errors.New("invalid value for boolean format")
   177  					}
   178  					tmpMem += t.RequireBytes(5)
   179  					arg = bool(b)
   180  					break ArgLoop
   181  				case '.':
   182  					foundDot = true
   183  				case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
   184  					if foundDot {
   185  						prec = prec*10 + int(format[i]-'0')
   186  						if prec >= 100 {
   187  							return "", errors.New("length too long")
   188  						}
   189  					} else {
   190  						length = length*10 + int(format[i]-'0')
   191  						if length >= 100 {
   192  							return "", errors.New("precision too long")
   193  						}
   194  					}
   195  				case '+', '-', '#', ' ':
   196  					// flag characters
   197  				default:
   198  					// Unrecognised verbs
   199  					return "", errors.New("invalid format string")
   200  				}
   201  			}
   202  			args[j] = arg
   203  			j++
   204  		}
   205  	}
   206  	if j < len(args) {
   207  		args = args[:j]
   208  	}
   209  
   210  	// Release temporary memory
   211  	return fmt.Sprintf(string(outFormat), args...), nil
   212  }
   213  
   214  // Quote returns a string representing the value as a valid Lua literal if
   215  // possible, the boolean returned indicating success or failure.
   216  func quote(v rt.Value) (string, bool) {
   217  	if v.IsNil() {
   218  		return "nil", true
   219  	}
   220  	switch v.Type() {
   221  	case rt.IntType:
   222  		return strconv.Itoa(int(v.AsInt())), true
   223  	case rt.FloatType:
   224  		x := v.AsFloat()
   225  		if math.IsInf(x, 0) {
   226  			// Use an out of range literal to represent infinity.  It works
   227  			// because that parses to infinity
   228  			if x > 0 {
   229  				return "1e9999", true
   230  			} else {
   231  				return "-1e9999", true
   232  			}
   233  		}
   234  		if math.IsNaN(x) {
   235  			return "(0/0)", true
   236  		}
   237  		return strconv.FormatFloat(x, 'g', -1, 64), true
   238  	case rt.BoolType:
   239  		return strconv.FormatBool(v.AsBool()), true
   240  	case rt.StringType:
   241  		return strconv.Quote(v.AsString()), true // An approximation
   242  	default:
   243  		return "", false
   244  	}
   245  }