blob: 30dce91c8f1a606fa2292583b42f4dc02cf01142 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"regexp"
"strings"
)
// A server handles fidlbolt requests.
type server struct {
// Deployment information, or nil.
deployment *deployment
// External programs used by fidlbolt.
fidlFormat, fidlLint, fidlc, fidlgenCpp, fidlgenHlcpp, fidlgenRust,
fidlgenGo, clangFormat, rustfmt program
// Optional external programs.
fidlgenDart program
// External files used by fidlbolt.
rustfmtToml string
// Cache of FIDL library information.
libraryAnalyzer *libraryAnalyzer
}
// newServer returns a new fidlbolt server. It takes three lists of paths: bin,
// for locating binaries, etc, for locating config files, and fidl, for locating
// FIDL libraries. Returns an error if a required file could not be located.
func newServer(bin, etc, fidl []string) (*server, error) {
var s server
var err error
if deploymentFile, err := findFile("fidlbolt_deployment.json", etc); err == nil {
deploymentBytes, err := ioutil.ReadFile(deploymentFile)
if err != nil {
return nil, fmt.Errorf("failed to read etc/fidlbolt_deployment.json: %s", err)
}
s.deployment = new(deployment)
if err = json.Unmarshal(deploymentBytes, s.deployment); err != nil {
return nil, fmt.Errorf("failed to parse etc/fidlbolt_deployment.json: %s", err)
}
}
s.fidlFormat, err = findProgram("fidl-format", bin)
if err != nil {
return nil, err
}
s.fidlLint, err = findProgram("fidl-lint", bin)
if err != nil {
return nil, err
}
s.fidlc, err = findProgram("fidlc", bin)
if err != nil {
return nil, err
}
s.fidlgenCpp, err = findProgram("fidlgen_cpp", bin)
if err != nil {
return nil, err
}
s.fidlgenHlcpp, err = findProgram("fidlgen_hlcpp", bin)
if err != nil {
return nil, err
}
s.fidlgenRust, err = findProgram("fidlgen_rust", bin)
if err != nil {
return nil, err
}
s.fidlgenGo, err = findProgram("fidlgen_go", bin)
if err != nil {
return nil, err
}
s.clangFormat, err = findProgram("clang-format", bin)
if err != nil {
return nil, err
}
s.rustfmt, err = findProgram("rustfmt", bin)
if err != nil {
return nil, err
}
s.fidlgenDart, err = findProgram("fidlgen_dart", bin)
if err != nil {
log.Printf("warning: %s", err)
}
s.rustfmtToml, err = findFile("rustfmt.toml", etc)
if err != nil {
return nil, err
}
s.libraryAnalyzer = newLibraryAnalyzer(fidl)
return &s, nil
}
// A request provides input to be converted to an output mode.
type request struct {
// Content to be converted.
Content string `json:"content"`
// The mode of Content.
InputMode mode `json:"inputMode"`
// The desired output mode.
OutputMode mode `json:"outputMode"`
// Options for configuring the conversion.
Options options `json:"options"`
}
// A response contains the result of converting to an output mode.
type response struct {
// True if conversion was successful.
Ok bool `json:"ok"`
// If Ok is true, Content contains the conversion result (in the requested
// output mode). Otherwise, it contains an error message.
Content string `json:"content"`
// List of annotations to apply to on the input.
Annotations []annotation `json:"annotations"`
// Information about the deployment that served this response.
Deployment *deployment `json:"deployment,omitempty"`
}
// A deployment describes the provenance of all resources used to service a
// request, including the fidlbolt server itself, external binaries, FIDL
// libaries, configuration files, etc.
type deployment struct {
// This repository.
// https://fuchsia.googlesource.com/fidlbolt
Fidlbolt commit `json:"fidlbolt"`
// Fuchsia provides most resources used by fidlbolt.
// https://fuchsia.googlesource.com/fuchsia
Fuchsia commit `json:"fuchsia"`
// ClangFormat provides the clang-format binary.
// https://llvm.googlesource.com/a/llvm-project
ClangFormat version `json:"clang-format"`
// Rustfmt provides the rustfmt binary.
// https://github.com/rust-lang/rustfmt
Rustfmt version `json:"rustfmt"`
}
// A commit designates a commit in a source code repository.
type commit struct {
// Full hash of the commit.
Hash string `json:"hash"`
// Unix timestamp of the commit, in seconds.
Timestamp uint64 `json:"timestamp"`
}
// A version designates a software version by a string like "1.2.3".
type version struct {
Version string `json:"version"`
}
// options are used to configure mode conversions.
type options struct {
// File to generate (header, source, etc.).
File string `json:"file"`
// Lint in addition to formatting.
Lint bool `json:"lint"`
// Number of byte columns.
Columns int `json:"columns"`
// Byte grouping size.
Group int `json:"group"`
// Use capital letters for hex values.
Capital bool `json:"capital"`
// Show byte offsets at the start of each line.
Offsets bool `json:"offsets"`
// Show ASCII characters at the end of lines like xxd.
ASCII bool `json:"ascii"`
// Deprecated: Convert FIDL from old to new (RFC-0050) syntax.
ConvertSyntax bool `json:"convertSyntax"`
// Zero or more space-separated arguments to pass to fidlc with --available,
// e.g. "", "fuchsia:HEAD", or "foo:1 bar:2".
VersionSelection string `json:"versionSelection"`
}
// versionSelectionRegexp is used to validate Options.VersionSelection.
var versionSelectionRegexp = regexp.MustCompile(`^\s*([a-z][a-z0-9_]*:([0-9]+|NEXT|HEAD)(,([0-9]+|NEXT|HEAD))*\s*)*$`)
// An annotation is a message targeted at a specific location of a file.
type annotation struct {
// One-based line number.
Line int `json:"line"`
// One-based column number.
Column int `json:"column"`
// Kind of annotation.
Kind annotationKind `json:"kind"`
// Body of the annotation.
Text string `json:"text"`
}
// annotationKind is a string enum for annotation.
type annotationKind string
// Enumeration of annotationKind values.
const (
Info annotationKind = "info"
Warning annotationKind = "warning"
Error annotationKind = "error"
)
// A mode is an input or output format.
type mode string
// Enumeration of mode values.
const (
Bytes mode = "bytes"
BytesPlus mode = "bytes+"
CPP mode = "cpp"
Dart mode = "dart"
Diff mode = "diff"
FIDL mode = "fidl"
FIDLText mode = "fidltext"
Go mode = "go"
HLCPP mode = "hlcpp"
JSON mode = "json"
Rust mode = "rust"
)
type modeSet = map[mode]struct{}
var inputModes = modeSet{
FIDL: {},
FIDLText: {},
Bytes: {},
}
var outputModes = map[mode]modeSet{
FIDL: {
FIDL: {},
JSON: {},
CPP: {},
HLCPP: {},
Rust: {},
Go: {},
Dart: {},
},
FIDLText: {
FIDLText: {},
Bytes: {},
BytesPlus: {},
},
Bytes: {
Bytes: {},
Diff: {},
FIDLText: {},
BytesPlus: {},
},
}
func (m *mode) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
// TODO(fxbug.dev/106610): This is kept for backwards compatibility with old
// clients. Remove this eventually.
if s == "llcpp" {
s = "cpp"
}
*m = mode(s)
return nil
}
// validate validates a request. Once validated, any errors in processing the
// request are considered internal server errors.
func (r *request) validate() error {
if err := r.InputMode.validate(inputModes); err != nil {
return err
}
if err := r.OutputMode.validate(outputModes[r.InputMode]); err != nil {
return err
}
if r.Options.ConvertSyntax {
return errors.New("convertSyntax is no longer supported")
}
if !versionSelectionRegexp.MatchString(r.Options.VersionSelection) {
return fmt.Errorf("invalid version selection: %s", r.Options.VersionSelection)
}
// TODO(fxbug.dev/93781): Instead of identifiers like "header" and "source",
// use actual file names, as we have started doing for C++.
switch r.OutputMode {
case JSON:
switch r.Options.File {
case "ir", "schema", "deps":
default:
return fmt.Errorf("invalid JSON IR file option: %q", r.Options.File)
}
case HLCPP:
switch r.Options.File {
case "header", "source", "test", "tables":
default:
return fmt.Errorf("invalid HLCPP file option: %q", r.Options.File)
}
case CPP:
switch r.Options.File {
case "common_types.h", "markers.h", "wire_types.h",
"wire_types.cc", "wire.h", "wire_messaging.h",
"wire_messaging.cc", "wire_test_base.h", "natural_types.h",
"natural_types.cc", "fidl.h", "natural_messaging.h",
"natural_messaging.cc", "driver/wire.h", "driver/wire_messaging.h",
"driver/wire_messaging.cc", "driver/natural_messaging.h",
"driver/natural_messaging.cc", "type_conversions.h",
"type_conversions.cc":
default:
return fmt.Errorf("invalid C++ file option: %q", r.Options.File)
}
case Dart:
switch r.Options.File {
case "library", "test":
default:
return fmt.Errorf("invalid Dart file option: %q", r.Options.File)
}
default:
if r.Options.File != "" {
return fmt.Errorf("unexpected file option: %q", r.Options.File)
}
}
if r.OutputMode == Bytes {
if r.Options.Columns <= 0 {
return fmt.Errorf("invalid columns: %d", r.Options.Columns)
}
if r.Options.Group <= 0 {
return fmt.Errorf("invalid group: %d", r.Options.Group)
}
}
return nil
}
func (m mode) validate(allowed modeSet) error {
if _, ok := allowed[m]; !ok {
return fmt.Errorf("invalid mode: %q", m)
}
return nil
}
// serve processes a request and serves a response. It returns an error if the
// request could not be processed for some reason (e.g., an IO failure).
func (s *server) serve(ctx context.Context, r *request) (response, error) {
res, err := s.handle(ctx, r)
if err != nil {
return response{}, err
}
res.Deployment = s.deployment
return res, nil
}
func (s *server) handle(ctx context.Context, r *request) (response, error) {
switch r.InputMode {
case FIDL:
return s.handleFIDL(ctx, r)
case FIDLText:
return s.handleFIDLText(ctx, r)
case Bytes:
return s.handleBytes(ctx, r)
}
return response{}, fmt.Errorf("invalid input mode: %q", r.InputMode)
}
func (s *server) handleFIDL(ctx context.Context, r *request) (response, error) {
temp, err := newTempDir()
if err != nil {
return response{}, err
}
defer os.RemoveAll(temp.path)
fidl, err := temp.createFile("fidlbolt.fidl", r.Content)
if err != nil {
return response{}, err
}
analysis := s.libraryAnalyzer.analyze(fidl, r.Content)
res, err := s.handleFIDLWithTemp(ctx, r, temp, fidl, analysis)
if err != nil {
return res, err
}
res.Annotations = append(res.Annotations, analysis.annotations...)
// Remove all occurrences of the temp path to avoid exposing it to the user.
// This is mostly needed for error output, but it is also needed e.g. for
// the JSON IR, which includes source paths.
res.Content = strings.ReplaceAll(res.Content, temp.path+"/", "")
res.Content = strings.ReplaceAll(res.Content, temp.path, "")
return res, nil
}
const jsonDepIndentSize = 4
func (s *server) handleFIDLWithTemp(ctx context.Context, r *request, temp tempDir, fidl string, analysis analysis) (response, error) {
// Run fidlc, automatically appending the correct --files arguments.
fidlc := func(args ...string) (response, error) {
fileArgs, err := analysis.libs.fidlcFileArgs()
if err != nil {
msg := fmt.Sprintf("%s:1:1: error: %s", fidl, err)
return response{Ok: false, Content: msg}, nil
}
for _, availableArg := range strings.Fields(r.Options.VersionSelection) {
args = append(args, "--available", availableArg)
}
args = append(args, fileArgs...)
run, err := s.fidlc.run(ctx, args...)
if err != nil {
return response{}, err
}
res := response{
Ok: run.success,
Annotations: parseFidlAnnotations(run.stderr),
}
if run.success {
res.Content = run.stdout
} else {
res.Content = run.stderr
}
if run.success && analysis.root == "" {
return response{}, fmt.Errorf("input has no library declaration, yet fidlc succeeded")
}
return res, nil
}
switch r.OutputMode {
case FIDL:
prog := s.fidlFormat
if r.Options.Lint {
prog = s.fidlLint
}
run, err := prog.run(ctx, fidl)
if err != nil {
return response{}, err
}
res := response{
Ok: run.success,
Annotations: parseFidlAnnotations(run.stderr),
}
if run.success {
if r.Options.Lint {
res.Content = "// Lint passed!"
} else {
res.Content = run.stdout
}
} else {
res.Content = run.stderr
}
return res, nil
case JSON:
switch r.Options.File {
case "ir":
return fidlc("--json", "/dev/stdout")
case "schema":
run, err := s.fidlc.runInfallible(ctx, "--json-schema")
if err != nil {
return response{}, err
}
return response{Ok: true, Content: run.stdout}, nil
case "deps":
graph, err := analysis.libs.dependencyGraph()
if err != nil {
msg := fmt.Sprintf("%s:1:1: error: %s", fidl, err)
return response{Ok: false, Content: msg}, nil
}
b, err := json.MarshalIndent(graph, "", strings.Repeat(" ", jsonDepIndentSize))
if err != nil {
msg := fmt.Sprintf("%s:1:1: error: %s", fidl, err)
return response{Ok: false, Content: msg}, nil
}
return response{Ok: true, Content: string(b)}, nil
default:
return response{}, fmt.Errorf("invalid file: %q", r.Options.File)
}
}
jsonIR := fidl + ".json"
res, err := fidlc("--json", jsonIR)
if err != nil {
return response{}, err
}
if !res.Ok {
return res, nil
}
if r.OutputMode == Dart && s.fidlgenDart.path == "" {
res.Content = "This build of fidlbolt does not include fidlgen_dart"
res.Ok = false
return res, nil
}
res.Content, err = s.handleFIDLWithIR(ctx, r, temp, jsonIR, analysis)
if err != nil {
return response{}, err
}
return res, nil
}
func (s *server) handleFIDLWithIR(ctx context.Context, r *request, temp tempDir, jsonIR string, analysis analysis) (string, error) {
switch r.OutputMode {
case HLCPP:
if _, err := s.fidlgenHlcpp.runInfallible(ctx,
"-json", jsonIR,
"-root", temp.path,
"-clang-format-path", s.clangFormat.path,
); err != nil {
return "", err
}
// TODO(fxbug.dev/85703): Sanitize paths.
libraryDirs := strings.ReplaceAll(string(analysis.root), ".", "/")
switch r.Options.File {
case "header":
return temp.readFile(libraryDirs, "cpp", "fidl.h")
case "source":
return temp.readFile(libraryDirs, "cpp", "fidl.cc")
case "test":
return temp.readFile(libraryDirs, "cpp", "fidl_test_base.h")
case "tables":
return temp.readFile(libraryDirs, "cpp", "tables.c")
default:
return "", fmt.Errorf("invalid file: %q", r.Options.File)
}
case CPP:
if _, err := s.fidlgenCpp.runInfallible(ctx,
"-json", jsonIR,
"-root", temp.path,
"-clang-format-path", s.clangFormat.path,
); err != nil {
return "", err
}
switch r.Options.File {
case "common_types.h", "markers.h", "wire_types.h",
"wire_types.cc", "wire.h", "wire_messaging.h",
"wire_messaging.cc", "wire_test_base.h", "natural_types.h",
"natural_types.cc", "fidl.h", "natural_messaging.h",
"natural_messaging.cc", "driver/wire.h", "driver/wire_messaging.h",
"driver/wire_messaging.cc", "driver/natural_messaging.h",
"driver/natural_messaging.cc", "type_conversions.h",
"type_conversions.cc":
// TODO(fxbug.dev/85703): Sanitize paths.
return temp.readFile("fidl", string(analysis.root), "cpp", r.Options.File)
default:
return "", fmt.Errorf("invalid file: %q", r.Options.File)
}
case Rust:
if _, err := s.fidlgenRust.runInfallible(ctx,
"-json", jsonIR,
"-output-filename", temp.join("impl.rs"),
"-rustfmt", s.rustfmt.path,
"-rustfmt-config", s.rustfmtToml,
); err != nil {
return "", err
}
return temp.readFile("impl.rs")
case Go:
if _, err := s.fidlgenGo.runInfallible(ctx,
"-json", jsonIR,
"-output-impl", temp.join("impl.go"),
); err != nil {
return "", err
}
return temp.readFile("impl.go")
case Dart:
var name string
switch r.Options.File {
case "library":
name = "async"
case "test":
name = "test"
default:
return "", fmt.Errorf("invalid file: %q", r.Options.File)
}
// TODO(fxbug.dev/70703): Pass -dartfmt.
if _, err := s.fidlgenDart.runInfallible(ctx,
"-json", jsonIR,
"-output-"+name, temp.join("impl.dart"),
); err != nil {
return "", err
}
return temp.readFile("impl.dart")
}
return "", fmt.Errorf("invalid output mode: %q", r.OutputMode)
}
func (s *server) handleFIDLText(ctx context.Context, r *request) (response, error) {
return response{Ok: false, Content: "Not implemented"}, nil
}
func (s *server) handleBytes(ctx context.Context, r *request) (response, error) {
switch r.OutputMode {
case Bytes:
b, pos, err := parseBytes(r.Content)
if err != nil {
msg := fmt.Sprintf("%s: error: %s", pos, err)
ann := annotation{
Line: pos.Line,
Column: pos.Column,
Kind: Error,
Text: err.Error(),
}
return response{Ok: false, Content: msg, Annotations: []annotation{ann}}, nil
}
formatted, err := formatBytes(b, &r.Options)
if err != nil {
return response{}, err
}
return response{Ok: true, Content: formatted}, nil
case Diff, FIDLText, BytesPlus:
return response{Ok: false, Content: "Not implemented"}, nil
}
return response{}, fmt.Errorf("invalid output mode: %q", r.OutputMode)
}