<commit_before>// Copyright 2015 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package config

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/google/syzkaller/fileutil"
	"github.com/google/syzkaller/sys"
	"github.com/google/syzkaller/vm"
)

type Config struct {
	Name     string // Instance name (used for identification and as GCE instance prefix)
	Http     string // TCP address to serve HTTP stats page (e.g. "localhost:50000")
	Rpc      string // TCP address to serve RPC for fuzzer processes (optional, only useful for type "none")
	Workdir  string
	Vmlinux  string
	Kernel   string // e.g. arch/x86/boot/bzImage
	Tag      string // arbitrary optional tag that is saved along with crash reports (e.g. kernel branch/commit)
	Cmdline  string // kernel command line
	Image    string // linux image for VMs
	Initrd   string // linux initial ramdisk. (optional)
	Cpu      int    // number of VM CPUs
	Mem      int    // amount of VM memory in MBs
	Sshkey   string // root ssh key for the image
	Bin      string // qemu/lkvm binary name
	Bin_Args string // additional command line arguments for qemu/lkvm binary
	Debug    bool   // dump all VM output to console
	Output   string // one of stdout/dmesg/file (useful only for local VM)

	Hub_Addr string
	Hub_Key  string

	Syzkaller string   // path to syzkaller checkout (syz-manager will look for binaries in bin subdir)
	Type      string   // VM type (qemu, kvm, local)
	Count     int      // number of VMs (don't secify for adb, instead specify devices)
	Devices   []string // device IDs for adb
	Procs     int      // number of parallel processes inside of every VM

	Sandbox string // type of sandbox to use during fuzzing:
	// "none": don't do anything special (has false positives, e.g. due to killing init)
	// "setuid": impersonate into user nobody (65534), default
	// "namespace": create a new namespace for fuzzer using CLONE_NEWNS/CLONE_NEWNET/CLONE_NEWPID/etc,
	//	requires building kernel with CONFIG_NAMESPACES, CONFIG_UTS_NS, CONFIG_USER_NS, CONFIG_PID_NS and CONFIG_NET_NS.

	Machine_Type string // GCE machine type (e.g. "n1-highcpu-2")

	Cover     bool // use kcov coverage (default: true)
	Leak      bool // do memory leak checking
	Reproduce bool // reproduce, localize and minimize crashers (on by default)

	Enable_Syscalls  []string
	Disable_Syscalls []string
	Suppressions     []string // don't save reports matching these regexps, but reboot VM after them
	Ignores          []string // completely ignore reports matching these regexps (don't save nor reboot)

	// Implementation details beyond this point.
	ParsedSuppressions []*regexp.Regexp `json:"-"`
	ParsedIgnores      []*regexp.Regexp `json:"-"`
}

func Parse(filename string) (*Config, map[int]bool, error) {
	if filename == "" {
		return nil, nil, fmt.Errorf("supply config in -config flag")
	}
	data, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to read config file: %v", err)
	}
	return parse(data)
}

func parse(data []byte) (*Config, map[int]bool, error) {
	unknown, err := checkUnknownFields(data)
	if err != nil {
		return nil, nil, err
	}
	if unknown != "" {
		return nil, nil, fmt.Errorf("unknown field '%v' in config", unknown)
	}
	cfg := new(Config)
	cfg.Cover = true
	cfg.Reproduce = true
	cfg.Sandbox = "setuid"
	if err := json.Unmarshal(data, cfg); err != nil {
		return nil, nil, fmt.Errorf("failed to parse config file: %v", err)
	}
	if _, err := os.Stat(filepath.Join(cfg.Syzkaller, "bin/syz-fuzzer")); err != nil {
		return nil, nil, fmt.Errorf("bad config syzkaller param: can't find bin/syz-fuzzer")
	}
	if _, err := os.Stat(filepath.Join(cfg.Syzkaller, "bin/syz-executor")); err != nil {
		return nil, nil, fmt.Errorf("bad config syzkaller param: can't find bin/syz-executor")
	}
	if cfg.Http == "" {
		return nil, nil, fmt.Errorf("config param http is empty")
	}
	if cfg.Workdir == "" {
		return nil, nil, fmt.Errorf("config param workdir is empty")
	}
	if cfg.Vmlinux == "" {
		return nil, nil, fmt.Errorf("config param vmlinux is empty")
	}
	if cfg.Type == "" {
		return nil, nil, fmt.Errorf("config param type is empty")
	}
	switch cfg.Type {
	case "none":
		if cfg.Count != 0 {
			return nil, nil, fmt.Errorf("invalid config param count: %v, type \"none\" does not support param count", cfg.Count)
		}
		if cfg.Rpc == "" {
			return nil, nil, fmt.Errorf("config param rpc is empty (required for type \"none\")")
		}
		if len(cfg.Devices) != 0 {
			return nil, nil, fmt.Errorf("type %v does not support devices param", cfg.Type)
		}
	case "adb":
		if cfg.Count != 0 {
			return nil, nil, fmt.Errorf("don't specify count for adb, instead specify devices")
		}
		if len(cfg.Devices) == 0 {
			return nil, nil, fmt.Errorf("specify at least 1 adb device")
		}
		cfg.Count = len(cfg.Devices)
	case "gce":
		if cfg.Machine_Type == "" {
			return nil, nil, fmt.Errorf("machine_type parameter is empty (required for gce)")
		}
		fallthrough
	default:
		if cfg.Count <= 0 || cfg.Count > 1000 {
			return nil, nil, fmt.Errorf("invalid config param count: %v, want (1, 1000]", cfg.Count)
		}
		if len(cfg.Devices) != 0 {
			return nil, nil, fmt.Errorf("type %v does not support devices param", cfg.Type)
		}
	}
	if cfg.Rpc == "" {
		cfg.Rpc = "localhost:0"
	}
	if cfg.Procs <= 0 {
		cfg.Procs = 1
	}
	if cfg.Procs > 32 {
		return nil, nil, fmt.Errorf("config param procs has higher value '%v' then the max supported 32", cfg.Procs)
	}
	if cfg.Output == "" {
		if cfg.Type == "local" {
			cfg.Output = "none"
		} else {
			cfg.Output = "stdout"
		}
	}
	switch cfg.Output {
	case "none", "stdout", "dmesg", "file":
	default:
		return nil, nil, fmt.Errorf("config param output must contain one of none/stdout/dmesg/file")
	}
	switch cfg.Sandbox {
	case "none", "setuid", "namespace":
	default:
		return nil, nil, fmt.Errorf("config param sandbox must contain one of none/setuid/namespace")
	}

	wd, err := os.Getwd()
	if err != nil {
		return nil, nil, fmt.Errorf("failed to get wd: %v", err)
	}
	abs := func(path string) string {
		if path != "" && !filepath.IsAbs(path) {
			path = filepath.Join(wd, path)
		}
		return path
	}
	if cfg.Image != "9p" {
		cfg.Image = abs(cfg.Image)
	}
	cfg.Workdir = abs(cfg.Workdir)
	cfg.Kernel = abs(cfg.Kernel)
	cfg.Vmlinux = abs(cfg.Vmlinux)
	cfg.Syzkaller = abs(cfg.Syzkaller)
	cfg.Initrd = abs(cfg.Initrd)
	cfg.Sshkey = abs(cfg.Sshkey)
	cfg.Bin = abs(cfg.Bin)

	syscalls, err := parseSyscalls(cfg)
	if err != nil {
		return nil, nil, err
	}

	if err := parseSuppressions(cfg); err != nil {
		return nil, nil, err
	}

	return cfg, syscalls, nil
}

func parseSyscalls(cfg *Config) (map[int]bool, error) {
	match := func(call *sys.Call, str string) bool {
		if str == call.CallName || str == call.Name {
			return true
		}
		if len(str) > 1 && str[len(str)-1] == '*' && strings.HasPrefix(call.Name, str[:len(str)-1]) {
			return true
		}
		return false
	}

	syscalls := make(map[int]bool)
	if len(cfg.Enable_Syscalls) != 0 {
		for _, c := range cfg.Enable_Syscalls {
			n := 0
			for _, call := range sys.Calls {
				if match(call, c) {
					syscalls[call.ID] = true
					n++
				}
			}
			if n == 0 {
				return nil, fmt.Errorf("unknown enabled syscall: %v", c)
			}
		}
	} else {
		for _, call := range sys.Calls {
			syscalls[call.ID] = true
		}
	}
	for _, c := range cfg.Disable_Syscalls {
		n := 0
		for _, call := range sys.Calls {
			if match(call, c) {
				delete(syscalls, call.ID)
				n++
			}
		}
		if n == 0 {
			return nil, fmt.Errorf("unknown disabled syscall: %v", c)
		}
	}
	// mmap is used to allocate memory.
	syscalls[sys.CallMap["mmap"].ID] = true

	return syscalls, nil
}

func parseSuppressions(cfg *Config) error {
	// Add some builtin suppressions.
	supp := append(cfg.Suppressions, []string{
		"panic: failed to start executor binary",
		"panic: executor failed: pthread_create failed",
		"panic: failed to create temp dir",
		"fatal error: runtime: out of memory",
		"fatal error: runtime: cannot allocate memory",
		"fatal error: unexpected signal during runtime execution", // presubmably OOM turned into SIGBUS
		"Out of memory: Kill process .* \\(syz-fuzzer\\)",
		"lowmemorykiller: Killing 'syz-fuzzer'",
		//"INFO: lockdep is turned off", // printed by some sysrq that dumps scheduler state, but also on all lockdep reports
	}...)
	for _, s := range supp {
		re, err := regexp.Compile(s)
		if err != nil {
			return fmt.Errorf("failed to compile suppression '%v': %v", s, err)
		}
		cfg.ParsedSuppressions = append(cfg.ParsedSuppressions, re)
	}
	for _, ignore := range cfg.Ignores {
		re, err := regexp.Compile(ignore)
		if err != nil {
			return fmt.Errorf("failed to compile ignore '%v': %v", ignore, err)
		}
		cfg.ParsedIgnores = append(cfg.ParsedIgnores, re)
	}
	return nil
}

func CreateVMConfig(cfg *Config, index int) (*vm.Config, error) {
	if index < 0 || index >= cfg.Count {
		return nil, fmt.Errorf("invalid VM index %v (count %v)", index, cfg.Count)
	}
	workdir, err := fileutil.ProcessTempDir(cfg.Workdir)
	if err != nil {
		return nil, fmt.Errorf("failed to create instance temp dir: %v", err)
	}
	vmCfg := &vm.Config{
		Name:        fmt.Sprintf("%v-%v-%v", cfg.Type, cfg.Name, index),
		Index:       index,
		Workdir:     workdir,
		Bin:         cfg.Bin,
		BinArgs:     cfg.Bin_Args,
		Kernel:      cfg.Kernel,
		Cmdline:     cfg.Cmdline,
		Image:       cfg.Image,
		Initrd:      cfg.Initrd,
		Sshkey:      cfg.Sshkey,
		Executor:    filepath.Join(cfg.Syzkaller, "bin", "syz-executor"),
		Cpu:         cfg.Cpu,
		Mem:         cfg.Mem,
		Debug:       cfg.Debug,
		MachineType: cfg.Machine_Type,
	}
	if len(cfg.Devices) != 0 {
		vmCfg.Device = cfg.Devices[index]
	}
	return vmCfg, nil
}

func checkUnknownFields(data []byte) (string, error) {
	// While https://github.com/golang/go/issues/15314 is not resolved
	// we don't have a better way than to enumerate all known fields.
	var fields = []string{
		"Name",
		"Http",
		"Rpc",
		"Workdir",
		"Vmlinux",
		"Kernel",
		"Tag",
		"Cmdline",
		"Image",
		"Cpu",
		"Mem",
		"Sshkey",
		"Bin",
		"Bin_Args",
		"Debug",
		"Output",
		"Hub_Addr",
		"Hub_Key",
		"Syzkaller",
		"Type",
		"Count",
		"Devices",
		"Procs",
		"Cover",
		"Reproduce",
		"Sandbox",
		"Leak",
		"Enable_Syscalls",
		"Disable_Syscalls",
		"Suppressions",
		"Ignores",
		"Initrd",
		"Machine_Type",
	}
	f := make(map[string]interface{})
	if err := json.Unmarshal(data, &f); err != nil {
		return "", fmt.Errorf("failed to parse config file: %v", err)
	}
	for k := range f {
		ok := false
		for _, k1 := range fields {
			if strings.ToLower(k) == strings.ToLower(k1) {
				ok = true
				break
			}
		}
		if !ok {
			return k, nil
		}
	}
	return "", nil
}
<commit_msg>config: don't mess with image<commit_after>// Copyright 2015 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package config

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/google/syzkaller/fileutil"
	"github.com/google/syzkaller/sys"
	"github.com/google/syzkaller/vm"
)

type Config struct {
	Name     string // Instance name (used for identification and as GCE instance prefix)
	Http     string // TCP address to serve HTTP stats page (e.g. "localhost:50000")
	Rpc      string // TCP address to serve RPC for fuzzer processes (optional, only useful for type "none")
	Workdir  string
	Vmlinux  string
	Kernel   string // e.g. arch/x86/boot/bzImage
	Tag      string // arbitrary optional tag that is saved along with crash reports (e.g. kernel branch/commit)
	Cmdline  string // kernel command line
	Image    string // linux image for VMs
	Initrd   string // linux initial ramdisk. (optional)
	Cpu      int    // number of VM CPUs
	Mem      int    // amount of VM memory in MBs
	Sshkey   string // root ssh key for the image
	Bin      string // qemu/lkvm binary name
	Bin_Args string // additional command line arguments for qemu/lkvm binary
	Debug    bool   // dump all VM output to console
	Output   string // one of stdout/dmesg/file (useful only for local VM)

	Hub_Addr string
	Hub_Key  string

	Syzkaller string   // path to syzkaller checkout (syz-manager will look for binaries in bin subdir)
	Type      string   // VM type (qemu, kvm, local)
	Count     int      // number of VMs (don't secify for adb, instead specify devices)
	Devices   []string // device IDs for adb
	Procs     int      // number of parallel processes inside of every VM

	Sandbox string // type of sandbox to use during fuzzing:
	// "none": don't do anything special (has false positives, e.g. due to killing init)
	// "setuid": impersonate into user nobody (65534), default
	// "namespace": create a new namespace for fuzzer using CLONE_NEWNS/CLONE_NEWNET/CLONE_NEWPID/etc,
	//	requires building kernel with CONFIG_NAMESPACES, CONFIG_UTS_NS, CONFIG_USER_NS, CONFIG_PID_NS and CONFIG_NET_NS.

	Machine_Type string // GCE machine type (e.g. "n1-highcpu-2")

	Cover     bool // use kcov coverage (default: true)
	Leak      bool // do memory leak checking
	Reproduce bool // reproduce, localize and minimize crashers (on by default)

	Enable_Syscalls  []string
	Disable_Syscalls []string
	Suppressions     []string // don't save reports matching these regexps, but reboot VM after them
	Ignores          []string // completely ignore reports matching these regexps (don't save nor reboot)

	// Implementation details beyond this point.
	ParsedSuppressions []*regexp.Regexp `json:"-"`
	ParsedIgnores      []*regexp.Regexp `json:"-"`
}

func Parse(filename string) (*Config, map[int]bool, error) {
	if filename == "" {
		return nil, nil, fmt.Errorf("supply config in -config flag")
	}
	data, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to read config file: %v", err)
	}
	return parse(data)
}

func parse(data []byte) (*Config, map[int]bool, error) {
	unknown, err := checkUnknownFields(data)
	if err != nil {
		return nil, nil, err
	}
	if unknown != "" {
		return nil, nil, fmt.Errorf("unknown field '%v' in config", unknown)
	}
	cfg := new(Config)
	cfg.Cover = true
	cfg.Reproduce = true
	cfg.Sandbox = "setuid"
	if err := json.Unmarshal(data, cfg); err != nil {
		return nil, nil, fmt.Errorf("failed to parse config file: %v", err)
	}
	if _, err := os.Stat(filepath.Join(cfg.Syzkaller, "bin/syz-fuzzer")); err != nil {
		return nil, nil, fmt.Errorf("bad config syzkaller param: can't find bin/syz-fuzzer")
	}
	if _, err := os.Stat(filepath.Join(cfg.Syzkaller, "bin/syz-executor")); err != nil {
		return nil, nil, fmt.Errorf("bad config syzkaller param: can't find bin/syz-executor")
	}
	if cfg.Http == "" {
		return nil, nil, fmt.Errorf("config param http is empty")
	}
	if cfg.Workdir == "" {
		return nil, nil, fmt.Errorf("config param workdir is empty")
	}
	if cfg.Vmlinux == "" {
		return nil, nil, fmt.Errorf("config param vmlinux is empty")
	}
	if cfg.Type == "" {
		return nil, nil, fmt.Errorf("config param type is empty")
	}
	switch cfg.Type {
	case "none":
		if cfg.Count != 0 {
			return nil, nil, fmt.Errorf("invalid config param count: %v, type \"none\" does not support param count", cfg.Count)
		}
		if cfg.Rpc == "" {
			return nil, nil, fmt.Errorf("config param rpc is empty (required for type \"none\")")
		}
		if len(cfg.Devices) != 0 {
			return nil, nil, fmt.Errorf("type %v does not support devices param", cfg.Type)
		}
	case "adb":
		if cfg.Count != 0 {
			return nil, nil, fmt.Errorf("don't specify count for adb, instead specify devices")
		}
		if len(cfg.Devices) == 0 {
			return nil, nil, fmt.Errorf("specify at least 1 adb device")
		}
		cfg.Count = len(cfg.Devices)
	case "gce":
		if cfg.Machine_Type == "" {
			return nil, nil, fmt.Errorf("machine_type parameter is empty (required for gce)")
		}
		fallthrough
	default:
		if cfg.Count <= 0 || cfg.Count > 1000 {
			return nil, nil, fmt.Errorf("invalid config param count: %v, want (1, 1000]", cfg.Count)
		}
		if len(cfg.Devices) != 0 {
			return nil, nil, fmt.Errorf("type %v does not support devices param", cfg.Type)
		}
	}
	if cfg.Rpc == "" {
		cfg.Rpc = "localhost:0"
	}
	if cfg.Procs <= 0 {
		cfg.Procs = 1
	}
	if cfg.Procs > 32 {
		return nil, nil, fmt.Errorf("config param procs has higher value '%v' then the max supported 32", cfg.Procs)
	}
	if cfg.Output == "" {
		if cfg.Type == "local" {
			cfg.Output = "none"
		} else {
			cfg.Output = "stdout"
		}
	}
	switch cfg.Output {
	case "none", "stdout", "dmesg", "file":
	default:
		return nil, nil, fmt.Errorf("config param output must contain one of none/stdout/dmesg/file")
	}
	switch cfg.Sandbox {
	case "none", "setuid", "namespace":
	default:
		return nil, nil, fmt.Errorf("config param sandbox must contain one of none/setuid/namespace")
	}

	wd, err := os.Getwd()
	if err != nil {
		return nil, nil, fmt.Errorf("failed to get wd: %v", err)
	}
	abs := func(path string) string {
		if path != "" && !filepath.IsAbs(path) {
			path = filepath.Join(wd, path)
		}
		return path
	}
	cfg.Workdir = abs(cfg.Workdir)
	cfg.Kernel = abs(cfg.Kernel)
	cfg.Vmlinux = abs(cfg.Vmlinux)
	cfg.Syzkaller = abs(cfg.Syzkaller)
	cfg.Initrd = abs(cfg.Initrd)
	cfg.Sshkey = abs(cfg.Sshkey)
	cfg.Bin = abs(cfg.Bin)

	syscalls, err := parseSyscalls(cfg)
	if err != nil {
		return nil, nil, err
	}

	if err := parseSuppressions(cfg); err != nil {
		return nil, nil, err
	}

	return cfg, syscalls, nil
}

func parseSyscalls(cfg *Config) (map[int]bool, error) {
	match := func(call *sys.Call, str string) bool {
		if str == call.CallName || str == call.Name {
			return true
		}
		if len(str) > 1 && str[len(str)-1] == '*' && strings.HasPrefix(call.Name, str[:len(str)-1]) {
			return true
		}
		return false
	}

	syscalls := make(map[int]bool)
	if len(cfg.Enable_Syscalls) != 0 {
		for _, c := range cfg.Enable_Syscalls {
			n := 0
			for _, call := range sys.Calls {
				if match(call, c) {
					syscalls[call.ID] = true
					n++
				}
			}
			if n == 0 {
				return nil, fmt.Errorf("unknown enabled syscall: %v", c)
			}
		}
	} else {
		for _, call := range sys.Calls {
			syscalls[call.ID] = true
		}
	}
	for _, c := range cfg.Disable_Syscalls {
		n := 0
		for _, call := range sys.Calls {
			if match(call, c) {
				delete(syscalls, call.ID)
				n++
			}
		}
		if n == 0 {
			return nil, fmt.Errorf("unknown disabled syscall: %v", c)
		}
	}
	// mmap is used to allocate memory.
	syscalls[sys.CallMap["mmap"].ID] = true

	return syscalls, nil
}

func parseSuppressions(cfg *Config) error {
	// Add some builtin suppressions.
	supp := append(cfg.Suppressions, []string{
		"panic: failed to start executor binary",
		"panic: executor failed: pthread_create failed",
		"panic: failed to create temp dir",
		"fatal error: runtime: out of memory",
		"fatal error: runtime: cannot allocate memory",
		"fatal error: unexpected signal during runtime execution", // presubmably OOM turned into SIGBUS
		"Out of memory: Kill process .* \\(syz-fuzzer\\)",
		"lowmemorykiller: Killing 'syz-fuzzer'",
		//"INFO: lockdep is turned off", // printed by some sysrq that dumps scheduler state, but also on all lockdep reports
	}...)
	for _, s := range supp {
		re, err := regexp.Compile(s)
		if err != nil {
			return fmt.Errorf("failed to compile suppression '%v': %v", s, err)
		}
		cfg.ParsedSuppressions = append(cfg.ParsedSuppressions, re)
	}
	for _, ignore := range cfg.Ignores {
		re, err := regexp.Compile(ignore)
		if err != nil {
			return fmt.Errorf("failed to compile ignore '%v': %v", ignore, err)
		}
		cfg.ParsedIgnores = append(cfg.ParsedIgnores, re)
	}
	return nil
}

func CreateVMConfig(cfg *Config, index int) (*vm.Config, error) {
	if index < 0 || index >= cfg.Count {
		return nil, fmt.Errorf("invalid VM index %v (count %v)", index, cfg.Count)
	}
	workdir, err := fileutil.ProcessTempDir(cfg.Workdir)
	if err != nil {
		return nil, fmt.Errorf("failed to create instance temp dir: %v", err)
	}
	vmCfg := &vm.Config{
		Name:        fmt.Sprintf("%v-%v-%v", cfg.Type, cfg.Name, index),
		Index:       index,
		Workdir:     workdir,
		Bin:         cfg.Bin,
		BinArgs:     cfg.Bin_Args,
		Kernel:      cfg.Kernel,
		Cmdline:     cfg.Cmdline,
		Image:       cfg.Image,
		Initrd:      cfg.Initrd,
		Sshkey:      cfg.Sshkey,
		Executor:    filepath.Join(cfg.Syzkaller, "bin", "syz-executor"),
		Cpu:         cfg.Cpu,
		Mem:         cfg.Mem,
		Debug:       cfg.Debug,
		MachineType: cfg.Machine_Type,
	}
	if len(cfg.Devices) != 0 {
		vmCfg.Device = cfg.Devices[index]
	}
	return vmCfg, nil
}

func checkUnknownFields(data []byte) (string, error) {
	// While https://github.com/golang/go/issues/15314 is not resolved
	// we don't have a better way than to enumerate all known fields.
	var fields = []string{
		"Name",
		"Http",
		"Rpc",
		"Workdir",
		"Vmlinux",
		"Kernel",
		"Tag",
		"Cmdline",
		"Image",
		"Cpu",
		"Mem",
		"Sshkey",
		"Bin",
		"Bin_Args",
		"Debug",
		"Output",
		"Hub_Addr",
		"Hub_Key",
		"Syzkaller",
		"Type",
		"Count",
		"Devices",
		"Procs",
		"Cover",
		"Reproduce",
		"Sandbox",
		"Leak",
		"Enable_Syscalls",
		"Disable_Syscalls",
		"Suppressions",
		"Ignores",
		"Initrd",
		"Machine_Type",
	}
	f := make(map[string]interface{})
	if err := json.Unmarshal(data, &f); err != nil {
		return "", fmt.Errorf("failed to parse config file: %v", err)
	}
	for k := range f {
		ok := false
		for _, k1 := range fields {
			if strings.ToLower(k) == strings.ToLower(k1) {
				ok = true
				break
			}
		}
		if !ok {
			return k, nil
		}
	}
	return "", nil
}
<|endoftext|>


<commit_before>package config

import (
	"flag"

	"github.com/vharitonsky/iniflags"
)

var (
	Name     = flag.String("name", "tad", "Nick to use in IRC")
	Server   = flag.String("server", "127.0.0.1:6668", "Host:Port to connect to")
	Channels = flag.String("chan", "#tad", "Channels to join")
	Ssl      = flag.Bool("ssl", false, "Use SSL/TLS")
	Listen   = flag.Bool("listenChannel", false, "Listen for command on public channels")
	HostInfo = flag.String("hostInfo", "./data/va_host_info_report.json", "Path to host info report")
	Promises = flag.String("promises", "./data/promises.csv", "Path to promises report")
	Classes  = flag.String("classes", "./data/classes.txt", "Path to classes report")
)

func init() {
	iniflags.Parse()
}
<commit_msg>Fix paths to data files<commit_after>package config

import (
	"flag"

	"github.com/vharitonsky/iniflags"
)

var (
	Name     = flag.String("name", "tad", "Nick to use in IRC")
	Server   = flag.String("server", "127.0.0.1:6668", "Host:Port to connect to")
	Channels = flag.String("chan", "#tad", "Channels to join")
	Ssl      = flag.Bool("ssl", false, "Use SSL/TLS")
	Listen   = flag.Bool("listenChannel", false, "Listen for command on public channels")
	HostInfo = flag.String("hostInfo", "./data/va_host_info_report.json", "Path to host info report")
	Promises = flag.String("promises", "./data/promises_outcome.log", "Path to promises report")
	Classes  = flag.String("classes", "./data/classes.log", "Path to classes report")
)

func init() {
	iniflags.Parse()
}
<|endoftext|>


<commit_before>package config

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strings"
)

const (
	// File is the default name of the JSON file where the config written.
	// The user can pass an alternate filename when using the CLI.
	// TODO: rename to .exercism.json
	File = ".exercism.go"
	// Host is the default hostname for fetching problems and submitting exercises.
	// TODO: We need to operate against two hosts (one for problems and one for submissions),
	// or define a proxy that both APIs can go through.
	Host = "http://exercism.io"
	// DemoDirname is the default directory to download problems to.
	DemoDirname = "exercism-demo"
)

// Config represents the settings for particular user.
// This defines both the auth for talking to the API, as well as
// where to put problems that get downloaded.
type Config struct {
	GithubUsername    string `json:"githubUsername"`
	APIKey            string `json:"apiKey"`
	ExercismDirectory string `json:"exercismDirectory"`
	Hostname          string `json:"hostname"`
}

// ToFile writes a Config to a JSON file.
func (c Config) ToFile(path string) error {
	if path == "" {
		path = WithDefaultPath(path)
		err := NormalizeConfig(HomeDir())
		if err != nil {
			return err
		}
	}

	f, err := os.Create(path) // truncates existing file if it exists
	if err != nil {
		return err
	}
	defer f.Close()

	err = c.Encode(f)
	if err != nil {
		return err
	}
	return nil
}

// FromFile loads a Config object from a JSON file.
func FromFile(path string) (*Config, error) {
	if path == "" {
		path = WithDefaultPath(path)
		err := NormalizeConfig(HomeDir())
		if err != nil {
			return nil, err
		}
	}

	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	return Decode(f)
}

// Encode writes a Config into JSON format.
func (c *Config) Encode(w io.Writer) error {
	c.sanitize()
	e := json.NewEncoder(w)
	return e.Encode(c)
}

// Decode loads a Config from JSON format.
func Decode(r io.Reader) (*Config, error) {
	d := json.NewDecoder(r)
	var c *Config
	err := d.Decode(&c)
	if err != nil {
		return c, err
	}
	c.sanitize()

	return c, err
}

func WithDefaultPath(p string) string {
	if p == "" {
		return Filename(HomeDir())
	} else {
		return p
	}
}

// HomeDir return's the user's canonical home directory.
// See: http://stackoverflow.com/questions/7922270/obtain-users-home-directory
// we can't cross compile using cgo and use user.Current()
func HomeDir() string {
	if runtime.GOOS == "windows" {
		home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
		if home == "" {
			home = os.Getenv("USERPROFILE")
		}
		return home
	}

	return os.Getenv("HOME")
}

// Filename is the name of the JSON file containing the user's config.
func Filename(dir string) string {
	return fmt.Sprintf("%s/%s", dir, File)
}

// Demo is a default configuration for unauthenticated users.
func Demo() (*Config, error) {
	demoDir, err := demoDirectory()
	if err != nil {
		return nil, err
	}
	return &Config{
		Hostname:          Host,
		APIKey:            "",
		ExercismDirectory: demoDir,
	}, err
}

// ReplaceTilde replaces the short-hand home path with the absolute path.
func ReplaceTilde(oldPath string) string {
	return strings.Replace(oldPath, "~/", HomeDir()+"/", 1)
}

func NormalizeConfig(path string) error {
	var err error

	fi, err := os.Stat(path)
	if err != nil {
		return err
	}
	if !fi.IsDir() {
		return errors.New("expected path to be a directory")
	}

	currentPath := filepath.Join(path, ".exercism.json")
	oldPath := filepath.Join(path, ".exercism.go")

	// Do nothing if we already have a current config file
	_, err = os.Stat(currentPath)
	if !os.IsNotExist(err) {
		return nil
	}

	// Do nothing if we have no old file to rename
	_, err = os.Stat(oldPath)
	if os.IsNotExist(err) {
		return nil
	}

	err = os.Rename(oldPath, currentPath)
	if err != nil {
		return err
	}

	return nil
}

func demoDirectory() (string, error) {
	dir, err := os.Getwd()
	if err != nil {
		return "", err
	}
	return filepath.Join(dir, DemoDirname), nil
}

func (c *Config) sanitize() {
	c.GithubUsername = sanitizeField(c.GithubUsername)
	c.APIKey = sanitizeField(c.APIKey)
	c.ExercismDirectory = sanitizeField(c.ExercismDirectory)
	c.Hostname = sanitizeField(c.Hostname)
}

func sanitizeField(v string) string {
	return strings.TrimSpace(v)
}
<commit_msg>let the user know when you're renaming their files<commit_after>package config

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strings"
)

const (
	// File is the default name of the JSON file where the config written.
	// The user can pass an alternate filename when using the CLI.
	// TODO: rename to .exercism.json
	File = ".exercism.go"
	// Host is the default hostname for fetching problems and submitting exercises.
	// TODO: We need to operate against two hosts (one for problems and one for submissions),
	// or define a proxy that both APIs can go through.
	Host = "http://exercism.io"
	// DemoDirname is the default directory to download problems to.
	DemoDirname = "exercism-demo"
)

// Config represents the settings for particular user.
// This defines both the auth for talking to the API, as well as
// where to put problems that get downloaded.
type Config struct {
	GithubUsername    string `json:"githubUsername"`
	APIKey            string `json:"apiKey"`
	ExercismDirectory string `json:"exercismDirectory"`
	Hostname          string `json:"hostname"`
}

// ToFile writes a Config to a JSON file.
func (c Config) ToFile(path string) error {
	if path == "" {
		path = WithDefaultPath(path)
		err := NormalizeConfig(HomeDir())
		if err != nil {
			return err
		}
	}

	f, err := os.Create(path) // truncates existing file if it exists
	if err != nil {
		return err
	}
	defer f.Close()

	err = c.Encode(f)
	if err != nil {
		return err
	}
	return nil
}

// FromFile loads a Config object from a JSON file.
func FromFile(path string) (*Config, error) {
	if path == "" {
		path = WithDefaultPath(path)
		err := NormalizeConfig(HomeDir())
		if err != nil {
			return nil, err
		}
	}

	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	return Decode(f)
}

// Encode writes a Config into JSON format.
func (c *Config) Encode(w io.Writer) error {
	c.sanitize()
	e := json.NewEncoder(w)
	return e.Encode(c)
}

// Decode loads a Config from JSON format.
func Decode(r io.Reader) (*Config, error) {
	d := json.NewDecoder(r)
	var c *Config
	err := d.Decode(&c)
	if err != nil {
		return c, err
	}
	c.sanitize()

	return c, err
}

func WithDefaultPath(p string) string {
	if p == "" {
		return Filename(HomeDir())
	} else {
		return p
	}
}

// HomeDir return's the user's canonical home directory.
// See: http://stackoverflow.com/questions/7922270/obtain-users-home-directory
// we can't cross compile using cgo and use user.Current()
func HomeDir() string {
	if runtime.GOOS == "windows" {
		home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
		if home == "" {
			home = os.Getenv("USERPROFILE")
		}
		return home
	}

	return os.Getenv("HOME")
}

// Filename is the name of the JSON file containing the user's config.
func Filename(dir string) string {
	return fmt.Sprintf("%s/%s", dir, File)
}

// Demo is a default configuration for unauthenticated users.
func Demo() (*Config, error) {
	demoDir, err := demoDirectory()
	if err != nil {
		return nil, err
	}
	return &Config{
		Hostname:          Host,
		APIKey:            "",
		ExercismDirectory: demoDir,
	}, err
}

// ReplaceTilde replaces the short-hand home path with the absolute path.
func ReplaceTilde(oldPath string) string {
	return strings.Replace(oldPath, "~/", HomeDir()+"/", 1)
}

func NormalizeConfig(path string) error {
	var err error

	fi, err := os.Stat(path)
	if err != nil {
		return err
	}
	if !fi.IsDir() {
		return errors.New("expected path to be a directory")
	}

	currentPath := filepath.Join(path, ".exercism.json")
	oldPath := filepath.Join(path, ".exercism.go")

	// Do nothing if we already have a current config file
	_, err = os.Stat(currentPath)
	if !os.IsNotExist(err) {
		return nil
	}

	// Do nothing if we have no old file to rename
	_, err = os.Stat(oldPath)
	if os.IsNotExist(err) {
		return nil
	}

	err = os.Rename(oldPath, currentPath)
	if err == nil {
		fmt.Printf("renamed %s to %s\n", oldPath, currentPath)
	} else {
		return err
	}

	return nil
}

func demoDirectory() (string, error) {
	dir, err := os.Getwd()
	if err != nil {
		return "", err
	}
	return filepath.Join(dir, DemoDirname), nil
}

func (c *Config) sanitize() {
	c.GithubUsername = sanitizeField(c.GithubUsername)
	c.APIKey = sanitizeField(c.APIKey)
	c.ExercismDirectory = sanitizeField(c.ExercismDirectory)
	c.Hostname = sanitizeField(c.Hostname)
}

func sanitizeField(v string) string {
	return strings.TrimSpace(v)
}
<|endoftext|>


<commit_before>/* Package msgbuffer is a temporary buffer of ADS-B messages.

It uses a strict whitelist approach, so that it can immediately
discard irrelevant messages.

It caches useful data from previous messages, and generates
'composite' output messages with full data (i.e. every output packet
gets a callsign).

When the messages are at risk of getting stale, it will invoke the
supplied callback to flush them.

It contains enough memory housekeeping to be used indefinitely.

Sample usage:

    mb := msgbuffer.NewMsgBuffer()
    mb.MaxMessageAgeSeconds = 0  // How long to wait before flushing; 0==no wait
    mb.FlushFunc = func(msgs []*adsb.CompositeMsg) {
      fmt.Printf("Just flushed %d messages\n", len(msgs))
    }

    myMessages := []adsb.Message{ ... }
    for _,msg := range myMessages {
      mb.Add(msg}
    }

*/
package msgbuffer

import(
	"fmt"
	"time"
	"github.com/skypies/adsb"
)

// {{{ ADSBSender{}

// ADSBSender stores some data that we use to flesh out partial messages
type ADSBSender struct {
	LastSeen          time.Time
	LastGroundSpeed   int64
	LastVerticalSpeed int64
	LastTrack         int64
	LastCallsign      string
	LastSquawk        string
}

func (s ADSBSender)String() string {
	return fmt.Sprintf("[%-7.7s],[%s] % 3dk, % 5df/m, % 3ddeg @ %s [%s]",
		s.LastCallsign, s.LastSquawk, s.LastGroundSpeed, s.LastVerticalSpeed, s.LastTrack, s.LastSeen,
		time.Since(s.LastSeen))
}

// }}}
// {{{ MsgBuffer{}

type MsgBuffer struct {
	FlushFunc func([]*adsb.CompositeMsg)

	MaxMessageAgeSeconds int64   // If we've held a message for more than this, flush the buffer
	MaxQuietTimeSeconds int64 // If an aircraft sends no messages for more than this, remove it
	Senders    map[adsb.IcaoId]*ADSBSender // Which senders we're currently getting data from

	Messages []*adsb.CompositeMsg  // The actual buffer

	lastAgeOut time.Time
}

func (mb MsgBuffer)String() string {
	s := fmt.Sprintf("--{ MsgBuffer (maxage=%d, maxwait=%d) }--\n",
		mb.MaxMessageAgeSeconds, mb.MaxQuietTimeSeconds)
	for k,sender := range mb.Senders { s += fmt.Sprintf(" - %s %s\n", k, sender) }
	for i,m := range mb.Messages { s += fmt.Sprintf("[%02d] %s\n", i, m) }
	return s
}

// }}}

// {{{ NewMsgBuffer

func NewMsgBuffer() *MsgBuffer {
	dumbFunc := func(msgs []*adsb.CompositeMsg) {
		fmt.Printf("MsgBuffer flushed %d messages (default FlushFunc)\n", len(msgs))
	}
	return &MsgBuffer{
		FlushFunc: dumbFunc,
		MaxMessageAgeSeconds: 30,
		MaxQuietTimeSeconds: 360,
		Senders: make(map[adsb.IcaoId]*ADSBSender),
	}
}

// }}}

// {{{ ADSBSender.updateFromMsg

// Some subtype packets have data we don't get in the bulk of position packets (those of subtype:3),
// so just cache their interesting data and inject it into next position packet.
// http://woodair.net/SBS/Article/Barebones42_Socket_Data.htm
func (s *ADSBSender)updateFromMsg(m *adsb.Msg) {
	s.LastSeen = time.Now().UTC()
	if m.SubType == 1 {
		if m.Callsign != ""     { s.LastCallsign      = m.Callsign }

	} else if m.SubType == 2 {
		if m.GroundSpeed != 0   { s.LastGroundSpeed   = m.GroundSpeed }
		if m.Track != 0         { s.LastTrack         = m.Track }

	} else if m.SubType == 4 {
		if m.GroundSpeed != 0   { s.LastGroundSpeed   = m.GroundSpeed }
		if m.VerticalRate != 0  { s.LastVerticalSpeed = m.VerticalRate }
		if m.Track != 0         { s.LastTrack         = m.Track }

	} else if m.SubType == 6 {
		if m.Squawk != ""       { s.LastSquawk        = m.Squawk }
  }
}

// }}}
// {{{ ADSBSender.maybeCreateComposite

// If this message has new position info, *and* we have good backfill, then craft a CompositeMsg.
// Note, we don't wait for squawk info.
func (s *ADSBSender)maybeCreateComposite(m *adsb.Msg) *adsb.CompositeMsg {
	if !m.HasPosition() {
		return nil
	}
	if s.LastGroundSpeed == 0 || s.LastVerticalSpeed == 0 || s.LastTrack == 0 || s.LastCallsign == "" {
		return nil
	}

	cm := adsb.CompositeMsg{Msg:*m}  // Clone the input into the embedded struct

	// Overwrite with cached info (from previous packets), if we don't have it in this packet
	if cm.GroundSpeed == 0  { cm.GroundSpeed  = s.LastGroundSpeed }
	if cm.VerticalRate == 0 { cm.VerticalRate = s.LastVerticalSpeed }
	if cm.Track == 0        { cm.Track        = s.LastTrack }
	if cm.Callsign == ""    { cm.Callsign     = s.LastCallsign }
	if cm.Squawk == ""      { cm.Squawk       = s.LastSquawk }

	return &cm
}

// }}}

// {{{ MsgBuffer.ageOutQuietSenders

func (mb *MsgBuffer)ageOutQuietSenders() (removed int64) {
	if time.Since(mb.lastAgeOut) < time.Second { return } // Only run once per second.
	mb.lastAgeOut = time.Now()

	for id,_ := range mb.Senders {
		if time.Since(mb.Senders[id].LastSeen) >= time.Second * time.Duration(mb.MaxQuietTimeSeconds) {
			delete(mb.Senders, id)
			removed++
		}
	}

	return
}

// }}}
// {{{ MsgBuffer.flush

func (mb *MsgBuffer)flush() {
	msgsToFlush := mb.Messages

	mb.Messages = []*adsb.CompositeMsg{}
	// Now, the only reference to the messages we're flushing is in msgsToFlush

	mb.FlushFunc(msgsToFlush) // Spawn goroutine here ? Or expect caller to handle all that ?
}

// }}}

// {{{ MsgBuffer.Add

// MaybeAdd looks at a new message, and updates the buffer as appropriate.
func (mb *MsgBuffer)Add(m *adsb.Msg) {
	//fmt.Printf(" * %s\n", m)

	mb.ageOutQuietSenders()

	if _,exists := mb.Senders[m.Icao24]; exists == false {
		// Not seen this sender before. If we have position data, start the whitelisting thing. We only
		// Whitelist senders who will eventually send useful info (e.g. position), so wait until we see that.
		if m.HasPosition() {
			mb.Senders[m.Icao24] = &ADSBSender{LastSeen: time.Now().UTC()}
		}
	} else {
		mb.Senders[m.Icao24].updateFromMsg(m) // Pluck out anything interesting
		if composite := mb.Senders[m.Icao24].maybeCreateComposite(m); composite != nil {
			// We have a message to store !!
			mb.Messages = append(mb.Messages, composite)
		}
	}

	if len(mb.Messages) > 0 {
		t := mb.Messages[0].GeneratedTimestampUTC
		if time.Since(t) >= (time.Second * time.Duration(mb.MaxMessageAgeSeconds)) {
			mb.flush()
		}
	}
}

// }}}

// {{{ -------------------------={ E N D }=----------------------------------

// Local variables:
// folded-file: t
// end:

// }}}
<commit_msg>More solid version of the msggbuffer.<commit_after>/* Package msgbuffer is a temporary buffer of ADS-B messages.

It uses a strict whitelist approach, so that it can immediately
discard irrelevant messages.

It caches useful data from previous messages, and generates
'composite' output messages with full data (i.e. every output packet
gets a callsign).

When the messages are at risk of getting stale, it will invoke the
supplied callback to flush them.

It contains enough memory housekeeping to be used indefinitely.

Sample usage:

    mb := msgbuffer.NewMsgBuffer()
    mb.MaxMessageAgeSeconds = 0  // How long to wait before flushing; 0==no wait
    mb.FlushFunc = func(msgs []*adsb.CompositeMsg) {
      fmt.Printf("Just flushed %d messages\n", len(msgs))
    }

    myMessages := []adsb.Message{ ... }
    for _,msg := range myMessages {
      mb.Add(msg}
    }

*/
package msgbuffer

import(
	"fmt"
	"time"
	"github.com/skypies/adsb"
)

// {{{ ADSBSender{}

// ADSBSender stores some data that we use to flesh out partial messages
type ADSBSender struct {
	LastSeen          time.Time
	LastGroundSpeed   int64
	LastVerticalSpeed int64
	LastTrack         int64
	LastCallsign      string
	LastSquawk        string
}

func (s ADSBSender)String() string {
	return fmt.Sprintf("[%-7.7s],[%s] % 3dk, % 5df/m, % 3ddeg @ %s [%s]",
		s.LastCallsign, s.LastSquawk, s.LastGroundSpeed, s.LastVerticalSpeed, s.LastTrack, s.LastSeen,
		time.Since(s.LastSeen))
}

// }}}
// {{{ MsgBuffer{}

type MsgBuffer struct {
	FlushFunc func([]*adsb.CompositeMsg)

	MaxMessageAgeSeconds int64   // If we've held a message for more than this, flush the buffer
	MaxQuietTimeSeconds int64 // If an aircraft sends no messages for more than this, remove it
	Senders    map[adsb.IcaoId]*ADSBSender // Which senders we're currently getting data from

	Messages []*adsb.CompositeMsg  // The actual buffer

	lastAgeOut time.Time
}

func (mb MsgBuffer)String() string {
	s := fmt.Sprintf("--{ MsgBuffer (maxage=%d, maxwait=%d) }--\n",
		mb.MaxMessageAgeSeconds, mb.MaxQuietTimeSeconds)
	for k,sender := range mb.Senders { s += fmt.Sprintf(" - %s %s\n", k, sender) }
	for i,m := range mb.Messages { s += fmt.Sprintf("[%02d] %s\n", i, m) }
	return s
}

// }}}

// {{{ NewMsgBuffer

func NewMsgBuffer() *MsgBuffer {
	dumbFunc := func(msgs []*adsb.CompositeMsg) {
		fmt.Printf("MsgBuffer flushed %d messages (default FlushFunc)\n", len(msgs))
	}
	return &MsgBuffer{
		FlushFunc: dumbFunc,
		MaxMessageAgeSeconds: 30,
		MaxQuietTimeSeconds: 360,
		Senders: make(map[adsb.IcaoId]*ADSBSender),
	}
}

// }}}

// {{{ ADSBSender.updateFromMsg

// Some subtype packets have data we don't get in the bulk of position packets (those of subtype:3),
// so just cache their interesting data and inject it into next position packet.
// http://woodair.net/SBS/Article/Barebones42_Socket_Data.htm
func (s *ADSBSender)updateFromMsg(m *adsb.Msg) {
	s.LastSeen = time.Now().UTC()
	if m.SubType == 1 {
		if m.Callsign != ""     { s.LastCallsign      = m.Callsign }

	} else if m.SubType == 2 {
		if m.GroundSpeed != 0   { s.LastGroundSpeed   = m.GroundSpeed }
		if m.Track != 0         { s.LastTrack         = m.Track }

	} else if m.SubType == 4 {
		if m.GroundSpeed != 0   { s.LastGroundSpeed   = m.GroundSpeed }
		if m.VerticalRate != 0  { s.LastVerticalSpeed = m.VerticalRate }
		if m.Track != 0         { s.LastTrack         = m.Track }

	} else if m.SubType == 6 {
		if m.Squawk != ""       { s.LastSquawk        = m.Squawk }
  }
}

// }}}
// {{{ ADSBSender.maybeCreateComposite

// If this message has new position info, *and* we have good backfill, then craft a CompositeMsg.
// Note, we don't wait for squawk info.
func (s *ADSBSender)maybeCreateComposite(m *adsb.Msg) *adsb.CompositeMsg {
	if !m.HasPosition() {
		return nil
	}
	if s.LastGroundSpeed == 0 || s.LastVerticalSpeed == 0 || s.LastTrack == 0 || s.LastCallsign == "" {
		return nil
	}

	cm := adsb.CompositeMsg{Msg:*m}  // Clone the input into the embedded struct

	// Overwrite with cached info (from previous packets), if we don't have it in this packet
	if cm.GroundSpeed == 0  { cm.GroundSpeed  = s.LastGroundSpeed }
	if cm.VerticalRate == 0 { cm.VerticalRate = s.LastVerticalSpeed }
	if cm.Track == 0        { cm.Track        = s.LastTrack }
	if cm.Callsign == ""    { cm.Callsign     = s.LastCallsign }
	if cm.Squawk == ""      { cm.Squawk       = s.LastSquawk }

	return &cm
}

// }}}

// {{{ MsgBuffer.ageOutQuietSenders

func (mb *MsgBuffer)ageOutQuietSenders() (removed int64) {
	if time.Since(mb.lastAgeOut) < time.Second { return } // Only run once per second.
	mb.lastAgeOut = time.Now()

	for id,_ := range mb.Senders {
		if time.Since(mb.Senders[id].LastSeen) >= time.Second * time.Duration(mb.MaxQuietTimeSeconds) {
			delete(mb.Senders, id)
			removed++
		}
	}

	return
}

// }}}
// {{{ MsgBuffer.flush

func (mb *MsgBuffer)flush() {
	msgsToFlush := mb.Messages

	mb.Messages = []*adsb.CompositeMsg{}
	// Now, the only reference to the messages we're flushing is in msgsToFlush

	mb.FlushFunc(msgsToFlush) // Spawn goroutine here ? Or expect caller to handle all that ?
}

// }}}

// {{{ MsgBuffer.Add

// MaybeAdd looks at a new message, and updates the buffer as appropriate.
func (mb *MsgBuffer)Add(m *adsb.Msg) {

	mb.ageOutQuietSenders()

	if _,exists := mb.Senders[m.Icao24]; exists == false {
		// Not seen this sender before. If we have position data, start the whitelisting thing. We only
		// Whitelist senders who will eventually send useful info (e.g. position), so wait until we see that.
		if m.HasPosition() {
			mb.Senders[m.Icao24] = &ADSBSender{LastSeen: time.Now().UTC()}
		}
	} else {
		mb.Senders[m.Icao24].updateFromMsg(m) // Pluck out anything interesting
		if composite := mb.Senders[m.Icao24].maybeCreateComposite(m); composite != nil {
			// We have a message to store !!
			mb.Messages = append(mb.Messages, composite)
		}
	}

	if len(mb.Messages) > 0 {
		t := mb.Messages[0].GeneratedTimestampUTC
		if time.Since(t) >= (time.Second * time.Duration(mb.MaxMessageAgeSeconds)) {
			mb.flush()
		}
	}
}

// }}}

// {{{ -------------------------={ E N D }=----------------------------------

// Local variables:
// folded-file: t
// end:

// }}}
<|endoftext|>


<commit_before>// Package mysql_defaults_file provides a way of accessing MySQL via a defaults-file.
package mysql_defaults_file

import (
	"database/sql"
	"errors"
	go_ini "github.com/vaughan0/go-ini" // not sure what to do with dashes in names
	"log"
	"os"
)

// convert ~ to $HOME
func convertFilename(filename string) string {
	for i := range filename {
		if filename[i] == '~' {
			filename = filename[:i] + os.Getenv("HOME") + filename[i+1:]
			break
		}
	}

	return filename
}

// Read the given defaults file and return the different parameter values as a map.
func defaultsFileComponents(defaultsFile string) map[string]string {
	defaultsFile = convertFilename(defaultsFile)

	components := make(map[string]string)

	i, err := go_ini.LoadFile(defaultsFile)
	if err != nil {
		log.Fatal("Could not load ini file", err)
	}
	section := i.Section("client")

	user, ok := section["user"]
	if ok {
		components["user"] = user
	}
	password, ok := section["password"]
	if ok {
		components["password"] = password
	}
	socket, ok := section["socket"]
	if ok {
		components["socket"] = socket
	}
	host, ok := section["host"]
	if ok {
		components["host"] = host
	}
	port, ok := section["port"]
	if ok {
		components["port"] = port
	}
	database, ok := section["database"]
	if ok {
		components["database"] = database
	}

	return components
}

// BuildDSN builds the dsn we're going to use to connect with based on a
// parameter / value string map and return the dsn as a string.
func BuildDSN(components map[string]string, database string) string {
	dsn := ""

	// USER
	_, ok := components["user"]
	if ok {
		dsn = components["user"]
	} else {
		dsn = os.Getenv("USER")
	}
	// PASSWORD
	_, ok = components["password"]
	if ok {
		dsn += ":" + components["password"]
	}

	// SOCKET or HOST? SOCKET TAKES PRECEDENCE if both defined.
	_, okSocket := components["socket"]
	_, okHost := components["host"]
	if okSocket || okHost {
		if okSocket {
			dsn += "@unix(" + components["socket"] + ")/"
		} else {
			hostPort := components["host"]
			_, ok := components["port"]
			if ok {
				hostPort += ":" + components["port"] // stored as string so no need to convert
			} else {
				hostPort += ":3306" // we always need _some_ port so if we don't provide one assume MySQL's default port
			}

			dsn += "@tcp(" + hostPort + ")/"
		}
	} else {
		dsn += "@/" // but I'm guessing here.
	}

	if len(database) > 0 {
		dsn += database
	} else {
		_, ok := components["database"]
		if ok {
			dsn += components["database"]
		}
	}

	//	fmt.Println("final dsn from defaults file:", dsn )
	return dsn
}

// OpenUsingDefaultsFile opens a connection only using a defaults file
func OpenUsingDefaultsFile(sqlDriver string, defaultsFile string, database string) (*sql.DB, error) {
	if defaultsFile == "" {
		defaultsFile = "~/.my.cnf"
	}

	newDSN := BuildDSN(defaultsFileComponents(defaultsFile), database)

	return sql.Open(sqlDriver, newDSN)
}

// OpenUsingEnvironment will assume MYSQL_DSN is set and use that value for connecting.
func OpenUsingEnvironment(sqlDriver string) (*sql.DB, error) {
	if os.Getenv("MYSQL_DSN") == "" {
		return nil, errors.New("MYSQL_DSN not set or empty")
	}

	return sql.Open(sqlDriver, os.Getenv("MYSQL_DSN"))
}
<commit_msg>Remove unneeded go_ini<commit_after>// Package mysql_defaults_file provides a way of accessing MySQL via a defaults-file.
package mysql_defaults_file

import (
	"database/sql"
	"errors"
	"github.com/vaughan0/go-ini"
	"log"
	"os"
)

// convert ~ to $HOME
func convertFilename(filename string) string {
	for i := range filename {
		if filename[i] == '~' {
			filename = filename[:i] + os.Getenv("HOME") + filename[i+1:]
			break
		}
	}

	return filename
}

// Read the given defaults file and return the different parameter values as a map.
func defaultsFileComponents(defaultsFile string) map[string]string {
	defaultsFile = convertFilename(defaultsFile)

	components := make(map[string]string)

	i, err := ini.LoadFile(defaultsFile)
	if err != nil {
		log.Fatal("Could not load ini file", err)
	}
	section := i.Section("client")

	user, ok := section["user"]
	if ok {
		components["user"] = user
	}
	password, ok := section["password"]
	if ok {
		components["password"] = password
	}
	socket, ok := section["socket"]
	if ok {
		components["socket"] = socket
	}
	host, ok := section["host"]
	if ok {
		components["host"] = host
	}
	port, ok := section["port"]
	if ok {
		components["port"] = port
	}
	database, ok := section["database"]
	if ok {
		components["database"] = database
	}

	return components
}

// BuildDSN builds the dsn we're going to use to connect with based on a
// parameter / value string map and return the dsn as a string.
func BuildDSN(components map[string]string, database string) string {
	dsn := ""

	// USER
	_, ok := components["user"]
	if ok {
		dsn = components["user"]
	} else {
		dsn = os.Getenv("USER")
	}
	// PASSWORD
	_, ok = components["password"]
	if ok {
		dsn += ":" + components["password"]
	}

	// SOCKET or HOST? SOCKET TAKES PRECEDENCE if both defined.
	_, okSocket := components["socket"]
	_, okHost := components["host"]
	if okSocket || okHost {
		if okSocket {
			dsn += "@unix(" + components["socket"] + ")/"
		} else {
			hostPort := components["host"]
			_, ok := components["port"]
			if ok {
				hostPort += ":" + components["port"] // stored as string so no need to convert
			} else {
				hostPort += ":3306" // we always need _some_ port so if we don't provide one assume MySQL's default port
			}

			dsn += "@tcp(" + hostPort + ")/"
		}
	} else {
		dsn += "@/" // but I'm guessing here.
	}

	if len(database) > 0 {
		dsn += database
	} else {
		_, ok := components["database"]
		if ok {
			dsn += components["database"]
		}
	}

	//	fmt.Println("final dsn from defaults file:", dsn )
	return dsn
}

// OpenUsingDefaultsFile opens a connection only using a defaults file
func OpenUsingDefaultsFile(sqlDriver string, defaultsFile string, database string) (*sql.DB, error) {
	if defaultsFile == "" {
		defaultsFile = "~/.my.cnf"
	}

	newDSN := BuildDSN(defaultsFileComponents(defaultsFile), database)

	return sql.Open(sqlDriver, newDSN)
}

// OpenUsingEnvironment will assume MYSQL_DSN is set and use that value for connecting.
func OpenUsingEnvironment(sqlDriver string) (*sql.DB, error) {
	if os.Getenv("MYSQL_DSN") == "" {
		return nil, errors.New("MYSQL_DSN not set or empty")
	}

	return sql.Open(sqlDriver, os.Getenv("MYSQL_DSN"))
}
<|endoftext|>


<commit_before>// Copyright 2015 Geofrey Ernest <geofreyernest@live.com>. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

//Package forms contains form utilities used by zedlist
package forms

import (
	"fmt"
	"html/template"
	"net/http"

	"github.com/gernest/gt"
	"github.com/gernest/zedlist/modules/i18n"
	"github.com/gorilla/schema"
)

var (
	msgRequired  = "message_required"
	msgMinLength = "message_min_length"
	msgEmail     = "message_email"
	msgAge       = "message_age"
	msgEqual     = "message_equal"
)

var decoder = schema.NewDecoder()

// Login is the login form
type Login struct {
	Name     string `schema:"username"`
	Password string `schema:"password"`
	CSRF     string `schema:"csrf_token"`
}

func (l *Login) Valid() bool {
	return true
}

// Register is the registration form
type Register struct {
	UserName        string `schema:"username"`
	Email           string `schema:"email"`
	Password        string `schema:"password"`
	ConfirmPassword string `schema:"confirm_password"`
	CSRF            string `schema:"csrf_token"`
}

func (r *Register) Valid() bool {
	return true
}

// Form is contains form validation functions, it support translation
// of error messages. This uses schema.
//
// TODO translate  field names in widgets?
type Form struct {
	tr *gt.Build
	l  *Login
	r  *Register
}

// New returns a new Form with laguage Target set to lang
func New(lang string) *Form {
	l := i18n.CloneLang()
	l.SetTarget(lang)
	return &Form{tr: l}
}

func (f *Form) SetLogin(l *Login) {
	f.l = l
}

func (f *Form) Login() template.HTML {
	return template.HTML(fmt.Sprintf(`

		 %s
		<input type="text" name="username" placeholder="%s">

		 %s
		<input type="password" name="password" placeholder="%s">

	<button class="ui fluid large submit button"> %s</button>
`, f.tr.T("username"), f.tr.T("username_or_email"),
		f.tr.T("password"), f.tr.T("password"),
		f.tr.T("login"),
	))
}

func (f *Form) DecodeLogin(r *http.Request) (*Login, error) {
	if err := r.ParseForm(); err != nil {
		return nil, err
	}
	l := &Login{}
	if err := decoder.Decode(l, r.PostForm); err != nil {
		return nil, err
	}
	return l, nil
}

func (f *Form) SetRegister(r *Register) {
	f.r = r
}

func (f *Form) Register() template.HTML {
	return template.HTML(fmt.Sprintf(`

		 %s
		<input type="text" name="username" placeholder="%s">

		 %s
		<input type="text" name="email" placeholder="%s">

		 %s
		<input type="password" name="password" placeholder="%s">

		 %s
		<input type="password" name="confirm_password" placeholder="%s">

	<button class="ui fluid large submit button"> %s</button>
`, f.tr.T("username"), f.tr.T("username"),
		f.tr.T("email"), f.tr.T("email"),
		f.tr.T("password"), f.tr.T("password"),
		f.tr.T("confirm_password"), f.tr.T("confirm_password"),
		f.tr.T("register"),
	))
}

func (f *Form) DecodeRegister(r *http.Request) (*Register, error) {
	if err := r.ParseForm(); err != nil {
		return nil, err
	}
	l := &Register{}
	if err := decoder.Decode(l, r.PostForm); err != nil {
		return nil, err
	}
	return l, nil
}

func (f *Form) Delete() template.HTML {
	return template.HTML(`

	<input type="text" name="delete" placeholder="Type your username">

	`)
}

func (f *Form) DecodeDelete(r *http.Request) string {
	r.ParseForm()
	return r.Form.Get("delete")
}
<commit_msg>Cleanup Unused code<commit_after>// Copyright 2015 Geofrey Ernest <geofreyernest@live.com>. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

//Package forms contains form utilities used by zedlist
package forms

import (
	"fmt"
	"html/template"
	"net/http"

	"github.com/gernest/gt"
	"github.com/gernest/zedlist/modules/i18n"
	"github.com/gorilla/schema"
)

var (
	msgRequired  = "message_required"
	msgMinLength = "message_min_length"
	msgEmail     = "message_email"
	msgAge       = "message_age"
	msgEqual     = "message_equal"
)

var decoder = schema.NewDecoder()

// Login is the login form
type Login struct {
	Name     string `schema:"username"`
	Password string `schema:"password"`
	CSRF     string `schema:"csrf_token"`
}

func (l *Login) Valid() bool {
	return true
}

// Register is the registration form
type Register struct {
	UserName        string `schema:"username"`
	Email           string `schema:"email"`
	Password        string `schema:"password"`
	ConfirmPassword string `schema:"confirm_password"`
	CSRF            string `schema:"csrf_token"`
}

func (r *Register) Valid() bool {
	return true
}

// Form is contains form validation functions, it support translation
// of error messages. This uses schema.
//
// TODO translate  field names in widgets?
type Form struct {
	tr *gt.Build
	l  *Login
	r  *Register
}

// New returns a new Form with laguage Target set to lang
func New(lang string) *Form {
	l := i18n.CloneLang()
	l.SetTarget(lang)
	return &Form{tr: l}
}

func (f *Form) SetLogin(l *Login) {
	f.l = l
}

func (f *Form) DecodeLogin(r *http.Request) (*Login, error) {
	if err := r.ParseForm(); err != nil {
		return nil, err
	}
	l := &Login{}
	if err := decoder.Decode(l, r.PostForm); err != nil {
		return nil, err
	}
	return l, nil
}

func (f *Form) SetRegister(r *Register) {
	f.r = r
}

func (f *Form) Register() template.HTML {
	return template.HTML(fmt.Sprintf(`

		 %s
		<input type="text" name="username" placeholder="%s">

		 %s
		<input type="text" name="email" placeholder="%s">

		 %s
		<input type="password" name="password" placeholder="%s">

		 %s
		<input type="password" name="confirm_password" placeholder="%s">

	<button class="ui fluid large submit button"> %s</button>
`, f.tr.T("username"), f.tr.T("username"),
		f.tr.T("email"), f.tr.T("email"),
		f.tr.T("password"), f.tr.T("password"),
		f.tr.T("confirm_password"), f.tr.T("confirm_password"),
		f.tr.T("register"),
	))
}

func (f *Form) DecodeRegister(r *http.Request) (*Register, error) {
	if err := r.ParseForm(); err != nil {
		return nil, err
	}
	l := &Register{}
	if err := decoder.Decode(l, r.PostForm); err != nil {
		return nil, err
	}
	return l, nil
}

func (f *Form) Delete() template.HTML {
	return template.HTML(`

	<input type="text" name="delete" placeholder="Type your username">

	`)
}

func (f *Form) DecodeDelete(r *http.Request) string {
	r.ParseForm()
	return r.Form.Get("delete")
}
<|endoftext|>


<commit_before>//+build !netgo,!android

package engo

import (
	"image"
	"io"
	"log"
	"os"
	"os/signal"
	"runtime"
	"syscall"
	"time"

	"engo.io/gl"
	"github.com/go-gl/glfw/v3.1/glfw"
)

var (
	window *glfw.Window
	Gl     *gl.Context

	cursorArrow     *glfw.Cursor
	cursorIBeam     *glfw.Cursor
	cursorCrosshair *glfw.Cursor
	cursorHand      *glfw.Cursor
	cursorHResize   *glfw.Cursor
	cursorVResize   *glfw.Cursor

	headlessWidth             = 800
	headlessHeight            = 800
	gameWidth, gameHeight     float32
	windowWidth, windowHeight float32
	canvasWidth, canvasHeight float32
)

// fatalErr calls log.Fatal with the given error if it is non-nil.
func fatalErr(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

func CreateWindow(title string, width, height int, fullscreen bool, msaa int) {
	err := glfw.Init()
	fatalErr(err)

	cursorArrow = glfw.CreateStandardCursor(int(glfw.ArrowCursor))
	cursorIBeam = glfw.CreateStandardCursor(int(glfw.IBeamCursor))
	cursorCrosshair = glfw.CreateStandardCursor(int(glfw.CrosshairCursor))
	cursorHand = glfw.CreateStandardCursor(int(glfw.HandCursor))
	cursorHResize = glfw.CreateStandardCursor(int(glfw.HResizeCursor))
	cursorVResize = glfw.CreateStandardCursor(int(glfw.VResizeCursor))

	monitor := glfw.GetPrimaryMonitor()

	var mode *glfw.VidMode
	if monitor != nil {
		mode = monitor.GetVideoMode()
	} else {
		// Initialize default values if no monitor is found
		mode = &glfw.VidMode{
			Width:       1,
			Height:      1,
			RedBits:     8,
			GreenBits:   8,
			BlueBits:    8,
			RefreshRate: 60,
		}
	}

	gameWidth = float32(width)
	gameHeight = float32(height)

	if fullscreen {
		width = mode.Width
		height = mode.Height
		glfw.WindowHint(glfw.Decorated, 0)
	} else {
		monitor = nil
	}

	if opts.HeadlessMode {
		glfw.WindowHint(glfw.Visible, glfw.False)
	}

	glfw.WindowHint(glfw.ContextVersionMajor, 2)
	glfw.WindowHint(glfw.ContextVersionMinor, 1)

	glfw.WindowHint(glfw.Samples, msaa)

	window, err = glfw.CreateWindow(width, height, title, monitor, nil)
	fatalErr(err)

	window.MakeContextCurrent()

	if !fullscreen {
		window.SetPos((mode.Width-width)/2, (mode.Height-height)/2)
	}

	SetVSync(opts.VSync)

	// TODO: verify these for retina displays
	width, height = window.GetFramebufferSize()
	windowWidth, windowHeight = float32(width), float32(height)

	Gl = gl.NewContext()

	// TODO: verify these for retina displays
	vp := Gl.GetViewport()
	canvasWidth, windowWidth = float32(vp[2]), float32(vp[3])

	window.SetFramebufferSizeCallback(func(window *glfw.Window, w, h int) {
		width, height = window.GetFramebufferSize()
		Gl.Viewport(0, 0, width, height)

		// TODO: when do we want to handle resizing? and who should deal with it?
		// responder.Resize(w, h)
	})

	window.SetCursorPosCallback(func(window *glfw.Window, x, y float64) {
		Mouse.X, Mouse.Y = float32(x), float32(y)
		Mouse.Action = Move
	})

	window.SetMouseButtonCallback(func(window *glfw.Window, b glfw.MouseButton, a glfw.Action, m glfw.ModifierKey) {
		x, y := window.GetCursorPos()
		Mouse.X, Mouse.Y = float32(x), float32(y)
		// this is only valid because we use an internal structure that is
		// 100% compatible with glfw3.h
		Mouse.Button = MouseButton(b)
		Mouse.Modifier = Modifier(m)

		if a == glfw.Press {
			Mouse.Action = Press
		} else {
			Mouse.Action = Release
		}
	})

	window.SetScrollCallback(func(window *glfw.Window, xoff, yoff float64) {
		Mouse.ScrollX = float32(xoff)
		Mouse.ScrollY = float32(yoff)
	})

	window.SetKeyCallback(func(window *glfw.Window, k glfw.Key, s int, a glfw.Action, m glfw.ModifierKey) {
		key := Key(k)
		if a == glfw.Press {
			Input.keys.Set(key, true)
		} else if a == glfw.Release {
			Input.keys.Set(key, false)
		}
	})

	window.SetSizeCallback(func(w *glfw.Window, widthInt int, heightInt int) {
		message := WindowResizeMessage{
			OldWidth:  int(windowWidth),
			OldHeight: int(windowHeight),
			NewWidth:  widthInt,
			NewHeight: heightInt,
		}

		windowWidth = float32(widthInt)
		windowHeight = float32(heightInt)

		// TODO: verify these for retina displays & verify if needed here
		vp := Gl.GetViewport()
		canvasWidth, canvasHeight = float32(vp[2]), float32(vp[3])

		if !opts.ScaleOnResize {
			gameWidth, gameHeight = float32(widthInt), float32(heightInt)
		}

		Mailbox.Dispatch(message)
	})

	window.SetCharCallback(func(window *glfw.Window, char rune) {
		// TODO: what does this do, when can we use it?
		// it's like KeyCallback, but for specific characters instead of keys...?
		// responder.Type(char)
	})
}

func DestroyWindow() {
	glfw.Terminate()
}

func SetTitle(title string) {
	if opts.HeadlessMode {
		log.Println("Title set to:", title)
	} else {
		window.SetTitle(title)
	}
}

// RunIteration runs one iteration / frame
func RunIteration() {
	// First check for new keypresses
	if !opts.HeadlessMode {
		Input.update()
		glfw.PollEvents()
	}

	// Then update the world and all Systems
	currentWorld.Update(Time.Delta())

	// Lastly, forget keypresses and swap buffers
	if !opts.HeadlessMode {
		// reset values to avoid catching the same "signal" twice
		Mouse.ScrollX, Mouse.ScrollY = 0, 0
		Mouse.Action = Neutral

		window.SwapBuffers()
	}

	Time.Tick()
}

// RunPreparation is called only once, and is called automatically when calling Open
// It is only here for benchmarking in combination with OpenHeadlessNoRun
func RunPreparation(defaultScene Scene) {
	Time = NewClock()

	// Default WorldBounds values
	//WorldBounds.Max = Point{GameWidth(), GameHeight()}
	// TODO: move this to appropriate location

	SetScene(defaultScene, false)
}

func runLoop(defaultScene Scene, headless bool) {
	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt)
	signal.Notify(c, syscall.SIGTERM)
	go func() {
		<-c
		closeEvent()
	}()

	RunPreparation(defaultScene)
	ticker := time.NewTicker(time.Duration(int(time.Second) / opts.FPSLimit))

Outer:
	for {
		select {
		case <-ticker.C:
			RunIteration()
			if closeGame {
				break Outer
			}
			if !headless && window.ShouldClose() {
				closeEvent()
			}
		case <-resetLoopTicker:
			ticker.Stop()
			ticker = time.NewTicker(time.Duration(int(time.Second) / opts.FPSLimit))
		}
	}
	ticker.Stop()
}

func GameWidth() float32 {
	return gameWidth
}

func GameHeight() float32 {
	return gameHeight
}

func CursorPos() (x, y float64) {
	return window.GetCursorPos()
}

func WindowSize() (w, h int) {
	return window.GetSize()
}

func WindowWidth() float32 {
	return windowWidth
}

func WindowHeight() float32 {
	return windowHeight
}

func CanvasWidth() float32 {
	return canvasWidth
}

func CanvasHeight() float32 {
	return canvasHeight
}

// SetCursor sets the pointer of the mouse to the defined standard cursor
func SetCursor(c Cursor) {
	var cur *glfw.Cursor
	switch c {
	case CursorNone:
		cur = nil // just for the documentation, this isn't required
	case CursorArrow:
		cur = cursorArrow
	case CursorCrosshair:
		cur = cursorCrosshair
	case CursorHand:
		cur = cursorHand
	case CursorIBeam:
		cur = cursorIBeam
	case CursorHResize:
		cur = cursorHResize
	case CursorVResize:
		cur = cursorVResize
	}
	window.SetCursor(cur)
}

func SetVSync(enabled bool) {
	opts.VSync = enabled
	if opts.VSync {
		glfw.SwapInterval(1)
	} else {
		glfw.SwapInterval(0)
	}
}

func init() {
	runtime.LockOSThread()

	Dash = Key(glfw.KeyMinus)
	Apostrophe = Key(glfw.KeyApostrophe)
	Semicolon = Key(glfw.KeySemicolon)
	Equals = Key(glfw.KeyEqual)
	Comma = Key(glfw.KeyComma)
	Period = Key(glfw.KeyPeriod)
	Slash = Key(glfw.KeySlash)
	Backslash = Key(glfw.KeyBackslash)
	Backspace = Key(glfw.KeyBackspace)
	Tab = Key(glfw.KeyTab)
	CapsLock = Key(glfw.KeyCapsLock)
	Space = Key(glfw.KeySpace)
	Enter = Key(glfw.KeyEnter)
	Escape = Key(glfw.KeyEscape)
	Insert = Key(glfw.KeyInsert)
	PrintScreen = Key(glfw.KeyPrintScreen)
	Delete = Key(glfw.KeyDelete)
	PageUp = Key(glfw.KeyPageUp)
	PageDown = Key(glfw.KeyPageDown)
	Home = Key(glfw.KeyHome)
	End = Key(glfw.KeyEnd)
	Pause = Key(glfw.KeyPause)
	ScrollLock = Key(glfw.KeyScrollLock)
	ArrowLeft = Key(glfw.KeyLeft)
	ArrowRight = Key(glfw.KeyRight)
	ArrowDown = Key(glfw.KeyDown)
	ArrowUp = Key(glfw.KeyUp)
	LeftBracket = Key(glfw.KeyLeftBracket)
	LeftShift = Key(glfw.KeyLeftShift)
	LeftControl = Key(glfw.KeyLeftControl)
	LeftSuper = Key(glfw.KeyLeftSuper)
	LeftAlt = Key(glfw.KeyLeftAlt)
	RightBracket = Key(glfw.KeyRightBracket)
	RightShift = Key(glfw.KeyRightShift)
	RightControl = Key(glfw.KeyRightControl)
	RightSuper = Key(glfw.KeyRightSuper)
	RightAlt = Key(glfw.KeyRightAlt)
	Zero = Key(glfw.Key0)
	One = Key(glfw.Key1)
	Two = Key(glfw.Key2)
	Three = Key(glfw.Key3)
	Four = Key(glfw.Key4)
	Five = Key(glfw.Key5)
	Six = Key(glfw.Key6)
	Seven = Key(glfw.Key7)
	Eight = Key(glfw.Key8)
	Nine = Key(glfw.Key9)
	F1 = Key(glfw.KeyF1)
	F2 = Key(glfw.KeyF2)
	F3 = Key(glfw.KeyF3)
	F4 = Key(glfw.KeyF4)
	F5 = Key(glfw.KeyF5)
	F6 = Key(glfw.KeyF6)
	F7 = Key(glfw.KeyF7)
	F8 = Key(glfw.KeyF8)
	F9 = Key(glfw.KeyF9)
	F10 = Key(glfw.KeyF10)
	F11 = Key(glfw.KeyF11)
	F12 = Key(glfw.KeyF12)
	A = Key(glfw.KeyA)
	B = Key(glfw.KeyB)
	C = Key(glfw.KeyC)
	D = Key(glfw.KeyD)
	E = Key(glfw.KeyE)
	F = Key(glfw.KeyF)
	G = Key(glfw.KeyG)
	H = Key(glfw.KeyH)
	I = Key(glfw.KeyI)
	J = Key(glfw.KeyJ)
	K = Key(glfw.KeyK)
	L = Key(glfw.KeyL)
	M = Key(glfw.KeyM)
	N = Key(glfw.KeyN)
	O = Key(glfw.KeyO)
	P = Key(glfw.KeyP)
	Q = Key(glfw.KeyQ)
	R = Key(glfw.KeyR)
	S = Key(glfw.KeyS)
	T = Key(glfw.KeyT)
	U = Key(glfw.KeyU)
	V = Key(glfw.KeyV)
	W = Key(glfw.KeyW)
	X = Key(glfw.KeyX)
	Y = Key(glfw.KeyY)
	Z = Key(glfw.KeyZ)
	NumLock = Key(glfw.KeyNumLock)
	NumMultiply = Key(glfw.KeyKPMultiply)
	NumDivide = Key(glfw.KeyKPDivide)
	NumAdd = Key(glfw.KeyKPAdd)
	NumSubtract = Key(glfw.KeyKPSubtract)
	NumZero = Key(glfw.KeyKP0)
	NumOne = Key(glfw.KeyKP1)
	NumTwo = Key(glfw.KeyKP2)
	NumThree = Key(glfw.KeyKP3)
	NumFour = Key(glfw.KeyKP4)
	NumFive = Key(glfw.KeyKP5)
	NumSix = Key(glfw.KeyKP6)
	NumSeven = Key(glfw.KeyKP7)
	NumEight = Key(glfw.KeyKP8)
	NumNine = Key(glfw.KeyKP9)
	NumDecimal = Key(glfw.KeyKPDecimal)
	NumEnter = Key(glfw.KeyKPEnter)
}

func NewImageRGBA(img *image.RGBA) *ImageRGBA {
	return &ImageRGBA{img}
}

type ImageRGBA struct {
	data *image.RGBA
}

func (i *ImageRGBA) Data() interface{} {
	return i.data
}

func (i *ImageRGBA) Width() int {
	return i.data.Rect.Max.X
}

func (i *ImageRGBA) Height() int {
	return i.data.Rect.Max.Y
}

// openFile is the desktop-specific way of opening a file
func openFile(url string) (io.ReadCloser, error) {
	return os.Open(url)
}
<commit_msg>Hotfix for glfw's canvasHeight (#302)<commit_after>//+build !netgo,!android

package engo

import (
	"image"
	"io"
	"log"
	"os"
	"os/signal"
	"runtime"
	"syscall"
	"time"

	"engo.io/gl"
	"github.com/go-gl/glfw/v3.1/glfw"
)

var (
	window *glfw.Window
	Gl     *gl.Context

	cursorArrow     *glfw.Cursor
	cursorIBeam     *glfw.Cursor
	cursorCrosshair *glfw.Cursor
	cursorHand      *glfw.Cursor
	cursorHResize   *glfw.Cursor
	cursorVResize   *glfw.Cursor

	headlessWidth             = 800
	headlessHeight            = 800
	gameWidth, gameHeight     float32
	windowWidth, windowHeight float32
	canvasWidth, canvasHeight float32
)

// fatalErr calls log.Fatal with the given error if it is non-nil.
func fatalErr(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

func CreateWindow(title string, width, height int, fullscreen bool, msaa int) {
	err := glfw.Init()
	fatalErr(err)

	cursorArrow = glfw.CreateStandardCursor(int(glfw.ArrowCursor))
	cursorIBeam = glfw.CreateStandardCursor(int(glfw.IBeamCursor))
	cursorCrosshair = glfw.CreateStandardCursor(int(glfw.CrosshairCursor))
	cursorHand = glfw.CreateStandardCursor(int(glfw.HandCursor))
	cursorHResize = glfw.CreateStandardCursor(int(glfw.HResizeCursor))
	cursorVResize = glfw.CreateStandardCursor(int(glfw.VResizeCursor))

	monitor := glfw.GetPrimaryMonitor()

	var mode *glfw.VidMode
	if monitor != nil {
		mode = monitor.GetVideoMode()
	} else {
		// Initialize default values if no monitor is found
		mode = &glfw.VidMode{
			Width:       1,
			Height:      1,
			RedBits:     8,
			GreenBits:   8,
			BlueBits:    8,
			RefreshRate: 60,
		}
	}

	gameWidth = float32(width)
	gameHeight = float32(height)

	if fullscreen {
		width = mode.Width
		height = mode.Height
		glfw.WindowHint(glfw.Decorated, 0)
	} else {
		monitor = nil
	}

	if opts.HeadlessMode {
		glfw.WindowHint(glfw.Visible, glfw.False)
	}

	glfw.WindowHint(glfw.ContextVersionMajor, 2)
	glfw.WindowHint(glfw.ContextVersionMinor, 1)

	glfw.WindowHint(glfw.Samples, msaa)

	window, err = glfw.CreateWindow(width, height, title, monitor, nil)
	fatalErr(err)

	window.MakeContextCurrent()

	if !fullscreen {
		window.SetPos((mode.Width-width)/2, (mode.Height-height)/2)
	}

	SetVSync(opts.VSync)

	// TODO: verify these for retina displays
	width, height = window.GetFramebufferSize()
	windowWidth, windowHeight = float32(width), float32(height)

	Gl = gl.NewContext()

	// TODO: verify these for retina displays
	vp := Gl.GetViewport()
	canvasWidth, canvasHeight = float32(vp[2]), float32(vp[3])

	window.SetFramebufferSizeCallback(func(window *glfw.Window, w, h int) {
		width, height = window.GetFramebufferSize()
		Gl.Viewport(0, 0, width, height)

		// TODO: when do we want to handle resizing? and who should deal with it?
		// responder.Resize(w, h)
	})

	window.SetCursorPosCallback(func(window *glfw.Window, x, y float64) {
		Mouse.X, Mouse.Y = float32(x), float32(y)
		Mouse.Action = Move
	})

	window.SetMouseButtonCallback(func(window *glfw.Window, b glfw.MouseButton, a glfw.Action, m glfw.ModifierKey) {
		x, y := window.GetCursorPos()
		Mouse.X, Mouse.Y = float32(x), float32(y)
		// this is only valid because we use an internal structure that is
		// 100% compatible with glfw3.h
		Mouse.Button = MouseButton(b)
		Mouse.Modifier = Modifier(m)

		if a == glfw.Press {
			Mouse.Action = Press
		} else {
			Mouse.Action = Release
		}
	})

	window.SetScrollCallback(func(window *glfw.Window, xoff, yoff float64) {
		Mouse.ScrollX = float32(xoff)
		Mouse.ScrollY = float32(yoff)
	})

	window.SetKeyCallback(func(window *glfw.Window, k glfw.Key, s int, a glfw.Action, m glfw.ModifierKey) {
		key := Key(k)
		if a == glfw.Press {
			Input.keys.Set(key, true)
		} else if a == glfw.Release {
			Input.keys.Set(key, false)
		}
	})

	window.SetSizeCallback(func(w *glfw.Window, widthInt int, heightInt int) {
		message := WindowResizeMessage{
			OldWidth:  int(windowWidth),
			OldHeight: int(windowHeight),
			NewWidth:  widthInt,
			NewHeight: heightInt,
		}

		windowWidth = float32(widthInt)
		windowHeight = float32(heightInt)

		// TODO: verify these for retina displays & verify if needed here
		vp := Gl.GetViewport()
		canvasWidth, canvasHeight = float32(vp[2]), float32(vp[3])

		if !opts.ScaleOnResize {
			gameWidth, gameHeight = float32(widthInt), float32(heightInt)
		}

		Mailbox.Dispatch(message)
	})

	window.SetCharCallback(func(window *glfw.Window, char rune) {
		// TODO: what does this do, when can we use it?
		// it's like KeyCallback, but for specific characters instead of keys...?
		// responder.Type(char)
	})
}

func DestroyWindow() {
	glfw.Terminate()
}

func SetTitle(title string) {
	if opts.HeadlessMode {
		log.Println("Title set to:", title)
	} else {
		window.SetTitle(title)
	}
}

// RunIteration runs one iteration / frame
func RunIteration() {
	// First check for new keypresses
	if !opts.HeadlessMode {
		Input.update()
		glfw.PollEvents()
	}

	// Then update the world and all Systems
	currentWorld.Update(Time.Delta())

	// Lastly, forget keypresses and swap buffers
	if !opts.HeadlessMode {
		// reset values to avoid catching the same "signal" twice
		Mouse.ScrollX, Mouse.ScrollY = 0, 0
		Mouse.Action = Neutral

		window.SwapBuffers()
	}

	Time.Tick()
}

// RunPreparation is called only once, and is called automatically when calling Open
// It is only here for benchmarking in combination with OpenHeadlessNoRun
func RunPreparation(defaultScene Scene) {
	Time = NewClock()

	// Default WorldBounds values
	//WorldBounds.Max = Point{GameWidth(), GameHeight()}
	// TODO: move this to appropriate location

	SetScene(defaultScene, false)
}

func runLoop(defaultScene Scene, headless bool) {
	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt)
	signal.Notify(c, syscall.SIGTERM)
	go func() {
		<-c
		closeEvent()
	}()

	RunPreparation(defaultScene)
	ticker := time.NewTicker(time.Duration(int(time.Second) / opts.FPSLimit))

Outer:
	for {
		select {
		case <-ticker.C:
			RunIteration()
			if closeGame {
				break Outer
			}
			if !headless && window.ShouldClose() {
				closeEvent()
			}
		case <-resetLoopTicker:
			ticker.Stop()
			ticker = time.NewTicker(time.Duration(int(time.Second) / opts.FPSLimit))
		}
	}
	ticker.Stop()
}

func GameWidth() float32 {
	return gameWidth
}

func GameHeight() float32 {
	return gameHeight
}

func CursorPos() (x, y float64) {
	return window.GetCursorPos()
}

func WindowSize() (w, h int) {
	return window.GetSize()
}

func WindowWidth() float32 {
	return windowWidth
}

func WindowHeight() float32 {
	return windowHeight
}

func CanvasWidth() float32 {
	return canvasWidth
}

func CanvasHeight() float32 {
	return canvasHeight
}

// SetCursor sets the pointer of the mouse to the defined standard cursor
func SetCursor(c Cursor) {
	var cur *glfw.Cursor
	switch c {
	case CursorNone:
		cur = nil // just for the documentation, this isn't required
	case CursorArrow:
		cur = cursorArrow
	case CursorCrosshair:
		cur = cursorCrosshair
	case CursorHand:
		cur = cursorHand
	case CursorIBeam:
		cur = cursorIBeam
	case CursorHResize:
		cur = cursorHResize
	case CursorVResize:
		cur = cursorVResize
	}
	window.SetCursor(cur)
}

func SetVSync(enabled bool) {
	opts.VSync = enabled
	if opts.VSync {
		glfw.SwapInterval(1)
	} else {
		glfw.SwapInterval(0)
	}
}

func init() {
	runtime.LockOSThread()

	Dash = Key(glfw.KeyMinus)
	Apostrophe = Key(glfw.KeyApostrophe)
	Semicolon = Key(glfw.KeySemicolon)
	Equals = Key(glfw.KeyEqual)
	Comma = Key(glfw.KeyComma)
	Period = Key(glfw.KeyPeriod)
	Slash = Key(glfw.KeySlash)
	Backslash = Key(glfw.KeyBackslash)
	Backspace = Key(glfw.KeyBackspace)
	Tab = Key(glfw.KeyTab)
	CapsLock = Key(glfw.KeyCapsLock)
	Space = Key(glfw.KeySpace)
	Enter = Key(glfw.KeyEnter)
	Escape = Key(glfw.KeyEscape)
	Insert = Key(glfw.KeyInsert)
	PrintScreen = Key(glfw.KeyPrintScreen)
	Delete = Key(glfw.KeyDelete)
	PageUp = Key(glfw.KeyPageUp)
	PageDown = Key(glfw.KeyPageDown)
	Home = Key(glfw.KeyHome)
	End = Key(glfw.KeyEnd)
	Pause = Key(glfw.KeyPause)
	ScrollLock = Key(glfw.KeyScrollLock)
	ArrowLeft = Key(glfw.KeyLeft)
	ArrowRight = Key(glfw.KeyRight)
	ArrowDown = Key(glfw.KeyDown)
	ArrowUp = Key(glfw.KeyUp)
	LeftBracket = Key(glfw.KeyLeftBracket)
	LeftShift = Key(glfw.KeyLeftShift)
	LeftControl = Key(glfw.KeyLeftControl)
	LeftSuper = Key(glfw.KeyLeftSuper)
	LeftAlt = Key(glfw.KeyLeftAlt)
	RightBracket = Key(glfw.KeyRightBracket)
	RightShift = Key(glfw.KeyRightShift)
	RightControl = Key(glfw.KeyRightControl)
	RightSuper = Key(glfw.KeyRightSuper)
	RightAlt = Key(glfw.KeyRightAlt)
	Zero = Key(glfw.Key0)
	One = Key(glfw.Key1)
	Two = Key(glfw.Key2)
	Three = Key(glfw.Key3)
	Four = Key(glfw.Key4)
	Five = Key(glfw.Key5)
	Six = Key(glfw.Key6)
	Seven = Key(glfw.Key7)
	Eight = Key(glfw.Key8)
	Nine = Key(glfw.Key9)
	F1 = Key(glfw.KeyF1)
	F2 = Key(glfw.KeyF2)
	F3 = Key(glfw.KeyF3)
	F4 = Key(glfw.KeyF4)
	F5 = Key(glfw.KeyF5)
	F6 = Key(glfw.KeyF6)
	F7 = Key(glfw.KeyF7)
	F8 = Key(glfw.KeyF8)
	F9 = Key(glfw.KeyF9)
	F10 = Key(glfw.KeyF10)
	F11 = Key(glfw.KeyF11)
	F12 = Key(glfw.KeyF12)
	A = Key(glfw.KeyA)
	B = Key(glfw.KeyB)
	C = Key(glfw.KeyC)
	D = Key(glfw.KeyD)
	E = Key(glfw.KeyE)
	F = Key(glfw.KeyF)
	G = Key(glfw.KeyG)
	H = Key(glfw.KeyH)
	I = Key(glfw.KeyI)
	J = Key(glfw.KeyJ)
	K = Key(glfw.KeyK)
	L = Key(glfw.KeyL)
	M = Key(glfw.KeyM)
	N = Key(glfw.KeyN)
	O = Key(glfw.KeyO)
	P = Key(glfw.KeyP)
	Q = Key(glfw.KeyQ)
	R = Key(glfw.KeyR)
	S = Key(glfw.KeyS)
	T = Key(glfw.KeyT)
	U = Key(glfw.KeyU)
	V = Key(glfw.KeyV)
	W = Key(glfw.KeyW)
	X = Key(glfw.KeyX)
	Y = Key(glfw.KeyY)
	Z = Key(glfw.KeyZ)
	NumLock = Key(glfw.KeyNumLock)
	NumMultiply = Key(glfw.KeyKPMultiply)
	NumDivide = Key(glfw.KeyKPDivide)
	NumAdd = Key(glfw.KeyKPAdd)
	NumSubtract = Key(glfw.KeyKPSubtract)
	NumZero = Key(glfw.KeyKP0)
	NumOne = Key(glfw.KeyKP1)
	NumTwo = Key(glfw.KeyKP2)
	NumThree = Key(glfw.KeyKP3)
	NumFour = Key(glfw.KeyKP4)
	NumFive = Key(glfw.KeyKP5)
	NumSix = Key(glfw.KeyKP6)
	NumSeven = Key(glfw.KeyKP7)
	NumEight = Key(glfw.KeyKP8)
	NumNine = Key(glfw.KeyKP9)
	NumDecimal = Key(glfw.KeyKPDecimal)
	NumEnter = Key(glfw.KeyKPEnter)
}

func NewImageRGBA(img *image.RGBA) *ImageRGBA {
	return &ImageRGBA{img}
}

type ImageRGBA struct {
	data *image.RGBA
}

func (i *ImageRGBA) Data() interface{} {
	return i.data
}

func (i *ImageRGBA) Width() int {
	return i.data.Rect.Max.X
}

func (i *ImageRGBA) Height() int {
	return i.data.Rect.Max.Y
}

// openFile is the desktop-specific way of opening a file
func openFile(url string) (io.ReadCloser, error) {
	return os.Open(url)
}
<|endoftext|>


<commit_before>package job

import (
	"errors"
	"strings"

	"github.com/cozy/cozy-stack/model/permission"
	"github.com/cozy/cozy-stack/model/vfs"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/couchdb"
	"github.com/cozy/cozy-stack/pkg/logger"
	"github.com/cozy/cozy-stack/pkg/realtime"
)

// EventTrigger implements Trigger for realtime triggered events
type EventTrigger struct {
	*TriggerInfos
	unscheduled chan struct{}
	mask        []permission.Rule
}

// NewEventTrigger returns a new instance of EventTrigger given the specified
// options.
func NewEventTrigger(infos *TriggerInfos) (*EventTrigger, error) {
	args := strings.Split(infos.Arguments, " ")
	rules := make([]permission.Rule, len(args))
	for i, arg := range args {
		rule, err := permission.UnmarshalRuleString(arg)
		if err != nil {
			return nil, err
		}
		rules[i] = rule
	}
	return &EventTrigger{
		TriggerInfos: infos,
		unscheduled:  make(chan struct{}),
		mask:         rules,
	}, nil
}

// Type implements the Type method of the Trigger interface.
func (t *EventTrigger) Type() string {
	return t.TriggerInfos.Type
}

// Schedule implements the Schedule method of the Trigger interface.
func (t *EventTrigger) Schedule() <-chan *JobRequest {
	ch := make(chan *JobRequest)
	go func() {
		sub := realtime.GetHub().Subscriber(t)
		for _, m := range t.mask {
			_ = sub.Subscribe(m.Type)
		}
		defer func() {
			sub.Close()
			close(ch)
		}()
		for {
			select {
			case e := <-sub.Channel:
				found := false
				for _, m := range t.mask {
					if eventMatchRule(e, &m) {
						found = true
						break
					}
				}
				if found {
					if evt, err := t.Infos().JobRequestWithEvent(e); err == nil {
						ch <- evt
					}
				}
			case <-t.unscheduled:
				return
			}
		}
	}()
	return ch
}

// Unschedule implements the Unschedule method of the Trigger interface.
func (t *EventTrigger) Unschedule() {
	close(t.unscheduled)
}

// Infos implements the Infos method of the Trigger interface.
func (t *EventTrigger) Infos() *TriggerInfos {
	return t.TriggerInfos
}

func eventMatchRule(e *realtime.Event, rule *permission.Rule) bool {
	if e.Doc.DocType() != rule.Type {
		return false
	}

	if e.Verb == realtime.EventNotify {
		return false
	}

	if !rule.Verbs.Contains(permission.Verb(e.Verb)) {
		return false
	}

	if len(rule.Values) == 0 {
		return true
	}

	if rule.Selector == "" {
		if rule.ValuesContain(e.Doc.ID()) {
			return true
		}
		if e.Doc.DocType() == consts.Files {
			for _, value := range rule.Values {
				var dir vfs.DirDoc
				if err := couchdb.GetDoc(e, consts.Files, value, &dir); err != nil {
					logger.WithDomain(e.Domain).
						WithField("nspace", "event-trigger").
						Warnf("Cannot find io.cozy.files %s for trigger rule: %s", value, err)
					continue
				}
				// The trigger value was for a file, not a dir, and it should
				// match only on ID, not on path.
				if dir.Type != consts.DirType {
					continue
				}
				if testPath(&dir, e.Doc) {
					return true
				}
				if e.OldDoc != nil {
					if testPath(&dir, e.OldDoc) {
						return true
					}
				}
			}
		}
		return false
	}

	if len(rule.Values) == 1 && rule.Values[0] == "!=" {
		// Selector for a changed value
		if e.Verb != realtime.EventUpdate {
			return true // We consider that the value has changed on create and delete
		}
		if e.OldDoc == nil {
			return false
		}
		if doc, ok := e.Doc.(permission.Fetcher); ok {
			if old, ok := e.OldDoc.(permission.Fetcher); ok {
				return rule.ValuesChanged(old, doc)
			}
		}
	} else {
		// Selector with normal values
		if v, ok := e.Doc.(permission.Fetcher); ok {
			if rule.ValuesMatch(v) {
				return true
			}
			// Particular case where the new doc is not valid but the old one was.
			if e.OldDoc != nil {
				if vOld, okOld := e.OldDoc.(permission.Fetcher); okOld {
					return rule.ValuesMatch(vOld)
				}
			}
		}
	}

	return false
}

// DumpFilePather is a struct made for calling the Path method of a FileDoc and
// relying on the cached fullpath of this document (not trying to rebuild it)
type DumpFilePather struct{}

// FilePath only returns an error saying to not call this method
func (d DumpFilePather) FilePath(doc *vfs.FileDoc) (string, error) {
	logger.WithNamespace("event-trigger").Warning("FilePath method of DumpFilePather has been called")
	return "", errors.New("DumpFilePather FilePath should not have been called")
}

var dumpFilePather = DumpFilePather{}

func testPath(dir *vfs.DirDoc, doc realtime.Doc) bool {
	if d, ok := doc.(*vfs.DirDoc); ok {
		return strings.HasPrefix(d.Fullpath, dir.Fullpath+"/")
	}
	if f, ok := doc.(*vfs.FileDoc); ok {
		if f.Trashed {
			// XXX When a new file is uploaded, a document may be created in
			// couchdb with trashed: true. We should ignore it.
			if strings.HasPrefix(f.DocRev, "1-") {
				return false
			}
			if f.RestorePath != "" {
				return strings.HasPrefix(f.RestorePath, dir.Fullpath+"/")
			}
		}
		p, err := f.Path(dumpFilePather)
		if err != nil {
			return false
		}
		return strings.HasPrefix(p, dir.Fullpath+"/")
	}
	return false
}

var _ Trigger = &EventTrigger{}
<commit_msg>Remove a warning<commit_after>package job

import (
	"errors"
	"strings"

	"github.com/cozy/cozy-stack/model/permission"
	"github.com/cozy/cozy-stack/model/vfs"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/couchdb"
	"github.com/cozy/cozy-stack/pkg/logger"
	"github.com/cozy/cozy-stack/pkg/realtime"
)

// EventTrigger implements Trigger for realtime triggered events
type EventTrigger struct {
	*TriggerInfos
	unscheduled chan struct{}
	mask        []permission.Rule
}

// NewEventTrigger returns a new instance of EventTrigger given the specified
// options.
func NewEventTrigger(infos *TriggerInfos) (*EventTrigger, error) {
	args := strings.Split(infos.Arguments, " ")
	rules := make([]permission.Rule, len(args))
	for i, arg := range args {
		rule, err := permission.UnmarshalRuleString(arg)
		if err != nil {
			return nil, err
		}
		rules[i] = rule
	}
	return &EventTrigger{
		TriggerInfos: infos,
		unscheduled:  make(chan struct{}),
		mask:         rules,
	}, nil
}

// Type implements the Type method of the Trigger interface.
func (t *EventTrigger) Type() string {
	return t.TriggerInfos.Type
}

// Schedule implements the Schedule method of the Trigger interface.
func (t *EventTrigger) Schedule() <-chan *JobRequest {
	ch := make(chan *JobRequest)
	go func() {
		sub := realtime.GetHub().Subscriber(t)
		for _, m := range t.mask {
			_ = sub.Subscribe(m.Type)
		}
		defer func() {
			sub.Close()
			close(ch)
		}()
		for {
			select {
			case e := <-sub.Channel:
				found := false
				for _, m := range t.mask {
					if eventMatchRule(e, &m) {
						found = true
						break
					}
				}
				if found {
					if evt, err := t.Infos().JobRequestWithEvent(e); err == nil {
						ch <- evt
					}
				}
			case <-t.unscheduled:
				return
			}
		}
	}()
	return ch
}

// Unschedule implements the Unschedule method of the Trigger interface.
func (t *EventTrigger) Unschedule() {
	close(t.unscheduled)
}

// Infos implements the Infos method of the Trigger interface.
func (t *EventTrigger) Infos() *TriggerInfos {
	return t.TriggerInfos
}

func eventMatchRule(e *realtime.Event, rule *permission.Rule) bool {
	if e.Doc.DocType() != rule.Type {
		return false
	}

	if e.Verb == realtime.EventNotify {
		return false
	}

	if !rule.Verbs.Contains(permission.Verb(e.Verb)) {
		return false
	}

	if len(rule.Values) == 0 {
		return true
	}

	if rule.Selector == "" {
		if rule.ValuesContain(e.Doc.ID()) {
			return true
		}
		if e.Doc.DocType() == consts.Files {
			for _, value := range rule.Values {
				var dir vfs.DirDoc
				if err := couchdb.GetDoc(e, consts.Files, value, &dir); err != nil {
					logger.WithDomain(e.Domain).
						WithField("nspace", "event-trigger").
						Debugf("Cannot find io.cozy.files %s for trigger rule: %s", value, err)
					continue
				}
				// The trigger value was for a file, not a dir, and it should
				// match only on ID, not on path.
				if dir.Type != consts.DirType {
					continue
				}
				if testPath(&dir, e.Doc) {
					return true
				}
				if e.OldDoc != nil {
					if testPath(&dir, e.OldDoc) {
						return true
					}
				}
			}
		}
		return false
	}

	if len(rule.Values) == 1 && rule.Values[0] == "!=" {
		// Selector for a changed value
		if e.Verb != realtime.EventUpdate {
			return true // We consider that the value has changed on create and delete
		}
		if e.OldDoc == nil {
			return false
		}
		if doc, ok := e.Doc.(permission.Fetcher); ok {
			if old, ok := e.OldDoc.(permission.Fetcher); ok {
				return rule.ValuesChanged(old, doc)
			}
		}
	} else {
		// Selector with normal values
		if v, ok := e.Doc.(permission.Fetcher); ok {
			if rule.ValuesMatch(v) {
				return true
			}
			// Particular case where the new doc is not valid but the old one was.
			if e.OldDoc != nil {
				if vOld, okOld := e.OldDoc.(permission.Fetcher); okOld {
					return rule.ValuesMatch(vOld)
				}
			}
		}
	}

	return false
}

// DumpFilePather is a struct made for calling the Path method of a FileDoc and
// relying on the cached fullpath of this document (not trying to rebuild it)
type DumpFilePather struct{}

// FilePath only returns an error saying to not call this method
func (d DumpFilePather) FilePath(doc *vfs.FileDoc) (string, error) {
	logger.WithNamespace("event-trigger").Warning("FilePath method of DumpFilePather has been called")
	return "", errors.New("DumpFilePather FilePath should not have been called")
}

var dumpFilePather = DumpFilePather{}

func testPath(dir *vfs.DirDoc, doc realtime.Doc) bool {
	if d, ok := doc.(*vfs.DirDoc); ok {
		return strings.HasPrefix(d.Fullpath, dir.Fullpath+"/")
	}
	if f, ok := doc.(*vfs.FileDoc); ok {
		if f.Trashed {
			// XXX When a new file is uploaded, a document may be created in
			// couchdb with trashed: true. We should ignore it.
			if strings.HasPrefix(f.DocRev, "1-") {
				return false
			}
			if f.RestorePath != "" {
				return strings.HasPrefix(f.RestorePath, dir.Fullpath+"/")
			}
		}
		p, err := f.Path(dumpFilePather)
		if err != nil {
			return false
		}
		return strings.HasPrefix(p, dir.Fullpath+"/")
	}
	return false
}

var _ Trigger = &EventTrigger{}
<|endoftext|>