diff options
Diffstat (limited to 'user')
-rw-r--r-- | user/sane-airscan/APKBUILD | 9 | ||||
-rw-r--r-- | user/sane-airscan/git.patch | 1212 |
2 files changed, 1218 insertions, 3 deletions
diff --git a/user/sane-airscan/APKBUILD b/user/sane-airscan/APKBUILD index 8073f32be..c106204f8 100644 --- a/user/sane-airscan/APKBUILD +++ b/user/sane-airscan/APKBUILD @@ -2,7 +2,7 @@ # Maintainer: A. Wilcox <awilfox@adelielinux.org> pkgname=sane-airscan pkgver=0.99.8 -pkgrel=0 +pkgrel=1 pkgdesc="Universal scanner driver for AirScan (eSCL) scanners" url=" " arch="all" @@ -11,7 +11,9 @@ depends="" makedepends="avahi-dev glib-dev libjpeg-turbo-dev libpng-dev libsoup-dev libxml2-dev meson ninja sane-dev" subpackages="$pkgname-doc" -source="sane-airscan-$pkgver.tar.gz::https://github.com/alexpevzner/sane-airscan/archive/$pkgver.tar.gz" +source="sane-airscan-$pkgver.tar.gz::https://github.com/alexpevzner/sane-airscan/archive/$pkgver.tar.gz + git.patch + " build() { meson \ @@ -34,4 +36,5 @@ package() { rm "$pkgdir"/etc/sane.d/dll.conf } -sha512sums="2828deeea31297c64b9927bb2b2b982f16a16cbb446cf2ca685b13f79cb264e94c017b8930fe04004636a6e1122f1882bd58252a3302c0a13b3fc60566072155 sane-airscan-0.99.8.tar.gz" +sha512sums="2828deeea31297c64b9927bb2b2b982f16a16cbb446cf2ca685b13f79cb264e94c017b8930fe04004636a6e1122f1882bd58252a3302c0a13b3fc60566072155 sane-airscan-0.99.8.tar.gz +a195c23edaf399b6fc1ba7693afd34fa87504c9c8ff10d1d793b97c1b592bff90214cd18af764451a3af9860884509fa7701037322a551226a0bcd29473ca1cc git.patch" diff --git a/user/sane-airscan/git.patch b/user/sane-airscan/git.patch new file mode 100644 index 000000000..818580553 --- /dev/null +++ b/user/sane-airscan/git.patch @@ -0,0 +1,1212 @@ +diff --git a/COPYING b/COPYING +index 60549be..d159169 100644 +--- a/COPYING ++++ b/COPYING +@@ -1,12 +1,12 @@ +- GNU GENERAL PUBLIC LICENSE +- Version 2, June 1991 ++ GNU GENERAL PUBLIC LICENSE ++ Version 2, June 1991 + +- Copyright (C) 1989, 1991 Free Software Foundation, Inc. +- 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ++ Copyright (C) 1989, 1991 Free Software Foundation, Inc., ++ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +- Preamble ++ Preamble + + The licenses for most software are designed to take away your + freedom to share and change it. By contrast, the GNU General Public +@@ -15,7 +15,7 @@ software--to make sure the software is free for all its users. This + General Public License applies to most of the Free Software + Foundation's software and to any other program whose authors commit to + using it. (Some other Free Software Foundation software is covered by +-the GNU Library General Public License instead.) You can apply it to ++the GNU Lesser General Public License instead.) You can apply it to + your programs, too. + + When we speak of free software, we are referring to freedom, not +@@ -55,8 +55,8 @@ patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and + modification follow. +- +- GNU GENERAL PUBLIC LICENSE ++ ++ GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +@@ -110,7 +110,7 @@ above, provided that you also meet all of these conditions: + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) +- ++ + These requirements apply to the modified work as a whole. If + identifiable sections of that work are not derived from the Program, + and can be reasonably considered independent and separate works in +@@ -168,7 +168,7 @@ access to copy from a designated place, then offering equivalent + access to copy the source code from the same place counts as + distribution of the source code, even though third parties are not + compelled to copy the source along with the object code. +- ++ + 4. You may not copy, modify, sublicense, or distribute the Program + except as expressly provided under this License. Any attempt + otherwise to copy, modify, sublicense or distribute the Program is +@@ -225,7 +225,7 @@ impose that choice. + + This section is intended to make thoroughly clear what is believed to + be a consequence of the rest of this License. +- ++ + 8. If the distribution and/or use of the Program is restricted in + certain countries either by patents or by copyrighted interfaces, the + original copyright holder who places the Program under this License +@@ -255,7 +255,7 @@ make exceptions for this. Our decision will be guided by the two goals + of preserving the free status of all derivatives of our free software and + of promoting the sharing and reuse of software generally. + +- NO WARRANTY ++ NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY + FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +@@ -277,9 +277,9 @@ YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER + PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGES. + +- END OF TERMS AND CONDITIONS +- +- How to Apply These Terms to Your New Programs ++ END OF TERMS AND CONDITIONS ++ ++ How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest + possible use to the public, the best way to achieve this is to make it +@@ -291,7 +291,7 @@ convey the exclusion of warranty; and each file should have at least + the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> +- Copyright (C) 19yy <name of author> ++ Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by +@@ -303,17 +303,16 @@ the "copyright" line and a pointer to where the full notice is found. + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + +- You should have received a copy of the GNU General Public License +- along with this program; if not, write to the Free Software +- Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +- ++ You should have received a copy of the GNU General Public License along ++ with this program; if not, write to the Free Software Foundation, Inc., ++ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + Also add information on how to contact you by electronic and paper mail. + + If the program is interactive, make it output a short notice like this + when it starts in an interactive mode: + +- Gnomovision version 69, Copyright (C) 19yy name of author ++ Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. +@@ -336,5 +335,5 @@ necessary. Here is a sample; alter the names: + This General Public License does not permit incorporating your program into + proprietary programs. If your program is a subroutine library, you may + consider it more useful to permit linking proprietary applications with the +-library. If this is what you want to do, use the GNU Library General ++library. If this is what you want to do, use the GNU Lesser General + Public License instead of this License. +diff --git a/LICENSE b/LICENSE +index fe71f02..db02cb9 100644 +--- a/LICENSE ++++ b/LICENSE +@@ -16,8 +16,8 @@ General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software +-Foundation, Inc., 59 Temple Place - Suite 330, Boston, +-MA 02111-1307, USA. ++Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, ++MA 02110-1301 USA. + + As a special exception, the authors of sane-airscan give permission for + additional uses of the libraries contained in this release of sane-airscan. +diff --git a/Makefile b/Makefile +index 9d4e082..70555a8 100644 +--- a/Makefile ++++ b/Makefile +@@ -17,7 +17,7 @@ + + CC = gcc + COMPRESS = gzip +-CFLAGS = -O2 -g -W -Wall -Werror ++CFLAGS += -O2 -g -W -Wall -Werror $(CPPFLAGS) + MANDIR = /usr/share/man/ + PKG_CONFIG = /usr/bin/pkg-config + STRIP = -s +@@ -82,7 +82,7 @@ $(BACKEND): $(OBJDIR)airscan.o $(LIBAIRSCAN) airscan.sym + $(CC) -o $(BACKEND) -shared $(OBJDIR)/airscan.o $(LIBAIRSCAN) $(airscan_LDFLAGS) + + $(DISCOVER): $(OBJDIR)discover.o $(LIBAIRSCAN) +- $(CC) -o $(DISCOVER) discover.c $(CPPFLAGS) $(airscan_CFLAGS) $(LIBAIRSCAN) $(airscan_LIBS) -fPIE ++ $(CC) -o $(DISCOVER) discover.c $(CPPFLAGS) $(airscan_CFLAGS) $(LIBAIRSCAN) $(airscan_LIBS) $(LDFLAGS) -fPIE + + $(LIBAIRSCAN): $(OBJ) Makefile + ar cru $(LIBAIRSCAN) $(OBJ) +@@ -105,6 +105,13 @@ clean: + rm -f test $(BACKEND) tags + rm -rf $(OBJDIR) + ++uninstall: ++ rm -f $(DESTDIR)$(PREFIX)$(BINDIR)/$(DISCOVER) ++ rm -f $(DESTDIR)$(PREFIX)$(CONFDIR)/dll.d/airscan ++ rm -f $(DESTDIR)$(PREFIX)$(LIBDIR)/sane/$(BACKEND) ++ rm -f $(DESTDIR)$(PREFIX)$(MANDIR)/man1/$(MAN_DISCOVER)* ++ rm -f $(DESTDIR)$(PREFIX)$(MANDIR)/man5/$(MAN_BACKEND)* ++ + man: $(MAN_DISCOVER) $(MAN_BACKEND) + + $(MAN_DISCOVER): $(MAN_DISCOVER).md +@@ -114,7 +121,7 @@ $(MAN_BACKEND): $(MAN_BACKEND).md + ronn --roff --manual=$(MAN_BACKEND_TITLE) $(MAN_BACKEND).md + + test: $(BACKEND) test.c +- $(CC) -o test test.c $(BACKEND) -Wl,-rpath . ${airscan_CFLAGS} ++ $(CC) -o test test.c $(BACKEND) -Wl,-rpath . $(LDFLAGS) ${airscan_CFLAGS} + + test-decode: test-decode.c $(LIBAIRSCAN) +- $(CC) -o test-decode test-decode.c $(CPPFLAGS) $(airscan_CFLAGS) $(LIBAIRSCAN) $(airscan_LIBS) ++ $(CC) -o test-decode test-decode.c $(CPPFLAGS) $(airscan_CFLAGS) $(LIBAIRSCAN) $(LDFLAGS) $(airscan_LIBS) +diff --git a/README.md b/README.md +index 469a2cd..f05f4d0 100644 +--- a/README.md ++++ b/README.md +@@ -51,10 +51,13 @@ Legend: + | Canon D570 | Yes | | + | Canon ImageCLASS MF743cdw | Yes<sup>[1](#note1)</sup> | | + | Canon imageRUNNER ADVANCE 4545/4551| Yes | Yes | ++| Canon imageRUNNER C3120L | Yes | Yes | + | Canon i-SENSYS MF641C | No | Yes | +-| Canon Lide 400 | Yes | | ++| Canon Lide 400 | Yes<sup>[2](#note1)</sup> | | + | Canon MF745C/746C | Yes | Yes | ++| Canon PIXMA MG5500 Series | No | Yes | + | Canon PIXMA MG7700 Series | Yes | | ++| Canon PIXMA TS5000 Series | Yes | | + | Canon PIXMA TS 9550 Series | Yes | | + | Canon TR4529 (PIXMA TR4500 Series) | Yes | | + | Canon TR7500 Series | No | Yes | +@@ -89,10 +92,10 @@ Legend: + | HP OfficeJet Pro 8730 | Yes | Yes | + | HP OfficeJet Pro 9010 series | Yes | | + | HP Smart Tank Plus 550 series | Yes | | +-| Kyocera ECOSYS M2040dn | Yes | Yes<sup>[2](#note2)</sup> | +-| Lexmark CX317dn | Yes<sup>[3](#note3)</sup> | Yes<sup>[3](#note3)</sup> | ++| Kyocera ECOSYS M2040dn | Yes | Yes<sup>[3](#note2)</sup> | ++| Lexmark CX317dn | Yes<sup>[4](#note3)</sup> | Yes<sup>[4](#note3)</sup> | + | Lexmark MC2535adwe | Yes | | +-| Ricoh MP C3003 | No | Yes<sup>[4](#note3)</sup> | ++| Ricoh MP C3003 | No | Yes<sup>[5](#note3)</sup> | + | Samsung M288x Series | No | Yes | + | Xerox VersaLink B405 | Yes | | + | TODO | | | +@@ -102,15 +105,21 @@ Legend: + scanning on its web console: Home->Menu->Preferences->Network->TCP/IP + Settings->Network Link Scan Settings->On. + +-<a name="note2"><sup>[2]</sup></a>: this device requires manual action on its front ++<a name="note3"><sup>[2]</sup></a>: this USB device supports IPP over USB protocol, which ++allows it to be used with network protocols like eSCL. To enable IPP over USB, you need ++to install additional program: either [ippusbxd](https://github.com/OpenPrinting/ippusbxd), which ++comes with some distros, or [ipp-usb](https://github.com/OpenPrinting/ipp-usb). 'ipp-usb` ++works better, binary packages available for many popular distros. ++ ++<a name="note3"><sup>[3]</sup></a>: this device requires manual action on its front + panel to initiate WSD scan: Send->WSD Scan->From Computer + +-<a name="note3"><sup>[3]</sup></a>: when low in memory, this device may scan at 400 DPI ++<a name="note4"><sup>[4]</sup></a>: when low in memory, this device may scan at 400 DPI + instead of requested 600 DPI. As sane-airscan reports image parameters to SANE before actual + image is received, and then adjust actual image to reported parameters, image will + be scaled down by the factor 2/3 at this case. Lower resolutions works well. + +-<a name="note3"><sup>[4]</sup></a>: by default, WSD scan command is disabled on this ++<a name="note5"><sup>[5]</sup></a>: by default, WSD scan command is disabled on this + device and needs to be enabled before use: Click [Configuration], click [Initial Settings] + under [Scanner], and then set [Prohibit WSD Scan Command] to [Do not Prohibit] (from + http://support.ricoh.com/bb_v1oi/pub_e/oi_view/0001047/0001047003/view/scanner/int/0095.htm) +diff --git a/airscan-device.c b/airscan-device.c +index f76fa23..82e267a 100644 +--- a/airscan-device.c ++++ b/airscan-device.c +@@ -119,7 +119,6 @@ struct device { + SANE_Int read_line_end; /* If read_line_num>read_line_end + no more lines left in image */ + SANE_Int read_line_off; /* Current offset in the line */ +- SANE_Int read_skip_lines; /* How many lines to skip */ + SANE_Int read_skip_bytes; /* How many bytes to skip at line + beginning */ + }; +@@ -487,6 +486,8 @@ static void + device_probe_endpoint (device *dev, zeroconf_endpoint *endpoint) + { + /* Switch endpoint */ ++ log_assert(dev->log, endpoint->proto != ID_PROTO_UNKNOWN); ++ + if (dev->endpoint_current == NULL || + dev->endpoint_current->proto != endpoint->proto) { + device_proto_set(dev, endpoint->proto); +@@ -851,8 +852,8 @@ device_geom; + * First of all, we use 3 different units to deal with geometrical + * parameters: + * 1) we communicate with frontend in millimeters +- * 2) we communicate with scanner in pixels, assuming protoc-specific DPI +- * (defined by devcaps::units) ++ * 2) we communicate with scanner in pixels, assuming ++ * protocol-specific DPI (defined by devcaps::units) + * 3) when we deal with image, sizes are in pixels in real resolution + * + * Second, scanner returns minimal and maximal window size, but +@@ -1289,6 +1290,7 @@ device_read_next (device *dev) + SANE_Parameters params; + image_decoder *decoder = dev->decoders[dev->proto_ctx.params.format]; + int wid, hei; ++ int skip_lines = 0; + + log_assert(dev->log, decoder != NULL); + +@@ -1330,7 +1332,7 @@ device_read_next (device *dev) + /* Setup image clipping */ + if (dev->job_skip_x >= wid || dev->job_skip_y >= hei) { + /* Trivial case - just skip everything */ +- dev->read_skip_lines = hei; ++ dev->read_line_end = 0; + dev->read_skip_bytes = 0; + line_capacity = dev->opt.params.bytes_per_line; + } else { +@@ -1352,12 +1354,12 @@ device_read_next (device *dev) + dev->read_skip_bytes = bpp * (dev->job_skip_x - win.x_off); + } + +- dev->read_skip_lines = 0; + if (win.y_off != dev->job_skip_y) { +- dev->read_skip_lines = dev->job_skip_y - win.y_off; ++ skip_lines = dev->job_skip_y - win.y_off; + } + + line_capacity = math_max(dev->opt.params.bytes_per_line, wid * bpp); ++ line_capacity += dev->read_skip_bytes; + } + + /* Initialize image decoding */ +@@ -1366,7 +1368,14 @@ device_read_next (device *dev) + + dev->read_line_num = 0; + dev->read_line_off = dev->opt.params.bytes_per_line; +- dev->read_line_end = hei - dev->read_skip_lines; ++ dev->read_line_end = hei - skip_lines; ++ ++ for (;skip_lines > 0; skip_lines --) { ++ err = image_decoder_read_line(decoder, dev->read_line_buf); ++ if (err != NULL) { ++ goto DONE; ++ } ++ } + + /* Wake up reader */ + pollable_signal(dev->read_pollable); +@@ -1408,8 +1417,9 @@ device_read_decode_line (device *dev) + return SANE_STATUS_EOF; + } + +- if (n < dev->read_skip_lines || n >= dev->read_line_end) { +- memset(dev->read_line_buf, 0xff, dev->opt.params.bytes_per_line); ++ if (n >= dev->read_line_end) { ++ memset(dev->read_line_buf + dev->read_skip_bytes, 0xff, ++ dev->opt.params.bytes_per_line); + } else { + error err = image_decoder_read_line(decoder, dev->read_line_buf); + +@@ -1419,7 +1429,7 @@ device_read_decode_line (device *dev) + } + } + +- dev->read_line_off = dev->read_skip_bytes; ++ dev->read_line_off = 0; + dev->read_line_num ++; + + return SANE_STATUS_GOOD; +@@ -1482,12 +1492,14 @@ device_read (device *dev, SANE_Byte *data, SANE_Int max_len, SANE_Int *len_out) + /* Read line by line */ + for (len = 0; status == SANE_STATUS_GOOD && len < max_len; ) { + if (dev->read_line_off == dev->opt.params.bytes_per_line) { +- status = device_read_decode_line (dev); ++ status = device_read_decode_line(dev); + } else { + SANE_Int sz = math_min(max_len - len, + dev->opt.params.bytes_per_line - dev->read_line_off); + +- memcpy(data, dev->read_line_buf + dev->read_line_off, sz); ++ memcpy(data, dev->read_line_buf + dev->read_skip_bytes + ++ dev->read_line_off, sz); ++ + data += sz; + dev->read_line_off += sz; + len += sz; +diff --git a/airscan-escl.c b/airscan-escl.c +index dbaa370..163bdd1 100644 +--- a/airscan-escl.c ++++ b/airscan-escl.c +@@ -335,16 +335,16 @@ escl_devcaps_source_parse (xml_rd *xml, devcaps_source **out) + goto DONE; + } + +- if (src->max_wid_px != 0 && src->max_hei_px != 0 ) ++ if (src->max_wid_px != 0 && src->max_hei_px != 0) + { + /* Validate window size */ +- if (src->min_wid_px >= src->max_wid_px ) ++ if (src->min_wid_px > src->max_wid_px) + { + err = ERROR("Invalid scan:MinWidth or scan:MaxWidth"); + goto DONE; + } + +- if (src->min_hei_px >= src->max_hei_px) ++ if (src->min_hei_px > src->max_hei_px) + { + err = ERROR("Invalid scan:MinHeight or scan:MaxHeight"); + goto DONE; +diff --git a/airscan-http.c b/airscan-http.c +index 6758368..ca6f8b0 100644 +--- a/airscan-http.c ++++ b/airscan-http.c +@@ -114,6 +114,7 @@ http_uri_new_relative (const http_uri *base, const char *path, + + /* Free the URI + */ ++#ifndef __clang_analyzer__ + void + http_uri_free (http_uri *uri) + { +@@ -123,6 +124,7 @@ http_uri_free (http_uri *uri) + g_free(uri); + } + } ++#endif + + /* Get URI string + */ +@@ -689,9 +691,21 @@ http_client_onerror (http_client *client, + void + http_client_cancel (http_client *client) + { +- while (client->pending->len != 0) { +- http_query_cancel(client->pending->pdata[0]); ++ size_t i, len = client->pending->len; ++ size_t sz = len * sizeof(http_query*); ++ http_query **qlist = g_alloca(len); ++ ++ /* Note, without this stupid copying of client->pending->pdata, ++ * clang analyzer doesn't understand that http_query_cancel() ++ * has a side effect of removing http_query pointer from the ++ * array and erroneously claims that memory used after free ++ */ ++ memcpy(qlist, client->pending->pdata, sz); ++ for (i = 0; i < len; i ++) { ++ http_query_cancel(qlist[i]); + } ++ ++ log_assert(client->log, client->pending->len == 0); + } + + /* Cancel all pending queries with matching address family and uintptr +@@ -988,12 +1002,13 @@ http_query_cancel (http_query *q) + g_object_ref(q->msg); + soup_session_cancel_message(http_session, q->msg, SOUP_STATUS_CANCELLED); + soup_message_set_status(q->msg, SOUP_STATUS_CANCELLED); +- g_object_unref(q->msg); + + log_debug(q->client->log, "HTTP %s %s: %s", q->msg->method, + http_uri_str(q->uri), + soup_status_get_phrase(SOUP_STATUS_CANCELLED)); + ++ g_object_unref(q->msg); ++ + http_query_free(q); + } + +@@ -1225,6 +1240,8 @@ http_start_stop (bool start) + g_object_set_property(G_OBJECT(http_session), + SOUP_SESSION_SSL_STRICT, &val); + } else { ++ ll_node *node; ++ + soup_session_abort(http_session); + g_object_unref(http_session); + http_session = NULL; +@@ -1232,10 +1249,8 @@ http_start_stop (bool start) + /* Note, soup_session_abort() may leave some requests + * pending, so we must free them here explicitly + */ +- while (!ll_empty(&http_query_list)) { +- ll_node *node = ll_first(&http_query_list); ++ while ((node = ll_pop_beg(&http_query_list)) != NULL) { + http_query *q = OUTER_STRUCT(node, http_query, list_node); +- + http_query_free(q); + } + } +diff --git a/airscan-math.c b/airscan-math.c +index d801304..ee7f301 100644 +--- a/airscan-math.c ++++ b/airscan-math.c +@@ -99,7 +99,6 @@ math_range_merge (SANE_Range *out, const SANE_Range *r1, const SANE_Range *r2) + SANE_Word quant = math_lcm(r1->quant, r2->quant); + SANE_Word min, max, bounds_min, bounds_max; + +- min = math_min(r1->min, r2->min); + bounds_min = math_max(r1->min, r2->min); + bounds_max = math_min(r1->max, r2->max); + +diff --git a/airscan-netif.c b/airscan-netif.c +index a762377..a086792 100644 +--- a/airscan-netif.c ++++ b/airscan-netif.c +@@ -92,6 +92,41 @@ netif_distance_get (const struct sockaddr *addr) + return distance; + } + ++/* Check that interface has non-link-local address ++ * of particular address family ++ */ ++bool ++netif_has_non_link_local_addr (int af, int ifindex) ++{ ++ struct ifaddrs *ifa; ++ ++ for (ifa = netif_ifaddrs; ifa != NULL; ifa = ifa->ifa_next) { ++ struct sockaddr *addr; ++ ++ /* Skip interface without address */ ++ if ((addr = ifa->ifa_addr) == NULL) { ++ continue; ++ } ++ ++ /* Check address family against requested */ ++ if (addr->sa_family != af) { ++ continue; ++ } ++ ++ /* Skip link-local addresses */ ++ if (ip_sockaddr_is_linklocal(addr)) { ++ continue; ++ } ++ ++ /* Check interface index */ ++ if (ifindex == (int) if_nametoindex(ifa->ifa_name)) { ++ return true; ++ } ++ } ++ ++ return false; ++} ++ + /* Get list of network interfaces addresses + */ + netif_addr* +diff --git a/airscan-wsdd.c b/airscan-wsdd.c +index 15bf02a..c53105b 100644 +--- a/airscan-wsdd.c ++++ b/airscan-wsdd.c +@@ -26,6 +26,12 @@ + #define WSDD_RETRANSMIT_MAX 250 /* Max retransmit time */ + #define WSDD_DISCOVERY_TIME 2500 /* Overall discovery time */ + ++/* This delay is taken, if we have discovered, say, device's IPv6 ++ * addresses and have a strong suspicion that device has not yet ++ * discovered IPv4 addresses as well ++ */ ++#define WSDD_PUBLISH_DELAY 1000 ++ + /* WS-Discovery stable endpoint path + */ + #define WSDD_STABLE_ENDPOINT \ +@@ -49,12 +55,13 @@ typedef struct { + * device discovery + */ + typedef struct { +- zeroconf_finding finding; /* Base class */ +- const char *address; /* Device "address" in WS-SD sense */ +- ll_head xaddrs; /* List of wsdd_xaddr */ +- http_client *http_client; /* HTTP client */ +- ll_node list_node; /* In wsdd_finding_list */ +- bool published; /* This finding is published */ ++ zeroconf_finding finding; /* Base class */ ++ const char *address; /* Device "address" in WS-SD sense */ ++ ll_head xaddrs; /* List of wsdd_xaddr */ ++ http_client *http_client; /* HTTP client */ ++ ll_node list_node; /* In wsdd_finding_list */ ++ eloop_timer *publish_timer; /* WSDD_PUBLISH_DELAY timer */ ++ bool published; /* This finding is published */ + } wsdd_finding; + + /* wsdd_xaddr represents device transport address +@@ -207,9 +214,8 @@ wsdd_xaddr_list_purge (ll_head *list) + { + ll_node *node; + +- while ((node = ll_first(list)) != NULL) { ++ while ((node = ll_pop_beg(list)) != NULL) { + wsdd_xaddr *xaddr = OUTER_STRUCT(node, wsdd_xaddr, list_node); +- ll_del(&xaddr->list_node); + wsdd_xaddr_free(xaddr); + } + } +@@ -269,6 +275,10 @@ wsdd_finding_free (wsdd_finding *wsdd) + http_client_cancel(wsdd->http_client); + http_client_free(wsdd->http_client); + ++ if (wsdd->publish_timer != NULL) { ++ eloop_timer_cancel(wsdd->publish_timer); ++ } ++ + zeroconf_endpoint_list_free(wsdd->finding.endpoints); + g_free((char*) wsdd->address); + wsdd_xaddr_list_purge(&wsdd->xaddrs); +@@ -282,17 +292,100 @@ wsdd_finding_free (wsdd_finding *wsdd) + static void + wsdd_finding_publish (wsdd_finding *wsdd) + { +- if (!wsdd->published) { +- wsdd->published = true; +- zeroconf_finding_publish(&wsdd->finding); ++ if (wsdd->published) { ++ return; ++ } ++ ++ wsdd->published = true; ++ wsdd->finding.endpoints = zeroconf_endpoint_list_sort_dedup( ++ wsdd->finding.endpoints); ++ ++ if (wsdd->publish_timer != NULL) { ++ log_debug(wsdd_log, "\"%s\": publish-delay timer canceled", ++ wsdd->finding.model); ++ ++ eloop_timer_cancel(wsdd->publish_timer); ++ wsdd->publish_timer = NULL; ++ } ++ ++ zeroconf_finding_publish(&wsdd->finding); ++} ++ ++/* WSDD_PUBLISH_DELAY timer callback ++ */ ++static void ++wsdd_finding_publish_delay_timer_callback (void *data) ++{ ++ wsdd_finding *wsdd = data; ++ ++ wsdd->publish_timer = NULL; ++ log_debug(wsdd_log, "\"%s\": publish-delay timer expired", ++ wsdd->finding.model); ++ ++ wsdd_finding_publish(wsdd); ++} ++ ++/* Publish wsdd_finding with optional delay ++ */ ++static void ++wsdd_finding_publish_delay (wsdd_finding *wsdd) ++{ ++ bool delay = false; ++ ++ if (wsdd->published) { ++ return; ++ } ++ ++ /* Continue discovery, if interface has IPv4/IPv6 address, ++ * and we have not yet discovered address of the same address ++ * family of device ++ * ++ * Some devices doesn't return their IPv4 endpoints, if ++ * metadata is queried via IPv6, and visa versa. This is ++ * possible that we will finish discovery of the particular ++ * address family, before we'll ever know that the device ++ * may have address of another address family, so part ++ * of addresses will be never discovered (see #44 for details). ++ * ++ * To prevent this situation, we continue discovery with ++ * some reasonable delay, if network interface has IPv4/IPv6 ++ * address, but device is not yet. ++ */ ++ ++ if (netif_has_non_link_local_addr(AF_INET, wsdd->finding.ifindex) && ++ !zeroconf_endpoint_list_has_non_link_local_addr(AF_INET, ++ wsdd->finding.endpoints)) { ++ log_debug(wsdd_log, ++ "\"%s\": IPv4 address expected but not yet discovered", ++ wsdd->finding.model); ++ delay = true; ++ } ++ ++ if (netif_has_non_link_local_addr(AF_INET6, wsdd->finding.ifindex) && ++ !zeroconf_endpoint_list_has_non_link_local_addr(AF_INET6, ++ wsdd->finding.endpoints)) { ++ log_debug(wsdd_log, ++ "\"%s\": IPv6 address expected but not yet discovered", ++ wsdd->finding.model); ++ delay = true; ++ } ++ ++ if (delay) { ++ if (wsdd->publish_timer == NULL) { ++ wsdd->publish_timer = eloop_timer_new(WSDD_PUBLISH_DELAY, ++ wsdd_finding_publish_delay_timer_callback, wsdd); ++ } ++ ++ return; + } ++ ++ wsdd_finding_publish(wsdd); + } + +-/* Add wsdd_finding to the wsdd_finding_list. +- * If finding already present, does nothing and returns NULL ++/* Get existent finding or add a new one + */ + static wsdd_finding* +-wsdd_finding_add (int ifindex, const char *address) ++wsdd_finding_get (int ifindex, const char *address) + { + ll_node *node; + wsdd_finding *wsdd; +@@ -302,7 +395,7 @@ wsdd_finding_add (int ifindex, const char *address) + wsdd = OUTER_STRUCT(node, wsdd_finding, list_node); + if (wsdd->finding.ifindex == ifindex && + !strcmp(wsdd->address, address)) { +- return NULL; ++ return wsdd; + } + } + +@@ -343,6 +436,25 @@ wsdd_finding_by_address (ip_addr addr) + return NULL; + } + ++/* Check if finding already has particular xaddr ++ */ ++static bool ++wsdd_finding_has_xaddr (wsdd_finding *wsdd, const wsdd_xaddr *xaddr) ++{ ++ ll_node *node; ++ wsdd_xaddr *xaddr2; ++ ++ for (LL_FOR_EACH(node, &wsdd->xaddrs)) { ++ xaddr2 = OUTER_STRUCT(node, wsdd_xaddr, list_node); ++ ++ if (http_uri_equal(xaddr->uri, xaddr2->uri)) { ++ return true; ++ } ++ } ++ ++ return false; ++} ++ + /* Delete wsdd_finding from the wsdd_finding_list + */ + static void +@@ -532,24 +644,7 @@ DONE: + g_free(manufacturer); + + if (http_client_num_pending(wsdd->http_client) == 0) { +- zeroconf_endpoint *endpoint; +- +- wsdd->finding.endpoints = zeroconf_endpoint_list_sort_dedup( +- wsdd->finding.endpoints); +- +- log_debug(wsdd_log, "\"%s\": address: %s", +- wsdd->finding.model, wsdd->address); +- log_debug(wsdd_log, "\"%s\": uuid: %s", +- wsdd->finding.model, wsdd->finding.uuid.text); +- log_debug(wsdd_log, "\"%s\": discovered endpoints:", +- wsdd->finding.model); +- +- for (endpoint = wsdd->finding.endpoints; endpoint != NULL; +- endpoint = endpoint->next) { +- log_debug(wsdd_log, " %s", http_uri_str(endpoint->uri)); +- } +- +- wsdd_finding_publish(wsdd); ++ wsdd_finding_publish_delay(wsdd); + } + } + +@@ -738,28 +833,42 @@ wsdd_resolver_message_dispatch (wsdd_resolver *resolver, + goto DONE; + } + +- /* Add a finding. Do nothing if device already exist */ +- wsdd = wsdd_finding_add(resolver->ifindex, msg->address); +- if (wsdd == NULL) { +- goto DONE; +- } ++ /* Add a finding or get existent one */ ++ wsdd = wsdd_finding_get(resolver->ifindex, msg->address); + +- /* If device is not scanner or (which is very unlikely) +- * has no xaddrs, just publish it without endpoints, +- * so zeroconf stuff will know that there is no need to +- * wait anymore for a device with this UUID, and we +- * are done ++ /* Import newly discovered xaddrs and initiate metadata ++ * query + */ +- if (!msg->is_scanner || ll_empty(&msg->xaddrs)) { +- wsdd_finding_publish(wsdd); +- goto DONE; ++ while ((node = ll_pop_beg(&msg->xaddrs)) != NULL) { ++ xaddr = OUTER_STRUCT(node, wsdd_xaddr, list_node); ++ ++ if (wsdd_finding_has_xaddr(wsdd, xaddr)) { ++ wsdd_xaddr_free(xaddr); ++ continue; ++ } ++ ++ ll_push_end(&wsdd->xaddrs, &xaddr->list_node); ++ if (msg->is_scanner) { ++ wsdd_finding_get_metadata(wsdd, resolver->ifindex, xaddr); ++ } + } + +- /* Initiate metadata query */ +- ll_cat(&wsdd->xaddrs, &msg->xaddrs); +- for (LL_FOR_EACH(node, &wsdd->xaddrs)) { +- xaddr = OUTER_STRUCT(node, wsdd_xaddr, list_node); +- wsdd_finding_get_metadata(wsdd, resolver->ifindex, xaddr); ++ /* If there is no pending metadata queries, it may mean ++ * one of the following: ++ * 1) device is not scanner, metadata won't be requested ++ * 2) there is no xaddrs (which is very unlikely, but ++ * just in case...) ++ * 3) device already known and all metadata queries ++ * already finished ++ * ++ * At this case we can publish device now ++ */ ++ if (http_client_num_pending(wsdd->http_client) == 0) { ++ if (msg->is_scanner) { ++ wsdd_finding_publish_delay(wsdd); ++ } else { ++ wsdd_finding_publish(wsdd); ++ } + } + break; + +@@ -774,6 +883,7 @@ wsdd_resolver_message_dispatch (wsdd_resolver *resolver, + /* Cleanup and exit */ + DONE: + wsdd_message_free(msg); ++ log_trace(wsdd_log, ""); + } + + +diff --git a/airscan-zeroconf.c b/airscan-zeroconf.c +index dfff5b8..293a05d 100644 +--- a/airscan-zeroconf.c ++++ b/airscan-zeroconf.c +@@ -129,11 +129,8 @@ zeroconf_device_add (zeroconf_finding *finding) + } + + ll_init(&device->findings); +- +- device->ifaces_cap = ZEROCONF_DEVICE_IFACES_INITIAL_LEN; +- device->ifaces = g_malloc(device->ifaces_cap * sizeof(*device->ifaces)); +- + ll_push_end(&zeroconf_device_list, &device->node_list); ++ + return device; + } + +@@ -233,7 +230,12 @@ zeroconf_device_ifaces_add (zeroconf_device *device, int ifindex) + { + if (!zeroconf_device_ifaces_lookup(device, ifindex)) { + if (device->ifaces_len == device->ifaces_cap) { +- device->ifaces_cap *= 2; ++ if (device->ifaces_cap == 0) { ++ device->ifaces_cap = ZEROCONF_DEVICE_IFACES_INITIAL_LEN; ++ } else { ++ device->ifaces_cap *= 2; ++ } ++ + device->ifaces = g_realloc(device->ifaces, + device->ifaces_cap * sizeof(*device->ifaces)); + } +@@ -260,7 +262,7 @@ zeroconf_device_rebuild_sets (zeroconf_device *device) + finding = OUTER_STRUCT(node, zeroconf_finding, list_node); + proto = zeroconf_method_to_proto(finding->method); + +- zeroconf_device_ifaces_add(device, finding->ifindex ); ++ zeroconf_device_ifaces_add(device, finding->ifindex); + if (proto != ID_PROTO_UNKNOWN) { + device->protocols |= 1 << proto; + } +@@ -316,19 +318,26 @@ static void + zeroconf_device_borrow_findings (zeroconf_device *device, + int ifindex, ll_head *output) + { +- ll_node *node, *next; +- zeroconf_finding *finding; ++ ll_node *node; ++ ll_head leftover; + +- for (node = ll_first(&device->findings); node != NULL; node = next) { +- next = ll_next(&device->findings, node); ++ ll_init(&leftover); ++ ++ while ((node = ll_pop_beg(&device->findings)) != NULL) { ++ zeroconf_finding *finding; + + finding = OUTER_STRUCT(node, zeroconf_finding, list_node); ++ + if (finding->ifindex == ifindex) { + finding->device = NULL; + ll_push_end(output, node); ++ } else { ++ ll_push_end(&leftover, node); + } + } + ++ ll_cat(&device->findings, &leftover); ++ + if (ll_empty(&device->findings)) { + zeroconf_device_del(device); + return; +@@ -556,7 +565,7 @@ zeroconf_ident_split (const char *ident, unsigned int *devid, ID_PROTO *proto) + + /* Decode proto and devid */ + *proto = zeroconf_ident_proto_decode(*ident); +- if (*proto == NUM_ID_PROTO) { ++ if (*proto == ID_PROTO_UNKNOWN) { + return NULL; + } + +@@ -774,6 +783,25 @@ zeroconf_endpoint_list_sort_dedup (zeroconf_endpoint *list) + return list; + } + ++/* Check if endpoints list contains a non-link-local address ++ * of the specified address family ++ */ ++bool ++zeroconf_endpoint_list_has_non_link_local_addr (int af, ++ const zeroconf_endpoint *list) ++{ ++ for (;list != NULL; list = list->next) { ++ const struct sockaddr *addr = http_uri_addr(list->uri); ++ if (addr != NULL && addr->sa_family == af) { ++ if (!ip_sockaddr_is_linklocal(addr)) { ++ return true; ++ } ++ } ++ } ++ ++ return false; ++} ++ + /******************** Static configuration *********************/ + /* Look for device's static configuration by device name + */ +@@ -850,7 +878,7 @@ zeroconf_finding_publish (zeroconf_finding *finding) + */ + device = zeroconf_device_find_by_uuid(finding->uuid); + if (device != NULL && finding->name != NULL) { +- if (device->ifaces_len == 1 && device->ifaces[0] == finding->ifindex ){ ++ if (device->ifaces_len == 1 && device->ifaces[0] == finding->ifindex){ + /* Case 2: all findings belongs to the same network + * interface; upgrade anonymous device to named + */ +@@ -1055,6 +1083,41 @@ zeroconf_device_list_qsort_cmp (const void *p1, const void *p2) + return cmp; + } + ++/* Format list of protocols, for zeroconf_device_list_log ++ */ ++static void ++zeroconf_device_list_fmt_protocols (char *buf, size_t buflen, unsigned int protocols) ++{ ++ ID_PROTO proto; ++ size_t off = 0; ++ ++ buf[0] = '\0'; ++ for (proto = 0; proto < NUM_ID_PROTO; proto ++) { ++ if ((protocols & (1 << proto)) != 0) { ++ off += snprintf(buf + off, buflen - off, " %s", ++ id_proto_name(proto)); ++ } ++ } ++ ++ if (buf[0] == '\0') { ++ strcpy(buf, " none"); ++ } ++} ++ ++/* Log device information in a context of zeroconf_device_list_get ++ */ ++static void ++zeroconf_device_list_log (zeroconf_device *device, const char *name, unsigned int protocols) ++{ ++ char buf[64]; ++ ++ zeroconf_device_list_fmt_protocols(buf, sizeof(buf), device->protocols); ++ log_debug(zeroconf_log, "%s: supported protocols:%s", name, buf); ++ ++ zeroconf_device_list_fmt_protocols(buf, sizeof(buf), protocols); ++ log_debug(zeroconf_log, "%s: chosen protocols:%s", name, buf); ++} ++ + /* Get list of devices, in SANE format + */ + const SANE_Device** +@@ -1065,10 +1128,14 @@ zeroconf_device_list_get (void) + const SANE_Device **dev_list = sane_device_array_new(); + ll_node *node; + ++ log_debug(zeroconf_log, "zeroconf_device_list_get: requested"); ++ + /* Wait until device table is ready */ + zeroconf_initscan_wait(); + + /* Build list of devices */ ++ log_debug(zeroconf_log, "zeroconf_device_list_get: building list of devices"); ++ + dev_count = 0; + + for (dev_conf = conf.devices; dev_conf != NULL; dev_conf = dev_conf->next) { +@@ -1097,8 +1164,18 @@ zeroconf_device_list_get (void) + zeroconf_device_name_model(device, &name, &model); + protocols = zeroconf_device_protocols(device); + ++ zeroconf_device_list_log(device, name, protocols); ++ + if (zeroconf_find_static_by_name(name) != NULL) { + /* Static configuration overrides discovery */ ++ log_debug(zeroconf_log, ++ "%s: skipping, device clashes statically configured", name); ++ continue; ++ } ++ ++ if (protocols == 0) { ++ log_debug(zeroconf_log, ++ "%s: skipping, no of supported protocols discovered", name); + continue; + } + +@@ -1327,6 +1404,8 @@ zeroconf_init (void) + log_trace(zeroconf_log, " protocol = %s", s); + + s = "?"; ++ (void) s; /* Silence CLANG analyzer warning */ ++ + switch (conf.wsdd_mode) { + case WSDD_FAST: s = "fast"; break; + case WSDD_FULL: s = "full"; break; +diff --git a/airscan.h b/airscan.h +index 3aca207..2a36333 100644 +--- a/airscan.h ++++ b/airscan.h +@@ -130,8 +130,10 @@ ll_push_beg (ll_head *head, ll_node *node) + static inline void + ll_del (ll_node *node) + { +- node->ll_prev->ll_next = node->ll_next; +- node->ll_next->ll_prev = node->ll_prev; ++ ll_node *p = node->ll_prev, *n = node->ll_next; ++ ++ p->ll_next = n; ++ n->ll_prev = p; + + /* Make double-delete safe */ + node->ll_next = node->ll_prev = node; +@@ -143,14 +145,19 @@ ll_del (ll_node *node) + static inline ll_node* + ll_pop_beg (ll_head *head) + { +- ll_node *node; ++ ll_node *node, *next; + +- if (ll_empty(head)) { +- return NULL; ++ node = head->node.ll_next; ++ if (node == &head->node) { ++ return NULL; /* List is empty if it is looped to itself */ + } + +- node = head->node.ll_next; +- ll_del(node); ++ next = node->ll_next; ++ next->ll_prev = &head->node; ++ head->node.ll_next = next; ++ ++ /* Make double-delete safe */ ++ node->ll_next = node->ll_prev = node; + + return node; + } +@@ -161,14 +168,19 @@ ll_pop_beg (ll_head *head) + static inline ll_node* + ll_pop_end (ll_head *head) + { +- ll_node *node; ++ ll_node *node, *prev; + +- if (ll_empty(head)) { +- return NULL; ++ node = head->node.ll_prev; ++ if (node == &head->node) { ++ return NULL; /* List is empty if it is looped to itself */ + } + +- node = head->node.ll_prev; +- ll_del(node); ++ prev = node->ll_prev; ++ prev->ll_next = &head->node; ++ head->node.ll_prev = prev; ++ ++ /* Make double-delete safe */ ++ node->ll_next = node->ll_prev = node; + + return node; + } +@@ -702,6 +714,12 @@ typedef enum { + NETIF_DISTANCE + netif_distance_get (const struct sockaddr *addr); + ++/* Check that interface has non-link-local address ++ * of particular address family ++ */ ++bool ++netif_has_non_link_local_addr (int af, int ifindex); ++ + /* Compare addresses by distance. Returns: + * <0, if addr1 is closer that addr2 + * >0, if addr2 is farther that addr2 +@@ -2055,6 +2073,13 @@ zeroconf_endpoint_list_sort (zeroconf_endpoint *list); + zeroconf_endpoint* + zeroconf_endpoint_list_sort_dedup (zeroconf_endpoint *list); + ++/* Check if endpoints list contains a non-link-local address ++ * of the specified address family ++ */ ++bool ++zeroconf_endpoint_list_has_non_link_local_addr (int af, ++ const zeroconf_endpoint *list); ++ + /******************** MDNS Discovery ********************/ + /* Called by zeroconf to notify MDNS about initial scan timer expiration + */ +@@ -2594,6 +2619,7 @@ log_panic (log_ctx *log, const char *fmt, ...); + if (!(expr)) { \ + log_panic(log,"file %s: line %d (%s): assertion failed: (%s)",\ + __FILE__, __LINE__, __PRETTY_FUNCTION__, #expr); \ ++ __builtin_unreachable(); \ + } \ + } while (0) + +@@ -2603,6 +2629,7 @@ log_panic (log_ctx *log, const char *fmt, ...); + do { \ + log_panic(log,"file %s: line %d (%s): internal error", \ + __FILE__, __LINE__, __PRETTY_FUNCTION__); \ ++ __builtin_unreachable(); \ + } while (0) + + #endif +diff --git a/sane-airscan.5 b/sane-airscan.5 +index 84ed929..32656e1 100644 +--- a/sane-airscan.5 ++++ b/sane-airscan.5 +@@ -1,7 +1,7 @@ + .\" generated with Ronn/v0.7.3 + .\" http://github.com/rtomayko/ronn/tree/0.7.3 + . +-.TH "SANE\-AIRSCAN" "5" "May 2020" "" "AirScan (eSCL) and WSD SANE backend" ++.TH "SANE\-AIRSCAN" "5" "July 2020" "" "AirScan (eSCL) and WSD SANE backend" + . + .SH "NAME" + \fBsane\-airscan\fR \- SANE backend for AirScan (eSCL) and WSD scanners and MFP +@@ -82,7 +82,7 @@ To manually configure a device, add the following section to the configuration f + [devices] + "Kyocera eSCL" = http://192\.168\.1\.102:9095/eSCL, eSCL + "Kyocera WSD" = http://192\.168\.1\.102:5358/WSDScanner, WSD +-"Device I don\'t want to see" = disable ++"Device I do not want to see" = disable + . + .fi + . +@@ -146,8 +146,8 @@ Debuggung facilities can be controlled using the \fB[debug]\fR section of the co + enable = false | true + + ; Enable protocol trace and configure output directory +-; for trace files\. To specify path relative to user\'s +-; home directory, start it with tilda character, followed ++; for trace files\. Like in shell, to specify path relative to ++; the home directory, start it with tilda character, followed + ; by slash, i\.e\., "~/airscan/trace"\. The directory will + ; be created automatically\. + trace = path +diff --git a/sane-airscan.5.md b/sane-airscan.5.md +index 4461900..6c6ec10 100644 +--- a/sane-airscan.5.md ++++ b/sane-airscan.5.md +@@ -46,7 +46,7 @@ file: + [devices] + "Kyocera eSCL" = http://192.168.1.102:9095/eSCL, eSCL + "Kyocera WSD" = http://192.168.1.102:5358/WSDScanner, WSD +- "Device I don't want to see" = disable ++ "Device I do not want to see" = disable + + The `[devices]` section contains all manually configured devices, one line per + device, and each line contains a device name on a left side of equation and +@@ -109,8 +109,8 @@ of the configuration file: + enable = false | true + + ; Enable protocol trace and configure output directory +- ; for trace files. To specify path relative to user's +- ; home directory, start it with tilda character, followed ++ ; for trace files. Like in shell, to specify path relative to ++ ; the home directory, start it with tilda character, followed + ; by slash, i.e., "~/airscan/trace". The directory will + ; be created automatically. + trace = path +diff --git a/test-decode.c b/test-decode.c +index 5b2879e..af774a5 100644 +--- a/test-decode.c ++++ b/test-decode.c +@@ -26,7 +26,7 @@ typedef struct { + + /* Print error message and exit + */ +-void ++void __attribute__((noreturn)) + die (const char *format, ...) + { + va_list ap; +@@ -218,6 +218,7 @@ main (int argc, char **argv) + save_write(save, line); + } + ++ g_free(line); + save_close(save); + + return 0; |