mirror of
https://github.com/openharmony/third_party_sane-airscan.git
synced 2026-07-01 21:14:00 -04:00
684 lines
19 KiB
C
684 lines
19 KiB
C
/* sane-airscan zeroconf test
|
|
*
|
|
* Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com)
|
|
* See LICENSE for license terms and conditions
|
|
*/
|
|
|
|
#include "airscan.h"
|
|
|
|
#include <errno.h>
|
|
#include <glob.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#define TEST_FILES "testdata/test-zeroconf*.cfg"
|
|
#define TRACE_DIR "testdata/logs"
|
|
|
|
static const char *test_file;
|
|
static zeroconf_finding **findings = NULL;
|
|
|
|
/* Print error message and exit
|
|
*/
|
|
void __attribute__((noreturn))
|
|
die (const char *format, ...)
|
|
{
|
|
va_list ap;
|
|
|
|
va_start(ap, format);
|
|
vprintf(format, ap);
|
|
printf("\n");
|
|
va_end(ap);
|
|
|
|
exit(1);
|
|
}
|
|
|
|
/* devlist_item represents device list item
|
|
*/
|
|
typedef struct devlist_item devlist_item;
|
|
struct devlist_item {
|
|
const char *name; /* Device name */
|
|
ID_PROTO proto; /* Device protocol */
|
|
zeroconf_endpoint *endpoints; /* Device endpoints */
|
|
devlist_item *next; /* Next item in the list */
|
|
const char *file; /* Test file */
|
|
unsigned int line; /* Line in the test file */
|
|
};
|
|
|
|
/* Free device list
|
|
*/
|
|
static void
|
|
devlist_free (devlist_item *devlist)
|
|
{
|
|
devlist_item *next;
|
|
|
|
while (devlist != NULL) {
|
|
next = devlist->next;
|
|
mem_free((char*) devlist->name);
|
|
zeroconf_endpoint_list_free(devlist->endpoints);
|
|
mem_free(devlist);
|
|
devlist = next;
|
|
}
|
|
}
|
|
|
|
/* Revert device list
|
|
*/
|
|
static devlist_item*
|
|
devlist_revert (devlist_item *devlist)
|
|
{
|
|
devlist_item *reverted = NULL, *next;
|
|
|
|
while (devlist != NULL) {
|
|
next = devlist->next;
|
|
devlist->next = reverted;
|
|
reverted = devlist;
|
|
devlist = next;
|
|
}
|
|
|
|
return reverted;
|
|
}
|
|
|
|
/* Obtain list of devices from zeroconf
|
|
*/
|
|
static devlist_item*
|
|
devlist_from_zeroconf (void)
|
|
{
|
|
const SANE_Device **devices;
|
|
int i;
|
|
devlist_item *devlist = NULL;
|
|
|
|
devices = zeroconf_device_list_get();
|
|
for (i = 0; devices[i] != NULL; i ++) {
|
|
devlist_item *item = mem_new(devlist_item, 1);
|
|
zeroconf_devinfo *devinfo;
|
|
|
|
devinfo = zeroconf_devinfo_lookup(devices[i]->name);
|
|
if (devinfo == NULL) {
|
|
die("%s: zeroconf_devinfo_lookup() failed)", devices[i]->name);
|
|
}
|
|
|
|
item->name = str_dup(devinfo->name);
|
|
item->proto = id_proto_by_name(devices[i]->vendor);
|
|
item->endpoints = devinfo->endpoints;
|
|
devinfo->endpoints = NULL;
|
|
zeroconf_devinfo_free(devinfo);
|
|
|
|
item->next = devlist;
|
|
devlist = item;
|
|
}
|
|
zeroconf_device_list_free(devices);
|
|
|
|
return devlist_revert(devlist);
|
|
}
|
|
|
|
/* Parse device list item from configuration file record
|
|
*/
|
|
static devlist_item*
|
|
devlist_item_parse (const inifile_record *rec)
|
|
{
|
|
devlist_item *item = mem_new(devlist_item, 1);
|
|
unsigned int i;
|
|
|
|
if (rec->tokc < 2) {
|
|
die("%s:%d: usage: %s = protocol, endpoint, ...",
|
|
rec->file, rec->line, rec->variable);
|
|
}
|
|
|
|
item->name = str_dup(rec->variable);
|
|
item->proto = id_proto_by_name(rec->tokv[0]);
|
|
item->file = rec->file;
|
|
item->line = rec->line;
|
|
|
|
if (item->proto == ID_PROTO_UNKNOWN) {
|
|
die("%s:%d: unknown protocol %s",
|
|
rec->file, rec->line, rec->variable, rec->tokv[0]);
|
|
}
|
|
|
|
for (i = 1; i < rec->tokc; i ++) {
|
|
http_uri *uri = http_uri_new(rec->tokv[i], true);
|
|
zeroconf_endpoint *endpoint;
|
|
if (uri == NULL) {
|
|
die("%s:%d: invalid URI %s",
|
|
rec->file, rec->line, rec->variable, rec->tokv[i]);
|
|
}
|
|
|
|
endpoint = zeroconf_endpoint_new(item->proto, uri);
|
|
endpoint->next = item->endpoints;
|
|
item->endpoints = endpoint;
|
|
}
|
|
|
|
item->endpoints = zeroconf_endpoint_list_sort(item->endpoints);
|
|
return item;
|
|
}
|
|
|
|
/* Compare 2 device lists
|
|
*/
|
|
static void
|
|
devlist_compare (devlist_item *expected, devlist_item *discovered)
|
|
{
|
|
while (expected != NULL && discovered != NULL) {
|
|
zeroconf_endpoint *ep_expected = expected->endpoints;
|
|
zeroconf_endpoint *ep_discovered = discovered->endpoints;
|
|
|
|
if (strcmp(expected->name, discovered->name)) {
|
|
die("%s:%d: name mismatch: expected '%s', discovered '%s'",
|
|
expected->file, expected->line,
|
|
expected->name, discovered->name);
|
|
}
|
|
|
|
if (expected->proto != discovered->proto) {
|
|
die("%s:%d: proto mismatch: expected %s, discovered %s",
|
|
expected->file, expected->line,
|
|
id_proto_name(expected->proto),
|
|
id_proto_name(discovered->proto));
|
|
}
|
|
|
|
while (ep_expected != NULL && ep_discovered != NULL) {
|
|
if (!http_uri_equal(ep_expected->uri, ep_discovered->uri)) {
|
|
die("%s:%d: uri mismatch: expected %s, discovered %s",
|
|
expected->file, expected->line,
|
|
http_uri_str(ep_expected->uri),
|
|
http_uri_str(ep_discovered->uri));
|
|
}
|
|
|
|
ep_expected = ep_expected->next;
|
|
ep_discovered = ep_discovered->next;
|
|
}
|
|
|
|
if (ep_expected != NULL && ep_discovered == NULL) {
|
|
die("%s:%d: uri expected but not discovered: %s",
|
|
expected->file, expected->line,
|
|
http_uri_str(ep_expected->uri));
|
|
}
|
|
|
|
if (ep_expected == NULL && ep_discovered != NULL) {
|
|
die("%s:%d: uri not expected but discovered: %s",
|
|
expected->file, expected->line,
|
|
http_uri_str(ep_discovered->uri));
|
|
}
|
|
|
|
expected = expected->next;
|
|
discovered = discovered->next;
|
|
}
|
|
|
|
if (expected != NULL && discovered == NULL) {
|
|
die("%s:%d: device '%s' expected, but not discovered",
|
|
expected->file, expected->line, expected->name);
|
|
}
|
|
|
|
if (expected == NULL && discovered != NULL) {
|
|
die("'%s': device not expected, but discovered", discovered->name);
|
|
}
|
|
}
|
|
|
|
/* Parse ZEROCONF_METHOD
|
|
*/
|
|
static ZEROCONF_METHOD
|
|
parse_zeroconf_method (const inifile_record *rec)
|
|
{
|
|
static struct { const char *name; ZEROCONF_METHOD method; } methods[] = {
|
|
{"MDNS_HINT", ZEROCONF_MDNS_HINT},
|
|
{"USCAN_TCP", ZEROCONF_USCAN_TCP},
|
|
{"USCANS_TCP", ZEROCONF_USCANS_TCP},
|
|
{"WSD", ZEROCONF_WSD},
|
|
{NULL, 0}
|
|
};
|
|
int i;
|
|
char *usage;
|
|
|
|
for (i = 0; methods[i].name != NULL; i ++) {
|
|
if (inifile_match_name(rec->value, methods[i].name)) {
|
|
return methods[i].method;
|
|
}
|
|
}
|
|
|
|
usage = str_dup(methods[0].name);
|
|
for (i = 1; methods[i].name != NULL; i ++) {
|
|
usage = str_append(usage, "|");
|
|
usage = str_append(usage, methods[i].name);
|
|
}
|
|
|
|
die("%s:%d: usage: %s = %s", rec->file, rec->line, rec->variable, usage);
|
|
return -1;
|
|
}
|
|
|
|
/* Parse unsigned integer
|
|
*/
|
|
static int
|
|
parse_uint (const inifile_record *rec)
|
|
{
|
|
char *end;
|
|
unsigned long n = strtoul(rec->value, &end, 0);
|
|
|
|
if (end == rec->value || *end) {
|
|
die("%s:%d: usage: %s = NUM", rec->file, rec->line, rec->variable);
|
|
}
|
|
|
|
return (int) n;
|
|
}
|
|
|
|
/* Get finding by name
|
|
*/
|
|
static zeroconf_finding*
|
|
finding_by_name(ZEROCONF_METHOD method, int ifindex, const char *name)
|
|
{
|
|
size_t len = mem_len(findings);
|
|
size_t i;
|
|
|
|
for (i = 0; i < len; i ++) {
|
|
if (findings[i]->method == method &&
|
|
findings[i]->ifindex == ifindex &&
|
|
!strcasecmp(findings[i]->name, name)) {
|
|
return findings[i];
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Get finding by UUID
|
|
*/
|
|
static zeroconf_finding*
|
|
finding_by_uuid(ZEROCONF_METHOD method, int ifindex, uuid uuid)
|
|
{
|
|
size_t len = mem_len(findings);
|
|
size_t i;
|
|
|
|
for (i = 0; i < len; i ++) {
|
|
if (findings[i]->method == method &&
|
|
findings[i]->ifindex == ifindex &&
|
|
uuid_equal(findings[i]->uuid, uuid)) {
|
|
return findings[i];
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Get finding by name or UUID
|
|
*/
|
|
static zeroconf_finding*
|
|
finding_find(ZEROCONF_METHOD method, int ifindex, const char *name, uuid uuid)
|
|
{
|
|
if (name != NULL) {
|
|
return finding_by_name(method, ifindex, name);
|
|
} else {
|
|
return finding_by_uuid(method, ifindex, uuid);
|
|
}
|
|
}
|
|
|
|
/* Free the zeroconf_finding
|
|
*/
|
|
static void
|
|
finding_free (zeroconf_finding *finding)
|
|
{
|
|
ip_addrset_free(finding->addrs);
|
|
mem_free((char*) finding->name);
|
|
mem_free((char*) finding->model);
|
|
zeroconf_endpoint_list_free(finding->endpoints);
|
|
mem_free(finding);
|
|
}
|
|
|
|
/* Parse and execute [add] or [del] section
|
|
*
|
|
* These sections contains zeroconf_finding-s (one per section)
|
|
* to be added or deleted (as if they were actually discovered)
|
|
*
|
|
* Parameters are:
|
|
* method = MDNS_HINT | discovery method,
|
|
* USCAN_TCP | maps to ZEROCONF_METHOD
|
|
* USCANS_TCP |
|
|
* WSD
|
|
*
|
|
* name = "device name" DNS-SD device name, ignored
|
|
* for WSD
|
|
*
|
|
* model = "model name" model name
|
|
*
|
|
* uuid = 00000000-0000-0000-0000-000000000001 device UUID
|
|
*
|
|
* ifindex = N network interface index
|
|
*
|
|
* endpoint = URL HTTP url of device endpoint,
|
|
* may be used multiple times
|
|
*
|
|
* Note, for [del] section records, only method, ifindex name and uuid
|
|
* parameters are used
|
|
*/
|
|
static const inifile_record*
|
|
test_section_add_del (inifile *ini, const inifile_record *rec, bool add)
|
|
{
|
|
ZEROCONF_METHOD method = (ZEROCONF_METHOD) -1;
|
|
ID_PROTO proto = ID_PROTO_UNKNOWN;
|
|
char *name = NULL;
|
|
char *model = NULL;
|
|
uuid uuid;
|
|
int ifindex = -1;
|
|
zeroconf_endpoint *endpoints = NULL;
|
|
const char *section_file = rec->file;
|
|
unsigned int section_line = rec->line;
|
|
zeroconf_finding *finding;
|
|
|
|
/* Parse the section */
|
|
memset(&uuid, 0, sizeof(uuid));
|
|
rec = inifile_read(ini);
|
|
while (rec != NULL && rec->type == INIFILE_VARIABLE) {
|
|
if (inifile_match_name(rec->variable, "method")) {
|
|
method = parse_zeroconf_method(rec);
|
|
switch (method) {
|
|
case ZEROCONF_USCAN_TCP:
|
|
case ZEROCONF_USCANS_TCP:
|
|
proto = ID_PROTO_ESCL;
|
|
break;
|
|
case ZEROCONF_WSD:
|
|
proto = ID_PROTO_WSD;
|
|
break;
|
|
default:
|
|
proto = ID_PROTO_UNKNOWN;
|
|
}
|
|
} else if (inifile_match_name(rec->variable, "name")) {
|
|
mem_free(name);
|
|
name = str_dup(rec->value);
|
|
} else if (inifile_match_name(rec->variable, "model")) {
|
|
mem_free(model);
|
|
model = str_dup(rec->value);
|
|
} else if (inifile_match_name(rec->variable, "uuid")) {
|
|
uuid = uuid_parse(rec->value);
|
|
if (!uuid_valid(uuid)) {
|
|
die("%s:%d: bad UUID", rec->file, rec->line);
|
|
}
|
|
} else if (inifile_match_name(rec->variable, "ifindex")) {
|
|
ifindex = parse_uint(rec);
|
|
} else if (inifile_match_name(rec->variable, "endpoint")) {
|
|
http_uri *uri;
|
|
zeroconf_endpoint *endpoint;
|
|
|
|
if (proto == ID_PROTO_UNKNOWN) {
|
|
die("%s:%d: protocol not known; set method first",
|
|
rec->file, rec->line);
|
|
}
|
|
|
|
uri = http_uri_new(rec->value, true);
|
|
if (uri == NULL) {
|
|
die("%s:%d: invalid URI", rec->file, rec->line);
|
|
}
|
|
|
|
endpoint = zeroconf_endpoint_new(proto, uri);
|
|
endpoint->next = endpoints;
|
|
endpoints = endpoint;
|
|
} else {
|
|
die("%s:%d: unknown parameter %s", rec->file, rec->line,
|
|
rec->variable);
|
|
}
|
|
|
|
rec = inifile_read(ini);
|
|
}
|
|
|
|
/* In a case of obviously broken file, return immediately */
|
|
if (rec != NULL && rec->type != INIFILE_SECTION) {
|
|
return rec;
|
|
}
|
|
|
|
/* Validate things */
|
|
if (method == (ZEROCONF_METHOD) -1) {
|
|
die("%s:%d: missed method", section_file, section_line);
|
|
}
|
|
|
|
if (method != ZEROCONF_WSD && name == NULL) {
|
|
die("%s:%d: missed name", section_file, section_line);
|
|
}
|
|
|
|
if (method == ZEROCONF_WSD && name != NULL) {
|
|
mem_free(name);
|
|
name = NULL;
|
|
}
|
|
|
|
if (model == NULL && add) {
|
|
die("%s:%d: missed model", section_file, section_line);
|
|
}
|
|
|
|
if (!uuid_valid(uuid)) {
|
|
die("%s:%d: missed uuid", section_file, section_line);
|
|
}
|
|
|
|
if (ifindex == -1) {
|
|
die("%s:%d: missed ifindex", section_file, section_line);
|
|
}
|
|
|
|
if (method != ZEROCONF_MDNS_HINT && add && endpoints == NULL) {
|
|
die("%s:%d: missed endpoint", section_file, section_line);
|
|
}
|
|
|
|
/* Perform an action */
|
|
finding = finding_find(method, ifindex, name, uuid);
|
|
if (add) {
|
|
zeroconf_endpoint *endpoint;
|
|
|
|
if (finding != NULL) {
|
|
die("%s:%d: duplicate [add]", section_file, section_line);
|
|
}
|
|
|
|
finding = mem_new(zeroconf_finding, 1);
|
|
finding->method = method;
|
|
finding->name = name;
|
|
finding->model = model;
|
|
finding->uuid = uuid;
|
|
finding->addrs = ip_addrset_new();
|
|
finding->ifindex = ifindex;
|
|
finding->endpoints = zeroconf_endpoint_list_sort(endpoints);
|
|
|
|
for (endpoint = finding->endpoints; endpoint != NULL;
|
|
endpoint = endpoint->next) {
|
|
const struct sockaddr *sockaddr = http_uri_addr(endpoint->uri);
|
|
if (sockaddr != NULL) {
|
|
ip_addrset_add(finding->addrs, ip_addr_from_sockaddr(sockaddr));
|
|
}
|
|
}
|
|
|
|
zeroconf_finding_publish(finding);
|
|
findings = ptr_array_append(findings, finding);
|
|
} else {
|
|
if (finding == NULL) {
|
|
die("%s:%d: can't find device to [del]", section_file, section_line);
|
|
}
|
|
zeroconf_finding_withdraw(finding);
|
|
ptr_array_del(findings, ptr_array_find(findings, finding));
|
|
finding_free(finding);
|
|
|
|
mem_free(name);
|
|
mem_free(model);
|
|
zeroconf_endpoint_list_free(endpoints);
|
|
}
|
|
|
|
return rec;
|
|
}
|
|
|
|
/* Parse and execute [expect] or [merged] section
|
|
*
|
|
* These sections contains the final expected state of the zeroconf
|
|
* engine
|
|
*
|
|
* After running the test, list of "discovered" devices is
|
|
* compared against content of these sections.
|
|
*
|
|
* List of "disciveder" devices is defined in the form, similar to
|
|
* the airscan-discover output:
|
|
*
|
|
* [expect]
|
|
* "device 1" = escl, http://192.168.0.1/eSCL
|
|
* "device 1" = wsd, http://192.168.0.1/wsd
|
|
*
|
|
* The difference between [expect] and [merged] sections is in
|
|
* representation of the multi-protocol (i.e. eSCL+WSD) devices.
|
|
* [expect] section will contain all found instances, while
|
|
* [merged] section will only contain the instances with the
|
|
* "best" (i.e., automatically chosen) protocol. The difference
|
|
* is exactly the same as between protocol = auto and prococol = manual
|
|
* discovery modes of sane-airscan
|
|
*/
|
|
static const inifile_record*
|
|
test_section_expect (inifile *ini, const inifile_record *rec, bool merged)
|
|
{
|
|
devlist_item *expected = NULL, *discovered;
|
|
|
|
conf.proto_auto = merged;
|
|
|
|
/* Parse the section */
|
|
rec = inifile_read(ini);
|
|
while (rec != NULL && rec->type == INIFILE_VARIABLE) {
|
|
devlist_item *item = devlist_item_parse(rec);
|
|
item->next = expected;
|
|
expected = item;
|
|
rec = inifile_read(ini);
|
|
}
|
|
|
|
/* In a case of obviously broken file, return immediately */
|
|
if (rec != NULL && rec->type != INIFILE_SECTION) {
|
|
devlist_free(expected);
|
|
return rec;
|
|
}
|
|
|
|
expected = devlist_revert(expected);
|
|
discovered = devlist_from_zeroconf();
|
|
|
|
devlist_compare(expected, discovered);
|
|
|
|
devlist_free(discovered);
|
|
devlist_free(expected);
|
|
|
|
return rec;
|
|
}
|
|
|
|
/* Load and execute next test file section
|
|
* Returns inifile_record that follows the section
|
|
*/
|
|
static const inifile_record*
|
|
test_section (inifile *ini, const inifile_record *rec)
|
|
{
|
|
if (inifile_match_name(rec->section, "add")) {
|
|
rec = test_section_add_del(ini, rec, true);
|
|
} else if (inifile_match_name(rec->section, "del")) {
|
|
rec = test_section_add_del(ini, rec, false);
|
|
} else if (inifile_match_name(rec->section, "expect")) {
|
|
rec = test_section_expect(ini, rec, false);
|
|
} else if (inifile_match_name(rec->section, "merged")) {
|
|
rec = test_section_expect(ini, rec, true);
|
|
} else {
|
|
die("%s:%d: unexpected section [%s]", rec->file, rec->line,
|
|
rec->section);
|
|
}
|
|
return rec;
|
|
}
|
|
|
|
/* Load and execute all sections from the test file
|
|
*/
|
|
static void
|
|
test_all (inifile *ini)
|
|
{
|
|
const inifile_record *rec;
|
|
|
|
rec = inifile_read(ini);
|
|
while (rec != NULL) {
|
|
if (rec->type == INIFILE_SECTION) {
|
|
rec = test_section(ini, rec);
|
|
} else if (rec->type == INIFILE_SYNTAX) {
|
|
die("%s:%d: sytnax error", rec->file, rec->line);
|
|
} else {
|
|
die("%s:%d: section expected", rec->file, rec->line);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Run test in the eloop thread context
|
|
*/
|
|
static void
|
|
run_test_in_eloop_thread (void)
|
|
{
|
|
inifile *ini;
|
|
size_t i, len;
|
|
|
|
findings = ptr_array_new(zeroconf_finding*);
|
|
|
|
ini = inifile_open(test_file);
|
|
if (ini == NULL) {
|
|
die("%s: %s", test_file, strerror(errno));
|
|
}
|
|
|
|
test_all(ini);
|
|
|
|
inifile_close(ini);
|
|
|
|
for (i = 0, len = mem_len(findings); i < len; i ++) {
|
|
zeroconf_finding_withdraw(findings[i]);
|
|
finding_free(findings[i]);
|
|
}
|
|
|
|
mem_free(findings);
|
|
findings = NULL;
|
|
}
|
|
|
|
/* eloop_add_start_stop_callback callback
|
|
*/
|
|
static void
|
|
start_stop_callback (bool start)
|
|
{
|
|
if (start) {
|
|
run_test_in_eloop_thread();
|
|
}
|
|
}
|
|
|
|
/* Run test, using specified test file
|
|
*/
|
|
static void run_test (const char *file)
|
|
{
|
|
char title[1024];
|
|
|
|
conf.dbg_enabled = true;
|
|
conf.dbg_trace = str_dup(TRACE_DIR);
|
|
conf.discovery = false;
|
|
conf.proto_auto = false;
|
|
conf.model_is_netname = true;
|
|
|
|
test_file = file;
|
|
|
|
sprintf(title, "=== %s ===", file);
|
|
airscan_init(AIRSCAN_INIT_NO_CONF | AIRSCAN_INIT_NO_THREAD, title);
|
|
eloop_add_start_stop_callback(start_stop_callback);
|
|
eloop_thread_start();
|
|
eloop_thread_stop();
|
|
airscan_cleanup(NULL);
|
|
}
|
|
|
|
/* glob() error callback
|
|
*/
|
|
static int
|
|
glob_errfunc (const char *path, int err)
|
|
{
|
|
die("%s: %s", path, strerror(err));
|
|
}
|
|
|
|
/* The main function
|
|
*/
|
|
int
|
|
main (void)
|
|
{
|
|
glob_t glob_data;
|
|
int rc;
|
|
size_t i;
|
|
|
|
rc = glob(TEST_FILES, 0, glob_errfunc, &glob_data);
|
|
if (rc != 0) {
|
|
die("glob(%s): error %d", TEST_FILES, rc);
|
|
}
|
|
|
|
for (i = 0; i < glob_data.gl_pathc; i ++) {
|
|
run_test(glob_data.gl_pathv[i]);
|
|
}
|
|
|
|
globfree(&glob_data);
|
|
}
|
|
|
|
/* vim:ts=8:sw=4:et
|
|
*/
|