ooni-probe-cli/internal/errorsx/generator/main.go

230 lines
7.4 KiB
Go
Raw Normal View History

package main
import (
"fmt"
"log"
"os"
"time"
"github.com/iancoleman/strcase"
"golang.org/x/sys/execabs"
)
// ErrorSpec specifies the error we care about.
type ErrorSpec struct {
// errno is the error name as an errno value (e.g., ECONNREFUSED).
errno string
// failure is the error name according to OONI (e.g., FailureConnectionRefused).
failure string
}
// AsErrnoName returns the name of the corresponding errno, if this
// is a system error, or panics otherwise.
func (es *ErrorSpec) AsErrnoName() string {
if !es.IsSystemError() {
panic("not a system error")
}
return es.errno
}
// AsFailureVar returns the name of the failure var.
func (es *ErrorSpec) AsFailureVar() string {
return "Failure" + strcase.ToCamel(es.failure)
}
// AsFailureString returns the OONI failure string.
func (es *ErrorSpec) AsFailureString() string {
return es.failure
}
// NewSystemError constructs a new ErrorSpec representing a system
// error, i.e., an error returned by a system call.
func NewSystemError(errno, failure string) *ErrorSpec {
return &ErrorSpec{errno: errno, failure: failure}
}
// NewLibraryError constructs a new ErrorSpec representing a library
// error, i.e., an error returned by the Go standard library or by other
// dependecies written typicall in Go (e.g., quic-go).
func NewLibraryError(failure string) *ErrorSpec {
return &ErrorSpec{failure: failure}
}
// IsSystemError returns whether this ErrorSpec describes a system
// error, i.e., an error returned by a syscall.
func (es *ErrorSpec) IsSystemError() bool {
return es.errno != ""
}
// Specs contains all the error specs.
var Specs = []*ErrorSpec{
NewSystemError("ECANCELED", "operation_canceled"),
NewSystemError("ECONNREFUSED", "connection_refused"),
NewSystemError("ECONNRESET", "connection_reset"),
NewSystemError("EHOSTUNREACH", "host_unreachable"),
NewSystemError("ETIMEDOUT", "timed_out"),
NewSystemError("EAFNOSUPPORT", "address_family_not_supported"),
NewSystemError("EADDRINUSE", "address_in_use"),
NewSystemError("EADDRNOTAVAIL", "address_not_available"),
NewSystemError("EISCONN", "already_connected"),
NewSystemError("EFAULT", "bad_address"),
NewSystemError("EBADF", "bad_file_descriptor"),
NewSystemError("ECONNABORTED", "connection_aborted"),
NewSystemError("EALREADY", "connection_already_in_progress"),
NewSystemError("EDESTADDRREQ", "destination_address_required"),
NewSystemError("EINTR", "interrupted"),
NewSystemError("EINVAL", "invalid_argument"),
NewSystemError("EMSGSIZE", "message_size"),
NewSystemError("ENETDOWN", "network_down"),
NewSystemError("ENETRESET", "network_reset"),
NewSystemError("ENETUNREACH", "network_unreachable"),
NewSystemError("ENOBUFS", "no_buffer_space"),
NewSystemError("ENOPROTOOPT", "no_protocol_option"),
NewSystemError("ENOTSOCK", "not_a_socket"),
NewSystemError("ENOTCONN", "not_connected"),
NewSystemError("EWOULDBLOCK", "operation_would_block"),
NewSystemError("EACCES", "permission_denied"),
NewSystemError("EPROTONOSUPPORT", "protocol_not_supported"),
NewSystemError("EPROTOTYPE", "wrong_protocol_type"),
}
func fileCreate(filename string) *os.File {
filep, err := os.Create(filename)
if err != nil {
log.Fatal(err)
}
return filep
}
func fileWrite(filep *os.File, content string) {
if _, err := filep.WriteString(content); err != nil {
log.Fatal(err)
}
}
func fileClose(filep *os.File) {
if err := filep.Close(); err != nil {
log.Fatal(err)
}
}
func filePrintf(filep *os.File, format string, v ...interface{}) {
fileWrite(filep, fmt.Sprintf(format, v...))
}
func gofmt(filename string) {
cmd := execabs.Command("go", "fmt", filename)
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
func writeSystemSpecificFile(kind string) {
filename := "errno_" + kind + ".go"
filep := fileCreate(filename)
fileWrite(filep, "// Code generated by go generate; DO NOT EDIT.\n")
filePrintf(filep, "// Generated: %+v\n\n", time.Now())
fileWrite(filep, "package errorsx\n\n")
filePrintf(filep, "import \"golang.org/x/sys/%s\"\n\n", kind)
fileWrite(filep, "const (\n")
for _, spec := range Specs {
filePrintf(filep, "\t%s = %s.%s\n",
spec.AsErrnoName(), kind, spec.AsErrnoName())
}
fileWrite(filep, ")\n\n")
fileClose(filep)
gofmt(filename)
}
func writeGenericFile() {
filename := "errno.go"
filep := fileCreate(filename)
fileWrite(filep, "// Code generated by go generate; DO NOT EDIT.\n")
filePrintf(filep, "// Generated: %+v\n\n", time.Now())
fileWrite(filep, "package errorsx\n\n")
fileWrite(filep, "//go:generate go run ./generator/\n\n")
fileWrite(filep, "import (\n")
fileWrite(filep, "\t\"errors\"\n")
fileWrite(filep, "\t\"syscall\"\n")
fileWrite(filep, ")\n\n")
fileWrite(filep, "// This enumeration lists the syscall-derived failures defined at\n")
fileWrite(filep, "// https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md\n")
fileWrite(filep, "//\n")
fileWrite(filep, "// See also the enumeration at failures.go for the failures that\n")
fileWrite(filep, "// DO NOT derive from system call errors.\n")
fileWrite(filep, "const (\n")
for _, spec := range Specs {
filePrintf(filep, "\t%s = \"%s\"\n",
spec.AsFailureVar(),
spec.AsFailureString())
}
fileWrite(filep, ")\n\n")
fileWrite(filep, "// toSyscallErr converts a syscall error to the\n")
fileWrite(filep, "// proper OONI error. Returns the OONI error string\n")
fileWrite(filep, "// on success, an empty string otherwise.\n")
fileWrite(filep, "func toSyscallErr(err error) string {\n")
fileWrite(filep, "\t// filter out system errors: necessary to detect all windows errors\n")
fileWrite(filep, "\t// https://github.com/ooni/probe/issues/1526 describes the problem\n")
fileWrite(filep, "\t// of mapping localized windows errors.\n")
fileWrite(filep, "\tvar errno syscall.Errno\n")
fileWrite(filep, "\tif !errors.As(err, &errno) {\n")
fileWrite(filep, "\t\treturn \"\"\n")
fileWrite(filep, "\t}\n")
fileWrite(filep, "\tswitch errno {\n")
for _, spec := range Specs {
filePrintf(filep, "\tcase %s:\n", spec.AsErrnoName())
filePrintf(filep, "\t\treturn %s\n", spec.AsFailureVar())
}
fileWrite(filep, "\t}\n")
fileWrite(filep, "\treturn \"\"\n")
fileWrite(filep, "}\n\n")
fileClose(filep)
gofmt(filename)
}
func writeGenericTestFile() {
filename := "errno_test.go"
filep := fileCreate(filename)
fileWrite(filep, "// Code generated by go generate; DO NOT EDIT.\n")
filePrintf(filep, "// Generated: %+v\n\n", time.Now())
fileWrite(filep, "package errorsx\n\n")
fileWrite(filep, "import (\n")
fileWrite(filep, "\t\"io\"\n")
fileWrite(filep, "\t\"syscall\"\n")
fileWrite(filep, "\t\"testing\"\n")
fileWrite(filep, ")\n\n")
fileWrite(filep, "func TestToSyscallErr(t *testing.T) {\n")
fileWrite(filep, "\tif v := toSyscallErr(io.EOF); v != \"\" {\n")
fileWrite(filep, "\t\tt.Fatalf(\"expected empty string, got '%s'\", v)\n")
fileWrite(filep, "\t}\n")
for _, spec := range Specs {
filePrintf(filep, "\tif v := toSyscallErr(%s); v != %s {\n",
spec.AsErrnoName(), spec.AsFailureVar())
filePrintf(filep, "\t\tt.Fatalf(\"expected '%%s', got '%%s'\", %s, v)\n",
spec.AsFailureVar())
fileWrite(filep, "\t}\n")
}
fileWrite(filep, "\tif v := toSyscallErr(syscall.Errno(0)); v != \"\" {\n")
fileWrite(filep, "\t\tt.Fatalf(\"expected empty string, got '%s'\", v)\n")
fileWrite(filep, "\t}\n")
fileWrite(filep, "}\n")
fileClose(filep)
gofmt(filename)
}
func main() {
writeSystemSpecificFile("unix")
writeSystemSpecificFile("windows")
writeGenericFile()
writeGenericTestFile()
}