Skip to content

Commit

Permalink
Ensure resources are relative to the module path
Browse files Browse the repository at this point in the history
Just using std.read() for resources won't work in general, because the
desired path may not be under the input directory (and reads are
restricted to paths under the read directory).

To enable reads from module directories, while keeping the guard
against reading outside the allowed paths, keep track of each module
that imports `@jkcfg/std/resource' and look up the module path when
its `resource` procedure is called, to use as a base path.
  • Loading branch information
squaremo committed Mar 2, 2019
1 parent 7ec3b1a commit d1a0f6e
Show file tree
Hide file tree
Showing 14 changed files with 95 additions and 31 deletions.
2 changes: 1 addition & 1 deletion go.sum
Expand Up @@ -2,7 +2,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/google/flatbuffers v1.10.0 h1:oRiOciRvbejW4wxTr/DVVAczyQaxsEONq6NyxhDf3bY=
github.com/google/flatbuffers v1.10.0 h1:wHCM5N1xsJ3VwePcIpVqnmjAqRXlR44gv4hpGi+/LIw=
github.com/google/flatbuffers v1.10.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/jkcfg/v8worker2 v0.0.0-20181103131220-163e7fd126a2 h1:SsNwIj868+EIEnwSJT02K5Y3GtGWL64/5Ipy9O0cYXY=
github.com/jkcfg/v8worker2 v0.0.0-20181103131220-163e7fd126a2/go.mod h1:V1TBZ48loRvHpVCQEQSt39QYggAjIWgCaA63Z7NuGPI=
Expand Down
2 changes: 1 addition & 1 deletion pkg/resolve/magic_importer.go
Expand Up @@ -2,12 +2,12 @@ package resolve

// MagicImporter handles importing "magic" modules, that is modules
// that are calculated wherever they are imported.

type MagicImporter struct {
Specifier string
Generate func(string) ([]byte, string)
}

// Import implements the Importer interface for MagicImporter.
func (m *MagicImporter) Import(basePath, specifier, referrer string) ([]byte, string, []Candidate) {
if m.Specifier == specifier {
source, path := m.Generate(basePath)
Expand Down
31 changes: 23 additions & 8 deletions pkg/std/read.go
Expand Up @@ -99,23 +99,38 @@ func readerByPath(path string) readFunc {
return readJSON
}

// ReadBase represents the base directory for paths; it also serves
// the purpose of being the top-most directory for reads, in the case
// of paths including '..', or absolute paths.
// ResourceBaser is an interface for getting base paths for resources.
type ResourceBaser interface {
ResourceBase(string) (string, bool)
}

// ReadBase resolves relative paths, and resources (module-relative
// paths). Reads outside the base are forbidden and will return an
// error.
type ReadBase struct {
Path string
Path string
Resources ResourceBaser
}

func (r ReadBase) Read(path string, format __std.Format, encoding __std.Encoding) ([]byte, error) {
func (r ReadBase) Read(path string, format __std.Format, encoding __std.Encoding, module string) ([]byte, error) {
base := r.Path
if module != "" {
modBase, ok := r.Resources.ResourceBase(module)
if !ok {
return nil, fmt.Errorf("read from unknown module")
}
base = modBase
}

if !filepath.IsAbs(path) {
path = filepath.Join(r.Path, path)
path = filepath.Join(base, path)
}
rel, err := filepath.Rel(r.Path, path)
rel, err := filepath.Rel(base, path)
if err != nil {
return nil, err
}
if strings.HasPrefix(rel, "..") {
return nil, fmt.Errorf("reads outside input path forbidden")
return nil, fmt.Errorf("reads outside base path forbidden")
}
return read(path, format, encoding)
}
Expand Down
42 changes: 35 additions & 7 deletions pkg/std/resource.go
@@ -1,20 +1,48 @@
package std

import (
"crypto/sha256"
"fmt"
)

func MakeResourceModule(basePath string) ([]byte, string) {
return []byte(fmt.Sprintf(`
// ModuleResources keeps track of the base paths for modules, as well
// as generating the magic modules when they are imported.
type ModuleResources struct {
// module hash -> basePath for resource reads
modules map[string]string
}

// NewModuleResources initialises a new ModuleResources
func NewModuleResources() *ModuleResources {
return &ModuleResources{
modules: map[string]string{},
}
}

// ResourceBase provides the module base path given the hash.
func (r *ModuleResources) ResourceBase(hash string) (string, bool) {
path, ok := r.modules[hash]
return path, ok
}

// MakeModule generates resource module code (and path) given the
// importing module's base path.
func (r *ModuleResources) MakeModule(basePath string) ([]byte, string) {
hash := sha256.New()
hash.Write([]byte(basePath))
moduleHash := fmt.Sprintf("%x", hash.Sum(nil))
r.modules[moduleHash] = basePath

code := `
import std from '@jkcfg/std';
const base = %q;
const module = %q;
function resource(path, ...rest) {
return std.read(base +'/' + path, ...rest);
function resource(path, {...rest} = {}) {
return std.read(path, {...rest, module});
}
export default resource;
`,
basePath)), "resource:" + basePath
`
return []byte(fmt.Sprintf(code, moduleHash)), "resource:" + basePath
}
3 changes: 2 additions & 1 deletion pkg/std/std.go
Expand Up @@ -89,7 +89,8 @@ func Execute(msg []byte, res sender, options ExecuteOptions) []byte {
if path != "" && options.Verbose {
fmt.Printf("read %s\n", path)
}
ser := deferred.Register(func() ([]byte, error) { return options.Root.Read(path, args.Format(), args.Encoding()) }, sendFunc(res.SendBytes))
module := string(args.Module())
ser := deferred.Register(func() ([]byte, error) { return options.Root.Read(path, args.Format(), args.Encoding(), module) }, sendFunc(res.SendBytes))
return deferredResponse(ser)
case __std.ArgsFileInfoArgs:
args := __std.FileInfoArgs{}
Expand Down
9 changes: 6 additions & 3 deletions run.go
Expand Up @@ -123,14 +123,15 @@ func runArgs(cmd *cobra.Command, args []string) error {
type exec struct {
worker *v8.Worker
workingDir string
resources std.ResourceBaser
}

func (e *exec) onMessageReceived(msg []byte) []byte {
return std.Execute(msg, e.worker, std.ExecuteOptions{
Verbose: runOptions.verbose,
Parameters: runOptions.parameters,
OutputDirectory: runOptions.outputDirectory,
Root: std.ReadBase{Path: e.workingDir},
Root: std.ReadBase{Path: e.workingDir, Resources: e.resources},
})
}

Expand All @@ -149,7 +150,9 @@ func run(cmd *cobra.Command, args []string) {
}
}

engine := &exec{workingDir: inputDir}
resources := std.NewModuleResources()

engine := &exec{workingDir: inputDir, resources: resources}
worker := v8.New(engine.onMessageReceived)
engine.worker = worker
input, err := ioutil.ReadFile(filename)
Expand All @@ -165,7 +168,7 @@ func run(cmd *cobra.Command, args []string) {
}

resolver := resolve.NewResolver(worker, scriptDir,
&resolve.MagicImporter{Specifier: "@jkcfg/std/resource", Generate: std.MakeResourceModule},
&resolve.MagicImporter{Specifier: "@jkcfg/std/resource", Generate: resources.MakeModule},
&resolve.StaticImporter{Specifier: "std", Source: std.Module()},
&resolve.StaticImporter{Specifier: "@jkcfg/std", Source: std.Module()},
&resolve.FileImporter{},
Expand Down
1 change: 1 addition & 0 deletions std/__std_Read.fbs
Expand Up @@ -20,4 +20,5 @@ table ReadArgs {
timeout: uint;
encoding: Encoding;
format: Format;
module: string;
}
11 changes: 10 additions & 1 deletion std/std_read.js
Expand Up @@ -14,13 +14,22 @@ const stringify = bytes => String.fromCodePoint(...uint8ToUint16Array(bytes));

// read requests the path and returns a promise that will be resolved
// with the contents at the path, or rejected.
function read(path, { encoding = Encoding.JSON, format = Format.Auto } = {}) {
function read(path, opts = {}) {
const { encoding = Encoding.JSON, format = Format.Auto, module } = opts;

const builder = new flatbuffers.Builder(512);
const pathOffset = builder.createString(path);
let moduleOffset = 0;
if (module !== undefined) {
moduleOffset = builder.createString(module);
}
__std.ReadArgs.startReadArgs(builder);
__std.ReadArgs.addPath(builder, pathOffset);
__std.ReadArgs.addEncoding(builder, encoding);
__std.ReadArgs.addFormat(builder, format);
if (module !== undefined) {
__std.ReadArgs.addModule(builder, moduleOffset);
}
const argsOffset = __std.ReadArgs.endReadArgs(builder);
__std.Message.startMessage(builder);
__std.Message.addArgsType(builder, __std.Args.ReadArgs);
Expand Down
6 changes: 4 additions & 2 deletions tests/test-module-resource.js
@@ -1,4 +1,6 @@
import std from '@jkcfg/std';
import resource from './test-module-resource/resource';
import resource1 from './test-module-resource/resource';
import resource2 from './test-module-resource/submodule/resource';

resource.then(std.log);
resource1.then(std.log);
resource2.then(std.log);
2 changes: 1 addition & 1 deletion tests/test-module-resource.js.cmd
@@ -1 +1 @@
jk run -v --input-directory=/tmp %f
jk run --input-directory=/tmp %f
7 changes: 4 additions & 3 deletions tests/test-module-resource.js.expected
@@ -1,5 +1,6 @@
{
"foo": {
"bar": 1
}
"module": "top"
}
{
"module": "sub"
}
3 changes: 3 additions & 0 deletions tests/test-module-resource/submodule/resource.js
@@ -0,0 +1,3 @@
import resource from '@jkcfg/std/resource';

export default resource('values.json');
3 changes: 3 additions & 0 deletions tests/test-module-resource/submodule/values.json
@@ -0,0 +1,3 @@
{
"module": "sub"
}
4 changes: 1 addition & 3 deletions tests/test-module-resource/value.json
@@ -1,5 +1,3 @@
{
"foo": {
"bar": 1
}
"module": "top"
}

0 comments on commit d1a0f6e

Please sign in to comment.