| // 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]+|HEAD|LEGACY)\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) |
| } |