wa-lang.org/wazero@v1.0.2/internal/sys/sys.go (about)

     1  package sys
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"io/fs"
     9  	"time"
    10  
    11  	"wa-lang.org/wazero/internal/platform"
    12  	"wa-lang.org/wazero/sys"
    13  )
    14  
    15  // Context holds module-scoped system resources currently only supported by
    16  // built-in host functions.
    17  type Context struct {
    18  	args, environ         []string
    19  	argsSize, environSize uint32
    20  	stdin                 io.Reader
    21  	stdout, stderr        io.Writer
    22  
    23  	// Note: Using function pointers here keeps them stable for tests.
    24  
    25  	walltime           *sys.Walltime
    26  	walltimeResolution sys.ClockResolution
    27  	nanotime           *sys.Nanotime
    28  	nanotimeResolution sys.ClockResolution
    29  	nanosleep          *sys.Nanosleep
    30  	randSource         io.Reader
    31  	fsc                *FSContext
    32  }
    33  
    34  // Args is like os.Args and defaults to nil.
    35  //
    36  // Note: The count will never be more than math.MaxUint32.
    37  // See wazero.ModuleConfig WithArgs
    38  func (c *Context) Args() []string {
    39  	return c.args
    40  }
    41  
    42  // ArgsSize is the size to encode Args as Null-terminated strings.
    43  //
    44  // Note: To get the size without null-terminators, subtract the length of Args from this value.
    45  // See wazero.ModuleConfig WithArgs
    46  // See https://en.wikipedia.org/wiki/Null-terminated_string
    47  func (c *Context) ArgsSize() uint32 {
    48  	return c.argsSize
    49  }
    50  
    51  // Environ are "key=value" entries like os.Environ and default to nil.
    52  //
    53  // Note: The count will never be more than math.MaxUint32.
    54  // See wazero.ModuleConfig WithEnv
    55  func (c *Context) Environ() []string {
    56  	return c.environ
    57  }
    58  
    59  // EnvironSize is the size to encode Environ as Null-terminated strings.
    60  //
    61  // Note: To get the size without null-terminators, subtract the length of Environ from this value.
    62  // See wazero.ModuleConfig WithEnv
    63  // See https://en.wikipedia.org/wiki/Null-terminated_string
    64  func (c *Context) EnvironSize() uint32 {
    65  	return c.environSize
    66  }
    67  
    68  // Stdin is like exec.Cmd Stdin and defaults to a reader of os.DevNull.
    69  // See wazero.ModuleConfig WithStdin
    70  func (c *Context) Stdin() io.Reader {
    71  	return c.stdin
    72  }
    73  
    74  // Stdout is like exec.Cmd Stdout and defaults to io.Discard.
    75  // See wazero.ModuleConfig WithStdout
    76  func (c *Context) Stdout() io.Writer {
    77  	return c.stdout
    78  }
    79  
    80  // Stderr is like exec.Cmd Stderr and defaults to io.Discard.
    81  // See wazero.ModuleConfig WithStderr
    82  func (c *Context) Stderr() io.Writer {
    83  	return c.stderr
    84  }
    85  
    86  // Walltime implements sys.Walltime.
    87  func (c *Context) Walltime(ctx context.Context) (sec int64, nsec int32) {
    88  	return (*(c.walltime))(ctx)
    89  }
    90  
    91  // WalltimeResolution returns resolution of Walltime.
    92  func (c *Context) WalltimeResolution() sys.ClockResolution {
    93  	return c.walltimeResolution
    94  }
    95  
    96  // Nanotime implements sys.Nanotime.
    97  func (c *Context) Nanotime(ctx context.Context) int64 {
    98  	return (*(c.nanotime))(ctx)
    99  }
   100  
   101  // NanotimeResolution returns resolution of Nanotime.
   102  func (c *Context) NanotimeResolution() sys.ClockResolution {
   103  	return c.nanotimeResolution
   104  }
   105  
   106  // Nanosleep implements sys.Nanosleep.
   107  func (c *Context) Nanosleep(ctx context.Context, ns int64) {
   108  	(*(c.nanosleep))(ctx, ns)
   109  }
   110  
   111  // FS returns a possibly nil file system context.
   112  func (c *Context) FS(ctx context.Context) *FSContext {
   113  	// Override Context when it is passed via context
   114  	if fsValue := ctx.Value(FSKey{}); fsValue != nil {
   115  		return fsValue.(*FSContext)
   116  	}
   117  	return c.fsc
   118  }
   119  
   120  // RandSource is a source of random bytes and defaults to a deterministic source.
   121  // see wazero.ModuleConfig WithRandSource
   122  func (c *Context) RandSource() io.Reader {
   123  	return c.randSource
   124  }
   125  
   126  // eofReader is safer than reading from os.DevNull as it can never overrun operating system file descriptors.
   127  type eofReader struct{}
   128  
   129  // Read implements io.Reader
   130  // Note: This doesn't use a pointer reference as it has no state and an empty struct doesn't allocate.
   131  func (eofReader) Read([]byte) (int, error) {
   132  	return 0, io.EOF
   133  }
   134  
   135  // DefaultContext returns Context with no values set except a possibly nil fs.FS
   136  func DefaultContext(fs fs.FS) *Context {
   137  	if sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil, fs); err != nil {
   138  		panic(fmt.Errorf("BUG: DefaultContext should never error: %w", err))
   139  	} else {
   140  		return sysCtx
   141  	}
   142  }
   143  
   144  var (
   145  	_                = DefaultContext(nil) // Force panic on bug.
   146  	ns sys.Nanosleep = platform.FakeNanosleep
   147  )
   148  
   149  // NewContext is a factory function which helps avoid needing to know defaults or exporting all fields.
   150  // Note: max is exposed for testing. max is only used for env/args validation.
   151  func NewContext(
   152  	max uint32,
   153  	args, environ []string,
   154  	stdin io.Reader,
   155  	stdout, stderr io.Writer,
   156  	randSource io.Reader,
   157  	walltime *sys.Walltime,
   158  	walltimeResolution sys.ClockResolution,
   159  	nanotime *sys.Nanotime,
   160  	nanotimeResolution sys.ClockResolution,
   161  	nanosleep *sys.Nanosleep,
   162  	fs fs.FS,
   163  ) (sysCtx *Context, err error) {
   164  	sysCtx = &Context{args: args, environ: environ}
   165  
   166  	if sysCtx.argsSize, err = nullTerminatedByteCount(max, args); err != nil {
   167  		return nil, fmt.Errorf("args invalid: %w", err)
   168  	}
   169  
   170  	if sysCtx.environSize, err = nullTerminatedByteCount(max, environ); err != nil {
   171  		return nil, fmt.Errorf("environ invalid: %w", err)
   172  	}
   173  
   174  	if stdin == nil {
   175  		sysCtx.stdin = eofReader{}
   176  	} else {
   177  		sysCtx.stdin = stdin
   178  	}
   179  
   180  	if stdout == nil {
   181  		sysCtx.stdout = io.Discard
   182  	} else {
   183  		sysCtx.stdout = stdout
   184  	}
   185  
   186  	if stderr == nil {
   187  		sysCtx.stderr = io.Discard
   188  	} else {
   189  		sysCtx.stderr = stderr
   190  	}
   191  
   192  	if randSource == nil {
   193  		sysCtx.randSource = platform.NewFakeRandSource()
   194  	} else {
   195  		sysCtx.randSource = randSource
   196  	}
   197  
   198  	if walltime != nil {
   199  		if clockResolutionInvalid(walltimeResolution) {
   200  			return nil, fmt.Errorf("invalid Walltime resolution: %d", walltimeResolution)
   201  		}
   202  		sysCtx.walltime = walltime
   203  		sysCtx.walltimeResolution = walltimeResolution
   204  	} else {
   205  		sysCtx.walltime = platform.NewFakeWalltime()
   206  		sysCtx.walltimeResolution = sys.ClockResolution(time.Microsecond.Nanoseconds())
   207  	}
   208  
   209  	if nanotime != nil {
   210  		if clockResolutionInvalid(nanotimeResolution) {
   211  			return nil, fmt.Errorf("invalid Nanotime resolution: %d", nanotimeResolution)
   212  		}
   213  		sysCtx.nanotime = nanotime
   214  		sysCtx.nanotimeResolution = nanotimeResolution
   215  	} else {
   216  		sysCtx.nanotime = platform.NewFakeNanotime()
   217  		sysCtx.nanotimeResolution = sys.ClockResolution(time.Nanosecond)
   218  	}
   219  
   220  	if nanosleep != nil {
   221  		sysCtx.nanosleep = nanosleep
   222  	} else {
   223  		sysCtx.nanosleep = &ns
   224  	}
   225  
   226  	if fs != nil {
   227  		sysCtx.fsc = NewFSContext(fs)
   228  	} else {
   229  		sysCtx.fsc = NewFSContext(EmptyFS)
   230  	}
   231  
   232  	return
   233  }
   234  
   235  // clockResolutionInvalid returns true if the value stored isn't reasonable.
   236  func clockResolutionInvalid(resolution sys.ClockResolution) bool {
   237  	return resolution < 1 || resolution > sys.ClockResolution(time.Hour.Nanoseconds())
   238  }
   239  
   240  // nullTerminatedByteCount ensures the count or Nul-terminated length of the elements doesn't exceed max, and that no
   241  // element includes the nul character.
   242  func nullTerminatedByteCount(max uint32, elements []string) (uint32, error) {
   243  	count := uint32(len(elements))
   244  	if count > max {
   245  		return 0, errors.New("exceeds maximum count")
   246  	}
   247  
   248  	// The buffer size is the total size including null terminators. The null terminator count == value count, sum
   249  	// count with each value length. This works because in Go, the length of a string is the same as its byte count.
   250  	bufSize, maxSize := uint64(count), uint64(max) // uint64 to allow summing without overflow
   251  	for _, e := range elements {
   252  		// As this is null-terminated, We have to validate there are no null characters in the string.
   253  		for _, c := range e {
   254  			if c == 0 {
   255  				return 0, errors.New("contains NUL character")
   256  			}
   257  		}
   258  
   259  		nextSize := bufSize + uint64(len(e))
   260  		if nextSize > maxSize {
   261  			return 0, errors.New("exceeds maximum size")
   262  		}
   263  		bufSize = nextSize
   264  
   265  	}
   266  	return uint32(bufSize), nil
   267  }