Page MenuHomePhorge

No OneTemporary

Size
35 KB
Referenced Files
None
Subscribers
None
diff --git a/Makefile b/Makefile
index 77e47dc..4866a1c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,40 +1,42 @@
# Apprentice binary
CC = gcc
-CFLAGS = -std=c99 -g -Wall -Wextra -Werror
-LDFLAGS = -lm -lmagic
+ERL_EI_INCLUDE:=$(shell erl -eval 'io:format("~s", [code:lib_dir(erl_interface, include)])' -s init stop -noshell | head -1)
+ERL_EI_LIB:=$(shell erl -eval 'io:format("~s", [code:lib_dir(erl_interface, lib)])' -s init stop -noshell | head -1)
+CFLAGS = -std=c99 -g -Wall -Wextra -Werror -I$(ERL_EI_INCLUDE)
+LDFLAGS = -L/usr/include/linux/ -L$(ERL_EI_LIB) -lm -lmagic -lei -lpthread
HEADER_FILES = src
C_SOURCE_FILES = src/apprentice.c
OBJECT_FILES = $(C_SOURCE_FILES:.c=.o)
EXECUTABLE_DIRECTORY = priv
EXECUTABLE = $(EXECUTABLE_DIRECTORY)/apprentice
# Unit test custom magic file
MAGIC = file
TEST_DIRECTORY = test
TARGET_MAGIC = $(TEST_DIRECTORY)/elixir.mgc
SOURCE_MAGIC = $(TEST_DIRECTORY)/elixir
# Target
all: $(EXECUTABLE) $(TARGET_MAGIC)
# Compile
$(EXECUTABLE): $(OBJECT_FILES) $(EXECUTABLE_DIRECTORY)
$(CC) $(OBJECT_FILES) -o $@ $(LDFLAGS)
$(EXECUTABLE_DIRECTORY):
mkdir -p $(EXECUTABLE_DIRECTORY)
.o:
$(CC) $(CFLAGS) $< -o $@
# Test case
$(TARGET_MAGIC): $(SOURCE_MAGIC)
cd $(TEST_DIRECTORY); $(MAGIC) -C -m elixir
clean:
rm -f $(EXECUTABLE) $(OBJECT_FILES) $(BEAM_FILES)
diff --git a/lib/gen_magic/config.ex b/lib/gen_magic/config.ex
index c971e7c..0ee2ace 100644
--- a/lib/gen_magic/config.ex
+++ b/lib/gen_magic/config.ex
@@ -1,57 +1,57 @@
defmodule GenMagic.Config do
@moduledoc false
@otp_app Mix.Project.config()[:app]
@executable_name "apprentice"
@startup_timeout 1_000
@process_timeout 30_000
@recycle_threshold :infinity
@database_patterns [:default]
def get_port_name do
{:spawn_executable, to_charlist(get_executable_name())}
end
def get_port_options(options) do
- arguments = [:use_stdio, :stderr_to_stdout, :binary, :exit_status]
+ arguments = [:use_stdio, :binary, :exit_status, {:packet, 2}]
case get_executable_arguments(options) do
[] -> arguments
list -> [{:args, list} | arguments]
end
end
def get_startup_timeout(options) do
get_value(options, :startup_timeout, @startup_timeout)
end
def get_process_timeout(options) do
get_value(options, :process_timeout, @process_timeout)
end
def get_recycle_threshold(options) do
get_value(options, :recycle_threshold, @recycle_threshold)
end
defp get_executable_name do
Path.join(:code.priv_dir(@otp_app), @executable_name)
end
defp get_executable_arguments(options) do
Enum.flat_map(List.wrap(get(options, :database_patterns, @database_patterns)), fn
:default -> ["--database-default"]
pattern -> pattern |> Path.wildcard() |> Enum.flat_map(&["--database-file", &1])
end)
end
defp get(options, key, default) do
Keyword.get(options, key, default)
end
defp get_value(options, key, default) do
case get(options, key, default) do
value when is_integer(value) and value > 0 -> value
:infinity -> :infinity
_ -> raise ArgumentError, message: "Invalid #{key}"
end
end
end
diff --git a/lib/gen_magic/server.ex b/lib/gen_magic/server.ex
index fc76885..18e5955 100644
--- a/lib/gen_magic/server.ex
+++ b/lib/gen_magic/server.ex
@@ -1,268 +1,292 @@
defmodule GenMagic.Server do
@moduledoc """
Provides access to the underlying libmagic client, which performs file introspection.
The Server needs to be supervised, since it will terminate if it receives any unexpected error.
"""
@behaviour :gen_statem
alias GenMagic.Result
alias GenMagic.Server.Data
alias GenMagic.Server.Status
@typedoc """
Represents the reference to the underlying server, as returned by `:gen_statem`.
"""
@type t :: :gen_statem.server_ref()
@typedoc """
Represents values accepted as startup options, which can be passed to `start_link/1`.
- `:name`: If present, this will be the registered name for the underlying process.
Note that `:gen_statem` requires `{:local, name}`, but given widespread GenServer convention,
atoms are accepted and will be converted to `{:local, name}`.
- `:startup_timeout`: Specifies how long the Server waits for the C program to initialise.
However, if the underlying C program exits, then the process exits immediately.
Can be set to `:infinity`.
- `:process_timeout`: Specifies how long the Server waits for each request to complete.
Can be set to `:infinity`.
Please note that, if you have chosen a custom timeout value, you should also pass it when
using `GenMagic.Server.perform/3`.
- `:recycle_threshold`: Specifies the number of requests processed before the underlying C
program is recycled.
Can be set to `:infinity` if you do not wish for the program to be recycled.
- `:database_patterns`: Specifies what magic databases to load; you can specify a list of either
Path Patterns (see `Path.wildcard/2`) or `:default` to instruct the C program to load the
appropriate databases.
For example, if you have had to add custom magics, then you can set this value to:
[:default, "path/to/my/magic"]
"""
@type option ::
{:name, atom() | :gen_statem.server_name()}
| {:startup_timeout, timeout()}
| {:process_timeout, timeout()}
| {:recycle_threshold, non_neg_integer() | :infinity}
| {:database_patterns, nonempty_list(:default | Path.t())}
@typedoc """
Current state of the Server:
- `:pending`: This is the initial state; the Server will attempt to start the underlying Port
and the libmagic client, then automatically transition to either Available or Crashed.
- `:available`: This is the default state. In this state the Server is able to accept requests
and they will be replied in the same order.
- `:processing`: This is the state the Server will be in if it is processing requests. In this
state, further requests can still be lodged and they will be processed when the Server is
available again.
For proper concurrency, use a process pool like Poolboy, Sbroker, etc.
- `:recycling`: This is the state the Server will be in, if its underlying C program needs to be
recycled. This state is triggered whenever the cycle count reaches the defined value as per
`:recycle_threshold`.
In this state, the Server is able to accept requests, but they will not be processed until the
underlying C server program has been started again.
"""
@type state :: :starting | :processing | :available | :recycling
@spec child_spec([option()]) :: Supervisor.child_spec()
@spec start_link([option()]) :: :gen_statem.start_ret()
@spec perform(t(), Path.t(), timeout()) :: {:ok, Result.t()} | {:error, term()}
@spec status(t(), timeout()) :: {:ok, Status.t()} | {:error, term()}
@spec stop(t(), term(), timeout()) :: :ok
@doc """
Returns the default Child Specification for this Server for use in Supervisors.
You can override this with `Supervisor.child_spec/2` as required.
"""
def child_spec(options) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [options]},
type: :worker,
restart: :permanent,
shutdown: 500
}
end
@doc """
Starts a new Server.
See `t:option/0` for further details.
"""
def start_link(options) do
{name, options} = Keyword.pop(options, :name)
case name do
nil -> :gen_statem.start_link(__MODULE__, options, [])
name when is_atom(name) -> :gen_statem.start_link({:local, name}, __MODULE__, options, [])
{:global, _} -> :gen_statem.start_link(name, __MODULE__, options, [])
{:via, _, _} -> :gen_statem.start_link(name, __MODULE__, options, [])
{:local, _} -> :gen_statem.start_link(name, __MODULE__, options, [])
end
end
@doc """
Determines the type of the file provided.
"""
def perform(server_ref, path, timeout \\ 5000) do
case :gen_statem.call(server_ref, {:perform, path}, timeout) do
{:ok, %Result{} = result} -> {:ok, result}
{:error, reason} -> {:error, reason}
end
end
@doc """
Returns status of the Server.
"""
def status(server_ref, timeout \\ 5000) do
:gen_statem.call(server_ref, :status, timeout)
end
@doc """
Stops the Server with reason `:normal` and timeout `:infinity`.
"""
def stop(server_ref) do
:gen_statem.stop(server_ref)
end
@doc """
Stops the Server with the specified reason and timeout.
"""
def stop(server_ref, reason, timeout) do
:gen_statem.stop(server_ref, reason, timeout)
end
@impl :gen_statem
def init(options) do
import GenMagic.Config
data = %Data{
port_name: get_port_name(),
port_options: get_port_options(options),
startup_timeout: get_startup_timeout(options),
process_timeout: get_process_timeout(options),
recycle_threshold: get_recycle_threshold(options)
}
{:ok, :starting, data}
end
@impl :gen_statem
def callback_mode do
[:state_functions, :state_enter]
end
@doc false
- def starting(:enter, _, %{request: nil, port: nil} = data) do
+ def starting(:enter, x, %{request: nil, port: nil} = data) do
port = Port.open(data.port_name, data.port_options)
{:keep_state, %{data | port: port}, data.startup_timeout}
end
@doc false
def starting({:call, from}, :status, data) do
handle_status_call(from, :starting, data)
end
@doc false
def starting({:call, _from}, {:perform, _path}, _data) do
{:keep_state_and_data, :postpone}
end
@doc false
- def starting(:info, {port, {:data, "ok\n"}}, %{port: port} = data) do
- {:next_state, :available, data}
+ def starting(:info, {port, {:data, binary}}, %{port: port} = data) do
+ case :erlang.binary_to_term(binary) do
+ :ready ->
+ {:next_state, :available, data}
+ end
+ end
+
+ def starting(:info, {port, {:exit_status, code}}, %{port: port} = data) do
+ error =
+ case code do
+ 1 -> :no_database
+ 2 -> :no_argument
+ 3 -> :missing_database
+ 4 -> :term_error
+ 5 -> :ei_error
+ end
+
+ {:stop, {:error, error}, data}
end
@doc false
def available(:enter, _old_state, %{request: nil}) do
:keep_state_and_data
end
@doc false
def available({:call, from}, {:perform, path}, data) do
data = %{data | cycles: data.cycles + 1, request: {path, from, :erlang.now()}}
- _ = send(data.port, {self(), {:command, "file; " <> path <> "\n"}})
+ command = :erlang.term_to_binary({:file, path})
+ _ = send(data.port, {self(), {:command, command}})
{:next_state, :processing, data}
end
@doc false
def available({:call, from}, :status, data) do
handle_status_call(from, :available, data)
end
@doc false
def processing(:enter, _old_state, %{request: {_path, _from, _time}} = data) do
{:keep_state_and_data, data.process_timeout}
end
@doc false
def processing({:call, _from}, {:perform, _path}, _data) do
{:keep_state_and_data, :postpone}
end
@doc false
def processing({:call, from}, :status, data) do
handle_status_call(from, :processing, data)
end
@doc false
def processing(:info, {port, {:data, response}}, %{port: port} = data) do
{_, from, _} = data.request
data = %{data | request: nil}
response = {:reply, from, handle_response(response)}
next_state = (data.cycles >= data.recycle_threshold && :recycling) || :available
{:next_state, next_state, data, response}
end
@doc false
def recycling(:enter, _, %{request: nil, port: port} = data) when is_port(port) do
- _ = send(data.port, {self(), :close})
+ _ = send(data.port, {self(), {:command, :erlang.term_to_binary({:stop, :recycle})}})
{:keep_state_and_data, data.startup_timeout}
end
@doc false
def recycling({:call, _from}, {:perform, _path}, _data) do
{:keep_state_and_data, :postpone}
end
@doc false
def recycling({:call, from}, :status, data) do
handle_status_call(from, :recycling, data)
end
@doc false
- def recycling(:info, {port, :closed}, %{port: port} = data) do
+ def recycling(:info, {port, {:exit_status, 0}}, %{port: port} = data) do
{:next_state, :starting, %{data | port: nil, cycles: 0}}
end
- defp handle_response("ok; " <> message) do
- case message |> String.trim() |> String.split("\t") do
- [mime_type, encoding, content] -> {:ok, Result.build(mime_type, encoding, content)}
- _ -> {:error, :malformed_response}
+ @errnos %{
+ 2 => :enoent,
+ 13 => :eaccess,
+ 21 => :eisdir,
+ 20 => :enotdir,
+ 12 => :enomem,
+ 24 => :emfile
+ }
+ @errno Map.keys(@errnos)
+
+ defp handle_response(data) do
+ case :erlang.binary_to_term(data) do
+ {:ok, {mime_type, encoding, content}} -> {:ok, Result.build(mime_type, encoding, content)}
+ {:error, {errno, _}} when errno in @errno -> {:error, @errnos[errno]}
+ {:error, {errno, string}} -> {:error, "#{errno}: #{string}"}
end
end
- defp handle_response("error; " <> message) do
- {:error, String.trim(message)}
- end
-
defp handle_status_call(from, state, data) do
response = {:ok, %__MODULE__.Status{state: state, cycles: data.cycles}}
{:keep_state_and_data, {:reply, from, response}}
end
end
diff --git a/src/apprentice.c b/src/apprentice.c
index 8105b14..6d164f8 100644
--- a/src/apprentice.c
+++ b/src/apprentice.c
@@ -1,284 +1,362 @@
//
// The Sorcerer’s Apprentice
//
// To use this program, compile it with dynamically linked libmagic, as mirrored
// at https://github.com/threatstack/libmagic. You may install it with apt-get, yum or brew.
// Refer to the Makefile for further reference.
//
// This program is designed to run interactively as a backend daemon to the GenMagic library,
// and follows the command line pattern:
//
// $ apprentice --database-file <file> --database-default
//
// Where each argument either refers to a compiled or uncompiled magic database, or the default
// database. They will be loaded in the sequence that they were specified. Note that you must
// specify at least one database.
+// Erlang Term
//
-// Once the program starts, it will print info statements if run from a terminal, then it will
-// print `ok`. From this point onwards, additional commands can be passed:
-//
-// file; <path>
+// -- main: send atom ready
+// enter loop
//
-// Results will be printed tab-separated, e.g.:
+// -- while
+// get {:file, path} -> process_file -> ok | error
+// {:bytes, path} -> process_bytes -> ok | error
+// ok: {:ok, {type, encoding, name}}
+// error: {:error, :badarg} | {:error, {errno, String.t()}}
+// {:stop, _} -> exit(ERROR_OK) -> exit 0
//
-// ok; application/zip binary Zip archive data, at least v1.0 to extract
-
#include <errno.h>
#include <getopt.h>
#include <libgen.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
+#include <ei.h>
#include <magic.h>
-
#define USAGE "[--database-file <path/to/magic.mgc> | --database-default, ...]"
#define DELIMITER "\t"
#define ERROR_OK 0
#define ERROR_NO_DATABASE 1
#define ERROR_NO_ARGUMENT 2
#define ERROR_MISSING_DATABASE 3
+#define ERROR_BAD_TERM 4
+#define ERROR_EI 5
#define ANSI_INFO "\x1b[37m" // gray
#define ANSI_OK "\x1b[32m" // green
#define ANSI_ERROR "\x1b[31m" // red
#define ANSI_IGNORE "\x1b[90m" // red
#define ANSI_RESET "\x1b[0m"
#define MAGIC_FLAGS_COMMON (MAGIC_CHECK|MAGIC_ERROR)
magic_t magic_setup(int flags);
+typedef char byte;
+
+int read_cmd(byte *buf);
+int write_cmd(byte *buf, int len);
+
void setup_environment();
void setup_options(int argc, char **argv);
void setup_options_file(char *optarg);
void setup_options_default();
void setup_system();
+int process_command(byte *buf);
void process_line(char *line);
-void process_file(char *path);
-void print_info(const char *format, ...);
-void print_ok(const char *format, ...);
-void print_error(const char *format, ...);
+void process_file(char *path, ei_x_buff *result);
+void error(ei_x_buff *result, const char *error);
struct magic_file {
struct magic_file *prev;
struct magic_file *next;
char *path;
};
static struct magic_file* magic_database;
static magic_t magic_mime_type; // MAGIC_MIME_TYPE
static magic_t magic_mime_encoding; // MAGIC_MIME_ENCODING
static magic_t magic_type_name; // MAGIC_NONE
int main (int argc, char **argv) {
+ ei_init();
setup_environment();
setup_options(argc, argv);
setup_system();
- printf("ok\n");
- fflush(stdout);
- char line[4096];
- while (fgets(line, 4096, stdin)) {
- process_line(line);
+ ei_x_buff ok_buf;
+ if (ei_x_new_with_version(&ok_buf) || ei_x_encode_atom(&ok_buf, "ready")) return 5;
+ write_cmd(ok_buf.buff, ok_buf.index);
+ if (ei_x_free(&ok_buf) != 0)
+ exit(ERROR_EI);
+
+ byte buf[5000];
+ while (read_cmd(buf) > 0) {
+ process_command(buf);
}
return 0;
}
+int process_command(byte *buf) {
+ ei_x_buff result;
+ char atom[128];
+ int index, version, arity;
+ index = 0;
+
+ if (ei_decode_version(buf, &index, &version) != 0)
+ exit(ERROR_BAD_TERM);
+
+ // Initialize result
+ if (ei_x_new_with_version(&result) || ei_x_encode_tuple_header(&result, 2)) exit(ERROR_EI);
+
+ if (ei_decode_tuple_header(buf, &index, &arity) != 0) {
+ error(&result, "badarg");
+ return 1;
+ }
+
+ if (arity != 2) {
+ error(&result, "badarg");
+ return 1;
+ }
+
+ if (ei_decode_atom(buf, &index, atom) != 0) {
+ error(&result, "badarg");
+ return 1;
+ }
+
+ if (strncmp(atom, "file", 3) == 0) {
+ int pathtype;
+ int pathsize;
+ char path[4097];
+ ei_get_type(buf, &index, &pathtype, &pathsize);
+
+ if (pathtype == ERL_BINARY_EXT && pathsize < 4096) {
+ long bin_length;
+ ei_decode_binary(buf, &index, path, &bin_length);
+ path[pathsize] = '\0';
+ process_file(path, &result);
+ } else {
+ error(&result, "badarg");
+ return 1;
+ }
+ } else if (strncmp(atom, "bytes", 3) == 0) {
+ ei_x_encode_atom(&result, "ok");
+ ei_x_encode_atom(&result, "bytes_not_implemented");
+ } else if (strncmp(atom, "stop", 3) == 0) {
+ exit(ERROR_OK);
+ } else {
+ error(&result, "badarg");
+ return 1;
+ }
+
+ write_cmd(result.buff, result.index);
+
+ if (ei_x_free(&result) != 0)
+ exit(ERROR_EI);
+ return 0;
+}
+
void setup_environment() {
opterr = 0;
}
void setup_options(int argc, char **argv) {
const char *option_string = "f:";
static struct option long_options[] = {
{"database-file", required_argument, 0, 'f'},
{"database-default", no_argument, 0, 'd'},
{0, 0, 0, 0}
};
int option_character;
while (1) {
int option_index = 0;
option_character = getopt_long(argc, argv, option_string, long_options, &option_index);
if (-1 == option_character) {
break;
}
switch (option_character) {
case 'f': {
setup_options_file(optarg);
break;
}
case 'd': {
setup_options_default();
break;
}
case '?':
default: {
- print_info("%s %s\n", basename(argv[0]), USAGE);
exit(ERROR_NO_ARGUMENT);
break;
}
}
}
}
void setup_options_file(char *optarg) {
- print_info("Requested database %s", optarg);
if (0 != access(optarg, R_OK)) {
- print_error("Missing Database");
exit(ERROR_MISSING_DATABASE);
}
struct magic_file *next = malloc(sizeof(struct magic_file));
size_t path_length = strlen(optarg) + 1;
char *path = malloc(path_length);
memcpy(path, optarg, path_length);
next->path = path;
next->prev = magic_database;
if (magic_database) {
magic_database->next = next;
}
magic_database = next;
}
void setup_options_default() {
- print_info("requested default database");
struct magic_file *next = malloc(sizeof(struct magic_file));
next->path = NULL;
next->prev = magic_database;
if (magic_database) {
magic_database->next = next;
}
magic_database = next;
}
void setup_system() {
magic_mime_encoding = magic_setup(MAGIC_FLAGS_COMMON|MAGIC_MIME_ENCODING);
magic_mime_type = magic_setup(MAGIC_FLAGS_COMMON|MAGIC_MIME_TYPE);
magic_type_name = magic_setup(MAGIC_FLAGS_COMMON|MAGIC_NONE);
}
magic_t magic_setup(int flags) {
- print_info("starting libmagic instance for flags %i", flags);
magic_t magic = magic_open(flags);
struct magic_file *current_database = magic_database;
if (!current_database) {
- print_error("no database configured");
exit(ERROR_NO_DATABASE);
}
while (current_database->prev) {
current_database = current_database->prev;
}
while (current_database) {
- if (isatty(STDERR_FILENO)) {
- fprintf(stderr, ANSI_IGNORE);
- }
- if (!current_database->path) {
- print_info("loading default database");
- } else {
- print_info("loading database %s", current_database->path);
- }
magic_load(magic, current_database->path);
if (isatty(STDERR_FILENO)) {
fprintf(stderr, ANSI_RESET);
}
current_database = current_database->next;
}
return magic;
}
-void process_line(char *line) {
- char path[4096];
-
- if (0 == strcmp(line, "exit\n")) {
- exit(ERROR_OK);
- }
- if (1 != sscanf(line, "file; %[^\n]s", path)) {
- print_error("invalid commmand");
+void process_file(char *path, ei_x_buff *result) {
+ const char *mime_type_result = magic_file(magic_mime_type, path);
+ const char *mime_type_error = magic_error(magic_mime_type);
+ int mime_type_errno = magic_errno(magic_mime_type);
+
+ if (mime_type_errno > 0) {
+ ei_x_encode_atom(result, "error");
+ ei_x_encode_tuple_header(result, 2);
+ long errlon = (long)mime_type_errno;
+ ei_x_encode_long(result, errlon);
+ ei_x_encode_binary(result, mime_type_error, strlen(mime_type_error));
return;
}
- if (0 != access(path, R_OK)) {
- print_error("unable to access file");
+ const char *mime_encoding_result = magic_file(magic_mime_encoding, path);
+ const char *mime_encoding_error = magic_error(magic_mime_encoding);
+ int mime_encoding_errno = magic_errno(magic_mime_encoding);
+
+ if (mime_encoding_error) {
+ ei_x_encode_atom(result, "error");
+ ei_x_encode_tuple_header(result, 2);
+ long errlon = (long)mime_encoding_errno;
+ ei_x_encode_long(result, errlon);
+ ei_x_encode_binary(result, mime_encoding_error, strlen(mime_encoding_error));
return;
}
- process_file(path);
-}
-
-void process_file(char *path) {
- const char *mime_type_result = magic_file(magic_mime_type, path);
- const char *mime_type_error = magic_error(magic_mime_type);
- const char *mine_encoding_result = magic_file(magic_mime_encoding, path);
- const char *mine_encoding_error = magic_error(magic_mime_encoding);
const char *type_name_result = magic_file(magic_type_name, path);
const char *type_name_error = magic_error(magic_type_name);
+ int type_name_errno = magic_errno(magic_type_name);
- if (mime_type_error) {
- print_error(mime_type_error);
+ if (type_name_error) {
+ ei_x_encode_atom(result, "error");
+ ei_x_encode_tuple_header(result, 2);
+ long errlon = (long)type_name_errno;
+ ei_x_encode_long(result, errlon);
+ ei_x_encode_binary(result, type_name_error, strlen(type_name_error));
return;
}
- if (mine_encoding_error) {
- print_error(mine_encoding_error);
- return;
- }
+ ei_x_encode_atom(result, "ok");
+ ei_x_encode_tuple_header(result, 3);
+ ei_x_encode_binary(result, mime_type_result, strlen(mime_type_result));
+ ei_x_encode_binary(result, mime_encoding_result, strlen(mime_encoding_result));
+ ei_x_encode_binary(result, type_name_result, strlen(type_name_result));
+ return;
+}
- if (type_name_error) {
- print_error(type_name_error);
- return;
- }
+// From https://erlang.org/doc/tutorial/erl_interface.html
+int read_exact(byte *buf, int len)
+{
+ int i, got=0;
+
+ do {
+ if ((i = read(0, buf+got, len-got)) <= 0){
+ return(i);
+ }
+ got += i;
+ } while (got<len);
- print_ok("%s%s%s%s%s", mime_type_result, DELIMITER, mine_encoding_result, DELIMITER, type_name_result);
+ return(len);
}
-void print_info(const char *format, ...) {
- if (!isatty(STDOUT_FILENO)) {
- return;
- }
+int write_exact(byte *buf, int len)
+{
+ int i, wrote = 0;
+
+ do {
+ if ((i = write(1, buf+wrote, len-wrote)) <= 0)
+ return (i);
+ wrote += i;
+ } while (wrote<len);
- printf(ANSI_INFO "info; " ANSI_RESET);
- va_list arguments;
- va_start(arguments, format);
- vprintf(format, arguments);
- va_end(arguments);
- printf("\n");
+ return (len);
}
-void print_ok(const char *format, ...) {
- if (isatty(STDOUT_FILENO)) {
- printf(ANSI_OK "ok; " ANSI_RESET);
- } else {
- printf("ok; ");
- }
+int read_cmd(byte *buf)
+{
+ int len;
- va_list arguments;
- va_start(arguments, format);
- vprintf(format, arguments);
- va_end(arguments);
- printf("\n");
- fflush(stdout);
+ if (read_exact(buf, 2) != 2)
+ return(-1);
+ len = (buf[0] << 8) | buf[1];
+ return read_exact(buf, len);
}
-void print_error(const char *format, ...) {
- if (isatty(STDERR_FILENO)) {
- fprintf(stderr, ANSI_ERROR "error; " ANSI_RESET);
- } else {
- fprintf(stderr, "error; ");
- }
+int write_cmd(byte *buf, int len)
+{
+ byte li;
+
+ li = (len >> 8) & 0xff;
+ write_exact(&li, 1);
+
+ li = len & 0xff;
+ write_exact(&li, 1);
+
+ return write_exact(buf, len);
+}
+
+void error(ei_x_buff *result, const char *error) {
+ ei_x_encode_atom(result, "error");
+ ei_x_encode_atom(result, error);
+ write_cmd(result->buff, result->index);
- va_list arguments;
- va_start(arguments, format);
- vfprintf(stderr, format, arguments);
- va_end(arguments);
- fprintf(stderr, "\n");
- fflush(stderr);
+ if (ei_x_free(result) != 0)
+ exit(ERROR_EI);
}
diff --git a/test/gen_magic/apprentice_test.exs b/test/gen_magic/apprentice_test.exs
new file mode 100644
index 0000000..a28002e
--- /dev/null
+++ b/test/gen_magic/apprentice_test.exs
@@ -0,0 +1,101 @@
+defmodule GenMagic.ApprenticeTest do
+ use GenMagic.MagicCase
+
+ test "sends ready" do
+ port = Port.open(GenMagic.Config.get_port_name(), GenMagic.Config.get_port_options([]))
+ assert_ready(port)
+ end
+
+ test "stops" do
+ port = Port.open(GenMagic.Config.get_port_name(), GenMagic.Config.get_port_options([]))
+ assert_ready(port)
+ send(port, {self(), {:command, :erlang.term_to_binary({:stop, :stop})}})
+ assert_receive {^port, {:exit_status, 0}}
+ end
+
+ test "exits with no database" do
+ opts = [:use_stdio, :binary, :exit_status, {:packet, 2}, {:args, []}]
+ port = Port.open(GenMagic.Config.get_port_name(), opts)
+ assert_receive {^port, {:exit_status, 1}}
+ end
+
+ test "exits with a non existent database" do
+ opts = [
+ {:args, ["--database-file", "/no/such/database"]},
+ :use_stdio,
+ :binary,
+ :exit_status,
+ {:packet, 2}
+ ]
+
+ port = Port.open(GenMagic.Config.get_port_name(), opts)
+ assert_receive {^port, {:exit_status, 3}}
+ end
+
+ describe "port" do
+ setup do
+ port = Port.open(GenMagic.Config.get_port_name(), GenMagic.Config.get_port_options([]))
+ assert_ready(port)
+ %{port: port}
+ end
+
+ test "exits with badly formatted erlang terms", %{port: port} do
+ send(port, {self(), {:command, "i forgot to term_to_binary!!"}})
+ assert_receive {^port, {:exit_status, 4}}
+ end
+
+ test "errors with wrong command", %{port: port} do
+ send(port, {self(), {:command, :erlang.term_to_binary(:wrong)}})
+ assert_receive {^port, {:data, data}}
+ assert {:error, :badarg} = :erlang.binary_to_term(data)
+ refute_receive _
+
+ send(port, {self(), {:command, :erlang.term_to_binary({:file, 42})}})
+ assert_receive {^port, {:data, data}}
+ assert {:error, :badarg} = :erlang.binary_to_term(data)
+ refute_receive _
+
+ send(port, {self(), {:command, :erlang.term_to_binary("more wrong")}})
+ assert_receive {^port, {:data, data}}
+ assert {:error, :badarg} = :erlang.binary_to_term(data)
+ refute_receive _
+
+ send(port, {self(), {:command, :erlang.term_to_binary({"no", "no"})}})
+ assert_receive {^port, {:data, data}}
+ assert {:error, :badarg} = :erlang.binary_to_term(data)
+ refute_receive _
+ end
+
+ test "works", %{port: port} do
+ send(port, {self(), {:command, :erlang.term_to_binary({:file, Path.expand("Makefile")})}})
+ assert_receive {^port, {:data, data}}
+ assert {:ok, _} = :erlang.binary_to_term(data)
+ end
+
+ test "fails with non existent file", %{port: port} do
+ send(port, {self(), {:command, :erlang.term_to_binary({:file, "/path/to/nowhere"})}})
+ assert_receive {^port, {:data, data}}
+ assert {:error, _} = :erlang.binary_to_term(data)
+ end
+
+ test "works with a 4096 path", %{port: port} do
+ file = too_big() <> "/a"
+ File.mkdir_p!(too_big())
+ File.touch!(file)
+ on_exit(fn -> File.rm_rf!("/tmp/testmagicex/") end)
+ send(port, {self(), {:command, :erlang.term_to_binary({:file, file})}})
+ assert_receive {^port, {:data, data}}
+ assert {:ok, _} = :erlang.binary_to_term(data)
+ refute_receive _
+ end
+ end
+
+ def assert_ready(port) do
+ assert_receive {^port, {:data, data}}
+ assert :ready == :erlang.binary_to_term(data)
+ end
+
+ def too_big() do
+ "/tmp/testmagicex/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ end
+end
diff --git a/test/gen_magic/server_test.exs b/test/gen_magic/server_test.exs
index e152145..53e5830 100644
--- a/test/gen_magic/server_test.exs
+++ b/test/gen_magic/server_test.exs
@@ -1,30 +1,34 @@
defmodule GenMagic.ServerTest do
use GenMagic.MagicCase
doctest GenMagic.Server
describe "recycle_threshold" do
test "resets" do
{:ok, pid} = GenMagic.Server.start_link(recycle_threshold: 3)
path = absolute_path("Makefile")
assert {:ok, %{cycles: 0}} = GenMagic.Server.status(pid)
assert {:ok, _} = GenMagic.Server.perform(pid, path)
assert {:ok, %{cycles: 1}} = GenMagic.Server.status(pid)
assert {:ok, _} = GenMagic.Server.perform(pid, path)
assert {:ok, %{cycles: 2}} = GenMagic.Server.status(pid)
assert {:ok, _} = GenMagic.Server.perform(pid, path)
+ Process.sleep(100)
assert {:ok, %{cycles: 0}} = GenMagic.Server.status(pid)
end
test "resets before reply" do
{:ok, pid} = GenMagic.Server.start_link(recycle_threshold: 1)
path = absolute_path("Makefile")
assert {:ok, %{cycles: 0}} = GenMagic.Server.status(pid)
assert {:ok, _} = GenMagic.Server.perform(pid, path)
+ Process.sleep(100)
assert {:ok, %{cycles: 0}} = GenMagic.Server.status(pid)
assert {:ok, _} = GenMagic.Server.perform(pid, path)
+ Process.sleep(100)
assert {:ok, %{cycles: 0}} = GenMagic.Server.status(pid)
assert {:ok, _} = GenMagic.Server.perform(pid, path)
+ Process.sleep(100)
assert {:ok, %{cycles: 0}} = GenMagic.Server.status(pid)
end
end
end
diff --git a/test/support/magic_case.ex b/test/support/magic_case.ex
index 4915861..53df01e 100644
--- a/test/support/magic_case.ex
+++ b/test/support/magic_case.ex
@@ -1,33 +1,33 @@
defmodule GenMagic.MagicCase do
@moduledoc false
use ExUnit.CaseTemplate
using do
quote do
import unquote(__MODULE__)
end
end
def missing_filename do
:crypto.strong_rand_bytes(8) |> Base.url_encode64()
end
def files_stream do
Path.join(File.cwd!(), "deps/**/*")
|> Path.wildcard()
|> Stream.reject(&File.dir?/1)
|> Stream.chunk_every(10)
|> Stream.flat_map(&Enum.shuffle/1)
end
- def assert_no_file({:error, message}) do
- assert "unable to access file" = message
+ def assert_no_file(message) do
+ assert {:error, :enoent} = message
end
def absolute_path(path) do
__ENV__.file
|> Path.join("../../..")
|> Path.join(path)
|> Path.expand()
end
end

File Metadata

Mime Type
text/x-diff
Expires
Thu, Nov 28, 10:28 AM (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40776
Default Alt Text
(35 KB)

Event Timeline