pax_global_header00006660000000000000000000000064150041143710014506gustar00rootroot0000000000000052 comment=b8fe4c54283c81042c1223d87c07ef97999d348a sane-airscan-0.99.35/000077500000000000000000000000001500411437100142415ustar00rootroot00000000000000sane-airscan-0.99.35/.github/000077500000000000000000000000001500411437100156015ustar00rootroot00000000000000sane-airscan-0.99.35/.github/workflows/000077500000000000000000000000001500411437100176365ustar00rootroot00000000000000sane-airscan-0.99.35/.github/workflows/on-release-tag.yml000066400000000000000000000013701500411437100231650ustar00rootroot00000000000000name: UpdateReleases on: [create] jobs: update_releases: runs-on: ubuntu-latest steps: - name: Update refs/heads/releases to ${{ github.ref }} run: | if [[ `basename ${{ github.ref }}` == +([0-9]*.*.*) ]] then curl --request PATCH \ --url https://api.github.com/repos/${{ github.repository }}/git/refs/heads/releases \ --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \ --header 'content-type: application/json' \ --data '{ "sha": "${{ github.sha }}", "force": true }' curl -X POST \ -H "Authorization: Token ${{ secrets.OSC_TOKEN }}" \ https://api.opensuse.org/trigger/runservice fi sane-airscan-0.99.35/.gitignore000066400000000000000000000002371500411437100162330ustar00rootroot00000000000000# http://www.gnu.org/software/automake libsane-airscan.so.1 airscan-discover test test-decode test-multipart test-uri tags *.swp core core.* *.orig objs/* sane-airscan-0.99.35/.hgtags000066400000000000000000000005141500411437100155170ustar00rootroot00000000000000f40d6f550f76718647b7e9e6b649e87c7f8e3f1c 0.9.0 68bd6c12a8263d974c1f45fbcfff7f102e856931 0.9.0 68bd6c12a8263d974c1f45fbcfff7f102e856931 latest 68bd6c12a8263d974c1f45fbcfff7f102e856931 latest 0000000000000000000000000000000000000000 latest 68bd6c12a8263d974c1f45fbcfff7f102e856931 0.9.0 0000000000000000000000000000000000000000 0.9.0 sane-airscan-0.99.35/COPYING000066400000000000000000000432541500411437100153040ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 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 The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free 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 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 price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any 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 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this 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 themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering 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 void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot 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 may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes 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 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 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 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 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 free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively 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. Copyright (C) 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 the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 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., 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) 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. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice 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 Lesser General Public License instead of this License. sane-airscan-0.99.35/FUZZER.md000066400000000000000000000010161500411437100156060ustar00rootroot00000000000000The `fuzzer` directory contains several fuzz targets that make use of asan and [libFuzzer](https://llvm.org/docs/LibFuzzer.html) from clang 10 or later to test functions that are expected to process untrusted inputs. To build: ``` CXX=clang++-10 meson build ninja -C build fuzzer-$name ``` where `$name` can be any of the files in the `fuzzer` directory. You can then run the fuzzer as `build/fuzzer-$name`. The basic mode will run indefinitely until a problem is found. Pass `-help=1` to see additional fuzzer options. sane-airscan-0.99.35/LICENSE000066400000000000000000000035071500411437100152530ustar00rootroot00000000000000sane-airscan - SANE backend for AirScan (eSCL) scanners ======================================================= Copyright (C) 2019 by sane-airscan authors All rights reserved. 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 the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 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., 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. The exception is that, if you link a sane-airscan library with other files to produce an executable, this does not by itself cause the resulting executable to be covered by the GNU General Public License. Your use of that executable is in no way restricted on account of linking the sane-airscan library code into it. This exception does not, however, invalidate any other reasons why the executable file might be covered by the GNU General Public License. If you submit changes to sane-airscan to the maintainers to be included in a subsequent release, you agree by submitting the changes that those changes may be distributed with this exception intact. If you write modifications of your own for sane-airscan, it is your choice whether to permit this exception to apply to your modifications. If you do not wish that, delete this exception notice. sane-airscan-0.99.35/Makefile000066400000000000000000000143041500411437100157030ustar00rootroot00000000000000# USER-SETTABLE VARIABLES # # The following variables can be overridden by user (i.e., # make install DESTDIR=/tmp/xxx): # # Name Default Description # ---- ------- ----------- # DESTDIR Destination directory for make install # CC gcc C compiler # CPPFLAGS C preprocessor flags # CFLAGS -O2 -g -W -Wall -Werror C compiler flags # LDFLAGS Linker flags # COMPRESS gzip -n Program to compress man page, or "" # STRIP -s Stripping of debug symbols # PKG_CONFIG pkg-config Program to query dependencies info # INSTALL install Installation program # # Variables for Installation Directories # Name Linux BSD # ---- ----- --- # prefix /usr /usr/local # exec_prefix $(prefix) $(prefix) # sysconfdir /etc $(prefix)/etc # bindir $(exec_prefix)/bin $(exec_prefix)/bin # libdir $(shell $(PKG_CONFIG) --variable=libdir sane-backends) # datarootdir $(prefix)/share $(prefix)/share # mandir $(datarootdir)/man $(datarootdir)/man # CC = gcc COMPRESS = gzip -n CFLAGS += -O2 -g -W -Wall -Werror -pthread $(CPPFLAGS) PKG_CONFIG = pkg-config STRIP = -s INSTALL = install ifeq "$(shell uname -s)" "Linux" prefix ?= /usr else prefix ?= /usr/local endif ifeq "$(prefix)" "/usr" sysconfdir = /etc else sysconfdir = $(prefix)/etc endif exec_prefix = $(prefix) bindir = $(exec_prefix)/bin libdir = $(shell $(PKG_CONFIG) --variable=libdir sane-backends) datarootdir = $(prefix)/share mandir = $(datarootdir)/man # These variables are not intended to be user-settable OBJDIR = objs/ CONFDIR = $(sysconfdir)/sane.d BACKEND = libsane-airscan.so.1 DISCOVER = airscan-discover LIBAIRSCAN = $(OBJDIR)/libairscan.a MAN_DISCOVER = $(DISCOVER).1 MAN_DISCOVER_TITLE = "SANE Scanner Access Now Easy" MAN_BACKEND = sane-airscan.5 MAN_BACKEND_TITLE = "AirScan (eSCL) and WSD SANE backend" DEPS_COMMON := avahi-client libxml-2.0 gnutls DEPS_CODECS := libjpeg libpng libtiff-4 CFLAGS += -D CONFIG_SANE_CONFIG_DIR=\"$(CONFDIR)\" # Sources and object files SRC = $(wildcard airscan-*.c) sane_strstatus.c http_parser.c OBJ = $(addprefix $(OBJDIR), $(SRC:.c=.o)) # Obtain CFLAGS and LDFLAGS for dependencies deps_CFLAGS := $(foreach lib, $(DEPS_COMMON), $(shell $(PKG_CONFIG) --cflags $(lib))) deps_CFLAGS += $(foreach lib, $(DEPS_CODECS), $(shell $(PKG_CONFIG) --cflags $(lib))) deps_LIBS := $(foreach lib, $(DEPS_COMMON), $(shell $(PKG_CONFIG) --libs $(lib))) -lm -lpthread deps_LIBS_CODECS := $(foreach lib, $(DEPS_CODECS), $(shell $(PKG_CONFIG) --libs $(lib))) # Compute CFLAGS and LDFLAGS for backend and tools # # Note, CFLAGS are common, for simplicity, while LDFLAGS are not, to # avoid linking unneeded libraries common_CFLAGS := $(CFLAGS) $(deps_CFLAGS) common_CFLAGS += -fPIC backend_LDFLAGS := $(LDFLAGS) backend_LDFLAGS += $(deps_LIBS) $(deps_LIBS_CODECS) backend_LDFLAGS += -Wl,--version-script=airscan.sym -Wl,--no-undefined -lc tools_LDFLAGS := $(LDFLAGS) tools_LDFLAGS += $(deps_LIBS) tools_LDFLAGS += -fPIE tests_LDFLAGS := $(tools_LDFLAGS) $(deps_LIBS_CODECS) $(OBJDIR)%.o: %.c Makefile airscan.h mkdir -p $(OBJDIR) $(CC) -c -o $@ $< $(CPPFLAGS) $(common_CFLAGS) .PHONY: all clean install man all: tags $(BACKEND) $(DISCOVER) test test-decode test-devcaps test-multipart test-zeroconf test-uri tags: $(SRC) airscan.h test.c test-decode.c test-devcaps.c test-multipart.c test-zeroconf.c test-uri.c -ctags -R . $(BACKEND): $(OBJDIR)airscan.o $(LIBAIRSCAN) airscan.sym $(CC) -o $(BACKEND) -shared $(OBJDIR)/airscan.o $(LIBAIRSCAN) $(backend_LDFLAGS) $(DISCOVER): $(OBJDIR)discover.o $(LIBAIRSCAN) $(CC) -o $(DISCOVER) discover.c $(CPPFLAGS) $(common_CFLAGS) $(LIBAIRSCAN) $(tools_LDFLAGS) $(LIBAIRSCAN): $(OBJ) Makefile ar cru $(LIBAIRSCAN) $(OBJ) install: all mkdir -p $(DESTDIR)/$(bindir) mkdir -p $(DESTDIR)/$(CONFDIR) mkdir -p $(DESTDIR)/$(CONFDIR)/dll.d $(INSTALL) $(STRIP) $(DISCOVER) $(DESTDIR)/$(bindir) [ -e $(DESTDIR)/$(CONFDIR)/airscan.conf ] || cp airscan.conf $(DESTDIR)/$(CONFDIR) [ -e $(DESTDIR)/$(CONFDIR)/dll.d/airscan ] || cp dll.conf $(DESTDIR)/$(CONFDIR)/dll.d/airscan mkdir -p $(DESTDIR)/$(libdir)/sane $(INSTALL) $(STRIP) $(BACKEND) $(DESTDIR)/$(libdir)/sane mkdir -p $(DESTDIR)/$(mandir)/man1 mkdir -p $(DESTDIR)/$(mandir)/man5 $(INSTALL) -m 644 $(MAN_DISCOVER) $(DESTDIR)/$(mandir)/man1 $(INSTALL) -m 644 $(MAN_BACKEND) $(DESTDIR)/$(mandir)/man5 [ "$(COMPRESS)" = "" ] || $(COMPRESS) -f $(DESTDIR)/$(mandir)/man1/$(MAN_DISCOVER) [ "$(COMPRESS)" = "" ] || $(COMPRESS) -f $(DESTDIR)/$(mandir)/man5/$(MAN_BACKEND) clean: rm -f test test-decode test-devcaps test-multipart test-zeroconf test-uri $(BACKEND) tags rm -rf $(OBJDIR) uninstall: rm -f $(DESTDIR)/$(bindir)/$(DISCOVER) rm -f $(DESTDIR)/$(CONFDIR)/dll.d/airscan rm -f $(DESTDIR)/$(libdir)/sane/$(BACKEND) rm -f $(DESTDIR)/$(mandir)/man1/$(MAN_DISCOVER)* rm -f $(DESTDIR)/$(mandir)/man5/$(MAN_BACKEND)* check: all ./test-uri ./test-zeroconf man: $(MAN_DISCOVER) $(MAN_BACKEND) $(MAN_DISCOVER): $(MAN_DISCOVER).md ronn --roff --manual=$(MAN_DISCOVER_TITLE) $(MAN_DISCOVER).md $(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 . $(LDFLAGS) ${common_CFLAGS} test-decode: test-decode.c $(LIBAIRSCAN) $(CC) -o test-decode test-decode.c $(CPPFLAGS) $(common_CFLAGS) $(LIBAIRSCAN) $(tests_LDFLAGS) test-devcaps: test-devcaps.c $(LIBAIRSCAN) $(CC) -o test-devcaps test-devcaps.c $(CPPFLAGS) $(common_CFLAGS) $(LIBAIRSCAN) $(tests_LDFLAGS) test-multipart: test-multipart.c $(LIBAIRSCAN) $(CC) -o test-multipart test-multipart.c $(CPPFLAGS) $(common_CFLAGS) $(LIBAIRSCAN) $(tests_LDFLAGS) test-zeroconf: test-zeroconf.c $(LIBAIRSCAN) $(CC) -o test-zeroconf test-zeroconf.c $(CPPFLAGS) $(common_CFLAGS) $(LIBAIRSCAN) $(tests_LDFLAGS) test-uri: test-uri.c $(LIBAIRSCAN) $(CC) -o test-uri test-uri.c $(CPPFLAGS) $(common_CFLAGS) $(LIBAIRSCAN) $(tests_LDFLAGS) sane-airscan-0.99.35/README.md000066400000000000000000000711721500411437100155300ustar00rootroot00000000000000# sane-airscan -- SANE backend for AirScan (eSCL) and WSD document scanners Similar to how most modern network printers support "driverless" printing, using the universal vendor-neutral printing protocol, many modern network scanners and MFPs support "driverless" scanning. Driverless scanning comes in two flavors: * Apple **AirScan** or **AirPrint scanning** (official protocol name is eSCL) * Microsoft **WSD**, or **WS-Scan** (term WSD means "Web Services for Devices) This backend implements both protocols, choosing automatically between them. It was successfully tested with many devices from **Brother**, **Canon**, **Dell**, **Kyocera**, **Lexmark**, **Epson**, **HP**, **OKI**, **Panasonic**, **Pantum**, **Ricoh**, **Samsung** and **Xerox** both in WSD and eSCL modes. For eSCL devices, Apple maintains [a comprehensive list](https://support.apple.com/en-us/HT201311) of compatible devices, but please note, this list contains not only scanners and MFP, but pure printers as well. This backend doesn't require to install and doesn't conflict with vendor-provided proprietary software like ScanGear from Canon, HPLIP from HP and so on. ### Features 1. One backend for two different protocols, eSCL and WSD 2. Automatic and manual device discovery and configuration 3. Scan from platen and ADF, in duplex and simplex modes, multi-page scan from ADF supported as well 4. Scan in color and gray scale modes 5. Line-by-line image unpacking, for low memory footprint 6. The cancel operation is as fast as possible, depending on your hardware 7. Both IPv4 and IPv6 are supported ### Compatibility Any **eSCL** and **WSD** capable scanner expected to work. Here is a list of devices that were actually tested. If you have success with a scanner not included into this list, please let me know. In most cases, devices were tested with network connection. However, most (all?) of the **eSCL** devices will also work over **USB**, if **IPP-over-USB** daemon is installed on your computer. WSD-only devices cannot be used with the IPP-over-USB daemon. The **IPP-over-USB** comes with the `ipp-usb` package and often installed by default. If your distro comes without `ipp-usb`, please visit the project page to figure out alternative ways to obtain it: [ipp-usb](https://github.com/OpenPrinting/ipp-usb) Legend: * **Yes** - device works perfectly * **No** - protocol not supported by device * **?** - device works with `sane-airscan`, but protocol is not reported by user * Space - author has no information on this mode/device combination | Device | eSCL mode | WSD mode | | ---------------------------------- | :-----------------------: | :-----------------------: | | Brother ADS-2700W | No | Yes | | Brother ADS-4300N | Yes | Yes | | Brother DCP-7055W | No | Yes | | Brother DCP-7070DW | No | Yes | | Brother DCP-9020CDW | No | Yes | | Brother DCP-J552DW | No | Yes | | Brother DCP-L2540DW | No | Yes | | Brother DCP-L2550DN / DCP-L2550DW | Yes | | | Brother HL-L2380DW series | No | Yes | | Brother HL-L2395DW series | Yes | | | Brother MFC-7360N | No | Yes | | Brother MFC-8710DW | No | Yes | | Brother MFC-J1012DW | Yes | | | Brother MFC-J1300DW | Yes | | | Brother MFC-J4410DW | No | Yes | | Brother MFC-J4540DW | Yes | | | Brother MFC-J4620DW | No | Yes | | Brother MFC-J470DW | No | Yes | | Brother MFC-J485DW | Yes | | | Brother MFC-J625DW | No | Yes | | Brother MFC-L2700DW | No | Yes | | Brother MFC-L2710DN series | No | Yes | | Brother MFC-L2710DW | Yes | Yes | | Brother MFC-L2720DW | No | Yes | | Brother MFC-L2750DW | Yes | Yes | | Brother MFC-L3740CDWE | Yes | | | Brother MFC-L3750CDW | No | Yes | | Brother MFC-L3780CDW | No | Yes | | Brother MFC-T910DW | Yes | Yes | | Canon D570 | Yes | | | Canon G600 series | Yes | | | Canon imageCLASS MF642C/643C/644C | Yes | | | Canon imageCLASS MF743cdw | Yes[1](#note1) | | | Canon imageRUNNER 2625/2630 | Yes | Yes | | Canon imageRUNNER ADVANCE 4545/4551| Yes | Yes | | Canon imageRUNNER ADV C5550/5560 | Yes | | | Canon imageRUNNER C3120L | Yes | Yes | | Canon i-SENSYS MF4780w | No | Yes[4](#note4) | | Canon i-SENSYS MF641C | No | Yes[2](#note2) | | Canon LiDE 300 | Yes[3](#note3) | | | Canon LiDE 400 | Yes[3](#note3) | | | Canon MB5100 series | Yes | | | Canon MB5400 series | Yes | Yes | | Canon MF110/910 | Yes | | | Canon MF240 Series | No | Yes[4](#note4) | | Canon MF260 Series | Yes | Yes[4](#note4) | | Canon MF410 Series | Yes | Yes | | Canon MF440 Series | Yes | Yes | | Canon MF645Cx | Yes | | | Canon MF650C Series | Yes | | | Canon MF745C/746C | Yes | Yes | | Canon MG5200 series | No | Yes | | Canon MG5300 series | No | Yes | | Canon MX470 series | No | Yes | | Canon PIXMA G3000 series | No | Yes | | Canon PIXMA G4010 series | Yes[3](#note3) | | | Canon PIXMA MG3600 series | 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 | Yes | | Canon TR4700 series | Yes | | | Canon TR7500 Series | No | Yes | | Canon TR8600 Scanner | Yes | | | Canon TS 3100 | Yes | | | Canon TS 3300 | Yes | | | Canon TS 3400 series | Yes | | | Canon TS 6151 | Yes | | | Canon TS 6200 series | Yes | Yes | | Canon TS 6300 series | Yes | | | Canon TS 6400 series | Yes | | | Canon TS 8230 series | No | Yes | | Dell C1765nfw Color MFP | No | Yes | | Dell C2665dnf Color Laser Printer | No | Yes | | Dell C3765dnf Color MFP | No | Yes | | Dell E514dw | No | Yes | | EPSON ET-2650 Series | Yes | Yes | EPSON ET-2710 Series | No | Yes | | EPSON ET-2750 Series | Yes | | | EPSON ET-2760 Series | Yes | | | EPSON ET-2810 Series | No | Yes | | EPSON ET-2850 Series | Yes | | | EPSON ET-3750 Series | Yes | | | EPSON ET-4750 Series | No | Yes | | EPSON ET-4850 Series | Yes | | | EPSON ET-M2170 Series | Yes | | | EPSON L6570 Series | Yes | Yes | | EPSON Stylus SX535WD | No | Yes | | EPSON WF-2760 Series | | Yes | | EPSON WF-3620 Series | No | Yes | | EPSON WF-7710 Series | No | Yes | | EPSON XP-2100 Series | No | Yes | | EPSON XP-340 Series | Yes | | | EPSON XP-352 355 Series | No | Yes | | EPSON XP-442 445 Series | Yes | | | EPSON XP-5100 Series | Yes | | | EPSON XP-6100 Series | Yes | | | EPSON XP-7100 Series | Yes | | | EPSON XP-8600 Series | Yes | | | HP Color Laserjet MFP 178 178 | Yes | | | HP Color LaserJet MFP M182nw | Yes | | | HP Color LaserJet MFP M281fdw | Yes | | | HP Color LaserJet MFP M283fdw | Yes | | | HP Color LaserJet MFP M477fdw | Yes | Yes | | HP Color LaserJet Pro M478f-9f | Yes | | | HP Color LaserJet Pro MFP M277dw | Yes | | | HP DeskJet 2540 | Yes | | | HP DeskJet 2600 series | Yes | | | HP DeskJet 2700 series | Yes | | | HP Deskjet 3520 series | Yes | | | HP DeskJet 3700 series | Yes | | | HP DeskJet 4100 series | Yes | | | HP DeskJet 5000 series | Yes | | | HP DeskJet 5200 series | Yes | | | HP ENVY 4500 | Yes | | | HP ENVY 5055 series | Yes | | | HP ENVY 5530 series | Yes | | | HP ENVY 5540 | Yes | | | HP ENVY 5640 | Yes | | | HP ENVY 6000 series | Yes | | | HP ENVY Photo 6200 series | Yes | | | HP ENVY Photo 7800 series | Yes | | | HP ENVY Pro 6400 series | Yes | | | HP Color Laser MFP M178nw | Yes | | | HP LaserJet 200 colorMFP M276n | No | Yes | | HP LaserJet MFP E62655 | Yes | | | HP LaserJet MFP M130fn | Yes | Yes | | HP LaserJet MFP M130fw | Yes | Yes | | HP LaserJet MFP M140w | Yes | | | HP LaserJet MFP M227sdn | Yes | | | HP LaserJet MFP M426dw | Yes | | | HP LaserJet MFP M630 | Yes | | | HP LaserJet Pro M28a | Yes[3](#note3) | | | HP LaserJet Pro M28w | Yes | Yes | | HP LaserJet Pro M329 | Yes[9](#note3) | | | HP LaserJet Pro MFP 148fdw | Yes | | | HP LaserJet Pro MFP M125 series | No | Yes | | HP LaserJet Pro MFP M127fn | No | Yes | | HP LaserJet Pro MFP M225dn | No | Yes | | HP LaserJet Pro MFP M428dw | Yes[9](#note3) | | | HP LaserJet Pro MFP M521 series | No | Yes | | HP Laser MFP 131 133 135-138 | Yes | | | HP Neverstop Laser MFP 1202nw | Yes | | | HP OfficeJet 3830 series | Yes | | | HP Officejet 4630 | Yes | | | HP Officejet 5740 series | Yes | | | HP Officejet Pro 6970 | Yes | | | HP OfficeJet Pro 6978 | Yes | | | HP OfficeJet Pro 7740 | Yes | No | | HP OfficeJet Pro 8010 series | Yes | | | HP OfficeJet Pro 8020 Series | Yes | | | HP OfficeJet Pro 8730 | Yes | Yes | | HP OfficeJet Pro 9010 series | Yes | | | HP ScanJet Pro 2000 s2 | Yes[3](#note3) | | | HP ScanJet Pro 3500 fn1 | Yes[3](#note3) | | | HP ScanJet Pro 4500 fn1 | Yes | | | HP Smart Tank 5100 series | Yes | | | HP Smart Tank Plus 550 series | Yes | | | Kyocera ECOSYS M2035dn | No | Yes[5](#note5) | | Kyocera ECOSYS M2040dn | Yes | Yes[5](#note5) | | Kyocera ECOSYS M2640idw | Yes | Yes[5](#note5) | | Kyocera ECOSYS M5521cdw | Yes | Yes[5](#note5) | | Kyocera ECOSYS M5526cdw | Yes | | | Kyocera FS-1028MFP | No | Yes[5](#note5) | | Kyocera TASKalfa 3051ci | | Yes[5](#note5) | | Lexmark CX317dn | Yes[6](#note6) | Yes[6](#note6) | | Lexmark MB2236adw | Yes | | | Lexmark MC2535adwe | Yes | | | Lexmark MC3224adwe | Yes | | | Lexmark MC3326adwe | Yes | | | OKI-MB471 | No | Yes | | OKI-MC332dn | No | Yes | | OKI-MC362dn | No | Yes | | OKI-MC853 | Yes | | | Panasonic KV-S1058Y | No | Yes | | Pantum BM5100ADW series | Yes | Yes | | Pantum M6500W series | Yes | | | Ricoh MP C3003 | No | Yes[7](#note7) | | Samsung M2070 Series | No | Yes | | Samsung M267x 287x Series | No | Yes | | Samsung M288x Series | No | Yes | | Samsung M337x 387x 407x Series | No | Yes[8](#note8) | | Samsung SCX-3400 Series | No | Yes | | SHARP MX-3060N | Yes[9](#note9) | | | Xerox B205 | Yes | Yes | | Xerox B215 | Yes | Yes[10](#note10)| | Xerox C235 | Yes | | | Xerox VersaLink B405 | Yes | | | Xerox WorkCentre 3025 | No | Yes | | Xerox WorkCentre 6027 | No | Yes[10](#note10)| | Xerox WorkCentre 6515 | | Yes | | TODO | | | --- [1]: this device requires manual activation of AirPrint scanning on its web console: Home->Menu->Preferences->Network->TCP/IP Settings->Network Link Scan Settings->On. [2]: WS-Scan needs to be manually enabled on this device: Home->Menu->Preferences->Network->TCP/IP Settings->WSD Settings->Use WSD Scanning->ON [3]: this device is USB-only, but it works well with the IPP-over-USB daemon. [4]: by default, WS-Scan is disabled on this device and needs to be enabled before use: open web console, Click `[Settings/Registration]`, Click `[Network Settings]`->`[TCP/IP Settings]`, Click `[Edit]` in `[WSD Settings]`, enable `[Use WSD Scanning]` checkbox, Click `[OK]` [5]: this device requires manual action on its front panel to initiate WSD scan: Send->WSD Scan->From Computer [6]: 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. [7]: by default, WS-Scan is disabled on this device and needs to be enabled before use: open web console, 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) [8]: with old firmware (tested with V4.00.01.04 APR-09-2013) ADF scan causes device reboot. Firmware update helps, version V4.00.02.20 MAY-27-2020 known to work. [8]: eSCL needs to be manually enabled on this device: System-settings->Network settings->Airprint settings->Airscan [9]: Scanning remotely and with IPP-over-USB active is disabled by default. In the Embedded Web Server: Settings->Security->Administrator Settings->Enable Scan from a Computer or Mobile Device->Apply [10]: WSD scan is disabled by default. In the Embedded Web Server: Properties->Connectivity->Protocols->WSD->Enable ### Distros that come with sane-airscan The following distros (in alphabetical order) include `sane-airscan` officially: * ALT Linux (Sisyphus and p9) * Arch Linux (in extra repository) * Debian 10+ * Fedora 32+ * Gentoo (emerge media-gfx/sane-airscan) * NixOS * Ubuntu 20.10+ This list is constantly growing and may be very incomplete. Also, `sane-airscan` works on BSD and included into FreeBSD, NetBSD and OpenBSD ports. ### Installation from pre-build binaries If you use one of the following Linux distros: * **Debian** (9.0 and 10) * **Fedora** (29, 30, 31 and 32) * **openSUSE** (Leap and Tumbleweed) * **Ubuntu** (16.04, 18.04, 19.04, 19.10 and 20.04) [Follow this link](https://software.opensuse.org//download.html?project=home%3Apzz&package=sane-airscan), where you will find packages and very detailed installation instructions. Note, after a fresh build this link sometimes takes too long to update, so if you encounter "Resource is no longer available!" problems, there is a direct link to repositories: [https://download.opensuse.org/repositories/home:/pzz/](https://download.opensuse.org/repositories/home:/pzz/) I strongly recommend you to choose "Add repository and install manually" option rather that "Grab binary packages directly", because it will enable automatic updates of the sane-airscan package. **Linux Mint** users may use Ubuntu packages: * Linux Mint 18.x - use packages for Ubuntu 16.04 * Linux Mint 19.x - use packages for Ubuntu 18.04 * Linux Mint 20.x - use packages for Ubuntu 20.04 Big thanks to [openSUSE Build Service](https://build.opensuse.org/) for providing package build infrastructure. If your distro is not listed, see [Installation from sources](https://github.com/alexpevzner/sane-airscan#installation-from-sources) section below. ### Installation from sources #### Install required libraries - Fedora and similar As root, execute the following commands: ``` dnf install gcc git make pkgconf-pkg-config dnf install avahi-devel dnf install libxml2-devel dnf install libjpeg-turbo-devel libpng-devel libtiff-devel dnf install gnutls-devel dnf install sane-backends-devel ``` #### Install required libraries - Ubuntu, Debian and similar As root, execute the following commands: ``` apt-get install gcc git make pkg-config apt-get install libavahi-client-dev apt-get install libxml2-dev apt-get install libjpeg-dev libpng-dev libtiff5-dev apt-get install libsane-dev apt-get install gnutls-dev ``` #### Download, build and install sane-airscan ``` git clone https://github.com/alexpevzner/sane-airscan.git cd sane-airscan make make install ``` ### Contribution All contributions are welcome and greatly appreciated, assuming the following: 1. Feature that you propose has a general interest for many people 2. Your code is well-formatted and has a good quality Please note, this project has two branches: * stable branch: https://github.com/alexpevzner/sane-airscan * development branch: https://github.com/alexpevzner/sane-airscan-unstable Stable branch accepts mostly bug fixes and minor features with small change in code base. Major features should be contributed into the development branch. ### Paid consulting If your business depends on my project, and you require any specific feature not currently implemented here, you may consider contracting me on a paid basis. ### PVS-Studio [PVS-Studio](https://www.viva64.com/en/pvs-studio/) is a static code analyser, supporting C, C++, C# and Java. Once upon a time I was chatting with its authors in Russian software development forum and told them, that if their tool will find something interesting in my code, I will put a reference to their project here. Their tool actually found a couple real bugs, so I had to fulfill my promise :-) Now I regularly test this code with PVS-Studio, and it really helps. Their product is not free, but they offer free licenses for open source projects. ### Reporting bugs To report a bug, please [create a new GitHub issue](https://github.com/alexpevzner/sane-airscan/issues/new) To create a helpful bug report, please perform the following steps: 1. Enable protocol trace in the sane-airscan, by adding the following entries into the configuration file
(**/etc/sane.d/airscan.conf**): ``` [debug] trace = ~/airscan/trace ; Path to directory where trace files will be saved ``` You may use an arbitrary directory path, assuming you have enough rights to create and write this directory. The directory will be created automatically. 2. Reproduce the problem. Please, don't use any confidential documents when problem is being reproduces, as their content will be visible to others. 3. Explain the problem carefully 4. In the directory you've specified as the trace parameter, you will find two files. Assuming you are using program xsane and your device name is "Kyocera MFP Scanner", file names will be **"xsane-Kyocera-MFP-Scanner.log"** and **"xsane-Kyocera-MFP-Scanner.tar"**. Please, attach both of these files to the new issue. ## References The eSCL protocol is not documented, but this is simple protocol, based on HTTP and XML, easy for reverse engineering. There are many Internet resources around, related to this protocol, and among others I want to note the following links: * [kno10/python-scan-eSCL](https://github.com/kno10/python-scan-eSCL) - a tiny Python script, able to scan from eSCL-compatible scanners * [SimulPiscator/AirSane](https://github.com/SimulPiscator/AirSane) - this project solves the reverse problem, converting any SANE-compatible scanner into eSCL server. Author claims that it is compatible with Mopria and Apple clients * [markosjal/AirScan-eSCL.txt](https://gist.github.com/markosjal/79d03cc4f1fd287016906e7ff6f07136) - document, describing eSCL protocol, based on reverse engineering. Not complete and not always accurate, but gives the good introduction sane-airscan-0.99.35/_config.yml000066400000000000000000000000351500411437100163660ustar00rootroot00000000000000theme: jekyll-theme-architectsane-airscan-0.99.35/airscan-array.c000066400000000000000000000036221500411437100171440ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * SANE_Word/SANE_String/SANE_Device* arrays */ #include "airscan.h" #include #include /* Compare function for sane_word_array_sort */ static int sane_word_array_sort_cmp(const void *p1, const void *p2) { return *(SANE_Word*) p1 - *(SANE_Word*) p2; } /* Drop array elements that outside of specified boundary */ void sane_word_array_bound (SANE_Word *a, SANE_Word min, SANE_Word max) { SANE_Word len = a[0]; SANE_Word i, o; for (i = o = 1; i < len + 1; i ++) { if (min <= a[i] && a[i] <= max) { a[o ++] = a[i]; } } a[0] = o - 1; mem_shrink(a, o); } /* Sort array of SANE_Word in increasing order */ void sane_word_array_sort(SANE_Word *a) { SANE_Word len = a[0]; if (len) { qsort(a + 1, len, sizeof(SANE_Word), sane_word_array_sort_cmp); } } /* Intersect two sorted arrays. */ SANE_Word* sane_word_array_intersect_sorted (const SANE_Word *a1, const SANE_Word *a2) { const SANE_Word *end1 = a1 + sane_word_array_len(a1) + 1; const SANE_Word *end2 = a2 + sane_word_array_len(a2) + 1; SANE_Word *out = sane_word_array_new(); a1 ++; a2 ++; while (a1 < end1 && a2 < end2) { if (*a1 < *a2) { a1 ++; } else if (*a1 > *a2) { a2 ++; } else { out = sane_word_array_append(out, *a1); a1 ++; a2 ++; } } return out; } /* Compute max string length in array of strings */ size_t sane_string_array_max_strlen(const SANE_String *a) { size_t max_len = 0; for (; *a != NULL; a ++) { size_t len = strlen(*a); if (len > max_len) { max_len = len; } } return max_len; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-bmp.c000066400000000000000000000221471500411437100166070ustar00rootroot00000000000000/* sane - Scanner Access Now Easy. * * Copyright (C) 2020 Thierry HUCHARD * See LICENSE for license terms and conditions */ #include "airscan.h" #include #include #include #include #if defined(OS_HAVE_ENDIAN_H) # include #elif defined(OS_HAVE_SYS_ENDIAN_H) # include #endif /* BITMAPFILEHEADER structure, see MSDN for details */ #pragma pack (push,1) typedef struct { uint16_t bfType; /* File magic, always 'BM' */ uint32_t bfSize; /* File size in bytes */ uint16_t bfReserved1; /* Reserved; must be zero */ uint16_t bfReserved2; /* Reserved; must be zero */ uint32_t bfOffBits; /* Offset to bitmap bits */ } BITMAPFILEHEADER; #pragma pack (pop) /* BITMAPINFOHEADER structure, see MSDN for details */ #pragma pack (push,1) typedef struct { uint32_t biSize; /* Header size, bytes */ int32_t biWidth; /* Image width, pixels */ int32_t biHeight; /* Image height, pixels */ uint16_t biPlanes; /* Number of planes, always 1 */ uint16_t biBitCount; /* Bits per pixel */ uint32_t biCompression; /* Compression type, see MSDN */ uint32_t biSizeImage; /* Image size, can be o */ int32_t biXPelsPerMeter; /* Horizontal resolution, pixels per meter */ int32_t biYPelsPerMeter; /* Vertical resolution, pixels per meter */ uint32_t biClrUsed; /* Number of used palette indices */ uint32_t biClrImportant; /* Number of important palette indices */ } BITMAPINFOHEADER; #pragma pack (pop) /* BMP image decoder */ typedef struct { image_decoder decoder; /* Base class */ char error[256]; /* Error message buffer */ const uint8_t *image_data; /* Image data */ BITMAPINFOHEADER info_header; /* DIB header, decoded */ size_t bmp_row_size; /* Row size in BMP file */ SANE_Frame format; /* SANE_FRAME_GRAY/RBG */ unsigned int next_line; /* Next line to read */ } image_decoder_bmp; /* Free BMP decoder */ static void image_decoder_bmp_free (image_decoder *decoder) { image_decoder_bmp *bmp = (image_decoder_bmp*) decoder; mem_free(bmp); } /* Begin BMP decoding */ static error image_decoder_bmp_begin (image_decoder *decoder, const void *data, size_t size) { image_decoder_bmp *bmp = (image_decoder_bmp*) decoder; BITMAPFILEHEADER file_header; size_t header_size, padding; uint64_t size_required; /* Decode BMP header */ if (size < sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER)) { return ERROR("BMP: header truncated"); } memcpy(&file_header, data, sizeof(BITMAPFILEHEADER)); memcpy(&bmp->info_header, ((char*) data) + sizeof(BITMAPFILEHEADER), sizeof(BITMAPINFOHEADER)); file_header.bfType = le16toh(file_header.bfType); file_header.bfSize = le32toh(file_header.bfSize); file_header.bfOffBits = le32toh(file_header.bfOffBits); bmp->info_header.biSize = le32toh(bmp->info_header.biSize); bmp->info_header.biWidth = le32toh(bmp->info_header.biWidth); bmp->info_header.biHeight = le32toh(bmp->info_header.biHeight); bmp->info_header.biPlanes = le16toh(bmp->info_header.biPlanes); bmp->info_header.biBitCount = le16toh(bmp->info_header.biBitCount); bmp->info_header.biCompression = le32toh(bmp->info_header.biCompression); bmp->info_header.biSizeImage = le32toh(bmp->info_header.biSizeImage); bmp->info_header.biXPelsPerMeter = le32toh(bmp->info_header.biXPelsPerMeter); bmp->info_header.biYPelsPerMeter = le32toh(bmp->info_header.biYPelsPerMeter); bmp->info_header.biClrUsed = le32toh(bmp->info_header.biClrUsed); bmp->info_header.biClrImportant = le32toh(bmp->info_header.biClrImportant); /* Validate BMP header */ if (file_header.bfType != ('M' << 8 | 'B')) { return ERROR("BMP: invalid header signature"); } if (bmp->info_header.biSize < sizeof(BITMAPINFOHEADER)) { sprintf(bmp->error, "BMP: invalid header size %d", bmp->info_header.biSize); return ERROR(bmp->error); } if (bmp->info_header.biCompression != 0) { sprintf(bmp->error, "BMP: compression %d not supported", bmp->info_header.biCompression); return ERROR(bmp->error); } /* Ignore palette for 8-bit (grayscale) images, reject it otherwise */ if (bmp->info_header.biClrUsed != 0 && bmp->info_header.biBitCount != 8) { return ERROR("BMP: paletted images not supported"); } switch (bmp->info_header.biBitCount) { case 8: bmp->format = SANE_FRAME_GRAY; break; case 24: case 32: bmp->format = SANE_FRAME_RGB; break; default: sprintf(bmp->error, "BMP: %d bits per pixel not supported", bmp->info_header.biBitCount); return ERROR(bmp->error); } /* Compute BMP row size */ bmp->bmp_row_size = bmp->info_header.biWidth; bmp->bmp_row_size *= bmp->info_header.biBitCount / 8; padding = (4 - (bmp->bmp_row_size & 3)) & 3; bmp->bmp_row_size += padding; /* Make sure image is not truncated */ header_size = sizeof(BITMAPFILEHEADER) + bmp->info_header.biSize; header_size += (size_t) bmp->info_header.biClrUsed * 4; size_required = header_size; size_required += ((uint64_t) labs(bmp->info_header.biHeight)) * (uint64_t) bmp->bmp_row_size; size_required -= padding; /* Last row may be unpadded */ if (size_required > (uint64_t) size) { return ERROR("BMP: image truncated"); } /* Save pointer to image data */ bmp->image_data = header_size + (const uint8_t*) data; return NULL; } /* Reset BMP decoder */ static void image_decoder_bmp_reset (image_decoder *decoder) { image_decoder_bmp *bmp = (image_decoder_bmp*) decoder; size_t off = sizeof(bmp->decoder); memset(((char*) bmp) + off, 0, sizeof(*bmp) - off); } /* Get bytes count per pixel */ static int image_decoder_bmp_get_bytes_per_pixel (image_decoder *decoder) { image_decoder_bmp *bmp = (image_decoder_bmp*) decoder; return bmp->format == SANE_FRAME_GRAY ? 1 : 3; } /* Get image parameters */ static void image_decoder_bmp_get_params (image_decoder *decoder, SANE_Parameters *params) { image_decoder_bmp *bmp = (image_decoder_bmp*) decoder; params->last_frame = SANE_TRUE; params->pixels_per_line = bmp->info_header.biWidth; params->lines = labs(bmp->info_header.biHeight); params->depth = 8; params->format = bmp->format; params->bytes_per_line = params->pixels_per_line; if (params->format == SANE_FRAME_RGB) { params->bytes_per_line *= 3; } } /* Set clipping window */ static error image_decoder_bmp_set_window (image_decoder *decoder, image_window *win) { image_decoder_bmp *bmp = (image_decoder_bmp*) decoder; win->x_off = win->y_off = 0; win->wid = bmp->info_header.biWidth; win->hei = labs(bmp->info_header.biHeight); return NULL; } /* Read next line of image */ static error image_decoder_bmp_read_line (image_decoder *decoder, void *buffer) { image_decoder_bmp *bmp = (image_decoder_bmp*) decoder; size_t row_num; const uint8_t *row_data; int i, wid = bmp->info_header.biWidth; uint8_t *out = buffer; if (bmp->next_line == (unsigned int) labs(bmp->info_header.biHeight)) { return ERROR("BMP: end of file"); } /* Compute row number */ row_num = bmp->next_line ++; if (bmp->info_header.biHeight > 0) { row_num = bmp->info_header.biHeight - row_num - 1; } /* Compute row address */ row_data = bmp->image_data + row_num * bmp->bmp_row_size; /* Decode the row */ switch (bmp->info_header.biBitCount) { case 8: memcpy(out, row_data, wid); break; case 24: for (i = 0; i < wid; i ++) { out[0] = row_data[2]; /* Red */ out[1] = row_data[1]; /* Green */ out[2] = row_data[0]; /* Blue */ out += 3; row_data += 3; } break; case 32: for (i = 0; i < wid; i ++) { out[0] = row_data[2]; /* Red */ out[1] = row_data[1]; /* Green */ out[2] = row_data[0]; /* Blue */ out += 3; row_data += 4; } break; default: log_internal_error(NULL); } return NULL; } /* Create BMP image decoder */ image_decoder* image_decoder_bmp_new (void) { image_decoder_bmp *bmp = mem_new(image_decoder_bmp, 1); bmp->decoder.content_type = "image/bmp"; bmp->decoder.free = image_decoder_bmp_free; bmp->decoder.begin = image_decoder_bmp_begin; bmp->decoder.reset = image_decoder_bmp_reset; bmp->decoder.get_bytes_per_pixel = image_decoder_bmp_get_bytes_per_pixel; bmp->decoder.get_params = image_decoder_bmp_get_params; bmp->decoder.set_window = image_decoder_bmp_set_window; bmp->decoder.read_line = image_decoder_bmp_read_line; return &bmp->decoder; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-conf.c000066400000000000000000000345571500411437100167660ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Configuration file loader */ #include "airscan.h" #include #include #include #include /* Configuration data */ conf_data conf = CONF_INIT; static conf_data conf_init = CONF_INIT; /* Revert conf.devices list */ static void conf_device_list_revert (void) { conf_device *list = conf.devices, *prev = NULL, *next; while (list != NULL) { next = list->next; list->next = prev; prev = list; list = next; } conf.devices = prev; } /* Free conf.devices list */ static void conf_device_list_free (void) { conf_device *list = conf.devices, *next; conf.devices = NULL; while (list != NULL) { next = list->next; mem_free((char*) list->name); http_uri_free(list->uri); devid_free(list->devid); mem_free(list); list = next; } } /* Prepend device conf.devices list */ static void conf_device_list_prepend (const char *name, http_uri *uri, ID_PROTO proto) { conf_device *dev = mem_new(conf_device, 1); dev->name = str_dup(name); dev->devid = devid_alloc(); dev->proto = proto; dev->uri = uri; dev->next = conf.devices; conf.devices = dev; } /* Find device in conf.devices list */ static conf_device* conf_device_list_lookup (const char *name) { conf_device *dev = conf.devices; while (dev != NULL && strcmp(dev->name, name)) { dev = dev->next; } return dev; } /* Revert conf.blacklist list */ static void conf_blacklist_revert (void) { conf_blacklist *list = conf.blacklist, *prev = NULL, *next; while (list != NULL) { next = list->next; list->next = prev; prev = list; list = next; } conf.blacklist = prev; } /* Free conf.blacklist */ static void conf_blacklist_free (void) { while (conf.blacklist != NULL) { conf_blacklist *next = conf.blacklist->next; mem_free((char*) conf.blacklist->name); mem_free((char*) conf.blacklist->model); conf.blacklist = next; } } /* Expand path name. The returned string must be eventually * released with mem_free() */ static const char* conf_expand_path (const char *path) { const char *prefix = ""; char *ret; if (path[0] == '~' && (path[1] == '\0' || path[1] == '/')) { const char *home = os_homedir(); if (home != NULL) { prefix = home; path ++; } else { return NULL; } } ret = str_concat(prefix, path, NULL); ret = str_terminate(ret, '/'); return ret; } /* Report configuration file error */ static void conf_perror (const inifile_record *rec, const char *format, ...) { char buf[1024]; va_list ap; va_start(ap, format); vsnprintf(buf, sizeof(buf), format, ap); va_end(ap); log_debug(NULL, "%s:%d: %s", rec->file, rec->line, buf); } /* Decode a device configuration */ static void conf_decode_device (const inifile_record *rec) { http_uri *uri = NULL; ID_PROTO proto = ID_PROTO_ESCL; const char *uri_name = rec->tokc > 0 ? rec->tokv[0] : NULL; const char *proto_name = rec->tokc > 1 ? rec->tokv[1] : NULL; if (conf_device_list_lookup(rec->variable) != NULL) { conf_perror(rec, "device already defined"); } else if (!strcmp(rec->value, CONF_DEVICE_DISABLE)) { conf_device_list_prepend(rec->variable, NULL, ID_PROTO_UNKNOWN); } else if (rec->tokc != 1 && rec->tokc != 2) { conf_perror(rec, "usage: \"device name\" = URL[,protocol]"); } else if ((uri = http_uri_new(uri_name, true)) == NULL) { conf_perror(rec, "invalid URL"); } else if (proto_name != NULL&& (proto = id_proto_by_name(proto_name)) == ID_PROTO_UNKNOWN) { conf_perror(rec, "protocol must be \"escl\" or \"wsd\""); } if (uri != NULL && proto != ID_PROTO_UNKNOWN) { conf_device_list_prepend(rec->variable, uri, proto); } else { http_uri_free(uri); } } /* Parse binary option */ static void conf_load_bool (const inifile_record *rec, bool *out, const char *n_true, const char *n_false) { if (inifile_match_name(rec->value, n_true)) { *out = true; } else if (inifile_match_name(rec->value, n_false)) { *out = false; } else { conf_perror(rec, "usage: %s = %s | %s", rec->variable, n_true, n_false); } } /* Parse network address with mask */ static void conf_load_netaddr (const inifile_record *rec, ip_network *net) { char *addr, *mask; int af; int maxmask; memset(net, 0, sizeof(*net)); /* Split into address and mask */ addr = alloca(strlen(rec->value) + 1); strcpy(addr, rec->value); mask = strchr(addr, '/'); if (mask != NULL) { *mask = '\0'; mask ++; } /* Parse address */ if (strchr(addr, ':') == NULL) { af = AF_INET; maxmask = 32; } else { af = AF_INET6; maxmask = 128; } if (inet_pton(af, addr, &net->addr.ip) != 1) { conf_perror(rec, "invalid IP address %s", addr); return; } /* Parse mask, if any */ if (mask != NULL) { unsigned long l; char *end; l = strtoul(mask, &end, 10); if (end == mask || *end != '\0') { conf_perror(rec, "invalid network mask %s", mask); return; } if (l == 0 || l > (unsigned long) maxmask) { conf_perror(rec, "network mask out of range"); return; } net->mask = (int) l; } else { net->mask = maxmask; } /* Indicate success; all other return values already filled */ net->addr.af = af; } /* Load configuration from opened inifile */ static void conf_load_from_ini (inifile *ini) { const inifile_record *rec; while ((rec = inifile_read(ini)) != NULL) { switch (rec->type) { case INIFILE_SYNTAX: conf_perror(rec, "syntax error"); break; case INIFILE_VARIABLE: if (inifile_match_name(rec->section, "devices")) { conf_decode_device(rec); } else if (inifile_match_name(rec->section, "options")) { if (inifile_match_name(rec->variable, "discovery")) { conf_load_bool(rec, &conf.discovery, "enable", "disable"); } else if (inifile_match_name(rec->variable, "model")) { conf_load_bool(rec, &conf.model_is_netname, "network", "hardware"); } else if (inifile_match_name(rec->variable, "protocol")) { conf_load_bool(rec, &conf.proto_auto, "auto", "manual"); } else if (inifile_match_name(rec->variable, "ws-discovery")) { if (inifile_match_name(rec->value, "fast")) { conf.wsdd_mode = WSDD_FAST; } else if (inifile_match_name(rec->value, "full")) { conf.wsdd_mode = WSDD_FULL; } else if (inifile_match_name(rec->value, "off")) { conf.wsdd_mode = WSDD_OFF; } else { conf_perror(rec, "usage: %s = fast | full | off", rec->variable); } } else if (inifile_match_name(rec->variable, "socket_dir")) { mem_free((char*) conf.socket_dir); conf.socket_dir = conf_expand_path(rec->value); if (conf.socket_dir == NULL) { conf_perror(rec, "failed to expand socket_dir path"); } } else if (inifile_match_name(rec->variable, "pretend-local")) { conf_load_bool(rec, &conf.pretend_local, "true", "false"); } } else if (inifile_match_name(rec->section, "debug")) { if (inifile_match_name(rec->variable, "trace")) { mem_free((char*) conf.dbg_trace); conf.dbg_trace = conf_expand_path(rec->value); if (conf.dbg_trace == NULL) { conf_perror(rec, "failed to expand path"); } } else if (inifile_match_name(rec->variable, "enable")) { conf_load_bool(rec, &conf.dbg_enabled, "true", "false"); } else if (inifile_match_name(rec->variable, "hexdump")) { conf_load_bool(rec, &conf.dbg_hexdump, "true", "false"); } } else if (inifile_match_name(rec->section, "blacklist")) { conf_blacklist *ent = NULL; if (inifile_match_name(rec->variable, "name")) { ent = mem_new(conf_blacklist, 1); ent->name = str_dup(rec->value); } else if (inifile_match_name(rec->variable, "model")) { ent = mem_new(conf_blacklist, 1); ent->model = str_dup(rec->value); } else if (inifile_match_name(rec->variable, "ip")) { ip_network net; conf_load_netaddr(rec, &net); if (net.addr.af != AF_UNSPEC) { ent = mem_new(conf_blacklist, 1); ent->net = net; } } if (ent != NULL) { ent->next = conf.blacklist; conf.blacklist = ent; } } break; default: break; } } /* Trace implies console log */ if (conf.dbg_trace != NULL) { conf.dbg_enabled = true; } } /* Load configuration from the particular file */ static void conf_load_from_file (const char *name) { log_debug(NULL, "loading configuration file %s", name); inifile *ini = inifile_open(name); if (ini != NULL) { conf_load_from_ini(ini); inifile_close(ini); } } /* Load configuration from the specified directory * * This function uses its path parameter as its temporary * buffer and doesn't guarantee to preserve its content * * The `path' can be reallocated by this function; old * value is consumed and new is returned */ static char* conf_load_from_dir (char *path) { path = str_terminate(path, '/'); /* Load from CONFIG_AIRSCAN_CONF file */ size_t len = mem_len(path); path = str_append(path, CONFIG_AIRSCAN_CONF); conf_load_from_file(path); /* Scan CONFIG_AIRSCAN_D directory */ path = str_resize(path, len); path = str_append(path, CONFIG_AIRSCAN_D); path = str_terminate(path, '/'); len = mem_len(path); DIR *dir = opendir(path); if (dir) { struct dirent *ent; while ((ent = readdir(dir)) != NULL) { path = str_resize(path, len); path = str_append(path, ent->d_name); conf_load_from_file(path); } closedir(dir); } return path; } /* Load configuration from environment */ static void conf_load_from_env (void) { const char *env; /* Handle the CONFIG_ENV_AIRSCAN_DEBUG variable */ env = getenv(CONFIG_ENV_AIRSCAN_DEBUG); if (env != NULL) { if (inifile_match_name(env, "true")) { conf.dbg_enabled = true; } else if (inifile_match_name(env, "false")) { conf.dbg_enabled = false; } else { unsigned long v; char *end; v = strtoul(env, &end, 0); if (env != end && *end == '\0') { conf.dbg_enabled = v != 0; } else { log_debug(NULL, "usage: %s=true|false", CONFIG_ENV_AIRSCAN_DEBUG); } } } /* Handle the CONFIG_ENV_AIRSCAN_DEVICE variable */ env = getenv(CONFIG_ENV_AIRSCAN_DEVICE); if (env != NULL) { zeroconf_devinfo *devinfo = zeroconf_parse_devinfo_from_ident(env); /* Reset the static configuration and disable auto discovery. * * Note, if we can't parse CONFIG_ENV_AIRSCAN_DEVICE, we still * do this step. At this case user will see the empty list of * available devices. * * If it happens, this event will be logged into the debug log, * but this is the best what we can do. * * Unfortunately, we can't provide a more clear error indication * from this point; this is the SANE API limitation. */ conf_device_list_free(); devid_restart(); conf.discovery = false; if (devinfo != NULL) { zeroconf_endpoint *endpoint = devinfo->endpoints; conf_device_list_prepend(devinfo->name, http_uri_clone(endpoint->uri), endpoint->proto); zeroconf_devinfo_free(devinfo); } else { log_debug(NULL, "Invalid %s: \"%s\"", CONFIG_ENV_AIRSCAN_DEVICE, env); } } } /* Load configuration. Returns non-NULL (default configuration) * even if configuration file cannot be loaded */ void conf_load (void) { char *dir_list = str_new(); char *path = str_new(); char *s; /* Reset the configuration */ conf = conf_init; conf.socket_dir = str_dup(CONFIG_DEFAULT_SOCKET_DIR); devid_init(); /* Look to configuration path in environment */ s = getenv(CONFIG_PATH_ENV); if (s != NULL) { dir_list = str_assign(dir_list, s); } /* Append default directories */ dir_list = str_terminate(dir_list, ':'); dir_list = str_append(dir_list, CONFIG_SANE_CONFIG_DIR); /* Iterate over the dir_list */ for (s = dir_list; ; s ++) { if (*s == ':' || *s == '\0') { path = conf_load_from_dir(path); str_trunc(path); } else { path = str_append_c(path, *s); } if (*s == '\0') { break; } } /* Load configuration from environment */ conf_load_from_env(); /* Cleanup and exit */ conf_device_list_revert(); conf_blacklist_revert(); mem_free(dir_list); mem_free(path); } /* Free resources, allocated by conf_load, and reset configuration * data into initial state */ void conf_unload (void) { conf_device_list_free(); conf_blacklist_free(); mem_free((char*) conf.dbg_trace); mem_free((char*) conf.socket_dir); conf = conf_init; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-devcaps.c000066400000000000000000000164361500411437100174620ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Device capabilities */ #include "airscan.h" #include /* Allocate devcaps_source */ devcaps_source* devcaps_source_new (void) { devcaps_source *src = mem_new(devcaps_source, 1); src->resolutions = sane_word_array_new(); return src; } /* Free devcaps_source */ void devcaps_source_free (devcaps_source *src) { if (src != NULL) { sane_word_array_free(src->resolutions); mem_free(src); } } /* Clone a source */ devcaps_source* devcaps_source_clone (const devcaps_source *src) { devcaps_source *src2 = mem_new(devcaps_source, 1); unsigned int i, len; *src2 = *src; src2->resolutions = sane_word_array_new(); len = sane_word_array_len(src->resolutions); for (i = 1; i <= len; i ++) { SANE_Word res = src->resolutions[i]; src2->resolutions = sane_word_array_append(src2->resolutions, res); } return src2; } /* Merge two sources, resulting the source that contains * only capabilities, supported by two input sources * * Returns NULL, if sources cannot be merged */ devcaps_source* devcaps_source_merge (const devcaps_source *s1, const devcaps_source *s2) { devcaps_source *src = devcaps_source_new(); /* Merge flags */ src->flags = s1->flags & s2->flags; /* Merge formats */ src->formats = s1->formats & s2->formats; if ((src->formats & DEVCAPS_FORMATS_SUPPORTED) == 0) { goto FAIL; } /* Merge colormodes */ src->colormodes = s1->colormodes & s2->colormodes; if ((src->colormodes & DEVCAPS_COLORMODES_SUPPORTED) == 0) { goto FAIL; } /* Merge scanintents */ src->scanintents = s1->scanintents & s2->scanintents; /* Merge dimensions */ src->min_wid_px = math_max(s1->min_wid_px, s2->min_wid_px); src->max_wid_px = math_min(s1->max_wid_px, s2->max_wid_px); src->min_hei_px = math_max(s1->min_hei_px, s2->min_hei_px); src->max_hei_px = math_min(s1->max_hei_px, s2->max_hei_px); if ((src->min_wid_px > src->max_wid_px) || (src->min_hei_px > src->max_hei_px)) { goto FAIL; } if (!math_range_merge(&src->win_x_range_mm, &s1->win_x_range_mm, &s2->win_x_range_mm)) { goto FAIL; } if (!math_range_merge(&src->win_y_range_mm, &s1->win_y_range_mm, &s2->win_y_range_mm)) { goto FAIL; } /* Merge resolutions */ if ((src->flags & DEVCAPS_SOURCE_RES_DISCRETE) != 0) { sane_word_array_free(src->resolutions); src->resolutions = sane_word_array_intersect_sorted( s1->resolutions, s2->resolutions); if (sane_word_array_len(src->resolutions) == 0) { src->flags &= ~DEVCAPS_SOURCE_RES_DISCRETE; } } if ((src->flags & DEVCAPS_SOURCE_RES_RANGE) != 0) { if (!math_range_merge(&src->res_range, &s1->res_range, &s2->res_range)) { src->flags &= ~DEVCAPS_SOURCE_RES_RANGE; } } if ((src->flags & DEVCAPS_SOURCE_RES_ALL) == 0) { goto FAIL; } return src; FAIL: devcaps_source_free(src); return NULL; } /* Initialize Device Capabilities */ void devcaps_init (devcaps *caps) { (void) caps; } /* Cleanup Device Capabilities */ void devcaps_cleanup (devcaps *caps) { unsigned int i; for (i = 0; i < NUM_ID_SOURCE; i ++) { devcaps_source_free(caps->src[i]); } } /* Reset Device Capabilities into initial state */ void devcaps_reset (devcaps *caps) { devcaps_cleanup(caps); memset(caps, 0, sizeof(*caps)); devcaps_init(caps); } /* Dump device capabilities, for debugging * * The 3rd parameter, 'trace' configures the debug level * (log_debug vs log_trace) of the generated output */ void devcaps_dump (log_ctx *log, devcaps *caps, bool trace) { int i; char *buf = str_new(); void (*log_func) (log_ctx *log, const char *fmt, ...); log_func = trace ? log_trace : log_debug; log_func(log, "===== device capabilities ====="); log_func(log, " Size units: %d DPI", caps->units); log_func(log, " Protocol: %s", caps->protocol); if (caps->compression_ok) { log_func(log, " Compression min: %d", caps->compression_range.min); log_func(log, " Compression max: %d", caps->compression_range.max); log_func(log, " Compression step: %d", caps->compression_range.quant); log_func(log, " Compression norm: %d", caps->compression_norm); } str_trunc(buf); for (i = 0; i < NUM_ID_SOURCE; i ++) { if (caps->src[i] != NULL) { if (buf[0] != '\0') { buf = str_append(buf, ", "); } buf = str_append(buf, id_source_sane_name(i)); } } log_func(log, " Sources: %s", buf); ID_SOURCE id_src; for (id_src = (ID_SOURCE) 0; id_src < NUM_ID_SOURCE; id_src ++) { devcaps_source *src = caps->src[id_src]; char xbuf[64], ybuf[64]; if (src == NULL) { continue; } log_func(log, ""); log_func(log, " %s:", id_source_sane_name(id_src)); math_fmt_mm(math_px2mm_res(src->min_wid_px, caps->units), xbuf); math_fmt_mm(math_px2mm_res(src->min_hei_px, caps->units), ybuf); log_func(log, " Min window: %dx%d px, %sx%s mm", src->min_wid_px, src->min_hei_px, xbuf, ybuf); math_fmt_mm(math_px2mm_res(src->max_wid_px, caps->units), xbuf); math_fmt_mm(math_px2mm_res(src->max_hei_px, caps->units), ybuf); log_func(log, " Max window: %dx%d px, %sx%s mm", src->max_wid_px, src->max_hei_px, xbuf, ybuf); if (src->flags & DEVCAPS_SOURCE_RES_DISCRETE) { str_trunc(buf); for (i = 0; i < (int) sane_word_array_len(src->resolutions); i ++) { if (i != 0) { buf = str_append_c(buf, ' '); } buf = str_append_printf(buf, "%d", src->resolutions[i+1]); } log_func(log, " Resolutions: %s", buf); } str_trunc(buf); for (i = 0; i < NUM_ID_COLORMODE; i ++) { if ((src->colormodes & (1 << i)) != 0) { if (buf[0] != '\0') { buf = str_append(buf, ", "); } buf = str_append(buf, id_colormode_sane_name(i)); } } log_func(log, " Color modes: %s", buf); str_trunc(buf); for (i = 0; i < NUM_ID_FORMAT; i ++) { if ((src->formats & (1 << i)) != 0) { if (buf[0] != '\0') { buf = str_append(buf, ", "); } buf = str_append(buf, id_format_short_name(i)); } } log_func(log, " Formats: %s", buf); str_trunc(buf); for (i = 0; i < NUM_ID_SCANINTENT; i ++) { if ((src->scanintents & (1 << i)) != 0) { if (buf[0] != '\0') { buf = str_append(buf, ", "); } buf = str_append(buf, id_scanintent_sane_name(i)); } } log_func(log, " Intents: %s", buf); } mem_free(buf); log_func(log, ""); } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-device.c000066400000000000000000001546161500411437100172770ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Device management */ #include "airscan.h" #include #include #include /******************** Constants ********************/ /* HTTP timeouts, by operation, in milliseconds */ #define DEVICE_HTTP_TIMEOUT_DEVCAPS 20000 #define DEVICE_HTTP_TIMEOUT_PRECHECK 20000 #define DEVICE_HTTP_TIMEOUT_SCAN 30000 #define DEVICE_HTTP_TIMEOUT_LOAD -1 #define DEVICE_HTTP_TIMEOUT_CHECK 20000 #define DEVICE_HTTP_TIMEOUT_CLEANUP 30000 #define DEVICE_HTTP_TIMEOUT_CANCEL 30000 /* HTTP timeout for operation that was pending * in a moment of cancel (if any) */ #define DEVICE_HTTP_TIMEOUT_CANCELED_OP 10000 /******************** Device management ********************/ /* Device flags */ enum { DEVICE_SCANNING = (1 << 0), /* We are between sane_start() and final sane_read() */ DEVICE_READING = (1 << 1) /* sane_read() can be called */ }; /* Device state diagram * * OPENED * | * V * PROBING->PROBING_FAILED-------------------------------- * | | * V | * -->IDLE | * | | submit PROTO_OP_SCAN | * | V | * | SCANNING---------- | * | | | async cancel request received, | * | | | dev->stm_cancel_event signalled | * | | V | * | | CANCEL_REQ | * | | | dev->stm_cancel_event callback | * | | | | * | | +------ | * | | reached | | PROTO_OP_SCAN still pending | * | | CLEANUP | V | * | |<----------------CANCEL_DELAYED | * | | | | | PROTO_OP_SCAN failed | * | | V V ----------------- | * | | CANCEL_SENT | | * | | job | | cancel request | | * | | finished | | finished | | * | | V V | | * | | CANCEL_JOB_DONE CANCEL_REQ_DONE | | * | | cancel req | | job | | * | | finished | | finished | | * | V | | | | * | CLEANUP | | | | * | | | | | | * | V V V V | * ---DONE<-------------------------------------- | * | | * V | * CLOSED<------------------------------------------------ */ typedef enum { DEVICE_STM_OPENED, DEVICE_STM_PROBING, DEVICE_STM_PROBING_FAILED, DEVICE_STM_IDLE, DEVICE_STM_SCANNING, DEVICE_STM_CANCEL_REQ, DEVICE_STM_CANCEL_DELAYED, DEVICE_STM_CANCEL_SENT, DEVICE_STM_CANCEL_JOB_DONE, DEVICE_STM_CANCEL_REQ_DONE, DEVICE_STM_CLEANUP, DEVICE_STM_DONE, DEVICE_STM_CLOSED } DEVICE_STM_STATE; /* Device descriptor */ struct device { /* Common part */ zeroconf_devinfo *devinfo; /* Device info */ log_ctx *log; /* Logging context */ unsigned int flags; /* Device flags */ devopt opt; /* Device options */ int checking_http_status; /* HTTP status before CHECK_STATUS */ /* State machinery */ DEVICE_STM_STATE stm_state; /* Device state */ pthread_cond_t stm_cond; /* Signalled when state changes */ eloop_event *stm_cancel_event; /* Signalled to initiate cancel */ http_query *stm_cancel_query; /* CANCEL query */ bool stm_cancel_sent; /* Cancel was sent to device */ eloop_timer *stm_timer; /* Delay timer */ struct timespec stm_last_fail_time;/* Last failed sane_start() time */ /* Protocol handling */ proto_ctx proto_ctx; /* Protocol handler context */ /* I/O handling (AVAHI and HTTP) */ zeroconf_endpoint *endpoint_current; /* Current endpoint to probe */ /* Job status */ SANE_Status job_status; /* Job completion status */ SANE_Word job_skip_x; /* How much pixels to skip, */ SANE_Word job_skip_y; /* from left and top */ /* Image decoders */ image_decoder *decoders[NUM_ID_FORMAT]; /* Decoders by format */ /* Read machinery */ SANE_Bool read_non_blocking; /* Non-blocking I/O mode */ pollable *read_pollable; /* Signalled when read won't block */ http_data_queue *read_queue; /* Queue of received images */ http_data *read_image; /* Current image */ SANE_Byte *read_line_buf; /* Single-line buffer */ SANE_Int read_line_num; /* Current image line 0-based */ SANE_Int read_line_end; /* If read_line_num>read_line_end no more lines left in image */ SANE_Int read_line_real_wid; /* Real line width */ SANE_Int read_line_off; /* Current offset in the line */ SANE_Int read_skip_bytes; /* How many bytes to skip at line beginning */ bool read_24_to_8; /* Resample 24 to 8 bits */ filter *read_filters; /* Chain of image filters */ }; /* Static variables */ static device **device_table; /* Forward declarations */ static device* device_find_by_ident (const char *ident); static void device_http_cancel (device *dev); static void device_http_onerror (void *ptr, error err); static void device_proto_set (device *dev, ID_PROTO proto); static void device_scanner_capabilities_callback (void *ptr, http_query *q); static void device_probe_endpoint (device *dev, zeroconf_endpoint *endpoint); static void device_job_set_status (device *dev, SANE_Status status); static inline DEVICE_STM_STATE device_stm_state_get (device *dev); static void device_stm_state_set (device *dev, DEVICE_STM_STATE state); static bool device_stm_cancel_perform (device *dev, SANE_Status status); static void device_stm_op_callback (void *ptr, http_query *q); static void device_stm_cancel_event_callback (void *data); static void device_read_filters_setup (device *dev); static void device_read_filters_cleanup (device *dev); static void device_management_start_stop (bool start); /******************** Device table management ********************/ /* Create a device. * * May fail. At this case, NULL will be returned and status will be set */ static device* device_new (zeroconf_devinfo *devinfo) { device *dev; int i; /* Create device */ dev = mem_new(device, 1); dev->devinfo = devinfo; dev->log = log_ctx_new(dev->devinfo->name, NULL); log_debug(dev->log, "device created"); dev->proto_ctx.log = dev->log; dev->proto_ctx.devinfo = dev->devinfo; dev->proto_ctx.devcaps = &dev->opt.caps; devopt_init(&dev->opt); dev->proto_ctx.http = http_client_new(dev->log, dev); pthread_cond_init(&dev->stm_cond, NULL); dev->read_pollable = pollable_new(); dev->read_queue = http_data_queue_new(); /* Setup decoders * * Note, unfortunately, some devices lies in their supported * image formats list and may return image in different format, * that requested. See, for example, #281 for details * * So we must instantiate all supported decoders and dynamically * choose between them, based on the actual image content * * The previous approach, when we instantiated only decoders * that we are about to use, doesn't work anymore */ image_decoder_create_all(dev->decoders); for (i = 0; i < NUM_ID_FORMAT; i ++) { if (dev->decoders[i] != NULL) { log_debug(dev->log, "added image decoder: \"%s\"", id_format_short_name(i)); } } /* Add to the table */ device_table = ptr_array_append(device_table, dev); return dev; } /* Destroy a device */ static void device_free (device *dev, const char *log_msg) { /* Remove device from table */ log_debug(dev->log, "removed from device table"); ptr_array_del(device_table, ptr_array_find(device_table, dev)); /* Stop all pending I/O activity */ device_http_cancel(dev); if (dev->stm_cancel_event != NULL) { eloop_event_free(dev->stm_cancel_event); } if (dev->stm_timer != NULL) { eloop_timer_cancel(dev->stm_timer); } /* Release all memory */ device_proto_set(dev, ID_PROTO_UNKNOWN); devopt_cleanup(&dev->opt); http_client_free(dev->proto_ctx.http); http_uri_free(dev->proto_ctx.base_uri); http_uri_free(dev->proto_ctx.base_uri_nozone); mem_free((char*) dev->proto_ctx.location); pthread_cond_destroy(&dev->stm_cond); image_decoder_free_all(dev->decoders); http_data_queue_free(dev->read_queue); pollable_free(dev->read_pollable); device_read_filters_cleanup(dev); log_debug(dev->log, "device destroyed"); if (log_msg != NULL) { log_debug(dev->log, "%s", log_msg); } log_ctx_free(dev->log); zeroconf_devinfo_free(dev->devinfo); mem_free(dev); } /* Start probing. Called via eloop_call */ static void device_start_probing (void *data) { device *dev = data; device_probe_endpoint(dev, dev->devinfo->endpoints); } /* Start device I/O. */ static SANE_Status device_io_start (device *dev) { dev->stm_cancel_event = eloop_event_new(device_stm_cancel_event_callback, dev); if (dev->stm_cancel_event == NULL) { return SANE_STATUS_NO_MEM; } device_stm_state_set(dev, DEVICE_STM_PROBING); eloop_call(device_start_probing, dev); return SANE_STATUS_GOOD; } /* Find device by ident */ static device* device_find_by_ident (const char *ident) { size_t i, len = mem_len(device_table); for (i = 0; i < len; i ++) { device *dev = device_table[i]; if (!strcmp(dev->devinfo->ident, ident)) { return dev; } } return NULL; } /* Purge device_table */ static void device_table_purge (void) { while (mem_len(device_table) > 0) { device_free(device_table[0], NULL); } } /******************** Underlying protocol operations ********************/ /* Set protocol handler */ static void device_proto_set (device *dev, ID_PROTO proto) { if (dev->proto_ctx.proto != NULL) { log_debug(dev->log, "closed protocol \"%s\"", dev->proto_ctx.proto->name); dev->proto_ctx.proto->free(dev->proto_ctx.proto); dev->proto_ctx.proto = NULL; } if (proto != ID_PROTO_UNKNOWN) { dev->proto_ctx.proto = proto_handler_new(proto); log_assert(dev->log, dev->proto_ctx.proto != NULL); log_debug(dev->log, "using protocol \"%s\"", dev->proto_ctx.proto->name); } } /* Set base URI. `uri' ownership is taken by this function */ static void device_proto_set_base_uri (device *dev, http_uri *uri) { http_uri_free(dev->proto_ctx.base_uri); dev->proto_ctx.base_uri = uri; http_uri_free(dev->proto_ctx.base_uri_nozone); dev->proto_ctx.base_uri_nozone = http_uri_clone(uri); http_uri_strip_zone_suffux(dev->proto_ctx.base_uri_nozone); } /* Query device capabilities */ static void device_proto_devcaps_submit (device *dev, void (*callback) (void*, http_query*)) { http_query *q; q = dev->proto_ctx.proto->devcaps_query(&dev->proto_ctx); http_query_timeout(q, DEVICE_HTTP_TIMEOUT_DEVCAPS); http_query_submit(q, callback); dev->proto_ctx.query = q; } /* Decode device capabilities */ static error device_proto_devcaps_decode (device *dev, devcaps *caps) { return dev->proto_ctx.proto->devcaps_decode(&dev->proto_ctx, caps); } /* http_query_onrxhdr() callback */ static void device_proto_op_onrxhdr (void *p, http_query *q) { device *dev = p; if (dev->proto_ctx.op == PROTO_OP_LOAD && !dev->stm_cancel_sent) { http_query_timeout(q, -1); } } /* Submit operation request */ static void device_proto_op_submit (device *dev, PROTO_OP op, void (*callback) (void*, http_query*)) { http_query *(*func) (const proto_ctx *ctx) = NULL; int timeout = -1; http_query *q; switch (op) { case PROTO_OP_NONE: log_internal_error(dev->log); break; case PROTO_OP_FINISH: log_internal_error(dev->log); break; case PROTO_OP_PRECHECK: func = dev->proto_ctx.proto->precheck_query; timeout = DEVICE_HTTP_TIMEOUT_PRECHECK; break; case PROTO_OP_SCAN: func = dev->proto_ctx.proto->scan_query; timeout = DEVICE_HTTP_TIMEOUT_SCAN; break; case PROTO_OP_LOAD: func = dev->proto_ctx.proto->load_query; timeout = DEVICE_HTTP_TIMEOUT_LOAD; break; case PROTO_OP_CHECK: func = dev->proto_ctx.proto->status_query; timeout = DEVICE_HTTP_TIMEOUT_CHECK; break; case PROTO_OP_CLEANUP: func = dev->proto_ctx.proto->cleanup_query; timeout = DEVICE_HTTP_TIMEOUT_CLEANUP; break; } log_assert(dev->log, func != NULL); log_debug(dev->log, "%s: submitting: attempt=%d", proto_op_name(op), dev->proto_ctx.failed_attempt); dev->proto_ctx.op = op; q = func(&dev->proto_ctx); http_query_timeout(q, timeout); if (op == PROTO_OP_LOAD) { http_query_onrxhdr(q, device_proto_op_onrxhdr); } http_query_submit(q, callback); dev->proto_ctx.query = q; } /* Dummy decode for PROTO_OP_CANCEL and PROTO_OP_CLEANUP */ static proto_result device_proto_dummy_decode (const proto_ctx *ctx) { proto_result result = {0}; (void) ctx; result.next = PROTO_OP_FINISH; return result; } /* Decode operation response */ static proto_result device_proto_op_decode (device *dev, PROTO_OP op) { proto_result (*func) (const proto_ctx *ctx) = NULL; proto_result result; switch (op) { case PROTO_OP_NONE: log_internal_error(dev->log); break; case PROTO_OP_PRECHECK:func = dev->proto_ctx.proto->precheck_decode; break; case PROTO_OP_SCAN: func = dev->proto_ctx.proto->scan_decode; break; case PROTO_OP_LOAD: func = dev->proto_ctx.proto->load_decode; break; case PROTO_OP_CHECK: func = dev->proto_ctx.proto->status_decode; break; case PROTO_OP_CLEANUP: func = device_proto_dummy_decode; break; case PROTO_OP_FINISH: log_internal_error(dev->log); break; } log_assert(dev->log, func != NULL); log_debug(dev->log, "%s: decoding", proto_op_name(op)); result = func(&dev->proto_ctx); log_debug(dev->log, "%s: decoded: status=\"%s\" next=%s delay=%d", proto_op_name(op), sane_strstatus(result.status), proto_op_name(result.next), result.delay); if (result.next == PROTO_OP_CHECK) { int http_status = http_query_status(dev->proto_ctx.query); dev->proto_ctx.failed_op = op; dev->proto_ctx.failed_http_status = http_status; } if (op == PROTO_OP_CHECK) { dev->proto_ctx.failed_attempt ++; } return result; } /******************** HTTP operations ********************/ /* Cancel pending HTTP request, if any */ static void device_http_cancel (device *dev) { http_client_cancel(dev->proto_ctx.http); if (dev->stm_timer != NULL) { eloop_timer_cancel(dev->stm_timer); dev->stm_timer = NULL; } } /* http_client onerror callback */ static void device_http_onerror (void *ptr, error err) { device *dev = ptr; SANE_Status status; status = err == ERROR_ENOMEM ? SANE_STATUS_NO_MEM : SANE_STATUS_IO_ERROR; log_debug(dev->log, "cancelling job due to error: %s", ESTRING(err)); if (!device_stm_cancel_perform(dev, status)) { device_stm_state_set(dev, DEVICE_STM_DONE); } else { /* Scan job known to be done, now waiting for cancel * completion */ device_stm_state_set(dev, DEVICE_STM_CANCEL_JOB_DONE); } } /******************** Protocol initialization ********************/ /* Probe next device address */ 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); } dev->endpoint_current = endpoint; device_proto_set_base_uri(dev, http_uri_clone(endpoint->uri)); /* Fetch device capabilities */ device_proto_devcaps_submit (dev, device_scanner_capabilities_callback); } /* Scanner capabilities fetch callback */ static void device_scanner_capabilities_callback (void *ptr, http_query *q) { error err = NULL; device *dev = ptr; /* Check request status */ err = http_query_error(q); if (err != NULL) { err = eloop_eprintf("scanner capabilities query: %s", ESTRING(err)); goto DONE; } /* Parse XML response */ err = device_proto_devcaps_decode (dev, &dev->opt.caps); if (err != NULL) { err = eloop_eprintf("scanner capabilities: %s", err); goto DONE; } devcaps_dump(dev->log, &dev->opt.caps, true); devopt_set_defaults(&dev->opt); /* Update endpoint address in case of HTTP redirection */ if (!http_uri_equal(http_query_uri(q), http_query_real_uri(q))) { const char *uri_str = http_uri_str(http_query_uri(q)); const char *real_uri_str = http_uri_str(http_query_real_uri(q)); const char *base_str = http_uri_str(dev->proto_ctx.base_uri); if (str_has_prefix(uri_str, base_str)) { const char *tail = uri_str + strlen(base_str); if (str_has_suffix(real_uri_str, tail)) { size_t l = strlen(real_uri_str) - strlen(tail); char *new_uri_str = alloca(l + 1); http_uri *new_uri; memcpy(new_uri_str, real_uri_str, l); new_uri_str[l] = '\0'; log_debug(dev->log, "endpoint URI changed due to redirection:"); log_debug(dev->log, " old URL: %s", base_str); log_debug(dev->log, " new URL: %s", new_uri_str); new_uri = http_uri_new(new_uri_str, true); log_assert(dev->log, new_uri != NULL); device_proto_set_base_uri(dev, new_uri); } } } /* Cleanup and exit */ DONE: if (err != NULL) { log_debug(dev->log, ESTRING(err)); if (dev->endpoint_current != NULL && dev->endpoint_current->next != NULL) { device_probe_endpoint(dev, dev->endpoint_current->next); } else { device_stm_state_set(dev, DEVICE_STM_PROBING_FAILED); } } else { device_stm_state_set(dev, DEVICE_STM_IDLE); http_client_onerror(dev->proto_ctx.http, device_http_onerror); } } /******************** Scan state machinery ********************/ /* Get state name, for debugging */ static const char* device_stm_state_name (DEVICE_STM_STATE state) { switch (state) { case DEVICE_STM_OPENED: return "DEVICE_STM_OPENED"; case DEVICE_STM_PROBING: return "DEVICE_STM_PROBING"; case DEVICE_STM_PROBING_FAILED: return "DEVICE_STM_PROBING_FAILED"; case DEVICE_STM_IDLE: return "DEVICE_STM_IDLE"; case DEVICE_STM_SCANNING: return "DEVICE_STM_SCANNING"; case DEVICE_STM_CANCEL_REQ: return "DEVICE_STM_CANCEL_REQ"; case DEVICE_STM_CANCEL_DELAYED: return "DEVICE_STM_CANCEL_DELAYED"; case DEVICE_STM_CANCEL_SENT: return "DEVICE_STM_CANCEL_SENT"; case DEVICE_STM_CANCEL_JOB_DONE: return "DEVICE_STM_CANCEL_JOB_DONE"; case DEVICE_STM_CANCEL_REQ_DONE: return "DEVICE_STM_CANCEL_REQ_DONE"; case DEVICE_STM_CLEANUP: return "DEVICE_STM_CLEANUP"; case DEVICE_STM_DONE: return "DEVICE_STM_DONE"; case DEVICE_STM_CLOSED: return "DEVICE_STM_CLOSED"; } return NULL; } /* Get state */ static inline DEVICE_STM_STATE device_stm_state_get (device *dev) { return __atomic_load_n(&dev->stm_state, __ATOMIC_SEQ_CST); } /* Check if device is in working state */ static bool device_stm_state_working (device *dev) { DEVICE_STM_STATE state = device_stm_state_get(dev); return state > DEVICE_STM_IDLE && state < DEVICE_STM_DONE; } /* Set state */ static void device_stm_state_set (device *dev, DEVICE_STM_STATE state) { DEVICE_STM_STATE old_state = device_stm_state_get(dev); if (old_state != state) { log_debug(dev->log, "%s->%s", device_stm_state_name(old_state), device_stm_state_name(state)); __atomic_store_n(&dev->stm_state, state, __ATOMIC_SEQ_CST); pthread_cond_broadcast(&dev->stm_cond); if (!device_stm_state_working(dev)) { pollable_signal(dev->read_pollable); } } } /* cancel_query() callback */ static void device_stm_cancel_callback (void *ptr, http_query *q) { device *dev = ptr; (void) q; dev->stm_cancel_query = NULL; if (device_stm_state_get(dev) == DEVICE_STM_CANCEL_JOB_DONE) { device_stm_state_set(dev, DEVICE_STM_DONE); } else { device_stm_state_set(dev, DEVICE_STM_CANCEL_REQ_DONE); } } /* Perform cancel, if possible */ static bool device_stm_cancel_perform (device *dev, SANE_Status status) { proto_ctx *ctx = &dev->proto_ctx; device_job_set_status(dev, status); if (ctx->location != NULL && !dev->stm_cancel_sent) { if (ctx->params.src == ID_SOURCE_PLATEN && ctx->images_received > 0) { /* If we are not expecting more images, skip cancel * and simple wait until job is done * * Otherwise Xerox VersaLink B405 remains busy for * a quite long time without any need */ log_debug(dev->log, "cancel skipped as job is almost done"); return false; } else { /* Otherwise, perform a normal cancel operation */ device_stm_state_set(dev, DEVICE_STM_CANCEL_SENT); log_assert(dev->log, dev->stm_cancel_query == NULL); dev->stm_cancel_query = ctx->proto->cancel_query(ctx); http_query_onerror(dev->stm_cancel_query, NULL); http_query_timeout(dev->stm_cancel_query, DEVICE_HTTP_TIMEOUT_CANCEL); http_client_timeout(dev->proto_ctx.http, DEVICE_HTTP_TIMEOUT_CANCELED_OP); http_query_submit(dev->stm_cancel_query, device_stm_cancel_callback); dev->stm_cancel_sent = true; } return true; } return false; } /* stm_cancel_event callback */ static void device_stm_cancel_event_callback (void *data) { device *dev = data; log_debug(dev->log, "cancel processing started"); if (!device_stm_cancel_perform(dev, SANE_STATUS_CANCELLED)) { device_stm_state_set(dev, DEVICE_STM_CANCEL_DELAYED); } } /* Request cancel. * * Note, reason must be NULL, if cancel requested from the signal handler */ static void device_stm_cancel_req (device *dev, const char *reason) { DEVICE_STM_STATE expected = DEVICE_STM_SCANNING; bool ok = __atomic_compare_exchange_n(&dev->stm_state, &expected, DEVICE_STM_CANCEL_REQ, true, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); if (ok) { if (reason != NULL) { log_debug(dev->log, "cancel requested: %s", reason); } eloop_event_trigger(dev->stm_cancel_event); } } /* stm_timer callback */ static void device_stm_timer_callback (void *data) { device *dev = data; dev->stm_timer = NULL; device_proto_op_submit(dev, dev->proto_ctx.op, device_stm_op_callback); } /* Operation callback */ static void device_stm_op_callback (void *ptr, http_query *q) { device *dev = ptr; proto_result result = device_proto_op_decode(dev, dev->proto_ctx.op); (void) q; if (result.err != NULL) { log_debug(dev->log, "%s", ESTRING(result.err)); } /* Save useful result, if any */ if (dev->proto_ctx.op == PROTO_OP_SCAN) { if (result.data.location != NULL) { mem_free((char*) dev->proto_ctx.location); /* Just in case */ dev->proto_ctx.location = result.data.location; dev->proto_ctx.failed_attempt = 0; pthread_cond_broadcast(&dev->stm_cond); } } else if (dev->proto_ctx.op == PROTO_OP_LOAD) { if (result.data.image != NULL) { http_data_queue_push(dev->read_queue, result.data.image); dev->proto_ctx.images_received ++; pollable_signal(dev->read_pollable); dev->proto_ctx.failed_attempt = 0; pthread_cond_broadcast(&dev->stm_cond); } } /* Update job status */ device_job_set_status(dev, result.status); /* If CANCEL was sent, and next operation is CLEANUP or * current operation is CHECK, FINISH the job */ if (dev->stm_cancel_sent) { if (result.next == PROTO_OP_CLEANUP || dev->proto_ctx.op == PROTO_OP_CHECK) { result.next = PROTO_OP_FINISH; } } /* Check for FINISH */ if (result.next == PROTO_OP_FINISH) { if (dev->proto_ctx.images_received == 0) { /* If no images received, and no error status * yet set, use SANE_STATUS_IO_ERROR as default * error code */ device_job_set_status(dev, SANE_STATUS_IO_ERROR); } if (device_stm_state_get(dev) == DEVICE_STM_CANCEL_SENT) { device_stm_state_set(dev, DEVICE_STM_CANCEL_JOB_DONE); } else { device_stm_state_set(dev, DEVICE_STM_DONE); } return; } /* Handle switch to PROTO_OP_CLEANUP state */ if (result.next == PROTO_OP_CLEANUP) { device_stm_state_set(dev, DEVICE_STM_CLEANUP); } /* Handle delayed cancellation */ if (device_stm_state_get(dev) == DEVICE_STM_CANCEL_DELAYED) { if (!device_stm_cancel_perform(dev, SANE_STATUS_CANCELLED)) { /* Finish the job, if we has not yet reached cancellable * state */ device_stm_state_set(dev, DEVICE_STM_DONE); return; } } /* Handle delay */ if (result.delay != 0) { log_assert(dev->log, dev->stm_timer == NULL); dev->stm_timer = eloop_timer_new(result.delay, device_stm_timer_callback, dev); dev->proto_ctx.op = result.next; return; } /* Submit next operation */ device_proto_op_submit(dev, result.next, device_stm_op_callback); } /* Geometrical scan parameters */ typedef struct { SANE_Word off; /* Requested X/Y offset, in pixels assuming 300 DPI */ SANE_Word len; /* Requested width/height, in pixels, assuming 300 DPI */ SANE_Word skip; /* Pixels to skip in returned image, in pixels assuming actual resolution */ } device_geom; /* * Computing geometrical scan parameters * * Input: * tl, br - top-left, bottom-rights X/Y, in mm * minlen, maxlen - device-defined min and max width or height, * in pixels, assuming 300 dpi * res - scan resolution * * Output: * Filled device_geom structure * * Problem description. * * 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 * 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 * to simplify frontend's life, we pretend there is no such thing, * as a minimal width or height, otherwise TL and BR ranges become * dependent from each other. Instead, we always request image from * scanner not smaller that scanner's minimum, and clip excessive * image parts if required. * * This all makes things non-trivial. This function handles * this complexity */ static device_geom device_geom_compute (SANE_Fixed tl, SANE_Fixed br, SANE_Word minlen, SANE_Word maxlen, SANE_Word res, SANE_Word units) { device_geom geom; geom.off = math_mm2px_res(tl, units); geom.len = math_mm2px_res(br - tl, units); geom.skip = 0; minlen = math_max(minlen, 1); geom.len = math_bound(geom.len, minlen, maxlen); if (geom.off + geom.len > maxlen) { geom.skip = geom.off + geom.len - maxlen; geom.off -= geom.skip; geom.skip = math_muldiv(geom.skip, res, units); } return geom; } /* Choose image format */ static ID_FORMAT device_choose_format (device *dev, devcaps_source *src) { unsigned int formats = src->formats & DEVCAPS_FORMATS_SUPPORTED; size_t i; static const ID_FORMAT use[] = { ID_FORMAT_PNG, ID_FORMAT_JPEG, ID_FORMAT_TIFF, ID_FORMAT_BMP }; for (i = 0; i < sizeof(use)/sizeof(use[0]); i ++) { ID_FORMAT fmt = use[i]; if ((formats & (1 << fmt)) != 0) { return fmt; } } log_internal_error(dev->log); return ID_FORMAT_UNKNOWN; } /* Request scan */ static void device_stm_start_scan (device *dev) { device_geom geom_x, geom_y; proto_ctx *ctx = &dev->proto_ctx; proto_scan_params *params = &ctx->params; devcaps_source *src = dev->opt.caps.src[dev->opt.src]; SANE_Word x_resolution = dev->opt.resolution; SANE_Word y_resolution = dev->opt.resolution; char buf[64]; /* Prepare window parameters */ geom_x = device_geom_compute(dev->opt.tl_x, dev->opt.br_x, src->min_wid_px, src->max_wid_px, x_resolution, dev->opt.caps.units); geom_y = device_geom_compute(dev->opt.tl_y, dev->opt.br_y, src->min_hei_px, src->max_hei_px, y_resolution, dev->opt.caps.units); dev->job_skip_x = geom_x.skip; dev->job_skip_y = geom_y.skip; /* Fill proto_scan_params structure */ memset(params, 0, sizeof(*params)); params->x_off = geom_x.off; params->y_off = geom_y.off; params->wid = geom_x.len; params->hei = geom_y.len; params->x_res = x_resolution; params->y_res = y_resolution; params->src = dev->opt.src; params->colormode = dev->opt.colormode_real; params->scanintent = dev->opt.scanintent; params->format = device_choose_format(dev, src); /* Dump parameters */ log_trace(dev->log, "=============================="); log_trace(dev->log, "Starting scan, using the following parameters:"); log_trace(dev->log, " source: %s", id_source_sane_name(params->src)); log_trace(dev->log, " colormode_emul: %s", id_colormode_sane_name(dev->opt.colormode_emul)); log_trace(dev->log, " colormode_real: %s", id_colormode_sane_name(params->colormode)); log_trace(dev->log, " scanintent: %s", id_scanintent_sane_name(params->scanintent)); log_trace(dev->log, " tl_x: %s mm", math_fmt_mm(dev->opt.tl_x, buf)); log_trace(dev->log, " tl_y: %s mm", math_fmt_mm(dev->opt.tl_y, buf)); log_trace(dev->log, " br_x: %s mm", math_fmt_mm(dev->opt.br_x, buf)); log_trace(dev->log, " br_y: %s mm", math_fmt_mm(dev->opt.br_y, buf)); log_trace(dev->log, " image size: %dx%d", params->wid, params->hei); log_trace(dev->log, " image X offset: %d", params->x_off); log_trace(dev->log, " image Y offset: %d", params->y_off); log_trace(dev->log, " x_resolution: %d", params->x_res); log_trace(dev->log, " y_resolution: %d", params->y_res); log_trace(dev->log, " format: %s", id_format_short_name(params->format)); log_trace(dev->log, ""); /* Submit a request */ device_stm_state_set(dev, DEVICE_STM_SCANNING); if (dev->proto_ctx.proto->precheck_query != NULL) { device_proto_op_submit(dev, PROTO_OP_PRECHECK, device_stm_op_callback); } else { device_proto_op_submit(dev, PROTO_OP_SCAN, device_stm_op_callback); } } /* Wait until device leaves the working state */ static void device_stm_wait_while_working (device *dev) { while (device_stm_state_working(dev)) { eloop_cond_wait(&dev->stm_cond); } } /* Cancel scanning and wait until device leaves the working state */ static void device_stm_cancel_wait (device *dev, const char *reason) { device_stm_cancel_req(dev, reason); device_stm_wait_while_working(dev); } /******************** Scan Job management ********************/ /* Set job status. If status already set, it will not be changed */ static void device_job_set_status (device *dev, SANE_Status status) { /* Check status, new and present */ switch (status) { case SANE_STATUS_GOOD: return; case SANE_STATUS_CANCELLED: break; default: /* If error already is pending, leave it as is */ if (dev->job_status != SANE_STATUS_GOOD) { return; } } /* Update status */ if (status != dev->job_status) { log_debug(dev->log, "JOB status=%s", sane_strstatus(status)); dev->job_status = status; if (status == SANE_STATUS_CANCELLED) { http_data_queue_purge(dev->read_queue); } } } /******************** API helpers ********************/ /* Get device's logging context */ log_ctx* device_log_ctx (device *dev) { return dev ? dev->log : NULL; } /* Open a device */ device* device_open (const char *ident, SANE_Status *status) { device *dev = NULL; zeroconf_devinfo *devinfo; *status = SANE_STATUS_GOOD; /* Validate arguments */ if (ident == NULL || *ident == '\0') { log_debug(NULL, "device_open: invalid name"); *status = SANE_STATUS_INVAL; return NULL; } /* Already opened? */ dev = device_find_by_ident(ident); if (dev) { *status = SANE_STATUS_DEVICE_BUSY; return NULL; } /* Obtain device endpoints */ devinfo = zeroconf_devinfo_lookup(ident); if (devinfo == NULL) { log_debug(NULL, "device_open(%s): device not found", ident); *status = SANE_STATUS_INVAL; return NULL; } /* Create a device */ dev = device_new(devinfo); *status = device_io_start(dev); if (*status != SANE_STATUS_GOOD) { device_free(dev, NULL); return NULL; } /* Wait until device is initialized */ while (device_stm_state_get(dev) == DEVICE_STM_PROBING) { eloop_cond_wait(&dev->stm_cond); } if (device_stm_state_get(dev) == DEVICE_STM_PROBING_FAILED) { device_free(dev, NULL); *status = SANE_STATUS_IO_ERROR; return NULL; } return dev; } /* Close the device * If log_msg is not NULL, it is written to the device log as late as possible */ void device_close (device *dev, const char *log_msg) { /* Cancel job in progress, if any */ if (device_stm_state_working(dev)) { device_stm_cancel_wait(dev, "device close"); } /* Close the device */ device_stm_state_set(dev, DEVICE_STM_CLOSED); device_free(dev, log_msg); } /* Get option descriptor */ const SANE_Option_Descriptor* device_get_option_descriptor (device *dev, SANE_Int option) { if (0 <= option && option < NUM_OPTIONS) { return &dev->opt.desc[option]; } return NULL; } /* Get device option */ SANE_Status device_get_option (device *dev, SANE_Int option, void *value) { return devopt_get_option(&dev->opt, option, value); } /* Set device option */ SANE_Status device_set_option (device *dev, SANE_Int option, void *value, SANE_Word *info) { SANE_Status status; if ((dev->flags & DEVICE_SCANNING) != 0) { log_debug(dev->log, "device_set_option: already scanning"); return SANE_STATUS_INVAL; } status = devopt_set_option(&dev->opt, option, value, info); if (status == SANE_STATUS_GOOD && opt_is_enhancement(option)) { device_read_filters_setup(dev); } return status; } /* Get current scan parameters */ SANE_Status device_get_parameters (device *dev, SANE_Parameters *params) { *params = dev->opt.params; return SANE_STATUS_GOOD; } /* Start scanning operation - runs on a context of event loop thread */ static void device_start_do (void *data) { device *dev = data; device_stm_start_scan(dev); } /* Wait until new job is started */ static SANE_Status device_start_wait (device *dev) { for (;;) { DEVICE_STM_STATE state = device_stm_state_get(dev); switch (state) { case DEVICE_STM_IDLE: break; case DEVICE_STM_SCANNING: if (dev->proto_ctx.location != NULL) { return SANE_STATUS_GOOD; } break; case DEVICE_STM_DONE: return dev->job_status; default: return SANE_STATUS_GOOD; } eloop_cond_wait(&dev->stm_cond); } } /* Enforce CONFIG_START_RETRY_INTERVAL */ static void device_start_retry_pause (device *dev) { struct timespec now; int64_t pause_us; clock_gettime(CLOCK_MONOTONIC, &now); pause_us = (int64_t) (now.tv_sec - dev->stm_last_fail_time.tv_sec) * 1000000; pause_us += (int64_t) (now.tv_nsec - dev->stm_last_fail_time.tv_nsec) / 1000; pause_us = (int64_t) (CONFIG_START_RETRY_INTERVAL * 1000) - pause_us; if (pause_us > 1000) { log_debug(dev->log, "sane_start() retried too often; pausing for %d ms", (int) (pause_us / 1000)); eloop_mutex_unlock(); usleep((useconds_t) pause_us); eloop_mutex_lock(); } } /* Start new scanning job */ static SANE_Status device_start_new_job (device *dev) { SANE_Status status; device_start_retry_pause(dev); dev->stm_cancel_sent = false; dev->job_status = SANE_STATUS_GOOD; mem_free((char*) dev->proto_ctx.location); dev->proto_ctx.location = NULL; dev->proto_ctx.failed_op = PROTO_OP_NONE; dev->proto_ctx.failed_attempt = 0; dev->proto_ctx.images_received = 0; eloop_call(device_start_do, dev); log_debug(dev->log, "device_start_wait: waiting"); status = device_start_wait(dev); log_debug(dev->log, "device_start_wait: %s", sane_strstatus(status)); switch (status) { case SANE_STATUS_GOOD: case SANE_STATUS_CANCELLED: memset(&dev->stm_last_fail_time, 0, sizeof(dev->stm_last_fail_time)); break; default: clock_gettime(CLOCK_MONOTONIC, &dev->stm_last_fail_time); } if (status == SANE_STATUS_GOOD) { dev->flags |= DEVICE_READING; } else { dev->flags &= ~DEVICE_SCANNING; if (device_stm_state_get(dev) == DEVICE_STM_DONE) { device_stm_state_set(dev, DEVICE_STM_IDLE); } } return status; } /* Start scanning operation */ SANE_Status device_start (device *dev) { /* Already scanning? */ if ((dev->flags & DEVICE_SCANNING) != 0) { log_debug(dev->log, "device_start: already scanning"); return SANE_STATUS_INVAL; } /* Don's start if window is not valid */ if (dev->opt.params.lines == 0 || dev->opt.params.pixels_per_line == 0) { log_debug(dev->log, "device_start: invalid scan window"); return SANE_STATUS_INVAL; } /* Update state */ dev->flags |= DEVICE_SCANNING; pollable_reset(dev->read_pollable); dev->read_non_blocking = SANE_FALSE; /* Scanner idle? Start new job */ if (device_stm_state_get(dev) == DEVICE_STM_IDLE) { return device_start_new_job(dev); } /* Previous job still running. Synchronize with it */ while (device_stm_state_working(dev) && http_data_queue_len(dev->read_queue) == 0) { log_debug(dev->log, "device_start: waiting for background scan job"); eloop_cond_wait(&dev->stm_cond); } /* If we have more buffered images, just start * decoding the next one */ if (http_data_queue_len(dev->read_queue) > 0) { dev->flags |= DEVICE_READING; pollable_signal(dev->read_pollable); return SANE_STATUS_GOOD; } /* Seems that previous job has finished. * * If it failed by itself (but not cancelled), return its status now. * Otherwise, start new job */ log_assert (dev->log, device_stm_state_get(dev) == DEVICE_STM_DONE); device_stm_state_set(dev, DEVICE_STM_IDLE); if (dev->job_status != SANE_STATUS_GOOD && dev->job_status != SANE_STATUS_CANCELLED) { dev->flags &= ~DEVICE_SCANNING; return dev->job_status; } /* Start new scan job */ return device_start_new_job(dev); } /* Cancel scanning operation */ void device_cancel (device *dev) { /* Note, xsane calls sane_cancel() after each successful * scan "just in case", which kills scan job running in * background. So ignore cancel request, if from the API * point of view we are not "scanning" (i.e., not between * sane_start() and sane_read() completion) */ if ((dev->flags & DEVICE_SCANNING) == 0) { return; } device_stm_cancel_req(dev, NULL); } /* Set I/O mode */ SANE_Status device_set_io_mode (device *dev, SANE_Bool non_blocking) { if ((dev->flags & DEVICE_SCANNING) == 0) { log_debug(dev->log, "device_set_io_mode: not scanning"); return SANE_STATUS_INVAL; } dev->read_non_blocking = non_blocking; return SANE_STATUS_GOOD; } /* Get select file descriptor */ SANE_Status device_get_select_fd (device *dev, SANE_Int *fd) { if ((dev->flags & DEVICE_SCANNING) == 0) { log_debug(dev->log, "device_get_select_fd: not scanning"); return SANE_STATUS_INVAL; } *fd = pollable_get_fd(dev->read_pollable); return SANE_STATUS_GOOD; } /******************** Read machinery ********************/ /* Setup read_filters */ static void device_read_filters_setup (device *dev) { device_read_filters_cleanup(dev); dev->read_filters = filter_chain_push_xlat(NULL, &dev->opt); filter_chain_dump(dev->read_filters, dev->log); } /* Cleanup read_filters */ static void device_read_filters_cleanup (device *dev) { filter_chain_free(dev->read_filters); dev->read_filters = NULL; } /* Pull next image from the read queue and start decoding */ static SANE_Status device_read_next (device *dev) { error err; size_t line_capacity; SANE_Parameters params; image_decoder *decoder; int wid, hei; int skip_lines = 0; /* Pull next image out of the queue */ dev->read_image = http_data_queue_pull(dev->read_queue); if (dev->read_image == NULL) { return SANE_STATUS_EOF; } /* Guess format and choose decoder */ dev->proto_ctx.format_detected = image_format_detect(dev->read_image->bytes, dev->read_image->size); if (dev->proto_ctx.format_detected == ID_FORMAT_UNKNOWN) { err = eloop_eprintf("Can't detect image format"); goto DONE; } decoder = dev->decoders[dev->proto_ctx.format_detected]; if (decoder == NULL) { err = eloop_eprintf("Unsupported image format \"%s\"", id_format_short_name(dev->proto_ctx.format_detected)); goto DONE; } /* Start new image decoding */ err = image_decoder_begin(decoder, dev->read_image->bytes, dev->read_image->size); if (err != NULL) { goto DONE; } /* Obtain and dump image parameters */ image_decoder_get_params(decoder, ¶ms); log_trace(dev->log, "=============================="); log_trace(dev->log, "Image received with the following parameters:"); log_trace(dev->log, " format: %s", id_format_short_name(dev->proto_ctx.format_detected)); log_trace(dev->log, " content type: %s", image_content_type(decoder)); log_trace(dev->log, " frame format: %s", params.format == SANE_FRAME_GRAY ? "Gray" : "RGB" ); log_trace(dev->log, " image size: %dx%d", params.pixels_per_line, params.lines); log_trace(dev->log, " color depth: %d", params.depth); log_trace(dev->log, ""); /* Validate image parameters */ dev->read_24_to_8 = false; if (params.format == SANE_FRAME_RGB && dev->opt.params.format == SANE_FRAME_GRAY) { dev->read_24_to_8 = true; log_trace(dev->log, "resampling: RGB24->Grayscale8"); } else if (params.format != dev->opt.params.format) { /* This is what we cannot handle */ err = ERROR("Unexpected frame format"); goto DONE; } wid = params.pixels_per_line; hei = params.lines; /* Setup image clipping * * The following variants are possible: * * <------real image size------><--fill--> * <---skip---><---returned image size---> * <------------line capacity------------> * * <------------real image size------------> * <---skip---><--returned image size--> * <-------------line capacity-------------> * * Real image size is a size of image after decoder. * Returned image size is a size of image that we * return to the client * * If device for some reasons unable to handle X/Y * offset in hardware, we need to skip some bytes (horizontally) * or lines (vertically) * * If real image is smaller that expected, we need to * fill some bytes/lines with 0xff * * Line buffer capacity must be big enough to fit * real image size (we promised it do decoder) and * returned image size, whatever is large */ if (dev->job_skip_x >= wid || dev->job_skip_y >= hei) { /* Trivial case - just skip everything */ dev->read_line_end = 0; dev->read_skip_bytes = 0; dev->read_line_real_wid = 0; line_capacity = dev->opt.params.bytes_per_line; } else { image_window win; int bpp = dev->opt.params.format == SANE_FRAME_RGB ? 3 : 1; int returned_size_and_skip; win.x_off = dev->job_skip_x; win.y_off = dev->job_skip_y; win.wid = wid - dev->job_skip_x; win.hei = hei - dev->job_skip_y; err = image_decoder_set_window(decoder, &win); if (err != NULL) { goto DONE; } dev->read_skip_bytes = 0; if (win.x_off != dev->job_skip_x) { dev->read_skip_bytes = bpp * (dev->job_skip_x - win.x_off); } if (win.y_off != dev->job_skip_y) { skip_lines = dev->job_skip_y - win.y_off; } line_capacity = win.wid; if (params.format == SANE_FRAME_RGB) { line_capacity *= 3; } returned_size_and_skip = dev->read_skip_bytes + dev->opt.params.bytes_per_line; line_capacity = math_max(line_capacity, returned_size_and_skip); dev->read_line_real_wid = win.wid; } /* Initialize image decoding */ dev->read_line_buf = mem_new(SANE_Byte, line_capacity); memset(dev->read_line_buf, 0xff, line_capacity); dev->read_line_num = 0; dev->read_line_off = dev->opt.params.bytes_per_line; 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); DONE: if (err != NULL) { log_debug(dev->log, ESTRING(err)); http_data_unref(dev->read_image); dev->read_image = NULL; return SANE_STATUS_IO_ERROR; } return SANE_STATUS_GOOD; } /* Perform 24 to 8 bit image resampling for a single line */ static void device_read_24_to_8_resample (device *dev) { int i, len = dev->read_line_real_wid; uint8_t *in = dev->read_line_buf; uint8_t *out = dev->read_line_buf; for (i = 0; i < len; i ++) { /* Y = R * 0.299 + G * 0.587 + B * 0.114 * * 16777216 == 1 << 24 * 16777216 * 0.299 == 5016387.584 ~= 5016387 * 16777216 * 0.587 == 9848225.792 ~= 9848226 * 16777216 * 0.114 == 1912602.624 ~= 1912603 * * 5016387 + 9848226 + 1912603 == 16777216 */ unsigned long Y; Y = 5016387 * (unsigned long) *in ++; Y += 9848226 * (unsigned long) *in ++; Y += 1912603 * (unsigned long) *in ++; *out ++ = (Y + (1 << 23)) >> 24; } if (len < dev->opt.params.bytes_per_line) { memset(dev->read_line_buf + len, 0xff, dev->opt.params.bytes_per_line - len); } } /* Decode next image line * * Note, actual image size, returned by device, may be slightly different * from an image size, computed according to scan options and requested * from device. So here we adjust actual image to fit the expected (and * promised) parameters. * * Alternatively, we could make it problem of frontend. But fronends * expect image parameters to be accurate just after sane_start() returns, * so at this case sane_start() will have to wait a long time until image * is fully available. Taking in account that some popular frontends * (read "xsane") doesn't allow to cancel scanning before sane_start() * return, it is not good from the user experience perspective. */ static SANE_Status device_read_decode_line (device *dev) { const SANE_Int n = dev->read_line_num; image_decoder *decoder = dev->decoders[dev->proto_ctx.format_detected]; log_assert(dev->log, decoder != NULL); if (n == dev->opt.params.lines) { return SANE_STATUS_EOF; } 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); if (err != NULL) { log_debug(dev->log, ESTRING(err)); return SANE_STATUS_IO_ERROR; } if (dev->read_24_to_8) { device_read_24_to_8_resample(dev); } } filter_chain_apply(dev->read_filters, dev->read_line_buf, dev->opt.params.bytes_per_line); dev->read_line_off = 0; dev->read_line_num ++; return SANE_STATUS_GOOD; } /* Read scanned image */ SANE_Status device_read (device *dev, SANE_Byte *data, SANE_Int max_len, SANE_Int *len_out) { SANE_Int len = 0; SANE_Status status = SANE_STATUS_GOOD; image_decoder *decoder = dev->decoders[dev->proto_ctx.format_detected]; if (len_out != NULL) { *len_out = 0; /* Must return 0, if status is not GOOD */ } log_assert(dev->log, decoder != NULL); /* Check device state */ if ((dev->flags & DEVICE_READING) == 0) { log_debug(dev->log, "device_read: not scanning"); return SANE_STATUS_INVAL; } /* Validate arguments */ if (len_out == NULL) { log_debug(dev->log, "device_read: zero output buffer"); return SANE_STATUS_INVAL; } /* Wait until device is ready */ if (dev->read_image == NULL) { while (device_stm_state_working(dev) && http_data_queue_empty(dev->read_queue)) { if (dev->read_non_blocking) { *len_out = 0; return SANE_STATUS_GOOD; } eloop_cond_wait(&dev->stm_cond); } if (dev->job_status == SANE_STATUS_CANCELLED) { status = SANE_STATUS_CANCELLED; goto DONE; } if (http_data_queue_empty(dev->read_queue)) { status = dev->job_status; log_assert(dev->log, status != SANE_STATUS_GOOD); goto DONE; } status = device_read_next(dev); if (status != SANE_STATUS_GOOD) { goto DONE; } } /* 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); } 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_skip_bytes + dev->read_line_off, sz); data += sz; dev->read_line_off += sz; len += sz; } } if (status == SANE_STATUS_IO_ERROR) { device_job_set_status(dev, SANE_STATUS_IO_ERROR); device_stm_cancel_req(dev, "I/O error"); } /* Cleanup and exit */ DONE: if (status == SANE_STATUS_EOF && len > 0) { status = SANE_STATUS_GOOD; } if (status == SANE_STATUS_GOOD) { *len_out = len; return SANE_STATUS_GOOD; } /* Scan and read finished - cleanup device */ dev->flags &= ~(DEVICE_SCANNING | DEVICE_READING); image_decoder_reset(decoder); if (dev->read_image != NULL) { http_data_unref(dev->read_image); dev->read_image = NULL; } mem_free(dev->read_line_buf); dev->read_line_buf = NULL; if (device_stm_state_get(dev) == DEVICE_STM_DONE && (status != SANE_STATUS_EOF || dev->job_status == SANE_STATUS_GOOD)) { device_stm_state_set(dev, DEVICE_STM_IDLE); } return status; } /******************** Initialization/cleanup ********************/ /* Initialize device management */ SANE_Status device_management_init (void) { device_table = ptr_array_new(device*); eloop_add_start_stop_callback(device_management_start_stop); return SANE_STATUS_GOOD; } /* Cleanup device management */ void device_management_cleanup (void) { if (device_table != NULL) { log_assert(NULL, mem_len(device_table) == 0); mem_free(device_table); device_table = NULL; } } /* Start/stop device management */ static void device_management_start_stop (bool start) { if (!start) { device_table_purge(); } } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-devid.c000066400000000000000000000026721500411437100171250ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Unique device IDs */ #include "airscan.h" #include /* DEVID_RANGE defines device ID range, 0 ... DEVID_RANGE-1 * It must be power of two */ #define DEVID_RANGE 65536 static uint16_t devid_next; static uint32_t devid_bits[DEVID_RANGE/32]; /* Get bit in devid_bits[] */ static bool devid_bits_get(unsigned int id) { uint32_t mask = 1 << (id & 31); return (devid_bits[id / 32] & mask) != 0; } /* Set bit in devid_bits[] */ static void devid_bits_set(unsigned int id, bool v) { uint32_t mask = 1 << (id & 31); if (v) { devid_bits[id / 32] |= mask; } else { devid_bits[id / 32] &= ~mask; } } /* Allocate unique device ID */ unsigned int devid_alloc (void ) { while (devid_bits_get(devid_next)) { devid_next ++; } devid_bits_set(devid_next, true); return devid_next ++; } /* Free device ID */ void devid_free (unsigned int id) { devid_bits_set(id, false); } /* Restart device ID allocation counter. * Note, it doesn't free already allocated IDs, only restarts * counter from the beginning. */ void devid_restart (void) { devid_next = 0; } /* Initialize device ID allocator */ void devid_init (void) { devid_next = 0; memset(devid_bits, 0, sizeof(devid_bits)); } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-devops.c000066400000000000000000000601571500411437100173340ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Device options */ #include "airscan.h" #include #include /* Static variables */ static const SANE_Range devopt_percent_range = { .min = SANE_FIX(-100.0), .max = SANE_FIX(100.0), .quant = SANE_FIX(1.0) }; static const SANE_Range devopt_nonnegative_percent_range = { .min = SANE_FIX(0.0), .max = SANE_FIX(100.0), .quant = SANE_FIX(1.0) }; static const SANE_Range devopt_gamma_range = { .min = SANE_FIX(0.1), .max = SANE_FIX(4.0), }; /* Initialize device options */ void devopt_init (devopt *opt) { devcaps_init(&opt->caps); opt->src = ID_SOURCE_UNKNOWN; opt->colormode_emul = ID_COLORMODE_UNKNOWN; opt->colormode_real = ID_COLORMODE_UNKNOWN; opt->resolution = CONFIG_DEFAULT_RESOLUTION; opt->sane_sources = sane_string_array_new(); opt->sane_colormodes = sane_string_array_new(); opt->sane_scanintents = sane_string_array_new(); } /* Cleanup device options */ void devopt_cleanup (devopt *opt) { sane_string_array_free(opt->sane_sources); sane_string_array_free(opt->sane_colormodes); sane_string_array_free(opt->sane_scanintents); devcaps_cleanup(&opt->caps); } /* Choose default source */ static ID_SOURCE devopt_choose_default_source (devopt *opt) { /* Choose initial source */ ID_SOURCE id_src = (ID_SOURCE) 0; while (id_src < NUM_ID_SOURCE && (opt->caps.src[id_src]) == NULL) { id_src ++; } log_assert(NULL, id_src != NUM_ID_SOURCE); return id_src; } /* Get available color modes */ static unsigned int devopt_available_colormodes (const devcaps_source *src) { unsigned int colormodes = src->colormodes; if ((colormodes & (1 << ID_COLORMODE_COLOR)) != 0) { colormodes |= 1 << ID_COLORMODE_GRAYSCALE; /* We can resample! */ } return colormodes; } /* Chose "real" color mode that can be used for emulated color mode */ static ID_COLORMODE devopt_real_colormode (ID_COLORMODE emulated, const devcaps_source *src) { if ((src->colormodes & (1 << emulated)) != 0) { return emulated; } switch (emulated) { case ID_COLORMODE_GRAYSCALE: log_assert(NULL, (src->colormodes & (1 << ID_COLORMODE_COLOR)) != 0); return ID_COLORMODE_COLOR; default: log_internal_error(NULL); } return ID_COLORMODE_UNKNOWN; } /* Choose appropriate color mode */ static ID_COLORMODE devopt_choose_colormode (devopt *opt, ID_COLORMODE wanted) { devcaps_source *src = opt->caps.src[opt->src]; unsigned int colormodes = devopt_available_colormodes(src); /* Prefer wanted mode if possible and if not, try to find * a reasonable downgrade */ if (wanted != ID_COLORMODE_UNKNOWN) { while (wanted < NUM_ID_COLORMODE) { if ((colormodes & (1 << wanted)) != 0) { return wanted; } wanted ++; } } /* Nothing found in a previous step. Just choose the best mode * supported by the scanner */ wanted = (ID_COLORMODE) 0; while ((colormodes & (1 << wanted)) == 0) { log_assert(NULL, wanted < NUM_ID_COLORMODE); wanted ++; } return wanted; } /* Choose appropriate scanner resolution */ static SANE_Word devopt_choose_resolution (devopt *opt, SANE_Word wanted) { devcaps_source *src = opt->caps.src[opt->src]; if (src->flags & DEVCAPS_SOURCE_RES_DISCRETE) { SANE_Word res = src->resolutions[1]; SANE_Word delta = (SANE_Word) labs(wanted - res); size_t i, end = sane_word_array_len(src->resolutions) + 1; for (i = 2; i < end; i ++) { SANE_Word res2 = src->resolutions[i]; SANE_Word delta2 = (SANE_Word) labs(wanted - res2); if (delta2 <= delta) { res = res2; delta = delta2; } } return res; } else { return math_range_fit(&src->res_range, wanted); } } /* Rebuild option descriptors */ static void devopt_rebuild_opt_desc (devopt *opt) { SANE_Option_Descriptor *desc; devcaps_source *src = opt->caps.src[opt->src]; unsigned int colormodes = devopt_available_colormodes(src); unsigned int scanintents = src->scanintents; int i; const char *s; memset(opt->desc, 0, sizeof(opt->desc)); sane_string_array_reset(opt->sane_sources); sane_string_array_reset(opt->sane_colormodes); sane_string_array_reset(opt->sane_scanintents); for (i = 0; i < NUM_ID_SOURCE; i ++) { if (opt->caps.src[i] != NULL) { opt->sane_sources = sane_string_array_append( opt->sane_sources, (SANE_String) id_source_sane_name(i)); } } for (i = 0; i < NUM_ID_COLORMODE; i ++) { if ((colormodes & (1 << i)) != 0) { opt->sane_colormodes = sane_string_array_append( opt->sane_colormodes, (SANE_String) id_colormode_sane_name(i)); } } scanintents |= 1 << ID_SCANINTENT_UNSET; /* Always implicitly supported */ for (i = 0; i < NUM_ID_SCANINTENT; i ++) { if ((scanintents & (1 << i)) != 0) { opt->sane_scanintents = sane_string_array_append( opt->sane_scanintents, (SANE_String) id_scanintent_sane_name(i)); } } /* OPT_NUM_OPTIONS */ desc = &opt->desc[OPT_NUM_OPTIONS]; desc->name = SANE_NAME_NUM_OPTIONS; desc->title = SANE_TITLE_NUM_OPTIONS; desc->desc = SANE_DESC_NUM_OPTIONS; desc->type = SANE_TYPE_INT; desc->size = sizeof(SANE_Word); desc->cap = SANE_CAP_SOFT_DETECT; /* OPT_GROUP_STANDARD */ desc = &opt->desc[OPT_GROUP_STANDARD]; desc->name = SANE_NAME_STANDARD; desc->title = SANE_TITLE_STANDARD; desc->desc = SANE_DESC_STANDARD; desc->type = SANE_TYPE_GROUP; desc->cap = 0; /* OPT_SCAN_RESOLUTION */ desc = &opt->desc[OPT_SCAN_RESOLUTION]; desc->name = SANE_NAME_SCAN_RESOLUTION; desc->title = SANE_TITLE_SCAN_RESOLUTION; desc->desc = SANE_DESC_SCAN_RESOLUTION; desc->type = SANE_TYPE_INT; desc->size = sizeof(SANE_Word); desc->cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT; desc->unit = SANE_UNIT_DPI; if ((src->flags & DEVCAPS_SOURCE_RES_DISCRETE) != 0) { desc->constraint_type = SANE_CONSTRAINT_WORD_LIST; desc->constraint.word_list = src->resolutions; } else { desc->constraint_type = SANE_CONSTRAINT_RANGE; desc->constraint.range = &src->res_range; } /* OPT_SCAN_MODE */ desc = &opt->desc[OPT_SCAN_COLORMODE]; desc->name = SANE_NAME_SCAN_MODE; desc->title = SANE_TITLE_SCAN_MODE; desc->desc = SANE_DESC_SCAN_MODE; desc->type = SANE_TYPE_STRING; desc->size = sane_string_array_max_strlen(opt->sane_colormodes) + 1; desc->cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT; desc->constraint_type = SANE_CONSTRAINT_STRING_LIST; desc->constraint.string_list = (SANE_String_Const*) opt->sane_colormodes; /* OPT_SCAN_INTENT */ desc = &opt->desc[OPT_SCAN_INTENT]; desc->name = "scan-intent"; desc->title = "Scan intent"; desc->desc = "Optimize scan for Text/Photo/etc."; desc->type = SANE_TYPE_STRING; desc->size = sane_string_array_max_strlen(opt->sane_scanintents) + 1; desc->cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT; desc->constraint_type = SANE_CONSTRAINT_STRING_LIST; desc->constraint.string_list = (SANE_String_Const*) opt->sane_scanintents; /* OPT_SCAN_SOURCE */ desc = &opt->desc[OPT_SCAN_SOURCE]; desc->name = SANE_NAME_SCAN_SOURCE; desc->title = SANE_TITLE_SCAN_SOURCE; desc->desc = SANE_DESC_SCAN_SOURCE; desc->type = SANE_TYPE_STRING; desc->size = sane_string_array_max_strlen(opt->sane_sources) + 1; desc->cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT; desc->constraint_type = SANE_CONSTRAINT_STRING_LIST; desc->constraint.string_list = (SANE_String_Const*) opt->sane_sources; /* OPT_GROUP_GEOMETRY */ desc = &opt->desc[OPT_GROUP_GEOMETRY]; desc->name = SANE_NAME_GEOMETRY; desc->title = SANE_TITLE_GEOMETRY; desc->desc = SANE_DESC_GEOMETRY; desc->type = SANE_TYPE_GROUP; desc->cap = 0; /* OPT_SCAN_TL_X */ desc = &opt->desc[OPT_SCAN_TL_X]; desc->name = SANE_NAME_SCAN_TL_X; desc->title = SANE_TITLE_SCAN_TL_X; desc->desc = SANE_DESC_SCAN_TL_X; desc->type = SANE_TYPE_FIXED; desc->size = sizeof(SANE_Fixed); desc->cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT; desc->unit = SANE_UNIT_MM; desc->constraint_type = SANE_CONSTRAINT_RANGE; desc->constraint.range = &src->win_x_range_mm; /* OPT_SCAN_TL_Y */ desc = &opt->desc[OPT_SCAN_TL_Y]; desc->name = SANE_NAME_SCAN_TL_Y; desc->title = SANE_TITLE_SCAN_TL_Y; desc->desc = SANE_DESC_SCAN_TL_Y; desc->type = SANE_TYPE_FIXED; desc->size = sizeof(SANE_Fixed); desc->cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT; desc->unit = SANE_UNIT_MM; desc->constraint_type = SANE_CONSTRAINT_RANGE; desc->constraint.range = &src->win_y_range_mm; /* OPT_SCAN_BR_X */ desc = &opt->desc[OPT_SCAN_BR_X]; desc->name = SANE_NAME_SCAN_BR_X; desc->title = SANE_TITLE_SCAN_BR_X; desc->desc = SANE_DESC_SCAN_BR_X; desc->type = SANE_TYPE_FIXED; desc->size = sizeof(SANE_Fixed); desc->cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT; desc->unit = SANE_UNIT_MM; desc->constraint_type = SANE_CONSTRAINT_RANGE; desc->constraint.range = &src->win_x_range_mm; /* OPT_SCAN_BR_Y */ desc = &opt->desc[OPT_SCAN_BR_Y]; desc->name = SANE_NAME_SCAN_BR_Y; desc->title = SANE_TITLE_SCAN_BR_Y; desc->desc = SANE_DESC_SCAN_BR_Y; desc->type = SANE_TYPE_FIXED; desc->size = sizeof(SANE_Fixed); desc->cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT; desc->unit = SANE_UNIT_MM; desc->constraint_type = SANE_CONSTRAINT_RANGE; desc->constraint.range = &src->win_y_range_mm; /* OPT_GROUP_ENHANCEMENT */ desc = &opt->desc[OPT_GROUP_ENHANCEMENT]; desc->name = SANE_NAME_ENHANCEMENT; desc->title = SANE_TITLE_ENHANCEMENT; desc->desc = SANE_DESC_ENHANCEMENT; desc->type = SANE_TYPE_GROUP; desc->cap = 0; /* OPT_BRIGHTNESS */ desc = &opt->desc[OPT_BRIGHTNESS]; desc->name = SANE_NAME_BRIGHTNESS; desc->title = SANE_TITLE_BRIGHTNESS; desc->desc = SANE_DESC_BRIGHTNESS; desc->type = SANE_TYPE_FIXED; desc->size = sizeof(SANE_Fixed); desc->cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT | SANE_CAP_EMULATED; desc->unit = SANE_UNIT_PERCENT; desc->constraint_type = SANE_CONSTRAINT_RANGE; desc->constraint.range = &devopt_percent_range; /* OPT_CONTRAST */ desc = &opt->desc[OPT_CONTRAST]; desc->name = SANE_NAME_CONTRAST; desc->title = SANE_TITLE_CONTRAST; desc->desc = SANE_DESC_CONTRAST; desc->type = SANE_TYPE_FIXED; desc->size = sizeof(SANE_Fixed); desc->cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT | SANE_CAP_EMULATED; desc->unit = SANE_UNIT_PERCENT; desc->constraint_type = SANE_CONSTRAINT_RANGE; desc->constraint.range = &devopt_percent_range; /* OPT_SHADOW */ desc = &opt->desc[OPT_SHADOW]; desc->name = SANE_NAME_SHADOW; desc->title = SANE_TITLE_SHADOW; desc->desc = SANE_DESC_SHADOW; desc->type = SANE_TYPE_FIXED; desc->size = sizeof(SANE_Fixed); desc->cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT | SANE_CAP_EMULATED; desc->unit = SANE_UNIT_PERCENT; desc->constraint_type = SANE_CONSTRAINT_RANGE; desc->constraint.range = &devopt_nonnegative_percent_range; /* OPT_HIGHLIGHT */ desc = &opt->desc[OPT_HIGHLIGHT]; desc->name = SANE_NAME_HIGHLIGHT; desc->title = SANE_TITLE_HIGHLIGHT; desc->desc = SANE_DESC_HIGHLIGHT; desc->type = SANE_TYPE_FIXED; desc->size = sizeof(SANE_Fixed); desc->cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT | SANE_CAP_EMULATED; desc->unit = SANE_UNIT_PERCENT; desc->constraint_type = SANE_CONSTRAINT_RANGE; desc->constraint.range = &devopt_nonnegative_percent_range; /* OPT_GAMMA */ desc = &opt->desc[OPT_GAMMA]; desc->name = SANE_NAME_ANALOG_GAMMA; desc->title = SANE_TITLE_ANALOG_GAMMA; desc->desc = SANE_DESC_ANALOG_GAMMA; desc->type = SANE_TYPE_FIXED; desc->size = sizeof(SANE_Fixed); desc->cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT | SANE_CAP_EMULATED; desc->unit = SANE_UNIT_NONE; desc->constraint_type = SANE_CONSTRAINT_RANGE; desc->constraint.range = &devopt_gamma_range; /* OPT_NEGATIVE */ desc = &opt->desc[OPT_NEGATIVE]; desc->name = SANE_NAME_NEGATIVE; desc->title = SANE_TITLE_NEGATIVE; desc->desc = SANE_DESC_NEGATIVE; desc->type = SANE_TYPE_BOOL; desc->size = sizeof(SANE_Bool); desc->cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT | SANE_CAP_EMULATED; /* OPT_JUSTIFICATION_X */ desc = &opt->desc[OPT_JUSTIFICATION_X]; desc->name = SANE_NAME_ADF_JUSTIFICATION_X; desc->title = SANE_TITLE_ADF_JUSTIFICATION_X; desc->desc = SANE_DESC_ADF_JUSTIFICATION_X; desc->type = SANE_TYPE_STRING; desc->cap = SANE_CAP_SOFT_DETECT; if (opt->caps.justification_x == ID_JUSTIFICATION_UNKNOWN) { desc->cap |= SANE_CAP_INACTIVE; } s = id_justification_sane_name(opt->caps.justification_x); desc->size = (s ? strlen(s) : 0) + 1; /* OPT_JUSTIFICATION_Y */ desc = &opt->desc[OPT_JUSTIFICATION_Y]; desc->name = SANE_NAME_ADF_JUSTIFICATION_Y; desc->title = SANE_TITLE_ADF_JUSTIFICATION_Y; desc->desc = SANE_DESC_ADF_JUSTIFICATION_Y; desc->type = SANE_TYPE_STRING; desc->cap = SANE_CAP_SOFT_DETECT; if (opt->caps.justification_y == ID_JUSTIFICATION_UNKNOWN) { desc->cap |= SANE_CAP_INACTIVE; } s = id_justification_sane_name(opt->caps.justification_y); desc->size = (s ? strlen(s) : 0) + 1; } /* Update scan parameters, according to the currently set * scan options */ static void devopt_update_params (devopt *opt) { SANE_Fixed wid = math_max(0, opt->br_x - opt->tl_x); SANE_Fixed hei = math_max(0, opt->br_y - opt->tl_y); opt->params.last_frame = SANE_TRUE; opt->params.pixels_per_line = math_mm2px_res(wid, opt->resolution); opt->params.lines = math_mm2px_res(hei, opt->resolution); switch (opt->colormode_emul) { case ID_COLORMODE_COLOR: opt->params.format = SANE_FRAME_RGB; opt->params.depth = 8; opt->params.bytes_per_line = opt->params.pixels_per_line * 3; break; case ID_COLORMODE_GRAYSCALE: opt->params.format = SANE_FRAME_GRAY; opt->params.depth = 8; opt->params.bytes_per_line = opt->params.pixels_per_line; break; case ID_COLORMODE_BW1: opt->params.format = SANE_FRAME_GRAY; opt->params.depth = 1; opt->params.bytes_per_line = ((opt->params.pixels_per_line + 7) / 8) * 8; break; default: log_assert(NULL, !"internal error"); } } /* Set current resolution */ static SANE_Status devopt_set_resolution (devopt *opt, SANE_Word opt_resolution, SANE_Word *info) { if (opt->resolution == opt_resolution) { return SANE_STATUS_GOOD; } opt->resolution = devopt_choose_resolution(opt, opt_resolution); *info |= SANE_INFO_RELOAD_PARAMS; if (opt->resolution != opt_resolution) { *info |= SANE_INFO_INEXACT; } return SANE_STATUS_GOOD; } /* Set color mode */ static SANE_Status devopt_set_colormode (devopt *opt, ID_COLORMODE id_colormode, SANE_Word *info) { devcaps_source *src = opt->caps.src[opt->src]; unsigned int colormodes = devopt_available_colormodes(src); if (opt->colormode_emul == id_colormode) { return SANE_STATUS_GOOD; } if ((colormodes & (1 << id_colormode)) == 0) { return SANE_STATUS_INVAL; } opt->colormode_emul = id_colormode; opt->colormode_real = devopt_real_colormode(id_colormode, src); *info |= SANE_INFO_RELOAD_PARAMS; return SANE_STATUS_GOOD; } /* Set current source. Affects many other options */ static SANE_Status devopt_set_source (devopt *opt, ID_SOURCE id_src, SANE_Word *info) { devcaps_source *src = opt->caps.src[id_src]; if (src == NULL) { return SANE_STATUS_INVAL; } if (opt->src == id_src) { return SANE_STATUS_GOOD; } opt->src = id_src; /* Try to preserve current color mode */ opt->colormode_emul = devopt_choose_colormode(opt, opt->colormode_emul); /* Try to preserve resolution */ opt->resolution = devopt_choose_resolution(opt, opt->resolution); /* Reset window to maximum size */ opt->tl_x = 0; opt->tl_y = 0; opt->br_x = src->win_x_range_mm.max; opt->br_y = src->win_y_range_mm.max; *info |= SANE_INFO_RELOAD_OPTIONS | SANE_INFO_RELOAD_PARAMS; return SANE_STATUS_GOOD; } /* Set scan intent. */ static SANE_Status devopt_set_scanintent (devopt *opt, ID_SCANINTENT intent) { devcaps_source *src = opt->caps.src[opt->src]; unsigned int scanintents = src->scanintents; scanintents |= 1 << ID_SCANINTENT_UNSET; /* Always implicitly supported */ if ((scanintents & (1 << intent)) == 0) { return SANE_STATUS_INVAL; } opt->scanintent = intent; return SANE_STATUS_GOOD; } /* Set geometry option */ static SANE_Status devopt_set_geom (devopt *opt, SANE_Int option, SANE_Fixed val, SANE_Word *info) { SANE_Fixed *out = NULL; SANE_Range *range = NULL; devcaps_source *src = opt->caps.src[opt->src]; /* Choose destination and range */ switch (option) { case OPT_SCAN_TL_X: out = &opt->tl_x; range = &src->win_x_range_mm; break; case OPT_SCAN_TL_Y: out = &opt->tl_y; range = &src->win_y_range_mm; break; case OPT_SCAN_BR_X: out = &opt->br_x; range = &src->win_x_range_mm; break; case OPT_SCAN_BR_Y: out = &opt->br_y; range = &src->win_y_range_mm; break; default: log_internal_error(NULL); } /* Update option */ if (*out != val) { *out = math_range_fit(range, val); if (*out == val) { *info |= SANE_INFO_RELOAD_PARAMS; } else { *info |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_INEXACT; } } return SANE_STATUS_GOOD; } /* Set enhancement option */ static SANE_Status devopt_set_enh (devopt *opt, SANE_Int option, SANE_Fixed val, SANE_Word *info) { SANE_Fixed *out = NULL; SANE_Range range = *opt->desc[option].constraint.range; SANE_Fixed val_adjusted; switch (option) { case OPT_BRIGHTNESS: out = &opt->brightness; break; case OPT_CONTRAST: out = &opt->contrast; break; case OPT_SHADOW: out = &opt->shadow; range.max = opt->highlight - range.quant; break; case OPT_HIGHLIGHT: out = &opt->highlight; range.min = opt->shadow + range.quant; break; case OPT_GAMMA: out = &opt->gamma; break; default: log_internal_error(NULL); } val_adjusted = math_range_fit(&range, val); if (val_adjusted != val) { *info |= SANE_INFO_INEXACT; } *out = val_adjusted; return SANE_STATUS_GOOD; } /* Set default option values. Before call to this function, * devopt.caps needs to be properly filled. */ void devopt_set_defaults (devopt *opt) { devcaps_source *src; opt->src = devopt_choose_default_source(opt); src = opt->caps.src[opt->src]; opt->colormode_emul = devopt_choose_colormode(opt, ID_COLORMODE_UNKNOWN); opt->colormode_real = devopt_real_colormode(opt->colormode_emul, src); opt->scanintent = ID_SCANINTENT_UNSET; opt->resolution = devopt_choose_resolution(opt, CONFIG_DEFAULT_RESOLUTION); opt->tl_x = 0; opt->tl_y = 0; opt->br_x = src->win_x_range_mm.max; opt->br_y = src->win_y_range_mm.max; opt->brightness = SANE_FIX(0.0); opt->contrast = SANE_FIX(0.0); opt->shadow = SANE_FIX(0.0); opt->highlight = SANE_FIX(100.0); opt->gamma = SANE_FIX(1.0); devopt_rebuild_opt_desc(opt); devopt_update_params(opt); } /* Set device option */ SANE_Status devopt_set_option (devopt *opt, SANE_Int option, void *value, SANE_Word *info) { SANE_Status status = SANE_STATUS_GOOD; ID_SOURCE id_src; ID_COLORMODE id_colormode; ID_SCANINTENT id_scanintent; /* Simplify life of options handlers by ensuring info != NULL */ if (info == NULL) { static SANE_Word unused; info = &unused; } *info = 0; /* Switch by option */ switch (option) { case OPT_SCAN_RESOLUTION: status = devopt_set_resolution(opt, *(SANE_Word*)value, info); break; case OPT_SCAN_COLORMODE: id_colormode = id_colormode_by_sane_name(value); if (id_colormode == ID_COLORMODE_UNKNOWN) { status = SANE_STATUS_INVAL; } else { status = devopt_set_colormode(opt, id_colormode, info); } break; case OPT_SCAN_INTENT: id_scanintent = id_scanintent_by_sane_name(value); if (id_scanintent == ID_SCANINTENT_UNKNOWN) { status = SANE_STATUS_INVAL; } else { status = devopt_set_scanintent(opt, id_scanintent); } break; case OPT_SCAN_SOURCE: id_src = id_source_by_sane_name(value); if (id_src == ID_SOURCE_UNKNOWN) { status = SANE_STATUS_INVAL; } else { status = devopt_set_source(opt, id_src, info); } break; case OPT_SCAN_TL_X: case OPT_SCAN_TL_Y: case OPT_SCAN_BR_X: case OPT_SCAN_BR_Y: status = devopt_set_geom(opt, option, *(SANE_Fixed*)value, info); break; case OPT_BRIGHTNESS: case OPT_CONTRAST: case OPT_SHADOW: case OPT_HIGHLIGHT: case OPT_GAMMA: status = devopt_set_enh(opt, option, *(SANE_Fixed*)value, info); break; case OPT_NEGATIVE: opt->negative = *(SANE_Bool*)value != 0; break; default: status = SANE_STATUS_INVAL; } /* Rebuild option descriptors and update scan parameters, if needed */ if ((*info & SANE_INFO_RELOAD_OPTIONS) != 0) { devopt_rebuild_opt_desc(opt); } if ((*info & SANE_INFO_RELOAD_PARAMS) != 0) { devopt_update_params(opt); } return status; } /* Get device option */ SANE_Status devopt_get_option (devopt *opt, SANE_Int option, void *value) { SANE_Status status = SANE_STATUS_GOOD; const char *s; switch (option) { case OPT_NUM_OPTIONS: *(SANE_Word*) value = NUM_OPTIONS; break; case OPT_SCAN_RESOLUTION: *(SANE_Word*) value = opt->resolution; break; case OPT_SCAN_COLORMODE: strcpy(value, id_colormode_sane_name(opt->colormode_emul)); break; case OPT_SCAN_INTENT: strcpy(value, id_scanintent_sane_name(opt->scanintent)); break; case OPT_SCAN_SOURCE: strcpy(value, id_source_sane_name(opt->src)); break; case OPT_SCAN_TL_X: *(SANE_Fixed*) value = opt->tl_x; break; case OPT_SCAN_TL_Y: *(SANE_Fixed*) value = opt->tl_y; break; case OPT_SCAN_BR_X: *(SANE_Fixed*) value = opt->br_x; break; case OPT_SCAN_BR_Y: *(SANE_Fixed*) value = opt->br_y; break; case OPT_BRIGHTNESS: *(SANE_Fixed*) value = opt->brightness; break; case OPT_CONTRAST: *(SANE_Fixed*) value = opt->contrast; break; case OPT_SHADOW: *(SANE_Fixed*) value = opt->shadow; break; case OPT_HIGHLIGHT: *(SANE_Fixed*) value = opt->highlight; break; case OPT_GAMMA: *(SANE_Fixed*) value = opt->gamma; break; case OPT_NEGATIVE: *(SANE_Bool*)value = opt->negative; break; case OPT_JUSTIFICATION_X: s = id_justification_sane_name(opt->caps.justification_x); strcpy(value, s ? s : ""); break; case OPT_JUSTIFICATION_Y: s = id_justification_sane_name(opt->caps.justification_y); strcpy(value, s ? s : ""); break; default: status = SANE_STATUS_INVAL; } return status; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-discover.1000066400000000000000000000026171500411437100175650ustar00rootroot00000000000000.\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 .TH "AIRSCAN\-DISCOVER" "1" "February 2024" "" "SANE Scanner Access Now Easy" .SH "NAME" \fBairscan\-discover\fR \- Discover sane\-airscan compatible scanners .SH "SYNOPSIS" \fBairscan\-discover [options]\fR .SH "DESCRIPTION" \fBairscan\-discover\fR is a command\-line tool to find eSCL and WSD scanners on a local network .P It uses Avahi to discover DNS\-SD devices and its own implementation of WS\-Discovery to discover WSD devices\. .P On success, it outputs a fragment of sane\-airscan configuration file, that can be directly added to \fB/etc/sane\.d/airscan\.conf\fR .SH "OPTIONS" .TP \fB\-test\-fast\fR or \fB\-\-test\-fast\fR Fast discovery mode (see ane\-airscan(5) for details) .TP \fB\-test\-auto\fR or \fB\-\-test\-auto\fR Automatic protocol selection (see ane\-airscan(5) for details) .TP \fB\-d\fR Print debug messages to console .TP \fB\-t\fR Write a very detailed protocol trace to \fBairscan\-discover\-zeroconf\.log\fR and \fBairscan\-discover\-zeroconf\.tar\fR .TP \fB\-h\fR Print help screen .SH "FILES" .TP \fBairscan\-discover\-zeroconf\.log\fR Protocol trace .TP \fBairscan\-discover\-zeroconf\.tar\fR Non\-textual messages, if any, saved here\. Textual (i\.e\., XML) messages included directly into the \.log file .SH "SEE ALSO" \fBsane(7), sane\-airscan(5)\fR .SH "AUTHOR" Alexander Pevzner sane-airscan-0.99.35/airscan-discover.1.md000066400000000000000000000024031500411437100201550ustar00rootroot00000000000000airscan-discover -- Discover sane-airscan compatible scanners =================================================================== ## SYNOPSIS `airscan-discover [options]` ## DESCRIPTION `airscan-discover` is a command-line tool to find eSCL and WSD scanners on a local network It uses Avahi to discover DNS-SD devices and its own implementation of WS-Discovery to discover WSD devices. On success, it outputs a fragment of sane-airscan configuration file, that can be directly added to `/etc/sane.d/airscan.conf` ## OPTIONS * `-test-fast` or `--test-fast`: Fast discovery mode (see ane-airscan(5) for details) * `-test-auto` or `--test-auto`: Automatic protocol selection (see ane-airscan(5) for details) * `-d`: Print debug messages to console * `-t`: Write a very detailed protocol trace to `airscan-discover-zeroconf.log` and `airscan-discover-zeroconf.tar` * `-h`: Print help screen ## FILES * `airscan-discover-zeroconf.log`: Protocol trace * `airscan-discover-zeroconf.tar`: Non-textual messages, if any, saved here. Textual (i.e., XML) messages included directly into the .log file ## SEE ALSO **sane(7), sane-airscan(5)** ## AUTHOR Alexander Pevzner # vim:ts=8:sw=4:et sane-airscan-0.99.35/airscan-eloop.c000066400000000000000000000354241500411437100171510ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Event loop (runs in separate thread) */ #include "airscan.h" #include #include #include #include /******************** Constants *********************/ #define ELOOP_START_STOP_CALLBACKS_MAX 8 /******************** Static variables *********************/ static AvahiSimplePoll *eloop_poll; static pthread_t eloop_thread; static pthread_mutex_t eloop_mutex; static bool eloop_thread_running; static ll_head eloop_call_pending_list; static bool eloop_poll_restart; static __thread char eloop_estring[256]; static void (*eloop_start_stop_callbacks[ELOOP_START_STOP_CALLBACKS_MAX]) (bool); static int eloop_start_stop_callbacks_count; /******************** Standard errors *********************/ error ERROR_ENOMEM = (error) "Out of memory"; /******************** Forward declarations *********************/ static int eloop_poll_func (struct pollfd *ufds, unsigned int nfds, int timeout, void *p); static void eloop_call_execute (void); /* Initialize event loop */ SANE_Status eloop_init (void) { pthread_mutexattr_t attr; bool attr_initialized = false; bool mutex_initialized = false; SANE_Status status = SANE_STATUS_NO_MEM; ll_init(&eloop_call_pending_list); eloop_start_stop_callbacks_count = 0; /* Initialize eloop_mutex */ if (pthread_mutexattr_init(&attr)) { goto DONE; } attr_initialized = true; if (pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)) { goto DONE; } if (pthread_mutex_init(&eloop_mutex, &attr)) { goto DONE; } mutex_initialized = true; /* Create AvahiSimplePoll */ eloop_poll = avahi_simple_poll_new(); if (eloop_poll == NULL) { goto DONE; } avahi_simple_poll_set_func(eloop_poll, eloop_poll_func, NULL); /* Update status */ status = SANE_STATUS_GOOD; /* Cleanup and exit */ DONE: if (attr_initialized) { pthread_mutexattr_destroy(&attr); } if (status != SANE_STATUS_GOOD && mutex_initialized) { pthread_mutex_destroy(&eloop_mutex); } return status; } /* Cleanup event loop */ void eloop_cleanup (void) { if (eloop_poll != NULL) { avahi_simple_poll_free(eloop_poll); pthread_mutex_destroy(&eloop_mutex); eloop_poll = NULL; } } /* Add start/stop callback. This callback is called * on a event loop thread context, once when event * loop is started, and second time when it is stopped * * Start callbacks are called in the same order as * they were added. Stop callbacks are called in a * reverse order */ void eloop_add_start_stop_callback (void (*callback) (bool start)) { log_assert(NULL, eloop_start_stop_callbacks_count < ELOOP_START_STOP_CALLBACKS_MAX); eloop_start_stop_callbacks[eloop_start_stop_callbacks_count] = callback; eloop_start_stop_callbacks_count ++; } /* Poll function hook */ static int eloop_poll_func (struct pollfd *ufds, unsigned int nfds, int timeout, void *userdata) { int rc; (void) userdata; eloop_poll_restart = false; pthread_mutex_unlock(&eloop_mutex); rc = poll(ufds, nfds, timeout); pthread_mutex_lock(&eloop_mutex); /* Avahi multithreading support is semi-broken. Though new * AvahiWatch could be added from a context of any thread * (Avahi internal structures are properly interlocked, and * event loop thread is properly woken by poll->watch_new()), * Avahi internal indices are only rebuild in the beginning * of the avahi_simple_poll_iterate(), when avahi_simple_poll_prepare() * is called. So when Avahi returns from the poll() syscall * and calls avahi_simple_poll_dispatch(), it asserts, if new * AvahiWatch, which causes process to crash. * * To work around this crash, we force avahi_simple_poll_iterate() * to exit before calling of avahi_simple_poll_dispatch() by * returning error code from here. The subsequent call of * avahi_simple_poll_iterate() fixes the situation by calling * avahi_simple_poll_prepare() first. * * The returned error code cannot be EINTR, because interrupted * system call errors are ignored by Avahi (it simply restarts * the operation) and needs to be distinguished by a "normal" * errors, so event loop in the eloop_thread_func() will handle * it in appropriate manner. * * For now we use EBUSY error code for this purpose */ if (eloop_poll_restart) { /* We have to return an error other than EINTR to restart the avahi loop. */ errno = EBUSY; return -1; } return rc; } /* Event loop thread main function */ static void* eloop_thread_func (void *data) { int i; (void) data; pthread_mutex_lock(&eloop_mutex); for (i = 0; i < eloop_start_stop_callbacks_count; i ++) { eloop_start_stop_callbacks[i](true); } __atomic_store_n(&eloop_thread_running, true, __ATOMIC_SEQ_CST); do { eloop_call_execute(); i = avahi_simple_poll_iterate(eloop_poll, -1); } while (i == 0 || (i < 0 && (errno == EINTR || errno == EBUSY))); for (i = eloop_start_stop_callbacks_count - 1; i >= 0; i --) { eloop_start_stop_callbacks[i](false); } pthread_mutex_unlock(&eloop_mutex); return NULL; } /* Start event loop thread. */ void eloop_thread_start (void) { int rc; useconds_t usec = 100; rc = pthread_create(&eloop_thread, NULL, eloop_thread_func, NULL); if (rc != 0) { log_panic(NULL, "pthread_create: %s", strerror(rc)); } /* Wait until thread is started and all start callbacks are executed */ while (!__atomic_load_n(&eloop_thread_running, __ATOMIC_SEQ_CST)) { usleep(usec); usec += usec; } } /* Stop event loop thread and wait until its termination */ void eloop_thread_stop (void) { if (__atomic_load_n(&eloop_thread_running, __ATOMIC_SEQ_CST)) { avahi_simple_poll_quit(eloop_poll); pthread_join(eloop_thread, NULL); __atomic_store_n(&eloop_thread_running, false, __ATOMIC_SEQ_CST); } } /* Acquire event loop mutex */ void eloop_mutex_lock (void) { pthread_mutex_lock(&eloop_mutex); } /* Release event loop mutex */ void eloop_mutex_unlock (void) { pthread_mutex_unlock(&eloop_mutex); } /* Wait on conditional variable under the event loop mutex */ void eloop_cond_wait (pthread_cond_t *cond) { pthread_cond_wait(cond, &eloop_mutex); } /* Get AvahiPoll that runs in event loop thread */ const AvahiPoll* eloop_poll_get (void) { return avahi_simple_poll_get(eloop_poll); } /* eloop_call_pending represents a pending eloop_call */ typedef struct { void (*func)(void*); /* Function to be called */ void *data; /* It's argument */ uint64_t callid; /* For eloop_call_cancel() */ ll_node node; /* In eloop_call_pending_list */ } eloop_call_pending; /* Execute function calls deferred by eloop_call() */ static void eloop_call_execute (void) { ll_node *node; while ((node = ll_pop_beg(&eloop_call_pending_list)) != NULL) { eloop_call_pending *pending; pending = OUTER_STRUCT(node, eloop_call_pending, node); pending->func(pending->data); mem_free(pending); } } /* Call function on a context of event loop thread * The returned value can be supplied as a `callid' * parameter for the eloop_call_cancel() function */ uint64_t eloop_call (void (*func)(void*), void *data) { eloop_call_pending *p = mem_new(eloop_call_pending, 1); static uint64_t callid; uint64_t ret; p->func = func; p->data = data; pthread_mutex_lock(&eloop_mutex); ret = ++ callid; p->callid = ret; ll_push_end(&eloop_call_pending_list, &p->node); pthread_mutex_unlock(&eloop_mutex); avahi_simple_poll_wakeup(eloop_poll); return ret; } /* Cancel pending eloop_call * * This is safe to cancel already finished call (at this * case nothing will happen) */ void eloop_call_cancel (uint64_t callid) { ll_node *node; for (LL_FOR_EACH(node, &eloop_call_pending_list)) { eloop_call_pending *p = OUTER_STRUCT(node, eloop_call_pending, node); if (p->callid == callid) { ll_del(&p->node); mem_free(p); return; } } } /* Event notifier. Calls user-defined function on a context * of event loop thread, when event is triggered. This is * safe to trigger the event from a context of any thread * or even from a signal handler */ struct eloop_event { pollable *p; /* Underlying pollable event */ eloop_fdpoll *fdpoll; /* Underlying fdpoll */ void (*callback)(void*); /* user-defined callback */ void *data; /* callback's argument */ }; /* eloop_event eloop_fdpoll callback */ static void eloop_event_callback (int fd, void *data, ELOOP_FDPOLL_MASK mask) { eloop_event *event = data; (void) fd; (void) mask; pollable_reset(event->p); event->callback(event->data); } /* Create new event notifier. May return NULL */ eloop_event* eloop_event_new (void (*callback)(void *), void *data) { eloop_event *event; pollable *p; p = pollable_new(); if (p == NULL) { return NULL; } event = mem_new(eloop_event, 1); event->p = p; event->callback = callback; event->data = data; event->fdpoll = eloop_fdpoll_new(pollable_get_fd(p), eloop_event_callback, event); eloop_fdpoll_set_mask(event->fdpoll, ELOOP_FDPOLL_READ); return event; } /* Destroy event notifier */ void eloop_event_free (eloop_event *event) { eloop_fdpoll_free(event->fdpoll); pollable_free(event->p); mem_free(event); } /* Trigger an event */ void eloop_event_trigger (eloop_event *event) { pollable_signal(event->p); } /* Timer. Calls user-defined function after a specified * interval */ struct eloop_timer { AvahiTimeout *timeout; /* Underlying AvahiTimeout */ void (*callback)(void *); /* User callback */ void *data; /* User data */ }; /* eloop_timer callback for AvahiTimeout */ static void eloop_timer_callback (AvahiTimeout *t, void *data) { eloop_timer *timer = data; (void) t; timer->callback(timer->data); eloop_timer_cancel(timer); } /* Create new timer. Timeout is in milliseconds */ eloop_timer* eloop_timer_new (int timeout, void (*callback)(void *), void *data) { const AvahiPoll *poll = eloop_poll_get(); eloop_timer *timer = mem_new(eloop_timer, 1); struct timeval end; avahi_elapse_time(&end, timeout, 0); timer->timeout = poll->timeout_new(poll, &end, eloop_timer_callback, timer); timer->callback = callback; timer->data = data; return timer; } /* Cancel a timer * * Caller SHOULD NOT cancel expired timer (timer with called * callback) -- this is done automatically */ void eloop_timer_cancel (eloop_timer *timer) { const AvahiPoll *poll = eloop_poll_get(); poll->timeout_free(timer->timeout); mem_free(timer); } /* Convert ELOOP_FDPOLL_MASK to string. Used for logging. */ const char* eloop_fdpoll_mask_str (ELOOP_FDPOLL_MASK mask) { switch (mask & ELOOP_FDPOLL_BOTH) { case 0: return "{--}"; case ELOOP_FDPOLL_READ: return "{r-}"; case ELOOP_FDPOLL_WRITE: return "{-w}"; case ELOOP_FDPOLL_BOTH: return "{rw}"; } return "{??}"; /* Should never happen indeed */ } /* eloop_fdpoll notifies user when file becomes * readable, writable or both, depending on its * event mask */ struct eloop_fdpoll { AvahiWatch *watch; /* Underlying AvahiWatch */ int fd; /* Underlying file descriptor */ ELOOP_FDPOLL_MASK mask; /* Mask of active events */ void (*callback)( /* User-defined callback */ int, void*, ELOOP_FDPOLL_MASK); void *data; /* Callback's data */ }; /* eloop_fdpoll callback for AvahiWatch */ static void eloop_fdpoll_callback (AvahiWatch *w, int fd, AvahiWatchEvent event, void *data) { eloop_fdpoll *fdpoll = data; ELOOP_FDPOLL_MASK mask = 0; (void) w; if ((event & AVAHI_WATCH_IN) != 0) { mask |= ELOOP_FDPOLL_READ; } if ((event & AVAHI_WATCH_OUT) != 0) { mask |= ELOOP_FDPOLL_WRITE; } fdpoll->callback(fd, fdpoll->data, mask); } /* Create eloop_fdpoll * * Callback will be called, when file will be ready for read/write/both, * depending on mask * * Initial mask value is 0, and it can be changed, using * eloop_fdpoll_set_mask() function */ eloop_fdpoll* eloop_fdpoll_new (int fd, void (*callback) (int, void*, ELOOP_FDPOLL_MASK), void *data) { const AvahiPoll *poll = eloop_poll_get(); eloop_fdpoll *fdpoll = mem_new(eloop_fdpoll, 1); fdpoll->fd = fd; fdpoll->callback = callback; fdpoll->data = data; eloop_poll_restart = true; fdpoll->watch = poll->watch_new(poll, fd, 0, eloop_fdpoll_callback, fdpoll); return fdpoll; } /* Destroy eloop_fdpoll */ void eloop_fdpoll_free (eloop_fdpoll *fdpoll) { const AvahiPoll *poll = eloop_poll_get(); poll->watch_free(fdpoll->watch); mem_free(fdpoll); } /* Set eloop_fdpoll event mask. It returns a previous value of event mask */ ELOOP_FDPOLL_MASK eloop_fdpoll_set_mask (eloop_fdpoll *fdpoll, ELOOP_FDPOLL_MASK mask) { ELOOP_FDPOLL_MASK old_mask = fdpoll->mask; if (old_mask != mask) { const AvahiPoll *poll = eloop_poll_get(); AvahiWatchEvent events = 0; if ((mask & ELOOP_FDPOLL_READ) != 0) { events |= AVAHI_WATCH_IN; } if ((mask & ELOOP_FDPOLL_WRITE) != 0) { events |= AVAHI_WATCH_OUT; } fdpoll->mask = mask; poll->watch_update(fdpoll->watch, events); } return old_mask; } /* Format error string, as printf() does and save result * in the memory, owned by the event loop * * Caller should not free returned string. This is safe * to use the returned string as an argument to the * subsequent eloop_eprintf() call. * * The returned string remains valid until next call * to eloop_eprintf(), which makes it usable to * report errors up by the stack. However, it should * not be assumed, that the string will remain valid * on a next eloop roll, so don't save this string * anywhere, if you need to do so, create a copy! */ error eloop_eprintf(const char *fmt, ...) { va_list ap; char buf[sizeof(eloop_estring)]; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); strcpy(eloop_estring, buf); va_end(ap); return ERROR(eloop_estring); } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-escl.c000066400000000000000000001236521500411437100167620ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * ESCL protocol handler */ #include "airscan.h" /******************** Protocol constants ********************/ /* If HTTP 503 reply is received, how many retry attempts * to perform before giving up * * ESCL_RETRY_ATTEMPTS_LOAD - for NextDocument request * ESCL_RETRY_ATTEMPTS - for other requests * * Note, some printers (namely, HP LaserJet MFP M28w) require * a lot of retry attempts when loading next page at high res */ #define ESCL_RETRY_ATTEMPTS_LOAD 30 #define ESCL_RETRY_ATTEMPTS 10 /* And pause between retries, in milliseconds */ #define ESCL_RETRY_PAUSE 1000 /* Some devices (namely, Brother MFC-L2710DW) erroneously returns * HTTP 404 Not Found when scanning from ADF, if next LOAD request * send immediately after completion the previous one, and ScannerStatus * returns ScannerAdfEmpty at this case, which leads to premature * scan job termination with SANE_STATUS_NO_DOCS status * * Introducing a small delay between subsequent LOAD requests solves * this problem. * * To avoid performance regression on a very fast scanners, this * delay is limited to some fraction of the preceding LOAD * query time * * ESCL_NEXT_LOAD_DELAY - delay between LOAD requests, milliseconds * ESCL_NEXT_LOAD_DELAY_MAX - upper limit of this delay, as a fraction * of a previous LOAD time */ #define ESCL_NEXT_LOAD_DELAY 1000 #define ESCL_NEXT_LOAD_DELAY_MAX 0.5 /* proto_handler_escl represents eSCL protocol handler */ typedef struct { proto_handler proto; /* Base class */ /* Miscellaneous flags */ bool quirk_localhost; /* Set Host: localhost in ScanJobs rq */ bool quirk_canon_mf410_series; /* Canon MF410 Series */ bool quirk_port_in_host; /* Always set port in Host: header */ bool quirk_next_load_delay; /* Use ESCL_NEXT_LOAD_DELAY */ bool quirk_retry_on_404; /* Retry GET NextDocunemt on HTTP 404 */ bool quirk_retry_on_410; /* Retry GET NextDocunemt on HTTP 410 */ bool quirk_broken_ipv6_location; /* Invalid hostname in IPv6 Location: */ bool quirk_skip_cleanup; /* Don't cleanup after normal operations*/ } proto_handler_escl; /* XML namespace for XML writer */ static const xml_ns escl_xml_wr_ns[] = { {"pwg", "http://www.pwg.org/schemas/2010/12/sm"}, {"scan", "http://schemas.hp.com/imaging/escl/2011/05/03"}, {NULL, NULL} }; /******************** Miscellaneous types ********************/ /* escl_scanner_status represents decoded ScannerStatus response */ typedef struct { SANE_Status device_status; /* XXX */ SANE_Status adf_status; /* YYY */ } escl_scanner_status; /******************** Forward declarations ********************/ static error escl_parse_scanner_status (const proto_ctx *ctx, const char *xml_text, size_t xml_len, escl_scanner_status *out); /******************** HTTP utility functions ********************/ /* Create HTTP query */ static http_query* escl_http_query (const proto_ctx *ctx, const char *path, const char *method, char *body) { proto_handler_escl *escl = (proto_handler_escl*) ctx->proto; http_query *query = http_query_new_relative(ctx->http, ctx->base_uri, path, method, body, "text/xml"); if (escl->quirk_port_in_host) { http_query_force_port(query, true); } return query; } /* Create HTTP get query */ static http_query* escl_http_get (const proto_ctx *ctx, const char *path) { return escl_http_query(ctx, path, "GET", NULL); } /******************** Device Capabilities ********************/ /* Parse color modes */ static error escl_devcaps_source_parse_color_modes (xml_rd *xml, devcaps_source *src) { src->colormodes = 0; xml_rd_enter(xml); for (; !xml_rd_end(xml); xml_rd_next(xml)) { if(xml_rd_node_name_match(xml, "scan:ColorMode")) { const char *v = xml_rd_node_value(xml); if (!strcmp(v, "BlackAndWhite1")) { src->colormodes |= 1 << ID_COLORMODE_BW1; } else if (!strcmp(v, "Grayscale8")) { src->colormodes |= 1 << ID_COLORMODE_GRAYSCALE; } else if (!strcmp(v, "RGB24")) { src->colormodes |= 1 << ID_COLORMODE_COLOR; } } } xml_rd_leave(xml); return NULL; } /* Parse document formats */ static error escl_devcaps_source_parse_document_formats (xml_rd *xml, devcaps_source *src) { xml_rd_enter(xml); for (; !xml_rd_end(xml); xml_rd_next(xml)) { unsigned int flags = 0; if(xml_rd_node_name_match(xml, "pwg:DocumentFormat")) { flags |= DEVCAPS_SOURCE_PWG_DOCFMT; } if(xml_rd_node_name_match(xml, "scan:DocumentFormatExt")) { flags |= DEVCAPS_SOURCE_SCAN_DOCFMT_EXT; } if (flags != 0) { const char *v = xml_rd_node_value(xml); ID_FORMAT fmt = id_format_by_mime_name(v); if (fmt != ID_FORMAT_UNKNOWN) { src->formats |= 1 << fmt; src->flags |= flags; } } } xml_rd_leave(xml); return NULL; } /* Parse discrete resolutions. */ static error escl_devcaps_source_parse_discrete_resolutions (xml_rd *xml, devcaps_source *src) { error err = NULL; sane_word_array_reset(&src->resolutions); xml_rd_enter(xml); for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) { if (xml_rd_node_name_match(xml, "scan:DiscreteResolution")) { SANE_Word x = 0, y = 0; xml_rd_enter(xml); for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) { if (xml_rd_node_name_match(xml, "scan:XResolution")) { err = xml_rd_node_value_uint(xml, &x); } else if (xml_rd_node_name_match(xml, "scan:YResolution")) { err = xml_rd_node_value_uint(xml, &y); } } xml_rd_leave(xml); if (x && y && x == y) { src->resolutions = sane_word_array_append(src->resolutions, x); } } } xml_rd_leave(xml); if (sane_word_array_len(src->resolutions) > 0) { src->flags |= DEVCAPS_SOURCE_RES_DISCRETE; sane_word_array_sort(src->resolutions); } return err; } /* Parse resolutions range */ static error escl_devcaps_source_parse_resolutions_range (xml_rd *xml, devcaps_source *src) { error err = NULL; SANE_Range range_x = {0, 0, 0}, range_y = {0, 0, 0}; xml_rd_enter(xml); for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) { SANE_Range *range = NULL; if (xml_rd_node_name_match(xml, "scan:XResolution")) { range = &range_x; } else if (xml_rd_node_name_match(xml, "scan:XResolution")) { range = &range_y; } if (range != NULL) { xml_rd_enter(xml); for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) { if (xml_rd_node_name_match(xml, "scan:Min")) { err = xml_rd_node_value_uint(xml, &range->min); } else if (xml_rd_node_name_match(xml, "scan:Max")) { err = xml_rd_node_value_uint(xml, &range->max); } else if (xml_rd_node_name_match(xml, "scan:Step")) { err = xml_rd_node_value_uint(xml, &range->quant); } } xml_rd_leave(xml); } } xml_rd_leave(xml); if (range_x.min > range_x.max) { err = ERROR("Invalid scan:XResolution range"); goto DONE; } if (range_y.min > range_y.max) { err = ERROR("Invalid scan:YResolution range"); goto DONE; } /* If no quantization value, SANE uses 0, not 1 */ if (range_x.quant == 1) { range_x.quant = 0; } if (range_y.quant == 1) { range_y.quant = 0; } /* Try to merge x/y ranges */ if (!math_range_merge(&src->res_range, &range_x, &range_y)) { err = ERROR("Incompatible scan:XResolution and " "scan:YResolution ranges"); goto DONE; } src->flags |= DEVCAPS_SOURCE_RES_RANGE; DONE: return err; } /* Parse supported resolutions. */ static error escl_devcaps_source_parse_resolutions (xml_rd *xml, devcaps_source *src) { error err = NULL; xml_rd_enter(xml); for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) { if (xml_rd_node_name_match(xml, "scan:DiscreteResolutions")) { err = escl_devcaps_source_parse_discrete_resolutions(xml, src); } else if (xml_rd_node_name_match(xml, "scan:ResolutionRange")) { err = escl_devcaps_source_parse_resolutions_range(xml, src); } } xml_rd_leave(xml); /* Prefer discrete resolution, if both are provided */ if (src->flags & DEVCAPS_SOURCE_RES_DISCRETE) { src->flags &= ~DEVCAPS_SOURCE_RES_RANGE; } return err; } /* Parse setting profiles (color modes, document formats etc). */ static error escl_devcaps_source_parse_setting_profiles (xml_rd *xml, devcaps_source *src) { error err = NULL; /* Parse setting profiles */ xml_rd_enter(xml); for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) { if (xml_rd_node_name_match(xml, "scan:SettingProfile")) { xml_rd_enter(xml); for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) { if (xml_rd_node_name_match(xml, "scan:ColorModes")) { err = escl_devcaps_source_parse_color_modes(xml, src); } else if (xml_rd_node_name_match(xml, "scan:DocumentFormats")) { err = escl_devcaps_source_parse_document_formats(xml, src); } else if (xml_rd_node_name_match(xml, "scan:SupportedResolutions")) { err = escl_devcaps_source_parse_resolutions(xml, src); } } xml_rd_leave(xml); } } xml_rd_leave(xml); /* Validate results */ if (err == NULL) { src->colormodes &= DEVCAPS_COLORMODES_SUPPORTED; if (src->colormodes == 0) { return ERROR("no color modes detected"); } src->formats &= DEVCAPS_FORMATS_SUPPORTED; if (src->formats == 0) { return ERROR("no image formats detected"); } if (!(src->flags & (DEVCAPS_SOURCE_RES_DISCRETE| DEVCAPS_SOURCE_RES_RANGE))){ return ERROR("scan resolutions are not defined"); } } return err; } /* Parse supported intents (photo/document etc). */ static error escl_devcaps_source_parse_supported_intents (xml_rd *xml, devcaps_source *src) { error err = NULL; xml_rd_enter(xml); for (; !xml_rd_end(xml); xml_rd_next(xml)) { if(xml_rd_node_name_match(xml, "scan:SupportedIntent") || xml_rd_node_name_match(xml, "scan:Intent")) { const char *v = xml_rd_node_value(xml); if (!strcmp(v, "Document")) { src->scanintents |= 1 << ID_SCANINTENT_DOCUMENT; } else if (!strcmp(v, "TextAndGraphic")) { src->scanintents |= 1 << ID_SCANINTENT_TEXTANDGRAPHIC; } else if (!strcmp(v, "Photo")) { src->scanintents |= 1 << ID_SCANINTENT_PHOTO; } else if (!strcmp(v, "Preview")) { src->scanintents |= 1 << ID_SCANINTENT_PREVIEW; } else if (!strcmp(v, "Object")) { src->scanintents |= 1 << ID_SCANINTENT_OBJECT; } else if (!strcmp(v, "BusinessCard")) { src->scanintents |= 1 << ID_SCANINTENT_BUSINESSCARD; } else { log_debug(NULL, "unknown intent: %s", v); } } } xml_rd_leave(xml); return err; } /* Parse ADF justification */ static void escl_devcaps_parse_justification (xml_rd *xml, ID_JUSTIFICATION *x, ID_JUSTIFICATION *y) { xml_rd_enter(xml); *x = *y = ID_JUSTIFICATION_UNKNOWN; for (; !xml_rd_end(xml); xml_rd_next(xml)) { if(xml_rd_node_name_match(xml, "pwg:XImagePosition")){ const char *v = xml_rd_node_value(xml); if (!strcmp(v, "Right")){ *x = ID_JUSTIFICATION_RIGHT; } else if (!strcmp(v, "Center")) { *x = ID_JUSTIFICATION_CENTER; } else if (!strcmp(v, "Left")) { *x = ID_JUSTIFICATION_LEFT; } } else if(xml_rd_node_name_match(xml, "pwg:YImagePosition")){ const char *v = xml_rd_node_value(xml); if (!strcmp(v, "Top")){ *y = ID_JUSTIFICATION_TOP; } else if (!strcmp(v, "Center")) { *y = ID_JUSTIFICATION_CENTER; } else if (!strcmp(v, "Bottom")) { *y = ID_JUSTIFICATION_BOTTOM; } } } xml_rd_leave(xml); } /* Parse source capabilities. Returns NULL on success, error string otherwise */ static error escl_devcaps_source_parse (xml_rd *xml, devcaps_source **out) { devcaps_source *src = devcaps_source_new(); error err = NULL; xml_rd_enter(xml); for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) { if(xml_rd_node_name_match(xml, "scan:MinWidth")) { err = xml_rd_node_value_uint(xml, &src->min_wid_px); } else if (xml_rd_node_name_match(xml, "scan:MaxWidth")) { err = xml_rd_node_value_uint(xml, &src->max_wid_px); } else if (xml_rd_node_name_match(xml, "scan:MinHeight")) { err = xml_rd_node_value_uint(xml, &src->min_hei_px); } else if (xml_rd_node_name_match(xml, "scan:MaxHeight")) { err = xml_rd_node_value_uint(xml, &src->max_hei_px); } else if (xml_rd_node_name_match(xml, "scan:SettingProfiles")) { err = escl_devcaps_source_parse_setting_profiles(xml, src); } else if (xml_rd_node_name_match(xml, "scan:SupportedIntents")) { err = escl_devcaps_source_parse_supported_intents(xml, src); } } xml_rd_leave(xml); if (err != NULL) { goto DONE; } if (src->max_wid_px != 0 && src->max_hei_px != 0) { /* Validate window size */ 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) { err = ERROR("Invalid scan:MinHeight or scan:MaxHeight"); goto DONE; } src->flags |= DEVCAPS_SOURCE_HAS_SIZE; /* Set window ranges */ src->win_x_range_mm.min = src->win_y_range_mm.min = 0; src->win_x_range_mm.max = math_px2mm_res(src->max_wid_px, 300); src->win_y_range_mm.max = math_px2mm_res(src->max_hei_px, 300); } DONE: if (err != NULL) { devcaps_source_free(src); } else { if (*out == NULL) { *out = src; } else { /* Duplicate detected. Ignored for now */ devcaps_source_free(src); } } return err; } /* Parse compression factor parameters */ static error escl_devcaps_compression_parse (xml_rd *xml, devcaps *caps) { for (; !xml_rd_end(xml); xml_rd_next(xml)) { error err = NULL; if (xml_rd_node_name_match(xml, "scan:Min")) { err = xml_rd_node_value_uint(xml, &caps->compression_range.min); } else if (xml_rd_node_name_match(xml, "scan:Max")) { err = xml_rd_node_value_uint(xml, &caps->compression_range.max); } else if (xml_rd_node_name_match(xml, "scan:Step")) { err = xml_rd_node_value_uint(xml, &caps->compression_range.quant); } else if (xml_rd_node_name_match(xml, "scan:Normal")) { err = xml_rd_node_value_uint(xml, &caps->compression_norm); } if (err != NULL) { return err; } } /* Validate obtained parameters. * * Note, errors are silently ignored starting from this point */ if (caps->compression_range.min > caps->compression_range.max) { return NULL; } if (caps->compression_norm < caps->compression_range.min || caps->compression_norm > caps->compression_range.max) { return NULL; } caps->compression_ok = true; return NULL; } /* Parse device capabilities. devcaps structure must be initialized * before calling this function. */ static error escl_devcaps_parse (proto_handler_escl *escl, devcaps *caps, const char *xml_text, size_t xml_len) { error err = NULL; xml_rd *xml; bool quirk_canon_iR2625_2630 = false; ID_SOURCE id_src; bool src_ok = false; /* Fill "constant" part of device capabilities */ caps->units = 300; caps->protocol = escl->proto.name; caps->justification_x = caps->justification_y = ID_JUSTIFICATION_UNKNOWN; /* Parse capabilities XML */ err = xml_rd_begin(&xml, xml_text, xml_len, NULL); if (err != NULL) { goto DONE; } if (!xml_rd_node_name_match(xml, "scan:ScannerCapabilities")) { err = ERROR("XML: missed scan:ScannerCapabilities"); goto DONE; } xml_rd_enter(xml); for (; !xml_rd_end(xml); xml_rd_next(xml)) { if (xml_rd_node_name_match(xml, "pwg:MakeAndModel")) { const char *m = xml_rd_node_value(xml); if (!strcmp(m, "Canon iR2625/2630")) { quirk_canon_iR2625_2630 = true; } else if (!strcmp(m, "HP LaserJet MFP M630")) { escl->quirk_localhost = true; } else if (!strcmp(m, "HP Color LaserJet FlowMFP M578")) { escl->quirk_localhost = true; } else if (!strcmp(m, "MF410 Series")) { escl->quirk_canon_mf410_series = true; } else if (!strncasecmp(m, "EPSON ", 6)) { escl->quirk_port_in_host = true; } else if (!strncasecmp(m, "Brother ", 8)) { escl->quirk_next_load_delay = true; } else if (!strcmp(m, "B205") || !strcmp(m, "B215")) { /* Xerox B205/B215 machines may indicate temporary * unavailability of the scanned document (the need * for retry) using HTTP 404/410 codes. Normally, * HTTP 503 used for this purpose... */ escl->quirk_retry_on_404 = true; escl->quirk_retry_on_410 = true; escl->quirk_broken_ipv6_location = true; escl->quirk_skip_cleanup = true; } } else if (xml_rd_node_name_match(xml, "scan:Manufacturer")) { const char *m = xml_rd_node_value(xml); if (!strcasecmp(m, "EPSON")) { escl->quirk_port_in_host = true; } } else if (xml_rd_node_name_match(xml, "scan:Platen")) { xml_rd_enter(xml); if (xml_rd_node_name_match(xml, "scan:PlatenInputCaps")) { err = escl_devcaps_source_parse(xml, &caps->src[ID_SOURCE_PLATEN]); } xml_rd_leave(xml); } else if (xml_rd_node_name_match(xml, "scan:Adf")) { xml_rd_enter(xml); while (!xml_rd_end(xml)) { if (xml_rd_node_name_match(xml, "scan:AdfSimplexInputCaps")) { err = escl_devcaps_source_parse(xml, &caps->src[ID_SOURCE_ADF_SIMPLEX]); } else if (xml_rd_node_name_match(xml, "scan:AdfDuplexInputCaps")) { err = escl_devcaps_source_parse(xml, &caps->src[ID_SOURCE_ADF_DUPLEX]); } else if (xml_rd_node_name_match(xml, "scan:Justification")) { escl_devcaps_parse_justification(xml, &caps->justification_x, &caps->justification_y); } xml_rd_next(xml); } xml_rd_leave(xml); } else if (xml_rd_node_name_match(xml, "scan:CompressionFactorSupport")) { xml_rd_enter(xml); err = escl_devcaps_compression_parse(xml, caps); xml_rd_leave(xml); } if (err != NULL) { goto DONE; } } /* Check that we have at least one source */ for (id_src = (ID_SOURCE) 0; id_src < NUM_ID_SOURCE; id_src ++) { if (caps->src[id_src] != NULL) { src_ok = true; } } if (!src_ok) { err = ERROR("XML: neither platen nor ADF sources detected"); goto DONE; } /* Apply quirks, if any */ if (quirk_canon_iR2625_2630) { /* This device announces resolutions up to 600 DPI, * but actually doesn't support more that 300 * * https://oip.manual.canon/USRMA-4209-zz-CSL-2600-enUV/contents/devu-apdx-sys_spec-send.html?search=600 * * See #57 for details */ for (id_src = (ID_SOURCE) 0; id_src < NUM_ID_SOURCE; id_src ++) { devcaps_source *src = caps->src[id_src]; if (src != NULL && /* paranoia: array won't be empty after quirk applied */ sane_word_array_len(src->resolutions) > 0 && src->resolutions[1] <= 300) { sane_word_array_bound(src->resolutions, 0, 300); } } } DONE: if (err != NULL) { devcaps_reset(caps); } xml_rd_finish(&xml); return err; } /* Query device capabilities */ static http_query* escl_devcaps_query (const proto_ctx *ctx) { return escl_http_get(ctx, "ScannerCapabilities"); } /* Decode device capabilities */ static error escl_devcaps_decode (const proto_ctx *ctx, devcaps *caps) { proto_handler_escl *escl = (proto_handler_escl*) ctx->proto; http_data *data = http_query_get_response_data(ctx->query); const char *s; /* Most of devices that have Server: HP_Compact_Server * in their HTTP response header, require this quirk * (see #116) */ s = http_query_get_response_header(ctx->query, "server"); if (s != NULL && !strcmp(s, "HP_Compact_Server")) { escl->quirk_localhost = true; } return escl_devcaps_parse(escl, caps, data->bytes, data->size); } /* Create pre-scan check query */ static http_query* escl_precheck_query (const proto_ctx *ctx) { return escl_http_get(ctx, "ScannerStatus"); } /* Decode pre-scan check query results */ static proto_result escl_precheck_decode (const proto_ctx *ctx) { proto_handler_escl *escl = (proto_handler_escl*) ctx->proto; proto_result result = {0}; error err = NULL; escl_scanner_status sts; bool adf = ctx->params.src == ID_SOURCE_ADF_SIMPLEX || ctx->params.src == ID_SOURCE_ADF_DUPLEX; /* Initialize result to something optimistic :-) */ result.status = SANE_STATUS_GOOD; result.next = PROTO_OP_SCAN; /* Decode status */ err = http_query_error(ctx->query); if (err == NULL) { http_data *data = http_query_get_response_data(ctx->query); err = escl_parse_scanner_status(ctx, data->bytes, data->size, &sts); } if (err != NULL) { result.err = err; result.status = SANE_STATUS_IO_ERROR; result.next = PROTO_OP_FINISH; return result; } /* Note, the pre-check status is not always reliable, so normally * we ignore it. However, with Canon MF410 Series attempt to * scan from empty ADF causes ADF jam error (really, physical!), * so we must take care */ if (escl->quirk_canon_mf410_series) { if (adf) { switch (sts.adf_status) { case SANE_STATUS_JAMMED: case SANE_STATUS_NO_DOCS: result.status = sts.adf_status; result.next = PROTO_OP_FINISH; default: break; } } } return result; } /* Fix Location: URL * * It replaces host part of uri with the host part from `orig_uri` and * used for two purposes: * 1) To fix URL in the Location: header. We don't trust the device * and only allow it to specify path, not host (host is preserved * from the `orig_uri` -- the URL used to initiate scanning * 2) As redirection callback, together with the quirk_localhost. * At this case, Host: is purposely invalid, which makes * redirection unreliable (most likely, redirection will * use "localhost" from the Host: header as the host part * of URL). * * Can be directly used as http_query_onredir() callback */ static void escl_scan_fix_location (void *p, http_uri *uri, const http_uri *orig_uri) { (void) p; http_uri_fix_host(uri, orig_uri, "localhost"); } /* Initiate scanning */ static http_query* escl_scan_query (const proto_ctx *ctx) { proto_handler_escl *escl = (proto_handler_escl*) ctx->proto; const proto_scan_params *params = &ctx->params; const char *source = NULL; const char *colormode = NULL; const char *scanintent = NULL; const char *mime = id_format_mime_name(ctx->params.format); const devcaps_source *src = ctx->devcaps->src[params->src]; bool duplex = false; http_query *query; /* Prepare parameters */ switch (params->src) { case ID_SOURCE_PLATEN: source = "Platen"; duplex = false; break; case ID_SOURCE_ADF_SIMPLEX: source = "Feeder"; duplex = false; break; case ID_SOURCE_ADF_DUPLEX: source = "Feeder"; duplex = true; break; default: log_internal_error(ctx->log); } switch (params->colormode) { case ID_COLORMODE_COLOR: colormode = "RGB24"; break; case ID_COLORMODE_GRAYSCALE: colormode = "Grayscale8"; break; case ID_COLORMODE_BW1: colormode = "BlackAndWhite1"; break; default: log_internal_error(ctx->log); } switch (params->scanintent) { case ID_SCANINTENT_UNSET: break; case ID_SCANINTENT_DOCUMENT: scanintent = "Document"; break; case ID_SCANINTENT_TEXTANDGRAPHIC: scanintent = "TextAndGraphic"; break; case ID_SCANINTENT_PHOTO: scanintent = "Photo"; break; case ID_SCANINTENT_PREVIEW: scanintent = "Preview"; break; case ID_SCANINTENT_OBJECT: scanintent = "Object"; break; case ID_SCANINTENT_BUSINESSCARD: scanintent = "BusinessCard"; break; default: log_internal_error(ctx->log); } /* Build scan request */ xml_wr *xml = xml_wr_begin("scan:ScanSettings", escl_xml_wr_ns); xml_wr_add_text(xml, "pwg:Version", "2.0"); if (scanintent) { xml_wr_add_text(xml, "scan:Intent", scanintent); } xml_wr_enter(xml, "pwg:ScanRegions"); xml_wr_enter(xml, "pwg:ScanRegion"); xml_wr_add_text(xml, "pwg:ContentRegionUnits", "escl:ThreeHundredthsOfInches"); xml_wr_add_uint(xml, "pwg:XOffset", params->x_off); xml_wr_add_uint(xml, "pwg:YOffset", params->y_off); xml_wr_add_uint(xml, "pwg:Width", params->wid); xml_wr_add_uint(xml, "pwg:Height", params->hei); xml_wr_leave(xml); /* pwg:ScanRegion */ xml_wr_leave(xml); /* pwg:ScanRegions */ //xml_wr_add_text(xml, "scan:InputSource", source); xml_wr_add_text(xml, "pwg:InputSource", source); if (ctx->devcaps->compression_ok) { xml_wr_add_uint(xml, "scan:CompressionFactor", ctx->devcaps->compression_norm); } xml_wr_add_text(xml, "scan:ColorMode", colormode); xml_wr_add_text(xml, "pwg:DocumentFormat", mime); if ((src->flags & DEVCAPS_SOURCE_SCAN_DOCFMT_EXT) != 0) { xml_wr_add_text(xml, "scan:DocumentFormatExt", mime); } xml_wr_add_uint(xml, "scan:XResolution", params->x_res); xml_wr_add_uint(xml, "scan:YResolution", params->y_res); if (params->src != ID_SOURCE_PLATEN) { xml_wr_add_bool(xml, "scan:Duplex", duplex); } /* Send request to device */ query = escl_http_query(ctx, "ScanJobs", "POST", xml_wr_finish_compact(xml)); /* Kyocera ECOSYS M6526cdn drops TLS connection after sending * response HTTP headers, but before the body transfer is completed. * * As for this request we are only interested in the response * headers, we can ignore this kind of error * * See here for details: * https://github.com/alexpevzner/sane-airscan/issues/163 */ http_query_no_need_response_body(query); /* It's a dirty hack * * HP LaserJet MFP M630, HP Color LaserJet FlowMFP M578 and * probably some other HP devices don't allow eSCL scan, unless * Host is set to "localhost". It is probably bad and naive attempt * to enforce some access security. * * So here we forcibly set Host to "localhost". * * Note, this hack doesn't work with some other printers * see #92, #98 for details */ if (escl->quirk_localhost && !http_uri_is_loopback(ctx->base_uri)) { http_query_set_request_header(query, "Host", "localhost"); http_query_onredir(query, escl_scan_fix_location); } return query; } /* Decode result of scan request */ static proto_result escl_scan_decode (const proto_ctx *ctx) { proto_handler_escl *escl = (proto_handler_escl*) ctx->proto; proto_result result = {0}; error err = NULL; const char *location; http_uri *uri; /* Check HTTP status */ if (http_query_status(ctx->query) != HTTP_STATUS_CREATED) { err = eloop_eprintf("ScanJobs request: unexpected HTTP status %d", http_query_status(ctx->query)); result.next = PROTO_OP_CHECK; result.err = err; return result; } /* Obtain location */ location = http_query_get_response_header(ctx->query, "Location"); if (location == NULL || *location == '\0') { err = eloop_eprintf("ScanJobs request: empty location received"); goto ERROR; } /* Fix broken IPv6 location */ if (escl->quirk_broken_ipv6_location) { /* Xerox B205/B215 return Location that looks like this: * * Location: http://[fe80/eSCL/ScanJobs/4060 * * Note unbalanced square bracket and truncated IPv6 address. * As we anyway ignore Location: hostname, it is easy to fix... */ if (str_has_prefix(location, "http://[") || str_has_prefix(location, "https://[")) { if (strchr(location, ']') == NULL) { const char *s = strstr(location, "://"); s += 3; while (*s && *s != '/') { s ++; } if (*s == '/') { /* Skip URL until /path/... */ log_debug(ctx->log, "Broken IPv6 Location: %s", location); location = s; log_debug(ctx->log, "Fixed Location: %s", location); } } } } /* Validate and save location */ uri = http_uri_new_relative(ctx->base_uri, location, true, false); if (uri == NULL) { err = eloop_eprintf("ScanJobs request: invalid location received"); goto ERROR; } /* Don't trust hostname in Location, replace it with hostname * from the ctx->query (which represents scan request) * * The initial idea behind this approach is that scanner may * not have a strong knowledge of its own host name so hostname * supplied by device may be inaccurate. * * At least one device, HP Deskjet 3520 series, has demonstrated * this behavior when scanning via IPP over USB. Instead of (expected) * localhost host name, it returns CN26F178X605SZ.03f011b0.hpLedmUSB */ http_uri_fix_host(uri, http_query_uri(ctx->query), NULL); result.data.location = str_dup(http_uri_str(uri)); http_uri_free(uri); result.next = PROTO_OP_LOAD; return result; ERROR: result.next = PROTO_OP_FINISH; result.status = SANE_STATUS_IO_ERROR; result.err = err; return result; } /* Initiate image downloading */ static http_query* escl_load_query (const proto_ctx *ctx) { char *url, *sep; http_query *q; sep = str_has_suffix(ctx->location, "/") ? "" : "/"; url = str_concat(ctx->location, sep, "NextDocument", NULL); q = escl_http_get(ctx, url); mem_free(url); return q; } /* Decode result of image request */ static proto_result escl_load_decode (const proto_ctx *ctx) { proto_handler_escl *escl = (proto_handler_escl*) ctx->proto; proto_result result = {0}; error err = NULL; timestamp t = 0; /* Check HTTP status */ err = http_query_error(ctx->query); if (err != NULL) { if (ctx->params.src == ID_SOURCE_PLATEN && ctx->images_received > 0) { result.next = PROTO_OP_CLEANUP; if (escl->quirk_skip_cleanup) { result.next = PROTO_OP_FINISH; } } else { result.next = PROTO_OP_CHECK; result.err = eloop_eprintf("HTTP: %s", ESTRING(err)); } return result; } /* Compute delay until next load */ if (escl->quirk_next_load_delay && ctx->params.src != ID_SOURCE_PLATEN) { t = timestamp_now() - http_query_timestamp(ctx->query); t *= ESCL_NEXT_LOAD_DELAY_MAX; if (t > ESCL_NEXT_LOAD_DELAY) { t = ESCL_NEXT_LOAD_DELAY; } } /* Fill proto_result */ result.next = PROTO_OP_LOAD; result.delay = (int) t; result.data.image = http_data_ref(http_query_get_response_data(ctx->query)); return result; } /* Request device status */ static http_query* escl_status_query (const proto_ctx *ctx) { return escl_http_get(ctx, "ScannerStatus"); } /* Parse ScannerStatus response. * * Returned SANE_STATUS_UNSUPPORTED means status not understood */ static error escl_parse_scanner_status (const proto_ctx *ctx, const char *xml_text, size_t xml_len, escl_scanner_status *out) { error err = NULL; xml_rd *xml; const char *opname = proto_op_name(ctx->op); escl_scanner_status sts = {SANE_STATUS_UNSUPPORTED, SANE_STATUS_UNSUPPORTED}; /* Decode XML */ err = xml_rd_begin(&xml, xml_text, xml_len, NULL); if (err != NULL) { goto DONE; } if (!xml_rd_node_name_match(xml, "scan:ScannerStatus")) { err = ERROR("XML: missed scan:ScannerStatus"); goto DONE; } xml_rd_enter(xml); for (; !xml_rd_end(xml); xml_rd_next(xml)) { if (xml_rd_node_name_match(xml, "pwg:State")) { const char *state = xml_rd_node_value(xml); if (!strcmp(state, "Idle")) { sts.device_status = SANE_STATUS_GOOD; } else if (!strcmp(state, "Processing")) { sts.device_status = SANE_STATUS_DEVICE_BUSY; } else if (!strcmp(state, "Testing")) { /* HP LaserJet MFP M630 warm up */ sts.device_status = SANE_STATUS_DEVICE_BUSY; } else { sts.device_status = SANE_STATUS_UNSUPPORTED; } } else if (xml_rd_node_name_match(xml, "scan:AdfState")) { const char *state = xml_rd_node_value(xml); if (!strcmp(state, "ScannerAdfLoaded")) { sts.adf_status = SANE_STATUS_GOOD; } else if (!strcmp(state, "ScannerAdfJam")) { sts.adf_status = SANE_STATUS_JAMMED; } else if (!strcmp(state, "ScannerAdfDoorOpen")) { sts.adf_status = SANE_STATUS_COVER_OPEN; } else if (!strcmp(state, "ScannerAdfProcessing")) { /* Kyocera version */ sts.adf_status = SANE_STATUS_NO_DOCS; } else if (!strcmp(state, "ScannerAdfEmpty")) { /* Cannon TR4500, EPSON XP-7100 */ sts.adf_status = SANE_STATUS_NO_DOCS; } else { sts.adf_status = SANE_STATUS_UNSUPPORTED; } } } DONE: xml_rd_finish(&xml); if (err != NULL) { log_debug(ctx->log, "%s: %s", opname, ESTRING(err)); } else { log_debug(ctx->log, "%s: device status: %s", opname, sane_strstatus(sts.device_status)); log_debug(ctx->log, "%s: ADF status: %s", opname, sane_strstatus(sts.adf_status)); } *out = sts; return err; } /* Parse ScannerStatus response. * * Returned SANE_STATUS_UNSUPPORTED means status not understood */ static SANE_Status escl_decode_scanner_status (const proto_ctx *ctx, const char *xml_text, size_t xml_len) { escl_scanner_status sts; error err; SANE_Status status; const char *opname = proto_op_name(ctx->op); err = escl_parse_scanner_status(ctx, xml_text, xml_len, &sts); if (err != NULL) { return SANE_STATUS_IO_ERROR; } /* Decode Job status */ if (ctx->params.src != ID_SOURCE_PLATEN && sts.adf_status != SANE_STATUS_GOOD && sts.adf_status != SANE_STATUS_UNSUPPORTED) { status = sts.adf_status; } else { status = sts.device_status; } log_debug(ctx->log, "%s: job status: %s", opname, sane_strstatus(status)); return status; } /* Decode result of device status request */ static proto_result escl_status_decode (const proto_ctx *ctx) { proto_handler_escl *escl = (proto_handler_escl*) ctx->proto; proto_result result = {0}; error err = NULL; SANE_Status status; int max_attempts; bool temporary = false; /* Decode status */ err = http_query_error(ctx->query); if (err != NULL) { status = SANE_STATUS_IO_ERROR; goto FAIL; } else { http_data *data = http_query_get_response_data(ctx->query); status = escl_decode_scanner_status(ctx, data->bytes, data->size); } /* Now it's time to make a decision */ max_attempts = ESCL_RETRY_ATTEMPTS; if (ctx->failed_op == PROTO_OP_LOAD) { max_attempts = ESCL_RETRY_ATTEMPTS_LOAD; } switch (ctx->failed_http_status) { case HTTP_STATUS_SERVICE_UNAVAILABLE: temporary = true; break; case HTTP_STATUS_NOT_FOUND: if (escl->quirk_retry_on_404) { temporary = true; } break; case HTTP_STATUS_GONE: if (escl->quirk_retry_on_410) { temporary = true; } break; } if (temporary && ctx->failed_attempt < max_attempts) { /* Note, some devices may return HTTP 503 error core, meaning * that it makes sense to come back after small delay * * So if status doesn't cleanly indicate any error, lets retry * several times */ bool retry = false; switch (status) { case SANE_STATUS_GOOD: case SANE_STATUS_UNSUPPORTED: case SANE_STATUS_DEVICE_BUSY: retry = true; break; case SANE_STATUS_NO_DOCS: /* For some devices SANE_STATUS_NO_DOCS is not * reliable, if we have reached SANE_STATUS_NO_DOCS * operation: HTTP 503 may mean "I'm temporary not * ready, please try again", while ADF sensor * raises "ADF empty" signal. * * So retry at this case */ if (ctx->failed_op == PROTO_OP_LOAD) { retry = true; } break; default: break; } if (retry) { result.next = ctx->failed_op; result.delay = ESCL_RETRY_PAUSE; return result; } } /* If status cannot be cleanly decoded, look to HTTP status */ if (status == SANE_STATUS_GOOD || status == SANE_STATUS_UNSUPPORTED) { status = SANE_STATUS_IO_ERROR; switch (ctx->failed_http_status) { case HTTP_STATUS_SERVICE_UNAVAILABLE: status = SANE_STATUS_DEVICE_BUSY; break; case HTTP_STATUS_NOT_FOUND: if (ctx->params.src != ID_SOURCE_PLATEN && ctx->failed_op == PROTO_OP_LOAD) { status = SANE_STATUS_NO_DOCS; } break; } } /* Fill the result */ FAIL: result.next = ctx->location ? PROTO_OP_CLEANUP : PROTO_OP_FINISH; if (escl->quirk_skip_cleanup) { result.next = PROTO_OP_FINISH; } result.status = status; result.err = err; return result; } /* Cancel scan in progress */ static http_query* escl_cancel_query (const proto_ctx *ctx) { return escl_http_query(ctx, ctx->location, "DELETE", NULL); } /******************** Test interfaces ********************/ /* Test interface: decode device capabilities */ static error escl_test_decode_devcaps (proto_handler *proto, const void *xml_text, size_t xms_size, devcaps *caps) { proto_handler_escl *escl = (proto_handler_escl*) proto; return escl_devcaps_parse(escl, caps, xml_text, xms_size); } /******************** Constructor/destructor ********************/ /* Free ESCL protocol handler */ static void escl_free (proto_handler *proto) { mem_free(proto); } /* proto_handler_escl_new creates new eSCL protocol handler */ proto_handler* proto_handler_escl_new (void) { proto_handler_escl *escl = mem_new(proto_handler_escl, 1); escl->proto.name = "eSCL"; escl->proto.free = escl_free; escl->proto.devcaps_query = escl_devcaps_query; escl->proto.devcaps_decode = escl_devcaps_decode; escl->proto.precheck_query = escl_precheck_query; escl->proto.precheck_decode = escl_precheck_decode; escl->proto.scan_query = escl_scan_query; escl->proto.scan_decode = escl_scan_decode; escl->proto.load_query = escl_load_query; escl->proto.load_decode = escl_load_decode; escl->proto.status_query = escl_status_query; escl->proto.status_decode = escl_status_decode; escl->proto.cleanup_query = escl_cancel_query; escl->proto.cancel_query = escl_cancel_query; escl->proto.test_decode_devcaps = escl_test_decode_devcaps; return &escl->proto; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-filter.c000066400000000000000000000100571500411437100173130ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Image filters */ #include "airscan.h" /******************** Table filter ********************/ /* Type filter_xlat represents translation table based filter */ typedef struct { filter base; /* Base class */ uint8_t table[256]; /* Transformation table */ } filter_xlat; /* Dump filter to the log */ static void filter_xlat_dump (filter *f, log_ctx *log) { filter_xlat *filt = (filter_xlat*) f; size_t i; log_debug(log, " XLAT filter:"); for (i = 0; i < 256; i += 16) { uint8_t *row = filt->table + i; log_debug(log, " " "%.2x %.2x %.2x %.2x %.2x %.2x %.2x %.2x " "%.2x %.2x %.2x %.2x %.2x %.2x %.2x %.2x", row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7], row[8], row[9], row[10], row[11], row[12], row[13], row[14], row[15]); } } /* Apply filter to the image line */ static void filter_xlat_apply (filter *f, uint8_t *line, size_t size) { filter_xlat *filt = (filter_xlat*) f; size_t i; for (i = 0; i < size; i ++) { line[i] = filt->table[line[i]]; } } /* filter_xlat */ static filter* filter_xlat_new (const devopt *opt) { filter_xlat *filt; int i; double B = SANE_UNFIX(opt->brightness) / 200.0; double C = SANE_UNFIX(opt->contrast) / 100.0 + 1.0; double G = SANE_UNFIX(opt->gamma); uint8_t shadow = round(2.55 * SANE_UNFIX(opt->shadow)); uint8_t highlight = round(2.55 * SANE_UNFIX(opt->highlight)); if (opt->brightness == SANE_FIX(0.0) && opt->contrast == SANE_FIX(0.0) && opt->shadow == SANE_FIX(0.0) && opt->highlight == SANE_FIX(100.0) && opt->gamma == SANE_FIX(1.0) && !opt->negative) { return NULL; } filt = mem_new(filter_xlat, 1); filt->base.free = (void (*)(filter*)) mem_free; filt->base.dump = filter_xlat_dump; filt->base.apply = filter_xlat_apply; for (i = 0; i < 256; i ++) { uint8_t c = opt->negative ? (255 - i) : i; double v = c / 255.0; v = C * (v - 0.5) + 0.5 + B; v = math_bound_double(v, 0.0, 1.0); v = pow(v, 1/G); c = round(v * 255.0); if (c <= shadow) { c = 0; } else if (c >= highlight) { c = 255; } filt->table[i] = c; } return &filt->base; } /******************** Filter chain management ********************/ /* Push filter into the chain of filters. * Takes ownership on both arguments and returns updated chain */ static filter* filter_chain_push (filter *old_chain, filter *new_filter) { if (old_chain == NULL) { return new_filter; } if (new_filter != NULL) { /* Nothing to do */ } else if (old_chain->next == NULL) { old_chain->next = new_filter; } else { old_chain->next = filter_chain_push(old_chain->next, new_filter); } return old_chain; } /* Free chain of filters */ void filter_chain_free (filter *chain) { while (chain != NULL) { filter *next = chain->next; chain->free(chain); chain = next; } } /* Push translation table based filter, that handles the * following options: * - brightness * - contrast * - negative * * Returns updated chain */ filter* filter_chain_push_xlat (filter *old_chain, const devopt *opt) { return filter_chain_push(old_chain, filter_xlat_new(opt)); } /* Dump filter chain to the log */ void filter_chain_dump (filter *chain, log_ctx *log) { log_debug(log, "image filter chain:"); while (chain != NULL) { chain->dump(chain, log); chain = chain->next; } } /* Apply filter chain to the image line */ void filter_chain_apply (filter *chain, uint8_t *line, size_t size) { while (chain != NULL) { chain->apply(chain, line, size); chain = chain->next; } } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-http.c000066400000000000000000002531421500411437100170110ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * HTTP Client */ #define _GNU_SOURCE #include #define NO_HTTP_STATUS #include "airscan.h" #include "http_parser.h" #include #include #include #include #include #include #include #include #include #include /******************** Constants ********************/ /* I/O buffer size */ #define HTTP_IOBUF_SIZE 65536 /* Limit of chained HTTP redirects */ #define HTTP_REDIRECT_LIMIT 8 /* Default query timeout, milliseconds * Disabled, if negative */ #define HTTP_QUERY_TIMEOUT -1 /******************** Static variables ********************/ static gnutls_certificate_credentials_t gnutls_cred; /******************** Forward declarations ********************/ typedef struct http_multipart http_multipart; static http_data* http_data_new(http_data *parent, const char *bytes, size_t size); static void http_data_set_content_type (http_data *data, const char *content_type); static void http_query_timeout_cancel (http_query *q); static http_query* http_query_by_ll_node (ll_node *node); static void http_query_complete (http_query *q, error err); static void http_query_connect (http_query *q, error err); static void http_query_disconnect (http_query *q); static ssize_t http_query_sock_send (http_query *q, const void *data, size_t size); static ssize_t http_query_sock_recv (http_query *q, void *data, size_t size); static error http_query_sock_err (http_query *q, int rc); static void http_query_cancel (http_query *q); /******************** HTTP URI ********************/ /* Type http_uri represents HTTP URI */ struct http_uri { struct http_parser_url parsed; /* Parsed URI */ const char *str; /* URI string */ const char *path; /* URI path */ const char *host; /* URI host */ HTTP_SCHEME scheme; /* URI scheme */ union { /* Host address*/ struct sockaddr sockaddr; struct sockaddr_in in; struct sockaddr_in6 in6; } addr; }; typedef struct { const char *str; size_t len; } http_uri_field; /* Make http_uri_field */ static http_uri_field http_uri_field_make (const char *str) { http_uri_field field = {str, strlen(str)}; return field; } /* Check if URI has a particular field */ static inline bool http_uri_field_present (const http_uri *uri, int num) { return (uri->parsed.field_set & (1 << num)) != 0; } /* Check if URI field is present and non-empty */ static inline bool http_uri_field_nonempty (const http_uri *uri, int num) { return uri->parsed.field_data[num].len != 0; } /* Get first character of the field. Returns -1, if * field is empty, or non-negative character code otherwise */ static inline int http_uri_field_begin (const http_uri *uri, int num) { if (http_uri_field_nonempty(uri, num)) { return (unsigned char) uri->str[uri->parsed.field_data[num].off]; } else { return -1; } } /* Get field from URI */ static http_uri_field http_uri_field_get (const http_uri *uri, int num) { http_uri_field field = { uri->str + uri->parsed.field_data[num].off, uri->parsed.field_data[num].len }; return field; } /* Append field to buffer, and return pointer to * updated buffer tail */ static inline char * http_uri_field_append (http_uri_field field, char *buf) { memcpy(buf, field.str, field.len); return buf + field.len; } /* Get field from URI and append to buffer */ static inline char* http_uri_field_copy (const http_uri *uri, int num, char *buf) { return http_uri_field_append(http_uri_field_get(uri, num), buf); } /* Get and strdup the field */ static char* http_uri_field_strdup (const http_uri *uri, int num) { http_uri_field field = http_uri_field_get(uri, num); char *s = mem_new(char, field.len + 1); memcpy(s, field.str, field.len); s[field.len] = '\0'; return s; } /* Check are fields of two URIs are equal */ static bool http_uri_field_equal (const http_uri *uri1, const http_uri *uri2, int num, bool nocase) { http_uri_field f1 = http_uri_field_get(uri1, num); http_uri_field f2 = http_uri_field_get(uri2, num); if (f1.len != f2.len) { return false; } if (nocase) { return !strncasecmp(f1.str, f2.str, f1.len); } else { return !memcmp(f1.str, f2.str, f1.len); } } /* Replace particular URI field with val[len] string */ static void http_uri_field_replace_len (http_uri *uri, int num, const char *val, size_t len) { static const struct { char *pfx; int num; char *sfx; } fields[] = { {"", UF_SCHEMA, "://"}, {"", UF_USERINFO, "@"}, {"", UF_HOST, ""}, {":", UF_PORT, ""}, {"", UF_PATH, ""}, {"?", UF_QUERY, ""}, {"#", UF_FRAGMENT, ""}, {NULL, -1, NULL} }; int i; char *buf = alloca(strlen(uri->str) + len + 4); char *end = buf; http_uri *uri2; /* Rebuild URI string */ for (i = 0; fields[i].num != -1; i ++) { http_uri_field field; if (num == fields[i].num) { field.str = val; field.len = len; } else { field = http_uri_field_get(uri, fields[i].num); } if (field.len != 0) { bool ip6_host = false; if (fields[i].num == UF_HOST) { ip6_host = memchr(field.str, ':', field.len) != NULL; } if (fields[i].pfx != NULL) { http_uri_field pfx = http_uri_field_make(fields[i].pfx); end = http_uri_field_append(pfx, end); } if (ip6_host) { *end ++ = '['; } end = http_uri_field_append(field, end); if (ip6_host) { *end ++ = ']'; } if (fields[i].sfx != NULL) { http_uri_field sfx = http_uri_field_make(fields[i].sfx); end = http_uri_field_append(sfx, end); } } } *end = '\0'; /* Reconstruct the URI */ uri2 = http_uri_new(buf, false); log_assert(NULL, uri2 != NULL); mem_free((char*) uri->str); mem_free((char*) uri->path); mem_free((char*) uri->host); *uri = *uri2; mem_free(uri2); } /* Replace particular URI field */ static void http_uri_field_replace (http_uri *uri, int num, const char *val) { http_uri_field_replace_len (uri, num, val, strlen(val)); } /* Append UF_PATH part of URI to buffer up to the final '/' * character */ static char* http_uri_field_copy_basepath (const http_uri *uri, char *buf) { http_uri_field path = http_uri_field_get(uri, UF_PATH); const char *end = memrchr(path.str, '/', path.len); path.len = end ? (size_t)(end - path.str) : 0; buf = http_uri_field_append(path, buf); *(buf ++) = '/'; return buf; } /* Check if sting has an scheme prefix, where scheme * must be [a-zA-Z][a-zA-Z0-9+-.]*): * * If scheme is found, returns its length. 0 is returned otherwise */ static size_t http_uri_parse_scheme (const char *str) { char c = *str; size_t i; if (!(('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) { return 0; } i = 1; for (;;) { c = str[i ++]; if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || c == '+' || c == '-' || c == '.') { continue; } break; } return c == ':' ? i : 0; } /* Return error, if UF_HOST field of URI is present and invalid */ static error http_uri_parse_check_host (http_uri *uri) { http_uri_field field = http_uri_field_get(uri, UF_HOST); char *host, *zone; int rc; struct in6_addr in6; /* If UF_HOST is present and in square brackets, it must * be valid IPv6 literal. Note, http_parser_parse_url() * doesn't check it */ if (field.len == 0 || field.str == uri->str || field.str[-1] != '[') { return NULL; } if (field.str[field.len] != ']') { return ERROR("URI: missed ']' in IP6 address literal"); } host = alloca(field.len + 1); memcpy(host, field.str, field.len); host[field.len] = '\0'; zone = strchr(host, '%'); if (zone != NULL) { *zone = '\0'; } rc = inet_pton(AF_INET6, host, &in6); if (rc != 1) { return ERROR("URI: invalid IP6 address literal"); } return NULL; } /* Parse URI in place. The parsed URI doesn't create * a copy of URI string, and uses supplied string directly */ static error http_uri_parse (http_uri *uri, const char *str) { size_t scheme_len = http_uri_parse_scheme(str); const char *normalized = str; const char *prefix = NULL; size_t prefix_len = 0; size_t path_skip = 0; error err; /* Note, github.com/nodejs/http-parser fails to properly * parse relative URLs (URLs without scheme), so prepend * fake scheme, then remove it */ if (scheme_len == 0) { char *s; if (str[0] == '/' && str[1] == '/') { prefix = "s:"; } else if (str[0] == '/') { prefix = "s://h"; } else { prefix = "s://h/"; path_skip = 1; } prefix_len = strlen(prefix); s = alloca(prefix_len + strlen(str) + 1); memcpy(s, prefix, prefix_len); strcpy(s + prefix_len, str); normalized = s; } /* Parse URI */ memset(uri, 0, sizeof(*uri)); if (http_parser_parse_url(normalized, strlen(normalized), 0, &uri->parsed) != 0) { return ERROR("URI: parse error"); } uri->str = str; /* Adjust offsets */ if (path_skip != 0) { uri->parsed.field_data[UF_PATH].off ++; uri->parsed.field_data[UF_PATH].len --; } if (prefix_len != 0) { unsigned int i; for (i = 0; i < UF_MAX; i ++) { if ((uri->parsed.field_set & (1 << i)) != 0) { if (uri->parsed.field_data[i].off >= (uint16_t) prefix_len) { uri->parsed.field_data[i].off -= (uint16_t) prefix_len; } else { uri->parsed.field_data[i].off = 0; uri->parsed.field_data[i].len = 0; uri->parsed.field_set &= ~ (1 << i); } } } } /* Decode scheme */ uri->scheme = HTTP_SCHEME_UNSET; /* For sanity */ if (scheme_len != 0) { if (!strncasecmp(str, "http://", 7)) { uri->scheme = HTTP_SCHEME_HTTP; } else if (!strncasecmp(str, "https://", 8)) { uri->scheme = HTTP_SCHEME_HTTPS; } else if (!strncasecmp(str, "unix://", 7)) { uri->scheme = HTTP_SCHEME_UNIX; } else { return ERROR("URI: invalid scheme"); } } /* Check UF_HOST */ err = http_uri_parse_check_host(uri); if (err != NULL) { return err; } return NULL; } /* Un-escape host name in place */ static void http_uri_unescape_host (char *host) { char *zone = strstr(host, "%25"); if (zone != NULL) { memmove(zone + 1, zone + 3, strlen(zone + 3) + 1); } } /* Parse URI address */ static void http_uri_parse_addr (http_uri *uri) { http_uri_field field; char *host = NULL, *port = NULL; uint16_t portnum; int rc; /* Reset address */ memset(&uri->addr, 0, sizeof(uri->addr)); /* Get host and port */ field = http_uri_field_get(uri, UF_HOST); if (field.len != 0) { host = alloca(field.len + 1); memcpy(host, field.str, field.len); host[field.len] = '\0'; http_uri_unescape_host(host); } field = http_uri_field_get(uri, UF_PORT); if (field.len != 0) { port = alloca(field.len + 1); memcpy(port, field.str, field.len); port[field.len] = '\0'; } if (host == NULL) { return; } /* Parse port number */ if (port != NULL) { char *end; unsigned long val = strtoul(port, &end, 10); if (end == port || *end != '\0' || val > 0xffff) { return; } portnum = htons((uint16_t) val); } else { switch (uri->scheme) { case HTTP_SCHEME_HTTP: portnum = htons(80); break; case HTTP_SCHEME_HTTPS: portnum = htons(443); break; default: return; } } if (strchr(host, ':') != NULL) { struct in6_addr in6; /* Strip zone suffix */ char *s = strchr(host, '%'); if (s != NULL) { *s = '\0'; } rc = inet_pton(AF_INET6, host, &in6); if (rc != 1) { return; } uri->addr.in6.sin6_family = AF_INET6; uri->addr.in6.sin6_addr = in6; uri->addr.in6.sin6_port = portnum; } else { struct in_addr in; rc = inet_pton(AF_INET, host, &in); if (rc != 1) { return; } uri->addr.in.sin_family = AF_INET; uri->addr.in.sin_addr = in; uri->addr.in.sin_port = portnum; } } /* Create new URI, by parsing URI string */ http_uri* http_uri_new (const char *str, bool strip_fragment) { http_uri *uri = mem_new(http_uri, 1); char *buf; http_uri_field field; /* Parse URI */ if (http_uri_parse(uri, str) != NULL) { goto FAIL; } /* Don't allow relative URLs here */ if (uri->scheme == HTTP_SCHEME_UNSET) { goto FAIL; } uri->str = buf = str_dup(str); /* Honor strip_fragment flag */ if (strip_fragment && http_uri_field_present(uri, UF_FRAGMENT)) { buf[uri->parsed.field_data[UF_FRAGMENT].off - 1] = '\0'; uri->parsed.field_set &= ~(1 << UF_FRAGMENT); uri->parsed.field_data[UF_FRAGMENT].off = 0; uri->parsed.field_data[UF_FRAGMENT].len = 0; } /* Prepare addr, path, etc */ http_uri_parse_addr(uri); uri->path = http_uri_field_strdup(uri, UF_PATH); field = http_uri_field_get(uri, UF_HOST); if (memchr(field.str, ':', field.len) != NULL) { char *host = mem_resize((char*) NULL, field.len + 2, 1); host[0] = '['; memcpy(host + 1, field.str, field.len); host[field.len + 1] = ']'; host[field.len + 2] = '\0'; uri->host = host; } else { uri->host = http_uri_field_strdup(uri, UF_HOST); } return uri; /* Error: cleanup and exit */ FAIL: mem_free(uri); return NULL; } /* Clone the URI */ http_uri* http_uri_clone (const http_uri *old) { http_uri *uri = mem_new(http_uri, 1); *uri = *old; uri->str = str_dup(uri->str); uri->path = str_dup(uri->path); uri->host = str_dup(uri->host); return uri; } /* Check that string, defined by begin and end pointers, * has a specified prefix */ static bool http_uri_str_prefix(const char *beg, const char *end, const char *pfx) { size_t len = end - beg; size_t pfx_len = strlen(pfx); return len >= pfx_len && !memcmp(beg, pfx, pfx_len); } /* Check that string, defined by begin and end pointers, * equal to the specified pattern */ static bool http_uri_str_equal(const char *beg, const char *end, const char *pattern) { size_t len = end - beg; size_t plen = strlen(pattern); return len == plen && !memcmp(beg, pattern, len); } /* Find 1st occurrence of the character in the string, * defined by begin and end pointers */ static char * http_uri_str_chr(const char *beg, const char *end, char c) { return memchr(beg, c, end - beg); } /* Find last occurrence of the character in the string, * defined by begin and end pointers */ static char * http_uri_str_rchr(const char *beg, const char *end, char c) { return memrchr(beg, c, end - beg); } /* Remove last path segment. Returns pointer to the * path end */ static char* http_uri_remove_last_segment (char *path, char *end) { char *s = http_uri_str_rchr(path, end, '/'); return s == NULL ? end : s; } /* Remove path dot segments, per rfc3986, 5.2.4. */ static char* http_uri_remove_dot_segments (char *path, char *end) { char *input = path; char *path_end = path; while (input != end) { /* A. If the input buffer begins with a prefix of "../" or "./", * then remove that prefix from the input buffer; otherwise, */ if (http_uri_str_prefix(input, end, "../")) { input += 3; } else if (http_uri_str_prefix(input, end, "./")) { input += 2; /* B. if the input buffer begins with a prefix of "/./" or "/.", * where "." is a complete path segment, then replace that * prefix with "/" in the input buffer; otherwise, */ } else if (http_uri_str_prefix(input, end, "/./")) { input += 2; } else if (http_uri_str_equal(input, end, "/.")) { input ++; input[0] = '/'; /* C. if the input buffer begins with a prefix of "/../" or "/..", * where ".." is a complete path segment, then replace that * prefix with "/" in the input buffer and remove the last * segment and its preceding "/" (if any) from the output * buffer; otherwise, */ } else if (http_uri_str_prefix(input, end, "/../")) { path_end = http_uri_remove_last_segment(path, path_end); input += 3; } else if (http_uri_str_equal(input, end, "/..")) { path_end = http_uri_remove_last_segment(path, path_end); input += 2; input[0] = '/'; /* D. if the input buffer consists only of "." or "..", then remove * that from the input buffer; otherwise, */ } else if (http_uri_str_equal(input, end, ".") || http_uri_str_equal(input, end, "..")) { input = end; /* E. move the first path segment in the input buffer to the end of * the output buffer, including the initial "/" character (if * any) and any subsequent characters up to, but not including, * the next "/" character or the end of the input buffer. */ } else { char *s = http_uri_str_chr(input + 1, end, '/'); size_t sz = s ? s - input : end - input; memmove(path_end, input, sz); path_end += sz; input += sz; } } return path_end; } /* Create URI, relative to base URI. If `path_only' is * true, scheme, host and port are taken from the * base URI */ http_uri* http_uri_new_relative (const http_uri *base, const char *path, bool strip_fragment, bool path_only) { char *buf = alloca(strlen(base->str) + strlen(path) + 1); char *end = buf; http_uri ref; const http_uri *uri; http_uri_field field; char *path_beg; if (http_uri_parse(&ref, path) != NULL) { return NULL; } /* If the base URI uses the unix:// scheme, don't allow a relative URI to * change the scheme or host. */ if (base->scheme == HTTP_SCHEME_UNIX) { path_only = true; } /* Set schema, userinfo, host and port */ if (path_only || !http_uri_field_present(&ref, UF_SCHEMA)) { end = http_uri_field_copy(base, UF_SCHEMA, end); } else { end = http_uri_field_copy(&ref, UF_SCHEMA, end); } end = http_uri_field_append(http_uri_field_make("://"), end); if (path_only || !http_uri_field_present(&ref, UF_HOST)) { uri = base; } else { uri = &ref; } if (http_uri_field_present(uri, UF_USERINFO)) { end = http_uri_field_copy(uri, UF_USERINFO, end); end = http_uri_field_append(http_uri_field_make("@"), end); } field = http_uri_field_get(uri, UF_HOST); if (memchr(field.str, ':', field.len) != NULL) { *end ++ = '['; end = http_uri_field_append(field, end); *end ++ = ']'; } else { end = http_uri_field_append(field, end); } if (http_uri_field_present(uri, UF_PORT)) { end = http_uri_field_append(http_uri_field_make(":"), end); end = http_uri_field_copy(uri, UF_PORT, end); } /* Set path */ path_beg = end; if (!http_uri_field_nonempty(&ref, UF_PATH)) { end = http_uri_field_copy(base, UF_PATH, end); } else { if (http_uri_field_begin(&ref, UF_PATH) != '/') { end = http_uri_field_copy_basepath(base, end); } end = http_uri_field_copy(&ref, UF_PATH, end); } end = http_uri_remove_dot_segments(path_beg, end); /* Query and fragment */ if (http_uri_field_present(&ref, UF_QUERY)) { end = http_uri_field_append(http_uri_field_make("?"), end); end = http_uri_field_copy(&ref, UF_QUERY, end); } if (!strip_fragment && http_uri_field_present(&ref, UF_FRAGMENT)) { end = http_uri_field_append(http_uri_field_make("#"), end); end = http_uri_field_copy(&ref, UF_FRAGMENT, end); } *end = '\0'; return http_uri_new(buf, false); } /* Free the URI */ #ifndef __clang_analyzer__ void http_uri_free (http_uri *uri) { if (uri != NULL) { mem_free((char*) uri->str); mem_free((char*) uri->path); mem_free((char*) uri->host); mem_free(uri); } } #endif /* Get URI string */ const char* http_uri_str (http_uri *uri) { return uri->str; } /* Get URI's host address. If Host address is not literal, returns NULL */ const struct sockaddr* http_uri_addr (const http_uri *uri) { if (uri->addr.sockaddr.sa_family == AF_UNSPEC) { return NULL; } return &uri->addr.sockaddr; } /* Get URI path * * Note, if URL has empty path (i.e., "http://1.2.3.4"), the * empty string will be returned */ const char* http_uri_get_path (const http_uri *uri) { return uri->path; } /* Set URI path */ void http_uri_set_path (http_uri *uri, const char *path) { http_uri_field_replace(uri, UF_PATH, path); } /* Get URI host. It returns only host name, port number is * not included. * * IPv6 literal addresses are returned in square brackets * (i.e., [fe80::217:c8ff:fe7b:6a91%4]) * * Note, the subsequent modifications of URI, such as http_uri_fix_host(), * http_uri_fix_ipv6_zone() etc, may make the returned string invalid, * so if you need to keep it for a long time, better make a copy */ const char* http_uri_get_host (const http_uri *uri) { return uri->host; } /* http_uri_host_is checks if URI's host name is equal to the * specified string. * * It does its best to compare domain names correctly, taking * in account only significant difference (for example, the difference * in upper/lower case * in domain names is not significant). */ bool http_uri_host_is (const http_uri *uri, const char *host) { const char *uri_host = http_uri_get_host(uri); if (http_uri_host_is_literal(uri)) { return !strcasecmp(uri_host, host); } if (!avahi_is_valid_domain_name(uri_host)) { return false; } if (!avahi_is_valid_domain_name(host)) { return false; } return avahi_domain_equal(uri_host, host); } /* http_uri_host_is_literal returns true if URI uses literal * IP address */ bool http_uri_host_is_literal (const http_uri *uri) { return http_uri_addr(uri) != NULL; } /* Set URI host into the literal IP address. */ void http_uri_set_host_addr (http_uri *uri, ip_addr addr) { ip_straddr straddr = ip_addr_to_straddr(addr, true); char *host = straddr.text; char *s; /* Remove square brackets around IPv6 address */ if (host[0] == '[' && host[strlen(host) - 1] == ']') { host[strlen(host) - 1] = '\0'; host ++; } /* Escape % character (zone suffix delimiter) */ s = strchr(host, '%'); if (s != NULL) { /* Note, we replace '%' with '%25' plus don't forget * the terminating '\0'. Hence strlen(host) + 3 */ char *host2 = alloca(strlen(host) + 3); size_t sz = s - host; memcpy(host2, host, sz); memcpy(host2 + sz, "%25", 3); strcpy(host2 + sz + 3, s + 1); host = host2; } /* Set the host */ http_uri_field_replace(uri, UF_HOST, host); } /* Fix URI host: if `match` is NULL or uri's host matches `match`, * replace uri's host and port with values taken from the base_uri */ void http_uri_fix_host (http_uri *uri, const http_uri *base_uri, const char *match) { http_uri_field schema, host, port; if (match != NULL) { host = http_uri_field_get(uri, UF_HOST); if (strncasecmp(host.str, match, host.len)) { return; } } schema = http_uri_field_get(base_uri, UF_SCHEMA); host = http_uri_field_get(base_uri, UF_HOST); port = http_uri_field_get(base_uri, UF_PORT); http_uri_field_replace_len(uri, UF_SCHEMA, schema.str, schema.len); http_uri_field_replace_len(uri, UF_HOST, host.str, host.len); http_uri_field_replace_len(uri, UF_PORT, port.str, port.len); } /* Fix IPv6 address zone suffix */ void http_uri_fix_ipv6_zone (http_uri *uri, int ifindex) { http_uri_field field; char *host; /* Check if we need to change something */ if (uri->addr.sockaddr.sa_family != AF_INET6) { return; /* Not IPv6 */ } if (!ip_is_linklocal(AF_INET6, &uri->addr.in6.sin6_addr)) { return; /* Not link-local */ } field = http_uri_field_get(uri, UF_HOST); if (memchr(field.str, '%', field.len)) { return; /* Already has zone suffix */ } /* Obtain writable copy of host name */ host = alloca(field.len + 64); memcpy(host, field.str, field.len); /* Append zone suffix */ sprintf(host + field.len, "%%25%d", ifindex); /* Update URL's host */ http_uri_field_replace(uri, UF_HOST, host); uri->addr.in6.sin6_scope_id = ifindex; } /* Strip zone suffix from literal IPv6 host address * * If address is not IPv6 or doesn't have zone suffix, it is * not changed */ void http_uri_strip_zone_suffux (http_uri *uri) { http_uri_field field; const char *suffix; size_t len; char *host; /* Check if we need to change something */ if (uri->addr.sockaddr.sa_family != AF_INET6) { return; /* Not IPv6 */ } field = http_uri_field_get(uri, UF_HOST); suffix = memchr(field.str, '%', field.len); if (suffix == NULL) { return; /* No zone suffix */ } len = suffix - field.str; /* Update host */ host = alloca(len + 1); memcpy(host, field.str, len); host[len] = '\0'; http_uri_field_replace(uri, UF_HOST, host); uri->addr.in6.sin6_scope_id = 0; } /* Make sure URI's path ends with the slash character */ void http_uri_fix_end_slash (http_uri *uri) { const char *path = http_uri_get_path(uri); if (!str_has_suffix(path, "/")) { size_t len = strlen(path); char *path2 = alloca(len + 2); memcpy(path2, path, len); path2[len] = '/'; path2[len+1] = '\0'; http_uri_set_path(uri, path2); } } /* Check if 2 URIs are equal */ bool http_uri_equal (const http_uri *uri1, const http_uri *uri2) { return uri1->scheme == uri2->scheme && http_uri_field_equal(uri1, uri2, UF_HOST, true) && http_uri_field_equal(uri1, uri2, UF_PORT, true) && http_uri_field_equal(uri1, uri2, UF_PATH, false) && http_uri_field_equal(uri1, uri2, UF_QUERY, false) && http_uri_field_equal(uri1, uri2, UF_FRAGMENT, false) && http_uri_field_equal(uri1, uri2, UF_USERINFO, false); } /******************** HTTP header ********************/ /* http_hdr represents HTTP header */ typedef struct { ll_head fields; /* List of http_hdr_field */ } http_hdr; /* http_hdr_field represents a single HTTP header field */ typedef struct { char *name; /* Header name */ char *value; /* Header value, may be NULL */ ll_node chain; /* In http_hdr::fields */ } http_hdr_field; /* Create http_hdr_field. Name can be NULL */ static http_hdr_field* http_hdr_field_new (const char *name) { http_hdr_field *field = mem_new(http_hdr_field, 1); field->name = name ? str_dup(name) : str_new(); return field; } /* Destroy http_hdr_field */ static void http_hdr_field_free (http_hdr_field *field) { mem_free(field->name); mem_free(field->value); mem_free(field); } /* Initialize http_hdr in place */ static void http_hdr_init (http_hdr *hdr) { ll_init(&hdr->fields); } /* Cleanup http_hdr in place */ static void http_hdr_cleanup (http_hdr *hdr) { ll_node *node; while ((node = ll_pop_beg(&hdr->fields)) != NULL) { http_hdr_field *field = OUTER_STRUCT(node, http_hdr_field, chain); http_hdr_field_free(field); } } /* Write header to string buffer in wire format */ static char* http_hdr_write (const http_hdr *hdr, char *out) { ll_node *node; for (LL_FOR_EACH(node, &hdr->fields)) { http_hdr_field *field = OUTER_STRUCT(node, http_hdr_field, chain); out = str_append(out, field->name); out = str_append(out, ": "); out = str_append(out, field->value); out = str_append(out, "\r\n"); } return str_append(out, "\r\n"); } /* Lookup field in the header */ static http_hdr_field* http_hdr_lookup (const http_hdr *hdr, const char *name) { ll_node *node; for (LL_FOR_EACH(node, &hdr->fields)) { http_hdr_field *field = OUTER_STRUCT(node, http_hdr_field, chain); if (!strcasecmp(field->name, name)) { return field; } } return NULL; } /* Get header field */ static const char* http_hdr_get (const http_hdr *hdr, const char *name) { http_hdr_field *field = http_hdr_lookup(hdr, name); if (field == NULL) { return NULL; } if (field->value == NULL) { return ""; } return field->value; } /* Set header field */ static void http_hdr_set (http_hdr *hdr, const char *name, const char *value) { http_hdr_field *field = http_hdr_lookup(hdr, name); if (field == NULL) { field = http_hdr_field_new(name); ll_push_end(&hdr->fields, &field->chain); } if (field->value == NULL) { field->value = str_dup(value); } else { field->value = str_assign(field->value, value); } } /* Del header field */ static void http_hdr_del (http_hdr *hdr, const char *name) { http_hdr_field *field = http_hdr_lookup(hdr, name); if (field != NULL) { ll_del(&field->chain); http_hdr_field_free(field); } } /* Handle HTTP parser on_header_field callback * parser->data must point to the header being parsed */ static int http_hdr_on_header_field (http_parser *parser, const char *data, size_t size) { http_hdr *hdr = parser->data; ll_node *node; http_hdr_field *field = NULL; /* Get last field */ node = ll_last(&hdr->fields); if (node != NULL) { field = OUTER_STRUCT(node, http_hdr_field, chain); } /* If there is no last field, or last field already * has value, create a new field */ if (field == NULL || field->value != NULL) { field = http_hdr_field_new(NULL); ll_push_end(&hdr->fields, &field->chain); } /* Append data to the field name */ field->name = str_append_mem(field->name, data, size); return 0; } /* Handle HTTP parser on_header_value callback * parser->data must point to the header being parsed */ static int http_hdr_on_header_value (http_parser *parser, const char *data, size_t size) { http_hdr *hdr = parser->data; ll_node *node; http_hdr_field *field = NULL; /* Get last field */ node = ll_last(&hdr->fields); if (node != NULL) { field = OUTER_STRUCT(node, http_hdr_field, chain); } /* If there is no last field, just ignore the data. * Note, it actually should not happen */ if (field == NULL) { return 0; } /* Append data to field value */ if (field->value == NULL) { field->value = str_new(); } field->value = str_append_mem(field->value, data, size); return 0; } /* Handle HTTP parser on_headers_complete callback * This is used http_hdr_parse only and returns 1 to * tell parser to don't attempt to parse message body */ static int http_hdr_on_headers_complete (http_parser *parser) { (void) parser; return 1; } /* Parse http_hdr from memory buffer * * If `skip_line' is true, first line is skipped, which * is useful if first line contains HTTP request/status * or a multipart boundary */ static error http_hdr_parse (http_hdr *hdr, const char *data, size_t size, bool skip_line) { static http_parser_settings callbacks = { .on_header_field = http_hdr_on_header_field, .on_header_value = http_hdr_on_header_value, .on_headers_complete = http_hdr_on_headers_complete }; http_parser parser = {0}; static char prefix[] = "HTTP/1.1 200 OK\r\n"; /* Skip first line, if requested */ if (skip_line) { const char *s = memchr(data, '\n', size); if (s) { size_t skip = (s - data) + 1; data += skip; size -= skip; } } /* Initialize HTTP parser */ http_parser_init(&parser, HTTP_RESPONSE); parser.data = hdr; /* Note, http_parser unable to parse bare HTTP * header, without request or status line, so * we insert fake status line to make it happy */ http_parser_execute(&parser, &callbacks, prefix, sizeof(prefix) - 1); http_parser_execute(&parser, &callbacks, data, size); if (parser.http_errno != HPE_OK) { return ERROR(http_errno_description(parser.http_errno)); } return NULL; } /* Check if character is special, for http_hdr_params_parse */ static bool http_hdr_params_chr_isspec (char c) { if (0x20 < c && c < 0x7f) { switch (c) { case '(': case ')': case '<': case '>': case '@': case ',': case ';': case ':': case '\\': case '\"': case '/': case '[': case ']': case '?': case '=': return true; default: return safe_isspace(c); } } return true; } /* Check if character is space, for http_hdr_params_parse */ static bool http_hdr_params_chr_isspace (char c) { switch ((unsigned char) c) { case '\t': case '\n': case '\v': case '\f': case '\r': case ' ': case 0x85: case 0xA0: return true; } return false; } /* Parse HTTP header field with parameters. * Result is saved into `params' as collection of name/value fields */ static error http_hdr_params_parse (http_hdr *params, const char *name, const char *in) { enum { /* params ::= param [';' params] * param ::= SP1 NAME SP2 '=' SP3 value SP4 * value ::= TOKEN | STRING */ SP1, NAME, SP2, EQ, SP3, VALUE, TOKEN, STRING, STRING_BSLASH, SP4, END } state = SP1; char c; http_hdr_field *field = NULL; /* Parameters begin after ';' */ in = strchr(in, ';'); if (in == NULL) { return NULL; } in ++; while ((c = *in) != '\0') { switch (state) { case SP1: case SP2: case SP3: case SP4: if (!http_hdr_params_chr_isspace(c)) { state ++; continue; } break; case NAME: if (!http_hdr_params_chr_isspec(c)) { if (field == NULL) { field = http_hdr_field_new(NULL); field->value = str_new(); ll_push_end(¶ms->fields, &field->chain); } field->name = str_append_c(field->name, c); } else if (c == ';') { state = SP1; field = NULL; } else { state = SP2; continue; } break; case EQ: if (c == '=') { state = SP3; } else if (c == ';') { state = SP1; field = NULL; } else { return eloop_eprintf( "http %s: expected '=' or ';', got: %s", name, in); } break; case VALUE: if (c == '"') { state = STRING; } else { state = TOKEN; continue; } break; case STRING: if (c == '\\') { state = STRING_BSLASH; } else if (c == '"') { state = SP4; } else { field->value = str_append_c(field->value, c); } break; case STRING_BSLASH: field->value = str_append_c(field->value, c); state = STRING; break; case TOKEN: if (c != ';' && c != '"' && !safe_isspace(c)) { field->value = str_append_c(field->value, c); } else { state = SP4; continue; } break; case END: if (c != ';') { return eloop_eprintf( "http %s: expected ';', got: %s", name, in); } state = SP1; field = NULL; break; } in ++; } if (state == STRING || state == STRING_BSLASH) { return eloop_eprintf( "http %s: unterminated quoted string"); } return NULL; } /* Call callback for each header field */ static void hdr_for_each (const http_hdr *hdr, void (*callback)(const char *name, const char *value, void *ptr), void *ptr) { ll_node *node; for (LL_FOR_EACH(node, &hdr->fields)) { http_hdr_field *field = OUTER_STRUCT(node, http_hdr_field, chain); if (field->value != NULL) { callback(field->name, field->value, ptr); } } } /******************** HTTP multipart ********************/ /* http_multipart represents a decoded multipart message */ struct http_multipart { int count; /* Count of bodies */ http_data *data; /* Response data */ http_data **bodies; /* Multipart bodies, var-size */ }; /* Add multipart body */ static void http_multipart_add_body (http_multipart *mp, http_data *body) { mp->bodies = mem_resize(mp->bodies, mp->count + 1, 0); mp->bodies[mp->count ++] = body; } /* Find boundary within the multipart message data */ static const char* http_multipart_find_boundary (const char *boundary, size_t boundary_len, const char *data, size_t size) { /* Note, per RFC 2046, "the boundary delimiter MUST occur at the beginning * of a line, i.e., following a CRLF, and the initial CRLF is considered to * be attached to the boundary delimiter line rather than part of the * preceding part". * * However, Xerox WorkCentre 3025 violates this requirement, and * puts boundary delimiter without preceding CRLF, so we must relax * out expectations */ return memmem(data, size, boundary, boundary_len); } /* Adjust part of multipart message: * 1) skip header * 2) fetch content type */ static error http_multipart_adjust_part (http_data *part) { const char *split; http_hdr hdr; size_t hdr_len; error err; /* Locate end of headers */ split = memmem(part->bytes, part->size, "\r\n\r\n", 4); if (split == NULL) { return ERROR("http multipart: can't locate end of part headers"); } /* Parse headers and obtain content-type */ http_hdr_init(&hdr); hdr_len = 4 + split - (char*) part->bytes; err = http_hdr_parse(&hdr, part->bytes, hdr_len - 2, true); if (err == NULL) { const char *ct = http_hdr_get(&hdr, "Content-Type"); http_data_set_content_type(part, ct); } http_hdr_cleanup(&hdr); if (err != NULL) { return eloop_eprintf("http multipart: %s", ESTRING(err)); } /* Cut of header */ split += 4; part->size -= (split - (char*) part->bytes); part->bytes = split; /* Strip CR/LF preceding next boundary, if any */ if (split[part->size - 2] == '\r' && split[part->size - 1] == '\n') { part->size -= 2; } return NULL; } /* Free http_multipart */ static void http_multipart_free (http_multipart *mp) { int i; for (i = 0; i < mp->count; i ++) { http_data_unref(mp->bodies[i]); } mem_free(mp); } /* Parse MIME multipart message body * Saves result into `out'. Result may be NULL, if no multipart */ static error http_multipart_parse (http_multipart **out, log_ctx *log, http_data *data, const char *content_type) { http_multipart *mp; http_hdr params; const char *boundary; size_t boundary_len = 0; const char *data_beg, *data_end, *data_prev; error err; ll_node *node; /* Check MIME type */ *out = NULL; if (strncmp(data->content_type, "multipart/", 10)) { return NULL; } /* Obtain boundary */ http_hdr_init(¶ms); err = http_hdr_params_parse(¶ms, "Content-Type", content_type); if (err != NULL) { http_hdr_cleanup(¶ms); return err; } log_debug(log, "http multipart parameters:"); for (LL_FOR_EACH(node, ¶ms.fields)) { http_hdr_field *field = OUTER_STRUCT(node, http_hdr_field, chain); log_debug(log, " %s=\"%s\"", field->name, field->value); } boundary = http_hdr_get(¶ms, "boundary"); if (boundary) { char *s; boundary_len = strlen(boundary) + 2; s = alloca(boundary_len + 1); s[0] = '-'; s[1] = '-'; strcpy(s + 2, boundary); boundary = s; } http_hdr_cleanup(¶ms); if (!boundary) { return ERROR("http multipart: missed boundary parameter"); } /* Create http_multipart structure */ mp = mem_new(http_multipart, 1); mp->data = http_data_ref(data); /* Split data into parts */ data_beg = data->bytes; data_end = data_beg + data->size; data_prev = NULL; while (data_beg != data_end) { const char *part = http_multipart_find_boundary(boundary, boundary_len, data_beg, data_end - data_beg); const char *next = data_end; if (part != NULL) { if (data_prev != NULL) { http_data *body = http_data_new(data, data_prev, part - data_prev); http_multipart_add_body(mp, body); err = http_multipart_adjust_part(body); if (err != NULL) { http_multipart_free(mp); return err; } } data_prev = part; const char *tail = part + boundary_len; if (data_end - tail >= 2 && tail[0] == '\r' && tail[1] == '\n') { next = tail + 2; } } data_beg = next; } if (mp->count == 0) { http_multipart_free(mp); return ERROR("http multipart: no parts found"); } if (data_beg != data_end) { log_debug(log, "http multipart: found %d bytes of garbage at the end of message", (int)(data_end - data_beg)); } *out = mp; return NULL; } /******************** HTTP data ********************/ /* http_data + SoupBuffer */ typedef struct { http_data data; /* HTTP data */ volatile unsigned int refcnt; /* Reference counter */ http_data *parent; /* Parent data buffer */ } http_data_ex; /* Create new http_data * * If parent != NULL, supplied bytes buffer must be owned by * parent. Otherwise, newly created http_data takes ownership * on the supplied data buffer */ static http_data* http_data_new(http_data *parent, const char *bytes, size_t size) { http_data_ex *data_ex = mem_new(http_data_ex, 1); if (parent != NULL) { log_assert(NULL, bytes >= (char*) parent->bytes); log_assert(NULL, (bytes + size) <= ((char*) parent->bytes + parent->size)); } data_ex->data.content_type = str_new(); data_ex->data.bytes = bytes; data_ex->data.size = size; data_ex->refcnt = 1; data_ex->parent = parent ? http_data_ref(parent) : NULL; return &data_ex->data; } /* Set Content-type */ static void http_data_set_content_type (http_data *data, const char *content_type) { mem_free((char*) data->content_type); if (content_type == NULL) { content_type = str_dup("text/plain"); } else { char *s; content_type = str_dup_tolower(content_type); s = strchr(content_type, ';'); if (s != NULL) { *s = '\0'; } } data->content_type = content_type; } /* Dummy http_data in case no data is present */ static http_data http_data_empty = { .content_type = "", .bytes = "", .size = 0 }; /* Ref http_data */ http_data* http_data_ref (http_data *data) { if (data != NULL && data != &http_data_empty) { http_data_ex *data_ex = OUTER_STRUCT(data, http_data_ex, data); __sync_fetch_and_add(&data_ex->refcnt, 1); } return data; } /* Unref http_data */ void http_data_unref (http_data *data) { if (data != NULL && data != &http_data_empty) { http_data_ex *data_ex = OUTER_STRUCT(data, http_data_ex, data); if (__sync_fetch_and_sub(&data_ex->refcnt, 1) == 1) { if (data_ex->parent != NULL) { http_data_unref(data_ex->parent); } else { mem_free((void*) data_ex->data.bytes); } mem_free((char*) data_ex->data.content_type); mem_free(data_ex); } } } /* Append bytes to data. http_data must be owner of its * own buffer, i.e. it must have no parent * * Returns true on success, false on OOM */ static bool http_data_append (http_data *data, const char *bytes, size_t size) { http_data_ex *data_ex = OUTER_STRUCT(data, http_data_ex, data); void *p; log_assert(NULL, data_ex->parent == NULL); p = mem_try_resize((char*) data->bytes, data->size + size, 0); if (p == NULL) { return false; } data->bytes = p; memcpy((char*) data->bytes + data->size, bytes, size); data->size += size; return true; } /******************** HTTP data queue ********************/ /* http_data_queue represents a queue of http_data items */ struct http_data_queue { http_data **items; /* Array of http_data items */ }; /* Create new http_data_queue */ http_data_queue* http_data_queue_new (void) { http_data_queue *queue = mem_new(http_data_queue, 1); queue->items = ptr_array_new(http_data*); return queue; } /* Destroy http_data_queue */ void http_data_queue_free (http_data_queue *queue) { http_data_queue_purge(queue); mem_free(queue->items); mem_free(queue); } /* Push item into the http_data_queue. */ void http_data_queue_push (http_data_queue *queue, http_data *data) { queue->items = ptr_array_append(queue->items, data); } /* Pull an item from the http_data_queue. Returns NULL if queue is empty */ http_data* http_data_queue_pull (http_data_queue *queue) { return ptr_array_del(queue->items, 0); } /* Get queue length */ int http_data_queue_len (const http_data_queue *queue) { return (int) mem_len(queue->items); } /* Purge the queue */ void http_data_queue_purge (http_data_queue *queue) { http_data *data; while ((data = http_data_queue_pull(queue)) != NULL) { http_data_unref(data); } } /******************** HTTP client ********************/ /* Type http_client represents HTTP client instance */ struct http_client { void *ptr; /* Callback's user data */ log_ctx *log; /* Logging context */ ll_head pending; /* Pending queries */ void (*onerror)( /* Callback to be called on transport error */ void *ptr, error err); }; /* Create new http_client */ http_client* http_client_new (log_ctx *log, void *ptr) { http_client *client = mem_new(http_client, 1); client->ptr = ptr; client->log = log; ll_init(&client->pending); return client; } /* Destroy http_client */ void http_client_free (http_client *client) { log_assert(client->log, ll_empty(&client->pending)); mem_free(client); } /* Set on-error callback. If this callback is not NULL, * in a case of transport error it will be called instead * of the http_query callback */ void http_client_onerror (http_client *client, void (*onerror)(void *ptr, error err)) { client->onerror = onerror; } /* Cancel all pending queries, if any */ void http_client_cancel (http_client *client) { ll_node *node; while ((node = ll_pop_beg(&client->pending)) != NULL) { http_query *q; q = http_query_by_ll_node(node); http_query_cancel(q); } } /* Set timeout of all pending queries, if any. Timeout is in milliseconds */ void http_client_timeout (http_client *client, int timeout) { ll_node *node; while ((node = ll_pop_beg(&client->pending)) != NULL) { http_query *q; q = http_query_by_ll_node(node); http_query_timeout(q, timeout); } } /* Check if client has pending queries */ bool http_client_has_pending (const http_client *client) { return !ll_empty(&client->pending); } /******************** HTTP request handling ********************/ /* Type http_query represents HTTP query (both request and response) */ struct http_query { /* URI and method */ http_uri *uri; /* Query URI */ http_uri *real_uri; /* Real URI, may be NULL */ const char *method; /* Request method */ /* Request and response headers */ http_hdr request_header; /* Request header */ http_hdr response_header; /* Response header */ bool host_inserted; /* Host: auto-inserted */ bool force_port; /* Host: always includes port */ /* HTTP redirects */ int redirect_count; /* Count of redirects */ http_uri *orig_uri; /* Original URI */ const char *orig_method; /* Original method */ /* Query timeout */ eloop_timer *timeout_timer; /* Timeout timer */ int timeout_value; /* In milliseconds */ /* Miscellaneous options */ bool no_need_response_body; /* Response body not needed */ /* Low-level I/O */ bool submitted; /* http_query_submit() called */ uint64_t eloop_callid; /* For eloop_call_cancel */ error err; /* Transport error */ struct addrinfo *addrs; /* Addresses to connect to */ bool addrs_freeaddrinfo; /* Use freeaddrinfo(addrs) */ struct addrinfo *addr_next; /* Next address to try */ int sock; /* HTTP socket */ gnutls_session_t tls; /* NULL if not TLS */ bool handshake; /* TLS handshake in progress */ bool sending; /* We are now sending */ eloop_fdpoll *fdpoll; /* Polls q->sock */ ip_straddr straddr; /* q->sock peer addr, for log */ char *rq_buf; /* Formatted request */ size_t rq_off; /* send() offset in request */ /* HTTP parser */ http_parser http_parser; /* HTTP parser structure */ bool http_headers_received; /* HTTP headers received */ bool http_parser_done; /* Message parsing done */ /* Data handling */ http_data *request_data; /* NULL if none */ http_data *response_data; /* NULL if none */ http_multipart *response_multipart; /* NULL if not multipart */ /* Callbacks and context */ timestamp timestamp; /* Submission timestamp */ uintptr_t uintptr; /* User-defined parameter */ void (*onerror) (void *ptr, /* On-error callback */ error err); void (*onredir) (void *ptr, /* On-redirect callback */ http_uri *uri, const http_uri *orig_uri); void (*onrxhdr) (void *ptr, /* On-header reception */ http_query *q); void (*callback) (void *ptr, /* Completion callback */ http_query *q); /* Linkage to http_client */ http_client *client; /* Client that owns the query */ bool queued; /* Query is queued */ ll_node chain; /* In http_client::pending or http_client::queued */ }; /* Get http_query* by pointer to its http_query::chain */ static http_query* http_query_by_ll_node (ll_node *node) { return OUTER_STRUCT(node, http_query, chain); } /* Reset query into the state it had before http_query_submit() */ static void http_query_reset (http_query *q) { if (q->host_inserted) { http_hdr_del(&q->request_header, "Host"); q->host_inserted = false; } http_hdr_cleanup(&q->response_header); if (q->addrs != NULL) { if (q->addrs_freeaddrinfo) { freeaddrinfo(q->addrs); } else { mem_free(q->addrs->ai_addr); mem_free(q->addrs); } q->addrs = NULL; q->addr_next = NULL; } q->handshake = q->sending = false; http_query_disconnect(q); str_trunc(q->rq_buf); q->rq_off = 0; q->http_headers_received = false; q->http_parser_done = false; http_data_unref(q->response_data); q->response_data = NULL; if (q->response_multipart != NULL) { http_multipart_free(q->response_multipart); q->response_multipart = NULL; } } /* Free http_query */ static void http_query_free (http_query *q) { http_query_reset(q); http_query_timeout_cancel(q); http_uri_free(q->uri); http_uri_free(q->real_uri); http_uri_free(q->orig_uri); http_hdr_cleanup(&q->request_header); mem_free(q->rq_buf); http_data_unref(q->request_data); mem_free(q); } /* Set Host header in HTTP request */ static void http_query_set_host (http_query *q) { char *host, *end, *buf; size_t len; const struct sockaddr *addr = http_uri_addr(q->uri); if (q->uri->scheme == HTTP_SCHEME_UNIX) { http_query_set_request_header(q, "Host", "localhost"); return; } if (addr != NULL) { ip_straddr s; int dport; switch (q->uri->scheme) { case HTTP_SCHEME_HTTP: dport = 80; break; case HTTP_SCHEME_HTTPS: dport = 443; break; default: dport = -1; break; } if (q->force_port) { dport = -1; } s = ip_straddr_from_sockaddr_dport(addr, dport, false, true); http_query_set_request_header(q, "Host", s.text); return; } host = strstr(http_uri_str(q->uri), "//") + 2; end = strchr(host, '/'); len = end ? (size_t) (end - host) : strlen(host); buf = alloca(len + 1); memcpy(buf, host, len); buf[len] = '\0'; http_query_set_request_header(q, "Host", buf); } /* Create new http_query * * Newly created http_query takes ownership on uri and body (if not NULL). * The method and content_type assumed to be constant strings. */ http_query* http_query_new (http_client *client, http_uri *uri, const char *method, char *body, const char *content_type) { size_t len = body ? strlen(body) : 0; return http_query_new_len(client, uri, method, body, len, content_type); } /* Create new http_query * * Works like http_query_new(), but request body length is specified * explicitly */ http_query* http_query_new_len (http_client *client, http_uri *uri, const char *method, void *body, size_t body_len, const char *content_type) { http_query *q = mem_new(http_query, 1); q->client = client; q->uri = uri; q->method = method; http_hdr_init(&q->request_header); http_hdr_init(&q->response_header); q->sock = -1; q->rq_buf = str_new(); q->onerror = client->onerror; http_parser_init(&q->http_parser, HTTP_RESPONSE); q->http_parser.data = &q->response_header; /* Note, on Kyocera ECOSYS M2040dn connection keep-alive causes * scanned job to remain in "Processing" state about 10 seconds * after job has been actually completed, making scanner effectively * busy. * * Looks like Kyocera firmware bug. Force connection to close * as a workaround */ http_query_set_request_header(q, "Connection", "close"); /* Save request body and set Content-Type */ if (body != NULL) { q->request_data = http_data_new(NULL, body, body_len); if (content_type != NULL) { http_query_set_request_header(q, "Content-Type", content_type); http_data_set_content_type(q->request_data, content_type); } } /* Set default timeout */ http_query_timeout(q, HTTP_QUERY_TIMEOUT); return q; } /* Create new http_query, relative to base URI * * Newly created http_query takes ownership on body (if not NULL). * The method and content_type assumed to be constant strings. */ http_query* http_query_new_relative(http_client *client, const http_uri *base_uri, const char *path, const char *method, char *body, const char *content_type) { http_uri *uri = http_uri_new_relative(base_uri, path, true, false); log_assert(client->log, uri != NULL); return http_query_new(client, uri, method, body, content_type); } /* http_query_timeout callback */ static void http_query_timeout_callback (void *p) { http_query *q = (http_query*) p; q->timeout_timer = NULL; /* to prevent eloop_timer_cancel() */ http_query_complete(q, ERROR("timeout")); } /* Set query timeout, in milliseconds. Negative timeout means 'infinite' * * This function may be called multiple times (each subsequent call overrides * a previous one) */ void http_query_timeout (http_query *q, int timeout) { q->timeout_value = timeout; if (q->submitted) { http_query_timeout_cancel(q); if (timeout >= 0) { log_debug(q->client->log, "HTTP using timeout: %d ms", q->timeout_value); q->timeout_timer = eloop_timer_new(timeout, http_query_timeout_callback, q); } else { log_debug(q->client->log, "HTTP using timeout: none"); } } } /* Set 'no_need_response_body' flag * * This flag notifies, that http_query issued is only interested * in the HTTP response headers, not body * * If this flag is set, after successful reception of response * HTTP header, errors in fetching response body is ignored */ void http_query_no_need_response_body (http_query *q) { q->no_need_response_body = true; } /* Cancel query timeout timer */ static void http_query_timeout_cancel (http_query *q) { if (q->timeout_timer != NULL) { eloop_timer_cancel(q->timeout_timer); q->timeout_timer = NULL; } } /* Set forcing port to be added to the Host header for this query. * * This function may be called multiple times (each subsequent call overrides * a previous one). */ void http_query_force_port(http_query *q, bool force_port) { q->force_port = force_port; } /* For this particular query override on-error callback, previously * set by http_client_onerror() * * If canllback is NULL, the completion callback, specified on a * http_query_submit() call, will be used even in a case of * transport error. */ void http_query_onerror (http_query *q, void (*onerror)(void *ptr, error err)) { q->onerror = onerror; } /* Set on-redirect callback. It is called in a case of HTTP * redirect and may modify the supplied URI */ void http_query_onredir (http_query *q, void (*onredir)(void *ptr, http_uri *uri, const http_uri *orig_uri)) { q->onredir = onredir; } /* Set callback that will be called, when response headers reception * is completed */ void http_query_onrxhdr (http_query *q, void (*onrxhdr)(void *ptr, http_query *q)) { q->onrxhdr = onrxhdr; } /* Choose HTTP redirect method, based on HTTP status code * Returns NULL for non-redirection status code, and may * be used to detect if status code implies redirection */ static const char* http_query_redirect_method (const http_query *q) { const char *method = q->orig_method ? q->orig_method : q->method; switch(http_query_status(q)) { case 303: if (!strcmp(method, "POST") || !strcmp(method, "PUT")) { method = "GET"; } break; case 301: case 302: case 307: case 308: break; default: return NULL; } return method; } /* Handle HTTP redirection */ static error http_query_redirect (http_query *q, const char *method) { const char *location; http_uri *uri; /* Check and parse location */ location = http_query_get_response_header(q, "Location"); if (location == NULL || *location == '\0') { return ERROR("HTTP redirect: missed Location: field"); } uri = http_uri_new_relative(q->uri, location, true, false); if (uri == NULL) { return ERROR("HTTP redirect: invalid Location: field"); } /* Enforce redirects limit */ q->redirect_count ++; if (q->redirect_count == HTTP_REDIRECT_LIMIT) { return ERROR("HTTP redirect: too many redirects"); } /* Save original URI and method at the first redirect */ if (q->redirect_count == 1) { q->orig_uri = q->uri; q->orig_method = q->method; } else { http_uri_free(q->uri); q->uri = NULL; /* Just in case */ } /* Issue log message */ log_debug(q->client->log, "HTTP redirect %d: %s %s", q->redirect_count, method, http_uri_str(uri)); /* Call user hook, if any */ if (q->onredir != NULL) { char *old_uri_str = alloca(strlen(uri->str) + 1); strcpy(old_uri_str, uri->str); q->onredir(q->client->ptr, uri, q->orig_uri); if (strcmp(old_uri_str, uri->str)) { log_debug(q->client->log, "HTTP redirect override: %s %s", method, http_uri_str(uri)); } } /* Perform redirection */ http_query_reset(q); q->method = method; q->uri = uri; http_query_submit(q, q->callback); return NULL; } /* Check if I/O error should be ignored for this query */ static bool http_query_ignore_error (http_query *q) { int status; /* Headers must be received */ if (!q->http_headers_received) { return false; } /* Depending on HTTP status, response body may be irrelevant */ status = q->http_parser.status_code; switch (status / 100) { case 1: case 3: case 4: case 5: return true; } /* If `no_need_response_body' is set, ignore the error */ return q->no_need_response_body; } /* Complete query processing */ static void http_query_complete (http_query *q, error err) { http_client *client = q->client; /* Make sure latest response header field is terminated */ http_hdr_on_header_value(&q->http_parser, "", 0); /* Unlink query from a client */ ll_del(&q->chain); /* In some cases, I/O error can be ignored. Check for that */ if (err != NULL && http_query_ignore_error(q)) { log_debug(client->log, "HTTP %s %s: %s (ignored)", q->method, http_uri_str(q->uri), ESTRING(err)); err = NULL; } /* Issue log messages */ q->err = err; if (err != NULL) { log_debug(client->log, "HTTP %s %s: %s", q->method, http_uri_str(q->uri), http_query_status_string(q)); } else { log_debug(client->log, "HTTP %s %s: %d %s", q->method, http_uri_str(q->uri), http_query_status(q), http_query_status_string(q)); } trace_http_query_hook(log_ctx_trace(client->log), q); /* Handle redirection */ if (err == NULL) { const char *method = http_query_redirect_method(q); if (method != NULL) { q->err = err = http_query_redirect(q, method); if (err == NULL) { return; } } log_debug(client->log, "HTTP %s %s: %s", q->method, http_uri_str(q->uri), http_query_status_string(q)); } /* Restore original method and URI, modified in case of redirection */ if (q->orig_uri != NULL) { q->real_uri = q->uri; q->uri = q->orig_uri; q->method = q->orig_method; q->orig_uri = NULL; q->orig_method = NULL; } /* Call user callback */ if (err != NULL && q->onerror != NULL) { q->onerror(client->ptr, err); } else if (q->callback != NULL) { q->callback(client->ptr, q); } http_query_free(q); } /* HTTP parser on_body callback */ static int http_query_on_body_callback (http_parser *parser, const char *data, size_t size) { http_query *q = OUTER_STRUCT(parser, http_query, http_parser); if (size == 0) { return 0; /* Just in case */ } if (q->response_data == NULL) { q->response_data = http_data_new(NULL, NULL, 0); } if (!http_data_append(q->response_data, data, size)) { q->err = ERROR_ENOMEM; } return q->err ? 1 : 0; } /* HTTP parser on_headers_complete callback */ static int http_query_on_headers_complete (http_parser *parser) { http_query *q = OUTER_STRUCT(parser, http_query, http_parser); if (http_query_redirect_method(q) == NULL) { log_debug(q->client->log, "HTTP %s %s: got response headers (%d)", q->method, http_uri_str(q->uri), http_query_status(q)); q->http_headers_received = true; if (q->onrxhdr != NULL) { q->onrxhdr(q->client->ptr, q); } } return 0; } /* HTTP parser on_message_complete callback */ static int http_query_on_message_complete (http_parser *parser) { http_query *q = OUTER_STRUCT(parser, http_query, http_parser); if (q->response_data != NULL) { const char *content_type; content_type = http_query_get_response_header(q, "Content-Type"); if (content_type != NULL) { http_data_set_content_type(q->response_data, content_type); q->err = http_multipart_parse( &q->response_multipart, q->client->log, q->response_data, content_type); } } q->http_parser_done = true; return q->err ? 1 : 0; } /* HTTP parser callbacks */ static http_parser_settings http_query_callbacks = { .on_header_field = http_hdr_on_header_field, .on_header_value = http_hdr_on_header_value, .on_body = http_query_on_body_callback, .on_headers_complete = http_query_on_headers_complete, .on_message_complete = http_query_on_message_complete }; /* Set http_query::fdpoll event mask */ static void http_query_fdpoll_set_mask (http_query *q, ELOOP_FDPOLL_MASK mask) { ELOOP_FDPOLL_MASK old_mask = eloop_fdpoll_set_mask(q->fdpoll, mask); log_debug(q->client->log, "HTTP fdpoll: %s -> %s", eloop_fdpoll_mask_str(old_mask), eloop_fdpoll_mask_str(mask)); } /* http_query::fdpoll callback */ static void http_query_fdpoll_callback (int fd, void *data, ELOOP_FDPOLL_MASK mask) { http_query *q = data; size_t len = mem_len(q->rq_buf) - q->rq_off; ssize_t rc; (void) fd; (void) mask; if (q->handshake) { rc = gnutls_handshake(q->tls); if (rc < 0) { error err = http_query_sock_err(q, rc); if (err == NULL) { return; } log_debug(q->client->log, "HTTP %s: gnutls_handshake(): %s", q->straddr.text, ESTRING(err)); /* TLS handshake failed, try another address, if any */ http_query_disconnect(q); q->addr_next = q->addr_next->ai_next; http_query_connect(q, err); return; } log_debug(q->client->log, "HTTP done TLS handshake"); q->handshake = false; http_query_fdpoll_set_mask(q, ELOOP_FDPOLL_BOTH); } else if (q->sending) { rc = http_query_sock_send(q, q->rq_buf + q->rq_off, len); if (rc > 0) { log_debug(q->client->log, "HTTP %d bytes sent", (int) rc); trace_hexdump(log_ctx_trace(q->client->log), '>', q->rq_buf + q->rq_off, rc); } if (rc < 0) { error err = http_query_sock_err(q, rc); if (err == NULL) { return; } log_debug(q->client->log, "HTTP %s: send(): %s", q->straddr.text, ESTRING(err)); http_query_disconnect(q); if (q->rq_off == 0) { /* None sent, try another address, if any */ q->addr_next = q->addr_next->ai_next; http_query_connect(q, err); } else { /* Sending started and failed */ http_query_complete(q, err); } return; } q->rq_off += rc; if (q->rq_off == mem_len(q->rq_buf)) { log_debug(q->client->log, "HTTP done request sending"); q->sending = false; http_query_fdpoll_set_mask(q, ELOOP_FDPOLL_BOTH); /* Initialize HTTP parser */ http_parser_init(&q->http_parser, HTTP_RESPONSE); q->http_parser.data = &q->response_header; } } else { static char io_buf[HTTP_IOBUF_SIZE]; rc = http_query_sock_recv(q, io_buf, sizeof(io_buf)); if (rc > 0) { log_debug(q->client->log, "HTTP %d bytes received", (int) rc); trace_hexdump(log_ctx_trace(q->client->log), '<', io_buf, rc); } if (rc < 0) { error err = http_query_sock_err(q, rc); if (err != NULL) { http_query_complete(q, err); } return; } if (rc == 0) { log_debug(q->client->log, "HTTP end of input"); } http_parser_execute(&q->http_parser, &http_query_callbacks, io_buf, rc); if (q->http_parser.http_errno != HPE_OK) { error err = q->err; if (err == NULL) { err = ERROR(http_errno_description(q->http_parser.http_errno)); } http_query_complete(q, err); } else if (q->http_parser_done) { log_debug(q->client->log, "HTTP done response reception"); http_query_complete(q, NULL); } else if (rc == 0) { error err = ERROR("connection closed by device"); http_query_complete(q, err); } } } /* Try to connect to the next address. The err parameter is a query * completion error in a case there are no more addresses to try */ static void http_query_connect (http_query *q, error err) { int rc; /* Skip invalid addresses. Check that we have address to try */ AGAIN: while (q->addr_next != NULL && q->addr_next->ai_family != AF_INET && q->addr_next->ai_family != AF_INET6 && q->addr_next->ai_family != AF_UNIX) { q->addr_next = q->addr_next->ai_next; } if (q->addr_next == NULL) { http_query_complete(q, err); return; } q->straddr = ip_straddr_from_sockaddr(q->addr_next->ai_addr, true); log_debug(q->client->log, "HTTP trying %s", q->straddr.text); /* Create socket and try to connect */ log_assert(q->client->log, q->sock < 0); q->sock = socket(q->addr_next->ai_family, q->addr_next->ai_socktype | SOCK_NONBLOCK | SOCK_CLOEXEC, q->addr_next->ai_protocol); if (q->sock == -1) { err = ERROR(strerror(errno)); log_debug(q->client->log, "HTTP %s: socket(): %s", q->straddr.text, ESTRING(err)); q->addr_next = q->addr_next->ai_next; goto AGAIN; } do { rc = connect(q->sock, q->addr_next->ai_addr, q->addr_next->ai_addrlen); } while (rc < 0 && errno == EINTR); if (rc < 0 && errno != EINPROGRESS) { err = ERROR(strerror(errno)); log_debug(q->client->log, "HTTP %s: connect(): %s", q->straddr.text, ESTRING(err)); http_query_disconnect(q); q->addr_next = q->addr_next->ai_next; goto AGAIN; } /* Setup TLS, if required */ if (q->uri->scheme == HTTP_SCHEME_HTTPS) { int rc = gnutls_init(&q->tls, GNUTLS_CLIENT | GNUTLS_NONBLOCK | GNUTLS_NO_SIGNAL); if (rc == GNUTLS_E_SUCCESS) { rc = gnutls_set_default_priority(q->tls); } if (rc == GNUTLS_E_SUCCESS) { rc = gnutls_credentials_set(q->tls, GNUTLS_CRD_CERTIFICATE, gnutls_cred); } if (rc != GNUTLS_E_SUCCESS) { err = ERROR(gnutls_strerror(rc)); http_query_disconnect(q); http_query_complete(q, err); return; } gnutls_transport_set_int(q->tls, q->sock); } /* Create fdpoll, and we are done */ q->fdpoll = eloop_fdpoll_new(q->sock, http_query_fdpoll_callback, q); if (q->tls != NULL) { q->handshake = true; } q->sending = true; http_query_fdpoll_set_mask(q, ELOOP_FDPOLL_WRITE); } /* Close connection to the server, if any */ static void http_query_disconnect (http_query *q) { if (q->fdpoll != NULL) { eloop_fdpoll_free(q->fdpoll); q->fdpoll = NULL; } if (q->tls != NULL) { gnutls_deinit(q->tls); q->tls = NULL; } if (q->sock >= 0) { close(q->sock); q->sock = -1; } } /* Send data to socket (either via TCP or TLS) * On a error, returns negative error code. Use * http_query_sock_err() to decode it */ static ssize_t http_query_sock_send (http_query *q, const void *data, size_t size) { ssize_t rc; if (q->tls == NULL) { rc = send(q->sock, data, size, MSG_NOSIGNAL); if (rc < 0) { rc = -errno; } } else { rc = gnutls_record_send(q->tls, data, size); if (rc < 0) { gnutls_record_discard_queued(q->tls); } } return rc; } /* Recv data from socket (either via TCP or TLS) */ static ssize_t http_query_sock_recv (http_query *q, void *data, size_t size) { ssize_t rc; if (q->tls == NULL) { rc = recv(q->sock, data, size, MSG_NOSIGNAL); if (rc < 0) { rc = -errno; } } else { rc = gnutls_record_recv(q->tls, data, size); } return rc; } /* Get socket error. May return NULL if last operation * has failed in recoverable manner */ static error http_query_sock_err (http_query *q, int rc) { ELOOP_FDPOLL_MASK mask = 0; error err = NULL; if (q->tls == NULL) { rc = -rc; switch (rc) { case EINTR: break; case EWOULDBLOCK: mask = q->sending ? ELOOP_FDPOLL_WRITE : ELOOP_FDPOLL_READ; break; default: err = ERROR(strerror(errno)); } } else { switch (rc) { case GNUTLS_E_INTERRUPTED: break; case GNUTLS_E_AGAIN: mask = gnutls_record_get_direction(q->tls) ? ELOOP_FDPOLL_WRITE : ELOOP_FDPOLL_READ; break; default: if (gnutls_error_is_fatal(rc)) { err = ERROR(gnutls_strerror(rc)); } } } if (mask != 0) { http_query_fdpoll_set_mask(q, mask); } return err; } /* Start query processing. Called via eloop_call() */ static void http_query_start_processing (void *p) { http_query *q = (http_query*) p; http_uri_field field; char *host, *port; const char *path; struct addrinfo hints; int rc; /* Get host name from the URI */ field = http_uri_field_get(q->uri, UF_HOST); host = alloca(field.len + 1); memcpy(host, field.str, field.len); host[field.len] = '\0'; http_uri_unescape_host(host); /* Get port name from the URI */ if (http_uri_field_nonempty(q->uri, UF_PORT)) { field = http_uri_field_get(q->uri, UF_PORT); port = alloca(field.len + 1); memcpy(port, field.str, field.len); port[field.len] = '\0'; } else { port = q->uri->scheme == HTTP_SCHEME_HTTP ? "80" : "443"; } /* Lookup target addresses */ if (q->uri->scheme != HTTP_SCHEME_UNIX) { log_debug(q->client->log, "HTTP resolving %s %s", host, port); memset(&hints, 0, sizeof(hints)); hints.ai_flags = AI_ADDRCONFIG; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; q->addrs_freeaddrinfo = true; rc = getaddrinfo(host, port, &hints, &q->addrs); if (rc != 0) { http_query_complete(q, ERROR(gai_strerror(rc))); return; } } else { struct sockaddr_un *addr; size_t pathlen = strlen(conf.socket_dir) + 1 /* for / */ + strlen(host); char *path = alloca(pathlen + 1); sprintf(path, "%s/%s", conf.socket_dir, host); log_debug(q->client->log, "connecting to local socket %s", path); q->addrs_freeaddrinfo = false; q->addrs = mem_new(struct addrinfo, 1); q->addrs->ai_family = AF_UNIX; q->addrs->ai_socktype = SOCK_STREAM; q->addrs->ai_protocol = 0; addr = mem_new(struct sockaddr_un, 1); addr->sun_family = AF_UNIX; strncpy(addr->sun_path, path, sizeof(addr->sun_path)-1); q->addrs->ai_addrlen = sizeof(struct sockaddr_un); q->addrs->ai_addr = (struct sockaddr *)addr; if (pathlen >= sizeof(addr->sun_path)) { http_query_complete(q, ERROR("Socket path is too long.")); return; } } q->addr_next = q->addrs; /* Set Host: header, if not set by user */ if (http_hdr_lookup(&q->request_header, "Host") == NULL) { q->host_inserted = true; http_query_set_host(q); } /* Obtain path. Note, URL format allows path to be empty, * while HTTP request requires non-empty string */ path = http_uri_get_path(q->uri); if (*path == '\0') { path = "/"; } /* Format HTTP request */ str_trunc(q->rq_buf); q->rq_buf = str_append_printf(q->rq_buf, "%s %s HTTP/1.1\r\n", q->method, path); if (q->request_data != NULL) { char buf[64]; sprintf(buf, "%zd", q->request_data->size); http_hdr_set(&q->request_header, "Content-Length", buf); } q->rq_buf = http_hdr_write(&q->request_header, q->rq_buf); if (q->request_data != NULL) { q->rq_buf = str_append_mem(q->rq_buf, q->request_data->bytes, q->request_data->size); } /* Connect to the host */ http_query_connect(q, ERROR("no host addresses available")); } /* Submit the query. * * When query is finished, callback will be called. After return from * callback, memory, owned by http_query will be invalidated */ void http_query_submit (http_query *q, void (*callback)(void *ptr, http_query *q)) { q->callback = callback; /* Issue log message, set timestamp and start timeout timer */ log_debug(q->client->log, "HTTP %s %s", q->method, http_uri_str(q->uri)); if (!q->submitted) { q->submitted = true; q->timestamp = timestamp_now(); if (q->timeout_value >= 0) { http_query_timeout(q, q->timeout_value); } } /* Submit the query */ log_assert(q->client->log, q->sock == -1); ll_push_end(&q->client->pending, &q->chain); q->eloop_callid = eloop_call(http_query_start_processing, q); } /* Cancel unfinished http_query. Callback will not be called and * memory owned by the http_query will be released */ static void http_query_cancel (http_query *q) { log_debug(q->client->log, "HTTP %s %s: Cancelled", q->method, http_uri_str(q->uri)); ll_del(&q->chain); eloop_call_cancel(q->eloop_callid); http_query_free(q); } /* Get http_query timestamp. Timestamp is set when query is * submitted. And this function should not be called before * http_query_submit() */ timestamp http_query_timestamp (const http_query *q) { return q->timestamp; } /* Set uintptr_t parameter, associated with query. * Completion callback may later use http_query_get_uintptr() * to fetch this value */ void http_query_set_uintptr (http_query *q, uintptr_t u) { q->uintptr = u; } /* Get uintptr_t parameter, previously set by http_query_set_uintptr() */ uintptr_t http_query_get_uintptr (http_query *q) { return q->uintptr; } /* Get query error, if any * * Both transport errors and erroneous HTTP response codes * considered as errors here */ error http_query_error (const http_query *q) { if (q->err == NULL) { int status = http_query_status(q); if (200 <= status && status < 300) { return NULL; } } return ERROR(http_query_status_string(q)); } /* Get query transport error, if any * * Only transport errors considered errors here */ error http_query_transport_error (const http_query *q) { return q->err; } /* Get HTTP status code. Code not available, if query finished * with transport error */ int http_query_status (const http_query *q) { log_assert(q->client->log, q->err == NULL); return q->http_parser.status_code; } /* Get HTTP status string */ const char* http_query_status_string (const http_query *q) { if (q->err != NULL) { return ESTRING(q->err); } return http_status_str(q->http_parser.status_code); } /* Get query URI * * It works as http_query_orig_uri() before query is submitted * or after it is completed, and as http_query_real_uri() in * between * * This function is deprecated, use http_query_orig_uri() * or http_query_real_uri() instead */ http_uri* http_query_uri (const http_query *q) { return q->uri; } /* Get original URI (the same as used when http_query was created) */ http_uri* http_query_orig_uri (const http_query *q) { return q->orig_uri ? q->orig_uri : q->uri; } /* Get real URI, that can differ from the requested URI * in a case of HTTP redirection */ http_uri* http_query_real_uri (const http_query *q) { return q->real_uri ? q->real_uri : q->uri; } /* Get query method */ const char* http_query_method (const http_query *q) { return q->method; } /* Set request header */ void http_query_set_request_header (http_query *q, const char *name, const char *value) { http_hdr_set(&q->request_header, name, value); } /* Get request header */ const char* http_query_get_request_header (const http_query *q, const char *name) { return http_hdr_get(&q->request_header, name); } /* Get response header */ const char* http_query_get_response_header(const http_query *q, const char *name) { return http_hdr_get(&q->response_header, name); } /* Get request data */ http_data* http_query_get_request_data (const http_query *q) { return q->request_data ? q->request_data : &http_data_empty; } /* Get request data */ http_data* http_query_get_response_data (const http_query *q) { return q->response_data ? q->response_data : &http_data_empty; } /* Get multipart response bodies. For non-multipart response * returns NULL */ static http_multipart* http_query_get_mp_response (const http_query *q) { return q->response_multipart; } /* Get count of parts of multipart response */ int http_query_get_mp_response_count (const http_query *q) { http_multipart *mp = http_query_get_mp_response(q); return mp ? mp->count : 0; } /* Get data of Nth part of multipart response */ http_data* http_query_get_mp_response_data (const http_query *q, int n) { http_multipart *mp = http_query_get_mp_response(q); if (mp == NULL || n < 0 || n >= mp->count) { return NULL; } return mp->bodies[n]; } /* Call callback for each request header */ void http_query_foreach_request_header (const http_query *q, void (*callback)(const char *name, const char *value, void *ptr), void *ptr) { hdr_for_each(&q->request_header, callback, ptr); } /* Call callback for each response header */ void http_query_foreach_response_header (const http_query *q, void (*callback)(const char *name, const char *value, void *ptr), void *ptr) { hdr_for_each(&q->response_header, callback, ptr); } /* Decode response part of the query. * This function is intended for testing purposes, not for regular use */ error http_query_test_decode_response (http_query *q, const void *data, size_t size) { http_parser_execute(&q->http_parser, &http_query_callbacks, data, size); if (q->http_parser.http_errno == HPE_OK && !q->http_parser_done) { http_parser_execute(&q->http_parser, &http_query_callbacks, data, 0); } if (q->http_parser.http_errno != HPE_OK) { if (q->err != NULL) { return q->err; } return ERROR(http_errno_description(q->http_parser.http_errno)); } if (!q->http_parser_done) { return ERROR("truncated response"); } return NULL; } /******************** HTTP initialization & cleanup ********************/ /* Initialize HTTP client */ SANE_Status http_init (void) { int rc = gnutls_certificate_allocate_credentials(&gnutls_cred); return rc == GNUTLS_E_SUCCESS ? SANE_STATUS_GOOD : SANE_STATUS_NO_MEM; } /* Initialize HTTP client */ void http_cleanup (void) { if (gnutls_cred != NULL) { gnutls_certificate_free_credentials(gnutls_cred); gnutls_cred = NULL; } } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-id.c000066400000000000000000000154061500411437100164250ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Routines for SANE options handling */ #include "airscan.h" #include /******************** Generic functions for IDs ********************/ /* Name/value mapping entry */ typedef struct { int id; const char *name; } id_name_table; /* Get name by ID. For unknown ID returns NULL */ static const char* id_name (int id, const id_name_table *table) { int i; for (i = 0; table[i].name != NULL; i ++) { if (table[i].id == id) { return table[i].name; } } return NULL; } /* Get ID by name. For unknown name returns -1 */ static int id_by_name (const char *name, int (*cmp) (const char *s1, const char *s2), const id_name_table *table) { int i; for (i = 0; table[i].name != NULL; i ++) { if (!cmp(name, table[i].name)) { return table[i].id; } } return -1; } /******************** ID_PROTO ********************/ /* id_proto_name_table table represents name mapping for ID_PROTO */ static id_name_table id_proto_name_table[] = { {ID_PROTO_ESCL, "eSCL"}, {ID_PROTO_WSD, "WSD"}, {-1, NULL} }; /* id_proto_name returns protocol name * For unknown ID returns NULL */ const char* id_proto_name (ID_PROTO proto) { return id_name(proto, id_proto_name_table); } /* id_proto_by_name returns protocol identifier by name * For unknown name returns ID_PROTO_UNKNOWN */ ID_PROTO id_proto_by_name (const char* name) { return id_by_name(name, strcasecmp, id_proto_name_table); } /******************** ID_SOURCE ********************/ /* id_source_sane_name_table represents ID_SOURCE to * SANE name mapping */ static id_name_table id_source_sane_name_table[] = { {ID_SOURCE_PLATEN, OPTVAL_SOURCE_PLATEN}, {ID_SOURCE_ADF_SIMPLEX, OPTVAL_SOURCE_ADF_SIMPLEX}, {ID_SOURCE_ADF_DUPLEX, OPTVAL_SOURCE_ADF_DUPLEX}, {-1, NULL} }; /* id_source_sane_name returns SANE name for the source * For unknown ID returns NULL */ const char* id_source_sane_name (ID_SOURCE id) { return id_name(id, id_source_sane_name_table); } /* id_source_by_sane_name returns ID_SOURCE by its SANE name * For unknown name returns ID_SOURCE_UNKNOWN */ ID_SOURCE id_source_by_sane_name (const char *name) { return id_by_name(name, strcasecmp, id_source_sane_name_table); } /******************** ID_COLORMODE ********************/ /* id_colormode_sane_name_table represents ID_COLORMODE to * SANE name mapping */ static id_name_table id_colormode_sane_name_table[] = { {ID_COLORMODE_BW1, SANE_VALUE_SCAN_MODE_HALFTONE}, {ID_COLORMODE_GRAYSCALE, SANE_VALUE_SCAN_MODE_GRAY}, {ID_COLORMODE_COLOR, SANE_VALUE_SCAN_MODE_COLOR}, {-1, NULL} }; /* id_colormode_sane_name returns SANE name for the color mode * For unknown ID returns NULL */ const char* id_colormode_sane_name (ID_COLORMODE id) { return id_name(id, id_colormode_sane_name_table); } /* id_colormode_by_sane_name returns ID_COLORMODE nu its SANE name * For unknown name returns ID_COLORMODE_UNKNOWN */ ID_COLORMODE id_colormode_by_sane_name (const char *name) { return id_by_name(name, strcasecmp, id_colormode_sane_name_table); } /******************** ID_FORMAT ********************/ /* id_format_mime_name_table represents ID_FORMAT to * MIME name mapping */ static id_name_table id_format_mime_name_table[] = { {ID_FORMAT_JPEG, "image/jpeg"}, {ID_FORMAT_TIFF, "image/tiff"}, {ID_FORMAT_PNG, "image/png"}, {ID_FORMAT_PDF, "application/pdf"}, {ID_FORMAT_BMP, "application/bmp"}, {-1, NULL} }; /* id_format_mime_name returns MIME name for the image format */ const char* id_format_mime_name (ID_FORMAT id) { return id_name(id, id_format_mime_name_table); } /* id_format_by_mime_name returns ID_FORMAT by its MIME name * For unknown name returns ID_FORMAT_UNKNOWN */ ID_FORMAT id_format_by_mime_name (const char *name) { return id_by_name(name, strcasecmp, id_format_mime_name_table); } /* if_format_short_name returns short name for ID_FORMAT */ const char* id_format_short_name (ID_FORMAT id) { const char *mime = id_format_mime_name(id); const char *name = mime ? (strchr(mime, '/') + 1) : NULL; return name ? name : mime; } /******************** ID_SCANINTENT ********************/ /* id_scanintent_sane_name_table represents ID_SCANINTENT to * SANE name mapping */ static id_name_table id_scanintent_sane_name_table[] = { {ID_SCANINTENT_UNSET, "*unset*"}, {ID_SCANINTENT_AUTO, "Auto"}, {ID_SCANINTENT_DOCUMENT, "Document"}, {ID_SCANINTENT_TEXTANDGRAPHIC, "Text and Graphic"}, {ID_SCANINTENT_PHOTO, "Photo"}, {ID_SCANINTENT_PREVIEW, "Preview"}, {ID_SCANINTENT_OBJECT, "3D Object"}, {ID_SCANINTENT_BUSINESSCARD, "Business Card"}, {ID_SCANINTENT_HALFTONE, "Halftone"}, {-1, NULL} }; /* id_scanintent_sane_name returns SANE name for the scan intent * For unknown ID returns NULL */ const char* id_scanintent_sane_name (ID_SCANINTENT id) { return id_name(id, id_scanintent_sane_name_table); } /* id_scanintent_by_sane_name returns ID_SCANINTENT by its SANE name * For unknown name returns ID_SCANINTENT_UNKNOWN */ ID_SCANINTENT id_scanintent_by_sane_name (const char *name) { return id_by_name(name, strcasecmp, id_scanintent_sane_name_table); } /******************** ID_JUSTIFICATION ********************/ /* id_justification_sane_name_table represents ID_JUSTIFICATION to * SANE name mapping */ static id_name_table id_justification_sane_name_table[] = { {ID_JUSTIFICATION_LEFT, OPTVAL_JUSTIFICATION_LEFT}, {ID_JUSTIFICATION_CENTER, OPTVAL_JUSTIFICATION_CENTER}, {ID_JUSTIFICATION_RIGHT, OPTVAL_JUSTIFICATION_RIGHT}, {ID_JUSTIFICATION_TOP, OPTVAL_JUSTIFICATION_TOP}, {ID_JUSTIFICATION_BOTTOM, OPTVAL_JUSTIFICATION_BOTTOM}, }; /* id_justification_sane_name returns SANE name for the justification * For unknown ID returns NULL */ const char* id_justification_sane_name (ID_JUSTIFICATION id) { return id_name(id, id_justification_sane_name_table); } /******************** PROTO_OP ********************/ /* proto_op_name_table represents PROTO_OP to its * name mappind */ static id_name_table proto_op_name_table[] = { {PROTO_OP_NONE, "PROTO_OP_NONE"}, {PROTO_OP_PRECHECK, "PROTO_OP_PRECHECK"}, {PROTO_OP_SCAN, "PROTO_OP_SCAN"}, {PROTO_OP_LOAD, "PROTO_OP_LOAD"}, {PROTO_OP_CHECK, "PROTO_OP_CHECK"}, {PROTO_OP_CLEANUP, "PROTO_OP_CLEANUP"}, {PROTO_OP_FINISH, "PROTO_OP_FINISH"}, {-1, NULL} }; /* Get PROTO_OP name, for logging */ const char* proto_op_name (PROTO_OP op) { return id_name(op, proto_op_name_table); } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-image.c000066400000000000000000000037371500411437100171170ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Image decoding */ #include "airscan.h" #include /* image_format_magic defines magic bytes for detection of * particular format */ typedef struct { ID_FORMAT format; /* Detected format */ size_t off, len; /* Bytes offset and length within the image */ uint8_t bytes[16]; /* Bytes to match */ } image_format_magic; /* image_format_magic_tab contains magic bytes for known formats */ static image_format_magic image_format_magic_tab[] = { {ID_FORMAT_BMP, 0, 2, {'B', 'M'}}, {ID_FORMAT_JPEG, 0, 2, {0xff, 0xd8}}, {ID_FORMAT_PNG, 0, 8, {0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a}}, {ID_FORMAT_TIFF, 0, 4, {'I', 'I', '*', '\0'}}, {ID_FORMAT_TIFF, 0, 4, {'M', 'M', '\0', '*'}} }; /* image_format_match matches image against the magic * If image matches, it returns ID_FORMAT, as defined by magic. * Otherwise, it returns ID_FORMAT_UNKNOWN */ static ID_FORMAT image_format_match (const image_format_magic *magic, const void *data, size_t size) { int cmp; if (magic->off + magic->len > size) { return ID_FORMAT_UNKNOWN; } cmp = memcmp(((const char*) data) + magic->off, magic->bytes, magic->len); if (cmp == 0) { return magic->format; } return ID_FORMAT_UNKNOWN; } /* Detect image format by image data */ ID_FORMAT image_format_detect (const void *data, size_t size) { size_t max = sizeof(image_format_magic_tab) / sizeof(image_format_magic_tab[0]); size_t i; for (i = 0; i < max; i ++) { const image_format_magic *magic = &image_format_magic_tab[i]; ID_FORMAT format; format = image_format_match(magic, data, size); if (format != ID_FORMAT_UNKNOWN) { return format; } } return ID_FORMAT_UNKNOWN; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-inifile.c000066400000000000000000000307511500411437100174500ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * .INI file parser */ #include "airscan.h" #include #include /* Open the .INI file */ inifile* inifile_open (const char *name) { FILE *fp; inifile *file; fp = fopen(name, "r"); if (fp == NULL) { return NULL; } file = mem_new(inifile, 1); file->fp = fp; file->file = str_dup(name); file->line = 1; file->tk_buffer = str_new(); file->buffer = str_new(); file->section = str_new(); file->variable = str_new(); file->value = str_new(); return file; } /* Close the .INI file */ void inifile_close (inifile *file) { fclose(file->fp); mem_free((char*) file->file); mem_free(file->tk_buffer); mem_free(file->tk_offsets); mem_free(file->buffer); mem_free(file->section); mem_free(file->variable); mem_free(file->value); mem_free(file->record.tokv); mem_free(file); } /* Get next character from the file */ static inline int inifile_getc (inifile *file) { int c = getc(file->fp); if (c == '\n') { file->line ++; } return c; } /* Push character back to stream */ static inline void inifile_ungetc (inifile *file, int c) { if (c == '\n') { file->line --; } ungetc(c, file->fp); } /* Get next non-space character from the file */ static inline int inifile_getc_nonspace (inifile *file) { int c; while ((c = inifile_getc(file)) != EOF && safe_isspace(c)) ; return c; } /* Read until new line or EOF */ static inline int inifile_getc_nl (inifile *file) { int c; while ((c = inifile_getc(file)) != EOF && c != '\n') ; return c; } /* Check for commentary character */ static inline bool inifile_iscomment (int c) { return c == ';' || c == '#'; } /* Check for octal digit */ static inline bool inifile_isoctal (int c) { return '0' <= c && c <= '7'; } /* Check for token-breaking character */ static inline bool inifile_istkbreaker (int c) { return c == ','; } /* Translate hexadecimal digit character to its integer value */ static inline unsigned int inifile_hex2int (int c) { if (isdigit(c)) { return c - '0'; } else { return safe_toupper(c) - 'A' + 10; } } /* Reset tokeniser */ static inline void inifile_tk_reset (inifile *file) { file->tk_open = false; str_trunc(file->tk_buffer); file->tk_count = 0; } /* Push token to token array */ static void inifile_tk_array_push (inifile *file) { file->tk_offsets = mem_resize(file->tk_offsets, file->tk_count + 1, 0); file->tk_offsets[file->tk_count ++] = mem_len(file->tk_buffer); } /* Export token array to file->record */ static void inifile_tk_array_export (inifile *file) { unsigned int i; file->record.tokv = mem_resize(file->record.tokv, file->tk_count, 0); file->record.tokc = file->tk_count; for (i = 0; i < file->tk_count; i ++) { const char *token; token = file->tk_buffer + file->tk_offsets[i]; file->record.tokv[i] = token; } } /* Open token if it is not opened yet */ static void inifile_tk_open (inifile *file) { if (!file->tk_open) { inifile_tk_array_push(file); file->tk_open = true; } } /* Close current token */ static void inifile_tk_close (inifile *file) { if (file->tk_open) { file->tk_buffer = str_append_c(file->tk_buffer, '\0'); file->tk_open = false; } } /* Append character to token */ static inline void inifile_tk_append (inifile *file, int c) { inifile_tk_open(file); file->tk_buffer = str_append_c(file->tk_buffer, c); } /* Strip trailing space in line currently being read */ static inline void inifile_strip_trailing_space (inifile *file, unsigned int *trailing_space) { size_t len = mem_len(file->buffer) - *trailing_space; file->buffer = str_resize(file->buffer, len); *trailing_space = 0; } /* Read string until either one of following is true: * - new line or EOF or read error is reached * - delimiter character is reached (if specified) * * If linecont parameter is true, '\' at the end of line treated * as line continuation character */ static int inifile_gets (inifile *file, char delimiter, bool linecont, bool *syntax) { int c; unsigned int accumulator = 0; unsigned int count = 0; unsigned int trailing_space = 0; enum { PRS_SKIP_SPACE, PRS_BODY, PRS_STRING, PRS_STRING_BSLASH, PRS_STRING_HEX, PRS_STRING_OCTAL, PRS_COMMENT } state = PRS_SKIP_SPACE; str_trunc(file->buffer); inifile_tk_reset(file); /* Parse the string */ for (;;) { c = inifile_getc(file); if (c == EOF || c == '\n') { break; } if ((state == PRS_BODY || state == PRS_SKIP_SPACE) && c == delimiter) { inifile_tk_close(file); break; } switch(state) { case PRS_SKIP_SPACE: if (safe_isspace(c)) { break; } state = PRS_BODY; /* Fall through... */ case PRS_BODY: if (c == '"') { state = PRS_STRING; inifile_tk_open(file); } else if (inifile_iscomment(c)) { state = PRS_COMMENT; } else if (c == '\\' && linecont) { int c2 = inifile_getc(file); if (c2 == '\n') { inifile_strip_trailing_space(file, &trailing_space); state = PRS_SKIP_SPACE; } else { inifile_ungetc(file, c); } } else { file->buffer = str_append_c(file->buffer, c); } if (state == PRS_BODY) { if (safe_isspace(c)) { trailing_space ++; inifile_tk_close(file); } else { trailing_space = 0; if (inifile_istkbreaker(c)) { inifile_tk_close(file); } else { inifile_tk_append(file, c); } } } else { inifile_strip_trailing_space(file, &trailing_space); } break; case PRS_STRING: if (c == '\\') { state = PRS_STRING_BSLASH; } else if (c == '"') { state = PRS_BODY; } else { file->buffer = str_append_c(file->buffer, c); inifile_tk_append(file, c); } break; case PRS_STRING_BSLASH: if (c == 'x' || c == 'X') { state = PRS_STRING_HEX; accumulator = count = 0; } else if (inifile_isoctal(c)) { state = PRS_STRING_OCTAL; accumulator = inifile_hex2int(c); count = 1; } else { switch (c) { case 'a': c = '\a'; break; case 'b': c = '\b'; break; case 'e': c = '\x1b'; break; case 'f': c = '\f'; break; case 'n': c = '\n'; break; case 'r': c = '\r'; break; case 't': c = '\t'; break; case 'v': c = '\v'; break; } file->buffer = str_append_c(file->buffer, c); inifile_tk_append(file, c); state = PRS_STRING; } break; case PRS_STRING_HEX: if (safe_isxdigit(c)) { if (count != 2) { accumulator = accumulator * 16 + inifile_hex2int(c); count ++; } } else { state = PRS_STRING; inifile_ungetc(file, c); } if (state != PRS_STRING_HEX) { file->buffer = str_append_c(file->buffer, accumulator); inifile_tk_append(file, accumulator); } break; case PRS_STRING_OCTAL: if (inifile_isoctal(c)) { accumulator = accumulator * 8 + inifile_hex2int(c); count ++; if (count == 3) { state = PRS_STRING; } } else { state = PRS_STRING; inifile_ungetc(file, c); } if (state != PRS_STRING_OCTAL) { file->buffer = str_append_c(file->buffer, accumulator); inifile_tk_append(file, accumulator); } break; case PRS_COMMENT: break; } } /* Remove trailing space, if any */ inifile_strip_trailing_space(file, &trailing_space); /* Set syntax error flag */ *syntax = false; if (state != PRS_SKIP_SPACE && state != PRS_BODY && state != PRS_COMMENT) { *syntax = true; } return c; } /* Finish reading the record. Performs common cleanup operations, * feels record structure etc */ static const inifile_record* inifile_read_finish (inifile *file, int last_char, INIFILE_RECORD rec_type) { file->record.type = rec_type; file->record.file = file->file; file->record.section = file->section; file->record.variable = file->record.value = NULL; if (rec_type == INIFILE_VARIABLE || rec_type == INIFILE_COMMAND) { inifile_tk_array_export(file); if (rec_type == INIFILE_VARIABLE) { file->record.variable = file->variable; file->record.value = file->value; } else { log_assert(NULL, file->record.tokc); file->record.variable = file->record.tokv[0]; file->record.tokc --; if (file->record.tokc) { memmove((void*) file->record.tokv, file->record.tokv + 1, sizeof(file->record.tokv[0]) * file->record.tokc); } } } else { file->record.tokc = 0; } if (last_char == '\n') { file->record.line = file->line - 1; } else { file->record.line = file->line; if (last_char != EOF) { inifile_getc_nl(file); } } return &file->record; } /* Read next record */ const inifile_record* inifile_read (inifile *file) { int c; bool syntax; c = inifile_getc_nonspace(file); while (inifile_iscomment(c)) { inifile_getc_nl(file); c = inifile_getc_nonspace(file); } if (c == EOF) { return NULL; } if (c == '[') { c = inifile_gets(file, ']', false, &syntax); if (c == ']' && !syntax) { file->section = str_assign(file->section, file->buffer); return inifile_read_finish(file, c, INIFILE_SECTION); } } else if (c != '=') { inifile_ungetc(file, c); c = inifile_gets(file, '=', false, &syntax); if(c == '=' && !syntax) { file->variable = str_assign(file->variable, file->buffer); c = inifile_gets(file, EOF, true, &syntax); if(!syntax) { file->value = str_assign(file->value, file->buffer); return inifile_read_finish(file, c, INIFILE_VARIABLE); } } else if (!syntax) { return inifile_read_finish(file, c, INIFILE_COMMAND); } } return inifile_read_finish(file, c, INIFILE_SYNTAX); } /* Match name of section of variable * - match is case-insensitive * - difference in amount of free space is ignored * - leading and trailing space is ignored */ bool inifile_match_name (const char *n1, const char *n2) { /* Skip leading space */ while (safe_isspace(*n1)) { n1 ++; } while (safe_isspace(*n2)) { n2 ++; } /* Perform the match */ while (*n1 && *n2) { if (safe_isspace(*n1)) { if (!safe_isspace(*n2)) { break; } do { n1 ++; } while (safe_isspace(*n1)); do { n2 ++; } while (safe_isspace(*n2)); } else if (safe_toupper(*n1) == safe_toupper(*n2)) { n1 ++, n2 ++; } else { break; } } /* Skip trailing space */ while (safe_isspace(*n1)) { n1 ++; } while (safe_isspace(*n2)) { n2 ++; } /* Check results */ return *n1 == '\0' && *n2 == '\0'; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-init.c000066400000000000000000000040341500411437100167670ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Initialization/cleanup */ #include "airscan.h" /* Saved init flags */ static AIRSCAN_INIT_FLAGS airscan_init_flags; /* Initialize airscan */ SANE_Status airscan_init (AIRSCAN_INIT_FLAGS flags, const char *log_msg) { SANE_Status status; /* Save init flags */ airscan_init_flags = flags; /* Initialize logging -- do it early */ log_init(); trace_init(); if (log_msg != NULL) { log_debug(NULL, "%s", log_msg); } if ((flags & AIRSCAN_INIT_NO_CONF) == 0) { conf_load(); } log_configure(); /* As soon, as configuration is available */ /* Initialize all parts */ status = eloop_init(); if (status == SANE_STATUS_GOOD) { status = rand_init(); } if (status == SANE_STATUS_GOOD) { status = http_init(); } if (status == SANE_STATUS_GOOD) { status = netif_init(); } if (status == SANE_STATUS_GOOD) { status = zeroconf_init(); } if (status == SANE_STATUS_GOOD) { status = mdns_init(); } if (status == SANE_STATUS_GOOD) { status = wsdd_init(); } if (status != SANE_STATUS_GOOD) { airscan_cleanup(NULL); } else if ((flags & AIRSCAN_INIT_NO_THREAD) == 0) { eloop_thread_start(); } return status; } /* Cleanup airscan * If log_msg is not NULL, it is written to the log as late as possible */ void airscan_cleanup (const char *log_msg) { mdns_cleanup(); wsdd_cleanup(); zeroconf_cleanup(); netif_cleanup(); http_cleanup(); rand_cleanup(); eloop_cleanup(); if (log_msg != NULL) { log_debug(NULL, "%s", log_msg); } conf_unload(); trace_cleanup(); log_cleanup(); /* Must be the last thing to do */ } /* Get init flags from the airscan_init call */ AIRSCAN_INIT_FLAGS airscan_get_init_flags (void) { return airscan_init_flags; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-ip.c000066400000000000000000000301331500411437100164330ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Utility function for IP addresses */ #include "airscan.h" #include #include #include #include #include #if defined(OS_HAVE_ENDIAN_H) # include #elif defined(OS_HAVE_SYS_ENDIAN_H) # include #endif /* Format ip_straddr from IP address (struct in_addr or struct in6_addr) * af must be AF_INET or AF_INET6 */ ip_straddr ip_straddr_from_ip (int af, const void *addr) { ip_straddr straddr = {""}; inet_ntop(af, addr, straddr.text, sizeof(straddr.text)); return straddr; } /* Format ip_straddr from struct sockaddr. * AF_INET, AF_INET6, and AF_UNIX are supported * * If `withzone' is true, zone suffix will be appended, when appropriate */ ip_straddr ip_straddr_from_sockaddr (const struct sockaddr *addr, bool withzone) { return ip_straddr_from_sockaddr_dport(addr, -1, withzone, false); } /* Format ip_straddr from struct sockaddr. * AF_INET, AF_INET6, and AF_UNIX are supported * * Port will not be appended, if it matches provided default port * * If `withzone' is true, zone suffix will be appended, when appropriate * * If `withlocalhost` is true and address is 127.0.0.1 or ::1, * "localhost" will be used instead of the IP address literal */ ip_straddr ip_straddr_from_sockaddr_dport (const struct sockaddr *addr, int dport, bool withzone, bool withlocalhost) { ip_straddr straddr = {""}; struct sockaddr_in *addr_in = (struct sockaddr_in*) addr; struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6*) addr; struct sockaddr_un *addr_un = (struct sockaddr_un*) addr; uint16_t port = 0; switch (addr->sa_family) { case AF_INET: if (withlocalhost && ip_is_loopback(AF_INET, &addr_in->sin_addr)) { strcpy(straddr.text, "localhost"); } else { inet_ntop(AF_INET, &addr_in->sin_addr, straddr.text, sizeof(straddr.text)); } port = addr_in->sin_port; break; case AF_INET6: if (withlocalhost && ip_is_loopback(AF_INET6, &addr_in6->sin6_addr)) { strcpy(straddr.text, "localhost"); } else { straddr.text[0] = '['; inet_ntop(AF_INET6, &addr_in6->sin6_addr, straddr.text + 1, sizeof(straddr.text) - 2); if (withzone && addr_in6->sin6_scope_id != 0 && ip_sockaddr_is_linklocal(addr)) { sprintf(straddr.text + strlen(straddr.text), "%%%d", addr_in6->sin6_scope_id); } strcat(straddr.text, "]"); } port = addr_in6->sin6_port; break; case AF_UNIX: strncpy(straddr.text, addr_un->sun_path, sizeof(straddr.text) - 1); straddr.text[sizeof(straddr.text)-1] = '\0'; break; } port = htons(port); if (port != dport && addr->sa_family != AF_UNIX) { sprintf(straddr.text + strlen(straddr.text), ":%d", port); } return straddr; } /* Check if address is link-local * af must be AF_INET or AF_INET6 */ bool ip_is_linklocal (int af, const void *addr) { if (af == AF_INET) { /* 169.254.0.0/16 */ const uint32_t *a = addr; return (ntohl(*a) & 0xffff0000) == 0xa9fe0000; } else { const uint8_t *a = addr; return a[0] == 0xfe && (a[1] & 0xc0) == 0x80; } } /* Check if sockaddr is link-local */ bool ip_sockaddr_is_linklocal (const struct sockaddr *addr) { struct sockaddr_in *addr_in = (struct sockaddr_in*) addr; struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6*) addr; switch (addr->sa_family) { case AF_INET: return ip_is_linklocal(AF_INET, &addr_in->sin_addr); case AF_INET6: return ip_is_linklocal(AF_INET6, &addr_in6->sin6_addr); } return false; } /* Check if address is loopback * af must be AF_INET or AF_INET6 */ bool ip_is_loopback (int af, const void *addr) { if (af == AF_INET) { /* 169.254.0.0/16 */ const uint32_t *a = addr; return ntohl(*a) == 0x7f000001; } else { static const uint8_t loopback[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }; return !memcmp(addr, loopback, 16); } } /* Format ip_addr into ip_straddr */ ip_straddr ip_addr_to_straddr (ip_addr addr, bool withzone) { ip_straddr straddr = {""}; struct sockaddr_in addr_in; struct sockaddr_in6 addr_in6; struct sockaddr *sockaddr = NULL; switch (addr.af) { case AF_INET: memset(&addr_in, 0, sizeof(addr_in)); addr_in.sin_family = AF_INET; addr_in.sin_addr = addr.ip.v4; sockaddr = (struct sockaddr*) &addr_in; break; case AF_INET6: memset(&addr_in6, 0, sizeof(addr_in6)); addr_in6.sin6_family = AF_INET6; addr_in6.sin6_addr = addr.ip.v6; addr_in6.sin6_scope_id = addr.ifindex; sockaddr = (struct sockaddr*) &addr_in6; break; } if (sockaddr != NULL) { straddr = ip_straddr_from_sockaddr_dport(sockaddr, 0, withzone, false); } return straddr; } /* Format ip_network into ip_straddr */ ip_straddr ip_network_to_straddr (ip_network net) { ip_straddr straddr = {""}; size_t len; inet_ntop(net.addr.af, &net.addr.ip, straddr.text, sizeof(straddr.text)); len = strlen(straddr.text); sprintf(straddr.text + len, "/%d", net.mask); return straddr; } /* Check if ip_network contains ip_addr */ bool ip_network_contains (ip_network net, ip_addr addr) { struct in_addr a4, m4; uint64_t a6[2], m6[2]; if (net.addr.af != addr.af) { return false; } switch (net.addr.af) { case AF_INET: a4.s_addr = net.addr.ip.v4.s_addr ^ addr.ip.v4.s_addr; m4.s_addr = htonl(0xffffffff << (32 - net.mask)); return (a4.s_addr & m4.s_addr) == 0; case AF_INET6: /* a6 = net.addr.ip.v6 ^ addr.ip.v6 */ memcpy(a6, &addr.ip.v6, 16); memcpy(m6, &net.addr.ip.v6, 16); a6[0] ^= m6[0]; a6[1] ^= m6[1]; /* Compute and apply netmask */ memset(m6, 0, 16); if (net.mask <= 64) { m6[0] = htobe64(UINT64_MAX << (64 - net.mask)); m6[1] = 0; } else { m6[0] = UINT64_MAX; m6[1] = htobe64(UINT64_MAX << (128 - net.mask)); } a6[0] &= m6[0]; a6[1] &= m6[1]; /* Check result */ return (a6[0] | a6[1]) == 0; } return false; } /* ip_addr_set represents a set of IP addresses */ struct ip_addrset { ip_addr *addrs; /* Addresses in the set */ }; /* Create new ip_addrset */ ip_addrset* ip_addrset_new (void) { ip_addrset *addrset = mem_new(ip_addrset, 1); addrset->addrs = mem_new(ip_addr, 0); return addrset; } /* Free ip_addrset */ void ip_addrset_free (ip_addrset *addrset) { mem_free(addrset->addrs); mem_free(addrset); } /* Find address index within a set. Returns -1 if address was not found */ static int ip_addrset_index (const ip_addrset *addrset, ip_addr addr) { size_t i, len = mem_len(addrset->addrs); for (i = 0; i < len; i ++) { if (ip_addr_equal(addrset->addrs[i], addr)) { return (int) i; } } return -1; } /* Check if address is in set */ bool ip_addrset_lookup (const ip_addrset *addrset, ip_addr addr) { return ip_addrset_index(addrset, addr) >= 0; } /* Add address to the set. Returns true, if address was * actually added, false if it was already in the set */ bool ip_addrset_add (ip_addrset *addrset, ip_addr addr) { if (ip_addrset_lookup(addrset, addr)) { return false; } ip_addrset_add_unsafe(addrset, addr); return true; } /* Add address to the set without checking for duplicates */ void ip_addrset_add_unsafe (ip_addrset *addrset, ip_addr addr) { size_t len = mem_len(addrset->addrs); addrset->addrs = mem_resize(addrset->addrs, len + 1, 0); addrset->addrs[len] = addr; } /* Del address from the set. */ void ip_addrset_del (ip_addrset *addrset, ip_addr addr) { int i = ip_addrset_index(addrset, addr); if (i >= 0) { size_t len = mem_len(addrset->addrs); size_t tail = len - (size_t) i - 1; if (tail != 0) { tail *= sizeof(*addrset->addrs); memmove(&addrset->addrs[i], &addrset->addrs[i + 1], tail); } mem_shrink(addrset->addrs, len - 1); } } /* Delete all addresses from the set */ void ip_addrset_purge (ip_addrset *addrset) { mem_shrink(addrset->addrs, 0); } /* Merge two sets: * addrset += addrset2 */ void ip_addrset_merge (ip_addrset *addrset, const ip_addrset *addrset2) { size_t i, len = mem_len(addrset2->addrs); for (i = 0; i < len; i ++) { ip_addrset_add(addrset, addrset2->addrs[i]); } } /* Get access to array of addresses in the set */ const ip_addr* ip_addrset_addresses (const ip_addrset *addrset, size_t *count) { *count = mem_len(addrset->addrs); return addrset->addrs; } /* Check if two address sets are intersecting */ bool ip_addrset_is_intersect (const ip_addrset *set, const ip_addrset *set2) { size_t i, len = mem_len(set->addrs); for (i = 0; i < len; i ++) { if (ip_addrset_lookup(set2, set->addrs[i])) { return true; } } return false; } /* Check if some of addresses in the address set is on the * given network */ bool ip_addrset_on_network (const ip_addrset *set, ip_network net) { size_t i, len = mem_len(set->addrs); for (i = 0; i < len; i ++) { if (ip_network_contains(net, set->addrs[i])) { return true; } } return false; } /* Check if address set has some addresses of the specified * address family */ bool ip_addrset_has_af (const ip_addrset *set, int af) { size_t i, len = mem_len(set->addrs); for (i = 0; i < len; i ++) { if (set->addrs[i].af == af) { return true; } } return false; } /* Compare two ip_addrs, for sorting in ip_addrset_friendly_str() */ static int ip_addrset_friendly_sort_cmp (const void *p1, const void *p2) { const ip_addr *a1 = (const ip_addr*) p1; const ip_addr *a2 = (const ip_addr*) p2; bool ll1 = ip_is_linklocal(a1->af, &a1->ip); bool ll2 = ip_is_linklocal(a2->af, &a2->ip); ip_straddr s1, s2; /* Prefer normal addresses, rather that link-local */ if (ll1 != ll2) { return ll1 ? 1 : -1; } /* Put IP4 addresses first, they tell more to humans */ if (a1->af != a2->af) { return a1->af == AF_INET6 ? 1 : -1; } /* Otherwise, sort lexicographically */ s1 = ip_addr_to_straddr(*a1, true); s2 = ip_addr_to_straddr(*a2, true); return strcmp(s1.text, s2.text); } /* Create user-friendly string out of set of addresses, containing * in the ip_addrset: * * addresses are sorted, IP4 addresses goes first * * link-local addresses are skipped, if there are non-link-local ones * * Caller must use mem_free to release the returned string when * it is not needed anymore */ char* ip_addrset_friendly_str (const ip_addrset *set, char *s) { size_t i, j, len = mem_len(set->addrs); ip_addr *addrs = alloca(sizeof(ip_addr) * len); /* Gather addresses */ for (i = j = 0; i < len; i ++) { ip_addr *addr = &set->addrs[i]; if (!ip_is_linklocal(addr->af, &addr->ip)) { addrs[j ++] = *addr; } } if (j != 0) { len = j; } else { memcpy(addrs, set->addrs, sizeof(ip_addr) * len); } /* Sort addresses */ qsort(addrs, len, sizeof(ip_addr), ip_addrset_friendly_sort_cmp); /* And now stringify */ for (i = 0; i < len; i ++) { ip_straddr str = ip_addr_to_straddr(addrs[i], true); if (i != 0) { s = str_append(s, ", "); } if (str.text[0] != '[') { s = str_append(s, str.text); } else { str.text[strlen(str.text) - 1] = '\0'; s = str_append(s, str.text + 1); } } return s; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-jpeg.c000066400000000000000000000135541500411437100167600ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * JPEG image decoder */ #include "airscan.h" #include #include #include /* JPEG image decoder */ typedef struct { image_decoder decoder; /* Base class */ struct jpeg_decompress_struct cinfo; /* libjpeg decoder */ struct jpeg_error_mgr jerr; /* libjpeg error manager */ jmp_buf jmpb; /* For longjmp from libjpeg */ char errbuf[ /* Error buffer */ JMSG_LENGTH_MAX + 16]; JDIMENSION num_lines; /* Num of lines left to read */ } image_decoder_jpeg; /* Free JPEG decoder */ static void image_decoder_jpeg_free (image_decoder *decoder) { image_decoder_jpeg *jpeg = (image_decoder_jpeg*) decoder; jpeg_destroy_decompress(&jpeg->cinfo); mem_free(jpeg); } /* Begin JPEG decoding */ static error image_decoder_jpeg_begin (image_decoder *decoder, const void *data, size_t size) { image_decoder_jpeg *jpeg = (image_decoder_jpeg*) decoder; int rc; if (!setjmp(jpeg->jmpb)) { jpeg_mem_src(&jpeg->cinfo, (unsigned char*) data, size); rc = jpeg_read_header(&jpeg->cinfo, true); if (rc != JPEG_HEADER_OK) { jpeg_abort((j_common_ptr) &jpeg->cinfo); return ERROR("JPEG: invalid header"); } if (jpeg->cinfo.num_components != 1) { jpeg->cinfo.out_color_space = JCS_RGB; } jpeg_start_decompress(&jpeg->cinfo); jpeg->num_lines = jpeg->cinfo.image_height; return NULL; } return ERROR(jpeg->errbuf); } /* Reset JPEG decoder */ static void image_decoder_jpeg_reset (image_decoder *decoder) { image_decoder_jpeg *jpeg = (image_decoder_jpeg*) decoder; jpeg_abort((j_common_ptr) &jpeg->cinfo); } /* Get bytes count per pixel */ static int image_decoder_jpeg_get_bytes_per_pixel (image_decoder *decoder) { image_decoder_jpeg *jpeg = (image_decoder_jpeg*) decoder; return jpeg->cinfo.num_components; } /* Get image parameters */ static void image_decoder_jpeg_get_params (image_decoder *decoder, SANE_Parameters *params) { image_decoder_jpeg *jpeg = (image_decoder_jpeg*) decoder; params->last_frame = SANE_TRUE; params->pixels_per_line = jpeg->cinfo.image_width; params->lines = jpeg->cinfo.image_height; params->depth = 8; if (jpeg->cinfo.num_components == 1) { params->format = SANE_FRAME_GRAY; params->bytes_per_line = params->pixels_per_line; } else { params->format = SANE_FRAME_RGB; params->bytes_per_line = params->pixels_per_line * 3; } } /* Set clipping window */ static error image_decoder_jpeg_set_window (image_decoder *decoder, image_window *win) { image_decoder_jpeg *jpeg = (image_decoder_jpeg*) decoder; /* Note, image clipping cannot be supported on rather * old libjpeg version (i.e., on Ubuntu 16.04, because * jpeg_crop_scanline() and jpeg_skip_scanlines() functions * are missed. The safe default is to update window to * match the entire image dimensions. */ #if 1 win->x_off = win->y_off = 0; win->wid = jpeg->cinfo.image_width; win->hei = jpeg->cinfo.image_height; return NULL; #else JDIMENSION x_off = win->x_off; JDIMENSION wid = win->wid; if (!setjmp(jpeg->jmpb)) { jpeg_crop_scanline(&jpeg->cinfo, &x_off, &wid); if (win->y_off > 0) { jpeg_skip_scanlines(&jpeg->cinfo, win->y_off); } jpeg->num_lines = win->hei; win->x_off = x_off; win->wid = wid; return NULL; } return ERROR(jpeg->errbuf); #endif } /* Read next line of image */ static error image_decoder_jpeg_read_line (image_decoder *decoder, void *buffer) { image_decoder_jpeg *jpeg = (image_decoder_jpeg*) decoder; JSAMPROW lines[1] = {buffer}; if (!jpeg->num_lines) { return ERROR("JPEG: end of file"); } if (!setjmp(jpeg->jmpb)) { if (jpeg_read_scanlines(&jpeg->cinfo, lines, 1) == 0) { return ERROR(jpeg->errbuf); } jpeg->num_lines --; return NULL; } return ERROR(jpeg->errbuf); } /* "Output error message" callback for JPEG decoder */ static void image_decoder_jpeg_output_message (j_common_ptr cinfo) { image_decoder_jpeg *jpeg = OUTER_STRUCT(cinfo, image_decoder_jpeg, cinfo); memcpy(jpeg->errbuf, "JPEG: ", 6); (*cinfo->err->format_message)(cinfo, jpeg->errbuf + 6); } /* error_exit callback for JPEG decoder. The default callback * terminates a program, which is not good for us */ static void image_decoder_jpeg_error_exit (j_common_ptr cinfo) { image_decoder_jpeg *jpeg = OUTER_STRUCT(cinfo, image_decoder_jpeg, cinfo); image_decoder_jpeg_output_message(cinfo); jpeg_abort(cinfo); longjmp(jpeg->jmpb, 1); } /* Create JPEG image decoder */ image_decoder* image_decoder_jpeg_new (void) { image_decoder_jpeg *jpeg = mem_new(image_decoder_jpeg, 1); jpeg->decoder.content_type = "image/jpeg"; jpeg->decoder.free = image_decoder_jpeg_free; jpeg->decoder.begin = image_decoder_jpeg_begin; jpeg->decoder.reset = image_decoder_jpeg_reset; jpeg->decoder.get_bytes_per_pixel = image_decoder_jpeg_get_bytes_per_pixel; jpeg->decoder.get_params = image_decoder_jpeg_get_params; jpeg->decoder.set_window = image_decoder_jpeg_set_window; jpeg->decoder.read_line = image_decoder_jpeg_read_line; jpeg->cinfo.err = jpeg_std_error(&jpeg->jerr); jpeg->jerr.output_message = image_decoder_jpeg_output_message; jpeg->jerr.error_exit = image_decoder_jpeg_error_exit; jpeg_create_decompress(&jpeg->cinfo); return &jpeg->decoder; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-log.c000066400000000000000000000137151500411437100166130ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Logging */ #include "airscan.h" #include #include #include #include #include /* Static variables */ static char *log_buffer; static bool log_configured; static uint64_t log_start_time; static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER; /* Get time for logging purposes */ static uint64_t log_get_time (void) { struct timespec tms; clock_gettime(CLOCK_MONOTONIC, &tms); return ((uint64_t) tms.tv_nsec) + 1000000000 * (uint64_t) tms.tv_sec; } /* Initialize logging * * No log messages should be generated before this call */ void log_init (void) { log_buffer = str_new(); log_configured = false; log_start_time = log_get_time(); } /* Cleanup logging * * No log messages should be generated after this call */ void log_cleanup (void) { mem_free(log_buffer); log_buffer = NULL; } /* Flush buffered log to file */ static void log_flush (void) { int rc = write(2, log_buffer, mem_len(log_buffer)); (void) rc; str_trunc(log_buffer); } /* Notify logger that configuration is loaded and * logger can configure itself * * This is safe to generate log messages before log_configure() * is called. These messages will be buffered, and after * logger is configured, either written or abandoned, depending * on configuration */ void log_configure (void) { log_configured = true; if (conf.dbg_enabled) { log_flush(); } else { str_trunc(log_buffer); } } /* Format time elapsed since logging began */ static void log_fmt_time (char *buf, size_t size) { uint64_t t = log_get_time() - log_start_time; int hour, min, sec, msec; sec = (int) (t / 1000000000); msec = ((int) (t % 1000000000)) / 1000000; hour = sec / 3600; sec = sec % 3600; min = sec / 60; sec = sec % 60; snprintf(buf, size, "%2.2d:%2.2d:%2.2d.%3.3d", hour, min, sec, msec); } /* log_ctx represents logging context */ struct log_ctx { const char *name; /* Log name */ trace *trace; /* Associated trace */ }; /* log_ctx_new creates new logging context * If parent != NULL, new logging context will have its own prefix, * but trace file will be inherited from parent */ log_ctx* log_ctx_new (const char *name, log_ctx *parent) { log_ctx *log = mem_new(log_ctx, 1); log->name = str_trim(str_dup(name)); if (parent != NULL) { log->trace = trace_ref(parent->trace); } else { log->trace = trace_open(name); } return log; } /* log_ctx_free destroys logging context */ void log_ctx_free (log_ctx *log) { trace_unref(log->trace); mem_free((char*) log->name); mem_free(log); } /* Get protocol trace associated with logging context */ trace* log_ctx_trace (log_ctx *log) { return log->trace; } /* Write a log message */ static void log_message (log_ctx *log, bool trace_only, bool force, const char *fmt, va_list ap) { trace *t = log ? log->trace : NULL; char msg[4096]; int len = 0, namelen = 0, required_bytes = 0; bool dont_log = trace_only || (log_configured && !conf.dbg_enabled && !force); /* If logs suppressed and trace not in use, we have nothing * to do */ if ((t == NULL) && dont_log) { return; } /* Format a log message */ if (log != NULL) { len += sprintf(msg, "%.64s: ", log->name); namelen = len; } required_bytes = vsnprintf(msg + len, sizeof(msg) - len, fmt, ap); /* vsnprintf returns the number of bytes required for the whole message, * even if that exceeds the buffer size. * If required_bytes exceeds space remaining in msg, we know msg is full. * Otherwise, we can increment len by required_bytes. */ if (required_bytes >= (int) sizeof(msg) - len) { len = sizeof(msg) - 1; } else { len += required_bytes; } while (len > 0 && isspace((unsigned char) msg[len-1])) { len --; } msg[len] = '\0'; /* Write to log */ if (!dont_log) { pthread_mutex_lock(&log_mutex); log_buffer = str_append(log_buffer, msg); log_buffer = str_append_c(log_buffer, '\n'); if ((log_configured && conf.dbg_enabled) || force) { log_flush(); } pthread_mutex_unlock(&log_mutex); } /* Write to trace */ if (t != NULL) { if (len > namelen) { char prefix[64]; log_fmt_time(prefix, sizeof(prefix)); trace_printf(t, "%s: %s", prefix, msg); } else { trace_printf(t, ""); } } } /* Write a debug message. */ void log_debug (log_ctx *log, const char *fmt, ...) { va_list ap; va_start(ap, fmt); log_message(log, false, false, fmt, ap); va_end(ap); } /* Write a protocol trace message */ void log_trace (log_ctx *log, const char *fmt, ...) { va_list ap; va_start(ap, fmt); log_message(log, true, false, fmt, ap); va_end(ap); } /* Write a block of data into protocol trace */ void log_trace_data (log_ctx *log, const char *content_type, const void *bytes, size_t size) { http_data data = { .content_type = content_type, .bytes = bytes, .size = size }; trace_dump_body(log->trace, &data); } /* Write an error message and terminate a program. */ void log_panic (log_ctx *log, const char *fmt, ...) { va_list ap; /* Note, log_buffer is not empty only if logger is not * configured yet, but there are pending debug messages. * At this case we discard these messages, but panic * message is written anyway */ pthread_mutex_lock(&log_mutex); str_trunc(log_buffer); pthread_mutex_unlock(&log_mutex); va_start(ap, fmt); log_message(log, false, true, fmt, ap); va_end(ap); abort(); } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-math.c000066400000000000000000000105441500411437100167600ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Miscellaneous mathematical functions */ #include "airscan.h" #pragma GCC diagnostic ignored "-Wunused-result" /* Find greatest common divisor of two positive integers */ SANE_Word math_gcd (SANE_Word x, SANE_Word y) { log_assert(NULL, x > 0 && y > 0); while (x != y) { if (x > y) { x -= y; } else { y -= x; } } return x; } /* Find least common multiple of two positive integers */ SANE_Word math_lcm (SANE_Word x, SANE_Word y) { return (x * y) / math_gcd(x, y); } /* Check two ranges for equivalency */ static inline bool math_range_eq (const SANE_Range *r1, const SANE_Range *r2) { return r1->min == r2->min && r1->max == r2->max && r1->quant == r2->quant; } /* Check two ranges for overlapping */ static inline bool math_range_ovp (const SANE_Range *r1, const SANE_Range *r2) { return r1->max >= r2->min && r2->max >= r1->min; } /* Merge two ranges, if possible */ bool math_range_merge (SANE_Range *out, const SANE_Range *r1, const SANE_Range *r2) { /* Check for trivial cases */ if (math_range_eq(r1, r2)) { *out = *r1; return true; } if (!math_range_ovp(r1, r2)) { return false; } /* Ranges have equal quantization? If yes, just adjust min and max */ if (r1->quant == r2->quant) { out->min = math_max(r1->min, r2->min); out->max = math_min(r1->max, r2->max); out->quant = r1->quant; return true; } /* At least one of ranges don't have quantization? */ if (!r1->quant || !r2->quant) { /* To avoid code duplication, normalize things, so * r1 does have quantization and r2 doesn't. Note, * situation when both ranges don't have quantization * was covered before, when we checked for equal quantization */ if (r1->quant == 0) { const SANE_Range *tmp = r1; r1 = r2; r2 = tmp; } /* And fit r2 within r1 */ out->min = math_range_fit(r1, r2->min); out->max = math_range_fit(r1, r2->max); out->quant = r1->quant; return true; } /* Now the most difficult case */ SANE_Word quant = math_lcm(r1->quant, r2->quant); SANE_Word min, max, bounds_min, bounds_max; bounds_min = math_max(r1->min, r2->min); bounds_max = math_min(r1->max, r2->max); for (min = math_min(r1->min, r2->min); min < bounds_min; min += quant) ; if (min > bounds_max) { return false; } for (max = min; max + quant <= bounds_max; max += quant) ; out->min = min; out->max = max; out->quant = quant; return true; } /* Choose nearest integer in range */ SANE_Word math_range_fit(const SANE_Range *r, SANE_Word i) { if (i < r->min) { return r->min; } if (i > r->max) { return r->max; } if (r->quant == 0) { return i; } i -= r->min; i = ((i + r->quant / 2) / r->quant) * r->quant; i += r->min; return math_min(i, r->max); } /* Format millimeters, for printing */ char* math_fmt_mm (SANE_Word mm, char buf[]) { double mmd = SANE_UNFIX(mm); double integer, fraction; integer = floor(mmd); fraction = mmd - integer; if (fraction != 0) { sprintf(buf, "%d.%2.2d", (int) integer, (int) round(fraction * 100)); } else { sprintf(buf, "%d", (int) integer); } return buf; } /* Genrate random 32-bit integer */ uint32_t math_rand_u32 (void) { uint32_t r; rand_bytes(&r, sizeof(r)); return r; } /* Generate random integer in range [0...max], inclusively */ uint32_t math_rand_max (uint32_t max) { uint32_t mask, tmp; for (mask = 0, tmp = max; tmp != 0; tmp >>= 1) { mask |= tmp; } do { tmp = math_rand_u32() & mask; } while (tmp > max); return tmp; } /* Generate random integer in range [min...max], inclusively */ uint32_t math_rand_range (uint32_t min, uint32_t max) { /* Normalize range */ if (min == max) { return min; } if (min > max) { uint32_t tmp = max; max = min; min = tmp; } /* Generate random number */ return min + math_rand_max(max - min); } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-mdns.c000066400000000000000000001132121500411437100167640ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * MDNS device discovery */ #include "airscan.h" #include #include #include #include #include #include #include #include #include #include /******************** Constants *********************/ /* If failed, AVAHI client will be automatically * restarted after the following timeout expires, * in seconds */ #define MDNS_AVAHI_CLIENT_RESTART_TIMEOUT 1 /* Max time to wait until device table is ready, in seconds */ #define MDNS_READY_TIMEOUT 5 /* MDNS_SERVICE represents numerical identifiers for * DNS-SD service types we are interested in */ typedef enum { MDNS_SERVICE_UNKNOWN = -1, MDNS_SERVICE_IPP_TCP, /* _ipp._tcp */ MDNS_SERVICE_IPPS_TCP, /* _ipps._tcp */ MDNS_SERVICE_USCAN_TCP, /* _uscan._tcp */ MDNS_SERVICE_USCANS_TCP, /* _uscans._tcp */ MDNS_SERVICE_SCANNER_TCP, /* _scanner._tcp */ NUM_MDNS_SERVICE } MDNS_SERVICE; /* Action names for mdns_debug/mdns_perror messages */ #define MDNS_ACTION_BROWSE "browse" #define MDNS_ACTION_RESOLVE "resolve" #define MDNS_ACTION_LOOKUP "lookup" /******************** Local Types *********************/ /* mdns_finding represents zeroconf_finding for MDNS * device discovery */ typedef struct { zeroconf_finding finding; /* Base class */ AvahiServiceResolver **resolvers; /* Array of pending resolvers */ eloop_timer *publish_timer; /* ZEROCONF_PUBLISH_DELAY timer */ ll_node node_list; /* In mdns_finding_list */ bool should_publish; /* Should we publish this finding */ bool is_published; /* Finding actually published */ bool initscan; /* Device discovered during initial scan */ } mdns_finding; /* Static variables */ static log_ctx *mdns_log; static ll_head mdns_finding_list; static size_t mdns_finding_list_count; static const AvahiPoll *mdns_avahi_poll; static AvahiTimeout *mdns_avahi_restart_timer; static AvahiClient *mdns_avahi_client; static bool mdns_avahi_browser_running; static AvahiServiceBrowser *mdns_avahi_browser[NUM_MDNS_SERVICE]; static bool mdns_initscan[NUM_MDNS_SERVICE]; static int mdns_initscan_count[NUM_ZEROCONF_METHOD]; /* Forward declarations */ static void mdns_avahi_browser_stop (void); static void mdns_avahi_client_start (void); static void mdns_avahi_client_restart_defer (void); /* Print debug message */ static void mdns_debug (const char *action, AvahiIfIndex interface, AvahiProtocol protocol, AvahiLookupResultFlags flags, const char *in_type, const char *in_name, const char *out) { const char *af = protocol == AVAHI_PROTO_INET ? "ipv4" : "ipv6"; char ifname[IF_NAMESIZE] = "?"; char buf[512]; char buf2[128] = ""; if (interface == AVAHI_IF_UNSPEC) { strcpy(ifname, "*"); } else if (if_indextoname(interface, ifname) == NULL) { sprintf(ifname, "%d", interface); } if (in_name == NULL) { snprintf(buf, sizeof(buf), "\"%s\"", in_type); } else if (in_type == NULL) { snprintf(buf, sizeof(buf), "\"%s\"", in_name); } else { snprintf(buf, sizeof(buf), "\"%s\", \"%s\"", in_type, in_name); } flags &= AVAHI_LOOKUP_RESULT_CACHED | AVAHI_LOOKUP_RESULT_WIDE_AREA | AVAHI_LOOKUP_RESULT_MULTICAST | AVAHI_LOOKUP_RESULT_STATIC; if (flags != 0) { char *s = buf2 + 1; if ((flags & AVAHI_LOOKUP_RESULT_CACHED) != 0) { strcpy(s, " CACHED"); s += strlen(s); } if ((flags & AVAHI_LOOKUP_RESULT_WIDE_AREA) != 0) { strcpy(s, " WAN"); s += strlen(s); } if ((flags & AVAHI_LOOKUP_RESULT_MULTICAST) != 0) { strcpy(s, " MCAST"); s += strlen(s); } if ((flags & AVAHI_LOOKUP_RESULT_STATIC) != 0) { strcpy(s, " STATIC"); s += strlen(s); } strcpy(s, ")"); buf2[0] = ' '; buf2[1] = '('; } log_debug(mdns_log, "%s-%s@%s(%s): %s%s", action, af, ifname, buf, out, buf2); } /* Print error message */ static void mdns_perror (const char *action, AvahiIfIndex interface, AvahiProtocol protocol, const char *in_type, const char *in_name) { mdns_debug(action, interface, protocol, 0, in_type, in_name, avahi_strerror(avahi_client_errno(mdns_avahi_client))); } /* Get MDNS_SERVICE name */ static const char* mdns_service_name (MDNS_SERVICE service) { switch (service) { case MDNS_SERVICE_IPP_TCP: return "_ipp._tcp"; case MDNS_SERVICE_IPPS_TCP: return "_ipps._tcp"; case MDNS_SERVICE_USCAN_TCP: return "_uscan._tcp"; case MDNS_SERVICE_USCANS_TCP: return "_uscans._tcp"; case MDNS_SERVICE_SCANNER_TCP: return "_scanner._tcp"; case MDNS_SERVICE_UNKNOWN: case NUM_MDNS_SERVICE: break; } log_internal_error(mdns_log); return NULL; } /* Get MDNS_SERVICE by name */ static MDNS_SERVICE mdns_service_by_name (const char *name) { int i; for (i = 0; i < NUM_MDNS_SERVICE; i ++) { if (!strcasecmp(name, mdns_service_name(i))) { return i; } } return MDNS_SERVICE_UNKNOWN; } /* Map MDNS_SERVICE to ZEROCONF_METHOD */ static ZEROCONF_METHOD mdns_service_to_method (MDNS_SERVICE service) { switch (service) { case MDNS_SERVICE_USCAN_TCP: return ZEROCONF_USCAN_TCP; case MDNS_SERVICE_USCANS_TCP: return ZEROCONF_USCANS_TCP; default: return ZEROCONF_MDNS_HINT; } } /* Get AvahiResolverEvent name, for debugging */ static const char* mdns_avahi_resolver_event_name (AvahiResolverEvent e) { static char buf[64]; switch (e) { case AVAHI_RESOLVER_FOUND: return "AVAHI_RESOLVER_FOUND"; case AVAHI_RESOLVER_FAILURE: return "AVAHI_RESOLVER_FAILURE"; } /* Safe, because used only from the work thread */ sprintf(buf, "AVAHI_RESOLVER_UNKNOWN(%d)", e); return buf; } /* Get AvahiBrowserEvent name, for debugging */ static const char* mdns_avahi_browser_event_name (AvahiBrowserEvent e) { static char buf[64]; switch (e) { case AVAHI_BROWSER_NEW: return "AVAHI_BROWSER_NEW"; case AVAHI_BROWSER_REMOVE: return "AVAHI_BROWSER_REMOVE"; case AVAHI_BROWSER_CACHE_EXHAUSTED: return "AVAHI_BROWSER_CACHE_EXHAUSTED"; case AVAHI_BROWSER_ALL_FOR_NOW: return "AVAHI_BROWSER_ALL_FOR_NOW"; case AVAHI_BROWSER_FAILURE: return "AVAHI_BROWSER_FAILURE"; } /* Safe, because used only from the work thread */ sprintf(buf, "AVAHI_BROWSER_UNKNOWN(%d)", e); return buf; } /* Get AvahiClientState name, for debugging */ static const char* mdns_avahi_client_state_name (AvahiClientState s) { static char buf[64]; switch (s) { case AVAHI_CLIENT_S_REGISTERING: return "AVAHI_CLIENT_S_REGISTERING"; case AVAHI_CLIENT_S_RUNNING: return "AVAHI_CLIENT_S_RUNNING"; case AVAHI_CLIENT_S_COLLISION: return "AVAHI_CLIENT_S_COLLISION"; case AVAHI_CLIENT_FAILURE: return "AVAHI_CLIENT_FAILURE"; case AVAHI_CLIENT_CONNECTING: return "AVAHI_CLIENT_CONNECTING"; } /* Safe, because used only from the work thread */ sprintf(buf, "AVAHI_BROWSER_UNKNOWN(%d)", s); return buf; } /* Increment count of initial scan tasks */ static void mdns_initscan_count_inc (ZEROCONF_METHOD method) { mdns_initscan_count[method] ++; } /* Decrement count if initial scan tasks */ static void mdns_initscan_count_dec (ZEROCONF_METHOD method) { log_assert(mdns_log, mdns_initscan_count[method] > 0); mdns_initscan_count[method] --; if (mdns_initscan_count[method] == 0) { zeroconf_finding_done(method); } } /* Create new mdns_finding structure */ static mdns_finding* mdns_finding_new (ZEROCONF_METHOD method, int ifindex, const char *name, bool initscan) { mdns_finding *mdns = mem_new(mdns_finding, 1); mdns->finding.method = method; mdns->finding.ifindex = ifindex; mdns->finding.name = str_dup(name); mdns->finding.addrs = ip_addrset_new(); mdns->resolvers = ptr_array_new(AvahiServiceResolver*); mdns->initscan = initscan; if (mdns->initscan) { mdns_initscan_count_inc(mdns->finding.method); } return mdns; } /* Free mdns_finding structure */ static void mdns_finding_free (mdns_finding *mdns) { mem_free((char*) mdns->finding.name); mem_free((char*) mdns->finding.model); ip_addrset_free(mdns->finding.addrs); zeroconf_endpoint_list_free(mdns->finding.endpoints); if (mdns->initscan) { mdns_initscan_count_dec(mdns->finding.method); } mem_free(mdns->resolvers); mem_free(mdns); } /* Find mdns_finding */ static mdns_finding* mdns_finding_find (ZEROCONF_METHOD method, int ifindex, const char *name) { ll_node *node; for (LL_FOR_EACH(node, &mdns_finding_list)) { mdns_finding *mdns; mdns = OUTER_STRUCT(node, mdns_finding, node_list); if (mdns->finding.method == method && mdns->finding.ifindex == ifindex && !strcasecmp(mdns->finding.name, name)) { return mdns; } } return NULL; } /* Get mdns_finding: find existing or add a new one */ static mdns_finding* mdns_finding_get (ZEROCONF_METHOD method, int ifindex, const char *name, bool initscan) { mdns_finding *mdns; /* Check for duplicated device */ mdns = mdns_finding_find(method, ifindex, name); if (mdns != NULL) { return mdns; } /* Add new mdns_finding state */ mdns = mdns_finding_new(method, ifindex, name, initscan); ll_push_end(&mdns_finding_list, &mdns->node_list); mdns_finding_list_count ++; return mdns; } /* Kill pending resolvers * * It also cancels mdns->publish_timer, if it is active */ static void mdns_finding_kill_resolvers (mdns_finding *mdns) { size_t i, len = mem_len(mdns->resolvers); for (i = 0; i < len; i ++) { avahi_service_resolver_free(mdns->resolvers[i]); } ptr_array_trunc(mdns->resolvers); if (mdns->publish_timer != NULL) { eloop_timer_cancel(mdns->publish_timer); mdns->publish_timer = NULL; } } /* Del the mdns_finding */ static void mdns_finding_del (mdns_finding *mdns) { if (mdns->is_published) { zeroconf_finding_withdraw(&mdns->finding); } ll_del(&mdns->node_list); mdns_finding_list_count --; mdns_finding_kill_resolvers(mdns); mdns_finding_free(mdns); } /* Delete all mdns_finding */ static void mdns_finding_del_all (void) { ll_node *node; while ((node = ll_first(&mdns_finding_list)) != NULL) { mdns_finding *mdns; mdns = OUTER_STRUCT(node, mdns_finding, node_list); mdns_finding_del(mdns); } } /* Publish mdns_finding */ static void mdns_finding_publish (mdns_finding *mdns) { /* Cancel any activity, if still pending */ mdns_finding_kill_resolvers(mdns); /* Fixup endpoints */ mdns->finding.endpoints = zeroconf_endpoint_list_sort_dedup( mdns->finding.endpoints); /* Fixup model and UUID */ if (mdns->finding.model == NULL) { /* Very unlikely, just paranoia */ mdns->finding.model = str_dup(mdns->finding.name); } if (!uuid_valid(mdns->finding.uuid)) { /* Paranoia too * * If device UUID is not available from DNS-SD (which * is very unlikely), we generate a synthetic UUID, * based on device name hash */ mdns->finding.uuid = uuid_hash(mdns->finding.name); } /* Update initscan count */ if (mdns->initscan) { mdns->initscan = false; mdns_initscan_count_dec(mdns->finding.method); } /* Publish the finding */ if (mdns->should_publish && !mdns->is_published) { mdns->is_published = true; zeroconf_finding_publish(&mdns->finding); } } /* ZEROCONF_PUBLISH_DELAY timer callback */ static void mdns_finding_publish_delay_timer_callback (void *data) { mdns_finding *mdns = data; log_debug(mdns_log, "\"%s\": publish-delay timer expired", mdns->finding.name); mdns->publish_timer = NULL; mdns_finding_publish(mdns); } /* Publish mdns_finding with optional delay */ static void mdns_finding_publish_delay (mdns_finding *mdns) { if (mdns->resolvers[0] == NULL) { mdns_finding_publish(mdns); } else if (mdns->publish_timer == NULL) { mdns->publish_timer = eloop_timer_new(ZEROCONF_PUBLISH_DELAY, mdns_finding_publish_delay_timer_callback, mdns); } } /* Make zeroconf_endpoint for eSCL */ static zeroconf_endpoint* mdns_make_escl_endpoint (ZEROCONF_METHOD method, const AvahiAddress *addr, uint16_t port, const char *rs, AvahiIfIndex interface) { char str_addr[128]; int rs_len; char *u; http_uri *uri; const char *scheme; if (method == ZEROCONF_USCAN_TCP) { scheme = "http"; } else { scheme = "https"; } if (addr->proto == AVAHI_PROTO_INET) { avahi_address_snprint(str_addr, sizeof(str_addr), addr); } else { size_t len; str_addr[0] = '['; avahi_address_snprint(str_addr + 1, sizeof(str_addr) - 2, addr); len = strlen(str_addr); /* Connect to link-local address requires explicit scope */ if (ip_is_linklocal(AF_INET6, addr->data.data)) { /* Percent character in the IPv6 address literal * needs to be properly escaped, so it becomes %25 * See RFC6874 for details */ len += sprintf(str_addr + len, "%%25%d", interface); } str_addr[len++] = ']'; str_addr[len] = '\0'; } /* Normalize rs */ rs_len = 0; if (rs != NULL) { while (*rs == '/') { rs ++; } rs_len = (int) strlen(rs); while (rs_len != 0 && rs[rs_len - 1] == '/') { rs_len --; } } /* Make eSCL URL */ if (rs == NULL) { /* Assume /eSCL by default */ u = str_printf("%s://%s:%d/eSCL/", scheme, str_addr, port); } else if (rs_len == 0) { /* Empty rs, avoid double '/' */ u = str_printf("%s://%s:%d/", scheme, str_addr, port); } else { u = str_printf("%s://%s:%d/%.*s/", scheme, str_addr, port, rs_len, rs); } uri = http_uri_new(u, true); log_assert(mdns_log, uri != NULL); mem_free(u); return zeroconf_endpoint_new(ID_PROTO_ESCL, uri); } /* Handle AVAHI_RESOLVER_FOUND event */ static void mdns_avahi_resolver_found (mdns_finding *mdns, MDNS_SERVICE service, AvahiStringList *txt, const AvahiAddress *addr, uint16_t port, AvahiIfIndex interface) { const char *txt_ty = NULL; const char *txt_uuid = NULL; const char *txt_scan = NULL; const char *txt_rs = NULL; AvahiStringList *s; zeroconf_endpoint *endpoint; ZEROCONF_METHOD method = mdns->finding.method; ip_addr ip_addr = ip_addr_make(interface, addr->proto == AVAHI_PROTO_INET ? AF_INET : AF_INET6, &addr->data); /* Decode TXT record */ s = avahi_string_list_find(txt, "ty"); if (s != NULL && s->size > 3) { txt_ty = (char*) s->text + 3; } s = avahi_string_list_find(txt, "uuid"); if (s != NULL && s->size > 5) { txt_uuid = (char*) s->text + 5; } switch (service) { case MDNS_SERVICE_IPP_TCP: case MDNS_SERVICE_IPPS_TCP: s = avahi_string_list_find(txt, "scan"); if (s != NULL && s->size > 5) { txt_scan = (char*) s->text + 5; } break; default: break; } switch (service) { case MDNS_SERVICE_USCAN_TCP: case MDNS_SERVICE_USCANS_TCP: s = avahi_string_list_find(txt, "rs"); if (s != NULL && s->size > 3) { txt_rs = (char*) s->text + 3; } break; default: break; } /* Update finding */ if (mdns->finding.model == NULL && txt_ty != NULL) { mdns->finding.model = str_dup(txt_ty); } if (!uuid_valid(mdns->finding.uuid) && txt_uuid != NULL) { mdns->finding.uuid = uuid_parse(txt_uuid); } ip_addrset_add(mdns->finding.addrs, ip_addr); /* Handle the event */ switch (service) { case MDNS_SERVICE_IPP_TCP: case MDNS_SERVICE_IPPS_TCP: if (txt_scan != NULL && !strcasecmp(txt_scan, "t")) { mdns->should_publish = true; } break; case MDNS_SERVICE_USCAN_TCP: case MDNS_SERVICE_USCANS_TCP: endpoint = mdns_make_escl_endpoint(method, addr, port, txt_rs, interface); endpoint->next = mdns->finding.endpoints; mdns->finding.endpoints = endpoint; mdns->should_publish = true; break; case MDNS_SERVICE_SCANNER_TCP: mdns->should_publish = true; break; case MDNS_SERVICE_UNKNOWN: case NUM_MDNS_SERVICE: log_internal_error(mdns_log); } } /* AVAHI service resolver callback */ static void mdns_avahi_resolver_callback (AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, AvahiResolverEvent event, const char *name, const char *type, const char *domain, const char *host_name, const AvahiAddress *addr, uint16_t port, AvahiStringList *txt, AvahiLookupResultFlags flags, void *userdata) { mdns_finding *mdns = userdata; MDNS_SERVICE service = mdns_service_by_name(type); (void) domain; (void) host_name; (void) flags; /* Print debug message */ if (event == AVAHI_RESOLVER_FOUND) { char buf[128]; avahi_address_snprint(buf, sizeof(buf), addr); sprintf(buf + strlen(buf), ":%d", port); mdns_debug(MDNS_ACTION_RESOLVE, interface, protocol, flags, type, name, buf); } else if (event == AVAHI_RESOLVER_FAILURE) { mdns_perror(MDNS_ACTION_RESOLVE, interface, protocol, type, name); } else { mdns_debug(MDNS_ACTION_RESOLVE, interface, protocol, flags, type, name, mdns_avahi_resolver_event_name(event)); } /* Remove resolver from list of pending ones */ if (!ptr_array_del(mdns->resolvers, ptr_array_find(mdns->resolvers, r))) { mdns_debug(MDNS_ACTION_RESOLVE, interface, protocol, flags, type, name, "spurious avahi callback"); return; } avahi_service_resolver_free(r); /* Handle event */ switch (event) { case AVAHI_RESOLVER_FOUND: mdns_avahi_resolver_found(mdns, service, txt, addr, port, interface); break; case AVAHI_RESOLVER_FAILURE: break; } /* Perform appropriate actions, if resolving is done */ mdns_finding_publish_delay(mdns); /* Notify WSDD about newly discovered address */ if (event == AVAHI_RESOLVER_FOUND) { int af = addr->proto == AVAHI_PROTO_INET ? AF_INET : AF_INET6; wsdd_send_directed_probe(interface, af, &addr->data); } } /* AVAHI browser callback */ static void mdns_avahi_browser_callback (AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol, AvahiBrowserEvent event, const char *name, const char *type, const char *domain, AvahiLookupResultFlags flags, void* userdata) { mdns_finding *mdns; // Newer clang produces a warning about casting from void* to an enum. // userdata is only set to values from the MDNS_SERVICE enum, so we can // safely suppress this warning with an extra cast through intptr_t. MDNS_SERVICE service = (MDNS_SERVICE)(intptr_t) userdata; ZEROCONF_METHOD method = mdns_service_to_method(service); bool initscan = mdns_initscan[service]; (void) b; (void) flags; /* Print debug message */ mdns_debug(MDNS_ACTION_BROWSE, interface, protocol, flags, type, NULL, mdns_avahi_browser_event_name(event)); if (event == AVAHI_BROWSER_NEW) { size_t len = strlen(name); char *buf = alloca(len + 3); buf[0] = '"'; memcpy(buf + 1, name, len); buf[len + 1] = '"'; buf[len + 2] = '\0'; mdns_debug(MDNS_ACTION_BROWSE, interface, protocol, flags, type, NULL, buf); } switch (event) { case AVAHI_BROWSER_NEW: /* Add a device (or lookup for already added) */ mdns = mdns_finding_get(method, interface, name, initscan); /* Initiate resolver -- look for IPv4 addresses */ AvahiServiceResolver *r; r = avahi_service_resolver_new(mdns_avahi_client, interface, protocol, name, type, domain, AVAHI_PROTO_INET, 0, mdns_avahi_resolver_callback, mdns); if (r == NULL) { mdns_perror(MDNS_ACTION_RESOLVE, interface, AVAHI_PROTO_INET, type, name); mdns_avahi_client_restart_defer(); break; } mdns->resolvers = ptr_array_append(mdns->resolvers, r); /* Initiate resolver -- look for IPv6 addresses */ r = avahi_service_resolver_new(mdns_avahi_client, interface, protocol, name, type, domain, AVAHI_PROTO_INET6, 0, mdns_avahi_resolver_callback, mdns); if (r == NULL) { mdns_perror(MDNS_ACTION_RESOLVE, interface, AVAHI_PROTO_INET6, type, name); mdns_avahi_client_restart_defer(); break; } /* Attach resolver to device state */ mdns->resolvers = ptr_array_append(mdns->resolvers, r); break; case AVAHI_BROWSER_REMOVE: mdns = mdns_finding_find(method, interface, name); if (mdns != NULL) { mdns_finding_del(mdns); } break; case AVAHI_BROWSER_FAILURE: mdns_perror(MDNS_ACTION_BROWSE, interface, protocol, type, NULL); mdns_avahi_client_restart_defer(); break; case AVAHI_BROWSER_CACHE_EXHAUSTED: break; case AVAHI_BROWSER_ALL_FOR_NOW: if (mdns_initscan[service]) { mdns_initscan[service] = false; mdns_initscan_count_dec(method); } break; } } /* Start browser for specified service type */ static bool mdns_avahi_browser_start_for_type (MDNS_SERVICE service, const char *type) { bool ok; log_assert(mdns_log, mdns_avahi_browser[service] == NULL); mdns_avahi_browser[service] = avahi_service_browser_new(mdns_avahi_client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, type, NULL, 0, mdns_avahi_browser_callback, (void*) service); ok = mdns_avahi_browser[service] != NULL; if (!ok) { log_debug(mdns_log, "avahi_service_browser_new(%s): %s", type, avahi_strerror(avahi_client_errno(mdns_avahi_client))); } if (ok && mdns_initscan[service]) { mdns_initscan_count_inc(mdns_service_to_method(service)); } return mdns_avahi_browser[service] != NULL; } /* Start/restart service browser */ static bool mdns_avahi_browser_start (void) { int i; bool ok = true; log_assert(mdns_log, !mdns_avahi_browser_running); for (i = 0; ok && i < NUM_MDNS_SERVICE; i ++) { ok = mdns_avahi_browser_start_for_type(i, mdns_service_name(i)); } mdns_avahi_browser_running = true; return ok; } /* Stop service browser */ static void mdns_avahi_browser_stop (void) { MDNS_SERVICE service; for (service = 0; service < NUM_MDNS_SERVICE; service ++) { if (mdns_avahi_browser[service] != NULL) { avahi_service_browser_free(mdns_avahi_browser[service]); mdns_avahi_browser[service] = NULL; if (mdns_initscan[service]) { mdns_initscan_count_dec(mdns_service_to_method(service)); } } } mdns_finding_del_all(); mdns_avahi_browser_running = false; } /* AVAHI client callback */ static void mdns_avahi_client_callback (AvahiClient *client, AvahiClientState state, void *userdata) { (void) client; (void) userdata; log_debug(mdns_log, "%s", mdns_avahi_client_state_name(state)); switch (state) { case AVAHI_CLIENT_S_REGISTERING: case AVAHI_CLIENT_S_RUNNING: case AVAHI_CLIENT_S_COLLISION: /* Note, first callback may come before avahi_client_new() * return, so mdns_avahi_client may be still unset. * Fix it here */ mdns_avahi_client = client; if (!mdns_avahi_browser_running) { if (!mdns_avahi_browser_start()) { mdns_avahi_client_restart_defer(); } } break; case AVAHI_CLIENT_FAILURE: mdns_avahi_client_restart_defer(); break; case AVAHI_CLIENT_CONNECTING: break; } } /* Timer for differed AVAHI client restart */ static void mdns_avahi_restart_timer_callback(AvahiTimeout *t, void *userdata) { (void) t; (void) userdata; mdns_avahi_client_start(); } /* Stop AVAHI client */ static void mdns_avahi_client_stop (void) { if (mdns_avahi_client != NULL) { avahi_client_free(mdns_avahi_client); mdns_avahi_client = NULL; } } /* Start/restart the AVAHI client */ static void mdns_avahi_client_start (void) { int error; log_assert(mdns_log, mdns_avahi_client == NULL); mdns_avahi_client = avahi_client_new (mdns_avahi_poll, AVAHI_CLIENT_NO_FAIL, mdns_avahi_client_callback, NULL, &error); if (mdns_avahi_client == NULL) { log_debug(mdns_log, "avahi_client_new failed: %s", avahi_strerror(error)); } } /* Deferred client restart */ static void mdns_avahi_client_restart_defer (void) { struct timeval tv; mdns_avahi_browser_stop(); mdns_avahi_client_stop(); gettimeofday(&tv, NULL); tv.tv_sec += MDNS_AVAHI_CLIENT_RESTART_TIMEOUT; mdns_avahi_poll->timeout_update(mdns_avahi_restart_timer, &tv); } /* Called by zeroconf to notify MDNS about initial scan timer expiration */ void mdns_initscan_timer_expired (void) { } /***** Asynchronous MDNS resolver *****/ /* mdns_resolver asynchronously resolves IP addresses using MDNS */ struct mdns_resolver { int ifindex; /* Interface index */ ll_head pending; /* Pending queries */ }; /* mdns_query represents a single mdns_resolver query */ struct mdns_query { char *name; /* Requested name */ mdns_resolver *resolver; /* Back link to resolver */ ip_addrset *answer; /* Collected addresses */ uint64_t dummy_callid; /* Used when no resolvers can be created */ void (*callback)( /* Completion callback */ const mdns_query *query); void *ptr; /* Callback's user data */ AvahiHostNameResolver **resolvers; /* Avahi host name resolver */ ll_node chain; /* In mdns_resolver::pending */ }; /* mdns_resolver_new creates a new MDNS resolver */ mdns_resolver* mdns_resolver_new (int ifindex) { mdns_resolver *resolver = mem_new(mdns_resolver, 1); resolver->ifindex = ifindex; ll_init(&resolver->pending); return resolver; } /* mdns_resolver_free frees the mdns_resolver previously created * by mdns_resolver_new() */ void mdns_resolver_free (mdns_resolver *resolver) { log_assert(mdns_log, ll_empty(&resolver->pending)); mem_free(resolver); } /* mdns_resolver_cancel cancels all pending queries */ void mdns_resolver_cancel (mdns_resolver *resolver) { ll_node *node; while ((node = ll_first(&resolver->pending)) != NULL) { mdns_query *query = OUTER_STRUCT(node, mdns_query, chain); mdns_query_cancel(query); } } /* mdns_resolver_has_pending checks if resolver has pending queries */ bool mdns_resolver_has_pending (mdns_resolver *resolver) { return !ll_empty(&resolver->pending); } /* mdns_query_free frees the mdns_query */ static void mdns_query_free (mdns_query *query) { int i; ll_del(&query->chain); for (i = 0; query->resolvers[i] != NULL; i ++) { avahi_host_name_resolver_free(query->resolvers[i]); } eloop_call_cancel(query->dummy_callid); ip_addrset_free(query->answer); mem_free(query->resolvers); mem_free(query->name); mem_free(query); } /* mdns_query_callback_exec executes completion callback, then * frees the query */ static void mdns_query_callback_exec (mdns_query *query) { char *s = ip_addrset_friendly_str(query->answer, NULL); log_debug(mdns_log, "%s(%s): found %s", MDNS_ACTION_LOOKUP, query->name, s); mem_free(s); query->callback(query); mdns_query_free(query); } /* AvahiHostNameResolver callback for mdns_query */ static void mdns_query_callback (AvahiHostNameResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, AvahiResolverEvent event, const char *name, const AvahiAddress *addr, AvahiLookupResultFlags flags, void *userdata) { mdns_query *query = (mdns_query*) userdata; char buf[256]; /* Print debug message */ switch (event) { case AVAHI_RESOLVER_FOUND: avahi_address_snprint(buf, sizeof(buf), addr); mdns_debug(MDNS_ACTION_LOOKUP, interface, protocol, flags, NULL, name, buf); break; case AVAHI_RESOLVER_FAILURE: mdns_perror(MDNS_ACTION_LOOKUP, interface, protocol, NULL, name); break; default: mdns_debug(MDNS_ACTION_LOOKUP, interface, protocol, flags, NULL, name, mdns_avahi_resolver_event_name(event)); } /* Remove resolver from list of pending ones */ if (!ptr_array_del(query->resolvers, ptr_array_find(query->resolvers, r))) { mdns_debug(MDNS_ACTION_LOOKUP, interface, protocol, flags, NULL, name, "spurious avahi callback"); return; } /* Handle event */ if (event == AVAHI_RESOLVER_FOUND) { ip_addr ip = { .ifindex = interface }; bool ok = true; switch (protocol) { case AVAHI_PROTO_INET: ip.af = AF_INET; ip.ip.v4.s_addr = addr->data.ipv4.address; break; case AVAHI_PROTO_INET6: ip.af = AF_INET6; memcpy(&ip.ip.v6, addr->data.ipv6.address, 16); break; default: /* Very unlikely to happen, but just in case... */ ok = false; } if (ok) { ip_addrset_add(query->answer, ip); } } /* Raise callback, if all is done */ if (mem_len(query->resolvers) == 0) { mdns_query_callback_exec(query); } } /* eloop_call() callback for mdns_resolver that was created * without any active AvahiHostNameResolver * * The purpose of thus callback is to ensure that mdns_resolver * completion callback will be executed in any case */ static void mdns_query_dummy_callback (void *ptr) { mdns_query *query = (mdns_query*) ptr; mdns_query_callback_exec(query); } /* mdns_query_sumbit submits a new MDNS query for the specified domain * name. When resolving is done, successfully or not, callback will be * called * * The ptr parameter is passed to the callback without any interpretation * as a user-defined argument * * Answer is a set of discovered IP addresses. It is owned by resolver, * callback should not free it and should not assume that it is still * valid after return from callback */ mdns_query* mdns_query_submit (mdns_resolver *resolver, const char *name, void (*callback)(const mdns_query *query), void *ptr) { mdns_query *query = mem_new(mdns_query, 1); static AvahiProtocol protos[] = {AVAHI_PROTO_INET, AVAHI_PROTO_INET6}; int i; const char *name_fqdn = name; /* Normalize name, if it is not FQDN */ if (!avahi_is_valid_fqdn(name_fqdn)) { char *with_local; size_t sz = strlen(name_fqdn); with_local = alloca(strlen(name_fqdn) + 7); memcpy(with_local, name_fqdn, sz); strcpy(with_local + sz, ".local"); if (avahi_is_valid_fqdn(with_local)) { name_fqdn = with_local; } } /* Initialize a query structure */ query->name = str_dup(name); query->resolver = resolver; query->answer = ip_addrset_new(); query->dummy_callid = ELOOP_CALL_BADID; query->callback = callback; query->ptr = ptr; query->resolvers = ptr_array_new(AvahiHostNameResolver*); /* Create an AvahiHostNameResolver for each IPv4/v6 address family */ for (i = 0; i < 2; i ++) { AvahiProtocol proto = protos[i]; AvahiHostNameResolver *r; r = avahi_host_name_resolver_new( mdns_avahi_client, resolver->ifindex, proto, name_fqdn, proto, 0, mdns_query_callback, query ); if (r == NULL) { mdns_perror(MDNS_ACTION_LOOKUP, resolver->ifindex, proto, NULL, query->name); } else { query->resolvers = ptr_array_append(query->resolvers, r); mdns_debug(MDNS_ACTION_LOOKUP, resolver->ifindex, proto, 0, NULL, query->name, "started"); } } /* Make sure completion callback will be executed even * if no resolvers were created */ if (mem_len(query->resolvers) == 0) { query->dummy_callid = eloop_call(mdns_query_dummy_callback, query); } ll_push_end(&resolver->pending, &query->chain); return query; } /* mdns_query_cancel cancels the pending query. mdns_query memory will * be released and callback will not be called * * Note, mdns_query pointer is valid when obtained from mdns_query_sumbit * and until canceled or return from callback. */ void mdns_query_cancel (mdns_query *query) { mdns_query_free(query); } /* mdns_query_get_name returns domain name, as it was specified * when query was submitted */ const char* mdns_query_get_name (const mdns_query *query) { return query->name; } /* mdns_query_get_answer returns resolved addresses */ const ip_addrset* mdns_query_get_answer (const mdns_query *query) { return query->answer; } /* mdns_query_set_ptr gets the user-defined ptr, associated * with query when it was submitted */ void* mdns_query_get_ptr (const mdns_query *query) { return query->ptr; } /***** Miscellaneous *****/ /* mdns_device_count_by_model returns count of distinct devices * with model names matching the specified parent. * * Several instances of the same device (i.e. printer vs scanner) are * counted only once per network interface. * * WSDD uses this function to decide when to use extended discovery * time (some devices are known to be hard for WD-Discovery) * * Pattern is the glob-style expression, applied to the model name * of discovered devices. */ unsigned int mdns_device_count_by_model (int ifindex, const char *pattern) { mdns_finding **findings; unsigned int findings_count = 0; unsigned int i, answer; ll_node *node; /* Collect matching devices */ if (mdns_finding_list_count == 0) { return 0; } findings = alloca(sizeof(*findings) * mdns_finding_list_count); for (LL_FOR_EACH(node, &mdns_finding_list)) { mdns_finding *mdns = OUTER_STRUCT(node, mdns_finding, node_list); if (mdns->finding.ifindex == ifindex && mdns->finding.model != NULL && fnmatch(pattern, mdns->finding.model, 0) == 0) { findings[findings_count] = mdns; findings_count ++; } } /* Sort by ifindex + name, then count only distinct devices */ qsort(findings, findings_count, sizeof(*findings), zeroconf_finding_qsort_by_index_name); if (findings_count <= 1) { return findings_count; } answer = 1; for (i = 1; i < findings_count; i ++) { mdns_finding **p1 = &findings[i - 1]; mdns_finding **p2 = &findings[i]; if (zeroconf_finding_qsort_by_index_name(p1, p2) != 0) { answer ++; } } return answer; } /***** Initialization and cleanup *****/ /* Initialize MDNS */ SANE_Status mdns_init (void) { int i; mdns_log = log_ctx_new("MDNS", zeroconf_log); ll_init(&mdns_finding_list); if (!conf.discovery) { log_debug(mdns_log, "devices discovery disabled"); zeroconf_finding_done(ZEROCONF_MDNS_HINT); zeroconf_finding_done(ZEROCONF_USCAN_TCP); zeroconf_finding_done(ZEROCONF_USCANS_TCP); return SANE_STATUS_GOOD; } for (i = 0; i < NUM_MDNS_SERVICE; i ++) { mdns_initscan[i] = true; } for (i = 0; i < NUM_ZEROCONF_METHOD; i ++) { mdns_initscan_count[i] = 0; } mdns_avahi_poll = eloop_poll_get(); mdns_avahi_restart_timer = mdns_avahi_poll->timeout_new(mdns_avahi_poll, NULL, mdns_avahi_restart_timer_callback, NULL); if (mdns_avahi_restart_timer == NULL) { return SANE_STATUS_NO_MEM; } mdns_avahi_client_start(); if (mdns_avahi_client == NULL) { return SANE_STATUS_NO_MEM; } return SANE_STATUS_GOOD; } /* Cleanup MDNS */ void mdns_cleanup (void) { if (mdns_log == NULL) { return; /* MDNS not initialized */ } if (mdns_avahi_poll != NULL) { mdns_avahi_browser_stop(); mdns_avahi_client_stop(); mdns_finding_del_all(); if (mdns_avahi_restart_timer != NULL) { mdns_avahi_poll->timeout_free(mdns_avahi_restart_timer); mdns_avahi_restart_timer = NULL; } mdns_avahi_poll = NULL; } log_ctx_free(mdns_log); mdns_log = NULL; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-memstr.c000066400000000000000000000150331500411437100173340ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Memory allocation and strings */ #include "airscan.h" #include #include #include #include #include #include #include /******************** Memory allocation ********************/ /* While memory block grows, its size is doubled until * MEM_CAP_BLOCK is reached, then it grows linearly, * reminding a multiple of whole MEM_CAP_BLOCKs */ #define MEM_CAP_BLOCK 65536 /* Each memory block allocated from here has the following * control record */ typedef struct { uint32_t len, cap; /* Block length and capacity in bytes */ } mem_head; /* Check for OOM */ #define mem_check_oom(p,must) \ do{ \ if ((p) == NULL && must) { \ log_panic(NULL, "Out of memory"); \ __builtin_unreachable(); \ } \ } while(0) /* Truncate the vector length, preserving previously allocated buffer */ void mem_trunc (void *p) { ((mem_head*) p)[-1].len = 0; } /* Free memory, previously obtained from mem_new()/mem_expand() */ void mem_free (void *p) { if (p != NULL) { free(((mem_head*) p) - 1); } } /* Get memory block length, in bytes */ size_t mem_len_bytes (const void *p) { return p ? ((mem_head*) p)[-1].len : 0; } /* Get memory block capacity, in bytes */ size_t mem_cap_bytes (const void *p) { return p ? ((mem_head*) p)[-1].cap : 0; } /* Compute allocation size, including mem_head header, in bytes */ static inline int mem_alloc_size(size_t len, size_t extra, size_t elsize) { size_t sz = sizeof(mem_head) + elsize * (len + extra); if (sz < MEM_CAP_BLOCK) { /* Round up to the next power of 2 */ sz --; sz |= sz >> 1; sz |= sz >> 2; sz |= sz >> 4; sz |= sz >> 8; sz |= sz >> 16; sz ++; } else { /* Round up to the next block boundary */ sz += MEM_CAP_BLOCK - 1; sz &= ~(MEM_CAP_BLOCK - 1); } return sz; } /* Helper function: allocate new block of memory */ void* __mem_alloc (size_t len, size_t extra, size_t elsize, bool must) { size_t sz = mem_alloc_size(len, extra, elsize); mem_head *h = calloc(sz, 1); if (h == NULL) { mem_check_oom(h, must); return NULL; } h->len = len * elsize; h->cap = sz - (sizeof(mem_head)); return h + 1; } /* Helper function for memory allocation. * Allocated or resizes memory block */ void* __mem_resize (void *p, size_t len, size_t extra, size_t elsize, bool must) { size_t sz; mem_head *h; /* If `p' is NULL, just call __mem_alloc() */ if (p == NULL) { return __mem_alloc(len, extra, elsize, must); } /* Reallocate memory, if required */ h = ((mem_head*) p) - 1; sz = mem_alloc_size(len, extra, elsize); if (h->cap + sizeof(mem_head) < sz) { h = realloc(h, sz); if (h == NULL) { mem_check_oom(h, must); return NULL; } } /* Zero-fill newly added elements */ len *= elsize; if (len > h->len) { memset((char*) (h + 1) + h->len, 0, len - h->len); } /* Update control header and return a block */ h->len = len; h->cap = sz - (sizeof(mem_head)); return h + 1; } /* Helper function for memory allocation. * Shrinks memory block */ void __mem_shrink (void *p, size_t len, size_t elsize) { mem_head *h = ((mem_head*) p) - 1; len *= elsize; log_assert(NULL, len <= h->len); h->len = len; } /******************** Strings ********************/ /* Create new string as a lowercase copy of existent string */ char* str_dup_tolower (const char *s1) { char *s = str_dup(s1); size_t i; for (i = 0; s[i]; i ++) { s[i] = safe_tolower(s[i]); } return s; } /* Create new string and print to it */ char* str_printf (const char *format, ...) { va_list ap; char *s; va_start(ap, format); s = str_append_vprintf(NULL, format, ap); va_end(ap); return s; } /* Create new string and print to it, va_list version */ char* str_vprintf (const char *format, va_list ap) { return str_append_vprintf(NULL, format, ap); } /* Append formatted string to string * * `s' must be previously created by some of str_XXX functions, * `s' will be consumed and the new pointer will be returned */ char* str_append_printf (char *s, const char *format, ...) { va_list ap; va_start(ap, format); s = str_append_vprintf(s, format, ap); va_end(ap); return s; } /* Append formatted string to string -- va_list version */ char* str_append_vprintf (char *s, const char *format, va_list ap) { char buf[4096]; size_t len, oldlen; va_list ap2; va_copy(ap2, ap); len = vsnprintf(buf, sizeof(buf), format, ap2); va_end(ap2); if (len < sizeof(buf)) { return str_append_mem(s, buf, len); } oldlen = mem_len(s); s = mem_resize(s, oldlen + len, 1); va_copy(ap2, ap); vsnprintf(s + oldlen, len + 1, format, ap2); va_end(ap2); return s; } /* Concatenate several strings. Last pointer must be NULL. * The returned pointer must be eventually freed by mem_free */ char* str_concat (const char *s, ...) { va_list ap; char *ret = str_dup(s); va_start(ap, s); while ((s = va_arg(ap, const char*)) != NULL) { ret = str_append(ret, s); } va_end(ap); return ret; } /* Check if string has a specified prefix */ bool str_has_prefix (const char *s, const char *prefix) { size_t l1 = strlen(s); size_t l2 = strlen(prefix); return l1 >= l2 && !memcmp(s, prefix, l2); } /* Check if string has a specified suffix */ bool str_has_suffix (const char *s, const char *suffix) { size_t l1 = strlen(s); size_t l2 = strlen(suffix); return l1 >= l2 && !memcmp(s + (l1 - l2), suffix, l2); } /* Remove leading and trailing white space. * This function modifies string in place, and returns pointer * to original string, for convenience */ char* str_trim (char *s) { size_t len = strlen(s), skip; while (len > 0 && safe_isspace(s[len - 1])) { len --; } for (skip = 0; skip < len && safe_isspace(s[skip]); skip ++) { ; } len -= skip; if (len != 0 && skip != 0) { memmove(s, s + skip, len); } s[len] = '\0'; return s; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-netif.c000066400000000000000000000375501500411437100171420ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Network interfaces addresses */ #include "airscan.h" #include #include #include #include #include #ifdef OS_HAVE_RTNETLINK #include #include #endif #ifdef OS_HAVE_AF_ROUTE #include #endif #include #include /* Static variables */ static int netif_rtnetlink_sock = -1; static eloop_fdpoll *netif_rtnetlink_fdpoll; static ll_head netif_notifier_list; static struct ifaddrs *netif_ifaddrs; /* Forward declarations */ static netif_addr* netif_addr_list_sort (netif_addr *list); /* Get distance to the target address */ NETIF_DISTANCE netif_distance_get (const struct sockaddr *addr) { struct ifaddrs *ifa; struct in_addr addr4, ifaddr4, ifmask4; struct in6_addr addr6, ifaddr6, ifmask6; static struct in6_addr zero6; size_t i; NETIF_DISTANCE distance = NETIF_DISTANCE_ROUTED; for (ifa = netif_ifaddrs; ifa != NULL; ifa = ifa->ifa_next) { /* Skip interface without address or netmask */ if (ifa->ifa_addr == NULL || ifa->ifa_netmask == NULL) { continue; } /* Compare address family */ if (addr->sa_family != ifa->ifa_addr->sa_family) { continue; } /* Check direct reachability */ switch (addr->sa_family) { case AF_INET: addr4 = ((struct sockaddr_in*) addr)->sin_addr; ifaddr4 = ((struct sockaddr_in*) ifa->ifa_addr)->sin_addr; ifmask4 = ((struct sockaddr_in*) ifa->ifa_netmask)->sin_addr; if (addr4.s_addr == ifaddr4.s_addr) { return NETIF_DISTANCE_LOOPBACK; } if (((addr4.s_addr ^ ifaddr4.s_addr) & ifmask4.s_addr) == 0) { distance = NETIF_DISTANCE_DIRECT; } break; case AF_INET6: addr6 = ((struct sockaddr_in6*) addr)->sin6_addr; ifaddr6 = ((struct sockaddr_in6*) ifa->ifa_addr)->sin6_addr; ifmask6 = ((struct sockaddr_in6*) ifa->ifa_netmask)->sin6_addr; if (!memcmp(&addr6, &ifaddr6, sizeof(struct in6_addr))) { return NETIF_DISTANCE_LOOPBACK; } for (i = 0; i < sizeof(struct in6_addr); i ++) { addr6.s6_addr[i] ^= ifaddr6.s6_addr[i]; addr6.s6_addr[i] &= ifmask6.s6_addr[i]; } if (!memcmp(&addr6, &zero6, sizeof(struct in6_addr))) { distance = NETIF_DISTANCE_DIRECT; } break; } } 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 * * The returned memory is owned by caller and must be freed * using netif_addr_list_free/netif_addr_free_single */ netif_addr* netif_addr_list_get (void) { struct ifaddrs *ifa; netif_addr *list = NULL, *addr; for (ifa = netif_ifaddrs; ifa != NULL; ifa = ifa->ifa_next) { /* Skip interface without address */ if (ifa->ifa_addr == NULL) { continue; } /* Skip loopback interface */ if ((ifa->ifa_flags & IFF_LOOPBACK) != 0) { continue; } /* Obtain interface index. Skip address, if it failed */ int idx = if_nametoindex(ifa->ifa_name); if (idx <= 0) { continue; } /* Translate struct ifaddrs to netif_addr */ addr = mem_new(netif_addr, 1); addr->next = list; addr->ifindex = idx; strncpy(addr->ifname.text, ifa->ifa_name, sizeof(addr->ifname.text) - 1); switch (ifa->ifa_addr->sa_family) { case AF_INET: addr->ip.v4 = ((struct sockaddr_in*) ifa->ifa_addr)->sin_addr; inet_ntop(AF_INET, &addr->ip.v4, addr->straddr, sizeof(addr->straddr)); break; case AF_INET6: addr->ipv6 = true; addr->ip.v6 = ((struct sockaddr_in6*) ifa->ifa_addr)->sin6_addr; inet_ntop(AF_INET6, &addr->ip.v6, addr->straddr, sizeof(addr->straddr)); break; default: /* Paranoia; should not actually happen */ mem_free(addr); addr = NULL; break; } if (addr != NULL) { addr->next = list; list = addr; } } return netif_addr_list_sort(list); } /* Free a single netif_addr */ static void netif_addr_free_single (netif_addr *addr) { mem_free(addr); } /* Free list of network interfaces addresses */ void netif_addr_list_free (netif_addr *list) { while (list != NULL) { netif_addr *next = list->next; netif_addr_free_single(list); list = next; } } /* Compare two netif_addr addresses, for sorting */ static int netif_addr_cmp (netif_addr *a1, netif_addr *a2) { bool ll1, ll2; /* Compare interface indices */ if (a1->ifindex != a2->ifindex) { return a1->ifindex - a2->ifindex; } /* Prefer normal addresses, rather that link-local */ ll1 = ip_is_linklocal(a1->ipv6 ? AF_INET6 : AF_INET, &a1->ip); ll2 = ip_is_linklocal(a2->ipv6 ? AF_INET6 : AF_INET, &a2->ip); if (ll1 != ll2) { return ll1 ? 1 : -1; } /* Be in trend: prefer IPv6 addresses */ if (a1->ipv6 != a2->ipv6) { return (int) a2->ipv6 - (int) a1->ipv6; } /* Otherwise, sort lexicographically */ return strcmp(a1->straddr, a2->straddr); } /* Revert netif_addr list */ static netif_addr* netif_addr_list_revert (netif_addr *list) { netif_addr *prev = NULL, *next; while (list != NULL) { next = list->next; list->next = prev; prev = list; list = next; } return prev; } /* Sort list of addresses */ static netif_addr* netif_addr_list_sort (netif_addr *list) { netif_addr *halves[2] = {NULL, NULL}; int half = 0; if (list == NULL || list->next == NULL) { return list; } /* Split list into halves */ while (list != NULL) { netif_addr *next = list->next; list->next = halves[half]; halves[half] = list; half ^= 1; list = next; } /* Sort each half, recursively */ for (half = 0; half < 2; half ++) { halves[half] = netif_addr_list_sort(halves[half]); } /* Now merge the sorted halves */ list = NULL; while (halves[0] != NULL || halves[1] != NULL) { netif_addr *next; if (halves[0] == NULL) { half = 1; } else if (halves[1] == NULL) { half = 0; } else if (netif_addr_cmp(halves[0], halves[1]) < 0) { half = 0; } else { half = 1; } next = halves[half]->next; halves[half]->next = list; list = halves[half]; halves[half] = next; } /* And revert the list, as after merging it is reverted */ return netif_addr_list_revert(list); } /* Compute a difference between two lists of addresses. * * It works by tossing nodes between 3 output lists: * * if node is present in list2 only, it is moved * to netif_diff.added * * if node is present in list1 only, it is moved * to netif_diff.removed * * if node is present in both lists, node from * list1 is moved to preserved, and node from * list2 is released * * It assumes, both lists are sorted, as returned * by netif_addr_get(). Returned lists are also sorted */ netif_diff netif_diff_compute (netif_addr *list1, netif_addr *list2) { netif_diff diff = {NULL, NULL, NULL}; while (list1 != NULL || list2 != NULL) { netif_addr *addr; int cmp; if (list1 == NULL) { cmp = 1; } else if (list2 == NULL) { cmp = -1; } else { cmp = netif_addr_cmp(list1, list2); } if (cmp < 0) { addr = list1; list1 = list1->next; addr->next = diff.removed; diff.removed = addr; } else if (cmp > 0) { addr = list2; list2 = list2->next; addr->next = diff.added; diff.added = addr; } else { addr = list1; list1 = list1->next; addr->next = diff.preserved; diff.preserved = addr; addr = list2; list2 = list2->next; netif_addr_free_single(addr); } } diff.added = netif_addr_list_revert(diff.added); diff.removed = netif_addr_list_revert(diff.removed); diff.preserved = netif_addr_list_revert(diff.preserved); return diff; } /* Merge two lists of addresses * * Input lists are consumed and new list is created. * * Input lists are assumed to be sorted, and output * list will be sorted as well */ netif_addr* netif_addr_list_merge (netif_addr *list1, netif_addr *list2) { netif_addr *list = NULL; while (list1 != NULL || list2 != NULL) { netif_addr *addr; int cmp; if (list1 == NULL) { cmp = 1; } else if (list2 == NULL) { cmp = -1; } else { cmp = netif_addr_cmp(list1, list2); } if (cmp < 0) { addr = list1; list1 = list1->next; } else { addr = list2; list2 = list2->next; } addr->next = list; list = addr; } return netif_addr_list_revert(list); } /* Network interfaces addresses change notifier */ struct netif_notifier { void (*callback)(void*); /* Notification callback */ void *data; /* Callback data */ ll_node list_node; /* in the netif_notifier_list */ }; /* Get a new list of network interfaces and notify the callbacks */ static void netif_refresh_ifaddrs (void) { struct ifaddrs *new_ifaddrs; ll_node *node; int rc; rc = getifaddrs(&new_ifaddrs); if (rc >= 0) { if (netif_ifaddrs != NULL) { freeifaddrs(netif_ifaddrs); } netif_ifaddrs = new_ifaddrs; } /* Call all registered callbacks */ for (LL_FOR_EACH(node, &netif_notifier_list)) { netif_notifier *notifier; notifier = OUTER_STRUCT(node, netif_notifier, list_node); notifier->callback(notifier->data); } } /* netif_notifier read callback */ static void netif_notifier_read_callback (int fd, void *data, ELOOP_FDPOLL_MASK mask) { static uint8_t buf[16384]; int rc; (void) fd; (void) data; (void) mask; /* Get rtnetlink message */ rc = read(netif_rtnetlink_sock, buf, sizeof(buf)); if (rc < 0) { return; } #if defined(OS_HAVE_RTNETLINK) struct nlmsghdr *p; size_t sz; /* Parse rtnetlink message, to suppress unneeded (and relatively * expensive) netif_refresh_ifaddrs() calls. We are only interested * in RTM_NEWADDR/RTM_DELADDR notifications */ sz = (size_t) rc; for (p = (struct nlmsghdr*) buf; sz >= sizeof(struct nlmsghdr); p = NLMSG_NEXT(p, sz)) { if (!NLMSG_OK(p, sz) || sz < p->nlmsg_len) { return; } switch (p->nlmsg_type) { case NLMSG_DONE: return; case RTM_NEWADDR: case RTM_DELADDR: netif_refresh_ifaddrs(); return; } } #elif defined(OS_HAVE_AF_ROUTE) /* Note, on OpenBSD we have ROUTE_MSGFILTER, but FreeBSD lacks * this feature, so we have to filter received routing messages * manually, to avoid relatively expensive netif_refresh_ifaddrs() * calls */ struct rt_msghdr *rtm = (struct rt_msghdr*) buf; if (rc >= (int) sizeof(struct rt_msghdr)) { switch (rtm->rtm_type) { case RTM_NEWADDR: case RTM_DELADDR: netif_refresh_ifaddrs(); break; } } #endif } /* Create netif_notifier */ netif_notifier* netif_notifier_create (void (*callback) (void*), void *data) { netif_notifier *notifier = mem_new(netif_notifier, 1); notifier->callback = callback; notifier->data = data; ll_push_end(&netif_notifier_list, ¬ifier->list_node); return notifier; } /* Destroy netif_notifier */ void netif_notifier_free (netif_notifier *notifier) { ll_del(¬ifier->list_node); mem_free(notifier); } /* Start/stop callback */ static void netif_start_stop_callback (bool start) { if (start) { netif_rtnetlink_fdpoll = eloop_fdpoll_new(netif_rtnetlink_sock, netif_notifier_read_callback, NULL); eloop_fdpoll_set_mask(netif_rtnetlink_fdpoll, ELOOP_FDPOLL_READ); } else { eloop_fdpoll_free(netif_rtnetlink_fdpoll); netif_rtnetlink_fdpoll = NULL; } } /* Initialize network interfaces monitoring */ SANE_Status netif_init (void) { ll_init(&netif_notifier_list); #if defined(OS_HAVE_RTNETLINK) struct sockaddr_nl addr; int rc; /* Create AF_NETLINK socket */ netif_rtnetlink_sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, NETLINK_ROUTE); if (netif_rtnetlink_sock < 0) { log_debug(NULL, "can't open AF_NETLINK socket: %s", strerror(errno)); return SANE_STATUS_IO_ERROR; } /* Subscribe to notifications */ memset(&addr, 0, sizeof(addr)); addr.nl_family = AF_NETLINK; addr.nl_groups = RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR; rc = bind(netif_rtnetlink_sock, (struct sockaddr*) &addr, sizeof(addr)); if (rc < 0) { log_debug(NULL, "can't bind AF_NETLINK socket: %s", strerror(errno)); close(netif_rtnetlink_sock); return SANE_STATUS_IO_ERROR; } #elif defined(OS_HAVE_AF_ROUTE) /* Create AF_ROUTE socket */ netif_rtnetlink_sock = socket(AF_ROUTE, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, AF_UNSPEC); if (netif_rtnetlink_sock < 0) { log_debug(NULL, "can't open AF_ROUTE socket: %s", strerror(errno)); return SANE_STATUS_IO_ERROR; } #ifdef ROUTE_MSGFILTER unsigned int rtfilter = ROUTE_FILTER(RTM_NEWADDR) | ROUTE_FILTER(RTM_DELADDR); if (setsockopt(netif_rtnetlink_sock, AF_ROUTE, ROUTE_MSGFILTER, &rtfilter, sizeof(rtfilter)) < 0) { /* Note, this error is not fatal for us, it is enough to * log it and continue */ log_debug(NULL, "can't set ROUTE_MSGFILTER: %s", strerror(errno)); } #endif #endif /* Initialize netif_ifaddrs */ if (getifaddrs(&netif_ifaddrs) < 0) { log_debug(NULL, "getifaddrs(): %s", strerror(errno)); close(netif_rtnetlink_sock); return SANE_STATUS_IO_ERROR; } /* Register start/stop callback */ eloop_add_start_stop_callback(netif_start_stop_callback); return SANE_STATUS_GOOD; } /* Cleanup network interfaces monitoring */ void netif_cleanup (void) { if (netif_ifaddrs != NULL) { freeifaddrs(netif_ifaddrs); netif_ifaddrs = NULL; } if (netif_rtnetlink_sock >= 0) { close(netif_rtnetlink_sock); netif_rtnetlink_sock = -1; } } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-os.c000066400000000000000000000075611500411437100164550ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * OS Facilities */ #include "airscan.h" #include #include #include #include #include #include #include #include #if defined(__OpenBSD__) || defined(__FreeBSD__) # include # include #endif /* Static variables */ static pthread_once_t os_homedir_once = PTHREAD_ONCE_INIT; static char os_homedir_buf[PATH_MAX]; static pthread_once_t os_progname_once = PTHREAD_ONCE_INIT; static char os_progname_buf[PATH_MAX]; /* Initialize os_homedir_buf. Called once, on demand */ static void os_homedir_init (void) { const char *s = getenv("HOME"); struct passwd pwd, *result; char buf[16384]; /* Try $HOME first, so user can override it's home directory */ if (s != NULL && s[0] && strlen(s) < sizeof(os_homedir_buf)) { strcpy(os_homedir_buf, s); return; } /* Now try getpwuid_r */ getpwuid_r(getuid(), &pwd, buf, sizeof(buf), &result); if (result == NULL) { return; } if (result->pw_dir[0] && strlen(result->pw_dir) < sizeof(os_homedir_buf)) { strcpy(os_homedir_buf, result->pw_dir); } } /* Get user's home directory. * There is no need to free the returned string * * May return NULL in a case of error */ const char * os_homedir (void) { pthread_once(&os_homedir_once, os_homedir_init); return os_homedir_buf[0] ? os_homedir_buf : NULL; } /* Initialize os_progname_buf. Called once, on demand */ static void os_progname_init (void) { /* Obtain path to the executable */ #ifdef OS_HAVE_LINUX_PROCFS ssize_t rc; memset(os_progname_buf, 0, sizeof(os_progname_buf)); rc = readlink("/proc/self/exe", os_progname_buf, sizeof(os_progname_buf) - 1); if (rc < 0) { return; } #elif defined(__OpenBSD__) struct kinfo_proc kp; const int mib[6] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid(), sizeof(struct kinfo_proc), 1}; size_t len = sizeof(kp); int rc = sysctl(mib, 6, &kp, &len, NULL, 0); if (rc == -1) { return; } memmove(os_progname_buf, kp.p_comm, KI_MAXCOMLEN); #elif defined(__FreeBSD__) const int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1}; size_t len = sizeof(os_progname_buf); int rc = sysctl(mib, 4, os_progname_buf, &len, NULL, 0); if (rc < 0) { os_progname_buf[0] = '\0'; /* Just a paranoia */ } #else /* This is nice to have but not critical. The caller already has to handle os_progname returning NULL. The error is left as a reminder for anyone porting this. */ # error FIX ME #endif /* Strip the directory, leave only the file name */ char *s = strrchr(os_progname_buf, '/'); if (s != NULL) { memmove(os_progname_buf, s+1, strlen(s+1) + 1); } } /* Get base name of the calling program. * There is no need to free the returned string * * May return NULL in a case of error */ const char* os_progname (void) { pthread_once(&os_progname_once, os_progname_init); return os_progname_buf[0] ? os_progname_buf : NULL; } /* Make directory with parents */ int os_mkdir (const char *path, mode_t mode) { size_t len = strlen(path); char *p = alloca(len + 1), *s; if (len == 0) { errno = EINVAL; return -1; } strcpy(p, path); for (s = strchr(p + 1, '/'); s != NULL; s = strchr(s + 1, '/')) { *s = '\0'; /* Note, we have nothing to do with errors in creation of * intermediate directories, but status of creation of the * final directory in the path is checked below */ (void) mkdir(p, mode); *s = '/'; } return mkdir(p, mode); } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-png.c000066400000000000000000000153601500411437100166140ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * PNG image decoder */ #include "airscan.h" #include #include #include /* PNG image decoder */ typedef struct { image_decoder decoder; /* Base class */ png_struct *png_ptr; /* Underlying libpng decoder */ png_info *info_ptr; /* libpng info struct */ const uint8_t *image_data; /* Remaining image data */ size_t image_size; /* Remaining image data */ char error[1024]; /* Error message buffer */ png_uint_32 width, height; /* Image size in pixels */ int bit_depth; /* 1/2/4/8/16 */ int color_type; /* PNG_COLOR_TYPE_XXX */ int interlace_type; /* PNG_INTERLACE_XXX */ unsigned int num_lines; /* Num of lines left to read */ } image_decoder_png; /* Free PNG decoder */ static void image_decoder_png_free (image_decoder *decoder) { image_decoder_png *png = (image_decoder_png*) decoder; image_decoder_reset(decoder); mem_free(png); } /* libpng error callback */ static void image_decoder_png_error_fn (png_struct *png_ptr, const char *message) { image_decoder_png *png = png_get_error_ptr(png_ptr); snprintf(png->error, sizeof(png->error), "PNG: %s", message); } /* libpng warning callback */ static void image_decoder_png_warning_fn (png_struct *png_ptr, const char *message) { (void) png_ptr; (void) message; } /* libpng malloc callback */ static void* image_decoder_png_malloc_fn (png_struct *png_ptr, size_t size) { (void) png_ptr; return mem_new(char, size); } /* libpng free callback */ static void image_decoder_png_free_fn (png_struct *png_ptr, void *p) { (void) png_ptr; mem_free(p); } /* libpng read callback */ static void image_decoder_png_read_fn (png_struct *png_ptr, png_bytep data, size_t size) { image_decoder_png *png = png_get_io_ptr(png_ptr); if (size > png->image_size) { png_error(png_ptr, "unexpected EOF"); } memcpy(data, png->image_data, size); png->image_data += size; png->image_size -= size; } /* Begin PNG decoding */ static error image_decoder_png_begin (image_decoder *decoder, const void *data, size_t size) { image_decoder_png *png = (image_decoder_png*) decoder; /* Create libpng structures */ png->png_ptr = png_create_read_struct_2(PNG_LIBPNG_VER_STRING, png, image_decoder_png_error_fn, image_decoder_png_warning_fn, png, image_decoder_png_malloc_fn, image_decoder_png_free_fn); if (png->png_ptr == NULL) { return ERROR("PNG: png_create_read_struct_2() failed"); } png->info_ptr = png_create_info_struct(png->png_ptr); if (png->info_ptr == NULL) { image_decoder_reset(decoder); return ERROR("PNG: png_create_info_struct() failed"); } /* Setup read function */ png_set_read_fn(png->png_ptr, png, image_decoder_png_read_fn); png->image_data = data; png->image_size = size; /* Read image info */ if (setjmp(png_jmpbuf(png->png_ptr))) { image_decoder_reset(decoder); return ERROR(png->error); } png_read_info(png->png_ptr, png->info_ptr); png_get_IHDR(png->png_ptr, png->info_ptr, &png->width, &png->height, &png->bit_depth, &png->color_type, &png->interlace_type, NULL, NULL); png->num_lines = png->height; /* Reject interlaced images */ if (png->interlace_type != PNG_INTERLACE_NONE) { image_decoder_reset(decoder); return ERROR("PNG: interlaced images not supported"); } /* Setup input transformations */ if (png->color_type == PNG_COLOR_TYPE_PALETTE) { png_set_palette_to_rgb(png->png_ptr); } if (png->color_type == PNG_COLOR_TYPE_GRAY && png->bit_depth < 8) { png_set_expand_gray_1_2_4_to_8(png->png_ptr); png->bit_depth = 8; } if ((png->color_type & PNG_COLOR_MASK_ALPHA) != 0) { png_set_strip_alpha(png->png_ptr); } return NULL; } /* Reset PNG decoder */ static void image_decoder_png_reset (image_decoder *decoder) { image_decoder_png *png = (image_decoder_png*) decoder; if (png->png_ptr != NULL) { png_destroy_read_struct(&png->png_ptr, &png->info_ptr, NULL); png->png_ptr = NULL; png->info_ptr = NULL; } } /* Get bytes count per pixel */ static int image_decoder_png_get_bytes_per_pixel (image_decoder *decoder) { image_decoder_png *png = (image_decoder_png*) decoder; int bit_depth = png->bit_depth; if ((png->color_type & PNG_COLOR_MASK_COLOR) != 0) { bit_depth *= 3; } return bit_depth / 3; } /* Get image parameters */ static void image_decoder_png_get_params (image_decoder *decoder, SANE_Parameters *params) { image_decoder_png *png = (image_decoder_png*) decoder; params->last_frame = SANE_TRUE; params->pixels_per_line = png->width; params->lines = png->height; params->depth = png->bit_depth; if ((png->color_type & PNG_COLOR_MASK_COLOR) != 0) { params->format = SANE_FRAME_RGB; params->bytes_per_line = params->pixels_per_line * 3; } else { params->format = SANE_FRAME_GRAY; params->bytes_per_line = params->pixels_per_line; } } /* Set clipping window */ static error image_decoder_png_set_window (image_decoder *decoder, image_window *win) { image_decoder_png *png = (image_decoder_png*) decoder; win->x_off = win->y_off = 0; win->wid = png->width; win->hei = png->height; return NULL; } /* Read next line of image */ static error image_decoder_png_read_line (image_decoder *decoder, void *buffer) { image_decoder_png *png = (image_decoder_png*) decoder; if (!png->num_lines) { return ERROR("PNG: end of file"); } if (setjmp(png_jmpbuf(png->png_ptr))) { image_decoder_reset(decoder); return ERROR(png->error); } png_read_row(png->png_ptr, buffer, NULL); png->num_lines --; return NULL; } /* Create PNG image decoder */ image_decoder* image_decoder_png_new (void) { image_decoder_png *png = mem_new(image_decoder_png, 1); png->decoder.content_type = "image/png"; png->decoder.free = image_decoder_png_free; png->decoder.begin = image_decoder_png_begin; png->decoder.reset = image_decoder_png_reset; png->decoder.get_bytes_per_pixel = image_decoder_png_get_bytes_per_pixel; png->decoder.get_params = image_decoder_png_get_params; png->decoder.set_window = image_decoder_png_set_window; png->decoder.read_line = image_decoder_png_read_line; return &png->decoder; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-pollable.c000066400000000000000000000036171500411437100176240ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Pollable events */ #include "airscan.h" #ifdef OS_HAVE_EVENTFD #include #endif #include #include #include #pragma GCC diagnostic ignored "-Wunused-result" /* The pollable event */ struct pollable { int efd; /* Underlying eventfd handle */ #ifndef OS_HAVE_EVENTFD // Without eventfd we use a pipe, so we need a second fd. int write_fd; #endif }; /* Create new pollable event */ pollable* pollable_new (void) { #ifdef OS_HAVE_EVENTFD int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); #else int fds[2]; int r = pipe2(fds, O_CLOEXEC | O_NONBLOCK); int efd = r < 0 ? r : fds[0]; #endif if (efd< 0) { return NULL; } pollable *p = mem_new(pollable, 1); p->efd = efd; #ifndef OS_HAVE_EVENTFD p->write_fd = fds[1]; #endif return p; } /* Free pollable event */ void pollable_free (pollable *p) { close(p->efd); #ifndef OS_HAVE_EVENTFD close(p->write_fd); #endif mem_free(p); } /* Get file descriptor for poll()/select(). */ int pollable_get_fd (pollable *p) { return p->efd; } /* Make pollable event "ready" */ void pollable_signal (pollable *p) { static uint64_t c = 1; #ifdef OS_HAVE_EVENTFD write(p->efd, &c, sizeof(c)); #else write(p->write_fd, &c, sizeof(c)); #endif } /* Make pollable event "not ready" */ void pollable_reset (pollable *p) { uint64_t unused; (void) read(p->efd, &unused, sizeof(unused)); } /* Wait until pollable event is ready */ void pollable_wait (pollable *p) { int rc; do { struct pollfd pfd = { .fd = p->efd, .events = POLLIN, .revents = 0 }; rc = poll(&pfd, 1, -1); } while (rc < 1); } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-rand.c000066400000000000000000000020171500411437100167470ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Random bytes generator */ #include "airscan.h" #pragma GCC diagnostic ignored "-Wunused-result" #include #include #include #define RAND_SOURCE "/dev/urandom" static FILE *rand_fp; /* Get N random bytes */ void rand_bytes (void *buf, size_t n) { log_assert(NULL, rand_fp != NULL); /* Read from /dev/urandom never fails * */ (void) fread(buf, 1, n, rand_fp); } /* Initialize random bytes generator */ SANE_Status rand_init (void) { rand_fp = fopen(RAND_SOURCE, "rb"); if (rand_fp == NULL) { log_debug(NULL, "%s: %s", RAND_SOURCE, strerror(errno)); return SANE_STATUS_IO_ERROR; } return SANE_STATUS_GOOD; } /* Cleanup random bytes generator */ void rand_cleanup (void) { if (rand_fp != NULL) { fclose(rand_fp); rand_fp = NULL; } } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-tiff.c000066400000000000000000000254221500411437100167600ustar00rootroot00000000000000/* sane - Scanner Access Now Easy. * * Copyright (C) 2020 Thierry HUCHARD * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * * See LICENSE for license terms and conditions */ #include "airscan.h" #include #include #include /* TIFF image decoder */ typedef struct { image_decoder decoder; /* Base class */ TIFF* tif; /* libtiff decoder */ uint32_t current_line; /* Current line */ unsigned char *mem_file; /* Position of the beginning of the tiff file. */ toff_t offset_file; /* Moving the start position of the tiff file. */ tsize_t size_file; /* Size of the tiff file. */ /* JPEG-in-TIFF handling */ image_decoder *jpeg_decoder; /* JPEG decoder */ const void *jpeg_data; /* JPEG data withing TIFF file */ size_t jpeg_size; /* JPEG data size */ /* Image parameters */ uint16_t bytes_per_pixel; /* Bytes per pixel */ uint32_t image_width; /* Image width */ uint32_t image_height; /* Image height */ } image_decoder_tiff; /***** Forward declarations *****/ static void image_decoder_tiff_reset (image_decoder *decoder); static error image_decoder_tiff_setup_old_jpeg (image_decoder_tiff *tiff); /****** I/O callbacks for TIFFClientOpen() *****/ /* readproc callback for TIFFClientOpen() * * Works like read(2) */ static tsize_t image_decoder_tiff_readproc (thandle_t handle, tdata_t data, tsize_t n) { image_decoder_tiff *tiff = (image_decoder_tiff*) handle; tsize_t n_remaining, n_copy; void *src_addr; /* find the actual number of bytes to read (copy) */ n_copy = n; if( (tsize_t) tiff->offset_file >= tiff->size_file) { n_remaining = 0; } else { n_remaining = tiff->size_file - tiff->offset_file; } if (n_copy > n_remaining) { n_copy = n_remaining; } /* EOF, return immediately */ if (n_copy <= 0) { return 0; } src_addr = (void*)(&(tiff->mem_file[tiff->offset_file])); memcpy((void*)data, src_addr, n_copy); tiff->offset_file += n_copy; /* Actualisation de l'offset */ return n_copy; } /* writeproc callback for TIFFClientOpen() * * Works like write(2). Not needed for decoder */ static tsize_t image_decoder_tiff_writeproc (thandle_t handle, tdata_t data, tsize_t n) { (void) handle; (void) data; (void) n; return -1; } /* seekproc callback for TIFFClientOpen() * * Works like lseek(2) */ static toff_t image_decoder_tiff_seekproc (thandle_t handle, toff_t ofs, int whence) { image_decoder_tiff *tiff = (image_decoder_tiff*) handle; toff_t new_offset; /* find the location we plan to seek to */ switch (whence) { case SEEK_SET: new_offset = ofs; break; case SEEK_CUR: new_offset = tiff->offset_file + ofs; break; default: /* Not supported */ log_internal_error(NULL); return -1; } /* Updating the offset */ tiff->offset_file = new_offset; return tiff->offset_file; } /* closeproc callback for TIFFClientOpen() * * Works like close(2) */ static int image_decoder_tiff_closeproc (thandle_t handle) { (void) handle; return 0; } /* sizeproc callback for TIFFClientOpen() * * Returns file size, in bytes */ static toff_t image_decoder_tiff_sizeproc (thandle_t handle) { image_decoder_tiff *tiff = (image_decoder_tiff*) handle; return (toff_t) (tiff->size_file); } /* mapproc callback for TIFFClientOpen() * * Works like mmap(2). Not required and not implemented */ static int image_decoder_tiff_mapproc (thandle_t fd, tdata_t *pbase, toff_t *psize) { (void) fd; (void) pbase; (void) psize; return (0); } /* upmapproc callback for TIFFClientOpen() * * Works like munmap(2). Not required and not implemented */ static void image_decoder_tiff_unmapproc (thandle_t fd, tdata_t base, toff_t size) { (void) fd; (void) base; (void) size; } /****** image_decoder methods for TIFF decoder *****/ /* Free TIFF decoder */ static void image_decoder_tiff_free (image_decoder *decoder) { image_decoder_tiff *tiff = (image_decoder_tiff*) decoder; if (tiff->tif) { TIFFClose(tiff->tif); } image_decoder_free(tiff->jpeg_decoder); mem_free(tiff); } /* Begin TIFF decoding */ static error image_decoder_tiff_begin (image_decoder *decoder, const void *data, size_t size) { image_decoder_tiff *tiff = (image_decoder_tiff*) decoder; error err = NULL; uint16_t compression; /* Set the TiffClientOpen interface to read a file from memory. */ tiff->mem_file = (unsigned char*)data; tiff->offset_file = 0; tiff->size_file = size; tiff->tif = TIFFClientOpen("airscan TIFF Interface", "r", (image_decoder_tiff*)(tiff), image_decoder_tiff_readproc, image_decoder_tiff_writeproc, image_decoder_tiff_seekproc, image_decoder_tiff_closeproc, image_decoder_tiff_sizeproc, image_decoder_tiff_mapproc, image_decoder_tiff_unmapproc); if (tiff->tif == NULL) { return ERROR("TIFF: invalid open memory"); } if (tiff->tif == NULL) { return ERROR("TIFF: broken image");; } /* Obtain image parameters */ if (!TIFFGetField(tiff->tif, TIFFTAG_SAMPLESPERPIXEL, &tiff->bytes_per_pixel)) { err = ERROR("TIFF: can't get TIFFTAG_SAMPLESPERPIXEL"); goto FAIL; } if (!TIFFGetField(tiff->tif, TIFFTAG_IMAGEWIDTH, &tiff->image_width)) { err = ERROR("TIFF: can't get TIFFTAG_IMAGEWIDTH"); goto FAIL; } if (!TIFFGetField(tiff->tif, TIFFTAG_IMAGELENGTH, &tiff->image_height)) { err = ERROR("TIFF: can't get TIFFTAG_IMAGELENGTH"); goto FAIL; } if (!TIFFGetField(tiff->tif, TIFFTAG_COMPRESSION, &compression)) { err = ERROR("TIFF: can't get TIFFTAG_COMPRESSION"); goto FAIL; } switch (compression) { case COMPRESSION_OJPEG: err = image_decoder_tiff_setup_old_jpeg(tiff); break; } if (err == NULL && tiff->jpeg_data != NULL) { err = image_decoder_begin(tiff->jpeg_decoder, tiff->jpeg_data, tiff->jpeg_size); } if (err != NULL) { goto FAIL; } return NULL; /* Error: cleanup and exit */ FAIL: image_decoder_tiff_reset(decoder); return err; } /* Reset TIFF decoder */ static void image_decoder_tiff_reset (image_decoder *decoder) { image_decoder_tiff *tiff = (image_decoder_tiff*) decoder; if (tiff->tif != NULL) { TIFFClose(tiff->tif); tiff->tif = NULL; } if (tiff->jpeg_data != NULL) { image_decoder_reset(tiff->jpeg_decoder); tiff->jpeg_data = NULL; tiff->jpeg_size = 0; } } /* Get bytes count per pixel */ static int image_decoder_tiff_get_bytes_per_pixel (image_decoder *decoder) { image_decoder_tiff *tiff = (image_decoder_tiff*) decoder; if (tiff->jpeg_data != NULL) { return image_decoder_get_bytes_per_pixel(tiff->jpeg_decoder); } return (int) tiff->bytes_per_pixel; } /* Get image parameters */ static void image_decoder_tiff_get_params (image_decoder *decoder, SANE_Parameters *params) { image_decoder_tiff *tiff = (image_decoder_tiff*) decoder; if (tiff->jpeg_data != NULL) { image_decoder_get_params(tiff->jpeg_decoder, params); return; } params->last_frame = SANE_TRUE; params->pixels_per_line = (SANE_Int) tiff->image_width; params->lines = (SANE_Int) tiff->image_height; params->depth = 8; params->bytes_per_line = params->pixels_per_line * (SANE_Int) tiff->bytes_per_pixel; if (tiff->bytes_per_pixel == 1) { params->format = SANE_FRAME_GRAY; } else { params->format = SANE_FRAME_RGB; } } /* Set clipping window */ static error image_decoder_tiff_set_window (image_decoder *decoder, image_window *win) { image_decoder_tiff *tiff = (image_decoder_tiff*) decoder; if (tiff->jpeg_data != NULL) { return image_decoder_set_window(tiff->jpeg_decoder, win); } win->x_off = win->y_off = 0; win->wid = (int) tiff->image_width; win->hei = (int) tiff->image_height; return NULL; } /* Read next line of image */ static error image_decoder_tiff_read_line (image_decoder *decoder, void *buffer) { image_decoder_tiff *tiff = (image_decoder_tiff*) decoder; if (tiff->jpeg_data != NULL) { return image_decoder_read_line(tiff->jpeg_decoder, buffer); } if (tiff->current_line >= tiff->image_height) { return ERROR("TIFF: end of file"); } if (TIFFReadScanline(tiff->tif, buffer, tiff->current_line, 0) == -1) { return ERROR("TIFF: read scanline error"); } tiff->current_line ++; return NULL; } /* Create TIFF image decoder */ image_decoder* image_decoder_tiff_new (void) { image_decoder_tiff *tiff = mem_new(image_decoder_tiff, 1); tiff->decoder.content_type = "image/tiff"; tiff->decoder.free = image_decoder_tiff_free; tiff->decoder.begin = image_decoder_tiff_begin; tiff->decoder.reset = image_decoder_tiff_reset; tiff->decoder.get_bytes_per_pixel = image_decoder_tiff_get_bytes_per_pixel; tiff->decoder.get_params = image_decoder_tiff_get_params; tiff->decoder.set_window = image_decoder_tiff_set_window; tiff->decoder.read_line = image_decoder_tiff_read_line; tiff->jpeg_decoder = image_decoder_jpeg_new(); return &tiff->decoder; } /***** JPEG-in-TIFF handling *****/ /* Prepare decoder to handle OLD JPEG format * * Note, libtiff doesn't correctly decode OLD JPEG in line-by-line * mode (using TIFFReadScanline() interface), so we have to delegate * this work directly to the JPEG decoder */ static error image_decoder_tiff_setup_old_jpeg (image_decoder_tiff *tiff) { uint64_t jpeg_off, jpeg_size; /* Try TIFFTAG_JPEGIFOFFSET/TIFFTAG_JPEGIFBYTECOUNT first * * Note, libtiff validates these tags by itself, and * if they are not present or invalid, returned value * for TIFFTAG_JPEGIFOFFSET will be 0 */ TIFFGetField(tiff->tif, TIFFTAG_JPEGIFOFFSET, &jpeg_off); TIFFGetField(tiff->tif, TIFFTAG_JPEGIFBYTECOUNT, &jpeg_size); if (jpeg_off != 0) { tiff->jpeg_data = tiff->mem_file + (size_t) jpeg_off; tiff->jpeg_size = (size_t) jpeg_size; return NULL; } /* FIXME: we can still try to guess JPEG stream position, * using TIFFTAG_JPEGQTABLES/TIFFTAG_JPEGDCTABLES/TIFFTAG_JPEGIFBYTECOUNT * tags */ return ERROR("TIFF: unsupported old JPEG compression"); } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-trace.c000066400000000000000000000231431500411437100171240ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Protocol Trace */ #include "airscan.h" #include #include #include #include /* Trace file handle */ struct trace { volatile unsigned int refcnt; /* Reference count */ FILE *log; /* Log file */ FILE *data; /* Data file */ unsigned int index; /* Message index */ }; /* TAR file hader */ typedef struct { char name[100]; char mode[8]; char uid[8]; char gid[8]; char size[12]; char mtime[12]; char checksum[8]; char typeflag[1]; char linkname[100]; char magic[6]; char version[2]; char uname[32]; char gname[32]; char devmajor[8]; char devminor[8]; char prefix[155]; char pad[12]; } tar_header; /* Name of the process' executable */ static const char *trace_program; /* Full block of zero bytes */ static const char trace_zero_block[512]; /* Initialize protocol trace. Called at backend initialization */ SANE_Status trace_init (void) { trace_program = os_progname(); if (trace_program == NULL) { trace_program = "unknown"; } return SANE_STATUS_GOOD; } /* Cleanup protocol trace. Called at backend unload */ void trace_cleanup () { } /* Open protocol trace */ trace* trace_open (const char *device_name) { trace *t; char *path; size_t len; if (conf.dbg_trace == NULL) { return NULL; } (void) os_mkdir(conf.dbg_trace, 0755); t = mem_new(trace, 1); t->refcnt = 1; path = str_dup(conf.dbg_trace); path = str_terminate(path, '/'); len = strlen(path); path = str_append(path, trace_program); path = str_append(path, "-"); path = str_append(path, device_name); for (; path[len] != '\0'; len ++) { switch (path[len]) { case ' ': case '/': path[len] = '-'; break; } } path = str_append(path, ".log"); t->log = fopen(path, "w"); path = str_resize(path, str_len(path) - 4); path = str_append(path, ".tar"); t->data = fopen(path, "wb"); mem_free(path); if (t->log != NULL && t->data != NULL) { return t; } trace_unref(t); return NULL; } /* Ref the trace */ trace* trace_ref (trace *t) { if (t != NULL) { __sync_fetch_and_add(&t->refcnt, 1); } return t; } /* Unref the trace. When trace is not longer in use, it will be closed */ void trace_unref (trace *t) { if (t != NULL && (__sync_fetch_and_sub(&t->refcnt, 1) == 1)) { if (t->log != NULL) { fclose(t->log); } if (t->data != NULL) { if (t->log != NULL) { /* Normal close - write tar footer */ fwrite(trace_zero_block, sizeof(trace_zero_block), 1, t->data); fwrite(trace_zero_block, sizeof(trace_zero_block), 1, t->data); } fclose(t->data); } mem_free(t); } } /* http_query_foreach_request_header()/http_query_foreach_response_header() * callback */ static void trace_message_headers_foreach_callback (const char *name, const char *value, void *ptr) { trace *t = ptr; fprintf(t->log, "%s: %s\n", name, value); } /* Dump binary data. The data saved as a file into a .TAR archive. * Returns name of file, where data was saved */ static void trace_dump_data (trace *t, http_data *data) { tar_header hdr; uint32_t chsum; size_t i; const char *ext; log_assert(NULL, sizeof(hdr) == 512); memset(&hdr, 0, sizeof(hdr)); /* Guess file extension */ ext = ""; if (!strncmp(data->content_type, "image/", 6)) { ext = data->content_type + 6; } else if (!strncmp(data->content_type, "application/octet-stream", 24)) { ext = "dat"; } else if (!strncmp(data->content_type, "application/", 12)) { ext = data->content_type + 12; } else if (!strncmp(data->content_type, "text/", 5)) { ext = data->content_type + 5; } if (!*ext) { ext = "dat"; } /* Make file name */ sprintf(hdr.name, "%8.8d.%s", t->index ++, ext); /* Make tar header */ strcpy(hdr.mode, "644"); strcpy(hdr.uid, "0"); strcpy(hdr.gid, "0"); sprintf(hdr.size, "%lo", (unsigned long) data->size); sprintf(hdr.mtime, "%llo", (long long) time(NULL)); hdr.typeflag[0] = '0'; strcpy(hdr.magic, "ustar"); memcpy(hdr.version, "00", 2); strcpy(hdr.devmajor, "1"); strcpy(hdr.devminor, "1"); memset(hdr.checksum, ' ', sizeof(hdr.checksum)); chsum = 0; for (i = 0; i < sizeof(hdr); i ++) { chsum += ((char*) &hdr)[i]; } sprintf(hdr.checksum, "%6.6o", chsum & 0777777); /* Write header and file data */ fwrite(&hdr, sizeof(hdr), 1, t->data); fwrite(data->bytes, data->size, 1, t->data); /* Write padding */ i = data->size & (512-1); if (i != 0) { fwrite(trace_zero_block, 512 - i, 1, t->data); } /* Put a note into the log file */ fprintf(t->log, "%lu bytes of data saved as %s\n", (unsigned long) data->size, hdr.name); } /* Dump text data. The data will be saved directly to the log file */ static void trace_dump_text (trace *t, http_data *data, bool xml) { const char *d, *end = (char*) data->bytes + data->size; int last = -1; if (xml && xml_format(t->log, data->bytes, data->size)) { return; } for (d = data->bytes; d < end; d ++) { if (*d != '\r') { last = *d; putc(last, t->log); } } if (last != '\n') { putc('\n', t->log); } } /* Dump message body */ void trace_dump_body (trace *t, http_data *data) { if (t == NULL) { return; } if (data->size == 0) { return; } if (str_has_prefix(data->content_type, "text/") || str_has_prefix(data->content_type, "application/xml") || str_has_prefix(data->content_type, "application/soap+xml") || str_has_prefix(data->content_type, "application/xop+xml")) { trace_dump_text(t, data, strstr(data->content_type, "xml") != NULL); } else { trace_dump_data(t, data); } putc('\n', t->log); } /* Dump binary data (as hex dump) * Each line is prefixed with the `prefix` character */ void trace_hexdump (trace *t, char prefix, const void *data, size_t size) { const uint8_t *dp = data; unsigned int off = 0; char *buf; if (t == NULL || !conf.dbg_hexdump) { return; } buf = str_new(); while (size != 0) { size_t av = size > 16 ? 16 : size; unsigned int i; str_trunc(buf); buf = str_append_printf(buf, "%c %4.4x: ", prefix, off); for(i = 0; i < 16; i ++) { buf = str_append_printf(buf, i < av ? "%2.2x" : " ", dp[ i ]); switch(i) { case 3: case 11: buf = str_append_c(buf, i < av ? ':' : ' '); break; case 7: buf = str_append_c(buf, i < av ? '-' : ' '); break; default: buf = str_append_c(buf, ' '); } } buf = str_append(buf, " "); for( i = 0; i < av; i ++ ) { unsigned char c = dp[ i ]; buf = str_append_c(buf, safe_isprint( c ) ? c : '.' ); } buf = str_append_c(buf, '\n'); fwrite(buf, str_len(buf), 1, t->log); off += av; dp += av; size -= av; } mem_free(buf); } /* This hook is called on every http_query completion */ void trace_http_query_hook (trace *t, http_query *q) { error err; if (t != NULL) { fprintf(t->log, "==============================\n"); /* Dump request */ fprintf(t->log, "%s %s\n", http_query_method(q), http_uri_str(http_query_uri(q))); http_query_foreach_request_header(q, trace_message_headers_foreach_callback, t); fprintf(t->log, "\n"); trace_dump_body(t, http_query_get_request_data(q)); /* Dump response */ err = http_query_transport_error(q); if (err != NULL) { fprintf(t->log, "Error: %s\n", ESTRING(err)); } else { int mp_count; fprintf(t->log, "Status: %d %s\n", http_query_status(q), http_query_status_string(q)); http_query_foreach_response_header(q, trace_message_headers_foreach_callback, t); fprintf(t->log, "\n"); trace_dump_body(t, http_query_get_response_data(q)); mp_count = http_query_get_mp_response_count(q); if (mp_count != 0) { int i; for (i = 0; i < mp_count; i ++) { http_data *part = http_query_get_mp_response_data(q, i); fprintf(t->log, "===== Part %d =====\n", i); fprintf(t->log, "Content-Type: %s\n", part->content_type); trace_dump_body(t, part); } } } fflush(t->log); fflush(t->data); } } /* Printf to the trace log */ void trace_printf (trace *t, const char *fmt, ...) { if (t != NULL) { va_list ap; va_start(ap, fmt); vfprintf(t->log, fmt, ap); putc('\n', t->log); fflush(t->log); va_end(ap); } } /* Note an error in trace log */ void trace_error (trace *t, error err) { trace_printf(t, "---"); trace_printf(t, "%s", ESTRING(err)); trace_printf(t, ""); } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-uuid.c000066400000000000000000000047211500411437100167750ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * UUID utilities */ #include "airscan.h" #include #include #include #include #include #pragma GCC diagnostic ignored "-Wunused-result" /* Invalid uuid */ static uuid uuid_invalid; /* Format UUID from 16-byte binary representation into * the following form: * urn:uuid:ede05377-460e-4b4a-a5c0-423f9e02e8fa */ static uuid uuid_format (uint8_t in[16]) { uuid u; sprintf(u.text, "urn:uuid:" "%.2x%.2x%.2x%.2x-%.2x%.2x-%.2x%.2x-%.2x%.2x-%.2x%.2x%.2x%.2x%.2x%.2x", in[0], in[1], in[2], in[3], in[4], in[5], in[6], in[7], in[8], in[9], in[10], in[11], in[12], in[13], in[14], in[15]); return u; } /* Generate random UUID. Generated UUID has a following form: * urn:uuid:ede05377-460e-4b4a-a5c0-423f9e02e8fa */ uuid uuid_rand (void) { uint8_t rnd[16]; rand_bytes(rnd, sizeof(rnd)); return uuid_format(rnd); } /* Parse UUID. This function ignores all "decorations", like * urn:uuid: prefix and so on, and takes only hexadecimal digits * into considerations * * Check the returned uuid with uuid_valid() for possible parse errors */ uuid uuid_parse (const char *in) { uint8_t buf[16]; unsigned int cnt = 0; unsigned char c; if (!strncasecmp(in, "urn:", 4)) { in += 4; } if (!strncasecmp(in, "uuid:", 5)) { in += 5; } while ((c = *in ++) != '\0') { if (isxdigit(c)) { unsigned int v; if (cnt == 32) { return uuid_invalid; } if (isdigit(c)) { v = c - '0'; } else if (isupper(c)) { v = c - 'A' + 10; } else { v = c - 'a' + 10; } if ((cnt & 1) == 0) { buf[cnt / 2] = v << 4; } else { buf[cnt / 2] |= v; } cnt ++; } } if (cnt != 32) { return uuid_invalid; } return uuid_format(buf); } /* Generate uuid by cryptographically cacheing input string */ uuid uuid_hash (const char *s) { uint8_t buf[32]; int rc; rc = gnutls_hash_fast(GNUTLS_DIG_SHA256, s, strlen(s), buf); log_assert(NULL, rc == 0); return uuid_format(buf); } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-wsd.c000066400000000000000000001214471500411437100166310ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * ESCL protocol handler */ #define _GNU_SOURCE #include #include "airscan.h" #include /* Protocol constants */ /* Miscellaneous strings, used by protocol */ #define WSD_ADDR_ANONYMOUS \ "http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous" #define WSD_ACTION_GET_SCANNER_ELEMENTS \ "http://schemas.microsoft.com/windows/2006/08/wdp/scan/GetScannerElements" #define WSD_ACTION_CREATE_SCAN_JOB \ "http://schemas.microsoft.com/windows/2006/08/wdp/scan/CreateScanJob" #define WSD_ACTION_RETRIEVE_IMAGE \ "http://schemas.microsoft.com/windows/2006/08/wdp/scan/RetrieveImage" #define WSD_ACTION_CANCEL_JOB \ "http://schemas.microsoft.com/windows/2006/08/wdp/scan/CancelJob" /* Retry parameters * * If CreateScanJobRequest is failed due to temporary reason (Calibrating, * LampWarming), request is retries several times * * WSD_CREATE_SCAN_JOB_RETRY_PAUSE defines pause between retries, * in milliseconds. WSD_CREATE_SCAN_JOB_RETRY_ATTEMPTS defines * an attempt limit */ #define WSD_CREATE_SCAN_JOB_RETRY_PAUSE 1000 #define WSD_CREATE_SCAN_JOB_RETRY_ATTEMPTS 30 /* XML namespace translation for XML reader */ static const xml_ns wsd_ns_rd[] = { {"s", "http*://schemas.xmlsoap.org/soap/envelope"}, /* SOAP 1.1 */ {"s", "http*://www.w3.org/2003/05/soap-envelope"}, /* SOAP 1.2 */ {"d", "http*://schemas.xmlsoap.org/ws/2005/04/discovery"}, {"a", "http*://schemas.xmlsoap.org/ws/2004/08/addressing"}, {"scan", "http*://schemas.microsoft.com/windows/2006/08/wdp/scan"}, {NULL, NULL} }; /* XML namespace definitions for XML writer */ static const xml_ns wsd_ns_wr[] = { {"soap", "http://www.w3.org/2003/05/soap-envelope"}, /* SOAP 1.2 */ {"wsa", "http://schemas.xmlsoap.org/ws/2004/08/addressing"}, {"sca", "http://schemas.microsoft.com/windows/2006/08/wdp/scan"}, {NULL, NULL} }; /* proto_handler_wsd represents WSD protocol handler */ typedef struct { proto_handler proto; /* Base class */ /* Error reasons decoding */ char fault_code[64]; /* Supported formats: JPEG variants */ bool exif; bool jfif; /* Supported formats: TIFF variabls */ bool tiff_single_uncompressed; bool tiff_single_g4; bool tiff_single_g3mh; bool tiff_single_jpeg_tn2; /* Other formats */ bool pdf_a; bool png; bool dib; /* Quirks */ /* Scanner doesn't handle sca:ImagesToTransfer if set to "0" * (which means "scan until ADF is empty"). * * This is the Ricoh Aficio MP 201 case. * * The workaround is to set sca:ImagesToTransfer to some big * arbitrary number. */ bool quirk_broken_ImagesToTransfer; } proto_handler_wsd; /* Forward declarations */ static http_query* wsd_status_query (const proto_ctx *ctx); /* Free WSD protocol handler */ static void wsd_free (proto_handler *proto) { mem_free(proto); } /* Create a HTTP POST request */ static http_query* wsd_http_post (const proto_ctx *ctx, char *body) { http_query *q; q = http_query_new(ctx->http, http_uri_clone(ctx->base_uri), "POST", body, "application/soap+xml"); http_query_set_request_header(q, "Cache-Control", "no-cache"); http_query_set_request_header(q, "Pragma", "no-cache"); http_query_set_request_header(q, "User-Agent", "WSDAPI"); return q; } /* Make SOAP header for outgoing request */ static void wsd_make_request_header (const proto_ctx *ctx, xml_wr *xml, const char *action) { uuid u = uuid_rand(); xml_wr_enter(xml, "soap:Header"); xml_wr_add_text(xml, "wsa:MessageID", u.text); //xml_wr_add_text(xml, "wsa:To", WSD_ADDR_ANONYMOUS); xml_wr_add_text(xml, "wsa:To", http_uri_str(ctx->base_uri_nozone)); xml_wr_enter(xml, "wsa:ReplyTo"); xml_wr_add_text(xml, "wsa:Address", WSD_ADDR_ANONYMOUS); xml_wr_leave(xml); xml_wr_add_text(xml, "wsa:Action", action); xml_wr_leave(xml); } /* Query device capabilities */ static http_query* wsd_devcaps_query (const proto_ctx *ctx) { xml_wr *xml = xml_wr_begin("soap:Envelope", wsd_ns_wr); wsd_make_request_header(ctx, xml, WSD_ACTION_GET_SCANNER_ELEMENTS); xml_wr_enter(xml, "soap:Body"); xml_wr_enter(xml, "sca:GetScannerElementsRequest"); xml_wr_enter(xml, "sca:RequestedElements"); /* sca:ScannerConfiguration response defines scanner capabilities, * such as document formats support, available sources, avaliable * resolutions, color modes etc. */ xml_wr_add_text(xml, "sca:Name", "sca:ScannerConfiguration"); /* These elements are only requested for logging, to provide some * device information for troubleshooting purposes */ xml_wr_add_text(xml, "sca:Name", "sca:ScannerDescription"); xml_wr_add_text(xml, "sca:Name", "sca:DefaultScanTicket"); xml_wr_add_text(xml, "sca:Name", "sca:ScannerStatus"); xml_wr_leave(xml); xml_wr_leave(xml); xml_wr_leave(xml); return wsd_http_post(ctx, xml_wr_finish_compact(xml)); } /* Parse supported formats */ static error wsd_devcaps_parse_formats (proto_handler_wsd *wsd, devcaps *caps, xml_rd *xml, unsigned int *formats_out) { error err = NULL; unsigned int level = xml_rd_depth(xml); size_t prefixlen = strlen(xml_rd_node_path(xml)); unsigned int formats = 0; (void) caps; /* Decode supported formats */ while (!xml_rd_end(xml)) { const char *path = xml_rd_node_path(xml) + prefixlen; if (!strcmp(path, "/scan:FormatValue")) { const char *v = xml_rd_node_value(xml); if (!strcmp(v, "jfif")) { wsd->jfif = true; } else if (!strcmp(v, "exif")) { wsd->exif = true; } else if (!strcmp(v, "tiff-single-uncompressed")) { wsd->tiff_single_uncompressed = true; } else if (!strcmp(v, "tiff-single-g4")) { wsd->tiff_single_g4 = true; } else if (!strcmp(v, "tiff-single-g3mh")) { wsd->tiff_single_g3mh = true; } else if (!strcmp(v, "tiff-single-jpeg-tn2")) { wsd->tiff_single_jpeg_tn2 = true; } else if (!strcmp(v, "pdf-a")) { wsd->pdf_a = true; } else if (!strcmp(v, "png")) { wsd->png = true; } else if (!strcmp(v, "dib")) { wsd->dib = true; } } xml_rd_deep_next(xml, level); } /* Set formats bits */ if (wsd->jfif || wsd->exif) { formats |= 1 << ID_FORMAT_JPEG; } if (wsd->pdf_a) { formats |= 1 << ID_FORMAT_PDF; } if (wsd->png) { formats |= 1 << ID_FORMAT_PNG; } if (wsd->tiff_single_g4 || wsd->tiff_single_g3mh) { formats |= 1 << ID_FORMAT_TIFF; } if ((formats & DEVCAPS_FORMATS_SUPPORTED) == 0) { /* These used as last resort */ if (wsd->tiff_single_jpeg_tn2 || wsd->tiff_single_uncompressed) { formats |= 1 << ID_FORMAT_TIFF; } if (wsd->dib) { formats |= 1 << ID_FORMAT_BMP; } } *formats_out = formats; if (((formats) & DEVCAPS_FORMATS_SUPPORTED) == 0) { err = ERROR("no supported image formats"); } return err; } /* Parse supported content types and map them to scan intents */ static error wsd_devcaps_parse_content_types (devcaps *caps, xml_rd *xml, unsigned int *scanintents_out) { error err = NULL; unsigned int level = xml_rd_depth(xml); size_t prefixlen = strlen(xml_rd_node_path(xml)); unsigned int scanintents = 0; (void) caps; /* Decode supported content types */ while (!xml_rd_end(xml)) { const char *path = xml_rd_node_path(xml) + prefixlen; if (!strcmp(path, "/scan:ContentTypeValue")) { const char *v = xml_rd_node_value(xml); if (!strcmp(v, "Auto")) { scanintents |= 1 << ID_SCANINTENT_AUTO; } else if (!strcmp(v, "Text")) { scanintents |= 1 << ID_SCANINTENT_DOCUMENT; } else if (!strcmp(v, "Photo")) { scanintents |= 1 << ID_SCANINTENT_PHOTO; } else if (!strcmp(v, "Halftone")) { scanintents |= 1 << ID_SCANINTENT_HALFTONE; } else if (!strcmp(v, "Mixed")) { scanintents |= 1 << ID_SCANINTENT_TEXTANDGRAPHIC; } else { log_debug(NULL, "unknown content type: %s", v); } } xml_rd_deep_next(xml, level); } *scanintents_out = scanintents; return err; } /* Parse size */ static error wsd_devcaps_parse_size (SANE_Word *out, xml_rd *xml) { SANE_Word val; error err = xml_rd_node_value_uint(xml, &val); if (err == NULL && *out < 0) { *out = val; } return err; } /* Parse resolution and append it to array of resolutions */ static error wsd_devcaps_parse_res (SANE_Word **res, xml_rd *xml) { SANE_Word val; error err = xml_rd_node_value_uint(xml, &val); if (err == NULL) { *res = sane_word_array_append(*res, val); } return err; } /* Parse source configuration */ static error wsd_devcaps_parse_source (devcaps *caps, xml_rd *xml, ID_SOURCE src_id) { error err = NULL; unsigned int level = xml_rd_depth(xml); size_t prefixlen = strlen(xml_rd_node_path(xml)); devcaps_source *src = devcaps_source_new(); SANE_Word *x_res = sane_word_array_new(); SANE_Word *y_res = sane_word_array_new(); SANE_Word min_wid = -1, max_wid = -1, min_hei = -1, max_hei = -1; while (!xml_rd_end(xml)) { const char *path = xml_rd_node_path(xml) + prefixlen; if (!strcmp(path, "/scan:PlatenResolutions/scan:Widths/scan:Width") || !strcmp(path, "/scan:ADFResolutions/scan:Widths/scan:Width")) { err = wsd_devcaps_parse_res(&x_res, xml); } else if (!strcmp(path, "/scan:PlatenResolutions/scan:Heights/scan:Height") || !strcmp(path, "/scan:ADFResolutions/scan:Heights/scan:Height")) { err = wsd_devcaps_parse_res(&y_res, xml); } else if (!strcmp(path, "/scan:PlatenMinimumSize/scan:Width") || !strcmp(path, "/scan:ADFMinimumSize/scan:Width")) { err = wsd_devcaps_parse_size(&min_wid, xml); } else if (!strcmp(path, "/scan:PlatenMinimumSize/scan:Height") || !strcmp(path, "/scan:ADFMinimumSize/scan:Height")) { err = wsd_devcaps_parse_size(&min_hei, xml); } else if (!strcmp(path, "/scan:PlatenMaximumSize/scan:Width") || !strcmp(path, "/scan:ADFMaximumSize/scan:Width")) { err = wsd_devcaps_parse_size(&max_wid, xml); } else if (!strcmp(path, "/scan:PlatenMaximumSize/scan:Height") || !strcmp(path, "/scan:ADFMaximumSize/scan:Height")) { err = wsd_devcaps_parse_size(&max_hei, xml); } else if (!strcmp(path, "/scan:PlatenColor/scan:ColorEntry") || !strcmp(path, "/scan:ADFColor/scan:ColorEntry")) { const char *v = xml_rd_node_value(xml); if (!strcmp(v, "BlackAndWhite1")) { src->colormodes |= 1 << ID_COLORMODE_BW1; } else if (!strcmp(v, "Grayscale8")) { src->colormodes |= 1 << ID_COLORMODE_GRAYSCALE; } else if (!strcmp(v, "RGB24")) { src->colormodes |= 1 << ID_COLORMODE_COLOR; } } if (err != NULL) { break; } xml_rd_deep_next(xml, level); } /* Merge x/y resolutions */ if (err == NULL) { sane_word_array_sort(x_res); sane_word_array_sort(y_res); sane_word_array_free(src->resolutions); src->resolutions = sane_word_array_intersect_sorted(x_res, y_res); if (sane_word_array_len(src->resolutions) > 0) { src->flags |= DEVCAPS_SOURCE_RES_DISCRETE; } else { err = ERROR("no resolutions defined"); } } /* Check things */ src->colormodes &= DEVCAPS_COLORMODES_SUPPORTED; if (err == NULL && src->colormodes == 0) { err = ERROR("no color modes defined"); } if (err == NULL && min_wid < 0) { err = ERROR("minimum width not defined"); } if (err == NULL && min_hei < 0) { err = ERROR("minimum height not defined"); } if (err == NULL && max_wid < 0) { err = ERROR("maximum width not defined"); } if (err == NULL && max_hei < 0) { err = ERROR("maximum height not defined"); } if (err == NULL && min_wid > max_wid) { err = ERROR("minimum width > maximum width"); } if (err == NULL && min_hei > max_hei) { err = ERROR("minimum height > maximum height"); } /* Fix things * * Note. Some scanners (namely, Kyocera ECOSYS M2040dn) * return width and height swapped. As a workaround, * we flip if back, if width is greater that heigh * * FIXME: more reliable detection of need to flip * width and height is required */ if (max_wid > max_hei) { SANE_Word tmp; tmp = max_wid; max_wid = max_hei; max_hei = tmp; tmp = min_wid; min_wid = min_hei; min_hei = tmp; } /* Save min/max width and height */ src->min_wid_px = min_wid; src->max_wid_px = max_wid; src->min_hei_px = min_hei; src->max_hei_px = max_hei; /* Save source */ if (err == NULL) { if (caps->src[src_id] == NULL) { caps->src[src_id] = src; } else { devcaps_source_free(src); } } /* Cleanup and exit */ sane_word_array_free(x_res); sane_word_array_free(y_res); return err; } /* Parse scanner configuration */ static error wsd_devcaps_parse_configuration (proto_handler_wsd *wsd, devcaps *caps, xml_rd *xml) { error err = NULL; unsigned int level = xml_rd_depth(xml); size_t prefixlen = strlen(xml_rd_node_path(xml)); bool adf = false, duplex = false; unsigned int formats = 0; unsigned int scanintents = 0; int i; /* Parse configuration */ while (!xml_rd_end(xml)) { const char *path = xml_rd_node_path(xml) + prefixlen; if (!strcmp(path, "/scan:DeviceSettings/scan:FormatsSupported")) { err = wsd_devcaps_parse_formats(wsd, caps, xml, &formats); } else if (!strcmp(path, "/scan:DeviceSettings/scan:ContentTypesSupported")) { err = wsd_devcaps_parse_content_types(caps, xml, &scanintents); } else if (!strcmp(path, "/scan:Platen")) { err = wsd_devcaps_parse_source(caps, xml, ID_SOURCE_PLATEN); } else if (!strcmp(path, "/scan:ADF/scan:ADFFront")) { adf = true; err = wsd_devcaps_parse_source(caps, xml, ID_SOURCE_ADF_SIMPLEX); } else if (!strcmp(path, "/scan:ADF/scan:ADFBack")) { err = wsd_devcaps_parse_source(caps, xml, ID_SOURCE_ADF_DUPLEX); } else if (!strcmp(path, "/scan:ADF/scan:ADFSupportsDuplex")) { const char *v = xml_rd_node_value(xml); duplex = !strcmp(v, "1") || !strcmp(v, "true"); } else { //log_debug(NULL, "CONF: %s", path); } if (err != NULL) { return err; } xml_rd_deep_next(xml, level); } /* Adjust sources */ for (i = 0; i < NUM_ID_SOURCE; i ++) { devcaps_source *src = caps->src[i]; if (src != NULL) { src->formats = formats; src->scanintents = scanintents; /* Note, as we can clip in software, we indicate * minimal scan region size for SANE as 0x0. But * maximal size is defined by hardware */ src->win_x_range_mm.min = src->win_y_range_mm.min = 0; src->win_x_range_mm.max = math_px2mm_res(src->max_wid_px, 1000); src->win_y_range_mm.max = math_px2mm_res(src->max_hei_px, 1000); } } /* Workaround for Brother MFC-9340CDW * * This device reports ADFSupportsDuplex as 0, but returns both * ADFFront and ADFBack elements. * * As a workaround, we assume that if both ADFFront and ADFBack are * present (temporary saved under the ID_SOURCE_ADF_SIMPLEX and * ID_SOURCE_ADF_DUPLEX slots), scanner supports duplex mode, * regardless of the ADFSupportsDuplex value it returns */ if (adf && caps->src[ID_SOURCE_ADF_DUPLEX] != NULL) { duplex = true; } /* Please note that the standard model for SANE and for our implementation * involves having two separate configurations for the duplex ADF: one for * simplex mode and another for duplex mode. In duplex mode, it is assumed * that the front and back page scanning will have the same * characteristics. * * However, WSD employs a slightly different model. Instead of providing * separate source configurations for simplex and duplex modes, it offers a * source configuration for the ADF front, which is required when the ADF * is supported by the device, and an optional configuration for the ADF * back. * * According to the specification, the ADF back configuration is optional. * If the scanner indicates duplex support (via the ADFSupportsDuplex) but * does not provide a separate ADFBack element, the ADFBack should be * assumed to be the same as ADFFront. * * During the decoding process, we temporarily store the ADF front * information under the ID_SOURCE_ADF_SIMPLEX and the ADF back information * under the ID_SOURCE_ADF_DUPLEX slots, and then make adjustments. * * When adjusting, we assume that the ADF front applies to both simplex and * duplex modes, while the ADF back applies only to duplex mode. * * Therefore, if duplex is supported, we either merge the front and back * configurations if both are present or simply copy the front * configuration to the back if the back configuration is missing. */ if (adf && duplex) { log_assert(NULL, caps->src[ID_SOURCE_ADF_SIMPLEX] != NULL); if (caps->src[ID_SOURCE_ADF_DUPLEX] == NULL) { caps->src[ID_SOURCE_ADF_DUPLEX] = devcaps_source_clone(caps->src[ID_SOURCE_ADF_SIMPLEX]); } else { devcaps_source *src; src = devcaps_source_merge(caps->src[ID_SOURCE_ADF_SIMPLEX], caps->src[ID_SOURCE_ADF_DUPLEX]); devcaps_source_free(caps->src[ID_SOURCE_ADF_DUPLEX]); caps->src[ID_SOURCE_ADF_DUPLEX] = src; } } else if (caps->src[ID_SOURCE_ADF_DUPLEX] != NULL) { devcaps_source_free(caps->src[ID_SOURCE_ADF_DUPLEX]); caps->src[ID_SOURCE_ADF_DUPLEX] = NULL; } /* Workaround for yet another Kyocera bug. This device doesn't * honor scan region settings. I.e., it understands it, * properly mirrors in DocumentFinalParameters, but completely * ignores when generating the image. * * So we can't rely on device's ability to clip the image and * must implement clipping in software. It can be enforced * in our backend by setting minimum image size equal to * max size. */ for (i = 0; i < NUM_ID_SOURCE; i ++) { devcaps_source *src = caps->src[i]; if (src != NULL) { src->min_wid_px = src->max_wid_px; src->min_hei_px = src->max_hei_px; } } /* Check that we have at least one source */ ID_SOURCE id_src; bool src_ok = false; for (id_src = (ID_SOURCE) 0; id_src < NUM_ID_SOURCE; id_src ++) { if (caps->src[id_src] != NULL) { src_ok = true; } } if (!src_ok) { return ERROR("neither platen nor ADF sources detected"); } return NULL; } /* Parse device capabilities */ static error wsd_devcaps_parse (proto_handler_wsd *wsd, devcaps *caps, const char *xml_text, size_t xml_len) { error err = NULL; xml_rd *xml; bool found_configuration = false; /* Fill "constant" part of device capabilities */ caps->units = 1000; caps->protocol = wsd->proto.name; caps->justification_x = caps->justification_y = ID_JUSTIFICATION_UNKNOWN; /* Parse capabilities XML */ err = xml_rd_begin(&xml, xml_text, xml_len, wsd_ns_rd); if (err != NULL) { goto DONE; } while (!xml_rd_end(xml)) { const char *path = xml_rd_node_path(xml); if (!strcmp(path, "s:Envelope/s:Body" "/scan:GetScannerElementsResponse/scan:ScannerElements/" "scan:ElementData/scan:ScannerConfiguration")) { found_configuration = true; err = wsd_devcaps_parse_configuration(wsd, caps, xml); } if (err != NULL) { goto DONE; } xml_rd_deep_next(xml, 0); } /* Check things */ if (!found_configuration) { err = ERROR("ScannerConfiguration missed"); } DONE: if (err != NULL) { devcaps_reset(caps); } xml_rd_finish(&xml); return err; } /* Decode device capabilities */ static error wsd_devcaps_decode (const proto_ctx *ctx, devcaps *caps) { proto_handler_wsd *wsd = (proto_handler_wsd*) ctx->proto; http_data *data = http_query_get_response_data(ctx->query); error err; /* Setup quirks */ if (!strcmp(ctx->devinfo->model, "RICOH Aficio MP 201")) { wsd->quirk_broken_ImagesToTransfer = true; } /* Parse device capabilities response */ err = wsd_devcaps_parse(wsd, caps, data->bytes, data->size); return err; } /* Check if response is fault response without decoding it */ static bool wsd_fault_check (const proto_ctx *ctx) { http_data *data; static const char fault[] = "//schemas.xmlsoap.org/ws/2004/08/addressing/fault"; /* If we have erroneous HTTP status, we expect to see fault message * inside */ if (http_query_error(ctx->query) != NULL) { return true; } /* Some devices (namely Lexmark MB2236adw and Xerox WorkCentre 3225) * may use HTTP status 200 to return fault response, so check for * the HTTP status code is not enough to distinguish between normal * and fault response * * So we search the response body for the following string: * "//schemas.xmlsoap.org/ws/2004/08/addressing/fault" * * If this string is found, this is probably a fault response * * Note, the scheme is stripped from this string, because some * devices use "http://", why another may use "https://" * * Note, as optimization and to avoid searching this string * across the image date, we assume that if we have got MIME * multipart response, it is probably not fault. */ if (http_query_get_mp_response_count(ctx->query) != 0) { return false; } data = http_query_get_response_data(ctx->query); if (memmem(data->bytes, data->size, fault, sizeof(fault) - 1) != NULL) { return true; } return false; } /* Decode fault response */ static proto_result wsd_fault_decode (const proto_ctx *ctx, bool cleanup) { proto_handler_wsd *wsd = (proto_handler_wsd*) ctx->proto; proto_result result = {0}; http_data *data = http_query_get_response_data(ctx->query); xml_rd *xml; /* Parse XML */ result.err = xml_rd_begin(&xml, data->bytes, data->size, wsd_ns_rd); if (result.err != NULL) { result.next = cleanup ? PROTO_OP_CLEANUP : PROTO_OP_FINISH; result.status = SANE_STATUS_IO_ERROR; return result; } /* Decode XML, save fault code */ while (!xml_rd_end(xml)) { const char *path = xml_rd_node_path(xml); if (!strcmp(path, "s:Envelope/s:Body/s:Fault/s:Code/s:Subcode/s:Value")) { const char *fault = xml_rd_node_value(xml); const char *s; /* Skip namespace prefix */ s = strchr(fault, ':'); if (s != NULL) { fault = s + 1; } /* Save the status */ log_debug(ctx->log, "fault code: %s", fault); strncpy(wsd->fault_code, fault, sizeof(wsd->fault_code) - 1); } xml_rd_deep_next(xml, 0); } xml_rd_finish(&xml); result.next = PROTO_OP_CHECK; return result; } /* Create pre-scan check query */ static http_query* wsd_precheck_query (const proto_ctx *ctx) { return wsd_status_query(ctx); } /* Decode pre-scan check query results */ static proto_result wsd_precheck_decode (const proto_ctx *ctx) { proto_result result = {0}; (void) ctx; result.next = PROTO_OP_SCAN; result.status = SANE_STATUS_GOOD; return result; } /* Initiate scanning */ static http_query* wsd_scan_query (const proto_ctx *ctx) { proto_handler_wsd *wsd = (proto_handler_wsd*) ctx->proto; const proto_scan_params *params = &ctx->params; xml_wr *xml = xml_wr_begin("soap:Envelope", wsd_ns_wr); const char *source = NULL; const char *colormode = NULL; const char *contenttype = NULL; const char *format = NULL; static const char *sides_simplex[] = {"sca:MediaFront", NULL}; static const char *sides_duplex[] = {"sca:MediaFront", "sca:MediaBack", NULL}; const char **sides; int i; /* Prepare parameters */ switch (params->src) { case ID_SOURCE_PLATEN: source = "Platen"; break; case ID_SOURCE_ADF_SIMPLEX: source = "ADF"; break; case ID_SOURCE_ADF_DUPLEX: source = "ADFDuplex"; break; default: log_internal_error(ctx->log); } sides = params->src == ID_SOURCE_ADF_DUPLEX ? sides_duplex : sides_simplex; switch (params->colormode) { case ID_COLORMODE_COLOR: colormode = "RGB24"; break; case ID_COLORMODE_GRAYSCALE: colormode = "Grayscale8"; break; case ID_COLORMODE_BW1: colormode = "BlackAndWhite1"; break; default: log_internal_error(ctx->log); } switch (params->scanintent) { case ID_SCANINTENT_UNSET: break; case ID_SCANINTENT_AUTO: contenttype = "Auto"; break; case ID_SCANINTENT_DOCUMENT: contenttype = "Text"; break; case ID_SCANINTENT_PHOTO: contenttype = "Photo"; break; case ID_SCANINTENT_HALFTONE: contenttype = "Halftone"; break; case ID_SCANINTENT_TEXTANDGRAPHIC: contenttype = "Mixed"; break; default: log_internal_error(ctx->log); } /* Create scan request */ wsd_make_request_header(ctx, xml, WSD_ACTION_CREATE_SCAN_JOB); xml_wr_enter(xml, "soap:Body"); xml_wr_enter(xml, "sca:CreateScanJobRequest"); xml_wr_enter(xml, "sca:ScanTicket"); xml_wr_enter(xml, "sca:JobDescription"); xml_wr_add_text(xml, "sca:JobName", "sane-airscan request"); xml_wr_add_text(xml, "sca:JobOriginatingUserName", "sane-airscan"); /* WS-Scan specification says that this parameter is optional, * but without this parameter the Canon TR7500 rejects scan * request with the InvalidArgs error */ xml_wr_add_text(xml, "sca:JobInformation", "sane-airscan"); xml_wr_leave(xml); // sca:JobDescription xml_wr_enter(xml, "sca:DocumentParameters"); switch (ctx->params.format) { case ID_FORMAT_JPEG: if (wsd->jfif) { format = "jfif"; } else if (wsd->exif) { format = "exif"; } break; case ID_FORMAT_TIFF: if (wsd->tiff_single_g4) { format = "tiff-single-g4"; } else if (wsd->tiff_single_g3mh) { format = "tiff-single-g3mh"; } else if (wsd->tiff_single_jpeg_tn2) { format = "tiff-single-jpeg-tn2"; } else if (wsd->tiff_single_uncompressed) { format = "tiff-single-uncompressed"; } break; case ID_FORMAT_PNG: if (wsd->png) { format = "png"; } break; case ID_FORMAT_PDF: if (wsd->pdf_a) { format = "pdf-a"; } break; case ID_FORMAT_BMP: if (wsd->dib) { format = "dib"; } break; case ID_FORMAT_UNKNOWN: case NUM_ID_FORMAT: break; } log_assert(ctx->log, format != NULL); xml_wr_add_text(xml, "sca:Format", format); /* WS-Scan specification says unspecified scan amount should be 0 * ( unknown amount, check for more ) and for Flatbed that is 1. */ switch (params->src) { case ID_SOURCE_PLATEN: xml_wr_add_text(xml, "sca:ImagesToTransfer", "1"); break; case ID_SOURCE_ADF_SIMPLEX: case ID_SOURCE_ADF_DUPLEX: if (wsd->quirk_broken_ImagesToTransfer) { xml_wr_add_text(xml, "sca:ImagesToTransfer", "100"); } else { xml_wr_add_text(xml, "sca:ImagesToTransfer", "0"); } break; default: log_internal_error(ctx->log); } if (contenttype) { xml_wr_add_text(xml, "sca:ContentType", contenttype); } xml_wr_enter(xml, "sca:InputSize"); xml_wr_enter(xml, "sca:InputMediaSize"); xml_wr_add_uint(xml, "sca:Width", params->wid); xml_wr_add_uint(xml, "sca:Height", params->hei); xml_wr_leave(xml); // sca:InputMediaSize xml_wr_leave(xml); // sca:InputSize xml_wr_add_text(xml, "sca:InputSource", source); xml_wr_enter(xml, "sca:MediaSides"); for (i = 0; sides[i] != NULL; i ++) { xml_wr_enter(xml, sides[i]); xml_wr_add_text(xml, "sca:ColorProcessing", colormode); xml_wr_enter(xml, "sca:Resolution"); xml_wr_add_uint(xml, "sca:Width", params->x_res); xml_wr_add_uint(xml, "sca:Height", params->y_res); xml_wr_leave(xml); // sca:Resolution xml_wr_enter(xml, "sca:ScanRegion"); xml_wr_add_uint(xml, "sca:ScanRegionXOffset", params->x_off); xml_wr_add_uint(xml, "sca:ScanRegionYOffset", params->y_off); xml_wr_add_uint(xml, "sca:ScanRegionWidth", params->wid); xml_wr_add_uint(xml, "sca:ScanRegionHeight", params->hei); xml_wr_leave(xml); // sca:ScanRegion xml_wr_leave(xml); } xml_wr_leave(xml); // sca:MediaSides xml_wr_leave(xml); // sca:DocumentParameters xml_wr_leave(xml); // sca:ScanTicket xml_wr_leave(xml); // sca:CreateScanJobRequest xml_wr_leave(xml); // soap:Body //log_debug(0, "%s", xml_wr_finish_compact(xml)); exit(0); return wsd_http_post(ctx, xml_wr_finish_compact(xml)); } /* Decode result of scan request */ static proto_result wsd_scan_decode (const proto_ctx *ctx) { proto_result result = {0}; error err = NULL; xml_rd *xml = NULL; http_data *data; SANE_Word job_id = -1; char *job_token = NULL; result.next = PROTO_OP_FINISH; /* Decode error, if any */ if (wsd_fault_check(ctx)) { return wsd_fault_decode(ctx, false); } /* Decode CreateScanJobResponse */ data = http_query_get_response_data(ctx->query); err = xml_rd_begin(&xml, data->bytes, data->size, wsd_ns_rd); if (err != NULL) { err = eloop_eprintf("XML: %s", ESTRING(err)); goto DONE; } while (!xml_rd_end(xml)) { const char *path = xml_rd_node_path(xml); if (!strcmp(path, "s:Envelope/s:Body/scan:CreateScanJobResponse" "/scan:JobId")) { err = xml_rd_node_value_uint(xml, &job_id); } else if (!strcmp(path, "s:Envelope/s:Body/scan:CreateScanJobResponse" "/scan:JobToken")) { mem_free(job_token); job_token = str_dup(xml_rd_node_value(xml)); } xml_rd_deep_next(xml, 0); } if (job_id == -1) { err = ERROR("missed JobId"); goto DONE; } if (job_token == NULL) { err = ERROR("missed JobToken"); goto DONE; } result.next = PROTO_OP_LOAD; result.data.location = str_printf("%u:%s", job_id, job_token); /* Cleanup and exit */ DONE: xml_rd_finish(&xml); mem_free(job_token); if (err != NULL) { result.err = eloop_eprintf("CreateScanJobResponse: %s", ESTRING(err)); } if (result.next == PROTO_OP_FINISH) { result.status = SANE_STATUS_IO_ERROR; } return result; } /* Initiate image downloading */ static http_query* wsd_load_query (const proto_ctx *ctx) { xml_wr *xml = xml_wr_begin("soap:Envelope", wsd_ns_wr); char *job_id, *job_token; /* Split location into JobId and JobToken */ job_id = alloca(strlen(ctx->location) + 1); strcpy(job_id, ctx->location); job_token = strchr(job_id, ':'); *job_token ++ = '\0'; /* Build RetrieveImageRequest */ wsd_make_request_header(ctx, xml, WSD_ACTION_RETRIEVE_IMAGE); xml_wr_enter(xml, "soap:Body"); xml_wr_enter(xml, "sca:RetrieveImageRequest"); xml_wr_enter(xml, "sca:DocumentDescription"); xml_wr_add_text(xml, "sca:DocumentName", "IMAGE000.JPG"); xml_wr_leave(xml); xml_wr_add_text(xml, "sca:JobId", job_id); xml_wr_add_text(xml, "sca:JobToken", job_token); xml_wr_leave(xml); xml_wr_leave(xml); return wsd_http_post(ctx, xml_wr_finish_compact(xml)); } /* Decode result of image request */ static proto_result wsd_load_decode (const proto_ctx *ctx) { proto_result result = {0}; http_data *data; /* Check HTTP status */ if (wsd_fault_check(ctx)) { return wsd_fault_decode(ctx, true); } /* We expect multipart message with attached image */ data = http_query_get_mp_response_data(ctx->query, 1); if (data == NULL) { result.next = PROTO_OP_CLEANUP; result.err = ERROR("RetrieveImageRequest: invalid response"); return result; } if (ctx->params.src == ID_SOURCE_PLATEN) { result.next = PROTO_OP_FINISH; } else { result.next = PROTO_OP_LOAD; } result.data.image = http_data_ref(data); return result; } /* Request device status */ static http_query* wsd_status_query (const proto_ctx *ctx) { xml_wr *xml = xml_wr_begin("soap:Envelope", wsd_ns_wr); wsd_make_request_header(ctx, xml, WSD_ACTION_GET_SCANNER_ELEMENTS); xml_wr_enter(xml, "soap:Body"); xml_wr_enter(xml, "sca:GetScannerElementsRequest"); xml_wr_enter(xml, "sca:RequestedElements"); xml_wr_add_text(xml, "sca:Name", "sca:ScannerStatus"); xml_wr_leave(xml); xml_wr_leave(xml); xml_wr_leave(xml); return wsd_http_post(ctx, xml_wr_finish_compact(xml)); } /* Decode result of device status request */ static proto_result wsd_status_decode (const proto_ctx *ctx) { proto_handler_wsd *wsd = (proto_handler_wsd*) ctx->proto; proto_result result = {0}; http_data *data = http_query_get_response_data(ctx->query); xml_rd *xml; char scanner_state[64] = {0}; bool adf = ctx->params.src == ID_SOURCE_ADF_SIMPLEX || ctx->params.src == ID_SOURCE_ADF_DUPLEX; bool retry = false; log_debug(ctx->log, "PROTO_OP_CHECK: fault code: %s", wsd->fault_code); /* Initialize result */ result.next = PROTO_OP_FINISH; result.status = SANE_STATUS_GOOD; /* Look to the saved fault code. It it is specific enough, return * error immediately */ if (adf) { if (!strcmp(wsd->fault_code, "ClientErrorNoImagesAvailable")) { result.status = SANE_STATUS_NO_DOCS; return result; } /* Ricoh Aficio MP 201 reports "ADF empty" status this strange way */ if (!strcmp(wsd->fault_code, "ClientErrorJobIdNotFound")) { result.status = SANE_STATUS_NO_DOCS; return result; } } /* Parse XML */ result.err = xml_rd_begin(&xml, data->bytes, data->size, wsd_ns_rd); if (result.err != NULL) { return result; } /* Roll over parsed XML, until fault reason is known */ while (!xml_rd_end(xml) && result.status == SANE_STATUS_GOOD && !retry) { const char *path = xml_rd_node_path(xml); const char *val; if (!strcmp(path, "s:Envelope/s:Body/scan:GetScannerElementsResponse/" "scan:ScannerElements/scan:ElementData/scan:ScannerStatus/" "scan:ScannerState")) { val = xml_rd_node_value(xml); log_debug(ctx->log, "PROTO_OP_CHECK: ScannerState: %s", val); strncpy(scanner_state, val, sizeof(scanner_state) - 1); } else if (!strcmp(path, "s:Envelope/s:Body/scan:GetScannerElementsResponse/" "scan:ScannerElements/scan:ElementData/scan:ScannerStatus/" "scan:ScannerStateReasons/scan:ScannerStateReason")) { val = xml_rd_node_value(xml); log_debug(ctx->log, "PROTO_OP_CHECK: ScannerStateReason: %s", val); if (!strcmp(val, "AttentionRequired")) { result.status = SANE_STATUS_DEVICE_BUSY; } else if (!strcmp(val, "Calibrating")) { retry = true; } else if (!strcmp(val, "CoverOpen")) { result.status = SANE_STATUS_COVER_OPEN; } else if (!strcmp(val, "InterlockOpen")) { // Note, I have no idea what is interlock, but // let's assume it's a kind of cover... result.status = SANE_STATUS_COVER_OPEN; } else if (!strcmp(val, "InternalStorageFull")) { result.status = SANE_STATUS_NO_MEM; } else if (!strcmp(val, "LampError")) { result.status = SANE_STATUS_IO_ERROR; } else if (!strcmp(val, "LampWarming")) { retry = true; } else if (!strcmp(val, "MediaJam")) { result.status = SANE_STATUS_JAMMED; } else if (!strcmp(val, "MultipleFeedError")) { result.status = SANE_STATUS_JAMMED; } } xml_rd_deep_next(xml, 0); } xml_rd_finish(&xml); /* Retry? */ if (retry && ctx->failed_attempt < WSD_CREATE_SCAN_JOB_RETRY_ATTEMPTS) { result.next = PROTO_OP_SCAN; result.delay = WSD_CREATE_SCAN_JOB_RETRY_PAUSE; return result; } /* Reason was found? */ if (result.status != SANE_STATUS_GOOD) { return result; } /* ServerErrorNotAcceptingJobs? */ if (!strcmp(wsd->fault_code, "ServerErrorNotAcceptingJobs")) { /* Assume device not accepted jobs because require some * manual action/reconfiguration. For example, Kyocera * in the WSD mode scans only WSD scan is requested from * the front panel, otherwise it returns this kind * of error */ result.status = SANE_STATUS_DEVICE_BUSY; /* Canon MF410 Series reports ADF empty this way */ if (adf && !strcmp(scanner_state, "Idle")) { result.status = SANE_STATUS_NO_DOCS; } } /* Still no idea? */ if (result.status == SANE_STATUS_GOOD) { result.status = SANE_STATUS_IO_ERROR; } return result; } /* Cancel scan in progress */ static http_query* wsd_cancel_query (const proto_ctx *ctx) { xml_wr *xml = xml_wr_begin("soap:Envelope", wsd_ns_wr); char *job_id, *job_token; /* Split location into JobId and JobToken */ job_id = alloca(strlen(ctx->location) + 1); strcpy(job_id, ctx->location); job_token = strchr(job_id, ':'); *job_token ++ = '\0'; /* Build CancelJob Request */ wsd_make_request_header(ctx, xml, WSD_ACTION_CANCEL_JOB); xml_wr_enter(xml, "soap:Body"); xml_wr_enter(xml, "sca:CancelJobRequest"); xml_wr_add_text(xml, "sca:JobId", job_id); xml_wr_leave(xml); xml_wr_leave(xml); return wsd_http_post(ctx, xml_wr_finish_compact(xml)); } /* Test interface: decode device capabilities */ static error wsd_test_decode_devcaps (proto_handler *proto, const void *xml_text, size_t xms_size, devcaps *caps) { proto_handler_wsd *wsd = (proto_handler_wsd*) proto; return wsd_devcaps_parse(wsd, caps, xml_text, xms_size); } /* proto_handler_wsd_new creates new WSD protocol handler */ proto_handler* proto_handler_wsd_new (void) { proto_handler_wsd *wsd = mem_new(proto_handler_wsd, 1); wsd->proto.name = "WSD"; wsd->proto.free = wsd_free; wsd->proto.devcaps_query = wsd_devcaps_query; wsd->proto.devcaps_decode = wsd_devcaps_decode; wsd->proto.precheck_query = wsd_precheck_query; wsd->proto.precheck_decode = wsd_precheck_decode; wsd->proto.scan_query = wsd_scan_query; wsd->proto.scan_decode = wsd_scan_decode; wsd->proto.load_query = wsd_load_query; wsd->proto.load_decode = wsd_load_decode; wsd->proto.status_query = wsd_status_query; wsd->proto.status_decode = wsd_status_decode; wsd->proto.cleanup_query = wsd_cancel_query; wsd->proto.cancel_query = wsd_cancel_query; wsd->proto.test_decode_devcaps = wsd_test_decode_devcaps; return &wsd->proto; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-wsdd.c000066400000000000000000001634731500411437100170020ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Web Services Dynamic Discovery (WS-Discovery) */ #define _GNU_SOURCE #include "airscan.h" #include #include #include #include #include #include #include #include #ifdef BSD # include #endif #include /* Protocol times, in milliseconds */ #define WSDD_RETRANSMIT_MIN 100 /* Min retransmit time */ #define WSDD_RETRANSMIT_MAX 250 /* Max retransmit time */ #define WSDD_DISCOVERY_TIME 2500 /* Standard discovery time */ #define WSDD_DISCOVERY_TIME_EX 5000 /* Extended discovery time */ /* WS-Discovery stable endpoint path */ #define WSDD_STABLE_ENDPOINT \ "/StableWSDiscoveryEndpoint/schemas-xmlsoap-org_ws_2005_04_discovery" /* wsdd_resolver represents a per-interface WSDD resolver */ typedef struct { int fd; /* File descriptor */ int ifindex; /* Interface index */ netif_name ifname; /* Interface name, for logging */ bool ipv6; /* We are on IPv6 */ eloop_fdpoll *fdpoll; /* Socket fdpoll */ eloop_timer *timer; /* Retransmit timer */ uint32_t time_limit; /* Discovery time limit */ uint32_t time_elapsed; /* Elapsed time */ ip_straddr str_ifaddr; /* Interface address */ ip_straddr str_sockaddr; /* Per-interface socket address */ bool initscan; /* Initial scan in progress */ } wsdd_resolver; /* wsdd_finding represents zeroconf_finding for WSD * device discovery */ typedef struct { zeroconf_finding finding; /* Base class */ const char *address; /* Device WS-SD "address" */ ll_head xaddrs; /* List of wsdd_xaddr */ ll_head xaddrs_unresolved; /* wsdd_xaddr to be resolved */ zeroconf_endpoint *endpoints_unresolved; /* endpoints to be resolved */ mdns_resolver *mdns_resolver; /* MDNS resolver */ http_client *http_client; /* HTTP client */ ll_node list_node; /* In wsdd_finding_list */ eloop_timer *publish_timer; /* ZEROCONF_PUBLISH_DELAY timer */ bool is_printer; /* Device is printer */ bool is_scanner; /* Device is scanner */ bool published; /* This finding is published */ } wsdd_finding; /* wsdd_xaddr represents device transport address */ typedef struct { http_uri *uri; /* Device URI */ ll_node list_node; /* In wsdd_finding::xaddrs */ } wsdd_xaddr; /* WSDD_ACTION represents WSDD message action */ typedef enum { WSDD_ACTION_UNKNOWN, WSDD_ACTION_HELLO, WSDD_ACTION_BYE, WSDD_ACTION_PROBEMATCHES } WSDD_ACTION; /* wsdd_message represents a parsed WSDD message */ typedef struct { WSDD_ACTION action; /* Message action */ const char *address; /* Endpoint reference */ ll_head xaddrs; /* List of wsdd_xaddr */ bool is_scanner; /* Device is scanner */ bool is_printer; /* Device is printer */ } wsdd_message; /* Forward declarations */ static void wsdd_finding_add_xaddr (wsdd_finding *wsdd, wsdd_xaddr *xaddr); static void wsdd_finding_get_metadata (wsdd_finding *wsdd, int ifindex, wsdd_xaddr *xaddr); static void wsdd_message_free(wsdd_message *msg); static void wsdd_resolver_send_probe (wsdd_resolver *resolver); static wsdd_resolver* wsdd_netif_resolver_by_ifindex (int ifindex); /* Static variables */ static log_ctx *wsdd_log; static netif_notifier *wsdd_netif_notifier; static netif_addr *wsdd_netif_addr_list; static int wsdd_mcsock_ipv4 = -1; static int wsdd_mcsock_ipv6 = -1; static eloop_fdpoll *wsdd_fdpoll_ipv4; static eloop_fdpoll *wsdd_fdpoll_ipv6; static char wsdd_buf[65536]; static struct sockaddr_in wsdd_mcast_ipv4; static struct sockaddr_in6 wsdd_mcast_ipv6; static ll_head wsdd_finding_list; static int wsdd_initscan_count; static http_client *wsdd_http_client; static ip_addrset *wsdd_addrs_probing; /* WS-DD Probe template */ static const char *wsdd_probe_template = "" "" "" "http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe" "%s" "urn:schemas-xmlsoap-org:ws:2005:04:discovery" "" "" "" "wsdp:Device" "" "" ""; /* WS-DD Get (metadata) template */ static const char *wsdd_get_metadata_template = "" "" "" "http://schemas.xmlsoap.org/ws/2004/09/transfer/Get" "%s" "%s" "" "http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous" "" "" "" ""; /* XML namespace translation */ static const xml_ns wsdd_ns_rules[] = { {"s", "http*://schemas.xmlsoap.org/soap/envelope"}, /* SOAP 1.1 */ {"s", "http*://www.w3.org/2003/05/soap-envelope"}, /* SOAP 1.2 */ {"d", "http*://schemas.xmlsoap.org/ws/2005/04/discovery"}, {"a", "http*://schemas.xmlsoap.org/ws/2004/08/addressing"}, {"devprof", "http*://schemas.xmlsoap.org/ws/2006/02/devprof"}, {"mex", "http*://schemas.xmlsoap.org/ws/2004/09/mex"}, {"pnpx", "http*://schemas.microsoft.com/windows/pnpx/2005/10"}, {NULL, NULL} }; /******************** wsdd_xaddr operations ********************/ /* Create new wsdd_xaddr. Newly created wsdd_xaddr takes uri ownership */ static wsdd_xaddr* wsdd_xaddr_new (http_uri *uri) { wsdd_xaddr *xaddr = mem_new(wsdd_xaddr, 1); xaddr->uri = uri; return xaddr; } /* Destroy wsdd_xaddr */ static void wsdd_xaddr_free (wsdd_xaddr *xaddr) { http_uri_free(xaddr->uri); mem_free(xaddr); } /* Add wsdd_xaddr to the list. * Takes ownership on URI. */ static void wsdd_xaddr_list_add (ll_head *list, http_uri *uri) { wsdd_xaddr *xaddr; ll_node *node; /* Check for duplicates */ for (LL_FOR_EACH(node, list)) { xaddr = OUTER_STRUCT(node, wsdd_xaddr, list_node); if (http_uri_equal(xaddr->uri, uri)) { http_uri_free(uri); return; } } /* Add new xaddr */ xaddr = wsdd_xaddr_new(uri); ll_push_end(list, &xaddr->list_node); } /* Purge list of wsdd_xaddr */ static void wsdd_xaddr_list_purge (ll_head *list) { ll_node *node; while ((node = ll_pop_beg(list)) != NULL) { wsdd_xaddr *xaddr = OUTER_STRUCT(node, wsdd_xaddr, list_node); wsdd_xaddr_free(xaddr); } } /******************** wsdd_initscan_count operations ********************/ /* Increment wsdd_initscan_count */ static void wsdd_initscan_count_inc (void) { wsdd_initscan_count ++; } /* Decrement wsdd_initscan_count */ static void wsdd_initscan_count_dec (void) { log_assert(wsdd_log, wsdd_initscan_count > 0); wsdd_initscan_count --; if (wsdd_initscan_count == 0) { zeroconf_finding_done(ZEROCONF_WSD); } } /******************** wsdd_finding operations ********************/ /* Create new wsdd_finding */ static wsdd_finding* wsdd_finding_new (int ifindex, const char *address) { wsdd_finding *wsdd = mem_new(wsdd_finding, 1); wsdd->finding.method = ZEROCONF_WSD; wsdd->finding.uuid = uuid_parse(address); if (!uuid_valid(wsdd->finding.uuid)) { wsdd->finding.uuid = uuid_hash(address); } wsdd->finding.addrs = ip_addrset_new(); wsdd->finding.ifindex = ifindex; wsdd->address = str_dup(address); ll_init(&wsdd->xaddrs); ll_init(&wsdd->xaddrs_unresolved); wsdd->mdns_resolver = mdns_resolver_new(ifindex); wsdd->http_client = http_client_new(wsdd_log, wsdd); return wsdd; } /* Destroy wsdd_finding */ static void wsdd_finding_free (wsdd_finding *wsdd) { if (wsdd->published) { zeroconf_finding_withdraw(&wsdd->finding); } mdns_resolver_cancel(wsdd->mdns_resolver); mdns_resolver_free(wsdd->mdns_resolver); 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); mem_free((char*) wsdd->address); wsdd_xaddr_list_purge(&wsdd->xaddrs); wsdd_xaddr_list_purge(&wsdd->xaddrs_unresolved); zeroconf_endpoint_list_free(wsdd->endpoints_unresolved); ip_addrset_free(wsdd->finding.addrs); mem_free((char*) wsdd->finding.model); mem_free((char*) wsdd->finding.name); mem_free(wsdd); } /* Publish wsdd_finding */ static void wsdd_finding_publish (wsdd_finding *wsdd) { 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_finding_has_pending_queries checks if wsdd_finding * has pending MDNS or HTTP queries */ static bool wsdd_finding_has_pending_queries (wsdd_finding *wsdd) { return mdns_resolver_has_pending(wsdd->mdns_resolver) || http_client_has_pending(wsdd->http_client); } /* ZEROCONF_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(ZEROCONF_PUBLISH_DELAY, wsdd_finding_publish_delay_timer_callback, wsdd); } return; } wsdd_finding_publish(wsdd); } /* Get existent finding or add a new one */ static wsdd_finding* wsdd_finding_get (int ifindex, const char *address) { ll_node *node; wsdd_finding *wsdd; /* Lookup existent finding */ for (LL_FOR_EACH(node, &wsdd_finding_list)) { wsdd = OUTER_STRUCT(node, wsdd_finding, list_node); if (wsdd->finding.ifindex == ifindex && !strcmp(wsdd->address, address)) { return wsdd; } } /* Add new finding */ wsdd = wsdd_finding_new(ifindex, address); ll_push_end(&wsdd_finding_list, &wsdd->list_node); return wsdd; } /* Lookup wsdd_finding by IP address */ static wsdd_finding* wsdd_finding_by_address (ip_addr addr) { ll_node *node, *node2; wsdd_finding *wsdd; wsdd_xaddr *xaddr; const struct sockaddr *sockaddr; /* Roll over all findings */ for (LL_FOR_EACH(node, &wsdd_finding_list)) { wsdd = OUTER_STRUCT(node, wsdd_finding, list_node); /* Roll over all xaddrs */ for (LL_FOR_EACH(node2, &wsdd->xaddrs)) { xaddr = OUTER_STRUCT(node2, wsdd_xaddr, list_node); sockaddr = http_uri_addr(xaddr->uri); if (sockaddr != NULL) { ip_addr addr2 = ip_addr_from_sockaddr(sockaddr); if (ip_addr_equal(addr, addr2)) { return wsdd; } } } } 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; } } for (LL_FOR_EACH(node, &wsdd->xaddrs_unresolved)) { xaddr2 = OUTER_STRUCT(node, wsdd_xaddr, list_node); if (http_uri_equal(xaddr->uri, xaddr2->uri)) { return true; } } return false; } /* mdns_resolver callback for resolving non-literal * hostnames in xaddrs */ static void wsdd_finding_mdns_resolver_xaddr_callback (const mdns_query *query) { wsdd_finding *wsdd = mdns_query_get_ptr(query); const char *host = mdns_query_get_name(query); const ip_addrset *answer = mdns_query_get_answer(query); size_t count; const ip_addr *addrs = ip_addrset_addresses(answer, &count); ll_node *node; for (LL_FOR_EACH(node, &wsdd->xaddrs_unresolved)) { wsdd_xaddr *xaddr = OUTER_STRUCT(node, wsdd_xaddr, list_node); if (http_uri_host_is(xaddr->uri, host)) { size_t i; for (i = 0; i < count; i ++) { ip_addr addr = addrs[i]; http_uri *uri = http_uri_clone(xaddr->uri); wsdd_xaddr *xaddr_resolved; http_uri_set_host_addr(uri, addr); xaddr_resolved = wsdd_xaddr_new(uri); wsdd_finding_add_xaddr(wsdd, xaddr_resolved); } } } } /* mdns_resolver callback for resolving non-literal * hostnames in endpoints */ static void wsdd_finding_mdns_resolver_endpoint_callback (const mdns_query *query) { wsdd_finding *wsdd = mdns_query_get_ptr(query); const char *host = mdns_query_get_name(query); const ip_addrset *answer = mdns_query_get_answer(query); size_t count; const ip_addr *addrs = ip_addrset_addresses(answer, &count); zeroconf_endpoint *ep; for (ep = wsdd->endpoints_unresolved; ep != NULL; ep = ep->next) { if (http_uri_host_is(ep->uri, host)) { size_t i; for (i = 0; i < count; i ++) { ip_addr addr = addrs[i]; http_uri *uri = http_uri_clone(ep->uri); zeroconf_endpoint *ep_resolved; http_uri_set_host_addr(uri, addr); ip_addrset_add(wsdd->finding.addrs, addr); ep_resolved = zeroconf_endpoint_new(ID_PROTO_WSD, uri); ep_resolved->next = wsdd->finding.endpoints; wsdd->finding.endpoints = ep_resolved; } } } } /* Add newly discovered xaddr and optionally initiate metadata query * This function takes ownership over xaddr */ static void wsdd_finding_add_xaddr (wsdd_finding *wsdd, wsdd_xaddr *xaddr) { /* xaddr already known? */ if (wsdd_finding_has_xaddr(wsdd, xaddr)) { wsdd_xaddr_free(xaddr); return; } if (http_uri_is_literal(xaddr->uri)) { ll_push_end(&wsdd->xaddrs, &xaddr->list_node); if (wsdd->is_scanner) { wsdd_finding_get_metadata(wsdd, wsdd->finding.ifindex, xaddr); } } else { ll_push_end(&wsdd->xaddrs_unresolved, &xaddr->list_node); mdns_query_submit( wsdd->mdns_resolver, http_uri_get_host(xaddr->uri), wsdd_finding_mdns_resolver_xaddr_callback, wsdd ); } } /* Delete wsdd_finding from the wsdd_finding_list */ static void wsdd_finding_del (const char *address) { ll_node *node; /* Lookup finding in the list */ for (LL_FOR_EACH(node, &wsdd_finding_list)) { wsdd_finding *wsdd = OUTER_STRUCT(node, wsdd_finding, list_node); if (!strcmp(wsdd->address, address)) { ll_del(&wsdd->list_node); wsdd_finding_free(wsdd); return; } } } /* Delete all findings from the wsdd_finding_list */ static void wsdd_finding_list_purge (void) { ll_node *node; while ((node = ll_first(&wsdd_finding_list)) != NULL) { wsdd_finding *wsdd = OUTER_STRUCT(node, wsdd_finding, list_node); ll_del(&wsdd->list_node); wsdd_finding_free(wsdd); } } /* Parse endpoint addresses from the devprof:Hosted section of the * device metadata: * * * http://192.168.1.102:5358/WSDScanner * * scan:ScannerServiceType * uri:4509a320-00a0-008f-00b6-002507510eca/WSDScanner * http://schemas.microsoft.com/windows/2006/08/wdp/scan/ScannerServiceType * VEN_0103&DEV_069D * * * It ignores all endpoints except ScannerServiceType, extracts endpoint * URLs and prepends them to the wsdd->finding.endpoints */ static void wsdd_finding_parse_endpoints (wsdd_finding *wsdd, xml_rd *xml) { unsigned int level = xml_rd_depth(xml); size_t prefixlen = strlen(xml_rd_node_path(xml)); bool is_scanner = false; zeroconf_endpoint *endpoints = NULL; while (!xml_rd_end(xml)) { const char *path = xml_rd_node_path(xml) + prefixlen; const char *val; if (!strcmp(path, "/devprof:Types")) { val = xml_rd_node_value(xml); if (strstr(val, "ScannerServiceType") != NULL) { is_scanner = true; } } else if (!strcmp(path, "/a:EndpointReference/a:Address")) { http_uri *uri; zeroconf_endpoint *ep; val = xml_rd_node_value(xml); uri = http_uri_new(val, true); if (uri != NULL) { http_uri_fix_ipv6_zone(uri, wsdd->finding.ifindex); ep = zeroconf_endpoint_new(ID_PROTO_WSD, uri); ep->next = endpoints; endpoints = ep; } } xml_rd_deep_next(xml, level); } if (!is_scanner) { zeroconf_endpoint_list_free(endpoints); return; } while (endpoints != NULL) { zeroconf_endpoint *ep = endpoints; const struct sockaddr *addr = http_uri_addr(ep->uri); endpoints = endpoints->next; if (addr != NULL) { ip_addrset_add(wsdd->finding.addrs, ip_addr_from_sockaddr(addr)); ep->next = wsdd->finding.endpoints; wsdd->finding.endpoints = ep; } else if (!zeroconf_endpoint_list_contains(wsdd->endpoints_unresolved, ep)) { ep->next = wsdd->endpoints_unresolved; wsdd->endpoints_unresolved = ep; mdns_query_submit( wsdd->mdns_resolver, http_uri_get_host(ep->uri), wsdd_finding_mdns_resolver_endpoint_callback, wsdd ); } else { zeroconf_endpoint_free_single(ep); } } } /* Get metadata callback */ static void wsdd_finding_get_metadata_callback (void *ptr, http_query *q) { error err; xml_rd *xml = NULL; http_data *data; wsdd_finding *wsdd = ptr; char *model = NULL, *manufacturer = NULL; /* Check query status */ err = http_query_error(q); if (err != NULL) { log_trace(wsdd_log, "metadata query: %s", ESTRING(err)); goto DONE; } /* Parse XML */ data = http_query_get_response_data(q); if (data->size == 0) { log_trace(wsdd_log, "metadata query: no data"); goto DONE; } err = xml_rd_begin(&xml, data->bytes, data->size, wsdd_ns_rules); if (err != NULL) { log_trace(wsdd_log, "metadata query: %s", ESTRING(err)); goto DONE; } /* Decode XML */ while (!xml_rd_end(xml)) { const char *path = xml_rd_node_path(xml); if (!strcmp(path, "s:Envelope/s:Body/mex:Metadata/mex:MetadataSection" "/devprof:Relationship/devprof:Hosted")) { wsdd_finding_parse_endpoints(wsdd, xml); } else if (!strcmp(path, "s:Envelope/s:Body/mex:Metadata/mex:MetadataSection" "/devprof:ThisModel/devprof:Manufacturer")) { if (manufacturer == NULL) { manufacturer = str_dup(xml_rd_node_value(xml)); } } else if (!strcmp(path, "s:Envelope/s:Body/mex:Metadata/mex:MetadataSection" "/devprof:ThisModel/devprof:ModelName")) { if (model == NULL) { model = str_dup(xml_rd_node_value(xml)); } } xml_rd_deep_next(xml, 0); } if (wsdd->finding.model == NULL) { if (model != NULL && manufacturer != NULL && str_has_prefix(model, manufacturer)) { mem_free(manufacturer); manufacturer = NULL; } if (model != NULL && manufacturer != NULL) { wsdd->finding.model = str_printf("%s %s", manufacturer, model); } else if (model != NULL) { wsdd->finding.model = model; model = NULL; } else if (manufacturer != NULL) { wsdd->finding.model = manufacturer; manufacturer = NULL; } else { wsdd->finding.model = str_dup(wsdd->address); } } /* Cancel all unnecessary metadata requests * * Depending on the device, metadata request may either return all * endpoints, or metadata request sent to IPv4 address may return * only IPv4 endpoints and visa versa * * But if we have both IPv4/IPv6 endpoints, seems waiting for completion * of more metadata requests is not unnecessary. So lets cancel them, * if any. */ if (ip_addrset_has_af(wsdd->finding.addrs, AF_INET) && ip_addrset_has_af(wsdd->finding.addrs, AF_INET6)) { http_client_cancel(wsdd->http_client); } /* Cleanup and exit */ DONE: xml_rd_finish(&xml); mem_free(model); mem_free(manufacturer); if (!wsdd_finding_has_pending_queries(wsdd)) { wsdd_finding_publish_delay(wsdd); } } /* Query device metadata */ static void wsdd_finding_get_metadata (wsdd_finding *wsdd, int ifindex, wsdd_xaddr *xaddr) { uuid u = uuid_rand(); http_query *q; log_trace(wsdd_log, "querying metadata from %s", http_uri_str(xaddr->uri)); sprintf(wsdd_buf, wsdd_get_metadata_template, u.text, wsdd->address); q = http_query_new(wsdd->http_client, http_uri_clone(xaddr->uri), "POST", str_dup(wsdd_buf), "application/soap+xml; charset=utf-8"); http_query_set_uintptr(q, ifindex); http_query_submit(q, wsdd_finding_get_metadata_callback); } /******************** wsdd_message operations ********************/ /* Parse transport addresses. Universal function * for Hello/Bye/ProbeMatch message */ static void wsdd_message_parse_endpoint (wsdd_message *msg, xml_rd *xml) { unsigned int level = xml_rd_depth(xml); char *xaddrs_text = NULL; size_t prefixlen = strlen(xml_rd_node_path(xml)); while (!xml_rd_end(xml)) { const char *path = xml_rd_node_path(xml) + prefixlen; const char *val; if (!strcmp(path, "/d:Types")) { val = xml_rd_node_value(xml); if (strstr(val, "ScanDeviceType")) { msg->is_scanner = true; } if (strstr(val, "PrintDeviceType") != NULL) { msg->is_printer = true; } } else if (!strcmp(path, "/d:XAddrs")) { mem_free(xaddrs_text); xaddrs_text = str_dup(xml_rd_node_value(xml)); } else if (!strcmp(path, "/a:EndpointReference/a:Address")) { mem_free((char*) msg->address); msg->address = str_dup(xml_rd_node_value(xml)); } xml_rd_deep_next(xml, level); } if (xaddrs_text != NULL) { char *tok, *saveptr; static const char *delim = "\t\n\v\f\r \x85\xA0"; for (tok = strtok_r(xaddrs_text, delim, &saveptr); tok != NULL; tok = strtok_r(NULL, delim, &saveptr)) { http_uri *uri = http_uri_new(tok, true); if (uri != NULL) { wsdd_xaddr_list_add(&msg->xaddrs, uri); } } } mem_free(xaddrs_text); } /* Parse WSDD message */ static wsdd_message* wsdd_message_parse (const char *xml_text, size_t xml_len) { wsdd_message *msg = mem_new(wsdd_message, 1); xml_rd *xml; error err; ll_init(&msg->xaddrs); err = xml_rd_begin(&xml, xml_text, xml_len, wsdd_ns_rules); if (err != NULL) { goto DONE; } while (!xml_rd_end(xml)) { const char *path = xml_rd_node_path(xml); const char *val; if (!strcmp(path, "s:Envelope/s:Header/a:Action")) { val = xml_rd_node_value(xml); if (strstr(val, "Hello")) { msg->action = WSDD_ACTION_HELLO; } else if (strstr(val, "Bye")) { msg->action = WSDD_ACTION_BYE; } else if (strstr(val, "ProbeMatches")) { msg->action = WSDD_ACTION_PROBEMATCHES; } } else if (!strcmp(path, "s:Envelope/s:Body/d:Hello") || !strcmp(path, "s:Envelope/s:Body/d:Bye") || !strcmp(path, "s:Envelope/s:Body/d:ProbeMatches/d:ProbeMatch")) { wsdd_message_parse_endpoint(msg, xml); } xml_rd_deep_next(xml, 0); } DONE: xml_rd_finish(&xml); if (err != NULL || msg->action == WSDD_ACTION_UNKNOWN || msg->address == NULL || (msg->action == WSDD_ACTION_HELLO && ll_empty(&msg->xaddrs)) || (msg->action == WSDD_ACTION_PROBEMATCHES && ll_empty(&msg->xaddrs))) { wsdd_message_free(msg); msg = NULL; } return msg; } /* Free wsdd_message */ static void wsdd_message_free (wsdd_message *msg) { if (msg != NULL) { mem_free((char*) msg->address); wsdd_xaddr_list_purge(&msg->xaddrs); mem_free(msg); } } /* Get message action name, for debugging */ static const char* wsdd_message_action_name (const wsdd_message *msg) { switch (msg->action) { case WSDD_ACTION_UNKNOWN: break; case WSDD_ACTION_HELLO: return "Hello"; case WSDD_ACTION_BYE: return "Bye"; case WSDD_ACTION_PROBEMATCHES: return "ProbeMatches"; } return "UNKNOWN"; } /******************** Advanced socket options ********************/ /* Setup IP_PKTINFO/IP_RECVIF reception for IPv6 sockets */ static int wsdd_sock_enable_pktinfo_ip6 (int fd) { static int yes = 1; int rc; rc = setsockopt(fd, IPPROTO_IPV6, IPV6_RECVPKTINFO, &yes, sizeof(yes)); if (rc < 0) { log_debug(wsdd_log, "setsockopt(AF_INET6, IPV6_RECVPKTINFO): %s", strerror(errno)); } return rc; } /* Setup IP_PKTINFO/IP_RECVIF reception for IPv4 sockets */ static int wsdd_sock_enable_pktinfo_ip4 (int fd) { static int yes = 1; int rc; #ifdef IP_PKTINFO /* Linux version */ rc = setsockopt(fd, IPPROTO_IP, IP_PKTINFO, &yes, sizeof(yes)); #elif defined(IP_RECVIF) /* OpenBSD */ rc = setsockopt(fd, IPPROTO_IP, IP_RECVIF, &yes, sizeof(yes)); #else # error FIX ME #endif if (rc < 0) { log_debug(wsdd_log, "setsockopt(AF_INET,IP_PKTINFO/IP_RECVIF): %s", strerror(errno)); } return rc; } /******************** wsdd_resolver operations ********************/ /* Dispatch received WSDD message */ static void wsdd_resolver_message_dispatch (wsdd_resolver *resolver, wsdd_message *msg, const char *from) { wsdd_finding *wsdd; wsdd_xaddr *xaddr; ll_node *node; /* Write trace messages */ log_trace(wsdd_log, "%s message received from %s:", wsdd_message_action_name(msg), from); log_trace(wsdd_log, " address: %s", msg->address); log_trace(wsdd_log, " is_scanner: %s", msg->is_scanner ? "yes" : "no"); log_trace(wsdd_log, " is_printer: %s", msg->is_printer ? "yes" : "no"); for (LL_FOR_EACH(node, &msg->xaddrs)) { xaddr = OUTER_STRUCT(node, wsdd_xaddr, list_node); log_trace(wsdd_log, " xaddr: %s", http_uri_str(xaddr->uri)); } /* Handle some special cases */ if (msg->action != WSDD_ACTION_BYE) { /* Ignore devices that are neither scanner not printer */ if (!(msg->is_scanner || msg->is_printer)) { log_trace(wsdd_log, "skipped: device is neither scanner not printer"); goto DONE; } /* Ignore messages with no xaddrs */ if (ll_empty(&msg->xaddrs)) { log_trace(wsdd_log, "skipped: no xaddrs in the message"); goto DONE; } } /* Fixup ipv6 zones */ for (LL_FOR_EACH(node, &msg->xaddrs)) { xaddr = OUTER_STRUCT(node, wsdd_xaddr, list_node); http_uri_fix_ipv6_zone(xaddr->uri, resolver->ifindex); } /* Handle the message */ switch (msg->action) { case WSDD_ACTION_HELLO: case WSDD_ACTION_PROBEMATCHES: /* Add a finding or get existent one */ wsdd = wsdd_finding_get(resolver->ifindex, msg->address); /* Update is_printer/is_scanner flags */ wsdd->is_printer = wsdd->is_printer || msg->is_printer; wsdd->is_scanner = wsdd->is_scanner || msg->is_scanner; /* Import newly discovered xaddrs and initiate metadata * query */ while ((node = ll_pop_beg(&msg->xaddrs)) != NULL) { xaddr = OUTER_STRUCT(node, wsdd_xaddr, list_node); wsdd_finding_add_xaddr(wsdd, 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 (!wsdd_finding_has_pending_queries(wsdd)) { if (wsdd->is_scanner) { wsdd_finding_publish_delay(wsdd); } else { wsdd_finding_publish(wsdd); } } break; case WSDD_ACTION_BYE: wsdd_finding_del(msg->address); break; default: break; } /* Cleanup and exit */ DONE: wsdd_message_free(msg); log_trace(wsdd_log, ""); } /* Resolver read callback */ static void wsdd_resolver_read_callback (int fd, void *data, ELOOP_FDPOLL_MASK mask) { struct sockaddr_storage from, to; socklen_t tolen = sizeof(to); ip_straddr str_from, str_to; int rc; wsdd_message *msg; struct iovec vec = {wsdd_buf, sizeof(wsdd_buf)}; uint8_t aux[8192]; struct cmsghdr *cmsg; int ifindex = 0; wsdd_resolver *resolver; struct msghdr msghdr = { .msg_name = &from, .msg_namelen = sizeof(from), .msg_iov = &vec, .msg_iovlen = 1, .msg_control = aux, .msg_controllen = sizeof(aux) }; (void) mask; (void) data; /* Receive a packet */ rc = recvmsg(fd, &msghdr, 0); if (rc <= 0) { return; } /* Fetch interface index from auxiliary data */ for (cmsg = CMSG_FIRSTHDR(&msghdr); cmsg != NULL; cmsg = CMSG_NXTHDR(&msghdr, cmsg)) { if (cmsg->cmsg_level == IPPROTO_IPV6 && cmsg->cmsg_type == IPV6_PKTINFO) { struct in6_pktinfo *pkt = (struct in6_pktinfo*) CMSG_DATA(cmsg); ifindex = pkt->ipi6_ifindex; } #ifdef IP_PKTINFO /* Linux version */ else if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO) { struct in_pktinfo *pkt = (struct in_pktinfo*) CMSG_DATA(cmsg); ifindex = pkt->ipi_ifindex; } #elif defined(IP_RECVIF) /* OpenBSD */ else if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_RECVIF) { struct sockaddr_dl *pkt = (struct sockaddr_dl *) CMSG_DATA(cmsg); ifindex = pkt->sdl_index; } #else # error FIX ME #endif } str_from = ip_straddr_from_sockaddr((struct sockaddr*) &from, true); (void) getsockname(fd, (struct sockaddr*) &to, &tolen); str_to = ip_straddr_from_sockaddr((struct sockaddr*) &to, true); log_trace(wsdd_log, "%d bytes received: %s->%s", rc, str_from.text, str_to.text); log_trace_data(wsdd_log, "application/xml", wsdd_buf, rc); /* Lookup resolver by interface index */ resolver = wsdd_netif_resolver_by_ifindex(ifindex); if (resolver == NULL) { return; } /* Parse and dispatch the message */ msg = wsdd_message_parse(wsdd_buf, rc); if (msg != NULL) { wsdd_resolver_message_dispatch(resolver, msg, "UDP"); } } /* Count discovered devices with model names matching the specified parent. * * Pattern is the glob-style expression, applied to the model name * of discovered devices. */ static unsigned int wsdd_resolver_count_devices_by_model (wsdd_resolver *resolver, const char *pattern) { unsigned int answer = 0; ll_node *node; wsdd_finding *wsdd; /* Lookup existent finding */ for (LL_FOR_EACH(node, &wsdd_finding_list)) { wsdd = OUTER_STRUCT(node, wsdd_finding, list_node); if (wsdd->finding.ifindex == resolver->ifindex && wsdd->finding.model != NULL && wsdd->published && fnmatch(pattern, wsdd->finding.model, 0) == 0) { answer ++; } } return answer; } /* Retransmit timer callback */ static void wsdd_resolver_timer_callback (void *data) { wsdd_resolver *resolver = data; const char pattern[] = "Pantum*"; resolver->timer = NULL; /* Resolver is about to use all its time limit. * Should we use extended discovery time? */ if (resolver->time_elapsed >= resolver->time_limit && resolver->time_limit < WSDD_DISCOVERY_TIME_EX) { unsigned int cnt_mdns, cnt_wsdd; cnt_mdns = mdns_device_count_by_model(resolver->ifindex, pattern); cnt_wsdd = wsdd_resolver_count_devices_by_model(resolver, pattern); if (cnt_wsdd < cnt_mdns) { const char *proto = resolver->ipv6 ? "ipv6" : "ipv4"; log_debug(wsdd_log, "%s@%s: \"%s\": MDNS/WSDD count: %d/%d", proto, resolver->ifname.text, pattern, cnt_mdns, cnt_wsdd); log_debug(wsdd_log, "%s@%s: extending discovery time (%d->%d ms)", proto, resolver->ifname.text, resolver->time_limit, WSDD_DISCOVERY_TIME_EX); resolver->time_limit = WSDD_DISCOVERY_TIME_EX; } } /* Time limit not reached yet? */ if (resolver->time_elapsed < resolver->time_limit) { wsdd_resolver_send_probe(resolver); return; } /* Resolving is done, cleanup after that */ eloop_fdpoll_free(resolver->fdpoll); close(resolver->fd); resolver->fdpoll = NULL; resolver->fd = -1; log_debug(wsdd_log, "%s: done discovery", resolver->str_ifaddr.text); if (resolver->initscan) { resolver->initscan = false; wsdd_initscan_count_dec(); } } /* Set retransmit timer */ static void wsdd_resolver_timer_set (wsdd_resolver *resolver) { uint32_t t; log_assert(wsdd_log, resolver->timer == NULL); if (resolver->time_elapsed >= resolver->time_limit) { /* Should not happen, but just in case */ t = WSDD_RETRANSMIT_MIN; } else { /* Choose random time between WSDD_RETRANSMIT_MIN * and WSDD_RETRANSMIT_MAX, but don't exceed * available limit, if possible (i.e., if it * is not less that WSDD_RETRANSMIT_MIN) */ t = resolver->time_limit - resolver->time_elapsed; if (t < WSDD_RETRANSMIT_MIN) { t = WSDD_RETRANSMIT_MIN; } else { if (t > WSDD_RETRANSMIT_MAX) { t = WSDD_RETRANSMIT_MAX; } t = math_rand_range(WSDD_RETRANSMIT_MIN, t); } } resolver->time_elapsed += t; resolver->timer = eloop_timer_new(t, wsdd_resolver_timer_callback, resolver); } /* Send probe */ static void wsdd_resolver_send_probe (wsdd_resolver *resolver) { uuid u = uuid_rand(); int n = sprintf(wsdd_buf, wsdd_probe_template, u.text); int rc; struct sockaddr *addr; socklen_t addrlen; ip_straddr straddr; if (resolver->ipv6) { addr = (struct sockaddr*) &wsdd_mcast_ipv6; addrlen = sizeof(wsdd_mcast_ipv6); } else { addr = (struct sockaddr*) &wsdd_mcast_ipv4; addrlen = sizeof(wsdd_mcast_ipv4); } straddr = ip_straddr_from_sockaddr(addr, true); log_trace(wsdd_log, "probe sent: %s->%s", resolver->str_sockaddr.text, straddr.text); log_trace_data(wsdd_log, "application/xml", wsdd_buf, n); rc = sendto(resolver->fd, wsdd_buf, n, 0, addr, addrlen); if (rc < 0) { log_debug(wsdd_log, "send_probe: %s", strerror(errno)); } wsdd_resolver_timer_set(resolver); } /* Create wsdd_resolver */ static wsdd_resolver* wsdd_resolver_new (const netif_addr *addr, bool initscan) { wsdd_resolver *resolver = mem_new(wsdd_resolver, 1); int af = addr->ipv6 ? AF_INET6 : AF_INET; const char *af_name = addr->ipv6 ? "AF_INET6" : "AF_INET"; int rc; uint16_t port; /* Build resolver structure */ resolver->ifindex = addr->ifindex; resolver->ifname = addr->ifname; resolver->time_limit = WSDD_DISCOVERY_TIME; /* Open a socket */ resolver->ipv6 = addr->ipv6; resolver->fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); if (resolver->fd < 0) { log_debug(wsdd_log, "socket(%s): %s", af_name, strerror(errno)); goto FAIL; } /* Set socket options */ if (addr->ipv6) { rc = setsockopt(resolver->fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &addr->ifindex, sizeof(addr->ifindex)); if (rc < 0) { log_debug(wsdd_log, "setsockopt(AF_INET6,IPV6_MULTICAST_IF): %s", strerror(errno)); goto FAIL; } rc = wsdd_sock_enable_pktinfo_ip6(resolver->fd); if (rc < 0) { goto FAIL; } } else { rc = setsockopt(resolver->fd, IPPROTO_IP, IP_MULTICAST_IF, &addr->ip.v4, sizeof(addr->ip.v4)); if (rc < 0) { log_debug(wsdd_log, "setsockopt(AF_INET,IP_MULTICAST_IF): %s", strerror(errno)); goto FAIL; } rc = wsdd_sock_enable_pktinfo_ip4(resolver->fd); if (rc < 0) { goto FAIL; } } /* Bind the socket */ if (addr->ipv6) { struct sockaddr_in6 a; socklen_t alen = sizeof(a); memset(&a, 0, sizeof(a)); a.sin6_family = AF_INET6; a.sin6_addr = addr->ip.v6; a.sin6_scope_id = addr->ifindex; resolver->str_ifaddr = ip_straddr_from_ip(AF_INET6, &addr->ip); rc = bind(resolver->fd, (struct sockaddr*) &a, sizeof(a)); (void) getsockname(resolver->fd, (struct sockaddr*) &a, &alen); port = a.sin6_port; resolver->str_sockaddr = ip_straddr_from_sockaddr( (struct sockaddr*) &a, true); } else { struct sockaddr_in a; socklen_t alen = sizeof(a); memset(&a, 0, sizeof(a)); a.sin_family = AF_INET; a.sin_addr = addr->ip.v4; resolver->str_ifaddr = ip_straddr_from_ip(AF_INET, &addr->ip); resolver->str_sockaddr = ip_straddr_from_sockaddr( (struct sockaddr*) &a, true); rc = bind(resolver->fd, (struct sockaddr*) &a, sizeof(a)); (void) getsockname(resolver->fd, (struct sockaddr*) &a, &alen); port = a.sin_port; resolver->str_sockaddr = ip_straddr_from_sockaddr( (struct sockaddr*) &a, true); } log_debug(wsdd_log, "%s: started discovery, UDP port=%d", resolver->str_ifaddr.text, ntohs(port)); if (rc < 0) { log_debug(wsdd_log, "bind(%s): %s", resolver->str_sockaddr.text, strerror(errno)); goto FAIL; } /* Setup fdpoll */ resolver->fdpoll = eloop_fdpoll_new(resolver->fd, wsdd_resolver_read_callback, NULL); eloop_fdpoll_set_mask(resolver->fdpoll, ELOOP_FDPOLL_READ); wsdd_resolver_send_probe(resolver); /* Update wsdd_initscan_count */ resolver->initscan = initscan; if (resolver->initscan) { wsdd_initscan_count_inc(); } return resolver; /* Error: cleanup and exit */ FAIL: if (resolver->fd >= 0) { close(resolver->fd); resolver->fd = -1; } return resolver; } /* Destroy wsdd_resolver */ static void wsdd_resolver_free (wsdd_resolver *resolver) { if (resolver->initscan) { wsdd_initscan_count_dec(); } if (resolver->fdpoll != NULL) { eloop_fdpoll_free(resolver->fdpoll); close(resolver->fd); } if (resolver->timer != NULL) { eloop_timer_cancel(resolver->timer); } mem_free(resolver); } /******************** Miscellaneous events ********************/ /* Called by zeroconf to notify wsdd about initial scan timer expiration */ void wsdd_initscan_timer_expired (void) { ll_node *node; wsdd_finding *wsdd; /* Publish all incomplete but useful findings, if any * * Without it, if metadata query takes too long time (for example, * device has 2 IP addresses, one is unreachable from PC) it * effectively blocks the device from being discovered */ for (LL_FOR_EACH(node, &wsdd_finding_list)) { wsdd = OUTER_STRUCT(node, wsdd_finding, list_node); if (!wsdd->published && wsdd->finding.endpoints != NULL) { http_client_cancel(wsdd->http_client); wsdd_finding_publish(wsdd); } } } /******************** WS-Discovery directed probes ********************/ /* WS-Discovery send directed probe callback */ static void wsdd_send_directed_probe_callback (void *ptr, http_query *q) { error err; const struct sockaddr *sockaddr = http_uri_addr(http_query_uri(q)); int ifindex; http_data *data; wsdd_resolver *resolver; wsdd_message *msg; (void) ptr; /* Drop query address from list of pending probes */ if (sockaddr != NULL) { ip_addrset_del(wsdd_addrs_probing, ip_addr_from_sockaddr(sockaddr)); } err = http_query_error(q); if (err != NULL) { log_debug(wsdd_log, "directed probe: HTTP %s", ESTRING(err)); return; } /* Find appropriate resolver */ ifindex = (int) http_query_get_uintptr(q); resolver = wsdd_netif_resolver_by_ifindex(ifindex); if (resolver == NULL) { log_debug(wsdd_log, "directed probe: resolver not found for interface %d", ifindex); return; } /* Parse and dispatch the message */ data = http_query_get_response_data(q); msg = wsdd_message_parse(data->bytes, data->size); if (msg != NULL) { wsdd_resolver_message_dispatch(resolver, msg, "HTTP"); } } /* Send WD-Discovery directed probe * * WS-Discovery defines two mechanisms for sending Probes: * * probes can be send using UDP milticasts * * probes can be send directly via HTTP POST to the following URL: * http://addr//StableWSDiscoveryEndpoint/schemas-xmlsoap-org_ws_2005_04_discovery * * The second mechanism is called "Directed discovery Probe message", and * information about it is exceptionally hard to discover. * * BTW, this is why this protocol is called Web Services Discovery: you * need to browse the entire web to discover a bit of useful information * * This function is called from DNS-SD module when new device is found, * and sends directed probe to its HTTP stable discovery endpoint * * To avoid device overload with discovery requests, this function * sends only one request a time per address and doesn't send * requests to already known devices. */ void wsdd_send_directed_probe (int ifindex, int af, const void *addr) { char ifname[IF_NAMESIZE] = "?"; ip_straddr straddr = ip_straddr_from_ip(af, addr); char uri_buf[1024]; http_uri *uri; uuid u; http_query *q; ip_addr ipa = ip_addr_make(ifindex, af, addr); /* Do nothing, if discovery is disabled */ if (!conf.discovery || conf.wsdd_mode == WSDD_OFF) { return; } /* Write log messages */ if_indextoname(ifindex, ifname); log_debug(wsdd_log, "directed probe: trying if=%s, addr=%s", ifname, straddr.text); /* Skip loopback address, we will not find anything interesting there */ if (ip_is_loopback(af, addr)) { log_debug(wsdd_log, "directed probe: skipping loopback address"); return; } /* Already probing? */ if (ip_addrset_lookup(wsdd_addrs_probing, ipa)) { log_debug(wsdd_log, "directed probe: already in progress, skipping"); return; } /* Already contacted? */ if (wsdd_finding_by_address(ipa) != NULL) { log_debug(wsdd_log, "directed probe: device already contacted, skipping"); return; } ip_addrset_add_unsafe(wsdd_addrs_probing, ipa); /* Build request URI */ if (af == AF_INET) { sprintf(uri_buf, "http://%s", straddr.text); } else if (!ip_is_linklocal(af, addr)) { sprintf(uri_buf, "http://[%s]", straddr.text); } else { /* Percent character in the IPv6 address literal * needs to be properly escaped, so it becomes %25 * See RFC6874 for details */ sprintf(uri_buf, "http://[%s%%25%d]", straddr.text, ifindex); } strcat(uri_buf, WSDD_STABLE_ENDPOINT); uri = http_uri_new(uri_buf, true); log_assert(wsdd_log, uri != NULL); /* Build probe request */ u = uuid_rand(); sprintf(wsdd_buf, wsdd_probe_template, u.text); /* Send probe request */ q = http_query_new(wsdd_http_client, uri, "POST", str_dup(wsdd_buf), "application/soap+xml; charset=utf-8"); http_query_set_uintptr(q, ifindex); http_query_submit(q, wsdd_send_directed_probe_callback); } /******************** Management of multicast sockets ********************/ /* Open IPv4 or IPv6 multicast socket */ static int wsdd_mcsock_open (bool ipv6) { int af = ipv6 ? AF_INET6 : AF_INET; int fd, rc; const char *af_name = ipv6 ? "AF_INET6" : "AF_INET"; static int yes = 1; ip_straddr straddr; /* Open a socket */ fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); if (fd < 0) { log_debug(wsdd_log, "socket(%s): %s", af_name, strerror(errno)); return fd; } /* Set socket options */ rc = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); if (rc < 0) { log_debug(wsdd_log, "setsockopt(%s, SO_REUSEADDR): %s", af_name, strerror(errno)); goto FAIL; } if (ipv6) { rc = setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &yes, sizeof(yes)); if (rc < 0) { log_debug(wsdd_log, "setsockopt(%s, IPV6_V6ONLY): %s", af_name, strerror(errno)); goto FAIL; } rc = wsdd_sock_enable_pktinfo_ip6(fd); if (rc < 0) { goto FAIL; } } else { rc = wsdd_sock_enable_pktinfo_ip4(fd); if (rc < 0) { goto FAIL; } } /* Bind socket to WSDD multicast port; group membership * will be added later on per-interface-address basis */ if (ipv6) { struct sockaddr_in6 addr; memset(&addr, 0, sizeof(addr)); addr.sin6_family = AF_INET6; addr.sin6_port = wsdd_mcast_ipv6.sin6_port; straddr = ip_straddr_from_sockaddr((struct sockaddr*) &addr, true); rc = bind(fd, (struct sockaddr*) &addr, sizeof(addr)); } else { struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = wsdd_mcast_ipv4.sin_port; straddr = ip_straddr_from_sockaddr((struct sockaddr*) &addr, true); rc = bind(fd, (struct sockaddr*) &addr, sizeof(addr)); } if (rc < 0) { log_debug(wsdd_log, "bind(%s): %s", straddr.text, strerror(errno)); goto FAIL; } return fd; /* Error: cleanup and exit */ FAIL: rc = errno; close(fd); errno = rc; return -1; } /* Add or drop multicast group membership, on * per-interface-address basis */ static void wsdd_mcast_update_membership (int fd, netif_addr *addr, bool add) { int rc, opt; if (addr->ipv6) { struct ipv6_mreq mreq6; memset(&mreq6, 0, sizeof(mreq6)); mreq6.ipv6mr_multiaddr = wsdd_mcast_ipv6.sin6_addr; mreq6.ipv6mr_interface = addr->ifindex; opt = add ? IPV6_JOIN_GROUP : IPV6_LEAVE_GROUP; rc = setsockopt(fd, IPPROTO_IPV6, opt, &mreq6, sizeof(mreq6)); if (rc < 0) { log_debug(wsdd_log, "setsockopt(AF_INET6,%s): %s", add ? "IPV6_ADD_MEMBERSHIP" : "IPV6_DROP_MEMBERSHIP", strerror(errno)); } } else { #ifdef OS_HAVE_IP_MREQN struct ip_mreqn mreq4; #else struct ip_mreq mreq4; #endif memset(&mreq4, 0, sizeof(mreq4)); mreq4.imr_multiaddr = wsdd_mcast_ipv4.sin_addr; #ifdef OS_HAVE_IP_MREQN mreq4.imr_address = addr->ip.v4; mreq4.imr_ifindex = addr->ifindex; #else mreq4.imr_interface = addr->ip.v4; #endif opt = add ? IP_ADD_MEMBERSHIP : IP_DROP_MEMBERSHIP; rc = setsockopt(fd, IPPROTO_IP, opt, &mreq4, sizeof(mreq4)); if (rc < 0) { log_debug(wsdd_log, "setsockopt(AF_INET,%s): %s", add ? "IP_ADD_MEMBERSHIP" : "IP_DROP_MEMBERSHIP", strerror(errno)); } } } /******************** Monitoring of network interfaces ********************/ /* Dump list of network interfaces addresses */ static void wsdd_netif_dump_addresses (const char *prefix, netif_addr *list) { while (list != NULL) { log_debug(wsdd_log, "%s%s@%s, ifindex=%d", prefix, list->straddr, list->ifname.text, list->ifindex); list = list->next; } } /* Lookup wsdd_resolver by interface index */ static wsdd_resolver* wsdd_netif_resolver_by_ifindex (int ifindex) { netif_addr *addr; for (addr = wsdd_netif_addr_list; addr != NULL; addr = addr->next) { if (addr->ifindex == ifindex) { return addr->data; } } return NULL; } /* Update network interfaces addresses */ static void wsdd_netif_update_addresses (bool initscan) { netif_addr *addr_list = netif_addr_list_get(); netif_addr *addr; netif_diff diff = netif_diff_compute(wsdd_netif_addr_list, addr_list); log_debug(wsdd_log, "netif addresses update:"); wsdd_netif_dump_addresses(" + ", diff.added); wsdd_netif_dump_addresses(" - ", diff.removed); /* Update multicast group membership, and start/stop * per-interface-address resolvers */ for (addr = diff.removed; addr != NULL; addr = addr->next) { int fd = addr->ipv6 ? wsdd_mcsock_ipv6 : wsdd_mcsock_ipv4; wsdd_mcast_update_membership(fd, addr, false); wsdd_resolver_free(addr->data); } for (addr = diff.added; addr != NULL; addr = addr->next) { int fd = addr->ipv6 ? wsdd_mcsock_ipv6 : wsdd_mcsock_ipv4; wsdd_mcast_update_membership(fd, addr, true); addr->data = wsdd_resolver_new(addr, initscan); } /* Update wsdd_netif_addr_list */ wsdd_netif_addr_list = netif_addr_list_merge(diff.preserved, diff.added); netif_addr_list_free(diff.removed); } /* Network interfaces address change notification */ static void wsdd_netif_notifier_callback (void *data) { (void) data; log_debug(wsdd_log, "netif event"); wsdd_netif_update_addresses(false); } /******************** Initialization and cleanup ********************/ /* eloop start/stop callback */ static void wsdd_start_stop_callback (bool start) { if (start) { /* Setup WS-Discovery stable endpoint handling */ wsdd_addrs_probing = ip_addrset_new(); wsdd_http_client = http_client_new(wsdd_log, NULL); /* Setup WSDD multicast reception */ if (wsdd_mcsock_ipv4 >= 0) { wsdd_fdpoll_ipv4 = eloop_fdpoll_new(wsdd_mcsock_ipv4, wsdd_resolver_read_callback, NULL); eloop_fdpoll_set_mask(wsdd_fdpoll_ipv4, ELOOP_FDPOLL_READ); } if (wsdd_mcsock_ipv6 >= 0) { wsdd_fdpoll_ipv6 = eloop_fdpoll_new(wsdd_mcsock_ipv6, wsdd_resolver_read_callback, NULL); eloop_fdpoll_set_mask(wsdd_fdpoll_ipv6, ELOOP_FDPOLL_READ); } /* Update netif addresses. Initscan counter is incremented and * * decremented to ensure that initial scan completion notification * will be raised even if there are no network interfaces. */ wsdd_initscan_count_inc(); wsdd_netif_update_addresses(true); wsdd_initscan_count_dec(); } else { /* Cleanup WS-Discovery stable endpoint handling */ ip_addrset_free(wsdd_addrs_probing); http_client_cancel(wsdd_http_client); http_client_free(wsdd_http_client); wsdd_addrs_probing = NULL; wsdd_http_client = NULL; /* Stop multicast reception */ if (wsdd_fdpoll_ipv4 != NULL) { eloop_fdpoll_free(wsdd_fdpoll_ipv4); wsdd_fdpoll_ipv4 = NULL; } if (wsdd_fdpoll_ipv6 != NULL) { eloop_fdpoll_free(wsdd_fdpoll_ipv6); wsdd_fdpoll_ipv6 = NULL; } /* Cleanup resources */ wsdd_finding_list_purge(); } } /* Initialize WS-Discovery */ SANE_Status wsdd_init (void) { /* Initialize logging */ wsdd_log = log_ctx_new("WSDD", zeroconf_log); /* Initialize wsdd_finding_list */ ll_init(&wsdd_finding_list); /* All for now, if WS-Discovery is disabled */ if (!conf.discovery || conf.wsdd_mode == WSDD_OFF) { log_debug(wsdd_log, "devices discovery disabled"); zeroconf_finding_done(ZEROCONF_WSD); return SANE_STATUS_GOOD; } /* Create IPv4/IPv6 multicast addresses */ wsdd_mcast_ipv4.sin_family = AF_INET; inet_pton(AF_INET, "239.255.255.250", &wsdd_mcast_ipv4.sin_addr); wsdd_mcast_ipv4.sin_port = htons(3702); wsdd_mcast_ipv6.sin6_family = AF_INET6; inet_pton(AF_INET6, "ff02::c", &wsdd_mcast_ipv6.sin6_addr); wsdd_mcast_ipv6.sin6_port = htons(3702); /* Open multicast sockets */ wsdd_mcsock_ipv4 = wsdd_mcsock_open(false); if (wsdd_mcsock_ipv4 < 0) { goto FAIL; } wsdd_mcsock_ipv6 = wsdd_mcsock_open(true); if (wsdd_mcsock_ipv6 < 0 && errno != EAFNOSUPPORT) { goto FAIL; } /* Create netif notifier */ wsdd_netif_notifier = netif_notifier_create( wsdd_netif_notifier_callback, NULL); if (wsdd_netif_notifier == NULL) { goto FAIL; } /* Register start/stop callback */ eloop_add_start_stop_callback(wsdd_start_stop_callback); return SANE_STATUS_GOOD; /* Error: cleanup and exit */ FAIL: wsdd_cleanup(); return SANE_STATUS_IO_ERROR; } /* Cleanup WS-Discovery */ void wsdd_cleanup (void) { netif_addr *addr; if (wsdd_log == NULL) { return; /* WSDD not initialized */ } if (wsdd_netif_notifier != NULL) { netif_notifier_free(wsdd_netif_notifier); wsdd_netif_notifier = NULL; } for (addr = wsdd_netif_addr_list; addr != NULL; addr = addr->next) { wsdd_resolver_free(addr->data); } netif_addr_list_free(wsdd_netif_addr_list); wsdd_netif_addr_list = NULL; if (wsdd_mcsock_ipv4 >= 0) { close(wsdd_mcsock_ipv4); wsdd_mcsock_ipv4 = -1; } if (wsdd_mcsock_ipv6 >= 0) { close(wsdd_mcsock_ipv6); wsdd_mcsock_ipv6 = -1; } log_assert(wsdd_log, ll_empty(&wsdd_finding_list)); log_ctx_free(wsdd_log); wsdd_log = NULL; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-xml.c000066400000000000000000000520711500411437100166300ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * XML utilities */ #include "airscan.h" #include #include #include #include /******************** XML reader ********************/ /* XML reader */ struct xml_rd { xmlDoc *doc; /* XML document */ xmlNode *node; /* Current node */ xmlNode *parent; /* Parent node */ const char *name; /* Name of current node */ char *path; /* Path to current node, /-separated */ size_t *pathlen; /* Stack of path lengths */ const xmlChar *text; /* Textual value of current node */ unsigned int depth; /* Depth of current node, 0 for root */ const xml_ns *subst_rules; /* Substitution rules */ xml_ns *subst_cache; /* In the cache, glob-style patterns are replaced by exact-matching strings. */ }; /* Forward declarations */ static const char* xml_rd_ns_subst_lookup(xml_rd *xml, const char *prefix, const char *href); /* Skip dummy nodes. This is internal function, don't call directly */ static void xml_rd_skip_dummy (xml_rd *xml) { xmlNode *node = xml->node; while (node != NULL && node->type != XML_ELEMENT_NODE) { node = node->next; } xml->node = node; } /* Invalidate cached value */ static void xml_rd_node_invalidate_value (xml_rd *xml) { xmlFree((xmlChar*) xml->text); xml->text = NULL; } /* xml_rd_node_switched called when current node is switched. * It invalidates cached value and updates node name */ static void xml_rd_node_switched (xml_rd *xml) { size_t pathlen; /* Invalidate cached value */ xml_rd_node_invalidate_value(xml); /* Update node name */ pathlen = xml->depth ? xml->pathlen[xml->depth - 1] : 0; xml->path = str_resize(xml->path, pathlen); if (xml->node == NULL) { xml->name = NULL; } else { const char *prefix = NULL; if (xml->node->ns != NULL && xml->node->ns->prefix != NULL) { prefix = (const char*) xml->node->ns->prefix; prefix = xml_rd_ns_subst_lookup(xml, prefix, (const char*) xml->node->ns->href); } if (prefix != NULL) { xml->path = str_append(xml->path, prefix); xml->path = str_append_c(xml->path, ':'); } xml->path = str_append(xml->path, (const char*) xml->node->name); xml->name = xml->path + pathlen; } } /* XML parser error callback * * As XML parser leaves all error information in the xmlParserCtxt * structure, this callback does nothing; it's purpose is to * silence error message that libxml2 by default writes to * the stderr */ static void xml_rd_error_callback (void *userdata, xmlErrorPtr error) { (void) userdata; (void) error; } /* Parse XML document */ static error xml_rd_parse (xmlDoc **doc, const char *xml_text, size_t xml_len) { xmlParserCtxtPtr ctxt; error err = NULL; /* Setup XML parser */ ctxt = xmlNewParserCtxt(); if (ctxt == NULL) { err = ERROR("not enough memory"); goto DONE; } ctxt->sax->serror = (xmlStructuredErrorFunc) xml_rd_error_callback; /* Parse the document */ if (xmlCtxtResetPush(ctxt, xml_text, xml_len, NULL, NULL)) { /* It's poorly documented, but xmlCtxtResetPush() fails * only due to OOM. */ err = ERROR("not enough memory"); goto DONE; } xmlParseDocument(ctxt); if (ctxt->wellFormed) { *doc = ctxt->myDoc; } else { const xmlError *lasterr; /* Starting from some version, xmlParserCtxt.lastError * marked as deprecated and xmlCtxtGetLastError is provided * instead. * * I don't know exact version when it did happen, but * 2.12.10 (21210) known to work the both ways, so lets * use that point to switch to the new API. */ #if LIBXML_VERSION >= 21210 lasterr = xmlCtxtGetLastError(ctxt); #else lasterr = &ctxt->lastError; #endif if (lasterr != NULL && lasterr->message != NULL) { err = eloop_eprintf("XML: %s", lasterr->message); } else { err = ERROR("XML: parse error"); } *doc = NULL; } /* Cleanup and exit */ DONE: if (err != NULL && ctxt != NULL && ctxt->myDoc != NULL) { xmlFreeDoc(ctxt->myDoc); } if (ctxt != NULL) { xmlFreeParserCtxt(ctxt); } return err; } /* Parse XML text and initialize reader to iterate * starting from the root node * * The 'ns' argument, if not NULL, points to array of substitution * rules. Last element must have NULL prefix and url * * Array of rules considered to be statically allocated * (at least, it can remain valid during reader life time) * * On success, saves newly constructed reader into * the xml parameter. */ error xml_rd_begin (xml_rd **xml, const char *xml_text, size_t xml_len, const xml_ns *ns) { xmlDoc *doc; error err = xml_rd_parse(&doc, xml_text, xml_len); *xml = NULL; if (err != NULL) { return err; } *xml = mem_new(xml_rd, 1); (*xml)->doc = doc; (*xml)->node = xmlDocGetRootElement((*xml)->doc); (*xml)->path = str_new(); (*xml)->pathlen = mem_new(size_t, 0); (*xml)->subst_rules = ns; xml_rd_skip_dummy(*xml); xml_rd_node_switched(*xml); return NULL; } /* Finish reading, free allocated resources */ void xml_rd_finish (xml_rd **xml) { if (*xml) { if ((*xml)->doc) { xmlFreeDoc((*xml)->doc); } xml_rd_node_invalidate_value(*xml); if ((*xml)->subst_cache != NULL) { size_t i, len = mem_len((*xml)->subst_cache); for (i = 0; i < len; i ++) { mem_free((char*) (*xml)->subst_cache[i].uri); } mem_free((*xml)->subst_cache); } mem_free((*xml)->pathlen); mem_free((*xml)->path); mem_free(*xml); *xml = NULL; } } /* Perform namespace prefix substitution. Is substitution * is not setup or no match was found, the original prefix * will be returned */ static const char* xml_rd_ns_subst_lookup(xml_rd *xml, const char *prefix, const char *href) { size_t i, len = mem_len(xml->subst_cache); /* Substitution enabled? */ if (xml->subst_rules == NULL) { return prefix; } /* Lookup cache first */ for (i = 0; i < len; i ++) { if (!strcmp(href, xml->subst_cache[i].uri)) { return xml->subst_cache[i].prefix; } } /* Now try glob-style rules */ for (i = 0; xml->subst_rules[i].prefix != NULL; i ++) { if (!fnmatch(xml->subst_rules[i].uri, href, 0)) { prefix = xml->subst_rules[i].prefix; /* Update cache. Grow it if required */ xml->subst_cache = mem_resize(xml->subst_cache, len + 1, 0); xml->subst_cache[len].prefix = prefix; xml->subst_cache[len].uri = str_dup(href); /* Break out of loop */ break; } } return prefix; } /* Get current node depth in the tree. Root depth is 0 */ unsigned int xml_rd_depth (xml_rd *xml) { return xml->depth; } /* Check for end-of-document condition */ bool xml_rd_end (xml_rd *xml) { return xml->node == NULL; } /* Shift to the next node */ void xml_rd_next (xml_rd *xml) { if (xml->node) { xml->node = xml->node->next; xml_rd_skip_dummy(xml); xml_rd_node_switched(xml); } } /* Shift to the next node, visiting the nested nodes on the way * * If depth > 0, it will not return from nested nodes * upper the specified depth */ void xml_rd_deep_next (xml_rd *xml, unsigned int depth) { xml_rd_enter(xml); while (xml_rd_end(xml) && xml_rd_depth(xml) > depth + 1) { xml_rd_leave(xml); xml_rd_next(xml); } } /* Enter the current node - iterate its children */ void xml_rd_enter (xml_rd *xml) { if (xml->node) { /* Save current path length into pathlen stack */ xml->path = str_append_c(xml->path, '/'); xml->pathlen = mem_resize(xml->pathlen, xml->depth + 1, 0); xml->pathlen[xml->depth] = mem_len(xml->path); /* Enter the node */ xml->parent = xml->node; xml->node = xml->node->children; xml_rd_skip_dummy(xml); /* Increment depth and recompute node name */ xml->depth ++; xml_rd_skip_dummy(xml); xml_rd_node_switched(xml); } } /* Leave the current node - return to its parent */ void xml_rd_leave (xml_rd *xml) { if (xml->depth > 0) { xml->depth --; xml->node = xml->parent; if (xml->node) { xml->parent = xml->node->parent; } xml_rd_node_switched(xml); } } /* Get name of the current node. * * The returned string remains valid, until reader is cleaned up * or current node is changed (by set/next/enter/leave operations). * You don't need to free this string explicitly */ const char* xml_rd_node_name (xml_rd *xml) { return xml->name; } /* Get full path to the current node, '/'-separated */ const char* xml_rd_node_path (xml_rd *xml) { return xml->node ? xml->path : NULL; } /* Match name of the current node against the pattern */ bool xml_rd_node_name_match (xml_rd *xml, const char *pattern) { return xml->name != NULL && !strcmp(xml->name, pattern); } /* Get value of the current node as text * * The returned string remains valid, until reader is cleaned up * or current node is changed (by set/next/enter/leave operations). * You don't need to free this string explicitly */ const char* xml_rd_node_value (xml_rd *xml) { if (xml->text == NULL && xml->node != NULL) { xml->text = xmlNodeGetContent(xml->node); str_trim((char*) xml->text); } return (const char*) xml->text; } /* Get value of the current node as unsigned integer */ error xml_rd_node_value_uint (xml_rd *xml, SANE_Word *val) { const char *s = xml_rd_node_value(xml); char *end; unsigned long v; log_assert(NULL, s != NULL); v = strtoul(s, &end, 10); if (end == s || *end || v != (unsigned long) (SANE_Word) v) { return eloop_eprintf("%s: invalid numerical value", xml_rd_node_name(xml)); } *val = (SANE_Word) v; return NULL; } /******************** XML writer ********************/ /* XML writer node */ typedef struct xml_wr_node xml_wr_node; struct xml_wr_node { const char *name; /* Node name */ const char *value; /* Node value, if any */ const xml_attr *attrs; /* Attributes, if any */ xml_wr_node *children; /* Node children, if any */ xml_wr_node *next; /* Next sibling node, if any */ xml_wr_node *parent; /* Parent node, if any */ }; /* XML writer */ struct xml_wr { xml_wr_node *root; /* Root node */ xml_wr_node *current; /* Current node */ const xml_ns *ns; /* Namespace */ }; /* Create XML writer node */ static xml_wr_node* xml_wr_node_new (const char *name, const char *value, const xml_attr *attrs) { xml_wr_node *node = mem_new(xml_wr_node, 1); node->name = str_dup(name); node->attrs = attrs; if (value != NULL) { node->value = str_dup(value); } return node; } /* Free XML writer node */ static void xml_wr_node_free (xml_wr_node *node) { mem_free((char*) node->name); mem_free((char*) node->value); mem_free(node); } /* Free XML writer node with its children */ static void xml_wr_node_free_recursive (xml_wr_node *node) { xml_wr_node *node2, *next; for (node2 = node->children; node2 != NULL; node2 = next) { next = node2->next; xml_wr_node_free_recursive(node2); } xml_wr_node_free(node); } /* Begin writing XML document. Root node will be created automatically */ xml_wr* xml_wr_begin (const char *root, const xml_ns *ns) { xml_wr *xml = mem_new(xml_wr, 1); xml->root = xml_wr_node_new(root, NULL, NULL); xml->current = xml->root; xml->ns = ns; return xml; } /* Format indentation space */ static char* xml_wr_format_indent (char *buf, unsigned int level) { unsigned int i; for (i = 0; i < level; i ++) { buf = str_append_c(buf, ' '); buf = str_append_c(buf, ' '); } return buf; } /* Format node's value */ static char* xml_wr_format_value (char *buf, const char *value) { for (;;) { char c = *value ++; switch (c) { case '&': buf = str_append(buf, "&"); break; case '<': buf = str_append(buf, "<"); break; case '>': buf = str_append(buf, ">"); break; case '"': buf = str_append(buf, """); break; case '\'': buf = str_append(buf, "'"); break; case '\0': return buf; default: buf = str_append_c(buf, c); } } return buf; } /* Format node with its children, recursively */ static char* xml_wr_format_node (xml_wr *xml, char *buf, xml_wr_node *node, unsigned int level, bool compact) { if (!compact) { buf = xml_wr_format_indent(buf, level); } buf = str_append_printf(buf, "<%s", node->name); if (level == 0) { /* Root node defines namespaces */ int i; for (i = 0; xml->ns[i].uri != NULL; i ++) { buf = str_append_printf(buf, " xmlns:%s=\"%s\"", xml->ns[i].prefix, xml->ns[i].uri); } } if (node->attrs != NULL) { int i; for (i = 0; node->attrs[i].name != NULL; i ++) { buf = str_append_printf(buf, " %s=\"%s\"", node->attrs[i].name, node->attrs[i].value); } } buf = str_append_c(buf, '>'); if (node->children) { xml_wr_node *node2; if (!compact) { buf = str_append_c(buf, '\n'); } for (node2 = node->children; node2 != NULL; node2 = node2->next) { buf = xml_wr_format_node(xml, buf, node2, level + 1, compact); } if (!compact) { buf = xml_wr_format_indent(buf, level); } buf = str_append_printf(buf, "", node->name); if (!compact && level != 0) { buf = str_append_c(buf, '\n'); } } else { if (node->value != NULL) { buf = xml_wr_format_value(buf, node->value); } buf = str_append_printf(buf,"", node->name); if (!compact) { buf = str_append_c(buf, '\n'); } } return buf; } /* Revert list of node's children, recursively */ static void xml_wr_revert_children (xml_wr_node *node) { xml_wr_node *next, *prev = NULL, *node2; for (node2 = node->children; node2 != NULL; node2 = next) { xml_wr_revert_children (node2); next = node2->next; node2->next = prev; prev = node2; } node->children = prev; } /* xml_wr_finish(), internal version */ static char* xml_wr_finish_internal (xml_wr *xml, bool compact) { char *buf; buf = str_dup(""); if (!compact) { buf = str_append_c(buf, '\n'); } xml_wr_revert_children(xml->root); buf = xml_wr_format_node(xml, buf, xml->root, 0, compact); xml_wr_node_free_recursive(xml->root); mem_free(xml); return buf; } /* Finish writing, generate document string. * Caller must mem_free() this string after use */ char* xml_wr_finish (xml_wr *xml) { return xml_wr_finish_internal(xml, false); } /* Like xml_wr_finish, but returns compact representation * of XML (without indentation and new lines) */ char* xml_wr_finish_compact (xml_wr *xml) { return xml_wr_finish_internal(xml, true); } /* Add XML writer node to the current node's children */ static void xml_wr_add_node (xml_wr *xml, xml_wr_node *node) { node->parent = xml->current; node->next = xml->current->children; xml->current->children = node; } /* Add node with textual value */ void xml_wr_add_text (xml_wr *xml, const char *name, const char *value) { xml_wr_add_text_attr(xml, name, value, NULL); } /* Add text node with attributes */ void xml_wr_add_text_attr (xml_wr *xml, const char *name, const char *value, const xml_attr *attrs) { xml_wr_add_node(xml, xml_wr_node_new(name, value, attrs)); } /* Add node with unsigned integer value */ void xml_wr_add_uint (xml_wr *xml, const char *name, unsigned int value) { xml_wr_add_uint_attr(xml, name, value, NULL); } /* Add node with unsigned integer value and attributes */ void xml_wr_add_uint_attr (xml_wr *xml, const char *name, unsigned int value, const xml_attr *attrs) { char buf[64]; sprintf(buf, "%u", value); xml_wr_add_text_attr(xml, name, buf, attrs); } /* Add node with boolean value */ void xml_wr_add_bool (xml_wr *xml, const char *name, bool value) { xml_wr_add_bool_attr(xml, name, value, NULL); } /* Add node with boolean value and attributes */ void xml_wr_add_bool_attr (xml_wr *xml, const char *name, bool value, const xml_attr *attrs) { xml_wr_add_text_attr(xml, name, value ? "true" : "false", attrs); } /* Create node with children and enter newly added node */ void xml_wr_enter (xml_wr *xml, const char *name) { xml_wr_enter_attr(xml, name, NULL); } /* xml_wr_enter with attributes */ void xml_wr_enter_attr (xml_wr *xml, const char *name, const xml_attr *attrs) { xml_wr_node *node = xml_wr_node_new(name, NULL, attrs); xml_wr_add_node(xml, node); xml->current = node; } /* Leave the current node */ void xml_wr_leave (xml_wr *xml) { log_assert(NULL, xml->current->parent != NULL); xml->current = xml->current->parent; } /******************** XML formatter ********************/ /* Format node name with namespace prefix */ static void xml_format_node_name (FILE *fp, xmlNode *node) { if (node->ns != NULL && node->ns->prefix != NULL) { fputs((char*) node->ns->prefix, fp); putc(':', fp); } fputs((char*) node->name, fp); } /* Format node attributes */ static void xml_format_node_attrs (FILE *fp, xmlNode *node) { xmlNs *ns; xmlAttr *attr; /* Format namespace attributes */ for (ns = node->nsDef; ns != NULL; ns = ns->next) { if (ns->prefix == NULL) { continue; } /* Write namespace name */ putc(' ', fp); fputs("xmlns:", fp); fputs((char*) ns->prefix, fp); /* Write namespace value */ putc('=', fp); putc('"', fp); fputs((char*) ns->href, fp); putc('"', fp); } /* Format properties */ for (attr = node->properties; attr != NULL; attr = attr->next) { xmlChar *val = xmlNodeListGetString(node->doc, attr->children, 1); /* Write attribute name with namespace prefix */ putc(' ', fp); if (attr->ns != NULL && attr->ns->prefix != NULL) { fputs((char*) attr->ns->prefix, fp); putc(':', fp); } fputs((char*) attr->name, fp); /* Write attribute value */ putc('=', fp); putc('"', fp); fputs((char*) val, fp); putc('"', fp); xmlFree(val); } } /* Format indent */ static void xml_format_indent (FILE *fp, int indent) { int i; for (i = 0; i < indent; i ++) { putc(' ', fp); putc(' ', fp); } } /* Format entire node */ static void xml_format_node (FILE *fp, xmlNode *node, int indent) { xmlNode *child; bool with_children = false; bool with_value = false; /* Format opening tag */ xml_format_indent(fp, indent); putc('<', fp); xml_format_node_name(fp, node); xml_format_node_attrs(fp, node); for (child = node->children; child != NULL; child = child->next) { if (child->type == XML_ELEMENT_NODE) { if (!with_children) { putc('>', fp); putc('\n', fp); with_children = true; } xml_format_node(fp, child, indent + 1); } } if (!with_children) { xmlChar *val = xmlNodeGetContent(node); str_trim((char*) val); if (*val != '\0') { putc('>', fp); fputs((char*) val, fp); with_value = true; } xmlFree(val); } if (with_children) { xml_format_indent(fp, indent); } /* Format closing tag */ if (with_children || with_value) { putc('<', fp); putc('/', fp); xml_format_node_name(fp, node); putc('>', fp); } else { putc('/', fp); putc('>', fp); } putc('\n', fp); } /* Format XML to file. It either succeeds, writes a formatted XML * and returns true, or fails, writes nothing to file and returns false */ bool xml_format (FILE *fp, const char *xml_text, size_t xml_len) { xmlDoc *doc; error err = xml_rd_parse(&doc, xml_text, xml_len); xmlNode *node; if (err != NULL) { return err; } for (node = doc->children; node != NULL; node = node->next) { xml_format_node(fp, node, 0); } xmlFreeDoc(doc); return true; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan-zeroconf.c000066400000000000000000001152761500411437100176640ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * ZeroConf (device discovery) */ #include "airscan.h" #include #include #include #include #include /******************** Constants *********************/ /* Max time to wait until device table is ready, in milliseconds */ #define ZEROCONF_READY_TIMEOUT 5000 /******************** Local Types *********************/ /* zeroconf_device represents a single device */ struct zeroconf_device { unsigned int devid; /* Unique ident */ uuid uuid; /* Device UUID */ ip_addrset *addrs; /* Device's addresses */ const char *mdns_name; /* Device's MDNS name, NULL for WSDD */ const char *model; /* Device model name */ unsigned int protocols; /* Supported protocols, set of 1<devid = devid_alloc(); device->uuid = finding->uuid; device->addrs = ip_addrset_new(); if (finding->name != NULL) { device->mdns_name = str_dup(finding->name); } device->model = finding->model; ll_init(&device->findings); ll_push_end(&zeroconf_device_list, &device->node_list); return device; } /* Delete the device */ static void zeroconf_device_del (zeroconf_device *device) { ll_del(&device->node_list); ip_addrset_free(device->addrs); mem_free((char*) device->mdns_name); devid_free(device->devid); mem_free(device); } /* Check if device is MDNS device */ static bool zeroconf_device_is_mdns (zeroconf_device *device) { return device->mdns_name != NULL; } /* Rebuild device->ifaces, device->protocols and device->methods */ static void zeroconf_device_rebuild_sets (zeroconf_device *device) { ll_node *node; device->protocols = 0; device->methods = 0; for (LL_FOR_EACH(node, &device->findings)) { zeroconf_finding *finding; ID_PROTO proto; finding = OUTER_STRUCT(node, zeroconf_finding, list_node); proto = zeroconf_method_to_proto(finding->method); if (proto != ID_PROTO_UNKNOWN) { device->protocols |= 1 << proto; } device->methods |= 1 << finding->method; } } /* Update device->model */ static void zeroconf_device_update_model (zeroconf_device *device) { ll_node *node; zeroconf_finding *hint = NULL, *wsd = NULL; for (LL_FOR_EACH(node, &device->findings)) { zeroconf_finding *finding; finding = OUTER_STRUCT(node, zeroconf_finding, list_node); switch (finding->method) { case ZEROCONF_USCAN_TCP: case ZEROCONF_USCANS_TCP: device->model = finding->model; return; case ZEROCONF_MDNS_HINT: if (hint == NULL) { hint = finding; } break; case ZEROCONF_WSD: if (wsd == NULL) { wsd = finding; } break; default: log_internal_error(zeroconf_log); } } device->model = hint ? hint->model : wsd->model; } /* Add zeroconf_finding to zeroconf_device */ static void zeroconf_device_add_finding (zeroconf_device *device, zeroconf_finding *finding) { log_assert(zeroconf_log, finding->device == NULL); finding->device = device; ll_push_end(&device->findings, &finding->list_node); ip_addrset_merge(device->addrs, finding->addrs); if (finding->endpoints != NULL) { ID_PROTO proto = zeroconf_method_to_proto(finding->method); if (proto != ID_PROTO_UNKNOWN) { device->protocols |= 1 << proto; } device->methods |= 1 << finding->method; } zeroconf_device_update_model(device); } /* Delete zeroconf_finding from zeroconf_device */ static void zeroconf_device_del_finding (zeroconf_finding *finding) { zeroconf_device *device = finding->device; log_assert(zeroconf_log, device != NULL); ll_del(&finding->list_node); if (ll_empty(&device->findings)) { zeroconf_device_del(device); return; } zeroconf_device_rebuild_sets(device); zeroconf_device_update_model(device); } /* Get model name */ static const char* zeroconf_device_model (zeroconf_device *device) { if (device->model != NULL) { return device->model; } /* If model name is not available, fall back to UUID */ return device->uuid.text; } /* Get device name */ static const char* zeroconf_device_name (zeroconf_device *device) { if (zeroconf_device_is_mdns(device)) { return device->mdns_name; } if (device->buddy != NULL) { return device->buddy->mdns_name; } return zeroconf_device_model(device); } /* Get protocols, exposed by device */ static unsigned int zeroconf_device_protocols (zeroconf_device *device) { unsigned int protocols = device->protocols; if (!conf.proto_auto) { return protocols; } if ((protocols & (1 << ID_PROTO_ESCL)) != 0) { return 1 << ID_PROTO_ESCL; } if ((protocols & (1 << ID_PROTO_WSD)) != 0) { return 1 << ID_PROTO_WSD; } return 0; } /* Get device endpoints. * Caller is responsible to free the returned list */ static zeroconf_endpoint* zeroconf_device_endpoints (zeroconf_device *device, ID_PROTO proto) { zeroconf_endpoint *endpoints = NULL; ll_node *node; for (LL_FOR_EACH(node, &device->findings)) { zeroconf_finding *finding; finding = OUTER_STRUCT(node, zeroconf_finding, list_node); if (zeroconf_method_to_proto(finding->method) == proto) { zeroconf_endpoint *ep, *ep2; for (ep = finding->endpoints; ep != NULL; ep = ep->next) { ep2 = zeroconf_endpoint_copy_single(ep); ep2->next = endpoints; endpoints = ep2; } } } return zeroconf_endpoint_list_sort_dedup(endpoints); } /* Find zeroconf_device by ident * Protocol, encoded into ident, returned via second parameter */ static zeroconf_device* zeroconf_device_find_by_ident (const char *ident, ID_PROTO *proto) { unsigned int devid; const char *name; ll_node *node; zeroconf_device *device = NULL; name = zeroconf_ident_split(ident, &devid, proto); if (name == NULL) { return NULL; } /* Lookup device */ for (LL_FOR_EACH(node, &zeroconf_device_list)) { device = OUTER_STRUCT(node, zeroconf_device, node_list); if (device->devid == devid && !strcmp(name, zeroconf_device_name(device))) { break; } } if (device == NULL) return NULL; /* Check that device supports requested protocol */ if ((device->protocols & (1 << *proto)) != 0) { return device; } return NULL; } /* Check if device is blacklisted * * Returns reason string, if device is blacklisted, NULL, if not */ static const char* zeroconf_device_is_blacklisted (zeroconf_device *device) { conf_blacklist *ent; const char *name, *model; if (conf.blacklist == NULL) { return NULL; } name = zeroconf_device_name(device); model = zeroconf_device_model(device); for (ent = conf.blacklist; ent != NULL; ent = ent->next) { if (ent->name != NULL && !fnmatch(ent->name, name, 0)) { return "name"; } if (ent->model != NULL && !fnmatch(ent->model, model, 0)) { return "model"; } if (ent->net.addr.af != AF_UNSPEC && ip_addrset_on_network(device->addrs, ent->net)) { return "address"; } } return NULL; } /******************** Merging devices *********************/ /* Recompute device->buddy for all devices */ static void zeroconf_merge_recompute_buddies (void) { ll_node *node, *node2; zeroconf_device *device, *device2; for (LL_FOR_EACH(node, &zeroconf_device_list)) { device = OUTER_STRUCT(node, zeroconf_device, node_list); device->buddy = NULL; } for (LL_FOR_EACH(node, &zeroconf_device_list)) { device = OUTER_STRUCT(node, zeroconf_device, node_list); for (node2 = ll_next(&zeroconf_device_list, node); node2 != NULL; node2 = ll_next(&zeroconf_device_list, node2)) { device2 = OUTER_STRUCT(node2, zeroconf_device, node_list); if (zeroconf_device_is_mdns(device) != zeroconf_device_is_mdns(device2)) { if (ip_addrset_is_intersect(device->addrs, device2->addrs)) { device->buddy = device2; device2->buddy = device; } } } } } /* Check that new finding should me merged with existent device */ static bool zeroconf_merge_check (zeroconf_device *device, zeroconf_finding *finding) { if ((device->mdns_name == NULL) != (finding->name == NULL)) { return false; } if (device->mdns_name != NULL && strcasecmp(device->mdns_name, finding->name)) { return false; } if (uuid_equal(device->uuid, finding->uuid)) { return true; } return false; } /* Find device, suitable for merging with specified finding */ static zeroconf_device* zeroconf_merge_find (zeroconf_finding *finding) { ll_node *node; for (LL_FOR_EACH(node, &zeroconf_device_list)) { zeroconf_device *device; device = OUTER_STRUCT(node, zeroconf_device, node_list); if (zeroconf_merge_check(device, finding)) { return device; } } return NULL; } /******************** Ident Strings *********************/ /* Encode ID_PROTO for device ident */ static char zeroconf_ident_proto_encode (ID_PROTO proto) { switch (proto) { case ID_PROTO_ESCL: return 'e'; case ID_PROTO_WSD: return 'w'; case ID_PROTO_UNKNOWN: case NUM_ID_PROTO: break; } log_internal_error(zeroconf_log); return 0; } /* Decode ID_PROTO from device ident */ static ID_PROTO zeroconf_ident_proto_decode (char c) { switch (c) { case 'e': return ID_PROTO_ESCL; case 'w': return ID_PROTO_WSD; } return ID_PROTO_UNKNOWN; } /* Make device ident string * The returned string must be released with mem_free() */ static const char* zeroconf_ident_make (const char *name, unsigned int devid, ID_PROTO proto) { return str_printf("%c%x:%s", zeroconf_ident_proto_encode(proto), devid, name); } /* Split device ident string. * Returns NULL on error, device name on success. * Device name points somewhere into the input buffer */ static const char* zeroconf_ident_split (const char *ident, unsigned int *devid, ID_PROTO *proto) { const char *name; char *end; /* Find name */ name = strchr(ident, ':'); if (name == NULL) { return NULL; } name ++; /* Decode proto and devid */ *proto = zeroconf_ident_proto_decode(*ident); if (*proto == ID_PROTO_UNKNOWN) { return NULL; } ident ++; *devid = (unsigned int) strtoul(ident, &end, 16); if (end == ident || *end != ':') { return NULL; } return name; } /******************** Endpoints *********************/ /* Create new zeroconf_endpoint. Newly created endpoint * takes ownership of uri string */ zeroconf_endpoint* zeroconf_endpoint_new (ID_PROTO proto, http_uri *uri) { zeroconf_endpoint *endpoint = mem_new(zeroconf_endpoint, 1); endpoint->proto = proto; endpoint->uri = uri; if (proto == ID_PROTO_ESCL) { // We own the uri, so modify without making a separate copy. http_uri_fix_end_slash(endpoint->uri); } return endpoint; } /* Clone a single zeroconf_endpoint */ static zeroconf_endpoint* zeroconf_endpoint_copy_single (const zeroconf_endpoint *endpoint) { zeroconf_endpoint *endpoint2 = mem_new(zeroconf_endpoint, 1); *endpoint2 = *endpoint; endpoint2->uri = http_uri_clone(endpoint->uri); endpoint2->next = NULL; return endpoint2; } /* Free single zeroconf_endpoint */ void zeroconf_endpoint_free_single (zeroconf_endpoint *endpoint) { http_uri_free(endpoint->uri); mem_free(endpoint); } /* Create a copy of zeroconf_endpoint list */ zeroconf_endpoint* zeroconf_endpoint_list_copy (const zeroconf_endpoint *list) { zeroconf_endpoint *newlist = NULL, *last = NULL, *endpoint; while (list != NULL) { endpoint = zeroconf_endpoint_copy_single(list); if (last != NULL) { last->next = endpoint; } else { newlist = endpoint; } last = endpoint; list = list->next; } return newlist; } /* Free zeroconf_endpoint list */ void zeroconf_endpoint_list_free (zeroconf_endpoint *list) { while (list != NULL) { zeroconf_endpoint *next = list->next; zeroconf_endpoint_free_single(list); list = next; } } /* Compare two endpoints, for sorting */ static int zeroconf_endpoint_cmp (const zeroconf_endpoint *e1, const zeroconf_endpoint *e2) { const struct sockaddr *a1 = http_uri_addr(e1->uri); const struct sockaddr *a2 = http_uri_addr(e2->uri); if (a1 != NULL && a2 != NULL) { bool ll1 = ip_sockaddr_is_linklocal(a1); bool ll2 = ip_sockaddr_is_linklocal(a2); int cmp; /* Prefer directly reachable addresses */ cmp = netif_distance_cmp(a1, a2); if (cmp != 0) { return cmp; } /* Prefer normal addresses, rather that link-local */ if (ll1 != ll2) { return ll1 ? 1 : -1; } /* Be in trend: prefer IPv6 addresses */ if (a1->sa_family != a2->sa_family) { return a1->sa_family == AF_INET6 ? -1 : 1; } } /* Otherwise, sort lexicographically */ return strcmp(http_uri_str(e1->uri), http_uri_str(e2->uri)); } /* Revert zeroconf_endpoint list */ static zeroconf_endpoint* zeroconf_endpoint_list_revert (zeroconf_endpoint *list) { zeroconf_endpoint *prev = NULL, *next; while (list != NULL) { next = list->next; list->next = prev; prev = list; list = next; } return prev; } /* Sort list of endpoints */ zeroconf_endpoint* zeroconf_endpoint_list_sort (zeroconf_endpoint *list) { zeroconf_endpoint *halves[2] = {NULL, NULL}; int half = 0; if (list == NULL || list->next == NULL) { return list; } /* Split list into halves */ while (list != NULL) { zeroconf_endpoint *next = list->next; list->next = halves[half]; halves[half] = list; half ^= 1; list = next; } /* Sort each half, recursively */ for (half = 0; half < 2; half ++) { halves[half] = zeroconf_endpoint_list_sort(halves[half]); } /* Now merge the sorted halves */ list = NULL; while (halves[0] != NULL || halves[1] != NULL) { zeroconf_endpoint *next; if (halves[0] == NULL) { half = 1; } else if (halves[1] == NULL) { half = 0; } else if (zeroconf_endpoint_cmp(halves[0], halves[1]) < 0) { half = 0; } else { half = 1; } next = halves[half]->next; halves[half]->next = list; list = halves[half]; halves[half] = next; } /* And revert the list, as after merging it is reverted */ return zeroconf_endpoint_list_revert(list); } /* Sort list of endpoints and remove duplicates */ zeroconf_endpoint* zeroconf_endpoint_list_sort_dedup (zeroconf_endpoint *list) { zeroconf_endpoint *addr, *next; if (list == NULL) { return NULL; } list = zeroconf_endpoint_list_sort(list); addr = list; while ((next = addr->next) != NULL) { if (zeroconf_endpoint_cmp(addr, next) == 0) { addr->next = next->next; zeroconf_endpoint_free_single(next); } else { addr = next; } } return list; } /* Check if list of endpoints already contains the given * endpoint (i.e., endpoint with the same URI and protocol) */ bool zeroconf_endpoint_list_contains (const zeroconf_endpoint *list, const zeroconf_endpoint *endpoint) { while (list != NULL) { if (list->proto == endpoint->proto && http_uri_equal(list->uri, endpoint->uri)) { return true; } list = list->next; } return false; } /* 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 */ static conf_device* zeroconf_find_static_by_name (const char *name) { conf_device *dev_conf; for (dev_conf = conf.devices; dev_conf != NULL; dev_conf = dev_conf->next) { if (!strcasecmp(dev_conf->name, name)) { return dev_conf; } } return NULL; } /* Look for device's static configuration by device ident */ static conf_device* zeroconf_find_static_by_ident (const char *ident) { conf_device *dev_conf; ID_PROTO proto; unsigned int devid; const char *name; name = zeroconf_ident_split(ident, &devid, &proto); if (name == NULL) { return NULL; } for (dev_conf = conf.devices; dev_conf != NULL; dev_conf = dev_conf->next) { if (dev_conf->devid == devid && dev_conf->proto == proto && !strcmp(dev_conf->name, name)) { return dev_conf; } } return NULL; } /***** Miscellaneous functions for zeroconf_finding *****/ /* Compare two pointers to pointers to zeroconf_finding (zeroconf_finding**) * by index+name, for qsort */ int zeroconf_finding_qsort_by_index_name (const void *p1, const void *p2) { const zeroconf_finding *f1 = *(const zeroconf_finding * const *) p1; const zeroconf_finding *f2 = *(const zeroconf_finding * const *) p2; if (f1->ifindex < f2->ifindex) { return -1; } if (f1->ifindex > f2->ifindex) { return 1; } return strcmp(f1->name, f2->name); } /******************** Events from discovery providers *********************/ /* Publish the zeroconf_finding. */ void zeroconf_finding_publish (zeroconf_finding *finding) { size_t count, i; zeroconf_device *device; char ifname[IF_NAMESIZE]; const ip_addr *addrs; ID_PROTO proto = zeroconf_method_to_proto(finding->method); /* Print log messages */ if (if_indextoname(finding->ifindex, ifname) == NULL) { strcpy(ifname, "?"); } log_debug(zeroconf_log, "found %s", finding->uuid.text); log_debug(zeroconf_log, " method: %s", zeroconf_method_name(finding->method)); log_debug(zeroconf_log, " interface: %d (%s)", finding->ifindex, ifname); log_debug(zeroconf_log, " name: %s", finding->name ? finding->name : "-"); log_debug(zeroconf_log, " model: %s", finding->model ? finding->model : "-"); log_debug(zeroconf_log, " addresses:"); addrs = ip_addrset_addresses(finding->addrs, &count); for (i = 0; i < count; i ++) { ip_straddr straddr = ip_addr_to_straddr(addrs[i], true); log_debug(zeroconf_log, " %s", straddr.text); } if (proto != ID_PROTO_UNKNOWN) { zeroconf_endpoint *ep; log_debug(zeroconf_log, " protocol: %s", id_proto_name(proto)); log_debug(zeroconf_log, " endpoints:"); for (ep = finding->endpoints; ep != NULL; ep = ep->next) { log_debug(zeroconf_log, " %s", http_uri_str(ep->uri)); } } /* Handle new finding */ device = zeroconf_merge_find(finding); if (device != NULL) { log_debug(zeroconf_log, " device: %4.4x (found)", device->devid); } else { device = zeroconf_device_add(finding); log_debug(zeroconf_log, " device: %4.4x (created)", device->devid); } zeroconf_device_add_finding(device, finding); zeroconf_merge_recompute_buddies(); pthread_cond_broadcast(&zeroconf_initscan_cond); } /* Withdraw the finding */ void zeroconf_finding_withdraw (zeroconf_finding *finding) { char ifname[IF_NAMESIZE] = "?"; if_indextoname(finding->ifindex, ifname); log_debug(zeroconf_log, "device gone %s", finding->uuid.text); log_debug(zeroconf_log, " method: %s", zeroconf_method_name(finding->method)); log_debug(zeroconf_log, " interface: %d (%s)", finding->ifindex, ifname); zeroconf_device_del_finding(finding); zeroconf_merge_recompute_buddies(); pthread_cond_broadcast(&zeroconf_initscan_cond); } /* Notify zeroconf subsystem that initial scan * for the method is done */ void zeroconf_finding_done (ZEROCONF_METHOD method) { log_debug(zeroconf_log, "%s: initial scan finished", zeroconf_method_name(method)); zeroconf_initscan_bits &= ~(1 << method); pthread_cond_broadcast(&zeroconf_initscan_cond); } /******************** Support for SANE API *********************/ /* zeroconf_initscan_timer callback */ static void zeroconf_initscan_timer_callback (void *unused) { (void) unused; log_debug(zeroconf_log, "initial scan timer expired"); mdns_initscan_timer_expired(); wsdd_initscan_timer_expired(); zeroconf_initscan_timer = NULL; pthread_cond_broadcast(&zeroconf_initscan_cond); } /* Check if initial scan is done */ static bool zeroconf_initscan_done (void) { ll_node *node; zeroconf_device *device; /* If all discovery methods are done, we are done */ if (zeroconf_initscan_bits == 0) { return true; } /* Regardless of options, all DNS-SD methods must be done */ if ((zeroconf_initscan_bits & ~(1 << ZEROCONF_WSD)) != 0) { log_debug(zeroconf_log, "device_list wait: DNS-SD not finished..."); return false; } /* If we are here, ZEROCONF_WSD is not done yet, * and if we are not in fast-wsdd mode, we must wait */ log_assert(zeroconf_log, (zeroconf_initscan_bits & (1 << ZEROCONF_WSD)) != 0); if (conf.wsdd_mode != WSDD_FAST) { log_debug(zeroconf_log, "device_list wait: WSDD not finished..."); return false; } /* Check for completion, device by device: * * In manual protocol switch mode, WSDD buddy must be * found for device, so we have a choice. Otherwise, it's * enough if device has supported protocols */ for (LL_FOR_EACH(node, &zeroconf_device_list)) { device = OUTER_STRUCT(node, zeroconf_device, node_list); if (!conf.proto_auto) { if (zeroconf_device_is_mdns(device) && device->buddy == NULL) { log_debug(zeroconf_log, "device_list wait: waiting for WSDD buddy for '%s' (%d)", zeroconf_device_name(device), device->devid); return false; } } else { if (device->protocols == 0) { log_debug(zeroconf_log, "device_list wait: waiting for any proto for '%s' (%d)", zeroconf_device_name(device), device->devid); return false; } } } return true; } /* Wait until initial scan is done */ static void zeroconf_initscan_wait (void) { bool ok = false; log_debug(zeroconf_log, "device_list wait: requested"); for (;;) { ok = zeroconf_initscan_done(); if (ok || zeroconf_initscan_timer == NULL) { break; } eloop_cond_wait(&zeroconf_initscan_cond); } log_debug(zeroconf_log, "device_list wait: %s", ok ? "OK" : "timeout" ); } /* Compare SANE_Device*, for qsort */ static int zeroconf_device_list_qsort_cmp (const void *p1, const void *p2) { int cmp; const SANE_Device *d1 = *(SANE_Device**) p1; const SANE_Device *d2 = *(SANE_Device**) p2; cmp = strcasecmp(d1->model, d2->model); if (cmp == 0) { cmp = strcasecmp(d1->vendor, d2->vendor); } if (cmp == 0) { cmp = strcmp(d1->name, d2->name); } 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 can[64]; char use[64]; zeroconf_device_list_fmt_protocols(can, sizeof(can), device->protocols); zeroconf_device_list_fmt_protocols(use, sizeof(use), protocols); log_debug(zeroconf_log, "%s (%d): can:%s, use:%s", name, device->devid, can, use); } /* Get list of devices, in SANE format */ const SANE_Device** zeroconf_device_list_get (void) { size_t dev_count = 0, dev_count_static = 0; conf_device *dev_conf; const SANE_Device **dev_list = sane_device_array_new(); ll_node *node; int i; 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) { SANE_Device *info; const char *proto; const char *host; size_t hostlen; if (dev_conf->uri == NULL) { continue; } info = mem_new(SANE_Device, 1); proto = id_proto_name(dev_conf->proto); dev_list = sane_device_array_append(dev_list, info); dev_count ++; info->name = zeroconf_ident_make(dev_conf->name, dev_conf->devid, dev_conf->proto); info->vendor = str_dup(proto); info->model = str_dup(dev_conf->name); host = http_uri_get_host(dev_conf->uri); hostlen = strlen(host); if (host[0] == '[') { host ++; hostlen -= 2; } info->type = str_printf("ip=%.*s", (int) hostlen, host); } dev_count_static = dev_count; for (LL_FOR_EACH(node, &zeroconf_device_list)) { zeroconf_device *device; ID_PROTO proto; const char *name, *model, *blacklisted; unsigned int protocols; device = OUTER_STRUCT(node, zeroconf_device, node_list); name = zeroconf_device_name(device); model = zeroconf_device_model(device); 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 (%d): skipping, device clashes statically configured", name, device->devid); continue; } blacklisted = zeroconf_device_is_blacklisted(device); if (blacklisted != NULL) { log_debug(zeroconf_log, "%s (%d): skipping, device is blacklisted by %s", name, device->devid, blacklisted); continue; } if (conf.proto_auto && !zeroconf_device_is_mdns(device)) { zeroconf_device *device2 = device->buddy; if (device2 != NULL && zeroconf_device_protocols(device2) != 0) { log_debug(zeroconf_log, "%s (%d): skipping, shadowed by %s (%d)", name, device->devid, zeroconf_device_name(device2), device2->devid); continue; } } if (protocols == 0) { log_debug(zeroconf_log, "%s (%d): skipping, none of supported protocols discovered", name, device->devid); continue; } for (proto = 0; proto < NUM_ID_PROTO; proto ++) { if ((protocols & (1 << proto)) != 0) { SANE_Device *info = mem_new(SANE_Device, 1); const char *proto_name = id_proto_name(proto); char *type; dev_list = sane_device_array_append(dev_list, info); dev_count ++; info->name = zeroconf_ident_make(name, device->devid, proto); info->vendor = str_dup(proto_name); info->model = str_dup(conf.model_is_netname ? name : model); //info->type = str_printf("%s network scanner", proto_name); type = str_printf("ip=", proto_name); type = ip_addrset_friendly_str(device->addrs, type); info->type = type; } } } qsort(dev_list + dev_count_static, dev_count - dev_count_static, sizeof(*dev_list), zeroconf_device_list_qsort_cmp); log_debug(zeroconf_log, "zeroconf_device_list_get: resulting list:"); for (i = 0; dev_list[i] != NULL; i ++) { log_debug(zeroconf_log, " %-4s \"%s\"", dev_list[i]->vendor, dev_list[i]->name); } return dev_list; } /* Free list of devices, returned by zeroconf_device_list_get() */ void zeroconf_device_list_free (const SANE_Device **dev_list) { if (dev_list != NULL) { unsigned int i; const SANE_Device *info; for (i = 0; (info = dev_list[i]) != NULL; i ++) { mem_free((void*) info->name); mem_free((void*) info->vendor); mem_free((void*) info->model); mem_free((void*) info->type); mem_free((void*) info); } sane_device_array_free(dev_list); } } /* * The format "protocol:name:url" is accepted to directly specify a device * without listing it in the config or finding it with autodiscovery. Try * to parse an identifier as that format. On success, returns a newly allocated * zeroconf_devinfo that the caller must free with zeroconf_devinfo_free(). On * failure, returns NULL. */ zeroconf_devinfo* zeroconf_parse_devinfo_from_ident(const char *ident) { int buf_size; char *buf = NULL; ID_PROTO proto; char *name; char *uri_str; http_uri *uri; zeroconf_devinfo *devinfo; if (ident == NULL) { return NULL; } /* Copy the string so we can modify it in place while parsing. */ buf_size = strlen(ident) + 1; buf = alloca(buf_size); memcpy(buf, ident, buf_size); name = strchr(buf, ':'); if (name == NULL) { return NULL; } *name = '\0'; name++; proto = id_proto_by_name(buf); if (proto == ID_PROTO_UNKNOWN) { return NULL; } uri_str = strchr(name, ':'); if (uri_str == NULL) { return NULL; } *uri_str = '\0'; uri_str++; if (*name == '\0') { return NULL; } uri = http_uri_new(uri_str, true); if (uri == NULL) { return NULL; } /* Build a zeroconf_devinfo */ devinfo = mem_new(zeroconf_devinfo, 1); devinfo->ident = str_dup(ident); devinfo->name = str_dup(name); devinfo->model = str_dup(""); devinfo->endpoints = zeroconf_endpoint_new(proto, uri); return devinfo; } /* Lookup device by ident (ident is reported as SANE_Device::name) * by zeroconf_device_list_get()) * * Caller becomes owner of resources (name and list of endpoints), * referred by the returned zeroconf_devinfo * * Caller must free these resources, using zeroconf_devinfo_free() */ zeroconf_devinfo* zeroconf_devinfo_lookup (const char *ident) { conf_device *dev_conf = NULL; zeroconf_device *device = NULL; zeroconf_devinfo *devinfo; ID_PROTO proto = ID_PROTO_UNKNOWN; /* Check if the caller passed a direct device specification first. */ devinfo = zeroconf_parse_devinfo_from_ident(ident); if (devinfo != NULL) { return devinfo; } /* Wait until device table is ready */ zeroconf_initscan_wait(); /* Lookup a device, static first */ dev_conf = zeroconf_find_static_by_ident(ident); if (dev_conf == NULL) { device = zeroconf_device_find_by_ident(ident, &proto); if (device == NULL) { return NULL; } } /* Build a zeroconf_devinfo */ devinfo = mem_new(zeroconf_devinfo, 1); devinfo->ident = str_dup(ident); if (dev_conf != NULL) { http_uri *uri = http_uri_clone(dev_conf->uri); devinfo->name = str_dup(dev_conf->name); devinfo->model = str_dup(""); devinfo->endpoints = zeroconf_endpoint_new(dev_conf->proto, uri); } else { devinfo->name = str_dup(zeroconf_device_name(device)); devinfo->model = str_dup(device->model ? device->model : ""); devinfo->endpoints = zeroconf_device_endpoints(device, proto); } return devinfo; } /* Free zeroconf_devinfo, returned by zeroconf_devinfo_lookup() */ void zeroconf_devinfo_free (zeroconf_devinfo *devinfo) { mem_free((char*) devinfo->ident); mem_free((char*) devinfo->name); mem_free((char*) devinfo->model); zeroconf_endpoint_list_free(devinfo->endpoints); mem_free(devinfo); } /******************** Initialization and cleanup *********************/ /* ZeroConf start/stop callback */ static void zeroconf_start_stop_callback (bool start) { if (start) { zeroconf_initscan_timer = eloop_timer_new(ZEROCONF_READY_TIMEOUT, zeroconf_initscan_timer_callback, NULL); } else { if (zeroconf_initscan_timer != NULL) { eloop_timer_cancel(zeroconf_initscan_timer); zeroconf_initscan_timer = NULL; } pthread_cond_broadcast(&zeroconf_initscan_cond); } } /* Initialize ZeroConf */ SANE_Status zeroconf_init (void) { char *s; conf_device *dev; /* Initialize zeroconf */ zeroconf_log = log_ctx_new("zeroconf", NULL); ll_init(&zeroconf_device_list); pthread_cond_init(&zeroconf_initscan_cond, NULL); if (conf.discovery) { zeroconf_initscan_bits = (1 << ZEROCONF_MDNS_HINT) | (1 << ZEROCONF_USCAN_TCP) | (1 << ZEROCONF_USCANS_TCP) | (1 << ZEROCONF_WSD); } eloop_add_start_stop_callback(zeroconf_start_stop_callback); /* Dump zeroconf configuration to the log */ log_trace(zeroconf_log, "zeroconf configuration:"); s = conf.discovery ? "enable" : "disable"; log_trace(zeroconf_log, " discovery = %s", s); s = conf.model_is_netname ? "network" : "hardware"; log_trace(zeroconf_log, " model = %s", s); s = conf.proto_auto ? "auto" : "manual"; 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; case WSDD_OFF: s = "OFF"; break; } log_trace(zeroconf_log, " ws-discovery = %s", s); if (conf.devices != NULL) { log_trace(zeroconf_log, "statically configured devices:"); for (dev = conf.devices; dev != NULL; dev = dev->next) { if (dev->uri != NULL) { log_trace(zeroconf_log, " %s = %s, %s", dev->name, http_uri_str(dev->uri), id_proto_name(dev->proto)); } else { log_trace(zeroconf_log, " %s = disable", dev->name); } } } if (conf.blacklist != NULL) { conf_blacklist *ent; log_trace(zeroconf_log, "blacklist:"); for (ent = conf.blacklist; ent != NULL; ent = ent->next) { if (ent->model != NULL) { log_trace(zeroconf_log, " model = %s", ent->model); } if (ent->name != NULL) { log_trace(zeroconf_log, " name = %s", ent->name); } if (ent->net.addr.af != AF_UNSPEC) { ip_straddr straddr = ip_network_to_straddr(ent->net); log_trace(zeroconf_log, " ip = %s", straddr.text); } } } return SANE_STATUS_GOOD; } /* Cleanup ZeroConf */ void zeroconf_cleanup (void) { if (zeroconf_log != NULL) { log_ctx_free(zeroconf_log); zeroconf_log = NULL; pthread_cond_destroy(&zeroconf_initscan_cond); } } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan.c000066400000000000000000000254221500411437100160320ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * SANE API */ #include "airscan.h" /* Static variables */ static const SANE_Device **sane_device_list; /* Initialize the backend */ SANE_Status sane_init (SANE_Int *version_code, SANE_Auth_Callback authorize) { SANE_Status status; if (version_code != NULL) { *version_code = SANE_VERSION_CODE (SANE_CURRENT_MAJOR, SANE_CURRENT_MINOR, 0); } (void) authorize; status = airscan_init(0, "API: sane_init(): called"); if (status == SANE_STATUS_GOOD) { status = device_management_init(); } if (status != SANE_STATUS_GOOD) { log_debug(NULL, "API: sane_init(): %s", sane_strstatus(status)); } return status; } /* Exit the backend */ void sane_exit (void) { log_debug(NULL, "API: sane_exit(): called"); eloop_thread_stop(); device_management_cleanup(); airscan_cleanup("API: sane_exit(): OK"); } /* Get list of devices */ SANE_Status sane_get_devices (const SANE_Device ***device_list, SANE_Bool local_only) { log_debug(NULL, "API: sane_get_devices(): called"); if (local_only && !conf.pretend_local) { /* Note, all our devices are non-local */ static const SANE_Device *empty_devlist[1] = {0}; *device_list = empty_devlist; } else { eloop_mutex_lock(); zeroconf_device_list_free(sane_device_list); sane_device_list = zeroconf_device_list_get(); *device_list = sane_device_list; eloop_mutex_unlock(); } log_debug(NULL, "API: sane_get_devices(): done"); return SANE_STATUS_GOOD; } /* Open the device */ SANE_Status sane_open (SANE_String_Const name, SANE_Handle *handle) { SANE_Status status; device *dev; const SANE_Device **dev_list = NULL; log_debug(NULL, "API: sane_open(\"%s\"): called", name ? name : ""); eloop_mutex_lock(); /* If name is not set, open the first device */ if (name == NULL || *name == '\0') { dev_list = zeroconf_device_list_get(); if (dev_list[0] != NULL) { name = dev_list[0]->name; } } dev = device_open(name, &status); eloop_mutex_unlock(); if (dev != NULL) { *handle = (SANE_Handle) dev; } log_debug(device_log_ctx(dev), "API: sane_open(\"%s\"): %s", name ? name : "", sane_strstatus(status)); zeroconf_device_list_free(dev_list); return status; } /* Close the device */ void sane_close (SANE_Handle handle) { device *dev = (device*) handle; log_debug(device_log_ctx(dev), "API: sane_close(): called"); eloop_mutex_lock(); device_close((device*) handle, "API: sane_close(): done"); eloop_mutex_unlock(); } /* Get option descriptor */ const SANE_Option_Descriptor * sane_get_option_descriptor (SANE_Handle handle, SANE_Int option) { device *dev = (device*) handle; log_ctx *log = device_log_ctx(dev); const SANE_Option_Descriptor *desc; log_debug(log, "API: device_get_option_descriptor(): called"); eloop_mutex_lock(); desc = device_get_option_descriptor(dev, option); eloop_mutex_unlock(); log_debug(log, "API: device_get_option_descriptor(): done"); return desc; } /* Write sane_control_option operation to the log */ static void sane_control_option_log (log_ctx *log, const SANE_Option_Descriptor *desc, SANE_Int option, SANE_Action action, void *value, SANE_Int info) { char vbuf[128]; char ibuf[128] = ""; bool get; switch (action) { case SANE_ACTION_GET_VALUE: get = true; break; case SANE_ACTION_SET_VALUE: get = false; break; case SANE_ACTION_SET_AUTO: default: /* Not supported here */ return; } switch (desc->type) { case SANE_TYPE_BOOL: strcpy(vbuf, *((SANE_Word*) value) ? "true" : "false"); break; case SANE_TYPE_INT: sprintf(vbuf, "%d", *((SANE_Word*) value)); break; case SANE_TYPE_FIXED: sprintf(vbuf, "%g", SANE_UNFIX(*((SANE_Fixed*) value))); break; case SANE_TYPE_STRING: snprintf(vbuf, sizeof(vbuf), "\"%s\"", (char*) value); break; case SANE_TYPE_BUTTON: case SANE_TYPE_GROUP: default: /* Not supported here */ return; } if (action == SANE_ACTION_SET_VALUE && info != 0) { strcat(ibuf, " info: "); if ((info & SANE_INFO_INEXACT) != 0) { strcat(ibuf, "inexact"); info &= ~SANE_INFO_INEXACT; if (info != 0) { strcat(ibuf, ", "); } } if ((info & (SANE_INFO_RELOAD_OPTIONS | SANE_INFO_RELOAD_PARAMS)) != 0) { strcat(ibuf, "reload:"); if ((info & SANE_INFO_RELOAD_OPTIONS) != 0) { strcat(ibuf, " options"); } if ((info & SANE_INFO_RELOAD_PARAMS) != 0) { strcat(ibuf, " params"); } } } log_debug(log, "API: %s %s: %s %s", get ? "get" : "set", option ? desc->name : "(0)", vbuf, ibuf); } /* Get or set option value */ SANE_Status sane_control_option (SANE_Handle handle, SANE_Int option, SANE_Action action, void *value, SANE_Int *info) { SANE_Status status = SANE_STATUS_INVAL; device *dev = (device*) handle; const SANE_Option_Descriptor *desc; log_ctx *log = device_log_ctx(dev); eloop_mutex_lock(); /* Roughly validate arguments */ if (dev == NULL || value == NULL) { goto DONE; } desc = device_get_option_descriptor(dev, option); if (desc == NULL) { goto DONE; } if (action == SANE_ACTION_SET_VALUE && !SANE_OPTION_IS_SETTABLE(desc->cap)){ goto DONE; } /* Get/set the option */ if (action == SANE_ACTION_GET_VALUE) { status = device_get_option(dev, option, value); } else { status = device_set_option(dev, option, value, info); } DONE: eloop_mutex_unlock(); if (status == SANE_STATUS_GOOD) { sane_control_option_log(log, desc, option, action, value, info ? *info : 0); } return status; } /* Get current scan parameters */ SANE_Status sane_get_parameters (SANE_Handle handle, SANE_Parameters *params) { SANE_Status status = SANE_STATUS_GOOD; device *dev = (device*) handle; log_ctx *log = device_log_ctx(dev); log_debug(log, "API: sane_get_params(): called"); if (params != NULL) { eloop_mutex_lock(); status = device_get_parameters(dev, params); eloop_mutex_unlock(); } log_debug(log, "API: sane_get_params(): done"); return status; } /* Start scanning operation */ SANE_Status sane_start (SANE_Handle handle) { SANE_Status status; device *dev = (device*) handle; log_ctx *log = device_log_ctx(dev); log_debug(log, "API: sane_start(): called"); eloop_mutex_lock(); status = device_start(dev); eloop_mutex_unlock(); log_debug(log, "API: sane_start(): %s", sane_strstatus(status)); return status; } /* Read scanned image */ SANE_Status sane_read (SANE_Handle handle, SANE_Byte *data, SANE_Int max_len, SANE_Int *len) { SANE_Status status; device *dev = (device*) handle; log_ctx *log = device_log_ctx(dev); eloop_mutex_lock(); status = device_read(dev, data, max_len, len); eloop_mutex_unlock(); /* Note, as a special exception, we don't log every successful * call of sane_read(), because during loading of image there * are a lot of them */ if (status != SANE_STATUS_GOOD) { log_debug(log, "API: sane_read(): %s", sane_strstatus(status)); } return status; } /* Cancel scanning operation */ void sane_cancel (SANE_Handle handle) { device *dev = handle; /* Note, no mutex lock here and no logging. We can be called from * signal handler. device_cancel() properly handles it */ device_cancel(dev); } /* Set I/O mode */ SANE_Status sane_set_io_mode (SANE_Handle handle, SANE_Bool non_blocking) { SANE_Status status; device *dev = (device*) handle; log_ctx *log = device_log_ctx(dev); const char *mode = non_blocking ? "true" : "false"; log_debug(log, "API: sane_set_io_mode(%s): called", mode); eloop_mutex_lock(); status = device_set_io_mode(dev, non_blocking); eloop_mutex_unlock(); log_debug(log, "API: sane_set_io_mode(%s): %s", mode, sane_strstatus(status)); return status; } /* Get select file descriptor */ SANE_Status sane_get_select_fd (SANE_Handle handle, SANE_Int *fd) { SANE_Status status; device *dev = (device*) handle; log_ctx *log = device_log_ctx(dev); log_debug(log, "API: sane_get_select_fd(): called"); eloop_mutex_lock(); status = device_get_select_fd(dev, fd); eloop_mutex_unlock(); if (status == SANE_STATUS_GOOD) { log_debug(log, "API: sane_get_select_fd(): fd = %d", *fd); } else { log_debug(log, "API: sane_get_select_fd(): %s", sane_strstatus(status)); } return status; } /******************** API aliases for libsane-dll ********************/ SANE_Status __attribute__ ((alias ("sane_init"))) sane_airscan_init (SANE_Int *version_code, SANE_Auth_Callback authorize); void __attribute__ ((alias ("sane_exit"))) sane_airscan_exit (void); SANE_Status __attribute__ ((alias ("sane_get_devices"))) sane_airscan_get_devices (const SANE_Device ***device_list, SANE_Bool local_only); SANE_Status __attribute__ ((alias ("sane_open"))) sane_airscan_open (SANE_String_Const devicename, SANE_Handle *handle); void __attribute__ ((alias ("sane_close"))) sane_airscan_close (SANE_Handle handle); const SANE_Option_Descriptor * __attribute__ ((alias ("sane_get_option_descriptor"))) sane_airscan_get_option_descriptor (SANE_Handle handle, SANE_Int option); SANE_Status __attribute__ ((alias ("sane_control_option"))) sane_airscan_control_option (SANE_Handle handle, SANE_Int option, SANE_Action action, void *value, SANE_Int *info); SANE_Status __attribute__ ((alias ("sane_get_parameters"))) sane_airscan_get_parameters (SANE_Handle handle, SANE_Parameters *params); SANE_Status __attribute__ ((alias ("sane_start"))) sane_airscan_start (SANE_Handle handle); SANE_Status __attribute__ ((alias ("sane_read"))) sane_airscan_read (SANE_Handle handle, SANE_Byte *data, SANE_Int max_length, SANE_Int *length); void __attribute__ ((alias ("sane_cancel"))) sane_airscan_cancel (SANE_Handle handle); SANE_Status __attribute__ ((alias ("sane_set_io_mode"))) sane_airscan_set_io_mode (SANE_Handle handle, SANE_Bool non_blocking); SANE_Status __attribute__ ((alias ("sane_get_select_fd"))) sane_airscan_get_select_fd (SANE_Handle handle, SANE_Int * fd); /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan.conf000066400000000000000000000100341500411437100165260ustar00rootroot00000000000000# sane-airscan example configuration file # You can add scanners manually, using the following syntax: # [devices] # name1 = url1 ; add eSCL device # name2 = url2, protocol ; protocol can be escl or wsd # name2 = disable ; disable the device # # The airscan-discover utility, typically shipped together with # driver, can help to discover scanners for manual # addition (see man airscan-discover for more details) [devices] #"Kyocera MFP Scanner" = http://192.168.1.102:9095/eSCL #"Some Unwanted Scanner" = disable # Various options # # Automatic discovery may be enabled (the default) or disabled: # discovery = enable ; Enable automatic device discovery # discovery = disable ; Disable both DNS-SD and WS-Discovery # # Protocol choice (eSCL vs WSD if both are available) # protocol = auto ; Best protocol is chosen automatically, the default # protocol = manual ; Manual choice is offered # # WS-Discovery mode # ws-discovery = fast ; Fast discovery, the default # ws-discovery = full ; Full discovery, slow and accurate # ws-discovery = off ; Disable WS-Discovery # # Scanner "model" is a string that most of SANE apps display in a list # of devices. It may be more convenient to use scanner network name # for this purpose: # model = network ; use network device name (default) # model = hardware ; use hardware model name # # socket_dir gives an optional path to a directory where local (UNIX) sockets # can be found. If an eSCL device's URL is in the form unix://socket/eSCL/, # traffic will be sent through socket_dir/socket instead of TCP. If not # specified, sockets will be searched for in /var/run. # # Proxy mode # pretend-local = false ; Remote scanners are marked as such (DEFAULT) # pretend-local = true ; Remote scanners are treated as if they were local # # The SANE network protocol destinguishes between locally attached devices, # and devices that can only be accessed over the network. While this is # useful information to have, it isn't always used consistently. In particular, # the saned daemon refuses to make any scanners available that aren't local # to the machine. While well-intentioned, there are use-cases where this is # unexpected; for instance in proxies that translate from eSCL/WSD protocols # to the SANE protocol. Setting this configuration options instructs # sane-airscan to treat all eSCL/WSD devices as if they were attached locally. [options] #discovery = enable #model = network #protocol = auto #ws-discovery = fast #socket_dir = /var/run #pretend-local = false # Configuration of debug facilities # trace = path ; enables protocol trace and configures output # ; directory. The directory will be created # ; automatically. Path may start with tilde (~) # ; character, which means user home directory # # enable = true|false ; enable or disable console logging # hexdump = true|false ; hex dump all traffic (very verbose!) [debug] #trace = ~/airscan/trace #enable = true #hexdump = false # Blacklisting devices # model = pattern ; Blacklist devices by model name # name = pattern ; Blacklist devices by network name # ip = addr[/mask] ; Blacklist devices by IP addresses # # Notes # In model and network names glob-style wildcards can be used # (i.e., model = "Xerox*") # # Network names come from DNS-SD, WS-Discovery doesn't provide this # information. For filtering by network name to work, Avahi must be # enabled and device must be discoverable via DNS-SD (not necessarily # as a scanner, it's enough if WSD scanner is discoverable as a printer # via DNS-SD). # # If netmask is not set, address assumed to be device address, # not address of the entire subnet # # Blacklisting only affects automatic discovery, and doesn't # affect manually configured devices [blacklist] #model = "Xerox*" ; blacklist by model name #name = "HP*" ; blacklist by network name #ip = 192.168.0.1 ; blacklist by address #ip = 192.168.0.0/24 ; blacklist the whole subnet sane-airscan-0.99.35/airscan.h000066400000000000000000003042511500411437100160370ustar00rootroot00000000000000/* AirScan (a.k.a. eSCL) backend for SANE * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions */ #ifndef airscan_h #define airscan_h #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __cplusplus extern "C" { #endif /******************** Static configuration ********************/ /* Configuration path in environment */ #define CONFIG_PATH_ENV "SANE_CONFIG_DIR" /* Standard SANE configuration directory */ #ifndef CONFIG_SANE_CONFIG_DIR # define CONFIG_SANE_CONFIG_DIR "/etc/sane.d/" #endif /* Sane-airscan configuration file and subdirectory names */ #define CONFIG_AIRSCAN_CONF "airscan.conf" #define CONFIG_AIRSCAN_D "airscan.d" /* Environment variables: * * CONFIG_ENV_AIRSCAN_DEBUG - if set to "true" or non-zero number, * enables writing debug messages to stderr * * SANE_AIRSCAN_DEVICE - allows to forcibly set the target device. * See sane-airscan(5) for detains. */ #define CONFIG_ENV_AIRSCAN_DEBUG "SANE_DEBUG_AIRSCAN" #define CONFIG_ENV_AIRSCAN_DEVICE "SANE_AIRSCAN_DEVICE" /* Default resolution, DPI */ #define CONFIG_DEFAULT_RESOLUTION 300 /* Minimal interval between subsequent sane_start() * attempts, if previous sane_start was failed */ #define CONFIG_START_RETRY_INTERVAL 2500 /* Default directory for AF_UNIX sockets */ #define CONFIG_DEFAULT_SOCKET_DIR "/var/run" /******************** Forward declarations ********************/ /* log_ctx represents logging context */ typedef struct log_ctx log_ctx; /* Type http_uri represents HTTP URI */ typedef struct http_uri http_uri; /******************** Utility macros ********************/ /* Obtain pointer to outer structure from pointer to * its known member */ #define OUTER_STRUCT(member_p,struct_t,field) \ ((struct_t*)((char*)(member_p) - ((ptrdiff_t) &(((struct_t*) 0)->field)))) /******************** Circular Linked Lists ********************/ /* ll_node represents a linked data node. * Data nodes are embedded into the corresponding data structures: * struct data { * ll_node chain; // Linked list chain * ... * }; * * Use OUTER_STRUCT() macro to obtain pointer to containing * structure from the pointer to the list node */ typedef struct ll_node ll_node; struct ll_node { ll_node *ll_prev, *ll_next; }; /* ll_head represents a linked list head node * ll_head must be initialized before use with ll_init() function */ typedef struct { ll_node node; } ll_head; /* Initialize list head */ static inline void ll_init (ll_head *head) { head->node.ll_next = head->node.ll_prev = &head->node; } /* Check if list is empty */ static inline bool ll_empty (const ll_head *head) { return head->node.ll_next == &head->node; } /* Push node to the end of the list, represented * by its head node */ static inline void ll_push_end (ll_head *head, ll_node *node) { node->ll_prev = head->node.ll_prev; node->ll_next = &head->node; head->node.ll_prev->ll_next = node; head->node.ll_prev = node; } /* Push node to the beginning of the list, represented * by its head node */ static inline void ll_push_beg (ll_head *head, ll_node *node) { node->ll_next = head->node.ll_next; node->ll_prev = &head->node; head->node.ll_next->ll_prev = node; head->node.ll_next = node; } /* Delete node from the list */ static inline void ll_del (ll_node *node) { 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; } /* Pop node from the beginning of the list. * Returns NULL if list is empty */ static inline ll_node* ll_pop_beg (ll_head *head) { ll_node *node, *next; node = head->node.ll_next; if (node == &head->node) { return NULL; /* List is empty if it is looped to itself */ } 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; } /* Pop node from the end of the list. * Returns NULL if list is empty */ static inline ll_node* ll_pop_end (ll_head *head) { ll_node *node, *prev; node = head->node.ll_prev; if (node == &head->node) { return NULL; /* List is empty if it is looped to itself */ } 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; } /* Get next (from the beginning to the end) node of * the list. Returns NULL, if end of list is reached */ static inline ll_node* ll_next (const ll_head *head, const ll_node *node) { ll_node *next = node->ll_next; return next == &head->node ? NULL : next; } /* Get previous (from the beginning to the end) node of * the list. Returns NULL, if end of list is reached */ static inline ll_node* ll_prev (const ll_head *head, const ll_node *node) { ll_node *prev = node->ll_prev; return prev == &head->node ? NULL : prev; } /* Get first node of the list. * Returns NULL if list is empty */ static inline ll_node* ll_first (const ll_head *head) { return ll_next(head, &head->node); } /* Get last node of the list. * Returns NULL if list is empty */ static inline ll_node* ll_last (const ll_head *head) { return ll_prev(head, &head->node); } /* Concatenate lists: * list1 += list2 * list2 = empty */ static inline void ll_cat (ll_head *list1, ll_head *list2) { if (ll_empty(list2)) { return; } list2->node.ll_prev->ll_next = &list1->node; list2->node.ll_next->ll_prev = list1->node.ll_prev; list1->node.ll_prev->ll_next = list2->node.ll_next; list1->node.ll_prev = list2->node.ll_prev; ll_init(list2); } /* Helper macro for list iteration. * Usage: * for (LL_FOR_EACH(node, list)) { * // do something with the node * } */ #define LL_FOR_EACH(node,list) \ node = ll_first(list); node != NULL; node = ll_next(list, node) /******************** Memory allocation ********************/ /* Allocate `len' elements of type T */ #define mem_new(T,len) ((T*) __mem_alloc(len, 0, sizeof(T), true)) /* Resize memory. The returned memory block has length of `len' and * capacity at least of `len' + `extra' * * If p is NULL, new memory block will be allocated. Otherwise, * existent memory block will be resized, new pointer is returned, * while old becomes invalid (similar to how realloc() works). * * This function never returns NULL, it panics in a case of * memory allocation error. */ #define mem_resize(p,len,extra) \ ((__typeof__(p)) __mem_resize(p,len,extra,sizeof(*p),true)) /* Try to resize memory. It works like mem_resize() but may * return NULL if memory allocation failed. */ #define mem_try_resize(p,len,extra) __mem_resize(p,len,extra,sizeof(*p),false) /* Truncate the memory block length, preserving its capacity */ void mem_trunc (void *p); /* Shrink the memory block length, preserving its capacity */ #define mem_shrink(p,len) __mem_shrink(p,len, sizeof(*p)) /* Free memory block, obtained from mem_new() or mem_resize() * `p' can be NULL */ void mem_free (void *p); /* Get memory block length/capacity, in bytes * For NULL pointer return 0 */ size_t mem_len_bytes (const void *p); size_t mem_cap_bytes (const void *p); /* Get memory block length/capacity, in elements * For NULL pointer return 0 */ #define mem_len(v) (mem_len_bytes(v) / sizeof(*v)) #define mem_cap(v) (mem_cap_bytes(v) / sizeof(*v)) /* Helper functions for memory allocation, don't use directly */ void* __attribute__ ((__warn_unused_result__)) __mem_alloc (size_t len, size_t extra, size_t elsize, bool must); void* __attribute__ ((__warn_unused_result__)) __mem_resize (void *p, size_t len, size_t cap, size_t elsize, bool must); void __mem_shrink (void *p, size_t len, size_t elsize); /******************** Strings ********************/ /* Create new string */ static inline char* str_new (void) { char *s = mem_resize((char*) NULL, 0, 1); *s = '\0'; return s; } /* Create new string as a copy of existent string */ static inline char* str_dup (const char *s1) { size_t len = strlen(s1); char *s = mem_resize((char*) NULL, len, 1); memcpy(s, s1, len + 1); return s; } /* Get string length in bytes, not including terminating '\0' */ static inline size_t str_len (const char *s) { return mem_len(s); } /* Create new string as a lowercase copy of existent string */ char* str_dup_tolower (const char *s1); /* Create new string and print to it */ char* str_printf (const char *format, ...); /* Create new string and print to it, va_list version */ char* str_vprintf (const char *format, va_list ap); /* Truncate the string */ static inline void str_trunc (char *s) { mem_trunc(s); *s = '\0'; } /* Resize the string * * s1 must be previously created by some of str_XXX functions, * s1 will be consumed and the new pointer will be returned */ static inline char* str_resize (char *s, size_t len) { s = mem_resize(s, len, 1); s[len] = '\0'; return s; } /* Append memory to string: * s1 += s2[:l2] * * s1 must be previously created by some of str_XXX functions, * s1 will be consumed and the new pointer will be returned */ static inline char* str_append_mem (char *s1, const char *s2, size_t l2) { size_t l1 = str_len(s1); s1 = mem_resize(s1, l1 + l2, 1); memcpy(s1 + l1, s2, l2); s1[l1+l2] = '\0'; return s1; } /* Append string to string: * s1 += s2 * * s1 must be previously created by some of str_XXX functions, * s1 will be consumed and the new pointer will be returned */ static inline char* str_append (char *s1, const char *s2) { return str_append_mem(s1, s2, strlen(s2)); } /* Append character to string: * s1 += c * * `s' must be previously created by some of str_XXX functions, * `s' will be consumed and the new pointer will be returned */ static inline char* str_append_c (char *s, char c) { return str_append_mem(s, &c, 1); } /* Append formatted string to string * * `s' must be previously created by some of str_XXX functions, * `s' will be consumed and the new pointer will be returned */ char* str_append_printf (char *s, const char *format, ...); /* Append formatted string to string -- va_list version */ char* str_append_vprintf (char *s, const char *format, va_list ap); /* Assign value to string * * `s1' must be previously created by some of str_XXX functions, * `s1' will be consumed and the new pointer will be returned */ static inline char* str_assign (char *s1, const char *s2) { mem_trunc(s1); return str_append(s1, s2); } /* Concatenate several strings. Last pointer must be NULL. * The returned pointer must be eventually freed by mem_free */ char* str_concat (const char *s, ...); /* Make sure that string is terminated with the `c' character: * if string is not empty and the last character is not `c`, * append `c' to the string * * `s' must be previously created by some of str_XXX functions, * `s' will be consumed and the new pointer will be returned */ static inline char* str_terminate (char *s, char c) { if (s[0] != '\0' && s[str_len(s) - 1] != c) { s = str_append_c(s, c); } return s; } /* Check if string has a specified prefix */ bool str_has_prefix (const char *s, const char *prefix); /* Check if string has a specified suffix */ bool str_has_suffix (const char *s, const char *suffix); /* Remove leading and trailing white space. * This function modifies string in place, and returns pointer * to original string, for convenience */ char* str_trim (char *s); /******************** NULL-terminated pointer arrays ********************/ /* Create NULL-terminated array of pointers of type *T */ #define ptr_array_new(T) mem_resize((T*) NULL, 0, 1) /* Append pointer to the NULL-terminated array of pointers. * Returns new, potentially reallocated array */ #define ptr_array_append(a,p) \ ((__typeof__(a)) __ptr_array_append((void**)a, p)) /* Truncate NULL-terminated array of pointers */ #define ptr_array_trunc(a) \ do { \ mem_trunc(a); \ a[0] = NULL; \ } while(0) /* Find pointer within array of pointers. * Return non-negative index if pointer was found, -1 otherwise */ #define ptr_array_find(a,p) __ptr_array_find((void**) a, p) /* Delete element at given index. * Returns value of deleted pointer or NULL, if index is out of range */ #define ptr_array_del(a,i) \ ((__typeof__(*a)) __ptr_array_del((void**) a, i)) /* Helper function for ptr_array_append, don't use directly */ static inline void** __ptr_array_append (void **a, void *p) { size_t len = mem_len(a) + 1; a = mem_resize(a, len, 1); a[len - 1] = p; a[len] = NULL; return a; } /* Helper function for ptr_array_find, don't use directly */ static inline int __ptr_array_find (void **a, void *p) { size_t len = mem_len(a), i; for (i = 0; i < len; i ++) { if (a[i] == p) { return (int) i; } } return -1; } /* Helper function for ptr_array_del, don't use directly */ static inline void* __ptr_array_del (void **a, int i) { size_t len = mem_len(a); void *p; if (i < 0 || i >= (int) len) { return NULL; } len --; p = a[i]; memmove(&a[i], &a[i + 1], sizeof(void*) * (len - i)); mem_shrink(a, len); a[len] = NULL; return p; } /******************** Safe ctype macros ********************/ #define safe_isspace(c) isspace((unsigned char) c) #define safe_isxdigit(c) isxdigit((unsigned char) c) #define safe_iscntrl(c) iscntrl((unsigned char) c) #define safe_isprint(c) isprint((unsigned char) c) #define safe_toupper(c) toupper((unsigned char) c) #define safe_tolower(c) tolower((unsigned char) c) /******************** OS Facilities ********************/ /* The following macros, if defined, indicate that OS * has a particular features: * * OS_HAVE_EVENTFD - Linux-like eventfd (2) * OS_HAVE_RTNETLINK - Linux-like rtnetlink (7) * OS_HAVE_AF_ROUTE - BSD-like AF_ROUTE * OS_HAVE_LINUX_PROCFS - Linux-style procfs * OS_HAVE_IP_MREQN - OS defines struct ip_mreqn * OS_HAVE_ENDIAN_H - #include works * OS_HAVE_SYS_ENDIAN_H - #include works */ #ifdef __linux__ # define OS_HAVE_EVENTFD 1 # define OS_HAVE_RTNETLINK 1 # define OS_HAVE_LINUX_PROCFS 1 # define OS_HAVE_IP_MREQN 1 # define OS_HAVE_ENDIAN_H 1 #endif #ifdef BSD # define OS_HAVE_AF_ROUTE 1 # ifdef __FreeBSD__ # define OS_HAVE_SYS_ENDIAN_H 1 # else # define OS_HAVE_ENDIAN_H 1 # endif #endif /* Get user's home directory. There is no need to * free the returned string * * May return NULL in a case of error */ const char * os_homedir (void); /* Get base name of the calling program. * There is no need to free the returned string * * May return NULL in a case of error */ const char* os_progname (void); /* Make directory with parents */ int os_mkdir (const char *path, mode_t mode); /******************** Error handling ********************/ /* Type error represents an error. Its value either NULL, * which indicates "no error" condition, or some opaque * non-null pointer, which can be converted to string * with textual description of the error, using the ESTRING() * function * * Caller should not attempt to free the memory, referred * by error or string, obtained from an error using the * ESTRING() function */ typedef struct error_s *error; /* Standard errors */ extern error ERROR_ENOMEM; /* Construct error from a string */ static inline error ERROR (const char *s) { return (error) s; } /* Obtain textual representation of the error */ static inline const char* ESTRING (error err) { return (const char*) err; } /******************** Various identifiers ********************/ /* ID_PROTO represents protocol identifier */ typedef enum { ID_PROTO_UNKNOWN = -1, ID_PROTO_ESCL, ID_PROTO_WSD, NUM_ID_PROTO } ID_PROTO; /* id_proto_name returns protocol name * For unknown ID returns NULL */ const char* id_proto_name (ID_PROTO proto); /* id_proto_by_name returns protocol identifier by name * For unknown name returns ID_PROTO_UNKNOWN */ ID_PROTO id_proto_by_name (const char* name); /* ID_SOURCE represents scanning source */ typedef enum { ID_SOURCE_UNKNOWN = -1, ID_SOURCE_PLATEN, ID_SOURCE_ADF_SIMPLEX, ID_SOURCE_ADF_DUPLEX, NUM_ID_SOURCE } ID_SOURCE; /* id_source_sane_name returns SANE name for the source * For unknown ID returns NULL */ const char* id_source_sane_name (ID_SOURCE id); /* id_source_by_sane_name returns ID_SOURCE by its SANE name * For unknown name returns ID_SOURCE_UNKNOWN */ ID_SOURCE id_source_by_sane_name (const char *name); /* ID_JUSTIFICATION represents hardware-defined ADF justification * This value exposed to the SANE API as a couple of read-only * options, separate for width and height justification. * Not all scanners provide this information */ typedef enum { ID_JUSTIFICATION_UNKNOWN = -1, ID_JUSTIFICATION_LEFT, ID_JUSTIFICATION_CENTER, ID_JUSTIFICATION_RIGHT, ID_JUSTIFICATION_TOP, ID_JUSTIFICATION_BOTTOM, NUM_ID_JUSTIFICATION } ID_JUSTIFICATION; /* id_justification_sane_name returns SANE name for the width justification * For unknown ID returns NULL */ const char* id_justification_sane_name (ID_JUSTIFICATION id); /* ID_COLORMODE represents color mode */ typedef enum { ID_COLORMODE_UNKNOWN = -1, ID_COLORMODE_COLOR, ID_COLORMODE_GRAYSCALE, ID_COLORMODE_BW1, NUM_ID_COLORMODE } ID_COLORMODE; /* id_colormode_sane_name returns SANE name for the color mode * For unknown ID returns NULL */ const char* id_colormode_sane_name (ID_COLORMODE id); /* id_colormode_by_sane_name returns ID_COLORMODE by its SANE name * For unknown name returns ID_COLORMODE_UNKNOWN */ ID_COLORMODE id_colormode_by_sane_name (const char *name); /* ID_FORMAT represents image format */ typedef enum { ID_FORMAT_UNKNOWN = -1, ID_FORMAT_JPEG, ID_FORMAT_TIFF, ID_FORMAT_PNG, ID_FORMAT_PDF, ID_FORMAT_BMP, NUM_ID_FORMAT } ID_FORMAT; /* id_format_mime_name returns MIME name for the image format */ const char* id_format_mime_name (ID_FORMAT id); /* id_format_by_mime_name returns ID_FORMAT by its MIME name * For unknown name returns ID_FORMAT_UNKNOWN */ ID_FORMAT id_format_by_mime_name (const char *name); /* if_format_short_name returns short name for ID_FORMAT */ const char* id_format_short_name (ID_FORMAT id); /* ID_SCANINTENT represents scan intent * * Intent hints scanner on a purpose of requested scan, which may * imply carious parameters tweaks depending on that purpose. * * Intent maps to the eSCL Intent (see Mopria eSCL Technical Specification, 5) * and WSD ContentType. The semantics of these two parameters looks very * similar. * * Please note, eSCL defines also the ContentType parameter, but after * some thinking and discussion we came to conclusion that Intent better * maps our need. * * Dee discussion at: https://github.com/alexpevzner/sane-airscan/pull/351 */ typedef enum { ID_SCANINTENT_UNKNOWN = -1, ID_SCANINTENT_UNSET, /* Intent is not set */ ID_SCANINTENT_AUTO, /* WSD: Auto */ ID_SCANINTENT_DOCUMENT, /* eSCL: Docoment, WSD: Text */ ID_SCANINTENT_TEXTANDGRAPHIC, /* eSCL: TextAndGraphic, WSD: Mixed */ ID_SCANINTENT_PHOTO, /* eSCL: Photo, WSD: Photo */ ID_SCANINTENT_PREVIEW, /* eSCL: Preview */ ID_SCANINTENT_OBJECT, /* eSCL: Objects (3d scan) */ ID_SCANINTENT_BUSINESSCARD, /* eSCL: BusinessCard */ ID_SCANINTENT_HALFTONE, /* WSD: Halftone */ NUM_ID_SCANINTENT } ID_SCANINTENT; /* id_scanintent_sane_name returns SANE name for the scan intents * For unknown ID returns NULL */ const char* id_scanintent_sane_name (ID_SCANINTENT id); /* id_scanintent_by_sane_name returns ID_SCANINTENT by its SANE name * For unknown name returns ID_SCANINTENT_UNKNOWN */ ID_SCANINTENT id_scanintent_by_sane_name (const char *name); /******************** Device ID ********************/ /* Allocate unique device ID */ unsigned int devid_alloc (void); /* Free device ID */ void devid_free (unsigned int id); /* Restart device ID allocation counter. * Note, it doesn't free already allocated IDs, only restarts * counter from the beginning. */ void devid_restart (void); /* Initialize device ID allocator */ void devid_init (void); /******************** Random bytes ********************/ /* Get N random bytes */ void rand_bytes (void *buf, size_t n); /* Initialize random bytes generator */ SANE_Status rand_init (void); /* Cleanup random bytes generator */ void rand_cleanup (void); /******************** UUID utilities ********************/ /* Type uuid represents a random UUID string. * * It is wrapped into struct, so it can be returned * by value, without need to mess with memory allocation */ typedef struct { char text[sizeof("urn:uuid:ede05377-460e-4b4a-a5c0-423f9e02e8fa")]; } uuid; /* Check if uuid is valid */ static inline bool uuid_valid (uuid u) { return u.text[0] != '\0'; } /* Generate random UUID. Generated UUID has a following form: * urn:uuid:ede05377-460e-4b4a-a5c0-423f9e02e8fa */ uuid uuid_rand (void); /* Parse UUID. This function ignores all "decorations", like * urn:uuid: prefix and so on, and takes only hexadecimal digits * into considerations * * Check the returned uuid with uuid_valid() for possible parse errors */ uuid uuid_parse (const char *in); /* Generate uuid by cryptographically cacheing input string */ uuid uuid_hash (const char *s); /* Compare two uuids */ static inline bool uuid_equal (uuid u1, uuid u2) { return !strcmp(u1.text, u2.text); } /******************** Generic .INI file parser ********************/ /* Types of .INI file records */ typedef enum { INIFILE_SECTION, /* The [section name] string */ INIFILE_VARIABLE, /* The variable = value string */ INIFILE_COMMAND, /* command param1 param2 ... */ INIFILE_SYNTAX /* The syntax error */ } INIFILE_RECORD; /* .INI file record */ typedef struct { INIFILE_RECORD type; /* Record type */ const char *section; /* Section name */ const char *variable; /* Variable name */ const char *value; /* Variable value */ const char **tokv; /* Value split to tokens */ unsigned int tokc; /* Count of strings in tokv */ const char *file; /* File name */ unsigned int line; /* File line */ } inifile_record; /* .INI file (opaque) */ typedef struct { const char *file; /* File name */ unsigned int line; /* File handle */ FILE *fp; /* File pointer */ bool tk_open; /* Token is currently open */ char *tk_buffer; /* Parser buffer, tokenized */ unsigned int *tk_offsets; /* Tokens offsets */ unsigned int tk_count; /* Tokens count */ char *buffer; /* Parser buffer */ char *section; /* Section name string */ char *variable; /* Variable name string */ char *value; /* Value string */ inifile_record record; /* Record buffer */ } inifile; /* Open the .INI file */ inifile* inifile_open (const char *name); /* Close the .INI file */ void inifile_close (inifile *file); /* Read next record */ const inifile_record* inifile_read (inifile *file); /* Match name of section of variable * - match is case-insensitive * - difference in amount of free space is ignored * - leading and trailing space is ignored */ bool inifile_match_name (const char *n1, const char *n2); /******************** Utility functions for IP addresses ********************/ /* Address string, wrapped into structure so can * be passed by value */ typedef struct { /* Holds sun_path from sockaddr_un plus a null byte. */ char text[109]; } ip_straddr; /* Format ip_straddr from IP address (struct in_addr or struct in6_addr) * af must be AF_INET or AF_INET6 */ ip_straddr ip_straddr_from_ip (int af, const void *addr); /* Format ip_straddr from struct sockaddr. * AF_INET, AF_INET6, and AF_UNIX are supported * * If `withzone' is true, zone suffix will be appended, when appropriate */ ip_straddr ip_straddr_from_sockaddr(const struct sockaddr *addr, bool withzone); /* Format ip_straddr from struct sockaddr. * AF_INET, AF_INET6, and AF_UNIX are supported * * Port will not be appended, if it matches provided default port * * If `withzone' is true, zone suffix will be appended, when appropriate * * If `withlocalhost` is true and address is 127.0.0.1 or ::1, * "localhost" will be used instead of the IP address literal */ ip_straddr ip_straddr_from_sockaddr_dport (const struct sockaddr *addr, int dport, bool withzone, bool withlocalhost); /* Check if address is link-local * af must be AF_INET or AF_INET6 */ bool ip_is_linklocal (int af, const void *addr); /* Check if sockaddr is link-local */ bool ip_sockaddr_is_linklocal (const struct sockaddr *addr); /* Check if address is loopback * af must be AF_INET or AF_INET6 */ bool ip_is_loopback (int af, const void *addr); /* ip_addr represents IPv4 or IPv6 address */ typedef struct { int af; /* AF_INET or AF_INET6 */ int ifindex; /* For IPv6 link-local addresses */ union { struct in_addr v4; /* IPv4 address */ struct in6_addr v6; /* IPv4 address */ } ip; } ip_addr; /* Make ip_addr */ static inline ip_addr ip_addr_make (int ifindex, int af, const void *addr) { ip_addr ip_addr; memset(&ip_addr, 0, sizeof(ip_addr)); ip_addr.af = af; switch (ip_addr.af) { case AF_INET: memcpy(&ip_addr.ip.v4, addr, 4); break; case AF_INET6: memcpy(&ip_addr.ip, addr, 16); if (ip_is_linklocal(AF_INET6, &ip_addr.ip.v6)) { ip_addr.ifindex = ifindex; } break; } return ip_addr; } /* Extract ip_addr from sockaddr */ static inline ip_addr ip_addr_from_sockaddr (const struct sockaddr *sockaddr) { ip_addr addr; memset(&addr, 0, sizeof(addr)); addr.af = sockaddr->sa_family; switch (addr.af) { case AF_INET: addr.ip.v4 = ((struct sockaddr_in*) sockaddr)->sin_addr; break; case AF_INET6: addr.ip.v6 = ((struct sockaddr_in6*) sockaddr)->sin6_addr; if (ip_is_linklocal(AF_INET6, &addr.ip.v6)) { addr.ifindex = ((struct sockaddr_in6*) sockaddr)->sin6_scope_id; } break; } return addr; } /* Format ip_addr into ip_straddr */ ip_straddr ip_addr_to_straddr (ip_addr addr, bool withzone); /* Check if two addresses are equal */ static inline bool ip_addr_equal (ip_addr a1, ip_addr a2) { if (a1.af != a2.af) { return false; } switch (a1.af) { case AF_INET: return a1.ip.v4.s_addr == a2.ip.v4.s_addr; case AF_INET6: return a1.ifindex == a2.ifindex && !memcmp(a1.ip.v6.s6_addr, a2.ip.v6.s6_addr, 16); } return false; } /* ip_network represents IPv4 or IPv6 network (i.e., address with mask) */ typedef struct { ip_addr addr; /* Network address */ int mask; /* Network mask */ } ip_network; /* Format ip_network into ip_straddr */ ip_straddr ip_network_to_straddr (ip_network net); /* Check if ip_network contains ip_addr */ bool ip_network_contains (ip_network net, ip_addr addr); /* ip_addr_set represents a set of IP addresses */ typedef struct ip_addrset ip_addrset; /* Create new ip_addrset */ ip_addrset* ip_addrset_new (void); /* Free ip_addrset */ void ip_addrset_free (ip_addrset *addrset); /* Check if address is in set */ bool ip_addrset_lookup (const ip_addrset *addrset, ip_addr addr); /* Add address to the set. Returns true, if address was * actually added, false if it was already in the set */ bool ip_addrset_add (ip_addrset *addrset, ip_addr addr); /* Add address to the set without checking for duplicates */ void ip_addrset_add_unsafe (ip_addrset *addrset, ip_addr addr); /* Del address from the set. */ void ip_addrset_del (ip_addrset *addrset, ip_addr addr); /* Delete all addresses from the set */ void ip_addrset_purge (ip_addrset *addrset); /* Merge two sets: * addrset += addrset2 */ void ip_addrset_merge (ip_addrset *addrset, const ip_addrset *addrset2); /* Get access to array of addresses in the set */ const ip_addr* ip_addrset_addresses (const ip_addrset *addrset, size_t *count); /* Check if two address sets are intersecting */ bool ip_addrset_is_intersect (const ip_addrset *set, const ip_addrset *set2); /* Check if some of addresses in the address set is on the * given network */ bool ip_addrset_on_network (const ip_addrset *set, ip_network net); /* Check if address set has some addresses of the specified * address family */ bool ip_addrset_has_af (const ip_addrset *set, int af); /* Create user-friendly string out of set of addresses, containing * in the ip_addrset: * * addresses are sorted, IP4 addresses goes first * * link-local addresses are skipped, if there are non-link-local ones * * Caller must use mem_free to release the returned string when * it is not needed anymore */ char* ip_addrset_friendly_str (const ip_addrset *set, char *s); /******************** Network interfaces addresses ********************/ /* Network interface name, wrapped into structure, so * it can be passed by value */ typedef struct { char text[32]; } netif_name; /* Network interface address */ typedef struct netif_addr netif_addr; struct netif_addr { netif_addr *next; /* Next address in the list */ int ifindex; /* Interface index */ netif_name ifname; /* Interface name, for logging */ bool ipv6; /* This is an IPv6 address */ void *data; /* Placeholder for user data */ char straddr[64]; /* Address string */ union { struct in_addr v4; /* IPv4 address */ struct in6_addr v6; /* IPv6 address */ } ip; }; /* NETIF_DISTANCE represents a distance to the target address */ typedef enum { NETIF_DISTANCE_LOOPBACK, /* Target address is host's local address */ NETIF_DISTANCE_DIRECT, /* Target is on a local network */ NETIF_DISTANCE_ROUTED /* Target is behind a router */ } NETIF_DISTANCE; /* Get distance to the target address */ 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 * 0 if distance is equal */ static inline int netif_distance_cmp (const struct sockaddr *addr1, const struct sockaddr *addr2) { int d1 = (int) netif_distance_get(addr1); int d2 = (int) netif_distance_get(addr2); return d1 - d2; } /* Get list of network interfaces addresses * The returned list is sorted */ netif_addr* netif_addr_list_get (void); /* Free list of network interfaces addresses */ void netif_addr_list_free (netif_addr *list); /* netif_diff represents a difference between two * lists of network interface addresses */ typedef struct { netif_addr *added, *removed; /* What was added/removed */ netif_addr *preserved; } netif_diff; /* Compute a difference between two lists of addresses. * * It works by tossing nodes between 3 output lists: * * if node is present in list2 only, it is moved * to netif_diff.added * * if node is present in list1 only, it is moved * to netif_diff.removed * * if node is present in both lists, node from * list1 is moved to preserved, and node from * list2 is released * * It assumes, both lists are sorted, as returned * by netif_addr_get(). Returned lists are also sorted */ netif_diff netif_diff_compute (netif_addr *list1, netif_addr *list2); /* Merge two lists of addresses * * Input lists are consumed and new list is created. * * Input lists are assumed to be sorted, and output * list will be sorted as well */ netif_addr* netif_addr_list_merge (netif_addr *list1, netif_addr *list2); /* Network interfaces addresses change notifier */ typedef struct netif_notifier netif_notifier; /* Create netif_notifier */ netif_notifier* netif_notifier_create (void (*callback) (void*), void *data); /* Destroy netif_notifier */ void netif_notifier_free (netif_notifier *notifier); /* Initialize network interfaces monitoring */ SANE_Status netif_init (void); /* Cleanup network interfaces monitoring */ void netif_cleanup (void); /******************** Configuration file loader ********************/ /* Device URI for manually disabled device */ #define CONF_DEVICE_DISABLE "disable" /* Device configuration, for manually added devices */ typedef struct conf_device conf_device; struct conf_device { unsigned int devid; /* Device ident */ const char *name; /* Device name */ ID_PROTO proto; /* Protocol to use */ http_uri *uri; /* Device URI, parsed; NULL if device disabled */ conf_device *next; /* Next device in the list */ }; /* WSDD_MODE represents WS-Discovery mode */ typedef enum { WSDD_FAST, /* Use hints from DNS-SD to speed up WSDD */ WSDD_FULL, /* Full discovery, slow and fair */ WSDD_OFF /* Disable WSDD */ } WSDD_MODE; /* Device blacklist entry */ typedef struct conf_blacklist conf_blacklist; struct conf_blacklist { const char *model; /* If not NULL, match by model */ const char *name; /* If not NULL, match by network name */ ip_network net; /* if net.addr.af != AF_UNSPEC, match by net */ conf_blacklist *next; /* Next entry in the list */ }; /* Backend configuration */ typedef struct { bool dbg_enabled; /* Debugging enabled */ const char *dbg_trace; /* Trace directory */ bool dbg_hexdump; /* Hexdump all traffic to the trace */ conf_device *devices; /* Manually configured devices */ bool discovery; /* Scanners discovery enabled */ bool model_is_netname; /* Use network name instead of model */ bool proto_auto; /* Auto protocol selection */ WSDD_MODE wsdd_mode; /* WS-Discovery mode */ const char *socket_dir; /* Directory for AF_UNIX sockets */ conf_blacklist *blacklist; /* Devices blacklisted for discovery */ bool pretend_local; /* Pretend devices are local */ } conf_data; #define CONF_INIT { \ .dbg_enabled = false, \ .dbg_trace = NULL, \ .dbg_hexdump = false, \ .devices = NULL, \ .discovery = true, \ .model_is_netname = true, \ .proto_auto = true, \ .wsdd_mode = WSDD_FAST, \ .socket_dir = NULL, \ .pretend_local = false \ } extern conf_data conf; /* Load configuration. It updates content of a global conf variable */ void conf_load (void); /* Free resources, allocated by conf_load, and reset configuration * data into initial state */ void conf_unload (void); /******************** Pollable events ********************/ /* The pollable event * * Pollable events allow to wait until some event happens * and can be used in combination with select()/poll() * system calls */ typedef struct pollable pollable; /* Create new pollable event */ pollable* pollable_new (void); /* Free pollable event */ void pollable_free (pollable *p); /* Get file descriptor for poll()/select(). * * When pollable event becomes "ready", this file descriptor * becomes readable from the select/poll point of view */ int pollable_get_fd (pollable *p); /* Make pollable event "ready" */ void pollable_signal (pollable *p); /* Make pollable event "not ready" */ void pollable_reset (pollable *p); /* Wait until pollable event is ready */ void pollable_wait (pollable *p); /******************** Time stamps ********************/ /* timestamp represents a monotonic time, in milliseconds */ typedef int64_t timestamp; /* timestamp_now() returns a current time as timestamp */ static inline timestamp timestamp_now (void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return (timestamp) t.tv_sec * 1000 + (timestamp) t.tv_nsec / 1000000; } /******************** Event loop ********************/ /* Initialize event loop */ SANE_Status eloop_init (void); /* Cleanup event loop */ void eloop_cleanup (void); /* Add start/stop callback. This callback is called * on a event loop thread context, once when event * loop is started, and second time when it is stopped * * Start callbacks are called in the same order as * they were added. Stop callbacks are called in a * reverse order */ void eloop_add_start_stop_callback (void (*callback) (bool start)); /* Start event loop thread. */ void eloop_thread_start (void); /* Stop event loop thread and wait until its termination */ void eloop_thread_stop (void); /* Acquire event loop mutex */ void eloop_mutex_lock (void); /* Release event loop mutex */ void eloop_mutex_unlock (void); /* Wait on conditional variable under the event loop mutex */ void eloop_cond_wait (pthread_cond_t *cond); /* Get AvahiPoll that runs in event loop thread */ const AvahiPoll* eloop_poll_get (void); /* ELOOP_CALL_BADID is the invalid callid which will never be returned by * the eloop_call(). * * It is safe to use ELOOP_CALL_BADID as parameter to eloop_call_cancel(). * Calling eloop_call_cancel(ELOOP_CALL_BADID) is guaranteed to do nothing. */ #define ELOOP_CALL_BADID (~(uint64_t) 0) /* Call function on a context of event loop thread * The returned value can be supplied as a `callid' * parameter for the eloop_call_cancel() function */ uint64_t eloop_call (void (*func)(void*), void *data); /* Cancel pending eloop_call * * This is safe to cancel already finished call (at this * case nothing will happen) */ void eloop_call_cancel (uint64_t callid); /* Event notifier. Calls user-defined function on a context * of event loop thread, when event is triggered. This is * safe to trigger the event from a context of any thread * or even from a signal handler */ typedef struct eloop_event eloop_event; /* Create new event notifier. May return NULL */ eloop_event* eloop_event_new (void (*callback)(void *), void *data); /* Destroy event notifier */ void eloop_event_free (eloop_event *event); /* Trigger an event */ void eloop_event_trigger (eloop_event *event); /* Timer. Calls user-defined function after a specified * interval */ typedef struct eloop_timer eloop_timer; /* Create new timer. Timeout is in milliseconds */ eloop_timer* eloop_timer_new (int timeout, void (*callback)(void *), void *data); /* Cancel a timer * * Caller SHOULD NOT cancel expired timer (timer with called * callback) -- this is done automatically */ void eloop_timer_cancel (eloop_timer *timer); /* eloop_fdpoll notifies user when file becomes * readable, writable or both, depending on its * event mask */ typedef struct eloop_fdpoll eloop_fdpoll; /* Mask of file events user interested in */ typedef enum { ELOOP_FDPOLL_READ = (1 << 0), ELOOP_FDPOLL_WRITE = (1 << 1), ELOOP_FDPOLL_BOTH = ELOOP_FDPOLL_READ | ELOOP_FDPOLL_WRITE } ELOOP_FDPOLL_MASK; /* Convert ELOOP_FDPOLL_MASK to string. Used for logging. */ const char* eloop_fdpoll_mask_str (ELOOP_FDPOLL_MASK mask); /* Create eloop_fdpoll * * Callback will be called, when file will be ready for read/write/both, * depending on mask * * Initial mask value is 0, and it can be changed, using * eloop_fdpoll_set_mask() function */ eloop_fdpoll* eloop_fdpoll_new (int fd, void (*callback) (int, void*, ELOOP_FDPOLL_MASK), void *data); /* Destroy eloop_fdpoll */ void eloop_fdpoll_free (eloop_fdpoll *fdpoll); /* Set eloop_fdpoll event mask. It returns a previous value of event mask */ ELOOP_FDPOLL_MASK eloop_fdpoll_set_mask (eloop_fdpoll *fdpoll, ELOOP_FDPOLL_MASK mask); /* Format error string, as printf() does and save result * in the memory, owned by the event loop * * Caller should not free returned string. This is safe * to use the returned string as an argument to the * subsequent eloop_eprintf() call. * * The returned string remains valid until next call * to eloop_eprintf(), which makes it usable to * report errors up by the stack. However, it should * not be assumed, that the string will remain valid * on a next eloop roll, so don't save this string * anywhere, if you need to do so, create a copy! */ error eloop_eprintf(const char *fmt, ...); /******************** HTTP Client ********************/ /* Create new URI, by parsing URI string */ http_uri* http_uri_new (const char *str, bool strip_fragment); /* Clone an URI */ http_uri* http_uri_clone (const http_uri *old); /* Create URI, relative to base URI. If `path_only' is * true, scheme, host and port are taken from the * base URI */ http_uri* http_uri_new_relative (const http_uri *base, const char *path, bool strip_fragment, bool path_only); /* Free the URI */ void http_uri_free (http_uri *uri); /* Get URI string */ const char* http_uri_str (http_uri *uri); /* Get URI's host address. If Host address is not literal, returns NULL */ const struct sockaddr* http_uri_addr (const http_uri *uri); /* Get URI's address family. May return AF_UNSPEC, * if host address is not literal */ static inline int http_uri_af (const http_uri *uri) { const struct sockaddr *addr = http_uri_addr(uri); return addr ? addr->sa_family : AF_UNSPEC; } /* Tell if URI host is literal IP address */ static inline bool http_uri_is_literal (const http_uri *uri) { return http_uri_addr(uri) != NULL; } /* Tell if URI IP address is loopback */ static inline bool http_uri_is_loopback (const http_uri *uri) { const struct sockaddr *addr = http_uri_addr(uri); const void *ip = NULL; if (addr == NULL) { return false; } switch (addr->sa_family) { case AF_INET: ip = &(((struct sockaddr_in*) addr)->sin_addr); break; case AF_INET6: ip = &(((struct sockaddr_in6*) addr)->sin6_addr); break; } if (ip != NULL) { return ip_is_loopback(addr->sa_family, ip); } return false; } /* Get URI path * * Note, if URL has empty path (i.e., "http://1.2.3.4"), the * empty string will be returned */ const char* http_uri_get_path (const http_uri *uri); /* Set URI path */ void http_uri_set_path (http_uri *uri, const char *path); /* Get URI host. It returns only host name, port number is * not included. * * IPv6 literal addresses are returned in square brackets * (i.e., [fe80::217:c8ff:fe7b:6a91%4]) * * Note, the subsequent modifications of URI, such as http_uri_fix_host(), * http_uri_fix_ipv6_zone() etc, may make the returned string invalid, * so if you need to keep it for a long time, better make a copy */ const char* http_uri_get_host (const http_uri *uri); /* http_uri_host_is checks if URI's host name is equal to the * specified string. * * It does its best to compare domain names correctly, taking * in account only significant difference (for example, the difference * in upper/lower case * in domain names is not significant). */ bool http_uri_host_is (const http_uri *uri, const char *host); /* http_uri_host_is_literal returns true if URI uses literal * IP address */ bool http_uri_host_is_literal (const http_uri *uri); /* Set URI host into the literal IP address. */ void http_uri_set_host_addr (http_uri *uri, ip_addr addr); /* Fix URI host: if `match` is NULL or uri's host matches `match`, * replace uri's host and port with values taken from the base_uri */ void http_uri_fix_host (http_uri *uri, const http_uri *base_uri, const char *match); /* Fix IPv6 address zone suffix */ void http_uri_fix_ipv6_zone (http_uri *uri, int ifindex); /* Strip zone suffix from literal IPv6 host address * * If address is not IPv6 or doesn't have zone suffix, it is * not changed */ void http_uri_strip_zone_suffux (http_uri *uri); /* Make sure URI's path ends with the slash character */ void http_uri_fix_end_slash (http_uri *uri); /* Check if 2 URIs are equal */ bool http_uri_equal (const http_uri *uri1, const http_uri *uri2); /* HTTP data */ typedef struct { const char *content_type; /* Normalized: low-case with stripped directives */ const void *bytes; /* Data bytes */ size_t size; /* Data size */ } http_data; /* Ref http_data */ http_data* http_data_ref (http_data *data); /* Unref http_data */ void http_data_unref (http_data *data); /* http_data_queue represents a queue of http_data items */ typedef struct http_data_queue http_data_queue; /* Create new http_data_queue */ http_data_queue* http_data_queue_new (void); /* Destroy http_data_queue */ void http_data_queue_free (http_data_queue *queue); /* Push item into the http_data_queue. */ void http_data_queue_push (http_data_queue *queue, http_data *data); /* Pull an item from the http_data_queue. Returns NULL if queue is empty */ http_data* http_data_queue_pull (http_data_queue *queue); /* Get queue length */ int http_data_queue_len (const http_data_queue *queue); /* Check if queue is empty */ static inline bool http_data_queue_empty (const http_data_queue *queue) { return http_data_queue_len(queue) == 0; } /* Purge the queue */ void http_data_queue_purge (http_data_queue *queue); /* Type http_client represents HTTP client instance */ typedef struct http_client http_client; /* Create new http_client */ http_client* http_client_new (log_ctx *log, void *ptr); /* Destroy http_client */ void http_client_free (http_client *client); /* Set on-error callback. If this callback is not NULL, * in a case of transport error it will be called instead * of the http_query callback */ void http_client_onerror (http_client *client, void (*callback)(void *ptr, error err)); /* Cancel all pending queries, if any */ void http_client_cancel (http_client *client); /* Set timeout of all pending queries, if any. Timeout is in milliseconds */ void http_client_timeout (http_client *client, int timeout); /* Check if client has pending queries */ bool http_client_has_pending (const http_client *client); /* Type http_query represents HTTP query (both request and response) */ typedef struct http_query http_query; /* Create new http_query * * Newly created http_query takes ownership on uri and body (if not NULL). * The method and content_type assumed to be constant strings. */ http_query* http_query_new (http_client *client, http_uri *uri, const char *method, char *body, const char *content_type); /* Create new http_query * * Newly created http_query takes ownership on uri and body (if not NULL). * The method and content_type assumed to be constant strings. */ http_query* http_query_new_len (http_client *client, http_uri *uri, const char *method, void *body, size_t body_len, const char *content_type); /* Create new http_query, relative to base URI * * Newly created http_query takes ownership on body (if not NULL). * The method and content_type assumed to be constant strings. */ http_query* http_query_new_relative(http_client *client, const http_uri *base_uri, const char *path, const char *method, char *body, const char *content_type); /* Set query timeout, in milliseconds. Negative timeout means 'infinite' * * This function may be called multiple times (each subsequent call overrides * a previous one) */ void http_query_timeout (http_query *q, int timeout); /* Set 'no_need_response_body' flag * * This flag notifies, that http_query issued is only interested * in the HTTP response headers, not body * * If this flag is set, after successful reception of response * HTTP header, errors in fetching response body is ignored */ void http_query_no_need_response_body (http_query *q); /* Set forcing port to be added to the Host header for this query. * * This function may be called multiple times (each subsequent call overrides * a previous one). */ void http_query_force_port(http_query *q, bool force_port); /* For this particular query override on-error callback, previously * set by http_client_onerror() * * If canllback is NULL, the completion callback, specified on a * http_query_submit() call, will be used even in a case of * transport error. */ void http_query_onerror (http_query *q, void (*onerror)(void *ptr, error err)); /* Set on-redirect callback. It is called in a case of HTTP * redirect and may modify the supplied URI */ void http_query_onredir (http_query *q, void (*onredir)(void *ptr, http_uri *uri, const http_uri *orig_uri)); /* Set callback that will be called, when response headers reception * is completed */ void http_query_onrxhdr (http_query *q, void (*onrxhdr)(void *ptr, http_query *q)); /* Submit the query. * * When query is finished, callback will be called. After return from * callback, memory, owned by http_query will be invalidated */ void http_query_submit (http_query *q, void (*callback)(void *ptr, http_query *q)); /* Get http_query timestamp. Timestamp is set when query is * submitted. And this function should not be called before * http_query_submit() */ timestamp http_query_timestamp (const http_query *q); /* Set uintptr_t parameter, associated with query. * Completion callback may later use http_query_get_uintptr() * to fetch this value */ void http_query_set_uintptr (http_query *q, uintptr_t u); /* Get uintptr_t parameter, previously set by http_query_set_uintptr() */ uintptr_t http_query_get_uintptr (http_query *q); /* Get query error, if any * * Both transport errors and erroneous HTTP response codes * considered as errors here */ error http_query_error (const http_query *q); /* Get query transport error, if any * * Only transport errors considered errors here */ error http_query_transport_error (const http_query *q); /* Get HTTP status code. Code not available, if query finished * with error */ int http_query_status (const http_query *q); /* Get HTTP status string */ const char* http_query_status_string (const http_query *q); /* Get query URI * * It works as http_query_orig_uri() before query is submitted * or after it is completed, and as http_query_real_uri() in * between * * This function is deprecated, use http_query_orig_uri() * or http_query_real_uri() instead */ http_uri* http_query_uri (const http_query *q); /* Get original URI (the same as used when http_query was created) */ http_uri* http_query_orig_uri (const http_query *q); /* Get real URI, that can differ from the requested URI * in a case of HTTP redirection */ http_uri* http_query_real_uri (const http_query *q); /* Get query method */ const char* http_query_method (const http_query *q); /* Set request header */ void http_query_set_request_header (http_query *q, const char *name, const char *value); /* Get request header */ const char* http_query_get_request_header (const http_query *q, const char *name); /* Get response header */ const char* http_query_get_response_header (const http_query *q, const char *name); /* Get request data * * You need to http_data_ref(), if you want data to remain valid * after query end of life */ http_data* http_query_get_request_data (const http_query *q); /* Get request data * * You need to http_data_ref(), if you want data to remain valid * after query end of life */ http_data* http_query_get_response_data (const http_query *q); /* Get count of parts of multipart response */ int http_query_get_mp_response_count (const http_query *q); /* Get data of Nth part of multipart response * * You need to http_data_ref(), if you want data to remain valid * after query end of life */ http_data* http_query_get_mp_response_data (const http_query *q, int n); /* Call callback for each request header */ void http_query_foreach_request_header (const http_query *q, void (*callback)(const char *name, const char *value, void *ptr), void *ptr); /* Call callback for each response header */ void http_query_foreach_response_header (const http_query *q, void (*callback)(const char *name, const char *value, void *ptr), void *ptr); /* Decode response part of the query. * This function is intended for testing purposes, not for regular use */ error http_query_test_decode_response (http_query *q, const void *data, size_t size); /* HTTP schemes */ typedef enum { HTTP_SCHEME_UNSET = -1, HTTP_SCHEME_HTTP, HTTP_SCHEME_HTTPS, HTTP_SCHEME_UNIX } HTTP_SCHEME; /* Some HTTP status codes */ #ifndef NO_HTTP_STATUS enum { HTTP_STATUS_OK = 200, HTTP_STATUS_CREATED = 201, HTTP_STATUS_NOT_FOUND = 404, HTTP_STATUS_GONE = 410, HTTP_STATUS_SERVICE_UNAVAILABLE = 503 }; #endif /* Initialize HTTP client */ SANE_Status http_init (void); /* Initialize HTTP client */ void http_cleanup (void); /******************** Protocol trace ********************/ /* Type trace represents an opaque handle of trace * file */ typedef struct trace trace; /* Initialize protocol trace. Called at backend initialization */ SANE_Status trace_init (void); /* Cleanup protocol trace. Called at backend unload */ void trace_cleanup (void); /* Open protocol trace */ trace* trace_open (const char *device_name); /* Ref the trace */ trace* trace_ref (trace *t); /* Unref the trace. When trace is not longer in use, it will be closed */ void trace_unref (trace *t); /* This hook is called on every http_query completion */ void trace_http_query_hook (trace *t, http_query *q); /* Printf to the trace log */ void trace_printf (trace *t, const char *fmt, ...); /* Note an error in trace log */ void trace_error (trace *t, error err); /* Dump message body */ void trace_dump_body (trace *t, http_data *data); /* Dump binary data (as hex dump) * Each line is prefixed with the `prefix` character */ void trace_hexdump (trace *t, char prefix, const void *data, size_t size); /******************** SANE_Word/SANE_String arrays ********************/ /* Create array of SANE_Word */ static inline SANE_Word* sane_word_array_new (void) { return mem_new(SANE_Word,1); } /* Free array of SANE_Word */ static inline void sane_word_array_free (SANE_Word *a) { mem_free(a); } /* Reset array of SANE_Word */ static inline void sane_word_array_reset (SANE_Word **a) { (*a)[0] = 0; } /* Get length of the SANE_Word array */ static inline size_t sane_word_array_len (const SANE_Word *a) { return (size_t) a[0]; } /* Append word to array. Returns new array (old becomes invalid) */ static inline SANE_Word* sane_word_array_append (SANE_Word *a, SANE_Word w) { size_t len = sane_word_array_len(a) + 1; a = mem_resize(a, len + 1, 0); a[0] = len; a[len] = w; return a; } /* Drop array elements that outside of specified boundary */ void sane_word_array_bound (SANE_Word *a, SANE_Word min, SANE_Word max); /* Sort array of SANE_Word in increasing order */ void sane_word_array_sort (SANE_Word *a); /* Intersect two sorted arrays. */ SANE_Word* sane_word_array_intersect_sorted ( const SANE_Word *a1, const SANE_Word *a2); /* Create array of SANE_String */ static inline SANE_String* sane_string_array_new (void) { return ptr_array_new(SANE_String); } /* Free array of SANE_String */ static inline void sane_string_array_free (SANE_String *a) { mem_free(a); } /* Reset array of SANE_String */ static inline void sane_string_array_reset (SANE_String *a) { ptr_array_trunc(a); } /* Get length of the SANE_String array */ static inline size_t sane_string_array_len (const SANE_String *a) { return mem_len(a); } /* Append string to array Returns new array (old becomes invalid) */ static inline SANE_String* sane_string_array_append(SANE_String *a, SANE_String s) { return ptr_array_append(a, s); } /* Compute max string length in array of strings */ size_t sane_string_array_max_strlen(const SANE_String *a); /* Create array of SANE_Device */ static inline const SANE_Device** sane_device_array_new (void) { return ptr_array_new(const SANE_Device*); } /* Free array of SANE_Device */ static inline void sane_device_array_free (const SANE_Device **a) { mem_free(a); } /* Get length of the SANE_Device array */ static inline size_t sane_device_array_len (const SANE_Device * const *a) { return mem_len(a); } /* Append device to array. Returns new array (old becomes invalid) */ static inline const SANE_Device** sane_device_array_append(const SANE_Device **a, SANE_Device *d) { return ptr_array_append(a, d); } /******************** XML utilities ********************/ /* xml_ns defines XML namespace. * * For XML writer namespaces are simply added to the root * node attributes * * XML reader performs prefix substitutions * * If namespace substitution is enabled, for each note, if its * namespace matches the pattern, will be reported with name prefix * defined by substitution rule, regardless of prefix actually used * in the document * * Example: * * * * * * * rule: {"ns", "http://www.example.com/namespace"} * * With this rule set, all nodes will be reported as if they * had the "ns" prefix, though actually their prefix in document * is different * * XML reader interprets namespace uri as a glob-style pattern, * as used by fnmatch (3) function with flags = 0 */ typedef struct { const char *prefix; /* Short prefix */ const char *uri; /* The namespace uri (glob pattern for reader) */ } xml_ns; /* xml_attr represents an XML attribute. * * Attributes are supported by XML writer. Array of attributes * is terminated by the {NULL, NULL} attribute */ typedef struct { const char *name; /* Attribute name */ const char *value; /* Attribute value */ } xml_attr; /* XML reader */ typedef struct xml_rd xml_rd; /* Parse XML text and initialize reader to iterate * starting from the root node * * The 'ns' argument, if not NULL, points to array of substitution * rules. Last element must have NULL prefix and url * * Array of rules considered to be statically allocated * (at least, it can remain valid during reader life time) * * On success, saves newly constructed reader into * the xml parameter. */ error xml_rd_begin (xml_rd **xml, const char *xml_text, size_t xml_len, const xml_ns *ns); /* Finish reading, free allocated resources */ void xml_rd_finish (xml_rd **xml); /* Get current node depth in the tree. Root depth is 0 */ unsigned int xml_rd_depth (xml_rd *xml); /* Check for end-of-document condition */ bool xml_rd_end (xml_rd *xml); /* Shift to the next node */ void xml_rd_next (xml_rd *xml); /* Shift to the next node, visiting the nested nodes on the way * * If depth > 0, it will not return from nested nodes * upper the specified depth */ void xml_rd_deep_next (xml_rd *xml, unsigned int depth); /* Enter the current node - iterate its children */ void xml_rd_enter (xml_rd *xml); /* Leave the current node - return to its parent */ void xml_rd_leave (xml_rd *xml); /* Get name of the current node. * * The returned string remains valid, until reader is cleaned up * or current node is changed (by set/next/enter/leave operations). * You don't need to free this string explicitly */ const char* xml_rd_node_name (xml_rd *xml); /* Get full path to the current node, '/'-separated */ const char* xml_rd_node_path (xml_rd *xml); /* Match name of the current node against the pattern */ bool xml_rd_node_name_match (xml_rd *xml, const char *pattern); /* Get value of the current node as text * * The returned string remains valid, until reader is cleaned up * or current node is changed (by set/next/enter/leave operations). * You don't need to free this string explicitly */ const char* xml_rd_node_value (xml_rd *xml); /* Get value of the current node as unsigned integer */ error xml_rd_node_value_uint (xml_rd *xml, SANE_Word *val); /* XML writer */ typedef struct xml_wr xml_wr; /* Begin writing XML document. Root node will be created automatically * * The ns parameter must be terminated by {NULL, NULL} structure */ xml_wr* xml_wr_begin (const char *root, const xml_ns *ns); /* Finish writing, generate document string. * Caller must g_free() this string after use */ char* xml_wr_finish (xml_wr *xml); /* Like xml_wr_finish, but returns compact representation * of XML (without indentation and new lines) */ char* xml_wr_finish_compact (xml_wr *xml); /* Add node with textual value */ void xml_wr_add_text (xml_wr *xml, const char *name, const char *value); /* Add text node with attributes */ void xml_wr_add_text_attr (xml_wr *xml, const char *name, const char *value, const xml_attr *attrs); /* Add node with unsigned integer value */ void xml_wr_add_uint (xml_wr *xml, const char *name, unsigned int value); /* Add node with unsigned integer value and attributes */ void xml_wr_add_uint_attr (xml_wr *xml, const char *name, unsigned int value, const xml_attr *attrs); /* Add node with boolean value */ void xml_wr_add_bool (xml_wr *xml, const char *name, bool value); /* Add node with boolean value and attributes */ void xml_wr_add_bool_attr (xml_wr *xml, const char *name, bool value, const xml_attr *attrs); /* Create node with children and enter newly added node */ void xml_wr_enter (xml_wr *xml, const char *name); /* xml_wr_enter with attributes */ void xml_wr_enter_attr (xml_wr *xml, const char *name, const xml_attr *attrs); /* Leave the current node */ void xml_wr_leave (xml_wr *xml); /* Format XML to file. It either succeeds, writes a formatted XML * and returns true, or fails, writes nothing to file and returns false */ bool xml_format (FILE *fp, const char *xml_text, size_t xml_len); /******************** Sane Options********************/ /* Options numbers, for internal use */ enum { OPT_NUM_OPTIONS, /* Total number of options */ /* Standard options group */ OPT_GROUP_STANDARD, OPT_SCAN_RESOLUTION, OPT_SCAN_COLORMODE, /* I.e. color/grayscale etc */ OPT_SCAN_INTENT, /* Document/Photo etc */ OPT_SCAN_SOURCE, /* Platem/ADF/ADF Duplex */ /* Geometry options group */ OPT_GROUP_GEOMETRY, OPT_SCAN_TL_X, OPT_SCAN_TL_Y, OPT_SCAN_BR_X, OPT_SCAN_BR_Y, /* Image enhancement group */ OPT_GROUP_ENHANCEMENT, OPT_BRIGHTNESS, OPT_CONTRAST, OPT_SHADOW, OPT_HIGHLIGHT, OPT_GAMMA, OPT_NEGATIVE, /* Read-only options for ADF justification */ OPT_JUSTIFICATION_X, OPT_JUSTIFICATION_Y, /* Total count of options, computed by compiler */ NUM_OPTIONS }; /* String constants for certain SANE options values * (missed from sane/sameopt.h) */ #define OPTVAL_SOURCE_PLATEN "Flatbed" #define OPTVAL_SOURCE_ADF_SIMPLEX "ADF" #define OPTVAL_SOURCE_ADF_DUPLEX "ADF Duplex" #define OPTVAL_JUSTIFICATION_LEFT "left" #define OPTVAL_JUSTIFICATION_CENTER "center" #define OPTVAL_JUSTIFICATION_RIGHT "right" #define OPTVAL_JUSTIFICATION_TOP "top" #define OPTVAL_JUSTIFICATION_BOTTOM "bottom" /* Define options not included in saneopts.h */ #define SANE_NAME_ADF_JUSTIFICATION_X "adf-justification-x" #define SANE_TITLE_ADF_JUSTIFICATION_X SANE_I18N("ADF Width Justification") #define SANE_DESC_ADF_JUSTIFICATION_X \ SANE_I18N("ADF width justification (left/right/center)") #define SANE_NAME_ADF_JUSTIFICATION_Y "adf-justification-y" #define SANE_TITLE_ADF_JUSTIFICATION_Y SANE_I18N("ADF Height Justification") #define SANE_DESC_ADF_JUSTIFICATION_Y \ SANE_I18N("ADF height justification (top/bottom/center)") /* Check if option belongs to image enhancement group */ static inline bool opt_is_enhancement (int opt) { return OPT_BRIGHTNESS <= opt && opt <= OPT_NEGATIVE; } /******************** Device Capabilities ********************/ /* Source flags */ enum { /* Supported Intents */ DEVCAPS_SOURCE_INTENT_DOCUMENT = (1 << 3), DEVCAPS_SOURCE_INTENT_TXT_AND_GRAPH = (1 << 4), DEVCAPS_SOURCE_INTENT_PHOTO = (1 << 5), DEVCAPS_SOURCE_INTENT_PREVIEW = (1 << 6), DEVCAPS_SOURCE_INTENT_ALL = DEVCAPS_SOURCE_INTENT_DOCUMENT | DEVCAPS_SOURCE_INTENT_TXT_AND_GRAPH | DEVCAPS_SOURCE_INTENT_PHOTO | DEVCAPS_SOURCE_INTENT_PREVIEW, /* How resolutions are defined */ DEVCAPS_SOURCE_RES_DISCRETE = (1 << 7), /* Discrete resolutions */ DEVCAPS_SOURCE_RES_RANGE = (1 << 8), /* Range of resolutions */ DEVCAPS_SOURCE_RES_ALL = DEVCAPS_SOURCE_RES_DISCRETE | DEVCAPS_SOURCE_RES_RANGE, /* Miscellaneous flags */ DEVCAPS_SOURCE_HAS_SIZE = (1 << 12), /* max_width, max_height and derivatives are valid */ /* Protocol dialects */ DEVCAPS_SOURCE_PWG_DOCFMT = (1 << 13), /* pwg:DocumentFormat */ DEVCAPS_SOURCE_SCAN_DOCFMT_EXT = (1 << 14), /* scan:DocumentFormatExt */ }; /* Supported image formats */ #define DEVCAPS_FORMATS_SUPPORTED \ ((1 << ID_FORMAT_JPEG) | \ (1 << ID_FORMAT_PNG) | \ (1 << ID_FORMAT_TIFF) | \ (1 << ID_FORMAT_BMP)) /* Supported color modes * * Note, currently the only image format we support is JPEG * With JPEG, ID_COLORMODE_BW1 cannot be supported */ #define DEVCAPS_COLORMODES_SUPPORTED \ ((1 << ID_COLORMODE_COLOR) | \ (1 << ID_COLORMODE_GRAYSCALE)) /* Source Capabilities (each device may contain multiple sources) */ typedef struct { unsigned int flags; /* Source flags */ unsigned int colormodes; /* Set of 1 << ID_COLORMODE */ unsigned int formats; /* Set of 1 << ID_FORMAT */ unsigned int scanintents; /* Set of 1 << ID_SCANINTENT */ SANE_Word min_wid_px, max_wid_px; /* Min/max width, in pixels */ SANE_Word min_hei_px, max_hei_px; /* Min/max height, in pixels */ SANE_Word *resolutions; /* Discrete resolutions, in DPI */ SANE_Range res_range; /* Resolutions range, in DPI */ SANE_Range win_x_range_mm; /* Window x range, in mm */ SANE_Range win_y_range_mm; /* Window y range, in mm */ } devcaps_source; /* Allocate devcaps_source */ devcaps_source* devcaps_source_new (void); /* Free devcaps_source */ void devcaps_source_free (devcaps_source *src); /* Clone a source */ devcaps_source* devcaps_source_clone (const devcaps_source *src); /* Merge two sources, resulting the source that contains * only capabilities, supported by two input sources * * Returns NULL, if sources cannot be merged */ devcaps_source* devcaps_source_merge (const devcaps_source *s1, const devcaps_source *s2); /* Device Capabilities */ typedef struct { /* Fundamental values */ const char *protocol; /* Protocol name */ SANE_Word units; /* Size units, pixels per inch */ /* Image compression */ bool compression_ok; /* Compression params are supported */ SANE_Range compression_range; /* Compression range */ SANE_Word compression_norm; /* Normal compression */ /* Sources */ devcaps_source *src[NUM_ID_SOURCE]; /* Missed sources are NULL */ /* ADF Justification */ ID_JUSTIFICATION justification_x; /* Width justification*/ ID_JUSTIFICATION justification_y; /* Height justification*/ } devcaps; /* Initialize Device Capabilities */ void devcaps_init (devcaps *caps); /* Cleanup Device Capabilities */ void devcaps_cleanup (devcaps *caps); /* Reset Device Capabilities into initial state */ void devcaps_reset (devcaps *caps); /* Dump device capabilities, for debugging * * The 3rd parameter, 'trace' configures the debug level * (log_debug vs log_trace) of the generated output */ void devcaps_dump (log_ctx *log, devcaps *caps, bool trace); /******************** Device options ********************/ /* Scan options */ typedef struct { devcaps caps; /* Device capabilities */ SANE_Option_Descriptor desc[NUM_OPTIONS]; /* Option descriptors */ ID_SOURCE src; /* Current source */ ID_COLORMODE colormode_emul; /* Current "emulated" color mode*/ ID_COLORMODE colormode_real; /* Current real color mode*/ ID_SCANINTENT scanintent; /* Current scan intent */ SANE_Word resolution; /* Current resolution */ SANE_Fixed tl_x, tl_y; /* Top-left x/y */ SANE_Fixed br_x, br_y; /* Bottom-right x/y */ SANE_Parameters params; /* Scan parameters */ SANE_String *sane_sources; /* Sources, in SANE format */ SANE_String *sane_colormodes; /* Color modes in SANE format */ SANE_String *sane_scanintents; /* Scan intents in SANE format */ SANE_Fixed brightness; /* -100.0 ... +100.0 */ SANE_Fixed contrast; /* -100.0 ... +100.0 */ SANE_Fixed shadow; /* 0.0 ... +100.0 */ SANE_Fixed highlight; /* 0.0 ... +100.0 */ SANE_Fixed gamma; /* Small positive value */ bool negative; /* Flip black and white */ } devopt; /* Initialize device options */ void devopt_init (devopt *opt); /* Cleanup device options */ void devopt_cleanup (devopt *opt); /* Set default option values. Before call to this function, * devopt.caps needs to be properly filled. */ void devopt_set_defaults (devopt *opt); /* Set device option */ SANE_Status devopt_set_option (devopt *opt, SANE_Int option, void *value, SANE_Word *info); /* Get device option */ SANE_Status devopt_get_option (devopt *opt, SANE_Int option, void *value); /******************** ZeroConf (device discovery) ********************/ /* Due to the way how device discovery is implemented, resolving * of device IP addresses are independent between IPv4/IPv6 protocols * and between different network interfaces * * It means that some of device addresses may be already discovered, * while others still pending * * From another hand, some of addresses that we hope to discover may * be not available at all. For example, device may have IPv4 address * but IPv6 address may be missed. * * So once we have at least one address discovered, we limit discovery * of another addresses by this constant. * * This parameter is common for both MDNS and WSDD worlds * * The timeout is in milliseconds */ #define ZEROCONF_PUBLISH_DELAY 1000 /* Common logging context for device discovery */ extern log_ctx *zeroconf_log; /* zeroconf_device represents a single device */ typedef struct zeroconf_device zeroconf_device; /* zeroconf_endpoint represents a device endpoint */ typedef struct zeroconf_endpoint zeroconf_endpoint; struct zeroconf_endpoint { ID_PROTO proto; /* The protocol */ http_uri *uri; /* I.e, "http://192.168.1.1:8080/eSCL/" */ zeroconf_endpoint *next; /* Next endpoint in the list */ }; /* ZEROCONF_METHOD represents a method how device was discovered * The same device may be discovered using multiple methods */ typedef enum { /* The following findings serve as indirect signs of * scanner presence in the network */ ZEROCONF_MDNS_HINT, /* Hint finding from MDNS world */ /* The following findings are expected to bring actual * scanner endpoints */ ZEROCONF_USCAN_TCP, /* _uscan._tcp */ ZEROCONF_USCANS_TCP, /* _uscans._tcp */ ZEROCONF_WSD, /* WS-Discovery */ NUM_ZEROCONF_METHOD } ZEROCONF_METHOD; /* zeroconf_finding represents a single device discovery finding. * Multiple findings can point to the same device, and even * endpoints may duplicate between findings (say, if the same * device found using multiple network interfaces or using various * discovery methods) * * zeroconf_finding are bound to method and interface index */ typedef struct { ZEROCONF_METHOD method; /* Discovery method */ const char *name; /* Network-unique name, NULL for WSD */ const char *model; /* Model name, may be NULL for WSDD non-scanner devices */ uuid uuid; /* Device UUID */ ip_addrset *addrs; /* Device addresses */ int ifindex; /* Network interface index */ zeroconf_endpoint *endpoints; /* List of endpoints */ /* The following fields are reserved for zeroconf core * and should not be used by discovery providers */ zeroconf_device *device; /* Device the finding points to */ ll_node list_node; /* Node in device's list of findings */ } zeroconf_finding; /* Compare two pointers to pointers to zeroconf_finding (zeroconf_finding**) * by index+name, for qsort */ int zeroconf_finding_qsort_by_index_name (const void *p1, const void *p2); /* Publish the zeroconf_finding. * * Memory, referred by the finding, remains owned by * caller, and caller is responsible to keep this * memory valid until zeroconf_finding_withdraw() * is called * * The 'endpoinds' field may be NULL. This mechanism is * used by WS-Discovery to notify zeroconf that scanning * for particular UUID has been finished, though without * success. */ void zeroconf_finding_publish (zeroconf_finding *finding); /* Withdraw the finding */ void zeroconf_finding_withdraw (zeroconf_finding *finding); /* Notify zeroconf subsystem that initial scan * for the method is done */ void zeroconf_finding_done (ZEROCONF_METHOD method); /* zeroconf_devinfo represents a device information */ typedef struct { const char *ident; /* Unique ident */ const char *name; /* Human-friendly name */ const char *model; /* Model name, for quirks. "" if unknown */ zeroconf_endpoint *endpoints; /* Device endpoints */ } zeroconf_devinfo; /* Initialize ZeroConf */ SANE_Status zeroconf_init (void); /* Cleanup ZeroConf */ void zeroconf_cleanup (void); /* Get list of devices, in SANE format */ const SANE_Device** zeroconf_device_list_get (void); /* Free list of devices, returned by zeroconf_device_list_get() */ void zeroconf_device_list_free (const SANE_Device **dev_list); /* Lookup device by ident (ident is reported as SANE_Device::name) * by zeroconf_device_list_get()) * * Caller becomes owner of resources (name and list of endpoints), * referred by the returned zeroconf_devinfo * * Caller must free these resources, using zeroconf_devinfo_free() */ zeroconf_devinfo* zeroconf_devinfo_lookup (const char *ident); /* * The format "protocol:name:url" is accepted to directly specify a device * without listing it in the config or finding it with autodiscovery. Try * to parse an identifier as that format. On success, returns a newly allocated * zeroconf_devinfo that the caller must free with zeroconf_devinfo_free(). On * failure, returns NULL. */ zeroconf_devinfo* zeroconf_parse_devinfo_from_ident(const char *ident); /* Free zeroconf_devinfo, returned by zeroconf_devinfo_lookup() */ void zeroconf_devinfo_free (zeroconf_devinfo *devinfo); /* Create new zeroconf_endpoint. Newly created endpoint * takes ownership of uri string */ zeroconf_endpoint* zeroconf_endpoint_new (ID_PROTO proto, http_uri *uri); /* Free single zeroconf_endpoint */ void zeroconf_endpoint_free_single (zeroconf_endpoint *endpoint); /* Create a copy of zeroconf_endpoint list */ zeroconf_endpoint* zeroconf_endpoint_list_copy (const zeroconf_endpoint *list); /* Free zeroconf_endpoint list */ void zeroconf_endpoint_list_free (zeroconf_endpoint *list); /* Sort list of endpoints */ zeroconf_endpoint* zeroconf_endpoint_list_sort (zeroconf_endpoint *list); /* Sort list of endpoints and remove duplicates */ zeroconf_endpoint* zeroconf_endpoint_list_sort_dedup (zeroconf_endpoint *list); /* Check if list of endpoints already contains the given * endpoint (i.e., endpoint with the same URI and protocol) */ bool zeroconf_endpoint_list_contains (const zeroconf_endpoint *list, const zeroconf_endpoint *endpoint); /* 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 */ void mdns_initscan_timer_expired (void); /* Initialize MDNS */ SANE_Status mdns_init (void); /* Cleanup MDNS */ void mdns_cleanup (void); /* mdns_resolver asynchronously resolves IP addresses using MDNS */ typedef struct mdns_resolver mdns_resolver; /* mdns_query represents a single mdns_resolver query */ typedef struct mdns_query mdns_query; /* mdns_resolver_new creates a new MDNS resolver */ mdns_resolver* mdns_resolver_new (int ifindex); /* mdns_resolver_free frees the mdns_resolver previously created * by mdns_resolver_new() */ void mdns_resolver_free (mdns_resolver *resolver); /* mdns_resolver_cancel cancels all pending queries */ void mdns_resolver_cancel (mdns_resolver *resolver); /* mdns_resolver_has_pending checks if resolver has pending queries */ bool mdns_resolver_has_pending (mdns_resolver *resolver); /* mdns_query_submit submits a new MDNS query for the specified domain * name. When resolving is done, successfully or not, callback will be * called * * The ptr parameter is passed to the callback without any interpretation * as a user-defined argument * * Answer is a set of discovered IP addresses. It is owned by resolver, * callback should not free it and should not assume that it is still * valid after return from callback */ mdns_query* mdns_query_submit (mdns_resolver *resolver, const char *name, void (*callback)(const mdns_query *query), void *ptr); /* mdns_query_cancel cancels the pending query. mdns_query memory will * be released and callback will not be called * * Note, mdns_query pointer is valid when obtained from mdns_query_sumbit * and until canceled or return from callback. */ void mdns_query_cancel (mdns_query *query); /* mdns_query_get_name returns domain name, as it was specified * when query was submitted */ const char* mdns_query_get_name (const mdns_query *query); /* mdns_query_get_answer returns resolved addresses */ const ip_addrset* mdns_query_get_answer (const mdns_query *query); /* mdns_query_set_ptr gets the user-defined ptr, associated * with query when it was submitted */ void* mdns_query_get_ptr (const mdns_query *query); /* mdns_device_count_by_model returns count of distinct devices * with model names matching the specified parent. * * Several instances of the same device (i.e. printer vs scanner) are * counted only once per network interface. * * WSDD uses this function to decide when to use extended discovery * time (some devices are known to be hard for WD-Discovery) * * Pattern is the glob-style expression, applied to the model name * of discovered devices. */ unsigned int mdns_device_count_by_model (int ifindex, const char *pattern); /******************** WS-Discovery ********************/ /* Called by zeroconf to notify wsdd about initial scan timer expiration */ void wsdd_initscan_timer_expired (void); /* Send WD-Discovery directed probe */ void wsdd_send_directed_probe (int ifindex, int af, const void *addr); /* Initialize WS-Discovery */ SANE_Status wsdd_init (void); /* Cleanup WS-Discovery */ void wsdd_cleanup (void); /******************** Device Management ********************/ /* Type device represents a scanner device */ typedef struct device device; /* Open a device */ device* device_open (const char *name, SANE_Status *status); /* Close the device * If log_msg is not NULL, it is written to the device log as late as possible */ void device_close (device *dev, const char *log_msg); /* Get device's logging context */ log_ctx* device_log_ctx (device *dev); /* Get option descriptor */ const SANE_Option_Descriptor* device_get_option_descriptor (device *dev, SANE_Int option); /* Get device option */ SANE_Status device_get_option (device *dev, SANE_Int option, void *value); /* Set device option */ SANE_Status device_set_option (device *dev, SANE_Int option, void *value, SANE_Word *info); /* Get current scan parameters */ SANE_Status device_get_parameters (device *dev, SANE_Parameters *params); SANE_Status device_start (device *dev); /* Cancel scanning operation */ void device_cancel (device *dev); /* Set I/O mode */ SANE_Status device_set_io_mode (device *dev, SANE_Bool non_blocking); /* Get select file descriptor */ SANE_Status device_get_select_fd (device *dev, SANE_Int *fd); /* Read scanned image */ SANE_Status device_read (device *dev, SANE_Byte *data, SANE_Int max_len, SANE_Int *len); /* Initialize device management */ SANE_Status device_management_init (void); /* Cleanup device management */ void device_management_cleanup (void); /******************** Image filters ********************/ /* Type filter represents image filter */ typedef struct filter filter; struct filter { filter *next; /* Next filter in a chain */ void (*dump) (filter *f, /* Dump filter to the log */ log_ctx *log); void (*free) (filter *f); /* Free the filter */ void (*apply) (filter *f, /* Apply filter to the line of image */ uint8_t *line, size_t size); }; /* Free chain of filters */ void filter_chain_free (filter *chain); /* Push translation table based filter, that handles the * following options: * - brightness * - contrast * - negative * * Returns updated chain */ filter* filter_chain_push_xlat (filter *old_chain, const devopt *opt); /* Dump filter chain to the log */ void filter_chain_dump (filter *chain, log_ctx *log); /* Apply filter chain to the image line */ void filter_chain_apply (filter *chain, uint8_t *line, size_t size); /******************** Scan Protocol handling ********************/ /* PROTO_OP represents operation */ typedef enum { PROTO_OP_NONE, /* No operation */ PROTO_OP_PRECHECK,/* Pre-scan check */ PROTO_OP_SCAN, /* New scan */ PROTO_OP_LOAD, /* Load image */ PROTO_OP_CHECK, /* Check device status */ PROTO_OP_CLEANUP, /* Cleanup after scan */ PROTO_OP_FINISH /* Finish scanning */ } PROTO_OP; /* Get PROTO_OP name, for logging */ const char* proto_op_name (PROTO_OP op); /* proto_scan_params represents scan parameters */ typedef struct { int x_off, y_off; /* Scan area X/Y offset */ int wid, hei; /* Scan area width and height */ int x_res, y_res; /* X/Y resolution */ ID_SOURCE src; /* Desired source */ ID_COLORMODE colormode; /* Desired color mode */ ID_SCANINTENT scanintent; /* Desired scan intent */ ID_FORMAT format; /* Desired image format */ } proto_scan_params; /* proto_ctx represents request context */ typedef struct { /* Common context */ log_ctx *log; /* Logging context */ struct proto_handler *proto; /* Link to proto_handler */ const zeroconf_devinfo *devinfo; /* Device info, from zeroconf */ const devcaps *devcaps; /* Device capabilities */ PROTO_OP op; /* Current operation */ http_client *http; /* HTTP client for sending requests */ http_uri *base_uri; /* HTTP base URI for protocol */ http_uri *base_uri_nozone;/* base_uri without IPv6 zone */ proto_scan_params params; /* Scan parameters */ const char *location; /* Image location */ unsigned int images_received; /* Total count of received images */ /* Extra context for xxx_decode callbacks */ const http_query *query; /* Passed to xxx_decode callbacks */ /* Extra context for status_decode callback */ PROTO_OP failed_op; /* Failed operation */ int failed_http_status; /* Its HTTP status */ int failed_attempt; /* Retry count, 0-based */ /* Extra context for image decoding */ ID_FORMAT format_detected; /* Actual image format */ } proto_ctx; /* proto_result represents decoded query results */ typedef struct { PROTO_OP next; /* Next operation */ int delay; /* In milliseconds */ SANE_Status status; /* Job status */ error err; /* Error string, may be NULL */ union { const char *location; /* Image location, protocol-specific */ http_data *image; /* Image buffer */ } data; } proto_result; /* proto represents scan protocol implementation */ typedef struct proto_handler proto_handler; struct proto_handler { const char *name; /* Protocol name (i.e., "eSCL", "WSD", "IPP") */ /* Free protocol handler */ void (*free) (proto_handler *proto); /* Query and decode device capabilities */ http_query* (*devcaps_query) (const proto_ctx *ctx); error (*devcaps_decode) (const proto_ctx *ctx, devcaps *caps); /* Create pre-scan check query and decode result * These callback are optional, set to NULL, if * they are not implemented by the protocol * handler */ http_query* (*precheck_query) (const proto_ctx *ctx); proto_result (*precheck_decode) (const proto_ctx *ctx); /* Initiate scanning and decode result. * On success, scan_decode must set ctx->data.location */ http_query* (*scan_query) (const proto_ctx *ctx); proto_result (*scan_decode) (const proto_ctx *ctx); /* Initiate image downloading and decode result. * On success, load_decode must set ctx->data.image */ http_query* (*load_query) (const proto_ctx *ctx); proto_result (*load_decode) (const proto_ctx *ctx); /* Request device status and decode result */ http_query* (*status_query) (const proto_ctx *ctx); proto_result (*status_decode) (const proto_ctx *ctx); /* Cleanup after scan */ http_query* (*cleanup_query) (const proto_ctx *ctx); /* Cancel scan in progress */ http_query* (*cancel_query) (const proto_ctx *ctx); /* Test interfaces. Not for regular use! */ error (*test_decode_devcaps) (proto_handler *proto, const void *xml_text, size_t xms_size, devcaps *caps); }; /* proto_handler_escl_new creates new eSCL protocol handler */ proto_handler* proto_handler_escl_new (void); /* proto_handler_wsd_new creates new WSD protocol handler */ proto_handler* proto_handler_wsd_new (void); /* proto_handler_new creates new protocol handler by protocol ID */ static inline proto_handler* proto_handler_new (ID_PROTO proto) { switch (proto) { case ID_PROTO_ESCL: return proto_handler_escl_new(); case ID_PROTO_WSD: return proto_handler_wsd_new(); default: return NULL; } } /* proto_handler_free destroys protocol handler, previously * created by proto_handler_new/proto_handler_escl_new/ * proto_handler_wsd_new functions */ static inline void proto_handler_free (proto_handler *proto) { proto->free(proto); } /******************** Image decoding ********************/ /* The window withing the image * * Note, all sizes and coordinates are in pixels */ typedef struct { int x_off, y_off; /* Top-left corner offset */ int wid, hei; /* Image width and height */ } image_window; /* Image decoder, with virtual methods */ typedef struct image_decoder image_decoder; struct image_decoder { const char *content_type; void (*free) (image_decoder *decoder); error (*begin) (image_decoder *decoder, const void *data, size_t size); void (*reset) (image_decoder *decoder); int (*get_bytes_per_pixel) (image_decoder *decoder); void (*get_params) (image_decoder *decoder, SANE_Parameters *params); error (*set_window) (image_decoder *decoder, image_window *win); error (*read_line) (image_decoder *decoder, void *buffer); }; /* Detect image format by image data */ ID_FORMAT image_format_detect (const void *data, size_t size); /* Create JPEG image decoder */ image_decoder* image_decoder_jpeg_new (void); /* Create PNG image decoder */ image_decoder* image_decoder_png_new (void); /* Create TIFF image decoder */ image_decoder* image_decoder_tiff_new (void); /* Create BMP image decoder */ image_decoder* image_decoder_bmp_new (void); /* Free image decoder */ static inline void image_decoder_free (image_decoder *decoder) { decoder->free(decoder); } /* Get content type */ static inline const char* image_content_type (image_decoder *decoder) { return decoder->content_type; } /* Begin image decoding. Decoder may assume that provided data * buffer remains valid during a whole decoding cycle */ static inline error image_decoder_begin (image_decoder *decoder, const void *data, size_t size) { return decoder->begin(decoder, data, size); } /* Reset image decoder after use. After reset, decoding of the * another image can be started */ static inline void image_decoder_reset (image_decoder *decoder) { decoder->reset(decoder); } /* Get bytes count per pixel */ static inline int image_decoder_get_bytes_per_pixel (image_decoder *decoder) { return decoder->get_bytes_per_pixel(decoder); } /* Get image parameters. Can be called at any time between * image_decoder_begin() and image_decoder_reset() * * Decoder must return an actual image parameters, regardless * of clipping window set by image_decoder_set_window() */ static inline void image_decoder_get_params (image_decoder *decoder, SANE_Parameters *params) { decoder->get_params(decoder, params); } /* Set window within the image. Only part of image that fits the * window needs to be decoded. Decoder may assume that window is * always within the actual image boundaries * * Note, if decoder cannot handle exact window boundaries, it * it must update window to keep actual values * * In particular, if decoder doesn't implement image clipping * at all, it is safe that decoder will simply set window boundaries * to contain an entire image */ static inline error image_decoder_set_window (image_decoder *decoder, image_window *win) { return decoder->set_window(decoder, win); } /* Read next line of image. Decoder may safely assume the provided * buffer is big enough to keep the entire line */ static inline error image_decoder_read_line (image_decoder *decoder, void *buffer) { return decoder->read_line(decoder, buffer); } /* image_decoder_create_all creates all decoders * and fills array of decoders, indexed by ID_FORMAT * * Note, it is not guaranteed, that for all ID_FORMAT * decoder will be created. Missed entries will be set * to NULL. Be aware when using the filled array! */ static inline void image_decoder_create_all (image_decoder *decoders[NUM_ID_FORMAT]) { int i; /* Fill entire array with NULLs */ for (i = 0; i < NUM_ID_FORMAT; i ++) { decoders[i] = NULL; } /* Create known decoders */ decoders[ID_FORMAT_BMP] = image_decoder_bmp_new(); decoders[ID_FORMAT_JPEG] = image_decoder_jpeg_new(); decoders[ID_FORMAT_PNG] = image_decoder_png_new(); decoders[ID_FORMAT_TIFF] = image_decoder_tiff_new(); } /* image_decoder_free_all destroys all decoders, previously * created by image_decoder_create_all */ static inline void image_decoder_free_all (image_decoder *decoders[NUM_ID_FORMAT]) { int i; for (i = 0; i < NUM_ID_FORMAT; i ++) { image_decoder *decoder = decoders[i]; if (decoder != NULL) { image_decoder_free(decoder); decoders[i] = NULL; /* For sanity */ } } } /******************** Mathematical Functions ********************/ /* Find greatest common divisor of two positive integers */ SANE_Word math_gcd (SANE_Word x, SANE_Word y); /* Find least common multiple of two positive integers */ SANE_Word math_lcm (SANE_Word x, SANE_Word y); /* Find min of two words */ static inline SANE_Word math_min (SANE_Word a, SANE_Word b) { return a < b ? a : b; } /* Find max of two words */ static inline SANE_Word math_max (SANE_Word a, SANE_Word b) { return a > b ? a : b; } /* Bound integer within range */ static inline SANE_Word math_bound (SANE_Word x, SANE_Word min, SANE_Word max) { if (x < min) { return min; } else if (x > max) { return max; } else { return x; } } /* Bound double within range */ static inline double math_bound_double (double x, double min, double max) { if (x < min) { return min; } else if (x > max) { return max; } else { return x; } } /* Compute x * mul / div, taking in account rounding * and integer overflow */ static inline SANE_Word math_muldiv (SANE_Word x, SANE_Word mul, SANE_Word div) { int64_t tmp; tmp = (int64_t) x * (int64_t) mul; tmp += div / 2; tmp /= div; return (SANE_Word) tmp; } /* Merge two ranges, if possible */ bool math_range_merge (SANE_Range *out, const SANE_Range *r1, const SANE_Range *r2); /* Choose nearest integer in range */ SANE_Word math_range_fit (const SANE_Range *r, SANE_Word i); /* Convert pixels to millimeters, using given resolution */ static inline SANE_Fixed math_px2mm_res (SANE_Word px, SANE_Word res) { return SANE_FIX((double) px * 25.4 / res); } /* Convert millimeters to pixels, using given resolution */ static inline SANE_Word math_mm2px_res (SANE_Fixed mm, SANE_Word res) { return (SANE_Word) roundl(SANE_UNFIX(mm) * res / 25.4); } /* Format millimeters, for printing */ char* math_fmt_mm (SANE_Word mm, char buf[]); /* Genrate random 32-bit integer */ uint32_t math_rand_u32 (void); /* Generate random integer in range [0...max], inclusively */ uint32_t math_rand_max (uint32_t max); /* Generate random integer in range [min...max], inclusively */ uint32_t math_rand_range (uint32_t min, uint32_t max); /* Count nonzero bits in 32-bit integer */ static inline unsigned int math_popcount (unsigned int n) { unsigned int count = (n & 0x55555555) + ((n >> 1) & 0x55555555); count = (count & 0x33333333) + ((count >> 2) & 0x33333333); count = (count & 0x0F0F0F0F) + ((count >> 4) & 0x0F0F0F0F); count = (count & 0x00FF00FF) + ((count >> 8) & 0x00FF00FF); return (count & 0x0000FFFF) + ((count >> 16) & 0x0000FFFF); } /******************** Logging ********************/ /* Initialize logging * * No log messages should be generated before this call */ void log_init (void); /* Cleanup logging * * No log messages should be generated after this call */ void log_cleanup (void); /* Notify logger that configuration is loaded and * logger can configure itself * * This is safe to generate log messages before log_configure() * is called. These messages will be buffered, and after * logger is configured, either written or abandoned, depending * on configuration */ void log_configure (void); /* log_ctx_new creates new logging context * If parent != NULL, new logging context will have its own prefix, * but trace file will be inherited from parent */ log_ctx* log_ctx_new (const char *name, log_ctx *parent); /* log_ctx_free destroys logging context */ void log_ctx_free (log_ctx *log); /* Get protocol trace associated with logging context */ trace* log_ctx_trace (log_ctx *log); /* Write a debug message. */ void log_debug (log_ctx *log, const char *fmt, ...); /* Write a protocol trace message */ void log_trace (log_ctx *log, const char *fmt, ...); /* Write a block of data into protocol trace */ void log_trace_data (log_ctx *log, const char *content_type, const void *bytes, size_t size); /* Write an error message and terminate a program. */ void log_panic (log_ctx *log, const char *fmt, ...); /* Panic if assertion fails */ #define log_assert(log,expr) \ do { \ if (!(expr)) { \ log_panic(log,"file %s: line %d (%s): assertion failed: (%s)",\ __FILE__, __LINE__, __PRETTY_FUNCTION__, #expr); \ __builtin_unreachable(); \ } \ } while (0) /* Panic if this code is reached */ #define log_internal_error(log) \ do { \ log_panic(log,"file %s: line %d (%s): internal error", \ __FILE__, __LINE__, __PRETTY_FUNCTION__); \ __builtin_unreachable(); \ } while (0) /******************** Initialization/Cleanup ********************/ /* AIRSCAN_INIT_FLAGS represents airscan_init() flags * * These flags are mostly used for testing */ typedef enum { AIRSCAN_INIT_NO_CONF = (1 << 0), // Don't load configuration AIRSCAN_INIT_NO_THREAD = (1 << 1) // Don't start worker thread } AIRSCAN_INIT_FLAGS; /* Initialize airscan. * If log_msg is not NULL, it is written to the log early */ SANE_Status airscan_init (AIRSCAN_INIT_FLAGS flags, const char *log_msg); /* Cleanup airscan * If log_msg is not NULL, it is written to the log as late as possible */ void airscan_cleanup (const char *log_msg); /* Get init flags from the airscan_init call */ AIRSCAN_INIT_FLAGS airscan_get_init_flags (void); #ifdef __cplusplus }; #endif #endif /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/airscan.sym000066400000000000000000000011501500411437100164100ustar00rootroot00000000000000{ global: sane_airscan_cancel; sane_airscan_close; sane_airscan_control_option; sane_airscan_exit; sane_airscan_get_devices; sane_airscan_get_option_descriptor; sane_airscan_get_parameters; sane_airscan_get_select_fd; sane_airscan_init; sane_airscan_open; sane_airscan_read; sane_airscan_set_io_mode; sane_airscan_start; sane_cancel; sane_close; sane_control_option; sane_exit; sane_get_devices; sane_get_option_descriptor; sane_get_parameters; sane_get_select_fd; sane_init; sane_open; sane_read; sane_set_io_mode; sane_start; sane_strstatus; local: *; }; sane-airscan-0.99.35/discover.c000066400000000000000000000060351500411437100162270ustar00rootroot00000000000000/* Discovery tool for sane-airscan compatible devices * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions */ #include "airscan.h" #include #include #include #include /* Print usage and exit */ static void usage (char **argv) { printf("Usage:\n"); printf(" %s [options]\n", argv[0]); printf("\n"); printf("Options are:\n"); printf(" -test-fast Fast discovery mode, for testing.\n"); printf(" -test-auto automatic protocol selection, for testing\n"); printf(" -d enable debug mode\n"); printf(" -t enable protocol trace\n"); printf(" -h print help page\n"); exit(0); } /* Print usage error end exit */ static void usage_error (char **argv, char *arg) { printf("Invalid argument %s\n", arg); printf("Try %s -h for more information\n", argv[0]); exit(1); } /* Print error message and exit */ void die (const char *format, ...) { va_list ap; va_start(ap, format); vprintf(format, ap); printf("\n"); va_end(ap); exit(1); } /* The main function */ int main (int argc, char **argv) { int i; const SANE_Device **devices; /* Enforce some configuration parameters */ conf.proto_auto = false; conf.wsdd_mode = WSDD_FULL; /* Parse command-line options */ for (i = 1; i < argc; i ++) { if (!strcmp(argv[i], "-test-fast")) { conf.wsdd_mode = WSDD_FAST; } else if (!strcmp(argv[i], "--test-fast")) { conf.wsdd_mode = WSDD_FAST; } else if (!strcmp(argv[i], "-test-auto")) { conf.proto_auto = true; } else if (!strcmp(argv[i], "--test-auto")) { conf.proto_auto = true; } else if (!strcmp(argv[i], "-d")) { conf.dbg_enabled = true; } else if (!strcmp(argv[i], "-t")) { conf.dbg_trace = str_dup("./"); } else if (!strcmp(argv[i], "-h")) { usage(argv); } else { usage_error(argv, argv[i]); } } /* Initialize airscan */ airscan_init(AIRSCAN_INIT_NO_CONF, NULL); /* Get list of devices */ eloop_mutex_lock(); devices = zeroconf_device_list_get(); eloop_mutex_unlock(); /* Print list of devices */ printf("[devices]\n"); for (i = 0; devices[i] != NULL; i ++) { const SANE_Device *dev = devices[i]; zeroconf_devinfo *devinfo; zeroconf_endpoint *endpoint; eloop_mutex_lock(); devinfo = zeroconf_devinfo_lookup(dev->name); eloop_mutex_unlock(); for (endpoint = devinfo->endpoints; endpoint != NULL; endpoint = endpoint->next) { printf(" %s = %s, %s\n", devinfo->name, http_uri_str(endpoint->uri), id_proto_name(endpoint->proto)); } zeroconf_devinfo_free(devinfo); } zeroconf_device_list_free(devices); eloop_thread_stop(); airscan_cleanup(NULL); return 0; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/dll.conf000066400000000000000000000000521500411437100156600ustar00rootroot00000000000000# sane-dll entry for sane-airscan airscan sane-airscan-0.99.35/fuzzer/000077500000000000000000000000001500411437100155665ustar00rootroot00000000000000sane-airscan-0.99.35/fuzzer/query.cc000066400000000000000000000053541500411437100172510ustar00rootroot00000000000000// Copyright 2020 The Chromium OS Authors. All rights reserved. // See the sane-airscan top-level LICENSE for license terms and conditions. #include #include #include #include #include "airscan.h" constexpr int kMaxInputSize = 32 * 1024; namespace { struct LogWrapper { LogWrapper() { log_init(); } ~LogWrapper() { log_cleanup(); } }; struct EventLoop { EventLoop() { eloop_init(); } ~EventLoop() { eloop_cleanup(); } }; class LogContext { public: LogContext() : ctx_(log_ctx_new("http fuzzer", nullptr)) {} ~LogContext() { log_ctx_free(ctx_); } log_ctx *get() { return ctx_; } private: log_ctx *ctx_; }; class HttpClient { public: explicit HttpClient(log_ctx *ctx) : client_(http_client_new(ctx, nullptr)) {} ~HttpClient() { http_client_free(client_); } http_client *get() { return client_; } private: http_client *client_; }; void query_callback(void *, http_query *) {} } // namespace extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { // Limit fuzzer input size to 32KB. if (size > kMaxInputSize) { return 0; } LogWrapper log; EventLoop loop; LogContext ctx; if (!ctx.get()) { return 0; } HttpClient client(ctx.get()); if (!client.get()) { return 0; } FuzzedDataProvider provider(data, size); std::string uri_str = provider.ConsumeRandomLengthString( provider.remaining_bytes()); http_uri *uri = http_uri_new(uri_str.c_str(), provider.ConsumeBool()); if (!uri) { return 0; } std::string method = provider.ConsumeRandomLengthString( provider.remaining_bytes()); std::string body = provider.ConsumeRandomLengthString( provider.remaining_bytes()); std::string content_type = provider.ConsumeRandomLengthString( provider.remaining_bytes()); // str_dup() is airscan's internal string copy function which uses mem_new(). // Only body needs to be copied, since http_query_new() takes ownership. http_query *query = http_query_new(client.get(), uri, method.c_str(), str_dup(body.c_str()), content_type.c_str()); std::string unset_field = provider.ConsumeRandomLengthString( provider.remaining_bytes()); http_query_get_request_header(query, unset_field.c_str()); std::string set_field = provider.ConsumeRandomLengthString( provider.remaining_bytes()); std::string value = provider.ConsumeRandomLengthString( provider.remaining_bytes()); http_query_set_request_header(query, set_field.c_str(), value.c_str()); http_query_get_request_header(query, set_field.c_str()); http_query_status(query); http_query_status_string(query); http_query_submit(query, &query_callback); http_client_cancel(client.get()); return 0; } sane-airscan-0.99.35/fuzzer/uri.cc000066400000000000000000000032171500411437100166770ustar00rootroot00000000000000// Copyright 2020 The Chromium OS Authors. All rights reserved. // See the sane-airscan top-level LICENSE for license terms and conditions. #include #include #include #include #include "airscan.h" constexpr int kMaxInputSize = 32 * 1024; namespace { struct LogWrapper { LogWrapper() { log_init(); } ~LogWrapper() { log_cleanup(); } }; } // namespace extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { // Limit fuzzer input size to 32KB. URLs technically don't have a limit, but // practical sizes found in real life will be under 4KB. This gives us a // buffer to test longer URLs without wasting time on huge inputs. if (size > kMaxInputSize) { return 0; } FuzzedDataProvider data_provider(data, size); std::string str_input = data_provider.ConsumeRemainingBytesAsString(); LogWrapper log; http_uri *uri1 = http_uri_new(str_input.c_str(), true); if (uri1 == NULL) { return 0; } http_uri_free(uri1); uri1 = http_uri_new(str_input.c_str(), false); if (uri1 == NULL) { return 0; } http_uri *uri2 = http_uri_clone(uri1); if (uri2 == NULL) { http_uri_free(uri1); return 0; } http_uri_free(uri2); const char *str = http_uri_str(uri1); uri2 = http_uri_new(str, true); if (uri2 == NULL) { http_uri_free(uri1); return 0; } http_uri_free(uri2); const char *path = http_uri_get_path(uri1); uri2 = http_uri_new_relative(uri1, path, false, false); if (uri2 == NULL) { http_uri_free(uri1); return 0; } http_uri_free(uri2); http_uri_fix_end_slash(uri1); http_uri_free(uri1); return 0; } sane-airscan-0.99.35/fuzzer/xml.cc000066400000000000000000000016371500411437100167040ustar00rootroot00000000000000// Copyright 2020 The Chromium OS Authors. All rights reserved. // See the sane-airscan top-level LICENSE for license terms and conditions. #include #include #include #include #include "airscan.h" void noerrs(void *ctx, const char*msg, ...) { // Ignore the libxml error messages. } constexpr int kMaxInputSize = 32 * 1024; extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { // Limit fuzzer input size to 32KB. libxml can take a very long time to // parse very large inputs, which causes the fuzzer to time out. if (size > kMaxInputSize) { return 0; } xmlSetGenericErrorFunc(NULL, noerrs); xml_rd *xml = NULL; if (xml_rd_begin(&xml, (const char*)data, size, NULL)) { return 0; } if (xml == NULL) { return 0; } while (!xml_rd_end(xml)) { xml_rd_deep_next(xml, 0); } xml_rd_finish(&xml); return 0; } sane-airscan-0.99.35/http_parser.c000066400000000000000000002232031500411437100167420ustar00rootroot00000000000000/* Copyright Joyent, Inc. and other Node contributors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ #include "http_parser.h" #include #include #include #include #include static uint32_t max_header_size = HTTP_MAX_HEADER_SIZE; #ifndef ULLONG_MAX # define ULLONG_MAX ((uint64_t) -1) /* 2^64-1 */ #endif #ifndef MIN # define MIN(a,b) ((a) < (b) ? (a) : (b)) #endif #ifndef ARRAY_SIZE # define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) #endif #ifndef BIT_AT # define BIT_AT(a, i) \ (!!((unsigned int) (a)[(unsigned int) (i) >> 3] & \ (1 << ((unsigned int) (i) & 7)))) #endif #ifndef ELEM_AT # define ELEM_AT(a, i, v) ((unsigned int) (i) < ARRAY_SIZE(a) ? (a)[(i)] : (v)) #endif #define SET_ERRNO(e) \ do { \ parser->nread = nread; \ parser->http_errno = (e); \ } while(0) #define CURRENT_STATE() p_state #define UPDATE_STATE(V) p_state = (enum state) (V); #define RETURN(V) \ do { \ parser->nread = nread; \ parser->state = CURRENT_STATE(); \ return (V); \ } while (0); #define REEXECUTE() \ goto reexecute; \ #ifdef __GNUC__ # define LIKELY(X) __builtin_expect(!!(X), 1) # define UNLIKELY(X) __builtin_expect(!!(X), 0) #else # define LIKELY(X) (X) # define UNLIKELY(X) (X) #endif /* Run the notify callback FOR, returning ER if it fails */ #define CALLBACK_NOTIFY_(FOR, ER) \ do { \ assert(HTTP_PARSER_ERRNO(parser) == HPE_OK); \ \ if (LIKELY(settings->on_##FOR)) { \ parser->state = CURRENT_STATE(); \ if (UNLIKELY(0 != settings->on_##FOR(parser))) { \ SET_ERRNO(HPE_CB_##FOR); \ } \ UPDATE_STATE(parser->state); \ \ /* We either errored above or got paused; get out */ \ if (UNLIKELY(HTTP_PARSER_ERRNO(parser) != HPE_OK)) { \ return (ER); \ } \ } \ } while (0) /* Run the notify callback FOR and consume the current byte */ #define CALLBACK_NOTIFY(FOR) CALLBACK_NOTIFY_(FOR, p - data + 1) /* Run the notify callback FOR and don't consume the current byte */ #define CALLBACK_NOTIFY_NOADVANCE(FOR) CALLBACK_NOTIFY_(FOR, p - data) /* Run data callback FOR with LEN bytes, returning ER if it fails */ #define CALLBACK_DATA_(FOR, LEN, ER) \ do { \ assert(HTTP_PARSER_ERRNO(parser) == HPE_OK); \ \ if (FOR##_mark) { \ if (LIKELY(settings->on_##FOR)) { \ parser->state = CURRENT_STATE(); \ if (UNLIKELY(0 != \ settings->on_##FOR(parser, FOR##_mark, (LEN)))) { \ SET_ERRNO(HPE_CB_##FOR); \ } \ UPDATE_STATE(parser->state); \ \ /* We either errored above or got paused; get out */ \ if (UNLIKELY(HTTP_PARSER_ERRNO(parser) != HPE_OK)) { \ return (ER); \ } \ } \ FOR##_mark = NULL; \ } \ } while (0) /* Run the data callback FOR and consume the current byte */ #define CALLBACK_DATA(FOR) \ CALLBACK_DATA_(FOR, p - FOR##_mark, p - data + 1) /* Run the data callback FOR and don't consume the current byte */ #define CALLBACK_DATA_NOADVANCE(FOR) \ CALLBACK_DATA_(FOR, p - FOR##_mark, p - data) /* Set the mark FOR; non-destructive if mark is already set */ #define MARK(FOR) \ do { \ if (!FOR##_mark) { \ FOR##_mark = p; \ } \ } while (0) /* Don't allow the total size of the HTTP headers (including the status * line) to exceed max_header_size. This check is here to protect * embedders against denial-of-service attacks where the attacker feeds * us a never-ending header that the embedder keeps buffering. * * This check is arguably the responsibility of embedders but we're doing * it on the embedder's behalf because most won't bother and this way we * make the web a little safer. max_header_size is still far bigger * than any reasonable request or response so this should never affect * day-to-day operation. */ #define COUNT_HEADER_SIZE(V) \ do { \ nread += (uint32_t)(V); \ if (UNLIKELY(nread > max_header_size)) { \ SET_ERRNO(HPE_HEADER_OVERFLOW); \ goto error; \ } \ } while (0) #define PROXY_CONNECTION "proxy-connection" #define CONNECTION "connection" #define CONTENT_LENGTH "content-length" #define TRANSFER_ENCODING "transfer-encoding" #define UPGRADE "upgrade" #define CHUNKED "chunked" #define KEEP_ALIVE "keep-alive" #define CLOSE "close" static const char *method_strings[] = { #define XX(num, name, string) #string, HTTP_METHOD_MAP(XX) #undef XX }; /* Tokens as defined by rfc 2616. Also lowercases them. * token = 1* * separators = "(" | ")" | "<" | ">" | "@" * | "," | ";" | ":" | "\" | <"> * | "/" | "[" | "]" | "?" | "=" * | "{" | "}" | SP | HT */ static const char tokens[256] = { /* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */ 0, 0, 0, 0, 0, 0, 0, 0, /* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */ 0, 0, 0, 0, 0, 0, 0, 0, /* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */ 0, 0, 0, 0, 0, 0, 0, 0, /* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */ 0, 0, 0, 0, 0, 0, 0, 0, /* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */ ' ', '!', 0, '#', '$', '%', '&', '\'', /* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */ 0, 0, '*', '+', 0, '-', '.', 0, /* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */ '0', '1', '2', '3', '4', '5', '6', '7', /* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */ '8', '9', 0, 0, 0, 0, 0, 0, /* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */ 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', /* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */ 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', /* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', /* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */ 'x', 'y', 'z', 0, 0, 0, '^', '_', /* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */ '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', /* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */ 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', /* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', /* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */ 'x', 'y', 'z', 0, '|', 0, '~', 0 }; static const int8_t unhex[256] = {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 , 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,-1,-1,-1,-1,-1,-1 ,-1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1 ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 ,-1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1 ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 }; #if HTTP_PARSER_STRICT # define T(v) 0 #else # define T(v) v #endif static const uint8_t normal_url_char[32] = { /* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */ 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, /* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */ 0 | T(2) | 0 | 0 | T(16) | 0 | 0 | 0, /* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */ 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, /* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */ 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, /* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */ 0 | 2 | 4 | 0 | 16 | 32 | 64 | 128, /* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0, /* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, /* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0, }; #undef T enum state { s_dead = 1 /* important that this is > 0 */ , s_start_req_or_res , s_res_or_resp_H , s_start_res , s_res_H , s_res_HT , s_res_HTT , s_res_HTTP , s_res_http_major , s_res_http_dot , s_res_http_minor , s_res_http_end , s_res_first_status_code , s_res_status_code , s_res_status_start , s_res_status , s_res_line_almost_done , s_start_req , s_req_method , s_req_spaces_before_url , s_req_schema , s_req_schema_slash , s_req_schema_slash_slash , s_req_server_start , s_req_server , s_req_server_with_at , s_req_path , s_req_query_string_start , s_req_query_string , s_req_fragment_start , s_req_fragment , s_req_http_start , s_req_http_H , s_req_http_HT , s_req_http_HTT , s_req_http_HTTP , s_req_http_I , s_req_http_IC , s_req_http_major , s_req_http_dot , s_req_http_minor , s_req_http_end , s_req_line_almost_done , s_header_field_start , s_header_field , s_header_value_discard_ws , s_header_value_discard_ws_almost_done , s_header_value_discard_lws , s_header_value_start , s_header_value , s_header_value_lws , s_header_almost_done , s_chunk_size_start , s_chunk_size , s_chunk_parameters , s_chunk_size_almost_done , s_headers_almost_done , s_headers_done /* Important: 's_headers_done' must be the last 'header' state. All * states beyond this must be 'body' states. It is used for overflow * checking. See the PARSING_HEADER() macro. */ , s_chunk_data , s_chunk_data_almost_done , s_chunk_data_done , s_body_identity , s_body_identity_eof , s_message_done }; #define PARSING_HEADER(state) (state <= s_headers_done) enum header_states { h_general = 0 , h_C , h_CO , h_CON , h_matching_connection , h_matching_proxy_connection , h_matching_content_length , h_matching_transfer_encoding , h_matching_upgrade , h_connection , h_content_length , h_content_length_num , h_content_length_ws , h_transfer_encoding , h_upgrade , h_matching_transfer_encoding_token_start , h_matching_transfer_encoding_chunked , h_matching_transfer_encoding_token , h_matching_connection_token_start , h_matching_connection_keep_alive , h_matching_connection_close , h_matching_connection_upgrade , h_matching_connection_token , h_transfer_encoding_chunked , h_connection_keep_alive , h_connection_close , h_connection_upgrade }; enum http_host_state { s_http_host_dead = 1 , s_http_userinfo_start , s_http_userinfo , s_http_host_start , s_http_host_v6_start , s_http_host , s_http_host_v6 , s_http_host_v6_end , s_http_host_v6_zone_start , s_http_host_v6_zone , s_http_host_port_start , s_http_host_port }; /* Macros for character classes; depends on strict-mode */ #define CR '\r' #define LF '\n' #define LOWER(c) (unsigned char)(c | 0x20) #define IS_ALPHA(c) (LOWER(c) >= 'a' && LOWER(c) <= 'z') #define IS_NUM(c) ((c) >= '0' && (c) <= '9') #define IS_ALPHANUM(c) (IS_ALPHA(c) || IS_NUM(c)) #define IS_HEX(c) (IS_NUM(c) || (LOWER(c) >= 'a' && LOWER(c) <= 'f')) #define IS_MARK(c) ((c) == '-' || (c) == '_' || (c) == '.' || \ (c) == '!' || (c) == '~' || (c) == '*' || (c) == '\'' || (c) == '(' || \ (c) == ')') #define IS_USERINFO_CHAR(c) (IS_ALPHANUM(c) || IS_MARK(c) || (c) == '%' || \ (c) == ';' || (c) == ':' || (c) == '&' || (c) == '=' || (c) == '+' || \ (c) == '$' || (c) == ',') #define STRICT_TOKEN(c) ((c == ' ') ? 0 : tokens[(unsigned char)c]) #if HTTP_PARSER_STRICT #define TOKEN(c) STRICT_TOKEN(c) #define IS_URL_CHAR(c) (BIT_AT(normal_url_char, (unsigned char)c)) #define IS_HOST_CHAR(c) (IS_ALPHANUM(c) || (c) == '.' || (c) == '-') #else #define TOKEN(c) tokens[(unsigned char)c] #define IS_URL_CHAR(c) \ (BIT_AT(normal_url_char, (unsigned char)c) || ((c) & 0x80)) #define IS_HOST_CHAR(c) \ (IS_ALPHANUM(c) || (c) == '.' || (c) == '-' || (c) == '_') #endif /** * Verify that a char is a valid visible (printable) US-ASCII * character or %x80-FF **/ #define IS_HEADER_CHAR(ch) \ (ch == CR || ch == LF || ch == 9 || ((unsigned char)ch > 31 && ch != 127)) #define start_state (parser->type == HTTP_REQUEST ? s_start_req : s_start_res) #if HTTP_PARSER_STRICT # define STRICT_CHECK(cond) \ do { \ if (cond) { \ SET_ERRNO(HPE_STRICT); \ goto error; \ } \ } while (0) # define NEW_MESSAGE() (http_should_keep_alive(parser) ? start_state : s_dead) #else # define STRICT_CHECK(cond) # define NEW_MESSAGE() start_state #endif /* Map errno values to strings for human-readable output */ #define HTTP_STRERROR_GEN(n, s) { "HPE_" #n, s }, static struct { const char *name; const char *description; } http_strerror_tab[] = { HTTP_ERRNO_MAP(HTTP_STRERROR_GEN) }; #undef HTTP_STRERROR_GEN int http_message_needs_eof(const http_parser *parser); /* Our URL parser. * * This is designed to be shared by http_parser_execute() for URL validation, * hence it has a state transition + byte-for-byte interface. In addition, it * is meant to be embedded in http_parser_parse_url(), which does the dirty * work of turning state transitions URL components for its API. * * This function should only be invoked with non-space characters. It is * assumed that the caller cares about (and can detect) the transition between * URL and non-URL states by looking for these. */ static enum state parse_url_char(enum state s, const char ch) { if (ch == ' ' || ch == '\r' || ch == '\n') { return s_dead; } #if HTTP_PARSER_STRICT if (ch == '\t' || ch == '\f') { return s_dead; } #endif switch (s) { case s_req_spaces_before_url: /* Proxied requests are followed by scheme of an absolute URI (alpha). * All methods except CONNECT are followed by '/' or '*'. */ if (ch == '/' || ch == '*') { return s_req_path; } if (IS_ALPHA(ch)) { return s_req_schema; } break; case s_req_schema: if (IS_ALPHA(ch)) { return s; } if (ch == ':') { return s_req_schema_slash; } break; case s_req_schema_slash: if (ch == '/') { return s_req_schema_slash_slash; } break; case s_req_schema_slash_slash: if (ch == '/') { return s_req_server_start; } break; case s_req_server_with_at: if (ch == '@') { return s_dead; } /* fall through */ case s_req_server_start: case s_req_server: if (ch == '/') { return s_req_path; } if (ch == '?') { return s_req_query_string_start; } if (ch == '@') { return s_req_server_with_at; } if (IS_USERINFO_CHAR(ch) || ch == '[' || ch == ']') { return s_req_server; } break; case s_req_path: if (IS_URL_CHAR(ch)) { return s; } switch (ch) { case '?': return s_req_query_string_start; case '#': return s_req_fragment_start; } break; case s_req_query_string_start: case s_req_query_string: if (IS_URL_CHAR(ch)) { return s_req_query_string; } switch (ch) { case '?': /* allow extra '?' in query string */ return s_req_query_string; case '#': return s_req_fragment_start; } break; case s_req_fragment_start: if (IS_URL_CHAR(ch)) { return s_req_fragment; } switch (ch) { case '?': return s_req_fragment; case '#': return s; } break; case s_req_fragment: if (IS_URL_CHAR(ch)) { return s; } switch (ch) { case '?': case '#': return s; } break; default: break; } /* We should never fall out of the switch above unless there's an error */ return s_dead; } size_t http_parser_execute (http_parser *parser, const http_parser_settings *settings, const char *data, size_t len) { char c, ch; int8_t unhex_val; const char *p = data; const char *header_field_mark = 0; const char *header_value_mark = 0; const char *url_mark = 0; const char *body_mark = 0; const char *status_mark = 0; enum state p_state = (enum state) parser->state; const unsigned int lenient = parser->lenient_http_headers; uint32_t nread = parser->nread; /* We're in an error state. Don't bother doing anything. */ if (HTTP_PARSER_ERRNO(parser) != HPE_OK) { return 0; } if (len == 0) { switch (CURRENT_STATE()) { case s_body_identity_eof: /* Use of CALLBACK_NOTIFY() here would erroneously return 1 byte read if * we got paused. */ CALLBACK_NOTIFY_NOADVANCE(message_complete); return 0; case s_dead: case s_start_req_or_res: case s_start_res: case s_start_req: return 0; default: SET_ERRNO(HPE_INVALID_EOF_STATE); return 1; } } if (CURRENT_STATE() == s_header_field) header_field_mark = data; if (CURRENT_STATE() == s_header_value) header_value_mark = data; switch (CURRENT_STATE()) { case s_req_path: case s_req_schema: case s_req_schema_slash: case s_req_schema_slash_slash: case s_req_server_start: case s_req_server: case s_req_server_with_at: case s_req_query_string_start: case s_req_query_string: case s_req_fragment_start: case s_req_fragment: url_mark = data; break; case s_res_status: status_mark = data; break; default: break; } for (p=data; p != data + len; p++) { ch = *p; if (PARSING_HEADER(CURRENT_STATE())) COUNT_HEADER_SIZE(1); reexecute: switch (CURRENT_STATE()) { case s_dead: /* this state is used after a 'Connection: close' message * the parser will error out if it reads another message */ if (LIKELY(ch == CR || ch == LF)) break; SET_ERRNO(HPE_CLOSED_CONNECTION); goto error; case s_start_req_or_res: { if (ch == CR || ch == LF) break; parser->flags = 0; parser->extra_flags = 0; parser->content_length = ULLONG_MAX; if (ch == 'H') { UPDATE_STATE(s_res_or_resp_H); CALLBACK_NOTIFY(message_begin); } else { parser->type = HTTP_REQUEST; UPDATE_STATE(s_start_req); REEXECUTE(); } break; } case s_res_or_resp_H: if (ch == 'T') { parser->type = HTTP_RESPONSE; UPDATE_STATE(s_res_HT); } else { if (UNLIKELY(ch != 'E')) { SET_ERRNO(HPE_INVALID_CONSTANT); goto error; } parser->type = HTTP_REQUEST; parser->method = HTTP_HEAD; parser->index = 2; UPDATE_STATE(s_req_method); } break; case s_start_res: { if (ch == CR || ch == LF) break; parser->flags = 0; parser->extra_flags = 0; parser->content_length = ULLONG_MAX; if (ch == 'H') { UPDATE_STATE(s_res_H); } else { SET_ERRNO(HPE_INVALID_CONSTANT); goto error; } CALLBACK_NOTIFY(message_begin); break; } case s_res_H: STRICT_CHECK(ch != 'T'); UPDATE_STATE(s_res_HT); break; case s_res_HT: STRICT_CHECK(ch != 'T'); UPDATE_STATE(s_res_HTT); break; case s_res_HTT: STRICT_CHECK(ch != 'P'); UPDATE_STATE(s_res_HTTP); break; case s_res_HTTP: STRICT_CHECK(ch != '/'); UPDATE_STATE(s_res_http_major); break; case s_res_http_major: if (UNLIKELY(!IS_NUM(ch))) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } parser->http_major = ch - '0'; UPDATE_STATE(s_res_http_dot); break; case s_res_http_dot: { if (UNLIKELY(ch != '.')) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } UPDATE_STATE(s_res_http_minor); break; } case s_res_http_minor: if (UNLIKELY(!IS_NUM(ch))) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } parser->http_minor = ch - '0'; UPDATE_STATE(s_res_http_end); break; case s_res_http_end: { if (UNLIKELY(ch != ' ')) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } UPDATE_STATE(s_res_first_status_code); break; } case s_res_first_status_code: { if (!IS_NUM(ch)) { if (ch == ' ') { break; } SET_ERRNO(HPE_INVALID_STATUS); goto error; } parser->status_code = ch - '0'; UPDATE_STATE(s_res_status_code); break; } case s_res_status_code: { if (!IS_NUM(ch)) { switch (ch) { case ' ': UPDATE_STATE(s_res_status_start); break; case CR: case LF: UPDATE_STATE(s_res_status_start); REEXECUTE(); break; default: SET_ERRNO(HPE_INVALID_STATUS); goto error; } break; } parser->status_code *= 10; parser->status_code += ch - '0'; if (UNLIKELY(parser->status_code > 999)) { SET_ERRNO(HPE_INVALID_STATUS); goto error; } break; } case s_res_status_start: { MARK(status); UPDATE_STATE(s_res_status); parser->index = 0; if (ch == CR || ch == LF) REEXECUTE(); break; } case s_res_status: if (ch == CR) { UPDATE_STATE(s_res_line_almost_done); CALLBACK_DATA(status); break; } if (ch == LF) { UPDATE_STATE(s_header_field_start); CALLBACK_DATA(status); break; } break; case s_res_line_almost_done: STRICT_CHECK(ch != LF); UPDATE_STATE(s_header_field_start); break; case s_start_req: { if (ch == CR || ch == LF) break; parser->flags = 0; parser->extra_flags = 0; parser->content_length = ULLONG_MAX; if (UNLIKELY(!IS_ALPHA(ch))) { SET_ERRNO(HPE_INVALID_METHOD); goto error; } parser->method = (enum http_method) 0; parser->index = 1; switch (ch) { case 'A': parser->method = HTTP_ACL; break; case 'B': parser->method = HTTP_BIND; break; case 'C': parser->method = HTTP_CONNECT; /* or COPY, CHECKOUT */ break; case 'D': parser->method = HTTP_DELETE; break; case 'G': parser->method = HTTP_GET; break; case 'H': parser->method = HTTP_HEAD; break; case 'L': parser->method = HTTP_LOCK; /* or LINK */ break; case 'M': parser->method = HTTP_MKCOL; /* or MOVE, MKACTIVITY, MERGE, M-SEARCH, MKCALENDAR */ break; case 'N': parser->method = HTTP_NOTIFY; break; case 'O': parser->method = HTTP_OPTIONS; break; case 'P': parser->method = HTTP_POST; /* or PROPFIND|PROPPATCH|PUT|PATCH|PURGE */ break; case 'R': parser->method = HTTP_REPORT; /* or REBIND */ break; case 'S': parser->method = HTTP_SUBSCRIBE; /* or SEARCH, SOURCE */ break; case 'T': parser->method = HTTP_TRACE; break; case 'U': parser->method = HTTP_UNLOCK; /* or UNSUBSCRIBE, UNBIND, UNLINK */ break; default: SET_ERRNO(HPE_INVALID_METHOD); goto error; } UPDATE_STATE(s_req_method); CALLBACK_NOTIFY(message_begin); break; } case s_req_method: { const char *matcher; if (UNLIKELY(ch == '\0')) { SET_ERRNO(HPE_INVALID_METHOD); goto error; } matcher = method_strings[parser->method]; if (ch == ' ' && matcher[parser->index] == '\0') { UPDATE_STATE(s_req_spaces_before_url); } else if (ch == matcher[parser->index]) { ; /* nada */ } else if ((ch >= 'A' && ch <= 'Z') || ch == '-') { switch (parser->method << 16 | parser->index << 8 | ch) { #define XX(meth, pos, ch, new_meth) \ case (HTTP_##meth << 16 | pos << 8 | ch): \ parser->method = HTTP_##new_meth; break; XX(POST, 1, 'U', PUT) XX(POST, 1, 'A', PATCH) XX(POST, 1, 'R', PROPFIND) XX(PUT, 2, 'R', PURGE) XX(CONNECT, 1, 'H', CHECKOUT) XX(CONNECT, 2, 'P', COPY) XX(MKCOL, 1, 'O', MOVE) XX(MKCOL, 1, 'E', MERGE) XX(MKCOL, 1, '-', MSEARCH) XX(MKCOL, 2, 'A', MKACTIVITY) XX(MKCOL, 3, 'A', MKCALENDAR) XX(SUBSCRIBE, 1, 'E', SEARCH) XX(SUBSCRIBE, 1, 'O', SOURCE) XX(REPORT, 2, 'B', REBIND) XX(PROPFIND, 4, 'P', PROPPATCH) XX(LOCK, 1, 'I', LINK) XX(UNLOCK, 2, 'S', UNSUBSCRIBE) XX(UNLOCK, 2, 'B', UNBIND) XX(UNLOCK, 3, 'I', UNLINK) #undef XX default: SET_ERRNO(HPE_INVALID_METHOD); goto error; } } else { SET_ERRNO(HPE_INVALID_METHOD); goto error; } ++parser->index; break; } case s_req_spaces_before_url: { if (ch == ' ') break; MARK(url); if (parser->method == HTTP_CONNECT) { UPDATE_STATE(s_req_server_start); } UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch)); if (UNLIKELY(CURRENT_STATE() == s_dead)) { SET_ERRNO(HPE_INVALID_URL); goto error; } break; } case s_req_schema: case s_req_schema_slash: case s_req_schema_slash_slash: case s_req_server_start: { switch (ch) { /* No whitespace allowed here */ case ' ': case CR: case LF: SET_ERRNO(HPE_INVALID_URL); goto error; default: UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch)); if (UNLIKELY(CURRENT_STATE() == s_dead)) { SET_ERRNO(HPE_INVALID_URL); goto error; } } break; } case s_req_server: case s_req_server_with_at: case s_req_path: case s_req_query_string_start: case s_req_query_string: case s_req_fragment_start: case s_req_fragment: { switch (ch) { case ' ': UPDATE_STATE(s_req_http_start); CALLBACK_DATA(url); break; case CR: case LF: parser->http_major = 0; parser->http_minor = 9; UPDATE_STATE((ch == CR) ? s_req_line_almost_done : s_header_field_start); CALLBACK_DATA(url); break; default: UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch)); if (UNLIKELY(CURRENT_STATE() == s_dead)) { SET_ERRNO(HPE_INVALID_URL); goto error; } } break; } case s_req_http_start: switch (ch) { case ' ': break; case 'H': UPDATE_STATE(s_req_http_H); break; case 'I': if (parser->method == HTTP_SOURCE) { UPDATE_STATE(s_req_http_I); break; } /* fall through */ default: SET_ERRNO(HPE_INVALID_CONSTANT); goto error; } break; case s_req_http_H: STRICT_CHECK(ch != 'T'); UPDATE_STATE(s_req_http_HT); break; case s_req_http_HT: STRICT_CHECK(ch != 'T'); UPDATE_STATE(s_req_http_HTT); break; case s_req_http_HTT: STRICT_CHECK(ch != 'P'); UPDATE_STATE(s_req_http_HTTP); break; case s_req_http_I: STRICT_CHECK(ch != 'C'); UPDATE_STATE(s_req_http_IC); break; case s_req_http_IC: STRICT_CHECK(ch != 'E'); UPDATE_STATE(s_req_http_HTTP); /* Treat "ICE" as "HTTP". */ break; case s_req_http_HTTP: STRICT_CHECK(ch != '/'); UPDATE_STATE(s_req_http_major); break; case s_req_http_major: if (UNLIKELY(!IS_NUM(ch))) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } parser->http_major = ch - '0'; UPDATE_STATE(s_req_http_dot); break; case s_req_http_dot: { if (UNLIKELY(ch != '.')) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } UPDATE_STATE(s_req_http_minor); break; } case s_req_http_minor: if (UNLIKELY(!IS_NUM(ch))) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } parser->http_minor = ch - '0'; UPDATE_STATE(s_req_http_end); break; case s_req_http_end: { if (ch == CR) { UPDATE_STATE(s_req_line_almost_done); break; } if (ch == LF) { UPDATE_STATE(s_header_field_start); break; } SET_ERRNO(HPE_INVALID_VERSION); goto error; break; } /* end of request line */ case s_req_line_almost_done: { if (UNLIKELY(ch != LF)) { SET_ERRNO(HPE_LF_EXPECTED); goto error; } UPDATE_STATE(s_header_field_start); break; } case s_header_field_start: { if (ch == CR) { UPDATE_STATE(s_headers_almost_done); break; } if (ch == LF) { /* they might be just sending \n instead of \r\n so this would be * the second \n to denote the end of headers*/ UPDATE_STATE(s_headers_almost_done); REEXECUTE(); } c = TOKEN(ch); if (UNLIKELY(!c)) { SET_ERRNO(HPE_INVALID_HEADER_TOKEN); goto error; } MARK(header_field); parser->index = 0; UPDATE_STATE(s_header_field); switch (c) { case 'c': parser->header_state = h_C; break; case 'p': parser->header_state = h_matching_proxy_connection; break; case 't': parser->header_state = h_matching_transfer_encoding; break; case 'u': parser->header_state = h_matching_upgrade; break; default: parser->header_state = h_general; break; } break; } case s_header_field: { const char* start = p; for (; p != data + len; p++) { ch = *p; c = TOKEN(ch); if (!c) break; switch (parser->header_state) { case h_general: { size_t left = data + len - p; const char* pe = p + MIN(left, max_header_size); while (p+1 < pe && TOKEN(p[1])) { p++; } break; } case h_C: parser->index++; parser->header_state = (c == 'o' ? h_CO : h_general); break; case h_CO: parser->index++; parser->header_state = (c == 'n' ? h_CON : h_general); break; case h_CON: parser->index++; switch (c) { case 'n': parser->header_state = h_matching_connection; break; case 't': parser->header_state = h_matching_content_length; break; default: parser->header_state = h_general; break; } break; /* connection */ case h_matching_connection: parser->index++; if (parser->index > sizeof(CONNECTION)-1 || c != CONNECTION[parser->index]) { parser->header_state = h_general; } else if (parser->index == sizeof(CONNECTION)-2) { parser->header_state = h_connection; } break; /* proxy-connection */ case h_matching_proxy_connection: parser->index++; if (parser->index > sizeof(PROXY_CONNECTION)-1 || c != PROXY_CONNECTION[parser->index]) { parser->header_state = h_general; } else if (parser->index == sizeof(PROXY_CONNECTION)-2) { parser->header_state = h_connection; } break; /* content-length */ case h_matching_content_length: parser->index++; if (parser->index > sizeof(CONTENT_LENGTH)-1 || c != CONTENT_LENGTH[parser->index]) { parser->header_state = h_general; } else if (parser->index == sizeof(CONTENT_LENGTH)-2) { parser->header_state = h_content_length; } break; /* transfer-encoding */ case h_matching_transfer_encoding: parser->index++; if (parser->index > sizeof(TRANSFER_ENCODING)-1 || c != TRANSFER_ENCODING[parser->index]) { parser->header_state = h_general; } else if (parser->index == sizeof(TRANSFER_ENCODING)-2) { parser->header_state = h_transfer_encoding; parser->extra_flags |= F_TRANSFER_ENCODING >> 8; } break; /* upgrade */ case h_matching_upgrade: parser->index++; if (parser->index > sizeof(UPGRADE)-1 || c != UPGRADE[parser->index]) { parser->header_state = h_general; } else if (parser->index == sizeof(UPGRADE)-2) { parser->header_state = h_upgrade; } break; case h_connection: case h_content_length: case h_transfer_encoding: case h_upgrade: if (ch != ' ') parser->header_state = h_general; break; default: assert(0 && "Unknown header_state"); break; } } if (p == data + len) { --p; COUNT_HEADER_SIZE(p - start); break; } COUNT_HEADER_SIZE(p - start); if (ch == ':') { UPDATE_STATE(s_header_value_discard_ws); CALLBACK_DATA(header_field); break; } SET_ERRNO(HPE_INVALID_HEADER_TOKEN); goto error; } case s_header_value_discard_ws: if (ch == ' ' || ch == '\t') break; if (ch == CR) { UPDATE_STATE(s_header_value_discard_ws_almost_done); break; } if (ch == LF) { UPDATE_STATE(s_header_value_discard_lws); break; } /* fall through */ case s_header_value_start: { MARK(header_value); UPDATE_STATE(s_header_value); parser->index = 0; c = LOWER(ch); switch (parser->header_state) { case h_upgrade: parser->flags |= F_UPGRADE; parser->header_state = h_general; break; case h_transfer_encoding: /* looking for 'Transfer-Encoding: chunked' */ if ('c' == c) { parser->header_state = h_matching_transfer_encoding_chunked; } else { parser->header_state = h_matching_transfer_encoding_token; } break; /* Multi-value `Transfer-Encoding` header */ case h_matching_transfer_encoding_token_start: break; case h_content_length: if (UNLIKELY(!IS_NUM(ch))) { SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); goto error; } if (parser->flags & F_CONTENTLENGTH) { SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH); goto error; } parser->flags |= F_CONTENTLENGTH; parser->content_length = ch - '0'; parser->header_state = h_content_length_num; break; /* when obsolete line folding is encountered for content length * continue to the s_header_value state */ case h_content_length_ws: break; case h_connection: /* looking for 'Connection: keep-alive' */ if (c == 'k') { parser->header_state = h_matching_connection_keep_alive; /* looking for 'Connection: close' */ } else if (c == 'c') { parser->header_state = h_matching_connection_close; } else if (c == 'u') { parser->header_state = h_matching_connection_upgrade; } else { parser->header_state = h_matching_connection_token; } break; /* Multi-value `Connection` header */ case h_matching_connection_token_start: break; default: parser->header_state = h_general; break; } break; } case s_header_value: { const char* start = p; enum header_states h_state = (enum header_states) parser->header_state; for (; p != data + len; p++) { ch = *p; if (ch == CR) { UPDATE_STATE(s_header_almost_done); parser->header_state = h_state; CALLBACK_DATA(header_value); break; } if (ch == LF) { UPDATE_STATE(s_header_almost_done); COUNT_HEADER_SIZE(p - start); parser->header_state = h_state; CALLBACK_DATA_NOADVANCE(header_value); REEXECUTE(); } if (!lenient && !IS_HEADER_CHAR(ch)) { SET_ERRNO(HPE_INVALID_HEADER_TOKEN); goto error; } c = LOWER(ch); switch (h_state) { case h_general: { size_t left = data + len - p; const char* pe = p + MIN(left, max_header_size); for (; p != pe; p++) { ch = *p; if (ch == CR || ch == LF) { --p; break; } if (!lenient && !IS_HEADER_CHAR(ch)) { SET_ERRNO(HPE_INVALID_HEADER_TOKEN); goto error; } } if (p == data + len) --p; break; } case h_connection: case h_transfer_encoding: assert(0 && "Shouldn't get here."); break; case h_content_length: if (ch == ' ') break; h_state = h_content_length_num; /* fall through */ case h_content_length_num: { uint64_t t; if (ch == ' ') { h_state = h_content_length_ws; break; } if (UNLIKELY(!IS_NUM(ch))) { SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); parser->header_state = h_state; goto error; } t = parser->content_length; t *= 10; t += ch - '0'; /* Overflow? Test against a conservative limit for simplicity. */ if (UNLIKELY((ULLONG_MAX - 10) / 10 < parser->content_length)) { SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); parser->header_state = h_state; goto error; } parser->content_length = t; break; } case h_content_length_ws: if (ch == ' ') break; SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); parser->header_state = h_state; goto error; /* Transfer-Encoding: chunked */ case h_matching_transfer_encoding_token_start: /* looking for 'Transfer-Encoding: chunked' */ if ('c' == c) { h_state = h_matching_transfer_encoding_chunked; } else if (STRICT_TOKEN(c)) { /* TODO(indutny): similar code below does this, but why? * At the very least it seems to be inconsistent given that * h_matching_transfer_encoding_token does not check for * `STRICT_TOKEN` */ h_state = h_matching_transfer_encoding_token; } else if (c == ' ' || c == '\t') { /* Skip lws */ } else { h_state = h_general; } break; case h_matching_transfer_encoding_chunked: parser->index++; if (parser->index > sizeof(CHUNKED)-1 || c != CHUNKED[parser->index]) { h_state = h_matching_transfer_encoding_token; } else if (parser->index == sizeof(CHUNKED)-2) { h_state = h_transfer_encoding_chunked; } break; case h_matching_transfer_encoding_token: if (ch == ',') { h_state = h_matching_transfer_encoding_token_start; parser->index = 0; } break; case h_matching_connection_token_start: /* looking for 'Connection: keep-alive' */ if (c == 'k') { h_state = h_matching_connection_keep_alive; /* looking for 'Connection: close' */ } else if (c == 'c') { h_state = h_matching_connection_close; } else if (c == 'u') { h_state = h_matching_connection_upgrade; } else if (STRICT_TOKEN(c)) { h_state = h_matching_connection_token; } else if (c == ' ' || c == '\t') { /* Skip lws */ } else { h_state = h_general; } break; /* looking for 'Connection: keep-alive' */ case h_matching_connection_keep_alive: parser->index++; if (parser->index > sizeof(KEEP_ALIVE)-1 || c != KEEP_ALIVE[parser->index]) { h_state = h_matching_connection_token; } else if (parser->index == sizeof(KEEP_ALIVE)-2) { h_state = h_connection_keep_alive; } break; /* looking for 'Connection: close' */ case h_matching_connection_close: parser->index++; if (parser->index > sizeof(CLOSE)-1 || c != CLOSE[parser->index]) { h_state = h_matching_connection_token; } else if (parser->index == sizeof(CLOSE)-2) { h_state = h_connection_close; } break; /* looking for 'Connection: upgrade' */ case h_matching_connection_upgrade: parser->index++; if (parser->index > sizeof(UPGRADE) - 1 || c != UPGRADE[parser->index]) { h_state = h_matching_connection_token; } else if (parser->index == sizeof(UPGRADE)-2) { h_state = h_connection_upgrade; } break; case h_matching_connection_token: if (ch == ',') { h_state = h_matching_connection_token_start; parser->index = 0; } break; case h_transfer_encoding_chunked: if (ch != ' ') h_state = h_matching_transfer_encoding_token; break; case h_connection_keep_alive: case h_connection_close: case h_connection_upgrade: if (ch == ',') { if (h_state == h_connection_keep_alive) { parser->flags |= F_CONNECTION_KEEP_ALIVE; } else if (h_state == h_connection_close) { parser->flags |= F_CONNECTION_CLOSE; } else if (h_state == h_connection_upgrade) { parser->flags |= F_CONNECTION_UPGRADE; } h_state = h_matching_connection_token_start; parser->index = 0; } else if (ch != ' ') { h_state = h_matching_connection_token; } break; default: UPDATE_STATE(s_header_value); h_state = h_general; break; } } parser->header_state = h_state; if (p == data + len) --p; COUNT_HEADER_SIZE(p - start); break; } case s_header_almost_done: { if (UNLIKELY(ch != LF)) { SET_ERRNO(HPE_LF_EXPECTED); goto error; } UPDATE_STATE(s_header_value_lws); break; } case s_header_value_lws: { if (ch == ' ' || ch == '\t') { if (parser->header_state == h_content_length_num) { /* treat obsolete line folding as space */ parser->header_state = h_content_length_ws; } UPDATE_STATE(s_header_value_start); REEXECUTE(); } /* finished the header */ switch (parser->header_state) { case h_connection_keep_alive: parser->flags |= F_CONNECTION_KEEP_ALIVE; break; case h_connection_close: parser->flags |= F_CONNECTION_CLOSE; break; case h_transfer_encoding_chunked: parser->flags |= F_CHUNKED; break; case h_connection_upgrade: parser->flags |= F_CONNECTION_UPGRADE; break; default: break; } UPDATE_STATE(s_header_field_start); REEXECUTE(); } case s_header_value_discard_ws_almost_done: { STRICT_CHECK(ch != LF); UPDATE_STATE(s_header_value_discard_lws); break; } case s_header_value_discard_lws: { if (ch == ' ' || ch == '\t') { UPDATE_STATE(s_header_value_discard_ws); break; } else { switch (parser->header_state) { case h_connection_keep_alive: parser->flags |= F_CONNECTION_KEEP_ALIVE; break; case h_connection_close: parser->flags |= F_CONNECTION_CLOSE; break; case h_connection_upgrade: parser->flags |= F_CONNECTION_UPGRADE; break; case h_transfer_encoding_chunked: parser->flags |= F_CHUNKED; break; case h_content_length: /* do not allow empty content length */ SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); goto error; break; default: break; } /* header value was empty */ MARK(header_value); UPDATE_STATE(s_header_field_start); CALLBACK_DATA_NOADVANCE(header_value); REEXECUTE(); } } case s_headers_almost_done: { STRICT_CHECK(ch != LF); if (parser->flags & F_TRAILING) { /* End of a chunked request */ UPDATE_STATE(s_message_done); CALLBACK_NOTIFY_NOADVANCE(chunk_complete); REEXECUTE(); } /* Cannot us transfer-encoding and a content-length header together per the HTTP specification. (RFC 7230 Section 3.3.3) */ if ((parser->extra_flags & (F_TRANSFER_ENCODING >> 8)) && (parser->flags & F_CONTENTLENGTH)) { /* Allow it for lenient parsing as long as `Transfer-Encoding` is * not `chunked` */ if (!lenient || (parser->flags & F_CHUNKED)) { SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH); goto error; } } UPDATE_STATE(s_headers_done); /* Set this here so that on_headers_complete() callbacks can see it */ if ((parser->flags & F_UPGRADE) && (parser->flags & F_CONNECTION_UPGRADE)) { /* For responses, "Upgrade: foo" and "Connection: upgrade" are * mandatory only when it is a 101 Switching Protocols response, * otherwise it is purely informational, to announce support. */ parser->upgrade = (parser->type == HTTP_REQUEST || parser->status_code == 101); } else { parser->upgrade = (parser->method == HTTP_CONNECT); } /* Here we call the headers_complete callback. This is somewhat * different than other callbacks because if the user returns 1, we * will interpret that as saying that this message has no body. This * is needed for the annoying case of recieving a response to a HEAD * request. * * We'd like to use CALLBACK_NOTIFY_NOADVANCE() here but we cannot, so * we have to simulate it by handling a change in errno below. */ if (settings->on_headers_complete) { switch (settings->on_headers_complete(parser)) { case 0: break; case 2: parser->upgrade = 1; /* fall through */ case 1: parser->flags |= F_SKIPBODY; break; default: SET_ERRNO(HPE_CB_headers_complete); RETURN(p - data); /* Error */ } } if (HTTP_PARSER_ERRNO(parser) != HPE_OK) { RETURN(p - data); } REEXECUTE(); } case s_headers_done: { int hasBody; STRICT_CHECK(ch != LF); parser->nread = 0; nread = 0; hasBody = parser->flags & F_CHUNKED || (parser->content_length > 0 && parser->content_length != ULLONG_MAX); if (parser->upgrade && (parser->method == HTTP_CONNECT || (parser->flags & F_SKIPBODY) || !hasBody)) { /* Exit, the rest of the message is in a different protocol. */ UPDATE_STATE(NEW_MESSAGE()); CALLBACK_NOTIFY(message_complete); RETURN((p - data) + 1); } if (parser->flags & F_SKIPBODY) { UPDATE_STATE(NEW_MESSAGE()); CALLBACK_NOTIFY(message_complete); } else if (parser->flags & F_CHUNKED) { /* chunked encoding - ignore Content-Length header, * prepare for a chunk */ UPDATE_STATE(s_chunk_size_start); } else if (parser->extra_flags & (F_TRANSFER_ENCODING >> 8)) { if (parser->type == HTTP_REQUEST && !lenient) { /* RFC 7230 3.3.3 */ /* If a Transfer-Encoding header field * is present in a request and the chunked transfer coding is not * the final encoding, the message body length cannot be determined * reliably; the server MUST respond with the 400 (Bad Request) * status code and then close the connection. */ SET_ERRNO(HPE_INVALID_TRANSFER_ENCODING); RETURN(p - data); /* Error */ } else { /* RFC 7230 3.3.3 */ /* If a Transfer-Encoding header field is present in a response and * the chunked transfer coding is not the final encoding, the * message body length is determined by reading the connection until * it is closed by the server. */ UPDATE_STATE(s_body_identity_eof); } } else { if (parser->content_length == 0) { /* Content-Length header given but zero: Content-Length: 0\r\n */ UPDATE_STATE(NEW_MESSAGE()); CALLBACK_NOTIFY(message_complete); } else if (parser->content_length != ULLONG_MAX) { /* Content-Length header given and non-zero */ UPDATE_STATE(s_body_identity); } else { if (!http_message_needs_eof(parser)) { /* Assume content-length 0 - read the next */ UPDATE_STATE(NEW_MESSAGE()); CALLBACK_NOTIFY(message_complete); } else { /* Read body until EOF */ UPDATE_STATE(s_body_identity_eof); } } } break; } case s_body_identity: { uint64_t to_read = MIN(parser->content_length, (uint64_t) ((data + len) - p)); assert(parser->content_length != 0 && parser->content_length != ULLONG_MAX); /* The difference between advancing content_length and p is because * the latter will automaticaly advance on the next loop iteration. * Further, if content_length ends up at 0, we want to see the last * byte again for our message complete callback. */ MARK(body); parser->content_length -= to_read; p += to_read - 1; if (parser->content_length == 0) { UPDATE_STATE(s_message_done); /* Mimic CALLBACK_DATA_NOADVANCE() but with one extra byte. * * The alternative to doing this is to wait for the next byte to * trigger the data callback, just as in every other case. The * problem with this is that this makes it difficult for the test * harness to distinguish between complete-on-EOF and * complete-on-length. It's not clear that this distinction is * important for applications, but let's keep it for now. */ CALLBACK_DATA_(body, p - body_mark + 1, p - data); REEXECUTE(); } break; } /* read until EOF */ case s_body_identity_eof: MARK(body); p = data + len - 1; break; case s_message_done: UPDATE_STATE(NEW_MESSAGE()); CALLBACK_NOTIFY(message_complete); if (parser->upgrade) { /* Exit, the rest of the message is in a different protocol. */ RETURN((p - data) + 1); } break; case s_chunk_size_start: { assert(nread == 1); assert(parser->flags & F_CHUNKED); unhex_val = unhex[(unsigned char)ch]; if (UNLIKELY(unhex_val == -1)) { SET_ERRNO(HPE_INVALID_CHUNK_SIZE); goto error; } parser->content_length = unhex_val; UPDATE_STATE(s_chunk_size); break; } case s_chunk_size: { uint64_t t; assert(parser->flags & F_CHUNKED); if (ch == CR) { UPDATE_STATE(s_chunk_size_almost_done); break; } unhex_val = unhex[(unsigned char)ch]; if (unhex_val == -1) { if (ch == ';' || ch == ' ') { UPDATE_STATE(s_chunk_parameters); break; } SET_ERRNO(HPE_INVALID_CHUNK_SIZE); goto error; } t = parser->content_length; t *= 16; t += unhex_val; /* Overflow? Test against a conservative limit for simplicity. */ if (UNLIKELY((ULLONG_MAX - 16) / 16 < parser->content_length)) { SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); goto error; } parser->content_length = t; break; } case s_chunk_parameters: { assert(parser->flags & F_CHUNKED); /* just ignore this shit. TODO check for overflow */ if (ch == CR) { UPDATE_STATE(s_chunk_size_almost_done); break; } break; } case s_chunk_size_almost_done: { assert(parser->flags & F_CHUNKED); STRICT_CHECK(ch != LF); parser->nread = 0; nread = 0; if (parser->content_length == 0) { parser->flags |= F_TRAILING; UPDATE_STATE(s_header_field_start); } else { UPDATE_STATE(s_chunk_data); } CALLBACK_NOTIFY(chunk_header); break; } case s_chunk_data: { uint64_t to_read = MIN(parser->content_length, (uint64_t) ((data + len) - p)); assert(parser->flags & F_CHUNKED); assert(parser->content_length != 0 && parser->content_length != ULLONG_MAX); /* See the explanation in s_body_identity for why the content * length and data pointers are managed this way. */ MARK(body); parser->content_length -= to_read; p += to_read - 1; if (parser->content_length == 0) { UPDATE_STATE(s_chunk_data_almost_done); } break; } case s_chunk_data_almost_done: assert(parser->flags & F_CHUNKED); assert(parser->content_length == 0); STRICT_CHECK(ch != CR); UPDATE_STATE(s_chunk_data_done); CALLBACK_DATA(body); break; case s_chunk_data_done: assert(parser->flags & F_CHUNKED); STRICT_CHECK(ch != LF); parser->nread = 0; nread = 0; UPDATE_STATE(s_chunk_size_start); CALLBACK_NOTIFY(chunk_complete); break; default: assert(0 && "unhandled state"); SET_ERRNO(HPE_INVALID_INTERNAL_STATE); goto error; } } /* Run callbacks for any marks that we have leftover after we ran out of * bytes. There should be at most one of these set, so it's OK to invoke * them in series (unset marks will not result in callbacks). * * We use the NOADVANCE() variety of callbacks here because 'p' has already * overflowed 'data' and this allows us to correct for the off-by-one that * we'd otherwise have (since CALLBACK_DATA() is meant to be run with a 'p' * value that's in-bounds). */ assert(((header_field_mark ? 1 : 0) + (header_value_mark ? 1 : 0) + (url_mark ? 1 : 0) + (body_mark ? 1 : 0) + (status_mark ? 1 : 0)) <= 1); CALLBACK_DATA_NOADVANCE(header_field); CALLBACK_DATA_NOADVANCE(header_value); CALLBACK_DATA_NOADVANCE(url); CALLBACK_DATA_NOADVANCE(body); CALLBACK_DATA_NOADVANCE(status); RETURN(len); error: if (HTTP_PARSER_ERRNO(parser) == HPE_OK) { SET_ERRNO(HPE_UNKNOWN); } RETURN(p - data); } /* Does the parser need to see an EOF to find the end of the message? */ int http_message_needs_eof (const http_parser *parser) { if (parser->type == HTTP_REQUEST) { return 0; } /* See RFC 2616 section 4.4 */ if (parser->status_code / 100 == 1 || /* 1xx e.g. Continue */ parser->status_code == 204 || /* No Content */ parser->status_code == 304 || /* Not Modified */ parser->flags & F_SKIPBODY) { /* response to a HEAD request */ return 0; } /* RFC 7230 3.3.3, see `s_headers_almost_done` */ if ((parser->extra_flags & (F_TRANSFER_ENCODING >> 8)) && (parser->flags & F_CHUNKED) == 0) { return 1; } if ((parser->flags & F_CHUNKED) || parser->content_length != ULLONG_MAX) { return 0; } return 1; } int http_should_keep_alive (const http_parser *parser) { if (parser->http_major > 0 && parser->http_minor > 0) { /* HTTP/1.1 */ if (parser->flags & F_CONNECTION_CLOSE) { return 0; } } else { /* HTTP/1.0 or earlier */ if (!(parser->flags & F_CONNECTION_KEEP_ALIVE)) { return 0; } } return !http_message_needs_eof(parser); } const char * http_method_str (enum http_method m) { return ELEM_AT(method_strings, m, ""); } const char * http_status_str (enum http_status s) { switch (s) { #define XX(num, name, string) case HTTP_STATUS_##name: return #string; HTTP_STATUS_MAP(XX) #undef XX default: return ""; } } void http_parser_init (http_parser *parser, enum http_parser_type t) { void *data = parser->data; /* preserve application data */ memset(parser, 0, sizeof(*parser)); parser->data = data; parser->type = t; parser->state = (t == HTTP_REQUEST ? s_start_req : (t == HTTP_RESPONSE ? s_start_res : s_start_req_or_res)); parser->http_errno = HPE_OK; } void http_parser_settings_init(http_parser_settings *settings) { memset(settings, 0, sizeof(*settings)); } const char * http_errno_name(enum http_errno err) { assert(((size_t) err) < ARRAY_SIZE(http_strerror_tab)); return http_strerror_tab[err].name; } const char * http_errno_description(enum http_errno err) { assert(((size_t) err) < ARRAY_SIZE(http_strerror_tab)); return http_strerror_tab[err].description; } static enum http_host_state http_parse_host_char(enum http_host_state s, const char ch) { switch(s) { case s_http_userinfo: case s_http_userinfo_start: if (ch == '@') { return s_http_host_start; } if (IS_USERINFO_CHAR(ch)) { return s_http_userinfo; } break; case s_http_host_start: if (ch == '[') { return s_http_host_v6_start; } if (IS_HOST_CHAR(ch)) { return s_http_host; } break; case s_http_host: if (IS_HOST_CHAR(ch)) { return s_http_host; } /* fall through */ case s_http_host_v6_end: if (ch == ':') { return s_http_host_port_start; } break; case s_http_host_v6: if (ch == ']') { return s_http_host_v6_end; } /* fall through */ case s_http_host_v6_start: if (IS_HEX(ch) || ch == ':' || ch == '.') { return s_http_host_v6; } if (s == s_http_host_v6 && ch == '%') { return s_http_host_v6_zone_start; } break; case s_http_host_v6_zone: if (ch == ']') { return s_http_host_v6_end; } /* fall through */ case s_http_host_v6_zone_start: /* RFC 6874 Zone ID consists of 1*( unreserved / pct-encoded) */ if (IS_ALPHANUM(ch) || ch == '%' || ch == '.' || ch == '-' || ch == '_' || ch == '~') { return s_http_host_v6_zone; } break; case s_http_host_port: case s_http_host_port_start: if (IS_NUM(ch)) { return s_http_host_port; } break; default: break; } return s_http_host_dead; } static int http_parse_host(const char * buf, struct http_parser_url *u, int found_at) { enum http_host_state s; const char *p; size_t buflen = u->field_data[UF_HOST].off + u->field_data[UF_HOST].len; assert(u->field_set & (1 << UF_HOST)); u->field_data[UF_HOST].len = 0; s = found_at ? s_http_userinfo_start : s_http_host_start; for (p = buf + u->field_data[UF_HOST].off; p < buf + buflen; p++) { enum http_host_state new_s = http_parse_host_char(s, *p); if (new_s == s_http_host_dead) { return 1; } switch(new_s) { case s_http_host: if (s != s_http_host) { u->field_data[UF_HOST].off = (uint16_t)(p - buf); } u->field_data[UF_HOST].len++; break; case s_http_host_v6: if (s != s_http_host_v6) { u->field_data[UF_HOST].off = (uint16_t)(p - buf); } u->field_data[UF_HOST].len++; break; case s_http_host_v6_zone_start: case s_http_host_v6_zone: u->field_data[UF_HOST].len++; break; case s_http_host_port: if (s != s_http_host_port) { u->field_data[UF_PORT].off = (uint16_t)(p - buf); u->field_data[UF_PORT].len = 0; u->field_set |= (1 << UF_PORT); } u->field_data[UF_PORT].len++; break; case s_http_userinfo: if (s != s_http_userinfo) { u->field_data[UF_USERINFO].off = (uint16_t)(p - buf); u->field_data[UF_USERINFO].len = 0; u->field_set |= (1 << UF_USERINFO); } u->field_data[UF_USERINFO].len++; break; default: break; } s = new_s; } /* Make sure we don't end somewhere unexpected */ switch (s) { case s_http_host_start: case s_http_host_v6_start: case s_http_host_v6: case s_http_host_v6_zone_start: case s_http_host_v6_zone: case s_http_host_port_start: case s_http_userinfo: case s_http_userinfo_start: return 1; default: break; } return 0; } void http_parser_url_init(struct http_parser_url *u) { memset(u, 0, sizeof(*u)); } int http_parser_parse_url(const char *buf, size_t buflen, int is_connect, struct http_parser_url *u) { enum state s; const char *p; enum http_parser_url_fields uf, old_uf; int found_at = 0; if (buflen == 0) { return 1; } u->port = u->field_set = 0; s = is_connect ? s_req_server_start : s_req_spaces_before_url; old_uf = UF_MAX; for (p = buf; p < buf + buflen; p++) { s = parse_url_char(s, *p); /* Figure out the next field that we're operating on */ switch (s) { case s_dead: return 1; /* Skip delimeters */ case s_req_schema_slash: case s_req_schema_slash_slash: case s_req_server_start: case s_req_query_string_start: case s_req_fragment_start: continue; case s_req_schema: uf = UF_SCHEMA; break; case s_req_server_with_at: found_at = 1; /* fall through */ case s_req_server: uf = UF_HOST; break; case s_req_path: uf = UF_PATH; break; case s_req_query_string: uf = UF_QUERY; break; case s_req_fragment: uf = UF_FRAGMENT; break; default: assert(!"Unexpected state"); return 1; } /* Nothing's changed; soldier on */ if (uf == old_uf) { u->field_data[uf].len++; continue; } u->field_data[uf].off = (uint16_t)(p - buf); u->field_data[uf].len = 1; u->field_set |= (1 << uf); old_uf = uf; } /* host must be present if there is a schema */ /* parsing http:///toto will fail */ if ((u->field_set & (1 << UF_SCHEMA)) && (u->field_set & (1 << UF_HOST)) == 0) { return 1; } if (u->field_set & (1 << UF_HOST)) { if (http_parse_host(buf, u, found_at) != 0) { return 1; } } /* CONNECT requests can only contain "hostname:port" */ if (is_connect && u->field_set != ((1 << UF_HOST)|(1 << UF_PORT))) { return 1; } if (u->field_set & (1 << UF_PORT)) { uint16_t off; uint16_t len; const char* p; const char* end; unsigned long v; off = u->field_data[UF_PORT].off; len = u->field_data[UF_PORT].len; end = buf + off + len; /* NOTE: The characters are already validated and are in the [0-9] range */ assert((size_t) (off + len) <= buflen && "Port number overflow"); v = 0; for (p = buf + off; p < end; p++) { v *= 10; v += *p - '0'; /* Ports have a max value of 2^16 */ if (v > 0xffff) { return 1; } } u->port = (uint16_t) v; } return 0; } void http_parser_pause(http_parser *parser, int paused) { /* Users should only be pausing/unpausing a parser that is not in an error * state. In non-debug builds, there's not much that we can do about this * other than ignore it. */ if (HTTP_PARSER_ERRNO(parser) == HPE_OK || HTTP_PARSER_ERRNO(parser) == HPE_PAUSED) { uint32_t nread = parser->nread; /* used by the SET_ERRNO macro */ SET_ERRNO((paused) ? HPE_PAUSED : HPE_OK); } else { assert(0 && "Attempting to pause parser in error state"); } } int http_body_is_final(const struct http_parser *parser) { return parser->state == s_message_done; } unsigned long http_parser_version(void) { return HTTP_PARSER_VERSION_MAJOR * 0x10000 | HTTP_PARSER_VERSION_MINOR * 0x00100 | HTTP_PARSER_VERSION_PATCH * 0x00001; } void http_parser_set_max_header_size(uint32_t size) { max_header_size = size; } sane-airscan-0.99.35/http_parser.h000066400000000000000000000457551500411437100167650ustar00rootroot00000000000000/* Copyright Joyent, Inc. and other Node contributors. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ #ifndef http_parser_h #define http_parser_h #ifdef __cplusplus extern "C" { #endif /* Also update SONAME in the Makefile whenever you change these. */ #define HTTP_PARSER_VERSION_MAJOR 2 #define HTTP_PARSER_VERSION_MINOR 9 #define HTTP_PARSER_VERSION_PATCH 4 #include #if defined(_WIN32) && !defined(__MINGW32__) && \ (!defined(_MSC_VER) || _MSC_VER<1600) && !defined(__WINE__) #include typedef __int8 int8_t; typedef unsigned __int8 uint8_t; typedef __int16 int16_t; typedef unsigned __int16 uint16_t; typedef __int32 int32_t; typedef unsigned __int32 uint32_t; typedef __int64 int64_t; typedef unsigned __int64 uint64_t; #elif (defined(__sun) || defined(__sun__)) && defined(__SunOS_5_9) #include #else #include #endif /* Compile with -DHTTP_PARSER_STRICT=0 to make less checks, but run * faster */ #ifndef HTTP_PARSER_STRICT # define HTTP_PARSER_STRICT 1 #endif /* Maximium header size allowed. If the macro is not defined * before including this header then the default is used. To * change the maximum header size, define the macro in the build * environment (e.g. -DHTTP_MAX_HEADER_SIZE=). To remove * the effective limit on the size of the header, define the macro * to a very large number (e.g. -DHTTP_MAX_HEADER_SIZE=0x7fffffff) */ #ifndef HTTP_MAX_HEADER_SIZE # define HTTP_MAX_HEADER_SIZE (80*1024) #endif typedef struct http_parser http_parser; typedef struct http_parser_settings http_parser_settings; /* Callbacks should return non-zero to indicate an error. The parser will * then halt execution. * * The one exception is on_headers_complete. In a HTTP_RESPONSE parser * returning '1' from on_headers_complete will tell the parser that it * should not expect a body. This is used when receiving a response to a * HEAD request which may contain 'Content-Length' or 'Transfer-Encoding: * chunked' headers that indicate the presence of a body. * * Returning `2` from on_headers_complete will tell parser that it should not * expect neither a body nor any futher responses on this connection. This is * useful for handling responses to a CONNECT request which may not contain * `Upgrade` or `Connection: upgrade` headers. * * http_data_cb does not return data chunks. It will be called arbitrarily * many times for each string. E.G. you might get 10 callbacks for "on_url" * each providing just a few characters more data. */ typedef int (*http_data_cb) (http_parser*, const char *at, size_t length); typedef int (*http_cb) (http_parser*); /* Status Codes */ #define HTTP_STATUS_MAP(XX) \ XX(100, CONTINUE, Continue) \ XX(101, SWITCHING_PROTOCOLS, Switching Protocols) \ XX(102, PROCESSING, Processing) \ XX(200, OK, OK) \ XX(201, CREATED, Created) \ XX(202, ACCEPTED, Accepted) \ XX(203, NON_AUTHORITATIVE_INFORMATION, Non-Authoritative Information) \ XX(204, NO_CONTENT, No Content) \ XX(205, RESET_CONTENT, Reset Content) \ XX(206, PARTIAL_CONTENT, Partial Content) \ XX(207, MULTI_STATUS, Multi-Status) \ XX(208, ALREADY_REPORTED, Already Reported) \ XX(226, IM_USED, IM Used) \ XX(300, MULTIPLE_CHOICES, Multiple Choices) \ XX(301, MOVED_PERMANENTLY, Moved Permanently) \ XX(302, FOUND, Found) \ XX(303, SEE_OTHER, See Other) \ XX(304, NOT_MODIFIED, Not Modified) \ XX(305, USE_PROXY, Use Proxy) \ XX(307, TEMPORARY_REDIRECT, Temporary Redirect) \ XX(308, PERMANENT_REDIRECT, Permanent Redirect) \ XX(400, BAD_REQUEST, Bad Request) \ XX(401, UNAUTHORIZED, Unauthorized) \ XX(402, PAYMENT_REQUIRED, Payment Required) \ XX(403, FORBIDDEN, Forbidden) \ XX(404, NOT_FOUND, Not Found) \ XX(405, METHOD_NOT_ALLOWED, Method Not Allowed) \ XX(406, NOT_ACCEPTABLE, Not Acceptable) \ XX(407, PROXY_AUTHENTICATION_REQUIRED, Proxy Authentication Required) \ XX(408, REQUEST_TIMEOUT, Request Timeout) \ XX(409, CONFLICT, Conflict) \ XX(410, GONE, Gone) \ XX(411, LENGTH_REQUIRED, Length Required) \ XX(412, PRECONDITION_FAILED, Precondition Failed) \ XX(413, PAYLOAD_TOO_LARGE, Payload Too Large) \ XX(414, URI_TOO_LONG, URI Too Long) \ XX(415, UNSUPPORTED_MEDIA_TYPE, Unsupported Media Type) \ XX(416, RANGE_NOT_SATISFIABLE, Range Not Satisfiable) \ XX(417, EXPECTATION_FAILED, Expectation Failed) \ XX(421, MISDIRECTED_REQUEST, Misdirected Request) \ XX(422, UNPROCESSABLE_ENTITY, Unprocessable Entity) \ XX(423, LOCKED, Locked) \ XX(424, FAILED_DEPENDENCY, Failed Dependency) \ XX(426, UPGRADE_REQUIRED, Upgrade Required) \ XX(428, PRECONDITION_REQUIRED, Precondition Required) \ XX(429, TOO_MANY_REQUESTS, Too Many Requests) \ XX(431, REQUEST_HEADER_FIELDS_TOO_LARGE, Request Header Fields Too Large) \ XX(451, UNAVAILABLE_FOR_LEGAL_REASONS, Unavailable For Legal Reasons) \ XX(500, INTERNAL_SERVER_ERROR, Internal Server Error) \ XX(501, NOT_IMPLEMENTED, Not Implemented) \ XX(502, BAD_GATEWAY, Bad Gateway) \ XX(503, SERVICE_UNAVAILABLE, Service Unavailable) \ XX(504, GATEWAY_TIMEOUT, Gateway Timeout) \ XX(505, HTTP_VERSION_NOT_SUPPORTED, HTTP Version Not Supported) \ XX(506, VARIANT_ALSO_NEGOTIATES, Variant Also Negotiates) \ XX(507, INSUFFICIENT_STORAGE, Insufficient Storage) \ XX(508, LOOP_DETECTED, Loop Detected) \ XX(510, NOT_EXTENDED, Not Extended) \ XX(511, NETWORK_AUTHENTICATION_REQUIRED, Network Authentication Required) \ enum http_status { #define XX(num, name, string) HTTP_STATUS_##name = num, HTTP_STATUS_MAP(XX) #undef XX }; /* Request Methods */ #define HTTP_METHOD_MAP(XX) \ XX(0, DELETE, DELETE) \ XX(1, GET, GET) \ XX(2, HEAD, HEAD) \ XX(3, POST, POST) \ XX(4, PUT, PUT) \ /* pathological */ \ XX(5, CONNECT, CONNECT) \ XX(6, OPTIONS, OPTIONS) \ XX(7, TRACE, TRACE) \ /* WebDAV */ \ XX(8, COPY, COPY) \ XX(9, LOCK, LOCK) \ XX(10, MKCOL, MKCOL) \ XX(11, MOVE, MOVE) \ XX(12, PROPFIND, PROPFIND) \ XX(13, PROPPATCH, PROPPATCH) \ XX(14, SEARCH, SEARCH) \ XX(15, UNLOCK, UNLOCK) \ XX(16, BIND, BIND) \ XX(17, REBIND, REBIND) \ XX(18, UNBIND, UNBIND) \ XX(19, ACL, ACL) \ /* subversion */ \ XX(20, REPORT, REPORT) \ XX(21, MKACTIVITY, MKACTIVITY) \ XX(22, CHECKOUT, CHECKOUT) \ XX(23, MERGE, MERGE) \ /* upnp */ \ XX(24, MSEARCH, M-SEARCH) \ XX(25, NOTIFY, NOTIFY) \ XX(26, SUBSCRIBE, SUBSCRIBE) \ XX(27, UNSUBSCRIBE, UNSUBSCRIBE) \ /* RFC-5789 */ \ XX(28, PATCH, PATCH) \ XX(29, PURGE, PURGE) \ /* CalDAV */ \ XX(30, MKCALENDAR, MKCALENDAR) \ /* RFC-2068, section 19.6.1.2 */ \ XX(31, LINK, LINK) \ XX(32, UNLINK, UNLINK) \ /* icecast */ \ XX(33, SOURCE, SOURCE) \ enum http_method { #define XX(num, name, string) HTTP_##name = num, HTTP_METHOD_MAP(XX) #undef XX }; enum http_parser_type { HTTP_REQUEST, HTTP_RESPONSE, HTTP_BOTH }; /* Flag values for http_parser.flags field */ enum flags { F_CHUNKED = 1 << 0 , F_CONNECTION_KEEP_ALIVE = 1 << 1 , F_CONNECTION_CLOSE = 1 << 2 , F_CONNECTION_UPGRADE = 1 << 3 , F_TRAILING = 1 << 4 , F_UPGRADE = 1 << 5 , F_SKIPBODY = 1 << 6 , F_CONTENTLENGTH = 1 << 7 , F_TRANSFER_ENCODING = 1 << 8 /* Never set in http_parser.flags */ }; /* Map for errno-related constants * * The provided argument should be a macro that takes 2 arguments. */ #define HTTP_ERRNO_MAP(XX) \ /* No error */ \ XX(OK, "success") \ \ /* Callback-related errors */ \ XX(CB_message_begin, "the on_message_begin callback failed") \ XX(CB_url, "the on_url callback failed") \ XX(CB_header_field, "the on_header_field callback failed") \ XX(CB_header_value, "the on_header_value callback failed") \ XX(CB_headers_complete, "the on_headers_complete callback failed") \ XX(CB_body, "the on_body callback failed") \ XX(CB_message_complete, "the on_message_complete callback failed") \ XX(CB_status, "the on_status callback failed") \ XX(CB_chunk_header, "the on_chunk_header callback failed") \ XX(CB_chunk_complete, "the on_chunk_complete callback failed") \ \ /* Parsing-related errors */ \ XX(INVALID_EOF_STATE, "stream ended at an unexpected time") \ XX(HEADER_OVERFLOW, \ "too many header bytes seen; overflow detected") \ XX(CLOSED_CONNECTION, \ "data received after completed connection: close message") \ XX(INVALID_VERSION, "invalid HTTP version") \ XX(INVALID_STATUS, "invalid HTTP status code") \ XX(INVALID_METHOD, "invalid HTTP method") \ XX(INVALID_URL, "invalid URL") \ XX(INVALID_HOST, "invalid host") \ XX(INVALID_PORT, "invalid port") \ XX(INVALID_PATH, "invalid path") \ XX(INVALID_QUERY_STRING, "invalid query string") \ XX(INVALID_FRAGMENT, "invalid fragment") \ XX(LF_EXPECTED, "LF character expected") \ XX(INVALID_HEADER_TOKEN, "invalid character in header") \ XX(INVALID_CONTENT_LENGTH, \ "invalid character in content-length header") \ XX(UNEXPECTED_CONTENT_LENGTH, \ "unexpected content-length header") \ XX(INVALID_CHUNK_SIZE, \ "invalid character in chunk size header") \ XX(INVALID_CONSTANT, "invalid constant string") \ XX(INVALID_INTERNAL_STATE, "encountered unexpected internal state")\ XX(STRICT, "strict mode assertion failed") \ XX(PAUSED, "parser is paused") \ XX(UNKNOWN, "an unknown error occurred") \ XX(INVALID_TRANSFER_ENCODING, \ "request has invalid transfer-encoding") \ /* Define HPE_* values for each errno value above */ #define HTTP_ERRNO_GEN(n, s) HPE_##n, enum http_errno { HTTP_ERRNO_MAP(HTTP_ERRNO_GEN) }; #undef HTTP_ERRNO_GEN /* Get an http_errno value from an http_parser */ #define HTTP_PARSER_ERRNO(p) ((enum http_errno) (p)->http_errno) struct http_parser { /** PRIVATE **/ unsigned int type : 2; /* enum http_parser_type */ unsigned int flags : 8; /* F_* values from 'flags' enum; semi-public */ unsigned int state : 7; /* enum state from http_parser.c */ unsigned int header_state : 7; /* enum header_state from http_parser.c */ unsigned int index : 5; /* index into current matcher */ unsigned int extra_flags : 2; unsigned int lenient_http_headers : 1; uint32_t nread; /* # bytes read in various scenarios */ uint64_t content_length; /* # bytes in body. `(uint64_t) -1` (all bits one) * if no Content-Length header. */ /** READ-ONLY **/ unsigned short http_major; unsigned short http_minor; unsigned int status_code : 16; /* responses only */ unsigned int method : 8; /* requests only */ unsigned int http_errno : 7; /* 1 = Upgrade header was present and the parser has exited because of that. * 0 = No upgrade header present. * Should be checked when http_parser_execute() returns in addition to * error checking. */ unsigned int upgrade : 1; /** PUBLIC **/ void *data; /* A pointer to get hook to the "connection" or "socket" object */ }; struct http_parser_settings { http_cb on_message_begin; http_data_cb on_url; http_data_cb on_status; http_data_cb on_header_field; http_data_cb on_header_value; http_cb on_headers_complete; http_data_cb on_body; http_cb on_message_complete; /* When on_chunk_header is called, the current chunk length is stored * in parser->content_length. */ http_cb on_chunk_header; http_cb on_chunk_complete; }; enum http_parser_url_fields { UF_SCHEMA = 0 , UF_HOST = 1 , UF_PORT = 2 , UF_PATH = 3 , UF_QUERY = 4 , UF_FRAGMENT = 5 , UF_USERINFO = 6 , UF_MAX = 7 }; /* Result structure for http_parser_parse_url(). * * Callers should index into field_data[] with UF_* values iff field_set * has the relevant (1 << UF_*) bit set. As a courtesy to clients (and * because we probably have padding left over), we convert any port to * a uint16_t. */ struct http_parser_url { uint16_t field_set; /* Bitmask of (1 << UF_*) values */ uint16_t port; /* Converted UF_PORT string */ struct { uint16_t off; /* Offset into buffer in which field starts */ uint16_t len; /* Length of run in buffer */ } field_data[UF_MAX]; }; /* Returns the library version. Bits 16-23 contain the major version number, * bits 8-15 the minor version number and bits 0-7 the patch level. * Usage example: * * unsigned long version = http_parser_version(); * unsigned major = (version >> 16) & 255; * unsigned minor = (version >> 8) & 255; * unsigned patch = version & 255; * printf("http_parser v%u.%u.%u\n", major, minor, patch); */ unsigned long http_parser_version(void); void http_parser_init(http_parser *parser, enum http_parser_type type); /* Initialize http_parser_settings members to 0 */ void http_parser_settings_init(http_parser_settings *settings); /* Executes the parser. Returns number of parsed bytes. Sets * `parser->http_errno` on error. */ size_t http_parser_execute(http_parser *parser, const http_parser_settings *settings, const char *data, size_t len); /* If http_should_keep_alive() in the on_headers_complete or * on_message_complete callback returns 0, then this should be * the last message on the connection. * If you are the server, respond with the "Connection: close" header. * If you are the client, close the connection. */ int http_should_keep_alive(const http_parser *parser); /* Returns a string version of the HTTP method. */ const char *http_method_str(enum http_method m); /* Returns a string version of the HTTP status code. */ const char *http_status_str(enum http_status s); /* Return a string name of the given error */ const char *http_errno_name(enum http_errno err); /* Return a string description of the given error */ const char *http_errno_description(enum http_errno err); /* Initialize all http_parser_url members to 0 */ void http_parser_url_init(struct http_parser_url *u); /* Parse a URL; return nonzero on failure */ int http_parser_parse_url(const char *buf, size_t buflen, int is_connect, struct http_parser_url *u); /* Pause or un-pause the parser; a nonzero value pauses */ void http_parser_pause(http_parser *parser, int paused); /* Checks if this is the final chunk of the body. */ int http_body_is_final(const http_parser *parser); /* Change the maximum header size provided at compile time. */ void http_parser_set_max_header_size(uint32_t size); #ifdef __cplusplus } #endif #endif sane-airscan-0.99.35/index.md000066400000000000000000000014541500411437100156760ustar00rootroot00000000000000# sane-airscan -- Linux support of Apple AirScan (eSCL) compatible document scanners Currently many new document scanners and MFPs come with Apple AirScan support, also known as AirPrint scanning or eSCL protocol. And number of AirScan-compatible devices tends to grow. Looks that AirScan becomes de-facto standard protocol for document scanners, connected to the network. This is very convenient for (Apple) users, because installation of the new scanner becomes trivial task, everything "just works" without need to worry about device drivers, network configuration etc. Unfortunately, Linux doesn't support AirScan, and the goal of this project is to fix this situation. For more information and software downloads, please visit the [Project's page at GitHub](https://github.com/alexpevzner/sane-airscan) sane-airscan-0.99.35/meson.build000066400000000000000000000042171500411437100164070ustar00rootroot00000000000000project('sane-airscan', 'c', 'cpp') sources = [ 'airscan-array.c', 'airscan-bmp.c', 'airscan-conf.c', 'airscan-devcaps.c', 'airscan-device.c', 'airscan-devid.c', 'airscan-devops.c', 'airscan-eloop.c', 'airscan-escl.c', 'airscan-filter.c', 'airscan-http.c', 'airscan-id.c', 'airscan-image.c', 'airscan-inifile.c', 'airscan-init.c', 'airscan-ip.c', 'airscan-jpeg.c', 'airscan-log.c', 'airscan-math.c', 'airscan-mdns.c', 'airscan-memstr.c', 'airscan-netif.c', 'airscan-os.c', 'airscan-png.c', 'airscan-pollable.c', 'airscan-rand.c', 'airscan-trace.c', 'airscan-tiff.c', 'airscan-uuid.c', 'airscan-wsd.c', 'airscan-wsdd.c', 'airscan-xml.c', 'airscan-zeroconf.c', 'airscan.c', 'http_parser.c', 'sane_strstatus.c', ] cc = meson.get_compiler('c') m_dep = cc.find_library('m', required : false) shared_deps = [ m_dep, dependency('avahi-client'), dependency('gnutls'), dependency('libjpeg'), dependency('libpng'), dependency('libtiff-4'), dependency('libxml-2.0'), dependency('threads'), ] shared_library( meson.project_name(), sources, version: '1', dependencies: shared_deps, link_args : [ '-Wl,-z,nodelete', '-Wl,--version-script=' + join_paths(meson.current_source_dir(), 'airscan.sym') ], link_depends : [ 'airscan.sym' ], install : true, install_dir : join_paths(get_option('libdir'), 'sane') ) executable( 'airscan-discover', sources + ['discover.c'], dependencies: shared_deps, install: true ) dll_file = configure_file( input : 'dll.conf', output: 'airscan', copy: true ) foreach fuzzer : ['query', 'uri', 'xml'] executable( 'fuzzer-' + fuzzer, sources + ['fuzzer/@0@.cc'.format(fuzzer)], dependencies: shared_deps, build_by_default: false, cpp_args: ['-fsanitize=address', '-fsanitize=fuzzer-no-link'], link_args: ['-fsanitize=address', '-fsanitize=fuzzer'] ) endforeach install_man('sane-airscan.5') install_man('airscan-discover.1') install_data('airscan.conf', install_dir: join_paths(get_option('sysconfdir'), 'sane.d') ) install_data(dll_file, install_dir: join_paths(get_option('sysconfdir'), 'sane.d', 'dll.d') ) sane-airscan-0.99.35/sane-airscan.5000066400000000000000000000213231500411437100166740ustar00rootroot00000000000000.\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 .TH "SANE\-AIRSCAN" "5" "April 2025" "" "AirScan (eSCL) and WSD SANE backend" .SH "NAME" \fBsane\-airscan\fR \- SANE backend for AirScan (eSCL) and WSD scanners and MFP .SH "DESCRIPTION" The \fBsane\-airscan\fR is the universal backend for "driverless" document scanning\. Currently it supports two protocols: .IP "" 4 .nf 1\. eSCL, also known as AirScan or AirPrint scan 2\. WSD, also known as WS\-Scan .fi .IP "" 0 .SH "CONFIGURATION" The sane\-airscan loads its configuration files from the following places: .IP "" 4 .nf 1\. /etc/sane\.d/airscan\.conf 2\. /etc/sane\.d/airscan\.d/* .fi .IP "" 0 .P The configuration file syntax is very similar to the \.INI file syntax\. It consist of sections, each section contains some variables\. Comments are started from # or ; characters and continies until end of line .IP "" 4 .nf # This is a comment [section 1] variable 1 = value 1 ; and another comment variable 2 = value 2 .fi .IP "" 0 .P Leading and trailing spaces of variable name and value are striped\. If you want to preserve them, put name or value into quotes ("like this")\. .SH "CONFIGURATION OF DEVICES" If scanner and computer are connected to the same LAN segment, everything expected to "just work" out of box, without any need of manual configuration\. .P However, in some cases manual configuration can be useful\. For example: .IP "" 4 .nf 1\. If computer and scanner are connected via IP router 2\. There are a lot of devices on a corporate network, but only few of them are interesting 3\. Automatic discovery works unreliable .fi .IP "" 0 .P To manually configure a device, add the following section to the configuration file: .IP "" 4 .nf [devices] "Kyocera eSCL" = http://192\.168\.1\.102:9095/eSCL, eSCL "Kyocera WSD" = http://192\.168\.1\.102:5358/WSDScanner, WSD "Device I do not want to see" = disable .fi .IP "" 0 .P The \fB[devices]\fR section contains all manually configured devices, one line per device, and each line contains a device name on a left side of equation and device URL on a rights side, followed by protocol (eSCL or WSD)\. If protocol is omitted, eSCL is assumed\. You may also disable particular device by using the \fBdisable\fR keyword instead of URL\. .P In addition, you can manually configure a device by directly passing its URL in a device name without adding it to the configuration file\. This takes the format \fBprotocol:Device Name:URL\fR\. The examples above could be written as \fBescl:Kyocera eSCL:http://192\.168\.1\.102:9095/eSCL\fR and \fBwsd:Kyocera WSD:http://192\.168\.1\.102:5358/WSDScanner\fR\. .P To figure out URLs of available devices, the simplest way is to run the supplied \fBairscan\-discover(1)\fR tool on a computer connected with scanner to the same LAN segment\. On success, this program will dump to its standard output a list of discovered devices in a format suitable for inclusion into the configuration file\. .P If running \fBairscan\-discover(1)\fR on the same LAN segment as a scanner is not possible, you will have to follow a hard way\. Your administrator must know device IP address, consult your device manual for the eSCL port, and the URL path component most likely is the "/eSCL", though on some devices it may differ\. Discovering WSD URLs doing this way is much harder, because it is very difficult to guess TCP port and URL path, that in a case of eSCL\. .P For eSCL devices, the URL can also use the unix:// scheme, such as unix://scanner\.sock/eSCL\. The "host" from the URL is a file name that will be searched for in the directory specified by socket_dir (see below)\. When connecting to the scanner, all traffic will be sent to the specified UNIX socket instead of a TCP connection\. .P By default, sane\-airscan treats all scanners as remote devices\. This can be undesirable, if configuring a proxy that translates from eSCL/WSD to the SANE protocol, as \fBsaned(8)\fR ignores any device that isn't attached locally\. By setting \fBpretend\-local = true\fR, sane\-airscan can make its devices accessible to the SANE network daemon\. .SH "CONFIGURATION OPTIONS" Miscellaneous options all goes to the \fB[options]\fR section\. Currently the following options are supported: .IP "" 4 .nf [options] ; If there are a lot of scanners around and you are only ; interested in few of them, disable auto discovery and ; configure scanners manually\. discovery = enable | disable ; Choose what SANE apps will show in a list of devices: ; scanner network name (the default) or hardware model name\. model = network | hardware ; If device supports both eSCL and WSD protocol, sane\-airscan ; may either choose the "best" protocol automatically, or ; expose all variants for user, allowing manual protocol selection\. ; The default is "auto"\. protocol = auto | manual ; Discovery of WSD devices may be "fast" or "full"\. The "fast" ; mode works as fast as DNS\-SD discovery, but in some cases ; may be unreliable\. The "full" mode is slow and reliable\. ; It is also possible to disable automatic discovery ; of WSD devices\. The default is "fast"\. ws\-discovery = fast | full | off ; Scanners that use the unix:// schema in their URL can only ; specify a socket name (not a full path)\. The name will be ; searched for in the directory specified here\. ; The default is /var/run\. socket_dir = /path/to/directory ; Configure whether eSCL and WSD devices should be treated as ; if they were attached locally\. The default behavior considers ; them as remote devices that are accessed over the network\. ; This option has to be changed when exporting a scanner through ; saned\. The default is "false" pretend\-local = false | true .fi .IP "" 0 .SH "BLACKLISTING DEVICES" This feature can be useful, if you are on a very big network and have a lot of devices around you, while interesting only in a few of them\. .IP "" 4 .nf [blacklist] model = "Xerox*" ; blacklist by model name name = "HP*" ; blacklist by network name ip = 192\.168\.0\.1 ; blacklist by address ip = 192\.168\.0\.0/24 ; blacklist the whole subnet .fi .IP "" 0 .P Network names come from DNS\-SD, WS\-Discovery doesn't provide this information\. For filtering by network name to work, Avahi must be enabled and device must be discoverable via DNS\-SD (not necessarily as a scanner, it's enough if WSD scanner is discoverable as a printer via DNS\-SD)\. .P Blacklisting only affects automatic discovery, and doesn't affect manually configured devices\. .SH "DEBUGGING" sane\-airscan provides very good instrumentation for troubleshooting without physical access to the problemmatic device\. .P Debugging facilities can be controlled using the \fB[debug]\fR section of the configuration file: .IP "" 4 .nf [debug] ; Enable or disable console logging enable = false | true ; Enable protocol trace and configure output directory ; 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 ; Hex dump all traffic to the trace file (very verbose!) hexdump = false | true .fi .IP "" 0 .SH "FILES" .TP \fB/etc/sane\.d/airscan\.conf\fR, \fB/etc/sane\.d/airscan\.d/*\fR The backend configuration files .TP \fB/usr/LIBDIR/sane/libsane\-airscan\.so\fR The shared library implementing this backend .SH "ENVIRONMENT" .TP \fBSANE_DEBUG_AIRSCAN\fR This variable if set to \fBtrue\fR or non\-zero numerical value, enables debug messages, that are printed to stderr .TP \fBSANE_CONFIG_DIR\fR This variable alters the search path for configuration files\. This is a colon\-separated list of directories\. These directories are searched for the airscan\.conf configuration file and for the airscan\.d subdirectory, before the standard path (/etc/sane\.d) is searched\. .TP \fBSANE_AIRSCAN_DEVICE\fR This variable, if set, overrides all devices, manually configured in the log files and disables auto discovery\. .IP It consists of three parameters, delimited by the colons (\fB:\fR): .IP \fB"PROTO:DEVICE NAME:URL"\fR .IP Where: .IP \- \fBPROTO\fR is either \fBescl\fR or \fBwsd\fR\. .br \- \fBDEVICE NAME\fR will appear in the list of devices\. .br \- \fBURL\fR is the device URL, using \fBhttp:\fR or \fBhttps:\fR schemes\. .br .IP Examples: .IP \fB"escl:Kyocera eSCL:http://192\.168\.1\.102:9095/eSCL"\fR (eSCL) .br \fB"wsd:Kyocera WSD:http://192\.168\.1\.102:5358/WSDScanner"\fR (WSD) .br .IP The primary purpose of this variable is the automated testing of the \fBsane\-airscan\fR backend\. .SH "BUGS AND SUPPORT" If you have found a bug, please file a GitHub issue on a GitHub project page: \fBhttps://github\.com/alexpevzner/sane\-airscan\fR .SH "SEE ALSO" \fBsane(7), saned (8), scanimage(1), xsane(1), airscan\-discover(1)\fR .SH "AUTHOR" Alexander Pevzner sane-airscan-0.99.35/sane-airscan.5.md000066400000000000000000000207661500411437100173050ustar00rootroot00000000000000sane-airscan(5) -- SANE backend for AirScan (eSCL) and WSD scanners and MFP =========================================================================== ## DESCRIPTION The `sane-airscan` is the universal backend for "driverless" document scanning. Currently it supports two protocols: 1. eSCL, also known as AirScan or AirPrint scan 2. WSD, also known as WS-Scan ## CONFIGURATION The sane-airscan loads its configuration files from the following places: 1. /etc/sane.d/airscan.conf 2. /etc/sane.d/airscan.d/* The configuration file syntax is very similar to the .INI file syntax. It consist of sections, each section contains some variables. Comments are started from # or ; characters and continies until end of line # This is a comment [section 1] variable 1 = value 1 ; and another comment variable 2 = value 2 Leading and trailing spaces of variable name and value are striped. If you want to preserve them, put name or value into quotes ("like this"). ## CONFIGURATION OF DEVICES If scanner and computer are connected to the same LAN segment, everything expected to "just work" out of box, without any need of manual configuration. However, in some cases manual configuration can be useful. For example: 1. If computer and scanner are connected via IP router 2. There are a lot of devices on a corporate network, but only few of them are interesting 3. Automatic discovery works unreliable To manually configure a device, add the following section to the configuration file: [devices] "Kyocera eSCL" = http://192.168.1.102:9095/eSCL, eSCL "Kyocera WSD" = http://192.168.1.102:5358/WSDScanner, WSD "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 device URL on a rights side, followed by protocol (eSCL or WSD). If protocol is omitted, eSCL is assumed. You may also disable particular device by using the `disable` keyword instead of URL. In addition, you can manually configure a device by directly passing its URL in a device name without adding it to the configuration file. This takes the format `protocol:Device Name:URL`. The examples above could be written as `escl:Kyocera eSCL:http://192.168.1.102:9095/eSCL` and `wsd:Kyocera WSD:http://192.168.1.102:5358/WSDScanner`. To figure out URLs of available devices, the simplest way is to run the supplied `airscan-discover(1)` tool on a computer connected with scanner to the same LAN segment. On success, this program will dump to its standard output a list of discovered devices in a format suitable for inclusion into the configuration file. If running `airscan-discover(1)` on the same LAN segment as a scanner is not possible, you will have to follow a hard way. Your administrator must know device IP address, consult your device manual for the eSCL port, and the URL path component most likely is the "/eSCL", though on some devices it may differ. Discovering WSD URLs doing this way is much harder, because it is very difficult to guess TCP port and URL path, that in a case of eSCL. For eSCL devices, the URL can also use the unix:// scheme, such as unix://scanner.sock/eSCL. The "host" from the URL is a file name that will be searched for in the directory specified by socket_dir (see below). When connecting to the scanner, all traffic will be sent to the specified UNIX socket instead of a TCP connection. By default, sane-airscan treats all scanners as remote devices. This can be undesirable, if configuring a proxy that translates from eSCL/WSD to the SANE protocol, as `saned(8)` ignores any device that isn't attached locally. By setting `pretend-local = true`, sane-airscan can make its devices accessible to the SANE network daemon. ## CONFIGURATION OPTIONS Miscellaneous options all goes to the ``[options]`` section. Currently the following options are supported: [options] ; If there are a lot of scanners around and you are only ; interested in few of them, disable auto discovery and ; configure scanners manually. discovery = enable | disable ; Choose what SANE apps will show in a list of devices: ; scanner network name (the default) or hardware model name. model = network | hardware ; If device supports both eSCL and WSD protocol, sane-airscan ; may either choose the "best" protocol automatically, or ; expose all variants for user, allowing manual protocol selection. ; The default is "auto". protocol = auto | manual ; Discovery of WSD devices may be "fast" or "full". The "fast" ; mode works as fast as DNS-SD discovery, but in some cases ; may be unreliable. The "full" mode is slow and reliable. ; It is also possible to disable automatic discovery ; of WSD devices. The default is "fast". ws-discovery = fast | full | off ; Scanners that use the unix:// schema in their URL can only ; specify a socket name (not a full path). The name will be ; searched for in the directory specified here. ; The default is /var/run. socket_dir = /path/to/directory ; Configure whether eSCL and WSD devices should be treated as ; if they were attached locally. The default behavior considers ; them as remote devices that are accessed over the network. ; This option has to be changed when exporting a scanner through ; saned. The default is "false" pretend-local = false | true ## BLACKLISTING DEVICES This feature can be useful, if you are on a very big network and have a lot of devices around you, while interesting only in a few of them. [blacklist] model = "Xerox*" ; blacklist by model name name = "HP*" ; blacklist by network name ip = 192.168.0.1 ; blacklist by address ip = 192.168.0.0/24 ; blacklist the whole subnet Network names come from DNS-SD, WS-Discovery doesn't provide this information. For filtering by network name to work, Avahi must be enabled and device must be discoverable via DNS-SD (not necessarily as a scanner, it's enough if WSD scanner is discoverable as a printer via DNS-SD). Blacklisting only affects automatic discovery, and doesn't affect manually configured devices. ## DEBUGGING sane-airscan provides very good instrumentation for troubleshooting without physical access to the problemmatic device. Debugging facilities can be controlled using the ``[debug]`` section of the configuration file: [debug] ; Enable or disable console logging enable = false | true ; Enable protocol trace and configure output directory ; 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 ; Hex dump all traffic to the trace file (very verbose!) hexdump = false | true ## FILES * `/etc/sane.d/airscan.conf`, `/etc/sane.d/airscan.d/*`: The backend configuration files * `/usr/LIBDIR/sane/libsane-airscan.so`: The shared library implementing this backend ## ENVIRONMENT * `SANE_DEBUG_AIRSCAN`: This variable if set to `true` or non-zero numerical value, enables debug messages, that are printed to stderr * `SANE_CONFIG_DIR`: This variable alters the search path for configuration files. This is a colon-separated list of directories. These directories are searched for the airscan.conf configuration file and for the airscan.d subdirectory, before the standard path (/etc/sane.d) is searched. * `SANE_AIRSCAN_DEVICE`: This variable, if set, overrides all devices, manually configured in the log files and disables auto discovery. It consists of three parameters, delimited by the colons (`:`): `"PROTO:DEVICE NAME:URL"` Where: \- `PROTO` is either `escl` or `wsd`.
\- `DEVICE NAME` will appear in the list of devices.
\- `URL` is the device URL, using `http:` or `https:` schemes.
Examples: `"escl:Kyocera eSCL:http://192.168.1.102:9095/eSCL"` (eSCL)
`"wsd:Kyocera WSD:http://192.168.1.102:5358/WSDScanner"` (WSD)
The primary purpose of this variable is the automated testing of the `sane-airscan` backend. ## BUGS AND SUPPORT If you have found a bug, please file a GitHub issue on a GitHub project page: **https://github.com/alexpevzner/sane-airscan** ## SEE ALSO **sane(7), saned (8), scanimage(1), xsane(1), airscan-discover(1)** ## AUTHOR Alexander Pevzner # vim:ts=8:sw=4:et sane-airscan-0.99.35/sane_strstatus.c000066400000000000000000000066641500411437100175030ustar00rootroot00000000000000/* sane - Scanner Access Now Easy. Copyright (C) 1996, 1997 David Mosberger-Tang and Andreas Beck This file is part of the SANE package. 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 the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 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. As a special exception, the authors of SANE give permission for additional uses of the libraries contained in this release of SANE. The exception is that, if you link a SANE library with other files to produce an executable, this does not by itself cause the resulting executable to be covered by the GNU General Public License. Your use of that executable is in no way restricted on account of linking the SANE library code into it. This exception does not, however, invalidate any other reasons why the executable file might be covered by the GNU General Public License. If you submit changes to SANE to the maintainers to be included in a subsequent release, you agree by submitting the changes that those changes may be distributed with this exception intact. If you write modifications of your own for SANE, it is your choice whether to permit this exception to apply to your modifications. If you do not wish that, delete this exception notice. This file implements the backend-independent parts of SANE. */ #include #include #ifndef SANE_I18N #define SANE_I18N(text) text #endif SANE_String_Const sane_strstatus (SANE_Status status) { static char buf[80]; switch (status) { case SANE_STATUS_GOOD: return SANE_I18N("Success"); case SANE_STATUS_UNSUPPORTED: return SANE_I18N("Operation not supported"); case SANE_STATUS_CANCELLED: return SANE_I18N("Operation was canceled"); case SANE_STATUS_DEVICE_BUSY: return SANE_I18N("Device busy"); case SANE_STATUS_INVAL: return SANE_I18N("Invalid argument"); case SANE_STATUS_EOF: return SANE_I18N("End of file reached"); case SANE_STATUS_JAMMED: return SANE_I18N("Document feeder jammed"); case SANE_STATUS_NO_DOCS: return SANE_I18N("Document feeder out of documents"); case SANE_STATUS_COVER_OPEN: return SANE_I18N("Scanner cover is open"); case SANE_STATUS_IO_ERROR: return SANE_I18N("Error during device I/O"); case SANE_STATUS_NO_MEM: return SANE_I18N("Out of memory"); case SANE_STATUS_ACCESS_DENIED: return SANE_I18N("Access to resource has been denied"); #ifdef SANE_STATUS_WARMING_UP case SANE_STATUS_WARMING_UP: return SANE_I18N("Lamp not ready, please retry"); #endif #ifdef SANE_STATUS_HW_LOCKED case SANE_STATUS_HW_LOCKED: return SANE_I18N("Scanner mechanism locked for transport"); #endif default: /* non-reentrant, but better than nothing */ sprintf (buf, "Unknown SANE status code %d", status); return buf; } } sane-airscan-0.99.35/test-decode.c000066400000000000000000000126731500411437100166160ustar00rootroot00000000000000/* sane-airscan image decoders test * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions */ #include "airscan.h" #include #include #include #include #include #include /* save_file represents a PNG output file, used for * saving decoded image */ typedef struct { const char *name; /* Output file name */ FILE *fp; /* Output file handle */ png_struct *png_ptr; /* Underlying libpng encoder */ png_info *info_ptr; /* libpng info struct */ } save_file; /* 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); } /* libpng write callback */ void png_write_fn (png_struct *png_ptr, png_bytep data, size_t size) { save_file *file = png_get_io_ptr(png_ptr); if (size != fwrite(data, 1, size, file->fp)) { die("%s: %s", file->name, strerror(errno)); } } /* libpng error callback */ void png_error_fn (png_struct *png_ptr, const char *message) { save_file *file = png_get_error_ptr(png_ptr); die("%s: %s", file->name, message); } /* Open the save_file */ save_file* save_open (const char *name, const SANE_Parameters *params) { save_file *save = mem_new(save_file, 1); int color_type; save->name = str_dup(name); save->fp = fopen(name, "wb"); if (save->fp == NULL) { die("%s: %s", name, strerror(errno)); } save->png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (save->png_ptr == NULL) { die("%s: png_create_write_struct() failed", name); } png_set_write_fn(save->png_ptr, save, png_write_fn, NULL); png_set_error_fn(save->png_ptr, save, png_error_fn, NULL); save->info_ptr = png_create_info_struct(save->png_ptr); if (save->info_ptr == NULL) { die("%s: png_create_info_struct() failed", name); } if (params->format == SANE_FRAME_GRAY) { color_type = PNG_COLOR_TYPE_GRAY; } else { color_type = PNG_COLOR_TYPE_RGB; } png_set_IHDR(save->png_ptr, save->info_ptr, params->pixels_per_line, params->lines, params->depth, color_type, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); png_set_sRGB(save->png_ptr, save->info_ptr, PNG_sRGB_INTENT_PERCEPTUAL); png_write_info(save->png_ptr, save->info_ptr); return save; } /* Close the save file */ void save_close (save_file *save) { png_write_end(save->png_ptr, NULL); png_destroy_write_struct(&save->png_ptr, &save->info_ptr); fclose(save->fp); mem_free((char*) save->name); mem_free(save); } /* Write a row of image data */ void save_write (save_file *save, void *data) { png_write_row(save->png_ptr, data); } /* The main function */ int main (int argc, char **argv) { const char *file, *ext; ID_FORMAT format; image_decoder *decoders[NUM_ID_FORMAT]; image_decoder *decoder = NULL; FILE *fp; long size; int rc; void *data, *line; error err; SANE_Parameters params; int i; save_file *save; /* Parse command-line arguments */ if (argc != 2) { die( "test-decode - decodes image file and writes result to decoded.png\n" "usage: %s image.file", argv[0] ); } file = argv[1]; ext = strrchr(file, '.'); ext = ext ? ext + 1 : ""; /* Load the file */ fp = fopen(file, "rb"); if (fp == NULL) { die("%s: %s", file, strerror(errno)); } rc = fseek(fp, 0, SEEK_END); if (rc < 0) { die("%s: %s", file, strerror(errno)); } size = ftell(fp); if (size < 0) { die("%s: %s", file, strerror(errno)); } rc = fseek(fp, 0, SEEK_SET); if (rc < 0) { die("%s: %s", file, strerror(errno)); } data = mem_new(char, size); if ((size_t) size != fread(data, 1, size, fp)) { die("%s: read error", file); } fclose(fp); /* Create decoder */ format = image_format_detect(data, size); if (format == ID_FORMAT_UNKNOWN) { die("Unknown image format"); } image_decoder_create_all(decoders); decoder = decoders[format]; if (decoder == NULL) { die("Unsupported image format %s", id_format_short_name(format)); } /* Decode the image */ err = image_decoder_begin(decoder, data, size); if (err != NULL) { die("%s", err); } image_decoder_get_params(decoder, ¶ms); printf("format: %s\n", image_content_type(decoder)); printf("width: %d\n", params.pixels_per_line); printf("height: %d\n", params.lines); printf("bytes/line: %d\n", params.bytes_per_line); printf("bytes/pixel: %d\n", params.bytes_per_line / params.pixels_per_line); save = save_open("decoded.png", ¶ms); line = mem_new(char, params.bytes_per_line); for (i = 0; i < params.lines; i ++) { err = image_decoder_read_line(decoder, line); if (err != NULL) { die("line %d: %s", i, err); } save_write(save, line); } mem_free(line); mem_free(data); save_close(save); image_decoder_free_all(decoders); return 0; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/test-devcaps.c000066400000000000000000000044231500411437100170120ustar00rootroot00000000000000/* sane-airscan device capabilities parser test * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions */ #include "airscan.h" #include #include #include #include #include /* 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); } /* The main function */ int main (int argc, char **argv) { char *mode, *file; FILE *fp; void *data; long size; proto_handler *proto; int rc; error err; devcaps caps; /* Parse command-line arguments */ if (argc != 3) { die( "test-devcaps - decode and print device capabilities\n" "usage: %s [-escl|-wsd] file.xml", argv[0] ); } mode = argv[1]; file = argv[2]; if (!strcmp(mode, "-escl")) { proto = proto_handler_escl_new(); } else if (!strcmp(mode, "-wsd")) { proto = proto_handler_wsd_new(); } else { die("%s: unknown protocol", mode); } /* Load the file */ fp = fopen(file, "rb"); if (fp == NULL) { die("%s: %s", file, strerror(errno)); } rc = fseek(fp, 0, SEEK_END); if (rc < 0) { die("%s: %s", file, strerror(errno)); } size = ftell(fp); if (size < 0) { die("%s: %s", file, strerror(errno)); } rc = fseek(fp, 0, SEEK_SET); if (rc < 0) { die("%s: %s", file, strerror(errno)); } data = mem_new(char, size); if ((size_t) size != fread(data, 1, size, fp)) { die("%s: read error", file); } fclose(fp); /* Initialize logging */ conf.dbg_enabled = true; log_init(); log_configure(); /* Decode device capabilities */ memset(&caps, 0, sizeof(caps)); devcaps_init(&caps); err = proto->test_decode_devcaps(proto, data, size, &caps); if (err != NULL) { die("error: %s", ESTRING(err)); } devcaps_dump(NULL, &caps, false); /* Cleanup and exit */ proto_handler_free(proto); mem_free(data); return 0; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/test-multipart.c000066400000000000000000000051721500411437100174100ustar00rootroot00000000000000/* sane-airscan HTTP multipart decoder test * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions */ #include "airscan.h" #include #include #include #include #include /* 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); } /* The main function */ int main (int argc, char **argv) { const char *file; FILE *fp; long size; int rc; void *data; error err; http_client *client; http_uri *uri; http_query *q; int i, cnt; /* Parse command-line arguments */ if (argc != 2) { die("usage: %s file", argv[0]); } file = argv[1]; /* Load the file */ fp = fopen(file, "rb"); if (fp == NULL) { die("%s: %s", file, strerror(errno)); } rc = fseek(fp, 0, SEEK_END); if (rc < 0) { die("%s: %s", file, strerror(errno)); } size = ftell(fp); if (size < 0) { die("%s: %s", file, strerror(errno)); } rc = fseek(fp, 0, SEEK_SET); if (rc < 0) { die("%s: %s", file, strerror(errno)); } data = mem_new(char, size); if ((size_t) size != fread(data, 1, size, fp)) { die("%s: read error", file); } fclose(fp); /* Initialize logging */ log_init(); conf.dbg_enabled = true; log_configure(); /* Decode the image */ client = http_client_new(NULL, NULL); uri = http_uri_new("http://localhost", false); q = http_query_new(client, uri, "GET", NULL, NULL); err = http_query_test_decode_response(q, data, size); if (err != NULL) { die("%s", ESTRING(err)); } cnt = http_query_get_mp_response_count(q); if (cnt > 0) { printf("Part Size Saved As Content-Type\n"); printf("==== ==== ============= ============\n"); for (i = 0; i < cnt; i ++) { http_data *data = http_query_get_mp_response_data(q, i); char name[64]; FILE *fp; sprintf(name, "%8.8d.part", i); printf("%3d %8d %s %s\n", i, (int) data->size, name, data->content_type); fp = fopen(name, "wb"); if (fp == NULL) { die("%s: %s", strerror(errno)); } fwrite(data->bytes, 1, data->size, fp); fclose(fp); } } return 0; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/test-uri.c000066400000000000000000000124721500411437100161670ustar00rootroot00000000000000/* http_uri test * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions */ #include "airscan.h" #include #include static void fail (const char *fmt, ...) { va_list ap; va_start(ap, fmt); vprintf(fmt, ap); putchar('\n'); exit(1); } /* Test URI parser */ static void test_parse (const char *s, bool must_parse) { http_uri *uri = http_uri_new(s, false); if (uri == NULL && must_parse) { fail("URI parse failed: %s", s); } else if (uri != NULL && !must_parse) { fail("URI parse must fail: %s", s); } http_uri_free(uri); } /* Test http_uri_addr */ static void test_addr (const char *s, const char *expected) { http_uri *uri = http_uri_new(s, false); const struct sockaddr *sa; ip_straddr straddr; if (uri == NULL) { fail("URI parse failed: %s", s); } sa = http_uri_addr(uri); if (sa == NULL) { fail("URI addr not understood: %s", s); } straddr = ip_straddr_from_sockaddr(sa, true); if (strcmp(straddr.text, expected)) { fail("URI addr %s != %s: %s", straddr.text, expected, s); } http_uri_free(uri); } /* Test http_uri_get_path */ static void test_get_path (const char *s, const char *expected) { http_uri *uri = http_uri_new(s, false); const char *path; if (uri == NULL) { fail("URI parse failed: %s", s); } path = http_uri_get_path(uri); if (strcmp(path, expected)) { fail("URI path %s != %s: %s", path, expected, s); } http_uri_free(uri); } /* Test http_uri_fix_end_slash */ static void test_fix_end_slash (const char *s) { http_uri *uri = http_uri_new(s, false); const char *path; if (uri == NULL) { fail("URI parse failed: %s", s); } http_uri_fix_end_slash(uri); path = http_uri_get_path(uri); if (!str_has_suffix(path, "/")) { fail("fix_end_slash failed: %s, path=%s", s, path); } http_uri_free(uri); } /* Test http_uri_set_path */ static void test_set_path (const char *path, const char *expected) { http_uri *uri = http_uri_new("http://user@host:123/?q#frag", false); const char *path2; http_uri_set_path(uri, path); path2 = http_uri_get_path(uri); if (strcmp(path, path2)) { fail("URI set path: %s != %s", path, path2); } if (strcmp(http_uri_str(uri), expected)) { fail("URI set path: %s != %s", http_uri_str(uri), expected); } http_uri_free(uri); } /* Test http_uri_new_relative */ static void test_relative (const char *base, const char *ref, const char *expected) { http_uri *uri_base, *uri_rel; const char *s; uri_base = http_uri_new(base, false); if (uri_base == NULL) { fail("URI parse failed: %s", base); } uri_rel = http_uri_new_relative(uri_base, ref, false, false); if (uri_rel == NULL) { fail("URI base=%s ref=%s: ref parse failed", base, ref); } s = http_uri_str(uri_rel); if (strcmp(s, expected)) { fail("URI base=%s ref=%s: %s != %s", base, ref, s, expected); } http_uri_free(uri_base); http_uri_free(uri_rel); } /* The main function */ int main (void) { log_init(); test_parse("http://1.2.3.4/", true); test_parse("http:/1.2.3.4", false); test_parse("http:1.2.3.4", false); test_parse("http://1.2.3.4:8888/", true); test_parse("http://1.2.3.4:8888:9999/", false); test_parse("/", false); test_parse("", false); test_parse("http://[::1]/", true); test_parse("http://[::1%255]/", true); test_parse("http://[::1/", false); test_parse("http://[::1]:8888/", true); test_parse("http://[::1]:8888:9999/", false); test_parse("http://[A%2525]//MM", false); test_parse("http://[A%2525]//MM/", false); test_parse("http://[1%255]/a", false); test_fix_end_slash("http://[::1%255]/a"); test_addr("http://1.2.3.4/", "1.2.3.4:80"); test_addr("http://[::1]/", "[::1]:80"); test_addr("https://1.2.3.4/", "1.2.3.4:443"); test_addr("https://[::1]/", "[::1]:443"); test_addr("http://1.2.3.4:1234/", "1.2.3.4:1234"); test_addr("http://[::1]:1234/", "[::1]:1234"); test_get_path("http://1.2.3.4/", "/"); test_get_path("http://1.2.3.4/xxx", "/xxx"); test_get_path("http://1.2.3.4", ""); test_set_path("/xxx", "http://user@host:123/xxx?q#frag"); test_relative("http://host/", "//x/path", "http://x/path"); test_relative("http://host/", "/path", "http://host/path"); test_relative("http://host/", "noroot", "http://host/noroot"); test_relative("http://host/", "noroot/xxx", "http://host/noroot/xxx"); test_relative("http://host/xxx/", "noroot", "http://host/xxx/noroot"); test_relative("http://host/xxx", "noroot", "http://host/noroot"); test_relative("http://host/", "/a/b/c/./../../g", "http://host/a/g"); test_relative("http://[::1]:8080/eSCL/", "XXX", "http://[::1]:8080/eSCL/XXX"); return 0; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/test-zeroconf.c000066400000000000000000000453451500411437100172220ustar00rootroot00000000000000/* 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 #include #include #include #include #include #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 */ sane-airscan-0.99.35/test.c000066400000000000000000000041751500411437100153730ustar00rootroot00000000000000/* sane-airscan backend test * * Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions */ #include #include #include #include #include #include "airscan.h" SANE_Handle handle; void sigint_handler (int unused) { (void) unused; if (handle != NULL) { sane_cancel (handle); } } void check (SANE_Status status, const char *operation) { if (status != SANE_STATUS_GOOD) { printf("%s: %s\n", operation, sane_strstatus(status)); if (handle != NULL) { sane_close(handle); } exit(1); } } #define TRY(func, args...) \ do{ \ printf("%s: calling\n", #func); \ SANE_Status s = func(args); \ printf("%s: done, status=%s\n", #func, sane_strstatus(s)); \ check(s, #func); \ } while(0) void scan_test (void) { SANE_Status s; SANE_Byte buf[65536]; int len, count = 0; TRY(sane_start,handle); //sane_cancel (handle); for (;;) { s = sane_read(handle, buf, sizeof(buf), &len); if (s != SANE_STATUS_GOOD) { break; } count += len; } if (count != 0) { printf("%d bytes of data received\n", count); } //sane_cancel (handle); } int main (void) { SANE_Parameters params; struct sigaction act = { .sa_handler = sigint_handler, }; sigaction(SIGINT, &act, NULL); TRY(sane_init, NULL, NULL); TRY(sane_open, "", &handle); //TRY(sane_control_option, handle, OPT_SCAN_SOURCE, SANE_ACTION_SET_VALUE, OPTVAL_SOURCE_ADF_SIMPLEX, NULL); TRY(sane_get_parameters, handle, ¶ms); printf("image size: %dx%d\n", params.pixels_per_line, params.lines); scan_test(); // scan_test(); sane_close(handle); //getchar(); sane_exit(); return 0; } /* vim:ts=8:sw=4:et */ sane-airscan-0.99.35/testdata/000077500000000000000000000000001500411437100160525ustar00rootroot00000000000000sane-airscan-0.99.35/testdata/test-zeroconf-1.cfg000066400000000000000000000007431500411437100214770ustar00rootroot00000000000000[add] method = USCAN_TCP name = "device 1" model = "model 1" uuid = 00000000-0000-0000-0000-000000000001 ifindex = 1 endpoint = http://192.168.0.1/eSCL [add] method = WSD model = "model 1" uuid = 00000000-0000-0000-0000-000000000001 ifindex = 1 endpoint = http://192.168.0.1/wsd [expect] "device 1" = escl, http://192.168.0.1/eSCL "device 1" = wsd, http://192.168.0.1/wsd [merged] "device 1" = escl, http://192.168.0.1/eSCL sane-airscan-0.99.35/testdata/test-zeroconf-2.cfg000066400000000000000000000007431500411437100215000ustar00rootroot00000000000000[add] method = USCAN_TCP name = "device 1" model = "model 1" uuid = 00000000-0000-0000-0000-000000000001 ifindex = 1 endpoint = http://192.168.0.1/eSCL [add] method = WSD model = "model 1" uuid = 00000000-0000-0000-0000-000000000002 ifindex = 1 endpoint = http://192.168.0.1/wsd [expect] "device 1" = escl, http://192.168.0.1/eSCL "device 1" = wsd, http://192.168.0.1/wsd [merged] "device 1" = escl, http://192.168.0.1/eSCL sane-airscan-0.99.35/testdata/test-zeroconf-3.cfg000066400000000000000000000007431500411437100215010ustar00rootroot00000000000000[add] method = WSD model = "model 1" uuid = 00000000-0000-0000-0000-000000000001 ifindex = 1 endpoint = http://192.168.0.1/wsd [add] method = USCAN_TCP name = "device 1" model = "model 1" uuid = 00000000-0000-0000-0000-000000000001 ifindex = 1 endpoint = http://192.168.0.1/eSCL [expect] "device 1" = escl, http://192.168.0.1/eSCL "device 1" = wsd, http://192.168.0.1/wsd [merged] "device 1" = escl, http://192.168.0.1/eSCL sane-airscan-0.99.35/testdata/test-zeroconf-4.cfg000066400000000000000000000007531500411437100215030ustar00rootroot00000000000000[add] method = USCAN_TCP name = "device 1" model = "model 1" uuid = 00000000-0000-0000-0000-000000000001 ifindex = 1 endpoint = http://192.168.0.1/eSCL [add] method = WSD model = "model 1" uuid = 00000000-0000-0000-0000-000000000001 ifindex = 1 endpoint = http://192.168.0.1/wsd [del] method = WSD model = "model 1" uuid = 00000000-0000-0000-0000-000000000001 ifindex = 1 [expect] "device 1" = escl, http://192.168.0.1/eSCL sane-airscan-0.99.35/testdata/test-zeroconf-5.cfg000066400000000000000000000010321500411437100214730ustar00rootroot00000000000000[add] method = USCAN_TCP name = "device 1" model = "model 1" uuid = 00000000-0000-0000-0000-000000000001 ifindex = 1 endpoint = http://192.168.0.1/eSCL [add] method = WSD model = "model 1" uuid = 00000000-0000-0000-0000-000000000001 ifindex = 2 endpoint = http://192.168.0.2/wsd [expect] "device 1" = escl, http://192.168.0.1/eSCL ; Not merged with "device 1" because they are on ; different interfaces and have no common IP addresses "model 1" = wsd, http://192.168.0.2/wsd sane-airscan-0.99.35/testdata/test-zeroconf-6.cfg000066400000000000000000000006631500411437100215050ustar00rootroot00000000000000[add] method = USCAN_TCP name = "device 1" model = "model 1" uuid = 00000000-0000-0000-0000-000000000001 ifindex = 1 endpoint = http://192.168.0.1/eSCL [add] method = USCAN_TCP name = "device 1" model = "model 1" uuid = 00000000-0000-0000-0000-000000000001 ifindex = 2 endpoint = http://192.168.0.2/eSCL [expect] "device 1" = escl, http://192.168.0.1/eSCL, http://192.168.0.2/eSCL sane-airscan-0.99.35/testdata/test-zeroconf-7.cfg000066400000000000000000000016251500411437100215050ustar00rootroot00000000000000[add] method = WSD model = "model 1" uuid = 00000000-0000-0000-0000-000000000001 ifindex = 1 endpoint = http://192.168.1.102:5358/WSDScanner endpoint = http://[fe80::217:c8ff:fe7b:6a91%254]:5358/WSDScanner [add] method = USCANS_TCP name = "device 1" model = "model 1" uuid = 00000000-0000-0000-0000-000000000001 ifindex = 1 endpoint = https://192.168.1.102:9096/eSCL/ [add] method = USCAN_TCP name = "device 1" model = "model 1" uuid = 00000000-0000-0000-0000-000000000001 ifindex = 1 endpoint = https://192.168.1.102:9095/eSCL/ [expect] "device 1" = escl, https://192.168.1.102:9095/eSCL/, https://192.168.1.102:9096/eSCL/ "device 1" = wsd, http://192.168.1.102:5358/WSDScanner, http://[fe80::217:c8ff:fe7b:6a91%254]:5358/WSDScanner [merged] "device 1" = escl, https://192.168.1.102:9095/eSCL/, https://192.168.1.102:9096/eSCL/